@footgun/cobalt 0.10.0 → 0.11.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.
@@ -1,4 +1,6 @@
1
- export const LIGHTS_STRUCT_DEFINITION = `
1
+ class LightsBuffer {
2
+ static structs = {
3
+ definition: `
2
4
  struct Light { // align(16) size(48)
3
5
  color: vec3<f32>, // offset(0) align(16) size(12)
4
6
  radius: f32, // offset(12) align(4) size(4)
@@ -13,59 +15,61 @@ struct LightsBuffer { // align(16)
13
15
  // padding
14
16
  lights: array<Light>, // offset(16) align(16)
15
17
  };
16
- `
17
-
18
- export const LIGHTS_BUFFER_STRUCTS = {
19
- light: {
20
- radius: { offset: 12 },
21
- position: { offset: 16 },
22
- },
23
- lightsBuffer: {
24
- lights: { offset: 16, stride: 48 },
25
- },
26
- }
27
-
28
- export function computeLightsBufferByteLength(lightsCount) {
29
- return (
30
- LIGHTS_BUFFER_STRUCTS.lightsBuffer.lights.offset +
31
- LIGHTS_BUFFER_STRUCTS.lightsBuffer.lights.stride * lightsCount
32
- )
33
- }
34
-
35
- export function createLightsBuffer(device, maxLightsCount) {
36
- const lightsBufferCpu = new ArrayBuffer(computeLightsBufferByteLength(maxLightsCount))
37
- const lightsBufferGpu = device.createBuffer({
38
- label: 'LightsBuffer buffer',
39
- size: lightsBufferCpu.byteLength,
40
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX,
41
- })
42
- const data = { lightsBufferCpu, lightsBufferGpu, maxLightsCount, lightsCount: 0 }
43
- writeLightsBuffer(device, data, [])
44
- return data
45
- }
46
-
47
- export function writeLightsBuffer(device, data, lights) {
48
- if (lights.length > data.maxLightsCount) {
49
- throw new Error(`Too many lights "${lights.length}", max is "${data.maxLightsCount}".`)
18
+ `,
19
+ light: {
20
+ radius: { offset: 12 },
21
+ position: { offset: 16 },
22
+ },
23
+ lightsBuffer: {
24
+ lights: { offset: 16, stride: 48 },
25
+ },
26
+ };
27
+ device;
28
+ maxLightsCount;
29
+ currentLightsCount = 0;
30
+ buffer;
31
+ get gpuBuffer() {
32
+ return this.buffer.bufferGpu;
33
+ }
34
+ constructor(device, maxLightsCount) {
35
+ this.device = device;
36
+ this.maxLightsCount = maxLightsCount;
37
+ const bufferCpu = new ArrayBuffer(LightsBuffer.computeBufferBytesLength(maxLightsCount));
38
+ const bufferGpu = device.createBuffer({
39
+ label: "LightsBuffer buffer",
40
+ size: bufferCpu.byteLength,
41
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX,
42
+ });
43
+ this.buffer = { bufferCpu, bufferGpu };
44
+ this.setLights([]);
45
+ }
46
+ setLights(lights) {
47
+ if (lights.length > this.maxLightsCount) {
48
+ throw new Error(`Too many lights "${lights.length}", max is "${this.maxLightsCount}".`);
49
+ }
50
+ const newBufferLength = LightsBuffer.computeBufferBytesLength(lights.length);
51
+ new Uint32Array(this.buffer.bufferCpu, 0, 1).set([lights.length]);
52
+ lights.forEach((light, index) => {
53
+ new Float32Array(this.buffer.bufferCpu, LightsBuffer.structs.lightsBuffer.lights.offset + LightsBuffer.structs.lightsBuffer.lights.stride * index, 9).set([
54
+ ...light.color,
55
+ light.radius,
56
+ ...light.position,
57
+ light.intensity,
58
+ light.attenuationLinear,
59
+ light.attenuationExp
60
+ ]);
61
+ });
62
+ this.device.queue.writeBuffer(this.buffer.bufferGpu, 0, this.buffer.bufferCpu, 0, newBufferLength);
63
+ this.currentLightsCount = lights.length;
64
+ }
65
+ get lightsCount() {
66
+ return this.currentLightsCount;
67
+ }
68
+ destroy() {
69
+ this.buffer.bufferGpu.destroy();
70
+ }
71
+ static computeBufferBytesLength(lightsCount) {
72
+ return LightsBuffer.structs.lightsBuffer.lights.offset + LightsBuffer.structs.lightsBuffer.lights.stride * lightsCount;
50
73
  }
