diff --git a/src/AcDream.App/Rendering/StaticMeshRenderer.cs b/src/AcDream.App/Rendering/StaticMeshRenderer.cs index 6a2da70..135af25 100644 --- a/src/AcDream.App/Rendering/StaticMeshRenderer.cs +++ b/src/AcDream.App/Rendering/StaticMeshRenderer.cs @@ -135,6 +135,30 @@ public sealed unsafe class StaticMeshRenderer : IDisposable _gl.Enable(EnableCap.Blend); _gl.DepthMask(false); + // Phase 9.2: enable back-face culling for the translucent pass so + // closed-shell translucents (lifestone crystal, glow gems, any + // convex blended mesh) don't draw their back faces over their + // front faces in arbitrary iteration order. Without this, the + // 58 triangles of the lifestone crystal composited with an + // "inside-out" look where the user saw through one face into + // the hollow interior. With back-face culling on, back faces are + // dropped at rasterization time, front faces composite as-is, + // and depth ordering within the front-facing subset is a + // non-issue for closed convex-ish shells. Matches WorldBuilder's + // per-batch CullMode handling in + // references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ + // BaseObjectRenderManager.cs:361-365. + // + // Our fan triangulation emits pos-side polygons as + // (0, i, i+1) which is CCW in standard OpenGL conventions, so + // GL_BACK + CCW front is the correct state. Neg-side polygons + // (if any) use reversed winding and get culled here — that's a + // known limitation and matches the opaque-pass behavior since + // neg-side polys are virtually never translucent in AC content. + _gl.Enable(EnableCap.CullFace); + _gl.CullFace(TriangleFace.Back); + _gl.FrontFace(FrontFaceDirection.Ccw); + foreach (var entity in entityList) { if (entity.MeshRefs.Count == 0) @@ -191,6 +215,7 @@ public sealed unsafe class StaticMeshRenderer : IDisposable // Restore default GL state for subsequent renderers (terrain etc.). _gl.DepthMask(true); _gl.Disable(EnableCap.Blend); + _gl.Disable(EnableCap.CullFace); _gl.BindVertexArray(0); }