@3driseai/3drise-viewer 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Clouds.d.ts.map +1 -1
- package/dist/components/Clouds.js +0 -1
- package/dist/components/CloudsGenerator.d.ts +32 -0
- package/dist/components/CloudsGenerator.d.ts.map +1 -0
- package/dist/components/CloudsGenerator.js +58 -0
- package/dist/components/EnvironmentManager.d.ts +47 -5
- package/dist/components/EnvironmentManager.d.ts.map +1 -1
- package/dist/components/EnvironmentManager.js +35 -66
- package/dist/components/HdrEnvironment.d.ts +20 -0
- package/dist/components/HdrEnvironment.d.ts.map +1 -0
- package/dist/components/HdrEnvironment.js +44 -0
- package/dist/components/LandscapeBuilder.d.ts +23 -0
- package/dist/components/LandscapeBuilder.d.ts.map +1 -0
- package/dist/components/LandscapeBuilder.js +25 -0
- package/dist/components/LandscapeObject.d.ts +24 -0
- package/dist/components/LandscapeObject.d.ts.map +1 -0
- package/dist/components/LandscapeObject.js +27 -0
- package/dist/components/ObjectNode.d.ts.map +1 -1
- package/dist/components/ObjectNode.js +10 -0
- package/dist/components/OceanBuilder.d.ts +35 -0
- package/dist/components/OceanBuilder.d.ts.map +1 -0
- package/dist/components/OceanBuilder.js +53 -0
- package/dist/components/SceneBuilder.d.ts.map +1 -1
- package/dist/components/SkySystem.d.ts +27 -6
- package/dist/components/SkySystem.d.ts.map +1 -1
- package/dist/components/SkySystem.js +6 -5
- package/dist/components/SunLight.d.ts.map +1 -1
- package/dist/components/SunLight.js +4 -1
- package/dist/components/TerrainBuilder.d.ts +30 -0
- package/dist/components/TerrainBuilder.d.ts.map +1 -0
- package/dist/components/TerrainBuilder.js +44 -0
- package/dist/components/index.d.ts +17 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +10 -0
- package/dist/components/landscape/LandscapeMesh.d.ts +18 -0
- package/dist/components/landscape/LandscapeMesh.d.ts.map +1 -0
- package/dist/components/landscape/LandscapeMesh.js +278 -0
- package/dist/components/landscape/index.d.ts +3 -0
- package/dist/components/landscape/index.d.ts.map +1 -0
- package/dist/components/landscape/index.js +1 -0
- package/dist/components/landscape/types.d.ts +13 -0
- package/dist/components/landscape/types.d.ts.map +1 -0
- package/dist/components/landscape/types.js +1 -0
- package/dist/components/terrain/Canyon.d.ts +23 -0
- package/dist/components/terrain/Canyon.d.ts.map +1 -0
- package/dist/components/terrain/Canyon.js +29 -0
- package/dist/components/terrain/Dunes.d.ts +22 -0
- package/dist/components/terrain/Dunes.d.ts.map +1 -0
- package/dist/components/terrain/Dunes.js +25 -0
- package/dist/components/terrain/Hills.d.ts +17 -0
- package/dist/components/terrain/Hills.d.ts.map +1 -0
- package/dist/components/terrain/Hills.js +20 -0
- package/dist/components/terrain/Mountains.d.ts +19 -0
- package/dist/components/terrain/Mountains.d.ts.map +1 -0
- package/dist/components/terrain/Mountains.js +22 -0
- package/dist/components/terrain/TerrainSurface.d.ts +22 -0
- package/dist/components/terrain/TerrainSurface.d.ts.map +1 -0
- package/dist/components/terrain/TerrainSurface.js +94 -0
- package/dist/components/terrain/index.d.ts +7 -0
- package/dist/components/terrain/index.d.ts.map +1 -0
- package/dist/components/terrain/index.js +7 -0
- package/dist/components/terrain/types.d.ts +17 -0
- package/dist/components/terrain/types.d.ts.map +1 -0
- package/dist/components/terrain/types.js +1 -0
- package/dist/components/volumetricClouds/Cirrus.d.ts +14 -0
- package/dist/components/volumetricClouds/Cirrus.d.ts.map +1 -0
- package/dist/components/volumetricClouds/Cirrus.js +13 -0
- package/dist/components/volumetricClouds/Cumulonimbus.d.ts +17 -0
- package/dist/components/volumetricClouds/Cumulonimbus.d.ts.map +1 -0
- package/dist/components/volumetricClouds/Cumulonimbus.js +20 -0
- package/dist/components/volumetricClouds/Cumulus.d.ts +14 -0
- package/dist/components/volumetricClouds/Cumulus.d.ts.map +1 -0
- package/dist/components/volumetricClouds/Cumulus.js +17 -0
- package/dist/components/volumetricClouds/Stratocumulus.d.ts +12 -0
- package/dist/components/volumetricClouds/Stratocumulus.d.ts.map +1 -0
- package/dist/components/volumetricClouds/Stratocumulus.js +16 -0
- package/dist/components/volumetricClouds/VolumetricCloudsPass.d.ts +42 -0
- package/dist/components/volumetricClouds/VolumetricCloudsPass.d.ts.map +1 -0
- package/dist/components/volumetricClouds/VolumetricCloudsPass.js +298 -0
- package/dist/components/volumetricClouds/index.d.ts +8 -0
- package/dist/components/volumetricClouds/index.d.ts.map +1 -0
- package/dist/components/volumetricClouds/index.js +7 -0
- package/dist/components/volumetricClouds/types.d.ts +17 -0
- package/dist/components/volumetricClouds/types.d.ts.map +1 -0
- package/dist/components/volumetricClouds/types.js +1 -0
- package/dist/components/water/Host.d.ts +12 -0
- package/dist/components/water/Host.d.ts.map +1 -0
- package/dist/components/water/Host.js +12 -0
- package/dist/components/water/Lake.d.ts +18 -0
- package/dist/components/water/Lake.d.ts.map +1 -0
- package/dist/components/water/Lake.js +21 -0
- package/dist/components/water/OceanSurface.d.ts +27 -0
- package/dist/components/water/OceanSurface.d.ts.map +1 -0
- package/dist/components/water/OceanSurface.js +60 -0
- package/dist/components/water/Sea.d.ts +18 -0
- package/dist/components/water/Sea.d.ts.map +1 -0
- package/dist/components/water/Sea.js +17 -0
- package/dist/components/water/Shore.d.ts +16 -0
- package/dist/components/water/Shore.d.ts.map +1 -0
- package/dist/components/water/Shore.js +15 -0
- package/dist/components/water/Storm.d.ts +14 -0
- package/dist/components/water/Storm.d.ts.map +1 -0
- package/dist/components/water/Storm.js +13 -0
- package/dist/components/water/WaterBody.d.ts +31 -0
- package/dist/components/water/WaterBody.d.ts.map +1 -0
- package/dist/components/water/WaterBody.js +76 -0
- package/dist/components/water/index.d.ts +9 -0
- package/dist/components/water/index.d.ts.map +1 -0
- package/dist/components/water/index.js +11 -0
- package/dist/components/water/types.d.ts +20 -0
- package/dist/components/water/types.d.ts.map +1 -0
- package/dist/components/water/types.js +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +9 -0
- package/dist/hooks/index.d.ts +24 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +12 -0
- package/dist/hooks/useClipmap.d.ts +71 -0
- package/dist/hooks/useClipmap.d.ts.map +1 -0
- package/dist/hooks/useClipmap.js +81 -0
- package/dist/hooks/useCloudLive.d.ts +29 -0
- package/dist/hooks/useCloudLive.d.ts.map +1 -0
- package/dist/hooks/useCloudLive.js +43 -0
- package/dist/hooks/useCloudNoise.d.ts +28 -0
- package/dist/hooks/useCloudNoise.d.ts.map +1 -0
- package/dist/hooks/useCloudNoise.js +128 -0
- package/dist/hooks/useCloudSun.d.ts +49 -0
- package/dist/hooks/useCloudSun.d.ts.map +1 -0
- package/dist/hooks/useCloudSun.js +84 -0
- package/dist/hooks/useCubeCamera.d.ts +27 -9
- package/dist/hooks/useCubeCamera.d.ts.map +1 -1
- package/dist/hooks/useCubeCamera.js +28 -70
- package/dist/hooks/useEnvironmentProbe.d.ts +32 -0
- package/dist/hooks/useEnvironmentProbe.d.ts.map +1 -0
- package/dist/hooks/useEnvironmentProbe.js +35 -0
- package/dist/hooks/useFrameRenderer.d.ts +15 -0
- package/dist/hooks/useFrameRenderer.d.ts.map +1 -0
- package/dist/hooks/useFrameRenderer.js +23 -0
- package/dist/hooks/useLandscapeLive.d.ts +29 -0
- package/dist/hooks/useLandscapeLive.d.ts.map +1 -0
- package/dist/hooks/useLandscapeLive.js +41 -0
- package/dist/hooks/useLandscapeMaps.d.ts +32 -0
- package/dist/hooks/useLandscapeMaps.d.ts.map +1 -0
- package/dist/hooks/useLandscapeMaps.js +138 -0
- package/dist/hooks/useOceanLive.d.ts +29 -0
- package/dist/hooks/useOceanLive.d.ts.map +1 -0
- package/dist/hooks/useOceanLive.js +38 -0
- package/dist/hooks/useOceanMaterial.d.ts +39 -0
- package/dist/hooks/useOceanMaterial.d.ts.map +1 -0
- package/dist/hooks/useOceanMaterial.js +65 -0
- package/dist/hooks/useTerrainClipmap.d.ts +12 -0
- package/dist/hooks/useTerrainClipmap.d.ts.map +1 -0
- package/dist/hooks/useTerrainClipmap.js +10 -0
- package/dist/hooks/useTerrainLive.d.ts +34 -0
- package/dist/hooks/useTerrainLive.d.ts.map +1 -0
- package/dist/hooks/useTerrainLive.js +45 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -0
- package/dist/presets/landscapePresets.d.ts +60 -0
- package/dist/presets/landscapePresets.d.ts.map +1 -0
- package/dist/presets/landscapePresets.js +194 -0
- package/dist/presets/landscapeTypes.d.ts +195 -0
- package/dist/presets/landscapeTypes.d.ts.map +1 -0
- package/dist/presets/landscapeTypes.js +315 -0
- package/dist/presets/oceanSurfacePresets.d.ts +33 -0
- package/dist/presets/oceanSurfacePresets.d.ts.map +1 -0
- package/dist/presets/oceanSurfacePresets.js +245 -0
- package/dist/presets/terrainPresets.d.ts +28 -0
- package/dist/presets/terrainPresets.d.ts.map +1 -0
- package/dist/presets/terrainPresets.js +293 -0
- package/dist/presets/volumetricCloudPresets.d.ts +38 -0
- package/dist/presets/volumetricCloudPresets.d.ts.map +1 -0
- package/dist/presets/volumetricCloudPresets.js +480 -0
- package/dist/shaders/clouds/CirrusMaterial.d.ts +84 -0
- package/dist/shaders/clouds/CirrusMaterial.d.ts.map +1 -0
- package/dist/shaders/clouds/CirrusMaterial.js +167 -0
- package/dist/shaders/clouds/CloudCompositeMaterial.d.ts +20 -0
- package/dist/shaders/clouds/CloudCompositeMaterial.d.ts.map +1 -0
- package/dist/shaders/clouds/CloudCompositeMaterial.js +82 -0
- package/dist/shaders/clouds/CloudResolveMaterial.d.ts +22 -0
- package/dist/shaders/clouds/CloudResolveMaterial.d.ts.map +1 -0
- package/dist/shaders/clouds/CloudResolveMaterial.js +77 -0
- package/dist/shaders/clouds/CloudVolumeMaterial.d.ts +143 -0
- package/dist/shaders/clouds/CloudVolumeMaterial.d.ts.map +1 -0
- package/dist/shaders/clouds/CloudVolumeMaterial.js +249 -0
- package/dist/shaders/clouds/SkyBackgroundMaterial.d.ts +21 -0
- package/dist/shaders/clouds/SkyBackgroundMaterial.d.ts.map +1 -0
- package/dist/shaders/clouds/SkyBackgroundMaterial.js +42 -0
- package/dist/shaders/glsl/cloudCore.glsl.d.ts +24 -0
- package/dist/shaders/glsl/cloudCore.glsl.d.ts.map +1 -0
- package/dist/shaders/glsl/cloudCore.glsl.js +348 -0
- package/dist/shaders/glsl/landscapeCore.glsl.d.ts +49 -0
- package/dist/shaders/glsl/landscapeCore.glsl.d.ts.map +1 -0
- package/dist/shaders/glsl/landscapeCore.glsl.js +518 -0
- package/dist/shaders/glsl/noiseBake.glsl.d.ts +30 -0
- package/dist/shaders/glsl/noiseBake.glsl.d.ts.map +1 -0
- package/dist/shaders/glsl/noiseBake.glsl.js +164 -0
- package/dist/shaders/glsl/oceanCore.glsl.d.ts +52 -0
- package/dist/shaders/glsl/oceanCore.glsl.d.ts.map +1 -0
- package/dist/shaders/glsl/oceanCore.glsl.js +421 -0
- package/dist/shaders/glsl/terrainCore.glsl.d.ts +78 -0
- package/dist/shaders/glsl/terrainCore.glsl.d.ts.map +1 -0
- package/dist/shaders/glsl/terrainCore.glsl.js +625 -0
- package/dist/shaders/landscape/LandscapeMaterial.d.ts +73 -0
- package/dist/shaders/landscape/LandscapeMaterial.d.ts.map +1 -0
- package/dist/shaders/landscape/LandscapeMaterial.js +136 -0
- package/dist/shaders/ocean/OceanSurfaceMaterial.d.ts +57 -0
- package/dist/shaders/ocean/OceanSurfaceMaterial.d.ts.map +1 -0
- package/dist/shaders/ocean/OceanSurfaceMaterial.js +268 -0
- package/dist/shaders/terrain/TerrainMaterial.d.ts +80 -0
- package/dist/shaders/terrain/TerrainMaterial.d.ts.map +1 -0
- package/dist/shaders/terrain/TerrainMaterial.js +291 -0
- package/dist/types/cloudConfigs.d.ts +244 -0
- package/dist/types/cloudConfigs.d.ts.map +1 -0
- package/dist/types/cloudConfigs.js +282 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +4 -1
- package/dist/types/landscapeConfigs.d.ts +218 -0
- package/dist/types/landscapeConfigs.d.ts.map +1 -0
- package/dist/types/landscapeConfigs.js +256 -0
- package/dist/types/oceanConfigs.d.ts +290 -0
- package/dist/types/oceanConfigs.d.ts.map +1 -0
- package/dist/types/oceanConfigs.js +380 -0
- package/dist/types/terrainConfigs.d.ts +251 -0
- package/dist/types/terrainConfigs.d.ts.map +1 -0
- package/dist/types/terrainConfigs.js +292 -0
- package/dist/utils/cloudUniforms.d.ts +14 -0
- package/dist/utils/cloudUniforms.d.ts.map +1 -0
- package/dist/utils/cloudUniforms.js +122 -0
- package/dist/utils/frameRenderer.d.ts +33 -0
- package/dist/utils/frameRenderer.d.ts.map +1 -0
- package/dist/utils/frameRenderer.js +24 -0
- package/dist/utils/handleCloudsAnimations.d.ts +16 -0
- package/dist/utils/handleCloudsAnimations.d.ts.map +1 -0
- package/dist/utils/handleCloudsAnimations.js +64 -0
- package/dist/utils/landscapeGeometry.d.ts +61 -0
- package/dist/utils/landscapeGeometry.d.ts.map +1 -0
- package/dist/utils/landscapeGeometry.js +126 -0
- package/dist/utils/landscapeHeight.d.ts +44 -0
- package/dist/utils/landscapeHeight.d.ts.map +1 -0
- package/dist/utils/landscapeHeight.js +65 -0
- package/dist/utils/landscapeTextures.d.ts +94 -0
- package/dist/utils/landscapeTextures.d.ts.map +1 -0
- package/dist/utils/landscapeTextures.js +290 -0
- package/dist/utils/landscapeUniforms.d.ts +20 -0
- package/dist/utils/landscapeUniforms.d.ts.map +1 -0
- package/dist/utils/landscapeUniforms.js +61 -0
- package/dist/utils/oceanUniforms.d.ts +47 -0
- package/dist/utils/oceanUniforms.d.ts.map +1 -0
- package/dist/utils/oceanUniforms.js +216 -0
- package/dist/utils/rtin.d.ts +63 -0
- package/dist/utils/rtin.d.ts.map +1 -0
- package/dist/utils/rtin.js +202 -0
- package/dist/utils/terrainUniforms.d.ts +14 -0
- package/dist/utils/terrainUniforms.d.ts.map +1 -0
- package/dist/utils/terrainUniforms.js +196 -0
- package/package.json +2 -2
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { Rtin } from './rtin.js';
|
|
3
|
+
const tileCache = new WeakMap();
|
|
4
|
+
/** The error map depends only on the heightfield, so it is built once per
|
|
5
|
+
* decoded heightmap and reused for every error budget. */
|
|
6
|
+
export function rtinTileFor(height) {
|
|
7
|
+
const cached = tileCache.get(height);
|
|
8
|
+
if (cached)
|
|
9
|
+
return cached;
|
|
10
|
+
const tile = new Rtin(height.gridSize).createTile(height.data);
|
|
11
|
+
tileCache.set(height, tile);
|
|
12
|
+
return tile;
|
|
13
|
+
}
|
|
14
|
+
export function buildLandscapeGeometry(height, options) {
|
|
15
|
+
const { size, maxError } = options;
|
|
16
|
+
const chunksPerSide = Math.max(1, Math.min(16, Math.round(options.chunks)));
|
|
17
|
+
const tile = rtinTileFor(height);
|
|
18
|
+
const mesh = tile.getMesh(Math.max(0, maxError));
|
|
19
|
+
const grid = height.gridSize;
|
|
20
|
+
const tileSize = height.tileSize;
|
|
21
|
+
const vertexCount = mesh.vertices.length / 2;
|
|
22
|
+
const positions = new Float32Array(vertexCount * 3);
|
|
23
|
+
const uvs = new Float32Array(vertexCount * 2);
|
|
24
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
25
|
+
const gx = mesh.vertices[i * 2];
|
|
26
|
+
const gy = mesh.vertices[i * 2 + 1];
|
|
27
|
+
const u = gx / tileSize;
|
|
28
|
+
const v = gy / tileSize;
|
|
29
|
+
positions[i * 3 + 0] = (u - 0.5) * size;
|
|
30
|
+
positions[i * 3 + 1] = 0;
|
|
31
|
+
positions[i * 3 + 2] = (v - 0.5) * size;
|
|
32
|
+
uvs[i * 2 + 0] = u;
|
|
33
|
+
uvs[i * 2 + 1] = v;
|
|
34
|
+
}
|
|
35
|
+
const position = new THREE.BufferAttribute(positions, 3);
|
|
36
|
+
const uv = new THREE.BufferAttribute(uvs, 2);
|
|
37
|
+
// Bucket triangles by the chunk their centroid falls in.
|
|
38
|
+
const buckets = Array.from({ length: chunksPerSide * chunksPerSide }, () => []);
|
|
39
|
+
const triangleCount = mesh.triangles.length / 3;
|
|
40
|
+
for (let t = 0; t < triangleCount; t++) {
|
|
41
|
+
const a = mesh.triangles[t * 3];
|
|
42
|
+
const b = mesh.triangles[t * 3 + 1];
|
|
43
|
+
const c = mesh.triangles[t * 3 + 2];
|
|
44
|
+
const cx = (mesh.vertices[a * 2] + mesh.vertices[b * 2] + mesh.vertices[c * 2]) / 3;
|
|
45
|
+
const cy = (mesh.vertices[a * 2 + 1] + mesh.vertices[b * 2 + 1] + mesh.vertices[c * 2 + 1]) / 3;
|
|
46
|
+
const ix = Math.min(chunksPerSide - 1, Math.floor((cx / tileSize) * chunksPerSide));
|
|
47
|
+
const iy = Math.min(chunksPerSide - 1, Math.floor((cy / tileSize) * chunksPerSide));
|
|
48
|
+
buckets[iy * chunksPerSide + ix].push(a, b, c);
|
|
49
|
+
}
|
|
50
|
+
const chunks = [];
|
|
51
|
+
for (const indices of buckets) {
|
|
52
|
+
if (indices.length === 0)
|
|
53
|
+
continue;
|
|
54
|
+
const geometry = new THREE.BufferGeometry();
|
|
55
|
+
geometry.setAttribute('position', position);
|
|
56
|
+
geometry.setAttribute('uv', uv);
|
|
57
|
+
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
|
|
58
|
+
let minH01 = Infinity;
|
|
59
|
+
let maxH01 = -Infinity;
|
|
60
|
+
let minX = Infinity;
|
|
61
|
+
let maxX = -Infinity;
|
|
62
|
+
let minZ = Infinity;
|
|
63
|
+
let maxZ = -Infinity;
|
|
64
|
+
for (const index of indices) {
|
|
65
|
+
const gx = mesh.vertices[index * 2];
|
|
66
|
+
const gy = mesh.vertices[index * 2 + 1];
|
|
67
|
+
const h = height.data[gy * grid + gx];
|
|
68
|
+
if (h < minH01)
|
|
69
|
+
minH01 = h;
|
|
70
|
+
if (h > maxH01)
|
|
71
|
+
maxH01 = h;
|
|
72
|
+
const x = positions[index * 3];
|
|
73
|
+
const z = positions[index * 3 + 2];
|
|
74
|
+
if (x < minX)
|
|
75
|
+
minX = x;
|
|
76
|
+
if (x > maxX)
|
|
77
|
+
maxX = x;
|
|
78
|
+
if (z < minZ)
|
|
79
|
+
minZ = z;
|
|
80
|
+
if (z > maxZ)
|
|
81
|
+
maxZ = z;
|
|
82
|
+
}
|
|
83
|
+
// Placeholder Y; setLandscapeBounds writes the real range once the
|
|
84
|
+
// elevation scale is known, and again whenever it changes.
|
|
85
|
+
geometry.boundingBox = new THREE.Box3(new THREE.Vector3(minX, 0, minZ), new THREE.Vector3(maxX, 1, maxZ));
|
|
86
|
+
geometry.boundingSphere = new THREE.Sphere();
|
|
87
|
+
geometry.boundingBox.getBoundingSphere(geometry.boundingSphere);
|
|
88
|
+
chunks.push({ geometry, minH01, maxH01 });
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
chunks,
|
|
92
|
+
position,
|
|
93
|
+
uv,
|
|
94
|
+
vertexCount,
|
|
95
|
+
triangleCount,
|
|
96
|
+
size,
|
|
97
|
+
dispose() {
|
|
98
|
+
for (const chunk of chunks) {
|
|
99
|
+
// Drop the shared attributes first so disposing the geometry
|
|
100
|
+
// cannot free them once per chunk.
|
|
101
|
+
chunk.geometry.deleteAttribute('position');
|
|
102
|
+
chunk.geometry.deleteAttribute('uv');
|
|
103
|
+
chunk.geometry.dispose();
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Rewrite the vertical extent of every chunk's bounds. Called whenever the
|
|
109
|
+
* elevation scale or bias changes; without it a raised landscape culls against
|
|
110
|
+
* the flat plate it was built as and disappears when the camera tilts. */
|
|
111
|
+
export function setLandscapeBounds(geometry, scale, bias) {
|
|
112
|
+
for (const chunk of geometry.chunks) {
|
|
113
|
+
const box = chunk.geometry.boundingBox;
|
|
114
|
+
if (!box)
|
|
115
|
+
continue;
|
|
116
|
+
box.min.y = chunk.minH01 * scale + bias;
|
|
117
|
+
box.max.y = chunk.maxH01 * scale + bias;
|
|
118
|
+
// A degenerate box makes the sphere radius 0 and culls the chunk at
|
|
119
|
+
// grazing angles.
|
|
120
|
+
if (box.max.y - box.min.y < 1e-3)
|
|
121
|
+
box.max.y = box.min.y + 1e-3;
|
|
122
|
+
if (!chunk.geometry.boundingSphere)
|
|
123
|
+
chunk.geometry.boundingSphere = new THREE.Sphere();
|
|
124
|
+
box.getBoundingSphere(chunk.geometry.boundingSphere);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
/**
|
|
3
|
+
* CPU height queries against a landscape.
|
|
4
|
+
*
|
|
5
|
+
* The procedural terrain has never had this — its height field exists only in
|
|
6
|
+
* the shader, so nothing can be placed *on* it programmatically (terrain doc,
|
|
7
|
+
* open item #5). A landscape gets it for nothing, because RTIN already decoded
|
|
8
|
+
* the heightmap into a `Float32Array` on the way to building the mesh.
|
|
9
|
+
*
|
|
10
|
+
* What it is for: snapping a placed object to the ground, keeping a camera above
|
|
11
|
+
* it, and later letting the water read a landscape's shoreline the way the ocean
|
|
12
|
+
* plan reads the terrain's.
|
|
13
|
+
*/
|
|
14
|
+
export interface LandscapeHeightField {
|
|
15
|
+
/** Normalised 0..1 samples, `gridSize * gridSize`, row-major. */
|
|
16
|
+
data: Float32Array;
|
|
17
|
+
gridSize: number;
|
|
18
|
+
tileSize: number;
|
|
19
|
+
/** World units across the tile. */
|
|
20
|
+
size: number;
|
|
21
|
+
/** World centre of the tile. */
|
|
22
|
+
origin: THREE.Vector3;
|
|
23
|
+
/** Y rotation applied to the tile, in radians. */
|
|
24
|
+
rotationY: number;
|
|
25
|
+
elevationScale: number;
|
|
26
|
+
elevationBias: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function registerLandscapeHeightField(id: string, field: LandscapeHeightField): void;
|
|
29
|
+
export declare function unregisterLandscapeHeightField(id: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* World height at a world XZ, or null when the point is off the tile.
|
|
32
|
+
*
|
|
33
|
+
* Bilinear against the heightmap itself rather than against the mesh, so the
|
|
34
|
+
* answer does not change when the error budget does — the mesh is an
|
|
35
|
+
* approximation of this, not the other way round.
|
|
36
|
+
*/
|
|
37
|
+
export declare function sampleLandscapeHeight(field: LandscapeHeightField, x: number, z: number): number | null;
|
|
38
|
+
/** Highest landscape under a world XZ, across every registered landscape.
|
|
39
|
+
* Returns null when no landscape covers the point. */
|
|
40
|
+
export declare function getLandscapeHeightAt(x: number, z: number): number | null;
|
|
41
|
+
/** Drop an object onto the ground, keeping its XZ. No-op when nothing is under
|
|
42
|
+
* it, so calling this in a scene with no landscape is safe. */
|
|
43
|
+
export declare function snapToLandscape(object: THREE.Object3D, offset?: number): boolean;
|
|
44
|
+
//# sourceMappingURL=landscapeHeight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landscapeHeight.d.ts","sourceRoot":"","sources":["../../src/utils/landscapeHeight.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,oBAAoB;IACjC,iEAAiE;IACjE,IAAI,EAAE,YAAY,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,gCAAgC;IAChC,MAAM,EAAE,KAAK,CAAC,OAAO,CAAA;IACrB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;CACxB;AAID,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAE1F;AAED,wBAAgB,8BAA8B,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAE/D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,oBAAoB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgCtG;AAED;uDACuD;AACvD,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOxE;AAED;gEACgE;AAChE,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,EAAE,MAAM,SAAI,GAAG,OAAO,CAK3E"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const fields = new Map();
|
|
2
|
+
export function registerLandscapeHeightField(id, field) {
|
|
3
|
+
fields.set(id, field);
|
|
4
|
+
}
|
|
5
|
+
export function unregisterLandscapeHeightField(id) {
|
|
6
|
+
fields.delete(id);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* World height at a world XZ, or null when the point is off the tile.
|
|
10
|
+
*
|
|
11
|
+
* Bilinear against the heightmap itself rather than against the mesh, so the
|
|
12
|
+
* answer does not change when the error budget does — the mesh is an
|
|
13
|
+
* approximation of this, not the other way round.
|
|
14
|
+
*/
|
|
15
|
+
export function sampleLandscapeHeight(field, x, z) {
|
|
16
|
+
let dx = x - field.origin.x;
|
|
17
|
+
let dz = z - field.origin.z;
|
|
18
|
+
if (field.rotationY !== 0) {
|
|
19
|
+
const c = Math.cos(-field.rotationY);
|
|
20
|
+
const s = Math.sin(-field.rotationY);
|
|
21
|
+
const rx = dx * c - dz * s;
|
|
22
|
+
const rz = dx * s + dz * c;
|
|
23
|
+
dx = rx;
|
|
24
|
+
dz = rz;
|
|
25
|
+
}
|
|
26
|
+
const u = dx / field.size + 0.5;
|
|
27
|
+
const v = dz / field.size + 0.5;
|
|
28
|
+
if (u < 0 || u > 1 || v < 0 || v > 1)
|
|
29
|
+
return null;
|
|
30
|
+
const fx = u * field.tileSize;
|
|
31
|
+
const fy = v * field.tileSize;
|
|
32
|
+
const x0 = Math.min(field.tileSize, Math.floor(fx));
|
|
33
|
+
const y0 = Math.min(field.tileSize, Math.floor(fy));
|
|
34
|
+
const x1 = Math.min(field.gridSize - 1, x0 + 1);
|
|
35
|
+
const y1 = Math.min(field.gridSize - 1, y0 + 1);
|
|
36
|
+
const tx = fx - x0;
|
|
37
|
+
const ty = fy - y0;
|
|
38
|
+
const g = field.gridSize;
|
|
39
|
+
const h00 = field.data[y0 * g + x0];
|
|
40
|
+
const h10 = field.data[y0 * g + x1];
|
|
41
|
+
const h01 = field.data[y1 * g + x0];
|
|
42
|
+
const h11 = field.data[y1 * g + x1];
|
|
43
|
+
const h = (h00 * (1 - tx) + h10 * tx) * (1 - ty) + (h01 * (1 - tx) + h11 * tx) * ty;
|
|
44
|
+
return h * field.elevationScale + field.elevationBias + field.origin.y;
|
|
45
|
+
}
|
|
46
|
+
/** Highest landscape under a world XZ, across every registered landscape.
|
|
47
|
+
* Returns null when no landscape covers the point. */
|
|
48
|
+
export function getLandscapeHeightAt(x, z) {
|
|
49
|
+
let best = null;
|
|
50
|
+
for (const field of fields.values()) {
|
|
51
|
+
const h = sampleLandscapeHeight(field, x, z);
|
|
52
|
+
if (h !== null && (best === null || h > best))
|
|
53
|
+
best = h;
|
|
54
|
+
}
|
|
55
|
+
return best;
|
|
56
|
+
}
|
|
57
|
+
/** Drop an object onto the ground, keeping its XZ. No-op when nothing is under
|
|
58
|
+
* it, so calling this in a scene with no landscape is safe. */
|
|
59
|
+
export function snapToLandscape(object, offset = 0) {
|
|
60
|
+
const h = getLandscapeHeightAt(object.position.x, object.position.z);
|
|
61
|
+
if (h === null)
|
|
62
|
+
return false;
|
|
63
|
+
object.position.y = h + offset;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
export declare function loadImage(url: string): Promise<HTMLImageElement>;
|
|
3
|
+
/** Largest power of two at or below n, clamped to something RTIN can chew. */
|
|
4
|
+
export declare function floorPowerOfTwo(n: number): number;
|
|
5
|
+
export type HeightEncodingName = 'grayscale' | 'terrain-rgb' | 'rg16';
|
|
6
|
+
export interface DecodedHeightmap {
|
|
7
|
+
/** (size+1)^2 samples, normalised to 0..1, row-major, right and bottom edges
|
|
8
|
+
* backfilled so RTIN gets its 2^n+1 grid. */
|
|
9
|
+
data: Float32Array;
|
|
10
|
+
/** RTIN grid size — `tileSize + 1`. */
|
|
11
|
+
gridSize: number;
|
|
12
|
+
/** Heightmap resolution the grid was built from, a power of two. */
|
|
13
|
+
tileSize: number;
|
|
14
|
+
/** Source-unit range, before normalisation. Metres for terrain-rgb. */
|
|
15
|
+
min: number;
|
|
16
|
+
max: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Heightmap pixels to a normalised RTIN grid.
|
|
20
|
+
*
|
|
21
|
+
* Normalising to 0..1 here rather than baking `elevationScale` in is what lets
|
|
22
|
+
* the error map be built once: RTIN's error scales linearly with the height
|
|
23
|
+
* scale, so a change of scale is a change of error budget, not a new error map.
|
|
24
|
+
*/
|
|
25
|
+
export declare function decodeHeightmap(image: HTMLImageElement, encoding: HeightEncodingName): DecodedHeightmap;
|
|
26
|
+
export interface TextureArrayResult {
|
|
27
|
+
texture: THREE.DataArrayTexture;
|
|
28
|
+
/** Mean colour per layer, in the array's own encoding. Used by the
|
|
29
|
+
* variance-preserving stochastic blend, which needs the texture's mean to
|
|
30
|
+
* restore contrast the blend would otherwise wash out. Free to compute here
|
|
31
|
+
* because the pixels are already in hand. */
|
|
32
|
+
means: THREE.Vector3[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Pack images into one `DataArrayTexture`.
|
|
36
|
+
*
|
|
37
|
+
* Every layer must be the same size, and user uploads never are, so each image
|
|
38
|
+
* is drawn into a canvas at `size` first. `alphaFrom` supplies a second image
|
|
39
|
+
* per layer whose red channel becomes the layer's alpha — used to carry each
|
|
40
|
+
* surface's height map in the normal array's spare channel rather than paying
|
|
41
|
+
* for a whole third array.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildTextureArray(images: (HTMLImageElement | null)[], size: number, colorSpace: THREE.ColorSpace, alphaFrom?: (HTMLImageElement | null)[], fallback?: [number, number, number, number]): TextureArrayResult;
|
|
44
|
+
/** A plain 2D texture from an already-loaded image, without a round trip through
|
|
45
|
+
* TextureLoader — the image is in the cache either way. */
|
|
46
|
+
export declare function textureFromImage(image: HTMLImageElement, colorSpace: THREE.ColorSpace): THREE.Texture;
|
|
47
|
+
/**
|
|
48
|
+
* Light separable binomial blur over the decoded field.
|
|
49
|
+
*
|
|
50
|
+
* This is not cosmetic. An 8-bit heightmap has 256 levels, which over 340 m of
|
|
51
|
+
* relief is a 1.33 m step — and RTIN's error metric measures exactly that step.
|
|
52
|
+
* Measured on the highlands map: a 1.7 m budget sits above the quantisation
|
|
53
|
+
* noise and returns 68k triangles (visibly polygonal), while 0.4 m sits below it
|
|
54
|
+
* and returns 1.2M, the whole grid. There is no useful setting in between,
|
|
55
|
+
* because the staircase, not the terrain, is what the metric is seeing.
|
|
56
|
+
*
|
|
57
|
+
* Smoothing first removes the staircase and gives the error map a real signal,
|
|
58
|
+
* so the budget means what it says. The smoothed field is what RTIN, the field
|
|
59
|
+
* texture and the CPU height query all use; the vertex shader keeps sampling the
|
|
60
|
+
* raw texture, where its own bicubic filter does the same job on the GPU.
|
|
61
|
+
*/
|
|
62
|
+
export declare function smoothHeightfield(height: DecodedHeightmap, passes?: number): DecodedHeightmap;
|
|
63
|
+
export interface FieldTextureResult {
|
|
64
|
+
texture: THREE.DataTexture;
|
|
65
|
+
/** Largest absolute one-texel gradient in the field, used to unpack RG. */
|
|
66
|
+
gradScale: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The height field as something the FRAGMENT stage can read: one texture, one
|
|
70
|
+
* fetch, carrying both the surface gradient and the height.
|
|
71
|
+
*
|
|
72
|
+
* R, G the one-texel gradient in x and z, biased to 0..1 by `gradScale`
|
|
73
|
+
* B the height, 0..1
|
|
74
|
+
*
|
|
75
|
+
* ## Why this exists
|
|
76
|
+
*
|
|
77
|
+
* Because splatting and shading off the interpolated vertex height is wrong, and
|
|
78
|
+
* that is exactly what the first version did. A linearly interpolated quantity
|
|
79
|
+
* has straight iso-lines within a triangle that kink at its edges, so every
|
|
80
|
+
* splat threshold is drawn along the triangulation — big polygonal patches of
|
|
81
|
+
* flat colour — and the shading normal is faceted for the same reason. The
|
|
82
|
+
* terrain material's own comment says so: *shade against the evaluated surface,
|
|
83
|
+
* not the interpolated mesh vertex*.
|
|
84
|
+
*
|
|
85
|
+
* Sampling this in the fragment shader costs one texture fetch and makes the
|
|
86
|
+
* mesh's own resolution invisible: the ground shades and splats smoothly however
|
|
87
|
+
* coarse the triangles under it are.
|
|
88
|
+
*
|
|
89
|
+
* Eight bits per channel is enough here. The gradient is a normal map by another
|
|
90
|
+
* name, and the height only drives auto-splat band thresholds that are already
|
|
91
|
+
* noise-jittered and smoothstepped.
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildFieldTexture(height: DecodedHeightmap): FieldTextureResult;
|
|
94
|
+
//# sourceMappingURL=landscapeTextures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landscapeTextures.d.ts","sourceRoot":"","sources":["../../src/utils/landscapeTextures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAe9B,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAYhE;AAwBD,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,aAAa,GAAG,MAAM,CAAA;AAErE,MAAM,WAAW,gBAAgB;IAC7B;kDAC8C;IAC9C,IAAI,EAAE,YAAY,CAAA;IAClB,uCAAuC;IACvC,QAAQ,EAAE,MAAM,CAAA;IAChB,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAA;IAChB,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;CACd;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,kBAAkB,GAAG,gBAAgB,CA4CvG;AAED,MAAM,WAAW,kBAAkB;IAC/B,OAAO,EAAE,KAAK,CAAC,gBAAgB,CAAA;IAC/B;;;kDAG8C;IAC9C,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,CAAA;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC7B,MAAM,EAAE,CAAC,gBAAgB,GAAG,IAAI,CAAC,EAAE,EACnC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,KAAK,CAAC,UAAU,EAC5B,SAAS,CAAC,EAAE,CAAC,gBAAgB,GAAG,IAAI,CAAC,EAAE,EACvC,QAAQ,GAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAwB,GAClE,kBAAkB,CA2DpB;AAED;4DAC4D;AAC5D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,OAAO,CAUrG;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,EAAE,MAAM,SAAI,GAAG,gBAAgB,CA2BxF;AAED,MAAM,WAAW,kBAAkB;IAC/B,OAAO,EAAE,KAAK,CAAC,WAAW,CAAA;IAC1B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,GAAG,kBAAkB,CA0C9E"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
/**
|
|
3
|
+
* Image decoding and texture-array packing for the landscape.
|
|
4
|
+
*
|
|
5
|
+
* Everything here reads pixels through a 2D canvas, which means every image must
|
|
6
|
+
* be CORS-clean: a cross-origin image served without `Access-Control-Allow-Origin`
|
|
7
|
+
* taints the canvas and `getImageData` throws. The failure is confusing when it
|
|
8
|
+
* happens — the texture itself renders fine and only the landscape fails to
|
|
9
|
+
* build — so `loadImage` sets `crossOrigin` and the callers report the origin in
|
|
10
|
+
* the error.
|
|
11
|
+
*/
|
|
12
|
+
const imageCache = new Map();
|
|
13
|
+
export function loadImage(url) {
|
|
14
|
+
const cached = imageCache.get(url);
|
|
15
|
+
if (cached)
|
|
16
|
+
return cached;
|
|
17
|
+
const promise = new Promise((resolve, reject) => {
|
|
18
|
+
const image = new window.Image();
|
|
19
|
+
image.crossOrigin = 'anonymous';
|
|
20
|
+
image.onload = () => resolve(image);
|
|
21
|
+
image.onerror = () => reject(new Error(`landscape: failed to load ${url}`));
|
|
22
|
+
image.src = url;
|
|
23
|
+
});
|
|
24
|
+
imageCache.set(url, promise);
|
|
25
|
+
return promise;
|
|
26
|
+
}
|
|
27
|
+
function canvasOf(width, height) {
|
|
28
|
+
const canvas = document.createElement('canvas');
|
|
29
|
+
canvas.width = width;
|
|
30
|
+
canvas.height = height;
|
|
31
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
32
|
+
if (!ctx)
|
|
33
|
+
throw new Error('landscape: no 2d context');
|
|
34
|
+
return { canvas, ctx };
|
|
35
|
+
}
|
|
36
|
+
function readPixels(image, size) {
|
|
37
|
+
const { ctx } = canvasOf(size, size);
|
|
38
|
+
ctx.drawImage(image, 0, 0, size, size);
|
|
39
|
+
try {
|
|
40
|
+
return ctx.getImageData(0, 0, size, size).data;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new Error(`landscape: cannot read pixels from ${image.src} — the canvas is tainted. `
|
|
44
|
+
+ 'The asset host must send Access-Control-Allow-Origin.');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Largest power of two at or below n, clamped to something RTIN can chew. */
|
|
48
|
+
export function floorPowerOfTwo(n) {
|
|
49
|
+
return Math.max(16, Math.min(4096, 2 ** Math.floor(Math.log2(n))));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Heightmap pixels to a normalised RTIN grid.
|
|
53
|
+
*
|
|
54
|
+
* Normalising to 0..1 here rather than baking `elevationScale` in is what lets
|
|
55
|
+
* the error map be built once: RTIN's error scales linearly with the height
|
|
56
|
+
* scale, so a change of scale is a change of error budget, not a new error map.
|
|
57
|
+
*/
|
|
58
|
+
export function decodeHeightmap(image, encoding) {
|
|
59
|
+
const tileSize = floorPowerOfTwo(Math.min(image.width, image.height));
|
|
60
|
+
const gridSize = tileSize + 1;
|
|
61
|
+
const pixels = readPixels(image, tileSize);
|
|
62
|
+
const raw = new Float32Array(gridSize * gridSize);
|
|
63
|
+
let min = Infinity;
|
|
64
|
+
let max = -Infinity;
|
|
65
|
+
for (let y = 0; y < tileSize; y++) {
|
|
66
|
+
for (let x = 0; x < tileSize; x++) {
|
|
67
|
+
const k = (y * tileSize + x) * 4;
|
|
68
|
+
const r = pixels[k];
|
|
69
|
+
const g = pixels[k + 1];
|
|
70
|
+
const b = pixels[k + 2];
|
|
71
|
+
let h;
|
|
72
|
+
if (encoding === 'terrain-rgb') {
|
|
73
|
+
h = (r * 65536 + g * 256 + b) / 10 - 10000;
|
|
74
|
+
}
|
|
75
|
+
else if (encoding === 'rg16') {
|
|
76
|
+
h = (r * 256 + g) / 65535;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
h = r / 255;
|
|
80
|
+
}
|
|
81
|
+
raw[y * gridSize + x] = h;
|
|
82
|
+
if (h < min)
|
|
83
|
+
min = h;
|
|
84
|
+
if (h > max)
|
|
85
|
+
max = h;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// RTIN wants 2^n+1 samples; the last row and column are copies of their
|
|
89
|
+
// neighbours so the extra edge does not invent a cliff.
|
|
90
|
+
for (let x = 0; x < gridSize - 1; x++) {
|
|
91
|
+
raw[gridSize * (gridSize - 1) + x] = raw[gridSize * (gridSize - 2) + x];
|
|
92
|
+
}
|
|
93
|
+
for (let y = 0; y < gridSize; y++) {
|
|
94
|
+
raw[gridSize * y + gridSize - 1] = raw[gridSize * y + gridSize - 2];
|
|
95
|
+
}
|
|
96
|
+
const range = max - min;
|
|
97
|
+
if (range > 1e-9 && (encoding !== 'grayscale' || min !== 0 || max !== 1)) {
|
|
98
|
+
for (let i = 0; i < raw.length; i++)
|
|
99
|
+
raw[i] = (raw[i] - min) / range;
|
|
100
|
+
}
|
|
101
|
+
return { data: raw, gridSize, tileSize, min, max };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Pack images into one `DataArrayTexture`.
|
|
105
|
+
*
|
|
106
|
+
* Every layer must be the same size, and user uploads never are, so each image
|
|
107
|
+
* is drawn into a canvas at `size` first. `alphaFrom` supplies a second image
|
|
108
|
+
* per layer whose red channel becomes the layer's alpha — used to carry each
|
|
109
|
+
* surface's height map in the normal array's spare channel rather than paying
|
|
110
|
+
* for a whole third array.
|
|
111
|
+
*/
|
|
112
|
+
export function buildTextureArray(images, size, colorSpace, alphaFrom, fallback = [255, 255, 255, 255]) {
|
|
113
|
+
const layers = images.length;
|
|
114
|
+
const stride = size * size * 4;
|
|
115
|
+
const data = new Uint8Array(stride * layers);
|
|
116
|
+
const means = [];
|
|
117
|
+
for (let i = 0; i < layers; i++) {
|
|
118
|
+
const image = images[i];
|
|
119
|
+
const offset = i * stride;
|
|
120
|
+
let sumR = 0;
|
|
121
|
+
let sumG = 0;
|
|
122
|
+
let sumB = 0;
|
|
123
|
+
if (image) {
|
|
124
|
+
const pixels = readPixels(image, size);
|
|
125
|
+
data.set(pixels, offset);
|
|
126
|
+
for (let p = 0; p < stride; p += 4) {
|
|
127
|
+
sumR += pixels[p];
|
|
128
|
+
sumG += pixels[p + 1];
|
|
129
|
+
sumB += pixels[p + 2];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
for (let p = 0; p < stride; p += 4) {
|
|
134
|
+
data[offset + p] = fallback[0];
|
|
135
|
+
data[offset + p + 1] = fallback[1];
|
|
136
|
+
data[offset + p + 2] = fallback[2];
|
|
137
|
+
data[offset + p + 3] = fallback[3];
|
|
138
|
+
}
|
|
139
|
+
sumR = (fallback[0] * stride) / 4;
|
|
140
|
+
sumG = (fallback[1] * stride) / 4;
|
|
141
|
+
sumB = (fallback[2] * stride) / 4;
|
|
142
|
+
}
|
|
143
|
+
const alphaImage = alphaFrom?.[i];
|
|
144
|
+
if (alphaImage) {
|
|
145
|
+
const alpha = readPixels(alphaImage, size);
|
|
146
|
+
for (let p = 0; p < stride; p += 4)
|
|
147
|
+
data[offset + p + 3] = alpha[p];
|
|
148
|
+
}
|
|
149
|
+
else if (alphaFrom) {
|
|
150
|
+
// No height map for this layer: a flat mid height blends linearly,
|
|
151
|
+
// which is what every layer does in the reference implementation.
|
|
152
|
+
for (let p = 0; p < stride; p += 4)
|
|
153
|
+
data[offset + p + 3] = 128;
|
|
154
|
+
}
|
|
155
|
+
const texels = (size * size) || 1;
|
|
156
|
+
means.push(new THREE.Vector3(sumR / texels / 255, sumG / texels / 255, sumB / texels / 255));
|
|
157
|
+
}
|
|
158
|
+
const texture = new THREE.DataArrayTexture(data, size, size, layers);
|
|
159
|
+
texture.format = THREE.RGBAFormat;
|
|
160
|
+
texture.type = THREE.UnsignedByteType;
|
|
161
|
+
texture.colorSpace = colorSpace;
|
|
162
|
+
texture.minFilter = THREE.LinearMipMapLinearFilter;
|
|
163
|
+
texture.magFilter = THREE.LinearFilter;
|
|
164
|
+
texture.wrapS = THREE.RepeatWrapping;
|
|
165
|
+
texture.wrapT = THREE.RepeatWrapping;
|
|
166
|
+
texture.generateMipmaps = true;
|
|
167
|
+
texture.needsUpdate = true;
|
|
168
|
+
return { texture, means };
|
|
169
|
+
}
|
|
170
|
+
/** A plain 2D texture from an already-loaded image, without a round trip through
|
|
171
|
+
* TextureLoader — the image is in the cache either way. */
|
|
172
|
+
export function textureFromImage(image, colorSpace) {
|
|
173
|
+
const texture = new THREE.Texture(image);
|
|
174
|
+
texture.colorSpace = colorSpace;
|
|
175
|
+
texture.wrapS = THREE.ClampToEdgeWrapping;
|
|
176
|
+
texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
177
|
+
texture.minFilter = THREE.LinearMipMapLinearFilter;
|
|
178
|
+
texture.magFilter = THREE.LinearFilter;
|
|
179
|
+
texture.generateMipmaps = true;
|
|
180
|
+
texture.needsUpdate = true;
|
|
181
|
+
return texture;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Light separable binomial blur over the decoded field.
|
|
185
|
+
*
|
|
186
|
+
* This is not cosmetic. An 8-bit heightmap has 256 levels, which over 340 m of
|
|
187
|
+
* relief is a 1.33 m step — and RTIN's error metric measures exactly that step.
|
|
188
|
+
* Measured on the highlands map: a 1.7 m budget sits above the quantisation
|
|
189
|
+
* noise and returns 68k triangles (visibly polygonal), while 0.4 m sits below it
|
|
190
|
+
* and returns 1.2M, the whole grid. There is no useful setting in between,
|
|
191
|
+
* because the staircase, not the terrain, is what the metric is seeing.
|
|
192
|
+
*
|
|
193
|
+
* Smoothing first removes the staircase and gives the error map a real signal,
|
|
194
|
+
* so the budget means what it says. The smoothed field is what RTIN, the field
|
|
195
|
+
* texture and the CPU height query all use; the vertex shader keeps sampling the
|
|
196
|
+
* raw texture, where its own bicubic filter does the same job on the GPU.
|
|
197
|
+
*/
|
|
198
|
+
export function smoothHeightfield(height, passes = 1) {
|
|
199
|
+
const { gridSize } = height;
|
|
200
|
+
let src = height.data;
|
|
201
|
+
for (let pass = 0; pass < passes; pass++) {
|
|
202
|
+
const tmp = new Float32Array(src.length);
|
|
203
|
+
const out = new Float32Array(src.length);
|
|
204
|
+
// horizontal, then vertical: [1 2 1] / 4
|
|
205
|
+
for (let y = 0; y < gridSize; y++) {
|
|
206
|
+
const row = y * gridSize;
|
|
207
|
+
for (let x = 0; x < gridSize; x++) {
|
|
208
|
+
const l = src[row + Math.max(x - 1, 0)];
|
|
209
|
+
const c = src[row + x];
|
|
210
|
+
const r = src[row + Math.min(x + 1, gridSize - 1)];
|
|
211
|
+
tmp[row + x] = (l + 2 * c + r) * 0.25;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
for (let y = 0; y < gridSize; y++) {
|
|
215
|
+
const up = Math.max(y - 1, 0) * gridSize;
|
|
216
|
+
const dn = Math.min(y + 1, gridSize - 1) * gridSize;
|
|
217
|
+
const row = y * gridSize;
|
|
218
|
+
for (let x = 0; x < gridSize; x++) {
|
|
219
|
+
out[row + x] = (tmp[up + x] + 2 * tmp[row + x] + tmp[dn + x]) * 0.25;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
src = out;
|
|
223
|
+
}
|
|
224
|
+
return { ...height, data: src };
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The height field as something the FRAGMENT stage can read: one texture, one
|
|
228
|
+
* fetch, carrying both the surface gradient and the height.
|
|
229
|
+
*
|
|
230
|
+
* R, G the one-texel gradient in x and z, biased to 0..1 by `gradScale`
|
|
231
|
+
* B the height, 0..1
|
|
232
|
+
*
|
|
233
|
+
* ## Why this exists
|
|
234
|
+
*
|
|
235
|
+
* Because splatting and shading off the interpolated vertex height is wrong, and
|
|
236
|
+
* that is exactly what the first version did. A linearly interpolated quantity
|
|
237
|
+
* has straight iso-lines within a triangle that kink at its edges, so every
|
|
238
|
+
* splat threshold is drawn along the triangulation — big polygonal patches of
|
|
239
|
+
* flat colour — and the shading normal is faceted for the same reason. The
|
|
240
|
+
* terrain material's own comment says so: *shade against the evaluated surface,
|
|
241
|
+
* not the interpolated mesh vertex*.
|
|
242
|
+
*
|
|
243
|
+
* Sampling this in the fragment shader costs one texture fetch and makes the
|
|
244
|
+
* mesh's own resolution invisible: the ground shades and splats smoothly however
|
|
245
|
+
* coarse the triangles under it are.
|
|
246
|
+
*
|
|
247
|
+
* Eight bits per channel is enough here. The gradient is a normal map by another
|
|
248
|
+
* name, and the height only drives auto-splat band thresholds that are already
|
|
249
|
+
* noise-jittered and smoothstepped.
|
|
250
|
+
*/
|
|
251
|
+
export function buildFieldTexture(height) {
|
|
252
|
+
const { data, gridSize, tileSize } = height;
|
|
253
|
+
const size = tileSize;
|
|
254
|
+
const gx = new Float32Array(size * size);
|
|
255
|
+
const gy = new Float32Array(size * size);
|
|
256
|
+
let maxGrad = 1e-6;
|
|
257
|
+
for (let y = 0; y < size; y++) {
|
|
258
|
+
for (let x = 0; x < size; x++) {
|
|
259
|
+
const xl = Math.max(x - 1, 0);
|
|
260
|
+
const xr = Math.min(x + 1, size - 1);
|
|
261
|
+
const yu = Math.max(y - 1, 0);
|
|
262
|
+
const yd = Math.min(y + 1, size - 1);
|
|
263
|
+
const dx = (data[y * gridSize + xr] - data[y * gridSize + xl]) * 0.5;
|
|
264
|
+
const dy = (data[yd * gridSize + x] - data[yu * gridSize + x]) * 0.5;
|
|
265
|
+
gx[y * size + x] = dx;
|
|
266
|
+
gy[y * size + x] = dy;
|
|
267
|
+
const m = Math.max(Math.abs(dx), Math.abs(dy));
|
|
268
|
+
if (m > maxGrad)
|
|
269
|
+
maxGrad = m;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const pixels = new Uint8Array(size * size * 4);
|
|
273
|
+
for (let i = 0; i < size * size; i++) {
|
|
274
|
+
const x = i % size;
|
|
275
|
+
const y = (i / size) | 0;
|
|
276
|
+
pixels[i * 4 + 0] = Math.round(THREE.MathUtils.clamp(gx[i] / maxGrad * 0.5 + 0.5, 0, 1) * 255);
|
|
277
|
+
pixels[i * 4 + 1] = Math.round(THREE.MathUtils.clamp(gy[i] / maxGrad * 0.5 + 0.5, 0, 1) * 255);
|
|
278
|
+
pixels[i * 4 + 2] = Math.round(THREE.MathUtils.clamp(data[y * gridSize + x], 0, 1) * 255);
|
|
279
|
+
pixels[i * 4 + 3] = 255;
|
|
280
|
+
}
|
|
281
|
+
const texture = new THREE.DataTexture(pixels, size, size, THREE.RGBAFormat, THREE.UnsignedByteType);
|
|
282
|
+
texture.colorSpace = THREE.NoColorSpace;
|
|
283
|
+
texture.wrapS = THREE.ClampToEdgeWrapping;
|
|
284
|
+
texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
285
|
+
texture.minFilter = THREE.LinearMipMapLinearFilter;
|
|
286
|
+
texture.magFilter = THREE.LinearFilter;
|
|
287
|
+
texture.generateMipmaps = true;
|
|
288
|
+
texture.needsUpdate = true;
|
|
289
|
+
return { texture, gradScale: maxGrad };
|
|
290
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import type { LandscapeConfig, LandscapeQualityConfig } from '../types/landscapeConfigs';
|
|
3
|
+
import type { LandscapeUniforms } from '../shaders/landscape/LandscapeMaterial';
|
|
4
|
+
export interface LandscapeMapUniforms {
|
|
5
|
+
height: THREE.Texture | null;
|
|
6
|
+
heightSize: THREE.Vector2;
|
|
7
|
+
/** RG gradient + B height, sampled per fragment. */
|
|
8
|
+
field: THREE.Texture | null;
|
|
9
|
+
fieldSize: THREE.Vector2;
|
|
10
|
+
gradScale: number;
|
|
11
|
+
diffuse: THREE.DataArrayTexture | null;
|
|
12
|
+
normal: THREE.DataArrayTexture | null;
|
|
13
|
+
splat0: THREE.Texture | null;
|
|
14
|
+
splat1: THREE.Texture | null;
|
|
15
|
+
ao: THREE.Texture | null;
|
|
16
|
+
means: THREE.Vector3[];
|
|
17
|
+
}
|
|
18
|
+
export declare function applyLandscapeMaps(uniforms: LandscapeUniforms, maps: LandscapeMapUniforms): void;
|
|
19
|
+
export declare function applyLandscapeUniforms(uniforms: LandscapeUniforms, config: LandscapeConfig, quality: LandscapeQualityConfig): void;
|
|
20
|
+
//# sourceMappingURL=landscapeUniforms.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landscapeUniforms.d.ts","sourceRoot":"","sources":["../../src/utils/landscapeUniforms.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAA;AACxF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AAa/E,MAAM,WAAW,oBAAoB;IACjC,MAAM,EAAE,KAAK,CAAC,OAAO,GAAG,IAAI,CAAA;IAC5B,UAAU,EAAE,KAAK,CAAC,OAAO,CAAA;IACzB,oDAAoD;IACpD,KAAK,EAAE,KAAK,CAAC,OAAO,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,KAAK,CAAC,OAAO,CAAA;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,KAAK,CAAC,gBAAgB,GAAG,IAAI,CAAA;IACtC,MAAM,EAAE,KAAK,CAAC,gBAAgB,GAAG,IAAI,CAAA;IACrC,MAAM,EAAE,KAAK,CAAC,OAAO,GAAG,IAAI,CAAA;IAC5B,MAAM,EAAE,KAAK,CAAC,OAAO,GAAG,IAAI,CAAA;IAC5B,EAAE,EAAE,KAAK,CAAC,OAAO,GAAG,IAAI,CAAA;IACxB,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,CAAA;CACzB;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,IAAI,EAAE,oBAAoB,GAAG,IAAI,CAoBhG;AAED,wBAAgB,sBAAsB,CAClC,QAAQ,EAAE,iBAAiB,EAC3B,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,sBAAsB,GAChC,IAAI,CAmDN"}
|