Skip to content

Commit a3ccb48

Browse files
zjm-metameta-codesync[bot]
authored andcommitted
feat(core): Add MinMaxSoftOcclusion mode with two-pass depth preprocessing
Summary: Add a new MinMaxSoftOcclusion occlusion mode to DepthOccludable that produces edge-aware smooth occlusion boundaries at depth discontinuities, ported from the Meta SDK's OcclusionVisualizationRenderer two-pass pipeline. The implementation adds: - MinMaxSoftOcclusion enum value in OcclusionShadersMode - DepthPreprocessingPass: a fullscreen pass per eye at depth texture resolution (~256×192) that samples a 4×4 neighborhood using bimodal clustering to compute min/max/avg depth, output as RGBA = (minAvg, maxAvg, avg-minAvg, maxAvg-minAvg) in meters - DepthPreprocessingShader: GPU-aware preprocessing shader supporting both CPU and GPU inverse-depth formats, with vectorized clustering using dot/mask operations for the second pass - MinMax occlusion branch in the material shader that reads per-view preprocessed textures, computes per-cluster fade alphas with 4% metric distance range, and interpolates with smoothstep(0.2, 0.8 ) at depth edges Key fixes applied during development: - VIEW_ID uint comparison fix for multiview compatibility - XR-safe preprocessing rendering (temporarily disables renderer.xr.enabled) - Correct GPU depth texture dimensions instead of drawing buffer fallback - Cached minMaxEntityCount to avoid per-frame entity scanning Reviewed By: felixtrz Differential Revision: D94752919 fbshipit-source-id: ccda8492e36f19ef31c9ed4d94c1a9465d78f5dc
1 parent f8f2b56 commit a3ccb48

File tree

8 files changed

+375
-33
lines changed

8 files changed

+375
-33
lines changed

examples/depth-occlusion/src/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,10 @@ export class OcclusionDemoSystem extends createSystem({
155155
movementMode: MovementMode.MoveFromTarget,
156156
});
157157
entity.addComponent(XRAnchor);
158-
entity.addComponent(DepthOccludable);
158+
entity.addComponent(DepthOccludable, {
159+
mode: OcclusionShadersMode.MinMaxSoftOcclusion,
160+
});
161+
159162

160163
return entity;
161164
}

packages/core/src/depth/depth-occludable.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const OcclusionShadersMode = {
1313
SoftOcclusion: 'SoftOcclusion',
1414
/** Hard occlusion with a single depth sample per fragment. */
1515
HardOcclusion: 'HardOcclusion',
16+
/** MinMax soft occlusion with depth preprocessing for edge-aware smooth edges. */
17+
MinMaxSoftOcclusion: 'MinMaxSoftOcclusion',
1618
};
1719

1820
/**

packages/core/src/depth/depth-sensing-system.ts

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,19 @@
66
*/
77

88
import { createSystem, Entity, Types } from '../ecs/index.js';
9-
import { Mesh, Vector2 } from '../runtime/three.js';
9+
import { type IUniform, Mesh, Texture, Vector2 } from '../runtime/three.js';
1010
import { DepthOccludable, OcclusionShadersMode } from './depth-occludable.js';
1111
import { DepthTextures } from './depth-textures.js';
12-
import type { Shader, ShaderUniforms } from './types.js';
12+
import { DepthPreprocessingPass } from './occlusion/preprocessing-pass.js';
13+
14+
type ShaderUniforms = { [uniform: string]: IUniform };
15+
16+
interface Shader {
17+
uniforms: ShaderUniforms;
18+
defines?: { [key: string]: unknown };
19+
vertexShader: string;
20+
fragmentShader: string;
21+
}
1322

