@driftengine/core 4.13.5 → 4.14.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.json +62 -0
- package/README.md +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/render/backend/api.d.ts +19 -3
- package/dist/render/backend/webgl2/motionTarget.d.ts +39 -0
- package/dist/render/backend/webgl2/motionTarget.js +254 -0
- package/dist/render/backend/webgl2/pointSkinFeedback.d.ts +13 -3
- package/dist/render/backend/webgl2/pointSkinFeedback.js +45 -12
- package/dist/render/backend/webgl2/renderer.d.ts +101 -7
- package/dist/render/backend/webgl2/renderer.js +337 -9
- package/dist/render/backend/webgpu/pointSkinPass.d.ts +16 -3
- package/dist/render/backend/webgpu/pointSkinPass.js +79 -21
- package/dist/render/backend/webgpu/postPass.d.ts +6 -1
- package/dist/render/backend/webgpu/renderer.d.ts +41 -0
- package/dist/render/backend/webgpu/renderer.js +143 -5
- package/dist/render/baseDecals.d.ts +48 -0
- package/dist/render/baseDecals.js +71 -0
- package/dist/render/emissiveFlow.d.ts +40 -0
- package/dist/render/emissiveFlow.js +51 -0
- package/dist/render/fixtureAtlas.d.ts +8 -3
- package/dist/render/fixtureAtlas.js +17 -6
- package/dist/render/globalMedium.d.ts +17 -0
- package/dist/render/globalMedium.js +14 -0
- package/dist/render/globalMediumPass.js +3 -2
- package/dist/render/iridescentOverlay.d.ts +78 -0
- package/dist/render/iridescentOverlay.js +102 -0
- package/dist/render/mesh.d.ts +24 -0
- package/dist/render/mesh.js +79 -0
- package/dist/render/pointSkin.d.ts +18 -0
- package/dist/render/pointSkin.js +50 -5
- package/dist/render/recon/mover.d.ts +3 -2
- package/dist/render/recon/mover.js +3 -2
- package/dist/render/sceneTarget.d.ts +2 -0
- package/dist/render/sceneTarget.js +1 -1
- package/dist/render/shaders/flat/baseDecals.d.ts +17 -0
- package/dist/render/shaders/flat/baseDecals.js +71 -0
- package/dist/render/shaders/flat/emissiveFlow.d.ts +15 -0
- package/dist/render/shaders/flat/emissiveFlow.js +45 -0
- package/dist/render/shaders/flat/glassTint.d.ts +1 -1
- package/dist/render/shaders/flat/glassTint.js +3 -0
- package/dist/render/shaders/flat/index.d.ts +16 -0
- package/dist/render/shaders/flat/index.js +9 -0
- package/dist/render/shaders/flat/iridescentOverlay.d.ts +15 -0
- package/dist/render/shaders/flat/iridescentOverlay.js +65 -0
- package/dist/render/shaders/flat/layered.js +4 -4
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +12 -1
- package/dist/render/shaders/flat/mapReads.d.ts +1 -1
- package/dist/render/shaders/flat/mapReads.js +7 -3
- package/dist/render/shaders/flat/worldUv.js +6 -5
- package/dist/render/shaders/generated/flat.wgsl.d.ts +480 -16
- package/dist/render/shaders/generated/flat.wgsl.js +519 -55
- package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +15 -5
- package/dist/render/shaders/generated/temporalResolve.wgsl.js +15 -5
- package/dist/render/shaders/temporalResolve.js +25 -9
- package/dist/render/surfaceTexture.d.ts +32 -0
- package/dist/render/temporalPass.d.ts +3 -1
- package/dist/render/temporalPass.js +9 -1
- package/package.json +6 -6
- package/src/index.ts +3 -0
- package/src/render/backend/api.ts +19 -2
- package/src/render/backend/webgl2/motionTarget.ts +299 -0
- package/src/render/backend/webgl2/pointSkinFeedback.ts +49 -14
- package/src/render/backend/webgl2/renderer.ts +411 -9
- package/src/render/backend/webgpu/pointSkinPass.ts +90 -23
- package/src/render/backend/webgpu/renderer.ts +163 -3
- package/src/render/baseDecals.ts +93 -0
- package/src/render/emissiveFlow.ts +66 -0
- package/src/render/fixtureAtlas.ts +26 -7
- package/src/render/globalMedium.ts +31 -0
- package/src/render/globalMediumPass.ts +3 -7
- package/src/render/iridescentOverlay.ts +177 -0
- package/src/render/mesh.ts +82 -0
- package/src/render/pointSkin.ts +64 -5
- package/src/render/recon/mover.ts +3 -2
- package/src/render/sceneTarget.ts +3 -0
- package/src/render/shaders/flat/baseDecals.ts +71 -0
- package/src/render/shaders/flat/emissiveFlow.ts +45 -0
- package/src/render/shaders/flat/glassTint.ts +7 -1
- package/src/render/shaders/flat/index.ts +25 -0
- package/src/render/shaders/flat/iridescentOverlay.ts +65 -0
- package/src/render/shaders/flat/layered.ts +4 -4
- package/src/render/shaders/flat/main.ts +12 -1
- package/src/render/shaders/flat/mapReads.ts +7 -3
- package/src/render/shaders/flat/worldUv.ts +6 -5
- package/src/render/shaders/generated/flat.wgsl.ts +519 -55
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +15 -5
- package/src/render/shaders/temporalResolve.ts +25 -9
- package/src/render/surfaceTexture.ts +32 -0
- package/src/render/temporalPass.ts +8 -0
package/CHANGELOG.json
CHANGED
|
@@ -1,4 +1,66 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "4.14.0",
|
|
4
|
+
"date": "2026-10-18",
|
|
5
|
+
"entries": [
|
|
6
|
+
{
|
|
7
|
+
"kind": "added",
|
|
8
|
+
"short": "A point skin rewritten in part: `updatePointSkin(skin, first, data)` sends a range of vertices, where a skin that changed shape was made again whole.",
|
|
9
|
+
"text": "`updatePointSkin(skin, first, data)` rewrites a point skin's vertices from `first` onward, as many as `data` holds, their joints, weights, points and normals, and sends only those: a hand that closes or a face whose picture is taken away moves a few hundred vertices a few times a second, and a skin made again whole for each change was the largest part of a character's drawing on the main thread in the report that asked for this. The pose after the rewrite reads it and a pose before it in the frame does not, on both backends: WebGPU submits a pose it has already recorded before it writes, which a queue write would otherwise reach. The bounds take in what a rewrite reaches and never shrink, since the vertices it replaced may have been the farthest, so a skin changing back and forth settles at the box of all its shapes; a joint none of its vertices named before grows what the device holds for the joints, once. A range outside the mesh, arrays of different lengths and a joint past 511 are refused in words. Measured on an RX 9070 XT (`demo/dev/pointSkin.html?rewrite=grow`): a skin made with its hand on the elbow and rewritten onto a third joint at frame 30 draws what the skin made whole draws, to the pixel on both backends, where the skin left unrewritten differs by 25,968 pixels."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"kind": "added",
|
|
13
|
+
"short": "The global medium's sky fill in a colour of its own: `skyFill` beside `skyScatter`, for a scene whose surfaces are lit by more than its flat ambient.",
|
|
14
|
+
"text": "`setGlobalMedium` takes `skyFill`, the sky's light the medium scatters as a colour of the medium's own, in the units of `Environment.ambient`, which `skyScatter` then scales; absent, the medium scatters the frame's ambient as it did. A scene lit by a sky's cubemap, probes and a bake keeps its flat ambient for what falls outside them, so that ambient is the wrong colour for the sky's light in the air, and setting it to the right one would relight those surfaces too. Both backends upload the one decision (`mediumSkyFill`), and the shader is unchanged. Measured on both backends: a fill equal to the ambient draws what the ambient draws, to the pixel, and a blue fill with the sun scattering nothing raises blue alone at every pixel of the scene and leaves red and green where they were."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"kind": "added",
|
|
18
|
+
"short": "A material's glow moved and masked: `emissiveScroll` moves the emissive map by the surface clock, `emissiveMaskLayer` keeps it where a layer's alpha at the mesh's own coordinates says.",
|
|
19
|
+
"text": "`SurfaceMaterial.emissiveScroll` moves the emissive map alone, `[u, v]` in its own repeats a second by `Environment.surfaceTime`, wrapped to one repeat so a long clock keeps its precision, wherever the map is read: at the mesh's coordinates, at a layer's place or on a projection's planes. `emissiveMaskLayer` names a layer of the emissive array whose alpha multiplies the emission, read at the mesh's own first coordinates before `uScale`, `uOffset`, the layers or a projection, so a glow repeated eight times across a surface is held to cracks a map spanning it once draws, where folding the mask into each vertex's emission spread it over whole faces of a coarse mesh. The mask is alpha because alpha is never decoded as sRGB, and a layer of the emissive array because the lit stage has no texture binding left. A lit switch, `EMISSIVE_FLOW`, compiled in the first time a material asks, two uniform vectors, refused in words on a WebGL2 part with no room. Measured on both backends (`demo/dev/emissiveFlow.html`): the mask takes the glow off exactly half a wall whose stripes repeat sixteen times, a quarter repeat moves them, a whole repeat draws the control, and the two backends agree to the pixel."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"kind": "added",
|
|
23
|
+
"short": "A surface's iridescent overlay: a hue turned by the view laid over the base colour before it is lit, from a ramp of colours, a direction, four numbers and a mask.",
|
|
24
|
+
"text": "`SurfaceMaterial.iridescentOverlay` lays a hue that turns with the view over the base colour before any light reads it, the sheen of a coat running through a spectrum as it turns. With `N` the mapped normal, `V` toward the eye and `u` the mesh's own first coordinate, `x = N·V + u + blend (h − N·V)`, the sheen `h` being `N·H` raised to `contrast` for `H` halfway between `V` and the overlay's `direction`, and 0 where `N·H` is at or below zero; the `ramp` is read at `fract(x · tiling)`, and the base moves toward the overlay blend of itself and that colour by `x · mask · weight`. The mask is a channel of a layer of the material's ORM, model, albedo or emissive array. The lit stage has no texture binding left, so the ramp is thirty-two colours of uniforms, resampled from any length: thirty-six vectors behind a lit switch, `IRIDESCENT_OVERLAY`, which WebGL2 refuses in words on a part with no room. The same arithmetic runs on the CPU, and a square whose every term is constant draws on the device exactly the colour it gives, to the pixel on both backends (`demo/dev/iridescence.html?mode=probe`); a weight of 0 draws what no overlay draws."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"kind": "added",
|
|
28
|
+
"short": "Decals laid into the base colour before it is lit: `setBaseDecals` and `setBaseDecalImages`, the glow, roughness and normal left as they are, a material refusing them.",
|
|
29
|
+
"text": "`setBaseDecals(decals)` lays up to eight decals into the base colour of the draws after it, held across passes until the next call: each a box as a `DecalBox` is, an image of `setBaseDecalImages` read across its face, and an opacity. The base colour moves toward the image's colour by its alpha times the opacity before any light is added, so the surface is then lit and glows as it would; `DecalProjector` multiplies the finished pixel instead, which dims whatever glows under a mark. Images are 128-texel squares in a band of the fixture atlas, since the lit stage has no texture binding left, read as pictures, and rewriting them once a frame costs an upload of the squares. `facingCos` keeps a mark aimed at a floor off the sides of what stands on it, judged by the geometric normal, and `SurfaceMaterial.receivesDecals: false` keeps a surface in the box unmarked. Where a mesh names no emissive colour, its glow takes the base colour from before the decals. A lit switch, `BASE_DECALS`, thirty-four vectors, refused in words on a WebGL2 part with no room. Measured on both backends (`demo/dev/baseDecals.html`): a floor under a black decal at 60% and an orange one draws, to the pixel, a floor built in the colours they make with its glowing stripe glowing as it did."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"kind": "added",
|
|
33
|
+
"short": "WebGL2's temporal antialiasing follows what moves: rigid, skinned and rewritten movers drawn again into a motion target the resolve reads in place of the camera's motion.",
|
|
34
|
+
"text": "Under `temporalAa`, WebGL2's resolve reprojected the last frame by the camera's motion alone, which is exact for a still world and leaves a trail where something moves across a background close to its own colour. A draw naming a `Mover`, or last frame's model matrix, is now drawn again at the end of the frame into a motion target, depth-tested against the frame's own depth, each texel saying where the surface it shows stood last frame; a mesh rewritten with `updateMesh` or a point skin is followed by its kept positions without one. The resolve reads that offset where a mover wrote one and the camera's reprojection everywhere else. Rigid, skinned and rewritten meshes are followed; cloth, instanced batches and a multisampled frame keep the camera's motion, said once. WebGPU follows movers through its reconstruction, as before. Measured on an RX 9070 XT (`demo/dev/moverTaa.html`), a block, a skinned bar and a posed box moving under a still camera: 1,036 pixels differ from the frame without the resolve by 8 levels or more, where the camera's motion alone left 6,500, on ANGLE over Vulkan and over OpenGL alike; with nothing moving, the two agree within a level."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"kind": "added",
|
|
38
|
+
"short": "A container back out to glTF: `drft export` writes a `.drft` as a `.glb` that `drft bake` reads back to the same file, and `bakeModel` is the bake as a function.",
|
|
39
|
+
"text": "`npm run export -- model.drft -o model.glb`, and `drftToGlb` in `@driftengine/assets` under it, write a container as a glTF 2.0 binary, so a baked character or level opens in Blender or any glTF editor and bakes back. What glTF has a field for goes in it: meshes with every attribute, materials with vertex colours, the skeleton with the bone names a game binds by, every clip by name with its keys at their times, attachment points as nodes under their joints, morph targets and the lamps through `KHR_lights_punctual`. What it has no field for, the entity manifest, colliders, regions, placements and the outline, rides in `extras`, which Blender keeps, and the format's other chunks ride as their own bytes; each is used again only while the glTF fields beside it are unchanged, so an artist's edit always wins. A container the baker wrote comes back byte for byte: two fixtures carrying every one of those chunks, and twenty-three of the repository's own models, bake, export and bake back identical, and a scene of ten thousand lamps does too. A kit-built world and a splat capture are refused, since glTF has no form for either, and raw or block-compressed textures go out as PNG. `bakeModel` is the bake `drft bake` runs, moved out of the command so the export, a test and a tool share it."
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"kind": "fixed",
|
|
43
|
+
"short": "The desktop shell's `drift://` answers a byte range with 206 and its length, so a video seeks, and a missing file is 404; a movie is served as video.",
|
|
44
|
+
"text": "The packaged desktop app's `drift://` scheme streamed every file whole, with neither a length nor an answer to a `Range` request, so a `<video>` played from its start but could not be sought: a game seeking its movie to keep up with the frame it showed watched it go back to its first frames each time. Every answer now carries `Accept-Ranges` and a `Content-Length`, a single range is answered `206` with `Content-Range`, one starting past the end `416`, and a file that is not there `404` where it was a `200`. Several ranges at once are answered whole, as the RFC allows. `.mp4` and `.webm` are served as video. Measured in the real shell (`drift-package run`) on a twenty-second movie: seeks to 14, 3 and 17.5 seconds land there, where the shell before landed every one at 0."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"kind": "fixed",
|
|
48
|
+
"short": "An Android build on Windows from a folder with a backslash escape in its path: the SDK's folder is written escaped into `local.properties`, as a properties file reads it.",
|
|
49
|
+
"text": "The packager wrote `sdk.dir=` into the staged Android project's `local.properties` with the SDK's folder exactly as found. A properties file reads a backslash as an escape, so on Windows `C:\\tools\\new\\android-sdk` came back with a tab for `\\t`, a new line for `\\n` and every other backslash dropped, and Gradle's lint stopped on a folder nobody named. The value is now written as `Properties.store` writes one: each backslash doubled, a leading space escaped, and everything past printable ASCII as `\\uXXXX`, so a folder under a user name with letters past ASCII reads back as itself whatever encoding opens the file."
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"version": "4.13.6",
|
|
55
|
+
"date": "2026-10-14",
|
|
56
|
+
"entries": [
|
|
57
|
+
{
|
|
58
|
+
"kind": "fixed",
|
|
59
|
+
"short": "The packager builds for Android on Windows again: `sdkmanager.bat` and `gradle.bat` start through `cmd.exe`, which Node has required of a batch file since its April 2024 security releases.",
|
|
60
|
+
"text": "An Android build on Windows stopped at accepting the SDK licences with `spawn EINVAL` and nothing else. Since the April 2024 security releases (CVE-2024-27980: Node 18.20.2, 20.12.2, 21.7.3 and every release after them), Node refuses to start a `.bat` or `.cmd` without a shell, and the packager's Android toolchain on Windows is two of them, `sdkmanager.bat` and `gradle.bat`. A batch file on Windows now starts through `cmd.exe`, with its AutoRun commands left out, every part of the line quoted by hand with its own quotes doubled, and the line passed as it is: an SDK root under a user folder with a space in its name stays one argument, and so does a package such as `platforms;android-35`, whose semicolon a batch file would otherwise read as a separator. Node's own `shell: true` does not quote, so an argument with a space would arrive in two. Every other tool, and a batch file on any other platform, starts as it did."
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
2
64
|
{
|
|
3
65
|
"version": "4.13.5",
|
|
4
66
|
"date": "2026-10-12",
|
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: 966.4 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
|
@@ -234,6 +234,9 @@ export type { SurfaceProjection } from './render/surfaceProjection.ts';
|
|
|
234
234
|
export type { SurfaceLayers } from './render/surfaceLayers.ts';
|
|
235
235
|
export type { SurfaceDetail, SurfaceDetailRegion } from './render/surfaceDetail.ts';
|
|
236
236
|
export { MAX_DETAIL_REGIONS } from './render/surfaceDetail.ts';
|
|
237
|
+
export type { IridescentMaskMap, IridescentOverlay } from './render/iridescentOverlay.ts';
|
|
238
|
+
export type { BaseDecal } from './render/baseDecals.ts';
|
|
239
|
+
export { BASE_DECAL_TILE, MAX_BASE_DECALS } from './render/baseDecals.ts';
|
|
237
240
|
export type { SurfaceLayerLook } from './render/surfaceLayerLooks.ts';
|
|
238
241
|
export { compressedFormatName, isCompressedSource, levelBytes, uploadsCompressed, } from './render/compressedSource.ts';
|
|
239
242
|
export type { BlockFormat, CompressedTextureFormat, CompressedTextureSource, SurfaceSource, } from './render/compressedSource.ts';
|
package/dist/index.js
CHANGED
|
@@ -288,6 +288,7 @@ export { MAX_POINT_LIGHTS, SURFACE_TEXTURE_UNIT } from './render/lightBudget.js'
|
|
|
288
288
|
export { MAX_CLUSTERED_LIGHTS } from './render/clusteredLights.js';
|
|
289
289
|
export { SurfaceTexture } from './render/surfaceTexture.js';
|
|
290
290
|
export { MAX_DETAIL_REGIONS } from './render/surfaceDetail.js';
|
|
291
|
+
export { BASE_DECAL_TILE, MAX_BASE_DECALS } from './render/baseDecals.js';
|
|
291
292
|
/*
|
|
292
293
|
* Compressed textures — BC, ETC2 and EAC, ASTC: the source `createSurfaceTexture` takes as blocks,
|
|
293
294
|
* the one question to ask before handing one over — whether this device takes its format
|
|
@@ -214,7 +214,7 @@ export interface MeshOptions {
|
|
|
214
214
|
}
|
|
215
215
|
export type RendererApi = Omit<{
|
|
216
216
|
[K in keyof Renderer]: Renderer[K];
|
|
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'> & {
|
|
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' | 'updatePointSkin' | 'disposePointSkin'> & {
|
|
218
218
|
/**
|
|
219
219
|
* A mesh's cloth binding: which simulation triangle each of its vertices follows, where on it and
|
|
220
220
|
* by how much (`ClothBindingData`). The mesh must be skinned — a binding blends each vertex
|
|
@@ -260,6 +260,20 @@ export type RendererApi = Omit<{
|
|
|
260
260
|
* as it moves without the posed vertices ever coming back to the CPU.
|
|
261
261
|
*/
|
|
262
262
|
posePointSkin(skin: PointSkinHandle, matrices: Float32Array): void;
|
|
263
|
+
/**
|
|
264
|
+
* Rewrite part of a skin in place: vertices `first` onward, as many as `data` holds (its four
|
|
265
|
+
* arrays a range's worth, as `createPointSkin` takes them whole), their joints, weights, points and
|
|
266
|
+
* normals. **For a skin that changes shape a few hundred vertices at a time**, a hand closing or a
|
|
267
|
+
* face's picture taken away, which was made again whole for each change: the largest part of a
|
|
268
|
+
* character's drawing on the main thread in the report that asked for this.
|
|
269
|
+
*
|
|
270
|
+
* **The pose after it reads the rewrite, and a pose before it in the frame does not**, on both
|
|
271
|
+
* backends: WebGPU submits a pose it has already recorded before it writes. The mesh's bounds take
|
|
272
|
+
* in what the rewrite reaches and never shrink, since the vertices it replaced may have been the
|
|
273
|
+
* farthest; `createPointSkin` measures a skin afresh. A range outside the mesh, arrays of
|
|
274
|
+
* different lengths and a joint past 511 are refused in words, naming the mesh's own vertices.
|
|
275
|
+
*/
|
|
276
|
+
updatePointSkin(skin: PointSkinHandle, first: number, data: PointSkinData): void;
|
|
263
277
|
disposePointSkin(skin: PointSkinHandle): void;
|
|
264
278
|
/**
|
|
265
279
|
* What the following draws lay over their surfaces beyond their materials — a rim of light, a
|
|
@@ -660,8 +674,10 @@ export type RendererApi = Omit<{
|
|
|
660
674
|
* depth, which is exactly right for anything that did not move and wrong in proportion to how
|
|
661
675
|
* far a mover moved.
|
|
662
676
|
*
|
|
663
|
-
* **
|
|
664
|
-
*
|
|
677
|
+
* **Read by a reconstruction on WebGPU, and by `temporalAa` on WebGL2**, which draws the movers
|
|
678
|
+
* again into a motion target at the end of the frame (`backend/webgl2/motionTarget.ts`). A mover
|
|
679
|
+
* drawn twice in one frame is two objects wearing one identity: the second draw is warned about
|
|
680
|
+
* once and records no motion.
|
|
665
681
|
* Instanced batches and meshes rewritten with `updateMesh` need none — the batch and the mesh
|
|
666
682
|
* are their own identity.
|
|
667
683
|
*/
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Mesh } from '../../mesh.ts';
|
|
2
|
+
export declare class GlMotionPass {
|
|
3
|
+
private readonly programs;
|
|
4
|
+
private texture;
|
|
5
|
+
private framebuffer;
|
|
6
|
+
private width;
|
|
7
|
+
private height;
|
|
8
|
+
private depth;
|
|
9
|
+
/** This frame's palette and last frame's, uploaded record by record. */
|
|
10
|
+
private readonly paletteNow;
|
|
11
|
+
private readonly paletteWas;
|
|
12
|
+
/** The frame's movers: mesh, kind, two matrices, two palettes. Grown, never shrunk. */
|
|
13
|
+
private readonly meshes;
|
|
14
|
+
private kinds;
|
|
15
|
+
private matrices;
|
|
16
|
+
private readonly palettes;
|
|
17
|
+
private readonly wasPalettes;
|
|
18
|
+
private count;
|
|
19
|
+
/** A frame states its movers afresh. */
|
|
20
|
+
reset(): void;
|
|
21
|
+
get recorded(): number;
|
|
22
|
+
/** A rigid mover under last frame's model. */
|
|
23
|
+
recordRigid(mesh: Mesh, model: ArrayLike<number>, wasModel: ArrayLike<number>): void;
|
|
24
|
+
/** A skinned mover under last frame's model and palette. */
|
|
25
|
+
recordSkinned(mesh: Mesh, model: ArrayLike<number>, wasModel: ArrayLike<number>, palette: Float32Array, wasPalette: Float32Array): void;
|
|
26
|
+
/** A mesh rewritten this frame, under last frame's positions and model. */
|
|
27
|
+
recordDynamic(mesh: Mesh, model: ArrayLike<number>, wasModel: ArrayLike<number>): void;
|
|
28
|
+
private take;
|
|
29
|
+
/**
|
|
30
|
+
* Draw the recorded movers into the motion target against `depth`, and return the target; null
|
|
31
|
+
* where nothing was recorded. Leaves the framebuffer, program and array unbound, the depth test on
|
|
32
|
+
* with the frame's compare and writes on, no offset, and the palette units holding its palettes.
|
|
33
|
+
*/
|
|
34
|
+
draw(gl: WebGL2RenderingContext, depth: WebGLTexture, width: number, height: number, reversedDepth: boolean, jittered: Float32Array, viewProj: Float32Array, wasViewProj: Float32Array): WebGLTexture | null;
|
|
35
|
+
private programFor;
|
|
36
|
+
/** An `RGBA16F` target the frame's size, its depth the frame's own attachment. */
|
|
37
|
+
private ensureTarget;
|
|
38
|
+
dispose(gl: WebGL2RenderingContext): void;
|
|
39
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where every mover was last frame, on WebGL2: the motion its temporal resolve follows.
|
|
3
|
+
*
|
|
4
|
+
* **What it is for.** The resolve reprojects last frame's picture by the camera's motion, read
|
|
5
|
+
* back from the depth, which is exact for a world standing still and wrong for whatever moved: a
|
|
6
|
+
* head turning or a boot in a kick leaves a trail of its earlier pixels. WebGPU's reconstruction
|
|
7
|
+
* follows each mover's own motion through `shaders/recon/motion.wgsl.ts`; this is the same idea
|
|
8
|
+
* for the other backend. The frame's movers are recorded as they are drawn and drawn again at the
|
|
9
|
+
* end of the frame into a target of their own, each texel saying where the surface it shows stood
|
|
10
|
+
* last frame: the offset to it on screen, its distance from last frame's eye, and that it is a
|
|
11
|
+
* mover's. The resolve reads it where a mover's texel is and the camera's reprojection everywhere
|
|
12
|
+
* else.
|
|
13
|
+
*
|
|
14
|
+
* **Three kinds**, as `recordDrawMotion` on WebGPU decides them: a rigid mesh under last frame's
|
|
15
|
+
* model; a skinned one under last frame's palette as well; and a mesh rewritten in place, whose
|
|
16
|
+
* last frame's positions `Mesh.keepPrevious` copied before the first rewrite of the frame. A cloth
|
|
17
|
+
* draw and an instanced batch keep the camera's motion here, which the renderer says once.
|
|
18
|
+
*
|
|
19
|
+
* **Drawn against the frame's own depth**, attached and never written, with the test passing at its
|
|
20
|
+
* own surface and a nudge toward the eye to forgive the last bit of arithmetic: rasterised through
|
|
21
|
+
* the frame's jittered matrix so it covers the pixels the surface did, its motion taken between the
|
|
22
|
+
* unjittered ones. **What it gives up**: a mover hidden behind glass or seen in a mirror keeps the
|
|
23
|
+
* camera's motion; a multisampled frame keeps it everywhere; and a morph target's change of shape
|
|
24
|
+
* is not followed.
|
|
25
|
+
*/
|
|
26
|
+
import { MORPH_DELTA_TEXTURE_UNIT, SKIN_PALETTE_TEXTURE_UNIT } from '../../lightBudget.js';
|
|
27
|
+
import { compileProgram, uniformLocations } from '../../shader.js';
|
|
28
|
+
import { SkinPaletteTexture } from './skinPaletteTexture.js';
|
|
29
|
+
const RIGID = 0;
|
|
30
|
+
const SKINNED = 1;
|
|
31
|
+
const DYNAMIC = 2;
|
|
32
|
+
/** The vertex stage for one kind, skinned with eight influences where `eight` says. */
|
|
33
|
+
function vertexSource(kind, eight) {
|
|
34
|
+
return /* glsl */ `#version 300 es
|
|
35
|
+
precision highp float;
|
|
36
|
+
precision highp int;
|
|
37
|
+
layout(location = 0) in vec3 aPosition;
|
|
38
|
+
layout(location = 1) in vec3 aWas;
|
|
39
|
+
layout(location = 11) in vec4 aJoints;
|
|
40
|
+
layout(location = 12) in vec4 aWeights;
|
|
41
|
+
layout(location = 14) in vec4 aJoints2;
|
|
42
|
+
layout(location = 15) in vec4 aWeights2;
|
|
43
|
+
uniform mat4 uModel;
|
|
44
|
+
uniform mat4 uWasModel;
|
|
45
|
+
uniform mat4 uJittered;
|
|
46
|
+
uniform mat4 uViewProj;
|
|
47
|
+
uniform mat4 uWasViewProj;
|
|
48
|
+
uniform highp sampler2D uPaletteNow;
|
|
49
|
+
uniform highp sampler2D uPaletteWas;
|
|
50
|
+
out vec4 vNow;
|
|
51
|
+
out vec4 vWas;
|
|
52
|
+
|
|
53
|
+
mat4 joint(highp sampler2D palette, float index) {
|
|
54
|
+
int x = int(index) * 4;
|
|
55
|
+
return mat4(
|
|
56
|
+
texelFetch(palette, ivec2(x, 0), 0),
|
|
57
|
+
texelFetch(palette, ivec2(x + 1, 0), 0),
|
|
58
|
+
texelFetch(palette, ivec2(x + 2, 0), 0),
|
|
59
|
+
texelFetch(palette, ivec2(x + 3, 0), 0));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* The same blend as skinning.ts, joint by joint, so a skinned mover lands where its draw did. */
|
|
63
|
+
mat4 skin(highp sampler2D palette) {
|
|
64
|
+
mat4 m = joint(palette, aJoints.x) * aWeights.x + joint(palette, aJoints.y) * aWeights.y
|
|
65
|
+
+ joint(palette, aJoints.z) * aWeights.z + joint(palette, aJoints.w) * aWeights.w;
|
|
66
|
+
${eight
|
|
67
|
+
? `m += joint(palette, aJoints2.x) * aWeights2.x + joint(palette, aJoints2.y) * aWeights2.y
|
|
68
|
+
+ joint(palette, aJoints2.z) * aWeights2.z + joint(palette, aJoints2.w) * aWeights2.w;`
|
|
69
|
+
: ''}
|
|
70
|
+
return m;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
void main() {
|
|
74
|
+
vec4 now = vec4(aPosition, 1.0);
|
|
75
|
+
vec4 was = now;
|
|
76
|
+
${kind === SKINNED
|
|
77
|
+
? 'was = skin(uPaletteWas) * now;\n now = skin(uPaletteNow) * now;'
|
|
78
|
+
: kind === DYNAMIC
|
|
79
|
+
? 'was = vec4(aWas, 1.0);'
|
|
80
|
+
: ''}
|
|
81
|
+
vec4 world = uModel * now;
|
|
82
|
+
gl_Position = uJittered * world;
|
|
83
|
+
vNow = uViewProj * world;
|
|
84
|
+
vWas = uWasViewProj * (uWasModel * was);
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
const FRAGMENT = /* glsl */ `#version 300 es
|
|
89
|
+
precision highp float;
|
|
90
|
+
in vec4 vNow;
|
|
91
|
+
in vec4 vWas;
|
|
92
|
+
out vec4 motion;
|
|
93
|
+
/* The offset on screen to where this surface stood, its distance from last frame's eye, and 1. */
|
|
94
|
+
void main() {
|
|
95
|
+
vec2 now = vNow.xy / vNow.w * 0.5 + 0.5;
|
|
96
|
+
vec2 was = vWas.xy / max(vWas.w, 1e-6) * 0.5 + 0.5;
|
|
97
|
+
motion = vec4(was - now, vWas.w, 1.0);
|
|
98
|
+
}
|
|
99
|
+
`;
|
|
100
|
+
export class GlMotionPass {
|
|
101
|
+
programs = new Map();
|
|
102
|
+
texture = null;
|
|
103
|
+
framebuffer = null;
|
|
104
|
+
width = 0;
|
|
105
|
+
height = 0;
|
|
106
|
+
depth = null;
|
|
107
|
+
/** This frame's palette and last frame's, uploaded record by record. */
|
|
108
|
+
paletteNow = new SkinPaletteTexture();
|
|
109
|
+
paletteWas = new SkinPaletteTexture();
|
|
110
|
+
/** The frame's movers: mesh, kind, two matrices, two palettes. Grown, never shrunk. */
|
|
111
|
+
meshes = [];
|
|
112
|
+
kinds = new Uint8Array(16);
|
|
113
|
+
matrices = new Float32Array(16 * 32);
|
|
114
|
+
palettes = [];
|
|
115
|
+
wasPalettes = [];
|
|
116
|
+
count = 0;
|
|
117
|
+
/** A frame states its movers afresh. */
|
|
118
|
+
reset() {
|
|
119
|
+
this.count = 0;
|
|
120
|
+
}
|
|
121
|
+
get recorded() {
|
|
122
|
+
return this.count;
|
|
123
|
+
}
|
|
124
|
+
/** A rigid mover under last frame's model. */
|
|
125
|
+
recordRigid(mesh, model, wasModel) {
|
|
126
|
+
this.take(mesh, RIGID, model, wasModel, null, null);
|
|
127
|
+
}
|
|
128
|
+
/** A skinned mover under last frame's model and palette. */
|
|
129
|
+
recordSkinned(mesh, model, wasModel, palette, wasPalette) {
|
|
130
|
+
this.take(mesh, SKINNED, model, wasModel, palette, wasPalette);
|
|
131
|
+
}
|
|
132
|
+
/** A mesh rewritten this frame, under last frame's positions and model. */
|
|
133
|
+
recordDynamic(mesh, model, wasModel) {
|
|
134
|
+
this.take(mesh, DYNAMIC, model, wasModel, null, null);
|
|
135
|
+
}
|
|
136
|
+
take(mesh, kind, model, wasModel, palette, wasPalette) {
|
|
137
|
+
const at = this.count;
|
|
138
|
+
if (at >= this.kinds.length) {
|
|
139
|
+
const kinds = new Uint8Array(this.kinds.length * 2);
|
|
140
|
+
kinds.set(this.kinds);
|
|
141
|
+
this.kinds = kinds;
|
|
142
|
+
const matrices = new Float32Array(this.matrices.length * 2);
|
|
143
|
+
matrices.set(this.matrices);
|
|
144
|
+
this.matrices = matrices;
|
|
145
|
+
}
|
|
146
|
+
this.meshes[at] = mesh;
|
|
147
|
+
this.kinds[at] = kind;
|
|
148
|
+
this.matrices.set(model, at * 32);
|
|
149
|
+
this.matrices.set(wasModel, at * 32 + 16);
|
|
150
|
+
this.palettes[at] = palette;
|
|
151
|
+
this.wasPalettes[at] = wasPalette;
|
|
152
|
+
this.count = at + 1;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Draw the recorded movers into the motion target against `depth`, and return the target; null
|
|
156
|
+
* where nothing was recorded. Leaves the framebuffer, program and array unbound, the depth test on
|
|
157
|
+
* with the frame's compare and writes on, no offset, and the palette units holding its palettes.
|
|
158
|
+
*/
|
|
159
|
+
draw(gl, depth, width, height, reversedDepth, jittered, viewProj, wasViewProj) {
|
|
160
|
+
if (this.count === 0)
|
|
161
|
+
return null;
|
|
162
|
+
this.ensureTarget(gl, depth, width, height);
|
|
163
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
|
164
|
+
gl.viewport(0, 0, width, height);
|
|
165
|
+
gl.clearColor(0, 0, 0, 0);
|
|
166
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
167
|
+
gl.enable(gl.DEPTH_TEST);
|
|
168
|
+
gl.depthMask(false);
|
|
169
|
+
gl.depthFunc(reversedDepth ? gl.GEQUAL : gl.LEQUAL);
|
|
170
|
+
/* Toward the eye, which is a larger depth when depth is reversed. */
|
|
171
|
+
gl.enable(gl.POLYGON_OFFSET_FILL);
|
|
172
|
+
gl.polygonOffset(reversedDepth ? 1 : -1, reversedDepth ? 4 : -4);
|
|
173
|
+
gl.disable(gl.BLEND);
|
|
174
|
+
gl.disable(gl.CULL_FACE);
|
|
175
|
+
for (let i = 0; i < this.count; i++) {
|
|
176
|
+
const mesh = this.meshes[i];
|
|
177
|
+
const kind = this.kinds[i];
|
|
178
|
+
const eight = kind === SKINNED && mesh.isSkinnedEight;
|
|
179
|
+
const { program, uniforms } = this.programFor(gl, kind, eight);
|
|
180
|
+
gl.useProgram(program);
|
|
181
|
+
gl.uniformMatrix4fv(uniforms['uJittered'] ?? null, false, jittered);
|
|
182
|
+
gl.uniformMatrix4fv(uniforms['uViewProj'] ?? null, false, viewProj);
|
|
183
|
+
gl.uniformMatrix4fv(uniforms['uWasViewProj'] ?? null, false, wasViewProj);
|
|
184
|
+
gl.uniformMatrix4fv(uniforms['uModel'] ?? null, false, this.matrices, i * 32, 16);
|
|
185
|
+
gl.uniformMatrix4fv(uniforms['uWasModel'] ?? null, false, this.matrices, i * 32 + 16, 16);
|
|
186
|
+
if (kind === SKINNED) {
|
|
187
|
+
/* On the units a skinned draw binds its palette and morph on, which every one binds again
|
|
188
|
+
for itself: nothing the lit pass keeps across frames is moved. */
|
|
189
|
+
gl.activeTexture(gl.TEXTURE0 + SKIN_PALETTE_TEXTURE_UNIT);
|
|
190
|
+
this.paletteNow.update(gl, this.palettes[i]);
|
|
191
|
+
gl.activeTexture(gl.TEXTURE0 + MORPH_DELTA_TEXTURE_UNIT);
|
|
192
|
+
this.paletteWas.update(gl, this.wasPalettes[i]);
|
|
193
|
+
gl.uniform1i(uniforms['uPaletteNow'] ?? null, SKIN_PALETTE_TEXTURE_UNIT);
|
|
194
|
+
gl.uniform1i(uniforms['uPaletteWas'] ?? null, MORPH_DELTA_TEXTURE_UNIT);
|
|
195
|
+
}
|
|
196
|
+
mesh.drawMotion(gl, kind === DYNAMIC);
|
|
197
|
+
}
|
|
198
|
+
gl.disable(gl.POLYGON_OFFSET_FILL);
|
|
199
|
+
gl.polygonOffset(0, 0);
|
|
200
|
+
gl.depthMask(true);
|
|
201
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
202
|
+
gl.useProgram(null);
|
|
203
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
204
|
+
return this.texture;
|
|
205
|
+
}
|
|
206
|
+
programFor(gl, kind, eight) {
|
|
207
|
+
const key = kind * 2 + (eight ? 1 : 0);
|
|
208
|
+
const held = this.programs.get(key);
|
|
209
|
+
if (held !== undefined)
|
|
210
|
+
return held;
|
|
211
|
+
const program = compileProgram(gl, vertexSource(kind, eight), FRAGMENT, `motion.${key}`);
|
|
212
|
+
const made = { program, uniforms: uniformLocations(gl, program, `motion.${key}`) };
|
|
213
|
+
this.programs.set(key, made);
|
|
214
|
+
return made;
|
|
215
|
+
}
|
|
216
|
+
/** An `RGBA16F` target the frame's size, its depth the frame's own attachment. */
|
|
217
|
+
ensureTarget(gl, depth, width, height) {
|
|
218
|
+
if (this.texture !== null && this.width === width && this.height === height) {
|
|
219
|
+
if (this.depth === depth)
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
if (this.texture !== null)
|
|
224
|
+
gl.deleteTexture(this.texture);
|
|
225
|
+
this.texture = gl.createTexture();
|
|
226
|
+
gl.bindTexture(gl.TEXTURE_2D, this.texture);
|
|
227
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA16F, width, height);
|
|
228
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
229
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
230
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
231
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
232
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
233
|
+
this.width = width;
|
|
234
|
+
this.height = height;
|
|
235
|
+
}
|
|
236
|
+
this.framebuffer ??= gl.createFramebuffer();
|
|
237
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
|
|
238
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0);
|
|
239
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, depth, 0);
|
|
240
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
241
|
+
this.depth = depth;
|
|
242
|
+
}
|
|
243
|
+
dispose(gl) {
|
|
244
|
+
for (const { program } of this.programs.values())
|
|
245
|
+
gl.deleteProgram(program);
|
|
246
|
+
this.programs.clear();
|
|
247
|
+
if (this.texture !== null)
|
|
248
|
+
gl.deleteTexture(this.texture);
|
|
249
|
+
if (this.framebuffer !== null)
|
|
250
|
+
gl.deleteFramebuffer(this.framebuffer);
|
|
251
|
+
this.texture = null;
|
|
252
|
+
this.framebuffer = null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -8,13 +8,23 @@ export interface PointSkinPose {
|
|
|
8
8
|
export declare function createPointSkinPose(gl: WebGL2RenderingContext): PointSkinPose;
|
|
9
9
|
export declare class GlPointSkin {
|
|
10
10
|
readonly mesh: Mesh;
|
|
11
|
-
|
|
11
|
+
/** Widened by each rewrite, never narrowed: see `widenReach`. */
|
|
12
|
+
reach: PointSkinReach;
|
|
12
13
|
private readonly count;
|
|
13
14
|
private readonly vao;
|
|
15
|
+
/** Joints, weights, points and normals, in that order. */
|
|
14
16
|
private readonly buffers;
|
|
15
|
-
private
|
|
16
|
-
private
|
|
17
|
+
private joints;
|
|
18
|
+
private width;
|
|
17
19
|
constructor(gl: WebGL2RenderingContext, mesh: Mesh, data: PointSkinData);
|
|
20
|
+
/**
|
|
21
|
+
* Rewrite vertices `first` onward, as many as `data` holds, in place: their four arrays sent at
|
|
22
|
+
* their offsets and nothing else. A command in the stream like a pose, so the pose after it reads
|
|
23
|
+
* the rewrite and one before it does not. A joint none of the skin named before grows the joint
|
|
24
|
+
* texture, which is made again for it; a skin settles at the joints its shapes name, so that is
|
|
25
|
+
* rare after its first changes.
|
|
26
|
+
*/
|
|
27
|
+
rewrite(gl: WebGL2RenderingContext, first: number, data: PointSkinData): void;
|
|
18
28
|
/** One buffer, read as each `[location, size, stride, offset]` says. */
|
|
19
29
|
private attach;
|
|
20
30
|
/** Pose every vertex into the mesh's buffers now, and undo every binding it made. */
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* left bound for feedback while a draw reads it as vertices is an error WebGL2 raises at the draw.
|
|
10
10
|
*/
|
|
11
11
|
import { POINT_SKIN_TEXTURE_UNIT } from '../../lightBudget.js';
|
|
12
|
-
import { reachOf } from '../../pointSkin.js';
|
|
12
|
+
import { reachOf, widenReach } from '../../pointSkin.js';
|
|
13
13
|
import { compileProgram } from '../../shader.js';
|
|
14
14
|
const POSE_VERT = /* glsl */ `#version 300 es
|
|
15
15
|
precision highp float;
|
|
@@ -70,9 +70,11 @@ export function createPointSkinPose(gl) {
|
|
|
70
70
|
}
|
|
71
71
|
export class GlPointSkin {
|
|
72
72
|
mesh;
|
|
73
|
+
/** Widened by each rewrite, never narrowed: see `widenReach`. */
|
|
73
74
|
reach;
|
|
74
75
|
count;
|
|
75
76
|
vao;
|
|
77
|
+
/** Joints, weights, points and normals, in that order. */
|
|
76
78
|
buffers = [];
|
|
77
79
|
joints;
|
|
78
80
|
width;
|
|
@@ -96,18 +98,36 @@ export class GlPointSkin {
|
|
|
96
98
|
this.attach(gl, data.normals, [[6, 3, 12, 0]]);
|
|
97
99
|
gl.bindVertexArray(null);
|
|
98
100
|
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
99
|
-
/* One row, four texels a joint, as the skin palette is. */
|
|
100
101
|
this.width = Math.max(1, this.reach.joints * 4);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
this.joints = jointTexture(gl, this.width);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Rewrite vertices `first` onward, as many as `data` holds, in place: their four arrays sent at
|
|
106
|
+
* their offsets and nothing else. A command in the stream like a pose, so the pose after it reads
|
|
107
|
+
* the rewrite and one before it does not. A joint none of the skin named before grows the joint
|
|
108
|
+
* texture, which is made again for it; a skin settles at the joints its shapes name, so that is
|
|
109
|
+
* rare after its first changes.
|
|
110
|
+
*/
|
|
111
|
+
rewrite(gl, first, data) {
|
|
112
|
+
const sent = [
|
|
113
|
+
[data.joints, 16],
|
|
114
|
+
[data.weights, 16],
|
|
115
|
+
[data.points, 48],
|
|
116
|
+
[data.normals, 12],
|
|
117
|
+
];
|
|
118
|
+
for (let at = 0; at < sent.length; at++) {
|
|
119
|
+
const [values, stride] = sent[at];
|
|
120
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers[at]);
|
|
121
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, first * stride, values);
|
|
122
|
+
}
|
|
123
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
124
|
+
this.reach = widenReach(this.reach, data);
|
|
125
|
+
const width = Math.max(1, this.reach.joints * 4);
|
|
126
|
+
if (width > this.width) {
|
|
127
|
+
gl.deleteTexture(this.joints);
|
|
128
|
+
this.width = width;
|
|
129
|
+
this.joints = jointTexture(gl, width);
|
|
130
|
+
}
|
|
111
131
|
}
|
|
112
132
|
/** One buffer, read as each `[location, size, stride, offset]` says. */
|
|
113
133
|
attach(gl, values, reads) {
|
|
@@ -157,3 +177,16 @@ export class GlPointSkin {
|
|
|
157
177
|
gl.deleteVertexArray(this.vao);
|
|
158
178
|
}
|
|
159
179
|
}
|
|
180
|
+
/** One row, four texels a joint, as the skin palette is. */
|
|
181
|
+
function jointTexture(gl, width) {
|
|
182
|
+
const joints = gl.createTexture();
|
|
183
|
+
if (joints === null)
|
|
184
|
+
throw new Error('createTexture failed');
|
|
185
|
+
gl.activeTexture(gl.TEXTURE0 + POINT_SKIN_TEXTURE_UNIT);
|
|
186
|
+
gl.bindTexture(gl.TEXTURE_2D, joints);
|
|
187
|
+
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, width, 1);
|
|
188
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
189
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
190
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
191
|
+
return joints;
|
|
192
|
+
}
|