diff --git a/docs/plan.md b/docs/plan.md index f0e7f30..51b349d 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -42,7 +42,7 @@ | 20b | Тени без 3D: блоб-тени актёров + направленные тени tall-объектов по солнцу | ✅ сделано (2026-09-09): движок — `render/shadowSim.ts` (чистая математика солнца `sunShadow`/`blobPlacement`, 8 тестов) + `render/shadows.ts` (ступенчатая текстура блоба, раскладка); игра — `GameShadows` (ground-слой между картой и актёрами, блобы первого ребёнком вьюхи, чистка уничтоженных), сценарий `checks/shadows.mjs` (утро/полдень/вечер/ночь, скриншоты). Позже по желанию: контактный AO у подножий | | 21 | Перепаковка репо под две версии движка: v1 (черновик + демо) → `v1/` целиком, корень — диспетчер команд (`--prefix v1`); фиксы тихих поломок v1 (тени «сидят на базе», RT-тьма выводится на экран); скелет v2: `v2/` workspace, `packages/engine` (`@rpg/engine` 2.0.0-alpha, three.js), маниалы CLAUDE.md двух уровней | ✅ сделано (2026-09-09): см. «Уроки черновика» и «Направление v2» ниже; верификация — check:fast + agent:check из корня зелёные | | 22 | Спайк v2: воксельный рендер — чистая математика вокселей (сетка, culled-меш, классический AO 0..3), `VoxelRenderer` на three.js (вершинные цвета, изо-ортокамера, dirLight+shadowMap), low-res 480×270 + nearest-апскейл, `apps/demo` со сценкой, скриншот-гейт через агента с первого дня (день/вечер, тени ползут) | ✅ сделано (2026-09-09): движок — `voxel/grid.ts` + `voxel/mesher.ts` (12 тестов) + `render/voxelRenderer.ts`; тулзы — `tools/agent-lib.mjs` (каркас v2) + `png.mjs` (RGB-скриншоты Chromium); демка — сценка в палитре арт-библии, мост `__agent.setSun/stats`; гейт `apps/demo/tools/checks/render.mjs` зелёный (тени ползут: 76k пикселей разницы утро/вечер, апскейл 2×2 без нарушений, 67 цветов). Грабля спайка: минус в Y изо-направления = камера под землёй — снапшоты зелёные, увидел только скриншотом (урок №2 подтверждён в первый же день) | -| 23 | v2 трек 2: чанковый мир — `Chunk` (16×H×16), `VoxelWorld` (словарь чанков, get/set через границы, dirty-трекинг), `meshRegion` (мешер области по функции доступа — шов чанков бесшовный, AO через границы), демка на мире 3×3 чанков, мост `setVoxel`, гейт: воксель поставлен → кадр изменился, AO на шве не рвётся | ✅ сделано (2026-09-09): движок — `world/chunk.ts` + `world/world.ts` (правка кромки помечает соседа) + `world/worldMesher.ts`; мешер обобщён до `meshRegion` (meshVoxels — обёртка); тест шва — меш 4 чанков ≡ меш одной сеткой (поз+AO попиксельно), 20 тестов v2. Демка — мир 3×3 чанков с ремешингом dirty, мост `setVoxel`/`chunkStats` (правка атомарна с ремешингом). Гейт 8/8: в т.ч. столбик на шве x=16 меняет кадр (1112 пикселей). Урок счёта: интерфейсная грань двух вокселей — два квада (12−2=10, не 11) | +| 23 | v2 трек 2: чанковый мир — `Chunk` (16×H×16), `VoxelWorld` (словарь чанков, get/set через границы, dirty-трекинг), `meshRegion` (мешер области по функции доступа — шов чанков бесшовный, AO через границы), демка на мире 3×3 чанков, мост `setVoxel`, гейт: воксель поставлен → кадр изменился, AO на шве не рвётся | ✅ сделано (2026-09-09): движок — `world/chunk.ts` + `world/world.ts` (правка кромки помечает соседа) + `world/worldMesher.ts`; мешер обобщён до `meshRegion` (meshVoxels — обёртка); тест шва — меш 4 чанков ≡ меш одной сеткой (поз+AO попиксельно), 20 тестов v2. Демка — мир 6×6 чанков (96×96) с ремешингом dirty, мост `setVoxel`/`chunkStats`/`perf` (правка атомарна с ремешингом). Гейт 9/9: постройка 36 чанков + мешинг ~0.4–0.5 с (лимит 2 с), столбик на шве x=48 меняет кадр (~1000 пикселей). Урок счёта: интерфейсная грань двух вокселей — два квада (12−2=10, не 11); правка занятого вокселя лицом не меняет счётчики (тест-столбик дважды «исчезал»: вне кадра и внутри дома) | Отложено: **IsoLayout как параметр игры** — `DEFAULT_ISO` в `math/iso.ts` остаётся единственной точкой входа; прокидывание конфига игры через ~15 мест движка не даёт diff --git a/v2/CLAUDE.md b/v2/CLAUDE.md index a7e8dc4..ed7ae0a 100644 --- a/v2/CLAUDE.md +++ b/v2/CLAUDE.md @@ -60,11 +60,12 @@ - `tools/agent-lib.mjs` — браузерный каркас проверок (launchBrowser/ startDevServer/openGame/AgentClient/Checks), swiftshader в headless; - `tools/png.mjs` — PNG-кодек (colorType 6 и 2 — скриншоты Chromium RGB). -- `apps/demo` — демо-полигон: `src/main.ts` (мир 3×3 чанков «пепельный луг», - ремешинг по dirty-чанкам), `tools/checks/render.mjs` — скриншот-гейт: нет - pageerror, меш собран, тени ползут (утро/вечер), апскейл кратен 2×2, - многокрасочность, 9 чанков, `setVoxel` на шве меняет кадр. - Мост демки: `window.__agent.setSun(k)`/`stats()`/`setVoxel(x,y,z,c)`/`chunkStats()`. +- `apps/demo` — демо-полигон: `src/main.ts` (мир 6×6 чанков 96×96 «пепельный + луг», ремешинг по dirty-чанкам), `tools/checks/render.mjs` — скриншот-гейт: + нет pageerror, меш собран, тени ползут (утро/вечер), апскейл кратен 2×2, + многокрасочность, 36 чанков, постройка+мешинг < 2 с (замер ~0.5 с), + `setVoxel` на шве меняет кадр. + Мост демки: `window.__agent.setSun(k)`/`stats()`/`setVoxel(x,y,z,c)`/`chunkStats()`/`perf()`. ## Известные грабли (уже собранные) diff --git a/v2/apps/demo/src/main.ts b/v2/apps/demo/src/main.ts index 25e69d9..9a6590c 100644 --- a/v2/apps/demo/src/main.ts +++ b/v2/apps/demo/src/main.ts @@ -23,8 +23,8 @@ }, }; -const CHUNKS = 3; // мир 3×3 чанков -const S = CHUNKS * CHUNK_SX; // 48 — сторона площадки +const CHUNKS = 6; // мир 6×6 чанков (96×96) — демо масштаба движка +const S = CHUNKS * CHUNK_SX; // 96 — сторона площадки /** Сценка: земля, деревья, дом, валун, цветы — пишется прямо в мир. */ function buildScene(world: VoxelWorld): void { @@ -34,13 +34,17 @@ const patch = (x * 7 + z * 13) % 11 < 3; // детерминированные пятна world.set(x, 1, z, patch ? 3 : 1); } - // деревья: три обычных + одно на шве x=CHUNK_SX (проверка шва глазами) - for (const [tx, tz, h] of [[6, 8, 4], [24, 12, 5], [10, 25, 4], [CHUNK_SX, 20, 5]] as const) { + // деревья по площадке; (16,20) — ровно на шве x=CHUNK_SX (проверка шва глазами) + for (const [tx, tz, h] of [ + [6, 8, 4], [24, 12, 5], [10, 25, 4], [CHUNK_SX, 20, 5], + [52, 30, 4], [62, 54, 5], [40, 70, 4], [30, 60, 5], + [70, 40, 4], [86, 22, 5], [80, 80, 4], [50, 88, 5], + ] as const) { tree(world, tx, tz, h); } - house(world, 30, 6); - boulder(world, 22, 21); - for (const [fx, fz] of [[9, 17], [13, 21], [20, 16], [27, 9]] as const) { + house(world, 44, 44); + boulder(world, 52, 52); + for (const [fx, fz] of [[38, 50], [46, 56], [56, 48], [36, 38]] as const) { world.set(fx, 2, fz, 9); // цветок — единственный тёплый цвет } } @@ -85,6 +89,7 @@ } const world = new VoxelWorld(24); +const buildStart = performance.now(); buildScene(world); const canvas = document.getElementById('scene') as HTMLCanvasElement; @@ -106,29 +111,34 @@ } } remeshDirty(); +const buildMs = performance.now() - buildStart; // Агентный мост (DEV): солнце, статистика, правка мира атомарным ремешингом let sunK = 0.5; -(window as unknown as Record).__agent = { +function stats(): { vertices: number; triangles: number; sun: number } { + let vertices = 0, triangles = 0; + for (const m of chunkMeshes.values()) { + vertices += m.geometry.getAttribute('position').count; + triangles += (m.geometry.getIndex()?.count ?? 0) / 3; + } + return { vertices, triangles, sun: sunK }; +} +function chunkStats(): { chunks: number; meshes: number } { + return { chunks: world.chunkCount, meshes: chunkMeshes.size }; +} +const agent = { setSun(k: number): void { sunK = k; renderer.setSun(k); }, setVoxel(x: number, y: number, z: number, color: number): void { world.set(x, y, z, color); remeshDirty(); }, - stats(): { vertices: number; triangles: number; sun: number } { - let vertices = 0, triangles = 0; - const mesh = [...chunkMeshes.values()]; - for (const m of mesh) { - const count = m.geometry.getAttribute('position').count; - vertices += count; - triangles += (m.geometry.getIndex()?.count ?? 0) / 3; - } - return { vertices, triangles, sun: sunK }; - }, - chunkStats(): { chunks: number; meshes: number } { - return { chunks: world.chunkCount, meshes: chunkMeshes.size }; + stats, + chunkStats, + perf(): { chunks: number; meshes: number; buildMs: number; vertices: number; triangles: number } { + return { ...chunkStats(), buildMs, ...stats() }; }, }; +(window as unknown as Record).__agent = agent; function frame(): void { renderer.render(); diff --git a/v2/apps/demo/tools/checks/render.mjs b/v2/apps/demo/tools/checks/render.mjs index 891b09e..2ef917e 100644 --- a/v2/apps/demo/tools/checks/render.mjs +++ b/v2/apps/demo/tools/checks/render.mjs @@ -53,21 +53,23 @@ const colors = distinctColors(morning.rgba); check('картинка многокрасочная (палитра × AO × свет)', colors > 24, { colors }); - // Мир чанковый: земля 48×48 заполняет 3×3 чанков + // Мир чанковый: земля 96×96 заполняет 6×6 чанков; мешинг быстрых масштабов const cs = await client.call('chunkStats'); - check('мир из 9 чанков, меш у всех', cs.chunks === 9 && cs.meshes === 9, cs); + check('мир из 36 чанков, меш у всех', cs.chunks === 36 && cs.meshes === 36, cs); + const perf = await client.call('perf'); + check('постройка 36 чанков + мешинг < 2 с', perf.buildMs < 2000, perf); // Правка мира на шве чанков x=16: столбик в мир → кадр изменился await client.call('setSun', 0.5); const before = decodePng(readFileSync(await client.screenshot('/tmp/v2_render_before_edit.png'))); - await client.call('setVoxel', 15, 2, 10, 6); - await client.call('setVoxel', 16, 2, 10, 6); - await client.call('setVoxel', 16, 3, 10, 6); + await client.call('setVoxel', 47, 2, 30, 6); + await client.call('setVoxel', 48, 2, 30, 6); + await client.call('setVoxel', 48, 3, 30, 6); const after = decodePng(readFileSync(await client.screenshot('/tmp/v2_render_after_edit.png'))); const editDiff = countDiff(before.rgba, after.rgba); check('setVoxel на шве чанков меняет кадр', editDiff > 200, { editDiff }); const cs2 = await client.call('chunkStats'); - check('dirty-чанки после правки схлынули', cs2.chunks === 9 && cs2.meshes === 9, cs2); + check('dirty-чанки после правки схлынули', cs2.chunks === 36 && cs2.meshes === 36, cs2); report.ok = report.checks.every((c) => c.ok); } finally {