51
- const { lightsBufferCpu, lightsBufferGpu } = data
52
- const { offset, stride } = LIGHTS_BUFFER_STRUCTS.lightsBuffer.lights
53
- new Uint32Array(lightsBufferCpu, 0, 1).set([lights.length])
54
- lights.forEach((light, index) => {
55
- new Float32Array(lightsBufferCpu, offset + stride * index, 9).set([
56
- ...light.color,
57
- light.radius,
58
- ...light.position,
59
- light.intensity,
60
- light.attenuationLinear,
61
- light.attenuationExp,
62
- ])
63
- })
64
- const newBufferLength = computeLightsBufferByteLength(lights.length)
65
- device.queue.writeBuffer(lightsBufferGpu, 0, lightsBufferCpu, 0, newBufferLength)
66
- data.lightsCount = lights.length
67
- }
68
-
69
- export function destroyLightsBuffer(data) {
70
- data.lightsBufferGpu.destroy()
71
74
  }
75
+ export { LightsBuffer };
@@ -0,0 +1,230 @@
1
+ import { LightsBuffer } from "./lights-buffer.js";
2
+ import { LightsTexture } from "./texture/lights-texture.js";
3
+
4
+ class LightsRenderer {
5
+ device;
6
+ ambientLight = [0.2, 0.2, 0.2];
7
+ targetTexture;
8
+ renderPipeline;
9
+ uniformsBufferGpu;
10
+ bindgroup0;
11
+ bindgroup1;
12
+ renderBundle;
13
+ lightsBuffer;
14
+ lightsTexture;
15
+ constructor(params) {
16
+ this.device = params.device;
17
+ this.targetTexture = params.targetTexture;
18
+ this.lightsBuffer = params.lightsBuffer;
19
+ this.lightsTexture = new LightsTexture(params.device, params.lightsBuffer, params.lightsTextureProperties);
20
+ this.uniformsBufferGpu = params.device.createBuffer({
21
+ label: "LightsRenderer uniforms buffer",
22
+ size: 80,
23
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
24
+ });
25
+ const shaderModule = params.device.createShaderModule({
26
+ label: "LightsRenderer shader module",
27
+ code: `
28
+ struct Uniforms { // align(16) size(80)
29
+ invertViewMatrix: mat4x4<f32>, // offset(0) align(16) size(64)
30
+ ambientLight: vec3<f32>, // offset(64) align(16) size(12)
31
+ };
32
+
33
+ ${LightsBuffer.structs.definition}
34
+
35
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
36
+ @group(0) @binding(1) var<storage,read> lightsBuffer: LightsBuffer;
37
+ @group(0) @binding(2) var lightsTexture: texture_2d<f32>;
38
+ @group(0) @binding(3) var lightsTextureSampler: sampler;
39
+
40
+ @group(1) @binding(0) var albedoTexture: texture_2d<f32>;
41
+ @group(1) @binding(1) var albedoSampler: sampler;
42
+
43
+ struct VertexIn {
44
+ @builtin(vertex_index) vertexIndex: u32,
45
+ };
46
+
47
+ struct VertexOut {
48
+ @builtin(position) position: vec4<f32>,
49
+ @location(0) worldPosition: vec2<f32>,
50
+ @location(1) uv: vec2<f32>,
51
+ };
52
+
53
+ @vertex
54
+ fn main_vertex(in: VertexIn) -> VertexOut {
55
+ const corners = array<vec2<f32>, 4>(
56
+ vec2<f32>(-1, -1),
57
+ vec2<f32>(1, -1),
58
+ vec2<f32>(-1, 1),
59
+ vec2<f32>(1, 1),
60
+ );
61
+ let screenPosition = corners[in.vertexIndex];
62
+
63
+ var out: VertexOut;
64
+ out.position = vec4<f32>(screenPosition, 0.0, 1.0);
65
+ out.worldPosition = (uniforms.invertViewMatrix * out.position).xy;
66
+ out.uv = 0.5 + 0.5 * screenPosition * vec2<f32>(1.0, -1.0);
67
+ return out;
68
+ }
69
+
70
+ struct FragmentOut {
71
+ @location(0) color: vec4<f32>,
72
+ };
73
+
74
+ const cellsGridSizeU = vec2<u32>(${this.lightsTexture.gridSize.x}, ${this.lightsTexture.gridSize.y});
75
+ const cellsGridSizeF = vec2<f32>(${this.lightsTexture.gridSize.x}, ${this.lightsTexture.gridSize.y});
76
+
77
+ fn sampleLightBaseIntensity(lightId: u32, localUv: vec2<f32>) -> f32 {
78
+ let cellIndex = lightId / 4u;
79
+ let indexInCell = lightId % 4u;
80
+
81
+ let cellIdU = vec2<u32>(
82
+ cellIndex % cellsGridSizeU.x,
83
+ cellIndex / cellsGridSizeU.x,
84
+ );
85
+ let cellIdF = vec2<f32>(cellIdU);
86
+ let uv = (cellIdF + localUv) / cellsGridSizeF;
87
+ let uvYInverted = vec2<f32>(uv.x, 1.0 - uv.y);
88
+ let sample = textureSampleLevel(lightsTexture, lightsTextureSampler, uvYInverted, 0.0);
89
+ let channel = vec4<f32>(
90
+ vec4<u32>(indexInCell) == vec4<u32>(0u, 1u, 2u, 3u),
91
+ );
92
+ return dot(sample, channel);
93
+ }
94
+
95
+ fn compute_lights(worldPosition: vec2<f32>) -> vec3<f32> {
96
+ var color = vec3<f32>(uniforms.ambientLight);
97
+
98
+ const maxUvDistance = f32(${1 - 2 / params.lightsTextureProperties.resolutionPerLight});
99
+
100
+ let lightsCount = lightsBuffer.count;
101
+ for (var iLight = 0u; iLight < lightsCount; iLight++) {
102
+ let light = lightsBuffer.lights[iLight];
103
+ let lightSize = f32(${params.lightsTextureProperties.resolutionPerLight});
104
+ let relativePosition = (worldPosition - light.position) / lightSize;
105
+ if (max(abs(relativePosition.x), abs(relativePosition.y)) < maxUvDistance) {
106
+ let localUv = 0.5 + 0.5 * relativePosition;
107
+ let lightIntensity = light.intensity * sampleLightBaseIntensity(iLight, localUv);
108
+ color += lightIntensity * light.color;
109
+ }
110
+ }
111
+
112
+ return color;
113
+ }
114
+
115
+ @fragment
116
+ fn main_fragment(in: VertexOut) -> FragmentOut {
117
+ let light = compute_lights(in.worldPosition);
118
+ let albedo = textureSample(albedoTexture, albedoSampler, in.uv);
119
+ let color = albedo.rgb * light;
120
+
121
+ var out: FragmentOut;
122
+ out.color = vec4<f32>(color, 1.0);
123
+ return out;
124
+ }
125
+ `,
126
+ });
127
+ this.renderPipeline = params.device.createRenderPipeline({
128
+ label: "LightsRenderer renderpipeline",
129
+ layout: "auto",
130
+ vertex: {
131
+ module: shaderModule,
132
+ entryPoint: "main_vertex",
133
+ },
134
+ fragment: {
135
+ module: shaderModule,
136
+ entryPoint: "main_fragment",
137
+ targets: [{
138
+ format: this.targetTexture.format,
139
+ }],
140
+ },
141
+ primitive: {
142
+ cullMode: "none",
143
+ topology: "triangle-strip",
144
+ },
145
+ });
146
+ const bindgroupLayout = this.renderPipeline.getBindGroupLayout(0);
147
+ this.bindgroup0 = params.device.createBindGroup({
148
+ label: "LightsRenderer bindgroup 0",
149
+ layout: bindgroupLayout,
150
+ entries: [
151
+ {
152
+ binding: 0,
153
+ resource: { buffer: this.uniformsBufferGpu },
154
+ },
155
+ {
156
+ binding: 1,
157
+ resource: { buffer: this.lightsBuffer.gpuBuffer },
158
+ },
159
+ {
160
+ binding: 2,
161
+ resource: this.lightsTexture.texture.createView({ label: "LightsRenderer lightsTexture view" }),
162
+ },
163
+ {
164
+ binding: 3,
165
+ resource: params.device.createSampler({
166
+ label: "LightsRenderer sampler",
167
+ addressModeU: "clamp-to-edge",
168
+ addressModeV: "clamp-to-edge",
169
+ magFilter: params.lightsTextureProperties.filtering,
170
+ minFilter: params.lightsTextureProperties.filtering,
171
+ }),
172
+ },
173
+ ]
174
+ });
175
+ this.bindgroup1 = this.buildBindgroup1(params.albedo);
176
+ this.renderBundle = this.buildRenderBundle();
177
+ }
178
+ computeLightsTexture(commandEncoder) {
179
+ this.lightsTexture.update(commandEncoder);
180
+ }
181
+ render(renderpassEncoder, invertVpMatrix) {
182
+ const uniformsBufferCpu = new ArrayBuffer(80);
183
+ new Float32Array(uniformsBufferCpu, 0, 16).set(invertVpMatrix);
184
+ new Float32Array(uniformsBufferCpu, 64, 3).set(this.ambientLight);
185
+ this.device.queue.writeBuffer(this.uniformsBufferGpu, 0, uniformsBufferCpu);
186
+ renderpassEncoder.executeBundles([this.renderBundle]);
187
+ }
188
+ setAlbedo(albedo) {
189
+ this.bindgroup1 = this.buildBindgroup1(albedo);
190
+ this.renderBundle = this.buildRenderBundle();
191
+ }
192
+ setAmbientLight(color) {
193
+ this.ambientLight = [...color];
194
+ }
195
+ setObstacles(segments) {
196
+ this.lightsTexture.setObstacles(segments);
197
+ }
198
+ destroy() {
199
+ this.uniformsBufferGpu.destroy();
200
+ this.lightsTexture.destroy();
201
+ }
202
+ buildBindgroup1(albedo) {
203
+ return this.device.createBindGroup({
204
+ label: "LightsRenderer bindgroup 1",
205
+ layout: this.renderPipeline.getBindGroupLayout(1),
206
+ entries: [
207
+ {
208
+ binding: 0,
209
+ resource: albedo.view,
210
+ },
211
+ {
212
+ binding: 1,
213
+ resource: albedo.sampler,
214
+ },
215
+ ]
216
+ });
217
+ }
218
+ buildRenderBundle() {
219
+ const renderBundleEncoder = this.device.createRenderBundleEncoder({
220
+ label: "LightsRenderer renderbundle encoder",
221
+ colorFormats: [this.targetTexture.format],
222
+ });
223
+ renderBundleEncoder.setPipeline(this.renderPipeline);
224
+ renderBundleEncoder.setBindGroup(0, this.bindgroup0);
225
+ renderBundleEncoder.setBindGroup(1, this.bindgroup1);
226
+ renderBundleEncoder.draw(4);
227
+ return renderBundleEncoder.finish({ label: "LightsRenderer renderbundle" });
228
+ }
229
+ }
230
+ export { LightsRenderer };
@@ -1,4 +1,5 @@
1
- import { setMaskObstacles } from './texture/lights-texture-mask.js'
1
+ import { vec2 } from 'wgpu-matrix'
2
+ import uuid from '../uuid.js'
2
3
 
