diff --git a/CHANGELOG.md b/CHANGELOG.md index e4342d7a..3644e30b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ between (`--json` for structured output). ### Fixed +- Volumetric clouds no longer cut through distant buildings: the proxy dome writes per-fragment depth at the raymarch's first cloud hit, so geometry between the camera and the cloud slab occludes the clouds instead of the dome radius deciding. `STUDIO_STAGE_POST` ambient occlusion is softer (radius 1.1, intensity 1.5) so tower silhouettes stop streaking. + - Separate SDK publication validation from external Games quality checks while retaining the full local and main-branch gates. - Correct foot-IK endpoint recomputation after hip rotation; verify reachable targets and clamping without changing bone lengths. diff --git a/packages/core/src/render/postProcessing.ts b/packages/core/src/render/postProcessing.ts index 1672d28d..40acdc60 100644 --- a/packages/core/src/render/postProcessing.ts +++ b/packages/core/src/render/postProcessing.ts @@ -101,7 +101,7 @@ export const STUDIO_STAGE_POST: PostProcessingConfig = { toneMapping: "aces", exposure: 1.05, bloom: { strength: 0.28, radius: 0.6, threshold: 0.8 }, - ao: { radius: 1.4, intensity: 2.2, distanceFalloff: 3, blend: 1 }, + ao: { radius: 1.1, intensity: 1.5, distanceFalloff: 3, blend: 1 }, grade: { lift: [0.008, 0.008, 0.014], gain: [1.05, 1.02, 0.98], diff --git a/packages/shell/src/environment/VolumetricClouds.tsx b/packages/shell/src/environment/VolumetricClouds.tsx index 04a4fa6e..5287dcb7 100644 --- a/packages/shell/src/environment/VolumetricClouds.tsx +++ b/packages/shell/src/environment/VolumetricClouds.tsx @@ -17,7 +17,8 @@ export interface VolumetricCloudsProps { * Camera-centered proxy dome radius. Must sit inside the default camera far plane * (`CAMERA_FRUSTUM_DEFAULTS.far` = 300) — a bigger sphere is clipped whole and renders nothing. The * raymarch works in world space against the absolute cloud slab, so this size only has to cover the - * screen (camera is always at the dome center), never bound the clouds. + * screen (camera is always at the dome center), never bound the clouds; each fragment writes its own + * depth at the first cloud hit, so the dome's radius never decides what occludes the clouds. */ const INSIDE_FRUSTUM_DOME_RADIUS = 240; @@ -57,8 +58,18 @@ uniform float uScale; uniform float uSpeed; uniform float uTime; uniform float uSeedOffset; +uniform mat4 uProjection; +uniform mat4 uView; varying vec3 vJgCloudWorldPos; +// Depth-buffer value of a world point, so the proxy dome tests against the scene at the cloud's +// own distance instead of the dome's: a tower in front of the slab entry then occludes the cloud. +float jgCloudDepthAt(vec3 worldPos){ + vec4 clip = uProjection * uView * vec4(worldPos, 1.0); + float ndc = clip.z / max(clip.w, 1e-5); + return clamp(ndc * 0.5 + 0.5, 0.0, 0.999999); +} + float jgCloudHash13(vec3 p){ p = fract(p * 0.1031 + uSeedOffset * 0.0007); p += dot(p, p.yzx + 33.33); @@ -142,11 +153,13 @@ void main() { vec3 color = vec3(0.0); float sunDot = max(dot(rayDir, normalize(uSunDirection)), 0.0); float silver = pow(sunDot, 5.0) * uSunScatter; + float tHit = -1.0; for (int i = 0; i < STEPS; i++) { vec3 pos = rayOrigin + rayDir * t; float dens = jgCloudDensity(pos); if (dens > 0.002) { + if (tHit < 0.0) tHit = t; float light = jgCloudLight(pos); float powder = 1.0 - exp(-dens * 3.0); vec3 lit = mix(uColor, uSunColor, light) * (0.55 + 0.45 * powder) + uSunColor * silver * 0.5; @@ -160,7 +173,8 @@ void main() { } float alpha = (1.0 - transmittance) * smoothstep(0.015, 0.22, rayDir.y); - if (alpha < 0.01) discard; + if (alpha < 0.01 || tHit < 0.0) discard; + gl_FragDepthEXT = jgCloudDepthAt(rayOrigin + rayDir * tHit); gl_FragColor = vec4(color, alpha); } `; @@ -194,6 +208,8 @@ export function VolumetricClouds({ rules, sunDirection }: VolumetricCloudsProps) uSpeed: { value: rules.speed }, uTime: { value: 0 }, uSeedOffset: { value: hashSeed(rules.seed) }, + uProjection: { value: new THREE.Matrix4() }, + uView: { value: new THREE.Matrix4() }, }, vertexShader: CLOUD_VERTEX_SHADER, fragmentShader: CLOUD_FRAGMENT_SHADER, @@ -213,6 +229,8 @@ export function VolumetricClouds({ rules, sunDirection }: VolumetricCloudsProps) } const uniforms = material.uniforms; (uniforms.uCameraPos!.value as THREE.Vector3).copy(state.camera.position); + (uniforms.uProjection!.value as THREE.Matrix4).copy(state.camera.projectionMatrix); + (uniforms.uView!.value as THREE.Matrix4).copy(state.camera.matrixWorldInverse); uniforms.uTime!.value = timeRef.current; });