1423
/**
1524
* DepthSensingSystem - Manages WebXR depth sensing and occlusion.
@@ -67,6 +76,8 @@ export class DepthSensingSystem extends createSystem(
6776
// Occlusion
6877
private entityShaderMap = new Map<Entity, Set<ShaderUniforms>>();
6978
private readonly viewportSize = new Vector2();
79+
private preprocessingPass?: DepthPreprocessingPass;
80+
private minMaxEntityCount = 0;
7081

7182
/**
7283
* Get the raw value to meters conversion factor.
@@ -98,8 +109,20 @@ export class DepthSensingSystem extends createSystem(
98109

99110
this.queries.occludables.subscribe('qualify', (entity: Entity) => {
100111
this.attachOcclusionToEntity(entity);
112+
if (
113+
DepthOccludable.data.mode[entity.index] ===
114+
OcclusionShadersMode.MinMaxSoftOcclusion
115+
) {
116+
this.minMaxEntityCount++;
117+
}
101118
});
102119
this.queries.occludables.subscribe('disqualify', (entity: Entity) => {
120+
if (
121+
DepthOccludable.data.mode[entity.index] ===
122+
OcclusionShadersMode.MinMaxSoftOcclusion
123+
) {
124+
this.minMaxEntityCount--;
125+
}
103126
this.detachOcclusionFromEntity(entity);
104127
});
105128
}
@@ -166,6 +189,9 @@ export class DepthSensingSystem extends createSystem(
166189
shader.uniforms.uViewportSize = { value: new Vector2() };
167190
shader.uniforms.uOcclusionBlurRadius = { value: 20.0 };
168191
shader.uniforms.uOcclusionHardMode = { value: false };
192+
shader.uniforms.uOcclusionMinMaxMode = { value: false };
193+
shader.uniforms.uMinMaxTexture0 = { value: null };
194+
shader.uniforms.uMinMaxTexture1 = { value: null };
169195

170196
shader.defines = {
171197
...(shader.defines ?? {}),
@@ -200,6 +226,9 @@ export class DepthSensingSystem extends createSystem(
200226
'uniform vec2 uViewportSize;',
201227
'uniform float uOcclusionBlurRadius;',
202228
'uniform bool uOcclusionHardMode;',
229+
'uniform bool uOcclusionMinMaxMode;',
230+
'uniform sampler2D uMinMaxTexture0;',
231+
'uniform sampler2D uMinMaxTexture1;',
203232
'varying float vOcclusionViewDepth;',
204233
'',
205234
'uniform sampler2DArray uXRDepthTextureArray;',
@@ -231,7 +260,29 @@ export class DepthSensingSystem extends createSystem(
231260
' vec2 screenUV = gl_FragCoord.xy / uViewportSize;',
232261
' vec2 depthUV = uIsGPUDepth ? screenUV : vec2(screenUV.x, 1.0 - screenUV.y);',
233262
' float occlusion_value;',
234-
' if (uOcclusionHardMode) {',
263+
' if (uOcclusionMinMaxMode) {',
264+
' // MinMax soft occlusion — two-cluster edge-aware blending',
265+
' vec4 mmData;',
266+
' if (uint(VIEW_ID) == 0u) {',
267+
' mmData = texture2D(uMinMaxTexture0, depthUV);',
268+
' } else {',
269+
' mmData = texture2D(uMinMaxTexture1, depthUV);',
270+
' }',
271+
' float minAvgDepth = mmData.r;',
272+
' float maxAvgDepth = mmData.g;',
273+
' float midAvgDepth = mmData.r + mmData.b;',
274+
' float fadeRange = vOcclusionViewDepth * 0.04;',
275+
' float fadeRangeInv = 1.0 / max(fadeRange, 0.001);',
276+
' vec3 envDepths = vec3(minAvgDepth, maxAvgDepth, midAvgDepth);',
277+
' vec3 occAlphas = clamp((envDepths - vOcclusionViewDepth) * fadeRangeInv, 0.0, 1.0);',
278+
' occlusion_value = occAlphas.z;',
279+
' float alphaDiff = occAlphas.y - occAlphas.x;',
280+
' if (alphaDiff > 0.03) {',
281+
' float denom = mmData.a;',
282+
' float interp = denom > 0.001 ? mmData.b / denom : 0.5;',
283+
' occlusion_value = mix(occAlphas.x, occAlphas.y, smoothstep(0.2, 0.8, interp));',
284+
' }',
285+
' } else if (uOcclusionHardMode) {',
235286
' occlusion_value = OcclusionGetSample(depthUV, vec2(0.0));',
236287
' } else {',
237288
' vec2 texelSize = uOcclusionBlurRadius / uViewportSize;',
@@ -267,6 +318,9 @@ export class DepthSensingSystem extends createSystem(
267318
this.depthFeatureEnabled = undefined;
268319
this.cpuDepthData = [];
269320
this.gpuDepthData = [];
321+
this.preprocessingPass?.dispose();
322+
this.preprocessingPass = undefined;
323+
this.minMaxEntityCount = 0;
270324
}
271325

272326
private updateEnabledFeatures(xrSession: XRSession | null): void {
@@ -295,10 +349,62 @@ export class DepthSensingSystem extends createSystem(
295349
}
296350

297351
if (this.config.enableOcclusion.value) {
352+
this.runMinMaxPreprocessing();
298353
this.updateOcclusionUniforms();
299354
}
300355
}
301356

357+
/**
358+
* Runs the MinMax depth preprocessing pass if any entity uses MinMaxSoftOcclusion.
359+
* Renders a fullscreen pass per eye that computes min/max/avg depth in a 4×4
360+
* neighborhood, outputting to per-view 2D render targets.
361+
*/
362+
private runMinMaxPreprocessing(): void {
363+
if (this.minMaxEntityCount === 0) return;
364+
365+
const nativeTexture = this.depthTextures?.getNativeTexture();
366+
const dataArrayTexture = this.depthTextures?.getDataArrayTexture();
367+
const isGPUDepth = nativeTexture !== undefined;
368+
const depthTextureArray = isGPUDepth
369+
? (nativeTexture as Texture)
370+
: (dataArrayTexture as Texture);
371+
if (!depthTextureArray) return;
372+
373+
const depthNear =
374+
(this.gpuDepthData[0] as unknown as { depthNear: number } | undefined)
375+
?.depthNear ?? 0;
376+
377+
if (!this.preprocessingPass) {
378+
this.preprocessingPass = new DepthPreprocessingPass();
379+
}
380+
381+
this.preprocessingPass.setDepthTexture(
382+
depthTextureArray,
383+
this.rawValueToMeters,
384+
isGPUDepth,
385+
depthNear,
386+
);
387+
388+
// Determine depth texture dimensions
389+
let depthWidth: number;
390+
let depthHeight: number;
391+
if (this.cpuDepthData[0]) {
392+
depthWidth = this.cpuDepthData[0].width;
393+
depthHeight = this.cpuDepthData[0].height;
394+
} else if (this.gpuDepthData[0]) {
395+
// GPU depth textures have their own resolution (typically ~256×192),
396+
// which is much smaller than the drawing buffer.
397+
depthWidth = this.gpuDepthData[0].width;
398+
depthHeight = this.gpuDepthData[0].height;
399+
} else {
400+
return; // No depth data available
401+
}
402+
403+
// Render preprocessing for both eyes
404+
this.preprocessingPass.render(this.renderer, depthWidth, depthHeight, 0);
405+
this.preprocessingPass.render(this.renderer, depthWidth, depthHeight, 1);
406+
}
407+
302408
/**
303409
* Updates depth data from the XR frame.
304410
*/
@@ -382,6 +488,9 @@ export class DepthSensingSystem extends createSystem(
382488
const isHardMode =
383489
DepthOccludable.data.mode[entity.index] ===
384490
OcclusionShadersMode.HardOcclusion;
491+
const isMinMaxMode =
492+
DepthOccludable.data.mode[entity.index] ===
493+
OcclusionShadersMode.MinMaxSoftOcclusion;
385494

386495
for (const uniforms of entityUniforms) {
387496
uniforms.uXRDepthTextureArray.value = depthTextureArray;
@@ -391,7 +500,13 @@ export class DepthSensingSystem extends createSystem(
391500
(uniforms.uViewportSize.value as Vector2).copy(this.viewportSize);
392501
uniforms.uOcclusionBlurRadius.value = this.config.blurRadius.value;
393502
uniforms.uOcclusionHardMode.value = isHardMode;
503+
uniforms.uOcclusionMinMaxMode.value = isMinMaxMode;
394504
uniforms.occlusionEnabled.value = this.config.enableOcclusion.value;
505+
506+
if (isMinMaxMode && this.preprocessingPass) {
507+
uniforms.uMinMaxTexture0.value = this.preprocessingPass.getTexture(0);
508+
uniforms.uMinMaxTexture1.value = this.preprocessingPass.getTexture(1);
509+
}
395510
}
396511
}
397512
}

