@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,262 +1,386 @@
1
- import * as publicAPI from '../sprite/public-api.js'
2
- import { FLOAT32S_PER_SPRITE } from './constants.js'
1
+ import getPreferredFormat from '../get-preferred-format.js'
2
+ import * as publicAPI from './public-api.js'
3
+ import spriteWGSL from './sprite.wgsl'
4
+ import round from 'round-half-up-symmetric'
5
+ import { mat4, vec3 } from 'wgpu-matrix'
3
6
 
4
7
 
5
- // an emissive sprite renderer
8
+ // temporary variables, allocated once to avoid garbage collection
9
+ const _tmpVec3 = vec3.create(0, 0, 0)
6
10
 
11
+ // Packed instance layout: 48 bytes (aligned for vec4 fetch)
12
+ const INSTANCE_STRIDE = 64;
7
13
 
8
- /*
9
- Sprites are typically dynamic; they can move, they are animated, they can be colored, rotated etc.
14
+ // Offsets inside one instance (bytes)
15
+ const OFF_POS = 0; // float32x2 (8B)
16
+ const OFF_SIZE = 8; // float32x2 (8B)
17
+ const OFF_SCALE = 16; // float32x2 (8B)
18
+ const OFF_TINT = 24; // float32x4 (16B)
19
+ const OFF_SPRITEID = 40; // uint32 (4B)
20
+ const OFF_OPACITY = 44; // float32 (4B)
21
+ const OFF_ROT = 48; // float32 (4B)
10
22
 
11
- These use a `SpriteRenderPass` data structure which allows for dynamically adding/removing/updating sprites at run time.
12
-
13
- Internally, `SpriteRenderPass` objects are rendered as instanced triangles.
14
- Adding and removing sprites pre-sorts all triangles based on they layer they're on + the type of sprite they are.
15
- This lines up the data nicely for WebGpu such that they don't require any work in the render loop.
16
-
17
- Each type of sprite is rendered as 2 triangles, with a number instances for each sprite.
18
- This instance data is transfered to the GPU, which is then calculated in the shaders (position, rotation, scale, tinting, opacity, etc.)
19
-
20
- All of the matrix math for these sprites is done in a vertex shader, so they are fairly efficient to move, color and rotate, but it's not free.
21
- There is still some CPU required as the number of sprites increases.
22
- */
23
23
 
