@drawcall/charta 0.0.0

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.
Files changed (71) hide show
  1. package/LICENSE +7 -0
  2. package/dist/assets/loader.d.ts +21 -0
  3. package/dist/assets/loader.d.ts.map +1 -0
  4. package/dist/assets/loader.js +113 -0
  5. package/dist/errors.d.ts +11 -0
  6. package/dist/errors.d.ts.map +1 -0
  7. package/dist/errors.js +27 -0
  8. package/dist/grammar.d.ts +29 -0
  9. package/dist/grammar.d.ts.map +1 -0
  10. package/dist/grammar.js +119 -0
  11. package/dist/grass/index.d.ts +25 -0
  12. package/dist/grass/index.d.ts.map +1 -0
  13. package/dist/grass/index.js +177 -0
  14. package/dist/grass/material.d.ts +10 -0
  15. package/dist/grass/material.d.ts.map +1 -0
  16. package/dist/grass/material.js +80 -0
  17. package/dist/index.d.ts +13 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +12 -0
  20. package/dist/interpreter.d.ts +47 -0
  21. package/dist/interpreter.d.ts.map +1 -0
  22. package/dist/interpreter.js +226 -0
  23. package/dist/locations.d.ts +16 -0
  24. package/dist/locations.d.ts.map +1 -0
  25. package/dist/locations.js +58 -0
  26. package/dist/parser.d.ts +9 -0
  27. package/dist/parser.d.ts.map +1 -0
  28. package/dist/parser.js +47 -0
  29. package/dist/pillars/index.d.ts +7 -0
  30. package/dist/pillars/index.d.ts.map +1 -0
  31. package/dist/pillars/index.js +154 -0
  32. package/dist/pillars/material.d.ts +3 -0
  33. package/dist/pillars/material.d.ts.map +1 -0
  34. package/dist/pillars/material.js +43 -0
  35. package/dist/place/index.d.ts +37 -0
  36. package/dist/place/index.d.ts.map +1 -0
  37. package/dist/place/index.js +216 -0
  38. package/dist/tiles/geometry.d.ts +46 -0
  39. package/dist/tiles/geometry.d.ts.map +1 -0
  40. package/dist/tiles/geometry.js +463 -0
  41. package/dist/tiles/index.d.ts +18 -0
  42. package/dist/tiles/index.d.ts.map +1 -0
  43. package/dist/tiles/index.js +121 -0
  44. package/dist/tiles/material.d.ts +6 -0
  45. package/dist/tiles/material.d.ts.map +1 -0
  46. package/dist/tiles/material.js +88 -0
  47. package/dist/utils/instanced-mesh-group.d.ts +17 -0
  48. package/dist/utils/instanced-mesh-group.d.ts.map +1 -0
  49. package/dist/utils/instanced-mesh-group.js +59 -0
  50. package/dist/utils/random.d.ts +4 -0
  51. package/dist/utils/random.d.ts.map +1 -0
  52. package/dist/utils/random.js +19 -0
  53. package/dist/utils/texture.d.ts +3 -0
  54. package/dist/utils/texture.d.ts.map +1 -0
  55. package/dist/utils/texture.js +30 -0
  56. package/dist/walls/index.d.ts +87 -0
  57. package/dist/walls/index.d.ts.map +1 -0
  58. package/dist/walls/index.js +376 -0
  59. package/dist/walls/material.d.ts +3 -0
  60. package/dist/walls/material.d.ts.map +1 -0
  61. package/dist/walls/material.js +67 -0
  62. package/dist/water/index.d.ts +10 -0
  63. package/dist/water/index.d.ts.map +1 -0
  64. package/dist/water/index.js +46 -0
  65. package/dist/water/material.d.ts +5 -0
  66. package/dist/water/material.d.ts.map +1 -0
  67. package/dist/water/material.js +46 -0
  68. package/dist/water/texture.d.ts +15 -0
  69. package/dist/water/texture.d.ts.map +1 -0
  70. package/dist/water/texture.js +201 -0
  71. package/package.json +39 -0
