@footgun/cobalt 0.6.14 → 0.7.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 (41) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bundle.js +1318 -1304
  3. package/examples/01-primitives/index.html +1 -1
  4. package/examples/02-sprites/entity-sprite.js +10 -15
  5. package/examples/02-sprites/hdr.html +321 -0
  6. package/examples/02-sprites/index.html +21 -120
  7. package/examples/02-sprites/system-renderer.js +2 -2
  8. package/examples/03-tiles/index.html +87 -32
  9. package/examples/03-tiles/system-renderer.js +2 -2
  10. package/examples/04-overlay/index.html +178 -21
  11. package/examples/05-bloom/index.html +23 -23
  12. package/examples/05-bloom/system-renderer.js +2 -2
  13. package/examples/06-displacement/index.html +20 -112
  14. package/examples/06-displacement/system-renderer.js +2 -2
  15. package/examples/08-light/index.html +51 -123
  16. package/package.json +1 -1
  17. package/src/cobalt.js +8 -8
  18. package/src/sprite/public-api.js +57 -177
  19. package/src/sprite/sprite.js +301 -177
  20. package/src/sprite/sprite.wgsl +68 -87
  21. package/src/sprite-hdr/public-api.js +95 -0
  22. package/src/sprite-hdr/sprite.js +414 -0
  23. package/src/sprite-hdr/sprite.wgsl +101 -0
  24. package/src/{sprite → spritesheet}/create-sprite-quads.js +11 -11
  25. package/src/{sprite → spritesheet}/read-spritesheet.js +62 -28
  26. package/src/spritesheet/spritesheet.js +75 -0
  27. package/src/{tile → tile-hdr}/atlas.js +5 -3
  28. package/src/{tile → tile-hdr}/tile.js +15 -6
  29. package/examples/04-overlay/deps.js +0 -1
  30. package/src/overlay/constants.js +0 -1
  31. package/src/overlay/overlay.js +0 -343
  32. package/src/overlay/overlay.wgsl +0 -88
  33. package/src/sprite/constants.js +0 -1
  34. package/src/sprite/sorted-binary-insert.js +0 -45
  35. package/src/sprite/spritesheet.js +0 -215
  36. /package/examples/02-sprites/{Game.js → Global.js} +0 -0
  37. /package/examples/03-tiles/{Game.js → Global.js} +0 -0
  38. /package/examples/05-bloom/{Game.js → Global.js} +0 -0
  39. /package/examples/06-displacement/{Game.js → Global.js} +0 -0
  40. /package/examples/08-light/{Game.js → Global.js} +0 -0
  41. /package/src/{tile → tile-hdr}/tile.wgsl +0 -0
