@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.
@@ -1,54 +1,57 @@
1
+ import * as wgpuMatrix from 'wgpu-matrix'
1
2
  import getPreferredFormat from '../get-preferred-format.js'
2
- import { LightsBuffer } from './lights-buffer.js'
3
- import { LightsRenderer } from './lights-renderer.js'
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 { Viewport } from './viewport'
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
- // the inputs and outputs to this node
15
- refs: [
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
- // runs when the viewport position changes
45
- node.data.viewport.setTopLeft(...cobalt.viewport.position)
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
- // optional
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 lightsBuffer = new LightsBuffer(device, MAX_LIGHT_COUNT)
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
- const viewport = new Viewport({
63
- viewportSize: {
64
- width: cobalt.viewport.width,
65
- height: cobalt.viewport.height,
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
- center: cobalt.viewport.position,
68
- zoom: cobalt.viewport.zoom,
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
- const lightsRenderer = new LightsRenderer({
230
+ // --- mask sub-pass (shadow/occlusion geometry) ---
231
+ const maskData = createLightsTextureMask(
72
232
  device,
73
- albedo: {
74
- view: node.refs.in.data.view,
75
- sampler: node.refs.in.data.sampler,
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
- targetTexture: node.refs.out.data.texture,
78
- lightsBuffer,
79
- lightsTextureProperties: {
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
- return {
89
- lightsBuffer,
90
- lightsBufferNeedsUpdate: true,
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
- lightsTextureNeedsUpdate: true,
93
- lightsRenderer,
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
- viewport,
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
- if (node.data.lightsBufferNeedsUpdate) {
103
- const lightsBuffer = node.data.lightsBuffer
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
- const lightsRenderer = node.data.lightsRenderer
459
+ if (d.lightsBufferNeedsUpdate) {
460
+ writeLightsBuffer(device, d, d.lights)
461
+ d.lightsBufferNeedsUpdate = false
462
+ d.lightsTextureNeedsUpdate = true
463
+ }
110
464
 
111
- if (node.data.lightsTextureNeedsUpdate) {
112
- lightsRenderer.computeLightsTexture(commandEncoder)
113
- node.data.lightsTextureNeedsUpdate = false
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.lightsBuffer.destroy()
137
- node.data.lightsRenderer.destroy()
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.lightsRenderer.setAlbedo({
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
- node.data.viewport.setViewportSize(cobalt.viewport.width, cobalt.viewport.height)
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
  }