Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/render/postProcessing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
22 changes: 20 additions & 2 deletions packages/shell/src/environment/VolumetricClouds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
`;
Expand Down Expand Up @@ -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,
Expand All @@ -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;
});

Expand Down
Loading