3d-spinner 0.9.4 → 0.9.5
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/dist/animations/charged-orb.js +4 -2
- package/dist/animations/ghost-train.js +3 -1
- package/dist/animations/spin.d.ts +3 -1
- package/dist/animations/spin.js +6 -1
- package/dist/cjs/animations/charged-orb.cjs +170 -28
- package/dist/cjs/animations/grid-assembly.cjs +164 -26
- package/dist/cjs/animations/object-motion.cjs +156 -24
- package/dist/cjs/animations/particles.cjs +167 -26
- package/dist/cjs/animations/spin.cjs +169 -27
- package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +193 -45
- package/dist/cjs/engines/little-3d-engine/loaders/obj.cjs +51 -13
- package/dist/cjs/engines/little-3d-engine/renderers/canvas2d-textured.cjs +56 -6
- package/dist/cjs/engines/little-3d-engine/renderers/webgl-textured.cjs +55 -6
- package/dist/cjs/engines/little-3d-engine/renderers/webgpu-textured.cjs +64 -11
- package/dist/cjs/prefabs/prefabs.cjs +186 -40
- package/dist/engines/little-3d-engine/core/geometry.d.ts +15 -2
- package/dist/engines/little-3d-engine/core/geometry.js +25 -3
- package/dist/engines/little-3d-engine/core/light.d.ts +28 -4
- package/dist/engines/little-3d-engine/core/light.js +48 -7
- package/dist/engines/little-3d-engine/core/mesh.d.ts +33 -0
- package/dist/engines/little-3d-engine/core/mesh.js +12 -0
- package/dist/engines/little-3d-engine/little-3d-engine.d.ts +2 -2
- package/dist/engines/little-3d-engine/little-3d-engine.js +1 -1
- package/dist/engines/little-3d-engine/loaders/obj.d.ts +8 -7
- package/dist/engines/little-3d-engine/loaders/obj.js +74 -20
- package/dist/engines/little-3d-engine/renderers/canvas2d.js +23 -1
- package/dist/engines/little-3d-engine/renderers/webgl.js +34 -5
- package/dist/engines/little-3d-engine/renderers/webgpu.js +43 -10
- package/dist/engines/little-3d-engine/shapes/complex/plane.d.ts +8 -3
- package/dist/engines/little-3d-engine/shapes/complex/plane.js +10 -4
- package/dist/engines/little-3d-engine/shapes/primitives/cube.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/cube.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/octahedron.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/octahedron.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/pyramid.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/pyramid.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/quad.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/quad.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/icosphere.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/icosphere.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/tetrahedron.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/tetrahedron.js +4 -2
- package/dist/umd/spinner.global.js +255 -63
- package/dist/umd/spinner.global.min.js +58 -17
- package/package.json +1 -1
|
@@ -8,15 +8,56 @@ const DEFAULTS = {
|
|
|
8
8
|
function clamp01(value) {
|
|
9
9
|
return Math.min(1, Math.max(0, value));
|
|
10
10
|
}
|
|
11
|
+
function clamp255(value) {
|
|
12
|
+
return Math.round(Math.min(255, Math.max(0, value)));
|
|
13
|
+
}
|
|
11
14
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
15
|
+
* Shade a face and return `0..255` RGB channels.
|
|
16
|
+
*
|
|
17
|
+
* The diffuse term is flat Lambert: the base `color` brightened by how directly
|
|
18
|
+
* `normal` faces the light, floored at the ambient level. When a {@link Surface}
|
|
19
|
+
* supplies a specular material and a `viewDir`, a Blinn-Phong highlight (`Ks`
|
|
20
|
+
* tinted, tightened by `Ns`) is added; an emissive material (`Ke`) is always
|
|
21
|
+
* added on top. With no material this reduces exactly to the previous flat
|
|
22
|
+
* shading.
|
|
14
23
|
*/
|
|
15
|
-
export function
|
|
24
|
+
export function shade(normal, color, light, surface) {
|
|
16
25
|
const lambert = Math.max(0, dot(normal, light.toLight));
|
|
17
26
|
const brightness = clamp01(light.ambient + light.intensity * lambert);
|
|
18
|
-
const [
|
|
19
|
-
|
|
27
|
+
const [baseR, baseG, baseB] = parseColor(color);
|
|
28
|
+
let r = baseR * brightness;
|
|
29
|
+
let g = baseG * brightness;
|
|
30
|
+
let b = baseB * brightness;
|
|
31
|
+
const material = surface?.material;
|
|
32
|
+
const specular = material?.specular;
|
|
33
|
+
const viewDir = surface?.viewDir;
|
|
34
|
+
if (specular && viewDir && lambert > 0) {
|
|
35
|
+
const half = normalize({
|
|
36
|
+
x: light.toLight.x + viewDir.x,
|
|
37
|
+
y: light.toLight.y + viewDir.y,
|
|
38
|
+
z: light.toLight.z + viewDir.z,
|
|
39
|
+
});
|
|
40
|
+
const shininess = material?.shininess ?? 32;
|
|
41
|
+
const highlight = Math.pow(Math.max(0, dot(normal, half)), shininess) * light.intensity * 255;
|
|
42
|
+
r += highlight * specular[0];
|
|
43
|
+
g += highlight * specular[1];
|
|
44
|
+
b += highlight * specular[2];
|
|
45
|
+
}
|
|
46
|
+
const emissive = material?.emissive;
|
|
47
|
+
if (emissive) {
|
|
48
|
+
r += emissive[0] * 255;
|
|
49
|
+
g += emissive[1] * 255;
|
|
50
|
+
b += emissive[2] * 255;
|
|
51
|
+
}
|
|
52
|
+
return [clamp255(r), clamp255(g), clamp255(b)];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Shade a face and return an `rgb(...)` string. Thin wrapper over {@link shade};
|
|
56
|
+
* see it for the shading model. Passing no `surface` gives flat Lambert shading.
|
|
57
|
+
*/
|
|
58
|
+
export function shadeColor(normal, color, light, surface) {
|
|
59
|
+
const [r, g, b] = shade(normal, color, light, surface);
|
|
60
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
20
61
|
}
|
|
21
62
|
/** A single directional light with an ambient term, used for flat shading. */
|
|
22
63
|
export class Light {
|
|
@@ -29,7 +70,7 @@ export class Light {
|
|
|
29
70
|
};
|
|
30
71
|
}
|
|
31
72
|
/** Convenience wrapper around {@link shadeColor} using this light. */
|
|
32
|
-
shade(normal, color) {
|
|
33
|
-
return shadeColor(normal, color, this.params);
|
|
73
|
+
shade(normal, color, surface) {
|
|
74
|
+
return shadeColor(normal, color, this.params, surface);
|
|
34
75
|
}
|
|
35
76
|
}
|
|
@@ -1,16 +1,49 @@
|
|
|
1
1
|
import type { Vec3 } from "./math.js";
|
|
2
|
+
/**
|
|
3
|
+
* Surface response of a face, derived from Wavefront MTL properties. All fields
|
|
4
|
+
* are optional; a face with no material is flat-shaded from its `color` alone,
|
|
5
|
+
* exactly as before materials existed.
|
|
6
|
+
*/
|
|
7
|
+
export interface Material {
|
|
8
|
+
/**
|
|
9
|
+
* Specular reflectivity (`Ks`) as linear `0..1` RGB. Drives the color and
|
|
10
|
+
* strength of the highlight. Omit or `[0,0,0]` for a matte surface.
|
|
11
|
+
*/
|
|
12
|
+
specular?: [number, number, number];
|
|
13
|
+
/**
|
|
14
|
+
* Specular exponent (`Ns`, Wavefront range `0..1000`). Higher is a tighter,
|
|
15
|
+
* glossier highlight; lower is broad and soft. Ignored without `specular`.
|
|
16
|
+
*/
|
|
17
|
+
shininess?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Emissive color (`Ke`) as linear `0..1` RGB, added on top of shading so the
|
|
20
|
+
* face appears self-lit. Omit or `[0,0,0]` for no self-illumination.
|
|
21
|
+
*/
|
|
22
|
+
emissive?: [number, number, number];
|
|
23
|
+
}
|
|
2
24
|
/** A single flat polygon: indices into the mesh `vertices` plus a base color. */
|
|
3
25
|
export interface Face {
|
|
4
26
|
/** Vertex indices, wound counter-clockwise when viewed from outside. */
|
|
5
27
|
indices: number[];
|
|
6
28
|
/** Base CSS color, for example `"#3b82f6"`. Shading is applied on top of it. */
|
|
7
29
|
color: string;
|
|
30
|
+
/**
|
|
31
|
+
* Optional surface material (specular, shininess, emissive) from an MTL
|
|
32
|
+
* file. When absent the face is flat Lambert-shaded from `color`.
|
|
33
|
+
*/
|
|
34
|
+
material?: Material;
|
|
8
35
|
}
|
|
9
36
|
/** Geometry: a list of vertices and the colored faces that connect them. */
|
|
10
37
|
export interface Mesh {
|
|
11
38
|
vertices: Vec3[];
|
|
12
39
|
faces: Face[];
|
|
13
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Assign one {@link Material} to every face of a mesh, in place, and return it.
|
|
43
|
+
* A no-op when `material` is omitted. Shape builders use this to apply a uniform
|
|
44
|
+
* surface material (specular, shininess, emissive) across all their faces.
|
|
45
|
+
*/
|
|
46
|
+
export declare function attachMaterial(mesh: Mesh, material?: Material): Mesh;
|
|
14
47
|
/** Draw only outward-facing transparent surfaces. */
|
|
15
48
|
export interface OneSidedTransparency {
|
|
16
49
|
mode: "one-sided";
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assign one {@link Material} to every face of a mesh, in place, and return it.
|
|
3
|
+
* A no-op when `material` is omitted. Shape builders use this to apply a uniform
|
|
4
|
+
* surface material (specular, shininess, emissive) across all their faces.
|
|
5
|
+
*/
|
|
6
|
+
export function attachMaterial(mesh, material) {
|
|
7
|
+
if (material) {
|
|
8
|
+
for (const face of mesh.faces)
|
|
9
|
+
face.material = material;
|
|
10
|
+
}
|
|
11
|
+
return mesh;
|
|
12
|
+
}
|
|
1
13
|
/** Create a {@link Transform} with sensible defaults (origin, no rotation). */
|
|
2
14
|
export function transform(init) {
|
|
3
15
|
return {
|
|
@@ -80,8 +80,8 @@ export { starTexture } from "./textures/dynamic/star.js";
|
|
|
80
80
|
export { shineTexture } from "./textures/dynamic/shine.js";
|
|
81
81
|
export { streakTexture } from "./textures/dynamic/streak.js";
|
|
82
82
|
export { expandToTriangles } from "./core/geometry.js";
|
|
83
|
-
export type { Mesh, Face, Transform, Transparency, OneSidedTransparency, TwoSidedTransparency, } from "./core/mesh.js";
|
|
84
|
-
export { transform } from "./core/mesh.js";
|
|
83
|
+
export type { Mesh, Face, Material, Transform, Transparency, OneSidedTransparency, TwoSidedTransparency, } from "./core/mesh.js";
|
|
84
|
+
export { transform, attachMaterial } from "./core/mesh.js";
|
|
85
85
|
export type { Backend, Renderer, RendererFactory, RenderFrame, RenderItem, RendererOptions, } from "./renderer.js";
|
|
86
86
|
export { orderRenderItems } from "./renderer.js";
|
|
87
87
|
export { type Vec3, vec3, subtract, cross, dot, scale, normalize, } from "./core/math.js";
|
|
@@ -171,6 +171,6 @@ export { starTexture } from "./textures/dynamic/star.js";
|
|
|
171
171
|
export { shineTexture } from "./textures/dynamic/shine.js";
|
|
172
172
|
export { streakTexture } from "./textures/dynamic/streak.js";
|
|
173
173
|
export { expandToTriangles } from "./core/geometry.js";
|
|
174
|
-
export { transform } from "./core/mesh.js";
|
|
174
|
+
export { transform, attachMaterial } from "./core/mesh.js";
|
|
175
175
|
export { orderRenderItems } from "./renderer.js";
|
|
176
176
|
export { vec3, subtract, cross, dot, scale, normalize, } from "./core/math.js";
|
|
@@ -10,8 +10,9 @@ export interface ObjOptions {
|
|
|
10
10
|
/** Contents of an `.mtl` file referenced by the OBJ. */
|
|
11
11
|
mtl?: string;
|
|
12
12
|
/**
|
|
13
|
-
* Use MTL
|
|
14
|
-
*
|
|
13
|
+
* Use MTL material values for faces with matching `usemtl` statements: the
|
|
14
|
+
* diffuse color (`Kd`) becomes the face color, and specular (`Ks`/`Ns`) and
|
|
15
|
+
* emissive (`Ke`) become the face {@link Material}. Default `false`.
|
|
15
16
|
*/
|
|
16
17
|
useMtlColors?: boolean;
|
|
17
18
|
}
|
|
@@ -21,12 +22,12 @@ export interface ObjOptions {
|
|
|
21
22
|
* Reads `v` vertex positions and `f` faces (triangles, quads, or n-gons, in
|
|
22
23
|
* `v`, `v/vt`, `v/vt/vn`, or `v//vn` form, with 1-based or negative indices).
|
|
23
24
|
* Normals (`vn`) and texture coordinates (`vt`) are ignored - the engine
|
|
24
|
-
* computes a flat normal per face. Material names can select diffuse
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* outside.
|
|
25
|
+
* computes a flat normal per face. Material names can select the diffuse color
|
|
26
|
+
* (`Kd`) and surface material (specular `Ks`/`Ns`, emissive `Ke`) from supplied
|
|
27
|
+
* MTL text; groups and other statements are ignored. Face winding is preserved
|
|
28
|
+
* as-is; the engine expects CCW winding as seen from outside.
|
|
28
29
|
*
|
|
29
30
|
* @param text Contents of an `.obj` file.
|
|
30
|
-
* @param options Face palette and optional MTL
|
|
31
|
+
* @param options Face palette and optional MTL materials.
|
|
31
32
|
*/
|
|
32
33
|
export declare function parseObj(text: string, options?: ObjOptions): Mesh;
|
|
@@ -1,31 +1,82 @@
|
|
|
1
1
|
const DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
2
|
+
function clamp01(value) {
|
|
3
|
+
return Math.min(1, Math.max(0, value));
|
|
4
|
+
}
|
|
2
5
|
function channelToHex(value) {
|
|
3
6
|
const channel = Number.parseFloat(value);
|
|
4
7
|
if (!Number.isFinite(channel))
|
|
5
8
|
return undefined;
|
|
6
|
-
return Math.round(
|
|
9
|
+
return Math.round(clamp01(channel) * 255)
|
|
7
10
|
.toString(16)
|
|
8
11
|
.padStart(2, "0");
|
|
9
12
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
/** Parse an `Ks`/`Ke`-style RGB triple into clamped `0..1` channels. */
|
|
14
|
+
function parseRgb(parts) {
|
|
15
|
+
const channels = parts.slice(1, 4).map(Number.parseFloat);
|
|
16
|
+
if (channels.length !== 3 || !channels.every(Number.isFinite))
|
|
17
|
+
return undefined;
|
|
18
|
+
return [clamp01(channels[0]), clamp01(channels[1]), clamp01(channels[2])];
|
|
19
|
+
}
|
|
20
|
+
/** Fold specular/shininess/emissive into a {@link Material}, or `undefined`. */
|
|
21
|
+
function toMaterial(surface) {
|
|
22
|
+
const material = {};
|
|
23
|
+
if (surface.specular)
|
|
24
|
+
material.specular = surface.specular;
|
|
25
|
+
if (surface.shininess !== undefined)
|
|
26
|
+
material.shininess = surface.shininess;
|
|
27
|
+
if (surface.emissive)
|
|
28
|
+
material.emissive = surface.emissive;
|
|
29
|
+
return Object.keys(material).length > 0 ? material : undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse MTL text into a map of material name to its color and surface material.
|
|
33
|
+
* Reads `Kd` (diffuse color), `Ks` (specular), `Ns` (shininess), and `Ke`
|
|
34
|
+
* (emissive); other statements are ignored.
|
|
35
|
+
*/
|
|
36
|
+
function parseMtl(text) {
|
|
37
|
+
const materials = new Map();
|
|
38
|
+
const surfaces = new Map();
|
|
39
|
+
let name;
|
|
13
40
|
for (const line of text.split("\n")) {
|
|
14
41
|
const trimmed = line.trim();
|
|
15
42
|
if (trimmed === "" || trimmed.startsWith("#"))
|
|
16
43
|
continue;
|
|
17
44
|
const parts = trimmed.split(/\s+/);
|
|
18
|
-
|
|
19
|
-
|
|
45
|
+
const keyword = parts[0];
|
|
46
|
+
if (keyword === "newmtl") {
|
|
47
|
+
name = parts.slice(1).join(" ");
|
|
48
|
+
if (name && !materials.has(name)) {
|
|
49
|
+
materials.set(name, {});
|
|
50
|
+
surfaces.set(name, {});
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
20
53
|
}
|
|
21
|
-
|
|
54
|
+
if (!name)
|
|
55
|
+
continue;
|
|
56
|
+
const entry = materials.get(name);
|
|
57
|
+
const surface = surfaces.get(name);
|
|
58
|
+
if (keyword === "Kd") {
|
|
22
59
|
const channels = parts.slice(1, 4).map(channelToHex);
|
|
23
60
|
if (channels.length === 3 && channels.every((channel) => channel !== undefined)) {
|
|
24
|
-
|
|
61
|
+
entry.color = `#${channels.join("")}`;
|
|
25
62
|
}
|
|
26
63
|
}
|
|
64
|
+
else if (keyword === "Ks") {
|
|
65
|
+
surface.specular = parseRgb(parts);
|
|
66
|
+
}
|
|
67
|
+
else if (keyword === "Ns") {
|
|
68
|
+
const ns = Number.parseFloat(parts[1]);
|
|
69
|
+
if (Number.isFinite(ns))
|
|
70
|
+
surface.shininess = Math.max(0, ns);
|
|
71
|
+
}
|
|
72
|
+
else if (keyword === "Ke") {
|
|
73
|
+
surface.emissive = parseRgb(parts);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const [key, surface] of surfaces) {
|
|
77
|
+
materials.get(key).material = toMaterial(surface);
|
|
27
78
|
}
|
|
28
|
-
return
|
|
79
|
+
return materials;
|
|
29
80
|
}
|
|
30
81
|
function resolveIndex(token, vertexCount) {
|
|
31
82
|
const n = parseInt(token, 10);
|
|
@@ -37,18 +88,18 @@ function resolveIndex(token, vertexCount) {
|
|
|
37
88
|
* Reads `v` vertex positions and `f` faces (triangles, quads, or n-gons, in
|
|
38
89
|
* `v`, `v/vt`, `v/vt/vn`, or `v//vn` form, with 1-based or negative indices).
|
|
39
90
|
* Normals (`vn`) and texture coordinates (`vt`) are ignored - the engine
|
|
40
|
-
* computes a flat normal per face. Material names can select diffuse
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* outside.
|
|
91
|
+
* computes a flat normal per face. Material names can select the diffuse color
|
|
92
|
+
* (`Kd`) and surface material (specular `Ks`/`Ns`, emissive `Ke`) from supplied
|
|
93
|
+
* MTL text; groups and other statements are ignored. Face winding is preserved
|
|
94
|
+
* as-is; the engine expects CCW winding as seen from outside.
|
|
44
95
|
*
|
|
45
96
|
* @param text Contents of an `.obj` file.
|
|
46
|
-
* @param options Face palette and optional MTL
|
|
97
|
+
* @param options Face palette and optional MTL materials.
|
|
47
98
|
*/
|
|
48
99
|
export function parseObj(text, options = {}) {
|
|
49
100
|
const colors = options.colors ?? DEFAULT_COLORS;
|
|
50
|
-
const
|
|
51
|
-
?
|
|
101
|
+
const materials = options.useMtlColors && options.mtl
|
|
102
|
+
? parseMtl(options.mtl)
|
|
52
103
|
: undefined;
|
|
53
104
|
const vertices = [];
|
|
54
105
|
const faces = [];
|
|
@@ -76,10 +127,13 @@ export function parseObj(text, options = {}) {
|
|
|
76
127
|
indices.push(resolveIndex(vertexToken, vertices.length));
|
|
77
128
|
}
|
|
78
129
|
if (indices.length >= 3) {
|
|
79
|
-
const
|
|
80
|
-
const color =
|
|
81
|
-
?? (
|
|
82
|
-
|
|
130
|
+
const entry = material ? materials?.get(material) : undefined;
|
|
131
|
+
const color = entry?.color
|
|
132
|
+
?? (materials ? (colors[0] ?? "#888888") : colors[faces.length % colors.length]);
|
|
133
|
+
const face = { indices, color };
|
|
134
|
+
if (entry?.material)
|
|
135
|
+
face.material = entry.material;
|
|
136
|
+
faces.push(face);
|
|
83
137
|
}
|
|
84
138
|
}
|
|
85
139
|
}
|
|
@@ -60,9 +60,31 @@ export class Canvas2DRenderer {
|
|
|
60
60
|
depth += dot(d, d);
|
|
61
61
|
}
|
|
62
62
|
depth /= face.indices.length;
|
|
63
|
+
// Specular needs the view direction from the face toward the eye; use
|
|
64
|
+
// the face centroid. Emissive-only or plain faces skip that work.
|
|
65
|
+
let surface;
|
|
66
|
+
const material = face.material;
|
|
67
|
+
if (material) {
|
|
68
|
+
if (material.specular) {
|
|
69
|
+
let cx = 0;
|
|
70
|
+
let cy = 0;
|
|
71
|
+
let cz = 0;
|
|
72
|
+
for (const i of face.indices) {
|
|
73
|
+
cx += world[i].x;
|
|
74
|
+
cy += world[i].y;
|
|
75
|
+
cz += world[i].z;
|
|
76
|
+
}
|
|
77
|
+
const inv = 1 / face.indices.length;
|
|
78
|
+
const viewDir = normalize(subtract(frame.eye, { x: cx * inv, y: cy * inv, z: cz * inv }));
|
|
79
|
+
surface = { material, viewDir };
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
surface = { material };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
63
85
|
polygons.push({
|
|
64
86
|
points,
|
|
65
|
-
color: shadeColor(normal, face.color, frame.light),
|
|
87
|
+
color: shadeColor(normal, face.color, frame.light, surface),
|
|
66
88
|
depth,
|
|
67
89
|
opacity: faceOpacity,
|
|
68
90
|
});
|
|
@@ -4,28 +4,51 @@ const VERTEX_SHADER = `#version 300 es
|
|
|
4
4
|
in vec3 aPos;
|
|
5
5
|
in vec3 aNormal;
|
|
6
6
|
in vec3 aColor;
|
|
7
|
+
in vec3 aEmissive;
|
|
8
|
+
in vec4 aSpecular;
|
|
7
9
|
uniform mat4 uViewProj;
|
|
8
10
|
uniform mat4 uModel;
|
|
9
11
|
out vec3 vNormal;
|
|
10
12
|
out vec3 vColor;
|
|
13
|
+
out vec3 vEmissive;
|
|
14
|
+
out vec4 vSpecular;
|
|
15
|
+
out vec3 vWorldPos;
|
|
11
16
|
void main() {
|
|
12
17
|
vNormal = mat3(uModel) * aNormal;
|
|
13
18
|
vColor = aColor;
|
|
14
|
-
|
|
19
|
+
vEmissive = aEmissive;
|
|
20
|
+
vSpecular = aSpecular;
|
|
21
|
+
vec4 world = uModel * vec4(aPos, 1.0);
|
|
22
|
+
vWorldPos = world.xyz;
|
|
23
|
+
gl_Position = uViewProj * world;
|
|
15
24
|
}`;
|
|
16
25
|
const FRAGMENT_SHADER = `#version 300 es
|
|
17
26
|
precision mediump float;
|
|
18
27
|
in vec3 vNormal;
|
|
19
28
|
in vec3 vColor;
|
|
29
|
+
in vec3 vEmissive;
|
|
30
|
+
in vec4 vSpecular;
|
|
31
|
+
in vec3 vWorldPos;
|
|
20
32
|
uniform vec3 uToLight;
|
|
33
|
+
uniform vec3 uEye;
|
|
21
34
|
uniform float uIntensity;
|
|
22
35
|
uniform float uAmbient;
|
|
23
36
|
uniform float uOpacity;
|
|
24
37
|
out vec4 fragColor;
|
|
25
38
|
void main() {
|
|
26
|
-
|
|
39
|
+
vec3 normal = normalize(vNormal);
|
|
40
|
+
vec3 toLight = normalize(uToLight);
|
|
41
|
+
float lambert = max(dot(normal, toLight), 0.0);
|
|
27
42
|
float brightness = clamp(uAmbient + uIntensity * lambert, 0.0, 1.0);
|
|
28
|
-
|
|
43
|
+
vec3 lit = vColor * brightness;
|
|
44
|
+
if (lambert > 0.0) {
|
|
45
|
+
vec3 viewDir = normalize(uEye - vWorldPos);
|
|
46
|
+
vec3 halfVec = normalize(toLight + viewDir);
|
|
47
|
+
float highlight = pow(max(dot(normal, halfVec), 0.0), vSpecular.w) * uIntensity;
|
|
48
|
+
lit += highlight * vSpecular.xyz;
|
|
49
|
+
}
|
|
50
|
+
lit += vEmissive;
|
|
51
|
+
fragColor = vec4(lit, uOpacity);
|
|
29
52
|
}`;
|
|
30
53
|
function compile(gl, type, source) {
|
|
31
54
|
const shader = gl.createShader(type);
|
|
@@ -68,9 +91,12 @@ export class WebGLRenderer {
|
|
|
68
91
|
aPos: gl.getAttribLocation(this.program, "aPos"),
|
|
69
92
|
aNormal: gl.getAttribLocation(this.program, "aNormal"),
|
|
70
93
|
aColor: gl.getAttribLocation(this.program, "aColor"),
|
|
94
|
+
aEmissive: gl.getAttribLocation(this.program, "aEmissive"),
|
|
95
|
+
aSpecular: gl.getAttribLocation(this.program, "aSpecular"),
|
|
71
96
|
uViewProj: gl.getUniformLocation(this.program, "uViewProj"),
|
|
72
97
|
uModel: gl.getUniformLocation(this.program, "uModel"),
|
|
73
98
|
uToLight: gl.getUniformLocation(this.program, "uToLight"),
|
|
99
|
+
uEye: gl.getUniformLocation(this.program, "uEye"),
|
|
74
100
|
uIntensity: gl.getUniformLocation(this.program, "uIntensity"),
|
|
75
101
|
uAmbient: gl.getUniformLocation(this.program, "uAmbient"),
|
|
76
102
|
uOpacity: gl.getUniformLocation(this.program, "uOpacity"),
|
|
@@ -96,18 +122,20 @@ export class WebGLRenderer {
|
|
|
96
122
|
const data = expandToTriangles(mesh);
|
|
97
123
|
const vao = gl.createVertexArray();
|
|
98
124
|
gl.bindVertexArray(vao);
|
|
99
|
-
const attribute = (location, array) => {
|
|
125
|
+
const attribute = (location, array, size = 3) => {
|
|
100
126
|
if (location < 0)
|
|
101
127
|
return;
|
|
102
128
|
const buffer = gl.createBuffer();
|
|
103
129
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
104
130
|
gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
|
|
105
131
|
gl.enableVertexAttribArray(location);
|
|
106
|
-
gl.vertexAttribPointer(location,
|
|
132
|
+
gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
|
|
107
133
|
};
|
|
108
134
|
attribute(loc.aPos, data.positions);
|
|
109
135
|
attribute(loc.aNormal, data.normals);
|
|
110
136
|
attribute(loc.aColor, data.colors);
|
|
137
|
+
attribute(loc.aEmissive, data.emissives);
|
|
138
|
+
attribute(loc.aSpecular, data.speculars, 4);
|
|
111
139
|
gl.bindVertexArray(null);
|
|
112
140
|
const result = { vao, count: data.count };
|
|
113
141
|
this.cache.set(mesh, result);
|
|
@@ -123,6 +151,7 @@ export class WebGLRenderer {
|
|
|
123
151
|
gl.useProgram(this.program);
|
|
124
152
|
gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
|
|
125
153
|
gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
|
|
154
|
+
gl.uniform3f(loc.uEye, frame.eye.x, frame.eye.y, frame.eye.z);
|
|
126
155
|
gl.uniform1f(loc.uIntensity, frame.light.intensity);
|
|
127
156
|
gl.uniform1f(loc.uAmbient, frame.light.ambient);
|
|
128
157
|
gl.disable(gl.BLEND);
|
|
@@ -7,6 +7,7 @@ struct Uniforms {
|
|
|
7
7
|
model: mat4x4<f32>,
|
|
8
8
|
toLight: vec4<f32>,
|
|
9
9
|
params: vec4<f32>,
|
|
10
|
+
eye: vec4<f32>,
|
|
10
11
|
};
|
|
11
12
|
@group(0) @binding(0) var<uniform> u: Uniforms;
|
|
12
13
|
|
|
@@ -14,23 +15,40 @@ struct VSOut {
|
|
|
14
15
|
@builtin(position) position: vec4<f32>,
|
|
15
16
|
@location(0) normal: vec3<f32>,
|
|
16
17
|
@location(1) color: vec3<f32>,
|
|
18
|
+
@location(2) emissive: vec3<f32>,
|
|
19
|
+
@location(3) specular: vec4<f32>,
|
|
20
|
+
@location(4) worldPos: vec3<f32>,
|
|
17
21
|
};
|
|
18
22
|
|
|
19
23
|
@vertex
|
|
20
|
-
fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>) -> VSOut {
|
|
24
|
+
fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>, @location(3) emissive: vec3<f32>, @location(4) specular: vec4<f32>) -> VSOut {
|
|
21
25
|
var out: VSOut;
|
|
22
26
|
let m = mat3x3<f32>(u.model[0].xyz, u.model[1].xyz, u.model[2].xyz);
|
|
23
27
|
out.normal = m * normal;
|
|
24
28
|
out.color = color;
|
|
25
|
-
out.
|
|
29
|
+
out.emissive = emissive;
|
|
30
|
+
out.specular = specular;
|
|
31
|
+
let world = u.model * vec4<f32>(pos, 1.0);
|
|
32
|
+
out.worldPos = world.xyz;
|
|
33
|
+
out.position = u.viewProj * world;
|
|
26
34
|
return out;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
@fragment
|
|
30
38
|
fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
31
|
-
let
|
|
39
|
+
let normal = normalize(in.normal);
|
|
40
|
+
let toLight = normalize(u.toLight.xyz);
|
|
41
|
+
let lambert = max(dot(normal, toLight), 0.0);
|
|
32
42
|
let brightness = clamp(u.params.y + u.params.x * lambert, 0.0, 1.0);
|
|
33
|
-
|
|
43
|
+
var lit = in.color * brightness;
|
|
44
|
+
if (lambert > 0.0) {
|
|
45
|
+
let viewDir = normalize(u.eye.xyz - in.worldPos);
|
|
46
|
+
let halfVec = normalize(toLight + viewDir);
|
|
47
|
+
let highlight = pow(max(dot(normal, halfVec), 0.0), in.specular.w) * u.params.x;
|
|
48
|
+
lit = lit + highlight * in.specular.xyz;
|
|
49
|
+
}
|
|
50
|
+
lit = lit + in.emissive;
|
|
51
|
+
return vec4<f32>(lit, u.params.z);
|
|
34
52
|
}
|
|
35
53
|
`;
|
|
36
54
|
// Maps OpenGL clip space (z in -1..1) to WebGPU clip space (z in 0..1).
|
|
@@ -77,13 +95,15 @@ export class WebGPURenderer {
|
|
|
77
95
|
{
|
|
78
96
|
binding: 0,
|
|
79
97
|
visibility: stage.VERTEX | stage.FRAGMENT,
|
|
80
|
-
buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize:
|
|
98
|
+
buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 176 },
|
|
81
99
|
},
|
|
82
100
|
],
|
|
83
101
|
});
|
|
84
|
-
const vertexBuffer = (location) => ({
|
|
85
|
-
arrayStride:
|
|
86
|
-
attributes: [
|
|
102
|
+
const vertexBuffer = (location, components = 3) => ({
|
|
103
|
+
arrayStride: components * 4,
|
|
104
|
+
attributes: [
|
|
105
|
+
{ shaderLocation: location, offset: 0, format: `float32x${components}` },
|
|
106
|
+
],
|
|
87
107
|
});
|
|
88
108
|
const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
|
|
89
109
|
const blend = {
|
|
@@ -99,7 +119,13 @@ export class WebGPURenderer {
|
|
|
99
119
|
vertex: {
|
|
100
120
|
module,
|
|
101
121
|
entryPoint: "vs",
|
|
102
|
-
buffers: [
|
|
122
|
+
buffers: [
|
|
123
|
+
vertexBuffer(0),
|
|
124
|
+
vertexBuffer(1),
|
|
125
|
+
vertexBuffer(2),
|
|
126
|
+
vertexBuffer(3),
|
|
127
|
+
vertexBuffer(4, 4),
|
|
128
|
+
],
|
|
103
129
|
},
|
|
104
130
|
fragment: {
|
|
105
131
|
module,
|
|
@@ -155,6 +181,8 @@ export class WebGPURenderer {
|
|
|
155
181
|
position: upload(data.positions),
|
|
156
182
|
normal: upload(data.normals),
|
|
157
183
|
color: upload(data.colors),
|
|
184
|
+
emissive: upload(data.emissives),
|
|
185
|
+
specular: upload(data.speculars),
|
|
158
186
|
count: data.count,
|
|
159
187
|
};
|
|
160
188
|
this.cache.set(mesh, result);
|
|
@@ -212,7 +240,7 @@ export class WebGPURenderer {
|
|
|
212
240
|
const bindGroup = this.device.createBindGroup({
|
|
213
241
|
layout,
|
|
214
242
|
entries: [
|
|
215
|
-
{ binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size:
|
|
243
|
+
{ binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 176 } },
|
|
216
244
|
],
|
|
217
245
|
});
|
|
218
246
|
draws.forEach((draw, i) => {
|
|
@@ -221,6 +249,7 @@ export class WebGPURenderer {
|
|
|
221
249
|
data.set(draw.item.model, 16);
|
|
222
250
|
data.set([frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z, 0], 32);
|
|
223
251
|
data.set([frame.light.intensity, frame.light.ambient, draw.opacity, 0], 36);
|
|
252
|
+
data.set([frame.eye.x, frame.eye.y, frame.eye.z, 0], 40);
|
|
224
253
|
this.device.queue.writeBuffer(this.uniformBuffer, i * UNIFORM_STRIDE, data);
|
|
225
254
|
});
|
|
226
255
|
const encoder = this.device.createCommandEncoder();
|
|
@@ -247,6 +276,8 @@ export class WebGPURenderer {
|
|
|
247
276
|
pass.setVertexBuffer(0, mesh.position);
|
|
248
277
|
pass.setVertexBuffer(1, mesh.normal);
|
|
249
278
|
pass.setVertexBuffer(2, mesh.color);
|
|
279
|
+
pass.setVertexBuffer(3, mesh.emissive);
|
|
280
|
+
pass.setVertexBuffer(4, mesh.specular);
|
|
250
281
|
pass.draw(mesh.count);
|
|
251
282
|
});
|
|
252
283
|
pass.end();
|
|
@@ -258,6 +289,8 @@ export class WebGPURenderer {
|
|
|
258
289
|
mesh.position.destroy?.();
|
|
259
290
|
mesh.normal.destroy?.();
|
|
260
291
|
mesh.color.destroy?.();
|
|
292
|
+
mesh.emissive.destroy?.();
|
|
293
|
+
mesh.specular.destroy?.();
|
|
261
294
|
}
|
|
262
295
|
this.cache.clear();
|
|
263
296
|
this.uniformBuffer?.destroy?.();
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
import type
|
|
2
|
-
/**
|
|
3
|
-
|
|
1
|
+
import { type Material, type Mesh } from "../../core/mesh.js";
|
|
2
|
+
/**
|
|
3
|
+
* Build a low-poly plane mesh pointing along the positive X axis.
|
|
4
|
+
*
|
|
5
|
+
* @param colors CSS colors cycled across faces. Defaults to a built-in palette.
|
|
6
|
+
* @param material Optional surface material applied to every face.
|
|
7
|
+
*/
|
|
8
|
+
export declare function planeMesh(colors?: string[], material?: Material): Mesh;
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { attachMaterial } from "../../core/mesh.js";
|
|
1
2
|
const DEFAULT_COLORS = ["#e0f2fe", "#7dd3fc", "#38bdf8", "#f8fafc"];
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Build a low-poly plane mesh pointing along the positive X axis.
|
|
5
|
+
*
|
|
6
|
+
* @param colors CSS colors cycled across faces. Defaults to a built-in palette.
|
|
7
|
+
* @param material Optional surface material applied to every face.
|
|
8
|
+
*/
|
|
9
|
+
export function planeMesh(colors = DEFAULT_COLORS, material) {
|
|
10
|
+
return attachMaterial({
|
|
5
11
|
vertices: [
|
|
6
12
|
{ x: 0.9, y: 0, z: 0 },
|
|
7
13
|
{ x: -0.2, y: 0, z: 0.82 },
|
|
@@ -29,5 +35,5 @@ export function planeMesh(colors = DEFAULT_COLORS) {
|
|
|
29
35
|
{ indices: [3, 6, 8], color: colors[0] ?? DEFAULT_COLORS[0] },
|
|
30
36
|
{ indices: [3, 8, 6], color: colors[1] ?? DEFAULT_COLORS[1] },
|
|
31
37
|
],
|
|
32
|
-
};
|
|
38
|
+
}, material);
|
|
33
39
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Material, type Mesh } from "../../core/mesh.js";
|
|
2
2
|
/**
|
|
3
3
|
* Build a cube mesh centered on the origin.
|
|
4
4
|
*
|
|
5
5
|
* @param size Edge length. Defaults to `1`.
|
|
6
6
|
* @param colors Six CSS colors, one per face (front, back, top, bottom, right, left).
|
|
7
7
|
* Defaults to a built-in palette.
|
|
8
|
+
* @param material Optional surface material applied to every face.
|
|
8
9
|
*/
|
|
9
|
-
export declare function cube(size?: number, colors?: string[]): Mesh;
|
|
10
|
+
export declare function cube(size?: number, colors?: string[], material?: Material): Mesh;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { attachMaterial } from "../../core/mesh.js";
|
|
1
2
|
const DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
2
3
|
/**
|
|
3
4
|
* Build a cube mesh centered on the origin.
|
|
@@ -5,8 +6,9 @@ const DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "
|
|
|
5
6
|
* @param size Edge length. Defaults to `1`.
|
|
6
7
|
* @param colors Six CSS colors, one per face (front, back, top, bottom, right, left).
|
|
7
8
|
* Defaults to a built-in palette.
|
|
9
|
+
* @param material Optional surface material applied to every face.
|
|
8
10
|
*/
|
|
9
|
-
export function cube(size = 1, colors = DEFAULT_COLORS) {
|
|
11
|
+
export function cube(size = 1, colors = DEFAULT_COLORS, material) {
|
|
10
12
|
const h = size / 2;
|
|
11
13
|
const vertices = [
|
|
12
14
|
{ x: -h, y: -h, z: h },
|
|
@@ -26,5 +28,5 @@ export function cube(size = 1, colors = DEFAULT_COLORS) {
|
|
|
26
28
|
{ indices: [1, 5, 6, 2], color: colors[4 % colors.length] },
|
|
27
29
|
{ indices: [4, 0, 3, 7], color: colors[5 % colors.length] },
|
|
28
30
|
];
|
|
29
|
-
return { vertices, faces };
|
|
31
|
+
return attachMaterial({ vertices, faces }, material);
|
|
30
32
|
}
|