@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.
Files changed (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/bundle.js +284 -0
  4. package/cobalt2.jpeg +0 -0
  5. package/esbuild.js +20 -0
  6. package/examples/01-primitives/Game.js +8 -0
  7. package/examples/01-primitives/component-animation.js +8 -0
  8. package/examples/01-primitives/component-transform.js +13 -0
  9. package/examples/01-primitives/constants.js +6 -0
  10. package/examples/01-primitives/deps.js +2 -0
  11. package/examples/01-primitives/entity-sprite.js +47 -0
  12. package/examples/01-primitives/index.html +191 -0
  13. package/examples/01-primitives/system-renderer.js +37 -0
  14. package/examples/02-sprites/Game.js +8 -0
  15. package/examples/02-sprites/assets/spritesheet.json +6276 -0
  16. package/examples/02-sprites/assets/spritesheet.png +0 -0
  17. package/examples/02-sprites/assets/spritesheet_emissive.png +0 -0
  18. package/examples/02-sprites/component-animation.js +8 -0
  19. package/examples/02-sprites/component-transform.js +13 -0
  20. package/examples/02-sprites/constants.js +6 -0
  21. package/examples/02-sprites/deps.js +2 -0
  22. package/examples/02-sprites/entity-sprite.js +47 -0
  23. package/examples/02-sprites/index.html +310 -0
  24. package/examples/02-sprites/system-renderer.js +38 -0
  25. package/examples/03-tiles/Game.js +8 -0
  26. package/examples/03-tiles/assets/spelunky-tiles.png +0 -0
  27. package/examples/03-tiles/assets/spelunky0.png +0 -0
  28. package/examples/03-tiles/assets/spelunky1.png +0 -0
  29. package/examples/03-tiles/component-animation.js +8 -0
  30. package/examples/03-tiles/component-transform.js +13 -0
  31. package/examples/03-tiles/constants.js +6 -0
  32. package/examples/03-tiles/deps.js +2 -0
  33. package/examples/03-tiles/entity-sprite.js +47 -0
  34. package/examples/03-tiles/index.html +309 -0
  35. package/examples/03-tiles/system-renderer.js +38 -0
  36. package/examples/04-overlay/assets/spritesheet.json +22 -0
  37. package/examples/04-overlay/assets/spritesheet.png +0 -0
  38. package/examples/04-overlay/assets/spritesheet_emissive.png +0 -0
  39. package/examples/04-overlay/constants.js +6 -0
  40. package/examples/04-overlay/deps.js +1 -0
  41. package/examples/04-overlay/index.html +133 -0
  42. package/examples/05-bloom/Game.js +8 -0
  43. package/examples/05-bloom/assets/spritesheet.json +6276 -0
  44. package/examples/05-bloom/assets/spritesheet.png +0 -0
  45. package/examples/05-bloom/assets/spritesheet_emissive.png +0 -0
  46. package/examples/05-bloom/component-animation.js +8 -0
  47. package/examples/05-bloom/component-transform.js +13 -0
  48. package/examples/05-bloom/constants.js +6 -0
  49. package/examples/05-bloom/deps.js +2 -0
  50. package/examples/05-bloom/entity-sprite.js +47 -0
  51. package/examples/05-bloom/index.html +357 -0
  52. package/examples/05-bloom/system-renderer.js +38 -0
  53. package/examples/06-displacement/Game.js +8 -0
  54. package/examples/06-displacement/assets/displacement_map_repeat.jpg +0 -0
  55. package/examples/06-displacement/assets/spelunky-tiles.png +0 -0
  56. package/examples/06-displacement/assets/spelunky0.png +0 -0
  57. package/examples/06-displacement/assets/spelunky1.png +0 -0
  58. package/examples/06-displacement/component-animation.js +8 -0
  59. package/examples/06-displacement/component-transform.js +13 -0
  60. package/examples/06-displacement/constants.js +6 -0
  61. package/examples/06-displacement/deps.js +2 -0
  62. package/examples/06-displacement/entity-sprite.js +47 -0
  63. package/examples/06-displacement/index.html +350 -0
  64. package/examples/06-displacement/system-renderer.js +38 -0
  65. package/examples/07-sdl/assets/spritesheet.json +22 -0
  66. package/examples/07-sdl/assets/spritesheet.png +0 -0
  67. package/examples/07-sdl/assets/spritesheet_emissive.png +0 -0
  68. package/examples/07-sdl/main.js +109 -0
  69. package/examples/07-sdl/package.json +19 -0
  70. package/examples/08-light/Game.js +8 -0
  71. package/examples/08-light/assets/spelunky-tiles.png +0 -0
  72. package/examples/08-light/assets/spelunky0.png +0 -0
  73. package/examples/08-light/assets/spelunky1.png +0 -0
  74. package/examples/08-light/constants.js +6 -0
  75. package/examples/08-light/deps.js +2 -0
  76. package/examples/08-light/index.html +477 -0
  77. package/package.json +34 -0
  78. package/src/bloom/bloom.js +467 -0
  79. package/src/bloom/bloom.wgsl +176 -0
  80. package/src/cobalt.js +231 -0
  81. package/src/create-texture-from-buffer.js +39 -0
  82. package/src/create-texture-from-url.js +35 -0
  83. package/src/create-texture.js +46 -0
  84. package/src/deps.js +3 -0
  85. package/src/displacement/composition.wgsl +58 -0
  86. package/src/displacement/displacement-composition.ts +161 -0
  87. package/src/displacement/displacement-parameters-buffer.ts +44 -0
  88. package/src/displacement/displacement-texture.ts +221 -0
  89. package/src/displacement/displacement.js +160 -0
  90. package/src/displacement/displacement.wgsl +31 -0
  91. package/src/displacement/triangles-buffer.ts +95 -0
  92. package/src/fb-blit/fb-blit.js +161 -0
  93. package/src/fb-blit/fb-blit.wgsl +40 -0
  94. package/src/fb-texture/fb-texture.js +56 -0
  95. package/src/light/README.md +61 -0
  96. package/src/light/light.js +148 -0
  97. package/src/light/lights-buffer.ts +98 -0
  98. package/src/light/lights-renderer.ts +278 -0
  99. package/src/light/public-api.js +20 -0
  100. package/src/light/readme/01_illumination.webp +0 -0
  101. package/src/light/readme/02_lights_texture.webp +0 -0
  102. package/src/light/readme/03_lights_texture_decomposed.webp +0 -0
  103. package/src/light/readme/04_lights_texture_mask.webp +0 -0
  104. package/src/light/readme/05_lights_obstacle_decomposition.webp +0 -0
  105. package/src/light/readme/06_lights_hard_cast_shadows.webp +0 -0
  106. package/src/light/texture/lights-texture-initializer.ts +191 -0
  107. package/src/light/texture/lights-texture-mask.ts +286 -0
  108. package/src/light/texture/lights-texture.ts +121 -0
  109. package/src/light/types.ts +23 -0
  110. package/src/light/viewport.ts +63 -0
  111. package/src/overlay/constants.js +1 -0
  112. package/src/overlay/overlay.js +341 -0
  113. package/src/overlay/overlay.wgsl +88 -0
  114. package/src/primitives/constants.js +1 -0
  115. package/src/primitives/primitives.js +252 -0
  116. package/src/primitives/primitives.wgsl +54 -0
  117. package/src/primitives/public-api.js +325 -0
  118. package/src/scene-composite/scene-composite.js +168 -0
  119. package/src/scene-composite/scene-composite.wgsl +94 -0
  120. package/src/sprite/constants.js +1 -0
  121. package/src/sprite/create-sprite-quads.js +60 -0
  122. package/src/sprite/public-api.js +215 -0
  123. package/src/sprite/read-spritesheet.js +103 -0
  124. package/src/sprite/sorted-binary-insert.js +45 -0
  125. package/src/sprite/sprite.js +268 -0
  126. package/src/sprite/sprite.wgsl +103 -0
  127. package/src/sprite/spritesheet.js +212 -0
  128. package/src/tile/atlas.js +193 -0
  129. package/src/tile/tile.js +171 -0
  130. package/src/tile/tile.wgsl +105 -0
  131. 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
+