nova64 0.2.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 +786 -0
- package/index.html +651 -0
- package/package.json +255 -0
- package/public/os9-shell/assets/index-B1Uvacma.js +32825 -0
- package/public/os9-shell/assets/index-B1Uvacma.js.map +1 -0
- package/public/os9-shell/assets/index-DIHfrTaW.css +1 -0
- package/public/os9-shell/index.html +14 -0
- package/public/os9-shell/nova-icon.svg +12 -0
- package/runtime/api-2d.js +878 -0
- package/runtime/api-3d/camera.js +73 -0
- package/runtime/api-3d/instancing.js +180 -0
- package/runtime/api-3d/lights.js +51 -0
- package/runtime/api-3d/materials.js +47 -0
- package/runtime/api-3d/models.js +84 -0
- package/runtime/api-3d/pbr.js +69 -0
- package/runtime/api-3d/primitives.js +304 -0
- package/runtime/api-3d/scene.js +169 -0
- package/runtime/api-3d/transforms.js +161 -0
- package/runtime/api-3d.js +154 -0
- package/runtime/api-effects.js +753 -0
- package/runtime/api-presets.js +85 -0
- package/runtime/api-skybox.js +178 -0
- package/runtime/api-sprites.js +100 -0
- package/runtime/api-voxel.js +601 -0
- package/runtime/api.js +201 -0
- package/runtime/assets.js +27 -0
- package/runtime/audio.js +114 -0
- package/runtime/collision.js +47 -0
- package/runtime/console.js +101 -0
- package/runtime/editor.js +233 -0
- package/runtime/font.js +233 -0
- package/runtime/framebuffer.js +28 -0
- package/runtime/fullscreen-button.js +185 -0
- package/runtime/gpu-canvas2d.js +47 -0
- package/runtime/gpu-threejs.js +639 -0
- package/runtime/gpu-webgl2.js +310 -0
- package/runtime/index.js +22 -0
- package/runtime/input.js +225 -0
- package/runtime/logger.js +60 -0
- package/runtime/physics.js +101 -0
- package/runtime/screens.js +213 -0
- package/runtime/storage.js +38 -0
- package/runtime/store.js +151 -0
- package/runtime/textinput.js +68 -0
- package/runtime/ui/buttons.js +124 -0
- package/runtime/ui/panels.js +105 -0
- package/runtime/ui/text.js +86 -0
- package/runtime/ui/widgets.js +141 -0
- package/runtime/ui.js +111 -0
- package/src/main.js +474 -0
- package/vite.config.js +63 -0
|
@@ -0,0 +1,601 @@
|
|
|
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)
|
|
41
|
+
const BLOCK_COLORS = {
|
|
42
|
+
[BLOCK_TYPES.GRASS]: 0x44aa22,
|
|
43
|
+
[BLOCK_TYPES.DIRT]: 0x885533,
|
|
44
|
+
[BLOCK_TYPES.STONE]: 0x999999,
|
|
45
|
+
[BLOCK_TYPES.SAND]: 0xffdd88,
|
|
46
|
+
[BLOCK_TYPES.WATER]: 0x33aaff,
|
|
47
|
+
[BLOCK_TYPES.WOOD]: 0x774422,
|
|
48
|
+
[BLOCK_TYPES.LEAVES]: 0x228822,
|
|
49
|
+
[BLOCK_TYPES.COBBLESTONE]: 0x888888,
|
|
50
|
+
[BLOCK_TYPES.PLANKS]: 0xddaa55,
|
|
51
|
+
[BLOCK_TYPES.GLASS]: 0xccffff,
|
|
52
|
+
[BLOCK_TYPES.BRICK]: 0xcc4433,
|
|
53
|
+
[BLOCK_TYPES.SNOW]: 0xffffff,
|
|
54
|
+
[BLOCK_TYPES.ICE]: 0xbbffff,
|
|
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
|
+
|
|
62
|
+
// Noise function for terrain generation (simple Perlin-like)
|
|
63
|
+
function noise2D(x, z) {
|
|
64
|
+
const n = Math.sin(x * 12.9898 + z * 78.233) * 43758.5453;
|
|
65
|
+
return n - Math.floor(n);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function smoothNoise(x, z) {
|
|
69
|
+
const corners =
|
|
70
|
+
(noise2D(x - 1, z - 1) +
|
|
71
|
+
noise2D(x + 1, z - 1) +
|
|
72
|
+
noise2D(x - 1, z + 1) +
|
|
73
|
+
noise2D(x + 1, z + 1)) /
|
|
74
|
+
16;
|
|
75
|
+
const sides =
|
|
76
|
+
(noise2D(x - 1, z) + noise2D(x + 1, z) + noise2D(x, z - 1) + noise2D(x, z + 1)) / 8;
|
|
77
|
+
const center = noise2D(x, z) / 4;
|
|
78
|
+
return corners + sides + center;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function interpolatedNoise(x, z) {
|
|
82
|
+
const intX = Math.floor(x);
|
|
83
|
+
const fracX = x - intX;
|
|
84
|
+
const intZ = Math.floor(z);
|
|
85
|
+
const fracZ = z - intZ;
|
|
86
|
+
|
|
87
|
+
const v1 = smoothNoise(intX, intZ);
|
|
88
|
+
const v2 = smoothNoise(intX + 1, intZ);
|
|
89
|
+
const v3 = smoothNoise(intX, intZ + 1);
|
|
90
|
+
const v4 = smoothNoise(intX + 1, intZ + 1);
|
|
91
|
+
|
|
92
|
+
const i1 = v1 * (1 - fracX) + v2 * fracX;
|
|
93
|
+
const i2 = v3 * (1 - fracX) + v4 * fracX;
|
|
94
|
+
|
|
95
|
+
return i1 * (1 - fracZ) + i2 * fracZ;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function perlinNoise(x, z, octaves = 4, persistence = 0.5) {
|
|
99
|
+
let total = 0;
|
|
100
|
+
let frequency = 1;
|
|
101
|
+
let amplitude = 1;
|
|
102
|
+
let maxValue = 0;
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < octaves; i++) {
|
|
105
|
+
total += interpolatedNoise(x * frequency * 0.01, z * frequency * 0.01) * amplitude;
|
|
106
|
+
maxValue += amplitude;
|
|
107
|
+
amplitude *= persistence;
|
|
108
|
+
frequency *= 2;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return total / maxValue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Chunk class
|
|
115
|
+
class Chunk {
|
|
116
|
+
constructor(chunkX, chunkZ) {
|
|
117
|
+
this.chunkX = chunkX;
|
|
118
|
+
this.chunkZ = chunkZ;
|
|
119
|
+
this.blocks = new Uint8Array(CHUNK_SIZE * CHUNK_HEIGHT * CHUNK_SIZE);
|
|
120
|
+
this.dirty = true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getBlock(x, y, z) {
|
|
124
|
+
if (x < 0 || x >= CHUNK_SIZE || y < 0 || y >= CHUNK_HEIGHT || z < 0 || z >= CHUNK_SIZE) {
|
|
125
|
+
return BLOCK_TYPES.AIR;
|
|
126
|
+
}
|
|
127
|
+
const index = x + z * CHUNK_SIZE + y * CHUNK_SIZE * CHUNK_SIZE;
|
|
128
|
+
return this.blocks[index];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setBlock(x, y, z, blockType) {
|
|
132
|
+
if (x < 0 || x >= CHUNK_SIZE || y < 0 || y >= CHUNK_HEIGHT || z < 0 || z >= CHUNK_SIZE) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const index = x + z * CHUNK_SIZE + y * CHUNK_SIZE * CHUNK_SIZE;
|
|
136
|
+
this.blocks[index] = blockType;
|
|
137
|
+
this.dirty = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Get or create chunk
|
|
142
|
+
function getChunk(chunkX, chunkZ) {
|
|
143
|
+
const key = `${chunkX},${chunkZ}`;
|
|
144
|
+
if (!chunks.has(key)) {
|
|
145
|
+
const chunk = new Chunk(chunkX, chunkZ);
|
|
146
|
+
generateChunkTerrain(chunk);
|
|
147
|
+
chunks.set(key, chunk);
|
|
148
|
+
}
|
|
149
|
+
return chunks.get(key);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Get chunk without creating it (returns null if doesn't exist)
|
|
153
|
+
function getChunkIfExists(chunkX, chunkZ) {
|
|
154
|
+
const key = `${chunkX},${chunkZ}`;
|
|
155
|
+
return chunks.get(key) || null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Terrain generation
|
|
159
|
+
function generateChunkTerrain(chunk) {
|
|
160
|
+
const baseX = chunk.chunkX * CHUNK_SIZE;
|
|
161
|
+
const baseZ = chunk.chunkZ * CHUNK_SIZE;
|
|
162
|
+
|
|
163
|
+
for (let x = 0; x < CHUNK_SIZE; x++) {
|
|
164
|
+
for (let z = 0; z < CHUNK_SIZE; z++) {
|
|
165
|
+
const worldX = baseX + x;
|
|
166
|
+
const worldZ = baseZ + z;
|
|
167
|
+
|
|
168
|
+
// Generate height using Perlin noise
|
|
169
|
+
const height = Math.floor(perlinNoise(worldX, worldZ, 4, 0.5) * 20 + 32);
|
|
170
|
+
|
|
171
|
+
// Biome selection
|
|
172
|
+
const temperature = perlinNoise(worldX * 0.5, worldZ * 0.5, 2, 0.5);
|
|
173
|
+
const moisture = perlinNoise(worldX * 0.3 + 1000, worldZ * 0.3 + 1000, 2, 0.5);
|
|
174
|
+
|
|
175
|
+
for (let y = 0; y < CHUNK_HEIGHT; y++) {
|
|
176
|
+
if (y === 0) {
|
|
177
|
+
// Bedrock layer
|
|
178
|
+
chunk.setBlock(x, y, z, BLOCK_TYPES.BEDROCK);
|
|
179
|
+
} else if (y < height - 3) {
|
|
180
|
+
// Stone layer
|
|
181
|
+
chunk.setBlock(x, y, z, BLOCK_TYPES.STONE);
|
|
182
|
+
} else if (y < height - 1) {
|
|
183
|
+
// Dirt layer
|
|
184
|
+
chunk.setBlock(x, y, z, BLOCK_TYPES.DIRT);
|
|
185
|
+
} else if (y === height - 1) {
|
|
186
|
+
// Top layer based on biome
|
|
187
|
+
if (temperature < 0.3) {
|
|
188
|
+
chunk.setBlock(x, y, z, BLOCK_TYPES.SNOW);
|
|
189
|
+
} else if (moisture < 0.3) {
|
|
190
|
+
chunk.setBlock(x, y, z, BLOCK_TYPES.SAND);
|
|
191
|
+
} else {
|
|
192
|
+
chunk.setBlock(x, y, z, BLOCK_TYPES.GRASS);
|
|
193
|
+
}
|
|
194
|
+
} else if (y < 30 && y >= height) {
|
|
195
|
+
// Water level
|
|
196
|
+
chunk.setBlock(x, y, z, BLOCK_TYPES.WATER);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Cave generation
|
|
200
|
+
if (y > 0 && y < height - 5) {
|
|
201
|
+
const cave = perlinNoise(worldX * 0.5, y * 0.5, worldZ * 0.5, 3, 0.5);
|
|
202
|
+
if (cave > 0.6) {
|
|
203
|
+
chunk.setBlock(x, y, z, BLOCK_TYPES.AIR);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Greedy meshing algorithm for efficient rendering
|
|
212
|
+
function createChunkMesh(chunk) {
|
|
213
|
+
const geometry = new THREE.BufferGeometry();
|
|
214
|
+
const vertices = [];
|
|
215
|
+
const normals = [];
|
|
216
|
+
const colors = [];
|
|
217
|
+
const uvs = [];
|
|
218
|
+
const indices = [];
|
|
219
|
+
let vertexCount = 0;
|
|
220
|
+
|
|
221
|
+
// Face directions
|
|
222
|
+
const dirs = [
|
|
223
|
+
[0, 0, 1], // Front
|
|
224
|
+
[0, 0, -1], // Back
|
|
225
|
+
[1, 0, 0], // Right
|
|
226
|
+
[-1, 0, 0], // Left
|
|
227
|
+
[0, 1, 0], // Top
|
|
228
|
+
[0, -1, 0], // Bottom
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const dirNormals = [
|
|
232
|
+
[0, 0, 1],
|
|
233
|
+
[0, 0, -1],
|
|
234
|
+
[1, 0, 0],
|
|
235
|
+
[-1, 0, 0],
|
|
236
|
+
[0, 1, 0],
|
|
237
|
+
[0, -1, 0],
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
// Check if block face should be rendered
|
|
241
|
+
function shouldRenderFace(x, y, z, dir) {
|
|
242
|
+
const nx = x + dir[0];
|
|
243
|
+
const ny = y + dir[1];
|
|
244
|
+
const nz = z + dir[2];
|
|
245
|
+
|
|
246
|
+
// Check neighbor in same chunk
|
|
247
|
+
if (
|
|
248
|
+
nx >= 0 &&
|
|
249
|
+
nx < CHUNK_SIZE &&
|
|
250
|
+
ny >= 0 &&
|
|
251
|
+
ny < CHUNK_HEIGHT &&
|
|
252
|
+
nz >= 0 &&
|
|
253
|
+
nz < CHUNK_SIZE
|
|
254
|
+
) {
|
|
255
|
+
const neighbor = chunk.getBlock(nx, ny, nz);
|
|
256
|
+
return neighbor === BLOCK_TYPES.AIR || neighbor === BLOCK_TYPES.WATER;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check neighbor in adjacent chunk (only if it exists - don't create it!)
|
|
260
|
+
if (nx < 0 || nx >= CHUNK_SIZE || nz < 0 || nz >= CHUNK_SIZE) {
|
|
261
|
+
const neighborChunkX = chunk.chunkX + Math.floor(nx / CHUNK_SIZE);
|
|
262
|
+
const neighborChunkZ = chunk.chunkZ + Math.floor(nz / CHUNK_SIZE);
|
|
263
|
+
const neighborChunk = getChunkIfExists(neighborChunkX, neighborChunkZ);
|
|
264
|
+
|
|
265
|
+
// If neighbor chunk doesn't exist yet, assume it's air (render the face)
|
|
266
|
+
if (!neighborChunk) {
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const localX = ((nx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
|
271
|
+
const localZ = ((nz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
|
|
272
|
+
const neighbor = neighborChunk.getBlock(localX, ny, localZ);
|
|
273
|
+
return neighbor === BLOCK_TYPES.AIR || neighbor === BLOCK_TYPES.WATER;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return ny < 0 || ny >= CHUNK_HEIGHT;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Add face to mesh
|
|
280
|
+
function addFace(x, y, z, dir, dirIndex, blockType) {
|
|
281
|
+
const color = new THREE.Color(BLOCK_COLORS[blockType] || 0xffffff);
|
|
282
|
+
|
|
283
|
+
// Ambient occlusion factor based on face direction
|
|
284
|
+
const aoFactors = [0.9, 0.9, 0.85, 0.85, 1.0, 0.7];
|
|
285
|
+
const ao = aoFactors[dirIndex];
|
|
286
|
+
color.multiplyScalar(ao);
|
|
287
|
+
|
|
288
|
+
const normal = dirNormals[dirIndex];
|
|
289
|
+
|
|
290
|
+
// Define face vertices based on direction
|
|
291
|
+
let faceVertices;
|
|
292
|
+
if (dirIndex === 0) {
|
|
293
|
+
// Front (+Z)
|
|
294
|
+
faceVertices = [
|
|
295
|
+
[x, y, z + 1],
|
|
296
|
+
[x + 1, y, z + 1],
|
|
297
|
+
[x + 1, y + 1, z + 1],
|
|
298
|
+
[x, y + 1, z + 1],
|
|
299
|
+
];
|
|
300
|
+
} else if (dirIndex === 1) {
|
|
301
|
+
// Back (-Z)
|
|
302
|
+
faceVertices = [
|
|
303
|
+
[x + 1, y, z],
|
|
304
|
+
[x, y, z],
|
|
305
|
+
[x, y + 1, z],
|
|
306
|
+
[x + 1, y + 1, z],
|
|
307
|
+
];
|
|
308
|
+
} else if (dirIndex === 2) {
|
|
309
|
+
// Right (+X)
|
|
310
|
+
faceVertices = [
|
|
311
|
+
[x + 1, y, z + 1],
|
|
312
|
+
[x + 1, y, z],
|
|
313
|
+
[x + 1, y + 1, z],
|
|
314
|
+
[x + 1, y + 1, z + 1],
|
|
315
|
+
];
|
|
316
|
+
} else if (dirIndex === 3) {
|
|
317
|
+
// Left (-X)
|
|
318
|
+
faceVertices = [
|
|
319
|
+
[x, y, z],
|
|
320
|
+
[x, y, z + 1],
|
|
321
|
+
[x, y + 1, z + 1],
|
|
322
|
+
[x, y + 1, z],
|
|
323
|
+
];
|
|
324
|
+
} else if (dirIndex === 4) {
|
|
325
|
+
// Top (+Y)
|
|
326
|
+
faceVertices = [
|
|
327
|
+
[x, y + 1, z + 1],
|
|
328
|
+
[x + 1, y + 1, z + 1],
|
|
329
|
+
[x + 1, y + 1, z],
|
|
330
|
+
[x, y + 1, z],
|
|
331
|
+
];
|
|
332
|
+
} else {
|
|
333
|
+
// Bottom (-Y)
|
|
334
|
+
faceVertices = [
|
|
335
|
+
[x, y, z],
|
|
336
|
+
[x + 1, y, z],
|
|
337
|
+
[x + 1, y, z + 1],
|
|
338
|
+
[x, y, z + 1],
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Add vertices
|
|
343
|
+
const baseX = chunk.chunkX * CHUNK_SIZE;
|
|
344
|
+
const baseZ = chunk.chunkZ * CHUNK_SIZE;
|
|
345
|
+
|
|
346
|
+
for (let i = 0; i < 4; i++) {
|
|
347
|
+
vertices.push(faceVertices[i][0] + baseX, faceVertices[i][1], faceVertices[i][2] + baseZ);
|
|
348
|
+
normals.push(normal[0], normal[1], normal[2]);
|
|
349
|
+
colors.push(color.r, color.g, color.b);
|
|
350
|
+
}
|
|
351
|
+
// Add standard UV mapping
|
|
352
|
+
uvs.push(0, 1, 1, 1, 1, 0, 0, 0);
|
|
353
|
+
|
|
354
|
+
// Add indices (two triangles per face)
|
|
355
|
+
const offset = vertexCount;
|
|
356
|
+
indices.push(offset, offset + 1, offset + 2);
|
|
357
|
+
indices.push(offset, offset + 2, offset + 3);
|
|
358
|
+
vertexCount += 4;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Iterate through all blocks
|
|
362
|
+
for (let y = 0; y < CHUNK_HEIGHT; y++) {
|
|
363
|
+
for (let z = 0; z < CHUNK_SIZE; z++) {
|
|
364
|
+
for (let x = 0; x < CHUNK_SIZE; x++) {
|
|
365
|
+
const blockType = chunk.getBlock(x, y, z);
|
|
366
|
+
|
|
367
|
+
if (blockType === BLOCK_TYPES.AIR) continue;
|
|
368
|
+
|
|
369
|
+
// Check each face
|
|
370
|
+
for (let d = 0; d < 6; d++) {
|
|
371
|
+
if (shouldRenderFace(x, y, z, dirs[d])) {
|
|
372
|
+
addFace(x, y, z, dirs[d], d, blockType);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Build geometry
|
|
380
|
+
if (vertices.length > 0) {
|
|
381
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
|
382
|
+
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
|
|
383
|
+
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
|
|
384
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
|
385
|
+
geometry.setIndex(indices);
|
|
386
|
+
geometry.computeBoundingSphere();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return geometry;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Update chunk mesh if dirty
|
|
393
|
+
function updateChunkMesh(chunk) {
|
|
394
|
+
if (!chunk.dirty) return;
|
|
395
|
+
|
|
396
|
+
const key = `${chunk.chunkX},${chunk.chunkZ}`;
|
|
397
|
+
|
|
398
|
+
// Remove old mesh
|
|
399
|
+
if (chunkMeshes.has(key)) {
|
|
400
|
+
const oldMesh = chunkMeshes.get(key);
|
|
401
|
+
gpu.scene.remove(oldMesh);
|
|
402
|
+
oldMesh.geometry.dispose();
|
|
403
|
+
chunkMeshes.delete(key);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Create new mesh
|
|
407
|
+
const geometry = createChunkMesh(chunk);
|
|
408
|
+
if (geometry.attributes.position) {
|
|
409
|
+
const material =
|
|
410
|
+
window.VOXEL_MATERIAL ||
|
|
411
|
+
new THREE.MeshStandardMaterial({
|
|
412
|
+
vertexColors: true,
|
|
413
|
+
flatShading: true,
|
|
414
|
+
roughness: 0.8,
|
|
415
|
+
metalness: 0.1,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
419
|
+
mesh.castShadow = true;
|
|
420
|
+
mesh.receiveShadow = true;
|
|
421
|
+
|
|
422
|
+
gpu.scene.add(mesh);
|
|
423
|
+
chunkMeshes.set(key, mesh);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
chunk.dirty = false;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// World coordinates to chunk coordinates
|
|
430
|
+
function worldToChunk(x, z) {
|
|
431
|
+
return {
|
|
432
|
+
chunkX: Math.floor(x / CHUNK_SIZE),
|
|
433
|
+
chunkZ: Math.floor(z / CHUNK_SIZE),
|
|
434
|
+
localX: ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE,
|
|
435
|
+
localZ: ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Get block at world position
|
|
440
|
+
function getBlock(x, y, z) {
|
|
441
|
+
if (y < 0 || y >= CHUNK_HEIGHT) return BLOCK_TYPES.AIR;
|
|
442
|
+
|
|
443
|
+
const { chunkX, chunkZ, localX, localZ } = worldToChunk(x, z);
|
|
444
|
+
const chunk = getChunk(chunkX, chunkZ);
|
|
445
|
+
return chunk.getBlock(localX, y, localZ);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Set block at world position
|
|
449
|
+
function setBlock(x, y, z, blockType) {
|
|
450
|
+
if (y < 0 || y >= CHUNK_HEIGHT) return;
|
|
451
|
+
|
|
452
|
+
const { chunkX, chunkZ, localX, localZ } = worldToChunk(x, z);
|
|
453
|
+
const chunk = getChunk(chunkX, chunkZ);
|
|
454
|
+
chunk.setBlock(localX, y, localZ, blockType);
|
|
455
|
+
|
|
456
|
+
// Mark adjacent chunks as dirty if on boundary
|
|
457
|
+
if (localX === 0) getChunk(chunkX - 1, chunkZ).dirty = true;
|
|
458
|
+
if (localX === CHUNK_SIZE - 1) getChunk(chunkX + 1, chunkZ).dirty = true;
|
|
459
|
+
if (localZ === 0) getChunk(chunkX, chunkZ - 1).dirty = true;
|
|
460
|
+
if (localZ === CHUNK_SIZE - 1) getChunk(chunkX, chunkZ + 1).dirty = true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Update visible chunks around player
|
|
464
|
+
function updateChunks(playerX, playerZ) {
|
|
465
|
+
const centerChunkX = Math.floor(playerX / CHUNK_SIZE);
|
|
466
|
+
const centerChunkZ = Math.floor(playerZ / CHUNK_SIZE);
|
|
467
|
+
|
|
468
|
+
// Load chunks in render distance
|
|
469
|
+
for (let dx = -RENDER_DISTANCE; dx <= RENDER_DISTANCE; dx++) {
|
|
470
|
+
for (let dz = -RENDER_DISTANCE; dz <= RENDER_DISTANCE; dz++) {
|
|
471
|
+
const chunkX = centerChunkX + dx;
|
|
472
|
+
const chunkZ = centerChunkZ + dz;
|
|
473
|
+
const chunk = getChunk(chunkX, chunkZ);
|
|
474
|
+
updateChunkMesh(chunk);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Unload far chunks (optional, for memory management)
|
|
479
|
+
const keysToRemove = [];
|
|
480
|
+
for (const [key, mesh] of chunkMeshes.entries()) {
|
|
481
|
+
const [chunkX, chunkZ] = key.split(',').map(Number);
|
|
482
|
+
const dx = Math.abs(chunkX - centerChunkX);
|
|
483
|
+
const dz = Math.abs(chunkZ - centerChunkZ);
|
|
484
|
+
|
|
485
|
+
if (dx > RENDER_DISTANCE + 1 || dz > RENDER_DISTANCE + 1) {
|
|
486
|
+
gpu.scene.remove(mesh);
|
|
487
|
+
mesh.geometry.dispose();
|
|
488
|
+
mesh.material.dispose();
|
|
489
|
+
keysToRemove.push(key);
|
|
490
|
+
chunks.delete(key);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
keysToRemove.forEach(key => chunkMeshes.delete(key));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Raycast to find block player is looking at
|
|
498
|
+
function raycastBlock(origin, direction, maxDistance = 10) {
|
|
499
|
+
const step = 0.1;
|
|
500
|
+
const pos = { x: origin[0], y: origin[1], z: origin[2] };
|
|
501
|
+
const dir = { x: direction[0], y: direction[1], z: direction[2] };
|
|
502
|
+
|
|
503
|
+
for (let i = 0; i < maxDistance / step; i++) {
|
|
504
|
+
pos.x += dir.x * step;
|
|
505
|
+
pos.y += dir.y * step;
|
|
506
|
+
pos.z += dir.z * step;
|
|
507
|
+
|
|
508
|
+
const blockX = Math.floor(pos.x);
|
|
509
|
+
const blockY = Math.floor(pos.y);
|
|
510
|
+
const blockZ = Math.floor(pos.z);
|
|
511
|
+
|
|
512
|
+
const blockType = getBlock(blockX, blockY, blockZ);
|
|
513
|
+
|
|
514
|
+
if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.WATER) {
|
|
515
|
+
return {
|
|
516
|
+
hit: true,
|
|
517
|
+
position: [blockX, blockY, blockZ],
|
|
518
|
+
blockType: blockType,
|
|
519
|
+
distance: i * step,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return { hit: false };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Check collision with voxel world
|
|
528
|
+
function checkCollision(pos, size) {
|
|
529
|
+
const minX = Math.floor(pos[0] - size);
|
|
530
|
+
const maxX = Math.floor(pos[0] + size);
|
|
531
|
+
const minY = Math.floor(pos[1]);
|
|
532
|
+
const maxY = Math.floor(pos[1] + size * 2);
|
|
533
|
+
const minZ = Math.floor(pos[2] - size);
|
|
534
|
+
const maxZ = Math.floor(pos[2] + size);
|
|
535
|
+
|
|
536
|
+
for (let x = minX; x <= maxX; x++) {
|
|
537
|
+
for (let y = minY; y <= maxY; y++) {
|
|
538
|
+
for (let z = minZ; z <= maxZ; z++) {
|
|
539
|
+
const blockType = getBlock(x, y, z);
|
|
540
|
+
if (blockType !== BLOCK_TYPES.AIR && blockType !== BLOCK_TYPES.WATER) {
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Generate tree structure
|
|
551
|
+
function placeTree(x, y, z) {
|
|
552
|
+
const trunkHeight = 4 + Math.floor(Math.random() * 3);
|
|
553
|
+
|
|
554
|
+
// Trunk
|
|
555
|
+
for (let i = 0; i < trunkHeight; i++) {
|
|
556
|
+
setBlock(x, y + i, z, BLOCK_TYPES.WOOD);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Leaves
|
|
560
|
+
const leafY = y + trunkHeight;
|
|
561
|
+
for (let dx = -2; dx <= 2; dx++) {
|
|
562
|
+
for (let dy = -2; dy <= 2; dy++) {
|
|
563
|
+
for (let dz = -2; dz <= 2; dz++) {
|
|
564
|
+
if (Math.abs(dx) + Math.abs(dy) + Math.abs(dz) < 4) {
|
|
565
|
+
setBlock(x + dx, leafY + dy, z + dz, BLOCK_TYPES.LEAVES);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Public API
|
|
573
|
+
return {
|
|
574
|
+
BLOCK_TYPES,
|
|
575
|
+
CHUNK_SIZE,
|
|
576
|
+
CHUNK_HEIGHT,
|
|
577
|
+
|
|
578
|
+
// World management
|
|
579
|
+
updateChunks,
|
|
580
|
+
getBlock,
|
|
581
|
+
setBlock,
|
|
582
|
+
|
|
583
|
+
// Block interaction
|
|
584
|
+
raycastBlock,
|
|
585
|
+
checkCollision,
|
|
586
|
+
|
|
587
|
+
// Structures
|
|
588
|
+
placeTree,
|
|
589
|
+
|
|
590
|
+
// Expose to global game context
|
|
591
|
+
exposeTo: function (g) {
|
|
592
|
+
g.BLOCK_TYPES = BLOCK_TYPES;
|
|
593
|
+
g.updateVoxelWorld = updateChunks;
|
|
594
|
+
g.getVoxelBlock = getBlock;
|
|
595
|
+
g.setVoxelBlock = setBlock;
|
|
596
|
+
g.raycastVoxelBlock = raycastBlock;
|
|
597
|
+
g.checkVoxelCollision = checkCollision;
|
|
598
|
+
g.placeVoxelTree = placeTree;
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|