diff --git a/assets/skybox/default/back.png b/assets/skybox/default/back.png new file mode 100644 index 0000000..6dea976 Binary files /dev/null and b/assets/skybox/default/back.png differ diff --git a/assets/skybox/default/bottom.png b/assets/skybox/default/bottom.png new file mode 100644 index 0000000..ffd848e Binary files /dev/null and b/assets/skybox/default/bottom.png differ diff --git a/assets/skybox/default/front.png b/assets/skybox/default/front.png new file mode 100644 index 0000000..b4f5fb7 Binary files /dev/null and b/assets/skybox/default/front.png differ diff --git a/assets/skybox/default/left.png b/assets/skybox/default/left.png new file mode 100644 index 0000000..23a1de2 Binary files /dev/null and b/assets/skybox/default/left.png differ diff --git a/assets/skybox/default/right.png b/assets/skybox/default/right.png new file mode 100644 index 0000000..8ce890b Binary files /dev/null and b/assets/skybox/default/right.png differ diff --git a/assets/skybox/default/top.png b/assets/skybox/default/top.png new file mode 100644 index 0000000..0833cc8 Binary files /dev/null and b/assets/skybox/default/top.png differ diff --git a/assets/terrain/manifest.json b/assets/terrain/manifest.json new file mode 100644 index 0000000..f437960 --- /dev/null +++ b/assets/terrain/manifest.json @@ -0,0 +1,25 @@ +{ + "worldWidth": 1, + "worldHeight": 1, + "worldChunkSize": 256, + "worldHeightScale": 16, + "textureBombingNoise": "simplex-noise.png", + "textureSplattingSources": [ + "grass-flowers.png", + "grassy.png", + "mud.png", + "path.png" + ], + "regionMap": [ + { + "x": 0, + "y": 0, + "splat": [ + "grassy.png", + "mud.png", + "grass-flowers.png", + "path.png" + ] + } + ] +} diff --git a/assets/terrain/height-0-0.png b/assets/terrain/region/height-0-0.png similarity index 100% rename from assets/terrain/height-0-0.png rename to assets/terrain/region/height-0-0.png diff --git a/assets/terrain/splat-0-0.png b/assets/terrain/region/splat-0-0.png similarity index 100% rename from assets/terrain/splat-0-0.png rename to assets/terrain/region/splat-0-0.png diff --git a/src/client/game.ts b/src/client/game.ts index 9531bca..6554bcf 100644 --- a/src/client/game.ts +++ b/src/client/game.ts @@ -6,12 +6,14 @@ import { IcyNetUser } from '../common/types/user'; import { ThirdPersonCamera } from './object/camera'; import { Chat } from './object/chat'; import { Joystick } from './object/joystick'; +import { CubeMap } from './object/other/cubemap'; import { VideoPlayer } from './object/other/video-player'; import { Player } from './object/player'; import { PlayerEntity } from './object/player-entity'; import modelLoaderInstance from './object/pony-loader'; import { ClientWorld } from './object/world/ClientWorld'; import { ClientWorldLoader } from './object/world/ClientWorldLoader'; +import { ClientWorldManifest } from './object/world/ClientWorldManifest'; import { Renderer } from './renderer'; export class Game { @@ -24,7 +26,7 @@ export class Game { private character: CharacterPacket = {}; private party: string[] = []; - public world = new ClientWorld(new ClientWorldLoader()); + public world!: ClientWorld; public renderer = new Renderer(); private videoTest = new VideoPlayer(24, 12); @@ -32,6 +34,11 @@ export class Game { constructor(public socket: Socket) {} async initialize(): Promise { + const worldManifest = await ClientWorldManifest.loadManifest(); + this.world = new ClientWorld(new ClientWorldLoader(), worldManifest); + + const cube = await CubeMap.load('/assets/skybox/default'); + await modelLoaderInstance.loadPonyModel(); await this.world.initialize(); @@ -43,7 +50,8 @@ export class Game { // experimental this.videoTest.initialize(); - this.videoTest.mesh.position.set(0, 6, -20); + this.videoTest.mesh.position.set(0, 14, 20); + this.videoTest.mesh.rotateY(Math.PI / 2); this.renderer.scene.add(this.videoTest.mesh); this.party = (localStorage.getItem('party')?.split('|') || []).filter( (item) => item, @@ -64,6 +72,7 @@ export class Game { }); this.renderer.scene.add(this.world.world); + this.renderer.scene.background = cube.texture; } public dispose() { diff --git a/src/client/object/other/cubemap.ts b/src/client/object/other/cubemap.ts new file mode 100644 index 0000000..143d585 --- /dev/null +++ b/src/client/object/other/cubemap.ts @@ -0,0 +1,25 @@ +import { CubeTextureLoader, CubeTexture } from 'three'; + +const loader = new CubeTextureLoader(); + +export class CubeMap { + constructor(public source: string, public texture: CubeTexture) {} + + public static async load(base: string): Promise { + return new Promise((resolve, reject) => + loader.load( + [ + `${base}/left.png`, // pos-x + `${base}/right.png`, // neg-x + `${base}/top.png`, // pos-y + `${base}/bottom.png`, // neg-y + `${base}/front.png`, // pos-z + `${base}/back.png`, // neg-z + ], + (texture) => resolve(new CubeMap(base, texture)), + undefined, + reject, + ), + ); + } +} diff --git a/src/client/object/world/ClientWorld.ts b/src/client/object/world/ClientWorld.ts index 6ebc43a..09ed558 100644 --- a/src/client/object/world/ClientWorld.ts +++ b/src/client/object/world/ClientWorld.ts @@ -8,6 +8,8 @@ import { ClientWorldTexture } from './ClientWorldTexture'; // TODO: distance loading // TODO: LOD +const BASE = '/assets/terrain/'; + export class ClientWorld extends WorldManager { public world = new Object3D(); private _mesher = new ClientWorldMesher(); @@ -28,19 +30,19 @@ export class ClientWorld extends WorldManager { async initialize() { await this.loadWorld(); + + const noiseFile = `${BASE}/texture/${this.manifest.textureBombingNoise}`; + await this.loadTextureList([ + noiseFile, ...this._chunks.map( - (chunk) => `/assets/terrain/splat-${chunk.x}-${chunk.y}.png`, + (chunk) => `${BASE}/region/splat-${chunk.x}-${chunk.y}.png`, ), - '/assets/terrain/texture/simplex-noise.png', - '/assets/terrain/texture/grassy.png', - '/assets/terrain/texture/mud.png', - '/assets/terrain/texture/grass-flowers.png', - '/assets/terrain/texture/path.png', + ...this.gatherManifestTextures(), ]); - this._shader.initialize( - this._worldTextures.get('/assets/terrain/texture/simplex-noise.png'), - ); + + this._shader.initialize(); + this._shader.setNoise(this._worldTextures.get(noiseFile)); this.createMeshes(); } @@ -50,7 +52,6 @@ export class ClientWorld extends WorldManager { } const tex = await ClientWorldTexture.loadTexture(src); - // tex.texture.repeat.set(this.worldChunkSize, this.worldChunkSize); this._worldTextures.set(src, tex); return tex; } @@ -66,13 +67,8 @@ export class ClientWorld extends WorldManager { const chunk = this._chunkMeshQueue.shift(); const material = this._shader.getShader( chunk, - `/assets/terrain/splat-${chunk.x}-${chunk.y}.png`, - [ - '/assets/terrain/texture/grassy.png', - '/assets/terrain/texture/mud.png', - '/assets/terrain/texture/grass-flowers.png', - '/assets/terrain/texture/path.png', - ], + `${BASE}/region/splat-${chunk.x}-${chunk.y}.png`, + chunk.region.splat.map((file) => `${BASE}/texture/${file}`), ); const mesh = this._mesher.createTerrainMesh( @@ -92,4 +88,11 @@ export class ClientWorld extends WorldManager { this._chunkMeshQueue.push(chunk); }); } + + private gatherManifestTextures(): string[] { + return this.manifest.regionMap.reduce((list, entry) => { + const paths = entry.splat.map((file) => `${BASE}/texture/${file}`); + return [...list, ...paths.filter((path) => !list.includes(path))]; + }, []); + } } diff --git a/src/client/object/world/ClientWorldChunkShader.ts b/src/client/object/world/ClientWorldChunkShader.ts index 469f390..7700bac 100644 --- a/src/client/object/world/ClientWorldChunkShader.ts +++ b/src/client/object/world/ClientWorldChunkShader.ts @@ -169,7 +169,7 @@ export class ClientWorldChunkShader { constructor(public textureList: Map) {} - initialize(noise: ClientWorldTexture) { + initialize() { this.shader = new ShaderMaterial({ vertexShader: vertex, fragmentShader: fragment, @@ -185,7 +185,7 @@ export class ClientWorldChunkShader { UniformsLib.lights, { backgroundTex: { value: null, type: 't' }, - noiseTex: { value: noise.texture, type: 't' }, + noiseTex: { value: null, type: 't' }, rTex: { value: null, type: 't' }, gTex: { value: null, type: 't' }, bTex: { value: null, type: 't' }, @@ -196,6 +196,10 @@ export class ClientWorldChunkShader { }); } + public setNoise(noise: ClientWorldTexture) { + this.shader.uniforms.noiseTex.value = noise.texture; + } + public getShader( chunk: WorldChunk, splatMap: string, diff --git a/src/client/object/world/ClientWorldLoader.ts b/src/client/object/world/ClientWorldLoader.ts index 94caa84..7a0d8aa 100644 --- a/src/client/object/world/ClientWorldLoader.ts +++ b/src/client/object/world/ClientWorldLoader.ts @@ -3,14 +3,18 @@ import { to1D } from '../../../common/convert'; import { WorldLoader } from '../../../common/world/WorldLoader'; const loader = new ImageLoader(); -const worldPath = '/assets/terrain/'; +const worldPath = '/assets/terrain/region/'; export class ClientWorldLoader implements WorldLoader { - async loadHeightMap(chunkX: number, chunkY: number): Promise { + async loadHeightMap( + chunkX: number, + chunkY: number, + scale: number, + ): Promise { return new Promise((resolve, reject) => { loader.load( `${worldPath}/height-${chunkX}-${chunkY}.png`, - (data) => resolve(ClientWorldLoader.heightFromImage(data)), + (data) => resolve(ClientWorldLoader.heightFromImage(data, scale)), undefined, (err) => { reject(err); @@ -19,7 +23,10 @@ export class ClientWorldLoader implements WorldLoader { }); } - public static heightFromImage(image: HTMLImageElement): number[] { + public static heightFromImage( + image: HTMLImageElement, + scale: number, + ): number[] { const array = new Array(image.width * image.height); const ctx = document.createElement('canvas').getContext('2d'); ctx.canvas.width = image.width; @@ -31,7 +38,7 @@ export class ClientWorldLoader implements WorldLoader { for (let x = 0; x < image.width; x++) { for (let y = 0; y < image.height; y++) { const index = to1D(x, y, image.width); - array[index] = (data.data[index * 4] * 32) / 255; + array[index] = (data.data[index * 4] * scale) / 255; } } diff --git a/src/client/object/world/ClientWorldManifest.ts b/src/client/object/world/ClientWorldManifest.ts new file mode 100644 index 0000000..f29d4a2 --- /dev/null +++ b/src/client/object/world/ClientWorldManifest.ts @@ -0,0 +1,32 @@ +import { + WorldManifest, + WorldManifestRegion, +} from '../../../common/world/WorldManifest'; + +export class ClientWorldManifest implements WorldManifest { + constructor( + public worldWidth: number, + public worldHeight: number, + public worldChunkSize: number, + public worldHeightScale: number, + public textureBombingNoise: string, + public textureSplattingSources: string[], + public regionMap: WorldManifestRegion[], + ) {} + + public static async loadManifest(): Promise { + const file = await fetch('/assets/terrain/manifest.json'); + const json = await file.json(); + const manifest = new ClientWorldManifest( + json.worldWidth, + json.worldHeight, + json.worldChunkSize, + json.worldHeightScale, + json.textureBombingNoise, + json.textureSplattingSources, + json.regionMap, + ); + + return manifest; + } +} diff --git a/src/common/world/WorldChunk.ts b/src/common/world/WorldChunk.ts index 9d16b2e..49fdc0e 100644 --- a/src/common/world/WorldChunk.ts +++ b/src/common/world/WorldChunk.ts @@ -1,6 +1,7 @@ import { Vector3, Vector2 } from 'three'; import { to1D } from '../convert'; import { barycentricPoint } from '../helper'; +import { WorldManifestRegion } from './WorldManifest'; export class WorldChunk { constructor( @@ -8,6 +9,7 @@ export class WorldChunk { public x: number, public y: number, public size: number, + public region: WorldManifestRegion, public scaledSize = size, ) {} diff --git a/src/common/world/WorldLoader.ts b/src/common/world/WorldLoader.ts index c41628f..702e8ea 100644 --- a/src/common/world/WorldLoader.ts +++ b/src/common/world/WorldLoader.ts @@ -1,3 +1,7 @@ export interface WorldLoader { - loadHeightMap: (chunkX: number, chunkY: number) => Promise; + loadHeightMap: ( + chunkX: number, + chunkY: number, + scale: number, + ) => Promise; } diff --git a/src/common/world/WorldManager.ts b/src/common/world/WorldManager.ts index c528c7d..e8ebaa6 100644 --- a/src/common/world/WorldManager.ts +++ b/src/common/world/WorldManager.ts @@ -1,25 +1,40 @@ import { to1D } from '../convert'; import { WorldChunk } from './WorldChunk'; import { WorldLoader } from './WorldLoader'; +import { WorldManifest } from './WorldManifest'; export class WorldManager { protected _chunks!: WorldChunk[]; - constructor( - public loader: WorldLoader, - public worldWidth = 1, - public worldHeight = 1, - public worldChunkSize = 256, - ) { + constructor(public loader: WorldLoader, public manifest: WorldManifest) { this._chunks = new Array(this.worldWidth * this.worldHeight); } + public get worldWidth() { + return this.manifest.worldWidth; + } + + public get worldHeight() { + return this.manifest.worldHeight; + } + + public get worldChunkSize() { + return this.manifest.worldChunkSize; + } + async loadHeightData(chunkX: number, chunkY: number) { - const heightData = await this.loader.loadHeightMap(chunkX, chunkY); + const heightData = await this.loader.loadHeightMap( + chunkX, + chunkY, + this.manifest.worldHeightScale, + ); + this._chunks[to1D(chunkX, chunkY, this.worldWidth)] = new WorldChunk( heightData, - chunkX, chunkY, + chunkX, + chunkY, this.worldChunkSize, + this.manifest.regionMap.find(({ x, y }) => x === chunkX && y === chunkY), ); } @@ -61,7 +76,9 @@ export class WorldManager { return 0; } - return this._chunks[to1D(chunkX, chunkY, this.worldWidth)].getInterpolatedPoint( + return this._chunks[ + to1D(chunkX, chunkY, this.worldWidth) + ].getInterpolatedPoint( x - chunkX * this.worldChunkSize, y - chunkY * this.worldChunkSize, ); diff --git a/src/common/world/WorldManifest.ts b/src/common/world/WorldManifest.ts new file mode 100644 index 0000000..c5738bd --- /dev/null +++ b/src/common/world/WorldManifest.ts @@ -0,0 +1,15 @@ +export interface WorldManifestRegion { + x: number; + y: number; + splat: string[]; +} + +export interface WorldManifest { + worldWidth: number; + worldHeight: number; + worldChunkSize: number; + worldHeightScale: number; + textureBombingNoise: string; + textureSplattingSources: string[]; + regionMap: WorldManifestRegion[]; +}