@pascal-app/viewer 1.0.7 → 1.1.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 +54 -21
- package/dist/components/viewer/glb-replace-instances.d.ts +6 -3
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
- package/dist/components/viewer/glb-replace-instances.js +5 -4
- package/dist/components/viewer/glb-scene.d.ts +3 -1
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +2 -1
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +8 -4
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +2 -2
- package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -1
- package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
- package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -1
- package/dist/components/viewer/use-daylight-rooms.js +75 -33
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/lib/asset-url.d.ts +0 -1
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +3 -3
- package/dist/lib/daylight/room-context.d.ts.map +1 -1
- package/dist/lib/daylight/room-context.js +3 -2
- package/dist/lib/daylight/room-map.d.ts +124 -0
- package/dist/lib/daylight/room-map.d.ts.map +1 -0
- package/dist/lib/daylight/room-map.js +630 -0
- package/dist/lib/daylight/room-scene.d.ts +4 -2
- package/dist/lib/daylight/room-scene.d.ts.map +1 -1
- package/dist/lib/daylight/room-scene.js +72 -37
- package/dist/lib/daylight/rooms.d.ts +60 -30
- package/dist/lib/daylight/rooms.d.ts.map +1 -1
- package/dist/lib/daylight/rooms.js +331 -286
- package/dist/lib/ktx2-loader.d.ts.map +1 -1
- package/dist/lib/ktx2-loader.js +2 -1
- package/dist/lib/measurement-label.d.ts +4 -0
- package/dist/lib/measurement-label.d.ts.map +1 -0
- package/dist/lib/measurement-label.js +54 -0
- package/dist/lib/measurement-presentation.d.ts +5 -0
- package/dist/lib/measurement-presentation.d.ts.map +1 -0
- package/dist/lib/measurement-presentation.js +8 -0
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +3 -3
- package/dist/lib/texture-reference.d.ts.map +1 -1
- package/dist/lib/texture-reference.js +4 -3
- package/dist/lib/viewer-config.d.ts +21 -0
- package/dist/lib/viewer-config.d.ts.map +1 -0
- package/dist/lib/viewer-config.js +21 -0
- package/dist/lib/viewer-preference-storage.d.ts +7 -0
- package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
- package/dist/lib/viewer-preference-storage.js +23 -0
- package/dist/store/use-viewer.d.ts +2 -56
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +3 -1
- package/dist/systems/elevator/elevator-interaction-system.d.ts +16 -2
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-interaction-system.js +41 -8
- package/dist/systems/geometry/geometry-status.d.ts +8 -0
- package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-status.js +24 -0
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +107 -65
- package/dist/systems/wall/wall-system.d.ts +11 -0
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +56 -10
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Box3, Color, DataTexture, FloatType, Frustum, MathUtils, Matrix4, Mesh, NearestFilter, Raycaster,
|
|
2
|
-
import { abs, clamp, dot, Fn, float, If, int, ivec2, Loop, max, normalize, normalWorld, positionWorld, select,
|
|
3
|
-
|
|
1
|
+
import { Box3, Color, DataTexture, FloatType, Frustum, MathUtils, Matrix4, Mesh, NearestFilter, Raycaster, RGBAFormat, SRGBColorSpace, Vector2, Vector3, } from 'three';
|
|
2
|
+
import { abs, clamp, dot, Fn, float, If, int, ivec2, Loop, max, normalize, normalWorld, positionWorld, select, smoothstep, texture, uniform, vec3, vec4, } from 'three/tsl';
|
|
3
|
+
import { CELL, RoomMap, TILE } from './room-map';
|
|
4
4
|
/** Texels per portal: [center, halfW] [normal, halfH] [radiance, neighbour] [floorY, mainLamp, gain, kind]. */
|
|
5
5
|
const PSTRIDE = 4;
|
|
6
6
|
/** Target average lamp irradiance of a room at night (same units as the ~1.5 sky irradiance of day). */
|
|
@@ -8,7 +8,6 @@ const LAMP_E_TARGET = 1.1;
|
|
|
8
8
|
/** Lamps have a size (shade/diffuser): never evaluate the inverse square closer than ~0.8 m. */
|
|
9
9
|
const LAMP_MIN_D2 = 0.64;
|
|
10
10
|
const DATA_W = 256;
|
|
11
|
-
const MAX_LEVELS = 8;
|
|
12
11
|
/** Texels per exterior lamp: [pos, range] [rgb, cosInner] [spot dir, cosOuter]. */
|
|
13
12
|
const EXT_STRIDE = 3;
|
|
14
13
|
const _viewProj = new Matrix4();
|
|
@@ -21,13 +20,10 @@ export class RoomLighting {
|
|
|
21
20
|
exteriorLamps = [];
|
|
22
21
|
portals = [];
|
|
23
22
|
openings = [];
|
|
24
|
-
|
|
23
|
+
levels = [];
|
|
24
|
+
/** Which room a point is in (tiled per level; see `room-map.ts`). */
|
|
25
|
+
map = new RoomMap();
|
|
25
26
|
data = new Float32Array(4);
|
|
26
|
-
map = new Float32Array(1);
|
|
27
|
-
origin = new Vector2();
|
|
28
|
-
gridW = 1;
|
|
29
|
-
gridH = 1;
|
|
30
|
-
levels = 1;
|
|
31
27
|
lampTexel = new Map();
|
|
32
28
|
lastSky = null;
|
|
33
29
|
// ---- runtime controls (the enhanced viewer's daylight preset) -------------------------------
|
|
@@ -49,22 +45,10 @@ export class RoomLighting {
|
|
|
49
45
|
skyPatch = uniform(3);
|
|
50
46
|
/** Light through interior openings + threshold crossfade (off on the Low tier). */
|
|
51
47
|
softRooms = uniform(1);
|
|
48
|
+
/** Camera world Y (live). */
|
|
52
49
|
cameraY = uniform(0);
|
|
53
|
-
focusedLevel = uniform(-1, 'int');
|
|
54
50
|
// ---- shader inputs that change with the scene ----------------------------------------------
|
|
55
|
-
/**
|
|
56
|
-
* The level arrays live in one shared group, uploaded when a level moves: an array uniform in
|
|
57
|
-
* the default (per-object) group is re-sent for every object in every pass, whether it changed
|
|
58
|
-
* or not — some 760 uploads a frame in a furnished flat.
|
|
59
|
-
*/
|
|
60
|
-
levelGroup = sharedUniformGroup('daylightLevels');
|
|
61
51
|
u = {
|
|
62
|
-
levelYs: uniformArray(new Array(MAX_LEVELS).fill(1e9), 'float').setGroup(this.levelGroup),
|
|
63
|
-
levelOffsets: uniformArray(new Array(MAX_LEVELS).fill(0), 'float').setGroup(this.levelGroup),
|
|
64
|
-
levelCount: uniform(1, 'int'),
|
|
65
|
-
origin: uniform(new Vector2()),
|
|
66
|
-
gridW: uniform(1, 'int'),
|
|
67
|
-
gridH: uniform(1, 'int'),
|
|
68
52
|
portalCount: uniform(0, 'int'),
|
|
69
53
|
portalBase: uniform(0, 'int'),
|
|
70
54
|
extLampStart: uniform(0, 'int'),
|
|
@@ -73,8 +57,7 @@ export class RoomLighting {
|
|
|
73
57
|
activeBase: uniform(0, 'int'),
|
|
74
58
|
spillCount: uniform(0, 'int'),
|
|
75
59
|
extActiveCount: uniform(0, 'int'),
|
|
76
|
-
|
|
77
|
-
data: texture(emptyTexture(RGBAFormat)),
|
|
60
|
+
data: texture(emptyTexture()),
|
|
78
61
|
};
|
|
79
62
|
/** Rebuild from a scene: rooms, lamps, portals, openings, room-id map. */
|
|
80
63
|
build(input) {
|
|
@@ -83,24 +66,11 @@ export class RoomLighting {
|
|
|
83
66
|
this.portals = [];
|
|
84
67
|
this.openings = [];
|
|
85
68
|
this.lampTexel.clear();
|
|
86
|
-
|
|
87
|
-
if (b.isEmpty()) {
|
|
88
|
-
this.levelY = [0];
|
|
89
|
-
this.gridW = this.gridH = this.levels = 1;
|
|
90
|
-
this.map = new Float32Array(1);
|
|
91
|
-
this.uploadMap();
|
|
92
|
-
this.allocateData();
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
this.origin.set(b.min.x - 1, b.min.z - 1);
|
|
96
|
-
this.gridW = Math.max(1, Math.ceil((b.max.x - b.min.x + 2) / CELL));
|
|
97
|
-
this.gridH = Math.max(1, Math.ceil((b.max.z - b.min.z + 2) / CELL));
|
|
98
|
-
this.levelY = input.levelY.length ? input.levelY.slice(0, MAX_LEVELS) : [0];
|
|
99
|
-
this.levels = this.levelY.length;
|
|
69
|
+
this.levels = input.levels.length ? input.levels : [{ y: 0, building: 0 }];
|
|
100
70
|
// ---- rooms ---------------------------------------------------------------------------
|
|
101
71
|
const raycaster = new Raycaster();
|
|
102
72
|
input.configureRaycaster?.(raycaster);
|
|
103
|
-
const
|
|
73
|
+
const coverGrid = new MeshGrid(input.covers.map((c) => c.object));
|
|
104
74
|
const coverKind = new Map();
|
|
105
75
|
for (const c of input.covers)
|
|
106
76
|
c.object.traverse((o) => coverKind.set(o, c.kind));
|
|
@@ -112,16 +82,29 @@ export class RoomLighting {
|
|
|
112
82
|
}
|
|
113
83
|
return '';
|
|
114
84
|
};
|
|
85
|
+
// Level order inside each building (for the ceiling fallback) before the map is built.
|
|
86
|
+
const above = this.levels.map((level, i) => {
|
|
87
|
+
let next = -1;
|
|
88
|
+
this.levels.forEach((other, j) => {
|
|
89
|
+
if (j === i || other.building !== level.building || other.y <= level.y)
|
|
90
|
+
return;
|
|
91
|
+
if (next < 0 || other.y < this.levels[next].y)
|
|
92
|
+
next = j;
|
|
93
|
+
});
|
|
94
|
+
return next;
|
|
95
|
+
});
|
|
115
96
|
for (const zone of input.zones) {
|
|
116
|
-
const levelIndex = MathUtils.clamp(zone.levelIndex, 0, this.levels - 1);
|
|
117
|
-
const floorY = this.
|
|
118
|
-
const nextY = this.
|
|
119
|
-
const
|
|
120
|
-
const
|
|
97
|
+
const levelIndex = MathUtils.clamp(zone.levelIndex, 0, this.levels.length - 1);
|
|
98
|
+
const floorY = this.levels[levelIndex].y;
|
|
99
|
+
const nextY = above[levelIndex] >= 0 ? this.levels[above[levelIndex]].y : undefined;
|
|
100
|
+
const holes = zone.holes ?? [];
|
|
101
|
+
const area = Math.max(0, Math.abs(polygonArea(zone.polygon)) -
|
|
102
|
+
holes.reduce((sum, hole) => sum + Math.abs(polygonArea(hole)), 0));
|
|
103
|
+
const c = interiorPoint(zone.polygon, holes);
|
|
121
104
|
// Enclosed = something overhead (ceiling / roof / slab above). Open terraces get sky light.
|
|
122
105
|
raycaster.set(new Vector3(c.x, floorY + 1.2, c.y), new Vector3(0, 1, 0));
|
|
123
106
|
raycaster.far = 30;
|
|
124
|
-
const hits = intersectGeometry(raycaster,
|
|
107
|
+
const hits = intersectGeometry(raycaster, coverGrid.near(c.x, c.y, c.x, c.y), false);
|
|
125
108
|
const hit = hits[0];
|
|
126
109
|
const top = hits[hits.length - 1];
|
|
127
110
|
const topKind = top ? kindOf(top.object) : '';
|
|
@@ -142,52 +125,14 @@ export class RoomLighting {
|
|
|
142
125
|
portals: [],
|
|
143
126
|
});
|
|
144
127
|
}
|
|
145
|
-
// ---- room
|
|
146
|
-
const
|
|
147
|
-
this.rooms.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
for (const [x, z] of poly) {
|
|
154
|
-
minX = Math.min(minX, x);
|
|
155
|
-
maxX = Math.max(maxX, x);
|
|
156
|
-
minZ = Math.min(minZ, z);
|
|
157
|
-
maxZ = Math.max(maxZ, z);
|
|
158
|
-
}
|
|
159
|
-
const i0 = Math.max(0, Math.floor((minX - this.origin.x) / CELL));
|
|
160
|
-
const i1 = Math.min(this.gridW - 1, Math.ceil((maxX - this.origin.x) / CELL));
|
|
161
|
-
const j0 = Math.max(0, Math.floor((minZ - this.origin.y) / CELL));
|
|
162
|
-
const j1 = Math.min(this.gridH - 1, Math.ceil((maxZ - this.origin.y) / CELL));
|
|
163
|
-
const rowBase = room.levelIndex * this.gridH;
|
|
164
|
-
for (let j = j0; j <= j1; j++) {
|
|
165
|
-
const z = this.origin.y + (j + 0.5) * CELL;
|
|
166
|
-
for (let i = i0; i <= i1; i++) {
|
|
167
|
-
const x = this.origin.x + (i + 0.5) * CELL;
|
|
168
|
-
if (pointInPolygon(x, z, poly))
|
|
169
|
-
map[(rowBase + j) * this.gridW + i] = ri + 1;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
// Dilate by 2 cells into empty cells so wall-thickness gaps and polygon slop still resolve.
|
|
174
|
-
for (let pass = 0; pass < 2; pass++) {
|
|
175
|
-
const src = map.slice();
|
|
176
|
-
for (let l = 0; l < this.levels; l++)
|
|
177
|
-
for (let j = 1; j < this.gridH - 1; j++)
|
|
178
|
-
for (let i = 1; i < this.gridW - 1; i++) {
|
|
179
|
-
const k = (l * this.gridH + j) * this.gridW + i;
|
|
180
|
-
if (src[k])
|
|
181
|
-
continue;
|
|
182
|
-
const n = src[k - 1] || src[k + 1] || src[k - this.gridW] || src[k + this.gridW];
|
|
183
|
-
if (n)
|
|
184
|
-
map[k] = n;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
this.map = map;
|
|
188
|
-
this.uploadMap();
|
|
189
|
-
// ---- openings between rooms (soft room map) -------------------------------------------
|
|
190
|
-
this.openings = this.detectOpenings(input, raycaster);
|
|
128
|
+
// ---- room map + openings between rooms (walked on each level's grid) ---------------------
|
|
129
|
+
const openings = this.openingSearch(input, raycaster);
|
|
130
|
+
this.map.build(this.levels, this.rooms.map((room) => ({
|
|
131
|
+
level: room.levelIndex,
|
|
132
|
+
polygon: room.zone.polygon,
|
|
133
|
+
holes: room.zone.holes,
|
|
134
|
+
})), openings.walk);
|
|
135
|
+
this.openings = openings.finish();
|
|
191
136
|
// ---- lamps ---------------------------------------------------------------------------
|
|
192
137
|
for (const lamp of input.lamps) {
|
|
193
138
|
const ri = this.roomAt(lamp.position.clone().add(new Vector3(0, -0.3, 0)));
|
|
@@ -318,18 +263,9 @@ export class RoomLighting {
|
|
|
318
263
|
if (this.lastSky)
|
|
319
264
|
this.updateSky(...this.lastSky);
|
|
320
265
|
}
|
|
321
|
-
/** CPU-side room lookup (same rule as the shader, without the normal offset). */
|
|
266
|
+
/** CPU-side room lookup in assembled space (same rule as the shader, without the normal offset). */
|
|
322
267
|
roomAt(p) {
|
|
323
|
-
|
|
324
|
-
for (let i = 0; i < this.levelY.length; i++)
|
|
325
|
-
if (this.levelY[i] <= p.y - 0.12)
|
|
326
|
-
level = i;
|
|
327
|
-
const i = Math.floor((p.x - this.origin.x) / CELL);
|
|
328
|
-
const j = Math.floor((p.z - this.origin.y) / CELL);
|
|
329
|
-
if (i < 0 || j < 0 || i >= this.gridW || j >= this.gridH)
|
|
330
|
-
return -1;
|
|
331
|
-
const v = this.map[(level * this.gridH + j) * this.gridW + i];
|
|
332
|
-
return v ? v - 1 : -1;
|
|
268
|
+
return this.map.roomAt(p.x, p.y, p.z);
|
|
333
269
|
}
|
|
334
270
|
/**
|
|
335
271
|
* Whether each level is on screen. A camera over a level whose upper floors are hidden stands
|
|
@@ -337,6 +273,10 @@ export class RoomLighting {
|
|
|
337
273
|
* such, not as a lamplit room it cannot see.
|
|
338
274
|
*/
|
|
339
275
|
levelShown = [];
|
|
276
|
+
/** Per level: focused (its rooms open from above when the camera is over their cover). */
|
|
277
|
+
levelFocused = [];
|
|
278
|
+
/** Per level: the highest one shown in its building (its rooms seen from above). */
|
|
279
|
+
levelTopShown = [];
|
|
340
280
|
/**
|
|
341
281
|
* What the lit rooms in view add to an outside camera's meter: a blend of the exterior reading
|
|
342
282
|
* and the rooms' own irradiance (their indoor meter), weighted by the share of the frame each
|
|
@@ -360,10 +300,6 @@ export class RoomLighting {
|
|
|
360
300
|
const t = Math.tan(MathUtils.degToRad(c.fov) / 2);
|
|
361
301
|
return 4 * t * t * c.aspect;
|
|
362
302
|
})();
|
|
363
|
-
let topShown = -1;
|
|
364
|
-
for (let i = 0; i < this.levelY.length; i++)
|
|
365
|
-
if (this.levelShown[i] !== false)
|
|
366
|
-
topShown = i;
|
|
367
303
|
let weight = 0;
|
|
368
304
|
let weighted = 0;
|
|
369
305
|
_roomsLight.setRGB(0, 0, 0);
|
|
@@ -375,7 +311,7 @@ export class RoomLighting {
|
|
|
375
311
|
_toCamera.subVectors(camera.position, r.centroid);
|
|
376
312
|
const facing = Math.max(0.2, _toCamera.y / Math.max(_toCamera.length(), 1e-4));
|
|
377
313
|
const reach = ortho ? 1 : Math.max(_toCamera.lengthSq(), 1);
|
|
378
|
-
const open = r.levelIndex
|
|
314
|
+
const open = this.levelTopShown[r.levelIndex] !== false ? 1 : 0.15;
|
|
379
315
|
const w = (r.area * facing * open) / reach / view;
|
|
380
316
|
const E = this.meter(index, skyE);
|
|
381
317
|
weight += w;
|
|
@@ -407,33 +343,43 @@ export class RoomLighting {
|
|
|
407
343
|
return { sceneE, roomsE, illuminant };
|
|
408
344
|
}
|
|
409
345
|
isEnclosedAt(p) {
|
|
410
|
-
let level = 0;
|
|
411
|
-
for (let i = 0; i < this.levelY.length; i++)
|
|
412
|
-
if (this.levelY[i] <= p.y - 0.12)
|
|
413
|
-
level = i;
|
|
414
|
-
if (this.levelShown[level] === false)
|
|
415
|
-
return { room: -1, inside: false };
|
|
416
346
|
const room = this.roomAt(p);
|
|
417
347
|
const r = room >= 0 ? this.rooms[room] : undefined;
|
|
418
|
-
|
|
348
|
+
if (!r || this.levelShown[r.levelIndex] === false)
|
|
349
|
+
return { room: -1, inside: false };
|
|
350
|
+
return { room, inside: r.enclosed && p.y < r.ceilingY };
|
|
419
351
|
}
|
|
420
|
-
/**
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
if (
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
this.
|
|
352
|
+
/**
|
|
353
|
+
* Live level presentation, per level: explode offset (live world Y − assembled), whether it is
|
|
354
|
+
* shown, and whether it is the selected level of a cutaway (solo / levels above hidden). A shown
|
|
355
|
+
* level is focused — its rooms open from above — when selected so, or when every level over it
|
|
356
|
+
* in its building is hidden.
|
|
357
|
+
*/
|
|
358
|
+
setLevelPresentation(offsets, shown, selected) {
|
|
359
|
+
this.levelShown = shown;
|
|
360
|
+
const focused = this.levels.map((_, i) => {
|
|
361
|
+
if (shown[i] === false)
|
|
362
|
+
return false;
|
|
363
|
+
if (selected[i])
|
|
364
|
+
return true;
|
|
365
|
+
let up = this.map.levelAbove(i);
|
|
366
|
+
if (up < 0)
|
|
367
|
+
return false;
|
|
368
|
+
for (; up >= 0; up = this.map.levelAbove(up))
|
|
369
|
+
if (shown[up] !== false)
|
|
370
|
+
return false;
|
|
371
|
+
return true;
|
|
372
|
+
});
|
|
373
|
+
this.levelFocused = focused;
|
|
374
|
+
this.levelTopShown = this.levels.map((_, i) => {
|
|
375
|
+
if (shown[i] === false)
|
|
376
|
+
return false;
|
|
377
|
+
for (let up = this.map.levelAbove(i); up >= 0; up = this.map.levelAbove(up))
|
|
378
|
+
if (shown[up] !== false)
|
|
379
|
+
return false;
|
|
380
|
+
return true;
|
|
381
|
+
});
|
|
382
|
+
this.map.setPresentation(offsets, shown, focused);
|
|
437
383
|
}
|
|
438
384
|
/** Re-run the light transfer (after a quality-tier change of the soft room map). */
|
|
439
385
|
refresh() {
|
|
@@ -608,7 +554,7 @@ export class RoomLighting {
|
|
|
608
554
|
this.rooms.forEach((r, i) => {
|
|
609
555
|
this.set(i * 3, r.enclosed ? 1 : 0, lampBase + lamp * 2, r.lamps.length, 0);
|
|
610
556
|
this.set(i * 3 + 1, r.portals.length, 0, 0, 0);
|
|
611
|
-
this.set(i * 3 + 2, r.coverY, r.coverIsRoof ? 1 : 0, r.levelIndex, 0);
|
|
557
|
+
this.set(i * 3 + 2, r.coverY, r.coverIsRoof ? 1 : 0, this.map.gpuLevel(r.levelIndex), 0);
|
|
612
558
|
for (const l of r.lamps) {
|
|
613
559
|
this.lampTexel.set(l, lampBase + lamp * 2);
|
|
614
560
|
this.set(lampBase + lamp * 2, l.position.x, l.position.y, l.position.z, l.range);
|
|
@@ -644,7 +590,7 @@ export class RoomLighting {
|
|
|
644
590
|
const room = this.rooms[p.room];
|
|
645
591
|
const main = room.lamps.slice().sort((a, b) => b.intensity - a.intensity)[0];
|
|
646
592
|
this.set(t + 2, 0, 0, 0, p.neighbor);
|
|
647
|
-
this.set(t + 3, this.
|
|
593
|
+
this.set(t + 3, this.levels[room.levelIndex]?.y ?? 0, main ? this.lampTexel.get(main) : -1, p.gain, p.kind);
|
|
648
594
|
});
|
|
649
595
|
const previous = this.u.data.value;
|
|
650
596
|
const tex = new DataTexture(this.data, DATA_W, rows, RGBAFormat, FloatType);
|
|
@@ -686,19 +632,6 @@ export class RoomLighting {
|
|
|
686
632
|
this.u.spillCount.value = spill;
|
|
687
633
|
this.u.extActiveCount.value = lit;
|
|
688
634
|
}
|
|
689
|
-
uploadMap() {
|
|
690
|
-
const previous = this.u.map.value;
|
|
691
|
-
const tex = new DataTexture(this.map, this.gridW, this.gridH * this.levels, RedFormat, FloatType);
|
|
692
|
-
tex.magFilter = tex.minFilter = NearestFilter;
|
|
693
|
-
tex.needsUpdate = true;
|
|
694
|
-
this.u.map.value = tex;
|
|
695
|
-
previous.dispose();
|
|
696
|
-
this.u.origin.value.copy(this.origin);
|
|
697
|
-
this.u.gridW.value = this.gridW;
|
|
698
|
-
this.u.gridH.value = this.gridH;
|
|
699
|
-
this.u.levelCount.value = this.levels;
|
|
700
|
-
this.setLevelOffsets([]);
|
|
701
|
-
}
|
|
702
635
|
markData() {
|
|
703
636
|
;
|
|
704
637
|
this.u.data.value.needsUpdate = true;
|
|
@@ -732,148 +665,183 @@ export class RoomLighting {
|
|
|
732
665
|
this.data.set([a, b, c, d], texel * 4);
|
|
733
666
|
}
|
|
734
667
|
/**
|
|
735
|
-
* Walk every boundary between two room cells (or a room and outside) on
|
|
736
|
-
* short horizontal rays against the walls at 0.35 m and 1.6 m: open at both heights = a
|
|
737
|
-
* opening (doorway, arch, open-plan edge). Windows don't count (sills are above 0.35 m).
|
|
668
|
+
* Walk every boundary between two room cells (or a room and outside) on each level's grid and
|
|
669
|
+
* cast short horizontal rays against the walls at 0.35 m and 1.6 m: open at both heights = a
|
|
670
|
+
* walkable opening (doorway, arch, open-plan edge). Windows don't count (sills are above 0.35 m).
|
|
671
|
+
* `walk` runs per level while its grid exists; `finish` clusters the samples into openings.
|
|
738
672
|
*/
|
|
739
|
-
|
|
673
|
+
openingSearch(input, ray) {
|
|
740
674
|
const doors = input.openings
|
|
741
675
|
.filter((o) => o.kind === 'door')
|
|
742
676
|
.map((o) => ({ id: o.id, box: new Box3().setFromObject(o.object) }));
|
|
743
677
|
const doorHoles = doors.map((door) => door.box.clone().expandByScalar(0.05));
|
|
744
678
|
ray.far = 0.9;
|
|
679
|
+
const wallGrid = new MeshGrid(input.walls);
|
|
745
680
|
const samples = new Map();
|
|
746
|
-
const W = this.gridW;
|
|
747
|
-
const H = this.gridH;
|
|
748
|
-
const map = this.map;
|
|
749
681
|
const v = new Vector3();
|
|
750
682
|
const dirV = new Vector3();
|
|
751
|
-
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
683
|
+
const walk = (tile) => {
|
|
684
|
+
const { cells, stride, i0, j0, x0, z0, ci0, cj0, level: l } = tile;
|
|
685
|
+
const floorY = this.levels[l]?.y ?? 0;
|
|
686
|
+
const visit = (i, j, axis) => {
|
|
687
|
+
const di = axis === 'x' ? 1 : 0;
|
|
688
|
+
const dj = axis === 'z' ? 1 : 0;
|
|
689
|
+
if ((axis === 'x' ? cj0 + j : ci0 + i) % 2)
|
|
690
|
+
return; // every 2nd cell along the edge (0.2 m)
|
|
691
|
+
const A = cells[j * stride + i];
|
|
692
|
+
const B = cells[(j + dj) * stride + i + di];
|
|
693
|
+
if (A === B)
|
|
694
|
+
return;
|
|
695
|
+
const a = A ? A - 1 : -1;
|
|
696
|
+
const b = B ? B - 1 : -1;
|
|
697
|
+
// Outside openings only from enclosed rooms.
|
|
698
|
+
if (a < 0 && !this.rooms[b].enclosed)
|
|
699
|
+
return;
|
|
700
|
+
if (b < 0 && !this.rooms[a].enclosed)
|
|
701
|
+
return;
|
|
702
|
+
const x = x0 + (i + 0.5 + di * 0.5) * CELL;
|
|
703
|
+
const z = z0 + (j + 0.5 + dj * 0.5) * CELL;
|
|
704
|
+
dirV.set(di, 0, dj);
|
|
705
|
+
for (const h of [0.35, 1.6]) {
|
|
706
|
+
v.set(x, floorY + h, z).addScaledVector(dirV, -0.45);
|
|
707
|
+
ray.set(v, dirV);
|
|
708
|
+
// A doorway is open whatever wall-attached geometry crosses it (finishes, trims).
|
|
709
|
+
const near = wallGrid.near(Math.min(v.x, v.x + dirV.x * 0.9), Math.min(v.z, v.z + dirV.z * 0.9), Math.max(v.x, v.x + dirV.x * 0.9), Math.max(v.z, v.z + dirV.z * 0.9));
|
|
710
|
+
const blocked = intersectGeometry(ray, near, false).some((hit) => !doorHoles.some((hole) => hole.containsPoint(hit.point)));
|
|
711
|
+
if (blocked)
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
// Normalise so key/direction are from the lower room id (outside = last).
|
|
715
|
+
const lo = a < 0 ? b : b < 0 ? a : Math.min(a, b);
|
|
716
|
+
const hi = lo === a ? b : a;
|
|
717
|
+
const dir = lo === a ? 1 : -1;
|
|
718
|
+
const key = `${l}|${lo}|${hi}`;
|
|
719
|
+
let s = samples.get(key);
|
|
720
|
+
if (!s) {
|
|
721
|
+
s = { a: lo, b: hi, pts: [], facing: [], level: l };
|
|
722
|
+
samples.set(key, s);
|
|
723
|
+
}
|
|
724
|
+
s.pts.push(new Vector3(x, floorY, z));
|
|
725
|
+
s.facing.push(new Vector3(di * dir, 0, dj * dir));
|
|
726
|
+
};
|
|
727
|
+
// Each edge belongs to the tile of the cell before it. A uniform tile has none inside: only
|
|
728
|
+
// its borders with the next tiles can hold one.
|
|
729
|
+
const iEnd = i0 + TILE;
|
|
730
|
+
const jEnd = j0 + TILE;
|
|
731
|
+
if (tile.uniform) {
|
|
732
|
+
for (let j = j0; j < jEnd; j++)
|
|
733
|
+
visit(iEnd - 1, j, 'x');
|
|
734
|
+
for (let i = i0; i < iEnd; i++)
|
|
735
|
+
visit(i, jEnd - 1, 'z');
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
for (let j = j0; j < jEnd; j++)
|
|
739
|
+
for (let i = i0; i < iEnd; i++) {
|
|
740
|
+
visit(i, j, 'x');
|
|
741
|
+
visit(i, j, 'z');
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
const finish = () => {
|
|
745
|
+
const openings = [];
|
|
746
|
+
// Boundary samples a step apart along an edge belong to one opening. Off the grid axes (a
|
|
747
|
+
// rotated building) the edge is a staircase of cells, so adjacency is by distance.
|
|
748
|
+
const gap = Math.max(0.45, CELL * 3);
|
|
749
|
+
for (const s of samples.values()) {
|
|
750
|
+
const order = s.pts.map((_, k) => k).sort((p, q) => s.pts[p].x - s.pts[q].x);
|
|
751
|
+
const seen = new Uint8Array(order.length);
|
|
752
|
+
for (let seed = 0; seed < order.length; seed++) {
|
|
753
|
+
if (seen[seed])
|
|
754
|
+
continue;
|
|
755
|
+
seen[seed] = 1;
|
|
756
|
+
const members = [];
|
|
757
|
+
const queue = [seed];
|
|
758
|
+
while (queue.length) {
|
|
759
|
+
const k = queue.pop();
|
|
760
|
+
const p = s.pts[order[k]];
|
|
761
|
+
members.push(order[k]);
|
|
762
|
+
for (const step of [-1, 1]) {
|
|
763
|
+
for (let m = k + step; m >= 0 && m < order.length; m += step) {
|
|
764
|
+
const q = s.pts[order[m]];
|
|
765
|
+
if (Math.abs(q.x - p.x) > gap)
|
|
766
|
+
break;
|
|
767
|
+
if (seen[m] || Math.hypot(q.x - p.x, q.z - p.z) > gap)
|
|
768
|
+
continue;
|
|
769
|
+
seen[m] = 1;
|
|
770
|
+
queue.push(m);
|
|
785
771
|
}
|
|
786
772
|
}
|
|
787
|
-
if (!open)
|
|
788
|
-
continue;
|
|
789
|
-
// Normalise so key/direction are from the lower room id (outside = last).
|
|
790
|
-
const lo = a < 0 ? b : b < 0 ? a : Math.min(a, b);
|
|
791
|
-
const hi = lo === a ? b : a;
|
|
792
|
-
const dir = lo === a ? 1 : -1;
|
|
793
|
-
const key = `${l}|${lo}|${hi}|${axis}`;
|
|
794
|
-
let s = samples.get(key);
|
|
795
|
-
if (!s) {
|
|
796
|
-
s = { a: lo, b: hi, axis, dir, pts: [], level: l };
|
|
797
|
-
samples.set(key, s);
|
|
798
|
-
}
|
|
799
|
-
s.pts.push(new Vector3(x, floorY, z));
|
|
800
773
|
}
|
|
774
|
+
if (members.length < 3)
|
|
775
|
+
continue; // < 0.5 m
|
|
776
|
+
openings.push(this.openingFromRun(members, s, doors));
|
|
801
777
|
}
|
|
802
778
|
}
|
|
779
|
+
return openings;
|
|
780
|
+
};
|
|
781
|
+
return { walk, finish };
|
|
782
|
+
}
|
|
783
|
+
/** One opening from its boundary samples: measured along their own line, facing room b. */
|
|
784
|
+
openingFromRun(members, s, doors) {
|
|
785
|
+
const run = members.map((k) => s.pts[k]);
|
|
786
|
+
const c = run.reduce((acc, p) => acc.add(p), new Vector3()).multiplyScalar(1 / run.length);
|
|
787
|
+
// Principal direction of the samples (2×2 covariance in XZ).
|
|
788
|
+
let xx = 0;
|
|
789
|
+
let xz = 0;
|
|
790
|
+
let zz = 0;
|
|
791
|
+
for (const p of run) {
|
|
792
|
+
const dx = p.x - c.x;
|
|
793
|
+
const dz = p.z - c.z;
|
|
794
|
+
xx += dx * dx;
|
|
795
|
+
xz += dx * dz;
|
|
796
|
+
zz += dz * dz;
|
|
803
797
|
}
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if (run.length < 3) {
|
|
813
|
-
run = []; // < 0.5 m
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
const c = run.reduce((acc, p) => acc.add(p), new Vector3()).multiplyScalar(1 / run.length);
|
|
817
|
-
const width = along(run[run.length - 1]) - along(run[0]) + 0.2;
|
|
818
|
-
const normal = new Vector3(s.axis === 'x' ? s.dir : 0, 0, s.axis === 'z' ? s.dir : 0);
|
|
819
|
-
const roomA = this.rooms[s.a];
|
|
820
|
-
const roomB = s.b >= 0 ? this.rooms[s.b] : undefined;
|
|
821
|
-
const floorY = this.levelY[s.level] ?? 0;
|
|
822
|
-
// Door in this opening? → its height + state. Otherwise floor to the lower ceiling.
|
|
823
|
-
const door = doors.find((d) => d.box
|
|
824
|
-
.clone()
|
|
825
|
-
.expandByScalar(0.25)
|
|
826
|
-
.containsPoint(c.clone().setY(floorY + 1)));
|
|
827
|
-
const ceil = Math.min(roomA.ceilingY, roomB?.ceilingY ?? roomA.ceilingY);
|
|
828
|
-
const height = door
|
|
829
|
-
? Math.min(door.box.max.y - floorY, ceil - floorY)
|
|
830
|
-
: Math.max(1.9, ceil - floorY);
|
|
831
|
-
openings.push({
|
|
832
|
-
a: s.a,
|
|
833
|
-
b: s.b,
|
|
834
|
-
center: c.setY(floorY + height / 2),
|
|
835
|
-
normal,
|
|
836
|
-
width: door ? Math.min(width, door.box.getSize(new Vector3()).length()) : width,
|
|
837
|
-
height,
|
|
838
|
-
doorId: door?.id,
|
|
839
|
-
});
|
|
840
|
-
run = [];
|
|
841
|
-
};
|
|
842
|
-
for (const p of s.pts) {
|
|
843
|
-
const last = run[run.length - 1];
|
|
844
|
-
if (last && (Math.abs(across(p) - across(last)) > 0.11 || along(p) - along(last) > 0.45))
|
|
845
|
-
flush();
|
|
846
|
-
run.push(p);
|
|
847
|
-
}
|
|
848
|
-
flush();
|
|
798
|
+
const angle = 0.5 * Math.atan2(2 * xz, xx - zz);
|
|
799
|
+
const tangent = new Vector3(Math.cos(angle), 0, Math.sin(angle));
|
|
800
|
+
let lo = Number.POSITIVE_INFINITY;
|
|
801
|
+
let hi = Number.NEGATIVE_INFINITY;
|
|
802
|
+
for (const p of run) {
|
|
803
|
+
const t = (p.x - c.x) * tangent.x + (p.z - c.z) * tangent.z;
|
|
804
|
+
lo = Math.min(lo, t);
|
|
805
|
+
hi = Math.max(hi, t);
|
|
849
806
|
}
|
|
850
|
-
|
|
807
|
+
c.addScaledVector(tangent, (lo + hi) / 2);
|
|
808
|
+
const width = hi - lo + 2 * CELL;
|
|
809
|
+
const facing = members.reduce((acc, k) => acc.add(s.facing[k]), new Vector3());
|
|
810
|
+
const normal = new Vector3(-tangent.z, 0, tangent.x);
|
|
811
|
+
if (normal.dot(facing) < 0)
|
|
812
|
+
normal.negate();
|
|
813
|
+
const roomA = this.rooms[s.a];
|
|
814
|
+
const roomB = s.b >= 0 ? this.rooms[s.b] : undefined;
|
|
815
|
+
const floorY = this.levels[s.level]?.y ?? 0;
|
|
816
|
+
// Door in this opening? → its height + state. Otherwise floor to the lower ceiling.
|
|
817
|
+
const door = doors.find((d) => d.box
|
|
818
|
+
.clone()
|
|
819
|
+
.expandByScalar(0.25)
|
|
820
|
+
.containsPoint(c.clone().setY(floorY + 1)));
|
|
821
|
+
const ceil = Math.min(roomA.ceilingY, roomB?.ceilingY ?? roomA.ceilingY);
|
|
822
|
+
const height = door
|
|
823
|
+
? Math.min(door.box.max.y - floorY, ceil - floorY)
|
|
824
|
+
: Math.max(1.9, ceil - floorY);
|
|
825
|
+
return {
|
|
826
|
+
a: s.a,
|
|
827
|
+
b: s.b,
|
|
828
|
+
center: c.setY(floorY + height / 2),
|
|
829
|
+
normal,
|
|
830
|
+
width: door ? Math.min(width, door.box.getSize(new Vector3()).length()) : width,
|
|
831
|
+
height,
|
|
832
|
+
doorId: door?.id,
|
|
833
|
+
};
|
|
851
834
|
}
|
|
852
835
|
// ---- TSL ---------------------------------------------------------------------------------
|
|
853
|
-
/**
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
return level;
|
|
836
|
+
/** Room index (−1 outside) at a world-space position (live heights). */
|
|
837
|
+
roomAtNode = Fn(([p]) => this.map.roomAtNode(p));
|
|
838
|
+
/**
|
|
839
|
+
* The room a surface faces (probed 18 cm off it along its normal). One node per material: its
|
|
840
|
+
* lighting and its exterior mask share the lookup.
|
|
841
|
+
*/
|
|
842
|
+
probeRoomNode() {
|
|
843
|
+
return this.roomAtNode(positionWorld.add(normalWorld.mul(0.18)));
|
|
862
844
|
}
|
|
863
|
-
/** Room index (−1 outside) at a world-space position. */
|
|
864
|
-
roomAtNode = Fn(([p]) => {
|
|
865
|
-
const u = this.u;
|
|
866
|
-
const level = this.levelOfNode(p.y);
|
|
867
|
-
const cell = ivec2(p.xz.sub(u.origin).div(CELL).floor());
|
|
868
|
-
const inside = cell.x
|
|
869
|
-
.greaterThanEqual(0)
|
|
870
|
-
.and(cell.y.greaterThanEqual(0))
|
|
871
|
-
.and(cell.x.lessThan(u.gridW))
|
|
872
|
-
.and(cell.y.lessThan(u.gridH));
|
|
873
|
-
const safe = ivec2(clamp(cell.x, int(0), u.gridW.sub(1)), clamp(cell.y, int(0), u.gridH.sub(1)));
|
|
874
|
-
const id = u.map.load(ivec2(safe.x, safe.y.add(level.mul(u.gridH)))).r;
|
|
875
|
-
return select(inside, int(id).sub(1), int(-1));
|
|
876
|
-
});
|
|
877
845
|
/** E' = Emax·(1 − e^(−E/Emax)), on luminance, hue kept — near-field hot spots compress, never blow out. */
|
|
878
846
|
softClampLamps(e) {
|
|
879
847
|
const L = dot(e, vec3(0.2126, 0.7152, 0.0722)).max(1e-5);
|
|
@@ -885,11 +853,12 @@ export class RoomLighting {
|
|
|
885
853
|
* topmost cover and that cover is hidden from there: a ceiling (hidden from above), or anything
|
|
886
854
|
* once its level is focused (the levels over it hidden). From inside, under the cover, the room
|
|
887
855
|
* stays enclosed whatever is focused: it must not take the full sky on top of its windows.
|
|
888
|
-
* `h2` is the room's third header texel: [coverY, roof, level, 0]
|
|
856
|
+
* `h2` is the room's third header texel: [coverY, roof, level, 0]; `level` its level's
|
|
857
|
+
* [th, entryOffset, offset, flags] (flag 1: focused).
|
|
889
858
|
*/
|
|
890
|
-
openToSky(h2) {
|
|
891
|
-
const focused =
|
|
892
|
-
return this.cameraY.greaterThan(h2.x).and(h2.y.lessThan(0.5).or(focused));
|
|
859
|
+
openToSky(h2, level) {
|
|
860
|
+
const focused = level.w.mod(2).greaterThan(0.5);
|
|
861
|
+
return this.cameraY.sub(level.z).greaterThan(h2.x).and(h2.y.lessThan(0.5).or(focused));
|
|
893
862
|
}
|
|
894
863
|
texel(i) {
|
|
895
864
|
const idx = int(i);
|
|
@@ -900,21 +869,20 @@ export class RoomLighting {
|
|
|
900
869
|
* irradiance — lamp + portal + bounce diffuse irradiance (linear)
|
|
901
870
|
* enclosure — multiplier for the sky IBL (1 outdoors, `indoorEnv` in enclosed rooms)
|
|
902
871
|
*/
|
|
903
|
-
lightingNode = Fn(() => {
|
|
904
|
-
const
|
|
905
|
-
const room = this.roomAtNode(probe).toVar();
|
|
906
|
-
// Shade in assembled space so exploded floors stay lit exactly as when stacked.
|
|
907
|
-
const assembled = positionWorld.sub(vec3(0, this.u.levelOffsets.element(this.levelOfNode(probe.y)), 0));
|
|
872
|
+
lightingNode = Fn(([probeRoom]) => {
|
|
873
|
+
const room = int(probeRoom).toVar();
|
|
908
874
|
const irr = vec3(0).toVar();
|
|
909
875
|
const enclosure = float(1).toVar();
|
|
910
876
|
If(room.greaterThanEqual(0), () => {
|
|
911
877
|
const h0 = this.texel(room.mul(3));
|
|
912
878
|
const h1 = this.texel(room.mul(3).add(1));
|
|
913
879
|
const h2 = this.texel(room.mul(3).add(2));
|
|
914
|
-
|
|
880
|
+
const level = this.map.levelTexel1(h2.z);
|
|
881
|
+
If(h0.x.greaterThan(0.5).and(this.openToSky(h2, level).not()), () => {
|
|
915
882
|
enclosure.assign(this.indoorEnv);
|
|
916
883
|
});
|
|
917
|
-
|
|
884
|
+
// Shade in assembled space so exploded floors stay lit exactly as when stacked.
|
|
885
|
+
const P = positionWorld.sub(vec3(0, level.z, 0));
|
|
918
886
|
const Nw = normalWorld;
|
|
919
887
|
// Lamps: smooth windowed inverse-square, wrapped diffuse so the room reads as filled
|
|
920
888
|
// rather than spotlit — stands in for the bounce we don't compute.
|
|
@@ -1114,13 +1082,13 @@ export class RoomLighting {
|
|
|
1114
1082
|
return this.softClampLamps(out).mul(this.lampGain).mul(this.lampView);
|
|
1115
1083
|
});
|
|
1116
1084
|
/** 1 where exterior lights may shine (outside, or rooms open to the sky), 0 inside. */
|
|
1117
|
-
exteriorMaskNode = Fn(() => {
|
|
1118
|
-
const room =
|
|
1085
|
+
exteriorMaskNode = Fn(([probeRoom]) => {
|
|
1086
|
+
const room = int(probeRoom).toVar();
|
|
1119
1087
|
const mask = float(1).toVar();
|
|
1120
1088
|
If(room.greaterThanEqual(0), () => {
|
|
1121
1089
|
const h0 = this.texel(room.mul(3));
|
|
1122
1090
|
const h2 = this.texel(room.mul(3).add(2));
|
|
1123
|
-
If(h0.x.greaterThan(0.5).and(this.openToSky(h2).not()), () => {
|
|
1091
|
+
If(h0.x.greaterThan(0.5).and(this.openToSky(h2, this.map.levelTexel1(h2.z)).not()), () => {
|
|
1124
1092
|
mask.assign(0);
|
|
1125
1093
|
});
|
|
1126
1094
|
});
|
|
@@ -1157,8 +1125,59 @@ function intersectGeometry(ray, objects, recursive) {
|
|
|
1157
1125
|
visit(object);
|
|
1158
1126
|
return hits.sort((a, b) => a.distance - b.distance);
|
|
1159
1127
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1128
|
+
/**
|
|
1129
|
+
* Meshes (and the meshes under them) bucketed by their world XZ bounds, so a short doorway ray or
|
|
1130
|
+
* a vertical cover ray tests its neighbours, not every wall and slab of the scene.
|
|
1131
|
+
*/
|
|
1132
|
+
class MeshGrid {
|
|
1133
|
+
static SIZE = 2;
|
|
1134
|
+
cells = new Map();
|
|
1135
|
+
seen = new Set();
|
|
1136
|
+
constructor(roots) {
|
|
1137
|
+
const box = new Box3();
|
|
1138
|
+
for (const root of roots)
|
|
1139
|
+
root.traverse((object) => {
|
|
1140
|
+
const mesh = object;
|
|
1141
|
+
if (!mesh.isMesh)
|
|
1142
|
+
return;
|
|
1143
|
+
// From the vertices as they are now: a cached bounding box can predate geometry that was
|
|
1144
|
+
// rebuilt in place (walls finishing while a large scene loads).
|
|
1145
|
+
const position = mesh.geometry?.getAttribute('position');
|
|
1146
|
+
if (!position || position.count === 0)
|
|
1147
|
+
return;
|
|
1148
|
+
box.setFromBufferAttribute(position).applyMatrix4(mesh.matrixWorld);
|
|
1149
|
+
if (box.isEmpty())
|
|
1150
|
+
return;
|
|
1151
|
+
const s = MeshGrid.SIZE;
|
|
1152
|
+
for (let iz = Math.floor(box.min.z / s); iz <= Math.floor(box.max.z / s); iz++)
|
|
1153
|
+
for (let ix = Math.floor(box.min.x / s); ix <= Math.floor(box.max.x / s); ix++) {
|
|
1154
|
+
const key = cellKey(ix, iz);
|
|
1155
|
+
const list = this.cells.get(key);
|
|
1156
|
+
if (list)
|
|
1157
|
+
list.push(mesh);
|
|
1158
|
+
else
|
|
1159
|
+
this.cells.set(key, [mesh]);
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
/** Meshes whose bounds may reach the XZ rect. */
|
|
1164
|
+
near(minX, minZ, maxX, maxZ) {
|
|
1165
|
+
const s = MeshGrid.SIZE;
|
|
1166
|
+
const out = [];
|
|
1167
|
+
this.seen.clear();
|
|
1168
|
+
for (let iz = Math.floor(minZ / s); iz <= Math.floor(maxZ / s); iz++)
|
|
1169
|
+
for (let ix = Math.floor(minX / s); ix <= Math.floor(maxX / s); ix++)
|
|
1170
|
+
for (const mesh of this.cells.get(cellKey(ix, iz)) ?? [])
|
|
1171
|
+
if (!this.seen.has(mesh)) {
|
|
1172
|
+
this.seen.add(mesh);
|
|
1173
|
+
out.push(mesh);
|
|
1174
|
+
}
|
|
1175
|
+
return out;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
const cellKey = (ix, iz) => (ix + 1_000_000) * 2_000_003 + (iz + 1_000_000);
|
|
1179
|
+
function emptyTexture() {
|
|
1180
|
+
const tex = new DataTexture(new Float32Array(4), 1, 1, RGBAFormat, FloatType);
|
|
1162
1181
|
tex.magFilter = tex.minFilter = NearestFilter;
|
|
1163
1182
|
tex.needsUpdate = true;
|
|
1164
1183
|
return tex;
|
|
@@ -1201,6 +1220,32 @@ export function polygonCentroid(poly) {
|
|
|
1201
1220
|
return new Vector2(cx / (3 * a), cz / (3 * a));
|
|
1202
1221
|
}
|
|
1203
1222
|
/** Tanner Helland blackbody fit → linear RGB. */
|
|
1223
|
+
/** A point inside the polygon and outside its holes: the centroid when it is, else a scan. */
|
|
1224
|
+
export function interiorPoint(poly, holes = []) {
|
|
1225
|
+
const inside = (x, z) => pointInPolygon(x, z, poly) && !holes.some((hole) => pointInPolygon(x, z, hole));
|
|
1226
|
+
const c = polygonCentroid(poly);
|
|
1227
|
+
if (inside(c.x, c.y))
|
|
1228
|
+
return c;
|
|
1229
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
1230
|
+
let minZ = Number.POSITIVE_INFINITY;
|
|
1231
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
1232
|
+
let maxZ = Number.NEGATIVE_INFINITY;
|
|
1233
|
+
for (const [x, z] of poly) {
|
|
1234
|
+
minX = Math.min(minX, x);
|
|
1235
|
+
maxX = Math.max(maxX, x);
|
|
1236
|
+
minZ = Math.min(minZ, z);
|
|
1237
|
+
maxZ = Math.max(maxZ, z);
|
|
1238
|
+
}
|
|
1239
|
+
for (let n = 4; n <= 32; n *= 2)
|
|
1240
|
+
for (let j = 1; j < n; j++)
|
|
1241
|
+
for (let i = 1; i < n; i++) {
|
|
1242
|
+
const x = minX + ((maxX - minX) * i) / n;
|
|
1243
|
+
const z = minZ + ((maxZ - minZ) * j) / n;
|
|
1244
|
+
if (inside(x, z))
|
|
1245
|
+
return new Vector2(x, z);
|
|
1246
|
+
}
|
|
1247
|
+
return c;
|
|
1248
|
+
}
|
|
1204
1249
|
export function kelvin(k) {
|
|
1205
1250
|
const t = k / 100;
|
|
1206
1251
|
let r;
|