feat: add seal.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 8.6 MiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 241 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 360 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
.canvas {
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: black;
|
||||
z-index: 4444;
|
||||
transition: opacity 1s ease-in-out;
|
||||
pointer-events: none;
|
||||
|
||||
opacity: 1;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
.loader.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.niko-spin {
|
||||
width: 50vw;
|
||||
height: auto;
|
||||
display: block;
|
||||
animation: spin 3s ease-in-out infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
'use client';
|
||||
|
||||
import './page.css';
|
||||
|
||||
import { Environment, OrbitControls, useProgress } from "@react-three/drei";
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { Bloom, BrightnessContrast, DepthOfField, EffectComposer, HueSaturation, Noise, SMAA, SSAO, Vignette } from '@react-three/postprocessing';
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { folder, useControls, Leva } from 'leva';
|
||||
import SealCube from './scene-components/sealcube';
|
||||
import Terrain from './scene-components/terrain';
|
||||
|
||||
function Loader() {
|
||||
const { progress, active } = useProgress();
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!active && progress === 100) {
|
||||
const timeout = setTimeout(() => setVisible(false), 500);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [progress, active]);
|
||||
|
||||
return (
|
||||
<div className={`loader ${!visible ? 'loader hidden' : ''}`}>
|
||||
<picture>
|
||||
<img src='niko/img/niko.jpg' className='niko-spin' />
|
||||
</picture>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Scene() {
|
||||
const {
|
||||
terrainDryColor,
|
||||
terrainLushColor,
|
||||
chunks,
|
||||
chunkSize,
|
||||
resolution,
|
||||
hillScale,
|
||||
hillHeight,
|
||||
detailScale,
|
||||
detailHeight,
|
||||
grassDryColor,
|
||||
grassLushColor,
|
||||
grassCount,
|
||||
grassSize,
|
||||
grassLOD,
|
||||
grassBlades,
|
||||
grassSegments,
|
||||
grassLODStart,
|
||||
grassLODExponent
|
||||
} = useControls('Environment', {
|
||||
Terrain: folder({
|
||||
terrainDryColor: '#20270a',
|
||||
terrainLushColor: '#0f240f',
|
||||
chunks: { value: 16, min: 4, max: 24, step: 2 },
|
||||
chunkSize: { value: 10.0, min: 5.0, max: 40.0, step: 1.0 },
|
||||
resolution: { value: 8.0, min: 4.0, max: 30.0, step: 1.0 },
|
||||
hillScale: { value: 0.1, min: 0.01, max: 0.5, step: 0.01 },
|
||||
hillHeight: { value: 6.0, min: 0.0, max: 20.0, step: 0.5 },
|
||||
detailScale: { value: 1.0, min: 0.1, max: 5.0, step: 0.1 },
|
||||
detailHeight: { value: 0.2, min: 0.0, max: 2.0, step: 0.05 },
|
||||
}),
|
||||
Grass: folder({
|
||||
grassDryColor: '#495a17',
|
||||
grassLushColor: '#255825',
|
||||
grassCount: { value: 7000, min: 1000, max: 30000, step: 500 },
|
||||
grassSize: { value: 0.6, min: 0.1, max: 2.0, step: 0.05 },
|
||||
grassLOD: { value: 60, min: 10, max: 200, step: 5 },
|
||||
grassBlades: { value: 3, min: 1, max: 4, step: 1 },
|
||||
grassSegments: { value: 4, min: 1, max: 5, step: 1 },
|
||||
grassLODStart: { value: 0.5, min: 0.0, max: 0.9, step: 0.05 },
|
||||
grassLODExponent: { value: 1.0, min: 0.5, max: 3.0, step: 0.1 },
|
||||
})
|
||||
});
|
||||
|
||||
return (<>
|
||||
<Environment
|
||||
files={'niko/hdr/sky.hdr'}
|
||||
environmentIntensity={1}
|
||||
background
|
||||
/>
|
||||
|
||||
<fogExp2 attach='fog' args={[0x9a9a9a, 0.01]} />
|
||||
|
||||
<Terrain
|
||||
chunks={chunks}
|
||||
chunkSize={chunkSize}
|
||||
resolution={resolution}
|
||||
scale={1}
|
||||
hillScale={hillScale}
|
||||
hillHeight={hillHeight}
|
||||
detailScale={detailScale}
|
||||
detailHeight={detailHeight}
|
||||
grassCount={grassCount}
|
||||
grassSize={grassSize}
|
||||
grassLOD={grassLOD}
|
||||
terrainDryColor={terrainDryColor}
|
||||
terrainLushColor={terrainLushColor}
|
||||
grassDryColor={grassDryColor}
|
||||
grassLushColor={grassLushColor}
|
||||
grassBlades={grassBlades}
|
||||
grassSegments={grassSegments}
|
||||
grassLODStart={grassLODStart}
|
||||
grassLODExponent={grassLODExponent}
|
||||
/>
|
||||
|
||||
<SealCube />
|
||||
</>)
|
||||
}
|
||||
|
||||
function PostProcessing() {
|
||||
return (<EffectComposer>
|
||||
<DepthOfField target={[0, 3, 0]} focalLength={10} bokehScale={5} />
|
||||
<Vignette />
|
||||
<Noise opacity={0.05} />
|
||||
<Bloom
|
||||
intensity={2}
|
||||
luminanceThreshold={0.5}
|
||||
luminanceSmoothing={0.1}
|
||||
/>
|
||||
<SMAA />
|
||||
<HueSaturation saturation={0.1} />
|
||||
<BrightnessContrast brightness={0.05} contrast={-0.1} />
|
||||
</EffectComposer>)
|
||||
}
|
||||
|
||||
export default function Seal() {
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Leva hidden={isProduction} />
|
||||
|
||||
<Loader />
|
||||
<Canvas
|
||||
shadows
|
||||
camera={{ position: [0, 5, 15], fov: 50, far: 100 }}
|
||||
gl={{ antialias: true }}
|
||||
className='canvas'
|
||||
>
|
||||
<Scene />
|
||||
<PostProcessing />
|
||||
|
||||
<Environment
|
||||
files={'niko/hdr/sky.hdr'}
|
||||
environmentIntensity={1}
|
||||
background
|
||||
/>
|
||||
|
||||
<OrbitControls
|
||||
target={[0, 3, 0]}
|
||||
enablePan={false}
|
||||
makeDefault
|
||||
minDistance={2}
|
||||
maxDistance={10}
|
||||
/>
|
||||
</Canvas>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
import { useFrame, useLoader } from "@react-three/fiber";
|
||||
import { useLayoutEffect, useMemo, useRef } from "react";
|
||||
import { BufferAttribute, BufferGeometry, Color, DoubleSide, InstancedMesh, MeshStandardMaterial, Object3D, TextureLoader } from "three";
|
||||
import { getTerrainHeight, Shader } from "./helpers";
|
||||
|
||||
import grassVert from './shaders/grass.vert';
|
||||
import grassFrag from './shaders/grass.frag';
|
||||
|
||||
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;
|
||||
dryColor: string;
|
||||
lushColor: string;
|
||||
grassBlades?: number;
|
||||
grassSegments?: number;
|
||||
grassLODStart?: number;
|
||||
grassLODExponent?: number;
|
||||
}
|
||||
|
||||
export default function({
|
||||
x,
|
||||
y,
|
||||
size,
|
||||
count,
|
||||
grassSize,
|
||||
scale,
|
||||
hillScale,
|
||||
hillHeight,
|
||||
detailScale,
|
||||
detailHeight,
|
||||
noise2D,
|
||||
grassLOD,
|
||||
dryColor = '#556b19',
|
||||
lushColor = '#348a34',
|
||||
grassBlades = 3,
|
||||
grassSegments = 4,
|
||||
grassLODStart = 0.5,
|
||||
grassLODExponent = 1.0
|
||||
}: GrassProps) {
|
||||
const meshRef = useRef<InstancedMesh>(null);
|
||||
const dummyRef = useRef<Object3D>(new Object3D());
|
||||
|
||||
const [alphaMap, normalMap] = useLoader(TextureLoader, [
|
||||
'niko/img/grass_alpha.png',
|
||||
'niko/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 = grassSegments;
|
||||
const bladesCount = grassBlades;
|
||||
|
||||
const positions: number[] = [];
|
||||
const uvs: number[] = [];
|
||||
const indices: number[] = [];
|
||||
|
||||
for (let i = 0; i < bladesCount; i++) {
|
||||
const angle = (Math.PI / bladesCount) * 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;
|
||||
}, [grassBlades, grassSegments]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!meshRef.current) return;
|
||||
const dummy = dummyRef.current;
|
||||
|
||||
const worldXBase = x * size;
|
||||
const worldZBase = y * size;
|
||||
|
||||
const color = new Color();
|
||||
const lushColorObj = new Color(lushColor);
|
||||
const dryColorObj = new Color(dryColor);
|
||||
|
||||
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 * grassLODStart;
|
||||
const falloffEnd = maxDist;
|
||||
|
||||
let fallofFactor = (dist - falloffStart) / (falloffEnd - falloffStart);
|
||||
fallofFactor = Math.max(0, Math.min(1, fallofFactor));
|
||||
|
||||
const density = Math.pow(1.0 - fallofFactor, grassLODExponent);
|
||||
|
||||
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(dryColorObj, lushColorObj, 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,
|
||||
dryColor,
|
||||
lushColor,
|
||||
grassLODStart,
|
||||
grassLODExponent
|
||||
]);
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export 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;
|
||||
}
|
||||
|
||||
export interface Shader {
|
||||
uniforms: { [key: string]: { value: unknown } };
|
||||
vertexShader: string;
|
||||
fragmentShader: string;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useFrame, useLoader } from "@react-three/fiber";
|
||||
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||
import { Mesh, TextureLoader } from "three";
|
||||
|
||||
const SealCube = forwardRef<Mesh>((props, ref) => {
|
||||
const texture = useLoader(TextureLoader, 'niko/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} depthWrite={true} />
|
||||
</mesh>
|
||||
);
|
||||
});
|
||||
SealCube.displayName = 'SealCube';
|
||||
|
||||
export default SealCube;
|
||||
@@ -0,0 +1,13 @@
|
||||
float ao = smoothstep(0.0, 0.7, vGrassUv.y);
|
||||
ao = mix(0.05, 1.0, ao);
|
||||
|
||||
vec3 rootColor = diffuseColor.rgb * 0.4;
|
||||
vec3 tipColor = diffuseColor.rgb * 1.5;
|
||||
|
||||
vec3 grassColor = mix(rootColor, tipColor, vGrassUv.y);
|
||||
grassColor *= ao;
|
||||
|
||||
float translucency = pow(vGrassUv.y, 2.0) * 0.5;
|
||||
grassColor += diffuseColor.rgb * translucency;
|
||||
|
||||
diffuseColor.rgb = grassColor;
|
||||
@@ -0,0 +1,37 @@
|
||||
vec4 worldPos = modelMatrix * instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
||||
float gx = worldPos.x;
|
||||
float gz = worldPos.z;
|
||||
|
||||
vec2 mainWindDir = normalize(vec2(1.0, 0.6));
|
||||
float windSpeed = 1.5;
|
||||
|
||||
float windTime = uTime * windSpeed;
|
||||
vec2 windSamplePos = (worldPos.xz * 0.05) - (mainWindDir * windTime * 0.2);
|
||||
|
||||
float windBase = fbm(windSamplePos * 0.8) * 0.4 + 0.2;
|
||||
float gustNoise = fbm(windSamplePos * 0.4);
|
||||
float gust = pow(gustNoise, 3.0) * 1.8;
|
||||
float totalWind = windBase + gust;
|
||||
|
||||
float phase = gx * 0.5 + gz * 0.3;
|
||||
float spring = sin(uTime * 2.0 + phase) * 0.06 + sin(uTime * 4.5 + phase * 1.5) * 0.03;
|
||||
|
||||
float angleNoise = fbm(windSamplePos * 2.0 + uTime * 0.1) - 0.5;
|
||||
vec2 windDir = normalize(mainWindDir + vec2(-mainWindDir.y, mainWindDir.x) * angleNoise * 0.4);
|
||||
|
||||
float taperFactor = pow(uv.y, 4.0);
|
||||
float taper = 1.0 - taperFactor * 0.6;
|
||||
transformed.x *= taper;
|
||||
transformed.z *= taper;
|
||||
|
||||
float curveVal = fbm(vec2(gx, gz) * 0.5);
|
||||
float curveStrength = 2.0 + curveVal * 2.0;
|
||||
float curveAmount = uv.y * uv.y * curveStrength;
|
||||
vec2 curveDir = normalize(vec2(curveVal, fbm(vec2(gz, gx))) - 0.5);
|
||||
transformed.x += curveAmount * curveDir.x * 0.5;
|
||||
transformed.z += curveAmount * curveDir.y * 0.5;
|
||||
|
||||
float swayAmount = (totalWind + spring) * uv.y * uv.y;
|
||||
transformed.x += swayAmount * windDir.x;
|
||||
transformed.z += swayAmount * windDir.y;
|
||||
transformed.y -= abs(swayAmount) * 0.2;
|
||||
@@ -0,0 +1,279 @@
|
||||
import { useMemo, useRef } from "react";
|
||||
import { BufferAttribute, BufferGeometry, Color, Mesh } from "three";
|
||||
import { getTerrainHeight } from "./helpers";
|
||||
import Grass from "./grass";
|
||||
import { createNoise2D } from "simplex-noise";
|
||||
|
||||
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;
|
||||
grassCount: number;
|
||||
grassSize: number;
|
||||
grassLOD: number;
|
||||
terrainDryColor: string;
|
||||
terrainLushColor: string;
|
||||
grassDryColor: string;
|
||||
grassLushColor: string;
|
||||
grassBlades: number;
|
||||
grassSegments: number;
|
||||
grassLODStart: number;
|
||||
grassLODExponent: number;
|
||||
}
|
||||
function TerrainChunk({
|
||||
x,
|
||||
y,
|
||||
size,
|
||||
resolution,
|
||||
scale,
|
||||
hillScale,
|
||||
hillHeight,
|
||||
detailScale,
|
||||
detailHeight,
|
||||
noise2D,
|
||||
grassCount,
|
||||
grassSize,
|
||||
grassLOD,
|
||||
terrainDryColor,
|
||||
terrainLushColor,
|
||||
grassDryColor,
|
||||
grassLushColor,
|
||||
grassBlades,
|
||||
grassSegments,
|
||||
grassLODStart,
|
||||
grassLODExponent
|
||||
}: TerrainChunkProps) {
|
||||
const halfSize = size / 2;
|
||||
const worldXBase = x * size;
|
||||
const worldZBase = y * size;
|
||||
|
||||
const minX = Math.abs(worldXBase) <= halfSize ? 0 : Math.abs(worldXBase) - halfSize;
|
||||
const minZ = Math.abs(worldZBase) <= halfSize ? 0 : Math.abs(worldZBase) - halfSize;
|
||||
const chunkMinDist = Math.sqrt(minX * minX + minZ * minZ);
|
||||
|
||||
const shouldRenderGrass = chunkMinDist < grassLOD;
|
||||
|
||||
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 = new Color(terrainDryColor);
|
||||
const lushGreen = new Color(terrainLushColor);
|
||||
|
||||
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,
|
||||
terrainDryColor,
|
||||
terrainLushColor
|
||||
]);
|
||||
|
||||
return (
|
||||
<group>
|
||||
<mesh
|
||||
ref={meshRef}
|
||||
geometry={geometry}
|
||||
position={[x * size, 0, y * size]}
|
||||
>
|
||||
<meshStandardMaterial vertexColors roughness={1.0} metalness={0.0} />
|
||||
</mesh>
|
||||
{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}
|
||||
dryColor={grassDryColor}
|
||||
lushColor={grassLushColor}
|
||||
grassBlades={grassBlades}
|
||||
grassSegments={grassSegments}
|
||||
grassLODStart={grassLODStart}
|
||||
grassLODExponent={grassLODExponent}
|
||||
/>
|
||||
)}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
interface TerrainProps {
|
||||
chunks?: number;
|
||||
chunkSize?: number;
|
||||
resolution?: number;
|
||||
scale?: number;
|
||||
hillScale: number;
|
||||
hillHeight: number;
|
||||
detailScale: number;
|
||||
detailHeight: number;
|
||||
grassCount?: number;
|
||||
grassSize?: number;
|
||||
grassLOD?: number;
|
||||
terrainDryColor?: string;
|
||||
terrainLushColor?: string;
|
||||
grassDryColor?: string;
|
||||
grassLushColor?: string;
|
||||
grassBlades?: number;
|
||||
grassSegments?: number;
|
||||
grassLODStart?: number;
|
||||
grassLODExponent?: number;
|
||||
}
|
||||
export default function({
|
||||
chunks = 5,
|
||||
chunkSize = 10,
|
||||
resolution = 8,
|
||||
scale = 1,
|
||||
hillScale = 0.2,
|
||||
hillHeight = 3,
|
||||
detailScale = 3,
|
||||
detailHeight = 0.1,
|
||||
grassCount = 6000,
|
||||
grassSize = 0.6,
|
||||
grassLOD = 40,
|
||||
terrainDryColor = '#010100',
|
||||
terrainLushColor = '#000800',
|
||||
grassDryColor = '#556b19',
|
||||
grassLushColor = '#348a34',
|
||||
grassBlades = 2,
|
||||
grassSegments = 2,
|
||||
grassLODStart = 0.5,
|
||||
grassLODExponent = 1.0
|
||||
}: 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}
|
||||
grassCount={grassCount}
|
||||
grassSize={grassSize}
|
||||
grassLOD={grassLOD}
|
||||
terrainDryColor={terrainDryColor}
|
||||
terrainLushColor={terrainLushColor}
|
||||
grassDryColor={grassDryColor}
|
||||
grassLushColor={grassLushColor}
|
||||
grassBlades={grassBlades}
|
||||
grassSegments={grassSegments}
|
||||
grassLODStart={grassLODStart}
|
||||
grassLODExponent={grassLODExponent}
|
||||
/>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
+14
-14
@@ -74,24 +74,24 @@ body {
|
||||
animation: float 5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
|
||||
.main-frame::before,
|
||||
.main-frame::after {
|
||||
content: "✧";
|
||||
.decorative-sparkle {
|
||||
position: absolute;
|
||||
color: var(--sparkle);
|
||||
top: 10px;
|
||||
font-size: 1.5rem;
|
||||
text-shadow: 0 0 20px var(--sparkle-glow), 0 0 15px var(--sparkle-glow), 0 0 10px var(--sparkle-glow), 0 0 5px var(--sparkle-glow)
|
||||
color: var(--sparkle);
|
||||
text-shadow: 0 0 20px var(--sparkle-glow), 0 0 15px var(--sparkle-glow), 0 0 10px var(--sparkle-glow), 0 0 5px var(--sparkle-glow);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
cursor: url('/cur/kuromi-hearts.webp') 0 0, auto !important;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.main-frame::before {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.main-frame::after {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
.decorative-sparkle:hover {
|
||||
transform: scale(1.15) rotate(15deg);
|
||||
}
|
||||
|
||||
.content-box {
|
||||
|
||||
@@ -28,6 +28,9 @@ function Content() {
|
||||
return (
|
||||
<>
|
||||
<div className="main-frame">
|
||||
<a href="/niko" className="decorative-sparkle" title="✧" style={{ left: '10px' }}>✧</a>
|
||||
<a href="/img/boom.gif" className="decorative-sparkle" title="✧" style={{ right: '10px' }}>✧</a>
|
||||
|
||||
<header>
|
||||
<h1>neru</h1>
|
||||
<p className="motto">˚₊‧꒰ა 𓂋 ໒꒱ ‧₊˚</p>
|
||||
|
||||
Reference in New Issue
Block a user