66 */
77
88import { 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' ;
1010import { DepthOccludable , OcclusionShadersMode } from './depth-occludable.js' ;
1111import { 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 }
0 commit comments