3d-spinner 0.9.0 → 0.9.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/README.md +49 -8
- package/dist/animations/object-motion.d.ts +4 -1
- package/dist/animations/object-motion.js +2 -1
- package/dist/animations/spin.d.ts +4 -1
- package/dist/animations/spin.js +2 -1
- package/dist/cjs/animations/object-motion.cjs +1368 -0
- package/dist/cjs/animations/spin.cjs +1299 -0
- package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +1324 -0
- package/dist/cjs/engines/little-3d-engine/loaders/obj.cjs +58 -0
- package/dist/cjs/engines/little-tween-engine/little-tween-engine.cjs +279 -0
- package/dist/cjs/index.cjs +120 -0
- package/dist/cjs/motion/motion.cjs +140 -0
- package/dist/cjs/motion/transitions.cjs +80 -0
- package/dist/engines/little-3d-engine/core/mesh.d.ts +18 -0
- package/dist/engines/little-3d-engine/little-3d-engine.d.ts +10 -3
- package/dist/engines/little-3d-engine/little-3d-engine.js +12 -8
- package/dist/engines/little-3d-engine/loaders/obj.d.ts +2 -2
- package/dist/engines/little-3d-engine/loaders/obj.js +2 -2
- package/dist/engines/little-3d-engine/renderer.d.ts +15 -1
- package/dist/engines/little-3d-engine/renderer.js +36 -0
- package/dist/engines/little-3d-engine/renderers/canvas2d.d.ts +1 -1
- package/dist/engines/little-3d-engine/renderers/canvas2d.js +22 -2
- package/dist/engines/little-3d-engine/renderers/webgl.d.ts +1 -1
- package/dist/engines/little-3d-engine/renderers/webgl.js +38 -1
- package/dist/engines/little-3d-engine/renderers/webgpu.d.ts +3 -1
- package/dist/engines/little-3d-engine/renderers/webgpu.js +70 -17
- package/dist/umd/spinner.global.js +2408 -0
- package/dist/umd/spinner.global.min.js +58 -0
- package/package.json +26 -11
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type CameraOptions } from "./core/camera.js";
|
|
2
2
|
import { type LightOptions } from "./core/light.js";
|
|
3
|
-
import { type Mesh, type Transform } from "./core/mesh.js";
|
|
3
|
+
import { type Mesh, type Transform, type Transparency } from "./core/mesh.js";
|
|
4
4
|
import { type Backend } from "./renderer.js";
|
|
5
5
|
/** Options for {@link Little3dEngine}. */
|
|
6
6
|
export interface Little3dEngineOptions {
|
|
@@ -15,9 +15,15 @@ export interface Little3dEngineOptions {
|
|
|
15
15
|
export interface MeshHandle {
|
|
16
16
|
readonly mesh: Mesh;
|
|
17
17
|
readonly transform: Transform;
|
|
18
|
+
/** Optional per-instance transparency. Mutate or replace it between frames. */
|
|
19
|
+
transparency?: Transparency;
|
|
18
20
|
/** Remove this mesh from the scene. */
|
|
19
21
|
remove(): void;
|
|
20
22
|
}
|
|
23
|
+
/** Initial state for one mesh instance. */
|
|
24
|
+
export interface MeshInstanceOptions extends Partial<Transform> {
|
|
25
|
+
transparency?: Transparency;
|
|
26
|
+
}
|
|
21
27
|
/**
|
|
22
28
|
* A minimal software/hardware 3D engine. It projects colored meshes with flat
|
|
23
29
|
* directional lighting through a swappable {@link Backend} renderer. Mount it
|
|
@@ -47,7 +53,7 @@ export declare class Little3dEngine {
|
|
|
47
53
|
*/
|
|
48
54
|
mount(target: HTMLElement): Promise<void>;
|
|
49
55
|
/** Add a mesh to the scene and return a handle for animating it. */
|
|
50
|
-
add(mesh: Mesh, init?:
|
|
56
|
+
add(mesh: Mesh, init?: MeshInstanceOptions): MeshHandle;
|
|
51
57
|
private resize;
|
|
52
58
|
/** Draw a single frame from the current scene state. */
|
|
53
59
|
render(): void;
|
|
@@ -69,7 +75,8 @@ export { icosphere } from "./shapes/icosphere.js";
|
|
|
69
75
|
export { octaSphere } from "./shapes/octa-sphere.js";
|
|
70
76
|
export { cubeSphere } from "./shapes/cube-sphere.js";
|
|
71
77
|
export { expandToTriangles } from "./core/geometry.js";
|
|
72
|
-
export type { Mesh, Face, Transform } from "./core/mesh.js";
|
|
78
|
+
export type { Mesh, Face, Transform, Transparency, OneSidedTransparency, TwoSidedTransparency, } from "./core/mesh.js";
|
|
73
79
|
export { transform } from "./core/mesh.js";
|
|
74
80
|
export type { Backend, Renderer, RenderFrame, RenderItem, RendererOptions } from "./renderer.js";
|
|
81
|
+
export { orderRenderItems } from "./renderer.js";
|
|
75
82
|
export { type Vec3, vec3, subtract, cross, dot, scale, normalize, } from "./core/math.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Camera } from "./core/camera.js";
|
|
2
2
|
import { Light } from "./core/light.js";
|
|
3
3
|
import { multiply, rotationX, rotationY, rotationZ, scaleMatrix, translation, } from "./core/math.js";
|
|
4
|
-
import { transform as makeTransform } from "./core/mesh.js";
|
|
5
|
-
import { createRenderer } from "./renderer.js";
|
|
4
|
+
import { transform as makeTransform, } from "./core/mesh.js";
|
|
5
|
+
import { createRenderer, orderRenderItems, } from "./renderer.js";
|
|
6
6
|
function modelMatrix(t) {
|
|
7
7
|
const rotation = multiply(rotationZ(t.rotation.z), multiply(rotationY(t.rotation.y), rotationX(t.rotation.x)));
|
|
8
8
|
return multiply(translation(t.position.x, t.position.y, t.position.z), multiply(rotation, scaleMatrix(t.scale)));
|
|
@@ -75,17 +75,18 @@ export class Little3dEngine {
|
|
|
75
75
|
}
|
|
76
76
|
/** Add a mesh to the scene and return a handle for animating it. */
|
|
77
77
|
add(mesh, init) {
|
|
78
|
-
const entry = {
|
|
79
|
-
this.scene.push(entry);
|
|
80
|
-
return {
|
|
78
|
+
const entry = {
|
|
81
79
|
mesh,
|
|
82
|
-
transform:
|
|
80
|
+
transform: makeTransform(init),
|
|
81
|
+
transparency: init?.transparency,
|
|
83
82
|
remove: () => {
|
|
84
83
|
const i = this.scene.indexOf(entry);
|
|
85
84
|
if (i >= 0)
|
|
86
85
|
this.scene.splice(i, 1);
|
|
87
86
|
},
|
|
88
87
|
};
|
|
88
|
+
this.scene.push(entry);
|
|
89
|
+
return entry;
|
|
89
90
|
}
|
|
90
91
|
resize() {
|
|
91
92
|
const canvas = this.canvas;
|
|
@@ -109,11 +110,13 @@ export class Little3dEngine {
|
|
|
109
110
|
const items = this.scene.map((entry) => ({
|
|
110
111
|
mesh: entry.mesh,
|
|
111
112
|
model: modelMatrix(entry.transform),
|
|
113
|
+
transparency: entry.transparency,
|
|
112
114
|
}));
|
|
115
|
+
const eye = this.camera.options.position;
|
|
113
116
|
this.renderer.render({
|
|
114
|
-
items,
|
|
117
|
+
items: orderRenderItems(items, eye),
|
|
115
118
|
viewProjection: this.camera.viewProjection(width / height),
|
|
116
|
-
eye
|
|
119
|
+
eye,
|
|
117
120
|
light: this.light.params,
|
|
118
121
|
width,
|
|
119
122
|
height,
|
|
@@ -164,4 +167,5 @@ export { octaSphere } from "./shapes/octa-sphere.js";
|
|
|
164
167
|
export { cubeSphere } from "./shapes/cube-sphere.js";
|
|
165
168
|
export { expandToTriangles } from "./core/geometry.js";
|
|
166
169
|
export { transform } from "./core/mesh.js";
|
|
170
|
+
export { orderRenderItems } from "./renderer.js";
|
|
167
171
|
export { vec3, subtract, cross, dot, scale, normalize, } from "./core/math.js";
|
|
@@ -15,8 +15,8 @@ export interface ObjOptions {
|
|
|
15
15
|
* `v`, `v/vt`, `v/vt/vn`, or `v//vn` form, with 1-based or negative indices).
|
|
16
16
|
* Normals (`vn`) and texture coordinates (`vt`) are ignored - the engine
|
|
17
17
|
* computes a flat normal per face - as are groups, materials, and other
|
|
18
|
-
* statements. Face winding is preserved
|
|
19
|
-
*
|
|
18
|
+
* statements. Face winding is preserved as-is; the engine expects CCW
|
|
19
|
+
* winding as seen from outside.
|
|
20
20
|
*
|
|
21
21
|
* @param text Contents of an `.obj` file.
|
|
22
22
|
* @param options Color palette to apply to the faces.
|
|
@@ -10,8 +10,8 @@ function resolveIndex(token, vertexCount) {
|
|
|
10
10
|
* `v`, `v/vt`, `v/vt/vn`, or `v//vn` form, with 1-based or negative indices).
|
|
11
11
|
* Normals (`vn`) and texture coordinates (`vt`) are ignored - the engine
|
|
12
12
|
* computes a flat normal per face - as are groups, materials, and other
|
|
13
|
-
* statements. Face winding is preserved
|
|
14
|
-
*
|
|
13
|
+
* statements. Face winding is preserved as-is; the engine expects CCW
|
|
14
|
+
* winding as seen from outside.
|
|
15
15
|
*
|
|
16
16
|
* @param text Contents of an `.obj` file.
|
|
17
17
|
* @param options Color palette to apply to the faces.
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import type { LightParams } from "./core/light.js";
|
|
2
2
|
import type { Mat4, Vec3 } from "./core/math.js";
|
|
3
|
-
import type { Mesh } from "./core/mesh.js";
|
|
3
|
+
import type { Mesh, Transparency, TwoSidedTransparency } from "./core/mesh.js";
|
|
4
4
|
/** Rendering backend. Each is loaded on demand; unused ones are never fetched. */
|
|
5
5
|
export type Backend = "canvas2d" | "webgl" | "webgpu";
|
|
6
6
|
/** A mesh plus its world transform, ready to draw. */
|
|
7
7
|
export interface RenderItem {
|
|
8
8
|
mesh: Mesh;
|
|
9
9
|
model: Mat4;
|
|
10
|
+
transparency?: Transparency;
|
|
10
11
|
}
|
|
12
|
+
export declare const DEFAULT_ONE_SIDED_OPACITY = 0.35;
|
|
13
|
+
export declare const DEFAULT_BACK_OPACITY = 0.84;
|
|
14
|
+
export declare const DEFAULT_FRONT_OPACITY = 0.56;
|
|
15
|
+
/** Clamp an optional opacity to the range accepted by rendering backends. */
|
|
16
|
+
export declare function opacity(value: number | undefined, fallback: number): number;
|
|
17
|
+
/** Resolve two-sided defaults, shorthand, and explicit per-side overrides. */
|
|
18
|
+
export declare function resolveTwoSidedOpacity(transparency: TwoSidedTransparency): {
|
|
19
|
+
front: number;
|
|
20
|
+
back: number;
|
|
21
|
+
};
|
|
22
|
+
/** Draw opaque instances first, then transparent instances from farthest to nearest. */
|
|
23
|
+
export declare function orderRenderItems(items: ReadonlyArray<RenderItem>, eye: Vec3): RenderItem[];
|
|
11
24
|
/** Everything a renderer needs to draw one frame. */
|
|
12
25
|
export interface RenderFrame {
|
|
13
26
|
items: ReadonlyArray<RenderItem>;
|
|
@@ -28,6 +41,7 @@ export interface RendererOptions {
|
|
|
28
41
|
/**
|
|
29
42
|
* A pluggable drawing backend. The engine owns the canvas and sizing; a
|
|
30
43
|
* renderer only initializes its context, reacts to resizes, and draws frames.
|
|
44
|
+
* Renderer-specific features can produce intentional visual differences.
|
|
31
45
|
*/
|
|
32
46
|
export interface Renderer {
|
|
33
47
|
init(canvas: HTMLCanvasElement): void | Promise<void>;
|
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
export const DEFAULT_ONE_SIDED_OPACITY = 0.35;
|
|
2
|
+
export const DEFAULT_BACK_OPACITY = 0.84;
|
|
3
|
+
export const DEFAULT_FRONT_OPACITY = 0.56;
|
|
4
|
+
/** Clamp an optional opacity to the range accepted by rendering backends. */
|
|
5
|
+
export function opacity(value, fallback) {
|
|
6
|
+
return Math.max(0, Math.min(1, value ?? fallback));
|
|
7
|
+
}
|
|
8
|
+
/** Resolve two-sided defaults, shorthand, and explicit per-side overrides. */
|
|
9
|
+
export function resolveTwoSidedOpacity(transparency) {
|
|
10
|
+
const front = opacity(transparency.frontOpacity ?? transparency.opacity, DEFAULT_FRONT_OPACITY);
|
|
11
|
+
const backFallback = transparency.opacity === undefined
|
|
12
|
+
? DEFAULT_BACK_OPACITY
|
|
13
|
+
: front * (2 / 3);
|
|
14
|
+
return {
|
|
15
|
+
front,
|
|
16
|
+
back: opacity(transparency.backOpacity, backFallback),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Draw opaque instances first, then transparent instances from farthest to nearest. */
|
|
20
|
+
export function orderRenderItems(items, eye) {
|
|
21
|
+
const opaque = [];
|
|
22
|
+
const transparent = [];
|
|
23
|
+
for (const item of items) {
|
|
24
|
+
(item.transparency ? transparent : opaque).push(item);
|
|
25
|
+
}
|
|
26
|
+
transparent.sort((a, b) => {
|
|
27
|
+
const ax = a.model[12] - eye.x;
|
|
28
|
+
const ay = a.model[13] - eye.y;
|
|
29
|
+
const az = a.model[14] - eye.z;
|
|
30
|
+
const bx = b.model[12] - eye.x;
|
|
31
|
+
const by = b.model[13] - eye.y;
|
|
32
|
+
const bz = b.model[14] - eye.z;
|
|
33
|
+
return bx * bx + by * by + bz * bz - (ax * ax + ay * ay + az * az);
|
|
34
|
+
});
|
|
35
|
+
return opaque.concat(transparent);
|
|
36
|
+
}
|
|
1
37
|
/**
|
|
2
38
|
* Load and construct a renderer for `backend`. Each backend lives in its own
|
|
3
39
|
* module and is pulled in with a dynamic `import()`, so the bytes for the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Renderer, type RenderFrame, type RendererOptions } from "../renderer.js";
|
|
2
2
|
/** Software renderer: projects geometry on the CPU and fills 2D polygons. */
|
|
3
3
|
export declare class Canvas2DRenderer implements Renderer {
|
|
4
4
|
private readonly options;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { shadeColor } from "../core/light.js";
|
|
2
2
|
import { dot, cross, normalize, subtract, transformAffine, transformPoint } from "../core/math.js";
|
|
3
|
+
import { DEFAULT_ONE_SIDED_OPACITY, opacity, resolveTwoSidedOpacity, } from "../renderer.js";
|
|
3
4
|
/** Software renderer: projects geometry on the CPU and fills 2D polygons. */
|
|
4
5
|
export class Canvas2DRenderer {
|
|
5
6
|
constructor(options = {}) {
|
|
@@ -27,13 +28,25 @@ export class Canvas2DRenderer {
|
|
|
27
28
|
const polygons = [];
|
|
28
29
|
for (const item of frame.items) {
|
|
29
30
|
const world = item.mesh.vertices.map((v) => transformAffine(item.model, v));
|
|
31
|
+
const twoSidedOpacity = item.transparency?.mode === "two-sided"
|
|
32
|
+
? resolveTwoSidedOpacity(item.transparency)
|
|
33
|
+
: undefined;
|
|
30
34
|
for (const face of item.mesh.faces) {
|
|
31
35
|
const a = world[face.indices[0]];
|
|
32
36
|
const b = world[face.indices[1]];
|
|
33
37
|
const c = world[face.indices[2]];
|
|
34
38
|
const normal = normalize(cross(subtract(b, a), subtract(c, a)));
|
|
35
|
-
|
|
39
|
+
const frontFacing = dot(normal, subtract(frame.eye, a)) > 0;
|
|
40
|
+
const transparency = item.transparency;
|
|
41
|
+
if (!frontFacing && transparency?.mode !== "two-sided")
|
|
36
42
|
continue;
|
|
43
|
+
let faceOpacity = 1;
|
|
44
|
+
if (transparency?.mode === "one-sided") {
|
|
45
|
+
faceOpacity = opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY);
|
|
46
|
+
}
|
|
47
|
+
else if (twoSidedOpacity) {
|
|
48
|
+
faceOpacity = frontFacing ? twoSidedOpacity.front : twoSidedOpacity.back;
|
|
49
|
+
}
|
|
37
50
|
const points = face.indices.map((i) => {
|
|
38
51
|
const ndc = transformPoint(frame.viewProjection, world[i]);
|
|
39
52
|
return {
|
|
@@ -47,7 +60,12 @@ export class Canvas2DRenderer {
|
|
|
47
60
|
depth += dot(d, d);
|
|
48
61
|
}
|
|
49
62
|
depth /= face.indices.length;
|
|
50
|
-
polygons.push({
|
|
63
|
+
polygons.push({
|
|
64
|
+
points,
|
|
65
|
+
color: shadeColor(normal, face.color, frame.light),
|
|
66
|
+
depth,
|
|
67
|
+
opacity: faceOpacity,
|
|
68
|
+
});
|
|
51
69
|
}
|
|
52
70
|
}
|
|
53
71
|
polygons.sort((p, q) => q.depth - p.depth);
|
|
@@ -63,9 +81,11 @@ export class Canvas2DRenderer {
|
|
|
63
81
|
ctx.fillStyle = poly.color;
|
|
64
82
|
ctx.strokeStyle = poly.color;
|
|
65
83
|
ctx.lineWidth = 1;
|
|
84
|
+
ctx.globalAlpha = poly.opacity;
|
|
66
85
|
ctx.fill();
|
|
67
86
|
ctx.stroke();
|
|
68
87
|
}
|
|
88
|
+
ctx.globalAlpha = 1;
|
|
69
89
|
}
|
|
70
90
|
destroy() {
|
|
71
91
|
this.ctx = undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Renderer, type RenderFrame, type RendererOptions } from "../renderer.js";
|
|
2
2
|
/** Hardware renderer using WebGL2: GPU transforms with a real depth buffer. */
|
|
3
3
|
export declare class WebGLRenderer implements Renderer {
|
|
4
4
|
private gl?;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expandToTriangles, parseColor } from "../core/geometry.js";
|
|
2
|
+
import { DEFAULT_ONE_SIDED_OPACITY, opacity, resolveTwoSidedOpacity, } from "../renderer.js";
|
|
2
3
|
const VERTEX_SHADER = `#version 300 es
|
|
3
4
|
in vec3 aPos;
|
|
4
5
|
in vec3 aNormal;
|
|
@@ -19,11 +20,12 @@ in vec3 vColor;
|
|
|
19
20
|
uniform vec3 uToLight;
|
|
20
21
|
uniform float uIntensity;
|
|
21
22
|
uniform float uAmbient;
|
|
23
|
+
uniform float uOpacity;
|
|
22
24
|
out vec4 fragColor;
|
|
23
25
|
void main() {
|
|
24
26
|
float lambert = max(dot(normalize(vNormal), normalize(uToLight)), 0.0);
|
|
25
27
|
float brightness = clamp(uAmbient + uIntensity * lambert, 0.0, 1.0);
|
|
26
|
-
fragColor = vec4(vColor * brightness,
|
|
28
|
+
fragColor = vec4(vColor * brightness, uOpacity);
|
|
27
29
|
}`;
|
|
28
30
|
function compile(gl, type, source) {
|
|
29
31
|
const shader = gl.createShader(type);
|
|
@@ -71,6 +73,7 @@ export class WebGLRenderer {
|
|
|
71
73
|
uToLight: gl.getUniformLocation(this.program, "uToLight"),
|
|
72
74
|
uIntensity: gl.getUniformLocation(this.program, "uIntensity"),
|
|
73
75
|
uAmbient: gl.getUniformLocation(this.program, "uAmbient"),
|
|
76
|
+
uOpacity: gl.getUniformLocation(this.program, "uOpacity"),
|
|
74
77
|
};
|
|
75
78
|
gl.enable(gl.DEPTH_TEST);
|
|
76
79
|
gl.enable(gl.CULL_FACE);
|
|
@@ -122,12 +125,46 @@ export class WebGLRenderer {
|
|
|
122
125
|
gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
|
|
123
126
|
gl.uniform1f(loc.uIntensity, frame.light.intensity);
|
|
124
127
|
gl.uniform1f(loc.uAmbient, frame.light.ambient);
|
|
128
|
+
gl.disable(gl.BLEND);
|
|
129
|
+
gl.depthMask(true);
|
|
130
|
+
gl.cullFace(gl.BACK);
|
|
125
131
|
for (const item of frame.items) {
|
|
132
|
+
if (item.transparency)
|
|
133
|
+
continue;
|
|
126
134
|
const mesh = this.buffers(item.mesh);
|
|
127
135
|
gl.uniformMatrix4fv(loc.uModel, false, new Float32Array(item.model));
|
|
136
|
+
gl.uniform1f(loc.uOpacity, 1);
|
|
128
137
|
gl.bindVertexArray(mesh.vao);
|
|
129
138
|
gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
|
|
130
139
|
}
|
|
140
|
+
gl.enable(gl.BLEND);
|
|
141
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
142
|
+
gl.depthMask(false);
|
|
143
|
+
for (const item of frame.items) {
|
|
144
|
+
const transparency = item.transparency;
|
|
145
|
+
if (!transparency)
|
|
146
|
+
continue;
|
|
147
|
+
const mesh = this.buffers(item.mesh);
|
|
148
|
+
gl.uniformMatrix4fv(loc.uModel, false, new Float32Array(item.model));
|
|
149
|
+
gl.bindVertexArray(mesh.vao);
|
|
150
|
+
if (transparency.mode === "two-sided") {
|
|
151
|
+
const resolved = resolveTwoSidedOpacity(transparency);
|
|
152
|
+
gl.cullFace(gl.FRONT);
|
|
153
|
+
gl.uniform1f(loc.uOpacity, resolved.back);
|
|
154
|
+
gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
|
|
155
|
+
gl.cullFace(gl.BACK);
|
|
156
|
+
gl.uniform1f(loc.uOpacity, resolved.front);
|
|
157
|
+
gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
gl.cullFace(gl.BACK);
|
|
161
|
+
gl.uniform1f(loc.uOpacity, opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY));
|
|
162
|
+
gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
gl.depthMask(true);
|
|
166
|
+
gl.disable(gl.BLEND);
|
|
167
|
+
gl.cullFace(gl.BACK);
|
|
131
168
|
gl.bindVertexArray(null);
|
|
132
169
|
}
|
|
133
170
|
destroy() {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Renderer, type RenderFrame, type RendererOptions } from "../renderer.js";
|
|
2
2
|
/** Hardware renderer using WebGPU: GPU transforms with a real depth buffer. */
|
|
3
3
|
export declare class WebGPURenderer implements Renderer {
|
|
4
4
|
private canvas?;
|
|
5
5
|
private device;
|
|
6
6
|
private context;
|
|
7
7
|
private pipeline;
|
|
8
|
+
private transparentBackPipeline;
|
|
9
|
+
private transparentFrontPipeline;
|
|
8
10
|
private uniformBuffer;
|
|
9
11
|
private uniformCapacity;
|
|
10
12
|
private depthTexture;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { expandToTriangles, parseColor } from "../core/geometry.js";
|
|
2
2
|
import { multiply } from "../core/math.js";
|
|
3
|
+
import { DEFAULT_ONE_SIDED_OPACITY, opacity, resolveTwoSidedOpacity, } from "../renderer.js";
|
|
3
4
|
const WGSL = `
|
|
4
5
|
struct Uniforms {
|
|
5
6
|
viewProj: mat4x4<f32>,
|
|
@@ -29,7 +30,7 @@ fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2)
|
|
|
29
30
|
fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
30
31
|
let lambert = max(dot(normalize(in.normal), normalize(u.toLight.xyz)), 0.0);
|
|
31
32
|
let brightness = clamp(u.params.y + u.params.x * lambert, 0.0, 1.0);
|
|
32
|
-
return vec4<f32>(in.color * brightness,
|
|
33
|
+
return vec4<f32>(in.color * brightness, u.params.z);
|
|
33
34
|
}
|
|
34
35
|
`;
|
|
35
36
|
// Maps OpenGL clip space (z in -1..1) to WebGPU clip space (z in 0..1).
|
|
@@ -84,17 +85,37 @@ export class WebGPURenderer {
|
|
|
84
85
|
arrayStride: 12,
|
|
85
86
|
attributes: [{ shaderLocation: location, offset: 0, format: "float32x3" }],
|
|
86
87
|
});
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
|
|
89
|
+
const blend = {
|
|
90
|
+
color: {
|
|
91
|
+
srcFactor: "src-alpha",
|
|
92
|
+
dstFactor: "one-minus-src-alpha",
|
|
93
|
+
operation: "add",
|
|
94
|
+
},
|
|
95
|
+
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
96
|
+
};
|
|
97
|
+
const pipeline = (cullMode, transparent) => device.createRenderPipeline({
|
|
98
|
+
layout: pipelineLayout,
|
|
89
99
|
vertex: {
|
|
90
100
|
module,
|
|
91
101
|
entryPoint: "vs",
|
|
92
102
|
buffers: [vertexBuffer(0), vertexBuffer(1), vertexBuffer(2)],
|
|
93
103
|
},
|
|
94
|
-
fragment: {
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
fragment: {
|
|
105
|
+
module,
|
|
106
|
+
entryPoint: "fs",
|
|
107
|
+
targets: [{ format, ...(transparent ? { blend } : {}) }],
|
|
108
|
+
},
|
|
109
|
+
primitive: { topology: "triangle-list", cullMode, frontFace: "ccw" },
|
|
110
|
+
depthStencil: {
|
|
111
|
+
format: "depth24plus",
|
|
112
|
+
depthWriteEnabled: !transparent,
|
|
113
|
+
depthCompare: "less",
|
|
114
|
+
},
|
|
97
115
|
});
|
|
116
|
+
this.pipeline = pipeline("back", false);
|
|
117
|
+
this.transparentBackPipeline = pipeline("front", true);
|
|
118
|
+
this.transparentFrontPipeline = pipeline("back", true);
|
|
98
119
|
this.canvas = canvas;
|
|
99
120
|
this.device = device;
|
|
100
121
|
this.context = context;
|
|
@@ -139,15 +160,15 @@ export class WebGPURenderer {
|
|
|
139
160
|
this.cache.set(mesh, result);
|
|
140
161
|
return result;
|
|
141
162
|
}
|
|
142
|
-
ensureUniformCapacity(
|
|
143
|
-
if (
|
|
163
|
+
ensureUniformCapacity(draws) {
|
|
164
|
+
if (draws <= this.uniformCapacity && this.uniformBuffer)
|
|
144
165
|
return;
|
|
145
166
|
this.uniformBuffer?.destroy?.();
|
|
146
167
|
this.uniformBuffer = this.device.createBuffer({
|
|
147
|
-
size: Math.max(1,
|
|
168
|
+
size: Math.max(1, draws) * UNIFORM_STRIDE,
|
|
148
169
|
usage: globalThis.GPUBufferUsage.UNIFORM | globalThis.GPUBufferUsage.COPY_DST,
|
|
149
170
|
});
|
|
150
|
-
this.uniformCapacity =
|
|
171
|
+
this.uniformCapacity = draws;
|
|
151
172
|
}
|
|
152
173
|
render(frame) {
|
|
153
174
|
if (this.destroyed || !this.device || !this.context || !this.pipeline)
|
|
@@ -155,7 +176,37 @@ export class WebGPURenderer {
|
|
|
155
176
|
if (frame.width === 0 || frame.height === 0 || frame.items.length === 0)
|
|
156
177
|
return;
|
|
157
178
|
this.ensureDepth();
|
|
158
|
-
|
|
179
|
+
const draws = [];
|
|
180
|
+
for (const item of frame.items) {
|
|
181
|
+
if (!item.transparency)
|
|
182
|
+
draws.push({ item, opacity: 1, pipeline: this.pipeline });
|
|
183
|
+
}
|
|
184
|
+
for (const item of frame.items) {
|
|
185
|
+
const transparency = item.transparency;
|
|
186
|
+
if (!transparency)
|
|
187
|
+
continue;
|
|
188
|
+
if (transparency.mode === "two-sided") {
|
|
189
|
+
const resolved = resolveTwoSidedOpacity(transparency);
|
|
190
|
+
draws.push({
|
|
191
|
+
item,
|
|
192
|
+
opacity: resolved.back,
|
|
193
|
+
pipeline: this.transparentBackPipeline,
|
|
194
|
+
});
|
|
195
|
+
draws.push({
|
|
196
|
+
item,
|
|
197
|
+
opacity: resolved.front,
|
|
198
|
+
pipeline: this.transparentFrontPipeline,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
draws.push({
|
|
203
|
+
item,
|
|
204
|
+
opacity: opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY),
|
|
205
|
+
pipeline: this.transparentFrontPipeline,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
this.ensureUniformCapacity(draws.length);
|
|
159
210
|
const viewProj = multiply(CLIP_Z_FIX, frame.viewProjection);
|
|
160
211
|
const layout = this.pipeline.getBindGroupLayout(0);
|
|
161
212
|
const bindGroup = this.device.createBindGroup({
|
|
@@ -164,12 +215,12 @@ export class WebGPURenderer {
|
|
|
164
215
|
{ binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 160 } },
|
|
165
216
|
],
|
|
166
217
|
});
|
|
167
|
-
|
|
218
|
+
draws.forEach((draw, i) => {
|
|
168
219
|
const data = new Float32Array(UNIFORM_STRIDE / 4);
|
|
169
220
|
data.set(viewProj, 0);
|
|
170
|
-
data.set(item.model, 16);
|
|
221
|
+
data.set(draw.item.model, 16);
|
|
171
222
|
data.set([frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z, 0], 32);
|
|
172
|
-
data.set([frame.light.intensity, frame.light.ambient,
|
|
223
|
+
data.set([frame.light.intensity, frame.light.ambient, draw.opacity, 0], 36);
|
|
173
224
|
this.device.queue.writeBuffer(this.uniformBuffer, i * UNIFORM_STRIDE, data);
|
|
174
225
|
});
|
|
175
226
|
const encoder = this.device.createCommandEncoder();
|
|
@@ -189,9 +240,9 @@ export class WebGPURenderer {
|
|
|
189
240
|
depthStoreOp: "store",
|
|
190
241
|
},
|
|
191
242
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
243
|
+
draws.forEach((draw, i) => {
|
|
244
|
+
const mesh = this.buffers(draw.item.mesh);
|
|
245
|
+
pass.setPipeline(draw.pipeline);
|
|
195
246
|
pass.setBindGroup(0, bindGroup, [i * UNIFORM_STRIDE]);
|
|
196
247
|
pass.setVertexBuffer(0, mesh.position);
|
|
197
248
|
pass.setVertexBuffer(1, mesh.normal);
|
|
@@ -215,6 +266,8 @@ export class WebGPURenderer {
|
|
|
215
266
|
this.device = undefined;
|
|
216
267
|
this.context = undefined;
|
|
217
268
|
this.pipeline = undefined;
|
|
269
|
+
this.transparentBackPipeline = undefined;
|
|
270
|
+
this.transparentFrontPipeline = undefined;
|
|
218
271
|
this.uniformBuffer = undefined;
|
|
219
272
|
this.depthTexture = undefined;
|
|
220
273
|
this.canvas = undefined;
|