@driftengine/core 4.13.1 → 4.13.2
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.json +31 -0
- package/README.md +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/render/backend/api.d.ts +63 -8
- package/dist/render/backend/webgl2/pointSkinFeedback.d.ts +23 -0
- package/dist/render/backend/webgl2/pointSkinFeedback.js +159 -0
- package/dist/render/backend/webgl2/renderer.d.ts +21 -0
- package/dist/render/backend/webgl2/renderer.js +41 -2
- package/dist/render/backend/webgpu/buffers.d.ts +10 -2
- package/dist/render/backend/webgpu/buffers.js +66 -16
- package/dist/render/backend/webgpu/clothTextures.d.ts +6 -3
- package/dist/render/backend/webgpu/clothTextures.js +14 -11
- package/dist/render/backend/webgpu/drawCommand.js +3 -3
- package/dist/render/backend/webgpu/gpuClothSet.js +1 -0
- package/dist/render/backend/webgpu/groupShelf.d.ts +31 -0
- package/dist/render/backend/webgpu/groupShelf.js +54 -0
- package/dist/render/backend/webgpu/pointSkinPass.d.ts +27 -0
- package/dist/render/backend/webgpu/pointSkinPass.js +104 -0
- package/dist/render/backend/webgpu/renderer.d.ts +31 -7
- package/dist/render/backend/webgpu/renderer.js +118 -47
- package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +7 -2
- package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.d.ts +13 -0
- package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.js +57 -0
- package/dist/render/backend/webgpu/vertexFormats.js +1 -1
- package/dist/render/lightBudget.d.ts +6 -0
- package/dist/render/lightBudget.js +6 -0
- package/dist/render/mesh.d.ts +19 -5
- package/dist/render/mesh.js +39 -8
- package/dist/render/pointSkin.d.ts +55 -0
- package/dist/render/pointSkin.js +121 -0
- package/dist/render/rendererHarness.js +6 -0
- package/dist/render/shader.d.ts +3 -1
- package/dist/render/shader.js +6 -1
- package/dist/render/vertexPacking.d.ts +16 -4
- package/dist/render/vertexPacking.js +10 -3
- package/package.json +6 -6
- package/src/index.ts +2 -0
- package/src/render/backend/api.ts +70 -7
- package/src/render/backend/webgl2/pointSkinFeedback.ts +173 -0
- package/src/render/backend/webgl2/renderer.ts +63 -2
- package/src/render/backend/webgpu/buffers.ts +90 -16
- package/src/render/backend/webgpu/clothTextures.ts +14 -11
- package/src/render/backend/webgpu/drawCommand.ts +3 -3
- package/src/render/backend/webgpu/gpuClothSet.ts +1 -0
- package/src/render/backend/webgpu/groupShelf.ts +74 -0
- package/src/render/backend/webgpu/pointSkinPass.ts +115 -0
- package/src/render/backend/webgpu/renderer.ts +150 -82
- package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +7 -2
- package/src/render/backend/webgpu/shaders/pointSkin.wgsl.ts +58 -0
- package/src/render/backend/webgpu/vertexFormats.ts +1 -1
- package/src/render/lightBudget.ts +7 -0
- package/src/render/mesh.ts +49 -7
- package/src/render/pointSkin.ts +178 -0
- package/src/render/rendererHarness.ts +6 -0
- package/src/render/shader.ts +5 -0
- package/src/render/vertexPacking.ts +25 -5
package/CHANGELOG.json
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "4.13.2",
|
|
4
|
+
"date": "2026-10-11",
|
|
5
|
+
"entries": [
|
|
6
|
+
{
|
|
7
|
+
"kind": "added",
|
|
8
|
+
"short": "A mesh stored as weighted points in its joints' own spaces is posed on the device, so the main thread hands over the joints and nothing else: `createPointSkin` and `posePointSkin`.",
|
|
9
|
+
"text": "`createPointSkin(mesh, data)` and `posePointSkin(skin, matrices)` pose a dynamic mesh on the device from a format some models are stored in: each vertex up to four points, each in the space of the joint it follows, with a weight each, and a normal in the space of its first joint (`PointSkinData`). Posed, a vertex is the weighted sum of each point moved by its joint, which ordinary skinning cannot express, since it moves one bind-pose position by a blend of joints; such a rig was posed on the main thread and its positions uploaded every frame, measured by a consumer at 38% of the main thread's drawing time. WebGPU poses in a compute pass and WebGL2 by transform feedback, writing the mesh's own positions and normals, so every pass draws it as any dynamic mesh, its shadow and its motion included, and a skinned vertex stage that has no attribute left for four more points is untouched. The joints are each joint's transform into the mesh's model space, sixteen floats a joint with no inverse bind. The mesh's bounds become a box around each joint's reach, scaled by the joint's largest axis and by the most a vertex's weights sum to, so it is culled as it moves without the posed vertices coming back to the CPU. Measured on an RX 9070 XT (`demo/dev/pointSkin.html`): the device's pose draws the picture the same pose computed on the CPU draws, to the pixel on WebGPU and within 6 pixels on WebGL2, and differs from the rest pose by 70,792; under reconstruction the two part by 743 pixels a level deep, the rounding of single against double precision, where a motion fault parts them by 43,629."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"kind": "added",
|
|
13
|
+
"short": "A mesh's colours can be rewritten in place: a mesh made `{ dynamicColors: true }` takes `updateMeshColors`, so ground shaded again every frame is no longer made again.",
|
|
14
|
+
"text": "`updateMeshColors(mesh, colors)` rewrites a mesh's colours, three linear floats a vertex, on a mesh created with `{ dynamicColors: true }`, which keeps them as floats in a buffer of their own. Nothing else moves: not the positions, not the bounds, and no motion vector. `updateMesh` rewrites positions and normals only, so a surface whose colour changes while it stays where it is, such as ground darkened with its distance from a moving camera, was made again as a new mesh nearly every frame: a consumer measured 295 of 301 frames measured, a fifth of the drawing's main-thread time. The option is independent of `dynamic`, and costs twelve bytes a vertex where sixteen-bit colours cost six or eight and one colour every vertex shares costs nothing. On WebGPU the colours are the mesh's last vertex buffer, so every other buffer stays where it was, and every pipeline that draws such a mesh, the shadow pass's included, reads them there. Measured on an RX 9070 XT (`demo/dev/meshColors.html`): colours rewritten in place draw the picture a mesh made with the same colours draws, to the pixel on both backends, drawn alone and instanced."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"kind": "added",
|
|
18
|
+
"short": "A skinned cloth can be drawn back toward its skinned shape, so styled hair keeps the shape it was made in at rest and swings out of it as the body moves: `pull`, a stiffness.",
|
|
19
|
+
"text": "`SkinnedClothParameters.pull` draws every free particle of a garment back toward where its skinning puts it, as a stiffness: metres a second squared for every metre away. Applied first among the limits, after the constraints, each substep keeping `1 / (1 + pull · h²)` of a particle's offset, which is a compliant constraint's step, so the shape it holds against gravity is one length at every step: a particle at rest under gravity sits `9.81 / pull` metres below its skinned place. The limits could not do this, since a max distance is a sphere a particle may fall anywhere inside, so styled hair pinned at its roots fell to its edge and hung over the face. **A stiffness rather than a share of the offset kept a second as `damping` is, which is what was asked for**: a share scaled by the step holds a sag proportional to it, measured on hair authored to keep 0.7 of its offset a 1/60 step at 6.36, 3.49 and 1.83 mm at 1/60, 1/120 and 1/240, where the stiffness holds 6.36 mm at all three. A share `k` kept a step `s` is `(1 / k - 1) / s²` and keeps exactly `k` at that step. The same on the device and on the CPU: with a pull, the device's solve parts from the CPU's by 0.001 mm, no more than the CPU parts from itself nudged a tenth of a micrometre a frame (`demo/dev/skinnedCloth.html?parity=1&pull=400`)."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"kind": "added",
|
|
23
|
+
"short": "An Android app can be held the way its game is played: `\"android\": { \"orientation\": \"sensorLandscape\" }` in drift.package.json, written into the activity.",
|
|
24
|
+
"text": "`drift.package.json`'s `android.orientation` is written into the activity's `android:screenOrientation`, so an app starts the right way round before any of the game's code runs, which a `lockOrientation` taken at run time cannot do. It takes every value Android documents (`landscape`, `sensorLandscape`, `portrait`, `sensorPortrait`, `userLandscape`, `fullSensor` and the rest) and refuses one it does not when the manifest is read, naming the field, since Android ignores an attribute value it does not know and the build would quietly be the phone's choice again. `unspecified`, the default, leaves the copied manifest exactly as it was. Until now the template's `unspecified` was the only answer, so a game held sideways built from a copy of the packager's templates, copied again from each release. `drift-package doctor` prints it beside the permissions and the cleartext policy."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"kind": "fixed",
|
|
28
|
+
"short": "A skinned draw that follows cloth binds the groups it made before, where WebGPU made them again nearly every frame: 24 a frame for two characters, and none now.",
|
|
29
|
+
"text": "A skinned draw's bind group was kept one a palette slot and material and made again whenever a resource in it differed from the last draw's. A garment's particles take turns between two textures, a step each, so a draw following cloth found the other texture every frame; and several meshes following one cloth bind one particle texture each with a binding of its own, so they replaced each other's group draw by draw. A consumer counted 24 groups made a frame for two characters, and none with cloth off. Each key now keeps a shelf of up to sixteen groups, each found again by the four resources it was built with, so a steady garment's draws make none; the cast shadows' groups, which missed the same way, keep a shelf a palette slot. And the views a draw of one binding binds with a set of particles are kept for every binding rather than for the last, which made two objects a draw as several meshes took turns. Measured on an RX 9070 XT (`demo/dev/skinnedCloth.html`): the garment, the garment under reconstruction and three garments stepped as one draw the picture a build that makes every group again draws, to the pixel, where binding last step's particles moves 18,321 pixels."
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
2
33
|
{
|
|
3
34
|
"version": "4.13.1",
|
|
4
35
|
"date": "2026-10-11",
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ WebGL2 everywhere else, behind one API, and carries the fixed-step loop, the ren
|
|
|
6
6
|
input, the scene graph, geometry, text and the whole of `@driftengine/physics`, which it re-exports.
|
|
7
7
|
Everything else is a separate package a game adds when it reaches for it.
|
|
8
8
|
|
|
9
|
-
**Cost:
|
|
9
|
+
**Cost: 924.2 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`,
|
|
10
10
|
which fails the build if the figure drifts by more than 3%.
|
|
11
11
|
|
|
12
12
|
The manual is at **[driftengine.dev/docs](https://driftengine.dev/docs)**, with an example running
|
package/dist/index.d.ts
CHANGED
|
@@ -155,8 +155,9 @@ export { BLOOM_LEVELS } from './render/bloomChain.ts';
|
|
|
155
155
|
export type { BloomResponse } from './render/bloomChain.ts';
|
|
156
156
|
export { probeDevice } from './render/backend/probe.ts';
|
|
157
157
|
export type { ProbeVerdict } from './render/backend/probe.ts';
|
|
158
|
-
export type { BoltHandle, CausticsHandle, ClothBindingHandle, ClothParticlesHandle, FlockHandle, IncrementalMeshHandle, LineHandle, MeshHandle, MeshOptions, PrepareOptions, RenderBackend, RendererApi, ParticleHandle, PlumeHandle, ScatterHandle, InstancedHandle, SdfTextHandle, SurfaceTextureHandle, TextHandle, WaterHandle, WindStreakHandle, } from './render/backend/api.ts';
|
|
158
|
+
export type { BoltHandle, CausticsHandle, ClothBindingHandle, PointSkinHandle, ClothParticlesHandle, FlockHandle, IncrementalMeshHandle, LineHandle, MeshHandle, MeshOptions, PrepareOptions, RenderBackend, RendererApi, ParticleHandle, PlumeHandle, ScatterHandle, InstancedHandle, SdfTextHandle, SurfaceTextureHandle, TextHandle, WaterHandle, WindStreakHandle, } from './render/backend/api.ts';
|
|
159
159
|
export type { ClothBindingData } from './render/clothBindingData.ts';
|
|
160
|
+
export type { PointSkinData } from './render/pointSkin.ts';
|
|
160
161
|
export { anisotropicModel, eyeModel, hairModel, lightmapModel, skinModel, } from './render/surfaceModel.ts';
|
|
161
162
|
export type { AnisotropicModel, EyeModel, HairModel, LightmapModel, SkinModel, SurfaceModel, SurfaceModelKind, } from './render/surfaceModel.ts';
|
|
162
163
|
export { DEFAULT_FRAME_REFLECTIONS } from './render/frameReflections.ts';
|
|
@@ -12,6 +12,7 @@ import type { CompressedTextureSource, SurfaceSource } from '../compressedSource
|
|
|
12
12
|
import type { LightmapPage } from '../lightmap.ts';
|
|
13
13
|
import type { DeviceParticles } from '../particlePool.ts';
|
|
14
14
|
import type { ClothBindingData } from '../clothBindingData.ts';
|
|
15
|
+
import type { PointSkinData } from '../pointSkin.ts';
|
|
15
16
|
import type { TextStyle } from '../textLayout.ts';
|
|
16
17
|
import type { SdfFont } from '../sdfFont.ts';
|
|
17
18
|
import type { SdfTextStyle } from '../sdfTextLayout.ts';
|
|
@@ -49,6 +50,12 @@ export interface MeshHandle {
|
|
|
49
50
|
*/
|
|
50
51
|
export interface ClothBindingHandle {
|
|
51
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* A dynamic mesh's point skin on the device, from `createPointSkin`: its vertices as weighted points
|
|
55
|
+
* in its joints' spaces. Opaque likewise.
|
|
56
|
+
*/
|
|
57
|
+
export interface PointSkinHandle {
|
|
58
|
+
}
|
|
52
59
|
/** One character's cloth particles on the device, from `createClothParticles`. Opaque likewise. */
|
|
53
60
|
export interface ClothParticlesHandle {
|
|
54
61
|
}
|
|
@@ -190,15 +197,24 @@ export interface MeshOptions {
|
|
|
190
197
|
/**
|
|
191
198
|
* Whether this mesh's positions will be rewritten with `updateMesh`.
|
|
192
199
|
*
|
|
193
|
-
* Costs a `DYNAMIC_DRAW` hint on WebGL2 and
|
|
194
|
-
*
|
|
195
|
-
*
|
|
200
|
+
* Costs a `DYNAMIC_DRAW` hint on WebGL2, and on WebGPU two buffers of its own for the positions
|
|
201
|
+
* and the normals beside the rest of the vertex, bound with it on every draw, and floats for the
|
|
202
|
+
* normals where they might have been sixteen bits. Paid only where a consumer asks.
|
|
196
203
|
*/
|
|
197
204
|
readonly dynamic?: boolean;
|
|
205
|
+
/**
|
|
206
|
+
* Whether this mesh's colours will be rewritten with `updateMeshColors`.
|
|
207
|
+
*
|
|
208
|
+
* Costs the colours as floats in a buffer of their own, twelve bytes a vertex where they might
|
|
209
|
+
* have been sixteen bits or, for one colour every vertex shares, nothing at all. Independent of
|
|
210
|
+
* `dynamic`: a surface shaded again as a camera moves rewrites its colours every frame and its
|
|
211
|
+
* positions never, and should pay for neither the positions' buffers nor their motion.
|
|
212
|
+
*/
|
|
213
|
+
readonly dynamicColors?: boolean;
|
|
198
214
|
}
|
|
199
215
|
export type RendererApi = Omit<{
|
|
200
216
|
[K in keyof Renderer]: Renderer[K];
|
|
201
|
-
}, 'gpuTimer' | 'createMesh' | 'createMeshIncremental' | 'updateMesh' | 'drawMesh' | 'disposeMesh' | 'drawTranslucentMesh' | 'drawLightVolume' | 'drawFilm' | 'createScatter' | 'drawScatter' | 'uploadScatter' | 'disposeScatter' | 'createInstanced' | 'uploadInstanced' | 'drawInstanced' | 'drawTranslucentInstanced' | 'disposeInstanced' | 'createWater' | 'drawWater' | 'disposeWater' | 'createPlumes' | 'drawPlumes' | 'disposePlumes' | 'setPlumeScale' | 'createCaustics' | 'drawCaustics' | 'disposeCaustics' | 'createParticles' | 'drawParticles' | 'drawDeviceParticles' | 'disposeParticles' | 'createWindStreaks' | 'drawWindStreaks' | 'disposeWindStreaks' | 'createFlock' | 'drawFlock' | 'disposeFlock' | 'createBolts' | 'drawBolts' | 'disposeBolts' | 'createLines' | 'drawLines' | 'disposeLines' | 'createText' | 'setText' | 'setPlate' | 'drawText' | 'textWidth' | 'disposeText' | 'createSdfText' | 'setSdfText' | 'drawSdfText' | 'disposeSdfText' | 'createSurfaceTexture' | 'createSurfaceTextureArray' | 'createLightmap' | 'createSceneCapture' | 'captureScene' | 'updateSurfaceTexture' | 'disposeSurfaceTexture' | 'setSurfaceTexture' | 'setMaterial' | 'prepareMesh' | 'prepareInstanced' | 'setSurfaceOverlay' | 'createClothBinding' | 'createClothParticles' | 'updateClothParticles' | 'setCloth' | 'disposeClothBinding' | 'disposeClothParticles'> & {
|
|
217
|
+
}, 'gpuTimer' | 'createMesh' | 'createMeshIncremental' | 'updateMesh' | 'updateMeshColors' | 'drawMesh' | 'disposeMesh' | 'drawTranslucentMesh' | 'drawLightVolume' | 'drawFilm' | 'createScatter' | 'drawScatter' | 'uploadScatter' | 'disposeScatter' | 'createInstanced' | 'uploadInstanced' | 'drawInstanced' | 'drawTranslucentInstanced' | 'disposeInstanced' | 'createWater' | 'drawWater' | 'disposeWater' | 'createPlumes' | 'drawPlumes' | 'disposePlumes' | 'setPlumeScale' | 'createCaustics' | 'drawCaustics' | 'disposeCaustics' | 'createParticles' | 'drawParticles' | 'drawDeviceParticles' | 'disposeParticles' | 'createWindStreaks' | 'drawWindStreaks' | 'disposeWindStreaks' | 'createFlock' | 'drawFlock' | 'disposeFlock' | 'createBolts' | 'drawBolts' | 'disposeBolts' | 'createLines' | 'drawLines' | 'disposeLines' | 'createText' | 'setText' | 'setPlate' | 'drawText' | 'textWidth' | 'disposeText' | 'createSdfText' | 'setSdfText' | 'drawSdfText' | 'disposeSdfText' | 'createSurfaceTexture' | 'createSurfaceTextureArray' | 'createLightmap' | 'createSceneCapture' | 'captureScene' | 'updateSurfaceTexture' | 'disposeSurfaceTexture' | 'setSurfaceTexture' | 'setMaterial' | 'prepareMesh' | 'prepareInstanced' | 'setSurfaceOverlay' | 'createClothBinding' | 'createClothParticles' | 'updateClothParticles' | 'setCloth' | 'disposeClothBinding' | 'disposeClothParticles' | 'createPointSkin' | 'posePointSkin' | 'disposePointSkin'> & {
|
|
202
218
|
/**
|
|
203
219
|
* A mesh's cloth binding: which simulation triangle each of its vertices follows, where on it and
|
|
204
220
|
* by how much (`ClothBindingData`). The mesh must be skinned — a binding blends each vertex
|
|
@@ -222,6 +238,29 @@ export type RendererApi = Omit<{
|
|
|
222
238
|
setCloth(binding: ClothBindingHandle | null, particles?: ClothParticlesHandle | null): void;
|
|
223
239
|
disposeClothBinding(binding: ClothBindingHandle): void;
|
|
224
240
|
disposeClothParticles(particles: ClothParticlesHandle): void;
|
|
241
|
+
/**
|
|
242
|
+
* A dynamic mesh posed on the device from weighted points in its joints' own spaces
|
|
243
|
+
* (`PointSkinData`): each vertex the weighted sum of up to four points, each moved by its joint,
|
|
244
|
+
* and a normal turned by its first joint. Posed into the mesh's own positions and normals, so
|
|
245
|
+
* every pass draws it as any dynamic mesh, motion vectors included. See `pointSkin.ts`.
|
|
246
|
+
*
|
|
247
|
+
* **For a rig whose vertices are stored that way**, which ordinary skinning cannot pose: it moves
|
|
248
|
+
* one bind-pose position by a blend of joints. Such a rig was posed on the main thread and
|
|
249
|
+
* uploaded every frame, reported at 38% of the main thread's drawing time.
|
|
250
|
+
*
|
|
251
|
+
* The mesh must be `{ dynamic: true }` and have the skin's vertex count; any other is refused in
|
|
252
|
+
* words, as is a joint past 511.
|
|
253
|
+
*/
|
|
254
|
+
createPointSkin(mesh: MeshHandle, data: PointSkinData): PointSkinHandle;
|
|
255
|
+
/**
|
|
256
|
+
* Pose a skin: `matrices` holds each joint's transform into the mesh's model space, sixteen floats
|
|
257
|
+
* a joint, column-major, at least as many joints as the skin names. **Once a frame, before the
|
|
258
|
+
* mesh's draws**: a second pose in a frame reaches every draw of it on WebGPU, as a second
|
|
259
|
+
* `updateMesh` does. The mesh's bounds become a box around every joint's reach, so it is culled
|
|
260
|
+
* as it moves without the posed vertices ever coming back to the CPU.
|
|
261
|
+
*/
|
|
262
|
+
posePointSkin(skin: PointSkinHandle, matrices: Float32Array): void;
|
|
263
|
+
disposePointSkin(skin: PointSkinHandle): void;
|
|
225
264
|
/**
|
|
226
265
|
* What the following draws lay over their surfaces beyond their materials — a rim of light, a
|
|
227
266
|
* dissolve with a glowing edge, wrinkle normals by region — or null for none, which every pass
|
|
@@ -581,16 +620,32 @@ export type RendererApi = Omit<{
|
|
|
581
620
|
* quads with a matrix each, which caps the resolution at the number of draw calls a consumer
|
|
582
621
|
* can afford. A consumer found it by trying to hang a banner.
|
|
583
622
|
*
|
|
584
|
-
* **Only positions and normals.**
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
623
|
+
* **Only positions and normals.** Emissive and the rest describe what a surface *is* and do not
|
|
624
|
+
* change because it bent; rewriting them every frame would be uploading unchanged bytes. Colour
|
|
625
|
+
* is the exception a consumer found, a surface shaded again every frame without moving, and it
|
|
626
|
+
* has `updateMeshColors` of its own rather than a place here, so a rewrite of either sends only
|
|
627
|
+
* what changed. The vertex *count* is fixed: the index buffer, the pipeline and every other
|
|
628
|
+
* attribute are sized against it, and an update of a different size is refused naming both.
|
|
588
629
|
*
|
|
589
630
|
* The mesh must have been created with `{ dynamic: true }`. That is not ceremony — it decides
|
|
590
631
|
* the buffer hint on one backend and what has to be kept on the other, and a mesh that never
|
|
591
632
|
* deforms should pay for neither.
|
|
592
633
|
*/
|
|
593
634
|
updateMesh(mesh: MeshHandle, positions: Float32Array, normals?: Float32Array): void;
|
|
635
|
+
/**
|
|
636
|
+
* Rewrite a mesh's colours, three floats a vertex, linear as `MeshData.colors` is.
|
|
637
|
+
*
|
|
638
|
+
* **For a surface whose colour changes while it stays where it is**: ground shaded with its
|
|
639
|
+
* distance from a moving camera, a heat map, a tile lit by a game's own rules. Without it such a
|
|
640
|
+
* surface is made again as a new mesh every frame, which is a buffer allocated and let go each
|
|
641
|
+
* frame on both backends and every pipeline it draws with looked up again.
|
|
642
|
+
*
|
|
643
|
+
* The mesh must have been created with `{ dynamicColors: true }`, which gives the colours a buffer
|
|
644
|
+
* of their own; any other is refused in words. Nothing else moves: not the positions, not the
|
|
645
|
+
* bounds, and no motion vector, since a colour does not move. A different vertex count is refused
|
|
646
|
+
* naming both.
|
|
647
|
+
*/
|
|
648
|
+
updateMeshColors(mesh: MeshHandle, colors: Float32Array): void;
|
|
594
649
|
/** Draw a handle this renderer made. Passing one from the other backend is a caller error. */
|
|
595
650
|
drawMesh(mesh: MeshHandle, model: ReadonlyMat4, depthLayer?: number, tint?: Vec3 | null,
|
|
596
651
|
/**
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Mesh } from '../../mesh.ts';
|
|
2
|
+
import { type PointSkinData, type PointSkinReach } from '../../pointSkin.ts';
|
|
3
|
+
/** The program and feedback object every skin on a context poses with, made at the first skin. */
|
|
4
|
+
export interface PointSkinPose {
|
|
5
|
+
readonly program: WebGLProgram;
|
|
6
|
+
readonly feedback: WebGLTransformFeedback;
|
|
7
|
+
}
|
|
8
|
+
export declare function createPointSkinPose(gl: WebGL2RenderingContext): PointSkinPose;
|
|
9
|
+
export declare class GlPointSkin {
|
|
10
|
+
readonly mesh: Mesh;
|
|
11
|
+
readonly reach: PointSkinReach;
|
|
12
|
+
private readonly count;
|
|
13
|
+
private readonly vao;
|
|
14
|
+
private readonly buffers;
|
|
15
|
+
private readonly joints;
|
|
16
|
+
private readonly width;
|
|
17
|
+
constructor(gl: WebGL2RenderingContext, mesh: Mesh, data: PointSkinData);
|
|
18
|
+
/** One buffer, read as each `[location, size, stride, offset]` says. */
|
|
19
|
+
private attach;
|
|
20
|
+
/** Pose every vertex into the mesh's buffers now, and undo every binding it made. */
|
|
21
|
+
pose(gl: WebGL2RenderingContext, pose: PointSkinPose, matrices: Float32Array): void;
|
|
22
|
+
dispose(gl: WebGL2RenderingContext): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A point skin on WebGL2: posed by transform feedback, a vertex stage run once a vertex with the
|
|
3
|
+
* rasteriser off, its two outputs captured into the dynamic mesh's own position and normal buffers.
|
|
4
|
+
* What a point skin is, and why it poses into a mesh, is `pointSkin.ts`; the same arithmetic in the
|
|
5
|
+
* same order is `backend/webgpu/shaders/pointSkin.wgsl.ts`.
|
|
6
|
+
*
|
|
7
|
+
* **Every binding it makes it undoes**, because it runs between a frame's draws: the program, the
|
|
8
|
+
* vertex array, the joint texture on its own unit, the feedback buffers and the discard. A buffer
|
|
9
|
+
* left bound for feedback while a draw reads it as vertices is an error WebGL2 raises at the draw.
|
|
10
|
+
*/
|
|
11
|
+
import { POINT_SKIN_TEXTURE_UNIT } from '../../lightBudget.js';
|
|
12
|
+
import { reachOf } from '../../pointSkin.js';
|
|
13
|
+
import { compileProgram } from '../../shader.js';
|
|
14
|
+
const POSE_VERT = /* glsl */ `#version 300 es
|
|
15
|
+
precision highp float;
|
|
16
|
+
precision highp int;
|
|
17
|
+
layout(location = 0) in vec4 aNamed;
|
|
18
|
+
layout(location = 1) in vec4 aWeights;
|
|
19
|
+
layout(location = 2) in vec3 aPoint0;
|
|
20
|
+
layout(location = 3) in vec3 aPoint1;
|
|
21
|
+
layout(location = 4) in vec3 aPoint2;
|
|
22
|
+
layout(location = 5) in vec3 aPoint3;
|
|
23
|
+
layout(location = 6) in vec3 aNormal;
|
|
24
|
+
uniform highp sampler2D uJoints;
|
|
25
|
+
out vec3 vPosition;
|
|
26
|
+
out vec3 vNormal;
|
|
27
|
+
|
|
28
|
+
mat4 jointAt(float joint) {
|
|
29
|
+
int at = int(joint) * 4;
|
|
30
|
+
return mat4(
|
|
31
|
+
texelFetch(uJoints, ivec2(at, 0), 0),
|
|
32
|
+
texelFetch(uJoints, ivec2(at + 1, 0), 0),
|
|
33
|
+
texelFetch(uJoints, ivec2(at + 2, 0), 0),
|
|
34
|
+
texelFetch(uJoints, ivec2(at + 3, 0), 0)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
void main() {
|
|
39
|
+
vec3 p = vec3(0.0);
|
|
40
|
+
if (aWeights.x != 0.0) p += aWeights.x * (jointAt(aNamed.x) * vec4(aPoint0, 1.0)).xyz;
|
|
41
|
+
if (aWeights.y != 0.0) p += aWeights.y * (jointAt(aNamed.y) * vec4(aPoint1, 1.0)).xyz;
|
|
42
|
+
if (aWeights.z != 0.0) p += aWeights.z * (jointAt(aNamed.z) * vec4(aPoint2, 1.0)).xyz;
|
|
43
|
+
if (aWeights.w != 0.0) p += aWeights.w * (jointAt(aNamed.w) * vec4(aPoint3, 1.0)).xyz;
|
|
44
|
+
vec3 raw = mat3(jointAt(aNamed.x)) * aNormal;
|
|
45
|
+
float l = length(raw);
|
|
46
|
+
vNormal = l > 0.0 ? raw / l : raw;
|
|
47
|
+
vPosition = p;
|
|
48
|
+
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
const POSE_FRAG = /* glsl */ `#version 300 es
|
|
52
|
+
precision mediump float;
|
|
53
|
+
out vec4 colour;
|
|
54
|
+
void main() {
|
|
55
|
+
colour = vec4(0.0);
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
export function createPointSkinPose(gl) {
|
|
59
|
+
const program = compileProgram(gl, POSE_VERT, POSE_FRAG, 'pointSkin.pose', [
|
|
60
|
+
'vPosition',
|
|
61
|
+
'vNormal',
|
|
62
|
+
]);
|
|
63
|
+
gl.useProgram(program);
|
|
64
|
+
gl.uniform1i(gl.getUniformLocation(program, 'uJoints'), POINT_SKIN_TEXTURE_UNIT);
|
|
65
|
+
gl.useProgram(null);
|
|
66
|
+
const feedback = gl.createTransformFeedback();
|
|
67
|
+
if (feedback === null)
|
|
68
|
+
throw new Error('createTransformFeedback failed');
|
|
69
|
+
return { program, feedback };
|
|
70
|
+
}
|
|
71
|
+
export class GlPointSkin {
|
|
72
|
+
mesh;
|
|
73
|
+
reach;
|
|
74
|
+
count;
|
|
75
|
+
vao;
|
|
76
|
+
buffers = [];
|
|
77
|
+
joints;
|
|
78
|
+
width;
|
|
79
|
+
constructor(gl, mesh, data) {
|
|
80
|
+
this.mesh = mesh;
|
|
81
|
+
this.reach = reachOf(data);
|
|
82
|
+
this.count = data.weights.length / 4;
|
|
83
|
+
const vao = gl.createVertexArray();
|
|
84
|
+
if (vao === null)
|
|
85
|
+
throw new Error('createVertexArray failed');
|
|
86
|
+
this.vao = vao;
|
|
87
|
+
gl.bindVertexArray(vao);
|
|
88
|
+
this.attach(gl, data.joints, [[0, 4, 16, 0]]);
|
|
89
|
+
this.attach(gl, data.weights, [[1, 4, 16, 0]]);
|
|
90
|
+
this.attach(gl, data.points, [
|
|
91
|
+
[2, 3, 48, 0],
|
|
92
|
+
[3, 3, 48, 12],
|
|
93
|
+
[4, 3, 48, 24],
|
|
94
|
+
[5, 3, 48, 36],
|
|
95
|
+
]);
|
|
96
|
+
this.attach(gl, data.normals, [[6, 3, 12, 0]]);
|
|
97
|
+
gl.bindVertexArray(null);
|
|
98
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
99
|
+
/* One row, four texels a joint, as the skin palette is. */
|
|
100
|
+
this.width = Math.max(1, this.reach.joints * 4);
|
|
101
|
+
const joints = gl.createTexture();
|
|
102
|
+
if (joints === null)
|
|
103
|
+
throw new Error('createTexture failed');
|
|
104
|
+
this.joints = joints;
|
|
105
|
+
gl.activeTexture(gl.TEXTURE0 + POINT_SKIN_TEXTURE_UNIT);
|
|
106
|
+
gl.bindTexture(gl.TEXTURE_2D, joints);
|
|
107
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, this.width, 1);
|
|
108
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
109
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
110
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
111
|
+
}
|
|
112
|
+
/** One buffer, read as each `[location, size, stride, offset]` says. */
|
|
113
|
+
attach(gl, values, reads) {
|
|
114
|
+
const buffer = gl.createBuffer();
|
|
115
|
+
if (buffer === null)
|
|
116
|
+
throw new Error('createBuffer failed');
|
|
117
|
+
this.buffers.push(buffer);
|
|
118
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
119
|
+
gl.bufferData(gl.ARRAY_BUFFER, values, gl.STATIC_DRAW);
|
|
120
|
+
for (const [location, size, stride, offset] of reads) {
|
|
121
|
+
gl.enableVertexAttribArray(location);
|
|
122
|
+
gl.vertexAttribPointer(location, size, gl.FLOAT, false, stride, offset);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Pose every vertex into the mesh's buffers now, and undo every binding it made. */
|
|
126
|
+
pose(gl, pose, matrices) {
|
|
127
|
+
const streams = this.mesh.streams;
|
|
128
|
+
if (streams === null)
|
|
129
|
+
return;
|
|
130
|
+
gl.useProgram(pose.program);
|
|
131
|
+
gl.bindVertexArray(this.vao);
|
|
132
|
+
gl.activeTexture(gl.TEXTURE0 + POINT_SKIN_TEXTURE_UNIT);
|
|
133
|
+
gl.bindTexture(gl.TEXTURE_2D, this.joints);
|
|
134
|
+
if (this.reach.joints > 0) {
|
|
135
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, this.width, 1, gl.RGBA, gl.FLOAT, matrices, 0);
|
|
136
|
+
}
|
|
137
|
+
gl.enable(gl.RASTERIZER_DISCARD);
|
|
138
|
+
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, pose.feedback);
|
|
139
|
+
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, streams.positions);
|
|
140
|
+
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, streams.normals);
|
|
141
|
+
gl.beginTransformFeedback(gl.POINTS);
|
|
142
|
+
gl.drawArrays(gl.POINTS, 0, this.count);
|
|
143
|
+
gl.endTransformFeedback();
|
|
144
|
+
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
|
|
145
|
+
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);
|
|
146
|
+
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
|
|
147
|
+
gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, null);
|
|
148
|
+
gl.disable(gl.RASTERIZER_DISCARD);
|
|
149
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
150
|
+
gl.bindVertexArray(null);
|
|
151
|
+
gl.useProgram(null);
|
|
152
|
+
}
|
|
153
|
+
dispose(gl) {
|
|
154
|
+
for (const buffer of this.buffers)
|
|
155
|
+
gl.deleteBuffer(buffer);
|
|
156
|
+
gl.deleteTexture(this.joints);
|
|
157
|
+
gl.deleteVertexArray(this.vao);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -85,6 +85,8 @@ import type { FlockParams } from '../../flockRenderer.ts';
|
|
|
85
85
|
import { InstancedBatch } from './instanced.ts';
|
|
86
86
|
import type { BoneAnimationClip, BoneAnimationHandle } from '../../boneAnimation.ts';
|
|
87
87
|
import type { InstancedOptions, MeshInstances } from '../../instances.ts';
|
|
88
|
+
import { type PointSkinData } from '../../pointSkin.ts';
|
|
89
|
+
import { GlPointSkin } from './pointSkinFeedback.ts';
|
|
88
90
|
/** Reused each frame: which shader light slot each bound cubemap serves. */
|
|
89
91
|
export type { ShadowCasterSink, ShadowCasters } from '../../shadowCasters.ts';
|
|
90
92
|
/** Lighting and atmosphere for a frame. The game decides what goes in it. */
|
|
@@ -1869,6 +1871,25 @@ export declare class WebGL2Renderer implements RendererApi {
|
|
|
1869
1871
|
* buffer is the same buffer. Rebinding here would cost a state change for nothing.
|
|
1870
1872
|
*/
|
|
1871
1873
|
updateMesh(mesh: Mesh, positions: Float32Array, normals?: Float32Array): void;
|
|
1874
|
+
/** The program and feedback object every point skin poses with, made at the first one. */
|
|
1875
|
+
private pointSkinPose;
|
|
1876
|
+
/**
|
|
1877
|
+
* A dynamic mesh posed on the device from weighted points in its joints' spaces, by transform
|
|
1878
|
+
* feedback. Refused in words on a mesh that is not dynamic, whose buffers are not hinted for a
|
|
1879
|
+
* rewrite every frame.
|
|
1880
|
+
*/
|
|
1881
|
+
createPointSkin(mesh: Mesh, data: PointSkinData): GlPointSkin;
|
|
1882
|
+
/**
|
|
1883
|
+
* Pose a skin now: `matrices` holds each joint's transform into the mesh's model space, sixteen
|
|
1884
|
+
* floats, column-major. A command in the stream, so the draws after it read this pose.
|
|
1885
|
+
*/
|
|
1886
|
+
posePointSkin(skin: GlPointSkin, matrices: Float32Array): void;
|
|
1887
|
+
disposePointSkin(skin: GlPointSkin): void;
|
|
1888
|
+
/**
|
|
1889
|
+
* Rewrite a mesh's colours, on a mesh made `dynamicColors`. The VAO is left alone for the reason
|
|
1890
|
+
* `updateMesh` gives: the buffer is the same buffer.
|
|
1891
|
+
*/
|
|
1892
|
+
updateMeshColors(mesh: Mesh, colors: Float32Array): void;
|
|
1872
1893
|
/**
|
|
1873
1894
|
* Compile the skinned flat program, once, on the first rigged mesh.
|
|
1874
1895
|
*
|
|
@@ -134,6 +134,8 @@ import { CUTOUT_RESOLVE_CODE, resolveCutout } from '../../cutoutDither.js';
|
|
|
134
134
|
import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.js';
|
|
135
135
|
import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.js';
|
|
136
136
|
import { noLitSwitches } from '../../shaders/flat/glassTint.js';
|
|
137
|
+
import { poseBounds, validatePointSkin, validatePose, } from '../../pointSkin.js';
|
|
138
|
+
import { GlPointSkin, createPointSkinPose } from './pointSkinFeedback.js';
|
|
137
139
|
/**
|
|
138
140
|
* How many frames `firstFrameSettled` will ask before it stops asking.
|
|
139
141
|
*
|
|
@@ -2584,7 +2586,7 @@ export class WebGL2Renderer {
|
|
|
2584
2586
|
*/
|
|
2585
2587
|
const drawn = withLightmapUvs(data);
|
|
2586
2588
|
this.warmVariants(drawn);
|
|
2587
|
-
return new Mesh(this.gl, drawn, options.dynamic === true);
|
|
2589
|
+
return new Mesh(this.gl, drawn, options.dynamic === true, false, options.dynamicColors === true);
|
|
2588
2590
|
}
|
|
2589
2591
|
/**
|
|
2590
2592
|
* Geometry in, handle out — but the geometry lands over as many frames as the caller gives it.
|
|
@@ -2598,7 +2600,7 @@ export class WebGL2Renderer {
|
|
|
2598
2600
|
createMeshIncremental(data, options = {}) {
|
|
2599
2601
|
const drawn = withLightmapUvs(data);
|
|
2600
2602
|
this.warmVariants(drawn);
|
|
2601
|
-
const { mesh, upload } = createMeshIncremental(this.gl, drawn, options.dynamic === true);
|
|
2603
|
+
const { mesh, upload } = createMeshIncremental(this.gl, drawn, options.dynamic === true, options.dynamicColors === true);
|
|
2602
2604
|
return {
|
|
2603
2605
|
mesh,
|
|
2604
2606
|
upload: {
|
|
@@ -2641,6 +2643,43 @@ export class WebGL2Renderer {
|
|
|
2641
2643
|
updateMesh(mesh, positions, normals) {
|
|
2642
2644
|
mesh.update(this.gl, positions, normals);
|
|
2643
2645
|
}
|
|
2646
|
+
/** The program and feedback object every point skin poses with, made at the first one. */
|
|
2647
|
+
pointSkinPose = null;
|
|
2648
|
+
/**
|
|
2649
|
+
* A dynamic mesh posed on the device from weighted points in its joints' spaces, by transform
|
|
2650
|
+
* feedback. Refused in words on a mesh that is not dynamic, whose buffers are not hinted for a
|
|
2651
|
+
* rewrite every frame.
|
|
2652
|
+
*/
|
|
2653
|
+
createPointSkin(mesh, data) {
|
|
2654
|
+
if (mesh.streams === null) {
|
|
2655
|
+
throw new Error('a point skin poses a mesh created with { dynamic: true }, whose positions and normals ' +
|
|
2656
|
+
'are rewritten every frame. Say so at `createMesh`.');
|
|
2657
|
+
}
|
|
2658
|
+
validatePointSkin(data, mesh.vertexCount);
|
|
2659
|
+
this.pointSkinPose ??= createPointSkinPose(this.gl);
|
|
2660
|
+
return new GlPointSkin(this.gl, mesh, data);
|
|
2661
|
+
}
|
|
2662
|
+
/**
|
|
2663
|
+
* Pose a skin now: `matrices` holds each joint's transform into the mesh's model space, sixteen
|
|
2664
|
+
* floats, column-major. A command in the stream, so the draws after it read this pose.
|
|
2665
|
+
*/
|
|
2666
|
+
posePointSkin(skin, matrices) {
|
|
2667
|
+
validatePose(matrices, skin.reach);
|
|
2668
|
+
if (this.contextLost || this.pointSkinPose === null)
|
|
2669
|
+
return;
|
|
2670
|
+
poseBounds(matrices, skin.reach, skin.mesh.bounds);
|
|
2671
|
+
skin.pose(this.gl, this.pointSkinPose, matrices);
|
|
2672
|
+
}
|
|
2673
|
+
disposePointSkin(skin) {
|
|
2674
|
+
skin.dispose(this.gl);
|
|
2675
|
+
}
|
|
2676
|
+
/**
|
|
2677
|
+
* Rewrite a mesh's colours, on a mesh made `dynamicColors`. The VAO is left alone for the reason
|
|
2678
|
+
* `updateMesh` gives: the buffer is the same buffer.
|
|
2679
|
+
*/
|
|
2680
|
+
updateMeshColors(mesh, colors) {
|
|
2681
|
+
mesh.updateColors(this.gl, colors);
|
|
2682
|
+
}
|
|
2644
2683
|
/**
|
|
2645
2684
|
* Compile the skinned flat program, once, on the first rigged mesh.
|
|
2646
2685
|
*
|
|
@@ -162,6 +162,14 @@ export interface GpuMesh {
|
|
|
162
162
|
* layout, keyed by `:dynamic`, and two more buffers bound per draw of such a mesh.
|
|
163
163
|
*/
|
|
164
164
|
readonly update: ((device: GPUDevice, positions: Float32Array, normals?: Float32Array, staged?: StagedRewrite | null) => void) | null;
|
|
165
|
+
/**
|
|
166
|
+
* Rewrite this mesh's colours, three floats a vertex.
|
|
167
|
+
*
|
|
168
|
+
* Absent unless the mesh was created `dynamicColors`, whose colours are a buffer of their own at
|
|
169
|
+
* the end of `vertexBuffers` (`vertexPacking.ts`), so this is one `writeBuffer` and nothing else
|
|
170
|
+
* moves: not the positions, not the bounds, and no motion, since a colour does not move.
|
|
171
|
+
*/
|
|
172
|
+
readonly updateColors: ((device: GPUDevice, colors: Float32Array) => void) | null;
|
|
165
173
|
/**
|
|
166
174
|
* A dynamic mesh's motion state, which the renderer owns: a buffer for last frame's positions and
|
|
167
175
|
* one the new positions are staged in, each made the first time a reconstructing renderer
|
|
@@ -211,7 +219,7 @@ export interface IncrementalGpuMesh {
|
|
|
211
219
|
* two paths cannot write different bytes. The one that spreads the work is the one with the
|
|
212
220
|
* interesting decisions in it; read that.
|
|
213
221
|
*/
|
|
214
|
-
export declare function createGpuMesh(device: GPUDevice, data: MeshData, dynamic?: boolean): GpuMesh;
|
|
222
|
+
export declare function createGpuMesh(device: GPUDevice, data: MeshData, dynamic?: boolean, dynamicColors?: boolean): GpuMesh;
|
|
215
223
|
/**
|
|
216
224
|
* Upload a mesh over as many frames as the caller gives it.
|
|
217
225
|
*
|
|
@@ -237,7 +245,7 @@ export declare function createGpuMesh(device: GPUDevice, data: MeshData, dynamic
|
|
|
237
245
|
* streamer places and culls a region from them before a byte of it has landed, so a handle whose
|
|
238
246
|
* bounds were not yet known would not be usable at once, which is the whole promise here.
|
|
239
247
|
*/
|
|
240
|
-
export declare function createGpuMeshIncremental(device: GPUDevice, data: MeshData, dynamic?: boolean): IncrementalGpuMesh;
|
|
248
|
+
export declare function createGpuMeshIncremental(device: GPUDevice, data: MeshData, dynamic?: boolean, dynamicColors?: boolean): IncrementalGpuMesh;
|
|
241
249
|
export declare function vertexBufferLayouts(present: Readonly<Record<string, boolean>>,
|
|
242
250
|
/**
|
|
243
251
|
* Whether a third buffer carries one placement and colour per instance.
|