@oeave/bakery3 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +562 -2
- package/dist/animation-B1h0Ryvj.d.ts +97 -0
- package/dist/bake/index.d.ts +369 -0
- package/dist/bake/index.js +9 -0
- package/dist/bake/index.js.map +1 -0
- package/dist/bake-DZ-CJR6f.d.ts +1364 -0
- package/dist/catalog/index.d.ts +100 -0
- package/dist/catalog/index.js +154 -0
- package/dist/catalog/index.js.map +1 -0
- package/dist/catalog.gen-BM-aNf7n.d.ts +733 -0
- package/dist/chunk-3G5QL4F4.js +145 -0
- package/dist/chunk-3G5QL4F4.js.map +1 -0
- package/dist/chunk-4E5VV4QY.js +12 -0
- package/dist/chunk-4E5VV4QY.js.map +1 -0
- package/dist/chunk-62M6XXNX.js +142 -0
- package/dist/chunk-62M6XXNX.js.map +1 -0
- package/dist/chunk-6FYI6AJM.js +1157 -0
- package/dist/chunk-6FYI6AJM.js.map +1 -0
- package/dist/chunk-6NYR73Y7.js +832 -0
- package/dist/chunk-6NYR73Y7.js.map +1 -0
- package/dist/chunk-APWUCEEB.js +118 -0
- package/dist/chunk-APWUCEEB.js.map +1 -0
- package/dist/chunk-HNMSWQU7.js +1709 -0
- package/dist/chunk-HNMSWQU7.js.map +1 -0
- package/dist/chunk-JFYUDERE.js +1412 -0
- package/dist/chunk-JFYUDERE.js.map +1 -0
- package/dist/chunk-KNUAOILG.js +551 -0
- package/dist/chunk-KNUAOILG.js.map +1 -0
- package/dist/chunk-KZLVTSBI.js +191 -0
- package/dist/chunk-KZLVTSBI.js.map +1 -0
- package/dist/chunk-LAKXC4WR.js +2028 -0
- package/dist/chunk-LAKXC4WR.js.map +1 -0
- package/dist/chunk-NXBAZGNB.js +1107 -0
- package/dist/chunk-NXBAZGNB.js.map +1 -0
- package/dist/chunk-QRCT5UGZ.js +3286 -0
- package/dist/chunk-QRCT5UGZ.js.map +1 -0
- package/dist/chunk-S4AJTLLN.js +23 -0
- package/dist/chunk-S4AJTLLN.js.map +1 -0
- package/dist/chunk-UWBP7B54.js +92 -0
- package/dist/chunk-UWBP7B54.js.map +1 -0
- package/dist/chunk-XK35ANPJ.js +346 -0
- package/dist/chunk-XK35ANPJ.js.map +1 -0
- package/dist/chunk-Z7IYVH22.js +4781 -0
- package/dist/chunk-Z7IYVH22.js.map +1 -0
- package/dist/chunk-ZEBVAIJJ.js +156 -0
- package/dist/chunk-ZEBVAIJJ.js.map +1 -0
- package/dist/devtools/index.d.ts +536 -0
- package/dist/devtools/index.js +15 -0
- package/dist/devtools/index.js.map +1 -0
- package/dist/environments/index.d.ts +93 -0
- package/dist/environments/index.js +382 -0
- package/dist/environments/index.js.map +1 -0
- package/dist/hotspots/index.d.ts +111 -0
- package/dist/hotspots/index.js +285 -0
- package/dist/hotspots/index.js.map +1 -0
- package/dist/index.d.ts +975 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/node/index.cjs +5240 -0
- package/dist/node/index.cjs.map +1 -0
- package/dist/node/index.d.cts +4927 -0
- package/dist/node/index.d.ts +597 -0
- package/dist/node/index.js +1328 -0
- package/dist/node/index.js.map +1 -0
- package/dist/prepare-BtjY4G3q.d.ts +112 -0
- package/dist/presets/index.d.ts +562 -0
- package/dist/presets/index.js +14 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/r3f/index.d.ts +159 -0
- package/dist/r3f/index.js +592 -0
- package/dist/r3f/index.js.map +1 -0
- package/dist/room-FS26KAPQ.js +9 -0
- package/dist/room-FS26KAPQ.js.map +1 -0
- package/dist/rooms.gen-DItzBR9k.d.ts +1462 -0
- package/dist/session-VCIQEO26.js +9 -0
- package/dist/session-VCIQEO26.js.map +1 -0
- package/dist/shapes/index.d.ts +222 -0
- package/dist/shapes/index.js +836 -0
- package/dist/shapes/index.js.map +1 -0
- package/dist/testRun-20OARnQr.d.ts +1139 -0
- package/dist/timeline-ChwgD7bT.d.ts +470 -0
- package/dist/tsl/index.d.ts +165 -0
- package/dist/tsl/index.js +310 -0
- package/dist/tsl/index.js.map +1 -0
- package/package.json +170 -4
|
@@ -0,0 +1,1709 @@
|
|
|
1
|
+
import { rememberEnvironmentSource, sha256 } from './chunk-KZLVTSBI.js';
|
|
2
|
+
import { presetBaseUrl } from './chunk-4E5VV4QY.js';
|
|
3
|
+
import { applyRoomPrebake, roomPresetSpecOf } from './chunk-APWUCEEB.js';
|
|
4
|
+
import { HDRI_PRESETS, PRESETS_VERSION, MATERIAL_PRESETS, PEDESTAL_PRESETS } from './chunk-6NYR73Y7.js';
|
|
5
|
+
import { ROOM_PRESETS } from './chunk-LAKXC4WR.js';
|
|
6
|
+
import { Bakery3Error } from './chunk-3G5QL4F4.js';
|
|
7
|
+
import { EquirectangularReflectionMapping, Color, MathUtils, DataTexture, RGBAFormat, FloatType, LinearSRGBColorSpace, MeshStandardMaterial, Group, MeshPhysicalMaterial, DoubleSide, DirectionalLight, Object3D, Vector3, Float32BufferAttribute, Matrix4, Box3, SpotLight, HemisphereLight, ShadowMaterial, Mesh, LightProbe, FileLoader, HalfFloatType, LinearFilter, TextureLoader, RepeatWrapping, SRGBColorSpace, PlaneGeometry, DataUtils, SphericalHarmonics3, Shape, Path, ShapeGeometry, Texture, BoxGeometry, Quaternion, Euler, BufferGeometry } from 'three';
|
|
8
|
+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
9
|
+
import { GroundedSkybox } from 'three/addons/objects/GroundedSkybox.js';
|
|
10
|
+
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
|
|
11
|
+
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
|
|
12
|
+
|
|
13
|
+
var HALF_MAX = 65504;
|
|
14
|
+
function decodeRadianceHdr(buffer) {
|
|
15
|
+
const bytes = new Uint8Array(buffer);
|
|
16
|
+
let pos = 0;
|
|
17
|
+
const line = () => {
|
|
18
|
+
const end = bytes.indexOf(10, pos);
|
|
19
|
+
if (end < 0) return null;
|
|
20
|
+
let text = "";
|
|
21
|
+
for (let i = pos; i < end; i++) text += String.fromCharCode(bytes[i]);
|
|
22
|
+
pos = end + 1;
|
|
23
|
+
return text;
|
|
24
|
+
};
|
|
25
|
+
const refuse = (why) => new Error(`@oeave/bakery3/presets: not a readable .hdr file (${why}).`);
|
|
26
|
+
const magic = line();
|
|
27
|
+
if (!magic || !/^#\?\S+/.test(magic)) throw refuse("no #? header");
|
|
28
|
+
let format = false;
|
|
29
|
+
let width = 0;
|
|
30
|
+
let height = 0;
|
|
31
|
+
for (let text = line(); text !== null; text = line()) {
|
|
32
|
+
if (text.startsWith("#")) continue;
|
|
33
|
+
if (/^\s*FORMAT=32-bit_rle_rgbe\s*$/.test(text)) format = true;
|
|
34
|
+
const size = /^\s*-Y\s+(\d+)\s+\+X\s+(\d+)\s*$/.exec(text);
|
|
35
|
+
if (size) {
|
|
36
|
+
height = Number(size[1]);
|
|
37
|
+
width = Number(size[2]);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!format) throw refuse("missing FORMAT=32-bit_rle_rgbe");
|
|
42
|
+
if (!(width > 0 && height > 0)) throw refuse("missing -Y h +X w");
|
|
43
|
+
const rgbe = scanlines(bytes.subarray(pos), width, height, refuse);
|
|
44
|
+
const data = new Uint16Array(width * height * 4);
|
|
45
|
+
const one = DataUtils.toHalfFloat(1);
|
|
46
|
+
for (let i = 0; i < width * height; i++) {
|
|
47
|
+
const o = i * 4;
|
|
48
|
+
const e = rgbe[o + 3];
|
|
49
|
+
const scale = e ? Math.pow(2, e - 128) / 255 : 0;
|
|
50
|
+
data[o] = DataUtils.toHalfFloat(Math.min(rgbe[o] * scale, HALF_MAX));
|
|
51
|
+
data[o + 1] = DataUtils.toHalfFloat(
|
|
52
|
+
Math.min(rgbe[o + 1] * scale, HALF_MAX)
|
|
53
|
+
);
|
|
54
|
+
data[o + 2] = DataUtils.toHalfFloat(
|
|
55
|
+
Math.min(rgbe[o + 2] * scale, HALF_MAX)
|
|
56
|
+
);
|
|
57
|
+
data[o + 3] = one;
|
|
58
|
+
}
|
|
59
|
+
return { width, height, data };
|
|
60
|
+
}
|
|
61
|
+
function scanlines(body, width, height, refuse) {
|
|
62
|
+
const flat = width < 8 || width > 32767 || body[0] !== 2 || body[1] !== 2 || (body[2] & 128) !== 0;
|
|
63
|
+
if (flat) {
|
|
64
|
+
if (body.length < width * height * 4) throw refuse("file is cut short");
|
|
65
|
+
return body.subarray(0, width * height * 4);
|
|
66
|
+
}
|
|
67
|
+
const out = new Uint8Array(width * height * 4);
|
|
68
|
+
const planes = new Uint8Array(width * 4);
|
|
69
|
+
let pos = 0;
|
|
70
|
+
for (let row = 0; row < height; row++) {
|
|
71
|
+
if (pos + 4 > body.length) throw refuse("file is cut short");
|
|
72
|
+
if (body[pos] !== 2 || body[pos + 1] !== 2 || (body[pos + 2] << 8 | body[pos + 3]) !== width)
|
|
73
|
+
throw refuse("bad scanline header");
|
|
74
|
+
pos += 4;
|
|
75
|
+
let ptr = 0;
|
|
76
|
+
while (ptr < planes.length) {
|
|
77
|
+
if (pos >= body.length) throw refuse("file is cut short");
|
|
78
|
+
let count = body[pos++];
|
|
79
|
+
const run = count > 128;
|
|
80
|
+
if (run) count -= 128;
|
|
81
|
+
if (count === 0 || ptr + count > planes.length)
|
|
82
|
+
throw refuse("bad scanline data");
|
|
83
|
+
if (run) {
|
|
84
|
+
planes.fill(body[pos++], ptr, ptr + count);
|
|
85
|
+
} else {
|
|
86
|
+
planes.set(body.subarray(pos, pos + count), ptr);
|
|
87
|
+
pos += count;
|
|
88
|
+
}
|
|
89
|
+
ptr += count;
|
|
90
|
+
}
|
|
91
|
+
const base = row * width * 4;
|
|
92
|
+
for (let col = 0; col < width; col++) {
|
|
93
|
+
const o = base + col * 4;
|
|
94
|
+
out[o] = planes[col];
|
|
95
|
+
out[o + 1] = planes[width + col];
|
|
96
|
+
out[o + 2] = planes[2 * width + col];
|
|
97
|
+
out[o + 3] = planes[3 * width + col];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
async function loadRadianceHdr(url) {
|
|
103
|
+
const buffer = await new FileLoader().setResponseType("arraybuffer").loadAsync(url);
|
|
104
|
+
const image = decodeRadianceHdr(buffer);
|
|
105
|
+
const texture = new DataTexture(
|
|
106
|
+
image.data,
|
|
107
|
+
image.width,
|
|
108
|
+
image.height,
|
|
109
|
+
RGBAFormat,
|
|
110
|
+
HalfFloatType
|
|
111
|
+
);
|
|
112
|
+
texture.colorSpace = LinearSRGBColorSpace;
|
|
113
|
+
texture.minFilter = LinearFilter;
|
|
114
|
+
texture.magFilter = LinearFilter;
|
|
115
|
+
texture.generateMipmaps = false;
|
|
116
|
+
texture.flipY = true;
|
|
117
|
+
texture.needsUpdate = true;
|
|
118
|
+
return texture;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/presets/library.ts
|
|
122
|
+
function presetTextureUrl(name, map, size = "2k", base = presetBaseUrl()) {
|
|
123
|
+
return `${base}/v${PRESETS_VERSION}/materials/${name}/${size}/${MATERIAL_PRESETS[name].files[map]}`;
|
|
124
|
+
}
|
|
125
|
+
function pedestalUrl(name, base = presetBaseUrl()) {
|
|
126
|
+
return `${base}/v${PRESETS_VERSION}/objects/${name}.glb`;
|
|
127
|
+
}
|
|
128
|
+
function presetEnvironmentUrl(name, asset = "preview", base = presetBaseUrl()) {
|
|
129
|
+
const file = asset === "preview" ? "preview.hdr" : "render.exr";
|
|
130
|
+
return `${base}/v${PRESETS_VERSION}/hdri/${name}/${file}`;
|
|
131
|
+
}
|
|
132
|
+
async function loadPresetEnvironment(name, options = {}) {
|
|
133
|
+
const base = options.baseUrl ?? presetBaseUrl();
|
|
134
|
+
const texture = await loadPresetHdr(name, base);
|
|
135
|
+
rememberEnvironmentSource(
|
|
136
|
+
texture,
|
|
137
|
+
presetEnvironmentUrl(name, "render", base),
|
|
138
|
+
{ preset: true }
|
|
139
|
+
);
|
|
140
|
+
return texture;
|
|
141
|
+
}
|
|
142
|
+
async function loadPresetHdr(name, base = presetBaseUrl()) {
|
|
143
|
+
if (!HDRI_PRESETS[name]) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`'${String(name)}' is not an HDRI preset. Known: ${Object.keys(HDRI_PRESETS).join(", ")}.`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const texture = await loadRadianceHdr(
|
|
149
|
+
presetEnvironmentUrl(name, "preview", base)
|
|
150
|
+
);
|
|
151
|
+
texture.mapping = EquirectangularReflectionMapping;
|
|
152
|
+
texture.userData.bakery3Preset = {
|
|
153
|
+
kind: "hdri",
|
|
154
|
+
name,
|
|
155
|
+
version: PRESETS_VERSION,
|
|
156
|
+
previewResolution: HDRI_PRESETS[name].preview.resolution,
|
|
157
|
+
renderResolution: HDRI_PRESETS[name].render.resolution
|
|
158
|
+
};
|
|
159
|
+
return texture;
|
|
160
|
+
}
|
|
161
|
+
function createSkyEnvironment(options = {}) {
|
|
162
|
+
const zenith = new Color(options.zenith ?? 3108816);
|
|
163
|
+
const horizon = new Color(options.horizon ?? 10274037);
|
|
164
|
+
const ground = new Color(options.ground ?? 9406073);
|
|
165
|
+
const intensity = options.intensity ?? 1;
|
|
166
|
+
const width = Math.max(8, Math.round(options.width ?? 128));
|
|
167
|
+
const height = width / 2;
|
|
168
|
+
const data = new Float32Array(width * height * 4);
|
|
169
|
+
const color = new Color();
|
|
170
|
+
for (let row = 0; row < height; row++) {
|
|
171
|
+
const elevation = (0.5 - (row + 0.5) / height) * Math.PI;
|
|
172
|
+
if (elevation >= 0) {
|
|
173
|
+
color.copy(zenith).lerp(horizon, Math.pow(1 - Math.sin(elevation), 2.2));
|
|
174
|
+
} else {
|
|
175
|
+
color.copy(horizon).lerp(ground, Math.min(1, -elevation / MathUtils.degToRad(3)));
|
|
176
|
+
}
|
|
177
|
+
for (let col = 0; col < width; col++) {
|
|
178
|
+
const i = (row * width + col) * 4;
|
|
179
|
+
data[i] = color.r * intensity;
|
|
180
|
+
data[i + 1] = color.g * intensity;
|
|
181
|
+
data[i + 2] = color.b * intensity;
|
|
182
|
+
data[i + 3] = 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const texture = new DataTexture(data, width, height, RGBAFormat, FloatType);
|
|
186
|
+
texture.mapping = EquirectangularReflectionMapping;
|
|
187
|
+
texture.colorSpace = LinearSRGBColorSpace;
|
|
188
|
+
texture.flipY = true;
|
|
189
|
+
texture.needsUpdate = true;
|
|
190
|
+
texture.userData.bakery3Preset = { kind: "sky", ...options };
|
|
191
|
+
rememberEnvironmentSource(texture, encodeRadianceHdr(data, width, height));
|
|
192
|
+
return texture;
|
|
193
|
+
}
|
|
194
|
+
function encodeRadianceHdr(data, width, height) {
|
|
195
|
+
if (width < 8 || width > 32767) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`@oeave/bakery3/presets: a Radiance scanline is 8 to 32767 pixels wide, not ${width}.`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const header = new TextEncoder().encode(
|
|
201
|
+
`#?RADIANCE
|
|
202
|
+
FORMAT=32-bit_rle_rgbe
|
|
203
|
+
|
|
204
|
+
-Y ${height} +X ${width}
|
|
205
|
+
`
|
|
206
|
+
);
|
|
207
|
+
const chunks = Math.ceil(width / 128);
|
|
208
|
+
const scanline = 4 + 4 * (width + chunks);
|
|
209
|
+
const out = new Uint8Array(header.length + height * scanline);
|
|
210
|
+
out.set(header, 0);
|
|
211
|
+
let o = header.length;
|
|
212
|
+
const planes = new Uint8Array(width * 4);
|
|
213
|
+
for (let row = 0; row < height; row++) {
|
|
214
|
+
for (let col = 0; col < width; col++) {
|
|
215
|
+
const i = (row * width + col) * 4;
|
|
216
|
+
const r = data[i];
|
|
217
|
+
const g = data[i + 1];
|
|
218
|
+
const b = data[i + 2];
|
|
219
|
+
const max = Math.max(r, g, b);
|
|
220
|
+
if (max < 1e-32) {
|
|
221
|
+
planes[col] = planes[width + col] = planes[2 * width + col] = 0;
|
|
222
|
+
planes[3 * width + col] = 0;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
let e = Math.ceil(Math.log2(max));
|
|
226
|
+
let m = max / Math.pow(2, e);
|
|
227
|
+
if (m >= 1) {
|
|
228
|
+
m /= 2;
|
|
229
|
+
e += 1;
|
|
230
|
+
} else if (m < 0.5) {
|
|
231
|
+
m *= 2;
|
|
232
|
+
e -= 1;
|
|
233
|
+
}
|
|
234
|
+
const scale = m * 256 / max;
|
|
235
|
+
planes[col] = Math.min(255, Math.floor(r * scale));
|
|
236
|
+
planes[width + col] = Math.min(255, Math.floor(g * scale));
|
|
237
|
+
planes[2 * width + col] = Math.min(255, Math.floor(b * scale));
|
|
238
|
+
planes[3 * width + col] = e + 128;
|
|
239
|
+
}
|
|
240
|
+
out[o++] = 2;
|
|
241
|
+
out[o++] = 2;
|
|
242
|
+
out[o++] = width >> 8 & 255;
|
|
243
|
+
out[o++] = width & 255;
|
|
244
|
+
for (let plane = 0; plane < 4; plane++) {
|
|
245
|
+
for (let start = 0; start < width; start += 128) {
|
|
246
|
+
const n = Math.min(128, width - start);
|
|
247
|
+
out[o++] = n;
|
|
248
|
+
out.set(
|
|
249
|
+
planes.subarray(plane * width + start, plane * width + start + n),
|
|
250
|
+
o
|
|
251
|
+
);
|
|
252
|
+
o += n;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return out.buffer;
|
|
257
|
+
}
|
|
258
|
+
function presetRepeat(name) {
|
|
259
|
+
return 1 / MATERIAL_PRESETS[name].tileMeters;
|
|
260
|
+
}
|
|
261
|
+
var textureCache = /* @__PURE__ */ new Map();
|
|
262
|
+
var sharedLoader;
|
|
263
|
+
function loadTexture(url, srgb, repeat, anisotropy) {
|
|
264
|
+
const key = `${url}#${repeat}`;
|
|
265
|
+
const cached = textureCache.get(key);
|
|
266
|
+
if (cached) return cached;
|
|
267
|
+
sharedLoader ??= new TextureLoader();
|
|
268
|
+
const texture = sharedLoader.load(url);
|
|
269
|
+
texture.wrapS = texture.wrapT = RepeatWrapping;
|
|
270
|
+
texture.repeat.set(repeat, repeat);
|
|
271
|
+
if (srgb) texture.colorSpace = SRGBColorSpace;
|
|
272
|
+
texture.anisotropy = anisotropy;
|
|
273
|
+
textureCache.set(key, texture);
|
|
274
|
+
return texture;
|
|
275
|
+
}
|
|
276
|
+
function createPresetMaterial(name, options = {}) {
|
|
277
|
+
const info = MATERIAL_PRESETS[name];
|
|
278
|
+
const size = options.size ?? "2k";
|
|
279
|
+
const base = options.baseUrl ?? presetBaseUrl();
|
|
280
|
+
const repeat = presetRepeat(name) * (options.repeat ?? 1);
|
|
281
|
+
const anisotropy = options.anisotropy ?? 8;
|
|
282
|
+
const url = (map) => presetTextureUrl(name, map, size, base);
|
|
283
|
+
const material = new MeshStandardMaterial({
|
|
284
|
+
metalness: info.metalness,
|
|
285
|
+
roughness: 1,
|
|
286
|
+
map: loadTexture(url("albedo"), true, repeat, anisotropy),
|
|
287
|
+
normalMap: loadTexture(url("normal"), false, repeat, anisotropy),
|
|
288
|
+
roughnessMap: loadTexture(url("roughness"), false, repeat, anisotropy)
|
|
289
|
+
});
|
|
290
|
+
if (options.ao !== false) {
|
|
291
|
+
material.aoMap = loadTexture(url("ao"), false, repeat, anisotropy);
|
|
292
|
+
}
|
|
293
|
+
material.normalScale.setScalar(info.normalScale);
|
|
294
|
+
material.userData.bakery3Preset = { name, size };
|
|
295
|
+
return material;
|
|
296
|
+
}
|
|
297
|
+
async function loadPresetMaterial(name, options = {}) {
|
|
298
|
+
const material = createPresetMaterial(name, options);
|
|
299
|
+
const maps = [
|
|
300
|
+
material.map,
|
|
301
|
+
material.normalMap,
|
|
302
|
+
material.roughnessMap,
|
|
303
|
+
material.aoMap
|
|
304
|
+
];
|
|
305
|
+
await Promise.all(
|
|
306
|
+
maps.filter((t) => t !== null).map((t) => textureReady(t, name))
|
|
307
|
+
);
|
|
308
|
+
return material;
|
|
309
|
+
}
|
|
310
|
+
function textureReady(texture, presetName) {
|
|
311
|
+
return new Promise((resolve, reject) => {
|
|
312
|
+
if (texture.source.data) return resolve();
|
|
313
|
+
const started = Date.now();
|
|
314
|
+
const poll = setInterval(() => {
|
|
315
|
+
if (texture.source.data) {
|
|
316
|
+
clearInterval(poll);
|
|
317
|
+
resolve();
|
|
318
|
+
} else if (Date.now() - started > 3e4) {
|
|
319
|
+
clearInterval(poll);
|
|
320
|
+
reject(
|
|
321
|
+
new Error(
|
|
322
|
+
`@oeave/bakery3/presets: timed out loading a map for '${presetName}'`
|
|
323
|
+
)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}, 50);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
function planeMeters(width, height) {
|
|
330
|
+
const geometry = new PlaneGeometry(width, height);
|
|
331
|
+
const uv = geometry.attributes.uv;
|
|
332
|
+
for (let i = 0; i < uv.count; i++) {
|
|
333
|
+
uv.setXY(i, uv.getX(i) * width, uv.getY(i) * height);
|
|
334
|
+
}
|
|
335
|
+
return geometry;
|
|
336
|
+
}
|
|
337
|
+
var WINDOW_DEFAULTS = {
|
|
338
|
+
offset: 0,
|
|
339
|
+
width: 1.2,
|
|
340
|
+
height: 1.4,
|
|
341
|
+
sill: 0.9,
|
|
342
|
+
frame: true,
|
|
343
|
+
glass: false,
|
|
344
|
+
foliage: true
|
|
345
|
+
};
|
|
346
|
+
var WINDOW_MARGIN = 0.05;
|
|
347
|
+
function resolveWindows(wall, specs, wallWidth, roomHeight) {
|
|
348
|
+
const list = specs === void 0 ? [] : Array.isArray(specs) ? specs : [specs];
|
|
349
|
+
const openings = list.map((spec, index) => {
|
|
350
|
+
const { offset, width, height, sill, frame, glass, foliage } = {
|
|
351
|
+
...WINDOW_DEFAULTS,
|
|
352
|
+
...spec
|
|
353
|
+
};
|
|
354
|
+
const label = `@oeave/bakery3/presets: ${wall} window ${index}`;
|
|
355
|
+
if (width <= 0 || height <= 0) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
`${label} has a non-positive size (${width} \xD7 ${height} m).`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
if (sill < 0) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`${label} has its sill ${sill} m below the floor. Use sill: 0 or higher.`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
const opening = {
|
|
366
|
+
x0: offset - width / 2,
|
|
367
|
+
x1: offset + width / 2,
|
|
368
|
+
y0: sill,
|
|
369
|
+
y1: sill + height,
|
|
370
|
+
frame,
|
|
371
|
+
glass,
|
|
372
|
+
foliage
|
|
373
|
+
};
|
|
374
|
+
const limit = wallWidth / 2 - WINDOW_MARGIN;
|
|
375
|
+
if (opening.x0 < -limit || opening.x1 > limit) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
`${label} (offset ${offset}, width ${width}) does not fit the ${wallWidth} m wall. Keep it ${WINDOW_MARGIN} m clear of the corners.`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
if (opening.y1 > roomHeight - WINDOW_MARGIN) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`${label} reaches ${opening.y1} m but the room is ${roomHeight} m tall. Lower the sill or shrink the height.`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return opening;
|
|
386
|
+
});
|
|
387
|
+
for (let i = 0; i < openings.length; i++) {
|
|
388
|
+
for (let j = i + 1; j < openings.length; j++) {
|
|
389
|
+
const a = openings[i];
|
|
390
|
+
const b = openings[j];
|
|
391
|
+
if (a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`@oeave/bakery3/presets: ${wall} windows ${i} and ${j} overlap. Separate their offsets.`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return openings;
|
|
399
|
+
}
|
|
400
|
+
function wallFaceGeometry(width, height, openings) {
|
|
401
|
+
const shape = new Shape();
|
|
402
|
+
shape.moveTo(-width / 2, 0);
|
|
403
|
+
shape.lineTo(width / 2, 0);
|
|
404
|
+
shape.lineTo(width / 2, height);
|
|
405
|
+
shape.lineTo(-width / 2, height);
|
|
406
|
+
shape.closePath();
|
|
407
|
+
for (const opening of openings) {
|
|
408
|
+
const hole = new Path();
|
|
409
|
+
hole.moveTo(opening.x0, opening.y0);
|
|
410
|
+
hole.lineTo(opening.x0, opening.y1);
|
|
411
|
+
hole.lineTo(opening.x1, opening.y1);
|
|
412
|
+
hole.lineTo(opening.x1, opening.y0);
|
|
413
|
+
hole.closePath();
|
|
414
|
+
shape.holes.push(hole);
|
|
415
|
+
}
|
|
416
|
+
return new ShapeGeometry(shape);
|
|
417
|
+
}
|
|
418
|
+
var FRAME_MEMBER = 0.055;
|
|
419
|
+
var FRAME_DEPTH = 0.045;
|
|
420
|
+
function buildWindow(opening, index, thickness, materials) {
|
|
421
|
+
const { x0, x1, y0, y1 } = opening;
|
|
422
|
+
const w = x1 - x0;
|
|
423
|
+
const h = y1 - y0;
|
|
424
|
+
const cx = (x0 + x1) / 2;
|
|
425
|
+
const cy = (y0 + y1) / 2;
|
|
426
|
+
const z = -thickness / 2;
|
|
427
|
+
const group = new Group();
|
|
428
|
+
group.name = `window-${index}`;
|
|
429
|
+
const reveal = (geometry, position, rotation, name) => {
|
|
430
|
+
const mesh = new Mesh(geometry, materials.wall);
|
|
431
|
+
mesh.name = name;
|
|
432
|
+
mesh.position.set(...position);
|
|
433
|
+
mesh.rotation.set(...rotation);
|
|
434
|
+
mesh.castShadow = true;
|
|
435
|
+
mesh.receiveShadow = true;
|
|
436
|
+
group.add(mesh);
|
|
437
|
+
};
|
|
438
|
+
reveal(
|
|
439
|
+
planeMeters(thickness, h),
|
|
440
|
+
[x0, cy, z],
|
|
441
|
+
[0, Math.PI / 2, 0],
|
|
442
|
+
"reveal-left"
|
|
443
|
+
);
|
|
444
|
+
reveal(
|
|
445
|
+
planeMeters(thickness, h),
|
|
446
|
+
[x1, cy, z],
|
|
447
|
+
[0, -Math.PI / 2, 0],
|
|
448
|
+
"reveal-right"
|
|
449
|
+
);
|
|
450
|
+
reveal(
|
|
451
|
+
planeMeters(w, thickness),
|
|
452
|
+
[cx, y1, z],
|
|
453
|
+
[Math.PI / 2, 0, 0],
|
|
454
|
+
"reveal-top"
|
|
455
|
+
);
|
|
456
|
+
reveal(
|
|
457
|
+
planeMeters(w, thickness),
|
|
458
|
+
[cx, y0, z],
|
|
459
|
+
[-Math.PI / 2, 0, 0],
|
|
460
|
+
"reveal-sill"
|
|
461
|
+
);
|
|
462
|
+
if (opening.frame) {
|
|
463
|
+
const bar = (bw, bh, position) => {
|
|
464
|
+
const mesh = new Mesh(
|
|
465
|
+
new BoxGeometry(bw, bh, FRAME_DEPTH),
|
|
466
|
+
materials.frame
|
|
467
|
+
);
|
|
468
|
+
mesh.name = "frame";
|
|
469
|
+
mesh.position.set(...position);
|
|
470
|
+
mesh.castShadow = true;
|
|
471
|
+
mesh.receiveShadow = true;
|
|
472
|
+
group.add(mesh);
|
|
473
|
+
};
|
|
474
|
+
bar(w, FRAME_MEMBER, [cx, y0 + FRAME_MEMBER / 2, z]);
|
|
475
|
+
bar(w, FRAME_MEMBER, [cx, y1 - FRAME_MEMBER / 2, z]);
|
|
476
|
+
bar(FRAME_MEMBER, h - 2 * FRAME_MEMBER, [x0 + FRAME_MEMBER / 2, cy, z]);
|
|
477
|
+
bar(FRAME_MEMBER, h - 2 * FRAME_MEMBER, [x1 - FRAME_MEMBER / 2, cy, z]);
|
|
478
|
+
if (w > 1.05) bar(FRAME_MEMBER * 0.7, h - 2 * FRAME_MEMBER, [cx, cy, z]);
|
|
479
|
+
}
|
|
480
|
+
if (opening.glass) {
|
|
481
|
+
const pane = new Mesh(planeMeters(w, h), materials.glass);
|
|
482
|
+
pane.name = "glass";
|
|
483
|
+
pane.position.set(cx, cy, z + FRAME_DEPTH / 2 + 2e-3);
|
|
484
|
+
group.add(pane);
|
|
485
|
+
}
|
|
486
|
+
if (opening.foliage) {
|
|
487
|
+
group.add(buildFoliage(opening, index, thickness, materials.foliage));
|
|
488
|
+
}
|
|
489
|
+
return group;
|
|
490
|
+
}
|
|
491
|
+
function buildFoliage(opening, index, thickness, materials) {
|
|
492
|
+
const { x0, x1, y0, y1 } = opening;
|
|
493
|
+
const h = y1 - y0;
|
|
494
|
+
const random = mulberry32(
|
|
495
|
+
Math.round((x0 * 73 + y0 * 151 + x1 * 271 + y1 * 397) * 1e3) + index * 7919
|
|
496
|
+
);
|
|
497
|
+
const depth = -(thickness + 0.7);
|
|
498
|
+
const cell = 0.5;
|
|
499
|
+
const xStart = x0 - 0.35;
|
|
500
|
+
const xEnd = x1 + 0.35;
|
|
501
|
+
const yStart = y0 + h * 0.15;
|
|
502
|
+
const yEnd = y1 + 0.7;
|
|
503
|
+
const cols = Math.max(2, Math.round((xEnd - xStart) / cell));
|
|
504
|
+
const rows = Math.max(2, Math.round((yEnd - yStart) / cell));
|
|
505
|
+
const leaves = [];
|
|
506
|
+
const twigs = [];
|
|
507
|
+
const position = new Vector3();
|
|
508
|
+
const rotation = new Quaternion();
|
|
509
|
+
const unit = new Vector3(1, 1, 1);
|
|
510
|
+
const matrix = new Matrix4();
|
|
511
|
+
const euler = new Euler();
|
|
512
|
+
const place = (geometry, x, y, z, rx, ry, rz) => {
|
|
513
|
+
position.set(x, y, z);
|
|
514
|
+
euler.set(rx, ry, rz);
|
|
515
|
+
rotation.setFromEuler(euler);
|
|
516
|
+
matrix.compose(position, rotation, unit);
|
|
517
|
+
geometry.applyMatrix4(matrix);
|
|
518
|
+
return geometry;
|
|
519
|
+
};
|
|
520
|
+
for (let row = 0; row < rows; row++) {
|
|
521
|
+
for (let col = 0; col < cols; col++) {
|
|
522
|
+
if (random() < 0.3) continue;
|
|
523
|
+
const clumpX = xStart + (col + 0.5) * (xEnd - xStart) / cols + (random() - 0.5) * cell * 0.6;
|
|
524
|
+
const clumpY = yStart + (row + 0.5) * (yEnd - yStart) / rows + (random() - 0.5) * cell * 0.6;
|
|
525
|
+
const clumpZ = depth + (random() - 0.5) * 0.3;
|
|
526
|
+
const radius = 0.24 + random() * 0.14;
|
|
527
|
+
for (let t = 0; t < 3; t++) {
|
|
528
|
+
twigs.push(
|
|
529
|
+
place(
|
|
530
|
+
new BoxGeometry(8e-3, 8e-3, radius * 1.7),
|
|
531
|
+
clumpX + (random() - 0.5) * radius * 0.5,
|
|
532
|
+
clumpY + (random() - 0.5) * radius * 0.5,
|
|
533
|
+
clumpZ,
|
|
534
|
+
(random() - 0.5) * 0.8,
|
|
535
|
+
(random() - 0.5) * 3.2,
|
|
536
|
+
(random() - 0.5) * 0.8
|
|
537
|
+
)
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
const count = Math.round(radius * radius * Math.PI * 210);
|
|
541
|
+
for (let i = 0; i < count; i++) {
|
|
542
|
+
leaves.push(
|
|
543
|
+
place(
|
|
544
|
+
leafGeometry(0.08 + random() * 0.06),
|
|
545
|
+
clumpX + (random() + random() - 1) * radius,
|
|
546
|
+
clumpY + (random() + random() - 1) * radius * 0.85,
|
|
547
|
+
clumpZ + (random() - 0.5) * 0.2,
|
|
548
|
+
(random() - 0.5) * 1.4,
|
|
549
|
+
(random() - 0.5) * 1.4,
|
|
550
|
+
random() * Math.PI * 2
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const merge = (parts, name, material) => {
|
|
557
|
+
const geometry = mergeGeometries(parts, false) ?? new BufferGeometry();
|
|
558
|
+
for (const part of parts) part.dispose();
|
|
559
|
+
geometry.computeVertexNormals();
|
|
560
|
+
const mesh = new Mesh(geometry, material);
|
|
561
|
+
mesh.name = name;
|
|
562
|
+
mesh.castShadow = true;
|
|
563
|
+
mesh.receiveShadow = false;
|
|
564
|
+
return mesh;
|
|
565
|
+
};
|
|
566
|
+
const group = new Group();
|
|
567
|
+
group.name = "foliage";
|
|
568
|
+
group.add(
|
|
569
|
+
merge(leaves, "leaves", materials.leaf),
|
|
570
|
+
merge(twigs, "twigs", materials.twig)
|
|
571
|
+
);
|
|
572
|
+
return group;
|
|
573
|
+
}
|
|
574
|
+
function leafGeometry(length) {
|
|
575
|
+
const half = length * 0.34;
|
|
576
|
+
const shape = new Shape();
|
|
577
|
+
shape.moveTo(0, 0);
|
|
578
|
+
shape.quadraticCurveTo(half, length * 0.45, 0, length);
|
|
579
|
+
shape.quadraticCurveTo(-half, length * 0.55, 0, 0);
|
|
580
|
+
return new ShapeGeometry(shape, 5);
|
|
581
|
+
}
|
|
582
|
+
function mulberry32(seed) {
|
|
583
|
+
let a = seed >>> 0;
|
|
584
|
+
return () => {
|
|
585
|
+
a = a + 1831565813 >>> 0;
|
|
586
|
+
let t = a;
|
|
587
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
588
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
589
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function createRoom(options = {}) {
|
|
593
|
+
const width = options.width ?? 4;
|
|
594
|
+
const depth = options.depth ?? 4;
|
|
595
|
+
const height = options.height ?? 2.7;
|
|
596
|
+
const thickness = options.wallThickness ?? 0.15;
|
|
597
|
+
const materialFor = (name) => {
|
|
598
|
+
const material = createPresetMaterial(name, options.materialOptions);
|
|
599
|
+
material.shadowSide = DoubleSide;
|
|
600
|
+
return material;
|
|
601
|
+
};
|
|
602
|
+
const room = new Group();
|
|
603
|
+
room.name = "bakery3-room";
|
|
604
|
+
room.userData.bakery3Preset = {
|
|
605
|
+
room: { width, depth, height, windows: options.windows ?? null }
|
|
606
|
+
};
|
|
607
|
+
const surface2 = (name, geometry, material, position, rotation, parent = room) => {
|
|
608
|
+
const mesh = new Mesh(geometry, material);
|
|
609
|
+
mesh.name = name;
|
|
610
|
+
mesh.position.set(...position);
|
|
611
|
+
mesh.rotation.set(...rotation);
|
|
612
|
+
mesh.castShadow = true;
|
|
613
|
+
mesh.receiveShadow = true;
|
|
614
|
+
parent.add(mesh);
|
|
615
|
+
return mesh;
|
|
616
|
+
};
|
|
617
|
+
const floor = materialFor(options.floor ?? "oak_floor");
|
|
618
|
+
surface2(
|
|
619
|
+
"floor",
|
|
620
|
+
planeMeters(width, depth),
|
|
621
|
+
floor,
|
|
622
|
+
[0, 0, 0],
|
|
623
|
+
[-Math.PI / 2, 0, 0]
|
|
624
|
+
);
|
|
625
|
+
if (options.ceiling !== null) {
|
|
626
|
+
const ceiling = materialFor(options.ceiling ?? "matte_paint");
|
|
627
|
+
surface2(
|
|
628
|
+
"ceiling",
|
|
629
|
+
planeMeters(width, depth),
|
|
630
|
+
ceiling,
|
|
631
|
+
[0, height, 0],
|
|
632
|
+
[Math.PI / 2, 0, 0]
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
const wallMaterial = materialFor(options.walls ?? "white_plaster");
|
|
636
|
+
const frameMaterial = new MeshStandardMaterial({
|
|
637
|
+
color: 16053232,
|
|
638
|
+
roughness: 0.5,
|
|
639
|
+
metalness: 0
|
|
640
|
+
});
|
|
641
|
+
const glassMaterial = new MeshPhysicalMaterial({
|
|
642
|
+
roughness: 0.04,
|
|
643
|
+
metalness: 0,
|
|
644
|
+
transmission: 1,
|
|
645
|
+
ior: 1.52,
|
|
646
|
+
thickness: 6e-3,
|
|
647
|
+
side: DoubleSide
|
|
648
|
+
});
|
|
649
|
+
const leafMaterial = new MeshStandardMaterial({
|
|
650
|
+
color: 3824170,
|
|
651
|
+
roughness: 0.7,
|
|
652
|
+
metalness: 0,
|
|
653
|
+
side: DoubleSide
|
|
654
|
+
});
|
|
655
|
+
leafMaterial.name = "foliage-leaf";
|
|
656
|
+
const twigMaterial = new MeshStandardMaterial({
|
|
657
|
+
color: 4864556,
|
|
658
|
+
roughness: 0.9,
|
|
659
|
+
metalness: 0
|
|
660
|
+
});
|
|
661
|
+
twigMaterial.name = "foliage-twig";
|
|
662
|
+
const buildWall = (name, wallWidth, position, rotationY) => {
|
|
663
|
+
const openings = resolveWindows(
|
|
664
|
+
name,
|
|
665
|
+
options.windows?.[name],
|
|
666
|
+
wallWidth,
|
|
667
|
+
height
|
|
668
|
+
);
|
|
669
|
+
const wall = new Group();
|
|
670
|
+
wall.name = `wall-${name}`;
|
|
671
|
+
wall.position.set(...position);
|
|
672
|
+
wall.rotation.y = rotationY;
|
|
673
|
+
surface2(
|
|
674
|
+
"face",
|
|
675
|
+
wallFaceGeometry(wallWidth, height, openings),
|
|
676
|
+
wallMaterial,
|
|
677
|
+
[0, 0, 0],
|
|
678
|
+
[0, 0, 0],
|
|
679
|
+
wall
|
|
680
|
+
);
|
|
681
|
+
openings.forEach((opening, index) => {
|
|
682
|
+
wall.add(
|
|
683
|
+
buildWindow(opening, index, thickness, {
|
|
684
|
+
wall: wallMaterial,
|
|
685
|
+
frame: frameMaterial,
|
|
686
|
+
glass: glassMaterial,
|
|
687
|
+
foliage: { leaf: leafMaterial, twig: twigMaterial }
|
|
688
|
+
})
|
|
689
|
+
);
|
|
690
|
+
});
|
|
691
|
+
room.add(wall);
|
|
692
|
+
};
|
|
693
|
+
buildWall("north", width, [0, 0, -depth / 2], 0);
|
|
694
|
+
buildWall("south", width, [0, 0, depth / 2], Math.PI);
|
|
695
|
+
buildWall("west", depth, [-width / 2, 0, 0], Math.PI / 2);
|
|
696
|
+
buildWall("east", depth, [width / 2, 0, 0], -Math.PI / 2);
|
|
697
|
+
if (options.sun) {
|
|
698
|
+
room.add(
|
|
699
|
+
createRoomSun(
|
|
700
|
+
options.sun === true ? {} : options.sun,
|
|
701
|
+
width,
|
|
702
|
+
depth,
|
|
703
|
+
height
|
|
704
|
+
)
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
return room;
|
|
708
|
+
}
|
|
709
|
+
function createRoomSun(options, width, depth, height) {
|
|
710
|
+
const elevation = MathUtils.degToRad(options.elevation ?? 32);
|
|
711
|
+
const azimuth = MathUtils.degToRad(options.azimuth ?? 0);
|
|
712
|
+
const reach = Math.hypot(width, depth, height);
|
|
713
|
+
const group = new Group();
|
|
714
|
+
group.name = "sun";
|
|
715
|
+
const sun = new DirectionalLight(
|
|
716
|
+
options.color ?? 16773855,
|
|
717
|
+
options.intensity ?? 3.5
|
|
718
|
+
);
|
|
719
|
+
sun.name = "sun-light";
|
|
720
|
+
const horizontal = 2 * reach * Math.cos(elevation);
|
|
721
|
+
sun.position.set(
|
|
722
|
+
horizontal * Math.sin(azimuth),
|
|
723
|
+
2 * reach * Math.sin(elevation) + height / 2,
|
|
724
|
+
-horizontal * Math.cos(azimuth)
|
|
725
|
+
);
|
|
726
|
+
const target = new Object3D();
|
|
727
|
+
target.name = "sun-target";
|
|
728
|
+
target.position.set(0, height / 2, 0);
|
|
729
|
+
group.add(sun, target);
|
|
730
|
+
sun.target = target;
|
|
731
|
+
sun.userData.bakery3 = { angleDeg: options.angleDeg ?? 0.53 };
|
|
732
|
+
sun.castShadow = true;
|
|
733
|
+
sun.shadow.mapSize.set(2048, 2048);
|
|
734
|
+
sun.shadow.camera.left = sun.shadow.camera.bottom = -reach;
|
|
735
|
+
sun.shadow.camera.right = sun.shadow.camera.top = reach;
|
|
736
|
+
sun.shadow.camera.near = 0.1;
|
|
737
|
+
sun.shadow.camera.far = 6 * reach;
|
|
738
|
+
sun.shadow.bias = -3e-4;
|
|
739
|
+
return group;
|
|
740
|
+
}
|
|
741
|
+
var sharedGltfLoader;
|
|
742
|
+
var gltfCache = /* @__PURE__ */ new Map();
|
|
743
|
+
function loadGlb(url) {
|
|
744
|
+
let pending = gltfCache.get(url);
|
|
745
|
+
if (!pending) {
|
|
746
|
+
sharedGltfLoader ??= new GLTFLoader();
|
|
747
|
+
pending = sharedGltfLoader.loadAsync(url).then((gltf) => gltf.scene);
|
|
748
|
+
gltfCache.set(url, pending);
|
|
749
|
+
}
|
|
750
|
+
return pending;
|
|
751
|
+
}
|
|
752
|
+
function createPedestal(typeOrOptions = {}, more = {}) {
|
|
753
|
+
const options = typeof typeOrOptions === "string" ? { ...more, type: typeOrOptions } : typeOrOptions ?? {};
|
|
754
|
+
const type = options.type ?? "pedestal_block";
|
|
755
|
+
const info = PEDESTAL_PRESETS[type];
|
|
756
|
+
if (!info) {
|
|
757
|
+
throw new Bakery3Error({
|
|
758
|
+
code: "InvalidRequest",
|
|
759
|
+
message: `"${String(type)}" is not a pedestal.`,
|
|
760
|
+
fix: `Use one of: ${Object.keys(PEDESTAL_PRESETS).join(", ")}.`,
|
|
761
|
+
docs: "https://bakery3.com/docs/presets"
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
const material = createPresetMaterial(
|
|
765
|
+
options.material ?? info.defaultMaterial,
|
|
766
|
+
{
|
|
767
|
+
baseUrl: options.baseUrl,
|
|
768
|
+
...options.materialOptions
|
|
769
|
+
}
|
|
770
|
+
);
|
|
771
|
+
const pedestal = new Group();
|
|
772
|
+
pedestal.name = `bakery3-${type}`;
|
|
773
|
+
pedestal.userData.topY = info.size.y;
|
|
774
|
+
pedestal.userData.bakery3Preset = { object: type };
|
|
775
|
+
pedestal.userData.ready = loadGlb(
|
|
776
|
+
pedestalUrl(type, options.baseUrl ?? presetBaseUrl())
|
|
777
|
+
).then((glb) => {
|
|
778
|
+
const instance = glb.clone();
|
|
779
|
+
instance.traverse((node) => {
|
|
780
|
+
if (!node.isMesh) return;
|
|
781
|
+
const mesh = node;
|
|
782
|
+
mesh.geometry = mesh.geometry.clone();
|
|
783
|
+
mesh.material = material;
|
|
784
|
+
mesh.castShadow = true;
|
|
785
|
+
mesh.receiveShadow = true;
|
|
786
|
+
});
|
|
787
|
+
pedestal.add(instance);
|
|
788
|
+
return pedestal;
|
|
789
|
+
});
|
|
790
|
+
return pedestal;
|
|
791
|
+
}
|
|
792
|
+
function bakedRoomUrl(name, lighting, base = presetBaseUrl(), presetsVersion = 1) {
|
|
793
|
+
return `${base.replace(/\/$/, "")}/v${presetsVersion}/rooms/${name}/baked/${lighting}/`;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/presets/room.ts
|
|
797
|
+
var DOME_RADIUS = 60;
|
|
798
|
+
var GROUND_DROP = 0.05;
|
|
799
|
+
function presetRoomUrl(name, base = presetBaseUrl()) {
|
|
800
|
+
const info = ROOM_PRESETS[name];
|
|
801
|
+
return `${base.replace(/\/$/, "")}/v${info.presetsVersion}/${info.preview.glb}`;
|
|
802
|
+
}
|
|
803
|
+
var IES_PEAK_PER_WATT = 6e-3;
|
|
804
|
+
var FIXTURE_GLOW_STRENGTH = 5;
|
|
805
|
+
var DOWNLIGHT_SCALE = 9;
|
|
806
|
+
var CANDELA_PER_WATT = 683 / (4 * Math.PI);
|
|
807
|
+
var CAN_FULL_WATTS = 20;
|
|
808
|
+
var ACCENT_BEAM = {
|
|
809
|
+
narrow: { angleDeg: 17.5, penumbra: 0.5 },
|
|
810
|
+
medium: { angleDeg: 31.6, penumbra: 0.5 },
|
|
811
|
+
wide: { angleDeg: 48.6, penumbra: 0.5 }
|
|
812
|
+
};
|
|
813
|
+
var MODE_LOOK = {
|
|
814
|
+
day: { hemi: 0.55, sun: 0.42, point: 10, sky: "#e9eef4", ground: "#6a5f52" },
|
|
815
|
+
golden: { hemi: 0.4, sun: 0.5, point: 10, sky: "#f1dcc2", ground: "#6a5f52" },
|
|
816
|
+
overcast: {
|
|
817
|
+
hemi: 0.8,
|
|
818
|
+
sun: 0.3,
|
|
819
|
+
point: 10,
|
|
820
|
+
sky: "#dfe3e8",
|
|
821
|
+
ground: "#6a5f52"
|
|
822
|
+
},
|
|
823
|
+
night: { hemi: 0.06, sun: 0.3, point: 4, sky: "#2a3550", ground: "#1a1712" }
|
|
824
|
+
};
|
|
825
|
+
function hexLuminance(hex) {
|
|
826
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
827
|
+
if (!m) return 1;
|
|
828
|
+
const channel = (i) => {
|
|
829
|
+
const v = parseInt(m[1].slice(i, i + 2), 16) / 255;
|
|
830
|
+
return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
831
|
+
};
|
|
832
|
+
return 0.2126 * channel(0) + 0.7152 * channel(2) + 0.0722 * channel(4);
|
|
833
|
+
}
|
|
834
|
+
function hemisphereFill(look, fill) {
|
|
835
|
+
if (!fill?.sky || !fill?.ground) {
|
|
836
|
+
return {
|
|
837
|
+
sky: new Color(look.sky),
|
|
838
|
+
ground: new Color(look.ground),
|
|
839
|
+
intensity: 1
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
const skyLevel = hexLuminance(look.sky);
|
|
843
|
+
const groundLevel = hexLuminance(look.ground);
|
|
844
|
+
const peak = (rgb) => Math.max(rgb[0], rgb[1], rgb[2]);
|
|
845
|
+
const scale = Math.max(
|
|
846
|
+
skyLevel * peak(fill.sky),
|
|
847
|
+
groundLevel * peak(fill.ground),
|
|
848
|
+
1e-6
|
|
849
|
+
);
|
|
850
|
+
const color = (rgb, level) => new Color().setRGB(
|
|
851
|
+
rgb[0] * level / scale,
|
|
852
|
+
rgb[1] * level / scale,
|
|
853
|
+
rgb[2] * level / scale
|
|
854
|
+
);
|
|
855
|
+
return {
|
|
856
|
+
sky: color(fill.sky, skyLevel),
|
|
857
|
+
ground: color(fill.ground, groundLevel),
|
|
858
|
+
intensity: scale
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
var sharedLoader2;
|
|
862
|
+
var glbCache = /* @__PURE__ */ new Map();
|
|
863
|
+
function loadRoomGlb(url) {
|
|
864
|
+
let pending = glbCache.get(url);
|
|
865
|
+
if (!pending) {
|
|
866
|
+
if (!sharedLoader2) {
|
|
867
|
+
sharedLoader2 = new GLTFLoader();
|
|
868
|
+
sharedLoader2.setMeshoptDecoder(MeshoptDecoder);
|
|
869
|
+
}
|
|
870
|
+
pending = sharedLoader2.loadAsync(url).then((gltf) => gltf.scene);
|
|
871
|
+
glbCache.set(url, pending);
|
|
872
|
+
}
|
|
873
|
+
return pending;
|
|
874
|
+
}
|
|
875
|
+
var BAKED_SHADOW_REACH = 1.6;
|
|
876
|
+
var bakedCache = /* @__PURE__ */ new Map();
|
|
877
|
+
function loadBakedGlb(url, exposure) {
|
|
878
|
+
let pending = bakedCache.get(url);
|
|
879
|
+
if (!pending) {
|
|
880
|
+
pending = new GLTFLoader().loadAsync(url).then((gltf) => {
|
|
881
|
+
const scale = exposure > 0 ? 1 / exposure : 1;
|
|
882
|
+
const seen = /* @__PURE__ */ new Set();
|
|
883
|
+
gltf.scene.traverse((node) => {
|
|
884
|
+
const light = node;
|
|
885
|
+
if (light.isLight) light.intensity *= scale;
|
|
886
|
+
const mesh = node;
|
|
887
|
+
if (!mesh.isMesh) return;
|
|
888
|
+
for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
|
|
889
|
+
if (seen.has(material)) continue;
|
|
890
|
+
seen.add(material);
|
|
891
|
+
if (bakedLit(mesh) === "glow") material.side = DoubleSide;
|
|
892
|
+
const basic = material;
|
|
893
|
+
const standard = material;
|
|
894
|
+
if (basic.isMeshBasicMaterial) basic.color.multiplyScalar(scale);
|
|
895
|
+
else if (standard.isMeshStandardMaterial)
|
|
896
|
+
standard.emissiveIntensity *= scale;
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
return gltf.scene;
|
|
900
|
+
});
|
|
901
|
+
bakedCache.set(url, pending);
|
|
902
|
+
pending.catch(() => bakedCache.delete(url));
|
|
903
|
+
}
|
|
904
|
+
return pending;
|
|
905
|
+
}
|
|
906
|
+
function scaleHalfFloats(data, scale) {
|
|
907
|
+
if (scale === 1) return;
|
|
908
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
909
|
+
for (let c = 0; c < 3; c++) {
|
|
910
|
+
data[i + c] = DataUtils.toHalfFloat(
|
|
911
|
+
Math.min(DataUtils.fromHalfFloat(data[i + c]) * scale, 65e3)
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function irradianceOf(data, width, height) {
|
|
917
|
+
const sh = new SphericalHarmonics3();
|
|
918
|
+
const basis = new Array(9).fill(0);
|
|
919
|
+
const direction = new Vector3();
|
|
920
|
+
const color = new Vector3();
|
|
921
|
+
const step = Math.max(1, Math.floor(width / 128));
|
|
922
|
+
let total = 0;
|
|
923
|
+
for (let row = Math.floor(step / 2); row < height; row += step) {
|
|
924
|
+
const latitude = (0.5 - (row + 0.5) / height) * Math.PI;
|
|
925
|
+
const weight = Math.cos(latitude);
|
|
926
|
+
for (let column = Math.floor(step / 2); column < width; column += step) {
|
|
927
|
+
const longitude = ((column + 0.5) / width - 0.5) * 2 * Math.PI;
|
|
928
|
+
direction.set(
|
|
929
|
+
Math.cos(latitude) * Math.cos(longitude),
|
|
930
|
+
Math.sin(latitude),
|
|
931
|
+
Math.cos(latitude) * Math.sin(longitude)
|
|
932
|
+
);
|
|
933
|
+
const at = (row * width + column) * 4;
|
|
934
|
+
color.set(
|
|
935
|
+
DataUtils.fromHalfFloat(data[at]),
|
|
936
|
+
DataUtils.fromHalfFloat(data[at + 1]),
|
|
937
|
+
DataUtils.fromHalfFloat(data[at + 2])
|
|
938
|
+
);
|
|
939
|
+
SphericalHarmonics3.getBasisAt(direction, basis);
|
|
940
|
+
for (let j = 0; j < 9; j++)
|
|
941
|
+
sh.coefficients[j].addScaledVector(color, basis[j] * weight);
|
|
942
|
+
total += weight;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (total > 0) sh.scale(4 * Math.PI / total);
|
|
946
|
+
return sh;
|
|
947
|
+
}
|
|
948
|
+
function bakedLit(mesh) {
|
|
949
|
+
for (let at = mesh; at; at = at.parent) {
|
|
950
|
+
if (typeof at.userData.bakery3Lit === "string")
|
|
951
|
+
return at.userData.bakery3Lit;
|
|
952
|
+
}
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
function bakedRole(mesh) {
|
|
956
|
+
for (let at = mesh; at; at = at.parent) {
|
|
957
|
+
if (typeof at.userData.bakery3Role === "string")
|
|
958
|
+
return at.userData.bakery3Role;
|
|
959
|
+
}
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
var surface = (option) => option === void 0 ? void 0 : typeof option === "string" ? { material: option } : {
|
|
963
|
+
material: option.material,
|
|
964
|
+
...option.color ? { color: option.color } : {}
|
|
965
|
+
};
|
|
966
|
+
var PLANTER_PARTS = /* @__PURE__ */ new Set([
|
|
967
|
+
"pot",
|
|
968
|
+
"base",
|
|
969
|
+
"gravel",
|
|
970
|
+
"pebbles",
|
|
971
|
+
"ground",
|
|
972
|
+
"dirt"
|
|
973
|
+
]);
|
|
974
|
+
var ROOM_STAND_INS = 12;
|
|
975
|
+
function stampRoomStandIns(root, roomPlants) {
|
|
976
|
+
const plants = [...roomPlants].sort((a, b) => b.length - a.length);
|
|
977
|
+
const wanted = [];
|
|
978
|
+
const groups = /* @__PURE__ */ new Set();
|
|
979
|
+
root.traverse((node) => {
|
|
980
|
+
const mesh = node;
|
|
981
|
+
if (!mesh.isMesh) return;
|
|
982
|
+
const hint = plantPart(mesh, plants) ?? bookPart(mesh.name);
|
|
983
|
+
if (!hint) return;
|
|
984
|
+
wanted.push({ mesh, ...hint });
|
|
985
|
+
groups.add(hint.group);
|
|
986
|
+
});
|
|
987
|
+
if (groups.size > ROOM_STAND_INS) return 0;
|
|
988
|
+
for (const { mesh, to, group } of wanted) {
|
|
989
|
+
const existing = mesh.userData.bakery3 ?? {};
|
|
990
|
+
if (existing.simplify !== void 0) continue;
|
|
991
|
+
mesh.userData.bakery3 = { ...existing, simplify: { to, group } };
|
|
992
|
+
}
|
|
993
|
+
return groups.size;
|
|
994
|
+
}
|
|
995
|
+
function bookPart(name) {
|
|
996
|
+
const parts = name.split(".");
|
|
997
|
+
if (parts[0] !== "book" || parts.length < 4) return null;
|
|
998
|
+
return { to: "box", group: `room:books/${parts[1]}.${parts[2]}.${parts[3]}` };
|
|
999
|
+
}
|
|
1000
|
+
function plantPart(mesh, plants) {
|
|
1001
|
+
const preset = plants.find(
|
|
1002
|
+
(presetName) => mesh.name.startsWith(`${presetName}_`)
|
|
1003
|
+
);
|
|
1004
|
+
if (!preset) return null;
|
|
1005
|
+
const part = mesh.name.slice(preset.length + 1).split(".")[0] ?? "";
|
|
1006
|
+
const where = `${mesh.parent?.name ?? "room"}/${preset}`;
|
|
1007
|
+
return PLANTER_PARTS.has(part) ? { to: "lowpoly", group: `room:planter/${where}` } : { to: "billboard", group: `room:foliage/${where}` };
|
|
1008
|
+
}
|
|
1009
|
+
function stampRoomIds(root, name) {
|
|
1010
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1011
|
+
let named = 0;
|
|
1012
|
+
root.traverse((node) => {
|
|
1013
|
+
if (!node.isMesh) return;
|
|
1014
|
+
if (typeof node.userData.bakery3Id === "string" && node.userData.bakery3Id)
|
|
1015
|
+
return;
|
|
1016
|
+
const base = `room:${name}/${node.name || node.type}`;
|
|
1017
|
+
const before = seen.get(base) ?? 0;
|
|
1018
|
+
seen.set(base, before + 1);
|
|
1019
|
+
node.userData.bakery3Id = before === 0 ? base : `${base}#${before}`;
|
|
1020
|
+
named++;
|
|
1021
|
+
});
|
|
1022
|
+
return named;
|
|
1023
|
+
}
|
|
1024
|
+
var plainName = (object) => object.name.toLowerCase().replace(/[._]/g, "");
|
|
1025
|
+
async function hangPictures(instance, pictures, roomName) {
|
|
1026
|
+
const frames = [];
|
|
1027
|
+
instance.traverse((node) => {
|
|
1028
|
+
const mesh = node;
|
|
1029
|
+
if (mesh.isMesh && /^picture\.\d+\.art$/.test(mesh.name)) frames.push(mesh);
|
|
1030
|
+
});
|
|
1031
|
+
frames.sort(
|
|
1032
|
+
(a, b) => Number(a.name.split(".")[1]) - Number(b.name.split(".")[1])
|
|
1033
|
+
);
|
|
1034
|
+
if (pictures.length > frames.length) {
|
|
1035
|
+
throw new Bakery3Error({
|
|
1036
|
+
code: "InvalidRequest",
|
|
1037
|
+
message: `${roomName} hangs ${frames.length} picture${frames.length === 1 ? "" : "s"}, and ${pictures.length} were given.`,
|
|
1038
|
+
why: "Pictures fill the room's own frames in order; there is no wall to hang a spare on.",
|
|
1039
|
+
fix: `ROOM_PRESETS['${roomName}'].pictures says how many there are and which walls they are on.`,
|
|
1040
|
+
docs: "https://bakery3.com/docs/presets#pictures"
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
const hashes = [];
|
|
1044
|
+
const assets = [];
|
|
1045
|
+
for (const [index, picture] of pictures.entries()) {
|
|
1046
|
+
if (!picture) {
|
|
1047
|
+
hashes.push(null);
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
const frame = frames[index];
|
|
1051
|
+
const { hash, data, contentType, texture } = await loadPicture(picture);
|
|
1052
|
+
coverFit(texture, frame);
|
|
1053
|
+
const material = Array.isArray(frame.material) ? frame.material[0] : frame.material;
|
|
1054
|
+
const replacement = material.clone();
|
|
1055
|
+
replacement.map = texture;
|
|
1056
|
+
replacement.needsUpdate = true;
|
|
1057
|
+
frame.material = replacement;
|
|
1058
|
+
hashes.push(hash);
|
|
1059
|
+
assets.push({ hash, data, contentType, name: `picture-${index}` });
|
|
1060
|
+
}
|
|
1061
|
+
return { hashes, assets };
|
|
1062
|
+
}
|
|
1063
|
+
async function loadPicture(picture) {
|
|
1064
|
+
let blob;
|
|
1065
|
+
if (typeof picture === "string") {
|
|
1066
|
+
const response = await fetch(picture);
|
|
1067
|
+
if (!response.ok) {
|
|
1068
|
+
throw new Bakery3Error({
|
|
1069
|
+
code: "InvalidRequest",
|
|
1070
|
+
message: `The picture at ${picture} could not be loaded (${response.status} ${response.statusText}).`,
|
|
1071
|
+
why: "The page fetches it, so it has to be reachable from the browser and CORS-readable.",
|
|
1072
|
+
fix: "Serve it from your own origin, or with an Access-Control-Allow-Origin header.",
|
|
1073
|
+
docs: "https://bakery3.com/docs/presets#pictures"
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
blob = await response.blob();
|
|
1077
|
+
} else {
|
|
1078
|
+
blob = picture;
|
|
1079
|
+
}
|
|
1080
|
+
const data = await blob.arrayBuffer();
|
|
1081
|
+
const hash = await sha256(data);
|
|
1082
|
+
const bitmap = await createImageBitmap(
|
|
1083
|
+
new Blob([data], { type: blob.type || "image/jpeg" })
|
|
1084
|
+
);
|
|
1085
|
+
const texture = new Texture(bitmap);
|
|
1086
|
+
texture.flipY = false;
|
|
1087
|
+
texture.colorSpace = SRGBColorSpace;
|
|
1088
|
+
texture.needsUpdate = true;
|
|
1089
|
+
return { hash, data, contentType: blob.type || "image/jpeg", texture };
|
|
1090
|
+
}
|
|
1091
|
+
function coverFit(texture, frame) {
|
|
1092
|
+
const image = texture.image;
|
|
1093
|
+
if (!image?.width || !image?.height) return;
|
|
1094
|
+
if (!frame.geometry.boundingBox) frame.geometry.computeBoundingBox();
|
|
1095
|
+
const box = frame.geometry.boundingBox;
|
|
1096
|
+
if (!box) return;
|
|
1097
|
+
const height = box.max.y - box.min.y;
|
|
1098
|
+
const width = Math.max(box.max.x - box.min.x, box.max.z - box.min.z);
|
|
1099
|
+
if (!(height > 0) || !(width > 0)) return;
|
|
1100
|
+
const opening = width / height;
|
|
1101
|
+
const picture = image.width / image.height;
|
|
1102
|
+
if (picture > opening) texture.repeat.set(opening / picture, 1);
|
|
1103
|
+
else texture.repeat.set(1, picture / opening);
|
|
1104
|
+
texture.offset.set((1 - texture.repeat.x) / 2, (1 - texture.repeat.y) / 2);
|
|
1105
|
+
}
|
|
1106
|
+
var SHELF_MATERIAL = "oak_veneer_05";
|
|
1107
|
+
function strippedPreset(mesh) {
|
|
1108
|
+
const name = mesh.userData?.bakery3Material;
|
|
1109
|
+
if (typeof name !== "string" || !name) return null;
|
|
1110
|
+
const color = mesh.userData?.bakery3MaterialColor;
|
|
1111
|
+
return {
|
|
1112
|
+
material: name,
|
|
1113
|
+
...typeof color === "string" && color && color.toLowerCase() !== "#ffffff" ? { color } : {}
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
function coerceDefaultMaterial(mesh) {
|
|
1117
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
1118
|
+
for (const material of materials) {
|
|
1119
|
+
const standard = material;
|
|
1120
|
+
if (!standard.isMeshStandardMaterial) continue;
|
|
1121
|
+
const bare = standard.metalness === 1 && standard.roughness === 1 && !standard.map && standard.color.getHex() === 16777215;
|
|
1122
|
+
if (!bare) continue;
|
|
1123
|
+
standard.metalness = 0;
|
|
1124
|
+
standard.roughness = 0.6;
|
|
1125
|
+
standard.color.set("#b9b2a6");
|
|
1126
|
+
warnBareMaterial(mesh.name);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
var warnedBare = /* @__PURE__ */ new Set();
|
|
1130
|
+
function warnBareMaterial(name) {
|
|
1131
|
+
if (warnedBare.has(name)) return;
|
|
1132
|
+
warnedBare.add(name);
|
|
1133
|
+
console.warn(
|
|
1134
|
+
`[bakery3] "${name}" arrived with no material and has been given a plain one. Republish the room so it carries the preset material it was built with.`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
function createPresetRoom(name, options = {}) {
|
|
1138
|
+
const info = ROOM_PRESETS[name];
|
|
1139
|
+
if (!info) {
|
|
1140
|
+
throw new Error(
|
|
1141
|
+
`'${String(name)}' is not a room preset. Known rooms: ${Object.keys(ROOM_PRESETS).join(", ") || "none"}.`
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
const lighting = options.lighting ?? info.lighting;
|
|
1145
|
+
const materialOptions = {
|
|
1146
|
+
...options.baseUrl ? { baseUrl: options.baseUrl } : {},
|
|
1147
|
+
...options.materialOptions
|
|
1148
|
+
};
|
|
1149
|
+
const standType = options.pedestal === void 0 ? info.stand.type : options.pedestal;
|
|
1150
|
+
if (options.pedestal && !(options.pedestal in PEDESTAL_PRESETS)) {
|
|
1151
|
+
throw new Bakery3Error({
|
|
1152
|
+
code: "InvalidRequest",
|
|
1153
|
+
message: `"${String(options.pedestal)}" is not a pedestal.`,
|
|
1154
|
+
fix: `Use one of: ${Object.keys(PEDESTAL_PRESETS).join(", ")}, or null for the floor.`,
|
|
1155
|
+
docs: "https://bakery3.com/docs/presets"
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
const standInfo = standType ? PEDESTAL_PRESETS[standType] : void 0;
|
|
1159
|
+
const standBaked = Boolean(standInfo);
|
|
1160
|
+
const standMaterial = !standType || !standBaked ? null : options.pedestalMaterial ?? (options.pedestal === void 0 ? info.stand.material : standInfo?.defaultMaterial ?? null);
|
|
1161
|
+
const topY = !standType ? 0 : options.pedestal === void 0 ? info.stand.top : standInfo.size.y;
|
|
1162
|
+
const published = info.hdri?.[lighting] ?? null;
|
|
1163
|
+
const hdriName = options.hdri === false ? null : options.hdri ?? (published && published.name in HDRI_PRESETS ? published.name : null);
|
|
1164
|
+
const hdriChoice = hdriName ? {
|
|
1165
|
+
name: hdriName,
|
|
1166
|
+
// Turned so its sun sits at the room's, as the render will turn
|
|
1167
|
+
// it: the published map's turn is the room's sun minus that
|
|
1168
|
+
// map's sun, so another map's turn is the same sum with its own.
|
|
1169
|
+
rotationDeg: options.hdri ? hdriTurn(hdriName, published, lighting) : published.rotationDeg,
|
|
1170
|
+
groundHeight: HDRI_PRESETS[hdriName].groundHeight,
|
|
1171
|
+
// Shown at the room's own exposure: what this mode's sky delivers
|
|
1172
|
+
// over what the map's does. The render exposes its dome by the same
|
|
1173
|
+
// ratio.
|
|
1174
|
+
exposure: hdriExposure(
|
|
1175
|
+
info.skyIrradiance[lighting],
|
|
1176
|
+
HDRI_PRESETS[hdriName].skyIrradiance
|
|
1177
|
+
)
|
|
1178
|
+
} : null;
|
|
1179
|
+
const state = {
|
|
1180
|
+
name,
|
|
1181
|
+
build: info.build,
|
|
1182
|
+
lighting,
|
|
1183
|
+
...options.bake ? { standIn: true } : {},
|
|
1184
|
+
...options.hdri === false ? { hdri: null } : options.hdri ? { hdri: options.hdri } : {},
|
|
1185
|
+
...options.floor || options.walls || options.ceiling || options.sill ? {
|
|
1186
|
+
surfaces: {
|
|
1187
|
+
...options.floor ? { floor: surface(options.floor) } : {},
|
|
1188
|
+
...options.walls ? { walls: surface(options.walls) } : {},
|
|
1189
|
+
...options.ceiling ? { ceiling: surface(options.ceiling) } : {},
|
|
1190
|
+
...options.sill ? { sill: { material: options.sill } } : {}
|
|
1191
|
+
}
|
|
1192
|
+
} : {},
|
|
1193
|
+
...options.pedestal === void 0 ? {} : options.pedestal === null ? { pedestal: null } : {
|
|
1194
|
+
pedestal: {
|
|
1195
|
+
type: options.pedestal,
|
|
1196
|
+
...standMaterial ? { material: standMaterial } : {}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
const room = new Group();
|
|
1201
|
+
room.name = `bakery3-room-${name}`;
|
|
1202
|
+
room.userData.bakery3Preset = {
|
|
1203
|
+
roomPreset: state,
|
|
1204
|
+
// For the capture's compatibility note, so it never opens the catalog.
|
|
1205
|
+
roomFacts: {
|
|
1206
|
+
label: info.label,
|
|
1207
|
+
triangles: info.preview.triangles,
|
|
1208
|
+
...info.complexity ? { objects: info.complexity.objects } : {}
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
room.userData.topY = topY;
|
|
1212
|
+
room.userData.stand = new Vector3(info.stand.x, topY, info.stand.z);
|
|
1213
|
+
room.userData.exposure = info.exposure[lighting];
|
|
1214
|
+
const floor = surface(options.floor) ?? info.surfaces.floor;
|
|
1215
|
+
const walls = surface(options.walls) ?? info.surfaces.walls;
|
|
1216
|
+
const ceiling = surface(options.ceiling) ?? info.surfaces.ceiling;
|
|
1217
|
+
const sill = options.sill ?? info.surfaces.sill.material;
|
|
1218
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1219
|
+
const preset = (material, color) => {
|
|
1220
|
+
const key = `${material}|${color ?? ""}`;
|
|
1221
|
+
let made = cache.get(key);
|
|
1222
|
+
if (!made) {
|
|
1223
|
+
if (!(material in MATERIAL_PRESETS)) {
|
|
1224
|
+
throw new Error(`'${material}' is not a preset material.`);
|
|
1225
|
+
}
|
|
1226
|
+
made = createPresetMaterial(
|
|
1227
|
+
material,
|
|
1228
|
+
materialOptions
|
|
1229
|
+
);
|
|
1230
|
+
made.shadowSide = DoubleSide;
|
|
1231
|
+
if (color && color.toLowerCase() !== "#ffffff") made.color.set(color);
|
|
1232
|
+
cache.set(key, made);
|
|
1233
|
+
}
|
|
1234
|
+
return made;
|
|
1235
|
+
};
|
|
1236
|
+
const plain = (color, roughness = 0.5) => new MeshStandardMaterial({ color, roughness });
|
|
1237
|
+
const makeDome = () => hdriChoice ? loadPresetHdr(hdriChoice.name, options.baseUrl ?? presetBaseUrl()).then(
|
|
1238
|
+
(map) => {
|
|
1239
|
+
const dome = new GroundedSkybox(
|
|
1240
|
+
map,
|
|
1241
|
+
hdriChoice.groundHeight,
|
|
1242
|
+
DOME_RADIUS
|
|
1243
|
+
);
|
|
1244
|
+
dome.name = "hdri.dome";
|
|
1245
|
+
dome.position.y = hdriChoice.groundHeight - GROUND_DROP;
|
|
1246
|
+
dome.rotation.y = -MathUtils.degToRad(hdriChoice.rotationDeg);
|
|
1247
|
+
dome.castShadow = false;
|
|
1248
|
+
dome.receiveShadow = false;
|
|
1249
|
+
dome.userData.bakery3Dome = {
|
|
1250
|
+
name: hdriChoice.name,
|
|
1251
|
+
exposure: hdriChoice.exposure,
|
|
1252
|
+
rotationDeg: hdriChoice.rotationDeg,
|
|
1253
|
+
groundHeight: hdriChoice.groundHeight,
|
|
1254
|
+
radius: DOME_RADIUS
|
|
1255
|
+
};
|
|
1256
|
+
dome.userData.bakery3 = { exclude: true };
|
|
1257
|
+
checkDomeReachOnce(dome);
|
|
1258
|
+
return dome;
|
|
1259
|
+
}
|
|
1260
|
+
) : Promise.resolve(null);
|
|
1261
|
+
const buildStandIn = () => Promise.all([
|
|
1262
|
+
loadRoomGlb(presetRoomUrl(name, options.baseUrl)),
|
|
1263
|
+
makeDome()
|
|
1264
|
+
]).then(async ([glb, dome]) => {
|
|
1265
|
+
if (dome) room.add(dome);
|
|
1266
|
+
const instance = glb.clone();
|
|
1267
|
+
const look = MODE_LOOK[lighting];
|
|
1268
|
+
const glass = new MeshPhysicalMaterial({
|
|
1269
|
+
transmission: 1,
|
|
1270
|
+
roughness: 0.05,
|
|
1271
|
+
ior: 1.45,
|
|
1272
|
+
thickness: 0.01
|
|
1273
|
+
});
|
|
1274
|
+
let glowColor = null;
|
|
1275
|
+
instance.traverse((node) => {
|
|
1276
|
+
const lamp = node;
|
|
1277
|
+
if (!glowColor && (lamp.isPointLight || node.isSpotLight) && !/^accent\./.test(node.name)) {
|
|
1278
|
+
glowColor = lamp.color.clone();
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1281
|
+
const glow = new MeshStandardMaterial({
|
|
1282
|
+
color: 0,
|
|
1283
|
+
emissive: glowColor ?? new Color("#ffd9a8"),
|
|
1284
|
+
emissiveIntensity: FIXTURE_GLOW_STRENGTH,
|
|
1285
|
+
roughness: 1
|
|
1286
|
+
});
|
|
1287
|
+
instance.traverse((node) => {
|
|
1288
|
+
const mesh = node;
|
|
1289
|
+
if (mesh.isMesh) {
|
|
1290
|
+
mesh.geometry = options.bake ? plainFloatGeometry(mesh.geometry.clone()) : mesh.geometry.clone();
|
|
1291
|
+
mesh.castShadow = true;
|
|
1292
|
+
mesh.receiveShadow = true;
|
|
1293
|
+
const n = plainName(mesh);
|
|
1294
|
+
if (n.startsWith("wall"))
|
|
1295
|
+
mesh.material = preset(walls.material, walls.color);
|
|
1296
|
+
else if (n.startsWith("floor"))
|
|
1297
|
+
mesh.material = preset(floor.material, floor.color);
|
|
1298
|
+
else if (n.startsWith("ceiling"))
|
|
1299
|
+
mesh.material = preset(ceiling.material, ceiling.color);
|
|
1300
|
+
else if (n.startsWith("sill")) mesh.material = preset(sill);
|
|
1301
|
+
else if (n.startsWith("glass") || n.startsWith("skylightglass")) {
|
|
1302
|
+
mesh.material = glass;
|
|
1303
|
+
mesh.castShadow = false;
|
|
1304
|
+
mesh.userData.bakery3 = {
|
|
1305
|
+
...mesh.userData.bakery3 ?? {},
|
|
1306
|
+
exclude: true
|
|
1307
|
+
};
|
|
1308
|
+
} else if (n.startsWith("fixtureglow")) {
|
|
1309
|
+
mesh.material = glow;
|
|
1310
|
+
mesh.userData.bakery3 = {
|
|
1311
|
+
...mesh.userData.bakery3 ?? {},
|
|
1312
|
+
bake: false
|
|
1313
|
+
};
|
|
1314
|
+
} else if (n.startsWith("covestrip")) {
|
|
1315
|
+
mesh.material = glow;
|
|
1316
|
+
if (n.startsWith("covestripdetail")) {
|
|
1317
|
+
const both = glow.clone();
|
|
1318
|
+
both.side = DoubleSide;
|
|
1319
|
+
mesh.material = both;
|
|
1320
|
+
}
|
|
1321
|
+
mesh.userData.bakery3 = {
|
|
1322
|
+
...mesh.userData.bakery3 ?? {},
|
|
1323
|
+
exclude: true
|
|
1324
|
+
};
|
|
1325
|
+
} else if (n.startsWith("fixturebezel"))
|
|
1326
|
+
mesh.material = plain("#1a1a1c", 0.4);
|
|
1327
|
+
else if (n.startsWith("frame") || n.startsWith("baseboard") || n.startsWith("skylightcurb") || n.startsWith("coveledge")) {
|
|
1328
|
+
mesh.material = plain("#eeeae2", 0.35);
|
|
1329
|
+
} else if (n.startsWith("shelf") || n.startsWith("bookcase")) {
|
|
1330
|
+
const wore = strippedPreset(mesh);
|
|
1331
|
+
mesh.material = preset(
|
|
1332
|
+
wore?.material ?? SHELF_MATERIAL,
|
|
1333
|
+
wore?.color
|
|
1334
|
+
);
|
|
1335
|
+
} else if (n.startsWith("rug")) {
|
|
1336
|
+
mesh.material = new MeshPhysicalMaterial({
|
|
1337
|
+
color: "#8a7a68",
|
|
1338
|
+
roughness: 0.95,
|
|
1339
|
+
sheen: 1,
|
|
1340
|
+
sheenRoughness: 0.4
|
|
1341
|
+
});
|
|
1342
|
+
} else if (n.startsWith("pedestal")) {
|
|
1343
|
+
mesh.material = info.stand.material ? preset(info.stand.material) : plain("#d9d6d0");
|
|
1344
|
+
} else {
|
|
1345
|
+
const wore = strippedPreset(mesh);
|
|
1346
|
+
if (wore) mesh.material = preset(wore.material, wore.color);
|
|
1347
|
+
else {
|
|
1348
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
1349
|
+
const cloned = materials.map((m) => m.clone());
|
|
1350
|
+
mesh.material = Array.isArray(mesh.material) ? cloned : cloned[0];
|
|
1351
|
+
coerceDefaultMaterial(mesh);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
const standGroup = instance.getObjectByName("stand");
|
|
1357
|
+
if (standGroup && options.pedestal !== void 0) {
|
|
1358
|
+
for (const child of [...standGroup.children]) standGroup.remove(child);
|
|
1359
|
+
standGroup.position.set(0, 0, 0);
|
|
1360
|
+
standGroup.rotation.set(0, 0, 0);
|
|
1361
|
+
if (standType) {
|
|
1362
|
+
const object = createPedestal({
|
|
1363
|
+
type: standType,
|
|
1364
|
+
...standMaterial ? { material: standMaterial } : {},
|
|
1365
|
+
...options.baseUrl ? { baseUrl: options.baseUrl } : {},
|
|
1366
|
+
materialOptions
|
|
1367
|
+
});
|
|
1368
|
+
object.rotation.y = MathUtils.degToRad(info.stand.rotationDeg);
|
|
1369
|
+
object.position.set(info.stand.x, 0, info.stand.z);
|
|
1370
|
+
standGroup.add(object);
|
|
1371
|
+
}
|
|
1372
|
+
} else if (standGroup && standMaterial) {
|
|
1373
|
+
standGroup.traverse((node) => {
|
|
1374
|
+
if (node.isMesh)
|
|
1375
|
+
node.material = preset(standMaterial);
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
const brightness = info.previewBrightness[lighting];
|
|
1379
|
+
if (dome && hdriChoice) {
|
|
1380
|
+
dome.material.color.setScalar(
|
|
1381
|
+
hdriChoice.exposure * look.sun * brightness
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
const lights = [];
|
|
1385
|
+
instance.traverse((node) => {
|
|
1386
|
+
if (node.isLight) lights.push(node);
|
|
1387
|
+
});
|
|
1388
|
+
for (const light of lights)
|
|
1389
|
+
light.userData.bakery3 = {
|
|
1390
|
+
...light.userData.bakery3 ?? {},
|
|
1391
|
+
exclude: true
|
|
1392
|
+
};
|
|
1393
|
+
if (options.lights === false) {
|
|
1394
|
+
for (const light of lights) light.parent?.remove(light);
|
|
1395
|
+
} else {
|
|
1396
|
+
const reach = Math.max(info.size.width, info.size.depth);
|
|
1397
|
+
for (const light of lights) {
|
|
1398
|
+
if (light.isDirectionalLight) {
|
|
1399
|
+
const sun = light;
|
|
1400
|
+
sun.visible = lighting === "day" || lighting === "golden";
|
|
1401
|
+
sun.intensity = 3.5 * look.sun * brightness;
|
|
1402
|
+
sun.castShadow = true;
|
|
1403
|
+
sun.shadow.mapSize.set(2048, 2048);
|
|
1404
|
+
sun.shadow.camera.left = -reach;
|
|
1405
|
+
sun.shadow.camera.right = reach;
|
|
1406
|
+
sun.shadow.camera.top = reach;
|
|
1407
|
+
sun.shadow.camera.bottom = -reach;
|
|
1408
|
+
sun.shadow.camera.far = 60;
|
|
1409
|
+
sun.shadow.bias = -2e-4;
|
|
1410
|
+
sun.shadow.normalBias = 0.02;
|
|
1411
|
+
if (sun.target && !sun.target.parent) instance.add(sun.target);
|
|
1412
|
+
} else if (/^accent\./.test(light.name) && (light.isPointLight || light.isSpotLight)) {
|
|
1413
|
+
const lamp = light;
|
|
1414
|
+
const beam = ACCENT_BEAM[light.name.split(".")[1]] ?? ACCENT_BEAM.narrow;
|
|
1415
|
+
const watts = lamp.intensity * 4 * Math.PI / 683;
|
|
1416
|
+
const spot = new SpotLight(
|
|
1417
|
+
lamp.color,
|
|
1418
|
+
watts * IES_PEAK_PER_WATT * look.sun * brightness
|
|
1419
|
+
);
|
|
1420
|
+
spot.name = light.name;
|
|
1421
|
+
spot.angle = MathUtils.degToRad(beam.angleDeg);
|
|
1422
|
+
spot.penumbra = beam.penumbra;
|
|
1423
|
+
spot.decay = 2;
|
|
1424
|
+
spot.distance = 0;
|
|
1425
|
+
spot.castShadow = true;
|
|
1426
|
+
spot.shadow.mapSize.set(1024, 1024);
|
|
1427
|
+
spot.shadow.bias = -2e-4;
|
|
1428
|
+
spot.shadow.normalBias = 0.02;
|
|
1429
|
+
spot.position.copy(lamp.position);
|
|
1430
|
+
spot.target.position.copy(lamp.position).add(new Vector3(0, 0, -2).applyQuaternion(lamp.quaternion));
|
|
1431
|
+
spot.userData.bakery3 = { exclude: true };
|
|
1432
|
+
spot.target.userData.bakery3 = { exclude: true };
|
|
1433
|
+
const parent = lamp.parent ?? instance;
|
|
1434
|
+
parent.remove(lamp);
|
|
1435
|
+
parent.add(spot, spot.target);
|
|
1436
|
+
} else if (light.isPointLight || light.isSpotLight) {
|
|
1437
|
+
const lamp = light;
|
|
1438
|
+
const watts = lamp.intensity / CANDELA_PER_WATT / DOWNLIGHT_SCALE;
|
|
1439
|
+
lamp.intensity = look.point * Math.min(1, watts / CAN_FULL_WATTS) * brightness;
|
|
1440
|
+
lamp.decay = 2;
|
|
1441
|
+
lamp.distance = 0;
|
|
1442
|
+
lamp.castShadow = false;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
const colors = hemisphereFill(look, info.previewFill?.[lighting]);
|
|
1446
|
+
const fill = new HemisphereLight(
|
|
1447
|
+
colors.sky,
|
|
1448
|
+
colors.ground,
|
|
1449
|
+
look.hemi * brightness * colors.intensity
|
|
1450
|
+
);
|
|
1451
|
+
fill.name = "fill";
|
|
1452
|
+
fill.userData.bakery3 = { exclude: true };
|
|
1453
|
+
instance.add(fill);
|
|
1454
|
+
}
|
|
1455
|
+
for (const camera of instance.children.filter(
|
|
1456
|
+
(c) => c.isCamera
|
|
1457
|
+
)) {
|
|
1458
|
+
instance.remove(camera);
|
|
1459
|
+
}
|
|
1460
|
+
room.add(instance);
|
|
1461
|
+
if (options.pictures?.length) {
|
|
1462
|
+
const hung = await hangPictures(instance, options.pictures, name);
|
|
1463
|
+
if (hung.hashes.some(Boolean)) {
|
|
1464
|
+
state.pictures = hung.hashes;
|
|
1465
|
+
room.userData.bakery3Preset = {
|
|
1466
|
+
...room.userData.bakery3Preset,
|
|
1467
|
+
roomPreset: state,
|
|
1468
|
+
pictureAssets: hung.assets
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (options.simplify !== false) stampRoomStandIns(instance, info.plants);
|
|
1473
|
+
stampRoomIds(room, name);
|
|
1474
|
+
room.userData.shown = "standIn";
|
|
1475
|
+
return room;
|
|
1476
|
+
});
|
|
1477
|
+
const buildBaked = async () => {
|
|
1478
|
+
const base = options.baseUrl ?? presetBaseUrl();
|
|
1479
|
+
const url = bakedRoomUrl(name, lighting, base, info.presetsVersion);
|
|
1480
|
+
const response = await fetch(`${url}room.json`);
|
|
1481
|
+
if (!response.ok) throw new Error(`${url}room.json: ${response.status}`);
|
|
1482
|
+
const manifest = await response.json();
|
|
1483
|
+
const [source, probe, standSource] = await Promise.all([
|
|
1484
|
+
loadBakedGlb(`${url}${manifest.glb}`, manifest.exposureBakedIn),
|
|
1485
|
+
loadRadianceHdr(`${url}${manifest.probe.uri}`),
|
|
1486
|
+
info.standGlb ? loadRoomGlb(
|
|
1487
|
+
`${base.replace(/\/$/, "")}/v${info.presetsVersion}/${info.standGlb}`
|
|
1488
|
+
) : Promise.resolve(null)
|
|
1489
|
+
]);
|
|
1490
|
+
const baked = source.clone();
|
|
1491
|
+
baked.name = "baked";
|
|
1492
|
+
const stand = standSource?.clone() ?? null;
|
|
1493
|
+
const catchers = [];
|
|
1494
|
+
const doomed = [];
|
|
1495
|
+
let key = null;
|
|
1496
|
+
baked.traverse((node) => {
|
|
1497
|
+
node.userData.bakery3 = {
|
|
1498
|
+
...node.userData.bakery3 ?? {},
|
|
1499
|
+
exclude: true
|
|
1500
|
+
};
|
|
1501
|
+
node.userData.bakery3RoomShell = true;
|
|
1502
|
+
const light = node;
|
|
1503
|
+
if (light.isLight) key = light;
|
|
1504
|
+
const mesh = node;
|
|
1505
|
+
if (!mesh.isMesh) return;
|
|
1506
|
+
mesh.castShadow = false;
|
|
1507
|
+
mesh.receiveShadow = false;
|
|
1508
|
+
const role = bakedRole(mesh);
|
|
1509
|
+
if (role === "stand" && stand) doomed.push(mesh);
|
|
1510
|
+
else if (role === "floor") catchers.push(mesh);
|
|
1511
|
+
});
|
|
1512
|
+
for (const mesh of doomed) mesh.parent?.remove(mesh);
|
|
1513
|
+
room.add(baked);
|
|
1514
|
+
if (stand) {
|
|
1515
|
+
stand.traverse((node) => {
|
|
1516
|
+
const mesh = node;
|
|
1517
|
+
if (!mesh.isMesh) return;
|
|
1518
|
+
mesh.geometry = plainFloatGeometry(mesh.geometry.clone());
|
|
1519
|
+
mesh.castShadow = true;
|
|
1520
|
+
mesh.receiveShadow = true;
|
|
1521
|
+
if (plainName(mesh).startsWith("pedestal")) {
|
|
1522
|
+
mesh.material = info.stand.material ? preset(info.stand.material) : plain("#d9d6d0");
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
const wore = strippedPreset(mesh);
|
|
1526
|
+
if (wore) mesh.material = preset(wore.material, wore.color);
|
|
1527
|
+
else {
|
|
1528
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
1529
|
+
const cloned = materials.map((m) => m.clone());
|
|
1530
|
+
mesh.material = Array.isArray(mesh.material) ? cloned : cloned[0];
|
|
1531
|
+
coerceDefaultMaterial(mesh);
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
for (const camera of stand.children.filter(
|
|
1535
|
+
(c) => c.isCamera
|
|
1536
|
+
))
|
|
1537
|
+
stand.remove(camera);
|
|
1538
|
+
room.add(stand);
|
|
1539
|
+
}
|
|
1540
|
+
let lamp = key;
|
|
1541
|
+
if (!lamp) {
|
|
1542
|
+
const overhead = new DirectionalLight(16777215, 1e-4);
|
|
1543
|
+
overhead.name = "contact";
|
|
1544
|
+
overhead.position.set(info.stand.x + 0.4, topY + 6, info.stand.z + 0.3);
|
|
1545
|
+
overhead.target.position.set(info.stand.x, topY, info.stand.z);
|
|
1546
|
+
overhead.userData.bakery3 = { exclude: true };
|
|
1547
|
+
overhead.target.userData.bakery3 = { exclude: true };
|
|
1548
|
+
overhead.shadow.radius = 6;
|
|
1549
|
+
baked.add(overhead, overhead.target);
|
|
1550
|
+
lamp = overhead;
|
|
1551
|
+
}
|
|
1552
|
+
if (lamp) {
|
|
1553
|
+
lamp.castShadow = true;
|
|
1554
|
+
lamp.shadow.mapSize.set(1024, 1024);
|
|
1555
|
+
lamp.shadow.bias = -2e-4;
|
|
1556
|
+
lamp.shadow.normalBias = 0.02;
|
|
1557
|
+
if (lamp.isDirectionalLight) {
|
|
1558
|
+
const camera = lamp.shadow.camera;
|
|
1559
|
+
camera.left = camera.bottom = -BAKED_SHADOW_REACH;
|
|
1560
|
+
camera.right = camera.top = BAKED_SHADOW_REACH;
|
|
1561
|
+
camera.far = 20;
|
|
1562
|
+
}
|
|
1563
|
+
if (lamp.target && !lamp.target.parent) baked.add(lamp.target);
|
|
1564
|
+
const shadow = new ShadowMaterial({
|
|
1565
|
+
opacity: 0.45,
|
|
1566
|
+
depthWrite: false,
|
|
1567
|
+
polygonOffset: true,
|
|
1568
|
+
polygonOffsetFactor: -2,
|
|
1569
|
+
polygonOffsetUnits: -2
|
|
1570
|
+
});
|
|
1571
|
+
for (const mesh of catchers) {
|
|
1572
|
+
const catcher = new Mesh(mesh.geometry, shadow);
|
|
1573
|
+
catcher.name = `${mesh.name}.shadow`;
|
|
1574
|
+
catcher.receiveShadow = true;
|
|
1575
|
+
catcher.renderOrder = 1;
|
|
1576
|
+
catcher.userData.bakery3 = { exclude: true };
|
|
1577
|
+
mesh.add(catcher);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
probe.mapping = EquirectangularReflectionMapping;
|
|
1581
|
+
scaleHalfFloats(
|
|
1582
|
+
probe.image.data,
|
|
1583
|
+
1 / manifest.exposureBakedIn
|
|
1584
|
+
);
|
|
1585
|
+
probe.needsUpdate = true;
|
|
1586
|
+
const ambient = new LightProbe(
|
|
1587
|
+
irradianceOf(
|
|
1588
|
+
probe.image.data,
|
|
1589
|
+
probe.image.width,
|
|
1590
|
+
probe.image.height
|
|
1591
|
+
)
|
|
1592
|
+
);
|
|
1593
|
+
ambient.name = "fill";
|
|
1594
|
+
ambient.userData.bakery3 = { exclude: true };
|
|
1595
|
+
room.add(ambient);
|
|
1596
|
+
room.userData.environment = probe;
|
|
1597
|
+
room.userData.bakedManifest = manifest;
|
|
1598
|
+
stampRoomIds(room, name);
|
|
1599
|
+
room.userData.bakery3Preset = {
|
|
1600
|
+
...room.userData.bakery3Preset,
|
|
1601
|
+
shown: "baked"
|
|
1602
|
+
};
|
|
1603
|
+
room.userData.shown = "baked";
|
|
1604
|
+
return room;
|
|
1605
|
+
};
|
|
1606
|
+
const asPublished = options.floor === void 0 && options.walls === void 0 && options.ceiling === void 0 && options.sill === void 0 && options.pedestal === void 0 && options.pedestalMaterial === void 0 && options.hdri === void 0 && !options.pictures?.some(Boolean) && !options.bake && options.lights !== false;
|
|
1607
|
+
const wantsBaked = options.baked !== false && asPublished && info.baked.includes(lighting);
|
|
1608
|
+
room.userData.ready = wantsBaked ? buildBaked().catch((error) => {
|
|
1609
|
+
if (typeof console !== "undefined") {
|
|
1610
|
+
console.warn(
|
|
1611
|
+
`[bakery3] The baked version of ${name} (${lighting}) could not be loaded, so the room is shown as its stand-in: ${error instanceof Error ? error.message : String(error)}`
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
for (const child of [...room.children]) room.remove(child);
|
|
1615
|
+
return buildStandIn();
|
|
1616
|
+
}) : buildStandIn();
|
|
1617
|
+
room.userData.prebaked = options.prebaked === false || options.pedestalMaterial ? Promise.resolve(null) : room.userData.ready.then(
|
|
1618
|
+
() => (
|
|
1619
|
+
// A baked room carries its light already.
|
|
1620
|
+
room.userData.shown === "baked" ? null : applyRoomPrebake(
|
|
1621
|
+
room,
|
|
1622
|
+
state,
|
|
1623
|
+
options.baseUrl ?? presetBaseUrl(),
|
|
1624
|
+
info.presetsVersion
|
|
1625
|
+
)
|
|
1626
|
+
),
|
|
1627
|
+
() => null
|
|
1628
|
+
);
|
|
1629
|
+
return room;
|
|
1630
|
+
}
|
|
1631
|
+
function hdriExposure(roomSky, mapSky) {
|
|
1632
|
+
if (!(mapSky > 0) || !(roomSky > 0)) return 1;
|
|
1633
|
+
return Math.round(roomSky / mapSky * 1e3) / 1e3;
|
|
1634
|
+
}
|
|
1635
|
+
function checkDomeReachOnce(dome) {
|
|
1636
|
+
dome.onBeforeRender = (_renderer, _scene, camera) => {
|
|
1637
|
+
delete dome.onBeforeRender;
|
|
1638
|
+
const far = camera.far;
|
|
1639
|
+
if (typeof far !== "number") return;
|
|
1640
|
+
const reach = DOME_RADIUS * dome.matrixWorld.getMaxScaleOnAxis() + camera.getWorldPosition(new Vector3()).distanceTo(dome.getWorldPosition(new Vector3()));
|
|
1641
|
+
if (far >= reach || typeof console === "undefined") return;
|
|
1642
|
+
console.warn(
|
|
1643
|
+
[
|
|
1644
|
+
`[bakery3] The map outside this preset room hangs on a dome ${Math.round(reach)} m from the camera, but camera.far is ${far}:`,
|
|
1645
|
+
"the sky through the windows is clipped to black. Set camera.far to 100 or more (then",
|
|
1646
|
+
"camera.updateProjectionMatrix()), or pass hdri: false to createPresetRoom() for no map outside.",
|
|
1647
|
+
"https://bakery3.com/docs/presets#room-presets"
|
|
1648
|
+
].join("\n")
|
|
1649
|
+
);
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
function hdriTurn(name, published, lighting) {
|
|
1653
|
+
const category = HDRI_PRESETS[name].category;
|
|
1654
|
+
if (category !== "day" && category !== "golden") return 0;
|
|
1655
|
+
if (published && HDRI_PRESETS[published.name]?.category === category)
|
|
1656
|
+
return published.rotationDeg;
|
|
1657
|
+
return lighting === "day" || lighting === "golden" ? published?.rotationDeg ?? 0 : 0;
|
|
1658
|
+
}
|
|
1659
|
+
function plainFloatGeometry(geometry) {
|
|
1660
|
+
for (const name of Object.keys(geometry.attributes)) {
|
|
1661
|
+
const attribute = geometry.getAttribute(name);
|
|
1662
|
+
const plain = attribute.isInterleavedBufferAttribute === void 0 && attribute.array instanceof Float32Array && !attribute.normalized;
|
|
1663
|
+
if (plain) continue;
|
|
1664
|
+
const size = attribute.itemSize;
|
|
1665
|
+
const out = new Float32Array(attribute.count * size);
|
|
1666
|
+
const get = [
|
|
1667
|
+
attribute.getX,
|
|
1668
|
+
attribute.getY,
|
|
1669
|
+
attribute.getZ,
|
|
1670
|
+
attribute.getW
|
|
1671
|
+
];
|
|
1672
|
+
for (let i = 0; i < attribute.count; i++) {
|
|
1673
|
+
for (let c = 0; c < size; c++)
|
|
1674
|
+
out[i * size + c] = get[c].call(attribute, i);
|
|
1675
|
+
}
|
|
1676
|
+
geometry.setAttribute(name, new Float32BufferAttribute(out, size));
|
|
1677
|
+
}
|
|
1678
|
+
return geometry;
|
|
1679
|
+
}
|
|
1680
|
+
function roomLensFovDeg(lensMm, aspect) {
|
|
1681
|
+
const sensorV = aspect >= 1 ? 36 / aspect : 36;
|
|
1682
|
+
return MathUtils.radToDeg(2 * Math.atan(sensorV / (2 * lensMm)));
|
|
1683
|
+
}
|
|
1684
|
+
function applyPresetRoomCamera(camera, room) {
|
|
1685
|
+
const state = roomPresetSpecOf(room);
|
|
1686
|
+
if (!state)
|
|
1687
|
+
throw new Error(
|
|
1688
|
+
"applyPresetRoomCamera needs a group made by createPresetRoom()."
|
|
1689
|
+
);
|
|
1690
|
+
const info = ROOM_PRESETS[state.name];
|
|
1691
|
+
const world = new Matrix4().fromArray(state.matrix);
|
|
1692
|
+
const position = new Vector3(...info.camera.position).applyMatrix4(world);
|
|
1693
|
+
const target = new Vector3(...info.camera.target).applyMatrix4(world);
|
|
1694
|
+
camera.position.copy(position);
|
|
1695
|
+
camera.lookAt(target);
|
|
1696
|
+
camera.fov = roomLensFovDeg(info.camera.lensMm, camera.aspect || 1);
|
|
1697
|
+
camera.updateProjectionMatrix();
|
|
1698
|
+
}
|
|
1699
|
+
function presetRoomBounds(name) {
|
|
1700
|
+
const { width, height, depth } = ROOM_PRESETS[name].size;
|
|
1701
|
+
return new Box3(
|
|
1702
|
+
new Vector3(-width / 2, 0, -depth / 2),
|
|
1703
|
+
new Vector3(width / 2, height, depth / 2)
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
export { applyPresetRoomCamera, coverFit, createPedestal, createPresetMaterial, createPresetRoom, createRoom, createSkyEnvironment, loadPresetEnvironment, loadPresetMaterial, plainFloatGeometry, presetRoomBounds, presetRoomUrl, roomLensFovDeg, stampRoomIds, stampRoomStandIns };
|
|
1708
|
+
//# sourceMappingURL=chunk-HNMSWQU7.js.map
|
|
1709
|
+
//# sourceMappingURL=chunk-HNMSWQU7.js.map
|