feat: sculpt portfolio into an immersive midnight expressway
Author physical road furniture, layered terrain and atmosphere, projected illuminated route signs, and detailed Blender coachwork. Refine camera mass, wheel contact, chapter composition, and compact reading space while preserving approved content and audio contracts. Include isolated Blender source, before/after and hardware Chrome motion evidence, preservation hashes, validated preview checks, and documented inherited unit failures.
This commit is contained in:
@@ -2,7 +2,10 @@ import * as THREE from 'three';
|
||||
import type { EnvironmentChapter } from '@/journey/types';
|
||||
import type { RenderQualitySettings } from '@/scene/render-quality';
|
||||
import type { SceneResourceBag } from '@/scene/resources';
|
||||
import { CHAPTER_PALETTES, type MutableChapterPalette } from '@/scene/world/palette';
|
||||
import {
|
||||
CHAPTER_PALETTES,
|
||||
type MutableChapterPalette,
|
||||
} from '@/scene/world/palette';
|
||||
import {
|
||||
HORIZON_FOG_END,
|
||||
HORIZON_FOG_START,
|
||||
@@ -37,7 +40,10 @@ export function createTerrain(
|
||||
): TerrainWorld {
|
||||
const group = new THREE.Group();
|
||||
group.name = 'terrain-world';
|
||||
const geometry = resources.own('geometry', createIndexedTerrainGeometry(quality.terrainSegments));
|
||||
const geometry = resources.own(
|
||||
'geometry',
|
||||
createIndexedTerrainGeometry(quality.terrainSegments),
|
||||
);
|
||||
const initialPalette = CHAPTER_PALETTES['deep-night'];
|
||||
const lowColor = new THREE.Color(initialPalette.terrainLow);
|
||||
const highColor = new THREE.Color(initialPalette.terrainHigh);
|
||||
@@ -55,9 +61,11 @@ export function createTerrain(
|
||||
uFogColor: { value: new THREE.Color(initialPalette.fog) },
|
||||
uHazeColor: { value: new THREE.Color(initialPalette.haze) },
|
||||
};
|
||||
const material = resources.own('material', new THREE.ShaderMaterial({
|
||||
uniforms,
|
||||
vertexShader: `
|
||||
const material = resources.own(
|
||||
'material',
|
||||
new THREE.ShaderMaterial({
|
||||
uniforms,
|
||||
vertexShader: `
|
||||
attribute float roadMask;
|
||||
uniform float uDistance;
|
||||
uniform float uFlowDistance;
|
||||
@@ -71,6 +79,7 @@ export function createTerrain(
|
||||
varying float vRoadMask;
|
||||
varying float vDepthFade;
|
||||
varying float vFlow;
|
||||
varying vec3 vTerrainPosition;
|
||||
|
||||
float hash21(vec2 point) {
|
||||
point = mod(point, 256.0);
|
||||
@@ -106,6 +115,7 @@ export function createTerrain(
|
||||
float edgeGrowth = mix(7.0, uRelief, smoothstep(14.0, 150.0, abs(position.x)));
|
||||
float height = roadMask * ridgeNoise * edgeGrowth * uTerrainEnergy;
|
||||
displaced.y += height;
|
||||
vTerrainPosition = displaced;
|
||||
|
||||
vHeight = clamp(height / max(1.0, uRelief), 0.0, 1.0);
|
||||
vRoadMask = roadMask;
|
||||
@@ -117,7 +127,7 @@ export function createTerrain(
|
||||
gl_Position = projectionMatrix * viewPosition;
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
fragmentShader: `
|
||||
uniform vec3 uLowColor;
|
||||
uniform vec3 uHighColor;
|
||||
uniform vec3 uFogColor;
|
||||
@@ -127,25 +137,75 @@ export function createTerrain(
|
||||
varying float vRoadMask;
|
||||
varying float vDepthFade;
|
||||
varying float vFlow;
|
||||
varying vec3 vTerrainPosition;
|
||||
|
||||
void main() {
|
||||
float ridgeMix = smoothstep(0.16, 0.78, vHeight);
|
||||
vec3 color = mix(uLowColor, uHighColor, ridgeMix);
|
||||
color = mix(color, mix(uFogColor, uHazeColor, 0.2), 1.0 - vDepthFade);
|
||||
float pulse = 0.72 + 0.28 * smoothstep(0.16, 0.55, vFlow);
|
||||
float pulse = 0.88 + 0.12 * smoothstep(0.16, 0.55, vFlow);
|
||||
float routeCalm = mix(1.0, 0.9, smoothstep(0.82, 1.0, uRouteProgress));
|
||||
float alpha = vRoadMask * vDepthFade * pulse * routeCalm * mix(0.54, 0.88, ridgeMix);
|
||||
float alpha = vRoadMask * vDepthFade * pulse * routeCalm * mix(0.30, 0.83, ridgeMix);
|
||||
gl_FragColor = vec4(color * mix(1.15, 1.75, ridgeMix), alpha);
|
||||
#include <tonemapping_fragment>
|
||||
#include <colorspace_fragment>
|
||||
}
|
||||
`,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
toneMapped: true,
|
||||
}));
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
toneMapped: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const groundGeometry = resources.own('geometry', geometry.clone());
|
||||
const rows = Math.max(16, Math.floor(quality.terrainSegments));
|
||||
const columns = geometry.attributes.position.count / (rows + 1) - 1;
|
||||
const triangles: number[] = [];
|
||||
for (let row = 0; row < rows; row++)
|
||||
for (let column = 0; column < columns; column++) {
|
||||
const a = row * (columns + 1) + column,
|
||||
b = a + columns + 1;
|
||||
triangles.push(a, b, a + 1, a + 1, b, b + 1);
|
||||
}
|
||||
groundGeometry.setIndex(triangles);
|
||||
const groundMaterial = resources.own(
|
||||
'material',
|
||||
new THREE.ShaderMaterial({
|
||||
uniforms,
|
||||
vertexShader: material.vertexShader.replace(
|
||||
'displaced.y += height;',
|
||||
'displaced.y += height - 0.12;',
|
||||
),
|
||||
fragmentShader: `
|
||||
uniform vec3 uLowColor;
|
||||
uniform vec3 uHighColor;
|
||||
uniform vec3 uFogColor;
|
||||
varying float vHeight;
|
||||
varying float vDepthFade;
|
||||
varying vec3 vTerrainPosition;
|
||||
void main() {
|
||||
vec3 normal = normalize(cross(dFdx(vTerrainPosition), dFdy(vTerrainPosition)));
|
||||
float light = abs(dot(normal, normalize(vec3(-0.5, 0.8, -0.3))));
|
||||
vec3 color = mix(vec3(0.002, 0.008, 0.016), uLowColor * 0.11, light);
|
||||
color += uHighColor * pow(vHeight, 2.0) * light * 0.06;
|
||||
color = mix(uFogColor * 0.55, color, vDepthFade);
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
#include <tonemapping_fragment>
|
||||
#include <colorspace_fragment>
|
||||
}
|
||||
`,
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: true,
|
||||
depthTest: true,
|
||||
toneMapped: true,
|
||||
}),
|
||||
);
|
||||
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
||||
ground.name = 'sculpted-terrain-surface';
|
||||
ground.frustumCulled = false;
|
||||
group.add(ground);
|
||||
|
||||
const lines = new THREE.LineSegments(geometry, material);
|
||||
lines.name = 'terrain-grid';
|
||||
@@ -156,11 +216,13 @@ export function createTerrain(
|
||||
group,
|
||||
update(distance, speed, chapter, routeProgress, palette) {
|
||||
const chapterPalette = CHAPTER_PALETTES[chapter];
|
||||
uniforms.uDistance.value = ((finiteOrZero(distance) % NOISE_PERIOD) + NOISE_PERIOD) % NOISE_PERIOD;
|
||||
uniforms.uDistance.value =
|
||||
((finiteOrZero(distance) % NOISE_PERIOD) + NOISE_PERIOD) % NOISE_PERIOD;
|
||||
uniforms.uFlowDistance.value = uniforms.uDistance.value;
|
||||
uniforms.uSpeed.value = clamp01(speed);
|
||||
uniforms.uRouteProgress.value = clamp01(routeProgress);
|
||||
uniforms.uTerrainEnergy.value = palette?.terrainEnergy ?? chapterPalette.terrainEnergy;
|
||||
uniforms.uTerrainEnergy.value =
|
||||
palette?.terrainEnergy ?? chapterPalette.terrainEnergy;
|
||||
if (palette) {
|
||||
lowColor.copy(palette.terrainLow);
|
||||
highColor.copy(palette.terrainHigh);
|
||||
@@ -177,7 +239,10 @@ export function createTerrain(
|
||||
function createIndexedTerrainGeometry(segments: number) {
|
||||
const zSegments = Math.max(16, Math.floor(segments));
|
||||
const approximateCell = WORLD_DEPTH / zSegments;
|
||||
const xSegments = Math.max(48, Math.floor((TERRAIN_WIDTH / approximateCell) / 2) * 2);
|
||||
const xSegments = Math.max(
|
||||
48,
|
||||
Math.floor(TERRAIN_WIDTH / approximateCell / 2) * 2,
|
||||
);
|
||||
const columnCount = xSegments + 1;
|
||||
const rowCount = zSegments + 1;
|
||||
const positions = new Float32Array(columnCount * rowCount * 3);
|
||||
@@ -193,13 +258,18 @@ function createIndexedTerrainGeometry(segments: number) {
|
||||
positions[vertexOffset + 1] = 0;
|
||||
positions[vertexOffset + 2] = z;
|
||||
vertexOffset += 3;
|
||||
roadMasks[maskOffset] = smoothstep(ROAD_CLEAR_HALF_WIDTH, RIDGE_START, Math.abs(x));
|
||||
roadMasks[maskOffset] = smoothstep(
|
||||
ROAD_CLEAR_HALF_WIDTH,
|
||||
RIDGE_START,
|
||||
Math.abs(x),
|
||||
);
|
||||
maskOffset += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const indices: number[] = [];
|
||||
const vertexIndex = (column: number, row: number): number => row * columnCount + column;
|
||||
const vertexIndex = (column: number, row: number): number =>
|
||||
row * columnCount + column;
|
||||
for (let row = 0; row < rowCount; row += 1) {
|
||||
for (let column = 0; column < columnCount; column += 1) {
|
||||
const x = -TERRAIN_WIDTH / 2 + (column / xSegments) * TERRAIN_WIDTH;
|
||||
@@ -207,7 +277,8 @@ function createIndexedTerrainGeometry(segments: number) {
|
||||
indices.push(vertexIndex(column, row), vertexIndex(column, row + 1));
|
||||
}
|
||||
if (column < xSegments) {
|
||||
const nextX = -TERRAIN_WIDTH / 2 + ((column + 1) / xSegments) * TERRAIN_WIDTH;
|
||||
const nextX =
|
||||
-TERRAIN_WIDTH / 2 + ((column + 1) / xSegments) * TERRAIN_WIDTH;
|
||||
const midpoint = (x + nextX) * 0.5;
|
||||
if (Math.abs(midpoint) > ROAD_CLEAR_HALF_WIDTH) {
|
||||
indices.push(vertexIndex(column, row), vertexIndex(column + 1, row));
|
||||
@@ -225,7 +296,11 @@ function createIndexedTerrainGeometry(segments: number) {
|
||||
}
|
||||
|
||||
function smoothstep(edge0: number, edge1: number, value: number): number {
|
||||
const progress = THREE.MathUtils.clamp((value - edge0) / (edge1 - edge0), 0, 1);
|
||||
const progress = THREE.MathUtils.clamp(
|
||||
(value - edge0) / (edge1 - edge0),
|
||||
0,
|
||||
1,
|
||||
);
|
||||
return progress * progress * (3 - 2 * progress);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user