nova64 0.2.6 → 0.2.7
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/examples/strider-demo-3d/fix-game.sh +0 -0
- package/dist/runtime/api-2d.js +1158 -0
- package/dist/runtime/api-3d/camera.js +73 -0
- package/dist/runtime/api-3d/instancing.js +180 -0
- package/dist/runtime/api-3d/lights.js +51 -0
- package/dist/runtime/api-3d/materials.js +47 -0
- package/dist/runtime/api-3d/models.js +84 -0
- package/dist/runtime/api-3d/particles.js +296 -0
- package/dist/runtime/api-3d/pbr.js +113 -0
- package/dist/runtime/api-3d/primitives.js +304 -0
- package/dist/runtime/api-3d/scene.js +169 -0
- package/dist/runtime/api-3d/transforms.js +161 -0
- package/dist/runtime/api-3d.js +166 -0
- package/dist/runtime/api-effects.js +840 -0
- package/dist/runtime/api-gameutils.js +476 -0
- package/dist/runtime/api-generative.js +610 -0
- package/dist/runtime/api-presets.js +85 -0
- package/dist/runtime/api-skybox.js +232 -0
- package/dist/runtime/api-sprites.js +100 -0
- package/dist/runtime/api-voxel.js +712 -0
- package/dist/runtime/api.js +201 -0
- package/dist/runtime/assets.js +27 -0
- package/dist/runtime/audio.js +114 -0
- package/dist/runtime/collision.js +47 -0
- package/dist/runtime/console.js +101 -0
- package/dist/runtime/editor.js +233 -0
- package/dist/runtime/font.js +233 -0
- package/dist/runtime/framebuffer.js +28 -0
- package/dist/runtime/fullscreen-button.js +185 -0
- package/dist/runtime/gpu-canvas2d.js +47 -0
- package/dist/runtime/gpu-threejs.js +643 -0
- package/dist/runtime/gpu-webgl2.js +310 -0
- package/dist/runtime/index.d.ts +682 -0
- package/dist/runtime/index.js +22 -0
- package/dist/runtime/input.js +225 -0
- package/dist/runtime/logger.js +60 -0
- package/dist/runtime/physics.js +101 -0
- package/dist/runtime/screens.js +213 -0
- package/dist/runtime/storage.js +38 -0
- package/dist/runtime/store.js +151 -0
- package/dist/runtime/textinput.js +68 -0
- package/dist/runtime/ui/buttons.js +124 -0
- package/dist/runtime/ui/panels.js +105 -0
- package/dist/runtime/ui/text.js +86 -0
- package/dist/runtime/ui/widgets.js +141 -0
- package/dist/runtime/ui.js +111 -0
- package/package.json +34 -32
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nova64 Voxel Engine API
|
|
3
|
+
*
|
|
4
|
+
* Efficient voxel rendering system for Minecraft-style games with:
|
|
5
|
+
* - Chunk-based world management
|
|
6
|
+
* - Greedy meshing for performance
|
|
7
|
+
* - Multiple block types with textures
|
|
8
|
+
* - World generation (terrain, caves, trees)
|
|
9
|
+
* - Block placement/breaking
|
|
10
|
+
* - Collision detection
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as THREE from 'three';
|
|
14
|
+
|
|
15
|
+
export function voxelApi(gpu) {
|
|
16
|
+
// World configuration
|
|
17
|
+
const CHUNK_SIZE = 16;
|
|
18
|
+
const CHUNK_HEIGHT = 64;
|
|
19
|
+
const RENDER_DISTANCE = 4; // chunks in each direction
|
|
20
|
+
|
|
21
|
+
// Block types
|
|
22
|
+
const BLOCK_TYPES = {
|
|
23
|
+
AIR: 0,
|
|
24
|
+
GRASS: 1,
|
|
25
|
+
DIRT: 2,
|
|
26
|
+
STONE: 3,
|
|
27
|
+
SAND: 4,
|
|
28
|
+
WATER: 5,
|
|
29
|
+
WOOD: 6,
|
|
30
|
+
LEAVES: 7,
|
|
31
|
+
COBBLESTONE: 8,
|
|
32
|
+
PLANKS: 9,
|
|
33
|
+
GLASS: 10,
|
|
34
|
+
BRICK: 11,
|
|
35
|
+
SNOW: 12,
|
|
36
|
+
ICE: 13,
|
|
37
|
+
BEDROCK: 14,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Block colors (for texture-less rendering) — tuned for visual biome distinction
|
|
41
|
+
const BLOCK_COLORS = {
|
|
42
|
+
[BLOCK_TYPES.GRASS]: 0x55cc33,
|
|
43
|
+
[BLOCK_TYPES.DIRT]: 0x996644,
|
|
44
|
+
[BLOCK_TYPES.STONE]: 0xaaaaaa,
|
|
45
|
+
[BLOCK_TYPES.SAND]: 0xffdd88,
|
|
46
|
+
[BLOCK_TYPES.WATER]: 0x2288dd,
|
|
47
|
+
[BLOCK_TYPES.WOOD]: 0x774422,
|
|
48
|
+
[BLOCK_TYPES.LEAVES]: 0x116622,
|
|
49
|
+
[BLOCK_TYPES.COBBLESTONE]: 0x667788,
|
|
50
|
+
[BLOCK_TYPES.PLANKS]: 0xddaa55,
|
|
51
|
+
[BLOCK_TYPES.GLASS]: 0xccffff,
|
|
52
|
+
[BLOCK_TYPES.BRICK]: 0xcc4433,
|
|
53
|
+
[BLOCK_TYPES.SNOW]: 0xeeeeff,
|
|
54
|
+
[BLOCK_TYPES.ICE]: 0x99ddff,
|
|
55
|
+
[BLOCK_TYPES.BEDROCK]: 0x333333,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// World data
|
|
59
|
+
const chunks = new Map(); // key: "x,z" -> Chunk
|
|
60
|
+
const chunkMeshes = new Map(); // key: "x,z" -> THREE.Mesh
|
|
61
|
+
let worldSeed = Math.floor(Math.random() * 50000); // random start for biome variety
|
|
62
|
+
|
|
63
|
+
// Noise function for terrain generation (simple Perlin-like)
|
|
64
|
+
function noise2D(x, z) {
|
|
65
|
+
const n = Math.sin(x * 12.9898 + z * 78.233) * 43758.5453;
|
|
66
|
+
return n - Math.floor(n);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function smoothNoise(x, z) {
|
|
70
|
+
const corners =
|
|
71
|
+
(noise2D(x - 1, z - 1) +
|
|
72
|
+
noise2D(x + 1, z - 1) +
|
|
73
|
+
noise2D(x - 1, z + 1) +
|
|
74
|
+
noise2D(x + 1, z + 1)) /
|
|
75
|
+
16;
|
|
76
|
+
const sides =
|
|
77
|
+
(noise2D(x - 1, z) + noise2D(x + 1, z) + noise2D(x, z - 1) + noise2D(x, z + 1)) / 8;
|
|
78
|
+
const center = noise2D(x, z) / 4;
|
|
79
|
+
return corners + sides + center;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function interpolatedNoise(x, z) {
|
|
83
|
+
const intX = Math.floor(x);
|
|
84
|
+
const fracX = x - intX;
|
|
85
|
+
const intZ = Math.floor(z);
|
|
86
|
+
const fracZ = z - intZ;
|
|
87
|
+
|
|
88
|
+
const v1 = smoothNoise(intX, intZ);
|
|
89
|
+
const v2 = smoothNoise(intX + 1, intZ);
|
|
90
|
+
const v3 = smoothNoise(intX, intZ + 1);
|
|
91
|
+
const v4 = smoothNoise(intX + 1, intZ + 1);
|
|
92
|
+
|
|
93
|
+
const i1 = v1 * (1 - fracX) + v2 * fracX;
|
|
94
|
+
const i2 = v3 * (1 - fracX) + v4 * fracX;
|
|
95
|
+
|
|
96
|
+
return i1 * (1 - fracZ) + i2 * fracZ;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function perlinNoise(x, z, octaves = 4, persistence = 0.5) {
|
|
100
|
+
let total = 0;
|
|
101
|
+
let frequency = 1;
|
|
102
|
+
let amplitude = 1;
|
|
103
|
+
let maxValue = 0;
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < octaves; i++) {
|
|
106
|
+
total += interpolatedNoise(x * frequency * 0.01, z * frequency * 0.01) * amplitude;
|
|
107
|
+
maxValue += amplitude;
|
|
108
|
+
amplitude *= persistence;
|
|
109
|
+
frequency *= 2;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return total / maxValue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Chunk class
|
|
116
|
+
class Chunk {
|
|
117
|
+
constructor(chunkX, chunkZ) {
|
|
118
|
+
this.chunkX = chunkX;
|
|
119
|
+
this.chunkZ = chunkZ;
|
|
120
|
+
this.blocks = new Uint8Array(CHUNK_SIZE * CHUNK_HEIGHT * CHUNK_SIZE);
|
|
121
|
+
this.dirty = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
getBlock(x, y, z) {
|
|
125
|
+
if (x < 0 || x >= CHUNK_SIZE || y < 0 || y >= CHUNK_HEIGHT || z < 0 || z >= CHUNK_SIZE) {
|
|
126
|
+
return BLOCK_TYPES.AIR;
|
|
127
|
+
}
|
|
128
|
+
const index = x + z * CHUNK_SIZE + y * CHUNK_SIZE * CHUNK_SIZE;
|
|
129
|
+
return this.blocks[index];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
setBlock(x, y, z, blockType) {
|
|
133
|
+
if (x < 0 || x >= CHUNK_SIZE || y < 0 || y >= CHUNK_HEIGHT || z < 0 || z >= CHUNK_SIZE) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const index = x + z * CHUNK_SIZE + y * CHUNK_SIZE * CHUNK_SIZE;
|
|
137
|
+
this.blocks[index] = blockType;
|
|
138
|
+
this.dirty = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Get or create chunk
|
|
143
|
+
function getChunk(chunkX, chunkZ) {
|
|
144
|
+
const key = `${chunkX},${chunkZ}`;
|
|
145
|
+
if (!chunks.has(key)) {
|
|
146
|
+
const chunk = new Chunk(chunkX, chunkZ);
|
|
147
|
+
generateChunkTerrain(chunk);
|
|
148
|
+
chunks.set(key, chunk);
|
|
149
|
+
}
|
|
150
|
+
return chunks.get(key);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Get chunk without creating it (returns null if doesn't exist)
|
|
154
|
+
function getChunkIfExists(chunkX, chunkZ) {
|
|
155
|
+
const key = `${chunkX},${chunkZ}`;
|
|
156
|
+
return chunks.get(key) || null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Terrain generation
|
|
160
|
+
function generateChunkTerrain(chunk) {
|
|
161
|
+
const baseX = chunk.chunkX * CHUNK_SIZE;
|
|
162
|
+
const baseZ = chunk.chunkZ * CHUNK_SIZE;
|
|
163
|
+
|
|
164
|
+
// Pending trees to place after terrain fill (avoid overwriting terrain)
|
|
165
|
+
const pendingTrees = [];
|
|
166
|
+
|
|
167
|
+
for (let x = 0; x < CHUNK_SIZE; x++) {
|
|
168
|
+
for (let z = 0; z < CHUNK_SIZE; z++) {
|
|
169
|
+
const worldX = baseX + x;
|
|
170
|
+
const worldZ = baseZ + z;
|
|
171
|
+
|
|
172
|
+
// Biome selection via temperature + moisture noise
|
|
173
|
+
const temperature = perlinNoise(worldX * 0.5 + worldSeed, worldZ * 0.5 + worldSeed, 2, 0.5);
|
|
174
|
+
const moisture = perlinNoise(
|
|
175
|
+
worldX * 0.3 + 1000 + worldSeed,
|
|
176
|
+
worldZ * 0.3 + 1000 + worldSeed,
|
|
177
|
+
2,
|
|
178
|
+
0.5
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Per-biome height profile — each biome has unique surface, terrain shape, and density
|
|
182
|
+
let heightScale = 20;
|
|
183
|
+
let heightBase = 32;
|
|
184
|
+
let surfaceBlock = BLOCK_TYPES.GRASS;
|
|
185
|
+
let subBlock = BLOCK_TYPES.DIRT;
|
|
186
|
+
let treeChance = 0;
|
|
187
|
+
let waterLevel = 30;
|
|
188
|
+
|
|
189
|
+
if (temperature < 0.2) {
|
|
190
|
+
// ── Frozen Tundra ── flat icy plains
|
|
191
|
+
surfaceBlock = BLOCK_TYPES.SNOW;
|
|
192
|
+
subBlock = BLOCK_TYPES.ICE;
|
|
193
|
+
heightScale = 6;
|
|
194
|
+
heightBase = 33;
|
|
195
|
+
treeChance = 0;
|
|
196
|
+
} else if (temperature < 0.35 && moisture > 0.5) {
|
|
197
|
+
// ── Taiga ── gray rocky forest
|
|
198
|
+
surfaceBlock = BLOCK_TYPES.COBBLESTONE;
|
|
199
|
+
subBlock = BLOCK_TYPES.STONE;
|
|
200
|
+
heightScale = 18;
|
|
201
|
+
heightBase = 34;
|
|
202
|
+
treeChance = 0.06;
|
|
203
|
+
} else if (temperature > 0.7 && moisture < 0.25) {
|
|
204
|
+
// ── Desert ── flat sand dunes
|
|
205
|
+
surfaceBlock = BLOCK_TYPES.SAND;
|
|
206
|
+
subBlock = BLOCK_TYPES.SAND;
|
|
207
|
+
heightScale = 4;
|
|
208
|
+
heightBase = 31;
|
|
209
|
+
treeChance = 0;
|
|
210
|
+
} else if (temperature > 0.6 && moisture > 0.6) {
|
|
211
|
+
// ── Jungle ── dense dark-green valleys
|
|
212
|
+
surfaceBlock = BLOCK_TYPES.LEAVES;
|
|
213
|
+
subBlock = BLOCK_TYPES.DIRT;
|
|
214
|
+
heightScale = 22;
|
|
215
|
+
heightBase = 28;
|
|
216
|
+
treeChance = 0.15;
|
|
217
|
+
} else if (moisture < 0.3) {
|
|
218
|
+
// ── Savanna ── dry brown earth
|
|
219
|
+
surfaceBlock = BLOCK_TYPES.DIRT;
|
|
220
|
+
subBlock = BLOCK_TYPES.SAND;
|
|
221
|
+
heightScale = 5;
|
|
222
|
+
heightBase = 33;
|
|
223
|
+
treeChance = 0.005;
|
|
224
|
+
} else if (temperature > 0.4 && moisture > 0.4) {
|
|
225
|
+
// ── Forest ── classic green hills
|
|
226
|
+
surfaceBlock = BLOCK_TYPES.GRASS;
|
|
227
|
+
subBlock = BLOCK_TYPES.DIRT;
|
|
228
|
+
heightScale = 14;
|
|
229
|
+
heightBase = 32;
|
|
230
|
+
treeChance = 0.08;
|
|
231
|
+
} else if (temperature < 0.35) {
|
|
232
|
+
// ── Snowy Hills ── tall snowy mountains
|
|
233
|
+
surfaceBlock = BLOCK_TYPES.SNOW;
|
|
234
|
+
subBlock = BLOCK_TYPES.STONE;
|
|
235
|
+
heightScale = 35;
|
|
236
|
+
heightBase = 30;
|
|
237
|
+
treeChance = 0.02;
|
|
238
|
+
} else {
|
|
239
|
+
// ── Plains ── gentle rolling grassland
|
|
240
|
+
surfaceBlock = BLOCK_TYPES.GRASS;
|
|
241
|
+
subBlock = BLOCK_TYPES.DIRT;
|
|
242
|
+
heightScale = 6;
|
|
243
|
+
heightBase = 32;
|
|
244
|
+
treeChance = 0.015;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const height = Math.floor(perlinNoise(worldX, worldZ, 4, 0.5) * heightScale + heightBase);
|
|
248
|
+
|
|
249
|
+
for (let y = 0; y < CHUNK_HEIGHT; y++) {
|
|
250
|
+
if (y === 0) {
|
|
251
|
+
chunk.setBlock(x, y, z, BLOCK_TYPES.BEDROCK);
|
|
252
|
+
} else if (y < height - 3) {
|
|
253
|
+
chunk.setBlock(x, y, z, BLOCK_TYPES.STONE);
|
|
254
|
+
} else if (y < height - 1) {
|
|
255
|
+
chunk.setBlock(x, y, z, subBlock);
|
|
256
|
+
} else if (y === height - 1) {
|
|
257
|
+
chunk.setBlock(x, y, z, surfaceBlock);
|
|
258
|
+
} else if (y < waterLevel && y >= height) {
|
|
259
|
+
chunk.setBlock(x, y, z, BLOCK_TYPES.WATER);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Cave generation
|
|
263
|
+
if (y > 0 && y < height - 5) {
|
|
264
|
+
const cave = perlinNoise(worldX * 0.5, y * 0.5, worldZ * 0.5, 3, 0.5);
|
|
265
|
+
if (cave > 0.6) {
|
|
266
|
+
chunk.setBlock(x, y, z, BLOCK_TYPES.AIR);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Random tree placement (seeded by world position)
|
|
272
|
+
if (height > waterLevel && treeChance > 0) {
|
|
273
|
+
const treeSeed = Math.sin(worldX * 12.9898 + worldZ * 78.233) * 43758.5453;
|
|
274
|
+
const treeRoll = treeSeed - Math.floor(treeSeed);
|
|
275
|
+
if (treeRoll < treeChance && x > 2 && x < CHUNK_SIZE - 3 && z > 2 && z < CHUNK_SIZE - 3) {
|
|
276
|
+
pendingTrees.push({ x, y: height, z });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Place trees after terrain is fully generated
|
|
283
|
+
for (const t of pendingTrees) {
|
|
284
|
+
const trunkHeight = 4 + Math.floor(Math.abs(Math.sin(t.x * 7 + t.z * 13)) * 3);
|
|
285
|
+
for (let i = 0; i < trunkHeight; i++) {
|
|
286
|
+
chunk.setBlock(t.x, t.y + i, t.z, BLOCK_TYPES.WOOD);
|
|
287
|
+
}
|
|
288
|
+
const leafY = t.y + trunkHeight;
|
|
289
|
+
for (let dx = -2; dx <= 2; dx++) {
|
|
290
|
+
for (let dy = -2; dy <= 2; dy++) {
|
|
291
|
+
for (let dz = -2; dz <= 2; dz++) {
|
|
292
|
+
if (Math.abs(dx) + Math.abs(dy) + Math.abs(dz) < 4) {
|
|
293
|
+
const lx = t.x + dx,
|
|
294
|
+
ly = leafY + dy,
|
|
295
|
+
lz = t.z + dz;
|
|
296
|
+
if (lx >= 0 && lx < CHUNK_SIZE && lz >= 0 && lz < CHUNK_SIZE && ly < CHUNK_HEIGHT) {
|
|
297
|
+
chunk.setBlock(lx, ly, lz, BLOCK_TYPES.LEAVES);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Greedy meshing algorithm for efficient rendering
|
|
307
|
+
function createChunkMesh(chunk) {
|
|
308
|
+
const geometry = new THREE.BufferGeometry();
|
|
309
|
+
const vertices = [];
|
|
310
|
+
const normals = [];
|
|
311
|
+
const colors = [];
|
|
312
|
+
const uvs = [];
|
|
313
|
+
const indices = [];
|
|
314
|
+
let vertexCount = 0;
|
|
315
|
+
|
|
316
|
+
// Face directions
|
|
317
|
+
const dirs = [
|
|
318
|
+
[0, 0, 1], // Front
|
|
319
|
+
[0, 0, -1], // Back
|
|
320
|
+
[1, 0, 0], // Right
|
|
321
|
+
[-1, 0, 0], // Left
|
|
322
|
+
[0, 1, 0], // Top
|
|
323
|
+
[0, -1, 0], // Bottom
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
const dirNormals = [
|
|
327
|
+
[0, 0, 1],
|
|
328
|
+
[0, 0, -1],
|
|
329
|
+
[1, 0, 0],
|
|
330
|
+
[-1, 0, 0],
|
|
331
|
+
[0, 1, 0],
|
|
332
|
+
[0, -1, 0],
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
// Check if block face should be rendered
|
|
336
|
+
function shouldRenderFace(x, y, z, dir) {
|
|
337
|
+
const nx = x + dir[0];
|
|
338
|
+
const ny = y + dir[1];
|
|
339
|
+
const nz = z + dir[2];
|
|
340
|
+
|
|
341
|
+
// Check neighbor in same chunk
|
|
342
|
+
if (
|
|
343
|
+
nx >= 0 &&
|
|
344
|
+
nx < CHUNK_SIZE &&
|
|
345
|
+
ny >= 0 &&
|
|
346
|
+
ny < CHUNK_HEIGHT &&
|
|
347
|
+
nz >= 0 &&
|
|
348
|
+
nz < CHUNK_SIZE
|
|
349
|
+
) {
|
|
350
|
+
const neighbor = chunk.getBlock(nx, ny, nz);
|
|
351
|
+
return neighbor === BLOCK_TYPES.AIR || neighbor === BLOCK_TYPES.WATER;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Check neighbor in adjacent chunk (only if it exists - don't create it!)
|
|
355
|
+
if (nx < 0 || nx >= CHUNK_SIZE || nz < 0 || nz >= CHUNK_SIZE) {
|
|
356
|
+
const neighborChunkX = chunk.chunkX + Math.floor(nx / CHUNK_SIZE);
|
|
357
|
+
const neighborChunkZ = chunk.chunkZ + Math.floor(nz / CHUNK_SIZE);
|
|
358
|
+
const neighborChunk = getChunkIfExists(neighborChunkX, neighborChunkZ);
|
|
359
|
+
|
|
360
|
+
// If neighbor chunk doesn't exist yet, assume it's air (render the face)
|
|
361
|
+
if (!neighborChunk) {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const localX = ((nx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
|
366
|
+
const localZ = ((nz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
|
367
|
+
const neighbor = neighborChunk.getBlock(localX, ny, localZ);
|
|
368
|
+
return neighbor === BLOCK_TYPES.AIR || neighbor === BLOCK_TYPES.WATER;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return ny < 0 || ny >= CHUNK_HEIGHT;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Add face to mesh
|
|
375
|
+
function addFace(x, y, z, dir, dirIndex, blockType) {
|
|
376
|
+
const color = new THREE.Color(BLOCK_COLORS[blockType] || 0xffffff);
|
|
377
|
+
|
|
378
|
+
// Ambient occlusion factor based on face direction
|
|
379
|
+
const aoFactors = [0.9, 0.9, 0.85, 0.85, 1.0, 0.7];
|
|
380
|
+
const ao = aoFactors[dirIndex];
|
|
381
|
+
color.multiplyScalar(ao);
|
|
382
|
+
|
|
383
|
+
const normal = dirNormals[dirIndex];
|
|
384
|
+
|
|
385
|
+
// Define face vertices based on direction
|
|
386
|
+
let faceVertices;
|
|
387
|
+
if (dirIndex === 0) {
|
|
388
|
+
// Front (+Z)
|
|
389
|
+
faceVertices = [
|
|
390
|
+
[x, y, z + 1],
|
|
391
|
+
[x + 1, y, z + 1],
|
|
392
|
+
[x + 1, y + 1, z + 1],
|
|
393
|
+
[x, y + 1, z + 1],
|
|
394
|
+
];
|
|
395
|
+
} else if (dirIndex === 1) {
|
|
396
|
+
// Back (-Z)
|
|
397
|
+
faceVertices = [
|
|
398
|
+
[x + 1, y, z],
|
|
399
|
+
[x, y, z],
|
|
400
|
+
[x, y + 1, z],
|
|
401
|
+
[x + 1, y + 1, z],
|
|
402
|
+
];
|
|
403
|
+
} else if (dirIndex === 2) {
|
|
404
|
+
// Right (+X)
|
|
405
|
+
faceVertices = [
|
|
406
|
+
[x + 1, y, z + 1],
|
|
407
|
+
[x + 1, y, z],
|
|
408
|
+
[x + 1, y + 1, z],
|
|
409
|
+
[x + 1, y + 1, z + 1],
|
|
410
|
+
];
|
|
411
|
+
} else if (dirIndex === 3) {
|
|
412
|
+
// Left (-X)
|
|
413
|
+
faceVertices = [
|
|
414
|
+
[x, y, z],
|
|
415
|
+
[x, y, z + 1],
|
|
416
|
+
[x, y + 1, z + 1],
|
|
417
|
+
[x, y + 1, z],
|
|
418
|
+
];
|
|
419
|
+
} else if (dirIndex === 4) {
|
|
420
|
+
// Top (+Y)
|
|
421
|
+
faceVertices = [
|
|
422
|
+
[x, y + 1, z + 1],
|
|
423
|
+
[x + 1, y + 1, z + 1],
|
|
424
|
+
[x + 1, y + 1, z],
|
|
425
|
+
[x, y + 1, z],
|
|
426
|
+
];
|
|
427
|
+
} else {
|
|
428
|
+
// Bottom (-Y)
|
|
429
|
+
faceVertices = [
|
|
430
|
+
[x, y, z],
|
|
431
|
+
[x + 1, y, z],
|
|
432
|
+
[x + 1, y, z + 1],
|
|
433
|
+
[x, y, z + 1],
|
|
434
|
+
];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Add vertices
|
|
438
|
+
const baseX = chunk.chunkX * CHUNK_SIZE;
|
|
439
|
+
const baseZ = chunk.chunkZ * CHUNK_SIZE;
|
|
440
|
+
|
|
441
|
+
for (let i = 0; i < 4; i++) {
|
|
442
|
+
vertices.push(faceVertices[i][0] + baseX, faceVertices[i][1], faceVertices[i][2] + baseZ);
|
|
443
|
+
normals.push(normal[0], normal[1], normal[2]);
|
|
444
|
+
colors.push(color.r, color.g, color.b);
|
|
445
|
+
}
|
|
446
|
+
// Add standard UV mapping
|
|
447
|
+
uvs.push(0, 1, 1, 1, 1, 0, 0, 0);
|
|
448
|
+
|
|
449
|
+
// Add indices (two triangles per face)
|
|
450
|
+
const offset = vertexCount;
|
|
451
|
+
indices.push(offset, offset + 1, offset + 2);
|
|
452
|
+
indices.push(offset, offset + 2, offset + 3);
|
|
453
|
+
vertexCount += 4;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Iterate through all blocks
|
|
457
|
+
for (let y = 0; y < CHUNK_HEIGHT; y++) {
|
|
458
|
+
for (let z = 0; z < CHUNK_SIZE; z++) {
|
|
459
|
+
for (let x = 0; x < CHUNK_SIZE; x++) {
|
|
460
|
+
const blockType = chunk.getBlock(x, y, z);
|
|
461
|
+
|
|
462
|
+
if (blockType === BLOCK_TYPES.AIR) continue;
|
|
463
|
+
|
|
464
|
+
// Check each face
|
|
465
|
+
for (let d = 0; d < 6; d++) {
|
|
466
|
+
if (shouldRenderFace(x, y, z, dirs[d])) {
|
|
467
|
+
addFace(x, y, z, dirs[d], d, blockType);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Build geometry
|
|
475
|
+
if (vertices.length > 0) {
|
|
476
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
|
477
|
+
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
|
|
478
|
+
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
|
|
479
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
|
480
|
+
geometry.setIndex(indices);
|
|
481
|
+
geometry.computeBoundingSphere();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return geometry;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Update chunk mesh if dirty
|
|
488
|
+
function updateChunkMesh(chunk) {
|
|
489
|
+
if (!chunk.dirty) return;
|
|
490
|
+
|
|
491
|
+
const key = `${chunk.chunkX},${chunk.chunkZ}`;
|
|
492
|
+
|
|
493
|
+
// Remove old mesh
|
|
494
|
+
if (chunkMeshes.has(key)) {
|
|
495
|
+
const oldMesh = chunkMeshes.get(key);
|
|
496
|
+
gpu.scene.remove(oldMesh);
|
|
497
|
+
oldMesh.geometry.dispose();
|
|
498
|
+
chunkMeshes.delete(key);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Create new mesh
|
|
502
|
+
const geometry = createChunkMesh(chunk);
|
|
503
|
+
if (geometry.attributes.position) {
|
|
504
|
+
const material =
|
|
505
|
+
window.VOXEL_MATERIAL ||
|
|
506
|
+
new THREE.MeshStandardMaterial({
|
|
507
|
+
vertexColors: true,
|
|
508
|
+
flatShading: true,
|
|
509
|
+
roughness: 0.8,
|
|
510
|
+
metalness: 0.1,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
514
|
+
mesh.castShadow = true;
|
|
515
|
+
mesh.receiveShadow = true;
|
|
516
|
+
|
|
517
|
+
gpu.scene.add(mesh);
|
|
518
|
+
chunkMeshes.set(key, mesh);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
chunk.dirty = false;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// World coordinates to chunk coordinates
|
|
525
|
+
function worldToChunk(x, z) {
|
|
526
|
+
return {
|
|
527
|
+
chunkX: Math.floor(x / CHUNK_SIZE),
|
|
528
|
+
chunkZ: Math.floor(z / CHUNK_SIZE),
|
|
529
|
+
localX: ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE,
|
|
530
|
+
localZ: ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Get block at world position
|
|
535
|
+
function getBlock(x, y, z) {
|
|
536
|
+
if (y < 0 || y >= CHUNK_HEIGHT) return BLOCK_TYPES.AIR;
|
|
537
|
+
|
|
538
|
+
const { chunkX, chunkZ, localX, localZ } = worldToChunk(x, z);
|
|
539
|
+
const chunk = getChunk(chunkX, chunkZ);
|
|
540
|
+
return chunk.getBlock(localX, y, localZ);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Set block at world position
|
|
544
|
+
function setBlock(x, y, z, blockType) {
|
|
545
|
+
if (y < 0 || y >= CHUNK_HEIGHT) return;
|
|
546
|
+
|
|
547
|
+
const { chunkX, chunkZ, localX, localZ } = worldToChunk(x, z);
|
|
548
|
+
const chunk = getChunk(chunkX, chunkZ);
|
|
549
|
+
chunk.setBlock(localX, y, localZ, blockType);
|
|
550
|
+
|
|
551
|
+
// Mark adjacent chunks as dirty if on boundary
|
|
552
|
+
if (localX === 0) getChunk(chunkX - 1, chunkZ).dirty = true;
|
|
553
|
+
if (localX === CHUNK_SIZE - 1) getChunk(chunkX + 1, chunkZ).dirty = true;
|
|
554
|
+
if (localZ === 0) getChunk(chunkX, chunkZ - 1).dirty = true;
|
|
555
|
+
if (localZ === CHUNK_SIZE - 1) getChunk(chunkX, chunkZ + 1).dirty = true;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Update visible chunks around player
|
|
559
|
+
function updateChunks(playerX, playerZ) {
|
|
560
|
+
const centerChunkX = Math.floor(playerX / CHUNK_SIZE);
|
|
561
|
+
const centerChunkZ = Math.floor(playerZ / CHUNK_SIZE);
|
|
562
|
+
|
|
563
|
+
// Load chunks in render distance
|
|
564
|
+
for (let dx = -RENDER_DISTANCE; dx <= RENDER_DISTANCE; dx++) {
|
|
565
|
+
for (let dz = -RENDER_DISTANCE; dz <= RENDER_DISTANCE; dz++) {
|
|
566
|
+
const chunkX = centerChunkX + dx;
|
|
567
|
+
const chunkZ = centerChunkZ + dz;
|
|
568
|
+
const chunk = getChunk(chunkX, chunkZ);
|
|
569
|
+
updateChunkMesh(chunk);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Unload far chunks (optional, for memory management)
|
|
574
|
+
const keysToRemove = [];
|
|
575
|
+
for (const [key, mesh] of chunkMeshes.entries()) {
|
|
576
|
+
const [chunkX, chunkZ] = key.split(',').map(Number);
|
|
577
|
+
const dx = Math.abs(chunkX - centerChunkX);
|
|
578
|
+
const dz = Math.abs(chunkZ - centerChunkZ);
|
|
579
|
+
|
|
580
|
+
if (dx > RENDER_DISTANCE + 1 || dz > RENDER_DISTANCE + 1) {
|
|
581
|
+
gpu.scene.remove(mesh);
|
|
582
|
+
mesh.geometry.dispose();
|
|
583
|
+
mesh.material.dispose();
|
|
584
|
+
keysToRemove.push(key);
|
|
585
|
+
chunks.delete(key);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
keysToRemove.forEach(key => chunkMeshes.delete(key));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Reset entire world (clear all chunks and meshes)
|
|
593
|
+
function resetWorld() {
|
|
594
|
+
for (const mesh of chunkMeshes.values()) {
|
|
595
|
+
gpu.scene.remove(mesh);
|
|
596
|
+
mesh.geometry.dispose();
|
|
597
|
+
mesh.material.dispose();
|
|
598
|
+
}
|
|
599
|
+
chunkMeshes.clear();
|
|
600
|
+
chunks.clear();
|
|
601
|
+
worldSeed += 5000 + Math.floor(Math.random() * 10000);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Raycast to find block player is looking at
|
|
605
|
+
function raycastBlock(origin, direction, maxDistance = 10) {
|
|
606
|
+
const step = 0.1;
|
|
607
|
+
const pos = { x: origin[0], y: origin[1], z: origin[2] };
|
|
608
|
+
const dir = { x: direction[0], y: direction[1], z: direction[2] };
|
|
609
|
+
|
|
610
|
+
for (let i = 0; i < maxDistance / step; i++) {
|
|
611
|
+
pos.x += dir.x * step;
|
|
612
|
+
pos.y += dir.y * step;
|
|
613
|
+
pos.z += dir.z * step;
|
|
614
|
+
|
|
615
|
+
const blockX = Math.floor(pos.x);
|
|
616
|
+
const blockY = Math.floor(pos.y);
|
|
617
|
+
const blockZ = Math.floor(pos.z);
|
|
618
|
+
|
|
619
|
+
const blockType = getBlock(blockX, blockY, blockZ);
|
|
620
|
+
|
|
621
|
+
if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.WATER) {
|
|
622
|
+
return {
|
|
623
|
+
hit: true,
|
|
624
|
+
position: [blockX, blockY, blockZ],
|
|
625
|
+
blockType: blockType,
|
|
626
|
+
distance: i * step,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return { hit: false };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Check collision with voxel world
|
|
635
|
+
function checkCollision(pos, size) {
|
|
636
|
+
const minX = Math.floor(pos[0] - size);
|
|
637
|
+
const maxX = Math.floor(pos[0] + size);
|
|
638
|
+
const minY = Math.floor(pos[1]);
|
|
639
|
+
const maxY = Math.floor(pos[1] + size * 2);
|
|
640
|
+
const minZ = Math.floor(pos[2] - size);
|
|
641
|
+
const maxZ = Math.floor(pos[2] + size);
|
|
642
|
+
|
|
643
|
+
for (let x = minX; x <= maxX; x++) {
|
|
644
|
+
for (let y = minY; y <= maxY; y++) {
|
|
645
|
+
for (let z = minZ; z <= maxZ; z++) {
|
|
646
|
+
const blockType = getBlock(x, y, z);
|
|
647
|
+
if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.WATER) {
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Generate tree structure
|
|
658
|
+
function placeTree(x, y, z) {
|
|
659
|
+
const trunkHeight = 4 + Math.floor(Math.random() * 3);
|
|
660
|
+
|
|
661
|
+
// Trunk
|
|
662
|
+
for (let i = 0; i < trunkHeight; i++) {
|
|
663
|
+
setBlock(x, y + i, z, BLOCK_TYPES.WOOD);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Leaves
|
|
667
|
+
const leafY = y + trunkHeight;
|
|
668
|
+
for (let dx = -2; dx <= 2; dx++) {
|
|
669
|
+
for (let dy = -2; dy <= 2; dy++) {
|
|
670
|
+
for (let dz = -2; dz <= 2; dz++) {
|
|
671
|
+
if (Math.abs(dx) + Math.abs(dy) + Math.abs(dz) < 4) {
|
|
672
|
+
setBlock(x + dx, leafY + dy, z + dz, BLOCK_TYPES.LEAVES);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Public API
|
|
680
|
+
return {
|
|
681
|
+
BLOCK_TYPES,
|
|
682
|
+
CHUNK_SIZE,
|
|
683
|
+
CHUNK_HEIGHT,
|
|
684
|
+
|
|
685
|
+
// World management
|
|
686
|
+
updateChunks,
|
|
687
|
+
getBlock,
|
|
688
|
+
setBlock,
|
|
689
|
+
|
|
690
|
+
// Block interaction
|
|
691
|
+
raycastBlock,
|
|
692
|
+
checkCollision,
|
|
693
|
+
|
|
694
|
+
// Structures
|
|
695
|
+
placeTree,
|
|
696
|
+
|
|
697
|
+
// World reset
|
|
698
|
+
resetWorld,
|
|
699
|
+
|
|
700
|
+
// Expose to global game context
|
|
701
|
+
exposeTo: function (g) {
|
|
702
|
+
g.BLOCK_TYPES = BLOCK_TYPES;
|
|
703
|
+
g.updateVoxelWorld = updateChunks;
|
|
704
|
+
g.getVoxelBlock = getBlock;
|
|
705
|
+
g.setVoxelBlock = setBlock;
|
|
706
|
+
g.raycastVoxelBlock = raycastBlock;
|
|
707
|
+
g.checkVoxelCollision = checkCollision;
|
|
708
|
+
g.placeVoxelTree = placeTree;
|
|
709
|
+
g.resetVoxelWorld = resetWorld;
|
|
710
|
+
},
|
|
711
|
+
};
|
|
712
|
+
}
|