abstract-3d 4.0.0-beta.9 → 4.0.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/lib/renderers/react/react.js +2 -2
- package/lib/renderers/react/react.js.map +1 -1
- package/lib/renderers/svg/index.d.ts +1 -1
- package/lib/renderers/svg/index.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/shared.d.ts +14 -2
- package/lib/renderers/svg/svg-geometries/shared.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/shared.js +15 -1
- package/lib/renderers/svg/svg-geometries/shared.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-box.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-box.js +41 -30
- package/lib/renderers/svg/svg-geometries/svg-box.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-cone.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-cone.js +3 -3
- package/lib/renderers/svg/svg-geometries/svg-cone.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-cylinder.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-cylinder.js +18 -6
- package/lib/renderers/svg/svg-geometries/svg-cylinder.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-image.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-image.js +3 -2
- package/lib/renderers/svg/svg-geometries/svg-image.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-line.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-line.js +3 -1
- package/lib/renderers/svg/svg-geometries/svg-line.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-plane.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-plane.js +4 -2
- package/lib/renderers/svg/svg-geometries/svg-plane.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-polygon.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-polygon.js +4 -2
- package/lib/renderers/svg/svg-geometries/svg-polygon.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-shape.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-shape.js +4 -2
- package/lib/renderers/svg/svg-geometries/svg-shape.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-text.js +1 -1
- package/lib/renderers/svg/svg-geometries/svg-text.js.map +1 -1
- package/lib/renderers/svg/svg-order.d.ts +9 -0
- package/lib/renderers/svg/svg-order.d.ts.map +1 -0
- package/lib/renderers/svg/svg-order.js +148 -0
- package/lib/renderers/svg/svg-order.js.map +1 -0
- package/lib/renderers/svg/svg.d.ts.map +1 -1
- package/lib/renderers/svg/svg.js +13 -9
- package/lib/renderers/svg/svg.js.map +1 -1
- package/lib/renderers/three/build/build.d.ts +2 -0
- package/lib/renderers/three/build/build.d.ts.map +1 -1
- package/lib/renderers/three/build/build.js +1 -1
- package/lib/renderers/three/build/build.js.map +1 -1
- package/lib/renderers/three/build/materials.d.ts +5 -1
- package/lib/renderers/three/build/materials.d.ts.map +1 -1
- package/lib/renderers/three/build/materials.js +10 -3
- package/lib/renderers/three/build/materials.js.map +1 -1
- package/lib/renderers/three/viewer/options.d.ts +7 -1
- package/lib/renderers/three/viewer/options.d.ts.map +1 -1
- package/lib/renderers/three/viewer/options.js +1 -0
- package/lib/renderers/three/viewer/options.js.map +1 -1
- package/lib/renderers/three/viewer/viewer.d.ts.map +1 -1
- package/lib/renderers/three/viewer/viewer.js +31 -6
- package/lib/renderers/three/viewer/viewer.js.map +1 -1
- package/package.json +1 -1
- package/src/renderers/react/react.tsx +2 -2
- package/src/renderers/svg/index.ts +1 -1
- package/src/renderers/svg/svg-geometries/shared.ts +26 -3
- package/src/renderers/svg/svg-geometries/svg-box.ts +42 -36
- package/src/renderers/svg/svg-geometries/svg-cone.ts +17 -3
- package/src/renderers/svg/svg-geometries/svg-cylinder.ts +25 -6
- package/src/renderers/svg/svg-geometries/svg-image.ts +3 -2
- package/src/renderers/svg/svg-geometries/svg-line.ts +3 -1
- package/src/renderers/svg/svg-geometries/svg-plane.ts +9 -2
- package/src/renderers/svg/svg-geometries/svg-polygon.ts +9 -2
- package/src/renderers/svg/svg-geometries/svg-shape.ts +9 -2
- package/src/renderers/svg/svg-geometries/svg-text.ts +1 -1
- package/src/renderers/svg/svg-order.test.ts +35 -0
- package/src/renderers/svg/svg-order.ts +172 -0
- package/src/renderers/svg/svg.ts +13 -9
- package/src/renderers/three/build/build.ts +3 -1
- package/src/renderers/three/build/materials.ts +11 -3
- package/src/renderers/three/viewer/options.ts +8 -1
- package/src/renderers/three/viewer/viewer.ts +31 -5
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { Vec3 } from "../../abstract-3d.js";
|
|
2
|
+
import type { zOrderElement } from "./svg-geometries/shared.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Elements in drawing order, back to front, by Newell's ordering without splitting: of two elements that overlap on screen, one entirely behind the
|
|
6
|
+
* other's plane, or the other entirely in front of its plane, is drawn first. Elements are drawn by mean depth otherwise, each after those it must be
|
|
7
|
+
* drawn after, depth first; elements overlapping in a cycle, which only splitting them could resolve, are drawn where the cycle is met. Elements
|
|
8
|
+
* entirely inside an opaque convex polygon in front of them are left out, as they can't be seen
|
|
9
|
+
*/
|
|
10
|
+
export function orderElements(elements: ReadonlyArray<zOrderElement>): ReadonlyArray<zOrderElement> {
|
|
11
|
+
const items: Array<Item | undefined> = [];
|
|
12
|
+
const swept: Array<number> = [];
|
|
13
|
+
const byDepth: Array<number> = [];
|
|
14
|
+
elements.forEach((e, i) => {
|
|
15
|
+
items.push(e.polygon && createItem(e.polygon, !!e.opaque));
|
|
16
|
+
byDepth.push(i);
|
|
17
|
+
if (e.polygon) {
|
|
18
|
+
swept.push(i);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
swept.sort((a, b) => items[a]!.min.x - items[b]!.min.x);
|
|
22
|
+
byDepth.sort((a, b) => elements[a]!.zOrder - elements[b]!.zOrder);
|
|
23
|
+
|
|
24
|
+
// Sweep along x: each item is compared with the active ones whose bounds still overlap it on screen, dropping the others in the same pass
|
|
25
|
+
const behind: Array<Array<number>> = [];
|
|
26
|
+
const hidden = new Uint8Array(elements.length);
|
|
27
|
+
const active: Array<number> = [];
|
|
28
|
+
for (const i of swept) {
|
|
29
|
+
const a = items[i]!;
|
|
30
|
+
active.length = active.reduce((kept, j) => {
|
|
31
|
+
const b = items[j]!;
|
|
32
|
+
if (b.max.x <= a.min.x + eps) {
|
|
33
|
+
return kept;
|
|
34
|
+
}
|
|
35
|
+
active[kept] = j;
|
|
36
|
+
const order = b.min.y < a.max.y - eps && a.min.y < b.max.y - eps ? getOrder(a, b) : 0;
|
|
37
|
+
if (order !== 0) {
|
|
38
|
+
const [back, front] = order < 0 ? [i, j] : [j, i];
|
|
39
|
+
(behind[front] ??= []).push(back);
|
|
40
|
+
hidden[back] ||= covers(items[front]!, items[back]!) ? 1 : 0;
|
|
41
|
+
}
|
|
42
|
+
return kept + 1;
|
|
43
|
+
}, 0);
|
|
44
|
+
active.push(i);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Depth first: an element is drawn when it's met on the stack again, after the elements behind it; 1 while it waits, 2 once drawn
|
|
48
|
+
const state = new Uint8Array(elements.length);
|
|
49
|
+
const stack: Array<number> = [];
|
|
50
|
+
const ordered: Array<zOrderElement> = [];
|
|
51
|
+
for (const start of byDepth) {
|
|
52
|
+
stack.push(start);
|
|
53
|
+
while (stack.length > 0) {
|
|
54
|
+
const i = stack.pop()!;
|
|
55
|
+
if (state[i] === 0) {
|
|
56
|
+
state[i] = 1;
|
|
57
|
+
stack.push(i);
|
|
58
|
+
behind[i]?.forEach((j) => state[j] === 0 && stack.push(j));
|
|
59
|
+
} else if (state[i] === 1) {
|
|
60
|
+
state[i] = 2;
|
|
61
|
+
if (!hidden[i]) {
|
|
62
|
+
ordered.push(elements[i]!);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return ordered;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Distances below this, in the scene's units, are equal */
|
|
71
|
+
const eps = 1e-3;
|
|
72
|
+
|
|
73
|
+
type Item = {
|
|
74
|
+
readonly points: ReadonlyArray<Vec3>;
|
|
75
|
+
readonly min: Vec3;
|
|
76
|
+
readonly max: Vec3;
|
|
77
|
+
/** The screen's normals of the polygon's edges, which separate it from polygons it doesn't overlap */
|
|
78
|
+
readonly axes: ReadonlyArray<readonly [number, number]>;
|
|
79
|
+
/** Its plane, oriented toward the viewer, unless it's a line or a point, or seen edge on */
|
|
80
|
+
readonly plane?: { readonly normal: Vec3; readonly d: number };
|
|
81
|
+
/** The winding on screen of an opaque convex polygon, 1 or -1, by which it can hide others; 0 for others */
|
|
82
|
+
readonly occluder: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** In one pass over the points: their bounds, edge normals on screen, plane by Newell's method, and whether they're a convex polygon */
|
|
86
|
+
function createItem(points: ReadonlyArray<Vec3>, opaque: boolean): Item {
|
|
87
|
+
const min = { x: Infinity, y: Infinity, z: Infinity };
|
|
88
|
+
const max = { x: -Infinity, y: -Infinity, z: -Infinity };
|
|
89
|
+
const normal = { x: 0, y: 0, z: 0 };
|
|
90
|
+
// The smallest and largest turn at a corner on screen, the same sign throughout for a convex polygon
|
|
91
|
+
const turns = { min: 0, max: 0 };
|
|
92
|
+
const axes: Array<readonly [number, number]> = [];
|
|
93
|
+
points.forEach((p, i) => {
|
|
94
|
+
Object.assign(min, { x: Math.min(min.x, p.x), y: Math.min(min.y, p.y), z: Math.min(min.z, p.z) });
|
|
95
|
+
Object.assign(max, { x: Math.max(max.x, p.x), y: Math.max(max.y, p.y), z: Math.max(max.z, p.z) });
|
|
96
|
+
const q = points[(i + 1) % points.length]!;
|
|
97
|
+
normal.x += (p.y - q.y) * (p.z + q.z);
|
|
98
|
+
normal.y += (p.z - q.z) * (p.x + q.x);
|
|
99
|
+
normal.z += (p.x - q.x) * (p.y + q.y);
|
|
100
|
+
if (Math.abs(q.x - p.x) + Math.abs(q.y - p.y) > eps) {
|
|
101
|
+
axes.push([q.y - p.y, p.x - q.x]);
|
|
102
|
+
}
|
|
103
|
+
const o = points[(i + points.length - 1) % points.length]!;
|
|
104
|
+
const turn = (p.x - o.x) * (q.y - p.y) - (p.y - o.y) * (q.x - p.x);
|
|
105
|
+
Object.assign(turns, { min: Math.min(turns.min, turn), max: Math.max(turns.max, turn) });
|
|
106
|
+
});
|
|
107
|
+
const convex = points.length >= 3 && (turns.min >= -eps || turns.max <= eps);
|
|
108
|
+
const occluder = opaque && convex ? Math.sign(normal.z) : 0;
|
|
109
|
+
const length = Math.hypot(normal.x, normal.y, normal.z);
|
|
110
|
+
if (points.length < 3 || length < eps || Math.abs(normal.z) < 1e-6 * length) {
|
|
111
|
+
return { points, min, max, axes, occluder: 0 };
|
|
112
|
+
}
|
|
113
|
+
// Toward the viewer, at +z
|
|
114
|
+
const s = Math.sign(normal.z) / length;
|
|
115
|
+
const n = { x: normal.x * s, y: normal.y * s, z: normal.z * s };
|
|
116
|
+
const p0 = points[0]!;
|
|
117
|
+
return { points, min, max, axes, plane: { normal: n, d: n.x * p0.x + n.y * p0.y + n.z * p0.z }, occluder };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** -1 if `a` is drawn before `b`, 1 if after, 0 if they don't overlap or nothing decides */
|
|
121
|
+
function getOrder(a: Item, b: Item): number {
|
|
122
|
+
if (a.max.z <= b.min.z + eps) {
|
|
123
|
+
return -1;
|
|
124
|
+
}
|
|
125
|
+
if (b.max.z <= a.min.z + eps) {
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
if (!overlap(a, b)) {
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
const aToB = b.plane ? getSide(a.points, b.plane) : 0;
|
|
132
|
+
if (aToB !== 0) {
|
|
133
|
+
return aToB;
|
|
134
|
+
}
|
|
135
|
+
return a.plane ? -getSide(b.points, a.plane) : 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** -1 if the points are all behind the plane, 1 if all in front of it, 0 if on both sides or in it */
|
|
139
|
+
function getSide(points: ReadonlyArray<Vec3>, { normal: n, d }: NonNullable<Item["plane"]>): number {
|
|
140
|
+
const range = { min: Infinity, max: -Infinity };
|
|
141
|
+
points.forEach((p) => {
|
|
142
|
+
const distance = n.x * p.x + n.y * p.y + n.z * p.z - d;
|
|
143
|
+
Object.assign(range, { min: Math.min(range.min, distance), max: Math.max(range.max, distance) });
|
|
144
|
+
});
|
|
145
|
+
return range.max <= eps && range.min < -eps ? -1 : range.min >= -eps && range.max > eps ? 1 : 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Whether the polygons overlap on screen by more than touching: no edge normal of either separates them */
|
|
149
|
+
function overlap(a: Item, b: Item): boolean {
|
|
150
|
+
return a.axes.every(([x, y]) => !separates(a, b, x, y)) && b.axes.every(([x, y]) => !separates(a, b, x, y));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function separates(a: Item, b: Item, x: number, y: number): boolean {
|
|
154
|
+
const range = { aMin: Infinity, aMax: -Infinity, bMin: Infinity, bMax: -Infinity };
|
|
155
|
+
a.points.forEach((p) => Object.assign(range, { aMin: Math.min(range.aMin, p.x * x + p.y * y), aMax: Math.max(range.aMax, p.x * x + p.y * y) }));
|
|
156
|
+
b.points.forEach((p) => Object.assign(range, { bMin: Math.min(range.bMin, p.x * x + p.y * y), bMax: Math.max(range.bMax, p.x * x + p.y * y) }));
|
|
157
|
+
const tolerance = eps * Math.hypot(x, y);
|
|
158
|
+
return range.aMax <= range.bMin + tolerance || range.bMax <= range.aMin + tolerance;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Whether the front's opaque convex polygon contains all of the back's points on screen, so the back can't be seen; not for a single point, e.g. text */
|
|
162
|
+
function covers(front: Item, back: Item): boolean {
|
|
163
|
+
const { points, occluder } = front;
|
|
164
|
+
return (
|
|
165
|
+
occluder !== 0 &&
|
|
166
|
+
back.points.length >= 2 &&
|
|
167
|
+
points.every((p, i) => {
|
|
168
|
+
const q = points[(i + 1) % points.length]!;
|
|
169
|
+
return back.points.every((r) => occluder * ((q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x)) >= -eps * Math.hypot(q.x - p.x, q.y - p.y));
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
}
|
package/src/renderers/svg/svg.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { plane } from "./svg-geometries/svg-plane.js";
|
|
|
29
29
|
import { polygon } from "./svg-geometries/svg-polygon.js";
|
|
30
30
|
import { shape } from "./svg-geometries/svg-shape.js";
|
|
31
31
|
import { text } from "./svg-geometries/svg-text.js";
|
|
32
|
+
import { orderElements } from "./svg-order.js";
|
|
32
33
|
|
|
33
34
|
export type SvgScene = {
|
|
34
35
|
readonly scene: Scene;
|
|
@@ -89,26 +90,29 @@ function renderInternal(
|
|
|
89
90
|
rotation: options?.rotation ?? 0,
|
|
90
91
|
imageBg: options?.imageBg ?? false,
|
|
91
92
|
};
|
|
92
|
-
|
|
93
|
+
// Isometric: the front turned 45° to show the side, and tilted by arctan(1/√2) to show the top, so the three axes are foreshortened alike
|
|
94
|
+
const isometric = opts.view === "TopLeft" || opts.view === "TopRight";
|
|
95
|
+
const view = isometric ? "front" : opts.view;
|
|
96
|
+
const isometricRot = isometric
|
|
97
|
+
? vec3RotCombine(vec3(Math.atan(1 / Math.SQRT2), 0, 0), vec3(0, ((opts.view === "TopRight" ? -1 : 1) * Math.PI) / 4, 0))
|
|
98
|
+
: vec3Zero;
|
|
99
|
+
const cameraRot = vec3RotCombine(isometricRot, rotationForCameraPos(view));
|
|
93
100
|
const viewRot = opts.rotation ? vec3RotCombine(vec3(0, 0, (opts.rotation * Math.PI) / 180), cameraRot) : cameraRot;
|
|
94
101
|
const unitRot = vec3RotCombine(viewRot, scene.rotation_deprecated ?? vec3Zero);
|
|
95
102
|
|
|
96
|
-
const unitCenter = vec3Rot(scene.center_deprecated ?? vec3Zero, vec3Zero,
|
|
103
|
+
const unitCenter = vec3Rot(scene.center_deprecated ?? vec3Zero, vec3Zero, cameraRot);
|
|
97
104
|
const [unitSize] = sizeBoundsForCameraPos(scene.size_deprecated, unitCenter, unitRot);
|
|
98
105
|
const svgSize = vec2(unitSize.x + 1.5 * opts.stroke_thickness, unitSize.y + 1.5 * opts.stroke_thickness);
|
|
99
106
|
const svgCenter = vec2(offset.x + opts.stroke_thickness * 0.75, offset.y + opts.stroke_thickness * 0.75);
|
|
100
107
|
const point = (x: number, y: number): Vec2 => vec2(svgCenter.x + x, svgCenter.y - y);
|
|
101
108
|
const unitCenterFlipped = vec3Flip(unitCenter);
|
|
102
109
|
|
|
103
|
-
const elements =
|
|
104
|
-
for (const g of scene.groups) {
|
|
105
|
-
elements.push(...svgGroup(g, unitCenterFlipped, unitRot, point, opts));
|
|
106
|
-
}
|
|
110
|
+
const elements = [...orderElements(scene.groups.flatMap((g) => svgGroup(g, unitCenterFlipped, unitRot, point, opts)))];
|
|
107
111
|
const dimOpts: SvgOptions = { ...opts, only_stroke: false, gray_scale: false };
|
|
108
|
-
|
|
109
|
-
const visibleViews = calculateVisibleViews(opts.view, scene.rotation_deprecated);
|
|
112
|
+
const visibleViews = calculateVisibleViews(view, scene.rotation_deprecated);
|
|
110
113
|
|
|
111
|
-
|
|
114
|
+
// Dimensions are drawn for the flat views
|
|
115
|
+
for (const dim of isometric ? [] : (scene.dimensions_deprecated?.dimensions ?? [])) {
|
|
112
116
|
const d = dimensionConvertToTypeMesh(dim, scene.rotation_deprecated ?? vec3Zero, scene.dimensions_deprecated?.material);
|
|
113
117
|
if (isViewVisible(d.views[0], visibleViews)) {
|
|
114
118
|
const pos = vec3TransRot(d.pos, unitCenterFlipped, unitRot);
|
|
@@ -45,6 +45,8 @@ export type BuildOptions = {
|
|
|
45
45
|
readonly materialStateImages?: Record<string, string>;
|
|
46
46
|
readonly useAlphaTest?: boolean;
|
|
47
47
|
readonly onTextureLoaded?: () => void;
|
|
48
|
+
/** Lit by the studio, whose reflections metals show; true by default */
|
|
49
|
+
readonly studio?: boolean;
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
export type BuiltScene = {
|
|
@@ -233,7 +235,7 @@ function createPart(
|
|
|
233
235
|
});
|
|
234
236
|
if (first.role === "solid") {
|
|
235
237
|
const c = getMaterialClass(first, targetStates[first.target]!);
|
|
236
|
-
const part = createSolidPart(entries, createClassMaterial(c));
|
|
238
|
+
const part = createSolidPart(entries, createClassMaterial(c, options.studio !== false));
|
|
237
239
|
Object.assign(part.object, { castShadow: c.castShadow, receiveShadow: c.lit });
|
|
238
240
|
return part;
|
|
239
241
|
}
|
|
@@ -21,18 +21,26 @@ import { ERROR_IMG_KEY } from "../appearance/appearance.js";
|
|
|
21
21
|
|
|
22
22
|
/** White materials, colored per instance, for each material class; images' materials and textures */
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Without reflections, i.e. without the studio, metals keep at most a little metalness, satin from the lights' highlights, as they'd be black, having
|
|
26
|
+
* almost no diffuse light
|
|
27
|
+
*/
|
|
28
|
+
export function createClassMaterial(c: MaterialClass, reflections: boolean): Material {
|
|
25
29
|
const side = c.side === "front" ? FrontSide : c.side === "back" ? BackSide : DoubleSide;
|
|
26
30
|
const common = { side, transparent: c.transparent, depthWrite: !c.transparent };
|
|
27
31
|
if (!c.lit) {
|
|
28
32
|
return new MeshBasicMaterial(common);
|
|
29
33
|
}
|
|
34
|
+
const metalness = c.type ? (finishes[c.type].metalness ?? 0) : c.metalness;
|
|
30
35
|
const material = c.type
|
|
31
|
-
? new MeshPhysicalMaterial({ ...common, ...finishes[c.type] })
|
|
32
|
-
: new MeshStandardMaterial({ ...common, metalness:
|
|
36
|
+
? new MeshPhysicalMaterial({ ...common, ...finishes[c.type], metalness: reflections ? metalness : Math.min(metalness, plainMetalness) })
|
|
37
|
+
: new MeshStandardMaterial({ ...common, metalness: reflections ? metalness : Math.min(metalness, plainMetalness), roughness: c.roughness });
|
|
33
38
|
return Object.assign(material, { onBeforeCompile: roundEdges });
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
/** The most metalness without reflections */
|
|
42
|
+
const plainMetalness = 0.4;
|
|
43
|
+
|
|
36
44
|
/**
|
|
37
45
|
* The physical finish of each type of surface, in the material's color: metals reflect their color; galvanized and aluminum sheet keep some of their
|
|
38
46
|
* color diffuse, so faces that mirror the studio's dark surroundings aren't black; the others are dielectrics with a white reflection; painted sheet
|
|
@@ -43,8 +43,14 @@ export type ViewerOptions<Content> = {
|
|
|
43
43
|
readonly gizmo?: ControlsHelper;
|
|
44
44
|
/** Lights added to the photographed studio and key light that light the scene; none by default */
|
|
45
45
|
readonly lights?: ReadonlyArray<Object3D> | ((scene: Scene) => ReadonlyArray<Object3D>);
|
|
46
|
-
/** Of the studio's light, 0.
|
|
46
|
+
/** Of the studio's light, or without the studio of its plain light, 0.6 by default, the key light giving the rest */
|
|
47
47
|
readonly environmentIntensity?: number;
|
|
48
|
+
/**
|
|
49
|
+
* The photographed studio's light and reflections, on by default. Off, three-point lighting emulates it, a weak sky and floor light, the key light,
|
|
50
|
+
* a fill and a rim light, without reflections, so metals are satin at most; a first visit then shows the scene about 400 ms sooner, as its shaders
|
|
51
|
+
* compile without the reflection code. Changing it rebuilds the scene
|
|
52
|
+
*/
|
|
53
|
+
readonly studio?: boolean;
|
|
48
54
|
/** A soft shadow on the ground under the scene, rendered once per build; on by default */
|
|
49
55
|
readonly groundShadow?: boolean;
|
|
50
56
|
/** The solids' shadows on themselves, e.g. under a roof, cast by the key light from the upper left front; on by default */
|
|
@@ -90,6 +96,7 @@ export const effects: { readonly [K in keyof ViewerOptions<unknown>]-?: Readonly
|
|
|
90
96
|
gizmo: ["gizmo"],
|
|
91
97
|
lights: ["lights"],
|
|
92
98
|
environmentIntensity: ["lights"],
|
|
99
|
+
studio: ["build", "lights"],
|
|
93
100
|
groundShadow: ["shadow"],
|
|
94
101
|
shadows: ["shadow"],
|
|
95
102
|
quality: ["quality"],
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type BufferGeometry,
|
|
3
|
+
DirectionalLight,
|
|
3
4
|
Group,
|
|
5
|
+
HemisphereLight,
|
|
4
6
|
NeutralToneMapping,
|
|
5
7
|
OrthographicCamera,
|
|
6
8
|
PerspectiveCamera,
|
|
@@ -136,7 +138,9 @@ export function createViewer<Content>(container: HTMLElement, options: ViewerOpt
|
|
|
136
138
|
};
|
|
137
139
|
v.scene.add(v.lights);
|
|
138
140
|
// Loaded while the first frame builds the scene
|
|
139
|
-
|
|
141
|
+
if (options.studio !== false) {
|
|
142
|
+
loadStudio(v).catch(() => undefined);
|
|
143
|
+
}
|
|
140
144
|
const signal = { signal: v.listeners.signal };
|
|
141
145
|
canvas.addEventListener("pointermove", (e) => onPointerMove(v, e), signal);
|
|
142
146
|
canvas.addEventListener("pointerleave", (e) => setHovered(v, undefined, e), signal);
|
|
@@ -282,7 +286,7 @@ function rebuild<Content>(v: Viewer<Content>): void {
|
|
|
282
286
|
v.scene.add(v.built.root);
|
|
283
287
|
v.built.dimensions.visible = options.showDimensions ?? true;
|
|
284
288
|
// Compiled in parallel where the driver can, as three.js's compileAsync example, and after the studio arrived, which they use
|
|
285
|
-
loadStudio(v)
|
|
289
|
+
(options.studio === false ? Promise.resolve() : loadStudio(v))
|
|
286
290
|
.catch(() => undefined)
|
|
287
291
|
.then(() => v.renderer.compileAsync(v.scene, v.camera))
|
|
288
292
|
.then(() => onLoaded(v))
|
|
@@ -517,12 +521,26 @@ function getGroupArgs<Content>(
|
|
|
517
521
|
}
|
|
518
522
|
}
|
|
519
523
|
|
|
520
|
-
/** The scene is lit by a photographed studio as its environment; apps can add their own lights */
|
|
524
|
+
/** The scene is lit by a photographed studio as its environment, or without it by a plain sky and floor light; apps can add their own lights */
|
|
521
525
|
function updateLights<Content>(v: Viewer<Content>): void {
|
|
522
|
-
const { scene, lights, environmentIntensity = studioIntensity } = v.options;
|
|
526
|
+
const { scene, lights, environmentIntensity = studioIntensity, studio = true } = v.options;
|
|
523
527
|
v.lights.clear();
|
|
524
528
|
(typeof lights === "function" ? (scene ? lights(scene) : []) : (lights ?? [])).forEach((o) => v.lights.add(o));
|
|
525
529
|
v.scene.environmentIntensity = environmentIntensity;
|
|
530
|
+
v.scene.environment = studio ? (v.studio.texture ?? null) : null;
|
|
531
|
+
if (studio) {
|
|
532
|
+
loadStudio(v).catch(() => undefined);
|
|
533
|
+
} else {
|
|
534
|
+
// The studio emulated by three-point lighting: a weak sky and floor light, so faces differ by which way they turn, the key light, a fill from
|
|
535
|
+
// the other side, so the key light's shadow side isn't flat, and a rim light from behind, which outlines edges
|
|
536
|
+
const [fill, rim] = [
|
|
537
|
+
new DirectionalLight(0xffffff, environmentIntensity * fillIntensity),
|
|
538
|
+
new DirectionalLight(0xffffff, environmentIntensity * rimIntensity),
|
|
539
|
+
];
|
|
540
|
+
fill.position.copy(fillDirection);
|
|
541
|
+
rim.position.copy(rimDirection);
|
|
542
|
+
v.lights.add(new HemisphereLight(0xffffff, floorColor, environmentIntensity * skyIntensity), fill, rim);
|
|
543
|
+
}
|
|
526
544
|
}
|
|
527
545
|
|
|
528
546
|
/** The studio, loaded once, its own chunk; set when it arrives, so the scene's shaders are compiled after it, with it */
|
|
@@ -535,7 +553,7 @@ function loadStudio<Content>(v: Viewer<Content>): Promise<unknown> {
|
|
|
535
553
|
return texture;
|
|
536
554
|
}
|
|
537
555
|
v.studio.texture = texture;
|
|
538
|
-
v.scene.environment = texture;
|
|
556
|
+
v.scene.environment = v.options.studio === false ? null : texture;
|
|
539
557
|
invalidate(v);
|
|
540
558
|
return texture;
|
|
541
559
|
});
|
|
@@ -565,6 +583,14 @@ function updateOverlays<Content>(v: Viewer<Content>): void {
|
|
|
565
583
|
|
|
566
584
|
/** The studio's share of the light, the key light giving the rest, so the key light's shadows show */
|
|
567
585
|
const studioIntensity = 0.6;
|
|
586
|
+
/** Without the studio, of its three-point lighting, by the environment's intensity; the key light is the studio's, the floor as the studio's */
|
|
587
|
+
const skyIntensity = 1.5;
|
|
588
|
+
const fillIntensity = 1.5;
|
|
589
|
+
const rimIntensity = 2;
|
|
590
|
+
const floorColor = 0xe6e6e6;
|
|
591
|
+
/** From the right, opposite the key light from the upper left front, and lower; the rim light from behind and above */
|
|
592
|
+
const fillDirection = new Vector3(1.5, 0.5, 1);
|
|
593
|
+
const rimDirection = new Vector3(0.5, 1.5, -1.5);
|
|
568
594
|
|
|
569
595
|
const raycaster = new Raycaster();
|
|
570
596
|
const ndc = new Vector2();
|