@solidrt/2d 0.0.70 → 0.0.72
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/AGENTS.md +222 -40
- package/README.md +7 -3
- package/examples/README.md +1 -1
- package/examples/camera-probe.tsx +4 -4
- package/examples/markers.tsx +3 -3
- package/package.json +5 -4
- package/src/animation.ts +26 -5
- package/src/atlas.ts +8 -4
- package/src/camera.ts +2 -2
- package/src/components/camera2d.tsx +2 -2
- package/src/components/group.tsx +15 -3
- package/src/components/shots.tsx +2 -2
- package/src/components/sprite-layer.tsx +40 -12
- package/src/components/sprite.tsx +31 -3
- package/src/components/text2d.tsx +2 -2
- package/src/components/tile-layer.tsx +21 -7
- package/src/components/view2d.tsx +2 -2
- package/src/dispatch.ts +10 -6
- package/src/frames.ts +1 -1
- package/src/glsl.ts +399 -0
- package/src/index.ts +14 -6
- package/src/layer.ts +260 -113
- package/src/material.ts +368 -0
- package/src/records.ts +198 -104
- package/src/style.ts +98 -0
- package/src/text.ts +64 -11
- package/src/tiles.ts +119 -24
- package/src/views.ts +17 -9
- package/src/shaders.ts +0 -323
package/src/glsl.ts
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// The sprite GLSL and the instance layouts: what a layer's material
|
|
2
|
+
// compiles over, and what an app composes its own stages from
|
|
3
|
+
// (`@solidrt/2d/glsl`, the twin of `@solidrt/3d/glsl`).
|
|
4
|
+
//
|
|
5
|
+
// The VERTEX side is one stage for every layer kind: a unit quad
|
|
6
|
+
// instanced over the layer's records, mapped world -> clip through
|
|
7
|
+
// uCamera (offset + zoom), uCameraRot ([cos, sin, pivotX, pivotY]: the
|
|
8
|
+
// camera rotation about the pivot, identity [1, 0, 0, 0]) and uViewport -
|
|
9
|
+
// the projectCamera mapping (camera.ts) spelled in GLSL; the two must
|
|
10
|
+
// agree. World and clip space are both y-down (core gpu.ts pixel
|
|
11
|
+
// contract), so the mapping carries NO flip anywhere - do not add one.
|
|
12
|
+
// The stage reads its attributes BY NAME from whichever instance buffers
|
|
13
|
+
// the layer binds, which is why the two record layouts below share
|
|
14
|
+
// names: the records layer and the tile layer bind the 20-float
|
|
15
|
+
// RECORD_ATTRIBUTES, the node layer binds the core-written
|
|
16
|
+
// POSE_ATTRIBUTES (slot 0, the Pose2D record [x, y, angle, sx, sy]) and
|
|
17
|
+
// the JS-written SPRITE_ATTRIBUTES (slot 1); a material's own instance
|
|
18
|
+
// buffers follow. iRenderOrder is read by no stage - it exists for the
|
|
19
|
+
// core's instanceOrder (orderBy: "renderOrder"); a declared attribute
|
|
20
|
+
// without an active program attribute is skipped, its bytes pad the
|
|
21
|
+
// stride.
|
|
22
|
+
//
|
|
23
|
+
// The FRAGMENT side is GENERATED per layer (spriteSource): one sampler
|
|
24
|
+
// per declared atlas, the record's flat atlas index picking it - the
|
|
25
|
+
// multi-texture batch of PixiJS and Phaser, where Unity and Godot break
|
|
26
|
+
// the batch on every texture change - with a distance-field decode in
|
|
27
|
+
// the branch of every atlas declared `sdf` (Atlas.sdf). The set fills a
|
|
28
|
+
// `Sprite` struct, the contract every look is written against: the
|
|
29
|
+
// stock fragment (unlitFragment) is a surface slot over it, a custom
|
|
30
|
+
// fragment (shaderMaterialClass) a main of its own over the same set.
|
|
31
|
+
// The layer prepends the set to whatever fragment the material carries,
|
|
32
|
+
// so a fragment never names a sampler or branches on an atlas itself.
|
|
33
|
+
//
|
|
34
|
+
// `outline` is rgb plus a width in WORLD pixels; the vertex stage turns
|
|
35
|
+
// the width into screen pixels through the camera zoom and the
|
|
36
|
+
// screen-size clamp, so an outline scales with its glyph. A colour atlas
|
|
37
|
+
// ignores it. The vertex stage applies the screen-size clamp
|
|
38
|
+
// (SpriteOptions.minScreenPx/maxScreenPx) through core's screenSizeScale:
|
|
39
|
+
// the quad scales uniformly so its smaller axis stays within the bounds
|
|
40
|
+
// at the camera's zoom (the pixels per world unit), and picking applies
|
|
41
|
+
// the JS twin to the rect it tests. The rotations here (clockwise,
|
|
42
|
+
// y-down) and their JS partners must agree: iRot with pointInSprite in
|
|
43
|
+
// pick.ts, uCameraRot with projectCamera in camera.ts. The differential
|
|
44
|
+
// checks guard the JS side against oracles but NOT against these
|
|
45
|
+
// shaders - if you touch one rotation, touch all.
|
|
46
|
+
import { glsl, SCREEN_SIZE_GLSL, VERTEX_FORMATS } from "@solidrt/core/gpu"
|
|
47
|
+
import type { TextureBindings, TextureId, VertexAttribute, VertexFormat } from "@solidrt/core/gpu"
|
|
48
|
+
|
|
49
|
+
/** What the fragment set reads of a declared atlas: whether it is a
|
|
50
|
+
* distance field, and its range (see `Atlas.sdf`). An Atlas record is
|
|
51
|
+
* one. */
|
|
52
|
+
export type AtlasShading = { sdf?: { range: number } }
|
|
53
|
+
|
|
54
|
+
/** The core-written pose record of the node layer (slot 0): the Pose2D
|
|
55
|
+
* projection [x, y, angle, sx, sy]. */
|
|
56
|
+
export const POSE_ATTRIBUTES: VertexAttribute[] = [
|
|
57
|
+
{ name: "iPos", format: "float32x2" },
|
|
58
|
+
{ name: "iRot", format: "float32" },
|
|
59
|
+
{ name: "iScale", format: "float32x2" },
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
/** The JS-written sprite record of the node layer (slot 1, 16 floats):
|
|
63
|
+
* [u0, v0, u1, v1, tintR, tintG, tintB, tintA, renderOrder, minScreenPx,
|
|
64
|
+
* maxScreenPx, atlas, outlineR, outlineG, outlineB, outlineWidth]. */
|
|
65
|
+
export const SPRITE_ATTRIBUTES: VertexAttribute[] = [
|
|
66
|
+
{ name: "iUv", format: "float32x4" },
|
|
67
|
+
{ name: "iTint", format: "float32x4" },
|
|
68
|
+
{ name: "iRenderOrder", format: "float32" },
|
|
69
|
+
{ name: "iScreenPx", format: "float32x2" },
|
|
70
|
+
{ name: "iAtlas", format: "float32" },
|
|
71
|
+
{ name: "iOutline", format: "float32x4" },
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
/** The records layer's and the tile layer's one record (20 floats):
|
|
75
|
+
* [cx, cy, w, h, u0, v0, u1, v1, rot, tintR, tintG, tintB, tintA,
|
|
76
|
+
* minScreenPx, maxScreenPx, atlas, outlineR, outlineG, outlineB,
|
|
77
|
+
* outlineWidth]. */
|
|
78
|
+
export const RECORD_ATTRIBUTES: VertexAttribute[] = [
|
|
79
|
+
{ name: "iPos", format: "float32x2" },
|
|
80
|
+
{ name: "iScale", format: "float32x2" },
|
|
81
|
+
{ name: "iUv", format: "float32x4" },
|
|
82
|
+
{ name: "iRot", format: "float32" },
|
|
83
|
+
{ name: "iTint", format: "float32x4" },
|
|
84
|
+
{ name: "iScreenPx", format: "float32x2" },
|
|
85
|
+
{ name: "iAtlas", format: "float32" },
|
|
86
|
+
{ name: "iOutline", format: "float32x4" },
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
/** The attribute names the layers' own records and the unit quad take:
|
|
90
|
+
* what a material's instance buffers may not reuse. */
|
|
91
|
+
export const LAYER_ATTRIBUTE_NAMES: readonly string[] = ["aPos", ...[...POSE_ATTRIBUTES, ...SPRITE_ATTRIBUTES, ...RECORD_ATTRIBUTES].map(a => a.name)]
|
|
92
|
+
|
|
93
|
+
/** The uniforms every 2d vertex stage must declare and use: the camera
|
|
94
|
+
* mapping is theirs (see SPRITE_VERTEX_BODY), and a stage without them
|
|
95
|
+
* cannot place a sprite. shaderMaterialClass checks for them. */
|
|
96
|
+
export const VERTEX_UNIFORMS: readonly string[] = ["uCamera", "uCameraRot", "uViewport"]
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The varyings the fragment set reads and the uniforms the mapping
|
|
100
|
+
* needs: what a custom vertex stage declares beside its own `in`s.
|
|
101
|
+
* vUv is the sample uv, vFrame the frame's UV rect (flat), vTint the
|
|
102
|
+
* sprite's tint, vAtlas the sampler index (flat), vOutlineColor and
|
|
103
|
+
* vOutlinePx the outline a distance-field atlas draws (flat; write
|
|
104
|
+
* zeros when the stage has no outline).
|
|
105
|
+
*/
|
|
106
|
+
export const SPRITE_VARYINGS = glsl`
|
|
107
|
+
out vec2 vUv;
|
|
108
|
+
flat out vec4 vFrame;
|
|
109
|
+
out vec4 vTint;
|
|
110
|
+
flat out int vAtlas;
|
|
111
|
+
flat out vec3 vOutlineColor;
|
|
112
|
+
flat out float vOutlinePx;
|
|
113
|
+
uniform vec2 uViewport;
|
|
114
|
+
uniform vec4 uCamera;
|
|
115
|
+
uniform vec4 uCameraRot;
|
|
116
|
+
`
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The world -> clip mapping and the varying writes, the tail of every
|
|
120
|
+
* vertex stage: expects `corner` (the quad corner in world pixels,
|
|
121
|
+
* after the screen-size clamp), `center` (the sprite's world position),
|
|
122
|
+
* `rot` (its rotation) and `clampScale` (the clamp's scale, 1 without
|
|
123
|
+
* one) as locals, and the record's iUv, iTint, iAtlas and iOutline as
|
|
124
|
+
* `in`s. Compose it after your own placement.
|
|
125
|
+
*/
|
|
126
|
+
export const SPRITE_VERTEX_BODY = glsl`
|
|
127
|
+
float c = cos(rot), s = sin(rot);
|
|
128
|
+
vec2 world = center + vec2(corner.x * c - corner.y * s, corner.x * s + corner.y * c);
|
|
129
|
+
vec2 view = (world - uCamera.xy) * uCamera.zw;
|
|
130
|
+
vec2 screen = uCameraRot.zw + vec2(view.x * uCameraRot.x - view.y * uCameraRot.y, view.x * uCameraRot.y + view.y * uCameraRot.x);
|
|
131
|
+
// World and clip are both y-down, so the mapping carries no flip.
|
|
132
|
+
gl_Position = vec4(screen / uViewport * 2.0 - 1.0, 0.0, 1.0);
|
|
133
|
+
vUv = mix(iUv.xy, iUv.zw, aPos + 0.5);
|
|
134
|
+
vFrame = iUv;
|
|
135
|
+
vTint = iTint;
|
|
136
|
+
vAtlas = int(iAtlas);
|
|
137
|
+
vOutlineColor = iOutline.rgb;
|
|
138
|
+
// World pixels to screen pixels: the camera zoom, and the clamp the
|
|
139
|
+
// quad itself scaled by.
|
|
140
|
+
vOutlinePx = iOutline.a * uCamera.z * clampScale;
|
|
141
|
+
`
|
|
142
|
+
|
|
143
|
+
/** The GLSL type a vertex format feeds (its kind and component count):
|
|
144
|
+
* `float`/`vec2`/`vec3`/`vec4` for the float and normalized formats,
|
|
145
|
+
* `uint`/`uvec*` for uint*, `int`/`ivec*` for sint*. */
|
|
146
|
+
export function attributeType(format: VertexFormat): string {
|
|
147
|
+
let codec = VERTEX_FORMATS[format]
|
|
148
|
+
let prefix = codec.kind === "float" ? "" : codec.kind === "uint" ? "u" : "i"
|
|
149
|
+
if (codec.components === 1) return codec.kind === "float" ? "float" : codec.kind === "uint" ? "uint" : "int"
|
|
150
|
+
return `${prefix}vec${codec.components}`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The varying the stock vertex stage forwards an instance attribute
|
|
154
|
+
* as: `iFoo` becomes `vFoo` (flat, the record's exact value). An
|
|
155
|
+
* attribute not named `i` plus a capital has no forwarded name and
|
|
156
|
+
* throws - the layer's own records follow the same convention. */
|
|
157
|
+
export function forwardedName(name: string): string {
|
|
158
|
+
if (!/^i[A-Z]/.test(name)) throw new Error(`an instance attribute the stock stage forwards is named iSomething (forwarded as vSomething), got '${name}'`)
|
|
159
|
+
return "v" + name.slice(1)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The stock vertex stage, for every layer kind: the record's pose
|
|
164
|
+
* (iPos, iRot, iScale - the core's pose record or the records layer's
|
|
165
|
+
* fields, by name) placing the unit quad under the screen-size clamp,
|
|
166
|
+
* then SPRITE_VERTEX_BODY. `forwards` are a material's own instance
|
|
167
|
+
* attributes (its instanceBuffers'), each declared as an `in` and
|
|
168
|
+
* forwarded unchanged as a flat varying (forwardedName: `iPalette` ->
|
|
169
|
+
* `vPalette`), which is how the stock material's `surface` reads a
|
|
170
|
+
* per-sprite style record. The tier-1 custom look pairs its own stage
|
|
171
|
+
* with unlitFragment(); a wobble, a wind sway or a billboard-style
|
|
172
|
+
* effect is this stage with its placement changed.
|
|
173
|
+
*/
|
|
174
|
+
export function unlitVertex(forwards: readonly VertexAttribute[] = []): string {
|
|
175
|
+
let ins = ""
|
|
176
|
+
let outs = ""
|
|
177
|
+
let writes = ""
|
|
178
|
+
for (let attr of forwards) {
|
|
179
|
+
let type = attributeType(attr.format)
|
|
180
|
+
let varying = forwardedName(attr.name)
|
|
181
|
+
ins += `in ${type} ${attr.name};\n `
|
|
182
|
+
outs += `flat out ${type} ${varying};\n `
|
|
183
|
+
writes += `${varying} = ${attr.name};\n `
|
|
184
|
+
}
|
|
185
|
+
return glsl`
|
|
186
|
+
in vec2 aPos;
|
|
187
|
+
in vec2 iPos;
|
|
188
|
+
in float iRot;
|
|
189
|
+
in vec2 iScale;
|
|
190
|
+
in vec4 iUv;
|
|
191
|
+
in vec4 iTint;
|
|
192
|
+
in vec2 iScreenPx;
|
|
193
|
+
in float iAtlas;
|
|
194
|
+
in vec4 iOutline;
|
|
195
|
+
${ins}${outs}${SPRITE_VARYINGS}
|
|
196
|
+
${SCREEN_SIZE_GLSL}
|
|
197
|
+
void main() {
|
|
198
|
+
float clampScale = screenSizeScale(iScale, uCamera.z, iScreenPx);
|
|
199
|
+
vec2 corner = aPos * iScale * clampScale;
|
|
200
|
+
vec2 center = iPos;
|
|
201
|
+
float rot = iRot;
|
|
202
|
+
${writes}${SPRITE_VERTEX_BODY}
|
|
203
|
+
}
|
|
204
|
+
`
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The stock vertex stage with nothing forwarded: unlitVertex(). */
|
|
208
|
+
export const SPRITE_VERTEX = unlitVertex()
|
|
209
|
+
|
|
210
|
+
/** The sampler uniform of atlas `i` in a layer's program. */
|
|
211
|
+
export function atlasUniform(i: number): string {
|
|
212
|
+
return `uAtlas${i}`
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The texture bindings of a layer's atlases, by sampler uniform: what
|
|
216
|
+
* every target drawing with the layer's pipeline binds (a material's own
|
|
217
|
+
* textures merge over them). */
|
|
218
|
+
export function atlasBindings(atlases: readonly TextureId[]): TextureBindings {
|
|
219
|
+
let out: TextureBindings = {}
|
|
220
|
+
atlases.forEach((texture, i) => (out[atlasUniform(i)] = texture))
|
|
221
|
+
return out
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The fragment SET of a layer over its atlases - what the layer prepends
|
|
226
|
+
* to its material's fragment, so every look is written over it:
|
|
227
|
+
*
|
|
228
|
+
* - the varyings (SPRITE_VARYINGS' `in` side), one `uniform sampler2D
|
|
229
|
+
* uAtlasN` per atlas, and `uniform vec4 uTint`, the LAYER tint
|
|
230
|
+
* (multiplied over the per-instance vTint by the stock fragment;
|
|
231
|
+
* identity [1, 1, 1, 1] - GLSL uniforms default to ZERO, so every
|
|
232
|
+
* target creation pins it explicitly or everything renders
|
|
233
|
+
* transparent black);
|
|
234
|
+
* - `struct Sprite { vec4 color; vec2 uv; vec4 frame; vec4 tint; int
|
|
235
|
+
* atlas; }`: the contract. `color` is the stock result for the
|
|
236
|
+
* fragment, premultiplied, before the layer tint: the atlas texel
|
|
237
|
+
* times the instance tint for a colour atlas, the decoded field (the
|
|
238
|
+
* tint as the fill, the outline under it) for a distance-field one;
|
|
239
|
+
* `uv` the sample uv, `frame` the frame's UV rect, `tint` the
|
|
240
|
+
* instance tint, `atlas` the sampler index;
|
|
241
|
+
* - `Sprite spriteOf()`: fills the struct for this fragment. Takes the
|
|
242
|
+
* uv derivatives first, in main's uniform control flow, so the mip
|
|
243
|
+
* footprint is the quad's own and the lookup inside the per-instance
|
|
244
|
+
* branch is well defined (an implicit-derivative texture() in
|
|
245
|
+
* non-uniform control flow is not); a custom main calls it first;
|
|
246
|
+
* - `vec4 spriteSample(vec2 uv)`: the stock result at another uv of the
|
|
247
|
+
* fragment's own atlas - the pick, the frame clamp, the field decode,
|
|
248
|
+
* the tint - for scrolling and distortion. The clamp into the frame
|
|
249
|
+
* still applies, so a wrap is `fract` in frame space:
|
|
250
|
+
* `spriteSample(mix(s.frame.xy, s.frame.zw, fract(t)))`.
|
|
251
|
+
*
|
|
252
|
+
* The sample is clamped to its frame, pulled in by half a texel of ITS
|
|
253
|
+
* atlas: a quad at a fractional position lands edge samples so close to
|
|
254
|
+
* the frame's edge that a linear tap (and float interpolation under
|
|
255
|
+
* nearest) reaches the cell next door, painting a one-texel line of the
|
|
256
|
+
* neighbour along the sprite's edge. The clamp keeps the quad's texel
|
|
257
|
+
* mapping 1:1 and only moves those edge samples onto the edge texel, so
|
|
258
|
+
* touching cells do not bleed and no gutter is needed at mip level 0.
|
|
259
|
+
* It cannot help a mip chain, whose texels straddle the cell edge before
|
|
260
|
+
* sampling - that is extrude.ts. min/max because a flipped frame stores
|
|
261
|
+
* its UVs swapped, and the inner min/max because a sub-texel frame would
|
|
262
|
+
* otherwise hand clamp() a lower bound above its upper one. The tap is
|
|
263
|
+
* textureGrad over the derivatives spriteOf took, so a clamped uv (a
|
|
264
|
+
* zero derivative along the clamped axis, which would pick a sharper
|
|
265
|
+
* level along every edge) never decides the level.
|
|
266
|
+
*
|
|
267
|
+
* A distance-field atlas yields the instance tint as the fill,
|
|
268
|
+
* anti-aliased over one screen pixel: the field's range mapped through
|
|
269
|
+
* the quad's texel footprint gives the screen pixels per field unit
|
|
270
|
+
* (floored at 1, so a glyph far below its texel size fades instead of
|
|
271
|
+
* vanishing), the median of rgb is the edge, and when the instance
|
|
272
|
+
* carries an outline the true field in alpha, pushed out by the
|
|
273
|
+
* outline's screen pixels, paints the outline colour under the fill.
|
|
274
|
+
*/
|
|
275
|
+
export function spriteSource(atlases: readonly AtlasShading[]): string {
|
|
276
|
+
if (!Array.isArray(atlases) || atlases.length === 0) throw new Error(`spriteSource: atlases must be a non-empty list, got ${JSON.stringify(atlases)}`)
|
|
277
|
+
let samplers = ""
|
|
278
|
+
atlases.forEach((_, i) => (samplers += `uniform sampler2D ${atlasUniform(i)};\n `))
|
|
279
|
+
let shade = (i: number) => {
|
|
280
|
+
let sdf = atlases[i]!.sdf
|
|
281
|
+
if (sdf === undefined) return `spriteTap(${atlasUniform(i)}, uv) * vTint`
|
|
282
|
+
if (!(typeof sdf.range === "number" && sdf.range > 0 && Number.isFinite(sdf.range))) {
|
|
283
|
+
throw new Error(`spriteSource: atlases[${i}].sdf.range must be a positive number, got ${sdf.range}`)
|
|
284
|
+
}
|
|
285
|
+
return `spriteField(${atlasUniform(i)}, ${sdf.range.toFixed(2)}, uv)`
|
|
286
|
+
}
|
|
287
|
+
let cases = ""
|
|
288
|
+
atlases.forEach((_, i) => (cases += `case ${i}: return ${shade(i)};\n `))
|
|
289
|
+
let pick =
|
|
290
|
+
atlases.length === 1
|
|
291
|
+
? `return ${shade(0)};`
|
|
292
|
+
: `switch (vAtlas) {
|
|
293
|
+
${cases}default: return ${shade(0)};
|
|
294
|
+
}`
|
|
295
|
+
return glsl`
|
|
296
|
+
in vec2 vUv;
|
|
297
|
+
flat in vec4 vFrame;
|
|
298
|
+
in vec4 vTint;
|
|
299
|
+
flat in int vAtlas;
|
|
300
|
+
flat in vec3 vOutlineColor;
|
|
301
|
+
flat in float vOutlinePx;
|
|
302
|
+
${samplers}uniform vec4 uTint;
|
|
303
|
+
|
|
304
|
+
struct Sprite {
|
|
305
|
+
vec4 color;
|
|
306
|
+
vec2 uv;
|
|
307
|
+
vec4 frame;
|
|
308
|
+
vec4 tint;
|
|
309
|
+
int atlas;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// The quad's uv footprint per screen pixel, taken once by spriteOf in
|
|
313
|
+
// uniform control flow; every tap of this fragment uses it.
|
|
314
|
+
vec2 spriteDx;
|
|
315
|
+
vec2 spriteDy;
|
|
316
|
+
|
|
317
|
+
vec4 spriteTap(sampler2D atlas, vec2 uv) {
|
|
318
|
+
vec2 halfTexel = 0.5 / vec2(textureSize(atlas, 0));
|
|
319
|
+
vec2 lo = min(vFrame.xy, vFrame.zw) + halfTexel;
|
|
320
|
+
vec2 hi = max(vFrame.xy, vFrame.zw) - halfTexel;
|
|
321
|
+
return textureGrad(atlas, clamp(uv, min(lo, hi), max(lo, hi)), spriteDx, spriteDy);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
float spriteMedian3(vec3 v) {
|
|
325
|
+
return max(min(v.r, v.g), min(max(v.r, v.g), v.b));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Screen pixels per field unit (0..1 spans the range): the range in
|
|
329
|
+
// texels over the quad's texel footprint per screen pixel, floored at
|
|
330
|
+
// one so a minified glyph fades rather than aliasing away.
|
|
331
|
+
float spriteScreenPxRange(sampler2D atlas, float range) {
|
|
332
|
+
vec2 unitRange = vec2(range) / vec2(textureSize(atlas, 0));
|
|
333
|
+
vec2 screenTexSize = vec2(1.0) / (abs(spriteDx) + abs(spriteDy));
|
|
334
|
+
return max(0.5 * dot(unitRange, screenTexSize), 1.0);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
vec4 spriteField(sampler2D atlas, float range, vec2 uv) {
|
|
338
|
+
vec4 t = spriteTap(atlas, uv);
|
|
339
|
+
float px = spriteScreenPxRange(atlas, range);
|
|
340
|
+
float fill = clamp((spriteMedian3(t.rgb) - 0.5) * px + 0.5, 0.0, 1.0);
|
|
341
|
+
vec3 rgb = vTint.rgb;
|
|
342
|
+
float coverage = fill;
|
|
343
|
+
if (vOutlinePx > 0.0) {
|
|
344
|
+
float outline = clamp((t.a - 0.5) * px + vOutlinePx + 0.5, 0.0, 1.0);
|
|
345
|
+
coverage = max(fill, outline);
|
|
346
|
+
rgb = mix(vOutlineColor, vTint.rgb, fill);
|
|
347
|
+
}
|
|
348
|
+
float a = coverage * vTint.a;
|
|
349
|
+
return vec4(rgb * a, a);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
vec4 spriteSample(vec2 uv) {
|
|
353
|
+
${pick}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
Sprite spriteOf() {
|
|
357
|
+
spriteDx = dFdx(vUv);
|
|
358
|
+
spriteDy = dFdy(vUv);
|
|
359
|
+
return Sprite(spriteSample(vUv), vUv, vFrame, vTint, vAtlas);
|
|
360
|
+
}
|
|
361
|
+
`
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** The options of the stock fragment: the tier-2 slot. */
|
|
365
|
+
export type UnlitSourceOptions = {
|
|
366
|
+
/** GLSL at file scope, after the layer's set and before main:
|
|
367
|
+
* uniforms (each a layer param - `params` on the layer or
|
|
368
|
+
* `layer.setParams` - or a material texture) and helper functions. */
|
|
369
|
+
prelude?: string
|
|
370
|
+
/** GLSL declaring `void surface(inout Sprite s)`, called with the
|
|
371
|
+
* struct filled (spriteOf) and before the layer tint: rewrite
|
|
372
|
+
* `s.color` or `discard`. */
|
|
373
|
+
surface?: string
|
|
374
|
+
/** The instance attributes the stock vertex stage forwards
|
|
375
|
+
* (unlitVertex's `forwards`): declared here as the matching flat
|
|
376
|
+
* varyings, so the prelude and the surface read `vFoo` for a record's
|
|
377
|
+
* `iFoo`. */
|
|
378
|
+
forwards?: readonly VertexAttribute[]
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* The stock fragment over the layer's set (spriteSource, which the layer
|
|
383
|
+
* prepends): the sprite's stock colour, through the surface slot when
|
|
384
|
+
* one is given, times the layer tint. `unlit()` compiles exactly this;
|
|
385
|
+
* the tier-1 custom look pairs it with a vertex stage of its own.
|
|
386
|
+
*/
|
|
387
|
+
export function unlitFragment(o: UnlitSourceOptions = {}): string {
|
|
388
|
+
let ins = ""
|
|
389
|
+
for (let attr of o.forwards ?? []) ins += `flat in ${attributeType(attr.format)} ${forwardedName(attr.name)};\n `
|
|
390
|
+
return glsl`
|
|
391
|
+
${ins}${o.prelude ?? ""}
|
|
392
|
+
${o.surface ?? ""}
|
|
393
|
+
void main() {
|
|
394
|
+
Sprite s = spriteOf();
|
|
395
|
+
${o.surface ? "surface(s);" : ""}
|
|
396
|
+
fragColor = s.color * uTint;
|
|
397
|
+
}
|
|
398
|
+
`
|
|
399
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,21 +9,29 @@
|
|
|
9
9
|
// node whose Pose2D record sink writes the pose instance buffer at the
|
|
10
10
|
// core flush, so core producers reach sprites and picking walks the core
|
|
11
11
|
// BVH; style stays a JS-written second instance buffer. The records layer (createRecordLayer)
|
|
12
|
-
// is the raw escape hatch for motion only JS can compute:
|
|
12
|
+
// is the raw escape hatch for motion only JS can compute: 20 JS-owned
|
|
13
13
|
// floats per sprite in a mirror, published by dirty range through the
|
|
14
14
|
// 3d record mesh's verbs (records/updateRecords/setRecordCount). The baked
|
|
15
15
|
// tile layer (createTileLayer/TileLayer) is the static sibling: a tile
|
|
16
16
|
// world rendered once into textures and composited as a few quads,
|
|
17
|
-
// re-baked on change.
|
|
18
|
-
//
|
|
17
|
+
// re-baked on change. What any layer's pixels look like is its MATERIAL
|
|
18
|
+
// (material.ts): the stock `unlit()` with its blend and its
|
|
19
|
+
// prelude/surface slot, or a shaderMaterialClass over the layer's GLSL
|
|
20
|
+
// set (`@solidrt/2d/glsl`), with per-sprite style records in any vertex
|
|
21
|
+
// format - the same three tiers and the same words as @solidrt/3d, the
|
|
22
|
+
// material sitting on the layer because the layer is the batch. Two
|
|
23
|
+
// faces throughout: the imperative core (usable without Solid
|
|
24
|
+
// components) and the components (SpriteLayer/Sprite/Group/
|
|
19
25
|
// View2d/TileLayer) on top. Sheets reach the GPU through createAtlas,
|
|
20
26
|
// sliced by grid/namedFrames/fullFrame, and a mipmapped sheet is extruded
|
|
21
27
|
// first (extrudeGrid/extrudeRects). See AGENTS.md for the model and the
|
|
22
28
|
// traps.
|
|
23
29
|
|
|
24
|
-
export { addGroup, addSprite, createSpriteLayer, getSprite, destroyGroup, destroySprite, setGroup, setGroupTransition, setSprite, setSpriteParent, setSpriteTransition, worldPosition, POSE_FLOATS,
|
|
25
|
-
export { createRecordLayer, records, setRecordCount, updateRecords, INSTANCE_FLOATS } from "./records.ts"
|
|
30
|
+
export { addGroup, addSprite, createSpriteLayer, getSprite, destroyGroup, destroySprite, setGroup, setGroupTimeScale, setGroupTransition, setSprite, setSpriteParent, setSpriteTimeScale, setSpriteTransition, timeRate, worldPosition, POSE_FLOATS, SPRITE_FLOATS } from "./layer.ts"
|
|
31
|
+
export { createRecordLayer, instanceAttribute, records, setInstanceStyle, setRecordCount, updateRecords, INSTANCE_FLOATS } from "./records.ts"
|
|
26
32
|
export type { RecordLayer as RecordLayerHandle, RecordLayerOptions, UpdateRecordsOptions } from "./records.ts"
|
|
33
|
+
export { shaderMaterial, shaderMaterialClass, unlit } from "./material.ts"
|
|
34
|
+
export type { Material, ShaderMaterialClass, ShaderMaterialClassOptions, ShaderMaterialInstanceOptions, ShaderMaterialOptions, UnlitOptions } from "./material.ts"
|
|
27
35
|
export { floorReach, pointInSprite } from "./pick.ts"
|
|
28
36
|
export { projectCamera, unprojectCamera } from "./camera.ts"
|
|
29
37
|
export type { CameraState, CameraUpdate } from "./camera.ts"
|
|
@@ -70,7 +78,7 @@ export { moveAndSlide } from "./collision.ts"
|
|
|
70
78
|
export type { MoveLayer } from "./collision.ts"
|
|
71
79
|
export type { ViewHandle, ViewOptions } from "./views.ts"
|
|
72
80
|
export { createTileLayer } from "./tiles.ts"
|
|
73
|
-
export type { TileChunk, TileLayer as TileLayerHandle, TileLayerOptions } from "./tiles.ts"
|
|
81
|
+
export type { TileCellOptions, TileChunk, TileLayer as TileLayerHandle, TileLayerOptions } from "./tiles.ts"
|
|
74
82
|
|
|
75
83
|
export type { NodeMotionSpec, NodeTransitionSpec } from "flux:spatial"
|
|
76
84
|
export { fullFrame, grid, isFrame, namedFrames } from "./frames.ts"
|