@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.
- package/CHANGELOG.md +41 -0
- package/README.md +1 -1
- package/build/index.cjs +1 -1
- package/build/index.cjs.map +1 -1
- package/build/index.js +580 -598
- package/build/index.js.map +1 -1
- package/build/r3f.cjs +1 -1
- package/build/r3f.js +1 -1
- package/build/shared2.cjs +1 -1
- package/build/shared2.cjs.map +1 -1
- package/build/shared2.js +27 -212
- package/build/shared2.js.map +1 -1
- package/build/shared3.cjs +1 -1
- package/build/shared3.cjs.map +1 -1
- package/build/shared3.js +213 -8
- package/build/shared3.js.map +1 -1
- package/build/webgpu.cjs +6 -1
- package/build/webgpu.cjs.map +1 -1
- package/build/webgpu.js +1111 -768
- package/build/webgpu.js.map +1 -1
- package/package.json +1 -1
- package/src/EllipsoidGeometry.ts +1 -1
- package/src/PointOfView.ts +12 -5
- package/src/STBNLoader.ts +41 -19
- package/src/unrollLoops.ts +1 -1
- package/src/webgpu/CascadedShadowMapsNode.ts +48 -0
- package/src/webgpu/DualMipmapFilterNode.ts +8 -4
- package/src/webgpu/FilterNode.ts +5 -3
- package/src/webgpu/FnLayout.ts +17 -16
- package/src/webgpu/HighpVelocityNode.ts +9 -4
- package/src/webgpu/LensFlareNode.ts +12 -16
- package/src/webgpu/LensGlareNode.ts +28 -32
- package/src/webgpu/LensHaloNode.ts +2 -1
- package/src/webgpu/OutputTexture3DNode.ts +10 -0
- package/src/webgpu/OutputTextureNode.ts +10 -0
- package/src/webgpu/STBNTextureNode.ts +58 -0
- package/src/webgpu/ScreenSpaceShadowNode.ts +685 -0
- package/src/webgpu/SeparableFilterNode.ts +8 -5
- package/src/webgpu/SingleFilterNode.ts +5 -2
- package/src/webgpu/StorageTexture3DNode.ts +30 -0
- package/src/webgpu/TemporalAntialiasNode.ts +178 -137
- package/src/webgpu/accessors.ts +75 -36
- package/src/webgpu/debug.ts +38 -47
- package/src/webgpu/events.ts +18 -0
- package/src/webgpu/index.ts +5 -1
- package/src/webgpu/math.ts +116 -15
- package/src/webgpu/sampling.ts +39 -5
- package/src/webgpu/transformations.ts +71 -44
- package/types/PointOfView.d.ts +1 -1
- package/types/STBNLoader.d.ts +3 -4
- package/types/webgpu/CascadedShadowMapsNode.d.ts +13 -0
- package/types/webgpu/DualMipmapFilterNode.d.ts +1 -2
- package/types/webgpu/FnLayout.d.ts +4 -4
- package/types/webgpu/HighpVelocityNode.d.ts +1 -0
- package/types/webgpu/LensFlareNode.d.ts +2 -3
- package/types/webgpu/LensGlareNode.d.ts +1 -1
- package/types/webgpu/STBNTextureNode.d.ts +9 -0
- package/types/webgpu/ScreenSpaceShadowNode.d.ts +33 -0
- package/types/webgpu/SeparableFilterNode.d.ts +2 -3
- package/types/webgpu/SingleFilterNode.d.ts +1 -2
- package/types/webgpu/StorageTexture3DNode.d.ts +9 -0
- package/types/webgpu/TemporalAntialiasNode.d.ts +9 -10
- package/types/webgpu/accessors.d.ts +9 -8
- package/types/webgpu/debug.d.ts +4 -3
- package/types/webgpu/events.d.ts +3 -0
- package/types/webgpu/index.d.ts +5 -1
- package/types/webgpu/math.d.ts +3 -0
- package/types/webgpu/sampling.d.ts +2 -1
- package/types/webgpu/transformations.d.ts +7 -10
- package/src/webgpu/RTTextureNode.ts +0 -130
- 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 {
|
|
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(
|
|
28
|
-
protected readonly direction = uniform(
|
|
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('
|
|
34
|
-
this.verticalRT = this.createRenderTarget('
|
|
36
|
+
this.horizontalRT = this.createRenderTarget('horizontal')
|
|
37
|
+
this.verticalRT = this.createRenderTarget('vertical')
|
|
35
38
|
this.outputTexture = this.verticalRT.texture
|
|
36
39
|
}
|
|
37
40
|
|