24
24
  export default {
25
- type: 'cobalt:sprite',
25
+ type: "cobalt:sprite",
26
26
  refs: [
27
- { name: 'spritesheet', type: 'customResource', access: 'read' },
28
- { name: 'hdr', type: 'textureView', format: 'rgba16float', access: 'write' },
29
- { name: 'emissive', type: 'textureView', format: 'rgba16float', access: 'write' },
27
+ { name: "spritesheet", type: "customResource", access: "read" },
28
+ {
29
+ name: "color",
30
+ type: "textureView",
31
+ format: "rgba8unorm",
32
+ access: "write",
33
+ },
30
34
  ],
31
35
 
32
36
  // cobalt event handling functions
33
37
 
34
38
  // @params Object cobalt renderer world object
35
39
  // @params Object options optional data passed when initing this node
36
- onInit: async function (cobalt, options={}) {
37
- return init(cobalt, options)
40
+ onInit: async function (cobalt, options = {}) {
41
+ return init(cobalt, options);
38
42
  },
39
43
 
40
44
  onRun: function (cobalt, node, webGpuCommandEncoder) {
41
45
  // do whatever you need for this node. webgpu renderpasses, etc.
42
- draw(cobalt, node, webGpuCommandEncoder)
46
+ draw(cobalt, node, webGpuCommandEncoder);
43
47
  },
44
48
 
49
+ // Clean up GPU resources. Most WebGPU objects are GC-managed and don't
50
+ // expose destroy(); buffers/textures/query-sets do.
45
51
  onDestroy: function (cobalt, node) {
46
- // any cleanup for your node should go here (releasing textures, etc.)
47
- destroy(node)
52
+ // Explicitly destroy GPU resources that have a destroy() method
53
+ try { node.data.instanceBuf?.destroy(); } catch {}
54
+ try { node.data.spriteBuf?.destroy(); } catch {}
55
+ try { node.data.uniformBuffer?.destroy(); } catch {}
56
+
57
+ // These do not have destroy(); drop references to let GC reclaim
58
+ node.data.pipeline = null; // GPURenderPipeline
59
+ node.data.bindGroup = null; // GPUBindGroup
60
+ node.data.bindGroupLayout = null;// GPUBindGroupLayout
61
+
62
+ // CPU-side allocations
63
+ node.data.instanceStaging = null;
64
+ node.data.instanceView = null;
65
+ node.data.sprites.length = 0;
66
+ node.data.visible.length = 0;
48
67
  },
49
68
 
50
69
  onResize: function (cobalt, node) {
51
- // do whatever you need when the dimensions of the renderer change (resize textures, etc.)
70
+ _writeSpriteBuffer(cobalt, node)
52
71
  },
53
72
 
54
73
  onViewportPosition: function (cobalt, node) {
74
+ _writeSpriteBuffer(cobalt, node)
55
75
  },
56
76
 
57
77
  // optional
58
78
  customFunctions: {
59
79
  ...publicAPI,
60
80
  },
61
- }
62
-
63
-
64
- // This corresponds to a WebGPU render pass. It handles 1 sprite layer.
65
- async function init (cobalt, nodeData) {
66
- const { device } = cobalt
67
-
68
- const MAX_SPRITE_COUNT = 16192 // max number of sprites in a single sprite render pass
69
-
70
- const numInstances = MAX_SPRITE_COUNT
81
+ };
71
82
 
72
- const translateFloatCount = 2 // vec2
73
- const translateSize = Float32Array.BYTES_PER_ELEMENT * translateFloatCount // in bytes
83
+ async function init(cobalt, nodeData) {
84
+ const { device } = cobalt;
74
85
 
75
- const scaleFloatCount = 2 // vec2
76
- const scaleSize = Float32Array.BYTES_PER_ELEMENT * scaleFloatCount // in bytes
86
+ const { descs, names } = nodeData.refs.spritesheet.data.spritesheet
77
87
 
78
- const tintFloatCount = 4 // vec4
79
- const tintSize = Float32Array.BYTES_PER_ELEMENT * tintFloatCount // in bytes
80
-
81
- const opacityFloatCount = 4 // vec4. technically we only need 3 floats (opacity, rotation, emissiveIntensity) but that screws up data alignment in the shader
82
- const opacitySize = Float32Array.BYTES_PER_ELEMENT * opacityFloatCount // in bytes
83
-
84
- // instanced sprite data (scale, translation, tint, opacity, rotation, emissiveIntensity)
85
- const spriteBuffer = device.createBuffer({
86
- size: (translateSize + scaleSize + tintSize + opacitySize) * numInstances, // 4x4 matrix with 4 bytes per float32, per instance
87
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
88
- //mappedAtCreation: true,
88
+ const uniformBuffer = device.createBuffer({
89
+ size: 64 * 2, // 4x4 matrix with 4 bytes per float32, times 2 matrices (view, projection)
90
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
89
91
  })
90
92
 
91
- const spritesheet = nodeData.refs.spritesheet.data
93
+ // Pack into std430-like struct (4*float*? + vec2 + vec2 → 32 bytes). We'll just write tightly as 8 floats.
94
+ const BYTES_PER_DESC = 8 * 4; // 8 float32s
95
+ const buf = new ArrayBuffer(BYTES_PER_DESC * descs.length);
96
+ const f32 = new Float32Array(buf);
97
+ for (let i=0;i<descs.length;i++){
98
+ const d = descs[i];
99
+ const base = i * 8;
100
+ f32[base+0] = d.UvOrigin[0];
101
+ f32[base+1] = d.UvOrigin[1];
102
+ f32[base+2] = d.UvSpan[0];
103
+ f32[base+3] = d.UvSpan[1];
104
+ f32[base+4] = d.FrameSize[0];
105
+ f32[base+5] = d.FrameSize[1];
106
+ f32[base+6] = d.CenterOffset[0];
107
+ f32[base+7] = d.CenterOffset[1];
108
+ }
92
109
 
93
- const bindGroup = device.createBindGroup({
94
- layout: nodeData.refs.spritesheet.data.bindGroupLayout,
110
+ // create buffer for sprite uv lookup
111
+ const spriteBuf = device.createBuffer({
112
+ label: "sprite desc table",
113
+ size: Math.max(16, buf.byteLength),
114
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
115
+ });
116
+
117
+ device.queue.writeBuffer(spriteBuf, 0, buf);
118
+
119
+ // --- Instance buffer (growable) ---
120
+ const instanceCap = 1024;
121
+ const instanceBuf = device.createBuffer({
122
+ label: "sprite instances",
123
+ size: INSTANCE_STRIDE * instanceCap,
124
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
125
+ });
126
+ const instanceStaging = new ArrayBuffer(INSTANCE_STRIDE * instanceCap);
127
+ const instanceView = new DataView(instanceStaging);
128
+
129
+ // --- Pipeline ---
130
+ const shader = device.createShaderModule({ code: spriteWGSL });
131
+ const bgl = device.createBindGroupLayout({
95
132
  entries: [
96
133
  {
97
134
  binding: 0,
98
- resource: {
99
- buffer: spritesheet.uniformBuffer
100
- }
135
+ visibility: GPUShaderStage.VERTEX,
136
+ buffer: { type: "uniform" },
101
137
  },
102
138
  {
103
139
  binding: 1,
104
- resource: spritesheet.colorTexture.view
140
+ visibility: GPUShaderStage.FRAGMENT,
141
+ sampler: { type: "filtering" },
105
142
  },
106
143
  {
107
144
  binding: 2,
108
- resource: spritesheet.colorTexture.sampler
145
+ visibility: GPUShaderStage.FRAGMENT,
146
+ texture: { sampleType: "float" },
109
147
  },
110
148
  {
111
149
  binding: 3,
112
- resource: {
113
- buffer: spriteBuffer
114
- }
150
+ visibility: GPUShaderStage.VERTEX,
151
+ buffer: { type: "read-only-storage" },
115
152
  },
116
- {
117
- binding: 4,
118
- resource: spritesheet.emissiveTexture.view
119
- },
120
- ]
121
- })
153
+ ],
154
+ });
155
+ const pipelineLayout = device.createPipelineLayout({
156
+ bindGroupLayouts: [bgl],
157
+ });
158
+
159
+ const instLayout = {
160
+ arrayStride: INSTANCE_STRIDE,
161
+ stepMode: "instance",
162
+ attributes: [
163
+ { shaderLocation: 0, offset: OFF_POS, format: "float32x2" },
164
+ { shaderLocation: 1, offset: OFF_SIZE, format: "float32x2" },
165
+ { shaderLocation: 2, offset: OFF_SCALE, format: "float32x2" },
166
+ { shaderLocation: 3, offset: OFF_TINT, format: "float32x4" },
167
+ { shaderLocation: 4, offset: OFF_SPRITEID, format: "uint32" },
168
+ { shaderLocation: 5, offset: OFF_OPACITY, format: "float32" },
169
+ { shaderLocation: 6, offset: OFF_ROT, format: "float32" },
170
+ ],
171
+ };
172
+
173
+ const pipeline = device.createRenderPipeline({
174
+ layout: pipelineLayout,
175
+ vertex: {
176
+ module: shader,
177
+ entryPoint: "vs_main",
178
+ buffers: [instLayout],
179
+ },
180
+ fragment: {
181
+ module: shader,
182
+ entryPoint: "fs_main",
183
+ targets: [
184
+ // color
185
+ {
186
+ format: getPreferredFormat(cobalt),
187
+ blend: {
188
+ color: {
189
+ srcFactor: 'src-alpha',
190
+ dstFactor: 'one-minus-src-alpha',
191
+ },
192
+ alpha: {
193
+ srcFactor: 'zero',
194
+ dstFactor: 'one'
195
+ }
196
+ }
197
+ },
198
+ ],
199
+ },
200
+ primitive: { topology: "triangle-strip", cullMode: "none" },
201
+ multisample: { count: 1 },
202
+ });
203
+
204
+ const bindGroupLayout = bgl;
122
205
 
123
- return {
124
- // instancedDrawCalls is used to actually perform draw calls within the render pass
125
- // layout is interleaved with baseVtxIdx (the sprite type), and instanceCount (how many sprites)
126
- // [
127
- // baseVtxIdx0, instanceCount0,
128
- // baseVtxIdx1, instanceCount1,
129
- // ...
130
- // ]
131
- instancedDrawCalls: new Uint32Array(MAX_SPRITE_COUNT * 2),
132
- instancedDrawCallCount: 0,
206
+ const bindGroup = device.createBindGroup({
207
+ layout: bgl,
208
+ entries: [
209
+ // Uniform buffer (view + proj matrices)
210
+ { binding: 0, resource: { buffer: uniformBuffer } },
133
211
 
134
- bindGroup,
135
- spriteBuffer,
212
+ { binding: 1, resource: nodeData.refs.spritesheet.data.colorTexture.sampler },
213
+ { binding: 2, resource: nodeData.refs.spritesheet.data.colorTexture.view },
214
+ { binding: 3, resource: { buffer: spriteBuf } },
215
+ ],
216
+ });
136
217
 
137
- // actual sprite instance data. ordered by layer, then sprite type
138
- // this is used to update the spriteBuffer.
139
- spriteData: new Float32Array(MAX_SPRITE_COUNT * FLOAT32S_PER_SPRITE),
140
- spriteCount: 0,
218
+ return {
219
+ sprites: [ ],
220
+ visible: [ ],
221
+ visibleCount: 0,
222
+ viewRect: { x: 0, y: 0, w: 0, h: 0 },
223
+
224
+ spriteBuf,
225
+ uniformBuffer,
141
226
 
142
- spriteIndices: new Map(), // key is spriteId, value is insert index of the sprite. e.g., 0 means 1st sprite , 1 means 2nd sprite, etc.
227
+ instanceCap,
228
+ instanceView,
229
+ instanceBuf,
230
+ instanceStaging,
143
231
 
144
- // when a sprite is changed the renderpass is dirty, and should have it's instance data copied to the gpu
145
- dirty: false,
232
+ pipeline,
233
+ bindGroup,
146
234
  }
147
235
  }
148
236
 
149
237
 
150
- function draw (cobalt, node, commandEncoder) {
151
- const { device } = cobalt
238
+ function ensureCapacity (cobalt, node, nInstances) {
152
239
 
153
- // on the first render, we should clear the color attachment.
154
- // otherwise load it, so multiple sprite passes can build up data in the color and emissive textures
155
- const loadOp = node.options.loadOp || 'load'
240
+ const { instanceCap } = node.data
156
241
 
157
- if (node.data.dirty) {
158
- _rebuildSpriteDrawCalls(node.data)
159
- node.data.dirty = false
160
- }
242
+ if (nInstances <= instanceCap)
243
+ return;
161
244
 
162
- // TODO: somehow spriteCount can be come negative?! for now just guard against this
163
- if (node.data.spriteCount > 0) {
164
- const writeLength = node.data.spriteCount * FLOAT32S_PER_SPRITE * Float32Array.BYTES_PER_ELEMENT
165
- device.queue.writeBuffer(node.data.spriteBuffer, 0, node.data.spriteData.buffer, 0, writeLength)
166
- }
245
+ let newCap = instanceCap
246
+ if (newCap === 0)
247
+ newCap = 1024;
167
248
 
168
- const renderpass = commandEncoder.beginRenderPass({
169
- label: 'sprite',
170
- colorAttachments: [
171
- // color
172
- {
173
- view: node.refs.hdr.data.view,
174
- clearValue: cobalt.clearValue,
175
- loadOp,
176
- storeOp: 'store'
177
- },
249
+ while (newCap < nInstances)
250
+ newCap *= 2;
178
251
 
179
- // emissive
180
- {
181
- view: node.refs.emissive.data.view,
182
- clearValue: cobalt.clearValue,
183
- loadOp: 'clear',
184
- storeOp: 'store'
185
- }
186
- ]
187
- })
252
+ node.data.instanceBuf.destroy();
253
+ node.data.instanceBuf = cobalt.device.createBuffer({
254
+ size: INSTANCE_STRIDE * newCap,
255
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
256
+ });
188
257
 
189
- renderpass.setPipeline(node.refs.spritesheet.data.pipeline)
190
- renderpass.setBindGroup(0, node.data.bindGroup)
191
- renderpass.setVertexBuffer(0, node.refs.spritesheet.data.quads.buffer)
192
-
193
- // write sprite instance data into the storage buffer, sorted by sprite type. e.g.,
194
- // renderpass.draw(6, 1, 0, 0) // 1 hero instance
195
- // renderpass.draw(6, 14, 6, 1) // 14 bat instances
196
- // renderpass.draw(6, 5, 12, 15) // 5 bullet instances
197
-
198
- // render each sprite type's instances
199
- const vertexCount = 6
200
- let baseInstanceIdx = 0
201
-
202
- for (let i=0; i < node.data.instancedDrawCallCount; i++) {
203
- // [
204
- // baseVtxIdx0, instanceCount0,
205
- // baseVtxIdx1, instanceCount1,
206
- // ...
207
- // ]
208
- const baseVertexIdx = node.data.instancedDrawCalls[i*2 ] * vertexCount
209
- const instanceCount = node.data.instancedDrawCalls[i*2+1]
210
- renderpass.draw(vertexCount, instanceCount, baseVertexIdx, baseInstanceIdx)
211
- baseInstanceIdx += instanceCount
212
- }
213
-
214
- renderpass.end()
258
+ node.data.instanceStaging = new ArrayBuffer(INSTANCE_STRIDE * newCap);
259
+ node.data.instanceView = new DataView(node.data.instanceStaging);
260
+ node.data.instanceCap = newCap;
215
261
  }
216
262
 
217
263
 
218
- // build instancedDrawCalls
219
- function _rebuildSpriteDrawCalls (renderPass) {
220
- let currentSpriteType = -1
221
- let instanceCount = 0
222
- renderPass.instancedDrawCallCount = 0
264
+ function draw (cobalt, node, commandEncoder) {
265
+
266
+ const { device, context } = cobalt;
267
+
268
+ const { instanceView, instanceBuf, instanceStaging, pipeline, bindGroup } = node.data
223
269
 
224
- for (let i=0; i < renderPass.spriteCount; i++) {
270
+ const { descs } = node.refs.spritesheet.data.spritesheet
225
271
 
226
- // 12th float is order. lower bits 0-15 are spriteType, bits 16-23 are sprite Z index
227
- const spriteType = renderPass.spriteData[i * FLOAT32S_PER_SPRITE + 11] & 0xFFFF
272
+ const viewRect = node.data.viewRect
228
273
 
229
- if (spriteType !== currentSpriteType) {
230
- if (instanceCount > 0) {
231
- renderPass.instancedDrawCalls[renderPass.instancedDrawCallCount * 2] = currentSpriteType
232
- renderPass.instancedDrawCalls[renderPass.instancedDrawCallCount * 2 + 1] = instanceCount
233
- renderPass.instancedDrawCallCount++
234
- }
274
+ viewRect.x = cobalt.viewport.position[0]
275
+ viewRect.y = cobalt.viewport.position[1]
276
+ viewRect.w = cobalt.viewport.width
277
+ viewRect.h = cobalt.viewport.height
278
+
279
+ node.data.visibleCount = 0
235
280
 
236
- currentSpriteType = spriteType
237
- instanceCount = 0
281
+ for (const s of node.data.sprites) {
282
+ const d = descs[s.spriteID];
283
+ if (!d)
284
+ continue;
285
+
286
+ // avoid sprite viewport culling when drawing in screenspace mode (typically ui/hud layers)
287
+ if (!node.options.isScreenSpace) {
288
+ const sx = (d.FrameSize[0] * (s.sizeX) * s.scale[0]) * 0.5;
289
+ const sy = (d.FrameSize[1] * (s.sizeY) * s.scale[1]) * 0.5;
290
+ const rad = Math.hypot(sx, sy);
291
+ const x = s.position[0], y = s.position[1];
292
+ if (x + rad < viewRect.x || x - rad > viewRect.x + viewRect.w || y + rad < viewRect.y || y - rad > viewRect.y + viewRect.h)
293
+ continue
238
294
  }
239
295
 
240
- instanceCount++
296
+ node.data.visible[node.data.visibleCount] = s
297
+ node.data.visibleCount++
241
298
  }
242
299
 
243
- if (instanceCount > 0) {
244
- renderPass.instancedDrawCalls[renderPass.instancedDrawCallCount * 2] = currentSpriteType
245
- renderPass.instancedDrawCalls[renderPass.instancedDrawCallCount * 2 + 1] = instanceCount
246
- renderPass.instancedDrawCallCount++
300
+ ensureCapacity(cobalt, node, node.data.visibleCount)
301
+
302
+ // Pack instances into staging buffer
303
+ for (let i=0; i < node.data.visibleCount; i++){
304
+ const base = i * INSTANCE_STRIDE;
305
+ const s = node.data.visible[i];
306
+ const tint = s.tint;
307
+
308
+ instanceView.setFloat32(base + OFF_POS + 0, s.position[0], true);
309
+ instanceView.setFloat32(base + OFF_POS + 4, s.position[1], true);
310
+
311
+ instanceView.setFloat32(base + OFF_SIZE + 0, s.sizeX, true);
312
+ instanceView.setFloat32(base + OFF_SIZE + 4, s.sizeY, true);
313
+
314
+ instanceView.setFloat32(base + OFF_SCALE + 0, s.scale[0], true);
315
+ instanceView.setFloat32(base + OFF_SCALE + 4, s.scale[1], true);
316
+
317
+ instanceView.setFloat32(base + OFF_TINT + 0, tint[0], true);
318
+ instanceView.setFloat32(base + OFF_TINT + 4, tint[1], true);
319
+ instanceView.setFloat32(base + OFF_TINT + 8, tint[2], true);
320
+ instanceView.setFloat32(base + OFF_TINT + 12, tint[3], true);
321
+
322
+ instanceView.setUint32(base + OFF_SPRITEID, s.spriteID >>> 0, true);
323
+
324
+ instanceView.setFloat32(base + OFF_OPACITY, s.opacity, true);
325
+
326
+ instanceView.setFloat32(base + OFF_ROT, s.rotation, true);
247
327
  }
328
+
329
+ device.queue.writeBuffer(instanceBuf, 0, instanceStaging, 0, node.data.visibleCount * INSTANCE_STRIDE);
330
+
331
+ const loadOp = node.options.loadOp || 'load'
332
+
333
+ const pass = commandEncoder.beginRenderPass({
334
+ label: "sprite renderpass",
335
+ colorAttachments: [
336
+ // color
337
+ {
338
+ view: node.refs.color,
339
+ clearValue: cobalt.clearValue,
340
+ loadOp: loadOp,
341
+ storeOp: 'store',
342
+ },
343
+ ],
344
+ });
345
+
346
+ pass.setPipeline(pipeline);
347
+ pass.setBindGroup(0, bindGroup);
348
+ pass.setVertexBuffer(0, instanceBuf);
349
+ if (node.data.visibleCount)
350
+ pass.draw(4, node.data.visibleCount, 0, 0); // triangle strip, 4 verts per instance
351
+ pass.end();
248
352
  }
249
353
 
250
354
 
251
- function destroy (node) {
252
- node.data.instancedDrawCalls = null
253
-
254
- node.data.bindGroup = null
355
+ function _writeSpriteBuffer (cobalt, node) {
255
356
 
256
- node.data.spriteBuffer.destroy()
257
- node.data.spriteBuffer = null
357
+ const { device, viewport } = cobalt
258
358
 
259
- node.data.spriteData = null
260
- node.data.spriteIndices.clear()
261
- node.data.spriteIndices = null
359
+ const GAME_WIDTH = viewport.width / viewport.zoom
360
+ const GAME_HEIGHT = viewport.height / viewport.zoom
361
+
362
+ // left right bottom top near far
363
+ const projection = mat4.ortho(0, GAME_WIDTH, GAME_HEIGHT, 0, -10.0, 10.0)
364
+
365
+ // set 3d camera position
366
+ if (!!node.options.isScreenSpace) {
367
+ vec3.set(0, 0, 0, _tmpVec3)
368
+ }
369
+ else {
370
+ // TODO: if this doesn't introduce jitter into the crossroads render, remove this disabled code entirely.
371
+ //
372
+ // I'm disabling the rounding because I think it fails in cases where units are not expressed in pixels
373
+ // e.g., most physics engines operate on meters, not pixels, so we don't want to round to the nearest integer as that
374
+ // probably isn't high enough resolution. That would mean the camera could be snapped by up to 0.5 meters
375
+ // in that case. I think the better solution for expressing camera position in pixels is to round before calling
376
+ // cobalt.setViewportPosition(...)
377
+ //
378
+ vec3.set(-round(viewport.position[0]), -round(viewport.position[1]), 0, _tmpVec3)
379
+ //vec3.set(-viewport.position[0], -viewport.position[1], 0, _tmpVec3)
380
+ }
381
+
382
+ const view = mat4.translation(_tmpVec3)
383
+
384
+ device.queue.writeBuffer(node.data.uniformBuffer, 0, view.buffer)
385
+ device.queue.writeBuffer(node.data.uniformBuffer, 64, projection.buffer)
262
386
  }