diff --git a/public/textures/asphalt_02_Diffuse.jpg b/public/textures/asphalt_02_Diffuse.jpg new file mode 100644 index 0000000..d7a1b5e Binary files /dev/null and b/public/textures/asphalt_02_Diffuse.jpg differ diff --git a/public/textures/asphalt_02_arm.jpg b/public/textures/asphalt_02_arm.jpg new file mode 100644 index 0000000..a6c2fec Binary files /dev/null and b/public/textures/asphalt_02_arm.jpg differ diff --git a/public/textures/asphalt_02_nor_gl.jpg b/public/textures/asphalt_02_nor_gl.jpg new file mode 100644 index 0000000..19e3c72 Binary files /dev/null and b/public/textures/asphalt_02_nor_gl.jpg differ diff --git a/public/textures/concrete_pavement_03_Diffuse.jpg b/public/textures/concrete_pavement_03_Diffuse.jpg new file mode 100644 index 0000000..72a8ff8 Binary files /dev/null and b/public/textures/concrete_pavement_03_Diffuse.jpg differ diff --git a/public/textures/concrete_pavement_03_arm.jpg b/public/textures/concrete_pavement_03_arm.jpg new file mode 100644 index 0000000..69e97e2 Binary files /dev/null and b/public/textures/concrete_pavement_03_arm.jpg differ diff --git a/public/textures/concrete_pavement_03_nor_gl.jpg b/public/textures/concrete_pavement_03_nor_gl.jpg new file mode 100644 index 0000000..bf083f5 Binary files /dev/null and b/public/textures/concrete_pavement_03_nor_gl.jpg differ diff --git a/public/textures/grass_path_2_Diffuse.jpg b/public/textures/grass_path_2_Diffuse.jpg new file mode 100644 index 0000000..d28e534 Binary files /dev/null and b/public/textures/grass_path_2_Diffuse.jpg differ diff --git a/public/textures/grass_path_2_arm.jpg b/public/textures/grass_path_2_arm.jpg new file mode 100644 index 0000000..aff5004 Binary files /dev/null and b/public/textures/grass_path_2_arm.jpg differ diff --git a/public/textures/grass_path_2_nor_gl.jpg b/public/textures/grass_path_2_nor_gl.jpg new file mode 100644 index 0000000..c1f49fc Binary files /dev/null and b/public/textures/grass_path_2_nor_gl.jpg differ diff --git a/public/textures/leafy_grass_Diffuse.jpg b/public/textures/leafy_grass_Diffuse.jpg new file mode 100644 index 0000000..76abe0b Binary files /dev/null and b/public/textures/leafy_grass_Diffuse.jpg differ diff --git a/public/textures/leafy_grass_arm.jpg b/public/textures/leafy_grass_arm.jpg new file mode 100644 index 0000000..0961d9f Binary files /dev/null and b/public/textures/leafy_grass_arm.jpg differ diff --git a/public/textures/leafy_grass_nor_gl.jpg b/public/textures/leafy_grass_nor_gl.jpg new file mode 100644 index 0000000..5edb707 Binary files /dev/null and b/public/textures/leafy_grass_nor_gl.jpg differ diff --git a/public/textures/palm_bark_Diffuse.jpg b/public/textures/palm_bark_Diffuse.jpg new file mode 100644 index 0000000..a542a97 Binary files /dev/null and b/public/textures/palm_bark_Diffuse.jpg differ diff --git a/public/textures/palm_bark_arm.jpg b/public/textures/palm_bark_arm.jpg new file mode 100644 index 0000000..f708a9d Binary files /dev/null and b/public/textures/palm_bark_arm.jpg differ diff --git a/public/textures/palm_bark_nor_gl.jpg b/public/textures/palm_bark_nor_gl.jpg new file mode 100644 index 0000000..908cd59 Binary files /dev/null and b/public/textures/palm_bark_nor_gl.jpg differ diff --git a/public/textures/sand_01_Diffuse.jpg b/public/textures/sand_01_Diffuse.jpg new file mode 100644 index 0000000..e43d84e Binary files /dev/null and b/public/textures/sand_01_Diffuse.jpg differ diff --git a/public/textures/sand_01_arm.jpg b/public/textures/sand_01_arm.jpg new file mode 100644 index 0000000..c679db0 Binary files /dev/null and b/public/textures/sand_01_arm.jpg differ diff --git a/public/textures/sand_01_nor_gl.jpg b/public/textures/sand_01_nor_gl.jpg new file mode 100644 index 0000000..bca049a Binary files /dev/null and b/public/textures/sand_01_nor_gl.jpg differ diff --git a/public/textures/white_stucco_Diffuse.jpg b/public/textures/white_stucco_Diffuse.jpg new file mode 100644 index 0000000..b480ce6 Binary files /dev/null and b/public/textures/white_stucco_Diffuse.jpg differ diff --git a/public/textures/white_stucco_arm.jpg b/public/textures/white_stucco_arm.jpg new file mode 100644 index 0000000..172bd26 Binary files /dev/null and b/public/textures/white_stucco_arm.jpg differ diff --git a/public/textures/white_stucco_nor_gl.jpg b/public/textures/white_stucco_nor_gl.jpg new file mode 100644 index 0000000..199bb54 Binary files /dev/null and b/public/textures/white_stucco_nor_gl.jpg differ diff --git a/src/main.js b/src/main.js index 5545e86..b266e4f 100644 --- a/src/main.js +++ b/src/main.js @@ -1,9 +1,11 @@ -import { ACESFilmicToneMapping, AmbientLight, DirectionalLight, MeshStandardMaterial, - PerspectiveCamera, Scene, Timer, Vector3, WebGLRenderer } from "three"; +import { ACESFilmicToneMapping, AmbientLight, BoxGeometry, DirectionalLight, Fog, Mesh, + PerspectiveCamera, PlaneGeometry, Scene, Timer, Vector3, WebGLRenderer } from "three"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; +import { Sky } from "three/examples/jsm/objects/Sky.js"; import { playerController } from "three-player-controller"; import { ChunkStreamer } from "./world/streaming.js"; +import { createMaterials } from "./world/materials.js"; const container = document.querySelector("#container"); const scene = new Scene(); @@ -23,18 +25,25 @@ sun.position.set(60, 120, 40); sun.castShadow = true; scene.add(sun); -// Ein Material je Schicht aus buildChunkGeometry() (Task 7). buildings trägt die dort erzeugten -// Vertexfarben (Pastellton pro Gebäude) und bleibt deshalb weiß grundiert; die übrigen Schichten -// sind reine Auflagen/Landmassen ohne Vertexfarben und bekommen je einen flachen Grundton. -const materials = { - ground: new MeshStandardMaterial({ color: 0x6d7a52, roughness: 1 }), - roads: new MeshStandardMaterial({ color: 0x3a3a3d, roughness: 0.9 }), - bridges: new MeshStandardMaterial({ color: 0x8a8a8a, roughness: 0.8 }), - buildings: new MeshStandardMaterial({ vertexColors: true, roughness: 0.85 }), - sand: new MeshStandardMaterial({ color: 0xd9c48c, roughness: 1 }), - park: new MeshStandardMaterial({ color: 0x4f7a3d, roughness: 1 }), - urban: new MeshStandardMaterial({ color: 0xa8a8a0, roughness: 0.9 }), -}; +// Ein Material je Schicht aus buildChunkGeometry() (Task 7), jetzt mit echten Poly-Haven- +// Texturen statt Flächenfarben (Task 9, src/world/materials.js). buildings trägt zusätzlich +// die dort erzeugten Vertexfarben (Pastellton pro Gebäude) über der Stuckputz-Textur. +const materials = await createMaterials(); + +// Himmel und Nebel über der Szene; sunPosition zeigt in dieselbe Richtung wie das Sonnenlicht. +const sky = new Sky(); +sky.scale.setScalar(45000); +scene.add(sky); +sky.material.uniforms.turbidity.value = 8; +sky.material.uniforms.rayleigh.value = 1.6; +sky.material.uniforms.sunPosition.value.copy(sun.position.clone().normalize()); +scene.fog = new Fog(0xbcd7ea, 500, 2200); + +// Meer rund um den Ausschnitt: eine einzige große Ebene unter/um die Chunks, keine je Chunk. +const water = new Mesh(new PlaneGeometry(4000, 4000), materials.water); +water.rotation.x = -Math.PI / 2; +water.position.set(500, -0.6, -1000); +scene.add(water); // Vice Beach; Task 8, Schritt 5. Auch initPos, damit der Streamer von Anfang an den richtigen // Chunk anfragt statt erst nach dem späteren reset() umzuschwenken. @@ -53,6 +62,24 @@ await player.init({ }, }); +// Meeresboden: man watet hinein, statt endlos zu fallen. Einmalig, nicht je Chunk. +const seafloor = new Mesh(new BoxGeometry(4000, 1, 4000), materials.seafloor); +seafloor.position.set(500, -3.0, -1000); // Oberkante bei y = -2,5 +scene.add(seafloor); +seafloor.updateMatrixWorld(true); +player.addCollider({ motion: "static", shape: { kind: "mesh", source: seafloor } }); + +// Schattenkamera folgt dem Spieler, statt eine feste Box zu benutzen. +function updateShadow(target) { + sun.position.set(target.x + 120, 240, target.z + 80); + sun.target.position.copy(target); + sun.target.updateMatrixWorld(); + sun.shadow.camera.left = -150; sun.shadow.camera.right = 150; + sun.shadow.camera.top = 150; sun.shadow.camera.bottom = -150; + sun.shadow.camera.far = 600; + sun.shadow.camera.updateProjectionMatrix(); +} + // Chunk-Collider treffen erst nach und nach ein (Fetch + asynchroner BVH-Bau, siehe // streaming.js). setSkipCapsuleCollision(true) schaltet nur das horizontale Herausdrücken // ab; applyGravity läuft im Controller unabhängig davon weiter (CharacterMovement.ts), die @@ -99,6 +126,8 @@ renderer.setAnimationLoop((t) => { const delta = Math.min(timer.getDelta(), 1 / 40); player.update(delta); tick(delta); + updateShadow(player.playerCapsule.position); + materials.water.normalMap.offset.x += delta * 0.01; renderer.render(scene, camera); }); addEventListener("resize", () => { diff --git a/src/world/materials.js b/src/world/materials.js new file mode 100644 index 0000000..50751e1 --- /dev/null +++ b/src/world/materials.js @@ -0,0 +1,72 @@ +import { DataTexture, MeshStandardMaterial, RepeatWrapping, RGBAFormat, SRGBColorSpace, TextureLoader } from "three"; + +export const MATERIAL_KEYS = ["ground", "sand", "park", "urban", "roads", "bridges", "buildings"]; +export const TEXTURES = { + ground: "grass_path_2", // unbebautes Land + sand: "sand_01", // Strand + park: "leafy_grass", // Grünflächen + urban: "concrete_pavement_03", // versiegelte Stadtfläche + roads: "asphalt_02", + bridges: "concrete_pavement_03", + buildings: "white_stucco", +}; +const FALLBACK_COLORS = { + ground: 0xa8b878, sand: 0xe8d888, park: 0x8bc34a, urban: 0xd8d8d8, + roads: 0x505050, bridges: 0xb0b0b0, buildings: 0xe8e4dc, +}; + +/** Kachelbare Wellen-Normalmap, weil Poly Haven keine Wassertextur hat. */ +export function waterNormalData(size = 128) { + const data = new Uint8Array(size * size * 4); + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + const i = (y * size + x) * 4; + const u = (x / size) * Math.PI * 2; + const v = (y / size) * Math.PI * 2; + const dx = Math.sin(u * 3) * 0.5 + Math.sin(v * 2 + 1.3) * 0.3; + const dz = Math.cos(v * 3) * 0.5 + Math.cos(u * 2 + 0.7) * 0.3; + data[i] = 128 + dx * 40; + data[i + 1] = 128 + dz * 40; + data[i + 2] = 245; + data[i + 3] = 255; + } + } + return data; +} + +async function loadSet(loader, slug, fallback) { + let map, normalMap, arm; + try { + [map, normalMap, arm] = await Promise.all( + ["Diffuse", "nor_gl", "arm"].map((m) => loader.loadAsync(`./textures/${slug}_${m}.jpg`)), + ); + } catch (err) { + console.warn(`Textur ${slug} fehlt, einfarbiger Ersatz:`, err); + return new MeshStandardMaterial({ color: fallback, roughness: 0.9 }); + } + for (const t of [map, normalMap, arm]) { + t.wrapS = t.wrapT = RepeatWrapping; + } + map.colorSpace = SRGBColorSpace; + // arm packt Ambient Occlusion, Rauheit und Metallgrad in R, G und B. + return new MeshStandardMaterial({ map, normalMap, aoMap: arm, roughnessMap: arm, metalnessMap: arm, metalness: 0 }); +} + +export async function createMaterials() { + const loader = new TextureLoader(); + const entries = await Promise.all( + MATERIAL_KEYS.map(async (key) => [key, await loadSet(loader, TEXTURES[key], FALLBACK_COLORS[key])]), + ); + const materials = Object.fromEntries(entries); + materials.buildings.vertexColors = true; // pastellige Tönung je Gebäude + + const normal = new DataTexture(waterNormalData(128), 128, 128, RGBAFormat); + normal.wrapS = normal.wrapT = RepeatWrapping; + normal.repeat.set(60, 60); + normal.needsUpdate = true; + materials.water = new MeshStandardMaterial({ + color: 0x2c67a4, normalMap: normal, roughness: 0.15, metalness: 0.2, transparent: true, opacity: 0.92, + }); + materials.seafloor = new MeshStandardMaterial({ color: 0x1d4468, roughness: 1 }); + return materials; +} diff --git a/tests/materials.test.js b/tests/materials.test.js new file mode 100644 index 0000000..e996f9a --- /dev/null +++ b/tests/materials.test.js @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { MATERIAL_KEYS, TEXTURES, waterNormalData } from "../src/world/materials.js"; +import { buildChunkGeometry } from "../src/world/geometry.js"; + +describe("materials", () => { + it("deckt genau die Geometrie-Teile ab", () => { + const chunk = { chunk: [0, 0], origin: [0, 0], land: [], roads: [], centerlines: [], buildings: [], palms: [] }; + expect(Object.keys(buildChunkGeometry(chunk)).sort()).toEqual([...MATERIAL_KEYS].sort()); + }); + + it("nennt für jedes Material eine Textur", () => { + for (const key of MATERIAL_KEYS) expect(TEXTURES[key]).toBeTruthy(); + }); + + it("deckt Strand und Park mit eigenen Materialien ab", () => { + expect(MATERIAL_KEYS).toContain("sand"); + expect(MATERIAL_KEYS).toContain("park"); + }); + + it("erzeugt eine Wellen-Normalmap mit neutraler Grundfarbe", () => { + const data = waterNormalData(32); + expect(data.length).toBe(32 * 32 * 4); + // Normalen zeigen im Mittel nach oben: Blaukanal nahe 255. + const blue = [...data].filter((_, i) => i % 4 === 2); + expect(blue.reduce((a, b) => a + b, 0) / blue.length).toBeGreaterThan(200); + }); +}); diff --git a/tools/assets/textures.mjs b/tools/assets/textures.mjs new file mode 100644 index 0000000..198d1a7 --- /dev/null +++ b/tools/assets/textures.mjs @@ -0,0 +1,18 @@ +// Lädt die benötigten Poly-Haven-Texturen in 1k nach public/textures/. +import { mkdir, writeFile } from "node:fs/promises"; + +const UA = { "User-Agent": "leonida-assets/0.1 (local hobby project)" }; +const SLUGS = ["asphalt_02", "concrete_pavement_03", "white_stucco", "sand_01", "leafy_grass", "grass_path_2", "palm_bark"]; +const MAPS = ["Diffuse", "nor_gl", "arm"]; + +await mkdir("public/textures", { recursive: true }); +for (const slug of SLUGS) { + const files = await (await fetch(`https://api.polyhaven.com/files/${slug}`, { headers: UA })).json(); + for (const map of MAPS) { + const entry = files[map]?.["1k"]?.jpg; + if (!entry) { console.warn(`${slug}: ${map} fehlt`); continue; } + const buf = Buffer.from(await (await fetch(entry.url, { headers: UA })).arrayBuffer()); + await writeFile(`public/textures/${slug}_${map}.jpg`, buf); + console.log(`${slug}_${map}.jpg ${(buf.length / 1e6).toFixed(1)} MB`); + } +}