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