@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,56 @@
|
|
|
1
|
+
import createTexture from '../create-texture.js'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// Frame buffer textures automatically resize to match the cobalt viewport.
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
type: 'fbTexture',
|
|
8
|
+
refs: [ ],
|
|
9
|
+
|
|
10
|
+
// @params Object cobalt renderer world object
|
|
11
|
+
// @params Object options optional data passed when initing this node
|
|
12
|
+
onInit: async function (cobalt, options={}) {
|
|
13
|
+
return init(cobalt, options)
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
onRun: function (cobalt, node, webGpuCommandEncoder) {
|
|
17
|
+
// do whatever you need for this node. webgpu renderpasses, etc.
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
onDestroy: function (cobalt, node) {
|
|
21
|
+
// any cleanup for your node should go here (releasing textures, etc.)
|
|
22
|
+
destroy(data)
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
onResize: function (cobalt, node) {
|
|
26
|
+
// do whatever you need when the dimensions of the renderer change (resize textures, etc.)
|
|
27
|
+
resize(cobalt, node)
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
onViewportPosition: function (cobalt, node) { },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async function init (cobalt, node) {
|
|
35
|
+
const { device } = cobalt
|
|
36
|
+
|
|
37
|
+
const { label, mip_count, format, usage, viewportScale } = node.options
|
|
38
|
+
|
|
39
|
+
return createTexture(device, label, cobalt.viewport.width * viewportScale, cobalt.viewport.height * viewportScale, mip_count, format, usage)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
function destroy (node) {
|
|
44
|
+
// destroy the existing texture before we re-create it to avoid leaking memory
|
|
45
|
+
node.data.texture.destroy()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
function resize (cobalt, node) {
|
|
50
|
+
const { device } = cobalt
|
|
51
|
+
destroy(node)
|
|
52
|
+
const { width, height } = cobalt.viewport
|
|
53
|
+
const { options } = node
|
|
54
|
+
const scale = node.options.viewportScale
|
|
55
|
+
node.data = createTexture(device, options.label, width * scale, height * scale, options.mip_count, options.format, options.usage)
|
|
56
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
## Lighting system
|
|
2
|
+
Cobalt uses a deferred lighting system, where the `Light` node receives as input the albedo texture and combines it with the computed illumination to build the output.
|
|
3
|
+
|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
_On the left, the input albedo texture. On the right the output composition result._
|
|
7
|
+
|
|
8
|
+
### Algorithm
|
|
9
|
+
|
|
10
|
+
The lights are computed in 2 renderpasses:
|
|
11
|
+
- first renderpass: for each light we compute their illumination and shadows. We store the results in a texture called the "lights texture", where each lights has their data stored independently. This pass is handled by the `LightsTexture` class.
|
|
12
|
+
- second renderpass: composition time. For each pixel, we determine which lights might affect it. Then we check these lights contribution by sampling the lights texture. We sum all contributions to get a total illumination for the pixel. We then multiply it by the color sampled from the albedo texture. This pass is handled by the `LightsRenderer` class.
|
|
13
|
+
|
|
14
|
+
We must perform the composition for every frame. However, the lights texture only needs to be recomputed when either the lights (movement, intensity, color etc.) or the shadow-casting objects changed.
|
|
15
|
+
|
|
16
|
+
#### Lighting equation
|
|
17
|
+
We use a lighting equation that is not physically accurate, but is flexible and gives good enough results. You can find it in the `LightsTextureInitializer` shader
|
|
18
|
+
|
|
19
|
+
#### Lights texture
|
|
20
|
+
The lights texture stores the computed illumination of each light. This illumination is a combination of the light intensity and the cast shadows. It is a one-dimensional value because the light color is applied at composition-time. Here is an example of what the lights texture looks like:
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
For technical reasons, we store all lights into a single texture:
|
|
25
|
+
- each light is assigned a square area in the texture. The center of the area is the center of the light. In the example above, there are two areas. All lights have the same resolution, which means that there is a maximum light radius. If a light's radius is above this limit, it means that light is too big to be stored in its area, which will result in visual artifacts. If we stored lights with floating resolution (= the biggest lights are lower resolution than the small ones), we could remove this max radius limit.
|
|
26
|
+
- each light is stored into a single texture channel. This is possible because the light intensity is a simple float. This allows us to store 4 lights in a single texel, one per channel (Red, Green, Blue, Alpha). The above texture has two areas and four channels, which means it can store up to 8 lights.
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
_Illustration of a single area storing the data of 3 lights_
|
|
31
|
+
|
|
32
|
+
The lights texture stores normalized lights intensity (between 0 and 1). At composition-time we multiply the intensity sampled from texture by the maximum intensity of the light, which allows us to have lights brighter than 1.
|
|
33
|
+
|
|
34
|
+
Here is how the lights texture is computed by the `LightsTexture` class (illustrated with a single light for clarity):
|
|
35
|
+
- first we compute the base lights intensity (class `LightsTextureInitializer`)
|
|
36
|
+
- then we add the cast shadows as a mask (class `LightsTextureMask`)
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
_One the left, the base light intensity. On the right, the base light intensity with the cast shadows_
|
|
41
|
+
|
|
42
|
+
##### Cast shadows computing
|
|
43
|
+
All lights in Cobalt are point-lights.
|
|
44
|
+
|
|
45
|
+
The shadow-casting objects are decomposed as a series of segments.
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
_On the left, the base obstacle. On the right, its decomposition into segments P0-P1, P1-P2, P2-P3, P3-P0._
|
|
50
|
+
|
|
51
|
+
Then, the key is to observe that the cast shadow is a quad, where two points are the obstacle segments, and two points are their projection relatively to the point light.
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
_P0' and P3' are the projection of P0 and P3 respectively_
|
|
56
|
+
|
|
57
|
+
This projection is performed in the vertex shader. When the CPU declares an obstacle composed of `vec2` vertices P0 and P1, in the GPU buffer it creates 4 `vec3` vertices where the `z` components indicates whether the vertice will be projected:
|
|
58
|
+
- [P0x, P0y, 0] and [P1x, P1y, 0] will remain at the position of P0 and P1
|
|
59
|
+
- [P0x, P0, 1] and [P1x, P1y, 1] will be their projection
|
|
60
|
+
|
|
61
|
+
And then we draw the quad P0, P0', P1', P1 which covers the cast shadow.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as publicAPI from './public-api.js'
|
|
2
|
+
import { Viewport } from "./viewport";
|
|
3
|
+
import { LightsRenderer } from './lights-renderer.js';
|
|
4
|
+
import { LightsBuffer } from './lights-buffer.js';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 2D lighting and Shadows
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
type: 'cobalt:light',
|
|
13
|
+
|
|
14
|
+
// the inputs and outputs to this node
|
|
15
|
+
refs: [
|
|
16
|
+
{ name: 'in', type: 'textureView', format: 'rgba16float', access: 'read' },
|
|
17
|
+
{ name: 'out', type: 'textureView', format: 'rgba16float', access: 'write' },
|
|
18
|
+
],
|
|
19
|
+
|
|
20
|
+
// cobalt event handling functions
|
|
21
|
+
|
|
22
|
+
// @params Object cobalt renderer world object
|
|
23
|
+
// @params Object options optional data passed when initing this node
|
|
24
|
+
onInit: async function (cobalt, options = {}) {
|
|
25
|
+
return init(cobalt, options)
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
onRun: function (cobalt, node, webGpuCommandEncoder) {
|
|
29
|
+
// do whatever you need for this node. webgpu renderpasses, etc.
|
|
30
|
+
draw(cobalt, node, webGpuCommandEncoder)
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
onDestroy: function (cobalt, node) {
|
|
34
|
+
// any cleanup for your node should go here (releasing textures, etc.)
|
|
35
|
+
destroy(node)
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
onResize: function (cobalt, node) {
|
|
39
|
+
// runs when the viewport size changes (handle resizing textures, etc.)
|
|
40
|
+
resize(cobalt, node)
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
onViewportPosition: function (cobalt, node) {
|
|
44
|
+
// runs when the viewport position changes
|
|
45
|
+
node.data.viewport.setTopLeft(...cobalt.viewport.position);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// optional
|
|
49
|
+
customFunctions: {
|
|
50
|
+
...publicAPI,
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async function init(cobalt, node) {
|
|
56
|
+
|
|
57
|
+
const { device } = cobalt
|
|
58
|
+
|
|
59
|
+
// a 2048x2048 light texture with 4 channels (rgba) with each light lighting a 256x256 region can hold 256 lights
|
|
60
|
+
const MAX_LIGHT_COUNT = 256;
|
|
61
|
+
const MAX_LIGHT_SIZE = 256;
|
|
62
|
+
const lightsBuffer = new LightsBuffer(device, MAX_LIGHT_COUNT);
|
|
63
|
+
|
|
64
|
+
const viewport = new Viewport({
|
|
65
|
+
viewportSize: {
|
|
66
|
+
width: cobalt.viewport.width,
|
|
67
|
+
height: cobalt.viewport.height,
|
|
68
|
+
},
|
|
69
|
+
center: cobalt.viewport.position,
|
|
70
|
+
zoom: cobalt.viewport.zoom,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const lightsRenderer = new LightsRenderer({
|
|
74
|
+
device,
|
|
75
|
+
albedo: {
|
|
76
|
+
view: node.refs.in.data.view,
|
|
77
|
+
sampler: node.refs.in.data.sampler
|
|
78
|
+
},
|
|
79
|
+
targetTexture: node.refs.out.data.texture,
|
|
80
|
+
lightsBuffer,
|
|
81
|
+
lightsTextureProperties: {
|
|
82
|
+
resolutionPerLight: MAX_LIGHT_SIZE,
|
|
83
|
+
maxLightSize: MAX_LIGHT_SIZE,
|
|
84
|
+
antialiased: false,
|
|
85
|
+
filtering: "nearest",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
lightsBuffer,
|
|
91
|
+
lightsBufferNeedsUpdate: true,
|
|
92
|
+
|
|
93
|
+
lightsTextureNeedsUpdate: true,
|
|
94
|
+
lightsRenderer,
|
|
95
|
+
|
|
96
|
+
viewport,
|
|
97
|
+
|
|
98
|
+
lights: [],
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
function draw(cobalt, node, commandEncoder) {
|
|
104
|
+
if (node.data.lightsBufferNeedsUpdate) {
|
|
105
|
+
const lightsBuffer = node.data.lightsBuffer;
|
|
106
|
+
lightsBuffer.setLights(node.data.lights);
|
|
107
|
+
node.data.lightsBufferNeedsUpdate = false;
|
|
108
|
+
node.data.lightsTextureNeedsUpdate = true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const lightsRenderer = node.data.lightsRenderer;
|
|
112
|
+
|
|
113
|
+
if (node.data.lightsTextureNeedsUpdate) {
|
|
114
|
+
lightsRenderer.computeLightsTexture(commandEncoder);
|
|
115
|
+
node.data.lightsTextureNeedsUpdate = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const renderpass = commandEncoder.beginRenderPass({
|
|
119
|
+
colorAttachments: [
|
|
120
|
+
{
|
|
121
|
+
view: node.refs.out.data.view,
|
|
122
|
+
clearValue: cobalt.clearValue,
|
|
123
|
+
loadOp: 'load',
|
|
124
|
+
storeOp: 'store'
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
node.data.viewport.setZoom(cobalt.viewport.zoom);
|
|
130
|
+
const invertVpMatrix = node.data.viewport.invertViewProjectionMatrix;
|
|
131
|
+
lightsRenderer.render(renderpass, invertVpMatrix);
|
|
132
|
+
|
|
133
|
+
renderpass.end()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function destroy(node) {
|
|
137
|
+
node.data.lightsBuffer.destroy();
|
|
138
|
+
node.data.lightsRenderer.destroy();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resize(cobalt, node) {
|
|
142
|
+
node.data.lightsRenderer.setAlbedo({
|
|
143
|
+
view: node.refs.in.data.view,
|
|
144
|
+
sampler: node.refs.in.data.sampler
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
node.data.viewport.setViewportSize(cobalt.viewport.width, cobalt.viewport.height);
|
|
148
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/// <reference types="@webgpu/types"/>
|
|
2
|
+
|
|
3
|
+
import { type Light } from "./types";
|
|
4
|
+
|
|
5
|
+
class LightsBuffer {
|
|
6
|
+
public static readonly structs = {
|
|
7
|
+
definition: `
|
|
8
|
+
struct Light { // align(16) size(48)
|
|
9
|
+
color: vec3<f32>, // offset(0) align(16) size(12)
|
|
10
|
+
radius: f32, // offset(12) align(4) size(4)
|
|
11
|
+
position: vec2<f32>, // offset(16) align(8) size(8)
|
|
12
|
+
intensity: f32, // offset(24) align(4) size(4)
|
|
13
|
+
attenuationLinear: f32, // offset(28) align(4) size(4)
|
|
14
|
+
attenuationExp: f32, // offset(32) align(4) size(4)
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
struct LightsBuffer { // align(16)
|
|
18
|
+
count: u32, // offset(0) align(4) size(4)
|
|
19
|
+
// padding
|
|
20
|
+
lights: array<Light>, // offset(16) align(16)
|
|
21
|
+
};
|
|
22
|
+
`,
|
|
23
|
+
light: {
|
|
24
|
+
radius: { offset: 12 },
|
|
25
|
+
position: { offset: 16 },
|
|
26
|
+
},
|
|
27
|
+
lightsBuffer: {
|
|
28
|
+
lights: { offset: 16, stride: 48 },
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
private readonly device: GPUDevice;
|
|
33
|
+
|
|
34
|
+
public readonly maxLightsCount: number;
|
|
35
|
+
private currentLightsCount: number = 0;
|
|
36
|
+
|
|
37
|
+
private readonly buffer: {
|
|
38
|
+
readonly bufferCpu: ArrayBuffer;
|
|
39
|
+
readonly bufferGpu: GPUBuffer;
|
|
40
|
+
};
|
|
41
|
+
public get gpuBuffer(): GPUBuffer {
|
|
42
|
+
return this.buffer.bufferGpu;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public constructor(device: GPUDevice, maxLightsCount: number) {
|
|
46
|
+
this.device = device;
|
|
47
|
+
this.maxLightsCount = maxLightsCount;
|
|
48
|
+
|
|
49
|
+
const bufferCpu = new ArrayBuffer(LightsBuffer.computeBufferBytesLength(maxLightsCount));
|
|
50
|
+
const bufferGpu = device.createBuffer({
|
|
51
|
+
label: "LightsBuffer buffer",
|
|
52
|
+
size: bufferCpu.byteLength,
|
|
53
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.VERTEX,
|
|
54
|
+
});
|
|
55
|
+
this.buffer = { bufferCpu, bufferGpu };
|
|
56
|
+
|
|
57
|
+
this.setLights([]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public setLights(lights: ReadonlyArray<Light>): void {
|
|
61
|
+
if (lights.length > this.maxLightsCount) {
|
|
62
|
+
throw new Error(`Too many lights "${lights.length}", max is "${this.maxLightsCount}".`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const newBufferLength = LightsBuffer.computeBufferBytesLength(lights.length);
|
|
66
|
+
new Uint32Array(this.buffer.bufferCpu, 0, 1).set([lights.length]);
|
|
67
|
+
|
|
68
|
+
lights.forEach((light: Light, index: number) => {
|
|
69
|
+
new Float32Array(this.buffer.bufferCpu, LightsBuffer.structs.lightsBuffer.lights.offset + LightsBuffer.structs.lightsBuffer.lights.stride * index, 9).set([
|
|
70
|
+
...light.color,
|
|
71
|
+
light.radius,
|
|
72
|
+
...light.position,
|
|
73
|
+
light.intensity,
|
|
74
|
+
light.attenuationLinear,
|
|
75
|
+
light.attenuationExp
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
this.device.queue.writeBuffer(this.buffer.bufferGpu, 0, this.buffer.bufferCpu, 0, newBufferLength);
|
|
80
|
+
this.currentLightsCount = lights.length;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public get lightsCount(): number {
|
|
84
|
+
return this.currentLightsCount;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public destroy(): void {
|
|
88
|
+
this.buffer.bufferGpu.destroy();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private static computeBufferBytesLength(lightsCount: number): number {
|
|
92
|
+
return LightsBuffer.structs.lightsBuffer.lights.offset + LightsBuffer.structs.lightsBuffer.lights.stride * lightsCount;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
LightsBuffer
|
|
98
|
+
};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/// <reference types="@webgpu/types"/>
|
|
2
|
+
|
|
3
|
+
import * as wgpuMatrix from "wgpu-matrix";
|
|
4
|
+
import { LightsBuffer } from "./lights-buffer";
|
|
5
|
+
import { LightsTexture, type LightsTextureProperties } from "./texture/lights-texture";
|
|
6
|
+
import { type LightObstacleSegment } from "./texture/lights-texture-mask";
|
|
7
|
+
|
|
8
|
+
type TextureSamplable = {
|
|
9
|
+
readonly view: GPUTextureView;
|
|
10
|
+
readonly sampler: GPUSampler;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type TextureRenderable = {
|
|
14
|
+
readonly format: GPUTextureFormat;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type Parameters = {
|
|
18
|
+
readonly device: GPUDevice;
|
|
19
|
+
readonly albedo: TextureSamplable;
|
|
20
|
+
readonly targetTexture: TextureRenderable;
|
|
21
|
+
readonly lightsBuffer: LightsBuffer;
|
|
22
|
+
readonly lightsTextureProperties: LightsTextureProperties;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
class LightsRenderer {
|
|
26
|
+
private readonly device: GPUDevice;
|
|
27
|
+
|
|
28
|
+
private ambientLight: [number, number, number] = [0.2, 0.2, 0.2];
|
|
29
|
+
|
|
30
|
+
private readonly targetTexture: TextureRenderable;
|
|
31
|
+
|
|
32
|
+
private readonly renderPipeline: GPURenderPipeline;
|
|
33
|
+
private readonly uniformsBufferGpu: GPUBuffer;
|
|
34
|
+
private readonly bindgroup0: GPUBindGroup;
|
|
35
|
+
private bindgroup1: GPUBindGroup;
|
|
36
|
+
private renderBundle: GPURenderBundle;
|
|
37
|
+
|
|
38
|
+
private readonly lightsBuffer: LightsBuffer;
|
|
39
|
+
private readonly lightsTexture: LightsTexture;
|
|
40
|
+
|
|
41
|
+
public constructor(params: Parameters) {
|
|
42
|
+
this.device = params.device;
|
|
43
|
+
|
|
44
|
+
this.targetTexture = params.targetTexture;
|
|
45
|
+
this.lightsBuffer = params.lightsBuffer;
|
|
46
|
+
|
|
47
|
+
this.lightsTexture = new LightsTexture(params.device, params.lightsBuffer, params.lightsTextureProperties);
|
|
48
|
+
|
|
49
|
+
this.uniformsBufferGpu = params.device.createBuffer({
|
|
50
|
+
label: "LightsRenderer uniforms buffer",
|
|
51
|
+
size: 80,
|
|
52
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const shaderModule = params.device.createShaderModule({
|
|
56
|
+
label: "LightsRenderer shader module",
|
|
57
|
+
code: `
|
|
58
|
+
struct Uniforms { // align(16) size(80)
|
|
59
|
+
invertViewMatrix: mat4x4<f32>, // offset(0) align(16) size(64)
|
|
60
|
+
ambientLight: vec3<f32>, // offset(64) align(16) size(12)
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
${LightsBuffer.structs.definition}
|
|
64
|
+
|
|
65
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
66
|
+
@group(0) @binding(1) var<storage,read> lightsBuffer: LightsBuffer;
|
|
67
|
+
@group(0) @binding(2) var lightsTexture: texture_2d<f32>;
|
|
68
|
+
@group(0) @binding(3) var lightsTextureSampler: sampler;
|
|
69
|
+
|
|
70
|
+
@group(1) @binding(0) var albedoTexture: texture_2d<f32>;
|
|
71
|
+
@group(1) @binding(1) var albedoSampler: sampler;
|
|
72
|
+
|
|
73
|
+
struct VertexIn {
|
|
74
|
+
@builtin(vertex_index) vertexIndex: u32,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
struct VertexOut {
|
|
78
|
+
@builtin(position) position: vec4<f32>,
|
|
79
|
+
@location(0) worldPosition: vec2<f32>,
|
|
80
|
+
@location(1) uv: vec2<f32>,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
@vertex
|
|
84
|
+
fn main_vertex(in: VertexIn) -> VertexOut {
|
|
85
|
+
const corners = array<vec2<f32>, 4>(
|
|
86
|
+
vec2<f32>(-1, -1),
|
|
87
|
+
vec2<f32>(1, -1),
|
|
88
|
+
vec2<f32>(-1, 1),
|
|
89
|
+
vec2<f32>(1, 1),
|
|
90
|
+
);
|
|
91
|
+
let screenPosition = corners[in.vertexIndex];
|
|
92
|
+
|
|
93
|
+
var out: VertexOut;
|
|
94
|
+
out.position = vec4<f32>(screenPosition, 0.0, 1.0);
|
|
95
|
+
out.worldPosition = (uniforms.invertViewMatrix * out.position).xy;
|
|
96
|
+
out.uv = 0.5 + 0.5 * screenPosition * vec2<f32>(1.0, -1.0);
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
struct FragmentOut {
|
|
101
|
+
@location(0) color: vec4<f32>,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const cellsGridSizeU = vec2<u32>(${this.lightsTexture.gridSize.x}, ${this.lightsTexture.gridSize.y});
|
|
105
|
+
const cellsGridSizeF = vec2<f32>(${this.lightsTexture.gridSize.x}, ${this.lightsTexture.gridSize.y});
|
|
106
|
+
|
|
107
|
+
fn sampleLightBaseIntensity(lightId: u32, localUv: vec2<f32>) -> f32 {
|
|
108
|
+
let cellIndex = lightId / 4u;
|
|
109
|
+
let indexInCell = lightId % 4u;
|
|
110
|
+
|
|
111
|
+
let cellIdU = vec2<u32>(
|
|
112
|
+
cellIndex % cellsGridSizeU.x,
|
|
113
|
+
cellIndex / cellsGridSizeU.x,
|
|
114
|
+
);
|
|
115
|
+
let cellIdF = vec2<f32>(cellIdU);
|
|
116
|
+
let uv = (cellIdF + localUv) / cellsGridSizeF;
|
|
117
|
+
let uvYInverted = vec2<f32>(uv.x, 1.0 - uv.y);
|
|
118
|
+
let sample = textureSampleLevel(lightsTexture, lightsTextureSampler, uvYInverted, 0.0);
|
|
119
|
+
let channel = vec4<f32>(
|
|
120
|
+
vec4<u32>(indexInCell) == vec4<u32>(0u, 1u, 2u, 3u),
|
|
121
|
+
);
|
|
122
|
+
return dot(sample, channel);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fn compute_lights(worldPosition: vec2<f32>) -> vec3<f32> {
|
|
126
|
+
var color = vec3<f32>(uniforms.ambientLight);
|
|
127
|
+
|
|
128
|
+
const maxUvDistance = f32(${1 - 2 / params.lightsTextureProperties.resolutionPerLight});
|
|
129
|
+
|
|
130
|
+
let lightsCount = lightsBuffer.count;
|
|
131
|
+
for (var iLight = 0u; iLight < lightsCount; iLight++) {
|
|
132
|
+
let light = lightsBuffer.lights[iLight];
|
|
133
|
+
let lightSize = f32(${params.lightsTextureProperties.resolutionPerLight});
|
|
134
|
+
let relativePosition = (worldPosition - light.position) / lightSize;
|
|
135
|
+
if (max(abs(relativePosition.x), abs(relativePosition.y)) < maxUvDistance) {
|
|
136
|
+
let localUv = 0.5 + 0.5 * relativePosition;
|
|
137
|
+
let lightIntensity = light.intensity * sampleLightBaseIntensity(iLight, localUv);
|
|
138
|
+
color += lightIntensity * light.color;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return color;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@fragment
|
|
146
|
+
fn main_fragment(in: VertexOut) -> FragmentOut {
|
|
147
|
+
let light = compute_lights(in.worldPosition);
|
|
148
|
+
let albedo = textureSample(albedoTexture, albedoSampler, in.uv);
|
|
149
|
+
let color = albedo.rgb * light;
|
|
150
|
+
|
|
151
|
+
var out: FragmentOut;
|
|
152
|
+
out.color = vec4<f32>(color, 1.0);
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
`,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
this.renderPipeline = params.device.createRenderPipeline({
|
|
159
|
+
label: "LightsRenderer renderpipeline",
|
|
160
|
+
layout: "auto",
|
|
161
|
+
vertex: {
|
|
162
|
+
module: shaderModule,
|
|
163
|
+
entryPoint: "main_vertex",
|
|
164
|
+
},
|
|
165
|
+
fragment: {
|
|
166
|
+
module: shaderModule,
|
|
167
|
+
entryPoint: "main_fragment",
|
|
168
|
+
targets: [{
|
|
169
|
+
format: this.targetTexture.format,
|
|
170
|
+
}],
|
|
171
|
+
},
|
|
172
|
+
primitive: {
|
|
173
|
+
cullMode: "none",
|
|
174
|
+
topology: "triangle-strip",
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const bindgroupLayout = this.renderPipeline.getBindGroupLayout(0);
|
|
179
|
+
|
|
180
|
+
this.bindgroup0 = params.device.createBindGroup({
|
|
181
|
+
label: "LightsRenderer bindgroup 0",
|
|
182
|
+
layout: bindgroupLayout,
|
|
183
|
+
entries: [
|
|
184
|
+
{
|
|
185
|
+
binding: 0,
|
|
186
|
+
resource: { buffer: this.uniformsBufferGpu },
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
binding: 1,
|
|
190
|
+
resource: { buffer: this.lightsBuffer.gpuBuffer },
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
binding: 2,
|
|
194
|
+
resource: this.lightsTexture.texture.createView({ label: "LightsRenderer lightsTexture view" }),
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
binding: 3,
|
|
198
|
+
resource: params.device.createSampler({
|
|
199
|
+
label: "LightsRenderer sampler",
|
|
200
|
+
addressModeU: "clamp-to-edge",
|
|
201
|
+
addressModeV: "clamp-to-edge",
|
|
202
|
+
magFilter: params.lightsTextureProperties.filtering,
|
|
203
|
+
minFilter: params.lightsTextureProperties.filtering,
|
|
204
|
+
}),
|
|
205
|
+
},
|
|
206
|
+
]
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
this.bindgroup1 = this.buildBindgroup1(params.albedo);
|
|
210
|
+
this.renderBundle = this.buildRenderBundle();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public computeLightsTexture(commandEncoder: GPUCommandEncoder): void {
|
|
214
|
+
this.lightsTexture.update(commandEncoder);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public render(renderpassEncoder: GPURenderPassEncoder, invertVpMatrix: wgpuMatrix.Mat4Arg): void {
|
|
218
|
+
const uniformsBufferCpu = new ArrayBuffer(80);
|
|
219
|
+
new Float32Array(uniformsBufferCpu, 0, 16).set(invertVpMatrix);
|
|
220
|
+
new Float32Array(uniformsBufferCpu, 64, 3).set(this.ambientLight);
|
|
221
|
+
this.device.queue.writeBuffer(this.uniformsBufferGpu, 0, uniformsBufferCpu);
|
|
222
|
+
|
|
223
|
+
renderpassEncoder.executeBundles([this.renderBundle]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
public setAlbedo(albedo: TextureSamplable): void {
|
|
227
|
+
this.bindgroup1 = this.buildBindgroup1(albedo);
|
|
228
|
+
this.renderBundle = this.buildRenderBundle();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public setAmbientLight(color: [number, number, number]): void {
|
|
232
|
+
this.ambientLight = [...color];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
public setObstacles(segments: ReadonlyArray<LightObstacleSegment>): void {
|
|
236
|
+
this.lightsTexture.setObstacles(segments);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
public destroy(): void {
|
|
240
|
+
this.uniformsBufferGpu.destroy();
|
|
241
|
+
|
|
242
|
+
this.lightsTexture.destroy();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private buildBindgroup1(albedo: TextureSamplable): GPUBindGroup {
|
|
246
|
+
return this.device.createBindGroup({
|
|
247
|
+
label: "LightsRenderer bindgroup 1",
|
|
248
|
+
layout: this.renderPipeline.getBindGroupLayout(1),
|
|
249
|
+
entries: [
|
|
250
|
+
{
|
|
251
|
+
binding: 0,
|
|
252
|
+
resource: albedo.view,
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
binding: 1,
|
|
256
|
+
resource: albedo.sampler,
|
|
257
|
+
},
|
|
258
|
+
]
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private buildRenderBundle(): GPURenderBundle {
|
|
263
|
+
const renderBundleEncoder = this.device.createRenderBundleEncoder({
|
|
264
|
+
label: "LightsRenderer renderbundle encoder",
|
|
265
|
+
colorFormats: [this.targetTexture.format],
|
|
266
|
+
});
|
|
267
|
+
renderBundleEncoder.setPipeline(this.renderPipeline);
|
|
268
|
+
renderBundleEncoder.setBindGroup(0, this.bindgroup0);
|
|
269
|
+
renderBundleEncoder.setBindGroup(1, this.bindgroup1);
|
|
270
|
+
renderBundleEncoder.draw(4);
|
|
271
|
+
return renderBundleEncoder.finish({ label: "LightsRenderer renderbundle" });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export {
|
|
276
|
+
LightsRenderer
|
|
277
|
+
};
|
|
278
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import uuid from '../uuid.js'
|
|
2
|
+
import { vec2 } from '../deps.js'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// public API to interact with a lighting/shadows node.
|
|
6
|
+
|
|
7
|
+
export function setLights(cobalt, node, lights) {
|
|
8
|
+
node.data.lights = lights;
|
|
9
|
+
node.data.lightsBufferNeedsUpdate = true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function setAmbientLight(cobalt, node, color) {
|
|
13
|
+
node.data.lightsRenderer.setAmbientLight(color);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function setOccluders(cobalt, node, segmentsList) {
|
|
17
|
+
node.data.lightsRenderer.setObstacles(segmentsList);
|
|
18
|
+
node.data.lightsTextureNeedsUpdate = true;
|
|
19
|
+
}
|
|
20
|
+
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|