spoint 0.1.629 → 0.1.630
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/app.js +2 -15
- package/client/core/QualityPresets.js +3 -3
- package/client/core/RenderControls.js +2 -12
- package/client/core/VegImpostorTier.js +19 -0
- package/client/core/Vegetation.js +28 -4
- package/client/core/Weather.js +1 -13
- package/package.json +1 -1
- package/client/core/HalfResTransparents.js +0 -456
package/client/app.js
CHANGED
|
@@ -52,7 +52,6 @@ import { createRenderGraph } from './core/RenderGraph.js'
|
|
|
52
52
|
import { buildRenderSectionNodes } from './core/RenderGraph.nodes.js'
|
|
53
53
|
import { buildSSAONodes, installSSAO } from './core/SSAO.js'
|
|
54
54
|
import { buildSSRNodes, installSSR } from './core/SSR.js'
|
|
55
|
-
import { buildHalfResTransparentsNodes, installHalfResTransparents } from './core/HalfResTransparents.js'
|
|
56
55
|
import { buildBloomNodes, installBloom, registerBloomWebGPU } from './core/Bloom.js'
|
|
57
56
|
import { buildFSR1Nodes, installFSR1, registerFSR1WebGPU } from './core/FSR1.js'
|
|
58
57
|
import { installRenderControls, RenderControls } from './core/RenderControls.js'
|
|
@@ -535,14 +534,7 @@ function _ensureWeather(tb) {
|
|
|
535
534
|
const wcfg = _terrainCfg.weather
|
|
536
535
|
if (!wcfg || wcfg === false) return null
|
|
537
536
|
try {
|
|
538
|
-
|
|
539
|
-
// boot (client/app.js, before this async _buildWorldScenery path ever runs) -- read lazily via
|
|
540
|
-
// the window handle rather than a captured module const, since _ensureWeather's own definition
|
|
541
|
-
// runs before that module-level install line executes (function bodies don't evaluate until
|
|
542
|
-
// called; by call time the install has already happened). See HalfResTransparents.js header.
|
|
543
|
-
const hrt = (typeof window !== 'undefined') ? window.__halfResTransparentsDebug : null
|
|
544
|
-
const tagHalfRes = hrt ? (obj) => hrt.tagHalfRes(obj) : null
|
|
545
|
-
weather = createWeather({ renderer, scene, frame: tb && tb.frame, cfg: wcfg, tagHalfRes })
|
|
537
|
+
weather = createWeather({ renderer, scene, frame: tb && tb.frame, cfg: wcfg })
|
|
546
538
|
if (window.__app) window.__app.weather = weather
|
|
547
539
|
} catch (e) { console.error('[weather] init failed:', e?.message || e) }
|
|
548
540
|
return null
|
|
@@ -2702,7 +2694,7 @@ function tickPlayerAnimators(lid, frameDt, isEditor) {
|
|
|
2702
2694
|
// window.__renderGraph = live inspection surface (stats()/capture()/disable(id)/toMermaid()).
|
|
2703
2695
|
// _graphCtx is PERSISTENT so ctx.res carries last-frame resources (the deliberate one-frame
|
|
2704
2696
|
// near/far lag + shouldRun-skip semantics depend on it).
|
|
2705
|
-
const renderGraph = createRenderGraph([...buildRenderSectionNodes(), ...buildSSAONodes(), ...buildBloomNodes(), ...buildSSRNodes(), ...
|
|
2697
|
+
const renderGraph = createRenderGraph([...buildRenderSectionNodes(), ...buildSSAONodes(), ...buildBloomNodes(), ...buildSSRNodes(), ...buildFSR1Nodes()])
|
|
2706
2698
|
// pm (PlayerManager, declared above) exposes pm.playerMeshes (Map<id, THREE.Group>, real live
|
|
2707
2699
|
// per-player world positions, local + remote, server-snapshot-reconciled) -- foliage-lod-sync's grass
|
|
2708
2700
|
// player-bend wiring reads ctx.pm.playerMeshes every frame to feed Grass.js's nearby-bender buffer.
|
|
@@ -2732,11 +2724,6 @@ installBloom(_graphCtx, renderer)
|
|
|
2732
2724
|
// SSR shares SSAO's G-buffer when available (see SSR.js buildSSRNodes' ssr-compute node), so it
|
|
2733
2725
|
// pays zero extra scene-render cost beyond SSAO's own existing pass.
|
|
2734
2726
|
installSSR(_graphCtx, renderer, scene, camera)
|
|
2735
|
-
// Half-res transparents/particles GPU resources are built lazily too (RenderControls
|
|
2736
|
-
// ('halfResTransparents') gate, same discipline as SSAO/Bloom/SSR above) -- installed once here so
|
|
2737
|
-
// ctx.halfResTransparents is populated before the first frame the flag might be on, and so
|
|
2738
|
-
// _ensureWeather (below) can tag its particle pools onto the half-res layer as soon as they exist.
|
|
2739
|
-
installHalfResTransparents(_graphCtx, renderer, scene, camera)
|
|
2740
2727
|
// FSR1 GPU resources are built lazily too (RenderControls('fsr1') gate, same discipline as
|
|
2741
2728
|
// SSAO/Bloom/SSR above) -- installed once here so ctx.fsr1 is populated before the first frame the
|
|
2742
2729
|
// flag might be on. Only actually runs while the DPR controller has genuinely downscaled (see
|
|
@@ -40,7 +40,7 @@ const PRESETS = {
|
|
|
40
40
|
vegAllOff: false,
|
|
41
41
|
vegHideFar: null,
|
|
42
42
|
grassWind: false,
|
|
43
|
-
halfResWater:
|
|
43
|
+
halfResWater: false,
|
|
44
44
|
thc: false,
|
|
45
45
|
ssao: false,
|
|
46
46
|
bloom: false,
|
|
@@ -60,7 +60,7 @@ const PRESETS = {
|
|
|
60
60
|
vegAllOff: false,
|
|
61
61
|
vegHideFar: null,
|
|
62
62
|
grassWind: true,
|
|
63
|
-
halfResWater:
|
|
63
|
+
halfResWater: false,
|
|
64
64
|
thc: false,
|
|
65
65
|
ssao: true,
|
|
66
66
|
bloom: true,
|
|
@@ -76,7 +76,7 @@ const PRESETS = {
|
|
|
76
76
|
vegAllOff: false,
|
|
77
77
|
vegHideFar: null,
|
|
78
78
|
grassWind: true,
|
|
79
|
-
halfResWater:
|
|
79
|
+
halfResWater: false,
|
|
80
80
|
thc: false,
|
|
81
81
|
ssao: true,
|
|
82
82
|
bloom: true,
|
|
@@ -168,16 +168,6 @@ const CONTROLS = [
|
|
|
168
168
|
{ key: 'bloomBlurPasses', group: 'bloom', type: 'number', default: 1,
|
|
169
169
|
doc: 'Number of full horizontal+vertical separable-blur passes applied to the bright-pass texture. Higher = smoother/wider glow, more GPU cost.' },
|
|
170
170
|
|
|
171
|
-
// ---- Half-res transparents/particles (see HalfResTransparents.js -- half-resolution offscreen
|
|
172
|
-
// composite of THREE.Layers-tagged transparent/particle content, e.g. Weather.js rain/snow;
|
|
173
|
-
// does NOT touch the shared canvas depth contract documented in DepthComposite.js) ----
|
|
174
|
-
{ key: 'halfResTransparents', group: 'half-res-transparents', type: 'boolean', default: false,
|
|
175
|
-
doc: 'Render THREE.Layers-tagged transparent/particle content (see HalfResTransparents.HALFRES_LAYER, currently Weather.js rain/snow particle pools) into a half-resolution offscreen target instead of full-res, composited back with real depth-tested occlusion against the opaque scene. Perf win on overdraw-heavy particle scenes (weather); default off pending a device-tier default decision.' },
|
|
176
|
-
{ key: 'halfResTransparentsScale', group: 'half-res-transparents', type: 'number', default: 0.5,
|
|
177
|
-
doc: 'Render-target resolution scale (fraction of canvas size) for the half-res transparents target. Lower = cheaper, softer/blurrier particle edges.' },
|
|
178
|
-
{ key: 'halfResTransparentsDepthSigma', group: 'half-res-transparents', type: 'number', default: 2.0,
|
|
179
|
-
doc: 'Depth-delta scale (metres) at which the composite bilateral upsample halves a neighbor tap\'s weight (see HalfResTransparents.js _compositeFrag). Lower = more aggressive edge-preservation (sharper depth-discontinuity cutoff, more prone to single-texel dropout on noisy depth); higher = softer, closer to plain bilinear.' },
|
|
180
|
-
|
|
181
171
|
// ---- FSR1 spatial upscale/sharpen (see FSR1.js -- companion to the DPR controller below: a DPR
|
|
182
172
|
// drop shrinks the WebGL drawing buffer, which the browser then bilinear-stretches back up
|
|
183
173
|
// to CSS size; this pass replaces that dumb stretch with an EASU+RCAS upscale+sharpen so a
|
|
@@ -207,8 +197,8 @@ const CONTROLS = [
|
|
|
207
197
|
doc: 'READ-ONLY mirror of the live VRAM budget tracker (ModelPoolAdapter.getVramStats()), refreshed once per frame: {usedMB, estimatedVramMB, currentRatio, peakRatio, byteBudgetMB, totalBytes, unloadedCount, visibleEntities, invisibleEntities, recentEvents}. recentEvents is a real ring-buffer log (real vram-warning/vram-critical/budget-pressure/budget-relaxed/budget-adjust events the pool actually emitted, not synthetic). window.__vramBudget mirrors the same data plus setBudgetMB()/log() convenience calls.' },
|
|
208
198
|
|
|
209
199
|
// ---- mapspinner-side knobs (read in packages/mapspinner/src/gl-render.js; documented here for discovery) ----
|
|
210
|
-
{ key: 'halfResWater', group: 'mapspinner-water', type: 'boolean', default:
|
|
211
|
-
doc: 'mapspinner
|
|
200
|
+
{ key: 'halfResWater', group: 'mapspinner-water', type: 'boolean', default: false,
|
|
201
|
+
doc: 'mapspinner water-occlusion depth-share pass (near sea level). When active the scene renders into a single-sample VDRS FBO (losing MSAA) then upscales, purely so the water-occlusion depth test has a real depth buffer to blit (the default framebuffer cannot be read via blitFramebuffer). Default off (every quality preset sets this false) since the MSAA loss reads as a visible quality drop across the whole frame, not just water -- set true only to re-enable the occlusion-depth-share mechanism at the cost of full-frame MSAA.' },
|
|
212
202
|
{ key: 'vdrs', group: 'mapspinner-resolution', type: 'boolean', default: false,
|
|
213
203
|
doc: 'mapspinner explicit viewport dynamic-resolution mode (independent of the half-res-water-forced path): true routes terrain+water into a fixed-size offscreen FBO, renders it at a flexed __vdrsScale<1 sub-viewport, then LINEAR-upscales that sub-rect to the canvas at its actual (unchanged) size -- terrain renders fewer pixels, the canvas/presented frame stays full resolution. Pairs with vdrsScale. Normally driven automatically by the vdrsAuto controller (see resolution group above); settable directly for manual testing.' },
|
|
214
204
|
{ key: 'vdrsUpscaleFsr1', group: 'mapspinner-resolution', type: 'boolean', default: false,
|
|
@@ -111,6 +111,25 @@ export function createSharedImpostorMesh(renderer, atlas, dims, opts = {}) {
|
|
|
111
111
|
mat.polygonOffsetUnits = -8
|
|
112
112
|
// Base geo must be zero-area below nearCutoff (near trees draw the full branch mesh instead) or every near tree double-draws; plane is the far LOD.
|
|
113
113
|
const nearCutoff = opts.nearCutoff
|
|
114
|
+
const IMPOSTOR_DISSOLVE_FADE_BAND_M = 3.0
|
|
115
|
+
if (Number.isFinite(nearCutoff) && nearCutoff > 0) {
|
|
116
|
+
const baseCompile = mat.onBeforeCompile
|
|
117
|
+
mat.onBeforeCompile = (shader, r) => {
|
|
118
|
+
baseCompile?.call(mat, shader, r)
|
|
119
|
+
shader.uniforms.uImpNearCutoff = { value: nearCutoff }
|
|
120
|
+
shader.vertexShader = 'uniform float uImpNearCutoff;\nvarying float vImpCamDist;\n' + shader.vertexShader
|
|
121
|
+
shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>',
|
|
122
|
+
'#include <begin_vertex>\nvImpCamDist = distance(cameraPosition, (modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz);')
|
|
123
|
+
shader.fragmentShader = 'uniform float uImpNearCutoff;\nvarying float vImpCamDist;\n' + shader.fragmentShader
|
|
124
|
+
shader.fragmentShader = shader.fragmentShader.replace('#include <dithering_fragment>',
|
|
125
|
+
'#include <dithering_fragment>\n' +
|
|
126
|
+
`float _impFade = clamp(abs(vImpCamDist - uImpNearCutoff) / ${IMPOSTOR_DISSOLVE_FADE_BAND_M.toFixed(1)}, 0.0, 1.0);\n` +
|
|
127
|
+
'float _impDither = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));\n' +
|
|
128
|
+
'if (_impDither > _impFade) discard;')
|
|
129
|
+
}
|
|
130
|
+
const baseKey = mat.customProgramCacheKey
|
|
131
|
+
mat.customProgramCacheKey = () => baseKey() + '_impfade'
|
|
132
|
+
}
|
|
114
133
|
let baseGeo
|
|
115
134
|
if (Number.isFinite(nearCutoff) && nearCutoff > 0) {
|
|
116
135
|
baseGeo = new THREE.BufferGeometry()
|
|
@@ -75,7 +75,16 @@ const _vanMat = new THREE.Matrix4(), _vanProj = new THREE.Matrix4(), _vanFrustum
|
|
|
75
75
|
// shared wind uniform (one per veg system); advancing one .value per frame sways all LODs of all species with zero per-instance JS
|
|
76
76
|
function makeWindUniforms() { return { uVegTime: { value: 0 }, uVegWind: { value: 1 } } }
|
|
77
77
|
|
|
78
|
-
function
|
|
78
|
+
function _dissolveBoundaryDistanceShaderChunk(boundaries) {
|
|
79
|
+
const uniforms = boundaries.map((_, i) => `uniform float uVegLodB${i};`).join('\n')
|
|
80
|
+
const mindist = boundaries.map((_, i) => `_vegDistToB = min(_vegDistToB, abs(_vegCamDist - uVegLodB${i}));`).join('\n ')
|
|
81
|
+
return { uniforms, mindist }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const VEG_DISSOLVE_FADE_BAND_M = 3.0
|
|
85
|
+
|
|
86
|
+
function applyWind(material, wind, lodBoundaries) {
|
|
87
|
+
const chunk = lodBoundaries && lodBoundaries.length ? _dissolveBoundaryDistanceShaderChunk(lodBoundaries) : null
|
|
79
88
|
material.onBeforeCompile = (shader) => {
|
|
80
89
|
shader.uniforms.uVegTime = wind.uVegTime
|
|
81
90
|
shader.uniforms.uVegWind = wind.uVegWind
|
|
@@ -90,8 +99,23 @@ function applyWind(material, wind) {
|
|
|
90
99
|
// per-instance shade: multiply the lit diffuse by the instance tint (brightness variation).
|
|
91
100
|
shader.fragmentShader = shader.fragmentShader.replace('#include <color_fragment>',
|
|
92
101
|
'#include <color_fragment>\n diffuseColor.rgb *= tint;')
|
|
102
|
+
if (chunk) {
|
|
103
|
+
lodBoundaries.forEach((d, i) => { shader.uniforms['uVegLodB' + i] = { value: d } })
|
|
104
|
+
shader.vertexShader = chunk.uniforms + '\nvarying float vVegCamDist;\n' + shader.vertexShader
|
|
105
|
+
shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>',
|
|
106
|
+
'#include <begin_vertex>\nvVegCamDist = distance(cameraPosition, (modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz);')
|
|
107
|
+
shader.fragmentShader = chunk.uniforms + '\nvarying float vVegCamDist;\n' + shader.fragmentShader
|
|
108
|
+
shader.fragmentShader = shader.fragmentShader.replace('#include <dithering_fragment>',
|
|
109
|
+
'#include <dithering_fragment>\n' +
|
|
110
|
+
`float _vegCamDist = vVegCamDist;\n` +
|
|
111
|
+
`float _vegDistToB = 1e9;\n` +
|
|
112
|
+
` ${chunk.mindist}\n` +
|
|
113
|
+
`float _vegFade = clamp(_vegDistToB / ${VEG_DISSOLVE_FADE_BAND_M.toFixed(1)}, 0.0, 1.0);\n` +
|
|
114
|
+
'float _vegDither = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));\n' +
|
|
115
|
+
'if (_vegDither > _vegFade) discard;')
|
|
116
|
+
}
|
|
93
117
|
}
|
|
94
|
-
material.customProgramCacheKey = () => '
|
|
118
|
+
material.customProgramCacheKey = () => 'vegwind3' + (chunk ? '_fade' + lodBoundaries.join('_') : '')
|
|
95
119
|
return material
|
|
96
120
|
}
|
|
97
121
|
|
|
@@ -286,8 +310,8 @@ export async function createVegetation(opts = {}) {
|
|
|
286
310
|
const _treeSph = _treeBox.getBoundingSphere(new THREE.Sphere())
|
|
287
311
|
branchGeo0.boundingBox = _treeBox.clone(); leafGeo0.boundingBox = _treeBox.clone()
|
|
288
312
|
branchGeo0.boundingSphere = _treeSph.clone(); leafGeo0.boundingSphere = _treeSph.clone()
|
|
289
|
-
const branch = new InstancedMesh2(branchGeo0, applyWind(sp.branchMat, wind), { capacity: INIT_CAP, renderer })
|
|
290
|
-
const leaf = new InstancedMesh2(leafGeo0, applyWind(sp.leafMat, wind), { capacity: INIT_CAP, renderer })
|
|
313
|
+
const branch = new InstancedMesh2(branchGeo0, applyWind(sp.branchMat, wind, [D1, D2, D3]), { capacity: INIT_CAP, renderer })
|
|
314
|
+
const leaf = new InstancedMesh2(leafGeo0, applyWind(sp.leafMat, wind, [D1, D2]), { capacity: INIT_CAP, renderer })
|
|
291
315
|
for (const m of [branch, leaf]) {
|
|
292
316
|
m.initUniformsPerInstance({ vertex: { windPhase: 'float' }, fragment: { tint: 'float' } })
|
|
293
317
|
m.perObjectFrustumCulled = true
|
package/client/core/Weather.js
CHANGED
|
@@ -243,20 +243,12 @@ const _camPos = new THREE.Vector3(), _camQuat = new THREE.Quaternion()
|
|
|
243
243
|
|
|
244
244
|
// opts: { renderer, scene, frame (terrain frame, for ground-height sampling -- optional, falls back
|
|
245
245
|
// to a fixed splash plane if absent), cfg: { type: 'rain'|'snow'|'clear', intensity: 0..1,
|
|
246
|
-
// particleCount, farParticleCount, boxRadius, boxHeight, fallSpeed, snowAccumulation }
|
|
247
|
-
// tagHalfRes: optional (object) => void -- called once per particle pool right after scene.add(),
|
|
248
|
-
// see client/core/HalfResTransparents.js (half-res-transparents-particles-pass). Weather's own
|
|
249
|
-
// rain/snow/splash/far-sheet InstancedMesh2 pools are the first (and, at this row's first-slice
|
|
250
|
-
// scope, only) opt-in consumer of that half-res compositing pass -- exactly the "particle-heavy
|
|
251
|
-
// scenes (e.g. weather rain/snow tier)" case the PRD row names. Optional/no-op when absent so
|
|
252
|
-
// Weather.js has zero dependency on HalfResTransparents existing (dual-import-safe, matches every
|
|
253
|
-
// other opts.* callback in this module). }
|
|
246
|
+
// particleCount, farParticleCount, boxRadius, boxHeight, fallSpeed, snowAccumulation } }
|
|
254
247
|
export function createWeather(opts = {}) {
|
|
255
248
|
const { renderer, scene } = opts
|
|
256
249
|
if (!renderer || !scene) throw new Error('createWeather: renderer/scene required')
|
|
257
250
|
const cfg = opts.cfg || {}
|
|
258
251
|
const frame = opts.frame || null
|
|
259
|
-
const tagHalfRes = typeof opts.tagHalfRes === 'function' ? opts.tagHalfRes : null
|
|
260
252
|
|
|
261
253
|
const BOX_RADIUS = Number.isFinite(cfg.boxRadius) ? cfg.boxRadius : 22 // XZ half-extent of the near-tier falling-volume box, camera-centered
|
|
262
254
|
const BOX_HEIGHT = Number.isFinite(cfg.boxHeight) ? cfg.boxHeight : 18 // Y extent above the camera near-tier particles fall through
|
|
@@ -328,7 +320,6 @@ export function createWeather(opts = {}) {
|
|
|
328
320
|
im.frustumCulled = false
|
|
329
321
|
im.visible = false
|
|
330
322
|
scene.add(im)
|
|
331
|
-
if (tagHalfRes) tagHalfRes(im)
|
|
332
323
|
|
|
333
324
|
const geoSplash = makeSplashGeo()
|
|
334
325
|
const matSplash = makeSplashMaterial()
|
|
@@ -338,7 +329,6 @@ export function createWeather(opts = {}) {
|
|
|
338
329
|
imSplash.frustumCulled = false
|
|
339
330
|
imSplash.visible = false
|
|
340
331
|
scene.add(imSplash)
|
|
341
|
-
if (tagHalfRes) tagHalfRes(imSplash)
|
|
342
332
|
|
|
343
333
|
const geoFlake = makeFlakeGeo()
|
|
344
334
|
const matSnow = makeSnowMaterial()
|
|
@@ -347,7 +337,6 @@ export function createWeather(opts = {}) {
|
|
|
347
337
|
imSnow.frustumCulled = false
|
|
348
338
|
imSnow.visible = false
|
|
349
339
|
scene.add(imSnow)
|
|
350
|
-
if (tagHalfRes) tagHalfRes(imSnow)
|
|
351
340
|
|
|
352
341
|
// Far sheet: one pool, re-skinned (material swapped) per type rather than two separate meshes --
|
|
353
342
|
// only one of rain/snow is ever active at a time (type is a single enum), so there is no benefit to
|
|
@@ -362,7 +351,6 @@ export function createWeather(opts = {}) {
|
|
|
362
351
|
imFar.frustumCulled = false
|
|
363
352
|
imFar.visible = false
|
|
364
353
|
scene.add(imFar)
|
|
365
|
-
if (tagHalfRes) tagHalfRes(imFar)
|
|
366
354
|
let _farGeoIsSnow = false
|
|
367
355
|
|
|
368
356
|
// Snow ground accumulation: a REUSED src/terrain/GrassDecal.js sparse cell store (see file header --
|
package/package.json
CHANGED
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
// HalfResTransparents -- half-resolution transparent/particle compositing pass, gated behind
|
|
2
|
-
// RenderControls('halfResTransparents'). Roadmap #37 (half-res-transparents-particles-pass).
|
|
3
|
-
//
|
|
4
|
-
// WHY: overdraw-heavy transparent content (rain/snow particle volumes, future smoke/muzzle-flash
|
|
5
|
-
// sprites) costs the SAME per-pixel fragment-shader work as opaque geometry but contributes far
|
|
6
|
-
// less to perceived detail -- a classic half-res-offscreen-composite win (the exact discipline
|
|
7
|
-
// SSAO.js/Bloom.js/mapspinner's own half-res-water VDRS path already use in this codebase, see
|
|
8
|
-
// AGENTS.md water-depth-share-not-terrain-only). This module applies the same discipline to
|
|
9
|
-
// THREE-side transparent/particle objects: render them into a quarter-area (half-width x
|
|
10
|
-
// half-height) offscreen target instead of the full canvas, then composite back with a
|
|
11
|
-
// depth-aware upsample so the half-res edge doesn't bleed through nearer opaque geometry.
|
|
12
|
-
//
|
|
13
|
-
// HOW OBJECTS OPT IN: THREE.Layers, not a material/type check. Any object (or its material) that
|
|
14
|
-
// should render half-res sets `object.layers.set(HALFRES_LAYER)` (or `.layers.enable(HALFRES_LAYER)`
|
|
15
|
-
// if it also needs to stay on layer 0 for other purposes, e.g. raycasting) -- see `tagHalfRes(obj)`
|
|
16
|
-
// helper. The main scene camera then EXCLUDES that layer (camera.layers.disable(HALFRES_LAYER)) so
|
|
17
|
-
// scene-color's normal full-res render skips it entirely, and this module's own half-res camera
|
|
18
|
-
// (cloned near/far/fov from the main camera every frame, so projections match exactly) INCLUDES
|
|
19
|
-
// ONLY that layer. Layer-based opt-in (vs. a scene-graph split or a material flag) was chosen
|
|
20
|
-
// because it needs zero change to how/where an object is parented (Weather.js's InstancedMesh2
|
|
21
|
-
// pools stay `scene.add`-ed exactly where they are; the RenderGraph node here calls
|
|
22
|
-
// `installHalfResTransparents(...).tagHalfRes(im)` once at construction) and composes cleanly with
|
|
23
|
-
// THREE's own existing frustum-cull/raycast layer semantics.
|
|
24
|
-
//
|
|
25
|
-
// DEPTH-AWARE COMPOSITE: unlike Bloom (whole-canvas copy, no occlusion concern) this pass MUST NOT
|
|
26
|
-
// let a half-res particle draw over nearer opaque geometry it should be occluded by (e.g. rain
|
|
27
|
-
// behind a wall). DepthComposite.js's contract is that the ONLY depth buffer is the shared canvas
|
|
28
|
-
// depth (mapspinner + THREE, no offscreen scene depth texture exists) -- so, matching SSAO.js's own
|
|
29
|
-
// solved-this-already approach, this module renders its OWN small half-res depth-only pre-pass of
|
|
30
|
-
// the OPAQUE scene (camera.layers default, i.e. layer 0 only -- excludes HALFRES_LAYER) into the
|
|
31
|
-
// same-size target's depth attachment, then renders the half-res-layer objects against that depth
|
|
32
|
-
// (real GPU depth-test, not a shader trick) before compositing color back.
|
|
33
|
-
//
|
|
34
|
-
// BILATERAL (DEPTH-REJECT) UPSAMPLE (bilateral-upsample-fps-delta follow-up): the composite step
|
|
35
|
-
// that brings the half-res color target back onto the full-res canvas is a real bilateral filter,
|
|
36
|
-
// not plain bilinear -- the depth-only pre-pass ALSO encodes its linear view-space depth into a
|
|
37
|
-
// second small texture (_depthTarget, blitted from the pre-pass's color output each compute(), see
|
|
38
|
-
// step 1.5), and the composite shader (_compositeFrag) weights each of the 4 bilinear-neighborhood
|
|
39
|
-
// taps by how close ITS recorded opaque-scene depth is to the tap nearest the pixel being upsampled
|
|
40
|
-
// -- a tap on the far side of a depth discontinuity (e.g. a thin foreground rail) is down-weighted
|
|
41
|
-
// so its transparent color doesn't bleed across the edge. This uses the pass's OWN half-res depth as
|
|
42
|
-
// the edge/discontinuity proxy (documented option (b) from the original follow-up row) rather than a
|
|
43
|
-
// full-resolution THREE-only depth reference, since no such texture exists in this pipeline (see
|
|
44
|
-
// DepthComposite.js's shared-canvas-depth contract above) and standing one up purely for this would
|
|
45
|
-
// cost a second full-res depth-only scene render -- against the entire point of this pass being a
|
|
46
|
-
// perf win. uDepthSigma (RenderControls('halfResTransparentsDepthSigma')) tunes the rejection
|
|
47
|
-
// sensitivity.
|
|
48
|
-
//
|
|
49
|
-
// OVERRIDE MATERIAL, NOT EACH OBJECT'S REAL MATERIAL (live-debugged this session): the depth
|
|
50
|
-
// pre-pass uses `scene.overrideMaterial` with a single shared minimal depth-only ShaderMaterial --
|
|
51
|
-
// the SAME technique SSAO.js's G-buffer pass already uses (`_gbufferMat`) -- rather than letting
|
|
52
|
-
// each opaque object render through its own normal material with color writes masked off. Tried the
|
|
53
|
-
// real-material approach first; it produced a real, live-witnessed
|
|
54
|
-
// "GL_INVALID_OPERATION: glDrawArraysInstanced: Vertex shader input type does not match the type of
|
|
55
|
-
// the bound vertex attribute" storm, root-caused to InstancedMesh2's onBeforeRender/bindTextures
|
|
56
|
-
// hook (see node_modules/@three.ez/instanced-mesh's InstancedMesh2.js): it snapshots and restores
|
|
57
|
-
// `gl.CURRENT_PROGRAM` around a manual texture-uniform bind per draw call, and rendering the SAME
|
|
58
|
-
// InstancedMesh2 objects (vegetation/rocks/grass) through their normal per-object materials in TWO
|
|
59
|
-
// separate renderer.render() calls within one frame (once here, once in scene-color) left stale
|
|
60
|
-
// attribute-pointer state from the wrong program bound on the second pass. A single shared
|
|
61
|
-
// override material sidesteps this entirely -- InstancedMesh2's onBeforeRender still runs (needed
|
|
62
|
-
// for its own instance-index/matrix-texture update), but every opaque object now renders through
|
|
63
|
-
// the IDENTICAL program, so there is no per-object program churn within this pre-pass to desync.
|
|
64
|
-
|
|
65
|
-
import * as THREE from 'three'
|
|
66
|
-
import { RenderControls } from './RenderControls.js'
|
|
67
|
-
|
|
68
|
-
// Layer index reserved for half-res-transparent content. Layers 0 (default) and the small set THREE
|
|
69
|
-
// itself never reserves are otherwise free in this codebase (codesearch: no other `.layers.` use
|
|
70
|
-
// exists yet) -- 1 chosen as the first non-default layer, documented here as the ONE place this
|
|
71
|
-
// number is decided.
|
|
72
|
-
export const HALFRES_LAYER = 1
|
|
73
|
-
|
|
74
|
-
const _fullscreenVert = /* glsl */`
|
|
75
|
-
varying vec2 vUv;
|
|
76
|
-
void main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }
|
|
77
|
-
`
|
|
78
|
-
|
|
79
|
-
// Plain nearest-sample copy, used ONCE per compute() to blit the depth pre-pass's color output
|
|
80
|
-
// (linear-depth-in-alpha) from _target into the separate _depthTarget before _target's color gets
|
|
81
|
-
// overwritten by the transparent-layer draw -- see compute() step 1.5. depthTest/depthWrite off,
|
|
82
|
-
// opaque blending (source overwrites destination fully, no accumulation).
|
|
83
|
-
const _copyFrag = /* glsl */`
|
|
84
|
-
precision highp float;
|
|
85
|
-
varying vec2 vUv;
|
|
86
|
-
uniform sampler2D tSrc;
|
|
87
|
-
void main() { gl_FragColor = texture2D(tSrc, vUv); }
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
// Depth-reject BILATERAL upsample (bilateral-upsample-fps-delta follow-up to
|
|
91
|
-
// half-res-transparents-particles-pass). This codebase has no full-resolution THREE-only depth
|
|
92
|
-
// texture to reject against (DepthComposite.js's contract: the only depth buffer is the shared
|
|
93
|
-
// mapspinner+THREE canvas depth) -- so per that row's documented option (b), this uses the pass's
|
|
94
|
-
// OWN half-res opaque-scene depth (already rendered every compute() as a real GPU depth-test pre-
|
|
95
|
-
// pass, see _depthOnlyMat below) as a cheap edge/discontinuity proxy: the depth pre-pass now ALSO
|
|
96
|
-
// encodes linear view-space depth into a second half-res texture (tHalfDepth, same technique SSAO.js
|
|
97
|
-
// already uses -- pack raw metres into a color channel, no depth-curve reconstruction needed since
|
|
98
|
-
// this module owns the encode AND decode end to end), and the composite shader does a real bilateral
|
|
99
|
-
// filter over the 2x2 texel neighborhood around each upsampled sample: taps whose depth diverges from
|
|
100
|
-
// the others are down-weighted (Gaussian falloff on depth delta, uDepthSigma controls sensitivity),
|
|
101
|
-
// so a half-res transparent silhouette crossing a depth discontinuity (e.g. rain partially behind a
|
|
102
|
-
// thin foreground rail) samples mostly from the texel(s) on the SAME side of the discontinuity as the
|
|
103
|
-
// pixel being upsampled, instead of plain-bilinear's blend-across-the-edge blur/bleed.
|
|
104
|
-
const _compositeFrag = /* glsl */`
|
|
105
|
-
precision highp float;
|
|
106
|
-
varying vec2 vUv;
|
|
107
|
-
uniform sampler2D tColor;
|
|
108
|
-
uniform sampler2D tHalfDepth;
|
|
109
|
-
uniform vec2 uHalfTexelSize; // 1/halfResTargetSize, in the SAME uv space as tColor/tHalfDepth
|
|
110
|
-
uniform float uDepthSigma; // metres -- depth-delta scale at which a bilateral tap's weight halves
|
|
111
|
-
|
|
112
|
-
float sampleDepth(vec2 uv) { return texture2D(tHalfDepth, uv).a; }
|
|
113
|
-
|
|
114
|
-
void main() {
|
|
115
|
-
// Center depth at the exact half-res texel nearest this full-res pixel (nearest, not bilinear --
|
|
116
|
-
// this is the reference depth the 4 corner taps below get weighted against).
|
|
117
|
-
float centerDepth = sampleDepth(vUv);
|
|
118
|
-
|
|
119
|
-
// 2x2 bilinear-neighborhood taps, offset a half-texel in each diagonal direction so the four
|
|
120
|
-
// samples straddle the standard bilinear interpolation square around vUv.
|
|
121
|
-
vec2 o = uHalfTexelSize * 0.5;
|
|
122
|
-
vec2 uv00 = vUv + vec2(-o.x, -o.y);
|
|
123
|
-
vec2 uv10 = vUv + vec2( o.x, -o.y);
|
|
124
|
-
vec2 uv01 = vUv + vec2(-o.x, o.y);
|
|
125
|
-
vec2 uv11 = vUv + vec2( o.x, o.y);
|
|
126
|
-
|
|
127
|
-
vec4 c00 = texture2D(tColor, uv00);
|
|
128
|
-
vec4 c10 = texture2D(tColor, uv10);
|
|
129
|
-
vec4 c01 = texture2D(tColor, uv01);
|
|
130
|
-
vec4 c11 = texture2D(tColor, uv11);
|
|
131
|
-
|
|
132
|
-
float d00 = sampleDepth(uv00);
|
|
133
|
-
float d10 = sampleDepth(uv10);
|
|
134
|
-
float d01 = sampleDepth(uv01);
|
|
135
|
-
float d11 = sampleDepth(uv11);
|
|
136
|
-
|
|
137
|
-
// Depth-reject weights: a tap whose opaque-scene depth is far from centerDepth sits on the OTHER
|
|
138
|
-
// side of a discontinuity from the pixel being upsampled -- down-weight it so its transparent
|
|
139
|
-
// color does not bleed across the edge. Gaussian falloff, uDepthSigma tunes sensitivity; a tap
|
|
140
|
-
// with zero recorded depth (nothing opaque drawn there, e.g. sky) is never rejected (weight 1) so
|
|
141
|
-
// sky-only half-res regions still get plain bilinear quality.
|
|
142
|
-
float w00 = d00 <= 0.0 ? 1.0 : exp(-pow((d00 - centerDepth) / uDepthSigma, 2.0));
|
|
143
|
-
float w10 = d10 <= 0.0 ? 1.0 : exp(-pow((d10 - centerDepth) / uDepthSigma, 2.0));
|
|
144
|
-
float w01 = d01 <= 0.0 ? 1.0 : exp(-pow((d01 - centerDepth) / uDepthSigma, 2.0));
|
|
145
|
-
float w11 = d11 <= 0.0 ? 1.0 : exp(-pow((d11 - centerDepth) / uDepthSigma, 2.0));
|
|
146
|
-
|
|
147
|
-
float wSum = w00 + w10 + w01 + w11;
|
|
148
|
-
vec4 c;
|
|
149
|
-
if (wSum < 1e-5) {
|
|
150
|
-
// All 4 taps rejected (shouldn't normally happen since same-side-as-center always weight~1) --
|
|
151
|
-
// fall back to the plain nearest-center sample rather than dividing by zero.
|
|
152
|
-
c = texture2D(tColor, vUv);
|
|
153
|
-
} else {
|
|
154
|
-
c = (c00 * w00 + c10 * w10 + c01 * w01 + c11 * w11) / wSum;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (c.a < 0.003) discard;
|
|
158
|
-
gl_FragColor = c;
|
|
159
|
-
}
|
|
160
|
-
`
|
|
161
|
-
|
|
162
|
-
// Depth-only override material for the opaque-scene pre-pass (see header's OVERRIDE MATERIAL note).
|
|
163
|
-
// Fragment output now encodes LINEAR VIEW-SPACE DEPTH (raw metres, this module's own camera -- same
|
|
164
|
-
// packing convention as SSAO.js's G-buffer, see that file for why: cheap and exact since encode+decode
|
|
165
|
-
// are both owned here, no depth-curve reconstruction needed) into the alpha channel, read back by the
|
|
166
|
-
// bilateral composite above as the edge/discontinuity proxy. rgb is unused (kept 0) -- only .a matters.
|
|
167
|
-
const _depthOnlyVert = /* glsl */`
|
|
168
|
-
varying float vViewDepth;
|
|
169
|
-
void main() {
|
|
170
|
-
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
|
171
|
-
vViewDepth = -mvPosition.z;
|
|
172
|
-
gl_Position = projectionMatrix * mvPosition;
|
|
173
|
-
}
|
|
174
|
-
`
|
|
175
|
-
const _depthOnlyFrag = /* glsl */`
|
|
176
|
-
varying float vViewDepth;
|
|
177
|
-
void main() { gl_FragColor = vec4(0.0, 0.0, 0.0, vViewDepth); }
|
|
178
|
-
`
|
|
179
|
-
function _makeDepthOnlyMaterial() {
|
|
180
|
-
return new THREE.ShaderMaterial({ vertexShader: _depthOnlyVert, fragmentShader: _depthOnlyFrag, side: THREE.DoubleSide })
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export class HalfResTransparents {
|
|
184
|
-
constructor(renderer, scene, camera) {
|
|
185
|
-
this.renderer = renderer
|
|
186
|
-
this.scene = scene
|
|
187
|
-
this.camera = camera
|
|
188
|
-
this._w = 0
|
|
189
|
-
this._h = 0
|
|
190
|
-
this._built = false
|
|
191
|
-
this._hasContent = false
|
|
192
|
-
|
|
193
|
-
// Own camera clone kept in sync (near/far/fov/aspect/position/quaternion) every frame from the
|
|
194
|
-
// main camera -- a SEPARATE object (not the same camera instance) so this pass's layer mask
|
|
195
|
-
// never has to be swapped on/off the live main camera mid-frame (which would race the
|
|
196
|
-
// scene-color node if node ordering ever changed).
|
|
197
|
-
this._halfCam = camera.clone()
|
|
198
|
-
this._halfCam.layers.set(HALFRES_LAYER)
|
|
199
|
-
|
|
200
|
-
this._depthCam = camera.clone()
|
|
201
|
-
this._depthCam.layers.disableAll()
|
|
202
|
-
this._depthCam.layers.enable(0) // opaque scene only -- everything NOT tagged half-res
|
|
203
|
-
|
|
204
|
-
this._depthOnlyMat = _makeDepthOnlyMaterial()
|
|
205
|
-
|
|
206
|
-
this._quadScene = new THREE.Scene()
|
|
207
|
-
this._quadCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)
|
|
208
|
-
this._quadGeo = new THREE.PlaneGeometry(2, 2)
|
|
209
|
-
this._compositeMat = new THREE.ShaderMaterial({
|
|
210
|
-
vertexShader: _fullscreenVert,
|
|
211
|
-
fragmentShader: _compositeFrag,
|
|
212
|
-
uniforms: {
|
|
213
|
-
tColor: { value: null },
|
|
214
|
-
tHalfDepth: { value: null },
|
|
215
|
-
uHalfTexelSize: { value: new THREE.Vector2(1, 1) },
|
|
216
|
-
// Depth-delta scale (metres) at which a bilateral tap's weight halves. 2m default: tight
|
|
217
|
-
// enough to reject across a thin-object discontinuity (e.g. a ~0.1-0.3m rail/post) without
|
|
218
|
-
// over-rejecting on smoothly-varying terrain/foliage depth gradients within one half-res texel
|
|
219
|
-
// footprint. Exposed as a RenderControls knob (halfResTransparentsDepthSigma) rather than
|
|
220
|
-
// hardcoded so it can be tuned per-scene without a source edit.
|
|
221
|
-
uDepthSigma: { value: 2.0 },
|
|
222
|
-
},
|
|
223
|
-
transparent: true,
|
|
224
|
-
depthTest: false,
|
|
225
|
-
depthWrite: false,
|
|
226
|
-
blending: THREE.NormalBlending,
|
|
227
|
-
})
|
|
228
|
-
this._quad = new THREE.Mesh(this._quadGeo, this._compositeMat)
|
|
229
|
-
this._quad.frustumCulled = false
|
|
230
|
-
this._quadScene.add(this._quad)
|
|
231
|
-
|
|
232
|
-
this._copyMat = new THREE.ShaderMaterial({
|
|
233
|
-
vertexShader: _fullscreenVert,
|
|
234
|
-
fragmentShader: _copyFrag,
|
|
235
|
-
uniforms: { tSrc: { value: null } },
|
|
236
|
-
depthTest: false,
|
|
237
|
-
depthWrite: false,
|
|
238
|
-
})
|
|
239
|
-
this._copyQuad = new THREE.Mesh(this._quadGeo, this._copyMat)
|
|
240
|
-
this._copyQuad.frustumCulled = false
|
|
241
|
-
this._copyScene = new THREE.Scene()
|
|
242
|
-
this._copyScene.add(this._copyQuad)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Marks an object (and, when it has one, its immediate children -- InstancedMesh2 pools are a
|
|
246
|
-
// single object so this is usually a one-call tag) to render ONLY in this half-res pass. `also0`
|
|
247
|
-
// (default false) additionally keeps layer 0 enabled -- use when the object still needs to
|
|
248
|
-
// participate in a layer-0-scoped system (raycast pick, occlusion query) that this codebase does
|
|
249
|
-
// not yet route through a custom layer; Weather.js's particle pools have no such requirement
|
|
250
|
-
// (perObjectFrustumCulled=false, no raycast target), so the default (layers.set = layer-0 OFF)
|
|
251
|
-
// is correct for them.
|
|
252
|
-
tagHalfRes(object, also0 = false) {
|
|
253
|
-
if (also0) object.layers.enable(HALFRES_LAYER)
|
|
254
|
-
else object.layers.set(HALFRES_LAYER)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
untagHalfRes(object) {
|
|
258
|
-
object.layers.set(0)
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
_ensureTargets(fullW, fullH) {
|
|
262
|
-
const scale = RenderControls.get('halfResTransparentsScale') || 0.5
|
|
263
|
-
const w = Math.max(4, Math.floor(fullW * scale))
|
|
264
|
-
const h = Math.max(4, Math.floor(fullH * scale))
|
|
265
|
-
if (this._built && w === this._w && h === this._h) return
|
|
266
|
-
this._disposeTargets()
|
|
267
|
-
this._w = w; this._h = h
|
|
268
|
-
// depthBuffer:true -- this target owns a REAL depth attachment so the transparent-layer render
|
|
269
|
-
// gets genuine GPU depth-testing against the opaque-depth pre-pass below, not a shader-side
|
|
270
|
-
// approximation. Depth is never read back (no DepthTexture attached) -- only used internally by
|
|
271
|
-
// the depth-test/depth-write state during the two render calls into this same target.
|
|
272
|
-
this._target = new THREE.WebGLRenderTarget(w, h, {
|
|
273
|
-
type: THREE.HalfFloatType,
|
|
274
|
-
format: THREE.RGBAFormat,
|
|
275
|
-
minFilter: THREE.LinearFilter,
|
|
276
|
-
magFilter: THREE.LinearFilter,
|
|
277
|
-
depthBuffer: true,
|
|
278
|
-
wrapS: THREE.ClampToEdgeWrapping,
|
|
279
|
-
wrapT: THREE.ClampToEdgeWrapping,
|
|
280
|
-
})
|
|
281
|
-
// Separate small target the depth-only pre-pass's color output (linear-depth-in-alpha, see
|
|
282
|
-
// _depthOnlyFrag) is captured INTO, distinct from _target -- _target's own color attachment gets
|
|
283
|
-
// overwritten by the transparent-layer draw in step 2 of compute(), so the depth-reject composite
|
|
284
|
-
// needs its own place to read the pre-pass's depth from. NearestFilter (not Linear): this texture
|
|
285
|
-
// is sampled at exact texel centers by the composite's own 2x2-neighborhood taps, a bilinear
|
|
286
|
-
// sample here would blur two different opaque-scene depths together before the bilateral filter
|
|
287
|
-
// ever sees them, defeating the point of a depth-aware upsample.
|
|
288
|
-
this._depthTarget = new THREE.WebGLRenderTarget(w, h, {
|
|
289
|
-
type: THREE.HalfFloatType,
|
|
290
|
-
format: THREE.RGBAFormat,
|
|
291
|
-
minFilter: THREE.NearestFilter,
|
|
292
|
-
magFilter: THREE.NearestFilter,
|
|
293
|
-
depthBuffer: false,
|
|
294
|
-
wrapS: THREE.ClampToEdgeWrapping,
|
|
295
|
-
wrapT: THREE.ClampToEdgeWrapping,
|
|
296
|
-
})
|
|
297
|
-
this._compositeMat.uniforms.uHalfTexelSize.value.set(1 / w, 1 / h)
|
|
298
|
-
this._built = true
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
_disposeTargets() {
|
|
302
|
-
if (this._target) this._target.dispose()
|
|
303
|
-
if (this._depthTarget) this._depthTarget.dispose()
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
_syncCam(src, dst) {
|
|
307
|
-
dst.near = src.near; dst.far = src.far
|
|
308
|
-
if (src.fov !== undefined) dst.fov = src.fov
|
|
309
|
-
if (src.aspect !== undefined) dst.aspect = src.aspect
|
|
310
|
-
dst.position.copy(src.position)
|
|
311
|
-
dst.quaternion.copy(src.quaternion)
|
|
312
|
-
dst.updateProjectionMatrix()
|
|
313
|
-
dst.updateMatrixWorld(true)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Renders the half-res-layer content (opaque-depth pre-pass + transparent-layer draw) into the
|
|
317
|
-
// offscreen target. Does not composite -- callers read this._target.texture and composite
|
|
318
|
-
// explicitly (mirrors SSAO/Bloom's compute()/composite() split, one RenderGraph node per resource).
|
|
319
|
-
compute() {
|
|
320
|
-
const size = new THREE.Vector2()
|
|
321
|
-
this.renderer.getSize(size)
|
|
322
|
-
if (size.x <= 0 || size.y <= 0) { this._hasContent = false; return }
|
|
323
|
-
this._ensureTargets(size.x, size.y)
|
|
324
|
-
this._syncCam(this.camera, this._halfCam)
|
|
325
|
-
this._syncCam(this.camera, this._depthCam)
|
|
326
|
-
|
|
327
|
-
const prevTarget = this.renderer.getRenderTarget()
|
|
328
|
-
const prevAutoClear = this.renderer.autoClear
|
|
329
|
-
const prevClearColor = new THREE.Color()
|
|
330
|
-
this.renderer.getClearColor(prevClearColor)
|
|
331
|
-
const prevClearAlpha = this.renderer.getClearAlpha()
|
|
332
|
-
|
|
333
|
-
this.renderer.setRenderTarget(this._target)
|
|
334
|
-
this.renderer.autoClear = true
|
|
335
|
-
this.renderer.setClearColor(0x000000, 0)
|
|
336
|
-
this.renderer.clear(true, true, false)
|
|
337
|
-
|
|
338
|
-
// 1. Opaque-depth pre-pass: draw the NON-half-res scene through the shared depth-only override
|
|
339
|
-
// material (see header's OVERRIDE MATERIAL note -- NOT each object's own material, and NOT a
|
|
340
|
-
// colorMask trick), so the depth buffer is populated -- gives the transparent-layer draw below
|
|
341
|
-
// real occlusion against nearer walls/terrain-proxy geometry without a second full-color render
|
|
342
|
-
// and without the per-object program churn that broke InstancedMesh2's texture-bind cache when
|
|
343
|
-
// this pre-pass used to render through real materials. The color output now ALSO encodes linear
|
|
344
|
-
// view-space depth (see _depthOnlyFrag) -- read below by the blit step for the bilateral
|
|
345
|
-
// composite's depth-reject reference.
|
|
346
|
-
const prevOverride = this.scene.overrideMaterial
|
|
347
|
-
this.renderer.autoClear = false
|
|
348
|
-
this.scene.overrideMaterial = this._depthOnlyMat
|
|
349
|
-
this.renderer.render(this.scene, this._depthCam)
|
|
350
|
-
this.scene.overrideMaterial = prevOverride
|
|
351
|
-
|
|
352
|
-
// 1.5. Blit _target's just-rendered depth-encoded color into the separate _depthTarget, BEFORE
|
|
353
|
-
// step 2 overwrites _target's color with the transparent draw -- one small nearest-filtered
|
|
354
|
-
// fullscreen-quad copy at half-res, cheap relative to the scene draw in step 1 it depends on.
|
|
355
|
-
this.renderer.setRenderTarget(this._depthTarget)
|
|
356
|
-
this._copyMat.uniforms.tSrc.value = this._target.texture
|
|
357
|
-
this.renderer.render(this._copyScene, this._quadCamera)
|
|
358
|
-
this.renderer.setRenderTarget(this._target)
|
|
359
|
-
|
|
360
|
-
// 2. Transparent/particle layer: real depth-test against the pre-pass above, normal blending,
|
|
361
|
-
// depthWrite left as each material already configures it (Weather.js's particle materials
|
|
362
|
-
// already set depthWrite:false, matching how they draw in the full-res path today).
|
|
363
|
-
this.renderer.render(this.scene, this._halfCam)
|
|
364
|
-
|
|
365
|
-
this.renderer.setRenderTarget(prevTarget)
|
|
366
|
-
this.renderer.autoClear = prevAutoClear
|
|
367
|
-
this.renderer.setClearColor(prevClearColor, prevClearAlpha)
|
|
368
|
-
this._hasContent = true
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Composites the half-res target back onto whatever is currently bound as the render target (the
|
|
372
|
-
// canvas, called from the RenderGraph composite node after scene-color).
|
|
373
|
-
composite() {
|
|
374
|
-
if (!this._hasContent || !this._target) return
|
|
375
|
-
this._compositeMat.uniforms.tColor.value = this._target.texture
|
|
376
|
-
this._compositeMat.uniforms.tHalfDepth.value = this._depthTarget.texture
|
|
377
|
-
this._compositeMat.uniforms.uDepthSigma.value = RenderControls.get('halfResTransparentsDepthSigma') ?? 2.0
|
|
378
|
-
const prevAutoClear = this.renderer.autoClear
|
|
379
|
-
this.renderer.autoClear = false
|
|
380
|
-
this.renderer.render(this._quadScene, this._quadCamera)
|
|
381
|
-
this.renderer.autoClear = prevAutoClear
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
dispose() {
|
|
385
|
-
this._disposeTargets()
|
|
386
|
-
this._compositeMat.dispose()
|
|
387
|
-
this._depthOnlyMat.dispose()
|
|
388
|
-
this._copyMat.dispose()
|
|
389
|
-
this._quadGeo.dispose()
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// RenderGraph nodes -- declared-resource, gated behind RenderControls('halfResTransparents'),
|
|
394
|
-
// composited AFTER scene-color (same position as SSAO/Bloom). ctx.halfResTransparents is a lazily-
|
|
395
|
-
// built instance (see installHalfResTransparents) so a session that never enables the flag pays
|
|
396
|
-
// zero construction cost.
|
|
397
|
-
//
|
|
398
|
-
// CAMERA LAYER GATING: scene-color (RenderGraph.nodes.js) renders with `ctx.camera` UNCHANGED --
|
|
399
|
-
// when this pass is OFF, ctx.camera's layer mask must include layer 0 only (THREE's default,
|
|
400
|
-
// nothing tagged HALFRES_LAYER is ever added unless installHalfResTransparents' caller tags it), so
|
|
401
|
-
// half-res-tagged objects simply do not exist as far as the main camera/scene-color is concerned
|
|
402
|
-
// UNLESS the pass is enabled, in which case `hrt-gate-main-camera` (below) disables HALFRES_LAYER on
|
|
403
|
-
// the real ctx.camera so scene-color skips it (this pass draws it instead) -- and re-enables it the
|
|
404
|
-
// moment the flag is turned off so a live toggle never permanently hides tagged content.
|
|
405
|
-
export function buildHalfResTransparentsNodes() {
|
|
406
|
-
return [
|
|
407
|
-
{
|
|
408
|
-
id: 'hrt-gate-main-camera',
|
|
409
|
-
reads: [],
|
|
410
|
-
writes: ['hrtGated'],
|
|
411
|
-
// hrtGated is a pure on/off stamp -- no other node reads it; the real effect is entirely
|
|
412
|
-
// external (ctx.camera.layers enable/disable state consumed by THREE's own render() call in
|
|
413
|
-
// scene-color, not by another RenderGraph node's declared `reads`). See RenderGraph.js's NODE
|
|
414
|
-
// CONTRACT `terminal` doc.
|
|
415
|
-
terminal: true,
|
|
416
|
-
shouldRun: ctx => !!ctx.halfResTransparents,
|
|
417
|
-
run(ctx) {
|
|
418
|
-
const on = RenderControls.get('halfResTransparents') === true
|
|
419
|
-
if (on) ctx.camera.layers.disable(HALFRES_LAYER)
|
|
420
|
-
else ctx.camera.layers.enable(HALFRES_LAYER)
|
|
421
|
-
ctx.res.hrtGated = on
|
|
422
|
-
},
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
id: 'hrt-compute',
|
|
426
|
-
reads: ['sceneColor'],
|
|
427
|
-
writes: ['hrtComputed'],
|
|
428
|
-
shouldRun: ctx => RenderControls.get('halfResTransparents') === true && !!ctx.halfResTransparents,
|
|
429
|
-
run(ctx) {
|
|
430
|
-
ctx.halfResTransparents.compute()
|
|
431
|
-
ctx.res.hrtComputed = ctx.frameId
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
|
-
{
|
|
435
|
-
id: 'hrt-composite',
|
|
436
|
-
reads: ['hrtComputed'],
|
|
437
|
-
writes: ['hrtComposited'],
|
|
438
|
-
targets: { hrtComposited: 'canvas' },
|
|
439
|
-
shouldRun: ctx => RenderControls.get('halfResTransparents') === true && !!ctx.halfResTransparents && ctx.res.hrtComputed === ctx.frameId,
|
|
440
|
-
run(ctx) {
|
|
441
|
-
ctx.halfResTransparents.composite()
|
|
442
|
-
ctx.res.hrtComposited = ctx.frameId
|
|
443
|
-
},
|
|
444
|
-
},
|
|
445
|
-
]
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Lazy installer: constructs GPU resources on first call and stashes the instance on
|
|
449
|
-
// ctx.halfResTransparents. NAMING TRAP AVOIDED (same discipline as SSAO/Bloom): the debug instance
|
|
450
|
-
// handle is window.__halfResTransparentsDebug, NEVER window.__halfResTransparents -- RenderControls
|
|
451
|
-
// mirrors the boolean flag onto that exact global name.
|
|
452
|
-
export function installHalfResTransparents(ctx, renderer, scene, camera) {
|
|
453
|
-
if (!ctx.halfResTransparents) ctx.halfResTransparents = new HalfResTransparents(renderer, scene, camera)
|
|
454
|
-
if (typeof window !== 'undefined') window.__halfResTransparentsDebug = ctx.halfResTransparents
|
|
455
|
-
return ctx.halfResTransparents
|
|
456
|
-
}
|