diff --git a/.gitignore b/.gitignore index 792276ae..15141092 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ test/assets/classic/.downloaded # Skinned character: CesiumMan glTF is committed under test/assets/skinned/cesium_man/ # (see ATTRIBUTION.md). Re-run scripts/download_skinned_character.sh to refresh. .vscode/ +.local-tools/ .superpowers/ artifacts-local-test/ imgui.ini diff --git a/examples/building/imgui.ini b/examples/building/imgui.ini index 01a61e2b..84fe625b 100644 --- a/examples/building/imgui.ini +++ b/examples/building/imgui.ini @@ -13,3 +13,13 @@ Pos=12,8 Size=803,322 Collapsed=0 +[Window][###map-input/Window] +Pos=24,88 +Size=672,504 +Collapsed=0 + +[Window][###hud/建造面板] +Pos=724,20 +Size=532,756 +Collapsed=0 + diff --git a/examples/castle-generator/main.nut b/examples/castle-generator/main.nut index 825c8644..89371e34 100644 --- a/examples/castle-generator/main.nut +++ b/examples/castle-generator/main.nut @@ -65,19 +65,21 @@ function rebuildCastle() { if (castleCamera == null) { castleCamera = eve.Camera3D(); - castleCamera.setEye(54.0, 34.0, -58.0); - castleCamera.setTarget(0.0, 5.0, 0.0); - castleCamera.setUp(0.0, 1.0, 0.0); - castleCamera.setFov(43.0); - castleCamera.setAmbient(0.23, 0.25, 0.29); castleCamera.setActive(true); - gfx.setDirectionalLight(-0.55, -1.0, -0.35, 1.45, 1.30, 1.08); } +// Default far plane is 100 m; a rotating keep with outer rings sits past that. +castleCamera.setEye(88.0, 52.0, -96.0); +castleCamera.setTarget(0.0, 7.0, 0.0); +castleCamera.setUp(0.0, 1.0, 0.0); +castleCamera.setFov(38.0); +castleCamera.setClipPlanes(0.5, 2000.0); +castleCamera.setAmbient(0.23, 0.25, 0.29); +gfx.setDirectionalLight(-0.55, -1.0, -0.35, 1.45, 1.30, 1.08); if (castleParts.len() == 0) rebuildCastle(); gfx.setBackgroundColor(0.10, 0.14, 0.20, 1.0); function eve_update(dt) { - castleYaw += dt * 3.5; + castleYaw += dt * 0.22; foreach (part in castleParts) part.setYaw(castleYaw); if (castlePressed("r") || castlePressed("R")) { castleSeed += 1; rebuildCastle(); } if (castlePressed("1")) { rings = max(1, rings - 1); rebuildCastle(); } diff --git a/examples/character-motion-lab/main.nut b/examples/character-motion-lab/main.nut index 25f935f9..ea53d7e2 100644 --- a/examples/character-motion-lab/main.nut +++ b/examples/character-motion-lab/main.nut @@ -32,6 +32,34 @@ function humanoidProfile(target) { return p; } +// createRenderable enables the glTF extension PBR path whenever Assimp reports +// an alphaMode. That shader is meant for IBL/capture, not this UI viewport: +// albedo is decoded twice, linear ORM/normal maps are treated as sRGB, and the +// characters render as a flat red unlit mass. Rebuild a conventional Material +// so renderScene3DToCanvas uses mesh3d lighting like the original showcase. +function labPreviewMaterial(part, albedo) { + local surface = gfx.newMaterial(); + surface.setShadingModel("pbr"); + surface.setMetallic(0.0); + surface.setRoughness(0.78); + surface.setReceiveLight(true); + surface.setReceiveShadow(true); + surface.setCastShadow(true); + surface.setTint(part.getTintR(), part.getTintG(), part.getTintB(), 1.0); + if (albedo != null) { + surface.setAlbedoTexture(albedo); + surface.setTint(1.0, 1.0, 1.0, 1.0); + part.setTexture(albedo); + part.setTint(1.0, 1.0, 1.0, 1.0); + } + part.setMaterial(surface); + part.setMetallic(0.0); + part.setRoughness(0.78); + part.setCastShadow(true); + part.setReceiveShadow(true); + part.setReceiveLight(true); +} + function addLabActor(model, name, x, source) { local sk = source ? lab.skeleton : anim.newSkeletonFromModel(model); local a = { model=model, name=name, skeleton=sk, clips=[], parts=[], skins=[], @@ -50,14 +78,15 @@ function addLabActor(model, name, x, source) { if (!model.hasBones(i)) continue; local part = model3d.createRenderable(gfx, model, i); part.setPosition(x, 0.0, 0.0); - part.setCastShadow(true); part.setReceiveShadow(true); part.setReceiveLight(true); - part.setRoughness(0.78); + local albedo = null; + local imported = part.getMaterial(); + if (imported != null) albedo = imported.getAlbedoTexture(); local texturePath = model.getMaterialTexturePath(model.getMaterialIndex(i), "base_color", 0); if (texturePath != "") { - local texture = gfx.newTextureFromFile("assets/quaternius/" + texturePath); - a.textures.push(texture); part.setTexture(texture); - part.getMaterial().setAlbedoTexture(texture); + albedo = gfx.newTextureFromFile("assets/quaternius/" + texturePath); + a.textures.push(albedo); } + labPreviewMaterial(part, albedo); a.parts.push(part); a.skins.push(anim.newSkinFromModel(model, i, sk)); } @@ -98,11 +127,17 @@ eve_init = function() { "TARGET / Male", 2.5, false); lab.ground = eve.Renderable3D(); lab.ground.setMesh(gfx.newMeshCube(1.0)); lab.ground.setPosition(0.0,-0.10,0.0); lab.ground.setScale(10.0,0.16,5.0); - lab.ground.setTint(0.13,0.17,0.21,1.0); lab.ground.setReceiveShadow(true); + lab.ground.setTint(0.13,0.17,0.21,1.0); lab.ground.setRoughness(0.92); + lab.ground.setReceiveShadow(true); lab.ground.setReceiveLight(true); lab.ground.setCastShadow(false); lab.camera = eve.Camera3D(); labCamera(); lab.camera.setUp(0.0,1.0,0.0); lab.camera.setFov(26.0); lab.camera.setAmbient(0.38,0.40,0.46); lab.camera.setActive(true); + gfx.setDirectionalLight(-0.4, 1.0, 0.7, 1.60, 1.49, 1.34); local key = eve.Light3D(); key.setType("dir"); key.setDirection(-0.4,1.0,0.7); - key.setColor(1.0,0.93,0.84,1.6); lab.lights.push(key); + key.setColor(1.0,0.93,0.84,1.6); key.setCastShadow(true); key.setShadowStrength(0.85); + lab.lights.push(key); + local fill = eve.Light3D(); fill.setType("point"); + fill.setPosition(2.2, 2.6, 3.4); fill.setRadius(12.0); + fill.setColor(0.55, 0.68, 0.92, 1.15); lab.lights.push(fill); gfx.setBackgroundColor(0.045,0.065,0.09,1.0); ui.setScale(1.0); ui.setTheme("dark"); ui.beginBuild(); ui.beginWindow("Character Motion Lab", "root"); ui.text("CHARACTER MOTION LAB / Quaternius CC0", "title"); diff --git a/src/modules/procgen/algorithms/CastleMesh.cpp b/src/modules/procgen/algorithms/CastleMesh.cpp index 26077aca..69dcafb5 100644 --- a/src/modules/procgen/algorithms/CastleMesh.cpp +++ b/src/modules/procgen/algorithms/CastleMesh.cpp @@ -43,9 +43,18 @@ struct Builder { float x0=cx+std::cos(a0)*radius, z0=cz+std::sin(a0)*radius; float x1=cx+std::cos(a1)*radius, z1=cz+std::sin(a1)*radius; float am=(a0+a1)*0.5f; - quad(x0,y0,z0, x1,y0,z1, x1,y1,z1, x0,y1,z0, + // CCW from the outside, matching Graphics::newMeshCylinder / Vulkan + // front faces. The previous order wound the wall and cap inward, so + // back-face culling showed the interior of every tower. + quad(x0,y0,z0, x0,y1,z0, x1,y1,z1, x1,y0,z1, std::cos(am),0,std::sin(am)); - quad(cx,y1,cz, x0,y1,z0, x1,y1,z1, cx,y1,cz, 0,1,0); + const uint32_t cap = uint32_t(mesh.getVertexCount()); + mesh.addVertex(cx, y1, cz, 0, 1, 0, 0.5f, 0.5f); + mesh.addVertex(x1, y1, z1, 0, 1, 0, 0.5f + 0.5f * std::cos(a1), + 0.5f + 0.5f * std::sin(a1)); + mesh.addVertex(x0, y1, z0, 0, 1, 0, 0.5f + 0.5f * std::cos(a0), + 0.5f + 0.5f * std::sin(a0)); + mesh.addTriangle(cap, cap + 1, cap + 2); } } }; diff --git a/test/procgen.cpp b/test/procgen.cpp index 2bd611ad..bc3f1a05 100644 --- a/test/procgen.cpp +++ b/test/procgen.cpp @@ -369,6 +369,28 @@ bool meshNormalsFiniteUnit(const MeshBuild &m, float tol = 0.15f) { return true; } +bool meshWindingAgreesWithNormals(const MeshBuild &m, float minFraction = 0.9f) { + int ok = 0, counted = 0; + for (int t = 0; t + 2 < m.getIndexCount(); t += 3) { + const int i0 = m.getIndex(t), i1 = m.getIndex(t + 1), i2 = m.getIndex(t + 2); + const float ax = m.getPositionX(i1) - m.getPositionX(i0); + const float ay = m.getPositionY(i1) - m.getPositionY(i0); + const float az = m.getPositionZ(i1) - m.getPositionZ(i0); + const float bx = m.getPositionX(i2) - m.getPositionX(i0); + const float by = m.getPositionY(i2) - m.getPositionY(i0); + const float bz = m.getPositionZ(i2) - m.getPositionZ(i0); + const float gx = ay * bz - az * by, gy = az * bx - ax * bz, gz = ax * by - ay * bx; + const float gLen = std::sqrt(gx * gx + gy * gy + gz * gz); + if (gLen < 1e-8f) continue; + const float nx = (m.getNormalX(i0) + m.getNormalX(i1) + m.getNormalX(i2)) / 3.f; + const float ny = (m.getNormalY(i0) + m.getNormalY(i1) + m.getNormalY(i2)) / 3.f; + const float nz = (m.getNormalZ(i0) + m.getNormalZ(i1) + m.getNormalZ(i2)) / 3.f; + ++counted; + if (gx * nx + gy * ny + gz * nz > 0.f) ++ok; + } + return counted > 0 && float(ok) / float(counted) >= minFraction; +} + bool meshPositionsFinite(const MeshBuild &m) { for (int i = 0; i < m.getVertexCount(); ++i) { if (!std::isfinite(m.getPositionX(i)) || !std::isfinite(m.getPositionY(i)) || @@ -3536,6 +3558,13 @@ TEST_CASE("procgen.mesh.castle.multilevelDeterministicAndComplete") { CHECK(stairs->getVertexCount() > 100); CHECK_EQ(stairs->getMeta("group", ""), "stairs"); CHECK(meshIndicesInRange(*stairs)); + int towerGroup = -1; + for (int i = 0; i < a.getGroupCount(); ++i) + if (a.getGroupName(i) == "towers") towerGroup = i; + REQUIRE(towerGroup >= 0); + auto towers = a.copyGroup(towerGroup); + REQUIRE(towers.get() != nullptr); + CHECK(meshWindingAgreesWithNormals(*towers)); } TEST_CASE("procgen.mesh.castle.parametersControlTopologyAndBounds") {