@@ -0,0 +1,376 @@
1
+ import { Euler, Matrix4, Mesh, MeshBasicMaterial, Quaternion, Vector3, BufferAttribute, Texture, Shape, ExtrudeGeometry, Path, } from 'three';
2
+ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
3
+ import { coerce, object, string, enum as enum_ } from 'zod';
4
+ import { TilesGeometry } from '../tiles/geometry.js';
5
+ import { buildTextureArrayFromAssets } from '../utils/texture.js';
6
+ import { buildWallMeshMaterial } from './material.js';
7
+ import { ChartaError } from '../errors.js';
8
+ import { ceilingSchema, groundSchema } from '../tiles/index.js';
9
+ const eulerHelper = new Euler();
10
+ const scaleHelper = new Vector3(1, 1, 1);
11
+ export const windowSchema = object({
12
+ offsetX: coerce.number().optional(),
13
+ bottomY: coerce.number().optional(),
14
+ topY: coerce.number().optional(),
15
+ width: coerce.number().optional(),
16
+ });
17
+ export const doorSchema = object({
18
+ offsetX: coerce.number().optional(),
19
+ bottomY: coerce.number().optional(),
20
+ topY: coerce.number().optional(),
21
+ height: coerce.number().optional(),
22
+ width: coerce.number().optional(),
23
+ });
24
+ export const wallSchema = object({
25
+ dir: enum_(['top', 'bottom', 'left', 'right']),
26
+ texture: string(),
27
+ bottomY: coerce.number().optional(),
28
+ topY: coerce.number().optional(),
29
+ });
30
+ export const WALL_CONFIG = {
31
+ top: { zOffset: -0.5, rotation: 0, axis: 'z', sampleDir: 1 },
32
+ bottom: { zOffset: 0.5, rotation: 0, axis: 'z', sampleDir: 1 },
33
+ left: {
34
+ xOffset: -0.5,
35
+ rotation: Math.PI / 2,
36
+ axis: 'x',
37
+ sampleDir: -1,
38
+ },
39
+ right: {
40
+ xOffset: 0.5,
41
+ rotation: Math.PI / 2,
42
+ axis: 'x',
43
+ sampleDir: -1,
44
+ },
45
+ };
46
+ export const WALL_CONNECTION_TOLERANCE_RATIO = 1 / 8;
47
+ export function computeWallVerticalBounds(interpreter, tilesGeometry, row, col, wallIdx, parsed, config, loc) {
48
+ const rows = interpreter.getRows();
49
+ const cols = interpreter.getCols();
50
+ const cellSize = interpreter.getCellSize();
51
+ const snapThreshold = cellSize * WALL_CONNECTION_TOLERANCE_RATIO;
52
+ const isLayer = (c) => c.name === 'ground' || c.name === 'ceiling';
53
+ // Determine neighbor cell
54
+ let neighborRow = row;
55
+ let neighborCol = col;
56
+ if (parsed.dir === 'top')
57
+ neighborRow--;
58
+ else if (parsed.dir === 'bottom')
59
+ neighborRow++;
60
+ else if (parsed.dir === 'left')
61
+ neighborCol--;
62
+ else if (parsed.dir === 'right')
63
+ neighborCol++;
64
+ const [worldCellCenterX, worldCellCenterZ] = interpreter.getWorldCellCenter(row, col);
65
+ const localWallX = ('xOffset' in config ? config.xOffset : 0) * cellSize;
66
+ const localWallZ = ('zOffset' in config ? config.zOffset : 0) * cellSize;
67
+ const worldWallX = worldCellCenterX + localWallX;
68
+ const worldWallZ = worldCellCenterZ + localWallZ;
69
+ // Determine layer context
70
+ const layersBefore = interpreter.countCalls([row, col], isLayer, 0, wallIdx);
71
+ const layersAfter = interpreter.countCalls([row, col], isLayer, wallIdx + 1);
72
+ const resolveLayer = (explicitY, isTop) => {
73
+ // Case 1: No explicit height -> connect to adjacent layer in current cell
74
+ if (explicitY === undefined) {
75
+ if (isTop) {
76
+ if (layersAfter > 0)
77
+ return { col, row, layerIdx: layersBefore }; // Index of immediate next layer
78
+ interpreter.reportError(new ChartaError(`wall at ${row}/${col}: missing topY and no subsequent layer`, interpreter.getSource(), loc));
79
+ return undefined;
80
+ }
81
+ else {
82
+ if (layersBefore > 0)
83
+ return { col, row, layerIdx: layersBefore - 1 }; // Index of immediate prev layer
84
+ interpreter.reportError(new ChartaError(`wall at ${row}/${col}: missing bottomY and no preceding layer`, interpreter.getSource(), loc));
85
+ return undefined;
86
+ }
87
+ }
88
+ // Case 2: Explicit height -> check if close to any layer in current OR neighbor cell
89
+ let bestLayer;
90
+ let minDiff = Infinity;
91
+ const checkCell = (r, c) => {
92
+ if (r < 0 || r >= rows || c < 0 || c >= cols)
93
+ return;
94
+ const calls = interpreter.getCalls([r, c], { ground: groundSchema, ceiling: ceilingSchema });
95
+ for (let i = 0; i < calls.length; i++) {
96
+ const [, { y: layerY }] = calls[i];
97
+ const diff = Math.abs(layerY - explicitY);
98
+ if (diff < snapThreshold && diff < minDiff) {
99
+ minDiff = diff;
100
+ bestLayer = { col: c, row: r, layerIdx: i };
101
+ }
102
+ }
103
+ };
104
+ checkCell(row, col);
105
+ checkCell(neighborRow, neighborCol);
106
+ return bestLayer;
107
+ };
108
+ const bottomLayer = resolveLayer(parsed.bottomY, false);
109
+ const topLayer = resolveLayer(parsed.topY, true);
110
+ if (bottomLayer === undefined && parsed.bottomY === undefined)
111
+ return undefined;
112
+ if (topLayer === undefined && parsed.topY === undefined)
113
+ return undefined;
114
+ const sampleOffsets = [-cellSize / 2, 0, cellSize / 2];
115
+ const yStart = [0, 0, 0];
116
+ const yEnd = [0, 0, 0];
117
+ for (let k = 0; k < 3; k++) {
118
+ const off = sampleOffsets[k];
119
+ let sX = localWallX;
120
+ let sZ = localWallZ;
121
+ // Move along the wall direction
122
+ if (config.axis === 'z') {
123
+ sX += off * config.sampleDir;
124
+ }
125
+ else {
126
+ sZ += off * config.sampleDir;
127
+ }
128
+ // Sample Bottom
129
+ if (bottomLayer) {
130
+ const offX = sX + (col - bottomLayer.col) * cellSize;
131
+ const offZ = sZ + (row - bottomLayer.row) * cellSize;
132
+ yStart[k] = tilesGeometry.getHeight(bottomLayer.col, bottomLayer.row, bottomLayer.layerIdx, offX, offZ);
133
+ }
134
+ else {
135
+ yStart[k] = parsed.bottomY;
136
+ }
137
+ // Sample Top
138
+ if (topLayer) {
139
+ const offX = sX + (col - topLayer.col) * cellSize;
140
+ const offZ = sZ + (row - topLayer.row) * cellSize;
141
+ yEnd[k] = tilesGeometry.getHeight(topLayer.col, topLayer.row, topLayer.layerIdx, offX, offZ);
142
+ }
143
+ else {
144
+ yEnd[k] = parsed.topY;
145
+ }
146
+ }
147
+ // Ensure non-negative height (fix inverted walls)
148
+ for (let k = 0; k < 3; k++) {
149
+ if (yEnd[k] < yStart[k]) {
150
+ const tmp = yStart[k];
151
+ yStart[k] = yEnd[k];
152
+ yEnd[k] = tmp;
153
+ }
154
+ }
155
+ return {
156
+ yStart,
157
+ yEnd,
158
+ worldWallX,
159
+ worldWallZ,
160
+ };
161
+ }
162
+ export class WallMesh extends Mesh {
163
+ constructor(interpreter, material = new MeshBasicMaterial()) {
164
+ const rows = interpreter.getRows();
165
+ const cols = interpreter.getCols();
166
+ const cellSize = interpreter.getCellSize();
167
+ const tilesGeometry = interpreter.getAsset(TilesGeometry, 'tilesGeometry');
168
+ if (!tilesGeometry) {
169
+ super();
170
+ return;
171
+ }
172
+ const wallData = [];
173
+ const usedTextures = [];
174
+ const getWallTextureId = (texture) => {
175
+ let idx = usedTextures.indexOf(texture);
176
+ if (idx === -1) {
177
+ idx = usedTextures.length;
178
+ usedTextures.push(texture);
179
+ }
180
+ return idx;
181
+ };
182
+ for (let row = 0; row < rows; row++) {
183
+ for (let col = 0; col < cols; col++) {
184
+ const entries = interpreter.getCalls([row, col], {
185
+ wall: wallSchema,
186
+ window: windowSchema,
187
+ door: doorSchema,
188
+ });
189
+ let currentWall;
190
+ for (const [name, winOrDoor, wallIdx, loc] of entries) {
191
+ if (name === 'window' || name === 'door') {
192
+ if (!currentWall) {
193
+ interpreter.reportError(new ChartaError(`${name} without preceding wall`, interpreter.getSource(), loc));
194
+ continue;
195
+ }
196
+ const offsetX = winOrDoor.offsetX ?? 0;
197
+ const width = winOrDoor.width ?? (name === 'door' ? 1.0 : Math.min(0.8, cellSize - 0.2));
198
+ const halfWidth = currentWall.xzSize[0] / 2;
199
+ const xMin = offsetX - width / 2;
200
+ const xMax = offsetX + width / 2;
201
+ // Check X bounds
202
+ if (xMin < -halfWidth || xMax > halfWidth) {
203
+ interpreter.reportError(new ChartaError(`${name} outside of wall width`, interpreter.getSource(), loc));
204
+ continue;
205
+ }
206
+ // Helper to get wall Y at local x
207
+ const getWallY = (localX, yTriple) => {
208
+ const xNorm = (localX + halfWidth) / cellSize; // 0..1
209
+ if (xNorm < 0.5) {
210
+ // Interpolate between 0 (left) and 1 (center)
211
+ const t = xNorm * 2;
212
+ return yTriple[0] * (1 - t) + yTriple[1] * t;
213
+ }
214
+ else {
215
+ // Interpolate between 1 (center) and 2 (right)
216
+ const t = (xNorm - 0.5) * 2;
217
+ return yTriple[1] * (1 - t) + yTriple[2] * t;
218
+ }
219
+ };
220
+ // Check Y bounds at both ends of the window
221
+ // We check at xMin and xMax
222
+ const wallBottomLeft = getWallY(xMin, currentWall.yStart);
223
+ const wallTopLeft = getWallY(xMin, currentWall.yEnd);
224
+ const wallBottomRight = getWallY(xMax, currentWall.yStart);
225
+ const wallTopRight = getWallY(xMax, currentWall.yEnd);
226
+ const epsilon = 1e-3;
227
+ const wallSafeBottom = Math.max(wallBottomLeft, wallBottomRight);
228
+ const wallSafeTop = Math.min(wallTopLeft, wallTopRight);
229
+ let bottomY;
230
+ let topY;
231
+ if (name === 'door') {
232
+ bottomY = winOrDoor.bottomY ?? wallSafeBottom;
233
+ topY = winOrDoor.topY ?? Math.min(wallSafeTop, bottomY + 2.0);
234
+ }
235
+ else {
236
+ bottomY = winOrDoor.bottomY ?? Math.min(wallSafeTop, wallSafeBottom + 0.8);
237
+ topY = winOrDoor.topY ?? Math.min(wallSafeTop, bottomY + 1.1);
238
+ }
239
+ if (bottomY < wallSafeBottom - epsilon ||
240
+ topY > wallSafeTop + epsilon) {
241
+ interpreter.reportError(new ChartaError(`${name} outside of wall height`, interpreter.getSource(), loc));
242
+ continue;
243
+ }
244
+ currentWall.windows.push({
245
+ offsetX,
246
+ width,
247
+ bottomY,
248
+ topY,
249
+ });
250
+ continue;
251
+ }
252
+ const wallParsed = winOrDoor; // Type assertion since it could be wall or window in loop
253
+ const config = WALL_CONFIG[wallParsed.dir];
254
+ const bounds = computeWallVerticalBounds(interpreter, tilesGeometry, row, col, wallIdx, wallParsed, config, loc);
255
+ if (!bounds) {
256
+ currentWall = undefined;
257
+ continue;
258
+ }
259
+ const texture = interpreter.getAsset(Texture, `${wallParsed.texture}BaseColorTexture`);
260
+ if (!texture) {
261
+ currentWall = undefined;
262
+ continue;
263
+ }
264
+ const newWall = {
265
+ x: bounds.worldWallX,
266
+ z: bounds.worldWallZ,
267
+ rotationY: config.rotation,
268
+ xzSize: [cellSize, 0.1], // Walls are always cellSize width, 0.1 depth
269
+ yStart: bounds.yStart,
270
+ yEnd: bounds.yEnd,
271
+ textureId: getWallTextureId(texture),
272
+ windows: [],
273
+ };
274
+ wallData.push(newWall);
275
+ currentWall = newWall;
276
+ }
277
+ }
278
+ }
279
+ // Build per-wall geometries (baked) and merge
280
+ const bakedGeometries = [];
281
+ const tmpMatrix = new Matrix4();
282
+ const tmpQuat = new Quaternion();
283
+ const tmpPos = new Vector3();
284
+ const fixWallUVs = (geom, width, depth) => {
285
+ const pos = geom.getAttribute('position');
286
+ const norm = geom.getAttribute('normal');
287
+ const uv = geom.getAttribute('uv');
288
+ for (let i = 0; i < pos.count; i++) {
289
+ const x = pos.getX(i);
290
+ const y = pos.getY(i);
291
+ const z = pos.getZ(i);
292
+ const nx = norm.getX(i);
293
+ const nz = norm.getZ(i);
294
+ if (Math.abs(nz) > 0.5) {
295
+ // Front/Back
296
+ uv.setXY(i, x + width / 2, y);
297
+ }
298
+ else if (Math.abs(nx) > 0.5) {
299
+ // Side (Left/Right)
300
+ uv.setXY(i, z + depth / 2, y);
301
+ }
302
+ else {
303
+ // Top/Bottom
304
+ uv.setXY(i, x + width / 2, z + depth / 2);
305
+ }
306
+ }
307
+ uv.needsUpdate = true;
308
+ };
309
+ for (const w of wallData) {
310
+ const width = w.xzSize[0];
311
+ const depth = w.xzSize[1];
312
+ const halfW = width / 2;
313
+ const shape = new Shape();
314
+ // Bottom edge (left to right)
315
+ shape.moveTo(-halfW, w.yStart[0]);
316
+ shape.lineTo(0, w.yStart[1]);
317
+ shape.lineTo(halfW, w.yStart[2]);
318
+ // Top edge (right to left)
319
+ shape.lineTo(halfW, w.yEnd[2]);
320
+ shape.lineTo(0, w.yEnd[1]);
321
+ shape.lineTo(-halfW, w.yEnd[0]);
322
+ shape.closePath();
323
+ for (const win of w.windows) {
324
+ const hole = new Path();
325
+ const wxMin = win.offsetX - win.width / 2;
326
+ const wxMax = win.offsetX + win.width / 2;
327
+ // Hole should have opposite winding order?
328
+ // Shape outer is CCW (default). Holes should be CW?
329
+ // Three.js Shape/Path usually handles this if using .holes.
330
+ // Let's define it:
331
+ hole.moveTo(wxMin, win.bottomY);
332
+ hole.lineTo(wxMax, win.bottomY);
333
+ hole.lineTo(wxMax, win.topY);
334
+ hole.lineTo(wxMin, win.topY);
335
+ hole.closePath();
336
+ shape.holes.push(hole);
337
+ }
338
+ const geom = new ExtrudeGeometry(shape, {
339
+ depth,
340
+ bevelEnabled: false,
341
+ });
342
+ // Center Z
343
+ geom.translate(0, 0, -depth / 2);
344
+ fixWallUVs(geom, width, depth);
345
+ // Assign per-vertex texture id
346
+ const vertexCount = geom.getAttribute('position').count;
347
+ const texIds = new Float32Array(vertexCount);
348
+ texIds.fill(w.textureId);
349
+ geom.setAttribute('textureId', new BufferAttribute(texIds, 1));
350
+ // Apply rotation and translation
351
+ tmpPos.set(w.x, 0, w.z);
352
+ tmpQuat.setFromEuler(eulerHelper.set(0, w.rotationY, 0));
353
+ tmpMatrix.compose(tmpPos, tmpQuat, scaleHelper);
354
+ geom.applyMatrix4(tmpMatrix);
355
+ bakedGeometries.push(geom);
356
+ }
357
+ let geometry;
358
+ if (bakedGeometries.length > 0) {
359
+ geometry = mergeGeometries(bakedGeometries, false);
360
+ geometry.computeVertexNormals();
361
+ }
362
+ super(geometry, material);
363
+ if (bakedGeometries.length === 0) {
364
+ this.visible = false;
365
+ }
366
+ if (usedTextures.length > 0) {
367
+ // Build texture array for wall materials and apply materials
368
+ const textureArray = buildTextureArrayFromAssets(usedTextures);
369
+ buildWallMeshMaterial(this.material, textureArray);
370
+ }
371
+ }
372
+ }
373
+ export function isLayerConnectedToWall(wallTopY, wallBotY, y, tileSize) {
374
+ const tolerance = tileSize * WALL_CONNECTION_TOLERANCE_RATIO;
375
+ return Math.abs(y - wallTopY) < tolerance || Math.abs(y - wallBotY) < tolerance;
376
+ }
@@ -0,0 +1,3 @@
1
+ import { Material } from "three";
2
+ export declare function buildWallMeshMaterial(material: Material, textureArray: any, normalTextureArray?: any): void;
3
+ //# sourceMappingURL=material.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"material.d.ts","sourceRoot":"","sources":["../../src/walls/material.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEjC,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,GAAG,EACjB,kBAAkB,CAAC,EAAE,GAAG,GACvB,IAAI,CAgFN"}
@@ -0,0 +1,67 @@
1
+ export function buildWallMeshMaterial(material, textureArray, normalTextureArray) {
2
+ if (normalTextureArray) {
3
+ material.defines = {
4
+ ...material.defines,
5
+ USE_NORMALMAP_TANGENTSPACE: "",
6
+ USE_TANGENT: "",
7
+ };
8
+ }
9
+ material.onBeforeCompile = (shader) => {
10
+ shader.uniforms.uTextureArray = { value: textureArray };
11
+ if (normalTextureArray) {
12
+ shader.uniforms.uNormalTextureArray = { value: normalTextureArray };
13
+ }
14
+ shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>
15
+ attribute float textureId;
16
+ varying float vTextureId;
17
+ varying vec2 vUv;`);
18
+ shader.vertexShader = shader.vertexShader.replace("#include <uv_vertex>", `
19
+ #include <uv_vertex>
20
+ vUv = uv;
21
+ vTextureId = textureId;
22
+ `);
23
+ shader.fragmentShader =
24
+ `
25
+ varying float vTextureId;
26
+ uniform sampler2DArray uTextureArray;
27
+ ${normalTextureArray ? "uniform sampler2DArray uNormalTextureArray;" : ""}
28
+ #ifndef USE_MAP
29
+ varying vec2 vUv;
30
+ #endif
31
+
32
+ vec2 hash2( vec2 p ) {
33
+ return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453);
34
+ }
35
+
36
+ float voronoi( in vec2 x ) {
37
+ vec2 n = floor( x );
38
+ vec2 f = fract( x );
39
+ float m = 8.0;
40
+ for( int j=-1; j<=1; j++ )
41
+ for( int i=-1; i<=1; i++ ) {
42
+ vec2 g = vec2( float(i), float(j) );
43
+ vec2 o = hash2( n + g );
44
+ vec2 r = g - f + o;
45
+ float d = dot( r, r );
46
+ m = min( m, d );
47
+ }
48
+ return sqrt(m);
49
+ }
50
+ ` + shader.fragmentShader;
51
+ shader.fragmentShader = shader.fragmentShader.replace("#include <map_fragment>", `#include <map_fragment>
52
+
53
+ float noise = voronoi(vUv * 0.6);
54
+ float mixVal = smoothstep(0.4, 0.6, noise);
55
+
56
+ vec3 tex1 = texture(uTextureArray, vec3(vUv, vTextureId)).rgb;
57
+ vec3 tex2 = texture(uTextureArray, vec3(-vUv + 0.35, vTextureId)).rgb;
58
+
59
+ vec3 texSample = mix(tex1, tex2, mixVal) * (voronoi(vUv * 0.5 + 12.0) * 0.2 + 0.9);
60
+
61
+ diffuseColor.rgb *= texSample;`);
62
+ if (normalTextureArray) {
63
+ shader.fragmentShader = shader.fragmentShader.replace("#include <normal_fragment_maps>", `vec3 mapN = texture(uNormalTextureArray, vec3(vUv, vTextureId)).rgb * 2.0 - 1.0;
64
+ normal = normalize(tbn * mapN);`);
65
+ }
66
+ };
67
+ }
@@ -0,0 +1,10 @@
1
+ import { Mesh } from "three";
2
+ import { Interpreter } from "../interpreter.js";
3
+ export type WaterMeshOptions = {};
4
+ export declare class WaterMesh extends Mesh {
5
+ constructor(interpreter: Interpreter, options?: WaterMeshOptions);
6
+ dispose(): void;
7
+ }
8
+ export * from "./texture.js";
9
+ export * from "./material.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/water/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAY,MAAM,OAAO,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIhD,MAAM,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAIlC,qBAAa,SAAU,SAAQ,IAAI;gBACrB,WAAW,EAAE,WAAW,EAAE,OAAO,GAAE,gBAAqB;IAsCpE,OAAO,IAAI,IAAI;CAMhB;AAED,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA"}
@@ -0,0 +1,46 @@
1
+ import { Mesh, Material } from "three";
2
+ import { TilesGeometry } from "../tiles/geometry.js";
3
+ import { coerce, object } from "zod";
4
+ const waterSchema = object({ y: coerce.number() });
5
+ export class WaterMesh extends Mesh {
6
+ constructor(interpreter, options = {}) {
7
+ const rows = interpreter.getRows();
8
+ const cols = interpreter.getCols();
9
+ // Build tiles[z][x] => Array<Tile>, containing only water tiles
10
+ const tiles = new Array(rows)
11
+ .fill(undefined)
12
+ .map(() => new Array(cols).fill(undefined).map(() => []));
13
+ for (let z = 0; z < rows; z++) {
14
+ for (let x = 0; x < cols; x++) {
15
+ const entries = interpreter.getCalls([z, x], { water: waterSchema });
16
+ const stack = [];
17
+ for (const [, parsed] of entries) {
18
+ stack.push({
19
+ type: "water",
20
+ y: parsed.y,
21
+ textureId: 0,
22
+ cellIndexX: x,
23
+ cellIndexZ: z,
24
+ });
25
+ }
26
+ tiles[z][x] = stack;
27
+ }
28
+ }
29
+ const cellSize = interpreter.getCellSize();
30
+ const mapSizeX = cols * cellSize;
31
+ const mapSizeZ = rows * cellSize;
32
+ const geometry = new TilesGeometry(tiles, [], mapSizeX, mapSizeZ);
33
+ interpreter.setAsset("waterGeometry", geometry);
34
+ super(geometry);
35
+ this.renderOrder = 1;
36
+ this.frustumCulled = true;
37
+ }
38
+ dispose() {
39
+ this.geometry.dispose();
40
+ if (this.material instanceof Material) {
41
+ this.material.dispose();
42
+ }
43
+ }
44
+ }
45
+ export * from "./texture.js";
46
+ export * from "./material.js";
@@ -0,0 +1,5 @@
1
+ import { MeshStandardMaterial } from "three";
2
+ export declare function createWaterMaterial(opts?: {
3
+ color?: number | string;
4
+ }): MeshStandardMaterial;
5
+ //# sourceMappingURL=material.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"material.d.ts","sourceRoot":"","sources":["../../src/water/material.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EAIrB,MAAM,OAAO,CAAC;AAGf,wBAAgB,mBAAmB,CACjC,IAAI,GAAE;IACJ,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACpB,wBAoDP"}
@@ -0,0 +1,46 @@
1
+ import { MeshStandardMaterial, TangentSpaceNormalMap, Vector2, DoubleSide, } from "three";
2
+ import { createWaveHeightTexture, createWaveNormalTexture } from "./texture.js";
3
+ export function createWaterMaterial(opts = {}) {
4
+ // Water Effect Setup
5
+ const waveHeight = createWaveHeightTexture({
6
+ size: 512,
7
+ windSpeed: 8,
8
+ heightScale: 10.0, // Reduced scale because base wave height is now larger (physically correct)
9
+ });
10
+ // Create a separate normal map
11
+ const waveNormal = createWaveNormalTexture(waveHeight);
12
+ // Scale texture coordinates up to get more ripples per unit
13
+ waveHeight.repeat.setScalar(0.01);
14
+ waveNormal.repeat.setScalar(0.1);
15
+ const waterMat = new MeshStandardMaterial({
16
+ transparent: true,
17
+ color: opts.color ?? 0xccccff,
18
+ metalness: 0.3,
19
+ roughness: 0.1,
20
+ normalMap: waveNormal,
21
+ normalMapType: TangentSpaceNormalMap,
22
+ opacity: 0.5,
23
+ displacementScale: 0.1, // Use 1.0 for physically correct displacement
24
+ displacementMap: waveHeight,
25
+ normalScale: new Vector2().setScalar(0.5),
26
+ side: DoubleSide,
27
+ });
28
+ const waterFlow = { x: 0.005, y: 0.002 };
29
+ const normalFlow = { x: -0.005, y: 0.01 };
30
+ let waterTime = 0;
31
+ // Attach tick function to material
32
+ waterMat.tick = (delta) => {
33
+ waterTime += delta;
34
+ // Animate displacement map (height texture)
35
+ if (waterMat.displacementMap) {
36
+ waterMat.displacementMap.offset.x = (waterFlow.x * waterTime) % 1;
37
+ waterMat.displacementMap.offset.y = (waterFlow.y * waterTime) % 1;
38
+ }
39
+ // Animate normal map in a different direction for layered effect
40
+ if (waterMat.normalMap) {
41
+ waterMat.normalMap.offset.x = (normalFlow.x * waterTime) % 1;
42
+ waterMat.normalMap.offset.y = (normalFlow.y * waterTime) % 1;
43
+ }
44
+ };
45
+ return waterMat;
46
+ }
@@ -0,0 +1,15 @@
1
+ import { DataTexture, Vector2 } from "three";
2
+ export type WaveTextureOptions = {
3
+ size?: number;
4
+ seed?: number;
5
+ windDirection?: Vector2;
6
+ windSpeed?: number;
7
+ alignment?: number;
8
+ heightScale?: number;
9
+ };
10
+ export declare function createWaveHeightTexture(options?: WaveTextureOptions): DataTexture;
11
+ export type WaveNormalOptions = {
12
+ strength?: number;
13
+ };
14
+ export declare function createWaveNormalTexture(heightTexture: DataTexture, options?: WaveNormalOptions): DataTexture;
15
+ //# sourceMappingURL=texture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"texture.d.ts","sourceRoot":"","sources":["../../src/water/texture.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EAQX,OAAO,EACR,MAAM,OAAO,CAAC;AAIf,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,wBAAgB,uBAAuB,CACrC,OAAO,GAAE,kBAAuB,GAC/B,WAAW,CA+Jb;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,WAAW,EAC1B,OAAO,GAAE,iBAAsB,GAC9B,WAAW,CAsEb"}