manifest, skybox test
BIN
assets/skybox/default/back.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/skybox/default/bottom.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/skybox/default/front.png
Normal file
After Width: | Height: | Size: 991 KiB |
BIN
assets/skybox/default/left.png
Normal file
After Width: | Height: | Size: 938 KiB |
BIN
assets/skybox/default/right.png
Normal file
After Width: | Height: | Size: 1003 KiB |
BIN
assets/skybox/default/top.png
Normal file
After Width: | Height: | Size: 350 KiB |
25
assets/terrain/manifest.json
Normal file
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 554 KiB After Width: | Height: | Size: 554 KiB |
@ -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<void> {
|
||||
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() {
|
||||
|
25
src/client/object/other/cubemap.ts
Normal file
@ -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<CubeMap> {
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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<string[]>((list, entry) => {
|
||||
const paths = entry.splat.map((file) => `${BASE}/texture/${file}`);
|
||||
return [...list, ...paths.filter((path) => !list.includes(path))];
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ export class ClientWorldChunkShader {
|
||||
|
||||
constructor(public textureList: Map<string, ClientWorldTexture>) {}
|
||||
|
||||
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,
|
||||
|
@ -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<number[]> {
|
||||
async loadHeightMap(
|
||||
chunkX: number,
|
||||
chunkY: number,
|
||||
scale: number,
|
||||
): Promise<number[]> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
32
src/client/object/world/ClientWorldManifest.ts
Normal file
@ -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<ClientWorldManifest> {
|
||||
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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
) {}
|
||||
|
||||
|
@ -1,3 +1,7 @@
|
||||
export interface WorldLoader {
|
||||
loadHeightMap: (chunkX: number, chunkY: number) => Promise<number[]>;
|
||||
loadHeightMap: (
|
||||
chunkX: number,
|
||||
chunkY: number,
|
||||
scale: number,
|
||||
) => Promise<number[]>;
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
|
15
src/common/world/WorldManifest.ts
Normal file
@ -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[];
|
||||
}
|