3
4
  // public API to interact with a lighting/shadows node.
4
5
 
@@ -8,10 +9,10 @@ export function setLights(cobalt, node, lights) {
8
9
  }
9
10
 
10
11
  export function setAmbientLight(cobalt, node, color) {
11
- node.data.ambientLight = [...color]
12
+ node.data.lightsRenderer.setAmbientLight(color)
12
13
  }
13
14
 
14
15
  export function setOccluders(cobalt, node, segmentsList) {
15
- setMaskObstacles(cobalt.device, node.data, segmentsList)
16
+ node.data.lightsRenderer.setObstacles(segmentsList)
16
17
  node.data.lightsTextureNeedsUpdate = true
17
18
  }
@@ -0,0 +1,175 @@
1
+ import { LightsBuffer } from "../lights-buffer.js";
2
+
3
+ class LightsTextureInitializer {
4
+ lightsBuffer;
5
+ renderPipeline;
6
+ bindgroup;
7
+ renderBundle;
8
+ constructor(device, lightsBuffer, lightsTexture, maxLightSize) {
9
+ this.lightsBuffer = lightsBuffer;
10
+ const shaderModule = device.createShaderModule({
11
+ label: "LightsTextureInitializer shader module",
12
+ code: `
13
+ ${LightsBuffer.structs.definition}
14
+
15
+ @group(0) @binding(0) var<storage,read> lightsBuffer: LightsBuffer;
16
+
17
+ struct VertexIn {
18
+ @builtin(vertex_index) vertexIndex: u32,
19
+ };
20
+
21
+ struct VertexOut {
22
+ @builtin(position) position: vec4<f32>,
23
+ @location(0) uv: vec2<f32>,
24
+ };
25
+
26
+ const cellsGridSizeU = vec2<u32>(${lightsTexture.gridSize.x}, ${lightsTexture.gridSize.y});
27
+ const cellsGridSizeF = vec2<f32>(${lightsTexture.gridSize.x}, ${lightsTexture.gridSize.y});
28
+
29
+ @vertex
30
+ fn main_vertex(in: VertexIn) -> VertexOut {
31
+ const corners = array<vec2<f32>, 4>(
32
+ vec2<f32>(-1, -1),
33
+ vec2<f32>(1, -1),
34
+ vec2<f32>(-1, 1),
35
+ vec2<f32>(1, 1),
36
+ );
37
+ let screenPosition = corners[in.vertexIndex];
38
+
39
+ var out: VertexOut;
40
+ out.position = vec4<f32>(screenPosition, 0.0, 1.0);
41
+ out.uv = (0.5 + 0.5 * screenPosition) * cellsGridSizeF;
42
+ return out;
43
+ }
44
+
45
+ struct FragmentOut {
46
+ @location(0) color: vec4<f32>,
47
+ };
48
+
49
+ struct LightProperties {
50
+ radius: f32,
51
+ intensity: f32,
52
+ attenuationLinear: f32,
53
+ attenuationExp: f32,
54
+ };
55
+
56
+ fn get_light_properties(lightId: u32) -> LightProperties {
57
+ var lightProperties: LightProperties;
58
+ if (lightId < lightsBuffer.count) {
59
+ let light = lightsBuffer.lights[lightId];
60
+ lightProperties.radius = light.radius;
61
+ lightProperties.intensity = 1.0;
62
+ lightProperties.attenuationLinear = light.attenuationLinear;
63
+ lightProperties.attenuationExp = light.attenuationExp;
64
+ } else {
65
+ lightProperties.radius = 0.0;
66
+ lightProperties.intensity = 0.0;
67
+ lightProperties.attenuationLinear = 0.0;
68
+ lightProperties.attenuationExp = 0.0;
69
+ }
70
+ return lightProperties;
71
+ }
72
+
73
+ @fragment
74
+ fn main_fragment(in: VertexOut) -> FragmentOut {
75
+ let cellId = vec2<u32>(in.uv);
76
+
77
+ let lightIdFrom = 4u * (cellId.x + cellId.y * cellsGridSizeU.x);
78
+ let lightProperties = array<LightProperties, 4>(
79
+ get_light_properties(lightIdFrom + 0u),
80
+ get_light_properties(lightIdFrom + 1u),
81
+ get_light_properties(lightIdFrom + 2u),
82
+ get_light_properties(lightIdFrom + 3u),
83
+ );
84
+
85
+ let sizes = vec4<f32>(
86
+ lightProperties[0].radius,
87
+ lightProperties[1].radius,
88
+ lightProperties[2].radius,
89
+ lightProperties[3].radius,
90
+ );
91
+
92
+ let localUv = fract(in.uv);
93
+ let fromCenter = 2.0 * localUv - 1.0;
94
+ let uvDistanceFromCenter = distance(vec2<f32>(0,0), fromCenter);
95
+ let distancesFromCenter = vec4<f32>(uvDistanceFromCenter / sizes * f32(${maxLightSize}));
96
+
97
+ let intensities = vec4<f32>(
98
+ lightProperties[0].intensity * (1.0 + 1.0 * step(uvDistanceFromCenter, 0.01)),
99
+ lightProperties[1].intensity * (1.0 + 1.0 * step(uvDistanceFromCenter, 0.01)),
100
+ lightProperties[2].intensity * (1.0 + 1.0 * step(uvDistanceFromCenter, 0.01)),
101
+ lightProperties[3].intensity * (1.0 + 1.0 * step(uvDistanceFromCenter, 0.01)),
102
+ );
103
+ let attenuationsLinear = vec4<f32>(
104
+ lightProperties[0].attenuationLinear,
105
+ lightProperties[1].attenuationLinear,
106
+ lightProperties[2].attenuationLinear,
107
+ lightProperties[3].attenuationLinear,
108
+ );
109
+ let attenuationsExp = vec4<f32>(
110
+ lightProperties[0].attenuationExp,
111
+ lightProperties[1].attenuationExp,
112
+ lightProperties[2].attenuationExp,
113
+ lightProperties[3].attenuationExp,
114
+ );
115
+
116
+ var lightIntensities = intensities / (1.0 + distancesFromCenter * (attenuationsLinear + distancesFromCenter * attenuationsExp)); // base intensity equation
117
+ lightIntensities *= cos(distancesFromCenter * ${Math.PI / 2}); // soft limit;
118
+ lightIntensities *= step(distancesFromCenter, vec4<f32>(1.0)); // hard limit
119
+
120
+ var out: FragmentOut;
121
+ out.color = lightIntensities;
122
+ return out;
123
+ }
124
+ `,
125
+ });
126
+ this.renderPipeline = device.createRenderPipeline({
127
+ label: "LightsTextureInitializer renderpipeline",
128
+ layout: "auto",
129
+ vertex: {
130
+ module: shaderModule,
131
+ entryPoint: "main_vertex",
132
+ },
133
+ fragment: {
134
+ module: shaderModule,
135
+ entryPoint: "main_fragment",
136
+ targets: [{
137
+ format: lightsTexture.format,
138
+ }],
139
+ },
140
+ primitive: {
141
+ cullMode: "none",
142
+ topology: "triangle-strip",
143
+ },
144
+ multisample: {
145
+ count: lightsTexture.sampleCount,
146
+ },
147
+ });
148
+ this.bindgroup = device.createBindGroup({
149
+ label: "LightsTextureInitializer bindgroup 0",
150
+ layout: this.renderPipeline.getBindGroupLayout(0),
151
+ entries: [
152
+ {
153
+ binding: 0,
154
+ resource: { buffer: this.lightsBuffer.gpuBuffer },
155
+ },
156
+ ]
157
+ });
158
+ const renderBundleEncoder = device.createRenderBundleEncoder({
159
+ label: "LightsTextureInitializer renderbundle encoder",
160
+ colorFormats: [lightsTexture.format],
161
+ sampleCount: lightsTexture.sampleCount,
162
+ });
163
+ renderBundleEncoder.setPipeline(this.renderPipeline);
164
+ renderBundleEncoder.setBindGroup(0, this.bindgroup);
165
+ renderBundleEncoder.draw(4);
166
+ this.renderBundle = renderBundleEncoder.finish({ label: "LightsTextureInitializer renderbundle" });
167
+ }
168
+ getRenderBundle() {
169
+ return this.renderBundle;
170
+ }
171
+ destroy() {
172
+ // nothing to do
173
+ }
174
+ }
175
+ export { LightsTextureInitializer };