@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,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
|
+
|