* chore(ci): add clang-format gate, CODEOWNERS and agent collaboration rules * refactor(modules): clear remaining back-edges and split the largest TUs Three known dependency back-edges are gone (module_depgraph.py KNOWN_BACK_EDGES is now empty; --check passes with 0 violations, 0 cycles): - window -> graphics: window queries the new IWindowSurfaceHost capability (common/WindowSurfaceHost.h) instead of holding a Graphics pointer; Window::setGraphics was removed and all call sites updated. - window -> image: Window::setIcon was replaced by setIconRGBA(const uint8_t*, w, h); the window no longer decodes ImageData. - scene -> graphics: pickScreenAt / collectFrustumIdsAt implementations moved to graphics/ScenePicking.cpp (graphics -> scene is a legal downward edge); scene now depends only on spatial. TU splits (pure moves, verified identical method sets and brace balance): - vulkan/Graphics.cpp (5118 lines) -> Graphics.cpp (lifecycle), GraphicsPipeline, Graphics2D, Graphics3D, GraphicsMesh + shared GraphicsInternal.h. - scene/Scene.cpp -> Scene.cpp (script layer / host lifecycle / serialization) + SceneNodes.cpp + shared SceneInternal.h. Verified with a real build (MSVC 19.51, prebuilt third-party): full build green; window.* and Scene.* suites pass, including scene picking/culling and GPU cases. * refactor(graphics): narrow Graphics interfaces and clear public-header leaks Step 4 — interface segregation: - New lean interfaces: IGraphics2D, IGraphics3D, IResourceFactory, IPostFX (pure virtuals, forward-declared types only; graphics/Color.h extracted from Canvas.h so modules that only need the color type stop pulling Canvas.h). - Graphics now implements all four facets; consumers can depend on the narrow interface they use instead of the 129-method god class. - Migrated voxel (VoxelWorld::drawVisible -> IGraphics3D*) and model3d (ModelRenderer::buildRenderable(s) -> IResourceFactory&). - Graphics.h slims from 24 sub-header includes to a handful of small type headers + forward declarations; the destructor moved out-of-line so unique_ptr effect members are destroyed where types are complete. Step 5 — header leaks (module_depgraph.py '*' marks: 14 -> 7): - map/TileLayer.h, particles/ParticleEmitter.h, avatar/AvatarInstance.h, animation/SpineAnim.h, map/TileSystem.h, model3d/ModelData.h, model3d/ModelRenderer.h, audio/Audio.h + Source.h, image/ImageData.h + Image.h, dialogue/Dialogue.h, procgen Palette/OutputSpec/TextureRecipe/ PbrMaterial -> forward declarations, includes moved to .cpp. - voxel/VoxelWorld.h: terrain sampler Pimpl'd (unique_ptr + out-of-line destructor), removing the procgen header dependency. - gpgpu/vulkan backend headers stop pulling graphics/vulkan/Graphics.h. - Remaining '*' marks are deliberate: tiny POD/enum headers (GridConfig, MapObject, BlendMode, Color), an internal backend header (GraphicsInternal.h -> filesystem), and lean interface references. Verified with a full clean build (MSVC) and tests: window/Scene/voxel/ RenderSystem/Quad/shader/TextureSampler/resourceLifetime pass; broad suite only shows the pre-existing PHYSFS-init filesystem-environment failures. * feat(resources): revive ResourceManager as unified IAssetReloader-backed cache * fix(resources): leak ResourceManager singleton to avoid exit-time teardown crashes Cached CPU resources may own third-party handles (FreeType faces, Assimp scenes, image decode handlers) whose libraries are torn down at process exit in an unspecified TU order; destroying cached entries from the singleton destructor can segfault at exit (seen on CI Windows in model3d.materialApi.objMtl). Keep the singleton alive until OS reclaim; explicit unload()/clear() still release entries during the run. * refactor(devtools): consume business modules through capability interfaces DevTools / cmdline no longer include business-module headers, so trimming scene / physics / procgen / particles / audio / ui / graphics no longer requires disabling devtools, and devtools can be switched off with -DEVENGINE_MODULE_DEVTOOLS=OFF. New capability interfaces in common/: - ISceneQuery (scene) - host/node inspection + mutation + transform sync - IRenderCapture (graphics) - render status, PNG capture, camera control, visible-entity snapshot, entity-ID mask, G-buffer PNGs - IPhysicsQuery (physics) - 2D world lifecycle + ray casts - IProcgenQuery (procgen) - recipe listing, map/mesh generation - IParticlesQuery (particles) - emitter status + one-shot emit - IAudioQuery (audio) - volume / stop-all - IEditorHost (ui) - declarative editor host (used by cmdline too) Providers register in their module constructors. DevTools rewires McpServer (render/scene/procgen/physics/particles/audio/host tools), SceneInspect (now delegates to ISceneQuery + IRenderCapture) and RenderVision (uses IRenderCapture::capturePngDataUrl); cmdline/Mcp.cpp uses IEditorHost. manifest: devtools DEPS graphics scene physics procgen particles audio ui image event filesystem -> event; cmdline DEPS filesystem ui -> filesystem. Verified: full build green; devtools.mcp.* tests pass; broad suite shows only the pre-existing PHYSFS-init filesystem-environment failures. * style: apply clang-format to changed lines after dev merge * fix(webgpu): include RenderControl/TextureSampler explicitly after Graphics.h slimming * fix(webgpu): provide newFont/print stubs to satisfy IGraphics2D/IResourceFactory vtables * fix(common): make ProcgenQuery.h self-sufficient (cstdint/utility for GCC)
419 lines
13 KiB
C++
419 lines
13 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "ik/IK.h"
|
|
#include "ik/Skeleton2D.h"
|
|
#include "ik/Skeleton3D.h"
|
|
#include "ik/Solver2D.h"
|
|
#include "ik/Solver3D.h"
|
|
|
|
#include "common/Exception.h"
|
|
#include "graphics/AmbientOcclusion.h"
|
|
#include "graphics/AntiAliasing.h"
|
|
#include "graphics/Canvas.h"
|
|
#include "graphics/DrawItem2D.h"
|
|
#include "graphics/Font.h"
|
|
#include "graphics/GBuffer.h"
|
|
#include "graphics/GlobalIllumination.h"
|
|
#include "graphics/Graphics.h"
|
|
#include "graphics/Grass.h"
|
|
#include "graphics/Light.h"
|
|
#include "graphics/Material.h"
|
|
#include "graphics/Mesh.h"
|
|
#include "graphics/Outline.h"
|
|
#include "graphics/Quad.h"
|
|
#include "graphics/RenderControl.h"
|
|
#include "graphics/ScreenSpaceReflection.h"
|
|
#include "graphics/Shader.h"
|
|
#include "graphics/Texture.h"
|
|
#include "graphics/Volumetric.h"
|
|
#include "graphics/Water.h"
|
|
#include "graphics/Waterfall.h"
|
|
#include "window/Window.h"
|
|
|
|
#include <SDL2/SDL.h>
|
|
|
|
#include <cmath>
|
|
#include <memory>
|
|
// Color lives in eve::graphics (see graphics/Canvas.h); keep the unqualified form.
|
|
using eve::graphics::Color;
|
|
|
|
#ifndef M_PI
|
|
#define M_PI 3.14159265358979323846
|
|
#endif
|
|
|
|
using namespace eve::ik;
|
|
using namespace eve::graphics;
|
|
|
|
static bool near(float a, float b, float eps = 1e-3f) {
|
|
return std::fabs(a - b) <= eps;
|
|
}
|
|
|
|
TEST_CASE("ik.module.name") {
|
|
auto *mod = IK::create();
|
|
CHECK_EQ(mod->getName(), std::string("IK"));
|
|
CHECK_EQ(IK::create(), mod);
|
|
}
|
|
|
|
TEST_CASE("ik.skeleton2d.topology") {
|
|
std::unique_ptr<Skeleton2D> sk(new Skeleton2D());
|
|
CHECK_EQ(sk->getBoneCount(), 1);
|
|
CHECK_EQ(sk->getRootId(), 0);
|
|
CHECK_EQ(sk->getParent(0), -1);
|
|
|
|
int b1 = sk->createBone(0, 1.f);
|
|
int b2 = sk->createBone(b1, 1.f);
|
|
CHECK_EQ(sk->getBoneCount(), 3);
|
|
CHECK_EQ(b1, 1);
|
|
CHECK_EQ(b2, 2);
|
|
CHECK_EQ(sk->getParent(b2), b1);
|
|
CHECK_EQ(sk->getChildCount(0), 1);
|
|
CHECK_EQ(sk->getChild(0, 0), b1);
|
|
CHECK(near(sk->getLength(b1), 1.f));
|
|
CHECK(near(sk->totalLengthTo(b2), 2.f));
|
|
}
|
|
|
|
TEST_CASE("ik.skeleton2d.forwardKinematics") {
|
|
std::unique_ptr<Skeleton2D> sk(new Skeleton2D());
|
|
int b1 = sk->createBone(0, 1.f);
|
|
int b2 = sk->createBone(b1, 1.f);
|
|
|
|
sk->setPosition(0, 0.f, 0.f);
|
|
sk->setRotation(0, 0.f);
|
|
sk->setRotation(b1, 0.f);
|
|
sk->setRotation(b2, float(M_PI / 2.0));
|
|
sk->forwardKinematics();
|
|
|
|
CHECK(near(sk->getX(b1), 1.f));
|
|
CHECK(near(sk->getY(b1), 0.f));
|
|
CHECK(near(sk->getX(b2), 1.f));
|
|
CHECK(near(sk->getY(b2), 1.f));
|
|
}
|
|
|
|
TEST_CASE("ik.solver2d.reachesTarget") {
|
|
std::unique_ptr<Skeleton2D> sk(new Skeleton2D());
|
|
int b1 = sk->createBone(0, 1.f);
|
|
int b2 = sk->createBone(b1, 1.f);
|
|
sk->initStraightPose(0.f, 0.f);
|
|
|
|
std::unique_ptr<Solver2D> solver(new Solver2D());
|
|
solver->setMaxIterations(32);
|
|
solver->setTolerance(1e-3f);
|
|
solver->addTarget(b2, 1.f, 1.f, 1.f);
|
|
CHECK_EQ(solver->getTargetCount(), 1);
|
|
CHECK(solver->solve(sk.get()));
|
|
CHECK(near(sk->getX(b2), 1.f, 1e-2f));
|
|
CHECK(near(sk->getY(b2), 1.f, 1e-2f));
|
|
}
|
|
|
|
TEST_CASE("ik.solver2d.constraints") {
|
|
std::unique_ptr<Skeleton2D> sk(new Skeleton2D());
|
|
int b1 = sk->createBone(0, 1.f);
|
|
int b2 = sk->createBone(b1, 1.f);
|
|
sk->initStraightPose(0.f, 0.f);
|
|
sk->setConstraints(b1, -0.2f, 0.2f);
|
|
CHECK(sk->hasConstraints(b1));
|
|
|
|
std::unique_ptr<Solver2D> solver(new Solver2D());
|
|
solver->setMaxIterations(64);
|
|
solver->setTolerance(1e-3f);
|
|
solver->addTarget(b2, 0.f, 2.f, 1.f);
|
|
solver->solve(sk.get());
|
|
|
|
// Hinge limit on b1 should keep the chain from folding fully upward.
|
|
CHECK(std::fabs(sk->getRotation(b1)) <= 0.25f + 1e-3f);
|
|
}
|
|
|
|
TEST_CASE("ik.skeleton3d.straightPoseAndSolve") {
|
|
std::unique_ptr<Skeleton3D> sk(new Skeleton3D());
|
|
int b1 = sk->createBone(0, 1.f);
|
|
int b2 = sk->createBone(b1, 1.f);
|
|
sk->initStraightPose(0.f, 0.f, 0.f);
|
|
CHECK(near(sk->getX(b2), 2.f));
|
|
CHECK(near(sk->getY(b2), 0.f));
|
|
CHECK(near(sk->getZ(b2), 0.f));
|
|
|
|
std::unique_ptr<Solver3D> solver(new Solver3D());
|
|
solver->setMaxIterations(48);
|
|
solver->addTarget(b2, 1.f, 1.f, 0.f, 1.f);
|
|
CHECK(solver->solve(sk.get()));
|
|
CHECK(near(sk->getX(b2), 1.f, 2e-2f));
|
|
CHECK(near(sk->getY(b2), 1.f, 2e-2f));
|
|
}
|
|
|
|
TEST_CASE("ik.solver2d.solveChain") {
|
|
std::unique_ptr<Skeleton2D> sk(new Skeleton2D());
|
|
int b1 = sk->createBone(0, 1.f);
|
|
int b2 = sk->createBone(b1, 1.f);
|
|
int b3 = sk->createBone(0, 1.f); // side branch under the skeleton root
|
|
// createBone renumbers the skeleton (BFS order), so re-query ids afterwards.
|
|
b1 = sk->getChild(0, 0);
|
|
b3 = sk->getChild(0, 1);
|
|
b2 = sk->getChild(b1, 0);
|
|
sk->initStraightPose(10.f, 20.f);
|
|
CHECK(near(sk->getX(b1), 11.f));
|
|
CHECK(near(sk->getX(b2), 12.f));
|
|
CHECK(near(sk->getX(b3), 11.f));
|
|
|
|
std::unique_ptr<Solver2D> solver(new Solver2D());
|
|
solver->setMaxIterations(32);
|
|
solver->setTolerance(1e-3f);
|
|
solver->addTarget(b2, 11.f, 21.f, 1.f);
|
|
CHECK(solver->solveChain(sk.get(), b1, b2));
|
|
|
|
// Chain root (b1) stays pinned; skeleton root and the side branch are untouched.
|
|
CHECK(near(sk->getX(0), 10.f));
|
|
CHECK(near(sk->getY(0), 20.f));
|
|
CHECK(near(sk->getX(b1), 11.f));
|
|
CHECK(near(sk->getY(b1), 20.f));
|
|
CHECK(near(sk->getX(b3), 11.f));
|
|
CHECK(near(sk->getY(b3), 20.f));
|
|
CHECK(near(sk->getX(b2), 11.f, 1e-2f));
|
|
CHECK(near(sk->getY(b2), 21.f, 1e-2f));
|
|
}
|
|
|
|
TEST_CASE("ik.solver2d.solveChain.validation") {
|
|
std::unique_ptr<Skeleton2D> sk(new Skeleton2D());
|
|
int b1 = sk->createBone(0, 1.f);
|
|
int b2 = sk->createBone(b1, 1.f);
|
|
int b3 = sk->createBone(0, 1.f);
|
|
b1 = sk->getChild(0, 0);
|
|
b3 = sk->getChild(0, 1);
|
|
b2 = sk->getChild(b1, 0);
|
|
sk->initStraightPose(0.f, 0.f);
|
|
|
|
std::unique_ptr<Solver2D> solver(new Solver2D());
|
|
solver->addTarget(b3, 1.f, 1.f, 1.f);
|
|
// Target on the side branch is not part of the chain: nothing to solve.
|
|
CHECK(solver->solveChain(sk.get(), b1, b2));
|
|
CHECK(near(sk->getX(b2), 2.f));
|
|
CHECK_THROWS(solver->solveChain(sk.get(), b2, b1)); // tip not a descendant of root
|
|
CHECK_THROWS(solver->solveChain(sk.get(), b1, b1)); // root == tip
|
|
CHECK_THROWS(solver->solveChain(sk.get(), 0, 99)); // out of range
|
|
}
|
|
|
|
TEST_CASE("ik.solver2d.influence") {
|
|
std::unique_ptr<Skeleton2D> sk(new Skeleton2D());
|
|
int b1 = sk->createBone(0, 1.f);
|
|
int b2 = sk->createBone(b1, 1.f);
|
|
sk->initStraightPose(0.f, 0.f);
|
|
|
|
std::unique_ptr<Solver2D> solver(new Solver2D());
|
|
solver->setMaxIterations(32);
|
|
solver->addTarget(b2, 0.f, 2.f, 1.f);
|
|
|
|
solver->setInfluence(0.5f);
|
|
solver->solve(sk.get());
|
|
CHECK(near(sk->getX(b2), 1.f, 1e-2f));
|
|
CHECK(near(sk->getY(b2), 1.f, 1e-2f));
|
|
|
|
sk->initStraightPose(0.f, 0.f);
|
|
solver->setInfluence(0.f);
|
|
solver->solve(sk.get());
|
|
CHECK(near(sk->getX(b2), 2.f));
|
|
CHECK(near(sk->getY(b2), 0.f));
|
|
CHECK_THROWS((solver->setInfluence(1.5f), false));
|
|
}
|
|
|
|
TEST_CASE("ik.solver3d.solveChain.pinsChainRoot") {
|
|
std::unique_ptr<Skeleton3D> sk(new Skeleton3D());
|
|
int b1 = sk->createBone(0, 1.f);
|
|
int b2 = sk->createBone(b1, 1.f);
|
|
int b3 = sk->createBone(0, 1.f);
|
|
b1 = sk->getChild(0, 0);
|
|
b3 = sk->getChild(0, 1);
|
|
b2 = sk->getChild(b1, 0);
|
|
sk->initStraightPose(0.f, 0.f, 0.f);
|
|
|
|
std::unique_ptr<Solver3D> solver(new Solver3D());
|
|
solver->setMaxIterations(48);
|
|
solver->setTolerance(1e-3f);
|
|
solver->addTarget(b2, 1.f, 1.f, 0.f, 1.f);
|
|
CHECK(solver->solveChain(sk.get(), b1, b2));
|
|
|
|
CHECK(near(sk->getX(0), 0.f));
|
|
CHECK(near(sk->getX(b1), 1.f));
|
|
CHECK(near(sk->getY(b1), 0.f));
|
|
CHECK(near(sk->getX(b3), 1.f));
|
|
CHECK(near(sk->getY(b3), 0.f));
|
|
CHECK(near(sk->getX(b2), 1.f, 2e-2f));
|
|
CHECK(near(sk->getY(b2), 1.f, 2e-2f));
|
|
}
|
|
|
|
TEST_CASE("ik.solver3d.pole") {
|
|
std::unique_ptr<Skeleton3D> sk(new Skeleton3D());
|
|
int b1 = sk->createBone(0, 1.f);
|
|
int b2 = sk->createBone(b1, 1.f);
|
|
sk->initStraightPose(0.f, 0.f, 0.f);
|
|
|
|
std::unique_ptr<Solver3D> solver(new Solver3D());
|
|
solver->setMaxIterations(64);
|
|
solver->setTolerance(1e-4f);
|
|
solver->addTarget(b2, 1.5f, 0.5f, 0.f, 1.f);
|
|
|
|
solver->setPole(1.f, 1.f, 0.f, 1.f);
|
|
CHECK(solver->hasPole());
|
|
CHECK(near(solver->getPoleWeight(), 1.f));
|
|
CHECK(solver->solveChain(sk.get(), 0, b2));
|
|
const float upY = sk->getY(b1);
|
|
CHECK(upY > 0.1f);
|
|
|
|
sk->initStraightPose(0.f, 0.f, 0.f);
|
|
solver->setPole(1.f, -1.f, 0.f, 1.f);
|
|
CHECK(solver->solveChain(sk.get(), 0, b2));
|
|
const float downY = sk->getY(b1);
|
|
CHECK(downY < -0.1f);
|
|
|
|
sk->initStraightPose(0.f, 0.f, 0.f);
|
|
solver->clearPole();
|
|
CHECK(!solver->hasPole());
|
|
CHECK(solver->solveChain(sk.get(), 0, b2));
|
|
}
|
|
|
|
TEST_CASE("ik.solver3d.tipRotation") {
|
|
std::unique_ptr<Skeleton3D> sk(new Skeleton3D());
|
|
int b1 = sk->createBone(0, 1.f);
|
|
int b2 = sk->createBone(b1, 1.f);
|
|
sk->initStraightPose(0.f, 0.f, 0.f);
|
|
|
|
std::unique_ptr<Solver3D> solver(new Solver3D());
|
|
solver->setMaxIterations(48);
|
|
solver->addTarget(b2, 2.f, 0.f, 0.f, 1.f);
|
|
solver->setTipRotation(b2, 0.f, static_cast<float>(M_PI / 2.0), 1.f);
|
|
CHECK(near(solver->getTipRotationWeight(), 1.f));
|
|
CHECK(solver->solveChain(sk.get(), 0, b2));
|
|
|
|
// Tip keeps reaching the target while its world direction is pitched up.
|
|
CHECK(near(sk->getX(b2), 2.f, 2e-2f));
|
|
CHECK(near(sk->getOrientationZ(b2), 1.f, 1e-2f));
|
|
CHECK(near(sk->getRotationPitch(b2), static_cast<float>(M_PI / 2.0), 1e-2f));
|
|
|
|
solver->clearTipRotation();
|
|
CHECK(near(solver->getTipRotationWeight(), 0.f));
|
|
CHECK(solver->solveChain(sk.get(), 0, b2));
|
|
CHECK(near(sk->getOrientationZ(b2), 0.f, 1e-2f));
|
|
}
|
|
|
|
TEST_CASE("ik.solver2d.stepChain") {
|
|
std::unique_ptr<Skeleton2D> sk(new Skeleton2D());
|
|
int b1 = sk->createBone(0, 1.f);
|
|
int b2 = sk->createBone(b1, 1.f);
|
|
sk->initStraightPose(0.f, 0.f);
|
|
|
|
std::unique_ptr<Solver2D> solver(new Solver2D());
|
|
solver->addTarget(b2, 0.f, 2.f, 1.f);
|
|
solver->stepChain(sk.get(), 0, b2, 1.f);
|
|
CHECK(near(sk->getX(b2), 0.f, 1e-2f));
|
|
CHECK(near(sk->getY(b2), 2.f, 1e-2f));
|
|
}
|
|
|
|
TEST_CASE("ik.factory") {
|
|
auto *mod = IK::create();
|
|
std::unique_ptr<Skeleton2D> sk(mod->newSkeleton2D());
|
|
std::unique_ptr<Solver2D> solver(mod->newSolver2D());
|
|
CHECK_EQ(sk->getBoneCount(), 1);
|
|
CHECK_EQ(solver->getMaxIterations(), 16);
|
|
}
|
|
|
|
TEST_CASE("ik.invalidBoneThrows") {
|
|
std::unique_ptr<Skeleton2D> sk(new Skeleton2D());
|
|
bool threw = false;
|
|
try {
|
|
sk->getX(99);
|
|
} catch (const eve::Exception &) {
|
|
threw = true;
|
|
}
|
|
CHECK(threw);
|
|
}
|
|
|
|
static void drawSeg(Graphics *gfx, float x0, float y0, float x1, float y1, const Color &c) {
|
|
const float dx = x1 - x0;
|
|
const float dy = y1 - y0;
|
|
const float len = std::sqrt(dx * dx + dy * dy);
|
|
if (len < 0.5f) return;
|
|
const float steps = std::max(1.f, len / 2.5f);
|
|
for (float t = 0.f; t <= 1.f; t += 1.f / steps) {
|
|
gfx->drawSolidRect(x0 + dx * t - 2.f, y0 + dy * t - 2.f, 4.f, 4.f, c);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("ik.solver2d.armReachPreview") {
|
|
std::unique_ptr<Skeleton2D> sk(new Skeleton2D());
|
|
// Multi-segment arm in world units (will scale to pixels).
|
|
int b1 = sk->createBone(0, 1.1f);
|
|
int b2 = sk->createBone(b1, 0.95f);
|
|
int b3 = sk->createBone(b2, 0.7f);
|
|
sk->initStraightPose(0.f, 0.f);
|
|
sk->setConstraints(b1, -1.2f, 1.2f);
|
|
sk->setConstraints(b2, -1.6f, 1.6f);
|
|
|
|
std::unique_ptr<Solver2D> solver(new Solver2D());
|
|
solver->setMaxIterations(40);
|
|
solver->setTolerance(1e-3f);
|
|
solver->addTarget(b3, 1.5f, 0.8f, 1.f);
|
|
|
|
auto *win = eve::window::Window::create();
|
|
auto *gfx = Graphics::create();
|
|
REQUIRE(win != nullptr);
|
|
REQUIRE(gfx != nullptr);
|
|
eve::window::WindowSettings s;
|
|
s.width = 560;
|
|
s.height = 420;
|
|
s.centered = true;
|
|
REQUIRE(win->setWindowSettings(s));
|
|
|
|
const float scale = 110.f;
|
|
const float ox = float(gfx->getWidth()) * 0.35f;
|
|
const float oy = float(gfx->getHeight()) * 0.55f;
|
|
|
|
gfx->setBackgroundColorRGBA(0.07f, 0.08f, 0.11f, 1.f);
|
|
float tipErr = 1.f;
|
|
|
|
for (int frame = 0; frame < 120; ++frame) {
|
|
const float t = float(frame) * 0.05f;
|
|
// Orbiting reach target (figure-eight-ish).
|
|
const float tx = 1.1f + 0.85f * std::cos(t);
|
|
const float ty = 0.2f + 0.75f * std::sin(t * 1.3f);
|
|
solver->clearTargets();
|
|
solver->addTarget(b3, tx, ty, 1.f);
|
|
solver->solve(sk.get());
|
|
|
|
const float tipX = sk->getX(b3);
|
|
const float tipY = sk->getY(b3);
|
|
tipErr = std::sqrt((tipX - tx) * (tipX - tx) + (tipY - ty) * (tipY - ty));
|
|
|
|
gfx->clearScreen();
|
|
// Ground / base
|
|
gfx->drawSolidRect(ox - 18.f, oy - 6.f, 36.f, 12.f, Color(0.35f, 0.38f, 0.45f, 1.f));
|
|
|
|
auto toScreen = [&](float wx, float wy, float &sx, float &sy) {
|
|
sx = ox + wx * scale;
|
|
sy = oy - wy * scale;
|
|
};
|
|
|
|
float sx0, sy0, sx1, sy1;
|
|
toScreen(sk->getX(0), sk->getY(0), sx0, sy0);
|
|
for (int b = 1; b < sk->getBoneCount(); ++b) {
|
|
const int p = sk->getParent(b);
|
|
toScreen(sk->getX(p), sk->getY(p), sx0, sy0);
|
|
toScreen(sk->getX(b), sk->getY(b), sx1, sy1);
|
|
drawSeg(gfx, sx0, sy0, sx1, sy1, Color(0.45f, 0.85f, 1.f, 1.f));
|
|
gfx->drawSolidRect(sx1 - 3.f, sy1 - 3.f, 6.f, 6.f, Color(1.f, 1.f, 1.f, 1.f));
|
|
}
|
|
|
|
float tSx, tSy;
|
|
toScreen(tx, ty, tSx, tSy);
|
|
gfx->drawSolidRect(tSx - 5.f, tSy - 5.f, 10.f, 10.f, Color(1.f, 0.45f, 0.25f, 1.f));
|
|
gfx->present();
|
|
|
|
SDL_Event e;
|
|
while (SDL_PollEvent(&e)) {
|
|
if (e.type == SDL_QUIT) break;
|
|
}
|
|
SDL_Delay(16);
|
|
}
|
|
|
|
CHECK(tipErr < 0.25f);
|
|
win->close();
|
|
}
|