@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,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
+ ![Screenshot](readme/01_illumination.webp)
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
+ ![Screenshot](readme/02_lights_texture.webp)
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
+ ![Screenshot](readme/03_lights_texture_decomposed.webp)
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
+ ![Screenshot](readme/04_lights_texture_mask.webp)
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
+ ![Screenshot](readme/05_lights_obstacle_decomposition.webp)
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
+ ![Screenshot](readme/06_lights_hard_cast_shadows.webp)
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
+