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