3d-spinner 0.9.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/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/animation.d.ts +28 -0
- package/dist/animation.js +1 -0
- package/dist/animations/object-motion.d.ts +96 -0
- package/dist/animations/object-motion.js +339 -0
- package/dist/animations/spin.d.ts +44 -0
- package/dist/animations/spin.js +108 -0
- package/dist/engines/little-3d-engine/core/camera.d.ts +26 -0
- package/dist/engines/little-3d-engine/core/camera.js +32 -0
- package/dist/engines/little-3d-engine/core/geometry.d.ts +27 -0
- package/dist/engines/little-3d-engine/core/geometry.js +87 -0
- package/dist/engines/little-3d-engine/core/light.d.ts +31 -0
- package/dist/engines/little-3d-engine/core/light.js +35 -0
- package/dist/engines/little-3d-engine/core/math.d.ts +50 -0
- package/dist/engines/little-3d-engine/core/math.js +109 -0
- package/dist/engines/little-3d-engine/core/mesh.d.ts +22 -0
- package/dist/engines/little-3d-engine/core/mesh.js +8 -0
- package/dist/engines/little-3d-engine/little-3d-engine.d.ts +75 -0
- package/dist/engines/little-3d-engine/little-3d-engine.js +167 -0
- package/dist/engines/little-3d-engine/loaders/obj.d.ts +24 -0
- package/dist/engines/little-3d-engine/loaders/obj.js +48 -0
- package/dist/engines/little-3d-engine/renderer.d.ts +43 -0
- package/dist/engines/little-3d-engine/renderer.js +16 -0
- package/dist/engines/little-3d-engine/renderers/canvas2d.d.ts +12 -0
- package/dist/engines/little-3d-engine/renderers/canvas2d.js +73 -0
- package/dist/engines/little-3d-engine/renderers/webgl.d.ts +15 -0
- package/dist/engines/little-3d-engine/renderers/webgl.js +146 -0
- package/dist/engines/little-3d-engine/renderers/webgpu.d.ts +24 -0
- package/dist/engines/little-3d-engine/renderers/webgpu.js +222 -0
- package/dist/engines/little-3d-engine/shapes/cube-sphere.d.ts +11 -0
- package/dist/engines/little-3d-engine/shapes/cube-sphere.js +50 -0
- package/dist/engines/little-3d-engine/shapes/cube.d.ts +9 -0
- package/dist/engines/little-3d-engine/shapes/cube.js +30 -0
- package/dist/engines/little-3d-engine/shapes/icosphere.d.ts +11 -0
- package/dist/engines/little-3d-engine/shapes/icosphere.js +51 -0
- package/dist/engines/little-3d-engine/shapes/octa-sphere.d.ts +10 -0
- package/dist/engines/little-3d-engine/shapes/octa-sphere.js +31 -0
- package/dist/engines/little-3d-engine/shapes/octahedron.d.ts +8 -0
- package/dist/engines/little-3d-engine/shapes/octahedron.js +38 -0
- package/dist/engines/little-3d-engine/shapes/pyramid.d.ts +9 -0
- package/dist/engines/little-3d-engine/shapes/pyramid.js +26 -0
- package/dist/engines/little-3d-engine/shapes/tetrahedron.d.ts +8 -0
- package/dist/engines/little-3d-engine/shapes/tetrahedron.js +23 -0
- package/dist/engines/little-3d-engine/shapes/uv-sphere.d.ts +10 -0
- package/dist/engines/little-3d-engine/shapes/uv-sphere.js +50 -0
- package/dist/engines/little-tween-engine/core/tweens.d.ts +39 -0
- package/dist/engines/little-tween-engine/core/tweens.js +231 -0
- package/dist/engines/little-tween-engine/little-tween-engine.d.ts +21 -0
- package/dist/engines/little-tween-engine/little-tween-engine.js +16 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +110 -0
- package/dist/motion/circle.d.ts +17 -0
- package/dist/motion/circle.js +20 -0
- package/dist/motion/controller.d.ts +13 -0
- package/dist/motion/controller.js +1 -0
- package/dist/motion/figure-eight.d.ts +13 -0
- package/dist/motion/figure-eight.js +23 -0
- package/dist/motion/motion.d.ts +5 -0
- package/dist/motion/motion.js +4 -0
- package/dist/motion/square.d.ts +17 -0
- package/dist/motion/square.js +39 -0
- package/dist/motion/transitions.d.ts +44 -0
- package/dist/motion/transitions.js +60 -0
- package/dist/motion/wander.d.ts +22 -0
- package/dist/motion/wander.js +50 -0
- package/dist/progress-animation.d.ts +55 -0
- package/dist/progress-animation.js +128 -0
- package/package.json +74 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Camera } from "./core/camera.js";
|
|
2
|
+
import { Light } from "./core/light.js";
|
|
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";
|
|
6
|
+
function modelMatrix(t) {
|
|
7
|
+
const rotation = multiply(rotationZ(t.rotation.z), multiply(rotationY(t.rotation.y), rotationX(t.rotation.x)));
|
|
8
|
+
return multiply(translation(t.position.x, t.position.y, t.position.z), multiply(rotation, scaleMatrix(t.scale)));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A minimal software/hardware 3D engine. It projects colored meshes with flat
|
|
12
|
+
* directional lighting through a swappable {@link Backend} renderer. Mount it
|
|
13
|
+
* into any element to render in a component, or into a transparent positioned
|
|
14
|
+
* element to overlay a page.
|
|
15
|
+
*/
|
|
16
|
+
export class Little3dEngine {
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.scene = [];
|
|
19
|
+
this.cssWidth = 0;
|
|
20
|
+
this.cssHeight = 0;
|
|
21
|
+
this.ready = false;
|
|
22
|
+
this.generation = 0;
|
|
23
|
+
this.rafId = 0;
|
|
24
|
+
this.running = false;
|
|
25
|
+
this.camera = new Camera(options.camera);
|
|
26
|
+
this.light = new Light(options.light);
|
|
27
|
+
this.backend = options.backend ?? "canvas2d";
|
|
28
|
+
this.background = options.background;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create the canvas inside `target`, load the selected backend, and start
|
|
32
|
+
* tracking size. Resolves once the renderer is ready; rejects if the backend
|
|
33
|
+
* is unavailable. Drawing is a no-op until it resolves.
|
|
34
|
+
*/
|
|
35
|
+
async mount(target) {
|
|
36
|
+
const canvas = document.createElement("canvas");
|
|
37
|
+
canvas.style.display = "block";
|
|
38
|
+
canvas.style.width = "100%";
|
|
39
|
+
canvas.style.height = "100%";
|
|
40
|
+
target.appendChild(canvas);
|
|
41
|
+
this.canvas = canvas;
|
|
42
|
+
this.observer = new ResizeObserver(() => this.resize());
|
|
43
|
+
this.observer.observe(canvas);
|
|
44
|
+
this.resize();
|
|
45
|
+
const generation = this.generation;
|
|
46
|
+
const dropCanvas = () => {
|
|
47
|
+
if (this.canvas !== canvas)
|
|
48
|
+
return;
|
|
49
|
+
this.observer?.disconnect();
|
|
50
|
+
this.observer = undefined;
|
|
51
|
+
canvas.remove();
|
|
52
|
+
this.canvas = undefined;
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
const renderer = await createRenderer(this.backend, { background: this.background });
|
|
56
|
+
if (generation !== this.generation) {
|
|
57
|
+
renderer.destroy();
|
|
58
|
+
dropCanvas();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await renderer.init(canvas);
|
|
62
|
+
if (generation !== this.generation) {
|
|
63
|
+
renderer.destroy();
|
|
64
|
+
dropCanvas();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.renderer = renderer;
|
|
68
|
+
this.resize();
|
|
69
|
+
this.ready = true;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
dropCanvas();
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Add a mesh to the scene and return a handle for animating it. */
|
|
77
|
+
add(mesh, init) {
|
|
78
|
+
const entry = { mesh, transform: makeTransform(init) };
|
|
79
|
+
this.scene.push(entry);
|
|
80
|
+
return {
|
|
81
|
+
mesh,
|
|
82
|
+
transform: entry.transform,
|
|
83
|
+
remove: () => {
|
|
84
|
+
const i = this.scene.indexOf(entry);
|
|
85
|
+
if (i >= 0)
|
|
86
|
+
this.scene.splice(i, 1);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
resize() {
|
|
91
|
+
const canvas = this.canvas;
|
|
92
|
+
if (!canvas)
|
|
93
|
+
return;
|
|
94
|
+
const dpr = window.devicePixelRatio || 1;
|
|
95
|
+
this.cssWidth = canvas.clientWidth || canvas.parentElement?.clientWidth || 0;
|
|
96
|
+
this.cssHeight = canvas.clientHeight || canvas.parentElement?.clientHeight || 0;
|
|
97
|
+
canvas.width = Math.max(1, Math.round(this.cssWidth * dpr));
|
|
98
|
+
canvas.height = Math.max(1, Math.round(this.cssHeight * dpr));
|
|
99
|
+
this.renderer?.resize(this.cssWidth, this.cssHeight, dpr);
|
|
100
|
+
}
|
|
101
|
+
/** Draw a single frame from the current scene state. */
|
|
102
|
+
render() {
|
|
103
|
+
if (!this.ready || !this.renderer)
|
|
104
|
+
return;
|
|
105
|
+
const width = this.cssWidth;
|
|
106
|
+
const height = this.cssHeight;
|
|
107
|
+
if (width === 0 || height === 0)
|
|
108
|
+
return;
|
|
109
|
+
const items = this.scene.map((entry) => ({
|
|
110
|
+
mesh: entry.mesh,
|
|
111
|
+
model: modelMatrix(entry.transform),
|
|
112
|
+
}));
|
|
113
|
+
this.renderer.render({
|
|
114
|
+
items,
|
|
115
|
+
viewProjection: this.camera.viewProjection(width / height),
|
|
116
|
+
eye: this.camera.options.position,
|
|
117
|
+
light: this.light.params,
|
|
118
|
+
width,
|
|
119
|
+
height,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** Start an internal animation loop that calls {@link render} each frame. */
|
|
123
|
+
start() {
|
|
124
|
+
if (this.running)
|
|
125
|
+
return;
|
|
126
|
+
this.running = true;
|
|
127
|
+
const loop = () => {
|
|
128
|
+
if (!this.running)
|
|
129
|
+
return;
|
|
130
|
+
this.render();
|
|
131
|
+
this.rafId = requestAnimationFrame(loop);
|
|
132
|
+
};
|
|
133
|
+
this.rafId = requestAnimationFrame(loop);
|
|
134
|
+
}
|
|
135
|
+
/** Stop the internal animation loop started by {@link start}. */
|
|
136
|
+
stop() {
|
|
137
|
+
this.running = false;
|
|
138
|
+
if (this.rafId)
|
|
139
|
+
cancelAnimationFrame(this.rafId);
|
|
140
|
+
this.rafId = 0;
|
|
141
|
+
}
|
|
142
|
+
/** Stop animating, release the renderer, and remove the canvas. */
|
|
143
|
+
destroy() {
|
|
144
|
+
this.generation++;
|
|
145
|
+
this.ready = false;
|
|
146
|
+
this.stop();
|
|
147
|
+
this.observer?.disconnect();
|
|
148
|
+
this.observer = undefined;
|
|
149
|
+
this.renderer?.destroy();
|
|
150
|
+
this.renderer = undefined;
|
|
151
|
+
this.canvas?.remove();
|
|
152
|
+
this.canvas = undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export { Camera } from "./core/camera.js";
|
|
156
|
+
export { Light } from "./core/light.js";
|
|
157
|
+
export { cube } from "./shapes/cube.js";
|
|
158
|
+
export { tetrahedron } from "./shapes/tetrahedron.js";
|
|
159
|
+
export { octahedron } from "./shapes/octahedron.js";
|
|
160
|
+
export { pyramid } from "./shapes/pyramid.js";
|
|
161
|
+
export { uvSphere } from "./shapes/uv-sphere.js";
|
|
162
|
+
export { icosphere } from "./shapes/icosphere.js";
|
|
163
|
+
export { octaSphere } from "./shapes/octa-sphere.js";
|
|
164
|
+
export { cubeSphere } from "./shapes/cube-sphere.js";
|
|
165
|
+
export { expandToTriangles } from "./core/geometry.js";
|
|
166
|
+
export { transform } from "./core/mesh.js";
|
|
167
|
+
export { vec3, subtract, cross, dot, scale, normalize, } from "./core/math.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Mesh } from "../core/mesh.js";
|
|
2
|
+
/** Options for {@link parseObj}. */
|
|
3
|
+
export interface ObjOptions {
|
|
4
|
+
/**
|
|
5
|
+
* CSS colors assigned to faces in order, cycling when there are more faces
|
|
6
|
+
* than colors. Defaults to a built-in palette. Pass a single-entry array for
|
|
7
|
+
* a uniform color.
|
|
8
|
+
*/
|
|
9
|
+
colors?: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parse Wavefront OBJ text into a {@link Mesh}.
|
|
13
|
+
*
|
|
14
|
+
* Reads `v` vertex positions and `f` faces (triangles, quads, or n-gons, in
|
|
15
|
+
* `v`, `v/vt`, `v/vt/vn`, or `v//vn` form, with 1-based or negative indices).
|
|
16
|
+
* Normals (`vn`) and texture coordinates (`vt`) are ignored - the engine
|
|
17
|
+
* computes a flat normal per face - as are groups, materials, and other
|
|
18
|
+
* statements. Face winding is preserved, so models should be wound CCW as seen
|
|
19
|
+
* from outside for correct backface culling and shading.
|
|
20
|
+
*
|
|
21
|
+
* @param text Contents of an `.obj` file.
|
|
22
|
+
* @param options Color palette to apply to the faces.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseObj(text: string, options?: ObjOptions): Mesh;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
2
|
+
function resolveIndex(token, vertexCount) {
|
|
3
|
+
const n = parseInt(token, 10);
|
|
4
|
+
return n < 0 ? vertexCount + n : n - 1;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Parse Wavefront OBJ text into a {@link Mesh}.
|
|
8
|
+
*
|
|
9
|
+
* Reads `v` vertex positions and `f` faces (triangles, quads, or n-gons, in
|
|
10
|
+
* `v`, `v/vt`, `v/vt/vn`, or `v//vn` form, with 1-based or negative indices).
|
|
11
|
+
* Normals (`vn`) and texture coordinates (`vt`) are ignored - the engine
|
|
12
|
+
* computes a flat normal per face - as are groups, materials, and other
|
|
13
|
+
* statements. Face winding is preserved, so models should be wound CCW as seen
|
|
14
|
+
* from outside for correct backface culling and shading.
|
|
15
|
+
*
|
|
16
|
+
* @param text Contents of an `.obj` file.
|
|
17
|
+
* @param options Color palette to apply to the faces.
|
|
18
|
+
*/
|
|
19
|
+
export function parseObj(text, options = {}) {
|
|
20
|
+
const colors = options.colors ?? DEFAULT_COLORS;
|
|
21
|
+
const vertices = [];
|
|
22
|
+
const faces = [];
|
|
23
|
+
for (const line of text.split("\n")) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
26
|
+
continue;
|
|
27
|
+
const parts = trimmed.split(/\s+/);
|
|
28
|
+
const keyword = parts[0];
|
|
29
|
+
if (keyword === "v") {
|
|
30
|
+
vertices.push({
|
|
31
|
+
x: parseFloat(parts[1]),
|
|
32
|
+
y: parseFloat(parts[2]),
|
|
33
|
+
z: parseFloat(parts[3]),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else if (keyword === "f") {
|
|
37
|
+
const indices = [];
|
|
38
|
+
for (let i = 1; i < parts.length; i++) {
|
|
39
|
+
const vertexToken = parts[i].split("/")[0];
|
|
40
|
+
indices.push(resolveIndex(vertexToken, vertices.length));
|
|
41
|
+
}
|
|
42
|
+
if (indices.length >= 3) {
|
|
43
|
+
faces.push({ indices, color: colors[faces.length % colors.length] });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { vertices, faces };
|
|
48
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { LightParams } from "./core/light.js";
|
|
2
|
+
import type { Mat4, Vec3 } from "./core/math.js";
|
|
3
|
+
import type { Mesh } from "./core/mesh.js";
|
|
4
|
+
/** Rendering backend. Each is loaded on demand; unused ones are never fetched. */
|
|
5
|
+
export type Backend = "canvas2d" | "webgl" | "webgpu";
|
|
6
|
+
/** A mesh plus its world transform, ready to draw. */
|
|
7
|
+
export interface RenderItem {
|
|
8
|
+
mesh: Mesh;
|
|
9
|
+
model: Mat4;
|
|
10
|
+
}
|
|
11
|
+
/** Everything a renderer needs to draw one frame. */
|
|
12
|
+
export interface RenderFrame {
|
|
13
|
+
items: ReadonlyArray<RenderItem>;
|
|
14
|
+
/** Combined view-projection (OpenGL clip space, z in -1..1). */
|
|
15
|
+
viewProjection: Mat4;
|
|
16
|
+
/** Camera position, for backface culling. */
|
|
17
|
+
eye: Vec3;
|
|
18
|
+
light: LightParams;
|
|
19
|
+
/** Logical (CSS pixel) viewport size. */
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
}
|
|
23
|
+
/** Construction-time options shared by all backends. */
|
|
24
|
+
export interface RendererOptions {
|
|
25
|
+
/** Solid background; omit for a transparent canvas (overlay use). */
|
|
26
|
+
background?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A pluggable drawing backend. The engine owns the canvas and sizing; a
|
|
30
|
+
* renderer only initializes its context, reacts to resizes, and draws frames.
|
|
31
|
+
*/
|
|
32
|
+
export interface Renderer {
|
|
33
|
+
init(canvas: HTMLCanvasElement): void | Promise<void>;
|
|
34
|
+
resize(cssWidth: number, cssHeight: number, dpr: number): void;
|
|
35
|
+
render(frame: RenderFrame): void;
|
|
36
|
+
destroy(): void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Load and construct a renderer for `backend`. Each backend lives in its own
|
|
40
|
+
* module and is pulled in with a dynamic `import()`, so the bytes for the
|
|
41
|
+
* backends you do not use are never downloaded or compiled.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createRenderer(backend: Backend, options?: RendererOptions): Promise<Renderer>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load and construct a renderer for `backend`. Each backend lives in its own
|
|
3
|
+
* module and is pulled in with a dynamic `import()`, so the bytes for the
|
|
4
|
+
* backends you do not use are never downloaded or compiled.
|
|
5
|
+
*/
|
|
6
|
+
export async function createRenderer(backend, options = {}) {
|
|
7
|
+
switch (backend) {
|
|
8
|
+
case "webgl":
|
|
9
|
+
return new (await import("./renderers/webgl.js")).WebGLRenderer(options);
|
|
10
|
+
case "webgpu":
|
|
11
|
+
return new (await import("./renderers/webgpu.js")).WebGPURenderer(options);
|
|
12
|
+
case "canvas2d":
|
|
13
|
+
default:
|
|
14
|
+
return new (await import("./renderers/canvas2d.js")).Canvas2DRenderer(options);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Renderer, RenderFrame, RendererOptions } from "../renderer.js";
|
|
2
|
+
/** Software renderer: projects geometry on the CPU and fills 2D polygons. */
|
|
3
|
+
export declare class Canvas2DRenderer implements Renderer {
|
|
4
|
+
private readonly options;
|
|
5
|
+
private ctx?;
|
|
6
|
+
private dpr;
|
|
7
|
+
constructor(options?: RendererOptions);
|
|
8
|
+
init(canvas: HTMLCanvasElement): void;
|
|
9
|
+
resize(_cssWidth: number, _cssHeight: number, dpr: number): void;
|
|
10
|
+
render(frame: RenderFrame): void;
|
|
11
|
+
destroy(): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { shadeColor } from "../core/light.js";
|
|
2
|
+
import { dot, cross, normalize, subtract, transformAffine, transformPoint } from "../core/math.js";
|
|
3
|
+
/** Software renderer: projects geometry on the CPU and fills 2D polygons. */
|
|
4
|
+
export class Canvas2DRenderer {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
this.dpr = 1;
|
|
8
|
+
}
|
|
9
|
+
init(canvas) {
|
|
10
|
+
this.ctx = canvas.getContext("2d") ?? undefined;
|
|
11
|
+
}
|
|
12
|
+
resize(_cssWidth, _cssHeight, dpr) {
|
|
13
|
+
this.dpr = dpr;
|
|
14
|
+
this.ctx?.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
15
|
+
}
|
|
16
|
+
render(frame) {
|
|
17
|
+
const ctx = this.ctx;
|
|
18
|
+
if (!ctx)
|
|
19
|
+
return;
|
|
20
|
+
if (this.options.background) {
|
|
21
|
+
ctx.fillStyle = this.options.background;
|
|
22
|
+
ctx.fillRect(0, 0, frame.width, frame.height);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
ctx.clearRect(0, 0, frame.width, frame.height);
|
|
26
|
+
}
|
|
27
|
+
const polygons = [];
|
|
28
|
+
for (const item of frame.items) {
|
|
29
|
+
const world = item.mesh.vertices.map((v) => transformAffine(item.model, v));
|
|
30
|
+
for (const face of item.mesh.faces) {
|
|
31
|
+
const a = world[face.indices[0]];
|
|
32
|
+
const b = world[face.indices[1]];
|
|
33
|
+
const c = world[face.indices[2]];
|
|
34
|
+
const normal = normalize(cross(subtract(b, a), subtract(c, a)));
|
|
35
|
+
if (dot(normal, subtract(frame.eye, a)) <= 0)
|
|
36
|
+
continue;
|
|
37
|
+
const points = face.indices.map((i) => {
|
|
38
|
+
const ndc = transformPoint(frame.viewProjection, world[i]);
|
|
39
|
+
return {
|
|
40
|
+
x: (ndc.x * 0.5 + 0.5) * frame.width,
|
|
41
|
+
y: (1 - (ndc.y * 0.5 + 0.5)) * frame.height,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
let depth = 0;
|
|
45
|
+
for (const i of face.indices) {
|
|
46
|
+
const d = subtract(world[i], frame.eye);
|
|
47
|
+
depth += dot(d, d);
|
|
48
|
+
}
|
|
49
|
+
depth /= face.indices.length;
|
|
50
|
+
polygons.push({ points, color: shadeColor(normal, face.color, frame.light), depth });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
polygons.sort((p, q) => q.depth - p.depth);
|
|
54
|
+
for (const poly of polygons) {
|
|
55
|
+
if (poly.points.length < 3)
|
|
56
|
+
continue;
|
|
57
|
+
ctx.beginPath();
|
|
58
|
+
ctx.moveTo(poly.points[0].x, poly.points[0].y);
|
|
59
|
+
for (let i = 1; i < poly.points.length; i++) {
|
|
60
|
+
ctx.lineTo(poly.points[i].x, poly.points[i].y);
|
|
61
|
+
}
|
|
62
|
+
ctx.closePath();
|
|
63
|
+
ctx.fillStyle = poly.color;
|
|
64
|
+
ctx.strokeStyle = poly.color;
|
|
65
|
+
ctx.lineWidth = 1;
|
|
66
|
+
ctx.fill();
|
|
67
|
+
ctx.stroke();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
destroy() {
|
|
71
|
+
this.ctx = undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Renderer, RenderFrame, RendererOptions } from "../renderer.js";
|
|
2
|
+
/** Hardware renderer using WebGL2: GPU transforms with a real depth buffer. */
|
|
3
|
+
export declare class WebGLRenderer implements Renderer {
|
|
4
|
+
private gl?;
|
|
5
|
+
private program?;
|
|
6
|
+
private locations?;
|
|
7
|
+
private readonly cache;
|
|
8
|
+
private readonly clearColor;
|
|
9
|
+
constructor(options?: RendererOptions);
|
|
10
|
+
init(canvas: HTMLCanvasElement): void;
|
|
11
|
+
resize(): void;
|
|
12
|
+
private buffers;
|
|
13
|
+
render(frame: RenderFrame): void;
|
|
14
|
+
destroy(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { expandToTriangles, parseColor } from "../core/geometry.js";
|
|
2
|
+
const VERTEX_SHADER = `#version 300 es
|
|
3
|
+
in vec3 aPos;
|
|
4
|
+
in vec3 aNormal;
|
|
5
|
+
in vec3 aColor;
|
|
6
|
+
uniform mat4 uViewProj;
|
|
7
|
+
uniform mat4 uModel;
|
|
8
|
+
out vec3 vNormal;
|
|
9
|
+
out vec3 vColor;
|
|
10
|
+
void main() {
|
|
11
|
+
vNormal = mat3(uModel) * aNormal;
|
|
12
|
+
vColor = aColor;
|
|
13
|
+
gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
|
|
14
|
+
}`;
|
|
15
|
+
const FRAGMENT_SHADER = `#version 300 es
|
|
16
|
+
precision mediump float;
|
|
17
|
+
in vec3 vNormal;
|
|
18
|
+
in vec3 vColor;
|
|
19
|
+
uniform vec3 uToLight;
|
|
20
|
+
uniform float uIntensity;
|
|
21
|
+
uniform float uAmbient;
|
|
22
|
+
out vec4 fragColor;
|
|
23
|
+
void main() {
|
|
24
|
+
float lambert = max(dot(normalize(vNormal), normalize(uToLight)), 0.0);
|
|
25
|
+
float brightness = clamp(uAmbient + uIntensity * lambert, 0.0, 1.0);
|
|
26
|
+
fragColor = vec4(vColor * brightness, 1.0);
|
|
27
|
+
}`;
|
|
28
|
+
function compile(gl, type, source) {
|
|
29
|
+
const shader = gl.createShader(type);
|
|
30
|
+
gl.shaderSource(shader, source);
|
|
31
|
+
gl.compileShader(shader);
|
|
32
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
33
|
+
throw new Error(`3d-spinner: shader compile failed: ${gl.getShaderInfoLog(shader)}`);
|
|
34
|
+
}
|
|
35
|
+
return shader;
|
|
36
|
+
}
|
|
37
|
+
function link(gl) {
|
|
38
|
+
const program = gl.createProgram();
|
|
39
|
+
gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER));
|
|
40
|
+
gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER));
|
|
41
|
+
gl.linkProgram(program);
|
|
42
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
43
|
+
throw new Error(`3d-spinner: program link failed: ${gl.getProgramInfoLog(program)}`);
|
|
44
|
+
}
|
|
45
|
+
return program;
|
|
46
|
+
}
|
|
47
|
+
/** Hardware renderer using WebGL2: GPU transforms with a real depth buffer. */
|
|
48
|
+
export class WebGLRenderer {
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
this.cache = new Map();
|
|
51
|
+
if (options.background) {
|
|
52
|
+
const [r, g, b] = parseColor(options.background);
|
|
53
|
+
this.clearColor = [r / 255, g / 255, b / 255, 1];
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
this.clearColor = [0, 0, 0, 0];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
init(canvas) {
|
|
60
|
+
const gl = canvas.getContext("webgl2");
|
|
61
|
+
if (!gl)
|
|
62
|
+
throw new Error("3d-spinner: WebGL2 is not supported in this browser.");
|
|
63
|
+
this.gl = gl;
|
|
64
|
+
this.program = link(gl);
|
|
65
|
+
this.locations = {
|
|
66
|
+
aPos: gl.getAttribLocation(this.program, "aPos"),
|
|
67
|
+
aNormal: gl.getAttribLocation(this.program, "aNormal"),
|
|
68
|
+
aColor: gl.getAttribLocation(this.program, "aColor"),
|
|
69
|
+
uViewProj: gl.getUniformLocation(this.program, "uViewProj"),
|
|
70
|
+
uModel: gl.getUniformLocation(this.program, "uModel"),
|
|
71
|
+
uToLight: gl.getUniformLocation(this.program, "uToLight"),
|
|
72
|
+
uIntensity: gl.getUniformLocation(this.program, "uIntensity"),
|
|
73
|
+
uAmbient: gl.getUniformLocation(this.program, "uAmbient"),
|
|
74
|
+
};
|
|
75
|
+
gl.enable(gl.DEPTH_TEST);
|
|
76
|
+
gl.enable(gl.CULL_FACE);
|
|
77
|
+
gl.cullFace(gl.BACK);
|
|
78
|
+
gl.frontFace(gl.CCW);
|
|
79
|
+
}
|
|
80
|
+
resize() {
|
|
81
|
+
const gl = this.gl;
|
|
82
|
+
if (!gl)
|
|
83
|
+
return;
|
|
84
|
+
const canvas = gl.canvas;
|
|
85
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
86
|
+
}
|
|
87
|
+
buffers(mesh) {
|
|
88
|
+
const cached = this.cache.get(mesh);
|
|
89
|
+
if (cached)
|
|
90
|
+
return cached;
|
|
91
|
+
const gl = this.gl;
|
|
92
|
+
const loc = this.locations;
|
|
93
|
+
const data = expandToTriangles(mesh);
|
|
94
|
+
const vao = gl.createVertexArray();
|
|
95
|
+
gl.bindVertexArray(vao);
|
|
96
|
+
const attribute = (location, array) => {
|
|
97
|
+
if (location < 0)
|
|
98
|
+
return;
|
|
99
|
+
const buffer = gl.createBuffer();
|
|
100
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
101
|
+
gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
|
|
102
|
+
gl.enableVertexAttribArray(location);
|
|
103
|
+
gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
|
|
104
|
+
};
|
|
105
|
+
attribute(loc.aPos, data.positions);
|
|
106
|
+
attribute(loc.aNormal, data.normals);
|
|
107
|
+
attribute(loc.aColor, data.colors);
|
|
108
|
+
gl.bindVertexArray(null);
|
|
109
|
+
const result = { vao, count: data.count };
|
|
110
|
+
this.cache.set(mesh, result);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
render(frame) {
|
|
114
|
+
const gl = this.gl;
|
|
115
|
+
const loc = this.locations;
|
|
116
|
+
if (!gl || !this.program || !loc)
|
|
117
|
+
return;
|
|
118
|
+
gl.clearColor(...this.clearColor);
|
|
119
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
120
|
+
gl.useProgram(this.program);
|
|
121
|
+
gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
|
|
122
|
+
gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
|
|
123
|
+
gl.uniform1f(loc.uIntensity, frame.light.intensity);
|
|
124
|
+
gl.uniform1f(loc.uAmbient, frame.light.ambient);
|
|
125
|
+
for (const item of frame.items) {
|
|
126
|
+
const mesh = this.buffers(item.mesh);
|
|
127
|
+
gl.uniformMatrix4fv(loc.uModel, false, new Float32Array(item.model));
|
|
128
|
+
gl.bindVertexArray(mesh.vao);
|
|
129
|
+
gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
|
|
130
|
+
}
|
|
131
|
+
gl.bindVertexArray(null);
|
|
132
|
+
}
|
|
133
|
+
destroy() {
|
|
134
|
+
const gl = this.gl;
|
|
135
|
+
if (gl) {
|
|
136
|
+
for (const mesh of this.cache.values())
|
|
137
|
+
gl.deleteVertexArray(mesh.vao);
|
|
138
|
+
if (this.program)
|
|
139
|
+
gl.deleteProgram(this.program);
|
|
140
|
+
}
|
|
141
|
+
this.cache.clear();
|
|
142
|
+
this.gl = undefined;
|
|
143
|
+
this.program = undefined;
|
|
144
|
+
this.locations = undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Renderer, RenderFrame, RendererOptions } from "../renderer.js";
|
|
2
|
+
/** Hardware renderer using WebGPU: GPU transforms with a real depth buffer. */
|
|
3
|
+
export declare class WebGPURenderer implements Renderer {
|
|
4
|
+
private canvas?;
|
|
5
|
+
private device;
|
|
6
|
+
private context;
|
|
7
|
+
private pipeline;
|
|
8
|
+
private uniformBuffer;
|
|
9
|
+
private uniformCapacity;
|
|
10
|
+
private depthTexture;
|
|
11
|
+
private depthSize;
|
|
12
|
+
private destroyed;
|
|
13
|
+
private readonly cache;
|
|
14
|
+
private readonly clearValue;
|
|
15
|
+
private readonly alphaMode;
|
|
16
|
+
constructor(options?: RendererOptions);
|
|
17
|
+
init(canvas: HTMLCanvasElement): Promise<void>;
|
|
18
|
+
resize(): void;
|
|
19
|
+
private ensureDepth;
|
|
20
|
+
private buffers;
|
|
21
|
+
private ensureUniformCapacity;
|
|
22
|
+
render(frame: RenderFrame): void;
|
|
23
|
+
destroy(): void;
|
|
24
|
+
}
|