@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,191 @@
1
+ /// <reference types="@webgpu/types"/>
2
+
3
+ import { LightsBuffer } from "../lights-buffer";
4
+ import { type ILightsTexture } from "./lights-texture";
5
+
6
+ class LightsTextureInitializer {
7
+ private readonly lightsBuffer: LightsBuffer;
8
+
9
+ private readonly renderPipeline: GPURenderPipeline;
10
+ private readonly bindgroup: GPUBindGroup;
11
+
12
+ private readonly renderBundle: GPURenderBundle;
13
+
14
+ public constructor(device: GPUDevice, lightsBuffer: LightsBuffer, lightsTexture: ILightsTexture, maxLightSize: number) {
15
+ this.lightsBuffer = lightsBuffer;
16
+
17
+ const shaderModule = device.createShaderModule({
18
+ label: "LightsTextureInitializer shader module",
19
+ code: `
20
+ ${LightsBuffer.structs.definition}
21
+
22
+ @group(0) @binding(0) var<storage,read> lightsBuffer: LightsBuffer;
23
+
24
+ struct VertexIn {
25
+ @builtin(vertex_index) vertexIndex: u32,
26
+ };
27
+
28
+ struct VertexOut {
29
+ @builtin(position) position: vec4<f32>,
30
+ @location(0) uv: vec2<f32>,
31
+ };
32
+
33
+ const cellsGridSizeU = vec2<u32>(${lightsTexture.gridSize.x}, ${lightsTexture.gridSize.y});
34
+ const cellsGridSizeF = vec2<f32>(${lightsTexture.gridSize.x}, ${lightsTexture.gridSize.y});
35
+
36
+ @vertex
37
+ fn main_vertex(in: VertexIn) -> VertexOut {
38
+ const corners = array<vec2<f32>, 4>(
39
+ vec2<f32>(-1, -1),
40
+ vec2<f32>(1, -1),
41
+ vec2<f32>(-1, 1),
42
+ vec2<f32>(1, 1),
43
+ );
44
+ let screenPosition = corners[in.vertexIndex];
45
+
46
+ var out: VertexOut;
47
+ out.position = vec4<f32>(screenPosition, 0.0, 1.0);
48
+ out.uv = (0.5 + 0.5 * screenPosition) * cellsGridSizeF;
49
+ return out;
50
+ }
51
+
52
+ struct FragmentOut {
53
+ @location(0) color: vec4<f32>,
54
+ };
55
+
56
+ struct LightProperties {
57
+ radius: f32,
58
+ intensity: f32,
59
+ attenuationLinear: f32,
60
+ attenuationExp: f32,
61
+ };
62
+
63
+ fn get_light_properties(lightId: u32) -> LightProperties {
64
+ var lightProperties: LightProperties;
65
+ if (lightId < lightsBuffer.count) {
66
+ let light = lightsBuffer.lights[lightId];
67
+ lightProperties.radius = light.radius;
68
+ lightProperties.intensity = 1.0;
69
+ lightProperties.attenuationLinear = light.attenuationLinear;
70
+ lightProperties.attenuationExp = light.attenuationExp;
71
+ } else {
72
+ lightProperties.radius = 0.0;
73
+ lightProperties.intensity = 0.0;
74
+ lightProperties.attenuationLinear = 0.0;
75
+ lightProperties.attenuationExp = 0.0;
76
+ }
77
+ return lightProperties;
78
+ }
79
+
80
+ @fragment
81
+ fn main_fragment(in: VertexOut) -> FragmentOut {
82
+ let cellId = vec2<u32>(in.uv);
83
+
84
+ let lightIdFrom = 4u * (cellId.x + cellId.y * cellsGridSizeU.x);
85
+ let lightProperties = array<LightProperties, 4>(
86
+ get_light_properties(lightIdFrom + 0u),
87
+ get_light_properties(lightIdFrom + 1u),
88
+ get_light_properties(lightIdFrom + 2u),
89
+ get_light_properties(lightIdFrom + 3u),
90
+ );
91
+
92
+ let sizes = vec4<f32>(
93
+ lightProperties[0].radius,
94
+ lightProperties[1].radius,
95
+ lightProperties[2].radius,
96
+ lightProperties[3].radius,
97
+ );
98
+
99
+ let localUv = fract(in.uv);
100
+ let fromCenter = 2.0 * localUv - 1.0;
101
+ let uvDistanceFromCenter = distance(vec2<f32>(0,0), fromCenter);
102
+ let distancesFromCenter = vec4<f32>(uvDistanceFromCenter / sizes * f32(${maxLightSize}));
103
+
104
+ let intensities = vec4<f32>(
105
+ lightProperties[0].intensity * (1.0 + 1.0 * step(uvDistanceFromCenter, 0.01)),
106
+ lightProperties[1].intensity * (1.0 + 1.0 * step(uvDistanceFromCenter, 0.01)),
107
+ lightProperties[2].intensity * (1.0 + 1.0 * step(uvDistanceFromCenter, 0.01)),
108
+ lightProperties[3].intensity * (1.0 + 1.0 * step(uvDistanceFromCenter, 0.01)),
109
+ );
110
+ let attenuationsLinear = vec4<f32>(
111
+ lightProperties[0].attenuationLinear,
112
+ lightProperties[1].attenuationLinear,
113
+ lightProperties[2].attenuationLinear,
114
+ lightProperties[3].attenuationLinear,
115
+ );
116
+ let attenuationsExp = vec4<f32>(
117
+ lightProperties[0].attenuationExp,
118
+ lightProperties[1].attenuationExp,
119
+ lightProperties[2].attenuationExp,
120
+ lightProperties[3].attenuationExp,
121
+ );
122
+
123
+ var lightIntensities = intensities / (1.0 + distancesFromCenter * (attenuationsLinear + distancesFromCenter * attenuationsExp)); // base intensity equation
124
+ lightIntensities *= cos(distancesFromCenter * ${Math.PI / 2}); // soft limit;
125
+ lightIntensities *= step(distancesFromCenter, vec4<f32>(1.0)); // hard limit
126
+
127
+ var out: FragmentOut;
128
+ out.color = lightIntensities;
129
+ return out;
130
+ }
131
+ `,
132
+ });
133
+
134
+ this.renderPipeline = device.createRenderPipeline({
135
+ label: "LightsTextureInitializer renderpipeline",
136
+ layout: "auto",
137
+ vertex: {
138
+ module: shaderModule,
139
+ entryPoint: "main_vertex",
140
+ },
141
+ fragment: {
142
+ module: shaderModule,
143
+ entryPoint: "main_fragment",
144
+ targets: [{
145
+ format: lightsTexture.format,
146
+ }],
147
+ },
148
+ primitive: {
149
+ cullMode: "none",
150
+ topology: "triangle-strip",
151
+ },
152
+ multisample: {
153
+ count: lightsTexture.sampleCount,
154
+ },
155
+ });
156
+
157
+ this.bindgroup = device.createBindGroup({
158
+ label: "LightsTextureInitializer bindgroup 0",
159
+ layout: this.renderPipeline.getBindGroupLayout(0),
160
+ entries: [
161
+ {
162
+ binding: 0,
163
+ resource: { buffer: this.lightsBuffer.gpuBuffer },
164
+ },
165
+ ]
166
+ });
167
+
168
+ const renderBundleEncoder = device.createRenderBundleEncoder({
169
+ label: "LightsTextureInitializer renderbundle encoder",
170
+ colorFormats: [lightsTexture.format],
171
+ sampleCount: lightsTexture.sampleCount,
172
+ });
173
+ renderBundleEncoder.setPipeline(this.renderPipeline);
174
+ renderBundleEncoder.setBindGroup(0, this.bindgroup);
175
+ renderBundleEncoder.draw(4);
176
+ this.renderBundle = renderBundleEncoder.finish({ label: "LightsTextureInitializer renderbundle" });
177
+ }
178
+
179
+ public getRenderBundle(): GPURenderBundle {
180
+ return this.renderBundle;
181
+ }
182
+
183
+ public destroy(): void {
184
+ // nothing to do
185
+ }
186
+ }
187
+
188
+ export {
189
+ LightsTextureInitializer
190
+ };
191
+
@@ -0,0 +1,286 @@
1
+ /// <reference types="@webgpu/types"/>
2
+
3
+ import { LightsBuffer } from "../lights-buffer";
4
+ import { type Point } from "../types";
5
+ import { type ILightsTexture } from "./lights-texture";
6
+
7
+ type LightObstacleSegment = [Point, Point];
8
+
9
+ class LightsTextureMask {
10
+ private readonly device: GPUDevice;
11
+
12
+ private readonly renderPipeline: GPURenderPipeline;
13
+
14
+ private readonly renderBundleEncoderDescriptor: GPURenderBundleEncoderDescriptor;
15
+ private renderBundle: GPURenderBundle;
16
+
17
+ private readonly lightsBuffer: LightsBuffer;
18
+
19
+ private readonly indirectDrawing: {
20
+ readonly bufferCpu: ArrayBuffer;
21
+ readonly bufferGpu: GPUBuffer;
22
+ };
23
+
24
+ private obstacles: {
25
+ readonly positionsBufferGpu: GPUBuffer;
26
+ readonly indexBufferGpu: GPUBuffer;
27
+ } | null = null;
28
+
29
+ public constructor(device: GPUDevice, lightsBuffer: LightsBuffer, lightsTexture: ILightsTexture, uniformLightSize: number) {
30
+ this.device = device;
31
+ this.lightsBuffer = lightsBuffer;
32
+
33
+ const obstaclesAreTwoWay = true as boolean;
34
+
35
+ const shaderModule = device.createShaderModule({
36
+ label: "LightsTextureMask shader module",
37
+ code: `
38
+ struct VertexIn {
39
+ @builtin(instance_index) lightIndex: u32,
40
+ @location(0) position: vec3<f32>,
41
+ @location(1) lightSize: f32,
42
+ @location(2) lightPosition: vec2<f32>,
43
+ };
44
+
45
+ struct VertexOut {
46
+ @builtin(position) position: vec4<f32>,
47
+ @location(0) color: vec4<f32>,
48
+ @location(1) localPosition: vec2<f32>,
49
+ };
50
+
51
+ const cellsGridSizeU = vec2<u32>(${lightsTexture.gridSize.x}, ${lightsTexture.gridSize.y});
52
+ const cellsGridSizeF = vec2<f32>(${lightsTexture.gridSize.x}, ${lightsTexture.gridSize.y});
53
+
54
+ @vertex
55
+ fn main_vertex(in: VertexIn) -> VertexOut {
56
+ let worldPosition = in.lightPosition + (in.position.xy - in.lightPosition) * (1.0 + 10000.0 * in.position.z);
57
+
58
+ let scaling = f32(${uniformLightSize});
59
+
60
+ let cellIndex = in.lightIndex / 4u;
61
+ let indexInCell = in.lightIndex % 4u;
62
+
63
+ let cellIdU = vec2<u32>(
64
+ cellIndex % cellsGridSizeU.x,
65
+ cellIndex / cellsGridSizeU.x,
66
+ );
67
+ let cellIdF = vec2<f32>(cellIdU);
68
+
69
+ var out: VertexOut;
70
+ out.localPosition = (worldPosition - in.lightPosition) / scaling;
71
+ out.position = vec4<f32>(
72
+ (out.localPosition - (cellsGridSizeF - 1.0) + 2.0 * cellIdF) / cellsGridSizeF,
73
+ 0.0,
74
+ 1.0,
75
+ );
76
+ out.color = vec4<f32>(
77
+ vec4<u32>(indexInCell) != vec4<u32>(0u, 1u, 2u, 3u),
78
+ );
79
+ return out;
80
+ }
81
+
82
+ struct FragmentOut {
83
+ @location(0) color: vec4<f32>,
84
+ };
85
+
86
+ @fragment
87
+ fn main_fragment(in: VertexOut) -> FragmentOut {
88
+ if (in.localPosition.x < -1.0 || in.localPosition.x > 1.0 || in.localPosition.y <= -1.0 || in.localPosition.y > 1.0) {
89
+ discard;
90
+ }
91
+ var out: FragmentOut;
92
+ out.color = in.color;
93
+ return out;
94
+ }
95
+ `,
96
+ });
97
+
98
+ this.renderPipeline = device.createRenderPipeline({
99
+ label: "LightsTextureMask renderpipeline",
100
+ layout: "auto",
101
+ vertex: {
102
+ module: shaderModule,
103
+ entryPoint: "main_vertex",
104
+ buffers: [
105
+ {
106
+ attributes: [
107
+ {
108
+ shaderLocation: 0,
109
+ offset: 0,
110
+ format: "float32x3",
111
+ },
112
+ ],
113
+ arrayStride: 3 * Float32Array.BYTES_PER_ELEMENT,
114
+ stepMode: "vertex",
115
+ },
116
+ {
117
+ attributes: [
118
+ {
119
+ shaderLocation: 1,
120
+ offset: LightsBuffer.structs.light.radius.offset,
121
+ format: "float32",
122
+ },
123
+ {
124
+ shaderLocation: 2,
125
+ offset: LightsBuffer.structs.light.position.offset,
126
+ format: "float32x2",
127
+ },
128
+ ],
129
+ arrayStride: LightsBuffer.structs.lightsBuffer.lights.stride,
130
+ stepMode: "instance",
131
+ },
132
+ ],
133
+ },
134
+ fragment: {
135
+ module: shaderModule,
136
+ entryPoint: "main_fragment",
137
+ targets: [{
138
+ format: lightsTexture.format,
139
+ blend: {
140
+ color: {
141
+ operation: "min",
142
+ srcFactor: "one",
143
+ dstFactor: "one",
144
+ },
145
+ alpha: {
146
+ operation: "min",
147
+ srcFactor: "one",
148
+ dstFactor: "one",
149
+ },
150
+ },
151
+ }],
152
+ },
153
+ primitive: {
154
+ cullMode: obstaclesAreTwoWay ? "none" : "back",
155
+ topology: "triangle-list",
156
+ },
157
+ multisample: {
158
+ count: lightsTexture.sampleCount,
159
+ },
160
+ });
161
+
162
+ this.indirectDrawing = {
163
+ bufferCpu: new ArrayBuffer(20),
164
+ bufferGpu: device.createBuffer({
165
+ label: "LightsTextureMask indirect buffer",
166
+ size: 20,
167
+ usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST,
168
+ }),
169
+ };
170
+ this.uploadIndirectDrawingBuffer();
171
+
172
+ this.renderBundleEncoderDescriptor = {
173
+ label: "LightsTextureMask renderbundle encoder",
174
+ colorFormats: [lightsTexture.format],
175
+ sampleCount: lightsTexture.sampleCount,
176
+ };
177
+ this.renderBundle = this.buildRenderBundle();
178
+ }
179
+
180
+ public getRenderBundle(): GPURenderBundle {
181
+ return this.renderBundle;
182
+ }
183
+
184
+ public setObstacles(segments: ReadonlyArray<LightObstacleSegment>): void {
185
+ const positions: number[] = [];
186
+ const indices: number[] = [];
187
+ for (const segment of segments) {
188
+ const firstQuadIndex = positions.length / 3;
189
+
190
+ positions.push(
191
+ ...segment[0], 0,
192
+ ...segment[1], 0,
193
+ ...segment[0], 1,
194
+ ...segment[1], 1,
195
+ );
196
+
197
+ indices.push(
198
+ firstQuadIndex + 0, firstQuadIndex + 1, firstQuadIndex + 3,
199
+ firstQuadIndex + 0, firstQuadIndex + 3, firstQuadIndex + 2,
200
+ );
201
+ }
202
+
203
+ let gpuBuffersChanged = false;
204
+
205
+ const positionsArray = new Float32Array(positions);
206
+ let positionsBufferGpu = this.obstacles?.positionsBufferGpu;
207
+ if (!positionsBufferGpu || positionsBufferGpu.size < positionsArray.byteLength) {
208
+ positionsBufferGpu?.destroy();
209
+ positionsBufferGpu = this.device.createBuffer({
210
+ label: "LightsTextureMask positions buffer",
211
+ size: positionsArray.byteLength,
212
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
213
+ });
214
+ gpuBuffersChanged = true;
215
+ }
216
+ this.device.queue.writeBuffer(positionsBufferGpu, 0, positionsArray);
217
+
218
+ const indicesArray = new Uint16Array(indices);
219
+ let indexBufferGpu = this.obstacles?.indexBufferGpu;
220
+ if (!indexBufferGpu || indexBufferGpu.size < indicesArray.byteLength) {
221
+ indexBufferGpu?.destroy();
222
+ indexBufferGpu = this.device.createBuffer({
223
+ label: "LightsTextureMask index buffer",
224
+ size: indicesArray.byteLength,
225
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
226
+ });
227
+ gpuBuffersChanged = true;
228
+ }
229
+ this.device.queue.writeBuffer(indexBufferGpu, 0, indicesArray);
230
+
231
+ this.obstacles = { positionsBufferGpu, indexBufferGpu };
232
+
233
+ this.setIndirectIndexCount(indices.length);
234
+
235
+ if (gpuBuffersChanged) {
236
+ this.renderBundle = this.buildRenderBundle();
237
+ }
238
+ }
239
+
240
+ public setLightsCount(count: number): void {
241
+ this.setIndirectInstanceCount(count);
242
+ }
243
+
244
+ public destroy(): void {
245
+ this.indirectDrawing.bufferGpu.destroy();
246
+ this.obstacles?.positionsBufferGpu.destroy();
247
+ this.obstacles?.indexBufferGpu.destroy();
248
+ }
249
+
250
+ private setIndirectIndexCount(indexCount: number): void {
251
+ const drawIndexedIndirectParameters = new Uint32Array(this.indirectDrawing.bufferCpu);
252
+ if (drawIndexedIndirectParameters[0] !== indexCount) {
253
+ drawIndexedIndirectParameters[0] = indexCount;
254
+ this.uploadIndirectDrawingBuffer();
255
+ }
256
+ }
257
+
258
+ private setIndirectInstanceCount(instanceCount: number): void {
259
+ const drawIndexedIndirectParameters = new Uint32Array(this.indirectDrawing.bufferCpu);
260
+ if (drawIndexedIndirectParameters[1] !== instanceCount) {
261
+ drawIndexedIndirectParameters[1] = instanceCount;
262
+ this.uploadIndirectDrawingBuffer();
263
+ }
264
+ }
265
+
266
+ private buildRenderBundle(): GPURenderBundle {
267
+ const renderBundleEncoder = this.device.createRenderBundleEncoder(this.renderBundleEncoderDescriptor);
268
+ if (this.obstacles) {
269
+ renderBundleEncoder.setPipeline(this.renderPipeline);
270
+ renderBundleEncoder.setVertexBuffer(0, this.obstacles.positionsBufferGpu);
271
+ renderBundleEncoder.setVertexBuffer(1, this.lightsBuffer.gpuBuffer, LightsBuffer.structs.lightsBuffer.lights.offset);
272
+ renderBundleEncoder.setIndexBuffer(this.obstacles.indexBufferGpu, "uint16");
273
+ renderBundleEncoder.drawIndexedIndirect(this.indirectDrawing.bufferGpu, 0);
274
+ }
275
+ return renderBundleEncoder.finish({ label: "LightsTextureMask renderbundle" });
276
+ }
277
+
278
+ private uploadIndirectDrawingBuffer(): void {
279
+ this.device.queue.writeBuffer(this.indirectDrawing.bufferGpu, 0, this.indirectDrawing.bufferCpu);
280
+ }
281
+ }
282
+
283
+ export {
284
+ LightsTextureMask, type LightObstacleSegment
285
+ };
286
+
@@ -0,0 +1,121 @@
1
+ /// <reference types="@webgpu/types"/>
2
+
3
+ import { type LightsBuffer } from "../lights-buffer";
4
+ import { LightsTextureInitializer } from "./lights-texture-initializer";
5
+ import { type LightObstacleSegment, LightsTextureMask } from "./lights-texture-mask";
6
+
7
+ type ILightsTexture = {
8
+ readonly gridSize: { readonly x: number, readonly y: number };
9
+ readonly format: GPUTextureFormat;
10
+ readonly sampleCount: number;
11
+ };
12
+
13
+ type LightsTextureProperties = {
14
+ readonly resolutionPerLight: number;
15
+ readonly maxLightSize: number;
16
+ readonly antialiased: boolean;
17
+ readonly filtering: GPUFilterMode;
18
+ };
19
+
20
+ class LightsTexture {
21
+ private readonly lightsBuffer: LightsBuffer;
22
+
23
+ public readonly texture: GPUTexture;
24
+ public readonly gridSize: { readonly x: number, readonly y: number };
25
+
26
+ private readonly textureMultisampled: GPUTexture | null = null;
27
+ private readonly textureRenderpassDescriptor: GPURenderPassDescriptor;
28
+
29
+ private readonly textureInitializer: LightsTextureInitializer;
30
+ private readonly textureMask: LightsTextureMask;
31
+
32
+ public constructor(device: GPUDevice, lightsBuffer: LightsBuffer, lightsTextureProperties: LightsTextureProperties) {
33
+ this.lightsBuffer = lightsBuffer;
34
+
35
+ const cellsCount = this.lightsBuffer.maxLightsCount / 4;
36
+ const gridSize = {
37
+ x: Math.ceil(Math.sqrt(cellsCount)),
38
+ y: 0,
39
+ };
40
+ gridSize.y = Math.ceil(cellsCount / gridSize.x);
41
+ this.gridSize = gridSize;
42
+
43
+ const lightTextureSize = {
44
+ width: gridSize.x * lightsTextureProperties.resolutionPerLight,
45
+ height: gridSize.y * lightsTextureProperties.resolutionPerLight,
46
+ };
47
+
48
+ const format = "rgba8unorm";
49
+ this.texture = device.createTexture({
50
+ label: "LightsTextureMask texture",
51
+ size: [lightTextureSize.width, lightTextureSize.height],
52
+ format,
53
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
54
+ });
55
+
56
+ if (lightsTextureProperties.antialiased) {
57
+ this.textureMultisampled = device.createTexture({
58
+ label: "LightsTextureMask texture multisampled",
59
+ size: [lightTextureSize.width, lightTextureSize.height],
60
+ format,
61
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
62
+ sampleCount: 4,
63
+ });
64
+ }
65
+
66
+ const textureToRenderTo = this.textureMultisampled ?? this.texture;
67
+
68
+ const textureRenderpassColorAttachment: GPURenderPassColorAttachment = {
69
+ view: textureToRenderTo.createView(),
70
+ clearValue: [0, 0, 0, 1],
71
+ loadOp: "load",
72
+ storeOp: "store",
73
+ };
74
+ if (lightsTextureProperties.antialiased) {
75
+ textureRenderpassColorAttachment.resolveTarget = this.texture.createView();
76
+ }
77
+ this.textureRenderpassDescriptor = {
78
+ label: "lights-renderer render to texture renderpass",
79
+ colorAttachments: [textureRenderpassColorAttachment],
80
+ };
81
+
82
+ const lightsTexture: ILightsTexture = {
83
+ gridSize,
84
+ format,
85
+ sampleCount: this.textureMultisampled?.sampleCount ?? 1,
86
+ };
87
+ this.textureInitializer = new LightsTextureInitializer(device, lightsBuffer, lightsTexture, lightsTextureProperties.maxLightSize);
88
+ this.textureMask = new LightsTextureMask(device, lightsBuffer, lightsTexture, lightsTextureProperties.maxLightSize);
89
+ }
90
+
91
+ public update(commandEncoder: GPUCommandEncoder): void {
92
+ this.textureMask.setLightsCount(this.lightsBuffer.lightsCount);
93
+
94
+ const renderpassEncoder = commandEncoder.beginRenderPass(this.textureRenderpassDescriptor);
95
+ const [textureWidth, textureHeight] = [this.texture.width, this.texture.height];
96
+ renderpassEncoder.setViewport(0, 0, textureWidth, textureHeight, 0, 1);
97
+ renderpassEncoder.setScissorRect(0, 0, textureWidth, textureHeight);
98
+ renderpassEncoder.executeBundles([
99
+ this.textureInitializer.getRenderBundle(),
100
+ this.textureMask.getRenderBundle(),
101
+ ]);
102
+ renderpassEncoder.end();
103
+ }
104
+
105
+ public setObstacles(segments: ReadonlyArray<LightObstacleSegment>): void {
106
+ this.textureMask.setObstacles(segments);
107
+ }
108
+
109
+ public destroy(): void {
110
+ this.texture.destroy();
111
+ this.textureMultisampled?.destroy();
112
+
113
+ this.textureInitializer.destroy();
114
+ this.textureMask.destroy();
115
+ }
116
+ }
117
+
118
+ export {
119
+ LightsTexture, type ILightsTexture, type LightsTextureProperties
120
+ };
121
+
@@ -0,0 +1,23 @@
1
+ type Point = [number, number];
2
+
3
+ type Light = {
4
+ readonly position: Point; // center of the light
5
+ readonly radius: number; // radius of the light
6
+ readonly color: [number, number, number]; // color (normalized)
7
+ readonly intensity: number; // intensity at the center
8
+ readonly attenuationLinear: number; // describes how the intensity declines with distance
9
+ readonly attenuationExp: number; // describes how the intensity declines with distance
10
+ };
11
+ /* The light intensity is computed as follow:
12
+ intensity
13
+ ----------------------------------------------------- * cos(x * pi/2)
14
+ 1 + attenuationLinear * x + attenuationExp * (x * x)
15
+
16
+ where "x" is the normalized distance to the light position
17
+ */
18
+
19
+
20
+ export {
21
+ type Light,
22
+ type Point,
23
+ };
@@ -0,0 +1,63 @@
1
+ import * as wgpuMatrix from "wgpu-matrix";
2
+ import { type Point } from "./types";
3
+
4
+ type Parameters = {
5
+ readonly viewportSize: {
6
+ width: number;
7
+ height: number;
8
+ };
9
+ readonly center?: Point;
10
+ readonly zoom?: number;
11
+ };
12
+
13
+ class Viewport {
14
+ private readonly invViewProjectionMatrix: wgpuMatrix.Mat4Arg = wgpuMatrix.mat4.identity();
15
+
16
+ private readonly viewportSize = { width: 1, height: 1 };
17
+ private readonly topLeft: Point = [0, 0];
18
+ private zoom: number = 1;
19
+
20
+ public constructor(params: Parameters) {
21
+ this.setViewportSize(params.viewportSize.width, params.viewportSize.height);
22
+
23
+ const initialTopLeft = params.center ?? this.topLeft;
24
+ this.setTopLeft(...initialTopLeft);
25
+
26
+ const initialZoom = params.zoom ?? 1;
27
+ this.setZoom(initialZoom);
28
+ }
29
+
30
+ public get invertViewProjectionMatrix(): wgpuMatrix.Mat4Arg {
31
+ return this.invViewProjectionMatrix;
32
+ }
33
+
34
+ public setViewportSize(width: number, height: number): void {
35
+ this.viewportSize.width = width;
36
+ this.viewportSize.height = height;
37
+ this.updateMatrices();
38
+ }
39
+
40
+ public setTopLeft(x: number, y: number): void {
41
+ this.topLeft[0] = x;
42
+ this.topLeft[1] = y;
43
+ this.updateMatrices();
44
+ }
45
+
46
+ public setZoom(zoom: number): void {
47
+ this.zoom = zoom;
48
+ this.updateMatrices();
49
+ }
50
+
51
+ private updateMatrices(): void {
52
+ wgpuMatrix.mat4.identity(this.invViewProjectionMatrix);
53
+ wgpuMatrix.mat4.multiply(wgpuMatrix.mat4.scaling([1, -1, 0]), this.invViewProjectionMatrix, this.invViewProjectionMatrix);
54
+ wgpuMatrix.mat4.multiply(wgpuMatrix.mat4.translation([1, 1, 0]), this.invViewProjectionMatrix, this.invViewProjectionMatrix);
55
+ wgpuMatrix.mat4.multiply(wgpuMatrix.mat4.scaling([0.5 * this.viewportSize.width / this.zoom, 0.5 * this.viewportSize.height / this.zoom, 0]), this.invViewProjectionMatrix, this.invViewProjectionMatrix);
56
+ wgpuMatrix.mat4.multiply(wgpuMatrix.mat4.translation([this.topLeft[0], this.topLeft[1], 0]), this.invViewProjectionMatrix, this.invViewProjectionMatrix);
57
+ }
58
+ }
59
+
60
+ export {
61
+ Viewport
62
+ };
63
+
@@ -0,0 +1 @@
1
+ export const FLOAT32S_PER_SPRITE = 12 // vec2(translate) + vec2(scale) + vec4(tint) + opacity + rotation + emissiveIntensity + sortValue