This commit is contained in:
478
apps/web/src/DeadAir3D.tsx
Normal file
478
apps/web/src/DeadAir3D.tsx
Normal file
@@ -0,0 +1,478 @@
|
||||
import { useEffect, useRef, useState, type MutableRefObject } from "react";
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
DEAD_AIR_EXTRACTION,
|
||||
DEAD_AIR_MAP_SIZE,
|
||||
DEAD_AIR_POWER_SWITCH,
|
||||
DEAD_AIR_TICK_RATE,
|
||||
DEAD_AIR_WALLS,
|
||||
type DeadAirClientState,
|
||||
type DeadAirPlayerView,
|
||||
} from "@syncer/shared";
|
||||
import type { DeadAirRenderSource } from "./useDeadAirClient.js";
|
||||
|
||||
interface DeadAir3DProps {
|
||||
source: DeadAirRenderSource;
|
||||
playerId: number | null;
|
||||
unlockAudio(): void;
|
||||
}
|
||||
|
||||
interface PlayerMesh extends THREE.Group {
|
||||
userData: {
|
||||
lamp?: THREE.PointLight;
|
||||
beam?: THREE.SpotLight;
|
||||
};
|
||||
}
|
||||
|
||||
interface TemporaryEffect {
|
||||
object: THREE.Object3D;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
interface DeadAirRuntime {
|
||||
renderer: THREE.WebGLRenderer;
|
||||
scene: THREE.Scene;
|
||||
camera: THREE.PerspectiveCamera;
|
||||
ambient: THREE.HemisphereLight;
|
||||
poweredLights: THREE.PointLight[];
|
||||
flashlight: THREE.SpotLight;
|
||||
flashlightLens: THREE.Mesh;
|
||||
weapon: THREE.Group;
|
||||
muzzle: THREE.PointLight;
|
||||
players: Map<number, PlayerMesh>;
|
||||
artifact: THREE.Group;
|
||||
effects: TemporaryEffect[];
|
||||
resizeObserver: ResizeObserver;
|
||||
animationFrame: number;
|
||||
}
|
||||
|
||||
export function DeadAir3D({ source, playerId, unlockAudio }: DeadAir3DProps) {
|
||||
const hostRef = useRef<HTMLDivElement>(null);
|
||||
const playerIdRef = useRef(playerId);
|
||||
const sourceRef = useRef(source);
|
||||
const runtimeRef = useRef<DeadAirRuntime | null>(null);
|
||||
const lastEventIdRef = useRef(0);
|
||||
const [locked, setLocked] = useState(false);
|
||||
playerIdRef.current = playerId;
|
||||
sourceRef.current = source;
|
||||
|
||||
useEffect(() => {
|
||||
const host = hostRef.current;
|
||||
if (!host) return;
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 0.82;
|
||||
host.append(renderer.domElement);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x010304);
|
||||
scene.fog = new THREE.FogExp2(0x020506, 0.026);
|
||||
const camera = new THREE.PerspectiveCamera(76, 1, 0.035, 110);
|
||||
camera.rotation.order = "YXZ";
|
||||
scene.add(camera);
|
||||
|
||||
const ambient = new THREE.HemisphereLight(0x68828a, 0x050506, 0.16);
|
||||
scene.add(ambient);
|
||||
const poweredLights = buildLevel(scene);
|
||||
const artifact = buildArtifact();
|
||||
artifact.visible = false;
|
||||
scene.add(artifact);
|
||||
|
||||
const flashlightTarget = new THREE.Object3D();
|
||||
flashlightTarget.position.set(0, -0.08, -12);
|
||||
camera.add(flashlightTarget);
|
||||
const flashlight = new THREE.SpotLight(0xdffaff, 112, 31, 0.42, 0.42, 1.5);
|
||||
flashlight.position.set(0.12, -0.05, -0.12);
|
||||
flashlight.target = flashlightTarget;
|
||||
flashlight.castShadow = true;
|
||||
flashlight.shadow.mapSize.set(512, 512);
|
||||
flashlight.shadow.bias = -0.0005;
|
||||
camera.add(flashlight);
|
||||
|
||||
const weapon = buildWeapon();
|
||||
camera.add(weapon);
|
||||
const flashlightLens = weapon.getObjectByName("flashlight-lens") as THREE.Mesh;
|
||||
const muzzle = new THREE.PointLight(0xffd19b, 0, 5, 2);
|
||||
muzzle.position.set(0.33, -0.2, -1.05);
|
||||
camera.add(muzzle);
|
||||
|
||||
const runtime: DeadAirRuntime = {
|
||||
renderer,
|
||||
scene,
|
||||
camera,
|
||||
ambient,
|
||||
poweredLights,
|
||||
flashlight,
|
||||
flashlightLens,
|
||||
weapon,
|
||||
muzzle,
|
||||
players: new Map(),
|
||||
artifact,
|
||||
effects: [],
|
||||
resizeObserver: new ResizeObserver(() => resize(runtime, host)),
|
||||
animationFrame: 0,
|
||||
};
|
||||
runtimeRef.current = runtime;
|
||||
runtime.resizeObserver.observe(host);
|
||||
resize(runtime, host);
|
||||
|
||||
const animate = (time: number) => {
|
||||
updateRuntime(runtime, sourceRef.current, playerIdRef.current, lastEventIdRef, time);
|
||||
renderer.render(scene, camera);
|
||||
runtime.animationFrame = window.requestAnimationFrame(animate);
|
||||
};
|
||||
runtime.animationFrame = window.requestAnimationFrame(animate);
|
||||
const pointerLockChanged = () => setLocked(document.pointerLockElement === renderer.domElement);
|
||||
document.addEventListener("pointerlockchange", pointerLockChanged);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("pointerlockchange", pointerLockChanged);
|
||||
runtime.resizeObserver.disconnect();
|
||||
window.cancelAnimationFrame(runtime.animationFrame);
|
||||
scene.traverse(disposeObject);
|
||||
renderer.dispose();
|
||||
renderer.domElement.remove();
|
||||
runtimeRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const enter = () => {
|
||||
unlockAudio();
|
||||
const canvas = runtimeRef.current?.renderer.domElement;
|
||||
if (canvas) void canvas.requestPointerLock().catch(() => setLocked(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dead-air-viewport" ref={hostRef}>
|
||||
{!locked ? (
|
||||
<button className="dead-air-enter" type="button" onClick={enter}>
|
||||
<small>HEADPHONES STRONGLY RECOMMENDED</small>
|
||||
<strong>ENTER THE DEAD AIR</strong>
|
||||
<span>STEAL THE SCREAMING MICROWAVE // TRUST YOUR EARS</span>
|
||||
<em>WASD · MOUSE · CLICK FIRE · E INTERACT · F LIGHT · Q DECOY · R RELOAD</em>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function updateRuntime(
|
||||
runtime: DeadAirRuntime,
|
||||
source: DeadAirRenderSource,
|
||||
playerId: number | null,
|
||||
lastEventId: MutableRefObject<number>,
|
||||
time: number,
|
||||
): void {
|
||||
const frame = source.current;
|
||||
const world = frame.state;
|
||||
const local = world.players.find((player) => player.id === playerId);
|
||||
const fractionalSeconds = frame.interpolationAlpha / DEAD_AIR_TICK_RATE;
|
||||
if (local) {
|
||||
const correctionLife = Math.max(0, 1 - (time - frame.localCorrection.updatedAt) / 130);
|
||||
runtime.camera.position.set(
|
||||
local.x + local.velocityX * fractionalSeconds + frame.localCorrection.x * correctionLife,
|
||||
1.62,
|
||||
local.z + local.velocityZ * fractionalSeconds + frame.localCorrection.z * correctionLife,
|
||||
);
|
||||
runtime.camera.rotation.set(local.pitch, Math.PI + local.yaw, 0);
|
||||
runtime.flashlight.intensity = local.alive && local.flashlight ? 112 : 0;
|
||||
runtime.flashlightLens.visible = local.flashlight;
|
||||
runtime.weapon.visible = local.alive;
|
||||
const speed = Math.hypot(local.velocityX, local.velocityZ);
|
||||
runtime.weapon.position.x = 0.31 + Math.cos(time * 0.008) * Math.min(0.012, speed * 0.0018);
|
||||
runtime.weapon.position.y = -0.26 + Math.sin(time * 0.015) * Math.min(0.025, speed * 0.0035);
|
||||
} else {
|
||||
runtime.flashlight.intensity = 0;
|
||||
runtime.weapon.visible = false;
|
||||
}
|
||||
|
||||
runtime.ambient.intensity = world.powerOn ? 0.18 : 0.025;
|
||||
for (const light of runtime.poweredLights) {
|
||||
light.intensity = world.powerOn ? 3.4 + Math.sin(time * 0.003 + light.position.x) * 0.22 : 0;
|
||||
}
|
||||
updatePlayers(runtime, world, playerId, fractionalSeconds);
|
||||
updateArtifact(runtime, world, time);
|
||||
updateEffects(runtime, world, lastEventId, time);
|
||||
}
|
||||
|
||||
function buildLevel(scene: THREE.Scene): THREE.PointLight[] {
|
||||
const floor = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(DEAD_AIR_MAP_SIZE, DEAD_AIR_MAP_SIZE),
|
||||
new THREE.MeshStandardMaterial({ color: 0x101619, roughness: 0.91, metalness: 0.08 }),
|
||||
);
|
||||
floor.rotation.x = -Math.PI / 2;
|
||||
floor.receiveShadow = true;
|
||||
scene.add(floor);
|
||||
|
||||
const grid = new THREE.GridHelper(DEAD_AIR_MAP_SIZE, 36, 0x26343a, 0x182125);
|
||||
grid.position.y = 0.012;
|
||||
const gridMaterials = Array.isArray(grid.material) ? grid.material : [grid.material];
|
||||
for (const material of gridMaterials) {
|
||||
material.transparent = true;
|
||||
material.opacity = 0.21;
|
||||
}
|
||||
scene.add(grid);
|
||||
|
||||
for (const wall of DEAD_AIR_WALLS) {
|
||||
const mesh = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(wall.width, wall.height, wall.depth),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: wall.id.includes("vault") ? 0x253036 : 0x172126,
|
||||
roughness: 0.78,
|
||||
metalness: wall.id.includes("vault") ? 0.55 : 0.22,
|
||||
}),
|
||||
);
|
||||
mesh.position.set(wall.x, wall.height / 2, wall.z);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
scene.add(mesh);
|
||||
if (!wall.id.includes("north") && !wall.id.includes("south") && wall.width > 4) {
|
||||
const stripe = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(Math.max(0.2, wall.width - 0.08), 0.12, wall.depth + 0.025),
|
||||
new THREE.MeshStandardMaterial({ color: 0x5c4d2d, emissive: 0x1f1705, emissiveIntensity: 0.2 }),
|
||||
);
|
||||
stripe.position.set(wall.x, 1.05, wall.z);
|
||||
scene.add(stripe);
|
||||
}
|
||||
}
|
||||
|
||||
const extraction = new THREE.Mesh(
|
||||
new THREE.RingGeometry(DEAD_AIR_EXTRACTION.radius - 0.18, DEAD_AIR_EXTRACTION.radius, 48),
|
||||
new THREE.MeshBasicMaterial({ color: 0x5be6bf, transparent: true, opacity: 0.48, side: THREE.DoubleSide }),
|
||||
);
|
||||
extraction.rotation.x = -Math.PI / 2;
|
||||
extraction.position.set(DEAD_AIR_EXTRACTION.x, 0.045, DEAD_AIR_EXTRACTION.z);
|
||||
scene.add(extraction);
|
||||
const extractionLight = new THREE.PointLight(0x3de0b1, 2.4, 11, 2);
|
||||
extractionLight.position.set(DEAD_AIR_EXTRACTION.x, 1.1, DEAD_AIR_EXTRACTION.z);
|
||||
scene.add(extractionLight);
|
||||
|
||||
const powerBox = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(0.8, 1.4, 0.5),
|
||||
new THREE.MeshStandardMaterial({ color: 0x36434a, emissive: 0x601508, emissiveIntensity: 0.7, metalness: 0.65 }),
|
||||
);
|
||||
powerBox.position.set(DEAD_AIR_POWER_SWITCH.x, 0.9, DEAD_AIR_POWER_SWITCH.z);
|
||||
scene.add(powerBox);
|
||||
|
||||
const cratePositions: ReadonlyArray<readonly [number, number]> = [
|
||||
[-29, -29], [0, -27], [28, -28], [-28, 8], [0, 13], [28, 8], [-27, 27], [27, 27],
|
||||
];
|
||||
for (const [index, position] of cratePositions.entries()) {
|
||||
const crate = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(index % 2 ? 2.2 : 1.7, 0.18, index % 3 ? 1.5 : 2.1),
|
||||
new THREE.MeshStandardMaterial({ color: 0x302a22, roughness: 0.9, metalness: 0.12 }),
|
||||
);
|
||||
crate.position.set(position[0], 0.09, position[1]);
|
||||
crate.rotation.y = index * 0.37;
|
||||
crate.castShadow = true;
|
||||
crate.receiveShadow = true;
|
||||
scene.add(crate);
|
||||
}
|
||||
|
||||
const poweredLights: THREE.PointLight[] = [];
|
||||
const ceilingLightPositions: ReadonlyArray<readonly [number, number]> = [
|
||||
[-25, -10], [0, -14], [25, -10], [-25, 23], [1, 2], [25, 23],
|
||||
];
|
||||
for (const position of ceilingLightPositions) {
|
||||
const fixture = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(2.6, 0.08, 0.32),
|
||||
new THREE.MeshBasicMaterial({ color: 0xbad8dc }),
|
||||
);
|
||||
fixture.position.set(position[0], 4.1, position[1]);
|
||||
scene.add(fixture);
|
||||
const light = new THREE.PointLight(0xb8e4e9, 3.4, 16, 1.8);
|
||||
light.position.set(position[0], 3.85, position[1]);
|
||||
scene.add(light);
|
||||
poweredLights.push(light);
|
||||
}
|
||||
|
||||
const emergencyLightPositions: ReadonlyArray<readonly [number, number]> = [
|
||||
[-34, -4], [34, -4], [-2, -34], [-2, 34],
|
||||
];
|
||||
for (const position of emergencyLightPositions) {
|
||||
const emergency = new THREE.PointLight(0xff2f1c, 1.7, 9, 2);
|
||||
emergency.position.set(position[0], 2.3, position[1]);
|
||||
scene.add(emergency);
|
||||
}
|
||||
return poweredLights;
|
||||
}
|
||||
|
||||
function buildWeapon(): THREE.Group {
|
||||
const group = new THREE.Group();
|
||||
group.position.set(0.31, -0.26, -0.65);
|
||||
const body = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(0.19, 0.2, 0.9),
|
||||
new THREE.MeshStandardMaterial({ color: 0x151c20, roughness: 0.34, metalness: 0.78 }),
|
||||
);
|
||||
body.position.z = -0.16;
|
||||
group.add(body);
|
||||
const canister = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.055, 0.055, 0.52, 10),
|
||||
new THREE.MeshStandardMaterial({ color: 0x6fd2c4, emissive: 0x173c38, emissiveIntensity: 0.7, metalness: 0.38 }),
|
||||
);
|
||||
canister.rotation.x = Math.PI / 2;
|
||||
canister.position.set(-0.12, 0.06, -0.25);
|
||||
group.add(canister);
|
||||
const lens = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.075, 0.075, 0.06, 12),
|
||||
new THREE.MeshBasicMaterial({ color: 0xdffaff }),
|
||||
);
|
||||
lens.name = "flashlight-lens";
|
||||
lens.rotation.x = Math.PI / 2;
|
||||
lens.position.set(0.13, -0.08, -0.59);
|
||||
group.add(lens);
|
||||
return group;
|
||||
}
|
||||
|
||||
function buildPlayer(player: DeadAirPlayerView): PlayerMesh {
|
||||
const group = new THREE.Group() as PlayerMesh;
|
||||
const color = player.bot ? 0x7d1714 : 0x1d5960;
|
||||
const body = new THREE.Mesh(
|
||||
new THREE.CapsuleGeometry(0.46, 0.86, 4, 8),
|
||||
new THREE.MeshStandardMaterial({ color, roughness: 0.54, metalness: 0.3 }),
|
||||
);
|
||||
body.position.y = 1.03;
|
||||
body.castShadow = true;
|
||||
group.add(body);
|
||||
const visor = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(0.56, 0.19, 0.08),
|
||||
new THREE.MeshStandardMaterial({ color: 0x090d0f, emissive: player.bot ? 0x710b07 : 0x083e44, emissiveIntensity: 1.2 }),
|
||||
);
|
||||
visor.position.set(0, 1.62, 0.37);
|
||||
group.add(visor);
|
||||
const lamp = new THREE.PointLight(player.bot ? 0xff4a37 : 0xa8f6ff, 0, 7, 2);
|
||||
lamp.position.set(0.2, 1.55, 0.42);
|
||||
group.add(lamp);
|
||||
const target = new THREE.Object3D();
|
||||
target.position.set(0, 1.35, 9);
|
||||
group.add(target);
|
||||
const beam = new THREE.SpotLight(player.bot ? 0xff7661 : 0xc9f8ff, 0, 19, 0.42, 0.55, 1.6);
|
||||
beam.position.set(0.2, 1.55, 0.3);
|
||||
beam.target = target;
|
||||
group.add(beam);
|
||||
group.userData = { lamp, beam };
|
||||
return group;
|
||||
}
|
||||
|
||||
function updatePlayers(
|
||||
runtime: DeadAirRuntime,
|
||||
world: Readonly<DeadAirClientState>,
|
||||
playerId: number | null,
|
||||
fractionalSeconds: number,
|
||||
): void {
|
||||
const visible = new Set(world.players.map((player) => player.id));
|
||||
for (const player of world.players) {
|
||||
if (player.id === playerId) continue;
|
||||
let group = runtime.players.get(player.id);
|
||||
const targetX = player.x + player.velocityX * fractionalSeconds;
|
||||
const targetZ = player.z + player.velocityZ * fractionalSeconds;
|
||||
if (!group) {
|
||||
group = buildPlayer(player);
|
||||
group.position.set(targetX, 0, targetZ);
|
||||
runtime.players.set(player.id, group);
|
||||
runtime.scene.add(group);
|
||||
} else {
|
||||
group.position.x += (targetX - group.position.x) * 0.34;
|
||||
group.position.z += (targetZ - group.position.z) * 0.34;
|
||||
}
|
||||
group.rotation.y = player.yaw;
|
||||
group.visible = player.alive;
|
||||
if (group.userData.lamp) group.userData.lamp.intensity = player.flashlight ? 3.6 : 0;
|
||||
if (group.userData.beam) group.userData.beam.intensity = player.flashlight ? 34 : 0;
|
||||
}
|
||||
for (const [id, group] of runtime.players) {
|
||||
if (visible.has(id)) continue;
|
||||
runtime.players.delete(id);
|
||||
runtime.scene.remove(group);
|
||||
group.traverse(disposeObject);
|
||||
}
|
||||
}
|
||||
|
||||
function buildArtifact(): THREE.Group {
|
||||
const group = new THREE.Group();
|
||||
const body = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1.15, 0.75, 0.78),
|
||||
new THREE.MeshStandardMaterial({ color: 0xa78a31, roughness: 0.32, metalness: 0.82, emissive: 0x3c2300, emissiveIntensity: 0.6 }),
|
||||
);
|
||||
body.position.y = 0.58;
|
||||
body.castShadow = true;
|
||||
group.add(body);
|
||||
const door = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(0.78, 0.47, 0.035),
|
||||
new THREE.MeshStandardMaterial({ color: 0x15100b, emissive: 0xff3d0d, emissiveIntensity: 0.34, roughness: 0.18 }),
|
||||
);
|
||||
door.position.set(-0.12, 0.59, 0.405);
|
||||
group.add(door);
|
||||
const dial = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.075, 0.075, 0.06, 12),
|
||||
new THREE.MeshBasicMaterial({ color: 0xff8a35 }),
|
||||
);
|
||||
dial.rotation.x = Math.PI / 2;
|
||||
dial.position.set(0.42, 0.69, 0.43);
|
||||
group.add(dial);
|
||||
group.add(new THREE.PointLight(0xff4b17, 5, 8, 2));
|
||||
return group;
|
||||
}
|
||||
|
||||
function updateArtifact(runtime: DeadAirRuntime, world: Readonly<DeadAirClientState>, time: number): void {
|
||||
const artifact = world.artifact;
|
||||
runtime.artifact.visible = artifact.visible && artifact.x !== null && artifact.z !== null;
|
||||
if (!runtime.artifact.visible || artifact.x === null || artifact.z === null) return;
|
||||
runtime.artifact.position.set(artifact.x, 0.08 + Math.sin(time * 0.004) * 0.07, artifact.z);
|
||||
runtime.artifact.rotation.y = time * 0.00055;
|
||||
}
|
||||
|
||||
function updateEffects(
|
||||
runtime: DeadAirRuntime,
|
||||
world: Readonly<DeadAirClientState>,
|
||||
lastEventId: MutableRefObject<number>,
|
||||
time: number,
|
||||
): void {
|
||||
for (const entry of world.events) {
|
||||
if (entry.event.id <= lastEventId.current || entry.event.type !== "muzzle") continue;
|
||||
const event = entry.event;
|
||||
const direction = new THREE.Vector3(Math.sin(event.yaw), Math.sin(event.pitch), Math.cos(event.yaw)).normalize();
|
||||
const start = new THREE.Vector3(event.x, 1.45, event.z).addScaledVector(direction, 0.8);
|
||||
const end = start.clone().addScaledVector(direction, 27);
|
||||
const length = start.distanceTo(end);
|
||||
const tracer = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.018, 0.035, length, 5),
|
||||
new THREE.MeshBasicMaterial({ color: 0xffc78b, transparent: true, opacity: 0.7 }),
|
||||
);
|
||||
tracer.position.copy(start).add(end).multiplyScalar(0.5);
|
||||
tracer.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction);
|
||||
runtime.scene.add(tracer);
|
||||
runtime.effects.push({ object: tracer, expiresAt: time + 95 });
|
||||
runtime.muzzle.intensity = 16;
|
||||
}
|
||||
lastEventId.current = world.events.reduce(
|
||||
(latest, entry) => Math.max(latest, entry.event.id),
|
||||
lastEventId.current,
|
||||
);
|
||||
runtime.muzzle.intensity *= 0.75;
|
||||
runtime.effects = runtime.effects.filter((effect) => {
|
||||
if (effect.expiresAt > time) return true;
|
||||
runtime.scene.remove(effect.object);
|
||||
effect.object.traverse(disposeObject);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function resize(runtime: DeadAirRuntime, host: HTMLDivElement): void {
|
||||
const width = Math.max(1, host.clientWidth);
|
||||
const height = Math.max(1, host.clientHeight);
|
||||
runtime.renderer.setSize(width, height, false);
|
||||
runtime.camera.aspect = width / height;
|
||||
runtime.camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
function disposeObject(object: THREE.Object3D): void {
|
||||
if (!(object instanceof THREE.Mesh || object instanceof THREE.Line)) return;
|
||||
object.geometry.dispose();
|
||||
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
||||
for (const material of materials) material.dispose();
|
||||
}
|
||||
Reference in New Issue
Block a user