@footgun/cobalt 0.6.14 → 0.7.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/CHANGELOG.md +19 -0
- package/bundle.js +1318 -1304
- package/examples/01-primitives/index.html +1 -1
- package/examples/02-sprites/entity-sprite.js +10 -15
- package/examples/02-sprites/hdr.html +321 -0
- package/examples/02-sprites/index.html +21 -120
- package/examples/02-sprites/system-renderer.js +2 -2
- package/examples/03-tiles/index.html +87 -32
- package/examples/03-tiles/system-renderer.js +2 -2
- package/examples/04-overlay/index.html +178 -21
- package/examples/05-bloom/index.html +23 -23
- package/examples/05-bloom/system-renderer.js +2 -2
- package/examples/06-displacement/index.html +20 -112
- package/examples/06-displacement/system-renderer.js +2 -2
- package/examples/08-light/index.html +51 -123
- package/package.json +1 -1
- package/src/cobalt.js +8 -8
- package/src/sprite/public-api.js +57 -177
- package/src/sprite/sprite.js +301 -177
- package/src/sprite/sprite.wgsl +68 -87
- package/src/sprite-hdr/public-api.js +95 -0
- package/src/sprite-hdr/sprite.js +414 -0
- package/src/sprite-hdr/sprite.wgsl +101 -0
- package/src/{sprite → spritesheet}/create-sprite-quads.js +11 -11
- package/src/{sprite → spritesheet}/read-spritesheet.js +62 -28
- package/src/spritesheet/spritesheet.js +75 -0
- package/src/{tile → tile-hdr}/atlas.js +5 -3
- package/src/{tile → tile-hdr}/tile.js +15 -6
- package/examples/04-overlay/deps.js +0 -1
- package/src/overlay/constants.js +0 -1
- package/src/overlay/overlay.js +0 -343
- package/src/overlay/overlay.wgsl +0 -88
- package/src/sprite/constants.js +0 -1
- package/src/sprite/sorted-binary-insert.js +0 -45
- package/src/sprite/spritesheet.js +0 -215
- /package/examples/02-sprites/{Game.js → Global.js} +0 -0
- /package/examples/03-tiles/{Game.js → Global.js} +0 -0
- /package/examples/05-bloom/{Game.js → Global.js} +0 -0
- /package/examples/06-displacement/{Game.js → Global.js} +0 -0
- /package/examples/08-light/{Game.js → Global.js} +0 -0
- /package/src/{tile → tile-hdr}/tile.wgsl +0 -0
package/src/sprite/sprite.wgsl
CHANGED
|
@@ -1,103 +1,84 @@
|
|
|
1
|
-
struct
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
struct ViewParams {
|
|
2
|
+
view : mat4x4<f32>,
|
|
3
|
+
proj : mat4x4<f32>
|
|
4
4
|
};
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
tint: vec4<f32>,
|
|
10
|
-
opacity: f32,
|
|
11
|
-
rotation: f32,
|
|
12
|
-
emissiveIntensity: f32,
|
|
13
|
-
sortValue: f32,
|
|
14
|
-
};
|
|
6
|
+
@group(0) @binding(0) var<uniform> uView : ViewParams;
|
|
7
|
+
@group(0) @binding(1) var uSampler : sampler;
|
|
8
|
+
@group(0) @binding(2) var uTex : texture_2d<f32>;
|
|
15
9
|
|
|
16
|
-
struct
|
|
17
|
-
|
|
10
|
+
struct SpriteDesc {
|
|
11
|
+
uvOrigin : vec2<f32>,
|
|
12
|
+
uvSpan : vec2<f32>,
|
|
13
|
+
frameSize : vec2<f32>, // pixels
|
|
14
|
+
centerOffset : vec2<f32>, // pixels
|
|
18
15
|
};
|
|
19
16
|
|
|
20
|
-
@
|
|
21
|
-
@binding(1) @group(0) var myTexture: texture_2d<f32>;
|
|
22
|
-
@binding(2) @group(0) var mySampler: sampler;
|
|
23
|
-
@binding(3) @group(0) var<storage, read> sprites : SpritesBuffer;
|
|
24
|
-
@binding(4) @group(0) var emissiveTexture: texture_2d<f32>;
|
|
17
|
+
@group(0) @binding(3) var<storage, read> Sprites : array<SpriteDesc>;
|
|
25
18
|
|
|
26
19
|
|
|
27
|
-
struct
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
struct VSOut {
|
|
21
|
+
@builtin(position) pos : vec4<f32>,
|
|
22
|
+
@location(0) uv : vec2<f32>,
|
|
23
|
+
@location(1) tint : vec4<f32>,
|
|
24
|
+
@location(2) opacity : f32,
|
|
32
25
|
};
|
|
33
26
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@vertex
|
|
42
|
-
fn vs_main (@builtin(instance_index) i_id : u32,
|
|
43
|
-
@location(0) vertexPosition: vec3<f32>,
|
|
44
|
-
@location(1) vertexTexCoord: vec2<f32>) -> Fragment {
|
|
45
|
-
|
|
46
|
-
var output : Fragment;
|
|
47
|
-
|
|
48
|
-
var sx: f32 = sprites.models[i_id].scale.x;
|
|
49
|
-
var sy: f32 = sprites.models[i_id].scale.y;
|
|
50
|
-
var sz: f32 = 1.0;
|
|
51
|
-
|
|
52
|
-
var rot: f32 = sprites.models[i_id].rotation;
|
|
53
|
-
|
|
54
|
-
var tx: f32 = sprites.models[i_id].translate.x;
|
|
55
|
-
var ty: f32 = sprites.models[i_id].translate.y;
|
|
56
|
-
var tz: f32 = 0;
|
|
57
|
-
|
|
58
|
-
var s = sin(rot);
|
|
59
|
-
var c = cos(rot);
|
|
27
|
+
// corners for a unit-centered quad in strip order
|
|
28
|
+
const corners = array<vec2<f32>, 4>(
|
|
29
|
+
vec2<f32>(-0.5, -0.5),
|
|
30
|
+
vec2<f32>( 0.5, -0.5),
|
|
31
|
+
vec2<f32>(-0.5, 0.5),
|
|
32
|
+
vec2<f32>( 0.5, 0.5),
|
|
33
|
+
);
|
|
60
34
|
|
|
61
|
-
|
|
35
|
+
const uvBase = array<vec2<f32>, 4>(
|
|
36
|
+
vec2<f32>(0.0, 0.0),
|
|
37
|
+
vec2<f32>(1.0, 0.0),
|
|
38
|
+
vec2<f32>(0.0, 1.0),
|
|
39
|
+
vec2<f32>(1.0, 1.0),
|
|
40
|
+
);
|
|
62
41
|
|
|
63
|
-
var scaleM: mat4x4<f32> = mat4x4<f32>(sx, 0.0, 0.0, 0.0,
|
|
64
|
-
0.0, sy, 0.0, 0.0,
|
|
65
|
-
0.0, 0.0, sz, 0.0,
|
|
66
|
-
0, 0, 0, 1.0);
|
|
67
42
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
43
|
+
@vertex
|
|
44
|
+
fn vs_main(@builtin(vertex_index) vid : u32,
|
|
45
|
+
// per-instance attributes (locations 0..4)
|
|
46
|
+
@location(0) i_pos : vec2<f32>,
|
|
47
|
+
@location(1) i_size : vec2<f32>, // scales descriptor frame size (1,1 means use descriptor size)
|
|
48
|
+
@location(2) i_scale : vec2<f32>, // per-axis scale
|
|
49
|
+
@location(3) i_tint : vec4<f32>,
|
|
50
|
+
@location(4) i_spriteId : u32,
|
|
51
|
+
@location(5) i_opacity : f32,
|
|
52
|
+
@location(6) i_rotation : f32
|
|
53
|
+
) -> VSOut {
|
|
54
|
+
|
|
55
|
+
let rot = i_rotation;
|
|
56
|
+
let c = cos(rot);
|
|
57
|
+
let s = sin(rot);
|
|
58
|
+
|
|
59
|
+
let d = Sprites[i_spriteId];
|
|
60
|
+
let corner = corners[vid];
|
|
61
|
+
|
|
62
|
+
let sizePx = d.frameSize * i_size * i_scale; // per-axis scale applied on trimmed frame
|
|
63
|
+
var local = corner * sizePx;
|
|
64
|
+
local += d.centerOffset * i_scale; // compensate trimming // compensate trimming (so rotation is around original center)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
let rotated = vec2<f32>(local.x * c - local.y * s, local.x * s + local.y * c);
|
|
68
|
+
let world = vec4<f32>(rotated + i_pos, 0.0, 1.0);
|
|
69
|
+
|
|
70
|
+
var out : VSOut;
|
|
71
|
+
out.pos = uView.proj * uView.view * world;
|
|
72
|
+
out.uv = d.uvOrigin + d.uvSpan * uvBase[vid];
|
|
73
|
+
out.tint = i_tint;
|
|
74
|
+
out.opacity = i_opacity;
|
|
75
|
+
|
|
76
|
+
return out;
|
|
82
77
|
}
|
|
83
78
|
|
|
84
|
-
@fragment
|
|
85
|
-
fn fs_main (@location(0) TexCoord: vec2<f32>,
|
|
86
|
-
@location(1) Tint: vec4<f32>,
|
|
87
|
-
@location(2) Opacity: f32) -> GBufferOutput {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
var output : GBufferOutput;
|
|
91
79
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// the alpha channel in the emissive texture is used for emission strength
|
|
98
|
-
output.emissive = vec4(emissive.rgb, 1.0) * emissive.a;
|
|
99
|
-
|
|
100
|
-
//output.emissive = textureSample(emissiveTexture, mySampler, TexCoord) * EmissiveIntensity;
|
|
101
|
-
|
|
102
|
-
return output;
|
|
80
|
+
@fragment
|
|
81
|
+
fn fs_main(in : VSOut) -> @location(0) vec4<f32> {
|
|
82
|
+
let texel = textureSample(uTex, uSampler, in.uv);
|
|
83
|
+
return vec4<f32>(texel.rgb * (1.0 - in.tint.a) + (in.tint.rgb * in.tint.a), texel.a * in.opacity);
|
|
103
84
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import uuid from '../uuid.js'
|
|
2
|
+
import { vec2, vec4 } from 'wgpu-matrix'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// returns a unique identifier for the created sprite
|
|
6
|
+
export function addSprite (cobalt, renderPass, name, position, scale, tint, opacity, rotation) {
|
|
7
|
+
|
|
8
|
+
const { idByName } = renderPass.refs.spritesheet.data
|
|
9
|
+
|
|
10
|
+
renderPass.data.sprites.push({
|
|
11
|
+
position: vec2.clone(position),
|
|
12
|
+
sizeX: 1, sizeY: 1,
|
|
13
|
+
scale: vec2.clone(scale),
|
|
14
|
+
rotation,
|
|
15
|
+
opacity,
|
|
16
|
+
tint: vec4.clone(tint),
|
|
17
|
+
spriteID: idByName.get(name),
|
|
18
|
+
id: uuid(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return renderPass.data.sprites.at(-1).id
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
export function removeSprite (cobalt, renderPass, id) {
|
|
26
|
+
for (let i=0; i < renderPass.data.sprites.length; i++) {
|
|
27
|
+
if (renderPass.data.sprites[i].id === id) {
|
|
28
|
+
renderPass.data.sprites.splice(i, 1)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// remove all sprites
|
|
36
|
+
export function clear (cobalt, renderPass) {
|
|
37
|
+
renderPass.data.sprites.length = 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
export function setSpriteName (cobalt, renderPass, id, name) {
|
|
42
|
+
const sprite = renderPass.data.sprites.find((s) => s.id === id)
|
|
43
|
+
|
|
44
|
+
if (!sprite)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
const { idByName } = renderPass.refs.spritesheet.data
|
|
48
|
+
|
|
49
|
+
sprite.spriteID = idByName.get(name)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
export function setSpritePosition (cobalt, renderPass, id, position) {
|
|
54
|
+
const sprite = renderPass.data.sprites.find((s) => s.id === id)
|
|
55
|
+
if (!sprite)
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
vec2.copy(position, sprite.position)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
export function setSpriteTint (cobalt, renderPass, id, tint) {
|
|
63
|
+
const sprite = renderPass.data.sprites.find((s) => s.id === id)
|
|
64
|
+
if (!sprite)
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
vec4.copy(tint, sprite.tint)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
export function setSpriteOpacity (cobalt, renderPass, id, opacity) {
|
|
72
|
+
const sprite = renderPass.data.sprites.find((s) => s.id === id)
|
|
73
|
+
if (!sprite)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
sprite.opacity = opacity
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
export function setSpriteRotation (cobalt, renderPass, id, rotation) {
|
|
81
|
+
const sprite = renderPass.data.sprites.find((s) => s.id === id)
|
|
82
|
+
if (!sprite)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
sprite.rotation = rotation
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
export function setSpriteScale (cobalt, renderPass, id, scale) {
|
|
90
|
+
const sprite = renderPass.data.sprites.find((s) => s.id === id)
|
|
91
|
+
if (!sprite)
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
vec2.copy(scale, sprite.scale)
|
|
95
|
+
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import * as publicAPI from './public-api.js'
|
|
2
|
+
import spriteWGSL from './sprite.wgsl'
|
|
3
|
+
import round from 'round-half-up-symmetric'
|
|
4
|
+
import { mat4, vec3 } from 'wgpu-matrix'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// temporary variables, allocated once to avoid garbage collection
|
|
8
|
+
const _tmpVec3 = vec3.create(0, 0, 0)
|
|
9
|
+
|
|
10
|
+
// Packed instance layout: 48 bytes (aligned for vec4 fetch)
|
|
11
|
+
const INSTANCE_STRIDE = 64;
|
|
12
|
+
|
|
13
|
+
// Offsets inside one instance (bytes)
|
|
14
|
+
const OFF_POS = 0; // float32x2 (8B)
|
|
15
|
+
const OFF_SIZE = 8; // float32x2 (8B)
|
|
16
|
+
const OFF_SCALE = 16; // float32x2 (8B)
|
|
17
|
+
const OFF_TINT = 24; // float32x4 (16B)
|
|
18
|
+
const OFF_SPRITEID = 40; // uint32 (4B)
|
|
19
|
+
const OFF_OPACITY = 44; // float32 (4B)
|
|
20
|
+
const OFF_ROT = 48; // float32 (4B)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
type: "cobalt:spriteHDR",
|
|
25
|
+
refs: [
|
|
26
|
+
{ name: "spritesheet", type: "customResource", access: "read" },
|
|
27
|
+
{
|
|
28
|
+
name: "color",
|
|
29
|
+
type: "textureView",
|
|
30
|
+
format: "rgba16float",
|
|
31
|
+
access: "write",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "emissive",
|
|
35
|
+
type: "textureView",
|
|
36
|
+
format: "rgba16float",
|
|
37
|
+
access: "write",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
|
|
41
|
+
// cobalt event handling functions
|
|
42
|
+
|
|
43
|
+
// @params Object cobalt renderer world object
|
|
44
|
+
// @params Object options optional data passed when initing this node
|
|
45
|
+
onInit: async function (cobalt, options = {}) {
|
|
46
|
+
return init(cobalt, options);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
onRun: function (cobalt, node, webGpuCommandEncoder) {
|
|
50
|
+
// do whatever you need for this node. webgpu renderpasses, etc.
|
|
51
|
+
draw(cobalt, node, webGpuCommandEncoder);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Clean up GPU resources. Most WebGPU objects are GC-managed and don't
|
|
55
|
+
// expose destroy(); buffers/textures/query-sets do.
|
|
56
|
+
onDestroy: function (cobalt, node) {
|
|
57
|
+
// Explicitly destroy GPU resources that have a destroy() method
|
|
58
|
+
try { node.data.instanceBuf?.destroy(); } catch {}
|
|
59
|
+
try { node.data.spriteBuf?.destroy(); } catch {}
|
|
60
|
+
try { node.data.uniformBuffer?.destroy(); } catch {}
|
|
61
|
+
|
|
62
|
+
// These do not have destroy(); drop references to let GC reclaim
|
|
63
|
+
node.data.pipeline = null; // GPURenderPipeline
|
|
64
|
+
node.data.bindGroup = null; // GPUBindGroup
|
|
65
|
+
node.data.bindGroupLayout = null;// GPUBindGroupLayout
|
|
66
|
+
|
|
67
|
+
// CPU-side allocations
|
|
68
|
+
node.data.instanceStaging = null;
|
|
69
|
+
node.data.instanceView = null;
|
|
70
|
+
node.data.sprites.length = 0;
|
|
71
|
+
node.data.visible.length = 0;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
onResize: function (cobalt, node) {
|
|
75
|
+
_writeSpriteBuffer(cobalt, node)
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
onViewportPosition: function (cobalt, node) {
|
|
79
|
+
_writeSpriteBuffer(cobalt, node)
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// optional
|
|
83
|
+
customFunctions: {
|
|
84
|
+
...publicAPI,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
async function init(cobalt, nodeData) {
|
|
89
|
+
const { device } = cobalt;
|
|
90
|
+
|
|
91
|
+
const { descs, names } = nodeData.refs.spritesheet.data.spritesheet
|
|
92
|
+
|
|
93
|
+
const uniformBuffer = device.createBuffer({
|
|
94
|
+
size: 64 * 2, // 4x4 matrix with 4 bytes per float32, times 2 matrices (view, projection)
|
|
95
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Pack into std430-like struct (4*float*? + vec2 + vec2 → 32 bytes). We'll just write tightly as 8 floats.
|
|
99
|
+
const BYTES_PER_DESC = 8 * 4; // 8 float32s
|
|
100
|
+
const buf = new ArrayBuffer(BYTES_PER_DESC * descs.length);
|
|
101
|
+
const f32 = new Float32Array(buf);
|
|
102
|
+
for (let i=0;i<descs.length;i++){
|
|
103
|
+
const d = descs[i];
|
|
104
|
+
const base = i * 8;
|
|
105
|
+
f32[base+0] = d.UvOrigin[0];
|
|
106
|
+
f32[base+1] = d.UvOrigin[1];
|
|
107
|
+
f32[base+2] = d.UvSpan[0];
|
|
108
|
+
f32[base+3] = d.UvSpan[1];
|
|
109
|
+
f32[base+4] = d.FrameSize[0];
|
|
110
|
+
f32[base+5] = d.FrameSize[1];
|
|
111
|
+
f32[base+6] = d.CenterOffset[0];
|
|
112
|
+
f32[base+7] = d.CenterOffset[1];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// create buffer for sprite uv lookup
|
|
116
|
+
const spriteBuf = device.createBuffer({
|
|
117
|
+
label: "spriteHDR desc table",
|
|
118
|
+
size: Math.max(16, buf.byteLength),
|
|
119
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
device.queue.writeBuffer(spriteBuf, 0, buf);
|
|
123
|
+
|
|
124
|
+
// --- Instance buffer (growable) ---
|
|
125
|
+
const instanceCap = 1024;
|
|
126
|
+
const instanceBuf = device.createBuffer({
|
|
127
|
+
label: "spriteHDR instances",
|
|
128
|
+
size: INSTANCE_STRIDE * instanceCap,
|
|
129
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
130
|
+
});
|
|
131
|
+
const instanceStaging = new ArrayBuffer(INSTANCE_STRIDE * instanceCap);
|
|
132
|
+
const instanceView = new DataView(instanceStaging);
|
|
133
|
+
|
|
134
|
+
// --- Pipeline ---
|
|
135
|
+
const shader = device.createShaderModule({ code: spriteWGSL });
|
|
136
|
+
const bgl = device.createBindGroupLayout({
|
|
137
|
+
entries: [
|
|
138
|
+
{
|
|
139
|
+
binding: 0,
|
|
140
|
+
visibility: GPUShaderStage.VERTEX,
|
|
141
|
+
buffer: { type: "uniform" },
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
binding: 1,
|
|
145
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
146
|
+
sampler: { type: "filtering" },
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
binding: 2,
|
|
150
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
151
|
+
texture: { sampleType: "float" },
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
binding: 3,
|
|
155
|
+
visibility: GPUShaderStage.VERTEX,
|
|
156
|
+
buffer: { type: "read-only-storage" },
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
binding: 4,
|
|
160
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
161
|
+
texture: { sampleType: "float" },
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
],
|
|
165
|
+
});
|
|
166
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
167
|
+
bindGroupLayouts: [bgl],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const instLayout = {
|
|
171
|
+
arrayStride: INSTANCE_STRIDE,
|
|
172
|
+
stepMode: "instance",
|
|
173
|
+
attributes: [
|
|
174
|
+
{ shaderLocation: 0, offset: OFF_POS, format: "float32x2" },
|
|
175
|
+
{ shaderLocation: 1, offset: OFF_SIZE, format: "float32x2" },
|
|
176
|
+
{ shaderLocation: 2, offset: OFF_SCALE, format: "float32x2" },
|
|
177
|
+
{ shaderLocation: 3, offset: OFF_TINT, format: "float32x4" },
|
|
178
|
+
|
|
179
|
+
{ shaderLocation: 4, offset: OFF_SPRITEID, format: "uint32" },
|
|
180
|
+
{ shaderLocation: 5, offset: OFF_OPACITY, format: "float32" },
|
|
181
|
+
{ shaderLocation: 6, offset: OFF_ROT, format: "float32" },
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const pipeline = device.createRenderPipeline({
|
|
186
|
+
layout: pipelineLayout,
|
|
187
|
+
vertex: {
|
|
188
|
+
module: shader,
|
|
189
|
+
entryPoint: "vs_main",
|
|
190
|
+
buffers: [instLayout],
|
|
191
|
+
},
|
|
192
|
+
fragment: {
|
|
193
|
+
module: shader,
|
|
194
|
+
entryPoint: "fs_main",
|
|
195
|
+
targets: [
|
|
196
|
+
// color
|
|
197
|
+
{
|
|
198
|
+
format: 'rgba16float',
|
|
199
|
+
blend: {
|
|
200
|
+
color: {
|
|
201
|
+
srcFactor: 'src-alpha',
|
|
202
|
+
dstFactor: 'one-minus-src-alpha',
|
|
203
|
+
},
|
|
204
|
+
alpha: {
|
|
205
|
+
srcFactor: 'zero',
|
|
206
|
+
dstFactor: 'one'
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
// emissive
|
|
212
|
+
{
|
|
213
|
+
format: 'rgba16float',
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
primitive: { topology: "triangle-strip", cullMode: "none" },
|
|
219
|
+
multisample: { count: 1 },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const bindGroupLayout = bgl;
|
|
223
|
+
|
|
224
|
+
const bindGroup = device.createBindGroup({
|
|
225
|
+
layout: bgl,
|
|
226
|
+
entries: [
|
|
227
|
+
// Uniform buffer (view + proj matrices)
|
|
228
|
+
{ binding: 0, resource: { buffer: uniformBuffer } },
|
|
229
|
+
|
|
230
|
+
{ binding: 1, resource: nodeData.refs.spritesheet.data.colorTexture.sampler },
|
|
231
|
+
{ binding: 2, resource: nodeData.refs.spritesheet.data.colorTexture.view },
|
|
232
|
+
{ binding: 3, resource: { buffer: spriteBuf } },
|
|
233
|
+
{ binding: 4, resource: nodeData.refs.spritesheet.data.emissiveTexture.view },
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
sprites: [ ],
|
|
239
|
+
visible: [ ],
|
|
240
|
+
visibleCount: 0,
|
|
241
|
+
viewRect: { x: 0, y: 0, w: 0, h: 0 },
|
|
242
|
+
|
|
243
|
+
spriteBuf,
|
|
244
|
+
uniformBuffer,
|
|
245
|
+
|
|
246
|
+
instanceCap,
|
|
247
|
+
instanceView,
|
|
248
|
+
instanceBuf,
|
|
249
|
+
instanceStaging,
|
|
250
|
+
|
|
251
|
+
pipeline,
|
|
252
|
+
bindGroup,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
function ensureCapacity (cobalt, node, nInstances) {
|
|
258
|
+
|
|
259
|
+
const { instanceCap } = node.data
|
|
260
|
+
|
|
261
|
+
if (nInstances <= instanceCap)
|
|
262
|
+
return;
|
|
263
|
+
|
|
264
|
+
let newCap = instanceCap
|
|
265
|
+
if (newCap === 0)
|
|
266
|
+
newCap = 1024;
|
|
267
|
+
|
|
268
|
+
while (newCap < nInstances)
|
|
269
|
+
newCap *= 2;
|
|
270
|
+
|
|
271
|
+
node.data.instanceBuf.destroy();
|
|
272
|
+
node.data.instanceBuf = cobalt.device.createBuffer({
|
|
273
|
+
size: INSTANCE_STRIDE * newCap,
|
|
274
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
node.data.instanceStaging = new ArrayBuffer(INSTANCE_STRIDE * newCap);
|
|
278
|
+
node.data.instanceView = new DataView(node.data.instanceStaging);
|
|
279
|
+
node.data.instanceCap = newCap;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
function draw (cobalt, node, commandEncoder) {
|
|
284
|
+
|
|
285
|
+
const { device, context } = cobalt;
|
|
286
|
+
|
|
287
|
+
const { instanceView, instanceBuf, instanceStaging, pipeline, bindGroup } = node.data
|
|
288
|
+
|
|
289
|
+
const { descs } = node.refs.spritesheet.data.spritesheet
|
|
290
|
+
|
|
291
|
+
const viewRect = node.data.viewRect
|
|
292
|
+
|
|
293
|
+
viewRect.x = cobalt.viewport.position[0]
|
|
294
|
+
viewRect.y = cobalt.viewport.position[1]
|
|
295
|
+
viewRect.w = cobalt.viewport.width
|
|
296
|
+
viewRect.h = cobalt.viewport.height
|
|
297
|
+
|
|
298
|
+
node.data.visibleCount = 0
|
|
299
|
+
|
|
300
|
+
for (const s of node.data.sprites) {
|
|
301
|
+
const d = descs[s.spriteID];
|
|
302
|
+
if (!d)
|
|
303
|
+
continue;
|
|
304
|
+
|
|
305
|
+
// avoid sprite viewport culling when drawing in screenspace mode (typically ui/hud layers)
|
|
306
|
+
if (!node.options.isScreenSpace) {
|
|
307
|
+
const sx = (d.FrameSize[0] * (s.sizeX) * s.scale[0]) * 0.5;
|
|
308
|
+
const sy = (d.FrameSize[1] * (s.sizeY) * s.scale[1]) * 0.5;
|
|
309
|
+
const rad = Math.hypot(sx, sy);
|
|
310
|
+
const x = s.position[0], y = s.position[1];
|
|
311
|
+
if (x + rad < viewRect.x || x - rad > viewRect.x + viewRect.w || y + rad < viewRect.y || y - rad > viewRect.y + viewRect.h)
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
node.data.visible[node.data.visibleCount] = s
|
|
316
|
+
node.data.visibleCount++
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
ensureCapacity(cobalt, node, node.data.visibleCount)
|
|
320
|
+
|
|
321
|
+
// Pack instances into staging buffer
|
|
322
|
+
for (let i=0; i < node.data.visibleCount; i++){
|
|
323
|
+
const base = i * INSTANCE_STRIDE;
|
|
324
|
+
const s = node.data.visible[i];
|
|
325
|
+
const tint = s.tint;
|
|
326
|
+
|
|
327
|
+
instanceView.setFloat32(base + OFF_POS + 0, s.position[0], true);
|
|
328
|
+
instanceView.setFloat32(base + OFF_POS + 4, s.position[1], true);
|
|
329
|
+
|
|
330
|
+
instanceView.setFloat32(base + OFF_SIZE + 0, s.sizeX, true);
|
|
331
|
+
instanceView.setFloat32(base + OFF_SIZE + 4, s.sizeY, true);
|
|
332
|
+
|
|
333
|
+
instanceView.setFloat32(base + OFF_SCALE + 0, s.scale[0], true);
|
|
334
|
+
instanceView.setFloat32(base + OFF_SCALE + 4, s.scale[1], true);
|
|
335
|
+
|
|
336
|
+
instanceView.setFloat32(base + OFF_TINT + 0, tint[0], true);
|
|
337
|
+
instanceView.setFloat32(base + OFF_TINT + 4, tint[1], true);
|
|
338
|
+
instanceView.setFloat32(base + OFF_TINT + 8, tint[2], true);
|
|
339
|
+
instanceView.setFloat32(base + OFF_TINT + 12, tint[3], true);
|
|
340
|
+
|
|
341
|
+
instanceView.setUint32(base + OFF_SPRITEID, s.spriteID >>> 0, true);
|
|
342
|
+
|
|
343
|
+
instanceView.setFloat32(base + OFF_OPACITY, s.opacity, true);
|
|
344
|
+
|
|
345
|
+
instanceView.setFloat32(base + OFF_ROT, s.rotation, true);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
device.queue.writeBuffer(instanceBuf, 0, instanceStaging, 0, node.data.visibleCount * INSTANCE_STRIDE);
|
|
349
|
+
|
|
350
|
+
const loadOp = node.options.loadOp || 'load'
|
|
351
|
+
|
|
352
|
+
const pass = commandEncoder.beginRenderPass({
|
|
353
|
+
label: "spriteHDR renderpass",
|
|
354
|
+
colorAttachments: [
|
|
355
|
+
// color
|
|
356
|
+
{
|
|
357
|
+
view: node.refs.color.data.view,
|
|
358
|
+
clearValue: cobalt.clearValue,
|
|
359
|
+
loadOp: loadOp,
|
|
360
|
+
storeOp: 'store',
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
// emissive
|
|
364
|
+
{
|
|
365
|
+
view: node.refs.emissive.data.view,
|
|
366
|
+
clearValue: cobalt.clearValue,
|
|
367
|
+
loadOp: 'clear',
|
|
368
|
+
storeOp: 'store'
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
],
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
pass.setPipeline(pipeline);
|
|
375
|
+
pass.setBindGroup(0, bindGroup);
|
|
376
|
+
pass.setVertexBuffer(0, instanceBuf);
|
|
377
|
+
if (node.data.visibleCount)
|
|
378
|
+
pass.draw(4, node.data.visibleCount, 0, 0); // triangle strip, 4 verts per instance
|
|
379
|
+
pass.end();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
function _writeSpriteBuffer (cobalt, node) {
|
|
384
|
+
|
|
385
|
+
const { device, viewport } = cobalt
|
|
386
|
+
|
|
387
|
+
const GAME_WIDTH = viewport.width / viewport.zoom
|
|
388
|
+
const GAME_HEIGHT = viewport.height / viewport.zoom
|
|
389
|
+
|
|
390
|
+
// left right bottom top near far
|
|
391
|
+
const projection = mat4.ortho(0, GAME_WIDTH, GAME_HEIGHT, 0, -10.0, 10.0)
|
|
392
|
+
|
|
393
|
+
// set 3d camera position
|
|
394
|
+
if (!!node.options.isScreenSpace) {
|
|
395
|
+
vec3.set(0, 0, 0, _tmpVec3)
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
// TODO: if this doesn't introduce jitter into the crossroads render, remove this disabled code entirely.
|
|
399
|
+
//
|
|
400
|
+
// I'm disabling the rounding because I think it fails in cases where units are not expressed in pixels
|
|
401
|
+
// e.g., most physics engines operate on meters, not pixels, so we don't want to round to the nearest integer as that
|
|
402
|
+
// probably isn't high enough resolution. That would mean the camera could be snapped by up to 0.5 meters
|
|
403
|
+
// in that case. I think the better solution for expressing camera position in pixels is to round before calling
|
|
404
|
+
// cobalt.setViewportPosition(...)
|
|
405
|
+
//
|
|
406
|
+
vec3.set(-round(viewport.position[0]), -round(viewport.position[1]), 0, _tmpVec3)
|
|
407
|
+
//vec3.set(-viewport.position[0], -viewport.position[1], 0, _tmpVec3)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const view = mat4.translation(_tmpVec3)
|
|
411
|
+
|
|
412
|
+
device.queue.writeBuffer(node.data.uniformBuffer, 0, view.buffer)
|
|
413
|
+
device.queue.writeBuffer(node.data.uniformBuffer, 64, projection.buffer)
|
|
414
|
+
}
|