@takram/three-geospatial 0.7.1 → 0.9.0

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +1 -1
  3. package/build/index.cjs +1 -1
  4. package/build/index.cjs.map +1 -1
  5. package/build/index.js +580 -598
  6. package/build/index.js.map +1 -1
  7. package/build/r3f.cjs +1 -1
  8. package/build/r3f.js +1 -1
  9. package/build/shared2.cjs +1 -1
  10. package/build/shared2.cjs.map +1 -1
  11. package/build/shared2.js +27 -212
  12. package/build/shared2.js.map +1 -1
  13. package/build/shared3.cjs +1 -1
  14. package/build/shared3.cjs.map +1 -1
  15. package/build/shared3.js +213 -8
  16. package/build/shared3.js.map +1 -1
  17. package/build/webgpu.cjs +6 -1
  18. package/build/webgpu.cjs.map +1 -1
  19. package/build/webgpu.js +1111 -768
  20. package/build/webgpu.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/EllipsoidGeometry.ts +1 -1
  23. package/src/PointOfView.ts +12 -5
  24. package/src/STBNLoader.ts +41 -19
  25. package/src/unrollLoops.ts +1 -1
  26. package/src/webgpu/CascadedShadowMapsNode.ts +48 -0
  27. package/src/webgpu/DualMipmapFilterNode.ts +8 -4
  28. package/src/webgpu/FilterNode.ts +5 -3
  29. package/src/webgpu/FnLayout.ts +17 -16
  30. package/src/webgpu/HighpVelocityNode.ts +9 -4
  31. package/src/webgpu/LensFlareNode.ts +12 -16
  32. package/src/webgpu/LensGlareNode.ts +28 -32
  33. package/src/webgpu/LensHaloNode.ts +2 -1
  34. package/src/webgpu/OutputTexture3DNode.ts +10 -0
  35. package/src/webgpu/OutputTextureNode.ts +10 -0
  36. package/src/webgpu/STBNTextureNode.ts +58 -0
  37. package/src/webgpu/ScreenSpaceShadowNode.ts +685 -0
  38. package/src/webgpu/SeparableFilterNode.ts +8 -5
  39. package/src/webgpu/SingleFilterNode.ts +5 -2
  40. package/src/webgpu/StorageTexture3DNode.ts +30 -0
  41. package/src/webgpu/TemporalAntialiasNode.ts +178 -137
  42. package/src/webgpu/accessors.ts +75 -36
  43. package/src/webgpu/debug.ts +38 -47
  44. package/src/webgpu/events.ts +18 -0
  45. package/src/webgpu/index.ts +5 -1
  46. package/src/webgpu/math.ts +116 -15
  47. package/src/webgpu/sampling.ts +39 -5
  48. package/src/webgpu/transformations.ts +71 -44
  49. package/types/PointOfView.d.ts +1 -1
  50. package/types/STBNLoader.d.ts +3 -4
  51. package/types/webgpu/CascadedShadowMapsNode.d.ts +13 -0
  52. package/types/webgpu/DualMipmapFilterNode.d.ts +1 -2
  53. package/types/webgpu/FnLayout.d.ts +4 -4
  54. package/types/webgpu/HighpVelocityNode.d.ts +1 -0
  55. package/types/webgpu/LensFlareNode.d.ts +2 -3
  56. package/types/webgpu/LensGlareNode.d.ts +1 -1
  57. package/types/webgpu/STBNTextureNode.d.ts +9 -0
  58. package/types/webgpu/ScreenSpaceShadowNode.d.ts +33 -0
  59. package/types/webgpu/SeparableFilterNode.d.ts +2 -3
  60. package/types/webgpu/SingleFilterNode.d.ts +1 -2
  61. package/types/webgpu/StorageTexture3DNode.d.ts +9 -0
  62. package/types/webgpu/TemporalAntialiasNode.d.ts +9 -10
  63. package/types/webgpu/accessors.d.ts +9 -8
  64. package/types/webgpu/debug.d.ts +4 -3
  65. package/types/webgpu/events.d.ts +3 -0
  66. package/types/webgpu/index.d.ts +5 -1
  67. package/types/webgpu/math.d.ts +3 -0
  68. package/types/webgpu/sampling.d.ts +2 -1
  69. package/types/webgpu/transformations.d.ts +7 -10
  70. package/src/webgpu/RTTextureNode.ts +0 -130
  71. package/types/webgpu/RTTextureNode.d.ts +0 -22
