@aethervtt/shard-terrain 0.0.1
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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/biomes.d.ts +65 -0
- package/dist/biomes.js +163 -0
- package/dist/chunk.d.ts +99 -0
- package/dist/chunk.js +304 -0
- package/dist/colliders.d.ts +86 -0
- package/dist/colliders.js +356 -0
- package/dist/components.d.ts +49 -0
- package/dist/components.js +137 -0
- package/dist/cube.d.ts +51 -0
- package/dist/cube.js +87 -0
- package/dist/frame.d.ts +21 -0
- package/dist/frame.js +63 -0
- package/dist/heights.d.ts +51 -0
- package/dist/heights.js +137 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +193 -0
- package/dist/kernel.d.ts +47 -0
- package/dist/kernel.js +243 -0
- package/dist/lod.d.ts +22 -0
- package/dist/lod.js +44 -0
- package/dist/material.d.ts +14 -0
- package/dist/material.js +426 -0
- package/dist/methods.d.ts +12 -0
- package/dist/methods.js +259 -0
- package/dist/nav.d.ts +14 -0
- package/dist/nav.js +114 -0
- package/dist/overlays.d.ts +6 -0
- package/dist/overlays.js +67 -0
- package/dist/planet.d.ts +106 -0
- package/dist/planet.js +313 -0
- package/dist/plugin.d.ts +23 -0
- package/dist/plugin.js +98 -0
- package/dist/points.d.ts +62 -0
- package/dist/points.js +207 -0
- package/dist/quadtree.d.ts +153 -0
- package/dist/quadtree.js +587 -0
- package/dist/render.d.ts +179 -0
- package/dist/render.js +1222 -0
- package/dist/test-planet.d.ts +59 -0
- package/dist/test-planet.js +189 -0
- package/dist/testing.d.ts +42 -0
- package/dist/testing.js +96 -0
- package/dist/view.d.ts +12 -0
- package/dist/view.js +61 -0
- package/package.json +50 -0
package/dist/material.js
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { t } from "@aethervtt/shard-core";
|
|
2
|
+
import { defineMaterial } from "@aethervtt/shard-render";
|
|
3
|
+
const TEXTURE_PERIOD = 1024;
|
|
4
|
+
const frame = {
|
|
5
|
+
center: t.vec4({
|
|
6
|
+
hidden: true,
|
|
7
|
+
description: "Set by the terrain every frame: the planet center (origin frame) and radius."
|
|
8
|
+
}),
|
|
9
|
+
rot0: t.vec4({
|
|
10
|
+
hidden: true,
|
|
11
|
+
description: "Set by the terrain: origin \u2192 planet rotation rows (xyz), planet-space texture origin (w)."
|
|
12
|
+
}),
|
|
13
|
+
rot1: t.vec4({ hidden: true }),
|
|
14
|
+
rot2: t.vec4({ hidden: true }),
|
|
15
|
+
camera: t.vec4({
|
|
16
|
+
hidden: true,
|
|
17
|
+
description: "Set by the terrain: the selecting camera (xyz) and pixels per radian / errorPixels (w)."
|
|
18
|
+
}),
|
|
19
|
+
tiles: t.vec4({
|
|
20
|
+
hidden: true,
|
|
21
|
+
description: "Set by the terrain: vertices per chunk edge, normal-tile edge (0: no tiles), tiles per layer row, layer size."
|
|
22
|
+
})
|
|
23
|
+
};
|
|
24
|
+
const PlanetMaterial = defineMaterial("terrain/PlanetSurface", {
|
|
25
|
+
shader: "terrain::planet",
|
|
26
|
+
arrays: ["albedoArray", "normalArray", "ormArray", "normalTiles"],
|
|
27
|
+
// Its surface is its own (no standard_input): room for the normal tiles in 16 textures a stage.
|
|
28
|
+
standardTextures: false,
|
|
29
|
+
fields: {
|
|
30
|
+
albedoArray: t.handle("Texture", { description: "The BiomeSet\u2019s albedo array." }),
|
|
31
|
+
normalArray: t.handle("Texture", { description: "The BiomeSet\u2019s normal map array." }),
|
|
32
|
+
ormArray: t.handle("Texture", {
|
|
33
|
+
description: "The BiomeSet\u2019s occlusion/roughness/metallic array."
|
|
34
|
+
}),
|
|
35
|
+
normalTiles: t.handle("Texture", {
|
|
36
|
+
hidden: true,
|
|
37
|
+
description: "Set by the terrain: every chunk\u2019s normals at twice its vertex density, one tile per slot."
|
|
38
|
+
}),
|
|
39
|
+
biomeTable: t.handle("Texture", {
|
|
40
|
+
hidden: true,
|
|
41
|
+
description: "Set by the terrain: biome ranges, tints, and layers as a small float texture."
|
|
42
|
+
}),
|
|
43
|
+
...frame,
|
|
44
|
+
biomeParams: t.vec4({
|
|
45
|
+
hidden: true,
|
|
46
|
+
description: "Set by the terrain: biome count, latitude bias, snow line, texture period."
|
|
47
|
+
}),
|
|
48
|
+
debugParams: t.vec4({
|
|
49
|
+
hidden: true,
|
|
50
|
+
description: "Set by the terrain: debug mode (0 off, 1 dominant biome, 2 seams, 3 levels), far-texturing distance, has ORM, 0."
|
|
51
|
+
})
|
|
52
|
+
},
|
|
53
|
+
description: "Planet terrain: biome blending and triplanar texture-array layers, with geomorphing between LOD levels."
|
|
54
|
+
});
|
|
55
|
+
const OceanMaterial = defineMaterial("terrain/Ocean", {
|
|
56
|
+
shader: "terrain::ocean",
|
|
57
|
+
blend: "alpha",
|
|
58
|
+
fields: {
|
|
59
|
+
...frame,
|
|
60
|
+
shallow: t.color({ default: [0.05, 0.35, 0.4, 1], description: "Color over shallow ground." }),
|
|
61
|
+
deep: t.color({ default: [5e-3, 0.03, 0.08, 1], description: "Color over deep water." }),
|
|
62
|
+
water: t.vec4({
|
|
63
|
+
default: [0.08, 0.25, 0.35, 0.04],
|
|
64
|
+
description: "x: color depth falloff (1/m), y: opacity falloff (1/m), z: minimum opacity, w: roughness."
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
description: "Planet ocean: a lit transparent surface with scrolling wave normals."
|
|
68
|
+
});
|
|
69
|
+
const MORPH = (u) => `
|
|
70
|
+
/**
|
|
71
|
+
* How far toward its parent level a vertex has morphed: by distance within the parent's split
|
|
72
|
+
* band; overridden on ring edges by the instance's lock bits (render/InstanceData.y, two bits per
|
|
73
|
+
* edge: 1 this level, 2 the parent's), and on center lines where a partial draw (quadrant mask in
|
|
74
|
+
* InstanceData.x) meets a child's quadrant (this level, which the child's edge matches). Never less
|
|
75
|
+
* than the chunk's fade (the rest of InstanceData.x), 1 as it replaces its parent, easing to 0.
|
|
76
|
+
*/
|
|
77
|
+
/** The grid point (i, j) and normal tile of a vertex, from uv1.x (kernel.ts packGrid). */
|
|
78
|
+
fn terrain_grid() -> vec3u {
|
|
79
|
+
let n = u32(${u}.tiles.x + 0.5);
|
|
80
|
+
let p = u32(vertex_uv1().x + 0.5);
|
|
81
|
+
let ij = p % (n * n);
|
|
82
|
+
return vec3u(ij % n, ij / n, p / (n * n));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A grid point's lock code (chunk.ts lockCode): its edge on the border ring (0 bottom, 1 right,
|
|
87
|
+
* 2 top, 3 left, each edge owning its first corner), the center lines (4-7), the center (8), else -1.
|
|
88
|
+
*/
|
|
89
|
+
fn terrain_lock(i: u32, j: u32) -> i32 {
|
|
90
|
+
let s = u32(${u}.tiles.x + 0.5) - 1u;
|
|
91
|
+
let h = s / 2u;
|
|
92
|
+
if (j == 0u && i < s) { return 0; }
|
|
93
|
+
if (i == s && j < s) { return 1; }
|
|
94
|
+
if (j == s && i > 0u) { return 2; }
|
|
95
|
+
if (i == 0u && j > 0u) { return 3; }
|
|
96
|
+
if (i == h && j == h) { return 8; }
|
|
97
|
+
if (i == h) { return select(5, 4, j < h); }
|
|
98
|
+
if (j == h) { return select(7, 6, i < h); }
|
|
99
|
+
return -1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn terrain_morph(position: vec3f) -> f32 {
|
|
103
|
+
let lock = vertex_uv1();
|
|
104
|
+
let grid = terrain_grid();
|
|
105
|
+
let data = vertex_instance_data();
|
|
106
|
+
let mask = u32(floor(data.x * 0.5 + 0.001));
|
|
107
|
+
let fade = clamp(data.x - f32(mask) * 2.0, 0.0, 1.0);
|
|
108
|
+
var t = 0.0;
|
|
109
|
+
let split = lock.y * ${u}.camera.w;
|
|
110
|
+
if (split > 0.0) {
|
|
111
|
+
let d = distance(vertex_world(position), ${u}.camera.xyz);
|
|
112
|
+
t = clamp((d - 0.5 * split) / (0.45 * split), 0.0, 1.0);
|
|
113
|
+
}
|
|
114
|
+
let code = terrain_lock(grid.x, grid.y);
|
|
115
|
+
if (code >= 0 && code < 4) {
|
|
116
|
+
// Edges never fade: the neighbor across may not be fading.
|
|
117
|
+
let bits = (u32(data.y + 0.5) >> (u32(code) * 2u)) & 3u;
|
|
118
|
+
if (bits == 1u) { return 0.0; } else if (bits == 2u) { return 1.0; }
|
|
119
|
+
return t;
|
|
120
|
+
} else if (code >= 4 && mask != 15u) {
|
|
121
|
+
// Quadrants on either side of this center-line half (all four at the center).
|
|
122
|
+
var a = 0u;
|
|
123
|
+
var b = 1u;
|
|
124
|
+
if (code == 5) { a = 2u; b = 3u; } else if (code == 6) { a = 0u; b = 2u; } else if (code == 7) { a = 1u; b = 3u; }
|
|
125
|
+
let differ = ((mask >> a) & 1u) != ((mask >> b) & 1u);
|
|
126
|
+
if (differ || (code == 8 && mask != 0u)) { return 0.0; }
|
|
127
|
+
}
|
|
128
|
+
return max(t, fade);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
override fn vertex_position(position: vec3f, normal: vec3f, uv: vec2f) -> vec3f {
|
|
132
|
+
return position + vertex_tangent().xyz * terrain_morph(position);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** To the surface stage: the grid point as uv across the chunk (xy), its normal tile (z), tiles on (w). */
|
|
136
|
+
override fn vertex_extra(position: vec3f, normal: vec3f, uv: vec2f) -> vec4f {
|
|
137
|
+
let grid = terrain_grid();
|
|
138
|
+
let s = ${u}.tiles.x - 1.0;
|
|
139
|
+
return vec4f(f32(grid.x) / s, f32(grid.y) / s, f32(grid.z), select(0.0, 1.0, ${u}.tiles.y > 0.0));
|
|
140
|
+
}
|
|
141
|
+
`;
|
|
142
|
+
const TERRAIN_SHADERS = {
|
|
143
|
+
"terrain::planet": `
|
|
144
|
+
import shard::pbr::types::{ VertexOutput, PbrInput };
|
|
145
|
+
import shard::view::view;
|
|
146
|
+
import shard::mesh::{ vertex_uv1, vertex_tangent, vertex_world, vertex_instance_data };
|
|
147
|
+
import material::planet_surface::{ PlanetSurface, PlanetSurface_albedoArray, PlanetSurface_albedoArray_sampler, PlanetSurface_normalArray, PlanetSurface_normalArray_sampler, PlanetSurface_ormArray, PlanetSurface_ormArray_sampler, PlanetSurface_biomeTable, PlanetSurface_normalTiles, PlanetSurface_normalTiles_sampler };
|
|
148
|
+
${MORPH("PlanetSurface")}
|
|
149
|
+
fn biome_window(x: f32, lo: f32, hi: f32, blend: f32) -> f32 {
|
|
150
|
+
if (!(lo < hi)) { return 1.0; }
|
|
151
|
+
let soft = max(1e-6, blend * (hi - lo));
|
|
152
|
+
return smoothstep(lo - soft, lo + soft, x) * (1.0 - smoothstep(hi - soft, hi + soft, x));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fn table(texel: i32, biome: u32) -> vec4f {
|
|
156
|
+
return textureLoad(PlanetSurface_biomeTable, vec2i(texel, i32(biome)), 0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
struct Layered {
|
|
160
|
+
albedo: vec3f,
|
|
161
|
+
normal: vec3f,
|
|
162
|
+
orm: vec3f,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Screen derivatives of the planet-space position (taken in uniform control flow). */
|
|
166
|
+
struct Grads {
|
|
167
|
+
dx: vec3f,
|
|
168
|
+
dy: vec3f,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn albedo_at(uv: vec2f, l: i32, gx: vec2f, gy: vec2f) -> vec3f {
|
|
172
|
+
return textureSampleGrad(PlanetSurface_albedoArray, PlanetSurface_albedoArray_sampler, uv, l, gx, gy).rgb;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fn orm_at(uv: vec2f, l: i32, gx: vec2f, gy: vec2f) -> vec3f {
|
|
176
|
+
return textureSampleGrad(PlanetSurface_ormArray, PlanetSurface_ormArray_sampler, uv, l, gx, gy).rgb;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn normal_at(uv: vec2f, l: i32, gx: vec2f, gy: vec2f) -> vec3f {
|
|
180
|
+
return textureSampleGrad(PlanetSurface_normalArray, PlanetSurface_normalArray_sampler, uv, l, gx, gy).xyz * 2.0 - 1.0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** One array layer, triplanar in planet space (or top-down only, far away). */
|
|
184
|
+
fn sample_layer(layer: f32, scale: f32, q: vec3f, g: Grads, n: vec3f, bw: vec3f, near: bool) -> Layered {
|
|
185
|
+
var out: Layered;
|
|
186
|
+
let l = i32(layer + 0.5);
|
|
187
|
+
if (!near) {
|
|
188
|
+
// One projection, along the dominant axis, at a coarser scale: no shimmer from orbit.
|
|
189
|
+
let s = 1.0 / (scale * 8.0);
|
|
190
|
+
var uv = q.xz * s;
|
|
191
|
+
var gx = g.dx.xz * s;
|
|
192
|
+
var gy = g.dy.xz * s;
|
|
193
|
+
if (bw.x >= bw.y && bw.x >= bw.z) {
|
|
194
|
+
uv = q.zy * s;
|
|
195
|
+
gx = g.dx.zy * s;
|
|
196
|
+
gy = g.dy.zy * s;
|
|
197
|
+
} else if (bw.z >= bw.y) {
|
|
198
|
+
uv = q.xy * s;
|
|
199
|
+
gx = g.dx.xy * s;
|
|
200
|
+
gy = g.dy.xy * s;
|
|
201
|
+
}
|
|
202
|
+
out.albedo = albedo_at(uv, l, gx, gy);
|
|
203
|
+
out.orm = orm_at(uv, l, gx, gy);
|
|
204
|
+
out.normal = n;
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
let s = 1.0 / scale;
|
|
208
|
+
let ux = q.zy * s;
|
|
209
|
+
let uy = q.xz * s;
|
|
210
|
+
let uz = q.xy * s;
|
|
211
|
+
let xx = g.dx.zy * s;
|
|
212
|
+
let xy = g.dy.zy * s;
|
|
213
|
+
let yx = g.dx.xz * s;
|
|
214
|
+
let yy = g.dy.xz * s;
|
|
215
|
+
let zx = g.dx.xy * s;
|
|
216
|
+
let zy = g.dy.xy * s;
|
|
217
|
+
out.albedo = albedo_at(ux, l, xx, xy) * bw.x + albedo_at(uy, l, yx, yy) * bw.y + albedo_at(uz, l, zx, zy) * bw.z;
|
|
218
|
+
out.orm = orm_at(ux, l, xx, xy) * bw.x + orm_at(uy, l, yx, yy) * bw.y + orm_at(uz, l, zx, zy) * bw.z;
|
|
219
|
+
// Whiteout-blended tangent normals (Golus), in planet space.
|
|
220
|
+
var tx = normal_at(ux, l, xx, xy);
|
|
221
|
+
var ty = normal_at(uy, l, yx, yy);
|
|
222
|
+
var tz = normal_at(uz, l, zx, zy);
|
|
223
|
+
tx = vec3f(tx.xy + n.zy, abs(tx.z) * n.x);
|
|
224
|
+
ty = vec3f(ty.xy + n.xz, abs(ty.z) * n.y);
|
|
225
|
+
tz = vec3f(tz.xy + n.xy, abs(tz.z) * n.z);
|
|
226
|
+
out.normal = normalize(tx.zyx * bw.x + ty.xzy * bw.y + tz.xyz * bw.z);
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The surface normal from the chunk's normal tile (twice the vertex density, so relief finer than
|
|
232
|
+
* the geometry still shades), or the vertex normal without tiles.
|
|
233
|
+
*/
|
|
234
|
+
fn surface_normal(in: VertexOutput) -> vec3f {
|
|
235
|
+
if (in.extra.w < 0.5) { return normalize(in.world_normal); }
|
|
236
|
+
let t = PlanetSurface.tiles;
|
|
237
|
+
let per_row = u32(t.z + 0.5);
|
|
238
|
+
let tile = u32(round(in.extra.z));
|
|
239
|
+
let cell = tile % (per_row * per_row);
|
|
240
|
+
let layer = i32(tile / (per_row * per_row));
|
|
241
|
+
let corner = vec2f(f32(cell % per_row), f32(cell / per_row)) * t.y;
|
|
242
|
+
let texel = corner + 0.5 + clamp(in.extra.xy, vec2f(0.0), vec2f(1.0)) * (t.y - 1.0);
|
|
243
|
+
let c = textureSampleLevel(PlanetSurface_normalTiles, PlanetSurface_normalTiles_sampler, texel / t.w, layer, 0.0).xyz;
|
|
244
|
+
let np = normalize(c * 2.0 - 1.0);
|
|
245
|
+
// Planet space to world: the rotation rows transposed.
|
|
246
|
+
return normalize(PlanetSurface.rot0.xyz * np.x + PlanetSurface.rot1.xyz * np.y + PlanetSurface.rot2.xyz * np.z);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
override fn pbr_input(in: VertexOutput) -> PbrInput {
|
|
250
|
+
// Everything below sets the surface; the standard texture slots aren't bound (standardTextures: false).
|
|
251
|
+
var p: PbrInput;
|
|
252
|
+
p.base_color = vec3f(1.0);
|
|
253
|
+
p.alpha = 1.0;
|
|
254
|
+
p.normal = normalize(in.world_normal);
|
|
255
|
+
p.metallic = 0.0;
|
|
256
|
+
p.roughness = 0.9;
|
|
257
|
+
p.emissive = vec3f(0.0);
|
|
258
|
+
p.occlusion = 1.0;
|
|
259
|
+
let up = normalize(in.world_position - PlanetSurface.center.xyz);
|
|
260
|
+
let n = surface_normal(in);
|
|
261
|
+
let slope = degrees(acos(clamp(dot(n, up), -1.0, 1.0)));
|
|
262
|
+
let latitude = dot(up, PlanetSurface.rot1.xyz);
|
|
263
|
+
let height = in.world_tangent.w;
|
|
264
|
+
let count = u32(PlanetSurface.biomeParams.x);
|
|
265
|
+
let snow = select(0.0, max(height, 0.0) / PlanetSurface.biomeParams.z, PlanetSurface.biomeParams.z > 0.0);
|
|
266
|
+
let temperature = in.uv.x - PlanetSurface.biomeParams.y * abs(latitude) - snow;
|
|
267
|
+
var best_w = vec4f(0.0);
|
|
268
|
+
var best_i = vec4u(0u);
|
|
269
|
+
var sum = 0.0;
|
|
270
|
+
for (var b = 0u; b < count; b++) {
|
|
271
|
+
let r0 = table(0, b);
|
|
272
|
+
let r1 = table(1, b);
|
|
273
|
+
let blend = table(3, b).x;
|
|
274
|
+
let w = biome_window(temperature, r0.x, r0.y, blend) * biome_window(in.uv.y, r0.z, r0.w, blend) *
|
|
275
|
+
biome_window(height, r1.x, r1.y, blend) * biome_window(slope, r1.z, r1.w, blend);
|
|
276
|
+
sum += w;
|
|
277
|
+
if (w > best_w.x) {
|
|
278
|
+
best_w = vec4f(w, best_w.xyz);
|
|
279
|
+
best_i = vec4u(b, best_i.xyz);
|
|
280
|
+
} else if (w > best_w.y) {
|
|
281
|
+
best_w = vec4f(best_w.x, w, best_w.yz);
|
|
282
|
+
best_i = vec4u(best_i.x, b, best_i.yz);
|
|
283
|
+
} else if (w > best_w.z) {
|
|
284
|
+
best_w = vec4f(best_w.xy, w, best_w.z);
|
|
285
|
+
best_i = vec4u(best_i.xy, b, best_i.z);
|
|
286
|
+
} else if (w > best_w.w) {
|
|
287
|
+
best_w.w = w;
|
|
288
|
+
best_i.w = b;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (sum < 1e-6) {
|
|
292
|
+
best_w = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
293
|
+
best_i = vec4u(0u);
|
|
294
|
+
}
|
|
295
|
+
best_w /= max(1e-6, best_w.x + best_w.y + best_w.z + best_w.w);
|
|
296
|
+
// Planet-space position (offset to stay small; repeats line up across the offset) and normal.
|
|
297
|
+
let q = vec3f(dot(PlanetSurface.rot0.xyz, in.world_position), dot(PlanetSurface.rot1.xyz, in.world_position), dot(PlanetSurface.rot2.xyz, in.world_position)) +
|
|
298
|
+
vec3f(PlanetSurface.rot0.w, PlanetSurface.rot1.w, PlanetSurface.rot2.w);
|
|
299
|
+
var grads: Grads;
|
|
300
|
+
grads.dx = dpdx(q);
|
|
301
|
+
grads.dy = dpdy(q);
|
|
302
|
+
let debug = u32(PlanetSurface.debugParams.x);
|
|
303
|
+
if (debug == 1u) {
|
|
304
|
+
p.base_color = table(2, best_i.x).rgb;
|
|
305
|
+
p.roughness = 1.0;
|
|
306
|
+
p.metallic = 0.0;
|
|
307
|
+
return p;
|
|
308
|
+
}
|
|
309
|
+
if (debug == 2u) {
|
|
310
|
+
p.base_color = vec3f(1.0);
|
|
311
|
+
p.emissive = vec3f(20000.0);
|
|
312
|
+
return p;
|
|
313
|
+
}
|
|
314
|
+
if (debug == 4u) {
|
|
315
|
+
p.base_color = n * 0.5 + 0.5;
|
|
316
|
+
p.emissive = p.base_color * 3000.0;
|
|
317
|
+
return p;
|
|
318
|
+
}
|
|
319
|
+
if (debug == 3u) {
|
|
320
|
+
// Levels of detail: a hue per depth (from the depth's morph error, which halves per level).
|
|
321
|
+
let level = floor(log2(max(in.uv1.y, 1e-6)) * 1.0);
|
|
322
|
+
let hue = fract(level * 0.37);
|
|
323
|
+
p.base_color = 0.5 + 0.5 * cos(6.2832 * (vec3f(hue) + vec3f(0.0, 0.33, 0.67)));
|
|
324
|
+
p.emissive = p.base_color * 3000.0;
|
|
325
|
+
return p;
|
|
326
|
+
}
|
|
327
|
+
let np = vec3f(dot(PlanetSurface.rot0.xyz, n), dot(PlanetSurface.rot1.xyz, n), dot(PlanetSurface.rot2.xyz, n));
|
|
328
|
+
var bw = pow(abs(np), vec3f(4.0));
|
|
329
|
+
bw /= max(1e-6, bw.x + bw.y + bw.z);
|
|
330
|
+
let near = distance(in.world_position, view.cameraPosition) < PlanetSurface.debugParams.y;
|
|
331
|
+
var albedo = vec3f(0.0);
|
|
332
|
+
var normal = vec3f(0.0);
|
|
333
|
+
var orm = vec3f(0.0);
|
|
334
|
+
for (var k = 0u; k < 4u; k++) {
|
|
335
|
+
let w = best_w[k];
|
|
336
|
+
if (w <= 0.001) { continue; }
|
|
337
|
+
let b = best_i[k];
|
|
338
|
+
let tint = table(2, b).rgb;
|
|
339
|
+
let info = table(3, b);
|
|
340
|
+
let layers = table(4, b);
|
|
341
|
+
let scales = table(5, b);
|
|
342
|
+
// Layers go from flat ground (first) to cliffs (last), blended by slope.
|
|
343
|
+
let last = max(1.0, info.y) - 1.0;
|
|
344
|
+
let f = clamp(slope / 50.0, 0.0, 1.0) * last;
|
|
345
|
+
let k0 = u32(floor(f));
|
|
346
|
+
let k1 = min(k0 + 1u, u32(last));
|
|
347
|
+
let s = smoothstep(0.0, 1.0, f - floor(f));
|
|
348
|
+
let a = sample_layer(layers[k0], scales[k0], q, grads, np, bw, near);
|
|
349
|
+
var c = a;
|
|
350
|
+
if (k1 != k0 && s > 0.01) {
|
|
351
|
+
let d = sample_layer(layers[k1], scales[k1], q, grads, np, bw, near);
|
|
352
|
+
c.albedo = mix(a.albedo, d.albedo, s);
|
|
353
|
+
c.orm = mix(a.orm, d.orm, s);
|
|
354
|
+
c.normal = normalize(mix(a.normal, d.normal, s));
|
|
355
|
+
}
|
|
356
|
+
albedo += c.albedo * tint * w;
|
|
357
|
+
normal += c.normal * w;
|
|
358
|
+
orm += c.orm * w;
|
|
359
|
+
}
|
|
360
|
+
let pn = normalize(normal);
|
|
361
|
+
p.base_color = albedo;
|
|
362
|
+
p.normal = normalize(PlanetSurface.rot0.xyz * pn.x + PlanetSurface.rot1.xyz * pn.y + PlanetSurface.rot2.xyz * pn.z);
|
|
363
|
+
if (PlanetSurface.debugParams.z > 0.5) {
|
|
364
|
+
p.occlusion = orm.r;
|
|
365
|
+
p.roughness = clamp(orm.g, 0.045, 1.0);
|
|
366
|
+
p.metallic = orm.b;
|
|
367
|
+
} else {
|
|
368
|
+
p.occlusion = 1.0;
|
|
369
|
+
p.roughness = 0.9;
|
|
370
|
+
p.metallic = 0.0;
|
|
371
|
+
}
|
|
372
|
+
return p;
|
|
373
|
+
}
|
|
374
|
+
`,
|
|
375
|
+
"terrain::ocean": `
|
|
376
|
+
import shard::pbr::types::{ VertexOutput, PbrInput };
|
|
377
|
+
import shard::pbr::standard::standard_input;
|
|
378
|
+
import shard::globals::globals;
|
|
379
|
+
import shard::mesh::{ vertex_uv1, vertex_tangent, vertex_world, vertex_instance_data };
|
|
380
|
+
import material::ocean::Ocean;
|
|
381
|
+
${MORPH("Ocean")}
|
|
382
|
+
/** Slope (d/dx, d/dy) of a few scrolling sine waves at planet-space point xy. */
|
|
383
|
+
fn waves(xy: vec2f, t: f32) -> vec2f {
|
|
384
|
+
var g = vec2f(0.0);
|
|
385
|
+
let dirs = array<vec2f, 4>(vec2f(1.0, 0.2), vec2f(-0.4, 1.0), vec2f(0.7, -0.7), vec2f(-1.0, -0.3));
|
|
386
|
+
let lengths = array<f32, 4>(9.0, 5.3, 3.1, 1.7);
|
|
387
|
+
for (var i = 0u; i < 4u; i++) {
|
|
388
|
+
let d = normalize(dirs[i]);
|
|
389
|
+
let k = 6.2831853 / lengths[i];
|
|
390
|
+
let speed = sqrt(9.81 / k);
|
|
391
|
+
let a = 0.04 * lengths[i] / 9.0;
|
|
392
|
+
g += d * (a * k * cos(k * dot(d, xy) - speed * k * t));
|
|
393
|
+
}
|
|
394
|
+
return g;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
override fn pbr_input(in: VertexOutput) -> PbrInput {
|
|
398
|
+
var p = standard_input(in);
|
|
399
|
+
let depth = max(in.world_tangent.w, 0.0);
|
|
400
|
+
let up = normalize(in.world_position - Ocean.center.xyz);
|
|
401
|
+
// The point and a tangent frame in planet space, so the waves stay put as the planet turns.
|
|
402
|
+
let q = vec3f(dot(Ocean.rot0.xyz, in.world_position), dot(Ocean.rot1.xyz, in.world_position), dot(Ocean.rot2.xyz, in.world_position)) +
|
|
403
|
+
vec3f(Ocean.rot0.w, Ocean.rot1.w, Ocean.rot2.w);
|
|
404
|
+
let pu = vec3f(dot(Ocean.rot0.xyz, up), dot(Ocean.rot1.xyz, up), dot(Ocean.rot2.xyz, up));
|
|
405
|
+
let helper = select(vec3f(0.0, 1.0, 0.0), vec3f(1.0, 0.0, 0.0), abs(pu.y) > 0.9);
|
|
406
|
+
let tx = normalize(cross(helper, pu));
|
|
407
|
+
let ty = cross(pu, tx);
|
|
408
|
+
let g = waves(vec2f(dot(q, tx), dot(q, ty)), globals.time);
|
|
409
|
+
let pn = normalize(pu - tx * g.x - ty * g.y);
|
|
410
|
+
p.normal = normalize(Ocean.rot0.xyz * pn.x + Ocean.rot1.xyz * pn.y + Ocean.rot2.xyz * pn.z);
|
|
411
|
+
let shade = 1.0 - exp(-depth * Ocean.water.x);
|
|
412
|
+
p.base_color = mix(Ocean.shallow.rgb, Ocean.deep.rgb, shade);
|
|
413
|
+
p.alpha = clamp(1.0 - exp(-depth * Ocean.water.y), Ocean.water.z, 1.0);
|
|
414
|
+
p.roughness = Ocean.water.w;
|
|
415
|
+
p.metallic = 0.0;
|
|
416
|
+
p.occlusion = 1.0;
|
|
417
|
+
return p;
|
|
418
|
+
}
|
|
419
|
+
`
|
|
420
|
+
};
|
|
421
|
+
export {
|
|
422
|
+
OceanMaterial,
|
|
423
|
+
PlanetMaterial,
|
|
424
|
+
TERRAIN_SHADERS,
|
|
425
|
+
TEXTURE_PERIOD
|
|
426
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { AppMethod } from '@aethervtt/shard-runtime';
|
|
3
|
+
import type { PlanetRuntime } from './planet.js';
|
|
4
|
+
/** A planet from an entity id or scene path; the only planet when omitted. */
|
|
5
|
+
export declare function resolvePlanet(world: World, ref: unknown): PlanetRuntime;
|
|
6
|
+
/** Pixels of an equirectangular map: height (sea blue by depth, land dark to light) or biomes. */
|
|
7
|
+
export declare function terrainMap(rt: PlanetRuntime, width: number, mode: 'height' | 'biomes'): {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
data: Uint8Array<ArrayBuffer>;
|
|
11
|
+
};
|
|
12
|
+
export declare const terrainMethods: AppMethod[];
|
package/dist/methods.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineSchema,
|
|
3
|
+
findComponent,
|
|
4
|
+
ShardError,
|
|
5
|
+
t
|
|
6
|
+
} from "@aethervtt/shard-core";
|
|
7
|
+
import { encodePng, toBase64 } from "@aethervtt/shard-protocol";
|
|
8
|
+
import { collidersOf } from "./colliders.js";
|
|
9
|
+
import { Terrain, terrainSample } from "./heights.js";
|
|
10
|
+
function scenePath(world, entity) {
|
|
11
|
+
const member = findComponent("scene/SceneMember");
|
|
12
|
+
if (!member || !world.isAlive(entity)) return null;
|
|
13
|
+
const v = world.tryGet(entity, member);
|
|
14
|
+
return v?.path || null;
|
|
15
|
+
}
|
|
16
|
+
function resolvePlanet(world, ref) {
|
|
17
|
+
const planets = world.tryResource(Terrain)?.planets;
|
|
18
|
+
if (!planets || planets.size === 0) {
|
|
19
|
+
throw new ShardError("terrain/no-planet", "There is no terrain/Planet in the world", {
|
|
20
|
+
hint: "Add terrain/Planet (with a Grid) to an entity, and the terrain plugin to the app."
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
if (ref === void 0 || ref === null || ref === "") {
|
|
24
|
+
if (planets.size > 1) {
|
|
25
|
+
throw new ShardError("terrain/which-planet", `There are ${planets.size} planets; name one`, {
|
|
26
|
+
hint: "Pass planet: an entity id or a scene path (terrain.describe lists them)."
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return [...planets.values()][0];
|
|
30
|
+
}
|
|
31
|
+
for (const rt of planets.values()) {
|
|
32
|
+
if (rt.entity === ref) return rt;
|
|
33
|
+
const path = scenePath(world, rt.entity);
|
|
34
|
+
if (typeof ref === "string" && path && (path === ref || path.endsWith(`/${ref}`))) return rt;
|
|
35
|
+
}
|
|
36
|
+
throw new ShardError("terrain/not-a-planet", `No planet matches ${JSON.stringify(ref)}`, {
|
|
37
|
+
hint: "terrain.describe lists every planet with its entity and scene path."
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function usable(rt) {
|
|
41
|
+
if (rt.problem) throw rt.problem;
|
|
42
|
+
if (!rt.ready) {
|
|
43
|
+
throw new ShardError(
|
|
44
|
+
"terrain/not-ready",
|
|
45
|
+
`The planet is waiting for ${rt.waiting ?? "its settings"}`,
|
|
46
|
+
{
|
|
47
|
+
hint: "Its graphs and biomes load asynchronously; step a frame and ask again."
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const round = (v, digits = 3) => {
|
|
53
|
+
const k = 10 ** digits;
|
|
54
|
+
return Math.round(v * k) / k;
|
|
55
|
+
};
|
|
56
|
+
function latlon(lat, lon) {
|
|
57
|
+
const a = lat * Math.PI / 180;
|
|
58
|
+
const b = lon * Math.PI / 180;
|
|
59
|
+
return [Math.cos(a) * Math.sin(b), Math.sin(a), Math.cos(a) * Math.cos(b)];
|
|
60
|
+
}
|
|
61
|
+
function describePlanet(world, rt) {
|
|
62
|
+
const s = rt.settings;
|
|
63
|
+
const byDepth = [];
|
|
64
|
+
const t2 = rt.tree;
|
|
65
|
+
let partial = 0;
|
|
66
|
+
for (let i = 0; i < rt.selection.renderedCount; i++) {
|
|
67
|
+
const n = rt.selection.rendered[i];
|
|
68
|
+
byDepth[t2.depth[n]] = (byDepth[t2.depth[n]] ?? 0) + 1;
|
|
69
|
+
if (t2.mask[n] !== 15) partial++;
|
|
70
|
+
}
|
|
71
|
+
const pr = rt.parts.get("render");
|
|
72
|
+
const colliders = collidersOf(rt);
|
|
73
|
+
let underCamera = null;
|
|
74
|
+
if (pr?.hasCamera && rt.ready) {
|
|
75
|
+
const p = pr.view.position;
|
|
76
|
+
underCamera = terrainSample(rt, [p[0], p[1], p[2]]);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
entity: rt.entity,
|
|
80
|
+
path: scenePath(world, rt.entity),
|
|
81
|
+
ready: rt.ready,
|
|
82
|
+
waitingFor: rt.waiting,
|
|
83
|
+
problem: rt.problem ? rt.problem.toJSON() : null,
|
|
84
|
+
radius: s?.radius ?? null,
|
|
85
|
+
heightScale: s?.heightScale ?? null,
|
|
86
|
+
ocean: s ? s.ocean ? { seaLevel: s.seaLevel } : null : null,
|
|
87
|
+
maxDepth: rt.maxDepth,
|
|
88
|
+
colliderDepth: rt.colliderDepth,
|
|
89
|
+
spacing: {
|
|
90
|
+
finest: round(rt.spacing(rt.maxDepth)),
|
|
91
|
+
colliders: round(rt.spacing(rt.colliderDepth))
|
|
92
|
+
},
|
|
93
|
+
version: rt.version,
|
|
94
|
+
biomes: rt.table.count,
|
|
95
|
+
detail: {
|
|
96
|
+
vertexPixels: s?.vertexPixels ?? null,
|
|
97
|
+
// Over TerrainBudget.triangles the LOD bias rises above 1 (coarser), and falls back after.
|
|
98
|
+
lodBias: round(rt.lodBias),
|
|
99
|
+
triangles: rt.selection.renderedCount * 2 * ((s?.resolution ?? 33) - 1) ** 2
|
|
100
|
+
},
|
|
101
|
+
selected: {
|
|
102
|
+
chunks: rt.selection.renderedCount,
|
|
103
|
+
byDepth: Object.fromEntries(byDepth.flatMap((c, d) => c ? [[d, c]] : [])),
|
|
104
|
+
partial,
|
|
105
|
+
ocean: rt.oceanSelection.renderedCount,
|
|
106
|
+
requested: rt.selection.requestedCount + rt.oceanSelection.requestedCount,
|
|
107
|
+
waiting: rt.selection.waiting,
|
|
108
|
+
walks: rt.selection.passes
|
|
109
|
+
},
|
|
110
|
+
pool: pr ? {
|
|
111
|
+
slots: pr.slots.length,
|
|
112
|
+
shown: pr.slots.filter((x) => x.shown).length,
|
|
113
|
+
inFlight: pr.jobs.length,
|
|
114
|
+
generated: pr.stats.generated,
|
|
115
|
+
fromColliders: pr.stats.fromColliders,
|
|
116
|
+
evicted: pr.stats.evicted,
|
|
117
|
+
readbacks: pr.stats.readbacks,
|
|
118
|
+
jobsLastFrame: pr.stats.lastFrameJobs
|
|
119
|
+
} : null,
|
|
120
|
+
colliders: {
|
|
121
|
+
chunks: colliders.chunks.size,
|
|
122
|
+
active: [...colliders.chunks.values()].filter((c) => c.entity >= 0).length,
|
|
123
|
+
pending: colliders.jobs.size,
|
|
124
|
+
builtOnPool: colliders.builtPool,
|
|
125
|
+
builtOnMainThread: colliders.builtSync,
|
|
126
|
+
anchors: rt.anchors
|
|
127
|
+
},
|
|
128
|
+
underCamera: underCamera ? {
|
|
129
|
+
height: round(underCamera.height),
|
|
130
|
+
underwater: underCamera.underwater,
|
|
131
|
+
biome: underCamera.biome,
|
|
132
|
+
slope: round(underCamera.slope, 1)
|
|
133
|
+
} : null,
|
|
134
|
+
treeNodes: rt.tree.live
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function terrainMap(rt, width, mode) {
|
|
138
|
+
usable(rt);
|
|
139
|
+
const height = Math.max(1, Math.round(width / 2));
|
|
140
|
+
const data = new Uint8Array(width * height * 4);
|
|
141
|
+
const s = rt.settings;
|
|
142
|
+
const top = Math.max(1, rt.highest - Math.max(s.seaLevel, rt.lowest));
|
|
143
|
+
const low = Math.max(1, (s.ocean ? s.seaLevel : 0) - rt.lowest);
|
|
144
|
+
for (let y = 0; y < height; y++) {
|
|
145
|
+
const lat = 90 - (y + 0.5) / height * 180;
|
|
146
|
+
for (let x = 0; x < width; x++) {
|
|
147
|
+
const lon = (x + 0.5) / width * 360 - 180;
|
|
148
|
+
const sample = terrainSample(rt, latlon(lat, lon));
|
|
149
|
+
const o = (y * width + x) * 4;
|
|
150
|
+
let r;
|
|
151
|
+
let g;
|
|
152
|
+
let b;
|
|
153
|
+
if (sample.underwater) {
|
|
154
|
+
const d = Math.min(1, sample.depth / low);
|
|
155
|
+
r = 20 * (1 - d);
|
|
156
|
+
g = 90 * (1 - d) + 20;
|
|
157
|
+
b = 200 - 110 * d;
|
|
158
|
+
} else if (mode === "height") {
|
|
159
|
+
const k = Math.min(
|
|
160
|
+
1,
|
|
161
|
+
Math.max(0, (sample.height - (s.ocean ? s.seaLevel : rt.lowest)) / top)
|
|
162
|
+
);
|
|
163
|
+
r = 40 + 215 * k;
|
|
164
|
+
g = 80 + 175 * k;
|
|
165
|
+
b = 40 + 215 * k;
|
|
166
|
+
} else {
|
|
167
|
+
const tint = rt.table.tints.subarray(sample.biome * 4, sample.biome * 4 + 4);
|
|
168
|
+
const shade = 0.75 + 0.25 * Math.cos(sample.slope * Math.PI / 180);
|
|
169
|
+
r = Math.min(255, tint[0] ** (1 / 2.2) * 255 * shade);
|
|
170
|
+
g = Math.min(255, tint[1] ** (1 / 2.2) * 255 * shade);
|
|
171
|
+
b = Math.min(255, tint[2] ** (1 / 2.2) * 255 * shade);
|
|
172
|
+
}
|
|
173
|
+
data[o] = r;
|
|
174
|
+
data[o + 1] = g;
|
|
175
|
+
data[o + 2] = b;
|
|
176
|
+
data[o + 3] = 255;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { width, height, data };
|
|
180
|
+
}
|
|
181
|
+
const planetField = t.json({
|
|
182
|
+
description: "The planet: entity id or scene path (default: the only planet)."
|
|
183
|
+
});
|
|
184
|
+
const terrainMethods = [
|
|
185
|
+
{
|
|
186
|
+
name: "terrain.describe",
|
|
187
|
+
description: "Planet terrain as data: per planet, the chunks selected by depth (and partial ones waiting for children), requests and chunks in flight, pool usage, generation counts, collider chunks and anchors, the vertex spacing at the finest and collider depths, and the height, biome, and slope under the camera. Problems (a bad radius, a climate graph without temperature/moisture) and what it waits for are here too.",
|
|
188
|
+
params: defineSchema("terrain/DescribeParams", { planet: planetField }),
|
|
189
|
+
handler: ({ world }, p) => {
|
|
190
|
+
const planets = world.tryResource(Terrain)?.planets;
|
|
191
|
+
if (!planets) return { planets: [] };
|
|
192
|
+
if (p.planet !== void 0 && p.planet !== null && p.planet !== "")
|
|
193
|
+
return { planets: [describePlanet(world, resolvePlanet(world, p.planet))] };
|
|
194
|
+
return { planets: [...planets.values()].map((rt) => describePlanet(world, rt)) };
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "terrain.sample",
|
|
199
|
+
description: "The surface at points on a planet, from the CPU noise the colliders use (headless-safe): height (m above radius), underwater and water depth, slope (degrees), climate, and biome weights with the dominant biome. Points are directions from the planet center in its frame ([x, y, z], any length) or [lat, lon] in degrees (lat from the +Y axis\u2019s equator, lon around it).",
|
|
200
|
+
params: defineSchema("terrain/SampleParams", {
|
|
201
|
+
planet: planetField,
|
|
202
|
+
directions: t.json({ description: "Directions: [[x, y, z], \u2026] (at most 4096)." }),
|
|
203
|
+
latlon: t.json({ description: "Latitudes and longitudes in degrees: [[lat, lon], \u2026]." })
|
|
204
|
+
}),
|
|
205
|
+
handler: ({ world }, p) => {
|
|
206
|
+
const rt = resolvePlanet(world, p.planet);
|
|
207
|
+
usable(rt);
|
|
208
|
+
const dirs = [];
|
|
209
|
+
if (Array.isArray(p.directions)) for (const d of p.directions) dirs.push(d);
|
|
210
|
+
if (Array.isArray(p.latlon))
|
|
211
|
+
for (const [a, b] of p.latlon) dirs.push(latlon(a, b));
|
|
212
|
+
if (dirs.length === 0 || dirs.length > 4096) {
|
|
213
|
+
throw new ShardError(
|
|
214
|
+
"terrain/bad-points",
|
|
215
|
+
"Pass 1 to 4096 points in directions or latlon",
|
|
216
|
+
{
|
|
217
|
+
hint: 'e.g. { "latlon": [[0, 0], [45, 90]] }, or { "directions": [[0, 1, 0]] } for the north pole.'
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
samples: dirs.map((d) => {
|
|
223
|
+
const s = terrainSample(rt, d);
|
|
224
|
+
return {
|
|
225
|
+
height: round(s.height),
|
|
226
|
+
underwater: s.underwater,
|
|
227
|
+
depth: round(s.depth),
|
|
228
|
+
slope: round(s.slope, 2),
|
|
229
|
+
temperature: round(s.temperature),
|
|
230
|
+
moisture: round(s.moisture),
|
|
231
|
+
latitude: round(s.latitude),
|
|
232
|
+
biome: s.biome,
|
|
233
|
+
biomes: s.biomes.map((w) => round(w))
|
|
234
|
+
};
|
|
235
|
+
})
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: "terrain.map",
|
|
241
|
+
description: 'An equirectangular PNG of a planet (width \xD7 width/2; latitude +90 at the top, longitude \u2212180 to 180 left to right): mode "biomes" colors land by its dominant biome\u2019s tint (shaded by slope), mode "height" dark lowlands to white peaks; water is blue, darker when deeper. One image answers "are there continents, oceans, and polar caps".',
|
|
242
|
+
params: defineSchema("terrain/MapParams", {
|
|
243
|
+
planet: planetField,
|
|
244
|
+
size: t.u32({ default: 256, min: 16, max: 1024, description: "Width in pixels." }),
|
|
245
|
+
mode: t.enum(["biomes", "height"])
|
|
246
|
+
}),
|
|
247
|
+
handler: async ({ world }, p) => {
|
|
248
|
+
const rt = resolvePlanet(world, p.planet);
|
|
249
|
+
const image = terrainMap(rt, p.size, p.mode);
|
|
250
|
+
const png = await encodePng(image.data, image.width, image.height);
|
|
251
|
+
return { width: image.width, height: image.height, data: toBase64(png) };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
];
|
|
255
|
+
export {
|
|
256
|
+
resolvePlanet,
|
|
257
|
+
terrainMap,
|
|
258
|
+
terrainMethods
|
|
259
|
+
};
|