@footgun/cobalt 0.9.0 → 0.10.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 +9 -0
- package/bundle.js +91 -117
- package/examples/03-tiles/index.html +3 -104
- package/examples/06-displacement/index.html +2 -2
- package/examples/08-light/index.html +70 -10
- package/package.json +1 -1
- package/src/bloom/bloom.js +10 -7
- package/src/displacement/displacement.js +6 -10
- package/src/fb-blit/fb-blit.js +5 -4
- package/src/fb-texture/fb-texture.js +1 -2
- package/src/light/light.js +504 -69
- package/src/light/lights-buffer.js +55 -59
- package/src/light/public-api.js +3 -4
- package/src/light/texture/lights-texture-mask.js +187 -167
- package/src/primitives/primitives.js +4 -1
- package/src/scene-composite/scene-composite.js +8 -11
- package/src/sprite/public-api.js +5 -3
- package/src/sprite/sprite.js +6 -10
- package/src/sprite-hdr/public-api.js +5 -3
- package/src/sprite-hdr/sprite.js +6 -15
- package/src/spritesheet/spritesheet.js +0 -1
- package/src/tile-hdr/atlas.js +0 -1
- package/src/tile-hdr/tile.js +7 -6
- package/src/light/lights-renderer.js +0 -230
- package/src/light/texture/lights-texture-initializer.js +0 -175
- package/src/light/texture/lights-texture.js +0 -85
- package/src/light/viewport.js +0 -40
package/src/light/light.js
CHANGED
|
@@ -1,54 +1,57 @@
|
|
|
1
|
+
import * as wgpuMatrix from 'wgpu-matrix'
|
|
1
2
|
import getPreferredFormat from '../get-preferred-format.js'
|
|
2
|
-
import {
|
|
3
|
-
|
|
3
|
+
import {
|
|
4
|
+
createLightsBuffer,
|
|
5
|
+
destroyLightsBuffer,
|
|
6
|
+
LIGHTS_STRUCT_DEFINITION,
|
|
7
|
+
writeLightsBuffer,
|
|
8
|
+
} from './lights-buffer.js'
|
|
4
9
|
import * as publicAPI from './public-api.js'
|
|
5
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
createLightsTextureMask,
|
|
12
|
+
destroyMask,
|
|
13
|
+
setMaskLightsCount,
|
|
14
|
+
setMaskObstacles,
|
|
15
|
+
} from './texture/lights-texture-mask.js'
|
|
6
16
|
|
|
7
17
|
/**
|
|
8
18
|
* 2D lighting and Shadows
|
|
19
|
+
*
|
|
20
|
+
* Refs:
|
|
21
|
+
* in (textureView, rgba16float, read) - albedo input texture
|
|
22
|
+
* out (textureView, rgba16float, write) - lit output texture
|
|
9
23
|
*/
|
|
10
|
-
|
|
11
24
|
export default {
|
|
12
25
|
type: 'cobalt:light',
|
|
13
26
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{ name: 'in', type: 'textureView', format: 'rgba16float', access: 'read' },
|
|
17
|
-
{ name: 'out', type: 'textureView', format: 'rgba16float', access: 'write' },
|
|
18
|
-
],
|
|
19
|
-
|
|
20
|
-
// cobalt event handling functions
|
|
21
|
-
|
|
22
|
-
// @params Object cobalt renderer world object
|
|
23
|
-
// @params Object options optional data passed when initing this node
|
|
24
|
-
onInit: async function (cobalt, options = {}) {
|
|
25
|
-
return init(cobalt, options)
|
|
27
|
+
onInit: async function (cobalt, node) {
|
|
28
|
+
return init(cobalt, node)
|
|
26
29
|
},
|
|
27
30
|
|
|
28
31
|
onRun: function (cobalt, node, webGpuCommandEncoder) {
|
|
29
|
-
// do whatever you need for this node. webgpu renderpasses, etc.
|
|
30
32
|
draw(cobalt, node, webGpuCommandEncoder)
|
|
31
33
|
},
|
|
32
34
|
|
|
33
35
|
onDestroy: function (cobalt, node) {
|
|
34
|
-
// any cleanup for your node should go here (releasing textures, etc.)
|
|
35
36
|
destroy(node)
|
|
36
37
|
},
|
|
37
38
|
|
|
38
39
|
onResize: function (cobalt, node) {
|
|
39
|
-
// runs when the viewport size changes (handle resizing textures, etc.)
|
|
40
40
|
resize(cobalt, node)
|
|
41
41
|
},
|
|
42
42
|
|
|
43
43
|
onViewportPosition: function (cobalt, node) {
|
|
44
|
-
|
|
45
|
-
node.data.
|
|
44
|
+
const { viewport } = cobalt
|
|
45
|
+
node.data.viewportTopLeft = [...viewport.position]
|
|
46
|
+
node.data.invViewProjectionMatrix = computeInvertViewProjectionMatrix(
|
|
47
|
+
node.data.viewportWidth,
|
|
48
|
+
node.data.viewportHeight,
|
|
49
|
+
node.data.viewportZoom,
|
|
50
|
+
node.data.viewportTopLeft,
|
|
51
|
+
)
|
|
46
52
|
},
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
customFunctions: {
|
|
50
|
-
...publicAPI,
|
|
51
|
-
},
|
|
54
|
+
customFunctions: { ...publicAPI },
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
async function init(cobalt, node) {
|
|
@@ -57,62 +60,428 @@ async function init(cobalt, node) {
|
|
|
57
60
|
// a 2048x2048 light texture with 4 channels (rgba) with each light lighting a 256x256 region can hold 256 lights
|
|
58
61
|
const MAX_LIGHT_COUNT = 256
|
|
59
62
|
const MAX_LIGHT_SIZE = 256
|
|
60
|
-
const
|
|
63
|
+
const textureFormat = getPreferredFormat(cobalt)
|
|
64
|
+
const resolutionPerLight = MAX_LIGHT_SIZE
|
|
65
|
+
const filtering = 'nearest'
|
|
66
|
+
|
|
67
|
+
// --- lights storage buffer ---
|
|
68
|
+
const lightsBufferData = createLightsBuffer(device, MAX_LIGHT_COUNT)
|
|
69
|
+
|
|
70
|
+
// --- lights atlas texture ---
|
|
71
|
+
const cellsCount = MAX_LIGHT_COUNT / 4
|
|
72
|
+
const gridSize = {
|
|
73
|
+
x: Math.ceil(Math.sqrt(cellsCount)),
|
|
74
|
+
y: 0,
|
|
75
|
+
}
|
|
76
|
+
gridSize.y = Math.ceil(cellsCount / gridSize.x)
|
|
77
|
+
|
|
78
|
+
const lightTextureSize = {
|
|
79
|
+
width: gridSize.x * resolutionPerLight,
|
|
80
|
+
height: gridSize.y * resolutionPerLight,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const lightsTexture = device.createTexture({
|
|
84
|
+
label: 'LightsTexture texture',
|
|
85
|
+
size: [lightTextureSize.width, lightTextureSize.height],
|
|
86
|
+
format: textureFormat,
|
|
87
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const lightsTextureRenderpassDescriptor = {
|
|
91
|
+
label: 'lights-renderer render to texture renderpass',
|
|
92
|
+
colorAttachments: [
|
|
93
|
+
{
|
|
94
|
+
view: lightsTexture.createView(),
|
|
95
|
+
clearValue: [0, 0, 0, 1],
|
|
96
|
+
loadOp: 'load',
|
|
97
|
+
storeOp: 'store',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// descriptor object passed to initializer + mask (metadata only, no GPU resources)
|
|
103
|
+
const lightsTextureDescriptor = {
|
|
104
|
+
gridSize,
|
|
105
|
+
format: textureFormat,
|
|
106
|
+
sampleCount: 1,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- initializer sub-pass (radial attenuation pre-pass, baked render bundle) ---
|
|
110
|
+
const initializerShaderModule = device.createShaderModule({
|
|
111
|
+
label: 'LightsTextureInitializer shader module',
|
|
112
|
+
code: `
|
|
113
|
+
${LIGHTS_STRUCT_DEFINITION}
|
|
114
|
+
|
|
115
|
+
@group(0) @binding(0) var<storage,read> lightsBuffer: LightsBuffer;
|
|
116
|
+
|
|
117
|
+
struct VertexIn {
|
|
118
|
+
@builtin(vertex_index) vertexIndex: u32,
|
|
119
|
+
};
|
|
61
120
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
121
|
+
struct VertexOut {
|
|
122
|
+
@builtin(position) position: vec4<f32>,
|
|
123
|
+
@location(0) uv: vec2<f32>,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const cellsGridSizeU = vec2<u32>(${gridSize.x}, ${gridSize.y});
|
|
127
|
+
const cellsGridSizeF = vec2<f32>(${gridSize.x}, ${gridSize.y});
|
|
128
|
+
|
|
129
|
+
@vertex
|
|
130
|
+
fn main_vertex(in: VertexIn) -> VertexOut {
|
|
131
|
+
const corners = array<vec2<f32>, 4>(
|
|
132
|
+
vec2<f32>(-1, -1),
|
|
133
|
+
vec2<f32>(1, -1),
|
|
134
|
+
vec2<f32>(-1, 1),
|
|
135
|
+
vec2<f32>(1, 1),
|
|
136
|
+
);
|
|
137
|
+
let screenPosition = corners[in.vertexIndex];
|
|
138
|
+
|
|
139
|
+
var out: VertexOut;
|
|
140
|
+
out.position = vec4<f32>(screenPosition, 0.0, 1.0);
|
|
141
|
+
out.uv = (0.5 + 0.5 * screenPosition) * cellsGridSizeF;
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
struct FragmentOut {
|
|
146
|
+
@location(0) color: vec4<f32>,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
struct LightProperties {
|
|
150
|
+
radius: f32,
|
|
151
|
+
intensity: f32,
|
|
152
|
+
attenuationLinear: f32,
|
|
153
|
+
attenuationExp: f32,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
fn get_light_properties(lightId: u32) -> LightProperties {
|
|
157
|
+
var p: LightProperties;
|
|
158
|
+
if (lightId < lightsBuffer.count) {
|
|
159
|
+
let light = lightsBuffer.lights[lightId];
|
|
160
|
+
p.radius = light.radius;
|
|
161
|
+
p.intensity = 1.0;
|
|
162
|
+
p.attenuationLinear = light.attenuationLinear;
|
|
163
|
+
p.attenuationExp = light.attenuationExp;
|
|
164
|
+
}
|
|
165
|
+
return p;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@fragment
|
|
169
|
+
fn main_fragment(in: VertexOut) -> FragmentOut {
|
|
170
|
+
let cellId = vec2<u32>(in.uv);
|
|
171
|
+
let lightIdFrom = 4u * (cellId.x + cellId.y * cellsGridSizeU.x);
|
|
172
|
+
let p = array<LightProperties, 4>(
|
|
173
|
+
get_light_properties(lightIdFrom + 0u),
|
|
174
|
+
get_light_properties(lightIdFrom + 1u),
|
|
175
|
+
get_light_properties(lightIdFrom + 2u),
|
|
176
|
+
get_light_properties(lightIdFrom + 3u),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
let localUv = fract(in.uv);
|
|
180
|
+
let fromCenter = 2.0 * localUv - 1.0;
|
|
181
|
+
let uvDist = distance(vec2<f32>(0.0, 0.0), fromCenter);
|
|
182
|
+
let sizes = vec4<f32>(p[0].radius, p[1].radius, p[2].radius, p[3].radius);
|
|
183
|
+
let d = vec4<f32>(uvDist / sizes * f32(${MAX_LIGHT_SIZE}));
|
|
184
|
+
|
|
185
|
+
let intensities = vec4<f32>(p[0].intensity, p[1].intensity, p[2].intensity, p[3].intensity)
|
|
186
|
+
* (1.0 + step(uvDist, 0.01));
|
|
187
|
+
let attenuationL = vec4<f32>(p[0].attenuationLinear, p[1].attenuationLinear, p[2].attenuationLinear, p[3].attenuationLinear);
|
|
188
|
+
let attenuationE = vec4<f32>(p[0].attenuationExp, p[1].attenuationExp, p[2].attenuationExp, p[3].attenuationExp);
|
|
189
|
+
|
|
190
|
+
var lightIntensities = intensities / (1.0 + d * (attenuationL + d * attenuationE));
|
|
191
|
+
lightIntensities *= cos(d * ${Math.PI / 2});
|
|
192
|
+
lightIntensities *= step(d, vec4<f32>(1.0));
|
|
193
|
+
|
|
194
|
+
var out: FragmentOut;
|
|
195
|
+
out.color = lightIntensities;
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
`,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const initializerPipeline = device.createRenderPipeline({
|
|
202
|
+
label: 'LightsTextureInitializer renderpipeline',
|
|
203
|
+
layout: 'auto',
|
|
204
|
+
vertex: { module: initializerShaderModule, entryPoint: 'main_vertex' },
|
|
205
|
+
fragment: {
|
|
206
|
+
module: initializerShaderModule,
|
|
207
|
+
entryPoint: 'main_fragment',
|
|
208
|
+
targets: [{ format: textureFormat }],
|
|
66
209
|
},
|
|
67
|
-
|
|
68
|
-
|
|
210
|
+
primitive: { cullMode: 'none', topology: 'triangle-strip' },
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const initializerBindgroup = device.createBindGroup({
|
|
214
|
+
label: 'LightsTextureInitializer bindgroup',
|
|
215
|
+
layout: initializerPipeline.getBindGroupLayout(0),
|
|
216
|
+
entries: [{ binding: 0, resource: { buffer: lightsBufferData.lightsBufferGpu } }],
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const initializerBundleEncoder = device.createRenderBundleEncoder({
|
|
220
|
+
label: 'LightsTextureInitializer renderbundle encoder',
|
|
221
|
+
colorFormats: [textureFormat],
|
|
222
|
+
})
|
|
223
|
+
initializerBundleEncoder.setPipeline(initializerPipeline)
|
|
224
|
+
initializerBundleEncoder.setBindGroup(0, initializerBindgroup)
|
|
225
|
+
initializerBundleEncoder.draw(4)
|
|
226
|
+
const initializerRenderBundle = initializerBundleEncoder.finish({
|
|
227
|
+
label: 'LightsTextureInitializer renderbundle',
|
|
69
228
|
})
|
|
70
229
|
|
|
71
|
-
|
|
230
|
+
// --- mask sub-pass (shadow/occlusion geometry) ---
|
|
231
|
+
const maskData = createLightsTextureMask(
|
|
72
232
|
device,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
233
|
+
lightsBufferData.lightsBufferGpu,
|
|
234
|
+
lightsTextureDescriptor,
|
|
235
|
+
MAX_LIGHT_SIZE,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
// --- final composite render pipeline ---
|
|
239
|
+
const rendererTargetFormat = node.refs.out.data.texture.format
|
|
240
|
+
|
|
241
|
+
const rendererUniformsBufferGpu = device.createBuffer({
|
|
242
|
+
label: 'LightsRenderer uniforms buffer',
|
|
243
|
+
size: 80,
|
|
244
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
const rendererShaderModule = device.createShaderModule({
|
|
248
|
+
label: 'LightsRenderer shader module',
|
|
249
|
+
code: `
|
|
250
|
+
struct Uniforms { // align(16) size(80)
|
|
251
|
+
invertViewMatrix: mat4x4<f32>, // offset(0) align(16) size(64)
|
|
252
|
+
ambientLight: vec3<f32>, // offset(64) align(16) size(12)
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
${LIGHTS_STRUCT_DEFINITION}
|
|
256
|
+
|
|
257
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
258
|
+
@group(0) @binding(1) var<storage,read> lightsBuffer: LightsBuffer;
|
|
259
|
+
@group(0) @binding(2) var lightsTexture: texture_2d<f32>;
|
|
260
|
+
@group(0) @binding(3) var lightsTextureSampler: sampler;
|
|
261
|
+
|
|
262
|
+
@group(1) @binding(0) var albedoTexture: texture_2d<f32>;
|
|
263
|
+
@group(1) @binding(1) var albedoSampler: sampler;
|
|
264
|
+
|
|
265
|
+
struct VertexIn {
|
|
266
|
+
@builtin(vertex_index) vertexIndex: u32,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
struct VertexOut {
|
|
270
|
+
@builtin(position) position: vec4<f32>,
|
|
271
|
+
@location(0) worldPosition: vec2<f32>,
|
|
272
|
+
@location(1) uv: vec2<f32>,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
@vertex
|
|
276
|
+
fn main_vertex(in: VertexIn) -> VertexOut {
|
|
277
|
+
const corners = array<vec2<f32>, 4>(
|
|
278
|
+
vec2<f32>(-1, -1),
|
|
279
|
+
vec2<f32>(1, -1),
|
|
280
|
+
vec2<f32>(-1, 1),
|
|
281
|
+
vec2<f32>(1, 1),
|
|
282
|
+
);
|
|
283
|
+
let screenPosition = corners[in.vertexIndex];
|
|
284
|
+
|
|
285
|
+
var out: VertexOut;
|
|
286
|
+
out.position = vec4<f32>(screenPosition, 0.0, 1.0);
|
|
287
|
+
out.worldPosition = (uniforms.invertViewMatrix * out.position).xy;
|
|
288
|
+
out.uv = 0.5 + 0.5 * screenPosition * vec2<f32>(1.0, -1.0);
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
struct FragmentOut {
|
|
293
|
+
@location(0) color: vec4<f32>,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const cellsGridSizeU = vec2<u32>(${gridSize.x}, ${gridSize.y});
|
|
297
|
+
const cellsGridSizeF = vec2<f32>(${gridSize.x}, ${gridSize.y});
|
|
298
|
+
|
|
299
|
+
fn sampleLightBaseIntensity(lightId: u32, localUv: vec2<f32>) -> f32 {
|
|
300
|
+
let cellIndex = lightId / 4u;
|
|
301
|
+
let indexInCell = lightId % 4u;
|
|
302
|
+
|
|
303
|
+
let cellIdU = vec2<u32>(
|
|
304
|
+
cellIndex % cellsGridSizeU.x,
|
|
305
|
+
cellIndex / cellsGridSizeU.x,
|
|
306
|
+
);
|
|
307
|
+
let cellIdF = vec2<f32>(cellIdU);
|
|
308
|
+
let uv = (cellIdF + localUv) / cellsGridSizeF;
|
|
309
|
+
let uvYInverted = vec2<f32>(uv.x, 1.0 - uv.y);
|
|
310
|
+
let sample = textureSampleLevel(lightsTexture, lightsTextureSampler, uvYInverted, 0.0);
|
|
311
|
+
let channel = vec4<f32>(
|
|
312
|
+
vec4<u32>(indexInCell) == vec4<u32>(0u, 1u, 2u, 3u),
|
|
313
|
+
);
|
|
314
|
+
return dot(sample, channel);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fn compute_lights(worldPosition: vec2<f32>) -> vec3<f32> {
|
|
318
|
+
var color = vec3<f32>(uniforms.ambientLight);
|
|
319
|
+
|
|
320
|
+
const maxUvDistance = f32(${1 - 2 / resolutionPerLight});
|
|
321
|
+
|
|
322
|
+
let lightsCount = lightsBuffer.count;
|
|
323
|
+
for (var iLight = 0u; iLight < lightsCount; iLight++) {
|
|
324
|
+
let light = lightsBuffer.lights[iLight];
|
|
325
|
+
let lightSize = f32(${resolutionPerLight});
|
|
326
|
+
let relativePosition = (worldPosition - light.position) / lightSize;
|
|
327
|
+
if (max(abs(relativePosition.x), abs(relativePosition.y)) < maxUvDistance) {
|
|
328
|
+
let localUv = 0.5 + 0.5 * relativePosition;
|
|
329
|
+
let lightIntensity = light.intensity * sampleLightBaseIntensity(iLight, localUv);
|
|
330
|
+
color += lightIntensity * light.color;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return color;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@fragment
|
|
338
|
+
fn main_fragment(in: VertexOut) -> FragmentOut {
|
|
339
|
+
let light = clamp(compute_lights(in.worldPosition), vec3<f32>(0.0), vec3<f32>(1.0));
|
|
340
|
+
let albedo = textureSample(albedoTexture, albedoSampler, in.uv);
|
|
341
|
+
let color = albedo.rgb * light;
|
|
342
|
+
|
|
343
|
+
var out: FragmentOut;
|
|
344
|
+
out.color = vec4<f32>(color, 1.0);
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
`,
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
const rendererPipeline = device.createRenderPipeline({
|
|
351
|
+
label: 'LightsRenderer renderpipeline',
|
|
352
|
+
layout: 'auto',
|
|
353
|
+
vertex: {
|
|
354
|
+
module: rendererShaderModule,
|
|
355
|
+
entryPoint: 'main_vertex',
|
|
356
|
+
},
|
|
357
|
+
fragment: {
|
|
358
|
+
module: rendererShaderModule,
|
|
359
|
+
entryPoint: 'main_fragment',
|
|
360
|
+
targets: [{ format: rendererTargetFormat }],
|
|
76
361
|
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
resolutionPerLight: MAX_LIGHT_SIZE,
|
|
81
|
-
maxLightSize: MAX_LIGHT_SIZE,
|
|
82
|
-
antialiased: false,
|
|
83
|
-
filtering: 'nearest',
|
|
84
|
-
textureFormat: getPreferredFormat(cobalt),
|
|
362
|
+
primitive: {
|
|
363
|
+
cullMode: 'none',
|
|
364
|
+
topology: 'triangle-strip',
|
|
85
365
|
},
|
|
86
366
|
})
|
|
87
367
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
368
|
+
const rendererBindgroup0 = device.createBindGroup({
|
|
369
|
+
label: 'LightsRenderer bindgroup 0',
|
|
370
|
+
layout: rendererPipeline.getBindGroupLayout(0),
|
|
371
|
+
entries: [
|
|
372
|
+
{
|
|
373
|
+
binding: 0,
|
|
374
|
+
resource: { buffer: rendererUniformsBufferGpu },
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
binding: 1,
|
|
378
|
+
resource: { buffer: lightsBufferData.lightsBufferGpu },
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
binding: 2,
|
|
382
|
+
resource: lightsTexture.createView({ label: 'LightsRenderer lightsTexture view' }),
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
binding: 3,
|
|
386
|
+
resource: device.createSampler({
|
|
387
|
+
label: 'LightsRenderer sampler',
|
|
388
|
+
addressModeU: 'clamp-to-edge',
|
|
389
|
+
addressModeV: 'clamp-to-edge',
|
|
390
|
+
magFilter: filtering,
|
|
391
|
+
minFilter: filtering,
|
|
392
|
+
}),
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
})
|
|
91
396
|
|
|
92
|
-
|
|
93
|
-
|
|
397
|
+
const rendererBindgroup1 = buildRendererBindgroup1(device, rendererPipeline, {
|
|
398
|
+
view: node.refs.in.data.view,
|
|
399
|
+
sampler: node.refs.in.data.sampler,
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
// viewport
|
|
403
|
+
const viewportTopLeft = [...cobalt.viewport.position]
|
|
404
|
+
const invViewProjectionMatrix = computeInvertViewProjectionMatrix(
|
|
405
|
+
cobalt.viewport.width,
|
|
406
|
+
cobalt.viewport.height,
|
|
407
|
+
cobalt.viewport.zoom,
|
|
408
|
+
viewportTopLeft,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
const data = {
|
|
412
|
+
// lights storage buffer
|
|
413
|
+
...lightsBufferData,
|
|
414
|
+
|
|
415
|
+
// lights atlas texture
|
|
416
|
+
lightsTexture,
|
|
417
|
+
lightsTextureRenderpassDescriptor,
|
|
418
|
+
gridSize,
|
|
419
|
+
|
|
420
|
+
// initializer sub-pass
|
|
421
|
+
initializerPipeline,
|
|
422
|
+
initializerBindgroup,
|
|
423
|
+
initializerRenderBundle,
|
|
94
424
|
|
|
95
|
-
|
|
425
|
+
// mask sub-pass
|
|
426
|
+
...maskData,
|
|
96
427
|
|
|
428
|
+
// final composite pass
|
|
429
|
+
rendererTargetFormat,
|
|
430
|
+
rendererPipeline,
|
|
431
|
+
rendererUniformsBufferGpu,
|
|
432
|
+
rendererBindgroup0,
|
|
433
|
+
rendererBindgroup1,
|
|
434
|
+
rendererRenderBundle: null, // set below after data is fully constructed
|
|
435
|
+
|
|
436
|
+
// viewport (flat, no class)
|
|
437
|
+
invViewProjectionMatrix,
|
|
438
|
+
viewportWidth: cobalt.viewport.width,
|
|
439
|
+
viewportHeight: cobalt.viewport.height,
|
|
440
|
+
viewportZoom: cobalt.viewport.zoom,
|
|
441
|
+
viewportTopLeft,
|
|
442
|
+
|
|
443
|
+
// logical state
|
|
97
444
|
lights: [],
|
|
445
|
+
ambientLight: [0.2, 0.2, 0.2],
|
|
446
|
+
lightsBufferNeedsUpdate: true,
|
|
447
|
+
lightsTextureNeedsUpdate: true,
|
|
98
448
|
}
|
|
449
|
+
|
|
450
|
+
data.rendererRenderBundle = buildRendererRenderBundle(device, data)
|
|
451
|
+
|
|
452
|
+
return data
|
|
99
453
|
}
|
|
100
454
|
|
|
101
455
|
function draw(cobalt, node, commandEncoder) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
lightsBuffer.setLights(node.data.lights)
|
|
105
|
-
node.data.lightsBufferNeedsUpdate = false
|
|
106
|
-
node.data.lightsTextureNeedsUpdate = true
|
|
107
|
-
}
|
|
456
|
+
const d = node.data
|
|
457
|
+
const { device } = cobalt
|
|
108
458
|
|
|
109
|
-
|
|
459
|
+
if (d.lightsBufferNeedsUpdate) {
|
|
460
|
+
writeLightsBuffer(device, d, d.lights)
|
|
461
|
+
d.lightsBufferNeedsUpdate = false
|
|
462
|
+
d.lightsTextureNeedsUpdate = true
|
|
463
|
+
}
|
|
110
464
|
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
465
|
+
if (d.lightsTextureNeedsUpdate) {
|
|
466
|
+
computeLightsTexture(device, d, commandEncoder)
|
|
467
|
+
d.lightsTextureNeedsUpdate = false
|
|
114
468
|
}
|
|
115
469
|
|
|
470
|
+
// update zoom and recompute invert VP matrix
|
|
471
|
+
d.viewportZoom = cobalt.viewport.zoom
|
|
472
|
+
d.invViewProjectionMatrix = computeInvertViewProjectionMatrix(
|
|
473
|
+
d.viewportWidth,
|
|
474
|
+
d.viewportHeight,
|
|
475
|
+
d.viewportZoom,
|
|
476
|
+
d.viewportTopLeft,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
// write uniforms: invertViewMatrix (64 bytes) + ambientLight (12 bytes)
|
|
480
|
+
const uniformsCpu = new ArrayBuffer(80)
|
|
481
|
+
new Float32Array(uniformsCpu, 0, 16).set(d.invViewProjectionMatrix)
|
|
482
|
+
new Float32Array(uniformsCpu, 64, 3).set(d.ambientLight)
|
|
483
|
+
device.queue.writeBuffer(d.rendererUniformsBufferGpu, 0, uniformsCpu)
|
|
484
|
+
|
|
116
485
|
const renderpass = commandEncoder.beginRenderPass({
|
|
117
486
|
label: 'light',
|
|
118
487
|
colorAttachments: [
|
|
@@ -124,24 +493,90 @@ function draw(cobalt, node, commandEncoder) {
|
|
|
124
493
|
},
|
|
125
494
|
],
|
|
126
495
|
})
|
|
127
|
-
|
|
128
|
-
node.data.viewport.setZoom(cobalt.viewport.zoom)
|
|
129
|
-
const invertVpMatrix = node.data.viewport.invertViewProjectionMatrix
|
|
130
|
-
lightsRenderer.render(renderpass, invertVpMatrix)
|
|
131
|
-
|
|
496
|
+
renderpass.executeBundles([d.rendererRenderBundle])
|
|
132
497
|
renderpass.end()
|
|
133
498
|
}
|
|
134
499
|
|
|
135
500
|
function destroy(node) {
|
|
136
|
-
node.data
|
|
137
|
-
|
|
501
|
+
const d = node.data
|
|
502
|
+
destroyLightsBuffer(d)
|
|
503
|
+
d.lightsTexture.destroy()
|
|
504
|
+
d.rendererUniformsBufferGpu.destroy()
|
|
505
|
+
destroyMask(d)
|
|
138
506
|
}
|
|
139
507
|
|
|
140
508
|
function resize(cobalt, node) {
|
|
141
|
-
node.data
|
|
509
|
+
const d = node.data
|
|
510
|
+
const { device } = cobalt
|
|
511
|
+
|
|
512
|
+
d.viewportWidth = cobalt.viewport.width
|
|
513
|
+
d.viewportHeight = cobalt.viewport.height
|
|
514
|
+
d.invViewProjectionMatrix = computeInvertViewProjectionMatrix(
|
|
515
|
+
d.viewportWidth,
|
|
516
|
+
d.viewportHeight,
|
|
517
|
+
d.viewportZoom,
|
|
518
|
+
d.viewportTopLeft,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
// rebuild bindgroup1 with the new albedo texture view/sampler from the resized input node
|
|
522
|
+
d.rendererBindgroup1 = buildRendererBindgroup1(device, d.rendererPipeline, {
|
|
142
523
|
view: node.refs.in.data.view,
|
|
143
524
|
sampler: node.refs.in.data.sampler,
|
|
144
525
|
})
|
|
526
|
+
d.rendererRenderBundle = buildRendererRenderBundle(device, d)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ---- helpers ----
|
|
530
|
+
|
|
531
|
+
function computeLightsTexture(device, data, commandEncoder) {
|
|
532
|
+
setMaskLightsCount(device, data, data.lightsCount)
|
|
533
|
+
const renderpassEncoder = commandEncoder.beginRenderPass(data.lightsTextureRenderpassDescriptor)
|
|
534
|
+
const w = data.lightsTexture.width
|
|
535
|
+
const h = data.lightsTexture.height
|
|
536
|
+
renderpassEncoder.setViewport(0, 0, w, h, 0, 1)
|
|
537
|
+
renderpassEncoder.setScissorRect(0, 0, w, h)
|
|
538
|
+
renderpassEncoder.executeBundles([data.initializerRenderBundle, data.maskRenderBundle])
|
|
539
|
+
renderpassEncoder.end()
|
|
540
|
+
}
|
|
145
541
|
|
|
146
|
-
|
|
542
|
+
function buildRendererBindgroup1(device, pipeline, albedo) {
|
|
543
|
+
return device.createBindGroup({
|
|
544
|
+
label: 'LightsRenderer bindgroup 1',
|
|
545
|
+
layout: pipeline.getBindGroupLayout(1),
|
|
546
|
+
entries: [
|
|
547
|
+
{
|
|
548
|
+
binding: 0,
|
|
549
|
+
resource: albedo.view,
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
binding: 1,
|
|
553
|
+
resource: albedo.sampler,
|
|
554
|
+
},
|
|
555
|
+
],
|
|
556
|
+
})
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function computeInvertViewProjectionMatrix(width, height, zoom, topLeft) {
|
|
560
|
+
const m = wgpuMatrix.mat4.identity()
|
|
561
|
+
wgpuMatrix.mat4.multiply(wgpuMatrix.mat4.scaling([1, -1, 0]), m, m)
|
|
562
|
+
wgpuMatrix.mat4.multiply(wgpuMatrix.mat4.translation([1, 1, 0]), m, m)
|
|
563
|
+
wgpuMatrix.mat4.multiply(
|
|
564
|
+
wgpuMatrix.mat4.scaling([(0.5 * width) / zoom, (0.5 * height) / zoom, 0]),
|
|
565
|
+
m,
|
|
566
|
+
m,
|
|
567
|
+
)
|
|
568
|
+
wgpuMatrix.mat4.multiply(wgpuMatrix.mat4.translation([topLeft[0], topLeft[1], 0]), m, m)
|
|
569
|
+
return m
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function buildRendererRenderBundle(device, data) {
|
|
573
|
+
const renderBundleEncoder = device.createRenderBundleEncoder({
|
|
574
|
+
label: 'LightsRenderer renderbundle encoder',
|
|
575
|
+
colorFormats: [data.rendererTargetFormat],
|
|
576
|
+
})
|
|
577
|
+
renderBundleEncoder.setPipeline(data.rendererPipeline)
|
|
578
|
+
renderBundleEncoder.setBindGroup(0, data.rendererBindgroup0)
|
|
579
|
+
renderBundleEncoder.setBindGroup(1, data.rendererBindgroup1)
|
|
580
|
+
renderBundleEncoder.draw(4)
|
|
581
|
+
return renderBundleEncoder.finish({ label: 'LightsRenderer renderbundle' })
|
|
147
582
|
}
|