packages/core/src/depth/depth-textures.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,7 @@ export class DepthTextures {
106106
this.nativeTexture.sourceTexture = depthData.texture;
107107
}
108108
// Update the texture properties for three.js
109-
const textureProperties = renderer.properties.get(
110-
this.nativeTexture,
111-
) as {
109+
const textureProperties = renderer.properties.get(this.nativeTexture) as {
112110
__webglTexture: WebGLTexture;
113111
__version: number;
114112
};

packages/core/src/depth/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
// Types and configuration
9-
export * from './types.js';
10-
118
// Components
129
export * from './depth-occludable.js';
1310

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {
9+
FloatType,
10+
RGBAFormat,
11+
ShaderMaterial,
12+
Texture,
13+
Vector2,
14+
WebGLRenderer,
15+
WebGLRenderTarget,
16+
} from '../../runtime/three.js';
17+
import { FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';
18+
import { DepthPreprocessingShader } from './preprocessing-shader.glsl.js';
19+
20+
/**
21+
* Depth preprocessing pass — ported from Meta SDK DepthPreprocessing.shader.
22+
*
23+
* Runs a single fullscreen pass on the raw XR depth texture at depth texture
24+
* resolution (~256×192). For each output texel it samples a 4×4 neighborhood
25+
* and computes min/max/avg depth, writing the result to an RGBA float target:
26+
*
27+
* RGBA = (minAvg, maxAvg, avg - minAvg, maxAvg - minAvg) in meters
28+
*
29+
* The material shader then samples this preprocessed texture to apply soft
30+
* occlusion without requiring scene-override renders or Kawase blur.
31+
*/
32+
export class DepthPreprocessingPass {
33+
private material: ShaderMaterial;
34+
private renderTargets: [WebGLRenderTarget, WebGLRenderTarget];
35+
private fsQuad: FullScreenQuad;
36+
37+
constructor() {
38+
this.material = new ShaderMaterial({
39+
name: 'DepthPreprocessing',
40+
uniforms: {
41+
uXRDepthTextureArray: { value: null },
42+
uRawValueToMeters: { value: 0.001 },
43+
uViewId: { value: 0 },
44+
uDepthTextureSize: { value: new Vector2() },
45+
uIsGPUDepth: { value: false },
46+
uDepthNear: { value: 0 },
47+
},
48+
vertexShader: DepthPreprocessingShader.vertexShader,
49+
fragmentShader: DepthPreprocessingShader.fragmentShader,
50+
});
51+
52+
const rtOptions = { format: RGBAFormat, type: FloatType };
53+
this.renderTargets = [
54+
new WebGLRenderTarget(1, 1, rtOptions),
55+
new WebGLRenderTarget(1, 1, rtOptions),
56+
];
57+
58+
this.fsQuad = new FullScreenQuad(this.material);
59+
}
60+
61+
/**
62+
* Set the depth texture source for preprocessing.
63+
* @param depthTexture - The XR depth texture array (ExternalTexture or DataArrayTexture).
64+
* @param rawValueToMeters - Conversion factor from raw depth values to meters.
65+
* @param isGPUDepth - Whether the depth data is GPU-optimized (inverse depth).
66+
* @param depthNear - The near plane distance for GPU depth conversion.
67+
*/
68+
setDepthTexture(
69+
depthTexture: Texture,
70+
rawValueToMeters: number,
71+
isGPUDepth: boolean,
72+
depthNear: number,
73+
): void {
74+
this.material.uniforms.uXRDepthTextureArray.value = depthTexture;
75+
this.material.uniforms.uRawValueToMeters.value = rawValueToMeters;
76+
this.material.uniforms.uIsGPUDepth.value = isGPUDepth;
77+
this.material.uniforms.uDepthNear.value = depthNear;
78+
}
79+
80+
/**
81+
* Render the preprocessing pass.
82+
* @param renderer - The three.js WebGL renderer.
83+
* @param depthWidth - Width of the depth texture.
84+
* @param depthHeight - Height of the depth texture.
85+
* @param viewId - The view index to render (default 0).
86+
*/
87+
render(
88+
renderer: WebGLRenderer,
89+
depthWidth: number,
90+
depthHeight: number,
91+
viewId = 0,
92+
): void {
93+
const target = this.renderTargets[viewId];
94+
// Only resize if dimensions actually changed
95+
if (target.width !== depthWidth || target.height !== depthHeight) {
96+
target.setSize(depthWidth, depthHeight);
97+
}
98+
this.material.uniforms.uViewId.value = viewId;
99+
(this.material.uniforms.uDepthTextureSize.value as Vector2).set(
100+
depthWidth,
101+
depthHeight,
102+
);
103+
104+
const originalRenderTarget = renderer.getRenderTarget();
105+
// Temporarily disable XR so FullScreenQuad renders with its own
106+
// identity camera instead of the XR stereo/multiview cameras.
107+
const xrEnabled = renderer.xr.enabled;
108+
renderer.xr.enabled = false;
109+
renderer.setRenderTarget(target);
110+
this.fsQuad.render(renderer);
111+
renderer.setRenderTarget(originalRenderTarget);
112+
renderer.xr.enabled = xrEnabled;
113+
}
114+
115+
/**
116+
* Get the preprocessed depth texture for a given view.
117+
* RGBA = (minAvg, maxAvg, avg - minAvg, maxAvg - minAvg) in meters.
118+
* @param viewId - The view index (0 for left eye, 1 for right eye).
119+
*/
120+
getTexture(viewId = 0): Texture {
121+
return this.renderTargets[viewId].texture;
122+
}
123+
124+
/**
125+
* Dispose of all resources.
126+
*/
127+
dispose(): void {
128+
for (const target of this.renderTargets) {
129+
target.dispose();
130+
}
131+
this.material.dispose();
132+
this.fsQuad.dispose();
133+
}
134+
}

0 commit comments

Comments
 (0)