@footgun/cobalt 0.1.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/LICENSE +21 -0
- package/README.md +18 -0
- package/bundle.js +284 -0
- package/cobalt2.jpeg +0 -0
- package/esbuild.js +20 -0
- package/examples/01-primitives/Game.js +8 -0
- package/examples/01-primitives/component-animation.js +8 -0
- package/examples/01-primitives/component-transform.js +13 -0
- package/examples/01-primitives/constants.js +6 -0
- package/examples/01-primitives/deps.js +2 -0
- package/examples/01-primitives/entity-sprite.js +47 -0
- package/examples/01-primitives/index.html +191 -0
- package/examples/01-primitives/system-renderer.js +37 -0
- package/examples/02-sprites/Game.js +8 -0
- package/examples/02-sprites/assets/spritesheet.json +6276 -0
- package/examples/02-sprites/assets/spritesheet.png +0 -0
- package/examples/02-sprites/assets/spritesheet_emissive.png +0 -0
- package/examples/02-sprites/component-animation.js +8 -0
- package/examples/02-sprites/component-transform.js +13 -0
- package/examples/02-sprites/constants.js +6 -0
- package/examples/02-sprites/deps.js +2 -0
- package/examples/02-sprites/entity-sprite.js +47 -0
- package/examples/02-sprites/index.html +310 -0
- package/examples/02-sprites/system-renderer.js +38 -0
- package/examples/03-tiles/Game.js +8 -0
- package/examples/03-tiles/assets/spelunky-tiles.png +0 -0
- package/examples/03-tiles/assets/spelunky0.png +0 -0
- package/examples/03-tiles/assets/spelunky1.png +0 -0
- package/examples/03-tiles/component-animation.js +8 -0
- package/examples/03-tiles/component-transform.js +13 -0
- package/examples/03-tiles/constants.js +6 -0
- package/examples/03-tiles/deps.js +2 -0
- package/examples/03-tiles/entity-sprite.js +47 -0
- package/examples/03-tiles/index.html +309 -0
- package/examples/03-tiles/system-renderer.js +38 -0
- package/examples/04-overlay/assets/spritesheet.json +22 -0
- package/examples/04-overlay/assets/spritesheet.png +0 -0
- package/examples/04-overlay/assets/spritesheet_emissive.png +0 -0
- package/examples/04-overlay/constants.js +6 -0
- package/examples/04-overlay/deps.js +1 -0
- package/examples/04-overlay/index.html +133 -0
- package/examples/05-bloom/Game.js +8 -0
- package/examples/05-bloom/assets/spritesheet.json +6276 -0
- package/examples/05-bloom/assets/spritesheet.png +0 -0
- package/examples/05-bloom/assets/spritesheet_emissive.png +0 -0
- package/examples/05-bloom/component-animation.js +8 -0
- package/examples/05-bloom/component-transform.js +13 -0
- package/examples/05-bloom/constants.js +6 -0
- package/examples/05-bloom/deps.js +2 -0
- package/examples/05-bloom/entity-sprite.js +47 -0
- package/examples/05-bloom/index.html +357 -0
- package/examples/05-bloom/system-renderer.js +38 -0
- package/examples/06-displacement/Game.js +8 -0
- package/examples/06-displacement/assets/displacement_map_repeat.jpg +0 -0
- package/examples/06-displacement/assets/spelunky-tiles.png +0 -0
- package/examples/06-displacement/assets/spelunky0.png +0 -0
- package/examples/06-displacement/assets/spelunky1.png +0 -0
- package/examples/06-displacement/component-animation.js +8 -0
- package/examples/06-displacement/component-transform.js +13 -0
- package/examples/06-displacement/constants.js +6 -0
- package/examples/06-displacement/deps.js +2 -0
- package/examples/06-displacement/entity-sprite.js +47 -0
- package/examples/06-displacement/index.html +350 -0
- package/examples/06-displacement/system-renderer.js +38 -0
- package/examples/07-sdl/assets/spritesheet.json +22 -0
- package/examples/07-sdl/assets/spritesheet.png +0 -0
- package/examples/07-sdl/assets/spritesheet_emissive.png +0 -0
- package/examples/07-sdl/main.js +109 -0
- package/examples/07-sdl/package.json +19 -0
- package/examples/08-light/Game.js +8 -0
- package/examples/08-light/assets/spelunky-tiles.png +0 -0
- package/examples/08-light/assets/spelunky0.png +0 -0
- package/examples/08-light/assets/spelunky1.png +0 -0
- package/examples/08-light/constants.js +6 -0
- package/examples/08-light/deps.js +2 -0
- package/examples/08-light/index.html +477 -0
- package/package.json +34 -0
- package/src/bloom/bloom.js +467 -0
- package/src/bloom/bloom.wgsl +176 -0
- package/src/cobalt.js +231 -0
- package/src/create-texture-from-buffer.js +39 -0
- package/src/create-texture-from-url.js +35 -0
- package/src/create-texture.js +46 -0
- package/src/deps.js +3 -0
- package/src/displacement/composition.wgsl +58 -0
- package/src/displacement/displacement-composition.ts +161 -0
- package/src/displacement/displacement-parameters-buffer.ts +44 -0
- package/src/displacement/displacement-texture.ts +221 -0
- package/src/displacement/displacement.js +160 -0
- package/src/displacement/displacement.wgsl +31 -0
- package/src/displacement/triangles-buffer.ts +95 -0
- package/src/fb-blit/fb-blit.js +161 -0
- package/src/fb-blit/fb-blit.wgsl +40 -0
- package/src/fb-texture/fb-texture.js +56 -0
- package/src/light/README.md +61 -0
- package/src/light/light.js +148 -0
- package/src/light/lights-buffer.ts +98 -0
- package/src/light/lights-renderer.ts +278 -0
- package/src/light/public-api.js +20 -0
- package/src/light/readme/01_illumination.webp +0 -0
- package/src/light/readme/02_lights_texture.webp +0 -0
- package/src/light/readme/03_lights_texture_decomposed.webp +0 -0
- package/src/light/readme/04_lights_texture_mask.webp +0 -0
- package/src/light/readme/05_lights_obstacle_decomposition.webp +0 -0
- package/src/light/readme/06_lights_hard_cast_shadows.webp +0 -0
- package/src/light/texture/lights-texture-initializer.ts +191 -0
- package/src/light/texture/lights-texture-mask.ts +286 -0
- package/src/light/texture/lights-texture.ts +121 -0
- package/src/light/types.ts +23 -0
- package/src/light/viewport.ts +63 -0
- package/src/overlay/constants.js +1 -0
- package/src/overlay/overlay.js +341 -0
- package/src/overlay/overlay.wgsl +88 -0
- package/src/primitives/constants.js +1 -0
- package/src/primitives/primitives.js +252 -0
- package/src/primitives/primitives.wgsl +54 -0
- package/src/primitives/public-api.js +325 -0
- package/src/scene-composite/scene-composite.js +168 -0
- package/src/scene-composite/scene-composite.wgsl +94 -0
- package/src/sprite/constants.js +1 -0
- package/src/sprite/create-sprite-quads.js +60 -0
- package/src/sprite/public-api.js +215 -0
- package/src/sprite/read-spritesheet.js +103 -0
- package/src/sprite/sorted-binary-insert.js +45 -0
- package/src/sprite/sprite.js +268 -0
- package/src/sprite/sprite.wgsl +103 -0
- package/src/sprite/spritesheet.js +212 -0
- package/src/tile/atlas.js +193 -0
- package/src/tile/tile.js +171 -0
- package/src/tile/tile.wgsl +105 -0
- package/src/uuid.js +3 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import * as publicAPI from '../sprite/public-api.js'
|
|
2
|
+
import createSpriteQuads from '../sprite/create-sprite-quads.js'
|
|
3
|
+
import overlayWGSL from './overlay.wgsl'
|
|
4
|
+
import sortedBinaryInsert from '../sprite/sorted-binary-insert.js'
|
|
5
|
+
import uuid from '../uuid.js'
|
|
6
|
+
import { FLOAT32S_PER_SPRITE } from './constants.js'
|
|
7
|
+
import { mat4, vec3, vec4 } from '../deps.js'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// a sprite renderer with coordinates in screen space. useful for HUD/ui stuff
|
|
11
|
+
|
|
12
|
+
const _tmpVec4 = vec4.create()
|
|
13
|
+
const _tmpVec3 = vec3.create()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
type: 'cobalt:overlay',
|
|
18
|
+
refs: [
|
|
19
|
+
{ name: 'spritesheet', type: 'customResource', access: 'read' },
|
|
20
|
+
{ name: 'color', type: 'textView', format: 'rgba8unorm', access: 'write' },
|
|
21
|
+
],
|
|
22
|
+
|
|
23
|
+
// cobalt event handling functions
|
|
24
|
+
|
|
25
|
+
// @params Object cobalt renderer world object
|
|
26
|
+
// @params Object options optional data passed when initing this node
|
|
27
|
+
onInit: async function (cobalt, options={}) {
|
|
28
|
+
return init(cobalt, options)
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
onRun: function (cobalt, node, webGpuCommandEncoder) {
|
|
32
|
+
// do whatever you need for this node. webgpu renderpasses, etc.
|
|
33
|
+
draw(cobalt, node, webGpuCommandEncoder)
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
onDestroy: function (cobalt, node) {
|
|
37
|
+
// any cleanup for your node should go here (releasing textures, etc.)
|
|
38
|
+
destroy(node)
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
onResize: function (cobalt, node) {
|
|
42
|
+
_writeOverlayBuffer(cobalt, node)
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
onViewportPosition: function (cobalt, node) {
|
|
46
|
+
_writeOverlayBuffer(cobalt, node)
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// optional
|
|
50
|
+
customFunctions: { ...publicAPI },
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
// This corresponds to a WebGPU render pass. It handles 1 sprite layer.
|
|
55
|
+
async function init (cobalt, nodeData) {
|
|
56
|
+
const { device } = cobalt
|
|
57
|
+
|
|
58
|
+
const MAX_SPRITE_COUNT = 16192 // max number of sprites in a single sprite render pass
|
|
59
|
+
|
|
60
|
+
const numInstances = MAX_SPRITE_COUNT
|
|
61
|
+
|
|
62
|
+
const translateFloatCount = 2 // vec2
|
|
63
|
+
const translateSize = Float32Array.BYTES_PER_ELEMENT * translateFloatCount // in bytes
|
|
64
|
+
|
|
65
|
+
const scaleFloatCount = 2 // vec2
|
|
66
|
+
const scaleSize = Float32Array.BYTES_PER_ELEMENT * scaleFloatCount // in bytes
|
|
67
|
+
|
|
68
|
+
const tintFloatCount = 4 // vec4
|
|
69
|
+
const tintSize = Float32Array.BYTES_PER_ELEMENT * tintFloatCount // in bytes
|
|
70
|
+
|
|
71
|
+
const opacityFloatCount = 4 // vec4. technically we only need 3 floats (opacity, rotation, emissiveIntensity) but that screws up data alignment in the shader
|
|
72
|
+
const opacitySize = Float32Array.BYTES_PER_ELEMENT * opacityFloatCount // in bytes
|
|
73
|
+
|
|
74
|
+
// instanced sprite data (scale, translation, tint, opacity)
|
|
75
|
+
const spriteBuffer = device.createBuffer({
|
|
76
|
+
size: (translateSize + scaleSize + tintSize + opacitySize) * numInstances, // 4x4 matrix with 4 bytes per float32, per instance
|
|
77
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// the view and project matrices
|
|
81
|
+
const uniformBuffer = device.createBuffer({
|
|
82
|
+
size: 64 * 2, // 4x4 matrix with 4 bytes per float32, times 2 matrices (view, projection)
|
|
83
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
87
|
+
entries: [
|
|
88
|
+
{
|
|
89
|
+
binding: 0,
|
|
90
|
+
visibility: GPUShaderStage.VERTEX,
|
|
91
|
+
buffer: { }
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
binding: 1,
|
|
95
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
96
|
+
texture: { }
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
binding: 2,
|
|
100
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
101
|
+
sampler: { }
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
binding: 3,
|
|
105
|
+
visibility: GPUShaderStage.VERTEX,
|
|
106
|
+
buffer: {
|
|
107
|
+
type: 'read-only-storage'
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const bindGroup = device.createBindGroup({
|
|
114
|
+
layout: bindGroupLayout,
|
|
115
|
+
entries: [
|
|
116
|
+
{
|
|
117
|
+
binding: 0,
|
|
118
|
+
resource: {
|
|
119
|
+
buffer: uniformBuffer,
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
binding: 1,
|
|
124
|
+
resource: nodeData.refs.spritesheet.data.colorTexture.view
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
binding: 2,
|
|
128
|
+
resource: nodeData.refs.spritesheet.data.colorTexture.sampler
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
binding: 3,
|
|
132
|
+
resource: {
|
|
133
|
+
buffer: spriteBuffer
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
141
|
+
bindGroupLayouts: [ bindGroupLayout ]
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const pipeline = device.createRenderPipeline({
|
|
145
|
+
label: 'overlay',
|
|
146
|
+
vertex: {
|
|
147
|
+
module: device.createShaderModule({
|
|
148
|
+
code: overlayWGSL
|
|
149
|
+
}),
|
|
150
|
+
entryPoint: 'vs_main',
|
|
151
|
+
buffers: [ nodeData.refs.spritesheet.data.quads.bufferLayout ]
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
fragment: {
|
|
155
|
+
module: device.createShaderModule({
|
|
156
|
+
code: overlayWGSL
|
|
157
|
+
}),
|
|
158
|
+
entryPoint: 'fs_main',
|
|
159
|
+
targets: [
|
|
160
|
+
// color
|
|
161
|
+
{
|
|
162
|
+
format: 'bgra8unorm',
|
|
163
|
+
blend: {
|
|
164
|
+
color: {
|
|
165
|
+
srcFactor: 'src-alpha',
|
|
166
|
+
dstFactor: 'one-minus-src-alpha',
|
|
167
|
+
},
|
|
168
|
+
alpha: {
|
|
169
|
+
srcFactor: 'zero',
|
|
170
|
+
dstFactor: 'one'
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
primitive: {
|
|
178
|
+
topology: 'triangle-list'
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
layout: pipelineLayout
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
// instancedDrawCalls is used to actually perform draw calls within the render pass
|
|
186
|
+
// layout is interleaved with baseVtxIdx (the sprite type), and instanceCount (how many sprites)
|
|
187
|
+
// [
|
|
188
|
+
// baseVtxIdx0, instanceCount0,
|
|
189
|
+
// baseVtxIdx1, instanceCount1,
|
|
190
|
+
// ...
|
|
191
|
+
// ]
|
|
192
|
+
instancedDrawCalls: new Uint32Array(MAX_SPRITE_COUNT * 2),
|
|
193
|
+
instancedDrawCallCount: 0,
|
|
194
|
+
|
|
195
|
+
spriteBuffer,
|
|
196
|
+
uniformBuffer,
|
|
197
|
+
pipeline,
|
|
198
|
+
bindGroupLayout,
|
|
199
|
+
bindGroup,
|
|
200
|
+
|
|
201
|
+
// actual sprite instance data. ordered by layer, then sprite type
|
|
202
|
+
// this is used to update the spriteBuffer.
|
|
203
|
+
spriteData: new Float32Array(MAX_SPRITE_COUNT * FLOAT32S_PER_SPRITE),
|
|
204
|
+
spriteCount: 0,
|
|
205
|
+
|
|
206
|
+
spriteIndices: new Map(), // key is spriteId, value is insert index of the sprite. e.g., 0 means 1st sprite , 1 means 2nd sprite, etc.
|
|
207
|
+
|
|
208
|
+
// when a sprite is changed the renderpass is dirty, and should have it's instance data copied to the gpu
|
|
209
|
+
dirty: false,
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
function draw (cobalt, node, commandEncoder) {
|
|
215
|
+
const { device } = cobalt
|
|
216
|
+
|
|
217
|
+
// on the first render, we should clear the color attachment.
|
|
218
|
+
// otherwise load it, so multiple sprite passes can build up data in the color and emissive textures
|
|
219
|
+
const loadOp = node.options.loadOp || 'load'
|
|
220
|
+
|
|
221
|
+
if (node.data.dirty) {
|
|
222
|
+
_rebuildSpriteDrawCalls(node.data)
|
|
223
|
+
node.data.dirty = false
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// TODO: somehow spriteCount can be come negative?! for now just guard against this
|
|
227
|
+
if (node.data.spriteCount > 0) {
|
|
228
|
+
const writeLength = node.data.spriteCount * FLOAT32S_PER_SPRITE * Float32Array.BYTES_PER_ELEMENT
|
|
229
|
+
device.queue.writeBuffer(node.data.spriteBuffer, 0, node.data.spriteData.buffer, 0, writeLength)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
const renderpass = commandEncoder.beginRenderPass({
|
|
234
|
+
colorAttachments: [
|
|
235
|
+
// color
|
|
236
|
+
{
|
|
237
|
+
view: node.refs.color,
|
|
238
|
+
clearValue: cobalt.clearValue,
|
|
239
|
+
loadOp: loadOp,
|
|
240
|
+
storeOp: 'store'
|
|
241
|
+
},
|
|
242
|
+
]
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
renderpass.setPipeline(node.data.pipeline)
|
|
246
|
+
renderpass.setBindGroup(0, node.data.bindGroup)
|
|
247
|
+
renderpass.setVertexBuffer(0, node.refs.spritesheet.data.quads.buffer)
|
|
248
|
+
|
|
249
|
+
// write sprite instance data into the storage buffer, sorted by sprite type. e.g.,
|
|
250
|
+
// renderpass.draw(6, 1, 0, 0) // 1 hero instance
|
|
251
|
+
// renderpass.draw(6, 14, 6, 1) // 14 bat instances
|
|
252
|
+
// renderpass.draw(6, 5, 12, 15) // 5 bullet instances
|
|
253
|
+
|
|
254
|
+
// render each sprite type's instances
|
|
255
|
+
const vertexCount = 6
|
|
256
|
+
let baseInstanceIdx = 0
|
|
257
|
+
|
|
258
|
+
for (let i=0; i < node.data.instancedDrawCallCount; i++) {
|
|
259
|
+
// [
|
|
260
|
+
// baseVtxIdx0, instanceCount0,
|
|
261
|
+
// baseVtxIdx1, instanceCount1,
|
|
262
|
+
// ...
|
|
263
|
+
// ]
|
|
264
|
+
const baseVertexIdx = node.data.instancedDrawCalls[i*2 ] * vertexCount
|
|
265
|
+
const instanceCount = node.data.instancedDrawCalls[i*2+1]
|
|
266
|
+
renderpass.draw(vertexCount, instanceCount, baseVertexIdx, baseInstanceIdx)
|
|
267
|
+
baseInstanceIdx += instanceCount
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
renderpass.end()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
// build instancedDrawCalls
|
|
275
|
+
function _rebuildSpriteDrawCalls (renderPass) {
|
|
276
|
+
let currentSpriteType = -1
|
|
277
|
+
let instanceCount = 0
|
|
278
|
+
renderPass.instancedDrawCallCount = 0
|
|
279
|
+
|
|
280
|
+
for (let i=0; i < renderPass.spriteCount; i++) {
|
|
281
|
+
|
|
282
|
+
// 12th float is order. lower bits 0-15 are spriteType, bits 16-23 are sprite Z index
|
|
283
|
+
const spriteType = renderPass.spriteData[i * FLOAT32S_PER_SPRITE + 11] & 0xFFFF
|
|
284
|
+
|
|
285
|
+
if (spriteType !== currentSpriteType) {
|
|
286
|
+
if (instanceCount > 0) {
|
|
287
|
+
renderPass.instancedDrawCalls[renderPass.instancedDrawCallCount * 2] = currentSpriteType
|
|
288
|
+
renderPass.instancedDrawCalls[renderPass.instancedDrawCallCount * 2 + 1] = instanceCount
|
|
289
|
+
renderPass.instancedDrawCallCount++
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
currentSpriteType = spriteType
|
|
293
|
+
instanceCount = 0
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
instanceCount++
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (instanceCount > 0) {
|
|
300
|
+
renderPass.instancedDrawCalls[renderPass.instancedDrawCallCount * 2] = currentSpriteType
|
|
301
|
+
renderPass.instancedDrawCalls[renderPass.instancedDrawCallCount * 2 + 1] = instanceCount
|
|
302
|
+
renderPass.instancedDrawCallCount++
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
function _writeOverlayBuffer (cobalt, nodeData) {
|
|
308
|
+
// TODO: I think this buffer can be written just once since the overlays never change. (0,0 always top left corner)
|
|
309
|
+
const zoom = 1.0 // cobalt.viewport.zoom
|
|
310
|
+
|
|
311
|
+
// TODO: is rounding really needed here?
|
|
312
|
+
const GAME_WIDTH = Math.round(cobalt.viewport.width / zoom)
|
|
313
|
+
const GAME_HEIGHT = Math.round(cobalt.viewport.height / zoom)
|
|
314
|
+
|
|
315
|
+
const projection = mat4.ortho(0, GAME_WIDTH, GAME_HEIGHT, 0, -10.0, 10.0)
|
|
316
|
+
|
|
317
|
+
// set x,y,z camera position
|
|
318
|
+
vec3.set(0, 0, 0, _tmpVec3)
|
|
319
|
+
const view = mat4.translation(_tmpVec3)
|
|
320
|
+
|
|
321
|
+
cobalt.device.queue.writeBuffer(nodeData.data.uniformBuffer, 0, view.buffer)
|
|
322
|
+
cobalt.device.queue.writeBuffer(nodeData.data.uniformBuffer, 64, projection.buffer)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
function destroy (nodeData) {
|
|
327
|
+
nodeData.data.instancedDrawCalls = null
|
|
328
|
+
|
|
329
|
+
nodeData.data.bindGroup = null
|
|
330
|
+
|
|
331
|
+
nodeData.data.spriteBuffer.destroy()
|
|
332
|
+
nodeData.data.spriteBuffer = null
|
|
333
|
+
|
|
334
|
+
nodeData.data.uniformBuffer.destroy()
|
|
335
|
+
nodeData.data.uniformBuffer = null
|
|
336
|
+
|
|
337
|
+
nodeData.data.spriteData = null
|
|
338
|
+
nodeData.data.spriteIndices.clear()
|
|
339
|
+
nodeData.data.spriteIndices = null
|
|
340
|
+
}
|
|
341
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
struct TransformData {
|
|
2
|
+
view: mat4x4<f32>,
|
|
3
|
+
projection: mat4x4<f32>
|
|
4
|
+
};
|
|
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
|
+
};
|
|
15
|
+
|
|
16
|
+
struct SpritesBuffer {
|
|
17
|
+
models: array<Sprite>,
|
|
18
|
+
};
|
|
19
|
+
|
|
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
|
+
|
|
25
|
+
|
|
26
|
+
struct Fragment {
|
|
27
|
+
@builtin(position) Position : vec4<f32>,
|
|
28
|
+
@location(0) TexCoord : vec2<f32>,
|
|
29
|
+
@location(1) Tint : vec4<f32>,
|
|
30
|
+
@location(2) Opacity: f32,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@vertex
|
|
35
|
+
fn vs_main (@builtin(instance_index) i_id : u32,
|
|
36
|
+
@location(0) vertexPosition: vec3<f32>,
|
|
37
|
+
@location(1) vertexTexCoord: vec2<f32>) -> Fragment {
|
|
38
|
+
|
|
39
|
+
var output : Fragment;
|
|
40
|
+
|
|
41
|
+
var sx: f32 = sprites.models[i_id].scale.x;
|
|
42
|
+
var sy: f32 = sprites.models[i_id].scale.y;
|
|
43
|
+
var sz: f32 = 1.0;
|
|
44
|
+
|
|
45
|
+
var rot: f32 = sprites.models[i_id].rotation;
|
|
46
|
+
|
|
47
|
+
var tx: f32 = sprites.models[i_id].translate.x;
|
|
48
|
+
var ty: f32 = sprites.models[i_id].translate.y;
|
|
49
|
+
var tz: f32 = 0;
|
|
50
|
+
|
|
51
|
+
var s = sin(rot);
|
|
52
|
+
var c = cos(rot);
|
|
53
|
+
|
|
54
|
+
// TODO: can probably hardcode a view and projection matrix since this doesn't change
|
|
55
|
+
|
|
56
|
+
// https://webglfundamentals.org/webgl/lessons/webgl-2d-matrices.html
|
|
57
|
+
|
|
58
|
+
var scaleM: mat4x4<f32> = mat4x4<f32>(sx, 0.0, 0.0, 0.0,
|
|
59
|
+
0.0, sy, 0.0, 0.0,
|
|
60
|
+
0.0, 0.0, sz, 0.0,
|
|
61
|
+
0, 0, 0, 1.0);
|
|
62
|
+
|
|
63
|
+
// rotation and translation
|
|
64
|
+
var modelM: mat4x4<f32> = mat4x4<f32>(c, s, 0.0, 0.0,
|
|
65
|
+
-s, c, 0.0, 0.0,
|
|
66
|
+
0.0, 0.0, 1.0, 0.0,
|
|
67
|
+
tx, ty, tz, 1.0) * scaleM;
|
|
68
|
+
|
|
69
|
+
output.Position = transformUBO.projection * transformUBO.view * modelM * vec4<f32>(vertexPosition, 1.0);
|
|
70
|
+
|
|
71
|
+
output.TexCoord = vertexTexCoord;
|
|
72
|
+
output.Tint = sprites.models[i_id].tint;
|
|
73
|
+
output.Opacity = sprites.models[i_id].opacity;
|
|
74
|
+
|
|
75
|
+
return output;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@fragment
|
|
79
|
+
fn fs_main (@location(0) TexCoord: vec2<f32>,
|
|
80
|
+
@location(1) Tint: vec4<f32>,
|
|
81
|
+
@location(2) Opacity: f32) -> @location(0) vec4<f32> {
|
|
82
|
+
|
|
83
|
+
var outColor: vec4<f32> = textureSample(myTexture, mySampler, TexCoord);
|
|
84
|
+
var output = vec4<f32>(outColor.rgb * (1.0 - Tint.a) + (Tint.rgb * Tint.a), outColor.a * Opacity);
|
|
85
|
+
|
|
86
|
+
return output;
|
|
87
|
+
//return vec4<f32>(1.0, 0.0, 1.0, 1.0);
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const FLOAT32S_PER_SPRITE = 12 // vec2(translate) + vec2(scale) + vec4(tint) + opacity + rotation + emissiveIntensity + sortValue
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import primitivesWGSL from './primitives.wgsl'
|
|
2
|
+
import publicAPI from './public-api.js'
|
|
3
|
+
import { FLOAT32S_PER_SPRITE } from './constants.js'
|
|
4
|
+
import { round, mat4, vec2, vec3 } from '../deps.js'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// a graphics primitives renderer (lines, boxes, etc.)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// temporary variables, allocated once to avoid garbage collection
|
|
11
|
+
const _tmpVec3 = vec3.create(0, 0, 0)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
type: 'cobalt:primitives',
|
|
16
|
+
refs: [
|
|
17
|
+
{ name: 'color', type: 'textView', format: 'rgba8unorm', 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)
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
onRun: function (cobalt, node, webGpuCommandEncoder) {
|
|
29
|
+
// do whatever you need for this node. webgpu renderpasses, etc.
|
|
30
|
+
draw(cobalt, node, webGpuCommandEncoder)
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
onDestroy: function (cobalt, node) {
|
|
34
|
+
// any cleanup for your node should go here (releasing textures, etc.)
|
|
35
|
+
destroy(node)
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
onResize: function (cobalt, node) {
|
|
39
|
+
// do whatever you need when the dimensions of the renderer change (resize textures, etc.)
|
|
40
|
+
_writeMatricesBuffer(cobalt, node)
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
onViewportPosition: function (cobalt, node) {
|
|
44
|
+
_writeMatricesBuffer(cobalt, node)
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// optional
|
|
48
|
+
customFunctions: publicAPI,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// This corresponds to a WebGPU render pass. It handles 1 sprite layer.
|
|
53
|
+
async function init (cobalt, node) {
|
|
54
|
+
const { device } = cobalt
|
|
55
|
+
|
|
56
|
+
// Define vertices and indices for your line represented as two triangles (a rectangle)
|
|
57
|
+
// For example, this could represent a line segment from (10, 10) to (100, 10) with a thickness of 10 units
|
|
58
|
+
// Updated vertices in normalized device coordinates (NDC)
|
|
59
|
+
const vertices = new Float32Array(300000)
|
|
60
|
+
|
|
61
|
+
const vertexBuffer = device.createBuffer({
|
|
62
|
+
size: vertices.byteLength,
|
|
63
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
64
|
+
//mappedAtCreation: true,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
//new Float32Array(vertexBuffer.getMappedRange()).set(vertices);
|
|
69
|
+
//vertexBuffer.unmap()
|
|
70
|
+
|
|
71
|
+
const uniformBuffer = device.createBuffer({
|
|
72
|
+
size: 64 * 2, // 4x4 matrix with 4 bytes per float32, times 2 matrices (view, projection)
|
|
73
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Shader modules
|
|
77
|
+
const shaderModule = device.createShaderModule({
|
|
78
|
+
code: primitivesWGSL,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
82
|
+
entries: [
|
|
83
|
+
{
|
|
84
|
+
binding: 0,
|
|
85
|
+
visibility: GPUShaderStage.VERTEX,
|
|
86
|
+
buffer: { }
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
92
|
+
bindGroupLayouts: [ bindGroupLayout ]
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const bindGroup = device.createBindGroup({
|
|
96
|
+
layout: bindGroupLayout,
|
|
97
|
+
entries: [
|
|
98
|
+
{
|
|
99
|
+
binding: 0,
|
|
100
|
+
resource: {
|
|
101
|
+
buffer: uniformBuffer
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
// Create render pipeline
|
|
108
|
+
const pipeline = device.createRenderPipeline({
|
|
109
|
+
layout: pipelineLayout,
|
|
110
|
+
vertex: {
|
|
111
|
+
module: shaderModule,
|
|
112
|
+
entryPoint: 'vs_main',
|
|
113
|
+
buffers: [{
|
|
114
|
+
arrayStride: 6 * Float32Array.BYTES_PER_ELEMENT, // 2 floats per vertex position + 4 floats per vertex color
|
|
115
|
+
//stepMode: 'vertex',
|
|
116
|
+
attributes: [
|
|
117
|
+
// position
|
|
118
|
+
{
|
|
119
|
+
shaderLocation: 0,
|
|
120
|
+
offset: 0,
|
|
121
|
+
format: 'float32x2',
|
|
122
|
+
},
|
|
123
|
+
// color
|
|
124
|
+
{
|
|
125
|
+
shaderLocation: 1,
|
|
126
|
+
format: 'float32x4',
|
|
127
|
+
offset: 8
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
}],
|
|
131
|
+
},
|
|
132
|
+
fragment: {
|
|
133
|
+
module: shaderModule,
|
|
134
|
+
entryPoint: 'fs_main',
|
|
135
|
+
targets: [
|
|
136
|
+
{
|
|
137
|
+
format: 'bgra8unorm',
|
|
138
|
+
blend: {
|
|
139
|
+
color: {
|
|
140
|
+
srcFactor: 'src-alpha',
|
|
141
|
+
dstFactor: 'one-minus-src-alpha',
|
|
142
|
+
},
|
|
143
|
+
alpha: {
|
|
144
|
+
srcFactor: 'zero',
|
|
145
|
+
dstFactor: 'one'
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
primitive: {
|
|
152
|
+
topology: 'triangle-list',
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
uniformBuffer, // perspective and view matrices for the camera
|
|
159
|
+
vertexBuffer,
|
|
160
|
+
pipeline,
|
|
161
|
+
bindGroup,
|
|
162
|
+
|
|
163
|
+
// triangle data used to render the primitives
|
|
164
|
+
vertexCount: 0,
|
|
165
|
+
|
|
166
|
+
dirty: false, // when more stuff has been drawn and vertexBuffer needs updating
|
|
167
|
+
vertices, // x, y, x, y, ...
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
function draw (cobalt, node, commandEncoder) {
|
|
173
|
+
|
|
174
|
+
if (node.data.vertexCount === 0) // no primitives to draw, bail
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
const { device } = cobalt
|
|
178
|
+
|
|
179
|
+
if (node.data.dirty) {
|
|
180
|
+
node.data.dirty = false
|
|
181
|
+
const stride = 6 * Float32Array.BYTES_PER_ELEMENT // 2 floats per vertex position + 4 floats per vertex color
|
|
182
|
+
|
|
183
|
+
let byteCount = node.data.vertexCount * stride
|
|
184
|
+
if (byteCount > node.data.vertexBuffer.size) {
|
|
185
|
+
console.warn('too many primitives, bailing')
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
cobalt.device.queue.writeBuffer(node.data.vertexBuffer, 0, node.data.vertices.buffer, 0, byteCount)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const loadOp = node.options.loadOp || 'load'
|
|
193
|
+
|
|
194
|
+
const renderpass = commandEncoder.beginRenderPass({
|
|
195
|
+
colorAttachments: [
|
|
196
|
+
// color
|
|
197
|
+
{
|
|
198
|
+
view: node.refs.color, //node.refs.color.data.view,
|
|
199
|
+
clearValue: cobalt.clearValue,
|
|
200
|
+
loadOp,
|
|
201
|
+
storeOp: 'store'
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
renderpass.setPipeline(node.data.pipeline)
|
|
207
|
+
renderpass.setBindGroup(0, node.data.bindGroup)
|
|
208
|
+
renderpass.setVertexBuffer(0, node.data.vertexBuffer)
|
|
209
|
+
renderpass.draw(node.data.vertexCount) // Draw 18 vertices, forming six triangles
|
|
210
|
+
renderpass.end()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
function destroy (node) {
|
|
215
|
+
node.data.vertexBuffer.destroy()
|
|
216
|
+
node.data.vertexBuffer = null
|
|
217
|
+
node.data.uniformBuffer.destroy()
|
|
218
|
+
node.data.uniformBuffer = null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
function _writeMatricesBuffer (cobalt, node) {
|
|
223
|
+
const { device } = cobalt
|
|
224
|
+
|
|
225
|
+
const GAME_WIDTH = cobalt.viewport.width / cobalt.viewport.zoom
|
|
226
|
+
const GAME_HEIGHT = cobalt.viewport.height / cobalt.viewport.zoom
|
|
227
|
+
|
|
228
|
+
// left right bottom top near far
|
|
229
|
+
const projection = mat4.ortho(0, GAME_WIDTH, GAME_HEIGHT, 0, -10.0, 10.0)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
// TODO: if this doesn't introduce jitter into the crossroads render, remove this disabled code entirely.
|
|
234
|
+
//
|
|
235
|
+
// I'm disabling the rounding because I think it fails in cases where units are not expressed in pixels
|
|
236
|
+
// e.g., most physics engines operate on meters, not pixels, so we don't want to round to the nearest integer as that
|
|
237
|
+
// probably isn't high enough resolution. That would mean the camera could be snapped by up to 0.5 meters
|
|
238
|
+
// in that case. I think the better solution for expressing camera position in pixels is to round before calling
|
|
239
|
+
// cobalt.setViewportPosition(...)
|
|
240
|
+
//
|
|
241
|
+
// set 3d camera position
|
|
242
|
+
//vec3.set(-round(viewport.position[0]), -round(viewport.position[1]), 0, _tmpVec3)
|
|
243
|
+
|
|
244
|
+
vec3.set(-cobalt.viewport.position[0], -cobalt.viewport.position[1], 0, _tmpVec3)
|
|
245
|
+
|
|
246
|
+
const view = mat4.translation(_tmpVec3)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
device.queue.writeBuffer(node.data.uniformBuffer, 0, view.buffer)
|
|
250
|
+
device.queue.writeBuffer(node.data.uniformBuffer, 64, projection.buffer)
|
|
251
|
+
}
|
|
252
|
+
|