217 lines
5.9 KiB
TypeScript
217 lines
5.9 KiB
TypeScript
import * as THREE from 'three';
|
|
import type { SceneTheme } from './ThreeCanvas';
|
|
|
|
export interface ScenePalette {
|
|
accent: number;
|
|
accentSoft: number;
|
|
edge: number;
|
|
foliage: number;
|
|
ground: number;
|
|
mountain: number;
|
|
mountainDark: number;
|
|
mountainLight: number;
|
|
snow: number;
|
|
trunk: number;
|
|
water: number;
|
|
}
|
|
|
|
export const palettes: Record<SceneTheme, ScenePalette> = {
|
|
light: {
|
|
accent: 0x3d8afa,
|
|
accentSoft: 0x82b5ff,
|
|
edge: 0x1557a6,
|
|
foliage: 0x1557a6,
|
|
ground: 0xf8faff,
|
|
mountain: 0x1557a6,
|
|
mountainDark: 0x091c3f,
|
|
mountainLight: 0x82b5ff,
|
|
snow: 0xe6f0fe,
|
|
trunk: 0x0b2f5f,
|
|
water: 0x3d8afa,
|
|
},
|
|
dark: {
|
|
accent: 0x82b5ff,
|
|
accentSoft: 0x3d8afa,
|
|
edge: 0x426b9d,
|
|
foliage: 0x3d8afa,
|
|
ground: 0x07132f,
|
|
mountain: 0x0b2f5f,
|
|
mountainDark: 0x010d27,
|
|
mountainLight: 0x103f7f,
|
|
snow: 0x38526e,
|
|
trunk: 0x091c3f,
|
|
water: 0x1557a6,
|
|
},
|
|
};
|
|
|
|
export function fitOrthographicCamera(
|
|
camera: THREE.OrthographicCamera,
|
|
width: number,
|
|
height: number,
|
|
viewHeight: number,
|
|
) {
|
|
const aspect = width / Math.max(height, 1);
|
|
camera.left = (-viewHeight * aspect) / 2;
|
|
camera.right = (viewHeight * aspect) / 2;
|
|
camera.top = viewHeight / 2;
|
|
camera.bottom = -viewHeight / 2;
|
|
camera.updateProjectionMatrix();
|
|
}
|
|
|
|
export function createRibbonShape(points: THREE.Vector2[], width: number) {
|
|
const halfWidth = width / 2;
|
|
const n = points.length;
|
|
if (n < 2) return new THREE.Shape();
|
|
|
|
const left: THREE.Vector2[] = [];
|
|
const right: THREE.Vector2[] = [];
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
let normal: THREE.Vector2;
|
|
let miterScale = 1;
|
|
|
|
if (i === 0) {
|
|
const d = points[1].clone().sub(points[0]).normalize();
|
|
normal = new THREE.Vector2(-d.y, d.x);
|
|
} else if (i === n - 1) {
|
|
const d = points[n - 1].clone().sub(points[n - 2]).normalize();
|
|
normal = new THREE.Vector2(-d.y, d.x);
|
|
} else {
|
|
const d1 = points[i].clone().sub(points[i - 1]).normalize();
|
|
const d2 = points[i + 1].clone().sub(points[i]).normalize();
|
|
const n1 = new THREE.Vector2(-d1.y, d1.x);
|
|
const n2 = new THREE.Vector2(-d2.y, d2.x);
|
|
const miter = n1.clone().add(n2);
|
|
const len = miter.length();
|
|
if (len > 0.0001) {
|
|
miter.normalize();
|
|
const dot = Math.max(miter.dot(n1), 0.25);
|
|
miterScale = Math.min(1 / dot, 2.5);
|
|
normal = miter;
|
|
} else {
|
|
normal = n1;
|
|
}
|
|
}
|
|
|
|
const offset = normal.clone().multiplyScalar(halfWidth * miterScale);
|
|
left.push(points[i].clone().add(offset));
|
|
right.push(points[i].clone().sub(offset));
|
|
}
|
|
|
|
const shape = new THREE.Shape();
|
|
shape.moveTo(left[0].x, left[0].y);
|
|
for (let i = 1; i < left.length; i++) {
|
|
shape.lineTo(left[i].x, left[i].y);
|
|
}
|
|
for (let i = right.length - 1; i >= 0; i--) {
|
|
shape.lineTo(right[i].x, right[i].y);
|
|
}
|
|
shape.closePath();
|
|
return shape;
|
|
}
|
|
|
|
export function createMesa(
|
|
width: number,
|
|
height: number,
|
|
depth: number,
|
|
palette: ScenePalette,
|
|
) {
|
|
const halfWidth = width / 2;
|
|
const halfDepth = depth / 2;
|
|
const inset = Math.min(width, depth) * 0.19;
|
|
const positions = [
|
|
-halfWidth, 0, -halfDepth, halfWidth, 0, -halfDepth, halfWidth, 0, halfDepth, -halfWidth, 0, halfDepth,
|
|
-halfWidth + inset, height, -halfDepth + inset, halfWidth - inset, height, -halfDepth + inset,
|
|
halfWidth - inset, height, halfDepth - inset, -halfWidth + inset, height, halfDepth - inset,
|
|
];
|
|
const indices = [
|
|
4, 6, 5, 4, 7, 6,
|
|
0, 1, 5, 0, 5, 4,
|
|
1, 2, 6, 1, 6, 5,
|
|
2, 3, 7, 2, 7, 6,
|
|
3, 0, 4, 3, 4, 7,
|
|
];
|
|
const geometry = new THREE.BufferGeometry();
|
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
geometry.setIndex(indices);
|
|
geometry.clearGroups();
|
|
geometry.addGroup(0, 6, 0);
|
|
geometry.addGroup(6, 6, 1);
|
|
geometry.addGroup(12, 6, 2);
|
|
geometry.addGroup(18, 6, 1);
|
|
geometry.addGroup(24, 6, 2);
|
|
geometry.computeVertexNormals();
|
|
|
|
const mesa = new THREE.Mesh(
|
|
geometry,
|
|
[
|
|
new THREE.MeshStandardMaterial({
|
|
color: palette.mountainLight,
|
|
flatShading: true,
|
|
roughness: 0.9,
|
|
}),
|
|
new THREE.MeshStandardMaterial({
|
|
color: palette.mountain,
|
|
flatShading: true,
|
|
roughness: 0.92,
|
|
}),
|
|
new THREE.MeshStandardMaterial({
|
|
color: palette.mountainDark,
|
|
flatShading: true,
|
|
roughness: 0.94,
|
|
}),
|
|
],
|
|
);
|
|
mesa.castShadow = true;
|
|
mesa.receiveShadow = true;
|
|
return mesa;
|
|
}
|
|
|
|
export function createTree(palette: ScenePalette, scale = 1, pill = false) {
|
|
const tree = new THREE.Group();
|
|
const trunk = new THREE.Mesh(
|
|
new THREE.CylinderGeometry(0.08 * scale, 0.1 * scale, 0.45 * scale, 8),
|
|
new THREE.MeshStandardMaterial({ color: palette.trunk, roughness: 1 }),
|
|
);
|
|
trunk.position.y = 0.22 * scale;
|
|
tree.add(trunk);
|
|
|
|
const crown = new THREE.Mesh(
|
|
pill
|
|
? new THREE.CapsuleGeometry(0.2 * scale, 0.5 * scale, 4, 8)
|
|
: new THREE.ConeGeometry(0.38 * scale, 1.25 * scale, 10),
|
|
new THREE.MeshStandardMaterial({
|
|
color: pill ? palette.accentSoft : palette.foliage,
|
|
flatShading: true,
|
|
roughness: 0.9,
|
|
}),
|
|
);
|
|
crown.position.y = pill ? 0.9 * scale : 0.95 * scale;
|
|
crown.castShadow = true;
|
|
tree.add(crown);
|
|
return tree;
|
|
}
|
|
|
|
export function addSoftLights(scene: THREE.Scene, theme: SceneTheme, shadowArea: number) {
|
|
scene.add(
|
|
new THREE.HemisphereLight(
|
|
theme === 'dark' ? 0x3d8afa : 0xffffff,
|
|
theme === 'dark' ? 0x010d27 : 0xc3cfe0,
|
|
theme === 'dark' ? 1.7 : 2.8,
|
|
),
|
|
);
|
|
|
|
const key = new THREE.DirectionalLight(0xffffff, theme === 'dark' ? 1.7 : 2.8);
|
|
key.position.set(-7, 12, 8);
|
|
key.castShadow = true;
|
|
key.shadow.mapSize.set(1024, 1024);
|
|
key.shadow.camera.left = -shadowArea;
|
|
key.shadow.camera.right = shadowArea;
|
|
key.shadow.camera.top = shadowArea;
|
|
key.shadow.camera.bottom = -shadowArea;
|
|
key.shadow.camera.near = 1;
|
|
key.shadow.camera.far = 40;
|
|
key.shadow.bias = -0.0004;
|
|
scene.add(key);
|
|
}
|