Files
neru.rip/src/app/page.tsx
T
2026-01-02 03:44:54 -03:00

703 lines
15 KiB
TypeScript

'use client';
import {
Environment,
OrbitControls,
PerspectiveCamera
} from '@react-three/drei';
import { Canvas, useLoader, useFrame } from '@react-three/fiber';
import {
forwardRef,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState
} from 'react';
import {
BufferAttribute,
BufferGeometry,
Mesh,
Object3D,
InstancedMesh,
DoubleSide,
TextureLoader,
Color,
MeshStandardMaterial
} from 'three';
import './page.css';
import { createNoise2D } from 'simplex-noise';
import { useControls } from 'leva';
import grassVert from './shaders/grass.vert';
import grassFrag from './shaders/grass.frag';
import {
Bloom,
EffectComposer,
Noise,
Pixelation,
Vignette,
DepthOfField,
GodRays,
SMAA
} from '@react-three/postprocessing';
interface Shader {
uniforms: { [key: string]: { value: unknown } };
vertexShader: string;
fragmentShader: string;
}
function getTerrainHeight(
localX: number,
localZ: number,
worldXBase: number,
worldZBase: number,
scale: number,
hillScale: number,
hillHeight: number,
detailScale: number,
detailHeight: number,
noise2D: (x: number, y: number) => number
) {
const worldX = (worldXBase + localX) * 0.1;
const worldZ = (worldZBase + localZ) * 0.1;
const noiseHill =
noise2D(worldX * hillScale, worldZ * hillScale) * hillHeight;
const noiseDetail =
noise2D(worldX * detailScale, worldZ * detailScale) * detailHeight;
return (noiseHill + noiseDetail) * scale;
}
interface GrassProps {
x: number;
y: number;
size: number;
count: number;
grassSize: number;
scale: number;
hillScale: number;
hillHeight: number;
detailScale: number;
detailHeight: number;
noise2D: (x: number, y: number) => number;
grassLOD: number;
enableShadows?: boolean;
}
function Grass({
x,
y,
size,
count,
grassSize,
scale,
hillScale,
hillHeight,
detailScale,
detailHeight,
noise2D,
grassLOD
}: GrassProps) {
const meshRef = useRef<InstancedMesh>(null);
const dummyRef = useRef<Object3D>(new Object3D());
const [alphaMap, normalMap] = useLoader(TextureLoader, [
'/img/grass_alpha.png',
'/img/grass_normal.png'
]);
const materialRef = useRef<
MeshStandardMaterial & { userData: { shader: Shader } }
>(null);
useFrame((state) => {
if (
materialRef.current &&
materialRef.current.userData &&
materialRef.current.userData.shader
) {
(materialRef.current.userData.shader as Shader).uniforms.uTime.value =
state.clock.getElapsedTime();
}
});
const geometry = useMemo(() => {
const geo = new BufferGeometry();
const w = 0.5;
const h = 2;
const segments = 4;
const positions: number[] = [];
const uvs: number[] = [];
const indices: number[] = [];
for (let i = 0; i < 3; i++) {
const angle = (Math.PI / 3) * i;
const sinA = Math.sin(angle);
const cosA = Math.cos(angle);
const offset = positions.length / 3;
for (let y = 0; y <= segments; y++) {
const v = y / segments;
const yPos = v * h;
positions.push(-w * cosA, yPos, -w * sinA);
uvs.push(0, v);
positions.push(w * cosA, yPos, w * sinA);
uvs.push(1, v);
}
for (let y = 0; y < segments; y++) {
const row1 = offset + y * 2;
const row2 = offset + (y + 1) * 2;
indices.push(row1, row1 + 1, row2);
indices.push(row1 + 1, row2 + 1, row2);
}
}
geo.setAttribute(
'position',
new BufferAttribute(new Float32Array(positions), 3)
);
geo.setAttribute('uv', new BufferAttribute(new Float32Array(uvs), 2));
geo.setIndex(indices);
geo.computeVertexNormals();
return geo;
}, []);
useLayoutEffect(() => {
if (!meshRef.current) return;
const dummy = dummyRef.current;
const worldXBase = x * size;
const worldZBase = y * size;
const color = new Color();
const lushColor = new Color('#348a34');
const dryColor = new Color('#556b19');
const chunkRadius = size * Math.sqrt(2);
let instanceIndex = 0;
for (let i = 0; i < count; i++) {
const localX = (Math.random() - 0.5) * size;
const localZ = (Math.random() - 0.5) * size;
const globalX = worldXBase + localX;
const globalZ = worldZBase + localZ;
const dist = Math.sqrt(globalX * globalX + globalZ * globalZ);
const maxDist = grassLOD;
const falloffStart = maxDist * 0.1;
const falloffEnd = maxDist;
let fallofFactor = (dist - falloffStart) / (falloffEnd - falloffStart);
fallofFactor = Math.max(0, Math.min(1, fallofFactor));
const density = (1.0 - fallofFactor) * (1.0 - fallofFactor);
if (Math.random() > density)
continue;
const localY = getTerrainHeight(
localX,
localZ,
worldXBase,
worldZBase,
scale,
hillScale,
hillHeight,
detailScale,
detailHeight,
noise2D
);
dummy.position.set(localX, localY, localZ);
dummy.rotation.y = Math.random() * Math.PI * 2;
dummy.rotation.x = (Math.random() - 0.5) * 0.2;
dummy.rotation.z = (Math.random() - 0.5) * 0.2;
const baseScale = grassSize + Math.random() * grassSize * 0.5;
const heightMult = 0.5 + Math.random() * 1.0;
dummy.scale.set(baseScale, baseScale * heightMult, baseScale);
dummy.updateMatrix();
meshRef.current.setMatrixAt(instanceIndex, dummy.matrix);
const noiseVal = noise2D(globalX * 0.02, globalZ * 0.02);
const t = (noiseVal + 1) / 2;
const randomInternal = (Math.random() - 0.5) * 0.2;
const finalT = Math.max(0, Math.min(1, t + randomInternal));
color.lerpColors(dryColor, lushColor, finalT);
meshRef.current.setColorAt(instanceIndex, color);
instanceIndex++;
}
meshRef.current.count = instanceIndex;
meshRef.current.instanceMatrix.needsUpdate = true;
if (meshRef.current.instanceColor)
meshRef.current.instanceColor.needsUpdate = true;
}, [
x,
y,
size,
count,
grassSize,
scale,
hillScale,
hillHeight,
detailScale,
detailHeight,
noise2D,
grassLOD
]);
const onBeforeCompile = useMemo(
() => (shader: Shader) => {
shader.uniforms.uTime = { value: 0 };
shader.vertexShader = `
uniform float uTime;
varying vec2 vGrassUv;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm(vec2 p) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;
for(int i = 0; i < 4; i++) {
value += amplitude * noise(p * frequency);
frequency *= 2.0;
amplitude *= 0.5;
}
return value;
}
${shader.vertexShader}
`;
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
`
#include <begin_vertex>
vGrassUv = uv;
${grassVert}
`
);
shader.fragmentShader = `
uniform float uTime;
varying vec2 vGrassUv;
${shader.fragmentShader}
`;
shader.fragmentShader = shader.fragmentShader.replace(
'#include <color_fragment>',
`
#include <color_fragment>
${grassFrag}
`
);
if (materialRef.current) {
materialRef.current.userData.shader = shader;
}
},
[grassLOD]
);
return (
<instancedMesh
ref={meshRef}
args={[undefined, undefined, count]}
position={[x * size, 0, y * size]}
geometry={geometry}
>
<meshStandardMaterial
ref={materialRef}
color='#ffffff'
side={DoubleSide}
alphaMap={alphaMap}
alphaTest={0.5}
normalMap={normalMap}
roughness={0.8}
metalness={0.1}
onBeforeCompile={onBeforeCompile}
/>
</instancedMesh>
);
}
interface TerrainChunkProps {
x: number;
y: number;
size: number;
resolution: number;
scale: number;
hillScale: number;
hillHeight: number;
detailScale: number;
detailHeight: number;
noise2D: (x: number, y: number) => number;
wireframe?: boolean;
grassCount: number;
grassSize: number;
grassLOD: number;
}
function TerrainChunk({
x,
y,
size,
resolution,
scale,
hillScale,
hillHeight,
detailScale,
detailHeight,
noise2D,
wireframe = false,
grassCount,
grassSize,
grassLOD
}: TerrainChunkProps) {
const chunkDist = Math.sqrt((x * size) ** 2 + (y * size) ** 2);
const shouldRenderGrass = chunkDist < grassLOD + size;
const meshRef = useRef<Mesh>(null);
const geometry = useMemo(() => {
const geo = new BufferGeometry();
const vertices: Array<number> = [];
const indices: Array<number> = [];
const step = size / (resolution - 1);
const halfSize = size / 2;
const worldXBase = x * size;
const worldZBase = y * size;
/*
vtx gen
*/
for (let iz = 0; iz < resolution; iz++) {
for (let ix = 0; ix < resolution; ix++) {
const localX = ix * step - halfSize;
const localZ = iz * step - halfSize;
const localY = getTerrainHeight(
localX,
localZ,
worldXBase,
worldZBase,
scale,
hillScale,
hillHeight,
detailScale,
detailHeight,
noise2D
);
vertices.push(localX, localY, localZ);
}
}
/*
idx gen
*/
for (let iz = 0; iz < resolution - 1; iz++) {
for (let ix = 0; ix < resolution - 1; ix++) {
const topLeft = iz * resolution + ix;
const topRight = topLeft + 1;
const bottomLeft = (iz + 1) * resolution + ix;
const bottomRight = bottomLeft + 1;
indices.push(topLeft, bottomLeft, topRight);
indices.push(topRight, bottomLeft, bottomRight);
}
}
geo.setAttribute(
'position',
new BufferAttribute(new Float32Array(vertices), 3)
);
const colors: Array<number> = [];
for (let iz = 0; iz < resolution; iz++) {
for (let ix = 0; ix < resolution; ix++) {
const localX = ix * step - halfSize;
const localZ = iz * step - halfSize;
const globalX = worldXBase + localX;
const globalZ = worldZBase + localZ;
const colorNoise = noise2D(globalX * 0.02, globalZ * 0.02);
const t = (colorNoise + 1) / 2;
const dryGreen = { r: 0.01, g: 0.01, b: 0.0 };
const lushGreen = { r: 0.0, g: 0.05, b: 0.0 };
const r = dryGreen.r + (lushGreen.r - dryGreen.r) * t;
const g = dryGreen.g + (lushGreen.g - dryGreen.g) * t;
const b = dryGreen.b + (lushGreen.b - dryGreen.b) * t;
colors.push(r, g, b);
}
}
geo.setAttribute('color', new BufferAttribute(new Float32Array(colors), 3));
geo.setIndex(indices);
geo.computeVertexNormals();
return geo;
}, [
x,
y,
size,
resolution,
scale,
hillScale,
hillHeight,
detailScale,
detailHeight,
noise2D
]);
return (
<group>
<mesh
ref={meshRef}
geometry={geometry}
position={[x * size, 0, y * size]}
>
<meshStandardMaterial
vertexColors
roughness={1.0}
metalness={0.0}
wireframe={wireframe}
/>
</mesh>
{!wireframe && shouldRenderGrass && (
<Grass
x={x}
y={y}
size={size}
count={grassCount}
grassSize={grassSize}
scale={scale}
hillScale={hillScale}
hillHeight={hillHeight}
detailScale={detailScale}
detailHeight={detailHeight}
noise2D={noise2D}
grassLOD={grassLOD}
/>
)}
</group>
);
}
interface TerrainProps {
chunks?: number;
chunkSize?: number;
resolution?: number;
scale?: number;
hillScale: number;
hillHeight: number;
detailScale: number;
detailHeight: number;
wireframe?: boolean;
grassCount?: number;
grassSize?: number;
grassLOD?: number;
}
function Terrain({
chunks = 5,
chunkSize = 10,
resolution = 8,
scale = 1,
hillScale = 0.2,
hillHeight = 3,
detailScale = 3,
detailHeight = 0.1,
wireframe = false,
grassCount = 6000,
grassSize = 0.6,
grassLOD = 40
}: TerrainProps) {
const noise2D = useMemo(() => createNoise2D(), []);
const offset = -Math.floor(chunks / 2);
const chunkPositions = useMemo(() => {
const positions: [number, number][] = [];
for (let x = 0; x < chunks; x++)
for (let y = 0; y < chunks; y++) positions.push([x + offset, y + offset]);
return positions;
}, [chunks, offset]);
return (
<group>
{chunkPositions.map(([x, y], index) => (
<TerrainChunk
key={`${x}-${y}-${index}`}
x={x}
y={y}
size={chunkSize}
resolution={resolution}
scale={scale}
hillScale={hillScale}
hillHeight={hillHeight}
detailScale={detailScale}
detailHeight={detailHeight}
noise2D={noise2D}
wireframe={wireframe}
grassCount={grassCount}
grassSize={grassSize}
grassLOD={grassLOD}
/>
))}
</group>
);
}
const SealCube = forwardRef<Mesh>((props, ref) => {
const texture = useLoader(TextureLoader, '/img/niko.jpg');
const meshRef = useRef<Mesh>(null);
useImperativeHandle(ref, () => meshRef.current!, []);
useFrame((state, delta) => {
if (meshRef.current) {
meshRef.current.rotation.x += delta * 0.5;
meshRef.current.rotation.y += delta * 0.5;
}
});
return (
<mesh ref={meshRef} position={[0, 3, 0]} castShadow receiveShadow>
<boxGeometry args={[0.85, 0.85, 0.85]} />
<meshBasicMaterial map={texture} />
</mesh>
);
});
SealCube.displayName = 'SealCube';
export default function Home() {
// const { chunks, chunkSize, chunkRes, terrainScale, hillScale, hillHeight, detailScale, detailHeight, wireframe, grassCount, grassSize, grassLOD } = useControls(
// {
// chunks: 8,
// chunkSize: 10,
// chunkRes: { value: 8, min: 2, max: 64, step: 1 },
// terrainScale: 1,
// hillScale: 0.1,
// hillHeight: 6.0,
// detailScale: 1.0,
// detailHeight: 0.1,
// wireframe: false,
// grassCount: { value: 6000, min: 0, max: 10000, step: 100 },
// grassSize: { value: 0.6, min: 0.01, max: 1.0, step: 0.01 },
// grassLOD: { value: 40, min: 10, max: 100, step: 5, label: 'Grass LOD Distance' },
// }
// );
const [sealMesh, setSealMesh] = useState<Mesh | null>(null);
return (
<Canvas
shadows
camera={{ position: [0, 5, 15], fov: 50, far: 100 }}
gl={{ antialias: true }}
className='canvas'
>
<EffectComposer>
<DepthOfField target={[0, 5, 0]} focalLength={10} bokehScale={5} />
<Vignette />
<Noise opacity={0.05} />
<Bloom
intensity={2}
luminanceThreshold={0.5}
luminanceSmoothing={0.1}
/>
<SMAA />
{/* {sealMesh ? (
<GodRays
sun={sealMesh}
samples={16}
density={1}
decay={0.99}
weight={0.1}
exposure={0.2}
clampMax={1}
blur={true}
/>
) : (
<></>
)} */}
</EffectComposer>
<Environment files={'hdr/sky.hdr'} environmentIntensity={1} background />
<Terrain
chunks={16}
chunkSize={10.0}
resolution={8.0}
scale={1}
hillScale={0.1}
hillHeight={6.0}
detailScale={1.0}
detailHeight={0.2}
wireframe={false}
grassCount={6000}
grassSize={0.6}
grassLOD={60}
/>
<SealCube
ref={(mesh) => {
if (mesh && !sealMesh) setSealMesh(mesh);
}}
/>
<OrbitControls
target={[0, 3, 0]}
enablePan={false}
makeDefault
minDistance={2}
maxDistance={10}
/>
</Canvas>
);
}