@vibes.diy/prompts 14.3.32 → 14.3.36
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/llms/backend.md +18 -2
- package/llms/three-js.md +7 -1
- package/llms/voxel.draft.md +244 -0
- package/llms/voxel.js +1 -0
- package/llms/voxel.js.map +1 -1
- package/llms/voxel.md +75 -28
- package/package.json +4 -4
package/llms/backend.md
CHANGED
|
@@ -384,8 +384,24 @@ const gh = await ctx.github.fetch("/repos/acme/site/issues", { method: "POST", b
|
|
|
384
384
|
calling convention (codegen has been observed passing these positionally).
|
|
385
385
|
The message lands in that person's bell, inbox, and push notifications.
|
|
386
386
|
- `toHandle` is one handle, and the platform delivers to people already in
|
|
387
|
-
this app: the owner, an accepted invite or approved
|
|
388
|
-
named in the access function's persisted grant state
|
|
387
|
+
this app: the owner, who always qualifies, an accepted invite or approved
|
|
388
|
+
request, a handle named in the access function's persisted grant state, or
|
|
389
|
+
somebody whose authenticated write reached the app (anonymous writes don't
|
|
390
|
+
count). Invite, request, and writer membership resolve the person through
|
|
391
|
+
their bound handle; the grant-state clause matches the named handle
|
|
392
|
+
itself. Explicit revocations deny anyone but the owner. A passing check
|
|
393
|
+
plus valid fields writes the notification into the inbox history of a
|
|
394
|
+
recipient the platform can resolve — an unresolvable handle fails the call
|
|
395
|
+
after the participation check. Their switches silence the live bell, the
|
|
396
|
+
push, and the email — not the history.
|
|
397
|
+
- When the notification is the opt-in kind — your turn, your order shipped —
|
|
398
|
+
seat the player the way a guest takes their own seat.
|
|
399
|
+
A turn notification goes to a seat its owner took themselves.
|
|
400
|
+
A join write where the added handle must equal `user.userHandle` qualifies
|
|
401
|
+
the writer by their own act. This is an app-side convention for notifiable
|
|
402
|
+
seats, not a rule about read grants: chosen-peer grants that share without
|
|
403
|
+
notifying — inviting a collaborator into a shared list — stay intentional,
|
|
404
|
+
and a grant-named handle stays eligible whether or not its owner seated it.
|
|
389
405
|
- `body` is the whole message — plain text, 500 chars max. The platform
|
|
390
406
|
titles it with the app's name and links it back to the app, so spend the
|
|
391
407
|
body on what happened and who it concerns.
|
package/llms/three-js.md
CHANGED
|
@@ -1000,7 +1000,13 @@ gltf.scene.traverse((child) => {
|
|
|
1000
1000
|
|
|
1001
1001
|
### Minecraft World Generator
|
|
1002
1002
|
|
|
1003
|
-
Procedural voxel terrain
|
|
1003
|
+
Procedural voxel terrain. **For a block world, the voxel skill is the
|
|
1004
|
+
authority**: it draws each chunk as one `InstancedMesh` of the exposed blocks
|
|
1005
|
+
(or Block Craft's per-face `BufferGeometry` batching) and never a `Mesh` per
|
|
1006
|
+
block — follow it rather than the merge sketch below, which is only the noise
|
|
1007
|
+
half. The same rule holds outside voxels: many things of one shape are one
|
|
1008
|
+
`InstancedMesh` or `Points`, never a `Mesh` each, and nothing allocates a
|
|
1009
|
+
geometry or material inside the frame loop.
|
|
1004
1010
|
|
|
1005
1011
|
```javascript
|
|
1006
1012
|
import { ImprovedNoise } from "three/addons/math/ImprovedNoise.js";
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Voxel worlds — the first draft: a viewport that plays
|
|
2
|
+
|
|
3
|
+
A voxel app's first draft is a full-screen 3D viewport with a block world you
|
|
4
|
+
can fly through and look around, drawn the fast way, with the HUD floating over
|
|
5
|
+
it. Placing and breaking blocks, walking with collision, endless terrain,
|
|
6
|
+
saving edits and sharing the world are the next pass. What the draft owes that
|
|
7
|
+
pass is the right skeleton: the world lives in a flat `Map`, the terrain is
|
|
8
|
+
drawn as one `InstancedMesh`, and the engine is built once inside an effect —
|
|
9
|
+
so the next pass adds to this file rather than rewriting it.
|
|
10
|
+
|
|
11
|
+
Unlike the page-shaped draft above, a world does not flow down the page: the
|
|
12
|
+
viewport is `fixed inset-0` and the controls sit on top of it.
|
|
13
|
+
|
|
14
|
+
### `App.jsx`
|
|
15
|
+
```jsx
|
|
16
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
17
|
+
import { useFireproof } from "use-fireproof";
|
|
18
|
+
import * as THREE from "three";
|
|
19
|
+
import { Button, Card, CardHeader, CardTitle, CardContent } from "@vibes.diy/look";
|
|
20
|
+
|
|
21
|
+
// The block-in-hand palette (shared by materials and HUD swatches).
|
|
22
|
+
const VOXEL_PALETTE = {
|
|
23
|
+
grass: "#6fcf5b", dirt: "#8a5a3b", stone: "#9b9b9b", wood: "#b5793a", glass: "#9fe3ff", candy: "#ff6fb1",
|
|
24
|
+
neon: "#39ff9e", water: "#2f8fe0", lava: "#ff5a2f", erase: "#ff4d6d", ink: "#1a1325", light: "#ffffff",
|
|
25
|
+
};
|
|
26
|
+
const HOTBAR = ["grass", "dirt", "stone", "wood"];
|
|
27
|
+
const GAME_KEYS = new Set(["KeyW", "KeyA", "KeyS", "KeyD", "KeyC", "Space", "ShiftLeft"]);
|
|
28
|
+
const SIZE = 24; // half-width of the starting island, in blocks
|
|
29
|
+
const SEED = 7;
|
|
30
|
+
|
|
31
|
+
const keyOf = (x, y, z) => `${x}|${y}|${z}`;
|
|
32
|
+
const heightAt = (x, z) => Math.round(Math.sin((x + SEED) * 0.18) * 2 + Math.cos((z - SEED) * 0.15) * 2 + Math.sin((x + z) * 0.06) * 3) + 5;
|
|
33
|
+
|
|
34
|
+
// The world model: every block, buried or not. Collision and picking read this, never the scene.
|
|
35
|
+
function buildBlocks() {
|
|
36
|
+
const blocks = new Map();
|
|
37
|
+
for (let x = -SIZE; x <= SIZE; x++)
|
|
38
|
+
for (let z = -SIZE; z <= SIZE; z++) {
|
|
39
|
+
const h = heightAt(x, z);
|
|
40
|
+
for (let y = 0; y <= h; y++) blocks.set(keyOf(x, y, z), y === h ? "grass" : y > h - 3 ? "dirt" : "stone");
|
|
41
|
+
}
|
|
42
|
+
return blocks;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const NB = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]];
|
|
46
|
+
const isHidden = (blocks, x, y, z) => NB.every(([dx, dy, dz]) => blocks.has(keyOf(x + dx, y + dy, z + dz)));
|
|
47
|
+
|
|
48
|
+
// One shared cube, one shared material, one draw call: the exposed blocks as instances with a color each.
|
|
49
|
+
const CUBE = new THREE.BoxGeometry(1, 1, 1);
|
|
50
|
+
const WORLD_MAT = new THREE.MeshLambertMaterial({ color: 0xffffff });
|
|
51
|
+
function buildWorldMesh(blocks) {
|
|
52
|
+
const visible = [];
|
|
53
|
+
for (const [k, type] of blocks) {
|
|
54
|
+
const [x, y, z] = k.split("|").map(Number);
|
|
55
|
+
if (!isHidden(blocks, x, y, z)) visible.push([x, y, z, type]);
|
|
56
|
+
}
|
|
57
|
+
const mesh = new THREE.InstancedMesh(CUBE, WORLD_MAT, visible.length);
|
|
58
|
+
const m = new THREE.Matrix4();
|
|
59
|
+
const c = new THREE.Color();
|
|
60
|
+
visible.forEach(([x, y, z, type], i) => {
|
|
61
|
+
mesh.setMatrixAt(i, m.makeTranslation(x + 0.5, y + 0.5, z + 0.5));
|
|
62
|
+
mesh.setColorAt(i, c.set(VOXEL_PALETTE[type]));
|
|
63
|
+
});
|
|
64
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
65
|
+
mesh.instanceColor.needsUpdate = true;
|
|
66
|
+
return mesh;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default function App() {
|
|
70
|
+
const { useLiveQuery } = useFireproof("skyIsland");
|
|
71
|
+
const { docs: worlds } = useLiveQuery("type", { key: "world" });
|
|
72
|
+
const world = worlds[0];
|
|
73
|
+
const mountRef = useRef(null);
|
|
74
|
+
const engineRef = useRef(null);
|
|
75
|
+
const [locked, setLocked] = useState(false);
|
|
76
|
+
const [held, setHeld] = useState(HOTBAR[0]);
|
|
77
|
+
|
|
78
|
+
// The engine, built once: renderer, scene, world mesh, input, loop. React only draws the HUD over it.
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const mount = mountRef.current;
|
|
81
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
82
|
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
83
|
+
renderer.setSize(mount.clientWidth, mount.clientHeight);
|
|
84
|
+
mount.appendChild(renderer.domElement);
|
|
85
|
+
|
|
86
|
+
const scene = new THREE.Scene();
|
|
87
|
+
scene.background = new THREE.Color(0x8ecbff);
|
|
88
|
+
scene.fog = new THREE.Fog(0x8ecbff, 40, 140);
|
|
89
|
+
const camera = new THREE.PerspectiveCamera(75, mount.clientWidth / mount.clientHeight, 0.1, 500);
|
|
90
|
+
scene.add(new THREE.HemisphereLight(0xffffff, 0x8a5a3b, 0.9));
|
|
91
|
+
const sun = new THREE.DirectionalLight(0xffffff, 1);
|
|
92
|
+
sun.position.set(30, 60, 20);
|
|
93
|
+
scene.add(sun);
|
|
94
|
+
|
|
95
|
+
const blocks = buildBlocks();
|
|
96
|
+
const worldMesh = buildWorldMesh(blocks);
|
|
97
|
+
scene.add(worldMesh);
|
|
98
|
+
|
|
99
|
+
const state = { pos: new THREE.Vector3(0, heightAt(0, 0) + 3, 8), yaw: 0, pitch: -0.2, keys: {}, running: true };
|
|
100
|
+
const canvas = renderer.domElement;
|
|
101
|
+
const onLock = () => setLocked(document.pointerLockElement === canvas);
|
|
102
|
+
const onMove = (e) => {
|
|
103
|
+
if (document.pointerLockElement !== canvas) return;
|
|
104
|
+
state.yaw -= e.movementX * 0.002;
|
|
105
|
+
state.pitch = Math.max(-1.5, Math.min(1.5, state.pitch - e.movementY * 0.002));
|
|
106
|
+
};
|
|
107
|
+
const onKeyDown = (e) => {
|
|
108
|
+
if (document.pointerLockElement !== canvas) return; // only while the world holds the mouse
|
|
109
|
+
if (e.target.isContentEditable || e.target.closest("input, textarea")) return; // typing stays typing
|
|
110
|
+
if (!GAME_KEYS.has(e.code)) return; // browser shortcuts stay browser shortcuts
|
|
111
|
+
state.keys[e.code] = true;
|
|
112
|
+
e.preventDefault(); // Space flies up, never scrolls the page
|
|
113
|
+
};
|
|
114
|
+
const onKeyUp = (e) => {
|
|
115
|
+
state.keys[e.code] = false;
|
|
116
|
+
};
|
|
117
|
+
const onBlur = () => { state.keys = {}; };
|
|
118
|
+
const onResize = () => {
|
|
119
|
+
renderer.setSize(mount.clientWidth, mount.clientHeight);
|
|
120
|
+
camera.aspect = mount.clientWidth / mount.clientHeight;
|
|
121
|
+
camera.updateProjectionMatrix();
|
|
122
|
+
};
|
|
123
|
+
document.addEventListener("pointerlockchange", onLock);
|
|
124
|
+
document.addEventListener("mousemove", onMove);
|
|
125
|
+
window.addEventListener("keydown", onKeyDown);
|
|
126
|
+
window.addEventListener("keyup", onKeyUp);
|
|
127
|
+
window.addEventListener("blur", onBlur);
|
|
128
|
+
window.addEventListener("resize", onResize);
|
|
129
|
+
engineRef.current = { requestLock: () => canvas.requestPointerLock() };
|
|
130
|
+
|
|
131
|
+
const clock = new THREE.Clock();
|
|
132
|
+
const forward = new THREE.Vector3();
|
|
133
|
+
const right = new THREE.Vector3();
|
|
134
|
+
let raf;
|
|
135
|
+
const loop = () => {
|
|
136
|
+
if (!state.running) return;
|
|
137
|
+
const dt = Math.min(clock.getDelta(), 0.05);
|
|
138
|
+
const speed = (state.keys.ShiftLeft ? 18 : 9) * dt;
|
|
139
|
+
forward.set(-Math.sin(state.yaw), 0, -Math.cos(state.yaw));
|
|
140
|
+
right.set(forward.z, 0, -forward.x);
|
|
141
|
+
if (state.keys.KeyW) state.pos.addScaledVector(forward, speed);
|
|
142
|
+
if (state.keys.KeyS) state.pos.addScaledVector(forward, -speed);
|
|
143
|
+
if (state.keys.KeyD) state.pos.addScaledVector(right, speed);
|
|
144
|
+
if (state.keys.KeyA) state.pos.addScaledVector(right, -speed);
|
|
145
|
+
if (state.keys.Space) state.pos.y += speed;
|
|
146
|
+
if (state.keys.KeyC) state.pos.y -= speed;
|
|
147
|
+
camera.position.copy(state.pos);
|
|
148
|
+
camera.rotation.set(state.pitch, state.yaw, 0, "YXZ");
|
|
149
|
+
renderer.render(scene, camera);
|
|
150
|
+
raf = requestAnimationFrame(loop);
|
|
151
|
+
};
|
|
152
|
+
loop();
|
|
153
|
+
|
|
154
|
+
return () => {
|
|
155
|
+
state.running = false;
|
|
156
|
+
cancelAnimationFrame(raf);
|
|
157
|
+
document.removeEventListener("pointerlockchange", onLock);
|
|
158
|
+
document.removeEventListener("mousemove", onMove);
|
|
159
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
160
|
+
window.removeEventListener("keyup", onKeyUp);
|
|
161
|
+
window.removeEventListener("blur", onBlur);
|
|
162
|
+
window.removeEventListener("resize", onResize);
|
|
163
|
+
worldMesh.dispose();
|
|
164
|
+
renderer.dispose();
|
|
165
|
+
mount.removeChild(canvas);
|
|
166
|
+
};
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className="fixed inset-0 select-none touch-none bg-background text-foreground font-sans">
|
|
171
|
+
<div ref={mountRef} className="absolute inset-0" />
|
|
172
|
+
|
|
173
|
+
<div className="absolute left-4 top-4 rounded-lg bg-card/90 px-3 py-2 text-card-foreground shadow-md">
|
|
174
|
+
<p className="font-bold">{world?.name ?? "Sky Island"}</p>
|
|
175
|
+
<p className="text-muted-foreground">WASD fly · Space up · C down · Shift fast</p>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 gap-2 rounded-lg bg-card/90 p-2 shadow-md">
|
|
179
|
+
{HOTBAR.map((type) => (
|
|
180
|
+
<Button key={type} size="sm" variant={type === held ? "default" : "outline"} onClick={() => setHeld(type)}
|
|
181
|
+
style={{ backgroundColor: type === held ? VOXEL_PALETTE[type] : undefined }}>
|
|
182
|
+
{type}
|
|
183
|
+
</Button>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
{/* TODO(next pass): place and break the held block with a voxel raycast, walk with gravity and collision, save edits as documents */}
|
|
187
|
+
|
|
188
|
+
{!locked && (
|
|
189
|
+
<button onClick={() => engineRef.current?.requestLock()} className="absolute inset-0 z-20 flex items-center justify-center bg-black/55">
|
|
190
|
+
<Card className="max-w-sm">
|
|
191
|
+
<CardHeader>
|
|
192
|
+
<CardTitle>{world?.name ?? "Sky Island"}</CardTitle>
|
|
193
|
+
</CardHeader>
|
|
194
|
+
<CardContent>
|
|
195
|
+
<p>Click to play</p>
|
|
196
|
+
<p className="mt-2 text-muted-foreground">Mouse looks around. Esc gives the mouse back.</p>
|
|
197
|
+
</CardContent>
|
|
198
|
+
</Card>
|
|
199
|
+
</button>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### `seed.json`
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"skyIsland": [
|
|
210
|
+
{ "key": "island", "type": "world", "name": "Sky Island", "sky": "clear" }
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Draft note: the next pass adds placing and breaking blocks, walking with gravity and collision, saved edits per player, and inviting friends into the world.
|
|
216
|
+
|
|
217
|
+
## What stays the same in a voxel draft
|
|
218
|
+
|
|
219
|
+
- **The viewport fills the screen**: the outer element is `fixed inset-0
|
|
220
|
+
select-none touch-none`, the renderer mounts into an `absolute inset-0` div,
|
|
221
|
+
and every control is an `absolute` layer over it. Never the page-flow `main`
|
|
222
|
+
with padding from the shape above.
|
|
223
|
+
- **The engine is built once** in `useEffect(() => {...}, [])` with the
|
|
224
|
+
renderer, scene, world mesh, listeners and loop inside it, `engineRef.current` for the few calls
|
|
225
|
+
React makes in, and a cleanup that stops the loop, removes every listener
|
|
226
|
+
and disposes the mesh and renderer. Per-frame values live in the `state`
|
|
227
|
+
object, never in React state.
|
|
228
|
+
- **`setPixelRatio(Math.min(window.devicePixelRatio, 2))`**, a resize handler
|
|
229
|
+
that updates the renderer size and the camera aspect, and a `Fog` that
|
|
230
|
+
matches the sky color.
|
|
231
|
+
- **The world is a flat `Map` keyed `"x|y|z"`**, and it is drawn as ONE
|
|
232
|
+
`InstancedMesh` of the exposed blocks with one shared `BoxGeometry` and one
|
|
233
|
+
shared material, the color per instance. Never a `THREE.Mesh` per block, and
|
|
234
|
+
never a geometry or material created inside the block loop.
|
|
235
|
+
- **Pointer lock comes from a "Click to play" overlay** whose visibility follows
|
|
236
|
+
the real `pointerlockchange` state, so it returns when the player presses
|
|
237
|
+
Esc. Mouse look clamps pitch to ±1.5 rad. A keydown calls `preventDefault`
|
|
238
|
+
only when the canvas holds the lock, the target is not a text field, and the
|
|
239
|
+
key is in the game's own set; held keys clear on blur.
|
|
240
|
+
- **The palette table** is the Blocktopia voxel palette, in full, named once
|
|
241
|
+
for both the instance colors and the HUD swatches.
|
|
242
|
+
- **What waits for the next pass** is named in the `TODO(next pass)` comment
|
|
243
|
+
and the draft note: placing and breaking, collision and gravity, endless
|
|
244
|
+
terrain, saving edits, sharing. The draft draws none of it.
|
package/llms/voxel.js
CHANGED
|
@@ -13,6 +13,7 @@ export const voxelConfig = {
|
|
|
13
13
|
"minecraft, voxel, block world, sandbox, blocks, mining, building, first-person, 3d game, " +
|
|
14
14
|
"procedural world, chunks, terrain, overworld, creative mode, survival, crafting, nether",
|
|
15
15
|
cues: ["voxel", "voxels", "minecraft", "block world", "blocky", "sandbox game"],
|
|
16
|
+
draftVariant: true,
|
|
16
17
|
importModule: "three",
|
|
17
18
|
importName: "THREE",
|
|
18
19
|
importType: "namespace",
|
package/llms/voxel.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voxel.js","sourceRoot":"","sources":["../../jsr/llms/voxel.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,WAAW,GAAc;IACpC,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,OAAO;IACd,WAAW,EACT,yFAAyF;QACzF,6FAA6F;QAC7F,4FAA4F;QAC5F,yFAAyF;QACzF,uFAAuF;QACvF,gGAAgG;QAChG,2FAA2F;QAC3F,yFAAyF;QACzF,6CAA6C;QAC7C,2FAA2F;QAC3F,yFAAyF;IAC3F,IAAI,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"voxel.js","sourceRoot":"","sources":["../../jsr/llms/voxel.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,WAAW,GAAc;IACpC,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,OAAO;IACd,WAAW,EACT,yFAAyF;QACzF,6FAA6F;QAC7F,4FAA4F;QAC5F,yFAAyF;QACzF,uFAAuF;QACvF,gGAAgG;QAChG,2FAA2F;QAC3F,yFAAyF;QACzF,6CAA6C;QAC7C,2FAA2F;QAC3F,yFAAyF;IAC3F,IAAI,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,CAAC;IAG/E,YAAY,EAAE,IAAI;IAClB,YAAY,EAAE,OAAO;IACrB,UAAU,EAAE,OAAO;IACnB,UAAU,EAAE,WAAW;CACxB,CAAC"}
|
package/llms/voxel.md
CHANGED
|
@@ -13,9 +13,10 @@ veins, endless chunk streaming, day/night, live multiplayer edits, ambient
|
|
|
13
13
|
wildlife — and its bigger successor **Blockcraft World**, which adds the full
|
|
14
14
|
survival layer: crafting tiers, tool durability, smelting, armor, mob combat,
|
|
15
15
|
hunger, creative mode, nether/end dimensions, per-player inventory, and a
|
|
16
|
-
shared world reset. **Blocktopia** contributes
|
|
17
|
-
compact procedural island settings, and sparse
|
|
18
|
-
contributes exposed-face chunk meshes and
|
|
16
|
+
shared world reset. **Blocktopia** contributes the instanced chunk renderer,
|
|
17
|
+
material-colored controls, compact procedural island settings, and sparse
|
|
18
|
+
per-cell edits; **Block Craft** contributes exposed-face chunk meshes and
|
|
19
|
+
survival feedback. Adapt the useful
|
|
19
20
|
patterns to the persistence contracts below: whole-world autosaves and durable
|
|
20
21
|
movement heartbeats are expensive examples to improve, rather than defaults.
|
|
21
22
|
|
|
@@ -112,7 +113,8 @@ const state = {
|
|
|
112
113
|
blocks: new Map(), // "x|y|z" -> type (the loaded world, terrain + edits)
|
|
113
114
|
chunks: new Map(), // "cx,cz" -> Set(blockKey) currently-resident chunks
|
|
114
115
|
edits: new Map(), // "x|y|z" -> type|null player edits layered over terrain
|
|
115
|
-
|
|
116
|
+
chunkMeshes: new Map(), // "cx,cz" -> THREE.InstancedMesh (EXPOSED blocks only, one per chunk)
|
|
117
|
+
dirty: new Set(), // "cx,cz" chunks whose mesh must be rebuilt after an edit
|
|
116
118
|
pos: new THREE.Vector3(0, 20, 0),
|
|
117
119
|
vel: new THREE.Vector3(),
|
|
118
120
|
yaw: 0, pitch: 0, keys: {}, onGround: false, fly: false,
|
|
@@ -196,12 +198,17 @@ outline: the accent reinforces the selected mode rather than being its only
|
|
|
196
198
|
signal. On touch, keep movement/look separate from Place/Break/Jump controls,
|
|
197
199
|
and clear held input on pointer cancellation or lost capture.
|
|
198
200
|
|
|
199
|
-
## Rendering:
|
|
201
|
+
## Rendering: instance the EXPOSED blocks (this is the whole performance game)
|
|
200
202
|
|
|
201
203
|
The single most important rule: **do not create a `THREE.Mesh` per stored
|
|
202
|
-
block
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
block, and never allocate a geometry or material inside the block loop.** A
|
|
205
|
+
chunk is thousands of blocks and most are buried. Three.js pays one draw call
|
|
206
|
+
per mesh per material, so a mesh-per-block world with a six-material cube is
|
|
207
|
+
hundreds of thousands of draw calls a frame — CPU-bound, and a faster GPU does
|
|
208
|
+
not help. Keep buried blocks in the `blocks` map (collision and raycasting read
|
|
209
|
+
the map, not the scene) and draw the rest as **one `InstancedMesh` per chunk**.
|
|
210
|
+
|
|
211
|
+
First, the exposure test — a block with six occluding neighbors is never drawn:
|
|
205
212
|
|
|
206
213
|
```js
|
|
207
214
|
const NB = [[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]];
|
|
@@ -209,26 +216,62 @@ function occludes(t) { return t && t !== "water" && t !== "glass" && t !== "leav
|
|
|
209
216
|
function isHidden(x, y, z) {
|
|
210
217
|
for (const [dx, dy, dz] of NB)
|
|
211
218
|
if (!occludes(state.blocks.get(keyOf(x + dx, y + dy, z + dz)))) return false;
|
|
212
|
-
return true; // fully enclosed → no
|
|
219
|
+
return true; // fully enclosed → no instance
|
|
213
220
|
}
|
|
214
221
|
```
|
|
215
222
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
223
|
+
Then the chunk mesh: one shared `BoxGeometry` and one shared material for the
|
|
224
|
+
whole world, built once outside every loop, with the block color carried by
|
|
225
|
+
`instanceColor`. This is Blocktopia's terrain — the whole island is one draw
|
|
226
|
+
call, and the build is a `setMatrixAt` loop rather than vertex arithmetic:
|
|
220
227
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
228
|
+
```js
|
|
229
|
+
const CUBE = new THREE.BoxGeometry(1, 1, 1); // built ONCE
|
|
230
|
+
const TERRAIN_MAT = new THREE.MeshLambertMaterial({ color: 0xffffff }); // built ONCE; white so instanceColor shows
|
|
231
|
+
const tmpM = new THREE.Matrix4(), tmpC = new THREE.Color();
|
|
232
|
+
|
|
233
|
+
function buildChunkMesh(keys) { // keys: the chunk's resident block keys
|
|
234
|
+
const visible = [];
|
|
235
|
+
for (const k of keys) { const [x, y, z] = parseKey(k); if (!isHidden(x, y, z)) visible.push([x, y, z, state.blocks.get(k)]); }
|
|
236
|
+
const mesh = new THREE.InstancedMesh(CUBE, TERRAIN_MAT, visible.length);
|
|
237
|
+
visible.forEach(([x, y, z, type], i) => {
|
|
238
|
+
mesh.setMatrixAt(i, tmpM.makeTranslation(x + 0.5, y + 0.5, z + 0.5));
|
|
239
|
+
mesh.setColorAt(i, tmpC.setHex(BLOCK_TYPES[type].top));
|
|
240
|
+
});
|
|
241
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
242
|
+
mesh.instanceColor.needsUpdate = true;
|
|
243
|
+
mesh.frustumCulled = false; // a chunk's bounds are its blocks; let the fog bound distance
|
|
244
|
+
return mesh;
|
|
245
|
+
}
|
|
246
|
+
```
|
|
224
247
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
248
|
+
Blocks whose *material* differs in kind — transparent water and glass, an
|
|
249
|
+
emissive lava — get their own `InstancedMesh` per type sharing the same `CUBE`,
|
|
250
|
+
so the count of draw calls per chunk stays "one plus the number of special
|
|
251
|
+
materials present". Never mint a material per block; the material table is a
|
|
252
|
+
module-level `Object` keyed by type, built once.
|
|
253
|
+
|
|
254
|
+
**Edits rebuild the chunk, not the block.** When a block is placed or broken,
|
|
255
|
+
mark its chunk dirty — and the neighboring chunk too when the block sits on a
|
|
256
|
+
chunk boundary, because a face across the border may have just become exposed
|
|
257
|
+
or buried. Each frame, rebuild at most one or two dirty chunks: `scene.remove`
|
|
258
|
+
the old `InstancedMesh`, `dispose()` it, and add the fresh one. Rebuilding a
|
|
259
|
+
16×16 chunk is a few thousand `setMatrixAt` calls, well under a frame. A
|
|
260
|
+
just-placed block can pop in by starting its instance matrix at scale 0 and
|
|
261
|
+
easing it to 1 over a few frames.
|
|
262
|
+
|
|
263
|
+
Raycasting and collision must read `state.blocks` (the full model), never the
|
|
264
|
+
meshes or the instance buffers — otherwise you can't hit or collide with a face
|
|
265
|
+
that isn't currently drawn.
|
|
266
|
+
|
|
267
|
+
For texture-heavy or very large worlds, use Block Craft's **exposed-face chunk
|
|
268
|
+
batching** instead: append only the visible *faces* into a `BufferGeometry` per
|
|
269
|
+
material per chunk (position, normal, uv and an index per quad), sharing
|
|
270
|
+
materials across chunks. It uploads less than instancing — a buried face is
|
|
271
|
+
never sent at all — and allows a different texture per face, at the cost of
|
|
272
|
+
writing the six quad windings by hand. The dirty-chunk rule is identical.
|
|
273
|
+
Either of these two patterns is correct; a `Mesh` per block is not, even with
|
|
274
|
+
the exposure test in front of it.
|
|
232
275
|
|
|
233
276
|
## Procedural terrain: deterministic noise, layered
|
|
234
277
|
|
|
@@ -274,8 +317,9 @@ function loadChunk(cx, cz) {
|
|
|
274
317
|
if (t === null) { state.blocks.delete(k); keys.delete(k); }
|
|
275
318
|
else { state.blocks.set(k, t); keys.add(k); }
|
|
276
319
|
}
|
|
277
|
-
for (const k of keys) { const [x,y,z] = parseKey(k); if (!isHidden(x,y,z)) addMesh(k, state.blocks.get(k)); }
|
|
278
320
|
state.chunks.set(chunkKey(cx, cz), keys);
|
|
321
|
+
const mesh = buildChunkMesh(keys); // one InstancedMesh for the chunk (see Rendering)
|
|
322
|
+
scene.add(mesh); state.chunkMeshes.set(chunkKey(cx, cz), mesh);
|
|
279
323
|
}
|
|
280
324
|
```
|
|
281
325
|
|
|
@@ -298,7 +342,7 @@ Each frame, figure out which chunks should be resident around the player
|
|
|
298
342
|
(`VIEW_RADIUS` chunks in each direction), enqueue the missing ones sorted
|
|
299
343
|
nearest-first, and load **exactly one per frame** so the framerate never
|
|
300
344
|
stalls. Unload chunks that fall outside the radius (delete their blocks and
|
|
301
|
-
meshes). Never load a whole ring in one frame.
|
|
345
|
+
instanced meshes). Never load a whole ring in one frame.
|
|
302
346
|
|
|
303
347
|
## The player controller — where "Minecraft" is won or lost
|
|
304
348
|
|
|
@@ -1037,8 +1081,11 @@ None of this needs persistence — keep it purely visual and client-side:
|
|
|
1037
1081
|
|
|
1038
1082
|
## Performance checklist
|
|
1039
1083
|
|
|
1040
|
-
-
|
|
1041
|
-
|
|
1084
|
+
- One `InstancedMesh` per chunk of exposed blocks (or Block Craft face
|
|
1085
|
+
batching); keep buried ones in the map for collision/raycast.
|
|
1086
|
+
- Share one geometry + one material table across all blocks, built once
|
|
1087
|
+
outside every loop; never a `Mesh`, geometry or material per block.
|
|
1088
|
+
- Edits dirty a chunk; rebuild at most a chunk or two per frame.
|
|
1042
1089
|
- Stream one chunk per frame; unload out-of-range chunks (dispose meshes).
|
|
1043
1090
|
- Raycast and collide against the block map, never the scene meshes.
|
|
1044
1091
|
- Cap mobs and particles; dispose geometries/materials on unmount and on
|
|
@@ -1046,7 +1093,7 @@ None of this needs persistence — keep it purely visual and client-side:
|
|
|
1046
1093
|
- `renderer.setPixelRatio(Math.min(devicePixelRatio, 2))` and a `Fog` to bound
|
|
1047
1094
|
draw distance.
|
|
1048
1095
|
|
|
1049
|
-
That set — exposed-
|
|
1096
|
+
That set — instanced exposed-block chunks, per-axis collision with step-up and un-stick,
|
|
1050
1097
|
a click-to-play pointer-lock overlay, a DDA raycast, per-voxel Fireproof docs
|
|
1051
1098
|
folded forward monotonically, table-driven survival systems, and a flat
|
|
1052
1099
|
multi-file split once it grows — is what turns a voxel tech demo into a game
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibes.diy/prompts",
|
|
3
|
-
"version": "14.3.
|
|
3
|
+
"version": "14.3.36",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"exports": {
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"license": "Apache-2.0",
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@adviser/cement": "~0.5.34",
|
|
40
|
-
"@vibes.diy/call-ai-v2": "14.3.
|
|
41
|
-
"@vibes.diy/identity": "14.3.
|
|
42
|
-
"@vibes.diy/use-vibes-types": "14.3.
|
|
40
|
+
"@vibes.diy/call-ai-v2": "14.3.36",
|
|
41
|
+
"@vibes.diy/identity": "14.3.36",
|
|
42
|
+
"@vibes.diy/use-vibes-types": "14.3.36",
|
|
43
43
|
"arktype": "~2.2.3",
|
|
44
44
|
"json-schema-faker": "~0.6.3"
|
|
45
45
|
},
|