@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.
- package/LICENSE +7 -0
- package/dist/assets/loader.d.ts +21 -0
- package/dist/assets/loader.d.ts.map +1 -0
- package/dist/assets/loader.js +113 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +27 -0
- package/dist/grammar.d.ts +29 -0
- package/dist/grammar.d.ts.map +1 -0
- package/dist/grammar.js +119 -0
- package/dist/grass/index.d.ts +25 -0
- package/dist/grass/index.d.ts.map +1 -0
- package/dist/grass/index.js +177 -0
- package/dist/grass/material.d.ts +10 -0
- package/dist/grass/material.d.ts.map +1 -0
- package/dist/grass/material.js +80 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/interpreter.d.ts +47 -0
- package/dist/interpreter.d.ts.map +1 -0
- package/dist/interpreter.js +226 -0
- package/dist/locations.d.ts +16 -0
- package/dist/locations.d.ts.map +1 -0
- package/dist/locations.js +58 -0
- package/dist/parser.d.ts +9 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +47 -0
- package/dist/pillars/index.d.ts +7 -0
- package/dist/pillars/index.d.ts.map +1 -0
- package/dist/pillars/index.js +154 -0
- package/dist/pillars/material.d.ts +3 -0
- package/dist/pillars/material.d.ts.map +1 -0
- package/dist/pillars/material.js +43 -0
- package/dist/place/index.d.ts +37 -0
- package/dist/place/index.d.ts.map +1 -0
- package/dist/place/index.js +216 -0
- package/dist/tiles/geometry.d.ts +46 -0
- package/dist/tiles/geometry.d.ts.map +1 -0
- package/dist/tiles/geometry.js +463 -0
- package/dist/tiles/index.d.ts +18 -0
- package/dist/tiles/index.d.ts.map +1 -0
- package/dist/tiles/index.js +121 -0
- package/dist/tiles/material.d.ts +6 -0
- package/dist/tiles/material.d.ts.map +1 -0
- package/dist/tiles/material.js +88 -0
- package/dist/utils/instanced-mesh-group.d.ts +17 -0
- package/dist/utils/instanced-mesh-group.d.ts.map +1 -0
- package/dist/utils/instanced-mesh-group.js +59 -0
- package/dist/utils/random.d.ts +4 -0
- package/dist/utils/random.d.ts.map +1 -0
- package/dist/utils/random.js +19 -0
- package/dist/utils/texture.d.ts +3 -0
- package/dist/utils/texture.d.ts.map +1 -0
- package/dist/utils/texture.js +30 -0
- package/dist/walls/index.d.ts +87 -0
- package/dist/walls/index.d.ts.map +1 -0
- package/dist/walls/index.js +376 -0
- package/dist/walls/material.d.ts +3 -0
- package/dist/walls/material.d.ts.map +1 -0
- package/dist/walls/material.js +67 -0
- package/dist/water/index.d.ts +10 -0
- package/dist/water/index.d.ts.map +1 -0
- package/dist/water/index.js +46 -0
- package/dist/water/material.d.ts +5 -0
- package/dist/water/material.d.ts.map +1 -0
- package/dist/water/material.js +46 -0
- package/dist/water/texture.d.ts +15 -0
- package/dist/water/texture.d.ts.map +1 -0
- package/dist/water/texture.js +201 -0
- 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 @@
|
|
|
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 @@
|
|
|
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"}
|