Skip to content

Commit 5b9f146

Browse files
committed
1m opacity levels
1 parent be4cafe commit 5b9f146

File tree

3 files changed

+74
-51
lines changed

3 files changed

+74
-51
lines changed

client/src/game-client.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,15 +162,15 @@ export class GameClient {
162162
this.scene.add(house);
163163
this.worldObjects.push(house);
164164

165-
// Pole next to house (~1m to the right)
166-
const pole = WorldObjectFactory.createPole(-5, -4);
165+
// Pole outside the house (front yard)
166+
// House is centered at (-6, -4) with ~12.6m x 10.6m foundation footprint, so z > ~1.3 is safely outside.
167+
const pole = WorldObjectFactory.createPole(-5, 4);
167168
this.scene.add(pole);
168169
this.worldObjects.push(pole);
169170

170-
// Load and place bed near the house so it's immediately visible.
171-
// House is centered at (-6, -4) with a 12m x 10m footprint (units are meters).
172-
// Place the bed on the first-floor level (foundationHeight ~ 0.4m, floor at ~0.41m).
173-
WorldObjectFactory.loadBed(-7.5, -2.5, 0.42)
171+
// Load and place bed outside the house (front yard) so it's immediately visible.
172+
// Outside placement: keep y=0 so it rests on the ground plane instead of the interior floor.
173+
WorldObjectFactory.loadBed(-7.5, 3.5, 0)
174174
.then((bed) => {
175175
if (this.scene) {
176176
this.scene.add(bed);
@@ -210,10 +210,10 @@ export class GameClient {
210210
this.updateCameraStatusDisplay();
211211
}
212212

213-
// Height-based opacity: 1-9 hide objects above (n * 3m), 0 resets
213+
// Height-based opacity: 1-9 set cutoff in meters, 0 resets
214214
if (key >= '1' && key <= '9') {
215215
const level = parseInt(key, 10);
216-
const heightThreshold = level * 3; // 3m per level
216+
const heightThreshold = level; // meters
217217
this.heightOpacityManager?.setHeightOpacity(heightThreshold);
218218
} else if (key === '0') {
219219
this.heightOpacityManager?.setHeightOpacity(null); // Reset to fully opaque

client/src/world/HeightOpacityManager.ts

Lines changed: 59 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
import * as THREE from 'three';
22

3-
// Floor height constants (matching WorldObjectFactory/HouseBuilder)
4-
// Units: meters
5-
const FOUNDATION_HEIGHT = 0.4;
6-
const FLOOR_HEIGHT = 2.7;
7-
const FLOOR1_TOP = FOUNDATION_HEIGHT + FLOOR_HEIGHT; // 3.1m
8-
const FLOOR2_TOP = FOUNDATION_HEIGHT + FLOOR_HEIGHT * 2; // 5.8m
9-
10-
const HIDDEN_OPACITY = 0.15;
3+
const HIDDEN_OPACITY = 0.35;
114
const VISIBLE_OPACITY = 1.0;
125

6+
type OpacityMaterial = THREE.Material & {
7+
opacity: number;
8+
transparent: boolean;
9+
needsUpdate: boolean;
10+
userData: Record<string, unknown>;
11+
};
12+
1313
/**
1414
* Manages height-based opacity for world objects.
1515
* Objects above a threshold become semi-transparent.
16-
* Doors and roof elements are made transparent along with walls at the same floor level.
16+
*
17+
* Implementation detail:
18+
* - We treat "above the cutoff" as: the mesh's world-space bounding box extends above the cutoff.
19+
* This gives a better "cutaway" feel than using the mesh origin, while keeping doors (short)
20+
* opaque when the cutoff is above them.
1721
*/
1822
export class HeightOpacityManager {
1923
private readonly worldObjects: THREE.Object3D[];
24+
private readonly tmpBox = new THREE.Box3();
2025

2126
constructor(worldObjects: THREE.Object3D[]) {
2227
this.worldObjects = worldObjects;
@@ -25,44 +30,61 @@ export class HeightOpacityManager {
2530
/**
2631
* Sets opacity for objects based on height threshold.
2732
* Objects above the threshold become semi-transparent.
28-
* Doors and roof elements are made transparent along with walls at the same floor level.
2933
* @param heightThreshold - Height in meters above which objects become transparent. null = fully opaque.
3034
*/
3135
public setHeightOpacity(heightThreshold: number | null): void {
3236
for (const obj of this.worldObjects) {
3337
obj.traverse((child) => {
34-
if (child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial) {
35-
// Get world position of the mesh
36-
const worldPos = new THREE.Vector3();
37-
child.getWorldPosition(worldPos);
38+
if (!(child instanceof THREE.Mesh)) return;
39+
40+
const materials = Array.isArray(child.material) ? child.material : [child.material];
41+
if (materials.length === 0) return;
42+
43+
// Determine target opacity (per mesh) based on world-space height.
44+
let applyHidden = false;
45+
if (heightThreshold !== null) {
46+
// Ensure matrixWorld is current before we transform the local bbox.
47+
child.updateWorldMatrix(true, false);
48+
49+
const geom = child.geometry;
50+
if (geom?.boundingBox === null) {
51+
geom.computeBoundingBox();
52+
}
53+
if (geom?.boundingBox) {
54+
this.tmpBox.copy(geom.boundingBox).applyMatrix4(child.matrixWorld);
55+
applyHidden = this.tmpBox.max.y > heightThreshold;
56+
}
57+
}
58+
59+
for (const material of materials) {
60+
if (!material) continue;
61+
const mat = material as Partial<OpacityMaterial>;
62+
if (typeof mat.opacity !== 'number' || typeof mat.transparent !== 'boolean') continue;
63+
64+
// Cache original state once so we can restore on reset.
65+
const userData = (material.userData ??= {});
66+
if (userData.__heightOpacityOriginalOpacity === undefined) {
67+
userData.__heightOpacityOriginalOpacity = mat.opacity;
68+
}
69+
if (userData.__heightOpacityOriginalTransparent === undefined) {
70+
userData.__heightOpacityOriginalTransparent = mat.transparent;
71+
}
72+
73+
const originalOpacity = userData.__heightOpacityOriginalOpacity as number;
74+
const originalTransparent = userData.__heightOpacityOriginalTransparent as boolean;
3875

39-
// Determine target opacity based on type
40-
let targetOpacity: number;
4176
if (heightThreshold === null) {
42-
targetOpacity = VISIBLE_OPACITY;
77+
mat.opacity = originalOpacity;
78+
mat.transparent = originalTransparent || originalOpacity < VISIBLE_OPACITY;
79+
} else if (applyHidden) {
80+
mat.opacity = Math.min(originalOpacity, HIDDEN_OPACITY);
81+
mat.transparent = true;
4382
} else {
44-
const occlusionType = child.userData.occlusionType as string | undefined;
45-
const floorLevel = child.userData.floorLevel as number | undefined;
46-
47-
if (occlusionType === 'door' && floorLevel !== undefined) {
48-
// Doors become transparent when their floor's walls would be transparent
49-
// Floor 1 doors: transparent when threshold < floor1Top (3.1m)
50-
// Floor 2 doors: transparent when threshold < floor2Top (5.8m)
51-
const floorTop = floorLevel === 1 ? FLOOR1_TOP : FLOOR2_TOP;
52-
targetOpacity = heightThreshold < floorTop ? HIDDEN_OPACITY : VISIBLE_OPACITY;
53-
} else if (occlusionType === 'roof') {
54-
// Roof becomes transparent when threshold < floor2Top (when looking at 2nd floor)
55-
targetOpacity = heightThreshold < FLOOR2_TOP ? HIDDEN_OPACITY : VISIBLE_OPACITY;
56-
} else {
57-
// Default height-based check for walls and other objects
58-
targetOpacity = worldPos.y > heightThreshold ? HIDDEN_OPACITY : VISIBLE_OPACITY;
59-
}
83+
mat.opacity = originalOpacity;
84+
mat.transparent = originalTransparent || originalOpacity < VISIBLE_OPACITY;
6085
}
6186

62-
// Update material opacity
63-
child.material.transparent = true;
64-
child.material.opacity = targetOpacity;
65-
child.material.needsUpdate = true;
87+
mat.needsUpdate = true;
6688
}
6789
});
6890
}

client/src/world/WorldObjectFactory.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,24 +127,25 @@ export class WorldObjectFactory {
127127
const group = new THREE.Group();
128128

129129
// Main pole (grey/white)
130-
const poleGeometry = new THREE.CylinderGeometry(0.1, 0.1, height, 8);
130+
// Slightly thicker pole for visibility
131+
const poleRadius = 0.2;
132+
const poleGeometry = new THREE.CylinderGeometry(poleRadius, poleRadius, height, 12);
131133
const poleMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc });
132134
const pole = new THREE.Mesh(poleGeometry, poleMaterial);
133135
pole.position.y = height / 2;
134136
pole.castShadow = true;
135137
pole.receiveShadow = true;
136138
group.add(pole);
137139

138-
// Add red marks every 1 meter
139-
const markHeight = 0.3; // Height of each mark
140-
const markWidth = 0.4; // Width of each mark (extends outward from pole)
141-
const markGeometry = new THREE.BoxGeometry(markWidth, markHeight, markWidth);
140+
// Add red cylindrical marks (bands) every 1 meter
141+
const markHeight = 0.12; // band thickness (meters)
142+
const markRadius = poleRadius + 0.03; // slightly larger so it sits outside the pole surface
143+
const markGeometry = new THREE.CylinderGeometry(markRadius, markRadius, markHeight, 16);
142144
const markMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
143145

144146
for (let y = 1; y < height; y += 1) {
145147
const mark = new THREE.Mesh(markGeometry, markMaterial);
146148
mark.position.y = y;
147-
mark.position.x = 0.3; // Offset from center of pole (pole radius 0.1 + mark width/2)
148149
mark.castShadow = true;
149150
mark.receiveShadow = true;
150151
group.add(mark);

0 commit comments

Comments
 (0)