trid3d 0.1.0 → 0.2.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/README.md +16 -9
- package/package.json +8 -3
- package/trid/export/capture.js +150 -0
- package/trid/export/obj.js +141 -0
- package/trid/index.d.ts +988 -0
- package/trid/index.js +10 -3
- package/trid/loaders/obj.js +18 -2
- package/trid/render/canvasRenderer.js +94 -61
- package/trid/render/draw.js +15 -4
- package/trid/render/shapes.js +285 -23
- package/trid/render/uvMapping.js +225 -0
- package/trid/render/webglRenderer.js +148 -54
- package/trid/scene.js +23 -0
- package/trid/tools/hoverHighlight.js +2 -1
- package/trid/tools/orbitCamera.js +18 -0
package/trid/index.js
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
export { Scene } from "./scene.js";
|
|
14
14
|
|
|
15
15
|
// Shapes and meshes
|
|
16
|
-
export { makeShape, scaleMesh } from "./render/shapes.js";
|
|
16
|
+
export { makeShape, scaleMesh, lathe, extrude, triangulate } from "./render/shapes.js";
|
|
17
17
|
|
|
18
18
|
// Renderers
|
|
19
|
-
export { CanvasRenderer, DEFAULT_LIGHT, DEFAULT_AMBIENT } from "./render/canvasRenderer.js";
|
|
19
|
+
export { CanvasRenderer, DEFAULT_LIGHT, DEFAULT_AMBIENT, materialsOf } from "./render/canvasRenderer.js";
|
|
20
20
|
export { WebGLRenderer, DEFAULT_FAR } from "./render/webglRenderer.js";
|
|
21
21
|
|
|
22
22
|
// Effects
|
|
@@ -25,6 +25,9 @@ export {
|
|
|
25
25
|
buildEffects, effectsKey, uniformType, SURFACE_GLSL,
|
|
26
26
|
} from "./render/effects.js";
|
|
27
27
|
|
|
28
|
+
// Texture mapping
|
|
29
|
+
export { computeUVs, mapUVs, uvsFor, uvTransform, applyUVTransform, mappingOf, mappingKey, MAPPINGS } from "./render/uvMapping.js";
|
|
30
|
+
|
|
28
31
|
// Normals (smooth shading)
|
|
29
32
|
export { cornerNormals, creaseOf, newell, DEFAULT_CREASE } from "./render/normals.js";
|
|
30
33
|
|
|
@@ -32,6 +35,10 @@ export { cornerNormals, creaseOf, newell, DEFAULT_CREASE } from "./render/normal
|
|
|
32
35
|
export { parseOBJ, loadOBJ, readOBJFile, pickOBJFile, fitMesh } from "./loaders/obj.js";
|
|
33
36
|
export { loadTexture, textureReady, textureSize, textureIsLive } from "./loaders/texture.js";
|
|
34
37
|
|
|
38
|
+
// Exporting
|
|
39
|
+
export { exportOBJ } from "./export/obj.js";
|
|
40
|
+
export { screenshot, record, download } from "./export/capture.js";
|
|
41
|
+
|
|
35
42
|
// Tools
|
|
36
43
|
export {
|
|
37
44
|
OrbitCamera, FlyCamera, DragObject, HoverHighlight, FpsMeter, ObjUploader,
|
|
@@ -39,7 +46,7 @@ export {
|
|
|
39
46
|
} from "./tools/index.js";
|
|
40
47
|
|
|
41
48
|
// Drawing and the frame loop
|
|
42
|
-
export { clear, point, line, drawWireframe, drawInstance, edgeSegments } from "./render/draw.js";
|
|
49
|
+
export { clear, point, line, drawWireframe, drawInstance, edgeSegments, wireEdges } from "./render/draw.js";
|
|
43
50
|
export { loop } from "./render/loop.js";
|
|
44
51
|
export { parseColor } from "./render/color.js";
|
|
45
52
|
|
package/trid/loaders/obj.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Wavefront OBJ loading.
|
|
3
3
|
*
|
|
4
4
|
* Reads vertex positions (`v`), texture coordinates (`vt`), normals
|
|
5
|
-
* (`vn`), faces (`f`)
|
|
6
|
-
* groups) is ignored for now.
|
|
5
|
+
* (`vn`), faces (`f`), polylines (`l`) and material names (`usemtl`).
|
|
6
|
+
* Everything else (.mtl files, groups) is ignored for now.
|
|
7
7
|
*
|
|
8
8
|
* OBJ files are right-handed with +Z toward the viewer. Trid is
|
|
9
9
|
* left-handed with the camera looking down +Z, so Z is flipped on
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
* @property {{u:number,v:number}[]} [uvs] - Texture coordinates from the file (`vt`), if any
|
|
22
22
|
* @property {(number[]|null)[]} [faceUVs] - For each face, the texture coordinate index of
|
|
23
23
|
* each corner (null when the file gives none for that face). Used by textures.
|
|
24
|
+
* @property {string[]} [materialNames] - Material names from `usemtl` lines, in the order they first appear
|
|
25
|
+
* @property {number[]} [faceMaterials] - For each face, its index in `materialNames`. Give the
|
|
26
|
+
* object a list of materials in the same order to color or texture each part.
|
|
24
27
|
*
|
|
25
28
|
* @typedef {Object} ObjOptions
|
|
26
29
|
* @property {number} [fit] - If set, center the model on the origin and scale it
|
|
@@ -43,6 +46,9 @@ export function parseOBJ(text, { fit } = {}) {
|
|
|
43
46
|
const faceNormals = []; // per face: normal index of each corner, or null
|
|
44
47
|
const uvs = [];
|
|
45
48
|
const faceUVs = []; // per face: texture coordinate index of each corner, or null
|
|
49
|
+
const materialNames = [];
|
|
50
|
+
const faceMaterials = []; // per face: index in materialNames
|
|
51
|
+
let material = 0;
|
|
46
52
|
|
|
47
53
|
const src = text.split(/\r?\n/);
|
|
48
54
|
|
|
@@ -60,6 +66,10 @@ export function parseOBJ(text, { fit } = {}) {
|
|
|
60
66
|
uvs.push({ u: +(parts[1] ?? 0), v: +(parts[2] ?? 0) });
|
|
61
67
|
} else if (kind === "vn") {
|
|
62
68
|
normals.push({ x: +(parts[1] ?? 0), y: +(parts[2] ?? 0), z: -(parts[3] ?? 0) });
|
|
69
|
+
} else if (kind === "usemtl") {
|
|
70
|
+
const name = parts.slice(1).join(" ");
|
|
71
|
+
material = materialNames.indexOf(name);
|
|
72
|
+
if (material < 0) material = materialNames.push(name) - 1;
|
|
63
73
|
} else if (kind === "f" || kind === "l") {
|
|
64
74
|
// "7", "7/1", "7//3", "7/1/3" all point at vertex 7. Negative = counted from the end.
|
|
65
75
|
const ids = parts.slice(1).map((p) => {
|
|
@@ -72,6 +82,7 @@ export function parseOBJ(text, { fit } = {}) {
|
|
|
72
82
|
});
|
|
73
83
|
if (kind === "f" && ids.length >= 3) {
|
|
74
84
|
faces.push(ids.reverse()); // flipping Z flips the winding
|
|
85
|
+
faceMaterials.push(material);
|
|
75
86
|
// "7//3" and "7/1/3": the third number is the corner's normal
|
|
76
87
|
const ns = parts.slice(1).map((p) => {
|
|
77
88
|
const s = p.split("/")[2];
|
|
@@ -127,6 +138,11 @@ export function parseOBJ(text, { fit } = {}) {
|
|
|
127
138
|
mesh.uvs = uvs;
|
|
128
139
|
mesh.faceUVs = faceUVs;
|
|
129
140
|
}
|
|
141
|
+
// Parts using different materials
|
|
142
|
+
if (materialNames.length > 1) {
|
|
143
|
+
mesh.materialNames = materialNames;
|
|
144
|
+
mesh.faceMaterials = faceMaterials;
|
|
145
|
+
}
|
|
130
146
|
if (fit != null) fitMesh(mesh, fit);
|
|
131
147
|
return mesh;
|
|
132
148
|
}
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { localToWorld, worldToCamera, cameraToScreen, clipNear, rotateXYZ, DEFAULT_NEAR } from "../math/transform.js";
|
|
2
|
-
import { clear, drawInstance, edgeSegments } from "./draw.js";
|
|
2
|
+
import { clear, drawInstance, edgeSegments, wireEdges } from "./draw.js";
|
|
3
3
|
import { parseColor } from "./color.js";
|
|
4
4
|
import { newell, cornerNormals, creaseOf } from "./normals.js";
|
|
5
5
|
import { textureReady, textureSize, textureIsLive } from "../loaders/texture.js";
|
|
6
|
+
import { uvsFor, uvTransform, applyUVTransform } from "./uvMapping.js";
|
|
6
7
|
|
|
7
8
|
export { newell };
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* An object's materials as a list (`material` can be one material or a
|
|
12
|
+
* list of them, picked per face by `mesh.faceMaterials`). Null when the
|
|
13
|
+
* object is a wireframe.
|
|
14
|
+
*
|
|
15
|
+
* @returns {Material[]|null}
|
|
16
|
+
*/
|
|
17
|
+
export function materialsOf(obj) {
|
|
18
|
+
const m = obj.material;
|
|
19
|
+
if (!m) return null;
|
|
20
|
+
if (!Array.isArray(m)) return [m];
|
|
21
|
+
return m.length && m[0] ? m : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
/** Direction light comes from when the scene doesn't set one: above, left, in front. */
|
|
10
25
|
export const DEFAULT_LIGHT = { x: -0.5, y: 1, z: -0.8 };
|
|
11
26
|
|
|
@@ -16,6 +31,10 @@ export const DEFAULT_AMBIENT = 0.25;
|
|
|
16
31
|
* Material: how an object's faces look. Objects without one are drawn
|
|
17
32
|
* as wireframes using their `color` and `lineWidth`.
|
|
18
33
|
*
|
|
34
|
+
* An object's `material` can also be a list: each face then uses the
|
|
35
|
+
* entry `mesh.faceMaterials[face]` points at (the first one by default).
|
|
36
|
+
* Built-in cubes and boxes number their sides 0 to 5.
|
|
37
|
+
*
|
|
19
38
|
* @typedef {Object} Material
|
|
20
39
|
* @property {string} [fill] - Face color, any CSS color (default: the scene's color)
|
|
21
40
|
* @property {boolean} [shade=true] - Darken faces that turn away from the light
|
|
@@ -26,6 +45,11 @@ export const DEFAULT_AMBIENT = 0.25;
|
|
|
26
45
|
* @property {CanvasImageSource} [texture] - Image drawn on the faces (needs texture coordinates:
|
|
27
46
|
* built-in shapes and OBJ files with `vt` have them). `fill` tints it (default white).
|
|
28
47
|
* @property {boolean} [pixelated=false] - Keep texture pixels sharp instead of smoothing them
|
|
48
|
+
* @property {import("./uvMapping.js").Mapping} [mapping="uv"] - Where texture coordinates come
|
|
49
|
+
* from: the mesh's own ("uv"), or made from its shape ("planar", "box", "sphere", "cylinder")
|
|
50
|
+
* @property {number|{u:number,v:number}} [textureRepeat=1] - How many times the image repeats across
|
|
51
|
+
* @property {{u:number,v:number}} [textureOffset] - Moves the image (1 = its whole width or height)
|
|
52
|
+
* @property {number} [textureRotation=0] - Turns the image around its center, in radians
|
|
29
53
|
* @property {Object[]} [effects] - Effects applied in order, like `[toon(), rim()]`.
|
|
30
54
|
* See trid/render/effects.js. The canvas renderer runs them once per face.
|
|
31
55
|
*/
|
|
@@ -79,7 +103,7 @@ export class CanvasRenderer {
|
|
|
79
103
|
this._frame++;
|
|
80
104
|
|
|
81
105
|
// Wireframes only: nothing to sort
|
|
82
|
-
if (!scene.objects.some((o) => o
|
|
106
|
+
if (!scene.objects.some((o) => materialsOf(o))) {
|
|
83
107
|
for (const obj of scene.objects) {
|
|
84
108
|
drawInstance(ctx, obj, camera, w, h, obj.color ?? scene.color, obj.lineWidth ?? scene.lineWidth);
|
|
85
109
|
}
|
|
@@ -97,17 +121,17 @@ export class CanvasRenderer {
|
|
|
97
121
|
const background = this._rgba(ctx, scene.background);
|
|
98
122
|
|
|
99
123
|
for (const obj of scene.objects) {
|
|
124
|
+
const mats = materialsOf(obj);
|
|
100
125
|
// Split big faces (cached per mesh); extra vertices are midpoints of existing ones
|
|
101
|
-
const split =
|
|
126
|
+
const split = mats && this.maxFaceSize ? this._split(obj.mesh) : null;
|
|
102
127
|
const verts = split ? withMidpoints(obj.mesh.vertices, split.mids) : obj.mesh.vertices;
|
|
103
128
|
const world = verts.map((v) => localToWorld(v, obj));
|
|
104
129
|
const cam = world.map((p) => worldToCamera(p, camera));
|
|
105
130
|
const color = obj.color ?? scene.color;
|
|
106
131
|
const lineWidth = obj.lineWidth ?? scene.lineWidth;
|
|
107
|
-
const mat = obj.material;
|
|
108
132
|
|
|
109
|
-
if (!
|
|
110
|
-
for (const [i, j] of edgeSegments(obj.mesh
|
|
133
|
+
if (!mats) {
|
|
134
|
+
for (const [i, j] of edgeSegments(wireEdges(obj.mesh))) {
|
|
111
135
|
const seg = clipNear(cam[i], cam[j], near);
|
|
112
136
|
if (!seg) continue;
|
|
113
137
|
items.push({
|
|
@@ -119,19 +143,14 @@ export class CanvasRenderer {
|
|
|
119
143
|
continue;
|
|
120
144
|
}
|
|
121
145
|
|
|
122
|
-
const fill = mat.fill ?? (mat.texture ? "#ffffff" : scene.color);
|
|
123
|
-
const shade = mat.shade !== false;
|
|
124
|
-
const outline = mat.wireframe ? color : null;
|
|
125
|
-
|
|
126
|
-
// Smooth shading: a normal per face corner (split faces get midpoints' normals by averaging)
|
|
127
|
-
const crease = shade ? creaseOf(mat.smooth) : null;
|
|
128
|
-
const corners = crease ? cornerNormals(obj.mesh, crease) : null;
|
|
129
146
|
const sourceFaces = obj.mesh.faces ?? [];
|
|
130
147
|
const base = obj.mesh.vertices.length;
|
|
131
|
-
|
|
148
|
+
|
|
149
|
+
// Smooth shading: a normal per face corner (split faces get midpoints' normals by averaging)
|
|
150
|
+
const normalAt = (corners, F, i) => {
|
|
132
151
|
if (i < base) return corners[F][sourceFaces[F].indexOf(i)];
|
|
133
152
|
const [a, b] = split.mids[i - base];
|
|
134
|
-
const na = normalAt(F, a), nb = normalAt(F, b);
|
|
153
|
+
const na = normalAt(corners, F, a), nb = normalAt(corners, F, b);
|
|
135
154
|
return normalize({ x: na.x + nb.x, y: na.y + nb.y, z: na.z + nb.z });
|
|
136
155
|
};
|
|
137
156
|
const lightAt = (n, back) => {
|
|
@@ -140,47 +159,69 @@ export class CanvasRenderer {
|
|
|
140
159
|
const d = (back ? -1 : 1) * (r.x * light.x + r.y * light.y + r.z * light.z);
|
|
141
160
|
return ambient + (1 - ambient) * Math.max(0, d);
|
|
142
161
|
};
|
|
143
|
-
|
|
144
|
-
// Textures: each triangle is filled with the image, mapped by its corners' texture coordinates
|
|
145
|
-
const meshUVs = obj.mesh.uvs, faceUVs = obj.mesh.faceUVs;
|
|
146
|
-
let tex = null;
|
|
147
|
-
if (mat.texture && textureReady(mat.texture)) {
|
|
148
|
-
if (faceUVs && meshUVs) tex = mat.texture;
|
|
149
|
-
else if (!this._warned.has(obj.mesh)) {
|
|
150
|
-
this._warned.add(obj.mesh);
|
|
151
|
-
console.warn("Trid: this mesh has no texture coordinates (uvs / faceUVs), so its texture isn't drawn");
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
const uvAt = (F, i) => {
|
|
155
|
-
const t = faceUVs?.[F];
|
|
156
|
-
if (!t || !meshUVs) return null;
|
|
157
|
-
if (i < base) return meshUVs[t[sourceFaces[F].indexOf(i)]];
|
|
158
|
-
const [a, b] = split.mids[i - base];
|
|
159
|
-
const ta = uvAt(F, a), tb = uvAt(F, b);
|
|
160
|
-
return ta && tb ? { u: (ta.u + tb.u) / 2, v: (ta.v + tb.v) / 2 } : null;
|
|
161
|
-
};
|
|
162
162
|
const flatLight = (face, back) => {
|
|
163
163
|
const nw = normalize(newell(face.map((i) => world[i])));
|
|
164
164
|
const d = (back ? -1 : 1) * (nw.x * light.x + nw.y * light.y + nw.z * light.z);
|
|
165
165
|
return ambient + (1 - ambient) * Math.max(0, d);
|
|
166
166
|
};
|
|
167
|
-
|
|
168
|
-
// Effects run once per face, at its center (see trid/render/effects.js)
|
|
169
|
-
const effects = mat.effects?.length ? mat.effects : null;
|
|
170
167
|
const toWorldNormal = (n) =>
|
|
171
168
|
normalize(rotateXYZ({ x: n.x / obj.scale.x, y: n.y / obj.scale.y, z: n.z / obj.scale.z }, obj.rot));
|
|
172
169
|
|
|
170
|
+
// Everything a material needs, worked out once per object and frame
|
|
171
|
+
const prepare = (mat) => {
|
|
172
|
+
const shade = mat.shade !== false;
|
|
173
|
+
const crease = shade ? creaseOf(mat.smooth) : null;
|
|
174
|
+
// Texture coordinates: the mesh's own, or made from its shape (mat.mapping)
|
|
175
|
+
const { uvs: meshUVs, faceUVs } = uvsFor(obj.mesh, mat.mapping);
|
|
176
|
+
const xf = uvTransform(mat);
|
|
177
|
+
const rawUV = (F, i) => {
|
|
178
|
+
const t = faceUVs?.[F];
|
|
179
|
+
if (!t || !meshUVs) return null;
|
|
180
|
+
if (i < base) return meshUVs[t[sourceFaces[F].indexOf(i)]];
|
|
181
|
+
const [a, b] = split.mids[i - base];
|
|
182
|
+
const ta = rawUV(F, a), tb = rawUV(F, b);
|
|
183
|
+
return ta && tb ? { u: (ta.u + tb.u) / 2, v: (ta.v + tb.v) / 2 } : null;
|
|
184
|
+
};
|
|
185
|
+
let tex = null;
|
|
186
|
+
if (mat.texture && textureReady(mat.texture)) {
|
|
187
|
+
if (faceUVs && meshUVs) tex = mat.texture;
|
|
188
|
+
else if (!this._warned.has(obj.mesh)) {
|
|
189
|
+
this._warned.add(obj.mesh);
|
|
190
|
+
console.warn("Trid: this mesh has no texture coordinates (uvs / faceUVs), so its texture isn't drawn. Set material.mapping to make some.");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
mat, shade, tex,
|
|
195
|
+
fill: mat.fill ?? (mat.texture ? "#ffffff" : scene.color),
|
|
196
|
+
outline: mat.wireframe ? color : null,
|
|
197
|
+
corners: crease ? cornerNormals(obj.mesh, crease) : null,
|
|
198
|
+
effects: mat.effects?.length ? mat.effects : null,
|
|
199
|
+
uvAt: (F, i) => applyUVTransform(xf, rawUV(F, i)),
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
// Which material each face uses (mesh.faceMaterials), prepared when first needed
|
|
203
|
+
const looks = new Array(mats.length);
|
|
204
|
+
const faceMats = obj.mesh.faceMaterials;
|
|
205
|
+
const lookOf = (F) => {
|
|
206
|
+
let k = faceMats?.[F] ?? 0;
|
|
207
|
+
if (!(k >= 0 && k < mats.length && mats[k])) k = 0;
|
|
208
|
+
return (looks[k] ??= prepare(mats[k]));
|
|
209
|
+
};
|
|
210
|
+
|
|
173
211
|
const faces = split ? split.faces : sourceFaces;
|
|
174
212
|
for (let f = 0; f < faces.length; f++) {
|
|
175
213
|
const face = faces[f];
|
|
176
214
|
if (face.length < 3) continue;
|
|
215
|
+
const F = split ? split.origins[f] : f; // the mesh face this one comes from
|
|
216
|
+
const L = lookOf(F);
|
|
217
|
+
const { fill, shade, outline, corners } = L;
|
|
177
218
|
const pts = face.map((i) => cam[i]);
|
|
178
219
|
|
|
179
220
|
// Seen from behind? (camera sits at the origin of camera space)
|
|
180
221
|
const n = newell(pts);
|
|
181
222
|
const c = centroid(pts);
|
|
182
223
|
const back = n.x * c.x + n.y * c.y + n.z * c.z >= 0;
|
|
183
|
-
if (back && !mat.doubleSided) continue;
|
|
224
|
+
if (back && !L.mat.doubleSided) continue;
|
|
184
225
|
|
|
185
226
|
const clipped = clipPolygon(pts, near);
|
|
186
227
|
if (clipped.length < 3) continue;
|
|
@@ -188,13 +229,13 @@ export class CanvasRenderer {
|
|
|
188
229
|
// Which edges are real outlines (split faces have extra inner edges); null = all
|
|
189
230
|
const edges = split && clipped === pts ? split.masks[f] : null;
|
|
190
231
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const ts = face.map((i) => uvAt(F, i));
|
|
232
|
+
// Textures: each triangle is filled with the image, mapped by its corners' texture coordinates
|
|
233
|
+
if (L.tex && clipped === pts) {
|
|
234
|
+
const ts = face.map((i) => L.uvAt(F, i));
|
|
194
235
|
if (ts.every(Boolean)) {
|
|
195
236
|
let lit;
|
|
196
237
|
if (!shade) lit = face.map(() => 1);
|
|
197
|
-
else if (corners) lit = face.map((i) => lightAt(normalAt(F, i), back));
|
|
238
|
+
else if (corners) lit = face.map((i) => lightAt(normalAt(corners, F, i), back));
|
|
198
239
|
else lit = face.map(() => flatLight(face, back));
|
|
199
240
|
const screen = pts.map(toScreen);
|
|
200
241
|
const n = face.length;
|
|
@@ -203,7 +244,7 @@ export class CanvasRenderer {
|
|
|
203
244
|
items.push({
|
|
204
245
|
depth: c.z,
|
|
205
246
|
poly: [screen[0], screen[k], screen[k + 1]],
|
|
206
|
-
texture: { src: tex, uvs: [ts[0], ts[k], ts[k + 1]], pixelated: !!mat.pixelated, tint: fill },
|
|
247
|
+
texture: { src: L.tex, uvs: [ts[0], ts[k], ts[k + 1]], pixelated: !!L.mat.pixelated, tint: fill },
|
|
207
248
|
lit: [lit[0], lit[k], lit[k + 1]],
|
|
208
249
|
outline, lineWidth,
|
|
209
250
|
edges: n === 3 ? edges : fan,
|
|
@@ -213,12 +254,12 @@ export class CanvasRenderer {
|
|
|
213
254
|
}
|
|
214
255
|
}
|
|
215
256
|
|
|
216
|
-
|
|
257
|
+
// Effects run once per face, at its center (see trid/render/effects.js)
|
|
258
|
+
if (L.effects) {
|
|
217
259
|
// Normal: the face's own, or the average of its corners when smooth
|
|
218
260
|
let nw;
|
|
219
261
|
if (corners) {
|
|
220
|
-
const
|
|
221
|
-
const sum = face.map((i) => toWorldNormal(normalAt(F, i)))
|
|
262
|
+
const sum = face.map((i) => toWorldNormal(normalAt(corners, F, i)))
|
|
222
263
|
.reduce((a, b) => ({ x: a.x + b.x, y: a.y + b.y, z: a.z + b.z }), { x: 0, y: 0, z: 0 });
|
|
223
264
|
nw = normalize(sum);
|
|
224
265
|
} else {
|
|
@@ -230,16 +271,16 @@ export class CanvasRenderer {
|
|
|
230
271
|
const toCam = { x: camPos.x - position.x, y: camPos.y - position.y, z: camPos.z - position.z };
|
|
231
272
|
const diffuse = Math.max(0, nw.x * light.x + nw.y * light.y + nw.z * light.z);
|
|
232
273
|
const lit = shade ? ambient + (1 - ambient) * diffuse : 1;
|
|
233
|
-
const
|
|
274
|
+
const rgba = this._rgba(ctx, fill);
|
|
234
275
|
const s = {
|
|
235
|
-
base, normal: nw, position, local: centroid(face.map((i) => verts[i])),
|
|
276
|
+
base: rgba, normal: nw, position, local: centroid(face.map((i) => verts[i])),
|
|
236
277
|
view: normalize(toCam), lightDir: light, diffuse, light: lit, ambient,
|
|
237
|
-
uv: centerUV(face.map((i) => uvAt(
|
|
278
|
+
uv: centerUV(face.map((i) => L.uvAt(F, i))),
|
|
238
279
|
depth: Math.hypot(toCam.x, toCam.y, toCam.z), time, background,
|
|
239
280
|
rgba: (v) => (typeof v === "string" ? this._rgba(ctx, v) : Array.isArray(v) ? v : [v.x, v.y, v.z, 1]),
|
|
240
281
|
};
|
|
241
|
-
let col = [
|
|
242
|
-
for (const e of effects) if (e.canvas) col = e.canvas(col, s, e.uniforms ?? {});
|
|
282
|
+
let col = [rgba[0] * lit, rgba[1] * lit, rgba[2] * lit, rgba[3]];
|
|
283
|
+
for (const e of L.effects) if (e.canvas) col = e.canvas(col, s, e.uniforms ?? {});
|
|
243
284
|
const k = col.map((x) => Math.min(1, Math.max(0, x)));
|
|
244
285
|
items.push({
|
|
245
286
|
depth: c.z,
|
|
@@ -251,8 +292,7 @@ export class CanvasRenderer {
|
|
|
251
292
|
}
|
|
252
293
|
|
|
253
294
|
if (corners) {
|
|
254
|
-
const
|
|
255
|
-
const lit = face.map((i) => lightAt(normalAt(F, i), back));
|
|
295
|
+
const lit = face.map((i) => lightAt(normalAt(corners, F, i), back));
|
|
256
296
|
if (clipped === pts) {
|
|
257
297
|
// One gradient per triangle: brightness changes linearly across a triangle
|
|
258
298
|
const screen = pts.map(toScreen);
|
|
@@ -276,14 +316,7 @@ export class CanvasRenderer {
|
|
|
276
316
|
continue;
|
|
277
317
|
}
|
|
278
318
|
|
|
279
|
-
|
|
280
|
-
if (shade) {
|
|
281
|
-
const nw = normalize(newell(face.map((i) => world[i])));
|
|
282
|
-
const s = back ? -1 : 1;
|
|
283
|
-
const d = s * (nw.x * light.x + nw.y * light.y + nw.z * light.z);
|
|
284
|
-
brightness = ambient + (1 - ambient) * Math.max(0, d);
|
|
285
|
-
}
|
|
286
|
-
|
|
319
|
+
const brightness = shade ? flatLight(face, back) : 1;
|
|
287
320
|
items.push({
|
|
288
321
|
depth: c.z,
|
|
289
322
|
poly: clipped.map(toScreen),
|
package/trid/render/draw.js
CHANGED
|
@@ -59,11 +59,11 @@ export function line(ctx, p1, p2, color, width = 4) {
|
|
|
59
59
|
* - An edge with 3+ indices is a closed loop: the last index
|
|
60
60
|
* connects back to the first
|
|
61
61
|
*
|
|
62
|
-
* @param {number[][]} edges - Edge definitions using vertex indices
|
|
62
|
+
* @param {number[][]} [edges] - Edge definitions using vertex indices (none: no segments)
|
|
63
63
|
* @returns {Generator<[number, number]>} Index pairs to connect
|
|
64
64
|
*/
|
|
65
65
|
export function* edgeSegments(edges) {
|
|
66
|
-
for (const e of edges) {
|
|
66
|
+
for (const e of edges ?? []) {
|
|
67
67
|
const closed = e.length > 2;
|
|
68
68
|
const count = closed ? e.length : e.length - 1;
|
|
69
69
|
for (let i = 0; i < count; i++) {
|
|
@@ -72,6 +72,17 @@ export function* edgeSegments(edges) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* The lines to draw for a mesh shown as a wireframe: its `edges`, or the
|
|
77
|
+
* outline of each face when it has no `edges` (a face-only mesh).
|
|
78
|
+
*
|
|
79
|
+
* @param {{ edges?: number[][], faces?: number[][] }} mesh
|
|
80
|
+
* @returns {number[][]} Edge definitions (faces are closed loops)
|
|
81
|
+
*/
|
|
82
|
+
export function wireEdges(mesh) {
|
|
83
|
+
return mesh.edges ?? mesh.faces ?? [];
|
|
84
|
+
}
|
|
85
|
+
|
|
75
86
|
/**
|
|
76
87
|
* Draws a wireframe object by connecting projected vertices
|
|
77
88
|
* using index-based edge definitions.
|
|
@@ -115,7 +126,7 @@ export function drawWireframe(ctx, points2, edges, color, width = 4) {
|
|
|
115
126
|
*
|
|
116
127
|
* @param {CanvasRenderingContext2D} ctx - Canvas 2D context
|
|
117
128
|
* @param {{
|
|
118
|
-
* mesh:{ vertices:{x:number,y:number,z:number}[], edges
|
|
129
|
+
* mesh:{ vertices:{x:number,y:number,z:number}[], edges?:number[][], faces?:number[][] },
|
|
119
130
|
* pos:{x:number,y:number,z:number},
|
|
120
131
|
* rot:{x:number,y:number,z:number},
|
|
121
132
|
* scale:{x:number,y:number,z:number}
|
|
@@ -134,7 +145,7 @@ export function drawInstance(ctx, inst, camera, w, h, color, lineWidth = 3) {
|
|
|
134
145
|
ctx.strokeStyle = color;
|
|
135
146
|
|
|
136
147
|
ctx.beginPath();
|
|
137
|
-
for (const [i, j] of edgeSegments(inst.mesh
|
|
148
|
+
for (const [i, j] of edgeSegments(wireEdges(inst.mesh))) {
|
|
138
149
|
const seg = clipNear(ptsCam[i], ptsCam[j], near);
|
|
139
150
|
if (!seg) continue;
|
|
140
151
|
const a = cameraToScreen(seg[0], camera, w, h);
|