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 { ThirdPersonCamera } from './object/camera';
|
||||||
import { Chat } from './object/chat';
|
import { Chat } from './object/chat';
|
||||||
import { Joystick } from './object/joystick';
|
import { Joystick } from './object/joystick';
|
||||||
|
import { CubeMap } from './object/other/cubemap';
|
||||||
import { VideoPlayer } from './object/other/video-player';
|
import { VideoPlayer } from './object/other/video-player';
|
||||||
import { Player } from './object/player';
|
import { Player } from './object/player';
|
||||||
import { PlayerEntity } from './object/player-entity';
|
import { PlayerEntity } from './object/player-entity';
|
||||||
import modelLoaderInstance from './object/pony-loader';
|
import modelLoaderInstance from './object/pony-loader';
|
||||||
import { ClientWorld } from './object/world/ClientWorld';
|
import { ClientWorld } from './object/world/ClientWorld';
|
||||||
import { ClientWorldLoader } from './object/world/ClientWorldLoader';
|
import { ClientWorldLoader } from './object/world/ClientWorldLoader';
|
||||||
|
import { ClientWorldManifest } from './object/world/ClientWorldManifest';
|
||||||
import { Renderer } from './renderer';
|
import { Renderer } from './renderer';
|
||||||
|
|
||||||
export class Game {
|
export class Game {
|
||||||
@ -24,7 +26,7 @@ export class Game {
|
|||||||
private character: CharacterPacket = {};
|
private character: CharacterPacket = {};
|
||||||
private party: string[] = [];
|
private party: string[] = [];
|
||||||
|
|
||||||
public world = new ClientWorld(new ClientWorldLoader());
|
public world!: ClientWorld;
|
||||||
public renderer = new Renderer();
|
public renderer = new Renderer();
|
||||||
|
|
||||||
private videoTest = new VideoPlayer(24, 12);
|
private videoTest = new VideoPlayer(24, 12);
|
||||||
@ -32,6 +34,11 @@ export class Game {
|
|||||||
constructor(public socket: Socket) {}
|
constructor(public socket: Socket) {}
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
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 modelLoaderInstance.loadPonyModel();
|
||||||
await this.world.initialize();
|
await this.world.initialize();
|
||||||
|
|
||||||
@ -43,7 +50,8 @@ export class Game {
|
|||||||
|
|
||||||
// experimental
|
// experimental
|
||||||
this.videoTest.initialize();
|
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.renderer.scene.add(this.videoTest.mesh);
|
||||||
this.party = (localStorage.getItem('party')?.split('|') || []).filter(
|
this.party = (localStorage.getItem('party')?.split('|') || []).filter(
|
||||||
(item) => item,
|
(item) => item,
|
||||||
@ -64,6 +72,7 @@ export class Game {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.renderer.scene.add(this.world.world);
|
this.renderer.scene.add(this.world.world);
|
||||||
|
this.renderer.scene.background = cube.texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
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: distance loading
|
||||||
// TODO: LOD
|
// TODO: LOD
|
||||||
|
|
||||||
|
const BASE = '/assets/terrain/';
|
||||||
|
|
||||||
export class ClientWorld extends WorldManager {
|
export class ClientWorld extends WorldManager {
|
||||||
public world = new Object3D();
|
public world = new Object3D();
|
||||||
private _mesher = new ClientWorldMesher();
|
private _mesher = new ClientWorldMesher();
|
||||||
@ -28,19 +30,19 @@ export class ClientWorld extends WorldManager {
|
|||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
await this.loadWorld();
|
await this.loadWorld();
|
||||||
|
|
||||||
|
const noiseFile = `${BASE}/texture/${this.manifest.textureBombingNoise}`;
|
||||||
|
|
||||||
await this.loadTextureList([
|
await this.loadTextureList([
|
||||||
|
noiseFile,
|
||||||
...this._chunks.map(
|
...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',
|
...this.gatherManifestTextures(),
|
||||||
'/assets/terrain/texture/grassy.png',
|
|
||||||
'/assets/terrain/texture/mud.png',
|
|
||||||
'/assets/terrain/texture/grass-flowers.png',
|
|
||||||
'/assets/terrain/texture/path.png',
|
|
||||||
]);
|
]);
|
||||||
this._shader.initialize(
|
|
||||||
this._worldTextures.get('/assets/terrain/texture/simplex-noise.png'),
|
this._shader.initialize();
|
||||||
);
|
this._shader.setNoise(this._worldTextures.get(noiseFile));
|
||||||
this.createMeshes();
|
this.createMeshes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +52,6 @@ export class ClientWorld extends WorldManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tex = await ClientWorldTexture.loadTexture(src);
|
const tex = await ClientWorldTexture.loadTexture(src);
|
||||||
// tex.texture.repeat.set(this.worldChunkSize, this.worldChunkSize);
|
|
||||||
this._worldTextures.set(src, tex);
|
this._worldTextures.set(src, tex);
|
||||||
return tex;
|
return tex;
|
||||||
}
|
}
|
||||||
@ -66,13 +67,8 @@ export class ClientWorld extends WorldManager {
|
|||||||
const chunk = this._chunkMeshQueue.shift();
|
const chunk = this._chunkMeshQueue.shift();
|
||||||
const material = this._shader.getShader(
|
const material = this._shader.getShader(
|
||||||
chunk,
|
chunk,
|
||||||
`/assets/terrain/splat-${chunk.x}-${chunk.y}.png`,
|
`${BASE}/region/splat-${chunk.x}-${chunk.y}.png`,
|
||||||
[
|
chunk.region.splat.map((file) => `${BASE}/texture/${file}`),
|
||||||
'/assets/terrain/texture/grassy.png',
|
|
||||||
'/assets/terrain/texture/mud.png',
|
|
||||||
'/assets/terrain/texture/grass-flowers.png',
|
|
||||||
'/assets/terrain/texture/path.png',
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const mesh = this._mesher.createTerrainMesh(
|
const mesh = this._mesher.createTerrainMesh(
|
||||||
@ -92,4 +88,11 @@ export class ClientWorld extends WorldManager {
|
|||||||
this._chunkMeshQueue.push(chunk);
|
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>) {}
|
constructor(public textureList: Map<string, ClientWorldTexture>) {}
|
||||||
|
|
||||||
initialize(noise: ClientWorldTexture) {
|
initialize() {
|
||||||
this.shader = new ShaderMaterial({
|
this.shader = new ShaderMaterial({
|
||||||
vertexShader: vertex,
|
vertexShader: vertex,
|
||||||
fragmentShader: fragment,
|
fragmentShader: fragment,
|
||||||
@ -185,7 +185,7 @@ export class ClientWorldChunkShader {
|
|||||||
UniformsLib.lights,
|
UniformsLib.lights,
|
||||||
{
|
{
|
||||||
backgroundTex: { value: null, type: 't' },
|
backgroundTex: { value: null, type: 't' },
|
||||||
noiseTex: { value: noise.texture, type: 't' },
|
noiseTex: { value: null, type: 't' },
|
||||||
rTex: { value: null, type: 't' },
|
rTex: { value: null, type: 't' },
|
||||||
gTex: { value: null, type: 't' },
|
gTex: { value: null, type: 't' },
|
||||||
bTex: { 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(
|
public getShader(
|
||||||
chunk: WorldChunk,
|
chunk: WorldChunk,
|
||||||
splatMap: string,
|
splatMap: string,
|
||||||
|
@ -3,14 +3,18 @@ import { to1D } from '../../../common/convert';
|
|||||||
import { WorldLoader } from '../../../common/world/WorldLoader';
|
import { WorldLoader } from '../../../common/world/WorldLoader';
|
||||||
|
|
||||||
const loader = new ImageLoader();
|
const loader = new ImageLoader();
|
||||||
const worldPath = '/assets/terrain/';
|
const worldPath = '/assets/terrain/region/';
|
||||||
|
|
||||||
export class ClientWorldLoader implements WorldLoader {
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
loader.load(
|
loader.load(
|
||||||
`${worldPath}/height-${chunkX}-${chunkY}.png`,
|
`${worldPath}/height-${chunkX}-${chunkY}.png`,
|
||||||
(data) => resolve(ClientWorldLoader.heightFromImage(data)),
|
(data) => resolve(ClientWorldLoader.heightFromImage(data, scale)),
|
||||||
undefined,
|
undefined,
|
||||||
(err) => {
|
(err) => {
|
||||||
reject(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 array = new Array(image.width * image.height);
|
||||||
const ctx = document.createElement('canvas').getContext('2d');
|
const ctx = document.createElement('canvas').getContext('2d');
|
||||||
ctx.canvas.width = image.width;
|
ctx.canvas.width = image.width;
|
||||||
@ -31,7 +38,7 @@ export class ClientWorldLoader implements WorldLoader {
|
|||||||
for (let x = 0; x < image.width; x++) {
|
for (let x = 0; x < image.width; x++) {
|
||||||
for (let y = 0; y < image.height; y++) {
|
for (let y = 0; y < image.height; y++) {
|
||||||
const index = to1D(x, y, image.width);
|
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 { Vector3, Vector2 } from 'three';
|
||||||
import { to1D } from '../convert';
|
import { to1D } from '../convert';
|
||||||
import { barycentricPoint } from '../helper';
|
import { barycentricPoint } from '../helper';
|
||||||
|
import { WorldManifestRegion } from './WorldManifest';
|
||||||
|
|
||||||
export class WorldChunk {
|
export class WorldChunk {
|
||||||
constructor(
|
constructor(
|
||||||
@ -8,6 +9,7 @@ export class WorldChunk {
|
|||||||
public x: number,
|
public x: number,
|
||||||
public y: number,
|
public y: number,
|
||||||
public size: number,
|
public size: number,
|
||||||
|
public region: WorldManifestRegion,
|
||||||
public scaledSize = size,
|
public scaledSize = size,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
export interface WorldLoader {
|
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 { to1D } from '../convert';
|
||||||
import { WorldChunk } from './WorldChunk';
|
import { WorldChunk } from './WorldChunk';
|
||||||
import { WorldLoader } from './WorldLoader';
|
import { WorldLoader } from './WorldLoader';
|
||||||
|
import { WorldManifest } from './WorldManifest';
|
||||||
|
|
||||||
export class WorldManager {
|
export class WorldManager {
|
||||||
protected _chunks!: WorldChunk[];
|
protected _chunks!: WorldChunk[];
|
||||||
|
|
||||||
constructor(
|
constructor(public loader: WorldLoader, public manifest: WorldManifest) {
|
||||||
public loader: WorldLoader,
|
|
||||||
public worldWidth = 1,
|
|
||||||
public worldHeight = 1,
|
|
||||||
public worldChunkSize = 256,
|
|
||||||
) {
|
|
||||||
this._chunks = new Array(this.worldWidth * this.worldHeight);
|
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) {
|
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(
|
this._chunks[to1D(chunkX, chunkY, this.worldWidth)] = new WorldChunk(
|
||||||
heightData,
|
heightData,
|
||||||
chunkX, chunkY,
|
chunkX,
|
||||||
|
chunkY,
|
||||||
this.worldChunkSize,
|
this.worldChunkSize,
|
||||||
|
this.manifest.regionMap.find(({ x, y }) => x === chunkX && y === chunkY),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +76,9 @@ export class WorldManager {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._chunks[to1D(chunkX, chunkY, this.worldWidth)].getInterpolatedPoint(
|
return this._chunks[
|
||||||
|
to1D(chunkX, chunkY, this.worldWidth)
|
||||||
|
].getInterpolatedPoint(
|
||||||
x - chunkX * this.worldChunkSize,
|
x - chunkX * this.worldChunkSize,
|
||||||
y - chunkY * 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[];
|
||||||
|
}
|