@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.
- package/CHANGELOG.md +19 -0
- package/bundle.js +1318 -1304
- package/examples/01-primitives/index.html +1 -1
- package/examples/02-sprites/entity-sprite.js +10 -15
- package/examples/02-sprites/hdr.html +321 -0
- package/examples/02-sprites/index.html +21 -120
- package/examples/02-sprites/system-renderer.js +2 -2
- package/examples/03-tiles/index.html +87 -32
- package/examples/03-tiles/system-renderer.js +2 -2
- package/examples/04-overlay/index.html +178 -21
- package/examples/05-bloom/index.html +23 -23
- package/examples/05-bloom/system-renderer.js +2 -2
- package/examples/06-displacement/index.html +20 -112
- package/examples/06-displacement/system-renderer.js +2 -2
- package/examples/08-light/index.html +51 -123
- package/package.json +1 -1
- package/src/cobalt.js +8 -8
- package/src/sprite/public-api.js +57 -177
- package/src/sprite/sprite.js +301 -177
- package/src/sprite/sprite.wgsl +68 -87
- package/src/sprite-hdr/public-api.js +95 -0
- package/src/sprite-hdr/sprite.js +414 -0
- package/src/sprite-hdr/sprite.wgsl +101 -0
- package/src/{sprite → spritesheet}/create-sprite-quads.js +11 -11
- package/src/{sprite → spritesheet}/read-spritesheet.js +62 -28
- package/src/spritesheet/spritesheet.js +75 -0
- package/src/{tile → tile-hdr}/atlas.js +5 -3
- package/src/{tile → tile-hdr}/tile.js +15 -6
- package/examples/04-overlay/deps.js +0 -1
- package/src/overlay/constants.js +0 -1
- package/src/overlay/overlay.js +0 -343
- package/src/overlay/overlay.wgsl +0 -88
- package/src/sprite/constants.js +0 -1
- package/src/sprite/sorted-binary-insert.js +0 -45
- package/src/sprite/spritesheet.js +0 -215
- /package/examples/02-sprites/{Game.js → Global.js} +0 -0
- /package/examples/03-tiles/{Game.js → Global.js} +0 -0
- /package/examples/05-bloom/{Game.js → Global.js} +0 -0
- /package/examples/06-displacement/{Game.js → Global.js} +0 -0
- /package/examples/08-light/{Game.js → Global.js} +0 -0
- /package/src/{tile → tile-hdr}/tile.wgsl +0 -0
package/src/sprite/sprite.js
CHANGED
|
@@ -1,262 +1,386 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
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
|
-
//
|
|
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
|
-
|
|
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:
|
|
25
|
+
type: "cobalt:sprite",
|
|
26
26
|
refs: [
|
|
27
|
-
{ name:
|
|
28
|
-
{
|
|
29
|
-
|
|
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
|
-
//
|
|
47
|
-
destroy(
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
const
|
|
83
|
+
async function init(cobalt, nodeData) {
|
|
84
|
+
const { device } = cobalt;
|
|
74
85
|
|
|
75
|
-
const
|
|
76
|
-
const scaleSize = Float32Array.BYTES_PER_ELEMENT * scaleFloatCount // in bytes
|
|
86
|
+
const { descs, names } = nodeData.refs.spritesheet.data.spritesheet
|
|
77
87
|
|
|
78
|
-
const
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
135
|
+
visibility: GPUShaderStage.VERTEX,
|
|
136
|
+
buffer: { type: "uniform" },
|
|
101
137
|
},
|
|
102
138
|
{
|
|
103
139
|
binding: 1,
|
|
104
|
-
|
|
140
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
141
|
+
sampler: { type: "filtering" },
|
|
105
142
|
},
|
|
106
143
|
{
|
|
107
144
|
binding: 2,
|
|
108
|
-
|
|
145
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
146
|
+
texture: { sampleType: "float" },
|
|
109
147
|
},
|
|
110
148
|
{
|
|
111
149
|
binding: 3,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
150
|
+
visibility: GPUShaderStage.VERTEX,
|
|
151
|
+
buffer: { type: "read-only-storage" },
|
|
115
152
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
227
|
+
instanceCap,
|
|
228
|
+
instanceView,
|
|
229
|
+
instanceBuf,
|
|
230
|
+
instanceStaging,
|
|
143
231
|
|
|
144
|
-
|
|
145
|
-
|
|
232
|
+
pipeline,
|
|
233
|
+
bindGroup,
|
|
146
234
|
}
|
|
147
235
|
}
|
|
148
236
|
|
|
149
237
|
|
|
150
|
-
function
|
|
151
|
-
const { device } = cobalt
|
|
238
|
+
function ensureCapacity (cobalt, node, nInstances) {
|
|
152
239
|
|
|
153
|
-
|
|
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 (
|
|
158
|
-
|
|
159
|
-
node.data.dirty = false
|
|
160
|
-
}
|
|
242
|
+
if (nInstances <= instanceCap)
|
|
243
|
+
return;
|
|
161
244
|
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
270
|
+
const { descs } = node.refs.spritesheet.data.spritesheet
|
|
225
271
|
|
|
226
|
-
|
|
227
|
-
const spriteType = renderPass.spriteData[i * FLOAT32S_PER_SPRITE + 11] & 0xFFFF
|
|
272
|
+
const viewRect = node.data.viewRect
|
|
228
273
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
296
|
+
node.data.visible[node.data.visibleCount] = s
|
|
297
|
+
node.data.visibleCount++
|
|
241
298
|
}
|
|
242
299
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
252
|
-
node.data.instancedDrawCalls = null
|
|
253
|
-
|
|
254
|
-
node.data.bindGroup = null
|
|
355
|
+
function _writeSpriteBuffer (cobalt, node) {
|
|
255
356
|
|
|
256
|
-
|
|
257
|
-
node.data.spriteBuffer = null
|
|
357
|
+
const { device, viewport } = cobalt
|
|
258
358
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
}
|