From 8c8ac893f4b67ef641a05694d859f05ed8394241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Fri, 14 Aug 2026 21:02:30 +0800 Subject: [PATCH 01/21] fix(devtools): correct Squirrel stack frame level for call stack and locals The Squirrel native debug hook is invoked as a direct C call, so the top of the call stack (level 0) is the current script frame. The debugger, DAP adapter, and DevTool read locals/stack at level 1, which skipped the current frame: stackTrace omitted the innermost frame and the variables panel showed the caller's locals instead. Read stack frames/locals at level 0 across Debugger::stackTrace, Debugger::locals, Debugger::evaluate, Debugger::scriptStackDepth, DebugAdapter variables requests, and DevTool local sampling. Adds debugger_audit.cpp (breakpoint hits, stepInto/stepOver/stepOut, call stack at nested breakpoint, locals at breakpoint) and a threaded DAP end-to-end test that runs a real script and verifies the stopped event and the full call stack. --- src/engine/devtools/DebugAdapter.cpp | 2 +- src/engine/devtools/Debugger.cpp | 8 +- src/engine/devtools/Debugger.hpp | 3 +- src/engine/devtools/DevTool.cpp | 10 +- test/CMakeLists.txt | 1 + test/dap.cpp | 124 +++++++++++ test/debugger_audit.cpp | 299 +++++++++++++++++++++++++++ 7 files changed, 436 insertions(+), 11 deletions(-) create mode 100644 test/debugger_audit.cpp diff --git a/src/engine/devtools/DebugAdapter.cpp b/src/engine/devtools/DebugAdapter.cpp index 2c6b5fa1..decad5a8 100644 --- a/src/engine/devtools/DebugAdapter.cpp +++ b/src/engine/devtools/DebugAdapter.cpp @@ -525,7 +525,7 @@ void DebugAdapter::handleRequest(const std::string& json) { const int ref = args ? args->optValue("variablesReference", 0) : 0; Poco::JSON::Array::Ptr arr = new Poco::JSON::Array(); if (ref == varRefLocals_) { - for (const auto& v : dbg.locals(1)) { + for (const auto& v : dbg.locals(0)) { Poco::JSON::Object::Ptr o = new Poco::JSON::Object(); o->set("name", v.name); o->set("value", v.value); diff --git a/src/engine/devtools/Debugger.cpp b/src/engine/devtools/Debugger.cpp index 9d0186f0..46b011f5 100644 --- a/src/engine/devtools/Debugger.cpp +++ b/src/engine/devtools/Debugger.cpp @@ -182,7 +182,7 @@ int Debugger::scriptStackDepth() const { HSQUIRRELVM vm = vm_; if (!vm) return 0; int depth = 0; - for (int level = 1;; ++level) { + for (int level = 0;; ++level) { SQStackInfos si; if (SQ_FAILED(sq_stackinfos(vm, level, &si))) break; ++depth; @@ -481,7 +481,7 @@ VariableInfo Debugger::evaluate(const std::string& expression) const { } // Prefer local, then roottable slot. Full expression eval is intentionally // limited (safe for watches of variable names / dotted root paths). - auto local = readLocal(vm, 1, expression); + auto local = readLocal(vm, 0, expression); if (local.type != "error") return local; // Support a.b root path (tables only). @@ -522,7 +522,7 @@ std::vector Debugger::locals(int stackLevel) const { std::vector out; HSQUIRRELVM vm = vm_; if (!vm) return out; - const SQUnsignedInteger level = static_cast(stackLevel < 0 ? 1 : stackLevel); + const SQUnsignedInteger level = static_cast(stackLevel < 0 ? 0 : stackLevel); for (SQUnsignedInteger n = 0;; ++n) { const SQInteger top = sq_gettop(vm); const SQChar* name = sq_getlocal(vm, level, n); @@ -545,7 +545,7 @@ std::vector Debugger::stackTrace(int maxFrames) const { HSQUIRRELVM vm = vm_; if (!vm) return out; if (maxFrames <= 0) maxFrames = 32; - for (int level = 1; level <= maxFrames; ++level) { + for (int level = 0; level <= maxFrames; ++level) { SQStackInfos si; if (SQ_FAILED(sq_stackinfos(vm, level, &si))) break; StackFrameInfo f; diff --git a/src/engine/devtools/Debugger.hpp b/src/engine/devtools/Debugger.hpp index a65816a8..4fdf115e 100644 --- a/src/engine/devtools/Debugger.hpp +++ b/src/engine/devtools/Debugger.hpp @@ -135,7 +135,8 @@ public: void refreshWatches(); /** Evaluate a single expression (local name, or roottable slot). */ VariableInfo evaluate(const std::string& expression) const; - std::vector locals(int stackLevel = 1) const; + /** Locals of the given call-stack level (0 = current script frame). */ + std::vector locals(int stackLevel = 0) const; std::vector stackTrace(int maxFrames = 32) const; using PumpFn = std::function; diff --git a/src/engine/devtools/DevTool.cpp b/src/engine/devtools/DevTool.cpp index 43b407be..cf504444 100644 --- a/src/engine/devtools/DevTool.cpp +++ b/src/engine/devtools/DevTool.cpp @@ -393,10 +393,10 @@ void DevTool::sampleFrameLocals(HSQUIRRELVM vm, const SourceLoc& loc) { auto& prev = localSnap_[depth]; std::unordered_map cur; - // Level 0 = current function (hook runs inside the debug callback frame?); - // Squirrel docs: level is the call stack level — 0 is top (current). - // When the native hook fires, level 1 is typically the script frame of interest. - const SQUnsignedInteger level = 1; + // Level 0 = current script frame. The native debug hook is invoked as a + // direct C call (no CallInfo pushed), so the top of the Squirrel call + // stack is the script frame that triggered the line event. + const SQUnsignedInteger level = 0; for (SQUnsignedInteger n = 0;; ++n) { const SQInteger top = sq_gettop(vm); const SQChar* name = sq_getlocal(vm, level, n); @@ -489,7 +489,7 @@ void DevTool::markErrorUses(const SourceLoc& loc, } graph_.onLine(site); - const SQUnsignedInteger level = 1; + const SQUnsignedInteger level = 0; for (SQUnsignedInteger n = 0;; ++n) { const SQInteger top = sq_gettop(vm_); const SQChar* name = sq_getlocal(vm_, level, n); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1e494cb2..96eabd1c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -94,6 +94,7 @@ file(GLOB all_test_cpp callgraph.cpp renderflow.cpp debugger.cpp + debugger_audit.cpp dap.cpp mcp.cpp main.cpp diff --git a/test/dap.cpp b/test/dap.cpp index b0d390c5..780e1076 100644 --- a/test/dap.cpp +++ b/test/dap.cpp @@ -18,7 +18,9 @@ #include #include +#include #include +#include #include #include #include @@ -455,3 +457,125 @@ TEST_CASE("devtools.dap.scriptBreakpointHitViaHook") { dt.detach(); dap.stop(); } + +TEST_CASE("devtools.dap.realScriptBreakpointAndCallStack") { + auto& dap = DebugAdapter::instance(); + auto& dbg = Debugger::instance(); + auto& dt = DevTool::instance(); + + dap.stop(); + dt.detach(); + dbg.clearBreakpoints(); + + const char* src = + "function inner() {\n" // line 1 + " local a = 1\n" // line 2 + " local b = a + 1\n" // line 3 <-- breakpoint + "}\n" + "function outer() {\n" + " inner()\n" + "}\n" + "outer()\n"; + + std::atomic attached{false}; + std::atomic go{false}; + std::atomic scriptDone{false}; + + // Run the script on its own thread. DevTool must be attached HERE because + // g_active is thread_local and the Squirrel debug hook fires on this thread. + // DevTool::attach() internally calls detach()->stopDap(), so the DAP server + // must NOT be listening yet — the main thread waits for `attached` first. + std::thread scriptThread([&] { + ssq::VM vm(1024, ssq::Libs::ALL); + HSQUIRRELVM v = vm.getHandle(); + dt.attach(v, /*sampleLocals=*/false); + attached = true; + while (!go.load()) std::this_thread::sleep_for(std::chrono::milliseconds(2)); + const SQInteger len = static_cast(std::strlen(src)); + SQRESULT rc = sq_compilebuffer(v, src, len, _SC("e2e.nut"), SQTrue); + REQUIRE(SQ_SUCCEEDED(rc)); + sq_pushroottable(v); + REQUIRE(SQ_SUCCEEDED(sq_call(v, 1, SQFalse, SQTrue))); + sq_poptop(v); + dt.detach(); + scriptDone = true; + }); + + // Wait until DevTool is attached (and DAP stopped), then start DAP + handshake. + while (!attached.load()) std::this_thread::sleep_for(std::chrono::milliseconds(2)); + + const int port = dap.listen(0); + REQUIRE(port > 0); + + DapClient client(port); + pump(20); + client.sendRequest("initialize", "{\"adapterID\":\"eve\"}"); + REQUIRE(client.expectResponse("initialize")); + client.sendRequest("launch", "{\"program\":\".\"}"); + REQUIRE(client.expectResponse("launch")); + client.sendRequest("setBreakpoints", + "{\"source\":{\"path\":\"e2e.nut\",\"name\":\"e2e.nut\"}," + "\"breakpoints\":[{\"line\":3}]}"); + REQUIRE(client.expectResponse("setBreakpoints")); + client.sendRequest("configurationDone"); + REQUIRE(client.expectResponse("configurationDone")); + + // Release the script now that breakpoints are installed. + client.clearEvents(); + go = true; + + // The breakpoint hit produces a `stopped` event with the real location. + auto stopped = client.expectEvent("stopped", 8000); + REQUIRE(stopped); + { + auto body = stopped->getObject("body"); + REQUIRE(body); + CHECK_EQ(body->optValue("reason", ""), std::string("breakpoint")); + CHECK_EQ(body->optValue("line", 0), 3); + } + + // The call stack must include the CURRENT (innermost) frame `inner` first. + client.sendRequest("stackTrace", "{\"threadId\":1,\"startFrame\":0,\"levels\":20}"); + auto stResp = client.expectResponse("stackTrace"); + REQUIRE(stResp); + { + auto body = stResp->getObject("body"); + REQUIRE(body); + auto frames = body->getArray("stackFrames"); + REQUIRE(frames); + REQUIRE(frames->size() >= 3u); + auto f0 = frames->getObject(0); + REQUIRE(f0); + CHECK_EQ(f0->optValue("line", 0), 3); + CHECK(f0->optValue("name", "").find("inner") != std::string::npos); + auto f1 = frames->getObject(1); + REQUIRE(f1); + CHECK(f1->optValue("name", "").find("outer") != std::string::npos); + auto f2 = frames->getObject(2); + REQUIRE(f2); + CHECK(f2->optValue("name", "").find("main") != std::string::npos); + } + + // F5 continue from VS Code → script finishes. + client.sendRequest("continue", "{\"threadId\":1}"); + REQUIRE(client.expectResponse("continue")); + + // Poll-based join: if a previous CHECK/REQUIRE failed the script may never + // finish, so never block on an unconditional join (would hang the suite). + for (int i = 0; i < 200 && !scriptDone.load(); ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + dap.poll(); + } + if (scriptThread.joinable()) { + if (scriptDone.load()) { + scriptThread.join(); + } else { + scriptThread.detach(); // test already failed; avoid std::terminate + } + } + const bool done = scriptDone.load(); + CHECK(done); + CHECK(!dbg.isPaused()); + + dap.stop(); +} diff --git a/test/debugger_audit.cpp b/test/debugger_audit.cpp new file mode 100644 index 00000000..b22d53f1 --- /dev/null +++ b/test/debugger_audit.cpp @@ -0,0 +1,299 @@ +#include "zeroerr/assert.h" +#include "zeroerr/unittest.h" + +#include "devtools/Debugger.hpp" + +#include +#include + +#include +#include +#include +#include + +using namespace eve::dev; + +namespace { + +struct AuditDriver { + Debugger* dbg = nullptr; + int stops = 0; + std::vector stopLines; + std::vector stopFuncs; + std::vector stopDepths; + std::vector> actions; + std::function capture; // optional: capture extra state on each stop +}; + +void auditHook(HSQUIRRELVM v, SQInteger type, const SQChar* sourcename, SQInteger line, + const SQChar* funcname) { + auto* st = static_cast(sq_getforeignptr(v)); + if (!st || !st->dbg) return; + if (type != 'l') return; + SourceLoc loc; + loc.source = sourcename ? sourcename : ""; + loc.line = static_cast(line); + loc.function = funcname ? funcname : ""; + if (st->dbg->onScriptLine(loc)) { + const int idx = st->stops; + st->stopLines.push_back(loc.line); + st->stopFuncs.push_back(loc.function); + st->stopDepths.push_back(st->dbg->scriptStackDepth()); + if (st->capture) st->capture(); + if (idx < static_cast(st->actions.size())) { + st->actions[idx](); + } else { + st->dbg->resume(); + } + ++st->stops; + } +} + +/** Compile+run a buffer through the Debugger, driving it with `driver`. */ +void runAudit(const char* src, const char* bufname, AuditDriver& driver) { + ssq::VM vm(1024, ssq::Libs::ALL); + HSQUIRRELVM v = vm.getHandle(); + + Debugger& d = Debugger::instance(); + d.detach(); + driver.dbg = &d; + d.attach(v); + + sq_setforeignptr(v, &driver); + sq_enabledebuginfo(v, SQTrue); + sq_setnativedebughook(v, auditHook); + + SQRESULT rc = sq_compilebuffer(v, src, static_cast(std::strlen(src)), _SC(bufname), + SQTrue); + REQUIRE(SQ_SUCCEEDED(rc)); + sq_pushroottable(v); + REQUIRE(SQ_SUCCEEDED(sq_call(v, 1, SQFalse, SQTrue))); + sq_poptop(v); + + sq_setnativedebughook(v, nullptr); + sq_setforeignptr(v, nullptr); + d.detach(); + driver.dbg = nullptr; +} + +} // namespace + +// --------------------------------------------------------------------------- +// Call stack retrieval at a breakpoint (nested calls) +// --------------------------------------------------------------------------- + +TEST_CASE("devtools.audit.callStackAtNestedBreakpoint") { + const char* src = + "function inner() {\n" + " local a = 1\n" + "}\n" + "function outer() {\n" + " inner()\n" + "}\n" + "outer()\n"; + + AuditDriver driver; + driver.actions = {[]() { Debugger::instance().resume(); }}; + + std::vector captured; + driver.capture = [&]() { captured = Debugger::instance().stackTrace(16); }; + + Debugger& d = Debugger::instance(); + d.clearBreakpoints(); + d.setBreakpoint("audit1.nut", 2); + runAudit(src, "audit1.nut", driver); + + REQUIRE(driver.stops >= 1); + REQUIRE(captured.size() >= 3u); + CHECK_EQ(captured[0].loc.line, 2); + CHECK(captured[0].name.find("inner") != std::string::npos); + CHECK_EQ(captured[0].loc.source, std::string("audit1.nut")); + CHECK(captured[1].name.find("outer") != std::string::npos); + CHECK_EQ(captured[1].loc.line, 5); + // Top-level script frame present. + CHECK(!captured[2].loc.source.empty()); +} + +// --------------------------------------------------------------------------- +// stepOut: finish current function, stop in the caller after the call +// --------------------------------------------------------------------------- + +TEST_CASE("devtools.audit.stepOutReturnsToCaller") { + const char* src = + "function callee() {\n" + " local a = 1\n" + " local b = 2\n" + "}\n" + "function caller() {\n" + " callee()\n" + " local c = 3\n" + "}\n" + "caller()\n"; + + AuditDriver driver; + // Stop 1 (callee body) -> stepOut. Stop 2 (caller after call) -> resume. + driver.actions = { + []() { Debugger::instance().stepOut(); }, + []() { Debugger::instance().resume(); }, + }; + + Debugger& d = Debugger::instance(); + d.clearBreakpoints(); + d.setBreakpoint("audit2.nut", 2); + runAudit(src, "audit2.nut", driver); + + REQUIRE(driver.stops >= 2); + CHECK_EQ(driver.stopLines[0], 2); + CHECK(driver.stopFuncs[0].find("callee") != std::string::npos); + // After stepOut we should be back in the caller, past the call site. + CHECK(driver.stopFuncs[1].find("caller") != std::string::npos); + CHECK_EQ(driver.stopLines[1], 7); +} + +// --------------------------------------------------------------------------- +// stepInto: enter the callee and grow the call stack +// --------------------------------------------------------------------------- + +TEST_CASE("devtools.audit.stepIntoEntersAndGrowsStack") { + const char* src = + "function callee() {\n" + " local a = 1\n" + "}\n" + "function caller() {\n" + " callee()\n" + " local b = 2\n" + "}\n" + "caller()\n"; + + AuditDriver driver; + driver.actions = { + []() { Debugger::instance().stepInto(); }, + []() { Debugger::instance().resume(); }, + }; + + Debugger& d = Debugger::instance(); + d.clearBreakpoints(); + d.setBreakpoint("audit3.nut", 5); // callee() call site in caller + runAudit(src, "audit3.nut", driver); + + REQUIRE(driver.stops >= 2); + CHECK_EQ(driver.stopLines[0], 5); + CHECK(driver.stopFuncs[1].find("callee") != std::string::npos); + CHECK(driver.stopDepths[1] > driver.stopDepths[0]); +} + +// --------------------------------------------------------------------------- +// stepOver: skip the callee, stop on the next statement in the caller +// --------------------------------------------------------------------------- + +TEST_CASE("devtools.audit.stepOverSkipsCallee") { + const char* src = + "function callee() {\n" + " local a = 1\n" + "}\n" + "function caller() {\n" + " callee()\n" + " local b = 2\n" + "}\n" + "caller()\n"; + + AuditDriver driver; + driver.actions = { + []() { Debugger::instance().stepOver(); }, + []() { Debugger::instance().resume(); }, + }; + + Debugger& d = Debugger::instance(); + d.clearBreakpoints(); + d.setBreakpoint("audit4.nut", 5); // callee() call site in caller + runAudit(src, "audit4.nut", driver); + + REQUIRE(driver.stops >= 2); + CHECK_EQ(driver.stopLines[1], 6); // local b = 2 in caller, not inside callee + CHECK(driver.stopFuncs[1].find("caller") != std::string::npos); + CHECK_EQ(driver.stopDepths[1], driver.stopDepths[0]); +} + +// --------------------------------------------------------------------------- +// Breakpoints keep hitting on every loop iteration after continue +// --------------------------------------------------------------------------- + +TEST_CASE("devtools.audit.breakpointRehitsInLoop") { + const char* src = + "local total = 0\n" + "for (local i = 0; i < 3; ++i) {\n" + " total += i\n" + "}\n" + "print(total)\n"; + + AuditDriver driver; + // Continue (resume) after every stop; the breakpoint must hit each iteration. + driver.actions = { + []() { Debugger::instance().resume(); }, + []() { Debugger::instance().resume(); }, + []() { Debugger::instance().resume(); }, + }; + + Debugger& d = Debugger::instance(); + d.clearBreakpoints(); + d.setBreakpoint("audit5.nut", 3); + runAudit(src, "audit5.nut", driver); + + // i = 0,1,2 �?exactly three stops at the loop body line. + CHECK_EQ(driver.stops, 3); + for (int l : driver.stopLines) CHECK_EQ(l, 3); +} + +// --------------------------------------------------------------------------- +// Locals of the current frame are readable at a breakpoint +// --------------------------------------------------------------------------- + +TEST_CASE("devtools.audit.localsAtBreakpoint") { + const char* src = + "function compute(x, y) {\n" + " local z = x + y\n" + " local name = \"audit\"\n" + " return z\n" + "}\n" + "compute(3, 4)\n"; + + AuditDriver driver; + driver.actions = {[]() { Debugger::instance().resume(); }}; + + std::vector locals; + driver.capture = [&]() { locals = Debugger::instance().locals(0); }; + + Debugger& d = Debugger::instance(); + d.clearBreakpoints(); + // Pause on `return z`: all four locals (x, y, z, name) are assigned by then. + d.setBreakpoint("audit6.nut", 4); + runAudit(src, "audit6.nut", driver); + + REQUIRE(driver.stops >= 1); + bool sawX = false, sawY = false, sawZ = false, sawName = false; + for (const auto& lv : locals) { + if (lv.name == "x") { + sawX = true; + CHECK(lv.value.find("3") != std::string::npos); + } else if (lv.name == "y") { + sawY = true; + CHECK(lv.value.find("4") != std::string::npos); + } else if (lv.name == "z") { + sawZ = true; + CHECK(lv.value.find("7") != std::string::npos); + } else if (lv.name == "name") { + sawName = true; + CHECK(lv.value.find("audit") != std::string::npos); + } + } + CHECK(sawX); + CHECK(sawY); + CHECK(sawZ); + CHECK(sawName); + + // evaluate() must read the CURRENT frame's locals (regression: it used to + // read the caller's frame at level 1). + auto ev = Debugger::instance().evaluate("z"); + CHECK_EQ(ev.type, std::string("integer")); + CHECK(ev.value.find("7") != std::string::npos); +} From 0b06713d1391331c3d8655b8ff483c9084aa7712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Fri, 14 Aug 2026 22:40:52 +0800 Subject: [PATCH 02/21] Harden thread pool task lifetime and event wakeups --- docs/usr/modules/thread.md | 6 +- src/modules/event/Event.cpp | 7 +- src/modules/event/Event.h | 3 + src/modules/event/sdl/Event.cpp | 29 +++++- src/modules/event/sdl/Event.h | 2 + src/modules/thread/Channel.cpp | 52 +++++----- src/modules/thread/Channel.h | 19 +++- src/modules/thread/Task.cpp | 62 +++++++----- src/modules/thread/Task.h | 23 +++-- src/modules/thread/ThreadPool.cpp | 161 +++++++++++++++++++++--------- src/modules/thread/ThreadPool.h | 20 ++-- src/scripts/async.nut | 37 ++++++- test/async.cpp | 21 ++++ test/event.cpp | 19 ++++ test/thread.cpp | 84 ++++++++++++++++ 15 files changed, 418 insertions(+), 127 deletions(-) diff --git a/docs/usr/modules/thread.md b/docs/usr/modules/thread.md index df99e18c..157ced6e 100644 --- a/docs/usr/modules/thread.md +++ b/docs/usr/modules/thread.md @@ -25,13 +25,17 @@ pool.submitPush(channel, "done", 100); ### 等待关卡加载任务 -为每项任务保留 Task,轮询 `isDone()` / `hasFailed()`,显示进度;仅在退出或确定不会卡住界面时调用 `wait()` / `waitAll()`。 +需要观察状态时为每项任务保留 Task,轮询 `isDone()` / `hasFailed()`,显示进度。脚本提前释放 Task 或匿名 Channel 不会中止已提交工作;线程池会持有任务完成所需的原生状态。仅在退出或确定不会卡住界面时调用 `wait()` / `waitAll()`。 + +`newThreadPool(0)` 使用硬件并发度;显式线程数上限为 256。`waitAll()` 与 `stop()` 不能从同一个线程池的 worker 中调用,这类调用会立即抛出异常,避免自等待死锁。 ## 常见问题 - worker 调用 gfx/ui/Squirrel:这些对象不是线程安全的。 +- worker 调用所属 Pool 的 `waitAll()` / `stop()`:会抛出异常;把关闭与汇合操作放在主线程。 - 主线程立即 `waitAll()`:异步退化成卡顿的同步。 - 忽略 `hasFailed()` / `getError()`:失败任务会悄悄丢结果。 +- 并发任务使用相同 Event 名称:业务监听仍按队列消费;`asyncPost()` 的 Promise 按具体 Task 关联,不会因同名事件乱序而串线。 ## API 快查 diff --git a/src/modules/event/Event.cpp b/src/modules/event/Event.cpp index 94c19130..ad28678e 100644 --- a/src/modules/event/Event.cpp +++ b/src/modules/event/Event.cpp @@ -29,8 +29,11 @@ Event::~Event() {} void Event::push(Message* msg) { if (!msg) return; - std::lock_guard lock(queueMu_); - queue.push(msg); + { + std::lock_guard lock(queueMu_); + queue.push(msg); + } + wakeWaiters(); } void Event::pushData(std::string name, std::string data) { diff --git a/src/modules/event/Event.h b/src/modules/event/Event.h index 779d8a00..e45b22e7 100644 --- a/src/modules/event/Event.h +++ b/src/modules/event/Event.h @@ -78,6 +78,9 @@ public: virtual Message* wait() = 0; protected: + /** Wake a platform-specific blocking wait after a message is queued. */ + virtual void wakeWaiters() {} + std::mutex queueMu_; std::queue queue; std::string lastData_; diff --git a/src/modules/event/sdl/Event.cpp b/src/modules/event/sdl/Event.cpp index 3135f652..3dc3aee5 100644 --- a/src/modules/event/sdl/Event.cpp +++ b/src/modules/event/sdl/Event.cpp @@ -115,6 +115,11 @@ Event::Event() { if (SDL_InitSubSystem(SDL_INIT_EVENTS) < 0) throw eve::Exception("Could not initialize SDL events subsystem (%s)", SDL_GetError()); + wakeEventType_ = SDL_RegisterEvents(1); + if (wakeEventType_ == static_cast(-1)) { + SDL_QuitSubSystem(SDL_INIT_EVENTS); + throw eve::Exception("Could not reserve SDL wake event (%s)", SDL_GetError()); + } SDL_AddEventWatch(watchAppEvents, this); } @@ -149,12 +154,28 @@ Message *Event::wait() { exceptionIfInRenderPass("eve.event.wait"); - SDL_Event e; + for (;;) { + if (Message *queued = poll()) + return queued; - if (SDL_WaitEvent(&e) != 1) - return nullptr; + SDL_Event e; + if (SDL_WaitEvent(&e) != 1) + return nullptr; + if (e.type == wakeEventType_) + continue; - return convert(e); + if (Message *message = convert(e)) + return message; + } +} + +void Event::wakeWaiters() +{ + SDL_Event e{}; + e.type = wakeEventType_; + e.user.type = wakeEventType_; + e.user.data1 = this; + SDL_PushEvent(&e); } void Event::clear() diff --git a/src/modules/event/sdl/Event.h b/src/modules/event/sdl/Event.h index 520d01f0..37e28cda 100644 --- a/src/modules/event/sdl/Event.h +++ b/src/modules/event/sdl/Event.h @@ -34,8 +34,10 @@ public: void clear() override; private: + void wakeWaiters() override; void exceptionIfInRenderPass(const char *name); Message *convert(const SDL_Event &e); + Uint32 wakeEventType_ = static_cast(-1); }; // Event diff --git a/src/modules/thread/Channel.cpp b/src/modules/thread/Channel.cpp index 6099455b..0b1e22e6 100644 --- a/src/modules/thread/Channel.cpp +++ b/src/modules/thread/Channel.cpp @@ -5,62 +5,66 @@ namespace eve { namespace thread { -Channel::Channel(std::string name) : name_(std::move(name)) {} +Channel::Channel() : state_(std::make_shared()) {} + +Channel::Channel(std::string name) : state_(std::make_shared(std::move(name))) {} Channel::~Channel() = default; -std::string Channel::getName() const { return name_; } +std::string Channel::getName() const { return state_->name; } void Channel::push(std::string value) { { - std::lock_guard lock(mu_); - queue_.push(std::move(value)); + std::lock_guard lock(state_->mu); + state_->queue.push(std::move(value)); } - cv_.notify_one(); + state_->cv.notify_one(); } std::string Channel::pop() { - std::lock_guard lock(mu_); - if (queue_.empty()) + std::lock_guard lock(state_->mu); + if (state_->queue.empty()) return {}; - std::string v = std::move(queue_.front()); - queue_.pop(); + std::string v = std::move(state_->queue.front()); + state_->queue.pop(); return v; } std::string Channel::demand() { - std::unique_lock lock(mu_); - cv_.wait(lock, [this] { return !queue_.empty(); }); - std::string v = std::move(queue_.front()); - queue_.pop(); + auto state = state_; + std::unique_lock lock(state->mu); + state->cv.wait(lock, [&state] { return !state->queue.empty(); }); + std::string v = std::move(state->queue.front()); + state->queue.pop(); return v; } std::string Channel::supply(int timeoutMs) { - std::unique_lock lock(mu_); + auto state = state_; + std::unique_lock lock(state->mu); if (timeoutMs < 0) timeoutMs = 0; - if (!cv_.wait_for(lock, std::chrono::milliseconds(timeoutMs), [this] { return !queue_.empty(); })) + if (!state->cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), [&state] { return !state->queue.empty(); })) return {}; - std::string v = std::move(queue_.front()); - queue_.pop(); + std::string v = std::move(state->queue.front()); + state->queue.pop(); return v; } bool Channel::hasData() const { - std::lock_guard lock(mu_); - return !queue_.empty(); + std::lock_guard lock(state_->mu); + return !state_->queue.empty(); } int Channel::getCount() const { - std::lock_guard lock(mu_); - return static_cast(queue_.size()); + std::lock_guard lock(state_->mu); + return static_cast(state_->queue.size()); } void Channel::clear() { - std::lock_guard lock(mu_); - while (!queue_.empty()) - queue_.pop(); + std::lock_guard lock(state_->mu); + while (!state_->queue.empty()) + state_->queue.pop(); } } // namespace thread diff --git a/src/modules/thread/Channel.h b/src/modules/thread/Channel.h index 41ade07b..63b1fc0c 100644 --- a/src/modules/thread/Channel.h +++ b/src/modules/thread/Channel.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -15,7 +16,7 @@ namespace thread { */ class Channel { public: - Channel() = default; + Channel(); explicit Channel(std::string name); ~Channel(); @@ -34,10 +35,18 @@ public: void clear(); private: - std::string name_; - mutable std::mutex mu_; - std::condition_variable cv_; - std::queue queue_; + struct State { + explicit State(std::string channelName = {}) : name(std::move(channelName)) {} + + std::string name; + mutable std::mutex mu; + std::condition_variable cv; + std::queue queue; + }; + + std::shared_ptr state_; + + friend class ThreadPool; }; } // namespace thread diff --git a/src/modules/thread/Task.cpp b/src/modules/thread/Task.cpp index 30ddd070..e02e9c08 100644 --- a/src/modules/thread/Task.cpp +++ b/src/modules/thread/Task.cpp @@ -5,63 +5,71 @@ namespace eve { namespace thread { -Task::Task(std::function fn) : fn_(std::move(fn)) { - if (!fn_) +Task::Task(std::function fn) { + if (!fn) throw eve::Exception("Task function is null"); + state_ = std::make_shared(std::move(fn)); } +Task::Task(std::shared_ptr state) : state_(std::move(state)) {} + Task::~Task() = default; std::string Task::getStatus() const { - std::lock_guard lock(mu_); - return status_; + std::lock_guard lock(state_->mu); + return state_->status; } bool Task::isDone() const { - std::lock_guard lock(mu_); - return status_ == "done" || status_ == "failed"; + std::lock_guard lock(state_->mu); + return state_->status == "done" || state_->status == "failed"; } bool Task::hasFailed() const { - std::lock_guard lock(mu_); - return status_ == "failed"; + std::lock_guard lock(state_->mu); + return state_->status == "failed"; } std::string Task::getError() const { - std::lock_guard lock(mu_); - return error_; + std::lock_guard lock(state_->mu); + return state_->error; } void Task::wait() { - std::unique_lock lock(mu_); - cv_.wait(lock, [this] { return status_ == "done" || status_ == "failed"; }); + auto state = state_; + std::unique_lock lock(state->mu); + state->cv.wait(lock, [&state] { return state->status == "done" || state->status == "failed"; }); } void Task::run() { + run(state_); +} + +void Task::run(const std::shared_ptr &state) { { - std::lock_guard lock(mu_); - status_ = "running"; + std::lock_guard lock(state->mu); + state->status = "running"; } try { - fn_(); - std::lock_guard lock(mu_); - status_ = "done"; + state->fn(); + std::lock_guard lock(state->mu); + state->status = "done"; } catch (const eve::Exception &e) { - std::lock_guard lock(mu_); - status_ = "failed"; - error_ = e.what(); + std::lock_guard lock(state->mu); + state->status = "failed"; + state->error = e.what(); } catch (const std::exception &e) { - std::lock_guard lock(mu_); - status_ = "failed"; - error_ = e.what(); + std::lock_guard lock(state->mu); + state->status = "failed"; + state->error = e.what(); } catch (...) { - std::lock_guard lock(mu_); - status_ = "failed"; - error_ = "unknown exception"; + std::lock_guard lock(state->mu); + state->status = "failed"; + state->error = "unknown exception"; } - cv_.notify_all(); + state->cv.notify_all(); } } // namespace thread diff --git a/src/modules/thread/Task.h b/src/modules/thread/Task.h index fad50817..cb6bd702 100644 --- a/src/modules/thread/Task.h +++ b/src/modules/thread/Task.h @@ -1,8 +1,8 @@ #pragma once -#include #include #include +#include #include #include @@ -30,11 +30,22 @@ public: void run(); private: - std::function fn_; - mutable std::mutex mu_; - std::condition_variable cv_; - std::string status_ = "pending"; - std::string error_; + struct State { + explicit State(std::function taskFn) : fn(std::move(taskFn)) {} + + std::function fn; + mutable std::mutex mu; + std::condition_variable cv; + std::string status = "pending"; + std::string error; + }; + + explicit Task(std::shared_ptr state); + static void run(const std::shared_ptr &state); + + std::shared_ptr state_; + + friend class ThreadPool; }; } // namespace thread diff --git a/src/modules/thread/ThreadPool.cpp b/src/modules/thread/ThreadPool.cpp index cfde0d91..0f70cf0a 100644 --- a/src/modules/thread/ThreadPool.cpp +++ b/src/modules/thread/ThreadPool.cpp @@ -7,27 +7,63 @@ #include #include +#include #include #include namespace eve { namespace thread { +namespace { +constexpr int kMaxWorkerCount = 256; +thread_local const void *currentPoolState = nullptr; +std::mutex eventResolveMu; +} // namespace + +struct ThreadPool::State { + mutable std::mutex mu; + std::condition_variable cv; + std::condition_variable idleCv; + std::queue> queue; + int busy = 0; + bool stopping = false; +}; + ThreadPool::ThreadPool(int workerCount) { if (workerCount <= 0) { workerCount = static_cast(std::thread::hardware_concurrency()); if (workerCount <= 0) workerCount = 1; } + if (workerCount > kMaxWorkerCount) + throw eve::Exception("ThreadPool worker count exceeds limit (%d)", kMaxWorkerCount); + workerCount_ = workerCount; + state_ = std::make_shared(); workers_.reserve(static_cast(workerCount_)); - for (int i = 0; i < workerCount_; ++i) - workers_.emplace_back([this] { workerMain(); }); + try { + for (int i = 0; i < workerCount_; ++i) { + auto state = state_; + workers_.emplace_back([state] { workerMain(state); }); + } + } catch (...) { + { + std::lock_guard lock(state_->mu); + state_->stopping = true; + } + state_->cv.notify_all(); + for (auto &worker : workers_) { + if (worker.joinable()) + worker.join(); + } + workers_.clear(); + throw; + } } ThreadPool::~ThreadPool() { try { - stop(); + stopImpl(true); } catch (...) { } } @@ -35,29 +71,30 @@ ThreadPool::~ThreadPool() { int ThreadPool::getWorkerCount() const { return workerCount_; } int ThreadPool::getPendingCount() const { - std::lock_guard lock(mu_); - return static_cast(queue_.size()); + std::lock_guard lock(state_->mu); + return static_cast(state_->queue.size()); } bool ThreadPool::isRunning() const { - std::lock_guard lock(mu_); - return !stopping_; + std::lock_guard lock(state_->mu); + return !state_->stopping; } Task *ThreadPool::submit(std::function fn) { if (!fn) throw eve::Exception("ThreadPool::submit: null function"); - auto *task = new Task(std::move(fn)); + auto state = std::make_shared(std::move(fn)); + auto *task = new Task(state); { - std::lock_guard lock(mu_); - if (stopping_) { + std::lock_guard lock(state_->mu); + if (state_->stopping) { delete task; throw eve::Exception("ThreadPool is stopped"); } - queue_.push(task); + state_->queue.push(std::move(state)); } - cv_.notify_one(); + state_->cv.notify_one(); return task; } @@ -74,10 +111,15 @@ Task *ThreadPool::submitPush(Channel *channel, std::string message, int delayMs) throw eve::Exception("ThreadPool::submitPush: channel is null"); if (delayMs < 0) delayMs = 0; - return submit([channel, msg = std::move(message), delayMs] { + auto channelState = channel->state_; + return submit([channelState, msg = std::move(message), delayMs] { if (delayMs > 0) std::this_thread::sleep_for(std::chrono::milliseconds(delayMs)); - channel->push(msg); + { + std::lock_guard lock(channelState->mu); + channelState->queue.push(msg); + } + channelState->cv.notify_one(); }); } @@ -86,39 +128,65 @@ Task *ThreadPool::submitPost(std::string name, std::string data, int delayMs) { throw eve::Exception("ThreadPool::submitPost: name must not be empty"); if (delayMs < 0) delayMs = 0; - return submit([name = std::move(name), data = std::move(data), delayMs] { - if (delayMs > 0) - std::this_thread::sleep_for(std::chrono::milliseconds(delayMs)); - auto *ev = ModuleManager::getInstance("Event"); + event::Event *ev = nullptr; + { + // ModuleManager is not thread-safe. Resolve/create the singleton while + // submitPost is still on the submitting (script/main) thread. + std::lock_guard lock(eventResolveMu); + ev = ModuleManager::getInstance("Event"); if (!ev) ev = event::Event::create(); + } + return submit([ev, name = std::move(name), data = std::move(data), delayMs] { + if (delayMs > 0) + std::this_thread::sleep_for(std::chrono::milliseconds(delayMs)); ev->pushData(name, data); }); } void ThreadPool::waitAll() { - std::unique_lock lock(mu_); - idleCv_.wait(lock, [this] { return queue_.empty() && busy_ == 0; }); + auto state = state_; + if (currentPoolState == state.get()) + throw eve::Exception("ThreadPool::waitAll cannot be called from its worker"); + std::unique_lock lock(state->mu); + state->idleCv.wait(lock, [&state] { return state->queue.empty() && state->busy == 0; }); } void ThreadPool::stop() { + if (currentPoolState == state_.get()) + throw eve::Exception("ThreadPool::stop cannot be called from its worker"); + stopImpl(false); +} + +void ThreadPool::stopImpl(bool allowWorkerCaller) { + std::lock_guard lifecycleLock(lifecycleMu_); + auto state = state_; + const bool calledByWorker = currentPoolState == state.get(); + if (calledByWorker && !allowWorkerCaller) + throw eve::Exception("ThreadPool::stop cannot be called from its worker"); + { - std::lock_guard lock(mu_); - if (stopping_) - return; - stopping_ = true; + std::lock_guard lock(state->mu); + state->stopping = true; } - cv_.notify_all(); - const auto self = std::this_thread::get_id(); + state->cv.notify_all(); + + // Destruction from a running job cannot safely join any pool worker: a + // different worker may itself be waiting for the current Task to finish. + // Workers only retain State, not ThreadPool, so detaching all of them here + // is safe and lets them drain accepted work before exiting. + if (calledByWorker) { + for (auto &worker : workers_) { + if (worker.joinable()) + worker.detach(); + } + workers_.clear(); + return; + } + for (auto &w : workers_) { if (!w.joinable()) continue; - // MSVC throws std::system_error(resource_deadlock_would_occur) if this - // thread is a worker (stop from a task / TLS teardown). - if (w.get_id() == self) { - w.detach(); - continue; - } try { w.join(); } catch (const std::system_error &) { @@ -129,27 +197,30 @@ void ThreadPool::stop() { workers_.clear(); } -void ThreadPool::workerMain() { +void ThreadPool::workerMain(std::shared_ptr state) { + currentPoolState = state.get(); for (;;) { - Task *task = nullptr; + std::shared_ptr task; { - std::unique_lock lock(mu_); - cv_.wait(lock, [this] { return stopping_ || !queue_.empty(); }); - if (stopping_ && queue_.empty()) + std::unique_lock lock(state->mu); + state->cv.wait(lock, [&state] { return state->stopping || !state->queue.empty(); }); + if (state->stopping && state->queue.empty()) { + currentPoolState = nullptr; return; - task = queue_.front(); - queue_.pop(); - ++busy_; + } + task = std::move(state->queue.front()); + state->queue.pop(); + ++state->busy; } if (task) - task->run(); + Task::run(task); { - std::lock_guard lock(mu_); - --busy_; - if (queue_.empty() && busy_ == 0) - idleCv_.notify_all(); + std::lock_guard lock(state->mu); + --state->busy; + if (state->queue.empty() && state->busy == 0) + state->idleCv.notify_all(); } } } diff --git a/src/modules/thread/ThreadPool.h b/src/modules/thread/ThreadPool.h index 529aed1d..1698fbfb 100644 --- a/src/modules/thread/ThreadPool.h +++ b/src/modules/thread/ThreadPool.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -32,7 +33,7 @@ public: int getPendingCount() const; bool isRunning() const; - /** Submit a C++ callable. Caller owns the returned Task* (delete when done). */ + /** Submit a C++ callable. Caller owns the Task wrapper; work owns its shared state. */ Task *submit(std::function fn); /** Sleep on a worker, then mark done — useful from scripts / tests. */ @@ -47,23 +48,22 @@ public: */ Task *submitPost(std::string name, std::string data = "", int delayMs = 0); - /** Block until the queue is empty and no worker is busy. */ + /** Block until idle. Throws when called by a worker belonging to this pool. */ void waitAll(); - /** Stop accepting work, join workers. Idempotent. */ + /** Stop accepting work and join workers. Worker calls are rejected. Idempotent. */ void stop(); private: - void workerMain(); + struct State; + + static void workerMain(std::shared_ptr state); + void stopImpl(bool allowWorkerCaller); int workerCount_ = 0; std::vector workers_; - mutable std::mutex mu_; - std::condition_variable cv_; - std::condition_variable idleCv_; - std::queue queue_; - int busy_ = 0; - bool stopping_ = false; + std::shared_ptr state_; + std::mutex lifecycleMu_; }; } // namespace thread diff --git a/src/scripts/async.nut b/src/scripts/async.nut index d1b79634..d514c631 100644 --- a/src/scripts/async.nut +++ b/src/scripts/async.nut @@ -13,9 +13,13 @@ if (!("_async" in getroottable())) { maxDrain = 1000 // Pending worker polls: { ch, resolve, reject, task } waits = [] + // Task-correlated completions: { task, value, resolve, reject } + taskWaits = [] eventWaits = [] // { name, resolve, reject } }; } +if (!("taskWaits" in _async)) + _async.taskWaits <- []; function nextTick(fn) { if (typeof fn != "function") @@ -133,6 +137,30 @@ function _async_poll_waits() { _async.waits = remain; } +function _async_poll_tasks() { + if (_async.taskWaits.len() == 0) + return; + local remain = []; + foreach (w in _async.taskWaits) { + local done = false; + try { + if (w.task.hasFailed()) { + w.reject(w.task.getError()); + done = true; + } else if (w.task.isDone()) { + w.resolve(w.value); + done = true; + } + } catch (e) { + try { w.reject(e); } catch (e2) {} + done = true; + } + if (!done) + remain.append(w); + } + _async.taskWaits = remain; +} + // Drain timers, worker waits, and microtasks. Call once per frame (load.nut). function async_pump() { if (_async.draining) @@ -141,6 +169,7 @@ function async_pump() { try { _async_fire_timers(); _async_poll_waits(); + _async_poll_tasks(); _async_drain_microtasks(); // Timers / waits may have queued more microtasks. _async_drain_microtasks(); @@ -356,14 +385,16 @@ function async_dispatch_event(name, data) { return matched; } -// Delay on a worker, then post Event(name, data); resolves when that event is polled. +// Delay on a worker, then post Event(name, data). The Promise is correlated to +// its own native Task, so same-name events cannot resolve the wrong request. function asyncPost(name, data, ms) { if (data == null) data = ""; if (ms == null) ms = 0; return Promise(function(resolve, reject) { local pool = thread.getPool(); - pool.submitPost(name, "" + data, ms); - _async.eventWaits.append({ name = name, resolve = resolve, reject = reject }); + local value = "" + data; + local task = pool.submitPost(name, value, ms); + _async.taskWaits.append({ task = task, value = value, resolve = resolve, reject = reject }); }); } diff --git a/test/async.cpp b/test/async.cpp index a6a7045e..7be75899 100644 --- a/test/async.cpp +++ b/test/async.cpp @@ -112,6 +112,27 @@ TEST_CASE("async.asyncDelay.worker") { CHECK_EQ(out, std::string("hello")); } +TEST_CASE("async.asyncPost.sameNameCorrelatedByTask") { + std::string out = runAsyncSnippet(R"( + result <- "pending"; + local slow = ""; + local fast = ""; + asyncPost("same-name-correlation", "slow", 25).then(function(v) { slow = v; }); + asyncPost("same-name-correlation", "fast", 1).then(function(v) { fast = v; }); + thread.getPool().waitAll(); + + local name = event.poll(); + while (name != "") { + async_dispatch_event(name, event.getLastData()); + name = event.poll(); + } + for (local i = 0; i < 6; i++) + async_pump(); + result = slow + "," + fast; + )"); + CHECK_EQ(out, std::string("slow,fast")); +} + TEST_CASE("async.postMain.event") { auto *th = eve::thread::Thread::create(); auto *ev = eve::event::Event::create(); diff --git a/test/event.cpp b/test/event.cpp index 8b840f06..1f60a53d 100644 --- a/test/event.cpp +++ b/test/event.cpp @@ -3,6 +3,9 @@ #include "event/Event.h" +#include +#include + using eve::event::Event; using eve::event::Message; using eve::event::Variant; @@ -104,3 +107,19 @@ TEST_CASE("event.pumpSmoke") { auto* ev = Event::create(); ev->pump(); } + +TEST_CASE("event.workerPushWakesWait") { + auto *ev = Event::create(); + ev->clear(); + std::thread producer([ev] { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + ev->pushData("worker-wake", "ready"); + }); + Message *message = ev->wait(); + producer.join(); + REQUIRE(message != nullptr); + CHECK_EQ(message->name, std::string("worker-wake")); + REQUIRE(message->args.size() == 1); + CHECK_EQ(message->args[0].s, std::string("ready")); + delete message; +} diff --git a/test/thread.cpp b/test/thread.cpp index 759305c7..2889fc10 100644 --- a/test/thread.cpp +++ b/test/thread.cpp @@ -165,3 +165,87 @@ TEST_CASE("thread.pool.stopRejectsSubmit") { CHECK(!pool->isRunning()); CHECK(expectException([&] { pool->submitSleep(1); })); } + +TEST_CASE("thread.pool.queueOwnsTaskState") { + std::unique_ptr pool(threadModule()->newThreadPool(1)); + eve::thread::Task *task = pool->submitSleep(10); + delete task; + pool->waitAll(); + CHECK_EQ(pool->getPendingCount(), 0); +} + +TEST_CASE("thread.pool.jobOwnsAnonymousChannelState") { + std::unique_ptr pool(threadModule()->newThreadPool(1)); + auto *channel = threadModule()->newChannel(); + std::unique_ptr task(pool->submitPush(channel, "late", 10)); + delete channel; + task->wait(); + CHECK_EQ(task->getStatus(), std::string("done")); +} + +TEST_CASE("thread.pool.workerWaitAllRejected") { + std::unique_ptr pool(threadModule()->newThreadPool(1)); + std::atomic rejected{false}; + std::unique_ptr task(pool->submit([&] { + try { + pool->waitAll(); + } catch (const eve::Exception &) { + rejected.store(true, std::memory_order_relaxed); + } + })); + task->wait(); + CHECK(rejected.load(std::memory_order_relaxed)); +} + +TEST_CASE("thread.pool.workerStopRejected") { + std::unique_ptr pool(threadModule()->newThreadPool(1)); + std::atomic rejected{false}; + std::unique_ptr task(pool->submit([&] { + try { + pool->stop(); + } catch (const eve::Exception &) { + rejected.store(true, std::memory_order_relaxed); + } + })); + task->wait(); + CHECK(rejected.load(std::memory_order_relaxed)); + CHECK(pool->isRunning()); +} + +TEST_CASE("thread.pool.workerCountBounded") { + CHECK(expectException([&] { + std::unique_ptr pool(threadModule()->newThreadPool(257)); + })); +} + +TEST_CASE("thread.pool.destructorFromWorkerIsSafe") { + auto *pool = threadModule()->newThreadPool(1); + std::unique_ptr task(pool->submit([pool] { delete pool; })); + task->wait(); + CHECK_EQ(task->getStatus(), std::string("done")); +} + +TEST_CASE("thread.pool.workerDestructorDoesNotJoinDependentWorker") { + auto *pool = threadModule()->newThreadPool(2); + std::atomic deletingTask{nullptr}; + std::atomic waiterReady{false}; + + std::unique_ptr waiter(pool->submit([&] { + eve::thread::Task *target = nullptr; + while ((target = deletingTask.load(std::memory_order_acquire)) == nullptr) + std::this_thread::yield(); + waiterReady.store(true, std::memory_order_release); + target->wait(); + })); + std::unique_ptr destroyer(pool->submit([&] { + while (!waiterReady.load(std::memory_order_acquire)) + std::this_thread::yield(); + delete pool; + })); + deletingTask.store(destroyer.get(), std::memory_order_release); + + destroyer->wait(); + waiter->wait(); + CHECK_EQ(destroyer->getStatus(), std::string("done")); + CHECK_EQ(waiter->getStatus(), std::string("done")); +} From 4ebf393cc31c2e0a8c41c3d68b79df7ddb273dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Fri, 14 Aug 2026 23:01:11 +0800 Subject: [PATCH 03/21] Create a better ssq runtime --- src/engine/cmdline/Run.cpp | 14 +- src/engine/common/Module.cpp | 42 ++- src/engine/common/Module.h | 11 +- src/engine/common/Runtime.cpp | 495 +++++++++++++++++++++++++++++++++- src/engine/common/Runtime.h | 185 +++++++++++++ test/runtime.cpp | 80 ++++++ 6 files changed, 803 insertions(+), 24 deletions(-) create mode 100644 test/runtime.cpp diff --git a/src/engine/cmdline/Run.cpp b/src/engine/cmdline/Run.cpp index 93228620..316651b9 100644 --- a/src/engine/cmdline/Run.cpp +++ b/src/engine/cmdline/Run.cpp @@ -1,6 +1,7 @@ #include "cmdline.h" #include "scripts.h" #include "common/Module.h" +#include "common/Runtime.h" #include "common/config.h" #include "filesystem/Filesystem.h" #if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) @@ -98,13 +99,13 @@ int Cmdline::Run(std::string path, std::string root, bool debug, int dapPort, in } } - ssq::VM vm(2048, ssq::Libs::ALL); - ModuleManager::expose(vm); + Runtime runtime(2048, ssq::Libs::ALL); + runtime.initialize(); #if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) if (debug) { auto& dt = eve::dev::DevTool::instance(); - dt.attach(vm, /*sampleLocals=*/true); - dt.exposeScriptApi(vm); + dt.attach(runtime.vm(), /*sampleLocals=*/true); + dt.exposeScriptApi(runtime.vm()); if (dapPort > 0) { const int bound = dt.startDap(static_cast(dapPort)); if (bound > 0) { @@ -139,13 +140,12 @@ int Cmdline::Run(std::string path, std::string root, bool debug, int dapPort, in #endif // Embedded default demo (src/scripts/demo.nut); load.nut runs it when no main.nut. { - ssq::Table eve(vm.find("eve")); + ssq::Table eve = runtime.table("eve"); eve.set("demoScript", std::string(demo_content ? demo_content : "")); eve.set("asyncScript", std::string(async_content ? async_content : "")); } // Name the embedded root so DAP stack frames map to load.nut (not "buffer"). - ssq::Script script = vm.compileSource(root.c_str(), "load.nut"); - vm.run(script); + runtime.runSource(root, "load.nut"); #if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) if (debug) eve::dev::DevTool::instance().detach(); #endif diff --git a/src/engine/common/Module.cpp b/src/engine/common/Module.cpp index 884e137c..5163a137 100644 --- a/src/engine/common/Module.cpp +++ b/src/engine/common/Module.cpp @@ -1,5 +1,6 @@ #include "common/Module.h" #include "common/ECS.h" +#include "common/Runtime.h" #include @@ -33,28 +34,51 @@ void ModuleManager::register_module(const char* name, creator_t c, exposer_t e) } } -void ModuleManager::expose(ssq::VM& vm) { - inst().active_vm_ = &vm; - auto table = vm.addTable("eve"); +void ModuleManager::exposeVM(ssq::VM& vm) { + ssq::Table table; + try { + table = ssq::Table(vm.find("eve")); + } catch (const ssq::NotFoundException&) { + table = vm.addTable("eve"); + } for (auto& D : inst().registered_modules) { - D.second.exposer(table); + if (D.second.exposer) D.second.exposer(table); D.second.exposed = true; } // After modules: script ECS owns eve.Component / Entity / System / view. exposeECS(table); } -void ModuleManager::set_vm(ssq::VM* vm) { inst().active_vm_ = vm; } +void ModuleManager::expose(Runtime& runtime) { + inst().active_runtime_ = &runtime; + exposeVM(runtime.vm()); +} -ssq::VM* ModuleManager::vm() { return inst().active_vm_; } +void ModuleManager::expose(ssq::VM& vm) { + inst().active_runtime_ = nullptr; + exposeVM(vm); +} + +Runtime* ModuleManager::runtime() { return inst().active_runtime_; } + +ssq::VM* ModuleManager::vm() { + Runtime* active = runtime(); + return active ? &active->vm() : nullptr; +} + +void ModuleManager::detach(Runtime* runtime) { + if (inst().active_runtime_ == runtime) inst().active_runtime_ = nullptr; +} int ModuleManager::expose_pending() { - ssq::VM* v = inst().active_vm_; - if (!v) + Runtime* active = inst().active_runtime_; + if (!active) return -1; int count = 0; try { - ssq::Table table(v->find("eve")); + auto scope = active->enter(); + auto stack = active->guard(); + ssq::Table table = active->table("eve"); for (auto& D : inst().registered_modules) { if (D.second.exposed || !D.second.exposer) continue; diff --git a/src/engine/common/Module.h b/src/engine/common/Module.h index 7f41c27c..a84d3bf4 100644 --- a/src/engine/common/Module.h +++ b/src/engine/common/Module.h @@ -38,6 +38,8 @@ class Class; namespace eve { +class Runtime; + class EVENGINE_API Module { public: virtual ~Module() {} @@ -55,11 +57,14 @@ public: static void insert(const char* name, Module* inst); static void register_module(const char* name, creator_t c, exposer_t e); + static void expose(Runtime& runtime); + // Compatibility for embedders that still own their ssq::VM directly. static void expose(ssq::VM& vm); // Expose modules registered after the initial expose() (e.g. plugins). static int expose_pending(); - static void set_vm(ssq::VM* vm); + static Runtime* runtime(); static ssq::VM* vm(); + static void detach(Runtime* runtime); template static T* getInstance(const char* name) { @@ -74,6 +79,8 @@ public: } protected: + static void exposeVM(ssq::VM& vm); + struct ModuleInfo { creator_t creator = nullptr; exposer_t exposer = nullptr; @@ -82,7 +89,7 @@ protected: }; std::unordered_map registered_modules; - ssq::VM* active_vm_ = nullptr; + Runtime* active_runtime_ = nullptr; }; struct EVENGINE_API ModuleRegister { diff --git a/src/engine/common/Runtime.cpp b/src/engine/common/Runtime.cpp index 7f5071d0..dc9ba34a 100644 --- a/src/engine/common/Runtime.cpp +++ b/src/engine/common/Runtime.cpp @@ -1,7 +1,490 @@ -#include "Runtime.h" -#include +#include "common/Runtime.h" -int createRuntime() { - std::cout << "Runtime Printing" << std::endl; - return 0; -} \ No newline at end of file +#include "common/Module.h" + +#include +#include +#include + +namespace eve { +namespace { + +thread_local std::vector runtime_stack; + +const char* stageName(ScriptStage stage) { + switch (stage) { + case ScriptStage::Compile: return "compile"; + case ScriptStage::Execute: return "execute"; + case ScriptStage::Reflect: return "reflect"; + case ScriptStage::Unload: return "unload"; + case ScriptStage::Shutdown: return "shutdown"; + } + return "unknown"; +} + +std::string valueString(HSQUIRRELVM vm, SQInteger index) { + switch (sq_gettype(vm, index)) { + case OT_NULL: return "null"; + case OT_BOOL: { + SQBool value = SQFalse; + return SQ_SUCCEEDED(sq_getbool(vm, index, &value)) && value ? "true" : "false"; + } + case OT_INTEGER: { + SQInteger value = 0; + if (SQ_SUCCEEDED(sq_getinteger(vm, index, &value))) return std::to_string(value); + break; + } + case OT_FLOAT: { + SQFloat value = 0; + if (SQ_SUCCEEDED(sq_getfloat(vm, index, &value))) { + std::ostringstream out; + out << value; + return out.str(); + } + break; + } + case OT_STRING: { + const SQChar* value = nullptr; + if (SQ_SUCCEEDED(sq_getstring(vm, index, &value)) && value) return value; + break; + } + default: break; + } + return {}; +} + +SQUserPointer objectIdentity(const HSQOBJECT& object) { + return reinterpret_cast(object._unVal.pRefCounted); +} + +} // namespace + +struct Runtime::ScriptRecord { + ScriptInfo info; + std::string source_text; + bool from_file = false; + std::unique_ptr compiled; + std::unordered_map class_objects; +}; + +ScriptException::ScriptException(ScriptStage stage, std::string source, uint64_t scriptId, + const std::string& message) + : std::runtime_error([&] { + std::ostringstream out; + out << "Script " << stageName(stage); + if (!source.empty()) out << " failed in '" << source << "'"; + if (scriptId != 0) out << " [id=" << scriptId << "]"; + out << ": " << message; + return out.str(); + }()), + stage_(stage), source_(std::move(source)), script_id_(scriptId) {} + +Runtime::StackGuard::StackGuard(Runtime& runtime) noexcept + : vm_(runtime.handle()), top_(vm_ ? sq_gettop(vm_) : 0) {} + +Runtime::StackGuard::~StackGuard() { + if (vm_) sq_settop(vm_, top_); +} + +Runtime::StackGuard::StackGuard(StackGuard&& other) noexcept + : vm_(std::exchange(other.vm_, nullptr)), top_(other.top_) {} + +Runtime::StackGuard& Runtime::StackGuard::operator=(StackGuard&& other) noexcept { + if (this == &other) return *this; + if (vm_) sq_settop(vm_, top_); + vm_ = std::exchange(other.vm_, nullptr); + top_ = other.top_; + return *this; +} + +Runtime::Scope::Scope(Runtime& runtime) : runtime_(&runtime) { runtime_stack.push_back(&runtime); } + +Runtime::Scope::~Scope() { + if (!runtime_) return; + auto it = std::find(runtime_stack.rbegin(), runtime_stack.rend(), runtime_); + if (it != runtime_stack.rend()) runtime_stack.erase(std::next(it).base()); +} + +Runtime::Scope::Scope(Scope&& other) noexcept + : runtime_(std::exchange(other.runtime_, nullptr)) {} + +Runtime::Runtime(size_t stackSize, ssq::Libs::Flag libraries) + : vm_(std::make_unique(stackSize, libraries)) {} + +Runtime::~Runtime() { shutdown(); } + +void Runtime::initialize() { + if (initialized_) return; + auto scope = enter(); + auto stack = guard(); + try { + ModuleManager::expose(*this); + initialized_ = true; + } catch (const std::exception& error) { + ModuleManager::detach(this); + fail(ScriptStage::Execute, "", 0, error); + } +} + +void Runtime::shutdown() noexcept { + if (shutting_down_ || stopped_) return; + shutting_down_ = true; + unloadAll(); + ModuleManager::detach(this); + while (true) { + auto it = std::find(runtime_stack.begin(), runtime_stack.end(), this); + if (it == runtime_stack.end()) break; + runtime_stack.erase(it); + } + classes_.clear(); + class_owners_.clear(); + initialized_ = false; + vm_.reset(); + stopped_ = true; + shutting_down_ = false; +} + +bool Runtime::initialized() const noexcept { return initialized_; } +ssq::VM& Runtime::vm() noexcept { return *vm_; } +const ssq::VM& Runtime::vm() const noexcept { return *vm_; } +HSQUIRRELVM Runtime::handle() const noexcept { return vm_ ? vm_->getHandle() : nullptr; } +ssq::Table Runtime::root() const { return ssq::Table(static_cast(*vm_)); } +ssq::Table Runtime::table(const char* name) const { return ssq::Table(vm_->find(name)); } + +Runtime* Runtime::current() noexcept { + return runtime_stack.empty() ? nullptr : runtime_stack.back(); +} + +size_t Runtime::stackDepth() noexcept { return runtime_stack.size(); } + +Runtime::ScriptId Runtime::compileSource(std::string source, std::string sourceName) { + if (stopped_) throw ScriptException(ScriptStage::Compile, sourceName, 0, "runtime is shut down"); + auto scope = enter(); + auto stack = guard(); + const ScriptId id = next_script_id_++; + auto record = std::make_unique(); + record->info.id = id; + record->info.source = sourceName; + record->source_text = std::move(source); + try { + record->compiled = + std::make_unique(vm_->compileSource(record->source_text.c_str(), sourceName.c_str())); + auto [it, inserted] = scripts_.emplace(id, std::move(record)); + (void)inserted; + notifyLifecycle(it->second->info); + return id; + } catch (const std::exception& error) { + fail(ScriptStage::Compile, sourceName, id, error); + } +} + +Runtime::ScriptId Runtime::compileFile(const std::string& path) { + if (stopped_) throw ScriptException(ScriptStage::Compile, path, 0, "runtime is shut down"); + auto scope = enter(); + auto stack = guard(); + const ScriptId id = next_script_id_++; + auto record = std::make_unique(); + record->info.id = id; + record->info.source = path; + record->source_text = path; + record->from_file = true; + try { + record->compiled = std::make_unique(vm_->compileFile(path.c_str())); + auto [it, inserted] = scripts_.emplace(id, std::move(record)); + (void)inserted; + notifyLifecycle(it->second->info); + return id; + } catch (const std::exception& error) { + fail(ScriptStage::Compile, path, id, error); + } +} + +const ScriptInfo& Runtime::execute(ScriptId id) { + auto found = scripts_.find(id); + if (found == scripts_.end()) + throw ScriptException(ScriptStage::Execute, {}, id, "unknown script"); + ScriptRecord& record = *found->second; + if (!record.compiled || record.info.state == ScriptState::Unloaded) + throw ScriptException(ScriptStage::Execute, record.info.source, id, "script is unloaded"); + + auto scope = enter(); + auto stack = guard(); + const auto before = rootClasses(); + record.info.state = ScriptState::Running; + record.info.error.clear(); + notifyLifecycle(record.info); + try { + vm_->run(*record.compiled); + discoverClasses(record, before); + record.info.state = ScriptState::Loaded; + notifyLifecycle(record.info); + return record.info; + } catch (const ScriptException&) { + throw; + } catch (const std::exception& error) { + try { + discoverClasses(record, before); + } catch (...) { + } + record.info.state = ScriptState::Failed; + record.info.error = error.what(); + notifyLifecycle(record.info); + fail(ScriptStage::Execute, record.info.source, id, error); + } +} + +Runtime::ScriptId Runtime::runSource(std::string source, std::string sourceName) { + const ScriptId id = compileSource(std::move(source), std::move(sourceName)); + execute(id); + return id; +} + +Runtime::ScriptId Runtime::runFile(const std::string& path) { + const ScriptId id = compileFile(path); + execute(id); + return id; +} + +const ReflectedClass& Runtime::reflectClass(const std::string& name, const std::string& source) { + auto scope = enter(); + auto stack = guard(); + try { + ssq::Class cls = vm_->findClass(name.c_str()); + classes_[name] = inspectClass(name, cls, source); + return classes_.at(name); + } catch (const std::exception& error) { + fail(ScriptStage::Reflect, source.empty() ? name : source, 0, error); + } +} + +Runtime::ScriptId Runtime::reload(ScriptId id) { + auto found = scripts_.find(id); + if (found == scripts_.end()) + throw ScriptException(ScriptStage::Compile, {}, id, "unknown script"); + const bool fromFile = found->second->from_file; + const std::string input = found->second->source_text; + const std::string source = found->second->info.source; + unload(id); + return fromFile ? runFile(input) : runSource(input, source); +} + +bool Runtime::unload(ScriptId id) { + auto found = scripts_.find(id); + if (found == scripts_.end()) return false; + ScriptRecord& record = *found->second; + if (record.info.state == ScriptState::Unloaded) return false; + auto scope = enter(); + auto stack = guard(); + record.info.state = ScriptState::Unloading; + notifyLifecycle(record.info); + try { + for (const auto& pair : record.class_objects) { + auto owner = class_owners_.find(pair.first); + if (owner == class_owners_.end() || owner->second != id) continue; + ssq::Object currentObject = vm_->find(pair.first.c_str()); + if (currentObject.getType() == ssq::Type::CLASS && + objectIdentity(currentObject.getRaw()) == objectIdentity(pair.second.getRaw())) { + sq_pushroottable(handle()); + sq_pushstring(handle(), pair.first.c_str(), static_cast(pair.first.size())); + sq_rawdeleteslot(handle(), -2, SQFalse); + sq_pop(handle(), 1); + } + classes_.erase(pair.first); + class_owners_.erase(owner); + } + record.class_objects.clear(); + record.compiled.reset(); + record.info.state = ScriptState::Unloaded; + notifyLifecycle(record.info); + return true; + } catch (const std::exception& error) { + record.info.state = ScriptState::Failed; + record.info.error = error.what(); + notifyLifecycle(record.info); + fail(ScriptStage::Unload, record.info.source, id, error); + } +} + +void Runtime::unloadAll() noexcept { + std::vector ids; + ids.reserve(scripts_.size()); + for (const auto& pair : scripts_) ids.push_back(pair.first); + std::reverse(ids.begin(), ids.end()); + for (ScriptId id : ids) { + try { + unload(id); + } catch (...) { + } + } + scripts_.clear(); +} + +bool Runtime::contains(ScriptId id) const noexcept { return scripts_.count(id) != 0; } + +const ScriptInfo* Runtime::script(ScriptId id) const noexcept { + auto found = scripts_.find(id); + return found == scripts_.end() ? nullptr : &found->second->info; +} + +std::vector Runtime::scripts() const { + std::vector result; + result.reserve(scripts_.size()); + for (const auto& pair : scripts_) result.push_back(pair.second->info); + std::sort(result.begin(), result.end(), [](const ScriptInfo& a, const ScriptInfo& b) { + return a.id < b.id; + }); + return result; +} + +const ReflectedClass* Runtime::reflectedClass(const std::string& name) const noexcept { + auto found = classes_.find(name); + return found == classes_.end() ? nullptr : &found->second; +} + +std::vector Runtime::reflectedClasses() const { + std::vector result; + result.reserve(classes_.size()); + for (const auto& pair : classes_) result.push_back(pair.second); + std::sort(result.begin(), result.end(), [](const ReflectedClass& a, const ReflectedClass& b) { + return a.name < b.name; + }); + return result; +} + +ssq::Class Runtime::findClass(const std::string& name) const { return vm_->findClass(name.c_str()); } + +void Runtime::notifyLifecycle(const ScriptInfo& info) noexcept { + if (!lifecycle_handler_) return; + try { + lifecycle_handler_(info); + } catch (...) { + } +} + +[[noreturn]] void Runtime::fail(ScriptStage stage, const std::string& source, ScriptId id, + const std::exception& error) { + ScriptException wrapped(stage, source, id, error.what()); + if (error_handler_) { + try { + error_handler_(wrapped); + } catch (...) { + } + } + throw wrapped; +} + +std::unordered_map Runtime::rootClasses() const { + std::unordered_map result; + StackGuard stack(*const_cast(this)); + HSQUIRRELVM squirrel = handle(); + sq_pushroottable(squirrel); + sq_pushnull(squirrel); + while (SQ_SUCCEEDED(sq_next(squirrel, -2))) { + if (sq_gettype(squirrel, -2) == OT_STRING && sq_gettype(squirrel, -1) == OT_CLASS) { + const SQChar* name = nullptr; + HSQOBJECT object; + if (SQ_SUCCEEDED(sq_getstring(squirrel, -2, &name)) && name && + SQ_SUCCEEDED(sq_getstackobj(squirrel, -1, &object))) + result[name] = objectIdentity(object); + } + sq_pop(squirrel, 2); + } + return result; +} + +void Runtime::discoverClasses( + ScriptRecord& record, const std::unordered_map& before) { + const auto after = rootClasses(); + for (const auto& pair : after) { + auto old = before.find(pair.first); + if (old != before.end() && old->second == pair.second) continue; + ssq::Class cls = vm_->findClass(pair.first.c_str()); + record.class_objects.insert_or_assign(pair.first, cls); + if (std::find(record.info.classes.begin(), record.info.classes.end(), pair.first) == + record.info.classes.end()) + record.info.classes.push_back(pair.first); + classes_[pair.first] = inspectClass(pair.first, cls, record.info.source); + class_owners_[pair.first] = record.info.id; + } + std::sort(record.info.classes.begin(), record.info.classes.end()); + + // Resolve base class names after every newly defined class is registered. + for (const std::string& name : record.info.classes) { + auto info = classes_.find(name); + auto cls = record.class_objects.find(name); + if (info == classes_.end() || cls == record.class_objects.end()) continue; + StackGuard stack(*this); + sq_pushobject(handle(), cls->second.getRaw()); + if (SQ_SUCCEEDED(sq_getbase(handle(), -1)) && sq_gettype(handle(), -1) == OT_CLASS) { + HSQOBJECT baseObject; + if (SQ_SUCCEEDED(sq_getstackobj(handle(), -1, &baseObject))) { + const SQUserPointer identity = objectIdentity(baseObject); + for (const auto& candidate : after) { + if (candidate.second == identity) { + info->second.base = candidate.first; + break; + } + } + } + } + } +} + +ReflectedClass Runtime::inspectClass(const std::string& name, const ssq::Class& cls, + const std::string& source) const { + ReflectedClass info; + info.name = name; + info.source = source; + StackGuard stack(*const_cast(this)); + HSQUIRRELVM squirrel = handle(); + sq_pushobject(squirrel, cls.getRaw()); + sq_pushnull(squirrel); + while (SQ_SUCCEEDED(sq_next(squirrel, -2))) { + if (sq_gettype(squirrel, -2) == OT_STRING) { + const SQChar* memberName = nullptr; + if (SQ_SUCCEEDED(sq_getstring(squirrel, -2, &memberName)) && memberName) { + ReflectedMember member; + member.name = memberName; + member.type = static_cast(sq_gettype(squirrel, -1)); + member.method = member.type == ssq::Type::CLOSURE || + member.type == ssq::Type::NATIVECLOSURE; + + // sq_getattributes consumes the key at the stack top and replaces it + // with the attribute table (or null). + const SQInteger memberTop = sq_gettop(squirrel); + sq_push(squirrel, -2); + if (SQ_SUCCEEDED(sq_getattributes(squirrel, -5)) && + sq_gettype(squirrel, -1) == OT_TABLE) { + sq_pushnull(squirrel); + while (SQ_SUCCEEDED(sq_next(squirrel, -2))) { + if (sq_gettype(squirrel, -2) == OT_STRING) { + const SQChar* attributeName = nullptr; + if (SQ_SUCCEEDED(sq_getstring(squirrel, -2, &attributeName)) && + attributeName) { + ReflectedAttribute attribute; + attribute.name = attributeName; + attribute.type = + static_cast(sq_gettype(squirrel, -1)); + attribute.value = valueString(squirrel, -1); + member.attributes.push_back(std::move(attribute)); + } + } + sq_pop(squirrel, 2); + } + } + sq_settop(squirrel, memberTop); + std::sort(member.attributes.begin(), member.attributes.end(), + [](const ReflectedAttribute& a, const ReflectedAttribute& b) { + return a.name < b.name; + }); + info.members.push_back(std::move(member)); + } + } + sq_pop(squirrel, 2); + } + std::sort(info.members.begin(), info.members.end(), + [](const ReflectedMember& a, const ReflectedMember& b) { return a.name < b.name; }); + return info; +} + +} // namespace eve diff --git a/src/engine/common/Runtime.h b/src/engine/common/Runtime.h index e69de29b..2a742f00 100644 --- a/src/engine/common/Runtime.h +++ b/src/engine/common/Runtime.h @@ -0,0 +1,185 @@ +#pragma once + +#include "common/Export.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace eve { + +enum class ScriptState { + Compiled, + Running, + Loaded, + Unloading, + Unloaded, + Failed, +}; + +enum class ScriptStage { + Compile, + Execute, + Reflect, + Unload, + Shutdown, +}; + +/** Exception raised at the public Runtime boundary. */ +class EVENGINE_API ScriptException : public std::runtime_error { +public: + ScriptException(ScriptStage stage, std::string source, uint64_t scriptId, + const std::string& message); + + ScriptStage stage() const noexcept { return stage_; } + const std::string& source() const noexcept { return source_; } + uint64_t scriptId() const noexcept { return script_id_; } + +private: + ScriptStage stage_; + std::string source_; + uint64_t script_id_; +}; + +struct EVENGINE_API ReflectedAttribute { + std::string name; + ssq::Type type = ssq::Type::NULLPTR; + std::string value; +}; + +struct EVENGINE_API ReflectedMember { + std::string name; + ssq::Type type = ssq::Type::NULLPTR; + bool method = false; + std::vector attributes; +}; + +struct EVENGINE_API ReflectedClass { + std::string name; + std::string source; + std::string base; + std::vector members; +}; + +struct EVENGINE_API ScriptInfo { + uint64_t id = 0; + std::string source; + ScriptState state = ScriptState::Compiled; + std::vector classes; + std::string error; +}; + +class EVENGINE_API Runtime { +public: + using ScriptId = uint64_t; + using ErrorHandler = std::function; + using LifecycleHandler = std::function; + + /** Restores the native Squirrel stack when a binding operation leaves scope. */ + class EVENGINE_API StackGuard { + public: + explicit StackGuard(Runtime& runtime) noexcept; + ~StackGuard(); + StackGuard(StackGuard&& other) noexcept; + StackGuard& operator=(StackGuard&& other) noexcept; + StackGuard(const StackGuard&) = delete; + StackGuard& operator=(const StackGuard&) = delete; + + int top() const noexcept { return top_; } + void dismiss() noexcept { vm_ = nullptr; } + + private: + HSQUIRRELVM vm_ = nullptr; + SQInteger top_ = 0; + }; + + /** Pushes this Runtime on the current thread's runtime stack. */ + class EVENGINE_API Scope { + public: + explicit Scope(Runtime& runtime); + ~Scope(); + Scope(Scope&& other) noexcept; + Scope& operator=(Scope&& other) = delete; + Scope(const Scope&) = delete; + Scope& operator=(const Scope&) = delete; + + private: + Runtime* runtime_ = nullptr; + }; + + explicit Runtime(size_t stackSize = 2048, ssq::Libs::Flag libraries = ssq::Libs::ALL); + ~Runtime(); + Runtime(Runtime&&) = delete; + Runtime& operator=(Runtime&&) = delete; + Runtime(const Runtime&) = delete; + Runtime& operator=(const Runtime&) = delete; + + /** Exposes registered engine modules. Safe to call more than once. */ + void initialize(); + void shutdown() noexcept; + bool initialized() const noexcept; + + ssq::VM& vm() noexcept; + const ssq::VM& vm() const noexcept; + HSQUIRRELVM handle() const noexcept; + ssq::Table root() const; + ssq::Table table(const char* name) const; + + StackGuard guard() noexcept { return StackGuard(*this); } + Scope enter() { return Scope(*this); } + static Runtime* current() noexcept; + static size_t stackDepth() noexcept; + + ScriptId compileSource(std::string source, std::string sourceName = "buffer"); + ScriptId compileFile(const std::string& path); + const ScriptInfo& execute(ScriptId id); + ScriptId runSource(std::string source, std::string sourceName = "buffer"); + ScriptId runFile(const std::string& path); + ScriptId reflectFile(const std::string& path) { return runFile(path); } + const ReflectedClass& reflectClass(const std::string& name, + const std::string& source = {}); + ScriptId reload(ScriptId id); + bool unload(ScriptId id); + void unloadAll() noexcept; + + bool contains(ScriptId id) const noexcept; + const ScriptInfo* script(ScriptId id) const noexcept; + std::vector scripts() const; + const ReflectedClass* reflectedClass(const std::string& name) const noexcept; + std::vector reflectedClasses() const; + ssq::Class findClass(const std::string& name) const; + + void setErrorHandler(ErrorHandler handler) { error_handler_ = std::move(handler); } + void setLifecycleHandler(LifecycleHandler handler) { lifecycle_handler_ = std::move(handler); } + +private: + struct ScriptRecord; + + std::unique_ptr vm_; + std::unordered_map> scripts_; + std::unordered_map classes_; + std::unordered_map class_owners_; + ScriptId next_script_id_ = 1; + bool initialized_ = false; + bool shutting_down_ = false; + bool stopped_ = false; + ErrorHandler error_handler_; + LifecycleHandler lifecycle_handler_; + + void notifyLifecycle(const ScriptInfo& info) noexcept; + [[noreturn]] void fail(ScriptStage stage, const std::string& source, ScriptId id, + const std::exception& error); + void discoverClasses(ScriptRecord& record, + const std::unordered_map& before); + std::unordered_map rootClasses() const; + ReflectedClass inspectClass(const std::string& name, const ssq::Class& cls, + const std::string& source) const; +}; + +} // namespace eve diff --git a/test/runtime.cpp b/test/runtime.cpp new file mode 100644 index 00000000..48f352a3 --- /dev/null +++ b/test/runtime.cpp @@ -0,0 +1,80 @@ +#include "common/Runtime.h" + +#include "zeroerr/unittest.h" + +#include + +using namespace eve; + +TEST_CASE("runtime.scopeAndNativeStackAreRestored") { + Runtime runtime(256, ssq::Libs::ALL); + CHECK(Runtime::current() == nullptr); + const SQInteger top = runtime.vm().getTop(); + { + auto scope = runtime.enter(); + CHECK(Runtime::current() == &runtime); + CHECK_EQ(Runtime::stackDepth(), size_t(1)); + { + auto guard = runtime.guard(); + sq_pushinteger(runtime.handle(), 42); + CHECK_EQ(runtime.vm().getTop(), top + 1); + } + CHECK_EQ(runtime.vm().getTop(), top); + } + CHECK(Runtime::current() == nullptr); +} + +TEST_CASE("runtime.reflectsAndUnloadsScriptClasses") { + Runtime runtime(512, ssq::Libs::ALL); + const char* source = R"SQ( +class RuntimeProbe { + + speed = 3 + function update(dt) { speed += dt } +} +)SQ"; + + const Runtime::ScriptId id = runtime.runSource(source, "runtime-probe.nut"); + const ScriptInfo* script = runtime.script(id); + CHECK(script != nullptr); + CHECK(script->state == ScriptState::Loaded); + CHECK(std::find(script->classes.begin(), script->classes.end(), "RuntimeProbe") != + script->classes.end()); + + const ReflectedClass* cls = runtime.reflectedClass("RuntimeProbe"); + CHECK(cls != nullptr); + CHECK_EQ(cls->source, std::string("runtime-probe.nut")); + auto speed = std::find_if(cls->members.begin(), cls->members.end(), + [](const ReflectedMember& member) { return member.name == "speed"; }); + CHECK(speed != cls->members.end()); + CHECK(!speed->method); + CHECK(std::find_if(speed->attributes.begin(), speed->attributes.end(), + [](const ReflectedAttribute& attribute) { + return attribute.name == "editor" && attribute.value == "slider"; + }) != speed->attributes.end()); + + CHECK(runtime.unload(id)); + CHECK(runtime.reflectedClass("RuntimeProbe") == nullptr); + bool removed = false; + try { + runtime.findClass("RuntimeProbe"); + } catch (const ssq::NotFoundException&) { + removed = true; + } + CHECK(removed); +} + +TEST_CASE("runtimeWrapsCompileErrorsAndRestoresStack") { + Runtime runtime(256, ssq::Libs::ALL); + const SQInteger top = runtime.vm().getTop(); + bool caught = false; + try { + runtime.compileSource("class Broken {", "broken.nut"); + } catch (const ScriptException& error) { + caught = true; + CHECK(error.stage() == ScriptStage::Compile); + CHECK_EQ(error.source(), std::string("broken.nut")); + } + CHECK(caught); + CHECK_EQ(runtime.vm().getTop(), top); +} From a6901a0691248eaeffdbd556e6156540a4eb0ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Fri, 14 Aug 2026 23:05:42 +0800 Subject: [PATCH 04/21] feat(android): run zeroerr unit_test suite as an Android test app Adds EVENGINE_ANDROID_TEST_APP build mode: the unit_test sources are compiled into libmain.so (SDL entry) and packaged by the existing Gradle APK along with the test assets, so the full zeroerr suite can run on a device via adb logcat. - test/main.cpp: Android runner with LogcatBuf (cout/cerr -> logcat), chdir to /evengine_test, SDL_Init; keeps SDL_main symbol unmangled. - test/PathBesideSource.h: shared pathBesideThisSource helper that maps __FILE__ to the on-device unpacked tree on Android. - EVTestActivity: unpacks assets/test + assets/examples to internal storage and passes an optional --testcase filter from an intent extra. - test/CMakeLists.txt: builds `main` instead of unit_test on Android, excludes desktop-only devtools tests, links Android system libs. - Makefile: build/android-debug-test, sync/android-test-assets, run/android-test-debug, log/android-test targets. - Gradle wrapper bumped to 8.5 for Java 21 compatibility. --- .gitignore | 3 + CMakeLists.txt | 12 +- COMMIT_MSG.txt | 18 +++ Makefile | 63 +++++++++- .../apk/app/src/main/AndroidManifest.xml | 2 +- .../com/evengine/example/EVTestActivity.java | 114 ++++++++++++++++++ .../gradle/wrapper/gradle-wrapper.properties | 2 +- src/engine/CMakeLists.txt | 16 ++- test/CMakeLists.txt | 78 ++++++++++-- test/ClassicScenes.cpp | 8 +- test/IBL.cpp | 8 +- test/PathBesideSource.h | 65 ++++++++++ test/RenderImageAudit.cpp | 8 +- test/animation_skinned.cpp | 8 +- test/audio.cpp | 8 +- test/font.cpp | 8 +- test/graphics_font.cpp | 8 +- test/main.cpp | 88 +++++++++++++- test/model3d.cpp | 8 +- test/particles_attach_extra.cpp | 8 +- test/particles_attach_more.cpp | 8 +- test/particles_attach_skin.cpp | 8 +- test/sound.cpp | 8 +- 23 files changed, 463 insertions(+), 94 deletions(-) create mode 100644 COMMIT_MSG.txt create mode 100644 platform/android/apk/app/src/main/java/com/evengine/example/EVTestActivity.java create mode 100644 test/PathBesideSource.h diff --git a/.gitignore b/.gitignore index b3f34dd6..1cd3248b 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ platform/android/apk/app/src/main/jniLibs/ # Filled by `make sync/android-assets` (source: platform/android/game-shell or example/). platform/android/apk/app/src/main/assets/game/* !platform/android/apk/app/src/main/assets/game/.gitkeep +# Filled by `make sync/android-test-assets` (zeroerr suite + examples on-device tree). +platform/android/apk/app/src/main/assets/test/ +platform/android/apk/app/src/main/assets/examples/ diff --git a/CMakeLists.txt b/CMakeLists.txt index fd87a1bc..4350becf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,8 +7,12 @@ set(EVENGINE_PATCH_VERSION "0") option(BUILD_TESTING "enable testing" ON) option(EVENGINE_BUILD_DEMO "Build optional Demo module (procedural demo assets + default demo.nut)" ON) +option(EVENGINE_ANDROID_TEST_APP + "Build the unit_test suite as an Android test app (libmain.so entry replaced by the zeroerr runner)" + OFF) message("Build UnitTest: " ${BUILD_TESTING}) message("Build Demo module: " ${EVENGINE_BUILD_DEMO}) +message("Build Android test app: " ${EVENGINE_ANDROID_TEST_APP}) if (CMAKE_BUILD_TYPE STREQUAL "") set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "The build type (Debug, Release)") endif() @@ -46,8 +50,12 @@ function(main) set(USE_MOLD OFF CACHE BOOL "" FORCE) set(COLORFUL_OUTPUT OFF CACHE STRING "" FORCE) if(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) - set(BUILD_TESTING OFF CACHE BOOL "enable testing" FORCE) - message(STATUS "Android build: unit tests disabled") + if(EVENGINE_ANDROID_TEST_APP) + message(STATUS "Android build: unit tests enabled (Android test app)") + else() + set(BUILD_TESTING OFF CACHE BOOL "enable testing" FORCE) + message(STATUS "Android build: unit tests disabled") + endif() endif() if(CMAKE_SYSTEM_NAME STREQUAL "iOS") set(BUILD_TESTING OFF CACHE BOOL "enable testing" FORCE) diff --git a/COMMIT_MSG.txt b/COMMIT_MSG.txt new file mode 100644 index 00000000..472b5ee6 --- /dev/null +++ b/COMMIT_MSG.txt @@ -0,0 +1,18 @@ +feat(android): run zeroerr unit_test suite as an Android test app + +Adds EVENGINE_ANDROID_TEST_APP build mode: the unit_test sources are +compiled into libmain.so (SDL entry) and packaged by the existing Gradle +APK along with the test assets, so the full zeroerr suite can run on a +device via adb logcat. + +- test/main.cpp: Android runner with LogcatBuf (cout/cerr -> logcat), + chdir to /evengine_test, SDL_Init; keeps SDL_main symbol unmangled. +- test/PathBesideSource.h: shared pathBesideThisSource helper that maps + __FILE__ to the on-device unpacked tree on Android. +- EVTestActivity: unpacks assets/test + assets/examples to internal + storage and passes an optional --testcase filter from an intent extra. +- test/CMakeLists.txt: builds `main` instead of unit_test on Android, + excludes desktop-only devtools tests, links Android system libs. +- Makefile: build/android-debug-test, sync/android-test-assets, + run/android-test-debug, log/android-test targets. +- Gradle wrapper bumped to 8.5 for Java 21 compatibility. diff --git a/Makefile b/Makefile index 1d19d659..30be8ff2 100644 --- a/Makefile +++ b/Makefile @@ -100,11 +100,13 @@ GAME ?= .PHONY: all build/win32 build/linux build/macosx build/android build/ios \ build/win32-debug build/linux-debug build/macosx-debug build/android-debug build/ios-debug \ + build/android-debug-test \ wsl/linux wsl/linux-debug show-targets \ debug release example \ run run/win32 run/linux run/macosx \ run/win32-debug run/linux-debug run/macosx-debug \ - sync/android-libs sync/android-assets install/android-debug run/android-debug log/android \ + sync/android-libs sync/android-assets sync/android-test-assets install/android-debug run/android-debug log/android \ + run/android-test-debug log/android-test \ install/ios-debug run/ios-debug log/ios \ sdk/win32 sdk/linux sdk/macosx sdk/android sdk/ios \ sdk/win32-debug sdk/linux-debug sdk/macosx-debug sdk/android-debug sdk/ios-debug \ @@ -340,9 +342,16 @@ reinstall/third-party/ios-debug: cmake --build build/third-party/ios-debug --target install -j 8 # Copy native shared libraries into the Gradle jniLibs tree. +# In Android test-app mode the runner lives in test/libmain.so (zeroerr suite); +# otherwise it is the engine's src/engine/libmain.so. sync/android-libs: mkdir -p $(JNI_LIBS) - cp -f $(BUILD_DIR)/src/engine/libmain.so $(JNI_LIBS)/ + @if [ -f $(BUILD_DIR)/test/libmain.so ]; then \ + echo "syncing test runner libmain.so"; \ + cp -f $(BUILD_DIR)/test/libmain.so $(JNI_LIBS)/; \ + else \ + cp -f $(BUILD_DIR)/src/engine/libmain.so $(JNI_LIBS)/; \ + fi @TP_DIR=build/third-party-binary/$$(basename $(BUILD_DIR)); \ if [ ! -d "$$TP_DIR/lib" ]; then TP_DIR=build/third-party-binary/android-debug; fi; \ if [ ! -d "$$TP_DIR/lib" ]; then TP_DIR=build/third-party-binary/android; fi; \ @@ -373,6 +382,16 @@ sync/android-assets: echo "Synced Android game assets ($$src) -> $(ANDROID_ASSETS_GAME)"; \ ls -la "$(ANDROID_ASSETS_GAME)" +# Populate APK assets/test + assets/examples for the Android test app +# (EVTestActivity unpacks them to /evengine_test at first launch). +sync/android-test-assets: + @rm -rf "$(APK_DIR)/app/src/main/assets/test" "$(APK_DIR)/app/src/main/assets/examples" "$(APK_DIR)/app/src/main/assets/game" + @mkdir -p "$(APK_DIR)/app/src/main/assets/test" "$(APK_DIR)/app/src/main/assets/examples" + cp -R test/. "$(APK_DIR)/app/src/main/assets/test/" + cp -R examples/. "$(APK_DIR)/app/src/main/assets/examples/" + @echo "Synced Android test assets -> $(APK_DIR)/app/src/main/assets/{test,examples}" + ls -la "$(APK_DIR)/app/src/main/assets/test" | head -20 + install/android-debug: $(ANDROID_SDK)/platform-tools/adb install -r \ $(APK_DIR)/app/build/outputs/apk/debug/app-debug.apk @@ -383,6 +402,46 @@ run/android-debug: install/android-debug log/android: $(ANDROID_SDK)/platform-tools/adb logcat -s EVEngineActivity:I SDL:V SDL/APP:V vulkan:V libc:F DEBUG:F +# ---- Android test app (zeroerr suite as libmain.so) ---- +# make build/android-debug-test # configure + deps + engine/tests + APK +# make run/android-test-debug [FILTER=math.*] # install + launch on device +# make log/android-test # stream test results from logcat +build/android-debug-test: build/android-debug-test/build.ninja + cmake --build $@ --target deps -j $(ANDROID_JOBS) + cmake --build $@ -j $(ANDROID_JOBS) + $(MAKE) sync/android-libs BUILD_DIR=build/android-debug-test + $(MAKE) sync/android-test-assets + cd $(APK_DIR) && JAVA_HOME="$(JAVA_HOME)" ANDROID_HOME="$(ANDROID_SDK)" ./gradlew assembleDebug + +build/android-debug-test/build.ninja: + cmake -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE=$(ANDROID_NDK)/build/cmake/android.toolchain.cmake \ + -DANDROID_ABI=$(ANDROID_ABI) \ + -DANDROID_PLATFORM=$(ANDROID_PLATFORM) \ + -DANDROID_STL=$(ANDROID_STL) \ + -DCMAKE_BUILD_TYPE=Debug \ + -DBUILD_PLATFORM=android \ + -DBUILD_TESTING=ON \ + -DEVENGINE_ANDROID_TEST_APP=ON \ + -DEVENGINE_DOWNLOAD_CLASSIC_SCENES=OFF \ + -DEVENGINE_DOWNLOAD_SPINE_MODELS=OFF \ + -DEVENGINE_DOWNLOAD_SKINNED_CHARACTER=OFF \ + $(CMAKE_EXTRA_ARGS) \ + -B build/android-debug-test -S . + +# Convenience: reuse the game APK's install/run with the test filter extra. +FILTER ?= +run/android-test-debug: install/android-debug + @if [ -n "$(FILTER)" ]; then \ + $(ANDROID_SDK)/platform-tools/adb shell am start -n com.evengine.example/.EVTestActivity --es evengine.test.filter "$(FILTER)"; \ + else \ + $(ANDROID_SDK)/platform-tools/adb shell am start -n com.evengine.example/.EVTestActivity; \ + fi + +log/android-test: + $(ANDROID_SDK)/platform-tools/adb logcat -c + $(ANDROID_SDK)/platform-tools/adb logcat -s EVEngineTest:V SDL:V vulkan:V libc:F DEBUG:F + # Install Debug .app to the first connected physical iOS/iPadOS device. install/ios-debug: @test -n "$$(security find-identity -v -p codesigning 2>/dev/null | sed -n '/Apple Development/p')" \ diff --git a/platform/android/apk/app/src/main/AndroidManifest.xml b/platform/android/apk/app/src/main/AndroidManifest.xml index bca3cb03..fcb71b9b 100644 --- a/platform/android/apk/app/src/main/AndroidManifest.xml +++ b/platform/android/apk/app/src/main/AndroidManifest.xml @@ -16,7 +16,7 @@ android:hardwareAccelerated="true"> /evengine_test}) and launches the zeroerr runner built + * into {@code libmain.so}. SDL_main chdirs to that tree, so the compile-time + * {@code EVENGINE_SOURCE_DIR="."} / {@code EVENGINE_TEST_BINARY_DIR="."} paths + * resolve on-device. A test filter can be passed as the + * {@code evengine.test.filter} intent extra, e.g. + * {@code adb shell am start -n com.evengine.example/.EVTestActivity --es evengine.test.filter "math.*"}. + */ +public class EVTestActivity extends SDLActivity { + private static final String TAG = "EVTestActivity"; + private String testDir; + + @Override + protected void onCreate(Bundle savedInstanceState) { + testDir = new File(getFilesDir(), "evengine_test").getAbsolutePath(); + try { + // Wipe first so assets removed from the package do not linger across upgrades. + deleteRecursive(new File(testDir)); + copyAssetDir("test", testDir); + copyAssetDir("examples", testDir + "/examples"); + Log.i(TAG, "Test assets ready at " + testDir); + } catch (IOException e) { + Log.e(TAG, "Failed to unpack test assets", e); + } + super.onCreate(savedInstanceState); + } + + @Override + protected String[] getLibraries() { + return new String[] { + "SDL2", + "c++_shared", + "main" + }; + } + + @Override + protected String[] getArguments() { + if (getIntent() != null) { + String filter = getIntent().getStringExtra("evengine.test.filter"); + if (filter != null && !filter.isEmpty()) { + return new String[] { "--testcase=" + filter }; + } + } + return new String[0]; + } + + private void copyAssetDir(String assetPath, String destPath) throws IOException { + AssetManager am = getAssets(); + String[] children = am.list(assetPath); + File dest = new File(destPath); + if (!dest.exists() && !dest.mkdirs()) { + throw new IOException("Cannot create " + destPath); + } + if (children == null || children.length == 0) { + // Leaf file + copyAssetFile(assetPath, destPath); + return; + } + for (String child : children) { + String childAsset = assetPath.isEmpty() ? child : assetPath + "/" + child; + String childDest = destPath + "/" + child; + String[] grand = am.list(childAsset); + if (grand != null && grand.length > 0) { + copyAssetDir(childAsset, childDest); + } else { + copyAssetFile(childAsset, childDest); + } + } + } + + private void copyAssetFile(String assetPath, String destPath) throws IOException { + File outFile = new File(destPath); + File parent = outFile.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) { + throw new IOException("Cannot create parent for " + destPath); + } + try (InputStream in = getAssets().open(assetPath); + OutputStream out = new FileOutputStream(outFile)) { + byte[] buf = new byte[16 * 1024]; + int n; + while ((n = in.read(buf)) > 0) { + out.write(buf, 0, n); + } + } + } + + private static void deleteRecursive(File f) { + if (f == null || !f.exists()) return; + File[] children = f.listFiles(); + if (children != null) { + for (File c : children) deleteRecursive(c); + } + //noinspection ResultOfMethodCallIgnored + f.delete(); + } +} diff --git a/platform/android/apk/gradle/wrapper/gradle-wrapper.properties b/platform/android/apk/gradle/wrapper/gradle-wrapper.properties index 62f495df..1af9e093 100644 --- a/platform/android/apk/gradle/wrapper/gradle-wrapper.properties +++ b/platform/android/apk/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index e232d76d..e391cbf4 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -10,8 +10,12 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/common/config.h.in common/config.h) set(EVENGINE_IOS_DEPLOYMENT_TARGET "13.0" CACHE STRING "Minimum iOS/iPadOS deployment target") if(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) - # SDLActivity loads libmain.so and calls SDL_main (via SDL_main.h macro on main). - add_library(main SHARED main.cpp) + if(NOT EVENGINE_ANDROID_TEST_APP) + # SDLActivity loads libmain.so and calls SDL_main (via SDL_main.h macro on main). + # In Android test-app mode the shared `main` library is built by test/CMakeLists.txt + # from the zeroerr unit-test runner instead of the engine main.cpp. + add_library(main SHARED main.cpp) + endif() set(EVENGINE_NATIVE_TARGET main CACHE INTERNAL "Host target for SDK install") elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS) # UIKit SDL entry: MACOSX_BUNDLE .app with SDL_main. @@ -199,7 +203,8 @@ if(EVENGINE_PLATFORM_LIB) list(APPEND EVELIBS ${EVENGINE_PLATFORM_LIB}) endif() -if(CMAKE_GENERATOR MATCHES "Xcode") +if(NOT EVENGINE_ANDROID_TEST_APP) + if(CMAKE_GENERATOR MATCHES "Xcode") # create_module() emits STATIC libraries here, so the linker would drop the # translation units whose only job is to self-register a module or a # subcommand at static-init time. Pull the archives in whole. @@ -208,8 +213,9 @@ if(CMAKE_GENERATOR MATCHES "Xcode") list(APPEND _eve_whole_archive_libs "$") endforeach() target_link_libraries(${EVENGINE_NATIVE_TARGET} ${_eve_whole_archive_libs} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB}) -else() + else() target_link_libraries(${EVENGINE_NATIVE_TARGET} ${EVELIBS} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB}) + endif() endif() if(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS) @@ -300,6 +306,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS) endif() endif() +if(NOT EVENGINE_ANDROID_TEST_APP) # Plugins resolve ModuleManager / ModuleRegister symbols from the host process. # Export surface is EVENGINE_API (__declspec(dllexport)) only — do not enable # WINDOWS_EXPORT_ALL_SYMBOLS: it pulls Poco/ImGui template instantiations into @@ -326,3 +333,4 @@ elseif(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) VISIBILITY_INLINES_HIDDEN OFF ) endif() +endif() diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1e494cb2..5bf14d46 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -103,6 +103,16 @@ if(EVENGINE_BUILD_DEMO) list(APPEND all_test_cpp demo.cpp) endif() +# Desktop-only DevTools (DAP / MCP / debugger / callgraph / renderflow) are not +# compiled into the Android runtime, so their test files cannot link there. +# all_test_cpp holds absolute paths from file(GLOB), so filter by basename. +if(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) + foreach(_eve_devtool_test IN ITEMS callgraph.cpp renderflow.cpp debugger.cpp dap.cpp mcp.cpp) + list(FILTER all_test_cpp EXCLUDE REGEX "/${_eve_devtool_test}$") + endforeach() + message(STATUS "Android test: removed devtools test files, remaining=${all_test_cpp}") +endif() + file(GLOB all_test_scripts filesystem.nut graphic.nut @@ -119,15 +129,36 @@ add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/nut_scripts.cpp WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) -add_executable( - unit_test - ${all_test_cpp} - ${CMAKE_CURRENT_BINARY_DIR}/nut_scripts.cpp -) +# On Android the zeroerr runner is packaged as the app's native entry point +# (libmain.so loaded by SDLActivity), replacing the engine main.cpp. +if(EVENGINE_ANDROID_TEST_APP) + add_library(main SHARED + ${all_test_cpp} + ${CMAKE_CURRENT_BINARY_DIR}/nut_scripts.cpp + ) +else() + add_executable( + unit_test + ${all_test_cpp} + ${CMAKE_CURRENT_BINARY_DIR}/nut_scripts.cpp + ) +endif() if(WIN32) set(SystemLibraries ${SystemLibraries} winmm version imm32 Setupapi ws2_32 iphlpapi shlwapi) +elseif(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) + find_library(ANDROID_LOG_LIB log) + find_library(ANDROID_LIB android) + find_library(ANDROID_OPENSLES_LIB OpenSLES) + set(SystemLibraries ${SystemLibraries} + ${ANDROID_LOG_LIB} + ${ANDROID_LIB} + ${ANDROID_OPENSLES_LIB} + dl + m + c++_shared + ) elseif(APPLE) set(SystemLibraries ${SystemLibraries} dl pthread iconv @@ -191,6 +222,11 @@ else() set(ThirdParty ${ThirdParty} simplesquirrel_staticmd sqstdlib_staticmd squirrel_staticmd SDL2 PocoDataSQLitemd PocoDatamd PocoFoundationmd PocoNetmd PocoJSONmd PocoXMLmd ${SystemLibraries}) + elseif(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) + # Same unprefixed static names as the engine Android build; SDL2 stays shared. + set(ThirdParty ${ThirdParty} simplesquirrel_static sqstdlib_static squirrel_static SDL2 SDL2main + PocoDataSQLite PocoData PocoNet PocoXML PocoJSON PocoFoundation + ${SystemLibraries}) else() set(ThirdParty ${ThirdParty} simplesquirrel_static sqstdlib_static squirrel_static SDL2 PocoDataSQLite PocoData PocoNet PocoXML PocoJSON PocoFoundation @@ -209,7 +245,6 @@ set(EVELIBS EVTouch EVGraphics EVImage - EVDevTools EVCommon EVEvent EVTimer @@ -245,6 +280,10 @@ set(EVELIBS EVStylize EVEditor) +if(NOT (ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android"))) + list(APPEND EVELIBS EVDevTools) +endif() + if(EVENGINE_BUILD_DEMO) list(APPEND EVELIBS EVDemo) @@ -254,14 +293,30 @@ if(EVENGINE_PLATFORM_LIB) list(APPEND EVELIBS ${EVENGINE_PLATFORM_LIB}) endif() -target_link_libraries(unit_test ${EVELIBS} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB}) -target_compile_definitions(unit_test PRIVATE - EVENGINE_TEST_BINARY_DIR="${CMAKE_CURRENT_BINARY_DIR}" - EVENGINE_SOURCE_DIR="${CMAKE_SOURCE_DIR}") +if(EVENGINE_ANDROID_TEST_APP) + target_link_libraries(main ${EVELIBS} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB}) + # Source/binary roots are resolved at runtime: the Android test runner chdirs to + # the unpacked asset root before running, so "." means "repo root on device". + target_compile_definitions(main PRIVATE + EVENGINE_TEST_BINARY_DIR="." + EVENGINE_SOURCE_DIR="." + EVENGINE_ANDROID=1) + # Keep SDL_main / module registration symbols visible for the dlopened SDLActivity. + set_target_properties(main PROPERTIES + CXX_VISIBILITY_PRESET default + VISIBILITY_INLINES_HIDDEN OFF) +else() + target_link_libraries(unit_test ${EVELIBS} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB}) + target_compile_definitions(unit_test PRIVATE + EVENGINE_TEST_BINARY_DIR="${CMAKE_CURRENT_BINARY_DIR}" + EVENGINE_SOURCE_DIR="${CMAKE_SOURCE_DIR}") +endif() # Large classic scenes (Sponza / DamagedHelmet). Cornell Box is committed. # Enable with -DEVENGINE_DOWNLOAD_CLASSIC_SCENES=ON, or build target # `download_classic_scenes` before running ClassicScenes.* tests. +# (Skipped on the Android test app: bash/python3 download steps do not run there.) +if(NOT EVENGINE_ANDROID_TEST_APP) option(EVENGINE_DOWNLOAD_CLASSIC_SCENES "Download large classic render test scenes (Sponza, DamagedHelmet)" OFF) set(_eve_classic_scenes_script "${CMAKE_SOURCE_DIR}/scripts/download_classic_scenes.sh") @@ -326,6 +381,9 @@ if(EVENGINE_DOWNLOAD_SKINNED_CHARACTER) COMMENT "PRE_BUILD: download skinned character (CesiumMan)" VERBATIM) endif() +endif() # NOT EVENGINE_ANDROID_TEST_APP +if(NOT EVENGINE_ANDROID_TEST_APP) include(${CMAKE_SOURCE_DIR}/cmake/ZeroErrDiscoverTests.cmake) zeroerr_discover_tests(unit_test) +endif() diff --git a/test/ClassicScenes.cpp b/test/ClassicScenes.cpp index e3b1cf9f..82e49556 100644 --- a/test/ClassicScenes.cpp +++ b/test/ClassicScenes.cpp @@ -46,12 +46,8 @@ using namespace eve::graphics; namespace { -std::string pathBesideThisSource(const char *filename) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + filename; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() std::vector readBinaryFile(const std::string &path) { std::ifstream in(path, std::ios::binary); diff --git a/test/IBL.cpp b/test/IBL.cpp index 970106a2..0e6b5c81 100644 --- a/test/IBL.cpp +++ b/test/IBL.cpp @@ -24,12 +24,8 @@ using namespace eve::graphics; namespace { -std::string pathBesideThisSource(const char *filename) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + filename; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() std::vector readBinaryFile(const std::string &path) { std::ifstream in(path, std::ios::binary); diff --git a/test/PathBesideSource.h b/test/PathBesideSource.h new file mode 100644 index 00000000..32be9c1e --- /dev/null +++ b/test/PathBesideSource.h @@ -0,0 +1,65 @@ +#pragma once +// test/PathBesideSource.h +// +// Shared helper for resolving test asset paths relative to the test source file. +// On desktop the path is dirname(__FILE__); on Android the build-host path baked +// into __FILE__ is replaced with the on-device unpacked tree root so that +// `pathBesideThisSource("assets/...")` resolves to /evengine_test/assets/... +// +// Deliberately uses its own `eve_test_path` namespace (NOT `eve`): this header is +// included *inside* the anonymous namespaces of several test files, so introducing +// `namespace eve` there would shadow the global ::eve and break every +// `eve::window::...` / `eve::image::...` reference in those files. + +#include + +#if defined(EVENGINE_ANDROID) +#include +#endif + +namespace eve_test_path { + +#if defined(EVENGINE_ANDROID) +inline std::string androidTestRoot() { + static const std::string root = [] { + const char *internal = SDL_AndroidGetInternalStoragePath(); + return internal ? std::string(internal) + "/evengine_test" : std::string(); + }(); + return root; +} + +// Map a build-host source path (__FILE__) onto the on-device test layout. +// All test sources are flat in test/, and the APK unpacks assets/test/* directly +// into the device root (/evengine_test/*), so the basename is the only +// meaningful part of __FILE__ here. +inline std::string androidSourcePath(const char *file) { + std::string s = file ? file : ""; + const std::string root = androidTestRoot(); + if (root.empty()) + return s; + size_t slash = s.find_last_of("/\\"); + std::string rel = (slash == std::string::npos) ? s : s.substr(slash + 1); + return root + "/" + rel; +} +#endif // EVENGINE_ANDROID + +inline std::string pathBesideSourceImpl(const char *file, const char *relative) { +#if defined(EVENGINE_ANDROID) + std::string source = androidSourcePath(file); + size_t slash = source.find_last_of('/'); + std::string dir = (slash == std::string::npos) ? std::string(".") : source.substr(0, slash); + return dir + "/" + relative; +#else + std::string here = file ? file : ""; + auto slash = here.find_last_of("/\\"); + std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); + return dir + "/" + relative; +#endif +} + +} // namespace eve_test_path + +#define EVE_DEFINE_PATH_BESIDE_SOURCE() \ + static std::string pathBesideThisSource(const char *filename) { \ + return eve_test_path::pathBesideSourceImpl(__FILE__, filename); \ + } diff --git a/test/RenderImageAudit.cpp b/test/RenderImageAudit.cpp index c86fdbba..903c8d77 100644 --- a/test/RenderImageAudit.cpp +++ b/test/RenderImageAudit.cpp @@ -866,12 +866,8 @@ struct CloseWin { } }; -std::string pathBesideThisSource(const char *relative) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + relative; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() Mesh *makeFloor(Graphics *gfx, float half = 4.5f) { const float pos[] = { diff --git a/test/animation_skinned.cpp b/test/animation_skinned.cpp index f6b0c99f..8321b4ec 100644 --- a/test/animation_skinned.cpp +++ b/test/animation_skinned.cpp @@ -42,12 +42,8 @@ using namespace eve::graphics; namespace { -std::string pathBesideThisSource(const char *relative) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + relative; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() bool fileExists(const std::string &path) { return std::filesystem::is_regular_file(path); diff --git a/test/audio.cpp b/test/audio.cpp index f0327d97..f868aa1b 100644 --- a/test/audio.cpp +++ b/test/audio.cpp @@ -20,12 +20,8 @@ namespace { -std::string pathBesideThisSource(const char *filename) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + filename; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() std::vector readBinaryFile(const std::string &path) { std::ifstream in(path, std::ios::binary); diff --git a/test/font.cpp b/test/font.cpp index 7a148e37..43360f58 100644 --- a/test/font.cpp +++ b/test/font.cpp @@ -18,12 +18,8 @@ namespace { // FontAwesome private-use icon with a non-empty bitmap in the fixture. constexpr int kIconCodepoint = 0xF000; -std::string pathBesideThisSource(const char *filename) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + filename; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() std::vector readBinaryFile(const std::string &path) { std::ifstream in(path, std::ios::binary); diff --git a/test/graphics_font.cpp b/test/graphics_font.cpp index be53ae36..5b03d087 100644 --- a/test/graphics_font.cpp +++ b/test/graphics_font.cpp @@ -27,12 +27,8 @@ namespace { constexpr int kIconCodepoint = 0xF000; constexpr const char *kIconUtf8 = "\xEF\x80\x80"; // UTF-8 for U+F000 -std::string pathBesideThisSource(const char *filename) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + filename; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() std::vector readBinaryFile(const std::string &path) { std::ifstream in(path, std::ios::binary); diff --git a/test/main.cpp b/test/main.cpp index 5542f493..f93cf89b 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -1,5 +1,89 @@ #include "zeroerr/unittest.h" -int main(int argc, const char** argv) { - return zeroerr::UnitTest().parseArgs(argc, argv).run(); +#if defined(EVENGINE_ANDROID) +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +// Route C++ iostream output to Android logcat so `adb logcat` can show the +// zeroerr console reporter (stdout/stderr are not attached to anything on Android). +class LogcatBuf : public std::streambuf { +public: + LogcatBuf() { setp(buffer_, buffer_ + sizeof(buffer_) - 1); } + ~LogcatBuf() override { sync(); } + +protected: + int overflow(int ch) override { + if (ch != traits_type::eof()) { + *pptr() = static_cast(ch); + pbump(1); + } + return sync() == 0 ? traits_type::not_eof(ch) : traits_type::eof(); + } + + int sync() override { + if (pptr() == pbase()) + return 0; + *pptr() = '\0'; + __android_log_print(ANDROID_LOG_INFO, "EVEngineTest", "%s", pbase()); + setp(buffer_, buffer_ + sizeof(buffer_) - 1); + return 0; + } + +private: + char buffer_[1024]; +}; + +// Root of the unpacked test tree: /evengine_test. The Java +// activity unpacks the APK's assets/test + assets/examples here before SDL_main runs. +std::string testRoot() { + const char *internal = SDL_AndroidGetInternalStoragePath(); + if (!internal) + return {}; + return std::string(internal) + "/evengine_test"; +} + +} // namespace +#endif // EVENGINE_ANDROID + +// Signature must match the `extern "C" int SDL_main(int, char*[])` declared by +// SDL_main.h so the symbol stays unmangled ("SDL_main" for the SDLActivity JNI +// dlsym). A `const char**` parameter would become a C++ overload and be mangled. +int main(int argc, char **argv) { +#if defined(EVENGINE_ANDROID) + static LogcatBuf logcatBuf; + std::cout.rdbuf(&logcatBuf); + std::cerr.rdbuf(&logcatBuf); + std::clog.rdbuf(&logcatBuf); + std::cout.setf(std::ios::unitbuf); + std::cerr.setf(std::ios::unitbuf); + + const std::string root = testRoot(); + if (root.empty()) { + __android_log_print(ANDROID_LOG_ERROR, "EVEngineTest", "no internal storage path"); + } else if (chdir(root.c_str()) != 0) { + __android_log_print(ANDROID_LOG_ERROR, "EVEngineTest", "chdir(%s) failed: %s", + root.c_str(), strerror(errno)); + } else { + __android_log_print(ANDROID_LOG_INFO, "EVEngineTest", "test root: %s", root.c_str()); + } + + // SDLActivity already ran SDL_SetMainReady(); init the subsystems the tests use. + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER | SDL_INIT_AUDIO) != 0) { + __android_log_print(ANDROID_LOG_ERROR, "EVEngineTest", "SDL_Init failed: %s", + SDL_GetError()); + } + __android_log_print(ANDROID_LOG_INFO, "EVEngineTest", "unit_test android starting"); +#endif // EVENGINE_ANDROID + + return zeroerr::UnitTest().parseArgs(argc, const_cast(argv)).run(); } diff --git a/test/model3d.cpp b/test/model3d.cpp index 9cfb03d3..7628fe02 100644 --- a/test/model3d.cpp +++ b/test/model3d.cpp @@ -43,12 +43,8 @@ using namespace eve::graphics; namespace { -std::string pathBesideThisSource(const char *filename) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + filename; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() std::vector readBinaryFile(const std::string &path) { std::ifstream in(path, std::ios::binary); diff --git a/test/particles_attach_extra.cpp b/test/particles_attach_extra.cpp index a5f91678..279aa21e 100644 --- a/test/particles_attach_extra.cpp +++ b/test/particles_attach_extra.cpp @@ -40,12 +40,8 @@ using namespace eve::ik; namespace { -std::string pathBesideThisSource(const char *relative) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + relative; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() bool fileExists(const std::string &path) { return std::filesystem::is_regular_file(path); } diff --git a/test/particles_attach_more.cpp b/test/particles_attach_more.cpp index 508b6c43..aa1e3a59 100644 --- a/test/particles_attach_more.cpp +++ b/test/particles_attach_more.cpp @@ -38,12 +38,8 @@ using namespace eve::ik; namespace { -std::string pathBesideThisSource(const char *relative) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + relative; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() bool fileExists(const std::string &path) { return std::filesystem::is_regular_file(path); } diff --git a/test/particles_attach_skin.cpp b/test/particles_attach_skin.cpp index 3477959f..f97c18d9 100644 --- a/test/particles_attach_skin.cpp +++ b/test/particles_attach_skin.cpp @@ -26,12 +26,8 @@ using namespace eve::animation; namespace { -std::string pathBesideThisSource(const char *relative) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + relative; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() bool fileExists(const std::string &path) { return std::filesystem::is_regular_file(path); } diff --git a/test/sound.cpp b/test/sound.cpp index 0580f61e..8f40d32b 100644 --- a/test/sound.cpp +++ b/test/sound.cpp @@ -18,12 +18,8 @@ namespace { -std::string pathBesideThisSource(const char *filename) { - std::string here = __FILE__; - auto slash = here.find_last_of("/\\"); - std::string dir = (slash == std::string::npos) ? std::string(".") : here.substr(0, slash); - return dir + "/" + filename; -} +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() std::vector readBinaryFile(const std::string &path) { std::ifstream in(path, std::ios::binary); From 018a7e6bdb491acfd3dec4b95a88f928fc4484f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Fri, 14 Aug 2026 23:06:10 +0800 Subject: [PATCH 05/21] chore: remove temporary commit message file --- COMMIT_MSG.txt | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 COMMIT_MSG.txt diff --git a/COMMIT_MSG.txt b/COMMIT_MSG.txt deleted file mode 100644 index 472b5ee6..00000000 --- a/COMMIT_MSG.txt +++ /dev/null @@ -1,18 +0,0 @@ -feat(android): run zeroerr unit_test suite as an Android test app - -Adds EVENGINE_ANDROID_TEST_APP build mode: the unit_test sources are -compiled into libmain.so (SDL entry) and packaged by the existing Gradle -APK along with the test assets, so the full zeroerr suite can run on a -device via adb logcat. - -- test/main.cpp: Android runner with LogcatBuf (cout/cerr -> logcat), - chdir to /evengine_test, SDL_Init; keeps SDL_main symbol unmangled. -- test/PathBesideSource.h: shared pathBesideThisSource helper that maps - __FILE__ to the on-device unpacked tree on Android. -- EVTestActivity: unpacks assets/test + assets/examples to internal - storage and passes an optional --testcase filter from an intent extra. -- test/CMakeLists.txt: builds `main` instead of unit_test on Android, - excludes desktop-only devtools tests, links Android system libs. -- Makefile: build/android-debug-test, sync/android-test-assets, - run/android-test-debug, log/android-test targets. -- Gradle wrapper bumped to 8.5 for Java 21 compatibility. From bed4ce5cd2e6b5522f90abc5482cdcd39c09b4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Sat, 15 Aug 2026 00:26:06 +0800 Subject: [PATCH 06/21] fix(graphics): rebuild Vulkan surface/swapchain and ImGui context when native window is recreated Repeatedly closing/reopening a window and re-initializing the render system (single-process unit_test run) crashed because: - The Vulkan surface/swapchain stayed bound to the destroyed native window. SDL can hand back the same pointer for a recreated window, so pointer identity could not detect the stale surface; present() then hit getSurfaceCapabilitiesKHR: ErrorUnknown. - The ImGui context and Vulkan present-overlay survived the window, so a later present() drew into an unrelated window via the stale overlay. - headlessImGuiWalk destroyed the ImGui context while the UI backend's real context was still current (CreateContext only auto-switches current when GImGui is NULL), leaving a dangling context for later present() calls. Fix: - Window::close() calls Graphics::onNativeWindowDestroyed(); the Vulkan backend drops the surface/swapchain and fires registered window-destroyed callbacks (UI backend tears down its ImGui context here). - initWithWindow() rebuilds the swapchain synchronously and no longer relies on pointer identity to detect a stale surface. - ImGuiBackend owns its ImGui context explicitly and shuts it down via the window-destroyed hook; shutdown() switches to its own context first. - headlessImGuiWalk switches to and from its private ImGui context. Test-only fixes exposed once the suite can run to completion: - animation_skinned: Animation::create() returns the process-wide singleton; do not wrap it in unique_ptr (delete left a dangling ModuleManager pointer). - particles attach tests: emitters live in a process-wide ECS; detach (and clear skin sources) before local skeletons are destroyed. --- src/modules/graphics/Graphics.h | 36 ++++++++++++++++++++++++ src/modules/graphics/vulkan/Graphics.cpp | 33 ++++++++++++++++++++-- src/modules/graphics/vulkan/Graphics.h | 1 + src/modules/ui/imgui/ImGuiBackend.cpp | 30 +++++++++++++++++--- src/modules/ui/imgui/ImGuiBackend.h | 6 +++- src/modules/window/sdl/Window.cpp | 6 ++++ test/UISystem.cpp | 15 ++++++++-- test/animation_skinned.cpp | 7 ++++- test/particles_attach_extra.cpp | 26 +++++++++++++++++ test/particles_attach_more.cpp | 20 +++++++++++++ test/particles_attach_skin.cpp | 7 +++++ test/particles_dynamic_bones.cpp | 19 +++++++++++++ 12 files changed, 195 insertions(+), 11 deletions(-) diff --git a/src/modules/graphics/Graphics.h b/src/modules/graphics/Graphics.h index 662649b0..51dd791b 100644 --- a/src/modules/graphics/Graphics.h +++ b/src/modules/graphics/Graphics.h @@ -419,6 +419,41 @@ public: */ virtual void requestSurfaceRecreate() {} + /** + * Called by the Window module when the native window backing the render + * surface is destroyed (window close / recreation). Backends that keep a + * platform surface tied to the native window must drop it here so the next + * initWithWindow() rebuilds it against a fresh window — even when SDL hands + * back the same pointer for the recreated window. + * + * The present overlay is bound to the destroyed window, so it is also + * dropped here: a stale ImGui callback would otherwise keep drawing into + * later presents of unrelated windows. Registered window-destroyed + * callbacks (e.g. the UI backend's ImGui teardown) fire first so they can + * release their ImGui context while the surface is still valid. + */ + virtual void onNativeWindowDestroyed() { + for (auto &cb : windowDestroyedCallbacks_) cb.first(cb.second); + windowDestroyedCallbacks_.clear(); + clearPresentOverlay(); + } + + /** Drop the present overlay callback (window gone; re-register on UI init). */ + void clearPresentOverlay() { + presentOverlayFn_ = nullptr; + presentOverlayUser_ = nullptr; + } + + /** + * Register a callback invoked when the native window is destroyed. + * Used by the UI backend to tear down its ImGui context; callbacks are + * cleared (and invoked) on onNativeWindowDestroyed(). + */ + using WindowDestroyedCallback = void (*)(void *userdata); + void addWindowDestroyedCallback(WindowDestroyedCallback cb, void *userdata) { + windowDestroyedCallbacks_.emplace_back(cb, userdata); + } + /** * Optional overlay drawn inside the swapchain render pass (before end). * Used by declarative UI (ImGui). `commandBuffer` is a VkCommandBuffer as void*. @@ -667,6 +702,7 @@ protected: int msaaSamples = 4; PresentOverlayFn presentOverlayFn_ = nullptr; void *presentOverlayUser_ = nullptr; + std::vector> windowDestroyedCallbacks_; Shader *currentShader = nullptr; Font *currentFont = nullptr; std::unique_ptr renderControl_; diff --git a/src/modules/graphics/vulkan/Graphics.cpp b/src/modules/graphics/vulkan/Graphics.cpp index 6d369af8..40034f08 100644 --- a/src/modules/graphics/vulkan/Graphics.cpp +++ b/src/modules/graphics/vulkan/Graphics.cpp @@ -262,11 +262,18 @@ Graphics::~Graphics() { void Graphics::initWithWindow(void *nativeWindow) { if (initialized) { // Window module may destroy/recreate the SDL window (tests, setWindowSettings). - // The Vulkan surface is tied to the native window — rebuild it when the - // handle changes, otherwise acquire/present use a destroyed surface. - if (sdlWindow == nativeWindow) return; + // The Vulkan surface is tied to the native window. SDL can hand back the + // same pointer for a freshly recreated window, so pointer identity alone + // can't tell whether the surface is still valid: Window::close() drops it + // via onNativeWindowDestroyed(), and a missing surface must be rebuilt too. + if (sdlWindow == nativeWindow && surface) return; sdlWindow = nativeWindow; recreateSurfaceForResume(); + // Restore the initWithWindow contract (device + swapchain valid on + // return): rebuild the swapchain synchronously. On desktop this runs + // immediately; on Android/iOS isRenderSurfaceStable() defers it until + // the native surface settles, leaving swapchainDirty set (same as before). + rebuildSwapchainIfNeeded(); return; } sdlWindow = nativeWindow; @@ -357,6 +364,26 @@ void Graphics::initWithWindow(void *nativeWindow) { whiteTexture = newTexture(1, 1, whitePixel); } +void Graphics::onNativeWindowDestroyed() { + if (!initialized) return; + // Fire registered window-destroyed callbacks (UI backend tears down its + // ImGui context here) before the device/surface resources are released. + for (auto &cb : windowDestroyedCallbacks_) cb.first(cb.second); + windowDestroyedCallbacks_.clear(); + clearPresentOverlay(); + + device->waitIdle(); + destroySwapchainResources(); + swapchain.destroy(); + swapchain = vkb::Swapchain{}; + if (surface) { + inst.instance.destroySurfaceKHR(surface); + surface = VK_NULL_HANDLE; + } + sdlWindow = nullptr; + swapchainDirty = true; +} + void Graphics::destroySwapchainResources() { presentRecording = {}; swapchainPass = {}; diff --git a/src/modules/graphics/vulkan/Graphics.h b/src/modules/graphics/vulkan/Graphics.h index 580dd65d..7cc14f0c 100644 --- a/src/modules/graphics/vulkan/Graphics.h +++ b/src/modules/graphics/vulkan/Graphics.h @@ -155,6 +155,7 @@ public: void initWithWindow(void *nativeWindow) override; void present() override; void requestSurfaceRecreate() override { surfaceNeedsRecreate = true; } + void onNativeWindowDestroyed() override; void setScreenReadbackEnabled(bool enabled) override { eve::graphics::Graphics::setScreenReadbackEnabled(enabled); ensurePresentCaptureHook(); diff --git a/src/modules/ui/imgui/ImGuiBackend.cpp b/src/modules/ui/imgui/ImGuiBackend.cpp index 5d2f9cde..aafef8b8 100644 --- a/src/modules/ui/imgui/ImGuiBackend.cpp +++ b/src/modules/ui/imgui/ImGuiBackend.cpp @@ -99,7 +99,7 @@ bool ImGuiBackend::init(SDL_Window *window, eve::graphics::Graphics *gfx) { uiScale_ = computeInitialScale(); IMGUI_CHECKVERSION(); - ImGui::CreateContext(); + ctx_ = ::ImGui::CreateContext(); // Start from unified theme tokens (not a one-off ImGui palette). setThemeUiScale(uiScale_); applyThemeToImGui(globalTheme(), uiScale_); @@ -165,11 +165,20 @@ bool ImGuiBackend::init(SDL_Window *window, eve::graphics::Graphics *gfx) { fontsUploaded_ = true; gfx_->setPresentOverlay(&ImGuiBackend::presentOverlayThunk, this); + // The ImGui context + Vulkan pipeline are bound to the native window. When + // the window is destroyed, tear down so the next init() rebuilds against a + // fresh window — even if SDL hands back the same pointer. + gfx_->addWindowDestroyedCallback(&ImGuiBackend::windowDestroyedThunk, this); initialized_ = true; return true; } +void ImGuiBackend::windowDestroyedThunk(void *userdata) { + auto *self = static_cast(userdata); + if (self) self->shutdown(); +} + void ImGuiBackend::shutdown() { if (!initialized_) return; if (frameOpen_) { @@ -183,9 +192,22 @@ void ImGuiBackend::shutdown() { if (vkg) { vkDeviceWaitIdle(static_cast(vkg->getDevice().instance)); } - ImGui_ImplVulkan_Shutdown(); - ImGui_ImplSDL2_Shutdown(); - ImGui::DestroyContext(); + // The ImGui backend data lives on the context this backend created. It may + // not be the currently active context (headless tests switch contexts), so + // explicitly select it before tearing down ImGui_Impl* state. + if (ctx_) { + ::ImGuiContext *prev = ::ImGui::GetCurrentContext(); + ::ImGuiContext *mine = ctx_; + ::ImGui::SetCurrentContext(mine); + ::ImGui_ImplVulkan_Shutdown(); + ::ImGui_ImplSDL2_Shutdown(); + ::ImGui::DestroyContext(mine); + ctx_ = nullptr; + // Restore the previous current context unless it was the one we just + // destroyed (DestroyContext already reset it to null). + if (prev && prev != mine) ::ImGui::SetCurrentContext(prev); + else ::ImGui::SetCurrentContext(nullptr); + } if (imguiDescriptorPool_ && vkg) { vkDestroyDescriptorPool(static_cast(vkg->getDevice().instance), static_cast(imguiDescriptorPool_), nullptr); diff --git a/src/modules/ui/imgui/ImGuiBackend.h b/src/modules/ui/imgui/ImGuiBackend.h index 97204557..bc56e79e 100644 --- a/src/modules/ui/imgui/ImGuiBackend.h +++ b/src/modules/ui/imgui/ImGuiBackend.h @@ -2,6 +2,8 @@ #include "ui/UIBackend.h" +#include + namespace eve::ui { /** Dear ImGui + SDL input + Vulkan present overlay. */ @@ -29,6 +31,7 @@ public: private: void renderDrawData(void *vkCommandBuffer); static void presentOverlayThunk(void *userdata, void *commandBuffer); + static void windowDestroyedThunk(void *userdata); void applyScale(float scale); /** Initial UI scale derived from window DPI (desktop) or display DPI (mobile). */ float computeInitialScale() const; @@ -43,7 +46,8 @@ private: float uiScale_ = 1.f; eve::graphics::Graphics *gfx_ = nullptr; SDL_Window *window_ = nullptr; - void *imguiDescriptorPool_ = nullptr; // VkDescriptorPool + void *imguiDescriptorPool_ = nullptr; // VkDescriptorPool + ImGuiContext *ctx_ = nullptr; // ImGui context owned by this backend }; } // namespace eve::ui diff --git a/src/modules/window/sdl/Window.cpp b/src/modules/window/sdl/Window.cpp index ae156c8b..112b0703 100644 --- a/src/modules/window/sdl/Window.cpp +++ b/src/modules/window/sdl/Window.cpp @@ -557,6 +557,12 @@ void Window::close(bool allowExceptions) { SDL_DestroyWindow(window); window = nullptr; + // The Vulkan surface/swapchain is tied to the destroyed native window. + // Drop it now so the next initWithWindow() rebuilds it against a fresh + // window — SDL may hand back the same pointer for the recreated window, + // so pointer identity cannot be used to detect a stale surface. + if (graphics) graphics->onNativeWindowDestroyed(); + // The old window may have generated pending events which are no longer // relevant. Destroy them all! SDL_FlushEvent(SDL_WINDOWEVENT); diff --git a/test/UISystem.cpp b/test/UISystem.cpp index 7572f447..2cbe681b 100644 --- a/test/UISystem.cpp +++ b/test/UISystem.cpp @@ -81,8 +81,18 @@ TEST_CASE("UISystem.render.headlessImGuiWalk") { UIHost *h = UIHost::createHost("walk"); h->setTree(window("Panel", {text("Label", "label"), button("Btn", "btn")})); + // This test is headless: it drives UISystem::render() with a private ImGui + // context. When run in the same process after a real-window UI test, the UI + // module's ImGui backend keeps its own context (and present-overlay hook). + // ImGui::CreateContext() only auto-switches current when GImGui is NULL, so + // explicitly switch to the private context and back — otherwise the UI + // backend's context would be the one NewFrame/EndFrame and DestroyContext + // operate on, leaving a dangling ImGui context for later present() calls. + ImGuiContext *savedContext = ImGui::GetCurrentContext(); + IMGUI_CHECKVERSION(); - ImGui::CreateContext(); + ImGuiContext *headlessContext = ImGui::CreateContext(); + ImGui::SetCurrentContext(headlessContext); ImGuiIO &io = ImGui::GetIO(); io.DisplaySize = ImVec2(800.f, 600.f); io.IniFilename = nullptr; @@ -94,7 +104,8 @@ TEST_CASE("UISystem.render.headlessImGuiWalk") { ImGui::NewFrame(); UISystem::render(); ImGui::EndFrame(); - ImGui::DestroyContext(); + ImGui::DestroyContext(headlessContext); + if (savedContext) ImGui::SetCurrentContext(savedContext); CHECK(!h->tree()->dirty); } diff --git a/test/animation_skinned.cpp b/test/animation_skinned.cpp index f6b0c99f..2c4ac3b2 100644 --- a/test/animation_skinned.cpp +++ b/test/animation_skinned.cpp @@ -585,7 +585,12 @@ TEST_CASE("animation.skinned.cesiumMan.animationFactory") { loadCesiumMan("ev_ut_animation_skinned_factory")); REQUIRE(model.get() != nullptr); - std::unique_ptr anim(Animation::create()); + // Animation::create() returns the process-wide module singleton owned by + // ModuleManager; do NOT wrap it in unique_ptr (that would delete the + // singleton out from under ModuleManager and leave a dangling pointer for + // later tests that call Animation::create() again). + Animation *anim = Animation::create(); + REQUIRE(anim != nullptr); std::unique_ptr sk(anim->newSkeletonFromModel(model.get())); REQUIRE(sk.get() != nullptr); const int meshIndex = findFirstSkinnedMesh(model.get()); diff --git a/test/particles_attach_extra.cpp b/test/particles_attach_extra.cpp index a5f91678..8c07b0a6 100644 --- a/test/particles_attach_extra.cpp +++ b/test/particles_attach_extra.cpp @@ -124,6 +124,11 @@ TEST_CASE("particles.attach.resetClearsParticlesKeepsAttach") { CHECK(e->isAttached()); CHECK_EQ(e->getAttachKind(), std::string("ik2d")); CHECK(std::fabs(e->getX() - 11.f) < 1e-2f); + + // The emitter lives in a process-wide ECS; detach before the local + // skeleton is destroyed so later ParticleSimSystem::update() calls in + // other tests don't dereference the freed skeleton. + e->detach(); } TEST_CASE("particles.attach.moveToOverwrittenBySync") { @@ -139,6 +144,7 @@ TEST_CASE("particles.attach.moveToOverwrittenBySync") { ParticleSimSystem::update(0.016f); CHECK(std::fabs(e->getX() - 5.f) < 1e-2f); CHECK(std::fabs(e->getY() - 6.f) < 1e-2f); + e->detach(); } TEST_CASE("particles.attach.applyConfigPreservesAttach") { @@ -157,6 +163,7 @@ TEST_CASE("particles.attach.applyConfigPreservesAttach") { e->start(); ParticleSimSystem::update(0.1f); CHECK_GE(e->getCount(), 3); + e->detach(); } TEST_CASE("particles.attach.layerAndVisibleIndependent") { @@ -175,6 +182,7 @@ TEST_CASE("particles.attach.layerAndVisibleIndependent") { ParticleSimSystem::update(0.016f); CHECK(std::fabs(e->getX() - 1.f) < 1e-2f); CHECK(std::fabs(e->getY() - 2.f) < 1e-2f); + e->detach(); } TEST_CASE("particles.attach.ellipseAreaOnBone") { @@ -197,6 +205,7 @@ TEST_CASE("particles.attach.ellipseAreaOnBone") { if (dx * dx + dy * dy <= 1.01f) ++inside; } CHECK_EQ(inside, e->getCount()); + e->detach(); } TEST_CASE("particles.attach.tangentialForceUsesBoneOrigin") { @@ -218,6 +227,7 @@ TEST_CASE("particles.attach.tangentialForceUsesBoneOrigin") { ParticleSimSystem::update(0.1f); // Tangential to radial (+X) is +Y (rdx,rdy)=(1,0) → (-rdy, rdx)=(0,1) CHECK(e->sim()->particles[0].vy > 0.f); + e->detach(); } TEST_CASE("particles.attach.particleLifetimeExpiresOnBone") { @@ -235,6 +245,7 @@ TEST_CASE("particles.attach.particleLifetimeExpiresOnBone") { ParticleSimSystem::update(0.1f); CHECK_EQ(e->getCount(), 0); CHECK(e->isAttached()); + e->detach(); } TEST_CASE("particles.attach.largeDtBurstOnBone") { @@ -255,6 +266,7 @@ TEST_CASE("particles.attach.largeDtBurstOnBone") { CHECK(std::fabs(e->sim()->particles[size_t(i)].x - 7.f) < 1e-2f); CHECK(std::fabs(e->sim()->particles[size_t(i)].y - 8.f) < 1e-2f); } + e->detach(); } TEST_CASE("particles.attach.zeroDtStillSyncs") { @@ -277,6 +289,7 @@ TEST_CASE("particles.attach.zeroDtStillSyncs") { CHECK(std::fabs(e->getX() - 15.f) < 1e-2f); CHECK(std::fabs(e->getY() - 16.f) < 1e-2f); CHECK_EQ(e->getCount(), 2); // no new spawns at dt=0 + e->detach(); } TEST_CASE("particles.attach.cycleThroughAllKinds") { @@ -333,6 +346,7 @@ TEST_CASE("particles.attach.scaleChangeMidLifetimeResyncs") { e->setAttachScale(3.f); CHECK(std::fabs(e->getX() - 6.f) < 1e-3f); CHECK(std::fabs(e->getY() - 12.f) < 1e-3f); + e->detach(); } TEST_CASE("particles.attach.spinAndSizeVariationOnBone") { @@ -358,6 +372,7 @@ TEST_CASE("particles.attach.spinAndSizeVariationOnBone") { } CHECK(spinOk); CHECK(sizeVaried); + e->detach(); } TEST_CASE("particles.attach.ik2dConstraintsStillEmits") { @@ -383,6 +398,7 @@ TEST_CASE("particles.attach.ik2dConstraintsStillEmits") { CHECK(std::fabs(e->sim()->particles[size_t(i)].x - sk->getX(tip)) < 1e-1f); CHECK(std::fabs(e->sim()->particles[size_t(i)].y - sk->getY(tip)) < 1e-1f); } + e->detach(); } TEST_CASE("particles.attach.twoEmittersDifferentPlanes") { @@ -402,6 +418,8 @@ TEST_CASE("particles.attach.twoEmittersDifferentPlanes") { CHECK(std::fabs(xy->getY() - 4.f) < 1e-3f); CHECK(std::fabs(xz->getX() - 2.f) < 1e-3f); CHECK(std::fabs(xz->getY() - 6.f) < 1e-3f); + xy->detach(); + xz->detach(); } TEST_CASE("particles.attach.animChainOffsetThreeBones") { @@ -425,6 +443,7 @@ TEST_CASE("particles.attach.animChainOffsetThreeBones") { // world c = 10+1+1=12, + local offset 0.5 → 12.5 CHECK(std::fabs(e->getX() - 12.5f) < 1e-2f); CHECK(std::fabs(e->getY() - 0.f) < 1e-2f); + e->detach(); } TEST_CASE("particles.attach.spineBobAnimContinuousRate") { @@ -453,6 +472,7 @@ TEST_CASE("particles.attach.spineBobAnimContinuousRate") { } CHECK_GE(e->getCount(), 20); CHECK_GT(maxY - minY, 5.f); + e->detach(); } TEST_CASE("particles.attach.ik3dSolverStepContinuous") { @@ -481,6 +501,7 @@ TEST_CASE("particles.attach.ik3dSolverStepContinuous") { CHECK(std::fabs(e->getX() - sk->getX(tip)) < 1e-2f); } CHECK_GE(moved, 1); + e->detach(); } TEST_CASE("particles.skin.filterByIndexAndClearFilter") { @@ -512,6 +533,8 @@ TEST_CASE("particles.skin.filterByIndexAndClearFilter") { e->setSkinBoneFilter(-1, 0.f); // clear filter → all verts e->emitFromSkin(32); CHECK_EQ(e->getCount(), 32); + e->clearSkinSource(); + e->detach(); } TEST_CASE("particles.skin.emitFromSkinRespectsBuffer") { @@ -536,6 +559,8 @@ TEST_CASE("particles.skin.emitFromSkinRespectsBuffer") { e->emitFromSkin(100); CHECK_EQ(e->getCount(), 10); CHECK_EQ(e->getBufferSize(), 10); + e->clearSkinSource(); + e->detach(); } TEST_CASE("particles.attach.cesiumManBoneByNameIfPresent") { @@ -581,5 +606,6 @@ TEST_CASE("particles.attach.smokeAndFirePresetsOnIk3d") { e->start(); ParticleSimSystem::update(0.05f); CHECK_GE(e->getCount(), 1); + e->detach(); } } diff --git a/test/particles_attach_more.cpp b/test/particles_attach_more.cpp index 508b6c43..8704ba24 100644 --- a/test/particles_attach_more.cpp +++ b/test/particles_attach_more.cpp @@ -129,6 +129,7 @@ TEST_CASE("particles.attach.emitterLifeExpiresWhileAttached") { CHECK_EQ(e->getCount(), n); // Attach sync still updates origin even when inactive. CHECK(std::fabs(e->getX() - sk->getX(0)) < 1e-2f); + e->detach(); } TEST_CASE("particles.attach.bufferCapUnderRateOnBone") { @@ -145,6 +146,7 @@ TEST_CASE("particles.attach.bufferCapUnderRateOnBone") { ParticleSimSystem::update(1.f); CHECK_EQ(e->getCount(), 8); CHECK_EQ(e->getBufferSize(), 8); + e->detach(); } TEST_CASE("particles.attach.rebindDifferentBone") { @@ -164,6 +166,7 @@ TEST_CASE("particles.attach.rebindDifferentBone") { CHECK_EQ(e->getAttachKind(), std::string("ik2d")); CHECK(std::fabs(e->getX() - sk->getX(tip)) < 1e-2f); CHECK(std::fabs(e->getX() - sk->getX(mid)) > 1.f); + e->detach(); } TEST_CASE("particles.attach.zeroAndNegativeScale") { @@ -184,6 +187,7 @@ TEST_CASE("particles.attach.zeroAndNegativeScale") { e->setAttachScale(-2.f); CHECK(std::fabs(e->getX() - (-4.f)) < 1e-3f); CHECK(std::fabs(e->getY() - (-6.f)) < 1e-3f); + e->detach(); } TEST_CASE("particles.attach.stopStartPreservesAttach") { @@ -213,6 +217,7 @@ TEST_CASE("particles.attach.stopStartPreservesAttach") { ParticleSimSystem::update(0.05f); CHECK_GT(e->getCount(), n); CHECK_EQ(e->getAttachKind(), std::string("ik3d")); + e->detach(); } TEST_CASE("particles.attach.emissionAreaAroundBone") { @@ -241,6 +246,7 @@ TEST_CASE("particles.attach.emissionAreaAroundBone") { } CHECK_GT(maxX - minX, 5.f); CHECK_GT(maxY - minY, 2.f); + e->detach(); } TEST_CASE("particles.attach.followRotationAffectsSpawnVelocity") { @@ -266,6 +272,7 @@ TEST_CASE("particles.attach.followRotationAffectsSpawnVelocity") { // Direction ≈ π/2 → velocity mostly +Y CHECK(std::fabs(p.vx) < 1.f); CHECK(p.vy > 8.f); + e->detach(); } TEST_CASE("particles.attach.radialForceUsesMovingOrigin") { @@ -297,6 +304,7 @@ TEST_CASE("particles.attach.radialForceUsesMovingOrigin") { ParticleSimSystem::update(0.1f); // Away from origin at 100: particle at 10 → direction -X → negative vx CHECK(e->sim()->particles[0].vx < 0.f); + e->detach(); } TEST_CASE("particles.attach.getAttachBoneAfterDetach") { @@ -332,6 +340,7 @@ TEST_CASE("particles.attach.stableWhenPoseUnchanged") { // xz of (5,6,7) → (5,7) CHECK(std::fabs(x0 - 5.f) < 1e-3f); CHECK(std::fabs(y0 - 7.f) < 1e-3f); + e->detach(); } TEST_CASE("particles.attach.spineRotatedParentOffset") { @@ -355,6 +364,7 @@ TEST_CASE("particles.attach.spineRotatedParentOffset") { const float ey = spine->getBoneWorldY(hand) + d * 5.f; CHECK(std::fabs(e->getX() - ex) < 1e-2f); CHECK(std::fabs(e->getY() - ey) < 1e-2f); + e->detach(); } TEST_CASE("particles.attach.ik2dSolverStepTracksTip") { @@ -384,6 +394,7 @@ TEST_CASE("particles.attach.ik2dSolverStepTracksTip") { CHECK(std::fabs(e->getY() - sk->getY(tip)) < 1e-2f); } CHECK_GE(moved, 1); + e->detach(); } TEST_CASE("particles.attach.ik3dOffsetAlongBoneProjected") { @@ -400,6 +411,7 @@ TEST_CASE("particles.attach.ik3dOffsetAlongBoneProjected") { e->attachToSkeleton3D(sk.get(), tip); CHECK(std::fabs(e->getX() - 3.f) < 1e-2f); CHECK(std::fabs(e->getY() - 0.f) < 1e-2f); + e->detach(); } TEST_CASE("particles.attach.animByNameAfterDetach") { @@ -420,6 +432,7 @@ TEST_CASE("particles.attach.animByNameAfterDetach") { CHECK(e->isAttached()); CHECK_EQ(e->getAttachBone(), head); CHECK(std::fabs(e->getY() - 2.f) < 1e-3f); + e->detach(); } TEST_CASE("particles.attach.presetStillWorksWhenAttached") { @@ -436,6 +449,7 @@ TEST_CASE("particles.attach.presetStillWorksWhenAttached") { e->start(); ParticleSimSystem::update(0.05f); CHECK_GE(e->getCount(), 1); + e->detach(); } TEST_CASE("particles.attach.emitZeroAndNegativeNoOp") { @@ -447,6 +461,7 @@ TEST_CASE("particles.attach.emitZeroAndNegativeNoOp") { e->emit(0); e->emit(-5); CHECK_EQ(e->getCount(), 0); + e->detach(); } TEST_CASE("particles.skin.planeXzYzCesiumMan") { @@ -507,6 +522,8 @@ TEST_CASE("particles.skin.planeXzYzCesiumMan") { spreadYZ = (maxX - minX) + (maxY - minY); } CHECK_GT(spreadYZ, 0.5f); + e->clearSkinSource(); + e->detach(); } TEST_CASE("particles.skin.clearRestoresBoneOriginSpawn") { @@ -543,6 +560,7 @@ TEST_CASE("particles.skin.clearRestoresBoneOriginSpawn") { CHECK(std::fabs(e->sim()->particles[size_t(i)].x - e->getX()) < 1e-2f); CHECK(std::fabs(e->sim()->particles[size_t(i)].y - e->getY()) < 1e-2f); } + e->detach(); } TEST_CASE("particles.attach.manualSyncAttachWithoutSim") { @@ -557,6 +575,7 @@ TEST_CASE("particles.attach.manualSyncAttachWithoutSim") { e->syncAttach(); CHECK(std::fabs(e->getX() - 8.f) < 1e-2f); CHECK(std::fabs(e->getY() - 9.f) < 1e-2f); + e->detach(); } TEST_CASE("particles.attach.followRotationToggleResyncs") { @@ -582,4 +601,5 @@ TEST_CASE("particles.attach.followRotationToggleResyncs") { e->setDirection(0.3f); e->syncAttach(); CHECK(std::fabs(e->getDirection() - 0.3f) < 1e-4f); + e->detach(); } diff --git a/test/particles_attach_skin.cpp b/test/particles_attach_skin.cpp index 3477959f..f651ae42 100644 --- a/test/particles_attach_skin.cpp +++ b/test/particles_attach_skin.cpp @@ -161,6 +161,7 @@ TEST_CASE("particles.attach.followRotation") { e->setFollowBoneRotation(true); e->attachToBone(pose.get(), root); CHECK(std::fabs(e->getDirection() - (3.14159265f * 0.5f)) < 0.05f); + e->detach(); } TEST_CASE("particles.attach.emitsAtBone") { @@ -185,6 +186,7 @@ TEST_CASE("particles.attach.emitsAtBone") { CHECK(std::fabs(sim->particles[size_t(i)].x - 50.f) < 1e-2f); CHECK(std::fabs(sim->particles[size_t(i)].y - 60.f) < 1e-2f); } + e->detach(); } TEST_CASE("particles.skin.surfaceEmitCesiumMan") { @@ -308,6 +310,7 @@ TEST_CASE("particles.attach.animPoseDynamicEmitAcrossFrames") { // tip world x = root.x + 1 → scaled ×10; frames 0..4 → x = 10,30,50,70,90 CHECK(std::fabs(sim->particles[0].x - 10.f) < 1e-2f); CHECK(std::fabs(sim->particles[4].x - 90.f) < 1e-2f); + e->detach(); } TEST_CASE("particles.attach.yzPlaneAndDetachClearsKind") { @@ -376,6 +379,7 @@ TEST_CASE("particles.attach.clipSampleMovesEmitter") { CHECK_EQ(e->getCount(), 8); // Animated bone should move enough that spawn positions spread out. CHECK_GT(maxX - minX + maxY - minY, 1.f); + e->detach(); } TEST_CASE("particles.skin.emptyBoneFilterFallsBack") { @@ -413,6 +417,8 @@ TEST_CASE("particles.skin.emptyBoneFilterFallsBack") { // Fallback spawn at emitter position. CHECK(std::fabs(e->sim()->particles[0].x - 12.f) < 1e-2f); CHECK(std::fabs(e->sim()->particles[0].y - 34.f) < 1e-2f); + e->clearSkinSource(); + e->detach(); } TEST_CASE("particles.attach.followRotationOffKeepsDirection") { @@ -430,4 +436,5 @@ TEST_CASE("particles.attach.followRotationOffKeepsDirection") { e->setFollowBoneRotation(false); e->attachToBone(pose.get(), root); CHECK(std::fabs(e->getDirection() - 0.25f) < 1e-4f); + e->detach(); } diff --git a/test/particles_dynamic_bones.cpp b/test/particles_dynamic_bones.cpp index 69e3b7ca..d75ead90 100644 --- a/test/particles_dynamic_bones.cpp +++ b/test/particles_dynamic_bones.cpp @@ -108,6 +108,7 @@ TEST_CASE("particles.attach.spineBoneFollowsAndEmits") { e->attachToSpineBoneByName(spine.get(), "Missing"); CHECK(!e->isAttached()); CHECK_EQ(e->getAttachKind(), std::string("none")); + e->detach(); } TEST_CASE("particles.attach.spineDynamicEmitAcrossAnim") { @@ -140,6 +141,7 @@ TEST_CASE("particles.attach.spineDynamicEmitAcrossAnim") { } CHECK_EQ(e->getCount(), 6); CHECK_GE(moved, 1); // hand translate keyframes should move spawn X + e->detach(); } TEST_CASE("particles.attach.spineFollowRotation") { @@ -156,6 +158,7 @@ TEST_CASE("particles.attach.spineFollowRotation") { e->setFollowBoneRotation(true); e->attachToSpineBone(spine.get(), body); CHECK(std::fabs(e->getDirection() - float(M_PI * 0.5)) < 0.05f); + e->detach(); } TEST_CASE("particles.attach.ik2dBoneEmit") { @@ -194,6 +197,7 @@ TEST_CASE("particles.attach.ik2dBoneEmit") { sk->forwardKinematics(); ParticleSimSystem::update(0.016f); CHECK(std::fabs(e->getX() - sk->getX(tip)) < 1e-2f); + e->detach(); } TEST_CASE("particles.attach.ik2dFollowRotationAndSolver") { @@ -219,6 +223,7 @@ TEST_CASE("particles.attach.ik2dFollowRotationAndSolver") { const float fy = sk->getOrientationY(tip); const float expect = std::atan2(fy, fx); CHECK(std::fabs(e->getDirection() - expect) < 0.1f); + e->detach(); } TEST_CASE("particles.attach.ik3dBoneEmitProjected") { @@ -251,6 +256,7 @@ TEST_CASE("particles.attach.ik3dBoneEmitProjected") { e->start(); ParticleSimSystem::update(0.05f); CHECK_GE(e->getCount(), 2); + e->detach(); } TEST_CASE("particles.attach.switchKindsClearsPrevious") { @@ -268,6 +274,7 @@ TEST_CASE("particles.attach.switchKindsClearsPrevious") { CHECK_EQ(e->getAttachKind(), std::string("ik3d")); CHECK(e->attach()->ik2d == nullptr); CHECK(e->attach()->ik3d == sk3.get()); + e->detach(); } TEST_CASE("particles.attach.nullAndOutOfRangeDisable") { @@ -320,6 +327,7 @@ TEST_CASE("particles.attach.invalidPlaneFallsBackToXy") { CHECK_EQ(e->attach()->plane, std::string("xy")); CHECK(std::fabs(e->getX() - 3.f) < 1e-3f); CHECK(std::fabs(e->getY() - 4.f) < 1e-3f); + e->detach(); } TEST_CASE("particles.attach.multiEmittersOnSameSkeleton") { @@ -356,6 +364,8 @@ TEST_CASE("particles.attach.multiEmittersOnSameSkeleton") { // Spawn clusters stay near their respective bones. CHECK(std::fabs(rootFx->sim()->particles[0].x - sk->getX(0)) < 0.5f); CHECK(std::fabs(tipFx->sim()->particles[0].x - sk->getX(tip)) < 0.5f); + rootFx->detach(); + tipFx->detach(); } TEST_CASE("particles.attach.rateEmitTracksMovingIk2d") { @@ -382,6 +392,7 @@ TEST_CASE("particles.attach.rateEmitTracksMovingIk2d") { } // Tip world x ≈ root.x + length; root moves 0,10,20,30,40 → tip ~1,11,21,31,41 CHECK(xs.back() - xs.front() > 20.f); + e->detach(); } TEST_CASE("particles.attach.ik2dOffsetAlongOrientation") { @@ -404,6 +415,7 @@ TEST_CASE("particles.attach.ik2dOffsetAlongOrientation") { e->syncAttach(); CHECK(std::fabs(e->getX() - 10.f) < 1e-2f); CHECK(std::fabs(e->getY() - 3.f) < 1e-2f); + e->detach(); } TEST_CASE("particles.attach.ik3dYzPlaneAndFollowRotation") { @@ -431,6 +443,7 @@ TEST_CASE("particles.attach.ik3dYzPlaneAndFollowRotation") { CHECK(std::fabs(e->getDirection() - expect) < 0.15f); else (void)fx; // orientation collapsed on yz — direction unchanged is fine + e->detach(); } TEST_CASE("particles.attach.ik3dSolverMovesEmitter") { @@ -455,6 +468,7 @@ TEST_CASE("particles.attach.ik3dSolverMovesEmitter") { CHECK(std::fabs(e->getX() - sk->getX(tip)) < 5e-2f); CHECK(std::fabs(e->getY() - sk->getY(tip)) < 5e-2f); CHECK(std::fabs(e->getX() - x0) + std::fabs(e->getY() - y0) > 0.2f); + e->detach(); } TEST_CASE("particles.attach.spineScaleAndModuleUpdate") { @@ -479,6 +493,7 @@ TEST_CASE("particles.attach.spineScaleAndModuleUpdate") { mod->update(0.1f); // module path must sync attach + sim CHECK_GE(e->getCount(), 4); CHECK(std::fabs(e->sim()->particles[0].x - e->getX()) < 1e-2f); + e->detach(); } TEST_CASE("particles.attach.detachKeepsAliveParticles") { @@ -528,6 +543,7 @@ TEST_CASE("particles.attach.pauseStopsSpawnButStillSyncs") { ParticleSimSystem::update(0.05f); CHECK_EQ(e->getCount(), n); // paused: no new particles CHECK(std::fabs(e->getX() - sk->getX(0)) < 1e-2f); // but attach still syncs + e->detach(); } TEST_CASE("particles.attach.switchSpineToAnimClearsSpine") { @@ -556,6 +572,7 @@ TEST_CASE("particles.attach.switchSpineToAnimClearsSpine") { CHECK(e->attach()->pose == pose.get()); CHECK(std::fabs(e->getX() - 7.f) < 1e-3f); CHECK(std::fabs(e->getY() - 8.f) < 1e-3f); + e->detach(); } TEST_CASE("particles.attach.animOffsetRotatedByBone") { @@ -577,6 +594,7 @@ TEST_CASE("particles.attach.animOffsetRotatedByBone") { e->attachToBone(pose.get(), root); CHECK(std::fabs(e->getX() - 0.f) < 1e-2f); CHECK(std::fabs(e->getY() - 2.f) < 1e-2f); + e->detach(); } TEST_CASE("particles.attach.skinTakesPriorityOverBoneOrigin") { @@ -601,4 +619,5 @@ TEST_CASE("particles.attach.skinTakesPriorityOverBoneOrigin") { CHECK(std::fabs(e->sim()->particles[size_t(i)].y - 200.f) < 1e-2f); } CHECK(!e->hasSkinSource()); + e->detach(); } From c3a2da2d99d68d8eab2993110c133127cddb8f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Sat, 15 Aug 2026 00:37:50 +0800 Subject: [PATCH 07/21] feat: add physics-driven metroidvania demo --- CMakeLists.txt | 3 +- examples/metroidvania/README.md | 32 + examples/metroidvania/assets/KENNEY-CC0.txt | 22 + .../metroidvania/assets/background_forest.png | Bin 0 -> 23375 bytes .../metroidvania/assets/background_mine.png | Bin 0 -> 23028 bytes .../metroidvania/assets/background_ruins.png | Bin 0 -> 18863 bytes examples/metroidvania/assets/crate.png | Bin 0 -> 1203 bytes examples/metroidvania/assets/rock.png | Bin 0 -> 2721 bytes examples/metroidvania/assets/tiles.png | Bin 0 -> 15340 bytes examples/metroidvania/config.nut | 7 + examples/metroidvania/generate_map.py | 143 + examples/metroidvania/main.nut | 765 ++ examples/metroidvania/maps/world.json | 10791 ++++++++++++++++ examples/metroidvania/smoke_test.py | 49 + examples/metroidvania/tuning.nut | 45 + src/modules/animation/Animation.cpp | 1 + src/modules/animation/SpineAnim.cpp | 19 + src/modules/animation/SpineAnim.h | 6 + src/modules/filesystem/Filesystem.cpp | 8 + src/modules/graphics/Graphics.cpp | 1 + src/modules/map/Map.cpp | 12 +- src/modules/map/Map.h | 3 + src/modules/physics/Body.cpp | 25 +- src/modules/physics/Body.h | 8 + src/modules/physics/Fixture.cpp | 37 +- src/modules/physics/Fixture.h | 15 + src/modules/physics/Physics.cpp | 37 + src/modules/physics/World.cpp | 187 +- src/modules/physics/World.h | 58 + test/animation_sprite_spine.cpp | 12 + test/box2d.cpp | 99 + 31 files changed, 12377 insertions(+), 8 deletions(-) create mode 100644 examples/metroidvania/README.md create mode 100644 examples/metroidvania/assets/KENNEY-CC0.txt create mode 100644 examples/metroidvania/assets/background_forest.png create mode 100644 examples/metroidvania/assets/background_mine.png create mode 100644 examples/metroidvania/assets/background_ruins.png create mode 100644 examples/metroidvania/assets/crate.png create mode 100644 examples/metroidvania/assets/rock.png create mode 100644 examples/metroidvania/assets/tiles.png create mode 100644 examples/metroidvania/config.nut create mode 100644 examples/metroidvania/generate_map.py create mode 100644 examples/metroidvania/main.nut create mode 100644 examples/metroidvania/maps/world.json create mode 100644 examples/metroidvania/smoke_test.py create mode 100644 examples/metroidvania/tuning.nut diff --git a/CMakeLists.txt b/CMakeLists.txt index fd87a1bc..19bd2847 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,7 +37,8 @@ function(main) # /EHsc: CMAKE_CXX_FLAGS is empty in this tree, so MSVC/clang-cl otherwise compile # with exceptions off. clangd then errors on try/throw (zeroerr ASSERT, Exception). if(MSVC) - add_compile_options(/utf-8 /EHsc) + # /FS serializes writes to a target PDB when MSBuild schedules several cl.exe jobs. + add_compile_options(/utf-8 /EHsc /FS) endif() # zeroerr before modules so Debug ASSERT headers/defs are available when compiling engine code. diff --git a/examples/metroidvania/README.md b/examples/metroidvania/README.md new file mode 100644 index 00000000..8057429a --- /dev/null +++ b/examples/metroidvania/README.md @@ -0,0 +1,32 @@ +# Momentum Ruins + +Physics-driven side-scrolling action demo for EVEngine. The three connected regions teach +combo launching, wall jumping, air dashing, kicking props, momentum damage, and finally a +Dragon boss encounter. + +Run from the repository root with the platform-specific engine target, for example: + +```sh +make run/win32-debug GAME=examples/metroidvania +``` + +Controls: `A/D` or arrows move, `Space` jumps/wall-jumps, `J` attacks, `K` kicks props, and +either `Shift` key air-dashes after the upgrade. Enable `debug` in `config.nut` to see Box2D +fixtures. + +Run the startup/frame-error smoke test on Windows with: + +```powershell +python examples/metroidvania/smoke_test.py build/src/engine/Debug/eve.exe +``` + +## Asset licenses + +- Tile, prop, and background art is selected from Kenney's **Platformer Pack Redux** and is + CC0. The original license is included at `assets/KENNEY-CC0.txt`; source: + https://opengameart.org/node/39408 +- The animated Hero, Spineboy, and Dragon are loaded directly from `test/assets/spine`. + Their individual license files remain beside those assets. This demo is non-commercial: + Hero may not be redistributed and Spineboy may not be used commercially. + +The generated map can be reproduced with `python generate_map.py`. diff --git a/examples/metroidvania/assets/KENNEY-CC0.txt b/examples/metroidvania/assets/KENNEY-CC0.txt new file mode 100644 index 00000000..80343674 --- /dev/null +++ b/examples/metroidvania/assets/KENNEY-CC0.txt @@ -0,0 +1,22 @@ + +############################################################################### + + + Platformer Pack Redux + by Kenney Vleugels for Kenney (www.kenney.nl) + + ------------------------------ + + License (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + You may use these graphics in personal and commercial projects. + Credit (Kenney or www.kenney.nl) would be nice but is not mandatory. + + ------------------------------ + + Donate: http://donate.kenney.nl/ + Request: http://request.kenney.nl/ + + +############################################################################### \ No newline at end of file diff --git a/examples/metroidvania/assets/background_forest.png b/examples/metroidvania/assets/background_forest.png new file mode 100644 index 0000000000000000000000000000000000000000..8469d4aef0afba15fb640294fdc01728ae32e175 GIT binary patch literal 23375 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 V+fDGUW5@0SUNtl|!0EX>_&-K59#sGU literal 0 HcmV?d00001 diff --git a/examples/metroidvania/assets/crate.png b/examples/metroidvania/assets/crate.png new file mode 100644 index 0000000000000000000000000000000000000000..8f5737f02989c549e833b4a79370639d9534bfc7 GIT binary patch literal 1203 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV5#zSaSW-L^Y+$xKk0CZV-LSu z8!y|iBH<$U2iYU#0m>Icg*t^aZ(p!Za2KC?s43*a60N<=o8BrFiQN^s@YyzF4P z?f(lq^xFE_K3qTFzu)!HI>xJ=XVilqI#?^@>iqrk{PpU2`;8j*o;|*wF=BV#LkZ+flV(2e!1*2K0hV!lYs_|RICSsYr9I-C&z`{euffH`#w^mDbZPTxi6v7e1UUureAhm0 z6q2U3B1DUUq4{dS88Zf*zPRF9C+>B-pLaK}de;}Xy*+OG^0WI7C-qpolU-bV#j)q( zlfc#Q_f~wVDe+-wTVA&#wJ*M%o-j(i?;}P zH}za#`i|{wt$)w9pJ!0ux&Jq!w7kUn?xyr9oKkN$?mnEjnn57o%+l!#wM&08e2xkfd7dZqWZ&!h7s^XnwD(2vF&MnyKQ+&D*_!+B4+~~7a+==# z_*t@U&-W;eq!^8AiJ5lg?m9`PJLdn54&QNP#wx|7A-dH9n(jV=iekHdr$0R@_~m1N zc#q)2XNRBI1v5MmVc7ZSnRDLr`SrhFh-b!}K53fvsUd&gJjv5hR?HKC3XYX=u1I0~ zY9f1h|JRBh-3A-(ANKo$ruk3r{aCwfyN)HRgC8SEoe0BlWa`OZcbX|FPy-4Wqs<%F znr>rAOD{3n*}Wwj6m$&ktKTRtv0*&mc+=U#Q)X>KTiVTH56e3jc;@*`X9zlYk1jEaQxk(ST9)7b-ChvO#<6w&pI_g9KRoVmIB(}>4^Fd&$?qA%>N5ASUC=9(n5lN$ zF|m@_;=gOarmdIQCcQLTV|(|WJZ^MBLEqiU#&!+f@vQEr!@r(= z@b_uoU3LB0_H*jrI5GHd|IXh%;kxXNneQ@;GQ)H27;3)ApU%G~E`MMz;{vOb2U8y1 zdUoUIztqp)SI^t?Z8~fH8vV1+<8%&hWIlW-<=JG;OzR6B)AD=reyl5cJ%8Gs$oQIs zccFVdQ&MBb@082X; AO#lD@ literal 0 HcmV?d00001 diff --git a/examples/metroidvania/assets/rock.png b/examples/metroidvania/assets/rock.png new file mode 100644 index 0000000000000000000000000000000000000000..d8624b1169d77c643733cfd87c704d4ac5f1f838 GIT binary patch literal 2721 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 KMNUMnLSTY==<~|} literal 0 HcmV?d00001 diff --git a/examples/metroidvania/config.nut b/examples/metroidvania/config.nut new file mode 100644 index 00000000..e2bc1c16 --- /dev/null +++ b/examples/metroidvania/config.nut @@ -0,0 +1,7 @@ +config = { + width = 1280 + height = 720 + title = "EVEngine - Momentum Ruins" + debug = false + hotReload = true +}; diff --git a/examples/metroidvania/generate_map.py b/examples/metroidvania/generate_map.py new file mode 100644 index 00000000..2533db86 --- /dev/null +++ b/examples/metroidvania/generate_map.py @@ -0,0 +1,143 @@ +"""Regenerate maps/world.json for the EVEngine physics metroidvania demo.""" + +import json +from pathlib import Path + +W, H, TILE = 120, 22, 32 + + +def layer(fill=0): + return [fill] * (W * H) + + +def put(data, x, y, gid): + if 0 <= x < W and 0 <= y < H: + data[y * W + x] = gid + + +def rect(data, x, y, w, h, gid): + for yy in range(y, y + h): + for xx in range(x, x + w): + put(data, xx, yy, gid) + + +def obj(name, kind, tx, ty, w=1, h=1): + return { + "name": name, + "type": kind, + "x": tx * TILE, + "y": ty * TILE, + "width": w * TILE, + "height": h * TILE, + } + + +background = layer() +terrain = layer() +foreground = layer() +hazards = layer() + +# Region color language: grass forest, dirt ruins, stone mine/boss. +rect(background, 0, 18, 40, 1, 2) +rect(background, 40, 18, 40, 1, 4) +rect(background, 80, 18, 40, 1, 6) +rect(terrain, 0, 19, W, 3, 2) +for x in range(0, 40): + put(terrain, x, 19, 1) +for x in range(40, 80): + put(terrain, x, 19, 3) +for x in range(80, W): + put(terrain, x, 19, 5) + +# Forest: attack/kick playground and a wall-jump gate. +rect(terrain, 5, 16, 7, 1, 1) +rect(terrain, 15, 13, 7, 1, 1) +rect(terrain, 25, 16, 6, 1, 1) +rect(terrain, 34, 12, 1, 7, 2) +rect(terrain, 38, 10, 2, 9, 2) +rect(terrain, 35, 15, 3, 1, 1) + +# Ruins: vertical rooms for wall jumping, then a dash altar. +rect(terrain, 42, 16, 7, 1, 3) +rect(terrain, 51, 13, 5, 1, 3) +rect(terrain, 59, 12, 1, 7, 4) +rect(terrain, 64, 9, 1, 10, 4) +rect(terrain, 60, 15, 4, 1, 3) +rect(terrain, 65, 11, 7, 1, 3) +rect(terrain, 74, 15, 5, 1, 3) +for y in range(12, 19, 2): + put(foreground, 57, y, 9) + put(foreground, 66, y, 9) + +# Mine: lava gaps, movable rocks and a long dash bridge. +for x in range(84, 88): + put(terrain, x, 19, 0) + put(hazards, x, 19, 11) +rect(terrain, 81, 15, 3, 1, 5) +rect(terrain, 88, 15, 4, 1, 5) +rect(terrain, 94, 12, 5, 1, 5) +rect(terrain, 101, 16, 3, 1, 5) +for x in range(84, 88): + put(foreground, x, 17, 12) + +# Boss arena boundary and decoration. +rect(terrain, 105, 10, 1, 9, 6) +rect(terrain, 119, 10, 1, 9, 6) +for x in (108, 112, 116): + put(foreground, x, 18, 14) +put(foreground, 118, 18, 16) + +objects = [ + obj("player_start", "player", 3, 18), + obj("cp_forest", "checkpoint", 3, 18), + obj("crate_1", "prop_crate", 9, 15), + obj("crate_2", "prop_crate", 20, 18), + obj("rock_1", "prop_rock", 28, 15), + obj("scout_1", "enemy_melee", 18, 18), + obj("scout_2", "enemy_leaper", 29, 15), + obj("wall_jump", "ability_walljump", 36, 14), + obj("cp_ruins", "checkpoint", 42, 18), + obj("guard_1", "enemy_melee", 48, 18), + obj("guard_2", "enemy_leaper", 68, 10), + obj("dash", "ability_dash", 76, 14), + obj("cp_mine", "checkpoint", 81, 18), + obj("mine_platform", "moving_platform", 85, 17, 3, 1), + obj("rock_2", "prop_rock", 89, 14), + obj("crate_3", "prop_crate", 96, 11), + obj("miner_1", "enemy_melee", 91, 18), + obj("miner_2", "enemy_leaper", 100, 18), + obj("cp_boss", "checkpoint", 108, 18), + obj("dragon", "boss", 113, 12, 3, 3), + obj("boss_exit", "exit", 117, 18, 2, 2), +] + +tileset = { + "firstgid": 1, + "image": "assets/tiles.png", + "columns": 4, + "tilewidth": TILE, + "tileheight": TILE, + "imagewidth": 128, + "imageheight": 128, +} +doc = { + "width": W, + "height": H, + "tilewidth": TILE, + "tileheight": TILE, + "orientation": "orthogonal", + "autoReload": True, + "tilesets": [tileset], + "layers": [ + {"type": "tilelayer", "name": "background", "layer": -2, "width": W, "height": H, "data": background}, + {"type": "tilelayer", "name": "collision", "layer": 0, "width": W, "height": H, "data": terrain}, + {"type": "tilelayer", "name": "hazards", "layer": 1, "width": W, "height": H, "data": hazards}, + {"type": "tilelayer", "name": "foreground", "layer": 5, "width": W, "height": H, "data": foreground}, + {"type": "objectgroup", "name": "gameplay", "objects": objects}, + ], +} + +out = Path(__file__).with_name("maps") / "world.json" +out.parent.mkdir(parents=True, exist_ok=True) +out.write_text(json.dumps(doc, indent=2), encoding="utf-8") +print(out) diff --git a/examples/metroidvania/main.nut b/examples/metroidvania/main.nut new file mode 100644 index 00000000..2a8458b6 --- /dev/null +++ b/examples/metroidvania/main.nut @@ -0,0 +1,765 @@ +dofile("tuning.nut"); + +const CAT_TERRAIN = 1; +const CAT_PLAYER = 2; +const CAT_ENEMY = 4; +const CAT_PROP = 8; +const CAT_PLAYER_ATTACK = 16; +const CAT_PICKUP = 32; +const CAT_HAZARD = 64; + +if (!("game" in getroottable())) game <- null; +if (!("prevKeys" in getroottable())) prevKeys <- {}; + +function clampf(v, lo, hi) { + if (v < lo) return lo; + if (v > hi) return hi; + return v; +} + +function absf(v) { return v < 0.0 ? -v : v; } + +function startsWith(text, prefix) { + return text.len() >= prefix.len() && text.slice(0, prefix.len()) == prefix; +} + +function keyDown(primary, alternate = "") { + return keyboard.isDown(primary) || (alternate != "" && keyboard.isDown(alternate)); +} + +function keyPressed(primary, alternate = "") { + local now = keyDown(primary, alternate); + local key = primary + ":" + alternate; + local was = (key in prevKeys) ? prevKeys[key] : false; + prevKeys[key] <- now; + return now && !was; +} + +function bodyPair(a, b) { + return a < b ? (a + ":" + b) : (b + ":" + a); +} + +function setFixtureFilter(fx, category, mask, tag, sensor = false) { + fx.setCategoryBits(category); + fx.setMaskBits(mask); + fx.setTag(tag); + fx.setSensor(sensor); +} + +function makeSpine(model, animationName, scale) { + local modelPath = "spine/" + model + "/export/"; + local data = anim.newSpineSkeletonDataFromFile(modelPath + model + ".json"); + local atlas = anim.newSpineAtlasFromFile(modelPath + model + ".atlas"); + if (data == null || atlas == null) return null; + local skeleton = anim.newSpineSkeleton(data); + local player = anim.newSpineAnim(skeleton); + player.setAtlas(atlas); + local textures = []; + for (local i = 0; i < atlas.getPageCount(); i += 1) { + local tex = gfx.newTextureFromFile(modelPath + atlas.getPageName(i)); + player.setPageTexture(i, tex); + textures.push(tex); + } + player.setScale(scale, scale); + player.setLoop(true); + player.play(animationName); + return { data = data, atlas = atlas, skeleton = skeleton, player = player, + textures = textures, current = animationName }; +} + +function playSpine(holder, name, loop = true) { + if (holder == null || holder.current == name) return; + holder.current = name; + holder.player.setLoop(loop); + holder.player.play(name); +} + +function addBodyEntity(e) { + game.byBody[e.body.getId()] <- e; + game.entities.push(e); + return e; +} + +function spawnPlayer(x, y) { + local body = game.world.newBody("dynamic", x, y - 28.0); + body.setFixedRotation(true); + body.setBullet(true); + local main = body.newRectangleFixture(30.0, 54.0, 1.0, 0.25, 0.0); + setFixtureFilter(main, CAT_PLAYER, CAT_TERRAIN | CAT_ENEMY | CAT_PROP | CAT_HAZARD, + "player"); + local foot = body.newRectangleFixtureAt(22.0, 7.0, 0.0, 29.0, 0.0, 0.0, 0.0); + setFixtureFilter(foot, CAT_PLAYER, CAT_TERRAIN | CAT_PROP, "player_foot", true); + local wallL = body.newRectangleFixtureAt(6.0, 34.0, -18.0, 0.0, 0.0, 0.0, 0.0); + setFixtureFilter(wallL, CAT_PLAYER, CAT_TERRAIN, "player_wall_l", true); + local wallR = body.newRectangleFixtureAt(6.0, 34.0, 18.0, 0.0, 0.0, 0.0, 0.0); + setFixtureFilter(wallR, CAT_PLAYER, CAT_TERRAIN, "player_wall_r", true); + local attackL = body.newRectangleFixtureAt(48.0, 42.0, -37.0, -2.0, 0.0, 0.0, 0.0); + local attackR = body.newRectangleFixtureAt(48.0, 42.0, 37.0, -2.0, 0.0, 0.0, 0.0); + setFixtureFilter(attackL, CAT_PLAYER_ATTACK, 0, "player_attack", true); + setFixtureFilter(attackR, CAT_PLAYER_ATTACK, 0, "player_attack", true); + local e = { + kind = "player", body = body, fixtures = [main, foot, wallL, wallR, attackL, attackR], + attackL = attackL, attackR = attackR, hp = TUNE.playerHp, maxHp = TUNE.playerHp, + facing = 1, grounded = 0, wallLeft = 0, wallRight = 0, coyote = 0.0, + jumpBuffer = 0.0, dashTimer = 0.0, dashReady = true, attackTimer = 0.0, + attackActive = 0.0, combo = 0, comboGrace = 0.0, attackHits = {}, + invulnerable = 0.0, spine = makeSpine("hero", "Idle", 0.58) + }; + return addBodyEntity(e); +} + +function spawnEnemy(kind, x, y) { + local body = game.world.newBody("dynamic", x + 16.0, y - 27.0); + body.setFixedRotation(true); + body.setBullet(true); + local main = body.newRectangleFixture(31.0, 52.0, TUNE.enemyDensity, 0.35, 0.02); + setFixtureFilter(main, CAT_ENEMY, CAT_TERRAIN | CAT_PLAYER | CAT_PROP | CAT_ENEMY | + CAT_PLAYER_ATTACK, "enemy"); + local e = { + kind = kind, body = body, fixtures = [main], hp = TUNE.enemyHp, maxHp = TUNE.enemyHp, + stagger = 0.0, staggerDelay = 0.0, state = "normal", stateTimer = 0.0, + attackCooldown = 0.3, impactCooldown = 0.0, facing = -1, + spine = makeSpine("spineboy", "idle", 0.34) + }; + return addBodyEntity(e); +} + +function spawnProp(kind, x, y) { + local isRock = kind == "prop_rock"; + local body = game.world.newBody("dynamic", x + 16.0, y - 17.0); + body.setBullet(true); + local fx = isRock + ? body.newCircleFixture(15.0, TUNE.rockDensity, TUNE.frictionRock, TUNE.restitutionProp) + : body.newRectangleFixture(31.0, 31.0, TUNE.crateDensity, TUNE.frictionCrate, + TUNE.restitutionProp); + setFixtureFilter(fx, CAT_PROP, CAT_TERRAIN | CAT_PLAYER | CAT_ENEMY | CAT_PROP | + CAT_PLAYER_ATTACK, isRock ? "prop_rock" : "prop_crate"); + local e = { + kind = kind, body = body, fixtures = [fx], hp = isRock ? 999999.0 : TUNE.crateHp, + maxHp = isRock ? 999999.0 : TUNE.crateHp, impactCooldown = 0.0, + texture = isRock ? game.rockTexture : game.crateTexture, dead = false + }; + return addBodyEntity(e); +} + +function spawnSensor(kind, x, y, w, h) { + local body = game.world.newBody("static", x + w * 0.5, y - h * 0.5); + local fx = body.newRectangleFixture(w, h, 0.0, 0.0, 0.0); + setFixtureFilter(fx, kind == "hazard" ? CAT_HAZARD : CAT_PICKUP, CAT_PLAYER, kind, true); + return addBodyEntity({ kind = kind, body = body, fixtures = [fx], dead = false }); +} + +function spawnMovingPlatform(x, y, w, h) { + local body = game.world.newBody("kinematic", x + w * 0.5, y - h * 0.5); + local fx = body.newRectangleFixture(w, h, 0.0, TUNE.frictionTerrain, 0.0); + setFixtureFilter(fx, CAT_TERRAIN, CAT_PLAYER | CAT_ENEMY | CAT_PROP, "terrain"); + return addBodyEntity({ kind = "moving_platform", body = body, fixtures = [fx], time = 0.0, + speed = 130.0 }); +} + +function spawnBoss(x, y) { + local body = game.world.newBody("kinematic", x + 48.0, y - 48.0); + local fx = body.newRectangleFixture(94.0, 70.0, 1.0, 0.3, 0.03); + setFixtureFilter(fx, CAT_ENEMY, CAT_PLAYER | CAT_PROP | CAT_PLAYER_ATTACK, "boss"); + local e = { + kind = "boss", body = body, fixtures = [fx], hp = TUNE.bossHp, maxHp = TUNE.bossHp, + stagger = 0.0, staggerDelay = 0.0, state = "normal", stateTimer = 0.0, + impactCooldown = 0.0, phase = 1, time = 0.0, + homeX = x + 48.0, homeY = y - 48.0, spine = makeSpine("dragon", "flying", 0.30) + }; + game.boss = e; + return addBodyEntity(e); +} + +function buildTerrain(layer) { + local w = layer.getMapWidth(); + local h = layer.getMapHeight(); + local used = []; + for (local i = 0; i < w * h; i += 1) used.push(false); + for (local y = 0; y < h; y += 1) { + for (local x = 0; x < w; x += 1) { + local idx = y * w + x; + if (used[idx] || layer.getTile(x, y) == 0) continue; + local rw = 1; + while (x + rw < w && !used[y * w + x + rw] && layer.getTile(x + rw, y) != 0) + rw += 1; + local rh = 1; + local extend = true; + while (y + rh < h && extend) { + for (local xx = 0; xx < rw; xx += 1) { + if (used[(y + rh) * w + x + xx] || layer.getTile(x + xx, y + rh) == 0) { + extend = false; + break; + } + } + if (extend) rh += 1; + } + for (local yy = 0; yy < rh; yy += 1) + for (local xx = 0; xx < rw; xx += 1) + used[(y + yy) * w + x + xx] = true; + local tw = layer.getTileWidth(); + local th = layer.getTileHeight(); + local body = game.world.newBody("static", (x + rw * 0.5) * tw, (y + rh * 0.5) * th); + local fx = body.newRectangleFixture(rw * tw, rh * th, 0.0, TUNE.frictionTerrain, 0.0); + setFixtureFilter(fx, CAT_TERRAIN, CAT_PLAYER | CAT_ENEMY | CAT_PROP, "terrain"); + game.staticBodies.push({ body = body, fixture = fx }); + } + } +} + +function spawnMapObjects() { + for (local i = 0; i < map.getObjectCount(); i += 1) { + local kind = map.getObjectType(i); + local x = map.getObjectX(i); + local y = map.getObjectY(i); + local w = map.getObjectWidth(i); + local h = map.getObjectHeight(i); + if (kind == "player") game.player = spawnPlayer(x + 16.0, y); + else if (kind == "enemy_melee" || kind == "enemy_leaper") spawnEnemy(kind, x, y); + else if (kind == "prop_crate" || kind == "prop_rock") spawnProp(kind, x, y); + else if (kind == "moving_platform") spawnMovingPlatform(x, y, w, h); + else if (kind == "boss") spawnBoss(x, y); + else if (kind == "checkpoint" || kind == "ability_walljump" || kind == "ability_dash" || + kind == "exit") + spawnSensor(kind, x, y, w, h); + } + spawnSensor("hazard", 84.0 * 32.0, 20.0 * 32.0, 4.0 * 32.0, 32.0); +} + +function resetRun(preserveAbilities) { + local wall = preserveAbilities && game.hasWallJump; + local dash = preserveAbilities && game.hasDash; + if (game.world != null) game.world.destroy(); + game.world = physics.newWorld(0.0, TUNE.gravity, true); + game.entities = []; + game.byBody = {}; + game.staticBodies = []; + game.activeImpacts = {}; + game.effects = []; + game.boss = null; + game.hasWallJump = wall; + game.hasDash = dash; + buildTerrain(game.collisionLayer); + spawnMapObjects(); + game.player.body.setPosition(game.checkpointX, game.checkpointY); +} + +function beginAttack(stage, kick) { + local p = game.player; + p.combo = stage; + p.attackTimer = kick ? 0.32 : TUNE.attackDuration[stage - 1]; + p.attackActive = kick ? 0.13 : TUNE.attackActive[stage - 1]; + p.comboGrace = TUNE.comboGrace; + p.attackHits = {}; + local fx = p.facing > 0 ? p.attackR : p.attackL; + fx.setTag(kick ? "player_kick" : ("player_attack_" + stage)); + fx.setMaskBits(CAT_ENEMY | CAT_PROP); + playSpine(p.spine, "Attack", false); +} + +function damageActor(e, damage, stagger, impulseX, impulseY) { + if (e == null || e.hp <= 0.0 || e.impactCooldown > 0.0) return; + e.hp -= damage; + e.stagger += stagger; + e.staggerDelay = TUNE.staggerDecayDelay; + e.body.applyLinearImpulse(impulseX, impulseY); + e.impactCooldown = TUNE.impactInvulnerability; + game.hitStop = damage >= 12.0 ? 0.04 : 0.018; + if (e.hp <= 0.0) { + e.hp = 0.0; + e.state = "dead"; + if (e.kind == "boss") { + game.message = "Dragon defeated - reach the opened gate"; + game.messageTimer = 6.0; + } else playSpine(e.spine, "death", false); + return; + } + if (e.kind == "boss") { + if (e.stagger >= TUNE.staggerThreshold * 2.0) { + e.state = "knocked_down"; + e.stateTimer = 2.4; + e.stagger = 0.0; + } + return; + } + if (e.stagger >= TUNE.staggerThreshold) { + e.state = "launched"; + e.stateTimer = 0.0; + e.stagger = 0.0; + playSpine(e.spine, "hit", false); + } else { + e.state = "hit_stun"; + e.stateTimer = 0.16; + playSpine(e.spine, "hit", false); + } +} + +function handleAttackContact(aId, aTag, bId, bTag) { + local attackId = 0; + local targetId = 0; + local tag = ""; + if (startsWith(aTag, "player_attack") || aTag == "player_kick") { + attackId = aId; targetId = bId; tag = aTag; + } else if (startsWith(bTag, "player_attack") || bTag == "player_kick") { + attackId = bId; targetId = aId; tag = bTag; + } + if (attackId == 0 || attackId != game.player.body.getId()) return; + if (!(targetId in game.byBody) || (targetId in game.player.attackHits)) return; + local target = game.byBody[targetId]; + if (target.kind == "checkpoint" || target.kind == "ability_walljump" || + target.kind == "ability_dash" || target.kind == "exit") return; + game.player.attackHits[targetId] <- true; + local dir = game.player.facing.tofloat(); + if (tag == "player_kick") { + if (target.kind == "prop_crate" || target.kind == "prop_rock") { + target.body.applyLinearImpulse(TUNE.kickImpulseX * dir, TUNE.kickImpulseY); + } else if (target.kind == "enemy_melee" || target.kind == "enemy_leaper") { + damageActor(target, 5.0, 36.0, TUNE.kickImpulseX * dir, TUNE.kickImpulseY); + } + return; + } + local stage = game.player.combo; + if (target.kind == "enemy_melee" || target.kind == "enemy_leaper" || target.kind == "boss") + damageActor(target, TUNE.attackDamage[stage - 1], TUNE.attackStagger[stage - 1], + TUNE.attackImpulseX[stage - 1] * dir, TUNE.attackImpulseY[stage - 1]); +} + +function tagsAre(a, b, ta, tb) { + return (a == ta && b == tb) || (a == tb && b == ta); +} + +function processContactEvents() { + local p = game.player; + for (local i = 0; i < game.world.getBeginContactCount(); i += 1) { + local aId = game.world.getBeginContactBodyAId(i); + local bId = game.world.getBeginContactBodyBId(i); + local a = game.world.getBeginContactFixtureATag(i); + local b = game.world.getBeginContactFixtureBTag(i); + if (tagsAre(a, b, "player_foot", "terrain") || + tagsAre(a, b, "player_foot", "prop_crate") || tagsAre(a, b, "player_foot", "prop_rock")) + p.grounded += 1; + if (tagsAre(a, b, "player_wall_l", "terrain")) p.wallLeft += 1; + if (tagsAre(a, b, "player_wall_r", "terrain")) p.wallRight += 1; + handleAttackContact(aId, a, bId, b); + local other = aId == p.body.getId() ? bId : (bId == p.body.getId() ? aId : 0); + if (other != 0 && other in game.byBody) { + local e = game.byBody[other]; + if (e.kind == "checkpoint") { + game.checkpointX = e.body.getX(); game.checkpointY = e.body.getY() - 22.0; + game.message = "Checkpoint awakened"; game.messageTimer = 2.0; + } else if (e.kind == "ability_walljump" && !game.hasWallJump) { + game.hasWallJump = true; e.dead = true; e.body.setActive(false); + game.message = "Wall Jump acquired"; game.messageTimer = 3.0; + } else if (e.kind == "ability_dash" && !game.hasDash) { + game.hasDash = true; e.dead = true; e.body.setActive(false); + game.message = "Air Dash acquired"; game.messageTimer = 3.0; + } else if (e.kind == "exit") { + if (game.boss != null && game.boss.state == "dead") game.won = true; + else { game.message = "The dragon seals this gate"; game.messageTimer = 1.5; } + } else if (e.kind == "hazard") { + p.hp -= 20.0; p.body.setPosition(game.checkpointX, game.checkpointY); + p.body.setLinearVelocity(0.0, 0.0); + } + } + } + for (local i = 0; i < game.world.getEndContactCount(); i += 1) { + local aId = game.world.getEndContactBodyAId(i); + local bId = game.world.getEndContactBodyBId(i); + local a = game.world.getEndContactFixtureATag(i); + local b = game.world.getEndContactFixtureBTag(i); + if (tagsAre(a, b, "player_foot", "terrain") || + tagsAre(a, b, "player_foot", "prop_crate") || tagsAre(a, b, "player_foot", "prop_rock")) + p.grounded = p.grounded > 0 ? p.grounded - 1 : 0; + if (tagsAre(a, b, "player_wall_l", "terrain")) p.wallLeft = p.wallLeft > 0 ? p.wallLeft - 1 : 0; + if (tagsAre(a, b, "player_wall_r", "terrain")) p.wallRight = p.wallRight > 0 ? p.wallRight - 1 : 0; + local key = bodyPair(aId, bId); + if (key in game.activeImpacts) delete game.activeImpacts[key]; + } +} + +function impactCarrier(e, preSolveSpeed) { + if (e == null) return false; + if (e.kind == "prop_crate" || e.kind == "prop_rock") + return preSolveSpeed > TUNE.impactSpeedThreshold; + return ("state" in e) && e.state == "launched"; +} + +function massDamageFactor(target, other) { + if (target == null || other == null) return 1.0; + local targetMass = target.body.getMass(); + local otherMass = other.body.getMass(); + if (targetMass <= 0.0 || otherMass <= 0.0) return 1.0; + return clampf(otherMass / targetMass, 0.45, 1.8); +} + +function processImpacts() { + for (local i = 0; i < game.world.getImpactCount(); i += 1) { + local aId = game.world.getImpactBodyAId(i); + local bId = game.world.getImpactBodyBId(i); + local key = bodyPair(aId, bId); + if (key in game.activeImpacts) continue; + local speed = game.world.getImpactRelativeNormalSpeed(i); + local impulse = game.world.getImpactNormalImpulse(i); + if (speed < TUNE.impactSpeedThreshold || impulse < TUNE.impactImpulseThreshold) continue; + local a = (aId in game.byBody) ? game.byBody[aId] : null; + local b = (bId in game.byBody) ? game.byBody[bId] : null; + if (!impactCarrier(a, speed) && !impactCarrier(b, speed)) continue; + game.activeImpacts[key] <- true; + local damage = clampf(impulse * (speed - TUNE.impactSpeedThreshold) * TUNE.impactScale, + 1.0, TUNE.impactDamageCap); + local nx = game.world.getImpactNormalX(i); + local ny = game.world.getImpactNormalY(i); + if (a != null && (a.kind == "enemy_melee" || a.kind == "enemy_leaper" || a.kind == "boss")) + damageActor(a, damage * massDamageFactor(a, b), damage * 2.2, + -nx * impulse * 0.25, -ny * impulse * 0.25); + if (b != null && (b.kind == "enemy_melee" || b.kind == "enemy_leaper" || b.kind == "boss")) + damageActor(b, damage * massDamageFactor(b, a), damage * 2.2, + nx * impulse * 0.25, ny * impulse * 0.25); + if (a != null && a.kind == "prop_crate") { + a.hp -= damage; + if (impulse >= TUNE.crateBreakImpulse) a.hp = 0.0; + } + if (b != null && b.kind == "prop_crate") { + b.hp -= damage; + if (impulse >= TUNE.crateBreakImpulse) b.hp = 0.0; + } + if (a != null && "state" in a && a.state == "launched") { + a.state = "knocked_down"; a.stateTimer = TUNE.knockdownTime; + } + if (b != null && "state" in b && b.state == "launched") { + b.state = "knocked_down"; b.stateTimer = TUNE.knockdownTime; + } + game.shake = clampf(damage * 0.35, 2.0, 10.0); + game.hitStop = damage > 18.0 ? 0.05 : (damage > 7.0 ? 0.022 : game.hitStop); + local px = game.world.getImpactPointX(i); + local py = game.world.getImpactPointY(i); + for (local spark = 0; spark < 6; spark += 1) { + local spread = spark.tofloat() - 2.5; + game.effects.push({ x = px, y = py, vx = -nx * 120.0 + spread * 38.0, + vy = -ny * 120.0 - 75.0, life = 0.18 + spark * 0.015, + heavy = damage > 12.0 }); + } + } +} + +function updatePlayer(dt) { + local p = game.player; + if (p.hp <= 0.0 || p.body.getY() > 780.0) { + resetRun(true); + game.message = "Returned to checkpoint"; game.messageTimer = 2.0; + return; + } + if (p.grounded > 0) { p.coyote = TUNE.coyoteTime; p.dashReady = true; } + else p.coyote -= dt; + if (keyPressed("Space")) p.jumpBuffer = TUNE.jumpBuffer; + else p.jumpBuffer -= dt; + local move = 0.0; + if (keyDown("A", "Left")) move -= 1.0; + if (keyDown("D", "Right")) move += 1.0; + if (move != 0.0) p.facing = move > 0.0 ? 1 : -1; + + if (p.jumpBuffer > 0.0) { + if (p.coyote > 0.0) { + p.body.setLinearVelocity(p.body.getLinearVelocityX(), -TUNE.jumpSpeed); + p.jumpBuffer = 0.0; p.coyote = 0.0; + } else if (game.hasWallJump && (p.wallLeft > 0 || p.wallRight > 0)) { + local dir = p.wallLeft > 0 ? 1.0 : -1.0; + p.body.setLinearVelocity(TUNE.wallJumpX * dir, -TUNE.wallJumpY); + p.jumpBuffer = 0.0; + } + } + if (game.hasDash && p.dashReady && keyPressed("Left Shift", "Right Shift")) { + p.dashTimer = TUNE.dashTime; p.dashReady = false; + p.body.setLinearVelocity(TUNE.dashSpeed * p.facing, 0.0); + } + if (p.dashTimer > 0.0) { + p.dashTimer -= dt; + p.body.setLinearVelocity(TUNE.dashSpeed * p.facing, 0.0); + } else { + local control = p.grounded > 0 ? 1.0 : TUNE.airControl; + local target = move * TUNE.moveSpeed; + local vx = p.body.getLinearVelocityX(); + p.body.setLinearVelocity(vx + (target - vx) * clampf(dt * 14.0 * control, 0.0, 1.0), + p.body.getLinearVelocityY()); + if (game.hasWallJump && p.grounded == 0 && (p.wallLeft > 0 || p.wallRight > 0) && + p.body.getLinearVelocityY() > TUNE.wallSlideSpeed) + p.body.setLinearVelocity(p.body.getLinearVelocityX(), TUNE.wallSlideSpeed); + } + + p.comboGrace -= dt; + if (keyPressed("J")) { + local next = p.comboGrace > 0.0 ? (p.combo % 3) + 1 : 1; + beginAttack(next, false); + } + if (keyPressed("K")) beginAttack(1, true); + if (p.attackTimer > 0.0) { + p.attackTimer -= dt; p.attackActive -= dt; + if (p.attackActive <= 0.0) { p.attackL.setMaskBits(0); p.attackR.setMaskBits(0); } + } + if (p.invulnerable > 0.0) p.invulnerable -= dt; + + if (p.attackTimer <= 0.0) { + local vy = p.body.getLinearVelocityY(); + if (p.grounded > 0) playSpine(p.spine, absf(p.body.getLinearVelocityX()) > 25.0 ? "Run" : "Idle"); + else playSpine(p.spine, vy < 0.0 ? "Jump" : "Fall"); + } +} + +function updateEnemies(dt) { + local px = game.player.body.getX(); + local py = game.player.body.getY(); + foreach (e in game.entities) { + if (!("state" in e) || e.kind == "player" || e.kind == "boss" || e.state == "dead") continue; + if (e.impactCooldown > 0.0) e.impactCooldown -= dt; + if (e.staggerDelay > 0.0) e.staggerDelay -= dt; + else e.stagger = clampf(e.stagger - TUNE.staggerDecay * dt, 0.0, TUNE.staggerThreshold); + if (e.state == "hit_stun") { + e.stateTimer -= dt; + if (e.stateTimer <= 0.0) e.state = "normal"; + } else if (e.state == "knocked_down") { + e.stateTimer -= dt; + if (e.stateTimer <= 0.0) { e.state = "getting_up"; e.stateTimer = TUNE.getupTime; } + } else if (e.state == "getting_up") { + e.stateTimer -= dt; + if (e.stateTimer <= 0.0) e.state = "normal"; + } + if (e.state != "normal") continue; + e.attackCooldown -= dt; + local dx = px - e.body.getX(); + local dy = py - e.body.getY(); + e.facing = dx >= 0.0 ? 1 : -1; + if (absf(dx) < 42.0 && absf(dy) < 52.0 && e.attackCooldown <= 0.0) { + if (game.player.invulnerable <= 0.0) { + game.player.hp -= 9.0; + game.player.invulnerable = 0.55; + game.player.body.applyLinearImpulse(-e.facing * 240.0, -80.0); + } + e.attackCooldown = 1.0; + playSpine(e.spine, "shoot", false); + } else if (absf(dx) < 430.0) { + local speed = e.kind == "enemy_leaper" ? 105.0 : 78.0; + e.body.setLinearVelocity(e.facing * speed, e.body.getLinearVelocityY()); + if (e.kind == "enemy_leaper" && absf(dx) < 180.0 && e.attackCooldown <= 0.0) { + e.body.applyLinearImpulse(e.facing * 190.0, -420.0); + e.attackCooldown = 1.8; + playSpine(e.spine, "jump"); + } else playSpine(e.spine, "run"); + } else playSpine(e.spine, "idle"); + } +} + +function updateBoss(dt) { + local b = game.boss; + if (b == null || b.state == "dead") return; + if (b.impactCooldown > 0.0) b.impactCooldown -= dt; + b.time += dt; + b.phase = b.hp < b.maxHp * 0.33 ? 3 : (b.hp < b.maxHp * 0.66 ? 2 : 1); + if (b.state == "knocked_down") { + b.stateTimer -= dt; + if (b.stateTimer <= 0.0) b.state = "normal"; + return; + } + local radiusX = b.phase == 1 ? 115.0 : 175.0; + local radiusY = b.phase == 3 ? 105.0 : 65.0; + b.body.setPosition(b.homeX + sin(b.time * (0.7 + b.phase * 0.16)) * radiusX, + b.homeY + sin(b.time * 1.35) * radiusY); + if (absf(game.player.body.getX() - b.body.getX()) < 70.0 && + absf(game.player.body.getY() - b.body.getY()) < 65.0 && game.player.invulnerable <= 0.0) { + game.player.hp -= 14.0 + b.phase * 2.0; + game.player.invulnerable = 0.7; + game.player.body.applyLinearImpulse((game.player.body.getX() < b.body.getX() ? -420.0 : 420.0), -180.0); + } +} + +function updatePlatforms(dt) { + foreach (e in game.entities) { + if (e.kind != "moving_platform") continue; + e.time += dt; + e.body.setLinearVelocity(cos(e.time * 1.35) * e.speed, 0.0); + } +} + +function updateEffects(dt) { + for (local i = game.effects.len() - 1; i >= 0; i -= 1) { + local effect = game.effects[i]; + effect.life -= dt; + if (effect.life <= 0.0) { game.effects.remove(i); continue; } + effect.x += effect.vx * dt; + effect.y += effect.vy * dt; + effect.vy += 620.0 * dt; + } +} + +function removeBrokenProps() { + foreach (e in game.entities) { + if (e.kind == "prop_crate" && !e.dead && e.hp <= 0.0) { + e.dead = true; e.body.setActive(false); + game.message = "Crate shattered by momentum"; game.messageTimer = 1.4; + } + } +} + +function updateCamera(dt) { + local targetX = clampf(game.player.body.getX(), config.width * 0.5, 120.0 * 32.0 - config.width * 0.5); + local targetY = clampf(game.player.body.getY(), config.height * 0.5, 22.0 * 32.0 - config.height * 0.5); + game.cameraX += (targetX - game.cameraX) * clampf(dt * 5.5, 0.0, 1.0); + game.cameraY += (targetY - game.cameraY) * clampf(dt * 5.5, 0.0, 1.0); + game.camera.setPosition(game.cameraX, game.cameraY); + if (game.shake > 0.0) game.shake -= dt * 24.0; +} + +function buildHud() { + ui.beginBuild(); + ui.beginWindow("Momentum Ruins", "root"); + ui.progress(1.0, "hp", ""); + ui.text("HP", "hptext"); + ui.text("", "ability"); + ui.text("A/D move Space jump J combo K kick Shift dash", "help"); + ui.text("", "message"); + ui.progress(1.0, "boss", ""); + ui.text("", "bosstext"); + ui.end(); + ui.mountBuildAs("hud"); + ui.select("hud"); + ui.setHostOverlay(true); + ui.setHostPos(16.0, 16.0, 0.0, 0.0); +} + +function refreshHud() { + ui.select("hud"); + ui.setValue("hp", clampf(game.player.hp / game.player.maxHp, 0.0, 1.0)); + ui.setText("hptext", "HP " + game.player.hp.tointeger() + "/" + game.player.maxHp.tointeger()); + ui.setText("ability", "Abilities: Wall Jump " + (game.hasWallJump ? "ON" : "--") + + " Air Dash " + (game.hasDash ? "ON" : "--")); + ui.setText("message", game.won ? "RUINS CLEARED - press R to restart" : game.message); + if (game.boss != null && game.boss.hp > 0.0 && game.player.body.getX() > 104.0 * 32.0) { + ui.setValue("boss", game.boss.hp / game.boss.maxHp); + ui.setText("bosstext", "Dragon " + game.boss.hp.tointeger() + "/" + game.boss.maxHp.tointeger()); + } else { + ui.setValue("boss", 0.0); ui.setText("bosstext", ""); + } +} + +eve_init = function() { + gfx.setBackgroundColor(0.055, 0.075, 0.11, 1.0); + physics.setMeter(30.0); + local spineRoot = fs.getSourceBaseDirectory() + "/../test/assets/spine"; + fs.allowMountingForPath(spineRoot); + if (!fs.mountPath(spineRoot, "spine", false)) + print("warning: could not mount test Spine assets from " + spineRoot + "\n"); + map.loadFromFile("maps/world.json"); + local camera = eve.Camera2D(); + for (local i = 0; i < 4; i += 1) { + local layer = map.getLayer(i); + if (layer != null) layer.setCamera(camera); + } + game = { + world = null, entities = [], byBody = {}, staticBodies = [], player = null, boss = null, + collisionLayer = map.getLayer(1), camera = camera, cameraX = 640.0, cameraY = 360.0, + checkpointX = 112.0, checkpointY = 548.0, hasWallJump = false, hasDash = false, + activeImpacts = {}, crateTexture = gfx.newTextureFromFile("assets/crate.png"), + rockTexture = gfx.newTextureFromFile("assets/rock.png"), + backgrounds = [gfx.newTextureFromFile("assets/background_forest.png"), + gfx.newTextureFromFile("assets/background_ruins.png"), + gfx.newTextureFromFile("assets/background_mine.png")], + message = "Reach the ruins", + messageTimer = 3.0, shake = 0.0, hitStop = 0.0, accumulator = 0.0, + effects = [], won = false + }; + buildHud(); + resetRun(false); +}; + +function simulateStep(dt) { + if (game.won && keyPressed("R")) { game.won = false; resetRun(false); } + game.world.clearContactEvents(); + updatePlayer(dt); + updateEnemies(dt); + updateBoss(dt); + updatePlatforms(dt); + game.world.updateFull(dt, 8, 3); + processContactEvents(); + processImpacts(); + removeBrokenProps(); + updateEffects(dt); + anim.update(dt); + map.update(dt); + updateCamera(dt); + if (game.messageTimer > 0.0) game.messageTimer -= dt; + else game.message = ""; + refreshHud(); +} + +eve_update = function(dt) { + if (game.hitStop > 0.0) { game.hitStop -= clampf(dt, 0.0, 0.05); return; } + game.accumulator += clampf(dt, 0.0, 0.05); + local steps = 0; + while (game.accumulator >= 1.0 / 60.0 && steps < 4) { + simulateStep(1.0 / 60.0); + game.accumulator -= 1.0 / 60.0; + steps += 1; + if (game.hitStop > 0.0) break; + } +}; + +function screenX(wx) { + return game.camera.worldToScreenX(wx, 0.0, config.width.tofloat(), config.height.tofloat()); +} +function screenY(wy) { + return game.camera.worldToScreenY(0.0, wy, config.width.tofloat(), config.height.tofloat()); +} + +eve_render = function() { + gfx.clear(); + local region = game.player.body.getX() < 40.0 * 32.0 ? 0 : (game.player.body.getX() < 80.0 * 32.0 ? 1 : 2); + local tintR = region == 0 ? 0.08 : (region == 1 ? 0.10 : 0.13); + local tintG = region == 0 ? 0.16 : (region == 1 ? 0.10 : 0.09); + gfx.drawTexturedRect(game.backgrounds[region], 0.0, 0.0, + config.width.tofloat(), config.height.tofloat(), + 0.72, 0.72, 0.78, 1.0); + gfx.drawSolidRect(0.0, 0.0, config.width.tofloat(), config.height.tofloat(), + tintR, tintG, 0.18, 0.45); + map.render(gfx); + + foreach (e in game.entities) { + if ("dead" in e && e.dead) continue; + if (e.kind == "prop_crate" || e.kind == "prop_rock") { + local size = e.kind == "prop_rock" ? 42.0 : 40.0; + if (e.body.getLinearSpeed() > TUNE.impactSpeedThreshold) { + local trailX = e.body.getX() - e.body.getLinearVelocityX() * 0.045; + local trailY = e.body.getY() - e.body.getLinearVelocityY() * 0.045; + gfx.drawSolidRect(screenX(trailX) - size * 0.35, screenY(trailY) - size * 0.35, + size * 0.7, size * 0.7, 0.9, 0.7, 0.35, 0.30); + } + gfx.drawTexturedRect(e.texture, screenX(e.body.getX()) - size * 0.5, + screenY(e.body.getY()) - size * 0.5, size, size, 1.0, 1.0, 1.0, 1.0); + } else if (e.kind == "moving_platform") { + gfx.drawSolidRect(screenX(e.body.getX()) - 48.0, screenY(e.body.getY()) - 12.0, + 96.0, 24.0, 0.36, 0.42, 0.48, 1.0); + } else if ("spine" in e && e.spine != null && (!("state" in e) || e.state != "dead")) { + local sx = screenX(e.body.getX()); + local sy = screenY(e.body.getY()); + local facing = ("facing" in e) ? e.facing : 1; + local baseScale = e.kind == "player" ? 0.58 : (e.kind == "boss" ? 0.30 : 0.34); + e.spine.player.setScale(baseScale * facing, baseScale); + e.spine.player.setPosition(sx, sy + (e.kind == "boss" ? 20.0 : 27.0)); + if ("impactCooldown" in e && e.impactCooldown > 0.0) + e.spine.player.setColor(1.0, 0.55, 0.48, 1.0); + else e.spine.player.setColor(1.0, 1.0, 1.0, 1.0); + e.spine.player.draw(gfx); + } + } + foreach (effect in game.effects) { + local radius = effect.heavy ? 7.0 : 4.0; + gfx.drawSolidRect(screenX(effect.x) - radius, screenY(effect.y) - radius, + radius * 2.0, radius * 2.0, 1.0, + effect.heavy ? 0.42 : 0.76, 0.22, + clampf(effect.life * 4.0, 0.0, 1.0)); + } + if (config.debug) game.world.drawDebug(gfx); + ui.beginFrameAndRender(); +}; + +eve_quit = function() { + if (game != null && game.world != null) game.world.destroy(); +}; diff --git a/examples/metroidvania/maps/world.json b/examples/metroidvania/maps/world.json new file mode 100644 index 00000000..3f9f0ee3 --- /dev/null +++ b/examples/metroidvania/maps/world.json @@ -0,0 +1,10791 @@ +{ + "width": 120, + "height": 22, + "tilewidth": 32, + "tileheight": 32, + "orientation": "orthogonal", + "autoReload": true, + "tilesets": [ + { + "firstgid": 1, + "image": "assets/tiles.png", + "columns": 4, + "tilewidth": 32, + "tileheight": 32, + "imagewidth": 128, + "imageheight": 128 + } + ], + "layers": [ + { + "type": "tilelayer", + "name": "background", + "layer": -2, + "width": 120, + "height": 22, + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 4, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "type": "tilelayer", + "name": "collision", + "layer": 0, + "width": 120, + "height": 22, + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 2, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 5, + 5, + 5, + 5, + 5, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 2, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 3, + 3, + 3, + 3, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 2, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 1, + 1, + 1, + 2, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 3, + 3, + 3, + 3, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 3, + 3, + 3, + 3, + 0, + 0, + 5, + 5, + 5, + 0, + 0, + 0, + 0, + 5, + 5, + 5, + 5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 2, + 2, + 0, + 0, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 5, + 5, + 5, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 2, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 0, + 0, + 0, + 2, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 4, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 6, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 5, + 5, + 5, + 5, + 0, + 0, + 0, + 0, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ] + }, + { + "type": "tilelayer", + "name": "hazards", + "layer": 1, + "width": 120, + "height": 22, + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 11, + 11, + 11, + 11, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "type": "tilelayer", + "name": "foreground", + "layer": 5, + "width": 120, + "height": 22, + "data": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 12, + 12, + 12, + 12, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 9, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 14, + 0, + 0, + 0, + 14, + 0, + 0, + 0, + 14, + 0, + 16, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "type": "objectgroup", + "name": "gameplay", + "objects": [ + { + "name": "player_start", + "type": "player", + "x": 96, + "y": 576, + "width": 32, + "height": 32 + }, + { + "name": "cp_forest", + "type": "checkpoint", + "x": 96, + "y": 576, + "width": 32, + "height": 32 + }, + { + "name": "crate_1", + "type": "prop_crate", + "x": 288, + "y": 480, + "width": 32, + "height": 32 + }, + { + "name": "crate_2", + "type": "prop_crate", + "x": 640, + "y": 576, + "width": 32, + "height": 32 + }, + { + "name": "rock_1", + "type": "prop_rock", + "x": 896, + "y": 480, + "width": 32, + "height": 32 + }, + { + "name": "scout_1", + "type": "enemy_melee", + "x": 576, + "y": 576, + "width": 32, + "height": 32 + }, + { + "name": "scout_2", + "type": "enemy_leaper", + "x": 928, + "y": 480, + "width": 32, + "height": 32 + }, + { + "name": "wall_jump", + "type": "ability_walljump", + "x": 1152, + "y": 448, + "width": 32, + "height": 32 + }, + { + "name": "cp_ruins", + "type": "checkpoint", + "x": 1344, + "y": 576, + "width": 32, + "height": 32 + }, + { + "name": "guard_1", + "type": "enemy_melee", + "x": 1536, + "y": 576, + "width": 32, + "height": 32 + }, + { + "name": "guard_2", + "type": "enemy_leaper", + "x": 2176, + "y": 320, + "width": 32, + "height": 32 + }, + { + "name": "dash", + "type": "ability_dash", + "x": 2432, + "y": 448, + "width": 32, + "height": 32 + }, + { + "name": "cp_mine", + "type": "checkpoint", + "x": 2592, + "y": 576, + "width": 32, + "height": 32 + }, + { + "name": "mine_platform", + "type": "moving_platform", + "x": 2720, + "y": 544, + "width": 96, + "height": 32 + }, + { + "name": "rock_2", + "type": "prop_rock", + "x": 2848, + "y": 448, + "width": 32, + "height": 32 + }, + { + "name": "crate_3", + "type": "prop_crate", + "x": 3072, + "y": 352, + "width": 32, + "height": 32 + }, + { + "name": "miner_1", + "type": "enemy_melee", + "x": 2912, + "y": 576, + "width": 32, + "height": 32 + }, + { + "name": "miner_2", + "type": "enemy_leaper", + "x": 3200, + "y": 576, + "width": 32, + "height": 32 + }, + { + "name": "cp_boss", + "type": "checkpoint", + "x": 3456, + "y": 576, + "width": 32, + "height": 32 + }, + { + "name": "dragon", + "type": "boss", + "x": 3616, + "y": 384, + "width": 96, + "height": 96 + }, + { + "name": "boss_exit", + "type": "exit", + "x": 3744, + "y": 576, + "width": 64, + "height": 64 + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/metroidvania/smoke_test.py b/examples/metroidvania/smoke_test.py new file mode 100644 index 00000000..0b3c1dc0 --- /dev/null +++ b/examples/metroidvania/smoke_test.py @@ -0,0 +1,49 @@ +"""Launch Momentum Ruins and fail if the engine reports a frame/runtime error.""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +import time +from pathlib import Path + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("engine", type=Path, help="Path to the eve executable") + parser.add_argument("--seconds", type=float, default=6.0) + args = parser.parse_args() + + game_dir = Path(__file__).resolve().parent + process = subprocess.Popen( + [str(args.engine.resolve()), "run", str(game_dir)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding="utf-8", + errors="replace", + ) + time.sleep(args.seconds) + exited_early = process.poll() is not None + if not exited_early: + process.terminate() + try: + process.wait(timeout=3.0) + except subprocess.TimeoutExpired: + process.kill() + stdout, stderr = process.communicate() + combined = stdout + "\n" + stderr + bad_markers = ("frame error", "Run failed:", "Runtime error") + failures = [marker for marker in bad_markers if marker.lower() in combined.lower()] + + if failures or (exited_early and process.returncode != 0): + print(combined, file=sys.stderr) + print("Smoke test failed: " + ", ".join(failures), file=sys.stderr) + return 1 + print(f"Smoke test passed ({args.seconds:.1f}s, no frame/runtime errors).") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/metroidvania/tuning.nut b/examples/metroidvania/tuning.nut new file mode 100644 index 00000000..221a8a04 --- /dev/null +++ b/examples/metroidvania/tuning.nut @@ -0,0 +1,45 @@ +// All combat and physics tuning lives here so momentum behavior is easy to audit. +TUNE <- { + gravity = 1200.0, + moveSpeed = 240.0, + airControl = 0.72, + jumpSpeed = 470.0, + wallJumpX = 330.0, + wallJumpY = 440.0, + wallSlideSpeed = 105.0, + coyoteTime = 0.10, + jumpBuffer = 0.12, + dashSpeed = 650.0, + dashTime = 0.16, + playerHp = 100.0, + enemyHp = 55.0, + bossHp = 360.0, + staggerThreshold = 80.0, + staggerDecayDelay = 0.8, + staggerDecay = 24.0, + attackDamage = [7.0, 9.0, 14.0], + attackStagger = [20.0, 29.0, 48.0], + attackImpulseX = [250.0, 360.0, 780.0], + attackImpulseY = [-25.0, -55.0, -330.0], + attackDuration = [0.27, 0.30, 0.40], + attackActive = [0.10, 0.11, 0.14], + comboGrace = 0.32, + kickImpulseX = 720.0, + kickImpulseY = -105.0, + impactSpeedThreshold = 180.0, + impactImpulseThreshold = 35.0, + impactScale = 0.00022, + impactDamageCap = 35.0, + impactInvulnerability = 0.18, + crateHp = 30.0, + crateBreakImpulse = 520.0, + crateDensity = 1.0, + rockDensity = 8.0, + enemyDensity = 1.15, + frictionTerrain = 0.75, + frictionCrate = 0.45, + frictionRock = 0.85, + restitutionProp = 0.05, + knockdownTime = 1.15, + getupTime = 0.34 +}; diff --git a/src/modules/animation/Animation.cpp b/src/modules/animation/Animation.cpp index a932075d..68512d37 100644 --- a/src/modules/animation/Animation.cpp +++ b/src/modules/animation/Animation.cpp @@ -740,6 +740,7 @@ void Animation::expose(ssq::Table &table) { spanim.addFunc("getAnimationDuration", &SpineAnim::getAnimationDuration); spanim.addFunc("apply", &SpineAnim::apply); spanim.addFunc("update", &SpineAnim::update); + spanim.addFunc("draw", &SpineAnim::draw); // collectDrawItems is C++-only (shared DrawItem2D queue); scripts use getDrawSlot*. spanim.addFunc("getDrawSlotCount", &SpineAnim::getDrawSlotCount); spanim.addFunc("getDrawSlotX", &SpineAnim::getDrawSlotX); diff --git a/src/modules/animation/SpineAnim.cpp b/src/modules/animation/SpineAnim.cpp index dc4da5cd..31a265f6 100644 --- a/src/modules/animation/SpineAnim.cpp +++ b/src/modules/animation/SpineAnim.cpp @@ -4,6 +4,8 @@ #include "animation/SpineAtlas.h" #include "animation/SpineSkeleton.h" #include "common/Exception.h" +#include "graphics/Graphics.h" +#include "graphics/RenderSystem.h" #include #include @@ -286,6 +288,13 @@ bool SpineAnim::update(float dt) { return true; } +void SpineAnim::draw(graphics::Graphics *gfx) { + if (!gfx) return; + std::vector items; + collectDrawItems(items); + graphics::RenderSystem::drawItems(*gfx, items, false); +} + void SpineAnim::rebuildDrawSlots() { drawSlots_.clear(); if (!skeleton_) return; @@ -405,6 +414,16 @@ void SpineAnim::collectDrawItems(std::vector &out) { item.v0 = ds.v0; item.u1 = ds.u1; item.v1 = ds.v1; + // A reflected skeleton cannot be represented by rotation plus positive + // quad extents alone. Reconstructing the reflected quad adds a 180-degree + // rotation, so regular regions need a V flip to leave only the requested + // horizontal reflection. Rotated atlas regions exchange the UV axes. + if (scaleX_ < 0.f) { + if (ds.rotated) + std::swap(item.u0, item.u1); + else + std::swap(item.v0, item.v1); + } out.push_back(item); } } diff --git a/src/modules/animation/SpineAnim.h b/src/modules/animation/SpineAnim.h index b65efa59..3eb328f7 100644 --- a/src/modules/animation/SpineAnim.h +++ b/src/modules/animation/SpineAnim.h @@ -7,6 +7,10 @@ #include #include +namespace eve::graphics { +class Graphics; +} + namespace eve::animation { class Animation; @@ -77,6 +81,8 @@ public: /** Append region attachment quads into the shared 2D draw queue. */ void collectDrawItems(std::vector &out); + /** Draw the current pose into an existing frame without clearing or presenting it. */ + void draw(graphics::Graphics *gfx); /** * Query posed draw slots without textures (for unit tests). diff --git a/src/modules/filesystem/Filesystem.cpp b/src/modules/filesystem/Filesystem.cpp index 7f07738a..58379886 100644 --- a/src/modules/filesystem/Filesystem.cpp +++ b/src/modules/filesystem/Filesystem.cpp @@ -44,6 +44,14 @@ void Filesystem::expose(ssq::Class &cls) { cls.addFunc("getIdentity", &Filesystem::getIdentity); cls.addFunc("setSource", &Filesystem::setSource); cls.addFunc("getSource", &Filesystem::getSource); + cls.addFunc("allowMountingForPath", &Filesystem::allowMountingForPath); + cls.addFunc("mountPath", [](Filesystem *self, const std::string &archive, + const std::string &mountpoint, bool appendToPath) { + return self && self->mount(archive, mountpoint, appendToPath); + }); + cls.addFunc("unmountPath", [](Filesystem *self, const std::string &archive) { + return self && self->unmount(archive); + }); cls.addFunc("newFile", &Filesystem::newFile); cls.addFunc("newFileData", &Filesystem::newFileData); cls.addFunc("getWorkingDirectory", &Filesystem::getWorkingDirectory); diff --git a/src/modules/graphics/Graphics.cpp b/src/modules/graphics/Graphics.cpp index a41b2553..188687af 100644 --- a/src/modules/graphics/Graphics.cpp +++ b/src/modules/graphics/Graphics.cpp @@ -423,6 +423,7 @@ void Graphics::expose(ssq::Class &cls) { cls.addFunc("setBackgroundColor", &Graphics::setBackgroundColorRGBA); cls.addFunc("drawSolidRect", &Graphics::drawSolidRectRGBA); cls.addFunc("drawTexturedRect", &Graphics::drawTexturedRectRGBA); + cls.addFunc("newTextureFromFile", &Graphics::newTextureFromFile); cls.addFunc("newTexture", static_cast( &Graphics::newTextureFromImageData)); diff --git a/src/modules/map/Map.cpp b/src/modules/map/Map.cpp index c255e9ef..d1a10274 100644 --- a/src/modules/map/Map.cpp +++ b/src/modules/map/Map.cpp @@ -49,14 +49,16 @@ TileLayer *Map::newLayerFromFile(const std::string &path) { std::vector objs; auto layers = loadMapFile(path, &objs, nullptr); setObjects(std::move(objs)); - return layers.empty() ? nullptr : layers.front(); + loadedLayers_ = layers; + return loadedLayers_.empty() ? nullptr : loadedLayers_.front(); } int Map::loadFromFile(const std::string &path) { std::vector objs; auto layers = loadMapFile(path, &objs, nullptr); setObjects(std::move(objs)); - return int(layers.size()); + loadedLayers_ = std::move(layers); + return int(loadedLayers_.size()); } void Map::update(float dt) { @@ -82,6 +84,11 @@ int Map::getLayerCount() const { return n; } +TileLayer *Map::getLayer(int index) const { + if (index < 0 || index >= int(loadedLayers_.size())) return nullptr; + return loadedLayers_[size_t(index)]; +} + void Map::setObjects(std::vector objects) { objects_ = std::move(objects); } int Map::getObjectCount() const { return int(objects_.size()); } @@ -326,6 +333,7 @@ void Map::expose(ssq::Class &cls) { cls.addFunc("render", &Map::render); cls.addFunc("pollConfigs", &Map::pollConfigs); cls.addFunc("getLayerCount", &Map::getLayerCount); + cls.addFunc("getLayer", &Map::getLayer); cls.addFunc("getObjectCount", &Map::getObjectCount); cls.addFunc("getObjectName", &Map::getObjectName); cls.addFunc("getObjectType", &Map::getObjectType); diff --git a/src/modules/map/Map.h b/src/modules/map/Map.h index a276b899..d585f9e4 100644 --- a/src/modules/map/Map.h +++ b/src/modules/map/Map.h @@ -56,6 +56,8 @@ public: int pollConfigs(); int getLayerCount() const; + /** Layer created by the most recent loadFromFile/newLayerFromFile call. */ + TileLayer *getLayer(int index) const; int getObjectCount() const; std::string getObjectName(int i) const; @@ -87,6 +89,7 @@ public: private: std::vector objects_; + std::vector loadedLayers_; std::string dualGridError_; }; diff --git a/src/modules/physics/Body.cpp b/src/modules/physics/Body.cpp index e1ae4618..0f1f4437 100644 --- a/src/modules/physics/Body.cpp +++ b/src/modules/physics/Body.cpp @@ -120,6 +120,23 @@ float Body::getLinearVelocityY() const { return world_->toPixels(body_->GetLinearVelocity().y); } +float Body::getLinearSpeed() const { + if (!body_ || !world_) return 0.f; + return world_->toPixels(body_->GetLinearVelocity().Length()); +} + +float Body::getMass() const { return body_ ? body_->GetMass() : 0.f; } + +float Body::getWorldCenterX() const { + if (!body_ || !world_) return 0.f; + return world_->toPixels(body_->GetWorldCenter().x); +} + +float Body::getWorldCenterY() const { + if (!body_ || !world_) return 0.f; + return world_->toPixels(body_->GetWorldCenter().y); +} + void Body::setAngularVelocity(float omega) { if (!body_) return; body_->SetAngularVelocity(omega); @@ -193,12 +210,18 @@ bool Body::isAwake() const { return body_ ? body_->IsAwake() : false; } Fixture *Body::newRectangleFixture(float width, float height, float density, float friction, float restitution) { + return newRectangleFixtureAt(width, height, 0.f, 0.f, density, friction, restitution); +} + +Fixture *Body::newRectangleFixtureAt(float width, float height, float offsetX, float offsetY, + float density, float friction, float restitution) { if (!body_ || !world_) throw eve::Exception("Body.newRectangleFixture: body destroyed"); if (width <= 0.f || height <= 0.f) throw eve::Exception("Body.newRectangleFixture: width/height must be > 0"); b2PolygonShape shape; - shape.SetAsBox(world_->toMeters(width) * 0.5f, world_->toMeters(height) * 0.5f); + shape.SetAsBox(world_->toMeters(width) * 0.5f, world_->toMeters(height) * 0.5f, + b2Vec2(world_->toMeters(offsetX), world_->toMeters(offsetY)), 0.f); b2FixtureDef def; def.shape = &shape; diff --git a/src/modules/physics/Body.h b/src/modules/physics/Body.h index e7268c4b..05ce6088 100644 --- a/src/modules/physics/Body.h +++ b/src/modules/physics/Body.h @@ -29,6 +29,10 @@ public: void setLinearVelocity(float vx, float vy); float getLinearVelocityX() const; float getLinearVelocityY() const; + float getLinearSpeed() const; + float getMass() const; + float getWorldCenterX() const; + float getWorldCenterY() const; void setAngularVelocity(float omega); float getAngularVelocity() const; @@ -56,6 +60,10 @@ public: /** width/height in pixels; density kg/m² (Box2D units). */ Fixture *newRectangleFixture(float width, float height, float density = 1.f, float friction = 0.2f, float restitution = 0.f); + /** Rectangle fixture with a local pixel-space offset from the body origin. */ + Fixture *newRectangleFixtureAt(float width, float height, float offsetX, float offsetY, + float density = 1.f, float friction = 0.2f, + float restitution = 0.f); Fixture *newCircleFixture(float radius, float density = 1.f, float friction = 0.2f, float restitution = 0.f); diff --git a/src/modules/physics/Fixture.cpp b/src/modules/physics/Fixture.cpp index fe63018c..c8621f81 100644 --- a/src/modules/physics/Fixture.cpp +++ b/src/modules/physics/Fixture.cpp @@ -11,9 +11,9 @@ Fixture::Fixture(World *world, Body *body, b2Fixture *fixture) Fixture::~Fixture() { if (fixture_ && body_ && body_->raw()) { + if (world_) world_->forgetFixture(this); fixture_->SetUserData(nullptr); body_->raw()->DestroyFixture(fixture_); - if (world_) world_->forgetFixture(this); } fixture_ = nullptr; body_ = nullptr; @@ -32,9 +32,9 @@ void Fixture::destroy() { invalidate(); return; } + if (world_) world_->forgetFixture(this); fixture_->SetUserData(nullptr); body_->raw()->DestroyFixture(fixture_); - if (world_) world_->forgetFixture(this); fixture_ = nullptr; body_ = nullptr; world_ = nullptr; @@ -69,6 +69,39 @@ void Fixture::setDensity(float density) { float Fixture::getDensity() const { return fixture_ ? fixture_->GetDensity() : 0.f; } +void Fixture::setCategoryBits(int bits) { + if (!fixture_) return; + b2Filter filter = fixture_->GetFilterData(); + filter.categoryBits = static_cast(bits); + fixture_->SetFilterData(filter); +} + +int Fixture::getCategoryBits() const { + return fixture_ ? int(fixture_->GetFilterData().categoryBits) : 0; +} + +void Fixture::setMaskBits(int bits) { + if (!fixture_) return; + b2Filter filter = fixture_->GetFilterData(); + filter.maskBits = static_cast(bits); + fixture_->SetFilterData(filter); +} + +int Fixture::getMaskBits() const { return fixture_ ? int(fixture_->GetFilterData().maskBits) : 0; } + +void Fixture::setGroupIndex(int index) { + if (!fixture_) return; + b2Filter filter = fixture_->GetFilterData(); + filter.groupIndex = static_cast(index); + fixture_->SetFilterData(filter); +} + +int Fixture::getGroupIndex() const { + return fixture_ ? int(fixture_->GetFilterData().groupIndex) : 0; +} + +int Fixture::getBodyId() const { return body_ ? body_->getId() : 0; } + bool Fixture::testPoint(float x, float y) const { if (!fixture_ || !world_) return false; return fixture_->TestPoint(b2Vec2(world_->toMeters(x), world_->toMeters(y))); diff --git a/src/modules/physics/Fixture.h b/src/modules/physics/Fixture.h index 7aed244f..f501bb45 100644 --- a/src/modules/physics/Fixture.h +++ b/src/modules/physics/Fixture.h @@ -1,5 +1,7 @@ #pragma once +#include + class b2Fixture; namespace eve::physics { @@ -27,6 +29,18 @@ public: void setDensity(float density); float getDensity() const; + void setTag(const std::string &tag) { tag_ = tag; } + const std::string &getTag() const { return tag_; } + + void setCategoryBits(int bits); + int getCategoryBits() const; + void setMaskBits(int bits); + int getMaskBits() const; + void setGroupIndex(int index); + int getGroupIndex() const; + + int getBodyId() const; + Body *getBody() { return body_; } /** Pixel-space point-in-fixture test (uses World meter). */ @@ -46,6 +60,7 @@ private: World *world_ = nullptr; Body *body_ = nullptr; b2Fixture *fixture_ = nullptr; + std::string tag_; }; } // namespace eve::physics diff --git a/src/modules/physics/Physics.cpp b/src/modules/physics/Physics.cpp index 03cd1321..26037050 100644 --- a/src/modules/physics/Physics.cpp +++ b/src/modules/physics/Physics.cpp @@ -66,6 +66,29 @@ void Physics::expose(ssq::Table &table) { world.addFunc("queryAABB", &World::queryAABB); world.addFunc("getQueryCount", &World::getQueryCount); world.addFunc("getQueryBodyId", &World::getQueryBodyId); + world.addFunc("getBeginContactCount", &World::getBeginContactCount); + world.addFunc("getBeginContactBodyAId", &World::getBeginContactBodyAId); + world.addFunc("getBeginContactBodyBId", &World::getBeginContactBodyBId); + world.addFunc("getBeginContactFixtureATag", &World::getBeginContactFixtureATag); + world.addFunc("getBeginContactFixtureBTag", &World::getBeginContactFixtureBTag); + world.addFunc("getEndContactCount", &World::getEndContactCount); + world.addFunc("getEndContactBodyAId", &World::getEndContactBodyAId); + world.addFunc("getEndContactBodyBId", &World::getEndContactBodyBId); + world.addFunc("getEndContactFixtureATag", &World::getEndContactFixtureATag); + world.addFunc("getEndContactFixtureBTag", &World::getEndContactFixtureBTag); + world.addFunc("getImpactCount", &World::getImpactCount); + world.addFunc("getImpactBodyAId", &World::getImpactBodyAId); + world.addFunc("getImpactBodyBId", &World::getImpactBodyBId); + world.addFunc("getImpactFixtureATag", &World::getImpactFixtureATag); + world.addFunc("getImpactFixtureBTag", &World::getImpactFixtureBTag); + world.addFunc("getImpactPointX", &World::getImpactPointX); + world.addFunc("getImpactPointY", &World::getImpactPointY); + world.addFunc("getImpactNormalX", &World::getImpactNormalX); + world.addFunc("getImpactNormalY", &World::getImpactNormalY); + world.addFunc("getImpactRelativeNormalSpeed", &World::getImpactRelativeNormalSpeed); + world.addFunc("getImpactNormalImpulse", &World::getImpactNormalImpulse); + world.addFunc("getImpactTangentImpulse", &World::getImpactTangentImpulse); + world.addFunc("clearContactEvents", &World::clearContactEvents); auto world3 = table.addClass( "World3D", std::function([]() -> World3D * { return nullptr; }), true); @@ -103,6 +126,10 @@ void Physics::expose(ssq::Table &table) { body.addFunc("setLinearVelocity", &Body::setLinearVelocity); body.addFunc("getLinearVelocityX", &Body::getLinearVelocityX); body.addFunc("getLinearVelocityY", &Body::getLinearVelocityY); + body.addFunc("getLinearSpeed", &Body::getLinearSpeed); + body.addFunc("getMass", &Body::getMass); + body.addFunc("getWorldCenterX", &Body::getWorldCenterX); + body.addFunc("getWorldCenterY", &Body::getWorldCenterY); body.addFunc("setAngularVelocity", &Body::setAngularVelocity); body.addFunc("getAngularVelocity", &Body::getAngularVelocity); body.addFunc("applyForce", &Body::applyForce); @@ -120,6 +147,7 @@ void Physics::expose(ssq::Table &table) { body.addFunc("setAwake", &Body::setAwake); body.addFunc("isAwake", &Body::isAwake); body.addFunc("newRectangleFixture", &Body::newRectangleFixture); + body.addFunc("newRectangleFixtureAt", &Body::newRectangleFixtureAt); body.addFunc("newCircleFixture", &Body::newCircleFixture); body.addFunc("destroy", &Body::destroy); @@ -172,6 +200,15 @@ void Physics::expose(ssq::Table &table) { fixture.addFunc("getRestitution", &Fixture::getRestitution); fixture.addFunc("setDensity", &Fixture::setDensity); fixture.addFunc("getDensity", &Fixture::getDensity); + fixture.addFunc("setTag", &Fixture::setTag); + fixture.addFunc("getTag", &Fixture::getTag); + fixture.addFunc("setCategoryBits", &Fixture::setCategoryBits); + fixture.addFunc("getCategoryBits", &Fixture::getCategoryBits); + fixture.addFunc("setMaskBits", &Fixture::setMaskBits); + fixture.addFunc("getMaskBits", &Fixture::getMaskBits); + fixture.addFunc("setGroupIndex", &Fixture::setGroupIndex); + fixture.addFunc("getGroupIndex", &Fixture::getGroupIndex); + fixture.addFunc("getBodyId", &Fixture::getBodyId); fixture.addFunc("getBody", &Fixture::getBody); fixture.addFunc("testPoint", &Fixture::testPoint); fixture.addFunc("destroy", &Fixture::destroy); diff --git a/src/modules/physics/World.cpp b/src/modules/physics/World.cpp index 00b3ce36..4d9f877f 100644 --- a/src/modules/physics/World.cpp +++ b/src/modules/physics/World.cpp @@ -31,6 +31,26 @@ Body *bodyFromFixture(b2Fixture *f) { return static_cast(b->GetUserData()); } +Fixture *fixtureFromRaw(b2Fixture *f) { + return f ? static_cast(f->GetUserData()) : nullptr; +} + +World::ContactEvent contactEventFrom(b2Contact *contact) { + World::ContactEvent out; + if (!contact) return out; + Fixture *fa = fixtureFromRaw(contact->GetFixtureA()); + Fixture *fb = fixtureFromRaw(contact->GetFixtureB()); + if (fa) { + out.bodyAId = fa->getBodyId(); + out.fixtureATag = fa->getTag(); + } + if (fb) { + out.bodyBId = fb->getBodyId(); + out.fixtureBTag = fb->getTag(); + } + return out; +} + } // namespace class ContactRelay : public b2ContactListener { @@ -39,6 +59,12 @@ public: void BeginContact(b2Contact *contact) override { world_->onBeginContact(contact); } void EndContact(b2Contact *contact) override { world_->onEndContact(contact); } + void PreSolve(b2Contact *contact, const b2Manifold *oldManifold) override { + world_->onPreSolve(contact, oldManifold); + } + void PostSolve(b2Contact *contact, const b2ContactImpulse *impulse) override { + world_->onPostSolve(contact, impulse); + } private: World *world_; @@ -149,6 +175,7 @@ void World::destroy() { if (f) f->invalidate(); } fixtures_.clear(); + clearContactEvents(); if (world_) { world_->SetContactListener(nullptr); @@ -217,14 +244,50 @@ void World::destroyBody(Body *body) { body->destroy(); } -void World::forgetBody(Body *body) { bodies_.erase(body); } -void World::forgetFixture(Fixture *fixture) { fixtures_.erase(fixture); } +void World::forgetBody(Body *body) { + if (!body) return; + const int id = body->getId(); + bodies_.erase(body); + auto touches = [id](const ContactEvent &e) { return e.bodyAId == id || e.bodyBId == id; }; + beginContacts_.erase(std::remove_if(beginContacts_.begin(), beginContacts_.end(), touches), + beginContacts_.end()); + endContacts_.erase(std::remove_if(endContacts_.begin(), endContacts_.end(), touches), + endContacts_.end()); + impacts_.erase(std::remove_if(impacts_.begin(), impacts_.end(), touches), impacts_.end()); +} +void World::forgetFixture(Fixture *fixture) { + if (!fixture) return; + const int bodyId = fixture->getBodyId(); + const std::string tag = fixture->getTag(); + auto touches = [&](const ContactEvent &e) { + return (e.bodyAId == bodyId && e.fixtureATag == tag) || + (e.bodyBId == bodyId && e.fixtureBTag == tag); + }; + beginContacts_.erase(std::remove_if(beginContacts_.begin(), beginContacts_.end(), touches), + beginContacts_.end()); + endContacts_.erase(std::remove_if(endContacts_.begin(), endContacts_.end(), touches), + endContacts_.end()); + impacts_.erase(std::remove_if(impacts_.begin(), impacts_.end(), touches), impacts_.end()); + b2Fixture *raw = fixture->raw(); + for (auto it = preSolve_.begin(); it != preSolve_.end();) { + b2Contact *contact = it->first; + if (contact && (contact->GetFixtureA() == raw || contact->GetFixtureB() == raw)) + it = preSolve_.erase(it); + else + ++it; + } + fixtures_.erase(fixture); +} void World::onBeginContact(b2Contact *contact) { + if (!contact || !fixtureFromRaw(contact->GetFixtureA()) || + !fixtureFromRaw(contact->GetFixtureB())) return; Body *a = bodyFromFixture(contact->GetFixtureA()); Body *b = bodyFromFixture(contact->GetFixtureB()); if (!a || !b) return; + beginContacts_.push_back(contactEventFrom(contact)); + auto *ev = eve::ModuleManager::getInstance("Event"); if (!ev) return; std::vector args = {eve::event::Variant::makeInt(a->getId()), @@ -233,10 +296,15 @@ void World::onBeginContact(b2Contact *contact) { } void World::onEndContact(b2Contact *contact) { + preSolve_.erase(contact); + if (!contact || !fixtureFromRaw(contact->GetFixtureA()) || + !fixtureFromRaw(contact->GetFixtureB())) return; Body *a = bodyFromFixture(contact->GetFixtureA()); Body *b = bodyFromFixture(contact->GetFixtureB()); if (!a || !b) return; + endContacts_.push_back(contactEventFrom(contact)); + auto *ev = eve::ModuleManager::getInstance("Event"); if (!ev) return; std::vector args = {eve::event::Variant::makeInt(a->getId()), @@ -244,6 +312,121 @@ void World::onEndContact(b2Contact *contact) { ev->push(new eve::event::Message("endcontact", args)); } +void World::onPreSolve(b2Contact *contact, const b2Manifold * /*oldManifold*/) { + if (!contact || !world_) return; + b2WorldManifold manifold; + contact->GetWorldManifold(&manifold); + const b2Manifold *local = contact->GetManifold(); + if (!local || local->pointCount <= 0) return; + + b2Body *a = contact->GetFixtureA()->GetBody(); + b2Body *b = contact->GetFixtureB()->GetBody(); + if (!a || !b) return; + const b2Vec2 point = manifold.points[0]; + const b2Vec2 va = a->GetLinearVelocityFromWorldPoint(point); + const b2Vec2 vb = b->GetLinearVelocityFromWorldPoint(point); + + PreSolveData data; + data.pointX = toPixels(point.x); + data.pointY = toPixels(point.y); + data.normalX = manifold.normal.x; + data.normalY = manifold.normal.y; + data.relativeNormalSpeed = toPixels(std::max(0.f, b2Dot(va - vb, manifold.normal))); + preSolve_[contact] = data; +} + +void World::onPostSolve(b2Contact *contact, const b2ContactImpulse *impulse) { + if (!contact || !impulse) return; + auto found = preSolve_.find(contact); + if (found == preSolve_.end()) return; + + ImpactEvent out; + static_cast(out) = contactEventFrom(contact); + out.pointX = found->second.pointX; + out.pointY = found->second.pointY; + out.normalX = found->second.normalX; + out.normalY = found->second.normalY; + out.relativeNormalSpeed = found->second.relativeNormalSpeed; + const int count = contact->GetManifold() ? contact->GetManifold()->pointCount : 0; + for (int i = 0; i < count; ++i) { + out.normalImpulse += toPixels(impulse->normalImpulses[i]); + out.tangentImpulse += toPixels(std::fabs(impulse->tangentImpulses[i])); + } + if (out.normalImpulse > 0.f) impacts_.push_back(std::move(out)); +} + +namespace { +template +const Event *eventAt(const std::vector &events, int index) { + return index >= 0 && index < int(events.size()) ? &events[size_t(index)] : nullptr; +} +} // namespace + +int World::getBeginContactBodyAId(int index) const { + auto *e = eventAt(beginContacts_, index); return e ? e->bodyAId : 0; +} +int World::getBeginContactBodyBId(int index) const { + auto *e = eventAt(beginContacts_, index); return e ? e->bodyBId : 0; +} +std::string World::getBeginContactFixtureATag(int index) const { + auto *e = eventAt(beginContacts_, index); return e ? e->fixtureATag : std::string(); +} +std::string World::getBeginContactFixtureBTag(int index) const { + auto *e = eventAt(beginContacts_, index); return e ? e->fixtureBTag : std::string(); +} +int World::getEndContactBodyAId(int index) const { + auto *e = eventAt(endContacts_, index); return e ? e->bodyAId : 0; +} +int World::getEndContactBodyBId(int index) const { + auto *e = eventAt(endContacts_, index); return e ? e->bodyBId : 0; +} +std::string World::getEndContactFixtureATag(int index) const { + auto *e = eventAt(endContacts_, index); return e ? e->fixtureATag : std::string(); +} +std::string World::getEndContactFixtureBTag(int index) const { + auto *e = eventAt(endContacts_, index); return e ? e->fixtureBTag : std::string(); +} +int World::getImpactBodyAId(int index) const { + auto *e = eventAt(impacts_, index); return e ? e->bodyAId : 0; +} +int World::getImpactBodyBId(int index) const { + auto *e = eventAt(impacts_, index); return e ? e->bodyBId : 0; +} +std::string World::getImpactFixtureATag(int index) const { + auto *e = eventAt(impacts_, index); return e ? e->fixtureATag : std::string(); +} +std::string World::getImpactFixtureBTag(int index) const { + auto *e = eventAt(impacts_, index); return e ? e->fixtureBTag : std::string(); +} +float World::getImpactPointX(int index) const { + auto *e = eventAt(impacts_, index); return e ? e->pointX : 0.f; +} +float World::getImpactPointY(int index) const { + auto *e = eventAt(impacts_, index); return e ? e->pointY : 0.f; +} +float World::getImpactNormalX(int index) const { + auto *e = eventAt(impacts_, index); return e ? e->normalX : 0.f; +} +float World::getImpactNormalY(int index) const { + auto *e = eventAt(impacts_, index); return e ? e->normalY : 0.f; +} +float World::getImpactRelativeNormalSpeed(int index) const { + auto *e = eventAt(impacts_, index); return e ? e->relativeNormalSpeed : 0.f; +} +float World::getImpactNormalImpulse(int index) const { + auto *e = eventAt(impacts_, index); return e ? e->normalImpulse : 0.f; +} +float World::getImpactTangentImpulse(int index) const { + auto *e = eventAt(impacts_, index); return e ? e->tangentImpulse : 0.f; +} + +void World::clearContactEvents() { + beginContacts_.clear(); + endContacts_.clear(); + impacts_.clear(); + preSolve_.clear(); +} + void World::drawDebug(graphics::Graphics *gfx) { if (!world_ || !draw_ || !gfx) return; draw_->begin(gfx, meter_); diff --git a/src/modules/physics/World.h b/src/modules/physics/World.h index 2f613621..78658f58 100644 --- a/src/modules/physics/World.h +++ b/src/modules/physics/World.h @@ -3,12 +3,15 @@ #include #include #include +#include #include class b2World; class b2Body; class b2Fixture; class b2Contact; +class b2Manifold; +struct b2ContactImpulse; namespace eve::graphics { class Graphics; @@ -23,6 +26,22 @@ class DebugDraw; class World { public: + struct ContactEvent { + int bodyAId = 0; + int bodyBId = 0; + std::string fixtureATag; + std::string fixtureBTag; + }; + + struct ImpactEvent : ContactEvent { + float pointX = 0.f; + float pointY = 0.f; + float normalX = 0.f; + float normalY = 0.f; + float relativeNormalSpeed = 0.f; + float normalImpulse = 0.f; + float tangentImpulse = 0.f; + }; World(float gravityX, float gravityY, bool sleep, float meter); ~World(); @@ -70,6 +89,31 @@ public: int getQueryCount() const { return static_cast(queryBodyIds_.size()); } int getQueryBodyId(int index) const; + int getBeginContactCount() const { return int(beginContacts_.size()); } + int getBeginContactBodyAId(int index) const; + int getBeginContactBodyBId(int index) const; + std::string getBeginContactFixtureATag(int index) const; + std::string getBeginContactFixtureBTag(int index) const; + int getEndContactCount() const { return int(endContacts_.size()); } + int getEndContactBodyAId(int index) const; + int getEndContactBodyBId(int index) const; + std::string getEndContactFixtureATag(int index) const; + std::string getEndContactFixtureBTag(int index) const; + + int getImpactCount() const { return int(impacts_.size()); } + int getImpactBodyAId(int index) const; + int getImpactBodyBId(int index) const; + std::string getImpactFixtureATag(int index) const; + std::string getImpactFixtureBTag(int index) const; + float getImpactPointX(int index) const; + float getImpactPointY(int index) const; + float getImpactNormalX(int index) const; + float getImpactNormalY(int index) const; + float getImpactRelativeNormalSpeed(int index) const; + float getImpactNormalImpulse(int index) const; + float getImpactTangentImpulse(int index) const; + void clearContactEvents(); + float toMeters(float pixels) const; float toPixels(float meters) const; @@ -78,6 +122,8 @@ public: void onBeginContact(b2Contact *contact); void onEndContact(b2Contact *contact); + void onPreSolve(b2Contact *contact, const b2Manifold *oldManifold); + void onPostSolve(b2Contact *contact, const b2ContactImpulse *impulse); void forgetBody(Body *body); void forgetFixture(Fixture *fixture); @@ -106,6 +152,18 @@ private: float rayHitFraction_ = 0.f; std::vector queryBodyIds_; + + struct PreSolveData { + float pointX = 0.f; + float pointY = 0.f; + float normalX = 0.f; + float normalY = 0.f; + float relativeNormalSpeed = 0.f; + }; + std::unordered_map preSolve_; + std::vector beginContacts_; + std::vector endContacts_; + std::vector impacts_; }; } // namespace eve::physics diff --git a/test/animation_sprite_spine.cpp b/test/animation_sprite_spine.cpp index da9fdba8..96e0007a 100644 --- a/test/animation_sprite_spine.cpp +++ b/test/animation_sprite_spine.cpp @@ -239,6 +239,18 @@ TEST_CASE("animation.spine.skeletonAndAnim") { player->collectDrawItems(items); CHECK_EQ(static_cast(items.size()), 2); CHECK_EQ(items[0].layer, 0); + + std::vector mirrored; + player->setScale(-1.f, 1.f); + player->collectDrawItems(mirrored); + REQUIRE_EQ(static_cast(mirrored.size()), 2); + // Negative X scale reconstructs the reflected quad with a 180-degree + // rotation. A V-axis UV compensation keeps attachments upright while + // preserving the requested left/right mirror. + CHECK(std::fabs(mirrored[0].u0 - items[0].u0) < 1e-6f); + CHECK(std::fabs(mirrored[0].u1 - items[0].u1) < 1e-6f); + CHECK(std::fabs(mirrored[0].v0 - items[0].v1) < 1e-6f); + CHECK(std::fabs(mirrored[0].v1 - items[0].v0) < 1e-6f); } TEST_CASE("animation.spine.modulePump") { diff --git a/test/box2d.cpp b/test/box2d.cpp index 3d309280..75e814e3 100644 --- a/test/box2d.cpp +++ b/test/box2d.cpp @@ -91,6 +91,105 @@ TEST_CASE("box2d.fixture.sensor") { CHECK(std::fabs(fixture->getFriction() - 0.7f) < 0.001f); } +TEST_CASE("box2d.fixture.tagsFiltersAndBodyProperties") { + auto *mod = Physics::create(); + std::unique_ptr world(mod->newWorld(0.f, 0.f, true)); + std::unique_ptr body(world->newBody("dynamic", 30.f, 60.f)); + std::unique_ptr fixture(body->newRectangleFixture(30.f, 60.f, 2.f)); + + fixture->setTag("prop.crate"); + fixture->setCategoryBits(0x0020); + fixture->setMaskBits(0x0006); + fixture->setGroupIndex(-3); + CHECK_EQ(fixture->getTag(), std::string("prop.crate")); + CHECK_EQ(fixture->getCategoryBits(), 0x0020); + CHECK_EQ(fixture->getMaskBits(), 0x0006); + CHECK_EQ(fixture->getGroupIndex(), -3); + CHECK_EQ(fixture->getBodyId(), body->getId()); + CHECK_GT(body->getMass(), 0.f); + CHECK(std::fabs(body->getWorldCenterX() - 30.f) < 0.01f); + CHECK(std::fabs(body->getWorldCenterY() - 60.f) < 0.01f); + body->setLinearVelocity(300.f, 400.f); + CHECK(std::fabs(body->getLinearSpeed() - 500.f) < 0.1f); +} + +TEST_CASE("box2d.contacts.exposeTagsAndImpactData") { + auto *mod = Physics::create(); + std::unique_ptr world(mod->newWorld(0.f, 0.f, true)); + Body *wall = world->newBody("static", 300.f, 100.f); + Fixture *wallFixture = wall->newRectangleFixture(20.f, 160.f, 0.f, 0.2f, 0.f); + wallFixture->setTag("terrain"); + Body *crate = world->newBody("dynamic", 80.f, 100.f); + Fixture *crateFixture = crate->newRectangleFixture(40.f, 40.f, 1.f, 0.2f, 0.f); + crateFixture->setTag("prop.crate"); + crate->setBullet(true); + crate->setLinearVelocity(900.f, 0.f); + + for (int i = 0; i < 30 && world->getImpactCount() == 0; ++i) + world->update(1.f / 60.f); + + REQUIRE_GT(world->getBeginContactCount(), 0); + CHECK_EQ(world->getBeginContactFixtureATag(0), std::string("terrain")); + CHECK_EQ(world->getBeginContactFixtureBTag(0), std::string("prop.crate")); + REQUIRE_GT(world->getImpactCount(), 0); + CHECK_GT(world->getImpactRelativeNormalSpeed(0), 100.f); + CHECK_GT(world->getImpactNormalImpulse(0), 0.f); + CHECK(std::fabs(world->getImpactNormalX(0)) > 0.9f); + CHECK(std::fabs(world->getImpactPointX(0) - 290.f) < 5.f); + + world->clearContactEvents(); + CHECK_EQ(world->getBeginContactCount(), 0); + CHECK_EQ(world->getEndContactCount(), 0); + CHECK_EQ(world->getImpactCount(), 0); + crate->setPosition(80.f, 100.f); + crate->setLinearVelocity(0.f, 0.f); + for (int i = 0; i < 5; ++i) world->update(1.f / 60.f); + CHECK_EQ(world->getImpactCount(), 0); + + delete crateFixture; + delete crate; + delete wallFixture; + delete wall; +} + +TEST_CASE("box2d.gameplay.sameKickMovesLightPropFaster") { + auto *mod = Physics::create(); + std::unique_ptr world(mod->newWorld(0.f, 0.f, true)); + Body *crate = world->newBody("dynamic", 0.f, 0.f); + crate->newRectangleFixture(40.f, 40.f, 1.f, 0.45f, 0.05f); + Body *rock = world->newBody("dynamic", 100.f, 0.f); + rock->newRectangleFixture(40.f, 40.f, 8.f, 0.85f, 0.05f); + + crate->applyLinearImpulse(720.f, -105.f); + rock->applyLinearImpulse(720.f, -105.f); + + CHECK_GT(rock->getMass(), crate->getMass() * 7.f); + CHECK_GT(crate->getLinearSpeed(), rock->getLinearSpeed() * 7.f); +} + +TEST_CASE("box2d.contacts.destroyFixturePurgesQueuedEvents") { + auto *mod = Physics::create(); + std::unique_ptr world(mod->newWorld(0.f, 0.f, true)); + Body *wall = world->newBody("static", 100.f, 100.f); + Fixture *wallFixture = wall->newRectangleFixture(40.f, 40.f); + wallFixture->setTag("terrain"); + Body *box = world->newBody("dynamic", 100.f, 100.f); + Fixture *boxFixture = box->newRectangleFixture(30.f, 30.f, 1.f); + boxFixture->setTag("prop.crate"); + world->update(1.f / 60.f); + REQUIRE_GT(world->getBeginContactCount(), 0); + + boxFixture->destroy(); + CHECK_EQ(world->getBeginContactCount(), 0); + CHECK_EQ(world->getEndContactCount(), 0); + CHECK_EQ(world->getImpactCount(), 0); + + delete boxFixture; + delete box; + delete wallFixture; + delete wall; +} + TEST_CASE("box2d.body.destroy") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld(0.f, 0.f, true)); From b29b9b5f3dcd925b4d80e03834c1e78d25b7f9a9 Mon Sep 17 00:00:00 2001 From: sunxfancy Date: Sat, 15 Aug 2026 00:39:34 +0800 Subject: [PATCH 08/21] fix(graphics): stop MSAA and HiDPI from crashing macOS tests Use a transient MSAA color + DONT_CARE resolve so MoltenVK does not treat the multisample target as a sampled texture2d, and read back UI pixels in logical coordinates after high_dpi was enabled by default. Co-authored-by: Cursor --- src/modules/graphics/vulkan/Graphics.cpp | 21 ++++++++++++++++----- src/modules/ui/imgui/ImGuiBackend.cpp | 3 ++- test/UIOverlaySmoke.cpp | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/modules/graphics/vulkan/Graphics.cpp b/src/modules/graphics/vulkan/Graphics.cpp index 6d369af8..970a04c0 100644 --- a/src/modules/graphics/vulkan/Graphics.cpp +++ b/src/modules/graphics/vulkan/Graphics.cpp @@ -994,6 +994,7 @@ void Graphics::destroyGBufferResources() { void Graphics::destroySceneColorResources() { sceneColorPassOpen = false; + if (device.instance) device->waitIdle(); for (auto &slot : sceneColorSlots) { slot.colorTex.gpuHandle = nullptr; if (slot.framebuffer) { @@ -1032,8 +1033,9 @@ void Graphics::createUiColorResources(int width, int height) { if (samples != vk::SampleCountFlagBits::e1) { uiRenderPass = device.createRenderPass() - .addSampledColorAttachment(colorFmt, vk::AttachmentLoadOp::eClear, samples) - .addResolveColorAttachment(colorFmt) + .addColorAttachment(colorFmt, vk::AttachmentLoadOp::eClear, + vk::AttachmentStoreOp::eDontCare, samples) + .addResolveColorAttachment(colorFmt, vk::AttachmentLoadOp::eDontCare) .addSubpass(vkb::SubpassBuilder() .addAttachmentRef(0, vk::ImageLayout::eColorAttachmentOptimal) .addResolveAttachment(1, vk::ImageLayout::eColorAttachmentOptimal)) @@ -1049,6 +1051,9 @@ void Graphics::createUiColorResources(int width, int height) { .build(); } } + // Keep sample count in sync with the render pass even if target creation + // returns early (ImGui builds its pipeline from getUiMsaaSamples()). + uiColorSamples = samples; if (!texSetLayout || !descriptorPool) return; @@ -1107,6 +1112,7 @@ void Graphics::createUiColorResources(int width, int height) { } void Graphics::destroyUiColorTargets() { + if (device.instance) device->waitIdle(); for (auto &slot : uiColorSlots) { slot.colorTex.gpuHandle = nullptr; if (slot.framebuffer) { @@ -1119,7 +1125,7 @@ void Graphics::destroyUiColorTargets() { uiColorWidth = 0; uiColorHeight = 0; uiColorFormat = vk::Format::eUndefined; - uiColorSamples = vk::SampleCountFlagBits::e1; + // Keep uiColorSamples matching uiRenderPass; ImGui's pipeline is bound to it. } void Graphics::destroyUiColorResources() { @@ -1318,10 +1324,14 @@ void Graphics::createSceneColorResources(int width, int height) { } if (msaa) { + // MSAA color is transient (DONT_CARE store). Sampling the 1x resolve. + // addSampledColorAttachment on the MSAA target makes MoltenVK treat it + // as a shader-readable texture2d and segfaults on macOS. sceneColorRenderPass = device.createRenderPass() - .addSampledColorAttachment(colorFmt, vk::AttachmentLoadOp::eClear, samples) - .addResolveColorAttachment(colorFmt) + .addColorAttachment(colorFmt, vk::AttachmentLoadOp::eClear, + vk::AttachmentStoreOp::eDontCare, samples) + .addResolveColorAttachment(colorFmt, vk::AttachmentLoadOp::eDontCare) .addDepthAttachment(depthFmt, vk::AttachmentLoadOp::eClear, vk::AttachmentStoreOp::eDontCare, samples) .addSubpass(vkb::SubpassBuilder() @@ -3169,6 +3179,7 @@ vk::Pipeline Graphics::buildVoxelRectPipeline(const vkb::BuiltRenderPass &rp, vk::FrontFace::eCounterClockwise) .setMultisampler(false, samples) .setDepthStencil(true, true, vk::CompareOp::eLess) + .setColorAttachmentCount(1) .build(rp); } diff --git a/src/modules/ui/imgui/ImGuiBackend.cpp b/src/modules/ui/imgui/ImGuiBackend.cpp index 5d2f9cde..83efee43 100644 --- a/src/modules/ui/imgui/ImGuiBackend.cpp +++ b/src/modules/ui/imgui/ImGuiBackend.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -247,7 +248,7 @@ float ImGuiBackend::computeInitialScale() const { // Requires SDL_WINDOW_ALLOW_HIGHDPI to report a scale > 1 on HiDPI displays. int logicalW = 0, logicalH = 0, pixelW = 0, pixelH = 0; SDL_GetWindowSize(window_, &logicalW, &logicalH); - SDL_GL_GetDrawableSize(window_, &pixelW, &pixelH); + SDL_Vulkan_GetDrawableSize(window_, &pixelW, &pixelH); if (logicalW > 0 && pixelW > 0) { float s = float(pixelW) / float(logicalW); if (s > 0.f) return std::clamp(s, 1.f, 4.f); diff --git a/test/UIOverlaySmoke.cpp b/test/UIOverlaySmoke.cpp index 4ef76af9..44cd3a23 100644 --- a/test/UIOverlaySmoke.cpp +++ b/test/UIOverlaySmoke.cpp @@ -84,7 +84,7 @@ TEST_CASE("UI.smoke.RenderSystemSameFrameVulkanOverlay") { } // After present+readback, swapchain content should be readable. - Color p = gfx->getPixel(gfx->getPixelWidth() / 2, gfx->getPixelHeight() / 2); + Color p = gfx->getPixel(gfx->getWidth() / 2, gfx->getHeight() / 2); CHECK(std::isfinite(p.r)); CHECK(std::isfinite(p.g)); CHECK(std::isfinite(p.b)); From 2b8fd535020b8a7d14f817704d7c70f323ef5994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Sat, 15 Aug 2026 01:49:17 +0800 Subject: [PATCH 09/21] fix: tighten metroidvania controls and combat --- examples/metroidvania/README.md | 3 + examples/metroidvania/main.nut | 298 +++++++++++++++++++++------- examples/metroidvania/smoke_test.py | 65 +++++- examples/metroidvania/tuning.nut | 16 ++ 4 files changed, 310 insertions(+), 72 deletions(-) diff --git a/examples/metroidvania/README.md b/examples/metroidvania/README.md index 8057429a..98f45e17 100644 --- a/examples/metroidvania/README.md +++ b/examples/metroidvania/README.md @@ -20,6 +20,9 @@ Run the startup/frame-error smoke test on Windows with: python examples/metroidvania/smoke_test.py build/src/engine/Debug/eve.exe ``` +Add `--input-replay` to exercise movement, both jump directions, the three-hit combo, and +the kick action while checking the engine log for frame/runtime errors. + ## Asset licenses - Tile, prop, and background art is selected from Kenney's **Platformer Pack Redux** and is diff --git a/examples/metroidvania/main.nut b/examples/metroidvania/main.nut index 2a8458b6..f747b9ea 100644 --- a/examples/metroidvania/main.nut +++ b/examples/metroidvania/main.nut @@ -67,9 +67,10 @@ function makeSpine(model, animationName, scale) { textures = textures, current = animationName }; } -function playSpine(holder, name, loop = true) { - if (holder == null || holder.current == name) return; +function playSpine(holder, name, loop = true, restart = false, speed = 1.0) { + if (holder == null || (holder.current == name && !restart)) return; holder.current = name; + holder.player.setSpeed(speed); holder.player.setLoop(loop); holder.player.play(name); } @@ -84,7 +85,7 @@ function spawnPlayer(x, y) { local body = game.world.newBody("dynamic", x, y - 28.0); body.setFixedRotation(true); body.setBullet(true); - local main = body.newRectangleFixture(30.0, 54.0, 1.0, 0.25, 0.0); + local main = body.newRectangleFixture(30.0, 54.0, 1.0, 0.0, 0.0); setFixtureFilter(main, CAT_PLAYER, CAT_TERRAIN | CAT_ENEMY | CAT_PROP | CAT_HAZARD, "player"); local foot = body.newRectangleFixtureAt(22.0, 7.0, 0.0, 29.0, 0.0, 0.0, 0.0); @@ -100,10 +101,13 @@ function spawnPlayer(x, y) { local e = { kind = "player", body = body, fixtures = [main, foot, wallL, wallR, attackL, attackR], attackL = attackL, attackR = attackR, hp = TUNE.playerHp, maxHp = TUNE.playerHp, - facing = 1, grounded = 0, wallLeft = 0, wallRight = 0, coyote = 0.0, + facing = 1, grounded = 0, wallLeft = 0, wallRight = 0, + groundContacts = {}, wallLeftContacts = {}, wallRightContacts = {}, coyote = 0.0, jumpBuffer = 0.0, dashTimer = 0.0, dashReady = true, attackTimer = 0.0, - attackActive = 0.0, combo = 0, comboGrace = 0.0, attackHits = {}, - invulnerable = 0.0, spine = makeSpine("hero", "Idle", 0.58) + wallJumpLock = 0.0, attackStartup = 0.0, attackActive = 0.0, + attackEnabled = false, attackWindowDone = false, attackKind = "none", attackFixture = null, + queuedAttack = false, queuedKick = false, combo = 0, comboGrace = 0.0, attackHits = {}, + invulnerable = 0.0, spine = makeSpine("hero", "Idle", TUNE.heroVisualScale) }; return addBodyEntity(e); } @@ -112,14 +116,19 @@ function spawnEnemy(kind, x, y) { local body = game.world.newBody("dynamic", x + 16.0, y - 27.0); body.setFixedRotation(true); body.setBullet(true); - local main = body.newRectangleFixture(31.0, 52.0, TUNE.enemyDensity, 0.35, 0.02); + local main = body.newRectangleFixture(31.0, 52.0, TUNE.enemyDensity, 0.0, 0.02); setFixtureFilter(main, CAT_ENEMY, CAT_TERRAIN | CAT_PLAYER | CAT_PROP | CAT_ENEMY | CAT_PLAYER_ATTACK, "enemy"); + local foot = body.newRectangleFixtureAt(21.0, 7.0, 0.0, 28.0, 0.0, 0.0, 0.0); + setFixtureFilter(foot, CAT_ENEMY, CAT_TERRAIN | CAT_PROP, "enemy_foot", true); + local wall = body.newRectangleFixtureAt(37.0, 30.0, 0.0, 0.0, 0.0, 0.0, 0.0); + setFixtureFilter(wall, CAT_ENEMY, CAT_TERRAIN, "enemy_wall", true); local e = { - kind = kind, body = body, fixtures = [main], hp = TUNE.enemyHp, maxHp = TUNE.enemyHp, + kind = kind, body = body, fixtures = [main, foot, wall], hp = TUNE.enemyHp, maxHp = TUNE.enemyHp, stagger = 0.0, staggerDelay = 0.0, state = "normal", stateTimer = 0.0, - attackCooldown = 0.3, impactCooldown = 0.0, facing = -1, - spine = makeSpine("spineboy", "idle", 0.34) + attackCooldown = 0.3, attackDealt = false, impactCooldown = 0.0, facing = -1, + grounded = 0, groundContacts = {}, wallContacts = {}, + spine = makeSpine("spineboy", "idle", TUNE.enemyVisualScale) }; return addBodyEntity(e); } @@ -165,7 +174,8 @@ function spawnBoss(x, y) { kind = "boss", body = body, fixtures = [fx], hp = TUNE.bossHp, maxHp = TUNE.bossHp, stagger = 0.0, staggerDelay = 0.0, state = "normal", stateTimer = 0.0, impactCooldown = 0.0, phase = 1, time = 0.0, - homeX = x + 48.0, homeY = y - 48.0, spine = makeSpine("dragon", "flying", 0.30) + homeX = x + 48.0, homeY = y - 48.0, + spine = makeSpine("dragon", "flying", TUNE.bossVisualScale) }; game.boss = e; return addBodyEntity(e); @@ -235,6 +245,7 @@ function resetRun(preserveAbilities) { game.byBody = {}; game.staticBodies = []; game.activeImpacts = {}; + game.contactCounts = {}; game.effects = []; game.boss = null; game.hasWallJump = wall; @@ -246,35 +257,47 @@ function resetRun(preserveAbilities) { function beginAttack(stage, kick) { local p = game.player; + p.attackL.setMaskBits(0); + p.attackR.setMaskBits(0); p.combo = stage; - p.attackTimer = kick ? 0.32 : TUNE.attackDuration[stage - 1]; - p.attackActive = kick ? 0.13 : TUNE.attackActive[stage - 1]; - p.comboGrace = TUNE.comboGrace; + p.attackKind = kick ? "kick" : "attack"; + p.attackTimer = kick ? TUNE.kickDuration : TUNE.attackDuration[stage - 1]; + p.attackStartup = kick ? TUNE.kickStartup : TUNE.attackStartup[stage - 1]; + p.attackActive = kick ? TUNE.kickActive : TUNE.attackActive[stage - 1]; + p.attackEnabled = false; + p.attackWindowDone = false; + p.attackFixture = p.facing > 0 ? p.attackR : p.attackL; + p.comboGrace = 0.0; p.attackHits = {}; - local fx = p.facing > 0 ? p.attackR : p.attackL; - fx.setTag(kick ? "player_kick" : ("player_attack_" + stage)); - fx.setMaskBits(CAT_ENEMY | CAT_PROP); - playSpine(p.spine, "Attack", false); + p.attackFixture.setTag(kick ? "player_kick" : ("player_attack_" + stage)); + local animSpeed = kick ? 0.88 : (stage == 1 ? 1.15 : (stage == 2 ? 1.0 : 0.82)); + playSpine(p.spine, "Attack", false, true, animSpeed); } function damageActor(e, damage, stagger, impulseX, impulseY) { - if (e == null || e.hp <= 0.0 || e.impactCooldown > 0.0) return; + if (e == null || e.hp <= 0.0 || e.state == "dying" || e.state == "dead" || + e.impactCooldown > 0.0) return; e.hp -= damage; e.stagger += stagger; e.staggerDelay = TUNE.staggerDecayDelay; - e.body.applyLinearImpulse(impulseX, impulseY); e.impactCooldown = TUNE.impactInvulnerability; game.hitStop = damage >= 12.0 ? 0.04 : 0.018; if (e.hp <= 0.0) { e.hp = 0.0; - e.state = "dead"; if (e.kind == "boss") { + e.state = "dead"; game.message = "Dragon defeated - reach the opened gate"; game.messageTimer = 6.0; - } else playSpine(e.spine, "death", false); + } else { + e.state = "dying"; + e.stateTimer = TUNE.enemyDeathTime; + e.body.setLinearVelocity(0.0, 0.0); + playSpine(e.spine, "death", false, true); + } return; } if (e.kind == "boss") { + e.body.applyLinearImpulse(impulseX, impulseY); if (e.stagger >= TUNE.staggerThreshold * 2.0) { e.state = "knocked_down"; e.stateTimer = 2.4; @@ -283,14 +306,16 @@ function damageActor(e, damage, stagger, impulseX, impulseY) { return; } if (e.stagger >= TUNE.staggerThreshold) { + e.body.applyLinearImpulse(impulseX, impulseY); e.state = "launched"; e.stateTimer = 0.0; e.stagger = 0.0; - playSpine(e.spine, "hit", false); + playSpine(e.spine, "jump", true, true); } else { + e.body.applyLinearImpulse(impulseX * 0.55, impulseY * 0.22); e.state = "hit_stun"; - e.stateTimer = 0.16; - playSpine(e.spine, "hit", false); + e.stateTimer = 0.18; + playSpine(e.spine, "hit", false, true); } } @@ -328,6 +353,11 @@ function tagsAre(a, b, ta, tb) { return (a == ta && b == tb) || (a == tb && b == ta); } +function contactOtherId(aId, aTag, bId, tag) { return aTag == tag ? bId : aId; } + +function addContact(table, id) { if (!(id in table)) table[id] <- true; } +function removeContact(table, id) { if (id in table) delete table[id]; } + function processContactEvents() { local p = game.player; for (local i = 0; i < game.world.getBeginContactCount(); i += 1) { @@ -335,11 +365,28 @@ function processContactEvents() { local bId = game.world.getBeginContactBodyBId(i); local a = game.world.getBeginContactFixtureATag(i); local b = game.world.getBeginContactFixtureBTag(i); + local pairKey = bodyPair(aId, bId); + game.contactCounts[pairKey] <- (pairKey in game.contactCounts) ? + game.contactCounts[pairKey] + 1 : 1; if (tagsAre(a, b, "player_foot", "terrain") || tagsAre(a, b, "player_foot", "prop_crate") || tagsAre(a, b, "player_foot", "prop_rock")) - p.grounded += 1; - if (tagsAre(a, b, "player_wall_l", "terrain")) p.wallLeft += 1; - if (tagsAre(a, b, "player_wall_r", "terrain")) p.wallRight += 1; + addContact(p.groundContacts, contactOtherId(aId, a, bId, "player_foot")); + if (tagsAre(a, b, "player_wall_l", "terrain")) + addContact(p.wallLeftContacts, contactOtherId(aId, a, bId, "player_wall_l")); + if (tagsAre(a, b, "player_wall_r", "terrain")) + addContact(p.wallRightContacts, contactOtherId(aId, a, bId, "player_wall_r")); + if (a == "enemy_foot" || b == "enemy_foot") { + local enemyId = a == "enemy_foot" ? aId : bId; + if (enemyId in game.byBody) + addContact(game.byBody[enemyId].groundContacts, + contactOtherId(aId, a, bId, "enemy_foot")); + } + if (a == "enemy_wall" || b == "enemy_wall") { + local enemyId = a == "enemy_wall" ? aId : bId; + if (enemyId in game.byBody) + addContact(game.byBody[enemyId].wallContacts, + contactOtherId(aId, a, bId, "enemy_wall")); + } handleAttackContact(aId, a, bId, b); local other = aId == p.body.getId() ? bId : (bId == p.body.getId() ? aId : 0); if (other != 0 && other in game.byBody) { @@ -359,6 +406,8 @@ function processContactEvents() { } else if (e.kind == "hazard") { p.hp -= 20.0; p.body.setPosition(game.checkpointX, game.checkpointY); p.body.setLinearVelocity(0.0, 0.0); + p.groundContacts.clear(); p.wallLeftContacts.clear(); p.wallRightContacts.clear(); + p.grounded = 0; p.wallLeft = 0; p.wallRight = 0; p.coyote = 0.0; } } } @@ -369,12 +418,37 @@ function processContactEvents() { local b = game.world.getEndContactFixtureBTag(i); if (tagsAre(a, b, "player_foot", "terrain") || tagsAre(a, b, "player_foot", "prop_crate") || tagsAre(a, b, "player_foot", "prop_rock")) - p.grounded = p.grounded > 0 ? p.grounded - 1 : 0; - if (tagsAre(a, b, "player_wall_l", "terrain")) p.wallLeft = p.wallLeft > 0 ? p.wallLeft - 1 : 0; - if (tagsAre(a, b, "player_wall_r", "terrain")) p.wallRight = p.wallRight > 0 ? p.wallRight - 1 : 0; + removeContact(p.groundContacts, contactOtherId(aId, a, bId, "player_foot")); + if (tagsAre(a, b, "player_wall_l", "terrain")) + removeContact(p.wallLeftContacts, contactOtherId(aId, a, bId, "player_wall_l")); + if (tagsAre(a, b, "player_wall_r", "terrain")) + removeContact(p.wallRightContacts, contactOtherId(aId, a, bId, "player_wall_r")); + if (a == "enemy_foot" || b == "enemy_foot") { + local enemyId = a == "enemy_foot" ? aId : bId; + if (enemyId in game.byBody) + removeContact(game.byBody[enemyId].groundContacts, + contactOtherId(aId, a, bId, "enemy_foot")); + } + if (a == "enemy_wall" || b == "enemy_wall") { + local enemyId = a == "enemy_wall" ? aId : bId; + if (enemyId in game.byBody) + removeContact(game.byBody[enemyId].wallContacts, + contactOtherId(aId, a, bId, "enemy_wall")); + } local key = bodyPair(aId, bId); - if (key in game.activeImpacts) delete game.activeImpacts[key]; + if (key in game.contactCounts) { + game.contactCounts[key] -= 1; + if (game.contactCounts[key] <= 0) { + delete game.contactCounts[key]; + if (key in game.activeImpacts) delete game.activeImpacts[key]; + } + } } + p.grounded = p.groundContacts.len(); + p.wallLeft = p.wallLeftContacts.len(); + p.wallRight = p.wallRightContacts.len(); + foreach (e in game.entities) + if ("groundContacts" in e && e.kind != "player") e.grounded = e.groundContacts.len(); } function impactCarrier(e, preSolveSpeed) { @@ -404,6 +478,8 @@ function processImpacts() { local a = (aId in game.byBody) ? game.byBody[aId] : null; local b = (bId in game.byBody) ? game.byBody[bId] : null; if (!impactCarrier(a, speed) && !impactCarrier(b, speed)) continue; + local aWasLaunched = a != null && "state" in a && a.state == "launched"; + local bWasLaunched = b != null && "state" in b && b.state == "launched"; game.activeImpacts[key] <- true; local damage = clampf(impulse * (speed - TUNE.impactSpeedThreshold) * TUNE.impactScale, 1.0, TUNE.impactDamageCap); @@ -423,11 +499,13 @@ function processImpacts() { b.hp -= damage; if (impulse >= TUNE.crateBreakImpulse) b.hp = 0.0; } - if (a != null && "state" in a && a.state == "launched") { + if (aWasLaunched && a.state != "dying" && a.state != "dead") { a.state = "knocked_down"; a.stateTimer = TUNE.knockdownTime; + playSpine(a.spine, "hit", false, true); } - if (b != null && "state" in b && b.state == "launched") { + if (bWasLaunched && b.state != "dying" && b.state != "dead") { b.state = "knocked_down"; b.stateTimer = TUNE.knockdownTime; + playSpine(b.spine, "hit", false, true); } game.shake = clampf(damage * 0.35, 2.0, 10.0); game.hitStop = damage > 18.0 ? 0.05 : (damage > 7.0 ? 0.022 : game.hitStop); @@ -451,21 +529,30 @@ function updatePlayer(dt) { } if (p.grounded > 0) { p.coyote = TUNE.coyoteTime; p.dashReady = true; } else p.coyote -= dt; + if (p.wallJumpLock > 0.0) p.wallJumpLock -= dt; if (keyPressed("Space")) p.jumpBuffer = TUNE.jumpBuffer; else p.jumpBuffer -= dt; local move = 0.0; if (keyDown("A", "Left")) move -= 1.0; if (keyDown("D", "Right")) move += 1.0; - if (move != 0.0) p.facing = move > 0.0 ? 1 : -1; + // Keep the visual and the already-selected attack/dash fixture pointing in + // the same direction until that action completes. + if (move != 0.0 && p.attackTimer <= 0.0 && p.dashTimer <= 0.0) + p.facing = move > 0.0 ? 1 : -1; if (p.jumpBuffer > 0.0) { if (p.coyote > 0.0) { p.body.setLinearVelocity(p.body.getLinearVelocityX(), -TUNE.jumpSpeed); + p.groundContacts.clear(); p.grounded = 0; p.jumpBuffer = 0.0; p.coyote = 0.0; } else if (game.hasWallJump && (p.wallLeft > 0 || p.wallRight > 0)) { local dir = p.wallLeft > 0 ? 1.0 : -1.0; p.body.setLinearVelocity(TUNE.wallJumpX * dir, -TUNE.wallJumpY); - p.jumpBuffer = 0.0; + p.facing = dir > 0.0 ? 1 : -1; + p.wallJumpLock = TUNE.wallJumpLockTime; + p.wallLeftContacts.clear(); p.wallRightContacts.clear(); + p.wallLeft = 0; p.wallRight = 0; + p.jumpBuffer = 0.0; p.coyote = 0.0; } } if (game.hasDash && p.dashReady && keyPressed("Left Shift", "Right Shift")) { @@ -476,29 +563,63 @@ function updatePlayer(dt) { p.dashTimer -= dt; p.body.setLinearVelocity(TUNE.dashSpeed * p.facing, 0.0); } else { - local control = p.grounded > 0 ? 1.0 : TUNE.airControl; local target = move * TUNE.moveSpeed; local vx = p.body.getLinearVelocityX(); - p.body.setLinearVelocity(vx + (target - vx) * clampf(dt * 14.0 * control, 0.0, 1.0), - p.body.getLinearVelocityY()); - if (game.hasWallJump && p.grounded == 0 && (p.wallLeft > 0 || p.wallRight > 0) && + local acceleration = p.grounded > 0 ? TUNE.groundAcceleration : TUNE.airAcceleration; + local nextVx = p.wallJumpLock > 0.0 ? vx : + vx + (target - vx) * clampf(dt * acceleration, 0.0, 1.0); + local nextVy = p.body.getLinearVelocityY(); + if (!keyDown("Space") && nextVy < -80.0) + nextVy += TUNE.jumpCutAcceleration * dt; + p.body.setLinearVelocity(nextVx, nextVy); + local pressingIntoWall = (p.wallLeft > 0 && move < 0.0) || (p.wallRight > 0 && move > 0.0); + if (game.hasWallJump && p.grounded == 0 && pressingIntoWall && p.body.getLinearVelocityY() > TUNE.wallSlideSpeed) p.body.setLinearVelocity(p.body.getLinearVelocityX(), TUNE.wallSlideSpeed); } - p.comboGrace -= dt; + if (p.attackTimer <= 0.0) p.comboGrace -= dt; if (keyPressed("J")) { - local next = p.comboGrace > 0.0 ? (p.combo % 3) + 1 : 1; - beginAttack(next, false); + if (p.attackTimer > 0.0) p.queuedAttack = true; + else beginAttack(p.comboGrace > 0.0 ? (p.combo % 3) + 1 : 1, false); + } + if (keyPressed("K")) { + if (p.attackTimer > 0.0) p.queuedKick = true; + else beginAttack(1, true); } - if (keyPressed("K")) beginAttack(1, true); if (p.attackTimer > 0.0) { - p.attackTimer -= dt; p.attackActive -= dt; - if (p.attackActive <= 0.0) { p.attackL.setMaskBits(0); p.attackR.setMaskBits(0); } + p.attackTimer -= dt; + if (!p.attackEnabled && !p.attackWindowDone) { + p.attackStartup -= dt; + if (p.attackStartup <= 0.0) { + p.attackEnabled = true; + p.attackFixture.setMaskBits(CAT_ENEMY | CAT_PROP); + } + } else { + p.attackActive -= dt; + if (p.attackActive <= 0.0) { + p.attackEnabled = false; + p.attackWindowDone = true; + p.attackL.setMaskBits(0); p.attackR.setMaskBits(0); + } + } + if (p.attackTimer <= 0.0) { + p.attackL.setMaskBits(0); p.attackR.setMaskBits(0); + p.attackEnabled = false; + p.comboGrace = TUNE.comboGrace; + if (p.queuedKick) { + p.queuedKick = false; p.queuedAttack = false; + beginAttack(1, true); + } else if (p.queuedAttack) { + p.queuedAttack = false; + beginAttack((p.combo % 3) + 1, false); + } + } } if (p.invulnerable > 0.0) p.invulnerable -= dt; if (p.attackTimer <= 0.0) { + p.spine.player.setSpeed(1.0); local vy = p.body.getLinearVelocityY(); if (p.grounded > 0) playSpine(p.spine, absf(p.body.getLinearVelocityX()) > 25.0 ? "Run" : "Idle"); else playSpine(p.spine, vy < 0.0 ? "Jump" : "Fall"); @@ -511,38 +632,68 @@ function updateEnemies(dt) { foreach (e in game.entities) { if (!("state" in e) || e.kind == "player" || e.kind == "boss" || e.state == "dead") continue; if (e.impactCooldown > 0.0) e.impactCooldown -= dt; + if (e.attackCooldown > 0.0) e.attackCooldown -= dt; if (e.staggerDelay > 0.0) e.staggerDelay -= dt; else e.stagger = clampf(e.stagger - TUNE.staggerDecay * dt, 0.0, TUNE.staggerThreshold); - if (e.state == "hit_stun") { + if (e.state == "dying") { + e.stateTimer -= dt; + if (e.stateTimer <= 0.0) { e.state = "dead"; e.body.setActive(false); } + continue; + } else if (e.state == "attacking") { + e.stateTimer -= dt; + e.body.setLinearVelocity(0.0, e.body.getLinearVelocityY()); + if (!e.attackDealt && e.stateTimer <= TUNE.enemyAttackDuration - TUNE.enemyAttackHitTime) { + e.attackDealt = true; + local dxNow = game.player.body.getX() - e.body.getX(); + local dyNow = game.player.body.getY() - e.body.getY(); + if (absf(dxNow) < 54.0 && absf(dyNow) < 52.0 && game.player.invulnerable <= 0.0) { + game.player.hp -= 9.0; + game.player.invulnerable = 0.55; + game.player.body.applyLinearImpulse(e.facing * 240.0, -80.0); + } + } + if (e.stateTimer <= 0.0) { e.state = "normal"; e.attackCooldown = 0.72; } + continue; + } else if (e.state == "hit_stun") { e.stateTimer -= dt; if (e.stateTimer <= 0.0) e.state = "normal"; + } else if (e.state == "launched") { + if (e.grounded > 0 && absf(e.body.getLinearVelocityY()) < TUNE.launchedLandSpeed) { + e.state = "knocked_down"; + e.stateTimer = TUNE.knockdownTime; + e.body.setLinearVelocity(e.body.getLinearVelocityX() * 0.55, 0.0); + playSpine(e.spine, "hit", false, true); + } } else if (e.state == "knocked_down") { e.stateTimer -= dt; - if (e.stateTimer <= 0.0) { e.state = "getting_up"; e.stateTimer = TUNE.getupTime; } + if (e.stateTimer <= 0.0) { + e.state = "getting_up"; e.stateTimer = TUNE.getupTime; + playSpine(e.spine, "idle", true, true); + } } else if (e.state == "getting_up") { e.stateTimer -= dt; if (e.stateTimer <= 0.0) e.state = "normal"; } if (e.state != "normal") continue; - e.attackCooldown -= dt; local dx = px - e.body.getX(); local dy = py - e.body.getY(); e.facing = dx >= 0.0 ? 1 : -1; - if (absf(dx) < 42.0 && absf(dy) < 52.0 && e.attackCooldown <= 0.0) { - if (game.player.invulnerable <= 0.0) { - game.player.hp -= 9.0; - game.player.invulnerable = 0.55; - game.player.body.applyLinearImpulse(-e.facing * 240.0, -80.0); - } - e.attackCooldown = 1.0; - playSpine(e.spine, "shoot", false); + if (absf(dx) < 48.0 && absf(dy) < 52.0 && e.attackCooldown <= 0.0) { + e.state = "attacking"; + e.stateTimer = TUNE.enemyAttackDuration; + e.attackDealt = false; + e.body.setLinearVelocity(0.0, e.body.getLinearVelocityY()); + playSpine(e.spine, "shoot", false, true); } else if (absf(dx) < 430.0) { local speed = e.kind == "enemy_leaper" ? 105.0 : 78.0; - e.body.setLinearVelocity(e.facing * speed, e.body.getLinearVelocityY()); - if (e.kind == "enemy_leaper" && absf(dx) < 180.0 && e.attackCooldown <= 0.0) { + local blocked = e.wallContacts.len() > 0; + e.body.setLinearVelocity(blocked ? 0.0 : e.facing * speed, + e.body.getLinearVelocityY()); + if (e.kind == "enemy_leaper" && e.grounded > 0 && absf(dx) < 180.0 && + e.attackCooldown <= 0.0 && (!blocked || absf(dx) < 90.0)) { e.body.applyLinearImpulse(e.facing * 190.0, -420.0); e.attackCooldown = 1.8; - playSpine(e.spine, "jump"); + playSpine(e.spine, "jump", true); } else playSpine(e.spine, "run"); } else playSpine(e.spine, "idle"); } @@ -600,12 +751,18 @@ function removeBrokenProps() { } function updateCamera(dt) { - local targetX = clampf(game.player.body.getX(), config.width * 0.5, 120.0 * 32.0 - config.width * 0.5); - local targetY = clampf(game.player.body.getY(), config.height * 0.5, 22.0 * 32.0 - config.height * 0.5); + local worldWidth = 120.0 * 32.0; + local worldHeight = 22.0 * 32.0; + local targetX = worldWidth <= config.width ? worldWidth * 0.5 : + clampf(game.player.body.getX(), config.width * 0.5, worldWidth - config.width * 0.5); + local targetY = worldHeight <= config.height ? worldHeight * 0.5 : + clampf(game.player.body.getY(), config.height * 0.5, worldHeight - config.height * 0.5); game.cameraX += (targetX - game.cameraX) * clampf(dt * 5.5, 0.0, 1.0); game.cameraY += (targetY - game.cameraY) * clampf(dt * 5.5, 0.0, 1.0); - game.camera.setPosition(game.cameraX, game.cameraY); - if (game.shake > 0.0) game.shake -= dt * 24.0; + local shakeX = game.shake > 0.0 ? sin(game.time * 97.0) * game.shake : 0.0; + local shakeY = game.shake > 0.0 ? cos(game.time * 83.0) * game.shake * 0.55 : 0.0; + game.camera.setPosition(game.cameraX + shakeX, game.cameraY + shakeY); + if (game.shake > 0.0) game.shake = clampf(game.shake - dt * 24.0, 0.0, 10.0); } function buildHud() { @@ -657,13 +814,13 @@ eve_init = function() { world = null, entities = [], byBody = {}, staticBodies = [], player = null, boss = null, collisionLayer = map.getLayer(1), camera = camera, cameraX = 640.0, cameraY = 360.0, checkpointX = 112.0, checkpointY = 548.0, hasWallJump = false, hasDash = false, - activeImpacts = {}, crateTexture = gfx.newTextureFromFile("assets/crate.png"), + activeImpacts = {}, contactCounts = {}, crateTexture = gfx.newTextureFromFile("assets/crate.png"), rockTexture = gfx.newTextureFromFile("assets/rock.png"), backgrounds = [gfx.newTextureFromFile("assets/background_forest.png"), gfx.newTextureFromFile("assets/background_ruins.png"), gfx.newTextureFromFile("assets/background_mine.png")], message = "Reach the ruins", - messageTimer = 3.0, shake = 0.0, hitStop = 0.0, accumulator = 0.0, + messageTimer = 3.0, shake = 0.0, time = 0.0, hitStop = 0.0, accumulator = 0.0, effects = [], won = false }; buildHud(); @@ -671,6 +828,7 @@ eve_init = function() { }; function simulateStep(dt) { + game.time += dt; if (game.won && keyPressed("R")) { game.won = false; resetRun(false); } game.world.clearContactEvents(); updatePlayer(dt); @@ -740,10 +898,12 @@ eve_render = function() { local sx = screenX(e.body.getX()); local sy = screenY(e.body.getY()); local facing = ("facing" in e) ? e.facing : 1; - local baseScale = e.kind == "player" ? 0.58 : (e.kind == "boss" ? 0.30 : 0.34); + local baseScale = e.kind == "player" ? TUNE.heroVisualScale : + (e.kind == "boss" ? TUNE.bossVisualScale : TUNE.enemyVisualScale); e.spine.player.setScale(baseScale * facing, baseScale); - e.spine.player.setPosition(sx, sy + (e.kind == "boss" ? 20.0 : 27.0)); - if ("impactCooldown" in e && e.impactCooldown > 0.0) + e.spine.player.setPosition(sx, sy + (e.kind == "boss" ? 20.0 : TUNE.actorVisualYOffset)); + if ((e.kind == "player" && e.invulnerable > 0.0) || + ("impactCooldown" in e && e.impactCooldown > 0.0)) e.spine.player.setColor(1.0, 0.55, 0.48, 1.0); else e.spine.player.setColor(1.0, 1.0, 1.0, 1.0); e.spine.player.draw(gfx); diff --git a/examples/metroidvania/smoke_test.py b/examples/metroidvania/smoke_test.py index 0b3c1dc0..9fd3562e 100644 --- a/examples/metroidvania/smoke_test.py +++ b/examples/metroidvania/smoke_test.py @@ -3,16 +3,65 @@ from __future__ import annotations import argparse +import ctypes import subprocess import sys import time from pathlib import Path +def replay_input(process_id: int) -> None: + """Exercise movement, jumping, facing changes, combo attacks, and kicking.""" + if sys.platform != "win32": + raise RuntimeError("--input-replay currently requires Windows") + + user32 = ctypes.windll.user32 + target = ctypes.c_void_p() + enum_proc_type = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p) + + @enum_proc_type + def find_window(hwnd: int, _param: int) -> bool: + owner_pid = ctypes.c_ulong() + user32.GetWindowThreadProcessId(hwnd, ctypes.byref(owner_pid)) + if owner_pid.value == process_id and user32.IsWindowVisible(hwnd): + target.value = hwnd + return False + return True + + deadline = time.monotonic() + 3.0 + while not target.value and time.monotonic() < deadline: + user32.EnumWindows(find_window, 0) + time.sleep(0.05) + if not target.value: + raise RuntimeError("game window did not appear") + + user32.SetForegroundWindow(target.value) + + def key(vk: int, held: float = 0.08) -> None: + user32.keybd_event(vk, 0, 0, 0) + time.sleep(held) + user32.keybd_event(vk, 0, 2, 0) + + user32.keybd_event(ord("D"), 0, 0, 0) + time.sleep(0.8) + key(0x20, 0.12) # jump while moving right + time.sleep(0.65) + user32.keybd_event(ord("D"), 0, 2, 0) + for _ in range(3): + key(ord("J")) + time.sleep(0.2) + key(ord("K")) + user32.keybd_event(ord("A"), 0, 0, 0) + time.sleep(0.35) + key(0x20, 0.09) # reverse-facing jump + user32.keybd_event(ord("A"), 0, 2, 0) + + def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("engine", type=Path, help="Path to the eve executable") parser.add_argument("--seconds", type=float, default=6.0) + parser.add_argument("--input-replay", action="store_true") args = parser.parse_args() game_dir = Path(__file__).resolve().parent @@ -24,7 +73,14 @@ def main() -> int: encoding="utf-8", errors="replace", ) - time.sleep(args.seconds) + started = time.monotonic() + replay_error = None + if args.input_replay: + try: + replay_input(process.pid) + except RuntimeError as exc: + replay_error = str(exc) + time.sleep(max(0.0, args.seconds - (time.monotonic() - started))) exited_early = process.poll() is not None if not exited_early: process.terminate() @@ -37,11 +93,14 @@ def main() -> int: bad_markers = ("frame error", "Run failed:", "Runtime error") failures = [marker for marker in bad_markers if marker.lower() in combined.lower()] - if failures or (exited_early and process.returncode != 0): + if failures or replay_error or (exited_early and process.returncode != 0): print(combined, file=sys.stderr) + if replay_error: + print("Input replay failed: " + replay_error, file=sys.stderr) print("Smoke test failed: " + ", ".join(failures), file=sys.stderr) return 1 - print(f"Smoke test passed ({args.seconds:.1f}s, no frame/runtime errors).") + mode = " with input replay" if args.input_replay else "" + print(f"Smoke test passed ({args.seconds:.1f}s{mode}, no frame/runtime errors).") return 0 diff --git a/examples/metroidvania/tuning.nut b/examples/metroidvania/tuning.nut index 221a8a04..aa177094 100644 --- a/examples/metroidvania/tuning.nut +++ b/examples/metroidvania/tuning.nut @@ -2,6 +2,8 @@ TUNE <- { gravity = 1200.0, moveSpeed = 240.0, + groundAcceleration = 18.0, + airAcceleration = 8.5, airControl = 0.72, jumpSpeed = 470.0, wallJumpX = 330.0, @@ -9,9 +11,15 @@ TUNE <- { wallSlideSpeed = 105.0, coyoteTime = 0.10, jumpBuffer = 0.12, + jumpCutAcceleration = 1550.0, + wallJumpLockTime = 0.14, dashSpeed = 650.0, dashTime = 0.16, playerHp = 100.0, + heroVisualScale = 0.32, + enemyVisualScale = 0.15, + bossVisualScale = 0.20, + actorVisualYOffset = 27.0, enemyHp = 55.0, bossHp = 360.0, staggerThreshold = 80.0, @@ -22,8 +30,12 @@ TUNE <- { attackImpulseX = [250.0, 360.0, 780.0], attackImpulseY = [-25.0, -55.0, -330.0], attackDuration = [0.27, 0.30, 0.40], + attackStartup = [0.055, 0.070, 0.105], attackActive = [0.10, 0.11, 0.14], comboGrace = 0.32, + kickDuration = 0.34, + kickStartup = 0.09, + kickActive = 0.12, kickImpulseX = 720.0, kickImpulseY = -105.0, impactSpeedThreshold = 180.0, @@ -40,6 +52,10 @@ TUNE <- { frictionCrate = 0.45, frictionRock = 0.85, restitutionProp = 0.05, + enemyAttackDuration = 0.52, + enemyAttackHitTime = 0.28, + enemyDeathTime = 0.75, + launchedLandSpeed = 95.0, knockdownTime = 1.15, getupTime = 0.34 }; From ce0be524a85f344706012488648957a9e20fd6f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Sat, 15 Aug 2026 10:22:35 +0800 Subject: [PATCH 10/21] fix: make movement upgrades and level route playable --- examples/metroidvania/README.md | 2 +- examples/metroidvania/generate_map.py | 34 ++++++++++++-- examples/metroidvania/main.nut | 64 ++++++++++++++++++++++----- examples/metroidvania/maps/world.json | 20 ++++----- examples/metroidvania/smoke_test.py | 26 +++++++---- examples/metroidvania/tuning.nut | 6 ++- 6 files changed, 117 insertions(+), 35 deletions(-) diff --git a/examples/metroidvania/README.md b/examples/metroidvania/README.md index 98f45e17..66a3d088 100644 --- a/examples/metroidvania/README.md +++ b/examples/metroidvania/README.md @@ -10,7 +10,7 @@ Run from the repository root with the platform-specific engine target, for examp make run/win32-debug GAME=examples/metroidvania ``` -Controls: `A/D` or arrows move, `Space` jumps/wall-jumps, `J` attacks, `K` kicks props, and +Controls: `A/D` or arrows move, `Space` jumps/double-jumps/wall-jumps, `J` attacks, `K` kicks props, and either `Shift` key air-dashes after the upgrade. Enable `debug` in `config.nut` to see Box2D fixtures. diff --git a/examples/metroidvania/generate_map.py b/examples/metroidvania/generate_map.py index 2533db86..56fbf41e 100644 --- a/examples/metroidvania/generate_map.py +++ b/examples/metroidvania/generate_map.py @@ -53,9 +53,10 @@ for x in range(80, W): rect(terrain, 5, 16, 7, 1, 1) rect(terrain, 15, 13, 7, 1, 1) rect(terrain, 25, 16, 6, 1, 1) -rect(terrain, 34, 12, 1, 7, 2) +# The left shaft wall floats above a three-tile entrance. Players collect wall +# jump before it, walk underneath, then alternate between the two faces. +rect(terrain, 34, 12, 1, 4, 2) rect(terrain, 38, 10, 2, 9, 2) -rect(terrain, 35, 15, 3, 1, 1) # Ruins: vertical rooms for wall jumping, then a dash altar. rect(terrain, 42, 16, 7, 1, 3) @@ -95,7 +96,9 @@ objects = [ obj("rock_1", "prop_rock", 28, 15), obj("scout_1", "enemy_melee", 18, 18), obj("scout_2", "enemy_leaper", 29, 15), - obj("wall_jump", "ability_walljump", 36, 14), + # Full-height trigger before the shaft: running or double-jumping cannot + # accidentally skip the ability required by the following room. + obj("wall_jump", "ability_walljump", 32, 19, 2, 7), obj("cp_ruins", "checkpoint", 42, 18), obj("guard_1", "enemy_melee", 48, 18), obj("guard_2", "enemy_leaper", 68, 10), @@ -111,6 +114,31 @@ objects = [ obj("boss_exit", "exit", 117, 18, 2, 2), ] + +def validate_level(): + """Catch ability-order and jump-route regressions before writing the map.""" + wall_jump = next(item for item in objects if item["type"] == "ability_walljump") + trigger_left = wall_jump["x"] // TILE + trigger_right = (wall_jump["x"] + wall_jump["width"]) // TILE + assert trigger_left < 34 and trigger_right <= 34, "wall jump must be acquired before the shaft" + assert wall_jump["y"] - wall_jump["height"] <= 12 * TILE, "wall-jump trigger is too short" + assert all(terrain[y * W + 34] == 0 for y in range(16, 19)), "shaft entrance is blocked" + assert all(terrain[y * W + x] == 0 for y in range(10, 19) for x in range(35, 38)), ( + "wall-jump shaft contains an impassable crossbar" + ) + + gap = longest_gap = 0 + for x in range(W): + if terrain[19 * W + x] == 0: + gap += 1 + longest_gap = max(longest_gap, gap) + else: + gap = 0 + assert longest_gap <= 4, "ground gap exceeds the configured running-jump route" + + +validate_level() + tileset = { "firstgid": 1, "image": "assets/tiles.png", diff --git a/examples/metroidvania/main.nut b/examples/metroidvania/main.nut index f747b9ea..d1cdb4ed 100644 --- a/examples/metroidvania/main.nut +++ b/examples/metroidvania/main.nut @@ -86,7 +86,8 @@ function spawnPlayer(x, y) { body.setFixedRotation(true); body.setBullet(true); local main = body.newRectangleFixture(30.0, 54.0, 1.0, 0.0, 0.0); - setFixtureFilter(main, CAT_PLAYER, CAT_TERRAIN | CAT_ENEMY | CAT_PROP | CAT_HAZARD, + setFixtureFilter(main, CAT_PLAYER, + CAT_TERRAIN | CAT_ENEMY | CAT_PROP | CAT_PICKUP | CAT_HAZARD, "player"); local foot = body.newRectangleFixtureAt(22.0, 7.0, 0.0, 29.0, 0.0, 0.0, 0.0); setFixtureFilter(foot, CAT_PLAYER, CAT_TERRAIN | CAT_PROP, "player_foot", true); @@ -103,7 +104,9 @@ function spawnPlayer(x, y) { attackL = attackL, attackR = attackR, hp = TUNE.playerHp, maxHp = TUNE.playerHp, facing = 1, grounded = 0, wallLeft = 0, wallRight = 0, groundContacts = {}, wallLeftContacts = {}, wallRightContacts = {}, coyote = 0.0, - jumpBuffer = 0.0, dashTimer = 0.0, dashReady = true, attackTimer = 0.0, + jumpBuffer = 0.0, airJumpsRemaining = TUNE.airJumpCount, + wallCoyote = 0.0, wallJumpDir = 0.0, wallJumpFeedback = 0.0, + dashTimer = 0.0, dashReady = true, attackTimer = 0.0, wallJumpLock = 0.0, attackStartup = 0.0, attackActive = 0.0, attackEnabled = false, attackWindowDone = false, attackKind = "none", attackFixture = null, queuedAttack = false, queuedKick = false, combo = 0, comboGrace = 0.0, attackHits = {}, @@ -527,9 +530,23 @@ function updatePlayer(dt) { game.message = "Returned to checkpoint"; game.messageTimer = 2.0; return; } - if (p.grounded > 0) { p.coyote = TUNE.coyoteTime; p.dashReady = true; } + if (p.grounded > 0) { + p.coyote = TUNE.coyoteTime; + p.dashReady = true; + p.airJumpsRemaining = TUNE.airJumpCount; + } else p.coyote -= dt; if (p.wallJumpLock > 0.0) p.wallJumpLock -= dt; + if (p.wallJumpFeedback > 0.0) p.wallJumpFeedback -= dt; + if (p.grounded > 0) { + p.wallCoyote = 0.0; + } else if (p.wallLeft > 0) { + p.wallCoyote = TUNE.wallContactGrace; p.wallJumpDir = 1.0; + } else if (p.wallRight > 0) { + p.wallCoyote = TUNE.wallContactGrace; p.wallJumpDir = -1.0; + } else { + p.wallCoyote -= dt; + } if (keyPressed("Space")) p.jumpBuffer = TUNE.jumpBuffer; else p.jumpBuffer -= dt; local move = 0.0; @@ -541,18 +558,33 @@ function updatePlayer(dt) { p.facing = move > 0.0 ? 1 : -1; if (p.jumpBuffer > 0.0) { - if (p.coyote > 0.0) { - p.body.setLinearVelocity(p.body.getLinearVelocityX(), -TUNE.jumpSpeed); - p.groundContacts.clear(); p.grounded = 0; - p.jumpBuffer = 0.0; p.coyote = 0.0; - } else if (game.hasWallJump && (p.wallLeft > 0 || p.wallRight > 0)) { - local dir = p.wallLeft > 0 ? 1.0 : -1.0; + // Wall jump takes priority over the air jump so touching a wall always + // produces the expected horizontal kick instead of consuming double jump. + if (p.grounded == 0 && game.hasWallJump && p.wallCoyote > 0.0) { + local dir = p.wallJumpDir; p.body.setLinearVelocity(TUNE.wallJumpX * dir, -TUNE.wallJumpY); p.facing = dir > 0.0 ? 1 : -1; p.wallJumpLock = TUNE.wallJumpLockTime; + p.wallJumpFeedback = TUNE.wallJumpFeedbackTime; + p.wallCoyote = 0.0; + p.airJumpsRemaining = TUNE.airJumpCount; p.wallLeftContacts.clear(); p.wallRightContacts.clear(); p.wallLeft = 0; p.wallRight = 0; p.jumpBuffer = 0.0; p.coyote = 0.0; + playSpine(p.spine, "Jump", false, true, 1.15); + } else if (p.coyote > 0.0) { + p.body.setLinearVelocity(p.body.getLinearVelocityX(), -TUNE.jumpSpeed); + p.groundContacts.clear(); p.grounded = 0; + p.jumpBuffer = 0.0; p.coyote = 0.0; + } else if (p.airJumpsRemaining > 0) { + p.body.setLinearVelocity(p.body.getLinearVelocityX(), -TUNE.airJumpSpeed); + p.airJumpsRemaining -= 1; + p.jumpBuffer = 0.0; p.coyote = 0.0; + playSpine(p.spine, "Jump", false, true, 1.2); + for (local puff = 0; puff < 5; puff += 1) + game.effects.push({ x = p.body.getX() + (puff - 2) * 5.0, + y = p.body.getY() + 24.0, vx = (puff - 2) * 24.0, + vy = 45.0, life = 0.16, heavy = false }); } } if (game.hasDash && p.dashReady && keyPressed("Left Shift", "Right Shift")) { @@ -786,8 +818,8 @@ function refreshHud() { ui.select("hud"); ui.setValue("hp", clampf(game.player.hp / game.player.maxHp, 0.0, 1.0)); ui.setText("hptext", "HP " + game.player.hp.tointeger() + "/" + game.player.maxHp.tointeger()); - ui.setText("ability", "Abilities: Wall Jump " + (game.hasWallJump ? "ON" : "--") + - " Air Dash " + (game.hasDash ? "ON" : "--")); + ui.setText("ability", "Abilities: Double Jump ON Wall Jump " + + (game.hasWallJump ? "ON" : "--") + " Air Dash " + (game.hasDash ? "ON" : "--")); ui.setText("message", game.won ? "RUINS CLEARED - press R to restart" : game.message); if (game.boss != null && game.boss.hp > 0.0 && game.player.body.getX() > 104.0 * 32.0) { ui.setValue("boss", game.boss.hp / game.boss.maxHp); @@ -894,6 +926,12 @@ eve_render = function() { } else if (e.kind == "moving_platform") { gfx.drawSolidRect(screenX(e.body.getX()) - 48.0, screenY(e.body.getY()) - 12.0, 96.0, 24.0, 0.36, 0.42, 0.48, 1.0); + } else if (e.kind == "ability_walljump" || e.kind == "ability_dash") { + local pulse = 13.0 + sin(game.time * 5.0) * 3.0; + local r = e.kind == "ability_walljump" ? 0.25 : 0.85; + local g = e.kind == "ability_walljump" ? 0.78 : 0.45; + gfx.drawSolidRect(screenX(e.body.getX()) - pulse, screenY(e.body.getY()) - pulse, + pulse * 2.0, pulse * 2.0, r, g, 1.0, 0.85); } else if ("spine" in e && e.spine != null && (!("state" in e) || e.state != "dead")) { local sx = screenX(e.body.getX()); local sy = screenY(e.body.getY()); @@ -902,7 +940,9 @@ eve_render = function() { (e.kind == "boss" ? TUNE.bossVisualScale : TUNE.enemyVisualScale); e.spine.player.setScale(baseScale * facing, baseScale); e.spine.player.setPosition(sx, sy + (e.kind == "boss" ? 20.0 : TUNE.actorVisualYOffset)); - if ((e.kind == "player" && e.invulnerable > 0.0) || + if (e.kind == "player" && e.wallJumpFeedback > 0.0) + e.spine.player.setColor(0.45, 0.82, 1.0, 1.0); + else if ((e.kind == "player" && e.invulnerable > 0.0) || ("impactCooldown" in e && e.impactCooldown > 0.0)) e.spine.player.setColor(1.0, 0.55, 0.48, 1.0); else e.spine.player.setColor(1.0, 1.0, 1.0, 1.0); diff --git a/examples/metroidvania/maps/world.json b/examples/metroidvania/maps/world.json index 3f9f0ee3..2fc424d4 100644 --- a/examples/metroidvania/maps/world.json +++ b/examples/metroidvania/maps/world.json @@ -4508,9 +4508,9 @@ 0, 0, 2, - 1, - 1, - 1, + 0, + 0, + 0, 2, 2, 0, @@ -4627,7 +4627,7 @@ 0, 0, 0, - 2, + 0, 0, 0, 0, @@ -4747,7 +4747,7 @@ 0, 0, 0, - 2, + 0, 0, 0, 0, @@ -4867,7 +4867,7 @@ 0, 0, 0, - 2, + 0, 0, 0, 0, @@ -10676,10 +10676,10 @@ { "name": "wall_jump", "type": "ability_walljump", - "x": 1152, - "y": 448, - "width": 32, - "height": 32 + "x": 1024, + "y": 608, + "width": 64, + "height": 224 }, { "name": "cp_ruins", diff --git a/examples/metroidvania/smoke_test.py b/examples/metroidvania/smoke_test.py index 9fd3562e..f6da8e05 100644 --- a/examples/metroidvania/smoke_test.py +++ b/examples/metroidvania/smoke_test.py @@ -35,26 +35,36 @@ def replay_input(process_id: int) -> None: if not target.value: raise RuntimeError("game window did not appear") - user32.SetForegroundWindow(target.value) + def set_key(vk: int, down: bool) -> None: + scan = user32.MapVirtualKeyW(vk, 0) + flags = 1 | (scan << 16) + message = 0x0100 # WM_KEYDOWN + if not down: + message = 0x0101 # WM_KEYUP + flags |= (1 << 30) | (1 << 31) + if not user32.PostMessageW(target.value, message, vk, flags): + raise RuntimeError(f"failed to post key 0x{vk:02x}") def key(vk: int, held: float = 0.08) -> None: - user32.keybd_event(vk, 0, 0, 0) + set_key(vk, True) time.sleep(held) - user32.keybd_event(vk, 0, 2, 0) + set_key(vk, False) - user32.keybd_event(ord("D"), 0, 0, 0) + set_key(ord("D"), True) time.sleep(0.8) key(0x20, 0.12) # jump while moving right - time.sleep(0.65) - user32.keybd_event(ord("D"), 0, 2, 0) + time.sleep(0.16) + key(0x20, 0.09) # double jump + time.sleep(0.40) + set_key(ord("D"), False) for _ in range(3): key(ord("J")) time.sleep(0.2) key(ord("K")) - user32.keybd_event(ord("A"), 0, 0, 0) + set_key(ord("A"), True) time.sleep(0.35) key(0x20, 0.09) # reverse-facing jump - user32.keybd_event(ord("A"), 0, 2, 0) + set_key(ord("A"), False) def main() -> int: diff --git a/examples/metroidvania/tuning.nut b/examples/metroidvania/tuning.nut index aa177094..7d1b4085 100644 --- a/examples/metroidvania/tuning.nut +++ b/examples/metroidvania/tuning.nut @@ -6,13 +6,17 @@ TUNE <- { airAcceleration = 8.5, airControl = 0.72, jumpSpeed = 470.0, + airJumpSpeed = 445.0, + airJumpCount = 1, wallJumpX = 330.0, - wallJumpY = 440.0, + wallJumpY = 470.0, wallSlideSpeed = 105.0, coyoteTime = 0.10, jumpBuffer = 0.12, jumpCutAcceleration = 1550.0, wallJumpLockTime = 0.14, + wallContactGrace = 0.12, + wallJumpFeedbackTime = 0.16, dashSpeed = 650.0, dashTime = 0.16, playerHp = 100.0, From 8dd21d3ea3f24062668eb49312773d5755e840a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Sat, 15 Aug 2026 10:46:26 +0800 Subject: [PATCH 11/21] feat(webgpu): integrate WebGPU backend support and enhance build configuration - Added support for the WebGPU backend in the CMake configuration, enabling builds for Emscripten and native Dawn. - Implemented conditional compilation for WebGPU-specific features, including shader management and file system handling. - Updated the build process to exclude unnecessary modules and dependencies for WebGPU, optimizing the engine for browser environments. - Enhanced the installation script to include WebGPU-related files and directories in the output. - Refactored existing modules to accommodate WebGPU, ensuring compatibility and performance improvements across the engine. - Introduced new compile options and configurations tailored for WebGPU builds, including handling of third-party libraries and assets. --- CMakeLists.txt | 164 +- cmake/install_sdk.cmake | 18 + cmake/rescan_source.cmake | 36 +- cmake/scan_source.cmake | 43 +- platform/webgpu/CMakeLists.txt | 14 + platform/webgpu/README.md | 109 + platform/webgpu/game-shell/config.nut | 7 + platform/webgpu/game-shell/main.nut | 34 + platform/webgpu/game-shell/root.nut | 13 + platform/webgpu/shell.html | 94 + platform/webgpu/webplatform.cpp | 86 + platform/webgpu/webplatform.h | 68 + scripts/compile_webgpu_shaders.py | 142 + scripts/open_webgpu.bat | 30 + src/engine/CMakeLists.txt | 85 +- src/engine/cmdline/Run.cpp | 53 +- src/engine/common/config.h.in | 1 + src/engine/main.cpp | 20 +- src/modules/CMakeLists.txt | 175 +- src/modules/event/sdl/Event.cpp | 4 + src/modules/filesystem/FileWatch.cpp | 39 + src/modules/filesystem/FileWatch.h | 14 +- src/modules/filesystem/Filesystem.cpp | 4 + src/modules/filesystem/HotReload.cpp | 9 + src/modules/filesystem/physfs/Filesystem.cpp | 8 + src/modules/gpgpu/Gpgpu.cpp | 53 +- src/modules/gpgpu/webgpu/WebGpuGpgpu.cpp | 316 ++ src/modules/gpgpu/webgpu/WebGpuGpgpu.h | 77 + src/modules/graphics/Graphics.cpp | 13 + src/modules/graphics/webgpu/Canvas.cpp | 82 + src/modules/graphics/webgpu/Canvas.h | 50 + src/modules/graphics/webgpu/Graphics.cpp | 3024 ++++++++++++++++++ src/modules/graphics/webgpu/Graphics.h | 539 ++++ src/modules/graphics/webgpu/wgsl_shaders.h | 537 ++++ src/modules/system/System.cpp | 24 + src/modules/ui/imgui/ImGuiBackend.cpp | 82 +- src/modules/ui/imgui/imgui_impl_wgpu.cpp | 456 +++ src/modules/ui/imgui/imgui_impl_wgpu.h | 18 + src/modules/window/sdl/Window.cpp | 17 +- src/scripts/load_web.nut | 131 + src/scripts/scripts.h | 1 + src/scripts/template.cpp.in | 2 + 42 files changed, 6573 insertions(+), 119 deletions(-) create mode 100644 platform/webgpu/CMakeLists.txt create mode 100644 platform/webgpu/README.md create mode 100644 platform/webgpu/game-shell/config.nut create mode 100644 platform/webgpu/game-shell/main.nut create mode 100644 platform/webgpu/game-shell/root.nut create mode 100644 platform/webgpu/shell.html create mode 100644 platform/webgpu/webplatform.cpp create mode 100644 platform/webgpu/webplatform.h create mode 100644 scripts/compile_webgpu_shaders.py create mode 100644 scripts/open_webgpu.bat create mode 100644 src/modules/gpgpu/webgpu/WebGpuGpgpu.cpp create mode 100644 src/modules/gpgpu/webgpu/WebGpuGpgpu.h create mode 100644 src/modules/graphics/webgpu/Canvas.cpp create mode 100644 src/modules/graphics/webgpu/Canvas.h create mode 100644 src/modules/graphics/webgpu/Graphics.cpp create mode 100644 src/modules/graphics/webgpu/Graphics.h create mode 100644 src/modules/graphics/webgpu/wgsl_shaders.h create mode 100644 src/modules/ui/imgui/imgui_impl_wgpu.cpp create mode 100644 src/modules/ui/imgui/imgui_impl_wgpu.h create mode 100644 src/scripts/load_web.nut diff --git a/CMakeLists.txt b/CMakeLists.txt index fd87a1bc..125ed560 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ include(CTest) function(main) get_available_build() load_third_party() + load_webgpu_backend() set_include() check_ipo() set(CMAKE_CXX_STANDARD 20) @@ -53,6 +54,53 @@ function(main) set(BUILD_TESTING OFF CACHE BOOL "enable testing" FORCE) message(STATUS "iOS build: unit tests disabled") endif() + # WebGPU (browser WASM + native Dawn): unit tests are disabled by default — + # most tests link Vulkan-only helpers. Native Dawn builds can opt back in + # with -DBUILD_TESTING=ON once the test tree is backend-neutral. + if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set(BUILD_TESTING OFF CACHE BOOL "enable testing" FORCE) + message(STATUS "Emscripten/webgpu build: unit tests disabled") + # Emdawnwebgpu port: provides + + # and the browser WebGPU JS glue. Directory-scope so every module target + # (EVGraphics, EVUI, EVGpgpu, eve_imgui, ...) gets it at compile + link. + add_compile_options(--use-port=emdawnwebgpu) + add_link_options(--use-port=emdawnwebgpu) + # SDL2 and zlib come from Emscripten ports (vendored trees are skipped + # by third-party/CMakeLists.txt on Emscripten). -sUSE_SDL=2 must also be + # a compile flag so TUs that include (imgui_impl_sdl, window, + # event, mouse, keyboard, ...) resolve the port headers. + add_compile_options(-sUSE_SDL=2 -sUSE_ZLIB) + add_link_options(-sUSE_SDL=2 -sUSE_ZLIB) + # PROXY_TO_PTHREAD turns on shared memory; every TU must be compiled + # with atomics/bulk-memory or wasm-ld rejects the final link. + add_compile_options(-matomics -mbulk-memory) + # Trimmed third-party / module set for the WASM runtime. + set(EVENGINE_WEBGPU_TRIM TRUE CACHE BOOL "Trimmed dependency/module set for Emscripten" FORCE) + # Emscripten 6.x vendors a fork of CheckTypeSize.cmake that is + # incompatible with CMake >= 4.2 (mpg123/physfs/... try_compile fail + # with "/CheckTypeSize.c.in does not exist"). Force a CMake 3.29.x for + # Emscripten builds: it must be on PATH (or pip-installed) so that both + # this configure and the ExternalProject sub-builds use it. + find_program(EVENGINE_WEB_CMAKE NAMES cmake + PATHS "$ENV{APPDATA}/Python/Python314/Scripts" + "$ENV{APPDATA}/Python/Python311/Scripts" + "$ENV{USERPROFILE}/.local/bin") + if(EVENGINE_WEB_CMAKE) + execute_process(COMMAND "${EVENGINE_WEB_CMAKE}" --version + OUTPUT_VARIABLE _eve_cmake_ver OUTPUT_STRIP_TRAILING_WHITESPACE) + if(_eve_cmake_ver MATCHES "cmake version 3\.2[0-9]\.") + message(STATUS "Emscripten build: using CMake ${_eve_cmake_ver} from ${EVENGINE_WEB_CMAKE}") + set(CMAKE_COMMAND "${EVENGINE_WEB_CMAKE}" CACHE INTERNAL "CMake executable (forced 3.29 for Emscripten)") + else() + message(WARNING "Emscripten build needs CMake 3.29.x (found: ${_eve_cmake_ver}). " + "Install with: python -m pip install cmake==3.29.6") + endif() + else() + message(WARNING "Emscripten build needs CMake 3.29.x on PATH (pip: python -m pip install cmake==3.29.6)") + endif() + elseif(BUILD_PLATFORM STREQUAL "webgpu" AND BUILD_TESTING) + message(STATUS "webgpu (native Dawn) build: unit tests enabled") + endif() add_subdirectory(external/zeroerr) target_compile_definitions(zeroerr PUBLIC ZEROERR_NO_MAIN) # Strip ASSERT macros in non-Debug (upstream ZEROERR_NO_ASSERT under NDEBUG is commented out). @@ -85,6 +133,8 @@ macro(create_module libname folder) execute_process(COMMAND ${CMAKE_COMMAND} -Doutput_dir=${CMAKE_CURRENT_BINARY_DIR} -Dmodule_name=${folder} + -Dexclude_dirs=${EVENGINE_EXCLUDED_BACKEND_SUBDIRS} + "-Dexclude_files=${EVENGINE_EXCLUDED_MODULE_FILES}" -P ${CMAKE_SOURCE_DIR}/cmake/scan_source.cmake WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) @@ -105,7 +155,10 @@ macro(create_module libname folder) COMMAND ${CMAKE_COMMAND} -Doutput_dir=${CMAKE_CURRENT_BINARY_DIR} -Dbuild_dir=${CMAKE_BINARY_DIR} - -Dmodule_name=${folder} -P ${CMAKE_SOURCE_DIR}/cmake/rescan_source.cmake + -Dmodule_name=${folder} + -Dexclude_dirs=${EVENGINE_EXCLUDED_BACKEND_SUBDIRS} + "-Dexclude_files=${EVENGINE_EXCLUDED_MODULE_FILES}" + -P ${CMAKE_SOURCE_DIR}/cmake/rescan_source.cmake WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) @@ -127,6 +180,11 @@ function(set_include) ${CMAKE_CURRENT_SOURCE_DIR}/external/ECS.hpp/src ${CMAKE_CURRENT_SOURCE_DIR}/external/ik.hpp ${CMAKE_CURRENT_SOURCE_DIR}/external/zeroerr/include + # medialoader public headers (image/pixelformat.h etc.) — needed by + # ImageData.h; the vendored tree is present even when the library is + # not linked (Emscripten trim). + ${CMAKE_CURRENT_SOURCE_DIR}/third-party/medialoader/include + ${CMAKE_CURRENT_SOURCE_DIR}/third-party/medialoader/model-libs/assimp/include # Dungeon Template Library (header-only) ${CMAKE_CURRENT_SOURCE_DIR}/external/DungeonTemplateLibrary/include ) @@ -136,6 +194,15 @@ function(set_include) # On iOS, link/embed the SDK's MoltenVK.xcframework (iphoneos arm64 slice). # On Android, link the NDK loader (libvulkan.so). # + # WebGPU builds (browser WASM or native Dawn) use webgpu.h and do not touch + # the Vulkan SDK at all; the Vulkan backend directories are excluded by the + # source scanner, so this probe would only ever FATAL_ERROR a valid build. + if(EVENGINE_WEBGPU) + message(STATUS "WebGPU build: skipping Vulkan SDK probe") + set(EVENGINE_VULKAN_LIB "" CACHE INTERNAL "Vulkan link target (unused on webgpu)") + return() + endif() + # # Windows: installer sets Machine-level VULKAN_SDK, but long-lived IDEs (Cursor) # often still lack it in the process env. Probe C:/VulkanSDK/* when unset. if(WIN32 AND (NOT DEFINED ENV{VULKAN_SDK} OR "$ENV{VULKAN_SDK}" STREQUAL "")) @@ -262,8 +329,13 @@ endfunction() macro(get_available_build) set(AVAILABLE_PLATFORM "") + # Cross-compiling for Emscripten (web/WASM, browser WebGPU) must win over host detection. + if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + list(APPEND AVAILABLE_PLATFORM "webgpu") + set(BUILD_PLATFORM "webgpu" CACHE STRING "The build platfrom (${AVAILABLE_PLATFORM})") + SET_PROPERTY(CACHE BUILD_PLATFORM PROPERTY STRINGS ${AVAILABLE_PLATFORM}) # Cross-compiling for Android (NDK toolchain) must win over host Apple/Linux detection. - if(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) + elseif(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) list(APPEND AVAILABLE_PLATFORM "android") set(BUILD_PLATFORM "android" CACHE STRING "The build platfrom (${AVAILABLE_PLATFORM})") SET_PROPERTY(CACHE BUILD_PLATFORM PROPERTY STRINGS ${AVAILABLE_PLATFORM}) @@ -276,11 +348,13 @@ macro(get_available_build) list(APPEND AVAILABLE_PLATFORM "macosx") list(APPEND AVAILABLE_PLATFORM "ios") list(APPEND AVAILABLE_PLATFORM "android") + list(APPEND AVAILABLE_PLATFORM "webgpu") set(BUILD_PLATFORM "macosx" CACHE STRING "The build platfrom (${AVAILABLE_PLATFORM})") SET_PROPERTY(CACHE BUILD_PLATFORM PROPERTY STRINGS ${AVAILABLE_PLATFORM}) else() # Linux list(APPEND AVAILABLE_PLATFORM "linux") list(APPEND AVAILABLE_PLATFORM "android") + list(APPEND AVAILABLE_PLATFORM "webgpu") set(BUILD_PLATFORM "linux" CACHE STRING "The build platfrom (${AVAILABLE_PLATFORM})") SET_PROPERTY(CACHE BUILD_PLATFORM PROPERTY STRINGS ${AVAILABLE_PLATFORM}) endif() @@ -288,6 +362,7 @@ macro(get_available_build) if(WIN32) list(APPEND AVAILABLE_PLATFORM "win32") list(APPEND AVAILABLE_PLATFORM "android") + list(APPEND AVAILABLE_PLATFORM "webgpu") set(BUILD_PLATFORM "win32" CACHE STRING "The build platfrom (${AVAILABLE_PLATFORM})") MESSAGE("Build Platform: ${BUILD_PLATFORM}") SET_PROPERTY(CACHE BUILD_PLATFORM PROPERTY STRINGS ${AVAILABLE_PLATFORM}) @@ -298,7 +373,7 @@ macro(get_available_build) # when switching BUILD_PLATFORM in the same build tree. foreach(_eve_plat_var IN ITEMS EVENGINE_WINDOWS EVENGINE_MACOSX EVENGINE_LINUX - EVENGINE_ANDROID EVENGINE_IOS) + EVENGINE_ANDROID EVENGINE_IOS EVENGINE_WEBGPU) unset(${_eve_plat_var} CACHE) unset(${_eve_plat_var}) endforeach() @@ -317,6 +392,30 @@ macro(get_available_build) set_platform_var(linux EVENGINE_LINUX) set_platform_var(android EVENGINE_ANDROID) set_platform_var(ios EVENGINE_IOS) + set_platform_var(webgpu EVENGINE_WEBGPU) + + # Backend directories compiled out of the active build. Native backends + # (graphics/vulkan, gpgpu/vulkan) are excluded from a webgpu build; the + # webgpu backend is excluded from every other platform. ui/imgui stays in + # the scan — ImGuiBackend.cpp is the EVUI implementation and gains a + # webgpu branch instead. + if(BUILD_PLATFORM STREQUAL "webgpu") + set(EVENGINE_EXCLUDED_BACKEND_SUBDIRS "vulkan" CACHE INTERNAL "Backend subdirs excluded from webgpu builds") + else() + set(EVENGINE_EXCLUDED_BACKEND_SUBDIRS "webgpu" CACHE INTERNAL "Backend subdirs excluded from native builds") + endif() + + # Poco-heavy sources trimmed from the WASM (Emscripten) build. The data + # module keeps ByteData (needed by gpgpu/webgpu) but drops the JSON/XML/ + # DataModule files that require Poco. graphics/Font.cpp needs the EVFont + # module (FreeType/Poco, skipped on WASM), so it is trimmed as well. + if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set(EVENGINE_EXCLUDED_MODULE_FILES + "DataModule.cpp;JsonDocument.cpp;XmlDocument.cpp;Font.cpp" + CACHE INTERNAL "Exact basenames excluded from module source scan on Emscripten") + else() + set(EVENGINE_EXCLUDED_MODULE_FILES "" CACHE INTERNAL "Exact basenames excluded from module source scan") + endif() message("Availabe: ${AVAILABLE_PLATFORM}") @@ -472,6 +571,17 @@ function(check_third_party_project name repo) # Force Ninja for deps: parent Xcode generator makes SDL try_compile unbearably slow. set(_eve_tp_use_ninja TRUE) message(STATUS "third-party iOS toolchain: ARCH=${CMAKE_OSX_ARCHITECTURES} DEPLOY=${CMAKE_OSX_DEPLOYMENT_TARGET} GEN=Ninja FILE=${_eve_ios_tp_toolchain}") + elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + # Third-party aggregates for the browser build use emcmake so they pick + # up the emcc toolchain; Ninja keeps the many try_compile probes fast. + find_program(_eve_ninja ninja REQUIRED) + list(APPEND _eve_tp_cmake_args + -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} + -DCMAKE_SYSTEM_NAME=Emscripten + -DCMAKE_MAKE_PROGRAM=${_eve_ninja} + ) + set(_eve_tp_use_ninja TRUE) + message(STATUS "third-party Emscripten toolchain: ${CMAKE_TOOLCHAIN_FILE}") endif() # --config is required for multi-config generators (VS); ignored by Ninja. @@ -535,4 +645,52 @@ function(load_third_party) ADD_CUSTOM_TARGET(deps DEPENDS third-party) endfunction() + +# Resolve the WebGPU headers / library for BUILD_PLATFORM=webgpu. +# * Emscripten (browser): webgpu.h / webgpu_cpp.h ship with the emsdk and are +# already on emcc's include path; no link target is needed. +# * Native desktop: Google Dawn is fetched via FetchContent and linked through +# the imported target dawn::dawn (set into EVENGINE_WEBGPU_LIB). +function(load_webgpu_backend) + if(NOT BUILD_PLATFORM STREQUAL "webgpu") + set(EVENGINE_WEBGPU_LIB "" CACHE INTERNAL "WebGPU link target (unused)") + return() + endif() + + if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + message(STATUS "WebGPU: using emsdk headers (no Dawn build needed)") + set(EVENGINE_WEBGPU_LIB "" CACHE INTERNAL "WebGPU link target (unused on Emscripten)") + return() + endif() + + message(STATUS "WebGPU (native): fetching Google Dawn") + include(FetchContent) + set(DAWN_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) + set(DAWN_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(DAWN_BUILD_WGPU_TESTS OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_NULL OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_D3D11 OFF CACHE BOOL "" FORCE) + set(DAWN_ENABLE_OPENGLES OFF CACHE BOOL "" FORCE) + set(DAWN_USE_XCODE OFF CACHE BOOL "" FORCE) + + set(EVENGINE_DAWN_TAG "main" CACHE STRING + "Dawn commit/tag to fetch for the native WebGPU backend (pin for reproducible builds)") + FetchContent_Declare(dawn + GIT_REPOSITORY https://github.com/google/dawn.git + GIT_TAG ${EVENGINE_DAWN_TAG} + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + ) + FetchContent_MakeAvailable(dawn) + + if(NOT TARGET dawn::dawn) + message(FATAL_ERROR "Dawn fetch did not produce the imported target dawn::dawn") + endif() + # Dawn's webgpu.h / webgpu_cpp.h live under src/ in the checkout; expose + # them to every module target (engine, eve_imgui, gpgpu/webgpu, ...). + include_directories("${dawn_SOURCE_DIR}/src") + set(EVENGINE_WEBGPU_LIB dawn::dawn CACHE INTERNAL "WebGPU link target") + message(STATUS "WebGPU (native): Dawn ready, link target = ${EVENGINE_WEBGPU_LIB}") +endfunction() + main() diff --git a/cmake/install_sdk.cmake b/cmake/install_sdk.cmake index b7c3a133..169e8b24 100644 --- a/cmake/install_sdk.cmake +++ b/cmake/install_sdk.cmake @@ -20,6 +20,23 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS) BUNDLE DESTINATION bin RUNTIME DESTINATION bin ) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + install(TARGETS ${EVENGINE_NATIVE_TARGET} + RUNTIME DESTINATION bin + ) + # The browser build produces eve.js / eve.wasm / eve.html next to the + # runtime output; copy them plus the preloaded game into bin/. + install(FILES + "$/eve.js" + "$/eve.wasm" + "$/eve.html" + DESTINATION bin + OPTIONAL + ) + install(DIRECTORY "${CMAKE_SOURCE_DIR}/platform/webgpu/game-shell" + DESTINATION bin + OPTIONAL + ) else() install(TARGETS ${EVENGINE_NATIVE_TARGET} RUNTIME DESTINATION bin @@ -53,6 +70,7 @@ foreach(_eve_mod IN LISTS _eve_module_dirs) PATTERN "*.hpp" PATTERN "sdl" EXCLUDE PATTERN "vulkan" EXCLUDE + PATTERN "webgpu" EXCLUDE PATTERN "physfs" EXCLUDE PATTERN "openal" EXCLUDE PATTERN "imgui" EXCLUDE diff --git a/cmake/rescan_source.cmake b/cmake/rescan_source.cmake index 2b55b779..ecf44584 100644 --- a/cmake/rescan_source.cmake +++ b/cmake/rescan_source.cmake @@ -3,10 +3,42 @@ file(GLOB_RECURSE sub_source_files ${CMAKE_CURRENT_SOURCE_DIR}/${module_name}/** set(file_list "") foreach(file ${source_files}) - set(file_list "${file_list}${file}\n") + set(_eve_skip FALSE) + foreach(exc ${exclude_dirs}) + if(file MATCHES "/${exc}/" OR file MATCHES "${exc}[\\/]+") + set(_eve_skip TRUE) + endif() + endforeach() + if(NOT _eve_skip) + foreach(exc ${exclude_files}) + get_filename_component(_eve_fname "${file}" NAME) + if(_eve_fname STREQUAL "${exc}") + set(_eve_skip TRUE) + endif() + endforeach() + endif() + if(NOT _eve_skip) + set(file_list "${file_list}${file}\n") + endif() endforeach() foreach(file ${sub_source_files}) - set(file_list "${file_list}${file}\n") + set(_eve_skip FALSE) + foreach(exc ${exclude_dirs}) + if(file MATCHES "/${exc}/" OR file MATCHES "${exc}[\\/]+") + set(_eve_skip TRUE) + endif() + endforeach() + if(NOT _eve_skip) + foreach(exc ${exclude_files}) + get_filename_component(_eve_fname "${file}" NAME) + if(_eve_fname STREQUAL "${exc}") + set(_eve_skip TRUE) + endif() + endforeach() + endif() + if(NOT _eve_skip) + set(file_list "${file_list}${file}\n") + endif() endforeach() file(READ ${output_dir}/${module_name}_src.txt old_file_list) diff --git a/cmake/scan_source.cmake b/cmake/scan_source.cmake index ddd331af..72cb6079 100644 --- a/cmake/scan_source.cmake +++ b/cmake/scan_source.cmake @@ -1,13 +1,50 @@ file(GLOB_RECURSE source_files ${CMAKE_CURRENT_SOURCE_DIR}/${module_name}/*.cpp) file(GLOB_RECURSE sub_source_files ${CMAKE_CURRENT_SOURCE_DIR}/${module_name}/**/*.cpp) +# Semicolon-separated list of backend subdirectories to exclude from the module +# (e.g. "vulkan" for webgpu builds, "webgpu" for native builds). Entries match +# either an exact file name or a path fragment. +# Semicolon-separated list of exact file base names to exclude (e.g. Poco-heavy +# files trimmed from the WASM build). set(file_list "") foreach(file ${source_files}) - set(file_list "${file_list}${file}\n") + set(_eve_skip FALSE) + foreach(exc ${exclude_dirs}) + if(file MATCHES "/${exc}/" OR file MATCHES "${exc}[\\/]+") + set(_eve_skip TRUE) + endif() + endforeach() + if(NOT _eve_skip) + foreach(exc ${exclude_files}) + get_filename_component(_eve_fname "${file}" NAME) + if(_eve_fname STREQUAL "${exc}") + set(_eve_skip TRUE) + endif() + endforeach() + endif() + if(NOT _eve_skip) + set(file_list "${file_list}${file}\n") + endif() endforeach() foreach(file ${sub_source_files}) - set(file_list "${file_list}${file}\n") + set(_eve_skip FALSE) + foreach(exc ${exclude_dirs}) + if(file MATCHES "/${exc}/" OR file MATCHES "${exc}[\\/]+") + set(_eve_skip TRUE) + endif() + endforeach() + if(NOT _eve_skip) + foreach(exc ${exclude_files}) + get_filename_component(_eve_fname "${file}" NAME) + if(_eve_fname STREQUAL "${exc}") + set(_eve_skip TRUE) + endif() + endforeach() + endif() + if(NOT _eve_skip) + set(file_list "${file_list}${file}\n") + endif() endforeach() file(WRITE ${output_dir}/${module_name}_src.txt "${file_list}") -message(STATUS "detected source files: ${output_dir}/${module_name}_src.txt") \ No newline at end of file +message(STATUS "detected source files: ${output_dir}/${module_name}_src.txt") diff --git a/platform/webgpu/CMakeLists.txt b/platform/webgpu/CMakeLists.txt new file mode 100644 index 00000000..4645a09c --- /dev/null +++ b/platform/webgpu/CMakeLists.txt @@ -0,0 +1,14 @@ +# WebGPU platform: shared by the browser (Emscripten/WASM) and native Dawn +# desktop builds. The platform library is backend-neutral; toolchain-specific +# link flags (Emscripten) and game-asset packaging are applied in +# src/engine/CMakeLists.txt (which owns the `eve` host target). + +add_library(EVPlatformWebGPU STATIC webplatform.cpp) +target_include_directories(EVPlatformWebGPU PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..) +target_compile_features(EVPlatformWebGPU PUBLIC cxx_std_20) + +set(EVENGINE_PLATFORM_LIB EVPlatformWebGPU CACHE INTERNAL "Platform library for the active BUILD_PLATFORM") + +if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + message(STATUS "WebGPU (Emscripten): game dir = ${EVENGINE_WEBGPU_GAME_DIR}") +endif() diff --git a/platform/webgpu/README.md b/platform/webgpu/README.md new file mode 100644 index 00000000..6d58620c --- /dev/null +++ b/platform/webgpu/README.md @@ -0,0 +1,109 @@ +# WebGPU target (browser WASM + native Dawn) + +The `webgpu` platform runs the engine on WebGPU instead of Vulkan. It shares a +single backend under `src/modules/graphics/webgpu/` (and `src/modules/gpgpu/webgpu/`) +that targets the standard `webgpu.h` API, and can be produced two ways: + +- **Browser**: compiled with Emscripten to WASM, rendering with the browser's + native WebGPU (Chromium/Edge/Firefox with `--enable-unsafe-webgpu`). Shaders + must be WGSL. +- **Native desktop**: links Google Dawn (`dawn::dawn`), which accepts WGSL (and + SPIR-V) and runs on top of Vulkan/Metal/D3D12. + +## Prerequisites + +- **Emscripten** (`emsdk`, `emcmake`) with WebGPU support. Emscripten ships the + `webgpu.h`/`webgpu_cpp.h` headers. +- **CMake 3.29.x** (pip-installed). CMake 4.x is **incompatible** with the + Emscripten SDK fork of `CheckTypeSize.cmake` (`/CheckTypeSize.c.in does not + exist`), which breaks third-party configure (e.g. mpg123). Install and force + CMake 3.29: + ```sh + python -m pip install cmake==3.29.6 + # EMSCRIPTEN builds must use this cmake for BOTH the main and the + # third-party aggregate — the root CMakeLists detects and forces it. + ``` +- **Native Dawn**: fetched automatically via CMake `FetchContent` (fixed commit). +- A WebGPU-capable browser (Chrome 113+ / Edge 113+ / Firefox 118+). + +## Browser build (Emscripten / WASM) + +```sh +# activate emsdk once +emsdk activate latest && emsdk_env.bat # Windows +emcmake cmake -B build/webgpu-web -G Ninja -DCMAKE_BUILD_TYPE=Release +cmake --build build/webgpu-web # produces eve.js / eve.wasm / eve.data +``` + +The Emscripten build **trims** third-party dependencies and engine modules: + +- **Skipped deps**: Poco, OpenAL-soft, medialoader (assimp/mpg123/vorbis), FreeType, + googletest. SDL2 + zlib come from Emscripten ports (`-sUSE_SDL=2`, `-sUSE_ZLIB`). + Kept: squirrel/simplesquirrel, physfs, lz4, Box2D, xxHash, imgui, and a minimal + `medialoader_image` (stb/lodepng/dds/tinyexr image decoders, needed by EVImage). +- **Compiled-out modules** (their deps were dropped): + `EVAnimation EVAudio EVAvatar EVBuilding EVDatabase EVDialogue EVFont EVInventory + EVMap EVModel3D EVNetwork EVParticles EVPlugins EVProcgen EVRPG EVSound EVStylize + EVVoxel`, plus `graphics/Font.cpp` and the Poco-heavy `DataModule/JsonDocument/ + XmlDocument`. +- **Threads**: the engine main loop runs on a pthread + (`-sPROXY_TO_PTHREAD=1`), so **all TUs are compiled with `-matomics + -mbulk-memory`** (engine and third-party) and the page must be served with + COOP/COEP headers so `SharedArrayBuffer` is available. + +### Serving + browser verification + +The output directory is `build/webgpu-web/src/engine/`. Serve it with a real +HTTP server that sends the cross-origin-isolation headers: + +```sh +python build/serve_webgpu.py # serves http://127.0.0.1:8090/eve.html + # with COOP/COEP + no-store headers +``` + +Open `http://127.0.0.1:8090/eve.html` in Chrome/Edge (WebGPU enabled). +`shell.html` prints a status line with `crossOriginIsolated` and the resolved +WebGPU adapter, so init failures are visible without DevTools. + +**Headless verification** (optional): headless Chromium needs explicit GPU flags +for WebGPU to be exposed — plain `--headless` reports "No available adapters": + +```sh +msedge --headless=new --no-sandbox --disable-gpu-sandbox \ + --enable-unsafe-webgpu --enable-features=Vulkan --use-vulkan=swiftshader \ + --timeout=15000 --screenshot=frame.png http://127.0.0.1:8090/eve.html +``` + +Note: with `PROXY_TO_PTHREAD`, the main loop runs on a worker; use the real-time +`--timeout` wait (not `--virtual-time-budget`, which never advances the worker +clock) so the first frames are actually presented before the screenshot. + +The shell (`platform/webgpu/shell.html`) provides the `` +that the WebGPU surface attaches to. Game assets under +`platform/webgpu/game-shell/` are preloaded at `/game`. + +## Native Dawn build + +```sh +cmake -B build/webgpu-native -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_PLATFORM=webgpu +cmake --build build/webgpu-native +./build/webgpu-native/src/engine/eve run +``` + +Set `EVENGINE_WEBGPU_GAME_DIR` to point the platform helpers at a game folder +when not running from the game directory itself. + +## Notes / limitations + +- Runtime GLSL compilation (`gfx.newShader(glsl)`) is **not** available in the + browser — shaders must ship as pre-compiled WGSL (see the `*_wgsl.inc` + generation step). Native Dawn builds can compile GLSL via glslang+Tint. +- The Vulkan backend and `ui/imgui` Vulkan renderer are excluded from webgpu + builds by the source scanner (`EVENGINE_EXCLUDED_BACKEND_SUBDIRS`). +- ImGui overlays use `imgui_impl_wgpu.cpp` (compiled by the EVUI module scan; + `imgui_impl_wgpu.cpp` includes `common/config.h` **before** its + `#if defined(EVENGINE_WEBGPU)` guard so the platform define is visible). +- Poco/OpenAL/medialoader/FreeType are not built on WASM; the corresponding + modules are compiled out (see the trim list above). The native Dawn build + still compiles them. diff --git a/platform/webgpu/game-shell/config.nut b/platform/webgpu/game-shell/config.nut new file mode 100644 index 00000000..bb7bd814 --- /dev/null +++ b/platform/webgpu/game-shell/config.nut @@ -0,0 +1,7 @@ +config <- { + width = 800 + height = 600 + title = "EVEngine WebGPU" + debug = false + hotReload = false +}; diff --git a/platform/webgpu/game-shell/main.nut b/platform/webgpu/game-shell/main.nut new file mode 100644 index 00000000..aeeca8ec --- /dev/null +++ b/platform/webgpu/game-shell/main.nut @@ -0,0 +1,34 @@ +// EVEngine WebGPU minimal demo: bouncing 2D squares + HUD. +// Uses only the trimmed module set (Window/Graphics/Timer). + +eve_init <- function() { + gfx.setBackgroundColor(0.04, 0.05, 0.12, 1.0); + x <- 0.0; + y <- 0.0; + vx <- 180.0; + vy <- 120.0; + frameCount <- 0; + print("eve_init ok\n"); +}; + +eve_update <- function(dt) { + x = x + vx * dt; + y = y + vy * dt; + if (x < 0 || x > config.width - 40.0) vx = -vx; + if (y < 0 || y > config.height - 40.0) vy = -vy; +}; + +eve_render <- function() { + gfx.clear(); + + // Bouncing square. + gfx.drawSolidRect(x, y, 40.0, 40.0, 0.35, 0.85, 1.0, 1.0); + + // Static HUD bar. + gfx.drawSolidRect(10.0, 10.0, 180.0, 28.0, 0.15, 0.5, 0.95, 0.9); + + frameCount += 1; + if (frameCount % 30 == 0) { + print("frame " + frameCount + "\n"); + } +}; diff --git a/platform/webgpu/game-shell/root.nut b/platform/webgpu/game-shell/root.nut new file mode 100644 index 00000000..d23490a7 --- /dev/null +++ b/platform/webgpu/game-shell/root.nut @@ -0,0 +1,13 @@ +// WebGPU shell root script. When no main.nut ships with the game this runs the +// embedded demo (eve.demoScript) exactly like the desktop/iOS shells, but also +// exercises the WebGPU surface + ImGui overlay so a browser smoke test has +// something visible to validate. + +// demoScript is injected by the engine at runtime (src/scripts/demo.nut). +if ("demoScript" in eve && eve.demoScript != null && eve.demoScript != "") { + try { + compilestring(eve.demoScript)(); + } catch (e) { + print("embedded demo failed to load: " + e + "\n"); + } +} diff --git a/platform/webgpu/shell.html b/platform/webgpu/shell.html new file mode 100644 index 00000000..6e4524b7 --- /dev/null +++ b/platform/webgpu/shell.html @@ -0,0 +1,94 @@ + + + + + + EVEngine — WebGPU + + + +

+
+ Loading EVEngine… +
+ + + + {{{ SCRIPT }}} + + diff --git a/platform/webgpu/webplatform.cpp b/platform/webgpu/webplatform.cpp new file mode 100644 index 00000000..07023435 --- /dev/null +++ b/platform/webgpu/webplatform.cpp @@ -0,0 +1,86 @@ +#include "webgpu/webplatform.h" + +#include +#include +#include + +#if defined(__EMSCRIPTEN__) +#include +#endif + +namespace eve { +namespace webgpu_platform { + +namespace { +const char *envOr(const char *name, const char *fallback) { + const char *v = std::getenv(name); + return (v && *v) ? v : fallback; +} +} // namespace + +std::string getGameDirectory() { +#if defined(__EMSCRIPTEN__) + return "/game"; +#else + return envOr("EVENGINE_WEBGPU_GAME_DIR", "."); +#endif +} + +std::string getAppdataDirectory() { +#if defined(__EMSCRIPTEN__) + return "/persist"; +#else + const char *base = envOr("EVENGINE_WEBGPU_APPDATA", "appdata"); + return std::string(base); +#endif +} + +std::string getHomeDirectory() { +#if defined(__EMSCRIPTEN__) + return "/"; +#else + return "."; +#endif +} + +std::string getExecutablePath() { +#if defined(__EMSCRIPTEN__) + return std::string("eve.html"); +#else + return std::string("eve"); +#endif +} + +bool openURL(const std::string &url) { +#if defined(__EMSCRIPTEN__) + // Let the browser open the URL in a new tab. + char *js = new char[url.size() + 64]; + std::snprintf(js, url.size() + 64, "window.open('%s','_blank')", url.c_str()); + emscripten_run_script(js); + delete[] js; + return true; +#else + (void)url; + return false; +#endif +} + +void vibrate() { +#if defined(__EMSCRIPTEN__) + emscripten_run_script("if (navigator.vibrate) navigator.vibrate(500);"); +#endif +} + +bool hasBackgroundMusic() { return false; } + +void pumpEvents() { +#if defined(__EMSCRIPTEN__) + // Non-blocking browser event / callback drain. Works both on the main + // thread and inside pthread workers (emscripten_sleep below is a busy + // return to the browser, not a real sleep). + emscripten_sleep(0); +#endif +} + +} // namespace webgpu_platform +} // namespace eve diff --git a/platform/webgpu/webplatform.h b/platform/webgpu/webplatform.h new file mode 100644 index 00000000..dbbae7ce --- /dev/null +++ b/platform/webgpu/webplatform.h @@ -0,0 +1,68 @@ +#pragma once + +#include + +namespace eve { +namespace webgpu_platform { + +/** + * WebGPU platform services shared by the browser (Emscripten/WASM) and native + * Dawn builds. Mirrors the interface of platform/ios and platform/android so + * the engine code can call into it uniformly. + * + * On the browser the game directory is the preloaded VFS mount ("/game"), + * appdata is an IndexedDB-backed persistent dir ("/persist"), and the + * "executable" is the page itself. On native builds the helpers resolve to + * filesystem paths relative to the CWD. + */ + +/** + * Root directory containing the game assets (main.nut / config.nut / data). + * Browser: "/game" (--preload-file). Native: "." unless EVENGINE_WEBGPU_GAME_DIR + * is set. + **/ +std::string getGameDirectory(); + +/** + * Directory where files should be stored (saves / user data). + * Browser: "/persist" (IndexedDB). Native: "appdata" under the CWD. + **/ +std::string getAppdataDirectory(); + +/** + * Home directory. Browser: the VFS root "/". Native: CWD. + **/ +std::string getHomeDirectory(); + +/** + * Returns the full path to the "executable". + * Browser: the page URL. Native: argv[0]-style best effort. + **/ +std::string getExecutablePath(); + +/** + * Opens the specified URL in a new browser tab / system default browser. + * Browser: window.open. Native: no-op returning false. + **/ +bool openURL(const std::string &url); + +/** + * Causes devices with vibration support to vibrate for about 0.5 seconds. + * Browser: navigator.vibrate. Native: no-op. + **/ +void vibrate(); + +/** + * Returns whether another application is playing audio (browser: no-op). + **/ +bool hasBackgroundMusic(); + +/** + * Flush pending browser-side work (device requests, message queue). + * On Emscripten this processes the browser message queue so requestDevice + * callbacks fire on the main thread when PROXY_TO_PTHREAD is disabled. + **/ +void pumpEvents(); + +} // namespace webgpu_platform +} // namespace eve diff --git a/scripts/compile_webgpu_shaders.py b/scripts/compile_webgpu_shaders.py new file mode 100644 index 00000000..c6f34065 --- /dev/null +++ b/scripts/compile_webgpu_shaders.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Compile the EVEngine GLSL shader sources to WGSL C-arrays for the WebGPU +backend. + +For every `*.vert` / `*.frag` / `*.glsl` under src/modules/graphics/shaders, +this script runs the classic GLSL -> SPIR-V toolchain (glslc, or +glslangValidator as fallback) and then converts the SPIR-V to WGSL with Tint, +emitting a `_wgsl.inc` file holding a `static const uint32_t` array (UTF-8 +WGSL is embedded word-aligned via uint32_t storage, mirroring the existing +`*_spv.inc` convention). + +Existing files are only rewritten when the WGSL content actually changes, so the +committed `.inc` files stay stable across builds. + +Usage: + python3 scripts/compile_webgpu_shaders.py --shaders-dir [--tint ] [--glslc ] + +Environment overrides: EVENGINE_GLSLC, EVENGINE_TINT. +""" + +import argparse +import os +import re +import shutil +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + +STAGE_EXT = {".vert": "vertex", ".frag": "fragment", ".glsl": "fragment"} + + +def find_tool(name, env_name, extra_hints): + if env_name and os.environ.get(env_name): + return os.environ[env_name] + p = shutil.which(name) + if p: + return p + for hint in extra_hints: + if hint and os.path.exists(hint): + return hint + return None + + +def emit_wgsl_inc(src_path: Path, wgsl: bytes) -> Path: + out = src_path.with_suffix(src_path.suffix + "_wgsl.inc") + stem = src_path.stem.replace("-", "_").replace(".", "_") + array_name = f"{stem}_wgsl" + + # Pack the WGSL source so the C++ side can memcpy it as an opaque blob. + padded = wgsl + b"\x00" * (-len(wgsl) % 4) + words = struct.unpack(f"<{len(padded) // 4}I", padded) + lines = [f"static const uint32_t {array_name}[] = {{"] + for i in range(0, len(words), 8): + lines.append(" " + ", ".join(f"0x{w:08x}" for w in words[i : i + 8]) + ",") + lines.append("};") + lines.append(f"static const size_t {array_name}_count = {len(words)};") + content = "\n".join(lines) + "\n" + + if out.exists() and out.read_bytes() == content.encode("utf-8"): + return out + out.write_text(content, encoding="utf-8") + print(f"[webgpu-shaders] wrote {out}") + return out + + +def compile_one(glsl_path: Path, glslc, glslang, tint, scratch: Path): + ext = glsl_path.suffix.lower() + stage = STAGE_EXT.get(ext) + if stage is None: + return + + # Fragment includes (parallax_map.glsl / tex_cell_bomb.glsl / tonemap.glsl) + # are pulled into a main shader via #include; they have no entry point and + # cannot be compiled standalone. Skip them. + try: + src = glsl_path.read_text(encoding="utf-8", errors="replace") + except OSError: + return + if "void main" not in src: + print(f"[webgpu-shaders] SKIP {glsl_path.name}: include fragment (no main)") + return + + spv_path = scratch / (glsl_path.stem + ".spv") + + # 1) GLSL -> SPIR-V + if glslc: + cmd = [glslc, f"-fshader-stage={stage}", "-o", str(spv_path), str(glsl_path)] + elif glslang: + cmd = [glslang, f"-S{stage}", "-V", "-o", str(spv_path), str(glsl_path)] + else: + print(f"[webgpu-shaders] SKIP {glsl_path.name}: no glslc/glslangValidator on PATH") + return + subprocess.run(cmd, check=True, capture_output=True) + + # 2) SPIR-V -> WGSL + if not tint: + print(f"[webgpu-shaders] SKIP {glsl_path.name}: no tint on PATH") + return + wgsl_path = scratch / (glsl_path.stem + ".wgsl") + subprocess.run([tint, str(spv_path), "--format", "wgsl", "-o", str(wgsl_path)], + check=True, capture_output=True) + wgsl = wgsl_path.read_bytes() + emit_wgsl_inc(glsl_path, wgsl) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--shaders-dir", required=True) + ap.add_argument("--tint", default="") + ap.add_argument("--glslc", default="") + args = ap.parse_args() + + shaders_dir = Path(args.shaders_dir) + if not shaders_dir.is_dir(): + print(f"[webgpu-shaders] missing shaders dir {shaders_dir}") + return 1 + + glslc = args.glslc or find_tool("glslc", "EVENGINE_GLSLC", []) + glslang = args.glslc or find_tool("glslangValidator", "EVENGINE_GLSLANG", []) + tint = args.tint or find_tool("tint", "EVENGINE_TINT", []) + + if not (glslc or glslang): + print("[webgpu-shaders] WARNING: no GLSL compiler (glslc/glslangValidator); " + "WebGPU shader .inc files will not be (re)generated.") + if not tint: + print("[webgpu-shaders] WARNING: no tint found; WGSL .inc files will not be regenerated.") + + with tempfile.TemporaryDirectory(prefix="eve_wgsl_") as td: + scratch = Path(td) + for glsl_path in sorted(shaders_dir.glob("*")): + if glsl_path.suffix.lower() in STAGE_EXT: + try: + compile_one(glsl_path, glslc, glslang, tint, scratch) + except (subprocess.CalledProcessError, OSError) as e: + print(f"[webgpu-shaders] ERROR {glsl_path.name}: {e}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/open_webgpu.bat b/scripts/open_webgpu.bat new file mode 100644 index 00000000..26fc0cba --- /dev/null +++ b/scripts/open_webgpu.bat @@ -0,0 +1,30 @@ +@echo off +rem Launch EVEngine WebGPU demo in the default browser. +rem Serves build/webgpu-web/src/engine with COOP/COEP headers (required for +rem Emscripten pthreads / SharedArrayBuffer) and opens eve.html. + +setlocal +set "ROOT=%~dp0.." +set "ENGINE_DIR=%ROOT%\build\webgpu-web\src\engine" + +if not exist "%ENGINE_DIR%\eve.js" ( + echo eve.js not found. Build first: + echo emcmake cmake -B build/webgpu-web -G Ninja -DCMAKE_BUILD_TYPE=Release + echo cmake --build build/webgpu-web + exit /b 1 +) + +echo Checking for an existing server on 8090... +powershell -NoProfile -Command "try { (Invoke-WebRequest -Uri 'http://127.0.0.1:8090/eve.js' -Method Head -UseBasicParsing -TimeoutSec 2).StatusCode } catch { 'down' }" > "%TEMP%\eve_srv.txt" +set /p SRV=<"%TEMP%\eve_srv.txt" +if "%SRV%"=="200" ( + echo Server already running on 8090. +) else ( + echo Starting COOP/COEP server on 8090... + start "EVEngine WebGPU server" python "%ROOT%\build\serve_webgpu.py" + timeout /t 2 /nobreak >nul +) + +echo Opening http://127.0.0.1:8090/eve.html ... +start "" "http://127.0.0.1:8090/eve.html" +endlocal diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index e232d76d..dee989de 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -18,6 +18,7 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS) add_executable(eve MACOSX_BUNDLE main.cpp) set(EVENGINE_NATIVE_TARGET eve CACHE INTERNAL "Host target for SDK install") else() + # Desktop + webgpu (native Dawn and Emscripten/WASM) produce `eve`. add_executable(eve main.cpp) set(EVENGINE_NATIVE_TARGET eve CACHE INTERNAL "Host target for SDK install") endif() @@ -36,6 +37,10 @@ elseif(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) m c++_shared ) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + # Emscripten exposes pthread/dl/GL as virtual libraries; nothing to link. + # Game-side threading is enabled through -sPROXY_TO_PTHREAD (platform/webgpu). + set(SystemLibraries ${SystemLibraries}) elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS) set(SystemLibraries ${SystemLibraries} "-framework UIKit" @@ -129,6 +134,14 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS OR APPLE) freetype PocoDataSQLite PocoData PocoNet PocoXML PocoJSON PocoFoundation ${SystemLibraries}) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + # WASM-trimmed third-party: only the simple vendored libs built by the + # aggregate (squirrel/simplesquirrel/physfs/lz4/Box2D/xxHash). SDL2 and + # zlib come from Emscripten ports (-sUSE_SDL=2 / -sUSE_ZLIB); Poco/OpenAL/ + # medialoader/assimp/freetype are skipped (their modules are compiled out). + set(ThirdParty ${ThirdParty} simplesquirrel_static sqstdlib_static squirrel_static + medialoader_image physfs lz4 Box2D xxHash + ${SystemLibraries}) else() set(ThirdParty ${ThirdParty} simplesquirrel_static sqstdlib_static squirrel_static SDL2 SDL2main medialoader_image medialoader_model medialoader_sound assimp z physfs lz4 Box2D @@ -154,8 +167,9 @@ set(EVELIBS EVTouch EVCmdLine) -# Desktop only: DAP / MCP / DevTools (trimmed out of Android / iOS runtimes). -if(NOT (ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android") OR CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS)) +# Desktop only: DAP / MCP / DevTools (trimmed out of Android / iOS / browser runtimes). +if(NOT (ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android") OR CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS + OR CMAKE_SYSTEM_NAME STREQUAL "Emscripten")) list(APPEND EVELIBS EVDevTools) endif() @@ -191,7 +205,17 @@ list(APPEND EVELIBS EVStylize EVEditor) -if(EVENGINE_BUILD_DEMO) +# WASM (Emscripten) trims the modules whose third-party deps were dropped. +if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + foreach(_eve_trimmed IN ITEMS + EVAnimation EVAudio EVAvatar EVBuilding EVDatabase EVFont EVInventory + EVMap EVModel3D EVNetwork EVParticles EVPlugins EVRPG EVSound EVStylize EVVoxel + EVDialogue EVProcgen) + list(REMOVE_ITEM EVELIBS ${_eve_trimmed}) + endforeach() +endif() + +if(EVENGINE_BUILD_DEMO AND NOT (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")) list(APPEND EVELIBS EVDemo) endif() @@ -207,9 +231,9 @@ if(CMAKE_GENERATOR MATCHES "Xcode") foreach(_eve_lib IN LISTS EVELIBS) list(APPEND _eve_whole_archive_libs "$") endforeach() - target_link_libraries(${EVENGINE_NATIVE_TARGET} ${_eve_whole_archive_libs} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB}) + target_link_libraries(${EVENGINE_NATIVE_TARGET} ${_eve_whole_archive_libs} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB} ${EVENGINE_WEBGPU_LIB}) else() - target_link_libraries(${EVENGINE_NATIVE_TARGET} ${EVELIBS} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB}) + target_link_libraries(${EVENGINE_NATIVE_TARGET} ${EVELIBS} zeroerr ${ThirdParty} ${EVENGINE_VULKAN_LIB} ${EVENGINE_WEBGPU_LIB}) endif() if(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS) @@ -300,6 +324,48 @@ if(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS) endif() endif() +# ---- Emscripten (browser WebGPU / WASM) ---- +if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + # The engine's blocking script loop (load.nut) runs on a pthread; browser + # WebGPU calls are proxied to the main thread by Emscripten. + target_link_options(${EVENGINE_NATIVE_TARGET} PRIVATE + # main() runs on the browser main thread so emdawnwebgpu can reach the + # DOM canvas directly (PROXY_TO_PTHREAD would run main() on a worker + # with no DOM, and the OffscreenCanvas transfer path hits + # emscripten_proxy_async failures). pthreads stay available for the + # Thread module; ASYNCIFY lets the blocking main loop (emscripten_sleep) + # yield to the browser event loop. + -sUSE_PTHREADS=1 + -sALLOW_MEMORY_GROWTH=1 + -sEXIT_RUNTIME=0 + -sFORCE_FILESYSTEM=1 + -sASSERTIONS=1 + -sFETCH=1 + # The WebGPU backend spin-waits for requestAdapter/requestDevice with + # emscripten_sleep(0); on the main thread that requires ASYNCIFY. + -sASYNCIFY=1 + ) + + set(EVENGINE_WEBGPU_GAME_DIR "${CMAKE_SOURCE_DIR}/platform/webgpu/game-shell" CACHE PATH + "Directory preloaded as /game on the browser (default: platform/webgpu/game-shell)") + + set(_eve_web_shell "${CMAKE_SOURCE_DIR}/platform/webgpu/shell.html") + if(EXISTS "${_eve_web_shell}") + target_link_options(${EVENGINE_NATIVE_TARGET} PRIVATE --shell-file "${_eve_web_shell}") + endif() + + # --preload-file mounts the game assets at /game so getGameDirectory() + # ("/game") resolves directly against the preloaded VFS. + if(IS_DIRECTORY "${EVENGINE_WEBGPU_GAME_DIR}") + # --preload-file takes the source path relative to the CMake working + # directory; use the absolute game-shell dir so the wasm data embeds + # the assets directly at /game without a staging copy. + target_link_options(${EVENGINE_NATIVE_TARGET} PRIVATE + "--preload-file" "${EVENGINE_WEBGPU_GAME_DIR}@/game" + ) + endif() +endif() + # Plugins resolve ModuleManager / ModuleRegister symbols from the host process. # Export surface is EVENGINE_API (__declspec(dllexport)) only — do not enable # WINDOWS_EXPORT_ALL_SYMBOLS: it pulls Poco/ImGui template instantiations into @@ -317,8 +383,15 @@ if(WIN32) elseif(APPLE) # macOS + iOS: Apple ld rejects GNU --export-dynamic. target_link_options(${EVENGINE_NATIVE_TARGET} PRIVATE "LINKER:-export_dynamic") -elseif(UNIX AND NOT ANDROID AND NOT (CMAKE_SYSTEM_NAME STREQUAL "Android")) +elseif(UNIX AND NOT ANDROID AND NOT (CMAKE_SYSTEM_NAME STREQUAL "Android") + AND NOT (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")) target_link_options(${EVENGINE_NATIVE_TARGET} PRIVATE "LINKER:--export-dynamic") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + # WASM: dlopen/plugins are not supported; no symbol export flags needed. + set_target_properties(${EVENGINE_NATIVE_TARGET} PROPERTIES + CXX_VISIBILITY_PRESET default + VISIBILITY_INLINES_HIDDEN OFF + ) elseif(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) # libmain.so: keep default-visible EVENGINE_API symbols for dlopened plugins. set_target_properties(${EVENGINE_NATIVE_TARGET} PROPERTIES diff --git a/src/engine/cmdline/Run.cpp b/src/engine/cmdline/Run.cpp index 93228620..807a5524 100644 --- a/src/engine/cmdline/Run.cpp +++ b/src/engine/cmdline/Run.cpp @@ -3,7 +3,7 @@ #include "common/Module.h" #include "common/config.h" #include "filesystem/Filesystem.h" -#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) +#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) && !defined(EVENGINE_WEBGPU) #include "devtools/DevTool.hpp" #include "devtools/McpServer.hpp" #endif @@ -14,10 +14,34 @@ #include #include +#if defined(EVENGINE_WEBGPU) +#include + +namespace { +// Global frame-loop state: the root script (load_web.nut) defines a global +// eve_frame() function; emscripten_set_main_loop drives it per animation frame. +ssq::VM* gFrameVm = nullptr; +ssq::Function* gFrameFunc = nullptr; + +void webgpuFrameTick() { + if (!gFrameVm || !gFrameFunc || gFrameFunc->isEmpty()) return; + bool keep = true; + try { + ssq::Object r = gFrameVm->callFunc(*gFrameFunc, *gFrameVm); + if (r.getType() == ssq::Type::BOOL) keep = r.toBool(); + } catch (const std::exception& e) { + fprintf(stderr, "EVEngine: eve_frame error: %s\n", e.what()); + keep = false; + } + if (!keep) emscripten_cancel_main_loop(); +} +} // namespace +#endif + #if defined(EVENGINE_ANDROID) #include #define EVE_ANDROID_LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "EVEngine", __VA_ARGS__) -#elif defined(EVENGINE_IOS) +#elif defined(EVENGINE_IOS) || defined(EVENGINE_WEBGPU) #include #define EVE_ANDROID_LOGE(...) do { fprintf(stderr, "EVEngine: "); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } while (0) #else @@ -61,8 +85,14 @@ struct RunArgs : Handler { } std::string load_root((istreambuf_iterator(ifs)), (istreambuf_iterator())); return cmd.Run(current_path, load_root, debug, dap_port, mcp_port); - } else + } else { +#ifdef EVENGINE_WEBGPU + // The Emscripten runtime only wires the trimmed module set. + return cmd.Run(current_path, load_web_content, debug, dap_port, mcp_port); +#else return cmd.Run(current_path, load_content, debug, dap_port, mcp_port); +#endif + } } return -1; // not handle } @@ -100,7 +130,7 @@ int Cmdline::Run(std::string path, std::string root, bool debug, int dapPort, in ssq::VM vm(2048, ssq::Libs::ALL); ModuleManager::expose(vm); -#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) +#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) && !defined(EVENGINE_WEBGPU) if (debug) { auto& dt = eve::dev::DevTool::instance(); dt.attach(vm, /*sampleLocals=*/true); @@ -146,12 +176,21 @@ int Cmdline::Run(std::string path, std::string root, bool debug, int dapPort, in // Name the embedded root so DAP stack frames map to load.nut (not "buffer"). ssq::Script script = vm.compileSource(root.c_str(), "load.nut"); vm.run(script); -#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) +#if defined(EVENGINE_WEBGPU) + // Instead of a blocking while(running) Squirrel loop (which the browser + // never composites), drive the global eve_frame() function from an + // Emscripten requestAnimationFrame main loop. simulateInfiniteLoop=1 + // keeps main() alive; `vm` stays in scope because Run() never returns. + gFrameVm = &vm; + gFrameFunc = new ssq::Function(vm.find("eve_frame").toFunction()); + emscripten_set_main_loop(&webgpuFrameTick, 0, /*simulateInfiniteLoop=*/1); +#endif +#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) && !defined(EVENGINE_WEBGPU) if (debug) eve::dev::DevTool::instance().detach(); #endif return 0; } catch (const std::exception& e) { -#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) +#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) && !defined(EVENGINE_WEBGPU) if (debug) { const std::string report = eve::dev::DevTool::instance().notifyError(e.what()); @@ -166,7 +205,7 @@ int Cmdline::Run(std::string path, std::string root, bool debug, int dapPort, in EVE_ANDROID_LOGE("Run failed: %s", e.what()); return 3; } catch (...) { -#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) +#if !defined(EVENGINE_ANDROID) && !defined(EVENGINE_IOS) && !defined(EVENGINE_WEBGPU) if (debug) { const std::string report = eve::dev::DevTool::instance().notifyError("unknown exception"); diff --git a/src/engine/common/config.h.in b/src/engine/common/config.h.in index ec61498e..5803e3be 100644 --- a/src/engine/common/config.h.in +++ b/src/engine/common/config.h.in @@ -11,6 +11,7 @@ #cmakedefine EVENGINE_LINUX #cmakedefine EVENGINE_ANDROID #cmakedefine EVENGINE_IOS +#cmakedefine EVENGINE_WEBGPU #cmakedefine EVENGINE_WINDOWS_UWP diff --git a/src/engine/main.cpp b/src/engine/main.cpp index 6a0dbea0..4023ca14 100644 --- a/src/engine/main.cpp +++ b/src/engine/main.cpp @@ -6,7 +6,7 @@ #include #include -#if defined(EVENGINE_ANDROID) || defined(EVENGINE_IOS) +#if defined(EVENGINE_ANDROID) || defined(EVENGINE_IOS) || defined(EVENGINE_WEBGPU) #include #include #endif @@ -15,6 +15,10 @@ #include "ios/ios.h" #endif +#if defined(EVENGINE_WEBGPU) +#include "webgpu/webplatform.h" +#endif + using namespace eve; using namespace std; @@ -57,6 +61,20 @@ int main(int argc, char **argv) argv = injected.data(); } eve::ios::initAudioSessionInterruptionHandler(); +#elif defined(EVENGINE_WEBGPU) + // The browser launches with no CLI args; inject `run ` where + // the game is the preloaded VFS mount (/game) or the CWD. + static std::string gamePath; + static std::vector injected; + if (argc <= 1) { + gamePath = eve::webgpu_platform::getGameDirectory(); + if (gamePath.empty()) + gamePath = "."; + static char runFlag[] = "run"; + injected = {argv[0], runFlag, gamePath.data()}; + argc = static_cast(injected.size()); + argv = injected.data(); + } #endif return requireModInst(eve::cmd,Cmdline)->runArgs(argc, argv); } diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt index cf6009b6..cb753be2 100644 --- a/src/modules/CMakeLists.txt +++ b/src/modules/CMakeLists.txt @@ -1,10 +1,34 @@ create_module(EVFileSystem filesystem) -target_compile_definitions(EVFileSystem PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) +if(NOT EVENGINE_WEBGPU_TRIM) + target_compile_definitions(EVFileSystem PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) +endif() create_module(EVGraphics graphics) # Header-only VKBuilder: prefer the source tree over a stale third-party install. target_include_directories(EVGraphics BEFORE PUBLIC ${CMAKE_SOURCE_DIR}/third-party/VKBuilder/include) +if(EVENGINE_WEBGPU_TRIM) + # Graphics.h includes (header-only usage). The vendored + # assimp headers live in medialoader (skipped on WASM) but the interface is + # still needed; expose the header tree without linking the library. + target_include_directories(EVGraphics BEFORE PUBLIC + ${CMAKE_SOURCE_DIR}/third-party/medialoader/model-libs/assimp/include) +endif() +if(BUILD_PLATFORM STREQUAL "webgpu") + # Regenerate WGSL .inc arrays from the GLSL shader sources when tooling + # (glslc + tint) is available; committed .inc files keep the build going + # otherwise. + find_package(Python3 COMPONENTS Interpreter QUIET) + if(Python3_Interpreter_FOUND) + add_custom_target(webgpu_shaders + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/scripts/compile_webgpu_shaders.py + --shaders-dir ${CMAKE_SOURCE_DIR}/src/modules/graphics/shaders + COMMENT "Generating WGSL shader .inc files for the WebGPU backend" + VERBATIM) + add_dependencies(EVGraphics webgpu_shaders) + endif() +endif() create_module(EVWindow window) create_module(EVEvent event) create_module(EVTimer timer) @@ -13,37 +37,50 @@ create_module(EVMath math) create_module(EVSpatial spatial) create_module(EVIK ik) target_include_directories(EVIK PUBLIC ${CMAKE_SOURCE_DIR}/external/ik.hpp) -create_module(EVAnimation animation) -target_compile_definitions(EVAnimation PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) -# AnimImporter uses Assimp headers (same install tree as model3d / medialoader). -# Spine JSON uses Poco via data/JsonDocument; atlas/skeleton loaders use filesystem. -target_include_directories(EVAnimation PRIVATE - ${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}-debug/include - ${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}/include - ${CMAKE_SOURCE_DIR}/third-party/medialoader/include - ${CMAKE_SOURCE_DIR}/third-party/medialoader/assimp/include -) +if(NOT EVENGINE_WEBGPU_TRIM) + create_module(EVAnimation animation) + target_compile_definitions(EVAnimation PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) + # AnimImporter uses Assimp headers (same install tree as model3d / medialoader). + # Spine JSON uses Poco via data/JsonDocument; atlas/skeleton loaders use filesystem. + target_include_directories(EVAnimation PRIVATE + ${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}-debug/include + ${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}/include + ${CMAKE_SOURCE_DIR}/third-party/medialoader/include + ${CMAKE_SOURCE_DIR}/third-party/medialoader/assimp/include + ) +endif() create_module(EVTensor tensor) create_module(EVMouse mouse) create_module(EVKeyboard keyboard) create_module(EVJoystick joystick) create_module(EVTouch touch) create_module(EVData data) -target_compile_definitions(EVData PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) +if(NOT EVENGINE_WEBGPU_TRIM) + target_compile_definitions(EVData PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) +endif() create_module(EVImage image) -create_module(EVNetwork network) -target_compile_definitions(EVNetwork PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) -create_module(EVDatabase database) -target_compile_definitions(EVDatabase PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) -create_module(EVSound sound) -target_include_directories(EVSound PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/sound/include_shim - ${CMAKE_SOURCE_DIR}/third-party/medialoader/sound-libs - ${CMAKE_SOURCE_DIR}/third-party/medialoader/sound-libs/libmodplug/src - ${CMAKE_SOURCE_DIR}/third-party/medialoader/sound-libs/libogg/include -) -create_module(EVAudio audio) -target_compile_definitions(EVAudio PRIVATE AL_LIBTYPE_STATIC) +if(EVENGINE_WEBGPU_TRIM) + # ImageData.h includes medialoader/image/pixelformat.h; the vendored + # medialoader header tree is available in the source checkout even though the + # library itself is skipped on Emscripten. + target_include_directories(EVImage BEFORE PUBLIC + ${CMAKE_SOURCE_DIR}/third-party/medialoader/include) +endif() +if(NOT EVENGINE_WEBGPU_TRIM) + create_module(EVNetwork network) + target_compile_definitions(EVNetwork PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) + create_module(EVDatabase database) + target_compile_definitions(EVDatabase PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) + create_module(EVSound sound) + target_include_directories(EVSound PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/sound/include_shim + ${CMAKE_SOURCE_DIR}/third-party/medialoader/sound-libs + ${CMAKE_SOURCE_DIR}/third-party/medialoader/sound-libs/libmodplug/src + ${CMAKE_SOURCE_DIR}/third-party/medialoader/sound-libs/libogg/include + ) + create_module(EVAudio audio) + target_compile_definitions(EVAudio PRIVATE AL_LIBTYPE_STATIC) +endif() # Dear ImGui (compiled from third-party sources; also installed by third-party ExternalProject) set(_eve_imgui_sources @@ -54,13 +91,25 @@ set(_eve_imgui_sources ${CMAKE_SOURCE_DIR}/third-party/imgui/imgui_impl_sdl.cpp ${CMAKE_SOURCE_DIR}/third-party/imgui/imgui_impl_vulkan.cpp ) +if(BUILD_PLATFORM STREQUAL "webgpu") + # Vulkan renderer is compiled out; the WebGPU backend (imgui_impl_wgpu.cpp) + # is compiled by the EVUI module source scan (ui/imgui/) to avoid duplicate + # symbols between EVUI and eve_imgui. + list(REMOVE_ITEM _eve_imgui_sources + ${CMAKE_SOURCE_DIR}/third-party/imgui/imgui_impl_vulkan.cpp) +endif() add_library(eve_imgui STATIC ${_eve_imgui_sources}) target_include_directories(eve_imgui PUBLIC ${CMAKE_SOURCE_DIR}/third-party/imgui) # imgui_impl_sdl includes ; vendored install puts headers under include/SDL2/. -target_include_directories(eve_imgui PRIVATE - ${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}-debug/include/SDL2 - ${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}/include/SDL2 -) +# On Emscripten the SDL2 headers come from the port sysroot (EMSCRIPTEN_SYSROOT). +if(EVENGINE_WEBGPU_TRIM) + target_include_directories(eve_imgui PRIVATE ${EMSCRIPTEN_SYSROOT}/include) +else() + target_include_directories(eve_imgui PRIVATE + ${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}-debug/include/SDL2 + ${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}/include/SDL2 + ) +endif() # Avoid pulling imgui_demo.cpp (ShowFontAtlas etc.) into every link. target_compile_definitions(eve_imgui PUBLIC IMGUI_DISABLE_DEMO_WINDOWS) @@ -72,8 +121,10 @@ target_include_directories(EVUI BEFORE PUBLIC create_module(EVScene scene) -create_module(EVParticles particles) -target_compile_definitions(EVParticles PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) +if(NOT EVENGINE_WEBGPU_TRIM) + create_module(EVParticles particles) + target_compile_definitions(EVParticles PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) +endif() create_module(EVPhysics physics) # Box2D headers use ; prefer source tree over flattened install. target_include_directories(EVPhysics PUBLIC ${CMAKE_SOURCE_DIR}/third-party) @@ -85,37 +136,49 @@ set(BOX3D_DOCS OFF CACHE BOOL "" FORCE) set(BOX3D_VALIDATE OFF CACHE BOOL "" FORCE) add_subdirectory(${CMAKE_SOURCE_DIR}/external/box3d ${CMAKE_BINARY_DIR}/external/box3d) target_link_libraries(EVPhysics PUBLIC box3d::box3d) -create_module(EVMap map) -target_compile_definitions(EVMap PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) +if(NOT EVENGINE_WEBGPU_TRIM) + create_module(EVMap map) + target_compile_definitions(EVMap PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) +endif() create_module(EVGpgpu gpgpu) target_include_directories(EVGpgpu BEFORE PRIVATE ${CMAKE_SOURCE_DIR}/third-party/VKBuilder/include) -create_module(EVModel3D model3d) -create_module(EVFont font) -# ft2build.h lives under include/freetype2/ (installed by third-party FreeType). -target_include_directories(EVFont PRIVATE - ${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}-debug/include/freetype2 - ${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}/include/freetype2 -) +if(NOT EVENGINE_WEBGPU_TRIM) + create_module(EVModel3D model3d) + create_module(EVFont font) + # ft2build.h lives under include/freetype2/ (installed by third-party FreeType). + target_include_directories(EVFont PRIVATE + ${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}-debug/include/freetype2 + ${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}/include/freetype2 + ) +endif() create_module(EVThread thread) -create_module(EVPlugins plugins) -create_module(EVRPG rpg) -target_compile_definitions(EVRPG PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) -create_module(EVInventory inventory) -target_compile_definitions(EVInventory PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) -create_module(EVBuilding building) -target_compile_definitions(EVBuilding PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) -create_module(EVAvatar avatar) -create_module(EVDialogue dialogue) -create_module(EVProcgen procgen) +if(NOT EVENGINE_WEBGPU_TRIM) + create_module(EVPlugins plugins) + create_module(EVRPG rpg) + target_compile_definitions(EVRPG PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) + create_module(EVInventory inventory) + target_compile_definitions(EVInventory PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) + create_module(EVBuilding building) + target_compile_definitions(EVBuilding PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) + create_module(EVAvatar avatar) +endif() +if(NOT EVENGINE_WEBGPU_TRIM) + create_module(EVDialogue dialogue) + create_module(EVProcgen procgen) +endif() # Compat shims first (Apple Clang: missing drawOperatorArray in upstream DTL), then DTL. # BEFORE must precede the visibility keyword so -I dtl_compat wins over the submodule. -target_include_directories(EVProcgen BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/procgen/dtl_compat) -target_include_directories(EVProcgen PUBLIC ${CMAKE_SOURCE_DIR}/external/DungeonTemplateLibrary/include) -create_module(EVVoxel voxel) -target_compile_definitions(EVVoxel PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) -create_module(EVStylize stylize) +if(NOT EVENGINE_WEBGPU_TRIM) + target_include_directories(EVProcgen BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/procgen/dtl_compat) + target_include_directories(EVProcgen PUBLIC ${CMAKE_SOURCE_DIR}/external/DungeonTemplateLibrary/include) +endif() +if(NOT EVENGINE_WEBGPU_TRIM) + create_module(EVVoxel voxel) + target_compile_definitions(EVVoxel PUBLIC POCO_STATIC POCO_NO_AUTOMATIC_LIBS) + create_module(EVStylize stylize) +endif() create_module(EVEditor editor) -if(EVENGINE_BUILD_DEMO) +if(EVENGINE_BUILD_DEMO AND NOT EVENGINE_WEBGPU_TRIM) create_module(EVDemo demo) endif() diff --git a/src/modules/event/sdl/Event.cpp b/src/modules/event/sdl/Event.cpp index 3135f652..d433c670 100644 --- a/src/modules/event/sdl/Event.cpp +++ b/src/modules/event/sdl/Event.cpp @@ -5,7 +5,9 @@ #include "graphics/Graphics.h" #include "window/Window.h" #include "window/sdl/Window.h" +#ifndef EVENGINE_WEBGPU #include "audio/Audio.h" +#endif #include "touch/Touch.h" #include "touch/sdl/Touch.h" #include "keyboard/Keyboard.h" @@ -141,8 +143,10 @@ void Event::pump() // Ownership stays in the queue until poll()/clear() — do not delete here. } +#ifndef EVENGINE_WEBGPU if (auto *audio = eve::ModuleManager::getInstance("Audio")) audio->pump(); +#endif } Message *Event::wait() diff --git a/src/modules/filesystem/FileWatch.cpp b/src/modules/filesystem/FileWatch.cpp index a7cfc509..86acf5df 100644 --- a/src/modules/filesystem/FileWatch.cpp +++ b/src/modules/filesystem/FileWatch.cpp @@ -1,4 +1,7 @@ #include "filesystem/FileWatch.h" +#include "common/config.h" + +#ifndef EVENGINE_WEBGPU #include #include @@ -6,7 +9,17 @@ #include +#endif + namespace eve::filesystem { + +#ifdef EVENGINE_WEBGPU +// WebGPU (browser) build has no native directory watching; DirWatch stays an +// empty struct so the unique_ptr members in the header compile. +struct FileWatch::DirWatch {}; +#endif + +#ifndef EVENGINE_WEBGPU namespace { std::string basenameOf(const std::string &path) { @@ -41,6 +54,7 @@ struct FileWatch::DirWatch { std::unordered_map filters; // filter → reportPath int refs = 0; }; +#endif // !EVENGINE_WEBGPU FileWatch::FileWatch() = default; @@ -48,6 +62,14 @@ FileWatch::~FileWatch() { clear(); } bool FileWatch::add(const std::string &realDir, const std::string &filterName, const std::string &reportPath, int scanInterval) { +#ifdef EVENGINE_WEBGPU + // Browser VFS has no native directory watching; no-op stub. + (void)realDir; + (void)filterName; + (void)reportPath; + (void)scanInterval; + return false; +#else if (realDir.empty() || reportPath.empty()) return false; const std::string dir = normalizeDir(realDir); try { @@ -112,9 +134,14 @@ bool FileWatch::add(const std::string &realDir, const std::string &filterName, ++dw->refs; reportToDir_[reportPath] = dir; return true; +#endif } bool FileWatch::remove(const std::string &reportPath) { +#ifdef EVENGINE_WEBGPU + (void)reportPath; + return false; +#else std::lock_guard lock(mu_); auto prev = reportToDir_.find(reportPath); if (prev == reportToDir_.end()) return false; @@ -143,10 +170,15 @@ bool FileWatch::remove(const std::string &reportPath) { byDir_.erase(it); } return true; +#endif } void FileWatch::clear() { std::lock_guard lock(mu_); +#ifdef EVENGINE_WEBGPU + queue_.clear(); + return; +#else for (auto &kv : byDir_) { DirWatch *dw = kv.second.get(); if (dw && dw->watcher) { @@ -160,11 +192,16 @@ void FileWatch::clear() { byDir_.clear(); reportToDir_.clear(); queue_.clear(); +#endif } int FileWatch::count() const { std::lock_guard lock(mu_); +#ifdef EVENGINE_WEBGPU + return 0; +#else return int(reportToDir_.size()); +#endif } bool FileWatch::poll(Event &out) { @@ -175,6 +212,7 @@ bool FileWatch::poll(Event &out) { return true; } +#ifndef EVENGINE_WEBGPU void FileWatch::handlePocoEvent(const std::string &kind, const std::string &itemPath) { const std::string name = basenameOf(itemPath); Poco::Path parent(itemPath); @@ -233,5 +271,6 @@ void FileWatch::onMovedFrom(const void *, const Poco::DirectoryWatcher::Director void FileWatch::onMovedTo(const void *, const Poco::DirectoryWatcher::DirectoryEvent &event) { handlePocoEvent("movedTo", event.item.path()); } +#endif // !EVENGINE_WEBGPU } // namespace eve::filesystem diff --git a/src/modules/filesystem/FileWatch.h b/src/modules/filesystem/FileWatch.h index e97decc8..36b5426a 100644 --- a/src/modules/filesystem/FileWatch.h +++ b/src/modules/filesystem/FileWatch.h @@ -1,7 +1,5 @@ #pragma once -#include - #include #include #include @@ -11,8 +9,10 @@ namespace eve::filesystem { /** - * OS directory watch (Poco::DirectoryWatcher) with a main-thread event queue. - * Watch a directory (all children) or a single file (parent dir + name filter). + * OS directory watch with a main-thread event queue. + * Desktop/mobile: backed by Poco::DirectoryWatcher (see FileWatch.cpp). + * WebGPU (Emscripten): the browser VFS has no native file watching, so this is + * a no-op stub — add()/remove() return false, poll() returns false. */ class FileWatch { public: @@ -49,12 +49,6 @@ public: private: struct DirWatch; - void onAdded(const void *sender, const Poco::DirectoryWatcher::DirectoryEvent &event); - void onRemoved(const void *sender, const Poco::DirectoryWatcher::DirectoryEvent &event); - void onModified(const void *sender, const Poco::DirectoryWatcher::DirectoryEvent &event); - void onMovedFrom(const void *sender, const Poco::DirectoryWatcher::DirectoryEvent &event); - void onMovedTo(const void *sender, const Poco::DirectoryWatcher::DirectoryEvent &event); - void handlePocoEvent(const std::string &kind, const std::string &itemPath); mutable std::mutex mu_; diff --git a/src/modules/filesystem/Filesystem.cpp b/src/modules/filesystem/Filesystem.cpp index 7f07738a..e007df95 100644 --- a/src/modules/filesystem/Filesystem.cpp +++ b/src/modules/filesystem/Filesystem.cpp @@ -15,6 +15,8 @@ #include "ios/ios.h" #elif defined(EVENGINE_ANDROID) #include "android/android.h" +#elif defined(EVENGINE_WEBGPU) +#include "webgpu/webplatform.h" #elif defined(EVENGINE_WINDOWS) #include @@ -103,6 +105,8 @@ std::string Filesystem::getExecutablePath() const { return ios::getExecutablePath(); #elif defined(EVENGINE_ANDROID) return android::getExecutablePath(); +#elif defined(EVENGINE_WEBGPU) + return eve::webgpu_platform::getExecutablePath(); #elif defined(EVENGINE_WINDOWS) wchar_t buffer[MAX_PATH + 1] = {0}; if (GetModuleFileNameW(nullptr, buffer, MAX_PATH) == 0) return ""; diff --git a/src/modules/filesystem/HotReload.cpp b/src/modules/filesystem/HotReload.cpp index 53f32a55..d4321918 100644 --- a/src/modules/filesystem/HotReload.cpp +++ b/src/modules/filesystem/HotReload.cpp @@ -3,10 +3,12 @@ #include "common/Module.h" #include "filesystem/Filesystem.h" #include "graphics/Graphics.h" +#ifndef EVENGINE_WEBGPU #include "map/TileConfig.h" #include "map/TileLayer.h" #include "particles/ParticleConfig.h" #include "particles/ParticleEmitter.h" +#endif #include #include @@ -67,6 +69,7 @@ bool HotReload::isJsonPath(const std::string &normPath) const { return extensionOf(normPath) == ".json"; } +#ifndef EVENGINE_WEBGPU bool HotReload::reloadParticles(const std::string &normPath) { if (ecs::current()->getManager() == nullptr) return false; @@ -95,6 +98,10 @@ bool HotReload::reloadTilemaps(const std::string &normPath) { } return reloaded > 0; } +#else +bool HotReload::reloadParticles(const std::string &) { return false; } +bool HotReload::reloadTilemaps(const std::string &) { return false; } +#endif bool HotReload::reloadTextures(const std::string &normPath) { auto *gfx = eve::ModuleManager::getInstance("Graphics"); @@ -102,6 +109,7 @@ bool HotReload::reloadTextures(const std::string &normPath) { bool ok = gfx->reloadTextureFromFile(normPath); +#ifndef EVENGINE_WEBGPU // Emitters that reference this texture path: force re-bind via config reload or setTexture. if (ecs::current()->getManager() != nullptr) { auto view = ecs::View #ifdef EVENGINE_ANDROID @@ -400,6 +404,8 @@ std::string Filesystem::getUserDirectory() { #ifdef EVENGINE_IOS // PHYSFS_getUserDir doesn't give exactly the path we want on iOS. static std::string userDir = normalize(ios::getHomeDirectory()); +#elif defined(EVENGINE_WEBGPU) + static std::string userDir = normalize(eve::webgpu_platform::getHomeDirectory()); #else const char* dir = PHYSFS_getUserDir(); static std::string userDir; @@ -412,6 +418,8 @@ std::string Filesystem::getAppdataDirectory() { if (appdata.empty()) { #ifdef EVENGINE_WINDOWS_UWP appdata = getUserDirectory(); +#elif defined(EVENGINE_WEBGPU) + appdata = normalize(eve::webgpu_platform::getAppdataDirectory()); #elif defined(EVENGINE_WINDOWS) wchar_t *w_appdata = _wgetenv(L"APPDATA"); appdata = to_utf8(w_appdata); diff --git a/src/modules/gpgpu/Gpgpu.cpp b/src/modules/gpgpu/Gpgpu.cpp index 91ebc726..a17df99d 100644 --- a/src/modules/gpgpu/Gpgpu.cpp +++ b/src/modules/gpgpu/Gpgpu.cpp @@ -2,14 +2,20 @@ #include "gpgpu/ComputeShader.h" #include "gpgpu/EcsScriptPack.h" #include "gpgpu/GpuBuffer.h" -#include "gpgpu/vulkan/VulkanGpgpu.h" -#include "gpgpu/vulkan/VulkanUtil.h" #include "gpgpu/ShaderSystem.h" #include "common/Exception.h" #include "common/Module.h" +#include "filesystem/Filesystem.h" #include "graphics/Graphics.h" +#ifdef EVENGINE_WEBGPU +#include "gpgpu/webgpu/WebGpuGpgpu.h" +#else +#include "gpgpu/vulkan/VulkanGpgpu.h" +#include "gpgpu/vulkan/VulkanUtil.h" +#endif + #include #include @@ -29,39 +35,82 @@ std::string currentGraphicsBackend() { Module_IMPL(Gpgpu, new Gpgpu()); bool Gpgpu::isAvailable() const { +#ifdef EVENGINE_WEBGPU + if (currentGraphicsBackend() != "webgpu") return false; + return webgpuGpgpuReady(); +#else if (currentGraphicsBackend() != "vulkan") return false; return vulkanGpgpuReady(); +#endif } ComputeShader *Gpgpu::newShader(const std::string &source) { +#ifdef EVENGINE_WEBGPU + if (currentGraphicsBackend() != "webgpu") + throw Exception("Gpgpu.newShader: requires webgpu Graphics backend"); + // Source is WGSL on the WebGPU backend (browsers cannot compile GLSL). + return webgpuNewShaderFromWgsl(source); +#else if (currentGraphicsBackend() != "vulkan") throw Exception("Gpgpu.newShader: requires vulkan Graphics backend"); return vulkanNewShaderFromSpirv(compileComputeGlsl(source)); +#endif } ComputeShader *Gpgpu::newShaderFromBytecode(const std::string &path) { +#ifdef EVENGINE_WEBGPU + if (currentGraphicsBackend() != "webgpu") + throw Exception("Gpgpu.newShaderFromBytecode: requires webgpu Graphics backend"); + // Load WGSL text from the given path. + auto *fs = eve::filesystem::Filesystem::create(); + if (!fs) throw Exception("Gpgpu.newShaderFromBytecode: no filesystem"); + std::unique_ptr file(fs->read(path)); + if (!file) throw Exception("Gpgpu.newShaderFromBytecode: cannot open '%s'", path.c_str()); + std::string src(reinterpret_cast(file->getData()), file->getSize()); + return webgpuNewShaderFromWgsl(src); +#else if (currentGraphicsBackend() != "vulkan") throw Exception("Gpgpu.newShaderFromBytecode: requires vulkan Graphics backend"); return vulkanNewShaderFromSpirv(loadSpirvFile(path)); +#endif } ComputeShader *Gpgpu::newShaderFromSpvFile(const std::string &path) { +#ifdef EVENGINE_WEBGPU + if (currentGraphicsBackend() != "webgpu") + throw Exception("Gpgpu.newShaderFromSpvFile: requires webgpu Graphics backend"); + throw Exception("Gpgpu.newShaderFromSpvFile: SPIR-V is only supported on vulkan; " + "use newShaderFromBytecode with a .wgsl file on the WebGPU backend"); +#else if (currentGraphicsBackend() != "vulkan") throw Exception("Gpgpu.newShaderFromSpvFile: SPIR-V is only supported on vulkan"); return newShaderFromBytecode(path); +#endif } GpuBuffer *Gpgpu::newBuffer(int byteSize, const std::string &usage) { +#ifdef EVENGINE_WEBGPU + if (currentGraphicsBackend() != "webgpu") + throw Exception("Gpgpu.newBuffer: requires webgpu Graphics backend"); + return webgpuNewBuffer(byteSize, usage); +#else if (currentGraphicsBackend() != "vulkan") throw Exception("Gpgpu.newBuffer: requires vulkan Graphics backend"); return vulkanNewBuffer(byteSize, usage); +#endif } void Gpgpu::dispatch(ComputeShader *shader, int groupsX, int groupsY, int groupsZ) { if (!shader) return; +#ifdef EVENGINE_WEBGPU + if (currentGraphicsBackend() != "webgpu") + throw Exception("Gpgpu.dispatch: requires webgpu Graphics backend"); + webgpuDispatch(shader, groupsX, groupsY, groupsZ); +#else if (currentGraphicsBackend() != "vulkan") throw Exception("Gpgpu.dispatch: requires vulkan Graphics backend"); vulkanDispatch(shader, groupsX, groupsY, groupsZ); +#endif } void Gpgpu::expose(ssq::Table &table) { diff --git a/src/modules/gpgpu/webgpu/WebGpuGpgpu.cpp b/src/modules/gpgpu/webgpu/WebGpuGpgpu.cpp new file mode 100644 index 00000000..4400d3bf --- /dev/null +++ b/src/modules/gpgpu/webgpu/WebGpuGpgpu.cpp @@ -0,0 +1,316 @@ +#include "gpgpu/webgpu/WebGpuGpgpu.h" + +#include "common/Exception.h" +#include "common/Module.h" +#include "data/ByteData.h" +#include "graphics/Graphics.h" +#include "graphics/webgpu/Graphics.h" + +#if defined(__EMSCRIPTEN__) +#include +#endif + +#include +#include + +namespace eve::gpgpu { + +namespace { + +graphics::webgpu::Graphics *requireWebGpuGraphics() { + auto *gfx = eve::ModuleManager::getInstance("Graphics"); + if (!gfx) gfx = eve::graphics::Graphics::create(); + auto *wgg = dynamic_cast(gfx); + if (!wgg) throw Exception("Gpgpu: requires WebGPU Graphics backend"); + if (!wgg->getDevice()) + throw Exception("Gpgpu: Graphics device not initialized (create a window first)"); + return wgg; +} + +wgpu::Device &gpuDevice() { return requireWebGpuGraphics()->getDevice(); } +wgpu::Queue &gpuQueue() { return requireWebGpuGraphics()->getQueue(); } +wgpu::Instance &gpuInstance() { return requireWebGpuGraphics()->getInstance(); } + +/** Build a WGPUStringView from a C string (null-safe, length auto-computed). */ +WGPUStringView sv(const char *s) { return WGPUStringView{s, s ? std::strlen(s) : 0}; } + +} // namespace + +bool webgpuGpgpuReady() { + try { + auto *gfx = eve::ModuleManager::getInstance("Graphics"); + if (!gfx) return false; + auto *wgg = dynamic_cast(gfx); + if (!wgg) return false; + return bool(wgg->getDevice()); + } catch (...) { + return false; + } +} + +WebGpuComputeShader::~WebGpuComputeShader() = default; + +void WebGpuComputeShader::bindBuffer(int binding, GpuBuffer *buffer) { + if (binding < 0 || binding >= kMaxBindings) return; + bindings_[size_t(binding)] = buffer; +} + +GpuBuffer *WebGpuComputeShader::getBoundBuffer(int binding) const { + if (binding < 0 || binding >= kMaxBindings) return nullptr; + return bindings_[size_t(binding)]; +} + +void WebGpuComputeShader::setFloat(int index, float value) { + if (index < 0 || index >= kMaxFloats) return; + push_[size_t(index)] = value; +} + +float WebGpuComputeShader::getFloat(int index) const { + if (index < 0 || index >= kMaxFloats) return 0.f; + return push_[size_t(index)]; +} + +void WebGpuComputeShader::clearBindings() { + bindings_.fill(nullptr); +} + +WebGpuComputeShader *webgpuNewShaderFromWgsl(const std::string &wgsl) { + auto &device = gpuDevice(); + auto *shader = new WebGpuComputeShader(); + + // Bind group layout: 8 storage buffers (bindings 0..7) + push UBO (binding 8). + WGPUBindGroupLayoutEntry entries[ComputeShader::kMaxBindings + 1]{}; + for (int i = 0; i < ComputeShader::kMaxBindings; ++i) { + entries[size_t(i)].binding = uint32_t(i); + entries[size_t(i)].visibility = WGPUShaderStage_Compute; + entries[size_t(i)].buffer.type = WGPUBufferBindingType_Storage; + entries[size_t(i)].buffer.hasDynamicOffset = false; + entries[size_t(i)].buffer.minBindingSize = 0; + } + entries[ComputeShader::kMaxBindings].binding = uint32_t(ComputeShader::kMaxBindings); + entries[ComputeShader::kMaxBindings].visibility = WGPUShaderStage_Compute; + entries[ComputeShader::kMaxBindings].buffer.type = WGPUBufferBindingType_Uniform; + entries[ComputeShader::kMaxBindings].buffer.hasDynamicOffset = true; + entries[ComputeShader::kMaxBindings].buffer.minBindingSize = ComputeShader::kPushConstantBytes; + + WGPUBindGroupLayoutDescriptor bglDesc{}; + bglDesc.label = sv("eve_compute"); + bglDesc.entryCount = ComputeShader::kMaxBindings + 1; + bglDesc.entries = entries; + shader->setLayout = device.CreateBindGroupLayout(reinterpret_cast(&bglDesc)); + + WGPUBindGroupLayout bgl = shader->setLayout.Get(); + WGPUPipelineLayoutDescriptor plDesc{}; + plDesc.label = sv("eve_compute_layout"); + plDesc.bindGroupLayoutCount = 1; + plDesc.bindGroupLayouts = &bgl; + shader->pipelineLayout = device.CreatePipelineLayout(reinterpret_cast(&plDesc)); + + WGPUShaderSourceWGSL wd{}; + wd.chain.sType = WGPUSType_ShaderSourceWGSL; + wd.code = sv(wgsl.c_str()); + WGPUShaderModuleDescriptor md{}; + md.label = sv("eve_compute_module"); + md.nextInChain = &wd.chain; + wgpu::ShaderModule module = device.CreateShaderModule(reinterpret_cast(&md)); + + WGPUComputePipelineDescriptor pd{}; + pd.label = sv("eve_compute_pipeline"); + pd.layout = shader->pipelineLayout.Get(); + pd.compute.module = module.Get(); + pd.compute.entryPoint = sv("main"); + shader->pipeline = device.CreateComputePipeline(reinterpret_cast(&pd)); + + // Push-constant UBO (128 bytes, 256-aligned for dynamic offsets). + WGPUBufferDescriptor bd{}; + bd.label = sv("eve_compute_push"); + bd.size = ComputeShader::kPushConstantBytes; + bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform; + bd.mappedAtCreation = false; + shader->pushUbo = device.CreateBuffer(reinterpret_cast(&bd)); + shader->ready = true; + return shader; +} + +WebGpuComputeShader *webgpuNewShaderFromSpirv(const std::vector &spv) { + (void)spv; + throw Exception("Gpgpu.newShaderFromSpirv: SPIR-V compute shaders are not supported on the " + "WebGPU backend; use WGSL source instead."); +} + +WebGpuGpuBuffer *webgpuNewBuffer(int byteSize, const std::string &usage) { + if (byteSize <= 0) throw Exception("Gpgpu.newBuffer: byteSize must be > 0"); + auto &device = gpuDevice(); + bool staging = usage == "staging"; + WGPUBufferUsage bufUsage = staging ? (WGPUBufferUsage_CopySrc | WGPUBufferUsage_MapRead) + : (WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst | + WGPUBufferUsage_CopySrc | WGPUBufferUsage_Vertex); + WGPUBufferDescriptor bd{}; + bd.label = sv("eve_compute_buffer"); + bd.size = uint64_t(byteSize); + bd.usage = bufUsage; + bd.mappedAtCreation = false; + auto *b = new WebGpuGpuBuffer(); + b->buffer = device.CreateBuffer(reinterpret_cast(&bd)); + b->size_ = uint64_t(byteSize); + b->usage_ = usage; + return b; +} + +namespace { + +bool readBufferToCpu(wgpu::Buffer src, uint64_t srcOffset, void *dst, uint64_t nbytes) { + auto &device = gpuDevice(); + auto &queue = gpuQueue(); + if (!src || nbytes == 0) return false; + + WGPUBufferDescriptor bd{}; + bd.label = sv("eve_compute_readback"); + bd.size = nbytes; + bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead; + bd.mappedAtCreation = false; + wgpu::Buffer staging = device.CreateBuffer(reinterpret_cast(&bd)); + + wgpu::CommandEncoder enc = device.CreateCommandEncoder(); + enc.CopyBufferToBuffer(src, srcOffset, staging, 0, nbytes); + wgpu::CommandBuffer cmd = enc.Finish(); + queue.Submit(1, &cmd); + + bool mapped = false; + WGPUBufferMapCallbackInfo cbInfo{}; + cbInfo.mode = WGPUCallbackMode_AllowProcessEvents; + cbInfo.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*message*/, void *userdata1, + void * /*userdata2*/) { + bool *ok = static_cast(userdata1); + *ok = (status == WGPUMapAsyncStatus_Success); + }; + cbInfo.userdata1 = &mapped; + wgpuBufferMapAsync(staging.Get(), WGPUMapMode_Read, 0, nbytes, cbInfo); + + int guard = 0; + while (!mapped && guard < 2000) { +#if defined(__EMSCRIPTEN__) + emscripten_sleep(0); +#endif + wgpuInstanceProcessEvents(gpuInstance().Get()); + ++guard; + } + if (!mapped) return false; + const uint8_t *data = static_cast(staging.GetConstMappedRange(0, nbytes)); + if (!data) return false; + std::memcpy(dst, data, size_t(nbytes)); + staging.Unmap(); + return true; +} + +} // namespace + +void webgpuDispatch(ComputeShader *shader, int groupsX, int groupsY, int groupsZ) { + auto *ws = dynamic_cast(shader); + if (!ws || !ws->ready || !ws->pipeline) return; + if (groupsX <= 0) groupsX = 1; + if (groupsY <= 0) groupsY = 1; + if (groupsZ <= 0) groupsZ = 1; + + auto &device = gpuDevice(); + auto &queue = gpuQueue(); + + // Push constants -> uniform buffer (dynamic offset 0). + queue.WriteBuffer(ws->pushUbo, 0, shader->pushConstantData(), ComputeShader::kPushConstantBytes); + + // Build a bind group for the current storage-buffer bindings. + WGPUBindGroupEntry entries[ComputeShader::kMaxBindings + 1]{}; + WebGpuGpuBuffer *dummy = webgpuNewBuffer(4, "storage"); + for (int i = 0; i < ComputeShader::kMaxBindings; ++i) { + auto *vb = dynamic_cast(ws->getBoundBuffer(i)); + entries[size_t(i)].binding = uint32_t(i); + entries[size_t(i)].buffer = (vb && vb->buffer) ? vb->buffer.Get() : dummy->buffer.Get(); + entries[size_t(i)].size = (vb && vb->buffer) ? vb->size_ : 4; + } + entries[ComputeShader::kMaxBindings].binding = uint32_t(ComputeShader::kMaxBindings); + entries[ComputeShader::kMaxBindings].buffer = ws->pushUbo.Get(); + entries[ComputeShader::kMaxBindings].size = ComputeShader::kPushConstantBytes; + WGPUBindGroupDescriptor bgDesc{}; + bgDesc.layout = ws->setLayout.Get(); + bgDesc.entryCount = ComputeShader::kMaxBindings + 1; + bgDesc.entries = entries; + wgpu::BindGroup bg = device.CreateBindGroup(reinterpret_cast(&bgDesc)); + delete dummy; + + wgpu::CommandEncoder enc = device.CreateCommandEncoder(); + WGPUComputePassDescriptor cpDesc{}; + wgpu::ComputePassEncoder pass = enc.BeginComputePass(reinterpret_cast(&cpDesc)); + pass.SetPipeline(ws->pipeline); + uint32_t offsets[1] = {0}; + pass.SetBindGroup(0, bg, 1, offsets); + pass.DispatchWorkgroups(uint32_t(groupsX), uint32_t(groupsY), uint32_t(groupsZ)); + pass.End(); + wgpu::CommandBuffer cmd = enc.Finish(); + queue.Submit(1, &cmd); +} + +// --------------------------------------------------------------------------- +// WebGpuGpuBuffer +// --------------------------------------------------------------------------- + +WebGpuGpuBuffer::~WebGpuGpuBuffer() = default; + +void WebGpuGpuBuffer::uploadBytes(const void *src, uint64_t nbytes, uint64_t dstOffset) { + if (!buffer || !src || nbytes == 0) return; + if (dstOffset + nbytes > size_) + throw Exception("GpuBuffer.write: out of range"); + gpuQueue().WriteBuffer(buffer, dstOffset, src, nbytes); +} + +void WebGpuGpuBuffer::downloadBytes(void *dst, uint64_t nbytes, uint64_t srcOffset) const { + if (!buffer || !dst || nbytes == 0) return; + if (srcOffset + nbytes > size_) + throw Exception("GpuBuffer.read: out of range"); + readBufferToCpu(buffer, srcOffset, dst, nbytes); +} + +void WebGpuGpuBuffer::writeData(data::ByteData *data, int dstOffset) { + if (!data) return; + uploadBytes(data->getData(), data->getSize(), uint64_t(dstOffset < 0 ? 0 : dstOffset)); +} + +data::ByteData *WebGpuGpuBuffer::readData(int srcOffset, int size) { + const int off = srcOffset < 0 ? 0 : srcOffset; + int nbytes = size; + if (nbytes < 0) nbytes = int(size_) - off; + if (nbytes <= 0) return new data::ByteData(size_t(0)); + auto *out = new data::ByteData(size_t(nbytes)); + downloadBytes(out->getData(), uint64_t(nbytes), uint64_t(off)); + return out; +} + +void WebGpuGpuBuffer::writeFloat32(int floatIndex, float value) { + if (floatIndex < 0) return; + uploadBytes(&value, sizeof(float), uint64_t(floatIndex) * sizeof(float)); +} + +float WebGpuGpuBuffer::readFloat32(int floatIndex) { + if (floatIndex < 0) return 0.f; + float v = 0.f; + downloadBytes(&v, sizeof(float), uint64_t(floatIndex) * sizeof(float)); + return v; +} + +void WebGpuGpuBuffer::writeFloat32s(const float *data, int count, int startIndex) { + if (!data || count <= 0 || startIndex < 0) return; + uploadBytes(data, uint64_t(count) * sizeof(float), uint64_t(startIndex) * sizeof(float)); +} + +void WebGpuGpuBuffer::readFloat32s(float *out, int count, int startIndex) const { + if (!out || count <= 0 || startIndex < 0) return; + downloadBytes(out, uint64_t(count) * sizeof(float), uint64_t(startIndex) * sizeof(float)); +} + +void WebGpuGpuBuffer::fillFloat32(float value) { + if (size_ < sizeof(float)) return; + const size_t count = size_t(size_ / sizeof(float)); + std::vector tmp(count, value); + uploadBytes(tmp.data(), count * sizeof(float), 0); +} + +} // namespace eve::gpgpu diff --git a/src/modules/gpgpu/webgpu/WebGpuGpgpu.h b/src/modules/gpgpu/webgpu/WebGpuGpgpu.h new file mode 100644 index 00000000..e38b632a --- /dev/null +++ b/src/modules/gpgpu/webgpu/WebGpuGpgpu.h @@ -0,0 +1,77 @@ +#pragma once + +#include "gpgpu/ComputeShader.h" +#include "gpgpu/GpuBuffer.h" + +#include + +#include +#include +#include + +namespace eve::gpgpu { + +class WebGpuGpuBuffer; + +/** + * Compute program for the WebGPU backend. Accepts WGSL source; GLSL/SPIR-V + * input is rejected (browsers only accept WGSL at runtime). + */ +class WebGpuComputeShader final : public ComputeShader { +public: + WebGpuComputeShader() = default; + ~WebGpuComputeShader() override; + + void bindBuffer(int binding, GpuBuffer *buffer) override; + GpuBuffer *getBoundBuffer(int binding) const override; + + void setFloat(int index, float value) override; + float getFloat(int index) const override; + + void clearBindings() override; + + wgpu::ComputePipeline pipeline; + wgpu::PipelineLayout pipelineLayout; + wgpu::BindGroupLayout setLayout; + wgpu::Buffer pushUbo; + bool ready = false; + +private: + std::array bindings_{}; +}; + +/** Storage / staging buffer for the WebGPU backend. */ +class WebGpuGpuBuffer final : public GpuBuffer { +public: + WebGpuGpuBuffer() = default; + ~WebGpuGpuBuffer() override; + + int getSize() const override { return int(size_); } + std::string getUsage() const override { return usage_; } + + void writeData(data::ByteData *data, int dstOffset = 0) override; + data::ByteData *readData(int srcOffset = 0, int size = -1) override; + + void writeFloat32(int floatIndex, float value) override; + float readFloat32(int floatIndex) override; + void fillFloat32(float value) override; + + void writeFloat32s(const float *data, int count, int startIndex = 0) override; + void readFloat32s(float *out, int count, int startIndex = 0) const override; + + void uploadBytes(const void *src, uint64_t nbytes, uint64_t dstOffset = 0) override; + void downloadBytes(void *dst, uint64_t nbytes, uint64_t srcOffset = 0) const override; + + wgpu::Buffer buffer; + uint64_t size_ = 0; + std::string usage_; +}; + +// Backend entry points (mirror gpgpu/vulkan/VulkanGpgpu.h). +bool webgpuGpgpuReady(); +WebGpuComputeShader *webgpuNewShaderFromWgsl(const std::string &wgsl); +WebGpuComputeShader *webgpuNewShaderFromSpirv(const std::vector &spv); +WebGpuGpuBuffer *webgpuNewBuffer(int byteSize, const std::string &usage); +void webgpuDispatch(ComputeShader *shader, int groupsX, int groupsY, int groupsZ); + +} // namespace eve::gpgpu diff --git a/src/modules/graphics/Graphics.cpp b/src/modules/graphics/Graphics.cpp index a41b2553..bc674781 100644 --- a/src/modules/graphics/Graphics.cpp +++ b/src/modules/graphics/Graphics.cpp @@ -1,6 +1,11 @@ #include "graphics/Graphics.h" #include "graphics/HairShader.h" +#include "common/config.h" +#ifdef EVENGINE_WEBGPU +#include "graphics/webgpu/Graphics.h" +#else #include "graphics/vulkan/Graphics.h" +#endif #include "graphics/RenderSystem3D.h" #include "graphics/RenderSystem.h" #include "graphics/Light.h" @@ -14,7 +19,9 @@ #include "graphics/GlobalIllumination.h" #include "graphics/Material.h" #include "graphics/RenderControl.h" +#ifndef EVENGINE_WEBGPU #include "font/FontData.h" +#endif #include "image/ImageData.h" #include "common/Exception.h" #include "common/RenderTrace.h" @@ -27,7 +34,11 @@ namespace eve::graphics { +#ifdef EVENGINE_WEBGPU +Module_IMPL(Graphics, new eve::graphics::webgpu::Graphics()); +#else Module_IMPL(Graphics, new vulkan::Graphics()); +#endif void Graphics::render3D() { eve::debug::rtFrameBegin(); @@ -578,6 +589,7 @@ void Graphics::setTextureSamplerParams(Texture *texture, const std::string &filt Quad *Graphics::newQuad(int x, int y, int w, int h) { return new Quad(x, y, w, h); } +#ifndef EVENGINE_WEBGPU Font *Graphics::newFont(font::FontData *data, std::string charset) { return new Font(this, data, std::move(charset)); } @@ -619,5 +631,6 @@ void Graphics::print(const std::string &text, float x, float y, const Color &col prevCodepoint = code; } } +#endif // !EVENGINE_WEBGPU } // namespace eve::graphics diff --git a/src/modules/graphics/webgpu/Canvas.cpp b/src/modules/graphics/webgpu/Canvas.cpp new file mode 100644 index 00000000..2f4e08c8 --- /dev/null +++ b/src/modules/graphics/webgpu/Canvas.cpp @@ -0,0 +1,82 @@ +#include "graphics/webgpu/Canvas.h" +#include "graphics/webgpu/Graphics.h" + +#include "common/Exception.h" +#include "image/ImageData.h" + +#include + +namespace eve::graphics::webgpu { + +namespace { +/** Build a WGPUStringView from a C string (null-safe). */ +WGPUStringView sv(const char *s) { return WGPUStringView{s, s ? std::strlen(s) : 0}; } +} // namespace + +OffscreenCanvas::OffscreenCanvas(Graphics *gfx, int width, int height) + : gfx(gfx), width(width), height(height) { + if (!gfx || !gfx->getDevice() || width <= 0 || height <= 0) + throw Exception("OffscreenCanvas: invalid size or uninitialized device"); + + auto &device = gfx->getDevice(); + WGPUTextureDescriptor cd{}; + cd.label = sv("eve_canvas_color"); + cd.dimension = WGPUTextureDimension_2D; + cd.size = {static_cast(width), static_cast(height), 1}; + cd.sampleCount = 1; + cd.format = WGPUTextureFormat_RGBA8Unorm; + cd.mipLevelCount = 1; + cd.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment | + WGPUTextureUsage_CopySrc; + color = device.CreateTexture(reinterpret_cast(&cd)); + colorView = color.CreateView(); + + WGPUTextureDescriptor dd{}; + dd.label = sv("eve_canvas_depth"); + dd.dimension = WGPUTextureDimension_2D; + dd.size = {static_cast(width), static_cast(height), 1}; + dd.sampleCount = 1; + dd.format = WGPUTextureFormat_Depth32Float; + dd.mipLevelCount = 1; + dd.usage = WGPUTextureUsage_RenderAttachment; + depth = device.CreateTexture(reinterpret_cast(&dd)); + depthView = depth.CreateView(); + + colorGpu.texture = color; + colorGpu.view = colorView; + colorGpu.width = width; + colorGpu.height = height; + colorGpu.samplerState = TextureSampler::linearMipmap(); + WGPUSamplerDescriptor sd{}; + sd.label = sv("eve_canvas_sampler"); + sd.addressModeU = WGPUAddressMode_ClampToEdge; + sd.addressModeV = WGPUAddressMode_ClampToEdge; + sd.addressModeW = WGPUAddressMode_ClampToEdge; + sd.magFilter = WGPUFilterMode_Linear; + sd.minFilter = WGPUFilterMode_Linear; + sd.mipmapFilter = WGPUMipmapFilterMode_Nearest; + colorGpu.sampler = device.CreateSampler(reinterpret_cast(&sd)); + + colorTex.gpuHandle = &colorGpu; + colorTex.width = width; + colorTex.height = height; + colorTex.mipmapCount = 1; +} + +OffscreenCanvas::~OffscreenCanvas() = default; + +void OffscreenCanvas::clear(std::optional color, std::optional /*stencil*/, + std::optional /*depth*/) { + clearRequested = true; + clearColor = color ? *color : Color(0.f, 0.f, 0.f, 1.f); +} + +Color OffscreenCanvas::getPixel(int x, int y) { + return gfx->getPixelImpl(this, x, y); +} + +image::ImageData *OffscreenCanvas::newImageData() { + return gfx->newImageDataImpl(this); +} + +} // namespace eve::graphics::webgpu diff --git a/src/modules/graphics/webgpu/Canvas.h b/src/modules/graphics/webgpu/Canvas.h new file mode 100644 index 00000000..5f110c2c --- /dev/null +++ b/src/modules/graphics/webgpu/Canvas.h @@ -0,0 +1,50 @@ +#pragma once +#include "graphics/Canvas.h" +#include "graphics/Texture.h" +#include "graphics/webgpu/Graphics.h" + +#include + +#include + +namespace eve::graphics::webgpu { + +/** + * Offscreen render target (RGBA8Unorm color, optional Depth32Float). + * 2D batches are flushed into the canvas texture by Graphics::flush2DToCanvas. + */ +class OffscreenCanvas final : public eve::graphics::Canvas { +public: + OffscreenCanvas(Graphics *gfx, int width, int height); + ~OffscreenCanvas() override; + + int getWidth() const override { return width; } + int getHeight() const override { return height; } + Texture *getTexture() override { return &colorTex; } + + void clear(std::optional color, std::optional stencil, + std::optional depth) override; + Color getPixel(int x, int y) override; + image::ImageData *newImageData() override; + + void draw(Canvas *, const glm::mat4 &) const override {} + void draw(eve::graphics::Graphics *, const glm::mat4 &) const override {} + + bool clearRequested = false; + Color clearColor{0.f, 0.f, 0.f, 1.f}; + + friend class Graphics; + +private: + Graphics *gfx; + int width = 0; + int height = 0; + wgpu::Texture color; + wgpu::TextureView colorView; + wgpu::Texture depth; + wgpu::TextureView depthView; + GpuTexture colorGpu; + Texture colorTex; +}; + +} // namespace eve::graphics::webgpu diff --git a/src/modules/graphics/webgpu/Graphics.cpp b/src/modules/graphics/webgpu/Graphics.cpp new file mode 100644 index 00000000..5723d956 --- /dev/null +++ b/src/modules/graphics/webgpu/Graphics.cpp @@ -0,0 +1,3024 @@ +#include "graphics/webgpu/Graphics.h" +#include "graphics/webgpu/wgsl_shaders.h" +#include "graphics/webgpu/Canvas.h" +#include "graphics/Batcher.h" +#include "graphics/Texture.h" +#include "graphics/Mesh.h" +#include "graphics/Shader.h" +#include "graphics/Light.h" +#include "graphics/ClusteredLight.h" +#include "graphics/Shadow.h" + +#include "common/Exception.h" +#include "common/config.h" +#include "filesystem/Filesystem.h" +#include "image/Image.h" +#include "image/ImageData.h" + +#include + +#include +#include +#include +#include + +#include + +#include +#include + +#if defined(_WIN32) +#include +#endif + +#if defined(__EMSCRIPTEN__) +#include +#endif + +namespace eve::graphics::webgpu { + +namespace { + +/** Build a WGPUStringView from a C string (null-safe, length auto-computed). */ +WGPUStringView sv(const char *s) { + return WGPUStringView{s, s ? std::strlen(s) : 0}; +} + +// A shared default sampler used when no texture is provided. +wgpu::Sampler createLinearSampler(wgpu::Device &dev) { + WGPUSamplerDescriptor d{}; + d.label = sv("eve_linear"); + d.addressModeU = WGPUAddressMode_ClampToEdge; + d.addressModeV = WGPUAddressMode_ClampToEdge; + d.addressModeW = WGPUAddressMode_ClampToEdge; + d.magFilter = WGPUFilterMode_Linear; + d.minFilter = WGPUFilterMode_Linear; + d.mipmapFilter = WGPUMipmapFilterMode_Linear; + d.lodMinClamp = 0.f; + d.lodMaxClamp = 1000.f; + d.maxAnisotropy = 1; + return dev.CreateSampler(reinterpret_cast(&d)); +} + +int nextPOT(int v) { + int p = 1; + while (p < v) p <<= 1; + return p; +} + +} // namespace + +Graphics::Graphics() { + for (uint32_t i = 0; i < kFramesInFlight; ++i) { + uboArenas.emplace_back(); + vertexArenas.emplace_back(); + } +} + +Graphics::~Graphics() = default; + +// --------------------------------------------------------------------------- +// Init +// --------------------------------------------------------------------------- + +void Graphics::initWithWindow(void *nativeWindow) { + sdlWindow = nativeWindow; + if (deviceInitDone) return; + + createInstanceAndAdapter(); + requestDevice(); + if (!device) throw Exception("WebGPU: device request failed"); + + queue = device.GetQueue(); + + // ---- Surface ---- + WGPUSurfaceDescriptor surfDesc{}; + surfDesc.label = sv("eve_surface"); +#if defined(__EMSCRIPTEN__) + WGPUEmscriptenSurfaceSourceCanvasHTMLSelector canvasSel{}; + canvasSel.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector; + canvasSel.selector = sv("#canvas"); + surfDesc.nextInChain = &canvasSel.chain; + surface = instance.CreateSurface(reinterpret_cast(&surfDesc)); + surfaceFormat = WGPUTextureFormat_BGRA8Unorm; +#else + SDL_SysWMinfo wminfo; + SDL_VERSION(&wminfo.version); + if (!SDL_GetWindowWMInfo(static_cast(sdlWindow), &wminfo)) + throw Exception("WebGPU: SDL_GetWindowWMInfo failed: %s", SDL_GetError()); +#if defined(_WIN32) + WGPUSurfaceSourceWindowsHWND winChain{}; + winChain.chain.sType = WGPUSType_SurfaceSourceWindowsHWND; + winChain.hwnd = wminfo.info.win.window; + winChain.hinstance = GetModuleHandle(nullptr); + surfDesc.nextInChain = &winChain.chain; + surface = instance.CreateSurface(reinterpret_cast(&surfDesc)); +#elif defined(__linux__) + if (wminfo.subsystem == SDL_SYSWM_X11) { + WGPUSurfaceSourceXlibWindow x11Chain{}; + x11Chain.chain.sType = WGPUSType_SurfaceSourceXlibWindow; + x11Chain.display = wminfo.info.x11.display; + x11Chain.window = wminfo.info.x11.window; + surfDesc.nextInChain = &x11Chain.chain; + surface = instance.CreateSurface(reinterpret_cast(&surfDesc)); + } else if (wminfo.subsystem == SDL_SYSWM_WAYLAND) { + WGPUSurfaceSourceWaylandSurface wlChain{}; + wlChain.chain.sType = WGPUSType_SurfaceSourceWaylandSurface; + wlChain.display = wminfo.info.wl.display; + wlChain.surface = wminfo.info.wl.surface; + surfDesc.nextInChain = &wlChain.chain; + surface = instance.CreateSurface(reinterpret_cast(&surfDesc)); + } else { + throw Exception("WebGPU: unsupported SDL window subsystem on Linux"); + } +#elif defined(__APPLE__) + throw Exception("WebGPU: native macOS surface (Metal layer) not yet supported; " + "use the Emscripten browser build or the Vulkan backend on macOS"); +#else + throw Exception("WebGPU: unsupported native platform for surface creation"); +#endif + if (!surface) throw Exception("WebGPU: surface creation failed"); + // Preferred surface format comes from wgpuSurfaceGetCapabilities in the + // current ABI (Surface::GetPreferredFormat was removed). + WGPUSurfaceCapabilities caps{}; + if (wgpuSurfaceGetCapabilities(surface.Get(), adapter.Get(), &caps) == WGPUStatus_Success && + caps.formatCount > 0) { + surfaceFormat = caps.formats[0]; + } + wgpuSurfaceCapabilitiesFreeMembers(&caps); +#endif + + swapchainConfigured = false; + // Bind group layouts / pipelines must exist before the default textures + // (texture bind groups reference the 2D / mesh3d layouts), and the shadow + // depth array must exist before mesh bind groups are built at flush time. + createPipelineResources(); + createShadowResources(); + createDefaultTextures(); + initialized = true; +} + +void Graphics::createInstanceAndAdapter() { + instance = wgpu::CreateInstance(); + if (!instance) throw Exception("WebGPU: wgpuCreateInstance failed"); + + WGPURequestAdapterOptions opts{}; + opts.compatibleSurface = surface.Get(); + opts.powerPreference = WGPUPowerPreference_HighPerformance; + + adapterReceived = false; + WGPURequestAdapterCallbackInfo cbInfo{}; + cbInfo.nextInChain = nullptr; + cbInfo.mode = WGPUCallbackMode_AllowProcessEvents; + cbInfo.callback = [](WGPURequestAdapterStatus status, WGPUAdapter a, WGPUStringView msg, + void *userdata1, void * /*userdata2*/) { + auto *self = static_cast(userdata1); + if (status == WGPURequestAdapterStatus_Success && a) { + self->adapter = wgpu::Adapter(a); + } else { + self->adapterError = + msg.data ? std::string(msg.data, msg.length) : "unknown adapter error"; + } + self->adapterReceived.store(true); + }; + cbInfo.userdata1 = this; + cbInfo.userdata2 = nullptr; + + wgpuInstanceRequestAdapter(instance.Get(), &opts, cbInfo); + waitForAdapter(); + if (!adapter) { + throw Exception("WebGPU: no adapter found (%s)", adapterError.c_str()); + } +} + +void Graphics::requestDevice() { + WGPUDeviceDescriptor devDesc{}; + devDesc.label = sv("eve_device"); + + deviceReceived = false; + WGPURequestDeviceCallbackInfo cbInfo{}; + cbInfo.nextInChain = nullptr; + cbInfo.mode = WGPUCallbackMode_AllowProcessEvents; + cbInfo.callback = [](WGPURequestDeviceStatus status, WGPUDevice d, WGPUStringView msg, + void *userdata1, void * /*userdata2*/) { + auto *self = static_cast(userdata1); + if (status == WGPURequestDeviceStatus_Success && d) { + self->device = wgpu::Device(d); + // Default device limits support 8+ uniform/vertex/storage buffers. + self->deviceReceived.store(true); + self->deviceError.clear(); + } else { + self->deviceError = + msg.data ? std::string(msg.data, msg.length) : "unknown device error"; + self->deviceReceived.store(true); + } + }; + cbInfo.userdata1 = this; + cbInfo.userdata2 = nullptr; + + wgpuAdapterRequestDevice(adapter.Get(), &devDesc, cbInfo); + waitForDevice(); + if (!device) { + throw Exception("WebGPU: device request failed (%s)", deviceError.c_str()); + } +} + +void Graphics::waitForAdapter() { + while (!adapterReceived.load()) { +#if defined(__EMSCRIPTEN__) + // Yield to the browser event loop so the JS requestAdapter promise can + // resolve and fire the callback, then flush it with ProcessEvents. + emscripten_sleep(0); +#endif + wgpuInstanceProcessEvents(instance.Get()); + } +} + +void Graphics::waitForDevice() { + while (!deviceReceived.load()) { +#if defined(__EMSCRIPTEN__) + emscripten_sleep(0); +#endif + wgpuInstanceProcessEvents(instance.Get()); + } +} + +void Graphics::setVSync(bool enabled) { + if (vsyncEnabled == enabled) return; + eve::graphics::Graphics::setVSync(enabled); + markSwapchainDirty(); +} + +void Graphics::setViewportSize(int width, int height, int pixelwidth, int pixelheight) { + logicalW = width; + logicalH = height; + pixelW = pixelwidth; + pixelH = pixelheight; + if (width <= 0 || height <= 0) return; + if (initialized) configureSurface(width, height); +} + +void Graphics::configureSurface(int width, int height) { + if (!surface || !device || width <= 0 || height <= 0) return; + WGPUSurfaceConfiguration cfg{}; + cfg.nextInChain = nullptr; + cfg.device = device.Get(); + cfg.width = static_cast(width); + cfg.height = static_cast(height); + cfg.format = surfaceFormat; + cfg.usage = WGPUTextureUsage_RenderAttachment; + cfg.viewFormatCount = 0; + cfg.viewFormats = nullptr; + // emdawnwebgpu only accepts Fifo / Undefined present modes. + cfg.presentMode = WGPUPresentMode_Fifo; + cfg.alphaMode = WGPUCompositeAlphaMode_Auto; + surface.Configure(reinterpret_cast(&cfg)); + swapchainConfigured = true; + // Recreate the offscreen targets at the new size. + int w = pixelW > 0 ? pixelW : width; + int h = pixelH > 0 ? pixelH : height; + if (w > 0 && h > 0) { + createSceneColorResources(w, h); + createShadowResources(); + } +} + +void Graphics::rebuildSwapchainIfNeeded() { + if (!swapchainConfigured && surface && logicalW > 0 && logicalH > 0) { + configureSurface(logicalW, logicalH); + } +} + +// --------------------------------------------------------------------------- +// Default / placeholder resources +// --------------------------------------------------------------------------- + +void Graphics::createDefaultTextures() { + uint8_t white[4] = {255, 255, 255, 255}; + whiteTexture = static_cast(newTexture(1, 1, white, false, false)->gpuHandle); + + // Flat normal (0.5,0.5,1) in RGBA8 → sampled as (0,0,1) normal. + uint8_t flatNrm[4] = {128, 128, 255, 255}; + flatNormalTexture = static_cast(newTexture(1, 1, flatNrm, false, false)->gpuHandle); + flatNormalTexture3D = flatNormalTexture; + + uint8_t flatH[4] = {0, 0, 0, 255}; + flatHeightTexture3D = static_cast(newTexture(1, 1, flatH, false, false)->gpuHandle); + + // 1x1 white cubemap. + uint8_t cubeFace[4] = {255, 255, 255, 255}; + uint8_t cubeData[24]; + for (int f = 0; f < 6; ++f) std::memcpy(cubeData + f * 4, cubeFace, 4); + defaultEnvCubemap = + static_cast(newCubemap(1, cubeData)->gpuHandle); + + // Shared filtering sampler for WGSL bindings declared as plain `sampler`. + TextureSampler def; + mainSampler = makeSampler(def, 1); +} + +// --------------------------------------------------------------------------- +// Pipeline resource creation +// --------------------------------------------------------------------------- + +void Graphics::createPipelineResources() { + create2DPipelines(); + createMesh3DPipelines(); + createShadowPipelines(); + createGbufferPipelines(); + createVoxelPipelines(); +} + +// --------------------------------------------------------------------------- +// Bind group layouts +// --------------------------------------------------------------------------- + +wgpu::BindGroupLayout Graphics::make2DBindGroupLayout() { + WGPUBindGroupLayoutEntry entries[5]{}; + // 0: color texture + entries[0].binding = 0; + entries[0].visibility = WGPUShaderStage_Fragment; + entries[0].texture.sampleType = WGPUTextureSampleType_Float; + entries[0].texture.viewDimension = WGPUTextureViewDimension_2D; + entries[0].texture.multisampled = false; + // 1: depth texture + entries[1].binding = 1; + entries[1].visibility = WGPUShaderStage_Fragment; + entries[1].texture.sampleType = WGPUTextureSampleType_Float; + entries[1].texture.viewDimension = WGPUTextureViewDimension_2D; + // 2: color sampler + entries[2].binding = 2; + entries[2].visibility = WGPUShaderStage_Fragment; + entries[2].sampler.type = WGPUSamplerBindingType_Filtering; + // 3: depth sampler + entries[3].binding = 3; + entries[3].visibility = WGPUShaderStage_Fragment; + entries[3].sampler.type = WGPUSamplerBindingType_Filtering; + // 4: Externals UBO (push-constant replacement, dynamic offset). Sized for + // the largest consumer: custom 2D shaders (128 B) and lit2D (Lighting2DUBO). + entries[4].binding = 4; + entries[4].visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment; + entries[4].buffer.type = WGPUBufferBindingType_Uniform; + entries[4].buffer.hasDynamicOffset = true; + entries[4].buffer.minBindingSize = + std::max(Shader::kPushConstantBytes, uint32_t(sizeof(Lighting2DUBO))); + + WGPUBindGroupLayoutDescriptor desc{}; + desc.label = sv("eve_2d"); + desc.entryCount = 5; + desc.entries = entries; + return device.CreateBindGroupLayout(reinterpret_cast(&desc)); +} + +wgpu::BindGroupLayout Graphics::makeMesh3DBindGroupLayout() { + WGPUBindGroupLayoutEntry entries[9]{}; + // 0: Frame UBO (dynamic) + entries[0].binding = 0; + entries[0].visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment; + entries[0].buffer.type = WGPUBufferBindingType_Uniform; + entries[0].buffer.hasDynamicOffset = true; + entries[0].buffer.minBindingSize = sizeof(Mesh3DUBO); + // 1: albedo + entries[1].binding = 1; + entries[1].visibility = WGPUShaderStage_Fragment; + entries[1].texture.sampleType = WGPUTextureSampleType_Float; + entries[1].texture.viewDimension = WGPUTextureViewDimension_2D; + // 2: normal + entries[2].binding = 2; + entries[2].visibility = WGPUShaderStage_Fragment; + entries[2].texture.sampleType = WGPUTextureSampleType_Float; + entries[2].texture.viewDimension = WGPUTextureViewDimension_2D; + // 3: env cubemap + entries[3].binding = 3; + entries[3].visibility = WGPUShaderStage_Fragment; + entries[3].texture.sampleType = WGPUTextureSampleType_Float; + entries[3].texture.viewDimension = WGPUTextureViewDimension_Cube; + // 4: Shadow UBO (dynamic) + entries[4].binding = 4; + entries[4].visibility = WGPUShaderStage_Fragment; + entries[4].buffer.type = WGPUBufferBindingType_Uniform; + entries[4].buffer.hasDynamicOffset = true; + entries[4].buffer.minBindingSize = sizeof(ShadowUBO); + // 5: shadow depth array + entries[5].binding = 5; + entries[5].visibility = WGPUShaderStage_Fragment; + entries[5].texture.sampleType = WGPUTextureSampleType_Depth; + entries[5].texture.viewDimension = WGPUTextureViewDimension_2DArray; + // 6: height + entries[6].binding = 6; + entries[6].visibility = WGPUShaderStage_Fragment; + entries[6].texture.sampleType = WGPUTextureSampleType_Float; + entries[6].texture.viewDimension = WGPUTextureViewDimension_2D; + // 7: shared filtering sampler (WGSL `mainSamp`) + entries[7].binding = 7; + entries[7].visibility = WGPUShaderStage_Fragment; + entries[7].sampler.type = WGPUSamplerBindingType_Filtering; + // 8: shadow comparison sampler + entries[8].binding = 8; + entries[8].visibility = WGPUShaderStage_Fragment; + entries[8].sampler.type = WGPUSamplerBindingType_Comparison; + + WGPUBindGroupLayoutDescriptor desc{}; + desc.label = sv("eve_mesh3d"); + desc.entryCount = 9; + desc.entries = entries; + return device.CreateBindGroupLayout(reinterpret_cast(&desc)); +} + +wgpu::BindGroupLayout Graphics::makeShadowBindGroupLayout() { + WGPUBindGroupLayoutEntry entries[1]{}; + entries[0].binding = 0; + entries[0].visibility = WGPUShaderStage_Vertex; + entries[0].buffer.type = WGPUBufferBindingType_Uniform; + entries[0].buffer.hasDynamicOffset = true; + entries[0].buffer.minBindingSize = 64; // mat4 mvp + + WGPUBindGroupLayoutDescriptor desc{}; + desc.label = sv("eve_shadow"); + desc.entryCount = 1; + desc.entries = entries; + return device.CreateBindGroupLayout(reinterpret_cast(&desc)); +} + +wgpu::BindGroupLayout Graphics::makeGbufferBindGroupLayout() { + WGPUBindGroupLayoutEntry entries[3]{}; + // 0: Push UBO (dynamic; 128 bytes) + entries[0].binding = 0; + entries[0].visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment; + entries[0].buffer.type = WGPUBufferBindingType_Uniform; + entries[0].buffer.hasDynamicOffset = true; + entries[0].buffer.minBindingSize = 128; + // 1: albedo + entries[1].binding = 1; + entries[1].visibility = WGPUShaderStage_Fragment; + entries[1].texture.sampleType = WGPUTextureSampleType_Float; + entries[1].texture.viewDimension = WGPUTextureViewDimension_2D; + // 2: sampler + entries[2].binding = 2; + entries[2].visibility = WGPUShaderStage_Fragment; + entries[2].sampler.type = WGPUSamplerBindingType_Filtering; + + WGPUBindGroupLayoutDescriptor desc{}; + desc.label = sv("eve_gbuffer"); + desc.entryCount = 3; + desc.entries = entries; + return device.CreateBindGroupLayout(reinterpret_cast(&desc)); +} + +wgpu::BindGroupLayout Graphics::makeVoxelBindGroupLayout() { + WGPUBindGroupLayoutEntry entries[3]{}; + // 0: PC UBO (dynamic) + entries[0].binding = 0; + entries[0].visibility = WGPUShaderStage_Vertex; + entries[0].buffer.type = WGPUBufferBindingType_Uniform; + entries[0].buffer.hasDynamicOffset = true; + entries[0].buffer.minBindingSize = 112; + // 1: atlas texture + entries[1].binding = 1; + entries[1].visibility = WGPUShaderStage_Fragment; + entries[1].texture.sampleType = WGPUTextureSampleType_Float; + entries[1].texture.viewDimension = WGPUTextureViewDimension_2D; + // 2: atlas sampler + entries[2].binding = 2; + entries[2].visibility = WGPUShaderStage_Fragment; + entries[2].sampler.type = WGPUSamplerBindingType_Filtering; + + WGPUBindGroupLayoutDescriptor desc{}; + desc.label = sv("eve_voxel"); + desc.entryCount = 3; + desc.entries = entries; + return device.CreateBindGroupLayout(reinterpret_cast(&desc)); +} + +wgpu::PipelineLayout Graphics::make2DPipelineLayout() { + WGPUBindGroupLayout bgl = tex2DSetLayout.Get(); + WGPUPipelineLayoutDescriptor d{}; + d.label = sv("eve_2d_layout"); + d.bindGroupLayoutCount = 1; + d.bindGroupLayouts = &bgl; + return device.CreatePipelineLayout(reinterpret_cast(&d)); +} + +wgpu::PipelineLayout Graphics::makeMesh3DPipelineLayout() { + WGPUBindGroupLayout bgl = mesh3dSetLayout.Get(); + WGPUPipelineLayoutDescriptor d{}; + d.label = sv("eve_mesh3d_layout"); + d.bindGroupLayoutCount = 1; + d.bindGroupLayouts = &bgl; + return device.CreatePipelineLayout(reinterpret_cast(&d)); +} + +wgpu::PipelineLayout Graphics::makeShadowPipelineLayout() { + WGPUBindGroupLayout bgl = shadowSetLayout.Get(); + WGPUPipelineLayoutDescriptor d{}; + d.label = sv("eve_shadow_layout"); + d.bindGroupLayoutCount = 1; + d.bindGroupLayouts = &bgl; + return device.CreatePipelineLayout(reinterpret_cast(&d)); +} + +wgpu::PipelineLayout Graphics::makeGbufferPipelineLayout() { + WGPUBindGroupLayout bgl = gbufferSetLayout.Get(); + WGPUPipelineLayoutDescriptor d{}; + d.label = sv("eve_gbuffer_layout"); + d.bindGroupLayoutCount = 1; + d.bindGroupLayouts = &bgl; + return device.CreatePipelineLayout(reinterpret_cast(&d)); +} + +wgpu::PipelineLayout Graphics::makeVoxelPipelineLayout() { + WGPUBindGroupLayout bgl = voxelSetLayout.Get(); + WGPUPipelineLayoutDescriptor d{}; + d.label = sv("eve_voxel_layout"); + d.bindGroupLayoutCount = 1; + d.bindGroupLayouts = &bgl; + return device.CreatePipelineLayout(reinterpret_cast(&d)); +} + +// --------------------------------------------------------------------------- +// Pipelines +// --------------------------------------------------------------------------- + +namespace { + +// Returns a RAII-held module: the caller must keep it alive across +// CreateRenderPipeline. Returning a raw WGPUShaderModule would let the +// temporary wgpu::ShaderModule destructor Release() the handle, dropping it +// from emdawnwebgpu's jsObjects before the pipeline can reference it. +wgpu::ShaderModule makeWgslModule(wgpu::Device &dev, const char *wgsl) { + WGPUShaderSourceWGSL wgslDesc{}; + wgslDesc.chain.sType = WGPUSType_ShaderSourceWGSL; + wgslDesc.code = sv(wgsl); + WGPUShaderModuleDescriptor desc{}; + desc.nextInChain = &wgslDesc.chain; + return dev.CreateShaderModule(reinterpret_cast(&desc)); +} + +// Builds a WGSL shader-module descriptor (the chained WGPUShaderSourceWGSL is +// static so its chain pointer stays valid). The returned descriptor is a value +// that must outlive the CreateShaderModule call. +WGPUShaderModuleDescriptor mdDesc(const std::string &code) { + static WGPUShaderSourceWGSL wd{}; + wd.chain.sType = WGPUSType_ShaderSourceWGSL; + wd.code = sv(code.c_str()); + WGPUShaderModuleDescriptor md{}; + md.nextInChain = &wd.chain; + return md; +} + +void fillVertexLayout(WGPUVertexBufferLayout &layout, uint64_t stride, + const WGPUVertexAttribute *attrs, uint32_t attrCount) { + layout.arrayStride = stride; + layout.stepMode = WGPUVertexStepMode_Vertex; + layout.attributeCount = attrCount; + layout.attributes = attrs; +} + +WGPUBlendState alphaBlend() { + WGPUBlendState b{}; + b.color.srcFactor = WGPUBlendFactor_SrcAlpha; + b.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha; + b.color.operation = WGPUBlendOperation_Add; + b.alpha.srcFactor = WGPUBlendFactor_One; + b.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha; + b.alpha.operation = WGPUBlendOperation_Add; + return b; +} + +WGPUBlendState noBlend() { + WGPUBlendState b{}; + b.color.srcFactor = WGPUBlendFactor_One; + b.color.dstFactor = WGPUBlendFactor_Zero; + b.color.operation = WGPUBlendOperation_Add; + b.alpha.srcFactor = WGPUBlendFactor_One; + b.alpha.dstFactor = WGPUBlendFactor_Zero; + b.alpha.operation = WGPUBlendOperation_Add; + return b; +} + +} // namespace + +namespace { + +wgpu::RenderPipeline make2DColorPipeline(wgpu::Device &dev, WGPUTextureFormat format, bool blend) { + WGPUVertexAttribute attrs[2] = {}; + attrs[0].format = WGPUVertexFormat_Float32x2; + attrs[0].offset = 0; + attrs[0].shaderLocation = 0; + attrs[1].format = WGPUVertexFormat_Float32x4; + attrs[1].offset = 8; + attrs[1].shaderLocation = 1; + WGPUVertexBufferLayout vb{}; + fillVertexLayout(vb, 24, attrs, 2); + + WGPUColorTargetState target{}; + target.format = format; + target.blend = nullptr; + WGPUBlendState bs = alphaBlend(); + if (blend) target.blend = &bs; + + // The solid-color shader declares no bindings, so it must use an empty + // pipeline layout. Reusing the textured 2D layout here would require a + // bind group for every solid draw (WebGPU validation fails the whole + // command buffer otherwise). + WGPUPipelineLayoutDescriptor pld{}; + pld.label = sv("eve_2d_color_layout"); + pld.bindGroupLayoutCount = 0; + pld.bindGroupLayouts = nullptr; + wgpu::PipelineLayout emptyLayout = dev.CreatePipelineLayout(reinterpret_cast(&pld)); + + WGPURenderPipelineDescriptor pd{}; + pd.label = sv("eve_color2d"); + pd.layout = emptyLayout.Get(); + wgpu::ShaderModule vertModule = makeWgslModule(dev, kColorVertWgsl); + wgpu::ShaderModule fragModule = makeWgslModule(dev, kColorFragWgsl); + pd.vertex.module = vertModule.Get(); + pd.vertex.entryPoint = sv("vs_main"); + pd.vertex.bufferCount = 1; + pd.vertex.buffers = &vb; + WGPUFragmentState fs{}; + fs.module = fragModule.Get(); + fs.entryPoint = sv("fs_main"); + fs.targetCount = 1; + fs.targets = ⌖ + pd.fragment = &fs; + pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; + pd.primitive.frontFace = WGPUFrontFace_CCW; + pd.primitive.cullMode = WGPUCullMode_None; + pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; + pd.depthStencil = nullptr; + pd.multisample.count = 1; + return dev.CreateRenderPipeline(reinterpret_cast(&pd)); +} + +wgpu::RenderPipeline make2DTexturedPipeline(wgpu::Device &dev, wgpu::PipelineLayout layout, + WGPUTextureFormat format, bool blend) { + WGPUVertexAttribute attrs[3] = {}; + attrs[0].format = WGPUVertexFormat_Float32x2; + attrs[0].offset = 0; + attrs[0].shaderLocation = 0; + attrs[1].format = WGPUVertexFormat_Float32x4; + attrs[1].offset = 8; + attrs[1].shaderLocation = 1; + attrs[2].format = WGPUVertexFormat_Float32x2; + attrs[2].offset = 24; + attrs[2].shaderLocation = 2; + WGPUVertexBufferLayout vb{}; + fillVertexLayout(vb, 32, attrs, 3); + + WGPUColorTargetState target{}; + target.format = format; + WGPUBlendState bs = alphaBlend(); + if (blend) target.blend = &bs; + + WGPURenderPipelineDescriptor pd{}; + pd.label = sv("eve_textured2d"); + pd.layout = layout.Get(); + wgpu::ShaderModule vertModule = makeWgslModule(dev, kTexturedVertWgsl); + wgpu::ShaderModule fragModule = makeWgslModule(dev, kTexturedFragWgsl); + pd.vertex.module = vertModule.Get(); + pd.vertex.entryPoint = sv("vs_main"); + pd.vertex.bufferCount = 1; + pd.vertex.buffers = &vb; + WGPUFragmentState fs{}; + fs.module = fragModule.Get(); + fs.entryPoint = sv("fs_main"); + fs.targetCount = 1; + fs.targets = ⌖ + pd.fragment = &fs; + pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; + pd.primitive.frontFace = WGPUFrontFace_CCW; + pd.primitive.cullMode = WGPUCullMode_None; + pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; + pd.depthStencil = nullptr; + pd.multisample.count = 1; + return dev.CreateRenderPipeline(reinterpret_cast(&pd)); +} + +wgpu::RenderPipeline make2DLitPipeline(wgpu::Device &dev, wgpu::PipelineLayout layout, + WGPUTextureFormat format, bool blend) { + WGPUVertexAttribute attrs[3] = {}; + attrs[0].format = WGPUVertexFormat_Float32x2; + attrs[0].offset = 0; + attrs[0].shaderLocation = 0; + attrs[1].format = WGPUVertexFormat_Float32x4; + attrs[1].offset = 8; + attrs[1].shaderLocation = 1; + attrs[2].format = WGPUVertexFormat_Float32x2; + attrs[2].offset = 24; + attrs[2].shaderLocation = 2; + WGPUVertexBufferLayout vb{}; + fillVertexLayout(vb, 32, attrs, 3); + + WGPUColorTargetState target{}; + target.format = format; + WGPUBlendState bs = alphaBlend(); + if (blend) target.blend = &bs; + + WGPURenderPipelineDescriptor pd{}; + pd.label = sv("eve_lit2d"); + pd.layout = layout.Get(); + wgpu::ShaderModule vertModule = makeWgslModule(dev, kTexturedVertWgsl); + wgpu::ShaderModule fragModule = makeWgslModule(dev, kLit2DFragWgsl); + pd.vertex.module = vertModule.Get(); + pd.vertex.entryPoint = sv("vs_main"); + pd.vertex.bufferCount = 1; + pd.vertex.buffers = &vb; + WGPUFragmentState fs{}; + fs.module = fragModule.Get(); + fs.entryPoint = sv("fs_main"); + fs.targetCount = 1; + fs.targets = ⌖ + pd.fragment = &fs; + pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; + pd.primitive.frontFace = WGPUFrontFace_CCW; + pd.primitive.cullMode = WGPUCullMode_None; + pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; + pd.depthStencil = nullptr; + pd.multisample.count = 1; + return dev.CreateRenderPipeline(reinterpret_cast(&pd)); +} + +} // namespace + +void Graphics::create2DPipelines() { + tex2DSetLayout = make2DBindGroupLayout(); + tex2DPipelineLayout = make2DPipelineLayout(); + + colorPipeline = make2DColorPipeline(device, surfaceFormat, true); + texturedPipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, surfaceFormat, true); + lit2dPipeline = make2DLitPipeline(device, tex2DPipelineLayout, surfaceFormat, true); + + offscreenColorPipeline = make2DColorPipeline(device, WGPUTextureFormat_RGBA8Unorm, true); + offscreenTexturedPipeline = make2DTexturedPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, true); + offscreenLitPipeline = make2DLitPipeline(device, tex2DPipelineLayout, WGPUTextureFormat_RGBA8Unorm, true); +} + +void Graphics::createMesh3DPipelines() { + mesh3dSetLayout = makeMesh3DBindGroupLayout(); + mesh3dPipelineLayout = makeMesh3DPipelineLayout(); + + WGPUVertexAttribute attrs[3] = {}; + attrs[0].format = WGPUVertexFormat_Float32x3; // pos + attrs[0].offset = 0; + attrs[0].shaderLocation = 0; + attrs[1].format = WGPUVertexFormat_Float32x3; // normal + attrs[1].offset = 12; + attrs[1].shaderLocation = 1; + attrs[2].format = WGPUVertexFormat_Float32x2; // uv + attrs[2].offset = 24; + attrs[2].shaderLocation = 2; + WGPUVertexBufferLayout vb{}; + fillVertexLayout(vb, 32, attrs, 3); + + WGPUDepthStencilState ds{}; + ds.format = WGPUTextureFormat_Depth32Float; + ds.depthWriteEnabled = WGPUOptionalBool_True; + ds.depthCompare = WGPUCompareFunction_Less; + ds.stencilReadMask = 0; + ds.stencilWriteMask = 0; + + WGPUColorTargetState target{}; + target.format = sceneColorFormat; + target.blend = nullptr; + + WGPURenderPipelineDescriptor pd{}; + pd.label = sv("eve_mesh3d"); + pd.layout = mesh3dPipelineLayout.Get(); + wgpu::ShaderModule vertModule = makeWgslModule(device, kMesh3DVertWgsl); + wgpu::ShaderModule fragModule = makeWgslModule(device, kMesh3DFragWgsl); + pd.vertex.module = vertModule.Get(); + pd.vertex.entryPoint = sv("vs_main"); + pd.vertex.bufferCount = 1; + pd.vertex.buffers = &vb; + WGPUFragmentState fs{}; + fs.module = fragModule.Get(); + fs.entryPoint = sv("fs_main"); + fs.targetCount = 1; + fs.targets = ⌖ + pd.fragment = &fs; + pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; + pd.primitive.frontFace = WGPUFrontFace_CCW; + pd.primitive.cullMode = WGPUCullMode_None; + pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; + pd.depthStencil = &ds; + pd.multisample.count = 1; + mesh3dPipeline = device.CreateRenderPipeline(reinterpret_cast(&pd)); +} + +void Graphics::createShadowPipelines() { + shadowSetLayout = makeShadowBindGroupLayout(); + shadowPipelineLayout = makeShadowPipelineLayout(); + + WGPUVertexAttribute attrs[3] = {}; + attrs[0].format = WGPUVertexFormat_Float32x3; + attrs[0].offset = 0; + attrs[0].shaderLocation = 0; + attrs[1].format = WGPUVertexFormat_Float32x3; + attrs[1].offset = 12; + attrs[1].shaderLocation = 1; + attrs[2].format = WGPUVertexFormat_Float32x2; + attrs[2].offset = 24; + attrs[2].shaderLocation = 2; + WGPUVertexBufferLayout vb{}; + fillVertexLayout(vb, 32, attrs, 3); + + WGPUDepthStencilState ds{}; + ds.format = WGPUTextureFormat_Depth32Float; + ds.depthWriteEnabled = WGPUOptionalBool_True; + ds.depthCompare = WGPUCompareFunction_Less; + ds.stencilReadMask = 0; + ds.stencilWriteMask = 0; + + WGPURenderPipelineDescriptor pd{}; + pd.label = sv("eve_shadow"); + pd.layout = shadowPipelineLayout.Get(); + wgpu::ShaderModule vertModule = makeWgslModule(device, kMesh3DShadowVertWgsl); + pd.vertex.module = vertModule.Get(); + pd.vertex.entryPoint = sv("vs_main"); + pd.vertex.bufferCount = 1; + pd.vertex.buffers = &vb; + pd.fragment = nullptr; + pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; + pd.primitive.frontFace = WGPUFrontFace_CCW; + pd.primitive.cullMode = WGPUCullMode_None; + pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; + pd.depthStencil = &ds; + pd.multisample.count = 1; + mesh3dShadowPipeline = device.CreateRenderPipeline(reinterpret_cast(&pd)); +} + +void Graphics::createGbufferPipelines() { + gbufferSetLayout = makeGbufferBindGroupLayout(); + gbufferPipelineLayout = makeGbufferPipelineLayout(); + + WGPUVertexAttribute attrs[3] = {}; + attrs[0].format = WGPUVertexFormat_Float32x3; + attrs[0].offset = 0; + attrs[0].shaderLocation = 0; + attrs[1].format = WGPUVertexFormat_Float32x3; + attrs[1].offset = 12; + attrs[1].shaderLocation = 1; + attrs[2].format = WGPUVertexFormat_Float32x2; + attrs[2].offset = 24; + attrs[2].shaderLocation = 2; + WGPUVertexBufferLayout vb{}; + fillVertexLayout(vb, 32, attrs, 3); + + WGPUDepthStencilState ds{}; + ds.format = WGPUTextureFormat_Depth32Float; + ds.depthWriteEnabled = WGPUOptionalBool_True; + ds.depthCompare = WGPUCompareFunction_Less; + ds.stencilReadMask = 0; + ds.stencilWriteMask = 0; + + WGPUColorTargetState targets[3] = {}; + for (int i = 0; i < 3; ++i) { + targets[i].format = WGPUTextureFormat_RGBA8Unorm; + targets[i].blend = nullptr; + } + + WGPURenderPipelineDescriptor pd{}; + pd.label = sv("eve_gbuffer"); + pd.layout = gbufferPipelineLayout.Get(); + wgpu::ShaderModule vertModule = makeWgslModule(device, kMesh3DGbufferVertWgsl); + wgpu::ShaderModule fragModule = makeWgslModule(device, kMesh3DGbufferFragWgsl); + pd.vertex.module = vertModule.Get(); + pd.vertex.entryPoint = sv("vs_main"); + pd.vertex.bufferCount = 1; + pd.vertex.buffers = &vb; + WGPUFragmentState fs{}; + fs.module = fragModule.Get(); + fs.entryPoint = sv("fs_main"); + fs.targetCount = 3; + fs.targets = targets; + pd.fragment = &fs; + pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; + pd.primitive.frontFace = WGPUFrontFace_CCW; + pd.primitive.cullMode = WGPUCullMode_None; + pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; + pd.depthStencil = &ds; + pd.multisample.count = 1; + mesh3dGbufferPipeline = device.CreateRenderPipeline(reinterpret_cast(&pd)); +} + +void Graphics::createVoxelPipelines() { + voxelSetLayout = makeVoxelBindGroupLayout(); + voxelPipelineLayout = makeVoxelPipelineLayout(); + + WGPUVertexAttribute attrs[2] = {}; + attrs[0].format = WGPUVertexFormat_Float32x2; // corner + attrs[0].offset = 0; + attrs[0].shaderLocation = 0; + attrs[1].format = WGPUVertexFormat_Uint32; // packed + attrs[1].offset = 8; + attrs[1].shaderLocation = 1; + WGPUVertexBufferLayout cornerVb{}; + fillVertexLayout(cornerVb, 12, attrs, 2); + WGPUVertexBufferLayout instanceVb{}; + instanceVb.arrayStride = 4; + instanceVb.stepMode = WGPUVertexStepMode_Instance; + instanceVb.attributeCount = 0; + instanceVb.attributes = nullptr; + WGPUVertexBufferLayout vbs[2] = {cornerVb, instanceVb}; + + WGPUDepthStencilState ds{}; + ds.format = WGPUTextureFormat_Depth32Float; + ds.depthWriteEnabled = WGPUOptionalBool_True; + ds.depthCompare = WGPUCompareFunction_Less; + ds.stencilReadMask = 0; + ds.stencilWriteMask = 0; + + WGPUColorTargetState target{}; + target.format = sceneColorFormat; + target.blend = nullptr; + + WGPURenderPipelineDescriptor pd{}; + pd.label = sv("eve_voxel"); + pd.layout = voxelPipelineLayout.Get(); + wgpu::ShaderModule vertModule = makeWgslModule(device, kVoxelRectVertWgsl); + wgpu::ShaderModule fragModule = makeWgslModule(device, kVoxelRectFragWgsl); + pd.vertex.module = vertModule.Get(); + pd.vertex.entryPoint = sv("vs_main"); + pd.vertex.bufferCount = 2; + pd.vertex.buffers = vbs; + WGPUFragmentState fs{}; + fs.module = fragModule.Get(); + fs.entryPoint = sv("fs_main"); + fs.targetCount = 1; + fs.targets = ⌖ + pd.fragment = &fs; + pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; + pd.primitive.frontFace = WGPUFrontFace_CCW; + pd.primitive.cullMode = WGPUCullMode_None; + pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; + pd.depthStencil = &ds; + pd.multisample.count = 1; + voxelRectPipeline = device.CreateRenderPipeline(reinterpret_cast(&pd)); + + // Unit quad for instanced voxel faces (2 triangles, corner + packed uv slot). + float quad[8] = {0, 0, 1, 0, 1, 1, 0, 1}; + WGPUBufferDescriptor bd{}; + bd.label = sv("eve_voxel_quad"); + bd.size = sizeof(quad); + bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; + bd.mappedAtCreation = false; + voxelUnitQuadVerts = device.CreateBuffer(reinterpret_cast(&bd)); + queue.WriteBuffer(voxelUnitQuadVerts, 0, quad, sizeof(quad)); + + uint32_t indices[6] = {0, 1, 2, 2, 3, 0}; + WGPUBufferDescriptor ibd{}; + ibd.label = sv("eve_voxel_quad_idx"); + ibd.size = sizeof(indices); + ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index; + ibd.mappedAtCreation = false; + voxelUnitQuadIndices = device.CreateBuffer(reinterpret_cast(&ibd)); + queue.WriteBuffer(voxelUnitQuadIndices, 0, indices, sizeof(indices)); +} + +// --------------------------------------------------------------------------- +// Arena helpers +// --------------------------------------------------------------------------- + +uint32_t Graphics::UboArena::alloc(uint64_t size, uint64_t alignment) { + uint64_t aligned = (used + alignment - 1) / alignment * alignment; + used = aligned + size; + return static_cast(aligned); +} + +uint64_t Graphics::VertexArena::alloc(uint64_t bytes) { + uint64_t at = used; + used += bytes; + return at; +} + +Graphics::UboArena &Graphics::currentUboArena() { return uboArenas[currentFrameSlot()]; } +Graphics::VertexArena &Graphics::currentVertexArena() { return vertexArenas[currentFrameSlot()]; } + +void Graphics::ensureUboArena(UboArena &arena, uint64_t bytes) { + if (arena.buffer && arena.capacity >= bytes) return; + uint64_t size = arena.capacity; + while (size < bytes) size = size ? size * 2 : (64u << 10); + WGPUBufferDescriptor bd{}; + bd.label = sv("eve_ubo_arena"); + bd.size = size; + bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Uniform; + bd.mappedAtCreation = false; + arena.buffer = device.CreateBuffer(reinterpret_cast(&bd)); + arena.capacity = size; + arena.used = 0; +} + +void Graphics::ensureVertexArena(VertexArena &arena, uint64_t bytes) { + if (arena.buffer && arena.capacity >= bytes) return; + uint64_t size = arena.capacity; + while (size < bytes) size = size ? size * 2 : (256u << 10); + WGPUBufferDescriptor bd{}; + bd.label = sv("eve_vertex_arena"); + bd.size = size; + bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; + bd.mappedAtCreation = false; + arena.buffer = device.CreateBuffer(reinterpret_cast(&bd)); + arena.capacity = size; + arena.used = 0; +} + +// --------------------------------------------------------------------------- +// Sampler +// --------------------------------------------------------------------------- + +wgpu::Sampler Graphics::makeSampler(const TextureSampler &s, uint32_t mipLevels) const { + WGPUSamplerDescriptor d{}; + d.label = sv("eve_sampler"); + d.addressModeU = s.repeatU ? WGPUAddressMode_Repeat : WGPUAddressMode_ClampToEdge; + d.addressModeV = s.repeatV ? WGPUAddressMode_Repeat : WGPUAddressMode_ClampToEdge; + d.addressModeW = s.repeatW ? WGPUAddressMode_Repeat : WGPUAddressMode_ClampToEdge; + d.magFilter = s.mag == FilterMode::Nearest ? WGPUFilterMode_Nearest : WGPUFilterMode_Linear; + d.minFilter = s.min == FilterMode::Nearest ? WGPUFilterMode_Nearest : WGPUFilterMode_Linear; + if (s.mipmap == MipmapMode::Disabled || mipLevels <= 1) { + d.mipmapFilter = WGPUMipmapFilterMode_Nearest; + d.lodMinClamp = 0.f; + d.lodMaxClamp = 0.f; + } else { + d.mipmapFilter = + s.mipmap == MipmapMode::Nearest ? WGPUMipmapFilterMode_Nearest : WGPUMipmapFilterMode_Linear; + d.lodMinClamp = s.minLod; + d.lodMaxClamp = std::min(std::max(s.maxLod, 0.f), float(mipLevels)); + } + float aniso = s.maxAnisotropy > 1.f ? s.maxAnisotropy : 1.f; + d.maxAnisotropy = std::min(std::max(aniso, 1.f), std::max(maxSamplerAnisotropy, 1.f)); + return device.CreateSampler(reinterpret_cast(&d)); +} + +// --------------------------------------------------------------------------- +// Texture creation +// --------------------------------------------------------------------------- + +Texture *Graphics::newTexture(int width, int height, const uint8_t *rgba, bool repeatU, + bool repeatV) { + TextureCreateInfo info; + info.sampler.repeatU = repeatU; + info.sampler.repeatV = repeatV; + return newTexture(width, height, rgba, info); +} + +Texture *Graphics::newTexture(image::ImageData *data) { + if (!data) throw Exception("newTexture: null ImageData"); + TextureCreateInfo info; + return newTexture(data->getWidth(), data->getHeight(), + static_cast(data->getData()), info); +} + +Texture *Graphics::newTexture(image::ImageData *data, const TextureCreateInfo &info) { + if (!data) throw Exception("newTexture: null ImageData"); + if (data->getFormat() != "RGBA8") + throw Exception("newTexture: only RGBA8 supported"); + return newTexture(data->getWidth(), data->getHeight(), + static_cast(data->getData()), info); +} + +Texture *Graphics::newTexture(int width, int height, const uint8_t *rgba, + const TextureCreateInfo &info) { + if (width <= 0 || height <= 0) throw Exception("newTexture: invalid size %dx%d", width, height); + + auto gpu = std::make_unique(); + gpu->width = width; + gpu->height = height; + gpu->samplerState = info.sampler; + gpu->mipLevels = info.generateMipmaps ? uint32_t(mipmapCountForSize(width, height)) : 1u; + + WGPUTextureDescriptor td{}; + td.label = sv("eve_tex2d"); + td.dimension = WGPUTextureDimension_2D; + td.size.width = static_cast(width); + td.size.height = static_cast(height); + td.size.depthOrArrayLayers = 1; + td.sampleCount = 1; + td.format = WGPUTextureFormat_RGBA8Unorm; + td.mipLevelCount = gpu->mipLevels; + td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst | + WGPUTextureUsage_RenderAttachment; + gpu->texture = device.CreateTexture(reinterpret_cast(&td)); + + uploadTexturePixelsMips(gpu.get(), rgba, width, height); + + WGPUTextureViewDescriptor vd{}; + vd.format = WGPUTextureFormat_RGBA8Unorm; + vd.dimension = WGPUTextureViewDimension_2D; + vd.baseMipLevel = 0; + vd.mipLevelCount = gpu->mipLevels; + vd.baseArrayLayer = 0; + vd.arrayLayerCount = 1; + gpu->view = gpu->texture.CreateView(reinterpret_cast(&vd)); + gpu->sampler = makeSampler(info.sampler, gpu->mipLevels); + + auto *tex = new Texture(); + tex->width = width; + tex->height = height; + tex->mipmapCount = int(gpu->mipLevels); + tex->sampler = info.sampler; + tex->gpuHandle = gpu.get(); + + ownedGpuTextures.push_back(std::move(gpu)); + ownedTextures.push_back(std::unique_ptr(tex)); + return tex; +} + +void Graphics::uploadTexturePixels(GpuTexture *gt, const uint8_t *rgba, int w, int h, + const TextureCreateInfo &info) { + (void)info; + uploadTexturePixelsMips(gt, rgba, w, h); +} + +void Graphics::uploadTexturePixelsMips(GpuTexture *gt, const uint8_t *rgba, int w, int h) { + if (!rgba) return; + WGPUTexelCopyBufferLayout layout{}; + layout.offset = 0; + layout.bytesPerRow = static_cast(w * 4); + layout.rowsPerImage = static_cast(h); + WGPUExtent3D extent{static_cast(w), static_cast(h), 1}; + + for (uint32_t m = 0; m < gt->mipLevels; ++m) { + WGPUTexelCopyTextureInfo dst{}; + dst.texture = gt->texture.Get(); + dst.mipLevel = m; + dst.aspect = WGPUTextureAspect_All; + queue.WriteTexture(reinterpret_cast(&dst), rgba, + static_cast(w) * h * 4, + reinterpret_cast(&layout), + reinterpret_cast(&extent)); + if (m + 1 < gt->mipLevels) { + // Box-filter downsample into the CPU buffer for the next level. + std::vector next((w / 2) * (h / 2) * 4); + for (int y = 0; y < h / 2; ++y) { + for (int x = 0; x < w / 2; ++x) { + for (int c = 0; c < 4; ++c) { + uint32_t acc = 0; + acc += rgba[((y * 2 + 0) * w + (x * 2 + 0)) * 4 + c]; + acc += rgba[((y * 2 + 0) * w + (x * 2 + 1)) * 4 + c]; + acc += rgba[((y * 2 + 1) * w + (x * 2 + 0)) * 4 + c]; + acc += rgba[((y * 2 + 1) * w + (x * 2 + 1)) * 4 + c]; + next[(y * (w / 2) + x) * 4 + c] = uint8_t(acc / 4); + } + } + } + w /= 2; + h /= 2; + rgba = next.data(); + layout.bytesPerRow = static_cast(w * 4); + layout.rowsPerImage = static_cast(h); + extent = {static_cast(w), static_cast(h), 1}; + } + } +} + +Texture *Graphics::newCubemap(int faceSize, const uint8_t *rgbaFaces) { + TextureCreateInfo info; + return newCubemap(faceSize, rgbaFaces, info); +} + +Texture *Graphics::newCubemap(int faceSize, const uint8_t *rgbaFaces, + const TextureCreateInfo &info) { + if (faceSize <= 0 || !rgbaFaces) + throw Exception("newCubemap: invalid size or null data"); + + auto gpu = std::make_unique(); + gpu->width = faceSize; + gpu->height = faceSize; + gpu->samplerState = info.sampler; + gpu->isCube = true; + gpu->mipLevels = info.generateMipmaps ? uint32_t(mipmapCountForSize(faceSize, faceSize)) : 1u; + + WGPUTextureDescriptor td{}; + td.label = sv("eve_cubemap"); + td.dimension = WGPUTextureDimension_2D; + td.size.width = static_cast(faceSize); + td.size.height = static_cast(faceSize); + td.size.depthOrArrayLayers = 6; + td.sampleCount = 1; + td.format = WGPUTextureFormat_RGBA8Unorm; + td.mipLevelCount = gpu->mipLevels; + td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst; + gpu->texture = device.CreateTexture(reinterpret_cast(&td)); + + WGPUTexelCopyBufferLayout layout{}; + layout.offset = 0; + layout.bytesPerRow = static_cast(faceSize * 4); + layout.rowsPerImage = static_cast(faceSize); + WGPUExtent3D extent{static_cast(faceSize), static_cast(faceSize), 1}; + for (int f = 0; f < 6; ++f) { + WGPUTexelCopyTextureInfo dst{}; + dst.texture = gpu->texture.Get(); + dst.mipLevel = 0; + dst.aspect = WGPUTextureAspect_All; + dst.origin = {0, 0, static_cast(f)}; + queue.WriteTexture(reinterpret_cast(&dst), + rgbaFaces + f * faceSize * faceSize * 4, + static_cast(faceSize) * faceSize * 4, + reinterpret_cast(&layout), + reinterpret_cast(&extent)); + } + + WGPUTextureViewDescriptor vd{}; + vd.format = WGPUTextureFormat_RGBA8Unorm; + vd.dimension = WGPUTextureViewDimension_Cube; + vd.baseMipLevel = 0; + vd.mipLevelCount = gpu->mipLevels; + vd.baseArrayLayer = 0; + vd.arrayLayerCount = 6; + gpu->view = gpu->texture.CreateView(reinterpret_cast(&vd)); + gpu->sampler = makeSampler(info.sampler, gpu->mipLevels); + + auto *tex = new Texture(); + tex->width = faceSize; + tex->height = faceSize; + tex->mipmapCount = int(gpu->mipLevels); + tex->sampler = info.sampler; + tex->gpuHandle = gpu.get(); + + ownedGpuTextures.push_back(std::move(gpu)); + ownedTextures.push_back(std::unique_ptr(tex)); + return tex; +} + +void Graphics::setTextureSampler(Texture *texture, const TextureSampler &sampler) { + if (!texture) return; + auto *gpu = gpuForTexture(texture); + if (!gpu) return; + gpu->samplerState = sampler; + gpu->sampler = makeSampler(sampler, gpu->mipLevels); + texture->sampler = sampler; +} + +float Graphics::getMaxAnisotropy() const { return maxSamplerAnisotropy; } + +Texture *Graphics::newTextureFromFile(const std::string &filename) { + if (filename.empty()) throw Exception("newTextureFromFile: empty filename"); + auto it = texturesByPath.find(filename); + if (it != texturesByPath.end()) { + reloadTextureFromFile(filename); + return it->second; + } + auto *fs = eve::filesystem::Filesystem::create(); + if (!fs) throw Exception("newTextureFromFile: no filesystem"); + std::unique_ptr fileData(fs->read(filename)); + if (!fileData) throw Exception("newTextureFromFile: failed to read '%s'", filename.c_str()); + auto *imgMod = image::Image::create(); + std::unique_ptr data(imgMod->newImageData(fileData.get())); + if (!data) throw Exception("newTextureFromFile: cannot decode '%s'", filename.c_str()); + Texture *tex = newTexture(data.get()); + texturesByPath[filename] = tex; + return tex; +} + +bool Graphics::reloadTextureFromFile(const std::string &filename) { + auto it = texturesByPath.find(filename); + if (it == texturesByPath.end()) return false; + auto *fs = eve::filesystem::Filesystem::create(); + if (!fs) return false; + std::unique_ptr fileData; + try { + fileData.reset(fs->read(filename)); + } catch (...) { + return false; + } + if (!fileData) return false; + auto *imgMod = image::Image::create(); + std::unique_ptr data(imgMod->newImageData(fileData.get())); + if (!data) return false; + Texture *tex = it->second; + auto *gpu = gpuForTexture(tex); + if (gpu && data->getWidth() == tex->width && data->getHeight() == tex->height) { + uploadTexturePixelsMips(gpu, static_cast(data->getData()), tex->width, + tex->height); + } + return true; +} + +GpuTexture *Graphics::gpuForTexture(Texture *t) const { + return t ? static_cast(t->gpuHandle) : nullptr; +} + +GpuTexture *Graphics::gpuForTextureOrWhite(Texture *t) const { + GpuTexture *g = gpuForTexture(t); + return g ? g : whiteTexture; +} + +wgpu::BindGroup Graphics::makeTex2DBindGroup(GpuTexture *color, GpuTexture *depth) { + GpuTexture *c = color ? color : whiteTexture; + GpuTexture *d = depth ? depth : c; + WGPUBindGroupEntry entries[5]{}; + entries[0].binding = 0; + entries[0].textureView = c->view.Get(); + entries[1].binding = 1; + entries[1].textureView = d->view.Get(); + entries[2].binding = 2; + entries[2].sampler = c->sampler.Get(); + entries[3].binding = 3; + entries[3].sampler = d->sampler.Get(); + entries[4].binding = 4; + entries[4].buffer = currentUboArena().buffer.Get(); // replaced per-draw via dynamic offset + entries[4].size = + std::max(Shader::kPushConstantBytes, uint32_t(sizeof(Lighting2DUBO))); + WGPUBindGroupDescriptor desc{}; + desc.label = sv("eve_tex2d_group"); + desc.layout = tex2DSetLayout.Get(); + desc.entryCount = 5; + desc.entries = entries; + return device.CreateBindGroup(reinterpret_cast(&desc)); +} + +wgpu::BindGroup Graphics::makeMeshBindGroup(GpuTexture *albedo, GpuTexture *normal, GpuTexture *env, + GpuTexture *height, uint32_t frameUboOffset, + uint32_t shadowUboOffset, uint32_t pushUboOffset) { + GpuTexture *a = albedo ? albedo : whiteTexture; + GpuTexture *n = normal ? normal : flatNormalTexture; + GpuTexture *e = env ? env : defaultEnvCubemap; + GpuTexture *h = height ? height : flatHeightTexture3D; + + WGPUBindGroupEntry entries[9]{}; + entries[0].binding = 0; + entries[0].buffer = currentUboArena().buffer.Get(); + entries[0].size = sizeof(Mesh3DUBO); + entries[1].binding = 1; + entries[1].textureView = a->view.Get(); + entries[2].binding = 2; + entries[2].textureView = n->view.Get(); + entries[3].binding = 3; + entries[3].textureView = e->view.Get(); + entries[4].binding = 4; + entries[4].buffer = currentUboArena().buffer.Get(); + entries[4].size = sizeof(ShadowUBO); + entries[5].binding = 5; + entries[5].textureView = shadowDepthArray ? shadowDepthArray->view.Get() : nullptr; + entries[6].binding = 6; + entries[6].textureView = h->view.Get(); + entries[7].binding = 7; + entries[7].sampler = mainSampler.Get(); + entries[8].binding = 8; + entries[8].sampler = shadowDepthArray ? shadowDepthArray->sampler.Get() : nullptr; + + (void)frameUboOffset; + (void)shadowUboOffset; + (void)pushUboOffset; + WGPUBindGroupDescriptor desc{}; + desc.label = sv("eve_mesh_group"); + desc.layout = mesh3dSetLayout.Get(); + desc.entryCount = 9; + desc.entries = entries; + return device.CreateBindGroup(reinterpret_cast(&desc)); +} + +// --------------------------------------------------------------------------- +// Mesh creation +// --------------------------------------------------------------------------- + +Mesh *Graphics::newMeshFromArrays(const float *posXYZ, const float *nrmXYZ, const float *uvST, + int vertexCount, const uint32_t *indices, int indexCount) { + if (vertexCount <= 0 || !posXYZ) throw Exception("newMeshFromArrays: invalid vertex data"); + + std::vector verts; + verts.reserve(vertexCount * 8); + for (int i = 0; i < vertexCount; ++i) { + verts.push_back(posXYZ[i * 3 + 0]); + verts.push_back(posXYZ[i * 3 + 1]); + verts.push_back(posXYZ[i * 3 + 2]); + if (nrmXYZ) { + verts.push_back(nrmXYZ[i * 3 + 0]); + verts.push_back(nrmXYZ[i * 3 + 1]); + verts.push_back(nrmXYZ[i * 3 + 2]); + } else { + verts.push_back(0.f); + verts.push_back(0.f); + verts.push_back(1.f); + } + if (uvST) { + verts.push_back(uvST[i * 2 + 0]); + verts.push_back(uvST[i * 2 + 1]); + } else { + verts.push_back(0.f); + verts.push_back(0.f); + } + } + + auto gpu = std::make_unique(); + gpu->vertexCount = uint32_t(vertexCount); + gpu->indexCount = indexCount > 0 ? uint32_t(indexCount) : 0; + gpu->vertexStride = 32; + + WGPUBufferDescriptor vbd{}; + vbd.label = sv("eve_mesh_vb"); + vbd.size = verts.size() * sizeof(float); + vbd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; + vbd.mappedAtCreation = false; + gpu->vertexBuffer = device.CreateBuffer(reinterpret_cast(&vbd)); + queue.WriteBuffer(gpu->vertexBuffer, 0, verts.data(), vbd.size); + + if (indexCount > 0) { + WGPUBufferDescriptor ibd{}; + ibd.label = sv("eve_mesh_ib"); + ibd.size = uint64_t(indexCount) * sizeof(uint32_t); + ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index; + ibd.mappedAtCreation = false; + gpu->indexBuffer = device.CreateBuffer(reinterpret_cast(&ibd)); + queue.WriteBuffer(gpu->indexBuffer, 0, indices, ibd.size); + } + + auto *mesh = new Mesh(); + mesh->indexCount = indexCount; + mesh->gpuHandle = gpu.get(); + ownedGpuMeshes.push_back(std::move(gpu)); + ownedMeshes.push_back(std::unique_ptr(mesh)); + return mesh; +} + +Mesh *Graphics::newMeshFromAssimp(const ::aiMesh &mesh) { + aiMatrix4x4 ident; + return newMeshFromAssimp(mesh, ident); +} + +Mesh *Graphics::newMeshFromAssimp(const ::aiMesh &mesh, const aiMatrix4x4 &worldTransform) { + std::vector pos, nrm, uv; + std::vector idx; + pos.reserve(mesh.mNumVertices * 3); + nrm.reserve(mesh.mNumVertices * 3); + uv.reserve(mesh.mNumVertices * 2); + idx.reserve(mesh.mNumFaces * 3); + + for (unsigned i = 0; i < mesh.mNumVertices; ++i) { + aiVector3D p = worldTransform * mesh.mVertices[i]; + pos.push_back(p.x); + pos.push_back(p.y); + pos.push_back(p.z); + if (mesh.mNormals) { + aiMatrix3x3 rot(worldTransform); + aiVector3D n = rot * mesh.mNormals[i]; + n.Normalize(); + nrm.push_back(n.x); + nrm.push_back(n.y); + nrm.push_back(n.z); + } else { + nrm.push_back(0.f); + nrm.push_back(0.f); + nrm.push_back(1.f); + } + if (mesh.mTextureCoords[0]) { + uv.push_back(mesh.mTextureCoords[0][i].x); + uv.push_back(mesh.mTextureCoords[0][i].y); + } else { + uv.push_back(0.f); + uv.push_back(0.f); + } + } + for (unsigned f = 0; f < mesh.mNumFaces; ++f) { + for (unsigned v = 0; v < mesh.mFaces[f].mNumIndices; ++v) + idx.push_back(mesh.mFaces[f].mIndices[v]); + } + Mesh *m = newMeshFromArrays(pos.data(), nrm.data(), uv.data(), int(mesh.mNumVertices), idx.data(), + int(idx.size())); + if (m && mesh.mNumAnimMeshes > 0) { + m->initMorphBase(int(mesh.mNumVertices), pos.data(), nrm.data(), uv.data()); + for (unsigned a = 0; a < mesh.mNumAnimMeshes; ++a) { + const aiAnimMesh *am = mesh.mAnimMeshes[a]; + std::vector absPos(am->mNumVertices * 3); + for (unsigned i = 0; i < am->mNumVertices; ++i) { + absPos[i * 3 + 0] = am->mVertices[i].x; + absPos[i * 3 + 1] = am->mVertices[i].y; + absPos[i * 3 + 2] = am->mVertices[i].z; + } + std::string name = am->mName.C_Str(); + if (name.empty()) name = "morph" + std::to_string(a); + m->addMorphTargetAbsolute(name, absPos.data()); + } + } + return m; +} + +bool Graphics::bakeMeshMorph(Mesh *mesh) { + if (!mesh || !mesh->hasMorphData() || !mesh->isMorphDirty()) return false; + auto *gpu = static_cast(mesh->gpuHandle); + if (!gpu || !gpu->vertexBuffer) return false; + std::vector pos, nrm; + mesh->computeMorphedPositions(pos, nrm); + if (pos.empty()) return false; + std::vector verts; + verts.reserve(mesh->getVertexCount() * 8); + const auto &uvs = mesh->baseUv(); + for (int i = 0; i < mesh->getVertexCount(); ++i) { + verts.push_back(pos[i * 3 + 0]); + verts.push_back(pos[i * 3 + 1]); + verts.push_back(pos[i * 3 + 2]); + if (nrm.size() >= size_t((i + 1) * 3)) { + verts.push_back(nrm[i * 3 + 0]); + verts.push_back(nrm[i * 3 + 1]); + verts.push_back(nrm[i * 3 + 2]); + } else { + verts.push_back(0.f); + verts.push_back(0.f); + verts.push_back(1.f); + } + if (uvs.size() >= size_t((i + 1) * 2)) { + verts.push_back(uvs[i * 2 + 0]); + verts.push_back(uvs[i * 2 + 1]); + } else { + verts.push_back(0.f); + verts.push_back(0.f); + } + } + queue.WriteBuffer(gpu->vertexBuffer, 0, verts.data(), verts.size() * sizeof(float)); + mesh->markMorphClean(); + return true; +} + +Mesh *Graphics::newMeshSphere(int slices, int stacks) { + std::vector pos, nrm, uv; + std::vector idx; + for (int y = 0; y <= stacks; ++y) { + for (int x = 0; x <= slices; ++x) { + float u = float(x) / float(slices); + float v = float(y) / float(stacks); + float theta = u * 2.f * glm::pi(); + float phi = v * glm::pi(); + float sx = std::sin(phi) * std::cos(theta); + float sy = std::cos(phi); + float sz = std::sin(phi) * std::sin(theta); + pos.push_back(sx); + pos.push_back(sy); + pos.push_back(sz); + nrm.push_back(sx); + nrm.push_back(sy); + nrm.push_back(sz); + uv.push_back(u); + uv.push_back(v); + } + } + for (int y = 0; y < stacks; ++y) { + for (int x = 0; x < slices; ++x) { + int a = y * (slices + 1) + x; + int b = a + slices + 1; + idx.push_back(a); + idx.push_back(b); + idx.push_back(a + 1); + idx.push_back(b); + idx.push_back(b + 1); + idx.push_back(a + 1); + } + } + return newMeshFromArrays(pos.data(), nrm.data(), uv.data(), int(pos.size() / 3), idx.data(), + int(idx.size())); +} + +Mesh *Graphics::newMeshCylinder(int slices, int stacks, bool caps) { + std::vector pos, nrm, uv; + std::vector idx; + for (int y = 0; y <= stacks; ++y) { + for (int x = 0; x <= slices; ++x) { + float u = float(x) / float(slices); + float v = float(y) / float(stacks); + float theta = u * 2.f * glm::pi(); + pos.push_back(std::cos(theta)); + pos.push_back(v * 2.f - 1.f); + pos.push_back(std::sin(theta)); + nrm.push_back(std::cos(theta)); + nrm.push_back(0.f); + nrm.push_back(std::sin(theta)); + uv.push_back(u); + uv.push_back(v); + } + } + for (int y = 0; y < stacks; ++y) { + for (int x = 0; x < slices; ++x) { + int a = y * (slices + 1) + x; + int b = a + slices + 1; + idx.push_back(a); + idx.push_back(b); + idx.push_back(a + 1); + idx.push_back(b); + idx.push_back(b + 1); + idx.push_back(a + 1); + } + } + if (caps) { + int base = int(pos.size() / 3); + pos.push_back(0.f); pos.push_back(-1.f); pos.push_back(0.f); + nrm.push_back(0.f); nrm.push_back(-1.f); nrm.push_back(0.f); + uv.push_back(0.5f); uv.push_back(0.5f); + for (int x = 0; x < slices; ++x) { + float theta0 = float(x) / float(slices) * 2.f * glm::pi(); + float theta1 = float(x + 1) / float(slices) * 2.f * glm::pi(); + int i0 = int(pos.size() / 3); + pos.push_back(std::cos(theta0)); pos.push_back(-1.f); pos.push_back(std::sin(theta0)); + nrm.push_back(0.f); nrm.push_back(-1.f); nrm.push_back(0.f); + uv.push_back(0.5f + 0.5f * std::cos(theta0)); uv.push_back(0.5f + 0.5f * std::sin(theta0)); + int i1 = int(pos.size() / 3); + pos.push_back(std::cos(theta1)); pos.push_back(-1.f); pos.push_back(std::sin(theta1)); + nrm.push_back(0.f); nrm.push_back(-1.f); nrm.push_back(0.f); + uv.push_back(0.5f + 0.5f * std::cos(theta1)); uv.push_back(0.5f + 0.5f * std::sin(theta1)); + idx.push_back(base); + idx.push_back(i1); + idx.push_back(i0); + } + base = int(pos.size() / 3); + pos.push_back(0.f); pos.push_back(1.f); pos.push_back(0.f); + nrm.push_back(0.f); nrm.push_back(1.f); nrm.push_back(0.f); + uv.push_back(0.5f); uv.push_back(0.5f); + for (int x = 0; x < slices; ++x) { + float theta0 = float(x) / float(slices) * 2.f * glm::pi(); + float theta1 = float(x + 1) / float(slices) * 2.f * glm::pi(); + int i0 = int(pos.size() / 3); + pos.push_back(std::cos(theta0)); pos.push_back(1.f); pos.push_back(std::sin(theta0)); + nrm.push_back(0.f); nrm.push_back(1.f); nrm.push_back(0.f); + uv.push_back(0.5f + 0.5f * std::cos(theta0)); uv.push_back(0.5f + 0.5f * std::sin(theta0)); + int i1 = int(pos.size() / 3); + pos.push_back(std::cos(theta1)); pos.push_back(1.f); pos.push_back(std::sin(theta1)); + nrm.push_back(0.f); nrm.push_back(1.f); nrm.push_back(0.f); + uv.push_back(0.5f + 0.5f * std::cos(theta1)); uv.push_back(0.5f + 0.5f * std::sin(theta1)); + idx.push_back(base); + idx.push_back(i0); + idx.push_back(i1); + } + } + return newMeshFromArrays(pos.data(), nrm.data(), uv.data(), int(pos.size() / 3), idx.data(), + int(idx.size())); +} + +// --------------------------------------------------------------------------- +// 2D drawing +// --------------------------------------------------------------------------- + +void Graphics::drawSolidRect(float x, float y, float w, float h, const Color &color) { + solidBatch.addRect(x, y, w, h, color); +} + +void Graphics::drawTexturedRect(Texture *texture, float x, float y, float w, float h, + const Color &color) { + drawTexturedRectUV(texture, x, y, w, h, 0.f, 0.f, 1.f, 1.f, color); +} + +void Graphics::drawTexturedRectShader(Texture *texture, Shader *shader, float x, float y, float w, + float h, const Color &color) { + drawTexturedRectShaderUV(texture, shader, x, y, w, h, 0.f, 0.f, 1.f, 1.f, color); +} + +void Graphics::drawTexturedRectUV(Texture *texture, float x, float y, float w, float h, float u0, + float v0, float u1, float v1, const Color &color) { + drawTexturedRectShaderUV(texture, currentShader, x, y, w, h, u0, v0, u1, v1, color); +} + +void Graphics::drawTexturedRectShaderUV(Texture *texture, Shader *shader, float x, float y, float w, + float h, float u0, float v0, float u1, float v1, + const Color &color, bool rotatedUV) { + if (!texture) { + drawSolidRect(x, y, w, h, color); + return; + } + auto it = std::find_if(texturedBatches.begin(), texturedBatches.end(), + [&](const TexturedBatch &tb) { + return tb.texture == texture && tb.shader == shader && + tb.depth == nullptr; + }); + if (it == texturedBatches.end()) { + texturedBatches.push_back(TexturedBatch{texture, nullptr, shader, Batcher{}}); + it = texturedBatches.end() - 1; + } + it->batch.addTexturedRect(x, y, w, h, color, u0, v0, u1, v1, rotatedUV); +} + +void Graphics::drawTexturedRectShaderUVRotated(Texture *texture, Shader *shader, float cx, float cy, + float w, float h, float degrees, float u0, float v0, + float u1, float v1, const Color &color, + bool rotatedUV) { + if (!texture) { + drawSolidRect(cx - w * 0.5f, cy - h * 0.5f, w, h, color); + return; + } + auto it = std::find_if(texturedBatches.begin(), texturedBatches.end(), + [&](const TexturedBatch &tb) { + return tb.texture == texture && tb.shader == shader && + tb.depth == nullptr; + }); + if (it == texturedBatches.end()) { + texturedBatches.push_back(TexturedBatch{texture, nullptr, shader, Batcher{}}); + it = texturedBatches.end() - 1; + } + it->batch.addTexturedRectRotated(cx, cy, w, h, degrees, color, u0, v0, u1, v1, rotatedUV); +} + +void Graphics::drawTexturedRectShaderDepth(Texture *color, Texture *depth, Shader *shader, float x, + float y, float w, float h, const Color &tint) { + if (!color) { + drawSolidRect(x, y, w, h, tint); + return; + } + auto it = std::find_if(texturedBatches.begin(), texturedBatches.end(), + [&](const TexturedBatch &tb) { + return tb.texture == color && tb.shader == shader && + tb.depth == depth; + }); + if (it == texturedBatches.end()) { + texturedBatches.push_back(TexturedBatch{color, depth, shader, Batcher{}}); + it = texturedBatches.end() - 1; + } + it->batch.addTexturedRect(x, y, w, h, tint, 0.f, 0.f, 1.f, 1.f, false); +} + +void Graphics::drawTexturedRectLitUV(Texture *albedo, Texture *normal, float x, float y, float w, + float h, float u0, float v0, float u1, float v1, + const Color &color) { + if (!albedo) { + drawSolidRect(x, y, w, h, color); + return; + } + auto it = std::find_if(litBatches.begin(), litBatches.end(), + [&](const LitBatch &lb) { + return lb.albedo == albedo && lb.normal == normal; + }); + if (it == litBatches.end()) { + litBatches.push_back(LitBatch{albedo, normal, Batcher{}}); + it = litBatches.end() - 1; + } + it->batch.addTexturedRect(x, y, w, h, color, u0, v0, u1, v1, false); +} + +void Graphics::setLighting2D(const Lighting2DUBO &ubo) { + // Uploaded on demand before flushing lit batches. + lighting2dFrame = ubo; +} + +void Graphics::flush2D(wgpu::RenderPassEncoder pass, int viewW, int viewH, + WGPUTextureFormat format) { + // Solid batch. + if (!solidBatch.empty() && (colorPipeline || offscreenColorPipeline)) { + Batcher ndc = solidBatch; + ndc.toNDC(viewW, viewH); + auto verts = ndc.vertices(); + // Batcher::toNDC targets Vulkan's Y-down NDC ((-1,-1)=top-left), but + // WebGPU's clip space is Y-up ((-1,-1)=bottom-left). Flip Y so logical + // top-left maps to the swapchain's top-left. + for (auto &v : verts) v.pos.y = -v.pos.y; + if (!verts.empty()) { + std::vector data; + data.reserve(verts.size() * 6); + for (const auto &v : verts) { + data.push_back(v.pos.x); + data.push_back(v.pos.y); + data.push_back(v.color.r); + data.push_back(v.color.g); + data.push_back(v.color.b); + data.push_back(v.color.a); + } + uint64_t bytes = data.size() * sizeof(float); + auto &arena = currentVertexArena(); + ensureVertexArena(arena, arena.used + bytes); + uint64_t offset = arena.alloc(bytes); + queue.WriteBuffer(arena.buffer, offset, data.data(), bytes); + + wgpu::RenderPipeline pipe = + uint32_t(format) == uint32_t(surfaceFormat) ? colorPipeline : offscreenColorPipeline; + pass.SetPipeline(pipe); + pass.SetVertexBuffer(0, arena.buffer, offset, bytes); + pass.Draw(static_cast(verts.size()), 1, 0, 0); + } + } + solidBatch.clear(); + + // Textured batches. + for (auto &tb : texturedBatches) { + if (tb.batch.empty()) continue; + drawTexturedBatch(pass, tb, viewW, viewH, format, uint32_t(format) != uint32_t(surfaceFormat)); + } + texturedBatches.clear(); + + // Lit batches. + for (auto &lb : litBatches) { + if (lb.batch.empty()) continue; + drawLitBatch(pass, lb, viewW, viewH, format); + } + litBatches.clear(); +} + +void Graphics::drawTexturedBatch(wgpu::RenderPassEncoder pass, TexturedBatch &tb, int viewW, + int viewH, WGPUTextureFormat format, bool offscreen) { + Batcher ndc = tb.batch; + ndc.toNDC(viewW, viewH); + auto verts = ndc.vertices(); + for (auto &v : verts) v.pos.y = -v.pos.y; + if (verts.empty()) return; + + std::vector data; + data.reserve(verts.size() * 8); + for (const auto &v : verts) { + data.push_back(v.pos.x); + data.push_back(v.pos.y); + data.push_back(v.color.r); + data.push_back(v.color.g); + data.push_back(v.color.b); + data.push_back(v.color.a); + data.push_back(v.uv.x); + data.push_back(v.uv.y); + } + uint64_t bytes = data.size() * sizeof(float); + auto &arena = currentVertexArena(); + ensureVertexArena(arena, arena.used + bytes); + uint64_t vtxOffset = arena.alloc(bytes); + queue.WriteBuffer(arena.buffer, vtxOffset, data.data(), bytes); + + GpuTexture *gpu = gpuForTexture(tb.texture); + GpuTexture *depthGpu = gpuForTexture(tb.depth); + auto &uboArena = currentUboArena(); + ensureUboArena(uboArena, uboArena.used + 256); + + // Push-constant (Externals) block for custom shaders. + uint32_t pushOffset = 0; + if (tb.shader && tb.shader->pushConstantSize() > 0) { + pushOffset = uboArena.alloc(Shader::kPushConstantBytes, 256); + queue.WriteBuffer(uboArena.buffer, pushOffset, tb.shader->pushConstantData(), + Shader::kPushConstantBytes); + } + + wgpu::BindGroup bg = makeTex2DBindGroup(gpu, depthGpu); + uint32_t offsets[1] = {pushOffset}; + + wgpu::RenderPipeline pipe; + if (tb.shader && tb.shader->gpuHandle) { + auto *gs = static_cast(tb.shader->gpuHandle); + pipe = offscreen ? gs->offscreenPipeline : gs->swapchainPipeline; + } else { + pipe = offscreen ? offscreenTexturedPipeline : texturedPipeline; + } + if (!pipe) return; + pass.SetPipeline(pipe); + pass.SetBindGroup(0, bg, 1, offsets); + pass.SetVertexBuffer(0, arena.buffer, vtxOffset, bytes); + pass.Draw(static_cast(verts.size()), 1, 0, 0); +} + +void Graphics::drawLitBatch(wgpu::RenderPassEncoder pass, LitBatch &lb, int viewW, int viewH, + WGPUTextureFormat format) { + Batcher ndc = lb.batch; + ndc.toNDC(viewW, viewH); + auto verts = ndc.vertices(); + for (auto &v : verts) v.pos.y = -v.pos.y; + if (verts.empty()) return; + + std::vector data; + data.reserve(verts.size() * 8); + for (const auto &v : verts) { + data.push_back(v.pos.x); + data.push_back(v.pos.y); + data.push_back(v.color.r); + data.push_back(v.color.g); + data.push_back(v.color.b); + data.push_back(v.color.a); + data.push_back(v.uv.x); + data.push_back(v.uv.y); + } + uint64_t bytes = data.size() * sizeof(float); + auto &arena = currentVertexArena(); + ensureVertexArena(arena, arena.used + bytes); + uint64_t vtxOffset = arena.alloc(bytes); + queue.WriteBuffer(arena.buffer, vtxOffset, data.data(), bytes); + + GpuTexture *albedoGpu = gpuForTextureOrWhite(lb.albedo); + GpuTexture *normalGpu = gpuForTextureOrWhite(lb.normal); + + auto &uboArena = currentUboArena(); + ensureUboArena(uboArena, uboArena.used + 512); + uint32_t uboOffset = uboArena.alloc(sizeof(Lighting2DUBO), 256); + queue.WriteBuffer(uboArena.buffer, uboOffset, &lighting2dFrame, sizeof(Lighting2DUBO)); + + wgpu::BindGroup bg = makeTex2DBindGroup(albedoGpu, normalGpu); + uint32_t offsets[1] = {uboOffset}; + wgpu::RenderPipeline pipe = + uint32_t(format) == uint32_t(surfaceFormat) ? lit2dPipeline : offscreenLitPipeline; + if (!pipe) return; + pass.SetPipeline(pipe); + pass.SetBindGroup(0, bg, 1, offsets); + pass.SetVertexBuffer(0, arena.buffer, vtxOffset, bytes); + pass.Draw(static_cast(verts.size()), 1, 0, 0); +} + +// --------------------------------------------------------------------------- +// 3D rendering +// --------------------------------------------------------------------------- + +void Graphics::begin3DFrame() { + if (!initialized || !device) return; + frame3DStarted = true; + frameHad3DThisFrame = false; + frameHad3D = false; + sceneColorPassOpen = false; + mesh3dDraws.clear(); + shadowPassDraws.clear(); + for (int c = 0; c < ShadowConfig::kCascades; ++c) shadowCascadeDraws[c].clear(); + voxelDraws.clear(); + if (surfaceNeedsRecreate.load()) { + surfaceNeedsRecreate.store(false); + markSwapchainDirty(); + } + rebuildSwapchainIfNeeded(); +} + +void Graphics::setMesh3DViewProj(const glm::mat4 &viewProj) { mesh3dViewProj = viewProj; } +void Graphics::setMesh3DView(const glm::mat4 &view) { mesh3dView = view; } +void Graphics::setMesh3DClip(float nearZ, float farZ) { + mesh3dNear = nearZ; + mesh3dFar = farZ; +} + +Texture *Graphics::getSceneColorTexture() { return sceneColorTexture; } + +void Graphics::drawMesh(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint) { + drawMeshShader(mesh, model, texture, tint, nullptr); +} + +void Graphics::drawMeshShader(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint, + Shader *shader) { + if (!mesh || !mesh->gpuHandle) return; + frameHad3DThisFrame = true; + frameHad3D = true; + Mesh3dDraw d; + d.mesh = mesh; + d.texture = texture; + d.model = model; + d.tint = tint; + d.shader = shader; + mesh3dDraws.push_back(d); +} + +void Graphics::setMesh3DNormalTexture(Texture *normal) { mesh3dNormalTexture = normal; } +void Graphics::setMesh3DHeightTexture(Texture *height) { mesh3dHeightTexture = height; } +void Graphics::setMesh3DMaterial(float metallic, float roughness) { + mesh3dMetallic = metallic; + mesh3dRoughness = roughness; +} +void Graphics::setMesh3DTexCellBomb(float cellScale, float strength, float rotAmount) { + mesh3dTexBombScale = cellScale; + mesh3dTexBombStrength = strength; + mesh3dTexBombRot = rotAmount; +} +void Graphics::setMesh3DParallax(float scale, float minLayers, float maxLayers) { + mesh3dParallaxScale = scale; + mesh3dParallaxMin = minLayers; + mesh3dParallaxMax = maxLayers; +} +void Graphics::setMesh3DLighting(const Lighting3DPack &pack) { mesh3dLighting = pack; } +void Graphics::setMesh3DClusteredLighting(const ClusteredLightingUpload &upload) { + mesh3dClustered = upload; + mesh3dClusteredActive = upload.active; +} +void Graphics::setMesh3DLight(const glm::vec3 &dir, const glm::vec3 &color) { + mesh3dLighting.lights[0].posRadius = glm::vec4(dir, 0.f); + mesh3dLighting.lights[0].color = glm::vec4(color, 1.f); +} +void Graphics::setMesh3DCameraPos(const glm::vec3 &eye) { mesh3dCameraPos = eye; } +void Graphics::setMesh3DEnv(Texture *cube, float intensity) { + mesh3dEnvTexture = cube; + mesh3dEnvIntensity = intensity; +} +void Graphics::setMesh3DShadows(const ShadowUpload &upload) { mesh3dShadows = upload; } +void Graphics::setMesh3DShadowReceive(bool receive) { mesh3dShadowReceive = receive; } + +void Graphics::beginShadowPass(int cascadeIndex) { + shadowPassCascade = cascadeIndex; + shadowPassDraws.clear(); +} + +void Graphics::drawMeshShadow(Mesh *mesh, const glm::mat4 &lightMVP) { + if (!mesh || !mesh->gpuHandle) return; + ShadowDraw d; + d.mesh = mesh; + d.mvp = lightMVP; + shadowPassDraws.push_back(d); +} + +void Graphics::endShadowPass() { + if (shadowPassCascade < 0 || shadowPassCascade >= ShadowConfig::kCascades) { + shadowPassCascade = -1; + shadowPassDraws.clear(); + return; + } + shadowCascadeDraws[shadowPassCascade] = shadowPassDraws; + shadowPassDraws.clear(); + shadowPassCascade = -1; +} + +// --------------------------------------------------------------------------- +// GBuffer pass +// --------------------------------------------------------------------------- + +void Graphics::beginGBufferPass(int width, int height) { + if (!device) return; + createGbufferResources(width, height); + gbufferPassActive = true; + gbufferPassPending = true; + gbufferPassDraws.clear(); +} + +void Graphics::drawMeshGBuffer(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model, float nearZ, + float farZ, Texture *albedo, float tintR, float tintG, float tintB) { + if (!mesh || !mesh->gpuHandle) return; + GbufferDraw d; + d.mesh = mesh; + d.albedo = albedo; + d.mvp = mvp; + d.model = model; + d.nearZ = nearZ; + d.farZ = farZ; + d.tint = glm::vec4(tintR, tintG, tintB, 1.f); + gbufferPassDraws.push_back(d); +} + +void Graphics::endGBufferPass() { gbufferPassActive = false; } + +// --------------------------------------------------------------------------- +// Voxel +// --------------------------------------------------------------------------- + +void Graphics::drawVoxelFaceInstances(const uint32_t *packed, int count, float originX, + float originY, float originZ, const std::string &faceDir, + Texture *atlas, int tilesPerRow) { + if (!device || count <= 0 || !packed) return; + if (!voxelUnitQuadVerts) return; + frameHad3DThisFrame = true; + frameHad3D = true; + + int face = 5; + if (faceDir == "posX" || faceDir == "+x") face = 0; + else if (faceDir == "negX" || faceDir == "-x") face = 1; + else if (faceDir == "posY" || faceDir == "+y") face = 2; + else if (faceDir == "negY" || faceDir == "-y") face = 3; + else if (faceDir == "posZ" || faceDir == "+z") face = 4; + + VoxelDraw d; + d.count = uint32_t(count); + d.atlas = gpuForTextureOrWhite(atlas); + d.viewProj = mesh3dViewProj; + d.chunkOrigin = glm::vec4(originX, originY, originZ, float(face)); + d.atlasInfo = glm::vec4(float(tilesPerRow), 0.f, 0.f, 0.f); + d.tint = glm::vec4(1.f); + d.instanceBufferOffset = 0; + d.pushUboOffset = 0; + + // Upload packed instances into the voxel instance arena (per frame slot). + auto &arena = voxelInstanceArena; + if (!arena.buffer) { + WGPUBufferDescriptor bd{}; + bd.label = sv("eve_voxel_instances"); + bd.size = 1u << 20; + bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; + bd.mappedAtCreation = false; + arena.buffer = device.CreateBuffer(reinterpret_cast(&bd)); + arena.capacity = 1u << 20; + } + uint64_t need = uint64_t(count) * 4; + if (arena.used + need > arena.capacity) { + uint64_t cap = arena.capacity * 2; + WGPUBufferDescriptor bd{}; + bd.label = sv("eve_voxel_instances"); + bd.size = cap; + bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; + bd.mappedAtCreation = false; + arena.buffer = device.CreateBuffer(reinterpret_cast(&bd)); + arena.capacity = cap; + arena.used = 0; + } + d.instanceBufferOffset = static_cast(arena.used); + queue.WriteBuffer(arena.buffer, arena.used, packed, need); + arena.used += need; + voxelDraws.push_back(d); +} + +// --------------------------------------------------------------------------- +// Scene color / shadow / gbuffer resources +// --------------------------------------------------------------------------- + +void Graphics::createSceneColorResources(int width, int height) { + if (!device) return; + if (sceneColorWidth == width && sceneColorHeight == height && !sceneColorSlots.empty()) return; + + destroySceneColorResources(); + sceneColorWidth = width; + sceneColorHeight = height; + sceneColorSamples = 1; + for (int s = 0; s < int(kFramesInFlight); ++s) { + SceneColorSlot slot; + slot.sampleCount = sceneColorSamples; + + WGPUTextureDescriptor cd{}; + cd.label = sv("eve_scene_color"); + cd.dimension = WGPUTextureDimension_2D; + cd.size = {static_cast(width), static_cast(height), 1}; + cd.sampleCount = sceneColorSamples; + cd.format = sceneColorFormat; + cd.mipLevelCount = 1; + cd.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment | + WGPUTextureUsage_CopySrc; + slot.color = device.CreateTexture(reinterpret_cast(&cd)); + slot.colorView = slot.color.CreateView(); + + WGPUTextureDescriptor dd{}; + dd.label = sv("eve_scene_depth"); + dd.dimension = WGPUTextureDimension_2D; + dd.size = {static_cast(width), static_cast(height), 1}; + dd.sampleCount = sceneColorSamples; + dd.format = WGPUTextureFormat_Depth32Float; + dd.mipLevelCount = 1; + dd.usage = WGPUTextureUsage_RenderAttachment; + slot.depth = device.CreateTexture(reinterpret_cast(&dd)); + slot.depthView = slot.depth.CreateView(); + + slot.colorGpu.texture = slot.color; + slot.colorGpu.view = slot.colorView; + slot.colorGpu.width = width; + slot.colorGpu.height = height; + slot.colorGpu.sampler = createLinearSampler(device); + slot.colorGpu.samplerState = TextureSampler::linearMipmap(); + + slot.colorTex.gpuHandle = &slot.colorGpu; + slot.colorTex.width = width; + slot.colorTex.height = height; + slot.colorTex.mipmapCount = 1; + + sceneColorSlots.push_back(std::move(slot)); + } + sceneColorTexture = &sceneColorSlots[0].colorTex; +} + +void Graphics::destroySceneColorResources() { + sceneColorSlots.clear(); + sceneColorTexture = nullptr; +} + +void Graphics::createShadowResources() { + if (!device) return; + if (shadowDepthArray) return; + shadowMapSize = ShadowConfig::kMapSize; + + // Depth array: 3 cascade layers, renderable + sampleable. + auto *gpu = new GpuTexture(); + WGPUTextureDescriptor td{}; + td.label = sv("eve_shadow_depth"); + td.dimension = WGPUTextureDimension_2D; + td.size = {static_cast(shadowMapSize), static_cast(shadowMapSize), 3}; + td.sampleCount = 1; + td.format = WGPUTextureFormat_Depth32Float; + td.mipLevelCount = 1; + td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment; + gpu->texture = device.CreateTexture(reinterpret_cast(&td)); + + WGPUTextureViewDescriptor avd{}; + avd.format = WGPUTextureFormat_Depth32Float; + avd.dimension = WGPUTextureViewDimension_2DArray; + avd.baseMipLevel = 0; + avd.mipLevelCount = 1; + avd.baseArrayLayer = 0; + avd.arrayLayerCount = 3; + gpu->view = gpu->texture.CreateView(reinterpret_cast(&avd)); + + WGPUSamplerDescriptor sd{}; + sd.label = sv("eve_shadow_sampler"); + sd.addressModeU = WGPUAddressMode_ClampToEdge; + sd.addressModeV = WGPUAddressMode_ClampToEdge; + sd.addressModeW = WGPUAddressMode_ClampToEdge; + sd.magFilter = WGPUFilterMode_Linear; + sd.minFilter = WGPUFilterMode_Linear; + sd.mipmapFilter = WGPUMipmapFilterMode_Nearest; + sd.compare = WGPUCompareFunction_LessEqual; + sd.maxAnisotropy = 1.f; + gpu->sampler = device.CreateSampler(reinterpret_cast(&sd)); + shadowDepthArray = gpu; +} + +void Graphics::destroyShadowResources() { + shadowDepthArray = nullptr; +} + +void Graphics::createGbufferResources(int width, int height) { + if (!device) return; + if (gbufferWidth == width && gbufferHeight == height && !gbufferSlots.empty()) return; + destroyGbufferResources(); + gbufferWidth = width; + gbufferHeight = height; + + WGPUTextureDescriptor td{}; + td.label = sv("eve_gbuffer_target"); + td.dimension = WGPUTextureDimension_2D; + td.size = {static_cast(width), static_cast(height), 1}; + td.sampleCount = 1; + td.format = WGPUTextureFormat_RGBA8Unorm; + td.mipLevelCount = 1; + td.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_RenderAttachment; + + WGPUTextureDescriptor dd{}; + dd.label = sv("eve_gbuffer_depth"); + dd.dimension = WGPUTextureDimension_2D; + dd.size = {static_cast(width), static_cast(height), 1}; + dd.sampleCount = 1; + dd.format = WGPUTextureFormat_Depth32Float; + dd.mipLevelCount = 1; + dd.usage = WGPUTextureUsage_RenderAttachment; + + for (int s = 0; s < int(kFramesInFlight); ++s) { + GbufferSlot slot; + slot.normal = device.CreateTexture(reinterpret_cast(&td)); + slot.normalView = slot.normal.CreateView(); + slot.depthColor = device.CreateTexture(reinterpret_cast(&td)); + slot.depthColorView = slot.depthColor.CreateView(); + slot.albedo = device.CreateTexture(reinterpret_cast(&td)); + slot.albedoView = slot.albedo.CreateView(); + slot.depth = device.CreateTexture(reinterpret_cast(&dd)); + slot.depthView = slot.depth.CreateView(); + + slot.normalGpu.texture = slot.normal; + slot.normalGpu.view = slot.normalView; + slot.normalGpu.sampler = createLinearSampler(device); + slot.depthColorGpu.texture = slot.depthColor; + slot.depthColorGpu.view = slot.depthColorView; + slot.depthColorGpu.sampler = createLinearSampler(device); + slot.albedoGpu.texture = slot.albedo; + slot.albedoGpu.view = slot.albedoView; + slot.albedoGpu.sampler = createLinearSampler(device); + + slot.normalTex.gpuHandle = &slot.normalGpu; + slot.normalTex.width = width; + slot.normalTex.height = height; + slot.depthColorTex.gpuHandle = &slot.depthColorGpu; + slot.depthColorTex.width = width; + slot.depthColorTex.height = height; + slot.albedoTex.gpuHandle = &slot.albedoGpu; + slot.albedoTex.width = width; + slot.albedoTex.height = height; + + gbufferSlots.push_back(std::move(slot)); + } +} + +void Graphics::destroyGbufferResources() { gbufferSlots.clear(); } + +// --------------------------------------------------------------------------- +// Flush helpers +// --------------------------------------------------------------------------- + +void Graphics::flushMesh3D(wgpu::RenderPassEncoder pass, WGPUTextureFormat format) { + if (mesh3dDraws.empty()) return; + + auto &uboArena = currentUboArena(); + ensureUboArena(uboArena, uboArena.used + mesh3dDraws.size() * 2048); + auto &vtxArena = currentVertexArena(); + + // Pre-allocate per-draw UBO slots. + for (auto &d : mesh3dDraws) { + d.frameUboOffset = uboArena.alloc(sizeof(Mesh3DUBO), 256); + d.shadowUboOffset = uboArena.alloc(sizeof(ShadowUBO), 256); + d.pushUboOffset = 0; + if (d.shader && d.shader->pushConstantSize() > 0) + d.pushUboOffset = uboArena.alloc(Shader::kPushConstantBytes, 256); + } + + // Upload the per-draw frame UBO + shared shadow UBO. + for (auto &d : mesh3dDraws) { + Mesh3DUBO ubo; + ubo.mvp = mesh3dViewProj * d.model; + ubo.model = d.model; + ubo.lightDir = glm::vec4(glm::vec3(mesh3dLighting.lights[0].posRadius), float(mesh3dLighting.count)); + ubo.lightColor = mesh3dLighting.lights[0].color; + ubo.tint = d.tint; + ubo.cameraPos = glm::vec4(mesh3dCameraPos, mesh3dRoughness); + ubo.ambient = glm::vec4(glm::vec3(mesh3dLighting.ambient), mesh3dMetallic); + for (int i = 0; i < Lighting3DPack::kMaxLights; ++i) + ubo.lights[i] = mesh3dLighting.lights[i]; + ubo.texBomb = glm::vec4(mesh3dTexBombScale, mesh3dTexBombStrength, mesh3dTexBombRot, 0.f); + ubo.parallax = glm::vec4(mesh3dParallaxScale, mesh3dParallaxMin, mesh3dParallaxMax, 0.f); + ubo.view = mesh3dView; + ubo.clipInfo = glm::vec4(mesh3dNear, mesh3dFar, 0.f, 0.f); + ubo.lightColor.w = mesh3dEnvIntensity; + queue.WriteBuffer(uboArena.buffer, d.frameUboOffset, &ubo, sizeof(ubo)); + } + + // Shared shadow UBO written once. + ShadowUBO shadowUbo = mesh3dShadows.ubo; + if (!mesh3dShadows.active || !mesh3dShadowReceive) shadowUbo.bias.y = 0.f; + for (auto &d : mesh3dDraws) + queue.WriteBuffer(uboArena.buffer, d.shadowUboOffset, &shadowUbo, sizeof(shadowUbo)); + + for (auto &d : mesh3dDraws) { + auto *gpuMesh = static_cast(d.mesh->gpuHandle); + if (!gpuMesh || !gpuMesh->vertexBuffer) continue; + + wgpu::RenderPipeline pipe = mesh3dPipeline; + if (d.shader && d.shader->gpuHandle) { + auto *gs = static_cast(d.shader->gpuHandle); + if (gs->isMesh3D && gs->mesh3dPipeline) pipe = gs->mesh3dPipeline; + } + if (!pipe) continue; + pass.SetPipeline(pipe); + + GpuTexture *albedo = gpuForTexture(d.texture); + GpuTexture *normal = gpuForTexture(mesh3dNormalTexture); + GpuTexture *env = gpuForTexture(mesh3dEnvTexture); + GpuTexture *height = gpuForTexture(mesh3dHeightTexture); + wgpu::BindGroup bg = makeMeshBindGroup(albedo, normal, env, height, d.frameUboOffset, + d.shadowUboOffset, d.pushUboOffset); + uint32_t offsets[2] = {d.frameUboOffset, d.shadowUboOffset}; + pass.SetBindGroup(0, bg, 2, offsets); + + if (gpuMesh->indexBuffer) { + pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, gpuMesh->vertexCount * 32); + pass.SetIndexBuffer(gpuMesh->indexBuffer, wgpu::IndexFormat::Uint32, 0, + uint64_t(gpuMesh->indexCount) * 4); + pass.DrawIndexed(gpuMesh->indexCount, 1, 0, 0, 0); + } else { + pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, gpuMesh->vertexCount * 32); + pass.Draw(gpuMesh->vertexCount, 1, 0, 0); + } + } + mesh3dDraws.clear(); +} + +void Graphics::flushShadowPass(wgpu::RenderPassEncoder pass) { + auto &uboArena = currentUboArena(); + ensureUboArena(uboArena, uboArena.used + 4096); + pass.SetPipeline(mesh3dShadowPipeline); + + for (int c = 0; c < ShadowConfig::kCascades; ++c) { + if (shadowCascadeDraws[c].empty()) continue; + for (auto &d : shadowCascadeDraws[c]) { + auto *gpuMesh = static_cast(d.mesh->gpuHandle); + if (!gpuMesh || !gpuMesh->vertexBuffer) continue; + + uint32_t offset = uboArena.alloc(256, 256); + queue.WriteBuffer(uboArena.buffer, offset, &d.mvp, sizeof(glm::mat4)); + + WGPUBindGroupEntry entry{}; + entry.binding = 0; + entry.buffer = uboArena.buffer.Get(); + entry.size = 64; + WGPUBindGroupDescriptor bgd{}; + bgd.layout = shadowSetLayout.Get(); + bgd.entryCount = 1; + bgd.entries = &entry; + wgpu::BindGroup bg = device.CreateBindGroup(reinterpret_cast(&bgd)); + uint32_t offsets[1] = {offset}; + pass.SetBindGroup(0, bg, 1, offsets); + + if (gpuMesh->indexBuffer) { + pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, gpuMesh->vertexCount * 32); + pass.SetIndexBuffer(gpuMesh->indexBuffer, wgpu::IndexFormat::Uint32, 0, + uint64_t(gpuMesh->indexCount) * 4); + pass.DrawIndexed(gpuMesh->indexCount, 1, 0, 0, 0); + } else { + pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, gpuMesh->vertexCount * 32); + pass.Draw(gpuMesh->vertexCount, 1, 0, 0); + } + } + shadowCascadeDraws[c].clear(); + } +} + +void Graphics::flushGbufferPass(wgpu::RenderPassEncoder pass) { + if (gbufferPassDraws.empty() || gbufferSlots.empty()) return; + auto &uboArena = currentUboArena(); + ensureUboArena(uboArena, uboArena.used + gbufferPassDraws.size() * 512); + pass.SetPipeline(mesh3dGbufferPipeline); + + for (auto &d : gbufferPassDraws) { + auto *gpuMesh = static_cast(d.mesh->gpuHandle); + if (!gpuMesh || !gpuMesh->vertexBuffer) continue; + + struct GbufferPush { + glm::mat4 mvp; + glm::mat4 model; + glm::vec4 clip; + } push; + push.mvp = d.mvp; + push.model = d.model; + push.clip = glm::vec4(d.nearZ, d.farZ, 0.f, 0.f); + + uint32_t offset = uboArena.alloc(256, 256); + queue.WriteBuffer(uboArena.buffer, offset, &push, sizeof(push)); + + GpuTexture *albedo = gpuForTextureOrWhite(d.albedo); + WGPUBindGroupEntry entries[3]{}; + entries[0].binding = 0; + entries[0].buffer = uboArena.buffer.Get(); + entries[0].size = 128; + entries[1].binding = 1; + entries[1].textureView = albedo->view.Get(); + entries[2].binding = 2; + entries[2].sampler = albedo->sampler.Get(); + WGPUBindGroupDescriptor bgd{}; + bgd.layout = gbufferSetLayout.Get(); + bgd.entryCount = 3; + bgd.entries = entries; + wgpu::BindGroup bg = device.CreateBindGroup(reinterpret_cast(&bgd)); + uint32_t offsets[1] = {offset}; + pass.SetBindGroup(0, bg, 1, offsets); + + if (gpuMesh->indexBuffer) { + pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, gpuMesh->vertexCount * 32); + pass.SetIndexBuffer(gpuMesh->indexBuffer, wgpu::IndexFormat::Uint32, 0, + uint64_t(gpuMesh->indexCount) * 4); + pass.DrawIndexed(gpuMesh->indexCount, 1, 0, 0, 0); + } else { + pass.SetVertexBuffer(0, gpuMesh->vertexBuffer, 0, gpuMesh->vertexCount * 32); + pass.Draw(gpuMesh->vertexCount, 1, 0, 0); + } + } + gbufferPassDraws.clear(); + gbufferPassPending = false; +} + +void Graphics::flushVoxelDraws(wgpu::RenderPassEncoder pass, WGPUTextureFormat format) { + if (voxelDraws.empty() || !voxelRectPipeline) return; + auto &uboArena = currentUboArena(); + ensureUboArena(uboArena, uboArena.used + voxelDraws.size() * 512); + pass.SetPipeline(voxelRectPipeline); + + for (auto &d : voxelDraws) { + uint32_t offset = uboArena.alloc(256, 256); + struct VoxelPC { + glm::mat4 viewProj; + glm::vec4 chunkOrigin; + glm::vec4 atlasInfo; + glm::vec4 tint; + } pc; + pc.viewProj = d.viewProj; + pc.chunkOrigin = d.chunkOrigin; + pc.atlasInfo = d.atlasInfo; + pc.tint = d.tint; + queue.WriteBuffer(uboArena.buffer, offset, &pc, sizeof(pc)); + + WGPUBindGroupEntry entries[3]{}; + entries[0].binding = 0; + entries[0].buffer = uboArena.buffer.Get(); + entries[0].size = sizeof(pc); + entries[1].binding = 1; + entries[1].textureView = d.atlas->view.Get(); + entries[2].binding = 2; + entries[2].sampler = d.atlas->sampler.Get(); + WGPUBindGroupDescriptor bgd{}; + bgd.layout = voxelSetLayout.Get(); + bgd.entryCount = 3; + bgd.entries = entries; + wgpu::BindGroup bg = device.CreateBindGroup(reinterpret_cast(&bgd)); + uint32_t offsets[1] = {offset}; + pass.SetBindGroup(0, bg, 1, offsets); + + pass.SetVertexBuffer(0, voxelUnitQuadVerts, 0, 32); + pass.SetVertexBuffer(1, voxelInstanceArena.buffer, d.instanceBufferOffset, + uint64_t(d.count) * 4); + pass.SetIndexBuffer(voxelUnitQuadIndices, wgpu::IndexFormat::Uint32, 0, 24); + pass.DrawIndexed(6, d.count, 0, 0, 0); + } + voxelDraws.clear(); +} + +// --------------------------------------------------------------------------- +// Present +// --------------------------------------------------------------------------- + +bool Graphics::acquireSurfaceTexture(wgpu::TextureView &view, wgpu::Texture &texture) { + if (!surface || !device || !swapchainConfigured) return false; + wgpu::SurfaceTexture surfTex{}; + surface.GetCurrentTexture(&surfTex); + if (!surfTex.texture) return false; + texture = surfTex.texture; + view = texture.CreateView(); + return bool(view); +} + +void Graphics::present() { + if (!device || !surface || !swapchainConfigured) return; + rebuildSwapchainIfNeeded(); + if (!swapchainConfigured) return; + + wgpu::TextureView surfaceView; + wgpu::Texture surfaceTex; + if (!acquireSurfaceTexture(surfaceView, surfaceTex)) { + return; + } + + auto &uboArena = currentUboArena(); + uboArena.reset(); + auto &vtxArena = currentVertexArena(); + vtxArena.reset(); + voxelInstanceArena.used = 0; + ensureUboArena(uboArena, 4096); + ensureVertexArena(vtxArena, 4096); + + wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); + + // 1. Shadow passes (CSM cascade layers). + if (shadowDepthArray) { + bool anyShadow = false; + for (int c = 0; c < ShadowConfig::kCascades; ++c) + if (!shadowCascadeDraws[c].empty()) anyShadow = true; + if (anyShadow) { + for (int c = 0; c < ShadowConfig::kCascades; ++c) { + if (shadowCascadeDraws[c].empty()) continue; + WGPUTextureViewDescriptor lvd{}; + lvd.format = WGPUTextureFormat_Depth32Float; + lvd.dimension = WGPUTextureViewDimension_2D; + lvd.baseMipLevel = 0; + lvd.mipLevelCount = 1; + lvd.baseArrayLayer = static_cast(c); + lvd.arrayLayerCount = 1; + wgpu::TextureView layerView = shadowDepthArray->texture.CreateView( + reinterpret_cast(&lvd)); + WGPURenderPassDepthStencilAttachment ds{}; + ds.view = layerView.Get(); + ds.depthClearValue = 1.f; + ds.depthLoadOp = WGPULoadOp_Clear; + ds.depthStoreOp = WGPUStoreOp_Store; + ds.stencilClearValue = 0; + ds.stencilLoadOp = WGPULoadOp_Clear; + ds.stencilStoreOp = WGPUStoreOp_Discard; + WGPURenderPassDescriptor rp{}; + rp.depthStencilAttachment = &ds; + wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast(&rp)); + flushShadowPass(pass); + pass.End(); + } + } + } + + // 2. Scene color pass (3D) into the offscreen target. + wgpu::TextureView sceneView; + wgpu::Texture sceneTex; + if (frameHad3DThisFrame && !sceneColorSlots.empty()) { + SceneColorSlot &slot = sceneColorSlots[currentFrameSlot()]; + WGPURenderPassColorAttachment colorAtt{}; + colorAtt.view = slot.colorView.Get(); + colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; + colorAtt.loadOp = WGPULoadOp_Clear; + colorAtt.storeOp = WGPUStoreOp_Store; + colorAtt.clearValue = {clearColor.r, clearColor.g, clearColor.b, 1.f}; + WGPURenderPassDepthStencilAttachment ds{}; + ds.view = slot.depthView.Get(); + ds.depthClearValue = 1.f; + ds.depthLoadOp = WGPULoadOp_Clear; + ds.depthStoreOp = WGPUStoreOp_Store; + ds.stencilClearValue = 0; + ds.stencilLoadOp = WGPULoadOp_Clear; + ds.stencilStoreOp = WGPUStoreOp_Discard; + WGPURenderPassDescriptor rp{}; + rp.colorAttachmentCount = 1; + rp.colorAttachments = &colorAtt; + rp.depthStencilAttachment = &ds; + wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast(&rp)); + flushVoxelDraws(pass, sceneColorFormat); + flushMesh3D(pass, sceneColorFormat); + pass.End(); + sceneView = slot.colorView; + sceneTex = slot.color; + } + + // 3. GBuffer pass. + if (gbufferPassPending && !gbufferSlots.empty()) { + GbufferSlot &slot = gbufferSlots[currentFrameSlot()]; + WGPURenderPassColorAttachment colorAtts[3]{}; + for (int i = 0; i < 3; ++i) { + colorAtts[i].depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; + colorAtts[i].loadOp = WGPULoadOp_Clear; + colorAtts[i].storeOp = WGPUStoreOp_Store; + colorAtts[i].clearValue = {0.f, 0.f, 0.f, 1.f}; + } + colorAtts[0].view = slot.normalView.Get(); + colorAtts[1].view = slot.depthColorView.Get(); + colorAtts[2].view = slot.albedoView.Get(); + WGPURenderPassDepthStencilAttachment ds{}; + ds.view = slot.depthView.Get(); + ds.depthClearValue = 1.f; + ds.depthLoadOp = WGPULoadOp_Clear; + ds.depthStoreOp = WGPUStoreOp_Store; + ds.stencilClearValue = 0; + ds.stencilLoadOp = WGPULoadOp_Clear; + ds.stencilStoreOp = WGPUStoreOp_Discard; + WGPURenderPassDescriptor rp{}; + rp.colorAttachmentCount = 3; + rp.colorAttachments = colorAtts; + rp.depthStencilAttachment = &ds; + wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast(&rp)); + flushGbufferPass(pass); + pass.End(); + } + + // 4. Active canvas: flush 2D batches into the offscreen target instead. + if (activeCanvas) { + auto *oc = static_cast(activeCanvas); + flush2DToCanvas(oc); + } + + // 5. Swapchain pass: composite scene color, draw 2D, run the overlay. + { + WGPURenderPassColorAttachment colorAtt{}; + colorAtt.view = surfaceView.Get(); + colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; + colorAtt.loadOp = WGPULoadOp_Clear; + colorAtt.storeOp = WGPUStoreOp_Store; + colorAtt.clearValue = {clearColor.r, clearColor.g, clearColor.b, 1.f}; + WGPURenderPassDescriptor rp{}; + rp.colorAttachmentCount = 1; + rp.colorAttachments = &colorAtt; + wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(reinterpret_cast(&rp)); + + if (sceneView) { + if (!fullscreenQuadReady) { + // Textured vertex: pos(2) + color(4) + uv(2) = 32 bytes. + float verts[32] = { + -1.f, -1.f, 1.f, 1.f, 1.f, 1.f, 0.f, 0.f, + 1.f, -1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 0.f, + 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 1.f, + -1.f, 1.f, 1.f, 1.f, 1.f, 1.f, 0.f, 1.f, + }; + uint32_t indices[6] = {0, 1, 2, 2, 3, 0}; + WGPUBufferDescriptor vbd{}; + vbd.label = sv("eve_fullscreen_vb"); + vbd.size = sizeof(verts); + vbd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Vertex; + fullscreenQuadVb = device.CreateBuffer(reinterpret_cast(&vbd)); + queue.WriteBuffer(fullscreenQuadVb, 0, verts, sizeof(verts)); + WGPUBufferDescriptor ibd{}; + ibd.label = sv("eve_fullscreen_ib"); + ibd.size = sizeof(indices); + ibd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_Index; + fullscreenQuadIb = device.CreateBuffer(reinterpret_cast(&ibd)); + queue.WriteBuffer(fullscreenQuadIb, 0, indices, sizeof(indices)); + fullscreenQuadReady = true; + } + + GpuTexture sceneGpu; + sceneGpu.texture = sceneTex; + sceneGpu.view = sceneView; + sceneGpu.sampler = createLinearSampler(device); + wgpu::BindGroup bg = makeTex2DBindGroup(&sceneGpu, nullptr); + uint32_t offsets[1] = {0}; + pass.SetPipeline(texturedPipeline); + pass.SetBindGroup(0, bg, 1, offsets); + pass.SetVertexBuffer(0, fullscreenQuadVb, 0, 4 * 32); + pass.SetIndexBuffer(fullscreenQuadIb, wgpu::IndexFormat::Uint32, 0, 24); + pass.DrawIndexed(6, 1, 0, 0, 0); + } + + if (!activeCanvas) { + flush2D(pass, pixelW > 0 ? pixelW : logicalW, pixelH > 0 ? pixelH : logicalH, + surfaceFormat); + } + + if (presentOverlayFn_) { + WGPURenderPassEncoder cPass = pass.Get(); + presentOverlayFn_(presentOverlayUser_, &cPass); + } + pass.End(); + } + + wgpu::CommandBuffer cmd = encoder.Finish(); + queue.Submit(1, &cmd); + // emdawnwebgpu does not implement wgpuSurfacePresent (it aborts); the + // browser presents the canvas automatically once the command buffer is + // submitted from within a requestAnimationFrame callback. On Emscripten + // the engine frame is driven by emscripten_set_main_loop (rAF), so the + // Squirrel main loop no longer blocks and no sleep is needed here. +#if !defined(__EMSCRIPTEN__) + surface.Present(); +#endif + + frameIndex++; + frame3DStarted = false; + frameHad3DThisFrame = false; + frameHad3D = false; + sceneColorPassOpen = false; + gbufferPassPending = false; +} + +// --------------------------------------------------------------------------- +// Canvas +// --------------------------------------------------------------------------- + +Canvas *Graphics::newCanvas(int width, int height) { + auto *c = new OffscreenCanvas(this, width, height); + ownedCanvases.push_back(std::unique_ptr(c)); + return c; +} + +void Graphics::setCanvas(Canvas *canvas) { + if (activeCanvas && canvas != activeCanvas) { + flush2DToCanvas(static_cast(activeCanvas)); + } + activeCanvas = canvas; +} + +bool Graphics::isCanvasActive() const { return activeCanvas != nullptr; } +Canvas *Graphics::getCanvas() const { return activeCanvas; } + +void Graphics::flush2DToCanvas(OffscreenCanvas *canvas) { + if (!canvas || !canvas->getTexture()) return; + auto &uboArena = currentUboArena(); + uboArena.reset(); + auto &vtxArena = currentVertexArena(); + vtxArena.reset(); + + wgpu::CommandEncoder enc = device.CreateCommandEncoder(); + WGPURenderPassColorAttachment ca{}; + ca.view = canvas->colorView.Get(); + ca.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; + ca.loadOp = canvas->clearRequested ? WGPULoadOp_Clear : WGPULoadOp_Load; + ca.storeOp = WGPUStoreOp_Store; + ca.clearValue = {canvas->clearColor.r, canvas->clearColor.g, canvas->clearColor.b, + canvas->clearColor.a}; + WGPURenderPassDescriptor rp{}; + rp.colorAttachmentCount = 1; + rp.colorAttachments = &ca; + wgpu::RenderPassEncoder pass = enc.BeginRenderPass(reinterpret_cast(&rp)); + flush2D(pass, canvas->getWidth(), canvas->getHeight(), WGPUTextureFormat_RGBA8Unorm); + pass.End(); + wgpu::CommandBuffer cmd = enc.Finish(); + queue.Submit(1, &cmd); + canvas->clearRequested = false; +} + +Texture *Graphics::getTexture() { + if (activeCanvas) return activeCanvas->getTexture(); + return sceneColorTexture; +} + +void Graphics::draw(eve::graphics::Graphics *gfx, const glm::mat4 &matrix) const { + (void)gfx; + (void)matrix; +} + +void Graphics::draw(Canvas *C, const glm::mat4 &matrix) const { + (void)C; + (void)matrix; +} + +void Graphics::clear(std::optional color, std::optional /*stencil*/, + std::optional /*depth*/) { + clearColor = color.value_or(backgroundColor); + hasPendingClear = true; +} + +Color Graphics::getPixel(int x, int y) { + if (activeCanvas) return getPixelImpl(static_cast(activeCanvas), x, y); + if (!sceneColorSlots.empty()) { + return getPixelImpl(nullptr, x, y); + } + return clearColor; +} + +image::ImageData *Graphics::newImageData() { + if (activeCanvas) return newImageDataImpl(static_cast(activeCanvas)); + if (!sceneColorSlots.empty()) return newImageDataImpl(nullptr); + return new image::ImageData(1, 1, "RGBA8"); +} + +// --------------------------------------------------------------------------- +// Readback +// --------------------------------------------------------------------------- + +namespace { + +bool copyTextureToCpu(wgpu::Instance &instance, wgpu::Device &device, wgpu::Queue &queue, + wgpu::Texture src, int width, int height, std::vector &outRgba) { + if (!src) return false; + uint64_t bytesPerRow = static_cast(width * 4); + bytesPerRow = (bytesPerRow + 255) / 256 * 256; // copy alignment 256 + uint64_t size = bytesPerRow * height; + + WGPUBufferDescriptor bd{}; + bd.label = sv("eve_readback"); + bd.size = size; + bd.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead; + bd.mappedAtCreation = false; + wgpu::Buffer dst = device.CreateBuffer(reinterpret_cast(&bd)); + + wgpu::CommandEncoder enc = device.CreateCommandEncoder(); + WGPUTexelCopyTextureInfo from{}; + from.texture = src.Get(); + from.mipLevel = 0; + from.aspect = WGPUTextureAspect_All; + from.origin = {0, 0, 0}; + WGPUTexelCopyBufferInfo to{}; + to.buffer = dst.Get(); + to.layout.offset = 0; + to.layout.bytesPerRow = static_cast(bytesPerRow); + to.layout.rowsPerImage = static_cast(height); + WGPUExtent3D extent{static_cast(width), static_cast(height), 1}; + enc.CopyTextureToBuffer(reinterpret_cast(&from), + reinterpret_cast(&to), + reinterpret_cast(&extent)); + wgpu::CommandBuffer cmd = enc.Finish(); + queue.Submit(1, &cmd); + + bool mapped = false; + WGPUBufferMapCallbackInfo cbInfo{}; + cbInfo.mode = WGPUCallbackMode_AllowProcessEvents; + cbInfo.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*message*/, void *userdata1, + void * /*userdata2*/) { + bool *ok = static_cast(userdata1); + *ok = (status == WGPUMapAsyncStatus_Success); + }; + cbInfo.userdata1 = &mapped; + wgpuBufferMapAsync(dst.Get(), WGPUMapMode_Read, 0, size, cbInfo); + + int guard = 0; + while (!mapped && guard < 2000) { +#if defined(__EMSCRIPTEN__) + emscripten_sleep(0); +#endif + wgpuInstanceProcessEvents(instance.Get()); + ++guard; + } + if (!mapped) return false; + + const uint8_t *data = static_cast(dst.GetConstMappedRange(0, size)); + if (!data) return false; + outRgba.resize(static_cast(width) * height * 4); + for (int y = 0; y < height; ++y) + std::memcpy(outRgba.data() + size_t(y) * width * 4, data + size_t(y) * bytesPerRow, + size_t(width) * 4); + dst.Unmap(); + return true; +} + +} // namespace + +Color Graphics::getPixelImpl(OffscreenCanvas *canvas, int x, int y) { + int w = canvas ? canvas->getWidth() : (sceneColorWidth > 0 ? sceneColorWidth : 1); + int h = canvas ? canvas->getHeight() : (sceneColorHeight > 0 ? sceneColorHeight : 1); + if (x < 0 || y < 0 || x >= w || y >= h) return Color(0.f, 0.f, 0.f, 0.f); + std::vector rgba; + wgpu::Texture src = canvas ? canvas->color : (sceneColorSlots.empty() ? nullptr : sceneColorSlots[0].color); + if (!src || !copyTextureToCpu(instance, device, queue, src, w, h, rgba)) return clearColor; + const uint8_t *p = rgba.data() + (size_t(y) * w + x) * 4; + return Color(p[0] / 255.f, p[1] / 255.f, p[2] / 255.f, p[3] / 255.f); +} + +image::ImageData *Graphics::newImageDataImpl(OffscreenCanvas *canvas) { + int w = canvas ? canvas->getWidth() : (sceneColorWidth > 0 ? sceneColorWidth : 1); + int h = canvas ? canvas->getHeight() : (sceneColorHeight > 0 ? sceneColorHeight : 1); + auto *img = new image::ImageData(w, h, "RGBA8"); + std::vector rgba; + wgpu::Texture src = canvas ? canvas->color : (sceneColorSlots.empty() ? nullptr : sceneColorSlots[0].color); + if (src && copyTextureToCpu(instance, device, queue, src, w, h, rgba)) { + std::memcpy(img->getData(), rgba.data(), rgba.size()); + } + return img; +} + +// --------------------------------------------------------------------------- +// Shader creation +// --------------------------------------------------------------------------- + +namespace { + +wgpu::RenderPipeline buildPipelineFromWgsl(wgpu::Device &dev, wgpu::PipelineLayout layout, + WGPUTextureFormat format, const std::string &vert, + const std::string &frag, bool depth, bool blend, + bool mesh3d, bool hair, bool shadow, bool gbuffer, + uint32_t sampleCount) { + WGPURenderPipelineDescriptor pd{}; + pd.label = sv("eve_custom_shader"); + pd.layout = layout.Get(); + + WGPUVertexAttribute attrs[3]{}; + WGPUVertexBufferLayout vb{}; + if (mesh3d || shadow || gbuffer) { + attrs[0].format = WGPUVertexFormat_Float32x3; + attrs[0].offset = 0; + attrs[0].shaderLocation = 0; + attrs[1].format = WGPUVertexFormat_Float32x3; + attrs[1].offset = 12; + attrs[1].shaderLocation = 1; + attrs[2].format = WGPUVertexFormat_Float32x2; + attrs[2].offset = 24; + attrs[2].shaderLocation = 2; + vb.arrayStride = 32; + vb.stepMode = WGPUVertexStepMode_Vertex; + vb.attributeCount = 3; + vb.attributes = attrs; + } else { + attrs[0].format = WGPUVertexFormat_Float32x2; + attrs[0].offset = 0; + attrs[0].shaderLocation = 0; + attrs[1].format = WGPUVertexFormat_Float32x4; + attrs[1].offset = 8; + attrs[1].shaderLocation = 1; + attrs[2].format = WGPUVertexFormat_Float32x2; + attrs[2].offset = 24; + attrs[2].shaderLocation = 2; + vb.arrayStride = 32; + vb.stepMode = WGPUVertexStepMode_Vertex; + vb.attributeCount = 3; + vb.attributes = attrs; + } + pd.vertex.bufferCount = 1; + pd.vertex.buffers = &vb; + + if (!vert.empty()) { + WGPUShaderModuleDescriptor md = mdDesc(vert); + wgpu::ShaderModule vm = dev.CreateShaderModule( + reinterpret_cast(&md)); + pd.vertex.module = vm.Get(); + pd.vertex.entryPoint = sv("vs_main"); + } else { + // Default textured vertex shader. + WGPUShaderModuleDescriptor md = mdDesc(kTexturedVertWgsl); + wgpu::ShaderModule vm = dev.CreateShaderModule( + reinterpret_cast(&md)); + pd.vertex.module = vm.Get(); + pd.vertex.entryPoint = sv("vs_main"); + } + + if (!frag.empty()) { + WGPUFragmentState fs{}; + WGPUShaderModuleDescriptor md = mdDesc(frag); + wgpu::ShaderModule fm = dev.CreateShaderModule( + reinterpret_cast(&md)); + fs.module = fm.Get(); + fs.entryPoint = sv("fs_main"); + fs.targetCount = 1; + WGPUColorTargetState target{}; + target.format = format; + if (blend) { + static WGPUBlendState bs = alphaBlend(); + target.blend = &bs; + } + fs.targets = ⌖ + pd.fragment = &fs; + } else { + pd.fragment = nullptr; + } + + pd.primitive.topology = WGPUPrimitiveTopology_TriangleList; + pd.primitive.frontFace = WGPUFrontFace_CCW; + pd.primitive.cullMode = hair ? WGPUCullMode_None : WGPUCullMode_None; + pd.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; + + if (depth && !shadow) { + static WGPUDepthStencilState ds{}; + ds.format = WGPUTextureFormat_Depth32Float; + ds.depthWriteEnabled = WGPUOptionalBool_True; + ds.depthCompare = WGPUCompareFunction_Less; + pd.depthStencil = &ds; + } + pd.multisample.count = sampleCount ? sampleCount : 1; + return dev.CreateRenderPipeline(reinterpret_cast(&pd)); +} + +} // namespace + +Shader *Graphics::newShaderFromSpv(const std::vector &vertSpv, + const std::vector &fragSpv) { + if (!device) throw Exception("newShaderFromSpv: device not initialized"); + // Native Dawn can consume Vulkan SPIR-V directly; the browser build cannot + // (browsers only accept WGSL). The WebGPU backend therefore requires WGSL + // for custom shaders; SPIR-V input is rejected with a clear message. + throw Exception("newShaderFromSpv: SPIR-V custom shaders are not supported on the " + "WebGPU backend (browsers accept WGSL only). Recompile the shader " + "with the WebGPU toolchain (glslc+tint) and use newShaderFromWgsl."); +} + +Shader *Graphics::newShaderFromSpvFile(const std::string &vertPath, const std::string &fragPath) { + throw Exception("newShaderFromSpvFile: SPIR-V custom shaders are not supported on the " + "WebGPU backend. Use WGSL shaders instead."); +} + +Shader *Graphics::newShader(const std::string &vertGlsl, const std::string &fragGlsl) { + (void)vertGlsl; + (void)fragGlsl; + throw Exception("newShader: runtime GLSL compilation is not available on the WebGPU " + "backend (browser WGSL only). Ship pre-compiled WGSL shaders."); +} + +Shader *Graphics::newMeshShaderFromSpv(const std::vector &vertSpv, + const std::vector &fragSpv) { + (void)vertSpv; + (void)fragSpv; + throw Exception("newMeshShaderFromSpv: SPIR-V custom mesh shaders are not supported on the " + "WebGPU backend. Use WGSL shaders instead."); +} + +Shader *Graphics::newMeshShader(const std::string &vertGlsl, const std::string &fragGlsl) { + (void)vertGlsl; + (void)fragGlsl; + throw Exception("newMeshShader: runtime GLSL compilation is not available on the WebGPU " + "backend. Ship pre-compiled WGSL shaders."); +} + +Shader *Graphics::newHairShaderFromSpv(const std::vector &vertSpv, + const std::vector &fragSpv) { + (void)vertSpv; + (void)fragSpv; + throw Exception("newHairShaderFromSpv: SPIR-V custom hair shaders are not supported on the " + "WebGPU backend. Use WGSL shaders instead."); +} + +} // namespace eve::graphics::webgpu + + + + + + diff --git a/src/modules/graphics/webgpu/Graphics.h b/src/modules/graphics/webgpu/Graphics.h new file mode 100644 index 00000000..e2fc12bc --- /dev/null +++ b/src/modules/graphics/webgpu/Graphics.h @@ -0,0 +1,539 @@ +#pragma once +#include "graphics/Graphics.h" +#include "graphics/Batcher.h" +#include "graphics/Texture.h" +#include "graphics/Mesh.h" +#include "graphics/Shader.h" +#include "graphics/Light.h" +#include "graphics/ClusteredLight.h" +#include "graphics/Shadow.h" + +#include + +#include +#include +#include +#include +#include + +#include + +namespace eve::graphics::webgpu { + +class OffscreenCanvas; + +/** + * Frame UBO for the mesh3d pipeline. Mirrors the std140 layout of the WGSL + * `Frame` block and the legacy Vulkan Mesh3DUBO (so custom shader prefixes stay + * compatible). See wgsl_shaders.h. + */ +struct Mesh3DUBO { + glm::mat4 mvp{1.f}; + glm::mat4 model{1.f}; + glm::vec4 lightDir{0.4f, 1.f, 0.3f, 0.f}; // xyz = primary dir; w = lightCount + glm::vec4 lightColor{1.f, 1.f, 1.f, 0.f}; // rgb = primary color; w = envIntensity + glm::vec4 tint{1.f, 1.f, 1.f, 1.f}; + glm::vec4 cameraPos{0.f, 0.f, 3.f, 0.45f}; // xyz = eye; w = roughness + glm::vec4 ambient{0.12f, 0.12f, 0.14f, 0.f}; // rgb = ambient; w = metallic + Light3DGpu lights[Lighting3DPack::kMaxLights]{}; + glm::vec4 texBomb{4.f, 0.f, 1.f, 0.f}; // x=cellScale, y=strength, z=rotAmount + glm::vec4 parallax{0.f, 8.f, 32.f, 0.f}; // x=scale, y=minLayers, z=maxLayers + glm::mat4 view{1.f}; + glm::vec4 clipInfo{0.1f, 100.f, 0.f, 0.f}; // x=near, y=far +}; +static_assert(sizeof(Mesh3DUBO) == 576, "Mesh3DUBO layout must match the WGSL Frame block"); + +/** + * Texture resources backed by a wgpu texture + view + sampler + bind groups. + */ +struct GpuTexture { + wgpu::Texture texture; + wgpu::TextureView view; + wgpu::Sampler sampler; + // Bind group for the unified 2D layout (color at 0, depth at 1). + wgpu::BindGroup tex2DGroup; + // Bind group for the mesh3d layout (albedo at 1, normal at 2, env at 3, + // height at 6). Rebuilt when the mesh3d pipeline re-creates its layout. + wgpu::BindGroup meshGroup; + bool isCube = false; + int width = 0; + int height = 0; + uint32_t mipLevels = 1; + TextureSampler samplerState{}; +}; + +/** + * Vertex/index buffers for one mesh. + */ +struct GpuMesh { + wgpu::Buffer vertexBuffer; + wgpu::Buffer indexBuffer; + uint32_t indexCount = 0; + uint32_t vertexCount = 0; + uint32_t vertexStride = 0; +}; + +/** + * A compiled shader: one WebGPU pipeline + layout. Also holds the WGSL + * sources so custom shaders can be re-pipelined for offscreen targets. + */ +struct GpuShader { + wgpu::RenderPipeline swapchainPipeline; // 2D/offscreen color format + wgpu::RenderPipeline offscreenPipeline; // RGBA8Unorm canvas format + wgpu::RenderPipeline mesh3dPipeline; // scene color format + wgpu::RenderPipeline shadowPipeline; // depth-only + wgpu::RenderPipeline gbufferPipeline; // MRT gbuffer + wgpu::PipelineLayout pipelineLayout; + wgpu::BindGroupLayout setLayout; + bool isMesh3D = false; + bool isHair3D = false; + bool isShadow = false; + bool isGbuffer = false; + std::string wgslVert; + std::string wgslFrag; +}; + +class Graphics final : public eve::graphics::Graphics { +public: + Graphics(); + ~Graphics() override; + + std::string getBackendName() const override { return "webgpu"; } + + void initWithWindow(void *nativeWindow) override; + void present() override; + void requestSurfaceRecreate() override { surfaceNeedsRecreate = true; } + void setVSync(bool enabled) override; + int getMsaaSamples() const override { return msaaSamples; } + void setViewportSize(int width, int height, int pixelwidth, int pixelheight) override; + void drawSolidRect(float x, float y, float w, float h, const Color &color) override; + + Texture *newTexture(int width, int height, const uint8_t *rgba, bool repeatU = false, + bool repeatV = false) override; + Texture *newTexture(int width, int height, const uint8_t *rgba, + const TextureCreateInfo &info) override; + Texture *newCubemap(int faceSize, const uint8_t *rgbaFaces) override; + Texture *newCubemap(int faceSize, const uint8_t *rgbaFaces, + const TextureCreateInfo &info) override; + Texture *newTexture(image::ImageData *data) override; + Texture *newTexture(image::ImageData *data, const TextureCreateInfo &info) override; + void setTextureSampler(Texture *texture, const TextureSampler &sampler) override; + float getMaxAnisotropy() const override; + Texture *newTextureFromFile(const std::string &filename) override; + bool reloadTextureFromFile(const std::string &filename) override; + + void drawTexturedRect(Texture *texture, float x, float y, float w, float h, + const Color &color) override; + void drawTexturedRectShader(Texture *texture, Shader *shader, float x, float y, float w, float h, + const Color &color) override; + void drawTexturedRectUV(Texture *texture, float x, float y, float w, float h, float u0, float v0, + float u1, float v1, const Color &color) override; + void drawTexturedRectShaderUV(Texture *texture, Shader *shader, float x, float y, float w, + float h, float u0, float v0, float u1, float v1, + const Color &color, bool rotatedUV = false) override; + void drawTexturedRectShaderUVRotated(Texture *texture, Shader *shader, float cx, float cy, + float w, float h, float degrees, float u0, float v0, + float u1, float v1, const Color &color, + bool rotatedUV = false) override; + void drawTexturedRectShaderDepth(Texture *color, Texture *depth, Shader *shader, float x, float y, + float w, float h, const Color &tint) override; + void drawTexturedRectLitUV(Texture *albedo, Texture *normal, float x, float y, float w, float h, + float u0, float v0, float u1, float v1, const Color &color) override; + void setLighting2D(const Lighting2DUBO &ubo) override; + + Shader *newShaderFromSpv(const std::vector &vertSpv, + const std::vector &fragSpv) override; + Shader *newShaderFromSpvFile(const std::string &vertPath, const std::string &fragPath) override; + Shader *newShader(const std::string &vertGlsl, const std::string &fragGlsl) override; + Shader *newMeshShaderFromSpv(const std::vector &vertSpv, + const std::vector &fragSpv) override; + Shader *newMeshShader(const std::string &vertGlsl, const std::string &fragGlsl) override; + Shader *newHairShaderFromSpv(const std::vector &vertSpv, + const std::vector &fragSpv) override; + + Mesh *newMeshFromAssimp(const ::aiMesh &mesh) override; + Mesh *newMeshFromAssimp(const ::aiMesh &mesh, const aiMatrix4x4 &worldTransform) override; + Mesh *newMeshFromArrays(const float *posXYZ, const float *nrmXYZ, const float *uvST, + int vertexCount, const uint32_t *indices, int indexCount) override; + bool bakeMeshMorph(Mesh *mesh) override; + Mesh *newMeshSphere(int slices = 32, int stacks = 16) override; + Mesh *newMeshCylinder(int slices = 32, int stacks = 1, bool caps = true) override; + + void begin3DFrame() override; + void setMesh3DViewProj(const glm::mat4 &viewProj) override; + void setMesh3DView(const glm::mat4 &view) override; + void setMesh3DClip(float nearZ, float farZ) override; + Texture *getSceneColorTexture() override; + void drawMesh(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint) override; + void drawMeshShader(Mesh *mesh, const glm::mat4 &model, Texture *texture, const Color &tint, + Shader *shader) override; + void drawVoxelFaceInstances(const uint32_t *packed, int count, float originX, float originY, + float originZ, const std::string &faceDir, Texture *atlas, + int tilesPerRow = 16) override; + void setMesh3DNormalTexture(Texture *normal) override; + void setMesh3DHeightTexture(Texture *height) override; + void setMesh3DMaterial(float metallic, float roughness) override; + void setMesh3DTexCellBomb(float cellScale, float strength, float rotAmount = 1.f) override; + void setMesh3DParallax(float scale, float minLayers = 8.f, float maxLayers = 32.f) override; + void setMesh3DLighting(const Lighting3DPack &pack) override; + void setMesh3DClusteredLighting(const ClusteredLightingUpload &upload) override; + void setMesh3DLight(const glm::vec3 &dir, const glm::vec3 &color) override; + void setMesh3DCameraPos(const glm::vec3 &eye) override; + void setMesh3DEnv(Texture *cube, float intensity) override; + void setMesh3DShadows(const ShadowUpload &upload) override; + void setMesh3DShadowReceive(bool receive) override; + void beginShadowPass(int cascadeIndex) override; + void drawMeshShadow(Mesh *mesh, const glm::mat4 &lightMVP) override; + void endShadowPass() override; + + void beginGBufferPass(int width, int height) override; + void drawMeshGBuffer(Mesh *mesh, const glm::mat4 &mvp, const glm::mat4 &model, float nearZ, + float farZ, Texture *albedo = nullptr, float tintR = 1.f, float tintG = 1.f, + float tintB = 1.f) override; + void endGBufferPass() override; + + Canvas *newCanvas(int width, int height) override; + void setCanvas(Canvas *canvas) override; + bool isCanvasActive() const override; + Canvas *getCanvas() const override; + + Texture *getTexture() override; + image::ImageData *newImageData() override; + void draw(eve::graphics::Graphics *gfx, const glm::mat4 &matrix) const override; + void draw(Canvas *C, const glm::mat4 &matrix) const override; + void clear(std::optional color, std::optional stencil, + std::optional depth) override; + Color getPixel(int x, int y) override; + + /** Flush accumulated 2D batches into an offscreen canvas target. */ + void flush2DToCanvas(OffscreenCanvas *canvas); + /** Blocking CPU readback of an offscreen canvas or scene color target. */ + Color getPixelImpl(OffscreenCanvas *canvas, int x, int y); + image::ImageData *newImageDataImpl(OffscreenCanvas *canvas); + + wgpu::Instance &getInstance() { return instance; } + wgpu::Device &getDevice() { return device; } + wgpu::Queue &getQueue() { return queue; } + wgpu::Surface &getSurface() { return surface; } + WGPUTextureFormat getSurfaceFormat() const { return surfaceFormat; } + void *getSdlWindow() const { return sdlWindow; } + bool isReady() const { return initialized; } + + // The present overlay is rendered inside the swapchain render pass. The + // void* payload is a WGPURenderPassEncoder* on the WebGPU backend. + using PresentOverlayFn = eve::graphics::Graphics::PresentOverlayFn; + + friend class OffscreenCanvas; + + struct TexturedBatch { + Texture *texture = nullptr; + Texture *depth = nullptr; + Shader *shader = nullptr; + Batcher batch; + }; + struct LitBatch { + Texture *albedo = nullptr; + Texture *normal = nullptr; + Batcher batch; + }; + +private: + struct Mesh3dDraw { + Mesh *mesh = nullptr; + Texture *texture = nullptr; + glm::mat4 model{1.f}; + Color tint{1.f}; + Shader *shader = nullptr; + uint32_t frameUboOffset = 0; + uint32_t pushUboOffset = 0; + uint32_t shadowUboOffset = 0; + }; + struct ShadowDraw { + Mesh *mesh = nullptr; + glm::mat4 mvp{1.f}; + }; + struct GbufferDraw { + Mesh *mesh = nullptr; + Texture *albedo = nullptr; + glm::mat4 mvp{1.f}; + glm::mat4 model{1.f}; + float nearZ = 0.1f; + float farZ = 100.f; + glm::vec4 tint{1.f}; + uint32_t pushUboOffset = 0; + }; + struct VoxelDraw { + uint32_t instanceBufferOffset = 0; + uint32_t count = 0; + GpuTexture *atlas = nullptr; + glm::mat4 viewProj{1.f}; + glm::vec4 chunkOrigin{0.f}; + glm::vec4 atlasInfo{16.f, 0.f, 0.f, 0.f}; + glm::vec4 tint{1.f}; + uint32_t pushUboOffset = 0; + }; + + void createInstanceAndAdapter(); + void requestDevice(); + void configureSurface(int width, int height); + void waitForAdapter(); + void waitForDevice(); + void createDefaultTextures(); + void createPipelineResources(); + void create2DPipelines(); + void createMesh3DPipelines(); + void createShadowPipelines(); + void createGbufferPipelines(); + void createVoxelPipelines(); + void createSceneColorResources(int width, int height); + void destroySceneColorResources(); + void createShadowResources(); + void destroyShadowResources(); + void createGbufferResources(int width, int height); + void destroyGbufferResources(); + + wgpu::RenderPipeline createPipelineForShader(GpuShader *gs, wgpu::TextureFormat format, + bool depth, bool mesh3d, bool hair, + bool shadow, bool gbuffer, + wgpu::PipelineLayout layout); + wgpu::BindGroupLayout make2DBindGroupLayout(); + wgpu::BindGroupLayout makeMesh3DBindGroupLayout(); + wgpu::BindGroupLayout makeShadowBindGroupLayout(); + wgpu::BindGroupLayout makeGbufferBindGroupLayout(); + wgpu::BindGroupLayout makeVoxelBindGroupLayout(); + wgpu::PipelineLayout make2DPipelineLayout(); + wgpu::PipelineLayout makeMesh3DPipelineLayout(); + wgpu::PipelineLayout makeShadowPipelineLayout(); + wgpu::PipelineLayout makeGbufferPipelineLayout(); + wgpu::PipelineLayout makeVoxelPipelineLayout(); + + GpuTexture *gpuForTexture(Texture *t) const; + GpuTexture *gpuForTextureOrWhite(Texture *t) const; + wgpu::BindGroup makeTex2DBindGroup(GpuTexture *color, GpuTexture *depth); + wgpu::BindGroup makeMeshBindGroup(GpuTexture *albedo, GpuTexture *normal, GpuTexture *env, + GpuTexture *height, uint32_t frameUboOffset, + uint32_t shadowUboOffset, uint32_t pushUboOffset); + void ensureMeshBindGroupsForDraw(Mesh3dDraw &d); + wgpu::Sampler makeSampler(const TextureSampler &sampler, uint32_t mipLevels) const; + + void uploadTexturePixels(GpuTexture *gt, const uint8_t *rgba, int w, int h, + const TextureCreateInfo &info); + void uploadTexturePixelsMips(GpuTexture *gt, const uint8_t *rgba, int w, int h); + void flush2D(wgpu::RenderPassEncoder pass, int viewW, int viewH, WGPUTextureFormat format); + void drawTexturedBatch(wgpu::RenderPassEncoder pass, TexturedBatch &tb, int viewW, int viewH, + WGPUTextureFormat format, bool offscreen); + void drawLitBatch(wgpu::RenderPassEncoder pass, LitBatch &lb, int viewW, int viewH, + WGPUTextureFormat format); + void flushMesh3D(wgpu::RenderPassEncoder pass, WGPUTextureFormat format); + void flushShadowPass(wgpu::RenderPassEncoder pass); + void flushGbufferPass(wgpu::RenderPassEncoder pass); + void flushVoxelDraws(wgpu::RenderPassEncoder pass, WGPUTextureFormat format); + + // UBO arena: one growable uniform buffer per in-flight frame slot. + struct UboArena { + wgpu::Buffer buffer; + uint64_t capacity = 0; + uint64_t used = 0; + uint32_t alloc(uint64_t size, uint64_t alignment); + void reset() { used = 0; } + }; + UboArena ¤tUboArena(); + void ensureUboArena(UboArena &arena, uint64_t bytes); + + // Vertex arena for batched 2D vertices (one per frame slot). + struct VertexArena { + wgpu::Buffer buffer; + uint64_t capacity = 0; + uint64_t used = 0; + uint64_t alloc(uint64_t bytes); + void reset() { used = 0; } + }; + VertexArena ¤tVertexArena(); + void ensureVertexArena(VertexArena &arena, uint64_t bytes); + + uint32_t frameSlotCount() const { return kFramesInFlight; } + uint32_t currentFrameSlot() const { return frameIndex % kFramesInFlight; } + + // ---- state ---- + bool initialized = false; + bool deviceInitDone = false; + void *sdlWindow = nullptr; + int logicalW = 0, logicalH = 0; + int pixelW = 0, pixelH = 0; + float maxSamplerAnisotropy = 1.f; + + wgpu::Instance instance; + wgpu::Adapter adapter; + wgpu::Device device; + wgpu::Queue queue; + wgpu::Surface surface; + WGPUTextureFormat surfaceFormat = WGPUTextureFormat_BGRA8Unorm; + std::atomic surfaceNeedsRecreate{false}; + bool swapchainConfigured = false; + std::atomic adapterReceived{false}; + std::atomic deviceReceived{false}; + std::string adapterError; + std::string deviceError; + + // Per-frame command state (single command buffer per frame). + uint32_t frameIndex = 0; + static constexpr uint32_t kFramesInFlight = 2; + std::vector uboArenas; + std::vector vertexArenas; + wgpu::Buffer mesh3dFrameUboPool; + uint32_t mesh3dFrameUboSlots[kFramesInFlight]{}; + wgpu::Buffer shadowUboPool; + uint32_t shadowUboSlots[kFramesInFlight]{}; + wgpu::Buffer pushUboPool; + uint32_t pushUboSlots[kFramesInFlight]{}; + + // Default / placeholder resources. + GpuTexture *whiteTexture = nullptr; + GpuTexture *flatNormalTexture = nullptr; + GpuTexture *flatNormalTexture3D = nullptr; + GpuTexture *flatHeightTexture3D = nullptr; + GpuTexture *defaultEnvCubemap = nullptr; + GpuTexture *shadowDepthArray = nullptr; + + // Pipelines / layouts. + wgpu::PipelineLayout tex2DPipelineLayout; + wgpu::PipelineLayout mesh3dPipelineLayout; + wgpu::PipelineLayout shadowPipelineLayout; + wgpu::PipelineLayout gbufferPipelineLayout; + wgpu::PipelineLayout voxelPipelineLayout; + wgpu::BindGroupLayout tex2DSetLayout; + wgpu::BindGroupLayout mesh3dSetLayout; + wgpu::BindGroupLayout shadowSetLayout; + wgpu::BindGroupLayout gbufferSetLayout; + wgpu::BindGroupLayout voxelSetLayout; + // Shared filtering sampler for bindings declared as `sampler` in WGSL + // (e.g. mesh3d's @binding(7) mainSamp). + wgpu::Sampler mainSampler; + wgpu::RenderPipeline colorPipeline; // 2D solid + wgpu::RenderPipeline texturedPipeline; // 2D textured + wgpu::RenderPipeline mesh3dPipeline; + wgpu::RenderPipeline mesh3dShadowPipeline; + wgpu::RenderPipeline mesh3dGbufferPipeline; + wgpu::RenderPipeline voxelRectPipeline; + wgpu::RenderPipeline lit2dPipeline; + // RGBA8Unorm (offscreen canvas / scene) variants of the 2D pipelines. + wgpu::RenderPipeline offscreenColorPipeline; + wgpu::RenderPipeline offscreenTexturedPipeline; + wgpu::RenderPipeline offscreenLitPipeline; + // Fullscreen quad used to composite the scene color into the swapchain. + wgpu::Buffer fullscreenQuadVb; + wgpu::Buffer fullscreenQuadIb; + bool fullscreenQuadReady = false; + + // 2D batch state. + Batcher solidBatch; + std::vector texturedBatches; + std::vector litBatches; + Lighting2DUBO lighting2dFrame{}; + + // 3D frame state. + bool frame3DStarted = false; + bool sceneColorPassOpen = false; + glm::mat4 mesh3dViewProj{1.f}; + glm::mat4 mesh3dView{1.f}; + float mesh3dNear = 0.1f, mesh3dFar = 100.f; + Texture *sceneColorTexture = nullptr; + Texture *mesh3dNormalTexture = nullptr; + Texture *mesh3dHeightTexture = nullptr; + Texture *mesh3dEnvTexture = nullptr; + float mesh3dEnvIntensity = 0.f; + float mesh3dMetallic = 0.f; + float mesh3dRoughness = 0.45f; + float mesh3dTexBombScale = 4.f, mesh3dTexBombStrength = 0.f, mesh3dTexBombRot = 1.f; + float mesh3dParallaxScale = 0.f, mesh3dParallaxMin = 8.f, mesh3dParallaxMax = 32.f; + Lighting3DPack mesh3dLighting{}; + ShadowUpload mesh3dShadows{}; + bool mesh3dShadowReceive = true; + bool mesh3dClusteredActive = false; + ClusteredLightingUpload mesh3dClustered{}; + glm::vec3 mesh3dCameraPos{0.f, 0.f, 3.f}; + bool frameHad3DThisFrame = false; + std::vector mesh3dDraws; + Color clearColor{0.1f, 0.1f, 0.12f, 1.f}; + bool hasPendingClear = true; + + // Shadow pass state. + int shadowPassCascade = -1; + std::vector shadowPassDraws; + std::vector shadowCascadeDraws[ShadowConfig::kCascades]; + + // GBuffer pass state. + bool gbufferPassActive = false; + bool gbufferPassPending = false; + std::vector gbufferPassDraws; + + // Voxel state. + std::vector voxelDraws; + wgpu::Buffer voxelUnitQuadVerts; + wgpu::Buffer voxelUnitQuadIndices; + VertexArena voxelInstanceArena; + + // Scene color (offscreen 3D) target. + struct SceneColorSlot { + wgpu::Texture msaaColor; + wgpu::TextureView msaaView; + wgpu::Texture color; + wgpu::TextureView colorView; + wgpu::Texture depth; + wgpu::TextureView depthView; + GpuTexture colorGpu; + Texture colorTex; + uint32_t sampleCount = 1; + }; + int sceneColorWidth = 0, sceneColorHeight = 0; + WGPUTextureFormat sceneColorFormat = WGPUTextureFormat_RGBA8Unorm; + uint32_t sceneColorSamples = 1; + std::vector sceneColorSlots; + + // Shadow map state (CSM, 3 cascade layers). + struct ShadowMapSlot { + wgpu::Texture texture; + wgpu::TextureView view; + }; + std::vector shadowMaps; + int shadowMapSize = ShadowConfig::kMapSize; + + // GBuffer targets. + struct GbufferSlot { + wgpu::Texture normal; + wgpu::TextureView normalView; + wgpu::Texture depthColor; + wgpu::TextureView depthColorView; + wgpu::Texture albedo; + wgpu::TextureView albedoView; + wgpu::Texture depth; + wgpu::TextureView depthView; + GpuTexture normalGpu; + GpuTexture depthColorGpu; + GpuTexture albedoGpu; + Texture normalTex; + Texture depthColorTex; + Texture albedoTex; + }; + int gbufferWidth = 0, gbufferHeight = 0; + std::vector gbufferSlots; + + // Canvas state. + Canvas *activeCanvas = nullptr; + std::vector> ownedCanvases; + + // Owned resources. + std::vector> ownedTextures; + std::vector> ownedGpuTextures; + std::unordered_map texturesByPath; + std::vector> ownedMeshes; + std::vector> ownedGpuMeshes; + std::vector> ownedShaders; + std::vector> ownedGpuShaders; + + void markSwapchainDirty() override { swapchainConfigured = false; } + void rebuildSwapchainIfNeeded(); + bool acquireSurfaceTexture(wgpu::TextureView &view, wgpu::Texture &texture); +}; + +} // namespace eve::graphics::webgpu diff --git a/src/modules/graphics/webgpu/wgsl_shaders.h b/src/modules/graphics/webgpu/wgsl_shaders.h new file mode 100644 index 00000000..e6530284 --- /dev/null +++ b/src/modules/graphics/webgpu/wgsl_shaders.h @@ -0,0 +1,537 @@ +#pragma once +// WGSL shader sources for the WebGPU graphics backend. +// WebGPU has no push constants or combined image samplers, so every pipeline +// binds uniform buffers / textures / samplers explicitly through bind groups. +// The memory layouts match the Vulkan GLSL sources (std140) exactly so the +// existing C++ UBO structs (Mesh3DUBO, ShadowUBO, Lighting2DUBO) are reused. + +namespace eve::graphics::webgpu { + +// ---- 2D solid (color) ----------------------------------------------------- +inline const char *kColorVertWgsl = R"wgsl( +struct VSIn { + @location(0) pos: vec2f, + @location(1) color: vec4f, +}; +struct VSOut { + @builtin(position) pos: vec4f, + @location(0) color: vec4f, +}; +@vertex +fn vs_main(in: VSIn) -> VSOut { + var out: VSOut; + out.pos = vec4f(in.pos, 0.0, 1.0); + out.color = in.color; + return out; +} +)wgsl"; + +inline const char *kColorFragWgsl = R"wgsl( +struct FSIn { + @location(0) color: vec4f, +}; +@fragment +fn fs_main(in: FSIn) -> @location(0) vec4f { + return in.color; +} +)wgsl"; + +// ---- 2D textured ---------------------------------------------------------- +inline const char *kTexturedVertWgsl = R"wgsl( +struct VSIn { + @location(0) pos: vec2f, + @location(1) color: vec4f, + @location(2) uv: vec2f, +}; +struct VSOut { + @builtin(position) pos: vec4f, + @location(0) color: vec4f, + @location(1) uv: vec2f, +}; +@vertex +fn vs_main(in: VSIn) -> VSOut { + var out: VSOut; + out.pos = vec4f(in.pos, 0.0, 1.0); + out.color = in.color; + out.uv = in.uv; + return out; +} +)wgsl"; + +inline const char *kTexturedFragWgsl = R"wgsl( +struct FSIn { + @location(0) color: vec4f, + @location(1) uv: vec2f, +}; +@group(0) @binding(0) var mainTex: texture_2d; +@group(0) @binding(2) var mainSamp: sampler; +@fragment +fn fs_main(in: FSIn) -> @location(0) vec4f { + return textureSample(mainTex, mainSamp, in.uv) * in.color; +} +)wgsl"; + +// ---- 2D custom / post (Externals UBO: float data[32] == Shader push block) - +inline const char *kCustom2DFragWgsl = R"wgsl( +struct FSIn { + @location(0) color: vec4f, + @location(1) uv: vec2f, +}; +struct Externals { + data: array, +}; +@group(0) @binding(0) var mainTex: texture_2d; +@group(0) @binding(2) var mainSamp: sampler; +@group(0) @binding(4) var u: Externals; +@fragment +fn fs_main(in: FSIn) -> @location(0) vec4f { + return textureSample(mainTex, mainSamp, in.uv) * in.color; +} +)wgsl"; + +// ---- 2D lit --------------------------------------------------------------- +inline const char *kLit2DFragWgsl = R"wgsl( +struct FSIn { + @location(0) color: vec4f, + @location(1) uv: vec2f, +}; +struct Light2D { + posRadius: vec4f, + color: vec4f, +}; +struct Lighting2D { + ambient: vec4f, // rgb ambient + lightInfo: vec4f, // x = count, y = viewW, z = viewH + lights: array, +}; +@group(0) @binding(0) var albedoTex: texture_2d; +@group(0) @binding(1) var normalTex: texture_2d; +@group(0) @binding(2) var mainSamp: sampler; +@group(0) @binding(4) var u: Lighting2D; +@fragment +fn fs_main(in: FSIn) -> @location(0) vec4f { + let base = textureSample(albedoTex, mainSamp, in.uv) * in.color; + var lit = base.rgb * u.ambient.rgb; + let count = i32(u.lightInfo.x + 0.5); + let view = vec2f(u.lightInfo.y, u.lightInfo.z); + for (var i = 0; i < 8; i = i + 1) { + if (i >= count) { break; } + let l = u.lights[i]; + let px = in.uv * view; + if (l.posRadius.w <= 0.0) { + // Directional: light travels along posRadius.xy + let ndl = max(dot(vec3f(0.0, 0.0, 1.0), normalize(vec3f(l.posRadius.xy, 1.0))), 0.0); + lit += base.rgb * l.color.rgb * ndl; + } else { + let toL = l.posRadius.xy - px; + let dist = length(toL); + let att = clamp(1.0 - dist / max(l.posRadius.w, 1e-3), 0.0, 1.0); + lit += base.rgb * l.color.rgb * att * att; + } + } + return vec4f(lit, base.a); +} +)wgsl"; + +// ---- Mesh3D (full PBR, layout matches Mesh3DUBO / ShadowUBO) -------------- +inline const char *kMesh3DVertWgsl = R"wgsl( +struct VSIn { + @location(0) pos: vec3f, + @location(1) normal: vec3f, + @location(2) uv: vec2f, +}; +struct Light3D { + posRadius: vec4f, + color: vec4f, +}; +struct Frame { + mvp: mat4x4f, + model: mat4x4f, + lightDir: vec4f, + lightColor: vec4f, + tint: vec4f, + cameraPos: vec4f, + ambient: vec4f, + lights: array, + texBomb: vec4f, + parallax: vec4f, + view: mat4x4f, + clipInfo: vec4f, +}; +struct VSOut { + @builtin(position) pos: vec4f, + @location(0) vNormal: vec3f, + @location(1) vUV: vec2f, + @location(2) vTint: vec4f, + @location(3) vWorldPos: vec3f, + @location(4) vCameraPos: vec3f, + @location(5) vViewPos: vec3f, +}; +@group(0) @binding(0) var ubo: Frame; +// WGSL has no stdlib inverse(); implement 3x3 inverse for the normal matrix. +fn inverse3x3(m: mat3x3f) -> mat3x3f { + let a = m[0].x; let b = m[1].x; let c = m[2].x; + let d = m[0].y; let e = m[1].y; let f = m[2].y; + let g = m[0].z; let h = m[1].z; let i = m[2].z; + let det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g); + return mat3x3f( + vec3f((e * i - f * h) / det, (f * g - d * i) / det, (d * h - e * g) / det), + vec3f((c * h - b * i) / det, (a * i - c * g) / det, (b * g - a * h) / det), + vec3f((b * f - c * e) / det, (c * d - a * f) / det, (a * e - b * d) / det), + ); +} +@vertex +fn vs_main(in: VSIn) -> VSOut { + var out: VSOut; + out.pos = ubo.mvp * vec4f(in.pos, 1.0); + let world = ubo.model * vec4f(in.pos, 1.0); + out.vWorldPos = world.xyz; + out.vViewPos = (ubo.view * world).xyz; + let nrm = transpose(inverse3x3(mat3x3f(ubo.model[0].xyz, ubo.model[1].xyz, ubo.model[2].xyz))) * in.normal; + out.vNormal = normalize(nrm); + out.vUV = in.uv; + out.vTint = ubo.tint; + out.vCameraPos = ubo.cameraPos.xyz; + return out; +} +)wgsl"; + +inline const char *kMesh3DFragWgsl = R"wgsl( +struct Light3D { + posRadius: vec4f, + color: vec4f, +}; +struct Frame { + mvp: mat4x4f, + model: mat4x4f, + lightDir: vec4f, + lightColor: vec4f, + tint: vec4f, + cameraPos: vec4f, + ambient: vec4f, + lights: array, + texBomb: vec4f, + parallax: vec4f, + view: mat4x4f, + clipInfo: vec4f, +}; +struct ShadowFrame { + lightVP: array, + splits: vec4f, + bias: vec4f, + cascadeBias: vec4f, + cascadeTexel: vec4f, +}; +struct FSIn { + @location(0) vNormal: vec3f, + @location(1) vUV: vec2f, + @location(2) vTint: vec4f, + @location(3) vWorldPos: vec3f, + @location(4) vCameraPos: vec3f, + @location(5) vViewPos: vec3f, +}; +@group(0) @binding(0) var ubo: Frame; +@group(0) @binding(1) var albedoSampler: texture_2d; +@group(0) @binding(2) var normalSampler: texture_2d; +@group(0) @binding(3) var envSampler: texture_cube; +@group(0) @binding(4) var shadow: ShadowFrame; +@group(0) @binding(5) var shadowMap: texture_depth_2d_array; +@group(0) @binding(6) var heightSampler: texture_2d; +@group(0) @binding(7) var mainSamp: sampler; +@group(0) @binding(8) var shadowSamp: sampler_comparison; + +const PI: f32 = 3.14159265359; + +fn distGGX(n: vec3f, h: vec3f, rough: f32) -> f32 { + let a = max(rough * rough, 0.002); + let a2 = a * a; + let ndh = max(dot(n, h), 0.0); + let denom = (ndh * ndh * (a2 - 1.0) + 1.0); + return a2 / max(PI * denom * denom, 1e-4); +} +fn geomSchlick(ndv: f32, rough: f32) -> f32 { + let r = rough + 1.0; + let k = (r * r) / 8.0; + return ndv / max(ndv * (1.0 - k) + k, 1e-4); +} +fn geomSmith(n: vec3f, v: vec3f, l: vec3f, rough: f32) -> f32 { + return geomSchlick(max(dot(n, v), 0.0), rough) * geomSchlick(max(dot(n, l), 0.0), rough); +} +fn fresnelSchlick(cosT: f32, f0: vec3f) -> vec3f { + return f0 + (1.0 - f0) * pow(clamp(1.0 - cosT, 0.0, 1.0), 5.0); +} +fn shadeLight(n: vec3f, v: vec3f, albedo: vec3f, metallic: f32, rough: f32, l: vec3f, rad: vec3f) -> vec3f { + let ndl = max(dot(n, l), 0.0); + let diffuse = mix(ndl, ndl * 0.5 + 0.5, 0.25); + let h = normalize(v + l); + let f0 = mix(vec3f(0.04), albedo, metallic); + let ndf = distGGX(n, h, rough); + let g = geomSmith(n, v, l, rough); + let f = fresnelSchlick(max(dot(h, v), 0.0), f0); + let spec = (ndf * g * f) / max(4.0 * max(dot(n, v), 0.0) * max(ndl, 0.001), 1e-3); + let kd = (vec3f(1.0) - f) * (1.0 - metallic); + return (kd * albedo * diffuse + spec * ndl) * rad; +} +fn sampleShadowCascade(worldPos: vec3f, cascade: i32, bias: f32) -> f32 { + let lightClip = shadow.lightVP[cascade] * vec4f(worldPos, 1.0); + let ndc = lightClip.xyz / max(lightClip.w, 1e-6); + let uv = ndc.xy * 0.5 + 0.5; + let depth = ndc.z; + if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 || depth < 0.0 || depth > 1.0) { + return 1.0; + } + let texel = 1.0 / vec2f(textureDimensions(shadowMap)); + var sum: f32 = 0.0; + let zref = depth - bias; + for (var i = 0; i < 9; i = i + 1) { + var s: vec2f; + if (i == 0) { s = uv + vec2f(-0.5, -0.5) * texel; } + else if (i == 1) { s = uv + vec2f(0.0, -0.6) * texel; } + else if (i == 2) { s = uv + vec2f(0.5, -0.5) * texel; } + else if (i == 3) { s = uv + vec2f(-0.6, 0.0) * texel; } + else if (i == 4) { s = uv; } + else if (i == 5) { s = uv + vec2f(0.6, 0.0) * texel; } + else if (i == 6) { s = uv + vec2f(-0.5, 0.5) * texel; } + else if (i == 7) { s = uv + vec2f(0.0, 0.6) * texel; } + else { s = uv + vec2f(0.5, 0.5) * texel; } + if (s.x >= 0.0 && s.x <= 1.0 && s.y >= 0.0 && s.y <= 1.0) { + sum += textureSampleCompareLevel(shadowMap, shadowSamp, s, cascade, zref); + } + } + return sum / 9.0; +} +fn sampleShadowPCF(worldPos: vec3f, n: vec3f, viewDepth: f32, ndl: f32) -> f32 { + if (shadow.bias.y < 0.5 || shadow.bias.z < 0.5 || shadow.splits.w < 1e-4) { + return 1.0; + } + var cascade: i32 = 2; + if (viewDepth < shadow.splits.x) { cascade = 0; } + else if (viewDepth < shadow.splits.y) { cascade = 1; } + var tw: f32; + if (cascade == 0) { tw = shadow.cascadeTexel.x; } + else if (cascade == 1) { tw = shadow.cascadeTexel.y; } + else { tw = shadow.cascadeTexel.z; } + var cb: f32; + if (cascade == 0) { cb = shadow.cascadeBias.x; } + else if (cascade == 1) { cb = shadow.cascadeBias.y; } + else { cb = shadow.cascadeBias.z; } + if (cb < 1e-8) { cb = shadow.bias.x; } + let bias = cb * mix(0.75, 1.0, clamp(ndl, 0.0, 1.0)); + let p = worldPos + n * ((2.0 * max(tw, 1e-6)) / max(ndl, 0.2)); + let vis = sampleShadowCascade(p, cascade, bias); + return mix(0.04, 1.0, vis); +} +@fragment +fn fs_main(in: FSIn) -> @location(0) vec4f { + var nGeom = normalize(in.vNormal); + let v = normalize(in.vCameraPos - in.vWorldPos); + if (dot(nGeom, v) < 0.0) { nGeom = -nGeom; } + let base = textureSample(albedoSampler, mainSamp, in.vUV) * in.vTint; + if (base.a < 0.5) { discard; } + let albedo = base.rgb; + let metallic = clamp(ubo.ambient.w, 0.0, 1.0); + let rough = clamp(ubo.cameraPos.w, 0.04, 1.0); + let count = i32(ubo.lightDir.w + 0.5); + let nSmp = textureSample(normalSampler, mainSamp, in.vUV).xyz; + var n = nGeom; + if (length(nSmp - vec3f(0.5, 0.5, 1.0)) > 0.04) { + let mapN = nSmp * 2.0 - 1.0; + n = normalize(mapN); + } + var lo = vec3f(0.0); + let viewDepth = max(-in.vViewPos.z, 0.0); + let primaryL = normalize(ubo.lightDir.xyz); + let shadowVis = sampleShadowPCF(in.vWorldPos, n, viewDepth, max(dot(n, primaryL), 0.0)); + if (length(ubo.lightColor.rgb) > 1e-6) { + lo += shadeLight(n, v, albedo, metallic, rough, primaryL, ubo.lightColor.rgb) * shadowVis; + } + for (var i = 0; i < 8; i = i + 1) { + if (i >= count) { break; } + let lgt = ubo.lights[i]; + var rad = lgt.color.rgb; + var l: vec3f; + if (lgt.posRadius.w <= 0.0) { + l = normalize(lgt.posRadius.xyz); + if (length(l - primaryL) < 1e-3) { continue; } + } else { + let toL = lgt.posRadius.xyz - in.vWorldPos; + let dist = length(toL); + l = toL / max(dist, 1e-4); + let atten = clamp(1.0 - dist / max(lgt.posRadius.w, 1e-3), 0.0, 1.0); + rad *= atten * atten; + } + lo += shadeLight(n, v, albedo, metallic, rough, l, rad); + } + let hemi = clamp(n.y * 0.5 + 0.5, 0.0, 1.0); + let skyIrr = ubo.ambient.rgb * 1.1 + ubo.lightColor.rgb * 0.12; + let gndIrr = ubo.ambient.rgb * vec3f(0.72, 0.62, 0.52); + let irr = mix(gndIrr, skyIrr, hemi); + var color = albedo * irr * (1.0 - metallic) + lo; + let envIntensity = ubo.lightColor.w; + if (envIntensity > 1e-4) { + let r = reflect(-v, n); + let envSpec = textureSampleLevel(envSampler, mainSamp, r, rough * 5.0).rgb * envIntensity; + let f0 = mix(vec3f(0.04), albedo, metallic); + let f = fresnelSchlick(max(dot(n, v), 0.0), f0); + color += envSpec * f; + let irr2 = textureSampleLevel(envSampler, mainSamp, n, 5.0).rgb * envIntensity; + color += albedo * irr2 * (1.0 - metallic) * (1.0 - f) * 0.45; + } + color = color / (color + vec3f(1.0)); + let nearZ = max(ubo.clipInfo.x, 1e-4); + let farZ = max(ubo.clipInfo.y, nearZ + 1e-3); + let viewZ = max(-in.vViewPos.z, 0.0); + let linear01 = clamp((viewZ - nearZ) / (farZ - nearZ), 0.0, 1.0); + return vec4f(color, linear01); +} +)wgsl"; + +// ---- Mesh3D depth-only shadow -------------------------------------------------- +inline const char *kMesh3DShadowVertWgsl = R"wgsl( +struct VSIn { + @location(0) pos: vec3f, + @location(1) normal: vec3f, + @location(2) uv: vec2f, +}; +struct Push { + mvp: mat4x4f, +}; +@group(0) @binding(0) var pc: Push; +@vertex +fn vs_main(in: VSIn) -> @builtin(position) vec4f { + return pc.mvp * vec4f(in.pos, 1.0); +} +)wgsl"; + +// ---- GBuffer pass (normal / linear-depth / albedo) ------------------------- +inline const char *kMesh3DGbufferVertWgsl = R"wgsl( +struct VSIn { + @location(0) pos: vec3f, + @location(1) normal: vec3f, + @location(2) uv: vec2f, +}; +struct Push { + mvp: mat4x4f, + modelR0: vec4f, + modelR1: vec4f, + modelR2: vec4f, + clip: vec4f, +}; +struct VSOut { + @builtin(position) pos: vec4f, + @location(0) vNormal: vec3f, + @location(1) vNdcZ: f32, + @location(2) vUV: vec2f, +}; +@group(0) @binding(0) var pc: Push; +@vertex +fn vs_main(in: VSIn) -> VSOut { + var out: VSOut; + out.pos = pc.mvp * vec4f(in.pos, 1.0); + let nrm = mat3x3f(pc.modelR0.xyz, pc.modelR1.xyz, pc.modelR2.xyz) * in.normal; + out.vNormal = normalize(nrm); + out.vNdcZ = out.pos.z; + out.vUV = in.uv; + return out; +} +)wgsl"; + +inline const char *kMesh3DGbufferFragWgsl = R"wgsl( +struct FSIn { + @location(0) vNormal: vec3f, + @location(1) vNdcZ: f32, + @location(2) vUV: vec2f, +}; +struct Push { + mvp: mat4x4f, + modelR0: vec4f, + modelR1: vec4f, + modelR2: vec4f, + clip: vec4f, +}; +struct GBufOut { + @location(0) normal: vec4f, + @location(1) depthColor: vec4f, + @location(2) albedo: vec4f, +}; +@group(0) @binding(0) var pc: Push; +@group(0) @binding(1) var albedoSampler: texture_2d; +@group(0) @binding(2) var mainSamp: sampler; +@fragment +fn fs_main(in: FSIn) -> GBufOut { + var out: GBufOut; + out.normal = vec4f(in.vNormal * 0.5 + 0.5, 1.0); + let nearZ = max(pc.clip.x, 1e-4); + let farZ = max(pc.clip.y, nearZ + 1e-3); + // NDC z -> linear [0,1] over the clip range (matches scene color A). + let ndc = clamp(in.vNdcZ, 0.0, 1.0); + let linear = nearZ * farZ / max(farZ - ndc * (farZ - nearZ), 1e-6); + out.depthColor = vec4f(linear, linear, linear, 1.0); + out.albedo = textureSample(albedoSampler, mainSamp, in.vUV); + return out; +} +)wgsl"; + +// ---- Voxel rect -------------------------------------------------------------- +inline const char *kVoxelRectVertWgsl = R"wgsl( +struct VSIn { + @location(0) corner: vec2f, + @location(1) packed: u32, +}; +struct PC { + viewProj: mat4x4f, + chunkOrigin: vec4f, + atlasInfo: vec4f, + tint: vec4f, +}; +struct VSOut { + @builtin(position) pos: vec4f, + @location(0) uv: vec2f, + @location(1) tint: vec4f, +}; +@group(0) @binding(0) var pc: PC; +@vertex +fn vs_main(in: VSIn) -> VSOut { + var out: VSOut; + // packed: x(5) y(5) z(5) w(5) h(5) tex(7) + let px = f32(in.packed & 31u); + let py = f32((in.packed >> 5u) & 31u); + let pz = f32((in.packed >> 10u) & 31u); + let w = f32((in.packed >> 15u) & 31u); + let h = f32((in.packed >> 20u) & 31u); + let tex = f32((in.packed >> 25u) & 127u); + var world: vec3f = pc.chunkOrigin.xyz + vec3f(px, py, pz); + let face = i32(pc.chunkOrigin.w + 0.5); + var u: f32 = in.corner.x; + var v: f32 = in.corner.y; + if (face == 0) { world += vec3f(0.0, v * h, u * w); } + else if (face == 1) { world += vec3f(0.0, v * h, w - u * w); } + else if (face == 2) { world += vec3f(u * w, 0.0, v * h); } + else if (face == 3) { world += vec3f(w - u * w, 0.0, v * h); } + else if (face == 4) { world += vec3f(u * w, v * h, 0.0); } + else { world += vec3f(w - u * w, v * h, 0.0); } + out.pos = pc.viewProj * vec4f(world, 1.0); + let tiles = max(pc.atlasInfo.x, 1.0); + let tw = 1.0 / tiles; + let col = floor(tex / tiles); + let row = tex - col * tiles; + out.uv = vec2f((u + row) * tw, (v + col) * tw); + out.tint = pc.tint; + return out; +} +)wgsl"; + +inline const char *kVoxelRectFragWgsl = R"wgsl( +struct FSIn { + @location(0) uv: vec2f, + @location(1) tint: vec4f, +}; +@group(0) @binding(1) var atlas: texture_2d; +@group(0) @binding(2) var atlasSamp: sampler; +@fragment +fn fs_main(in: FSIn) -> @location(0) vec4f { + return textureSample(atlas, atlasSamp, in.uv) * in.tint; +} +)wgsl"; + +} // namespace eve::graphics::webgpu diff --git a/src/modules/system/System.cpp b/src/modules/system/System.cpp index 7df9206a..d7f41da1 100644 --- a/src/modules/system/System.cpp +++ b/src/modules/system/System.cpp @@ -4,7 +4,9 @@ #include "common/Module.h" #include "common/config.h" #include "graphics/Graphics.h" +#ifndef EVENGINE_WEBGPU #include "graphics/vulkan/Graphics.h" +#endif #include @@ -32,17 +34,21 @@ namespace eve::system { namespace { +#ifndef EVENGINE_WEBGPU eve::graphics::vulkan::Graphics *vulkanGraphicsOrNull() { auto *base = eve::ModuleManager::getInstance("Graphics"); if (!base) return nullptr; return dynamic_cast(base); } +#endif +#ifndef EVENGINE_WEBGPU bool gpuReady(eve::graphics::vulkan::Graphics *gfx) { if (!gfx) return false; vk::PhysicalDevice pd = gfx->getDevice().physical_device; return static_cast(pd) != VK_NULL_HANDLE; } +#endif std::string vendorNameFromId(uint32_t vendorID) { switch (vendorID) { @@ -61,6 +67,7 @@ std::string vendorNameFromId(uint32_t vendorID) { } } +#ifndef EVENGINE_WEBGPU const char *deviceTypeName(vk::PhysicalDeviceType t) { switch (t) { case vk::PhysicalDeviceType::eDiscreteGpu: return "discrete"; @@ -70,6 +77,7 @@ const char *deviceTypeName(vk::PhysicalDeviceType t) { default: return "other"; } } +#endif } // namespace @@ -200,24 +208,39 @@ void System::setClipboardText(const std::string &text) { } std::string System::getGpuName() const { +#ifdef EVENGINE_WEBGPU + return {}; +#else auto *gfx = vulkanGraphicsOrNull(); if (!gpuReady(gfx)) return {}; return gfx->getDevice().physical_device.properties.deviceName.data(); +#endif } std::string System::getGpuVendor() const { +#ifdef EVENGINE_WEBGPU + return {}; +#else auto *gfx = vulkanGraphicsOrNull(); if (!gpuReady(gfx)) return {}; return vendorNameFromId(gfx->getDevice().physical_device.properties.vendorID); +#endif } std::string System::getGpuDeviceType() const { +#ifdef EVENGINE_WEBGPU + return "webgpu"; +#else auto *gfx = vulkanGraphicsOrNull(); if (!gpuReady(gfx)) return {}; return deviceTypeName(gfx->getDevice().physical_device.properties.deviceType); +#endif } int System::getGpuMemoryTotalMB() const { +#ifdef EVENGINE_WEBGPU + return 0; +#else auto *gfx = vulkanGraphicsOrNull(); if (!gpuReady(gfx)) return 0; const auto &mem = gfx->getDevice().physical_device.memory_properties; @@ -227,6 +250,7 @@ int System::getGpuMemoryTotalMB() const { bytes += mem.memoryHeaps[i].size; } return static_cast(bytes / (1024ull * 1024ull)); +#endif } void System::expose(ssq::Table &table) { diff --git a/src/modules/ui/imgui/ImGuiBackend.cpp b/src/modules/ui/imgui/ImGuiBackend.cpp index 5d2f9cde..7c492d94 100644 --- a/src/modules/ui/imgui/ImGuiBackend.cpp +++ b/src/modules/ui/imgui/ImGuiBackend.cpp @@ -1,15 +1,21 @@ #include "ui/imgui/ImGuiBackend.h" +#include "ui/Theme.h" #include "common/Exception.h" #include "common/config.h" #include "graphics/Graphics.h" -#include "graphics/vulkan/Graphics.h" -#include "ui/Theme.h" -#include "vkbuilder.hpp" #include #include + +#ifdef EVENGINE_WEBGPU +#include "graphics/webgpu/Graphics.h" +#include "imgui_impl_wgpu.h" +#else +#include "graphics/vulkan/Graphics.h" #include +#include +#endif #include #include @@ -17,16 +23,17 @@ #include #include #include -#include namespace eve::ui { namespace { +#ifndef EVENGINE_WEBGPU void checkVk(VkResult err) { if (err == 0) return; throw eve::Exception("ImGui Vulkan error: VkResult = %d", int(err)); } +#endif /** Base glyph size in logical px (before the DPI scale is applied). */ constexpr float kBaseFontSizePx = 16.f; @@ -90,22 +97,29 @@ bool ImGuiBackend::init(SDL_Window *window, eve::graphics::Graphics *gfx) { if (initialized_) return true; if (!window || !gfx) return false; - auto *vkg = dynamic_cast(gfx); - if (!vkg) return false; - if (!vkg->getSwapchainRenderPass()) return false; - gfx_ = gfx; window_ = window; uiScale_ = computeInitialScale(); IMGUI_CHECKVERSION(); ImGui::CreateContext(); - // Start from unified theme tokens (not a one-off ImGui palette). setThemeUiScale(uiScale_); applyThemeToImGui(globalTheme(), uiScale_); ImGui_ImplSDL2_InitForVulkan(window); +#ifdef EVENGINE_WEBGPU + auto *wgg = dynamic_cast(gfx); + if (!wgg) return false; + ImGui_ImplWGPU_Init(wgg->getDevice().Get(), 2, wgg->getSurfaceFormat()); + loadFonts(); + ImGui_ImplWGPU_CreateFontsTexture(); + fontsUploaded_ = true; +#else + auto *vkg = dynamic_cast(gfx); + if (!vkg) return false; + if (!vkg->getSwapchainRenderPass()) return false; + auto &device = vkg->getDevice(); VkDescriptorPoolSize poolSizes[] = { {VK_DESCRIPTOR_TYPE_SAMPLER, 1000}, @@ -134,8 +148,6 @@ bool ImGuiBackend::init(SDL_Window *window, eve::graphics::Graphics *gfx) { uint32_t imageCount = vkg->getSwapchainImageCount(); if (imageCount < 2) imageCount = 2; - // The UI MSAA render pass must exist (and stay stable) before ImGui builds - // its pipeline against it; the sample count must match that render pass. vkg->ensureUiColorResources(); ImGui_ImplVulkan_InitInfo initInfo{}; @@ -163,6 +175,7 @@ bool ImGuiBackend::init(SDL_Window *window, eve::graphics::Graphics *gfx) { }); ImGui_ImplVulkan_DestroyFontUploadObjects(); fontsUploaded_ = true; +#endif gfx_->setPresentOverlay(&ImGuiBackend::presentOverlayThunk, this); @@ -179,18 +192,22 @@ void ImGuiBackend::shutdown() { if (gfx_) { if (gfx_->getPresentOverlayUser() == this) gfx_->setPresentOverlay(nullptr, nullptr); } +#ifdef EVENGINE_WEBGPU + ImGui_ImplWGPU_Shutdown(); +#else auto *vkg = dynamic_cast(gfx_); if (vkg) { vkDeviceWaitIdle(static_cast(vkg->getDevice().instance)); } ImGui_ImplVulkan_Shutdown(); - ImGui_ImplSDL2_Shutdown(); - ImGui::DestroyContext(); if (imguiDescriptorPool_ && vkg) { vkDestroyDescriptorPool(static_cast(vkg->getDevice().instance), static_cast(imguiDescriptorPool_), nullptr); imguiDescriptorPool_ = nullptr; } +#endif + ImGui_ImplSDL2_Shutdown(); + ImGui::DestroyContext(); gfx_ = nullptr; window_ = nullptr; fontsUploaded_ = false; @@ -204,13 +221,15 @@ void ImGuiBackend::processEvent(const SDL_Event *event) { void ImGuiBackend::newFrame() { if (!initialized_ || !window_) return; - // present() may soft-skip on Android while the surface settles after - // orientation change; close the previous ImGui frame so NewFrame is safe. if (frameOpen_) { ImGui::EndFrame(); frameOpen_ = false; } +#ifdef EVENGINE_WEBGPU + ImGui_ImplWGPU_NewFrame(); +#else ImGui_ImplVulkan_NewFrame(); +#endif ImGui_ImplSDL2_NewFrame(window_); ImGui::NewFrame(); frameOpen_ = true; @@ -222,11 +241,7 @@ void ImGuiBackend::applyScale(float scale) { const bool changed = scale != uiScale_; uiScale_ = scale; setThemeUiScale(scale); - // Re-apply design tokens so geometry stays in sync with theme + DPI. applyThemeToImGui(globalTheme(), scale); - // The font atlas is rasterized at physical-pixel size, so a scale change - // must re-rasterize it rather than rely on FontGlobalScale (which would - // blur the glyphs again). if (changed) rebuildFonts(); } @@ -237,17 +252,19 @@ void ImGuiBackend::setScale(float scale) { float ImGuiBackend::computeInitialScale() const { #if defined(EVENGINE_ANDROID) || defined(EVENGINE_IOS) - // Readable size on phone/tablet (mdpi = 160). float ddpi = 160.f; if (SDL_GetDisplayDPI(0, &ddpi, nullptr, nullptr) != 0 || ddpi < 1.f) ddpi = 320.f; float s = ddpi / 160.f; return std::clamp(s, 1.75f, 3.25f); #else - // Desktop: match the native window DPI scale (drawable / logical size). - // Requires SDL_WINDOW_ALLOW_HIGHDPI to report a scale > 1 on HiDPI displays. int logicalW = 0, logicalH = 0, pixelW = 0, pixelH = 0; SDL_GetWindowSize(window_, &logicalW, &logicalH); +#ifdef EVENGINE_WEBGPU + // No GL context on WebGPU; drawable size == window size for the canvas. + SDL_GetWindowSize(window_, &pixelW, &pixelH); +#else SDL_GL_GetDrawableSize(window_, &pixelW, &pixelH); +#endif if (logicalW > 0 && pixelW > 0) { float s = float(pixelW) / float(logicalW); if (s > 0.f) return std::clamp(s, 1.f, 4.f); @@ -261,7 +278,6 @@ void ImGuiBackend::loadFonts() { ImFontAtlas *atlas = io.Fonts; if (!atlas) return; - // Base glyph size in logical px at uiScale=1; physical size = base * uiScale. const float sizePx = kBaseFontSizePx * uiScale_; ImFontConfig cfg{}; @@ -278,8 +294,6 @@ void ImGuiBackend::loadFonts() { } if (!added) atlas->AddFontDefault(&cfg); - // Best-effort icon-font merge (FontAwesome, private-use glyphs). Missing - // file is silently ignored so the default glyphs above still render. ImFontConfig iconCfg{}; iconCfg.OversampleH = 2; iconCfg.OversampleV = 2; @@ -293,6 +307,12 @@ void ImGuiBackend::loadFonts() { } void ImGuiBackend::rebuildFonts() { +#ifdef EVENGINE_WEBGPU + loadFonts(); + ImGui_ImplWGPU_InvalidateDeviceObjects(); + ImGui_ImplWGPU_CreateFontsTexture(); + fontsUploaded_ = true; +#else auto *vkg = dynamic_cast(gfx_); if (!vkg) return; loadFonts(); @@ -306,6 +326,7 @@ void ImGuiBackend::rebuildFonts() { }); ImGui_ImplVulkan_DestroyFontUploadObjects(); fontsUploaded_ = true; +#endif } bool ImGuiBackend::wantCaptureMouse() const { @@ -318,12 +339,17 @@ bool ImGuiBackend::wantCaptureKeyboard() const { return ImGui::GetIO().WantCaptureKeyboard; } -void ImGuiBackend::renderDrawData(void *vkCommandBuffer) { - if (!initialized_ || !vkCommandBuffer) return; +void ImGuiBackend::renderDrawData(void *commandBuffer) { + if (!initialized_ || !commandBuffer) return; ImGui::Render(); frameOpen_ = false; +#ifdef EVENGINE_WEBGPU + WGPURenderPassEncoder enc = *static_cast(commandBuffer); + ImGui_ImplWGPU_RenderDrawData(ImGui::GetDrawData(), enc); +#else ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), - static_cast(vkCommandBuffer)); + static_cast(commandBuffer)); +#endif } void ImGuiBackend::presentOverlayThunk(void *userdata, void *commandBuffer) { diff --git a/src/modules/ui/imgui/imgui_impl_wgpu.cpp b/src/modules/ui/imgui/imgui_impl_wgpu.cpp new file mode 100644 index 00000000..698df0ff --- /dev/null +++ b/src/modules/ui/imgui/imgui_impl_wgpu.cpp @@ -0,0 +1,456 @@ +// dear imgui: Renderer Backend for WebGPU (wgpu), adapted for EVEngine's +// vendored imgui v1.83. Uses the stable webgpu.h C API so the same file +// compiles against Dawn (native) and Emscripten (browser). +// +// This is a self-contained renderer: imgui draws are issued into an existing +// WGPURenderPassEncoder supplied by the host (EVEngine's present overlay). + +// The source scanner compiles every *.cpp under ui/imgui/ on all platforms; +// keep this TU empty when the WebGPU backend is not active. +// config.h must be included before the guard: EVENGINE_WEBGPU is defined by +// config.h, not by a -D flag, so including it inside the guard would make this +// TU always empty. +#include "common/config.h" + +#if defined(EVENGINE_WEBGPU) + +#include "imgui_impl_wgpu.h" + +#include +#include + +// Avoid clashes with Windows headers. +#if defined(WIN32) && defined(min) +#undef min +#undef max +#endif + +//------------------------------------------------------------------------- +// Configuration +//------------------------------------------------------------------------- +#ifndef IMGUI_IMPL_WEBGPU_DEFAULT_FONT_SAMPLER +#define IMGUI_IMPL_WEBGPU_DEFAULT_FONT_SAMPLER 0 +#endif + +//----------------------------------------------------------------------------- +// Implementation +//----------------------------------------------------------------------------- + +struct ImGui_ImplWGPU_Data { + WGPUDevice wgpuDevice = nullptr; + WGPUTextureFormat rtFormat = WGPUTextureFormat_BGRA8Unorm; + + WGPUQueue defaultQueue = nullptr; + + WGPUBindGroupLayout bindGroupLayout = nullptr; + WGPUBindGroup bindGroup = nullptr; + WGPUBuffer uniformBuffer = nullptr; + uint32_t uniformBufferSize = 0; + WGPURenderPipeline renderPipeline = nullptr; + + WGPUTexture fontTexture = nullptr; + WGPUTextureView fontTextureView = nullptr; + WGPUSampler fontSampler = nullptr; + + int numFramesInFlight = 1; + int frameIndex = 0; +}; + +// Backend data stored in io.BackendRendererUserData to allow support for multiple +// Dear ImGui contexts. +static ImGui_ImplWGPU_Data *ImGui_ImplWGPU_GetBackendData() { + return ImGui::GetCurrentContext() ? (ImGui_ImplWGPU_Data *)ImGui::GetIO().BackendRendererUserData + : nullptr; +} + +static const char *ImGui_ImplWGPU_GetWGSLVertexShader() { + return R"( +struct Uniforms { + mvp: mat4x4f, +}; +struct VertexInput { + @location(0) position: vec2f, + @location(1) uv: vec2f, + @location(2) color: vec4f, +}; +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) uv: vec2f, + @location(1) color: vec4f, +}; +@group(0) @binding(0) var uniforms: Uniforms; +@vertex +fn vs_main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.position = uniforms.mvp * vec4f(in.position, 0.0, 1.0); + out.uv = in.uv; + out.color = in.color; + return out; +} +)"; +} + +static const char *ImGui_ImplWGPU_GetWGSLFragmentShader() { + return R"( +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) uv: vec2f, + @location(1) color: vec4f, +}; +@group(0) @binding(1) var fontSampler: sampler; +@group(0) @binding(2) var fontTexture: texture_2d; +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4f { + return textureSample(fontTexture, fontSampler, in.uv) * in.color; +} +)"; +} + +static void ImGui_ImplWGPU_SetupRenderState(ImDrawData *draw_data, + WGPURenderPassEncoder pass_encoder) { + ImGui_ImplWGPU_Data *bd = ImGui_ImplWGPU_GetBackendData(); + + // Setup orthographic projection matrix into our constant buffer. + { + float L = draw_data->DisplayPos.x; + float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x; + float T = draw_data->DisplayPos.y; + float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y; + float mvp[4][4] = { + {2.0f / (R - L), 0.0f, 0.0f, 0.0f}, + {0.0f, 2.0f / (T - B), 0.0f, 0.0f}, + {0.0f, 0.0f, 0.5f, 0.0f}, + {(R + L) / (L - R), (T + B) / (B - T), 0.5f, 1.0f}, + }; + wgpuQueueWriteBuffer(bd->defaultQueue, bd->uniformBuffer, 0, mvp, sizeof(mvp)); + } + + wgpuRenderPassEncoderSetPipeline(pass_encoder, bd->renderPipeline); + wgpuRenderPassEncoderSetBindGroup(pass_encoder, 0, bd->bindGroup, 0, nullptr); +} + +static void ImGui_ImplWGPU_CreatePipeline() { + ImGui_ImplWGPU_Data *bd = ImGui_ImplWGPU_GetBackendData(); + WGPUDevice device = bd->wgpuDevice; + + // Shader modules. + WGPUShaderSourceWGSL wgslVert{}; + wgslVert.chain.sType = WGPUSType_ShaderSourceWGSL; + wgslVert.code = WGPUStringView{ImGui_ImplWGPU_GetWGSLVertexShader(), + strlen(ImGui_ImplWGPU_GetWGSLVertexShader())}; + WGPUShaderModuleDescriptor vsDesc{}; + vsDesc.nextInChain = &wgslVert.chain; + WGPUShaderModule vsModule = wgpuDeviceCreateShaderModule(device, &vsDesc); + + WGPUShaderSourceWGSL wgslFrag{}; + wgslFrag.chain.sType = WGPUSType_ShaderSourceWGSL; + wgslFrag.code = WGPUStringView{ImGui_ImplWGPU_GetWGSLFragmentShader(), + strlen(ImGui_ImplWGPU_GetWGSLFragmentShader())}; + WGPUShaderModuleDescriptor fsDesc{}; + fsDesc.nextInChain = &wgslFrag.chain; + WGPUShaderModule fsModule = wgpuDeviceCreateShaderModule(device, &fsDesc); + + // Bind group layout. + WGPUBindGroupLayoutEntry entries[3] = {}; + entries[0].binding = 0; + entries[0].visibility = WGPUShaderStage_Vertex; + entries[0].buffer.type = WGPUBufferBindingType_Uniform; + entries[0].buffer.minBindingSize = 64; + entries[1].binding = 1; + entries[1].visibility = WGPUShaderStage_Fragment; + entries[1].sampler.type = WGPUSamplerBindingType_Filtering; + entries[2].binding = 2; + entries[2].visibility = WGPUShaderStage_Fragment; + entries[2].texture.sampleType = WGPUTextureSampleType_Float; + entries[2].texture.viewDimension = WGPUTextureViewDimension_2D; + WGPUBindGroupLayoutDescriptor bglDesc{}; + bglDesc.entryCount = 3; + bglDesc.entries = entries; + bd->bindGroupLayout = wgpuDeviceCreateBindGroupLayout(device, &bglDesc); + + // Pipeline layout. + WGPUPipelineLayoutDescriptor plDesc{}; + plDesc.bindGroupLayoutCount = 1; + plDesc.bindGroupLayouts = &bd->bindGroupLayout; + WGPUPipelineLayout pipelineLayout = wgpuDeviceCreatePipelineLayout(device, &plDesc); + + // Vertex layout: ImDrawVert { vec2 pos; vec2 uv; u32 col; } + WGPUVertexAttribute attrs[3] = {}; + attrs[0].format = WGPUVertexFormat_Float32x2; + attrs[0].offset = IM_OFFSETOF(ImDrawVert, pos); + attrs[0].shaderLocation = 0; + attrs[1].format = WGPUVertexFormat_Float32x2; + attrs[1].offset = IM_OFFSETOF(ImDrawVert, uv); + attrs[1].shaderLocation = 1; + attrs[2].format = WGPUVertexFormat_Unorm8x4; + attrs[2].offset = IM_OFFSETOF(ImDrawVert, col); + attrs[2].shaderLocation = 2; + WGPUVertexBufferLayout vbLayout{}; + vbLayout.arrayStride = sizeof(ImDrawVert); + vbLayout.stepMode = WGPUVertexStepMode_Vertex; + vbLayout.attributeCount = 3; + vbLayout.attributes = attrs; + + WGPUBlendState blend{}; + blend.color.srcFactor = WGPUBlendFactor_SrcAlpha; + blend.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha; + blend.color.operation = WGPUBlendOperation_Add; + blend.alpha.srcFactor = WGPUBlendFactor_One; + blend.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha; + blend.alpha.operation = WGPUBlendOperation_Add; + + WGPUColorTargetState colorTarget{}; + colorTarget.format = bd->rtFormat; + colorTarget.blend = &blend; + colorTarget.writeMask = WGPUColorWriteMask_All; + + WGPURenderPipelineDescriptor desc{}; + desc.layout = pipelineLayout; + desc.vertex.module = vsModule; + desc.vertex.entryPoint = WGPUStringView{"vs_main", 6}; + desc.vertex.bufferCount = 1; + desc.vertex.buffers = &vbLayout; + desc.primitive.topology = WGPUPrimitiveTopology_TriangleList; + desc.primitive.frontFace = WGPUFrontFace_CCW; + desc.primitive.cullMode = WGPUCullMode_None; + desc.primitive.stripIndexFormat = WGPUIndexFormat_Undefined; + WGPUFragmentState fragState{}; + fragState.module = fsModule; + fragState.entryPoint = WGPUStringView{"fs_main", 6}; + fragState.targetCount = 1; + fragState.targets = &colorTarget; + desc.fragment = &fragState; + desc.multisample.count = 1; + bd->renderPipeline = wgpuDeviceCreateRenderPipeline(device, &desc); +} + +static void ImGui_ImplWGPU_CreateUniformBuffer() { + ImGui_ImplWGPU_Data *bd = ImGui_ImplWGPU_GetBackendData(); + WGPUDevice device = bd->wgpuDevice; + + bd->uniformBufferSize = 64; + WGPUBufferDescriptor desc{}; + desc.size = bd->uniformBufferSize; + desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; + desc.mappedAtCreation = false; + bd->uniformBuffer = wgpuDeviceCreateBuffer(device, &desc); +} + +bool ImGui_ImplWGPU_CreateFontsTexture() { + ImGuiIO &io = ImGui::GetIO(); + ImGui_ImplWGPU_Data *bd = ImGui_ImplWGPU_GetBackendData(); + WGPUDevice device = bd->wgpuDevice; + + unsigned char *pixels; + int width, height, size; + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height, &size); + size_t stride = static_cast(width) * 4; + + WGPUTextureDescriptor texDesc{}; + texDesc.dimension = WGPUTextureDimension_2D; + texDesc.size.width = width; + texDesc.size.height = height; + texDesc.size.depthOrArrayLayers = 1; + texDesc.sampleCount = 1; + texDesc.format = WGPUTextureFormat_RGBA8Unorm; + texDesc.mipLevelCount = 1; + texDesc.usage = WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst; + bd->fontTexture = wgpuDeviceCreateTexture(device, &texDesc); + + WGPUTexelCopyBufferLayout layout{}; + layout.offset = 0; + layout.bytesPerRow = stride; + layout.rowsPerImage = height; + WGPUTexelCopyTextureInfo copyDst{}; + copyDst.texture = bd->fontTexture; + copyDst.mipLevel = 0; + copyDst.aspect = WGPUTextureAspect_All; + WGPUExtent3D extent{static_cast(width), static_cast(height), 1}; + wgpuQueueWriteTexture(bd->defaultQueue, ©Dst, pixels, stride * height, &layout, &extent); + + WGPUTextureViewDescriptor viewDesc{}; + viewDesc.format = WGPUTextureFormat_RGBA8Unorm; + viewDesc.dimension = WGPUTextureViewDimension_2D; + viewDesc.baseMipLevel = 0; + viewDesc.mipLevelCount = 1; + viewDesc.baseArrayLayer = 0; + viewDesc.arrayLayerCount = 1; + bd->fontTextureView = wgpuTextureCreateView(bd->fontTexture, &viewDesc); + + WGPUSamplerDescriptor samplerDesc{}; + samplerDesc.minFilter = WGPUFilterMode_Linear; + samplerDesc.magFilter = WGPUFilterMode_Linear; + samplerDesc.mipmapFilter = WGPUMipmapFilterMode_Linear; + samplerDesc.addressModeU = WGPUAddressMode_ClampToEdge; + samplerDesc.addressModeV = WGPUAddressMode_ClampToEdge; + samplerDesc.addressModeW = WGPUAddressMode_ClampToEdge; + bd->fontSampler = wgpuDeviceCreateSampler(device, &samplerDesc); + + WGPUBindGroupEntry entries[3] = {}; + entries[0].binding = 0; + entries[0].buffer = bd->uniformBuffer; + entries[0].size = bd->uniformBufferSize; + entries[1].binding = 1; + entries[1].sampler = bd->fontSampler; + entries[2].binding = 2; + entries[2].textureView = bd->fontTextureView; + + WGPUBindGroupDescriptor bgDesc{}; + bgDesc.layout = bd->bindGroupLayout; + bgDesc.entryCount = 3; + bgDesc.entries = entries; + bd->bindGroup = wgpuDeviceCreateBindGroup(device, &bgDesc); + + io.Fonts->SetTexID((ImTextureID)(intptr_t)bd->fontTextureView); + return true; +} + +void ImGui_ImplWGPU_InvalidateDeviceObjects() { + ImGui_ImplWGPU_Data *bd = ImGui_ImplWGPU_GetBackendData(); + if (!bd) return; + if (bd->fontTexture) { wgpuTextureRelease(bd->fontTexture); bd->fontTexture = nullptr; } + if (bd->fontTextureView) { wgpuTextureViewRelease(bd->fontTextureView); bd->fontTextureView = nullptr; } + if (bd->fontSampler) { wgpuSamplerRelease(bd->fontSampler); bd->fontSampler = nullptr; } + if (bd->bindGroup) { wgpuBindGroupRelease(bd->bindGroup); bd->bindGroup = nullptr; } + if (bd->bindGroupLayout) { wgpuBindGroupLayoutRelease(bd->bindGroupLayout); bd->bindGroupLayout = nullptr; } + if (bd->uniformBuffer) { wgpuBufferRelease(bd->uniformBuffer); bd->uniformBuffer = nullptr; } + if (bd->renderPipeline) { wgpuRenderPipelineRelease(bd->renderPipeline); bd->renderPipeline = nullptr; } + if (ImGui::GetCurrentContext() && ImGui::GetIO().Fonts) + ImGui::GetIO().Fonts->SetTexID(0); +} + +bool ImGui_ImplWGPU_Init(WGPUDevice device, int num_frames_in_flight, WGPUTextureFormat rt_format) { + ImGuiIO &io = ImGui::GetIO(); + IM_ASSERT(io.BackendRendererUserData == NULL && "Already initialized a renderer backend!"); + + ImGui_ImplWGPU_Data *bd = IM_NEW(ImGui_ImplWGPU_Data)(); + io.BackendRendererUserData = (void *)bd; + io.BackendRendererName = "imgui_impl_webgpu"; + io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; + + bd->wgpuDevice = device; + bd->rtFormat = rt_format; + bd->numFramesInFlight = num_frames_in_flight; + bd->defaultQueue = wgpuDeviceGetQueue(device); + + ImGui_ImplWGPU_CreateUniformBuffer(); + ImGui_ImplWGPU_CreatePipeline(); + ImGui_ImplWGPU_CreateFontsTexture(); + + return true; +} + +void ImGui_ImplWGPU_Shutdown() { + ImGui_ImplWGPU_Data *bd = ImGui_ImplWGPU_GetBackendData(); + IM_ASSERT(bd != NULL && "No renderer backend to shutdown, or already shutdown?"); + ImGui_ImplWGPU_InvalidateDeviceObjects(); + if (bd->defaultQueue) { wgpuQueueRelease(bd->defaultQueue); bd->defaultQueue = nullptr; } + if (bd->wgpuDevice) { wgpuDeviceRelease(bd->wgpuDevice); bd->wgpuDevice = nullptr; } + ImGuiIO &io = ImGui::GetIO(); + io.BackendRendererName = NULL; + io.BackendRendererUserData = NULL; + IM_DELETE(bd); +} + +void ImGui_ImplWGPU_NewFrame() { + ImGui_ImplWGPU_Data *bd = ImGui_ImplWGPU_GetBackendData(); + IM_ASSERT(bd != NULL && "Did you call ImGui_ImplWGPU_Init()?"); + bd->frameIndex = (bd->frameIndex + 1) % bd->numFramesInFlight; +} + +void ImGui_ImplWGPU_RenderDrawData(ImDrawData *draw_data, WGPURenderPassEncoder pass_encoder) { + ImGui_ImplWGPU_Data *bd = ImGui_ImplWGPU_GetBackendData(); + if (draw_data->CmdListsCount == 0) return; + + ImGui_ImplWGPU_SetupRenderState(draw_data, pass_encoder); + + // Will project scissor/clipping rectangles into framebuffer space. + ImVec2 clip_off = draw_data->DisplayPos; + ImVec2 clip_scale = draw_data->FramebufferScale; + + const float fb_width = draw_data->DisplaySize.x * clip_scale.x; + const float fb_height = draw_data->DisplaySize.y * clip_scale.y; + if (fb_width <= 0.0f || fb_height <= 0.0f) return; + + for (int n = 0; n < draw_data->CmdListsCount; n++) { + const ImDrawList *cmd_list = draw_data->CmdLists[n]; + + // Create and upload vertex/index buffers. + size_t vbSize = (size_t)cmd_list->VtxBuffer.Size * sizeof(ImDrawVert); + size_t ibSize = (size_t)cmd_list->IdxBuffer.Size * sizeof(ImDrawIdx); + WGPUBuffer vb = nullptr; + WGPUBuffer ib = nullptr; + + WGPUBufferDescriptor vbDesc{}; + vbDesc.size = vbSize; + vbDesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; + vbDesc.mappedAtCreation = false; + vb = wgpuDeviceCreateBuffer(bd->wgpuDevice, &vbDesc); + wgpuQueueWriteBuffer(bd->defaultQueue, vb, 0, cmd_list->VtxBuffer.Data, vbSize); + + WGPUBufferDescriptor ibDesc{}; + ibDesc.size = ibSize; + ibDesc.usage = WGPUBufferUsage_Index | WGPUBufferUsage_CopyDst; + ibDesc.mappedAtCreation = false; + ib = wgpuDeviceCreateBuffer(bd->wgpuDevice, &ibDesc); + wgpuQueueWriteBuffer(bd->defaultQueue, ib, 0, cmd_list->IdxBuffer.Data, ibSize); + + wgpuRenderPassEncoderSetVertexBuffer(pass_encoder, 0, vb, 0, vbSize); + wgpuRenderPassEncoderSetIndexBuffer(pass_encoder, ib, + sizeof(ImDrawIdx) == 2 ? WGPUIndexFormat_Uint16 + : WGPUIndexFormat_Uint32, + 0, ibSize); + + for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) { + const ImDrawCmd *pcmd = &cmd_list->CmdBuffer[cmd_i]; + if (pcmd->UserCallback != NULL) { + pcmd->UserCallback(cmd_list, pcmd); + } else { + // Project scissor/clipping rectangles into framebuffer space. + ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, + (pcmd->ClipRect.y - clip_off.y) * clip_scale.y); + ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, + (pcmd->ClipRect.w - clip_off.y) * clip_scale.y); + if (clip_min.x < 0.0f) clip_min.x = 0.0f; + if (clip_min.y < 0.0f) clip_min.y = 0.0f; + if (clip_max.x > fb_width) clip_max.x = fb_width; + if (clip_max.y > fb_height) clip_max.y = fb_height; + if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) continue; + + wgpuRenderPassEncoderSetScissorRect(pass_encoder, + (uint32_t)clip_min.x, (uint32_t)clip_min.y, + (uint32_t)(clip_max.x - clip_min.x), + (uint32_t)(clip_max.y - clip_min.y)); + + // Bind font texture when the texture id changes. + WGPUTextureView fontTextureView = + (WGPUTextureView)(intptr_t)pcmd->TextureId; + if (fontTextureView != bd->fontTextureView) { + WGPUBindGroupEntry entries[3] = {}; + entries[0].binding = 0; + entries[0].buffer = bd->uniformBuffer; + entries[0].size = bd->uniformBufferSize; + entries[1].binding = 1; + entries[1].sampler = bd->fontSampler; + entries[2].binding = 2; + entries[2].textureView = fontTextureView; + WGPUBindGroupDescriptor bgDesc{}; + bgDesc.layout = bd->bindGroupLayout; + bgDesc.entryCount = 3; + bgDesc.entries = entries; + WGPUBindGroup bg = wgpuDeviceCreateBindGroup(bd->wgpuDevice, &bgDesc); + wgpuRenderPassEncoderSetBindGroup(pass_encoder, 0, bg, 0, nullptr); + wgpuBindGroupRelease(bg); + } + + wgpuRenderPassEncoderDrawIndexed(pass_encoder, pcmd->ElemCount, 1, + pcmd->IdxOffset, (int32_t)pcmd->VtxOffset, 0); + } + } + + wgpuBufferRelease(vb); + wgpuBufferRelease(ib); + } +} + +#endif // EVENGINE_WEBGPU diff --git a/src/modules/ui/imgui/imgui_impl_wgpu.h b/src/modules/ui/imgui/imgui_impl_wgpu.h new file mode 100644 index 00000000..0d7e4325 --- /dev/null +++ b/src/modules/ui/imgui/imgui_impl_wgpu.h @@ -0,0 +1,18 @@ +// dear imgui: Renderer Backend for WebGPU (wgpu), adapted for EVEngine's +// vendored imgui v1.83. Uses the stable webgpu.h C API so the same file +// compiles against Dawn (native) and Emscripten (browser). + +#pragma once + +#include "imgui.h" + +#include + +IMGUI_IMPL_API bool ImGui_ImplWGPU_Init(WGPUDevice device, int num_frames_in_flight, + WGPUTextureFormat rt_format); +IMGUI_IMPL_API void ImGui_ImplWGPU_Shutdown(); +IMGUI_IMPL_API void ImGui_ImplWGPU_NewFrame(); +IMGUI_IMPL_API void ImGui_ImplWGPU_RenderDrawData(ImDrawData *draw_data, + WGPURenderPassEncoder pass_encoder); +IMGUI_IMPL_API bool ImGui_ImplWGPU_CreateFontsTexture(); +IMGUI_IMPL_API void ImGui_ImplWGPU_InvalidateDeviceObjects(); diff --git a/src/modules/window/sdl/Window.cpp b/src/modules/window/sdl/Window.cpp index ae156c8b..d4c21d77 100644 --- a/src/modules/window/sdl/Window.cpp +++ b/src/modules/window/sdl/Window.cpp @@ -1,7 +1,9 @@ #include "Window.h" #include +#ifndef EVENGINE_WEBGPU #include +#endif #include #include @@ -82,7 +84,10 @@ bool Window::setWindowSettings(WindowSettings f) { f.height = mode.h; } - Uint32 sdlflags = SDL_WINDOW_VULKAN; + Uint32 sdlflags = 0; +#ifndef EVENGINE_WEBGPU + sdlflags = SDL_WINDOW_VULKAN; +#endif // On Android, disable fullscreen first on window creation so it's // possible to change the orientation by specifying portait width and @@ -174,7 +179,13 @@ bool Window::setWindowSettings(WindowSettings f) { if (graphics) { int pw = 0, ph = 0; +#ifdef EVENGINE_WEBGPU + // No Vulkan drawable-size helper on WebGPU; the window size is the + // canvas size (the browser applies DPI scaling itself). + SDL_GetWindowSize(window, &pw, &ph); +#else SDL_Vulkan_GetDrawableSize(window, &pw, &ph); +#endif if (pw <= 0 || ph <= 0) { pw = f.width; ph = f.height; @@ -571,7 +582,11 @@ void Window::updateSettings(const WindowSettings &newsettings, bool updateGraphi settings = newsettings; if (window) { SDL_GetWindowSize(window, &windowWidth, &windowHeight); +#ifdef EVENGINE_WEBGPU + SDL_GetWindowSize(window, &pixelWidth, &pixelHeight); +#else SDL_Vulkan_GetDrawableSize(window, &pixelWidth, &pixelHeight); +#endif } if (updateGraphicsViewport && graphics && window) { graphics->setViewportSize(windowWidth, windowHeight, pixelWidth, pixelHeight); diff --git a/src/scripts/load_web.nut b/src/scripts/load_web.nut new file mode 100644 index 00000000..0334343d --- /dev/null +++ b/src/scripts/load_web.nut @@ -0,0 +1,131 @@ +// WebGPU (browser/WASM) root script — trimmed module set. +// The Emscripten build compiles out the modules whose third-party deps (Poco, +// OpenAL, assimp/medialoader, freetype) were dropped, so this root only wires +// the modules that exist in the WASM runtime. + +function file_exists(path) { + try { + file(path, "r").close(); + return true; + } catch (e) { + return false; + } +} + +function path_endswith(str, suffix) { + if (str == null || suffix == null) return false; + if (str.len() < suffix.len()) return false; + return str.slice(str.len() - suffix.len()) == suffix; +} + +function normalize_path(path) { + if (path == null) return ""; + local s = path; + while (s.len() >= 2 && s.slice(0, 2) == "./") + s = s.slice(2); + return s; +} + +config <- { + width = 800 + height = 600 + title = "EVEngine WebGPU" + debug = false + hotReload = false +}; + +if (file_exists("config.nut")) { + dofile("config.nut"); +} + +win <- eve.Window(); +gfx <- eve.Graphics(); +win.setGraphics(gfx); + +local s = eve.WindowSettings(); +s.width = config.width; +s.height = config.height; +s.centered = true; +if (!win.setWindowSettings(s)) { + print("setWindowSettings failed\n"); + return; +} + +config.width = win.getWidth(); +config.height = win.getHeight(); + +event <- eve.Event(); +timer <- eve.Timer(); +system <- eve.System(); +math <- eve.Math(); +spatial <- eve.Spatial(); +editor <- eve.Editor(); +ui <- eve.UI(); +scene <- eve.Scene(); +physics <- eve.Physics(); +keyboard <- eve.Keyboard(); +mouse <- eve.Mouse(); +touch <- eve.Touch(); +gpgpu <- eve.Gpgpu(); +thread <- eve.Thread(); +fs <- eve.Filesystem(); + +eve_init <- function() {}; +eve_update <- function(dt) {}; +eve_render <- function() { + gfx.clear(); +}; +eve_quit <- function() {}; + +if (file_exists("main.nut")) { + dofile("main.nut"); +} else if ("demoScript" in eve && eve.demoScript != null && eve.demoScript != "") { + try { + compilestring(eve.demoScript)(); + } catch (e) { + print("Embedded demo failed to load: " + e + "\n"); + } +} + +try { + eve_init(); +} catch (e) { + print("eve_init failed: " + e + "\n"); +} + +// One frame of the game loop, driven from C++ via emscripten_set_main_loop +// (requestAnimationFrame). Returns false to stop the loop (e.g. on quit). +eve_frame <- function() { + event.pump(); + while (true) { + local name = event.poll(); + if (name == "") break; + if (name == "quit") return false; + } + + config.width = win.getWidth(); + config.height = win.getHeight(); + + local dt = timer.step(); + try { + eve_update(dt); + } catch (e) { + print("eve_update error: " + e + "\n"); + } + try { + eve_render(); + } catch (e) { + print("eve_render error: " + e + "\n"); + } + try { + ui.dispatchEvents(); + } catch (e) { + print("ui.dispatchEvents error: " + e + "\n"); + } + try { + gfx.present(); + } catch (e) { + print("present error: " + e + "\n"); + } + return true; +}; diff --git a/src/scripts/scripts.h b/src/scripts/scripts.h index 641903cb..914a7af8 100644 --- a/src/scripts/scripts.h +++ b/src/scripts/scripts.h @@ -3,6 +3,7 @@ namespace eve { extern const char* boot_content; extern const char* load_content; +extern const char* load_web_content; extern const char* async_content; extern const char* demo_content; extern const char* vertex_content; diff --git a/src/scripts/template.cpp.in b/src/scripts/template.cpp.in index e778ba48..bd3bafb5 100644 --- a/src/scripts/template.cpp.in +++ b/src/scripts/template.cpp.in @@ -5,6 +5,8 @@ const char* boot_content = R"(@boot_content@)"; const char* load_content = R"(@load_content@)"; +const char* load_web_content = R"(@load_web_content@)"; + const char* async_content = R"(@async_content@)"; const char* demo_content = R"(@demo_content@)"; From c0b2b125e5027a58ac078dc87e928a6c4d8573ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Sat, 15 Aug 2026 12:35:08 +0800 Subject: [PATCH 12/21] feat(procgen): add random low-poly rock generator --- CMakeLists.txt | 41 ++- cmake/install_sdk.cmake | 16 +- examples/rock-generator/README.md | 23 ++ examples/rock-generator/config.nut | 7 + examples/rock-generator/main.nut | 183 +++++++++++ .../procgen/algorithms/MarchingCubes.cpp | 36 ++- src/modules/procgen/algorithms/RockMesh.cpp | 283 ++++++++++++++++++ src/modules/procgen/algorithms/RockMesh.h | 13 + test/procgen.cpp | 52 ++++ 9 files changed, 642 insertions(+), 12 deletions(-) create mode 100644 examples/rock-generator/README.md create mode 100644 examples/rock-generator/config.nut create mode 100644 examples/rock-generator/main.nut create mode 100644 src/modules/procgen/algorithms/RockMesh.cpp create mode 100644 src/modules/procgen/algorithms/RockMesh.h diff --git a/CMakeLists.txt b/CMakeLists.txt index fd87a1bc..1b3ce294 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,8 @@ set(EVENGINE_PATCH_VERSION "0") option(BUILD_TESTING "enable testing" ON) option(EVENGINE_BUILD_DEMO "Build optional Demo module (procedural demo assets + default demo.nut)" ON) +set(EVENGINE_THIRD_PARTY_BINARY_DIR "" CACHE PATH + "Use a prebuilt third-party install tree (include/lib/bin) without rebuilding or modifying it") message("Build UnitTest: " ${BUILD_TESTING}) message("Build Demo module: " ${EVENGINE_BUILD_DEMO}) if (CMAKE_BUILD_TYPE STREQUAL "") @@ -411,6 +413,29 @@ endfunction() function(check_third_party_project name repo) include(ExternalProject) + if(EVENGINE_THIRD_PARTY_BINARY_DIR) + get_filename_component(_eve_tp_binary "${EVENGINE_THIRD_PARTY_BINARY_DIR}" ABSOLUTE) + if(NOT IS_DIRECTORY "${_eve_tp_binary}/include" OR + NOT IS_DIRECTORY "${_eve_tp_binary}/lib") + message(FATAL_ERROR + "EVENGINE_THIRD_PARTY_BINARY_DIR must contain include/ and lib/: " + "${_eve_tp_binary}") + endif() + add_custom_target(${name}) + include_directories("${_eve_tp_binary}/include") + # SDL2 and FreeType install their public headers one level below include/. + # Their sources use and , so expose those roots too. + if(IS_DIRECTORY "${_eve_tp_binary}/include/SDL2") + include_directories("${_eve_tp_binary}/include/SDL2") + endif() + if(IS_DIRECTORY "${_eve_tp_binary}/include/freetype2") + include_directories("${_eve_tp_binary}/include/freetype2") + endif() + link_directories("${_eve_tp_binary}/lib") + message(STATUS "third-party: using read-only prebuilt tree ${_eve_tp_binary}") + return() + endif() + if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${name}/CMakeLists.txt) # This is a git clone version message("load ${name} folder from git") @@ -523,13 +548,15 @@ function(load_third_party) # EVEngine's database module uses POCO Data and its bundled SQLite connector. # The third-party aggregate disables these by default to reduce its footprint; # enable the two targets in the checked-out aggregate before configuring it. - set(_eve_tp_cmake "${CMAKE_CURRENT_SOURCE_DIR}/third-party/CMakeLists.txt") - file(READ "${_eve_tp_cmake}" _eve_tp_contents) - string(REPLACE "set(ENABLE_DATA OFF CACHE BOOL \"\" FORCE)" - "set(ENABLE_DATA ON CACHE BOOL \"\" FORCE)" _eve_tp_contents "${_eve_tp_contents}") - string(REPLACE "set(ENABLE_DATA_SQLITE OFF CACHE BOOL \"\" FORCE)" - "set(ENABLE_DATA_SQLITE ON CACHE BOOL \"\" FORCE)" _eve_tp_contents "${_eve_tp_contents}") - file(WRITE "${_eve_tp_cmake}" "${_eve_tp_contents}") + if(NOT EVENGINE_THIRD_PARTY_BINARY_DIR) + set(_eve_tp_cmake "${CMAKE_CURRENT_SOURCE_DIR}/third-party/CMakeLists.txt") + file(READ "${_eve_tp_cmake}" _eve_tp_contents) + string(REPLACE "set(ENABLE_DATA OFF CACHE BOOL \"\" FORCE)" + "set(ENABLE_DATA ON CACHE BOOL \"\" FORCE)" _eve_tp_contents "${_eve_tp_contents}") + string(REPLACE "set(ENABLE_DATA_SQLITE OFF CACHE BOOL \"\" FORCE)" + "set(ENABLE_DATA_SQLITE ON CACHE BOOL \"\" FORCE)" _eve_tp_contents "${_eve_tp_contents}") + file(WRITE "${_eve_tp_cmake}" "${_eve_tp_contents}") + endif() check_third_party_project(third-party "https://github.com/EVEngine/third-party") ADD_CUSTOM_TARGET(deps DEPENDS third-party) diff --git a/cmake/install_sdk.cmake b/cmake/install_sdk.cmake index b7c3a133..110da093 100644 --- a/cmake/install_sdk.cmake +++ b/cmake/install_sdk.cmake @@ -95,8 +95,12 @@ endif() # ---- Android runtime shared libs needed to assemble an APK ---- if(BUILD_PLATFORM STREQUAL "android") - set(_eve_tp_lib "${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}") - if(CMAKE_BUILD_TYPE STREQUAL "Debug") + if(EVENGINE_THIRD_PARTY_BINARY_DIR) + set(_eve_tp_lib "${EVENGINE_THIRD_PARTY_BINARY_DIR}") + else() + set(_eve_tp_lib "${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}") + endif() + if(NOT EVENGINE_THIRD_PARTY_BINARY_DIR AND CMAKE_BUILD_TYPE STREQUAL "Debug") set(_eve_tp_lib "${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}-debug") endif() if(EXISTS "${_eve_tp_lib}/lib/libSDL2.so") @@ -117,8 +121,12 @@ if(BUILD_PLATFORM STREQUAL "android") endif() # ---- Selected third-party headers needed to compile plugins (SSQ bindings) ---- -set(_eve_tp_inc "${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}") -if(CMAKE_BUILD_TYPE STREQUAL "Debug") +if(EVENGINE_THIRD_PARTY_BINARY_DIR) + set(_eve_tp_inc "${EVENGINE_THIRD_PARTY_BINARY_DIR}") +else() + set(_eve_tp_inc "${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}") +endif() +if(NOT EVENGINE_THIRD_PARTY_BINARY_DIR AND CMAKE_BUILD_TYPE STREQUAL "Debug") set(_eve_tp_inc "${CMAKE_SOURCE_DIR}/build/third-party-binary/${BUILD_PLATFORM}-debug") endif() if(EXISTS "${_eve_tp_inc}/include") diff --git a/examples/rock-generator/README.md b/examples/rock-generator/README.md new file mode 100644 index 00000000..60d8a4fe --- /dev/null +++ b/examples/rock-generator/README.md @@ -0,0 +1,23 @@ +# LITHIC — Random 3D Rock Generator + +A deterministic real-time rock authoring example for EVEngine. Its default path deforms a +shared-vertex icosphere, adds seeded procedural stone albedo and normals, and exposes the defining +parameters through an in-engine UI. Three geometric LODs are generated automatically. + +Run it with the same command used for other examples, selecting `examples/rock-generator` as the game. + +Controls: + +- **New specimen** or **R** advances to a new deterministic seed. +- **Next base shape** cycles through mixed, boulder, slab, block, and shard silhouettes. +- Shape sliders control flattening, fracture planes, erosion, and geologic noise scale. +- Surface roughness updates without rebuilding the mesh. +- **Pause rotation** freezes the turntable for inspection. + +The reusable default recipe is `mesh.rock`. `baseShape` accepts `mixed`, `boulder`, `slab`, +`block`, or `shard`; `mixed` chooses a profile deterministically from the seed. Shape composition +can be tuned further with `variation`, `axisX/Y/Z`, `shapePower`, `skewX/Z`, `cutCount`, and +`cutDepth`, alongside `radius`, `flattening`, `angularity`, `erosion`, `scale`, `octaves`, +`subdivisions`, and `seed`. Subdivision levels 3, 2, and 1 contain 1,280, 320, and 80 triangles +respectively. The volumetric +`mesh.marchingcubes`/`field = "rock"` path remains available for complex topology. diff --git a/examples/rock-generator/config.nut b/examples/rock-generator/config.nut new file mode 100644 index 00000000..8930dabe --- /dev/null +++ b/examples/rock-generator/config.nut @@ -0,0 +1,7 @@ +config <- { + width = 1180 + height = 720 + title = "LITHIC / Random 3D Rock Generator" + debug = false + hotReload = true +}; diff --git a/examples/rock-generator/main.nut b/examples/rock-generator/main.nut new file mode 100644 index 00000000..ce7e8bf3 --- /dev/null +++ b/examples/rock-generator/main.nut @@ -0,0 +1,183 @@ +// LITHIC — deterministic, real-time 3D rock authoring example. +// Every seed recreates the same mesh and procedural stone textures. + +if (!("rock" in getroottable())) rock <- null; +if (!("camera" in getroottable())) camera <- null; +if (!("seed" in getroottable())) seed <- 1847; +if (!("shapeIndex" in getroottable())) shapeIndex <- 0; +if (!("shapeNames" in getroottable())) shapeNames <- ["mixed", "boulder", "slab", "block", "shard"]; +if (!("flattening" in getroottable())) flattening <- 0.22; +if (!("angularity" in getroottable())) angularity <- 0.38; +if (!("erosion" in getroottable())) erosion <- 0.16; +if (!("noiseScale" in getroottable())) noiseScale <- 2.4; +if (!("roughness" in getroottable())) roughness <- 0.86; +if (!("autoSpin" in getroottable())) autoSpin <- true; +if (!("yaw" in getroottable())) yaw <- 0.0; +if (!("uiReady" in getroottable())) uiReady <- false; +if (!("rWasDown" in getroottable())) rWasDown <- false; + +function coreId() { + local s = seed.tostring(); + while (s.len() < 6) s = "0" + s; + return "BAS-" + s; +} + +function buildRock() { + local p = procgen.newParams(); + p.setSeed(seed); + p.setInt("subdivisions", 3); + p.setString("baseShape", shapeNames[shapeIndex]); + p.setFloat("variation", 0.48); + p.setFloat("radius", 0.72); + p.setFloat("flattening", flattening); + p.setFloat("angularity", angularity); + p.setFloat("erosion", erosion); + p.setFloat("scale", noiseScale); + p.setInt("octaves", 4); + + local mesh = procgen.generateMesh("mesh.rock", p, gfx); + if (mesh == null) { + ui.select("lab"); + ui.setText("status", "Generation failed: " + procgen.lastError()); + return; + } + p.setInt("subdivisions", 2); + local lod1 = procgen.generateMesh("mesh.rock", p, gfx); + p.setInt("subdivisions", 1); + local lod2 = procgen.generateMesh("mesh.rock", p, gfx); + + local tp = procgen.newParams(); + tp.setSeed(seed); + tp.setSize(256, 256); + tp.setFloat("scale", 4.2); + tp.setInt("octaves", 5); + tp.setInt("seamless", 1); + local albedo = procgen.generateTexture("tex.stone", tp, gfx); + local normalImage = procgen.generateNormalImage("tex.stone", tp); + local normal = normalImage == null ? null : gfx.newTexture(normalImage, true, true); + + if (rock == null) rock = eve.Renderable3D(); + rock.clearMeshLod(); + rock.setMeshLod(0, mesh, 0.0); + if (lod1 != null) rock.setMeshLod(1, lod1, 8.0); + if (lod2 != null) rock.setMeshLod(2, lod2, 16.0); + rock.setTexture(albedo); + if (normal != null) rock.setNormalTexture(normal); + rock.setScale(2.25, 2.25, 2.25); + rock.setPosition(-0.42, -0.08, 0.0); + rock.setTint(0.69, 0.64, 0.54, 1.0); + rock.setMetallic(0.02); + rock.setRoughness(roughness); + rock.setCastShadow(true); + rock.setReceiveShadow(true); + + if (uiReady) { + ui.select("lab"); + ui.setText("sample", coreId()); + ui.setText("status", shapeNames[shapeIndex] + " base / 1,280 triangles / 3 LODs"); + } +} + +function buildPanel() { + ui.setTheme("dark"); + ui.setNavKeyboard(true); + ui.beginBuild(); + ui.beginWindow("LITHIC / FIELD LAB", "root"); + ui.text("CURRENT CORE", "eyebrow"); + ui.text(coreId(), "sample"); + ui.text("Shape", "shapeLabel"); + ui.slider("Flattening", flattening, 0.0, 0.65, "flattening"); + ui.slider("Fracture planes", angularity, 0.0, 1.0, "angularity"); + ui.slider("Surface erosion", erosion, 0.0, 0.42, "erosion"); + ui.slider("Geologic scale", noiseScale, 0.6, 5.5, "noiseScale"); + ui.text("Surface", "surfaceLabel"); + ui.slider("Roughness", roughness, 0.35, 1.0, "roughness"); + ui.beginRow("actions", 8.0); + ui.button("New specimen", "randomize"); + ui.button("Rebuild", "rebuild"); + ui.end(); + ui.button("Next base shape", "nextShape"); + ui.button("Pause rotation", "spin"); + ui.text("Drag-free inspection • R creates a new specimen", "hint"); + ui.text("Generating…", "status"); + ui.end(); + ui.mountBuildAs("lab"); + ui.select("lab"); + ui.setHostOverlay(true); + ui.setHostPos(850.0, 30.0, 300.0, 650.0); + uiReady = true; +} + +function randomize() { + seed = (seed * 1664525 + 1013904223) & 0x7fffffff; + buildRock(); +} + +function randomizeKeyPressed() { + local down = keyboard.isDown("r") || keyboard.isDown("R"); + local pressed = down && !rWasDown; + rWasDown = down; + return pressed; +} + +eve_init = function() { + gfx.setBackgroundColor(0.045, 0.052, 0.050, 1.0); + camera = eve.Camera3D(); + camera.setEye(3.25, 2.15, 5.1); + camera.setTarget(-0.4, -0.1, 0.0); + camera.setFov(37.0); + camera.setAmbient(0.24, 0.25, 0.23); + gfx.setDirectionalLight(-0.55, 0.85, 0.35, 2.0, 1.72, 1.35); + if (!uiReady) buildPanel(); + buildRock(); +}; + +eve_update = function(dt) { + if (autoSpin && rock != null) { + yaw += dt * 16.0; + rock.setYaw(yaw); + } + if (randomizeKeyPressed()) randomize(); + + local clicked = ui.consumeClick(); + while (clicked != "") { + if (clicked == "lab/randomize") randomize(); + else if (clicked == "lab/rebuild") buildRock(); + else if (clicked == "lab/nextShape") { + shapeIndex = (shapeIndex + 1) % shapeNames.len(); + buildRock(); + } + else if (clicked == "lab/spin") { + autoSpin = !autoSpin; + ui.select("lab"); + ui.setText("spin", autoSpin ? "Pause rotation" : "Resume rotation"); + } + clicked = ui.consumeClick(); + } + + local changed = ui.consumeChange(); + local rebuild = false; + while (changed != "") { + ui.select("lab"); + if (changed == "lab/flattening") { flattening = ui.getValue("flattening"); rebuild = true; } + else if (changed == "lab/angularity") { angularity = ui.getValue("angularity"); rebuild = true; } + else if (changed == "lab/erosion") { erosion = ui.getValue("erosion"); rebuild = true; } + else if (changed == "lab/noiseScale") { noiseScale = ui.getValue("noiseScale"); rebuild = true; } + else if (changed == "lab/roughness") { + roughness = ui.getValue("roughness"); + if (rock != null) rock.setRoughness(roughness); + } + changed = ui.consumeChange(); + } + if (rebuild) buildRock(); +}; + +eve_render = function() { + gfx.clear(); + gfx.render3D(); + // Copper datum lines make the viewport read like a geological instrument. + gfx.drawSolidRect(54, 650, 720, 1, 0.55, 0.34, 0.18, 0.55); + gfx.drawSolidRect(54, 646, 1, 9, 0.55, 0.34, 0.18, 0.75); + gfx.drawSolidRect(774, 646, 1, 9, 0.55, 0.34, 0.18, 0.75); + ui.beginFrameAndRender(); +}; diff --git a/src/modules/procgen/algorithms/MarchingCubes.cpp b/src/modules/procgen/algorithms/MarchingCubes.cpp index 1a76746e..5f46ee2c 100644 --- a/src/modules/procgen/algorithms/MarchingCubes.cpp +++ b/src/modules/procgen/algorithms/MarchingCubes.cpp @@ -1,4 +1,5 @@ #include "procgen/algorithms/MarchingCubes.h" +#include "procgen/algorithms/RockMesh.h" #include #include @@ -220,6 +221,38 @@ bool fillDensityField(const Params ¶ms, std::vector &density, int &nx if (field == "sphere") { const float r = params.getFloat("radius", 0.7f); d = r - std::sqrt(px * px + py * py + pz * pz); + } else if (field == "rock") { + // An ellipsoid SDF whose radius is displaced by low-frequency strata and + // higher-frequency erosion. Quantising the direction before sampling the + // strata creates broad, natural fracture planes instead of a noisy sphere. + const float radius = params.getFloat("radius", 0.68f); + const float flattening = + std::clamp(params.getFloat("flattening", 0.22f), 0.f, 0.7f); + const float angularity = + std::clamp(params.getFloat("angularity", 0.35f), 0.f, 1.f); + const float erosion = + std::clamp(params.getFloat("erosion", 0.18f), 0.f, 0.45f); + const float detailScale = std::max(0.25f, scale); + const float sy = std::max(0.3f, 1.f - flattening); + const float ex = px; + const float ey = py / sy; + const float ez = pz; + const float len = std::sqrt(ex * ex + ey * ey + ez * ez); + const float invLen = len > 1e-5f ? 1.f / len : 0.f; + const float steps = 3.f + angularity * 9.f; + const float qx = std::round(ex * invLen * steps) / steps; + const float qy = std::round(ey * invLen * steps) / steps; + const float qz = std::round(ez * invLen * steps) / steps; + const float strata = fbm3((qx + 2.3f) * detailScale, + (qy + 4.7f) * detailScale, + (qz + 8.1f) * detailScale, seed, octaves); + const float pits = fbm3((px + 7.2f) * detailScale * 2.7f, + (py + 1.9f) * detailScale * 2.7f, + (pz + 5.4f) * detailScale * 2.7f, + seed + 7919u, std::max(2, octaves - 1)); + const float displacement = strata * (0.08f + angularity * 0.16f) - + std::max(0.f, pits) * erosion; + d = radius + displacement - len; } else if (field == "torus") { const float R = params.getFloat("majorRadius", 0.55f); const float r = params.getFloat("minorRadius", 0.22f); @@ -235,7 +268,7 @@ bool fillDensityField(const Params ¶ms, std::vector &density, int &nx d = n - params.getFloat("threshold", 0.05f); } else { error = "mesh.marchingcubes: unknown field '" + field + - "' (use sphere|torus|noise|terrain)"; + "' (use sphere|rock|torus|noise|terrain)"; return false; } // Soft boundary falloff so surfaces close. @@ -300,6 +333,7 @@ std::vector MeshRecipeRegistry::list() const { void MeshRecipeRegistry::registerBuiltins() { if (builtinsRegistered_) return; registerRecipe("mesh.marchingcubes", generateMarchingCubesMesh); + registerRecipe("mesh.rock", generateRockMesh); builtinsRegistered_ = true; } diff --git a/src/modules/procgen/algorithms/RockMesh.cpp b/src/modules/procgen/algorithms/RockMesh.cpp new file mode 100644 index 00000000..10f47982 --- /dev/null +++ b/src/modules/procgen/algorithms/RockMesh.cpp @@ -0,0 +1,283 @@ +#include "procgen/algorithms/RockMesh.h" + +#include +#include +#include +#include +#include + +namespace eve::procgen { +namespace { + +struct Vec3 { + float x = 0.f, y = 0.f, z = 0.f; +}; + +struct Tri { + uint32_t a = 0, b = 0, c = 0; +}; + +struct ShapeProfile { + const char *name; + float axisX, axisY, axisZ; + float power; + float skewX, skewZ; + int cuts; + float cutDepth; +}; + +Vec3 normalize(Vec3 v) { + const float len = std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z); + if (len <= 1e-8f) return {0.f, 1.f, 0.f}; + return {v.x / len, v.y / len, v.z / len}; +} + +Vec3 cross(Vec3 a, Vec3 b) { + return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, + a.x * b.y - a.y * b.x}; +} + +inline float fade(float t) { return t * t * t * (t * (t * 6.f - 15.f) + 10.f); } +inline float mix(float a, float b, float t) { return a + (b - a) * t; } + +float grad3(uint32_t h, float x, float y, float z) { + const uint32_t g = h & 15u; + const float u = g < 8 ? x : y; + const float v = g < 4 ? y : (g == 12 || g == 14 ? x : z); + return ((g & 1u) ? -u : u) + ((g & 2u) ? -v : v); +} + +float noise3(float x, float y, float z, uint32_t seed) { + const int xi = int(std::floor(x)), yi = int(std::floor(y)), zi = int(std::floor(z)); + const float xf = x - float(xi), yf = y - float(yi), zf = z - float(zi); + const float u = fade(xf), v = fade(yf), w = fade(zf); + auto hash = [&](int ix, int iy, int iz) { + return uint32_t(ix) * 374761393u + uint32_t(iy) * 668265263u + + uint32_t(iz) * 1274126177u + seed * 2246822519u; + }; + const float n000 = grad3(hash(xi, yi, zi), xf, yf, zf); + const float n100 = grad3(hash(xi + 1, yi, zi), xf - 1.f, yf, zf); + const float n010 = grad3(hash(xi, yi + 1, zi), xf, yf - 1.f, zf); + const float n110 = grad3(hash(xi + 1, yi + 1, zi), xf - 1.f, yf - 1.f, zf); + const float n001 = grad3(hash(xi, yi, zi + 1), xf, yf, zf - 1.f); + const float n101 = grad3(hash(xi + 1, yi, zi + 1), xf - 1.f, yf, zf - 1.f); + const float n011 = grad3(hash(xi, yi + 1, zi + 1), xf, yf - 1.f, zf - 1.f); + const float n111 = grad3(hash(xi + 1, yi + 1, zi + 1), xf - 1.f, yf - 1.f, zf - 1.f); + return mix(mix(mix(n000, n100, u), mix(n010, n110, u), v), + mix(mix(n001, n101, u), mix(n011, n111, u), v), w); +} + +float fbm3(float x, float y, float z, uint32_t seed, int octaves) { + float sum = 0.f, amplitude = 0.5f, frequency = 1.f, norm = 0.f; + for (int i = 0; i < octaves; ++i) { + sum += noise3(x * frequency, y * frequency, z * frequency, + seed + uint32_t(i) * 1013u) * amplitude; + norm += amplitude; + amplitude *= 0.5f; + frequency *= 2.f; + } + return norm > 0.f ? sum / norm : 0.f; +} + +uint32_t nextRandom(uint32_t &state) { + state ^= state << 13u; + state ^= state >> 17u; + state ^= state << 5u; + return state; +} + +float random01(uint32_t &state) { + return float(nextRandom(state) & 0x00ffffffu) / float(0x01000000u); +} + +float randomSigned(uint32_t &state) { return random01(state) * 2.f - 1.f; } + +bool resolveShape(const std::string &requested, uint32_t seed, ShapeProfile &shape) { + static constexpr ShapeProfile profiles[] = { + {"boulder", 1.f, 1.f, 0.94f, 0.92f, 0.02f, -0.01f, 3, 0.13f}, + {"slab", 1.28f, 0.62f, 1.02f, 0.74f, 0.08f, -0.03f, 5, 0.18f}, + {"block", 1.04f, 0.90f, 0.96f, 0.46f, 0.03f, 0.02f, 6, 0.20f}, + {"shard", 0.70f, 1.34f, 0.56f, 0.62f, 0.22f, -0.12f, 7, 0.24f}, + }; + std::string name = requested; + if (name == "mixed") name = profiles[seed % 4u].name; + for (const ShapeProfile &candidate : profiles) { + if (name == candidate.name) { + shape = candidate; + return true; + } + } + return false; +} + +float shapedCoordinate(float value, float power) { + return std::copysign(std::pow(std::abs(value), power), value); +} + +uint64_t edgeKey(uint32_t a, uint32_t b) { + if (a > b) std::swap(a, b); + return (uint64_t(a) << 32u) | uint64_t(b); +} + +uint32_t midpoint(uint32_t a, uint32_t b, std::vector &vertices, + std::unordered_map &cache) { + const uint64_t key = edgeKey(a, b); + const auto found = cache.find(key); + if (found != cache.end()) return found->second; + const Vec3 &va = vertices[a], &vb = vertices[b]; + const uint32_t index = uint32_t(vertices.size()); + vertices.push_back(normalize({(va.x + vb.x) * 0.5f, (va.y + vb.y) * 0.5f, + (va.z + vb.z) * 0.5f})); + cache.emplace(key, index); + return index; +} + +void createIcosphere(int subdivisions, std::vector &vertices, std::vector &triangles) { + constexpr float phi = 1.6180339887498948482f; + vertices = {{-1, phi, 0}, {1, phi, 0}, {-1, -phi, 0}, {1, -phi, 0}, + {0, -1, phi}, {0, 1, phi}, {0, -1, -phi}, {0, 1, -phi}, + {phi, 0, -1}, {phi, 0, 1}, {-phi, 0, -1}, {-phi, 0, 1}}; + for (Vec3 &v : vertices) v = normalize(v); + triangles = {{0, 11, 5}, {0, 5, 1}, {0, 1, 7}, {0, 7, 10}, {0, 10, 11}, + {1, 5, 9}, {5, 11, 4}, {11, 10, 2}, {10, 7, 6}, {7, 1, 8}, + {3, 9, 4}, {3, 4, 2}, {3, 2, 6}, {3, 6, 8}, {3, 8, 9}, + {4, 9, 5}, {2, 4, 11}, {6, 2, 10}, {8, 6, 7}, {9, 8, 1}}; + + for (int level = 0; level < subdivisions; ++level) { + std::unordered_map cache; + std::vector next; + next.reserve(triangles.size() * 4); + for (const Tri &t : triangles) { + const uint32_t ab = midpoint(t.a, t.b, vertices, cache); + const uint32_t bc = midpoint(t.b, t.c, vertices, cache); + const uint32_t ca = midpoint(t.c, t.a, vertices, cache); + next.push_back({t.a, ab, ca}); + next.push_back({t.b, bc, ab}); + next.push_back({t.c, ca, bc}); + next.push_back({ab, bc, ca}); + } + triangles.swap(next); + } +} + +} // namespace + +bool generateRockMesh(const Params ¶ms, MeshBuild &out, std::string &error) { + const int subdivisions = params.getInt("subdivisions", 3); + if (subdivisions < 0 || subdivisions > 5) { + error = "mesh.rock: subdivisions must be in [0,5]"; + return false; + } + const float radius = std::max(0.05f, params.getFloat("radius", 0.72f)); + const float flattening = std::clamp(params.getFloat("flattening", 0.22f), 0.f, 0.7f); + const float angularity = std::clamp(params.getFloat("angularity", 0.38f), 0.f, 1.f); + const float erosion = std::clamp(params.getFloat("erosion", 0.16f), 0.f, 0.45f); + const float scale = std::max(0.25f, params.getFloat("scale", 2.4f)); + const int octaves = std::clamp(params.getInt("octaves", 4), 1, 8); + const uint32_t seed = params.getSeed(); + const std::string requestedShape = params.getString("baseShape", "mixed"); + ShapeProfile shape{}; + if (!resolveShape(requestedShape, seed, shape)) { + error = "mesh.rock: unknown baseShape '" + requestedShape + + "' (use mixed|boulder|slab|block|shard)"; + return false; + } + + uint32_t randomState = seed ^ 0x9e3779b9u; + const float variation = std::clamp(params.getFloat("variation", 0.42f), 0.f, 1.f); + shape.axisX *= 1.f + randomSigned(randomState) * 0.18f * variation; + shape.axisY *= 1.f + randomSigned(randomState) * 0.14f * variation; + shape.axisZ *= 1.f + randomSigned(randomState) * 0.18f * variation; + shape.power = std::clamp(shape.power + randomSigned(randomState) * 0.12f * variation, + 0.32f, 1.1f); + shape.skewX += randomSigned(randomState) * 0.12f * variation; + shape.skewZ += randomSigned(randomState) * 0.12f * variation; + if (params.has("axisX")) shape.axisX = std::max(0.2f, params.getFloat("axisX", shape.axisX)); + if (params.has("axisY")) shape.axisY = std::max(0.2f, params.getFloat("axisY", shape.axisY)); + if (params.has("axisZ")) shape.axisZ = std::max(0.2f, params.getFloat("axisZ", shape.axisZ)); + if (params.has("shapePower")) + shape.power = std::clamp(params.getFloat("shapePower", shape.power), 0.25f, 1.25f); + if (params.has("skewX")) shape.skewX = params.getFloat("skewX", shape.skewX); + if (params.has("skewZ")) shape.skewZ = params.getFloat("skewZ", shape.skewZ); + const int cutCount = std::clamp(params.getInt("cutCount", shape.cuts), 0, 12); + const float cutDepth = std::clamp(params.getFloat("cutDepth", shape.cutDepth), 0.f, 0.42f); + + std::vector directions; + std::vector triangles; + createIcosphere(subdivisions, directions, triangles); + std::vector positions(directions.size()); + std::vector normals(directions.size()); + const float steps = 3.f + angularity * 9.f; + for (size_t i = 0; i < directions.size(); ++i) { + const Vec3 d = directions[i]; + const float qx = std::round(d.x * steps) / steps; + const float qy = std::round(d.y * steps) / steps; + const float qz = std::round(d.z * steps) / steps; + const float strata = fbm3((qx + 2.3f) * scale, (qy + 4.7f) * scale, + (qz + 8.1f) * scale, seed, octaves); + const float pits = fbm3((d.x + 7.2f) * scale * 2.7f, + (d.y + 1.9f) * scale * 2.7f, + (d.z + 5.4f) * scale * 2.7f, + seed + 7919u, std::max(2, octaves - 1)); + const float displacement = strata * (0.08f + angularity * 0.16f) - + std::max(0.f, pits) * erosion; + const float r = std::max(radius * 0.45f, radius + displacement); + Vec3 p = {shapedCoordinate(d.x, shape.power) * r * shape.axisX, + shapedCoordinate(d.y, shape.power) * r * shape.axisY * (1.f - flattening), + shapedCoordinate(d.z, shape.power) * r * shape.axisZ}; + p.x += p.y * shape.skewX; + p.z += p.y * shape.skewZ; + positions[i] = p; + } + + // Clamp the deformed surface against deterministic planes. This preserves the icosphere's + // economical topology while creating broad fracture faces and non-round silhouettes. + for (int cut = 0; cut < cutCount; ++cut) { + Vec3 plane = normalize({randomSigned(randomState), randomSigned(randomState), + randomSigned(randomState)}); + if (cut == 0) plane = normalize({0.2f, -1.f, 0.1f}); // stable resting face + const float support = radius * std::sqrt( + plane.x * plane.x * shape.axisX * shape.axisX + + plane.y * plane.y * shape.axisY * shape.axisY * (1.f - flattening) * + (1.f - flattening) + + plane.z * plane.z * shape.axisZ * shape.axisZ); + const float depth = cutDepth * (0.72f + random01(randomState) * 0.56f); + const float limit = support * (1.f - depth); + for (Vec3 &p : positions) { + const float excess = p.x * plane.x + p.y * plane.y + p.z * plane.z - limit; + if (excess > 0.f) { + p.x -= plane.x * excess; + p.y -= plane.y * excess; + p.z -= plane.z * excess; + } + } + } + + for (const Tri &t : triangles) { + const Vec3 &a = positions[t.a], &b = positions[t.b], &c = positions[t.c]; + const Vec3 face = cross({b.x - a.x, b.y - a.y, b.z - a.z}, + {c.x - a.x, c.y - a.y, c.z - a.z}); + for (uint32_t index : {t.a, t.b, t.c}) { + normals[index].x += face.x; + normals[index].y += face.y; + normals[index].z += face.z; + } + } + for (Vec3 &n : normals) n = normalize(n); + + out.clear(); + out.reserve(int(positions.size()), int(triangles.size() * 3)); + for (size_t i = 0; i < positions.size(); ++i) { + const Vec3 &p = positions[i], &n = normals[i]; + out.addVertex(p.x, p.y, p.z, n.x, n.y, n.z, p.x + 0.5f, p.y + 0.5f); + } + for (const Tri &t : triangles) out.addTriangle(t.a, t.b, t.c); + out.setMeta("algorithm", "mesh.rock"); + out.setMeta("topology", "icosphere"); + out.setMeta("baseShape", shape.name); + out.setMeta("cutCount", std::to_string(cutCount)); + out.setMeta("subdivisions", std::to_string(subdivisions)); + return true; +} + +} // namespace eve::procgen diff --git a/src/modules/procgen/algorithms/RockMesh.h b/src/modules/procgen/algorithms/RockMesh.h new file mode 100644 index 00000000..6c3cba88 --- /dev/null +++ b/src/modules/procgen/algorithms/RockMesh.h @@ -0,0 +1,13 @@ +#pragma once + +#include "procgen/MeshBuild.h" +#include "procgen/Params.h" + +#include + +namespace eve::procgen { + +/** Build a shared-vertex, deformed icosphere rock for economical game props. */ +bool generateRockMesh(const Params ¶ms, MeshBuild &out, std::string &error); + +} // namespace eve::procgen diff --git a/test/procgen.cpp b/test/procgen.cpp index a251eddf..c6691eb8 100644 --- a/test/procgen.cpp +++ b/test/procgen.cpp @@ -220,6 +220,58 @@ TEST_CASE("procgen.registry.builtins") { CHECK(GeneratorRegistry::instance().has("wfc.simple")); } +TEST_CASE("procgen.mesh.rock.reproducibleAndControllable") { + MeshRecipeRegistry::instance().registerBuiltins(); + Params p; + p.setSeed(1847); + p.setInt("subdivisions", 3); + p.setFloat("radius", 0.7f); + p.setFloat("flattening", 0.3f); + p.setFloat("angularity", 0.45f); + p.setFloat("erosion", 0.16f); + p.setFloat("scale", 2.4f); + MeshBuild a, b, flatter; + std::string err; + REQUIRE(MeshRecipeRegistry::instance().generate("mesh.rock", p, a, err)); + REQUIRE(MeshRecipeRegistry::instance().generate("mesh.rock", p, b, err)); + CHECK_EQ(a.getVertexCount(), 642); + CHECK_EQ(a.getIndexCount() / 3, 1280); + CHECK(a.positions() == b.positions()); + CHECK(a.indices() == b.indices()); + CHECK(meshIndicesInRange(a)); + CHECK(meshNormalsFiniteUnit(a)); + + p.setFloat("flattening", 0.58f); + REQUIRE(MeshRecipeRegistry::instance().generate("mesh.rock", p, flatter, err)); + CHECK(a.positions() != flatter.positions()); + + p.setInt("subdivisions", 2); + MeshBuild lod1; + REQUIRE(MeshRecipeRegistry::instance().generate("mesh.rock", p, lod1, err)); + CHECK_EQ(lod1.getVertexCount(), 162); + CHECK_EQ(lod1.getIndexCount() / 3, 320); + + std::vector> shapePositions; + for (const char *shape : {"boulder", "slab", "block", "shard"}) { + p.setString("baseShape", shape); + p.setInt("subdivisions", 3); + MeshBuild variant; + REQUIRE(MeshRecipeRegistry::instance().generate("mesh.rock", p, variant, err)); + CHECK_EQ(variant.getMeta("baseShape", ""), shape); + CHECK_EQ(variant.getVertexCount(), 642); + CHECK(meshIndicesInRange(variant)); + CHECK(meshNormalsFiniteUnit(variant)); + shapePositions.push_back(variant.positions()); + } + CHECK(shapePositions[0] != shapePositions[1]); + CHECK(shapePositions[1] != shapePositions[2]); + CHECK(shapePositions[2] != shapePositions[3]); + + p.setString("baseShape", "invalid"); + MeshBuild invalid; + CHECK(!MeshRecipeRegistry::instance().generate("mesh.rock", p, invalid, err)); +} + TEST_CASE("procgen.dungeon.bsp.reproducible") { Params p; p.setSeed(42); From 3f3f01298dc9f0acfcbc951db179600ad3f126ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Sat, 15 Aug 2026 12:41:06 +0800 Subject: [PATCH 13/21] feat(procgen): add procedural tree generator --- CMakeLists.txt | 20 + docs/dev/依赖项.md | 10 + docs/usr/modules/procgen.md | 32 + examples/tree-generator/README.md | 70 +++ examples/tree-generator/config.nut | 7 + examples/tree-generator/main.nut | 82 +++ src/modules/procgen/Procgen.h | 2 +- .../procgen/algorithms/MarchingCubes.cpp | 2 + src/modules/procgen/algorithms/TreeMesh.cpp | 558 ++++++++++++++++++ src/modules/procgen/algorithms/TreeMesh.h | 13 + test/procgen.cpp | 170 ++++++ 11 files changed, 965 insertions(+), 1 deletion(-) create mode 100644 examples/tree-generator/README.md create mode 100644 examples/tree-generator/config.nut create mode 100644 examples/tree-generator/main.nut create mode 100644 src/modules/procgen/algorithms/TreeMesh.cpp create mode 100644 src/modules/procgen/algorithms/TreeMesh.h diff --git a/CMakeLists.txt b/CMakeLists.txt index fd87a1bc..fd2d1bd2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,8 @@ set(EVENGINE_PATCH_VERSION "0") option(BUILD_TESTING "enable testing" ON) option(EVENGINE_BUILD_DEMO "Build optional Demo module (procedural demo assets + default demo.nut)" ON) +set(EVENGINE_PREBUILT_THIRD_PARTY "" CACHE PATH + "Read-only prebuilt third-party install (contains include/, lib/, bin/); skips ExternalProject") message("Build UnitTest: " ${BUILD_TESTING}) message("Build Demo module: " ${EVENGINE_BUILD_DEMO}) if (CMAKE_BUILD_TYPE STREQUAL "") @@ -519,6 +521,24 @@ function(load_third_party) set(TP_BUILD_PATH ${BUILD_PLATFORM}${DEBUG_APPENDIX}) message("Third Party Build Path: ${TP_BUILD_PATH}") + if(EVENGINE_PREBUILT_THIRD_PARTY) + cmake_path(ABSOLUTE_PATH EVENGINE_PREBUILT_THIRD_PARTY NORMALIZE + OUTPUT_VARIABLE _eve_prebuilt_tp) + if(NOT IS_DIRECTORY "${_eve_prebuilt_tp}/include" OR + NOT IS_DIRECTORY "${_eve_prebuilt_tp}/lib") + message(FATAL_ERROR + "EVENGINE_PREBUILT_THIRD_PARTY must contain include/ and lib/: ${_eve_prebuilt_tp}") + endif() + message(STATUS "third-party: using read-only prebuilt install ${_eve_prebuilt_tp}") + include_directories("${_eve_prebuilt_tp}/include") + link_directories("${_eve_prebuilt_tp}/lib") + # Preserve the dependency target expected by create_module(), but give it no + # configure/build/install commands so the prebuilt tree can remain read-only. + add_custom_target(third-party) + add_custom_target(deps DEPENDS third-party) + return() + endif() + ensure_third_party_checkout() # EVEngine's database module uses POCO Data and its bundled SQLite connector. # The third-party aggregate disables these by default to reduce its footprint; diff --git a/docs/dev/依赖项.md b/docs/dev/依赖项.md index 79539cf9..57757d63 100644 --- a/docs/dev/依赖项.md +++ b/docs/dev/依赖项.md @@ -21,6 +21,16 @@ smmalloc - 用来优化内存分配和实现snapshot的内存分配器 CLI11 - 命令行解析 rang - 命令行彩色输出 +### 只读预编译第三方目录 + +已有完整第三方安装树时,可在配置阶段传入: + +```sh +cmake -S . -B build -DEVENGINE_PREBUILT_THIRD_PARTY=/path/to/third-party-binary/win32-debug +``` + +该目录必须包含 `include/` 与 `lib/`。启用后 CMake 只从该目录读取头文件和库,并创建兼容现有模块依赖关系的空 `third-party` 目标;不会配置、构建或安装到这个目录。 + ### 程序化生成(`procgen`) | 库 | 许可 | 用途 | 状态 | diff --git a/docs/usr/modules/procgen.md b/docs/usr/modules/procgen.md index 2d83eda8..579228b8 100644 --- a/docs/usr/modules/procgen.md +++ b/docs/usr/modules/procgen.md @@ -34,6 +34,37 @@ local cpu = gen.buildMesh("mesh.marchingcubes", p); local gpu = gen.generateMesh("mesh.marchingcubes", p, gfx); ``` +### 生成随机树木网格 + +`mesh.tree` 生成可复现的树干、分枝和叶片网格。`branchAlgorithm` 的两个值互斥: + +- `weberPenn`(默认):按层级、枝序和分枝角快速生成稳定骨架。 +- `spaceColonization`:让枝梢向树冠吸引点迭代生长,适合更不规则的冠形。 + +```squirrel +local p = gen.newParams(); +p.setSeed(31415); +p.setString("style", "lowpoly"); // lowpoly | realistic +p.setString("branchAlgorithm", "weberPenn"); +p.setString("leafMode", "canopy"); // cards | canopy | none +p.setFloat("leafDensity", 0.75); +p.setFloat("height", 6.0); +p.setFloat("crownRadius", 2.0); + +local tree = gen.generateMesh("mesh.tree", p, gfx); +``` + +两种算法共享主干曲率、向性、下垂、随高度变化的枝长/枝径,以及上下层叶片覆盖参数。常用调整: + +- 外形与精度:`height`、`trunkRadius`、`crownRadius`、`radialSegments`、`curveSegments`。 +- 通用枝形:`trunkCurve`、`branchCurve`、`curveBack`、`tropism`、`droop`。 +- 层级生长:`branchLengthFalloff`、`branchRadiusFalloff`;默认表现为下层枝更粗更长。 +- 叶片:`leafSize`、`leafDensity`、`foliageStart`、`lowerLeafCoverage`、`upperLeafCoverage`。 +- Weber–Penn:`branchLevels`、`branchCount`、`branchAngle`、`branchAngleVariation`、`phyllotaxis`、`apicalDominance`。 +- 空间殖民:`attractorCount`、`colonizationIterations`、`influenceRadius`、`killRadius`、`growthStep`、`branchInertia`、`maxTurnAngle`、`maxCumulativeAngle`、`maxChildren`。 + +完整参数范围、交互快捷键和可运行脚本见 [`examples/tree-generator`](../../../examples/tree-generator/README.md)。生成网格有一定成本,应在加载、换 seed 或修改参数时重建,不要每帧调用。 + ### 生成无缝材质 设置 texture recipe、尺寸、octaves、pixelSize 和 seamless,调用纹理或法线图生成接口;开发期切换 seed 预览,发布时缓存 Texture,不能每帧重新生成。 @@ -43,6 +74,7 @@ local gpu = gen.generateMesh("mesh.marchingcubes", p, gfx); - 未保存 seed,无法复现玩家问题。 - Output palette 缺少算法输出的 tile key。 - 在每帧 update 生成大地图或纹理。 +- 在每帧重新生成树木网格;应缓存 `Mesh`,仅在 seed 或参数变化时重建。 ## API 快查 diff --git a/examples/tree-generator/README.md b/examples/tree-generator/README.md new file mode 100644 index 00000000..fbdca0e6 --- /dev/null +++ b/examples/tree-generator/README.md @@ -0,0 +1,70 @@ +# Arbor Lab — 随机树木生成器 + +`mesh.tree` 是 EVEngine 的原生程序化网格配方。相同 seed 与参数会生成一致的网格,可通过 +`procgen.buildMesh` 获取 CPU 数据,或通过 `procgen.generateMesh` 直接上传 GPU。 + +## 快速使用 + +```squirrel +local p = procgen.newParams(); +p.setSeed(31415); +p.setString("style", "lowpoly"); +p.setString("branchAlgorithm", "weberPenn"); +p.setString("leafMode", "canopy"); +p.setFloat("leafDensity", 0.75); + +local mesh = procgen.generateMesh("mesh.tree", p, gfx); +``` + +`branchAlgorithm` 只能选择一种骨架算法:`weberPenn` 是默认值,规则稳定、生成较快; +`spaceColonization` 通过树冠吸引点迭代生长,轮廓更不规则。主干、枝条层级和叶片系统由两种算法共享。 + +## 通用参数 + +- `style`: `lowpoly` / `realistic` +- `leafMode`: `cards`(独立双面叶片)/ `canopy`(整片冠层网格)/ `none` +- `leafDensity`: `0..1` +- `height`, `trunkRadius`, `crownRadius`, `leafSize` +- `foliageStart`: 树冠起始高度比例,`0.1..0.9` +- `radialSegments`: 枝干横截面精度,`3..24` +- `curveSegments`: 主干/枝条弯曲分段数,`2..20` +- `trunkCurve`: 主干整体弯曲强度,`0..0.45` +- `branchCurve`: 枝条弯曲强度,`0..0.5` +- `curveBack`: S 形回弯,`-0.5..0.5` +- `tropism`: 向上生长趋势,`-0.5..0.8` +- `droop`: 枝梢下垂,`0..0.8` +- `branchLengthFalloff`: 随树冠高度增加的枝长衰减,`0..0.9` +- `branchRadiusFalloff`: 随树冠高度增加的枝径衰减,`0..0.9` +- `lowerLeafCoverage`: 下层老枝沿枝条分布叶片的概率,`0..1` +- `upperLeafCoverage`: 上层新梢沿枝条分布叶片的概率,`0..1` + +## Weber–Penn 参数 + +- `branchLevels`: `1..5` +- `branchCount`: 每层主要分枝数量,`2..20` +- `branchAngle`, `branchAngleVariation`: 分枝角度与随机变化(度) +- `phyllotaxis`: 枝序旋转角(度),默认 `137.5` +- `apicalDominance`: 顶端优势,`0..1` + +## 空间殖民参数 + +- `attractorCount`: 树冠吸引点数量,`12..1200` +- `colonizationIterations`: 最大迭代数,`4..160` +- `influenceRadius`, `killRadius`: 吸引范围与到达判定距离 +- `growthStep`: 每轮枝梢生长步长 +- `branchInertia`: 延续上一段方向的惯性,`0..4` +- `maxTurnAngle`: 相邻生长段允许的最大转角(度) +- `maxCumulativeAngle`: 枝条偏离初始生长轴的最大角度,用于阻止 U 形回卷 +- `maxChildren`: 单个生长节点允许的最大子枝数,`1..4` + +## 运行示例 + +```sh +make run/linux-debug GAME=examples/tree-generator +make run/win32-debug GAME=examples/tree-generator +``` + +快捷键:`R` 换 seed,`1/2` 切换 Low Poly/写实,`A` 切换分枝算法,`L` 开关叶片, +`C` 切换整片冠层,`[` / `]` 调节叶片密度。 + +树木网格应在加载、换 seed 或修改参数时生成并缓存,不要每帧重新生成。 diff --git a/examples/tree-generator/config.nut b/examples/tree-generator/config.nut new file mode 100644 index 00000000..f331131b --- /dev/null +++ b/examples/tree-generator/config.nut @@ -0,0 +1,7 @@ +config <- { + width = 1100 + height = 720 + title = "Arbor Lab — Procedural Tree Generator" + debug = false + hotReload = true +}; diff --git a/examples/tree-generator/main.nut b/examples/tree-generator/main.nut new file mode 100644 index 00000000..daf71945 --- /dev/null +++ b/examples/tree-generator/main.nut @@ -0,0 +1,82 @@ +// Arbor Lab — interactive procedural tree recipe showcase. +// R regenerate, 1/2 Low Poly/Realistic, A algorithm, L leaves, [/] density, C canopy. + +if (!("treeSeed" in getroottable())) treeSeed <- 31415; +if (!("treeStyle" in getroottable())) treeStyle <- "lowpoly"; +if (!("treeAlgorithm" in getroottable())) treeAlgorithm <- "weberPenn"; +if (!("leafMode" in getroottable())) leafMode <- "cards"; +if (!("leafDensity" in getroottable())) leafDensity <- 0.65; +if (!("treeMesh" in getroottable())) treeMesh <- null; +if (!("treeObject" in getroottable())) treeObject <- null; +if (!("treeCamera" in getroottable())) treeCamera <- null; +if (!("treeYaw" in getroottable())) treeYaw <- 0.0; +if (!("prevTreeKeys" in getroottable())) prevTreeKeys <- {}; + +function pressed(k) { + local down = keyboard.isDown(k); + local old = k in prevTreeKeys ? prevTreeKeys[k] : false; + prevTreeKeys[k] <- down; + return down && !old; +} + +function rebuildTree() { + local p = procgen.newParams(); + p.setSeed(treeSeed); + p.setString("style", treeStyle); + p.setString("branchAlgorithm", treeAlgorithm); + p.setString("leafMode", leafMode); + p.setFloat("leafDensity", leafDensity); + p.setFloat("height", 6.2); + p.setFloat("crownRadius", 2.15); + p.setInt("branchLevels", treeStyle == "realistic" ? 3 : 2); + p.setInt("branchCount", treeStyle == "realistic" ? 10 : 6); + treeMesh = procgen.generateMesh("mesh.tree", p, gfx); + if (treeObject == null) { + treeObject = eve.Renderable3D(); + treeObject.setPosition(1.35, -3.0, 0.0); + treeObject.setTint(0.34, 0.48, 0.22, 1.0); + treeObject.setRoughness(0.86); + treeObject.setCastShadow(true); + } + treeObject.setMesh(treeMesh); +} + +if (treeCamera == null) { + treeCamera = eve.Camera3D(); + treeCamera.setEye(10.0, 4.6, 12.0); + treeCamera.setTarget(1.1, 0.2, 0.0); + treeCamera.setUp(0.0, 1.0, 0.0); + treeCamera.setFov(38.0); + treeCamera.setAmbient(0.32, 0.38, 0.30); + treeCamera.setActive(true); + gfx.setDirectionalLight(-0.45, -1.0, -0.35, 1.35, 1.24, 1.02); +} +if (treeMesh == null) rebuildTree(); + +function update(dt) { + treeYaw += dt * 7.0; + treeObject.setYaw(treeYaw); + if (pressed("r") || pressed("R")) { treeSeed += 1; rebuildTree(); } + if (pressed("1")) { treeStyle = "lowpoly"; rebuildTree(); } + if (pressed("2")) { treeStyle = "realistic"; rebuildTree(); } + if (pressed("a") || pressed("A")) { + treeAlgorithm = treeAlgorithm == "weberPenn" ? "spaceColonization" : "weberPenn"; + rebuildTree(); + } + if (pressed("l") || pressed("L")) { + leafMode = leafMode == "none" ? "cards" : "none"; + rebuildTree(); + } + if (pressed("c") || pressed("C")) { + leafMode = leafMode == "canopy" ? "cards" : "canopy"; + rebuildTree(); + } + if (pressed("leftbracket")) { leafDensity = max(0.0, leafDensity - 0.1); rebuildTree(); } + if (pressed("rightbracket")) { leafDensity = min(1.0, leafDensity + 0.1); rebuildTree(); } +} + +function draw() { + gfx.clear(0.055, 0.075, 0.07, 1.0); + gfx.render3D(); + gfx.drawSolidRect(28, 28, 310, 212, 0.92, 0.94, 0.86, 0.96); +} diff --git a/src/modules/procgen/Procgen.h b/src/modules/procgen/Procgen.h index ddeb27e2..2dafa81f 100644 --- a/src/modules/procgen/Procgen.h +++ b/src/modules/procgen/Procgen.h @@ -71,7 +71,7 @@ public: bool hasTextureRecipe(const std::string &recipeId) const; // --- Mesh recipes (Marching Cubes, …) --- - /** CPU mesh (caller owns). Recipes: mesh.marchingcubes. */ + /** CPU mesh (caller owns). Recipes: mesh.marchingcubes, mesh.tree. */ MeshBuild *buildMesh(const std::string &recipeId, Params *params); /** Build + upload to GPU Mesh (owned by Graphics). */ graphics::Mesh *generateMesh(const std::string &recipeId, Params *params, diff --git a/src/modules/procgen/algorithms/MarchingCubes.cpp b/src/modules/procgen/algorithms/MarchingCubes.cpp index 1a76746e..f85961c1 100644 --- a/src/modules/procgen/algorithms/MarchingCubes.cpp +++ b/src/modules/procgen/algorithms/MarchingCubes.cpp @@ -1,4 +1,5 @@ #include "procgen/algorithms/MarchingCubes.h" +#include "procgen/algorithms/TreeMesh.h" #include #include @@ -300,6 +301,7 @@ std::vector MeshRecipeRegistry::list() const { void MeshRecipeRegistry::registerBuiltins() { if (builtinsRegistered_) return; registerRecipe("mesh.marchingcubes", generateMarchingCubesMesh); + registerRecipe("mesh.tree", generateTreeMesh); builtinsRegistered_ = true; } diff --git a/src/modules/procgen/algorithms/TreeMesh.cpp b/src/modules/procgen/algorithms/TreeMesh.cpp new file mode 100644 index 00000000..9317d9c2 --- /dev/null +++ b/src/modules/procgen/algorithms/TreeMesh.cpp @@ -0,0 +1,558 @@ +#include "procgen/algorithms/TreeMesh.h" + +#include +#include +#include +#include +#include + +namespace eve::procgen { +namespace { + +constexpr float kPi = 3.14159265358979323846f; + +struct V3 { + float x = 0.f, y = 0.f, z = 0.f; +}; + +V3 add(V3 a, V3 b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; } +V3 sub(V3 a, V3 b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; } +V3 mul(V3 a, float s) { return {a.x * s, a.y * s, a.z * s}; } +float dot(V3 a, V3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; } +float distanceSquared(V3 a, V3 b) { return dot(sub(a, b), sub(a, b)); } +V3 cross(V3 a, V3 b) { return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; } +V3 norm(V3 a) { + const float n = std::sqrt(std::max(1e-12f, dot(a, a))); + return mul(a, 1.f / n); +} + +float randomRange(std::mt19937& rng, float lo, float hi) { return std::uniform_real_distribution(lo, hi)(rng); } + +float hash01(uint32_t value) { + value ^= value >> 16; + value *= 0x7feb352du; + value ^= value >> 15; + value *= 0x846ca68bu; + value ^= value >> 16; + return float(value & 0x00ffffffu) / float(0x01000000u); +} + +void basisFor(V3 axis, V3& right, V3& forward) { + axis = norm(axis); + const V3 helper = std::fabs(axis.y) < 0.92f ? V3{0.f, 1.f, 0.f} : V3{1.f, 0.f, 0.f}; + right = norm(cross(helper, axis)); + forward = norm(cross(axis, right)); +} + +void addTaperedCylinder(MeshBuild& out, V3 a, V3 b, float r0, float r1, int sides) { + const V3 axis = norm(sub(b, a)); + V3 right, forward; + basisFor(axis, right, forward); + const uint32_t base = uint32_t(out.getVertexCount()); + for (int ring = 0; ring < 2; ++ring) { + const V3 center = ring ? b : a; + const float radius = ring ? r1 : r0; + for (int i = 0; i < sides; ++i) { + const float t = float(i) / float(sides); + const float angle = t * 2.f * kPi; + const V3 radial = add(mul(right, std::cos(angle)), mul(forward, std::sin(angle))); + const V3 p = add(center, mul(radial, radius)); + // Left side of the tree atlas is bark; foliage uses the right side. + out.addVertex(p.x, p.y, p.z, radial.x, radial.y, radial.z, t * 0.45f, float(ring)); + } + } + for (int i = 0; i < sides; ++i) { + const uint32_t n = uint32_t((i + 1) % sides); + const uint32_t i0 = base + uint32_t(i), i1 = base + n; + const uint32_t i2 = base + uint32_t(sides) + uint32_t(i); + const uint32_t i3 = base + uint32_t(sides) + n; + out.addTriangle(i0, i2, i1); + out.addTriangle(i1, i2, i3); + } +} + +void addLeafCard(MeshBuild& out, V3 c, V3 direction, float size, float twist) { + V3 right, up; + basisFor(norm(direction), right, up); + right = add(mul(right, std::cos(twist)), mul(up, std::sin(twist))); + up = norm(cross(norm(direction), right)); + const V3 r = mul(right, size * 0.5f), u = mul(up, size); + const V3 normal = norm(cross(right, up)); + const uint32_t base = uint32_t(out.getVertexCount()); + const V3 points[4] = {sub(sub(c, r), u), add(sub(c, u), r), add(add(c, r), u), add(sub(c, r), u)}; + const float uv[4][2] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; + for (int i = 0; i < 4; ++i) + out.addVertex(points[i].x, points[i].y, points[i].z, normal.x, normal.y, normal.z, 0.55f + uv[i][0] * 0.45f, + uv[i][1]); + out.addTriangle(base, base + 1, base + 2); + out.addTriangle(base, base + 2, base + 3); + out.addTriangle(base + 2, base + 1, base); + out.addTriangle(base + 3, base + 2, base); +} + +void addCanopyBlob(MeshBuild& out, V3 center, V3 radius, int rings, int sides) { + const uint32_t base = uint32_t(out.getVertexCount()); + for (int y = 0; y <= rings; ++y) { + const float v = float(y) / float(rings); + const float phi = v * kPi; + for (int x = 0; x < sides; ++x) { + const float u = float(x) / float(sides); + const float theta = u * 2.f * kPi; + const V3 n = {std::sin(phi) * std::cos(theta), std::cos(phi), std::sin(phi) * std::sin(theta)}; + const V3 p = {center.x + n.x * radius.x, center.y + n.y * radius.y, center.z + n.z * radius.z}; + out.addVertex(p.x, p.y, p.z, n.x, n.y, n.z, 0.55f + u * 0.45f, v); + } + } + for (int y = 0; y < rings; ++y) { + for (int x = 0; x < sides; ++x) { + const int nx = (x + 1) % sides; + const uint32_t a = base + uint32_t(y * sides + x); + const uint32_t b = base + uint32_t(y * sides + nx); + const uint32_t c = base + uint32_t((y + 1) * sides + x); + const uint32_t d = base + uint32_t((y + 1) * sides + nx); + out.addTriangle(a, c, b); + out.addTriangle(b, c, d); + } + } +} + +struct Tip { + V3 p; + V3 dir; + float scale; +}; + +struct StemPath { + std::vector points; + V3 endDirection; +}; + +StemPath makeCurvedStem(V3 start, V3 direction, V3 bendAxis, float length, float curve, float curveBack, + float verticalAcceleration, int segments) { + StemPath path; + path.points.reserve(size_t(segments + 1)); + direction = norm(direction); + bendAxis = norm(sub(bendAxis, mul(direction, dot(bendAxis, direction)))); + const V3 up{0.f, 1.f, 0.f}; + for (int i = 0; i <= segments; ++i) { + const float t = float(i) / float(segments); + // Both offsets have zero slope at the branch base, avoiding an immediate kink. + const float primary = curve * t * t; + const float back = curveBack * std::sin(2.f * kPi * t) * t * (1.f - t); + const V3 p = add(start, add(mul(direction, length * t), add(mul(bendAxis, length * (primary + back)), + mul(up, length * verticalAcceleration * t * t)))); + path.points.push_back(p); + } + path.endDirection = norm(sub(path.points.back(), path.points[path.points.size() - 2])); + return path; +} + +void addStem(MeshBuild& out, const StemPath& path, float r0, float r1, int sides) { + const int segmentCount = int(path.points.size()) - 1; + for (int i = 0; i < segmentCount; ++i) { + const float t0 = float(i) / float(segmentCount); + const float t1 = float(i + 1) / float(segmentCount); + const float a = std::pow(t0, 1.25f); + const float b = std::pow(t1, 1.25f); + addTaperedCylinder(out, path.points[size_t(i)], path.points[size_t(i + 1)], r0 + (r1 - r0) * a, + r0 + (r1 - r0) * b, sides); + } +} + +void samplePath(const StemPath& path, float t, V3& point, V3& direction) { + const int segmentCount = int(path.points.size()) - 1; + const float x = std::clamp(t, 0.f, 1.f) * float(segmentCount); + const int i = std::min(int(x), segmentCount - 1); + const float local = x - float(i); + point = add(path.points[size_t(i)], mul(sub(path.points[size_t(i + 1)], path.points[size_t(i)]), local)); + direction = norm(sub(path.points[size_t(i + 1)], path.points[size_t(i)])); +} + +struct ColonyNode { + V3 p; + V3 dir; + V3 heading{0.f, 1.f, 0.f}; + int parent = -1; + int descendants = 1; + int children = 0; + int depth = 0; + float remainingLength = 0.f; + float vigor = 1.f; + float budHeight = 0.f; +}; + +struct ColonizationSettings { + float height; + float foliageStart; + float crownRadius; + float trunkRadius; + float tropism; + float droop; + float inertia; + int attractorCount; + int iterations; + float influenceRadius; + float killRadius; + float step; + float maxTurnAngle; + float maxCumulativeAngle; + float lengthFalloff; + float radiusFalloff; + float lowerLeafCoverage; + float upperLeafCoverage; + int maxChildren; + int sides; +}; + +void growSpaceColonizedTree(MeshBuild& out, const StemPath& trunk, std::mt19937& rng, + const ColonizationSettings& settings, std::vector& foliageAnchors) { + V3 crownCenter, ignoredDirection; + samplePath(trunk, settings.foliageStart + (1.f - settings.foliageStart) * 0.53f, crownCenter, ignoredDirection); + const float crownHalfHeight = settings.height * (1.f - settings.foliageStart) * 0.52f; + + std::vector attractors; + attractors.reserve(size_t(settings.attractorCount)); + while (int(attractors.size()) < settings.attractorCount) { + const V3 q{randomRange(rng, -1.f, 1.f), randomRange(rng, -1.f, 1.f), randomRange(rng, -1.f, 1.f)}; + if (dot(q, q) > 1.f) continue; + // A modest upward bias avoids a perfectly symmetric, balloon-like crown. + attractors.push_back(add(crownCenter, {q.x * settings.crownRadius, + q.y * crownHalfHeight + 0.12f * crownHalfHeight * (1.f - q.y * q.y), + q.z * settings.crownRadius})); + } + + std::vector nodes; + // Multiple trunk buds provide the primary scaffold. Colonization then shapes and + // splits those limbs; starting from a single bud produces a shrub-like fan. + const int seedCount = 9; + for (int i = 0; i < seedCount; ++i) { + V3 p, dir; + const float t = + settings.foliageStart + (1.f - settings.foliageStart) * (0.08f + 0.78f * float(i) / float(seedCount - 1)); + samplePath(trunk, t, p, dir); + const float crownHeight = float(i) / float(seedCount - 1); + const float lengthScale = 1.f - settings.lengthFalloff * crownHeight; + const float vigor = 1.f - settings.radiusFalloff * crownHeight; + nodes.push_back({p, dir, dir, -1, 1, 0, 0, settings.crownRadius * 1.35f * lengthScale, vigor, crownHeight}); + } + + std::vector alive(size_t(settings.attractorCount), 1); + const float influenceSquared = settings.influenceRadius * settings.influenceRadius; + const float killSquared = settings.killRadius * settings.killRadius; + const float separationSquared = settings.step * settings.step * 0.22f; + const float maxTurn = settings.maxTurnAngle * kPi / 180.f; + const float maxTurnCos = std::cos(maxTurn); + const float maxTurnSin = std::sin(maxTurn); + const float cumulativeTurn = settings.maxCumulativeAngle * kPi / 180.f; + const float cumulativeTurnCos = std::cos(cumulativeTurn); + const float cumulativeTurnSin = std::sin(cumulativeTurn); + for (int iteration = 0; iteration < settings.iterations; ++iteration) { + const size_t nodeCount = nodes.size(); + std::vector directionSums(nodeCount); + std::vector directionCounts(nodeCount, 0); + int liveAttractors = 0; + for (size_t ai = 0; ai < attractors.size(); ++ai) { + if (!alive[ai]) continue; + ++liveAttractors; + size_t nearest = 0; + float best = influenceSquared; + bool found = false; + for (size_t ni = 0; ni < nodeCount; ++ni) { + const V3 toward = sub(attractors[ai], nodes[ni].p); + const float d = dot(toward, toward); + if (d < killSquared) { + alive[ai] = 0; + found = false; + break; + } + // Once a shoot has left the trunk, attraction from behind may not + // reverse it. This is the main distinction between a branch and a vine. + if (nodes[ni].depth > 0) { + const V3 attractionDirection = norm(toward); + if (dot(attractionDirection, nodes[ni].dir) < -0.08f || + dot(attractionDirection, nodes[ni].heading) < 0.04f) + continue; + } + if (d < best) { + best = d; + nearest = ni; + found = true; + } + } + if (found) { + directionSums[nearest] = add(directionSums[nearest], norm(sub(attractors[ai], nodes[nearest].p))); + ++directionCounts[nearest]; + } + } + if (liveAttractors == 0) break; + + std::vector additions; + for (size_t ni = 0; ni < nodeCount; ++ni) { + if (directionCounts[ni] == 0 || nodes[ni].children >= settings.maxChildren || + nodes[ni].remainingLength < settings.step) + continue; + V3 direction = norm( + add(mul(directionSums[ni], 1.f / float(directionCounts[ni])), mul(nodes[ni].dir, settings.inertia))); + const float vertical = settings.tropism - settings.droop * std::max(0.f, 1.f - direction.y); + direction = norm(add(direction, {0.f, vertical, 0.f})); + // Clamp curvature per growth step. Linear blending alone can still turn + // sharply when the remaining attractors move to the other side of a tip. + const float alignment = std::clamp(dot(nodes[ni].dir, direction), -1.f, 1.f); + if (nodes[ni].depth > 0 && alignment < maxTurnCos) { + V3 tangent = sub(direction, mul(nodes[ni].dir, alignment)); + if (dot(tangent, tangent) > 1e-8f) + direction = norm(add(mul(nodes[ni].dir, maxTurnCos), mul(norm(tangent), maxTurnSin))); + else + direction = nodes[ni].dir; + } + if (nodes[ni].depth > 0) { + const float headingAlignment = std::clamp(dot(nodes[ni].heading, direction), -1.f, 1.f); + if (headingAlignment < cumulativeTurnCos) { + V3 tangent = sub(direction, mul(nodes[ni].heading, headingAlignment)); + if (dot(tangent, tangent) > 1e-8f) + direction = + norm(add(mul(nodes[ni].heading, cumulativeTurnCos), mul(norm(tangent), cumulativeTurnSin))); + else + direction = nodes[ni].heading; + } + } else if (direction.y < -0.05f) { + // Primary limbs may spread horizontally, but do not launch downward. + direction = norm({direction.x, -0.05f, direction.z}); + } + const V3 candidate = add(nodes[ni].p, mul(direction, settings.step)); + bool separated = true; + for (size_t oi = 0; oi < nodes.size() && separated; ++oi) + separated = distanceSquared(candidate, nodes[oi].p) >= separationSquared; + for (size_t oi = 0; oi < additions.size() && separated; ++oi) + separated = distanceSquared(candidate, additions[oi].p) >= separationSquared; + if (separated) { + const V3 heading = nodes[ni].depth == 0 ? direction : nodes[ni].heading; + additions.push_back({candidate, direction, heading, int(ni), 1, 0, nodes[ni].depth + 1, + nodes[ni].remainingLength - settings.step, nodes[ni].vigor, nodes[ni].budHeight}); + ++nodes[ni].children; + } + } + if (additions.empty()) break; + nodes.insert(nodes.end(), additions.begin(), additions.end()); + } + + for (int i = int(nodes.size()) - 1; i >= 0; --i) { + const int parent = nodes[size_t(i)].parent; + if (parent >= 0) { + nodes[size_t(parent)].descendants += nodes[size_t(i)].descendants; + } + } + const float twigRadius = settings.trunkRadius * 0.055f; + for (size_t i = 0; i < nodes.size(); ++i) { + const ColonyNode& node = nodes[i]; + if (node.parent < 0) continue; + const ColonyNode& parent = nodes[size_t(node.parent)]; + const float r0 = std::min(settings.trunkRadius * 0.34f * parent.vigor, + twigRadius * parent.vigor * (1.f + std::sqrt(float(parent.descendants)))); + const float r1 = std::min(r0 * 0.92f, twigRadius * node.vigor * (1.f + std::sqrt(float(node.descendants)))); + addTaperedCylinder(out, parent.p, node.p, r0, r1, std::max(3, settings.sides - 2)); + if (node.children == 0) { + foliageAnchors.push_back({node.p, node.dir, 0.42f + 0.22f * node.vigor}); + } else if (node.depth >= 2 && (node.depth % 2) == 0) { + // Older lower limbs carry foliage along the branch, whereas young upper + // shoots keep most foliage near their tips. + const float coverage = + settings.lowerLeafCoverage + (settings.upperLeafCoverage - settings.lowerLeafCoverage) * node.budHeight; + if (hash01(uint32_t(i) * 747796405u + 2891336453u) < coverage) + foliageAnchors.push_back({node.p, node.dir, 0.48f + 0.20f * (1.f - node.budHeight)}); + } + } +} + +} // namespace + +bool generateTreeMesh(const Params& params, MeshBuild& out, std::string& error) { + const std::string style = params.getString("style", "lowpoly"); + const std::string leafMode = params.getString("leafMode", "cards"); + const std::string branchAlgorithm = params.getString("branchAlgorithm", "weberPenn"); + if (style != "lowpoly" && style != "realistic") { + error = "mesh.tree: style must be lowpoly|realistic"; + return false; + } + if (leafMode != "cards" && leafMode != "canopy" && leafMode != "none") { + error = "mesh.tree: leafMode must be cards|canopy|none"; + return false; + } + if (branchAlgorithm != "weberPenn" && branchAlgorithm != "spaceColonization") { + error = "mesh.tree: branchAlgorithm must be weberPenn|spaceColonization"; + return false; + } + + const bool realistic = style == "realistic"; + const float height = std::max(0.5f, params.getFloat("height", 6.f)); + const float trunkRadius = std::max(0.02f, params.getFloat("trunkRadius", height * 0.055f)); + const float crownRadius = std::max(0.1f, params.getFloat("crownRadius", height * 0.34f)); + const float leafSize = std::max(0.02f, params.getFloat("leafSize", height * 0.075f)); + const float density = std::clamp(params.getFloat("leafDensity", 0.65f), 0.f, 1.f); + const float foliageStart = std::clamp(params.getFloat("foliageStart", 0.35f), 0.1f, 0.9f); + const int branchLevels = std::clamp(params.getInt("branchLevels", realistic ? 3 : 2), 1, 5); + const int branchCount = std::clamp(params.getInt("branchCount", realistic ? 9 : 6), 2, 20); + const int sides = std::clamp(params.getInt("radialSegments", realistic ? 10 : 6), 3, 24); + const int curveSegments = std::clamp(params.getInt("curveSegments", realistic ? 9 : 5), 2, 20); + const float trunkCurve = std::clamp(params.getFloat("trunkCurve", 0.10f), 0.f, 0.45f); + const float curveBack = std::clamp(params.getFloat("curveBack", 0.16f), -0.5f, 0.5f); + const float branchCurve = std::clamp(params.getFloat("branchCurve", 0.13f), 0.f, 0.5f); + const float branchAngle = std::clamp(params.getFloat("branchAngle", 62.f), 5.f, 88.f); + const float angleVariation = std::clamp(params.getFloat("branchAngleVariation", 12.f), 0.f, 40.f); + const float phyllotaxis = params.getFloat("phyllotaxis", 137.5f) * kPi / 180.f; + const float tropism = std::clamp(params.getFloat("tropism", 0.22f), -0.5f, 0.8f); + const float droop = std::clamp(params.getFloat("droop", 0.18f), 0.f, 0.8f); + const float apicalDominance = std::clamp(params.getFloat("apicalDominance", 0.62f), 0.f, 1.f); + const int attractorCount = std::clamp(params.getInt("attractorCount", realistic ? 180 : 80), 12, 1200); + const int colonizationIterations = std::clamp(params.getInt("colonizationIterations", realistic ? 46 : 30), 4, 160); + const float influenceRadius = std::max(0.05f, params.getFloat("influenceRadius", crownRadius * 1.08f)); + const float killRadius = std::max(0.01f, params.getFloat("killRadius", crownRadius * 0.13f)); + const float growthStep = std::max(0.01f, params.getFloat("growthStep", crownRadius * (realistic ? 0.105f : 0.14f))); + const float branchInertia = std::clamp(params.getFloat("branchInertia", 1.20f), 0.f, 4.f); + const float maxTurnAngle = std::clamp(params.getFloat("maxTurnAngle", realistic ? 16.f : 22.f), 2.f, 60.f); + const float maxCumulativeAngle = std::clamp(params.getFloat("maxCumulativeAngle", 58.f), 10.f, 85.f); + const float branchLengthFalloff = std::clamp(params.getFloat("branchLengthFalloff", 0.58f), 0.f, 0.9f); + const float branchRadiusFalloff = std::clamp(params.getFloat("branchRadiusFalloff", 0.50f), 0.f, 0.9f); + const float lowerLeafCoverage = std::clamp(params.getFloat("lowerLeafCoverage", 0.72f), 0.f, 1.f); + const float upperLeafCoverage = std::clamp(params.getFloat("upperLeafCoverage", 0.18f), 0.f, 1.f); + const int maxChildren = std::clamp(params.getInt("maxChildren", 2), 1, 4); + std::mt19937 rng(params.getSeed()); + out.clear(); + + const V3 root{0.f, 0.f, 0.f}; + const float trunkAzimuth = randomRange(rng, 0.f, 2.f * kPi); + const V3 trunkBend{std::cos(trunkAzimuth), 0.f, std::sin(trunkAzimuth)}; + const StemPath trunk = + makeCurvedStem(root, {0.f, 1.f, 0.f}, trunkBend, height, trunkCurve * randomRange(rng, 0.72f, 1.28f), + curveBack * randomRange(rng, 0.72f, 1.28f), 0.f, curveSegments + 2); + addStem(out, trunk, trunkRadius, trunkRadius * 0.16f, sides); + + std::vector tips; + std::vector foliageAnchors; + if (branchAlgorithm == "spaceColonization") { + const ColonizationSettings colonization{ + .height = height, + .foliageStart = foliageStart, + .crownRadius = crownRadius, + .trunkRadius = trunkRadius, + .tropism = tropism, + .droop = droop * 0.45f, + .inertia = branchInertia, + .attractorCount = attractorCount, + .iterations = colonizationIterations, + .influenceRadius = influenceRadius, + .killRadius = killRadius, + .step = growthStep, + .maxTurnAngle = maxTurnAngle, + .maxCumulativeAngle = maxCumulativeAngle, + .lengthFalloff = branchLengthFalloff, + .radiusFalloff = branchRadiusFalloff, + .lowerLeafCoverage = lowerLeafCoverage, + .upperLeafCoverage = upperLeafCoverage, + .maxChildren = maxChildren, + .sides = sides, + }; + growSpaceColonizedTree(out, trunk, rng, colonization, foliageAnchors); + } else { + for (int level = 0; level < branchLevels; ++level) { + const int count = std::max(2, branchCount - level * 2); + const float levelScale = std::pow(0.62f, float(level)); + std::vector next; + for (int i = 0; i < count; ++i) { + const float h = foliageStart + (1.f - foliageStart) * (float(i) + 0.35f) / float(count); + const float angle = float(i) * phyllotaxis + randomRange(rng, -0.20f, 0.20f) + level; + V3 start; + V3 parentDirection; + if (level == 0) { + samplePath(trunk, h, start, parentDirection); + } else { + const Tip& parent = tips[size_t(i) % tips.size()]; + start = parent.p; + parentDirection = parent.dir; + } + V3 ringRight, ringForward; + basisFor(parentDirection, ringRight, ringForward); + const V3 radial = add(mul(ringRight, std::cos(angle)), mul(ringForward, std::sin(angle))); + const float levelAngle = + branchAngle - float(level) * 7.f + randomRange(rng, -angleVariation, angleVariation); + const float radians = std::clamp(levelAngle, 5.f, 88.f) * kPi / 180.f; + V3 dir = norm(add(mul(parentDirection, std::cos(radians)), mul(radial, std::sin(radians)))); + dir = norm(add(dir, mul(V3{0.f, 1.f, 0.f}, apicalDominance * 0.24f * h))); + const float relativeHeight = + std::clamp((start.y / height - foliageStart) / (1.f - foliageStart), 0.f, 1.f); + const float lengthTaper = 1.f - branchLengthFalloff * relativeHeight; + const float radiusTaper = 1.f - branchRadiusFalloff * relativeHeight; + const float length = crownRadius * randomRange(rng, 0.72f, 1.18f) * lengthTaper * levelScale; + const float r = trunkRadius * (0.36f * radiusTaper) * levelScale; + V3 bendRight, bendForward; + basisFor(dir, bendRight, bendForward); + const V3 bendAxis = + norm(add(mul(bendRight, std::cos(angle + 1.1f)), mul(bendForward, std::sin(angle + 1.1f)))); + const float flexibility = 1.f + float(level) * 0.38f; + const float downWeight = droop * (0.35f + 0.65f * (1.f - h)) * flexibility; + const float verticalAcceleration = tropism * (0.55f + apicalDominance * h) - downWeight; + const StemPath stem = makeCurvedStem(start, dir, bendAxis, length, + branchCurve * flexibility * randomRange(rng, 0.65f, 1.35f), + curveBack * 0.55f * flexibility * randomRange(rng, 0.55f, 1.25f), + verticalAcceleration, std::max(2, curveSegments - level)); + addStem(out, stem, r, r * 0.22f, std::max(3, sides - level * 2)); + const float coverage = lowerLeafCoverage + (upperLeafCoverage - lowerLeafCoverage) * relativeHeight; + const uint32_t coverageKey = params.getSeed() ^ uint32_t(level * 131 + i * 977); + if (hash01(coverageKey) < coverage && stem.points.size() > 3) { + const size_t middle = stem.points.size() / 2; + const V3 middleDirection = norm(sub(stem.points[middle], stem.points[middle - 1])); + foliageAnchors.push_back( + {stem.points[middle], middleDirection, levelScale * (0.72f + 0.28f * (1.f - relativeHeight))}); + } + next.push_back({stem.points.back(), stem.endDirection, levelScale}); + } + foliageAnchors.insert(foliageAnchors.end(), next.begin(), next.end()); + tips.swap(next); + } + } + + if (leafMode == "cards") { + const int perTip = int(std::round((realistic ? 12.f : 6.f) * density)); + for (const Tip& tip : foliageAnchors) { + for (int i = 0; i < perTip; ++i) { + V3 c = add(tip.p, {randomRange(rng, -1.f, 1.f) * crownRadius * 0.24f * tip.scale, + randomRange(rng, -0.25f, 0.55f) * crownRadius * tip.scale, + randomRange(rng, -1.f, 1.f) * crownRadius * 0.24f * tip.scale}); + const V3 face = + norm({randomRange(rng, -1.f, 1.f), randomRange(rng, -0.2f, 0.8f), randomRange(rng, -1.f, 1.f)}); + addLeafCard(out, c, face, leafSize * randomRange(rng, 0.72f, 1.25f), randomRange(rng, 0.f, kPi)); + } + } + } else if (leafMode == "canopy" && density > 0.f) { + // A canopy lobe belongs to a branch: center it just behind the terminal + // point so the branch visibly penetrates the foliage instead of ending in air. + int wanted = std::max(1, int(std::round(float(foliageAnchors.size()) * (0.28f + density * 0.62f)))); + if (branchAlgorithm == "spaceColonization") wanted = int(foliageAnchors.size()); + const int stride = std::max(1, int(foliageAnchors.size()) / wanted); + int emitted = 0; + for (int i = int(foliageAnchors.size()) - 1; i >= 0 && emitted < wanted; i -= stride) { + const Tip& tip = foliageAnchors[size_t(i)]; + const float lobeMin = branchAlgorithm == "spaceColonization" ? 0.14f : 0.27f; + const float lobeMax = branchAlgorithm == "spaceColonization" ? 0.22f : 0.40f; + const float r = crownRadius * randomRange(rng, lobeMin, lobeMax) * (0.72f + 0.28f * density) * + (0.58f + 0.42f * tip.scale); + V3 c = sub(tip.p, mul(tip.dir, r * 0.22f)); + c.y += r * randomRange(rng, 0.08f, 0.22f); + addCanopyBlob(out, c, {r, r * randomRange(rng, 0.72f, 1.15f), r}, realistic ? 8 : 4, realistic ? 12 : 7); + ++emitted; + } + } + + out.setMeta("recipe", "mesh.tree"); + out.setMeta("style", style); + out.setMeta("leafMode", leafMode); + out.setMeta("branchAlgorithm", branchAlgorithm); + out.setMeta("seed", std::to_string(params.getSeed())); + if (out.empty()) { + error = "mesh.tree: generated an empty mesh"; + return false; + } + return true; +} + +} // namespace eve::procgen diff --git a/src/modules/procgen/algorithms/TreeMesh.h b/src/modules/procgen/algorithms/TreeMesh.h new file mode 100644 index 00000000..5ac482fa --- /dev/null +++ b/src/modules/procgen/algorithms/TreeMesh.h @@ -0,0 +1,13 @@ +#pragma once + +#include "procgen/MeshBuild.h" +#include "procgen/Params.h" + +#include + +namespace eve::procgen { + +/** Build a deterministic procedural tree. Registered as the `mesh.tree` recipe. */ +bool generateTreeMesh(const Params& params, MeshBuild& out, std::string& error); + +} // namespace eve::procgen diff --git a/test/procgen.cpp b/test/procgen.cpp index a251eddf..cfa4b66f 100644 --- a/test/procgen.cpp +++ b/test/procgen.cpp @@ -13,13 +13,18 @@ #include "map/TileLayer.h" #include "image/ImageData.h" #include "graphics/Graphics.h" +#include "graphics/RenderSystem.h" +#include "graphics/RenderSystem3D.h" #include "window/Window.h" +#include "RenderImageAudit.h" #include #include #include +#include #include +#include #include #include #include @@ -220,6 +225,171 @@ TEST_CASE("procgen.registry.builtins") { CHECK(GeneratorRegistry::instance().has("wfc.simple")); } +TEST_CASE("procgen.mesh.tree.stylesAndLeaves") { + MeshRecipeRegistry::instance().registerBuiltins(); + Params low; + low.setSeed(1234); + low.setString("style", "lowpoly"); + low.setString("leafMode", "cards"); + low.setFloat("leafDensity", 0.8f); + MeshBuild a, b; + std::string err; + CHECK(MeshRecipeRegistry::instance().generate("mesh.tree", low, a, err)); + CHECK(MeshRecipeRegistry::instance().generate("mesh.tree", low, b, err)); + CHECK(a.getVertexCount() > 0); + CHECK(a.positions() == b.positions()); + CHECK(a.indices() == b.indices()); + CHECK(meshIndicesInRange(a)); + CHECK(meshPositionsFinite(a)); + CHECK(meshNormalsFiniteUnit(a)); + CHECK_EQ(a.getMeta("style", ""), "lowpoly"); + CHECK_EQ(a.getMeta("leafMode", ""), "cards"); + + Params realistic; + realistic.setSeed(22); + realistic.setString("style", "realistic"); + realistic.setString("leafMode", "canopy"); + realistic.setString("branchAlgorithm", "spaceColonization"); + realistic.setFloat("leafDensity", 1.f); + MeshBuild canopy; + CHECK(MeshRecipeRegistry::instance().generate("mesh.tree", realistic, canopy, err)); + CHECK(canopy.getVertexCount() > a.getVertexCount() / 2); + CHECK(meshIndicesInRange(canopy)); + CHECK(meshNormalsFiniteUnit(canopy)); + CHECK_EQ(canopy.getMeta("branchAlgorithm", ""), "spaceColonization"); + + Params sparseCoverage = realistic; + sparseCoverage.setFloat("lowerLeafCoverage", 0.f); + sparseCoverage.setFloat("upperLeafCoverage", 0.f); + Params fullCoverage = realistic; + fullCoverage.setFloat("lowerLeafCoverage", 1.f); + fullCoverage.setFloat("upperLeafCoverage", 1.f); + MeshBuild sparseLeaves, coveredBranches; + CHECK(MeshRecipeRegistry::instance().generate( + "mesh.tree", sparseCoverage, sparseLeaves, err)); + CHECK(MeshRecipeRegistry::instance().generate( + "mesh.tree", fullCoverage, coveredBranches, err)); + CHECK(coveredBranches.getVertexCount() > sparseLeaves.getVertexCount()); + + Params bare; + bare.setSeed(22); + bare.setString("leafMode", "none"); + MeshBuild noLeaves; + CHECK(MeshRecipeRegistry::instance().generate("mesh.tree", bare, noLeaves, err)); + CHECK(noLeaves.getVertexCount() < canopy.getVertexCount()); + + Params straight = bare; + straight.setFloat("trunkCurve", 0.f); + straight.setFloat("branchCurve", 0.f); + straight.setFloat("curveBack", 0.f); + straight.setFloat("tropism", 0.f); + straight.setFloat("droop", 0.f); + Params expressive = straight; + expressive.setFloat("trunkCurve", 0.2f); + expressive.setFloat("branchCurve", 0.24f); + expressive.setFloat("curveBack", 0.2f); + expressive.setFloat("tropism", 0.35f); + expressive.setFloat("droop", 0.28f); + MeshBuild rigidTree, curvedTree; + CHECK(MeshRecipeRegistry::instance().generate("mesh.tree", straight, rigidTree, err)); + CHECK(MeshRecipeRegistry::instance().generate("mesh.tree", expressive, curvedTree, err)); + CHECK(rigidTree.positions() != curvedTree.positions()); + CHECK(meshPositionsFinite(curvedTree)); + CHECK(meshNormalsFiniteUnit(curvedTree)); +} + +TEST_CASE("procgen.mesh.tree.validatesOptions") { + MeshRecipeRegistry::instance().registerBuiltins(); + Params p; + p.setString("style", "watercolor"); + MeshBuild mesh; + std::string err; + CHECK(!MeshRecipeRegistry::instance().generate("mesh.tree", p, mesh, err)); + CHECK(err.find("style") != std::string::npos); + p.setString("style", "lowpoly"); + p.setString("branchAlgorithm", "crystalGrowth"); + CHECK(!MeshRecipeRegistry::instance().generate("mesh.tree", p, mesh, err)); + CHECK(err.find("branchAlgorithm") != std::string::npos); +} + +TEST_CASE("procgen.mesh.tree.renderDump") { + const char *outputPath = std::getenv("EVENGINE_TREE_RENDER_PNG"); + if (!outputPath || !outputPath[0]) return; + + auto *win = eve::window::Window::create(); + auto *gfx = Graphics::create(); + REQUIRE(win != nullptr); + REQUIRE(gfx != nullptr); + win->setGraphics(gfx); + eve::window::WindowSettings ws; + ws.width = 900; + ws.height = 700; + ws.centered = true; + REQUIRE(win->setWindowSettings(ws)); + + Params params; + params.setSeed(31415); + params.setString("style", "lowpoly"); + const char *leafModeOverride = std::getenv("EVENGINE_TREE_LEAF_MODE"); + params.setString("leafMode", leafModeOverride ? leafModeOverride : "canopy"); + params.setString("branchAlgorithm", "spaceColonization"); + params.setFloat("leafDensity", 0.82f); + params.setFloat("height", 6.2f); + params.setFloat("crownRadius", 2.15f); + params.setInt("branchLevels", 3); + params.setInt("branchCount", 8); + params.setInt("attractorCount", 120); + params.setInt("colonizationIterations", 38); + params.setFloat("branchInertia", 1.15f); + params.setFloat("tropism", 0.20f); + params.setFloat("droop", 0.08f); + params.setFloat("growthStep", 0.25f); + params.setFloat("maxTurnAngle", 18.f); + params.setInt("maxChildren", 2); + + Procgen generator; + Mesh *treeMesh = generator.generateMesh("mesh.tree", ¶ms, gfx); + REQUIRE(treeMesh != nullptr); + + // 4px bark/foliage atlas; UVs are partitioned by the mesh recipe. + const uint8_t atlasPixels[] = { + 111, 70, 42, 255, 128, 82, 47, 255, + 64, 119, 57, 255, 82, 145, 67, 255, + }; + Texture *atlas = gfx->newTexture(4, 1, atlasPixels); + REQUIRE(atlas != nullptr); + + auto *tree = Renderable3D::create(); + tree->setMesh(treeMesh); + tree->setTexture(atlas); + tree->setTint(1.f, 1.f, 1.f, 1.f); + tree->setRoughness(0.88f); + tree->setRotation(-18.f, 0.f, 0.f); + + auto *camera = Camera3D::createCamera(); + camera->setEye(9.2f, 5.1f, 11.4f); + camera->setTarget(0.f, 3.1f, 0.f); + camera->setUp(0.f, 1.f, 0.f); + camera->setFov(36.f); + camera->setAmbient(0.30f, 0.34f, 0.28f); + camera->setActive(true); + + gfx->setScreenReadbackEnabled(true); + gfx->setBackgroundColor(Color(0.075f, 0.105f, 0.095f, 1.f)); + RenderSystem3D::setDirectionalLight(-0.55f, -1.f, -0.35f, 1.45f, 1.32f, 1.08f); + + // Warm up pipelines and read back the final stable frame. + for (int frame = 0; frame < 4; ++frame) { + RenderSystem3D::render(*gfx); + RenderSystem::render(*gfx); + } + std::unique_ptr image(gfx->newImageData()); + REQUIRE(image.get() != nullptr); + REQUIRE(saveImagePng(*image, outputPath)); + std::printf("tree render saved: %s\n", outputPath); + win->close(); +} + TEST_CASE("procgen.dungeon.bsp.reproducible") { Params p; p.setSeed(42); From a1e7fffe990b624a62ae78176b2cb3c46933b61b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Sat, 15 Aug 2026 13:11:27 +0800 Subject: [PATCH 14/21] feat(procgen): add hex-grid planet generator --- docs/dev/程序化生成模块设计.md | 15 ++ docs/usr/modules/procgen.md | 31 ++++ src/modules/procgen/Procgen.h | 2 +- .../procgen/algorithms/MarchingCubes.cpp | 136 ++++++++++++++++++ .../procgen/algorithms/MarchingCubes.h | 7 + test/procgen.cpp | 117 ++++++++++++++- 6 files changed, 306 insertions(+), 2 deletions(-) diff --git a/docs/dev/程序化生成模块设计.md b/docs/dev/程序化生成模块设计.md index a674f80e..ffa07144 100644 --- a/docs/dev/程序化生成模块设计.md +++ b/docs/dev/程序化生成模块设计.md @@ -132,6 +132,7 @@ OutputSpec { | `rooms.scatter` | 房间 | 随机房间放置 + 最近邻/MST 连廊(轻量) | | `wfc.simple` | 约束 | 简易 Wave Function Collapse(preset: dungeon/cave/terrain) | | `mesh.marchingcubes` | 网格 | Marching Cubes 等值面(sphere/torus/noise/terrain 标量场 → MeshBuild) | +| `mesh.hexplanet` | 网格 | 细分二十面体的对偶网格:12 个五边形 + 其余六边形,输出球面单元 MeshBuild | ### 注册 API @@ -258,10 +259,24 @@ procgen.generate* → Grid2D ### Phase C — 装饰物 / 网格(进行中) - [x] Marching Cubes:`mesh.marchingcubes` → `MeshBuild` / `Graphics::newMeshFromArrays` +- [x] 六边形星球:`mesh.hexplanet` → `MeshBuild` / `Graphics::newMeshFromArrays` - [x] Wave Function Collapse:`wfc.simple`(tiled 邻接约束,内置 preset) - [ ] 低模树/石/草 billboard 或简易网格 - [ ] 按 height/biome/mask 散布实例数据(非 GPU scatter) +#### 六边形星球网格约定 + +- 实现采用细分二十面体的对偶网格,不依赖第三方几何库。 +- 细分等级为 `n` 时,单元数为 `10 * 4^n + 2`;固定有 12 个五边形, + 其余单元为六边形。这是欧拉示性数决定的球面拓扑约束。 +- `radius` 控制球半径;`tileInset` 只收缩每个单元的边界,收缩后重新归一化, + 因而所有顶点仍落在同一球面上。 +- 每个单元使用独立顶点并扇形三角化,便于后续按单元设置材质、地形高度或 + 选择状态;当前 recipe 输出统一法线与球面 UV。 +- 拓扑测试验证索引范围、有限且单位长度的法线、半径、单元数量及 12 个五边形; + 渲染测试通过 EVEngine 的 3D 管线读取帧缓冲并编码 PNG,输出到测试构建目录的 + `out/hex_planet.png`。 + ## 目录建议 ```text diff --git a/docs/usr/modules/procgen.md b/docs/usr/modules/procgen.md index 2d83eda8..eabd4655 100644 --- a/docs/usr/modules/procgen.md +++ b/docs/usr/modules/procgen.md @@ -34,6 +34,37 @@ local cpu = gen.buildMesh("mesh.marchingcubes", p); local gpu = gen.generateMesh("mesh.marchingcubes", p, gfx); ``` +### 生成六边形网格星球 + +`mesh.hexplanet` 生成细分二十面体的对偶网格。星球表面以六边形单元为主, +并包含球面拓扑必需的 12 个五边形单元。所有顶点都位于指定半径的球面上, +可直接交给 3D 渲染系统使用。 + +```squirrel +local gen = eve.Procgen(); +local p = gen.newParams(); +p.setFloat("radius", 1.0); +p.setInt("subdivisions", 3); +p.setFloat("tileInset", 0.12); + +local cpu = gen.buildMesh("mesh.hexplanet", p); +local gpu = gen.generateMesh("mesh.hexplanet", p, gfx); +``` + +参数: + +- `radius`:星球半径,必须大于 `0`,默认 `1.0`。 +- `subdivisions`:二十面体细分次数,范围 `[0, 7]`,默认 `2`。单元总数为 + `10 * 4^subdivisions + 2`;每增加一级,网格规模约增至四倍。 +- `tileInset`:每个单元向自身中心收缩的比例,范围 `[0, 0.5)`,默认 + `0.06`。设为 `0` 时单元无缝相接;增大该值可形成清晰的网格间隙。 + +生成结果的 metadata 包含 `algorithm`、`cells`、`pentagons`、`hexagons` +和 `subdivisions`。网格始终含 12 个五边形,其余单元均为六边形;这些五边形 +是用多边形铺满球面时无法消除的拓扑要求。 + +建议在创建关卡或切换星球时生成一次并缓存 GPU Mesh,不要逐帧重新生成。 + ### 生成无缝材质 设置 texture recipe、尺寸、octaves、pixelSize 和 seamless,调用纹理或法线图生成接口;开发期切换 seed 预览,发布时缓存 Texture,不能每帧重新生成。 diff --git a/src/modules/procgen/Procgen.h b/src/modules/procgen/Procgen.h index ddeb27e2..33b09e07 100644 --- a/src/modules/procgen/Procgen.h +++ b/src/modules/procgen/Procgen.h @@ -71,7 +71,7 @@ public: bool hasTextureRecipe(const std::string &recipeId) const; // --- Mesh recipes (Marching Cubes, …) --- - /** CPU mesh (caller owns). Recipes: mesh.marchingcubes. */ + /** CPU mesh (caller owns). Recipes: mesh.marchingcubes, mesh.hexplanet. */ MeshBuild *buildMesh(const std::string &recipeId, Params *params); /** Build + upload to GPU Mesh (owned by Graphics). */ graphics::Mesh *generateMesh(const std::string &recipeId, Params *params, diff --git a/src/modules/procgen/algorithms/MarchingCubes.cpp b/src/modules/procgen/algorithms/MarchingCubes.cpp index 1a76746e..6545c4f4 100644 --- a/src/modules/procgen/algorithms/MarchingCubes.cpp +++ b/src/modules/procgen/algorithms/MarchingCubes.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace eve::procgen { namespace { @@ -94,8 +95,142 @@ float fbm3(float x, float y, float z, uint32_t seed, int octaves) { return norm > 0.f ? sum / norm : 0.f; } +struct Vec3 { + float x = 0.f, y = 0.f, z = 0.f; +}; + +Vec3 add(Vec3 a, Vec3 b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; } +Vec3 sub(Vec3 a, Vec3 b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; } +Vec3 mul(Vec3 a, float s) { return {a.x * s, a.y * s, a.z * s}; } +float dot(Vec3 a, Vec3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; } +Vec3 cross(Vec3 a, Vec3 b) { + return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; +} +Vec3 normalized(Vec3 v) { + const float len = std::sqrt(dot(v, v)); + return len > 1e-8f ? mul(v, 1.f / len) : Vec3{0.f, 1.f, 0.f}; +} + +struct Triangle { + uint32_t a, b, c; +}; + +uint64_t edgeKey(uint32_t a, uint32_t b) { + if (a > b) std::swap(a, b); + return (uint64_t(a) << 32u) | uint64_t(b); +} + +uint32_t midpoint(uint32_t a, uint32_t b, std::vector &vertices, + std::map &cache) { + const uint64_t key = edgeKey(a, b); + const auto it = cache.find(key); + if (it != cache.end()) return it->second; + const uint32_t index = uint32_t(vertices.size()); + vertices.push_back(normalized(add(vertices[a], vertices[b]))); + cache.emplace(key, index); + return index; +} + +void addPlanetVertex(MeshBuild &out, Vec3 p, float radius) { + p = normalized(p); + constexpr float kPi = 3.14159265358979323846f; + const float u = std::atan2(p.z, p.x) / (2.f * kPi) + 0.5f; + const float v = std::asin(std::clamp(p.y, -1.f, 1.f)) / kPi + 0.5f; + out.addVertex(p.x * radius, p.y * radius, p.z * radius, p.x, p.y, p.z, u, v); +} + } // namespace +bool generateHexPlanetMesh(const Params ¶ms, MeshBuild &out, std::string &error) { + const int subdivisions = params.getInt("subdivisions", 2); + const float radius = params.getFloat("radius", 1.f); + const float inset = params.getFloat("tileInset", 0.06f); + if (subdivisions < 0 || subdivisions > 7) { + error = "mesh.hexplanet: subdivisions must be in [0, 7]"; + return false; + } + if (!(radius > 0.f)) { + error = "mesh.hexplanet: radius must be positive"; + return false; + } + if (inset < 0.f || inset >= 0.5f) { + error = "mesh.hexplanet: tileInset must be in [0, 0.5)"; + return false; + } + + const float phi = (1.f + std::sqrt(5.f)) * 0.5f; + std::vector vertices = { + {-1, phi, 0}, {1, phi, 0}, {-1, -phi, 0}, {1, -phi, 0}, + {0, -1, phi}, {0, 1, phi}, {0, -1, -phi}, {0, 1, -phi}, + {phi, 0, -1}, {phi, 0, 1}, {-phi, 0, -1}, {-phi, 0, 1}, + }; + for (Vec3 &v : vertices) v = normalized(v); + std::vector faces = { + {0, 11, 5}, {0, 5, 1}, {0, 1, 7}, {0, 7, 10}, {0, 10, 11}, + {1, 5, 9}, {5, 11, 4}, {11, 10, 2}, {10, 7, 6}, {7, 1, 8}, + {3, 9, 4}, {3, 4, 2}, {3, 2, 6}, {3, 6, 8}, {3, 8, 9}, + {4, 9, 5}, {2, 4, 11}, {6, 2, 10}, {8, 6, 7}, {9, 8, 1}, + }; + for (int level = 0; level < subdivisions; ++level) { + std::map cache; + std::vector next; + next.reserve(faces.size() * 4u); + for (const Triangle &f : faces) { + const uint32_t ab = midpoint(f.a, f.b, vertices, cache); + const uint32_t bc = midpoint(f.b, f.c, vertices, cache); + const uint32_t ca = midpoint(f.c, f.a, vertices, cache); + next.insert(next.end(), {{f.a, ab, ca}, {f.b, bc, ab}, {f.c, ca, bc}, {ab, bc, ca}}); + } + faces.swap(next); + } + + std::vector> incident(vertices.size()); + std::vector faceCenters; + faceCenters.reserve(faces.size()); + for (uint32_t i = 0; i < faces.size(); ++i) { + const Triangle &f = faces[i]; + faceCenters.push_back(normalized(add(add(vertices[f.a], vertices[f.b]), vertices[f.c]))); + incident[f.a].push_back(i); + incident[f.b].push_back(i); + incident[f.c].push_back(i); + } + + out.clear(); + int pentagons = 0, hexagons = 0; + for (uint32_t cell = 0; cell < vertices.size(); ++cell) { + const Vec3 center = vertices[cell]; + const Vec3 reference = std::fabs(center.y) < 0.9f ? normalized(cross({0, 1, 0}, center)) + : normalized(cross({1, 0, 0}, center)); + const Vec3 tangent = cross(center, reference); + auto &ring = incident[cell]; + std::sort(ring.begin(), ring.end(), [&](uint32_t lhs, uint32_t rhs) { + const Vec3 a = sub(faceCenters[lhs], mul(center, dot(faceCenters[lhs], center))); + const Vec3 b = sub(faceCenters[rhs], mul(center, dot(faceCenters[rhs], center))); + return std::atan2(dot(a, tangent), dot(a, reference)) < + std::atan2(dot(b, tangent), dot(b, reference)); + }); + if (ring.size() == 5) ++pentagons; + else if (ring.size() == 6) ++hexagons; + + const uint32_t base = uint32_t(out.getVertexCount()); + addPlanetVertex(out, center, radius); + for (uint32_t faceIndex : ring) { + // Pull dual corners toward this cell's center. Re-normalizing keeps every tile spherical. + addPlanetVertex(out, normalized(add(mul(faceCenters[faceIndex], 1.f - inset), + mul(center, inset))), radius); + } + for (uint32_t i = 0; i < ring.size(); ++i) { + out.addTriangle(base, base + 1u + i, base + 1u + (i + 1u) % uint32_t(ring.size())); + } + } + out.setMeta("algorithm", "mesh.hexplanet"); + out.setMeta("cells", std::to_string(vertices.size())); + out.setMeta("pentagons", std::to_string(pentagons)); + out.setMeta("hexagons", std::to_string(hexagons)); + out.setMeta("subdivisions", std::to_string(subdivisions)); + return true; +} + bool marchingCubes(const float *density, int nx, int ny, int nz, float isolevel, MeshBuild &out, std::string *error) { if (!density) { @@ -300,6 +435,7 @@ std::vector MeshRecipeRegistry::list() const { void MeshRecipeRegistry::registerBuiltins() { if (builtinsRegistered_) return; registerRecipe("mesh.marchingcubes", generateMarchingCubesMesh); + registerRecipe("mesh.hexplanet", generateHexPlanetMesh); builtinsRegistered_ = true; } diff --git a/src/modules/procgen/algorithms/MarchingCubes.h b/src/modules/procgen/algorithms/MarchingCubes.h index edaaf2ea..d86bd151 100644 --- a/src/modules/procgen/algorithms/MarchingCubes.h +++ b/src/modules/procgen/algorithms/MarchingCubes.h @@ -27,6 +27,13 @@ bool fillDensityField(const Params ¶ms, std::vector &density, int &nx /** Build mesh from Params (field + resolution + isolevel). */ bool generateMarchingCubesMesh(const Params ¶ms, MeshBuild &out, std::string &error); +/** + * Build the dual of a subdivided icosahedron. The result is a closed planet made + * of hexagonal cells plus the twelve pentagons required by spherical topology. + * Params: radius (1), subdivisions (2), tileInset (0.06, in [0, 0.5)). + */ +bool generateHexPlanetMesh(const Params ¶ms, MeshBuild &out, std::string &error); + using MeshRecipeFn = std::function; class MeshRecipeRegistry { diff --git a/test/procgen.cpp b/test/procgen.cpp index a251eddf..9a88d8cb 100644 --- a/test/procgen.cpp +++ b/test/procgen.cpp @@ -13,13 +13,20 @@ #include "map/TileLayer.h" #include "image/ImageData.h" #include "graphics/Graphics.h" +#include "graphics/RenderSystem.h" +#include "graphics/RenderSystem3D.h" #include "window/Window.h" +#include "image/Image.h" +#include "filesystem/FileData.h" #include #include #include #include +#include +#include +#include #include #include #include @@ -493,6 +500,110 @@ TEST_CASE("procgen.mesh.marchingcubes.sphere") { CHECK(mesh.indices() == mesh2.indices()); } +TEST_CASE("procgen.mesh.hexplanet.topology") { + MeshRecipeRegistry::instance().registerBuiltins(); + Params p; + p.setFloat("radius", 2.f); + p.setInt("subdivisions", 2); + p.setFloat("tileInset", 0.05f); + MeshBuild mesh; + std::string err; + CHECK(MeshRecipeRegistry::instance().generate("mesh.hexplanet", p, mesh, err)); + CHECK(meshIndicesInRange(mesh)); + CHECK(meshPositionsFinite(mesh)); + CHECK(meshNormalsFiniteUnit(mesh)); + CHECK_EQ(mesh.getMeta("algorithm", ""), std::string("mesh.hexplanet")); + CHECK_EQ(mesh.getMeta("pentagons", ""), std::string("12")); + // V = 10 * 4^n + 2 for a subdivided icosahedron. + CHECK_EQ(mesh.getMeta("cells", ""), std::string("162")); + CHECK_EQ(mesh.getMeta("hexagons", ""), std::string("150")); + CHECK_EQ(mesh.getIndexCount() % 3, 0); + for (int i = 0; i < mesh.getVertexCount(); ++i) { + const float x = mesh.getPositionX(i), y = mesh.getPositionY(i), z = mesh.getPositionZ(i); + CHECK(std::fabs(std::sqrt(x * x + y * y + z * z) - 2.f) < 1e-4f); + } +} + +TEST_CASE("procgen.render.hexplanetPng") { + auto *win = eve::window::Window::create(); + auto *gfx = Graphics::create(); + REQUIRE(win != nullptr); + REQUIRE(gfx != nullptr); + win->setGraphics(gfx); + eve::window::WindowSettings settings; + settings.width = 768; + settings.height = 768; + settings.centered = true; + REQUIRE(win->setWindowSettings(settings)); + + Procgen *procgen = Procgen::create(); + Params params; + params.setFloat("radius", 1.f); + params.setInt("subdivisions", 3); + params.setFloat("tileInset", 0.12f); + Mesh *planetMesh = procgen->generateMesh("mesh.hexplanet", ¶ms, gfx); + REQUIRE(planetMesh != nullptr); + + const uint8_t oceanBlue[4] = {42, 155, 181, 255}; + Texture *planetTexture = gfx->newTexture(1, 1, oceanBlue); + REQUIRE(planetTexture != nullptr); + + auto *planet = eve::graphics::Renderable3D::create(); + planet->setMesh(planetMesh); + planet->setTexture(planetTexture); + planet->setMetallic(0.05f); + planet->setRoughness(0.72f); + planet->setCastShadow(false); + planet->setReceiveShadow(false); + planet->transform()->yaw = 0.42f; + planet->transform()->pitch = -0.22f; + + auto *camera = eve::graphics::Camera3D::createCamera(); + camera->setEye(2.65f, 1.55f, 3.05f); + camera->setTarget(0.f, 0.f, 0.f); + camera->setFov(38.f); + camera->setAmbient(0.16f, 0.19f, 0.24f); + + // A transparent 2D entity drives the normal present path used by render tests. + auto *present = eve::graphics::Renderable2D::create(); + present->transform()->x = 0.f; + present->transform()->y = 0.f; + present->sprite()->width = 1.f; + present->sprite()->height = 1.f; + present->sprite()->a = 0.f; + + gfx->setBackgroundColor(Color(0.012f, 0.018f, 0.035f, 1.f)); + gfx->setScreenReadbackEnabled(true); + eve::graphics::RenderSystem3D::setDirectionalLight(0.55f, 0.85f, 1.1f, 1.15f, 0.98f, + 0.88f); + for (int frame = 0; frame < 8; ++frame) { + planet->transform()->yaw += 0.025f; + eve::graphics::RenderSystem3D::render(*gfx); + eve::graphics::RenderSystem::render(*gfx); + } + + eve::image::Image::create(); + std::unique_ptr image(gfx->newImageData()); + REQUIRE(static_cast(image)); + std::unique_ptr png( + image->encode(medialoader::FormatHandler::ENCODED_PNG, "hex_planet.png", false)); + REQUIRE(static_cast(png)); + REQUIRE(png->getSize() > 0); + + const std::filesystem::path outDir = + std::filesystem::path(EVENGINE_TEST_BINARY_DIR) / "out"; + std::filesystem::create_directories(outDir); + const std::filesystem::path outPath = outDir / "hex_planet.png"; + std::ofstream output(outPath, std::ios::binary); + REQUIRE(output.good()); + output.write(static_cast(png->getData()), + static_cast(png->getSize())); + REQUIRE(output.good()); + output.close(); + std::printf("hex planet render saved: %s\n", outPath.string().c_str()); + win->close(); +} + TEST_CASE("procgen.mesh.marchingcubes.allFields") { MeshRecipeRegistry::instance().registerBuiltins(); const char *fields[] = {"sphere", "torus", "noise", "terrain"}; @@ -673,7 +784,11 @@ TEST_CASE("procgen.mesh.marchingcubes.viaModule") { CHECK(meshIndicesInRange(*m)); CHECK(mod->hasMeshRecipe("mesh.marchingcubes")); CHECK(mod->getMeshRecipeCount() >= 1); - CHECK_EQ(mod->getMeshRecipeId(0), std::string("mesh.marchingcubes")); + bool listed = false; + for (int i = 0; i < mod->getMeshRecipeCount(); ++i) { + if (mod->getMeshRecipeId(i) == "mesh.marchingcubes") listed = true; + } + CHECK(listed); delete m; Params bad; From f29752dbb19317546652d221160d2e613b6bb556 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 15 Aug 2026 06:15:15 +0000 Subject: [PATCH 15/21] feat(graphics): add t3ssel8r-style stylized grass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sample terrain meshes with Poisson-disk or Halton blue noise, expand root-pivoted billboard quads, and shade them with a 4-frame sway atlas. Instance ids desync the animation, the CSM shadow map picks between light and dark green, and a second sparse pass adds always-dark tufts. Co-authored-by: 西风逍遥游 --- scripts/compile_mesh3d_shaders.py | 2 + src/modules/graphics/Graphics.cpp | 24 + src/modules/graphics/Graphics.h | 13 + src/modules/graphics/Grass.cpp | 540 ++++++++++++++++++ src/modules/graphics/Grass.h | 156 +++++ src/modules/graphics/Shader.h | 3 + .../graphics/shaders/mesh3d_grass.frag | 118 ++++ .../graphics/shaders/mesh3d_grass.frag.spv | Bin 0 -> 11224 bytes .../graphics/shaders/mesh3d_grass.vert | 75 +++ .../graphics/shaders/mesh3d_grass.vert.spv | Bin 0 -> 6308 bytes .../shaders/mesh3d_grass_frag_spv.inc | 354 ++++++++++++ .../shaders/mesh3d_grass_vert_spv.inc | 201 +++++++ test/CMakeLists.txt | 1 + test/grass.cpp | 223 ++++++++ 14 files changed, 1710 insertions(+) create mode 100644 src/modules/graphics/Grass.cpp create mode 100644 src/modules/graphics/Grass.h create mode 100644 src/modules/graphics/shaders/mesh3d_grass.frag create mode 100644 src/modules/graphics/shaders/mesh3d_grass.frag.spv create mode 100644 src/modules/graphics/shaders/mesh3d_grass.vert create mode 100644 src/modules/graphics/shaders/mesh3d_grass.vert.spv create mode 100644 src/modules/graphics/shaders/mesh3d_grass_frag_spv.inc create mode 100644 src/modules/graphics/shaders/mesh3d_grass_vert_spv.inc create mode 100644 test/grass.cpp diff --git a/scripts/compile_mesh3d_shaders.py b/scripts/compile_mesh3d_shaders.py index 7405dae6..5b1a7955 100644 --- a/scripts/compile_mesh3d_shaders.py +++ b/scripts/compile_mesh3d_shaders.py @@ -27,6 +27,8 @@ SHADERS = [ ("mesh3d_clustered.frag", "frag"), ("mesh3d_hair.vert", "vert"), ("mesh3d_hair.frag", "frag"), + ("mesh3d_grass.vert", "vert"), + ("mesh3d_grass.frag", "frag"), ("mesh3d_gbuffer.vert", "vert"), ("mesh3d_gbuffer.frag", "frag"), ("mesh3d_toon.vert", "vert"), diff --git a/src/modules/graphics/Graphics.cpp b/src/modules/graphics/Graphics.cpp index 188687af..12d9a929 100644 --- a/src/modules/graphics/Graphics.cpp +++ b/src/modules/graphics/Graphics.cpp @@ -1,5 +1,6 @@ #include "graphics/Graphics.h" #include "graphics/HairShader.h" +#include "graphics/Grass.h" #include "graphics/vulkan/Graphics.h" #include "graphics/RenderSystem3D.h" #include "graphics/RenderSystem.h" @@ -331,6 +332,23 @@ void Graphics::expose(ssq::Table &table) { vol.addFunc("getRayMarchShader", &Volumetric::getRayMarchShader); vol.addFunc("getFogShader", &Volumetric::getFogShader); + auto grassField = table.addClass( + "GrassField", std::function([]() -> GrassField * { return nullptr; }), true); + grassField.addFunc("bakePlane", + static_cast(&GrassField::bakePlane)); + grassField.addFunc("update", &GrassField::update); + grassField.addFunc("setTime", &GrassField::setTime); + grassField.addFunc("getTime", &GrassField::getTime); + grassField.addFunc("setFrameDuration", &GrassField::setFrameDuration); + grassField.addFunc("getFrameDuration", &GrassField::getFrameDuration); + grassField.addFunc("draw", static_cast(&GrassField::draw)); + grassField.addFunc("getDenseMesh", &GrassField::getDenseMesh); + grassField.addFunc("getSparseMesh", &GrassField::getSparseMesh); + grassField.addFunc("getShader", &GrassField::getShader); + grassField.addFunc("getAtlas", &GrassField::getAtlas); + grassField.addFunc("getDenseCount", &GrassField::getDenseCount); + grassField.addFunc("getSparseCount", &GrassField::getSparseCount); + auto ao = table.addClass( "AmbientOcclusion", std::function([]() -> AmbientOcclusion * { return nullptr; }), true); @@ -438,6 +456,8 @@ void Graphics::expose(ssq::Class &cls) { cls.addFunc("newMeshShader", static_cast(&Graphics::newMeshShader)); cls.addFunc("newHairShader", &Graphics::newHairShader); + cls.addFunc("newGrassShader", &Graphics::newGrassShader); + cls.addFunc("newGrassField", &Graphics::newGrassField); cls.addFunc("newShaderFromSpvFile", static_cast(&Graphics::newShaderFromSpvFile)); cls.addFunc("setShader", static_cast(&Graphics::setShader)); @@ -492,6 +512,10 @@ AntiAliasing *Graphics::newAntiAliasing() { return new AntiAliasing(this); } Shader *Graphics::newHairShader() { return hair::createShader(this); } +Shader *Graphics::newGrassShader() { return grass::createShader(this); } + +GrassField *Graphics::newGrassField() { return new GrassField(this); } + void Graphics::draw(Drawable *drawable, const glm::mat4 &m) { if (drawable) drawable->draw(this, m); } diff --git a/src/modules/graphics/Graphics.h b/src/modules/graphics/Graphics.h index 51dd791b..2e16f4e5 100644 --- a/src/modules/graphics/Graphics.h +++ b/src/modules/graphics/Graphics.h @@ -16,6 +16,7 @@ #include "graphics/Volumetric.h" #include "graphics/AmbientOcclusion.h" #include "graphics/GlobalIllumination.h" +#include "graphics/Grass.h" #include "graphics/Material.h" #include "graphics/GBuffer.h" #include "graphics/RenderControl.h" @@ -530,6 +531,18 @@ public: /** Built-in hair shader with default anisotropic parameters. */ Shader *newHairShader(); + /** + * t3ssel8r-style grass billboard shader (alpha test + shadow two-tone). + * Owned by Graphics. See grass:: / GrassField. + */ + Shader *newGrassShader(); + + /** + * Dense + sparse stylized grass field. Caller owns GrassField*; + * its Mesh / Shader / Texture are owned by Graphics. + */ + GrassField *newGrassField(); + /** Create an offscreen render target (sampleable). Owned by Graphics. */ virtual Canvas *newCanvas(int width, int height) = 0; diff --git a/src/modules/graphics/Grass.cpp b/src/modules/graphics/Grass.cpp new file mode 100644 index 00000000..c29887e9 --- /dev/null +++ b/src/modules/graphics/Grass.cpp @@ -0,0 +1,540 @@ +#include "graphics/Grass.h" + +#include "common/Exception.h" +#include "graphics/Graphics.h" +#include "graphics/Mesh.h" +#include "graphics/Shader.h" +#include "graphics/Texture.h" +#include "graphics/TextureSampler.h" +#include "graphics/shaders/mesh3d_grass_frag_spv.inc" +#include "graphics/shaders/mesh3d_grass_vert_spv.inc" + +#include +#include +#include +#include +#include + +namespace eve::graphics::grass { +namespace { + +const std::array kParamSlots = { + "time", "frameDuration", "grassWidth", "grassHeight", "alphaCutoff", "alwaysDark", + "lightGreenX", "lightGreenY", "lightGreenZ", "darkGreenX", "darkGreenY", "darkGreenZ", + "frameCount"}; + +std::vector copySpv(const uint32_t *data, size_t count) { + return std::vector(data, data + count); +} + +float radicalInverse(uint32_t n, uint32_t base) { + const float inv = 1.f / float(base); + float f = inv; + float val = 0.f; + while (n > 0) { + val += float(n % base) * f; + n /= base; + f *= inv; + } + return val; +} + +float wrap01(float x) { + x = x - std::floor(x); + return x < 0.f ? x + 1.f : x; +} + +uint32_t mixSeed(uint32_t seed, uint32_t i) { + seed ^= 0x9e3779b9u + (i << 6) + (i >> 2); + seed *= 0x85ebca6bu; + return seed ^ (seed >> 13); +} + +struct Triangle { + uint32_t i0 = 0, i1 = 0, i2 = 0; + float area = 0.f; + glm::vec3 n{0.f, 1.f, 0.f}; +}; + +glm::vec3 readPos(const float *pos, int i) { + return glm::vec3(pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]); +} + +glm::vec3 readNrm(const float *nrm, int i) { + if (!nrm) return glm::vec3(0.f, 1.f, 0.f); + return glm::vec3(nrm[i * 3], nrm[i * 3 + 1], nrm[i * 3 + 2]); +} + +bool buildTriangles(const float *posXYZ, const float *nrmXYZ, int vertexCount, + const uint32_t *indices, int indexCount, float minSlopeDot, + std::vector &tris, std::vector &cdf) { + tris.clear(); + cdf.clear(); + if (!posXYZ || !indices || vertexCount < 3 || indexCount < 3) return false; + if (indexCount % 3 != 0) return false; + + float total = 0.f; + for (int t = 0; t + 2 < indexCount; t += 3) { + const uint32_t i0 = indices[t]; + const uint32_t i1 = indices[t + 1]; + const uint32_t i2 = indices[t + 2]; + if (int(i0) >= vertexCount || int(i1) >= vertexCount || int(i2) >= vertexCount) continue; + const glm::vec3 a = readPos(posXYZ, int(i0)); + const glm::vec3 b = readPos(posXYZ, int(i1)); + const glm::vec3 c = readPos(posXYZ, int(i2)); + glm::vec3 n = glm::cross(b - a, c - a); + const float twiceArea = glm::length(n); + if (twiceArea < 1e-10f) continue; + n /= twiceArea; + if (nrmXYZ) { + glm::vec3 ns = readNrm(nrmXYZ, int(i0)) + readNrm(nrmXYZ, int(i1)) + readNrm(nrmXYZ, int(i2)); + if (glm::dot(ns, ns) > 1e-8f) n = glm::normalize(ns); + } + if (n.y < minSlopeDot) continue; + Triangle tri; + tri.i0 = i0; + tri.i1 = i1; + tri.i2 = i2; + tri.area = 0.5f * twiceArea; + tri.n = n; + total += tri.area; + tris.push_back(tri); + cdf.push_back(total); + } + return total > 1e-12f && !tris.empty(); +} + +int pickTriangle(const std::vector &cdf, float u) { + const float target = u * cdf.back(); + auto it = std::lower_bound(cdf.begin(), cdf.end(), target); + int idx = int(it - cdf.begin()); + if (idx >= int(cdf.size())) idx = int(cdf.size()) - 1; + return idx; +} + +void sampleOnTriangle(const float *posXYZ, const Triangle &tri, float u, float v, glm::vec3 &p) { + if (u + v > 1.f) { + u = 1.f - u; + v = 1.f - v; + } + const glm::vec3 a = readPos(posXYZ, int(tri.i0)); + const glm::vec3 b = readPos(posXYZ, int(tri.i1)); + const glm::vec3 c = readPos(posXYZ, int(tri.i2)); + p = a + u * (b - a) + v * (c - a); +} + +struct GridKey { + int x = 0, y = 0, z = 0; + bool operator==(const GridKey &o) const { return x == o.x && y == o.y && z == o.z; } +}; + +struct GridKeyHash { + size_t operator()(const GridKey &k) const { + return size_t(k.x) * 73856093u ^ size_t(k.y) * 19349663u ^ size_t(k.z) * 83492791u; + } +}; + +GridKey toKey(const glm::vec3 &p, float cell) { + return {int(std::floor(p.x / cell)), int(std::floor(p.y / cell)), int(std::floor(p.z / cell))}; +} + +bool tooClose(const glm::vec3 &p, float radius, + const std::vector &accepted, + const std::unordered_map, GridKeyHash> &grid, float cell) { + const GridKey c = toKey(p, cell); + const float r2 = radius * radius; + for (int dz = -1; dz <= 1; ++dz) { + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + GridKey k{c.x + dx, c.y + dy, c.z + dz}; + auto it = grid.find(k); + if (it == grid.end()) continue; + for (int idx : it->second) { + const glm::vec3 d = accepted[size_t(idx)].position - p; + if (glm::dot(d, d) < r2) return true; + } + } + } + } + return false; +} + +float hash01(uint32_t id) { + float h = std::fmod(float(id) * 0.61803398875f, 1.f); + if (h < 0.f) h += 1.f; + return h; +} + +void plotPixel(std::vector &rgba, int w, int h, int x, int y, uint8_t a) { + if (x < 0 || y < 0 || x >= w || y >= h) return; + const size_t i = (size_t(y) * size_t(w) + size_t(x)) * 4u; + if (a > rgba[i + 3]) { + rgba[i + 0] = 255; + rgba[i + 1] = 255; + rgba[i + 2] = 255; + rgba[i + 3] = a; + } +} + +void drawBlade(std::vector &rgba, int atlasW, int atlasH, int ox, int frameW, int frameH, + float baseX, float lean, int thickness) { + const int steps = std::max(frameH - 2, 8); + for (int s = 0; s <= steps; ++s) { + const float t = float(s) / float(steps); // 0 root .. 1 tip + const float x = baseX + lean * t * t * float(frameW) * 0.45f; + const float yFromRoot = t * float(frameH - 2); + const int py = frameH - 1 - int(std::round(yFromRoot)); + const int px = ox + int(std::round(x)); + const uint8_t a = uint8_t(255.f * (1.f - t * 0.15f)); + const int half = std::max(1, int(std::round(float(thickness) * (1.f - t * 0.7f)))); + for (int dx = -half; dx <= half; ++dx) + plotPixel(rgba, atlasW, atlasH, px + dx, py, a); + } +} + +} // namespace + +int paramCount() { return int(kParamSlots.size()); } + +std::string paramName(int index) { + if (index < 0 || index >= int(kParamSlots.size())) return {}; + return kParamSlots[size_t(index)]; +} + +void bindDefaults(Shader *shader) { + if (!shader) throw eve::Exception("grass::bindDefaults: null shader"); + shader->declareFloat("time"); + shader->declareFloat("frameDuration"); + shader->declareFloat("grassWidth"); + shader->declareFloat("grassHeight"); + shader->declareFloat("alphaCutoff"); + shader->declareFloat("alwaysDark"); + shader->declareVec3("lightGreen"); + shader->declareVec3("darkGreen"); + shader->declareFloat("frameCount"); + shader->sendFloat("time", 0.f); + shader->sendFloat("frameDuration", 0.12f); + shader->sendFloat("grassWidth", 0.45f); + shader->sendFloat("grassHeight", 0.7f); + shader->sendFloat("alphaCutoff", 0.35f); + shader->sendFloat("alwaysDark", 0.f); + shader->sendVec3("lightGreen", 0.55f, 0.82f, 0.28f); + shader->sendVec3("darkGreen", 0.12f, 0.32f, 0.14f); + shader->sendFloat("frameCount", 4.f); +} + +void bindLayer(Shader *shader, bool alwaysDark) { + if (!shader) throw eve::Exception("grass::bindLayer: null shader"); + shader->sendFloat("alwaysDark", alwaysDark ? 1.f : 0.f); +} + +void setTime(Shader *shader, float seconds) { + if (!shader) throw eve::Exception("grass::setTime: null shader"); + shader->sendFloat("time", seconds); +} + +void setFrameDuration(Shader *shader, float seconds) { + if (!shader) throw eve::Exception("grass::setFrameDuration: null shader"); + shader->sendFloat("frameDuration", seconds > 1e-4f ? seconds : 1e-4f); +} + +Shader *createShader(Graphics *gfx) { + if (!gfx) throw eve::Exception("grass::createShader: null graphics"); + auto vert = copySpv(mesh3d_grass_vert_spv, mesh3d_grass_vert_spv_count); + auto frag = copySpv(mesh3d_grass_frag_spv, mesh3d_grass_frag_spv_count); + Shader *sh = gfx->newMeshShaderFromSpv(vert, frag); + if (!sh || !sh->gpuHandle) + throw eve::Exception("grass::createShader: failed to create grass shader"); + bindDefaults(sh); + return sh; +} + +int swayFrame(float time, float frameDuration, uint32_t instanceId, int frameCount) { + if (frameCount < 1) frameCount = 1; + const float dur = frameDuration > 1e-4f ? frameDuration : 1e-4f; + const float t = time / dur + hash01(instanceId) * float(frameCount); + int f = int(std::floor(t)) % frameCount; + if (f < 0) f += frameCount; + return f; +} + +int swayAtlasWidth(int frameW, int frames) { return std::max(frameW, 1) * std::max(frames, 1); } +int swayAtlasHeight(int frameH) { return std::max(frameH, 1); } + +void makeSwayAtlasRGBA(int frameW, int frameH, int frames, std::vector &rgbaOut) { + frameW = std::max(frameW, 8); + frameH = std::max(frameH, 8); + frames = std::max(frames, 1); + const int w = swayAtlasWidth(frameW, frames); + const int h = swayAtlasHeight(frameH); + rgbaOut.assign(size_t(w * h * 4), 0); + + const float bladeXs[] = {0.28f, 0.42f, 0.50f, 0.58f, 0.72f}; + const float bladeLean[] = {-0.22f, -0.08f, 0.02f, 0.12f, 0.26f}; + const int bladeThick[] = {1, 1, 2, 1, 1}; + + for (int f = 0; f < frames; ++f) { + const float wind = (float(f) - 1.5f) / 1.5f; // -1 .. +1 across 4 frames + const int ox = f * frameW; + for (int b = 0; b < 5; ++b) { + const float base = bladeXs[b] * float(frameW); + const float lean = (bladeLean[b] + wind * 0.55f); + drawBlade(rgbaOut, w, h, ox, frameW, frameH, base, lean, bladeThick[b]); + } + } +} + +Texture *createSwayAtlas(Graphics *gfx, int frameW, int frameH, int frames) { + if (!gfx) throw eve::Exception("grass::createSwayAtlas: null graphics"); + std::vector rgba; + makeSwayAtlasRGBA(frameW, frameH, frames, rgba); + TextureCreateInfo info; + info.sampler = TextureSampler::nearest(); + return gfx->newTexture(swayAtlasWidth(frameW, frames), swayAtlasHeight(frameH), rgba.data(), + info); +} + +std::vector sampleHalton(const float *posXYZ, const float *nrmXYZ, int vertexCount, + const uint32_t *indices, int indexCount, int count, uint32_t seed, + float minSlopeDot) { + std::vector out; + if (count <= 0) return out; + std::vector tris; + std::vector cdf; + if (!buildTriangles(posXYZ, nrmXYZ, vertexCount, indices, indexCount, minSlopeDot, tris, cdf)) + return out; + + out.reserve(size_t(count)); + for (int i = 0; i < count; ++i) { + const uint32_t n = mixSeed(seed, uint32_t(i + 1)); + const float uTri = wrap01(radicalInverse(n, 2)); + const float u = wrap01(radicalInverse(n, 3)); + const float v = wrap01(radicalInverse(n, 5)); + const Triangle &tri = tris[size_t(pickTriangle(cdf, uTri))]; + Point p; + sampleOnTriangle(posXYZ, tri, u, v, p.position); + p.normal = tri.n; + p.id = uint32_t(i); + p.scale = 0.85f + 0.3f * hash01(p.id + seed); + out.push_back(p); + } + return out; +} + +std::vector samplePoisson(const float *posXYZ, const float *nrmXYZ, int vertexCount, + const uint32_t *indices, int indexCount, + const SampleParams ¶ms) { + std::vector accepted; + if (params.maxPoints <= 0 || params.radius <= 1e-6f) return accepted; + + std::vector tris; + std::vector cdf; + if (!buildTriangles(posXYZ, nrmXYZ, vertexCount, indices, indexCount, params.minSlopeDot, tris, + cdf)) + return accepted; + + const float cell = params.radius; + std::unordered_map, GridKeyHash> grid; + const int attempts = std::max(params.maxPoints * 24, params.maxPoints + 16); + + for (int i = 0; i < attempts && int(accepted.size()) < params.maxPoints; ++i) { + const uint32_t n = mixSeed(params.seed, uint32_t(i + 1)); + const float uTri = wrap01(radicalInverse(n, 2)); + const float u = wrap01(radicalInverse(n, 3)); + const float v = wrap01(radicalInverse(n, 5)); + const Triangle &tri = tris[size_t(pickTriangle(cdf, uTri))]; + glm::vec3 pos; + sampleOnTriangle(posXYZ, tri, u, v, pos); + if (tooClose(pos, params.radius, accepted, grid, cell)) continue; + + Point p; + p.position = pos; + p.normal = tri.n; + p.id = uint32_t(accepted.size()); + p.scale = 0.85f + 0.3f * hash01(p.id + params.seed); + const int idx = int(accepted.size()); + accepted.push_back(p); + grid[toKey(pos, cell)].push_back(idx); + } + return accepted; +} + +BillboardMesh buildBillboards(const std::vector &points, float width, float height) { + (void)width; + (void)height; + BillboardMesh mesh; + const size_t n = points.size(); + mesh.posXYZ.reserve(n * 4 * 3); + mesh.nrmXYZ.reserve(n * 4 * 3); + mesh.uvST.reserve(n * 4 * 2); + mesh.indices.reserve(n * 6); + + // Local corners: (0,0) bottom-left, (1,0) bottom-right, (1,1) top-right, (0,1) top-left. + // Root is the bottom-center of the rectangle, i.e. UV (0.5, 0). + const float cu[4] = {0.f, 1.f, 1.f, 0.f}; + const float cv[4] = {0.f, 0.f, 1.f, 1.f}; + const uint32_t corners[6] = {0, 1, 2, 0, 2, 3}; + + for (size_t i = 0; i < n; ++i) { + const Point &p = points[i]; + const uint32_t base = uint32_t(i * 4); + for (int c = 0; c < 4; ++c) { + mesh.posXYZ.push_back(p.position.x); + mesh.posXYZ.push_back(p.position.y); + mesh.posXYZ.push_back(p.position.z); + mesh.nrmXYZ.push_back(float(p.id)); + mesh.nrmXYZ.push_back(p.scale > 1e-3f ? p.scale : 1.f); + mesh.nrmXYZ.push_back(0.f); + mesh.uvST.push_back(cu[c]); + mesh.uvST.push_back(cv[c]); + } + for (uint32_t k : corners) mesh.indices.push_back(base + k); + } + return mesh; +} + +void makePlane(float sizeX, float sizeZ, int segX, int segZ, std::vector &posXYZ, + std::vector &nrmXYZ, std::vector &indices) { + segX = std::max(segX, 1); + segZ = std::max(segZ, 1); + sizeX = std::max(sizeX, 1e-3f); + sizeZ = std::max(sizeZ, 1e-3f); + const int nx = segX + 1; + const int nz = segZ + 1; + posXYZ.clear(); + nrmXYZ.clear(); + indices.clear(); + posXYZ.reserve(size_t(nx * nz * 3)); + nrmXYZ.reserve(size_t(nx * nz * 3)); + indices.reserve(size_t(segX * segZ * 6)); + + for (int z = 0; z < nz; ++z) { + for (int x = 0; x < nx; ++x) { + const float px = (float(x) / float(segX) - 0.5f) * sizeX; + const float pz = (float(z) / float(segZ) - 0.5f) * sizeZ; + posXYZ.push_back(px); + posXYZ.push_back(0.f); + posXYZ.push_back(pz); + nrmXYZ.push_back(0.f); + nrmXYZ.push_back(1.f); + nrmXYZ.push_back(0.f); + } + } + for (int z = 0; z < segZ; ++z) { + for (int x = 0; x < segX; ++x) { + const uint32_t i0 = uint32_t(z * nx + x); + const uint32_t i1 = i0 + 1; + const uint32_t i2 = i0 + uint32_t(nx); + const uint32_t i3 = i2 + 1; + indices.push_back(i0); + indices.push_back(i2); + indices.push_back(i1); + indices.push_back(i1); + indices.push_back(i2); + indices.push_back(i3); + } + } +} + +} // namespace eve::graphics::grass + +namespace eve::graphics { + +GrassField::GrassField(Graphics *gfx) : gfx_(gfx) { + if (!gfx_) throw eve::Exception("GrassField: null graphics"); +} + +void GrassField::bake(const float *posXYZ, const float *nrmXYZ, int vertexCount, + const uint32_t *indices, int indexCount, const BakeParams ¶ms) { + if (!gfx_) throw eve::Exception("GrassField::bake: null graphics"); + + grass::SampleParams denseP; + denseP.radius = params.denseRadius; + denseP.maxPoints = params.maxDense; + denseP.seed = params.seed; + denseP.minSlopeDot = params.minSlopeDot; + + grass::SampleParams sparseP = denseP; + sparseP.radius = params.sparseRadius; + sparseP.maxPoints = params.maxSparse; + sparseP.seed = params.seed * 7477u + 13u; + + const auto densePts = + grass::samplePoisson(posXYZ, nrmXYZ, vertexCount, indices, indexCount, denseP); + const auto sparsePts = + grass::samplePoisson(posXYZ, nrmXYZ, vertexCount, indices, indexCount, sparseP); + denseCount_ = int(densePts.size()); + sparseCount_ = int(sparsePts.size()); + + const auto denseMesh = grass::buildBillboards(densePts, params.width, params.height); + const auto sparseMesh = grass::buildBillboards(sparsePts, params.width, params.height); + + if (!shader_) shader_ = grass::createShader(gfx_); + if (!atlas_) + atlas_ = grass::createSwayAtlas(gfx_, params.atlasFrameW, params.atlasFrameH, + params.atlasFrames); + + grass::bindDefaults(shader_); + shader_->sendFloat("grassWidth", params.width); + shader_->sendFloat("grassHeight", params.height); + shader_->sendFloat("frameCount", float(std::max(params.atlasFrames, 1))); + shader_->sendFloat("frameDuration", frameDuration_); + shader_->sendFloat("time", time_); + + denseMesh_ = nullptr; + sparseMesh_ = nullptr; + if (!denseMesh.indices.empty()) + denseMesh_ = gfx_->newMeshFromArrays(denseMesh.posXYZ.data(), denseMesh.nrmXYZ.data(), + denseMesh.uvST.data(), int(denseMesh.posXYZ.size() / 3), + denseMesh.indices.data(), int(denseMesh.indices.size())); + if (!sparseMesh.indices.empty()) + sparseMesh_ = + gfx_->newMeshFromArrays(sparseMesh.posXYZ.data(), sparseMesh.nrmXYZ.data(), + sparseMesh.uvST.data(), int(sparseMesh.posXYZ.size() / 3), + sparseMesh.indices.data(), int(sparseMesh.indices.size())); +} + +void GrassField::bakePlane(float sizeX, float sizeZ, int segX, int segZ) { + bakePlane(sizeX, sizeZ, segX, segZ, BakeParams{}); +} + +void GrassField::bakePlane(float sizeX, float sizeZ, int segX, int segZ, const BakeParams ¶ms) { + std::vector pos, nrm; + std::vector idx; + grass::makePlane(sizeX, sizeZ, segX, segZ, pos, nrm, idx); + bake(pos.data(), nrm.data(), int(pos.size() / 3), idx.data(), int(idx.size()), params); +} + +void GrassField::update(float dt) { + time_ += dt; + if (shader_) grass::setTime(shader_, time_); +} + +void GrassField::setTime(float seconds) { + time_ = seconds; + if (shader_) grass::setTime(shader_, time_); +} + +void GrassField::setFrameDuration(float seconds) { + frameDuration_ = seconds > 1e-4f ? seconds : 1e-4f; + if (shader_) grass::setFrameDuration(shader_, frameDuration_); +} + +void GrassField::draw() { draw(glm::mat4(1.f)); } + +void GrassField::draw(const glm::mat4 &model) { + if (!gfx_ || !shader_ || !atlas_) return; + const Color tint(1.f, 1.f, 1.f, 1.f); + grass::setTime(shader_, time_); + grass::setFrameDuration(shader_, frameDuration_); + if (denseMesh_) { + grass::bindLayer(shader_, false); + gfx_->drawMeshShader(denseMesh_, model, atlas_, tint, shader_); + } + if (sparseMesh_) { + grass::bindLayer(shader_, true); + gfx_->drawMeshShader(sparseMesh_, model, atlas_, tint, shader_); + } +} + +} // namespace eve::graphics diff --git a/src/modules/graphics/Grass.h b/src/modules/graphics/Grass.h new file mode 100644 index 00000000..1e56c65e --- /dev/null +++ b/src/modules/graphics/Grass.h @@ -0,0 +1,156 @@ +#pragma once + +#include "graphics/Shader.h" + +#include +#include +#include + +#include + +namespace eve::graphics { + +class Graphics; +class Mesh; +class Texture; + +/** + * t3ssel8r-style stylized grass. + * + * Pipeline: + * 1. Blue-noise (Poisson disk) or Halton samples on a terrain mesh + * 2. Expand a root-pivoted rectangle into a GPU mesh (one quad per sample) + * 3. Vertex shader cylindrical-billboards each card around its root + * 4. 4-frame sway atlas, phase-offset by instance id + * 5. Shadow map → mix(light green, dark green) + * 6. Sparse second pass, always dark, as decoration + */ +namespace grass { + +struct Point { + glm::vec3 position{0.f}; + glm::vec3 normal{0.f, 1.f, 0.f}; + uint32_t id = 0; + float scale = 1.f; +}; + +struct SampleParams { + float radius = 0.35f; + int maxPoints = 4096; + uint32_t seed = 1; + /** Skip faces whose Y-up slope is below this (0 = keep walls). */ + float minSlopeDot = 0.25f; +}; + +struct BillboardMesh { + std::vector posXYZ; + std::vector nrmXYZ; + std::vector uvST; + std::vector indices; +}; + +/** Fast Poisson-disk / dart-throwing blue noise on a triangle mesh. */ +std::vector samplePoisson(const float *posXYZ, const float *nrmXYZ, int vertexCount, + const uint32_t *indices, int indexCount, + const SampleParams ¶ms = {}); + +/** Area-weighted Halton samples (deterministic, evenly spread). */ +std::vector sampleHalton(const float *posXYZ, const float *nrmXYZ, int vertexCount, + const uint32_t *indices, int indexCount, int count, + uint32_t seed = 1, float minSlopeDot = 0.25f); + +/** + * Expand a unit rectangle per point. Vertex layout: + * pos = grass root + * uv = quad corner in [0,1]^2 (0.5, 0) is the root + * normal.x = instance id, normal.y = scale + */ +BillboardMesh buildBillboards(const std::vector &points, float width = 0.45f, + float height = 0.7f); + +/** Discrete 4-frame index with a per-instance phase offset. */ +int swayFrame(float time, float frameDuration, uint32_t instanceId, int frameCount = 4); + +/** Procedural 4-frame white+alpha sway atlas (horizontal strip). */ +void makeSwayAtlasRGBA(int frameW, int frameH, int frames, std::vector &rgbaOut); +int swayAtlasWidth(int frameW, int frames); +int swayAtlasHeight(int frameH); + +Texture *createSwayAtlas(Graphics *gfx, int frameW = 32, int frameH = 32, int frames = 4); + +Shader *createShader(Graphics *gfx); +void bindDefaults(Shader *shader); +void bindLayer(Shader *shader, bool alwaysDark); +void setTime(Shader *shader, float seconds); +void setFrameDuration(Shader *shader, float seconds); + +int paramCount(); +std::string paramName(int index); + +/** Unit XZ plane (Y-up) for tests / demos. */ +void makePlane(float sizeX, float sizeZ, int segX, int segZ, std::vector &posXYZ, + std::vector &nrmXYZ, std::vector &indices); + +} // namespace grass + +/** + * Dense grass + sparse dark tufts on a mesh. Caller owns GrassField*; + * GPU Mesh / Shader / Texture are owned by Graphics. + */ +class GrassField { +public: + struct BakeParams { + float denseRadius = 0.35f; + float sparseRadius = 1.4f; + int maxDense = 4096; + int maxSparse = 256; + uint32_t seed = 1; + float width = 0.45f; + float height = 0.7f; + float minSlopeDot = 0.25f; + int atlasFrameW = 32; + int atlasFrameH = 32; + int atlasFrames = 4; + }; + + explicit GrassField(Graphics *gfx); + ~GrassField() = default; + + GrassField(const GrassField &) = delete; + GrassField &operator=(const GrassField &) = delete; + + void bake(const float *posXYZ, const float *nrmXYZ, int vertexCount, const uint32_t *indices, + int indexCount, const BakeParams ¶ms = {}); + void bakePlane(float sizeX, float sizeZ, int segX, int segZ); + void bakePlane(float sizeX, float sizeZ, int segX, int segZ, const BakeParams ¶ms); + + void update(float dt); + void setTime(float seconds); + float getTime() const { return time_; } + void setFrameDuration(float seconds); + float getFrameDuration() const { return frameDuration_; } + + void draw(const glm::mat4 &model); + void draw(); + + Mesh *getDenseMesh() const { return denseMesh_; } + Mesh *getSparseMesh() const { return sparseMesh_; } + Shader *getShader() const { return shader_; } + Texture *getAtlas() const { return atlas_; } + + int getDenseCount() const { return denseCount_; } + int getSparseCount() const { return sparseCount_; } + +private: + Graphics *gfx_ = nullptr; + Shader *shader_ = nullptr; + Texture *atlas_ = nullptr; + Mesh *denseMesh_ = nullptr; + Mesh *sparseMesh_ = nullptr; + int denseCount_ = 0; + int sparseCount_ = 0; + float time_ = 0.f; + float frameDuration_ = 0.12f; +}; + +} // namespace eve::graphics diff --git a/src/modules/graphics/Shader.h b/src/modules/graphics/Shader.h index f03de8c2..5382da7f 100644 --- a/src/modules/graphics/Shader.h +++ b/src/modules/graphics/Shader.h @@ -23,6 +23,9 @@ namespace eve::graphics { * * **Hair/fur cards** (`newHairShader` / `newHairShaderFromSpv`): * Same layout as mesh shaders; pipeline uses alpha blend, no back-face cull, no depth write. + * + * **Stylized grass** (`newGrassShader`): Mesh3D layout + CSM shadow bindings. + * Alpha-tested billboards (depth write on). See graphics/Grass.h. */ class Shader { public: diff --git a/src/modules/graphics/shaders/mesh3d_grass.frag b/src/modules/graphics/shaders/mesh3d_grass.frag new file mode 100644 index 00000000..3a523a58 --- /dev/null +++ b/src/modules/graphics/shaders/mesh3d_grass.frag @@ -0,0 +1,118 @@ +#version 450 +// t3ssel8r-style grass fragment: 4-frame sway atlas + shadow-mapped two-tone green. +// Push: [0] time [1] frameDuration [2] width [3] height +// [4] alphaCutoff [5] alwaysDark +// [6..8] lightGreen [9..11] darkGreen [12] frameCount + +layout(location = 0) in vec2 vUV; +layout(location = 1) in vec3 vWorldPos; +layout(location = 2) in vec3 vViewPos; +layout(location = 3) in vec3 vRootPos; +layout(location = 4) in float vInstanceId; +layout(location = 5) in vec4 vTint; + +struct Light3D { + vec4 posRadius; + vec4 color; +}; + +layout(set = 0, binding = 0, std140) uniform Frame { + mat4 mvp; + mat4 model; + vec4 lightDirIntensity; + vec4 lightColor; + vec4 tint; + vec4 cameraPos; + vec4 ambient; + Light3D lights[8]; + vec4 texBomb; + vec4 parallax; + mat4 view; + vec4 clipInfo; +} ubo; + +layout(set = 0, binding = 1) uniform sampler2D albedoSampler; + +layout(set = 0, binding = 4, std140) uniform ShadowFrame { + mat4 lightVP[3]; + vec4 splits; + vec4 bias; + vec4 cascadeBias; + vec4 cascadeTexel; +} shadow; + +layout(set = 0, binding = 5) uniform sampler2DArrayShadow shadowMap; + +layout(push_constant) uniform Externals { + float data[32]; +} u; + +layout(location = 0) out vec4 outColor; + +float sampleShadowCascade(vec3 worldPos, int cascade, float ndcBias) { + vec4 lightClip = shadow.lightVP[cascade] * vec4(worldPos, 1.0); + vec3 ndc = lightClip.xyz / max(lightClip.w, 1e-6); + vec2 uv = ndc.xy * 0.5 + 0.5; + float depth = ndc.z; + if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 || depth < 0.0 || depth > 1.0) + return 1.0; + + vec2 texel = 1.0 / vec2(textureSize(shadowMap, 0).xy); + const vec2 kRotOffsets[9] = vec2[9]( + vec2(-0.5, -0.5), vec2(0.0, -0.6), vec2(0.5, -0.5), + vec2(-0.6, 0.0), vec2(0.0, 0.0), vec2(0.6, 0.0), + vec2(-0.5, 0.5), vec2(0.0, 0.6), vec2(0.5, 0.5) + ); + float sum = 0.0; + float zref = depth - ndcBias; + for (int i = 0; i < 9; ++i) { + vec2 s = uv + kRotOffsets[i] * texel; + if (s.x < 0.0 || s.x > 1.0 || s.y < 0.0 || s.y > 1.0) + continue; + sum += texture(shadowMap, vec4(s, float(cascade), zref)); + } + return sum / 9.0; +} + +float cascadeNdcBias(int cascade) { + float b = cascade == 0 ? shadow.cascadeBias.x + : (cascade == 1 ? shadow.cascadeBias.y : shadow.cascadeBias.z); + return b > 1e-8 ? b : shadow.bias.x; +} + +float sampleGrassShadow(vec3 worldPos, float viewDepth) { + if (shadow.bias.y < 0.5 || shadow.bias.z < 0.5 || shadow.splits.w < 1e-4) + return 1.0; + + int cascade = 2; + if (viewDepth < shadow.splits.x) cascade = 0; + else if (viewDepth < shadow.splits.y) cascade = 1; + + float vis = sampleShadowCascade(worldPos, cascade, cascadeNdcBias(cascade)); + vis = mix(1.0, vis, clamp(shadow.splits.w, 0.0, 1.0)); + return vis; +} + +void main() { + float frames = max(u.data[12], 1.0); + float duration = max(u.data[1], 1e-4); + float hash = fract(sin(vInstanceId * 12.9898) * 43758.5453); + float t = u.data[0] / duration + hash * frames; + int frame = int(mod(floor(t), frames)); + + // inUV.y = 0 at the root (bottom). Vulkan/image v=0 is the top row. + vec2 atlasUV = vec2((vUV.x + float(frame)) / frames, 1.0 - vUV.y); + vec4 mask = texture(albedoSampler, atlasUV) * vTint; + if (mask.a < max(u.data[4], 0.01)) + discard; + + float viewDepth = max(-vViewPos.z, 0.0); + float vis = sampleGrassShadow(vRootPos, viewDepth); + if (u.data[5] > 0.5) + vis = 0.0; + + vec3 lightGreen = vec3(u.data[6], u.data[7], u.data[8]); + vec3 darkGreen = vec3(u.data[9], u.data[10], u.data[11]); + vec3 grassCol = mix(darkGreen, lightGreen, vis); + outColor = vec4(grassCol * mask.rgb, mask.a); +} diff --git a/src/modules/graphics/shaders/mesh3d_grass.frag.spv b/src/modules/graphics/shaders/mesh3d_grass.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..e7223463575b7db807a30d73c3ef77321c7d824d GIT binary patch literal 11224 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 ZO#Y>bcO3gf4f-SZ8DMMh%!z$g{XfU6MJxaS literal 0 HcmV?d00001 diff --git a/src/modules/graphics/shaders/mesh3d_grass.vert b/src/modules/graphics/shaders/mesh3d_grass.vert new file mode 100644 index 00000000..9029bd33 --- /dev/null +++ b/src/modules/graphics/shaders/mesh3d_grass.vert @@ -0,0 +1,75 @@ +#version 450 +// t3ssel8r-style grass billboard vertex. +// inPos = grass ROOT (object space) +// inNormal.x = instance id, inNormal.y = scale +// inUV = quad corner in [0,1]^2; (0.5, 0) is the root (bottom center) + +struct Light3D { + vec4 posRadius; + vec4 color; +}; + +layout(location = 0) in vec3 inPos; +layout(location = 1) in vec3 inNormal; +layout(location = 2) in vec2 inUV; + +layout(set = 0, binding = 0, std140) uniform Frame { + mat4 mvp; + mat4 model; + vec4 lightDirIntensity; + vec4 lightColor; + vec4 tint; + vec4 cameraPos; + vec4 ambient; + Light3D lights[8]; + vec4 texBomb; + vec4 parallax; + mat4 view; + vec4 clipInfo; +} ubo; + +layout(push_constant) uniform Externals { + float data[32]; +} u; + +layout(location = 0) out vec2 vUV; +layout(location = 1) out vec3 vWorldPos; +layout(location = 2) out vec3 vViewPos; +layout(location = 3) out vec3 vRootPos; +layout(location = 4) out float vInstanceId; +layout(location = 5) out vec4 vTint; + +void main() { + float width = max(u.data[2], 0.01); + float height = max(u.data[3], 0.01); + float scale = max(inNormal.y, 0.05); + + mat4 invModel = inverse(ubo.model); + vec3 worldUpObj = inverse(mat3(ubo.model)) * vec3(0.0, 1.0, 0.0); + float upLen = length(worldUpObj); + worldUpObj = upLen > 1e-5 ? worldUpObj / upLen : vec3(0.0, 1.0, 0.0); + + vec3 camObj = (invModel * vec4(ubo.cameraPos.xyz, 1.0)).xyz; + vec3 toCam = camObj - inPos; + toCam = toCam - worldUpObj * dot(toCam, worldUpObj); + vec3 right; + float toCamLen = length(toCam); + if (toCamLen > 1e-4) { + right = normalize(cross(worldUpObj, toCam)); + } else { + vec3 fallback = abs(worldUpObj.y) < 0.95 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0); + right = normalize(cross(worldUpObj, fallback)); + } + + vec3 obj = inPos + right * (inUV.x - 0.5) * width * scale + worldUpObj * inUV.y * height * scale; + vec4 world = ubo.model * vec4(obj, 1.0); + vec4 rootWorld = ubo.model * vec4(inPos, 1.0); + + gl_Position = ubo.mvp * vec4(obj, 1.0); + vUV = inUV; + vWorldPos = world.xyz; + vViewPos = (ubo.view * world).xyz; + vRootPos = rootWorld.xyz; + vInstanceId = inNormal.x; + vTint = ubo.tint; +} diff --git a/src/modules/graphics/shaders/mesh3d_grass.vert.spv b/src/modules/graphics/shaders/mesh3d_grass.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..2d5637c05be45f329c549614e896f4b969d96f54 GIT binary patch literal 6308 zcmZ9O`u z$J{BU(xlRvrKzRAm-6TAQU^>4uAIr${TuohjJAgsTzS<(Bc_)+vp{3c;-60!7_QVB z>KA}2H~=04$H5735p`E`jSe&7zgmEoZjzBhAtEm7&`9=*KwcGT5v)TS)iuTI8x$W!ReKWTTdb z$40U}=y%Oy82Krd7 zx+CZE*fTK(>$Q=-#+IhM=X5!8`#>|B`PJwZNjyn~>~b z+&_rjZuVA&li&H)8u|V_TMK3WJVlnnrds)R$m;IG-og$BDuZ{U!gZGRA)7Q|P4956 zvC-0*eFT~Hnc0&|N0F^&vwfSrXI#1TW8~KQ9qX#CP1P1d!^~zT@1w8DS=O2j_$gVu z=b!PM+lJb`Ze`r(J;2w?_*T|N+m*&(b=>D2(6_x_ANRQj-#Z_5-@h@o;7m@aXU(3T z(@D8a99T|ealWTx>)BX=^SvGGYvdjD4H4Isy~ks>=9g_Xw&(1f@(!-1w(qR{uOV99 zXKpU=cV+M29BlV*jPH+6!~~~bZU%7OWk~CEXLjfK`?3a~_u9uh@A|O4*RC&er|abF ztW#X4J#k(1vaFEHJ(_EGf$L11cPMH)7vl~WIP1whRN&m3+_3^@A9BwWIN!^Rqp7Ij zdnxyijLY}sJ6YV9_QZY3x$d8ZT%M=gLN@5!eP?@sPuLgcW@cB>Jx?F~?p@pYcprQR z)a^Ci2W$8QNBQg?DT8y3ah(NDT`s?CUD-V^);@tR?v}lH&rkQ#mHifJn@7L*TRZak z8>Q{s&Y^32=pov8_4Co<4tBH3c?!Anusw%v;B#rRopCpqjcxD7%FQ8{?}wbZUW#KFEF zy-R`j-&y)SFh*_;e*yT;yEgL1+K|SF9x>Kk0PoCP?>@D?^Kvn#h3K)*E0WI~uD=R7 z1vn@7b#2m}W6XU8x=+}>nYDZAWlY3%D}gcc%7}Xd`h!o-zvf)Jd1K-|_uGMY)cTu| z_Uhi^oO~zkH}d%VKl9;d7l*wEd(ooN@%nqQqqg-uultbJ>`eB4WEoh?x*tJWL*BaD zzWY(v^EwE8qVA#GTo z=X(*Z3tY{qI$w1kbN(B~vv#fX^8PEso;ktx@7?hGPF-BEy{rD+6@KNag026i3ATR- zMZEsOf-T=H*!ul@s~Ep)g1xU`dk2n8u#YBtIVbNo>>My1_^tE%wuCu2+jD`wX;jlb zA4~%7S>Ji!1}5md^hLgxqes43=#lRg==vhxE72pLzR2gi^)2UAdXN_azl+@mB4v;$K{-%{?$OvJl>Txz&tB}KG&>5%EuY@ zq02o=4Znl^K+bRSzldLp^!YC}^xX_@1-`H9QF|S_wIlB>=;n2=_O>3#1@2FK+K|QM ze*5#il=~0)t!*9ah0nbgea1u&8_``Kv(eV)eYy>Jf4n=Jk#7Rto!fyi^N?=_@^OA| zNu0cov+MxglQQzHz!;z4-i9t`Y;boZ&b8}!kF9klaJ}*Rdx7VyuLA7%P7u8epvwy% z^PA7TZbIshISir8g}nv63ihU%Z$&po-t~h>xrpC}E*EwUJpS}%up8_FzHj#--wWge52W7SmwfiU7uee_ zq><^B8*YCo~N3Y*Q_Z~&B-$$483GN5za>ZVM zn7CrEKZ0|;@%oFsK8$T`W9;=PQZ9P^3A(+`05R8}qRU6GKSM9h_2<}fKGEwjbh%=$ zzerrM*T>;pe+=j^_WDa~Ya3&KPavNJj{*C$e{JLJZ9nqY-~iBfJlXo@q5l?mHu1ak zRN~})?A6{}djhFH=JGpq=khd&xjcg|ANij}H-FrV-=iBNA2t7gE-!qFHBVyek9>bb zKMG>)pU~xmPjT&Y*!u0a2l;1k0XPWES4RE?$h*H&$u@p7`d@+jdl5waza`%I7tpo; IN7cpP|M(;-4*&oF literal 0 HcmV?d00001 diff --git a/src/modules/graphics/shaders/mesh3d_grass_frag_spv.inc b/src/modules/graphics/shaders/mesh3d_grass_frag_spv.inc new file mode 100644 index 00000000..776a1d06 --- /dev/null +++ b/src/modules/graphics/shaders/mesh3d_grass_frag_spv.inc @@ -0,0 +1,354 @@ +static const uint32_t mesh3d_grass_frag_spv[] = { + 0x07230203, 0x00010000, 0x0008000b, 0x000001b9, 0x00000000, 0x00020011, 0x00000001, 0x00020011, + 0x00000032, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, + 0x00000000, 0x00000001, 0x000c000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x0000013b, + 0x00000154, 0x00000169, 0x00000179, 0x0000017f, 0x000001a7, 0x000001b2, 0x00030010, 0x00000004, + 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x00040005, 0x00000004, 0x6e69616d, 0x00000000, + 0x000a0005, 0x00000010, 0x706d6173, 0x6853656c, 0x776f6461, 0x63736143, 0x28656461, 0x3b336676, + 0x663b3169, 0x00003b31, 0x00050005, 0x0000000d, 0x6c726f77, 0x736f5064, 0x00000000, 0x00040005, + 0x0000000e, 0x63736163, 0x00656461, 0x00040005, 0x0000000f, 0x4263646e, 0x00736169, 0x00070005, + 0x00000014, 0x63736163, 0x4e656461, 0x69426364, 0x69287361, 0x00003b31, 0x00040005, 0x00000013, + 0x63736163, 0x00656461, 0x00090005, 0x00000019, 0x706d6173, 0x7247656c, 0x53737361, 0x6f646168, + 0x66762877, 0x31663b33, 0x0000003b, 0x00050005, 0x00000017, 0x6c726f77, 0x736f5064, 0x00000000, + 0x00050005, 0x00000018, 0x77656976, 0x74706544, 0x00000068, 0x00050005, 0x0000001d, 0x6867696c, + 0x696c4374, 0x00000070, 0x00050005, 0x00000022, 0x64616853, 0x7246776f, 0x00656d61, 0x00050006, + 0x00000022, 0x00000000, 0x6867696c, 0x00505674, 0x00050006, 0x00000022, 0x00000001, 0x696c7073, + 0x00007374, 0x00050006, 0x00000022, 0x00000002, 0x73616962, 0x00000000, 0x00060006, 0x00000022, + 0x00000003, 0x63736163, 0x42656461, 0x00736169, 0x00070006, 0x00000022, 0x00000004, 0x63736163, + 0x54656461, 0x6c657865, 0x00000000, 0x00040005, 0x00000024, 0x64616873, 0x0000776f, 0x00030005, + 0x00000031, 0x0063646e, 0x00030005, 0x0000003c, 0x00007675, 0x00040005, 0x00000043, 0x74706564, + 0x00000068, 0x00040005, 0x0000006c, 0x65786574, 0x0000006c, 0x00050005, 0x00000070, 0x64616873, + 0x614d776f, 0x00000070, 0x00030005, 0x0000007a, 0x006d7573, 0x00040005, 0x0000007b, 0x6665727a, + 0x00000000, 0x00030005, 0x0000007f, 0x00000069, 0x00030005, 0x00000088, 0x00000073, 0x00050005, + 0x0000009b, 0x65646e69, 0x6c626178, 0x00000065, 0x00030005, 0x000000d0, 0x00000062, 0x00040005, + 0x0000010a, 0x63736163, 0x00656461, 0x00030005, 0x00000118, 0x00736976, 0x00040005, 0x00000119, + 0x61726170, 0x0000006d, 0x00040005, 0x0000011c, 0x61726170, 0x0000006d, 0x00040005, 0x0000011e, + 0x61726170, 0x0000006d, 0x00040005, 0x00000120, 0x61726170, 0x0000006d, 0x00040005, 0x0000012a, + 0x6d617266, 0x00007365, 0x00050005, 0x0000012d, 0x65747845, 0x6c616e72, 0x00000073, 0x00050006, + 0x0000012d, 0x00000000, 0x61746164, 0x00000000, 0x00030005, 0x0000012f, 0x00000075, 0x00050005, + 0x00000135, 0x61727564, 0x6e6f6974, 0x00000000, 0x00040005, 0x00000139, 0x68736168, 0x00000000, + 0x00050005, 0x0000013b, 0x736e4976, 0x636e6174, 0x00644965, 0x00030005, 0x00000143, 0x00000074, + 0x00040005, 0x0000014c, 0x6d617266, 0x00000065, 0x00040005, 0x00000152, 0x616c7461, 0x00565573, + 0x00030005, 0x00000154, 0x00565576, 0x00040005, 0x00000160, 0x6b73616d, 0x00000000, 0x00060005, + 0x00000164, 0x65626c61, 0x61536f64, 0x656c706d, 0x00000072, 0x00040005, 0x00000169, 0x6e695476, + 0x00000074, 0x00050005, 0x00000177, 0x77656976, 0x74706544, 0x00000068, 0x00050005, 0x00000179, + 0x65695676, 0x736f5077, 0x00000000, 0x00030005, 0x0000017e, 0x00736976, 0x00050005, 0x0000017f, + 0x6f6f5276, 0x736f5074, 0x00000000, 0x00040005, 0x00000180, 0x61726170, 0x0000006d, 0x00040005, + 0x00000182, 0x61726170, 0x0000006d, 0x00050005, 0x0000018b, 0x6867696c, 0x65724774, 0x00006e65, + 0x00050005, 0x00000196, 0x6b726164, 0x65657247, 0x0000006e, 0x00050005, 0x000001a0, 0x73617267, + 0x6c6f4373, 0x00000000, 0x00050005, 0x000001a7, 0x4374756f, 0x726f6c6f, 0x00000000, 0x00050005, + 0x000001b2, 0x726f5776, 0x6f50646c, 0x00000073, 0x00040005, 0x000001b3, 0x6867694c, 0x00443374, + 0x00060006, 0x000001b3, 0x00000000, 0x52736f70, 0x75696461, 0x00000073, 0x00050006, 0x000001b3, + 0x00000001, 0x6f6c6f63, 0x00000072, 0x00040005, 0x000001b6, 0x6d617246, 0x00000065, 0x00040006, + 0x000001b6, 0x00000000, 0x0070766d, 0x00050006, 0x000001b6, 0x00000001, 0x65646f6d, 0x0000006c, + 0x00080006, 0x000001b6, 0x00000002, 0x6867696c, 0x72694474, 0x65746e49, 0x7469736e, 0x00000079, + 0x00060006, 0x000001b6, 0x00000003, 0x6867696c, 0x6c6f4374, 0x0000726f, 0x00050006, 0x000001b6, + 0x00000004, 0x746e6974, 0x00000000, 0x00060006, 0x000001b6, 0x00000005, 0x656d6163, 0x6f506172, + 0x00000073, 0x00050006, 0x000001b6, 0x00000006, 0x69626d61, 0x00746e65, 0x00050006, 0x000001b6, + 0x00000007, 0x6867696c, 0x00007374, 0x00050006, 0x000001b6, 0x00000008, 0x42786574, 0x00626d6f, + 0x00060006, 0x000001b6, 0x00000009, 0x61726170, 0x78616c6c, 0x00000000, 0x00050006, 0x000001b6, + 0x0000000a, 0x77656976, 0x00000000, 0x00060006, 0x000001b6, 0x0000000b, 0x70696c63, 0x6f666e49, + 0x00000000, 0x00030005, 0x000001b8, 0x006f6275, 0x00040047, 0x00000021, 0x00000006, 0x00000040, + 0x00030047, 0x00000022, 0x00000002, 0x00040048, 0x00000022, 0x00000000, 0x00000005, 0x00050048, + 0x00000022, 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x00000022, 0x00000000, 0x00000023, + 0x00000000, 0x00050048, 0x00000022, 0x00000001, 0x00000023, 0x000000c0, 0x00050048, 0x00000022, + 0x00000002, 0x00000023, 0x000000d0, 0x00050048, 0x00000022, 0x00000003, 0x00000023, 0x000000e0, + 0x00050048, 0x00000022, 0x00000004, 0x00000023, 0x000000f0, 0x00040047, 0x00000024, 0x00000021, + 0x00000004, 0x00040047, 0x00000024, 0x00000022, 0x00000000, 0x00040047, 0x00000070, 0x00000021, + 0x00000005, 0x00040047, 0x00000070, 0x00000022, 0x00000000, 0x00040047, 0x0000012c, 0x00000006, + 0x00000004, 0x00030047, 0x0000012d, 0x00000002, 0x00050048, 0x0000012d, 0x00000000, 0x00000023, + 0x00000000, 0x00040047, 0x0000013b, 0x0000001e, 0x00000004, 0x00040047, 0x00000154, 0x0000001e, + 0x00000000, 0x00040047, 0x00000164, 0x00000021, 0x00000001, 0x00040047, 0x00000164, 0x00000022, + 0x00000000, 0x00040047, 0x00000169, 0x0000001e, 0x00000005, 0x00040047, 0x00000179, 0x0000001e, + 0x00000002, 0x00040047, 0x0000017f, 0x0000001e, 0x00000003, 0x00040047, 0x000001a7, 0x0000001e, + 0x00000000, 0x00040047, 0x000001b2, 0x0000001e, 0x00000001, 0x00050048, 0x000001b3, 0x00000000, + 0x00000023, 0x00000000, 0x00050048, 0x000001b3, 0x00000001, 0x00000023, 0x00000010, 0x00040047, + 0x000001b5, 0x00000006, 0x00000020, 0x00030047, 0x000001b6, 0x00000002, 0x00040048, 0x000001b6, + 0x00000000, 0x00000005, 0x00050048, 0x000001b6, 0x00000000, 0x00000007, 0x00000010, 0x00050048, + 0x000001b6, 0x00000000, 0x00000023, 0x00000000, 0x00040048, 0x000001b6, 0x00000001, 0x00000005, + 0x00050048, 0x000001b6, 0x00000001, 0x00000007, 0x00000010, 0x00050048, 0x000001b6, 0x00000001, + 0x00000023, 0x00000040, 0x00050048, 0x000001b6, 0x00000002, 0x00000023, 0x00000080, 0x00050048, + 0x000001b6, 0x00000003, 0x00000023, 0x00000090, 0x00050048, 0x000001b6, 0x00000004, 0x00000023, + 0x000000a0, 0x00050048, 0x000001b6, 0x00000005, 0x00000023, 0x000000b0, 0x00050048, 0x000001b6, + 0x00000006, 0x00000023, 0x000000c0, 0x00050048, 0x000001b6, 0x00000007, 0x00000023, 0x000000d0, + 0x00050048, 0x000001b6, 0x00000008, 0x00000023, 0x000001d0, 0x00050048, 0x000001b6, 0x00000009, + 0x00000023, 0x000001e0, 0x00040048, 0x000001b6, 0x0000000a, 0x00000005, 0x00050048, 0x000001b6, + 0x0000000a, 0x00000007, 0x00000010, 0x00050048, 0x000001b6, 0x0000000a, 0x00000023, 0x000001f0, + 0x00050048, 0x000001b6, 0x0000000b, 0x00000023, 0x00000230, 0x00040047, 0x000001b8, 0x00000021, + 0x00000000, 0x00040047, 0x000001b8, 0x00000022, 0x00000000, 0x00020013, 0x00000002, 0x00030021, + 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, + 0x00000003, 0x00040020, 0x00000008, 0x00000007, 0x00000007, 0x00040015, 0x00000009, 0x00000020, + 0x00000001, 0x00040020, 0x0000000a, 0x00000007, 0x00000009, 0x00040020, 0x0000000b, 0x00000007, + 0x00000006, 0x00060021, 0x0000000c, 0x00000006, 0x00000008, 0x0000000a, 0x0000000b, 0x00040021, + 0x00000012, 0x00000006, 0x0000000a, 0x00050021, 0x00000016, 0x00000006, 0x00000008, 0x0000000b, + 0x00040017, 0x0000001b, 0x00000006, 0x00000004, 0x00040020, 0x0000001c, 0x00000007, 0x0000001b, + 0x00040018, 0x0000001e, 0x0000001b, 0x00000004, 0x00040015, 0x0000001f, 0x00000020, 0x00000000, + 0x0004002b, 0x0000001f, 0x00000020, 0x00000003, 0x0004001c, 0x00000021, 0x0000001e, 0x00000020, + 0x0007001e, 0x00000022, 0x00000021, 0x0000001b, 0x0000001b, 0x0000001b, 0x0000001b, 0x00040020, + 0x00000023, 0x00000002, 0x00000022, 0x0004003b, 0x00000023, 0x00000024, 0x00000002, 0x0004002b, + 0x00000009, 0x00000025, 0x00000000, 0x00040020, 0x00000027, 0x00000002, 0x0000001e, 0x0004002b, + 0x00000006, 0x0000002b, 0x3f800000, 0x0004002b, 0x00000006, 0x00000036, 0x358637bd, 0x00040017, + 0x0000003a, 0x00000006, 0x00000002, 0x00040020, 0x0000003b, 0x00000007, 0x0000003a, 0x0004002b, + 0x00000006, 0x0000003f, 0x3f000000, 0x0004002b, 0x0000001f, 0x00000044, 0x00000002, 0x00020014, + 0x00000047, 0x0004002b, 0x0000001f, 0x00000048, 0x00000000, 0x0004002b, 0x00000006, 0x0000004b, + 0x00000000, 0x0004002b, 0x0000001f, 0x00000057, 0x00000001, 0x00090019, 0x0000006d, 0x00000006, + 0x00000001, 0x00000001, 0x00000001, 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x0000006e, + 0x0000006d, 0x00040020, 0x0000006f, 0x00000000, 0x0000006e, 0x0004003b, 0x0000006f, 0x00000070, + 0x00000000, 0x00040017, 0x00000073, 0x00000009, 0x00000003, 0x00040017, 0x00000075, 0x00000009, + 0x00000002, 0x0004002b, 0x00000009, 0x00000086, 0x00000009, 0x0004002b, 0x0000001f, 0x0000008a, + 0x00000009, 0x0004001c, 0x0000008b, 0x0000003a, 0x0000008a, 0x0004002b, 0x00000006, 0x0000008c, + 0xbf000000, 0x0005002c, 0x0000003a, 0x0000008d, 0x0000008c, 0x0000008c, 0x0004002b, 0x00000006, + 0x0000008e, 0xbf19999a, 0x0005002c, 0x0000003a, 0x0000008f, 0x0000004b, 0x0000008e, 0x0005002c, + 0x0000003a, 0x00000090, 0x0000003f, 0x0000008c, 0x0005002c, 0x0000003a, 0x00000091, 0x0000008e, + 0x0000004b, 0x0005002c, 0x0000003a, 0x00000092, 0x0000004b, 0x0000004b, 0x0004002b, 0x00000006, + 0x00000093, 0x3f19999a, 0x0005002c, 0x0000003a, 0x00000094, 0x00000093, 0x0000004b, 0x0005002c, + 0x0000003a, 0x00000095, 0x0000008c, 0x0000003f, 0x0005002c, 0x0000003a, 0x00000096, 0x0000004b, + 0x00000093, 0x0005002c, 0x0000003a, 0x00000097, 0x0000003f, 0x0000003f, 0x000c002c, 0x0000008b, + 0x00000098, 0x0000008d, 0x0000008f, 0x00000090, 0x00000091, 0x00000092, 0x00000094, 0x00000095, + 0x00000096, 0x00000097, 0x00040020, 0x0000009a, 0x00000007, 0x0000008b, 0x0004002b, 0x00000009, + 0x000000c9, 0x00000001, 0x0004002b, 0x00000006, 0x000000cc, 0x41100000, 0x0004002b, 0x00000009, + 0x000000d6, 0x00000003, 0x00040020, 0x000000d7, 0x00000002, 0x00000006, 0x0004002b, 0x00000006, + 0x000000e8, 0x322bcc77, 0x0004002b, 0x00000009, 0x000000ef, 0x00000002, 0x0004002b, 0x00000006, + 0x00000104, 0x38d1b717, 0x0004002b, 0x0000001f, 0x0000012b, 0x00000020, 0x0004001c, 0x0000012c, + 0x00000006, 0x0000012b, 0x0003001e, 0x0000012d, 0x0000012c, 0x00040020, 0x0000012e, 0x00000009, + 0x0000012d, 0x0004003b, 0x0000012e, 0x0000012f, 0x00000009, 0x0004002b, 0x00000009, 0x00000130, + 0x0000000c, 0x00040020, 0x00000131, 0x00000009, 0x00000006, 0x00040020, 0x0000013a, 0x00000001, + 0x00000006, 0x0004003b, 0x0000013a, 0x0000013b, 0x00000001, 0x0004002b, 0x00000006, 0x0000013d, + 0x414fd639, 0x0004002b, 0x00000006, 0x00000140, 0x472aee8c, 0x00040020, 0x00000153, 0x00000001, + 0x0000003a, 0x0004003b, 0x00000153, 0x00000154, 0x00000001, 0x00090019, 0x00000161, 0x00000006, + 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x00000162, + 0x00000161, 0x00040020, 0x00000163, 0x00000000, 0x00000162, 0x0004003b, 0x00000163, 0x00000164, + 0x00000000, 0x00040020, 0x00000168, 0x00000001, 0x0000001b, 0x0004003b, 0x00000168, 0x00000169, + 0x00000001, 0x0004002b, 0x00000009, 0x0000016e, 0x00000004, 0x0004002b, 0x00000006, 0x00000171, + 0x3c23d70a, 0x00040020, 0x00000178, 0x00000001, 0x00000007, 0x0004003b, 0x00000178, 0x00000179, + 0x00000001, 0x0004003b, 0x00000178, 0x0000017f, 0x00000001, 0x0004002b, 0x00000009, 0x00000185, + 0x00000005, 0x0004002b, 0x00000009, 0x0000018c, 0x00000006, 0x0004002b, 0x00000009, 0x0000018f, + 0x00000007, 0x0004002b, 0x00000009, 0x00000192, 0x00000008, 0x0004002b, 0x00000009, 0x00000199, + 0x0000000a, 0x0004002b, 0x00000009, 0x0000019c, 0x0000000b, 0x00040020, 0x000001a6, 0x00000003, + 0x0000001b, 0x0004003b, 0x000001a6, 0x000001a7, 0x00000003, 0x0004003b, 0x00000178, 0x000001b2, + 0x00000001, 0x0004001e, 0x000001b3, 0x0000001b, 0x0000001b, 0x0004002b, 0x0000001f, 0x000001b4, + 0x00000008, 0x0004001c, 0x000001b5, 0x000001b3, 0x000001b4, 0x000e001e, 0x000001b6, 0x0000001e, + 0x0000001e, 0x0000001b, 0x0000001b, 0x0000001b, 0x0000001b, 0x0000001b, 0x000001b5, 0x0000001b, + 0x0000001b, 0x0000001e, 0x0000001b, 0x00040020, 0x000001b7, 0x00000002, 0x000001b6, 0x0004003b, + 0x000001b7, 0x000001b8, 0x00000002, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, + 0x000200f8, 0x00000005, 0x0004003b, 0x0000000b, 0x0000012a, 0x00000007, 0x0004003b, 0x0000000b, + 0x00000135, 0x00000007, 0x0004003b, 0x0000000b, 0x00000139, 0x00000007, 0x0004003b, 0x0000000b, + 0x00000143, 0x00000007, 0x0004003b, 0x0000000a, 0x0000014c, 0x00000007, 0x0004003b, 0x0000003b, + 0x00000152, 0x00000007, 0x0004003b, 0x0000001c, 0x00000160, 0x00000007, 0x0004003b, 0x0000000b, + 0x00000177, 0x00000007, 0x0004003b, 0x0000000b, 0x0000017e, 0x00000007, 0x0004003b, 0x00000008, + 0x00000180, 0x00000007, 0x0004003b, 0x0000000b, 0x00000182, 0x00000007, 0x0004003b, 0x00000008, + 0x0000018b, 0x00000007, 0x0004003b, 0x00000008, 0x00000196, 0x00000007, 0x0004003b, 0x00000008, + 0x000001a0, 0x00000007, 0x00060041, 0x00000131, 0x00000132, 0x0000012f, 0x00000025, 0x00000130, + 0x0004003d, 0x00000006, 0x00000133, 0x00000132, 0x0007000c, 0x00000006, 0x00000134, 0x00000001, + 0x00000028, 0x00000133, 0x0000002b, 0x0003003e, 0x0000012a, 0x00000134, 0x00060041, 0x00000131, + 0x00000136, 0x0000012f, 0x00000025, 0x000000c9, 0x0004003d, 0x00000006, 0x00000137, 0x00000136, + 0x0007000c, 0x00000006, 0x00000138, 0x00000001, 0x00000028, 0x00000137, 0x00000104, 0x0003003e, + 0x00000135, 0x00000138, 0x0004003d, 0x00000006, 0x0000013c, 0x0000013b, 0x00050085, 0x00000006, + 0x0000013e, 0x0000013c, 0x0000013d, 0x0006000c, 0x00000006, 0x0000013f, 0x00000001, 0x0000000d, + 0x0000013e, 0x00050085, 0x00000006, 0x00000141, 0x0000013f, 0x00000140, 0x0006000c, 0x00000006, + 0x00000142, 0x00000001, 0x0000000a, 0x00000141, 0x0003003e, 0x00000139, 0x00000142, 0x00060041, + 0x00000131, 0x00000144, 0x0000012f, 0x00000025, 0x00000025, 0x0004003d, 0x00000006, 0x00000145, + 0x00000144, 0x0004003d, 0x00000006, 0x00000146, 0x00000135, 0x00050088, 0x00000006, 0x00000147, + 0x00000145, 0x00000146, 0x0004003d, 0x00000006, 0x00000148, 0x00000139, 0x0004003d, 0x00000006, + 0x00000149, 0x0000012a, 0x00050085, 0x00000006, 0x0000014a, 0x00000148, 0x00000149, 0x00050081, + 0x00000006, 0x0000014b, 0x00000147, 0x0000014a, 0x0003003e, 0x00000143, 0x0000014b, 0x0004003d, + 0x00000006, 0x0000014d, 0x00000143, 0x0006000c, 0x00000006, 0x0000014e, 0x00000001, 0x00000008, + 0x0000014d, 0x0004003d, 0x00000006, 0x0000014f, 0x0000012a, 0x0005008d, 0x00000006, 0x00000150, + 0x0000014e, 0x0000014f, 0x0004006e, 0x00000009, 0x00000151, 0x00000150, 0x0003003e, 0x0000014c, + 0x00000151, 0x00050041, 0x0000013a, 0x00000155, 0x00000154, 0x00000048, 0x0004003d, 0x00000006, + 0x00000156, 0x00000155, 0x0004003d, 0x00000009, 0x00000157, 0x0000014c, 0x0004006f, 0x00000006, + 0x00000158, 0x00000157, 0x00050081, 0x00000006, 0x00000159, 0x00000156, 0x00000158, 0x0004003d, + 0x00000006, 0x0000015a, 0x0000012a, 0x00050088, 0x00000006, 0x0000015b, 0x00000159, 0x0000015a, + 0x00050041, 0x0000013a, 0x0000015c, 0x00000154, 0x00000057, 0x0004003d, 0x00000006, 0x0000015d, + 0x0000015c, 0x00050083, 0x00000006, 0x0000015e, 0x0000002b, 0x0000015d, 0x00050050, 0x0000003a, + 0x0000015f, 0x0000015b, 0x0000015e, 0x0003003e, 0x00000152, 0x0000015f, 0x0004003d, 0x00000162, + 0x00000165, 0x00000164, 0x0004003d, 0x0000003a, 0x00000166, 0x00000152, 0x00050057, 0x0000001b, + 0x00000167, 0x00000165, 0x00000166, 0x0004003d, 0x0000001b, 0x0000016a, 0x00000169, 0x00050085, + 0x0000001b, 0x0000016b, 0x00000167, 0x0000016a, 0x0003003e, 0x00000160, 0x0000016b, 0x00050041, + 0x0000000b, 0x0000016c, 0x00000160, 0x00000020, 0x0004003d, 0x00000006, 0x0000016d, 0x0000016c, + 0x00060041, 0x00000131, 0x0000016f, 0x0000012f, 0x00000025, 0x0000016e, 0x0004003d, 0x00000006, + 0x00000170, 0x0000016f, 0x0007000c, 0x00000006, 0x00000172, 0x00000001, 0x00000028, 0x00000170, + 0x00000171, 0x000500b8, 0x00000047, 0x00000173, 0x0000016d, 0x00000172, 0x000300f7, 0x00000175, + 0x00000000, 0x000400fa, 0x00000173, 0x00000174, 0x00000175, 0x000200f8, 0x00000174, 0x000100fc, + 0x000200f8, 0x00000175, 0x00050041, 0x0000013a, 0x0000017a, 0x00000179, 0x00000044, 0x0004003d, + 0x00000006, 0x0000017b, 0x0000017a, 0x0004007f, 0x00000006, 0x0000017c, 0x0000017b, 0x0007000c, + 0x00000006, 0x0000017d, 0x00000001, 0x00000028, 0x0000017c, 0x0000004b, 0x0003003e, 0x00000177, + 0x0000017d, 0x0004003d, 0x00000007, 0x00000181, 0x0000017f, 0x0003003e, 0x00000180, 0x00000181, + 0x0004003d, 0x00000006, 0x00000183, 0x00000177, 0x0003003e, 0x00000182, 0x00000183, 0x00060039, + 0x00000006, 0x00000184, 0x00000019, 0x00000180, 0x00000182, 0x0003003e, 0x0000017e, 0x00000184, + 0x00060041, 0x00000131, 0x00000186, 0x0000012f, 0x00000025, 0x00000185, 0x0004003d, 0x00000006, + 0x00000187, 0x00000186, 0x000500ba, 0x00000047, 0x00000188, 0x00000187, 0x0000003f, 0x000300f7, + 0x0000018a, 0x00000000, 0x000400fa, 0x00000188, 0x00000189, 0x0000018a, 0x000200f8, 0x00000189, + 0x0003003e, 0x0000017e, 0x0000004b, 0x000200f9, 0x0000018a, 0x000200f8, 0x0000018a, 0x00060041, + 0x00000131, 0x0000018d, 0x0000012f, 0x00000025, 0x0000018c, 0x0004003d, 0x00000006, 0x0000018e, + 0x0000018d, 0x00060041, 0x00000131, 0x00000190, 0x0000012f, 0x00000025, 0x0000018f, 0x0004003d, + 0x00000006, 0x00000191, 0x00000190, 0x00060041, 0x00000131, 0x00000193, 0x0000012f, 0x00000025, + 0x00000192, 0x0004003d, 0x00000006, 0x00000194, 0x00000193, 0x00060050, 0x00000007, 0x00000195, + 0x0000018e, 0x00000191, 0x00000194, 0x0003003e, 0x0000018b, 0x00000195, 0x00060041, 0x00000131, + 0x00000197, 0x0000012f, 0x00000025, 0x00000086, 0x0004003d, 0x00000006, 0x00000198, 0x00000197, + 0x00060041, 0x00000131, 0x0000019a, 0x0000012f, 0x00000025, 0x00000199, 0x0004003d, 0x00000006, + 0x0000019b, 0x0000019a, 0x00060041, 0x00000131, 0x0000019d, 0x0000012f, 0x00000025, 0x0000019c, + 0x0004003d, 0x00000006, 0x0000019e, 0x0000019d, 0x00060050, 0x00000007, 0x0000019f, 0x00000198, + 0x0000019b, 0x0000019e, 0x0003003e, 0x00000196, 0x0000019f, 0x0004003d, 0x00000007, 0x000001a1, + 0x00000196, 0x0004003d, 0x00000007, 0x000001a2, 0x0000018b, 0x0004003d, 0x00000006, 0x000001a3, + 0x0000017e, 0x00060050, 0x00000007, 0x000001a4, 0x000001a3, 0x000001a3, 0x000001a3, 0x0008000c, + 0x00000007, 0x000001a5, 0x00000001, 0x0000002e, 0x000001a1, 0x000001a2, 0x000001a4, 0x0003003e, + 0x000001a0, 0x000001a5, 0x0004003d, 0x00000007, 0x000001a8, 0x000001a0, 0x0004003d, 0x0000001b, + 0x000001a9, 0x00000160, 0x0008004f, 0x00000007, 0x000001aa, 0x000001a9, 0x000001a9, 0x00000000, + 0x00000001, 0x00000002, 0x00050085, 0x00000007, 0x000001ab, 0x000001a8, 0x000001aa, 0x00050041, + 0x0000000b, 0x000001ac, 0x00000160, 0x00000020, 0x0004003d, 0x00000006, 0x000001ad, 0x000001ac, + 0x00050051, 0x00000006, 0x000001ae, 0x000001ab, 0x00000000, 0x00050051, 0x00000006, 0x000001af, + 0x000001ab, 0x00000001, 0x00050051, 0x00000006, 0x000001b0, 0x000001ab, 0x00000002, 0x00070050, + 0x0000001b, 0x000001b1, 0x000001ae, 0x000001af, 0x000001b0, 0x000001ad, 0x0003003e, 0x000001a7, + 0x000001b1, 0x000100fd, 0x00010038, 0x00050036, 0x00000006, 0x00000010, 0x00000000, 0x0000000c, + 0x00030037, 0x00000008, 0x0000000d, 0x00030037, 0x0000000a, 0x0000000e, 0x00030037, 0x0000000b, + 0x0000000f, 0x000200f8, 0x00000011, 0x0004003b, 0x0000001c, 0x0000001d, 0x00000007, 0x0004003b, + 0x00000008, 0x00000031, 0x00000007, 0x0004003b, 0x0000003b, 0x0000003c, 0x00000007, 0x0004003b, + 0x0000000b, 0x00000043, 0x00000007, 0x0004003b, 0x0000003b, 0x0000006c, 0x00000007, 0x0004003b, + 0x0000000b, 0x0000007a, 0x00000007, 0x0004003b, 0x0000000b, 0x0000007b, 0x00000007, 0x0004003b, + 0x0000000a, 0x0000007f, 0x00000007, 0x0004003b, 0x0000003b, 0x00000088, 0x00000007, 0x0004003b, + 0x0000009a, 0x0000009b, 0x00000007, 0x0004003d, 0x00000009, 0x00000026, 0x0000000e, 0x00060041, + 0x00000027, 0x00000028, 0x00000024, 0x00000025, 0x00000026, 0x0004003d, 0x0000001e, 0x00000029, + 0x00000028, 0x0004003d, 0x00000007, 0x0000002a, 0x0000000d, 0x00050051, 0x00000006, 0x0000002c, + 0x0000002a, 0x00000000, 0x00050051, 0x00000006, 0x0000002d, 0x0000002a, 0x00000001, 0x00050051, + 0x00000006, 0x0000002e, 0x0000002a, 0x00000002, 0x00070050, 0x0000001b, 0x0000002f, 0x0000002c, + 0x0000002d, 0x0000002e, 0x0000002b, 0x00050091, 0x0000001b, 0x00000030, 0x00000029, 0x0000002f, + 0x0003003e, 0x0000001d, 0x00000030, 0x0004003d, 0x0000001b, 0x00000032, 0x0000001d, 0x0008004f, + 0x00000007, 0x00000033, 0x00000032, 0x00000032, 0x00000000, 0x00000001, 0x00000002, 0x00050041, + 0x0000000b, 0x00000034, 0x0000001d, 0x00000020, 0x0004003d, 0x00000006, 0x00000035, 0x00000034, + 0x0007000c, 0x00000006, 0x00000037, 0x00000001, 0x00000028, 0x00000035, 0x00000036, 0x00060050, + 0x00000007, 0x00000038, 0x00000037, 0x00000037, 0x00000037, 0x00050088, 0x00000007, 0x00000039, + 0x00000033, 0x00000038, 0x0003003e, 0x00000031, 0x00000039, 0x0004003d, 0x00000007, 0x0000003d, + 0x00000031, 0x0007004f, 0x0000003a, 0x0000003e, 0x0000003d, 0x0000003d, 0x00000000, 0x00000001, + 0x0005008e, 0x0000003a, 0x00000040, 0x0000003e, 0x0000003f, 0x00050050, 0x0000003a, 0x00000041, + 0x0000003f, 0x0000003f, 0x00050081, 0x0000003a, 0x00000042, 0x00000040, 0x00000041, 0x0003003e, + 0x0000003c, 0x00000042, 0x00050041, 0x0000000b, 0x00000045, 0x00000031, 0x00000044, 0x0004003d, + 0x00000006, 0x00000046, 0x00000045, 0x0003003e, 0x00000043, 0x00000046, 0x00050041, 0x0000000b, + 0x00000049, 0x0000003c, 0x00000048, 0x0004003d, 0x00000006, 0x0000004a, 0x00000049, 0x000500b8, + 0x00000047, 0x0000004c, 0x0000004a, 0x0000004b, 0x000400a8, 0x00000047, 0x0000004d, 0x0000004c, + 0x000300f7, 0x0000004f, 0x00000000, 0x000400fa, 0x0000004d, 0x0000004e, 0x0000004f, 0x000200f8, + 0x0000004e, 0x00050041, 0x0000000b, 0x00000050, 0x0000003c, 0x00000048, 0x0004003d, 0x00000006, + 0x00000051, 0x00000050, 0x000500ba, 0x00000047, 0x00000052, 0x00000051, 0x0000002b, 0x000200f9, + 0x0000004f, 0x000200f8, 0x0000004f, 0x000700f5, 0x00000047, 0x00000053, 0x0000004c, 0x00000011, + 0x00000052, 0x0000004e, 0x000400a8, 0x00000047, 0x00000054, 0x00000053, 0x000300f7, 0x00000056, + 0x00000000, 0x000400fa, 0x00000054, 0x00000055, 0x00000056, 0x000200f8, 0x00000055, 0x00050041, + 0x0000000b, 0x00000058, 0x0000003c, 0x00000057, 0x0004003d, 0x00000006, 0x00000059, 0x00000058, + 0x000500b8, 0x00000047, 0x0000005a, 0x00000059, 0x0000004b, 0x000200f9, 0x00000056, 0x000200f8, + 0x00000056, 0x000700f5, 0x00000047, 0x0000005b, 0x00000053, 0x0000004f, 0x0000005a, 0x00000055, + 0x000400a8, 0x00000047, 0x0000005c, 0x0000005b, 0x000300f7, 0x0000005e, 0x00000000, 0x000400fa, + 0x0000005c, 0x0000005d, 0x0000005e, 0x000200f8, 0x0000005d, 0x00050041, 0x0000000b, 0x0000005f, + 0x0000003c, 0x00000057, 0x0004003d, 0x00000006, 0x00000060, 0x0000005f, 0x000500ba, 0x00000047, + 0x00000061, 0x00000060, 0x0000002b, 0x000200f9, 0x0000005e, 0x000200f8, 0x0000005e, 0x000700f5, + 0x00000047, 0x00000062, 0x0000005b, 0x00000056, 0x00000061, 0x0000005d, 0x0004003d, 0x00000006, + 0x00000063, 0x00000043, 0x000500b8, 0x00000047, 0x00000064, 0x00000063, 0x0000004b, 0x000500a6, + 0x00000047, 0x00000065, 0x00000062, 0x00000064, 0x0004003d, 0x00000006, 0x00000066, 0x00000043, + 0x000500ba, 0x00000047, 0x00000067, 0x00000066, 0x0000002b, 0x000500a6, 0x00000047, 0x00000068, + 0x00000065, 0x00000067, 0x000300f7, 0x0000006a, 0x00000000, 0x000400fa, 0x00000068, 0x00000069, + 0x0000006a, 0x000200f8, 0x00000069, 0x000200fe, 0x0000002b, 0x000200f8, 0x0000006a, 0x0004003d, + 0x0000006e, 0x00000071, 0x00000070, 0x00040064, 0x0000006d, 0x00000072, 0x00000071, 0x00050067, + 0x00000073, 0x00000074, 0x00000072, 0x00000025, 0x0007004f, 0x00000075, 0x00000076, 0x00000074, + 0x00000074, 0x00000000, 0x00000001, 0x0004006f, 0x0000003a, 0x00000077, 0x00000076, 0x00050050, + 0x0000003a, 0x00000078, 0x0000002b, 0x0000002b, 0x00050088, 0x0000003a, 0x00000079, 0x00000078, + 0x00000077, 0x0003003e, 0x0000006c, 0x00000079, 0x0003003e, 0x0000007a, 0x0000004b, 0x0004003d, + 0x00000006, 0x0000007c, 0x00000043, 0x0004003d, 0x00000006, 0x0000007d, 0x0000000f, 0x00050083, + 0x00000006, 0x0000007e, 0x0000007c, 0x0000007d, 0x0003003e, 0x0000007b, 0x0000007e, 0x0003003e, + 0x0000007f, 0x00000025, 0x000200f9, 0x00000080, 0x000200f8, 0x00000080, 0x000400f6, 0x00000082, + 0x00000083, 0x00000000, 0x000200f9, 0x00000084, 0x000200f8, 0x00000084, 0x0004003d, 0x00000009, + 0x00000085, 0x0000007f, 0x000500b1, 0x00000047, 0x00000087, 0x00000085, 0x00000086, 0x000400fa, + 0x00000087, 0x00000081, 0x00000082, 0x000200f8, 0x00000081, 0x0004003d, 0x0000003a, 0x00000089, + 0x0000003c, 0x0004003d, 0x00000009, 0x00000099, 0x0000007f, 0x0003003e, 0x0000009b, 0x00000098, + 0x00050041, 0x0000003b, 0x0000009c, 0x0000009b, 0x00000099, 0x0004003d, 0x0000003a, 0x0000009d, + 0x0000009c, 0x0004003d, 0x0000003a, 0x0000009e, 0x0000006c, 0x00050085, 0x0000003a, 0x0000009f, + 0x0000009d, 0x0000009e, 0x00050081, 0x0000003a, 0x000000a0, 0x00000089, 0x0000009f, 0x0003003e, + 0x00000088, 0x000000a0, 0x00050041, 0x0000000b, 0x000000a1, 0x00000088, 0x00000048, 0x0004003d, + 0x00000006, 0x000000a2, 0x000000a1, 0x000500b8, 0x00000047, 0x000000a3, 0x000000a2, 0x0000004b, + 0x000400a8, 0x00000047, 0x000000a4, 0x000000a3, 0x000300f7, 0x000000a6, 0x00000000, 0x000400fa, + 0x000000a4, 0x000000a5, 0x000000a6, 0x000200f8, 0x000000a5, 0x00050041, 0x0000000b, 0x000000a7, + 0x00000088, 0x00000048, 0x0004003d, 0x00000006, 0x000000a8, 0x000000a7, 0x000500ba, 0x00000047, + 0x000000a9, 0x000000a8, 0x0000002b, 0x000200f9, 0x000000a6, 0x000200f8, 0x000000a6, 0x000700f5, + 0x00000047, 0x000000aa, 0x000000a3, 0x00000081, 0x000000a9, 0x000000a5, 0x000400a8, 0x00000047, + 0x000000ab, 0x000000aa, 0x000300f7, 0x000000ad, 0x00000000, 0x000400fa, 0x000000ab, 0x000000ac, + 0x000000ad, 0x000200f8, 0x000000ac, 0x00050041, 0x0000000b, 0x000000ae, 0x00000088, 0x00000057, + 0x0004003d, 0x00000006, 0x000000af, 0x000000ae, 0x000500b8, 0x00000047, 0x000000b0, 0x000000af, + 0x0000004b, 0x000200f9, 0x000000ad, 0x000200f8, 0x000000ad, 0x000700f5, 0x00000047, 0x000000b1, + 0x000000aa, 0x000000a6, 0x000000b0, 0x000000ac, 0x000400a8, 0x00000047, 0x000000b2, 0x000000b1, + 0x000300f7, 0x000000b4, 0x00000000, 0x000400fa, 0x000000b2, 0x000000b3, 0x000000b4, 0x000200f8, + 0x000000b3, 0x00050041, 0x0000000b, 0x000000b5, 0x00000088, 0x00000057, 0x0004003d, 0x00000006, + 0x000000b6, 0x000000b5, 0x000500ba, 0x00000047, 0x000000b7, 0x000000b6, 0x0000002b, 0x000200f9, + 0x000000b4, 0x000200f8, 0x000000b4, 0x000700f5, 0x00000047, 0x000000b8, 0x000000b1, 0x000000ad, + 0x000000b7, 0x000000b3, 0x000300f7, 0x000000ba, 0x00000000, 0x000400fa, 0x000000b8, 0x000000b9, + 0x000000ba, 0x000200f8, 0x000000b9, 0x000200f9, 0x00000083, 0x000200f8, 0x000000ba, 0x0004003d, + 0x0000006e, 0x000000bc, 0x00000070, 0x0004003d, 0x0000003a, 0x000000bd, 0x00000088, 0x0004003d, + 0x00000009, 0x000000be, 0x0000000e, 0x0004006f, 0x00000006, 0x000000bf, 0x000000be, 0x0004003d, + 0x00000006, 0x000000c0, 0x0000007b, 0x00050051, 0x00000006, 0x000000c1, 0x000000bd, 0x00000000, + 0x00050051, 0x00000006, 0x000000c2, 0x000000bd, 0x00000001, 0x00070050, 0x0000001b, 0x000000c3, + 0x000000c1, 0x000000c2, 0x000000bf, 0x000000c0, 0x00050051, 0x00000006, 0x000000c4, 0x000000c3, + 0x00000003, 0x00060059, 0x00000006, 0x000000c5, 0x000000bc, 0x000000c3, 0x000000c4, 0x0004003d, + 0x00000006, 0x000000c6, 0x0000007a, 0x00050081, 0x00000006, 0x000000c7, 0x000000c6, 0x000000c5, + 0x0003003e, 0x0000007a, 0x000000c7, 0x000200f9, 0x00000083, 0x000200f8, 0x00000083, 0x0004003d, + 0x00000009, 0x000000c8, 0x0000007f, 0x00050080, 0x00000009, 0x000000ca, 0x000000c8, 0x000000c9, + 0x0003003e, 0x0000007f, 0x000000ca, 0x000200f9, 0x00000080, 0x000200f8, 0x00000082, 0x0004003d, + 0x00000006, 0x000000cb, 0x0000007a, 0x00050088, 0x00000006, 0x000000cd, 0x000000cb, 0x000000cc, + 0x000200fe, 0x000000cd, 0x00010038, 0x00050036, 0x00000006, 0x00000014, 0x00000000, 0x00000012, + 0x00030037, 0x0000000a, 0x00000013, 0x000200f8, 0x00000015, 0x0004003b, 0x0000000b, 0x000000d0, + 0x00000007, 0x0004003b, 0x0000000b, 0x000000d3, 0x00000007, 0x0004003b, 0x0000000b, 0x000000dd, + 0x00000007, 0x0004003b, 0x0000000b, 0x000000ea, 0x00000007, 0x0004003d, 0x00000009, 0x000000d1, + 0x00000013, 0x000500aa, 0x00000047, 0x000000d2, 0x000000d1, 0x00000025, 0x000300f7, 0x000000d5, + 0x00000000, 0x000400fa, 0x000000d2, 0x000000d4, 0x000000da, 0x000200f8, 0x000000d4, 0x00060041, + 0x000000d7, 0x000000d8, 0x00000024, 0x000000d6, 0x00000048, 0x0004003d, 0x00000006, 0x000000d9, + 0x000000d8, 0x0003003e, 0x000000d3, 0x000000d9, 0x000200f9, 0x000000d5, 0x000200f8, 0x000000da, + 0x0004003d, 0x00000009, 0x000000db, 0x00000013, 0x000500aa, 0x00000047, 0x000000dc, 0x000000db, + 0x000000c9, 0x000300f7, 0x000000df, 0x00000000, 0x000400fa, 0x000000dc, 0x000000de, 0x000000e2, + 0x000200f8, 0x000000de, 0x00060041, 0x000000d7, 0x000000e0, 0x00000024, 0x000000d6, 0x00000057, + 0x0004003d, 0x00000006, 0x000000e1, 0x000000e0, 0x0003003e, 0x000000dd, 0x000000e1, 0x000200f9, + 0x000000df, 0x000200f8, 0x000000e2, 0x00060041, 0x000000d7, 0x000000e3, 0x00000024, 0x000000d6, + 0x00000044, 0x0004003d, 0x00000006, 0x000000e4, 0x000000e3, 0x0003003e, 0x000000dd, 0x000000e4, + 0x000200f9, 0x000000df, 0x000200f8, 0x000000df, 0x0004003d, 0x00000006, 0x000000e5, 0x000000dd, + 0x0003003e, 0x000000d3, 0x000000e5, 0x000200f9, 0x000000d5, 0x000200f8, 0x000000d5, 0x0004003d, + 0x00000006, 0x000000e6, 0x000000d3, 0x0003003e, 0x000000d0, 0x000000e6, 0x0004003d, 0x00000006, + 0x000000e7, 0x000000d0, 0x000500ba, 0x00000047, 0x000000e9, 0x000000e7, 0x000000e8, 0x000300f7, + 0x000000ec, 0x00000000, 0x000400fa, 0x000000e9, 0x000000eb, 0x000000ee, 0x000200f8, 0x000000eb, + 0x0004003d, 0x00000006, 0x000000ed, 0x000000d0, 0x0003003e, 0x000000ea, 0x000000ed, 0x000200f9, + 0x000000ec, 0x000200f8, 0x000000ee, 0x00060041, 0x000000d7, 0x000000f0, 0x00000024, 0x000000ef, + 0x00000048, 0x0004003d, 0x00000006, 0x000000f1, 0x000000f0, 0x0003003e, 0x000000ea, 0x000000f1, + 0x000200f9, 0x000000ec, 0x000200f8, 0x000000ec, 0x0004003d, 0x00000006, 0x000000f2, 0x000000ea, + 0x000200fe, 0x000000f2, 0x00010038, 0x00050036, 0x00000006, 0x00000019, 0x00000000, 0x00000016, + 0x00030037, 0x00000008, 0x00000017, 0x00030037, 0x0000000b, 0x00000018, 0x000200f8, 0x0000001a, + 0x0004003b, 0x0000000a, 0x0000010a, 0x00000007, 0x0004003b, 0x0000000b, 0x00000118, 0x00000007, + 0x0004003b, 0x0000000a, 0x00000119, 0x00000007, 0x0004003b, 0x00000008, 0x0000011c, 0x00000007, + 0x0004003b, 0x0000000a, 0x0000011e, 0x00000007, 0x0004003b, 0x0000000b, 0x00000120, 0x00000007, + 0x00060041, 0x000000d7, 0x000000f5, 0x00000024, 0x000000ef, 0x00000057, 0x0004003d, 0x00000006, + 0x000000f6, 0x000000f5, 0x000500b8, 0x00000047, 0x000000f7, 0x000000f6, 0x0000003f, 0x000400a8, + 0x00000047, 0x000000f8, 0x000000f7, 0x000300f7, 0x000000fa, 0x00000000, 0x000400fa, 0x000000f8, + 0x000000f9, 0x000000fa, 0x000200f8, 0x000000f9, 0x00060041, 0x000000d7, 0x000000fb, 0x00000024, + 0x000000ef, 0x00000044, 0x0004003d, 0x00000006, 0x000000fc, 0x000000fb, 0x000500b8, 0x00000047, + 0x000000fd, 0x000000fc, 0x0000003f, 0x000200f9, 0x000000fa, 0x000200f8, 0x000000fa, 0x000700f5, + 0x00000047, 0x000000fe, 0x000000f7, 0x0000001a, 0x000000fd, 0x000000f9, 0x000400a8, 0x00000047, + 0x000000ff, 0x000000fe, 0x000300f7, 0x00000101, 0x00000000, 0x000400fa, 0x000000ff, 0x00000100, + 0x00000101, 0x000200f8, 0x00000100, 0x00060041, 0x000000d7, 0x00000102, 0x00000024, 0x000000c9, + 0x00000020, 0x0004003d, 0x00000006, 0x00000103, 0x00000102, 0x000500b8, 0x00000047, 0x00000105, + 0x00000103, 0x00000104, 0x000200f9, 0x00000101, 0x000200f8, 0x00000101, 0x000700f5, 0x00000047, + 0x00000106, 0x000000fe, 0x000000fa, 0x00000105, 0x00000100, 0x000300f7, 0x00000108, 0x00000000, + 0x000400fa, 0x00000106, 0x00000107, 0x00000108, 0x000200f8, 0x00000107, 0x000200fe, 0x0000002b, + 0x000200f8, 0x00000108, 0x0003003e, 0x0000010a, 0x000000ef, 0x0004003d, 0x00000006, 0x0000010b, + 0x00000018, 0x00060041, 0x000000d7, 0x0000010c, 0x00000024, 0x000000c9, 0x00000048, 0x0004003d, + 0x00000006, 0x0000010d, 0x0000010c, 0x000500b8, 0x00000047, 0x0000010e, 0x0000010b, 0x0000010d, + 0x000300f7, 0x00000110, 0x00000000, 0x000400fa, 0x0000010e, 0x0000010f, 0x00000111, 0x000200f8, + 0x0000010f, 0x0003003e, 0x0000010a, 0x00000025, 0x000200f9, 0x00000110, 0x000200f8, 0x00000111, + 0x0004003d, 0x00000006, 0x00000112, 0x00000018, 0x00060041, 0x000000d7, 0x00000113, 0x00000024, + 0x000000c9, 0x00000057, 0x0004003d, 0x00000006, 0x00000114, 0x00000113, 0x000500b8, 0x00000047, + 0x00000115, 0x00000112, 0x00000114, 0x000300f7, 0x00000117, 0x00000000, 0x000400fa, 0x00000115, + 0x00000116, 0x00000117, 0x000200f8, 0x00000116, 0x0003003e, 0x0000010a, 0x000000c9, 0x000200f9, + 0x00000117, 0x000200f8, 0x00000117, 0x000200f9, 0x00000110, 0x000200f8, 0x00000110, 0x0004003d, + 0x00000009, 0x0000011a, 0x0000010a, 0x0003003e, 0x00000119, 0x0000011a, 0x00050039, 0x00000006, + 0x0000011b, 0x00000014, 0x00000119, 0x0004003d, 0x00000007, 0x0000011d, 0x00000017, 0x0003003e, + 0x0000011c, 0x0000011d, 0x0004003d, 0x00000009, 0x0000011f, 0x0000010a, 0x0003003e, 0x0000011e, + 0x0000011f, 0x0003003e, 0x00000120, 0x0000011b, 0x00070039, 0x00000006, 0x00000121, 0x00000010, + 0x0000011c, 0x0000011e, 0x00000120, 0x0003003e, 0x00000118, 0x00000121, 0x0004003d, 0x00000006, + 0x00000122, 0x00000118, 0x00060041, 0x000000d7, 0x00000123, 0x00000024, 0x000000c9, 0x00000020, + 0x0004003d, 0x00000006, 0x00000124, 0x00000123, 0x0008000c, 0x00000006, 0x00000125, 0x00000001, + 0x0000002b, 0x00000124, 0x0000004b, 0x0000002b, 0x0008000c, 0x00000006, 0x00000126, 0x00000001, + 0x0000002e, 0x0000002b, 0x00000122, 0x00000125, 0x0003003e, 0x00000118, 0x00000126, 0x0004003d, + 0x00000006, 0x00000127, 0x00000118, 0x000200fe, 0x00000127, 0x00010038, +}; +static const size_t mesh3d_grass_frag_spv_count = sizeof(mesh3d_grass_frag_spv)/sizeof(mesh3d_grass_frag_spv[0]); diff --git a/src/modules/graphics/shaders/mesh3d_grass_vert_spv.inc b/src/modules/graphics/shaders/mesh3d_grass_vert_spv.inc new file mode 100644 index 00000000..278e63b9 --- /dev/null +++ b/src/modules/graphics/shaders/mesh3d_grass_vert_spv.inc @@ -0,0 +1,201 @@ +static const uint32_t mesh3d_grass_vert_spv[] = { + 0x07230203, 0x00010000, 0x0008000b, 0x000000df, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x000f000f, 0x00000000, 0x00000004, 0x6e69616d, 0x00000000, 0x0000001f, 0x00000065, 0x00000090, + 0x000000bb, 0x000000c7, 0x000000ca, 0x000000cd, 0x000000d4, 0x000000d8, 0x000000db, 0x00030003, + 0x00000002, 0x000001c2, 0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x00040005, 0x00000008, + 0x74646977, 0x00000068, 0x00050005, 0x0000000c, 0x65747845, 0x6c616e72, 0x00000073, 0x00050006, + 0x0000000c, 0x00000000, 0x61746164, 0x00000000, 0x00030005, 0x0000000e, 0x00000075, 0x00040005, + 0x00000017, 0x67696568, 0x00007468, 0x00040005, 0x0000001c, 0x6c616373, 0x00000065, 0x00050005, + 0x0000001f, 0x6f4e6e69, 0x6c616d72, 0x00000000, 0x00050005, 0x00000029, 0x4d766e69, 0x6c65646f, + 0x00000000, 0x00040005, 0x0000002a, 0x6867694c, 0x00443374, 0x00060006, 0x0000002a, 0x00000000, + 0x52736f70, 0x75696461, 0x00000073, 0x00050006, 0x0000002a, 0x00000001, 0x6f6c6f63, 0x00000072, + 0x00040005, 0x0000002d, 0x6d617246, 0x00000065, 0x00040006, 0x0000002d, 0x00000000, 0x0070766d, + 0x00050006, 0x0000002d, 0x00000001, 0x65646f6d, 0x0000006c, 0x00080006, 0x0000002d, 0x00000002, + 0x6867696c, 0x72694474, 0x65746e49, 0x7469736e, 0x00000079, 0x00060006, 0x0000002d, 0x00000003, + 0x6867696c, 0x6c6f4374, 0x0000726f, 0x00050006, 0x0000002d, 0x00000004, 0x746e6974, 0x00000000, + 0x00060006, 0x0000002d, 0x00000005, 0x656d6163, 0x6f506172, 0x00000073, 0x00050006, 0x0000002d, + 0x00000006, 0x69626d61, 0x00746e65, 0x00050006, 0x0000002d, 0x00000007, 0x6867696c, 0x00007374, + 0x00050006, 0x0000002d, 0x00000008, 0x42786574, 0x00626d6f, 0x00060006, 0x0000002d, 0x00000009, + 0x61726170, 0x78616c6c, 0x00000000, 0x00050006, 0x0000002d, 0x0000000a, 0x77656976, 0x00000000, + 0x00060006, 0x0000002d, 0x0000000b, 0x70696c63, 0x6f666e49, 0x00000000, 0x00030005, 0x0000002f, + 0x006f6275, 0x00050005, 0x00000036, 0x6c726f77, 0x4f705564, 0x00006a62, 0x00040005, 0x00000046, + 0x654c7075, 0x0000006e, 0x00040005, 0x00000056, 0x4f6d6163, 0x00006a62, 0x00040005, 0x00000063, + 0x61436f74, 0x0000006d, 0x00040005, 0x00000065, 0x6f506e69, 0x00000073, 0x00050005, 0x0000006f, + 0x61436f74, 0x6e654c6d, 0x00000000, 0x00040005, 0x00000077, 0x68676972, 0x00000074, 0x00050005, + 0x0000007d, 0x6c6c6166, 0x6b636162, 0x00000000, 0x00030005, 0x0000008b, 0x006a626f, 0x00040005, + 0x00000090, 0x56556e69, 0x00000000, 0x00040005, 0x000000a6, 0x6c726f77, 0x00000064, 0x00050005, + 0x000000af, 0x746f6f72, 0x6c726f57, 0x00000064, 0x00060005, 0x000000b9, 0x505f6c67, 0x65567265, + 0x78657472, 0x00000000, 0x00060006, 0x000000b9, 0x00000000, 0x505f6c67, 0x7469736f, 0x006e6f69, + 0x00070006, 0x000000b9, 0x00000001, 0x505f6c67, 0x746e696f, 0x657a6953, 0x00000000, 0x00070006, + 0x000000b9, 0x00000002, 0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006, 0x000000b9, + 0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, 0x00030005, 0x000000bb, 0x00000000, + 0x00030005, 0x000000c7, 0x00565576, 0x00050005, 0x000000ca, 0x726f5776, 0x6f50646c, 0x00000073, + 0x00050005, 0x000000cd, 0x65695676, 0x736f5077, 0x00000000, 0x00050005, 0x000000d4, 0x6f6f5276, + 0x736f5074, 0x00000000, 0x00050005, 0x000000d8, 0x736e4976, 0x636e6174, 0x00644965, 0x00040005, + 0x000000db, 0x6e695476, 0x00000074, 0x00040047, 0x0000000b, 0x00000006, 0x00000004, 0x00030047, + 0x0000000c, 0x00000002, 0x00050048, 0x0000000c, 0x00000000, 0x00000023, 0x00000000, 0x00040047, + 0x0000001f, 0x0000001e, 0x00000001, 0x00050048, 0x0000002a, 0x00000000, 0x00000023, 0x00000000, + 0x00050048, 0x0000002a, 0x00000001, 0x00000023, 0x00000010, 0x00040047, 0x0000002c, 0x00000006, + 0x00000020, 0x00030047, 0x0000002d, 0x00000002, 0x00040048, 0x0000002d, 0x00000000, 0x00000005, + 0x00050048, 0x0000002d, 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x0000002d, 0x00000000, + 0x00000023, 0x00000000, 0x00040048, 0x0000002d, 0x00000001, 0x00000005, 0x00050048, 0x0000002d, + 0x00000001, 0x00000007, 0x00000010, 0x00050048, 0x0000002d, 0x00000001, 0x00000023, 0x00000040, + 0x00050048, 0x0000002d, 0x00000002, 0x00000023, 0x00000080, 0x00050048, 0x0000002d, 0x00000003, + 0x00000023, 0x00000090, 0x00050048, 0x0000002d, 0x00000004, 0x00000023, 0x000000a0, 0x00050048, + 0x0000002d, 0x00000005, 0x00000023, 0x000000b0, 0x00050048, 0x0000002d, 0x00000006, 0x00000023, + 0x000000c0, 0x00050048, 0x0000002d, 0x00000007, 0x00000023, 0x000000d0, 0x00050048, 0x0000002d, + 0x00000008, 0x00000023, 0x000001d0, 0x00050048, 0x0000002d, 0x00000009, 0x00000023, 0x000001e0, + 0x00040048, 0x0000002d, 0x0000000a, 0x00000005, 0x00050048, 0x0000002d, 0x0000000a, 0x00000007, + 0x00000010, 0x00050048, 0x0000002d, 0x0000000a, 0x00000023, 0x000001f0, 0x00050048, 0x0000002d, + 0x0000000b, 0x00000023, 0x00000230, 0x00040047, 0x0000002f, 0x00000021, 0x00000000, 0x00040047, + 0x0000002f, 0x00000022, 0x00000000, 0x00040047, 0x00000065, 0x0000001e, 0x00000000, 0x00040047, + 0x00000090, 0x0000001e, 0x00000002, 0x00030047, 0x000000b9, 0x00000002, 0x00050048, 0x000000b9, + 0x00000000, 0x0000000b, 0x00000000, 0x00050048, 0x000000b9, 0x00000001, 0x0000000b, 0x00000001, + 0x00050048, 0x000000b9, 0x00000002, 0x0000000b, 0x00000003, 0x00050048, 0x000000b9, 0x00000003, + 0x0000000b, 0x00000004, 0x00040047, 0x000000c7, 0x0000001e, 0x00000000, 0x00040047, 0x000000ca, + 0x0000001e, 0x00000001, 0x00040047, 0x000000cd, 0x0000001e, 0x00000002, 0x00040047, 0x000000d4, + 0x0000001e, 0x00000003, 0x00040047, 0x000000d8, 0x0000001e, 0x00000004, 0x00040047, 0x000000db, + 0x0000001e, 0x00000005, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, + 0x00000006, 0x00000020, 0x00040020, 0x00000007, 0x00000007, 0x00000006, 0x00040015, 0x00000009, + 0x00000020, 0x00000000, 0x0004002b, 0x00000009, 0x0000000a, 0x00000020, 0x0004001c, 0x0000000b, + 0x00000006, 0x0000000a, 0x0003001e, 0x0000000c, 0x0000000b, 0x00040020, 0x0000000d, 0x00000009, + 0x0000000c, 0x0004003b, 0x0000000d, 0x0000000e, 0x00000009, 0x00040015, 0x0000000f, 0x00000020, + 0x00000001, 0x0004002b, 0x0000000f, 0x00000010, 0x00000000, 0x0004002b, 0x0000000f, 0x00000011, + 0x00000002, 0x00040020, 0x00000012, 0x00000009, 0x00000006, 0x0004002b, 0x00000006, 0x00000015, + 0x3c23d70a, 0x0004002b, 0x0000000f, 0x00000018, 0x00000003, 0x00040017, 0x0000001d, 0x00000006, + 0x00000003, 0x00040020, 0x0000001e, 0x00000001, 0x0000001d, 0x0004003b, 0x0000001e, 0x0000001f, + 0x00000001, 0x0004002b, 0x00000009, 0x00000020, 0x00000001, 0x00040020, 0x00000021, 0x00000001, + 0x00000006, 0x0004002b, 0x00000006, 0x00000024, 0x3d4ccccd, 0x00040017, 0x00000026, 0x00000006, + 0x00000004, 0x00040018, 0x00000027, 0x00000026, 0x00000004, 0x00040020, 0x00000028, 0x00000007, + 0x00000027, 0x0004001e, 0x0000002a, 0x00000026, 0x00000026, 0x0004002b, 0x00000009, 0x0000002b, + 0x00000008, 0x0004001c, 0x0000002c, 0x0000002a, 0x0000002b, 0x000e001e, 0x0000002d, 0x00000027, + 0x00000027, 0x00000026, 0x00000026, 0x00000026, 0x00000026, 0x00000026, 0x0000002c, 0x00000026, + 0x00000026, 0x00000027, 0x00000026, 0x00040020, 0x0000002e, 0x00000002, 0x0000002d, 0x0004003b, + 0x0000002e, 0x0000002f, 0x00000002, 0x0004002b, 0x0000000f, 0x00000030, 0x00000001, 0x00040020, + 0x00000031, 0x00000002, 0x00000027, 0x00040020, 0x00000035, 0x00000007, 0x0000001d, 0x00040018, + 0x00000039, 0x0000001d, 0x00000003, 0x0004002b, 0x00000006, 0x00000042, 0x00000000, 0x0004002b, + 0x00000006, 0x00000043, 0x3f800000, 0x0006002c, 0x0000001d, 0x00000044, 0x00000042, 0x00000043, + 0x00000042, 0x0004002b, 0x00000006, 0x0000004a, 0x3727c5ac, 0x00020014, 0x0000004b, 0x0004002b, + 0x0000000f, 0x00000058, 0x00000005, 0x00040020, 0x00000059, 0x00000002, 0x00000026, 0x0004003b, + 0x0000001e, 0x00000065, 0x00000001, 0x0004002b, 0x00000006, 0x00000073, 0x38d1b717, 0x0004002b, + 0x00000006, 0x00000081, 0x3f733333, 0x0006002c, 0x0000001d, 0x00000083, 0x00000043, 0x00000042, + 0x00000042, 0x00040017, 0x00000084, 0x0000004b, 0x00000003, 0x00040017, 0x0000008e, 0x00000006, + 0x00000002, 0x00040020, 0x0000008f, 0x00000001, 0x0000008e, 0x0004003b, 0x0000008f, 0x00000090, + 0x00000001, 0x0004002b, 0x00000009, 0x00000091, 0x00000000, 0x0004002b, 0x00000006, 0x00000094, + 0x3f000000, 0x00040020, 0x000000a5, 0x00000007, 0x00000026, 0x0004001c, 0x000000b8, 0x00000006, + 0x00000020, 0x0006001e, 0x000000b9, 0x00000026, 0x00000006, 0x000000b8, 0x000000b8, 0x00040020, + 0x000000ba, 0x00000003, 0x000000b9, 0x0004003b, 0x000000ba, 0x000000bb, 0x00000003, 0x00040020, + 0x000000c4, 0x00000003, 0x00000026, 0x00040020, 0x000000c6, 0x00000003, 0x0000008e, 0x0004003b, + 0x000000c6, 0x000000c7, 0x00000003, 0x00040020, 0x000000c9, 0x00000003, 0x0000001d, 0x0004003b, + 0x000000c9, 0x000000ca, 0x00000003, 0x0004003b, 0x000000c9, 0x000000cd, 0x00000003, 0x0004002b, + 0x0000000f, 0x000000ce, 0x0000000a, 0x0004003b, 0x000000c9, 0x000000d4, 0x00000003, 0x00040020, + 0x000000d7, 0x00000003, 0x00000006, 0x0004003b, 0x000000d7, 0x000000d8, 0x00000003, 0x0004003b, + 0x000000c4, 0x000000db, 0x00000003, 0x0004002b, 0x0000000f, 0x000000dc, 0x00000004, 0x00050036, + 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003b, 0x00000007, + 0x00000008, 0x00000007, 0x0004003b, 0x00000007, 0x00000017, 0x00000007, 0x0004003b, 0x00000007, + 0x0000001c, 0x00000007, 0x0004003b, 0x00000028, 0x00000029, 0x00000007, 0x0004003b, 0x00000035, + 0x00000036, 0x00000007, 0x0004003b, 0x00000007, 0x00000046, 0x00000007, 0x0004003b, 0x00000035, + 0x0000004d, 0x00000007, 0x0004003b, 0x00000035, 0x00000056, 0x00000007, 0x0004003b, 0x00000035, + 0x00000063, 0x00000007, 0x0004003b, 0x00000007, 0x0000006f, 0x00000007, 0x0004003b, 0x00000035, + 0x00000077, 0x00000007, 0x0004003b, 0x00000035, 0x0000007d, 0x00000007, 0x0004003b, 0x00000035, + 0x0000008b, 0x00000007, 0x0004003b, 0x000000a5, 0x000000a6, 0x00000007, 0x0004003b, 0x000000a5, + 0x000000af, 0x00000007, 0x00060041, 0x00000012, 0x00000013, 0x0000000e, 0x00000010, 0x00000011, + 0x0004003d, 0x00000006, 0x00000014, 0x00000013, 0x0007000c, 0x00000006, 0x00000016, 0x00000001, + 0x00000028, 0x00000014, 0x00000015, 0x0003003e, 0x00000008, 0x00000016, 0x00060041, 0x00000012, + 0x00000019, 0x0000000e, 0x00000010, 0x00000018, 0x0004003d, 0x00000006, 0x0000001a, 0x00000019, + 0x0007000c, 0x00000006, 0x0000001b, 0x00000001, 0x00000028, 0x0000001a, 0x00000015, 0x0003003e, + 0x00000017, 0x0000001b, 0x00050041, 0x00000021, 0x00000022, 0x0000001f, 0x00000020, 0x0004003d, + 0x00000006, 0x00000023, 0x00000022, 0x0007000c, 0x00000006, 0x00000025, 0x00000001, 0x00000028, + 0x00000023, 0x00000024, 0x0003003e, 0x0000001c, 0x00000025, 0x00050041, 0x00000031, 0x00000032, + 0x0000002f, 0x00000030, 0x0004003d, 0x00000027, 0x00000033, 0x00000032, 0x0006000c, 0x00000027, + 0x00000034, 0x00000001, 0x00000022, 0x00000033, 0x0003003e, 0x00000029, 0x00000034, 0x00050041, + 0x00000031, 0x00000037, 0x0000002f, 0x00000030, 0x0004003d, 0x00000027, 0x00000038, 0x00000037, + 0x00050051, 0x00000026, 0x0000003a, 0x00000038, 0x00000000, 0x0008004f, 0x0000001d, 0x0000003b, + 0x0000003a, 0x0000003a, 0x00000000, 0x00000001, 0x00000002, 0x00050051, 0x00000026, 0x0000003c, + 0x00000038, 0x00000001, 0x0008004f, 0x0000001d, 0x0000003d, 0x0000003c, 0x0000003c, 0x00000000, + 0x00000001, 0x00000002, 0x00050051, 0x00000026, 0x0000003e, 0x00000038, 0x00000002, 0x0008004f, + 0x0000001d, 0x0000003f, 0x0000003e, 0x0000003e, 0x00000000, 0x00000001, 0x00000002, 0x00060050, + 0x00000039, 0x00000040, 0x0000003b, 0x0000003d, 0x0000003f, 0x0006000c, 0x00000039, 0x00000041, + 0x00000001, 0x00000022, 0x00000040, 0x00050091, 0x0000001d, 0x00000045, 0x00000041, 0x00000044, + 0x0003003e, 0x00000036, 0x00000045, 0x0004003d, 0x0000001d, 0x00000047, 0x00000036, 0x0006000c, + 0x00000006, 0x00000048, 0x00000001, 0x00000042, 0x00000047, 0x0003003e, 0x00000046, 0x00000048, + 0x0004003d, 0x00000006, 0x00000049, 0x00000046, 0x000500ba, 0x0000004b, 0x0000004c, 0x00000049, + 0x0000004a, 0x000300f7, 0x0000004f, 0x00000000, 0x000400fa, 0x0000004c, 0x0000004e, 0x00000054, + 0x000200f8, 0x0000004e, 0x0004003d, 0x0000001d, 0x00000050, 0x00000036, 0x0004003d, 0x00000006, + 0x00000051, 0x00000046, 0x00060050, 0x0000001d, 0x00000052, 0x00000051, 0x00000051, 0x00000051, + 0x00050088, 0x0000001d, 0x00000053, 0x00000050, 0x00000052, 0x0003003e, 0x0000004d, 0x00000053, + 0x000200f9, 0x0000004f, 0x000200f8, 0x00000054, 0x0003003e, 0x0000004d, 0x00000044, 0x000200f9, + 0x0000004f, 0x000200f8, 0x0000004f, 0x0004003d, 0x0000001d, 0x00000055, 0x0000004d, 0x0003003e, + 0x00000036, 0x00000055, 0x0004003d, 0x00000027, 0x00000057, 0x00000029, 0x00050041, 0x00000059, + 0x0000005a, 0x0000002f, 0x00000058, 0x0004003d, 0x00000026, 0x0000005b, 0x0000005a, 0x0008004f, + 0x0000001d, 0x0000005c, 0x0000005b, 0x0000005b, 0x00000000, 0x00000001, 0x00000002, 0x00050051, + 0x00000006, 0x0000005d, 0x0000005c, 0x00000000, 0x00050051, 0x00000006, 0x0000005e, 0x0000005c, + 0x00000001, 0x00050051, 0x00000006, 0x0000005f, 0x0000005c, 0x00000002, 0x00070050, 0x00000026, + 0x00000060, 0x0000005d, 0x0000005e, 0x0000005f, 0x00000043, 0x00050091, 0x00000026, 0x00000061, + 0x00000057, 0x00000060, 0x0008004f, 0x0000001d, 0x00000062, 0x00000061, 0x00000061, 0x00000000, + 0x00000001, 0x00000002, 0x0003003e, 0x00000056, 0x00000062, 0x0004003d, 0x0000001d, 0x00000064, + 0x00000056, 0x0004003d, 0x0000001d, 0x00000066, 0x00000065, 0x00050083, 0x0000001d, 0x00000067, + 0x00000064, 0x00000066, 0x0003003e, 0x00000063, 0x00000067, 0x0004003d, 0x0000001d, 0x00000068, + 0x00000063, 0x0004003d, 0x0000001d, 0x00000069, 0x00000036, 0x0004003d, 0x0000001d, 0x0000006a, + 0x00000063, 0x0004003d, 0x0000001d, 0x0000006b, 0x00000036, 0x00050094, 0x00000006, 0x0000006c, + 0x0000006a, 0x0000006b, 0x0005008e, 0x0000001d, 0x0000006d, 0x00000069, 0x0000006c, 0x00050083, + 0x0000001d, 0x0000006e, 0x00000068, 0x0000006d, 0x0003003e, 0x00000063, 0x0000006e, 0x0004003d, + 0x0000001d, 0x00000070, 0x00000063, 0x0006000c, 0x00000006, 0x00000071, 0x00000001, 0x00000042, + 0x00000070, 0x0003003e, 0x0000006f, 0x00000071, 0x0004003d, 0x00000006, 0x00000072, 0x0000006f, + 0x000500ba, 0x0000004b, 0x00000074, 0x00000072, 0x00000073, 0x000300f7, 0x00000076, 0x00000000, + 0x000400fa, 0x00000074, 0x00000075, 0x0000007c, 0x000200f8, 0x00000075, 0x0004003d, 0x0000001d, + 0x00000078, 0x00000036, 0x0004003d, 0x0000001d, 0x00000079, 0x00000063, 0x0007000c, 0x0000001d, + 0x0000007a, 0x00000001, 0x00000044, 0x00000078, 0x00000079, 0x0006000c, 0x0000001d, 0x0000007b, + 0x00000001, 0x00000045, 0x0000007a, 0x0003003e, 0x00000077, 0x0000007b, 0x000200f9, 0x00000076, + 0x000200f8, 0x0000007c, 0x00050041, 0x00000007, 0x0000007e, 0x00000036, 0x00000020, 0x0004003d, + 0x00000006, 0x0000007f, 0x0000007e, 0x0006000c, 0x00000006, 0x00000080, 0x00000001, 0x00000004, + 0x0000007f, 0x000500b8, 0x0000004b, 0x00000082, 0x00000080, 0x00000081, 0x00060050, 0x00000084, + 0x00000085, 0x00000082, 0x00000082, 0x00000082, 0x000600a9, 0x0000001d, 0x00000086, 0x00000085, + 0x00000044, 0x00000083, 0x0003003e, 0x0000007d, 0x00000086, 0x0004003d, 0x0000001d, 0x00000087, + 0x00000036, 0x0004003d, 0x0000001d, 0x00000088, 0x0000007d, 0x0007000c, 0x0000001d, 0x00000089, + 0x00000001, 0x00000044, 0x00000087, 0x00000088, 0x0006000c, 0x0000001d, 0x0000008a, 0x00000001, + 0x00000045, 0x00000089, 0x0003003e, 0x00000077, 0x0000008a, 0x000200f9, 0x00000076, 0x000200f8, + 0x00000076, 0x0004003d, 0x0000001d, 0x0000008c, 0x00000065, 0x0004003d, 0x0000001d, 0x0000008d, + 0x00000077, 0x00050041, 0x00000021, 0x00000092, 0x00000090, 0x00000091, 0x0004003d, 0x00000006, + 0x00000093, 0x00000092, 0x00050083, 0x00000006, 0x00000095, 0x00000093, 0x00000094, 0x0005008e, + 0x0000001d, 0x00000096, 0x0000008d, 0x00000095, 0x0004003d, 0x00000006, 0x00000097, 0x00000008, + 0x0005008e, 0x0000001d, 0x00000098, 0x00000096, 0x00000097, 0x0004003d, 0x00000006, 0x00000099, + 0x0000001c, 0x0005008e, 0x0000001d, 0x0000009a, 0x00000098, 0x00000099, 0x00050081, 0x0000001d, + 0x0000009b, 0x0000008c, 0x0000009a, 0x0004003d, 0x0000001d, 0x0000009c, 0x00000036, 0x00050041, + 0x00000021, 0x0000009d, 0x00000090, 0x00000020, 0x0004003d, 0x00000006, 0x0000009e, 0x0000009d, + 0x0005008e, 0x0000001d, 0x0000009f, 0x0000009c, 0x0000009e, 0x0004003d, 0x00000006, 0x000000a0, + 0x00000017, 0x0005008e, 0x0000001d, 0x000000a1, 0x0000009f, 0x000000a0, 0x0004003d, 0x00000006, + 0x000000a2, 0x0000001c, 0x0005008e, 0x0000001d, 0x000000a3, 0x000000a1, 0x000000a2, 0x00050081, + 0x0000001d, 0x000000a4, 0x0000009b, 0x000000a3, 0x0003003e, 0x0000008b, 0x000000a4, 0x00050041, + 0x00000031, 0x000000a7, 0x0000002f, 0x00000030, 0x0004003d, 0x00000027, 0x000000a8, 0x000000a7, + 0x0004003d, 0x0000001d, 0x000000a9, 0x0000008b, 0x00050051, 0x00000006, 0x000000aa, 0x000000a9, + 0x00000000, 0x00050051, 0x00000006, 0x000000ab, 0x000000a9, 0x00000001, 0x00050051, 0x00000006, + 0x000000ac, 0x000000a9, 0x00000002, 0x00070050, 0x00000026, 0x000000ad, 0x000000aa, 0x000000ab, + 0x000000ac, 0x00000043, 0x00050091, 0x00000026, 0x000000ae, 0x000000a8, 0x000000ad, 0x0003003e, + 0x000000a6, 0x000000ae, 0x00050041, 0x00000031, 0x000000b0, 0x0000002f, 0x00000030, 0x0004003d, + 0x00000027, 0x000000b1, 0x000000b0, 0x0004003d, 0x0000001d, 0x000000b2, 0x00000065, 0x00050051, + 0x00000006, 0x000000b3, 0x000000b2, 0x00000000, 0x00050051, 0x00000006, 0x000000b4, 0x000000b2, + 0x00000001, 0x00050051, 0x00000006, 0x000000b5, 0x000000b2, 0x00000002, 0x00070050, 0x00000026, + 0x000000b6, 0x000000b3, 0x000000b4, 0x000000b5, 0x00000043, 0x00050091, 0x00000026, 0x000000b7, + 0x000000b1, 0x000000b6, 0x0003003e, 0x000000af, 0x000000b7, 0x00050041, 0x00000031, 0x000000bc, + 0x0000002f, 0x00000010, 0x0004003d, 0x00000027, 0x000000bd, 0x000000bc, 0x0004003d, 0x0000001d, + 0x000000be, 0x0000008b, 0x00050051, 0x00000006, 0x000000bf, 0x000000be, 0x00000000, 0x00050051, + 0x00000006, 0x000000c0, 0x000000be, 0x00000001, 0x00050051, 0x00000006, 0x000000c1, 0x000000be, + 0x00000002, 0x00070050, 0x00000026, 0x000000c2, 0x000000bf, 0x000000c0, 0x000000c1, 0x00000043, + 0x00050091, 0x00000026, 0x000000c3, 0x000000bd, 0x000000c2, 0x00050041, 0x000000c4, 0x000000c5, + 0x000000bb, 0x00000010, 0x0003003e, 0x000000c5, 0x000000c3, 0x0004003d, 0x0000008e, 0x000000c8, + 0x00000090, 0x0003003e, 0x000000c7, 0x000000c8, 0x0004003d, 0x00000026, 0x000000cb, 0x000000a6, + 0x0008004f, 0x0000001d, 0x000000cc, 0x000000cb, 0x000000cb, 0x00000000, 0x00000001, 0x00000002, + 0x0003003e, 0x000000ca, 0x000000cc, 0x00050041, 0x00000031, 0x000000cf, 0x0000002f, 0x000000ce, + 0x0004003d, 0x00000027, 0x000000d0, 0x000000cf, 0x0004003d, 0x00000026, 0x000000d1, 0x000000a6, + 0x00050091, 0x00000026, 0x000000d2, 0x000000d0, 0x000000d1, 0x0008004f, 0x0000001d, 0x000000d3, + 0x000000d2, 0x000000d2, 0x00000000, 0x00000001, 0x00000002, 0x0003003e, 0x000000cd, 0x000000d3, + 0x0004003d, 0x00000026, 0x000000d5, 0x000000af, 0x0008004f, 0x0000001d, 0x000000d6, 0x000000d5, + 0x000000d5, 0x00000000, 0x00000001, 0x00000002, 0x0003003e, 0x000000d4, 0x000000d6, 0x00050041, + 0x00000021, 0x000000d9, 0x0000001f, 0x00000091, 0x0004003d, 0x00000006, 0x000000da, 0x000000d9, + 0x0003003e, 0x000000d8, 0x000000da, 0x00050041, 0x00000059, 0x000000dd, 0x0000002f, 0x000000dc, + 0x0004003d, 0x00000026, 0x000000de, 0x000000dd, 0x0003003e, 0x000000db, 0x000000de, 0x000100fd, + 0x00010038, +}; +static const size_t mesh3d_grass_vert_spv_count = sizeof(mesh3d_grass_vert_spv)/sizeof(mesh3d_grass_vert_spv[0]); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index cf752f58..ba4d1afd 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -67,6 +67,7 @@ file(GLOB all_test_cpp softbody.cpp hotreload.cpp shader.cpp + grass.cpp simplesquirrel.cpp ScriptECS.cpp sound.cpp diff --git a/test/grass.cpp b/test/grass.cpp new file mode 100644 index 00000000..25aa2390 --- /dev/null +++ b/test/grass.cpp @@ -0,0 +1,223 @@ +#include "zeroerr/assert.h" +#include "zeroerr/unittest.h" + +#include "graphics/Grass.h" +#include "graphics/Graphics.h" +#include "graphics/Shader.h" +#include "graphics/shaders/mesh3d_grass_frag_spv.inc" +#include "graphics/shaders/mesh3d_grass_vert_spv.inc" + +#include +#include +#include + +using eve::graphics::GrassField; +using eve::graphics::Shader; +using eve::graphics::grass::BillboardMesh; +using eve::graphics::grass::Point; +using eve::graphics::grass::SampleParams; + +namespace { + +void makeTestPlane(std::vector &pos, std::vector &nrm, std::vector &idx) { + eve::graphics::grass::makePlane(4.f, 4.f, 4, 4, pos, nrm, idx); +} + +float minPairDist(const std::vector &pts) { + float best = 1e9f; + for (size_t i = 0; i < pts.size(); ++i) { + for (size_t j = i + 1; j < pts.size(); ++j) { + const glm::vec3 d = pts[i].position - pts[j].position; + best = std::min(best, glm::length(d)); + } + } + return best; +} + +} // namespace + +TEST_CASE("graphics.Grass.bindDefaults") { + Shader sh; + sh.setKind(Shader::Kind::eMesh3D); + eve::graphics::grass::bindDefaults(&sh); + CHECK(sh.hasUniform("time")); + CHECK(sh.hasUniform("frameDuration")); + CHECK(sh.hasUniform("lightGreen")); + CHECK(sh.hasUniform("darkGreen")); + CHECK(sh.hasUniform("alwaysDark")); + CHECK_EQ(sh.getUniformIndex("time"), 0); + CHECK_EQ(sh.getUniformIndex("alwaysDark"), 5); + CHECK_EQ(sh.getUniformIndex("lightGreen"), 6); + CHECK_EQ(sh.getUniformIndex("darkGreen"), 9); + CHECK_EQ(sh.getUniformIndex("frameCount"), 12); + CHECK_EQ(sh.usedFloats(), 13); +} + +TEST_CASE("graphics.Grass.paramNames") { + CHECK_EQ(eve::graphics::grass::paramCount(), 13); + CHECK_EQ(eve::graphics::grass::paramName(0), std::string("time")); + CHECK_EQ(eve::graphics::grass::paramName(5), std::string("alwaysDark")); + CHECK_EQ(eve::graphics::grass::paramName(12), std::string("frameCount")); +} + +TEST_CASE("graphics.Grass.spvMagic") { + CHECK(mesh3d_grass_vert_spv_count > 0); + CHECK(mesh3d_grass_frag_spv_count > 0); + CHECK_EQ(mesh3d_grass_vert_spv[0], 0x07230203u); + CHECK_EQ(mesh3d_grass_frag_spv[0], 0x07230203u); +} + +TEST_CASE("graphics.Grass.poissonMinDistance") { + std::vector pos, nrm; + std::vector idx; + makeTestPlane(pos, nrm, idx); + + SampleParams p; + p.radius = 0.45f; + p.maxPoints = 200; + p.seed = 7; + p.minSlopeDot = 0.1f; + const auto pts = + eve::graphics::grass::samplePoisson(pos.data(), nrm.data(), int(pos.size() / 3), idx.data(), + int(idx.size()), p); + CHECK(pts.size() >= 8); + CHECK(int(pts.size()) <= p.maxPoints); + const float minD = minPairDist(pts); + CHECK(minD + 1e-4f >= p.radius); + + for (const auto &pt : pts) { + CHECK(std::abs(pt.position.y) < 1e-4f); + CHECK(pt.position.x >= -2.01f); + CHECK(pt.position.x <= 2.01f); + CHECK(pt.position.z >= -2.01f); + CHECK(pt.position.z <= 2.01f); + } +} + +TEST_CASE("graphics.Grass.haltonCountAndCoverage") { + std::vector pos, nrm; + std::vector idx; + makeTestPlane(pos, nrm, idx); + + const auto pts = eve::graphics::grass::sampleHalton( + pos.data(), nrm.data(), int(pos.size() / 3), idx.data(), int(idx.size()), 64, 3, 0.1f); + CHECK_EQ(int(pts.size()), 64); + CHECK_EQ(pts.front().id, 0u); + CHECK_EQ(pts.back().id, 63u); + + float minX = 1e9f, maxX = -1e9f, minZ = 1e9f, maxZ = -1e9f; + for (const auto &pt : pts) { + minX = std::min(minX, pt.position.x); + maxX = std::max(maxX, pt.position.x); + minZ = std::min(minZ, pt.position.z); + maxZ = std::max(maxZ, pt.position.z); + } + CHECK(maxX - minX > 2.f); + CHECK(maxZ - minZ > 2.f); +} + +TEST_CASE("graphics.Grass.sparseIsSparser") { + std::vector pos, nrm; + std::vector idx; + makeTestPlane(pos, nrm, idx); + + SampleParams dense; + dense.radius = 0.3f; + dense.maxPoints = 400; + dense.seed = 1; + SampleParams sparse = dense; + sparse.radius = 1.2f; + sparse.maxPoints = 80; + sparse.seed = 99; + + const auto d = eve::graphics::grass::samplePoisson( + pos.data(), nrm.data(), int(pos.size() / 3), idx.data(), int(idx.size()), dense); + const auto s = eve::graphics::grass::samplePoisson( + pos.data(), nrm.data(), int(pos.size() / 3), idx.data(), int(idx.size()), sparse); + CHECK(d.size() > s.size()); + CHECK(s.size() >= 1); +} + +TEST_CASE("graphics.Grass.billboardRootPivot") { + std::vector pts(1); + pts[0].position = glm::vec3(3.f, 1.5f, -2.f); + pts[0].id = 11; + pts[0].scale = 1.25f; + const BillboardMesh mesh = eve::graphics::grass::buildBillboards(pts, 0.4f, 0.8f); + CHECK_EQ(int(mesh.posXYZ.size()), 12); + CHECK_EQ(int(mesh.uvST.size()), 8); + CHECK_EQ(int(mesh.indices.size()), 6); + for (int i = 0; i < 4; ++i) { + CHECK(std::abs(mesh.posXYZ[size_t(i * 3 + 0)] - 3.f) < 1e-5f); + CHECK(std::abs(mesh.posXYZ[size_t(i * 3 + 1)] - 1.5f) < 1e-5f); + CHECK(std::abs(mesh.posXYZ[size_t(i * 3 + 2)] + 2.f) < 1e-5f); + CHECK(std::abs(mesh.nrmXYZ[size_t(i * 3 + 0)] - 11.f) < 1e-5f); + CHECK(std::abs(mesh.nrmXYZ[size_t(i * 3 + 1)] - 1.25f) < 1e-5f); + } + // Bottom edge UVs sit at v=0; their midpoint is the root (u=0.5). + CHECK(std::abs(mesh.uvST[1]) < 1e-5f); + CHECK(std::abs(mesh.uvST[3]) < 1e-5f); + CHECK(std::abs(0.5f * (mesh.uvST[0] + mesh.uvST[2]) - 0.5f) < 1e-5f); +} + +TEST_CASE("graphics.Grass.swayDesyncByInstanceId") { + const int a = eve::graphics::grass::swayFrame(0.f, 0.12f, 0, 4); + const int b = eve::graphics::grass::swayFrame(0.f, 0.12f, 1, 4); + const int c = eve::graphics::grass::swayFrame(0.f, 0.12f, 2, 4); + CHECK(a >= 0); + CHECK(a < 4); + CHECK(b >= 0); + CHECK(b < 4); + CHECK(c >= 0); + CHECK(c < 4); + CHECK((a != b || b != c)); + + const int later = eve::graphics::grass::swayFrame(0.48f, 0.12f, 0, 4); + CHECK(later != a); +} + +TEST_CASE("graphics.Grass.swayAtlasFourFrames") { + std::vector rgba; + eve::graphics::grass::makeSwayAtlasRGBA(32, 32, 4, rgba); + CHECK_EQ(eve::graphics::grass::swayAtlasWidth(32, 4), 128); + CHECK_EQ(eve::graphics::grass::swayAtlasHeight(32), 32); + CHECK_EQ(int(rgba.size()), 128 * 32 * 4); + + int opaque = 0; + int perFrame[4] = {}; + for (int y = 0; y < 32; ++y) { + for (int x = 0; x < 128; ++x) { + const uint8_t a = rgba[(size_t(y) * 128u + size_t(x)) * 4u + 3u]; + if (a > 16) { + ++opaque; + perFrame[x / 32]++; + } + } + } + CHECK(opaque > 80); + for (int f = 0; f < 4; ++f) CHECK(perFrame[f] > 10); + + // Frames actually differ (wind lean). + int diffs = 0; + for (int y = 0; y < 32; ++y) { + for (int x = 0; x < 32; ++x) { + const uint8_t a0 = rgba[(size_t(y) * 128u + size_t(x)) * 4u + 3u]; + const uint8_t a3 = rgba[(size_t(y) * 128u + size_t(x + 96)) * 4u + 3u]; + if (a0 != a3) ++diffs; + } + } + CHECK(diffs > 8); +} + +TEST_CASE("graphics.Grass.layerFlag") { + Shader sh; + sh.setKind(Shader::Kind::eMesh3D); + eve::graphics::grass::bindDefaults(&sh); + eve::graphics::grass::bindLayer(&sh, true); + float dark = 0.f; + REQUIRE_EQ(sh.getFromVar("alwaysDark", &dark, sizeof(dark)), int(sizeof(dark))); + CHECK(dark > 0.5f); + eve::graphics::grass::bindLayer(&sh, false); + REQUIRE_EQ(sh.getFromVar("alwaysDark", &dark, sizeof(dark)), int(sizeof(dark))); + CHECK(dark < 0.5f); +} From 981a1cb8ea328ff15181e6c53fa59618ce5de3b7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 15 Aug 2026 06:17:43 +0000 Subject: [PATCH 16/21] fix(graphics): avoid nested BakeParams default argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clang rejects `BakeParams params = {}` on GrassField while the nested struct still has in-class initializers. Callers pass BakeParams explicitly. Co-authored-by: 西风逍遥游 --- src/modules/graphics/Grass.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/graphics/Grass.h b/src/modules/graphics/Grass.h index 1e56c65e..7dc75e0b 100644 --- a/src/modules/graphics/Grass.h +++ b/src/modules/graphics/Grass.h @@ -120,7 +120,7 @@ public: GrassField &operator=(const GrassField &) = delete; void bake(const float *posXYZ, const float *nrmXYZ, int vertexCount, const uint32_t *indices, - int indexCount, const BakeParams ¶ms = {}); + int indexCount, const BakeParams ¶ms); void bakePlane(float sizeX, float sizeZ, int segX, int segZ); void bakePlane(float sizeX, float sizeZ, int segX, int segZ, const BakeParams ¶ms); From 27f7b176cb831c8d9f7c161120182b8446aa687e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 15 Aug 2026 06:30:51 +0000 Subject: [PATCH 17/21] test(graphics): GPU-render t3ssel8r grass and save a screenshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bake alwaysDark into billboard normals so dense and sparse tufts can share one mesh shader in the 3D pass, then add a windowed GPU test that draws a field with a shadow-casting occluder and writes grass_field.png. Co-authored-by: 西风逍遥游 --- src/modules/graphics/Grass.cpp | 9 +- src/modules/graphics/Grass.h | 4 +- .../graphics/shaders/mesh3d_grass.frag | 3 +- .../graphics/shaders/mesh3d_grass.frag.spv | Bin 11224 -> 11412 bytes .../graphics/shaders/mesh3d_grass.vert | 4 +- .../graphics/shaders/mesh3d_grass.vert.spv | Bin 6308 -> 6428 bytes .../shaders/mesh3d_grass_frag_spv.inc | 706 +++++++++--------- .../shaders/mesh3d_grass_vert_spv.inc | 397 +++++----- test/grass.cpp | 251 +++++++ 9 files changed, 819 insertions(+), 555 deletions(-) diff --git a/src/modules/graphics/Grass.cpp b/src/modules/graphics/Grass.cpp index c29887e9..4148b27b 100644 --- a/src/modules/graphics/Grass.cpp +++ b/src/modules/graphics/Grass.cpp @@ -359,7 +359,8 @@ std::vector samplePoisson(const float *posXYZ, const float *nrmXYZ, int v return accepted; } -BillboardMesh buildBillboards(const std::vector &points, float width, float height) { +BillboardMesh buildBillboards(const std::vector &points, float width, float height, + bool alwaysDark) { (void)width; (void)height; BillboardMesh mesh; @@ -384,7 +385,7 @@ BillboardMesh buildBillboards(const std::vector &points, float width, flo mesh.posXYZ.push_back(p.position.z); mesh.nrmXYZ.push_back(float(p.id)); mesh.nrmXYZ.push_back(p.scale > 1e-3f ? p.scale : 1.f); - mesh.nrmXYZ.push_back(0.f); + mesh.nrmXYZ.push_back(alwaysDark ? 1.f : 0.f); mesh.uvST.push_back(cu[c]); mesh.uvST.push_back(cv[c]); } @@ -466,8 +467,8 @@ void GrassField::bake(const float *posXYZ, const float *nrmXYZ, int vertexCount, denseCount_ = int(densePts.size()); sparseCount_ = int(sparsePts.size()); - const auto denseMesh = grass::buildBillboards(densePts, params.width, params.height); - const auto sparseMesh = grass::buildBillboards(sparsePts, params.width, params.height); + const auto denseMesh = grass::buildBillboards(densePts, params.width, params.height, false); + const auto sparseMesh = grass::buildBillboards(sparsePts, params.width, params.height, true); if (!shader_) shader_ = grass::createShader(gfx_); if (!atlas_) diff --git a/src/modules/graphics/Grass.h b/src/modules/graphics/Grass.h index 7dc75e0b..e68ab4b5 100644 --- a/src/modules/graphics/Grass.h +++ b/src/modules/graphics/Grass.h @@ -63,10 +63,10 @@ std::vector sampleHalton(const float *posXYZ, const float *nrmXYZ, int ve * Expand a unit rectangle per point. Vertex layout: * pos = grass root * uv = quad corner in [0,1]^2 (0.5, 0) is the root - * normal.x = instance id, normal.y = scale + * normal.x = instance id, normal.y = scale, normal.z = alwaysDark (0/1) */ BillboardMesh buildBillboards(const std::vector &points, float width = 0.45f, - float height = 0.7f); + float height = 0.7f, bool alwaysDark = false); /** Discrete 4-frame index with a per-instance phase offset. */ int swayFrame(float time, float frameDuration, uint32_t instanceId, int frameCount = 4); diff --git a/src/modules/graphics/shaders/mesh3d_grass.frag b/src/modules/graphics/shaders/mesh3d_grass.frag index 3a523a58..4d07f885 100644 --- a/src/modules/graphics/shaders/mesh3d_grass.frag +++ b/src/modules/graphics/shaders/mesh3d_grass.frag @@ -10,6 +10,7 @@ layout(location = 2) in vec3 vViewPos; layout(location = 3) in vec3 vRootPos; layout(location = 4) in float vInstanceId; layout(location = 5) in vec4 vTint; +layout(location = 6) in float vAlwaysDark; struct Light3D { vec4 posRadius; @@ -108,7 +109,7 @@ void main() { float viewDepth = max(-vViewPos.z, 0.0); float vis = sampleGrassShadow(vRootPos, viewDepth); - if (u.data[5] > 0.5) + if (u.data[5] > 0.5 || vAlwaysDark > 0.5) vis = 0.0; vec3 lightGreen = vec3(u.data[6], u.data[7], u.data[8]); diff --git a/src/modules/graphics/shaders/mesh3d_grass.frag.spv b/src/modules/graphics/shaders/mesh3d_grass.frag.spv index e7223463575b7db807a30d73c3ef77321c7d824d..8866d6723da4ce6c305feaa0bea15411a5ba76c2 100644 GIT binary patch delta 1894 zcmZ9N+iy%!6vp?Mi&j<2L?$lLHqwViipCo$CaRUV){A(MIG8Xo)tYIG7vn{uZdJ!M z?$^32<5rCFAMhse;$QF}ZZE#yp0k@_&sp7Tec!j&T6>>;($5=CZLAKDn?2r`kf|~i z=KWxGOQ?EqWkcoBkTH*BBTst&gr^eqB{O@xeYsXIyVFGVJL&I^#9J9v?u>Fqmu7POj}e#DIKuH1TWTe>$V zKc*buP+Nl(Q4=vm0c}~YGi5cUSgz^p>$Yo(eseS)D*DT#wM}Tl;xg0<`XDcOM0ihn zR}X9Hq391r6OAx2AztT}bV{UC>$pl;)SAWoPtmx|Un!>B%X*oNw^#1iiV5=j(y2Wq zjQo>hiO^`FF*Xp&bGWmt*qC;;!ch@3h)050lUWfQ1l(-?XgCZWtVYJVw*qLt&c%R0tc`+5|Fg$CoAHsVNgptrX>DH8E7cKv`j}iumcYBuT{ib$jFJq zTy=~vDu4}_VQc{Gy~F%(K~EI{^HD2px0*SB)L$)D+9qdT-aGL>z55-N@dkRT3Rcfo z+Z^DiuzFz(8yEU3zeMwkL@gA>d(&g)D5kKlfOcJcMlpq4d|ok4yZEYNnsMu^1=(0rY*MS%#VXfvjA}KgiF_(ca2yB*_q<1^5rnU*x zt>VhzQ~sTB-W{hxdE!w&Rr96(q&zkSO72Vl#X|*e@}Eflj_7vL{h|-aeyH;!ni;>v zfghHE{ZuLZ7U7)VG5MVBYrQsd9G4HaME);n-5TcaY6i`KeCqVdq|e)g6de@6peUjf zGBAqbFP3J%=9SWr+;PlFwbQKmlx&`?K^k7NYxH!$Io;VYoo@G;kiRCeuxXuga9$V1 zp+6_PAfqm43GUWk_LGT3{)&*xW#~2et~yu7RlhDD&aJ*#rsFC(-7WcWu9MsHslj?U zI^ivXYfav^dq)8vNofDBeA;zM@5$%bT+;jU!JJLh z%O48mO8_@iCcP57lmR^1&{nvhsZ)yB35Yejs(`eNc8 z>H?7{TD)KIQjQdS(|_Qj3GqMZlNUnb_Z@c0k!>eC^UcgRv$K1~j(_W^@l`ag6T%Qx zA}Br;YFdrzLT8it!VqG%bZN=gd=pwKdHPJQEt^PWgfQ`&0M;MNo>h>G6=lh9?PH#O zA$Q15*;#?b=viQcJ$5$L-({b7F~4wt9VP4D!yXC(v{04 zznE4ty%@>LsnM{m5(Jh@3>~!7eJd<-S@ExjXE2$#h;5a>3VJ-19O%fLvWc6*Cl;J& zV8n=UNOi=p+Rz*aL>rrEN1!8(pq1CdXapKkk#h(MKyPqupL_TU=;CCy@XrCbc~Yn`O2A05(fDfbThUAM~w|{mSM$AVzx#I^O~Q0XznLAumTKftt)dlYT&!G0UE8`6Xr&aV|*oZ%hi z({ZDoIL!6LYG45lI(4A*1ajb-a11(qU7+Xr#-Xza^XX%=Jo~sD4u@rMOSLS9Tk@K9 z3Lf1iuSv5$gifDkeFXg;6gd_}% z0^>uSk+E7U`4-3wq#G!&?j3YSb7}0o!U${ZgTnX@yHJB|m4Gpy@${3M$Klg2Ip1sN GH~j-88rj1D diff --git a/src/modules/graphics/shaders/mesh3d_grass.vert b/src/modules/graphics/shaders/mesh3d_grass.vert index 9029bd33..98ec2b0e 100644 --- a/src/modules/graphics/shaders/mesh3d_grass.vert +++ b/src/modules/graphics/shaders/mesh3d_grass.vert @@ -1,7 +1,7 @@ #version 450 // t3ssel8r-style grass billboard vertex. // inPos = grass ROOT (object space) -// inNormal.x = instance id, inNormal.y = scale +// inNormal.x = instance id, inNormal.y = scale, inNormal.z = alwaysDark (0/1) // inUV = quad corner in [0,1]^2; (0.5, 0) is the root (bottom center) struct Light3D { @@ -38,6 +38,7 @@ layout(location = 2) out vec3 vViewPos; layout(location = 3) out vec3 vRootPos; layout(location = 4) out float vInstanceId; layout(location = 5) out vec4 vTint; +layout(location = 6) out float vAlwaysDark; void main() { float width = max(u.data[2], 0.01); @@ -72,4 +73,5 @@ void main() { vRootPos = rootWorld.xyz; vInstanceId = inNormal.x; vTint = ubo.tint; + vAlwaysDark = inNormal.z; } diff --git a/src/modules/graphics/shaders/mesh3d_grass.vert.spv b/src/modules/graphics/shaders/mesh3d_grass.vert.spv index 2d5637c05be45f329c549614e896f4b969d96f54..dc7b9cdf64339b733e6a6a0dcf8253a111933668 100644 GIT binary patch delta 168 zcmZ2tILC;WnMs+Qfq{{Mn}LJj@kCxzMuCaWmW=l|R$gWnW@TVyxX-}AQ0ACZo>*Dz zl30|z*_-7q6R$f13s^!9D9N^2mA#xr)*2{u9moZ#X9i+zAfFRRKLBE;%`>@~c`Y1) n#wh~14}n-7h#vs4El?R7kbVTjAaOgOW{?pe{$HT8EPxmQBbpui delta 53 zcmbPZw8W5?nMs+Qfq{{Mn}LJj{zP6=M*fM;mKz%`FmFy_xx=(MfW3re@e^)dj=w-P H7C;ODRCWy@ diff --git a/src/modules/graphics/shaders/mesh3d_grass_frag_spv.inc b/src/modules/graphics/shaders/mesh3d_grass_frag_spv.inc index 776a1d06..175661dc 100644 --- a/src/modules/graphics/shaders/mesh3d_grass_frag_spv.inc +++ b/src/modules/graphics/shaders/mesh3d_grass_frag_spv.inc @@ -1,354 +1,360 @@ static const uint32_t mesh3d_grass_frag_spv[] = { - 0x07230203, 0x00010000, 0x0008000b, 0x000001b9, 0x00000000, 0x00020011, 0x00000001, 0x00020011, + 0x07230203, 0x00010000, 0x0008000b, 0x000001c0, 0x00000000, 0x00020011, 0x00000001, 0x00020011, 0x00000032, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, - 0x00000000, 0x00000001, 0x000c000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x0000013b, - 0x00000154, 0x00000169, 0x00000179, 0x0000017f, 0x000001a7, 0x000001b2, 0x00030010, 0x00000004, - 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x00040005, 0x00000004, 0x6e69616d, 0x00000000, - 0x000a0005, 0x00000010, 0x706d6173, 0x6853656c, 0x776f6461, 0x63736143, 0x28656461, 0x3b336676, - 0x663b3169, 0x00003b31, 0x00050005, 0x0000000d, 0x6c726f77, 0x736f5064, 0x00000000, 0x00040005, - 0x0000000e, 0x63736163, 0x00656461, 0x00040005, 0x0000000f, 0x4263646e, 0x00736169, 0x00070005, - 0x00000014, 0x63736163, 0x4e656461, 0x69426364, 0x69287361, 0x00003b31, 0x00040005, 0x00000013, - 0x63736163, 0x00656461, 0x00090005, 0x00000019, 0x706d6173, 0x7247656c, 0x53737361, 0x6f646168, - 0x66762877, 0x31663b33, 0x0000003b, 0x00050005, 0x00000017, 0x6c726f77, 0x736f5064, 0x00000000, - 0x00050005, 0x00000018, 0x77656976, 0x74706544, 0x00000068, 0x00050005, 0x0000001d, 0x6867696c, - 0x696c4374, 0x00000070, 0x00050005, 0x00000022, 0x64616853, 0x7246776f, 0x00656d61, 0x00050006, - 0x00000022, 0x00000000, 0x6867696c, 0x00505674, 0x00050006, 0x00000022, 0x00000001, 0x696c7073, - 0x00007374, 0x00050006, 0x00000022, 0x00000002, 0x73616962, 0x00000000, 0x00060006, 0x00000022, - 0x00000003, 0x63736163, 0x42656461, 0x00736169, 0x00070006, 0x00000022, 0x00000004, 0x63736163, - 0x54656461, 0x6c657865, 0x00000000, 0x00040005, 0x00000024, 0x64616873, 0x0000776f, 0x00030005, - 0x00000031, 0x0063646e, 0x00030005, 0x0000003c, 0x00007675, 0x00040005, 0x00000043, 0x74706564, - 0x00000068, 0x00040005, 0x0000006c, 0x65786574, 0x0000006c, 0x00050005, 0x00000070, 0x64616873, - 0x614d776f, 0x00000070, 0x00030005, 0x0000007a, 0x006d7573, 0x00040005, 0x0000007b, 0x6665727a, - 0x00000000, 0x00030005, 0x0000007f, 0x00000069, 0x00030005, 0x00000088, 0x00000073, 0x00050005, - 0x0000009b, 0x65646e69, 0x6c626178, 0x00000065, 0x00030005, 0x000000d0, 0x00000062, 0x00040005, - 0x0000010a, 0x63736163, 0x00656461, 0x00030005, 0x00000118, 0x00736976, 0x00040005, 0x00000119, - 0x61726170, 0x0000006d, 0x00040005, 0x0000011c, 0x61726170, 0x0000006d, 0x00040005, 0x0000011e, - 0x61726170, 0x0000006d, 0x00040005, 0x00000120, 0x61726170, 0x0000006d, 0x00040005, 0x0000012a, - 0x6d617266, 0x00007365, 0x00050005, 0x0000012d, 0x65747845, 0x6c616e72, 0x00000073, 0x00050006, - 0x0000012d, 0x00000000, 0x61746164, 0x00000000, 0x00030005, 0x0000012f, 0x00000075, 0x00050005, - 0x00000135, 0x61727564, 0x6e6f6974, 0x00000000, 0x00040005, 0x00000139, 0x68736168, 0x00000000, - 0x00050005, 0x0000013b, 0x736e4976, 0x636e6174, 0x00644965, 0x00030005, 0x00000143, 0x00000074, - 0x00040005, 0x0000014c, 0x6d617266, 0x00000065, 0x00040005, 0x00000152, 0x616c7461, 0x00565573, - 0x00030005, 0x00000154, 0x00565576, 0x00040005, 0x00000160, 0x6b73616d, 0x00000000, 0x00060005, - 0x00000164, 0x65626c61, 0x61536f64, 0x656c706d, 0x00000072, 0x00040005, 0x00000169, 0x6e695476, - 0x00000074, 0x00050005, 0x00000177, 0x77656976, 0x74706544, 0x00000068, 0x00050005, 0x00000179, - 0x65695676, 0x736f5077, 0x00000000, 0x00030005, 0x0000017e, 0x00736976, 0x00050005, 0x0000017f, - 0x6f6f5276, 0x736f5074, 0x00000000, 0x00040005, 0x00000180, 0x61726170, 0x0000006d, 0x00040005, - 0x00000182, 0x61726170, 0x0000006d, 0x00050005, 0x0000018b, 0x6867696c, 0x65724774, 0x00006e65, - 0x00050005, 0x00000196, 0x6b726164, 0x65657247, 0x0000006e, 0x00050005, 0x000001a0, 0x73617267, - 0x6c6f4373, 0x00000000, 0x00050005, 0x000001a7, 0x4374756f, 0x726f6c6f, 0x00000000, 0x00050005, - 0x000001b2, 0x726f5776, 0x6f50646c, 0x00000073, 0x00040005, 0x000001b3, 0x6867694c, 0x00443374, - 0x00060006, 0x000001b3, 0x00000000, 0x52736f70, 0x75696461, 0x00000073, 0x00050006, 0x000001b3, - 0x00000001, 0x6f6c6f63, 0x00000072, 0x00040005, 0x000001b6, 0x6d617246, 0x00000065, 0x00040006, - 0x000001b6, 0x00000000, 0x0070766d, 0x00050006, 0x000001b6, 0x00000001, 0x65646f6d, 0x0000006c, - 0x00080006, 0x000001b6, 0x00000002, 0x6867696c, 0x72694474, 0x65746e49, 0x7469736e, 0x00000079, - 0x00060006, 0x000001b6, 0x00000003, 0x6867696c, 0x6c6f4374, 0x0000726f, 0x00050006, 0x000001b6, - 0x00000004, 0x746e6974, 0x00000000, 0x00060006, 0x000001b6, 0x00000005, 0x656d6163, 0x6f506172, - 0x00000073, 0x00050006, 0x000001b6, 0x00000006, 0x69626d61, 0x00746e65, 0x00050006, 0x000001b6, - 0x00000007, 0x6867696c, 0x00007374, 0x00050006, 0x000001b6, 0x00000008, 0x42786574, 0x00626d6f, - 0x00060006, 0x000001b6, 0x00000009, 0x61726170, 0x78616c6c, 0x00000000, 0x00050006, 0x000001b6, - 0x0000000a, 0x77656976, 0x00000000, 0x00060006, 0x000001b6, 0x0000000b, 0x70696c63, 0x6f666e49, - 0x00000000, 0x00030005, 0x000001b8, 0x006f6275, 0x00040047, 0x00000021, 0x00000006, 0x00000040, - 0x00030047, 0x00000022, 0x00000002, 0x00040048, 0x00000022, 0x00000000, 0x00000005, 0x00050048, - 0x00000022, 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x00000022, 0x00000000, 0x00000023, - 0x00000000, 0x00050048, 0x00000022, 0x00000001, 0x00000023, 0x000000c0, 0x00050048, 0x00000022, - 0x00000002, 0x00000023, 0x000000d0, 0x00050048, 0x00000022, 0x00000003, 0x00000023, 0x000000e0, - 0x00050048, 0x00000022, 0x00000004, 0x00000023, 0x000000f0, 0x00040047, 0x00000024, 0x00000021, - 0x00000004, 0x00040047, 0x00000024, 0x00000022, 0x00000000, 0x00040047, 0x00000070, 0x00000021, - 0x00000005, 0x00040047, 0x00000070, 0x00000022, 0x00000000, 0x00040047, 0x0000012c, 0x00000006, - 0x00000004, 0x00030047, 0x0000012d, 0x00000002, 0x00050048, 0x0000012d, 0x00000000, 0x00000023, - 0x00000000, 0x00040047, 0x0000013b, 0x0000001e, 0x00000004, 0x00040047, 0x00000154, 0x0000001e, - 0x00000000, 0x00040047, 0x00000164, 0x00000021, 0x00000001, 0x00040047, 0x00000164, 0x00000022, - 0x00000000, 0x00040047, 0x00000169, 0x0000001e, 0x00000005, 0x00040047, 0x00000179, 0x0000001e, - 0x00000002, 0x00040047, 0x0000017f, 0x0000001e, 0x00000003, 0x00040047, 0x000001a7, 0x0000001e, - 0x00000000, 0x00040047, 0x000001b2, 0x0000001e, 0x00000001, 0x00050048, 0x000001b3, 0x00000000, - 0x00000023, 0x00000000, 0x00050048, 0x000001b3, 0x00000001, 0x00000023, 0x00000010, 0x00040047, - 0x000001b5, 0x00000006, 0x00000020, 0x00030047, 0x000001b6, 0x00000002, 0x00040048, 0x000001b6, - 0x00000000, 0x00000005, 0x00050048, 0x000001b6, 0x00000000, 0x00000007, 0x00000010, 0x00050048, - 0x000001b6, 0x00000000, 0x00000023, 0x00000000, 0x00040048, 0x000001b6, 0x00000001, 0x00000005, - 0x00050048, 0x000001b6, 0x00000001, 0x00000007, 0x00000010, 0x00050048, 0x000001b6, 0x00000001, - 0x00000023, 0x00000040, 0x00050048, 0x000001b6, 0x00000002, 0x00000023, 0x00000080, 0x00050048, - 0x000001b6, 0x00000003, 0x00000023, 0x00000090, 0x00050048, 0x000001b6, 0x00000004, 0x00000023, - 0x000000a0, 0x00050048, 0x000001b6, 0x00000005, 0x00000023, 0x000000b0, 0x00050048, 0x000001b6, - 0x00000006, 0x00000023, 0x000000c0, 0x00050048, 0x000001b6, 0x00000007, 0x00000023, 0x000000d0, - 0x00050048, 0x000001b6, 0x00000008, 0x00000023, 0x000001d0, 0x00050048, 0x000001b6, 0x00000009, - 0x00000023, 0x000001e0, 0x00040048, 0x000001b6, 0x0000000a, 0x00000005, 0x00050048, 0x000001b6, - 0x0000000a, 0x00000007, 0x00000010, 0x00050048, 0x000001b6, 0x0000000a, 0x00000023, 0x000001f0, - 0x00050048, 0x000001b6, 0x0000000b, 0x00000023, 0x00000230, 0x00040047, 0x000001b8, 0x00000021, - 0x00000000, 0x00040047, 0x000001b8, 0x00000022, 0x00000000, 0x00020013, 0x00000002, 0x00030021, - 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, - 0x00000003, 0x00040020, 0x00000008, 0x00000007, 0x00000007, 0x00040015, 0x00000009, 0x00000020, - 0x00000001, 0x00040020, 0x0000000a, 0x00000007, 0x00000009, 0x00040020, 0x0000000b, 0x00000007, - 0x00000006, 0x00060021, 0x0000000c, 0x00000006, 0x00000008, 0x0000000a, 0x0000000b, 0x00040021, - 0x00000012, 0x00000006, 0x0000000a, 0x00050021, 0x00000016, 0x00000006, 0x00000008, 0x0000000b, - 0x00040017, 0x0000001b, 0x00000006, 0x00000004, 0x00040020, 0x0000001c, 0x00000007, 0x0000001b, - 0x00040018, 0x0000001e, 0x0000001b, 0x00000004, 0x00040015, 0x0000001f, 0x00000020, 0x00000000, - 0x0004002b, 0x0000001f, 0x00000020, 0x00000003, 0x0004001c, 0x00000021, 0x0000001e, 0x00000020, - 0x0007001e, 0x00000022, 0x00000021, 0x0000001b, 0x0000001b, 0x0000001b, 0x0000001b, 0x00040020, - 0x00000023, 0x00000002, 0x00000022, 0x0004003b, 0x00000023, 0x00000024, 0x00000002, 0x0004002b, - 0x00000009, 0x00000025, 0x00000000, 0x00040020, 0x00000027, 0x00000002, 0x0000001e, 0x0004002b, - 0x00000006, 0x0000002b, 0x3f800000, 0x0004002b, 0x00000006, 0x00000036, 0x358637bd, 0x00040017, - 0x0000003a, 0x00000006, 0x00000002, 0x00040020, 0x0000003b, 0x00000007, 0x0000003a, 0x0004002b, - 0x00000006, 0x0000003f, 0x3f000000, 0x0004002b, 0x0000001f, 0x00000044, 0x00000002, 0x00020014, - 0x00000047, 0x0004002b, 0x0000001f, 0x00000048, 0x00000000, 0x0004002b, 0x00000006, 0x0000004b, - 0x00000000, 0x0004002b, 0x0000001f, 0x00000057, 0x00000001, 0x00090019, 0x0000006d, 0x00000006, - 0x00000001, 0x00000001, 0x00000001, 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x0000006e, - 0x0000006d, 0x00040020, 0x0000006f, 0x00000000, 0x0000006e, 0x0004003b, 0x0000006f, 0x00000070, - 0x00000000, 0x00040017, 0x00000073, 0x00000009, 0x00000003, 0x00040017, 0x00000075, 0x00000009, - 0x00000002, 0x0004002b, 0x00000009, 0x00000086, 0x00000009, 0x0004002b, 0x0000001f, 0x0000008a, - 0x00000009, 0x0004001c, 0x0000008b, 0x0000003a, 0x0000008a, 0x0004002b, 0x00000006, 0x0000008c, - 0xbf000000, 0x0005002c, 0x0000003a, 0x0000008d, 0x0000008c, 0x0000008c, 0x0004002b, 0x00000006, - 0x0000008e, 0xbf19999a, 0x0005002c, 0x0000003a, 0x0000008f, 0x0000004b, 0x0000008e, 0x0005002c, - 0x0000003a, 0x00000090, 0x0000003f, 0x0000008c, 0x0005002c, 0x0000003a, 0x00000091, 0x0000008e, - 0x0000004b, 0x0005002c, 0x0000003a, 0x00000092, 0x0000004b, 0x0000004b, 0x0004002b, 0x00000006, - 0x00000093, 0x3f19999a, 0x0005002c, 0x0000003a, 0x00000094, 0x00000093, 0x0000004b, 0x0005002c, - 0x0000003a, 0x00000095, 0x0000008c, 0x0000003f, 0x0005002c, 0x0000003a, 0x00000096, 0x0000004b, - 0x00000093, 0x0005002c, 0x0000003a, 0x00000097, 0x0000003f, 0x0000003f, 0x000c002c, 0x0000008b, - 0x00000098, 0x0000008d, 0x0000008f, 0x00000090, 0x00000091, 0x00000092, 0x00000094, 0x00000095, - 0x00000096, 0x00000097, 0x00040020, 0x0000009a, 0x00000007, 0x0000008b, 0x0004002b, 0x00000009, - 0x000000c9, 0x00000001, 0x0004002b, 0x00000006, 0x000000cc, 0x41100000, 0x0004002b, 0x00000009, - 0x000000d6, 0x00000003, 0x00040020, 0x000000d7, 0x00000002, 0x00000006, 0x0004002b, 0x00000006, - 0x000000e8, 0x322bcc77, 0x0004002b, 0x00000009, 0x000000ef, 0x00000002, 0x0004002b, 0x00000006, - 0x00000104, 0x38d1b717, 0x0004002b, 0x0000001f, 0x0000012b, 0x00000020, 0x0004001c, 0x0000012c, - 0x00000006, 0x0000012b, 0x0003001e, 0x0000012d, 0x0000012c, 0x00040020, 0x0000012e, 0x00000009, - 0x0000012d, 0x0004003b, 0x0000012e, 0x0000012f, 0x00000009, 0x0004002b, 0x00000009, 0x00000130, - 0x0000000c, 0x00040020, 0x00000131, 0x00000009, 0x00000006, 0x00040020, 0x0000013a, 0x00000001, - 0x00000006, 0x0004003b, 0x0000013a, 0x0000013b, 0x00000001, 0x0004002b, 0x00000006, 0x0000013d, - 0x414fd639, 0x0004002b, 0x00000006, 0x00000140, 0x472aee8c, 0x00040020, 0x00000153, 0x00000001, - 0x0000003a, 0x0004003b, 0x00000153, 0x00000154, 0x00000001, 0x00090019, 0x00000161, 0x00000006, - 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x00000162, - 0x00000161, 0x00040020, 0x00000163, 0x00000000, 0x00000162, 0x0004003b, 0x00000163, 0x00000164, - 0x00000000, 0x00040020, 0x00000168, 0x00000001, 0x0000001b, 0x0004003b, 0x00000168, 0x00000169, - 0x00000001, 0x0004002b, 0x00000009, 0x0000016e, 0x00000004, 0x0004002b, 0x00000006, 0x00000171, - 0x3c23d70a, 0x00040020, 0x00000178, 0x00000001, 0x00000007, 0x0004003b, 0x00000178, 0x00000179, - 0x00000001, 0x0004003b, 0x00000178, 0x0000017f, 0x00000001, 0x0004002b, 0x00000009, 0x00000185, - 0x00000005, 0x0004002b, 0x00000009, 0x0000018c, 0x00000006, 0x0004002b, 0x00000009, 0x0000018f, - 0x00000007, 0x0004002b, 0x00000009, 0x00000192, 0x00000008, 0x0004002b, 0x00000009, 0x00000199, - 0x0000000a, 0x0004002b, 0x00000009, 0x0000019c, 0x0000000b, 0x00040020, 0x000001a6, 0x00000003, - 0x0000001b, 0x0004003b, 0x000001a6, 0x000001a7, 0x00000003, 0x0004003b, 0x00000178, 0x000001b2, - 0x00000001, 0x0004001e, 0x000001b3, 0x0000001b, 0x0000001b, 0x0004002b, 0x0000001f, 0x000001b4, - 0x00000008, 0x0004001c, 0x000001b5, 0x000001b3, 0x000001b4, 0x000e001e, 0x000001b6, 0x0000001e, - 0x0000001e, 0x0000001b, 0x0000001b, 0x0000001b, 0x0000001b, 0x0000001b, 0x000001b5, 0x0000001b, - 0x0000001b, 0x0000001e, 0x0000001b, 0x00040020, 0x000001b7, 0x00000002, 0x000001b6, 0x0004003b, - 0x000001b7, 0x000001b8, 0x00000002, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, - 0x000200f8, 0x00000005, 0x0004003b, 0x0000000b, 0x0000012a, 0x00000007, 0x0004003b, 0x0000000b, - 0x00000135, 0x00000007, 0x0004003b, 0x0000000b, 0x00000139, 0x00000007, 0x0004003b, 0x0000000b, - 0x00000143, 0x00000007, 0x0004003b, 0x0000000a, 0x0000014c, 0x00000007, 0x0004003b, 0x0000003b, - 0x00000152, 0x00000007, 0x0004003b, 0x0000001c, 0x00000160, 0x00000007, 0x0004003b, 0x0000000b, - 0x00000177, 0x00000007, 0x0004003b, 0x0000000b, 0x0000017e, 0x00000007, 0x0004003b, 0x00000008, - 0x00000180, 0x00000007, 0x0004003b, 0x0000000b, 0x00000182, 0x00000007, 0x0004003b, 0x00000008, - 0x0000018b, 0x00000007, 0x0004003b, 0x00000008, 0x00000196, 0x00000007, 0x0004003b, 0x00000008, - 0x000001a0, 0x00000007, 0x00060041, 0x00000131, 0x00000132, 0x0000012f, 0x00000025, 0x00000130, - 0x0004003d, 0x00000006, 0x00000133, 0x00000132, 0x0007000c, 0x00000006, 0x00000134, 0x00000001, - 0x00000028, 0x00000133, 0x0000002b, 0x0003003e, 0x0000012a, 0x00000134, 0x00060041, 0x00000131, - 0x00000136, 0x0000012f, 0x00000025, 0x000000c9, 0x0004003d, 0x00000006, 0x00000137, 0x00000136, - 0x0007000c, 0x00000006, 0x00000138, 0x00000001, 0x00000028, 0x00000137, 0x00000104, 0x0003003e, - 0x00000135, 0x00000138, 0x0004003d, 0x00000006, 0x0000013c, 0x0000013b, 0x00050085, 0x00000006, - 0x0000013e, 0x0000013c, 0x0000013d, 0x0006000c, 0x00000006, 0x0000013f, 0x00000001, 0x0000000d, - 0x0000013e, 0x00050085, 0x00000006, 0x00000141, 0x0000013f, 0x00000140, 0x0006000c, 0x00000006, - 0x00000142, 0x00000001, 0x0000000a, 0x00000141, 0x0003003e, 0x00000139, 0x00000142, 0x00060041, - 0x00000131, 0x00000144, 0x0000012f, 0x00000025, 0x00000025, 0x0004003d, 0x00000006, 0x00000145, - 0x00000144, 0x0004003d, 0x00000006, 0x00000146, 0x00000135, 0x00050088, 0x00000006, 0x00000147, - 0x00000145, 0x00000146, 0x0004003d, 0x00000006, 0x00000148, 0x00000139, 0x0004003d, 0x00000006, - 0x00000149, 0x0000012a, 0x00050085, 0x00000006, 0x0000014a, 0x00000148, 0x00000149, 0x00050081, - 0x00000006, 0x0000014b, 0x00000147, 0x0000014a, 0x0003003e, 0x00000143, 0x0000014b, 0x0004003d, - 0x00000006, 0x0000014d, 0x00000143, 0x0006000c, 0x00000006, 0x0000014e, 0x00000001, 0x00000008, - 0x0000014d, 0x0004003d, 0x00000006, 0x0000014f, 0x0000012a, 0x0005008d, 0x00000006, 0x00000150, - 0x0000014e, 0x0000014f, 0x0004006e, 0x00000009, 0x00000151, 0x00000150, 0x0003003e, 0x0000014c, - 0x00000151, 0x00050041, 0x0000013a, 0x00000155, 0x00000154, 0x00000048, 0x0004003d, 0x00000006, - 0x00000156, 0x00000155, 0x0004003d, 0x00000009, 0x00000157, 0x0000014c, 0x0004006f, 0x00000006, - 0x00000158, 0x00000157, 0x00050081, 0x00000006, 0x00000159, 0x00000156, 0x00000158, 0x0004003d, - 0x00000006, 0x0000015a, 0x0000012a, 0x00050088, 0x00000006, 0x0000015b, 0x00000159, 0x0000015a, - 0x00050041, 0x0000013a, 0x0000015c, 0x00000154, 0x00000057, 0x0004003d, 0x00000006, 0x0000015d, - 0x0000015c, 0x00050083, 0x00000006, 0x0000015e, 0x0000002b, 0x0000015d, 0x00050050, 0x0000003a, - 0x0000015f, 0x0000015b, 0x0000015e, 0x0003003e, 0x00000152, 0x0000015f, 0x0004003d, 0x00000162, - 0x00000165, 0x00000164, 0x0004003d, 0x0000003a, 0x00000166, 0x00000152, 0x00050057, 0x0000001b, - 0x00000167, 0x00000165, 0x00000166, 0x0004003d, 0x0000001b, 0x0000016a, 0x00000169, 0x00050085, - 0x0000001b, 0x0000016b, 0x00000167, 0x0000016a, 0x0003003e, 0x00000160, 0x0000016b, 0x00050041, - 0x0000000b, 0x0000016c, 0x00000160, 0x00000020, 0x0004003d, 0x00000006, 0x0000016d, 0x0000016c, - 0x00060041, 0x00000131, 0x0000016f, 0x0000012f, 0x00000025, 0x0000016e, 0x0004003d, 0x00000006, - 0x00000170, 0x0000016f, 0x0007000c, 0x00000006, 0x00000172, 0x00000001, 0x00000028, 0x00000170, - 0x00000171, 0x000500b8, 0x00000047, 0x00000173, 0x0000016d, 0x00000172, 0x000300f7, 0x00000175, - 0x00000000, 0x000400fa, 0x00000173, 0x00000174, 0x00000175, 0x000200f8, 0x00000174, 0x000100fc, - 0x000200f8, 0x00000175, 0x00050041, 0x0000013a, 0x0000017a, 0x00000179, 0x00000044, 0x0004003d, - 0x00000006, 0x0000017b, 0x0000017a, 0x0004007f, 0x00000006, 0x0000017c, 0x0000017b, 0x0007000c, - 0x00000006, 0x0000017d, 0x00000001, 0x00000028, 0x0000017c, 0x0000004b, 0x0003003e, 0x00000177, - 0x0000017d, 0x0004003d, 0x00000007, 0x00000181, 0x0000017f, 0x0003003e, 0x00000180, 0x00000181, - 0x0004003d, 0x00000006, 0x00000183, 0x00000177, 0x0003003e, 0x00000182, 0x00000183, 0x00060039, - 0x00000006, 0x00000184, 0x00000019, 0x00000180, 0x00000182, 0x0003003e, 0x0000017e, 0x00000184, - 0x00060041, 0x00000131, 0x00000186, 0x0000012f, 0x00000025, 0x00000185, 0x0004003d, 0x00000006, - 0x00000187, 0x00000186, 0x000500ba, 0x00000047, 0x00000188, 0x00000187, 0x0000003f, 0x000300f7, - 0x0000018a, 0x00000000, 0x000400fa, 0x00000188, 0x00000189, 0x0000018a, 0x000200f8, 0x00000189, - 0x0003003e, 0x0000017e, 0x0000004b, 0x000200f9, 0x0000018a, 0x000200f8, 0x0000018a, 0x00060041, - 0x00000131, 0x0000018d, 0x0000012f, 0x00000025, 0x0000018c, 0x0004003d, 0x00000006, 0x0000018e, - 0x0000018d, 0x00060041, 0x00000131, 0x00000190, 0x0000012f, 0x00000025, 0x0000018f, 0x0004003d, - 0x00000006, 0x00000191, 0x00000190, 0x00060041, 0x00000131, 0x00000193, 0x0000012f, 0x00000025, - 0x00000192, 0x0004003d, 0x00000006, 0x00000194, 0x00000193, 0x00060050, 0x00000007, 0x00000195, - 0x0000018e, 0x00000191, 0x00000194, 0x0003003e, 0x0000018b, 0x00000195, 0x00060041, 0x00000131, - 0x00000197, 0x0000012f, 0x00000025, 0x00000086, 0x0004003d, 0x00000006, 0x00000198, 0x00000197, - 0x00060041, 0x00000131, 0x0000019a, 0x0000012f, 0x00000025, 0x00000199, 0x0004003d, 0x00000006, - 0x0000019b, 0x0000019a, 0x00060041, 0x00000131, 0x0000019d, 0x0000012f, 0x00000025, 0x0000019c, - 0x0004003d, 0x00000006, 0x0000019e, 0x0000019d, 0x00060050, 0x00000007, 0x0000019f, 0x00000198, - 0x0000019b, 0x0000019e, 0x0003003e, 0x00000196, 0x0000019f, 0x0004003d, 0x00000007, 0x000001a1, - 0x00000196, 0x0004003d, 0x00000007, 0x000001a2, 0x0000018b, 0x0004003d, 0x00000006, 0x000001a3, - 0x0000017e, 0x00060050, 0x00000007, 0x000001a4, 0x000001a3, 0x000001a3, 0x000001a3, 0x0008000c, - 0x00000007, 0x000001a5, 0x00000001, 0x0000002e, 0x000001a1, 0x000001a2, 0x000001a4, 0x0003003e, - 0x000001a0, 0x000001a5, 0x0004003d, 0x00000007, 0x000001a8, 0x000001a0, 0x0004003d, 0x0000001b, - 0x000001a9, 0x00000160, 0x0008004f, 0x00000007, 0x000001aa, 0x000001a9, 0x000001a9, 0x00000000, - 0x00000001, 0x00000002, 0x00050085, 0x00000007, 0x000001ab, 0x000001a8, 0x000001aa, 0x00050041, - 0x0000000b, 0x000001ac, 0x00000160, 0x00000020, 0x0004003d, 0x00000006, 0x000001ad, 0x000001ac, - 0x00050051, 0x00000006, 0x000001ae, 0x000001ab, 0x00000000, 0x00050051, 0x00000006, 0x000001af, - 0x000001ab, 0x00000001, 0x00050051, 0x00000006, 0x000001b0, 0x000001ab, 0x00000002, 0x00070050, - 0x0000001b, 0x000001b1, 0x000001ae, 0x000001af, 0x000001b0, 0x000001ad, 0x0003003e, 0x000001a7, - 0x000001b1, 0x000100fd, 0x00010038, 0x00050036, 0x00000006, 0x00000010, 0x00000000, 0x0000000c, - 0x00030037, 0x00000008, 0x0000000d, 0x00030037, 0x0000000a, 0x0000000e, 0x00030037, 0x0000000b, - 0x0000000f, 0x000200f8, 0x00000011, 0x0004003b, 0x0000001c, 0x0000001d, 0x00000007, 0x0004003b, - 0x00000008, 0x00000031, 0x00000007, 0x0004003b, 0x0000003b, 0x0000003c, 0x00000007, 0x0004003b, - 0x0000000b, 0x00000043, 0x00000007, 0x0004003b, 0x0000003b, 0x0000006c, 0x00000007, 0x0004003b, - 0x0000000b, 0x0000007a, 0x00000007, 0x0004003b, 0x0000000b, 0x0000007b, 0x00000007, 0x0004003b, - 0x0000000a, 0x0000007f, 0x00000007, 0x0004003b, 0x0000003b, 0x00000088, 0x00000007, 0x0004003b, - 0x0000009a, 0x0000009b, 0x00000007, 0x0004003d, 0x00000009, 0x00000026, 0x0000000e, 0x00060041, - 0x00000027, 0x00000028, 0x00000024, 0x00000025, 0x00000026, 0x0004003d, 0x0000001e, 0x00000029, - 0x00000028, 0x0004003d, 0x00000007, 0x0000002a, 0x0000000d, 0x00050051, 0x00000006, 0x0000002c, - 0x0000002a, 0x00000000, 0x00050051, 0x00000006, 0x0000002d, 0x0000002a, 0x00000001, 0x00050051, - 0x00000006, 0x0000002e, 0x0000002a, 0x00000002, 0x00070050, 0x0000001b, 0x0000002f, 0x0000002c, - 0x0000002d, 0x0000002e, 0x0000002b, 0x00050091, 0x0000001b, 0x00000030, 0x00000029, 0x0000002f, - 0x0003003e, 0x0000001d, 0x00000030, 0x0004003d, 0x0000001b, 0x00000032, 0x0000001d, 0x0008004f, - 0x00000007, 0x00000033, 0x00000032, 0x00000032, 0x00000000, 0x00000001, 0x00000002, 0x00050041, - 0x0000000b, 0x00000034, 0x0000001d, 0x00000020, 0x0004003d, 0x00000006, 0x00000035, 0x00000034, - 0x0007000c, 0x00000006, 0x00000037, 0x00000001, 0x00000028, 0x00000035, 0x00000036, 0x00060050, - 0x00000007, 0x00000038, 0x00000037, 0x00000037, 0x00000037, 0x00050088, 0x00000007, 0x00000039, - 0x00000033, 0x00000038, 0x0003003e, 0x00000031, 0x00000039, 0x0004003d, 0x00000007, 0x0000003d, - 0x00000031, 0x0007004f, 0x0000003a, 0x0000003e, 0x0000003d, 0x0000003d, 0x00000000, 0x00000001, - 0x0005008e, 0x0000003a, 0x00000040, 0x0000003e, 0x0000003f, 0x00050050, 0x0000003a, 0x00000041, - 0x0000003f, 0x0000003f, 0x00050081, 0x0000003a, 0x00000042, 0x00000040, 0x00000041, 0x0003003e, - 0x0000003c, 0x00000042, 0x00050041, 0x0000000b, 0x00000045, 0x00000031, 0x00000044, 0x0004003d, - 0x00000006, 0x00000046, 0x00000045, 0x0003003e, 0x00000043, 0x00000046, 0x00050041, 0x0000000b, - 0x00000049, 0x0000003c, 0x00000048, 0x0004003d, 0x00000006, 0x0000004a, 0x00000049, 0x000500b8, - 0x00000047, 0x0000004c, 0x0000004a, 0x0000004b, 0x000400a8, 0x00000047, 0x0000004d, 0x0000004c, - 0x000300f7, 0x0000004f, 0x00000000, 0x000400fa, 0x0000004d, 0x0000004e, 0x0000004f, 0x000200f8, - 0x0000004e, 0x00050041, 0x0000000b, 0x00000050, 0x0000003c, 0x00000048, 0x0004003d, 0x00000006, - 0x00000051, 0x00000050, 0x000500ba, 0x00000047, 0x00000052, 0x00000051, 0x0000002b, 0x000200f9, - 0x0000004f, 0x000200f8, 0x0000004f, 0x000700f5, 0x00000047, 0x00000053, 0x0000004c, 0x00000011, - 0x00000052, 0x0000004e, 0x000400a8, 0x00000047, 0x00000054, 0x00000053, 0x000300f7, 0x00000056, - 0x00000000, 0x000400fa, 0x00000054, 0x00000055, 0x00000056, 0x000200f8, 0x00000055, 0x00050041, - 0x0000000b, 0x00000058, 0x0000003c, 0x00000057, 0x0004003d, 0x00000006, 0x00000059, 0x00000058, - 0x000500b8, 0x00000047, 0x0000005a, 0x00000059, 0x0000004b, 0x000200f9, 0x00000056, 0x000200f8, - 0x00000056, 0x000700f5, 0x00000047, 0x0000005b, 0x00000053, 0x0000004f, 0x0000005a, 0x00000055, - 0x000400a8, 0x00000047, 0x0000005c, 0x0000005b, 0x000300f7, 0x0000005e, 0x00000000, 0x000400fa, - 0x0000005c, 0x0000005d, 0x0000005e, 0x000200f8, 0x0000005d, 0x00050041, 0x0000000b, 0x0000005f, - 0x0000003c, 0x00000057, 0x0004003d, 0x00000006, 0x00000060, 0x0000005f, 0x000500ba, 0x00000047, - 0x00000061, 0x00000060, 0x0000002b, 0x000200f9, 0x0000005e, 0x000200f8, 0x0000005e, 0x000700f5, - 0x00000047, 0x00000062, 0x0000005b, 0x00000056, 0x00000061, 0x0000005d, 0x0004003d, 0x00000006, - 0x00000063, 0x00000043, 0x000500b8, 0x00000047, 0x00000064, 0x00000063, 0x0000004b, 0x000500a6, - 0x00000047, 0x00000065, 0x00000062, 0x00000064, 0x0004003d, 0x00000006, 0x00000066, 0x00000043, - 0x000500ba, 0x00000047, 0x00000067, 0x00000066, 0x0000002b, 0x000500a6, 0x00000047, 0x00000068, - 0x00000065, 0x00000067, 0x000300f7, 0x0000006a, 0x00000000, 0x000400fa, 0x00000068, 0x00000069, - 0x0000006a, 0x000200f8, 0x00000069, 0x000200fe, 0x0000002b, 0x000200f8, 0x0000006a, 0x0004003d, - 0x0000006e, 0x00000071, 0x00000070, 0x00040064, 0x0000006d, 0x00000072, 0x00000071, 0x00050067, - 0x00000073, 0x00000074, 0x00000072, 0x00000025, 0x0007004f, 0x00000075, 0x00000076, 0x00000074, - 0x00000074, 0x00000000, 0x00000001, 0x0004006f, 0x0000003a, 0x00000077, 0x00000076, 0x00050050, - 0x0000003a, 0x00000078, 0x0000002b, 0x0000002b, 0x00050088, 0x0000003a, 0x00000079, 0x00000078, - 0x00000077, 0x0003003e, 0x0000006c, 0x00000079, 0x0003003e, 0x0000007a, 0x0000004b, 0x0004003d, - 0x00000006, 0x0000007c, 0x00000043, 0x0004003d, 0x00000006, 0x0000007d, 0x0000000f, 0x00050083, - 0x00000006, 0x0000007e, 0x0000007c, 0x0000007d, 0x0003003e, 0x0000007b, 0x0000007e, 0x0003003e, - 0x0000007f, 0x00000025, 0x000200f9, 0x00000080, 0x000200f8, 0x00000080, 0x000400f6, 0x00000082, - 0x00000083, 0x00000000, 0x000200f9, 0x00000084, 0x000200f8, 0x00000084, 0x0004003d, 0x00000009, - 0x00000085, 0x0000007f, 0x000500b1, 0x00000047, 0x00000087, 0x00000085, 0x00000086, 0x000400fa, - 0x00000087, 0x00000081, 0x00000082, 0x000200f8, 0x00000081, 0x0004003d, 0x0000003a, 0x00000089, - 0x0000003c, 0x0004003d, 0x00000009, 0x00000099, 0x0000007f, 0x0003003e, 0x0000009b, 0x00000098, - 0x00050041, 0x0000003b, 0x0000009c, 0x0000009b, 0x00000099, 0x0004003d, 0x0000003a, 0x0000009d, - 0x0000009c, 0x0004003d, 0x0000003a, 0x0000009e, 0x0000006c, 0x00050085, 0x0000003a, 0x0000009f, - 0x0000009d, 0x0000009e, 0x00050081, 0x0000003a, 0x000000a0, 0x00000089, 0x0000009f, 0x0003003e, - 0x00000088, 0x000000a0, 0x00050041, 0x0000000b, 0x000000a1, 0x00000088, 0x00000048, 0x0004003d, - 0x00000006, 0x000000a2, 0x000000a1, 0x000500b8, 0x00000047, 0x000000a3, 0x000000a2, 0x0000004b, - 0x000400a8, 0x00000047, 0x000000a4, 0x000000a3, 0x000300f7, 0x000000a6, 0x00000000, 0x000400fa, - 0x000000a4, 0x000000a5, 0x000000a6, 0x000200f8, 0x000000a5, 0x00050041, 0x0000000b, 0x000000a7, - 0x00000088, 0x00000048, 0x0004003d, 0x00000006, 0x000000a8, 0x000000a7, 0x000500ba, 0x00000047, - 0x000000a9, 0x000000a8, 0x0000002b, 0x000200f9, 0x000000a6, 0x000200f8, 0x000000a6, 0x000700f5, - 0x00000047, 0x000000aa, 0x000000a3, 0x00000081, 0x000000a9, 0x000000a5, 0x000400a8, 0x00000047, - 0x000000ab, 0x000000aa, 0x000300f7, 0x000000ad, 0x00000000, 0x000400fa, 0x000000ab, 0x000000ac, - 0x000000ad, 0x000200f8, 0x000000ac, 0x00050041, 0x0000000b, 0x000000ae, 0x00000088, 0x00000057, - 0x0004003d, 0x00000006, 0x000000af, 0x000000ae, 0x000500b8, 0x00000047, 0x000000b0, 0x000000af, - 0x0000004b, 0x000200f9, 0x000000ad, 0x000200f8, 0x000000ad, 0x000700f5, 0x00000047, 0x000000b1, - 0x000000aa, 0x000000a6, 0x000000b0, 0x000000ac, 0x000400a8, 0x00000047, 0x000000b2, 0x000000b1, - 0x000300f7, 0x000000b4, 0x00000000, 0x000400fa, 0x000000b2, 0x000000b3, 0x000000b4, 0x000200f8, - 0x000000b3, 0x00050041, 0x0000000b, 0x000000b5, 0x00000088, 0x00000057, 0x0004003d, 0x00000006, - 0x000000b6, 0x000000b5, 0x000500ba, 0x00000047, 0x000000b7, 0x000000b6, 0x0000002b, 0x000200f9, - 0x000000b4, 0x000200f8, 0x000000b4, 0x000700f5, 0x00000047, 0x000000b8, 0x000000b1, 0x000000ad, - 0x000000b7, 0x000000b3, 0x000300f7, 0x000000ba, 0x00000000, 0x000400fa, 0x000000b8, 0x000000b9, - 0x000000ba, 0x000200f8, 0x000000b9, 0x000200f9, 0x00000083, 0x000200f8, 0x000000ba, 0x0004003d, - 0x0000006e, 0x000000bc, 0x00000070, 0x0004003d, 0x0000003a, 0x000000bd, 0x00000088, 0x0004003d, - 0x00000009, 0x000000be, 0x0000000e, 0x0004006f, 0x00000006, 0x000000bf, 0x000000be, 0x0004003d, - 0x00000006, 0x000000c0, 0x0000007b, 0x00050051, 0x00000006, 0x000000c1, 0x000000bd, 0x00000000, - 0x00050051, 0x00000006, 0x000000c2, 0x000000bd, 0x00000001, 0x00070050, 0x0000001b, 0x000000c3, - 0x000000c1, 0x000000c2, 0x000000bf, 0x000000c0, 0x00050051, 0x00000006, 0x000000c4, 0x000000c3, - 0x00000003, 0x00060059, 0x00000006, 0x000000c5, 0x000000bc, 0x000000c3, 0x000000c4, 0x0004003d, - 0x00000006, 0x000000c6, 0x0000007a, 0x00050081, 0x00000006, 0x000000c7, 0x000000c6, 0x000000c5, - 0x0003003e, 0x0000007a, 0x000000c7, 0x000200f9, 0x00000083, 0x000200f8, 0x00000083, 0x0004003d, - 0x00000009, 0x000000c8, 0x0000007f, 0x00050080, 0x00000009, 0x000000ca, 0x000000c8, 0x000000c9, - 0x0003003e, 0x0000007f, 0x000000ca, 0x000200f9, 0x00000080, 0x000200f8, 0x00000082, 0x0004003d, - 0x00000006, 0x000000cb, 0x0000007a, 0x00050088, 0x00000006, 0x000000cd, 0x000000cb, 0x000000cc, - 0x000200fe, 0x000000cd, 0x00010038, 0x00050036, 0x00000006, 0x00000014, 0x00000000, 0x00000012, - 0x00030037, 0x0000000a, 0x00000013, 0x000200f8, 0x00000015, 0x0004003b, 0x0000000b, 0x000000d0, - 0x00000007, 0x0004003b, 0x0000000b, 0x000000d3, 0x00000007, 0x0004003b, 0x0000000b, 0x000000dd, - 0x00000007, 0x0004003b, 0x0000000b, 0x000000ea, 0x00000007, 0x0004003d, 0x00000009, 0x000000d1, - 0x00000013, 0x000500aa, 0x00000047, 0x000000d2, 0x000000d1, 0x00000025, 0x000300f7, 0x000000d5, - 0x00000000, 0x000400fa, 0x000000d2, 0x000000d4, 0x000000da, 0x000200f8, 0x000000d4, 0x00060041, - 0x000000d7, 0x000000d8, 0x00000024, 0x000000d6, 0x00000048, 0x0004003d, 0x00000006, 0x000000d9, - 0x000000d8, 0x0003003e, 0x000000d3, 0x000000d9, 0x000200f9, 0x000000d5, 0x000200f8, 0x000000da, - 0x0004003d, 0x00000009, 0x000000db, 0x00000013, 0x000500aa, 0x00000047, 0x000000dc, 0x000000db, - 0x000000c9, 0x000300f7, 0x000000df, 0x00000000, 0x000400fa, 0x000000dc, 0x000000de, 0x000000e2, - 0x000200f8, 0x000000de, 0x00060041, 0x000000d7, 0x000000e0, 0x00000024, 0x000000d6, 0x00000057, - 0x0004003d, 0x00000006, 0x000000e1, 0x000000e0, 0x0003003e, 0x000000dd, 0x000000e1, 0x000200f9, - 0x000000df, 0x000200f8, 0x000000e2, 0x00060041, 0x000000d7, 0x000000e3, 0x00000024, 0x000000d6, - 0x00000044, 0x0004003d, 0x00000006, 0x000000e4, 0x000000e3, 0x0003003e, 0x000000dd, 0x000000e4, - 0x000200f9, 0x000000df, 0x000200f8, 0x000000df, 0x0004003d, 0x00000006, 0x000000e5, 0x000000dd, - 0x0003003e, 0x000000d3, 0x000000e5, 0x000200f9, 0x000000d5, 0x000200f8, 0x000000d5, 0x0004003d, - 0x00000006, 0x000000e6, 0x000000d3, 0x0003003e, 0x000000d0, 0x000000e6, 0x0004003d, 0x00000006, - 0x000000e7, 0x000000d0, 0x000500ba, 0x00000047, 0x000000e9, 0x000000e7, 0x000000e8, 0x000300f7, - 0x000000ec, 0x00000000, 0x000400fa, 0x000000e9, 0x000000eb, 0x000000ee, 0x000200f8, 0x000000eb, - 0x0004003d, 0x00000006, 0x000000ed, 0x000000d0, 0x0003003e, 0x000000ea, 0x000000ed, 0x000200f9, - 0x000000ec, 0x000200f8, 0x000000ee, 0x00060041, 0x000000d7, 0x000000f0, 0x00000024, 0x000000ef, - 0x00000048, 0x0004003d, 0x00000006, 0x000000f1, 0x000000f0, 0x0003003e, 0x000000ea, 0x000000f1, - 0x000200f9, 0x000000ec, 0x000200f8, 0x000000ec, 0x0004003d, 0x00000006, 0x000000f2, 0x000000ea, - 0x000200fe, 0x000000f2, 0x00010038, 0x00050036, 0x00000006, 0x00000019, 0x00000000, 0x00000016, - 0x00030037, 0x00000008, 0x00000017, 0x00030037, 0x0000000b, 0x00000018, 0x000200f8, 0x0000001a, - 0x0004003b, 0x0000000a, 0x0000010a, 0x00000007, 0x0004003b, 0x0000000b, 0x00000118, 0x00000007, - 0x0004003b, 0x0000000a, 0x00000119, 0x00000007, 0x0004003b, 0x00000008, 0x0000011c, 0x00000007, - 0x0004003b, 0x0000000a, 0x0000011e, 0x00000007, 0x0004003b, 0x0000000b, 0x00000120, 0x00000007, - 0x00060041, 0x000000d7, 0x000000f5, 0x00000024, 0x000000ef, 0x00000057, 0x0004003d, 0x00000006, - 0x000000f6, 0x000000f5, 0x000500b8, 0x00000047, 0x000000f7, 0x000000f6, 0x0000003f, 0x000400a8, - 0x00000047, 0x000000f8, 0x000000f7, 0x000300f7, 0x000000fa, 0x00000000, 0x000400fa, 0x000000f8, - 0x000000f9, 0x000000fa, 0x000200f8, 0x000000f9, 0x00060041, 0x000000d7, 0x000000fb, 0x00000024, - 0x000000ef, 0x00000044, 0x0004003d, 0x00000006, 0x000000fc, 0x000000fb, 0x000500b8, 0x00000047, - 0x000000fd, 0x000000fc, 0x0000003f, 0x000200f9, 0x000000fa, 0x000200f8, 0x000000fa, 0x000700f5, - 0x00000047, 0x000000fe, 0x000000f7, 0x0000001a, 0x000000fd, 0x000000f9, 0x000400a8, 0x00000047, - 0x000000ff, 0x000000fe, 0x000300f7, 0x00000101, 0x00000000, 0x000400fa, 0x000000ff, 0x00000100, - 0x00000101, 0x000200f8, 0x00000100, 0x00060041, 0x000000d7, 0x00000102, 0x00000024, 0x000000c9, - 0x00000020, 0x0004003d, 0x00000006, 0x00000103, 0x00000102, 0x000500b8, 0x00000047, 0x00000105, - 0x00000103, 0x00000104, 0x000200f9, 0x00000101, 0x000200f8, 0x00000101, 0x000700f5, 0x00000047, - 0x00000106, 0x000000fe, 0x000000fa, 0x00000105, 0x00000100, 0x000300f7, 0x00000108, 0x00000000, - 0x000400fa, 0x00000106, 0x00000107, 0x00000108, 0x000200f8, 0x00000107, 0x000200fe, 0x0000002b, - 0x000200f8, 0x00000108, 0x0003003e, 0x0000010a, 0x000000ef, 0x0004003d, 0x00000006, 0x0000010b, - 0x00000018, 0x00060041, 0x000000d7, 0x0000010c, 0x00000024, 0x000000c9, 0x00000048, 0x0004003d, - 0x00000006, 0x0000010d, 0x0000010c, 0x000500b8, 0x00000047, 0x0000010e, 0x0000010b, 0x0000010d, - 0x000300f7, 0x00000110, 0x00000000, 0x000400fa, 0x0000010e, 0x0000010f, 0x00000111, 0x000200f8, - 0x0000010f, 0x0003003e, 0x0000010a, 0x00000025, 0x000200f9, 0x00000110, 0x000200f8, 0x00000111, - 0x0004003d, 0x00000006, 0x00000112, 0x00000018, 0x00060041, 0x000000d7, 0x00000113, 0x00000024, - 0x000000c9, 0x00000057, 0x0004003d, 0x00000006, 0x00000114, 0x00000113, 0x000500b8, 0x00000047, - 0x00000115, 0x00000112, 0x00000114, 0x000300f7, 0x00000117, 0x00000000, 0x000400fa, 0x00000115, - 0x00000116, 0x00000117, 0x000200f8, 0x00000116, 0x0003003e, 0x0000010a, 0x000000c9, 0x000200f9, - 0x00000117, 0x000200f8, 0x00000117, 0x000200f9, 0x00000110, 0x000200f8, 0x00000110, 0x0004003d, - 0x00000009, 0x0000011a, 0x0000010a, 0x0003003e, 0x00000119, 0x0000011a, 0x00050039, 0x00000006, - 0x0000011b, 0x00000014, 0x00000119, 0x0004003d, 0x00000007, 0x0000011d, 0x00000017, 0x0003003e, - 0x0000011c, 0x0000011d, 0x0004003d, 0x00000009, 0x0000011f, 0x0000010a, 0x0003003e, 0x0000011e, - 0x0000011f, 0x0003003e, 0x00000120, 0x0000011b, 0x00070039, 0x00000006, 0x00000121, 0x00000010, - 0x0000011c, 0x0000011e, 0x00000120, 0x0003003e, 0x00000118, 0x00000121, 0x0004003d, 0x00000006, - 0x00000122, 0x00000118, 0x00060041, 0x000000d7, 0x00000123, 0x00000024, 0x000000c9, 0x00000020, - 0x0004003d, 0x00000006, 0x00000124, 0x00000123, 0x0008000c, 0x00000006, 0x00000125, 0x00000001, - 0x0000002b, 0x00000124, 0x0000004b, 0x0000002b, 0x0008000c, 0x00000006, 0x00000126, 0x00000001, - 0x0000002e, 0x0000002b, 0x00000122, 0x00000125, 0x0003003e, 0x00000118, 0x00000126, 0x0004003d, - 0x00000006, 0x00000127, 0x00000118, 0x000200fe, 0x00000127, 0x00010038, + 0x00000000, 0x00000001, 0x000d000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x0000013b, + 0x00000154, 0x00000169, 0x00000179, 0x0000017f, 0x0000018c, 0x000001ae, 0x000001b9, 0x00030010, + 0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x00040005, 0x00000004, 0x6e69616d, + 0x00000000, 0x000a0005, 0x00000010, 0x706d6173, 0x6853656c, 0x776f6461, 0x63736143, 0x28656461, + 0x3b336676, 0x663b3169, 0x00003b31, 0x00050005, 0x0000000d, 0x6c726f77, 0x736f5064, 0x00000000, + 0x00040005, 0x0000000e, 0x63736163, 0x00656461, 0x00040005, 0x0000000f, 0x4263646e, 0x00736169, + 0x00070005, 0x00000014, 0x63736163, 0x4e656461, 0x69426364, 0x69287361, 0x00003b31, 0x00040005, + 0x00000013, 0x63736163, 0x00656461, 0x00090005, 0x00000019, 0x706d6173, 0x7247656c, 0x53737361, + 0x6f646168, 0x66762877, 0x31663b33, 0x0000003b, 0x00050005, 0x00000017, 0x6c726f77, 0x736f5064, + 0x00000000, 0x00050005, 0x00000018, 0x77656976, 0x74706544, 0x00000068, 0x00050005, 0x0000001d, + 0x6867696c, 0x696c4374, 0x00000070, 0x00050005, 0x00000022, 0x64616853, 0x7246776f, 0x00656d61, + 0x00050006, 0x00000022, 0x00000000, 0x6867696c, 0x00505674, 0x00050006, 0x00000022, 0x00000001, + 0x696c7073, 0x00007374, 0x00050006, 0x00000022, 0x00000002, 0x73616962, 0x00000000, 0x00060006, + 0x00000022, 0x00000003, 0x63736163, 0x42656461, 0x00736169, 0x00070006, 0x00000022, 0x00000004, + 0x63736163, 0x54656461, 0x6c657865, 0x00000000, 0x00040005, 0x00000024, 0x64616873, 0x0000776f, + 0x00030005, 0x00000031, 0x0063646e, 0x00030005, 0x0000003c, 0x00007675, 0x00040005, 0x00000043, + 0x74706564, 0x00000068, 0x00040005, 0x0000006c, 0x65786574, 0x0000006c, 0x00050005, 0x00000070, + 0x64616873, 0x614d776f, 0x00000070, 0x00030005, 0x0000007a, 0x006d7573, 0x00040005, 0x0000007b, + 0x6665727a, 0x00000000, 0x00030005, 0x0000007f, 0x00000069, 0x00030005, 0x00000088, 0x00000073, + 0x00050005, 0x0000009b, 0x65646e69, 0x6c626178, 0x00000065, 0x00030005, 0x000000d0, 0x00000062, + 0x00040005, 0x0000010a, 0x63736163, 0x00656461, 0x00030005, 0x00000118, 0x00736976, 0x00040005, + 0x00000119, 0x61726170, 0x0000006d, 0x00040005, 0x0000011c, 0x61726170, 0x0000006d, 0x00040005, + 0x0000011e, 0x61726170, 0x0000006d, 0x00040005, 0x00000120, 0x61726170, 0x0000006d, 0x00040005, + 0x0000012a, 0x6d617266, 0x00007365, 0x00050005, 0x0000012d, 0x65747845, 0x6c616e72, 0x00000073, + 0x00050006, 0x0000012d, 0x00000000, 0x61746164, 0x00000000, 0x00030005, 0x0000012f, 0x00000075, + 0x00050005, 0x00000135, 0x61727564, 0x6e6f6974, 0x00000000, 0x00040005, 0x00000139, 0x68736168, + 0x00000000, 0x00050005, 0x0000013b, 0x736e4976, 0x636e6174, 0x00644965, 0x00030005, 0x00000143, + 0x00000074, 0x00040005, 0x0000014c, 0x6d617266, 0x00000065, 0x00040005, 0x00000152, 0x616c7461, + 0x00565573, 0x00030005, 0x00000154, 0x00565576, 0x00040005, 0x00000160, 0x6b73616d, 0x00000000, + 0x00060005, 0x00000164, 0x65626c61, 0x61536f64, 0x656c706d, 0x00000072, 0x00040005, 0x00000169, + 0x6e695476, 0x00000074, 0x00050005, 0x00000177, 0x77656976, 0x74706544, 0x00000068, 0x00050005, + 0x00000179, 0x65695676, 0x736f5077, 0x00000000, 0x00030005, 0x0000017e, 0x00736976, 0x00050005, + 0x0000017f, 0x6f6f5276, 0x736f5074, 0x00000000, 0x00040005, 0x00000180, 0x61726170, 0x0000006d, + 0x00040005, 0x00000182, 0x61726170, 0x0000006d, 0x00050005, 0x0000018c, 0x776c4176, 0x44737961, + 0x006b7261, 0x00050005, 0x00000192, 0x6867696c, 0x65724774, 0x00006e65, 0x00050005, 0x0000019d, + 0x6b726164, 0x65657247, 0x0000006e, 0x00050005, 0x000001a7, 0x73617267, 0x6c6f4373, 0x00000000, + 0x00050005, 0x000001ae, 0x4374756f, 0x726f6c6f, 0x00000000, 0x00050005, 0x000001b9, 0x726f5776, + 0x6f50646c, 0x00000073, 0x00040005, 0x000001ba, 0x6867694c, 0x00443374, 0x00060006, 0x000001ba, + 0x00000000, 0x52736f70, 0x75696461, 0x00000073, 0x00050006, 0x000001ba, 0x00000001, 0x6f6c6f63, + 0x00000072, 0x00040005, 0x000001bd, 0x6d617246, 0x00000065, 0x00040006, 0x000001bd, 0x00000000, + 0x0070766d, 0x00050006, 0x000001bd, 0x00000001, 0x65646f6d, 0x0000006c, 0x00080006, 0x000001bd, + 0x00000002, 0x6867696c, 0x72694474, 0x65746e49, 0x7469736e, 0x00000079, 0x00060006, 0x000001bd, + 0x00000003, 0x6867696c, 0x6c6f4374, 0x0000726f, 0x00050006, 0x000001bd, 0x00000004, 0x746e6974, + 0x00000000, 0x00060006, 0x000001bd, 0x00000005, 0x656d6163, 0x6f506172, 0x00000073, 0x00050006, + 0x000001bd, 0x00000006, 0x69626d61, 0x00746e65, 0x00050006, 0x000001bd, 0x00000007, 0x6867696c, + 0x00007374, 0x00050006, 0x000001bd, 0x00000008, 0x42786574, 0x00626d6f, 0x00060006, 0x000001bd, + 0x00000009, 0x61726170, 0x78616c6c, 0x00000000, 0x00050006, 0x000001bd, 0x0000000a, 0x77656976, + 0x00000000, 0x00060006, 0x000001bd, 0x0000000b, 0x70696c63, 0x6f666e49, 0x00000000, 0x00030005, + 0x000001bf, 0x006f6275, 0x00040047, 0x00000021, 0x00000006, 0x00000040, 0x00030047, 0x00000022, + 0x00000002, 0x00040048, 0x00000022, 0x00000000, 0x00000005, 0x00050048, 0x00000022, 0x00000000, + 0x00000007, 0x00000010, 0x00050048, 0x00000022, 0x00000000, 0x00000023, 0x00000000, 0x00050048, + 0x00000022, 0x00000001, 0x00000023, 0x000000c0, 0x00050048, 0x00000022, 0x00000002, 0x00000023, + 0x000000d0, 0x00050048, 0x00000022, 0x00000003, 0x00000023, 0x000000e0, 0x00050048, 0x00000022, + 0x00000004, 0x00000023, 0x000000f0, 0x00040047, 0x00000024, 0x00000021, 0x00000004, 0x00040047, + 0x00000024, 0x00000022, 0x00000000, 0x00040047, 0x00000070, 0x00000021, 0x00000005, 0x00040047, + 0x00000070, 0x00000022, 0x00000000, 0x00040047, 0x0000012c, 0x00000006, 0x00000004, 0x00030047, + 0x0000012d, 0x00000002, 0x00050048, 0x0000012d, 0x00000000, 0x00000023, 0x00000000, 0x00040047, + 0x0000013b, 0x0000001e, 0x00000004, 0x00040047, 0x00000154, 0x0000001e, 0x00000000, 0x00040047, + 0x00000164, 0x00000021, 0x00000001, 0x00040047, 0x00000164, 0x00000022, 0x00000000, 0x00040047, + 0x00000169, 0x0000001e, 0x00000005, 0x00040047, 0x00000179, 0x0000001e, 0x00000002, 0x00040047, + 0x0000017f, 0x0000001e, 0x00000003, 0x00040047, 0x0000018c, 0x0000001e, 0x00000006, 0x00040047, + 0x000001ae, 0x0000001e, 0x00000000, 0x00040047, 0x000001b9, 0x0000001e, 0x00000001, 0x00050048, + 0x000001ba, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x000001ba, 0x00000001, 0x00000023, + 0x00000010, 0x00040047, 0x000001bc, 0x00000006, 0x00000020, 0x00030047, 0x000001bd, 0x00000002, + 0x00040048, 0x000001bd, 0x00000000, 0x00000005, 0x00050048, 0x000001bd, 0x00000000, 0x00000007, + 0x00000010, 0x00050048, 0x000001bd, 0x00000000, 0x00000023, 0x00000000, 0x00040048, 0x000001bd, + 0x00000001, 0x00000005, 0x00050048, 0x000001bd, 0x00000001, 0x00000007, 0x00000010, 0x00050048, + 0x000001bd, 0x00000001, 0x00000023, 0x00000040, 0x00050048, 0x000001bd, 0x00000002, 0x00000023, + 0x00000080, 0x00050048, 0x000001bd, 0x00000003, 0x00000023, 0x00000090, 0x00050048, 0x000001bd, + 0x00000004, 0x00000023, 0x000000a0, 0x00050048, 0x000001bd, 0x00000005, 0x00000023, 0x000000b0, + 0x00050048, 0x000001bd, 0x00000006, 0x00000023, 0x000000c0, 0x00050048, 0x000001bd, 0x00000007, + 0x00000023, 0x000000d0, 0x00050048, 0x000001bd, 0x00000008, 0x00000023, 0x000001d0, 0x00050048, + 0x000001bd, 0x00000009, 0x00000023, 0x000001e0, 0x00040048, 0x000001bd, 0x0000000a, 0x00000005, + 0x00050048, 0x000001bd, 0x0000000a, 0x00000007, 0x00000010, 0x00050048, 0x000001bd, 0x0000000a, + 0x00000023, 0x000001f0, 0x00050048, 0x000001bd, 0x0000000b, 0x00000023, 0x00000230, 0x00040047, + 0x000001bf, 0x00000021, 0x00000000, 0x00040047, 0x000001bf, 0x00000022, 0x00000000, 0x00020013, + 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, + 0x00000007, 0x00000006, 0x00000003, 0x00040020, 0x00000008, 0x00000007, 0x00000007, 0x00040015, + 0x00000009, 0x00000020, 0x00000001, 0x00040020, 0x0000000a, 0x00000007, 0x00000009, 0x00040020, + 0x0000000b, 0x00000007, 0x00000006, 0x00060021, 0x0000000c, 0x00000006, 0x00000008, 0x0000000a, + 0x0000000b, 0x00040021, 0x00000012, 0x00000006, 0x0000000a, 0x00050021, 0x00000016, 0x00000006, + 0x00000008, 0x0000000b, 0x00040017, 0x0000001b, 0x00000006, 0x00000004, 0x00040020, 0x0000001c, + 0x00000007, 0x0000001b, 0x00040018, 0x0000001e, 0x0000001b, 0x00000004, 0x00040015, 0x0000001f, + 0x00000020, 0x00000000, 0x0004002b, 0x0000001f, 0x00000020, 0x00000003, 0x0004001c, 0x00000021, + 0x0000001e, 0x00000020, 0x0007001e, 0x00000022, 0x00000021, 0x0000001b, 0x0000001b, 0x0000001b, + 0x0000001b, 0x00040020, 0x00000023, 0x00000002, 0x00000022, 0x0004003b, 0x00000023, 0x00000024, + 0x00000002, 0x0004002b, 0x00000009, 0x00000025, 0x00000000, 0x00040020, 0x00000027, 0x00000002, + 0x0000001e, 0x0004002b, 0x00000006, 0x0000002b, 0x3f800000, 0x0004002b, 0x00000006, 0x00000036, + 0x358637bd, 0x00040017, 0x0000003a, 0x00000006, 0x00000002, 0x00040020, 0x0000003b, 0x00000007, + 0x0000003a, 0x0004002b, 0x00000006, 0x0000003f, 0x3f000000, 0x0004002b, 0x0000001f, 0x00000044, + 0x00000002, 0x00020014, 0x00000047, 0x0004002b, 0x0000001f, 0x00000048, 0x00000000, 0x0004002b, + 0x00000006, 0x0000004b, 0x00000000, 0x0004002b, 0x0000001f, 0x00000057, 0x00000001, 0x00090019, + 0x0000006d, 0x00000006, 0x00000001, 0x00000001, 0x00000001, 0x00000000, 0x00000001, 0x00000000, + 0x0003001b, 0x0000006e, 0x0000006d, 0x00040020, 0x0000006f, 0x00000000, 0x0000006e, 0x0004003b, + 0x0000006f, 0x00000070, 0x00000000, 0x00040017, 0x00000073, 0x00000009, 0x00000003, 0x00040017, + 0x00000075, 0x00000009, 0x00000002, 0x0004002b, 0x00000009, 0x00000086, 0x00000009, 0x0004002b, + 0x0000001f, 0x0000008a, 0x00000009, 0x0004001c, 0x0000008b, 0x0000003a, 0x0000008a, 0x0004002b, + 0x00000006, 0x0000008c, 0xbf000000, 0x0005002c, 0x0000003a, 0x0000008d, 0x0000008c, 0x0000008c, + 0x0004002b, 0x00000006, 0x0000008e, 0xbf19999a, 0x0005002c, 0x0000003a, 0x0000008f, 0x0000004b, + 0x0000008e, 0x0005002c, 0x0000003a, 0x00000090, 0x0000003f, 0x0000008c, 0x0005002c, 0x0000003a, + 0x00000091, 0x0000008e, 0x0000004b, 0x0005002c, 0x0000003a, 0x00000092, 0x0000004b, 0x0000004b, + 0x0004002b, 0x00000006, 0x00000093, 0x3f19999a, 0x0005002c, 0x0000003a, 0x00000094, 0x00000093, + 0x0000004b, 0x0005002c, 0x0000003a, 0x00000095, 0x0000008c, 0x0000003f, 0x0005002c, 0x0000003a, + 0x00000096, 0x0000004b, 0x00000093, 0x0005002c, 0x0000003a, 0x00000097, 0x0000003f, 0x0000003f, + 0x000c002c, 0x0000008b, 0x00000098, 0x0000008d, 0x0000008f, 0x00000090, 0x00000091, 0x00000092, + 0x00000094, 0x00000095, 0x00000096, 0x00000097, 0x00040020, 0x0000009a, 0x00000007, 0x0000008b, + 0x0004002b, 0x00000009, 0x000000c9, 0x00000001, 0x0004002b, 0x00000006, 0x000000cc, 0x41100000, + 0x0004002b, 0x00000009, 0x000000d6, 0x00000003, 0x00040020, 0x000000d7, 0x00000002, 0x00000006, + 0x0004002b, 0x00000006, 0x000000e8, 0x322bcc77, 0x0004002b, 0x00000009, 0x000000ef, 0x00000002, + 0x0004002b, 0x00000006, 0x00000104, 0x38d1b717, 0x0004002b, 0x0000001f, 0x0000012b, 0x00000020, + 0x0004001c, 0x0000012c, 0x00000006, 0x0000012b, 0x0003001e, 0x0000012d, 0x0000012c, 0x00040020, + 0x0000012e, 0x00000009, 0x0000012d, 0x0004003b, 0x0000012e, 0x0000012f, 0x00000009, 0x0004002b, + 0x00000009, 0x00000130, 0x0000000c, 0x00040020, 0x00000131, 0x00000009, 0x00000006, 0x00040020, + 0x0000013a, 0x00000001, 0x00000006, 0x0004003b, 0x0000013a, 0x0000013b, 0x00000001, 0x0004002b, + 0x00000006, 0x0000013d, 0x414fd639, 0x0004002b, 0x00000006, 0x00000140, 0x472aee8c, 0x00040020, + 0x00000153, 0x00000001, 0x0000003a, 0x0004003b, 0x00000153, 0x00000154, 0x00000001, 0x00090019, + 0x00000161, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000, + 0x0003001b, 0x00000162, 0x00000161, 0x00040020, 0x00000163, 0x00000000, 0x00000162, 0x0004003b, + 0x00000163, 0x00000164, 0x00000000, 0x00040020, 0x00000168, 0x00000001, 0x0000001b, 0x0004003b, + 0x00000168, 0x00000169, 0x00000001, 0x0004002b, 0x00000009, 0x0000016e, 0x00000004, 0x0004002b, + 0x00000006, 0x00000171, 0x3c23d70a, 0x00040020, 0x00000178, 0x00000001, 0x00000007, 0x0004003b, + 0x00000178, 0x00000179, 0x00000001, 0x0004003b, 0x00000178, 0x0000017f, 0x00000001, 0x0004002b, + 0x00000009, 0x00000185, 0x00000005, 0x0004003b, 0x0000013a, 0x0000018c, 0x00000001, 0x0004002b, + 0x00000009, 0x00000193, 0x00000006, 0x0004002b, 0x00000009, 0x00000196, 0x00000007, 0x0004002b, + 0x00000009, 0x00000199, 0x00000008, 0x0004002b, 0x00000009, 0x000001a0, 0x0000000a, 0x0004002b, + 0x00000009, 0x000001a3, 0x0000000b, 0x00040020, 0x000001ad, 0x00000003, 0x0000001b, 0x0004003b, + 0x000001ad, 0x000001ae, 0x00000003, 0x0004003b, 0x00000178, 0x000001b9, 0x00000001, 0x0004001e, + 0x000001ba, 0x0000001b, 0x0000001b, 0x0004002b, 0x0000001f, 0x000001bb, 0x00000008, 0x0004001c, + 0x000001bc, 0x000001ba, 0x000001bb, 0x000e001e, 0x000001bd, 0x0000001e, 0x0000001e, 0x0000001b, + 0x0000001b, 0x0000001b, 0x0000001b, 0x0000001b, 0x000001bc, 0x0000001b, 0x0000001b, 0x0000001e, + 0x0000001b, 0x00040020, 0x000001be, 0x00000002, 0x000001bd, 0x0004003b, 0x000001be, 0x000001bf, + 0x00000002, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, + 0x0004003b, 0x0000000b, 0x0000012a, 0x00000007, 0x0004003b, 0x0000000b, 0x00000135, 0x00000007, + 0x0004003b, 0x0000000b, 0x00000139, 0x00000007, 0x0004003b, 0x0000000b, 0x00000143, 0x00000007, + 0x0004003b, 0x0000000a, 0x0000014c, 0x00000007, 0x0004003b, 0x0000003b, 0x00000152, 0x00000007, + 0x0004003b, 0x0000001c, 0x00000160, 0x00000007, 0x0004003b, 0x0000000b, 0x00000177, 0x00000007, + 0x0004003b, 0x0000000b, 0x0000017e, 0x00000007, 0x0004003b, 0x00000008, 0x00000180, 0x00000007, + 0x0004003b, 0x0000000b, 0x00000182, 0x00000007, 0x0004003b, 0x00000008, 0x00000192, 0x00000007, + 0x0004003b, 0x00000008, 0x0000019d, 0x00000007, 0x0004003b, 0x00000008, 0x000001a7, 0x00000007, + 0x00060041, 0x00000131, 0x00000132, 0x0000012f, 0x00000025, 0x00000130, 0x0004003d, 0x00000006, + 0x00000133, 0x00000132, 0x0007000c, 0x00000006, 0x00000134, 0x00000001, 0x00000028, 0x00000133, + 0x0000002b, 0x0003003e, 0x0000012a, 0x00000134, 0x00060041, 0x00000131, 0x00000136, 0x0000012f, + 0x00000025, 0x000000c9, 0x0004003d, 0x00000006, 0x00000137, 0x00000136, 0x0007000c, 0x00000006, + 0x00000138, 0x00000001, 0x00000028, 0x00000137, 0x00000104, 0x0003003e, 0x00000135, 0x00000138, + 0x0004003d, 0x00000006, 0x0000013c, 0x0000013b, 0x00050085, 0x00000006, 0x0000013e, 0x0000013c, + 0x0000013d, 0x0006000c, 0x00000006, 0x0000013f, 0x00000001, 0x0000000d, 0x0000013e, 0x00050085, + 0x00000006, 0x00000141, 0x0000013f, 0x00000140, 0x0006000c, 0x00000006, 0x00000142, 0x00000001, + 0x0000000a, 0x00000141, 0x0003003e, 0x00000139, 0x00000142, 0x00060041, 0x00000131, 0x00000144, + 0x0000012f, 0x00000025, 0x00000025, 0x0004003d, 0x00000006, 0x00000145, 0x00000144, 0x0004003d, + 0x00000006, 0x00000146, 0x00000135, 0x00050088, 0x00000006, 0x00000147, 0x00000145, 0x00000146, + 0x0004003d, 0x00000006, 0x00000148, 0x00000139, 0x0004003d, 0x00000006, 0x00000149, 0x0000012a, + 0x00050085, 0x00000006, 0x0000014a, 0x00000148, 0x00000149, 0x00050081, 0x00000006, 0x0000014b, + 0x00000147, 0x0000014a, 0x0003003e, 0x00000143, 0x0000014b, 0x0004003d, 0x00000006, 0x0000014d, + 0x00000143, 0x0006000c, 0x00000006, 0x0000014e, 0x00000001, 0x00000008, 0x0000014d, 0x0004003d, + 0x00000006, 0x0000014f, 0x0000012a, 0x0005008d, 0x00000006, 0x00000150, 0x0000014e, 0x0000014f, + 0x0004006e, 0x00000009, 0x00000151, 0x00000150, 0x0003003e, 0x0000014c, 0x00000151, 0x00050041, + 0x0000013a, 0x00000155, 0x00000154, 0x00000048, 0x0004003d, 0x00000006, 0x00000156, 0x00000155, + 0x0004003d, 0x00000009, 0x00000157, 0x0000014c, 0x0004006f, 0x00000006, 0x00000158, 0x00000157, + 0x00050081, 0x00000006, 0x00000159, 0x00000156, 0x00000158, 0x0004003d, 0x00000006, 0x0000015a, + 0x0000012a, 0x00050088, 0x00000006, 0x0000015b, 0x00000159, 0x0000015a, 0x00050041, 0x0000013a, + 0x0000015c, 0x00000154, 0x00000057, 0x0004003d, 0x00000006, 0x0000015d, 0x0000015c, 0x00050083, + 0x00000006, 0x0000015e, 0x0000002b, 0x0000015d, 0x00050050, 0x0000003a, 0x0000015f, 0x0000015b, + 0x0000015e, 0x0003003e, 0x00000152, 0x0000015f, 0x0004003d, 0x00000162, 0x00000165, 0x00000164, + 0x0004003d, 0x0000003a, 0x00000166, 0x00000152, 0x00050057, 0x0000001b, 0x00000167, 0x00000165, + 0x00000166, 0x0004003d, 0x0000001b, 0x0000016a, 0x00000169, 0x00050085, 0x0000001b, 0x0000016b, + 0x00000167, 0x0000016a, 0x0003003e, 0x00000160, 0x0000016b, 0x00050041, 0x0000000b, 0x0000016c, + 0x00000160, 0x00000020, 0x0004003d, 0x00000006, 0x0000016d, 0x0000016c, 0x00060041, 0x00000131, + 0x0000016f, 0x0000012f, 0x00000025, 0x0000016e, 0x0004003d, 0x00000006, 0x00000170, 0x0000016f, + 0x0007000c, 0x00000006, 0x00000172, 0x00000001, 0x00000028, 0x00000170, 0x00000171, 0x000500b8, + 0x00000047, 0x00000173, 0x0000016d, 0x00000172, 0x000300f7, 0x00000175, 0x00000000, 0x000400fa, + 0x00000173, 0x00000174, 0x00000175, 0x000200f8, 0x00000174, 0x000100fc, 0x000200f8, 0x00000175, + 0x00050041, 0x0000013a, 0x0000017a, 0x00000179, 0x00000044, 0x0004003d, 0x00000006, 0x0000017b, + 0x0000017a, 0x0004007f, 0x00000006, 0x0000017c, 0x0000017b, 0x0007000c, 0x00000006, 0x0000017d, + 0x00000001, 0x00000028, 0x0000017c, 0x0000004b, 0x0003003e, 0x00000177, 0x0000017d, 0x0004003d, + 0x00000007, 0x00000181, 0x0000017f, 0x0003003e, 0x00000180, 0x00000181, 0x0004003d, 0x00000006, + 0x00000183, 0x00000177, 0x0003003e, 0x00000182, 0x00000183, 0x00060039, 0x00000006, 0x00000184, + 0x00000019, 0x00000180, 0x00000182, 0x0003003e, 0x0000017e, 0x00000184, 0x00060041, 0x00000131, + 0x00000186, 0x0000012f, 0x00000025, 0x00000185, 0x0004003d, 0x00000006, 0x00000187, 0x00000186, + 0x000500ba, 0x00000047, 0x00000188, 0x00000187, 0x0000003f, 0x000400a8, 0x00000047, 0x00000189, + 0x00000188, 0x000300f7, 0x0000018b, 0x00000000, 0x000400fa, 0x00000189, 0x0000018a, 0x0000018b, + 0x000200f8, 0x0000018a, 0x0004003d, 0x00000006, 0x0000018d, 0x0000018c, 0x000500ba, 0x00000047, + 0x0000018e, 0x0000018d, 0x0000003f, 0x000200f9, 0x0000018b, 0x000200f8, 0x0000018b, 0x000700f5, + 0x00000047, 0x0000018f, 0x00000188, 0x00000175, 0x0000018e, 0x0000018a, 0x000300f7, 0x00000191, + 0x00000000, 0x000400fa, 0x0000018f, 0x00000190, 0x00000191, 0x000200f8, 0x00000190, 0x0003003e, + 0x0000017e, 0x0000004b, 0x000200f9, 0x00000191, 0x000200f8, 0x00000191, 0x00060041, 0x00000131, + 0x00000194, 0x0000012f, 0x00000025, 0x00000193, 0x0004003d, 0x00000006, 0x00000195, 0x00000194, + 0x00060041, 0x00000131, 0x00000197, 0x0000012f, 0x00000025, 0x00000196, 0x0004003d, 0x00000006, + 0x00000198, 0x00000197, 0x00060041, 0x00000131, 0x0000019a, 0x0000012f, 0x00000025, 0x00000199, + 0x0004003d, 0x00000006, 0x0000019b, 0x0000019a, 0x00060050, 0x00000007, 0x0000019c, 0x00000195, + 0x00000198, 0x0000019b, 0x0003003e, 0x00000192, 0x0000019c, 0x00060041, 0x00000131, 0x0000019e, + 0x0000012f, 0x00000025, 0x00000086, 0x0004003d, 0x00000006, 0x0000019f, 0x0000019e, 0x00060041, + 0x00000131, 0x000001a1, 0x0000012f, 0x00000025, 0x000001a0, 0x0004003d, 0x00000006, 0x000001a2, + 0x000001a1, 0x00060041, 0x00000131, 0x000001a4, 0x0000012f, 0x00000025, 0x000001a3, 0x0004003d, + 0x00000006, 0x000001a5, 0x000001a4, 0x00060050, 0x00000007, 0x000001a6, 0x0000019f, 0x000001a2, + 0x000001a5, 0x0003003e, 0x0000019d, 0x000001a6, 0x0004003d, 0x00000007, 0x000001a8, 0x0000019d, + 0x0004003d, 0x00000007, 0x000001a9, 0x00000192, 0x0004003d, 0x00000006, 0x000001aa, 0x0000017e, + 0x00060050, 0x00000007, 0x000001ab, 0x000001aa, 0x000001aa, 0x000001aa, 0x0008000c, 0x00000007, + 0x000001ac, 0x00000001, 0x0000002e, 0x000001a8, 0x000001a9, 0x000001ab, 0x0003003e, 0x000001a7, + 0x000001ac, 0x0004003d, 0x00000007, 0x000001af, 0x000001a7, 0x0004003d, 0x0000001b, 0x000001b0, + 0x00000160, 0x0008004f, 0x00000007, 0x000001b1, 0x000001b0, 0x000001b0, 0x00000000, 0x00000001, + 0x00000002, 0x00050085, 0x00000007, 0x000001b2, 0x000001af, 0x000001b1, 0x00050041, 0x0000000b, + 0x000001b3, 0x00000160, 0x00000020, 0x0004003d, 0x00000006, 0x000001b4, 0x000001b3, 0x00050051, + 0x00000006, 0x000001b5, 0x000001b2, 0x00000000, 0x00050051, 0x00000006, 0x000001b6, 0x000001b2, + 0x00000001, 0x00050051, 0x00000006, 0x000001b7, 0x000001b2, 0x00000002, 0x00070050, 0x0000001b, + 0x000001b8, 0x000001b5, 0x000001b6, 0x000001b7, 0x000001b4, 0x0003003e, 0x000001ae, 0x000001b8, + 0x000100fd, 0x00010038, 0x00050036, 0x00000006, 0x00000010, 0x00000000, 0x0000000c, 0x00030037, + 0x00000008, 0x0000000d, 0x00030037, 0x0000000a, 0x0000000e, 0x00030037, 0x0000000b, 0x0000000f, + 0x000200f8, 0x00000011, 0x0004003b, 0x0000001c, 0x0000001d, 0x00000007, 0x0004003b, 0x00000008, + 0x00000031, 0x00000007, 0x0004003b, 0x0000003b, 0x0000003c, 0x00000007, 0x0004003b, 0x0000000b, + 0x00000043, 0x00000007, 0x0004003b, 0x0000003b, 0x0000006c, 0x00000007, 0x0004003b, 0x0000000b, + 0x0000007a, 0x00000007, 0x0004003b, 0x0000000b, 0x0000007b, 0x00000007, 0x0004003b, 0x0000000a, + 0x0000007f, 0x00000007, 0x0004003b, 0x0000003b, 0x00000088, 0x00000007, 0x0004003b, 0x0000009a, + 0x0000009b, 0x00000007, 0x0004003d, 0x00000009, 0x00000026, 0x0000000e, 0x00060041, 0x00000027, + 0x00000028, 0x00000024, 0x00000025, 0x00000026, 0x0004003d, 0x0000001e, 0x00000029, 0x00000028, + 0x0004003d, 0x00000007, 0x0000002a, 0x0000000d, 0x00050051, 0x00000006, 0x0000002c, 0x0000002a, + 0x00000000, 0x00050051, 0x00000006, 0x0000002d, 0x0000002a, 0x00000001, 0x00050051, 0x00000006, + 0x0000002e, 0x0000002a, 0x00000002, 0x00070050, 0x0000001b, 0x0000002f, 0x0000002c, 0x0000002d, + 0x0000002e, 0x0000002b, 0x00050091, 0x0000001b, 0x00000030, 0x00000029, 0x0000002f, 0x0003003e, + 0x0000001d, 0x00000030, 0x0004003d, 0x0000001b, 0x00000032, 0x0000001d, 0x0008004f, 0x00000007, + 0x00000033, 0x00000032, 0x00000032, 0x00000000, 0x00000001, 0x00000002, 0x00050041, 0x0000000b, + 0x00000034, 0x0000001d, 0x00000020, 0x0004003d, 0x00000006, 0x00000035, 0x00000034, 0x0007000c, + 0x00000006, 0x00000037, 0x00000001, 0x00000028, 0x00000035, 0x00000036, 0x00060050, 0x00000007, + 0x00000038, 0x00000037, 0x00000037, 0x00000037, 0x00050088, 0x00000007, 0x00000039, 0x00000033, + 0x00000038, 0x0003003e, 0x00000031, 0x00000039, 0x0004003d, 0x00000007, 0x0000003d, 0x00000031, + 0x0007004f, 0x0000003a, 0x0000003e, 0x0000003d, 0x0000003d, 0x00000000, 0x00000001, 0x0005008e, + 0x0000003a, 0x00000040, 0x0000003e, 0x0000003f, 0x00050050, 0x0000003a, 0x00000041, 0x0000003f, + 0x0000003f, 0x00050081, 0x0000003a, 0x00000042, 0x00000040, 0x00000041, 0x0003003e, 0x0000003c, + 0x00000042, 0x00050041, 0x0000000b, 0x00000045, 0x00000031, 0x00000044, 0x0004003d, 0x00000006, + 0x00000046, 0x00000045, 0x0003003e, 0x00000043, 0x00000046, 0x00050041, 0x0000000b, 0x00000049, + 0x0000003c, 0x00000048, 0x0004003d, 0x00000006, 0x0000004a, 0x00000049, 0x000500b8, 0x00000047, + 0x0000004c, 0x0000004a, 0x0000004b, 0x000400a8, 0x00000047, 0x0000004d, 0x0000004c, 0x000300f7, + 0x0000004f, 0x00000000, 0x000400fa, 0x0000004d, 0x0000004e, 0x0000004f, 0x000200f8, 0x0000004e, + 0x00050041, 0x0000000b, 0x00000050, 0x0000003c, 0x00000048, 0x0004003d, 0x00000006, 0x00000051, + 0x00000050, 0x000500ba, 0x00000047, 0x00000052, 0x00000051, 0x0000002b, 0x000200f9, 0x0000004f, + 0x000200f8, 0x0000004f, 0x000700f5, 0x00000047, 0x00000053, 0x0000004c, 0x00000011, 0x00000052, + 0x0000004e, 0x000400a8, 0x00000047, 0x00000054, 0x00000053, 0x000300f7, 0x00000056, 0x00000000, + 0x000400fa, 0x00000054, 0x00000055, 0x00000056, 0x000200f8, 0x00000055, 0x00050041, 0x0000000b, + 0x00000058, 0x0000003c, 0x00000057, 0x0004003d, 0x00000006, 0x00000059, 0x00000058, 0x000500b8, + 0x00000047, 0x0000005a, 0x00000059, 0x0000004b, 0x000200f9, 0x00000056, 0x000200f8, 0x00000056, + 0x000700f5, 0x00000047, 0x0000005b, 0x00000053, 0x0000004f, 0x0000005a, 0x00000055, 0x000400a8, + 0x00000047, 0x0000005c, 0x0000005b, 0x000300f7, 0x0000005e, 0x00000000, 0x000400fa, 0x0000005c, + 0x0000005d, 0x0000005e, 0x000200f8, 0x0000005d, 0x00050041, 0x0000000b, 0x0000005f, 0x0000003c, + 0x00000057, 0x0004003d, 0x00000006, 0x00000060, 0x0000005f, 0x000500ba, 0x00000047, 0x00000061, + 0x00000060, 0x0000002b, 0x000200f9, 0x0000005e, 0x000200f8, 0x0000005e, 0x000700f5, 0x00000047, + 0x00000062, 0x0000005b, 0x00000056, 0x00000061, 0x0000005d, 0x0004003d, 0x00000006, 0x00000063, + 0x00000043, 0x000500b8, 0x00000047, 0x00000064, 0x00000063, 0x0000004b, 0x000500a6, 0x00000047, + 0x00000065, 0x00000062, 0x00000064, 0x0004003d, 0x00000006, 0x00000066, 0x00000043, 0x000500ba, + 0x00000047, 0x00000067, 0x00000066, 0x0000002b, 0x000500a6, 0x00000047, 0x00000068, 0x00000065, + 0x00000067, 0x000300f7, 0x0000006a, 0x00000000, 0x000400fa, 0x00000068, 0x00000069, 0x0000006a, + 0x000200f8, 0x00000069, 0x000200fe, 0x0000002b, 0x000200f8, 0x0000006a, 0x0004003d, 0x0000006e, + 0x00000071, 0x00000070, 0x00040064, 0x0000006d, 0x00000072, 0x00000071, 0x00050067, 0x00000073, + 0x00000074, 0x00000072, 0x00000025, 0x0007004f, 0x00000075, 0x00000076, 0x00000074, 0x00000074, + 0x00000000, 0x00000001, 0x0004006f, 0x0000003a, 0x00000077, 0x00000076, 0x00050050, 0x0000003a, + 0x00000078, 0x0000002b, 0x0000002b, 0x00050088, 0x0000003a, 0x00000079, 0x00000078, 0x00000077, + 0x0003003e, 0x0000006c, 0x00000079, 0x0003003e, 0x0000007a, 0x0000004b, 0x0004003d, 0x00000006, + 0x0000007c, 0x00000043, 0x0004003d, 0x00000006, 0x0000007d, 0x0000000f, 0x00050083, 0x00000006, + 0x0000007e, 0x0000007c, 0x0000007d, 0x0003003e, 0x0000007b, 0x0000007e, 0x0003003e, 0x0000007f, + 0x00000025, 0x000200f9, 0x00000080, 0x000200f8, 0x00000080, 0x000400f6, 0x00000082, 0x00000083, + 0x00000000, 0x000200f9, 0x00000084, 0x000200f8, 0x00000084, 0x0004003d, 0x00000009, 0x00000085, + 0x0000007f, 0x000500b1, 0x00000047, 0x00000087, 0x00000085, 0x00000086, 0x000400fa, 0x00000087, + 0x00000081, 0x00000082, 0x000200f8, 0x00000081, 0x0004003d, 0x0000003a, 0x00000089, 0x0000003c, + 0x0004003d, 0x00000009, 0x00000099, 0x0000007f, 0x0003003e, 0x0000009b, 0x00000098, 0x00050041, + 0x0000003b, 0x0000009c, 0x0000009b, 0x00000099, 0x0004003d, 0x0000003a, 0x0000009d, 0x0000009c, + 0x0004003d, 0x0000003a, 0x0000009e, 0x0000006c, 0x00050085, 0x0000003a, 0x0000009f, 0x0000009d, + 0x0000009e, 0x00050081, 0x0000003a, 0x000000a0, 0x00000089, 0x0000009f, 0x0003003e, 0x00000088, + 0x000000a0, 0x00050041, 0x0000000b, 0x000000a1, 0x00000088, 0x00000048, 0x0004003d, 0x00000006, + 0x000000a2, 0x000000a1, 0x000500b8, 0x00000047, 0x000000a3, 0x000000a2, 0x0000004b, 0x000400a8, + 0x00000047, 0x000000a4, 0x000000a3, 0x000300f7, 0x000000a6, 0x00000000, 0x000400fa, 0x000000a4, + 0x000000a5, 0x000000a6, 0x000200f8, 0x000000a5, 0x00050041, 0x0000000b, 0x000000a7, 0x00000088, + 0x00000048, 0x0004003d, 0x00000006, 0x000000a8, 0x000000a7, 0x000500ba, 0x00000047, 0x000000a9, + 0x000000a8, 0x0000002b, 0x000200f9, 0x000000a6, 0x000200f8, 0x000000a6, 0x000700f5, 0x00000047, + 0x000000aa, 0x000000a3, 0x00000081, 0x000000a9, 0x000000a5, 0x000400a8, 0x00000047, 0x000000ab, + 0x000000aa, 0x000300f7, 0x000000ad, 0x00000000, 0x000400fa, 0x000000ab, 0x000000ac, 0x000000ad, + 0x000200f8, 0x000000ac, 0x00050041, 0x0000000b, 0x000000ae, 0x00000088, 0x00000057, 0x0004003d, + 0x00000006, 0x000000af, 0x000000ae, 0x000500b8, 0x00000047, 0x000000b0, 0x000000af, 0x0000004b, + 0x000200f9, 0x000000ad, 0x000200f8, 0x000000ad, 0x000700f5, 0x00000047, 0x000000b1, 0x000000aa, + 0x000000a6, 0x000000b0, 0x000000ac, 0x000400a8, 0x00000047, 0x000000b2, 0x000000b1, 0x000300f7, + 0x000000b4, 0x00000000, 0x000400fa, 0x000000b2, 0x000000b3, 0x000000b4, 0x000200f8, 0x000000b3, + 0x00050041, 0x0000000b, 0x000000b5, 0x00000088, 0x00000057, 0x0004003d, 0x00000006, 0x000000b6, + 0x000000b5, 0x000500ba, 0x00000047, 0x000000b7, 0x000000b6, 0x0000002b, 0x000200f9, 0x000000b4, + 0x000200f8, 0x000000b4, 0x000700f5, 0x00000047, 0x000000b8, 0x000000b1, 0x000000ad, 0x000000b7, + 0x000000b3, 0x000300f7, 0x000000ba, 0x00000000, 0x000400fa, 0x000000b8, 0x000000b9, 0x000000ba, + 0x000200f8, 0x000000b9, 0x000200f9, 0x00000083, 0x000200f8, 0x000000ba, 0x0004003d, 0x0000006e, + 0x000000bc, 0x00000070, 0x0004003d, 0x0000003a, 0x000000bd, 0x00000088, 0x0004003d, 0x00000009, + 0x000000be, 0x0000000e, 0x0004006f, 0x00000006, 0x000000bf, 0x000000be, 0x0004003d, 0x00000006, + 0x000000c0, 0x0000007b, 0x00050051, 0x00000006, 0x000000c1, 0x000000bd, 0x00000000, 0x00050051, + 0x00000006, 0x000000c2, 0x000000bd, 0x00000001, 0x00070050, 0x0000001b, 0x000000c3, 0x000000c1, + 0x000000c2, 0x000000bf, 0x000000c0, 0x00050051, 0x00000006, 0x000000c4, 0x000000c3, 0x00000003, + 0x00060059, 0x00000006, 0x000000c5, 0x000000bc, 0x000000c3, 0x000000c4, 0x0004003d, 0x00000006, + 0x000000c6, 0x0000007a, 0x00050081, 0x00000006, 0x000000c7, 0x000000c6, 0x000000c5, 0x0003003e, + 0x0000007a, 0x000000c7, 0x000200f9, 0x00000083, 0x000200f8, 0x00000083, 0x0004003d, 0x00000009, + 0x000000c8, 0x0000007f, 0x00050080, 0x00000009, 0x000000ca, 0x000000c8, 0x000000c9, 0x0003003e, + 0x0000007f, 0x000000ca, 0x000200f9, 0x00000080, 0x000200f8, 0x00000082, 0x0004003d, 0x00000006, + 0x000000cb, 0x0000007a, 0x00050088, 0x00000006, 0x000000cd, 0x000000cb, 0x000000cc, 0x000200fe, + 0x000000cd, 0x00010038, 0x00050036, 0x00000006, 0x00000014, 0x00000000, 0x00000012, 0x00030037, + 0x0000000a, 0x00000013, 0x000200f8, 0x00000015, 0x0004003b, 0x0000000b, 0x000000d0, 0x00000007, + 0x0004003b, 0x0000000b, 0x000000d3, 0x00000007, 0x0004003b, 0x0000000b, 0x000000dd, 0x00000007, + 0x0004003b, 0x0000000b, 0x000000ea, 0x00000007, 0x0004003d, 0x00000009, 0x000000d1, 0x00000013, + 0x000500aa, 0x00000047, 0x000000d2, 0x000000d1, 0x00000025, 0x000300f7, 0x000000d5, 0x00000000, + 0x000400fa, 0x000000d2, 0x000000d4, 0x000000da, 0x000200f8, 0x000000d4, 0x00060041, 0x000000d7, + 0x000000d8, 0x00000024, 0x000000d6, 0x00000048, 0x0004003d, 0x00000006, 0x000000d9, 0x000000d8, + 0x0003003e, 0x000000d3, 0x000000d9, 0x000200f9, 0x000000d5, 0x000200f8, 0x000000da, 0x0004003d, + 0x00000009, 0x000000db, 0x00000013, 0x000500aa, 0x00000047, 0x000000dc, 0x000000db, 0x000000c9, + 0x000300f7, 0x000000df, 0x00000000, 0x000400fa, 0x000000dc, 0x000000de, 0x000000e2, 0x000200f8, + 0x000000de, 0x00060041, 0x000000d7, 0x000000e0, 0x00000024, 0x000000d6, 0x00000057, 0x0004003d, + 0x00000006, 0x000000e1, 0x000000e0, 0x0003003e, 0x000000dd, 0x000000e1, 0x000200f9, 0x000000df, + 0x000200f8, 0x000000e2, 0x00060041, 0x000000d7, 0x000000e3, 0x00000024, 0x000000d6, 0x00000044, + 0x0004003d, 0x00000006, 0x000000e4, 0x000000e3, 0x0003003e, 0x000000dd, 0x000000e4, 0x000200f9, + 0x000000df, 0x000200f8, 0x000000df, 0x0004003d, 0x00000006, 0x000000e5, 0x000000dd, 0x0003003e, + 0x000000d3, 0x000000e5, 0x000200f9, 0x000000d5, 0x000200f8, 0x000000d5, 0x0004003d, 0x00000006, + 0x000000e6, 0x000000d3, 0x0003003e, 0x000000d0, 0x000000e6, 0x0004003d, 0x00000006, 0x000000e7, + 0x000000d0, 0x000500ba, 0x00000047, 0x000000e9, 0x000000e7, 0x000000e8, 0x000300f7, 0x000000ec, + 0x00000000, 0x000400fa, 0x000000e9, 0x000000eb, 0x000000ee, 0x000200f8, 0x000000eb, 0x0004003d, + 0x00000006, 0x000000ed, 0x000000d0, 0x0003003e, 0x000000ea, 0x000000ed, 0x000200f9, 0x000000ec, + 0x000200f8, 0x000000ee, 0x00060041, 0x000000d7, 0x000000f0, 0x00000024, 0x000000ef, 0x00000048, + 0x0004003d, 0x00000006, 0x000000f1, 0x000000f0, 0x0003003e, 0x000000ea, 0x000000f1, 0x000200f9, + 0x000000ec, 0x000200f8, 0x000000ec, 0x0004003d, 0x00000006, 0x000000f2, 0x000000ea, 0x000200fe, + 0x000000f2, 0x00010038, 0x00050036, 0x00000006, 0x00000019, 0x00000000, 0x00000016, 0x00030037, + 0x00000008, 0x00000017, 0x00030037, 0x0000000b, 0x00000018, 0x000200f8, 0x0000001a, 0x0004003b, + 0x0000000a, 0x0000010a, 0x00000007, 0x0004003b, 0x0000000b, 0x00000118, 0x00000007, 0x0004003b, + 0x0000000a, 0x00000119, 0x00000007, 0x0004003b, 0x00000008, 0x0000011c, 0x00000007, 0x0004003b, + 0x0000000a, 0x0000011e, 0x00000007, 0x0004003b, 0x0000000b, 0x00000120, 0x00000007, 0x00060041, + 0x000000d7, 0x000000f5, 0x00000024, 0x000000ef, 0x00000057, 0x0004003d, 0x00000006, 0x000000f6, + 0x000000f5, 0x000500b8, 0x00000047, 0x000000f7, 0x000000f6, 0x0000003f, 0x000400a8, 0x00000047, + 0x000000f8, 0x000000f7, 0x000300f7, 0x000000fa, 0x00000000, 0x000400fa, 0x000000f8, 0x000000f9, + 0x000000fa, 0x000200f8, 0x000000f9, 0x00060041, 0x000000d7, 0x000000fb, 0x00000024, 0x000000ef, + 0x00000044, 0x0004003d, 0x00000006, 0x000000fc, 0x000000fb, 0x000500b8, 0x00000047, 0x000000fd, + 0x000000fc, 0x0000003f, 0x000200f9, 0x000000fa, 0x000200f8, 0x000000fa, 0x000700f5, 0x00000047, + 0x000000fe, 0x000000f7, 0x0000001a, 0x000000fd, 0x000000f9, 0x000400a8, 0x00000047, 0x000000ff, + 0x000000fe, 0x000300f7, 0x00000101, 0x00000000, 0x000400fa, 0x000000ff, 0x00000100, 0x00000101, + 0x000200f8, 0x00000100, 0x00060041, 0x000000d7, 0x00000102, 0x00000024, 0x000000c9, 0x00000020, + 0x0004003d, 0x00000006, 0x00000103, 0x00000102, 0x000500b8, 0x00000047, 0x00000105, 0x00000103, + 0x00000104, 0x000200f9, 0x00000101, 0x000200f8, 0x00000101, 0x000700f5, 0x00000047, 0x00000106, + 0x000000fe, 0x000000fa, 0x00000105, 0x00000100, 0x000300f7, 0x00000108, 0x00000000, 0x000400fa, + 0x00000106, 0x00000107, 0x00000108, 0x000200f8, 0x00000107, 0x000200fe, 0x0000002b, 0x000200f8, + 0x00000108, 0x0003003e, 0x0000010a, 0x000000ef, 0x0004003d, 0x00000006, 0x0000010b, 0x00000018, + 0x00060041, 0x000000d7, 0x0000010c, 0x00000024, 0x000000c9, 0x00000048, 0x0004003d, 0x00000006, + 0x0000010d, 0x0000010c, 0x000500b8, 0x00000047, 0x0000010e, 0x0000010b, 0x0000010d, 0x000300f7, + 0x00000110, 0x00000000, 0x000400fa, 0x0000010e, 0x0000010f, 0x00000111, 0x000200f8, 0x0000010f, + 0x0003003e, 0x0000010a, 0x00000025, 0x000200f9, 0x00000110, 0x000200f8, 0x00000111, 0x0004003d, + 0x00000006, 0x00000112, 0x00000018, 0x00060041, 0x000000d7, 0x00000113, 0x00000024, 0x000000c9, + 0x00000057, 0x0004003d, 0x00000006, 0x00000114, 0x00000113, 0x000500b8, 0x00000047, 0x00000115, + 0x00000112, 0x00000114, 0x000300f7, 0x00000117, 0x00000000, 0x000400fa, 0x00000115, 0x00000116, + 0x00000117, 0x000200f8, 0x00000116, 0x0003003e, 0x0000010a, 0x000000c9, 0x000200f9, 0x00000117, + 0x000200f8, 0x00000117, 0x000200f9, 0x00000110, 0x000200f8, 0x00000110, 0x0004003d, 0x00000009, + 0x0000011a, 0x0000010a, 0x0003003e, 0x00000119, 0x0000011a, 0x00050039, 0x00000006, 0x0000011b, + 0x00000014, 0x00000119, 0x0004003d, 0x00000007, 0x0000011d, 0x00000017, 0x0003003e, 0x0000011c, + 0x0000011d, 0x0004003d, 0x00000009, 0x0000011f, 0x0000010a, 0x0003003e, 0x0000011e, 0x0000011f, + 0x0003003e, 0x00000120, 0x0000011b, 0x00070039, 0x00000006, 0x00000121, 0x00000010, 0x0000011c, + 0x0000011e, 0x00000120, 0x0003003e, 0x00000118, 0x00000121, 0x0004003d, 0x00000006, 0x00000122, + 0x00000118, 0x00060041, 0x000000d7, 0x00000123, 0x00000024, 0x000000c9, 0x00000020, 0x0004003d, + 0x00000006, 0x00000124, 0x00000123, 0x0008000c, 0x00000006, 0x00000125, 0x00000001, 0x0000002b, + 0x00000124, 0x0000004b, 0x0000002b, 0x0008000c, 0x00000006, 0x00000126, 0x00000001, 0x0000002e, + 0x0000002b, 0x00000122, 0x00000125, 0x0003003e, 0x00000118, 0x00000126, 0x0004003d, 0x00000006, + 0x00000127, 0x00000118, 0x000200fe, 0x00000127, 0x00010038, }; static const size_t mesh3d_grass_frag_spv_count = sizeof(mesh3d_grass_frag_spv)/sizeof(mesh3d_grass_frag_spv[0]); diff --git a/src/modules/graphics/shaders/mesh3d_grass_vert_spv.inc b/src/modules/graphics/shaders/mesh3d_grass_vert_spv.inc index 278e63b9..808cb910 100644 --- a/src/modules/graphics/shaders/mesh3d_grass_vert_spv.inc +++ b/src/modules/graphics/shaders/mesh3d_grass_vert_spv.inc @@ -1,201 +1,204 @@ static const uint32_t mesh3d_grass_vert_spv[] = { - 0x07230203, 0x00010000, 0x0008000b, 0x000000df, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x07230203, 0x00010000, 0x0008000b, 0x000000e3, 0x00000000, 0x00020011, 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, - 0x000f000f, 0x00000000, 0x00000004, 0x6e69616d, 0x00000000, 0x0000001f, 0x00000065, 0x00000090, - 0x000000bb, 0x000000c7, 0x000000ca, 0x000000cd, 0x000000d4, 0x000000d8, 0x000000db, 0x00030003, - 0x00000002, 0x000001c2, 0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x00040005, 0x00000008, - 0x74646977, 0x00000068, 0x00050005, 0x0000000c, 0x65747845, 0x6c616e72, 0x00000073, 0x00050006, - 0x0000000c, 0x00000000, 0x61746164, 0x00000000, 0x00030005, 0x0000000e, 0x00000075, 0x00040005, - 0x00000017, 0x67696568, 0x00007468, 0x00040005, 0x0000001c, 0x6c616373, 0x00000065, 0x00050005, - 0x0000001f, 0x6f4e6e69, 0x6c616d72, 0x00000000, 0x00050005, 0x00000029, 0x4d766e69, 0x6c65646f, - 0x00000000, 0x00040005, 0x0000002a, 0x6867694c, 0x00443374, 0x00060006, 0x0000002a, 0x00000000, - 0x52736f70, 0x75696461, 0x00000073, 0x00050006, 0x0000002a, 0x00000001, 0x6f6c6f63, 0x00000072, - 0x00040005, 0x0000002d, 0x6d617246, 0x00000065, 0x00040006, 0x0000002d, 0x00000000, 0x0070766d, - 0x00050006, 0x0000002d, 0x00000001, 0x65646f6d, 0x0000006c, 0x00080006, 0x0000002d, 0x00000002, - 0x6867696c, 0x72694474, 0x65746e49, 0x7469736e, 0x00000079, 0x00060006, 0x0000002d, 0x00000003, - 0x6867696c, 0x6c6f4374, 0x0000726f, 0x00050006, 0x0000002d, 0x00000004, 0x746e6974, 0x00000000, - 0x00060006, 0x0000002d, 0x00000005, 0x656d6163, 0x6f506172, 0x00000073, 0x00050006, 0x0000002d, - 0x00000006, 0x69626d61, 0x00746e65, 0x00050006, 0x0000002d, 0x00000007, 0x6867696c, 0x00007374, - 0x00050006, 0x0000002d, 0x00000008, 0x42786574, 0x00626d6f, 0x00060006, 0x0000002d, 0x00000009, - 0x61726170, 0x78616c6c, 0x00000000, 0x00050006, 0x0000002d, 0x0000000a, 0x77656976, 0x00000000, - 0x00060006, 0x0000002d, 0x0000000b, 0x70696c63, 0x6f666e49, 0x00000000, 0x00030005, 0x0000002f, - 0x006f6275, 0x00050005, 0x00000036, 0x6c726f77, 0x4f705564, 0x00006a62, 0x00040005, 0x00000046, - 0x654c7075, 0x0000006e, 0x00040005, 0x00000056, 0x4f6d6163, 0x00006a62, 0x00040005, 0x00000063, - 0x61436f74, 0x0000006d, 0x00040005, 0x00000065, 0x6f506e69, 0x00000073, 0x00050005, 0x0000006f, - 0x61436f74, 0x6e654c6d, 0x00000000, 0x00040005, 0x00000077, 0x68676972, 0x00000074, 0x00050005, - 0x0000007d, 0x6c6c6166, 0x6b636162, 0x00000000, 0x00030005, 0x0000008b, 0x006a626f, 0x00040005, - 0x00000090, 0x56556e69, 0x00000000, 0x00040005, 0x000000a6, 0x6c726f77, 0x00000064, 0x00050005, - 0x000000af, 0x746f6f72, 0x6c726f57, 0x00000064, 0x00060005, 0x000000b9, 0x505f6c67, 0x65567265, - 0x78657472, 0x00000000, 0x00060006, 0x000000b9, 0x00000000, 0x505f6c67, 0x7469736f, 0x006e6f69, - 0x00070006, 0x000000b9, 0x00000001, 0x505f6c67, 0x746e696f, 0x657a6953, 0x00000000, 0x00070006, - 0x000000b9, 0x00000002, 0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006, 0x000000b9, - 0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, 0x00030005, 0x000000bb, 0x00000000, - 0x00030005, 0x000000c7, 0x00565576, 0x00050005, 0x000000ca, 0x726f5776, 0x6f50646c, 0x00000073, - 0x00050005, 0x000000cd, 0x65695676, 0x736f5077, 0x00000000, 0x00050005, 0x000000d4, 0x6f6f5276, - 0x736f5074, 0x00000000, 0x00050005, 0x000000d8, 0x736e4976, 0x636e6174, 0x00644965, 0x00040005, - 0x000000db, 0x6e695476, 0x00000074, 0x00040047, 0x0000000b, 0x00000006, 0x00000004, 0x00030047, - 0x0000000c, 0x00000002, 0x00050048, 0x0000000c, 0x00000000, 0x00000023, 0x00000000, 0x00040047, - 0x0000001f, 0x0000001e, 0x00000001, 0x00050048, 0x0000002a, 0x00000000, 0x00000023, 0x00000000, - 0x00050048, 0x0000002a, 0x00000001, 0x00000023, 0x00000010, 0x00040047, 0x0000002c, 0x00000006, - 0x00000020, 0x00030047, 0x0000002d, 0x00000002, 0x00040048, 0x0000002d, 0x00000000, 0x00000005, - 0x00050048, 0x0000002d, 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x0000002d, 0x00000000, - 0x00000023, 0x00000000, 0x00040048, 0x0000002d, 0x00000001, 0x00000005, 0x00050048, 0x0000002d, - 0x00000001, 0x00000007, 0x00000010, 0x00050048, 0x0000002d, 0x00000001, 0x00000023, 0x00000040, - 0x00050048, 0x0000002d, 0x00000002, 0x00000023, 0x00000080, 0x00050048, 0x0000002d, 0x00000003, - 0x00000023, 0x00000090, 0x00050048, 0x0000002d, 0x00000004, 0x00000023, 0x000000a0, 0x00050048, - 0x0000002d, 0x00000005, 0x00000023, 0x000000b0, 0x00050048, 0x0000002d, 0x00000006, 0x00000023, - 0x000000c0, 0x00050048, 0x0000002d, 0x00000007, 0x00000023, 0x000000d0, 0x00050048, 0x0000002d, - 0x00000008, 0x00000023, 0x000001d0, 0x00050048, 0x0000002d, 0x00000009, 0x00000023, 0x000001e0, - 0x00040048, 0x0000002d, 0x0000000a, 0x00000005, 0x00050048, 0x0000002d, 0x0000000a, 0x00000007, - 0x00000010, 0x00050048, 0x0000002d, 0x0000000a, 0x00000023, 0x000001f0, 0x00050048, 0x0000002d, - 0x0000000b, 0x00000023, 0x00000230, 0x00040047, 0x0000002f, 0x00000021, 0x00000000, 0x00040047, - 0x0000002f, 0x00000022, 0x00000000, 0x00040047, 0x00000065, 0x0000001e, 0x00000000, 0x00040047, - 0x00000090, 0x0000001e, 0x00000002, 0x00030047, 0x000000b9, 0x00000002, 0x00050048, 0x000000b9, - 0x00000000, 0x0000000b, 0x00000000, 0x00050048, 0x000000b9, 0x00000001, 0x0000000b, 0x00000001, - 0x00050048, 0x000000b9, 0x00000002, 0x0000000b, 0x00000003, 0x00050048, 0x000000b9, 0x00000003, - 0x0000000b, 0x00000004, 0x00040047, 0x000000c7, 0x0000001e, 0x00000000, 0x00040047, 0x000000ca, - 0x0000001e, 0x00000001, 0x00040047, 0x000000cd, 0x0000001e, 0x00000002, 0x00040047, 0x000000d4, - 0x0000001e, 0x00000003, 0x00040047, 0x000000d8, 0x0000001e, 0x00000004, 0x00040047, 0x000000db, - 0x0000001e, 0x00000005, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, - 0x00000006, 0x00000020, 0x00040020, 0x00000007, 0x00000007, 0x00000006, 0x00040015, 0x00000009, - 0x00000020, 0x00000000, 0x0004002b, 0x00000009, 0x0000000a, 0x00000020, 0x0004001c, 0x0000000b, - 0x00000006, 0x0000000a, 0x0003001e, 0x0000000c, 0x0000000b, 0x00040020, 0x0000000d, 0x00000009, - 0x0000000c, 0x0004003b, 0x0000000d, 0x0000000e, 0x00000009, 0x00040015, 0x0000000f, 0x00000020, - 0x00000001, 0x0004002b, 0x0000000f, 0x00000010, 0x00000000, 0x0004002b, 0x0000000f, 0x00000011, - 0x00000002, 0x00040020, 0x00000012, 0x00000009, 0x00000006, 0x0004002b, 0x00000006, 0x00000015, - 0x3c23d70a, 0x0004002b, 0x0000000f, 0x00000018, 0x00000003, 0x00040017, 0x0000001d, 0x00000006, - 0x00000003, 0x00040020, 0x0000001e, 0x00000001, 0x0000001d, 0x0004003b, 0x0000001e, 0x0000001f, - 0x00000001, 0x0004002b, 0x00000009, 0x00000020, 0x00000001, 0x00040020, 0x00000021, 0x00000001, - 0x00000006, 0x0004002b, 0x00000006, 0x00000024, 0x3d4ccccd, 0x00040017, 0x00000026, 0x00000006, - 0x00000004, 0x00040018, 0x00000027, 0x00000026, 0x00000004, 0x00040020, 0x00000028, 0x00000007, - 0x00000027, 0x0004001e, 0x0000002a, 0x00000026, 0x00000026, 0x0004002b, 0x00000009, 0x0000002b, - 0x00000008, 0x0004001c, 0x0000002c, 0x0000002a, 0x0000002b, 0x000e001e, 0x0000002d, 0x00000027, - 0x00000027, 0x00000026, 0x00000026, 0x00000026, 0x00000026, 0x00000026, 0x0000002c, 0x00000026, - 0x00000026, 0x00000027, 0x00000026, 0x00040020, 0x0000002e, 0x00000002, 0x0000002d, 0x0004003b, - 0x0000002e, 0x0000002f, 0x00000002, 0x0004002b, 0x0000000f, 0x00000030, 0x00000001, 0x00040020, - 0x00000031, 0x00000002, 0x00000027, 0x00040020, 0x00000035, 0x00000007, 0x0000001d, 0x00040018, - 0x00000039, 0x0000001d, 0x00000003, 0x0004002b, 0x00000006, 0x00000042, 0x00000000, 0x0004002b, - 0x00000006, 0x00000043, 0x3f800000, 0x0006002c, 0x0000001d, 0x00000044, 0x00000042, 0x00000043, - 0x00000042, 0x0004002b, 0x00000006, 0x0000004a, 0x3727c5ac, 0x00020014, 0x0000004b, 0x0004002b, - 0x0000000f, 0x00000058, 0x00000005, 0x00040020, 0x00000059, 0x00000002, 0x00000026, 0x0004003b, - 0x0000001e, 0x00000065, 0x00000001, 0x0004002b, 0x00000006, 0x00000073, 0x38d1b717, 0x0004002b, - 0x00000006, 0x00000081, 0x3f733333, 0x0006002c, 0x0000001d, 0x00000083, 0x00000043, 0x00000042, - 0x00000042, 0x00040017, 0x00000084, 0x0000004b, 0x00000003, 0x00040017, 0x0000008e, 0x00000006, - 0x00000002, 0x00040020, 0x0000008f, 0x00000001, 0x0000008e, 0x0004003b, 0x0000008f, 0x00000090, - 0x00000001, 0x0004002b, 0x00000009, 0x00000091, 0x00000000, 0x0004002b, 0x00000006, 0x00000094, - 0x3f000000, 0x00040020, 0x000000a5, 0x00000007, 0x00000026, 0x0004001c, 0x000000b8, 0x00000006, - 0x00000020, 0x0006001e, 0x000000b9, 0x00000026, 0x00000006, 0x000000b8, 0x000000b8, 0x00040020, - 0x000000ba, 0x00000003, 0x000000b9, 0x0004003b, 0x000000ba, 0x000000bb, 0x00000003, 0x00040020, - 0x000000c4, 0x00000003, 0x00000026, 0x00040020, 0x000000c6, 0x00000003, 0x0000008e, 0x0004003b, - 0x000000c6, 0x000000c7, 0x00000003, 0x00040020, 0x000000c9, 0x00000003, 0x0000001d, 0x0004003b, - 0x000000c9, 0x000000ca, 0x00000003, 0x0004003b, 0x000000c9, 0x000000cd, 0x00000003, 0x0004002b, - 0x0000000f, 0x000000ce, 0x0000000a, 0x0004003b, 0x000000c9, 0x000000d4, 0x00000003, 0x00040020, - 0x000000d7, 0x00000003, 0x00000006, 0x0004003b, 0x000000d7, 0x000000d8, 0x00000003, 0x0004003b, - 0x000000c4, 0x000000db, 0x00000003, 0x0004002b, 0x0000000f, 0x000000dc, 0x00000004, 0x00050036, - 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003b, 0x00000007, - 0x00000008, 0x00000007, 0x0004003b, 0x00000007, 0x00000017, 0x00000007, 0x0004003b, 0x00000007, - 0x0000001c, 0x00000007, 0x0004003b, 0x00000028, 0x00000029, 0x00000007, 0x0004003b, 0x00000035, - 0x00000036, 0x00000007, 0x0004003b, 0x00000007, 0x00000046, 0x00000007, 0x0004003b, 0x00000035, - 0x0000004d, 0x00000007, 0x0004003b, 0x00000035, 0x00000056, 0x00000007, 0x0004003b, 0x00000035, - 0x00000063, 0x00000007, 0x0004003b, 0x00000007, 0x0000006f, 0x00000007, 0x0004003b, 0x00000035, - 0x00000077, 0x00000007, 0x0004003b, 0x00000035, 0x0000007d, 0x00000007, 0x0004003b, 0x00000035, - 0x0000008b, 0x00000007, 0x0004003b, 0x000000a5, 0x000000a6, 0x00000007, 0x0004003b, 0x000000a5, - 0x000000af, 0x00000007, 0x00060041, 0x00000012, 0x00000013, 0x0000000e, 0x00000010, 0x00000011, - 0x0004003d, 0x00000006, 0x00000014, 0x00000013, 0x0007000c, 0x00000006, 0x00000016, 0x00000001, - 0x00000028, 0x00000014, 0x00000015, 0x0003003e, 0x00000008, 0x00000016, 0x00060041, 0x00000012, - 0x00000019, 0x0000000e, 0x00000010, 0x00000018, 0x0004003d, 0x00000006, 0x0000001a, 0x00000019, - 0x0007000c, 0x00000006, 0x0000001b, 0x00000001, 0x00000028, 0x0000001a, 0x00000015, 0x0003003e, - 0x00000017, 0x0000001b, 0x00050041, 0x00000021, 0x00000022, 0x0000001f, 0x00000020, 0x0004003d, - 0x00000006, 0x00000023, 0x00000022, 0x0007000c, 0x00000006, 0x00000025, 0x00000001, 0x00000028, - 0x00000023, 0x00000024, 0x0003003e, 0x0000001c, 0x00000025, 0x00050041, 0x00000031, 0x00000032, - 0x0000002f, 0x00000030, 0x0004003d, 0x00000027, 0x00000033, 0x00000032, 0x0006000c, 0x00000027, - 0x00000034, 0x00000001, 0x00000022, 0x00000033, 0x0003003e, 0x00000029, 0x00000034, 0x00050041, - 0x00000031, 0x00000037, 0x0000002f, 0x00000030, 0x0004003d, 0x00000027, 0x00000038, 0x00000037, - 0x00050051, 0x00000026, 0x0000003a, 0x00000038, 0x00000000, 0x0008004f, 0x0000001d, 0x0000003b, - 0x0000003a, 0x0000003a, 0x00000000, 0x00000001, 0x00000002, 0x00050051, 0x00000026, 0x0000003c, - 0x00000038, 0x00000001, 0x0008004f, 0x0000001d, 0x0000003d, 0x0000003c, 0x0000003c, 0x00000000, - 0x00000001, 0x00000002, 0x00050051, 0x00000026, 0x0000003e, 0x00000038, 0x00000002, 0x0008004f, - 0x0000001d, 0x0000003f, 0x0000003e, 0x0000003e, 0x00000000, 0x00000001, 0x00000002, 0x00060050, - 0x00000039, 0x00000040, 0x0000003b, 0x0000003d, 0x0000003f, 0x0006000c, 0x00000039, 0x00000041, - 0x00000001, 0x00000022, 0x00000040, 0x00050091, 0x0000001d, 0x00000045, 0x00000041, 0x00000044, - 0x0003003e, 0x00000036, 0x00000045, 0x0004003d, 0x0000001d, 0x00000047, 0x00000036, 0x0006000c, - 0x00000006, 0x00000048, 0x00000001, 0x00000042, 0x00000047, 0x0003003e, 0x00000046, 0x00000048, - 0x0004003d, 0x00000006, 0x00000049, 0x00000046, 0x000500ba, 0x0000004b, 0x0000004c, 0x00000049, - 0x0000004a, 0x000300f7, 0x0000004f, 0x00000000, 0x000400fa, 0x0000004c, 0x0000004e, 0x00000054, - 0x000200f8, 0x0000004e, 0x0004003d, 0x0000001d, 0x00000050, 0x00000036, 0x0004003d, 0x00000006, - 0x00000051, 0x00000046, 0x00060050, 0x0000001d, 0x00000052, 0x00000051, 0x00000051, 0x00000051, - 0x00050088, 0x0000001d, 0x00000053, 0x00000050, 0x00000052, 0x0003003e, 0x0000004d, 0x00000053, - 0x000200f9, 0x0000004f, 0x000200f8, 0x00000054, 0x0003003e, 0x0000004d, 0x00000044, 0x000200f9, - 0x0000004f, 0x000200f8, 0x0000004f, 0x0004003d, 0x0000001d, 0x00000055, 0x0000004d, 0x0003003e, - 0x00000036, 0x00000055, 0x0004003d, 0x00000027, 0x00000057, 0x00000029, 0x00050041, 0x00000059, - 0x0000005a, 0x0000002f, 0x00000058, 0x0004003d, 0x00000026, 0x0000005b, 0x0000005a, 0x0008004f, - 0x0000001d, 0x0000005c, 0x0000005b, 0x0000005b, 0x00000000, 0x00000001, 0x00000002, 0x00050051, - 0x00000006, 0x0000005d, 0x0000005c, 0x00000000, 0x00050051, 0x00000006, 0x0000005e, 0x0000005c, - 0x00000001, 0x00050051, 0x00000006, 0x0000005f, 0x0000005c, 0x00000002, 0x00070050, 0x00000026, - 0x00000060, 0x0000005d, 0x0000005e, 0x0000005f, 0x00000043, 0x00050091, 0x00000026, 0x00000061, - 0x00000057, 0x00000060, 0x0008004f, 0x0000001d, 0x00000062, 0x00000061, 0x00000061, 0x00000000, - 0x00000001, 0x00000002, 0x0003003e, 0x00000056, 0x00000062, 0x0004003d, 0x0000001d, 0x00000064, - 0x00000056, 0x0004003d, 0x0000001d, 0x00000066, 0x00000065, 0x00050083, 0x0000001d, 0x00000067, - 0x00000064, 0x00000066, 0x0003003e, 0x00000063, 0x00000067, 0x0004003d, 0x0000001d, 0x00000068, - 0x00000063, 0x0004003d, 0x0000001d, 0x00000069, 0x00000036, 0x0004003d, 0x0000001d, 0x0000006a, - 0x00000063, 0x0004003d, 0x0000001d, 0x0000006b, 0x00000036, 0x00050094, 0x00000006, 0x0000006c, - 0x0000006a, 0x0000006b, 0x0005008e, 0x0000001d, 0x0000006d, 0x00000069, 0x0000006c, 0x00050083, - 0x0000001d, 0x0000006e, 0x00000068, 0x0000006d, 0x0003003e, 0x00000063, 0x0000006e, 0x0004003d, - 0x0000001d, 0x00000070, 0x00000063, 0x0006000c, 0x00000006, 0x00000071, 0x00000001, 0x00000042, - 0x00000070, 0x0003003e, 0x0000006f, 0x00000071, 0x0004003d, 0x00000006, 0x00000072, 0x0000006f, - 0x000500ba, 0x0000004b, 0x00000074, 0x00000072, 0x00000073, 0x000300f7, 0x00000076, 0x00000000, - 0x000400fa, 0x00000074, 0x00000075, 0x0000007c, 0x000200f8, 0x00000075, 0x0004003d, 0x0000001d, - 0x00000078, 0x00000036, 0x0004003d, 0x0000001d, 0x00000079, 0x00000063, 0x0007000c, 0x0000001d, - 0x0000007a, 0x00000001, 0x00000044, 0x00000078, 0x00000079, 0x0006000c, 0x0000001d, 0x0000007b, - 0x00000001, 0x00000045, 0x0000007a, 0x0003003e, 0x00000077, 0x0000007b, 0x000200f9, 0x00000076, - 0x000200f8, 0x0000007c, 0x00050041, 0x00000007, 0x0000007e, 0x00000036, 0x00000020, 0x0004003d, - 0x00000006, 0x0000007f, 0x0000007e, 0x0006000c, 0x00000006, 0x00000080, 0x00000001, 0x00000004, - 0x0000007f, 0x000500b8, 0x0000004b, 0x00000082, 0x00000080, 0x00000081, 0x00060050, 0x00000084, - 0x00000085, 0x00000082, 0x00000082, 0x00000082, 0x000600a9, 0x0000001d, 0x00000086, 0x00000085, - 0x00000044, 0x00000083, 0x0003003e, 0x0000007d, 0x00000086, 0x0004003d, 0x0000001d, 0x00000087, - 0x00000036, 0x0004003d, 0x0000001d, 0x00000088, 0x0000007d, 0x0007000c, 0x0000001d, 0x00000089, - 0x00000001, 0x00000044, 0x00000087, 0x00000088, 0x0006000c, 0x0000001d, 0x0000008a, 0x00000001, - 0x00000045, 0x00000089, 0x0003003e, 0x00000077, 0x0000008a, 0x000200f9, 0x00000076, 0x000200f8, - 0x00000076, 0x0004003d, 0x0000001d, 0x0000008c, 0x00000065, 0x0004003d, 0x0000001d, 0x0000008d, - 0x00000077, 0x00050041, 0x00000021, 0x00000092, 0x00000090, 0x00000091, 0x0004003d, 0x00000006, - 0x00000093, 0x00000092, 0x00050083, 0x00000006, 0x00000095, 0x00000093, 0x00000094, 0x0005008e, - 0x0000001d, 0x00000096, 0x0000008d, 0x00000095, 0x0004003d, 0x00000006, 0x00000097, 0x00000008, - 0x0005008e, 0x0000001d, 0x00000098, 0x00000096, 0x00000097, 0x0004003d, 0x00000006, 0x00000099, - 0x0000001c, 0x0005008e, 0x0000001d, 0x0000009a, 0x00000098, 0x00000099, 0x00050081, 0x0000001d, - 0x0000009b, 0x0000008c, 0x0000009a, 0x0004003d, 0x0000001d, 0x0000009c, 0x00000036, 0x00050041, - 0x00000021, 0x0000009d, 0x00000090, 0x00000020, 0x0004003d, 0x00000006, 0x0000009e, 0x0000009d, - 0x0005008e, 0x0000001d, 0x0000009f, 0x0000009c, 0x0000009e, 0x0004003d, 0x00000006, 0x000000a0, - 0x00000017, 0x0005008e, 0x0000001d, 0x000000a1, 0x0000009f, 0x000000a0, 0x0004003d, 0x00000006, - 0x000000a2, 0x0000001c, 0x0005008e, 0x0000001d, 0x000000a3, 0x000000a1, 0x000000a2, 0x00050081, - 0x0000001d, 0x000000a4, 0x0000009b, 0x000000a3, 0x0003003e, 0x0000008b, 0x000000a4, 0x00050041, - 0x00000031, 0x000000a7, 0x0000002f, 0x00000030, 0x0004003d, 0x00000027, 0x000000a8, 0x000000a7, - 0x0004003d, 0x0000001d, 0x000000a9, 0x0000008b, 0x00050051, 0x00000006, 0x000000aa, 0x000000a9, - 0x00000000, 0x00050051, 0x00000006, 0x000000ab, 0x000000a9, 0x00000001, 0x00050051, 0x00000006, - 0x000000ac, 0x000000a9, 0x00000002, 0x00070050, 0x00000026, 0x000000ad, 0x000000aa, 0x000000ab, - 0x000000ac, 0x00000043, 0x00050091, 0x00000026, 0x000000ae, 0x000000a8, 0x000000ad, 0x0003003e, - 0x000000a6, 0x000000ae, 0x00050041, 0x00000031, 0x000000b0, 0x0000002f, 0x00000030, 0x0004003d, - 0x00000027, 0x000000b1, 0x000000b0, 0x0004003d, 0x0000001d, 0x000000b2, 0x00000065, 0x00050051, - 0x00000006, 0x000000b3, 0x000000b2, 0x00000000, 0x00050051, 0x00000006, 0x000000b4, 0x000000b2, - 0x00000001, 0x00050051, 0x00000006, 0x000000b5, 0x000000b2, 0x00000002, 0x00070050, 0x00000026, - 0x000000b6, 0x000000b3, 0x000000b4, 0x000000b5, 0x00000043, 0x00050091, 0x00000026, 0x000000b7, - 0x000000b1, 0x000000b6, 0x0003003e, 0x000000af, 0x000000b7, 0x00050041, 0x00000031, 0x000000bc, - 0x0000002f, 0x00000010, 0x0004003d, 0x00000027, 0x000000bd, 0x000000bc, 0x0004003d, 0x0000001d, - 0x000000be, 0x0000008b, 0x00050051, 0x00000006, 0x000000bf, 0x000000be, 0x00000000, 0x00050051, - 0x00000006, 0x000000c0, 0x000000be, 0x00000001, 0x00050051, 0x00000006, 0x000000c1, 0x000000be, - 0x00000002, 0x00070050, 0x00000026, 0x000000c2, 0x000000bf, 0x000000c0, 0x000000c1, 0x00000043, - 0x00050091, 0x00000026, 0x000000c3, 0x000000bd, 0x000000c2, 0x00050041, 0x000000c4, 0x000000c5, - 0x000000bb, 0x00000010, 0x0003003e, 0x000000c5, 0x000000c3, 0x0004003d, 0x0000008e, 0x000000c8, - 0x00000090, 0x0003003e, 0x000000c7, 0x000000c8, 0x0004003d, 0x00000026, 0x000000cb, 0x000000a6, - 0x0008004f, 0x0000001d, 0x000000cc, 0x000000cb, 0x000000cb, 0x00000000, 0x00000001, 0x00000002, - 0x0003003e, 0x000000ca, 0x000000cc, 0x00050041, 0x00000031, 0x000000cf, 0x0000002f, 0x000000ce, - 0x0004003d, 0x00000027, 0x000000d0, 0x000000cf, 0x0004003d, 0x00000026, 0x000000d1, 0x000000a6, - 0x00050091, 0x00000026, 0x000000d2, 0x000000d0, 0x000000d1, 0x0008004f, 0x0000001d, 0x000000d3, - 0x000000d2, 0x000000d2, 0x00000000, 0x00000001, 0x00000002, 0x0003003e, 0x000000cd, 0x000000d3, - 0x0004003d, 0x00000026, 0x000000d5, 0x000000af, 0x0008004f, 0x0000001d, 0x000000d6, 0x000000d5, - 0x000000d5, 0x00000000, 0x00000001, 0x00000002, 0x0003003e, 0x000000d4, 0x000000d6, 0x00050041, - 0x00000021, 0x000000d9, 0x0000001f, 0x00000091, 0x0004003d, 0x00000006, 0x000000da, 0x000000d9, - 0x0003003e, 0x000000d8, 0x000000da, 0x00050041, 0x00000059, 0x000000dd, 0x0000002f, 0x000000dc, - 0x0004003d, 0x00000026, 0x000000de, 0x000000dd, 0x0003003e, 0x000000db, 0x000000de, 0x000100fd, - 0x00010038, + 0x0010000f, 0x00000000, 0x00000004, 0x6e69616d, 0x00000000, 0x0000001f, 0x00000065, 0x00000090, + 0x000000bb, 0x000000c7, 0x000000ca, 0x000000cd, 0x000000d4, 0x000000d8, 0x000000db, 0x000000df, + 0x00030003, 0x00000002, 0x000001c2, 0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x00040005, + 0x00000008, 0x74646977, 0x00000068, 0x00050005, 0x0000000c, 0x65747845, 0x6c616e72, 0x00000073, + 0x00050006, 0x0000000c, 0x00000000, 0x61746164, 0x00000000, 0x00030005, 0x0000000e, 0x00000075, + 0x00040005, 0x00000017, 0x67696568, 0x00007468, 0x00040005, 0x0000001c, 0x6c616373, 0x00000065, + 0x00050005, 0x0000001f, 0x6f4e6e69, 0x6c616d72, 0x00000000, 0x00050005, 0x00000029, 0x4d766e69, + 0x6c65646f, 0x00000000, 0x00040005, 0x0000002a, 0x6867694c, 0x00443374, 0x00060006, 0x0000002a, + 0x00000000, 0x52736f70, 0x75696461, 0x00000073, 0x00050006, 0x0000002a, 0x00000001, 0x6f6c6f63, + 0x00000072, 0x00040005, 0x0000002d, 0x6d617246, 0x00000065, 0x00040006, 0x0000002d, 0x00000000, + 0x0070766d, 0x00050006, 0x0000002d, 0x00000001, 0x65646f6d, 0x0000006c, 0x00080006, 0x0000002d, + 0x00000002, 0x6867696c, 0x72694474, 0x65746e49, 0x7469736e, 0x00000079, 0x00060006, 0x0000002d, + 0x00000003, 0x6867696c, 0x6c6f4374, 0x0000726f, 0x00050006, 0x0000002d, 0x00000004, 0x746e6974, + 0x00000000, 0x00060006, 0x0000002d, 0x00000005, 0x656d6163, 0x6f506172, 0x00000073, 0x00050006, + 0x0000002d, 0x00000006, 0x69626d61, 0x00746e65, 0x00050006, 0x0000002d, 0x00000007, 0x6867696c, + 0x00007374, 0x00050006, 0x0000002d, 0x00000008, 0x42786574, 0x00626d6f, 0x00060006, 0x0000002d, + 0x00000009, 0x61726170, 0x78616c6c, 0x00000000, 0x00050006, 0x0000002d, 0x0000000a, 0x77656976, + 0x00000000, 0x00060006, 0x0000002d, 0x0000000b, 0x70696c63, 0x6f666e49, 0x00000000, 0x00030005, + 0x0000002f, 0x006f6275, 0x00050005, 0x00000036, 0x6c726f77, 0x4f705564, 0x00006a62, 0x00040005, + 0x00000046, 0x654c7075, 0x0000006e, 0x00040005, 0x00000056, 0x4f6d6163, 0x00006a62, 0x00040005, + 0x00000063, 0x61436f74, 0x0000006d, 0x00040005, 0x00000065, 0x6f506e69, 0x00000073, 0x00050005, + 0x0000006f, 0x61436f74, 0x6e654c6d, 0x00000000, 0x00040005, 0x00000077, 0x68676972, 0x00000074, + 0x00050005, 0x0000007d, 0x6c6c6166, 0x6b636162, 0x00000000, 0x00030005, 0x0000008b, 0x006a626f, + 0x00040005, 0x00000090, 0x56556e69, 0x00000000, 0x00040005, 0x000000a6, 0x6c726f77, 0x00000064, + 0x00050005, 0x000000af, 0x746f6f72, 0x6c726f57, 0x00000064, 0x00060005, 0x000000b9, 0x505f6c67, + 0x65567265, 0x78657472, 0x00000000, 0x00060006, 0x000000b9, 0x00000000, 0x505f6c67, 0x7469736f, + 0x006e6f69, 0x00070006, 0x000000b9, 0x00000001, 0x505f6c67, 0x746e696f, 0x657a6953, 0x00000000, + 0x00070006, 0x000000b9, 0x00000002, 0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006, + 0x000000b9, 0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, 0x00030005, 0x000000bb, + 0x00000000, 0x00030005, 0x000000c7, 0x00565576, 0x00050005, 0x000000ca, 0x726f5776, 0x6f50646c, + 0x00000073, 0x00050005, 0x000000cd, 0x65695676, 0x736f5077, 0x00000000, 0x00050005, 0x000000d4, + 0x6f6f5276, 0x736f5074, 0x00000000, 0x00050005, 0x000000d8, 0x736e4976, 0x636e6174, 0x00644965, + 0x00040005, 0x000000db, 0x6e695476, 0x00000074, 0x00050005, 0x000000df, 0x776c4176, 0x44737961, + 0x006b7261, 0x00040047, 0x0000000b, 0x00000006, 0x00000004, 0x00030047, 0x0000000c, 0x00000002, + 0x00050048, 0x0000000c, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x0000001f, 0x0000001e, + 0x00000001, 0x00050048, 0x0000002a, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x0000002a, + 0x00000001, 0x00000023, 0x00000010, 0x00040047, 0x0000002c, 0x00000006, 0x00000020, 0x00030047, + 0x0000002d, 0x00000002, 0x00040048, 0x0000002d, 0x00000000, 0x00000005, 0x00050048, 0x0000002d, + 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x0000002d, 0x00000000, 0x00000023, 0x00000000, + 0x00040048, 0x0000002d, 0x00000001, 0x00000005, 0x00050048, 0x0000002d, 0x00000001, 0x00000007, + 0x00000010, 0x00050048, 0x0000002d, 0x00000001, 0x00000023, 0x00000040, 0x00050048, 0x0000002d, + 0x00000002, 0x00000023, 0x00000080, 0x00050048, 0x0000002d, 0x00000003, 0x00000023, 0x00000090, + 0x00050048, 0x0000002d, 0x00000004, 0x00000023, 0x000000a0, 0x00050048, 0x0000002d, 0x00000005, + 0x00000023, 0x000000b0, 0x00050048, 0x0000002d, 0x00000006, 0x00000023, 0x000000c0, 0x00050048, + 0x0000002d, 0x00000007, 0x00000023, 0x000000d0, 0x00050048, 0x0000002d, 0x00000008, 0x00000023, + 0x000001d0, 0x00050048, 0x0000002d, 0x00000009, 0x00000023, 0x000001e0, 0x00040048, 0x0000002d, + 0x0000000a, 0x00000005, 0x00050048, 0x0000002d, 0x0000000a, 0x00000007, 0x00000010, 0x00050048, + 0x0000002d, 0x0000000a, 0x00000023, 0x000001f0, 0x00050048, 0x0000002d, 0x0000000b, 0x00000023, + 0x00000230, 0x00040047, 0x0000002f, 0x00000021, 0x00000000, 0x00040047, 0x0000002f, 0x00000022, + 0x00000000, 0x00040047, 0x00000065, 0x0000001e, 0x00000000, 0x00040047, 0x00000090, 0x0000001e, + 0x00000002, 0x00030047, 0x000000b9, 0x00000002, 0x00050048, 0x000000b9, 0x00000000, 0x0000000b, + 0x00000000, 0x00050048, 0x000000b9, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, 0x000000b9, + 0x00000002, 0x0000000b, 0x00000003, 0x00050048, 0x000000b9, 0x00000003, 0x0000000b, 0x00000004, + 0x00040047, 0x000000c7, 0x0000001e, 0x00000000, 0x00040047, 0x000000ca, 0x0000001e, 0x00000001, + 0x00040047, 0x000000cd, 0x0000001e, 0x00000002, 0x00040047, 0x000000d4, 0x0000001e, 0x00000003, + 0x00040047, 0x000000d8, 0x0000001e, 0x00000004, 0x00040047, 0x000000db, 0x0000001e, 0x00000005, + 0x00040047, 0x000000df, 0x0000001e, 0x00000006, 0x00020013, 0x00000002, 0x00030021, 0x00000003, + 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040020, 0x00000007, 0x00000007, 0x00000006, + 0x00040015, 0x00000009, 0x00000020, 0x00000000, 0x0004002b, 0x00000009, 0x0000000a, 0x00000020, + 0x0004001c, 0x0000000b, 0x00000006, 0x0000000a, 0x0003001e, 0x0000000c, 0x0000000b, 0x00040020, + 0x0000000d, 0x00000009, 0x0000000c, 0x0004003b, 0x0000000d, 0x0000000e, 0x00000009, 0x00040015, + 0x0000000f, 0x00000020, 0x00000001, 0x0004002b, 0x0000000f, 0x00000010, 0x00000000, 0x0004002b, + 0x0000000f, 0x00000011, 0x00000002, 0x00040020, 0x00000012, 0x00000009, 0x00000006, 0x0004002b, + 0x00000006, 0x00000015, 0x3c23d70a, 0x0004002b, 0x0000000f, 0x00000018, 0x00000003, 0x00040017, + 0x0000001d, 0x00000006, 0x00000003, 0x00040020, 0x0000001e, 0x00000001, 0x0000001d, 0x0004003b, + 0x0000001e, 0x0000001f, 0x00000001, 0x0004002b, 0x00000009, 0x00000020, 0x00000001, 0x00040020, + 0x00000021, 0x00000001, 0x00000006, 0x0004002b, 0x00000006, 0x00000024, 0x3d4ccccd, 0x00040017, + 0x00000026, 0x00000006, 0x00000004, 0x00040018, 0x00000027, 0x00000026, 0x00000004, 0x00040020, + 0x00000028, 0x00000007, 0x00000027, 0x0004001e, 0x0000002a, 0x00000026, 0x00000026, 0x0004002b, + 0x00000009, 0x0000002b, 0x00000008, 0x0004001c, 0x0000002c, 0x0000002a, 0x0000002b, 0x000e001e, + 0x0000002d, 0x00000027, 0x00000027, 0x00000026, 0x00000026, 0x00000026, 0x00000026, 0x00000026, + 0x0000002c, 0x00000026, 0x00000026, 0x00000027, 0x00000026, 0x00040020, 0x0000002e, 0x00000002, + 0x0000002d, 0x0004003b, 0x0000002e, 0x0000002f, 0x00000002, 0x0004002b, 0x0000000f, 0x00000030, + 0x00000001, 0x00040020, 0x00000031, 0x00000002, 0x00000027, 0x00040020, 0x00000035, 0x00000007, + 0x0000001d, 0x00040018, 0x00000039, 0x0000001d, 0x00000003, 0x0004002b, 0x00000006, 0x00000042, + 0x00000000, 0x0004002b, 0x00000006, 0x00000043, 0x3f800000, 0x0006002c, 0x0000001d, 0x00000044, + 0x00000042, 0x00000043, 0x00000042, 0x0004002b, 0x00000006, 0x0000004a, 0x3727c5ac, 0x00020014, + 0x0000004b, 0x0004002b, 0x0000000f, 0x00000058, 0x00000005, 0x00040020, 0x00000059, 0x00000002, + 0x00000026, 0x0004003b, 0x0000001e, 0x00000065, 0x00000001, 0x0004002b, 0x00000006, 0x00000073, + 0x38d1b717, 0x0004002b, 0x00000006, 0x00000081, 0x3f733333, 0x0006002c, 0x0000001d, 0x00000083, + 0x00000043, 0x00000042, 0x00000042, 0x00040017, 0x00000084, 0x0000004b, 0x00000003, 0x00040017, + 0x0000008e, 0x00000006, 0x00000002, 0x00040020, 0x0000008f, 0x00000001, 0x0000008e, 0x0004003b, + 0x0000008f, 0x00000090, 0x00000001, 0x0004002b, 0x00000009, 0x00000091, 0x00000000, 0x0004002b, + 0x00000006, 0x00000094, 0x3f000000, 0x00040020, 0x000000a5, 0x00000007, 0x00000026, 0x0004001c, + 0x000000b8, 0x00000006, 0x00000020, 0x0006001e, 0x000000b9, 0x00000026, 0x00000006, 0x000000b8, + 0x000000b8, 0x00040020, 0x000000ba, 0x00000003, 0x000000b9, 0x0004003b, 0x000000ba, 0x000000bb, + 0x00000003, 0x00040020, 0x000000c4, 0x00000003, 0x00000026, 0x00040020, 0x000000c6, 0x00000003, + 0x0000008e, 0x0004003b, 0x000000c6, 0x000000c7, 0x00000003, 0x00040020, 0x000000c9, 0x00000003, + 0x0000001d, 0x0004003b, 0x000000c9, 0x000000ca, 0x00000003, 0x0004003b, 0x000000c9, 0x000000cd, + 0x00000003, 0x0004002b, 0x0000000f, 0x000000ce, 0x0000000a, 0x0004003b, 0x000000c9, 0x000000d4, + 0x00000003, 0x00040020, 0x000000d7, 0x00000003, 0x00000006, 0x0004003b, 0x000000d7, 0x000000d8, + 0x00000003, 0x0004003b, 0x000000c4, 0x000000db, 0x00000003, 0x0004002b, 0x0000000f, 0x000000dc, + 0x00000004, 0x0004003b, 0x000000d7, 0x000000df, 0x00000003, 0x0004002b, 0x00000009, 0x000000e0, + 0x00000002, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, + 0x0004003b, 0x00000007, 0x00000008, 0x00000007, 0x0004003b, 0x00000007, 0x00000017, 0x00000007, + 0x0004003b, 0x00000007, 0x0000001c, 0x00000007, 0x0004003b, 0x00000028, 0x00000029, 0x00000007, + 0x0004003b, 0x00000035, 0x00000036, 0x00000007, 0x0004003b, 0x00000007, 0x00000046, 0x00000007, + 0x0004003b, 0x00000035, 0x0000004d, 0x00000007, 0x0004003b, 0x00000035, 0x00000056, 0x00000007, + 0x0004003b, 0x00000035, 0x00000063, 0x00000007, 0x0004003b, 0x00000007, 0x0000006f, 0x00000007, + 0x0004003b, 0x00000035, 0x00000077, 0x00000007, 0x0004003b, 0x00000035, 0x0000007d, 0x00000007, + 0x0004003b, 0x00000035, 0x0000008b, 0x00000007, 0x0004003b, 0x000000a5, 0x000000a6, 0x00000007, + 0x0004003b, 0x000000a5, 0x000000af, 0x00000007, 0x00060041, 0x00000012, 0x00000013, 0x0000000e, + 0x00000010, 0x00000011, 0x0004003d, 0x00000006, 0x00000014, 0x00000013, 0x0007000c, 0x00000006, + 0x00000016, 0x00000001, 0x00000028, 0x00000014, 0x00000015, 0x0003003e, 0x00000008, 0x00000016, + 0x00060041, 0x00000012, 0x00000019, 0x0000000e, 0x00000010, 0x00000018, 0x0004003d, 0x00000006, + 0x0000001a, 0x00000019, 0x0007000c, 0x00000006, 0x0000001b, 0x00000001, 0x00000028, 0x0000001a, + 0x00000015, 0x0003003e, 0x00000017, 0x0000001b, 0x00050041, 0x00000021, 0x00000022, 0x0000001f, + 0x00000020, 0x0004003d, 0x00000006, 0x00000023, 0x00000022, 0x0007000c, 0x00000006, 0x00000025, + 0x00000001, 0x00000028, 0x00000023, 0x00000024, 0x0003003e, 0x0000001c, 0x00000025, 0x00050041, + 0x00000031, 0x00000032, 0x0000002f, 0x00000030, 0x0004003d, 0x00000027, 0x00000033, 0x00000032, + 0x0006000c, 0x00000027, 0x00000034, 0x00000001, 0x00000022, 0x00000033, 0x0003003e, 0x00000029, + 0x00000034, 0x00050041, 0x00000031, 0x00000037, 0x0000002f, 0x00000030, 0x0004003d, 0x00000027, + 0x00000038, 0x00000037, 0x00050051, 0x00000026, 0x0000003a, 0x00000038, 0x00000000, 0x0008004f, + 0x0000001d, 0x0000003b, 0x0000003a, 0x0000003a, 0x00000000, 0x00000001, 0x00000002, 0x00050051, + 0x00000026, 0x0000003c, 0x00000038, 0x00000001, 0x0008004f, 0x0000001d, 0x0000003d, 0x0000003c, + 0x0000003c, 0x00000000, 0x00000001, 0x00000002, 0x00050051, 0x00000026, 0x0000003e, 0x00000038, + 0x00000002, 0x0008004f, 0x0000001d, 0x0000003f, 0x0000003e, 0x0000003e, 0x00000000, 0x00000001, + 0x00000002, 0x00060050, 0x00000039, 0x00000040, 0x0000003b, 0x0000003d, 0x0000003f, 0x0006000c, + 0x00000039, 0x00000041, 0x00000001, 0x00000022, 0x00000040, 0x00050091, 0x0000001d, 0x00000045, + 0x00000041, 0x00000044, 0x0003003e, 0x00000036, 0x00000045, 0x0004003d, 0x0000001d, 0x00000047, + 0x00000036, 0x0006000c, 0x00000006, 0x00000048, 0x00000001, 0x00000042, 0x00000047, 0x0003003e, + 0x00000046, 0x00000048, 0x0004003d, 0x00000006, 0x00000049, 0x00000046, 0x000500ba, 0x0000004b, + 0x0000004c, 0x00000049, 0x0000004a, 0x000300f7, 0x0000004f, 0x00000000, 0x000400fa, 0x0000004c, + 0x0000004e, 0x00000054, 0x000200f8, 0x0000004e, 0x0004003d, 0x0000001d, 0x00000050, 0x00000036, + 0x0004003d, 0x00000006, 0x00000051, 0x00000046, 0x00060050, 0x0000001d, 0x00000052, 0x00000051, + 0x00000051, 0x00000051, 0x00050088, 0x0000001d, 0x00000053, 0x00000050, 0x00000052, 0x0003003e, + 0x0000004d, 0x00000053, 0x000200f9, 0x0000004f, 0x000200f8, 0x00000054, 0x0003003e, 0x0000004d, + 0x00000044, 0x000200f9, 0x0000004f, 0x000200f8, 0x0000004f, 0x0004003d, 0x0000001d, 0x00000055, + 0x0000004d, 0x0003003e, 0x00000036, 0x00000055, 0x0004003d, 0x00000027, 0x00000057, 0x00000029, + 0x00050041, 0x00000059, 0x0000005a, 0x0000002f, 0x00000058, 0x0004003d, 0x00000026, 0x0000005b, + 0x0000005a, 0x0008004f, 0x0000001d, 0x0000005c, 0x0000005b, 0x0000005b, 0x00000000, 0x00000001, + 0x00000002, 0x00050051, 0x00000006, 0x0000005d, 0x0000005c, 0x00000000, 0x00050051, 0x00000006, + 0x0000005e, 0x0000005c, 0x00000001, 0x00050051, 0x00000006, 0x0000005f, 0x0000005c, 0x00000002, + 0x00070050, 0x00000026, 0x00000060, 0x0000005d, 0x0000005e, 0x0000005f, 0x00000043, 0x00050091, + 0x00000026, 0x00000061, 0x00000057, 0x00000060, 0x0008004f, 0x0000001d, 0x00000062, 0x00000061, + 0x00000061, 0x00000000, 0x00000001, 0x00000002, 0x0003003e, 0x00000056, 0x00000062, 0x0004003d, + 0x0000001d, 0x00000064, 0x00000056, 0x0004003d, 0x0000001d, 0x00000066, 0x00000065, 0x00050083, + 0x0000001d, 0x00000067, 0x00000064, 0x00000066, 0x0003003e, 0x00000063, 0x00000067, 0x0004003d, + 0x0000001d, 0x00000068, 0x00000063, 0x0004003d, 0x0000001d, 0x00000069, 0x00000036, 0x0004003d, + 0x0000001d, 0x0000006a, 0x00000063, 0x0004003d, 0x0000001d, 0x0000006b, 0x00000036, 0x00050094, + 0x00000006, 0x0000006c, 0x0000006a, 0x0000006b, 0x0005008e, 0x0000001d, 0x0000006d, 0x00000069, + 0x0000006c, 0x00050083, 0x0000001d, 0x0000006e, 0x00000068, 0x0000006d, 0x0003003e, 0x00000063, + 0x0000006e, 0x0004003d, 0x0000001d, 0x00000070, 0x00000063, 0x0006000c, 0x00000006, 0x00000071, + 0x00000001, 0x00000042, 0x00000070, 0x0003003e, 0x0000006f, 0x00000071, 0x0004003d, 0x00000006, + 0x00000072, 0x0000006f, 0x000500ba, 0x0000004b, 0x00000074, 0x00000072, 0x00000073, 0x000300f7, + 0x00000076, 0x00000000, 0x000400fa, 0x00000074, 0x00000075, 0x0000007c, 0x000200f8, 0x00000075, + 0x0004003d, 0x0000001d, 0x00000078, 0x00000036, 0x0004003d, 0x0000001d, 0x00000079, 0x00000063, + 0x0007000c, 0x0000001d, 0x0000007a, 0x00000001, 0x00000044, 0x00000078, 0x00000079, 0x0006000c, + 0x0000001d, 0x0000007b, 0x00000001, 0x00000045, 0x0000007a, 0x0003003e, 0x00000077, 0x0000007b, + 0x000200f9, 0x00000076, 0x000200f8, 0x0000007c, 0x00050041, 0x00000007, 0x0000007e, 0x00000036, + 0x00000020, 0x0004003d, 0x00000006, 0x0000007f, 0x0000007e, 0x0006000c, 0x00000006, 0x00000080, + 0x00000001, 0x00000004, 0x0000007f, 0x000500b8, 0x0000004b, 0x00000082, 0x00000080, 0x00000081, + 0x00060050, 0x00000084, 0x00000085, 0x00000082, 0x00000082, 0x00000082, 0x000600a9, 0x0000001d, + 0x00000086, 0x00000085, 0x00000044, 0x00000083, 0x0003003e, 0x0000007d, 0x00000086, 0x0004003d, + 0x0000001d, 0x00000087, 0x00000036, 0x0004003d, 0x0000001d, 0x00000088, 0x0000007d, 0x0007000c, + 0x0000001d, 0x00000089, 0x00000001, 0x00000044, 0x00000087, 0x00000088, 0x0006000c, 0x0000001d, + 0x0000008a, 0x00000001, 0x00000045, 0x00000089, 0x0003003e, 0x00000077, 0x0000008a, 0x000200f9, + 0x00000076, 0x000200f8, 0x00000076, 0x0004003d, 0x0000001d, 0x0000008c, 0x00000065, 0x0004003d, + 0x0000001d, 0x0000008d, 0x00000077, 0x00050041, 0x00000021, 0x00000092, 0x00000090, 0x00000091, + 0x0004003d, 0x00000006, 0x00000093, 0x00000092, 0x00050083, 0x00000006, 0x00000095, 0x00000093, + 0x00000094, 0x0005008e, 0x0000001d, 0x00000096, 0x0000008d, 0x00000095, 0x0004003d, 0x00000006, + 0x00000097, 0x00000008, 0x0005008e, 0x0000001d, 0x00000098, 0x00000096, 0x00000097, 0x0004003d, + 0x00000006, 0x00000099, 0x0000001c, 0x0005008e, 0x0000001d, 0x0000009a, 0x00000098, 0x00000099, + 0x00050081, 0x0000001d, 0x0000009b, 0x0000008c, 0x0000009a, 0x0004003d, 0x0000001d, 0x0000009c, + 0x00000036, 0x00050041, 0x00000021, 0x0000009d, 0x00000090, 0x00000020, 0x0004003d, 0x00000006, + 0x0000009e, 0x0000009d, 0x0005008e, 0x0000001d, 0x0000009f, 0x0000009c, 0x0000009e, 0x0004003d, + 0x00000006, 0x000000a0, 0x00000017, 0x0005008e, 0x0000001d, 0x000000a1, 0x0000009f, 0x000000a0, + 0x0004003d, 0x00000006, 0x000000a2, 0x0000001c, 0x0005008e, 0x0000001d, 0x000000a3, 0x000000a1, + 0x000000a2, 0x00050081, 0x0000001d, 0x000000a4, 0x0000009b, 0x000000a3, 0x0003003e, 0x0000008b, + 0x000000a4, 0x00050041, 0x00000031, 0x000000a7, 0x0000002f, 0x00000030, 0x0004003d, 0x00000027, + 0x000000a8, 0x000000a7, 0x0004003d, 0x0000001d, 0x000000a9, 0x0000008b, 0x00050051, 0x00000006, + 0x000000aa, 0x000000a9, 0x00000000, 0x00050051, 0x00000006, 0x000000ab, 0x000000a9, 0x00000001, + 0x00050051, 0x00000006, 0x000000ac, 0x000000a9, 0x00000002, 0x00070050, 0x00000026, 0x000000ad, + 0x000000aa, 0x000000ab, 0x000000ac, 0x00000043, 0x00050091, 0x00000026, 0x000000ae, 0x000000a8, + 0x000000ad, 0x0003003e, 0x000000a6, 0x000000ae, 0x00050041, 0x00000031, 0x000000b0, 0x0000002f, + 0x00000030, 0x0004003d, 0x00000027, 0x000000b1, 0x000000b0, 0x0004003d, 0x0000001d, 0x000000b2, + 0x00000065, 0x00050051, 0x00000006, 0x000000b3, 0x000000b2, 0x00000000, 0x00050051, 0x00000006, + 0x000000b4, 0x000000b2, 0x00000001, 0x00050051, 0x00000006, 0x000000b5, 0x000000b2, 0x00000002, + 0x00070050, 0x00000026, 0x000000b6, 0x000000b3, 0x000000b4, 0x000000b5, 0x00000043, 0x00050091, + 0x00000026, 0x000000b7, 0x000000b1, 0x000000b6, 0x0003003e, 0x000000af, 0x000000b7, 0x00050041, + 0x00000031, 0x000000bc, 0x0000002f, 0x00000010, 0x0004003d, 0x00000027, 0x000000bd, 0x000000bc, + 0x0004003d, 0x0000001d, 0x000000be, 0x0000008b, 0x00050051, 0x00000006, 0x000000bf, 0x000000be, + 0x00000000, 0x00050051, 0x00000006, 0x000000c0, 0x000000be, 0x00000001, 0x00050051, 0x00000006, + 0x000000c1, 0x000000be, 0x00000002, 0x00070050, 0x00000026, 0x000000c2, 0x000000bf, 0x000000c0, + 0x000000c1, 0x00000043, 0x00050091, 0x00000026, 0x000000c3, 0x000000bd, 0x000000c2, 0x00050041, + 0x000000c4, 0x000000c5, 0x000000bb, 0x00000010, 0x0003003e, 0x000000c5, 0x000000c3, 0x0004003d, + 0x0000008e, 0x000000c8, 0x00000090, 0x0003003e, 0x000000c7, 0x000000c8, 0x0004003d, 0x00000026, + 0x000000cb, 0x000000a6, 0x0008004f, 0x0000001d, 0x000000cc, 0x000000cb, 0x000000cb, 0x00000000, + 0x00000001, 0x00000002, 0x0003003e, 0x000000ca, 0x000000cc, 0x00050041, 0x00000031, 0x000000cf, + 0x0000002f, 0x000000ce, 0x0004003d, 0x00000027, 0x000000d0, 0x000000cf, 0x0004003d, 0x00000026, + 0x000000d1, 0x000000a6, 0x00050091, 0x00000026, 0x000000d2, 0x000000d0, 0x000000d1, 0x0008004f, + 0x0000001d, 0x000000d3, 0x000000d2, 0x000000d2, 0x00000000, 0x00000001, 0x00000002, 0x0003003e, + 0x000000cd, 0x000000d3, 0x0004003d, 0x00000026, 0x000000d5, 0x000000af, 0x0008004f, 0x0000001d, + 0x000000d6, 0x000000d5, 0x000000d5, 0x00000000, 0x00000001, 0x00000002, 0x0003003e, 0x000000d4, + 0x000000d6, 0x00050041, 0x00000021, 0x000000d9, 0x0000001f, 0x00000091, 0x0004003d, 0x00000006, + 0x000000da, 0x000000d9, 0x0003003e, 0x000000d8, 0x000000da, 0x00050041, 0x00000059, 0x000000dd, + 0x0000002f, 0x000000dc, 0x0004003d, 0x00000026, 0x000000de, 0x000000dd, 0x0003003e, 0x000000db, + 0x000000de, 0x00050041, 0x00000021, 0x000000e1, 0x0000001f, 0x000000e0, 0x0004003d, 0x00000006, + 0x000000e2, 0x000000e1, 0x0003003e, 0x000000df, 0x000000e2, 0x000100fd, 0x00010038, }; static const size_t mesh3d_grass_vert_spv_count = sizeof(mesh3d_grass_vert_spv)/sizeof(mesh3d_grass_vert_spv[0]); diff --git a/test/grass.cpp b/test/grass.cpp index 25aa2390..ccf52ed6 100644 --- a/test/grass.cpp +++ b/test/grass.cpp @@ -1,18 +1,42 @@ #include "zeroerr/assert.h" #include "zeroerr/unittest.h" +#include "filesystem/FileData.h" #include "graphics/Grass.h" #include "graphics/Graphics.h" +#include "graphics/Light.h" +#include "graphics/Mesh.h" +#include "graphics/RenderControl.h" +#include "graphics/RenderSystem.h" +#include "graphics/RenderSystem3D.h" #include "graphics/Shader.h" #include "graphics/shaders/mesh3d_grass_frag_spv.inc" #include "graphics/shaders/mesh3d_grass_vert_spv.inc" +#include "image/Image.h" +#include "image/ImageData.h" +#include "window/Window.h" +#include #include #include +#include +#include +#include +#include +#include #include +using eve::graphics::Camera3D; +using eve::graphics::Graphics; using eve::graphics::GrassField; +using eve::graphics::Light3D; +using eve::graphics::Mesh; +using eve::graphics::Renderable2D; +using eve::graphics::Renderable3D; +using eve::graphics::RenderSystem; +using eve::graphics::RenderSystem3D; using eve::graphics::Shader; +using eve::graphics::Texture; using eve::graphics::grass::BillboardMesh; using eve::graphics::grass::Point; using eve::graphics::grass::SampleParams; @@ -153,6 +177,7 @@ TEST_CASE("graphics.Grass.billboardRootPivot") { CHECK(std::abs(mesh.posXYZ[size_t(i * 3 + 2)] + 2.f) < 1e-5f); CHECK(std::abs(mesh.nrmXYZ[size_t(i * 3 + 0)] - 11.f) < 1e-5f); CHECK(std::abs(mesh.nrmXYZ[size_t(i * 3 + 1)] - 1.25f) < 1e-5f); + CHECK(std::abs(mesh.nrmXYZ[size_t(i * 3 + 2)]) < 1e-5f); } // Bottom edge UVs sit at v=0; their midpoint is the root (u=0.5). CHECK(std::abs(mesh.uvST[1]) < 1e-5f); @@ -221,3 +246,229 @@ TEST_CASE("graphics.Grass.layerFlag") { REQUIRE_EQ(sh.getFromVar("alwaysDark", &dark, sizeof(dark)), int(sizeof(dark))); CHECK(dark < 0.5f); } + +TEST_CASE("graphics.Grass.billboardAlwaysDarkFlag") { + std::vector pts(1); + pts[0].position = glm::vec3(0.f, 0.f, 0.f); + pts[0].id = 3; + pts[0].scale = 1.f; + const BillboardMesh dark = eve::graphics::grass::buildBillboards(pts, 0.4f, 0.8f, true); + CHECK_EQ(int(dark.nrmXYZ.size()), 12); + for (int i = 0; i < 4; ++i) CHECK(dark.nrmXYZ[size_t(i * 3 + 2)] > 0.5f); +} + +namespace { + +void resetGrassScene() { + if (ecs::current()->getManager() != nullptr) { + auto view = ecs::View(); + for (auto it = view.begin(); it != view.end(); ++it) { + auto [mr] = *it; + mr->visible = false; + } + } + if (ecs::current()->getManager() != nullptr) { + auto camView = ecs::View(); + for (auto it = camView.begin(); it != camView.end(); ++it) { + auto [data] = *it; + data->active = false; + } + } + if (ecs::current()->getManager() != nullptr) { + auto lightView = ecs::View(); + for (auto it = lightView.begin(); it != lightView.end(); ++it) { + auto [d] = *it; + d->enabled = false; + } + } + if (ecs::current()->getManager() != nullptr) { + auto view = ecs::View(); + for (auto it = view.begin(); it != view.end(); ++it) { + auto [sp] = *it; + sp->visible = false; + } + } +} + +Texture *makeSolidTex(Graphics *gfx, uint8_t r, uint8_t g, uint8_t b) { + const uint8_t px[4] = {r, g, b, 255}; + return gfx->newTexture(1, 1, px); +} + +Mesh *makeGroundMesh(Graphics *gfx, float sizeX, float sizeZ) { + std::vector pos, nrm; + std::vector idx; + eve::graphics::grass::makePlane(sizeX, sizeZ, 12, 12, pos, nrm, idx); + std::vector uv(size_t(pos.size() / 3) * 2u, 0.f); + for (size_t i = 0; i < pos.size() / 3; ++i) { + uv[i * 2] = pos[i * 3] / sizeX + 0.5f; + uv[i * 2 + 1] = pos[i * 3 + 2] / sizeZ + 0.5f; + } + return gfx->newMeshFromArrays(pos.data(), nrm.data(), uv.data(), int(pos.size() / 3), + idx.data(), int(idx.size())); +} + +void savePng(eve::image::ImageData *frame, const std::string &path) { + REQUIRE(frame != nullptr); + eve::image::Image::create(); + eve::filesystem::FileData *png = + frame->encode(medialoader::FormatHandler::ENCODED_PNG, "grass.png", false); + REQUIRE(png != nullptr); + REQUIRE(png->getSize() > 0); + std::error_code ec; + std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); + { + std::ofstream out(path, std::ios::binary); + REQUIRE(out.good()); + out.write(static_cast(png->getData()), + static_cast(png->getSize())); + REQUIRE(out.good()); + } + delete png; + std::printf("grass render saved: %s\n", path.c_str()); +} + +} // namespace + +TEST_CASE("graphics.Grass.gpuRenderScreenshot") { + auto *win = eve::window::Window::create(); + auto *gfx = Graphics::create(); + REQUIRE(win != nullptr); + REQUIRE(gfx != nullptr); + win->setGraphics(gfx); + gfx->setMsaaSamples(0); + eve::window::WindowSettings s; + s.width = 960; + s.height = 540; + s.centered = true; + REQUIRE(win->setWindowSettings(s)); + + resetGrassScene(); + + auto *rc = gfx->getRenderControl(); + REQUIRE(rc != nullptr); + rc->disable("aa"); + rc->disable("msaa"); + rc->disable("ao"); + rc->disable("gi"); + rc->enable("shadow"); + rc->enable("forward"); + rc->compile(); + + constexpr float kField = 10.f; + auto *ground = Renderable3D::create(); + ground->setMesh(makeGroundMesh(gfx, kField, kField)); + ground->setTexture(makeSolidTex(gfx, 92, 72, 42)); + ground->setPosition(0.f, 0.f, 0.f); + ground->setMetallic(0.f); + ground->setRoughness(0.92f); + ground->setCastShadow(false); + ground->setReceiveShadow(true); + + GrassField::BakeParams bake; + bake.denseRadius = 0.28f; + bake.sparseRadius = 1.15f; + bake.maxDense = 1800; + bake.maxSparse = 80; + bake.width = 0.42f; + bake.height = 0.78f; + bake.seed = 11; + bake.minSlopeDot = 0.2f; + + std::unique_ptr field(gfx->newGrassField()); + REQUIRE(field != nullptr); + field->bakePlane(kField, kField, 16, 16, bake); + field->setTime(0.22f); + REQUIRE(field->getDenseCount() >= 40); + REQUIRE(field->getDenseMesh() != nullptr); + REQUIRE(field->getShader() != nullptr); + REQUIRE(field->getAtlas() != nullptr); + + auto *dense = Renderable3D::create(); + dense->setMesh(field->getDenseMesh()); + dense->setTexture(field->getAtlas()); + dense->setShader(field->getShader()); + dense->setCastShadow(false); + dense->setReceiveShadow(true); + + if (field->getSparseMesh()) { + auto *sparse = Renderable3D::create(); + sparse->setMesh(field->getSparseMesh()); + sparse->setTexture(field->getAtlas()); + sparse->setShader(field->getShader()); + sparse->setCastShadow(false); + sparse->setReceiveShadow(true); + } + + auto *post = Renderable3D::create(); + post->setMesh(gfx->newMeshCylinder(18, 1, true)); + post->setTexture(makeSolidTex(gfx, 168, 148, 122)); + post->setPosition(1.35f, 0.95f, -0.4f); + post->setScale(0.28f, 0.95f, 0.28f); + post->setMetallic(0.f); + post->setRoughness(0.7f); + post->setCastShadow(true); + post->setReceiveShadow(false); + + auto *cam = Camera3D::createCamera(); + cam->setEye(6.4f, 4.6f, 7.8f); + cam->setTarget(0.f, 0.25f, 0.f); + cam->setAmbient(0.18f, 0.20f, 0.16f); + cam->data()->nearZ = 0.1f; + cam->data()->farZ = 80.f; + + auto *sun = Light3D::createLight("dir"); + sun->setDirection(0.62f, 1.05f, 0.28f); + sun->setColor(1.f, 0.97f, 0.88f, 2.4f); + sun->setCastShadow(true); + sun->setShadowStrength(1.f); + sun->setShadowBias(0.003f); + + auto *hud = Renderable2D::create(); + hud->transform()->x = 0; + hud->transform()->y = 0; + hud->sprite()->width = 2; + hud->sprite()->height = 2; + hud->sprite()->r = 0.f; + hud->sprite()->g = 0.f; + hud->sprite()->b = 0.f; + hud->sprite()->a = 0.f; + hud->sprite()->visible = true; + + gfx->setScreenReadbackEnabled(true); + gfx->setBackgroundColor(::Color(0.52f, 0.72f, 0.92f, 1.f)); + + for (int i = 0; i < 4; ++i) { + RenderSystem3D::render(*gfx); + RenderSystem::render(*gfx); + } + + int greenish = 0; + int darkGreen = 0; + int litGreen = 0; + const int w = gfx->getWidth(); + const int h = gfx->getHeight(); + for (int y = 0; y < h; y += 2) { + for (int x = 0; x < w; x += 2) { + const ::Color c = gfx->getPixel(x, y); + if (c.g > c.r + 0.04f && c.g > c.b + 0.02f) { + ++greenish; + const float luma = (c.r + c.g + c.b) / 3.f; + if (luma < 0.28f) ++darkGreen; + if (luma > 0.38f) ++litGreen; + } + } + } + CHECK(greenish > 80); + CHECK(darkGreen > 4); + CHECK(litGreen > 8); + + eve::image::Image::create(); + std::unique_ptr frame(gfx->newImageData()); + REQUIRE(frame != nullptr); + const std::string outDir = std::string(EVENGINE_TEST_BINARY_DIR) + "/out"; + savePng(frame.get(), outDir + "/grass_field.png"); + savePng(frame.get(), "/opt/cursor/artifacts/grass_field.png"); + + win->close(); +} From 23e83a4dcb5003309391fb109ea404e744ad8a0f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 15 Aug 2026 06:37:04 +0000 Subject: [PATCH 18/21] fix(test): avoid unique_ptr in grass GPU REQUIRE checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zeroerr Expression copies the compared value, which is illegal for std::unique_ptr. Compare raw pointers instead. Co-authored-by: 西风逍遥游 --- test/grass.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/grass.cpp b/test/grass.cpp index ccf52ed6..aefffefa 100644 --- a/test/grass.cpp +++ b/test/grass.cpp @@ -375,7 +375,7 @@ TEST_CASE("graphics.Grass.gpuRenderScreenshot") { bake.seed = 11; bake.minSlopeDot = 0.2f; - std::unique_ptr field(gfx->newGrassField()); + GrassField *field = gfx->newGrassField(); REQUIRE(field != nullptr); field->bakePlane(kField, kField, 16, 16, bake); field->setTime(0.22f); @@ -464,11 +464,12 @@ TEST_CASE("graphics.Grass.gpuRenderScreenshot") { CHECK(litGreen > 8); eve::image::Image::create(); - std::unique_ptr frame(gfx->newImageData()); + eve::image::ImageData *frame = gfx->newImageData(); REQUIRE(frame != nullptr); + std::unique_ptr ownedFrame(frame); const std::string outDir = std::string(EVENGINE_TEST_BINARY_DIR) + "/out"; - savePng(frame.get(), outDir + "/grass_field.png"); - savePng(frame.get(), "/opt/cursor/artifacts/grass_field.png"); + savePng(frame, outDir + "/grass_field.png"); + savePng(frame, "/opt/cursor/artifacts/grass_field.png"); win->close(); } From e8e11ef2f9447687e91f9559a90efe82953c8d4a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 15 Aug 2026 06:43:13 +0000 Subject: [PATCH 19/21] fix(test): make grass sway-frame advance check wrap-safe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.48s at 0.12s/frame is exactly 4 frames and can land on the same atlas index after modulo. Use 0.36s (3 frames) so the CHECK is stable. Co-authored-by: 西风逍遥游 --- test/grass.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/grass.cpp b/test/grass.cpp index aefffefa..8d645f08 100644 --- a/test/grass.cpp +++ b/test/grass.cpp @@ -197,7 +197,7 @@ TEST_CASE("graphics.Grass.swayDesyncByInstanceId") { CHECK(c < 4); CHECK((a != b || b != c)); - const int later = eve::graphics::grass::swayFrame(0.48f, 0.12f, 0, 4); + const int later = eve::graphics::grass::swayFrame(0.36f, 0.12f, 0, 4); CHECK(later != a); } @@ -325,7 +325,7 @@ void savePng(eve::image::ImageData *frame, const std::string &path) { REQUIRE(out.good()); } delete png; - std::printf("grass render saved: %s\n", path.c_str()); + std::printf("\ngrass render saved: %s\n", path.c_str()); } } // namespace From 5b2bcdcbef11ef330483df1ad0eb3bfac08d62fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E9=A3=8E=E9=80=8D=E9=81=A5=E6=B8=B8?= Date: Sat, 15 Aug 2026 15:09:12 +0800 Subject: [PATCH 20/21] feat(procgen): add Perlin heightmap sampling for pixels and tilemaps Provide a deterministic sample(x, y) terrain function so height can be queried at any map coordinate, baked into an in-memory heightmap, or classified into a Grid2D for tilemap generation. Co-authored-by: Cursor --- src/modules/procgen/Procgen.cpp | 92 +++++++++ src/modules/procgen/Procgen.h | 12 ++ .../procgen/algorithms/BuiltinAlgorithms.cpp | 3 + .../procgen/heightmap/BuiltinHeightmap.cpp | 38 ++++ src/modules/procgen/heightmap/Heightmap.cpp | 95 ++++++++++ src/modules/procgen/heightmap/Heightmap.h | 51 +++++ .../procgen/heightmap/TerrainSampler.cpp | 178 ++++++++++++++++++ .../procgen/heightmap/TerrainSampler.h | 111 +++++++++++ src/modules/procgen/texture/NoiseField.cpp | 58 ++++++ src/modules/procgen/texture/NoiseField.h | 5 + 10 files changed, 643 insertions(+) create mode 100644 src/modules/procgen/heightmap/BuiltinHeightmap.cpp create mode 100644 src/modules/procgen/heightmap/Heightmap.cpp create mode 100644 src/modules/procgen/heightmap/Heightmap.h create mode 100644 src/modules/procgen/heightmap/TerrainSampler.cpp create mode 100644 src/modules/procgen/heightmap/TerrainSampler.h diff --git a/src/modules/procgen/Procgen.cpp b/src/modules/procgen/Procgen.cpp index b46e874c..314a18c7 100644 --- a/src/modules/procgen/Procgen.cpp +++ b/src/modules/procgen/Procgen.cpp @@ -260,6 +260,40 @@ bool Procgen::hasMeshRecipe(const std::string &recipeId) const { return MeshRecipeRegistry::instance().has(recipeId); } +TerrainSampler *Procgen::newTerrainSampler() { return new TerrainSampler(); } + +Heightmap *Procgen::newHeightmap(int width, int height) { + return new Heightmap(width, height); +} + +Heightmap *Procgen::generateHeightmap(Params *params) { + lastError_.clear(); + if (!params) { + lastError_ = "generateHeightmap: null params"; + return nullptr; + } + const TerrainSampler sampler = TerrainSampler::fromParams(*params); + return new Heightmap(Heightmap::generate(sampler, params->getWidth(), params->getHeight())); +} + +bool Procgen::heightmapToGrid(Heightmap *heightmap, Params *params, Grid2D *out) { + lastError_.clear(); + if (!heightmap) { + lastError_ = "heightmapToGrid: null heightmap"; + return false; + } + if (!out) { + lastError_ = "heightmapToGrid: null grid"; + return false; + } + const TerrainBands bands = params ? TerrainBands::fromParams(*params) : TerrainBands(); + if (!heightmap->toGrid(*out, bands)) { + lastError_ = "heightmapToGrid: heightmap is empty"; + return false; + } + return true; +} + void Procgen::expose(ssq::Table &table) { auto cls = table.addClass(name, Procgen::create, false); expose(cls); @@ -331,6 +365,60 @@ void Procgen::expose(ssq::Table &table) { mesh.addFunc("getIndex", &MeshBuild::getIndex); mesh.addFunc("setMeta", &MeshBuild::setMeta); mesh.addFunc("getMeta", &MeshBuild::getMeta); + + auto sampler = table.addClass( + "ProcgenTerrainSampler", + std::function([]() -> TerrainSampler * { return nullptr; }), true); + sampler.addFunc("sample", &TerrainSampler::sample); + sampler.addFunc("sampleTile", &TerrainSampler::sampleTile); + sampler.addFunc("setSeed", &TerrainSampler::setSeed); + sampler.addFunc("getSeed", &TerrainSampler::getSeed); + sampler.addFunc("setScale", &TerrainSampler::setScale); + sampler.addFunc("getScale", &TerrainSampler::getScale); + sampler.addFunc("setFrequency", &TerrainSampler::setFrequency); + sampler.addFunc("getFrequency", &TerrainSampler::getFrequency); + sampler.addFunc("setWavelength", &TerrainSampler::setWavelength); + sampler.addFunc("getWavelength", &TerrainSampler::getWavelength); + sampler.addFunc("setOctaves", &TerrainSampler::setOctaves); + sampler.addFunc("getOctaves", &TerrainSampler::getOctaves); + sampler.addFunc("setLacunarity", &TerrainSampler::setLacunarity); + sampler.addFunc("getLacunarity", &TerrainSampler::getLacunarity); + sampler.addFunc("setGain", &TerrainSampler::setGain); + sampler.addFunc("getGain", &TerrainSampler::getGain); + sampler.addFunc("setRidge", &TerrainSampler::setRidge); + sampler.addFunc("getRidge", &TerrainSampler::getRidge); + sampler.addFunc("setWarp", &TerrainSampler::setWarp); + sampler.addFunc("getWarp", &TerrainSampler::getWarp); + sampler.addFunc("setExponent", &TerrainSampler::setExponent); + sampler.addFunc("getExponent", &TerrainSampler::getExponent); + sampler.addFunc("setContinent", &TerrainSampler::setContinent); + sampler.addFunc("getContinent", &TerrainSampler::getContinent); + sampler.addFunc("setIsland", &TerrainSampler::setIsland); + sampler.addFunc("getIsland", &TerrainSampler::getIsland); + sampler.addFunc("setCoastSoftness", &TerrainSampler::setCoastSoftness); + sampler.addFunc("getCoastSoftness", &TerrainSampler::getCoastSoftness); + sampler.addFunc("setWorldSize", &TerrainSampler::setWorldSize); + sampler.addFunc("getWorldWidth", &TerrainSampler::getWorldWidth); + sampler.addFunc("getWorldHeight", &TerrainSampler::getWorldHeight); + sampler.addFunc("setBase", &TerrainSampler::setBase); + sampler.addFunc("getBase", &TerrainSampler::getBase); + sampler.addFunc("setAmplitude", &TerrainSampler::setAmplitude); + sampler.addFunc("getAmplitude", &TerrainSampler::getAmplitude); + sampler.addFunc("setClamp", &TerrainSampler::setClamp); + sampler.addFunc("isClamped", &TerrainSampler::isClamped); + sampler.addFunc("getClampMin", &TerrainSampler::getClampMin); + sampler.addFunc("getClampMax", &TerrainSampler::getClampMax); + + auto heightmap = table.addClass( + "ProcgenHeightmap", std::function([]() -> Heightmap * { return nullptr; }), + true); + heightmap.addFunc("resize", &Heightmap::resize); + heightmap.addFunc("getWidth", &Heightmap::getWidth); + heightmap.addFunc("getHeight", &Heightmap::getHeight); + heightmap.addFunc("setHeight", &Heightmap::setHeight); + heightmap.addFunc("height", &Heightmap::height); + heightmap.addFunc("sampleBilinear", &Heightmap::sampleBilinear); + heightmap.addFunc("sampleBilinearSeamless", &Heightmap::sampleBilinearSeamless); } void Procgen::expose(ssq::Class &cls) { @@ -359,6 +447,10 @@ void Procgen::expose(ssq::Class &cls) { cls.addFunc("getMeshRecipeCount", &Procgen::getMeshRecipeCount); cls.addFunc("getMeshRecipeId", &Procgen::getMeshRecipeId); cls.addFunc("hasMeshRecipe", &Procgen::hasMeshRecipe); + cls.addFunc("newTerrainSampler", &Procgen::newTerrainSampler); + cls.addFunc("newHeightmap", &Procgen::newHeightmap); + cls.addFunc("generateHeightmap", &Procgen::generateHeightmap); + cls.addFunc("heightmapToGrid", &Procgen::heightmapToGrid); } } // namespace eve::procgen diff --git a/src/modules/procgen/Procgen.h b/src/modules/procgen/Procgen.h index ddeb27e2..f5e2840c 100644 --- a/src/modules/procgen/Procgen.h +++ b/src/modules/procgen/Procgen.h @@ -6,6 +6,8 @@ #include "procgen/OutputSpec.h" #include "procgen/Palette.h" #include "procgen/Params.h" +#include "procgen/heightmap/Heightmap.h" +#include "procgen/heightmap/TerrainSampler.h" #include #include @@ -81,6 +83,16 @@ public: std::string getMeshRecipeId(int index) const; bool hasMeshRecipe(const std::string &recipeId) const; + // --- Phase D: terrain height sampling --- + /** Sampling function over continuous map coordinates (caller owns). */ + TerrainSampler *newTerrainSampler(); + /** Empty in-memory heightmap (caller owns). */ + Heightmap *newHeightmap(int width, int height); + /** Build a sampler from params (seed/scale/octaves/…) and materialize it (caller owns). */ + Heightmap *generateHeightmap(Params *params); + /** Classify a heightmap into a semantic Grid2D using params bands (waterMax…). */ + bool heightmapToGrid(Heightmap *heightmap, Params *params, Grid2D *out); + PaletteTable &palettes() { return palettes_; } private: diff --git a/src/modules/procgen/algorithms/BuiltinAlgorithms.cpp b/src/modules/procgen/algorithms/BuiltinAlgorithms.cpp index d4ac877e..9db6087d 100644 --- a/src/modules/procgen/algorithms/BuiltinAlgorithms.cpp +++ b/src/modules/procgen/algorithms/BuiltinAlgorithms.cpp @@ -195,6 +195,8 @@ bool genCaveDrunkard(const Params ¶ms, Grid2D &out, std::string &error) { // Defined in WfcSimple.cpp void registerWfcSimple(GeneratorRegistry ®istry); +// Defined in heightmap/BuiltinHeightmap.cpp +void registerTerrainHeightmapAlgorithm(GeneratorRegistry ®istry); void GeneratorRegistry::registerBuiltins() { if (builtinsRegistered_) return; @@ -204,6 +206,7 @@ void GeneratorRegistry::registerBuiltins() { registerAlgorithm("maze.backtrack", genMazeBacktrack); registerAlgorithm("noise.terrain", genNoiseTerrain); registerWfcSimple(*this); + registerTerrainHeightmapAlgorithm(*this); builtinsRegistered_ = true; } diff --git a/src/modules/procgen/heightmap/BuiltinHeightmap.cpp b/src/modules/procgen/heightmap/BuiltinHeightmap.cpp new file mode 100644 index 00000000..e98afe11 --- /dev/null +++ b/src/modules/procgen/heightmap/BuiltinHeightmap.cpp @@ -0,0 +1,38 @@ +#include "procgen/GeneratorRegistry.h" +#include "procgen/Semantic.h" +#include "procgen/heightmap/Heightmap.h" + +#include + +namespace eve::procgen { +namespace { + +bool genTerrainHeightmap(const Params ¶ms, Grid2D &out, std::string &error) { + const int w = params.getWidth(); + const int h = params.getHeight(); + if (w < 1 || h < 1) { + error = "terrain.heightmap: width/height must be positive"; + return false; + } + const TerrainSampler sampler = TerrainSampler::fromParams(params); + const TerrainBands bands = TerrainBands::fromParams(params); + + const Heightmap hm = Heightmap::generate(sampler, w, h); + if (!hm.toGrid(out, bands)) { + error = "terrain.heightmap: failed to classify heightmap"; + return false; + } + + out.setMeta("algorithm", "terrain.heightmap"); + out.setMeta("scale", std::to_string(sampler.getScale())); + out.setMeta("octaves", std::to_string(sampler.getOctaves())); + return true; +} + +} // namespace + +void registerTerrainHeightmapAlgorithm(GeneratorRegistry ®istry) { + registry.registerAlgorithm("terrain.heightmap", genTerrainHeightmap); +} + +} // namespace eve::procgen diff --git a/src/modules/procgen/heightmap/Heightmap.cpp b/src/modules/procgen/heightmap/Heightmap.cpp new file mode 100644 index 00000000..0ef7356f --- /dev/null +++ b/src/modules/procgen/heightmap/Heightmap.cpp @@ -0,0 +1,95 @@ +#include "procgen/heightmap/Heightmap.h" + +#include +#include + +namespace eve::procgen { + +Heightmap::Heightmap(int width, int height) { resize(width, height); } + +void Heightmap::resize(int width, int height) { + width_ = width > 0 ? width : 0; + height_ = height > 0 ? height : 0; + data_.assign(size_t(width_) * size_t(height_), 0.f); +} + +int Heightmap::getWidth() const { return width_; } +int Heightmap::getHeight() const { return height_; } + +bool Heightmap::inBounds(int x, int y) const { + return x >= 0 && y >= 0 && x < width_ && y < height_; +} + +void Heightmap::setHeight(int x, int y, float h) { + if (!inBounds(x, y)) return; + data_[size_t(y * width_ + x)] = h; +} + +float Heightmap::height(int x, int y) const { + if (!inBounds(x, y)) return 0.f; + return data_[size_t(y * width_ + x)]; +} + +namespace { +float lerpHeight(float a, float b, float t) { return a + (b - a) * t; } +} // namespace + +float Heightmap::sampleBilinear(float x, float y) const { + if (width_ <= 0 || height_ <= 0) return 0.f; + const float fx = std::clamp(x, 0.f, float(width_ - 1)); + const float fy = std::clamp(y, 0.f, float(height_ - 1)); + const int x0 = int(std::floor(fx)); + const int y0 = int(std::floor(fy)); + const int x1 = std::min(x0 + 1, width_ - 1); + const int y1 = std::min(y0 + 1, height_ - 1); + const float tx = fx - float(x0); + const float ty = fy - float(y0); + const float top = lerpHeight(data_[size_t(y0 * width_ + x0)], data_[size_t(y0 * width_ + x1)], tx); + const float bot = lerpHeight(data_[size_t(y1 * width_ + x0)], data_[size_t(y1 * width_ + x1)], tx); + return lerpHeight(top, bot, ty); +} + +float Heightmap::sampleBilinearSeamless(float x, float y) const { + if (width_ <= 0 || height_ <= 0) return 0.f; + float px = std::fmod(x, float(width_)); + float py = std::fmod(y, float(height_)); + if (px < 0.f) px += float(width_); + if (py < 0.f) py += float(height_); + const int x0 = int(std::floor(px)); + const int y0 = int(std::floor(py)); + const int x1 = (x0 + 1) % width_; + const int y1 = (y0 + 1) % height_; + const float tx = px - float(x0); + const float ty = py - float(y0); + const float top = lerpHeight(data_[size_t(y0 * width_ + x0)], data_[size_t(y0 * width_ + x1)], tx); + const float bot = lerpHeight(data_[size_t(y1 * width_ + x0)], data_[size_t(y1 * width_ + x1)], tx); + return lerpHeight(top, bot, ty); +} + +Heightmap Heightmap::generate(const TerrainSampler &sampler, int width, int height) { + TerrainSampler local = sampler; + if (local.getWorldWidth() <= 0 || local.getWorldHeight() <= 0) { + local.setWorldSize(width, height); + } + Heightmap hm(width, height); + const auto fn = local.asFunction(); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + hm.setHeight(x, y, fn(float(x) + 0.5f, float(y) + 0.5f)); + } + } + return hm; +} + +bool Heightmap::toGrid(Grid2D &out, const TerrainBands &bands) const { + if (width_ <= 0 || height_ <= 0) return false; + out.resize(width_, height_); + for (int y = 0; y < height_; ++y) { + for (int x = 0; x < width_; ++x) { + out.setCell(x, y, int(bands.semanticAt(data_[size_t(y * width_ + x)]))); + } + } + return true; +} + +} // namespace eve::procgen diff --git a/src/modules/procgen/heightmap/Heightmap.h b/src/modules/procgen/heightmap/Heightmap.h new file mode 100644 index 00000000..4e7ea0e4 --- /dev/null +++ b/src/modules/procgen/heightmap/Heightmap.h @@ -0,0 +1,51 @@ +#pragma once + +#include "procgen/Grid2D.h" +#include "procgen/heightmap/TerrainSampler.h" + +#include +#include + +namespace eve::procgen { + +/** + * In-memory terrain heightmap: a dense float grid (row-major, + * index = y * width + x) materialized from a TerrainSampler or filled by hand. + * + * Use `generate()` to sample a TerrainSampler per-pixel, `height(x, y)` / + * `sampleBilinear(x, y)` to read values back, and `toGrid()` to classify the + * heights into a semantic Grid2D ready for tilemap generation. + */ +class Heightmap { +public: + Heightmap() = default; + Heightmap(int width, int height); + + void resize(int width, int height); + int getWidth() const; + int getHeight() const; + bool inBounds(int x, int y) const; + + void setHeight(int x, int y, float h); + float height(int x, int y) const; + + /** Bilinear height at continuous coordinate within [0, w) x [0, h). */ + float sampleBilinear(float x, float y) const; + /** Bilinear height with wrap-around (seamless tiling). */ + float sampleBilinearSeamless(float x, float y) const; + + const std::vector &data() const { return data_; } + + /** Materialize a Heightmap by sampling `sampler` at every pixel center. */ + static Heightmap generate(const TerrainSampler &sampler, int width, int height); + + /** Classify heights into a semantic Grid2D (source for tilemap generation). */ + bool toGrid(Grid2D &out, const TerrainBands &bands) const; + +private: + int width_ = 0; + int height_ = 0; + std::vector data_; +}; + +} // namespace eve::procgen diff --git a/src/modules/procgen/heightmap/TerrainSampler.cpp b/src/modules/procgen/heightmap/TerrainSampler.cpp new file mode 100644 index 00000000..2b4a649f --- /dev/null +++ b/src/modules/procgen/heightmap/TerrainSampler.cpp @@ -0,0 +1,178 @@ +#include "procgen/heightmap/TerrainSampler.h" + +#include "procgen/Semantic.h" + +#include +#include + +namespace eve::procgen { + +void TerrainSampler::setSeed(uint32_t seed) { field_.seed = seed; } +uint32_t TerrainSampler::getSeed() const { return field_.seed; } +void TerrainSampler::setScale(float scale) { frequency_ = std::max(0.0001f, scale); } +float TerrainSampler::getScale() const { return frequency_; } +void TerrainSampler::setFrequency(float frequency) { setScale(frequency); } +float TerrainSampler::getFrequency() const { return frequency_; } +void TerrainSampler::setWavelength(float wavelength) { + setFrequency(1.f / std::max(0.001f, wavelength)); +} +float TerrainSampler::getWavelength() const { return 1.f / frequency_; } +void TerrainSampler::setOctaves(int octaves) { octaves_ = std::max(1, octaves); } +int TerrainSampler::getOctaves() const { return octaves_; } +void TerrainSampler::setLacunarity(float lacunarity) { lacunarity_ = std::max(0.1f, lacunarity); } +float TerrainSampler::getLacunarity() const { return lacunarity_; } +void TerrainSampler::setGain(float gain) { gain_ = std::max(0.01f, gain); } +float TerrainSampler::getGain() const { return gain_; } +void TerrainSampler::setRidge(float ridge) { ridge_ = std::clamp(ridge, 0.f, 1.f); } +float TerrainSampler::getRidge() const { return ridge_; } +void TerrainSampler::setWarp(float warp) { warp_ = std::max(0.f, warp); } +float TerrainSampler::getWarp() const { return warp_; } +void TerrainSampler::setExponent(float exponent) { exponent_ = std::max(0.1f, exponent); } +float TerrainSampler::getExponent() const { return exponent_; } +void TerrainSampler::setContinent(float continent) { continent_ = std::clamp(continent, 0.f, 1.f); } +float TerrainSampler::getContinent() const { return continent_; } +void TerrainSampler::setIsland(float island) { island_ = std::clamp(island, 0.f, 1.f); } +float TerrainSampler::getIsland() const { return island_; } +void TerrainSampler::setCoastSoftness(float softness) { coastSoft_ = std::clamp(softness, 0.02f, 0.4f); } +float TerrainSampler::getCoastSoftness() const { return coastSoft_; } +void TerrainSampler::setWorldSize(int width, int height) { + worldW_ = width > 0 ? width : 0; + worldH_ = height > 0 ? height : 0; +} +int TerrainSampler::getWorldWidth() const { return worldW_; } +int TerrainSampler::getWorldHeight() const { return worldH_; } +void TerrainSampler::setBase(float base) { base_ = base; } +float TerrainSampler::getBase() const { return base_; } +void TerrainSampler::setAmplitude(float amplitude) { amplitude_ = std::max(0.f, amplitude); } +float TerrainSampler::getAmplitude() const { return amplitude_; } +void TerrainSampler::setClamp(bool enabled, float minHeight, float maxHeight) { + clamp_ = enabled; + clampMin_ = std::min(minHeight, maxHeight); + clampMax_ = std::max(minHeight, maxHeight); +} +bool TerrainSampler::isClamped() const { return clamp_; } +float TerrainSampler::getClampMin() const { return clampMin_; } +float TerrainSampler::getClampMax() const { return clampMax_; } + +namespace { +float smoother(float edge0, float edge1, float x) { + if (edge1 <= edge0) return x < edge0 ? 0.f : 1.f; + const float t = std::clamp((x - edge0) / (edge1 - edge0), 0.f, 1.f); + return t * t * (3.f - 2.f * t); +} +} // namespace + +float TerrainSampler::sample(float x, float y) const { + const float mx = x * frequency_; + const float my = y * frequency_; + + // Continent: 3 low/mid octaves (~100 / 50 / 25 px bays on a 512 map). + // Warp at the same scale so the silhouette twists without pixel breakup. + float cx = mx * 0.36f; + float cy = my * 0.36f; + const float maskWarp = 0.70f; + cx += (field_.perlinNoise(mx * 0.18f + 2.4f, my * 0.18f + 9.1f) - 0.5f) * maskWarp; + cy += (field_.perlinNoise(mx * 0.18f + 7.6f, my * 0.18f + 1.3f) - 0.5f) * maskWarp; + float macro = field_.fbmPerlin(cx, cy, 3, 2.f, 0.42f); + + if (island_ > 0.f && worldW_ > 0 && worldH_ > 0) { + const float nx = x / float(worldW_) * 2.f - 1.f; + const float ny = y / float(worldH_) * 2.f - 1.f; + const float dSq = 1.f - (1.f - nx * nx) * (1.f - ny * ny); + const float dEu = std::sqrt(std::min(1.f, nx * nx + ny * ny)); + const float d = std::clamp(dSq * 0.5f + dEu * 0.5f, 0.f, 1.f); + // Multiply, don't lerp: corners sink, the noise silhouette stays. + macro *= 1.f - island_ * std::pow(d, 1.25f); + } + + // Medium-scale shore wiggle (~70 px). Amplitude stays below the smoothstep + // width so it cannot punch isolated pixels through the waterline. + const float wiggle = + (field_.perlinNoise(mx * 0.42f + 11.f, my * 0.42f + 4.f) - 0.5f) * coastSoft_ * 0.55f; + const float sea = 0.54f - continent_ * 0.14f + wiggle; + const float land = smoother(sea - coastSoft_ * 0.45f, sea + coastSoft_ * 0.55f, macro); + const float inland = land * land; + + float dx = mx; + float dy = my; + if (warp_ > 0.f) { + dx += (field_.fbmPerlin(mx + 19.1f, my + 7.3f, 2) - 0.5f) * warp_; + dy += (field_.fbmPerlin(mx + 5.2f, my + 31.7f, 2) - 0.5f) * warp_; + } + + float detail = field_.fbmPerlin(dx, dy, octaves_, lacunarity_, gain_); + if (ridge_ > 0.f) { + const float mountains = + field_.ridgedPerlin(dx * 1.4f, dy * 1.4f, octaves_, lacunarity_, gain_); + detail = detail + (mountains - detail) * ridge_; + } + detail = std::pow(std::clamp(detail, 0.f, 1.f), exponent_); + + // land=0 stays below waterMax (0.25). Detail cannot open holes in the sea. + const float shelf = 0.06f + land * 0.32f; + const float e = shelf + inland * detail * 0.62f; + + float h = base_ + e * amplitude_; + if (clamp_) h = std::clamp(h, clampMin_, clampMax_); + return h; +} + +float TerrainSampler::sampleTile(int tileX, int tileY) const { + return sample(float(tileX) + 0.5f, float(tileY) + 0.5f); +} + +std::function TerrainSampler::asFunction() const { + return [self = *this](float x, float y) { return self.sample(x, y); }; +} + +TerrainSampler TerrainSampler::fromParams(const Params ¶ms) { + TerrainSampler s; + s.setSeed(params.getSeed()); + s.setWorldSize(params.getWidth(), params.getHeight()); + + const float wavelength = params.getFloat("wavelength", 0.f); + if (wavelength > 0.f) { + s.setWavelength(wavelength); + } else if (params.has("frequency")) { + s.setFrequency(params.getFloat("frequency", 1.f / 32.f)); + } else { + s.setScale(params.getFloat("scale", 1.f / 32.f)); + } + + s.setOctaves(params.getInt("octaves", 5)); + s.setLacunarity(params.getFloat("lacunarity", 2.f)); + s.setGain(params.getFloat("gain", 0.5f)); + s.setRidge(params.getFloat("ridge", 0.35f)); + s.setWarp(params.getFloat("warp", 0.35f)); + s.setExponent(params.getFloat("exponent", 2.f)); + s.setContinent(params.getFloat("continent", 0.55f)); + s.setIsland(params.getFloat("island", 0.38f)); + s.setCoastSoftness(params.getFloat("coast", 0.12f)); + s.setBase(params.getFloat("base", 0.f)); + s.setAmplitude(params.getFloat("amplitude", 1.f)); + const float lo = params.getFloat("heightMin", 0.f); + const float hi = params.getFloat("heightMax", 1.f); + s.setClamp(params.getInt("clamp", 1) != 0, lo, hi); + return s; +} + +uint32_t TerrainBands::semanticAt(float height) const { + if (height < waterMax) return Semantic::Water; + if (height < sandMax) return Semantic::Sand; + if (height < grassMax) return Semantic::Grass; + if (height < dirtMax) return Semantic::Dirt; + if (height < stoneMax) return Semantic::Stone; + return Semantic::Snow; +} + +TerrainBands TerrainBands::fromParams(const Params ¶ms) { + TerrainBands bands; + bands.waterMax = std::clamp(params.getFloat("waterMax", 0.25f), 0.f, 1.f); + bands.sandMax = std::clamp(params.getFloat("sandMax", 0.35f), 0.f, 1.f); + bands.grassMax = std::clamp(params.getFloat("grassMax", 0.65f), 0.f, 1.f); + bands.dirtMax = std::clamp(params.getFloat("dirtMax", 0.80f), 0.f, 1.f); + bands.stoneMax = std::clamp(params.getFloat("stoneMax", 0.92f), 0.f, 1.f); + return bands; +} + +} // namespace eve::procgen diff --git a/src/modules/procgen/heightmap/TerrainSampler.h b/src/modules/procgen/heightmap/TerrainSampler.h new file mode 100644 index 00000000..a9ab8423 --- /dev/null +++ b/src/modules/procgen/heightmap/TerrainSampler.h @@ -0,0 +1,111 @@ +#pragma once + +#include "procgen/Params.h" +#include "procgen/texture/NoiseField.h" + +#include +#include + +namespace eve::procgen { + +/** + * Deterministic terrain height sampling (Red Blob Games / Perlin fBm recipe). + * + * `sample(x, y)` maps any continuous map coordinate (tile/world units) to a + * height in [0, 1] by default. Same coordinate + same config ⇒ same height. + * + * Frequency is **cycles per world unit**. A wavelength of 32 (frequency 1/32) + * means one large hill every 32 tiles/pixels — this is what makes coherent + * terrain instead of white noise. `scale` is an alias for frequency. + * + * Pipeline: + * 1. 2-octave Perlin continent (low frequency) — this is the land shape + * 2. low-frequency domain warp of that field (bays / peninsulas) + * 3. optional multiplicative island falloff (kills map corners, not the shape) + * 4. smoothstep → solid coastline; hills / ridges only inland + */ +class TerrainSampler { +public: + TerrainSampler() = default; + + float sample(float x, float y) const; + /** Height at the center of map tile (tileX, tileY). */ + float sampleTile(int tileX, int tileY) const; + std::function asFunction() const; + + void setSeed(uint32_t seed); + uint32_t getSeed() const; + /** Cycles per world unit (alias of frequency). Default 1/32. */ + void setScale(float scale); + float getScale() const; + void setFrequency(float frequency); + float getFrequency() const; + /** Distance per large oscillation. Sets frequency = 1/wavelength. */ + void setWavelength(float wavelength); + float getWavelength() const; + void setOctaves(int octaves); + int getOctaves() const; + void setLacunarity(float lacunarity); + float getLacunarity() const; + void setGain(float gain); + float getGain() const; + void setRidge(float ridge); + float getRidge() const; + void setWarp(float warp); + float getWarp() const; + void setExponent(float exponent); + float getExponent() const; + void setContinent(float continent); + float getContinent() const; + void setIsland(float island); + float getIsland() const; + /** Shore width of the land mask smoothstep. Smaller = cleaner, harder coast. */ + void setCoastSoftness(float softness); + float getCoastSoftness() const; + void setWorldSize(int width, int height); + int getWorldWidth() const; + int getWorldHeight() const; + void setBase(float base); + float getBase() const; + void setAmplitude(float amplitude); + float getAmplitude() const; + void setClamp(bool enabled, float minHeight, float maxHeight); + bool isClamped() const; + float getClampMin() const; + float getClampMax() const; + + static TerrainSampler fromParams(const Params ¶ms); + +private: + NoiseField field_; + float frequency_ = 1.f / 32.f; + int octaves_ = 5; + float lacunarity_ = 2.f; + float gain_ = 0.5f; + float ridge_ = 0.35f; + float warp_ = 0.35f; + float exponent_ = 2.f; + float continent_ = 0.55f; + float island_ = 0.38f; + float coastSoft_ = 0.12f; + int worldW_ = 0; + int worldH_ = 0; + float base_ = 0.f; + float amplitude_ = 1.f; + bool clamp_ = true; + float clampMin_ = 0.f; + float clampMax_ = 1.f; +}; + +struct TerrainBands { + float waterMax = 0.25f; + float sandMax = 0.35f; + float grassMax = 0.65f; + float dirtMax = 0.80f; + float stoneMax = 0.92f; + + uint32_t semanticAt(float height) const; + static TerrainBands fromParams(const Params ¶ms); +}; + +} // namespace eve::procgen diff --git a/src/modules/procgen/texture/NoiseField.cpp b/src/modules/procgen/texture/NoiseField.cpp index 5530700f..9bc98476 100644 --- a/src/modules/procgen/texture/NoiseField.cpp +++ b/src/modules/procgen/texture/NoiseField.cpp @@ -7,6 +7,7 @@ namespace eve::procgen { namespace { float fade(float t) { return t * t * (3.f - 2.f * t); } +float fade5(float t) { return t * t * t * (t * (t * 6.f - 15.f) + 10.f); } int wrapIndex(int i, int period) { if (period <= 0) return i; @@ -42,6 +43,32 @@ float NoiseField::valueNoise(float x, float y) const { return nx0 + (nx1 - nx0) * fy; } +float NoiseField::perlinNoise(float x, float y) const { + const int x0 = int(std::floor(x)); + const int y0 = int(std::floor(y)); + const float dx = x - float(x0); + const float dy = y - float(y0); + const float fx = fade5(dx); + const float fy = fade5(dy); + + auto grad = [&](int ix, int iy, float px, float py) -> float { + // 8 compass gradients (Perlin 2002 style). + static const float gx[8] = {1.f, -1.f, 1.f, -1.f, 1.f, -1.f, 0.f, 0.f}; + static const float gy[8] = {1.f, 1.f, -1.f, -1.f, 0.f, 0.f, 1.f, -1.f}; + const int h = int(hash01(ix, iy) * 7.999f) & 7; + return gx[h] * (px - float(ix)) + gy[h] * (py - float(iy)); + }; + + const float n00 = grad(x0, y0, x, y); + const float n10 = grad(x0 + 1, y0, x, y); + const float n01 = grad(x0, y0 + 1, x, y); + const float n11 = grad(x0 + 1, y0 + 1, x, y); + const float nx0 = n00 + (n10 - n00) * fx; + const float nx1 = n01 + (n11 - n01) * fx; + const float n = nx0 + (nx1 - nx0) * fy; + return std::clamp(n * 0.5f + 0.5f, 0.f, 1.f); +} + float NoiseField::fbm(float x, float y, int octaves, float lacunarity, float gain) const { octaves = std::max(1, octaves); float sum = 0.f, amp = 1.f, norm = 0.f, freq = 1.f; @@ -54,6 +81,21 @@ float NoiseField::fbm(float x, float y, int octaves, float lacunarity, float gai return norm > 0.f ? sum / norm : 0.f; } +float NoiseField::fbmPerlin(float x, float y, int octaves, float lacunarity, float gain) const { + octaves = std::max(1, octaves); + float sum = 0.f, amp = 1.f, norm = 0.f, freq = 1.f; + for (int i = 0; i < octaves; ++i) { + // Offset each octave so they are uncorrelated (Red Blob Games). + const float ox = float(i) * 17.8f; + const float oy = float(i) * 23.5f; + sum += perlinNoise(x * freq + ox, y * freq + oy) * amp; + norm += amp; + amp *= gain; + freq *= lacunarity; + } + return norm > 0.f ? sum / norm : 0.f; +} + float NoiseField::ridged(float x, float y, int octaves, float lacunarity, float gain) const { octaves = std::max(1, octaves); float sum = 0.f, amp = 1.f, norm = 0.f, freq = 1.f; @@ -68,6 +110,22 @@ float NoiseField::ridged(float x, float y, int octaves, float lacunarity, float return norm > 0.f ? sum / norm : 0.f; } +float NoiseField::ridgedPerlin(float x, float y, int octaves, float lacunarity, float gain) const { + octaves = std::max(1, octaves); + float sum = 0.f, amp = 1.f, norm = 0.f, freq = 1.f; + for (int i = 0; i < octaves; ++i) { + const float ox = float(i) * 11.3f; + const float oy = float(i) * 19.7f; + float n = perlinNoise(x * freq + ox, y * freq + oy); + n = 1.f - std::fabs(n * 2.f - 1.f); + sum += n * n * amp; + norm += amp; + amp *= gain; + freq *= lacunarity; + } + return norm > 0.f ? sum / norm : 0.f; +} + float NoiseField::warp(float x, float y, float amp, int octaves) const { const float wx = fbm(x + 19.1f, y + 7.3f, octaves) - 0.5f; const float wy = fbm(x + 5.2f, y + 31.7f, octaves) - 0.5f; diff --git a/src/modules/procgen/texture/NoiseField.h b/src/modules/procgen/texture/NoiseField.h index fba8187c..4435e289 100644 --- a/src/modules/procgen/texture/NoiseField.h +++ b/src/modules/procgen/texture/NoiseField.h @@ -15,8 +15,13 @@ struct NoiseField { float hash01(int ix, int iy) const; float valueNoise(float x, float y) const; + /** Classic 2D Perlin gradient noise, remapped to [0, 1]. */ + float perlinNoise(float x, float y) const; float fbm(float x, float y, int octaves = 4, float lacunarity = 2.f, float gain = 0.5f) const; + float fbmPerlin(float x, float y, int octaves = 4, float lacunarity = 2.f, float gain = 0.5f) const; float ridged(float x, float y, int octaves = 4, float lacunarity = 2.f, float gain = 0.5f) const; + float ridgedPerlin(float x, float y, int octaves = 4, float lacunarity = 2.f, + float gain = 0.5f) const; float warp(float x, float y, float amp, int octaves = 3) const; }; From bc128ffdf3d7c68cb53f65247bea6f4115965832 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 15 Aug 2026 07:36:32 +0000 Subject: [PATCH 21/21] feat(graphics): use authored 2x2 grass and leaf sway atlases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Load the four grass and two leaf masks from test/Texture instead of painting a procedural strip. Pack them into one 2x2-per-variant atlas, sample frames by instance id, and tint with the existing two-tone green. Co-authored-by: 西风逍遥游 --- src/modules/graphics/Grass.cpp | 315 +++++++- src/modules/graphics/Grass.h | 56 +- .../graphics/shaders/mesh3d_grass.frag | 26 +- .../graphics/shaders/mesh3d_grass.frag.spv | Bin 11412 -> 13484 bytes .../shaders/mesh3d_grass_frag_spv.inc | 725 ++++++++++-------- test/Texture/grass_sway_0.png | Bin 0 -> 19446 bytes test/Texture/grass_sway_1.png | Bin 0 -> 27705 bytes test/Texture/grass_sway_2.png | Bin 0 -> 38266 bytes test/Texture/grass_sway_3.png | Bin 0 -> 18076 bytes test/Texture/leaf_sway_0.png | Bin 0 -> 17820 bytes test/Texture/leaf_sway_1.png | Bin 0 -> 23837 bytes test/grass.cpp | 124 ++- 12 files changed, 845 insertions(+), 401 deletions(-) create mode 100644 test/Texture/grass_sway_0.png create mode 100644 test/Texture/grass_sway_1.png create mode 100644 test/Texture/grass_sway_2.png create mode 100644 test/Texture/grass_sway_3.png create mode 100644 test/Texture/leaf_sway_0.png create mode 100644 test/Texture/leaf_sway_1.png diff --git a/src/modules/graphics/Grass.cpp b/src/modules/graphics/Grass.cpp index 4148b27b..3d6daae7 100644 --- a/src/modules/graphics/Grass.cpp +++ b/src/modules/graphics/Grass.cpp @@ -1,6 +1,7 @@ #include "graphics/Grass.h" #include "common/Exception.h" +#include "data/ByteData.h" #include "graphics/Graphics.h" #include "graphics/Mesh.h" #include "graphics/Shader.h" @@ -8,20 +9,27 @@ #include "graphics/TextureSampler.h" #include "graphics/shaders/mesh3d_grass_frag_spv.inc" #include "graphics/shaders/mesh3d_grass_vert_spv.inc" +#include "image/Image.h" +#include "image/ImageData.h" #include #include #include +#include +#include +#include +#include #include #include namespace eve::graphics::grass { namespace { -const std::array kParamSlots = { - "time", "frameDuration", "grassWidth", "grassHeight", "alphaCutoff", "alwaysDark", - "lightGreenX", "lightGreenY", "lightGreenZ", "darkGreenX", "darkGreenY", "darkGreenZ", - "frameCount"}; +const std::array kParamSlots = { + "time", "frameDuration", "grassWidth", "grassHeight", "alphaCutoff", + "alwaysDark", "lightGreenX", "lightGreenY", "lightGreenZ", "darkGreenX", + "darkGreenY", "darkGreenZ", "frameCount", "atlasCols", "atlasRows", + "grassVariantCount", "leafVariantCount", "leafRowOffset"}; std::vector copySpv(const uint32_t *data, size_t count) { return std::vector(data, data + count); @@ -165,30 +173,107 @@ float hash01(uint32_t id) { return h; } -void plotPixel(std::vector &rgba, int w, int h, int x, int y, uint8_t a) { +float clampf(float x, float lo, float hi) { return std::min(hi, std::max(lo, x)); } + +float coverEdge(float distPx, float radiusPx) { + // 1 inside the stroke, 0 outside, ~1px antialiased fringe for nearest upscale. + return clampf(radiusPx + 0.65f - distPx, 0.f, 1.f); +} + +void stampOver(std::vector &rgba, int w, int h, int x, int y, float luma, float a) { if (x < 0 || y < 0 || x >= w || y >= h) return; + a = clampf(a, 0.f, 1.f); + luma = clampf(luma, 0.f, 1.f); + if (a < 0.02f) return; const size_t i = (size_t(y) * size_t(w) + size_t(x)) * 4u; - if (a > rgba[i + 3]) { - rgba[i + 0] = 255; - rgba[i + 1] = 255; - rgba[i + 2] = 255; - rgba[i + 3] = a; + const float oa = float(rgba[i + 3]) / 255.f; + const float or_ = float(rgba[i + 0]) / 255.f; + const float og = float(rgba[i + 1]) / 255.f; + const float ob = float(rgba[i + 2]) / 255.f; + const float outA = a + oa * (1.f - a); + if (outA < 1e-4f) return; + const float nr = (luma * a + or_ * oa * (1.f - a)) / outA; + const float ng = (luma * a + og * oa * (1.f - a)) / outA; + const float nb = (luma * a + ob * oa * (1.f - a)) / outA; + rgba[i + 0] = uint8_t(std::round(clampf(nr, 0.f, 1.f) * 255.f)); + rgba[i + 1] = uint8_t(std::round(clampf(ng, 0.f, 1.f) * 255.f)); + rgba[i + 2] = uint8_t(std::round(clampf(nb, 0.f, 1.f) * 255.f)); + rgba[i + 3] = uint8_t(std::round(clampf(outA, 0.f, 1.f) * 255.f)); +} + +void stampBlade(std::vector &rgba, int atlasW, int atlasH, int ox, int frameW, int frameH, + float baseU, float height, float lean, float baseHalf, float tipHalf, float luma0, + float luma1) { + const float fw = float(frameW); + const float fh = float(frameH); + height = clampf(height, 0.12f, 1.f); + for (int y = 0; y < frameH; ++y) { + for (int x = 0; x < frameW; ++x) { + const float u = (float(x) + 0.5f) / fw; + const float v = 1.f - (float(y) + 0.5f) / fh; + if (v < -0.02f || v > height + 0.04f) continue; + const float t = clampf(v / height, 0.f, 1.f); + const float cx = baseU + lean * t * t; + const float half = baseHalf * (1.f - t * 0.82f) + tipHalf * t; + const float dist = std::abs(u - cx) * fw; + float cov = coverEdge(dist, half * fw); + cov *= 1.f - clampf((v - height) / 0.03f, 0.f, 1.f); + cov *= clampf((v + 0.02f) / 0.05f, 0.f, 1.f); + if (cov > 0.5f) cov = 1.f; + else if (cov < 0.25f) cov = 0.f; + if (cov <= 0.f) continue; + const float side = (u - cx) * fw; // +right + float luma = luma0 + (luma1 - luma0) * t; + if (side > 0.15f) luma *= 0.78f; // 1px-ish self-shadow on the right + stampOver(rgba, atlasW, atlasH, ox + x, y, luma, cov); + } } } -void drawBlade(std::vector &rgba, int atlasW, int atlasH, int ox, int frameW, int frameH, - float baseX, float lean, int thickness) { - const int steps = std::max(frameH - 2, 8); - for (int s = 0; s <= steps; ++s) { - const float t = float(s) / float(steps); // 0 root .. 1 tip - const float x = baseX + lean * t * t * float(frameW) * 0.45f; - const float yFromRoot = t * float(frameH - 2); - const int py = frameH - 1 - int(std::round(yFromRoot)); - const int px = ox + int(std::round(x)); - const uint8_t a = uint8_t(255.f * (1.f - t * 0.15f)); - const int half = std::max(1, int(std::round(float(thickness) * (1.f - t * 0.7f)))); - for (int dx = -half; dx <= half; ++dx) - plotPixel(rgba, atlasW, atlasH, px + dx, py, a); +struct BladeDesc { + float u; + float height; + float lean; + float baseHalf; + float tipHalf; + float luma0; + float luma1; +}; + +void stampTuft(std::vector &rgba, int atlasW, int atlasH, int ox, int frameW, int frameH, + float wind) { + // Tiny root pad only — a solid mound turns overlapping cards into a flat lime slab. + const float fw = float(frameW); + const float fh = float(frameH); + for (int y = 0; y < frameH; ++y) { + for (int x = 0; x < frameW; ++x) { + const float u = (float(x) + 0.5f) / fw; + const float v = 1.f - (float(y) + 0.5f) / fh; + if (v > 0.16f) continue; + const float cx = 0.50f + wind * 0.02f; + const float hw = 0.16f * (1.f - v / 0.16f); + float cov = coverEdge(std::abs(u - cx) * fw, hw * fw); + if (cov > 0.5f) cov = 1.f; + else if (cov < 0.25f) cov = 0.f; + if (cov > 0.f) stampOver(rgba, atlasW, atlasH, ox + x, y, 0.72f, cov); + } + } + + // 7 separated blades with gaps so later cards show through. Outline then fill. + const BladeDesc blades[] = { + {0.50f, 0.98f, 0.02f, 0.070f, 0.018f, 0.86f, 1.00f}, {0.38f, 0.90f, -0.14f, 0.062f, 0.016f, 0.82f, 0.97f}, + {0.62f, 0.92f, 0.16f, 0.062f, 0.016f, 0.82f, 0.97f}, {0.28f, 0.74f, -0.26f, 0.056f, 0.016f, 0.78f, 0.93f}, + {0.72f, 0.76f, 0.28f, 0.056f, 0.016f, 0.78f, 0.93f}, {0.44f, 0.84f, -0.06f, 0.050f, 0.014f, 0.84f, 0.98f}, + {0.56f, 0.86f, 0.08f, 0.050f, 0.014f, 0.84f, 0.98f}, + }; + const float outline = 1.15f / fw; + for (const BladeDesc &b : blades) { + const float lean = b.lean + wind * 0.32f; + const float u = b.u + wind * 0.03f; + stampBlade(rgba, atlasW, atlasH, ox, frameW, frameH, u, b.height, lean, b.baseHalf + outline, + b.tipHalf + outline * 0.5f, 0.42f, 0.55f); + stampBlade(rgba, atlasW, atlasH, ox, frameW, frameH, u, b.height, lean, b.baseHalf, b.tipHalf, + b.luma0, b.luma1); } } @@ -212,15 +297,35 @@ void bindDefaults(Shader *shader) { shader->declareVec3("lightGreen"); shader->declareVec3("darkGreen"); shader->declareFloat("frameCount"); + shader->declareFloat("atlasCols"); + shader->declareFloat("atlasRows"); + shader->declareFloat("grassVariantCount"); + shader->declareFloat("leafVariantCount"); + shader->declareFloat("leafRowOffset"); shader->sendFloat("time", 0.f); shader->sendFloat("frameDuration", 0.12f); - shader->sendFloat("grassWidth", 0.45f); - shader->sendFloat("grassHeight", 0.7f); + shader->sendFloat("grassWidth", 0.62f); + shader->sendFloat("grassHeight", 0.95f); shader->sendFloat("alphaCutoff", 0.35f); shader->sendFloat("alwaysDark", 0.f); - shader->sendVec3("lightGreen", 0.55f, 0.82f, 0.28f); - shader->sendVec3("darkGreen", 0.12f, 0.32f, 0.14f); + shader->sendVec3("lightGreen", 0.58f, 0.84f, 0.26f); + shader->sendVec3("darkGreen", 0.10f, 0.28f, 0.12f); shader->sendFloat("frameCount", 4.f); + shader->sendFloat("atlasCols", 2.f); + shader->sendFloat("atlasRows", 2.f); + shader->sendFloat("grassVariantCount", 1.f); + shader->sendFloat("leafVariantCount", 1.f); + shader->sendFloat("leafRowOffset", 0.f); +} + +void bindAtlasLayout(Shader *shader, const PackedAtlasInfo &info) { + if (!shader) throw eve::Exception("grass::bindAtlasLayout: null shader"); + shader->sendFloat("frameCount", float(std::max(info.frames, 1))); + shader->sendFloat("atlasCols", float(std::max(info.atlasCols, 1))); + shader->sendFloat("atlasRows", float(std::max(info.atlasRows, 1))); + shader->sendFloat("grassVariantCount", float(std::max(info.grassVariants, 1))); + shader->sendFloat("leafVariantCount", float(std::max(info.leafVariants, 1))); + shader->sendFloat("leafRowOffset", float(std::max(info.leafRowOffset, 0))); } void bindLayer(Shader *shader, bool alwaysDark) { @@ -269,18 +374,9 @@ void makeSwayAtlasRGBA(int frameW, int frameH, int frames, std::vector const int h = swayAtlasHeight(frameH); rgbaOut.assign(size_t(w * h * 4), 0); - const float bladeXs[] = {0.28f, 0.42f, 0.50f, 0.58f, 0.72f}; - const float bladeLean[] = {-0.22f, -0.08f, 0.02f, 0.12f, 0.26f}; - const int bladeThick[] = {1, 1, 2, 1, 1}; - for (int f = 0; f < frames; ++f) { const float wind = (float(f) - 1.5f) / 1.5f; // -1 .. +1 across 4 frames - const int ox = f * frameW; - for (int b = 0; b < 5; ++b) { - const float base = bladeXs[b] * float(frameW); - const float lean = (bladeLean[b] + wind * 0.55f); - drawBlade(rgbaOut, w, h, ox, frameW, frameH, base, lean, bladeThick[b]); - } + stampTuft(rgbaOut, w, h, f * frameW, frameW, frameH, wind); } } @@ -294,6 +390,131 @@ Texture *createSwayAtlas(Graphics *gfx, int frameW, int frameH, int frames) { info); } +namespace { + +bool readWholeFile(const std::string &path, std::vector &out) { + std::ifstream in(path, std::ios::binary); + if (!in) return false; + out.assign(std::istreambuf_iterator(in), std::istreambuf_iterator()); + return in.good() || !out.empty(); +} + +void maskToTintable(std::vector &rgba) { + for (size_t i = 0; i + 3 < rgba.size(); i += 4) { + const uint8_t luma = std::max(rgba[i], std::max(rgba[i + 1], rgba[i + 2])); + const uint8_t a = std::max(luma, rgba[i + 3]); + rgba[i + 0] = 255; + rgba[i + 1] = 255; + rgba[i + 2] = 255; + rgba[i + 3] = a; + } +} + +bool loadSwayMaskPng(const std::string &path, std::vector &rgba, int &w, int &h) { + std::vector raw; + if (!readWholeFile(path, raw)) return false; + eve::image::Image::create(); + eve::data::ByteData bytes(raw.data(), raw.size()); + std::unique_ptr img(eve::image::Image::create()->newImageData(&bytes)); + if (!img) return false; + w = img->getWidth(); + h = img->getHeight(); + if (w < 2 || h < 2) return false; + const size_t n = size_t(w) * size_t(h) * 4u; + if (img->getSize() < n) return false; + rgba.resize(n); + std::memcpy(rgba.data(), img->getData(), n); + maskToTintable(rgba); + return true; +} + +void blitRgba(std::vector &dst, int dw, int dh, int dx, int dy, + const std::vector &src, int sw, int sh) { + for (int y = 0; y < sh; ++y) { + const int ty = dy + y; + if (ty < 0 || ty >= dh) continue; + for (int x = 0; x < sw; ++x) { + const int tx = dx + x; + if (tx < 0 || tx >= dw) continue; + const size_t si = (size_t(y) * size_t(sw) + size_t(x)) * 4u; + const size_t di = (size_t(ty) * size_t(dw) + size_t(tx)) * 4u; + dst[di + 0] = src[si + 0]; + dst[di + 1] = src[si + 1]; + dst[di + 2] = src[si + 2]; + dst[di + 3] = src[si + 3]; + } + } +} + +} // namespace + +void packSwayAtlasRGBA(const std::vector &grassFiles, + const std::vector &leafFiles, std::vector &rgbaOut, + PackedAtlasInfo &info) { + if (grassFiles.empty()) throw eve::Exception("grass::packSwayAtlasRGBA: no grass atlas files"); + + struct Loaded { + std::vector rgba; + int w = 0; + int h = 0; + }; + auto loadOne = [](const std::string &path) { + Loaded img; + if (!loadSwayMaskPng(path, img.rgba, img.w, img.h)) + throw eve::Exception("grass::packSwayAtlasRGBA: failed to load '%s'", path.c_str()); + return img; + }; + + std::vector grass; + grass.reserve(grassFiles.size()); + for (const auto &p : grassFiles) grass.push_back(loadOne(p)); + std::vector leaf; + leaf.reserve(leafFiles.size()); + for (const auto &p : leafFiles) leaf.push_back(loadOne(p)); + + const int tw = grass.front().w; + const int th = grass.front().h; + auto checkSize = [tw, th](const Loaded &img, const char *kind) { + if (img.w != tw || img.h != th) + throw eve::Exception("grass::packSwayAtlasRGBA: %s atlas size mismatch (%dx%d vs %dx%d)", + kind, img.w, img.h, tw, th); + }; + for (const auto &img : grass) checkSize(img, "grass"); + for (const auto &img : leaf) checkSize(img, "leaf"); + + const int nGrass = int(grass.size()); + const int nLeaf = int(leaf.size()); + const int nX = std::max(nGrass, std::max(nLeaf, 1)); + const int nY = nLeaf > 0 ? 2 : 1; + info.frames = 4; + info.grassVariants = nGrass; + info.leafVariants = nLeaf > 0 ? nLeaf : 1; + info.leafRowOffset = nLeaf > 0 ? 2 : 0; + info.atlasCols = nX * 2; + info.atlasRows = nY * 2; + info.width = nX * tw; + info.height = nY * th; + rgbaOut.assign(size_t(info.width) * size_t(info.height) * 4u, 0); + + for (int i = 0; i < nGrass; ++i) + blitRgba(rgbaOut, info.width, info.height, i * tw, 0, grass[size_t(i)].rgba, tw, th); + for (int i = 0; i < nLeaf; ++i) + blitRgba(rgbaOut, info.width, info.height, i * tw, th, leaf[size_t(i)].rgba, tw, th); +} + +Texture *createSwayAtlasFromFiles(Graphics *gfx, const std::vector &grassFiles, + const std::vector &leafFiles, + PackedAtlasInfo *infoOut) { + if (!gfx) throw eve::Exception("grass::createSwayAtlasFromFiles: null graphics"); + PackedAtlasInfo info; + std::vector rgba; + packSwayAtlasRGBA(grassFiles, leafFiles, rgba, info); + if (infoOut) *infoOut = info; + TextureCreateInfo texInfo; + texInfo.sampler = TextureSampler::linear(); + return gfx->newTexture(info.width, info.height, rgba.data(), texInfo); +} + std::vector sampleHalton(const float *posXYZ, const float *nrmXYZ, int vertexCount, const uint32_t *indices, int indexCount, int count, uint32_t seed, float minSlopeDot) { @@ -471,14 +692,26 @@ void GrassField::bake(const float *posXYZ, const float *nrmXYZ, int vertexCount, const auto sparseMesh = grass::buildBillboards(sparsePts, params.width, params.height, true); if (!shader_) shader_ = grass::createShader(gfx_); - if (!atlas_) - atlas_ = grass::createSwayAtlas(gfx_, params.atlasFrameW, params.atlasFrameH, - params.atlasFrames); + grass::PackedAtlasInfo layout; + if (!params.grassAtlasFiles.empty()) { + atlas_ = grass::createSwayAtlasFromFiles(gfx_, params.grassAtlasFiles, params.leafAtlasFiles, + &layout); + } else { + if (!atlas_) + atlas_ = grass::createSwayAtlas(gfx_, params.atlasFrameW, params.atlasFrameH, + params.atlasFrames); + layout.frames = std::max(params.atlasFrames, 1); + layout.atlasCols = layout.frames; + layout.atlasRows = 1; + layout.grassVariants = 1; + layout.leafVariants = 1; + layout.leafRowOffset = 0; + } grass::bindDefaults(shader_); + grass::bindAtlasLayout(shader_, layout); shader_->sendFloat("grassWidth", params.width); shader_->sendFloat("grassHeight", params.height); - shader_->sendFloat("frameCount", float(std::max(params.atlasFrames, 1))); shader_->sendFloat("frameDuration", frameDuration_); shader_->sendFloat("time", time_); diff --git a/src/modules/graphics/Grass.h b/src/modules/graphics/Grass.h index e68ab4b5..3cb80544 100644 --- a/src/modules/graphics/Grass.h +++ b/src/modules/graphics/Grass.h @@ -35,8 +35,8 @@ struct Point { }; struct SampleParams { - float radius = 0.35f; - int maxPoints = 4096; + float radius = 0.14f; + int maxPoints = 8192; uint32_t seed = 1; /** Skip faces whose Y-up slope is below this (0 = keep walls). */ float minSlopeDot = 0.25f; @@ -65,22 +65,46 @@ std::vector sampleHalton(const float *posXYZ, const float *nrmXYZ, int ve * uv = quad corner in [0,1]^2 (0.5, 0) is the root * normal.x = instance id, normal.y = scale, normal.z = alwaysDark (0/1) */ -BillboardMesh buildBillboards(const std::vector &points, float width = 0.45f, - float height = 0.7f, bool alwaysDark = false); +BillboardMesh buildBillboards(const std::vector &points, float width = 0.62f, + float height = 0.95f, bool alwaysDark = false); /** Discrete 4-frame index with a per-instance phase offset. */ int swayFrame(float time, float frameDuration, uint32_t instanceId, int frameCount = 4); -/** Procedural 4-frame white+alpha sway atlas (horizontal strip). */ +/** + * Procedural 4-frame fallback atlas (horizontal strip). GPU paths should load + * authored 2x2 PNG masks via packSwayAtlasRGBA / createSwayAtlasFromFiles. + */ void makeSwayAtlasRGBA(int frameW, int frameH, int frames, std::vector &rgbaOut); int swayAtlasWidth(int frameW, int frames); int swayAtlasHeight(int frameH); -Texture *createSwayAtlas(Graphics *gfx, int frameW = 32, int frameH = 32, int frames = 4); +Texture *createSwayAtlas(Graphics *gfx, int frameW = 64, int frameH = 64, int frames = 4); + +/** Layout of a packed 2x2-per-variant sway atlas (4 grass + 2 leaf typical). */ +struct PackedAtlasInfo { + int width = 0; + int height = 0; + int atlasCols = 2; + int atlasRows = 2; + int grassVariants = 1; + int leafVariants = 1; + int leafRowOffset = 0; + int frames = 4; +}; + +/** Load white-on-black (or RGBA) 2x2 sway PNGs and pack them into one atlas. */ +void packSwayAtlasRGBA(const std::vector &grassFiles, + const std::vector &leafFiles, std::vector &rgbaOut, + PackedAtlasInfo &info); +Texture *createSwayAtlasFromFiles(Graphics *gfx, const std::vector &grassFiles, + const std::vector &leafFiles, + PackedAtlasInfo *infoOut = nullptr); Shader *createShader(Graphics *gfx); void bindDefaults(Shader *shader); void bindLayer(Shader *shader, bool alwaysDark); +void bindAtlasLayout(Shader *shader, const PackedAtlasInfo &info); void setTime(Shader *shader, float seconds); void setFrameDuration(Shader *shader, float seconds); @@ -100,17 +124,21 @@ void makePlane(float sizeX, float sizeZ, int segX, int segZ, std::vector class GrassField { public: struct BakeParams { - float denseRadius = 0.35f; - float sparseRadius = 1.4f; - int maxDense = 4096; - int maxSparse = 256; + /** Poisson spacing. Keep this well below `width` so tufts overlap. */ + float denseRadius = 0.14f; + float sparseRadius = 0.62f; + int maxDense = 8192; + int maxSparse = 384; uint32_t seed = 1; - float width = 0.45f; - float height = 0.7f; + float width = 0.62f; + float height = 0.95f; float minSlopeDot = 0.25f; - int atlasFrameW = 32; - int atlasFrameH = 32; + int atlasFrameW = 64; + int atlasFrameH = 64; int atlasFrames = 4; + /** Authored 2x2 sway masks (4 grass + 2 leaf). Empty = procedural strip. */ + std::vector grassAtlasFiles; + std::vector leafAtlasFiles; }; explicit GrassField(Graphics *gfx); diff --git a/src/modules/graphics/shaders/mesh3d_grass.frag b/src/modules/graphics/shaders/mesh3d_grass.frag index 4d07f885..b5c89079 100644 --- a/src/modules/graphics/shaders/mesh3d_grass.frag +++ b/src/modules/graphics/shaders/mesh3d_grass.frag @@ -3,6 +3,11 @@ // Push: [0] time [1] frameDuration [2] width [3] height // [4] alphaCutoff [5] alwaysDark // [6..8] lightGreen [9..11] darkGreen [12] frameCount +// [13] atlasCols [14] atlasRows +// [15] grassVariantCount [16] leafVariantCount [17] leafRowOffset +// Authored atlases are 2x2 per variant. Packed layout (4 grass + 2 leaf): +// 8 frame-cols x 4 frame-rows; grass variants along the top two rows, +// leaf variants along the bottom two. Strip atlases use atlasRows=1. layout(location = 0) in vec2 vUV; layout(location = 1) in vec3 vWorldPos; @@ -101,8 +106,27 @@ void main() { float t = u.data[0] / duration + hash * frames; int frame = int(mod(floor(t), frames)); + int cols = int(max(u.data[13], 1.0)); + int rows = int(max(u.data[14], 1.0)); + int grassVars = int(max(u.data[15], 1.0)); + int leafVars = int(max(u.data[16], 1.0)); + int leafRowOffset = int(u.data[17] + 0.5); + bool leaf = (vAlwaysDark > 0.5) || (u.data[5] > 0.5); + int nVar = leaf ? leafVars : grassVars; + int variant = int(mod(vInstanceId, float(nVar))); + // inUV.y = 0 at the root (bottom). Vulkan/image v=0 is the top row. - vec2 atlasUV = vec2((vUV.x + float(frame)) / frames, 1.0 - vUV.y); + vec2 atlasUV; + if (rows <= 1) { + atlasUV = vec2((vUV.x + float(frame)) / frames, 1.0 - vUV.y); + } else { + int localCol = frame % 2; + int localRow = frame / 2; + int col = variant * 2 + localCol; + int row = (leaf ? leafRowOffset : 0) + localRow; + atlasUV = vec2((float(col) + vUV.x) / float(cols), + (float(row) + (1.0 - vUV.y)) / float(rows)); + } vec4 mask = texture(albedoSampler, atlasUV) * vTint; if (mask.a < max(u.data[4], 0.01)) discard; diff --git a/src/modules/graphics/shaders/mesh3d_grass.frag.spv b/src/modules/graphics/shaders/mesh3d_grass.frag.spv index 8866d6723da4ce6c305feaa0bea15411a5ba76c2..c3a5b71016d9d4d3ea034eeb09411dfb72335b3d 100644 GIT binary patch literal 13484 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 G-2VXJ^LPLN diff --git a/src/modules/graphics/shaders/mesh3d_grass_frag_spv.inc b/src/modules/graphics/shaders/mesh3d_grass_frag_spv.inc index 175661dc..b33b4df6 100644 --- a/src/modules/graphics/shaders/mesh3d_grass_frag_spv.inc +++ b/src/modules/graphics/shaders/mesh3d_grass_frag_spv.inc @@ -1,8 +1,8 @@ static const uint32_t mesh3d_grass_frag_spv[] = { - 0x07230203, 0x00010000, 0x0008000b, 0x000001c0, 0x00000000, 0x00020011, 0x00000001, 0x00020011, + 0x07230203, 0x00010000, 0x0008000b, 0x0000021c, 0x00000000, 0x00020011, 0x00000001, 0x00020011, 0x00000032, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x000d000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x0000013b, - 0x00000154, 0x00000169, 0x00000179, 0x0000017f, 0x0000018c, 0x000001ae, 0x000001b9, 0x00030010, + 0x00000172, 0x0000018e, 0x000001c7, 0x000001d7, 0x000001dd, 0x0000020a, 0x00000215, 0x00030010, 0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x00040005, 0x00000004, 0x6e69616d, 0x00000000, 0x000a0005, 0x00000010, 0x706d6173, 0x6853656c, 0x776f6461, 0x63736143, 0x28656461, 0x3b336676, 0x663b3169, 0x00003b31, 0x00050005, 0x0000000d, 0x6c726f77, 0x736f5064, 0x00000000, @@ -28,333 +28,398 @@ static const uint32_t mesh3d_grass_frag_spv[] = { 0x00050006, 0x0000012d, 0x00000000, 0x61746164, 0x00000000, 0x00030005, 0x0000012f, 0x00000075, 0x00050005, 0x00000135, 0x61727564, 0x6e6f6974, 0x00000000, 0x00040005, 0x00000139, 0x68736168, 0x00000000, 0x00050005, 0x0000013b, 0x736e4976, 0x636e6174, 0x00644965, 0x00030005, 0x00000143, - 0x00000074, 0x00040005, 0x0000014c, 0x6d617266, 0x00000065, 0x00040005, 0x00000152, 0x616c7461, - 0x00565573, 0x00030005, 0x00000154, 0x00565576, 0x00040005, 0x00000160, 0x6b73616d, 0x00000000, - 0x00060005, 0x00000164, 0x65626c61, 0x61536f64, 0x656c706d, 0x00000072, 0x00040005, 0x00000169, - 0x6e695476, 0x00000074, 0x00050005, 0x00000177, 0x77656976, 0x74706544, 0x00000068, 0x00050005, - 0x00000179, 0x65695676, 0x736f5077, 0x00000000, 0x00030005, 0x0000017e, 0x00736976, 0x00050005, - 0x0000017f, 0x6f6f5276, 0x736f5074, 0x00000000, 0x00040005, 0x00000180, 0x61726170, 0x0000006d, - 0x00040005, 0x00000182, 0x61726170, 0x0000006d, 0x00050005, 0x0000018c, 0x776c4176, 0x44737961, - 0x006b7261, 0x00050005, 0x00000192, 0x6867696c, 0x65724774, 0x00006e65, 0x00050005, 0x0000019d, - 0x6b726164, 0x65657247, 0x0000006e, 0x00050005, 0x000001a7, 0x73617267, 0x6c6f4373, 0x00000000, - 0x00050005, 0x000001ae, 0x4374756f, 0x726f6c6f, 0x00000000, 0x00050005, 0x000001b9, 0x726f5776, - 0x6f50646c, 0x00000073, 0x00040005, 0x000001ba, 0x6867694c, 0x00443374, 0x00060006, 0x000001ba, - 0x00000000, 0x52736f70, 0x75696461, 0x00000073, 0x00050006, 0x000001ba, 0x00000001, 0x6f6c6f63, - 0x00000072, 0x00040005, 0x000001bd, 0x6d617246, 0x00000065, 0x00040006, 0x000001bd, 0x00000000, - 0x0070766d, 0x00050006, 0x000001bd, 0x00000001, 0x65646f6d, 0x0000006c, 0x00080006, 0x000001bd, - 0x00000002, 0x6867696c, 0x72694474, 0x65746e49, 0x7469736e, 0x00000079, 0x00060006, 0x000001bd, - 0x00000003, 0x6867696c, 0x6c6f4374, 0x0000726f, 0x00050006, 0x000001bd, 0x00000004, 0x746e6974, - 0x00000000, 0x00060006, 0x000001bd, 0x00000005, 0x656d6163, 0x6f506172, 0x00000073, 0x00050006, - 0x000001bd, 0x00000006, 0x69626d61, 0x00746e65, 0x00050006, 0x000001bd, 0x00000007, 0x6867696c, - 0x00007374, 0x00050006, 0x000001bd, 0x00000008, 0x42786574, 0x00626d6f, 0x00060006, 0x000001bd, - 0x00000009, 0x61726170, 0x78616c6c, 0x00000000, 0x00050006, 0x000001bd, 0x0000000a, 0x77656976, - 0x00000000, 0x00060006, 0x000001bd, 0x0000000b, 0x70696c63, 0x6f666e49, 0x00000000, 0x00030005, - 0x000001bf, 0x006f6275, 0x00040047, 0x00000021, 0x00000006, 0x00000040, 0x00030047, 0x00000022, - 0x00000002, 0x00040048, 0x00000022, 0x00000000, 0x00000005, 0x00050048, 0x00000022, 0x00000000, - 0x00000007, 0x00000010, 0x00050048, 0x00000022, 0x00000000, 0x00000023, 0x00000000, 0x00050048, - 0x00000022, 0x00000001, 0x00000023, 0x000000c0, 0x00050048, 0x00000022, 0x00000002, 0x00000023, - 0x000000d0, 0x00050048, 0x00000022, 0x00000003, 0x00000023, 0x000000e0, 0x00050048, 0x00000022, - 0x00000004, 0x00000023, 0x000000f0, 0x00040047, 0x00000024, 0x00000021, 0x00000004, 0x00040047, - 0x00000024, 0x00000022, 0x00000000, 0x00040047, 0x00000070, 0x00000021, 0x00000005, 0x00040047, - 0x00000070, 0x00000022, 0x00000000, 0x00040047, 0x0000012c, 0x00000006, 0x00000004, 0x00030047, - 0x0000012d, 0x00000002, 0x00050048, 0x0000012d, 0x00000000, 0x00000023, 0x00000000, 0x00040047, - 0x0000013b, 0x0000001e, 0x00000004, 0x00040047, 0x00000154, 0x0000001e, 0x00000000, 0x00040047, - 0x00000164, 0x00000021, 0x00000001, 0x00040047, 0x00000164, 0x00000022, 0x00000000, 0x00040047, - 0x00000169, 0x0000001e, 0x00000005, 0x00040047, 0x00000179, 0x0000001e, 0x00000002, 0x00040047, - 0x0000017f, 0x0000001e, 0x00000003, 0x00040047, 0x0000018c, 0x0000001e, 0x00000006, 0x00040047, - 0x000001ae, 0x0000001e, 0x00000000, 0x00040047, 0x000001b9, 0x0000001e, 0x00000001, 0x00050048, - 0x000001ba, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x000001ba, 0x00000001, 0x00000023, - 0x00000010, 0x00040047, 0x000001bc, 0x00000006, 0x00000020, 0x00030047, 0x000001bd, 0x00000002, - 0x00040048, 0x000001bd, 0x00000000, 0x00000005, 0x00050048, 0x000001bd, 0x00000000, 0x00000007, - 0x00000010, 0x00050048, 0x000001bd, 0x00000000, 0x00000023, 0x00000000, 0x00040048, 0x000001bd, - 0x00000001, 0x00000005, 0x00050048, 0x000001bd, 0x00000001, 0x00000007, 0x00000010, 0x00050048, - 0x000001bd, 0x00000001, 0x00000023, 0x00000040, 0x00050048, 0x000001bd, 0x00000002, 0x00000023, - 0x00000080, 0x00050048, 0x000001bd, 0x00000003, 0x00000023, 0x00000090, 0x00050048, 0x000001bd, - 0x00000004, 0x00000023, 0x000000a0, 0x00050048, 0x000001bd, 0x00000005, 0x00000023, 0x000000b0, - 0x00050048, 0x000001bd, 0x00000006, 0x00000023, 0x000000c0, 0x00050048, 0x000001bd, 0x00000007, - 0x00000023, 0x000000d0, 0x00050048, 0x000001bd, 0x00000008, 0x00000023, 0x000001d0, 0x00050048, - 0x000001bd, 0x00000009, 0x00000023, 0x000001e0, 0x00040048, 0x000001bd, 0x0000000a, 0x00000005, - 0x00050048, 0x000001bd, 0x0000000a, 0x00000007, 0x00000010, 0x00050048, 0x000001bd, 0x0000000a, - 0x00000023, 0x000001f0, 0x00050048, 0x000001bd, 0x0000000b, 0x00000023, 0x00000230, 0x00040047, - 0x000001bf, 0x00000021, 0x00000000, 0x00040047, 0x000001bf, 0x00000022, 0x00000000, 0x00020013, - 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, - 0x00000007, 0x00000006, 0x00000003, 0x00040020, 0x00000008, 0x00000007, 0x00000007, 0x00040015, - 0x00000009, 0x00000020, 0x00000001, 0x00040020, 0x0000000a, 0x00000007, 0x00000009, 0x00040020, - 0x0000000b, 0x00000007, 0x00000006, 0x00060021, 0x0000000c, 0x00000006, 0x00000008, 0x0000000a, - 0x0000000b, 0x00040021, 0x00000012, 0x00000006, 0x0000000a, 0x00050021, 0x00000016, 0x00000006, - 0x00000008, 0x0000000b, 0x00040017, 0x0000001b, 0x00000006, 0x00000004, 0x00040020, 0x0000001c, - 0x00000007, 0x0000001b, 0x00040018, 0x0000001e, 0x0000001b, 0x00000004, 0x00040015, 0x0000001f, - 0x00000020, 0x00000000, 0x0004002b, 0x0000001f, 0x00000020, 0x00000003, 0x0004001c, 0x00000021, - 0x0000001e, 0x00000020, 0x0007001e, 0x00000022, 0x00000021, 0x0000001b, 0x0000001b, 0x0000001b, - 0x0000001b, 0x00040020, 0x00000023, 0x00000002, 0x00000022, 0x0004003b, 0x00000023, 0x00000024, - 0x00000002, 0x0004002b, 0x00000009, 0x00000025, 0x00000000, 0x00040020, 0x00000027, 0x00000002, - 0x0000001e, 0x0004002b, 0x00000006, 0x0000002b, 0x3f800000, 0x0004002b, 0x00000006, 0x00000036, - 0x358637bd, 0x00040017, 0x0000003a, 0x00000006, 0x00000002, 0x00040020, 0x0000003b, 0x00000007, - 0x0000003a, 0x0004002b, 0x00000006, 0x0000003f, 0x3f000000, 0x0004002b, 0x0000001f, 0x00000044, - 0x00000002, 0x00020014, 0x00000047, 0x0004002b, 0x0000001f, 0x00000048, 0x00000000, 0x0004002b, - 0x00000006, 0x0000004b, 0x00000000, 0x0004002b, 0x0000001f, 0x00000057, 0x00000001, 0x00090019, - 0x0000006d, 0x00000006, 0x00000001, 0x00000001, 0x00000001, 0x00000000, 0x00000001, 0x00000000, - 0x0003001b, 0x0000006e, 0x0000006d, 0x00040020, 0x0000006f, 0x00000000, 0x0000006e, 0x0004003b, - 0x0000006f, 0x00000070, 0x00000000, 0x00040017, 0x00000073, 0x00000009, 0x00000003, 0x00040017, - 0x00000075, 0x00000009, 0x00000002, 0x0004002b, 0x00000009, 0x00000086, 0x00000009, 0x0004002b, - 0x0000001f, 0x0000008a, 0x00000009, 0x0004001c, 0x0000008b, 0x0000003a, 0x0000008a, 0x0004002b, - 0x00000006, 0x0000008c, 0xbf000000, 0x0005002c, 0x0000003a, 0x0000008d, 0x0000008c, 0x0000008c, - 0x0004002b, 0x00000006, 0x0000008e, 0xbf19999a, 0x0005002c, 0x0000003a, 0x0000008f, 0x0000004b, - 0x0000008e, 0x0005002c, 0x0000003a, 0x00000090, 0x0000003f, 0x0000008c, 0x0005002c, 0x0000003a, - 0x00000091, 0x0000008e, 0x0000004b, 0x0005002c, 0x0000003a, 0x00000092, 0x0000004b, 0x0000004b, - 0x0004002b, 0x00000006, 0x00000093, 0x3f19999a, 0x0005002c, 0x0000003a, 0x00000094, 0x00000093, - 0x0000004b, 0x0005002c, 0x0000003a, 0x00000095, 0x0000008c, 0x0000003f, 0x0005002c, 0x0000003a, - 0x00000096, 0x0000004b, 0x00000093, 0x0005002c, 0x0000003a, 0x00000097, 0x0000003f, 0x0000003f, - 0x000c002c, 0x0000008b, 0x00000098, 0x0000008d, 0x0000008f, 0x00000090, 0x00000091, 0x00000092, - 0x00000094, 0x00000095, 0x00000096, 0x00000097, 0x00040020, 0x0000009a, 0x00000007, 0x0000008b, - 0x0004002b, 0x00000009, 0x000000c9, 0x00000001, 0x0004002b, 0x00000006, 0x000000cc, 0x41100000, - 0x0004002b, 0x00000009, 0x000000d6, 0x00000003, 0x00040020, 0x000000d7, 0x00000002, 0x00000006, - 0x0004002b, 0x00000006, 0x000000e8, 0x322bcc77, 0x0004002b, 0x00000009, 0x000000ef, 0x00000002, - 0x0004002b, 0x00000006, 0x00000104, 0x38d1b717, 0x0004002b, 0x0000001f, 0x0000012b, 0x00000020, - 0x0004001c, 0x0000012c, 0x00000006, 0x0000012b, 0x0003001e, 0x0000012d, 0x0000012c, 0x00040020, - 0x0000012e, 0x00000009, 0x0000012d, 0x0004003b, 0x0000012e, 0x0000012f, 0x00000009, 0x0004002b, - 0x00000009, 0x00000130, 0x0000000c, 0x00040020, 0x00000131, 0x00000009, 0x00000006, 0x00040020, - 0x0000013a, 0x00000001, 0x00000006, 0x0004003b, 0x0000013a, 0x0000013b, 0x00000001, 0x0004002b, - 0x00000006, 0x0000013d, 0x414fd639, 0x0004002b, 0x00000006, 0x00000140, 0x472aee8c, 0x00040020, - 0x00000153, 0x00000001, 0x0000003a, 0x0004003b, 0x00000153, 0x00000154, 0x00000001, 0x00090019, - 0x00000161, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000, - 0x0003001b, 0x00000162, 0x00000161, 0x00040020, 0x00000163, 0x00000000, 0x00000162, 0x0004003b, - 0x00000163, 0x00000164, 0x00000000, 0x00040020, 0x00000168, 0x00000001, 0x0000001b, 0x0004003b, - 0x00000168, 0x00000169, 0x00000001, 0x0004002b, 0x00000009, 0x0000016e, 0x00000004, 0x0004002b, - 0x00000006, 0x00000171, 0x3c23d70a, 0x00040020, 0x00000178, 0x00000001, 0x00000007, 0x0004003b, - 0x00000178, 0x00000179, 0x00000001, 0x0004003b, 0x00000178, 0x0000017f, 0x00000001, 0x0004002b, - 0x00000009, 0x00000185, 0x00000005, 0x0004003b, 0x0000013a, 0x0000018c, 0x00000001, 0x0004002b, - 0x00000009, 0x00000193, 0x00000006, 0x0004002b, 0x00000009, 0x00000196, 0x00000007, 0x0004002b, - 0x00000009, 0x00000199, 0x00000008, 0x0004002b, 0x00000009, 0x000001a0, 0x0000000a, 0x0004002b, - 0x00000009, 0x000001a3, 0x0000000b, 0x00040020, 0x000001ad, 0x00000003, 0x0000001b, 0x0004003b, - 0x000001ad, 0x000001ae, 0x00000003, 0x0004003b, 0x00000178, 0x000001b9, 0x00000001, 0x0004001e, - 0x000001ba, 0x0000001b, 0x0000001b, 0x0004002b, 0x0000001f, 0x000001bb, 0x00000008, 0x0004001c, - 0x000001bc, 0x000001ba, 0x000001bb, 0x000e001e, 0x000001bd, 0x0000001e, 0x0000001e, 0x0000001b, - 0x0000001b, 0x0000001b, 0x0000001b, 0x0000001b, 0x000001bc, 0x0000001b, 0x0000001b, 0x0000001e, - 0x0000001b, 0x00040020, 0x000001be, 0x00000002, 0x000001bd, 0x0004003b, 0x000001be, 0x000001bf, - 0x00000002, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, - 0x0004003b, 0x0000000b, 0x0000012a, 0x00000007, 0x0004003b, 0x0000000b, 0x00000135, 0x00000007, - 0x0004003b, 0x0000000b, 0x00000139, 0x00000007, 0x0004003b, 0x0000000b, 0x00000143, 0x00000007, - 0x0004003b, 0x0000000a, 0x0000014c, 0x00000007, 0x0004003b, 0x0000003b, 0x00000152, 0x00000007, - 0x0004003b, 0x0000001c, 0x00000160, 0x00000007, 0x0004003b, 0x0000000b, 0x00000177, 0x00000007, - 0x0004003b, 0x0000000b, 0x0000017e, 0x00000007, 0x0004003b, 0x00000008, 0x00000180, 0x00000007, - 0x0004003b, 0x0000000b, 0x00000182, 0x00000007, 0x0004003b, 0x00000008, 0x00000192, 0x00000007, - 0x0004003b, 0x00000008, 0x0000019d, 0x00000007, 0x0004003b, 0x00000008, 0x000001a7, 0x00000007, - 0x00060041, 0x00000131, 0x00000132, 0x0000012f, 0x00000025, 0x00000130, 0x0004003d, 0x00000006, - 0x00000133, 0x00000132, 0x0007000c, 0x00000006, 0x00000134, 0x00000001, 0x00000028, 0x00000133, - 0x0000002b, 0x0003003e, 0x0000012a, 0x00000134, 0x00060041, 0x00000131, 0x00000136, 0x0000012f, - 0x00000025, 0x000000c9, 0x0004003d, 0x00000006, 0x00000137, 0x00000136, 0x0007000c, 0x00000006, - 0x00000138, 0x00000001, 0x00000028, 0x00000137, 0x00000104, 0x0003003e, 0x00000135, 0x00000138, - 0x0004003d, 0x00000006, 0x0000013c, 0x0000013b, 0x00050085, 0x00000006, 0x0000013e, 0x0000013c, - 0x0000013d, 0x0006000c, 0x00000006, 0x0000013f, 0x00000001, 0x0000000d, 0x0000013e, 0x00050085, - 0x00000006, 0x00000141, 0x0000013f, 0x00000140, 0x0006000c, 0x00000006, 0x00000142, 0x00000001, - 0x0000000a, 0x00000141, 0x0003003e, 0x00000139, 0x00000142, 0x00060041, 0x00000131, 0x00000144, - 0x0000012f, 0x00000025, 0x00000025, 0x0004003d, 0x00000006, 0x00000145, 0x00000144, 0x0004003d, - 0x00000006, 0x00000146, 0x00000135, 0x00050088, 0x00000006, 0x00000147, 0x00000145, 0x00000146, - 0x0004003d, 0x00000006, 0x00000148, 0x00000139, 0x0004003d, 0x00000006, 0x00000149, 0x0000012a, - 0x00050085, 0x00000006, 0x0000014a, 0x00000148, 0x00000149, 0x00050081, 0x00000006, 0x0000014b, - 0x00000147, 0x0000014a, 0x0003003e, 0x00000143, 0x0000014b, 0x0004003d, 0x00000006, 0x0000014d, - 0x00000143, 0x0006000c, 0x00000006, 0x0000014e, 0x00000001, 0x00000008, 0x0000014d, 0x0004003d, - 0x00000006, 0x0000014f, 0x0000012a, 0x0005008d, 0x00000006, 0x00000150, 0x0000014e, 0x0000014f, - 0x0004006e, 0x00000009, 0x00000151, 0x00000150, 0x0003003e, 0x0000014c, 0x00000151, 0x00050041, - 0x0000013a, 0x00000155, 0x00000154, 0x00000048, 0x0004003d, 0x00000006, 0x00000156, 0x00000155, - 0x0004003d, 0x00000009, 0x00000157, 0x0000014c, 0x0004006f, 0x00000006, 0x00000158, 0x00000157, - 0x00050081, 0x00000006, 0x00000159, 0x00000156, 0x00000158, 0x0004003d, 0x00000006, 0x0000015a, - 0x0000012a, 0x00050088, 0x00000006, 0x0000015b, 0x00000159, 0x0000015a, 0x00050041, 0x0000013a, - 0x0000015c, 0x00000154, 0x00000057, 0x0004003d, 0x00000006, 0x0000015d, 0x0000015c, 0x00050083, - 0x00000006, 0x0000015e, 0x0000002b, 0x0000015d, 0x00050050, 0x0000003a, 0x0000015f, 0x0000015b, - 0x0000015e, 0x0003003e, 0x00000152, 0x0000015f, 0x0004003d, 0x00000162, 0x00000165, 0x00000164, - 0x0004003d, 0x0000003a, 0x00000166, 0x00000152, 0x00050057, 0x0000001b, 0x00000167, 0x00000165, - 0x00000166, 0x0004003d, 0x0000001b, 0x0000016a, 0x00000169, 0x00050085, 0x0000001b, 0x0000016b, - 0x00000167, 0x0000016a, 0x0003003e, 0x00000160, 0x0000016b, 0x00050041, 0x0000000b, 0x0000016c, - 0x00000160, 0x00000020, 0x0004003d, 0x00000006, 0x0000016d, 0x0000016c, 0x00060041, 0x00000131, - 0x0000016f, 0x0000012f, 0x00000025, 0x0000016e, 0x0004003d, 0x00000006, 0x00000170, 0x0000016f, - 0x0007000c, 0x00000006, 0x00000172, 0x00000001, 0x00000028, 0x00000170, 0x00000171, 0x000500b8, - 0x00000047, 0x00000173, 0x0000016d, 0x00000172, 0x000300f7, 0x00000175, 0x00000000, 0x000400fa, - 0x00000173, 0x00000174, 0x00000175, 0x000200f8, 0x00000174, 0x000100fc, 0x000200f8, 0x00000175, - 0x00050041, 0x0000013a, 0x0000017a, 0x00000179, 0x00000044, 0x0004003d, 0x00000006, 0x0000017b, - 0x0000017a, 0x0004007f, 0x00000006, 0x0000017c, 0x0000017b, 0x0007000c, 0x00000006, 0x0000017d, - 0x00000001, 0x00000028, 0x0000017c, 0x0000004b, 0x0003003e, 0x00000177, 0x0000017d, 0x0004003d, - 0x00000007, 0x00000181, 0x0000017f, 0x0003003e, 0x00000180, 0x00000181, 0x0004003d, 0x00000006, - 0x00000183, 0x00000177, 0x0003003e, 0x00000182, 0x00000183, 0x00060039, 0x00000006, 0x00000184, - 0x00000019, 0x00000180, 0x00000182, 0x0003003e, 0x0000017e, 0x00000184, 0x00060041, 0x00000131, - 0x00000186, 0x0000012f, 0x00000025, 0x00000185, 0x0004003d, 0x00000006, 0x00000187, 0x00000186, - 0x000500ba, 0x00000047, 0x00000188, 0x00000187, 0x0000003f, 0x000400a8, 0x00000047, 0x00000189, - 0x00000188, 0x000300f7, 0x0000018b, 0x00000000, 0x000400fa, 0x00000189, 0x0000018a, 0x0000018b, - 0x000200f8, 0x0000018a, 0x0004003d, 0x00000006, 0x0000018d, 0x0000018c, 0x000500ba, 0x00000047, - 0x0000018e, 0x0000018d, 0x0000003f, 0x000200f9, 0x0000018b, 0x000200f8, 0x0000018b, 0x000700f5, - 0x00000047, 0x0000018f, 0x00000188, 0x00000175, 0x0000018e, 0x0000018a, 0x000300f7, 0x00000191, - 0x00000000, 0x000400fa, 0x0000018f, 0x00000190, 0x00000191, 0x000200f8, 0x00000190, 0x0003003e, - 0x0000017e, 0x0000004b, 0x000200f9, 0x00000191, 0x000200f8, 0x00000191, 0x00060041, 0x00000131, - 0x00000194, 0x0000012f, 0x00000025, 0x00000193, 0x0004003d, 0x00000006, 0x00000195, 0x00000194, - 0x00060041, 0x00000131, 0x00000197, 0x0000012f, 0x00000025, 0x00000196, 0x0004003d, 0x00000006, - 0x00000198, 0x00000197, 0x00060041, 0x00000131, 0x0000019a, 0x0000012f, 0x00000025, 0x00000199, - 0x0004003d, 0x00000006, 0x0000019b, 0x0000019a, 0x00060050, 0x00000007, 0x0000019c, 0x00000195, - 0x00000198, 0x0000019b, 0x0003003e, 0x00000192, 0x0000019c, 0x00060041, 0x00000131, 0x0000019e, - 0x0000012f, 0x00000025, 0x00000086, 0x0004003d, 0x00000006, 0x0000019f, 0x0000019e, 0x00060041, - 0x00000131, 0x000001a1, 0x0000012f, 0x00000025, 0x000001a0, 0x0004003d, 0x00000006, 0x000001a2, - 0x000001a1, 0x00060041, 0x00000131, 0x000001a4, 0x0000012f, 0x00000025, 0x000001a3, 0x0004003d, - 0x00000006, 0x000001a5, 0x000001a4, 0x00060050, 0x00000007, 0x000001a6, 0x0000019f, 0x000001a2, - 0x000001a5, 0x0003003e, 0x0000019d, 0x000001a6, 0x0004003d, 0x00000007, 0x000001a8, 0x0000019d, - 0x0004003d, 0x00000007, 0x000001a9, 0x00000192, 0x0004003d, 0x00000006, 0x000001aa, 0x0000017e, - 0x00060050, 0x00000007, 0x000001ab, 0x000001aa, 0x000001aa, 0x000001aa, 0x0008000c, 0x00000007, - 0x000001ac, 0x00000001, 0x0000002e, 0x000001a8, 0x000001a9, 0x000001ab, 0x0003003e, 0x000001a7, - 0x000001ac, 0x0004003d, 0x00000007, 0x000001af, 0x000001a7, 0x0004003d, 0x0000001b, 0x000001b0, - 0x00000160, 0x0008004f, 0x00000007, 0x000001b1, 0x000001b0, 0x000001b0, 0x00000000, 0x00000001, - 0x00000002, 0x00050085, 0x00000007, 0x000001b2, 0x000001af, 0x000001b1, 0x00050041, 0x0000000b, - 0x000001b3, 0x00000160, 0x00000020, 0x0004003d, 0x00000006, 0x000001b4, 0x000001b3, 0x00050051, - 0x00000006, 0x000001b5, 0x000001b2, 0x00000000, 0x00050051, 0x00000006, 0x000001b6, 0x000001b2, - 0x00000001, 0x00050051, 0x00000006, 0x000001b7, 0x000001b2, 0x00000002, 0x00070050, 0x0000001b, - 0x000001b8, 0x000001b5, 0x000001b6, 0x000001b7, 0x000001b4, 0x0003003e, 0x000001ae, 0x000001b8, - 0x000100fd, 0x00010038, 0x00050036, 0x00000006, 0x00000010, 0x00000000, 0x0000000c, 0x00030037, - 0x00000008, 0x0000000d, 0x00030037, 0x0000000a, 0x0000000e, 0x00030037, 0x0000000b, 0x0000000f, - 0x000200f8, 0x00000011, 0x0004003b, 0x0000001c, 0x0000001d, 0x00000007, 0x0004003b, 0x00000008, - 0x00000031, 0x00000007, 0x0004003b, 0x0000003b, 0x0000003c, 0x00000007, 0x0004003b, 0x0000000b, - 0x00000043, 0x00000007, 0x0004003b, 0x0000003b, 0x0000006c, 0x00000007, 0x0004003b, 0x0000000b, - 0x0000007a, 0x00000007, 0x0004003b, 0x0000000b, 0x0000007b, 0x00000007, 0x0004003b, 0x0000000a, - 0x0000007f, 0x00000007, 0x0004003b, 0x0000003b, 0x00000088, 0x00000007, 0x0004003b, 0x0000009a, - 0x0000009b, 0x00000007, 0x0004003d, 0x00000009, 0x00000026, 0x0000000e, 0x00060041, 0x00000027, - 0x00000028, 0x00000024, 0x00000025, 0x00000026, 0x0004003d, 0x0000001e, 0x00000029, 0x00000028, - 0x0004003d, 0x00000007, 0x0000002a, 0x0000000d, 0x00050051, 0x00000006, 0x0000002c, 0x0000002a, - 0x00000000, 0x00050051, 0x00000006, 0x0000002d, 0x0000002a, 0x00000001, 0x00050051, 0x00000006, - 0x0000002e, 0x0000002a, 0x00000002, 0x00070050, 0x0000001b, 0x0000002f, 0x0000002c, 0x0000002d, - 0x0000002e, 0x0000002b, 0x00050091, 0x0000001b, 0x00000030, 0x00000029, 0x0000002f, 0x0003003e, - 0x0000001d, 0x00000030, 0x0004003d, 0x0000001b, 0x00000032, 0x0000001d, 0x0008004f, 0x00000007, - 0x00000033, 0x00000032, 0x00000032, 0x00000000, 0x00000001, 0x00000002, 0x00050041, 0x0000000b, - 0x00000034, 0x0000001d, 0x00000020, 0x0004003d, 0x00000006, 0x00000035, 0x00000034, 0x0007000c, - 0x00000006, 0x00000037, 0x00000001, 0x00000028, 0x00000035, 0x00000036, 0x00060050, 0x00000007, - 0x00000038, 0x00000037, 0x00000037, 0x00000037, 0x00050088, 0x00000007, 0x00000039, 0x00000033, - 0x00000038, 0x0003003e, 0x00000031, 0x00000039, 0x0004003d, 0x00000007, 0x0000003d, 0x00000031, - 0x0007004f, 0x0000003a, 0x0000003e, 0x0000003d, 0x0000003d, 0x00000000, 0x00000001, 0x0005008e, - 0x0000003a, 0x00000040, 0x0000003e, 0x0000003f, 0x00050050, 0x0000003a, 0x00000041, 0x0000003f, - 0x0000003f, 0x00050081, 0x0000003a, 0x00000042, 0x00000040, 0x00000041, 0x0003003e, 0x0000003c, - 0x00000042, 0x00050041, 0x0000000b, 0x00000045, 0x00000031, 0x00000044, 0x0004003d, 0x00000006, - 0x00000046, 0x00000045, 0x0003003e, 0x00000043, 0x00000046, 0x00050041, 0x0000000b, 0x00000049, - 0x0000003c, 0x00000048, 0x0004003d, 0x00000006, 0x0000004a, 0x00000049, 0x000500b8, 0x00000047, - 0x0000004c, 0x0000004a, 0x0000004b, 0x000400a8, 0x00000047, 0x0000004d, 0x0000004c, 0x000300f7, - 0x0000004f, 0x00000000, 0x000400fa, 0x0000004d, 0x0000004e, 0x0000004f, 0x000200f8, 0x0000004e, - 0x00050041, 0x0000000b, 0x00000050, 0x0000003c, 0x00000048, 0x0004003d, 0x00000006, 0x00000051, - 0x00000050, 0x000500ba, 0x00000047, 0x00000052, 0x00000051, 0x0000002b, 0x000200f9, 0x0000004f, - 0x000200f8, 0x0000004f, 0x000700f5, 0x00000047, 0x00000053, 0x0000004c, 0x00000011, 0x00000052, - 0x0000004e, 0x000400a8, 0x00000047, 0x00000054, 0x00000053, 0x000300f7, 0x00000056, 0x00000000, - 0x000400fa, 0x00000054, 0x00000055, 0x00000056, 0x000200f8, 0x00000055, 0x00050041, 0x0000000b, - 0x00000058, 0x0000003c, 0x00000057, 0x0004003d, 0x00000006, 0x00000059, 0x00000058, 0x000500b8, - 0x00000047, 0x0000005a, 0x00000059, 0x0000004b, 0x000200f9, 0x00000056, 0x000200f8, 0x00000056, - 0x000700f5, 0x00000047, 0x0000005b, 0x00000053, 0x0000004f, 0x0000005a, 0x00000055, 0x000400a8, - 0x00000047, 0x0000005c, 0x0000005b, 0x000300f7, 0x0000005e, 0x00000000, 0x000400fa, 0x0000005c, - 0x0000005d, 0x0000005e, 0x000200f8, 0x0000005d, 0x00050041, 0x0000000b, 0x0000005f, 0x0000003c, - 0x00000057, 0x0004003d, 0x00000006, 0x00000060, 0x0000005f, 0x000500ba, 0x00000047, 0x00000061, - 0x00000060, 0x0000002b, 0x000200f9, 0x0000005e, 0x000200f8, 0x0000005e, 0x000700f5, 0x00000047, - 0x00000062, 0x0000005b, 0x00000056, 0x00000061, 0x0000005d, 0x0004003d, 0x00000006, 0x00000063, - 0x00000043, 0x000500b8, 0x00000047, 0x00000064, 0x00000063, 0x0000004b, 0x000500a6, 0x00000047, - 0x00000065, 0x00000062, 0x00000064, 0x0004003d, 0x00000006, 0x00000066, 0x00000043, 0x000500ba, - 0x00000047, 0x00000067, 0x00000066, 0x0000002b, 0x000500a6, 0x00000047, 0x00000068, 0x00000065, - 0x00000067, 0x000300f7, 0x0000006a, 0x00000000, 0x000400fa, 0x00000068, 0x00000069, 0x0000006a, - 0x000200f8, 0x00000069, 0x000200fe, 0x0000002b, 0x000200f8, 0x0000006a, 0x0004003d, 0x0000006e, - 0x00000071, 0x00000070, 0x00040064, 0x0000006d, 0x00000072, 0x00000071, 0x00050067, 0x00000073, - 0x00000074, 0x00000072, 0x00000025, 0x0007004f, 0x00000075, 0x00000076, 0x00000074, 0x00000074, - 0x00000000, 0x00000001, 0x0004006f, 0x0000003a, 0x00000077, 0x00000076, 0x00050050, 0x0000003a, - 0x00000078, 0x0000002b, 0x0000002b, 0x00050088, 0x0000003a, 0x00000079, 0x00000078, 0x00000077, - 0x0003003e, 0x0000006c, 0x00000079, 0x0003003e, 0x0000007a, 0x0000004b, 0x0004003d, 0x00000006, - 0x0000007c, 0x00000043, 0x0004003d, 0x00000006, 0x0000007d, 0x0000000f, 0x00050083, 0x00000006, - 0x0000007e, 0x0000007c, 0x0000007d, 0x0003003e, 0x0000007b, 0x0000007e, 0x0003003e, 0x0000007f, - 0x00000025, 0x000200f9, 0x00000080, 0x000200f8, 0x00000080, 0x000400f6, 0x00000082, 0x00000083, - 0x00000000, 0x000200f9, 0x00000084, 0x000200f8, 0x00000084, 0x0004003d, 0x00000009, 0x00000085, - 0x0000007f, 0x000500b1, 0x00000047, 0x00000087, 0x00000085, 0x00000086, 0x000400fa, 0x00000087, - 0x00000081, 0x00000082, 0x000200f8, 0x00000081, 0x0004003d, 0x0000003a, 0x00000089, 0x0000003c, - 0x0004003d, 0x00000009, 0x00000099, 0x0000007f, 0x0003003e, 0x0000009b, 0x00000098, 0x00050041, - 0x0000003b, 0x0000009c, 0x0000009b, 0x00000099, 0x0004003d, 0x0000003a, 0x0000009d, 0x0000009c, - 0x0004003d, 0x0000003a, 0x0000009e, 0x0000006c, 0x00050085, 0x0000003a, 0x0000009f, 0x0000009d, - 0x0000009e, 0x00050081, 0x0000003a, 0x000000a0, 0x00000089, 0x0000009f, 0x0003003e, 0x00000088, - 0x000000a0, 0x00050041, 0x0000000b, 0x000000a1, 0x00000088, 0x00000048, 0x0004003d, 0x00000006, - 0x000000a2, 0x000000a1, 0x000500b8, 0x00000047, 0x000000a3, 0x000000a2, 0x0000004b, 0x000400a8, - 0x00000047, 0x000000a4, 0x000000a3, 0x000300f7, 0x000000a6, 0x00000000, 0x000400fa, 0x000000a4, - 0x000000a5, 0x000000a6, 0x000200f8, 0x000000a5, 0x00050041, 0x0000000b, 0x000000a7, 0x00000088, - 0x00000048, 0x0004003d, 0x00000006, 0x000000a8, 0x000000a7, 0x000500ba, 0x00000047, 0x000000a9, - 0x000000a8, 0x0000002b, 0x000200f9, 0x000000a6, 0x000200f8, 0x000000a6, 0x000700f5, 0x00000047, - 0x000000aa, 0x000000a3, 0x00000081, 0x000000a9, 0x000000a5, 0x000400a8, 0x00000047, 0x000000ab, - 0x000000aa, 0x000300f7, 0x000000ad, 0x00000000, 0x000400fa, 0x000000ab, 0x000000ac, 0x000000ad, - 0x000200f8, 0x000000ac, 0x00050041, 0x0000000b, 0x000000ae, 0x00000088, 0x00000057, 0x0004003d, - 0x00000006, 0x000000af, 0x000000ae, 0x000500b8, 0x00000047, 0x000000b0, 0x000000af, 0x0000004b, - 0x000200f9, 0x000000ad, 0x000200f8, 0x000000ad, 0x000700f5, 0x00000047, 0x000000b1, 0x000000aa, - 0x000000a6, 0x000000b0, 0x000000ac, 0x000400a8, 0x00000047, 0x000000b2, 0x000000b1, 0x000300f7, - 0x000000b4, 0x00000000, 0x000400fa, 0x000000b2, 0x000000b3, 0x000000b4, 0x000200f8, 0x000000b3, - 0x00050041, 0x0000000b, 0x000000b5, 0x00000088, 0x00000057, 0x0004003d, 0x00000006, 0x000000b6, - 0x000000b5, 0x000500ba, 0x00000047, 0x000000b7, 0x000000b6, 0x0000002b, 0x000200f9, 0x000000b4, - 0x000200f8, 0x000000b4, 0x000700f5, 0x00000047, 0x000000b8, 0x000000b1, 0x000000ad, 0x000000b7, - 0x000000b3, 0x000300f7, 0x000000ba, 0x00000000, 0x000400fa, 0x000000b8, 0x000000b9, 0x000000ba, - 0x000200f8, 0x000000b9, 0x000200f9, 0x00000083, 0x000200f8, 0x000000ba, 0x0004003d, 0x0000006e, - 0x000000bc, 0x00000070, 0x0004003d, 0x0000003a, 0x000000bd, 0x00000088, 0x0004003d, 0x00000009, - 0x000000be, 0x0000000e, 0x0004006f, 0x00000006, 0x000000bf, 0x000000be, 0x0004003d, 0x00000006, - 0x000000c0, 0x0000007b, 0x00050051, 0x00000006, 0x000000c1, 0x000000bd, 0x00000000, 0x00050051, - 0x00000006, 0x000000c2, 0x000000bd, 0x00000001, 0x00070050, 0x0000001b, 0x000000c3, 0x000000c1, - 0x000000c2, 0x000000bf, 0x000000c0, 0x00050051, 0x00000006, 0x000000c4, 0x000000c3, 0x00000003, - 0x00060059, 0x00000006, 0x000000c5, 0x000000bc, 0x000000c3, 0x000000c4, 0x0004003d, 0x00000006, - 0x000000c6, 0x0000007a, 0x00050081, 0x00000006, 0x000000c7, 0x000000c6, 0x000000c5, 0x0003003e, - 0x0000007a, 0x000000c7, 0x000200f9, 0x00000083, 0x000200f8, 0x00000083, 0x0004003d, 0x00000009, - 0x000000c8, 0x0000007f, 0x00050080, 0x00000009, 0x000000ca, 0x000000c8, 0x000000c9, 0x0003003e, - 0x0000007f, 0x000000ca, 0x000200f9, 0x00000080, 0x000200f8, 0x00000082, 0x0004003d, 0x00000006, - 0x000000cb, 0x0000007a, 0x00050088, 0x00000006, 0x000000cd, 0x000000cb, 0x000000cc, 0x000200fe, - 0x000000cd, 0x00010038, 0x00050036, 0x00000006, 0x00000014, 0x00000000, 0x00000012, 0x00030037, - 0x0000000a, 0x00000013, 0x000200f8, 0x00000015, 0x0004003b, 0x0000000b, 0x000000d0, 0x00000007, - 0x0004003b, 0x0000000b, 0x000000d3, 0x00000007, 0x0004003b, 0x0000000b, 0x000000dd, 0x00000007, - 0x0004003b, 0x0000000b, 0x000000ea, 0x00000007, 0x0004003d, 0x00000009, 0x000000d1, 0x00000013, - 0x000500aa, 0x00000047, 0x000000d2, 0x000000d1, 0x00000025, 0x000300f7, 0x000000d5, 0x00000000, - 0x000400fa, 0x000000d2, 0x000000d4, 0x000000da, 0x000200f8, 0x000000d4, 0x00060041, 0x000000d7, - 0x000000d8, 0x00000024, 0x000000d6, 0x00000048, 0x0004003d, 0x00000006, 0x000000d9, 0x000000d8, - 0x0003003e, 0x000000d3, 0x000000d9, 0x000200f9, 0x000000d5, 0x000200f8, 0x000000da, 0x0004003d, - 0x00000009, 0x000000db, 0x00000013, 0x000500aa, 0x00000047, 0x000000dc, 0x000000db, 0x000000c9, - 0x000300f7, 0x000000df, 0x00000000, 0x000400fa, 0x000000dc, 0x000000de, 0x000000e2, 0x000200f8, - 0x000000de, 0x00060041, 0x000000d7, 0x000000e0, 0x00000024, 0x000000d6, 0x00000057, 0x0004003d, - 0x00000006, 0x000000e1, 0x000000e0, 0x0003003e, 0x000000dd, 0x000000e1, 0x000200f9, 0x000000df, - 0x000200f8, 0x000000e2, 0x00060041, 0x000000d7, 0x000000e3, 0x00000024, 0x000000d6, 0x00000044, - 0x0004003d, 0x00000006, 0x000000e4, 0x000000e3, 0x0003003e, 0x000000dd, 0x000000e4, 0x000200f9, - 0x000000df, 0x000200f8, 0x000000df, 0x0004003d, 0x00000006, 0x000000e5, 0x000000dd, 0x0003003e, - 0x000000d3, 0x000000e5, 0x000200f9, 0x000000d5, 0x000200f8, 0x000000d5, 0x0004003d, 0x00000006, - 0x000000e6, 0x000000d3, 0x0003003e, 0x000000d0, 0x000000e6, 0x0004003d, 0x00000006, 0x000000e7, - 0x000000d0, 0x000500ba, 0x00000047, 0x000000e9, 0x000000e7, 0x000000e8, 0x000300f7, 0x000000ec, - 0x00000000, 0x000400fa, 0x000000e9, 0x000000eb, 0x000000ee, 0x000200f8, 0x000000eb, 0x0004003d, - 0x00000006, 0x000000ed, 0x000000d0, 0x0003003e, 0x000000ea, 0x000000ed, 0x000200f9, 0x000000ec, - 0x000200f8, 0x000000ee, 0x00060041, 0x000000d7, 0x000000f0, 0x00000024, 0x000000ef, 0x00000048, - 0x0004003d, 0x00000006, 0x000000f1, 0x000000f0, 0x0003003e, 0x000000ea, 0x000000f1, 0x000200f9, - 0x000000ec, 0x000200f8, 0x000000ec, 0x0004003d, 0x00000006, 0x000000f2, 0x000000ea, 0x000200fe, - 0x000000f2, 0x00010038, 0x00050036, 0x00000006, 0x00000019, 0x00000000, 0x00000016, 0x00030037, - 0x00000008, 0x00000017, 0x00030037, 0x0000000b, 0x00000018, 0x000200f8, 0x0000001a, 0x0004003b, - 0x0000000a, 0x0000010a, 0x00000007, 0x0004003b, 0x0000000b, 0x00000118, 0x00000007, 0x0004003b, - 0x0000000a, 0x00000119, 0x00000007, 0x0004003b, 0x00000008, 0x0000011c, 0x00000007, 0x0004003b, - 0x0000000a, 0x0000011e, 0x00000007, 0x0004003b, 0x0000000b, 0x00000120, 0x00000007, 0x00060041, - 0x000000d7, 0x000000f5, 0x00000024, 0x000000ef, 0x00000057, 0x0004003d, 0x00000006, 0x000000f6, - 0x000000f5, 0x000500b8, 0x00000047, 0x000000f7, 0x000000f6, 0x0000003f, 0x000400a8, 0x00000047, - 0x000000f8, 0x000000f7, 0x000300f7, 0x000000fa, 0x00000000, 0x000400fa, 0x000000f8, 0x000000f9, - 0x000000fa, 0x000200f8, 0x000000f9, 0x00060041, 0x000000d7, 0x000000fb, 0x00000024, 0x000000ef, - 0x00000044, 0x0004003d, 0x00000006, 0x000000fc, 0x000000fb, 0x000500b8, 0x00000047, 0x000000fd, - 0x000000fc, 0x0000003f, 0x000200f9, 0x000000fa, 0x000200f8, 0x000000fa, 0x000700f5, 0x00000047, - 0x000000fe, 0x000000f7, 0x0000001a, 0x000000fd, 0x000000f9, 0x000400a8, 0x00000047, 0x000000ff, - 0x000000fe, 0x000300f7, 0x00000101, 0x00000000, 0x000400fa, 0x000000ff, 0x00000100, 0x00000101, - 0x000200f8, 0x00000100, 0x00060041, 0x000000d7, 0x00000102, 0x00000024, 0x000000c9, 0x00000020, - 0x0004003d, 0x00000006, 0x00000103, 0x00000102, 0x000500b8, 0x00000047, 0x00000105, 0x00000103, - 0x00000104, 0x000200f9, 0x00000101, 0x000200f8, 0x00000101, 0x000700f5, 0x00000047, 0x00000106, - 0x000000fe, 0x000000fa, 0x00000105, 0x00000100, 0x000300f7, 0x00000108, 0x00000000, 0x000400fa, - 0x00000106, 0x00000107, 0x00000108, 0x000200f8, 0x00000107, 0x000200fe, 0x0000002b, 0x000200f8, - 0x00000108, 0x0003003e, 0x0000010a, 0x000000ef, 0x0004003d, 0x00000006, 0x0000010b, 0x00000018, - 0x00060041, 0x000000d7, 0x0000010c, 0x00000024, 0x000000c9, 0x00000048, 0x0004003d, 0x00000006, - 0x0000010d, 0x0000010c, 0x000500b8, 0x00000047, 0x0000010e, 0x0000010b, 0x0000010d, 0x000300f7, - 0x00000110, 0x00000000, 0x000400fa, 0x0000010e, 0x0000010f, 0x00000111, 0x000200f8, 0x0000010f, - 0x0003003e, 0x0000010a, 0x00000025, 0x000200f9, 0x00000110, 0x000200f8, 0x00000111, 0x0004003d, - 0x00000006, 0x00000112, 0x00000018, 0x00060041, 0x000000d7, 0x00000113, 0x00000024, 0x000000c9, - 0x00000057, 0x0004003d, 0x00000006, 0x00000114, 0x00000113, 0x000500b8, 0x00000047, 0x00000115, - 0x00000112, 0x00000114, 0x000300f7, 0x00000117, 0x00000000, 0x000400fa, 0x00000115, 0x00000116, - 0x00000117, 0x000200f8, 0x00000116, 0x0003003e, 0x0000010a, 0x000000c9, 0x000200f9, 0x00000117, - 0x000200f8, 0x00000117, 0x000200f9, 0x00000110, 0x000200f8, 0x00000110, 0x0004003d, 0x00000009, - 0x0000011a, 0x0000010a, 0x0003003e, 0x00000119, 0x0000011a, 0x00050039, 0x00000006, 0x0000011b, - 0x00000014, 0x00000119, 0x0004003d, 0x00000007, 0x0000011d, 0x00000017, 0x0003003e, 0x0000011c, - 0x0000011d, 0x0004003d, 0x00000009, 0x0000011f, 0x0000010a, 0x0003003e, 0x0000011e, 0x0000011f, - 0x0003003e, 0x00000120, 0x0000011b, 0x00070039, 0x00000006, 0x00000121, 0x00000010, 0x0000011c, - 0x0000011e, 0x00000120, 0x0003003e, 0x00000118, 0x00000121, 0x0004003d, 0x00000006, 0x00000122, - 0x00000118, 0x00060041, 0x000000d7, 0x00000123, 0x00000024, 0x000000c9, 0x00000020, 0x0004003d, - 0x00000006, 0x00000124, 0x00000123, 0x0008000c, 0x00000006, 0x00000125, 0x00000001, 0x0000002b, - 0x00000124, 0x0000004b, 0x0000002b, 0x0008000c, 0x00000006, 0x00000126, 0x00000001, 0x0000002e, - 0x0000002b, 0x00000122, 0x00000125, 0x0003003e, 0x00000118, 0x00000126, 0x0004003d, 0x00000006, - 0x00000127, 0x00000118, 0x000200fe, 0x00000127, 0x00010038, + 0x00000074, 0x00040005, 0x0000014c, 0x6d617266, 0x00000065, 0x00040005, 0x00000152, 0x736c6f63, + 0x00000000, 0x00040005, 0x00000158, 0x73776f72, 0x00000000, 0x00050005, 0x0000015e, 0x73617267, + 0x72615673, 0x00000073, 0x00050005, 0x00000164, 0x6661656c, 0x73726156, 0x00000000, 0x00060005, + 0x0000016a, 0x6661656c, 0x4f776f52, 0x65736666, 0x00000074, 0x00040005, 0x00000171, 0x6661656c, + 0x00000000, 0x00050005, 0x00000172, 0x776c4176, 0x44737961, 0x006b7261, 0x00040005, 0x0000017d, + 0x7261566e, 0x00000000, 0x00040005, 0x00000182, 0x69726176, 0x00746e61, 0x00040005, 0x0000018c, + 0x616c7461, 0x00565573, 0x00030005, 0x0000018e, 0x00565576, 0x00050005, 0x0000019b, 0x61636f6c, + 0x6c6f436c, 0x00000000, 0x00050005, 0x0000019e, 0x61636f6c, 0x776f526c, 0x00000000, 0x00030005, + 0x000001a1, 0x006c6f63, 0x00030005, 0x000001a6, 0x00776f72, 0x00040005, 0x000001be, 0x6b73616d, + 0x00000000, 0x00060005, 0x000001c2, 0x65626c61, 0x61536f64, 0x656c706d, 0x00000072, 0x00040005, + 0x000001c7, 0x6e695476, 0x00000074, 0x00050005, 0x000001d5, 0x77656976, 0x74706544, 0x00000068, + 0x00050005, 0x000001d7, 0x65695676, 0x736f5077, 0x00000000, 0x00030005, 0x000001dc, 0x00736976, + 0x00050005, 0x000001dd, 0x6f6f5276, 0x736f5074, 0x00000000, 0x00040005, 0x000001de, 0x61726170, + 0x0000006d, 0x00040005, 0x000001e0, 0x61726170, 0x0000006d, 0x00050005, 0x000001ee, 0x6867696c, + 0x65724774, 0x00006e65, 0x00050005, 0x000001f9, 0x6b726164, 0x65657247, 0x0000006e, 0x00050005, + 0x00000203, 0x73617267, 0x6c6f4373, 0x00000000, 0x00050005, 0x0000020a, 0x4374756f, 0x726f6c6f, + 0x00000000, 0x00050005, 0x00000215, 0x726f5776, 0x6f50646c, 0x00000073, 0x00040005, 0x00000216, + 0x6867694c, 0x00443374, 0x00060006, 0x00000216, 0x00000000, 0x52736f70, 0x75696461, 0x00000073, + 0x00050006, 0x00000216, 0x00000001, 0x6f6c6f63, 0x00000072, 0x00040005, 0x00000219, 0x6d617246, + 0x00000065, 0x00040006, 0x00000219, 0x00000000, 0x0070766d, 0x00050006, 0x00000219, 0x00000001, + 0x65646f6d, 0x0000006c, 0x00080006, 0x00000219, 0x00000002, 0x6867696c, 0x72694474, 0x65746e49, + 0x7469736e, 0x00000079, 0x00060006, 0x00000219, 0x00000003, 0x6867696c, 0x6c6f4374, 0x0000726f, + 0x00050006, 0x00000219, 0x00000004, 0x746e6974, 0x00000000, 0x00060006, 0x00000219, 0x00000005, + 0x656d6163, 0x6f506172, 0x00000073, 0x00050006, 0x00000219, 0x00000006, 0x69626d61, 0x00746e65, + 0x00050006, 0x00000219, 0x00000007, 0x6867696c, 0x00007374, 0x00050006, 0x00000219, 0x00000008, + 0x42786574, 0x00626d6f, 0x00060006, 0x00000219, 0x00000009, 0x61726170, 0x78616c6c, 0x00000000, + 0x00050006, 0x00000219, 0x0000000a, 0x77656976, 0x00000000, 0x00060006, 0x00000219, 0x0000000b, + 0x70696c63, 0x6f666e49, 0x00000000, 0x00030005, 0x0000021b, 0x006f6275, 0x00040047, 0x00000021, + 0x00000006, 0x00000040, 0x00030047, 0x00000022, 0x00000002, 0x00040048, 0x00000022, 0x00000000, + 0x00000005, 0x00050048, 0x00000022, 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x00000022, + 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x00000022, 0x00000001, 0x00000023, 0x000000c0, + 0x00050048, 0x00000022, 0x00000002, 0x00000023, 0x000000d0, 0x00050048, 0x00000022, 0x00000003, + 0x00000023, 0x000000e0, 0x00050048, 0x00000022, 0x00000004, 0x00000023, 0x000000f0, 0x00040047, + 0x00000024, 0x00000021, 0x00000004, 0x00040047, 0x00000024, 0x00000022, 0x00000000, 0x00040047, + 0x00000070, 0x00000021, 0x00000005, 0x00040047, 0x00000070, 0x00000022, 0x00000000, 0x00040047, + 0x0000012c, 0x00000006, 0x00000004, 0x00030047, 0x0000012d, 0x00000002, 0x00050048, 0x0000012d, + 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x0000013b, 0x0000001e, 0x00000004, 0x00040047, + 0x00000172, 0x0000001e, 0x00000006, 0x00040047, 0x0000018e, 0x0000001e, 0x00000000, 0x00040047, + 0x000001c2, 0x00000021, 0x00000001, 0x00040047, 0x000001c2, 0x00000022, 0x00000000, 0x00040047, + 0x000001c7, 0x0000001e, 0x00000005, 0x00040047, 0x000001d7, 0x0000001e, 0x00000002, 0x00040047, + 0x000001dd, 0x0000001e, 0x00000003, 0x00040047, 0x0000020a, 0x0000001e, 0x00000000, 0x00040047, + 0x00000215, 0x0000001e, 0x00000001, 0x00050048, 0x00000216, 0x00000000, 0x00000023, 0x00000000, + 0x00050048, 0x00000216, 0x00000001, 0x00000023, 0x00000010, 0x00040047, 0x00000218, 0x00000006, + 0x00000020, 0x00030047, 0x00000219, 0x00000002, 0x00040048, 0x00000219, 0x00000000, 0x00000005, + 0x00050048, 0x00000219, 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x00000219, 0x00000000, + 0x00000023, 0x00000000, 0x00040048, 0x00000219, 0x00000001, 0x00000005, 0x00050048, 0x00000219, + 0x00000001, 0x00000007, 0x00000010, 0x00050048, 0x00000219, 0x00000001, 0x00000023, 0x00000040, + 0x00050048, 0x00000219, 0x00000002, 0x00000023, 0x00000080, 0x00050048, 0x00000219, 0x00000003, + 0x00000023, 0x00000090, 0x00050048, 0x00000219, 0x00000004, 0x00000023, 0x000000a0, 0x00050048, + 0x00000219, 0x00000005, 0x00000023, 0x000000b0, 0x00050048, 0x00000219, 0x00000006, 0x00000023, + 0x000000c0, 0x00050048, 0x00000219, 0x00000007, 0x00000023, 0x000000d0, 0x00050048, 0x00000219, + 0x00000008, 0x00000023, 0x000001d0, 0x00050048, 0x00000219, 0x00000009, 0x00000023, 0x000001e0, + 0x00040048, 0x00000219, 0x0000000a, 0x00000005, 0x00050048, 0x00000219, 0x0000000a, 0x00000007, + 0x00000010, 0x00050048, 0x00000219, 0x0000000a, 0x00000023, 0x000001f0, 0x00050048, 0x00000219, + 0x0000000b, 0x00000023, 0x00000230, 0x00040047, 0x0000021b, 0x00000021, 0x00000000, 0x00040047, + 0x0000021b, 0x00000022, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, + 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000003, 0x00040020, + 0x00000008, 0x00000007, 0x00000007, 0x00040015, 0x00000009, 0x00000020, 0x00000001, 0x00040020, + 0x0000000a, 0x00000007, 0x00000009, 0x00040020, 0x0000000b, 0x00000007, 0x00000006, 0x00060021, + 0x0000000c, 0x00000006, 0x00000008, 0x0000000a, 0x0000000b, 0x00040021, 0x00000012, 0x00000006, + 0x0000000a, 0x00050021, 0x00000016, 0x00000006, 0x00000008, 0x0000000b, 0x00040017, 0x0000001b, + 0x00000006, 0x00000004, 0x00040020, 0x0000001c, 0x00000007, 0x0000001b, 0x00040018, 0x0000001e, + 0x0000001b, 0x00000004, 0x00040015, 0x0000001f, 0x00000020, 0x00000000, 0x0004002b, 0x0000001f, + 0x00000020, 0x00000003, 0x0004001c, 0x00000021, 0x0000001e, 0x00000020, 0x0007001e, 0x00000022, + 0x00000021, 0x0000001b, 0x0000001b, 0x0000001b, 0x0000001b, 0x00040020, 0x00000023, 0x00000002, + 0x00000022, 0x0004003b, 0x00000023, 0x00000024, 0x00000002, 0x0004002b, 0x00000009, 0x00000025, + 0x00000000, 0x00040020, 0x00000027, 0x00000002, 0x0000001e, 0x0004002b, 0x00000006, 0x0000002b, + 0x3f800000, 0x0004002b, 0x00000006, 0x00000036, 0x358637bd, 0x00040017, 0x0000003a, 0x00000006, + 0x00000002, 0x00040020, 0x0000003b, 0x00000007, 0x0000003a, 0x0004002b, 0x00000006, 0x0000003f, + 0x3f000000, 0x0004002b, 0x0000001f, 0x00000044, 0x00000002, 0x00020014, 0x00000047, 0x0004002b, + 0x0000001f, 0x00000048, 0x00000000, 0x0004002b, 0x00000006, 0x0000004b, 0x00000000, 0x0004002b, + 0x0000001f, 0x00000057, 0x00000001, 0x00090019, 0x0000006d, 0x00000006, 0x00000001, 0x00000001, + 0x00000001, 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x0000006e, 0x0000006d, 0x00040020, + 0x0000006f, 0x00000000, 0x0000006e, 0x0004003b, 0x0000006f, 0x00000070, 0x00000000, 0x00040017, + 0x00000073, 0x00000009, 0x00000003, 0x00040017, 0x00000075, 0x00000009, 0x00000002, 0x0004002b, + 0x00000009, 0x00000086, 0x00000009, 0x0004002b, 0x0000001f, 0x0000008a, 0x00000009, 0x0004001c, + 0x0000008b, 0x0000003a, 0x0000008a, 0x0004002b, 0x00000006, 0x0000008c, 0xbf000000, 0x0005002c, + 0x0000003a, 0x0000008d, 0x0000008c, 0x0000008c, 0x0004002b, 0x00000006, 0x0000008e, 0xbf19999a, + 0x0005002c, 0x0000003a, 0x0000008f, 0x0000004b, 0x0000008e, 0x0005002c, 0x0000003a, 0x00000090, + 0x0000003f, 0x0000008c, 0x0005002c, 0x0000003a, 0x00000091, 0x0000008e, 0x0000004b, 0x0005002c, + 0x0000003a, 0x00000092, 0x0000004b, 0x0000004b, 0x0004002b, 0x00000006, 0x00000093, 0x3f19999a, + 0x0005002c, 0x0000003a, 0x00000094, 0x00000093, 0x0000004b, 0x0005002c, 0x0000003a, 0x00000095, + 0x0000008c, 0x0000003f, 0x0005002c, 0x0000003a, 0x00000096, 0x0000004b, 0x00000093, 0x0005002c, + 0x0000003a, 0x00000097, 0x0000003f, 0x0000003f, 0x000c002c, 0x0000008b, 0x00000098, 0x0000008d, + 0x0000008f, 0x00000090, 0x00000091, 0x00000092, 0x00000094, 0x00000095, 0x00000096, 0x00000097, + 0x00040020, 0x0000009a, 0x00000007, 0x0000008b, 0x0004002b, 0x00000009, 0x000000c9, 0x00000001, + 0x0004002b, 0x00000006, 0x000000cc, 0x41100000, 0x0004002b, 0x00000009, 0x000000d6, 0x00000003, + 0x00040020, 0x000000d7, 0x00000002, 0x00000006, 0x0004002b, 0x00000006, 0x000000e8, 0x322bcc77, + 0x0004002b, 0x00000009, 0x000000ef, 0x00000002, 0x0004002b, 0x00000006, 0x00000104, 0x38d1b717, + 0x0004002b, 0x0000001f, 0x0000012b, 0x00000020, 0x0004001c, 0x0000012c, 0x00000006, 0x0000012b, + 0x0003001e, 0x0000012d, 0x0000012c, 0x00040020, 0x0000012e, 0x00000009, 0x0000012d, 0x0004003b, + 0x0000012e, 0x0000012f, 0x00000009, 0x0004002b, 0x00000009, 0x00000130, 0x0000000c, 0x00040020, + 0x00000131, 0x00000009, 0x00000006, 0x00040020, 0x0000013a, 0x00000001, 0x00000006, 0x0004003b, + 0x0000013a, 0x0000013b, 0x00000001, 0x0004002b, 0x00000006, 0x0000013d, 0x414fd639, 0x0004002b, + 0x00000006, 0x00000140, 0x472aee8c, 0x0004002b, 0x00000009, 0x00000153, 0x0000000d, 0x0004002b, + 0x00000009, 0x00000159, 0x0000000e, 0x0004002b, 0x00000009, 0x0000015f, 0x0000000f, 0x0004002b, + 0x00000009, 0x00000165, 0x00000010, 0x0004002b, 0x00000009, 0x0000016b, 0x00000011, 0x00040020, + 0x00000170, 0x00000007, 0x00000047, 0x0004003b, 0x0000013a, 0x00000172, 0x00000001, 0x0004002b, + 0x00000009, 0x00000178, 0x00000005, 0x00040020, 0x0000018d, 0x00000001, 0x0000003a, 0x0004003b, + 0x0000018d, 0x0000018e, 0x00000001, 0x00090019, 0x000001bf, 0x00000006, 0x00000001, 0x00000000, + 0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x000001c0, 0x000001bf, 0x00040020, + 0x000001c1, 0x00000000, 0x000001c0, 0x0004003b, 0x000001c1, 0x000001c2, 0x00000000, 0x00040020, + 0x000001c6, 0x00000001, 0x0000001b, 0x0004003b, 0x000001c6, 0x000001c7, 0x00000001, 0x0004002b, + 0x00000009, 0x000001cc, 0x00000004, 0x0004002b, 0x00000006, 0x000001cf, 0x3c23d70a, 0x00040020, + 0x000001d6, 0x00000001, 0x00000007, 0x0004003b, 0x000001d6, 0x000001d7, 0x00000001, 0x0004003b, + 0x000001d6, 0x000001dd, 0x00000001, 0x0004002b, 0x00000009, 0x000001ef, 0x00000006, 0x0004002b, + 0x00000009, 0x000001f2, 0x00000007, 0x0004002b, 0x00000009, 0x000001f5, 0x00000008, 0x0004002b, + 0x00000009, 0x000001fc, 0x0000000a, 0x0004002b, 0x00000009, 0x000001ff, 0x0000000b, 0x00040020, + 0x00000209, 0x00000003, 0x0000001b, 0x0004003b, 0x00000209, 0x0000020a, 0x00000003, 0x0004003b, + 0x000001d6, 0x00000215, 0x00000001, 0x0004001e, 0x00000216, 0x0000001b, 0x0000001b, 0x0004002b, + 0x0000001f, 0x00000217, 0x00000008, 0x0004001c, 0x00000218, 0x00000216, 0x00000217, 0x000e001e, + 0x00000219, 0x0000001e, 0x0000001e, 0x0000001b, 0x0000001b, 0x0000001b, 0x0000001b, 0x0000001b, + 0x00000218, 0x0000001b, 0x0000001b, 0x0000001e, 0x0000001b, 0x00040020, 0x0000021a, 0x00000002, + 0x00000219, 0x0004003b, 0x0000021a, 0x0000021b, 0x00000002, 0x00050036, 0x00000002, 0x00000004, + 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003b, 0x0000000b, 0x0000012a, 0x00000007, + 0x0004003b, 0x0000000b, 0x00000135, 0x00000007, 0x0004003b, 0x0000000b, 0x00000139, 0x00000007, + 0x0004003b, 0x0000000b, 0x00000143, 0x00000007, 0x0004003b, 0x0000000a, 0x0000014c, 0x00000007, + 0x0004003b, 0x0000000a, 0x00000152, 0x00000007, 0x0004003b, 0x0000000a, 0x00000158, 0x00000007, + 0x0004003b, 0x0000000a, 0x0000015e, 0x00000007, 0x0004003b, 0x0000000a, 0x00000164, 0x00000007, + 0x0004003b, 0x0000000a, 0x0000016a, 0x00000007, 0x0004003b, 0x00000170, 0x00000171, 0x00000007, + 0x0004003b, 0x0000000a, 0x0000017d, 0x00000007, 0x0004003b, 0x0000000a, 0x00000182, 0x00000007, + 0x0004003b, 0x0000003b, 0x0000018c, 0x00000007, 0x0004003b, 0x0000000a, 0x0000019b, 0x00000007, + 0x0004003b, 0x0000000a, 0x0000019e, 0x00000007, 0x0004003b, 0x0000000a, 0x000001a1, 0x00000007, + 0x0004003b, 0x0000000a, 0x000001a6, 0x00000007, 0x0004003b, 0x0000001c, 0x000001be, 0x00000007, + 0x0004003b, 0x0000000b, 0x000001d5, 0x00000007, 0x0004003b, 0x0000000b, 0x000001dc, 0x00000007, + 0x0004003b, 0x00000008, 0x000001de, 0x00000007, 0x0004003b, 0x0000000b, 0x000001e0, 0x00000007, + 0x0004003b, 0x00000008, 0x000001ee, 0x00000007, 0x0004003b, 0x00000008, 0x000001f9, 0x00000007, + 0x0004003b, 0x00000008, 0x00000203, 0x00000007, 0x00060041, 0x00000131, 0x00000132, 0x0000012f, + 0x00000025, 0x00000130, 0x0004003d, 0x00000006, 0x00000133, 0x00000132, 0x0007000c, 0x00000006, + 0x00000134, 0x00000001, 0x00000028, 0x00000133, 0x0000002b, 0x0003003e, 0x0000012a, 0x00000134, + 0x00060041, 0x00000131, 0x00000136, 0x0000012f, 0x00000025, 0x000000c9, 0x0004003d, 0x00000006, + 0x00000137, 0x00000136, 0x0007000c, 0x00000006, 0x00000138, 0x00000001, 0x00000028, 0x00000137, + 0x00000104, 0x0003003e, 0x00000135, 0x00000138, 0x0004003d, 0x00000006, 0x0000013c, 0x0000013b, + 0x00050085, 0x00000006, 0x0000013e, 0x0000013c, 0x0000013d, 0x0006000c, 0x00000006, 0x0000013f, + 0x00000001, 0x0000000d, 0x0000013e, 0x00050085, 0x00000006, 0x00000141, 0x0000013f, 0x00000140, + 0x0006000c, 0x00000006, 0x00000142, 0x00000001, 0x0000000a, 0x00000141, 0x0003003e, 0x00000139, + 0x00000142, 0x00060041, 0x00000131, 0x00000144, 0x0000012f, 0x00000025, 0x00000025, 0x0004003d, + 0x00000006, 0x00000145, 0x00000144, 0x0004003d, 0x00000006, 0x00000146, 0x00000135, 0x00050088, + 0x00000006, 0x00000147, 0x00000145, 0x00000146, 0x0004003d, 0x00000006, 0x00000148, 0x00000139, + 0x0004003d, 0x00000006, 0x00000149, 0x0000012a, 0x00050085, 0x00000006, 0x0000014a, 0x00000148, + 0x00000149, 0x00050081, 0x00000006, 0x0000014b, 0x00000147, 0x0000014a, 0x0003003e, 0x00000143, + 0x0000014b, 0x0004003d, 0x00000006, 0x0000014d, 0x00000143, 0x0006000c, 0x00000006, 0x0000014e, + 0x00000001, 0x00000008, 0x0000014d, 0x0004003d, 0x00000006, 0x0000014f, 0x0000012a, 0x0005008d, + 0x00000006, 0x00000150, 0x0000014e, 0x0000014f, 0x0004006e, 0x00000009, 0x00000151, 0x00000150, + 0x0003003e, 0x0000014c, 0x00000151, 0x00060041, 0x00000131, 0x00000154, 0x0000012f, 0x00000025, + 0x00000153, 0x0004003d, 0x00000006, 0x00000155, 0x00000154, 0x0007000c, 0x00000006, 0x00000156, + 0x00000001, 0x00000028, 0x00000155, 0x0000002b, 0x0004006e, 0x00000009, 0x00000157, 0x00000156, + 0x0003003e, 0x00000152, 0x00000157, 0x00060041, 0x00000131, 0x0000015a, 0x0000012f, 0x00000025, + 0x00000159, 0x0004003d, 0x00000006, 0x0000015b, 0x0000015a, 0x0007000c, 0x00000006, 0x0000015c, + 0x00000001, 0x00000028, 0x0000015b, 0x0000002b, 0x0004006e, 0x00000009, 0x0000015d, 0x0000015c, + 0x0003003e, 0x00000158, 0x0000015d, 0x00060041, 0x00000131, 0x00000160, 0x0000012f, 0x00000025, + 0x0000015f, 0x0004003d, 0x00000006, 0x00000161, 0x00000160, 0x0007000c, 0x00000006, 0x00000162, + 0x00000001, 0x00000028, 0x00000161, 0x0000002b, 0x0004006e, 0x00000009, 0x00000163, 0x00000162, + 0x0003003e, 0x0000015e, 0x00000163, 0x00060041, 0x00000131, 0x00000166, 0x0000012f, 0x00000025, + 0x00000165, 0x0004003d, 0x00000006, 0x00000167, 0x00000166, 0x0007000c, 0x00000006, 0x00000168, + 0x00000001, 0x00000028, 0x00000167, 0x0000002b, 0x0004006e, 0x00000009, 0x00000169, 0x00000168, + 0x0003003e, 0x00000164, 0x00000169, 0x00060041, 0x00000131, 0x0000016c, 0x0000012f, 0x00000025, + 0x0000016b, 0x0004003d, 0x00000006, 0x0000016d, 0x0000016c, 0x00050081, 0x00000006, 0x0000016e, + 0x0000016d, 0x0000003f, 0x0004006e, 0x00000009, 0x0000016f, 0x0000016e, 0x0003003e, 0x0000016a, + 0x0000016f, 0x0004003d, 0x00000006, 0x00000173, 0x00000172, 0x000500ba, 0x00000047, 0x00000174, + 0x00000173, 0x0000003f, 0x000400a8, 0x00000047, 0x00000175, 0x00000174, 0x000300f7, 0x00000177, + 0x00000000, 0x000400fa, 0x00000175, 0x00000176, 0x00000177, 0x000200f8, 0x00000176, 0x00060041, + 0x00000131, 0x00000179, 0x0000012f, 0x00000025, 0x00000178, 0x0004003d, 0x00000006, 0x0000017a, + 0x00000179, 0x000500ba, 0x00000047, 0x0000017b, 0x0000017a, 0x0000003f, 0x000200f9, 0x00000177, + 0x000200f8, 0x00000177, 0x000700f5, 0x00000047, 0x0000017c, 0x00000174, 0x00000005, 0x0000017b, + 0x00000176, 0x0003003e, 0x00000171, 0x0000017c, 0x0004003d, 0x00000047, 0x0000017e, 0x00000171, + 0x0004003d, 0x00000009, 0x0000017f, 0x00000164, 0x0004003d, 0x00000009, 0x00000180, 0x0000015e, + 0x000600a9, 0x00000009, 0x00000181, 0x0000017e, 0x0000017f, 0x00000180, 0x0003003e, 0x0000017d, + 0x00000181, 0x0004003d, 0x00000006, 0x00000183, 0x0000013b, 0x0004003d, 0x00000009, 0x00000184, + 0x0000017d, 0x0004006f, 0x00000006, 0x00000185, 0x00000184, 0x0005008d, 0x00000006, 0x00000186, + 0x00000183, 0x00000185, 0x0004006e, 0x00000009, 0x00000187, 0x00000186, 0x0003003e, 0x00000182, + 0x00000187, 0x0004003d, 0x00000009, 0x00000188, 0x00000158, 0x000500b3, 0x00000047, 0x00000189, + 0x00000188, 0x000000c9, 0x000300f7, 0x0000018b, 0x00000000, 0x000400fa, 0x00000189, 0x0000018a, + 0x0000019a, 0x000200f8, 0x0000018a, 0x00050041, 0x0000013a, 0x0000018f, 0x0000018e, 0x00000048, + 0x0004003d, 0x00000006, 0x00000190, 0x0000018f, 0x0004003d, 0x00000009, 0x00000191, 0x0000014c, + 0x0004006f, 0x00000006, 0x00000192, 0x00000191, 0x00050081, 0x00000006, 0x00000193, 0x00000190, + 0x00000192, 0x0004003d, 0x00000006, 0x00000194, 0x0000012a, 0x00050088, 0x00000006, 0x00000195, + 0x00000193, 0x00000194, 0x00050041, 0x0000013a, 0x00000196, 0x0000018e, 0x00000057, 0x0004003d, + 0x00000006, 0x00000197, 0x00000196, 0x00050083, 0x00000006, 0x00000198, 0x0000002b, 0x00000197, + 0x00050050, 0x0000003a, 0x00000199, 0x00000195, 0x00000198, 0x0003003e, 0x0000018c, 0x00000199, + 0x000200f9, 0x0000018b, 0x000200f8, 0x0000019a, 0x0004003d, 0x00000009, 0x0000019c, 0x0000014c, + 0x0005008b, 0x00000009, 0x0000019d, 0x0000019c, 0x000000ef, 0x0003003e, 0x0000019b, 0x0000019d, + 0x0004003d, 0x00000009, 0x0000019f, 0x0000014c, 0x00050087, 0x00000009, 0x000001a0, 0x0000019f, + 0x000000ef, 0x0003003e, 0x0000019e, 0x000001a0, 0x0004003d, 0x00000009, 0x000001a2, 0x00000182, + 0x00050084, 0x00000009, 0x000001a3, 0x000001a2, 0x000000ef, 0x0004003d, 0x00000009, 0x000001a4, + 0x0000019b, 0x00050080, 0x00000009, 0x000001a5, 0x000001a3, 0x000001a4, 0x0003003e, 0x000001a1, + 0x000001a5, 0x0004003d, 0x00000047, 0x000001a7, 0x00000171, 0x0004003d, 0x00000009, 0x000001a8, + 0x0000016a, 0x000600a9, 0x00000009, 0x000001a9, 0x000001a7, 0x000001a8, 0x00000025, 0x0004003d, + 0x00000009, 0x000001aa, 0x0000019e, 0x00050080, 0x00000009, 0x000001ab, 0x000001a9, 0x000001aa, + 0x0003003e, 0x000001a6, 0x000001ab, 0x0004003d, 0x00000009, 0x000001ac, 0x000001a1, 0x0004006f, + 0x00000006, 0x000001ad, 0x000001ac, 0x00050041, 0x0000013a, 0x000001ae, 0x0000018e, 0x00000048, + 0x0004003d, 0x00000006, 0x000001af, 0x000001ae, 0x00050081, 0x00000006, 0x000001b0, 0x000001ad, + 0x000001af, 0x0004003d, 0x00000009, 0x000001b1, 0x00000152, 0x0004006f, 0x00000006, 0x000001b2, + 0x000001b1, 0x00050088, 0x00000006, 0x000001b3, 0x000001b0, 0x000001b2, 0x0004003d, 0x00000009, + 0x000001b4, 0x000001a6, 0x0004006f, 0x00000006, 0x000001b5, 0x000001b4, 0x00050041, 0x0000013a, + 0x000001b6, 0x0000018e, 0x00000057, 0x0004003d, 0x00000006, 0x000001b7, 0x000001b6, 0x00050083, + 0x00000006, 0x000001b8, 0x0000002b, 0x000001b7, 0x00050081, 0x00000006, 0x000001b9, 0x000001b5, + 0x000001b8, 0x0004003d, 0x00000009, 0x000001ba, 0x00000158, 0x0004006f, 0x00000006, 0x000001bb, + 0x000001ba, 0x00050088, 0x00000006, 0x000001bc, 0x000001b9, 0x000001bb, 0x00050050, 0x0000003a, + 0x000001bd, 0x000001b3, 0x000001bc, 0x0003003e, 0x0000018c, 0x000001bd, 0x000200f9, 0x0000018b, + 0x000200f8, 0x0000018b, 0x0004003d, 0x000001c0, 0x000001c3, 0x000001c2, 0x0004003d, 0x0000003a, + 0x000001c4, 0x0000018c, 0x00050057, 0x0000001b, 0x000001c5, 0x000001c3, 0x000001c4, 0x0004003d, + 0x0000001b, 0x000001c8, 0x000001c7, 0x00050085, 0x0000001b, 0x000001c9, 0x000001c5, 0x000001c8, + 0x0003003e, 0x000001be, 0x000001c9, 0x00050041, 0x0000000b, 0x000001ca, 0x000001be, 0x00000020, + 0x0004003d, 0x00000006, 0x000001cb, 0x000001ca, 0x00060041, 0x00000131, 0x000001cd, 0x0000012f, + 0x00000025, 0x000001cc, 0x0004003d, 0x00000006, 0x000001ce, 0x000001cd, 0x0007000c, 0x00000006, + 0x000001d0, 0x00000001, 0x00000028, 0x000001ce, 0x000001cf, 0x000500b8, 0x00000047, 0x000001d1, + 0x000001cb, 0x000001d0, 0x000300f7, 0x000001d3, 0x00000000, 0x000400fa, 0x000001d1, 0x000001d2, + 0x000001d3, 0x000200f8, 0x000001d2, 0x000100fc, 0x000200f8, 0x000001d3, 0x00050041, 0x0000013a, + 0x000001d8, 0x000001d7, 0x00000044, 0x0004003d, 0x00000006, 0x000001d9, 0x000001d8, 0x0004007f, + 0x00000006, 0x000001da, 0x000001d9, 0x0007000c, 0x00000006, 0x000001db, 0x00000001, 0x00000028, + 0x000001da, 0x0000004b, 0x0003003e, 0x000001d5, 0x000001db, 0x0004003d, 0x00000007, 0x000001df, + 0x000001dd, 0x0003003e, 0x000001de, 0x000001df, 0x0004003d, 0x00000006, 0x000001e1, 0x000001d5, + 0x0003003e, 0x000001e0, 0x000001e1, 0x00060039, 0x00000006, 0x000001e2, 0x00000019, 0x000001de, + 0x000001e0, 0x0003003e, 0x000001dc, 0x000001e2, 0x00060041, 0x00000131, 0x000001e3, 0x0000012f, + 0x00000025, 0x00000178, 0x0004003d, 0x00000006, 0x000001e4, 0x000001e3, 0x000500ba, 0x00000047, + 0x000001e5, 0x000001e4, 0x0000003f, 0x000400a8, 0x00000047, 0x000001e6, 0x000001e5, 0x000300f7, + 0x000001e8, 0x00000000, 0x000400fa, 0x000001e6, 0x000001e7, 0x000001e8, 0x000200f8, 0x000001e7, + 0x0004003d, 0x00000006, 0x000001e9, 0x00000172, 0x000500ba, 0x00000047, 0x000001ea, 0x000001e9, + 0x0000003f, 0x000200f9, 0x000001e8, 0x000200f8, 0x000001e8, 0x000700f5, 0x00000047, 0x000001eb, + 0x000001e5, 0x000001d3, 0x000001ea, 0x000001e7, 0x000300f7, 0x000001ed, 0x00000000, 0x000400fa, + 0x000001eb, 0x000001ec, 0x000001ed, 0x000200f8, 0x000001ec, 0x0003003e, 0x000001dc, 0x0000004b, + 0x000200f9, 0x000001ed, 0x000200f8, 0x000001ed, 0x00060041, 0x00000131, 0x000001f0, 0x0000012f, + 0x00000025, 0x000001ef, 0x0004003d, 0x00000006, 0x000001f1, 0x000001f0, 0x00060041, 0x00000131, + 0x000001f3, 0x0000012f, 0x00000025, 0x000001f2, 0x0004003d, 0x00000006, 0x000001f4, 0x000001f3, + 0x00060041, 0x00000131, 0x000001f6, 0x0000012f, 0x00000025, 0x000001f5, 0x0004003d, 0x00000006, + 0x000001f7, 0x000001f6, 0x00060050, 0x00000007, 0x000001f8, 0x000001f1, 0x000001f4, 0x000001f7, + 0x0003003e, 0x000001ee, 0x000001f8, 0x00060041, 0x00000131, 0x000001fa, 0x0000012f, 0x00000025, + 0x00000086, 0x0004003d, 0x00000006, 0x000001fb, 0x000001fa, 0x00060041, 0x00000131, 0x000001fd, + 0x0000012f, 0x00000025, 0x000001fc, 0x0004003d, 0x00000006, 0x000001fe, 0x000001fd, 0x00060041, + 0x00000131, 0x00000200, 0x0000012f, 0x00000025, 0x000001ff, 0x0004003d, 0x00000006, 0x00000201, + 0x00000200, 0x00060050, 0x00000007, 0x00000202, 0x000001fb, 0x000001fe, 0x00000201, 0x0003003e, + 0x000001f9, 0x00000202, 0x0004003d, 0x00000007, 0x00000204, 0x000001f9, 0x0004003d, 0x00000007, + 0x00000205, 0x000001ee, 0x0004003d, 0x00000006, 0x00000206, 0x000001dc, 0x00060050, 0x00000007, + 0x00000207, 0x00000206, 0x00000206, 0x00000206, 0x0008000c, 0x00000007, 0x00000208, 0x00000001, + 0x0000002e, 0x00000204, 0x00000205, 0x00000207, 0x0003003e, 0x00000203, 0x00000208, 0x0004003d, + 0x00000007, 0x0000020b, 0x00000203, 0x0004003d, 0x0000001b, 0x0000020c, 0x000001be, 0x0008004f, + 0x00000007, 0x0000020d, 0x0000020c, 0x0000020c, 0x00000000, 0x00000001, 0x00000002, 0x00050085, + 0x00000007, 0x0000020e, 0x0000020b, 0x0000020d, 0x00050041, 0x0000000b, 0x0000020f, 0x000001be, + 0x00000020, 0x0004003d, 0x00000006, 0x00000210, 0x0000020f, 0x00050051, 0x00000006, 0x00000211, + 0x0000020e, 0x00000000, 0x00050051, 0x00000006, 0x00000212, 0x0000020e, 0x00000001, 0x00050051, + 0x00000006, 0x00000213, 0x0000020e, 0x00000002, 0x00070050, 0x0000001b, 0x00000214, 0x00000211, + 0x00000212, 0x00000213, 0x00000210, 0x0003003e, 0x0000020a, 0x00000214, 0x000100fd, 0x00010038, + 0x00050036, 0x00000006, 0x00000010, 0x00000000, 0x0000000c, 0x00030037, 0x00000008, 0x0000000d, + 0x00030037, 0x0000000a, 0x0000000e, 0x00030037, 0x0000000b, 0x0000000f, 0x000200f8, 0x00000011, + 0x0004003b, 0x0000001c, 0x0000001d, 0x00000007, 0x0004003b, 0x00000008, 0x00000031, 0x00000007, + 0x0004003b, 0x0000003b, 0x0000003c, 0x00000007, 0x0004003b, 0x0000000b, 0x00000043, 0x00000007, + 0x0004003b, 0x0000003b, 0x0000006c, 0x00000007, 0x0004003b, 0x0000000b, 0x0000007a, 0x00000007, + 0x0004003b, 0x0000000b, 0x0000007b, 0x00000007, 0x0004003b, 0x0000000a, 0x0000007f, 0x00000007, + 0x0004003b, 0x0000003b, 0x00000088, 0x00000007, 0x0004003b, 0x0000009a, 0x0000009b, 0x00000007, + 0x0004003d, 0x00000009, 0x00000026, 0x0000000e, 0x00060041, 0x00000027, 0x00000028, 0x00000024, + 0x00000025, 0x00000026, 0x0004003d, 0x0000001e, 0x00000029, 0x00000028, 0x0004003d, 0x00000007, + 0x0000002a, 0x0000000d, 0x00050051, 0x00000006, 0x0000002c, 0x0000002a, 0x00000000, 0x00050051, + 0x00000006, 0x0000002d, 0x0000002a, 0x00000001, 0x00050051, 0x00000006, 0x0000002e, 0x0000002a, + 0x00000002, 0x00070050, 0x0000001b, 0x0000002f, 0x0000002c, 0x0000002d, 0x0000002e, 0x0000002b, + 0x00050091, 0x0000001b, 0x00000030, 0x00000029, 0x0000002f, 0x0003003e, 0x0000001d, 0x00000030, + 0x0004003d, 0x0000001b, 0x00000032, 0x0000001d, 0x0008004f, 0x00000007, 0x00000033, 0x00000032, + 0x00000032, 0x00000000, 0x00000001, 0x00000002, 0x00050041, 0x0000000b, 0x00000034, 0x0000001d, + 0x00000020, 0x0004003d, 0x00000006, 0x00000035, 0x00000034, 0x0007000c, 0x00000006, 0x00000037, + 0x00000001, 0x00000028, 0x00000035, 0x00000036, 0x00060050, 0x00000007, 0x00000038, 0x00000037, + 0x00000037, 0x00000037, 0x00050088, 0x00000007, 0x00000039, 0x00000033, 0x00000038, 0x0003003e, + 0x00000031, 0x00000039, 0x0004003d, 0x00000007, 0x0000003d, 0x00000031, 0x0007004f, 0x0000003a, + 0x0000003e, 0x0000003d, 0x0000003d, 0x00000000, 0x00000001, 0x0005008e, 0x0000003a, 0x00000040, + 0x0000003e, 0x0000003f, 0x00050050, 0x0000003a, 0x00000041, 0x0000003f, 0x0000003f, 0x00050081, + 0x0000003a, 0x00000042, 0x00000040, 0x00000041, 0x0003003e, 0x0000003c, 0x00000042, 0x00050041, + 0x0000000b, 0x00000045, 0x00000031, 0x00000044, 0x0004003d, 0x00000006, 0x00000046, 0x00000045, + 0x0003003e, 0x00000043, 0x00000046, 0x00050041, 0x0000000b, 0x00000049, 0x0000003c, 0x00000048, + 0x0004003d, 0x00000006, 0x0000004a, 0x00000049, 0x000500b8, 0x00000047, 0x0000004c, 0x0000004a, + 0x0000004b, 0x000400a8, 0x00000047, 0x0000004d, 0x0000004c, 0x000300f7, 0x0000004f, 0x00000000, + 0x000400fa, 0x0000004d, 0x0000004e, 0x0000004f, 0x000200f8, 0x0000004e, 0x00050041, 0x0000000b, + 0x00000050, 0x0000003c, 0x00000048, 0x0004003d, 0x00000006, 0x00000051, 0x00000050, 0x000500ba, + 0x00000047, 0x00000052, 0x00000051, 0x0000002b, 0x000200f9, 0x0000004f, 0x000200f8, 0x0000004f, + 0x000700f5, 0x00000047, 0x00000053, 0x0000004c, 0x00000011, 0x00000052, 0x0000004e, 0x000400a8, + 0x00000047, 0x00000054, 0x00000053, 0x000300f7, 0x00000056, 0x00000000, 0x000400fa, 0x00000054, + 0x00000055, 0x00000056, 0x000200f8, 0x00000055, 0x00050041, 0x0000000b, 0x00000058, 0x0000003c, + 0x00000057, 0x0004003d, 0x00000006, 0x00000059, 0x00000058, 0x000500b8, 0x00000047, 0x0000005a, + 0x00000059, 0x0000004b, 0x000200f9, 0x00000056, 0x000200f8, 0x00000056, 0x000700f5, 0x00000047, + 0x0000005b, 0x00000053, 0x0000004f, 0x0000005a, 0x00000055, 0x000400a8, 0x00000047, 0x0000005c, + 0x0000005b, 0x000300f7, 0x0000005e, 0x00000000, 0x000400fa, 0x0000005c, 0x0000005d, 0x0000005e, + 0x000200f8, 0x0000005d, 0x00050041, 0x0000000b, 0x0000005f, 0x0000003c, 0x00000057, 0x0004003d, + 0x00000006, 0x00000060, 0x0000005f, 0x000500ba, 0x00000047, 0x00000061, 0x00000060, 0x0000002b, + 0x000200f9, 0x0000005e, 0x000200f8, 0x0000005e, 0x000700f5, 0x00000047, 0x00000062, 0x0000005b, + 0x00000056, 0x00000061, 0x0000005d, 0x0004003d, 0x00000006, 0x00000063, 0x00000043, 0x000500b8, + 0x00000047, 0x00000064, 0x00000063, 0x0000004b, 0x000500a6, 0x00000047, 0x00000065, 0x00000062, + 0x00000064, 0x0004003d, 0x00000006, 0x00000066, 0x00000043, 0x000500ba, 0x00000047, 0x00000067, + 0x00000066, 0x0000002b, 0x000500a6, 0x00000047, 0x00000068, 0x00000065, 0x00000067, 0x000300f7, + 0x0000006a, 0x00000000, 0x000400fa, 0x00000068, 0x00000069, 0x0000006a, 0x000200f8, 0x00000069, + 0x000200fe, 0x0000002b, 0x000200f8, 0x0000006a, 0x0004003d, 0x0000006e, 0x00000071, 0x00000070, + 0x00040064, 0x0000006d, 0x00000072, 0x00000071, 0x00050067, 0x00000073, 0x00000074, 0x00000072, + 0x00000025, 0x0007004f, 0x00000075, 0x00000076, 0x00000074, 0x00000074, 0x00000000, 0x00000001, + 0x0004006f, 0x0000003a, 0x00000077, 0x00000076, 0x00050050, 0x0000003a, 0x00000078, 0x0000002b, + 0x0000002b, 0x00050088, 0x0000003a, 0x00000079, 0x00000078, 0x00000077, 0x0003003e, 0x0000006c, + 0x00000079, 0x0003003e, 0x0000007a, 0x0000004b, 0x0004003d, 0x00000006, 0x0000007c, 0x00000043, + 0x0004003d, 0x00000006, 0x0000007d, 0x0000000f, 0x00050083, 0x00000006, 0x0000007e, 0x0000007c, + 0x0000007d, 0x0003003e, 0x0000007b, 0x0000007e, 0x0003003e, 0x0000007f, 0x00000025, 0x000200f9, + 0x00000080, 0x000200f8, 0x00000080, 0x000400f6, 0x00000082, 0x00000083, 0x00000000, 0x000200f9, + 0x00000084, 0x000200f8, 0x00000084, 0x0004003d, 0x00000009, 0x00000085, 0x0000007f, 0x000500b1, + 0x00000047, 0x00000087, 0x00000085, 0x00000086, 0x000400fa, 0x00000087, 0x00000081, 0x00000082, + 0x000200f8, 0x00000081, 0x0004003d, 0x0000003a, 0x00000089, 0x0000003c, 0x0004003d, 0x00000009, + 0x00000099, 0x0000007f, 0x0003003e, 0x0000009b, 0x00000098, 0x00050041, 0x0000003b, 0x0000009c, + 0x0000009b, 0x00000099, 0x0004003d, 0x0000003a, 0x0000009d, 0x0000009c, 0x0004003d, 0x0000003a, + 0x0000009e, 0x0000006c, 0x00050085, 0x0000003a, 0x0000009f, 0x0000009d, 0x0000009e, 0x00050081, + 0x0000003a, 0x000000a0, 0x00000089, 0x0000009f, 0x0003003e, 0x00000088, 0x000000a0, 0x00050041, + 0x0000000b, 0x000000a1, 0x00000088, 0x00000048, 0x0004003d, 0x00000006, 0x000000a2, 0x000000a1, + 0x000500b8, 0x00000047, 0x000000a3, 0x000000a2, 0x0000004b, 0x000400a8, 0x00000047, 0x000000a4, + 0x000000a3, 0x000300f7, 0x000000a6, 0x00000000, 0x000400fa, 0x000000a4, 0x000000a5, 0x000000a6, + 0x000200f8, 0x000000a5, 0x00050041, 0x0000000b, 0x000000a7, 0x00000088, 0x00000048, 0x0004003d, + 0x00000006, 0x000000a8, 0x000000a7, 0x000500ba, 0x00000047, 0x000000a9, 0x000000a8, 0x0000002b, + 0x000200f9, 0x000000a6, 0x000200f8, 0x000000a6, 0x000700f5, 0x00000047, 0x000000aa, 0x000000a3, + 0x00000081, 0x000000a9, 0x000000a5, 0x000400a8, 0x00000047, 0x000000ab, 0x000000aa, 0x000300f7, + 0x000000ad, 0x00000000, 0x000400fa, 0x000000ab, 0x000000ac, 0x000000ad, 0x000200f8, 0x000000ac, + 0x00050041, 0x0000000b, 0x000000ae, 0x00000088, 0x00000057, 0x0004003d, 0x00000006, 0x000000af, + 0x000000ae, 0x000500b8, 0x00000047, 0x000000b0, 0x000000af, 0x0000004b, 0x000200f9, 0x000000ad, + 0x000200f8, 0x000000ad, 0x000700f5, 0x00000047, 0x000000b1, 0x000000aa, 0x000000a6, 0x000000b0, + 0x000000ac, 0x000400a8, 0x00000047, 0x000000b2, 0x000000b1, 0x000300f7, 0x000000b4, 0x00000000, + 0x000400fa, 0x000000b2, 0x000000b3, 0x000000b4, 0x000200f8, 0x000000b3, 0x00050041, 0x0000000b, + 0x000000b5, 0x00000088, 0x00000057, 0x0004003d, 0x00000006, 0x000000b6, 0x000000b5, 0x000500ba, + 0x00000047, 0x000000b7, 0x000000b6, 0x0000002b, 0x000200f9, 0x000000b4, 0x000200f8, 0x000000b4, + 0x000700f5, 0x00000047, 0x000000b8, 0x000000b1, 0x000000ad, 0x000000b7, 0x000000b3, 0x000300f7, + 0x000000ba, 0x00000000, 0x000400fa, 0x000000b8, 0x000000b9, 0x000000ba, 0x000200f8, 0x000000b9, + 0x000200f9, 0x00000083, 0x000200f8, 0x000000ba, 0x0004003d, 0x0000006e, 0x000000bc, 0x00000070, + 0x0004003d, 0x0000003a, 0x000000bd, 0x00000088, 0x0004003d, 0x00000009, 0x000000be, 0x0000000e, + 0x0004006f, 0x00000006, 0x000000bf, 0x000000be, 0x0004003d, 0x00000006, 0x000000c0, 0x0000007b, + 0x00050051, 0x00000006, 0x000000c1, 0x000000bd, 0x00000000, 0x00050051, 0x00000006, 0x000000c2, + 0x000000bd, 0x00000001, 0x00070050, 0x0000001b, 0x000000c3, 0x000000c1, 0x000000c2, 0x000000bf, + 0x000000c0, 0x00050051, 0x00000006, 0x000000c4, 0x000000c3, 0x00000003, 0x00060059, 0x00000006, + 0x000000c5, 0x000000bc, 0x000000c3, 0x000000c4, 0x0004003d, 0x00000006, 0x000000c6, 0x0000007a, + 0x00050081, 0x00000006, 0x000000c7, 0x000000c6, 0x000000c5, 0x0003003e, 0x0000007a, 0x000000c7, + 0x000200f9, 0x00000083, 0x000200f8, 0x00000083, 0x0004003d, 0x00000009, 0x000000c8, 0x0000007f, + 0x00050080, 0x00000009, 0x000000ca, 0x000000c8, 0x000000c9, 0x0003003e, 0x0000007f, 0x000000ca, + 0x000200f9, 0x00000080, 0x000200f8, 0x00000082, 0x0004003d, 0x00000006, 0x000000cb, 0x0000007a, + 0x00050088, 0x00000006, 0x000000cd, 0x000000cb, 0x000000cc, 0x000200fe, 0x000000cd, 0x00010038, + 0x00050036, 0x00000006, 0x00000014, 0x00000000, 0x00000012, 0x00030037, 0x0000000a, 0x00000013, + 0x000200f8, 0x00000015, 0x0004003b, 0x0000000b, 0x000000d0, 0x00000007, 0x0004003b, 0x0000000b, + 0x000000d3, 0x00000007, 0x0004003b, 0x0000000b, 0x000000dd, 0x00000007, 0x0004003b, 0x0000000b, + 0x000000ea, 0x00000007, 0x0004003d, 0x00000009, 0x000000d1, 0x00000013, 0x000500aa, 0x00000047, + 0x000000d2, 0x000000d1, 0x00000025, 0x000300f7, 0x000000d5, 0x00000000, 0x000400fa, 0x000000d2, + 0x000000d4, 0x000000da, 0x000200f8, 0x000000d4, 0x00060041, 0x000000d7, 0x000000d8, 0x00000024, + 0x000000d6, 0x00000048, 0x0004003d, 0x00000006, 0x000000d9, 0x000000d8, 0x0003003e, 0x000000d3, + 0x000000d9, 0x000200f9, 0x000000d5, 0x000200f8, 0x000000da, 0x0004003d, 0x00000009, 0x000000db, + 0x00000013, 0x000500aa, 0x00000047, 0x000000dc, 0x000000db, 0x000000c9, 0x000300f7, 0x000000df, + 0x00000000, 0x000400fa, 0x000000dc, 0x000000de, 0x000000e2, 0x000200f8, 0x000000de, 0x00060041, + 0x000000d7, 0x000000e0, 0x00000024, 0x000000d6, 0x00000057, 0x0004003d, 0x00000006, 0x000000e1, + 0x000000e0, 0x0003003e, 0x000000dd, 0x000000e1, 0x000200f9, 0x000000df, 0x000200f8, 0x000000e2, + 0x00060041, 0x000000d7, 0x000000e3, 0x00000024, 0x000000d6, 0x00000044, 0x0004003d, 0x00000006, + 0x000000e4, 0x000000e3, 0x0003003e, 0x000000dd, 0x000000e4, 0x000200f9, 0x000000df, 0x000200f8, + 0x000000df, 0x0004003d, 0x00000006, 0x000000e5, 0x000000dd, 0x0003003e, 0x000000d3, 0x000000e5, + 0x000200f9, 0x000000d5, 0x000200f8, 0x000000d5, 0x0004003d, 0x00000006, 0x000000e6, 0x000000d3, + 0x0003003e, 0x000000d0, 0x000000e6, 0x0004003d, 0x00000006, 0x000000e7, 0x000000d0, 0x000500ba, + 0x00000047, 0x000000e9, 0x000000e7, 0x000000e8, 0x000300f7, 0x000000ec, 0x00000000, 0x000400fa, + 0x000000e9, 0x000000eb, 0x000000ee, 0x000200f8, 0x000000eb, 0x0004003d, 0x00000006, 0x000000ed, + 0x000000d0, 0x0003003e, 0x000000ea, 0x000000ed, 0x000200f9, 0x000000ec, 0x000200f8, 0x000000ee, + 0x00060041, 0x000000d7, 0x000000f0, 0x00000024, 0x000000ef, 0x00000048, 0x0004003d, 0x00000006, + 0x000000f1, 0x000000f0, 0x0003003e, 0x000000ea, 0x000000f1, 0x000200f9, 0x000000ec, 0x000200f8, + 0x000000ec, 0x0004003d, 0x00000006, 0x000000f2, 0x000000ea, 0x000200fe, 0x000000f2, 0x00010038, + 0x00050036, 0x00000006, 0x00000019, 0x00000000, 0x00000016, 0x00030037, 0x00000008, 0x00000017, + 0x00030037, 0x0000000b, 0x00000018, 0x000200f8, 0x0000001a, 0x0004003b, 0x0000000a, 0x0000010a, + 0x00000007, 0x0004003b, 0x0000000b, 0x00000118, 0x00000007, 0x0004003b, 0x0000000a, 0x00000119, + 0x00000007, 0x0004003b, 0x00000008, 0x0000011c, 0x00000007, 0x0004003b, 0x0000000a, 0x0000011e, + 0x00000007, 0x0004003b, 0x0000000b, 0x00000120, 0x00000007, 0x00060041, 0x000000d7, 0x000000f5, + 0x00000024, 0x000000ef, 0x00000057, 0x0004003d, 0x00000006, 0x000000f6, 0x000000f5, 0x000500b8, + 0x00000047, 0x000000f7, 0x000000f6, 0x0000003f, 0x000400a8, 0x00000047, 0x000000f8, 0x000000f7, + 0x000300f7, 0x000000fa, 0x00000000, 0x000400fa, 0x000000f8, 0x000000f9, 0x000000fa, 0x000200f8, + 0x000000f9, 0x00060041, 0x000000d7, 0x000000fb, 0x00000024, 0x000000ef, 0x00000044, 0x0004003d, + 0x00000006, 0x000000fc, 0x000000fb, 0x000500b8, 0x00000047, 0x000000fd, 0x000000fc, 0x0000003f, + 0x000200f9, 0x000000fa, 0x000200f8, 0x000000fa, 0x000700f5, 0x00000047, 0x000000fe, 0x000000f7, + 0x0000001a, 0x000000fd, 0x000000f9, 0x000400a8, 0x00000047, 0x000000ff, 0x000000fe, 0x000300f7, + 0x00000101, 0x00000000, 0x000400fa, 0x000000ff, 0x00000100, 0x00000101, 0x000200f8, 0x00000100, + 0x00060041, 0x000000d7, 0x00000102, 0x00000024, 0x000000c9, 0x00000020, 0x0004003d, 0x00000006, + 0x00000103, 0x00000102, 0x000500b8, 0x00000047, 0x00000105, 0x00000103, 0x00000104, 0x000200f9, + 0x00000101, 0x000200f8, 0x00000101, 0x000700f5, 0x00000047, 0x00000106, 0x000000fe, 0x000000fa, + 0x00000105, 0x00000100, 0x000300f7, 0x00000108, 0x00000000, 0x000400fa, 0x00000106, 0x00000107, + 0x00000108, 0x000200f8, 0x00000107, 0x000200fe, 0x0000002b, 0x000200f8, 0x00000108, 0x0003003e, + 0x0000010a, 0x000000ef, 0x0004003d, 0x00000006, 0x0000010b, 0x00000018, 0x00060041, 0x000000d7, + 0x0000010c, 0x00000024, 0x000000c9, 0x00000048, 0x0004003d, 0x00000006, 0x0000010d, 0x0000010c, + 0x000500b8, 0x00000047, 0x0000010e, 0x0000010b, 0x0000010d, 0x000300f7, 0x00000110, 0x00000000, + 0x000400fa, 0x0000010e, 0x0000010f, 0x00000111, 0x000200f8, 0x0000010f, 0x0003003e, 0x0000010a, + 0x00000025, 0x000200f9, 0x00000110, 0x000200f8, 0x00000111, 0x0004003d, 0x00000006, 0x00000112, + 0x00000018, 0x00060041, 0x000000d7, 0x00000113, 0x00000024, 0x000000c9, 0x00000057, 0x0004003d, + 0x00000006, 0x00000114, 0x00000113, 0x000500b8, 0x00000047, 0x00000115, 0x00000112, 0x00000114, + 0x000300f7, 0x00000117, 0x00000000, 0x000400fa, 0x00000115, 0x00000116, 0x00000117, 0x000200f8, + 0x00000116, 0x0003003e, 0x0000010a, 0x000000c9, 0x000200f9, 0x00000117, 0x000200f8, 0x00000117, + 0x000200f9, 0x00000110, 0x000200f8, 0x00000110, 0x0004003d, 0x00000009, 0x0000011a, 0x0000010a, + 0x0003003e, 0x00000119, 0x0000011a, 0x00050039, 0x00000006, 0x0000011b, 0x00000014, 0x00000119, + 0x0004003d, 0x00000007, 0x0000011d, 0x00000017, 0x0003003e, 0x0000011c, 0x0000011d, 0x0004003d, + 0x00000009, 0x0000011f, 0x0000010a, 0x0003003e, 0x0000011e, 0x0000011f, 0x0003003e, 0x00000120, + 0x0000011b, 0x00070039, 0x00000006, 0x00000121, 0x00000010, 0x0000011c, 0x0000011e, 0x00000120, + 0x0003003e, 0x00000118, 0x00000121, 0x0004003d, 0x00000006, 0x00000122, 0x00000118, 0x00060041, + 0x000000d7, 0x00000123, 0x00000024, 0x000000c9, 0x00000020, 0x0004003d, 0x00000006, 0x00000124, + 0x00000123, 0x0008000c, 0x00000006, 0x00000125, 0x00000001, 0x0000002b, 0x00000124, 0x0000004b, + 0x0000002b, 0x0008000c, 0x00000006, 0x00000126, 0x00000001, 0x0000002e, 0x0000002b, 0x00000122, + 0x00000125, 0x0003003e, 0x00000118, 0x00000126, 0x0004003d, 0x00000006, 0x00000127, 0x00000118, + 0x000200fe, 0x00000127, 0x00010038, }; static const size_t mesh3d_grass_frag_spv_count = sizeof(mesh3d_grass_frag_spv)/sizeof(mesh3d_grass_frag_spv[0]); diff --git a/test/Texture/grass_sway_0.png b/test/Texture/grass_sway_0.png new file mode 100644 index 0000000000000000000000000000000000000000..b3f93ed3e9810859754351f420b78e8d88189bb6 GIT binary patch literal 19446 zcmb`PcT`i`_ux-L6X`{Y0zp7QQJP8@P=laS6%bJb1wrW|y(NfB6~qQeM-ik5s1&7z zhcsy-O{5D*@4aSreBbYHX3d&E$F<&iE^~A4z31${KRdyCIvPy7_wGgrF`d)IUqlEC zf5oC*wD7+rkG2hjE|i|bpEB@>o9^{?;_LL4Tr*o$mYS^?*iNBtFc+ zuj|ky+sFdWQ|#yJ!_S>hS;y{T7qtGeIfZMJKFfN}-0I8b()hYx#YK4{5 zYcPTzlarIbCT`Uw`do40Lum4%nOV-Ya=-0=aN8}oX*PZaVuRaEPt14M(&+Q4J}DNM z&*s$V!P=CyYVk=v=EAMHK-I0_F=J#|aCfL$!qBjEb7gvxo1dYUh93QtcyWGRg}mNO z#)U~++_*9LcFY4=c6|S?r;B%48fzizJBcB*x{{ycJI%XN784WmfO5ggDuKLOUddze z=*C#F+q4ZUKT9o@Ffz}4yicy<+<3Z6TqY&d_E-wLPz1y^>{RI~0;4pzPrk zHRhovFy>QXq^r8Ul&b~*<$Ets(UWn3Bf32_Xat+CBt`wGUr}^w2*jS$AR#Z|tnPP!efS0$sH}*YQ z>%xT#INZ+a<~Cb-zog<3bO*k6;91?2pwN%w(^m3)4!r#Q_H6vzwHVlkKR@#<%*?}h zx5$Y~?lWJ1{9xzce`L(V%D8egW^Y zv$ftC6&0l(NGo`>nrEKvgu1?gP|9?7A&!0Do?0v{|2xJyVZA6L<9emkr~LALs3TN$ z?W90NqO=twBO_n!h;#JSfD`*MsQ$pSbN$I@0{!5VIh~N8v5_dVor1BS$g=t;o%MGY zQ^~gW_9_saSZ+>sO=@~%JTunvn!{f$>3ZifmT+~ykO}-@=jA3&m0Zuc=%qVP?#s93 zE}+5^jkUG4C#piZ`(+ru@++t^pHWFFnPo$7cPQVeZhx%( zNm_q`h^c;)g?^K|ycV>xZ`LG;4Xr+B7>B6vQl`yvEi|g++ow zuVP}(J!jT^$*kYh+)S2<)J)n9QbQ4Hv3Mi#(we43Re}&7b0{+#A0s?aj73d2zdon> z(cX=kejS~7#pT}2%Q-2#Sk?9%*FI)M(AMXCA#L@kpy1EF&(}2Z*97TAka=)NhmmW4 znQwW0;m&-{BDFaP`~9LZ)RUZ-C$06#+3YU7hq6Ef0oJ}E9qG>q?{Q#dwm&f#FO;3h zO}^__t|}%Zf#i4v1o||W2C0et_by$!q67CqPzIKCMLzoq49Z z(Gu}zDuS70y8GejK?ibmCq_$0XQn_>=m|YLl4`GHQSG^x_KlF)duNj!Jx?nvl+*aM zkSkeL-qG19XMfLFkRG-vx_LYx{nVf>cx^m0($GXd8?>oE$pFb-+$Izxn@+ahdG_B*ZT74Rg>Jb z_)b}nkF~`2?*!}7@{k;STBg5#{rVb=+~_GRdMocVK-!+3o{n`jmF$_$ejtiEbIi(b zI=Y}4OP)u5IfT*Gn-Wpoy#gK3V$wXZ9BVr?!tgM^e9>$R87e3w7@wqS}lEW(a!7k}ixD)rLMH{9ZEkNM$RUFC@@^l`fbHNY*Kk=aC1PDRRGgM-ODL46gQ~FkO!bwTiY{D9R)z5F|zNcP&iE-n0SNX+DKvC z0h9hY)t2+4_m(HxJ>h1=;(=}#4Gj$z3x0$Q-ehNMn_j!-6CqvASoyt65$6!-n^QkotOs~T$-74f1bN;(2^JORSpil5Bc zZXW+~-GH;|6$#mB9az0Mu1M4rxgVQX4a$GAa4nVyq_!%Xbr;-VOq z+Wh|MZdqs|Uu7=|)l8kEj{K@$pUN9+Y`P!;Xv#wGIv?#>M$0NLxL>v=!KToQVNpw0?AVcWWE?!SND2 zJUl+G>-%dxL8LGI@=oR-J13c7uQ~$a+~sQ`aO9UKA~9_Vp!@o#8#^&!5fL~kIiV*Y z`NoAc0^UoVw*#UQ-f;Y`VXH z|1Ph8>yq=9^`l3R1f-NCp3uSbwieiT3$6R?AcC#f>gXPI{S%ot{E^ESAHuwZtmIyzY_PB9xOp$}ox8|5^+&IK+zp;r{j(8_%*+MRdpIb8M^Ed| zpT=(r3*D_#E6F9aq|f(8b&ec8dSjm;@ywj}vBc)pkZ_u>;EYy}D=8_x&d4ZyO3^r- zKT};Fb0j1sWB2hPgRls};l*{fmOLVlCK`ATrf=aZhwhdDoBn}R!i9&Y_fN^uI6=9d(TSNqtU#=YDX=?3{NU*d~!bSbbPOUO$#ZXjvo2c?rd+$UG`dz;J7?iD1htRBcX`8Rv3P=*Bpk>n>+m=fL3a!xE9o0lYUBA3I7UWBPQNwf zd;{J`CF%9+%41SexrwzU>x9H+Zkdmj*mT#boAaIGYnfFX#O$kuO^;6RKPEx{6b>Gj z>%BUY050gwq*wAUHsX%};>)P0yR59la}a26CF`X}=6rnj$7JjFz@I;Vrof*&9d}{^ zp~X4P4OCqD_}=g-+SLLp38v<3TW%Xb z^c;Uc#u2P@?d0W^zh7o1=Y#ap<$xG=`tUDG`jQDb2u zgF%0uW3IcNEMuM9e=LWg=@B*Zo1Po2+ziUd*(v-rQXkbE>v@uBV!e`LqVhXRCr$Kq zi(bIvB~N|UV>$f0sGZuxh{2Bn1E1k7e;KLyWvdD5C2AeiXUjb+h(SU^ze>GV zgCI&j7D&FpjQwKQaRNh3&&$j6ukC?&Zd?pc^pqzK3c9^I)2quGb9FnyZ1QU` zhhwa(HWK^+PvWYW`k~7%(Xf-&8g}-1{m7QEurSUdj8`C{X6dN6Km=XlF~kv5%0Rq* zYr6SQV=V6Y=C1**v1orvTrW2{m#B1#*BsrGuGY%Qs?(VIF}~Eqv`=k*u*!F9`j9AQQa#KF6htV`h%`cI*5++hQQM+Lx|eUMtMkStMrTs zQ)6R!B{{jm+a0bL;_b#I#7~P4iD%B7xkEXN2h>T`kHf=dHD0&49b+bcezvNC zoIrenA=HcC(>dZxf?)1oR*{N413PiAI|xk#YfN$c?GsrTBo(#C76qwz9Cwo9`IJKM z1R>&yBYe!QkYgl@3Vpb_tlBbtheiaUKOb&#qZYad+L@V|0?X%^I4XKIruq@*6gPo8H-QT9nQDj;K#2tfkb_j@A#Fq^(g zA}%k3iIoM8YXWY z?B9`(JlRbSntFE(>DfkWUpb+R?pK(fk%%F zmI~W}Fk4@~YK{Z(ylG=&6H?o^PS9>WqGXGbjMjb16pMfTK6lp43CM*3e=1ij>r-sEfdeJ*?5oA>*ev&m^N&2S_c>^2xOL)mcCBeac99{ z(rW!xNP_0h!btt-v>i(l*VmzWBcaV1>#}rr(5qV--jtV+;ARfId6{Dcv#1@%x%qNM4%gnnYCnLrNyFhB@h0Fs;sSjxI@8*M?`8q`KZOc6SGm@D*jLFe8_$)2TsYBDi)xyAOm z%gp=3 zANoiqQN_2S$qm!RHG{@ipP(lrnH|9sL$QL}0z zwWx~9iicKKRsw?Ts$|LB9IVrU$WdE!@uW>bj8pR9;Gik~W+0i@;?dZidgL$!@np)c zKY`eXm4kmEm?NgklaxVN4swMa>aueajf|!tZuR(6id6UR-3xUqWErr!TIk^WMDv=n zBhMpnsHfiXDUw{c@wBnDkMe_Pfy?rBF^~t(9jd>DOyl+ z-BAs4zcFKqUR@LFM6AB%qNFFa@+3aEvsLR`?Vl-B0~dwdF(v(F=kcHJw7xAB8bbHr z`D37LsH*b-tA-rF5`K?7nSeeO7P5T$^l4yyzrq&AbhvDDX3@>nm5C!6u@d(|dD2Me z?yCEEvf4X?!$*W@rC0>mC7#xg{Dx?+PKuF<9ubR=A3rV@qh%!;xpuTHBg5-smt7|8 zET4yFQSI8=8fux&R)nyW5_0g}bzF=``{W!NAZ^YFpeCsccno0L$aGGJD)u42$E%Xt z+uK*wYq0GV0skBjiVXU2cj)Y3WdJf~%IVCMAME=h4P}cKb15-JzV!QZEF96okc5H< z)L7e!gVHhSny;^Ki-&UAsinP%KjM*JIj%}Y|VRYWZKr{@o z_FktOSlz6QkB|3j$x|EQo4;{?0t~+6L3RRG5E)laE-R*#!`adVR6Kh5lB)ru*}#Sq zOYeiX!U%C9tWzdLZ;$a!KSwIfj(;gGj>XqtU!1`XV8KV(D8?u3{|7d@klmAW&PExd zdGOh4a5&B3z`%zLZ!W%bf;4kasr)XKCA=Y+bg?9{Zgf#N#lrvF;Eh3SmtBFOp`j#2 zVlQ;Mnz}G%o;ZHoB`6$oIFOn|$#0s$I&#%$ZXUIW7 z|G1Wyp9&ejhUdHF!pJvk`+MFat!9XgpV?p+3nTZ~%uF>s5lM({hz(ry5jPj(w9!hv z_;a_|A!R8M_kQ11&*I`@oZx9J8nDtSE(c+69%aY}Au%>Tr!YZ9GXk2O$wC{z^b>=S z+wbLFu)UWk4?p0)|1&fcjz=B~2|z?w#!9Ci2_%#Pwv(Hm%7B)hI9y=YC39-HWbhP{ z(mlS{P$s50g{X@s8n9z!!Qo`eR;2B1+m{Z9pZmN^=O%@Tt7`DA}cTd^CCod zm0POB;yt128eAJU!+0SqDN9Qel~Cjo!?B~_k;V1vLZe2=MQ_|0y#o`uekNH@*rLJd z8z-#!+oFtzcldWS3;Xj3+}FW3%>I`+)t9|Wqq(W2D&Q&p208EQ{N(x09!2yDGZmQ0 zsPn0OQAjK#1*|qG5);U#e#<;6s(?O5NQy-fN8hSl*N0nYU5@Cf)F{g*DF@?EkB^Vv zZ5R~HELFu06cr)M=XI7Q!Es8x+>EpqDHg*RvXMbWRJUKU&H zNssz{As=wNA)e&Jw?073XJTe{Tk|PnbwjAgEYH|w7~W(0jR+6pLH1zkaGK|g?C9Kd z&*yvGk&Au15XlF&?*pS$4h<{BP|xZt@h-}G0wUto3;EF8UGku>I%#2D^JB*jDP<%ZCxD0+ChB2qjGV@YXaEiErk?|1%_ zV%Epv3OVA&wx8uT``slfYL%?Avc8r*DA?$ccR4Fj#y%I1Q^Z^78bTT5%yr&7HLHE zg+k#*P+UB6f@%goh{*C8tY9RRUPYdr3qz>w36ysGIN{7Z2+y?+77iMldk*XuV$12g z02StEyXcWx_H8&ruD@4+mMNVSh}(UHK^ZUdqqmoT*Td6~b^5To&>!c8Q)+zuyLom$ z+Q@^E5nQZWSxOH{Cw-xV~rvX=%*2sRE5)p;Z+7YIf|+3LEG0v8oqZ(o@R zWmD>$%NMzyfeQ^AGdA~qf;1mE=G|S;Ge4BrJj%gOMVyNBVf`NepmH#GjK|#fjmHy` z{X2v6A=Jd0Lf6R-I`9jE$I@`r1|j{cR}1W7pgEZp@fs(Rd-~BCq7fL0a1|0YX`EC2 z?$#-_OfcHf|5=8}7l^VSoj|Dl*)1l7ONNeEu^xjO^F?J%wI+Tf&w!E?AKlP=1|C7j z2m)v!=n?S{so?=nzdb{g*KT#%3mqew7%efbP}gr#3pKVFyijC^r3^0{KTB3)3r@^9 zQd49vz9#qj0mBLt%5#47P`*C8>_sJWUM}6XbezfvJeU3S zE`wmpbQ4JV&D|*J^oN-@ktb$Mgc~d0ysRsr(bMN_mts6B5aCEE&4+~3sTo1>5v1%G zu=3*j53)F4gVUFXJ(M6WLYWAd=+3ibm(*5KRZ@_FuK z#7iE^0vesU7R< zYT9&0i!f-DJztGT5j)I)jJs@%G9*o@hKMQi9QrO zqBZ-xRU%d4x7`=8#!2(nS0zpb`b6Bp@2k7hHU)(saxUHu`E`QXTz z)R&*~^HcFPdQ4Y1z(qmbEK0pbkKV6Yls$MephA!x(VZEcGKbRTxwO_x_=PE|>)Hi{ z+$e2qdb*LVE(*VBf+)dqRDCiRG2q;CMaBugab`%Og268J-|{V*;W4Qy_xYy-al&xa^oZODV=(dXKaYa)i_j?Xwi8YM77q_VzB+c&*=n zQGD)#%c8iRy+OlTD9E9F+Z$3LWq}+HfE2VKz=;o|!`v-t&dAB>CsLh?zwnLj!cmWM zTGH{z5_)u5jPp&_9cICht24aXtq#AHrAD7_RQ$HeGuEOcYghVFkP#PRhMa<;z8rqd zwO(Y4-FnQLi0^Q`PK~AV?oXVvCE$?QA(nkYUv6!w4c~Z+pc3;I@t>#MYtBRY{d%{U z7&X%4lIuN|k@4k^``vw=%9cG)sjw4{>^XtfYO8+!{CVcTOYmB67eD^ z*XfgbL|3Q!(v*J-HQ-2E$B$=V6rYF-NH}l=9WEA3H$q>@REot4(`2oS8^whRUZeWh*h|mwQ~uiz-ZUkRnk*sKpbhq=+0m zJw30uFd`YJ*3y4ArTWtRKSBnSv^iU@sxGpMRSPFGL+wGKB@fjS`^6o;kUbM=n}oLM z8XiMUG-!;cp#xgeN5F6K7R_J3YmxYeR_yzL8l?LDnGCQm^IF z6btD~@OK(4<`%FIC;Txp*j3e^^dr3dVgeE385!&rVVvqSs%&I_#>%9J4{3U~kS%GO z<^L-*^#53FKMneG-9RIVZ%y~_OcOpk4gPbbqA)*UG!e* zvfkvem?HU8*~2*+NBC~8D8pi2HI zncUmI-Rl?{PG?>!;5C+fcBjjCu~Cj3`}ew*j*hq2%Y4?upaTmJcEN1phg*v@Cabgh z@xDng+Bp+{d$xcv+~h>a{AAfidyYyYj|GUgNtEwLwxV!!%-5Zid7nz zTrRkf|DdSo5Fj9-Rlj8{!Ww}?`S9)?{Z*4wd%?AQ%eU?9yAdW0W{L7%4Gn_9G^i+Q zmO$PlRPa~;Z$WDj77zW1Ng$BKG^BXmeE$5|@ws1k`D*FxcFb52LVS||>KxkX_^N@* z0H-=BtSCWr_MN=izhSm!9g{-?O_TTB{M>EKjIsBRvj;y|s3C0Ckbz@?vb`av`T?Ph zCik*M{+2w9L8TPqL=nWrh;LMqydbIitv`BAU;;AtYg1!mb3x%uN!JK#frK?y zi!f09Ta{8Sl~PM1BO{r0mOn%lWz(5>Fz65?6BA9M?@Ct=^qZmJnmOs^A2gQVd3`g@ zz0XsFn;)n4`6c{WBS1+MHg*>^c3C4rAfA<%8xdc}QC>Jqx_b2d`WD%dyy6%U0(WIE zcAs@PvaPkKPYmUjEPH5r6e0Z5)?zbDmcYhn{0O*TO&1^Z0)Q)x9x29nQIwiEpO)yS zD6Xi2P%_l-+Mw=Vl0E6wBE?v`^mzQST;G~?%RI&)3d5&?BZzB5_A^I%LKgO04e$nm-L<vZ4ZYj)2B zWU=!NJ>DaDCa=w=PdWbyv0Em;}ms@w`T&GU3+Yq)W&!HR3$H}64YHv5PG}ztFvrH>kgfJ&i?PvK`-ND zw%EoM)tA~}p7&9KN`Uh#`WFMG1t!R=f5+2#E`q!kL4GzCjcIVB6pnICi&vUT-AGae zAI=Vlo2`HQ1RtS{g+@oeMb-DqD`p$Bi5qQcKT<4crLIEy@&WjT_b#6%bv#mh{xrnQ z(gxrZ@vKOV+2`lI>_F97PRHIa`O$TjTefDrY7&$0X!KrDOI80Aj!^z_SkVCyLwshv z`!_!>i=dK9?|%(>G$!lf%T0r(>txSl)t+u`S2R@pDx=PktD>7&AeznKmP?umVx+l1=0vMjJKwlU1?s0 zVAbZ8+p)cm3siw^sx+#h0aVSoZ)bg9GiU?X$F3uP$QAjNMRZ?;d-O7tyYEn(AWpr8 z3FSIW#&1eakM(&?9yK6SUEb^&RgwGDx0$lk_p`wMD&A#EP*Mv+w11nHHkp=|rfLlZ z;(3n_pVxAXXM}||ItkmI<@qX#=&s*JYvRuR+qb^i9^LBmSZdKoJHp6tjTunTw{f4z z7e#gA*78oUVdN{9Cp#G^m_TWNPDVyl(AYplKzZ;hga&SpG;J@=o?twa2;MX}97j68 zyuADka4|{VVt45zC2cC7VPV`IHh$Gw3Z)t9+`j`=z>JJOU?F$58s+*+9PC}t&G9cf zl>H05y}cTAgJ-WrLo`4dj_?WE0}YG|MLmUtGMB9wn*sr9;*)HS90_DQdrn& zt$uCbI#@-SF1rr$+E4`f9lT|K!6yNqh(%raGU1E!$nK94r_pG-2nNt!*HS1^QE7zp z@yaP3bdm#%F-FpFK2)~@xLO{IYS5TqDKbm`_}cuCBWw25yqVaiDMhY$*5Zz2=jJ!a!d?Keh@`8ksQu zBlS`=Incxz;M%Leg&rC88}!g7RtB=neiV|0LhKzoI|B*RFD@BR;XpFhQ=_hLh_{r} z5#xW)7dfvlj2sKgF5f#a!G`$aXhul8Q1r}y?Pt{H3gs5$XNgKE=)If+HooVlJO zr&rgUkud=RG&w!ptDvGHa6(DeNfcfP z0FN@2UD&swqJN7XdSY&~ecKPZ@-QfC+*)z@0hEf8^4R_3FCA{As3*6?pylMKamgWOmAK#u0r|= z@KZ}mi*ESi$A+#)oq%F8524&>2rursRbS=Kbd7@aL3r!e-ZO@VohI7e6$Tsi4U1Q# z4KU+WlcF|4<4-V`$OQZ=|InJmVdMI(&# z@*#lLp~m`qnRYU~DEmAY833m%+Ak}oh&Hh>{TfK(P^ znzyWhRvPaiyesVUvTX{33iT3&SULjeQl?ml$@potloxl18aQ8}^ z=WEO~T1i&W><|MEv3z(He`1w0eAlCVb3i|h8k1F1p}>$LHbIG@6$m>%Ee$*vOXrbq zd)#1?X4ws|GZ2R~sGFObX5>T_5fTc)3w;Aez50sRkz6u04$)8mS!81s%R1fNzq69~ zvXA@4@70lFuuZK{L=D<0C3%AD-P2X**zcR2oxR;&w#|`UGF}2vOTP@P=xMn5rx~0A z)cEuB^OR=52Cl zW&!}9gj{p7JI|sfE#}-+@){cnnuSpfK`d%kuihFOXfNulIS@~JuPEDhoTxYno7xr5 zryw%}rUsmSMk!a~=^~H$-&d`h6S6nF(1&A67#KEy_Ty}A?XISuk5~ZSxj{~}5<~L~ z3Nk2!w)xqS@^;4qs#}9BBXF($@yKOOttK2&0+UEr~p-44DH9?^; zL{dDtIrWhP>T6YD-w1r(1k6CQ-^#~luoWaG2*|sxI+iZ`3N2=00cP(f11oVn=C1Y= zXc`$`f@}lW z@YyABKvvV*2Ti&#QgU+cO&}RX6*0%Gb)tQ>ujemT(BXU}I8SQ}!m&-x^?oht z_R~{zt;A!#4F)u{#eH`V&_-&0eMSJXd1U73<+btJ>N@xE18OugehI?4S@sx;n|O*~4r5(-k*jXz_5r@28Q`r#w@yJ z0LpC^8QLS_SCgw(+pDUoRvWUm3cnogqF97GckY~G-<|sn#z%fm-IFuwCs2%tougyT z_T|dCnn0yr!#=&bvL0bE+f0U8key+O6UAE#6fi|qzyZC4w8!l_41L5D?Rhe` zvWxhB@*#AXZ@_&|T3z2a48sI7V}CQKiC=cN=tyL1!+l8Rl41MPj99*MI`^5L&-EFy zl*tuRpk42m6Us6&&JSjJWj8lU7CpC@J0lV5dH4c+S`H4%5xO-)clnqNUQ@1C-d<{r z04JM&^Y3P@Y=8r}Q?-n-tl<%cTEP`p-?CkWrKP@$ko1-sP7^;;z|Y$b4n{2eic7^| z(v=~Ff06ip@^-)E)w{l^3knY(DYXOkD<2usmcBfD_AHh{+kqgj5d(34MQQKe9kQtU zt(x+JokZ~@5HFh@jrH|Kh2LF!f3SCL?Igs-^*G*Aq5zO!J}Al4$IJXVZ^86KN<~Sn z%x@{dkk#?VADaQ(tG|Ff*r~2Qn?;p=3ck>}u*ZtN<964gVh0G6?F$+d&E-kID)@0wNiUY`C>sOk}^1EJ$JCY{9+;S8sIp)0z z+10h{)r^SL%Nbgg(r!QZt}FtA*6wTvhB#nlw|+s6u)r&{$g*)LQoN1P^!BI+ z6A{W^7gS-6F6U}HGjSi#PgggrRSp+Ix$KPFC%Pse@Nc`hxv56F?x~Sa_#ah2YR{nN z7S{Fgf)TI;77aW+Jn~QKEbVwP)YfVMaDq|%03L%oC=0hEsX#Ng_JiFDu!5>b+Nj*> zUn+MW4p-h>aQ2iuAYTqPHW{ga{ePeMJ7{4JlK~n=1ylpHuio++T^<=e-+|HUZV~tk z1FrsSv`q8!lsq9PySrPEv=@+IE^aQariJ}p;@!U|5uooY(Jw%11u^M2?CtF*KrlYy zJGjV$9{ZqhXgkYjhysCR+y_pcc7v?}MS0!LD2&#dfpih(x z(72aUH|^$rOu95bKYs$$Ww-GwSv#X1*tt-%t=~^g?Xk5wUtjV1iT#9gJXqXK3mAI3 zG*qu<=pUZecFW!oH93U+R8rzIa=Cn?{R6-Ke_YH{v zNYd$Kr=hL&#ey6q^rRh}W1q+e_rt^pMh7q|1Pcq{4one+e`SAs_63B_SA@I+$)2Oj zP%5P;lrarvYUgA7b?q$B zR<`5tVP*<(7{y_@6*zQ@=-5+qcOEWG$3n;MW}Y!;mx|fn`7c2kW_UOh1w)-SZ2DW) zC(8SAY=mDp1TbSGD^p#I$0Lj6f}Tz$F~u{djOJqn;z z-uCUAfBD63C_>1`HqP7%<4s{l60E!M_yQL6LYnamIA|p)sqX;)NO4m@$LQ$VxfdlV zHwT0fC0`9s=K<7XOP~+NU?gCi9k7f5=Ny{_DoB6>-~1bX2_t zoc<;R6J{KHV6i|^L_w{X9-dCA#5{u*R&sB#`|5+!$p>2ioBnZ}$q->gsu-{^NNz1> z_mlXlPhv1AkScl-YDphcEW}dp{5lFCiW10Wc?AqV{BAp)Wq`3kN7% zjVH`;6asOVF&Z66Z4i;uG~(E&Gq~&}O@juoJ~nsZ_OF3>n*c~4GB`L`BRK8^Sr`A6 zr;ju5vuS7p#C8kk}h4h7qt^5>90 zLeYTa>*1W(XqCsIMtP{0^&_Vt_@L5>Bag;cLaR}M4-QzRq`wAMadnnLaP|xPF;Ci{!&s^F7xJTydP^WaGF zZ2<3@M>W9QOa=x9;cw)#tEPc4;rKPaW&Q7?A$Q)~A?aFj_D+HxPVIsyR;D{|+(;WR*oVy&!g*t%!$1GazM;tg z0NjA3zG#Ssr2))p1mmK1Hb(2Uj$MWZdpC^B*b(Hm3kR(}K2BhYk^bHD+QBGxhe)EAbhy zDB%+D`n}-#rTpF&WYMDtZx0XoL*SKTY7>q}wP7KGW9Z=rrtg9XJ5!29I;}2(U#Zzk zAd&OD01O};;d=IgoI$fzDQ|Fu>k=SLf3Y{w$>f75 zMXIi_X|AfC0&W4zhOeP=Ke!xF)kv8UfQw_|n z&_g~DJS>164ypS6blCP4vb*$Ol>6x{YwpF8+25_a1Ij;Cuk{>$ao(PbKOp8>ZYDj6 zD@+;|axkG5vu7W218j~`a_Q~ueT`p;KvLC=5E=ke_&=ZgXBD zGC$Di@>dpM0eVbF=rafLM=1&DQ3*u>#B2Gn%L8VxqkQRDBTfPa2fN)eNaELo_x|Xj z$Pc3Z_ydmW+645NqG9NQuCNF4D9Usb%*aT?bQA11?oUWyXgAol^5)TdRZN$h1@(+6 z5bVHe$kZhi2h$bmLTsIOwl46)8O)S{9S0$I7qsOLrG{||-pIIC`upu>;_97&Ba~Nz zkT*!Yx_Szi>K{9^r!R2+KLA`WfeabpXKv)YUX;agD2* zJ3QA(1CL~0(AU>@gJRA}k5@zXZ623X^QWcz0zdAdY|$`4=HS;ou{P^8RQ=eK;))SK zX<*tasRK@(Klto5ydwA>LB#}{`TqSeTH&+-r<_}s4id+m$N}*HcWMc;>OmEJ3W{l} z-29<2@b@FEyTss$aWDcP+RYJ*-Hc^HonTIl#G4pJfWS&xnkRr*HnaoO}kWl6^ z^E}VC{XYAg^Lww~AMd|!*SW6mxv( z{}+u=kima8J^$<@h#jJey>{0#Wog*M*QnP^@Nj12OoJ;m87B$7rFh^?gyaR!H3lU2 z_34`e51SsvbCl)f^;O?AS;_qtZ)g8N=7+a=^J#XO_UaK|lUDZ{_nIP$TQ5r)bX0Q+ zpXxA$^B?2y{E}oiBL9&ug6HLzw%VPcq=N&0xuZn%d222wr)9t8GaaXhgZlsbpSYsB zdSs*rHL~|HC+Fw%#vW$mF=BCjk`tNDsH+=UYYsSUmRgS@;o;8LD@tGNF?6&a=OjlO znvDYRTPMV7H@oCliNF2(=TD!B_grRxQ>9VObriDuv3@VPzTba)vA@@=85I*kNKAMPGWo8&yqw~|#n{-GvO^D9|2tCaV|oH}&FdB^lG=9*$ryH_{Hq@$K2cnOTsX` zP}@Z=oUhyA?ppTR+L{X%E00#|mhKJLq%PL4)NK5cpE^m~@Zc1x<>H!W&G&Jq&nJ{(LqcBsm^?jl9yOWEcO$As z5&Ke2K+wRz;FDp6gK><98ZxJdjU~bmJgk5wGYd-whd7RgjEpQyFW{CyTwG30j_J{Q z!hvTqDZ;F+t!>WE%`Kns8cC54IMNem#iEh7f${P2Pk;RQK^2zXlGnirk9>HS?3oen zUC!06fdMwx>fzI2=_+=|6m)cy;w?wsp(Zoq1T_Vem6d-{Jmop|l;><1@890uos0`S zlW17Z_Zb;{ir8=N2#i^pgT@2iWrq4~U{TTet=Z0u4o2f;54*J`*nU1@qb&&YbN1_8 z4<9{xWEsGWxpr4aXKl~-Rgs=>!$nb?ulwxpcL^b)f$HFPm_pGadUHf^7ZRiUiWUz z)8_05&OswZ`tjUie~DwEF@o);daQMlu8o>f;mz#){3HI?b*u01=;&OM<3(%}Q&QfW zH%Iw>ri%$_u`0Q&Z>hk+NNp8l+IDq(&2u_s#zUGzFOvFg2L52DUPU4mDQv=-6jU6> z8#>HQvvW2q!fp<=Y>m(7+n=RDwr5V9yJt6fBBB3JZ?DX!%IVj4Mjk8pU-n!b`;ug8 zj&~d^x8FI*9eeqi;;C`Qv|UUXZxRy&?kY^%p$k5>aa>6J0`om>_bPTz#TpC??QxCo0 zCiqH~Cj5CZ+~# z3zkBgCv-QWNUVY}JhW})q~fo5DB|U*TPD_i@6;3Uqer{V@<(hddOU;_84rys9?L^x zZI}j_xX_SK)yq}#+upVD3&Fakep{TO*auHNIK*XOcbe6S;IP;i{HG(Le%RHmMLPQC zDKT66&)#3I7&)Pd|NT%ZOhWRsi=@X*MysznzA=6x6}VBrZ}N<_il)~x_a`$ z!h-Gr-oN# z5zXaiUsqobU@B*OTj)cf^VCo0=w@w!R_Cu)C2KU~R!`tfh2dexJDWQYGXiI%axV)D zi=iY5VOpwwuNN?yFhe*@Bm9zKZZU=9e^q(2lO9Q*bGw|YTX2;3>JfT#Yb&3J_l4!< zPy>r;izL(UzV~!>dzCiVFu1+3#5?AJ7eWtK>g)ZQ!cKU=(Urq*c8hI9k+1g}I@Y?q zw%NuSy?B2H_Pl-OYs9wq)9_8~-P^av8e=DsD?(?_GKd;|c2b0ssKunO{^jW4aBrje z>6f=t%E;NXXTQCB_l^%RBugXJ`|r$#{_-di(cRn20`cRO<>l;OJgQx%b#(r+nEI}* zcr`zK2K)a%5oeLD^nT;>2P5adG@m-x`ZzK&(g7wVM<`69mjb8bhhH!y*gbyyI1unM zx1esnq_b1YmL?E)3J%tJxhq$^-KRBR3c4B~&ZLDkJY&t?e`%o0MBaFUt^9sQZG zr*+nfpYKmf7xXT;N^>ml)y=G%HRQ2l*6#%b1a#V03~%GHyyum%o8hL1|IUw`zvj=* z#T9RDU`wUV2PQ)x!>ADT#@mgiygZ(!XQ`S{xf28|TLxgVO|F*UA?+;XH;O zH>a_3ydM9zwo8`YPZjsX3vlQ07qh|;n`Jda9i1kKv1Dje>r0Z68ZQ?cGqW)&2+)d5 zN(ae~b$hKBt$&%FyfEwF>bmKdUdf0kX}$01=@B`n`)QMnJM}#J366LM8QW{cPqv}B zHKJ^7Z4H%6oo>YO|G04dF)_Lm1UomCcC+O1bEE5#6nXy|o{)$!wx`CF7;r)$;}jPc zz7E*VLKgVNAn(=%d0*+vpVHIQPo=Ju+`K#{v;3KlA-w6+3;u^cVW1FefKQlnuY7J` zvywR>euAB${%C)JTU>y50_N%N;L^4tKQbLmD;P|QM!hR4DoV2%uarE;LBVt^=vjf^ zlC8YCHE&3|jlApZZ%6l~fgpoPvygrbQnjWJDBjqX{gDxqw4%xZ!} zfO+Cs-Djjzf0ag^bd%=rG7aB&Le4c?@bRPG*;ePL$G^xO?M}!~axG1t`8EP-d{ zrl%_&4$a8zcmPmICb$21)8M~TJ-Rf>$*$K7XXkXP^y7a)dkjdAu9V}5FI+Mw=7xGJ z6z0C|NOvr#%lkIU9Kyr>SFc_j?TJccbI=4gh9r9K?f2Fn_1_ly({dz&)hbQ9>|)h% zvE%9n*GgZ50c`N1WSiw6T%OQv509O!j7}Sq6QmEQkbVh{1F4Ya2`@bIA{S<$aC>`O zIejzCesY5irzbppSUY-9OZfTx9_(N``r7rM=c3>3bF=Qd-_V_8VR-4cBR9U2j~w_9RW#tuTQ0E`fw=duh-k z#^%WX$C1NAPyP^|SlF|wsw$K8%{LOf zu_|#|MJFFlcgwSTW4GkTrhqdf8`%gVotqL3`4uBo11-Co7hF(0E1qWeVeEGS0p2xF zoUxRu3#yZ62>lir(yDSX?I`p%o_1HpZ{HQ}7$NpYtbb)uq+)12>pS*CK@t_>)l5Rw z2+w9?V^f4xk##9*H-d}bkDNbg56|x0{C$=<>QJnrtjly8J9a}1W1;k9eqrHYW84l` z$&5Vpf|b?E&(A-Oo#U=Zay+?br%A};Lc>*>adC03z{>-%R6Cm62g$V7ByD8~UE8?= zhhz7;Iy;MA2oW3>=Uy+(rOA4|-RE8{cO0!_31zN3Io;il=pl(dtJkJJnu+TbC9dc) zopxmY*1o-|DdBfs3Fc#|E$L!b*PdLt%V~ztyp`o;_XW-blR`vX3dVEIq(Jv$L4l?% z28Z{PU^8CW*js-fwhSYKDDv@$+^WdB;|(8nZkQT237k1opsl$_v2!F;hzq0KM~+GN z>L4@m&(DPL7#=!`XB+R>R*f_XcDvkv2@TW&0s=4QM4y}cxe{5#9Q!N(|MS#Xm2VSk zo;O)sM#}kBwfAlx_bksM+^s*H5)1gwCyKjEPRqr?Nu;3OJimmUcb4Gf1{BHB=b`-iw^}_s5z3TyR-X3!4NF8J1Y6un(6#PrYB3h&LawNy| z9@Ff1cE`3K-kflkZ~DmeGYk(o|)6`eq@_U ze72kG%Crsbk|VwSKYrV3Bk?xCbwAN@ISs$lttpR}NEkz`)*vsbQ+s(tffH^GRp7Z@ z;KkChSA2S?m(laW#*(%0bfvVJ=a-gNb3N&Gd@zl%1GjuoytT}IDog`1x> z-nRy95R3C3to%&%wx-5tsLE|2Bky-dTfH@b23~4YGa{PC%mMaUs>+O>*>g^7V}1Sp z{@z~QN7x7hp>yYQ`C!e%SD#EJ78hSgXq`Iy>;1ob0Q8m44UqYb8UPKD)NLoxVp=K| z7Uc3-EaJTSk0h~mt~-k{LDZPdSWVt?V-|K`wMKnQsrKEw{1Orp?`s7p+eo?fqO$Yz zMNSzUGAr<=fB*j7x%OYzbs+q^4J5ROW_4KsaMP8#gXKD@QD2vc8xe@h$&)9wXu0JM zCA9YOukdenwzvInHvg8;hRL6W9j%3ymKrB%J-@ZJrAucSk6?LY4N|{$H6524`ptSQ zh;7E3IEUA;o7JQEWk*2)r+u-+4#+>f(wj~r5Hn&`Ebu>HovYnfPN?1A-+ur*N||zO z2)1i;X=&-V&rN$$m$mstn?L4oH5TJQRyJ&@WP${7~IPT zEdhK&07zNRa1Y`bBSbl8nQ1a_!DE9Scn7S>k z-0m35j8Be=2*GFe_bEF9iT0Jp|Gu3`ZyM(5zswD9=I7y2VOT#~JmFmVv9i)jmYxCk zpZiItYJ8b<^}gWe$ItL1G*FqCm-ZBx5-V0%F4mvn_#mhrJ^6t2pK?c zzF}{UrbT})EZ@hEA8nFtV?`6WA+mJ+`7^n3fd3nQnN0fcw{PFlgZaEZ&%E(*IQ_cr zM+ezdz$q5bt6Vn(3e4+AiF-nGFr-x5&~U*6$eV;M$D|280~Jit^&y7}zBqPyAK>xr zcM@7N{mj@NIXH-ZUi;3@ow_oYMzKMNVIT27SSo5_B6-lq=;q^dutiKh@w{zDH*VY@ z={yX*5?F$?Q368D&CiF?W%iEWDZ=n@&@>y1CS+e+gXCl3Cou=S_s^qz<;$V`WSCkQ zie8xS{;V z>h`je4GUYxjvqgs8`4$Hbp}$xz468z;@#AuQ36%IYmb3FEAW^NbR2FEIudSPIjwY_ zKwCKpd3}N9dezIuCs*K~FI(`d6BUCZn7}Wmv3=;o*3~alA$CzHE2;D+aF?0MA92kg7n0GpnUsqA?S28-_paQIwG?Ftg@x68_(A4H z1IPbvrI9HVcBXlJxt?$&mbKxRNC-CC0KF_#;OC-hQmxh2{NG+1PF+c2ILR}khaAxb z_yN|-t5T-h9M=vM-tCL$)uV^or58tc0PmdJAsGCkhhzOdk7V~{QaMR^MTHc@I5o~F zL09U9FEa(^SeZ?8#Flj3W~P|mnc*zTHp2B!Nhv96H(yA+`eEDe-T;8z-Q8{eI^PwT zz%VwE(BZbb>in(Pveh7z*_6agK=p;%tD=hXa^^ha>TO!)fqgi30WInv2)$-KdGd1V zT+C%3b;w7ew>vFA)YbWSX1#d5Hr_Z7(;t$ZogMLpht-`Pa^kH4?EA+H$&Hbw-FIb>#e)zvb&-AYM%IUB;Uqhb%ScqW zhq6Yvl`^Y)u6BPucd#_%4C5EG-=NE07eZcg!;<5E{=6w~s1|2$NE4i)5T00E)YlXM z)D0j~P(Wa0v_62KM52iK{pS z-rk<4>%-!QlRbzq@y#|n{O12;4}*dr(Jo4cbA<~c87&i#Lao9#Z{8%MpCAcMHA_oN zx^(X_PIZ8qB=ht0Y@wz4u8lq{4G&ld^y(~#_jnKsSGUv^#1CXZ3!iR_W)fW;9XB@* z50-~a1L;6@bm)Psu)uaxu(!{*a`FFRvHs$oz2$=Yc4EHyak?eSi?}&_WU()3NIkmlIl*04GTr;*1Atxc9}X2i#Sph6&1dmwlW zxh5LNe`cXms+{I+M#jYcZ6OOAc~Ou!APwkYAdVYu@(D!fIoB()2JMxn<9<+@EFdh^ zyU{k@EiN;^Qsp~AwKF%W@zfdyUCc40Sf(>BpM-)e&<&>hd3=w(jg6BR2+NB|3pepz z#l>Aqph=d^yVhKB5C|vSSsJN5(2@Oz77x2e<<$SNudLUk+8}(pOUJ-q-W`Bejaymm zZV-nY4%09hFKTRX>*YbN!$tkW8?Ih0+FDExJKo4PZ-pIiptyehdRCcD@aZ*nNxRSb zaEHJB{U=ZT^u=6%@jj*Q`4k^NBEUjT79LW13xQ=}YU=Y(ockw#53vE^VDa7c;P0E^ zbP5609xMKDgbwQ3dQhOOM#B0%=cF~#*VljbW8qE|?~V;Sy#*B|?g!9o5x$@Pvu|`5 zHX$H)+Wg5^cb)rTLEKnD6QBTeq{?{EXwt@XmkN=qZFWvPk+L7So2!&O2mH~raU|e; zfjM59D_UxSp2dO&Zulx8q1X0rd`?an2qx4R^z(Ro&aHaaDqg?dcs9LlB7vV$=c+mxB`);Di4&BaHm*38!MKWul<;un zj9+%l1bX_VfwF}Y`4edI0T5tDPj?BR%z6V~ozrz_FVA=VJnV3am0I-F&#V3;Ix6E6 zF-T%!PL4&z;H3Zq7NyR@sL#N(eyPEI?xT1n6TW^`Y=7Z-Ppk0LCkJ&2w!e7KF%|xQ zYo1p#`%_eoO$X9;eM|5BXc!G6;6U{OEw05TwX;8fOXIEw(;SbEihA>R-a=DJiyr{M zxYm1%LF8Yurln=3{NRP}W^yWsnlK#&lSRpjyFiBS#P;Tb6kd)*#&j`$7?4zg>HGOm*_AdI*88H(eVPS&z{023myV+|-Ouq^Oe zD(m&n^0Pp`mkch250F*U#j61P(%Af+HiC;2vg!mU$$b+AdWMIG72f;x*#CJnMC6>P z^hIWIfNz_6cY4&S%h92io0HS(8D9H+i3MGV3hd$G;cmvg+FDwjG4||w7#rgF+g5gf zTxuc_vmvswvLh<|#U@%zXW}rp+G(QNp5y0=_(nuTr0{ip&@6F5-Mo1-os*MONuOMb zJgq|z$aNXE|MW&5*?(@9S6(iu_#R)FOi$SW3Pi8vmBS8ZSq%-1H}dX_eT7_WsMWV_ z7drNJA$kVKFMWW#O|oK;^QYwYZK1pAI%;o^!vVNwXgK)cMm41Iz2tT$Ao$0!3kPV3}>Nzhd~dRrl}TW35(F3_j|* zx(TP0!g>7QEq_iqL!i?qiFM~g@={g!yWT7LxXdjqoP$(wvCXwZyjuDV=t51gvGj7* zEF`lEe|Lo)B>6{&hw*1>f04p*sDCPS@LuT!zcKe!vo+XtcOj*sqY0kC4rroa7Cy%> zwU_!GY%hdfkiQgRY!k&TUw5PRGvX-|aCAt@Yaizg;q-~Z+Yeb;Sw`WK)*!*<@ODko z@io$jKHcO&iDyH(q@s1|*u0k4xg<4{uHxFe5VJmmB*e}0(|0$`*v7+eiy#Bp53 zaJ2_FUzcRIH9oVxzWzq8q&BE_n+%rR7nPAVa!Q`|x$^e(UQ?wsP5SgIFw3kTR@(DvF1J9#lVLWg#LLQ_K1E z>C-cnL7m1tJ|e6~Y6##H4p!CAICjU&o*E2H&<^bcQ9P9K`dIC=__U%!^@{qx7> zN}I`lGrc@@zcjT8INu9fpLCJ%Kup8?^iCVuSO-dlp(S_Q5&CnWN!QiY`Gj5=6-y~b zR)e5y^ep2(Q`=A2}RD)*Y@m->BN*fp*WhN!H8e=&X)A;U}Hh3 zl9VgAx^E?hVIZwDy90Q;5i(-(gR6;t&?AfwE15_k*l5E;xMD2#%wtm8po2aiBSW7PAnXl60t3Rz^Ld!6B#cDNlL-$ zyVtjQ=nDJoB(u009rD_53r%+BwA)C`FV4^Jzs$+GSw53W1;47Yoc0712Z;qy5R78%joxy?(BA)T?+@A0 zD|@7)3W&=fayBT_4s|t0$quoyDCu{VIT0-@;p*YhYdsi$uNld@!$hj>Bk&Z{@a*0v zqsky9WJdUo8&xg?~}RyiPm@T$K&^Y8fvGZQ$e zDjn?dwlWN%5)V5~935A(LW;SMtb-ACSRu~K&K5@DeMpq{9H{$mC{PW@;6i30;}ffgLa^sQT+JmAZ#3R( z60Au7qpiYGzGvuMB1^{FM$4V|1FDq6;RroFD4jp-2K%5GQUTh!$a8`|di9sg3boZB zGcKur$=$f18ji3=R`=lGppViAdz;79;)&dHdU0@!`Bl4Kj;v%TWkD@vqurpai2(=D zdY_7C@=|T?LUCQGU4L=JjlI&V@JuS=GaK(`4KC5l)vgK*ZfA6UEUB(O=r*`ZxaBt8 zmZWV^Mrx+)?YMqlY7q+Puae5&^`39|h_MlcIs5S8!}kT?dRrgSiBK7_cI)oR+2?NT zp{s#0SA(Pchy z!ZIbwRtFz#vrqRH%Jdt}(cIiUImXDv0muPnS65dD?Y_L}St|J|15n%9)zuZjJoCtp zx{%8{MqwL5ikbwQh+tR1N`NwpL;Zi|gdcYnkFLw#sdRCE1lwc6zO5QUFt6 z@a7u2|2o?2f`9#Jp+SF=Nl0nl(G4h9M?^-Z_NL1sB^Pt5OrV~zu4a<8b-p1&DD3l0 zB841v+}!N!EFTQiUh;;^BOSchC1&1qsS(c!!lOW`KvZKs68y5v}ne=M39yp$=SYz(I2LA{TEu1T;5;Qt|;nY~FFFK7ByHi;w)p z<1#M8oHZAg6YE)Qh#4g@om<&4>x~7CF5jSS;fjpR!X49s)2c5dE(bA{KEX6#f3~&` zD>I=USm>MFB`^5&sT|_)kji`f5D$H9_uQYQ#JkAFPVeMlyS_{BtE+vwADFOEU=FeS z{7AY`dUP@cU?L?39i&{N`9bM&yqKv8aD5-Zmo!-^6oVqh^GhkNOBvNhiKDHS|CJX#-^NQsLVUf9|)xNmsuhO+W) z0VV}LyV__dd_p*Pe}8aABPkS#Nb*1Z455$TIvfVx5sb0q9!H(JMv3b zlLtSFQ??CAYNrYMKEwp+W~)1@t?gre-QpQJu4vPwUppWW%TQ5KktjEj`vPsCgp3s| zLXj#2dc^8{s!{YF;VuvamMt5WhKNP?>h6%8`@Ju~b(9CGBGs>2xZEdVD2&>7oJ5im zRI_J|X8X5c=$tHnMo8E;DoLC)8+3Wqe`LvQ0J!#LG*GDm?bP8Y$Q|E`urODwpZ1%^2_m ziK5qGHfF}Sj63%PtRV!uQTl2{wrD=p4Clm_L^4&yTTzv8X ziXc>PD1Z0&R|;%hG61ufsz98##PO4GU>MScoK?eFettJEN-P0+1#|?ZXi^*u{H>V# zMMr!4VNgzI@FP$fT>jH*b21R3EFg1Gc zloJ;hS9srXC~v#5I^Njv4tAH^oqZowKfhYXPb;<#4%@_(9uha%-czUJRNHSIzitJO ze-=xLXzlm!g2G?HFjqlHvw2J!ikPkSQJC1;E~LnyFxTid7iXvo<}`MtVQ+PIcb9~= zd}c!;_*Grx`kNn|w>CB&W`kYUPtkA-tbT_1gJig9Tku7_URZ(Go)6g^eVwSn517XQ znD|(_=dc?12;T9-hKP&69K7JHZ0FKr$U~gylEw(}6f2(sQcFhuZ z{oU`B1OKfQg5c{T{b(-P{dle=k_OAX0*JnO>wCT>0z;5ZsC2OQx$!fE{+ZvCi;;uj zLIwMojYQR(mR-zSD85^MTIuvTO2g*`({lUkMsK6WF7))GpD2{IOZv3udtP;Dxg-Pu zf76b;3-%R;Aru4P)-+KItX|OiplW1;kbRq!PQf%=CIHkHD_L)uSd}(!k>9T&6s#m- z&t(MCFT}hmoH!X=7*Q{Sy?wOb2h8?6XNlCF6^Qh2aI>*>lkoa%=&ce(K zNqZjPZyVd#I~&OwB=IO8B}`3;qkQ@D<+G3ob>z-H(f~V62XKm(z#(oAAqBS*8CL|E znSP#_6fDx4$`ij^{#{KMdeB**3JKnm?Cdj*9?1U-nnx{7Z-HjDeI&(!R9Hj?x{jS7 z?*BmakuhKbN$h#rQ!eTaFt|j3(z(DQ6VKv|)#|9q9+iZ<>W} z_mO|JoH-N?rf;yc2Ph#=xb@k{vH_cJ`r1YWBHd7ly#F*MkvfS(F#ghP8V2l zUp{^Q7dHKeAn9yb2mZ+(d?KhIzwYXsPHvyp+m$JMWxyuEK@+gca=;Eg~K)==4T z*gKaBy*%qj9raEqq({rU5)Ba2P-0349Glc4!HG~D3vM*ozQo6jd?>F60Oj1kj~Eq*XuXF(W<#wHg8q5jsh14Vb%&Koz7NZv5sd#Kk4ynTA7 zP=yQ9=kxtR5e2;#gvZKQDkM}KjWmS!E^N`$H%+Wj#1{4P@ZF+q9D=7v!oTk_&%q^o zo;Q7Yc{vmN-H4*^_QdjVHSH|k1QCziswKJ$1$hA{OnA-r@oic+#;7O}YtX#gvA#c% zk{vrRSn~4D&(7+Z-xXezeg6FU_bq=hWLFxByMh-!9OG$Z0LRB2+dn>EiI22;UQ(1G ztlxDL`)6RFDygN<;+sRoga=)Nf}NkHFted1_74Q}TNZZpn*0ou86#BF&%?#6!Noe1 zg@6U&`So`(w~dU9tT)}UFEOOzBjkf?XfXeRk&@%mK3+-dTWvFD^_0`(#vXNk!=(f` zxN8u1KOdu_3is^cLlAt?(GoDDd-(=Bcfci=y6?tkhH2n%dN6vmC47dyjNY>c^Y&IH3oi7%Bzl_&!j&7+t8rET_BA# z=q*;2K&toZ>c}XyZ5l#}=)&GU0%iof zF`kqoHS3!T6qjPUKdu7qxxdrXCvS+e8bSU)E2xM@%uZjrbV>H=h%S>7d-P6Fv?97e zONeQmZY|lvTFM8?4@)}-x!Uz)$UZ&h3sa_(m&m@lKwLtEzviyA(cmq*nlI(0hKOl5QEM zMR%^`k62niK-SCkC=?RGax5svtGPiN4y*#b_z|ouQ*^Two&L%hq+y<4wb=I*M+)W_ zx5HJAq<7Tf2x~`e*6}%Rlt}ru1FGe*7aS7ym(oO=m19`I@>Q46&v*SN3 z82FH9@N-ygLHFum=kNdoC&WP`Gwk5eGQSYu#4jEYkDr=2PT632kDG>cfik#pgjguf zjYf~Dm=Pzgi%1M8E+G&z@1X-T`Qua%e=$a3BuQ?K`g#`cN2bZ*B_t)|H=;-zRGu~k z*E;bRdg<*8e2W`h5j$P%z}xVj1J-((3AwI+cofa|ME%b(d9y*j>1fLbV+fW{4Yw&&|ECscM{UmyvkJ|sCb^|8XeX(pbkbBQ*}JV zLl`m315H)!83x?oq)o^kY`tsJ9NSe!pC06{kdPJfs=g35KDMlh@hX?2fFfGqnLW--mguwz$*UzzG(LbTHoIr@1Iu_k>7;7B zWorVuDfX?I&I^8hxj9Qp1s=N9ji{!d@GfuD_nU&9_=WT$U!@&dgRrGkV_taSkk@$( za>N>QaVieVWA_5x#JW{)LwM|vVLo>q-GI_Sr#$Do_T`b&u@MRV_#(cie66E6&xXhV zHqqy1r$rOOzm7Yx(|;nt>d}5lo(e#D74ECzu+j`nc|WF{DaW0pHJ%-5K0E42wuIk( zQN<;UtK2Tkfr9@R8M2Yzg!V7RQamP33}ZKlKWE_Z-_V*R5Hku^4fLV3(ieGBz3Yz% z73&mcXEy`(wYQ>ZJ@h_7Vlx2t9%~*Pc}3tVRwepJ9Cq7|t6#=dSoV}j z-pX$h9?lXDVJLb~i|Y@fVu=D*Y-zt;{4d}B`fC8lBJ`=#oU}N2i>!Bqjo>8~4ou*+ zYdSi`-b%N(9i7I%k_wZMwJH$msqx(Zf4ABEf84*}(IZ>ir5bbO_Z^rcz^n#p><9|y zMXY*%A?L1%_257Z;7`u&wOPw`H^|T+NcTXo<*lZ0vAW%znhCfLV0dY(*E9eMGZZ!E zs4+@DL_Dh+EJPzupQ343q>)nB>9&(zzP^WFJ?amJq2U5>`pT_%;i7Y4yz*Orv_~g7 zsc=1VJCz+GP+2oBd-N(Aj#G^p2C;HGSgI&sz+6+~XNZG){~p-rkPpz)Vw!V2ns%&f zmSIK^?aNMM4Fzyvn%8uaBOHGPTBvQ~gP`#v;HZP(b$Ue)LDb%5Wi_AsugyAOH#lHd z7@B7`SKA%^UBZQ1e}K@oqM)Qis(k&6^Xe^H?h;VH+`!AKigktRcp$Xn{BDfmlrdpa z>NGJh7}zWWORfg=0U=IP8+*^dYrLq%q;yY52frat*l4dYNB<7&m|@gNldCn-Yv80r^9Wr{dMC03DX@Eh;Si6|Lf_&J#PjiA1~G2MO&ZiTS^0@e?t z%+DD5C&1?of8j9nxhT=Y2=N9lO);>N+HU28go7eE@)$Myu=kU(yawl16@Gj0u?C`t?m-%YB^`$PZegLdhL_^0kZRYTmX?sO zGNOpA?A~M?Asnx-qchbCy*0E>8VI6y?A%e~BLo@|mTzlm(ThHd>D@m#2#3%E?#klq z0`P-d;9z9PbG=IE%miZ$$d;SkngQX&(NYB|eg&o-4Ws^9oC2*YUduz3*T0z2{SceH zq=o|3(;f^f3V^9vOrd`2bQmM#)C!dBv%%H%H5z&`P@vpFEXH#CZFa&gCqaG>e+eWf zq{^a-=y7R;!x>ZXKjY~8+!a(`h-PY#zm=v#Cd-KCTu6h2a*vv)uMsvi39iRi`j7~S z6Y^c;dJKmhgu{-V)u`hNVu@&^YWz9-Kaf>nMtQduz>X1A^8hX0H8y7U0OpQBoT65u zFP&JURF@jwOkgF&`37N-*__-@RiPn}0-fI?5M4;8-aJOJ&VeC**A&}5Xa);??AWnG zTG`FtoGI5~aI92#(3n%DU}BparSK{1}q=JT>@>GIHkz3TlmO zqn^#sXcXEn>Ml$V=FhL8FJ3T2&}Bj;`m444LBG(7w1WJ`=_VNbJr2^3`T1o9#?{QU zBwiFOo>b<+g!lId<^aP`>WPg0SyQ)>x^j4w1SbpHTsAL)OQw$K)u#6=HH8(WqnABg zZKT}@i%fw2ruZTELHl0#J48al<%{;bb_a3Gykp)k1+Sn}KO8Vdz$tv@J<_|{dTvZ( z;LmcD1|R(d{1l0Vrjj<`ohet^l3pWVSy;1p{P;Wr!fR0HTX{oAXX}m6^22FPs@VeA zf{+JnFSk8JazByo&g<4!nds<1$0ar2cVusDBaPr|{iuI5N%OK;{o%U&T2v?X&0Dvc zh|q#)1ee4cSG(g`Gmr$??LKo_Xbf}gFV1}GA^cXv!j-w~VfP0SY_|@9RkHW;g$vG6 zH;k+{GPv=x=RmktAKmY%@F#o?qS9l$Pp>JrHyNyh(yn)CDntD1<@v#zN|Ym^n^7Si z$PCPU5aY#QY3(#meO6X8tjozgh@ra$qF* z-T`wT01YMbUuC$EmM&iwDaq5Tvl-D2W1NDT>{1TGRs=a(`9ml970(8}cehofp~_mc z_rl^_9w~@*+;YM-us-ttIB5}!IkBUEVxLm&kL0&(P4V-(urzTA>+8;I(CSqH1m_Jv z0o|LU{5dfAZ}FAnlN5Fd5j)PLLC3@Dh^5!8)7gKVf?}Fi z*S_`k*Ej3dt-o>uNS2!(ps~r+!a*Su5_)H#*bpTqfb@t02r4|!$<4JM{wJ~V<9m)J zj@y5C++UIbB-cwurlu=noQ$jVk;rz8b z*Qbm$FJ-foMY2mVZ$$mIAvTc+Fodpw_Zn<4`%>@SjUxWeCN);Q?Exvhd}ymmf}TP`py;9 ztY`cY#NW01xB(4T++Y@oH(1;lUh&`g@JBE;>y7t(G6be0Zyz5&@C;ioh-GIWd;16L z#6?y~MSCvfSC}|e7E&WTGuRBRnLmI2tQxtzUR)qrD`8(*GT`h7%Y$^b)@$maT^8_r zV=I(~c?HDg=#EzsCH`?>x=2n=rt)mgDfZWhA=~CfbXq~1Tl`0F-Fx>0{8xNN4@3_7?In|4pmtn80M0|wk)kJ;0n4elWjMuO zL0SX?#Nj`dde7vd*g3HzHG%##z)7fFf_t#1@A)3;Kxz4#jFF&>LXNX{Je!hUz?YnZWKm99hAS1hiKe^Fb0D zicdPN0Pc(9hrDQ<5@_SD`Y~=C>`A7hRaPW(X%)(5snrpdsiN?}MVm>YADw7{28v!IP5c zLSQX|k&(YV!G*AU1buw02RkwU@t*JI!ZE5(f$C}xJwxFK0L2z)7Vv#)v)p+R@?$u; zd$fdc`9pg-&g5L7u(qSNPwZe(v)xs~82n5&y_-1tJ(7J8rWInwH^osI35m@*`txg> zTLl4lZHSf&u$b`jCHIH8568&mr#aaZJ6t+scU;mddr^h_#qZzqdk@-IG(|>ga4RV( zDS~dRgH6b~tcU*n`*-2bg)IZCSaF44KKpZ7^{=k9$&ZRp^F?31&>&+XTQqOAIQley zc?1e_^=nbb+aA6{8!Yaq04steCdAVkPKAYs%R4`^nHPQM%Ql6y*fupaeT7CmdFfFO zt4HWn>wsFTAG06luq{+k)WUd`mw<_{mR`Q@s`HaZ%>ruj<)&mQpV94}H z^VPWSxzagydhP(i4*ZWuJb+Bwbo{Dj#N7`dRCf3+ zcPoF(WKq(RyGJc-DcImV!3Ud~NC=G~>`f}VBxGMt-t%NId!(wpy-_SrSPhmxq+Z!= zIE3tR*e)cX&i~uWaDIo~#1&r#n_lv*E6ZQ)T``I*dJrPfT!n2=r++WOrL-`;iM;p%e8WpE9X(edzjb{O2Ey zR52wrViSFcxrN0f_29hJ#@8BXX(oJ^QEm|lU($x|P;wfM-AgNX(F@Lr=Ir*FEy)~G(ZvobMyc@rnet4B~74UK-<6_16%>C;6gX;QeI_Akjn2~P) zX>Zi64fDYV0DX^88MKwd&uNOCKdYP{k}kcHbP7O6=esh-;5s^{xg6MoeNh1vvAl|S z9KaPf4m~Y?TF~rTK9e@P-qZ?x!{Dbp7%1@H%8k2f{%mM7dGv6a97|O|Vg}Q;42{WR zvurPE_*GwT43Y4rM@2=MnDe3Y!6~ZE6&(zl$C1D4kpIQ#4i;;M08C( zC&M6fsvaHdZC1YK|T@KFSx1iW14V8ziIH(_nZ ztGSL9x(M4UtQT%9TGtVIDijlH^u?)Tr0Q0K#9Z6Uan_=X2Shh}{uHN=B*64WSLUsZ zlGE~wiras*HT~i|Qep;)-Y~+u9IBO1_odYS7w3AQo!qne%_qS$-}M5kTd;p$s_=h@ z+Q%@ArtSVQ9?h+t9aGhp{(Gk1txBdgM@AeW%Zmn!wOHqn`~j{1YM|pu_7juI(7Ulb zgPqGKF{2_&)yUcbuxn$3cBkh;VJ;?AFz3)CSzTJn1paPV!QiW0w9s$v%ij5Knh+X- zxz^Hus+Jh5q22P3@PTW)@uS`$Ih!}Yvnl^g`7c)ZRp6xf^4^8^P&onBt`yQ@y?@={ zi#;#O`?EoMW5iSA6_kz|_1g`_{{L}fEY50jUyzyFi}-9@AtBfGo1K6bIA zilbuxNpR93lr2?3EAZD4){+wxn4h83n$FNLG5UjmQZMZ9*SEoNy(D-$JV0c z88<`@&0XSQUbDzJmKR+HRy5c*n>{xXmL(PZTdKE)@aa$^iR-Wm27cIiEti zD0BE_!ff=>>nXzGP~`>-b`Fkt1}Qky@$dx)W^KXr zoSa_=E=5{TKz7%uypW0FCN|D*K_63?&EpLMAUN2i0o^Buj$cR(3cz9^zat#_O{72d z-GO%dTGi6eLFMqoMR%X^fWv6QngE`+bwu&jlfDiPYO(~d$nqnI6}Y8dy63O|8Gj6< zkJ$13M1d#k$_9ABzET}0c6q|VsMFTh4}HcW!3g9N#G%IUC0jW8V40S&nAS$){p-bn z1Y3MMgXq*=-8$9kTwGDn+bmh{& z>U}qXT~mqnpjCRokikH_9{B}E{cSJ{nAHWAhI|N^CN|zeb5{Q3A5v~+W}@hA>3)%5 z#%g@<*0u2WS8)9czJzalc&reaMqTH5#Ur1szq)CB21e)xW)gP~k5LeOC;p06wPlvD zCK{WA&3hmt0Q;64zEl7@G*ywFoowenW~6w$6~6i)?+=M_N(<7ZzuWwr5U>S|X{x0~ zMdQ#Y#t@oJU5Gt_6@4H75hDavp)!15N(^LIf{r`ifbyd~k>~9(x3)j`TY(gHJMl5QF)OL-brY@^a`0KB=v3aLMIg%D%2CcHGFmc_mp<- z$0ovKb)H4s#GxL){TwnOa^P}SJqNHwFl*%UyI*=E!;a@Yg!bkRx=d3#n-9dL+89uN zSqtS%wU;B8#sf>Fpr?WdX7%fmyR|PuH{p6kT|R2(_jOvo8^&W1BwK-j0kE4qR*5l| zJTihd&$>B^Q`4 z$bhHGfEV8I1G6?A7WCI8cbUxy@=f>{gr*Sya4T@yi*ff=B-=cQGTp$950U@=Js6kA zXAw3#=E)5YrVmWEcc6n`Gr&^K)yy)93G<*&sDb!Z1GkGhWNOwJoQ?NDMs6-uGkS)n z`|#^E_j?56cFyHoq$ph;fPhh5K9-om z?DkAfzCVw8I%Ro>PUsb=3&0|_2sZ3G)4(5l=tT|{z8uxn*7`-#KYmgUOY{VA&`g{) zuhr@(t)+(YKP~9N-E*6wrMuCy&gl0-VVj^q9G=YhN8$;7_9$q@#FY#Sr>3S3f_LPM z@;E3%2X-X998@X9&iE%_@Rzi{K#-0bcIfcjR1kySC9iIEDZFKsCV`upKv-^KG3B^; zRv9pHLdW#RhTxksw61P$`Mf`UkJ4%# zh^EKVh+9Y7PK=HYqo?(eSWS&y~uKeu(x+55Y{!)JKE-aEJST#hA#yd6P}e`vQ~00-#%nAd19 z-1LGyOTdYj)B(TGBPgLV<_Fv}#x-w8N-tmKt{S77h@F&7-_<%|&!k@a6Ox*E_VPBf z{Vu`1x@$`yx?7EW`-NGYxvcBd1gc`0QI20yy@g;bfp0o~>{zz`p;NwJc0-Gl;4dyP zFk=SvTPpgLCQx zM62QF1=oDTc*6ClEHf)hKPV&N+u7$lv{EgA%+~uMr2n`yy}Z<&BFrsTTt=RD`*t`Q zS6>~oIZdU0fm;Zx>)z2F>o_eoj0C&MyM0%wVG&ntg7Q)Ct`!5>v4W@>8iYeC=LG#_^`DzHQrshW$ZA+YUs%Ghm1>*r!WhU;m3NT@|$J-!G&{NhMIY3pyc6V0ZK<{;11J>a$U7)(+I0MfY>zRS?U3!Ox2*|dsnuRcnYsN2 z3V#SyHq~S?K8hr-iz9RA^cN2uKOofSeu=wff>qK8PLr#s;aoX438#-WadnO}9BQ3c zXhK*edB46sw>3d^G|`dmdYa3@5WTEkk(?}F@hGn_OI-gq?pyq!!$U`o!p7L$kXO2MF}-&=I5>Ektn|k! zOHgP&$i*vga<7%CR7`N#zlg};b<^go^9o+fw3XPQHp>v(lb$`Zr^)6q)2yBzj@gmv z>+T+IAxbldU24-C06wzNhXY9<(_L7X3zu~srX%-*(h!N7zDF1{bqfVaj)KEVQ~!>6 zHn2P+QaczG6=hG-V7@+LanTi(0Rr=e0dq}7%^EvslVe~F^x{W(i7cU1Kx9LhFv+co zxmTb0^1E%uC`Ds0c?mSmJ-fr|{Zz4IM{G%NkExDMQ%*pZ*%_P^^OHBZY*aW!cbwms zn1$FCcjXmgI`&Mu^A2(JnHXF3sa`2N)>XP;=CBf4OZN(yHa6)~Ux=${(&?CWgUwa< zXnSTA|8F)TD2P1DweDO^+JdPh$h^xENpUBrrfXJ-dPW%vQ))@wbBjv{x}1Ya2F2aj z(6G)y%0+X?edkVp#poEgPc@;IS_IxO#kF4b zdoiqe(GnI;lBGPmSWT!o+&lG2=JQxNyZmQSGJ4~2}|p4u!h}TH@AHpk?Q{j z5y_Z-buwI#*Z_5D)4iUA*jUfdJnGvsoll{O>89f7r6hwAS>_oztH?jI?U=Ps-y4_& zHLbH{FCGOb$z?RPibzbus%*BW7)d~J+VuY9s3&BCXPVVF*OTi!1DSIurQKmrKZT+E z`LOuJA0r^pV)gG~S9Oc~bkp9pP)}@-kkz;B&UnlI!}16_kP~=KWM!S%NpU>?qGobP zCv&cw&rs`}Wt|X%s^{cj!()=>E)@}lOGTwziXV(L2BTgc48PmFOP@l)I#6+W!dQU$ zsY31v$W~X-vsOVj)@r9>y8|NN(qxP5ot6E+1Sg<6zhde3^+Rw1){W7ifGzqplfghH#Sg72G(f~}s`)!_geFIZW4 z?@i1yYdnFoYlA?@kg3z&5Iq`hx9|K8I`I#I9m4_Y(!`*Q?kCP4M08Q@Cp`-^wuoBW zl{`Wrej#|Ofx&46{o7V5xC7FC4>1Kf&tl9oG}i79PkUwgQuYRBej-w7BX-#pxMNAz z>}Ycf3k{w=2YT3lh6;CvT6Cti=wPnaIf;wXvPeUmyZf5lJEeFUKqZbh#}~ALRp2t} z-_LgNJ3`yVc74{16pl9q_U`|_@mf<-79;7X#Ae4?n(AVmG>X*I?p*4APT(z_U3aCS-}&RZLt zp~?7J_5vvL2(%&fkg=-91YWdb-mKe0xRz@1y{99K3C9 zC9@c#*5RUz;DiXDCrIfJ7t}HWy5S*BiBjw*CT{^ZH^C{=&I7v63oaqZo1v$SYkYO+ zd19iosL@ob(4T@Q2f$Ta5)bMyy8v6iE@OlwCW1P(Tswd`|Ad99X-oK_FC_l1rlLas zl?6lK=q^Pg8cxsy;nq~Ox@EH>i!_O9OE-t6h|Zk<;q zc{kw)(s8zEQLgRNnm=IvYuslT8I820`DrxtjnFWcUz=wRuTE~4k>b^Bc&?`@F2lG@ z;PXE?wSq{^o@Arr%<>f;qDFJ%8pqm7U<>nGlIQDeU>jf}ML&Wm__k#%%`U#E=tR>P ztu2h;K2!$P;novRSGL2Vg~?J;5SFATHR=8=v`I52w{bWz9lYDQ+6gWQim*yEC5=*G?8eIPJ4)t!f#>oje`UOc^~3;@K%iY4eZr`XN5+7* z0S6`LC_{apdjou==Ij;px?iZWc$%{>lhzBJz-0X|gHR*g2eFN#7^nm9zVt3m+5O=gp71BtX{f2W z{FwE7$zpSo#NnX=maL57BQnhv{+K{;!H+1IJS+BgP&IANo;};r!N3$q8-&KzT!sO1 z9mV@{LhP(+ZKf;d%7fS5h?rgUNA1N{&B*mG(Q@XeI1d~`bl5pL4+F9(Nc2>o8Fyr# zEmG@HN3EiGV8Y~#QT84PvPOR2Yi(^Eai^*{F=6`ZN%0(mv)-kwm_}K1g(c~z*yZjV zkB#^)N6!dTsvI_G({V+xLpy8ze0oPB(9Gs+-i{PsAS^X+xncNcbPI zu)LGyn$ver$KJ)Ei4$Q6I<0IF4*R_j3<3Q64c4F72)eS%rjTD1MP7~Un?9tRQxR|;Gl3bSA@#B$ZQ2pyYJ0_0BQ zZ@Lk0vBnTt4YnQds??_EhcY9xr1yvSJFdxq$;%pcRwK6Jr{p)v3rvuBLbB_ti@BuB zq~6mSsTZGEXxKf}1&H0XzO#Mzxwwb2BkpTwrjSOp&s~DzU(TwbP`lku_zK}*KOB9d z857=Dada^yDTyEW_nZ>X=`9POF>arQ^VWLpx?eV}!GE~<3=W08umYX_1_`4s7G_H; zbNvdyHWr0`K791qI^ykYfAlw~Wl2xT(8D|>jJoA=e+2k!W$n?MIo(-)rE| zfbEvk8ZJ53e}K(lsI|>A*|7E4oD{pYapR6!bo3J{x&m0)2+3|pfl0m3@T^#p{Et^A ziK?5~&>+qXlf7^tY|qq>UD0N!NQg*zTqq}7Hx?Ea*-me!473HGZGx zqR|}hx?j=rSZ06jq((u2Yx%>|XXQ;DKM535XZ&{Be)||Y&2u_^=GIDFXaMG#%e_!1A9@l(g zpe|0lab6pB4fy8`kUdVpiCh>EJVpWK&c68-Vc3WifxC@8Ain{^08jIX=H{wPq<1ns z%W$h1v2wOD=&@T%0BHiN7r~O%M!hUwm21H8NaPWm>y#7B)6vp>laexSE#(pt zZ3Cx*L>P6R)1mv9(*Ejjb#~6nefxIX*lIs8iG}2`Wggc5Sme9 zP@~f|Thp#Zl)%6-Bp+MmBmEsTqHFY#TO1jmwq1M)rSU^y_o;Zb0!NK(mWUt54A%bYA$`=T%7p>20jM^ z_ZXqDh}47_Je{{VuadQ?dVUSJky0lMNL+$Sh1``UovbTwWL8_ zkWtH0{|(vgOID}D*fVj73BecGJ%)ycev+rBc?*dwdxZh-Sr{CYRTQ{=AB}I}pF8E> z3E^+w!2E9!23<8>5A2V-9l99bk~+YP{IVwnlMq=HzO>IH98LlyPGq=!WBmYhAYY`9k`{(f z&CSU$cs5T>8NS4?jK_B;!Btrop^(Lq-#V}mal{V2HKU z32aPT?LZD8U*$MCdloDt`l!kbi4vvBm z@YyP&HrC>hh%HMM5uGTdKYDq5czCt3v9WyfDe}u6?!0ud4y7i?Tsjw#3}JUk*4wlL ze#nZ)Q01?UPC=N5^ZujtlfCjtZN`3jue+GR5xJ%L5Ni;Lw>WgERO_dF)uA-4D*pMc z?850zby{TpWaB}A*V;UPWFs{W9_Y>q*8SFo{QwZEb5fVYwJyJt&?gK?XOy>=im2Q; zOQ?3QEQZH`m_*~idoa*At;V(kH&f=@AI>h?d-4)#y{1|>4R`fVW0ML~^~IGOS9~4} zZ#d<7_{Gtepyy;%k1@hn_96Cs=gF=}<()gj5H&le$;;_0{;+SQV_tvvDJ;7^a!86^ z%+tN$R8%xOH{zo0X!Dg(H3vjH4reOU*RLUkWV^K6S15;|#c_+1i>s(+YG`E9 zfApjEi4)>+BfjAkPhWTN9%ujQ?U!ji&Jei^ge`njUGG}`5scq)whSJbOlGEiKi&H0}5yLpa#UsA}A zN$UsYyfnacg>T;qH4}%J@nAAYLmgFD2AczAmhkCx7<+Kc%F`qfBUG7|G*WXMBFI3! zM&d1LOP>o>MOpAoxvA1QA3)Se9zW9L*ZwL-7j5^wb279@AYpX4sIp3T-?9<{llQsz zL4kf5qW;$h7p;@ug!GVo8xIJwlNf0EFOGFC=A_c|NYkf$>+v$)MSwr7Qj7XxZCn2a zn}Lne1EbCRKbtH3Ebse$;ak-@=JK#pSp)t6j(gC}U^54m%u)c*hqOc9E82H5n{FU|d)fwXd5^zqm(_ zkKk;ngS%jnZGSi5rr(sgFazwg+atf~Gxb;BJcgbJA6?}ruYPPeYccFx>Q6uEzVo{m zExbUGfK_R>8*lx0u_%A3^F|H`^*|$=l$<=)oVTFlJbzWt`tyrcU*bVUEL2Lo!kTMt z|Hb20Q+-)iU;Y3ELxsi9`_9I#wkg-U9-p9@fKk~aILmt|<-;^?H-RPa<~CGrv1!3i z;|^(tbjU7t5ACMxa>!-5XIWLe7g#Fk-M8)K%e8;^dWCe?z9L4k;mr-<36 zpFcmgFTgQh=@oJL*^dLn=geowN=**P=ZdjtO%xb$)LSADL`KLDncqF$pZiI}C%mcP z`m)QXpGEBWRK$-5j1p0WS=g36pZV3)J99qguVDd!_+tm_gEcG9<#mEPUg&Lk3J&)# z>HcLS7Ve~f{T*ibnWb|5RBd3Rw~r!ImG(G<$8jf>IDrR58&E06j3D!Iu)3>k|Lvr^%$%*%rpSnvy-=FY ztd0)-oS^LgsN*lM^Qh)2aEpEz^b)=bF9wW?w{-jVm$3f03h)rO`6_5;0}(H~nnHo+ zt&^*3OM$8oEY5t%ZW)L@TBMsfQ+bz#q}1@x(D7YQmyL?kdw<4~ZtxpO8u?oJItv1% z?L%zEF8T_DjFfw|C%+`&bi)c8SG2Epsm-UKC8^5aHP%VE=XC~NEjoAt-5xKgcpSy+ z^#zGETQ-6w`9~*Ke~_|OMaA~>4K4M#;xKVm66t|?;NMWEa8N#qGZ}0x;0=KHx&nm) zz41u!&B!ntLFuf`BE)2=z_*CIcelA zyme{>s^xbeYc)%=tJo|(upB(|`UY#Ni287AW8+`yDk>_cKg~a1w=$#a<@DdPz3;0c z-Gry>Y$5D_m7SekjT|BLbxWy-$yQo}cLZbXCV`OVO*c-?T+XLR*_t&nOnmz*ocB4- zgl3+fos2W@=8po?`O3rAae1W;3+MGKBk`?ww=i6UaX{@0|C*rb(9)BO*jNzXsOd@B z*@L~5Q&d!^^3?hP^Gbw0%Nn|hAd{&N6^-ao^ovuw##h(~z+GMhRzSICM~)PGsywV4 zU=z+^3bs0in~ack7oTK)&rjg}5~5|(g26$R_=D@Mh}1VP^xz~m+^Goe?X?|cW3Afe zIc$6qe?bc6TjmyzFMl5#d|L=Nuj9k5lOJ0IkzarDH6X+p9Z`OZ4tgubR6WI#{c9Wj z2sbmmsn?0H#G5T%>lJv z?r|wyV0{v!yOqhrmp;x`Y=PaG^!&-r?g)T~ZCdb_UuD z?7lYe07%}pns$cPe;GI+WF6*GOKOBS3}r$Bi~j=FM-6baJFcG5E?W((ZZB1=x^fkv zPZXc=Ze3Dl+8@pf-bRM7R5+gYG%#qHQCG~Y5l%C)PmT^T3*Qr8)k}^MbR?(pPxaJ&#e^-+LFoTqz>umQ9kf=5Fbl#35qSlS?t6g7Z1eES=$^>oA(}yckqb|ssUT3E9-DP4SfVdsFEdae7l$Q*Ijhd+r$8D>L_=dqbbp4$j_dM+F zFIiPdoVb?2of_q>Wo>9Tc2qYwb*X|Kdrgmf4tT?C9$RKYF7VwMP}wRxyaei!cB&Pc z><7~)Ypx6a=P}{QM@)I_m7;Hes9tD)xXeMFS#&wEox%j>2;!z+VP%!_J`=t0syQIT zi$fzG)sO*ECK@TDUGPLZig@XUz7uX#ntJ3;X0=0U-R_&6a-}WiP zYm3NUyL6h@uPQNrl+@P#hxV=Npb<$2!)2<*{cKzDq@POUZ#<{~gE>ykyp8aT0 zPx&tm2{E^Cao0j5%~rjl`SZ8z`C&hMLRSo1%qDuK5;5us8yOeyeiyb8*YFOednt?u z{$4sOKw^jM`B#(fgW5cb1cd_R@4I`BG6!*wjtT2sR=jquXuH`I>xY9m8=N-feUF{{SviM!Wz3 literal 0 HcmV?d00001 diff --git a/test/Texture/grass_sway_2.png b/test/Texture/grass_sway_2.png new file mode 100644 index 0000000000000000000000000000000000000000..7b82d9c524d1f9d9d316473c549bfff4f4d16058 GIT binary patch literal 38266 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

o6Bi!32G#=uQ1m=#>EbhMdZC8ITY?Qv0h+B( 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 Ce8j#0 literal 0 HcmV?d00001 diff --git a/test/Texture/grass_sway_3.png b/test/Texture/grass_sway_3.png new file mode 100644 index 0000000000000000000000000000000000000000..0a260d5b2b67068b2e348a44c1c05acbe8d038c7 GIT binary patch literal 18076 zcmbWfc_36@{P2IrzGSVCJ(ZA9k}ZsESwd3Co>19}v1A=2M5OGILAGQ|$gY$ndn(yw zm$GEvW`6JM^L?J*@A>`t%s*z%J@?*o&wZcw`Z~h(bu}31IOz}sVbHv!YJebQ@Rbaq zp@KiFUf;G6MBPSHRoT!hDrV4?Vq(jHx;_P*Q-Z@X<%6Y z*B`BaEytqIQSy*8+fCBoA4{G)7c1wrG*%HDMNU&1OwHu)bNjZ?VH|fDmHaHi%rLsy zBuzY@s@q4!L(ObAhpSrWf~HaUg?MG;=jVU9pR!xMShw>+N6?0nhlyD|NvFwsW7sob zLPoa^j*HBC>2Gmh@QW{f7Aj9V4Myek&TXp-u;65{LMK>hHFJ8`%AK0i(1XEI)QHyC z-rnbl$;q5XT6y&BVkvJ53T!Em_u&HrH|9Tv(4UMmxC@_}oSabSBXu_^kUH;$%9)q1 z3kxko1#X(KZE)S=lGAvdpUuu)J(j) z9``#_@s#bKE6P@(VJZ zIvN_yEm1u3oIz3aap6=*-KHmLVbt$$6X}U2zTxk%sD^XAHlAImWoal`%Hg$!x;nAI z;LDTGH$`p9*!bBZPQ)54w6wI;o0r;McJE#${Od*<+$=XgvU@a7EUQIL_}*<9eJgq$ zK`QRsM9D-+9*k~g_xnX}`IE;`C)ey8vSfJvMHz3v?lMW#b&p+VnUv5qlM@_HC(-ys zHp6XpxtWjD)$bTj#rZM=gU?Y{SLb~3;>Dtec?DtAUL?zmC1!MQ!BdIpyd3`!E^0QV zM6?Z#bq@{sZnU&qvMKIK{|W=T+J#QDKq=)LW5u+t1SNq6Zs|6bRBgEmqzDd6nwy(n z15p}Hh%+d(*fxrkMfRWEaWLvs*3iyE^O;lNoWu#42U)RI8zVk$%FVP7`)fBwUYzPox>-0c*W25B z@DD9gBteGg`4U&sM&m9WKPz3hCw|@ByyFxycHTMR7>wvRYk2#N5^2kPDv}m)p8r{5 zqg355&fe$F19I1vAYMN@GNMb445|D0`26`d)wVcaQ`vuMpv0!xpADCS@CyiVNjta8 zL_M$nUYup1WnSa8j1x<+IgH%nsx<4Y@>(9B8y~7Hp}og_vi|-=lTLTZN8cJUY%8zN zpOU|@xXk-b5B=_f^X(7w-}8yL4WE#y7!q%E9}aRFTyEy|ar8OTNQQqQ@4vgLU6K6W zQ0P#0oWbw055c31a=Hx^|B3Lz&>yLW<%woXIyv3MXci>)77q8$W9my>=5|7B0Wk%x z$<2dE%zIR3XbfC0G?SH+le=Ns`Ck9QZAAZB&P(s#rJkHbc62FKnDHQ1KT=)QqzCmB z*1p-d9X=>>kO5&6ebI44-g9C1SxZ{lZjLY`8QJty6M3NXD>tW^&Q#0shuGL4Dulk= zX|VhemmabDFxq2Ux6;7(;uth?emTr8!UWfGBoX(b!_4@e)%2!K@$*|bPd_^)H-lp& zqgN)!Q^G3zu`pasQlI?MNct>8LzDQTa`JI!<#4qp>9XMS^vb57Zp2D?{ZD`Co4mX{ z()E+1JKPi$?Og?o6e8JrG>&)g{vIv0?|S=BEmZ3wTyQJDemh^heX46tz~k4)kbI&# zg&|eyDbHl1%nm8`K&~FdY9u*1`Oj(7a$n`i?W)Pj-Be@Irdq1@4`yy^Gb8b*P3yY9 zeN*+f9(jL9!i7n3(`sh?bA;*Qxm`<9-e|WXjGIKCSx*s$P26H=dn}mAW19yaRPD%0 zxqa75KexToc-#J~SFhUGccxU1+v{MnWZb6P<6!J1mFN2eR@yWXC@zH{kt04$61q9 z$D>YBV!|#7#5`3MIFmEszm| zLoV{6%%SgFlB7Lj#E`fA*iAWhcV{v;HS~;HpN(Dtw-4l$2OW z`_3BSvlUd>nT*cMR)x0_3Huk)E}iSEs~7I&WCpmryWNu^%V1CT-dRyLQ*YwW+}s1J zmFW%>rHPBS>6nI;zXN3sm+IY8+S2;!Eg1O}ci+~>VB*wf+RfAv)xvtJV$rq{7}V4H znDl4xwmUuk4y-+F8S)hBRU~dstPoWx470bUBRe%yGaJmHgKkxI^+6RCm1s~rmE6Ru zjy)M_5xbA+b5&P5l8i;;^|dO|qn@6grIS={Fd4G_Z*AM&stUv;%`err{3&|YXW{cV zEC2?fG1Km=K4{i=8dVGM?q#@X_D^{5r=)pW;a!U|w&7#@29|J>zV(_aEQ^KQ%F6x` zkZ(x=H>F*Tt6G)uwk0Y@B$(oDP25tE+_|A4^k7Fv$2Sio%jaL~N%=T~*I=GTolU>K zaJu3>_chD&_x!xDdc%cpeSL!oV&>lVVuKj=UfJ!*=X$O(%nM;3Bjx4pjWv)=zl@3> zqWeQe)2@;yd-ZN786M~9dGg=Z?xe4O$RW~Q*#P41G8ktI46>pR5|^qJK-~1zKvvVb zW>%iRp=wB#Aj1z1!0H%Y_VSGZ#k;cQ<6YJeb46~7J)aITyeF=Tx#>3qk_>;0KHt?%+tlBL}@ z4@G=t`zC{2foq}Wh}&Ke4lyw?NqfU`qGmvx@9eDquU~s$sE1{i?QbJX_Nb*cy>WvI z~>td{JAwt)ZobrVaC7Z;yqcN|Ree)2D2uqHQnX#rrJQ zZWMa4A7SQ!FC+Up#vu7#YNK2ZW~@AlS7AHDKub32d0N-e;tIm{(O&VVS|&i^PF_NKj(mv%VfvrN<>p+I)oR|)@|hpWm4BNFvx z;Yo!-GgYf;nR>sfILgDvr}VsQrk0K&EY??1d}3n4UX`t!r#RcqWac~pCQ$F|+`9uz zOp6DFopvnE-@p_u2M7uZ#u_bSuADt{CVjs_Rm%Z+98bQWo%4VmZ*mUVeE#y~OT(*I zdv9dL&VN2rSMF3wDXdQ>?%MKJ)Kqxtyp&YI-?=%>2Q2L9n2BI7ahFBew7r&zma3oj zeXkQsBB9*PN#UPwhWGUJNLl#u{?~J~&d-un*l+&I>9j6ApJiFB;e$98xg7T>asjxl zsbn;OTf@Ccp3zgwvZb$=xng;_Is@?Cp zYBv}?5PDNO($M$GP@d0L`Cw;Q7+{|V!WIUll-ac5Ft`*4YBG$(^4W2`=v^AYa zQzztRFsjC|y8nEMJ2mrolqG)qWskf04C)hAR8*7>(FiI3zdq8xySxo?r;Iww%FCan zu{6_QKQX{!&hDL47f&FW#M{oSY&pQIK2cy!mEr8`JJd0=(pS@d88i;8yNRVGi4{a3 zii&@{+-0=x%B$5U>dv=J;(4Ndw$(9VDP9*A`a4sSJKa-v!o)c7VS`of^O*u8^(1zd zE02SbK!eQ~6}dk5g=1B>5rH?gwF*B52dlL|Rqv6cijVD%OUyCx@XhBrRaE@RaBIiC?KM-i-6Msdwzr=gz-u3Y@sp!8t_PU}%`un$U z@2(3^ZC|mfvA9m78pl@&b5RiM`XV{`$NKtub`8-tF_4NsV*ytEOR#tMxSJEwx4kaB zT$*qT%w0Z}@IPV<`k|sZx=UlT2T}b7@rN@D>Cta-v-kIYM0ZhUG!>bBtt@iDWQ0=j z2mP-lw@lot=PmTzTk8v$nwlZIlI7MqZ-kNC2n`M0IK#C6khwWI&3zl7a__HWs4o06 z5QK07yO&5Nag`*e4v%J*$=!KzX%p=TbWRdUnpBL4}#$c_5Kz@-dK7$KC`dzO|Z zAmKNN_{frE0*;|u3e$6uocWBqyZd!Z%h6x~;u}&QYdA$df-r3+X;)FLuB|B;8yVSe zx$`_c$bkLy#w?yB-*Zk{9;&y}Q>yIN$ zvBwftql2DsNf-Xxxk1CkiKjWj!SVC>_~=eRZRyyjPoJny>ZWjPOmJeqfi=15d-LWv zd97O-UthPBavzNEC_djz!}2HM{-iVUe?iEj3@EVCK(FcY3Cgu@OlF5)k|)F>FRs&8(T zy;h95bn6Rf%7@nwywR69?ZSCB)^Jt3@2{_|*;ewCX^=Cs3|6>|3XSi@WJovCkvEu5 zwFsMY;R>)*nfe-^#>NPXi=;A$syk?qF@~(zLv$f`>WeZRf(sAl`of@!y_Rq0=j7NM zei~)Kug-@Y(}=1$cmBLOWHqcD9d*!EcOs^rdnLG#K|w(wGlE^*?uZKoEKRJDkq?94nT<8NkdKk0eMrVDZrQAaBR_up5L^8HTWi}-fg+lUpRC+@cz_22YuTu)@zj!T z2;vfVDhFLD%@YymG4rVTqsE7R(->Dc4_nC=8AXLZ!p6qR=X^1jWIg~dbn%_AU=Ulxrp+r`C(BG*RYHLa}N zpy6e}h78s`Sh)cUhG|%+Q?UV`^CjtO)%Dqdz%Od#`oGRQ4ZPESRU^4_xEjH{6tUNx zyOHidEyB)#rEG)P>h-mJbv5H;YAa`4fpOX0@3#e?TdNT0npDz*+`*YNH8l=(Z^Lt^z4vwpn>wnnZ*)fISK4E1~30bKuG%a?L9~m7TWyOguF~QZjxVdXg zA3k}2v_}^nsd_{>+k+Sucf62ZWDy>v!XhHU|GPDJd)vD@Ki?sg<{*MWdqXBc9(>?D zzQd0Le$ss(9ZiFwYPXeOx}O+R(Rk$#$!1AYe1WE&ot=K>+>3DK+{-V{7ckx>&{Ssz6PcE*r->x}~~M4{LaxLvj0 z@x-9}XiPX&CCE%EmlM=tZ?26-#3K57dU`iMKR#?4Jb(y6kY|bEMep$ld+Pd|p98(U zjVz;w!!DW!!jy0w3|AG*Wxywr(|DcAO>s4^cnWETy}i9BffR@(jFcb+_T`W@IX{xs zBcPi|)rF?uJ~P1kP9jVMdU{8NAMw&ReEL1#$b~%}gN&Uv zzlwXJY&)iW<>(DlQ=7kk|IVx3MiA&53ubW*Mpgp1k%n`^7`V=hUd}RDgM?2vJnggp z5>I)Cul>OSYeQ~x1ii|m(9pie#qg>EW(P7H7}_<+^^g82Fs)E~_$vS*c9*I3aVAEG zs)e8m^C=BHRKOt~h6{&ahNze#=o%RD$Zk^*`eGV^cCI)eyS~@BD|Ok!Pn!S^hi zkRNL@eq?lxGt+^wy@@=N?A!P6WfVN*_*ufVAr&XmXj^KMpP6ZsNplQAcC#GN^kfs{ z6d>cP)kxlWZ`sStwbCqqf53B`&)cSMOe}kK|01YfkT_7`yY){k&+u-z!IvzU(vx5S zvSMFoWyOnH*7;t6+~hgv)Yso%`Res+k*O-@qqD|{p<fL#QUwt&2Z=_~3ya zk*;O6x_7TwVb-|#-Z_M79=r}p1pl^IN~4cIF`TNw%FoYlBW#JthkY=Yoaw;a{wcW8 ziid}%5f#`k`LjCsUj&;EAD71@3~g*OhX)4a_|d`C;5zPr?z`>i_=?E!I6y|l-NOSs zS6uLX6(u~w6nI~MVJ2WqI5+^pL*E$OsGonm;BL8-JZ}>%KK}Dps|IcBcH#fy1&5?# zB)*w@4u2P5?L3S~N=oW>pF!e|M}pOeE>x3XV!)@vivA(YTDU=~;>WqWpJtF_BRrc; zp}X2xwBJSVHZN~t8py~7B~RlM-2EGsu2cW;Dj1Q+op};c$PEq$mgz@S4cOLO1`yDz zNRtsd+S}VfQuH-HCSI*thxO_q_`H?S_<`)TMei`kQ4yX!l*+*|e3hcLgDKPpm(V+A zX^*p;F682@ohZ2=y@T%IRJ!+yv&JwGU;NlQB=j!UE~^`J9c(bP?#e6fJ3eHV>mssS zX(nb5&-ElM3{(!2952$7<)Ck5)OSfNfqqzva)_4!Z_OJ15E8{Qe)QPekORUA#1JqN z2cX^C$EOq=4QGaVUIfWdS)>FfW&Fp;$cSV2vX#FVo&ypV*OteqnFS%?2Z}{xRz^bU z!leU}5)#D*s-N74yPXe^@yp>(e=Vb(hHL6TplI5CW=NBx;xwg>-9_Cb{TEAjVAagc z&(lvuS$+OkWp_m48cgABal`|W(z(qi@V-(<@ajp{PAY73Y}-^k6%W0>CQdg*cG2C5 zY%CZa&b$gY$CDN73bmA@nKmE;J{5(9Vyi0gc-r5;f4?ayDe;VO6V<`#pn0ayTBMz| z%9*tyzHRDeiLq$wF(r!aRZkGk#G9ZEKQ%C_?YUPH)yS>FZk-i=^5jW6FGeW7YbKRogNOvuR=#pDfY{CcY|;Ta z?)EFQe8=AxR#gpv5ON(l12StpZ_q#?IQgtl)5*yx`1|+o>Cx~Y0&0knApnRtVSD9GNpt8gY%ChPB^>lrjy&WyM*pvY@h&*qV z!2D!}H;#Ikt}K%A$m6gBjEo)>@LFkcVoA?-s;(d1y^S2W0?%~prY?)|+gr5M!3b}6 z6+V?#y=3!9KhPpdqT*boa5h-RP2!!#DAgJIvkedBQd*8XWrj_-v-h<^FBAA$z6qA&cbV|AT3?HP= zxR_wwvce5P&l%a-*#$9@yJ5+#@L-r#A)cmLvF5+gQ-t?_gbDl##ko5!E{p86q}epZ z1EQc;-$6cs9go=`01Pu*NA|!gFYWGIXImxs^@DDmz4MK2Y+Irj4sn*=F^LT!J_@>Y z`SO$dN?9vgyHZ((-PVZuRV_`$w0bri_loV!j>iI=2>I&9hG%%#`$AJ1x%e%KTU%$D zZ)8tBExb}?cXSA*S>M#u;aj1%mT(2CwA!ZL*W+0yU1h7`m+i;-`kUF`-{fXsxd`?3 zBnuYF5zOy!Y`T3>TU$6iaZh zto3$3KkIeRM_oZbLx?S+KU-mQElkbGh&?9=bD~@J)_@Sf*2F`UP4cy~8Vd<~ls&rn z$&U2b&v!u}+Q=CxC6YUj_HrUDk`6}SmY5E?LaEE_^vw}LtiW`t4dcTn;qWkVh2}Kk zocO1uI;oV$1)}Z@CSs1-xH2Z0Rc8cI@lS%@iKpOcym;E=?JLDu4&-@Kii%)gVJ-nf zQyCZ-$Rl9)7elc5UP5ln@sH2{>~!|y*C0l%1RGPq(kGe~uo;<0ohE_o`3_X_E=+cCXyK+ua@)#6_K^?Q9 zd3P{~6g*T`$KhU?7@86Y%vD9=Ze$!7qEHo{$_DKMi#(iQgV9li^UoniLBqe)gxcko zr=<;2|2aoGg7X7|gObY49^+Ey^LfZCH;bY1ha#)vvUn@?kgWpb8GX> zlGlWZBqkm=_+*Bq(8%#NoSRhIZDM`$G{2yrEY{%WK>2{&G}OxT(2=SU$^Wl;ga6-& zRP*!mrnoMJjaL91#Qg;-Kyzbl?KGVnBhqlv;=xp0z2$z%`aa=pfk|ohcASyL)(&9; zWVgk$ZY8x#B+F1Fj!y27;d?`o6I#M+sf)fdrJz`lm~zj>5xcC|?X{wSJ*D6%%ATcp zOVW0LNY=NJTAvrtAQZxbLmDo+<8X$48`Yx}N3bEoKSs-=Da5I`u9=gZF7eQaW;td}tcQO^5eUA`s z@?7v*8q^ZL24!Bj%eh4o3xxs`VN(&BQ3`|}5&0TOPA%u&b8R1tKnDmB+z2W2jkmWq zD>^1|7u7YPggG>eo9q^^S(21v%T|Yr|H=fxM&;a`PtoQ}w8|Q=-f!dOG)R@cxQlkb^EGhJy)op%Be3F>V^9M@<*8_6YYtKI%a zP}D(~=5~9S8&VQ@Tl2{?Xm+ZYYl|T}kl?l8lUcdWqba<|AZ%RC1YZPvT zTttg$QWYW@pBIgj3`2@5zrXs<^#cEQu%eHG=M?g=^HfpIzl{n9WYr;%N8aNb70)Ss zM-b*RX)%*p6Y`}mr;2v{x=YBgxwDp}3K7Eg@b6~60Nn!KdlqmWmW77h+C;_Bhu!|D)Z3a&4Xj(Svo%=`AI`g@VzUk9$BDE9YB``;cfejk*=>})K$ z(92mMG(+JBPZIOI!dC6f`rIL(w>mb)ubs!N1sRMy$RGOU8zG-JHqlPPvtAf}eQHMT z_j7?t(u%tPNwgBVGgU@-8Q@|949={5C%xusgv=3WbYI zk2fn3mje>D-QX%gO-H^9Rg+UQ;?PR^Se5gK^1qvZiEj+P9A=Tl&2}p#oRlM++qU?= zu}}cYVtWWdZY?Ylm*Tw+Dl`! zbep%12QnStPoe#ys;Km17I5RE1&`D>7KR78aeo?o{|r}GJUF@`caoAUR|ZnXMM?8d za}Cn`NuvV;Uv>gGvdYTbEntd;m?Ie$b*6ViU;;7_xItW9r=8Imf)!^4>#keIGQ6{`tKj*6mnIWw+{)rIqBPy)UP z-34YB5rlsIBfYrq{kr;~Rf$=LqS|#P7-{TyS)avD=(vx!7_M+%4<3Mf7(a!wPap^L`JM|9cgco^a4F&Row(#`H^G|dX5wnqZd$I?wXO2IOO-@ zZ98@>e4rr1w8g@%FGgP(db;f`RAJrS*T!eSU<>noBtuAOmfrg)2!%MoS5Zk8FhaU* zA0&%|lT&MIWvex063c%`|gfXueF@^%sO7zog{;ns2~fgc%>^ zf6UD@r5?;w*5cGwBofvhMFzQx!jWoEmjr0WYdmXx#jW7I`ni&K%O{@&@3*sFM!9E> zi2oU^C~`V_4AJV~J?y(W+rth?if&6v#QAS}DDPe4+a*_jZx_!1JVw!$rZ4=zOy-Io zPxhRd_iTD7XAn^gKmy>wx=3I!J1++O70UY7lc`~O0DTZS@737|P&mF^2+(0^q}>^k z)a)V#kuLLI`~5pa^nr*N4&0vSX}gn3ROop2CQmADMWbiTWtD)(Zl}qAq47A@YdYU@ z@3&>sKJC{oI=PsyC#VUg|0yj#S7`g?^_3KwT-pEO{??$Yw#zYcJnMG=L{R%t_cSXY z`9D)`TJC7Vy}E&^d1u=e|9P8eF-wEa@Yc#qbm?(sY_9Lhi&5??uiqWj&5GrOLVIs_ zH#Sg3?auAn#m&vlU57^JSF+sZUySZfU+NyEMu?N&@~+mK&3;wNvOB54BZr#}RSsZp zVA?l)%e8e+x$m3z?SquTM6uihyI(K1q8OqrtgvJMxeMZxl0Ywnh1G2T&R1Fk!jOBl zwqS8>>n(tjRnF;-o}H%<@jpz6H9*M&>b+E}^Xfpd)f4CI ziS=r1bv2rk+fQ?CP1ZyC{5Pk8Zen})keQ|$$fb7Q&0nD|zZHn=+p=lb+3xCBZSlQ?aUcJqxVp=*)&UVPq=Y+(sUyW!U}%0OL#Cfz zcC9FpsJ4Q7vuQOEj5K_H(Ftd7W@|7*MQpkukleI3f)Sa91ZEpLlt)Chx80VKS{-%0 zLjc4)Bh<5x<(OA{oEEN|(LO?{ua(E7w7c{Kmz)O)%Gtb}mpAeZ@2Dhw4snDkUn2z1 z#@(azOIM-JzTYtJ6OfKu_|KswS|9?GpiA~h|1vP$^$!7QDhr_5md8vj4LB6-sVvu8 z-X&k!86^<|(5Dcb2J%f^L&JfZnwk%633J3}tsCVz_9~=bOaYrA(mj}8*^6ri*U7@d zKHV&N2sKp5x;AUT3AN#jS5TtN2Ry)fN2n3TNyN;l7u6HDj}|G?(@2Uhy6@*bcl+(r z_TCz$(Zce8toLuA9?XT@=hCvfWcff3QSJ@nUH?ZRX%_u!2EPl{z1ItFT;o~w3^w5a zZ1=+6YHu|x;Z%T?vYhX{j&%iPCvH*WwGBnvm#ZS`{Ou#4p;{p^_p)5HP0amJ2L!+O zQ<83-HQCr#Od{&YW-QRpwLm)*&0F$j5xsk!C;Fp{Dw`HY zVm--Q@OcTq6cW_Oki=5(BSj%Js~e24BngzhNat4a->t~fpiWd7UHKCH;GFj%LS^OF z0(85l#XlWMg}AMMI;eMHs(FT2-s8_{B}I0cQ}qiE>Rk_EA3_rOx_PblT8+_jQf#t0 z^2O>3nH@elp-!RpG>Uakf3# z#hR7Qd=zsoqKUl*`5(`b2Q%Nt&6Wgmz~1S;ys$;@O6T^023eS`+gUIjfSkXI-`lw` z_Ov|3q+Q(h>fETV?preEp0l#D!Z~_r>y84Da~es>FD-Rz_S+b)_!iNnYMY5^;PIQ& zw%mRe<{9#OvowlZ_8(V~;z5bmp5f4h;f^5hpU{(}Fna18k6M}27l;7-^8 zAYnzOwBVPTiPqe+D=U+>qXx&RJ~$~Xe>xb9rdl9=e;{S8d8=H%cI}7h;)As$yyj}{ zqOFaSNxV5kOmF)gS77?j%zmFC3nNI6`mO}{$X?OR(_6HxbL>CcrEwTJ2>D0Rg{ex; zN68n+B1X}OTVTfGdHq&< zSYp0*ovx4BzLoJ+ezC^wXImGgPNp07feefOt@HNojO5ZPnto6m^t=sSJYm&TBIX(q^WYZ_R6@WvM#nUscpt<5w-WqvajoQ1tO zzIxSj;G9Bc==bVeFZ*59z?@+9hbz2?QS!j zwPgS&*qtC3cng2$hbrA%wo*OODTJcKW>2WnFS~}c3rh{;JZQ0C&0*70?-Fb;_?Dj_ zZh74zK+!4#nDYFqIl8qX<6WOj_c*PV$3GV^U06bjFiR(4cn9l4X}+H)jf*c4cyeg{8mRa}jdBZ&qMHx7P8`KpZDx`O=sYwPqe#c2xK?JcX>*`oggMShJKB zFUp*?*hDL=M}{eB$D9L*1m%@sdZKsS85&e!@$;quZ@3~)QnhpAL0&-$^bCL)znTwB za@h-{0VaNW{RFk*=@%W_RjdRVrktFdlaM}97@ee_1|aP%z-iS`H*dP1a-_a6-Cmjx z)M{LKI4#ul)WFUDhLY?o%l0Sm==&3hX#v=6#<{)_oy1larIS^+lM(T<*#b z4#=TQOxSxU@ZN&*Z5j^NQ1b{WdHdFb-gK2vi{*Jvd@Igbh%bSx=|3|D%;iaG$DdD^ z=j9}TmH7lwuS+#-^H2)qi9q%9UD-nN#B8(5U;hPQb$&bL2@~FEiZje-GDd@_{;7US zBbm6{7|N8kv?7Pk!#T&EV*el5%n?;2z`aHNwxsa4XnQLvpVnn54`8ko`YM0#ZmskI zuq#zRGd{~wkM?sOdLPq=u&9$WTSqHy&t8&!M1uVk;JAQyAO;&#?tblZz+|HmG>NuA z5q(FN343c@sv$F4QEq(3o!J>h$#ev5VF!gJ+j2n57538Q<>k_-iOmPX4aQLuEwG?X zSV&zI-LmzlLOS_eQ%O98873_kxW*>HrgqxS>=s4z&-La6H1Z-19hY@1`WPtO#vg?hU&^aX~!-el#Dxf z_UzZ65F{;v$H-kxnsZ1|^xHPCFQQD;nYmQ??<-`tAv_`h3tNN{2m@SgABFuis?Z!U zcXPc{e$m$~Y95pQRn{Pi3VW-ZYBm{vI~`Cn7>Ls8anhfFnEn&^=g*(BqxvfzA*)#K z(crtRtE+1v+H8Q0>E|Rkkm2dhL(~M7wB?nS9lac?}BFc-uu-!IxzVLzf7cutZQhewwd*7dhNnc3?k2)Med&RLJ|()>h3e^k;Z% z5vIEg+ef8J4WJiFfKyRXDdxwzww3Ja-6{{OU2ftkil6Yt&*2JV^D$wcZ_;MP%1cNr z^@G>^_8a9xln-+pso5$7{#|%xT4?~PC}QzD$@uV~mcvj?2v-#-1^%=8i=Pt(pnwKea za5lO4PH*-}w4hON5Q;=BCGc|_6s7Idz@FYM4<<-9-0}%pMn?ad&5__T4%=1Gl-i_0LvAx@)SYka)l0 z7Z%0xSYS5-@y$N|QJF#tb^_&laxJf7|6m-5+*bx_)GNt7|Yx*CNh6%%?K{;@& z(y)PmpA!$ei58juV378X$;nqW@XL1iz@abz5?!si8jIbwbc*MS9B zDGbj)LW*c-dyXDFHRF!yol6n+?CrdOifisYG_*l;;O~YONfA(gzs1}~sdLz{bM^rO z8{)Xlfz6f+i~gV zaJA%`LP;@z%{?M0u3lP|Qi5F-`BQc0ttLfOJ>?>g^Xz#-I2F4W(N|P+TMP;pW=(}t5CCZpn=Uw z*bo?Q>Gt+E?;+ahz4{UNC6vnr-{w9eBEj^(Io$%#R4w=nV^f0Xm<0d@C#V3)GZ|KK2N4XBugO5X3N7TRibuEp z84}{-<9|X1mw$&edP(^6PJk6AsFTNB9umNcgI~XXwBS7)MnxcV`dMs!2da9WusI@^ z1@RjO55t1)f3BC*2x!x!YvnphIxz@Pp(7-N5jrx{I?w9gD!Q>9Ry zdC#VL06&NBfcOh*%|2L$2@C1V|8VbnHq2)ll+aW6w|NBlk%}P2G&K@m?c}*;fum+iy2c|U2aua&Cj=j|4{y`>GBT_n zB1U6Ov|c9au>JwRY5Cn4zJ++O`984DFe zkY1bd3=`pO$oTZdxsU6>5KUR}Vr|C(6Fcwtv-libF#?7NTXn787EkIgUqCu3bGg&K z^a;4e#zqIVk1N1DA5ZI|Rt!EOVUp;2sgKRv!AcR}zx%Lj(&aF1^HROoA+)6B&x;cU zAB8a_y`*m+F%w@3rPKm!;0>UIL*lGp!`c9s2U5kWdr#{UpBpg}P| z3eBs{k3$~J{QbROH@izfu4E6zS1MFB%_ftVhKy%_{D`KjATg9~g5f{Q)%>e5RF9g6 z9(0U~b=%*lsHlRqwYBsxTKZ-s!Y=?R_iN{1*o_}Q1fEvqFq2hs@zhiB8o&W@JU=)D zR46?>fuNx@`?O1M_T~IW8I1nq_Wf*xO)OIO)k!kvfp>S@yGsb?fT-X?foay4M?c=Q ze(~Wzq9AnG=gy2~c{vl);O5FP_s$~#VQ_Pj#qvkW8lGdal9)BtPwrMYC*dh2CG`VE zEh8T0V*si|x7Ns`oA%IRtH*SE(kXO5pLLrHOU&U@*BuM<@@PBM)e`HwhYSdSTj-Zc*v&=>9(D1bm2?}}V9fvwobe&t1>^Vn7tR$+ zz=XNOxbh@)k$0kPg|A=J$sK684K3-M!@ZDMGrV>!3pQ{54vM0rXRqR-caxTpd4J=$ zuRL=1Xi7Z=$B8%as8NN=5(gRZAI^h!X?olYxN}2Ab2l7AP?Fa@4v~a56W~(=jN_H$Z0r`E8Q7r#V<_gq_-0Bt zThk>9y{Y`v(=ZMAiax*|Wk4=I5qN_j%m>VUfJ&;1;lXr#YHUOyUE|o+Tk(&a0A!x+ zK8TS_YB%tpICqeVi5!wJVzl!BY68JU^Al1W$5Chu*g*h-O@&!UXyJ%*AHVr?D>eW| z3LqPUeYinFg0dW*7HUe+Gcl6`4!VG00SxfNSE);V#}h-S!>l2C{yRNAU4~+X&{f~f zZDpXTN&R<>^(M8Xj=jCT94JHJP6!x;`8YrXu-X8OP5%!IP2}U{e*|@0le>c+DRBRf z5UmfC^DIIOCr&hx5BN?rn{WE?FoJwk?h`P7VZ{h-DB^$Y<4yqbi{KDbcv*s)i6t07 znz`9o4mN;enHj6tAg_7g<%r4z?)y^u@TvER@zPq2^u)%U3s}6WGa*4 zAwNPc0lwqFT@#8HYwGAhI9|ctmL!M_WhiK{#D91B~185G{lTQ3ci2J0p(g+n(_gDEb|LK>oMR=u>3P;_U3tOCv9i zy^}tqc$zIj6O7{FGnvd_{>lM3Bs5|ouV1@%qSCDJ@~6Za7ua{N92zW3_m?3=Z~o68 z9hCCO7R8{EKKgG1kjU)VXHZLo|El1bkwAtX1rl47@|@~E3YMOJ6joo{SzAgx?PWSE zva7C|!c%bhEJ!;T0Q_2cX1pn=Wo*ohr=JEsT!E)aB@7j1IWFT z-$WpI<+uqOg5*`(k1$d&pRPJT2ME`M^-Vy8DrJC-8PmyK z+y&&`G4mH_gN-;M2^WFgBbyPHwVj0j$5<3!-?tz`QPUQA-dUM z*AD`c@Wne)sNIc@$0Bs^Frb~QRoj1(V@Y%^K>iI64_6W`3AqXO;uo z$J49Qhg2aHm=B7wK5EuyD{e)g$ewNrXK!n2o4-N`4v{{vv zl}4Xxs0goLKTc}E((1GwHP3`Jq0e*8P6|4Cl$MIUbo>ptO8zWvoX8&UJs`L9L8C|{ zbsjx9O$uxOkqZZjssh$OIGbqBjt8>Z0s#)uOw4&Fw&Ji$7kKS^1qc*^(@zb)Tt3b^ z2m3)(yGzE_ZzC5A|3}Pqk|@pEKaEp9L`P?Y0hhZ9mhlMYVagSzSfLw08rFt!lpYCd z`6Dr=urX9B2{82YIqtK0TYY6(ePRz2&PZP~EHEy~F#q(13JcWzYeqS6l;DtDRlsyrn=075^uIh*?pH z{Eiydh3Xm5Ex`}v`Vw@+V5*47-XO8Lbm6-2T`6$AuSBnl20W>k^^5D^K2U?L0%2ucp4 zND>592IpYz7&dCp@4JxwNh4tfMZn6xfkxPl-Q z@J|Yajt2g-;@!H1AQx@5E~pxNC(icy`J1#QEsz76wDyk(@+Ssa5L47HeZTlp{-pkK zz6H*hjB9@?x(VM*C+CmO?Ckx72+G`qni`F&@nvxeys7{OfAB_5 zhmn<)D7?a4*U>R^Y|-1C68%M`)iPkUO&p8G&Y}YnK7Bg-L5{|pcOUXjL{Txo+uq(@ z1!iF)B`r;>8P0KWJ6v^;TCEj(|NexDiAl(m43c@*(NSV(Xef?`nDQLqBkTlLI(FvT z@U-1SOxyqdeHY0t{xAt)XFZ5qFu8W^nv=RZWfhsoCwcxn$BmrWHO@!h8G_O;>*{g` zuFYI6AY&f=Zf{RLe*8F-5jSlXe3jwoqId7FU%#Bllr;5Xj8g|_g5hHHt5;(#&dwj# znHe7x6ciX-yT;)xL-Q<_cJG%@pFY`>c-bl7zi)s1xNxNPCh-!&BBiCJ)ju_LcfS}W zM8($DHVmf42-DKl(Ro&yk@46^+nVmnmoMxvtpf-nJ1oKX`SntY2l#Bc{X~(esi_|e zgH=Z{M0^y&clz(&zc;6+rsQBnk)j}&k@(FYVVzrBzGLJE{H^`{JeT$K#x7Yi(xJnQ zFIT&hl*PWlSUkME`tlK&!D`hX$4{InQG3h6i?a^3&`nuO!avk+k` zMa1|7k+73%WXh*eW-w^Al1XT2gfzOt|R((V5-w85OJ? zPSmgawP+sNywc;J?7f za_4|r7?=t-!Iomisj|ZqK?JohB!O?0+5;bGQfY$e8y1Eyk#*Z~&%WP1UxD@%(z{ z&#_9l*|Ou&xy5#@B0C+`HFtOSOBI+4?WOY-+d3PXR&@LizgfR#QDbFg%`)cZwKn5C z@3i6K9l($iD(^Mte+}!8I619OwY8K6?%3;JpyO|Pqv*${q;vxtho41bR8x|?msnW^ zR+qx`(={m*Y$D=xr@zc+-&=!p0akkKeP_Jgdsg~jxm!(%_57nmmhJY!3M|&E-fP2H z#nQ5G5f-q(kCI&u49djN&`^*wA)CoyBX?w~Ei)}Mf-U#8GI_JvQ6v@#B5&)yKEJDi z%|e{)8eW}^Txs11A{Q!?79%4GaAP#OC&iZe2JegcFRfI7&zI^#|&N9iep6BP*>NKwbx3V?uz($R9Ha$6aJ$NTy zy6?VG-qZ%h{5Pyj@|l`ZyHYe@Egrn#-4do*6LQ6~@On_ZBU_{nmOhl>%Am zYRxbT?PzP`W>y}rtQ=_uC+61G)g3iBXpMlG-t6h_p7QJ2>WE&*K=dZ4+^#Am>&xGt z_}#wnrb?wgxf1q2&sdqw`r@Pbkm+oZik+o~0n=R_MCb1A)+#gO#YY01+^~}C{eJTU zCg^>mqobuk?43I}vpl_lfb~9GxQawiPTMH&+OUW4Fw*oCrLs;WKXBTeeq5$jl$W<8 z=0%Nc@Gj*hTZ?s`|6N=&S7$c_jx)&B)=X!vWn(jVjXy*LLr2E#4l?7+KW%LyaVl0K zn@61<<$KNd4-f+Nh^NOI;}41c_|3TMRc(ym$$w(C18FWtoPelo9C=it=Ka=H%o*>+ zYPNYMvNn58_hKvClkdrM}>fxt~8T_HHZ=!(Jy2jmBD$ z3Rt4z*XJWqP_K@s%F7D1^ z)*g->+2x8m>Rit5=J{W~+z*|~LmCA|MO{B|@f#UR2mkA~CKjM}c`B$RvQltd`gT*| zq><$b{8c9~O#Zf2iJk6$BO`WTp7QL7=!fYJNfalLew>!klC8m=J@&6Nv+ZpDpnhv?!dl=~qn(`sfv4_0%x_@6% zH~yS`+|&}rI5R!{Io{eu#}Ntt1_>htDlkrCUCZsDY1}U-9=Z^PKqo_LEv%zRiJm68p>xoUWEkEm0Vo zf4LK7wc1|!2EFaM!uWubI;{WAJm;PQA>&xKlh?iSZ)^GNU#eh{>J2VhCeRwkA%h{$k~SHI6?IuGVkA<^Mhs;b{E zx7pKPh>mX>n_xIPzGs;t@`z>DwXb&xEbSA9m)s|oA$QRh2Q%u4d(HJyWj_0>wL~{x zQc^M;SeaZxA6Ff;@6b(7PR{1|dkru(0q^Ngg{c`LntU8@e8%GK)y&UaWqaqrYTZ|Y z^~w}cN0Rfqf8%7j=(4RUNtm`#x(nUuhVOf0Vq!u+w=_jmX3C?_-=bWdG#$cyOsJ*QUxVIJbYOa<0yE ze6f}{Ij6(|Q+w*{**^XtpKKOxVV{d)PoG+ALP?nIG2X=ea=v6}>iiw|?$1z7DhLQX zidHxqWc8~0)7AVFqC!HqGqR^Kg+1Kw<=(utX4j~jUOi!f+g&vyL{PKeZAsJD=TP)r zk*pF^6NznDa?WiSb!XszHZn4jmG;JWs)sO);o=t(s^x@?oK-YlDdcXx5@U8_Gh*r- zO-Of(zH)%}hMx-?Lb_TcTU!c+2E1?s`U z!Hy=e;?|C6I2nv-av3Qb%DS3AWD+YSW<2)m_3h&KxNY?UO*F&6HQ(Etn?gcM+q>E5 zY$845)n_6sTEuXq9jCU>i27)g{CuNFX`gtAO^|WzK`niS#UFCi#^8mjJX-a%&m5(%qr6y62^i`&IE}`hJQtf&riXg zyR0?@ZmI7-?$TGX8cJy3VSg4caeHgV^MO@R;jGZiB4o22cB{V1@r2gEB58q{Wx~!iL?~&u_b0EBXYJ(XKo~ zb9cVKyz=mO(o?U}o2JPgYMOGd+j)ynwn-!c`say@ICseuD+b?c1TDs@tquvHJ?k-U`R$ndwiqZ zW3amE5*~IX|BX7o`Esuc^Z`NS#P~>mWhz#Ls`1yaq)c>f*_EWNj|sQP`SK!~o)&Y( z<8OE~BdD!L>e&9Cu2>9rwHRILuk@eG%F5b&{P=Mq9i8^#rhoU3$A{SNCgdMNEITN# zUcJgtx%$b~@{=6TefJRthr?&gvMtUk4O||!Q=@~n^W(;DVC0lJp2RNinI$bCFwDUp zG}tPr)GJ-N`Rg#jRz>I_68*EGfipffw(uIAjW<}ql(+G{Vhb1kps451pF>KINtfeE zQ9hynM&T#8o6J3ua$?VH!~r#SFzd{`{QRqocMihm9cON~rb_WxY<+pks0RFTA`)!L zCqYB;)l*D>(m;BTOR00hl(*ZyefWK6uYIXYhBtPLJ$oMQN4C^Wl!=nZSf7%(-DL63 zp(%;XER?2Vnq|ING-dAgc-ZQ)gzv^+Xl0JW3Ws2qw_bvzLj-(O#bq6x`}VhQ52QOZ z$s_6VODBGuGD__|@7iAmg*cvycpqkG%O+y6p?HJ|`(mHU&6`6f!P5e;)rZY~M6iqB zd=_1}=y^PCd9G}^Num0!iU39|37zLP3k!=ph%_AG+IF%KK7_$(ov4G3KLaqpVPj!X z;As7{puE)5UAy#Z+8Vo&9rkjv=(fKm)jmX6mB(s!g&P?hkt9O6KO)YQ!m|(mSQx%o z+5YFto<#ozeJ=}<*dDQG23M~9?W~U$%0JBVin!R;7d^|_f=opradl4)pP6fy^ks}4 zb}#OQ=8%hqO8a@OnMBO`MIFjGTG3Q3!Q;n^A>{6m?{>^uIIj4cC08Orbq@>RQSDNokYX zAv@Ujd)u>xII9+Rkr`b+7P|BZ%*3x}Mpeh5M-ytOb$0Wko)t z*&qK&?g;XTjgjUN!B$LOR`Hy3_m>QU?Y+|nkwfN?+J+rvYhJMapjzGt0x#@$s^cHpvx-)acs85q6HeK2GJpotdH{nYKZh{k+`4HIcl@6A+N1 zkd>s4>Csj(;t9J_Y2THJ3;;>gE8abej8HR5)j68!z{S?hmiJc1bEb12m^!;0gn{YR z8YtRBMwEVDLRb@I?hRdN6E_dmK~A4OO)#mYu)b3SDS_pC*wd%l6+=(Xbkeii3nKKo zZxnpqq<*$f&rzyWdugMFuv7SZvudF(XK!zr;~baj+c#Vk(KJl5Uw3LTKvSM zZ|Gfwov(G^*r#GuXZr#^!5xqb6{|!ApFEA0ONg~gS>h7_U|h)403xxRIKg8jm!+q+<>bk0;7uPyg=h^w#6K{~ne` zUJwzJD%LD2E87Y1M2M#oUcbJ0@nSV?LXF{onCL+yE@4J7CN>uOT87`efJpdBg=3Y_ zAI!fCCHpT$!6%Vqe3tBrr`WTCJdk&hR7fgdhhxf-&6FwG~pas{Zlg zM|#9J2XAqkYwnnT!3=C9`0dXT08VxdZ``=S$w+g3^t|bP{S%kIe*KCCAL4)sys}XE z>N-$y_Uu`Ivdvk@YIj^+T(a$iRGRH5VDTRe|99WTxcR9hCrMcS0YjLp6%!NF&`5o> zFxAX^*w6}1LyZS#zWbZagXc@L&`H9GyAoiBl=qPYPfRK79OmoSl(I zi$X+wEzXXaKKC7fK}3kX>tOrREr`{LL-^s^$ay2~r-!1DlkPK}pWC4T^@U2LisG#( zT9rLeN%3iZqnKt~OTkzJgP(Wcm_|wjdMcWgwr6brbMs>mgomJ*PQJ@=CaCl;R`DOi zZ(3%aUSm_!njK|fX8=%K;kkek0nM$SKYz+hXamG|vAesQbi4B@)xW29K2A_NQNxNo zGJzZvF!()K{n=VTJS5!7iHSSHA|gsq)(9QL7+l)#_vfU4R4-}0hSPPY5bW~X-Me=W z%6rau>$Ql4yJSSv3XkX|qiuKo?PeO2+Tjb})ZB``UvyeJkFiD!;yV98i8|0KnTq*d zAJJUPiz_Ss8ngQQ^unNFIyCxMZha%ziZg_eZ8UW#{o{p0cemHcv5+60Fh{B#K79Br z+6PP0$I|gLM%E5HJ3D{!*)QU*Z;{hSoWTAR{5Ranr@Rw?E{eXp z%j>`JEm<;L@%!L;aP1pdMxsGap{o_v)^_+ROabfrT{O8eD{H14>e^|r6G#o!r0q)b zc20suj>>*AbhUJ`3Pc0I*%C>l?Rl~WosEp*U~DtF>vr<2DfiQ8*{%1sH0a9) zWX_a%+_(>QH@UyJx6!0N_v6{?1v^cX^%bdDPjAoJ?$tL_=Ez3~F9i&H0O6oPh|R<| z>s;rhk@_lQvJ(E1maZ<=3_v0&Ms;aMbu-I(k1(%G_4>5kXpsUjFJ>F`g zTD(r)T^}Gv3TQ^$PmiDtdkGM^EkFg1mQXEZlw$1Uy%zp``!wF1?6q=@t_?CeUcO4y zXYylg;72(czQ6SJ^yfLDs-F6iyy1`e*W+5bOAChdb97|HTV75sAyGy!eGjZGfA{X) zrPX;%*el!Gi1IT7xHhaZWla@p;60K$cWuNy*l;$k}TE*C%j9w>eMq1FB7#SIzzuS89g`tdw zuWt$9`jZb^j!^zwn~l=siT||ag=io;?{jk(a&6f8VfW%}#C?_qO0d(WfVayKgs5(3 zW@SnC`%Zttd32{I*A6dogd8|6EUb_vcZ6tsSzFtSZt$6pYS(f`R+d8Iix)#zEZ0=N z09qCfVAMp=mdJ(MrKVS|G;*& z8iY>y9@TRHkppq`l^nB0O@Fx`?o3cQC)K7CH8u4alUV7LYg$_Mp7E@i*{`Dx0v-#a z4Zt_?W{S8Hzsyu{@|CS8zkWR(M;%EV9T^|L4b9vKs}s@?C$!Bh`|%FpJc`|YXdVe` zlE|M8>Q`Q{_vE2|9vZ+c>uz@)ZJOTEG@_lMKAlY^52hTK;)nkJKAu^5XF<4alrgR! zu=>BjxdWamzGUJkl=I~RLFH2c8=eDz0edE4$|05JTiWvZks+3+ozRJ6$t-z>$uFMH zE2Jh{;P1T7%C(O=R&}GA)qR{gX3c6(XF=aO&h9ko4M`0aio+EW1aL}ePXNZ^) z-BDL!;LjT1e7=|iK>EX}bJUI5DJj^d!ePUjglF-ReiAuCf&JfWip3YLDSD6Ps z%CtgU=ll&6bI+b~r^*tPmhOP8*pnqg&lkBHFkik-vc$?#yefqB#RLv*I>i*^y4~>e z=gl78-}=+-pGvROO@~v|dAk;mzA|pJr!#+Z*p)H*488+p2TFF3GiZ|!+|0S+Bt2}> z9E0plLomf8TigTwn;-MP+Y1-~K=|_I%aJRVo_G@!+t8<*79^i*+S%C=29&8YB&LZz zR$RoYLcs`Ij4^!$_D#sn7Ww-98h)X_a>||3sh0jdvNUXEY`mh~HcH+3fQwpeJ0oR< z^nmW%_DPiuXB_lWD;Xl9#QiT`ys$t~JZP5y4UA55C8*Q5}!Va*SQRtBjUE5om;=M*pKth-7A#*jb`ii3f0xs)5fs}to?3r zQ9lnS{s)8c5+fj=_M(i>uP=OJ{@t!M&yMPBW1>6(vW~yYs&e8O*oQ9P6;`Ur7VLNec-^mZYhR+k93X6&k#KgtbJ;tfLRM;Ge z);8`4044@q0GZiC27L1J@}{HVHhGK1lc}%Sw`kwp`4M&?`^l3h$4s6+=vt$V!xK2G z;Ckz|3V)x}CncitT-)pOI1}tqBw}oR+wxC82Wwy#vpN|s2brsq^;n4(ey8+^u({Zk z)n&>Ew%Jdr?u*Q^yU5*NiLxzhzZ_J$YMlDZHf`FP_y6-ihmzu0ejth9i!V5G7`Vv0 z+_@to6}You-qy_Y&jZ{eY@;#R@OIVL9nXMt{lz2O2eyrf{FjdH??XV+Li! z<4Nnj2ZCh$#gVZw-%wvxmGg+c(&os)wr1LYOCei9DlXfU!ngLA2onJorFH3>Kj_9t z$RSSgn?adnW&RxovvflR%EX)J&YkOppf{>6PS3}QM@=X_Py{Kzo@b|Z*}ISXWc-EJ z_V!mEJ-pWZk;EcLRtAGMMF>Q%he{9T!tx|PTGJQ#;ptk1w#wBnACFy~ua4ETgdn6-AJ z-B_{Qn8T<@2)uV6cWmojqm}MwedEIgsZs4=0zoDgT9*%f4os6%lzkz2_ z`;RGDM8bHcl>b>nFW$elw)RwwE^05ZDj@Gdx0q#mp&sO|a2xz?1+*>S7CijuSG5bpq(KO(B|7h;g{-2Uv{C~%(_&1fA69GaDK6^X6qG$+OAcQCxHswadjDaq<+!5 z?d@;f@&y7PRWqzmw;*Di@Y;6ZvJOGP(}P|NPNcbX>C(SD)^QXt#wiJjmX_95);o^F z+sxBN!HZGBiB=wX@SaVmH`6NeTKF zfaV>LlbRHW_ECT;7NF!VOpl;sPlrcRG1m0()vUno&p>a;54-m059l8n>*`W=gRhj~ zbDw4ARLF%q!dcS-m*vpV*!T=YCW#<2c_j{^VPk1&$rF7DJNTViKuJl-6wBpy9#%r_ z^5rLI*cqojeG(>xDt;*_pn(m#^XM(3D6g>1&cQAI(C?-#5RGJ_ybIB;>4W z_*l6Vx&7AZK)7c+!>xc*VQzz|WNhvR#%52`JSAIe;4%OYUxD z7^Q7&RTVH+u`Dhw5~}c&U?;6Rf#e0q%oAWY$36lxq$nW~-G_w9K?iOMilVfNiZ`$< zj<=iNFhR?E?=;^#ke8&Q2_oq4-z%o%s&*7Q5M#G{$zTYJZ^R*W_oX}w-Gs9=j@Tz9@;$RP`JF0y!4S zq9%MbPw*nv2@xCdS4v(37AuEMjjrooJ=MPm`8jKSNv`3s*RFa@I5NB$vb7Z;MLCm` zlUbpv7Zz@+##@oMI+z>ZD9S6d(tkyHmQ;rXXnJ&8zCevFqUn5JWku)pdFO|z*a|X0 z9W{Uq`}%H49=aoBI*;$$7TfXYm`R47h*`Fzhc*Tu>j}FzUUmI;aE3m+*-ho%z#p2nNH>isCm_EPFaw$)A zz0EAKhr_v@=C=pPq%RHOVOZT~>p=HD_`#0Z^z86=nT(JO~78;!}bH%Fj9Q@+D zIyFhKrN#4vb4rV(D)W*T-ZV7_uH|1hsb#+K_aQZd2CV=3R~MLcgCPFm14O3*q+=-m z%wJ|Uz_!#(8WqKDDie}1VIPurXKojU2$cuuQrx#>ZijD7mU29zd5cnvt#V*~%J(M_ zoc+I}1T+gW0{8R1fI2C9fGIVC3n=!&f%)+vVPuSjv^cNdtntF*EuflTLDF;_SPbp| zO|Jp*+w&1R97?2(;nf*N#G3wkfs@aLuqJA4N`5~X?O2h~bF7_z-$yFR>4!ZyUVl)Y z5{avMDq7(^Z_W92A$gq6xYJ}$uCb-OGjO?=QIp+gL<3)db9ke%x6-Gc>Lj4Nc^U+yHhFwq0=iq; z+S*vFh>cm%E}4Gd#%zt1g~W`}BmtQKCk)aNvX|Lyz$Ta9O^s7o+j@*OPIerq+&w-n zh1+bf3e;WAr%?Y`*h3B>^k-R?u6edK(-T9`CIWm_6GR@hR$)#3job5;X6G; zE@V#OBh(aOVZg^=l9xJo^Fjej!b+Yx^|+Y z#9tp~AE(A?X)@7&5afPnJwRyeP`y?# zd+#QRl0Q_OnM)?u5SoTZQE_n-a)2eM@*jh3jM1S$r5qV0T!?v{0s0wB^N!F0#3UsA z96XVmb=|dGN;j--HevD;&twTtR%WKW5_YO}kkCMZr$(vn$9aokYqeG{LK%N0E8~Ku zmVgRH3wF8#eS>5-_r7m$)k6!mb^~FlipqensYVNN&7qf-q~}ay>y_kP2SNiCJ6&>f zT9TsQ8lL43FKIqo#JMYP^gx?E1@Qwa8y%w7Fz2c->iiiYSTL07T`kgr`S%iSZ0r{)b_3luln>xV`uiBuz=%K82~ z$7PuozUBmkzsA^{LY)g$_{DgT`=YRWEU<~C2EuAo+?OMLj(@XWKQPZ2D~s7(oOQ*? zYtpScI+O@Zw=sy8xkXO71LQnZ{WLnbNb-V10m_<(j{`-!K2K<%pNQb-0e(}Gl=1Ns8BZ0R=y3LdGrgYmae$a1eu@&Tx0HTju7AjAQ*UGTwJ`=?U*zA zoR4_w|2AW>W{QsugSS-7f4#f-bVn_&(xrZ5KeyZ$DWx|(kCGm~H{os^Uv>cW#!nW? z#drU!ZAg6sAo-KWkB3_uLu|ig+vk-Zv!1m5H}zg?`zfrDRH^6eRxrz05_$f~DuSonOLt)t19Q^sy^M5cj>@`!$b|mDf^NBq z>{acH5Xb)07JXGBskuq;W!ZP-*-tJVJKKg$Qy#ff{p}GgIcMtJ1-n0Ca*a@l+FJQ9 zR^Q|gI&kQaZ3g22V1i6lbfBP(XsWN*GT>(ZjRFddvGZ9|=37<6i{6kyw?T1KQE}^6 zf&x$w?8p4p=XRaFU%5)bbTXgEI$U+8QVDCyz^m~6KIhu6@WPom)t+tXSIW|Ub)|#* zsJt}g_r`M)5{f9^tY&_jRCy-82&f@ec&YmujkSq;|G-bsy%Zf~X?N>!F0`k!>5DqA zRlwG)Oy21*i9LnKK;i3!Vz6I_9)8HSb0vW5TYym6(cJp7XywjQVQ_BX_p=vnAM|L^ zmA;1+vzD3%KGJp=C*GkkYG6~ZS*7e(}K$?5qyUb_J?_OES z?R)Hnj@=$Ch~1>H932}=SlSx+_8dD0^0Tp*Dn$BM!dG7$yx}vec0xj5d3WV3p@Dtk zZuOJHhk|ll%S)k(sU?WmGBS-BIG>i1YTJcQ=*T{P#@Gh?%z=FWH&7)mKzj+fF4h2TO)slpPQnIKPmQlrQ zdF)rIQHw}`m`Uxqm%T1t&se@+($;=m22zcA(155&qdHP4|2hz{^xlQKa1u0iUCE3s zaWePb&5n^vZ=NP9fyg4>F{j*fLm#)*!VQ%c%I{(oqDB_IZw9jvst{7`2d!ugpYndH zOm1=yuMTx5ZKVPy${B)i&aqNkM~7<`7!l9c+p!~?Q~UPqn*?`O0;~87k|NjNlH0$? zp|lq%+l}u+_ z^=~=g%U3pb68q}M{U_drW}8^9;i2dPDX|_}=CG{EAnDRe_!Ouyy^LcO zX=>W95~3BhXYwoCCAU8*%>4q9g%BYE^IK9ERe}SC|Lef{Uf&+>;v)bc`DuZO<<*q; zZ(IRRE9sx6^41?FiCc{usYW3rsZSnZSm-a7fhM|>kcyTm-^Q22-7iz5IK)o9W%=$s z^SQP?BZ7K&JS?`g2`zGY16Ka%>Y1YdL8K}Uy&$*IPF5K77~x6G6yxghg;{`&Q{ z7uPRmMM31q`gaD=9Xr6R%I(|IZu2+p~`CIOLKo_%KyOc~u8JujL`_PXo0);3C9hGq?=1#{AzuDD8?dg<^vOPMs-&>zK|^G@K>y@lKCGn zbGRelkIEw@=k|CGv{bicCCYKdWOUaX=_{_zK701eF_;~yta{aKGg}7x)qBJa1QmDsA$Osbj*4z<(hO^d-L)G=D9u_C#JUB2)^+?qc%jj0n z+uQqNV`IY@v|f5Th~%f`AYv$w@5Dt7OxCw3Q9_#{d1txO3wS#{=Q}0trG1j;4Ni-Q zh}2{V+&lOlX|JOFaT65)0^jj!s)WbQ_mM!b8o%bJl90_t$o6?)ElHx7DFzrWn2LCl z$8Y8{_PZ)=_{s(UioUymZVp2abNkRk$5T?_hDheU9nBw?_r~+^AUBCUV<{&$QSm|V zeHHyc&<0KUaUfoM6nU!Gi<6U0*U6O>Sy5tMxbAf56h z<9&eemlZ#C${{P788a0DgbO|YGgJ=t^vKyIh!9pa4R_sFNPe|Pgwt6M9U3018c_5R zQ40(tS;9187yl;oLMgkY?Mqf%cIOpiJw`55kfnE9{v}NYEF|W9quoYuTqpSXE_CT- zUG;3Fl?x{>0xm0bIFurVbICfTwYBvzkTlCOZHxb&0=+f!jh1)kRhA0SkOJ`tN*Z3l z3L_%GbO2LAc5S=2ch_kEK-ZuT+c4t~`WB8;T=p&J0TIQ-8>dQ_vc(z`h%jI5Pk`{` zN@1U8&zHff&`4m2h&#CExJT5Dy0G9nI*kkesgulqKW2Wa1*Z^(?&+4^PXV-`F=85z z@rX`}lZTv8O0ZQ0!PKDyiQ9TRAaioDogNsiPdWf}=Qy;#BEWqTN9#RN8LSnz#4Sc6 z&l9Q?)@sa38~>6LYWjxCL02^gKIgkQbWzXd(YZwRKk`1R&zr|Nzuup`;z=1twGUs9 z78WGZJed@l&|d6K6U%n!kc7gC5Y>a}hYlT5m+xlw>b+OnVN#3N_P(ck3$$|rEiElM zgXVwU)aQ4I-YD}v!Olq7PrKg}{3Ze%T`yoZK9L3u49=iM$IASMl^q8bv`nvsoirb= zgP>fRw3L)M6@xthrPok>Y6+0yu@shXgp*-UdK(*cG^BMwPi2QLXwF@Zjwfp|9n;}P z+?0vGh8M*wJ2XMJ6T-xK{w64HEDktP_}l3S%F1?O`>-7WCM}($4$iI?IaI>Y_Lhsn}}nmS8YyYeKMndE=ktC-1_wC&wMvdkEHJNxb1w?Of#fUGRCiLu~I zD;jSoIstkK82a(p6GGvHypJdm=&XEDxZL-@qjM8Qk6@K7!1^eNZ|ZNf145__P@FXb z-^-p#9PV-IXYWE~sD7mMLFb}LVSO-KNS{XvRVg--*Y!>ypcvT#T?qns04z;A%<$)g z<-FZdOP|pgR7Z2aEb9b{)9Saxsn0N6)d1Bg99TiKi;1!Em#=8E!)`EdMl%pW3Dg~-a5<apYO{=aV;RT58dpQ~*kncTE;bSr2ess8QlMLwE6DYR6sA3gVicq-+Y|K;H zrc$pw|1mfuq6PXZ4UnTjzgnrZl|YXJut)L7m+{XZI| z8roVaKqRvF#@ARy1rCp(Dw|_vW2<}*dZ>WoL3~kWu=Z4Nn#>3Z_y+1;DG9E!Eva@DJHyRQ!{c3kDI`;{5$o0Vz^iur~uaWq9-#S6utZTc`| zzy&C7>Ipvs;0c>!V`FIn!rQxV`Eg8#Ugh0Nx_#z_Jb5ZanHdfZ1PyoU! zy7O=A8=+@|ioH#+3bzi!p-L@0J?H>c#MozG;g8nmlO6Bg{aN5T5U2(li!0k4d)@57 z(Ss>PwUV>5vrK9F$+rx1)WG!@{%G>fwn~mMl)eZh??5%QRS24jpER%Xu+#0wQZW@e zbT**>@d5!!F_=OAr7NfY2fS#sJp_(LRSypd=VWwr zH&A07)Sce>H+V&05Z)ci%o{j79I5PL$jvOqf@)^CCQ%_g#F{kJGPkz2OhI!95f_fW z9bO|<SO<-t@#ASl;3KbS@9lV!VaJY2*6c08nOOLdQ#ls05Dq=MN%>#6Es70-!Zv*eGa*UDsezI zSPFg%QxpWQBhL&(a(45T>FEW;q zz%~k63>eo^GRA-*!x@$|<=y-iSGt!B$Wx$gSwktpsfd!`F$t($B=)L5Uw)*#JE_l$ zD!E$&mtPO;=<&FMdej8orUTR;X`zZ)A8cJLW_+Q)&n}6STPFgV<7a&yzAhQ z;oco+qdL#K-_Xd&HtTw!%SZ4Ic?a#34gc9Hppf<-VPk`;jaKX3iyv5a^J zkTVHe(%pWB)ZOl_#E~-z4cuWTgh1Lb?&##C09C(gTFZqE`?$urzS2zeJl%kv9P>XE zR(k`I#5?YPPvZS6;LtIMtc(7Ioo8atefaQUj2hhmfZJZC&K(dOr()O>W4NkNr#+*n zD+&A$Sn=>bAb_0s0w*%mk>8HRZN}n&{Yz2XIEhb+q9xN*#oO%S*cfPN5y^zqrVRdVpC-rcW zGOqt5WERzynLH7M|E)^X^?og#`d8G^Xa}x;c9*em>W={r@ym z_6+7tP+Epi1FtJDy#Gab_kZVhEHDz+K;~Qr6`f5em5S#z(7@I0OD3x9aUgUyvo{`; zkrhC31L%V+tvPeFy^?Us3<_=aX{p z#i66cH#D$)+;qjO^AN*s*Z0mCVXsoWS|0|mux8E z9v&at^cbSUJ3#+=o*<%5yqr~X_W38A3RM<*)QLZJRPAj9R72+jwpL`?MrqA; z4GsT@2Irb4bzl`c`}+fF6k%>K(__szNY|*3hX!kp6w8zbiQgyz$s)?~v-y49?K$jbRRN~f#9H{U{ zPWeHZAk($~-i#;Pl;Y5}y!@)pPL`*r4%x%VgkzFeSBe=!sFAO8hP8&80{j>E%$lx4XgPfn`y#yz{Y?+{K|*g56hGW=6fhf7JiO^lLn(k7^ih=9hK3O z2nb?0PNz}W4WX`&p1UxKrT%A5(7+T;G8<^O86)fd$cclqhKS1+Wh($d_n~-Yz36e8 zZzor@a-dm}7k~E;4|NJH5?8e}+8_}e*58G2tx?Arz*=?R>1D0c#o>!lA-A3elogw~`G)3Z z{kYYIto(sEij!gm$&gRPHg%Q_dJN;qcch9snQ>E~t9hM+4ppP7=Mp)JMQVuGZ zL3^~Ij=^uOI8>o?2L=3tZZGW~1|=QjEfEx6-_y&$xkn-9R7yTvHoBKnw=rS}9A}sV zf$`FVM%te=*65Szf%}nN>A3y)ouknA2P*brWA3Z*2Qp;q$;5pa3c^@~eTpEaQc0mcB<6C-GY^Sanlo23Qp+RM*0kX#|vJam| zj}FS+Le$em3#r{lgsk<^>!iYu+OetW5o|=3>B%^Qh#jh#%tC?h#k0)0#pnTaRL#=o z%mAlDoYmamAv5>0&$Fgq=+HxzA?gk?BwJKMQyM|x*2Vmf5!JAXn2{Oi=}id{%qmo9 z$gI-De9y;;Y-47e0o|!+Xu}okV)pZCV$g&7T>AyLX%ioSM#dbn)Bg{FjoTpYKgm`c zaS&?rodr~iPxv1p4VM$jSg!+kD+5x8Fr_Kw|F_aoKs{|Amgjx9nel&tI(l95dzBdfA1Gb1yVy$(qs6f(0alxBDYZPe=Xuv9rez1Uas8?WzHS5W$~B2n8AZ z+VJY#Ly$XD8dsHXdc9d5_4d2DS)+K=d09N^r9Y2siV{O()HOtvosy{B@cu1(M`sS> zF5f_lz^&lr)A?PdDjk(DN3{?W1fc(G4lMm7yn3MOmX8; z(u{4=^qbu`k7hOxS|smzrzh?Db?lec&~m^d{J;4pqQ9ziI4PoF+rP*zrE|Nr|0 zQF-1v9rNHk(nty4BPu_P%>Dc19eO`-R8Jxlr@7#>ke6Bs7c>k%ozB6qJF#OBWXI6X zE=N{a`1s-Z5K_M`3{tzfJDculckkY}6?oQDFX1G=Pgz+{t43W;jRY5ANf4x7%hB+0*9*3s2fXQK>#N-KpZv0b=ZHN_-mMUr-cI`k>r zH-mO_bMv8-lTZ|Ku;qW(Xkj$>>%I*4-H5fgwqtQ+@QkEW;{A=eZd;ric1z;YC9jL( z;?~|RnMOCVwHwqlkHfQB@^W%=W@lxo{$(MzH}~|Edc_rHns3-kLZM`3b)Fvi@0e!W zQc7WA;f|;%nqRUEUC)MxO`9@|nAr~CIC=GYE`laZrXn={MWlPf`? zMX%D*SoQDT9sMaw)5X?HvXU+|`SF+OGZLin`|}f=Zx`zOs- zCyGY0o#Ea>gHDo}#l_tXaH@z9@eVIP|IbEQGA1U4Hadoql9IQ}9$%k%p35*dCR>T- z(zr?Yz{j8!p2zt4ix-zH_t=8<&CNyB)z#l&f?Ca}$ zyj(u;_QDr7wP;r7G&%2!;h(YRA3l0y&6oA~-^drrYU}PQ*chso6<7a=CijMsQZd{) z&BBuUCM6}ML6$-OjF{Ec$Jei4r^QRxGyVGYtIEg6=Ys2KmAfq7xGc&b+#fmnj6{j| z*s)`46dd<()!tGoQK}+&hM(A5FD@@E{AD@DboJi7_g&rH`ieHc;<#xQ4_4%GtM(?^ z+S=cyTN8>m$mm|g#hoh7%$%qeYBlipKm0N^b&tzM;q%_YCoxjWa7}IPQ>M3XJKi&i zzjMKeF@9<GLl1fWU^{Y;wlCb{%f~oS)@=F_&hhIl&H2M9(uJC_Z5*XT3q-dGBF~MSPf4^Pp zpU~6%*fQy6pn2cY(vkr`SSBPT#n^OFZ{)bt$jfW|SutE+__N};F@lD9KW11``q?pE z3PKqBk?_-k3`y-X95Gyntyd%r3(XEi3lqy+rhdHQ(N+vTtIZSJwMe+lrSZ^tsAM`Y zTRSOuG8@Tw@%}wmkn@qXIO4Ryp%KIUZlv~lx`Mn62M1<0TFI^MXtS_zlXcg(lkAPW z@7D1p^3UfL6~}&|=4gvtTEh9_+qJG=Pi1c{QKt(b_!shJmGFhnNI3S7G>9t*hlho6 zlnCAUIvRBb(Hr^fHp#Pfl*|`y8{N_)?!7kkr^0C{_z)R;D1FXrhe3(m;Kq&SnVFei zxOah?{5Ix`OG{2nGLQLUq(xxv#G+Yb8KOBzZ~1L>tN*Fq{_E8gfDLqSml(LZ9)(d# z=r6dN^2ax~xUgeJ@n}*pdp+uwR*?{qLs&$_$+KtAc9e2ed#1$Mn5=opb-)jqA5Ad{ zLRa+r_wSk#AuT8WVyUf7^q*?$zVOeVr7dULk}StogJlY;76cx_cJenbeOU9y2241t z-Ii+6pO24(gboXqXSXHQJ{Nhozc$?}?%k3{8q0J2Yz*w`{?rQ}?2Zn0CGjbKfrCpU zi-mQyQJ!sbnVRR7kYBB>+q9SVwppEjlnA{}NWjmslrr22JltHZwnipRKR>XH5uwC- z(igc+e)q$dDkvTB7fqD#KfY>Gz@rJax6`SQ@FLrbJ);}OKOxzIvK zg1`I4iE762x$bu&Z!SNwMg76&P+B&Z1z7EjGCU`in6l-1(4PwDeG`~#O1w1kIJ2lo zG{S(OPSF0Wz%|!@+m%I)KZ~5^oYxCaA&Wa&@j_=#XzxG%a_4TGL6!IV4BJX=yq+}J z*9F*#y}dVu#EGeAc2*}vN>$DW@gT6;EzxX&N!#Tizovf1J}YtvBd29OIEnYF|19Y; zlBoL;&y4Ih26+68;pX%*k4|1&U(a7M#aAsB6jx7Ty2=g?`~-dLbu&__vW!^iPtlzI z>?E1;U~6eeYOp*ZE>5mpmPfPzc1xa3m`BEPtT4X3qN4wvt?k*ZZ@*wsq)Htx2nzmb zk!3}$TOa31>}cMfrDap7PTH3E$S)(4d`{LQ@A+@Rce&Zw*^qynl}yQFFVjhr`vOxS ziO}?_Mao#ePEU8W8TdJD``JZ@jOmo4XoH6k^}f7X8Y!QEfT_m3lvH2^$EW=KWh?3B zlF+W=At#mFxBsYW9*!Y{jGT;=YpiWW?LYa9Xu4>#)Ce!S?J)no;%o26nGM}+mYM4B z??2myV9yrR?k(^owDx6vYp?T^y$eQ3k6*l@X`U5hLXWk3jBLE!m>c`rt?y86f^{sc zk&M4CT+%V&=n z>#bfb$(MM-EJBZN0S5>ul&D@>d*iOb3S51O*3tp95{dQ8yd#o+4QK6+JibXIy zDr&|*N%`l`pT&Fbrf z#lpgOW%zqP?FWtxufbT43hFw|ii{2PZ{946*>)$hhAVq+Z=WqL9Cd49dFRzanL+uC zhGnnRG&Ou14@pp0y0^Jwk)$i`fEeFVA{CD?-IqUI_O7h#apCr?_I)SC2s0_tnW-sb zh-*tuJOy>NKdy<#AQ)!F0KfO!G8Lu}c1Bz(tq*oqtNev#b8mh6t}nB)A-G>j&gQq` zgIf(Qre;$Z=^Y&%Z5)m@b$;z1x|#Tm=F*;>@mgF;N<=%B4(E4YRMVo)ys#$%H*G|| z;Ab+sJ7^s({Fg%7fI5Y>1h*r*62q?1J4d+S?NeDA@jMu z1eGw;X|=UGi{0Js+$XNm2^yxdX62k;6FDQv-&O-tHue` zVIE|3uBOTR_OI2tnD17-O4{_)$ti`5tEuP~9fs_iL-;58w-$qAL&erDqt%6@Mjt>8pQ3(ntgY5H?mtyu+a*8_RLweo_XuZ?naK4-<%qiFv?9*VtNO(|Yj`rmee2sJ_DaPXNOY$8Yh+ zz_*|5+&^2-RPbdcc}~x%e|fP~sI}zg-Mju*KTYQo3tN33E;rMaLrC(ftNjAmBYBK{ zl1MFV0*FmqZ4@8jYqoW!{aGxiaVdJQonJk%4qV?HFgI`6cc0VJ(D>OCR4wVmkn!cq z7y43!PBLD^yu)Ur)83x}>s}Gme@9AF`E(aeg*opjW`ulC+tFE3sYpC2krZ2W!_>5@ zvhZR>X5&RXuc$SSrQ~=cW#m>DJmmKA&ufuFLCnd=iPxI0fZU zS$X;0_m>{?B705r+jeQ*GwRjm2^Q)E>J-usg@uZYC!hSqTIgj8>@Ny>#P&8md1U6d z+WeL8mP_R8!!+}Qzl?(v6v|1G&Zajr_Vs(~>8F@oK6AVpaD2O=z_aBWFMK=x=VCH) z^K@=WU9FYW&%0ke@+Wqy@{Ehm|B+?oE?l0^NjGa;A66o`)E#b3#5v@QmK=&f!S&2b zn3PXqZ@ZR9lV2F-q;t{-uJQ}LO7Y{zk9%{nvfNAr(=XcmdI{k_w87zEKf=AAa4@Tw zKH+xP;#{=i(T*qIv?o!9$kKYdHNR5+S)QeBVe{H^rHF+Pv!v6H8}GVFBAQDrn?kGg z>Ubnmu1ouFEiuFP_!7>2^7!%Nv*ksW$2Jzx-IMBhyZc!DOCghzdsjc0MTCTqWaj2B zygbXCG}K0Bi>cQG`wW3pM^iZR=uBG612c&H?s2)|C2MJ;pBHjJwPjXc5RHdq-#>0m z9@E9>(c}ab5>?-Q#_~muBCbym5Cu;S5NA~BDQ>iw%SBBT4Sbt^=>F0DZeCMIXJ=*T zLRGynnoezPZJBEBrXzD&eCcNs)6y;m}Cr+a&Q zk4(=UisM=yz`wWBN%@3?+TPwgl(-d-jQA|(H}%%?h-NPqe7(?-(4x2gZMb`(?-TE* zUDH#iPv^hCc^E|PTQl34-g)?#UP_s=*36FQPxijS7IbrVG=&Potyml!NI}4kP@(coEan1yKZZ1 zd)3IuC^kNaaE8Ge{mZnIT(4@d(_*k}IqW2_N3NT^h2Q?x@}FxTE9?-a=$6SiuAem) z4MeI`B&*VmZF|po)BPS6#|#kBU93)LpAnHrr_E3+PH!mc_~lY*-YAZ#H-|bh8C_Tg zYgN@|47WKsYf7R?Nq1M*0|g0*)6R?=vyD%w$ZMFh`#)~_A?4wRJ3iOFco33Chw@_{ zUk~ZChUMVwEZvQ7-@d&>7cMUJNW$XD({jgw838w8ghL!$I#WKL^&eG)H@LfSx!0UL zJfRaxf@PSdEDpEpPx&y=PN$$bK=B_Ze97!UbZKP{)+>JV+)7ASHd>a!Nm~F zl$Dh=KM)j@lps=&nDy9#<@65>q*GOdT!?lj;8wH|ieCTiQK|NQ@%Ow!GZqfSe23ra zlnSpvZt?lES8NPk80%2GU4zdu$W1RN;EYGc_Ho+=#Qw*<3if^119+79!p{Y{70r6i z*_^yr$x+h~5%juw01Jzlk2J5)UXv;!D@;9`6=<3I?gKG=a@9 z&w#wR+d;jV%T94Vdj3ji>Z_>eXf}_&TSg{c&6quLpN%;ol^yLsaxFR%{dgPQJ9kF9 z#tvqr1P=~((Lw>zgOMNcBz@1jjq(&tzdW3FmYYPFZr-``M$gE|Gt_26PJC9W_%nb5 z9V8{xbPMZ8Pa4EA>~!i;=XR;G_U;g6Z2StXhsdzMG2e4I+?w&aTk$JeZbGIQ4#n;$ zx3y0YqakK}rd>-zS2z7URAteeFg`-)qRWlW9bWV$tB(1)n zT40y!r_YgR#L1Y$>g~WZ-KwoA$QA?7!zM}hTl?7pwRB@+qfq{fBg!wo*|>RmqiZ(% zZ?~*PwS0K&UHbcMjV?hvNQfA6Ts0@Ox`Zk-Nt>r#yRwwT0?@4{?-ruHmx+mdfT{?w zPo<2`RZmjJ{k&IlEoKS|q5bB!su3}MyGebYj1;vcjQ0`^1PbqbSu+BFYwpJ5$Cq{? zebX(uFPQe9QGlx);C=VbKSf^7Yh^s0uMZc;A2FMiV7B+=7aK;D0*z5+WjFP62^DSG ztF36EzJF^Ggx*o+sl%|!p?8gV7p5$$H&J@x~)z9 zEPmooK#grVl-b2Nr@XtaviqI7?q3$-xhF$T3>$K5+dScKO=7r=baXl*TN6Y&&&~Nl zTJta6p*$drOXEUBGyQF21XiZ49Cx0{3f6gqZKjGQNST9>&qcPETH?y^>Z4Ugx(9eegy}B!BYbP-Q-L6 zR|ku&_uurJ*Z6Es0mL(!IsGwErFKZ^Ua_$iRBi=BOqkwp4pvZ165sYE+y;Epq}pq> zCdB?L!Ak3-C(e5o`-FaljULP1mMkGC%)`TT>(kJqjC&?qSJl+Mcc#hj=i&(QuU?&4 z-I^R6}pPF9Y8(@?C zVGQ?kh2ovMr_%vr*2X?HGoy2U>enmrcNPOT^z=mQw(I1fil}=Y5y7naTr%gVrg)J_ z-skZ}ys^@LNoi>hg_xNy&s2(|DU7iRXKu~uAnbCq&O;H*rx7D4DJki^`nUN5Wd{3o z&v8>J$NoIk5PO2!oJhQzh;h;Qn3YvQ++VY*$Mes*wc?$A@AWD+$*e&tY`YRgS|%bW z=y-|ayB0E7f|D4y^Qa}5l!~5`P{8cF=|H}aY!)wKLjP#9&q!q1)s=oncipwx(8kVA-6X&N(}vfT4N~Um+3D#> z-l-IEf{(Gt#&=u-Oe!D{nB|is4MjPsSN{u-P*NZ6gEd`O-0O}uT#fo^4naVOLCeY9 zH5B8j&l$ z=k?^Y!ige*GIc zI_2QlDBwxhxw)I2N{7_ui%;x^M>5dU)2k(G^F*+!9|_F>hN`V{Mjz|?{H=YzHEKLl zs&ph`A|kE=c;gpJ32a2Tix)5cg_6b~vI^71oOQF`@$KYm{r=Rj)K~k-qeE>I= zT90a`p!Q!kX#Ey;Ql0+oA<)$HDvdu(N^P@Nqtx;6^cDcPiBpLEqeR|WkVb-C zJp%+yGTBou!}(Ot)vEe{R)itJb3P5 z+hXI#-oLmmu0AFI^L$s5TVKhXPiRM2y2;aIEzq!fF$=khy8$Qi|y9; zNajj=aQ>lkp)3MB8T`#!UGP?D-f{$dh#@Cp);ltp>U z`GnF|r9-@W_wMam`lbmAb|vYs@bK_)7UG@I)cRym%SH-Z@!S~~3ly~fz(;JMlptdM z&+Y5z=(VNI$zcE@(mbjEy9oGQ$*7krA21eN#u#kFO+!NuPY(~tB7R%ksuICCM)Kj* zHMx0lifhP0Ncp|P%(!2JBot3hkOTq8At$X#ZcHR<+m@IF71CTRaR^DhZmDg%=`{+; zWAhkazv59BLM{ZFC+LL26J)=B{aT%(fjYh4TmI9%&p$(gu|bX3+jJr2k=88`AL~Fe zeo#^8W@cu>HV~EGx>r9pH}@Xu@l(6U=;#r}Ow+_8zkNAkoF$g#4M5A%97ehAK^3Z3 zuU>8W{{1cX2KFU_o$!`pWKVF2w|+B z{dO;2gwz5Mv`;OQ^On=L%3qC%EJ}y+-dyPx=0YsIVz?}?Fz&q1&HdF4G{}>8*x?r2 z`#jf^<62>PRfv$#*;TJQfJOS-ySnB8{Le-o9E}ELt+wtGL~n8T?%g@|db{cA=~=)QpIB=lmoH!T<|8}B1m7AvaSr^9 z$}5|x}UUxt82=!%YD6sh=n zr$FZ%X)ifsn@_B3gBr+3bX_W$%NyOX@&#W|(96cgMhfqxqzF#uO?u$bE}_WbQT1d{ z5Qje1IWuWR7Fmxac8)>z_wIR<1s^_0_u_C-6PTT8z$9JiV3-z8U=dRfzdU%aO>P|Q zv9~eL`Q{3SMaFMu#ZpDIip09<*&p2#gU)ZM@+K7kwCBeaV~=3X3UuOaXeEh)UvXeo zdZ1F!q->zE%s;8xz2x!rYv-HG>k<%e0tEyGS7nLqg<8sqF7}6q4sybc1l$|ir9f_G4Rju@P$V-s+A9j zspNnWat&Iv5CSd4DB}EsQ;7 z%28o%uFT`V-zeoUbf?^atke-Awg(WMLA84XM)`1}k+f_RfxC2}cpodrHKZ)Wy~F7j zgA%_nSrkVxXY{{&>0)V#=dVJwY z1TTAz1~)OsfIde;f~VA_OK;RQESxP;*Pl``(y+?=l*M|=SqP~|vo7a-{K$_!eUZX8 ztR>r8lxio;><3_TtG98!opizHIZW$irfYn+6=3^|W&^c#ltu>wz@&s-c3du*s=)h6n z?p6M&y9lQV5wCDU=1E7g3RR46N4g?@iiMWx#;seg(Xz&uq9Kn>*GzXGkO`5xZ%*2D z@T8d)6^4*2^fHH7nCekgxQ-pE|dS&Jf53P(HEa?9fA@x6V0s<=SHp&ftLMKzMl zY4aHw>KU%DmECZ7p_;3pv&x-gHGhhAROG!&qM3qvap3 zI1{{Eh$pB@k<5k#e-yN<0rN~>k7|LukdLC1nl>K=96hV8u`8h_*e;90WppzQR8`4v z-9DyFl))}1e#d`lGat;IUqB%DFAFVVi&2@4l)?={mduGBvDNnA4Xq*VAIgKZRuR{6 z7Ak~etc?i78gUj3N+_n0s$;kRj4fb?6w)}zO&ZPoH~Z8@&+3uOy7c}2{cs}5E^U_= z?}a2LmH{v(vyb=Csn5oYC>aTjWeK&$2({9aHbXAKn-%9W@>wi> zd!Ksz__CRN-Y6pjgY9ZSI)8Q2rbR#_AO?YD9(^mwghl@GLH6oC7vF=Ro@ERacJGrT|m-f=k1tf4nSIV(+%g7w+Zw+b$aGN6Gybrx2gaO%p_t z-Fq(c*^G9oj6}F_{%X`G5utv_zeXGP{Yh8;KzuR>=KD8X*d-CbleR;Uf;r5tftiiG{@kV*d zQLL_lclZ8SS#Wcy-|j<@E-;qKP<0K2R-s^pX8F)R8^G}< z$hod*Y3+~6&@J}orzn4{h&c`C77hww*OWU zu&doFJ>Oh;MsxblpFfWLeLCvcZpFD<1_m)GdZVVbkNrQ2=kK?$SGZz_{AiWGw4s?< z^GX#HIXO8kX|b`^^DK+F8eb{B+{h>-6`4zo&mJ(LM%l+3cxfo~zM;gl@P`q)bj5ole7L^9=aVAL52@_Q^Q`4`><1 zr8}A}--kNSo4DEJ&YkM8*B?o(Z>Gb5}aPqz&w-q7UrPjIhBnd}Dq0UIHr5#5{2TZZXa;W*q1}K z{`(K{v_2npWcKX57(}fC#X*OVyY>e|cM<&LoC?0v^FvXsEx@v^`# zUj6_pwo(swXK3s*y5I+4VsH}E;T2=bJ|QL1MNLq%rUEg|vHiNR=r$Il=gWXS#BDAPbQ;dtkap3Q=MoF_ zVdzw@1q9T-HgLDa7{5_fRTVt#bs6)RQ$(aQS1rr}Kcxu5rMbgPP+;uy->N+<9eeh0 zjIN^BOAY&*1>snDVQ5_5P*qhW4uU4WeM(F9$o6_o9zqCRiGBB5u70!E;obY&!lq?> z8e!6y#~Sk{NR}+3(wjtD-*d!1!hiSt;Mb)>gP)2Vzi5#`{AI}gZL_RMy12RRFuhEN z;+h^rOP7zmm>MgXn1zQ6Q2 zH^srI$C?<)qDNjFVXBKvuu8I+tw#fMn6Ni<{21-e*oe8rslk8X! z_HTZ^xRiW{!Tlqf*7%RL?-aUBkpBDQBO*#;M3)(u z7MLCN^}96#tv8MWdJhmLY~8e!_lPku@TOxha>T#&*ROD>OV<*PUKCw9R96TDj1)S8ne)--{#BdJCrYfKQqsTG_ATb zU}wv!3QG~=ed3k7485; zY#db0TbYN%AI8!6k{S0K2nSinV9*eN6WgqKIQ{D!p2?`-PEwOB55j4&oX(B>=e6;P zK3De1^)vuVveZ3>>Tw`ey_jOT;fm%&D6y{*GlIr`_%|7S+)rq#I??#_2}p9#Yy-YpoqypocOvqhFoZ{eQ|qiaZ`wx3`1 zkV_?yewd69&gLDfluC)9I(p^o z>>LM@HBEj%!P76@H=qVR*9IAUDVC(|)!CqTmYLZ`g+k-{^=3QfdRlu?OD-`neE^Y+ z3FJ+?OQoGvJlctLow78DyfP7;8Zd*}G|#Sp#uxkh91Zz>d;16X0M7=PT}k9667mXk zdyj74PDw!(%zT1^k9fGaGAc=y_7wl~I4MsWe!4ZcwDfI5k;&}#ZDv~x z;)hkE=YePX#&IPy5hjT4m`=_Oh)mcZnAMZ3tE)2=6_SiFVwKE-0-gW=7XX|BjLnCk zwtNS-=H%riKZ7VK!ZU^?CMG_Ir;sC|X#Ad~mGC|!B;;Bf5wdX{ChWo2(TBdivbG>j zh6r1(`$qZ%?J=S`VtTA7hX%dN5uFD*ml4h9hn*yYz}iipkg!vKYi_0lXsm}o7q6+M zCEF6o%-9N(nGcgm1{^yupL09nczgWIMEp9yFws?+v?@dU-i z#c}B5oD@fnp?h#>a`gub3(Me*Oir3-PoTW}EJ`*06~x%#If7@IVH_5cDb4Zm3?oIS zUcP)u=XP}9%?s+lz(mrO^idb}RZ-$A@Yp2E^)&XRZHZ!>*#!kOQh}+BLRf+*F^bty zUjXBQXK2_9du#@^%V>$pbv~`-@aW1xpoGK=1-qE5p?fo#!H0h94pbO>K89w3_^wLu^vSn z$EiWzEL1;!Ri8^T<#;(leT)*@psJ>p4CdUQPaKz;$^s$)Y-U}99~AWletvR$b;=;Q z(SYY>Jd16Zps;uV`Mx%E2}mEaQGx_09#Dg~wC9wJj2JbvwZYwze;{THAdZNPJpB(O zi`>bQ*ZU&m<8&1G^ z5tDEsK*O@9GM}*~H_6fnw}P%@EeZp;BI>e1JK(spGBLQil!Lm{iG$Kr;Pr1*8feY~ ziu2C9TwA*OBoLSk?VOLfWPKzi*wp(w*AY*7g}dJB!&srn!ZMhm^tW zwl+4S560_*65hTOJJFRg>asn~$BxyRnw`DPpVbS}27S%>6xZ6_bWmhRpn_Eg5JfXB zhlEvE%Y6c=IM{otfo4Q_xZzrqhGQrVi+~QWo)oZe>&c(dvf-9e5p*$lPtwzi*33VF z;aX3cGim7PFrZ+gPi2DhpNhj5o_9>2$kQV-FF{nE6=wjU4o7I#!8Kfmg7{e9L}Nss z!)a80&X)yZ$S`iqHt2*d;fn85>U=}y>QU`%_;%#jvWF~R)<$&T!Q{4AOSTPEBv7DF zwAhv&Y7v3R`rB|DsvKO6(iH@+o8iVR<C zp@R$tnh-^DjWb`1!d!A%8Z1U(yXjT0H+h`agFHECMda1;Q5BMdzj6FGihlA_Z@xx~ z2<wd%+Ps`+pa&s|rF>?MT(a9-=!^B$ zJ@mCU51dW)WSwF8)DI#7V^MMX+PxHv#h`%_TIophVjzeA3w39=4pN4O2zzJe{8s>$ z7?ID67ME964rJ%#SogPR0ljnVO-L~=2ibE_#A(zgidnZ+ThCa+if2F2!M->qMVxy3 ztH&}CN*o#pY^Uwnfk22`<6(1i^YI#hdBs&&-Wh_R0vtdji~v!D4v)FQQBgpK;HV*| z3bC)A_yDnUT4Y)=SF65$?KVWp&V!8d?_(KRva^TVW@LkCrG;~IYz8`^>ER`EBhX!l zH24nGi8#-}2vjGGFEXiPFqiJ3l2cz#ZDLExU?`hHY!DSx753oV&xX2W#lYHj-{e5i zJT;}yuc@QL!_R}BCv2959HCQj`8*MN6_$KsdS=X*YUN?HzGvHAElr*BG44sHoVw3HmBaac_J|-JfNTBAo!VY^t^< zcIM;f_cs0LVbiOk{PgKllcl{uwZEv|u=gZ}0#otmZkSH4r=3l2X$^Q7!o&6LBM~)` z`g1am=Q=ejggF_GS@jT>B&-M$za@z54ZXd01I$}r?}bbJ{SQQ}F4R5F7I?6_j$?);Up2JcMZx@v%<&Z z&1c23#^T6WWgdUX%w9_aj?Mbzu{tu^cvOsW3TB==!;Mg0Ov-;q=S~N_|WW2TfWh(@_R&h$6Jj`Qi1qF zP7DGv7p~#{evelM?(|3ve^u_cyO3q5(R{|7D|cO-Wx7wHd85Hh(Kye+Z+t}g46t5-b}_L$Ba z>Ec)e0-M2K5zaU=vvP+%X%Asy_o-`0{cS+;x_-gc>jjXy-!V}BFcdydJDKgGV2psK(3lnR< zkrs>DUKFMCSkLJtykcJL`k%hMmS)@)-5j{|2)*vT)%u{0Biaz|DR*~wyOlSHe|^gP z6DV%?CN}obJE2xqP}5`{?E|t0I+rE1wd(1f>;8YDNx)R3?MjEfldQ`C86W+-IOxbY z-;g=FPLLwl9@siK2b_#ycGhGN$gwK`G7C_Axy2p+HpxH$U|`+dCbpvo38rF1Oqu{4 zcLDi6*_oX3+qj~z?=5S?i-R+g3;%iLX@B|pbWoVn=$ zzUBWv7_GXHTxlI`TVZp0cY|p0Wx@Zn@L^KiE=O@@^K)97nrSos*Xz@N>k?6u?8`|> zN}_tfhpDevVDAk=J007SQpjDDuJ%}~W@S1Tk4VEg^!?!cA`Jwf6R1Glez0t8xpFM6 zQ=Gj2CVQr+011R}%5+4vQbmqc z04oKQKug_E&F>Tzd;lpwI@MUzR`d4lUj#q!lx|w>h&oCDbCQvf*Kc<^LH+ff6KO>I z5u~~KQ>#8#RaIZ1kVu@sx=&GiTYi4Ngb$w|3Ee{wVcWa76ij{D8cQ*Z#~2tGeEa_W z`$f&yT2aO=fY=wGPa7aR;O61^pJ;X=9le*cJq|hxyb0^ zH+)sG0QC1NAFM@Bwr)BrbZD~F_7>4{<604i(F6XP-`$n4VwOP* zcL`7*dSBx+i-73UY>?YwK;hVgx_BGD0#YT$sB@Rs81ZCpxt-2f0BXY_&zl+-w^1h* zD4}z;D5^FqD@#B@ft6B;iE8=@_Mmw$1f`))oP2jQUyl+mDj$7vUJh=eY4EK&CM5qh z2-GO>p_vkSoL(@+c*^O;H8h+l&%Z9$%POv6K=(Y_&ir(3Sn0OPAv=3uvECl!bUx(g%Yy?F=UDP)kq)3zqWB1E#At(M4~0BV^t;zkv&Fnx1LF@+`?yz5H|QA9 zbX;zrA|W^<#>_C0kngzhB?Hr zn1tgk;u;+k%v6#1 zfONh24>@DqnWUB6^V*qsHv4hcr$~mv;rjmece8C!vSn)DXX@FbN17mU)=BlbK0jbF z*Oh7I>+36=l{rKamgA;kEC`U={KSmz{OhP$z~+au`xlEH`f}ak0(!(k9K;#E7638o zo@hEiI=tO=g|^)?y++UPUvOssm0Qf^tYz2LD5iHOmzb;bJG0K}?Ry zGEa|*R-)LHW})_RQ0g1oZKHWl#)d>tz`<5!o$$($oIOj#ji0>az~XO^YF_!nR$SNo z=`Y8D;5|!@80h_K@9eD81+fV}nNSOIF$l6~udN(Z70fh$b{%y^p z0P|}x-)d;&R<7`8Ra>t-1Lf_HlBFS7DRS`j%8Y#|MY9ABH~wjrBE4!iw8G4`C?2iG z_T=c!ZjTeQhk(i>={WR2u!qoTegwsjd9m^O`g-B51y3GsZb~#GmV#0=<)y0(Nmf85 zU>Vpn$ovg7Z6M9I9VoclNxHuenrhQ}m+zY2W(7T1(27AIy1Lq22(W$r-at0?_Zwwf_{jWrRQ<(QOgsK%HtlPyLG5C} zlDv|B%*uD2^f?<^Y^B9Eamd0FL1}R80yK~mZunBwHc?}L&2*-__Ds&DzZBd@P(9z1 zQSt|!^4r>bA9$BrWDYFl6ZuW*Gx;U z{Z;>^@VtiHtZ*5vGN*rASRUwlGsqvIVi3CK2MJHddfL|Zck4+GEPeGhG1~c7w=!$j z)6>%oraO9tqV7=Y1@zBqemnq+Igsb)r)&4cbG+#Rt9DEVz{*=h3|asnswg*Nr@Qeg$phpz)|CzU$u)4Lwy63 zyDMv_apP$j>(hr*yoz6V6chqJGn~DhVH~djU6pu{4)hmtzW@_oP4&cG{_fkNh&@r9 zgeB}15{4%idxR4%+}*oTKDmY(nx5t~bOSpz2`SCZuVuey1ogSsbGpN^o&S^nnnRNy z=i%;`N}SK)7og!OA%2!^ouvrIc91=eWSMMjYZKbpEDWr%D8fTq_Q~0{q|%tfyuA}N zI{y>dgU^=)lAo3ms|n5HPA7S^KR>Rs}IyPl&0AQ^kt z+#U^_*iTq0i{kdjzya^$QGn$5Mh&W{`qX?zL$dX=ZF{maic^OPo*QpA{0UX}zk5T% zc;nz^nFxR&?$`PeU?SBbS?t@lWJ@BLOSyNdUI(0%I99wus*WCjR}mRZ4X2Rj?(xjel22Kr*I@V_qiTXw1Z znzHceoSahKph5j)v|<3epx`2*tefp<<=Mb(p?G7Bm`f&v;AJ}|DVLG!klFBqhMzHc zIb(AP^2bZxW)xlH47^cgFQ8o^N40inmX-)hf7kTx_ICF46iKi8@%Y2y&PQlh1d7_f z!VwgpfeP;vm~N=AISr`$QL^W`36&l;GWn{lVMlB6ix<~k6zz60|7jHkyKAdGy4$*V zgw!Op+cuS55A7o}{`_E~5JaqbON)?QHv$BGNUTyO&u6Jl1bX^9$^Sal(;i?+lq@U9 z0(=#sbDW<3`d=5ishXPFH*kOoUDjW$DRg;t(}Xy$?sHlN7Ef29)GXEhFkH^Le8|Zf z4AuV7XTFj*YOv>q^4;Vk(YbDT%LShM>YpSJcH@>;v9aQxw(I6?M%l0ccg>&w=0%Ax z`)Q{u9Oftzb~~XJ=?7@oau%nr())HO2Lv4XCz9%=XDS?2x2MU=LNBmmZFe?-@=Ht? zG*Fn~{pl^xde6sXo7@6K_?G($l2j|{gx&N`xKR0k**?pbd+n>;P;LRRy~^xdhO@6O zDA4&7Lp?LEmUfgA_Y3->G{9Ths!)6Z2=H%zp?RHd3>TxzQ3fwh?9U=~B@@fkyu3VS zAb6z!xwYomm%$JEHEa=$JZu%Fg}h>dHS2u!=VPX2_T9P^-MZP!w34~cBO|Xf!%G}4 zafBQ!y9Vy7gD~}ypjxA>8e$G8c@D@-#TI7cxiNrUu|-Fnm2i;c*qcune5eYU;4n-W z+Ai|r@oG@l6R3|&Ax#P(2c7YPeWG($W?YOqtM^nWye^@=|2Jm<#_tyN4~^n*M-p02 z-YxIQ;u9CY_&b#QWa^_rBj_7fLrwCX4&mfO85->GX4N|$II$0r?upRncu?Nwz>?MSF;H^&Q(m52 z@9?n02P@HWS(A8F)J&XtNgE_e%~oN;LPB?b$TGb9eQP%V7R>NHkcC##zfF&nY)l z{=9ebK>IlL5+JD0D8F13BpGh9Wd^5PmFOv?7DA$g0j%gt65ZjsQcAnLQoB^ekV0S; zWbsTE7O?*3DS~%W@qV+o23A#?qo(76*8s9_e6JB(t98vQkHD-7>ZOx-CTwA+BH=(# z<>hC`GoL75rll#^z*{-Iw2K;78fbq*7Q+e5$8UXdb8{hvhzrpo!4EH(RS#-Y&?EWQ zTAz%`^jH;klz;vBagFKXz2~PFpYQ#B_yrXd*8)w!0{*V=M>F;Xw(qP7Y+?c<)D5!d zt^h6Z9z?-*^uh10`ZUDfhXf(^BCZ%QhrvDEguXyA;tXe}Gwjf>^B24xyaUFXYiFGN zJ}BK!g2eb}-Aj&X_30NFtm%y#>l_@?lo{^Os{p&mX8v~;Dd^7Vxe(=FKYu2vQuLqL z3^5SUiV2}%`+@4ccsLRpdSv?R7*3r!K<6;iiOx*=x z;pK4_+7(YQfkz?iK?86a^mYj`DfoR3^OREyqV=akUmKz2=XW6b2I*o%Ay<#D`lK}f z-RWuT^ukr5LT6AmL$E_L3cI$%pj_;+hUY29dB+z)Nt>T&Iwvo4;;tJ*f)1NJ(gmj|BismqJ;#Uq(_( z9F&RN|I^$l7Y#WXs&u){k@}{i$|wxzfn`C0Nf6_(Z!v9&HvykpP&wqpN0o=bOeF$i zcY$M2={=(pi5m=70EEsSH*ej#3u1igsMAjHeh-Z^5)Qp|u(L2Oca*PbXq%o(QhUV}vl0h|xn7mN0MQ-uiurDvE*%m53RcAneS&i>`evle)}D@!r{t7gG=! z5z(Z9W#_C!KR9b1$d>J|o#*B%8&k`MRQY&B(LWS{p zH-V-6(+o?*3X)Vw79gOcI6jbPkpHJwwIpK)rD*-ipuu(M4dsYKLXQAanJt1G4HUVI zf%Dn&MH^&-f%_t!~p<*o6D9JcXC>jn~YvNFrY&DfVoii9(P$^m@ zr*bBd>}jGfO|(o|${zAq5;e$Hj&nTkuX(QPcU`~h_t)>gT%B{D`<(l`e7>Le>;3*C zUh?P4zD5&ry`XJmBoan_F(}lZ{D{|_GwuQ#*l5_PA!FWirl&ckX}#n>N=}Kek5OgmB%cM0^bN?2D2$DPH;VBIb%B=DKUweg%`w(Ps$&Y(E9Xf9ctNiz=1Xm27k3 z4!0y&yqt-EoCgKHiRvvADdLj@#7hNBMIw6AqR~R7l3aFNA|Pp`K~+Q}73p6>4Qdco zusrZ)Mtk12wVlvhK6d(X@5%z?ag0jT_|o&i!B0kPH4Upkz!cm#$vP6A$G>v(U}$xb z6_MjnrIw18NHn+x`u5!yb&>uwSC6lS6%`do?kbHlZNm%sBWSvZbdN9m6i36tnxO5= zNY<<|w?(}JPt#pA2T2B(3V!gc+`POl%)H11Q#RaIhkk7jQj6Wiu7Mnz{g3lw2lcI2 z=G3c96IMM;8s(sHmSRHRW%57%kHNqc3ra5B7WF1#_hXmNLut(~sQmmqkvt|IuV-Fh ziM*@>7~-^OnSg+RlpCetV}#0v4IwS4&Un=AT}EToN(patT|99^hTGU2a^F?_PX|>b z&|EId&d*=09j(X~S-BlY`v3?>ycPAbtzu!T`U+V55l=kY+QJy#G!47Y(h_Q}sCE9Y z%NwjC>%2t>ncdN0W1qQYN=u_n;;BmKTsWGYGpAt~jHsj4GF)5+b@lWFQX2k!l|>>| z@cmrD@{7t+2r5o=mTZtne{$$6);zB#D-4qGV6*Z-2ntC8wPturdi~mqeJ)>c9EQ|d*%^24S+TBz&4K@7n;j2g4QQM~aww6~*t{gi zC`s^3Ie7BlAK;qms4^P6n;(0063~92rNyN*R8&KV(MAj)A}^@n8*V)Dn`} zra##moVyK`7o)OOh3TUN!iUIB;VI^%%x)CgOa2nh!SE}=c05P6uzuCkji{k($XvCT zNEdnoh}nNJNX5r??pME$iL&?c(X{(%jkF|tkw@e}nhaTxMh1H6Q76tJ<*rn$gc`=u z_ZCfmbs$3TN(gXw{emMft9ND5E&gZX{pn8Y`mm;35SdpY z8Ys?r={rm3XA|{iRp*bJ?0spHE`Wd4<~DIj5!d5rgcvc1C|XXCrzHbGT{B3Dt{*Bi zS-8JOZJa-taZsSvJWmf_*kgf+|413_j{NsY?GW#xfYALb|2N)lyr5G$@k%H};5ZYG zbZv7;^sUJZBYiZ{(ONgG?Ab$Ax}kx4TW`-j z-SZQjiy8`0gv>qRP2_6o{ssZ_R=!JK1D5xiS)uNtsakJz?1rQ&dv>we(yCjWNNf$) zDf+TeAoI-D&rcWQqWhnv%5{6T%9Gp(ANO?->aNU0FETg_1jTv=HEz3KdU)q}Z7Pot zZd&nYtL6su%OhEPZQXVfUcCk!yhPhS6nVai&Wl+%vtHfG&OQ+@yZ+dhL+2qS|0&1} zPwH+9yogTWz+9NHbO4c>cjZ1*0q&6c`ol+MY}KnZ;)zyLj9tMpu!Pc8tLQp@0Ikrr zTVk`J6E8R2a>&h(q3-O9`ut4$o9Ma+B#h%ifR4LY(Z}wGyo;6HKTxdwK0cOhpQdet zdKQfuZ96c(Yj}6a1hK)7Mm$WUPMv^kjeX8~O|zAJbxIQ{;M=#e8#^4dl=5&FL@9hX zr6&9(kUu+B0-%16_YU|;ddyy%y!txvzO&8Oq}y=4?lto7h=(ldR3vHWI^-t5Vm~QF z9aj-uXB^aK0QA*Wf!pCG#w-E|ep4#4@*q`1obqQhi?(*kWjKyPjr^c+Rt{bwB5z5V?BXrpCDnDj_A;}*CP6)-mZhyG>wwdB==7DbgE{owdx&>bHY zdC2A!*9$gFNlBHI%H9f>Cfe%t<5iT?V`BP1Rr@pFtY zqX}h~+Js>1M3615%W1*OaV?)2casEn7#eP#l@SBExDTa&(L{%GKbU#h;1zrKXEGqX z3K);9DVx_86@CLp+fQaa^7opT;dK)3L#B6+i0tyMenQ8V@i$bS(JQX1Knoz7QK?S_ zcstKTlxiEDFDD~-v66PZJC&Y;-O?xg60aedfN9qs(rRksk%pK5j$jHaIm(pcG>e<9Enr~8}(Ye z#h~s-$>w9@Xu0T!EVmWx-3CRnz2)I5J3@S zKg1kuZK2tQPxP6H&@5Y$0z1RuJLnolX*WJ%Zt;G=qY$SQgy zzw;_)pN`{^EoVc85#8cwWhFMukLx8HS>lD<@FSl_y*veEE-)&N3*mdVytyjMl%#uj zT>I{lG7?t4v&&ar%de_mBW|!IFas zX2c~M8XLO@VQX}{S>oNg7l|J9ldTGLeKmp}KkUqPbhCI!#KfA~r^D$vhEmmcD{^xD zx$*_tY`xp~Gvp2l%+U098rjZ47$x>Nk3y#T4f&sihfXf{`#UX!w_v(xaUl$SyhT*h zmRy1P7sn_iw&_<}*a*(UrL@LWiY-UQn}iK|-2OK`pp*|j4jkm)NvS6n1qf&n4u`?~ zFX38INvS2&$NlE0{+1ItpU+hEc`CqOvC<8?^{Q$>eTY@UEatk9=+abeKW>Sk@=+ zooO^z1N&E;*$ zp{O8o3vSnEne`}b*RBqeBbc_LfcjD;jOOd!>DHb^fygRQ_+ZtKy*sGQnI6n~B+?}L z&Y_B;y+D%E7ZZwc<70uDuzT*GL+aJmuwN@-&(HQ5*hC!qdfQZUxrf#@Fc*B>8_sSwD*sX9y@uiNhb~gvqEzQZ4!H=Ho18E z3bj=c|HVnyt5g)RgWbc}UcB*-!I&Fbrlv6siSb3Nx!@MXZn7vdX`??{EWR`K-1inT z=WY)};h{#ly;Q#`0`elq1nKMCKN9`^FC8nYYxrP%{;!SmOv7S361apW6ob27gMDtw z_4vh^p{yN{w^0k`&SJh8vSvTrDi}+WqnFRBcm;Y*js_+nm?-Nd+M1@rd2@Od?E&0t zR;&$6bq9e9>VQ5unl~e1;^bq-Z}d#seQANZ4?fMGAptbTqivT67Ser2%GYq+S2u5V z;hX4q)!gDN!5V0aQ_VvDUJ1hV`lxc&bPuvAl)H-zNqEf1K#IX&F#A+h*nj^6?awpP z`IB|n`FhfQ+`SFc)6zcmsVcDL3a3W_nyulM-oGEA0N@_+MV;@v1Q;XpMx&P~>E8c8 zvkybymentc;d_6eWp`D%xH#*P4H(Cfk&*fxsr@0yAG5WyYk4Cr%Jx7Fb&f>Dp?=AQ z^2?o8Fr)o4EyKA%oD5MYgco|ipMlsT?9tP1Emy{LvS9^^X60uH-}2jK^8qIj-oEal zNpZmbn$THRaW9~KxH&Uim+wwi8qiG8BWocwQvU)Sz7#g@R#duu5p_-K0P~mfhh3|# za0gPaVDXPlx6{UzJIVr&7R=cH0C_|S A9RL6T literal 0 HcmV?d00001 diff --git a/test/grass.cpp b/test/grass.cpp index 8d645f08..511b30ff 100644 --- a/test/grass.cpp +++ b/test/grass.cpp @@ -39,10 +39,14 @@ using eve::graphics::Shader; using eve::graphics::Texture; using eve::graphics::grass::BillboardMesh; using eve::graphics::grass::Point; +using eve::graphics::grass::PackedAtlasInfo; using eve::graphics::grass::SampleParams; namespace { +#include "PathBesideSource.h" +EVE_DEFINE_PATH_BESIDE_SOURCE() + void makeTestPlane(std::vector &pos, std::vector &nrm, std::vector &idx) { eve::graphics::grass::makePlane(4.f, 4.f, 4, 4, pos, nrm, idx); } @@ -74,14 +78,21 @@ TEST_CASE("graphics.Grass.bindDefaults") { CHECK_EQ(sh.getUniformIndex("lightGreen"), 6); CHECK_EQ(sh.getUniformIndex("darkGreen"), 9); CHECK_EQ(sh.getUniformIndex("frameCount"), 12); - CHECK_EQ(sh.usedFloats(), 13); + CHECK_EQ(sh.getUniformIndex("atlasCols"), 13); + CHECK_EQ(sh.getUniformIndex("atlasRows"), 14); + CHECK_EQ(sh.getUniformIndex("grassVariantCount"), 15); + CHECK_EQ(sh.getUniformIndex("leafVariantCount"), 16); + CHECK_EQ(sh.getUniformIndex("leafRowOffset"), 17); + CHECK_EQ(sh.usedFloats(), 18); } TEST_CASE("graphics.Grass.paramNames") { - CHECK_EQ(eve::graphics::grass::paramCount(), 13); + CHECK_EQ(eve::graphics::grass::paramCount(), 18); CHECK_EQ(eve::graphics::grass::paramName(0), std::string("time")); CHECK_EQ(eve::graphics::grass::paramName(5), std::string("alwaysDark")); CHECK_EQ(eve::graphics::grass::paramName(12), std::string("frameCount")); + CHECK_EQ(eve::graphics::grass::paramName(13), std::string("atlasCols")); + CHECK_EQ(eve::graphics::grass::paramName(17), std::string("leafRowOffset")); } TEST_CASE("graphics.Grass.spvMagic") { @@ -222,6 +233,19 @@ TEST_CASE("graphics.Grass.swayAtlasFourFrames") { CHECK(opaque > 80); for (int f = 0; f < 4; ++f) CHECK(perFrame[f] > 10); + // Filled tuft: the root band is denser than the tips (not five 1px sticks). + int baseOpaque = 0, tipOpaque = 0; + for (int x = 0; x < 32; ++x) { + for (int y = 24; y < 32; ++y) { + if (rgba[(size_t(y) * 128u + size_t(x)) * 4u + 3u] > 16) ++baseOpaque; + } + for (int y = 0; y < 8; ++y) { + if (rgba[(size_t(y) * 128u + size_t(x)) * 4u + 3u] > 16) ++tipOpaque; + } + } + CHECK(baseOpaque > tipOpaque); + CHECK(baseOpaque > 40); + // Frames actually differ (wind lean). int diffs = 0; for (int y = 0; y < 32; ++y) { @@ -234,6 +258,55 @@ TEST_CASE("graphics.Grass.swayAtlasFourFrames") { CHECK(diffs > 8); } +TEST_CASE("graphics.Grass.fileSwayAtlasPacksFourGrassAndTwoLeaf") { + const std::vector grass = { + pathBesideThisSource("Texture/grass_sway_0.png"), + pathBesideThisSource("Texture/grass_sway_1.png"), + pathBesideThisSource("Texture/grass_sway_2.png"), + pathBesideThisSource("Texture/grass_sway_3.png"), + }; + const std::vector leaf = { + pathBesideThisSource("Texture/leaf_sway_0.png"), + pathBesideThisSource("Texture/leaf_sway_1.png"), + }; + for (const auto &p : grass) REQUIRE(std::filesystem::exists(p)); + for (const auto &p : leaf) REQUIRE(std::filesystem::exists(p)); + + std::vector rgba; + PackedAtlasInfo packed; + eve::graphics::grass::packSwayAtlasRGBA(grass, leaf, rgba, packed); + CHECK_EQ(packed.grassVariants, 4); + CHECK_EQ(packed.leafVariants, 2); + CHECK_EQ(packed.atlasCols, 8); + CHECK_EQ(packed.atlasRows, 4); + CHECK_EQ(packed.leafRowOffset, 2); + CHECK_EQ(packed.frames, 4); + CHECK(packed.width >= 512); + CHECK(packed.height >= 512); + CHECK_EQ(int(rgba.size()), packed.width * packed.height * 4); + + auto opaqueInCell = [&](int col, int row) { + const int cellW = packed.width / packed.atlasCols; + const int cellH = packed.height / packed.atlasRows; + int n = 0; + for (int y = row * cellH; y < (row + 1) * cellH; ++y) { + for (int x = col * cellW; x < (col + 1) * cellW; ++x) { + if (rgba[(size_t(y) * size_t(packed.width) + size_t(x)) * 4u + 3u] > 16) ++n; + } + } + return n; + }; + // Each grass variant is a 2x2 block on the top two frame-rows. + for (int v = 0; v < 4; ++v) { + CHECK(opaqueInCell(v * 2, 0) + opaqueInCell(v * 2 + 1, 0) + opaqueInCell(v * 2, 1) + + opaqueInCell(v * 2 + 1, 1) > + 80); + } + // Leaf variants occupy the bottom two frame-rows. + CHECK(opaqueInCell(0, 2) + opaqueInCell(1, 2) + opaqueInCell(0, 3) + opaqueInCell(1, 3) > 80); + CHECK(opaqueInCell(2, 2) + opaqueInCell(3, 2) + opaqueInCell(2, 3) + opaqueInCell(3, 3) > 80); +} + TEST_CASE("graphics.Grass.layerFlag") { Shader sh; sh.setKind(Shader::Kind::eMesh3D); @@ -358,7 +431,7 @@ TEST_CASE("graphics.Grass.gpuRenderScreenshot") { constexpr float kField = 10.f; auto *ground = Renderable3D::create(); ground->setMesh(makeGroundMesh(gfx, kField, kField)); - ground->setTexture(makeSolidTex(gfx, 92, 72, 42)); + ground->setTexture(makeSolidTex(gfx, 68, 54, 30)); ground->setPosition(0.f, 0.f, 0.f); ground->setMetallic(0.f); ground->setRoughness(0.92f); @@ -366,20 +439,31 @@ TEST_CASE("graphics.Grass.gpuRenderScreenshot") { ground->setReceiveShadow(true); GrassField::BakeParams bake; - bake.denseRadius = 0.28f; - bake.sparseRadius = 1.15f; - bake.maxDense = 1800; - bake.maxSparse = 80; - bake.width = 0.42f; - bake.height = 0.78f; + bake.denseRadius = 0.10f; + bake.sparseRadius = 0.46f; + bake.maxDense = 7000; + bake.maxSparse = 220; + bake.width = 0.58f; + bake.height = 0.98f; bake.seed = 11; bake.minSlopeDot = 0.2f; + bake.grassAtlasFiles = { + pathBesideThisSource("Texture/grass_sway_0.png"), + pathBesideThisSource("Texture/grass_sway_1.png"), + pathBesideThisSource("Texture/grass_sway_2.png"), + pathBesideThisSource("Texture/grass_sway_3.png"), + }; + bake.leafAtlasFiles = { + pathBesideThisSource("Texture/leaf_sway_0.png"), + pathBesideThisSource("Texture/leaf_sway_1.png"), + }; GrassField *field = gfx->newGrassField(); REQUIRE(field != nullptr); field->bakePlane(kField, kField, 16, 16, bake); field->setTime(0.22f); - REQUIRE(field->getDenseCount() >= 40); + std::printf("\ngrass dense=%d sparse=%d\n", field->getDenseCount(), field->getSparseCount()); + REQUIRE(field->getDenseCount() >= 800); REQUIRE(field->getDenseMesh() != nullptr); REQUIRE(field->getShader() != nullptr); REQUIRE(field->getAtlas() != nullptr); @@ -411,8 +495,8 @@ TEST_CASE("graphics.Grass.gpuRenderScreenshot") { post->setReceiveShadow(false); auto *cam = Camera3D::createCamera(); - cam->setEye(6.4f, 4.6f, 7.8f); - cam->setTarget(0.f, 0.25f, 0.f); + cam->setEye(4.8f, 2.9f, 5.9f); + cam->setTarget(0.f, 0.45f, 0.f); cam->setAmbient(0.18f, 0.20f, 0.16f); cam->data()->nearZ = 0.1f; cam->data()->farZ = 80.f; @@ -459,9 +543,9 @@ TEST_CASE("graphics.Grass.gpuRenderScreenshot") { } } } - CHECK(greenish > 80); - CHECK(darkGreen > 4); - CHECK(litGreen > 8); + CHECK(greenish > 400); + CHECK(darkGreen > 8); + CHECK(litGreen > 40); eve::image::Image::create(); eve::image::ImageData *frame = gfx->newImageData(); @@ -471,5 +555,15 @@ TEST_CASE("graphics.Grass.gpuRenderScreenshot") { savePng(frame, outDir + "/grass_field.png"); savePng(frame, "/opt/cursor/artifacts/grass_field.png"); + std::vector atlasRgba; + PackedAtlasInfo atlasInfo; + eve::graphics::grass::packSwayAtlasRGBA(bake.grassAtlasFiles, bake.leafAtlasFiles, atlasRgba, + atlasInfo); + auto *atlasImg = new eve::image::ImageData(atlasInfo.width, atlasInfo.height, "RGBA8", + atlasRgba.data(), false); + savePng(atlasImg, outDir + "/grass_atlas.png"); + savePng(atlasImg, "/opt/cursor/artifacts/grass_atlas.png"); + delete atlasImg; + win->close(); }