@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,215 @@
1
+ import sortedBinaryInsert from './sorted-binary-insert.js'
2
+ import uuid from '../uuid.js'
3
+ import { FLOAT32S_PER_SPRITE } from './constants.js'
4
+
5
+
6
+ // returns a unique identifier for the created sprite
7
+ export function addSprite (cobalt, renderPass, name, position, scale, tint, opacity, rotation, zIndex) {
8
+
9
+ const spritesheet = renderPass.refs.spritesheet.data.spritesheet
10
+ renderPass = renderPass.data
11
+
12
+ const spriteType = spritesheet.locations.indexOf(name)
13
+
14
+ // find the place in our spriteData where this sprite belongs.
15
+ const insertIdx = sortedBinaryInsert(zIndex, spriteType, renderPass)
16
+
17
+ // shift down all the data in spriteData from insertIdx to spriteCount-1
18
+ // https://stackoverflow.com/questions/35563529/how-to-copy-typedarray-into-another-typedarray
19
+ const offset = (insertIdx + 1) * FLOAT32S_PER_SPRITE
20
+ renderPass.spriteData.set(
21
+ renderPass.spriteData.subarray(insertIdx * FLOAT32S_PER_SPRITE, renderPass.spriteCount * FLOAT32S_PER_SPRITE),
22
+ offset
23
+ )
24
+
25
+ copySpriteDataToBuffer(renderPass, spritesheet, insertIdx, name, position, scale, tint, opacity, rotation, zIndex)
26
+
27
+ // shift down all of the sprite indices
28
+ for (const [ spriteId, idx ] of renderPass.spriteIndices)
29
+ if (idx >= insertIdx)
30
+ renderPass.spriteIndices.set(spriteId, idx+1)
31
+
32
+ // store the location of this sprite's data in the renderPass's float32array so that we can
33
+ // reference it later, when we need to remove or update this sprite component
34
+ const spriteId = uuid()
35
+
36
+ renderPass.spriteIndices.set(spriteId, insertIdx)
37
+ renderPass.spriteCount++
38
+ renderPass.dirty = true
39
+
40
+ return spriteId
41
+ }
42
+
43
+
44
+ export function removeSprite (cobalt, renderPass, spriteId) {
45
+ renderPass = renderPass.data
46
+ const removeIdx = renderPass.spriteIndices.get(spriteId)
47
+
48
+ // shift up all of the sprites after the remove location by 1
49
+ for (const [ spriteId, idx ] of renderPass.spriteIndices)
50
+ if (idx > removeIdx)
51
+ renderPass.spriteIndices.set(spriteId, idx-1)
52
+
53
+ // shift up all the data in spriteData from removeIdx to spriteCount-1
54
+ // https://stackoverflow.com/questions/35563529/how-to-copy-typedarray-into-another-typedarray
55
+ let offset = removeIdx * FLOAT32S_PER_SPRITE
56
+ renderPass.spriteData.set(
57
+ renderPass.spriteData.subarray((removeIdx + 1) * FLOAT32S_PER_SPRITE, renderPass.spriteCount * FLOAT32S_PER_SPRITE),
58
+ offset
59
+ )
60
+
61
+ renderPass.spriteIndices.delete(spriteId)
62
+ renderPass.spriteCount--
63
+ renderPass.dirty = true
64
+ }
65
+
66
+
67
+ // remove all sprites
68
+ export function clear (cobalt, renderPass) {
69
+ renderPass = renderPass.data
70
+ renderPass.spriteIndices.clear()
71
+ renderPass.spriteCount = 0
72
+ renderPass.instancedDrawCallCount = 0
73
+ renderPass.dirty = true
74
+ }
75
+
76
+
77
+ export function setSpriteName (cobalt, renderPass, spriteId, name, scale) {
78
+ const spritesheet = renderPass.refs.spritesheet.data.spritesheet
79
+ renderPass = renderPass.data
80
+
81
+ const spriteType = spritesheet.locations.indexOf(name)
82
+
83
+ const SPRITE_WIDTH = spritesheet.spriteMeta[name].w
84
+ const SPRITE_HEIGHT = spritesheet.spriteMeta[name].h
85
+
86
+ const spriteIdx = renderPass.spriteIndices.get(spriteId)
87
+ const offset = spriteIdx * FLOAT32S_PER_SPRITE
88
+
89
+ renderPass.spriteData[offset+2] = SPRITE_WIDTH * scale[0]
90
+ renderPass.spriteData[offset+3] = SPRITE_HEIGHT * scale[1]
91
+
92
+ // 12th float is order. lower bits 0-15 are spriteType, bits 16-23 are sprite Z index
93
+ const zIndex = renderPass.spriteData[offset + 11] >> 16 & 0xFF
94
+
95
+ // sortValue is used to sort the sprite by layer, then sprite type
96
+ // zIndex 0-255 (8 bits)
97
+ // spriteType 0-65,535 (16 bits)
98
+ const sortValue = (zIndex << 16 & 0xFF0000) | (spriteType & 0xFFFF)
99
+ renderPass.spriteData[offset+11] = sortValue
100
+
101
+ renderPass.dirty = true
102
+ }
103
+
104
+
105
+ export function setSpritePosition (cobalt, renderPass, spriteId, position) {
106
+ renderPass = renderPass.data
107
+
108
+ const spriteIdx = renderPass.spriteIndices.get(spriteId)
109
+ const offset = spriteIdx * FLOAT32S_PER_SPRITE
110
+
111
+ renderPass.spriteData[offset] = position[0]
112
+ renderPass.spriteData[offset+1] = position[1]
113
+
114
+ renderPass.dirty = true
115
+ }
116
+
117
+
118
+ export function setSpriteTint (cobalt, renderPass, spriteId, tint) {
119
+ renderPass = renderPass.data
120
+
121
+ const spriteIdx = renderPass.spriteIndices.get(spriteId)
122
+ const offset = spriteIdx * FLOAT32S_PER_SPRITE
123
+
124
+ renderPass.spriteData[offset+4] = tint[0]
125
+ renderPass.spriteData[offset+5] = tint[1]
126
+ renderPass.spriteData[offset+6] = tint[2]
127
+ renderPass.spriteData[offset+7] = tint[3]
128
+
129
+ renderPass.dirty = true
130
+ }
131
+
132
+
133
+ export function setSpriteOpacity (cobalt, renderPass, spriteId, opacity) {
134
+ renderPass = renderPass.data
135
+
136
+ const spriteIdx = renderPass.spriteIndices.get(spriteId)
137
+ const offset = spriteIdx * FLOAT32S_PER_SPRITE
138
+
139
+ renderPass.spriteData[offset+8] = opacity
140
+
141
+ renderPass.dirty = true
142
+ }
143
+
144
+
145
+ export function setSpriteRotation (cobalt, renderPass, spriteId, rotation) {
146
+ renderPass = renderPass.data
147
+
148
+ const spriteIdx = renderPass.spriteIndices.get(spriteId)
149
+ const offset = spriteIdx * FLOAT32S_PER_SPRITE
150
+
151
+ renderPass.spriteData[offset+9] = rotation
152
+ renderPass.dirty = true
153
+ }
154
+
155
+
156
+ export function setSpriteScale (cobalt, renderPass, spriteId, name, scale) {
157
+ const spritesheet = renderPass.refs.spritesheet.data.spritesheet
158
+ renderPass = renderPass.data
159
+
160
+ const spriteIdx = renderPass.spriteIndices.get(spriteId)
161
+ const offset = spriteIdx * FLOAT32S_PER_SPRITE
162
+
163
+ const SPRITE_WIDTH = spritesheet.spriteMeta[name].w
164
+ const SPRITE_HEIGHT = spritesheet.spriteMeta[name].h
165
+
166
+ renderPass.spriteData[offset+2] = SPRITE_WIDTH * scale[0]
167
+ renderPass.spriteData[offset+3] = SPRITE_HEIGHT * scale[1]
168
+
169
+ renderPass.dirty = true
170
+ }
171
+
172
+
173
+ export function setSprite (cobalt, renderPass, spriteId, name, position, scale, tint, opacity, rotation, zIndex) {
174
+ const spritesheet = renderPass.refs.spritesheet.data.spritesheet
175
+ renderPass = renderPass.data
176
+
177
+ const spriteIdx = renderPass.spriteIndices.get(spriteId)
178
+ copySpriteDataToBuffer(renderPass, spritesheet, spriteIdx, name, position, scale, tint, opacity, rotation, zIndex)
179
+
180
+ renderPass.dirty = true
181
+ }
182
+
183
+
184
+ // copy sprite data into the webgpu renderpass
185
+ function copySpriteDataToBuffer (renderPass, spritesheet, insertIdx, name, position, scale, tint, opacity, rotation, zIndex) {
186
+
187
+ if (!spritesheet.spriteMeta[name])
188
+ throw new Error(`Sprite name ${name} could not be found in the spritesheet metaData`)
189
+
190
+ const offset = insertIdx * FLOAT32S_PER_SPRITE
191
+
192
+ const SPRITE_WIDTH = spritesheet.spriteMeta[name].w
193
+ const SPRITE_HEIGHT = spritesheet.spriteMeta[name].h
194
+
195
+ // sortValue is used to sort the sprite by layer, then sprite type
196
+ // layer can be a value up to 255 (8 bits)
197
+ // spriteType can be a value up to 65,535 (16 bits)
198
+ const spriteType = spritesheet.locations.indexOf(name)
199
+ const sortValue = (zIndex << 16 & 0xFF0000) | (spriteType & 0xFFFF)
200
+
201
+ renderPass.spriteData[offset] = position[0]
202
+ renderPass.spriteData[offset+1] = position[1]
203
+ renderPass.spriteData[offset+2] = SPRITE_WIDTH * scale[0]
204
+ renderPass.spriteData[offset+3] = SPRITE_HEIGHT * scale[1]
205
+ renderPass.spriteData[offset+4] = tint[0]
206
+ renderPass.spriteData[offset+5] = tint[1]
207
+ renderPass.spriteData[offset+6] = tint[2]
208
+ renderPass.spriteData[offset+7] = tint[3]
209
+ renderPass.spriteData[offset+8] = opacity
210
+ renderPass.spriteData[offset+9] = rotation
211
+ // we used to set emissive intensity per-sprite, but now we use the alpha channel in the emissions texure,
212
+ // which enables us to adjust emission strength on a per-pixel basis. Copying it into the sprite data is a leftover
213
+ //renderPass.spriteData[offset+10] = emissiveIntensity
214
+ renderPass.spriteData[offset+11] = sortValue
215
+ }
@@ -0,0 +1,103 @@
1
+ // take a texturepacker json hash export and convert it into a Float32Array
2
+ // copied into the renderer's vertex buffer
3
+ //
4
+ // @return Float32Array vertices (interleaved positions and uvs)
5
+ export default function readSpriteSheet (spritesheetJson) {
6
+
7
+ // a sprite is a quad (2 triangles) so it has 6 vertices
8
+ // each vertex has 5 float32 (interleaved vec3 position, vec2 uv)
9
+ const spriteFloatCount = 5 * 6
10
+
11
+ // each key in the spritesheet is a unique sprite type
12
+ const spriteCount = Object.keys(spritesheetJson.frames).length
13
+
14
+ const vertices = new Float32Array(spriteCount * spriteFloatCount)
15
+
16
+ /*
17
+ stores mapping between sprite name and first vertex index. e.g.,
18
+ [
19
+ 'hero_run-0', // 1st vertex index is at 0
20
+ 'bullet_travel-0' // 1st vertex index is at 6
21
+ 'bob_idle-1' // 1st vertex index is at 12
22
+ ]
23
+ these will alway be multiples of 6, because there are 6 vertices per sprite
24
+ */
25
+ const locations = [ ]
26
+
27
+ const spriteMeta = { }
28
+
29
+ let i = 0
30
+
31
+ for (const frameName in spritesheetJson.frames) {
32
+ const frame = spritesheetJson.frames[frameName]
33
+
34
+ locations.push(frameName)
35
+
36
+ spriteMeta[frameName] = frame.sourceSize
37
+
38
+ // iterate over each sprite and fill it's position and u,v coords in the output
39
+
40
+ // calculate normalized vertex coordinates, accounting for trimmed space
41
+ const minX = -0.5 + (frame.spriteSourceSize.x / frame.sourceSize.w)
42
+ const minY = -0.5 + (frame.spriteSourceSize.y / frame.sourceSize.h)
43
+
44
+ const maxX = -0.5 + ((frame.spriteSourceSize.x + frame.spriteSourceSize.w) / frame.sourceSize.w)
45
+ const maxY = -0.5 + ((frame.spriteSourceSize.y + frame.spriteSourceSize.h) / frame.sourceSize.h)
46
+
47
+ const p0 = [ minX, minY, 0 ]
48
+ const p1 = [ minX, maxY, 0 ]
49
+ const p2 = [ maxX, maxY, 0 ]
50
+ const p3 = [ maxX, minY, 0 ]
51
+
52
+
53
+ // calculate uvs
54
+ // u,v coordinates specify top left as 0,0 bottom right as 1,1
55
+ const minU = 0.0 + (frame.frame.x / spritesheetJson.meta.size.w)
56
+ const minV = 0.0 + (frame.frame.y / spritesheetJson.meta.size.h)
57
+ const maxU = 0.0 + ((frame.frame.x + frame.frame.w) / spritesheetJson.meta.size.w)
58
+ const maxV = 0.0 + ((frame.frame.y + frame.frame.h) / spritesheetJson.meta.size.h)
59
+
60
+ const uv0 = [ minU, minV ]
61
+ const uv1 = [ minU, maxV ]
62
+ const uv2 = [ maxU, maxV ]
63
+ const uv3 = [ maxU, minV ]
64
+
65
+
66
+ // quad triangles are [ p0, p1, p2 ] , [ p0, p2, p3 ]
67
+ // vertex data is interleaved; a single vertex has a vec3 position followed immediately by vec2 uv
68
+ vertices.set(p0, i)
69
+ vertices.set(uv0, i + 3)
70
+
71
+ vertices.set(p1, i + 5)
72
+ vertices.set(uv1, i + 8)
73
+
74
+ vertices.set(p2, i + 10)
75
+ vertices.set(uv2, i + 13)
76
+
77
+ vertices.set(p0, i + 15)
78
+ vertices.set(uv0, i + 18)
79
+
80
+ vertices.set(p2, i + 20)
81
+ vertices.set(uv2, i + 23)
82
+
83
+ vertices.set(p3, i + 25)
84
+ vertices.set(uv3, i + 28)
85
+
86
+ i += spriteFloatCount
87
+ }
88
+
89
+ return { /*spriteCount, */ spriteMeta, locations, vertices }
90
+ }
91
+
92
+
93
+ /*
94
+ texturepacker frame structure:
95
+ "f2.png":
96
+ {
97
+ "frame": {"x":15,"y":1,"w":10,"h":15},
98
+ "rotated": false,
99
+ "trimmed": true,
100
+ "spriteSourceSize": {"x":22,"y":17,"w":10,"h":15},
101
+ "sourceSize": {"w":32,"h":32}
102
+ },
103
+ */
@@ -0,0 +1,45 @@
1
+ import { FLOAT32S_PER_SPRITE } from './constants.js'
2
+
3
+
4
+ // return the index into the renderPass. array where the new sprite should be inserted
5
+ export default function sortedBinaryInsert (spriteZIndex, spriteType, renderPass) {
6
+
7
+ if (renderPass.spriteCount === 0)
8
+ return 0
9
+
10
+ let low = 0
11
+ let high = renderPass.spriteCount - 1
12
+
13
+ // order is used to sort the sprite by layer, then sprite type
14
+ // zIndex 0-255 (8 bits)
15
+ // spriteType 0-65,535 (16 bits)
16
+ const order = (spriteZIndex << 16 & 0xFF0000) | (spriteType & 0xFFFF)
17
+
18
+ // binary search through spriteData since it's already sorted low to high
19
+ while (low <= high) {
20
+
21
+ // the 12th float of each sprite stores the sortValue
22
+
23
+ const lowOrder = renderPass.spriteData[low * FLOAT32S_PER_SPRITE + 11]
24
+ if (order <= lowOrder)
25
+ return low
26
+
27
+ const highOrder = renderPass.spriteData[high * FLOAT32S_PER_SPRITE + 11]
28
+ if (order >= highOrder)
29
+ return high + 1
30
+
31
+ const mid = Math.floor((low + high) / 2)
32
+
33
+ const midOrder = renderPass.spriteData[mid * FLOAT32S_PER_SPRITE + 11]
34
+
35
+ if(order === midOrder)
36
+ return mid + 1
37
+
38
+ if (order > midOrder)
39
+ low = mid + 1
40
+ else
41
+ high = mid - 1
42
+ }
43
+
44
+ return low
45
+ }
@@ -0,0 +1,268 @@
1
+ import * as publicAPI from '../sprite/public-api.js'
2
+ import { FLOAT32S_PER_SPRITE } from './constants.js'
3
+
4
+
5
+ // an emissive sprite renderer
6
+
7
+
8
+ /*
9
+ Sprites are typically dynamic; they can move, they are animated, they can be colored, rotated etc.
10
+
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
+
24
+ export default {
25
+ type: 'cobalt:sprite',
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' },
30
+ ],
31
+
32
+ // cobalt event handling functions
33
+
34
+ // @params Object cobalt renderer world object
35
+ // @params Object options optional data passed when initing this node
36
+ onInit: async function (cobalt, options={}) {
37
+ return init(cobalt, options)
38
+ },
39
+
40
+ onRun: function (cobalt, node, webGpuCommandEncoder) {
41
+ // do whatever you need for this node. webgpu renderpasses, etc.
42
+ draw(cobalt, node, webGpuCommandEncoder)
43
+ },
44
+
45
+ onDestroy: function (cobalt, node) {
46
+ // any cleanup for your node should go here (releasing textures, etc.)
47
+ destroy(node)
48
+ },
49
+
50
+ onResize: function (cobalt, node) {
51
+ // do whatever you need when the dimensions of the renderer change (resize textures, etc.)
52
+ },
53
+
54
+ onViewportPosition: function (cobalt, node) {
55
+ },
56
+
57
+ // optional
58
+ customFunctions: {
59
+ ...publicAPI,
60
+ },
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
71
+
72
+ const translateFloatCount = 2 // vec2
73
+ const translateSize = Float32Array.BYTES_PER_ELEMENT * translateFloatCount // in bytes
74
+
75
+ const scaleFloatCount = 2 // vec2
76
+ const scaleSize = Float32Array.BYTES_PER_ELEMENT * scaleFloatCount // in bytes
77
+
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,
89
+ })
90
+
91
+ const spritesheet = nodeData.refs.spritesheet.data
92
+
93
+ const bindGroup = device.createBindGroup({
94
+ layout: nodeData.refs.spritesheet.data.bindGroupLayout,
95
+ entries: [
96
+ {
97
+ binding: 0,
98
+ resource: {
99
+ buffer: spritesheet.uniformBuffer
100
+ }
101
+ },
102
+ {
103
+ binding: 1,
104
+ resource: spritesheet.colorTexture.view
105
+ },
106
+ {
107
+ binding: 2,
108
+ resource: spritesheet.colorTexture.sampler
109
+ },
110
+ {
111
+ binding: 3,
112
+ resource: {
113
+ buffer: spriteBuffer
114
+ }
115
+ },
116
+ {
117
+ binding: 4,
118
+ resource: spritesheet.emissiveTexture.view
119
+ },
120
+ ]
121
+ })
122
+
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,
133
+
134
+ bindGroup,
135
+ spriteBuffer,
136
+
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,
141
+
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.
143
+
144
+ // when a sprite is changed the renderpass is dirty, and should have it's instance data copied to the gpu
145
+ dirty: false,
146
+ }
147
+ }
148
+
149
+
150
+ function draw (cobalt, node, commandEncoder) {
151
+ const { device } = cobalt
152
+
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'
156
+
157
+ if (node.data.dirty) {
158
+ _rebuildSpriteDrawCalls(node.data)
159
+ node.data.dirty = false
160
+ }
161
+
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
+ }
167
+
168
+ const renderpass = commandEncoder.beginRenderPass({
169
+ colorAttachments: [
170
+ // color
171
+ {
172
+ view: node.refs.hdr.data.view,
173
+ clearValue: cobalt.clearValue,
174
+ loadOp,
175
+ storeOp: 'store'
176
+ },
177
+
178
+ // emissive
179
+ {
180
+ view: node.refs.emissive.data.view,
181
+ clearValue: cobalt.clearValue,
182
+ loadOp: 'clear',
183
+ storeOp: 'store'
184
+ }
185
+ ]
186
+ })
187
+
188
+ renderpass.setPipeline(node.refs.spritesheet.data.pipeline)
189
+ renderpass.setBindGroup(0, node.data.bindGroup)
190
+ renderpass.setVertexBuffer(0, node.refs.spritesheet.data.quads.buffer)
191
+
192
+ // write sprite instance data into the storage buffer, sorted by sprite type. e.g.,
193
+ // renderpass.draw(6, 1, 0, 0) // 1 hero instance
194
+ // renderpass.draw(6, 14, 6, 1) // 14 bat instances
195
+ // renderpass.draw(6, 5, 12, 15) // 5 bullet instances
196
+
197
+ // render each sprite type's instances
198
+ const vertexCount = 6
199
+ let baseInstanceIdx = 0
200
+
201
+ for (let i=0; i < node.data.instancedDrawCallCount; i++) {
202
+ // [
203
+ // baseVtxIdx0, instanceCount0,
204
+ // baseVtxIdx1, instanceCount1,
205
+ // ...
206
+ // ]
207
+ const baseVertexIdx = node.data.instancedDrawCalls[i*2 ] * vertexCount
208
+ const instanceCount = node.data.instancedDrawCalls[i*2+1]
209
+ renderpass.draw(vertexCount, instanceCount, baseVertexIdx, baseInstanceIdx)
210
+ baseInstanceIdx += instanceCount
211
+ }
212
+
213
+ renderpass.end()
214
+ }
215
+
216
+
217
+ // build instancedDrawCalls
218
+ function _rebuildSpriteDrawCalls (renderPass) {
219
+ let currentSpriteType = -1
220
+ let instanceCount = 0
221
+ renderPass.instancedDrawCallCount = 0
222
+
223
+ for (let i=0; i < renderPass.spriteCount; i++) {
224
+
225
+ // 12th float is order. lower bits 0-15 are spriteType, bits 16-23 are sprite Z index
226
+ const spriteType = renderPass.spriteData[i * FLOAT32S_PER_SPRITE + 11] & 0xFFFF
227
+
228
+ if (spriteType !== currentSpriteType) {
229
+ if (instanceCount > 0) {
230
+ renderPass.instancedDrawCalls[renderPass.instancedDrawCallCount * 2] = currentSpriteType
231
+ renderPass.instancedDrawCalls[renderPass.instancedDrawCallCount * 2 + 1] = instanceCount
232
+ renderPass.instancedDrawCallCount++
233
+ }
234
+
235
+ currentSpriteType = spriteType
236
+ instanceCount = 0
237
+ }
238
+
239
+ instanceCount++
240
+ }
241
+
242
+ if (instanceCount > 0) {
243
+ renderPass.instancedDrawCalls[renderPass.instancedDrawCallCount * 2] = currentSpriteType
244
+ renderPass.instancedDrawCalls[renderPass.instancedDrawCallCount * 2 + 1] = instanceCount
245
+ renderPass.instancedDrawCallCount++
246
+ }
247
+ }
248
+
249
+
250
+ function destroy (node) {
251
+ node.data.instancedDrawCalls = null
252
+
253
+ node.data.bindGroup = null
254
+
255
+ node.data.spriteBuffer.destroy()
256
+ node.data.spriteBuffer = null
257
+
258
+ node.data.spriteData = null
259
+ node.data.spriteIndices.clear()
260
+ node.data.spriteIndices = null
261
+ }
262
+
263
+
264
+ async function fetchJson (url) {
265
+ const raw = await fetch(url)
266
+ return raw.json()
267
+ }
268
+