# Conflicts: # src/modules/camera/CameraController.cpp # src/modules/camera/CameraController.h # test/audio.cpp
423 lines
14 KiB
C++
423 lines
14 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "camera/CameraController.h"
|
|
#include "common/CameraObstruction.h"
|
|
#include "common/Capability.h"
|
|
#include "graphics/RenderSystem3D.h"
|
|
|
|
#include <cmath>
|
|
|
|
using namespace eve::camera;
|
|
using eve::graphics::Camera3D;
|
|
|
|
namespace {
|
|
|
|
bool near(float a, float b, float eps = 1e-3f) { return std::fabs(a - b) < eps; }
|
|
|
|
class ObstructionMock final : public eve::ICameraObstructionQuery {
|
|
public:
|
|
bool sphereCast(float, float, float, float, float, float, float, uint64_t maskBits, int ignoredBodyId,
|
|
eve::CameraObstructionHit* out) override {
|
|
seenMask = maskBits;
|
|
seenIgnored = ignoredBodyId;
|
|
*out = {true, 42, 0.3f, 0.f, 0.f, 3.f, 0.f, 0.f, -1.f};
|
|
return true;
|
|
}
|
|
uint64_t seenMask = 0;
|
|
int seenIgnored = -1;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("camera.modeRoundTrip") {
|
|
CameraController cc;
|
|
CHECK_EQ(cc.getMode(), std::string("follow"));
|
|
cc.setMode("orbit");
|
|
CHECK_EQ(cc.getMode(), std::string("orbit"));
|
|
cc.setMode("topdown");
|
|
CHECK_EQ(cc.getMode(), std::string("topdown"));
|
|
cc.setMode("firstperson");
|
|
CHECK_EQ(cc.getMode(), std::string("firstperson"));
|
|
cc.setMode("cinematic");
|
|
CHECK_EQ(cc.getMode(), std::string("cinematic"));
|
|
cc.setMode("not-a-mode");
|
|
CHECK_EQ(cc.getMode(), std::string("cinematic"));
|
|
}
|
|
|
|
TEST_CASE("graphics.camera3d.layerCullDistancesRoundTrip") {
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
CHECK_EQ(cam->getLayerCullDistance(7), 0.f);
|
|
CHECK_EQ(cam->getShadowLayerCullDistance(7), 0.f);
|
|
cam->setLayerCullDistance(7, 125.f);
|
|
cam->setShadowLayerCullDistance(7, 80.f);
|
|
CHECK_EQ(cam->getLayerCullDistance(7), 125.f);
|
|
CHECK_EQ(cam->getShadowLayerCullDistance(7), 80.f);
|
|
|
|
auto* object = eve::graphics::Renderable3D::create();
|
|
object->setLayer(7);
|
|
CHECK_EQ(object->getLayer(), 7);
|
|
|
|
cam->setDepthOfField(20.f, 5.f, 12.f);
|
|
CHECK_EQ(cam->data()->dofFocusDistance, 20.f);
|
|
CHECK_EQ(cam->data()->dofMaxBlurPx, 5.f);
|
|
CHECK_EQ(cam->data()->dofFocusRange, 12.f);
|
|
cam->clearDepthOfField();
|
|
CHECK_EQ(cam->data()->dofMaxBlurPx, 0.f);
|
|
}
|
|
|
|
TEST_CASE("camera.paramsRoundTrip") {
|
|
CameraController cc;
|
|
cc.setTarget(1.f, 2.f, 3.f);
|
|
CHECK_EQ(cc.getTargetX(), 1.f);
|
|
CHECK_EQ(cc.getTargetY(), 2.f);
|
|
CHECK_EQ(cc.getTargetZ(), 3.f);
|
|
cc.setOffset(0.5f, 1.5f, 2.5f);
|
|
cc.setLookAhead(0.1f, 0.2f, 0.3f);
|
|
cc.setRadius(7.f);
|
|
cc.setAzimuth(90.f);
|
|
cc.setElevation(45.f);
|
|
cc.setOrbitSpeed(30.f);
|
|
cc.setYaw(10.f);
|
|
cc.setPitch(-5.f);
|
|
cc.setSmooth(9.f);
|
|
cc.setMaxSpeed(12.f);
|
|
// Negative damping / max-speed are clamped to 0, never crash.
|
|
cc.setSmooth(-1.f);
|
|
cc.setMaxSpeed(-1.f);
|
|
CHECK_EQ(cc.getCamera(), nullptr);
|
|
}
|
|
|
|
TEST_CASE("camera.followSnapPlacesEye") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
CHECK_EQ(cc.getCamera(), cam);
|
|
cc.setTarget(0.f, 0.f, 0.f);
|
|
cc.setOffset(0.f, 2.f, 6.f);
|
|
cc.snap();
|
|
CHECK(near(cam->data()->eyeX, 0.f));
|
|
CHECK(near(cam->data()->eyeY, 2.f));
|
|
CHECK(near(cam->data()->eyeZ, 6.f));
|
|
CHECK(near(cam->data()->targetX, 0.f));
|
|
CHECK(near(cam->data()->targetY, 0.f));
|
|
CHECK(near(cam->data()->targetZ, 0.f));
|
|
}
|
|
|
|
TEST_CASE("camera.orbitAndTopdownSnap") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
cc.setTarget(4.f, 1.f, -2.f);
|
|
cc.setRadius(10.f);
|
|
cc.setAzimuth(0.f);
|
|
cc.setElevation(0.f);
|
|
cc.setMode("orbit");
|
|
cc.snap();
|
|
// az=0, el=0 -> dir = (0, 0, 1); eye = target + dir*radius.
|
|
CHECK(near(cam->data()->eyeX, 4.f));
|
|
CHECK(near(cam->data()->eyeY, 1.f));
|
|
CHECK(near(cam->data()->eyeZ, 8.f));
|
|
CHECK(near(cam->data()->targetX, 4.f));
|
|
CHECK(near(cam->data()->targetY, 1.f));
|
|
CHECK(near(cam->data()->targetZ, -2.f));
|
|
|
|
cc.setMode("topdown");
|
|
cc.snap();
|
|
CHECK(near(cam->data()->eyeX, 4.f));
|
|
CHECK(near(cam->data()->eyeY, 11.f));
|
|
CHECK(near(cam->data()->eyeZ, -2.f));
|
|
CHECK(near(cam->data()->upX, 0.f));
|
|
CHECK(near(cam->data()->upY, 0.f));
|
|
CHECK(near(cam->data()->upZ, -1.f));
|
|
}
|
|
|
|
TEST_CASE("camera.lensAndComposition") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
cc.setFov(200.f);
|
|
CHECK(near(cc.getFov(), 179.f));
|
|
cc.setComposition(0.25f, 0.f);
|
|
cc.setTarget(0.f, 0.f, 0.f);
|
|
cc.setOffset(0.f, 0.f, 10.f);
|
|
cc.snap();
|
|
CHECK(near(cam->data()->fovYDeg, 179.f));
|
|
CHECK(std::fabs(cam->data()->targetX) > 1.f);
|
|
}
|
|
|
|
TEST_CASE("camera.inputAndFovModifier") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
cc.setMode("orbit");
|
|
cc.setRadius(10.f);
|
|
cc.addInput(90.f, 10.f, -2.f);
|
|
cc.snap();
|
|
CHECK(glm::length(glm::vec3(cam->data()->eyeX, cam->data()->eyeY, cam->data()->eyeZ)) < 8.1f);
|
|
cc.setPositionSmooth(20.f);
|
|
cc.setTargetSmooth(5.f);
|
|
cc.addFovImpulse(20.f, 1.f);
|
|
cc.update(0.1f);
|
|
CHECK(cam->data()->fovYDeg > cc.getFov());
|
|
}
|
|
|
|
TEST_CASE("camera.collisionRetractsAndRecovers") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
cc.setTarget(0.f, 0.f, 0.f);
|
|
cc.setOffset(0.f, 0.f, 10.f);
|
|
cc.setSmooth(100.f);
|
|
cc.setCollisionEnabled(true);
|
|
cc.setCollisionRadius(0.25f);
|
|
cc.addCollisionBox(-1.f, -1.f, 4.f, 1.f, 1.f, 5.f);
|
|
cc.update(1.f / 60.f);
|
|
CHECK(cc.isObstructed());
|
|
CHECK(cam->data()->eyeZ < 4.f);
|
|
cc.clearCollisionBoxes();
|
|
cc.update(1.f / 60.f);
|
|
CHECK(!cc.isObstructed());
|
|
}
|
|
|
|
TEST_CASE("camera.dynamicObstructionUsesCapability") {
|
|
ObstructionMock mock;
|
|
auto* previous = eve::cap::query<eve::ICameraObstructionQuery>();
|
|
eve::cap::provide<eve::ICameraObstructionQuery>(&mock);
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
cc.setTarget(0.f, 0.f, 0.f);
|
|
cc.setOffset(0.f, 0.f, 10.f);
|
|
cc.setCollisionEnabled(true);
|
|
cc.setCollisionMask(7);
|
|
cc.setCollisionIgnoredBody(9);
|
|
cc.update(0.f);
|
|
CHECK(cc.isObstructed());
|
|
CHECK_EQ(cc.getCollisionBodyId(), 42);
|
|
CHECK(cam->data()->eyeZ < 3.f);
|
|
CHECK_EQ(mock.seenMask, uint64_t(7));
|
|
CHECK_EQ(mock.seenIgnored, 9);
|
|
if (previous)
|
|
eve::cap::provide<eve::ICameraObstructionQuery>(previous);
|
|
else
|
|
eve::cap::revoke<eve::ICameraObstructionQuery>(&mock);
|
|
}
|
|
|
|
TEST_CASE("camera.directorSelectsPriorityAndBlends") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
CHECK(cc.addRig("gameplay", "follow", 10));
|
|
CHECK(cc.addRig("boss", "orbit", 20));
|
|
CHECK(!cc.addRig("bad", "unknown", 30));
|
|
cc.update(0.f);
|
|
CHECK_EQ(cc.getActiveRig(), std::string("boss"));
|
|
CHECK_EQ(cc.getMode(), std::string("orbit"));
|
|
CHECK(cc.setRigEnabled("boss", false));
|
|
cc.update(0.3f);
|
|
CHECK_EQ(cc.getActiveRig(), std::string("gameplay"));
|
|
CHECK(cc.removeRig("boss"));
|
|
}
|
|
|
|
TEST_CASE("camera.impulseExpires") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
cc.setTarget(0.f, 0.f, 0.f);
|
|
cc.setOffset(0.f, 0.f, 5.f);
|
|
cc.snap();
|
|
cc.addImpulse(1.f, 1.f, 0.1f, 7);
|
|
cc.update(0.05f);
|
|
CHECK(std::fabs(cam->data()->eyeX) > 1e-3f);
|
|
cc.update(0.1f);
|
|
for (int i = 0; i < 120; ++i) cc.update(1.f / 60.f);
|
|
CHECK(near(cam->data()->eyeX, 0.f, 0.05f));
|
|
}
|
|
|
|
TEST_CASE("camera.actionCueRegistrationDrivesRealControllerImpulse") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
cc.setTarget(0.f, 0.f, 0.f);
|
|
cc.setOffset(0.f, 0.f, 5.f);
|
|
cc.snap();
|
|
CHECK(!cc.getActionCuesEnabled());
|
|
cc.setActionCuesEnabled(true);
|
|
CHECK(cc.getActionCuesEnabled());
|
|
|
|
auto registry = eve::action::ActionNotifyRegistry::withBuiltins();
|
|
REQUIRE(registry.ok());
|
|
eve::action::ActionTimelineEvent event{
|
|
eve::action::ActionTimelineEventKind::Notify,
|
|
*eve::LogicalId::parse("camera-track:main"),
|
|
*eve::LogicalId::parse("camera-cue:impact"),
|
|
*eve::LogicalId::parse("presentation:camera"),
|
|
eve::Duration::zero(),
|
|
{{"cue", "combat:impact"}, {"positionAmplitude", 1.0}, {"rotationAmplitude", 0.0},
|
|
{"fovAmplitude", 5.0}, {"durationSeconds", 0.2}, {"seed", 9}}};
|
|
eve::action::ActionNotifyContext context;
|
|
context.executionId = eve::action::ActionExecutionId{303};
|
|
REQUIRE(registry.value().dispatch(event, context).ok());
|
|
cc.update(0.05f);
|
|
CHECK(std::fabs(cam->data()->eyeX) > 1e-3f);
|
|
CHECK(cam->data()->fovYDeg > cc.getFov());
|
|
cc.setActionCuesEnabled(false);
|
|
CHECK(!cc.getActionCuesEnabled());
|
|
}
|
|
|
|
TEST_CASE("camera.timelineCutsAndEmitsMarkers") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
REQUIRE(cc.addRig("wide", "follow", 0));
|
|
REQUIRE(cc.addRig("hero", "orbit", 0));
|
|
CHECK(cc.addTimelineCut(0.f, "wide", 0.f));
|
|
CHECK(cc.addTimelineCut(1.f, "hero", 0.25f));
|
|
CHECK(cc.addTimelineEvent(0.5f, "camera.beat", "intro"));
|
|
cc.playTimeline(false);
|
|
cc.update(0.6f);
|
|
CHECK_EQ(cc.getActiveRig(), std::string("wide"));
|
|
CHECK_EQ(cc.consumeTimelineEvent(), std::string("camera.beat"));
|
|
CHECK_EQ(cc.getTimelineEventData(), std::string("intro"));
|
|
cc.update(0.5f);
|
|
CHECK_EQ(cc.getActiveRig(), std::string("hero"));
|
|
CHECK(!cc.isTimelinePlaying());
|
|
cc.seekTimeline(0.f, false);
|
|
CHECK(near(cc.getTimelineTime(), 0.f));
|
|
}
|
|
|
|
TEST_CASE("camera.timelineQueuesMarkersInterpolatesAndSerializes") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
REQUIRE(cc.addRig("base", "orbit", 1));
|
|
CHECK(cc.addTimelineEvent(0.1f, "one", "1"));
|
|
CHECK(cc.addTimelineEvent(0.2f, "two", "2"));
|
|
CHECK(cc.addTimelineFloat(0.f, "fov", 40.f));
|
|
CHECK(cc.addTimelineFloat(1.f, "fov", 80.f));
|
|
CHECK(!cc.addTimelineFloat(0.f, "unknown", 1.f));
|
|
cc.playTimeline(false);
|
|
cc.update(0.5f);
|
|
CHECK_EQ(cc.getPendingTimelineEventCount(), 2);
|
|
const std::string first = cc.consumeTimelineEvent();
|
|
const std::string firstData = cc.getTimelineEventData();
|
|
const std::string second = cc.consumeTimelineEvent();
|
|
const std::string secondData = cc.getTimelineEventData();
|
|
CHECK_EQ(first, std::string("one"));
|
|
CHECK_EQ(firstData, std::string("1"));
|
|
CHECK_EQ(second, std::string("two"));
|
|
CHECK_EQ(secondData, std::string("2"));
|
|
CHECK(near(cc.getFov(), 60.f));
|
|
const std::string asset = cc.serializeAsset();
|
|
CameraController loaded;
|
|
CHECK(loaded.deserializeAsset(asset));
|
|
CHECK_EQ(loaded.getRigCount(), 1);
|
|
CHECK(near(loaded.getTimelineDuration(), 1.f));
|
|
CHECK(!loaded.deserializeAsset("{}"));
|
|
}
|
|
|
|
TEST_CASE("camera.firstPersonSnap") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
cc.setTarget(0.f, 0.f, 0.f);
|
|
cc.setYaw(0.f);
|
|
cc.setPitch(0.f);
|
|
cc.setMode("firstperson");
|
|
cc.snap();
|
|
CHECK(near(cam->data()->eyeX, 0.f));
|
|
CHECK(near(cam->data()->eyeY, 0.f));
|
|
CHECK(near(cam->data()->eyeZ, 0.f));
|
|
CHECK(near(cam->data()->targetX, 0.f));
|
|
CHECK(near(cam->data()->targetY, 0.f));
|
|
CHECK(near(cam->data()->targetZ, 100.f));
|
|
}
|
|
|
|
TEST_CASE("camera.updateSmoothsTowardDesired") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
cc.setTarget(0.f, 0.f, 0.f);
|
|
cc.setOffset(0.f, 2.f, 6.f);
|
|
cc.setSmooth(10.f);
|
|
cc.update(1.f / 60.f); // first update snaps cur to desired
|
|
CHECK(near(cam->data()->eyeY, 2.f, 1e-2f));
|
|
CHECK(near(cam->data()->eyeZ, 6.f, 1e-2f));
|
|
// Moving the target drags the camera along (after enough frames).
|
|
cc.setTarget(10.f, 10.f, 10.f);
|
|
for (int i = 0; i < 600; ++i) cc.update(1.f / 60.f);
|
|
CHECK(near(cam->data()->eyeX, 10.f, 0.1f));
|
|
CHECK(near(cam->data()->eyeY, 12.f, 0.1f));
|
|
CHECK(near(cam->data()->eyeZ, 16.f, 0.1f));
|
|
}
|
|
|
|
TEST_CASE("camera.maxSpeedLimitsStep") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
cc.setTarget(100.f, 0.f, 0.f);
|
|
cc.setOffset(0.f, 0.f, 0.f);
|
|
cc.setSmooth(100.f);
|
|
cc.setMaxSpeed(1.f);
|
|
cc.update(1.f / 60.f); // first frame snaps to (100,0,0)
|
|
CHECK(near(cam->data()->eyeX, 100.f, 1e-2f));
|
|
// Retarget far away: one frame may move at most maxSpeed*dt.
|
|
cc.setTarget(0.f, 0.f, 0.f);
|
|
cc.update(1.f / 60.f);
|
|
CHECK(cam->data()->eyeX > 99.f);
|
|
CHECK(cam->data()->eyeX < 100.f);
|
|
}
|
|
|
|
TEST_CASE("camera.cinematicViewsAndSwitch") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
cc.setMode("cinematic");
|
|
cc.addView("a", 0.f, 1.f, 2.f, 3.f, 4.f, 5.f);
|
|
cc.addView("b", 6.f, 7.f, 8.f, 9.f, 10.f, 11.f);
|
|
CHECK(!cc.switchTo("nope", 1.f));
|
|
CHECK(cc.switchTo("a", 0.f));
|
|
cc.update(0.f);
|
|
CHECK(near(cam->data()->eyeX, 0.f));
|
|
CHECK(near(cam->data()->eyeZ, 2.f));
|
|
|
|
CHECK(cc.switchTo("b", 2.f));
|
|
cc.update(1.f); // t=0.5, smoothstep(0.5)=0.5 -> halfway
|
|
CHECK(near(cam->data()->eyeX, 3.f, 0.05f));
|
|
cc.update(1.f); // blend done -> view b
|
|
CHECK(near(cam->data()->eyeX, 6.f, 0.05f));
|
|
CHECK(near(cam->data()->eyeZ, 8.f, 0.05f));
|
|
}
|
|
|
|
TEST_CASE("camera.playSequenceCyclesViews") {
|
|
CameraController cc;
|
|
Camera3D* cam = Camera3D::createCamera();
|
|
cc.setCamera(cam);
|
|
cc.setMode("cinematic");
|
|
cc.addView("a", 0.f, 0.f, 0.f, 0.f, 0.f, 0.f);
|
|
cc.addView("b", 10.f, 0.f, 0.f, 0.f, 0.f, 0.f);
|
|
cc.playSequence(0.5f);
|
|
CHECK(cc.isPlaying());
|
|
cc.update(0.6f); // timer >= stepTime -> switch to b, blend starts
|
|
CHECK(cc.isPlaying());
|
|
CHECK(cam->data()->eyeX > 0.01f);
|
|
CHECK(cam->data()->eyeX < 10.f);
|
|
cc.stopSequence();
|
|
CHECK(!cc.isPlaying());
|
|
// Playback stopped: the blend still finishes at view b and stays there.
|
|
for (int i = 0; i < 120; ++i) cc.update(1.f / 60.f);
|
|
CHECK(near(cam->data()->eyeX, 10.f, 0.05f));
|
|
}
|
|
|
|
TEST_CASE("camera.noCameraNoOp") {
|
|
CameraController cc;
|
|
cc.setMode("orbit");
|
|
cc.snap();
|
|
cc.update(0.1f);
|
|
cc.playSequence(1.f);
|
|
cc.update(1.f);
|
|
cc.stopSequence();
|
|
CHECK(!cc.isPlaying());
|
|
}
|