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.
Files changed (76) hide show
  1. package/lib/renderers/react/react.js +2 -2
  2. package/lib/renderers/react/react.js.map +1 -1
  3. package/lib/renderers/svg/index.d.ts +1 -1
  4. package/lib/renderers/svg/index.d.ts.map +1 -1
  5. package/lib/renderers/svg/svg-geometries/shared.d.ts +14 -2
  6. package/lib/renderers/svg/svg-geometries/shared.d.ts.map +1 -1
  7. package/lib/renderers/svg/svg-geometries/shared.js +15 -1
  8. package/lib/renderers/svg/svg-geometries/shared.js.map +1 -1
  9. package/lib/renderers/svg/svg-geometries/svg-box.d.ts.map +1 -1
  10. package/lib/renderers/svg/svg-geometries/svg-box.js +41 -30
  11. package/lib/renderers/svg/svg-geometries/svg-box.js.map +1 -1
  12. package/lib/renderers/svg/svg-geometries/svg-cone.d.ts.map +1 -1
  13. package/lib/renderers/svg/svg-geometries/svg-cone.js +3 -3
  14. package/lib/renderers/svg/svg-geometries/svg-cone.js.map +1 -1
  15. package/lib/renderers/svg/svg-geometries/svg-cylinder.d.ts.map +1 -1
  16. package/lib/renderers/svg/svg-geometries/svg-cylinder.js +18 -6
  17. package/lib/renderers/svg/svg-geometries/svg-cylinder.js.map +1 -1
  18. package/lib/renderers/svg/svg-geometries/svg-image.d.ts.map +1 -1
  19. package/lib/renderers/svg/svg-geometries/svg-image.js +3 -2
  20. package/lib/renderers/svg/svg-geometries/svg-image.js.map +1 -1
  21. package/lib/renderers/svg/svg-geometries/svg-line.d.ts.map +1 -1
  22. package/lib/renderers/svg/svg-geometries/svg-line.js +3 -1
  23. package/lib/renderers/svg/svg-geometries/svg-line.js.map +1 -1
  24. package/lib/renderers/svg/svg-geometries/svg-plane.d.ts.map +1 -1
  25. package/lib/renderers/svg/svg-geometries/svg-plane.js +4 -2
  26. package/lib/renderers/svg/svg-geometries/svg-plane.js.map +1 -1
  27. package/lib/renderers/svg/svg-geometries/svg-polygon.d.ts.map +1 -1
  28. package/lib/renderers/svg/svg-geometries/svg-polygon.js +4 -2
  29. package/lib/renderers/svg/svg-geometries/svg-polygon.js.map +1 -1
  30. package/lib/renderers/svg/svg-geometries/svg-shape.d.ts.map +1 -1
  31. package/lib/renderers/svg/svg-geometries/svg-shape.js +4 -2
  32. package/lib/renderers/svg/svg-geometries/svg-shape.js.map +1 -1
  33. package/lib/renderers/svg/svg-geometries/svg-text.js +1 -1
  34. package/lib/renderers/svg/svg-geometries/svg-text.js.map +1 -1
  35. package/lib/renderers/svg/svg-order.d.ts +9 -0
  36. package/lib/renderers/svg/svg-order.d.ts.map +1 -0
  37. package/lib/renderers/svg/svg-order.js +148 -0
  38. package/lib/renderers/svg/svg-order.js.map +1 -0
  39. package/lib/renderers/svg/svg.d.ts.map +1 -1
  40. package/lib/renderers/svg/svg.js +13 -9
  41. package/lib/renderers/svg/svg.js.map +1 -1
  42. package/lib/renderers/three/build/build.d.ts +2 -0
  43. package/lib/renderers/three/build/build.d.ts.map +1 -1
  44. package/lib/renderers/three/build/build.js +1 -1
  45. package/lib/renderers/three/build/build.js.map +1 -1
  46. package/lib/renderers/three/build/materials.d.ts +5 -1
  47. package/lib/renderers/three/build/materials.d.ts.map +1 -1
  48. package/lib/renderers/three/build/materials.js +10 -3
  49. package/lib/renderers/three/build/materials.js.map +1 -1
  50. package/lib/renderers/three/viewer/options.d.ts +7 -1
  51. package/lib/renderers/three/viewer/options.d.ts.map +1 -1
  52. package/lib/renderers/three/viewer/options.js +1 -0
  53. package/lib/renderers/three/viewer/options.js.map +1 -1
  54. package/lib/renderers/three/viewer/viewer.d.ts.map +1 -1
  55. package/lib/renderers/three/viewer/viewer.js +31 -6
  56. package/lib/renderers/three/viewer/viewer.js.map +1 -1
  57. package/package.json +1 -1
  58. package/src/renderers/react/react.tsx +2 -2
  59. package/src/renderers/svg/index.ts +1 -1
  60. package/src/renderers/svg/svg-geometries/shared.ts +26 -3
  61. package/src/renderers/svg/svg-geometries/svg-box.ts +42 -36
  62. package/src/renderers/svg/svg-geometries/svg-cone.ts +17 -3
  63. package/src/renderers/svg/svg-geometries/svg-cylinder.ts +25 -6
  64. package/src/renderers/svg/svg-geometries/svg-image.ts +3 -2
  65. package/src/renderers/svg/svg-geometries/svg-line.ts +3 -1
  66. package/src/renderers/svg/svg-geometries/svg-plane.ts +9 -2
  67. package/src/renderers/svg/svg-geometries/svg-polygon.ts +9 -2
  68. package/src/renderers/svg/svg-geometries/svg-shape.ts +9 -2
  69. package/src/renderers/svg/svg-geometries/svg-text.ts +1 -1
  70. package/src/renderers/svg/svg-order.test.ts +35 -0
  71. package/src/renderers/svg/svg-order.ts +172 -0
  72. package/src/renderers/svg/svg.ts +13 -9
  73. package/src/renderers/three/build/build.ts +3 -1
  74. package/src/renderers/three/build/materials.ts +11 -3
  75. package/src/renderers/three/viewer/options.ts +8 -1
  76. 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
+ }
@@ -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
- const cameraRot = rotationForCameraPos(opts.view);
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, rotationForCameraPos(opts.view));
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 = Array<zOrderElement>();
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
- elements.sort((a, b) => a.zOrder - b.zOrder);
109
- const visibleViews = calculateVisibleViews(opts.view, scene.rotation_deprecated);
112
+ const visibleViews = calculateVisibleViews(view, scene.rotation_deprecated);
110
113
 
111
- for (const dim of scene.dimensions_deprecated?.dimensions ?? []) {
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
- export function createClassMaterial(c: MaterialClass): Material {
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: c.metalness, roughness: c.roughness });
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.8 by default, the key light giving the rest */
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
- loadStudio(v).catch(() => undefined);
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();