v0.4.5: tegelvloer met witte rand i.p.v. gras, Lucht geen/dag/nacht, Render met daglicht + lampen

- core/skybox.js: tegelvloer (tiles.jpg) passend om het object, rand 100x25 mm wit
- nacht-panorama; nacht = 15% omgevingslicht + lampen 100%
- Render-knop met camera-icoon: echte lampen in het gekozen licht
- gras verwijderd (bestanden lokaal bewaard, uit git)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-09-30 12:34:19 +02:00
co-authored by Claude Opus 5.5
parent 9ed9cee2e0
commit 7ae76fe390
12 changed files with 172 additions and 122 deletions
+2
View File
@@ -5,3 +5,5 @@ examples/
# Lokale exports # Lokale exports
*.pdf *.pdf
.DS_Store .DS_Store
# Oude grastextuur (niet meer gebruikt), lokaal bewaard
assets/grass*.jpg
+10 -9
View File
@@ -157,15 +157,16 @@ assets/ logo (preloader) + bestemmingsmap voor toekomstige
Koppeling, Dak). Labels: zelfstandig naamwoord, geen dubbele punt, uitleg Koppeling, Dak). Labels: zelfstandig naamwoord, geen dubbele punt, uitleg
in een hint (i-icoon) i.p.v. tussen haakjes. Sneltoets T wisselt in drie in een hint (i-icoon) i.p.v. tussen haakjes. Sneltoets T wisselt in drie
stappen: standaard -> 3D groot -> alleen de render -> standaard. stappen: standaard -> 3D groot -> alleen de render -> standaard.
- **Lucht en gras (v0.4.3).** Knop "Lucht" in elke 3D-werkbalk (core/skybox.js, - **Omgeving en render (v0.4.5).** Knop "Lucht" in elke 3D-werkbalk wisselt
keuze onthouden). Het panorama (assets/skybox.webp) wordt op de grond geen -> dag -> nacht (core/skybox.js, keuze onthouden). Het panorama
geprojecteerd (port van three.js GroundedSkybox), met scherp gras (assets/skybox.webp / skybox-night.webp) wordt op de grond geprojecteerd
(assets/grass.jpg, v0.4.4: kleurgecorrigeerd naar het gemiddelde grasgroen van het panorama onder de camera, belichting gecompenseerd) in een cirkel van 12 m (port van three.js GroundedSkybox); onder het object ligt een ronde
dat daarna overvloeit in het panorama. De zon werpt schaduw op het gras en tegelvloer (assets/tiles.jpg), groot genoeg voor het hele object, met een
de camera kan niet onder de grond. Woning: het gras ligt op het maaiveld witte opstaande rand van 100 x 25 mm. Vloer en rand vangen licht en
(woning-instelling, standaard 350 mm onder bovenkant vloer), zodat het schaduw. Woning: de tegelvloer ligt op het maaiveld (woning-instelling,
ingegraven deel van vloer en sandwichpanelen eronder verdwijnt. Wand/vloer: standaard 350 mm onder bovenkant vloer). Knop "Render" (camera): lampen en
gras op de onderkant van het element. De volle render blijft een avondscène. spots als echte lichtbronnen in het gewone daglicht; Nacht: omgevingslicht
15%, lampen altijd aan (100%).
- **Mijlpaal v0.4 — gevelopbouw.** Samenvatting van de v0.3.11–0.3.19-iteraties: - **Mijlpaal v0.4 — gevelopbouw.** Samenvatting van de v0.3.11–0.3.19-iteraties:
kruislat-latwerk op vochtdoek (richting gevelbekleding globaal), lampen op de kruislat-latwerk op vochtdoek (richting gevelbekleding globaal), lampen op de
afgewerkte gevel (+52 mm), logo-watermerk op het doek, trasraam (cement- afgewerkte gevel (+52 mm), logo-watermerk op het doek, trasraam (cement-
+3 -2
View File
@@ -1,4 +1,5 @@
Assets voor de 3D-weergave en de PDF: Assets voor de 3D-weergave en de PDF:
- logo.svg / logo-animated.svg logo (splash, about); PDF-logo staat als data in js/core/logo-data.js - logo.svg / logo-animated.svg logo (splash, about); PDF-logo staat als data in js/core/logo-data.js
- skybox.webp equirectangulair panorama (6000x3000) voor "Lucht" (core/skybox.js) - skybox.webp equirectangulair panorama dag (4096x2048), "Lucht: Dag" (core/skybox.js)
- grass.jpg grastextuur, kleurgecorrigeerd naar het panorama-gras (bron: "grass (kopie).jpg", niet in git) - skybox-night.webp equirectangulair panorama nacht (4096x2048), "Lucht: Nacht"
- tiles.jpg tegeltextuur (1024x1024) voor de ronde vloer onder het object
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 MiB

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

+1 -1
View File
@@ -4,7 +4,7 @@
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
* Zie LICENTIE.md. * Zie LICENTIE.md.
*/ */
const APP_VERSION = '0.4.4'; const APP_VERSION = '0.4.5';
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang. // 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
let tabs = []; let tabs = [];
+121 -81
View File
@@ -5,11 +5,12 @@
* Zie LICENTIE.md. * Zie LICENTIE.md.
*/ */
// ============================================================ // ============================================================
// core/skybox.js — omgeving voor de 3D-weergaven ("Lucht"-knop): een // core/skybox.js — omgeving voor de 3D-weergaven ("Lucht": geen/dag/nacht).
// panorama (assets/skybox.webp) dat op de grond geprojecteerd wordt, zodat // Een panorama (assets/skybox.webp of skybox-night.webp) wordt op de grond
// het object echt op het gras staat, plus scherp gras (assets/grass.jpg) in // geprojecteerd, zodat het object op de grond staat; onder het object ligt
// een cirkel rond het object dat naar buiten toe overvloeit in het gras van // een ronde tegelvloer (assets/tiles.jpg) met een witte opstaande rand, groot
// het panorama. Het gras vangt ook de schaduw op. // genoeg voor het hele object. Vloer en rand reageren op het licht en vangen
// de schaduw op. Nacht: omgevingslicht op NIGHT_FACTOR (lampen blijven 100%).
// //
// Grondprojectie: port van three.js GroundedSkybox (r157+, MIT; wij draaien // Grondprojectie: port van three.js GroundedSkybox (r157+, MIT; wij draaien
// r128): een binnenstebuiten bol waarvan de onderhelft plat gedrukt wordt op // r128): een binnenstebuiten bol waarvan de onderhelft plat gedrukt wordt op
@@ -19,33 +20,25 @@
// ============================================================ // ============================================================
window.Skybox = (() => { window.Skybox = (() => {
const PREF_KEY = 'tectum.skybox'; const PREF_KEY = 'tectum.sky';
const SKY_URL = 'assets/skybox.webp'; const MODES = ['none', 'day', 'night'];
const GRASS_URL = 'assets/grass.jpg'; const SKY_URLS = { day: 'assets/skybox.webp', night: 'assets/skybox-night.webp' };
const CAMERA_HEIGHT_M = 1.7; // hoogte van de panoramacamera boven het gras const TILES_URL = 'assets/tiles.jpg';
const CAMERA_HEIGHT_M = 1.7; // hoogte van de panoramacamera boven de grond
const RADIUS_M = 60; // straal van de omgevingsbol const RADIUS_M = 60; // straal van de omgevingsbol
const GRASS_RADIUS_M = 12; // tot hier is het eigen gras volledig zichtbaar const TILE_M = 2.4; // één tegeltextuur (1024 px) beslaat zoveel meter
const GRASS_FADE_M = 6; // daarna vloeit het over in het panorama const MARGIN_M = 1.5; // vrije rand tussen object en opstaande rand
const GRASS_TILE_M = 2.0; // breedte van één grastegel (hoogte volgt de beeldverhouding) const RIM_HEIGHT_M = 0.10, RIM_THICK_M = 0.025;
// assets/grass.jpg is kleurgecorrigeerd naar het gemiddelde grasgroen van het const NIGHT_FACTOR = 0.15; // omgevingslicht 's nachts
// panorama recht onder de camera (bron: "grass (kopie).jpg"). Het panorama
// is onbelicht; het gras wordt belicht door ambient (0.6) + zon (0.8, vanuit
// SUN_OFFSET). Deze factor maakt belicht gras gelijk aan de textuurkleur.
const SUN_OFFSET = { x: 8, y: 14, z: 6 }; const SUN_OFFSET = { x: 8, y: 14, z: 6 };
const GRASS_SHADE = 1 / (0.6 + 0.8 * SUN_OFFSET.y / Math.hypot(SUN_OFFSET.x, SUN_OFFSET.y, SUN_OFFSET.z));
let loading = null; const cache = {};
function loadTextures() { function loadTexture(url) {
if (loading) return loading; if (!cache[url]) {
const loader = new THREE.TextureLoader(); cache[url] = new Promise((resolve, reject) => new THREE.TextureLoader().load(url, resolve, undefined, reject));
const load = (url) => new Promise((resolve, reject) => loader.load(url, resolve, undefined, reject)); cache[url].catch(() => { delete cache[url]; });
loading = Promise.all([load(SKY_URL), load(GRASS_URL)]).then(([sky, grass]) => { }
grass.wrapS = grass.wrapT = THREE.RepeatWrapping; return cache[url];
grass.anisotropy = 8;
return { sky, grass };
});
loading.catch(() => { loading = null; });
return loading;
} }
function buildSky(map) { function buildSky(map) {
@@ -65,98 +58,145 @@ window.Skybox = (() => {
} }
pos.needsUpdate = true; pos.needsUpdate = true;
const mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ map, depthWrite: false })); const mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ map, depthWrite: false }));
mesh.position.y = h - 0.01; // vlakke vloer net onder het eigen gras mesh.position.y = h - 0.01; // vlakke vloer net onder de tegelvloer
mesh.renderOrder = -1; mesh.renderOrder = -1;
return mesh; return mesh;
} }
function buildGrass(map) { // Ronde tegelvloer (straal r) + witte opstaande rand erlangs.
const R = GRASS_RADIUS_M + GRASS_FADE_M; function buildTerrace(map, r) {
const group = new THREE.Group();
const tex = map.clone(); const tex = map.clone();
tex.needsUpdate = true; tex.needsUpdate = true;
const aspect = (map.image && map.image.width) ? map.image.height / map.image.width : 1; tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(2 * R / GRASS_TILE_M, 2 * R / (GRASS_TILE_M * aspect)); tex.anisotropy = 8;
const material = new THREE.MeshLambertMaterial({ tex.repeat.set(2 * r / TILE_M, 2 * r / TILE_M);
map: tex, color: new THREE.Color(GRASS_SHADE, GRASS_SHADE, GRASS_SHADE), transparent: true, depthWrite: false const floor = new THREE.Mesh(new THREE.CircleGeometry(r, 128), new THREE.MeshStandardMaterial({ map: tex, roughness: 0.85 }));
}); floor.rotation.x = -Math.PI / 2;
// Randvervaging op afstand tot het midden (eigen varying: de UV's zijn floor.receiveShadow = true;
// al herhaald voor de textuur). group.add(floor);
material.onBeforeCompile = (shader) => {
shader.vertexShader = 'varying float vGrassR;\n' + shader.vertexShader.replace( const ring = new THREE.Shape();
'#include <begin_vertex>', '#include <begin_vertex>\nvGrassR = length(transformed.xy);'); ring.absarc(0, 0, r + RIM_THICK_M, 0, Math.PI * 2, false);
shader.fragmentShader = 'varying float vGrassR;\n' + shader.fragmentShader.replace( const hole = new THREE.Path();
'#include <dithering_fragment>', hole.absarc(0, 0, r, 0, Math.PI * 2, true);
`#include <dithering_fragment>\ngl_FragColor.a *= 1.0 - smoothstep(${GRASS_RADIUS_M.toFixed(1)}, ${R.toFixed(1)}, vGrassR);`); ring.holes.push(hole);
}; const rim = new THREE.Mesh(
const mesh = new THREE.Mesh(new THREE.CircleGeometry(R, 96), material); new THREE.ExtrudeGeometry(ring, { depth: RIM_HEIGHT_M, bevelEnabled: false, curveSegments: 128 }),
mesh.rotation.x = -Math.PI / 2; new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.5 })
mesh.receiveShadow = true; );
return mesh; rim.rotation.x = -Math.PI / 2; // extrusie omhoog
rim.castShadow = rim.receiveShadow = true;
group.add(rim);
return group;
} }
// Omgeving voor één scene. Geeft { group, setPlacement(x, groundY, z) }: // Omgeving voor één scene: { group, setMode(mode), fitTo(object3D, groundY) }.
// het midden (m) waar het object staat en de hoogte van het maaiveld (m).
function createEnvironment(scene) { function createEnvironment(scene) {
const group = new THREE.Group(); const group = new THREE.Group();
group.visible = false; group.visible = false;
scene.add(group); scene.add(group);
loadTextures().then(({ sky, grass }) => { const skies = {};
group.add(buildSky(sky)); let terrace = null, terraceR = 0, tilesTex = null, mode = 'none';
group.add(buildGrass(grass));
}).catch(() => console.warn('Tectum: lucht/gras niet geladen (draait de app via een webserver?)')); function showSky() {
Object.keys(skies).forEach(k => { skies[k].visible = k === mode; });
if (mode === 'none' || skies[mode]) return;
const wanted = mode;
loadTexture(SKY_URLS[wanted]).then(tex => {
if (!skies[wanted]) { skies[wanted] = buildSky(tex); group.add(skies[wanted]); }
Object.keys(skies).forEach(k => { skies[k].visible = k === mode; });
}).catch(() => console.warn('Tectum: lucht niet geladen (draait de app via een webserver?)'));
}
function setTerrace(r) {
if (terrace && Math.abs(r - terraceR) < 0.05) return;
terraceR = r;
const build = () => {
if (terrace) { group.remove(terrace); terrace.traverse(o => { if (o.geometry) o.geometry.dispose(); }); }
terrace = buildTerrace(tilesTex, terraceR);
group.add(terrace);
};
if (tilesTex) build();
else loadTexture(TILES_URL).then(t => { tilesTex = t; build(); }).catch(() => {});
}
return { return {
group, group,
setPlacement(x, groundY, z) { group.position.set(x, groundY, z); } setMode(m) { mode = m; group.visible = m !== 'none'; showSky(); },
// Midden en straal uit de omhullende van het object (horizontaal), vloer op groundY (m).
fitTo(object3D, groundY) {
if (!object3D) return;
const box = new THREE.Box3().setFromObject(object3D);
if (box.isEmpty()) return;
const c = box.getCenter(new THREE.Vector3());
group.position.set(c.x, groundY, c.z);
setTerrace(Math.hypot(box.max.x - box.min.x, box.max.z - box.min.z) / 2 + MARGIN_M);
}
}; };
} }
function isOn() { function getMode() {
try { return localStorage.getItem(PREF_KEY) === '1'; } catch (e) { return false; } try {
const v = localStorage.getItem(PREF_KEY);
if (MODES.includes(v)) return v;
return localStorage.getItem('tectum.skybox') === '1' ? 'day' : 'none'; // oude aan/uit-voorkeur
} catch (e) { return 'none'; }
} }
function remember(on) { function remember(m) {
try { localStorage.setItem(PREF_KEY, on ? '1' : '0'); } catch (e) { /* geen opslag: alleen deze sessie */ } try { localStorage.setItem(PREF_KEY, m); } catch (e) { /* geen opslag: alleen deze sessie */ }
} }
// Lucht aan/uit voor een weergave: omgeving tonen, raster verbergen, camera const lightFactor = (m) => m === 'night' ? NIGHT_FACTOR : 1;
// niet onder de grond, zon met schaduw op het gras.
// o: { env, grid, controls, sun, shadowRoot (Object3D die schaduw werpt) } // Omgeving toepassen: lucht/vloer, raster, camera niet onder de grond, zon
function apply(on, o) { // met schaduw, en het licht (nacht: NIGHT_FACTOR).
o.env.group.visible = on; // o: { env, grid, controls, sun, ambient, base: { ambient, sun }, shadowRoot }
function apply(m, o) {
const on = m !== 'none';
o.env.setMode(m);
if (o.grid) o.grid.visible = !on; if (o.grid) o.grid.visible = !on;
if (o.controls) o.controls.maxPolarAngle = on ? Math.PI / 2 - 0.03 : Math.PI - 0.001; if (o.controls) o.controls.maxPolarAngle = on ? Math.PI / 2 - 0.03 : Math.PI - 0.001;
const f = lightFactor(m);
if (o.ambient && o.base) o.ambient.intensity = o.base.ambient * f;
if (o.sun && o.base) o.sun.intensity = o.base.sun * f;
if (o.sun) { if (o.sun) {
o.sun.castShadow = on; o.sun.castShadow = on;
if (on) { if (on) {
const cam = o.sun.shadow.camera; const p = o.env.group.position, cam = o.sun.shadow.camera;
cam.left = cam.bottom = -GRASS_RADIUS_M; cam.right = cam.top = GRASS_RADIUS_M; cam.left = cam.bottom = -20; cam.right = cam.top = 20;
cam.near = 0.5; cam.far = 60; cam.near = 0.5; cam.far = 80;
cam.updateProjectionMatrix(); cam.updateProjectionMatrix();
o.sun.shadow.mapSize.set(2048, 2048); o.sun.shadow.mapSize.set(2048, 2048);
o.sun.shadow.bias = -0.0005; o.sun.shadow.bias = -0.0005;
o.sun.position.set(o.env.group.position.x + SUN_OFFSET.x, o.env.group.position.y + SUN_OFFSET.y, o.env.group.position.z + SUN_OFFSET.z); o.sun.position.set(p.x + SUN_OFFSET.x, p.y + SUN_OFFSET.y, p.z + SUN_OFFSET.z);
o.sun.target.position.copy(o.env.group.position); o.sun.target.position.copy(p);
o.sun.target.updateMatrixWorld(); o.sun.target.updateMatrixWorld();
} }
} }
if (o.shadowRoot) o.shadowRoot.traverse(m => { if (m.isMesh) m.castShadow = on; }); if (o.shadowRoot) o.shadowRoot.traverse(obj => { if (obj.isMesh) obj.castShadow = on; });
} }
// "Lucht"-knop in een 3D-werkbalk. onChange(on) bij elke klik. // Knop in een 3D-werkbalk die door geen -> dag -> nacht wisselt.
const LABELS = { none: ['cloud', 'Lucht'], day: ['sun', 'Dag'], night: ['moon-stars', 'Nacht'] };
function createToggle(toolbar, onChange) { function createToggle(toolbar, onChange) {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.type = 'button'; btn.type = 'button';
btn.className = 'btn-3d-toggle' + (isOn() ? ' active' : ''); btn.title = 'Omgeving: geen / dag / nacht';
btn.title = 'Lucht en gras als omgeving'; let m = getMode();
btn.innerHTML = '<i class="bi bi-cloud-sun"></i>Lucht'; const sync = () => {
btn.className = 'btn-3d-toggle' + (m !== 'none' ? ' active' : '');
btn.innerHTML = `<i class="bi bi-${LABELS[m][0]}"></i>${LABELS[m][1]}`;
};
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const on = !btn.classList.contains('active'); m = MODES[(MODES.indexOf(m) + 1) % MODES.length];
btn.classList.toggle('active', on); remember(m);
remember(on); sync();
onChange(on); onChange(m);
}); });
sync();
toolbar.appendChild(btn); toolbar.appendChild(btn);
return btn; return btn;
} }
return { CAMERA_HEIGHT_M, createEnvironment, apply, isOn, createToggle }; return { CAMERA_HEIGHT_M, NIGHT_FACTOR, createEnvironment, apply, getMode, lightFactor, createToggle };
})(); })();
+9 -5
View File
@@ -700,7 +700,8 @@ function mountFloorEditor(container, callbacks) {
controls.update(); controls.update();
} }
scene.add(new THREE.AmbientLight(0xffffff, 0.6)); const floorAmbient = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(floorAmbient);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(5, 8, 5); dirLight.position.set(5, 8, 5);
dirLight.castShadow = true; dirLight.castShadow = true;
@@ -727,11 +728,14 @@ function mountFloorEditor(container, callbacks) {
isolatieVisible = v.isolatie; isolatieVisible = v.isolatie;
update3DScene(); update3DScene();
}); });
// Lucht en gras (core/skybox.js); de vloer staat met zijn onderkant op het gras. // Omgeving (core/skybox.js: geen/dag/nacht); de vloer staat op de tegelvloer.
let skyOn = Skybox.isOn(); let skyMode = Skybox.getMode();
const skyEnv = Skybox.createEnvironment(scene); const skyEnv = Skybox.createEnvironment(scene);
const applySky = () => Skybox.apply(skyOn, { env: skyEnv, grid, controls, sun: dirLight, shadowRoot: floorGroup3D }); const applySky = () => {
Skybox.createToggle(document.getElementById('floor-toolbar-3d'), (on) => { skyOn = on; applySky(); }); skyEnv.fitTo(floorGroup3D, 0);
Skybox.apply(skyMode, { env: skyEnv, grid, controls, sun: dirLight, ambient: floorAmbient, base: { ambient: 0.6, sun: 0.8 }, shadowRoot: floorGroup3D });
};
Skybox.createToggle(document.getElementById('floor-toolbar-3d'), (m) => { skyMode = m; applySky(); });
function update3DScene() { function update3DScene() {
if (floorGroup3D) scene.remove(floorGroup3D); if (floorGroup3D) scene.remove(floorGroup3D);
+17 -19
View File
@@ -84,7 +84,7 @@ const HOUSE_OVERVIEW_TEMPLATE = `
<div id="ho-pane-3d" class="ho-pane"> <div id="ho-pane-3d" class="ho-pane">
<div class="pane-label">3D</div> <div class="pane-label">3D</div>
<div class="toolbar-3d" id="ho-toolbar-3d"> <div class="toolbar-3d" id="ho-toolbar-3d">
<button type="button" id="ho-btn-render" class="btn-3d-toggle" title="Avondscène: elke lamp en spot is een echte lichtbron"><i class="bi bi-moon-stars"></i>Render</button> <button type="button" id="ho-btn-render" class="btn-3d-toggle" title="Render: elke lamp en spot is een echte lichtbron"><i class="bi bi-camera"></i>Render</button>
</div> </div>
</div> </div>
<div id="ho-specs"></div> <div id="ho-specs"></div>
@@ -301,7 +301,7 @@ function mountHouseEditor(container, callbacks) {
let cameraFitted = CameraMemory.restore(overviewCamKey, camera, controls); let cameraFitted = CameraMemory.restore(overviewCamKey, camera, controls);
let beplatingVisible = true, isolatieVisible = true, lampenVisible = true; let beplatingVisible = true, isolatieVisible = true, lampenVisible = true;
let fullRender = false; let fullRender = false;
let skyOn = Skybox.isOn(); let skyMode = Skybox.getMode();
const skyEnv = Skybox.createEnvironment(scene); const skyEnv = Skybox.createEnvironment(scene);
// Weergave-filter (core/view-filter.js). Beplating = wandplaten, vloer- // Weergave-filter (core/view-filter.js). Beplating = wandplaten, vloer-
@@ -319,30 +319,28 @@ function mountHouseEditor(container, callbacks) {
refresh({ keepProps: true }); refresh({ keepProps: true });
}); });
// Volle render: avondscène, lampen en spots als echte lichtbronnen. // Omgeving (core/skybox.js: geen/dag/nacht) en Render. Render = lampen en
// spots als echte lichtbronnen, in het licht van de gekozen omgeving;
// Nacht = omgevingslicht op 15% en de lampen altijd aan (100%). De
// tegelvloer ligt op het maaiveld, dus het ingegraven deel van vloer en
// sandwichpanelen verdwijnt eronder.
let lightsGroup = null; let lightsGroup = null;
// Lucht en gras (core/skybox.js): alleen in de gewone weergave (de volle
// render is een avondscène). Het gras ligt op het maaiveld, dus het
// ingegraven deel van vloer en sandwichpanelen verdwijnt eronder.
function applyRenderMode() { function applyRenderMode() {
const sky = skyOn && !fullRender; const lampsOn = fullRender || skyMode === 'night';
scene.background = new THREE.Color(fullRender ? 0x0b0f18 : 0x1a1a1a); scene.background = new THREE.Color(0x1a1a1a);
Skybox.apply(sky, { env: skyEnv, controls, sun: dirLight, shadowRoot: houseGroup }); Skybox.apply(skyMode, { env: skyEnv, controls, sun: dirLight, ambient, base: { ambient: 0.6, sun: 0.8 }, shadowRoot: houseGroup });
ambient.intensity = fullRender ? 0.1 : 0.6; ground.visible = fullRender && skyMode === 'none'; // zonder omgeving: vlak waarop het lamplicht valt
dirLight.intensity = fullRender ? 0.12 : 0.8; grid.visible = skyMode === 'none' && !fullRender;
ground.visible = fullRender;
grid.visible = !fullRender && !sky;
if (lightsGroup) { scene.remove(lightsGroup); lightsGroup = null; } if (lightsGroup) { scene.remove(lightsGroup); lightsGroup = null; }
// De nep-lichtbundels (kegels) alleen in de snelle weergave; in de // De nep-lichtbundels (kegels) alleen zonder echte lampen.
// volle render schijnt er echt licht. if (parts && parts.lightingGroup) parts.lightingGroup.traverse(o => { if (o.userData.beam) o.visible = !lampsOn; });
if (parts && parts.lightingGroup) parts.lightingGroup.traverse(o => { if (o.userData.beam) o.visible = !fullRender; }); if (lampsOn && parts && lampenVisible) {
if (fullRender && parts && lampenVisible) {
const edge = dakVisible ? parts.roofEdge : null; const edge = dakVisible ? parts.roofEdge : null;
lightsGroup = LightingModel.buildLights3D(parts.lighting.lamps, edge ? edge.spots : [], edge ? edge.top - edge.fasciaHeight : 0); lightsGroup = LightingModel.buildLights3D(parts.lighting.lamps, edge ? edge.spots : [], edge ? edge.top - edge.fasciaHeight : 0);
scene.add(lightsGroup); scene.add(lightsGroup);
} }
} }
Skybox.createToggle(document.getElementById('ho-toolbar-3d'), (on) => { skyOn = on; applyRenderMode(); }); Skybox.createToggle(document.getElementById('ho-toolbar-3d'), (m) => { skyMode = m; applyRenderMode(); });
document.getElementById('ho-btn-render').addEventListener('click', (e) => { document.getElementById('ho-btn-render').addEventListener('click', (e) => {
fullRender = !fullRender; fullRender = !fullRender;
e.currentTarget.classList.toggle('active', fullRender); e.currentTarget.classList.toggle('active', fullRender);
@@ -873,7 +871,7 @@ function mountHouseEditor(container, callbacks) {
grid.position.set(cx, 0, cz); grid.position.set(cx, 0, cz);
// Maaiveld: settings.groundLevel onder het loopvlak (bovenkant vloer). // Maaiveld: settings.groundLevel onder het loopvlak (bovenkant vloer).
const groundY = (Math.max(...parts.units.map(u => u.layout.floorTop)) - state.settings.groundLevel) * toM; const groundY = (Math.max(...parts.units.map(u => u.layout.floorTop)) - state.settings.groundLevel) * toM;
skyEnv.setPlacement(cx, groundY, cz); skyEnv.fitTo(houseGroup, groundY);
ground.position.set(cx, groundY - 0.002, cz); ground.position.set(cx, groundY - 0.002, cz);
applyRenderMode(); applyRenderMode();
if (!cameraFitted) { if (!cameraFitted) {
+9 -5
View File
@@ -1197,7 +1197,8 @@ function mountWallEditor(container, callbacks) {
controls.update(); controls.update();
} }
scene.add(new THREE.AmbientLight(0xffffff, 0.6)); const ambient = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambient);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(5, 8, 5); dirLight.position.set(5, 8, 5);
dirLight.castShadow = true; dirLight.castShadow = true;
@@ -1242,11 +1243,14 @@ function mountWallEditor(container, callbacks) {
isolatieVisible = v.isolatie; isolatieVisible = v.isolatie;
update3DScene(); update3DScene();
}); });
// Lucht en gras (core/skybox.js); de wand staat met zijn onderkant op het gras. // Omgeving (core/skybox.js: geen/dag/nacht); de wand staat op de tegelvloer.
let skyOn = Skybox.isOn(); let skyMode = Skybox.getMode();
const skyEnv = Skybox.createEnvironment(scene); const skyEnv = Skybox.createEnvironment(scene);
const applySky = () => Skybox.apply(skyOn, { env: skyEnv, grid, controls, sun: dirLight, shadowRoot: wallGroup3D }); const applySky = () => {
Skybox.createToggle(document.getElementById('wall-toolbar-3d'), (on) => { skyOn = on; applySky(); }); skyEnv.fitTo(wallGroup3D, 0);
Skybox.apply(skyMode, { env: skyEnv, grid, controls, sun: dirLight, ambient, base: { ambient: 0.6, sun: 0.8 }, shadowRoot: wallGroup3D });
};
Skybox.createToggle(document.getElementById('wall-toolbar-3d'), (m) => { skyMode = m; applySky(); });
// 3D-opbouw + volume-boekhouding komen uit WallModel.build3D (gedeeld // 3D-opbouw + volume-boekhouding komen uit WallModel.build3D (gedeeld
// met de unit-editor). // met de unit-editor).