Compare commits

...

50 Commits

Author SHA1 Message Date
neru 2e7ce54672 feat: add icon and webmanifest 2026-01-05 01:46:03 -03:00
neru ac0eea917e style: run format:apply 2026-01-03 07:21:10 -03:00
neru 31ca429ed9 style: unwrap obj 2026-01-03 07:20:33 -03:00
neru deaafc9e22 feat: move seal page to its own file 2026-01-03 06:42:19 -03:00
neru 7dc2fc2568 fix: missing format on import 2026-01-02 07:49:38 -03:00
neru b8e51d1544 style: run format:apply 2026-01-02 07:45:03 -03:00
neru 429ee073e3 style: fix all lint warnings 2026-01-02 07:44:45 -03:00
neru e52745ead2 style: cleanup unused stuff 2026-01-02 07:37:39 -03:00
neru f1f05590fc feat: redo wind shader 2026-01-02 07:34:28 -03:00
neru 211e2128be style: cc adjustments 2026-01-02 05:41:04 -03:00
neru aa0c1866e6 feat: change lut 2026-01-02 05:39:32 -03:00
neru 7b5a0941cb feat: upload LUT 2026-01-02 05:36:15 -03:00
neru f541b1b8ce feat: add LUT + CC 2026-01-02 05:36:09 -03:00
neru 67457e252e style: run format:apply 2026-01-02 04:58:34 -03:00
neru 4148f2729b feat: add loading screen 2026-01-02 04:57:51 -03:00
neru 1e5b9c8884 feat: add loading effect 2026-01-02 04:52:30 -03:00
neru a2e4637c8d style: run format:apply 2026-01-02 04:32:41 -03:00
neru b5b89d5d6b style: dof adjustements 2026-01-02 03:55:54 -03:00
neru 27cb73a17c style: reduce focal length (again !!) 2026-01-02 03:54:29 -03:00
neru 06edc62214 fix: dof wrong target, low focal length 2026-01-02 03:53:54 -03:00
neru dfdf017835 fix: write depth on cube mat 2026-01-02 03:53:34 -03:00
neru f0a0e188fc style: misc cleanup 2026-01-02 03:51:45 -03:00
neru 57c064ea00 style: run format:apply pass 2026-01-02 03:49:12 -03:00
neru 1c70ffd80d style: reduce grass instance count 2026-01-02 03:44:54 -03:00
neru 9301f3a8a0 feat: re-enable dof 2026-01-02 03:44:22 -03:00
neru 0e2daee849 fix: offset cube to avoid clipping with terrain 2026-01-02 03:44:14 -03:00
neru 240db3f764 fix: bad terrain mat params 2026-01-02 03:44:00 -03:00
neru 74c6963290 fix: restore terrain parameters 2026-01-02 03:41:02 -03:00
neru 2b8202336a style: remove unused comment 2026-01-02 03:40:37 -03:00
neru 669a0356e0 style: reduce curve strength 2026-01-02 03:38:19 -03:00
neru dd249aabbd feat: increment grass segments, add extra taper at ends 2026-01-02 03:37:45 -03:00
neru 35f3c74663 feat: add SMAA 2026-01-02 02:22:54 -03:00
neru 0d097aa643 style: touch up grass colours 2026-01-02 02:22:50 -03:00
neru 590711db49 feat: rewrite grass LOD system and visuals 2026-01-02 02:15:34 -03:00
neru 6187dfa6f7 style: visual param tweaks 2026-01-02 01:26:37 -03:00
neru 76450e3113 style: run format:apply 2026-01-02 01:24:30 -03:00
neru e40108e62c style: increase granularity 2026-01-02 01:24:18 -03:00
neru 9d2526d77f style: tweak grass colours 2026-01-02 01:23:19 -03:00
neru 0ba6446757 feat: add godrays 2026-01-02 01:23:07 -03:00
neru b6a7ca09c9 fix: create ref for SealCube 2026-01-02 01:22:52 -03:00
neru 486e5ca1ba fix: reimplement SealCube as fwdRef 2026-01-02 01:22:32 -03:00
neru 6d21c9a94c style: misc fx tweaks 2026-01-02 01:12:43 -03:00
neru 5579308470 style: misc visual changes 2026-01-02 01:11:27 -03:00
neru 05dd49cbe4 fix: make sealcube tinier 2026-01-02 01:11:12 -03:00
neru c33d61f765 feat: add dof 2026-01-02 01:11:00 -03:00
neru f7a3539852 feat: add env 2026-01-02 01:10:46 -03:00
neru 8b1706b8fe style: format with prettier 2026-01-02 00:40:01 -03:00
neru 76209cfcd7 feat: add postprocessing 2026-01-02 00:39:28 -03:00
neru 801bc775f7 feat: add grass and terrain 2026-01-02 00:37:39 -03:00
neru e4a1e3cfd2 chore: add leva and simplex-noise 2026-01-02 00:03:22 -03:00
19 changed files with 34594 additions and 165 deletions
+999 -82
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -14,10 +14,12 @@
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.4.2",
"@react-three/postprocessing": "^3.0.4",
"leva": "^0.10.1",
"next": "16.1.1",
"prettier": "^3.7.4",
"react": "19.2.3",
"react-dom": "19.2.3",
"simplex-noise": "^4.0.3",
"three": "^0.182.0"
},
"devDependencies": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "⛧",
"short_name": "⛧",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
+23 -3
View File
@@ -12,8 +12,28 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang='en'>
<body className={`antialiased`}>{children}</body>
</html>
<>
<link
rel='apple-touch-icon'
sizes='180x180'
href='/apple-touch-icon.png'
/>
<link
rel='icon'
type='image/png'
sizes='32x32'
href='/favicon-32x32.png'
/>
<link
rel='icon'
type='image/png'
sizes='16x16'
href='/favicon-16x16.png'
/>
<link rel='manifest' href='/site.webmanifest' />
<html lang='en'>
<body className={`antialiased`}>{children}</body>
</html>
</>
);
}
+42
View File
@@ -2,3 +2,45 @@
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);
}
}
+2 -80
View File
@@ -1,86 +1,8 @@
'use client';
import { OrbitControls } from '@react-three/drei';
import { Canvas, useFrame, useLoader } from '@react-three/fiber';
import {
Bloom,
EffectComposer,
GodRays,
Noise
} from '@react-three/postprocessing';
import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { Mesh, TextureLoader } from 'three';
import './page.css';
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} castShadow receiveShadow {...props}>
<boxGeometry args={[2.5, 2.5, 2.5]} />
<meshBasicMaterial map={texture} />
</mesh>
);
});
SealCube.displayName = 'SealCube';
import SealHome from './pages/seal';
export default function Home() {
const [sunMesh, setSunMesh] = useState<Mesh | null>(null);
return (
<Canvas
shadows
camera={{ position: [5, 5, 5], fov: 50 }}
gl={{ antialias: true }}
className='canvas'
>
<SealCube
ref={(mesh) => {
if (mesh && !sunMesh) {
setSunMesh(mesh);
}
}}
/>
<EffectComposer>
<Noise opacity={0.1} />
<Bloom
intensity={2}
luminanceThreshold={0.5}
luminanceSmoothing={0.1}
/>
{
sunMesh ? (
<GodRays
sun={sunMesh}
samples={30}
density={1.5}
decay={0.8}
weight={0.2}
exposure={0.5}
clampMax={1}
blur={true}
/>
) : (
<></>
)
// it feels genuinely so cursed to have to do : <></> just because EffectComposer can't handle null
}
</EffectComposer>
<OrbitControls />
</Canvas>
);
return <SealHome />;
}
+684
View File
@@ -0,0 +1,684 @@
'use client';
import {
forwardRef,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
useState
} from 'react';
import {
Bloom,
EffectComposer,
Noise,
Vignette,
DepthOfField,
SMAA,
HueSaturation,
BrightnessContrast,
LUT
} from '@react-three/postprocessing';
import { Environment, OrbitControls, useProgress } from '@react-three/drei';
import { Canvas, useLoader, useFrame } from '@react-three/fiber';
import {
BufferAttribute,
BufferGeometry,
Mesh,
Object3D,
InstancedMesh,
DoubleSide,
TextureLoader,
Color,
MeshStandardMaterial
} from 'three';
import { LUTCubeLoader } from 'three/examples/jsm/loaders/LUTCubeLoader.js';
import { createNoise2D } from 'simplex-noise';
import grassVert from './shaders/grass.vert';
import grassFrag from './shaders/grass.frag';
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');
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;
}
},
[]
);
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;
grassCount: number;
grassSize: number;
grassLOD: number;
}
function TerrainChunk({
x,
y,
size,
resolution,
scale,
hillScale,
hillHeight,
detailScale,
detailHeight,
noise2D,
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} />
</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}
/>
)}
</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;
}
function Terrain({
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
}: 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}
/>
))}
</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} depthWrite={true} />
</mesh>
);
});
SealCube.displayName = 'SealCube';
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 alt='niko!!' src='/img/niko.jpg' className='niko-spin' />
</picture>
</div>
);
}
function LutEffect() {
const lutTexture = useLoader(LUTCubeLoader, '/lut/Landscape6.cube');
return <LUT lut={lutTexture.texture3D} />;
}
export default function SealHome() {
return (
<>
<Loader />
<Canvas
shadows
camera={{ position: [0, 5, 15], fov: 50, far: 100 }}
gl={{ antialias: true }}
className='canvas'
>
<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} />
<LutEffect />
</EffectComposer>
<Environment
files={'hdr/sky.hdr'}
environmentIntensity={1}
background
/>
<fogExp2 attach='fog' args={[0x9a9a9a, 0.01]} />
<Terrain
chunks={16}
chunkSize={10.0}
resolution={8.0}
scale={1}
hillScale={0.1}
hillHeight={6.0}
detailScale={1.0}
detailHeight={0.2}
grassCount={6000}
grassSize={0.6}
grassLOD={60}
/>
<SealCube />
<OrbitControls
target={[0, 3, 0]}
enablePan={false}
makeDefault
minDistance={2}
maxDistance={10}
/>
</Canvas>
</>
);
}
+13
View File
@@ -0,0 +1,13 @@
float ao = smoothstep(0.0, 0.4, vGrassUv.y);
ao = mix(0.2, 1.0, ao); // Roots are 20% brightness
vec3 rootColor = diffuseColor.rgb * 0.4;
vec3 tipColor = diffuseColor.rgb * 1.3 + vec3(0.1, 0.1, 0.0);
vec3 grassColor = mix(rootColor, tipColor, vGrassUv.y);
grassColor *= ao;
float translucency = pow(vGrassUv.y, 2.0) * 0.5;
grassColor += vec3(0.1, 0.2, 0.0) * translucency;
diffuseColor.rgb = grassColor;
+37
View File
@@ -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;