@@ -1,103 +1,84 @@
1
- struct TransformData {
2
- view: mat4x4<f32>,
3
- projection: mat4x4<f32>
1
+ struct ViewParams {
2
+ view : mat4x4<f32>,
3
+ proj : mat4x4<f32>
4
4
  };
5
5
 
6
- struct Sprite {
7
- translate: vec2<f32>,
8
- scale: vec2<f32>,
9
- tint: vec4<f32>,
10
- opacity: f32,
11
- rotation: f32,
12
- emissiveIntensity: f32,
13
- sortValue: f32,
14
- };
6
+ @group(0) @binding(0) var<uniform> uView : ViewParams;
7
+ @group(0) @binding(1) var uSampler : sampler;
8
+ @group(0) @binding(2) var uTex : texture_2d<f32>;
15
9
 
16
- struct SpritesBuffer {
17
- models: array<Sprite>,
10
+ struct SpriteDesc {
11
+ uvOrigin : vec2<f32>,
12
+ uvSpan : vec2<f32>,
13
+ frameSize : vec2<f32>, // pixels
14
+ centerOffset : vec2<f32>, // pixels
18
15
  };
19
16
 
20
- @binding(0) @group(0) var<uniform> transformUBO: TransformData;
21
- @binding(1) @group(0) var myTexture: texture_2d<f32>;
22
- @binding(2) @group(0) var mySampler: sampler;
23
- @binding(3) @group(0) var<storage, read> sprites : SpritesBuffer;
24
- @binding(4) @group(0) var emissiveTexture: texture_2d<f32>;
17
+ @group(0) @binding(3) var<storage, read> Sprites : array<SpriteDesc>;
25
18
 
26
19
 
27
- struct Fragment {
28
- @builtin(position) Position : vec4<f32>,
29
- @location(0) TexCoord : vec2<f32>,
30
- @location(1) Tint : vec4<f32>,
31
- @location(2) Opacity: f32,
20
+ struct VSOut {
21
+ @builtin(position) pos : vec4<f32>,
22
+ @location(0) uv : vec2<f32>,
23
+ @location(1) tint : vec4<f32>,
24
+ @location(2) opacity : f32,
32
25
  };
33
26
 
34
- // multiple render targets
35
- struct GBufferOutput {
36
- @location(0) color : vec4<f32>,
37
- @location(1) emissive : vec4<f32>,
38
- }
39
-
40
-
41
- @vertex
42
- fn vs_main (@builtin(instance_index) i_id : u32,
43
- @location(0) vertexPosition: vec3<f32>,
44
- @location(1) vertexTexCoord: vec2<f32>) -> Fragment {
45
-
46
- var output : Fragment;
47
-
48
- var sx: f32 = sprites.models[i_id].scale.x;
49
- var sy: f32 = sprites.models[i_id].scale.y;
50
- var sz: f32 = 1.0;
51
-
52
- var rot: f32 = sprites.models[i_id].rotation;
53
-
54
- var tx: f32 = sprites.models[i_id].translate.x;
55
- var ty: f32 = sprites.models[i_id].translate.y;
56
- var tz: f32 = 0;
57
-
58
- var s = sin(rot);
59
- var c = cos(rot);
27
+ // corners for a unit-centered quad in strip order
28
+ const corners = array<vec2<f32>, 4>(
29
+ vec2<f32>(-0.5, -0.5),
30
+ vec2<f32>( 0.5, -0.5),
31
+ vec2<f32>(-0.5, 0.5),
32
+ vec2<f32>( 0.5, 0.5),
33
+ );
60
34
 
61
- // https://webglfundamentals.org/webgl/lessons/webgl-2d-matrices.html
35
+ const uvBase = array<vec2<f32>, 4>(
36
+ vec2<f32>(0.0, 0.0),
37
+ vec2<f32>(1.0, 0.0),
38
+ vec2<f32>(0.0, 1.0),
39
+ vec2<f32>(1.0, 1.0),
40
+ );
62
41
 
63
- var scaleM: mat4x4<f32> = mat4x4<f32>(sx, 0.0, 0.0, 0.0,
64
- 0.0, sy, 0.0, 0.0,
65
- 0.0, 0.0, sz, 0.0,
66
- 0, 0, 0, 1.0);
67
42
 
68
- // rotation and translation
69
- var modelM: mat4x4<f32> = mat4x4<f32>(c, s, 0.0, 0.0,
70
- -s, c, 0.0, 0.0,
71
- 0.0, 0.0, 1.0, 0.0,
72
- tx, ty, tz, 1.0) * scaleM;
73
-
74
- //output.Position = transformUBO.projection * transformUBO.view * sprites.models[i_id].modelMatrix * vec4<f32>(vertexPosition, 1.0);
75
- output.Position = transformUBO.projection * transformUBO.view * modelM * vec4<f32>(vertexPosition, 1.0);
76
-
77
- output.TexCoord = vertexTexCoord;
78
- output.Tint = sprites.models[i_id].tint;
79
- output.Opacity = sprites.models[i_id].opacity;
80
-
81
- return output;
43
+ @vertex
44
+ fn vs_main(@builtin(vertex_index) vid : u32,
45
+ // per-instance attributes (locations 0..4)
46
+ @location(0) i_pos : vec2<f32>,
47
+ @location(1) i_size : vec2<f32>, // scales descriptor frame size (1,1 means use descriptor size)
48
+ @location(2) i_scale : vec2<f32>, // per-axis scale
49
+ @location(3) i_tint : vec4<f32>,
50
+ @location(4) i_spriteId : u32,
51
+ @location(5) i_opacity : f32,
52
+ @location(6) i_rotation : f32
53
+ ) -> VSOut {
54
+
55
+ let rot = i_rotation;
56
+ let c = cos(rot);
57
+ let s = sin(rot);
58
+
59
+ let d = Sprites[i_spriteId];
60
+ let corner = corners[vid];
61
+
62
+ let sizePx = d.frameSize * i_size * i_scale; // per-axis scale applied on trimmed frame
63
+ var local = corner * sizePx;
64
+ local += d.centerOffset * i_scale; // compensate trimming // compensate trimming (so rotation is around original center)
65
+
66
+
67
+ let rotated = vec2<f32>(local.x * c - local.y * s, local.x * s + local.y * c);
68
+ let world = vec4<f32>(rotated + i_pos, 0.0, 1.0);
69
+
70
+ var out : VSOut;
71
+ out.pos = uView.proj * uView.view * world;
72
+ out.uv = d.uvOrigin + d.uvSpan * uvBase[vid];
73
+ out.tint = i_tint;
74
+ out.opacity = i_opacity;
75
+
76
+ return out;
82
77
  }
83
78
 
84
- @fragment
85
- fn fs_main (@location(0) TexCoord: vec2<f32>,
86
- @location(1) Tint: vec4<f32>,
87
- @location(2) Opacity: f32) -> GBufferOutput {
88
-
89
-
90
- var output : GBufferOutput;
91
79
 
92
- var outColor: vec4<f32> = textureSample(myTexture, mySampler, TexCoord);
93
- output.color = vec4<f32>(outColor.rgb * (1.0 - Tint.a) + (Tint.rgb * Tint.a), outColor.a * Opacity);
94
-
95
- let emissive = textureSample(emissiveTexture, mySampler, TexCoord);
96
-
97
- // the alpha channel in the emissive texture is used for emission strength
98
- output.emissive = vec4(emissive.rgb, 1.0) * emissive.a;
99
-
100
- //output.emissive = textureSample(emissiveTexture, mySampler, TexCoord) * EmissiveIntensity;
101
-
102
- return output;
80
+ @fragment
81
+ fn fs_main(in : VSOut) -> @location(0) vec4<f32> {
82
+ let texel = textureSample(uTex, uSampler, in.uv);
83
+ return vec4<f32>(texel.rgb * (1.0 - in.tint.a) + (in.tint.rgb * in.tint.a), texel.a * in.opacity);
103
84
  }
@@ -0,0 +1,95 @@
1
+ import uuid from '../uuid.js'
2
+ import { vec2, vec4 } from 'wgpu-matrix'
3
+
4
+
5
+ // returns a unique identifier for the created sprite
6
+ export function addSprite (cobalt, renderPass, name, position, scale, tint, opacity, rotation) {
7
+
8
+ const { idByName } = renderPass.refs.spritesheet.data
9
+
10
+ renderPass.data.sprites.push({
11
+ position: vec2.clone(position),
12
+ sizeX: 1, sizeY: 1,
13
+ scale: vec2.clone(scale),
14
+ rotation,
15
+ opacity,
16
+ tint: vec4.clone(tint),
17
+ spriteID: idByName.get(name),
18
+ id: uuid(),
19
+ });
20
+
21
+ return renderPass.data.sprites.at(-1).id
22
+ }
23
+
24
+
25
+ export function removeSprite (cobalt, renderPass, id) {
26
+ for (let i=0; i < renderPass.data.sprites.length; i++) {
27
+ if (renderPass.data.sprites[i].id === id) {
28
+ renderPass.data.sprites.splice(i, 1)
29
+ return
30
+ }
31
+ }
32
+ }
33
+
34
+
35
+ // remove all sprites
36
+ export function clear (cobalt, renderPass) {
37
+ renderPass.data.sprites.length = 0
38
+ }
39
+
40
+
41
+ export function setSpriteName (cobalt, renderPass, id, name) {
42
+ const sprite = renderPass.data.sprites.find((s) => s.id === id)
43
+
44
+ if (!sprite)
45
+ return
46
+
47
+ const { idByName } = renderPass.refs.spritesheet.data
48
+
49
+ sprite.spriteID = idByName.get(name)
50
+ }
51
+
52
+
53
+ export function setSpritePosition (cobalt, renderPass, id, position) {
54
+ const sprite = renderPass.data.sprites.find((s) => s.id === id)
55
+ if (!sprite)
56
+ return
57
+
58
+ vec2.copy(position, sprite.position)
59
+ }
60
+
61
+
62
+ export function setSpriteTint (cobalt, renderPass, id, tint) {
63
+ const sprite = renderPass.data.sprites.find((s) => s.id === id)
64
+ if (!sprite)
65
+ return
66
+
67
+ vec4.copy(tint, sprite.tint)
68
+ }
69
+
70
+
71
+ export function setSpriteOpacity (cobalt, renderPass, id, opacity) {
72
+ const sprite = renderPass.data.sprites.find((s) => s.id === id)
73
+ if (!sprite)
74
+ return
75
+
76
+ sprite.opacity = opacity
77
+ }
78
+
79
+
80
+ export function setSpriteRotation (cobalt, renderPass, id, rotation) {
81
+ const sprite = renderPass.data.sprites.find((s) => s.id === id)
82
+ if (!sprite)
83
+ return
84
+
85
+ sprite.rotation = rotation
86
+ }
87
+
88
+
89
+ export function setSpriteScale (cobalt, renderPass, id, scale) {
90
+ const sprite = renderPass.data.sprites.find((s) => s.id === id)
91
+ if (!sprite)
92
+ return
93
+
94
+ vec2.copy(scale, sprite.scale)
95
+ }
@@ -0,0 +1,414 @@
1
+ import * as publicAPI from './public-api.js'
2
+ import spriteWGSL from './sprite.wgsl'
3
+ import round from 'round-half-up-symmetric'
4
+ import { mat4, vec3 } from 'wgpu-matrix'
5
+
6
+
7
+ // temporary variables, allocated once to avoid garbage collection
8
+ const _tmpVec3 = vec3.create(0, 0, 0)
9
+
10
+ // Packed instance layout: 48 bytes (aligned for vec4 fetch)
11
+ const INSTANCE_STRIDE = 64;
12
+
13
+ // Offsets inside one instance (bytes)
14
+ const OFF_POS = 0; // float32x2 (8B)
15
+ const OFF_SIZE = 8; // float32x2 (8B)
16
+ const OFF_SCALE = 16; // float32x2 (8B)
17
+ const OFF_TINT = 24; // float32x4 (16B)
18
+ const OFF_SPRITEID = 40; // uint32 (4B)
19
+ const OFF_OPACITY = 44; // float32 (4B)
20
+ const OFF_ROT = 48; // float32 (4B)
21
+
22
+
23
+ export default {
24
+ type: "cobalt:spriteHDR",
25
+ refs: [
26
+ { name: "spritesheet", type: "customResource", access: "read" },
27
+ {
28
+ name: "color",
29
+ type: "textureView",
30
+ format: "rgba16float",
31
+ access: "write",
32
+ },
33
+ {
34
+ name: "emissive",
35
+ type: "textureView",
36
+ format: "rgba16float",
37
+ access: "write",
38
+ },
39
+ ],
40
+
41
+ // cobalt event handling functions
42
+
43
+ // @params Object cobalt renderer world object
44
+ // @params Object options optional data passed when initing this node
45
+ onInit: async function (cobalt, options = {}) {
46
+ return init(cobalt, options);
47
+ },
48
+
49
+ onRun: function (cobalt, node, webGpuCommandEncoder) {
50
+ // do whatever you need for this node. webgpu renderpasses, etc.
51
+ draw(cobalt, node, webGpuCommandEncoder);
52
+ },
53
+
54
+ // Clean up GPU resources. Most WebGPU objects are GC-managed and don't
55
+ // expose destroy(); buffers/textures/query-sets do.
56
+ onDestroy: function (cobalt, node) {
57
+ // Explicitly destroy GPU resources that have a destroy() method
58
+ try { node.data.instanceBuf?.destroy(); } catch {}
59
+ try { node.data.spriteBuf?.destroy(); } catch {}
60
+ try { node.data.uniformBuffer?.destroy(); } catch {}
61
+
62
+ // These do not have destroy(); drop references to let GC reclaim
63
+ node.data.pipeline = null; // GPURenderPipeline
64
+ node.data.bindGroup = null; // GPUBindGroup
65
+ node.data.bindGroupLayout = null;// GPUBindGroupLayout
66
+
67
+ // CPU-side allocations
68
+ node.data.instanceStaging = null;
69
+ node.data.instanceView = null;
70
+ node.data.sprites.length = 0;
71
+ node.data.visible.length = 0;
72
+ },
73
+
74
+ onResize: function (cobalt, node) {
75
+ _writeSpriteBuffer(cobalt, node)
76
+ },
77
+
78
+ onViewportPosition: function (cobalt, node) {
79
+ _writeSpriteBuffer(cobalt, node)
80
+ },
81
+
82
+ // optional
83
+ customFunctions: {
84
+ ...publicAPI,
85
+ },
86
+ };
87
+
88
+ async function init(cobalt, nodeData) {
89
+ const { device } = cobalt;
90
+
91
+ const { descs, names } = nodeData.refs.spritesheet.data.spritesheet
92
+
93
+ const uniformBuffer = device.createBuffer({
94
+ size: 64 * 2, // 4x4 matrix with 4 bytes per float32, times 2 matrices (view, projection)
95
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
96
+ })
97
+
98
+ // Pack into std430-like struct (4*float*? + vec2 + vec2 → 32 bytes). We'll just write tightly as 8 floats.
99
+ const BYTES_PER_DESC = 8 * 4; // 8 float32s
100
+ const buf = new ArrayBuffer(BYTES_PER_DESC * descs.length);
101
+ const f32 = new Float32Array(buf);
102
+ for (let i=0;i<descs.length;i++){
103
+ const d = descs[i];
104
+ const base = i * 8;
105
+ f32[base+0] = d.UvOrigin[0];
106
+ f32[base+1] = d.UvOrigin[1];
107
+ f32[base+2] = d.UvSpan[0];
108
+ f32[base+3] = d.UvSpan[1];
109
+ f32[base+4] = d.FrameSize[0];
110
+ f32[base+5] = d.FrameSize[1];
111
+ f32[base+6] = d.CenterOffset[0];
112
+ f32[base+7] = d.CenterOffset[1];
113
+ }
114
+
115
+ // create buffer for sprite uv lookup
116
+ const spriteBuf = device.createBuffer({
117
+ label: "spriteHDR desc table",
118
+ size: Math.max(16, buf.byteLength),
119
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
120
+ });
121
+
122
+ device.queue.writeBuffer(spriteBuf, 0, buf);
123
+
124
+ // --- Instance buffer (growable) ---
125
+ const instanceCap = 1024;
126
+ const instanceBuf = device.createBuffer({
127
+ label: "spriteHDR instances",
128
+ size: INSTANCE_STRIDE * instanceCap,
129
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
130
+ });
131
+ const instanceStaging = new ArrayBuffer(INSTANCE_STRIDE * instanceCap);
132
+ const instanceView = new DataView(instanceStaging);
133
+
134
+ // --- Pipeline ---
135
+ const shader = device.createShaderModule({ code: spriteWGSL });
136
+ const bgl = device.createBindGroupLayout({
137
+ entries: [
138
+ {
139
+ binding: 0,
140
+ visibility: GPUShaderStage.VERTEX,
141
+ buffer: { type: "uniform" },
142
+ },
143
+ {
144
+ binding: 1,
145
+ visibility: GPUShaderStage.FRAGMENT,
146
+ sampler: { type: "filtering" },
147
+ },
148
+ {
149
+ binding: 2,
150
+ visibility: GPUShaderStage.FRAGMENT,
151
+ texture: { sampleType: "float" },
152
+ },
153
+ {
154
+ binding: 3,
155
+ visibility: GPUShaderStage.VERTEX,
156
+ buffer: { type: "read-only-storage" },
157
+ },
158
+ {
159
+ binding: 4,
160
+ visibility: GPUShaderStage.FRAGMENT,
161
+ texture: { sampleType: "float" },
162
+ },
163
+
164
+ ],
165
+ });
166
+ const pipelineLayout = device.createPipelineLayout({
167
+ bindGroupLayouts: [bgl],
168
+ });
169
+
170
+ const instLayout = {
171
+ arrayStride: INSTANCE_STRIDE,
172
+ stepMode: "instance",
173
+ attributes: [
174
+ { shaderLocation: 0, offset: OFF_POS, format: "float32x2" },
175
+ { shaderLocation: 1, offset: OFF_SIZE, format: "float32x2" },
176
+ { shaderLocation: 2, offset: OFF_SCALE, format: "float32x2" },
177
+ { shaderLocation: 3, offset: OFF_TINT, format: "float32x4" },
178
+
179
+ { shaderLocation: 4, offset: OFF_SPRITEID, format: "uint32" },
180
+ { shaderLocation: 5, offset: OFF_OPACITY, format: "float32" },
181
+ { shaderLocation: 6, offset: OFF_ROT, format: "float32" },
182
+ ],
183
+ };
184
+
185
+ const pipeline = device.createRenderPipeline({
186
+ layout: pipelineLayout,
187
+ vertex: {
188
+ module: shader,
189
+ entryPoint: "vs_main",
190
+ buffers: [instLayout],
191
+ },
192
+ fragment: {
193
+ module: shader,
194
+ entryPoint: "fs_main",
195
+ targets: [
196
+ // color
197
+ {
198
+ format: 'rgba16float',
199
+ blend: {
200
+ color: {
201
+ srcFactor: 'src-alpha',
202
+ dstFactor: 'one-minus-src-alpha',
203
+ },
204
+ alpha: {
205
+ srcFactor: 'zero',
206
+ dstFactor: 'one'
207
+ }
208
+ }
209
+ },
210
+
211
+ // emissive
212
+ {
213
+ format: 'rgba16float',
214
+ }
215
+
216
+ ],
217
+ },
218
+ primitive: { topology: "triangle-strip", cullMode: "none" },
219
+ multisample: { count: 1 },
220
+ });
221
+
222
+ const bindGroupLayout = bgl;
223
+
224
+ const bindGroup = device.createBindGroup({
225
+ layout: bgl,
226
+ entries: [
227
+ // Uniform buffer (view + proj matrices)
228
+ { binding: 0, resource: { buffer: uniformBuffer } },
229
+
230
+ { binding: 1, resource: nodeData.refs.spritesheet.data.colorTexture.sampler },
231
+ { binding: 2, resource: nodeData.refs.spritesheet.data.colorTexture.view },
232
+ { binding: 3, resource: { buffer: spriteBuf } },
233
+ { binding: 4, resource: nodeData.refs.spritesheet.data.emissiveTexture.view },
234
+ ],
235
+ });
236
+
237
+ return {
238
+ sprites: [ ],
239
+ visible: [ ],
240
+ visibleCount: 0,
241
+ viewRect: { x: 0, y: 0, w: 0, h: 0 },
242
+
243
+ spriteBuf,
244
+ uniformBuffer,
245
+
246
+ instanceCap,
247
+ instanceView,
248
+ instanceBuf,
249
+ instanceStaging,
250
+
251
+ pipeline,
252
+ bindGroup,
253
+ }
254
+ }
255
+
256
+
257
+ function ensureCapacity (cobalt, node, nInstances) {
258
+
259
+ const { instanceCap } = node.data
260
+
261
+ if (nInstances <= instanceCap)
262
+ return;
263
+
264
+ let newCap = instanceCap
265
+ if (newCap === 0)
266
+ newCap = 1024;
267
+
268
+ while (newCap < nInstances)
269
+ newCap *= 2;
270
+
271
+ node.data.instanceBuf.destroy();
272
+ node.data.instanceBuf = cobalt.device.createBuffer({
273
+ size: INSTANCE_STRIDE * newCap,
274
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
275
+ });
276
+
277
+ node.data.instanceStaging = new ArrayBuffer(INSTANCE_STRIDE * newCap);
278
+ node.data.instanceView = new DataView(node.data.instanceStaging);
279
+ node.data.instanceCap = newCap;
280
+ }
281
+
282
+
283
+ function draw (cobalt, node, commandEncoder) {
284
+
285
+ const { device, context } = cobalt;
286
+
287
+ const { instanceView, instanceBuf, instanceStaging, pipeline, bindGroup } = node.data
288
+
289
+ const { descs } = node.refs.spritesheet.data.spritesheet
290
+
291
+ const viewRect = node.data.viewRect
292
+
293
+ viewRect.x = cobalt.viewport.position[0]
294
+ viewRect.y = cobalt.viewport.position[1]
295
+ viewRect.w = cobalt.viewport.width
296
+ viewRect.h = cobalt.viewport.height
297
+
298
+ node.data.visibleCount = 0
299
+
300
+ for (const s of node.data.sprites) {
301
+ const d = descs[s.spriteID];
302
+ if (!d)
303
+ continue;
304
+
305
+ // avoid sprite viewport culling when drawing in screenspace mode (typically ui/hud layers)
306
+ if (!node.options.isScreenSpace) {
307
+ const sx = (d.FrameSize[0] * (s.sizeX) * s.scale[0]) * 0.5;
308
+ const sy = (d.FrameSize[1] * (s.sizeY) * s.scale[1]) * 0.5;
309
+ const rad = Math.hypot(sx, sy);
310
+ const x = s.position[0], y = s.position[1];
311
+ if (x + rad < viewRect.x || x - rad > viewRect.x + viewRect.w || y + rad < viewRect.y || y - rad > viewRect.y + viewRect.h)
312
+ continue
313
+ }
314
+
315
+ node.data.visible[node.data.visibleCount] = s
316
+ node.data.visibleCount++
317
+ }
318
+
319
+ ensureCapacity(cobalt, node, node.data.visibleCount)
320
+
321
+ // Pack instances into staging buffer
322
+ for (let i=0; i < node.data.visibleCount; i++){
323
+ const base = i * INSTANCE_STRIDE;
324
+ const s = node.data.visible[i];
325
+ const tint = s.tint;
326
+
327
+ instanceView.setFloat32(base + OFF_POS + 0, s.position[0], true);
328
+ instanceView.setFloat32(base + OFF_POS + 4, s.position[1], true);
329
+
330
+ instanceView.setFloat32(base + OFF_SIZE + 0, s.sizeX, true);
331
+ instanceView.setFloat32(base + OFF_SIZE + 4, s.sizeY, true);
332
+
333
+ instanceView.setFloat32(base + OFF_SCALE + 0, s.scale[0], true);
334
+ instanceView.setFloat32(base + OFF_SCALE + 4, s.scale[1], true);
335
+
336
+ instanceView.setFloat32(base + OFF_TINT + 0, tint[0], true);
337
+ instanceView.setFloat32(base + OFF_TINT + 4, tint[1], true);
338
+ instanceView.setFloat32(base + OFF_TINT + 8, tint[2], true);
339
+ instanceView.setFloat32(base + OFF_TINT + 12, tint[3], true);
340
+
341
+ instanceView.setUint32(base + OFF_SPRITEID, s.spriteID >>> 0, true);
342
+
343
+ instanceView.setFloat32(base + OFF_OPACITY, s.opacity, true);
344
+
345
+ instanceView.setFloat32(base + OFF_ROT, s.rotation, true);
346
+ }
347
+
348
+ device.queue.writeBuffer(instanceBuf, 0, instanceStaging, 0, node.data.visibleCount * INSTANCE_STRIDE);
349
+
350
+ const loadOp = node.options.loadOp || 'load'
351
+
352
+ const pass = commandEncoder.beginRenderPass({
353
+ label: "spriteHDR renderpass",
354
+ colorAttachments: [
355
+ // color
356
+ {
357
+ view: node.refs.color.data.view,
358
+ clearValue: cobalt.clearValue,
359
+ loadOp: loadOp,
360
+ storeOp: 'store',
361
+ },
362
+
363
+ // emissive
364
+ {
365
+ view: node.refs.emissive.data.view,
366
+ clearValue: cobalt.clearValue,
367
+ loadOp: 'clear',
368
+ storeOp: 'store'
369
+ }
370
+
371
+ ],
372
+ });
373
+
374
+ pass.setPipeline(pipeline);
375
+ pass.setBindGroup(0, bindGroup);
376
+ pass.setVertexBuffer(0, instanceBuf);
377
+ if (node.data.visibleCount)
378
+ pass.draw(4, node.data.visibleCount, 0, 0); // triangle strip, 4 verts per instance
379
+ pass.end();
380
+ }
381
+
382
+
383
+ function _writeSpriteBuffer (cobalt, node) {
384
+
385
+ const { device, viewport } = cobalt
386
+
387
+ const GAME_WIDTH = viewport.width / viewport.zoom
388
+ const GAME_HEIGHT = viewport.height / viewport.zoom
389
+
390
+ // left right bottom top near far
391
+ const projection = mat4.ortho(0, GAME_WIDTH, GAME_HEIGHT, 0, -10.0, 10.0)
392
+
393
+ // set 3d camera position
394
+ if (!!node.options.isScreenSpace) {
395
+ vec3.set(0, 0, 0, _tmpVec3)
396
+ }
397
+ else {
398
+ // TODO: if this doesn't introduce jitter into the crossroads render, remove this disabled code entirely.
399
+ //
400
+ // I'm disabling the rounding because I think it fails in cases where units are not expressed in pixels
401
+ // e.g., most physics engines operate on meters, not pixels, so we don't want to round to the nearest integer as that
402
+ // probably isn't high enough resolution. That would mean the camera could be snapped by up to 0.5 meters
403
+ // in that case. I think the better solution for expressing camera position in pixels is to round before calling
404
+ // cobalt.setViewportPosition(...)
405
+ //
406
+ vec3.set(-round(viewport.position[0]), -round(viewport.position[1]), 0, _tmpVec3)
407
+ //vec3.set(-viewport.position[0], -viewport.position[1], 0, _tmpVec3)
408
+ }
409
+
410
+ const view = mat4.translation(_tmpVec3)
411
+
412
+ device.queue.writeBuffer(node.data.uniformBuffer, 0, view.buffer)
413
+ device.queue.writeBuffer(node.data.uniformBuffer, 64, projection.buffer)
414
+ }