-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
486 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import * as THREE from 'three'; | ||
import { MapChunk, MapArea } from '@wowserhq/format'; | ||
import { createSplatTexture } from './material.js'; | ||
import { createTerrainIndexBuffer, createTerrainVertexBuffer } from './geometry.js'; | ||
import TextureManager from '../TextureManager.js'; | ||
import TerrainMaterial from './TerrainMaterial.js'; | ||
import TerrainMesh from './TerrainMesh.js'; | ||
|
||
class TerrainManager { | ||
#textureManager: TextureManager; | ||
#loadedAreas = new globalThis.Map<number, THREE.Group>(); | ||
#loadingAreas = new globalThis.Map<number, Promise<THREE.Group>>(); | ||
|
||
constructor(textureManager: TextureManager) { | ||
this.#textureManager = textureManager; | ||
} | ||
|
||
getArea(areaId: number, area: MapArea): Promise<THREE.Group> { | ||
const loaded = this.#loadedAreas.get(areaId); | ||
if (loaded) { | ||
return Promise.resolve(loaded); | ||
} | ||
|
||
const alreadyLoading = this.#loadingAreas.get(areaId); | ||
if (alreadyLoading) { | ||
return alreadyLoading; | ||
} | ||
|
||
const loading = this.#loadArea(areaId, area); | ||
this.#loadingAreas.set(areaId, loading); | ||
|
||
return loading; | ||
} | ||
|
||
async #loadArea(areaId: number, area: MapArea) { | ||
const group = new THREE.Group(); | ||
group.name = 'terrain'; | ||
|
||
for (const chunk of area.chunks) { | ||
const mesh = await this.#createMesh(chunk); | ||
group.add(mesh); | ||
} | ||
|
||
this.#loadedAreas.set(areaId, group); | ||
this.#loadingAreas.delete(areaId); | ||
|
||
return group; | ||
} | ||
|
||
async #createMesh(chunk: MapChunk) { | ||
const [geometry, material] = await Promise.all([ | ||
this.#createGeometry(chunk), | ||
this.#createMaterial(chunk), | ||
]); | ||
|
||
return new TerrainMesh(chunk.position, geometry, material); | ||
} | ||
|
||
async #createGeometry(chunk: MapChunk) { | ||
const [vertexBuffer, indexBuffer] = await Promise.all([ | ||
createTerrainVertexBuffer(chunk), | ||
createTerrainIndexBuffer(chunk), | ||
]); | ||
|
||
const geometry = new THREE.BufferGeometry(); | ||
|
||
const positions = new THREE.InterleavedBuffer(new Float32Array(vertexBuffer), 4); | ||
geometry.setAttribute('position', new THREE.InterleavedBufferAttribute(positions, 3, 0, false)); | ||
|
||
const normals = new THREE.InterleavedBuffer(new Int8Array(vertexBuffer), 16); | ||
geometry.setAttribute('normal', new THREE.InterleavedBufferAttribute(normals, 4, 12, true)); | ||
|
||
const index = new THREE.BufferAttribute(new Uint16Array(indexBuffer), 1, false); | ||
geometry.setIndex(index); | ||
|
||
return geometry; | ||
} | ||
|
||
async #createMaterial(chunk: MapChunk) { | ||
const [splatTexture, ...layerTextures] = await Promise.all([ | ||
createSplatTexture(chunk.layers), | ||
...chunk.layers.map((layer) => this.#textureManager.get(layer.texture)), | ||
]); | ||
|
||
return new TerrainMaterial(chunk.layers.length, layerTextures, splatTexture); | ||
} | ||
} | ||
|
||
export default TerrainManager; | ||
export { TerrainManager }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import * as THREE from 'three'; | ||
import { fragmentShader, vertexShader } from './shaders.js'; | ||
|
||
class TerrainMaterial extends THREE.RawShaderMaterial { | ||
constructor(layerCount: number, layerTextures: THREE.Texture[], splatTexture: THREE.Texture) { | ||
super(); | ||
|
||
this.uniforms = { | ||
layerCount: { value: layerCount }, | ||
layers: { value: layerTextures }, | ||
splat: { value: splatTexture }, | ||
fogColor: { value: new THREE.Color(0.25, 0.5, 0.8) }, | ||
fogParams: { value: new THREE.Vector3(0.0, 1066.0, 1.0) }, | ||
}; | ||
|
||
this.vertexShader = vertexShader; | ||
this.fragmentShader = fragmentShader; | ||
this.glslVersion = THREE.GLSL3; | ||
this.side = THREE.FrontSide; | ||
this.blending = 0; | ||
} | ||
} | ||
|
||
export default TerrainMaterial; | ||
export { TerrainMaterial }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import * as THREE from 'three'; | ||
|
||
class TerrainMesh extends THREE.Mesh { | ||
constructor(position: Float32Array, geometry: THREE.BufferGeometry, material: THREE.Material) { | ||
// We need these for frustum culling, so we might as well take the hit on creation | ||
geometry.computeBoundingSphere(); | ||
|
||
super(geometry); | ||
|
||
this.material = material; | ||
|
||
this.position.set(position[0], position[1], position[2]); | ||
this.updateMatrixWorld(); | ||
} | ||
} | ||
|
||
export default TerrainMesh; | ||
export { TerrainMesh }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { MAP_CHUNK_FACE_COUNT_X, MAP_CHUNK_FACE_COUNT_Y, MapChunk } from '@wowserhq/format'; | ||
import terrainWorker from './worker.js'; | ||
|
||
const createTerrainVertexBuffer = (chunk: MapChunk) => | ||
terrainWorker.call('createTerrainVertexBuffer', chunk.vertexHeights, chunk.vertexNormals); | ||
|
||
const createTerrainIndexBuffer = (chunk: MapChunk) => | ||
terrainWorker.call( | ||
'createTerrainIndexBuffer', | ||
chunk.holes, | ||
MAP_CHUNK_FACE_COUNT_X, | ||
MAP_CHUNK_FACE_COUNT_Y, | ||
); | ||
|
||
export { createTerrainVertexBuffer, createTerrainIndexBuffer }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import * as THREE from 'three'; | ||
import { MAP_LAYER_SPLAT_X, MAP_LAYER_SPLAT_Y, MapLayer } from '@wowserhq/format'; | ||
import terrainWorker from './worker.js'; | ||
|
||
const SPLAT_TEXTURE_PLACEHOLDER = new THREE.Texture(); | ||
|
||
const createSplatTexture = async (layers: MapLayer[]) => { | ||
// Handle no splat | ||
|
||
if (layers.length <= 1) { | ||
// Return placeholder texture to keep uniforms consistent | ||
return SPLAT_TEXTURE_PLACEHOLDER; | ||
} | ||
|
||
// Handle single splat (2 layers) | ||
|
||
if (layers.length === 2) { | ||
const texture = new THREE.DataTexture( | ||
layers[1].splat, | ||
MAP_LAYER_SPLAT_X, | ||
MAP_LAYER_SPLAT_Y, | ||
THREE.RedFormat, | ||
); | ||
texture.minFilter = texture.magFilter = THREE.LinearFilter; | ||
texture.anisotropy = 16; | ||
texture.needsUpdate = true; | ||
|
||
return texture; | ||
} | ||
|
||
// Handle multiple splats (3+ layers) | ||
|
||
const layerSplats = layers.slice(1).map((layer) => layer.splat); | ||
const data = await terrainWorker.call( | ||
'mergeLayerSplats', | ||
layerSplats, | ||
MAP_LAYER_SPLAT_X, | ||
MAP_LAYER_SPLAT_Y, | ||
); | ||
|
||
const texture = new THREE.DataTexture( | ||
data, | ||
MAP_LAYER_SPLAT_X, | ||
MAP_LAYER_SPLAT_Y, | ||
THREE.RGBAFormat, | ||
); | ||
texture.minFilter = texture.magFilter = THREE.LinearFilter; | ||
texture.anisotropy = 16; | ||
texture.needsUpdate = true; | ||
|
||
return texture; | ||
}; | ||
|
||
export { createSplatTexture }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
const vertexShader = ` | ||
precision highp float; | ||
uniform mat4 modelMatrix; | ||
uniform mat4 modelViewMatrix; | ||
uniform mat4 projectionMatrix; | ||
uniform vec3 cameraPosition; | ||
in vec3 position; | ||
in vec3 normal; | ||
out vec2 layerCoord; | ||
out vec2 splatCoord; | ||
out float light; | ||
out float cameraDistance; | ||
void main() { | ||
// Terrain tileset textures repeat 8 times over the terrain chunk | ||
vec4 layerScale = vec4(-0.24, -0.24, 0.0, 0.0); | ||
layerCoord.xy = position.xy * layerScale.xy; | ||
// Splat textures do not repeat over the terrain chunk | ||
vec4 splatScale = vec4(-0.03, -0.03, 0.0, 0.0); | ||
splatCoord.yx = position.xy * splatScale.xy; | ||
// TODO - Replace with lighting manager controlled value | ||
vec3 lightDirection = vec3(-1, -1, -1); | ||
light = dot(normal, -normalize(lightDirection)); | ||
// Calculate camera distance for fog coloring in fragment shader | ||
vec4 worldPosition = modelMatrix * vec4(position, 1.0); | ||
cameraDistance = distance(cameraPosition, worldPosition.xyz); | ||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); | ||
} | ||
`; | ||
|
||
const fragmentShader = ` | ||
precision highp float; | ||
uniform int layerCount; | ||
uniform sampler2D layers[4]; | ||
uniform sampler2D splat; | ||
uniform vec3 fogColor; | ||
uniform vec3 fogParams; | ||
in vec2 layerCoord; | ||
in vec2 splatCoord; | ||
in float light; | ||
in float cameraDistance; | ||
out vec4 color; | ||
vec4 applyFog(vec4 color) { | ||
float fogStart = fogParams.x; | ||
float fogEnd = fogParams.y; | ||
float fogModifier = fogParams.z; | ||
float fogFactor = (fogEnd - cameraDistance) / (fogEnd - fogStart); | ||
fogFactor = clamp(fogFactor * fogModifier, 0.0, 1.0); | ||
vec4 mixed = vec4(mix(color.rgb, fogColor.rgb, 1.0 - fogFactor), color.a); | ||
return mixed; | ||
} | ||
vec4 blendLayer(vec4 color, vec4 layer, vec4 blend) { | ||
return (layer * blend) + ((1.0 - blend) * color); | ||
} | ||
void main() { | ||
vec4 layer; | ||
vec4 blend; | ||
// 1st layer | ||
color = texture(layers[0], layerCoord); | ||
blend = texture(splat, splatCoord); | ||
// 2nd layer | ||
if (layerCount > 1) { | ||
layer = texture(layers[1], layerCoord); | ||
color = blendLayer(color, layer, blend.rrrr); | ||
} | ||
if (layerCount > 2) { | ||
layer = texture(layers[2], layerCoord); | ||
color = blendLayer(color, layer, blend.gggg); | ||
} | ||
// 3rd layer | ||
if (layerCount > 3) { | ||
layer = texture(layers[3], layerCoord); | ||
color = blendLayer(color, layer, blend.bbbb); | ||
} | ||
// Fixed lighting | ||
vec3 lightDiffuse = normalize(vec3(0.25, 0.5, 1.0)); | ||
vec3 lightAmbient = normalize(vec3(0.5, 0.5, 0.5)); | ||
color.rgb *= lightDiffuse * light + lightAmbient; | ||
color.rgb = applyFog(color).rgb; | ||
// Terrain is always opaque | ||
color.a = 1.0; | ||
} | ||
`; | ||
|
||
export { vertexShader, fragmentShader }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import context from './worker/context.js'; | ||
import SceneWorker from '../SceneWorker.js'; | ||
|
||
const terrainWorker = new SceneWorker('terrain-worker', context); | ||
|
||
export default terrainWorker; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import * as geometry from './geometry.js'; | ||
import * as material from './material.js'; | ||
|
||
const context = { | ||
...geometry, | ||
...material, | ||
}; | ||
|
||
export default context; |
Oops, something went wrong.