@@ -0,0 +1,685 @@
1
+ // Based on Bend Studio's technique: https://www.bendstudio.com/blog/inside-bend-screen-space-shadows/
2
+
3
+ /**
4
+ * Copyright 2023 Sony Interactive Entertainment.
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
7
+ * use this file except in compliance with the License. You may obtain a copy of
8
+ * the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
+ * License for the specific language governing permissions and limitations under
16
+ * the License.
17
+ *
18
+ * Modified from the original source code.
19
+ */
20
+
21
+ import {
22
+ LinearFilter,
23
+ Matrix4,
24
+ NoColorSpace,
25
+ Vector2,
26
+ Vector3,
27
+ Vector4,
28
+ type Camera,
29
+ type DirectionalLight
30
+ } from 'three'
31
+ import { hash } from 'three/src/nodes/core/NodeUtils.js'
32
+ import {
33
+ abs,
34
+ and,
35
+ float,
36
+ Fn,
37
+ greaterThan,
38
+ int,
39
+ invocationLocalIndex,
40
+ ivec2,
41
+ min,
42
+ mix,
43
+ textureStore,
44
+ uniform,
45
+ vec2,
46
+ vec4,
47
+ workgroupArray,
48
+ workgroupBarrier,
49
+ workgroupId
50
+ } from 'three/tsl'
51
+ import {
52
+ NodeUpdateType,
53
+ StorageTexture,
54
+ TempNode,
55
+ type ComputeNode,
56
+ type NodeBuilder,
57
+ type NodeFrame,
58
+ type TextureNode
59
+ } from 'three/webgpu'
60
+
61
+ import { cameraFar, cameraNear } from './accessors'
62
+ import type { Node } from './node'
63
+ import { outputTexture } from './OutputTextureNode'
64
+ import { logarithmicToPerspectiveDepth } from './transformations'
65
+
66
+ // Workgroup size of the compute shader running this code.
67
+ const GROUP_SIZE = 64
68
+
69
+ function toDispatchIndex(value: number): number {
70
+ return Math.floor(Math.max(0, value) / GROUP_SIZE)
71
+ }
72
+
73
+ class Dispatch {
74
+ readonly size = [0, 0, 0]
75
+ readonly offset = { x: 0, y: 0 }
76
+
77
+ copy(other: Dispatch): this {
78
+ ;[this.size[0], this.size[1], this.size[2]] = other.size
79
+ this.offset.x = other.offset.x
80
+ this.offset.y = other.offset.y
81
+ return this
82
+ }
83
+ }
84
+
85
+ const vector3Scratch = /*#__PURE__*/ new Vector3()
86
+ const vector4Scratch = /*#__PURE__*/ new Vector4()
87
+ const sizeScratch = /*#__PURE__*/ new Vector2()
88
+ const matrixScratch = /*#__PURE__*/ new Matrix4()
89
+
90
+ export class ScreenSpaceShadowNode extends TempNode {
91
+ override get type(): string {
92
+ return 'ScreenSpaceShadowNode'
93
+ }
94
+
95
+ depthNode: TextureNode
96
+ camera: Camera
97
+ mainLight: DirectionalLight
98
+
99
+ sampleCount = 60
100
+ hardShadowSamples = 4
101
+ fadeOutSamples = 8
102
+
103
+ readonly outputTexture: StorageTexture
104
+ private readonly textureNode: TextureNode
105
+
106
+ thickness = uniform(0.005)
107
+ shadowContrast = uniform(4)
108
+ shadowIntensity = uniform(1)
109
+ bilinearThreshold = uniform(0.02)
110
+
111
+ // xy: Screen coordinate
112
+ // z: Normalized Z
113
+ // w: Direction sign
114
+ private readonly lightCoordinate = uniform('vec4')
115
+ private readonly dispatchOffset = uniform('ivec2')
116
+ private readonly dispatchIndex = uniform(0)
117
+
118
+ private readonly dispatches: readonly Dispatch[] = Array.from(
119
+ // Populate the max number of dispatches
120
+ { length: 8 },
121
+ () => new Dispatch()
122
+ )
123
+ private dispatchCount = 0
124
+
125
+ private readonly computeNode: ComputeNode
126
+
127
+ constructor(
128
+ depthNode: TextureNode,
129
+ camera: Camera,
130
+ mainLight: DirectionalLight
131
+ ) {
132
+ super('float')
133
+ this.updateBeforeType = NodeUpdateType.FRAME
134
+
135
+ this.depthNode = depthNode
136
+ this.camera = camera
137
+ this.mainLight = mainLight
138
+
139
+ const texture = new StorageTexture(1, 1)
140
+ texture.colorSpace = NoColorSpace
141
+ texture.minFilter = LinearFilter
142
+ texture.magFilter = LinearFilter
143
+ texture.generateMipmaps = false
144
+ texture.name = 'ScreenSpaceShadow'
145
+
146
+ this.outputTexture = texture
147
+ this.textureNode = outputTexture(this, texture)
148
+ this.computeNode = this.createComputeNode()
149
+ }
150
+
151
+ override customCacheKey(): number {
152
+ return hash(
153
+ this.camera?.id ?? -1,
154
+ this.mainLight?.id ?? -1,
155
+ this.sampleCount,
156
+ this.hardShadowSamples,
157
+ this.fadeOutSamples
158
+ )
159
+ }
160
+
161
+ getTextureNode(): TextureNode {
162
+ return this.textureNode
163
+ }
164
+
165
+ setSize(width: number, height: number): this {
166
+ const { outputTexture } = this
167
+ if (width !== outputTexture.width || height !== outputTexture.height) {
168
+ outputTexture.setSize(width, height, 0)
169
+ outputTexture.needsUpdate = true
170
+ }
171
+ return this
172
+ }
173
+
174
+ override updateBefore(frame: NodeFrame): void {
175
+ const { renderer } = frame
176
+ if (renderer == null) {
177
+ return
178
+ }
179
+
180
+ const size = renderer.getDrawingBufferSize(sizeScratch)
181
+ this.setSize(size.width, size.height)
182
+
183
+ const { camera, mainLight } = this
184
+
185
+ // Compute light projection and update dispatch list.
186
+ const viewProjection = matrixScratch.multiplyMatrices(
187
+ camera.projectionMatrix,
188
+ camera.matrixWorldInverse
189
+ )
190
+ const direction = vector3Scratch
191
+ .copy(mainLight.position)
192
+ .sub(mainLight.target.position)
193
+ .normalize()
194
+ const lightProjection = vector4Scratch
195
+ .set(direction.x, direction.y, direction.z, 0)
196
+ .applyMatrix4(viewProjection)
197
+
198
+ this.updateDispatchList(lightProjection, size)
199
+
200
+ for (let index = 0; index < this.dispatchCount; ++index) {
201
+ const dispatch = this.dispatches[index]
202
+ this.dispatchOffset.value.set(dispatch.offset.x, dispatch.offset.y)
203
+ this.dispatchIndex.value = index
204
+ void renderer.compute(this.computeNode, dispatch.size)
205
+ }
206
+ }
207
+
208
+ // See bend_sss_cpu.h
209
+ private updateDispatchList(
210
+ lightProjection: Vector4,
211
+ { width, height }: Vector2
212
+ ): void {
213
+ // Floating point division in the shader has a practical limit for precision
214
+ // when the light is *very* far off screen (~1m pixels+).
215
+ // So when computing the light XY coordinate, use an adjusted w value to
216
+ // handle these extreme values.
217
+ let lightW = lightProjection.w
218
+ const fpLimit = 0.000002 * GROUP_SIZE
219
+ if (lightW >= 0 && lightW < fpLimit) {
220
+ lightW = fpLimit
221
+ } else if (lightW < 0 && lightW > -fpLimit) {
222
+ lightW = -fpLimit
223
+ }
224
+
225
+ // Need precise XY pixel coordinates of the light.
226
+ this.lightCoordinate.value.set(
227
+ ((lightProjection.x / lightW) * 0.5 + 0.5) * width,
228
+ ((lightProjection.y / lightW) * -0.5 + 0.5) * height,
229
+ lightProjection.w === 0 ? 0 : lightProjection.z / lightProjection.w,
230
+ lightProjection.w > 0 ? 1 : -1
231
+ )
232
+
233
+ const lightX = Math.round(this.lightCoordinate.value.x)
234
+ const lightY = Math.round(this.lightCoordinate.value.y)
235
+
236
+ // Make the bounds inclusive, relative to the light.
237
+ const left = -lightX
238
+ const bottom = -(height - lightY)
239
+ const right = width - lightX
240
+ const top = lightY
241
+
242
+ // Process 4 quadrants around the light center.
243
+ // They each form a rectangle with one corner on the light XY coordinate.
244
+ // If the rectangle isn't square, it will need breaking in two on the larger
245
+ // axis 0 = bottom left, 1 = bottom right, 2 = top left, 3 = top right.
246
+ let dispatchCount = 0
247
+ for (let q = 0; q < 4; ++q) {
248
+ // Quads 0 and 3 needs to be +1 vertically, 1 and 2 need to be +1
249
+ // horizontally.
250
+ const vertical = q === 0 || q === 3
251
+ const qx = (q & 1) > 0
252
+ const qy = (q & 2) > 0
253
+
254
+ // Bounds relative to the quadrant.
255
+ const x1 = toDispatchIndex(qx ? left : -right)
256
+ const y1 = toDispatchIndex(qy ? bottom : -top)
257
+ const padX = GROUP_SIZE * (vertical ? 1 : 2) - 1
258
+ const padY = GROUP_SIZE * (vertical ? 2 : 1) - 1
259
+ const x2 = toDispatchIndex((qx ? right : -left) + padX)
260
+ const y2 = toDispatchIndex((qy ? top : -bottom) + padY)
261
+
262
+ if (x2 - x1 > 0 && y2 - y1 > 0) {
263
+ const biasX = q === 2 || q === 3 ? 1 : 0
264
+ const biasY = q === 1 || q === 3 ? 1 : 0
265
+
266
+ const dispatch1 = this.dispatches[dispatchCount++]
267
+ dispatch1.size[0] = GROUP_SIZE
268
+ dispatch1.size[1] = x2 - x1
269
+ dispatch1.size[2] = y2 - y1
270
+ dispatch1.offset.x = (qx ? x1 : -x2) + biasX
271
+ dispatch1.offset.y = (qy ? -y2 : y1) + biasY
272
+
273
+ // We want the far corner of this quadrant relative to the light,
274
+ // as we need to know where the diagonal light ray intersects with the
275
+ // edge of the bounds.
276
+ let axisDelta: number
277
+ if (q === 0) {
278
+ axisDelta = left - bottom
279
+ } else if (q === 1) {
280
+ axisDelta = right + bottom
281
+ } else if (q === 2) {
282
+ axisDelta = -left - top
283
+ } else {
284
+ axisDelta = -right + top
285
+ }
286
+
287
+ axisDelta = ((axisDelta + GROUP_SIZE - 1) / GROUP_SIZE) | 0
288
+
289
+ if (axisDelta > 0) {
290
+ // Take copy of current dispatch
291
+ const dispatch2 = this.dispatches[dispatchCount++].copy(dispatch1)
292
+
293
+ if (q === 0) {
294
+ // Split on Y, split becomes -1 larger on x.
295
+ dispatch2.size[2] = Math.min(dispatch1.size[2], axisDelta)
296
+ dispatch1.size[2] -= dispatch2.size[2]
297
+ dispatch2.offset.y = dispatch1.offset.y + dispatch1.size[2]
298
+ dispatch2.offset.x -= 1
299
+ dispatch2.size[1] += 1
300
+ } else if (q === 1) {
301
+ // Split on X, split becomes +1 larger on y.
302
+ dispatch2.size[1] = Math.min(dispatch1.size[1], axisDelta)
303
+ dispatch1.size[1] -= dispatch2.size[1]
304
+ dispatch2.offset.x = dispatch1.offset.x + dispatch1.size[1]
305
+ dispatch2.size[2] += 1
306
+ } else if (q === 2) {
307
+ // Split on X, split becomes -1 larger on y.
308
+ dispatch2.size[1] = Math.min(dispatch1.size[1], axisDelta)
309
+ dispatch1.size[1] -= dispatch2.size[1]
310
+ dispatch1.offset.x += dispatch2.size[1]
311
+ dispatch2.size[2] += 1
312
+ dispatch2.offset.y -= 1
313
+ } else if (q === 3) {
314
+ // Split on Y, split becomes +1 larger on x.
315
+ dispatch2.size[2] = Math.min(dispatch1.size[2], axisDelta)
316
+ dispatch1.size[2] -= dispatch2.size[2]
317
+ dispatch1.offset.y += dispatch2.size[2]
318
+ ++dispatch2.size[1]
319
+ }
320
+
321
+ // Remove if too small.
322
+ if (dispatch2.size[1] <= 0 || dispatch2.size[2] <= 0) {
323
+ dispatch2.copy(this.dispatches[--dispatchCount])
324
+ }
325
+ if (dispatch1.size[1] <= 0 || dispatch1.size[2] <= 0) {
326
+ dispatch1.copy(this.dispatches[--dispatchCount])
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ // Scale the shader values by the workgroup size, the shader expects this.
333
+ for (let i = 0; i < dispatchCount; ++i) {
334
+ const dispatch = this.dispatches[i]
335
+ dispatch.offset.x *= GROUP_SIZE
336
+ dispatch.offset.y *= GROUP_SIZE
337
+ }
338
+ this.dispatchCount = dispatchCount
339
+ }
340
+
341
+ // See bend_sss_gpu.h
342
+ private createComputeNode(): ComputeNode {
343
+ const {
344
+ depthNode,
345
+ camera,
346
+ sampleCount,
347
+ hardShadowSamples,
348
+ fadeOutSamples,
349
+ outputTexture,
350
+ thickness,
351
+ shadowContrast,
352
+ shadowIntensity,
353
+ bilinearThreshold,
354
+ lightCoordinate,
355
+ dispatchOffset
356
+ } = this
357
+
358
+ // Number of bilinear sample reads performed per-thread.
359
+ const readCount = Math.floor(sampleCount / GROUP_SIZE) + 2
360
+ const workgroupBuffer = workgroupArray('float', readCount * GROUP_SIZE)
361
+
362
+ // Gets the start pixel coordinates for the pixels in the workgroup.
363
+ // Also returns the delta to get to the next pixel after GROUP_SIZE pixels
364
+ // along the ray.
365
+ const getWorkgroupExtents = (): {
366
+ pixelXY: Node<'vec2'>
367
+ pixelDistance: Node<'float'>
368
+ xyDelta: Node<'vec2'>
369
+ xAxisMajor: Node<'bool'>
370
+ } => {
371
+ const xy = ivec2(workgroupId.yz)
372
+ .mul(GROUP_SIZE)
373
+ .add(dispatchOffset)
374
+ .toConst()
375
+
376
+ // Integer light position / fractional component
377
+ const lightXY = lightCoordinate.xy.floor().add(0.5).toConst()
378
+ const lightXYFraction = lightCoordinate.xy.sub(lightXY).toConst()
379
+ const reverseDirection = lightCoordinate.w.greaterThan(0)
380
+
381
+ const signXY = ivec2(xy.sign()).toConst()
382
+
383
+ const horizontal = abs(xy.x.add(signXY.y))
384
+ .lessThan(abs(xy.y.sub(signXY.x)))
385
+ .toConst()
386
+
387
+ const axis = ivec2(
388
+ horizontal.select(signXY.y, 0),
389
+ horizontal.select(0, signXY.x.negate())
390
+ )
391
+
392
+ // Apply workgroup offset along the axis
393
+ const xyF = vec2(axis.mul(workgroupId.x).add(xy)).toConst()
394
+
395
+ // For interpolation to the light center, we only really care about the
396
+ // larger of the two axis.
397
+ const xAxisMajor = abs(xyF.x).greaterThan(abs(xyF.y)).toConst()
398
+ const majorAxis = xAxisMajor.select(xyF.x, xyF.y).toConst()
399
+
400
+ const majorAxisStart = majorAxis.abs().toConst()
401
+ const majorAxisEnd = majorAxisStart.sub(GROUP_SIZE)
402
+
403
+ const maLightFrac = xAxisMajor
404
+ .select(lightXYFraction.x, lightXYFraction.y)
405
+ .toVar()
406
+ maLightFrac.assign(
407
+ majorAxis.greaterThan(0).select(maLightFrac.negate(), maLightFrac)
408
+ )
409
+
410
+ // Back in to screen direction.
411
+ const startXY = xyF.add(lightXY).toConst()
412
+
413
+ // For the very inner most ring, we need to interpolate to a pixel
414
+ // centered UV, so the UV->pixel rounding doesn't skip output pixels.
415
+ const endXY = mix(
416
+ lightCoordinate.xy,
417
+ startXY,
418
+ majorAxisEnd.add(maLightFrac).div(majorAxisStart.add(maLightFrac))
419
+ ).toConst()
420
+
421
+ // The major axis should be a round number.
422
+ const xyDelta = startXY.sub(endXY).toConst()
423
+
424
+ // Inverse the read order when reverse direction is true.
425
+ const threadStep = float(
426
+ reverseDirection.select(
427
+ invocationLocalIndex,
428
+ invocationLocalIndex.bitXor(GROUP_SIZE - 1)
429
+ )
430
+ ).toConst()
431
+
432
+ const pixelXY = mix(startXY, endXY, threadStep.div(GROUP_SIZE)).toVar()
433
+ const pixelDistance = majorAxisStart
434
+ .sub(threadStep)
435
+ .add(maLightFrac)
436
+ .toConst()
437
+
438
+ return { pixelXY, pixelDistance, xyDelta, xAxisMajor }
439
+ }
440
+
441
+ return Fn(builder => {
442
+ const [nearDepth, farDepth] = builder.renderer.reversedDepthBuffer
443
+ ? [float(1), float(0)]
444
+ : [float(0), float(1)]
445
+
446
+ const loadDepth = (coord: Node<'ivec2'>): Node<'float'> => {
447
+ let depth: Node = depthNode.load(coord)
448
+ if (builder.renderer.logarithmicDepthBuffer) {
449
+ depth = logarithmicToPerspectiveDepth(
450
+ depth,
451
+ cameraNear(camera),
452
+ cameraFar(camera)
453
+ )
454
+ }
455
+ depth = depth.toConst()
456
+
457
+ // Emulate a point sampler in bend_sss_gpu.h, with Wrap Mode set to
458
+ // Clamp-To-Border-Color, and Border Color set to farDepth.
459
+ return and(
460
+ coord.greaterThanEqual(0).all(),
461
+ coord.lessThan(depthNode.size()).all()
462
+ ).select(depth, farDepth)
463
+ }
464
+
465
+ const { pixelXY, xyDelta, pixelDistance, xAxisMajor } =
466
+ getWorkgroupExtents()
467
+
468
+ const direction = lightCoordinate.w.negate()
469
+ const zSign = nearDepth.greaterThan(farDepth).select(-1, 1).toConst()
470
+
471
+ // Must save pixelXY here before modifying it.
472
+ const writeXY = ivec2(pixelXY.floor()).toConst()
473
+
474
+ let samplingDepth0!: Node<'float'>
475
+ let depthThicknessScale0!: Node<'float'>
476
+ let sampleDistance0!: Node<'float'>
477
+
478
+ for (let i = 0; i < readCount; ++i) {
479
+ // We sample depth twice per pixel per sample, and interpolate with an
480
+ // edge detect filter. Interpolation should only occur on the minor axis
481
+ // of the ray - major axis coordinates should be at pixel centers.
482
+ const readXY = ivec2(pixelXY.floor()).toConst()
483
+ const minorAxis = xAxisMajor.select(pixelXY.y, pixelXY.x)
484
+
485
+ const bias = minorAxis
486
+ .fract()
487
+ .sub(0.5)
488
+ .greaterThan(0)
489
+ .select(1, -1)
490
+ .toConst()
491
+ const bilinearOffset = ivec2(
492
+ xAxisMajor.select(0, bias),
493
+ xAxisMajor.select(bias, 0)
494
+ )
495
+
496
+ const depthCenter = loadDepth(readXY).toConst()
497
+ const depthNeighbor = loadDepth(readXY.add(bilinearOffset)).toConst()
498
+
499
+ // Depth thresholds (bilinear/shadow thickness) are based on a
500
+ // fractional ratio of the difference between sampled depth and the far
501
+ // clip depth.
502
+ const depthThicknessScale = farDepth.sub(depthCenter).abs().toConst()
503
+
504
+ // If depth variance is more than a specific threshold, then just use
505
+ // point filtering.
506
+ const usePointFilter = greaterThan(
507
+ depthCenter.sub(depthNeighbor).abs(),
508
+ depthThicknessScale.mul(bilinearThreshold)
509
+ )
510
+
511
+ // Any sample in this workgroup is possibly interpolated towards the
512
+ // bilinear sample. So we should use a shadowing depth that is further
513
+ // away, based on the difference between the two samples.
514
+ const shadowDepth = depthCenter.add(
515
+ abs(depthCenter.sub(depthNeighbor)).mul(zSign)
516
+ )
517
+
518
+ // Shadows cast from this depth.
519
+ const shadowingDepth = usePointFilter.select(depthCenter, shadowDepth)
520
+
521
+ const sampleDistance =
522
+ i === 0
523
+ ? pixelDistance
524
+ : direction
525
+ .mul(GROUP_SIZE * i)
526
+ .add(pixelDistance)
527
+ .toConst()
528
+
529
+ // Perspective correct the shadowing depth, in this space, all light
530
+ // rays are parallel.
531
+ let storedDepth: Node = shadowingDepth
532
+ .sub(lightCoordinate.z)
533
+ .div(sampleDistance)
534
+ .toConst()
535
+
536
+ if (i > 0) {
537
+ // For pixels within sampling distance of the light, it is possible
538
+ // that sampling will overshoot the light coordinate for extended
539
+ // reads. We want to ignore these samples.
540
+ storedDepth = sampleDistance
541
+ .greaterThan(0)
542
+ .select(storedDepth, 1e10)
543
+ .toConst()
544
+ }
545
+
546
+ // Store the depth values in workgroup shared memory.
547
+ workgroupBuffer
548
+ .element(invocationLocalIndex.add(GROUP_SIZE * i))
549
+ .assign(storedDepth)
550
+
551
+ if (i === 0) {
552
+ samplingDepth0 = depthCenter
553
+ depthThicknessScale0 = depthThicknessScale
554
+ sampleDistance0 = sampleDistance
555
+ }
556
+
557
+ // Iterate to the next pixel along the ray. This will be GROUP_SIZE
558
+ // pixels along the ray...
559
+ pixelXY.addAssign(xyDelta.mul(direction))
560
+ }
561
+
562
+ // Sync threads within the workgroup now workgroupDepthData is written.
563
+ workgroupBarrier()
564
+
565
+ // Perspective correct the depth.
566
+ const startDepth = samplingDepth0
567
+ .sub(lightCoordinate.z)
568
+ .div(sampleDistance0)
569
+ .toVar()
570
+
571
+ // This is the inverse of how large the shadowing window is for the
572
+ // projected sample data.
573
+ // All values in the workgroup shared memory are scaled by
574
+ // 1.0 / sample_distance, such that all light directions become parallel.
575
+ // The multiply by sample_distance[0] here is to compensate for the
576
+ // projection divide in the data.
577
+ // The 1.0 / SurfaceThickness is to adjust user selected thickness. So a
578
+ // 0.5% thickness will scale depth values from [0,1] to [0,200]. The
579
+ // shadow window is always 1 wide.
580
+ // 1.0 / depth_thickness_scale[0] is because SurfaceThickness is
581
+ // percentage of remaining depth between the sample and the far clip - not
582
+ // a percentage of the full depth range.
583
+ // The min() function is to make sure the window is a minimum width when
584
+ // very close to the light. The +direction term will bias the result so
585
+ // the pixel at the very center of the light is either fully lit or
586
+ // shadowed.
587
+ const depthScale = sampleDistance0
588
+ .add(direction)
589
+ .min(thickness.reciprocal())
590
+ .mul(sampleDistance0)
591
+ .div(depthThicknessScale0)
592
+ .toConst()
593
+
594
+ startDepth.assign(startDepth.mul(depthScale).sub(zSign))
595
+
596
+ // Start by reading the next value.
597
+ const sampleIndex = invocationLocalIndex.add(1).toConst()
598
+ const hardShadow = float(1).toVar()
599
+
600
+ // The first number of hard shadow samples, a single pixel can produce a
601
+ // full shadow:
602
+ for (let i = 0; i < hardShadowSamples; ++i) {
603
+ const depthDelta = startDepth
604
+ .sub(workgroupBuffer.element(sampleIndex.add(i)).mul(depthScale))
605
+ .abs()
606
+
607
+ // We want to find the distance of the sample that is closest to the
608
+ // reference depth.
609
+ hardShadow.assign(hardShadow.min(depthDelta))
610
+ }
611
+
612
+ const shadowValue = vec4(1).toVar()
613
+
614
+ // The main shadow samples, averaged in to a set of 4 shadow values:
615
+ for (let i = hardShadowSamples; i < sampleCount - fadeOutSamples; ++i) {
616
+ const depthDelta = startDepth
617
+ .sub(workgroupBuffer.element(sampleIndex.add(i)).mul(depthScale))
618
+ .abs()
619
+
620
+ // Do the same as the hard_shadow code above, but this will accumulate
621
+ // to 4 separate values. By using 4 values, the average shadow can be
622
+ // taken, which can help soften single-pixel shadows.
623
+ const channel = int(i & 3).toConst()
624
+ shadowValue
625
+ .element(channel)
626
+ .assign(shadowValue.element(channel).min(depthDelta))
627
+ }
628
+
629
+ // Final fade out samples:
630
+ for (let i = sampleCount - fadeOutSamples; i < sampleCount; ++i) {
631
+ const depthDelta = startDepth
632
+ .sub(workgroupBuffer.element(sampleIndex.add(i)).mul(depthScale))
633
+ .abs()
634
+
635
+ // Add the fade value to these samples.
636
+ const fadeOut =
637
+ ((i + 1 - (sampleCount - fadeOutSamples)) / (fadeOutSamples + 1)) *
638
+ 0.75
639
+
640
+ const channel = int(i & 3).toConst()
641
+ shadowValue
642
+ .element(channel)
643
+ .assign(shadowValue.element(channel).min(depthDelta.add(fadeOut)))
644
+ }
645
+
646
+ // Apply the contrast value.
647
+ // A value of 0 indicates a sample was exactly matched to the reference
648
+ // depth (and the result is fully shadowed). We want some boost to this
649
+ // range, so samples don't have to exactly match to produce a full shadow.
650
+ const contrastOffset = shadowContrast.oneMinus().toConst()
651
+ hardShadow.assign(
652
+ hardShadow.mul(shadowContrast).add(contrastOffset).saturate()
653
+ )
654
+ shadowValue.assign(
655
+ shadowValue.mul(shadowContrast).add(contrastOffset).saturate()
656
+ )
657
+
658
+ const result = float(0).toVar()
659
+
660
+ // Take the average of 4 samples, this is useful to reduce aliasing noise
661
+ // in the source depth, especially with long shadows.
662
+ result.assign(shadowValue.dot(vec4(0.25)))
663
+
664
+ // If the first samples are always producing a hard shadow, then compute
665
+ // this value separately.
666
+ result.assign(min(hardShadow, result))
667
+
668
+ // Asking the GPU to write scattered single-byte pixels isn't great,
669
+ // But thankfully the latency is hidden by all the work we're doing...
670
+ textureStore(outputTexture, writeXY, mix(1, result, shadowIntensity))
671
+ })().computeKernel([GROUP_SIZE, 1, 1])
672
+ }
673
+
674
+ override setup(builder: NodeBuilder): unknown {
675
+ return this.textureNode
676
+ }
677
+
678
+ override dispose(): void {
679
+ this.outputTexture.dispose()
680
+ }
681
+ }
682
+
683
+ export const screenSpaceShadow = (
684
+ ...args: ConstructorParameters<typeof ScreenSpaceShadowNode>
685
+ ): ScreenSpaceShadowNode => new ScreenSpaceShadowNode(...args)
@@ -1,4 +1,4 @@
1
- import { Vector2, type RenderTarget } from 'three'
1
+ import type { RenderTarget } from 'three'
2
2
  import { uniform } from 'three/tsl'
3
3
  import {
4
4
  NodeMaterial,
@@ -24,14 +24,17 @@ export abstract class SeparableFilterNode extends FilterNode {
24
24
  private readonly mesh = new QuadMesh(this.material)
25
25
  private rendererState?: RendererUtils.RendererState
26
26
 
27
- protected readonly inputTexelSize = uniform(new Vector2())
28
- protected readonly direction = uniform(new Vector2())
27
+ protected readonly inputTexelSize = uniform('vec2')
28
+ protected readonly direction = uniform('vec2')
29
29
 
30
30
  constructor(inputNode?: TextureNode | null) {
31
31
  super(inputNode)
32
+ const typeName = (this.constructor as typeof Node).type.replace(/Node$/, '')
33
+ this.material.name = typeName
34
+ this.mesh.name = typeName
32
35
 
33
- this.horizontalRT = this.createRenderTarget('Horizontal')
34
- this.verticalRT = this.createRenderTarget('Vertical')
36
+ this.horizontalRT = this.createRenderTarget('horizontal')
37
+ this.verticalRT = this.createRenderTarget('vertical')
35
38
  this.outputTexture = this.verticalRT.texture
36
39
  }
37
40