@expofp/renderer 3.34.1 → 3.35.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/dist/Renderer.d.ts +14 -0
- package/dist/Renderer.js +16 -4
- package/dist/core/shader-source.d.ts +20 -0
- package/dist/core/shader-source.js +26 -0
- package/dist/debug/components/Gui.d.ts +2 -0
- package/dist/debug/components/Gui.js +11 -1
- package/dist/external/ExternalCanvas.js +5 -6
- package/dist/external/external-camera.js +26 -11
- package/dist/index.d.ts +1 -0
- package/dist/objects/Background.js +1 -1
- package/dist/objects/containers/SlotContainer.js +7 -1
- package/dist/objects/containers/hooks.js +1 -1
- package/dist/objects/containers/props.d.ts +10 -0
- package/dist/objects/images/Images.js +9 -2
- package/dist/objects/images/image-container.js +1 -1
- package/dist/objects/layer/Layer.js +10 -10
- package/dist/objects/layer/mount-scheduler.d.ts +83 -54
- package/dist/objects/layer/mount-scheduler.js +204 -60
- package/dist/objects/lighting/Lighting.d.ts +92 -0
- package/dist/objects/lighting/Lighting.js +385 -0
- package/dist/objects/lighting/OcclusionPasses.d.ts +23 -0
- package/dist/objects/lighting/OcclusionPasses.js +271 -0
- package/dist/objects/lighting/index.d.ts +11 -0
- package/dist/objects/lighting/index.js +10 -0
- package/dist/objects/lighting/lighting-gui.d.ts +24 -0
- package/dist/objects/lighting/lighting-gui.js +114 -0
- package/dist/objects/lighting/occluder.d.ts +34 -0
- package/dist/objects/lighting/occluder.js +62 -0
- package/dist/objects/lighting/occlusion-targets.d.ts +143 -0
- package/dist/objects/lighting/occlusion-targets.js +276 -0
- package/dist/objects/lines/Lines.js +3 -1
- package/dist/objects/materials/ColorMaterial.d.ts +58 -51
- package/dist/objects/materials/ColorMaterial.js +74 -52
- package/dist/objects/materials/LineMaterial.js +1 -1
- package/dist/objects/materials/ModelMaterial.d.ts +30 -18
- package/dist/objects/materials/ModelMaterial.js +68 -41
- package/dist/objects/materials/TextureMaterial.js +1 -1
- package/dist/objects/materials/effects/ao/ambient-occlusion-pass.d.ts +77 -0
- package/dist/objects/materials/effects/ao/ambient-occlusion-pass.js +296 -0
- package/dist/objects/materials/effects/ao/ambient-occlusion-patch.d.ts +29 -0
- package/dist/objects/materials/effects/ao/ambient-occlusion-patch.js +115 -0
- package/dist/objects/materials/effects/ao/blur-pass.d.ts +61 -0
- package/dist/objects/materials/effects/ao/blur-pass.js +101 -0
- package/dist/objects/materials/effects/ao/index.d.ts +129 -0
- package/dist/objects/materials/effects/ao/index.js +72 -0
- package/dist/objects/materials/{dim.d.ts → effects/color-dim.d.ts} +4 -4
- package/dist/objects/materials/{dim.js → effects/color-dim.js} +6 -24
- package/dist/objects/materials/effects/contact-shadow.d.ts +100 -0
- package/dist/objects/materials/effects/contact-shadow.js +167 -0
- package/dist/objects/materials/effects/effects-context.d.ts +52 -0
- package/dist/objects/materials/effects/effects-context.js +24 -0
- package/dist/objects/models/Models.js +18 -2
- package/dist/objects/models/model-container.js +1 -1
- package/dist/objects/shapes/Shapes.js +20 -2
- package/dist/objects/text/Texts.js +3 -1
- package/dist/objects/text/text-container.js +1 -1
- package/dist/space/Scene.d.ts +3 -0
- package/dist/space/Scene.js +17 -5
- package/package.json +2 -2
- package/dist/objects/Lighting.d.ts +0 -20
- package/dist/objects/Lighting.js +0 -85
|
@@ -5,6 +5,7 @@ import { InstancedMesh } from '../../core/instanced/InstancedMesh';
|
|
|
5
5
|
import { sameSequence } from '../../utils/arrays';
|
|
6
6
|
import { createLogger } from '../../utils/logging';
|
|
7
7
|
import { defKey, useContainer } from '../containers';
|
|
8
|
+
import { useOccluder } from '../lighting/occluder';
|
|
8
9
|
import { ModelMaterial } from '../materials/ModelMaterial';
|
|
9
10
|
import { loadModel } from './gltf-cache';
|
|
10
11
|
import { MODEL_KINDS, ModelContainer, modelKindOf, OPACITY_UNIFORM, } from './model-container';
|
|
@@ -28,6 +29,10 @@ const logger = createLogger('models');
|
|
|
28
29
|
*/
|
|
29
30
|
export function Models(props) {
|
|
30
31
|
const { defs } = props;
|
|
32
|
+
// Handed to each part's mesh as a prefix of *its* members, the way `Shapes` hands one to each of
|
|
33
|
+
// its batches: a model layer is small and its meshes are few, and the scheduler sizes the next
|
|
34
|
+
// slice from what this one actually cost.
|
|
35
|
+
const mounted = props.mounted ?? defs.length;
|
|
31
36
|
// The layer split two ways at once: by url, because that is what a mesh's geometry comes from,
|
|
32
37
|
// and by kind, because `transparent` is a material property and a translucent copy of a model
|
|
33
38
|
// therefore cannot share a mesh with an opaque one (see `modelKindOf`).
|
|
@@ -84,7 +89,7 @@ export function Models(props) {
|
|
|
84
89
|
const members = partition.batches[kind].get(asset.url);
|
|
85
90
|
if (!members)
|
|
86
91
|
return null;
|
|
87
|
-
return (_jsx(Fragment, { children: asset.parts.map((part) => (_jsx(ModelPartMesh, { asset: asset, part: part, defs: members, kind: kind, renderOrder: renderOrder, onKindMismatch: repartition }, part.id))) }, kind));
|
|
92
|
+
return (_jsx(Fragment, { children: asset.parts.map((part) => (_jsx(ModelPartMesh, { asset: asset, part: part, defs: members, mounted: mounted, kind: kind, renderOrder: renderOrder, onKindMismatch: repartition }, part.id))) }, kind));
|
|
88
93
|
}) }, asset.url))) }));
|
|
89
94
|
}
|
|
90
95
|
/**
|
|
@@ -128,11 +133,14 @@ function emptied(previous) {
|
|
|
128
133
|
*/
|
|
129
134
|
function ModelPartMesh(props) {
|
|
130
135
|
const { asset, part, defs, kind, renderOrder, onKindMismatch } = props;
|
|
136
|
+
// Only the instances are staged; the container is sized and ordered from every member.
|
|
137
|
+
const mounted = props.mounted ?? defs.length;
|
|
131
138
|
// Ordering is a no-op for models — their container declares it so, and the mesh depth-sorts itself
|
|
132
139
|
// instead — but the hit resolution is not: `InstancedMeshExt.raycast` walks the live slots,
|
|
133
140
|
// skipping deleted and hidden ones, and raycasts the real merged geometry, so a hit names the
|
|
134
141
|
// model's actual triangles and the slot that drew them.
|
|
135
142
|
const container = useContainer(() => new ModelContainer(asset.extents, defs.length, kind), defs);
|
|
143
|
+
const occluder = useOccluder();
|
|
136
144
|
// Assigned after mount rather than passed to the constructor: the container is built once and
|
|
137
145
|
// outlives every render, so it cannot close over a current callback. Nothing can fire before this
|
|
138
146
|
// runs — the split that placed these defs was computed from the same opacities they mount with.
|
|
@@ -150,7 +158,15 @@ function ModelPartMesh(props) {
|
|
|
150
158
|
// needs (see `ModelContainer.meshMatrix`); `matrixAutoUpdate` off because it is composed, not
|
|
151
159
|
// decomposed from position/rotation/scale. `sortObjects` is what a depth-writing draw wants in
|
|
152
160
|
// place of paint order — nearest first while opaque, farthest first once the material blends.
|
|
153
|
-
|
|
161
|
+
//
|
|
162
|
+
// The opaque mesh asks for occlusion, or a model would neither occlude nor be occluded; the
|
|
163
|
+
// translucent one stays out, because a surface you can see through must not cast full occlusion,
|
|
164
|
+
// and asks with `null` — which is also what a detaching `ref` says, so the layer number and the
|
|
165
|
+
// scene's registry are `useOccluder`'s alone.
|
|
166
|
+
return (_jsxs(_Fragment, { children: [_jsxs(InstancedMesh, { args: container.args, ref: (mesh) => {
|
|
167
|
+
container.bind(mesh);
|
|
168
|
+
occluder(kind === 'opaque' ? mesh : null);
|
|
169
|
+
}, matrixAutoUpdate: false, matrix: container.meshMatrix, sortObjects: true, renderOrder: renderOrder, children: [_jsx("primitive", { object: part.geometry, attach: "geometry", dispose: null }), _jsx(ModelMaterial, { source: part.material, opacityUniform: OPACITY_UNIFORM, translucent: kind === 'translucent' })] }), defs.slice(0, mounted).map((def) => (_jsx(ModelInstance, { def: def, container: container }, defKey(def))))] }));
|
|
154
170
|
}
|
|
155
171
|
/**
|
|
156
172
|
* Loads one model, reporting a failure rather than rejecting — one missing model should not cost a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Matrix4, Quaternion, Vector3 } from 'three';
|
|
2
2
|
import { SlotContainer } from '../containers';
|
|
3
|
-
import { DIM_SCHEMA } from '../materials/dim';
|
|
3
|
+
import { DIM_SCHEMA } from '../materials/effects/color-dim';
|
|
4
4
|
/**
|
|
5
5
|
* The per-instance uniform a model mesh reads its opacity from. The container owns the name: it
|
|
6
6
|
* declares the schema on bind and writes each slot's value, and hands the name to `ModelMaterial`,
|
|
@@ -2,6 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { BatchedMesh } from '../../core/batch/BatchedMesh';
|
|
4
4
|
import { defKey, useContainer } from '../containers';
|
|
5
|
+
import { useOccluder } from '../lighting/occluder';
|
|
5
6
|
import { ColorMaterial } from '../materials/ColorMaterial';
|
|
6
7
|
import { SHAPE_KINDS, ShapeContainer, shapeKindOf } from './shape-container';
|
|
7
8
|
import { ShapeInstance } from './ShapeInstance';
|
|
@@ -22,6 +23,11 @@ import { ShapeInstance } from './ShapeInstance';
|
|
|
22
23
|
*/
|
|
23
24
|
export function Shapes(props) {
|
|
24
25
|
const { defs } = props;
|
|
26
|
+
// Handed to each batch as a prefix of *its* members rather than apportioned between them: the
|
|
27
|
+
// batches are three at most, so a slice can overshoot the scheduler's budget by that factor at
|
|
28
|
+
// worst, and the scheduler measures what a slice actually cost and sizes the next one from it.
|
|
29
|
+
// Apportioning would cost a walk of the layer's defs per render to find where the prefix falls.
|
|
30
|
+
const mounted = props.mounted ?? defs.length;
|
|
25
31
|
// Held in state rather than derived each render, the same "adjust on prop change" shape `Images`
|
|
26
32
|
// uses for its pack — because the split has a second trigger besides `defs`. A def's color and its
|
|
27
33
|
// shape are ordinary slot writes that never re-render this component, so either one crossing a
|
|
@@ -64,7 +70,7 @@ export function Shapes(props) {
|
|
|
64
70
|
const members = batches[kind];
|
|
65
71
|
if (!members)
|
|
66
72
|
return null;
|
|
67
|
-
return (_jsx(ShapeBatch, { defs: members, kind: kind, renderOrder: renderOrder, onKindMismatch: repartition }, kind));
|
|
73
|
+
return (_jsx(ShapeBatch, { defs: members, mounted: mounted, kind: kind, renderOrder: renderOrder, onKindMismatch: repartition }, kind));
|
|
68
74
|
}) }));
|
|
69
75
|
}
|
|
70
76
|
/**
|
|
@@ -92,6 +98,8 @@ function partitionShapes(defs, mounted) {
|
|
|
92
98
|
*/
|
|
93
99
|
function ShapeBatch(props) {
|
|
94
100
|
const { defs, kind, renderOrder, onKindMismatch } = props;
|
|
101
|
+
// Only the instances are staged; the container is sized, reserved and ordered from every member.
|
|
102
|
+
const mounted = props.mounted ?? defs.length;
|
|
95
103
|
// Sized from the first render's defs; membership changes grow the mesh in place, never rebuild
|
|
96
104
|
// it. The container is bookkeeping plus CPU-side template geometries, so the copy StrictMode's
|
|
97
105
|
// double-invoked initializer discards and the one an unmount strands are both plain garbage.
|
|
@@ -99,6 +107,7 @@ function ShapeBatch(props) {
|
|
|
99
107
|
// Stickiness costs this nothing: a batch mounts on the render that first fills it, so the defs
|
|
100
108
|
// sizing the container are a real membership rather than the empty one it may later stand at.
|
|
101
109
|
const container = useContainer(() => new ShapeContainer(defs, kind), defs);
|
|
110
|
+
const occluder = useOccluder();
|
|
102
111
|
// Assigned after mount rather than passed to the constructor: the container is built once and
|
|
103
112
|
// outlives every render, so it cannot close over a current callback. Nothing can fire before this
|
|
104
113
|
// runs — the split that placed these defs was computed from the same colors they mount with.
|
|
@@ -116,5 +125,14 @@ function ShapeBatch(props) {
|
|
|
116
125
|
// claimed slot — `capacity` is a frozen snapshot, so that never happens. The `ref` arrow is
|
|
117
126
|
// deliberate: `ref={container.bind}` would make the React Compiler infer `container` itself as a
|
|
118
127
|
// ref and reject its render-time reads.
|
|
119
|
-
|
|
128
|
+
//
|
|
129
|
+
// The opaque volumes are what the depth prepass draws, so their batch asks for occlusion: the
|
|
130
|
+
// translucent ones stay out because a surface you can see through must not cast full occlusion,
|
|
131
|
+
// and the planar ones stay out because that is what keeps ambient occlusion and the contact
|
|
132
|
+
// shadow disjoint. A batch of either of those kinds asks with `null`, which is also what a
|
|
133
|
+
// detaching `ref` says — so the layer number and the scene's registry are `useOccluder`'s alone.
|
|
134
|
+
return (_jsxs(_Fragment, { children: [_jsx(BatchedMesh, { args: container.capacity, ref: (mesh) => {
|
|
135
|
+
container.bind(mesh);
|
|
136
|
+
occluder(kind === 'solid' ? mesh : null);
|
|
137
|
+
}, customSort: container.customSort, renderOrder: renderOrder, children: _jsx(ColorMaterial, { solid: kind !== 'planar', translucent: kind === 'translucent' }) }), defs.slice(0, mounted).map((def) => (_jsx(ShapeInstance, { def: def, container: container }, defKey(def))))] }));
|
|
120
138
|
}
|
|
@@ -31,11 +31,13 @@ import { TextInstance } from './TextInstance';
|
|
|
31
31
|
*/
|
|
32
32
|
export function Texts(props) {
|
|
33
33
|
const { defs } = props;
|
|
34
|
+
// Only the instances are staged; the container is built and ordered from the whole membership.
|
|
35
|
+
const mounted = props.mounted ?? defs.length;
|
|
34
36
|
const invalidate = useThree((state) => state.invalidate);
|
|
35
37
|
// The container is bookkeeping only — the members and the mesh are created against R3F's object —
|
|
36
38
|
// so the copy StrictMode's double-invoked initializer discards is plain garbage.
|
|
37
39
|
const container = useContainer(() => new TextContainer(invalidate), defs);
|
|
38
40
|
// The `ref` arrow (not `ref={container.bind}`) keeps the React Compiler from inferring `container`
|
|
39
41
|
// itself as a ref and rejecting its render-time reads.
|
|
40
|
-
return (_jsxs(_Fragment, { children: [_jsx(BatchedText, { ref: (mesh) => container.bind(mesh), children: _jsx(ColorMaterial, {}) }), defs.map((def) => (_jsx(TextInstance, { def: def, container: container }, defKey(def))))] }));
|
|
42
|
+
return (_jsxs(_Fragment, { children: [_jsx(BatchedText, { ref: (mesh) => container.bind(mesh), children: _jsx(ColorMaterial, {}) }), defs.slice(0, mounted).map((def) => (_jsx(TextInstance, { def: def, container: container }, defKey(def))))] }));
|
|
41
43
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TextExt } from '../../core/text/Text';
|
|
2
2
|
import { sameSequence } from '../../utils/arrays';
|
|
3
3
|
import { SlotContainer } from '../containers';
|
|
4
|
-
import { dimOrdered, isDimmed } from '../materials/dim';
|
|
4
|
+
import { dimOrdered, isDimmed } from '../materials/effects/color-dim';
|
|
5
5
|
import { LAYOUT_FONT_SIZE, LINE_HEIGHT } from './text-layout';
|
|
6
6
|
/**
|
|
7
7
|
* One text layer's bookkeeping: a group of members per def, and the paint order across them.
|
package/dist/space/Scene.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Box } from '@expofp/geometry';
|
|
2
2
|
import { type ReactNode } from 'react';
|
|
3
|
+
import { type LightingProps } from '../objects/lighting';
|
|
3
4
|
import { type SceneDef } from '../types/defs';
|
|
4
5
|
import { type CameraProps } from './Camera';
|
|
5
6
|
/** Props for {@link Scene}: the scene to draw, plus the camera limits it passes straight down. */
|
|
@@ -15,6 +16,8 @@ export interface SceneProps extends CameraProps {
|
|
|
15
16
|
* The part of the canvas the plan should be framed inside; see {@link RendererProps.visibleRect}.
|
|
16
17
|
*/
|
|
17
18
|
visibleRect?: Box;
|
|
19
|
+
/** The lighting and darkening values a caller set; see {@link RendererProps.lighting}. */
|
|
20
|
+
lighting?: LightingProps;
|
|
18
21
|
/**
|
|
19
22
|
* Whether an external host owns the camera. When set, the scene mounts no {@link Camera} — and
|
|
20
23
|
* with it no fit, no `visibleRect` framing and no camera commands: the view arrives from the
|
package/dist/space/Scene.js
CHANGED
|
@@ -6,7 +6,8 @@ import { Background } from '../objects/Background';
|
|
|
6
6
|
import { TextureBudget, TextureBudgetContext } from '../objects/images/texture-budget';
|
|
7
7
|
import { Layer } from '../objects/layer/index';
|
|
8
8
|
import { createMountScheduler, MountSchedulerContext } from '../objects/layer/mount-scheduler';
|
|
9
|
-
import { Lighting } from '../objects/
|
|
9
|
+
import { Lighting, OcclusionPasses } from '../objects/lighting';
|
|
10
|
+
import { createSceneEffects, SceneEffectsContext, } from '../objects/materials/effects/effects-context';
|
|
10
11
|
import { createLogger, printScene } from '../utils/logging';
|
|
11
12
|
import { Camera } from './Camera';
|
|
12
13
|
import { useCanvasSize } from './canvas-size';
|
|
@@ -26,14 +27,14 @@ const logger = createLogger('scene');
|
|
|
26
27
|
* @returns The scene.
|
|
27
28
|
*/
|
|
28
29
|
export function Scene(props) {
|
|
29
|
-
const { sceneDef, memoryLimit, visibleRect, external, children, ...cameraProps } = props;
|
|
30
|
+
const { sceneDef, memoryLimit, lighting, visibleRect, external, children, ...cameraProps } = props;
|
|
30
31
|
const { viewbox, staticTransform } = sceneDef;
|
|
31
32
|
useEffect(() => {
|
|
32
33
|
if (visibleRect && staticTransform) {
|
|
33
34
|
logger.warn('visibleRect is ignored for a scene with a staticTransform');
|
|
34
35
|
}
|
|
35
36
|
}, [visibleRect, staticTransform]);
|
|
36
|
-
const graph = (_jsx(SceneGraph, { sceneDef: sceneDef, memoryLimit: memoryLimit, children: children }));
|
|
37
|
+
const graph = (_jsx(SceneGraph, { sceneDef: sceneDef, memoryLimit: memoryLimit, lighting: lighting, children: children }));
|
|
37
38
|
return (_jsx(PlanSpace, { sceneDef: sceneDef, children: external ? (graph) : (_jsxs(Camera, { ...cameraProps, children: [visibleRect && !staticTransform && _jsx(VisibleRect, { rect: visibleRect, viewbox: viewbox }), graph] })) }));
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
@@ -78,7 +79,7 @@ function PlanSpace(props) {
|
|
|
78
79
|
* @returns The graph.
|
|
79
80
|
*/
|
|
80
81
|
function SceneGraph(props) {
|
|
81
|
-
const { sceneDef, memoryLimit, children } = props;
|
|
82
|
+
const { sceneDef, memoryLimit, lighting, children } = props;
|
|
82
83
|
const { viewbox, staticTransform, rootLayer } = sceneDef;
|
|
83
84
|
const { width, height, dpr } = useCanvasSize();
|
|
84
85
|
const invalidate = useThree((state) => state.invalidate);
|
|
@@ -87,6 +88,17 @@ function SceneGraph(props) {
|
|
|
87
88
|
// One budget for the scene's lifetime; the limit is synced rather than the budget rebuilt, so an
|
|
88
89
|
// atlas's registration survives a `memoryLimit` change.
|
|
89
90
|
const [budget] = useState(() => new TextureBudget(memoryLimit));
|
|
91
|
+
// One object for the scene's lifetime, published above both halves of the tree: the passes write
|
|
92
|
+
// into it and the lit materials inside the world group read it, and neither may import the other.
|
|
93
|
+
// The plan matrix uniform holds the space's own `matrixInverse`. The space recomputes that matrix
|
|
94
|
+
// in place on every refit and never replaces it, so a lit material reads the current fit with no
|
|
95
|
+
// copy, from its very first commit. Never write through `planFromWorld.value`, because the write
|
|
96
|
+
// lands in the space itself.
|
|
97
|
+
const [effects] = useState(() => {
|
|
98
|
+
const effects = createSceneEffects();
|
|
99
|
+
effects.planFromWorld.value = space.matrixInverse;
|
|
100
|
+
return effects;
|
|
101
|
+
});
|
|
90
102
|
const worldRef = useRef(null);
|
|
91
103
|
useLayoutEffect(() => {
|
|
92
104
|
space.refit({ viewbox, width, height, dpr, staticTransform });
|
|
@@ -108,5 +120,5 @@ function SceneGraph(props) {
|
|
|
108
120
|
useEffect(() => {
|
|
109
121
|
printScene(sceneDef, logger.debug);
|
|
110
122
|
}, [sceneDef]);
|
|
111
|
-
return (_jsxs(LayoutDriver, { children: [_jsxs("group", { name: "world", ref: worldRef, matrixAutoUpdate: false, matrix: space.matrix, children: [_jsx(
|
|
123
|
+
return (_jsx(SceneEffectsContext, { value: effects, children: _jsxs(LayoutDriver, { children: [_jsxs("group", { name: "world", ref: worldRef, matrixAutoUpdate: false, matrix: space.matrix, children: [_jsx(TextureBudgetContext, { value: budget, children: _jsx(MountSchedulerContext, { value: scheduler, children: _jsx(Layer, { layer: rootLayer, children: sceneDef.background !== undefined && _jsx(Background, { color: sceneDef.background }) }) }) }), _jsx(Lighting, { ...lighting })] }), _jsx(OcclusionPasses, {}), children] }) }));
|
|
112
124
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expofp/renderer",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.35.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/expofp/efp-app.git",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"troika-three-text": "^0.52.5",
|
|
43
43
|
"tslib": "^2.3.0",
|
|
44
44
|
"webgl-memory": "^1.1.3",
|
|
45
|
-
"@expofp/geometry": "3.
|
|
45
|
+
"@expofp/geometry": "3.35.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@react-three/test-renderer": "^9.1.0",
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scene lighting: a hemisphere fill from above the plan plus a directional key that follows the
|
|
3
|
-
* camera, so a surface square-on to the viewer is shaded by exactly its own color (see
|
|
4
|
-
* {@link HALF_ALBEDO}).
|
|
5
|
-
*
|
|
6
|
-
* Only lit materials see any of this — solid shape batches and models. Every 2D primitive draws
|
|
7
|
-
* through a `MeshBasicMaterial`, so lines, text and images are unaffected by a light no matter what
|
|
8
|
-
* the scene holds. That is deliberate: a route line or a booth label has no meaningful surface
|
|
9
|
-
* normal, and shading one would make its color depend on where the camera is.
|
|
10
|
-
*
|
|
11
|
-
* **These sit inside the scene's world matrix**, so every direction below is expressed in the plan's
|
|
12
|
-
* own space and taken there through {@link Space.matrixInverse} — one inverse, shared, rather than
|
|
13
|
-
* one per light. Three reads a light's direction from where it ends up in the *world*, so the two
|
|
14
|
-
* transforms cancel and the light points where it was aimed under any fit. The key light needs a
|
|
15
|
-
* target inside the same space for that to hold: its direction is the difference of two world
|
|
16
|
-
* positions, and only a target under the same matrix cancels the fit's translation.
|
|
17
|
-
* @returns The fill and key lights, and the key's target.
|
|
18
|
-
*/
|
|
19
|
-
export declare function Lighting(): import("react").JSX.Element;
|
|
20
|
-
//# sourceMappingURL=Lighting.d.ts.map
|
package/dist/objects/Lighting.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useFrame } from '@react-three/fiber';
|
|
3
|
-
import { useLayoutEffect, useRef } from 'react';
|
|
4
|
-
import { Color } from 'three';
|
|
5
|
-
import { useSpace } from '../space/Space';
|
|
6
|
-
/**
|
|
7
|
-
* Scene lighting: a hemisphere fill from above the plan plus a directional key that follows the
|
|
8
|
-
* camera, so a surface square-on to the viewer is shaded by exactly its own color (see
|
|
9
|
-
* {@link HALF_ALBEDO}).
|
|
10
|
-
*
|
|
11
|
-
* Only lit materials see any of this — solid shape batches and models. Every 2D primitive draws
|
|
12
|
-
* through a `MeshBasicMaterial`, so lines, text and images are unaffected by a light no matter what
|
|
13
|
-
* the scene holds. That is deliberate: a route line or a booth label has no meaningful surface
|
|
14
|
-
* normal, and shading one would make its color depend on where the camera is.
|
|
15
|
-
*
|
|
16
|
-
* **These sit inside the scene's world matrix**, so every direction below is expressed in the plan's
|
|
17
|
-
* own space and taken there through {@link Space.matrixInverse} — one inverse, shared, rather than
|
|
18
|
-
* one per light. Three reads a light's direction from where it ends up in the *world*, so the two
|
|
19
|
-
* transforms cancel and the light points where it was aimed under any fit. The key light needs a
|
|
20
|
-
* target inside the same space for that to hold: its direction is the difference of two world
|
|
21
|
-
* positions, and only a target under the same matrix cancels the fit's translation.
|
|
22
|
-
* @returns The fill and key lights, and the key's target.
|
|
23
|
-
*/
|
|
24
|
-
export function Lighting() {
|
|
25
|
-
const fillRef = useRef(null);
|
|
26
|
-
const keyRef = useRef(null);
|
|
27
|
-
const targetRef = useRef(null);
|
|
28
|
-
const space = useSpace();
|
|
29
|
-
useLayoutEffect(() => {
|
|
30
|
-
keyRef.current.target = targetRef.current;
|
|
31
|
-
}, []);
|
|
32
|
-
// Taken from the camera's own backward axis rather than from the controls' target: only the
|
|
33
|
-
// direction matters for a directional light — the target below pins the other end — so this needs
|
|
34
|
-
// no access to `controls`, and unlike normalizing the camera's *position* it stays correct when
|
|
35
|
-
// the plan's coordinates are far from the origin, which SVG viewboxes routinely are.
|
|
36
|
-
//
|
|
37
|
-
// Column 2 of `matrixWorld` is that backward axis: where the light belongs, shining from the
|
|
38
|
-
// camera toward what it looks at. Read with `setFromMatrixColumn` rather than through
|
|
39
|
-
// `Camera.getWorldDirection`, which negates the axis and, worse, re-runs `updateWorldMatrix` —
|
|
40
|
-
// leaving the camera's `matrixWorldNeedsUpdate` set on every single call, which kept the scene
|
|
41
|
-
// perpetually dirty and no screenshot ever settled.
|
|
42
|
-
//
|
|
43
|
-
// Under the on-demand loop this runs only on frames something already asked for, and camera
|
|
44
|
-
// movement is what asks for them, so a still plan costs nothing.
|
|
45
|
-
useFrame((state) => {
|
|
46
|
-
keyRef.current.position
|
|
47
|
-
.setFromMatrixColumn(state.camera.matrixWorld, 2)
|
|
48
|
-
.transformDirection(space.matrixInverse);
|
|
49
|
-
// The plan's own up axis, as a point whose world position lands back on it — which is what three
|
|
50
|
-
// reads a hemisphere light's sky axis from. It is world -z either way, the side the camera is
|
|
51
|
-
// on: every path into world space ends with handedness flip.
|
|
52
|
-
//
|
|
53
|
-
// Computed here rather than memoized against the space because the space is refitted in place
|
|
54
|
-
// and so never changes identity — and it is four vector operations on a frame that was going to
|
|
55
|
-
// aim the key light anyway.
|
|
56
|
-
fillRef.current.position
|
|
57
|
-
.setFromMatrixColumn(space.matrix, 2)
|
|
58
|
-
.normalize()
|
|
59
|
-
.applyMatrix4(space.matrixInverse);
|
|
60
|
-
});
|
|
61
|
-
return (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { ref: fillRef, color: LIGHT_COLOR, groundColor: GROUND_COLOR, intensity: HALF_ALBEDO }), _jsx("directionalLight", { ref: keyRef, color: LIGHT_COLOR, intensity: HALF_ALBEDO }), _jsx("object3D", { ref: targetRef })] }));
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Each light's intensity, so the two together sum to `π`.
|
|
65
|
-
*
|
|
66
|
-
* three's diffuse BRDF is `albedo / π`, so a light of intensity `I` reaching a surface head-on
|
|
67
|
-
* contributes `I · albedo / π`. At `0.5π` each contributes half the albedo, and the pair sums to
|
|
68
|
-
* exactly the albedo — a surface facing the camera therefore renders at precisely the color the plan
|
|
69
|
-
* author picked, and only faces turning away darken. The key light tracks the camera and the fill's
|
|
70
|
-
* sky axis points at it too, so "facing the camera" is the same set of surfaces for both.
|
|
71
|
-
*
|
|
72
|
-
* Two things elsewhere are load-bearing for that identity rather than cosmetic: the lit material sets
|
|
73
|
-
* `ior = 1` and `specularIntensity = 0`, which drive `F0` to zero so no specular lobe pushes a facing
|
|
74
|
-
* surface *above* its albedo, and `<Canvas flat>` keeps tone mapping off, which would remap the
|
|
75
|
-
* result. Change either and the identity quietly stops holding.
|
|
76
|
-
*/
|
|
77
|
-
const HALF_ALBEDO = 0.5 * Math.PI;
|
|
78
|
-
/**
|
|
79
|
-
* Both lights' color — one shared constant, because the {@link HALF_ALBEDO} identity is a property
|
|
80
|
-
* of the pair: two halves sum back to the author's color only when they are halves of the *same*
|
|
81
|
-
* color. Each light copies it on mount, so writing to a light's own color never reaches the other.
|
|
82
|
-
*/
|
|
83
|
-
const LIGHT_COLOR = new Color('white');
|
|
84
|
-
/** The fill's ground half: pure black, so the side facing away from the sky axis adds nothing. */
|
|
85
|
-
const GROUND_COLOR = new Color('black');
|