@solidrt/2d 0.0.71 → 0.0.73

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/src/shaders.ts DELETED
@@ -1,323 +0,0 @@
1
- // The sprite pipelines: a unit quad instanced over a layer's atlases,
2
- // mapped world -> clip through uCamera (offset + zoom), uCameraRot ([cos,
3
- // sin, pivotX, pivotY]: the camera rotation about the pivot, identity
4
- // [1, 0, 0, 0]) and uViewport - the projectCamera mapping (camera.ts)
5
- // spelled in GLSL; the two must agree. World and clip space are both
6
- // y-down (core gpu.ts pixel contract), so the mapping carries NO flip
7
- // anywhere - do not add one. Two record layouts share the fragment stage:
8
- // - VERTEX + INSTANCE_ATTRIBUTES: the 20-float interleaved record
9
- // [cx, cy, w, h, u0, v0, u1, v1, rot, tintR, tintG, tintB, tintA,
10
- // minScreenPx, maxScreenPx, atlas, outlineR, outlineG, outlineB,
11
- // outlineWidth], used by the records layer (records.ts) and the tile
12
- // layer (tiles.ts).
13
- // - VERTEX_SPLIT + INSTANCE_LAYOUTS_SPLIT: the node-backed live layer
14
- // (layer.ts), pose and style in separate instance-buffer slots - slot 0
15
- // is the core-written Pose2D record [x, y, angle, sx, sy], slot 1 the
16
- // JS-written style record [u0, v0, u1, v1, tintR, tintG, tintB, tintA,
17
- // renderOrder, minScreenPx, maxScreenPx, atlas, outlineR, outlineG,
18
- // outlineB, outlineWidth]. The shader never reads iRenderOrder - it
19
- // exists for the core's instanceOrder (orderBy: "renderOrder");
20
- // declared attributes without an active program attribute are skipped,
21
- // their bytes pad the stride.
22
- // `atlas` is the sampler index of the record's texture in the layer's
23
- // atlas list: the fragment stage is GENERATED per layer with one sampler
24
- // per declared atlas (fragmentFor), the flat per-instance index picking
25
- // the sampler - the multi-texture batch of PixiJS and Phaser, where Unity
26
- // and Godot break the batch on every texture change. Any number of sheets
27
- // interleave in one draw, key order included. An atlas declared as a
28
- // distance field (`Atlas.sdf`) is decoded in its branch instead of
29
- // sampled as colour: the median of rgb is the edge, anti-aliased over one
30
- // screen pixel from the quad's own texel footprint (the msdfgen reference
31
- // shader), the true field in alpha grows the per-instance outline.
32
- // `outline` is rgb plus a width in WORLD pixels; the vertex stage turns
33
- // the width into screen pixels through the camera zoom and the screen-size
34
- // clamp, so an outline scales with its glyph. A colour atlas ignores it.
35
- // Both vertex stages apply the screen-size clamp (SpriteOptions.
36
- // minScreenPx/maxScreenPx) through core's screenSizeScale: the quad
37
- // scales uniformly so its smaller axis stays within the bounds at the
38
- // camera's zoom (the pixels per world unit), and picking applies the JS
39
- // twin to the rect it tests.
40
- // The rotations here (clockwise, y-down) and their JS partners must
41
- // agree: iRot with pointInSprite in pick.ts, uCameraRot with
42
- // projectCamera in camera.ts. The differential checks guard the JS side
43
- // against oracles but NOT against these shaders - if you touch one
44
- // rotation, touch all.
45
- import { compileShader, createBuffer, createRenderPipeline, destroyBuffer, destroyProgram, destroyRenderPipeline, destroyShader, glsl, linkProgram, SCREEN_SIZE_GLSL } from "@solidrt/core/gpu"
46
- import type { BlendMode, BufferId, RenderPipelineId, TextureBindings, TextureId, VertexAttribute } from "@solidrt/core/gpu"
47
-
48
- /** What the fragment stage reads of a declared atlas: whether it is a
49
- * distance field, and its range (see `Atlas.sdf`). */
50
- export type AtlasShading = { sdf?: { range: number } }
51
-
52
- // The world -> clip mapping and the varyings both vertex stages share;
53
- // `corner` is the quad corner after the screen-size clamp, `center` the
54
- // sprite's world position.
55
- const VERTEX_BODY = glsl`
56
- float c = cos(rot), s = sin(rot);
57
- vec2 world = center + vec2(corner.x * c - corner.y * s, corner.x * s + corner.y * c);
58
- vec2 view = (world - uCamera.xy) * uCamera.zw;
59
- vec2 screen = uCameraRot.zw + vec2(view.x * uCameraRot.x - view.y * uCameraRot.y, view.x * uCameraRot.y + view.y * uCameraRot.x);
60
- // World and clip are both y-down, so the mapping carries no flip.
61
- gl_Position = vec4(screen / uViewport * 2.0 - 1.0, 0.0, 1.0);
62
- vUv = mix(iUv.xy, iUv.zw, aPos + 0.5);
63
- vFrame = iUv;
64
- vTint = iTint;
65
- vAtlas = int(iAtlas);
66
- vOutlineColor = iOutline.rgb;
67
- // World pixels to screen pixels: the camera zoom, and the clamp the
68
- // quad itself scaled by.
69
- vOutlinePx = iOutline.a * uCamera.z * clampScale;
70
- `
71
-
72
- const VARYINGS = glsl`
73
- out vec2 vUv;
74
- flat out vec4 vFrame;
75
- out vec4 vTint;
76
- flat out int vAtlas;
77
- flat out vec3 vOutlineColor;
78
- flat out float vOutlinePx;
79
- uniform vec2 uViewport;
80
- uniform vec4 uCamera;
81
- uniform vec4 uCameraRot;
82
- `
83
-
84
- export let VERTEX = glsl`
85
- in vec2 aPos;
86
- in vec2 iCenter;
87
- in vec2 iSize;
88
- in vec4 iUv;
89
- in float iRot;
90
- in vec4 iTint;
91
- in vec2 iScreenPx;
92
- in float iAtlas;
93
- in vec4 iOutline;
94
- ${VARYINGS}
95
- ${SCREEN_SIZE_GLSL}
96
- void main() {
97
- float clampScale = screenSizeScale(iSize, uCamera.z, iScreenPx);
98
- vec2 corner = aPos * iSize * clampScale;
99
- vec2 center = iCenter;
100
- float rot = iRot;
101
- ${VERTEX_BODY}
102
- }
103
- `
104
-
105
- /** The sampler uniform of atlas `i` in a layer's pipeline. */
106
- export function atlasUniform(i: number): string {
107
- return `uAtlas${i}`
108
- }
109
-
110
- /** The texture bindings of a layer's atlases, by sampler uniform: what
111
- * every target drawing with the layer's pipeline binds. */
112
- export function atlasBindings(atlases: readonly TextureId[]): TextureBindings {
113
- let out: TextureBindings = {}
114
- atlases.forEach((texture, i) => (out[atlasUniform(i)] = texture))
115
- return out
116
- }
117
-
118
- /**
119
- * The fragment stage of a layer over its atlases: one sampler per atlas,
120
- * the record's atlas index (flat, so one per instance) picking it, and a
121
- * distance-field decode in the branch of every atlas declared `sdf`.
122
- *
123
- * uTint is the LAYER tint, multiplied over the per-instance vTint
124
- * (identity [1, 1, 1, 1] - GLSL uniforms default to ZERO, so every target
125
- * creation must pin it explicitly or everything renders transparent
126
- * black).
127
- *
128
- * The sample is clamped to its frame, pulled in by half a texel of ITS
129
- * atlas: a quad at a fractional position lands edge samples so close to
130
- * the frame's edge that a linear tap (and float interpolation under
131
- * nearest) reaches the cell next door, painting a one-texel line of the
132
- * neighbour along the sprite's edge. The clamp keeps the quad's texel
133
- * mapping 1:1 and only moves those edge samples onto the edge texel, so
134
- * touching cells do not bleed and no gutter is needed at mip level 0. It
135
- * cannot help a mip chain, whose texels straddle the cell edge before
136
- * sampling - that is extrude.ts. min/max because a flipped frame stores
137
- * its UVs swapped, and the inner min/max because a sub-texel frame would
138
- * otherwise hand clamp() a lower bound above its upper one.
139
- *
140
- * The derivatives are taken from the UNCLAMPED uv before any branch and
141
- * the tap is textureGrad: the mip footprint is the quad's own (a clamped
142
- * uv has a zero derivative along the clamped axis, which would pick a
143
- * sharper level along every edge), and the lookup is well defined inside
144
- * the per-instance branch (an implicit-derivative texture() in
145
- * non-uniform control flow is not, which the PixiJS-style batch lives
146
- * with). A single-atlas layer skips the branch and keeps the same tap.
147
- *
148
- * A colour atlas yields the texel times the instance tint (premultiplied
149
- * in, premultiplied out). A distance-field atlas yields the instance
150
- * tint as the fill, anti-aliased over one screen pixel: the field's range
151
- * mapped through the quad's texel footprint gives the screen pixels per
152
- * field unit (floored at 1, so a glyph far below its texel size fades
153
- * instead of vanishing), the median of rgb is the edge, and when the
154
- * instance carries an outline the true field in alpha, pushed out by the
155
- * outline's screen pixels, paints the outline colour under the fill.
156
- */
157
- export function fragmentFor(atlases: readonly AtlasShading[]): string {
158
- if (!Array.isArray(atlases) || atlases.length === 0) throw new Error(`fragmentFor: atlases must be a non-empty list, got ${JSON.stringify(atlases)}`)
159
- let samplers = ""
160
- atlases.forEach((_, i) => (samplers += `uniform sampler2D ${atlasUniform(i)};\n `))
161
- let shade = (i: number) => {
162
- let sdf = atlases[i]!.sdf
163
- if (sdf === undefined) return `tap(${atlasUniform(i)}, dx, dy) * vTint`
164
- if (!(typeof sdf.range === "number" && sdf.range > 0 && Number.isFinite(sdf.range))) {
165
- throw new Error(`fragmentFor: atlases[${i}].sdf.range must be a positive number, got ${sdf.range}`)
166
- }
167
- return `field(${atlasUniform(i)}, ${sdf.range.toFixed(2)}, dx, dy)`
168
- }
169
- let cases = ""
170
- atlases.forEach((_, i) => (cases += `case ${i}: color = ${shade(i)}; break;\n `))
171
- let pick =
172
- atlases.length === 1
173
- ? `vec4 color = ${shade(0)};`
174
- : `vec4 color;
175
- switch (vAtlas) {
176
- ${cases}default: color = ${shade(0)}; break;
177
- }`
178
- return glsl`
179
- in vec2 vUv;
180
- flat in vec4 vFrame;
181
- in vec4 vTint;
182
- flat in int vAtlas;
183
- flat in vec3 vOutlineColor;
184
- flat in float vOutlinePx;
185
- ${samplers}uniform vec4 uTint;
186
-
187
- vec4 tap(sampler2D atlas, vec2 dx, vec2 dy) {
188
- vec2 halfTexel = 0.5 / vec2(textureSize(atlas, 0));
189
- vec2 lo = min(vFrame.xy, vFrame.zw) + halfTexel;
190
- vec2 hi = max(vFrame.xy, vFrame.zw) - halfTexel;
191
- return textureGrad(atlas, clamp(vUv, min(lo, hi), max(lo, hi)), dx, dy);
192
- }
193
-
194
- float median3(vec3 v) {
195
- return max(min(v.r, v.g), min(max(v.r, v.g), v.b));
196
- }
197
-
198
- // Screen pixels per field unit (0..1 spans the range): the range in
199
- // texels over the quad's texel footprint per screen pixel, floored at
200
- // one so a minified glyph fades rather than aliasing away.
201
- float screenPxRange(sampler2D atlas, float range, vec2 dx, vec2 dy) {
202
- vec2 unitRange = vec2(range) / vec2(textureSize(atlas, 0));
203
- vec2 screenTexSize = vec2(1.0) / (abs(dx) + abs(dy));
204
- return max(0.5 * dot(unitRange, screenTexSize), 1.0);
205
- }
206
-
207
- vec4 field(sampler2D atlas, float range, vec2 dx, vec2 dy) {
208
- vec4 t = tap(atlas, dx, dy);
209
- float px = screenPxRange(atlas, range, dx, dy);
210
- float fill = clamp((median3(t.rgb) - 0.5) * px + 0.5, 0.0, 1.0);
211
- vec3 rgb = vTint.rgb;
212
- float coverage = fill;
213
- if (vOutlinePx > 0.0) {
214
- float outline = clamp((t.a - 0.5) * px + vOutlinePx + 0.5, 0.0, 1.0);
215
- coverage = max(fill, outline);
216
- rgb = mix(vOutlineColor, vTint.rgb, fill);
217
- }
218
- float a = coverage * vTint.a;
219
- return vec4(rgb * a, a);
220
- }
221
-
222
- void main() {
223
- vec2 dx = dFdx(vUv);
224
- vec2 dy = dFdy(vUv);
225
- ${pick}
226
- fragColor = color * uTint;
227
- }
228
- `
229
- }
230
-
231
- /** The instance attribute list matching the 20-float record layout. */
232
- export const INSTANCE_ATTRIBUTES: VertexAttribute[] = [
233
- { name: "iCenter", format: "float32x2" },
234
- { name: "iSize", format: "float32x2" },
235
- { name: "iUv", format: "float32x4" },
236
- { name: "iRot", format: "float32" },
237
- { name: "iTint", format: "float32x4" },
238
- { name: "iScreenPx", format: "float32x2" },
239
- { name: "iAtlas", format: "float32" },
240
- { name: "iOutline", format: "float32x4" },
241
- ]
242
-
243
- export let VERTEX_SPLIT = glsl`
244
- in vec2 aPos;
245
- in vec2 iPos;
246
- in float iRot;
247
- in vec2 iScale;
248
- in vec4 iUv;
249
- in vec4 iTint;
250
- in vec2 iScreenPx;
251
- in float iAtlas;
252
- in vec4 iOutline;
253
- ${VARYINGS}
254
- ${SCREEN_SIZE_GLSL}
255
- void main() {
256
- float clampScale = screenSizeScale(iScale, uCamera.z, iScreenPx);
257
- vec2 corner = aPos * iScale * clampScale;
258
- vec2 center = iPos;
259
- float rot = iRot;
260
- ${VERTEX_BODY}
261
- }
262
- `
263
-
264
- /** The split layout: one instance buffer holding the Pose2D record, a
265
- * second holding the style record (iRenderOrder is layout-only - see the
266
- * header note). */
267
- export const INSTANCE_LAYOUTS_SPLIT: VertexAttribute[][] = [
268
- [
269
- { name: "iPos", format: "float32x2" },
270
- { name: "iRot", format: "float32" },
271
- { name: "iScale", format: "float32x2" },
272
- ],
273
- [
274
- { name: "iUv", format: "float32x4" },
275
- { name: "iTint", format: "float32x4" },
276
- { name: "iRenderOrder", format: "float32" },
277
- { name: "iScreenPx", format: "float32x2" },
278
- { name: "iAtlas", format: "float32" },
279
- { name: "iOutline", format: "float32x4" },
280
- ],
281
- ]
282
-
283
- /** What a layer's targets draw with: the unit quad every instance reuses
284
- * and the pipeline over one of the two layouts above. */
285
- export type SpritePipeline = { quad: BufferId; pipeline: RenderPipelineId; dispose(): void }
286
-
287
- /**
288
- * Compile one sprite pipeline: `vertex` with its matching attribute list
289
- * (VERTEX + INSTANCE_ATTRIBUTES, or the split pair) over the fragment
290
- * stage generated for `atlases` (count and distance-field decodes),
291
- * triangle strips blended with `blend` (the layer's `blend` option,
292
- * "alpha" by default). Spelled out (not the fused createPipelineTexture)
293
- * so a layer's targets - its views, a tile layer's chunks - share the one
294
- * compile. The program lives as long as the pipeline; `dispose` frees
295
- * both and the quad.
296
- */
297
- export function createSpritePipeline(label: string, vertex: string, instanceLayouts: VertexAttribute[][], blend: BlendMode, atlases: readonly AtlasShading[]): SpritePipeline {
298
- // One unit quad (triangle strip), reused by every instance.
299
- let quad = createBuffer(new Float32Array([-0.5, -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5]), {
300
- label: `${label}-quad`,
301
- autoFree: false,
302
- })
303
- let vs = compileShader("vertex", vertex, { header: true })
304
- let fs = compileShader("fragment", fragmentFor(atlases), { header: true })
305
- let program = linkProgram(vs, fs, { label })
306
- destroyShader(vs)
307
- destroyShader(fs)
308
- let pipeline = createRenderPipeline(program, {
309
- label,
310
- topology: "triangle-strip",
311
- buffers: [{ attributes: [{ name: "aPos", format: "float32x2" }] }, ...instanceLayouts.map(attributes => ({ stepMode: "instance" as const, attributes }))],
312
- blend,
313
- })
314
- return {
315
- quad,
316
- pipeline,
317
- dispose() {
318
- destroyRenderPipeline(pipeline)
319
- destroyProgram(program)
320
- destroyBuffer(quad)
321
- },
322
- }
323
- }