626 lines
20 KiB
TypeScript
626 lines
20 KiB
TypeScript
import * as THREE from 'three';
|
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
import grandTourerUrl from '@/assets/nightdrive-grand-tourer-evolution.glb?url';
|
|
import type { SceneResourceBag } from '@/scene/resources';
|
|
import type { SceneDetailMode } from '@/scene/types';
|
|
|
|
export interface CarWorld {
|
|
group: CarGroup;
|
|
setDetail(detail: SceneDetailMode): void;
|
|
update(speed: number, acceleration: number, elapsedSeconds: number): void;
|
|
}
|
|
|
|
interface CarGroup {
|
|
name: string;
|
|
children: unknown[];
|
|
position: { x: number; y: number; z: number; set(x: number, y: number, z: number): void };
|
|
rotation: { x: number; y: number; z: number };
|
|
scale: { set(x: number, y: number, z: number): void };
|
|
visible: boolean;
|
|
add(...objects: unknown[]): void;
|
|
getObjectByName(name: string): unknown;
|
|
updateMatrixWorld(force: boolean): void;
|
|
}
|
|
|
|
interface AuthoredMaterial {
|
|
dispose(): void;
|
|
isMeshStandardMaterial?: boolean;
|
|
emissive?: { setHex(value: number): void };
|
|
emissiveIntensity: number;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface AuthoredObject {
|
|
name: string;
|
|
isMesh?: boolean;
|
|
geometry?: { dispose(): void };
|
|
material?: AuthoredMaterial | AuthoredMaterial[];
|
|
}
|
|
|
|
interface AuthoredCarGroup extends CarGroup {
|
|
removeFromParent(): void;
|
|
traverse(callback: (object: AuthoredObject) => void): void;
|
|
}
|
|
|
|
const FULL_BASE_Y = 0.58;
|
|
const REDUCED_BASE_Y = 0.42;
|
|
const GRAND_TOURER_URL = grandTourerUrl;
|
|
|
|
export async function loadGrandTourer(signal: AbortSignal): Promise<AuthoredCarGroup> {
|
|
const response = await fetch(GRAND_TOURER_URL, { signal });
|
|
if (!response.ok) {
|
|
throw new Error(`Grand tourer asset failed to load (${response.status}).`);
|
|
}
|
|
const bytes = await response.arrayBuffer();
|
|
if (signal.aborted) throw new DOMException('Scene build aborted', 'AbortError');
|
|
const gltf = await new GLTFLoader().parseAsync(bytes, GRAND_TOURER_URL);
|
|
if (signal.aborted) throw new DOMException('Scene build aborted', 'AbortError');
|
|
const authored = gltf.scene.getObjectByName('evolution-chase-car') ?? gltf.scene.getObjectByName('chase-car') ?? gltf.scene;
|
|
authored.removeFromParent();
|
|
return authored as AuthoredCarGroup;
|
|
}
|
|
|
|
export function createCar(
|
|
resources: SceneResourceBag,
|
|
authoredModel?: AuthoredCarGroup,
|
|
): CarWorld {
|
|
if (authoredModel) return createAuthoredCar(resources, authoredModel);
|
|
const group = new THREE.Group();
|
|
group.name = 'chase-car';
|
|
let baseY = FULL_BASE_Y;
|
|
group.position.set(0, baseY, 0.72);
|
|
group.scale.set(1.1, 0.82, 0.98);
|
|
|
|
const bodyMaterial = resources.own('material', new THREE.MeshStandardMaterial({
|
|
color: 0x304e68,
|
|
emissive: 0x091725,
|
|
emissiveIntensity: 0.42,
|
|
roughness: 0.36,
|
|
metalness: 0.68,
|
|
}));
|
|
const shoulderMaterial = resources.own('material', new THREE.MeshStandardMaterial({
|
|
color: 0x365b77,
|
|
emissive: 0x0a1b2a,
|
|
emissiveIntensity: 0.36,
|
|
roughness: 0.3,
|
|
metalness: 0.74,
|
|
}));
|
|
const lowerBodyMaterial = resources.own('material', new THREE.MeshStandardMaterial({
|
|
color: 0x111c29,
|
|
roughness: 0.48,
|
|
metalness: 0.54,
|
|
}));
|
|
const glassMaterial = resources.own('material', new THREE.MeshStandardMaterial({
|
|
color: 0x071522,
|
|
emissive: 0x061522,
|
|
emissiveIntensity: 0.34,
|
|
roughness: 0.12,
|
|
metalness: 0.42,
|
|
transparent: true,
|
|
opacity: 0.92,
|
|
}));
|
|
const fasciaMaterial = resources.own('material', new THREE.MeshStandardMaterial({
|
|
color: 0x080d15,
|
|
roughness: 0.52,
|
|
metalness: 0.42,
|
|
}));
|
|
const tailMaterial = resources.own('material', new THREE.MeshStandardMaterial({
|
|
color: 0x8f0b2d,
|
|
emissive: 0xff174f,
|
|
emissiveIntensity: 1.25,
|
|
roughness: 0.26,
|
|
metalness: 0.18,
|
|
toneMapped: true,
|
|
}));
|
|
const cyanMaterial = resources.own('material', new THREE.MeshStandardMaterial({
|
|
color: 0x2b9bab,
|
|
emissive: 0x44dceb,
|
|
emissiveIntensity: 0.7,
|
|
roughness: 0.34,
|
|
metalness: 0.32,
|
|
toneMapped: true,
|
|
}));
|
|
const tireMaterial = resources.own('material', new THREE.MeshStandardMaterial({
|
|
color: 0x020407,
|
|
roughness: 0.94,
|
|
metalness: 0.04,
|
|
}));
|
|
const hubMaterial = resources.own('material', new THREE.MeshStandardMaterial({
|
|
color: 0x354451,
|
|
roughness: 0.42,
|
|
metalness: 0.78,
|
|
}));
|
|
|
|
const rearProfile = createRearProfile();
|
|
const silhouetteGeometry = resources.own('geometry', new THREE.ExtrudeGeometry(rearProfile, {
|
|
depth: 5.8,
|
|
steps: 1,
|
|
bevelEnabled: true,
|
|
bevelSegments: 1,
|
|
bevelSize: 0.11,
|
|
bevelThickness: 0.1,
|
|
}));
|
|
silhouetteGeometry.translate(0, 0, -2.9);
|
|
silhouetteGeometry.computeVertexNormals();
|
|
const silhouette = new THREE.Mesh(silhouetteGeometry, bodyMaterial);
|
|
silhouette.name = 'rear-silhouette';
|
|
group.add(silhouette);
|
|
|
|
const rearFaceMaterial = resources.own('material', new THREE.MeshBasicMaterial({
|
|
color: 0x1b3246,
|
|
toneMapped: true,
|
|
}));
|
|
const rearFace = new THREE.Mesh(
|
|
resources.own('geometry', new THREE.ShapeGeometry(rearProfile)),
|
|
rearFaceMaterial,
|
|
);
|
|
rearFace.name = 'rear-body-face';
|
|
rearFace.position.z = 3.025;
|
|
group.add(rearFace);
|
|
|
|
const leftHaunch = new THREE.Mesh(
|
|
resources.own('geometry', createHaunchGeometry(-1)),
|
|
shoulderMaterial,
|
|
);
|
|
leftHaunch.name = 'left-rear-haunch';
|
|
group.add(leftHaunch);
|
|
const rightHaunch = new THREE.Mesh(
|
|
resources.own('geometry', createHaunchGeometry(1)),
|
|
shoulderMaterial,
|
|
);
|
|
rightHaunch.name = 'right-rear-haunch';
|
|
group.add(rightHaunch);
|
|
|
|
const glass = new THREE.Mesh(
|
|
resources.own('geometry', createCanopyGeometry()),
|
|
glassMaterial,
|
|
);
|
|
glass.name = 'rear-glass';
|
|
group.add(glass);
|
|
|
|
const rearGlassProfile = new THREE.Shape();
|
|
rearGlassProfile.moveTo(-1.58, 0.8);
|
|
rearGlassProfile.lineTo(-1.15, 1.41);
|
|
rearGlassProfile.lineTo(1.15, 1.41);
|
|
rearGlassProfile.lineTo(1.58, 0.8);
|
|
rearGlassProfile.closePath();
|
|
const rearGlassMaterial = resources.own('material', new THREE.MeshBasicMaterial({
|
|
color: 0x0b2537,
|
|
transparent: true,
|
|
opacity: 0.94,
|
|
toneMapped: true,
|
|
}));
|
|
const rearGlass = new THREE.Mesh(
|
|
resources.own('geometry', new THREE.ShapeGeometry(rearGlassProfile)),
|
|
rearGlassMaterial,
|
|
);
|
|
rearGlass.name = 'rear-glass-panel';
|
|
rearGlass.position.z = 1.44;
|
|
group.add(rearGlass);
|
|
|
|
const fasciaGeometry = resources.own('geometry', new THREE.BoxGeometry(4.84, 0.56, 0.16));
|
|
const fascia = new THREE.Mesh(fasciaGeometry, fasciaMaterial);
|
|
fascia.name = 'rear-fascia';
|
|
fascia.position.set(0, 0.02, 3.07);
|
|
group.add(fascia);
|
|
|
|
const lightGeometry = resources.own('geometry', new THREE.BoxGeometry(4.55, 0.13, 0.09));
|
|
const lightBar = new THREE.Mesh(lightGeometry, tailMaterial);
|
|
lightBar.name = 'rear-light-bar';
|
|
lightBar.position.set(0, 0.46, 3.17);
|
|
group.add(lightBar);
|
|
|
|
const diffuserGeometry = resources.own('geometry', createDiffuserGeometry());
|
|
const diffuser = new THREE.Mesh(diffuserGeometry, lowerBodyMaterial);
|
|
diffuser.name = 'rear-diffuser';
|
|
diffuser.position.z = 3.08;
|
|
group.add(diffuser);
|
|
|
|
const wheelGeometry = resources.own('geometry', new THREE.CylinderGeometry(0.66, 0.66, 0.5, 24));
|
|
const hubGeometry = resources.own('geometry', new THREE.CylinderGeometry(0.31, 0.31, 0.515, 16));
|
|
for (const [x, side] of [[-2.38, 'left'], [2.38, 'right']] as const) {
|
|
const wheel = new THREE.Mesh(wheelGeometry, tireMaterial);
|
|
wheel.name = `rear-wheel-${side}`;
|
|
wheel.rotation.z = Math.PI / 2;
|
|
wheel.position.set(x, -0.16, 2.1);
|
|
group.add(wheel);
|
|
const hub = new THREE.Mesh(hubGeometry, hubMaterial);
|
|
hub.name = `rear-hub-${side}`;
|
|
hub.rotation.z = Math.PI / 2;
|
|
hub.position.copy(wheel.position);
|
|
group.add(hub);
|
|
}
|
|
|
|
const underglowUniforms = {
|
|
uColor: { value: new THREE.Color(0x49d7e5) },
|
|
uOpacity: { value: 0.12 },
|
|
};
|
|
const underglowGeometry = resources.own('geometry', new THREE.PlaneGeometry(3.9, 4.2));
|
|
const underglowMaterial = resources.own('material', new THREE.ShaderMaterial({
|
|
uniforms: underglowUniforms,
|
|
vertexShader: `
|
|
varying vec2 vGlowUv;
|
|
void main() {
|
|
vGlowUv = uv;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`,
|
|
fragmentShader: `
|
|
uniform vec3 uColor;
|
|
uniform float uOpacity;
|
|
varying vec2 vGlowUv;
|
|
void main() {
|
|
vec2 centered = (vGlowUv - 0.5) * vec2(1.55, 1.0);
|
|
float glow = 1.0 - smoothstep(0.12, 0.72, length(centered));
|
|
gl_FragColor = vec4(uColor, glow * uOpacity);
|
|
#include <tonemapping_fragment>
|
|
#include <colorspace_fragment>
|
|
}
|
|
`,
|
|
transparent: true,
|
|
depthWrite: false,
|
|
depthTest: true,
|
|
blending: THREE.AdditiveBlending,
|
|
toneMapped: true,
|
|
}));
|
|
const underglow = new THREE.Mesh(underglowGeometry, underglowMaterial);
|
|
underglow.name = 'car-underglow';
|
|
underglow.rotation.x = -Math.PI / 2;
|
|
underglow.position.set(0, -0.67, 0.45);
|
|
group.add(underglow);
|
|
const contact = createContactShadow(resources, 4.5, 4.8, 0.22);
|
|
contact.mesh.position.set(0, -0.695, 0.52);
|
|
group.add(contact.mesh);
|
|
|
|
const secondary = new THREE.Group();
|
|
secondary.name = 'secondary-detail';
|
|
const trimGeometry = resources.own('geometry', new THREE.BoxGeometry(0.38, 0.075, 0.07));
|
|
for (const x of [-1.98, 1.98]) {
|
|
const trim = new THREE.Mesh(trimGeometry, cyanMaterial);
|
|
trim.position.set(x, -0.15, 3.13);
|
|
secondary.add(trim);
|
|
}
|
|
const deckGeometry = resources.own('geometry', new THREE.BoxGeometry(3.25, 0.075, 0.62));
|
|
const rearDeck = new THREE.Mesh(deckGeometry, shoulderMaterial);
|
|
rearDeck.position.set(0, 0.86, 2.15);
|
|
rearDeck.rotation.x = -0.08;
|
|
secondary.add(rearDeck);
|
|
const frontWheelGeometry = resources.own('geometry', new THREE.CylinderGeometry(0.58, 0.58, 0.42, 18));
|
|
for (const x of [-2.18, 2.18]) {
|
|
const wheel = new THREE.Mesh(frontWheelGeometry, tireMaterial);
|
|
wheel.rotation.z = Math.PI / 2;
|
|
wheel.position.set(x, -0.14, -2.05);
|
|
secondary.add(wheel);
|
|
}
|
|
group.add(secondary);
|
|
|
|
let detail: SceneDetailMode = 'full';
|
|
let squat = 0;
|
|
let squatVelocity = 0;
|
|
let previousElapsed: number | null = null;
|
|
|
|
const setDetail = (nextDetail: SceneDetailMode): void => {
|
|
if (detail === nextDetail) return;
|
|
detail = nextDetail;
|
|
secondary.visible = detail === 'full';
|
|
if (detail === 'reduced') {
|
|
baseY = REDUCED_BASE_Y;
|
|
group.scale.set(0.86, 0.66, 0.76);
|
|
group.position.z = 0.62;
|
|
} else {
|
|
baseY = FULL_BASE_Y;
|
|
group.scale.set(1.1, 0.82, 0.98);
|
|
group.position.z = 0.72;
|
|
}
|
|
group.position.y = baseY - squat;
|
|
};
|
|
|
|
return {
|
|
group,
|
|
setDetail,
|
|
update(speed, acceleration, elapsedSeconds) {
|
|
const safeSpeed = clamp01(speed);
|
|
const response = clamp01(acceleration);
|
|
const elapsed = Number.isFinite(elapsedSeconds) ? Math.max(0, elapsedSeconds) : 0;
|
|
const deltaSeconds = previousElapsed === null
|
|
? 1 / 60
|
|
: THREE.MathUtils.clamp(elapsed - previousElapsed, 0, 0.05);
|
|
previousElapsed = elapsed;
|
|
const targetSquat = response * 0.088;
|
|
const springAcceleration = (targetSquat - squat) * 34 - squatVelocity * 10;
|
|
squatVelocity += springAcceleration * deltaSeconds;
|
|
squat += squatVelocity * deltaSeconds;
|
|
const vibration = Math.sin(elapsed * (8.2 + safeSpeed * 2.4)) * (0.002 + safeSpeed * 0.003);
|
|
group.position.y = baseY - squat + vibration;
|
|
group.rotation.x = -squat * 0.045;
|
|
group.rotation.z = (
|
|
Math.sin(elapsed * 0.78) * safeSpeed * 0.0024
|
|
+ Math.sin(elapsed * 1.31) * response * 0.0014
|
|
);
|
|
tailMaterial.emissiveIntensity = 1.25 + response * 1.35;
|
|
underglowUniforms.uOpacity.value = 0.1 + safeSpeed * 0.055 + response * 0.07;
|
|
contact.uniforms.uOpacity.value = 0.2 + response * 0.06;
|
|
},
|
|
};
|
|
}
|
|
|
|
function createAuthoredCar(
|
|
resources: SceneResourceBag,
|
|
authoredModel: AuthoredCarGroup,
|
|
): CarWorld {
|
|
const group = new THREE.Group();
|
|
group.name = 'chase-car';
|
|
authoredModel.name = 'grand-tourer-model';
|
|
authoredModel.rotation.y = Math.PI;
|
|
group.add(authoredModel);
|
|
|
|
const tailMaterials: AuthoredMaterial[] = [];
|
|
const cyanMaterials: AuthoredMaterial[] = [];
|
|
authoredModel.traverse((object) => {
|
|
if (object.name.startsWith('evolution-')) object.name = object.name.slice('evolution-'.length);
|
|
// Three removes Blender's suffix dot when sanitizing imported node names.
|
|
object.name = object.name.replace(/\.?\d{3}$/u, '');
|
|
if (!object.isMesh || !object.geometry || !object.material) return;
|
|
resources.own('geometry', object.geometry);
|
|
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
|
for (const material of materials) {
|
|
resources.own('material', material);
|
|
ownMaterialTextures(resources, material);
|
|
if (
|
|
material.isMeshStandardMaterial
|
|
&& (object.name === 'car-body' || object.name === 'body-haunches')
|
|
) {
|
|
material.emissive?.setHex(0x0b2738);
|
|
material.emissiveIntensity = 0.26;
|
|
}
|
|
if (material.isMeshStandardMaterial && object.name === 'rear-glass') {
|
|
material.emissive?.setHex(0x041725);
|
|
material.emissiveIntensity = 0.31;
|
|
}
|
|
if (object.name === 'rear-light-bar' && material.isMeshStandardMaterial) {
|
|
tailMaterials.push(material);
|
|
}
|
|
if (object.name === 'car-underglow-source' && material.isMeshStandardMaterial) {
|
|
cyanMaterials.push(material);
|
|
}
|
|
}
|
|
});
|
|
|
|
const underglowUniforms = {
|
|
uColor: { value: new THREE.Color(0x47ddec) },
|
|
uOpacity: { value: 0.12 },
|
|
};
|
|
const underglow = new THREE.Mesh(
|
|
resources.own('geometry', new THREE.PlaneGeometry(1.72, 3.82)),
|
|
resources.own('material', new THREE.ShaderMaterial({
|
|
uniforms: underglowUniforms,
|
|
vertexShader: `
|
|
varying vec2 vGlowUv;
|
|
void main() {
|
|
vGlowUv = uv;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`,
|
|
fragmentShader: `
|
|
uniform vec3 uColor;
|
|
uniform float uOpacity;
|
|
varying vec2 vGlowUv;
|
|
void main() {
|
|
vec2 centered = (vGlowUv - 0.5) * vec2(1.7, 1.0);
|
|
float glow = 1.0 - smoothstep(0.08, 0.74, length(centered));
|
|
gl_FragColor = vec4(uColor * 1.35, glow * uOpacity);
|
|
#include <tonemapping_fragment>
|
|
#include <colorspace_fragment>
|
|
}
|
|
`,
|
|
transparent: true,
|
|
depthWrite: false,
|
|
depthTest: true,
|
|
blending: THREE.AdditiveBlending,
|
|
toneMapped: true,
|
|
})),
|
|
);
|
|
underglow.name = 'car-underglow';
|
|
underglow.rotation.x = -Math.PI / 2;
|
|
underglow.position.set(0, 0.012, 0.08);
|
|
group.add(underglow);
|
|
const contact = createContactShadow(resources, 1.98, 3.95, 0.24);
|
|
contact.mesh.position.set(0, 0.004, 0.14);
|
|
group.add(contact.mesh);
|
|
|
|
let detail: SceneDetailMode = 'full';
|
|
let baseY = 0.02;
|
|
let squat = 0;
|
|
let squatVelocity = 0;
|
|
let previousElapsed: number | null = null;
|
|
const setScale = (nextDetail: SceneDetailMode): void => {
|
|
if (nextDetail === 'reduced') {
|
|
baseY = -0.02;
|
|
group.scale.set(2.32, 2.32, 2.32);
|
|
group.position.z = 0.62;
|
|
} else {
|
|
baseY = 0.02;
|
|
group.scale.set(2.78, 2.78, 2.78);
|
|
group.position.z = 0.72;
|
|
}
|
|
group.position.y = baseY - squat;
|
|
};
|
|
setScale(detail);
|
|
|
|
return {
|
|
group,
|
|
setDetail(nextDetail) {
|
|
if (detail === nextDetail) return;
|
|
detail = nextDetail;
|
|
setScale(detail);
|
|
},
|
|
update(speed, acceleration, elapsedSeconds) {
|
|
const safeSpeed = clamp01(speed);
|
|
const response = clamp01(acceleration);
|
|
const elapsed = Number.isFinite(elapsedSeconds) ? Math.max(0, elapsedSeconds) : 0;
|
|
const deltaSeconds = previousElapsed === null
|
|
? 1 / 60
|
|
: THREE.MathUtils.clamp(elapsed - previousElapsed, 0, 0.05);
|
|
previousElapsed = elapsed;
|
|
const targetSquat = response * 0.04;
|
|
const springAcceleration = (targetSquat - squat) * 34 - squatVelocity * 10;
|
|
squatVelocity += springAcceleration * deltaSeconds;
|
|
squat += squatVelocity * deltaSeconds;
|
|
const vibration = Math.sin(elapsed * (7.4 + safeSpeed * 2.2)) * (0.0014 + safeSpeed * 0.0022);
|
|
group.position.y = baseY - squat + vibration;
|
|
group.rotation.x = -squat * 0.035;
|
|
group.rotation.z = Math.sin(elapsed * 0.8) * safeSpeed * 0.0018;
|
|
for (const material of tailMaterials) material.emissiveIntensity = 2.2 + response * 1.2;
|
|
for (const material of cyanMaterials) material.emissiveIntensity = 2.2 + response * 1.6;
|
|
underglowUniforms.uOpacity.value = 0.09 + safeSpeed * 0.04 + response * 0.075;
|
|
contact.uniforms.uOpacity.value = 0.21 + response * 0.055;
|
|
},
|
|
};
|
|
}
|
|
|
|
function createContactShadow(
|
|
resources: SceneResourceBag,
|
|
width: number,
|
|
depth: number,
|
|
opacity: number,
|
|
) {
|
|
const uniforms = {
|
|
uOpacity: { value: opacity },
|
|
};
|
|
const material = resources.own('material', new THREE.ShaderMaterial({
|
|
uniforms,
|
|
vertexShader: `
|
|
varying vec2 vShadowUv;
|
|
void main() {
|
|
vShadowUv = uv;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`,
|
|
fragmentShader: `
|
|
uniform float uOpacity;
|
|
varying vec2 vShadowUv;
|
|
void main() {
|
|
vec2 centered = (vShadowUv - 0.5) * vec2(1.65, 1.0);
|
|
float shadow = 1.0 - smoothstep(0.16, 0.72, length(centered));
|
|
gl_FragColor = vec4(vec3(0.002, 0.004, 0.008), shadow * uOpacity);
|
|
#include <tonemapping_fragment>
|
|
#include <colorspace_fragment>
|
|
}
|
|
`,
|
|
transparent: true,
|
|
depthWrite: false,
|
|
depthTest: true,
|
|
blending: THREE.NormalBlending,
|
|
toneMapped: true,
|
|
}));
|
|
const mesh = new THREE.Mesh(
|
|
resources.own('geometry', new THREE.PlaneGeometry(width, depth)),
|
|
material,
|
|
);
|
|
mesh.name = 'car-contact-shadow';
|
|
mesh.rotation.x = -Math.PI / 2;
|
|
mesh.renderOrder = 2;
|
|
return { mesh, uniforms };
|
|
}
|
|
|
|
function ownMaterialTextures(
|
|
resources: SceneResourceBag,
|
|
material: AuthoredMaterial,
|
|
): void {
|
|
for (const key of [
|
|
'map',
|
|
'emissiveMap',
|
|
'normalMap',
|
|
'roughnessMap',
|
|
'metalnessMap',
|
|
'aoMap',
|
|
]) {
|
|
const texture = material[key] as { isTexture?: boolean; dispose(): void } | null | undefined;
|
|
if (texture?.isTexture) resources.own('texture', texture);
|
|
}
|
|
}
|
|
|
|
function createRearProfile() {
|
|
const shape = new THREE.Shape();
|
|
shape.moveTo(-2.7, -0.42);
|
|
shape.lineTo(-2.63, 0.2);
|
|
shape.lineTo(-2.32, 0.64);
|
|
shape.lineTo(-1.56, 0.96);
|
|
shape.lineTo(1.56, 0.96);
|
|
shape.lineTo(2.32, 0.64);
|
|
shape.lineTo(2.63, 0.2);
|
|
shape.lineTo(2.7, -0.42);
|
|
shape.closePath();
|
|
return shape;
|
|
}
|
|
|
|
function createHaunchGeometry(side: -1 | 1) {
|
|
const inner = 0.95 * side;
|
|
const outer = 2.62 * side;
|
|
const frontOuter = 2.25 * side;
|
|
const positions = new Float32Array([
|
|
inner, 0.48, -1.75,
|
|
frontOuter, 0.38, -1.75,
|
|
inner, 0.92, -1.75,
|
|
outer, 0.68, -1.75,
|
|
inner, 0.48, 2.62,
|
|
outer, 0.33, 2.62,
|
|
inner, 0.92, 2.62,
|
|
outer, 0.73, 2.62,
|
|
]);
|
|
const geometry = new THREE.BufferGeometry();
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
geometry.setIndex([
|
|
0, 1, 2, 2, 1, 3,
|
|
4, 6, 5, 6, 7, 5,
|
|
0, 4, 1, 1, 4, 5,
|
|
2, 3, 6, 3, 7, 6,
|
|
1, 5, 3, 3, 5, 7,
|
|
0, 2, 4, 2, 6, 4,
|
|
]);
|
|
geometry.computeVertexNormals();
|
|
return geometry;
|
|
}
|
|
|
|
function createCanopyGeometry() {
|
|
const positions = new Float32Array([
|
|
-1.18, 0.78, -1.9,
|
|
1.18, 0.78, -1.9,
|
|
-0.86, 1.82, -1.65,
|
|
0.86, 1.82, -1.65,
|
|
-1.62, 0.78, 1.42,
|
|
1.62, 0.78, 1.42,
|
|
-1.18, 1.42, 1.06,
|
|
1.18, 1.42, 1.06,
|
|
]);
|
|
const geometry = new THREE.BufferGeometry();
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
geometry.setIndex([
|
|
0, 1, 2, 2, 1, 3,
|
|
4, 6, 5, 6, 7, 5,
|
|
0, 2, 4, 2, 6, 4,
|
|
1, 5, 3, 3, 5, 7,
|
|
2, 3, 6, 3, 7, 6,
|
|
0, 4, 1, 1, 4, 5,
|
|
]);
|
|
geometry.computeVertexNormals();
|
|
return geometry;
|
|
}
|
|
|
|
function createDiffuserGeometry() {
|
|
const shape = new THREE.Shape();
|
|
shape.moveTo(-2.28, -0.42);
|
|
shape.lineTo(-1.8, 0.04);
|
|
shape.lineTo(-0.62, -0.12);
|
|
shape.lineTo(0, 0.08);
|
|
shape.lineTo(0.62, -0.12);
|
|
shape.lineTo(1.8, 0.04);
|
|
shape.lineTo(2.28, -0.42);
|
|
shape.closePath();
|
|
return new THREE.ShapeGeometry(shape);
|
|
}
|
|
|
|
function clamp01(value: number): number {
|
|
return Number.isFinite(value) ? THREE.MathUtils.clamp(value, 0, 1) : 0;
|
|
}
|