@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,103 @@
|
|
|
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
|
+
@binding(4) @group(0) var emissiveTexture: texture_2d<f32>;
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
struct Fragment {
|
|
28
|
+
@builtin(position) Position : vec4<f32>,
|
|
29
|
+
@location(0) TexCoord : vec2<f32>,
|
|
30
|
+
@location(1) Tint : vec4<f32>,
|
|
31
|
+
@location(2) Opacity: f32,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// multiple render targets
|
|
35
|
+
struct GBufferOutput {
|
|
36
|
+
@location(0) color : vec4<f32>,
|
|
37
|
+
@location(1) emissive : vec4<f32>,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@vertex
|
|
42
|
+
fn vs_main (@builtin(instance_index) i_id : u32,
|
|
43
|
+
@location(0) vertexPosition: vec3<f32>,
|
|
44
|
+
@location(1) vertexTexCoord: vec2<f32>) -> Fragment {
|
|
45
|
+
|
|
46
|
+
var output : Fragment;
|
|
47
|
+
|
|
48
|
+
var sx: f32 = sprites.models[i_id].scale.x;
|
|
49
|
+
var sy: f32 = sprites.models[i_id].scale.y;
|
|
50
|
+
var sz: f32 = 1.0;
|
|
51
|
+
|
|
52
|
+
var rot: f32 = sprites.models[i_id].rotation;
|
|
53
|
+
|
|
54
|
+
var tx: f32 = sprites.models[i_id].translate.x;
|
|
55
|
+
var ty: f32 = sprites.models[i_id].translate.y;
|
|
56
|
+
var tz: f32 = 0;
|
|
57
|
+
|
|
58
|
+
var s = sin(rot);
|
|
59
|
+
var c = cos(rot);
|
|
60
|
+
|
|
61
|
+
// https://webglfundamentals.org/webgl/lessons/webgl-2d-matrices.html
|
|
62
|
+
|
|
63
|
+
var scaleM: mat4x4<f32> = mat4x4<f32>(sx, 0.0, 0.0, 0.0,
|
|
64
|
+
0.0, sy, 0.0, 0.0,
|
|
65
|
+
0.0, 0.0, sz, 0.0,
|
|
66
|
+
0, 0, 0, 1.0);
|
|
67
|
+
|
|
68
|
+
// rotation and translation
|
|
69
|
+
var modelM: mat4x4<f32> = mat4x4<f32>(c, s, 0.0, 0.0,
|
|
70
|
+
-s, c, 0.0, 0.0,
|
|
71
|
+
0.0, 0.0, 1.0, 0.0,
|
|
72
|
+
tx, ty, tz, 1.0) * scaleM;
|
|
73
|
+
|
|
74
|
+
//output.Position = transformUBO.projection * transformUBO.view * sprites.models[i_id].modelMatrix * vec4<f32>(vertexPosition, 1.0);
|
|
75
|
+
output.Position = transformUBO.projection * transformUBO.view * modelM * vec4<f32>(vertexPosition, 1.0);
|
|
76
|
+
|
|
77
|
+
output.TexCoord = vertexTexCoord;
|
|
78
|
+
output.Tint = sprites.models[i_id].tint;
|
|
79
|
+
output.Opacity = sprites.models[i_id].opacity;
|
|
80
|
+
|
|
81
|
+
return output;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@fragment
|
|
85
|
+
fn fs_main (@location(0) TexCoord: vec2<f32>,
|
|
86
|
+
@location(1) Tint: vec4<f32>,
|
|
87
|
+
@location(2) Opacity: f32) -> GBufferOutput {
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
var output : GBufferOutput;
|
|
91
|
+
|
|
92
|
+
var outColor: vec4<f32> = textureSample(myTexture, mySampler, TexCoord);
|
|
93
|
+
output.color = vec4<f32>(outColor.rgb * (1.0 - Tint.a) + (Tint.rgb * Tint.a), outColor.a * Opacity);
|
|
94
|
+
|
|
95
|
+
let emissive = textureSample(emissiveTexture, mySampler, TexCoord);
|
|
96
|
+
|
|
97
|
+
// the alpha channel in the emissive texture is used for emission strength
|
|
98
|
+
output.emissive = vec4(emissive.rgb, 1.0) * emissive.a;
|
|
99
|
+
|
|
100
|
+
//output.emissive = textureSample(emissiveTexture, mySampler, TexCoord) * EmissiveIntensity;
|
|
101
|
+
|
|
102
|
+
return output;
|
|
103
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import createSpriteQuads from './create-sprite-quads.js'
|
|
2
|
+
import createTextureFromBuffer from '../create-texture-from-buffer.js'
|
|
3
|
+
import createTextureFromUrl from '../create-texture-from-url.js'
|
|
4
|
+
import readSpriteSheet from './read-spritesheet.js'
|
|
5
|
+
import spriteWGSL from './sprite.wgsl'
|
|
6
|
+
import { round, mat4, vec3 } from '../deps.js'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// shared spritesheet resource, used by each sprite render node
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// temporary variables, allocated once to avoid garbage collection
|
|
13
|
+
const _tmpVec3 = vec3.create(0, 0, 0)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
type: 'cobalt:spritesheet',
|
|
18
|
+
refs: [ ],
|
|
19
|
+
|
|
20
|
+
// @params Object cobalt renderer world object
|
|
21
|
+
// @params Object options optional data passed when initing this node
|
|
22
|
+
onInit: async function (cobalt, options={}) {
|
|
23
|
+
return init(cobalt, options)
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
onRun: function (cobalt, node, webGpuCommandEncoder) {
|
|
27
|
+
// do whatever you need for this node. webgpu renderpasses, etc.
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
onDestroy: function (cobalt, node) {
|
|
31
|
+
// any cleanup for your node should go here (releasing textures, etc.)
|
|
32
|
+
destroy(node)
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
onResize: function (cobalt, node) {
|
|
36
|
+
// do whatever you need when the dimensions of the renderer change (resize textures, etc.)
|
|
37
|
+
_writeSpriteBuffer(cobalt, node)
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
onViewportPosition: function (cobalt, node) {
|
|
41
|
+
_writeSpriteBuffer(cobalt, node)
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
// configure the common settings for sprite rendering
|
|
47
|
+
async function init (cobalt, node) {
|
|
48
|
+
const { canvas, device } = cobalt
|
|
49
|
+
|
|
50
|
+
let spritesheet, colorTexture, emissiveTexture
|
|
51
|
+
|
|
52
|
+
if (canvas) {
|
|
53
|
+
// browser (canvas) path
|
|
54
|
+
spritesheet = await fetchJson(node.options.spriteSheetJsonUrl)
|
|
55
|
+
spritesheet = readSpriteSheet(spritesheet)
|
|
56
|
+
|
|
57
|
+
colorTexture = await createTextureFromUrl(cobalt, 'sprite', node.options.colorTextureUrl, 'rgba8unorm')
|
|
58
|
+
emissiveTexture = await createTextureFromUrl(cobalt, 'emissive sprite', node.options.emissiveTextureUrl, 'rgba8unorm')
|
|
59
|
+
|
|
60
|
+
// for some reason this needs to be done _after_ creating the material, or the rendering will be blurry
|
|
61
|
+
canvas.style.imageRendering = 'pixelated'
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// sdl + gpu path
|
|
65
|
+
spritesheet = readSpriteSheet(node.options.spriteSheetJson)
|
|
66
|
+
|
|
67
|
+
colorTexture = await createTextureFromBuffer(cobalt, 'sprite', node.options.colorTexture, 'rgba8unorm')
|
|
68
|
+
emissiveTexture = await createTextureFromBuffer(cobalt, 'emissive sprite', node.options.emissiveTexture, 'rgba8unorm')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const quads = createSpriteQuads(device, spritesheet)
|
|
72
|
+
|
|
73
|
+
const uniformBuffer = device.createBuffer({
|
|
74
|
+
size: 64 * 2, // 4x4 matrix with 4 bytes per float32, times 2 matrices (view, projection)
|
|
75
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const bindGroupLayout = device.createBindGroupLayout({
|
|
79
|
+
entries: [
|
|
80
|
+
{
|
|
81
|
+
binding: 0,
|
|
82
|
+
visibility: GPUShaderStage.VERTEX,
|
|
83
|
+
buffer: { }
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
binding: 1,
|
|
87
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
88
|
+
texture: { }
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
binding: 2,
|
|
92
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
93
|
+
sampler: { }
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
binding: 3,
|
|
97
|
+
visibility: GPUShaderStage.VERTEX,
|
|
98
|
+
buffer: {
|
|
99
|
+
type: 'read-only-storage'
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
binding: 4,
|
|
104
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
105
|
+
texture: { }
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
111
|
+
bindGroupLayouts: [ bindGroupLayout ]
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const pipeline = device.createRenderPipeline({
|
|
115
|
+
label: 'sprite',
|
|
116
|
+
vertex: {
|
|
117
|
+
module: device.createShaderModule({
|
|
118
|
+
code: spriteWGSL
|
|
119
|
+
}),
|
|
120
|
+
entryPoint: 'vs_main',
|
|
121
|
+
buffers: [ quads.bufferLayout ]
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
fragment: {
|
|
125
|
+
module: device.createShaderModule({
|
|
126
|
+
code: spriteWGSL
|
|
127
|
+
}),
|
|
128
|
+
entryPoint: 'fs_main',
|
|
129
|
+
targets: [
|
|
130
|
+
// color
|
|
131
|
+
{
|
|
132
|
+
format: 'rgba16float',
|
|
133
|
+
blend: {
|
|
134
|
+
color: {
|
|
135
|
+
srcFactor: 'src-alpha',
|
|
136
|
+
dstFactor: 'one-minus-src-alpha',
|
|
137
|
+
},
|
|
138
|
+
alpha: {
|
|
139
|
+
srcFactor: 'zero',
|
|
140
|
+
dstFactor: 'one'
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// emissive
|
|
146
|
+
{
|
|
147
|
+
format: 'rgba16float',
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
primitive: {
|
|
153
|
+
topology: 'triangle-list'
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
layout: pipelineLayout
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
pipeline,
|
|
161
|
+
uniformBuffer, // perspective and view matrices for the camera
|
|
162
|
+
quads,
|
|
163
|
+
colorTexture,
|
|
164
|
+
emissiveTexture,
|
|
165
|
+
bindGroupLayout,
|
|
166
|
+
spritesheet,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
function destroy (node) {
|
|
172
|
+
node.data.quads.buffer.destroy()
|
|
173
|
+
node.data.colorTexture.buffer.destroy()
|
|
174
|
+
node.data.uniformBuffer.destroy()
|
|
175
|
+
node.data.emissiveTexture.texture.destroy()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async function fetchJson (url) {
|
|
180
|
+
const raw = await fetch(url)
|
|
181
|
+
return raw.json()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
function _writeSpriteBuffer (cobalt, node) {
|
|
186
|
+
|
|
187
|
+
const { device, viewport } = cobalt
|
|
188
|
+
|
|
189
|
+
const GAME_WIDTH = viewport.width / viewport.zoom
|
|
190
|
+
const GAME_HEIGHT = viewport.height / viewport.zoom
|
|
191
|
+
|
|
192
|
+
// left right bottom top near far
|
|
193
|
+
const projection = mat4.ortho(0, GAME_WIDTH, GAME_HEIGHT, 0, -10.0, 10.0)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
// TODO: if this doesn't introduce jitter into the crossroads render, remove this disabled code entirely.
|
|
197
|
+
//
|
|
198
|
+
// I'm disabling the rounding because I think it fails in cases where units are not expressed in pixels
|
|
199
|
+
// e.g., most physics engines operate on meters, not pixels, so we don't want to round to the nearest integer as that
|
|
200
|
+
// probably isn't high enough resolution. That would mean the camera could be snapped by up to 0.5 meters
|
|
201
|
+
// in that case. I think the better solution for expressing camera position in pixels is to round before calling
|
|
202
|
+
// cobalt.setViewportPosition(...)
|
|
203
|
+
//
|
|
204
|
+
// set 3d camera position
|
|
205
|
+
//vec3.set(-round(viewport.position[0]), -round(viewport.position[1]), 0, _tmpVec3)
|
|
206
|
+
vec3.set(-viewport.position[0], -viewport.position[1], 0, _tmpVec3)
|
|
207
|
+
const view = mat4.translation(_tmpVec3)
|
|
208
|
+
|
|
209
|
+
device.queue.writeBuffer(node.data.uniformBuffer, 0, view.buffer)
|
|
210
|
+
device.queue.writeBuffer(node.data.uniformBuffer, 64, projection.buffer)
|
|
211
|
+
}
|
|
212
|
+
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import createTextureFromUrl from '../create-texture-from-url.js'
|
|
2
|
+
import tileWGSL from './tile.wgsl'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const _buf = new Float32Array(8) //(136) // tile instance data stored in a UBO
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// shared tile atlas resource, used by each tile render node
|
|
9
|
+
export default {
|
|
10
|
+
type: 'cobalt:tileAtlas',
|
|
11
|
+
refs: [ ],
|
|
12
|
+
|
|
13
|
+
// @params Object cobalt renderer world object
|
|
14
|
+
// @params Object options optional data passed when initing this node
|
|
15
|
+
onInit: async function (cobalt, options={}) {
|
|
16
|
+
return init(cobalt, options)
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
onRun: function (cobalt, node, webGpuCommandEncoder) {
|
|
20
|
+
// do whatever you need for this node. webgpu renderpasses, etc.
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
onDestroy: function (cobalt, node) {
|
|
24
|
+
// any cleanup for your node should go here (releasing textures, etc.)
|
|
25
|
+
destroy(data)
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
onResize: function (cobalt, node) {
|
|
29
|
+
// do whatever you need when the dimensions of the renderer change (resize textures, etc.)
|
|
30
|
+
_writeTileBuffer(cobalt, node)
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
onViewportPosition: function (cobalt, node) {
|
|
34
|
+
_writeTileBuffer(cobalt, node)
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async function init (cobalt, nodeData) {
|
|
40
|
+
const { device } = cobalt
|
|
41
|
+
|
|
42
|
+
const atlasMaterial = await createTextureFromUrl(cobalt, 'tile atlas', nodeData.options.textureUrl)
|
|
43
|
+
|
|
44
|
+
const uniformBuffer = device.createBuffer({
|
|
45
|
+
size: 32, //32 + (16 * 32), // in bytes. 32 for common data + (32 max tile layers * 16 bytes per tile layer)
|
|
46
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const atlasBindGroupLayout = device.createBindGroupLayout({
|
|
50
|
+
entries: [
|
|
51
|
+
{
|
|
52
|
+
binding: 0,
|
|
53
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
54
|
+
buffer: { }
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
binding: 1,
|
|
58
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
59
|
+
texture: { }
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
binding: 2,
|
|
63
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
64
|
+
sampler: { }
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const atlasBindGroup = device.createBindGroup({
|
|
70
|
+
layout: atlasBindGroupLayout,
|
|
71
|
+
entries: [
|
|
72
|
+
{
|
|
73
|
+
binding: 0,
|
|
74
|
+
resource: {
|
|
75
|
+
buffer: uniformBuffer
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
binding: 1,
|
|
80
|
+
resource: atlasMaterial.view
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
binding: 2,
|
|
84
|
+
resource: atlasMaterial.sampler
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const tileBindGroupLayout = device.createBindGroupLayout({
|
|
90
|
+
entries: [
|
|
91
|
+
{
|
|
92
|
+
binding: 0,
|
|
93
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
94
|
+
buffer: { }
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
binding: 1,
|
|
98
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
99
|
+
texture: { }
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
binding: 2,
|
|
103
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
104
|
+
sampler: { }
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
110
|
+
bindGroupLayouts: [ tileBindGroupLayout, atlasBindGroupLayout ]
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const pipeline = device.createRenderPipeline({
|
|
114
|
+
label: 'tile',
|
|
115
|
+
vertex: {
|
|
116
|
+
module: device.createShaderModule({
|
|
117
|
+
code: tileWGSL
|
|
118
|
+
}),
|
|
119
|
+
entryPoint: 'vs_main',
|
|
120
|
+
buffers: [ ]
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
fragment: {
|
|
124
|
+
module: device.createShaderModule({
|
|
125
|
+
code: tileWGSL
|
|
126
|
+
}),
|
|
127
|
+
entryPoint: 'fs_main',
|
|
128
|
+
targets: [
|
|
129
|
+
{
|
|
130
|
+
format: 'rgba16float',
|
|
131
|
+
blend: {
|
|
132
|
+
color: {
|
|
133
|
+
srcFactor: 'src-alpha',
|
|
134
|
+
dstFactor: 'one-minus-src-alpha',
|
|
135
|
+
},
|
|
136
|
+
alpha: {
|
|
137
|
+
srcFactor: 'zero',
|
|
138
|
+
dstFactor: 'one'
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
primitive: {
|
|
146
|
+
topology: 'triangle-list'
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
layout: pipelineLayout
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
pipeline,
|
|
154
|
+
uniformBuffer,
|
|
155
|
+
atlasBindGroup, // tile atlas texture, transform UBO
|
|
156
|
+
atlasMaterial,
|
|
157
|
+
|
|
158
|
+
tileBindGroupLayout,
|
|
159
|
+
|
|
160
|
+
tileSize: nodeData.options.tileSize,
|
|
161
|
+
tileScale: nodeData.options.tileScale,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
function destroy (data) {
|
|
167
|
+
data.atlasMaterial.texture.destroy()
|
|
168
|
+
data.atlasMaterial.texture = undefined
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
function _writeTileBuffer (c, nodeData) {
|
|
173
|
+
// c.viewport.position is the top left visible corner of the level
|
|
174
|
+
_buf[0] = c.viewport.position[0]
|
|
175
|
+
_buf[1] = c.viewport.position[1]
|
|
176
|
+
|
|
177
|
+
const tile = nodeData.data
|
|
178
|
+
const { tileScale, tileSize } = tile
|
|
179
|
+
|
|
180
|
+
const GAME_WIDTH = c.viewport.width / c.viewport.zoom
|
|
181
|
+
const GAME_HEIGHT = c.viewport.height / c.viewport.zoom
|
|
182
|
+
|
|
183
|
+
_buf[2] = GAME_WIDTH / tileScale // viewportSize[0]
|
|
184
|
+
_buf[3] = GAME_HEIGHT / tileScale // viewportSize[1]
|
|
185
|
+
|
|
186
|
+
_buf[4] = 1 / tile.atlasMaterial.texture.width // inverseAtlasTextureSize[0]
|
|
187
|
+
_buf[5] = 1 / tile.atlasMaterial.texture.height // inverseAtlasTextureSize[1]
|
|
188
|
+
|
|
189
|
+
_buf[6] = tileSize
|
|
190
|
+
_buf[7] = 1.0 / tileSize // inverseTileSize
|
|
191
|
+
|
|
192
|
+
c.device.queue.writeBuffer(tile.uniformBuffer, 0, _buf, 0, 8)
|
|
193
|
+
}
|
package/src/tile/tile.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import createTextureFromUrl from '../create-texture-from-url.js'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Tile layers are totally static, and there are usually many of them in a grid, in several layers.
|
|
6
|
+
|
|
7
|
+
These use a `TileRenderPass` data structure which provides 100% GPU hardware based tile rendering, making them _almost_ free CPU-wise.
|
|
8
|
+
|
|
9
|
+
Internally, `TileRenderPass` objects store 1 or more layers, which hold a reference to a sprite texture, and a layer texture.
|
|
10
|
+
When a tile layer is drawn, it loads the 2 textures into the gpu.
|
|
11
|
+
One of these textures is a lookup table, where each pixel corresponds to a type of sprite.
|
|
12
|
+
Because this processing can happen completely in the fragment shader, there's no need to do expensive loops over slow arrays in js land, which is the typical approach for current state-of-the-art web renderers.
|
|
13
|
+
|
|
14
|
+
Inspired by/ported from https://blog.tojicode.com/2012/07/sprite-tile-maps-on-gpu.html
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
type: 'cobalt:tile',
|
|
20
|
+
refs: [
|
|
21
|
+
{ name: 'tileAtlas', type: 'textureView', format: 'rgba8unorm', access: 'write' },
|
|
22
|
+
],
|
|
23
|
+
|
|
24
|
+
// @params Object cobalt renderer world object
|
|
25
|
+
// @params Object options optional data passed when initing this node
|
|
26
|
+
onInit: async function (cobalt, options={}) {
|
|
27
|
+
return init(cobalt, options)
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
onRun: function (cobalt, node, webGpuCommandEncoder) {
|
|
31
|
+
// do whatever you need for this node. webgpu renderpasses, etc.
|
|
32
|
+
draw(cobalt, node, webGpuCommandEncoder)
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
onDestroy: function (cobalt, node) {
|
|
36
|
+
// any cleanup for your node should go here (releasing textures, etc.)
|
|
37
|
+
destroy(node)
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
onResize: function (cobalt, node) {
|
|
41
|
+
// do whatever you need when the dimensions of the renderer change (resize textures, etc.)
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
onViewportPosition: function (cobalt, node) {
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// optional
|
|
48
|
+
customFunctions: {
|
|
49
|
+
setTexture: async function (cobalt, node, textureUrl) {
|
|
50
|
+
const { device } = cobalt
|
|
51
|
+
|
|
52
|
+
destroy(node)
|
|
53
|
+
node.options.textureUrl = textureUrl
|
|
54
|
+
const material = await createTextureFromUrl(cobalt, 'tile map', node.options.textureUrl)
|
|
55
|
+
|
|
56
|
+
const bindGroup = device.createBindGroup({
|
|
57
|
+
layout: node.refs.tileAtlas.data.tileBindGroupLayout,
|
|
58
|
+
entries: [
|
|
59
|
+
{
|
|
60
|
+
binding: 0,
|
|
61
|
+
resource: {
|
|
62
|
+
buffer: node.data.uniformBuffer
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
binding: 1,
|
|
67
|
+
resource: material.view
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
binding: 2,
|
|
71
|
+
resource: material.sampler
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
node.data.bindGroup = bindGroup
|
|
77
|
+
node.data.material = material
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async function init (cobalt, nodeData) {
|
|
84
|
+
const { device } = cobalt
|
|
85
|
+
|
|
86
|
+
// build the tile layer and add it to the cobalt data structure
|
|
87
|
+
const material = await createTextureFromUrl(cobalt, 'tile map', nodeData.options.textureUrl)
|
|
88
|
+
|
|
89
|
+
const dat = new Float32Array([ nodeData.options.scrollScale, nodeData.options.scrollScale ])
|
|
90
|
+
|
|
91
|
+
const usage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
92
|
+
|
|
93
|
+
const descriptor = {
|
|
94
|
+
size: dat.byteLength,
|
|
95
|
+
usage,
|
|
96
|
+
// make this memory space accessible from the CPU (host visible)
|
|
97
|
+
mappedAtCreation: true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const uniformBuffer = device.createBuffer(descriptor)
|
|
101
|
+
new Float32Array(uniformBuffer.getMappedRange()).set(dat)
|
|
102
|
+
uniformBuffer.unmap()
|
|
103
|
+
|
|
104
|
+
const bindGroup = device.createBindGroup({
|
|
105
|
+
layout: nodeData.refs.tileAtlas.data.tileBindGroupLayout,
|
|
106
|
+
entries: [
|
|
107
|
+
{
|
|
108
|
+
binding: 0,
|
|
109
|
+
resource: {
|
|
110
|
+
buffer: uniformBuffer
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
binding: 1,
|
|
115
|
+
resource: material.view
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
binding: 2,
|
|
119
|
+
resource: material.sampler
|
|
120
|
+
},
|
|
121
|
+
]
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
bindGroup,
|
|
126
|
+
material,
|
|
127
|
+
uniformBuffer,
|
|
128
|
+
scrollScale: nodeData.options.scrollScale,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
function draw (cobalt, nodeData, commandEncoder) {
|
|
134
|
+
|
|
135
|
+
const { device } = cobalt
|
|
136
|
+
|
|
137
|
+
// on the first render, we should clear the color attachment.
|
|
138
|
+
// otherwise load it, so multiple sprite passes can build up data in the color and emissive textures
|
|
139
|
+
const loadOp = nodeData.options.loadOp || 'load'
|
|
140
|
+
|
|
141
|
+
const renderpass = commandEncoder.beginRenderPass({
|
|
142
|
+
colorAttachments: [
|
|
143
|
+
{
|
|
144
|
+
view: nodeData.refs.hdr.data.view,
|
|
145
|
+
clearValue: cobalt.clearValue,
|
|
146
|
+
loadOp,
|
|
147
|
+
storeOp: 'store'
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const tileAtlas = nodeData.refs.tileAtlas.data
|
|
153
|
+
|
|
154
|
+
renderpass.setPipeline(tileAtlas.pipeline)
|
|
155
|
+
|
|
156
|
+
renderpass.setBindGroup(0, nodeData.data.bindGroup)
|
|
157
|
+
|
|
158
|
+
// common stuff; the transform data and the tile atlas texture
|
|
159
|
+
renderpass.setBindGroup(1, tileAtlas.atlasBindGroup)
|
|
160
|
+
|
|
161
|
+
renderpass.draw(3) // fullscreen triangle
|
|
162
|
+
|
|
163
|
+
renderpass.end()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
function destroy (nodeData) {
|
|
168
|
+
nodeData.data.material.texture.destroy()
|
|
169
|
+
nodeData.data.material.texture = undefined
|
|
170
|
+
}
|
|
171
|
+
|