// -*- C++ -*- #include #include #include #include #include #include #include #include "material.h" #include "model.h" #include "physics/constraint.h" #include "physics/force.h" #include "physics/particle.h" #include "physics/shape.h" #include "physics/vector.h" #include "player.h" #include "room.h" #include "room_object.h" #include "world.h" using namespace mbostock; static const float axleRadius = .02f; static const float axleLength = .14f; static const float bodySize = .15f; static const float eyeRadius = .02f; static const float wheelRadius = .1f; static const float wheelWeight = 1.f; static const float counterWeightRadius = .07f; static const float counterWeightOffset = .07f; static const float counterWeight = .1f; static const float forwardForce = 12.f; static const float backwardForce = 5.f; static const float turnForce = 1.f; static const float motorFriction = 1.f; static const float brakeFriction = 3.f; static const Vector spokeColor(.5f, .5f, .5f); static const Vector eyeColor(.5f, .5f, .5f); static const Vector axleMaterialDiffuse(.9f, .8f, .8f); static const Vector tireMaterialDiffuse(.1f, .1f, .1f); static const Vector bodyMaterialDiffuse(.6f, .2f, .3f); namespace mbostock { /** * A small model which displays just the player's wheel, so that this model * can be compiled into a display list for faster display. This model uses the * parent model's quadric. */ class PlayerWheelModel : public Model { public: PlayerWheelModel(const PlayerModel& model); virtual void display(); private: const PlayerModel& model_; }; /** * A small model which displays just the player's body (and axle, and eyes), * so that this model can be compiled into a display list for faster display. * This model uses the parent model's quadric. */ class PlayerBodyModel : public Model { public: PlayerBodyModel(const PlayerModel& model); virtual void display(); private: const PlayerModel& model_; }; } PlayerWheelModel::PlayerWheelModel(const PlayerModel& model) : model_(model) { } void PlayerWheelModel::display() { /* Spokes. */ glDisable(GL_LIGHTING); glColorv(spokeColor); glLineWidth(2.f); gluQuadricDrawStyle(model_.quadric_, GLU_LINE); gluDisk(model_.quadric_, 0, wheelRadius - wheelRadius / 8.f, 8, 1); gluQuadricDrawStyle(model_.quadric_, GLU_FILL); glEnable(GL_LIGHTING); /* Tire. */ glMaterialv(GL_FRONT_AND_BACK, GL_DIFFUSE, tireMaterialDiffuse); glutSolidTorus(wheelRadius / 4.f, 3 * wheelRadius / 4.f, 16, 32); } PlayerBodyModel::PlayerBodyModel(const PlayerModel& model) : model_(model) { } void PlayerBodyModel::display() { float l = axleLength + wheelRadius / 4.f + wheelRadius; /* Axle endcap. */ glMaterialv(GL_FRONT_AND_BACK, GL_DIFFUSE, axleMaterialDiffuse); glPushMatrix(); glTranslatef(0.f, 0.f, -l / 2.f); glRotatef(180.f, 0.f, 1.f, 0.f); gluDisk(model_.quadric_, 0.f, axleRadius, 16, 8); glPopMatrix(); /* Axle. */ glPushMatrix(); glTranslatef(0.f, 0.f, -l / 2.f); gluCylinder(model_.quadric_, axleRadius, axleRadius, l, 16, 1); glPopMatrix(); /* Axle endcap. */ glPushMatrix(); glTranslatef(0.f, 0.f, l / 2.f); gluDisk(model_.quadric_, 0.f, axleRadius, 16, 8); glPopMatrix(); /* Body. */ glMaterialv(GL_FRONT_AND_BACK, GL_DIFFUSE, bodyMaterialDiffuse); glutSolidCube(bodySize); /* Eyes. */ static const float eyeDepth = .01f; glMaterialv(GL_FRONT_AND_BACK, GL_DIFFUSE, axleMaterialDiffuse); glColorv(eyeColor); glTranslatef(bodySize / 2.f, bodySize / 6.f, bodySize / 4.f); glPushMatrix(); glRotatef(90.f, 0.f, 1.f, 0.f); gluCylinder(model_.quadric_, eyeRadius, eyeRadius, eyeDepth, 16, 1); glTranslatef(0.f, 0.f, eyeDepth); gluDisk(model_.quadric_, 0, eyeRadius, 8, 1); glTranslatef(0.f, -.004f, .001f); glMaterialv(GL_FRONT_AND_BACK, GL_DIFFUSE, tireMaterialDiffuse); gluDisk(model_.quadric_, 0, eyeRadius / 2.f, 8, 1); glMaterialv(GL_FRONT_AND_BACK, GL_DIFFUSE, axleMaterialDiffuse); glPopMatrix(); glTranslatef(0.f, 0.f, -bodySize / 2.f); glPushMatrix(); glRotatef(90.f, 0.f, 1.f, 0.f); gluCylinder(model_.quadric_, eyeRadius, eyeRadius, eyeDepth, 16, 1); glTranslatef(0.f, 0.f, eyeDepth); gluDisk(model_.quadric_, 0, eyeRadius, 8, 1); glTranslatef(0.f, -.004f, .001f); glMaterialv(GL_FRONT_AND_BACK, GL_DIFFUSE, tireMaterialDiffuse); gluDisk(model_.quadric_, 0, eyeRadius / 2.f, 8, 1); glPopMatrix(); } PlayerModel::PlayerModel(const Player& player) : player_(player), quadric_(NULL), wheelModel_(Models::compile(new PlayerWheelModel(*this))), bodyModel_(Models::compile(new PlayerBodyModel(*this))) { for (int i = 0; i < 15; i++) { orientation_[i] = 0.f; } orientation_[15] = 1.f; } PlayerModel::~PlayerModel() { delete wheelModel_; delete bodyModel_; if (quadric_ != NULL) { gluDeleteQuadric(quadric_); } } void PlayerModel::initialize() { if (quadric_ != NULL) { gluDeleteQuadric(quadric_); } quadric_ = gluNewQuadric(); wheelModel_->initialize(); bodyModel_->initialize(); } float* PlayerModel::orientation() { const Vector& x = player_.x(); const Vector& y = player_.y(); const Vector& z = player_.z(); orientation_[0] = x.x; orientation_[1] = x.y; orientation_[2] = x.z; orientation_[4] = y.x; orientation_[5] = y.y; orientation_[6] = y.z; orientation_[8] = z.x; orientation_[9] = z.y; orientation_[10] = z.z; return orientation_; } void PlayerModel::display() { Materials::blank().bind(); glPushMatrix(); glTranslatev(player_.origin()); glMultMatrixf(orientation()); /* Left wheel. */ if (!World::world()->debug() || player_.leftWheelFriction()) { glPushMatrix(); glTranslatef(0.f, 0.f, -axleLength / 2.f - wheelRadius / 2.f); glRotatef(player_.leftWheelAngle(), 0.f, 0.f, 1.f); glRotatef(180.f, 0.f, 1.f, 0.f); wheelModel_->display(); glPopMatrix(); } /* Right wheel. */ if (!World::world()->debug() || player_.rightWheelFriction()) { glPushMatrix(); glTranslatef(0.f, 0.f, axleLength / 2.f + wheelRadius / 2.f); glRotatef(player_.rightWheelAngle(), 0.f, 0.f, 1.f); wheelModel_->display(); glPopMatrix(); } /* Axes. */ if (World::world()->debug()) { displayAxes(); } /* Body. */ bodyModel_->display(); glPopMatrix(); } void PlayerModel::displayAxes() { glDisable(GL_LIGHTING); glBegin(GL_LINES); glColor3f(1.f, 0.f, 0.f); glVertex3f(0.f, 0.f, 0.f); glVertex3f(2 * bodySize, 0.f, 0.f); glColor3f(0.f, 1.f, 0.f); glVertex3f(0.f, 0.f, 0.f); glVertex3f(0.f, 2 * bodySize, 0.f); glColor3f(0.f, 0.f, 1.f); glVertex3f(0.f, 0.f, 0.f); glVertex3f(0.f, 0.f, 2 * bodySize); glEnd(); glEnable(GL_LIGHTING); } Player::Player() : turnState_(NONE), moveState_(NONE), sphere_(Vector::ZERO(), wheelRadius * 2.f), model_(*this) { counterWeight_.inverseMass = 1.f / counterWeight; } float Player::mass() const { return counterWeight + 3.f; } Player::Wheel::Wheel() : angle(0.f), angleStep(0.f) { inverseMass = 1.f / wheelWeight; } void Player::setOrigin(const Vector& origin) { static const float a2 = axleLength / 2.f; leftWheel_.position = origin + Vector(0.f, 0.f, -a2); rightWheel_.position = origin + Vector(0.f, 0.f, a2); body_.position = origin; counterWeight_.position = origin + Vector(-counterWeightOffset, 0.f, 0.f); leftWheel_.previousPosition = leftWheel_.position; rightWheel_.previousPosition = rightWheel_.position; body_.previousPosition = body_.position; counterWeight_.previousPosition = counterWeight_.position; origin_ = origin; x_ = Vector::X(); y_ = Vector::Y(); z_ = Vector::Z(); } void Player::setVelocity(const Vector& velocity) { /* TODO: Rotate the player to face the velocity. But this works for now. */ if (velocity.x < 0) { static const float a2 = axleLength / 2.f; leftWheel_.position = origin_ + Vector(0.f, 0.f, a2); rightWheel_.position = origin_ + Vector(0.f, 0.f, -a2); counterWeight_.position = origin_ + Vector(counterWeightOffset, 0.f, 0.f); leftWheel_.previousPosition = leftWheel_.position; rightWheel_.previousPosition = rightWheel_.position; counterWeight_.previousPosition = counterWeight_.position; } Vector v = velocity * ParticleSimulator::timeStep(); leftWheel_.previousPosition = leftWheel_.position - v; rightWheel_.previousPosition = rightWheel_.position - v; body_.previousPosition = body_.position - v; counterWeight_.previousPosition = counterWeight_.position - v; } void Player::resetForces() { leftWheel_.force = Vector::ZERO(); rightWheel_.force = Vector::ZERO(); body_.force = Vector::ZERO(); counterWeight_.force = Vector::ZERO(); /* Moving forces. */ switch (moveState_) { case FORWARD: { leftWheel_.applyForce(z_, forwardForce); rightWheel_.applyForce(z_, forwardForce); break; } case BACKWARD: { leftWheel_.applyForce(z_, -backwardForce); rightWheel_.applyForce(z_, -backwardForce); break; } } /* Turning forces. */ switch (turnState_) { case RIGHT: { leftWheel_.applyForce(z_, turnForce); rightWheel_.applyForce(z_, -turnForce); break; } case LEFT: { leftWheel_.applyForce(z_, -turnForce); rightWheel_.applyForce(z_, turnForce); break; } } /* Uniform drag forces. */ if ((turnState_ != NONE) || (moveState_ == FORWARD)) { leftWheel_.applyQuadraticDrag(motorFriction); rightWheel_.applyQuadraticDrag(motorFriction); } else { leftWheel_.applyLinearDrag(brakeFriction); rightWheel_.applyLinearDrag(brakeFriction); } /* Flipping force if upside-down. */ if ((leftWheel_.contact && rightWheel_.contact) && (y_.y < (.5 * leftWheel_.contactNormal.y))) { counterWeight_.force -= y_; } } void Player::Wheel::applyContact(const RoomObject& o) { const Vector& normal = Constraints::projection().normal; if ((normal.y > o.slip()) && (!contact || (contactNormal.y < normal.y))) { contactNormal = normal; contactVelocity = o.velocity(position); contact = true; } } void Player::Wheel::applyForce(const Vector& z, float f) { if (friction()) { force += z.cross(contactNormal) * -f; } } void Player::Wheel::applyLinearDrag(float kd) { if (friction()) { Vector v = (position - previousPosition - contactVelocity) / ParticleSimulator::timeStep(); float vs = v.squared(); if (vs < 1E-3) { kd *= 10.f; } else if (vs < 1E-2) { kd *= 2.f; } force -= v * kd; } } void Player::Wheel::applyQuadraticDrag(float kd) { if (friction()) { Vector v = (position - previousPosition - contactVelocity) / ParticleSimulator::timeStep(); force -= v * v.length() * kd; } } void Player::applyForce(UnaryForce& force) { force.apply(leftWheel_); force.apply(rightWheel_); force.apply(body_); force.apply(counterWeight_); } void Player::step(const ParticleSimulator& s) { leftWheel_.contact = false; rightWheel_.contact = false; s.step(leftWheel_); s.step(rightWheel_); s.step(body_); s.step(counterWeight_); sphere_.x() = body_.position; } bool Player::intersects(const Shape& s) { return s.intersects(sphere_); } bool Player::constrainOutside(const RoomObject& o) { const Shape& s = o.shape(); bool contact = false; if (s.intersects(sphere_)) { if (Constraints::outside(leftWheel_, s, wheelRadius)) { constrainGlancing(o); leftWheel_.applyContact(o); contact = true; } if (Constraints::outside(rightWheel_, s, wheelRadius)) { constrainGlancing(o); rightWheel_.applyContact(o); contact = true; } if (Constraints::outside(body_, s, wheelRadius)) { sphere_.x() = body_.position; contact = true; } if (Constraints::outside(counterWeight_, s, counterWeightRadius)) { contact = true; } } return contact; } void Player::constrainGlancing(const RoomObject& o) { const Projection& j = Constraints::projection(); /* * This constraint only applies if we are moving parallel to the wall, so if * the body particle isn't moving, don't do anything. */ if ((body_.position - body_.previousPosition).squared() < 1E-5) { return; } /* For glancing collisions, align parallel to the wall. */ float n = j.normal.dot(z_); if (fabsf(n) > .8f) { Plane p = Plane(j.x, j.normal); Vector x = p.project((leftWheel_.position + rightWheel_.position) / 2.f).x; Vector wp1 = x + j.normal * wheelRadius; Vector wp2 = x + j.normal * (wheelRadius + axleLength); Vector bp = x + j.normal * (wheelRadius + axleLength / 2.f); const Vector& lp = (n > 0.f) ? wp1 : wp2; const Vector& rp = (n > 0.f) ? wp2 : wp1; /* * Only allow this constraint to move the player a small amount, as we don't * want this constraint to apply in weird cases like hitting a corner. * Otherwise, this constraint might throw the player large distances. */ if (((bp - body_.position).squared() + (lp - leftWheel_.position).squared() + (rp - rightWheel_.position).squared()) > 1E-3) { return; } body_.position = bp; leftWheel_.position = lp; rightWheel_.position = rp; /* * Include a small fudge factor to push the player away from the wall, so * that the player can turn. Otherwise, the player seemingly gets locked to * the wall surface! */ body_.position += j.normal * 1E-4; leftWheel_.position += j.normal * 1E-4; rightWheel_.position += j.normal * 1E-4; body_.previousPosition += j.normal * 1E-4; leftWheel_.previousPosition += j.normal * 1E-4; rightWheel_.previousPosition += j.normal * 1E-4; } } void Player::constrainBody() { Constraints::distance(leftWheel_, rightWheel_, axleLength); Constraints::distance(leftWheel_, body_, axleLength / 2.f); Constraints::distance(rightWheel_, body_, axleLength / 2.f); static const float d = sqrtf(axleLength * axleLength / 4.f + counterWeightOffset * counterWeightOffset); Constraints::distance(leftWheel_, counterWeight_, d); Constraints::distance(rightWheel_, counterWeight_, d); Constraints::distance(body_, counterWeight_, counterWeightOffset); } void Player::constrainInternal() { leftWheel_.constrainDirection(z_); rightWheel_.constrainDirection(z_); constrainBody(); origin_ = body_.position; z_ = (rightWheel_.position - leftWheel_.position) / axleLength; x_ = (body_.position - counterWeight_.position) / counterWeightOffset; y_ = -x_.cross(z_); } void Player::Wheel::constrainDirection(const Vector& z) { if (!friction()) { angle += angleStep; return; // no normal force, no friction } /* Don't let upwards-moving platforms launch us into the air. */ Vector cv = contactVelocity; if (cv.y > 0) { cv.y = 0.f; } /* Cancel the sideways component of the velocity. */ Vector v = position - previousPosition - cv; v -= z * z.dot(v); previousPosition = position - v - cv; /* Rotate the wheel. */ Vector forward = z.cross(contactNormal); Vector vForward = forward * forward.dot(v); float l = vForward.length(); if (vForward.cross(contactNormal).dot(z) > 0) { l *= -1; } angleStep = 360.f * l / (M_PI * 2 * wheelRadius); angle += angleStep; } void Player::jiggle() { Vector v = Vector::randomVector(ParticleSimulator::timeStep()); v.y = ParticleSimulator::timeStep(); leftWheel_.position += v; rightWheel_.position += v; body_.position += v; counterWeight_.position += v; } void Player::move(Direction d) { switch (d) { case LEFT: { if (turnState_ == NONE) { turnState_ = LEFT; } else { turnState_ = NONE; } break; } case RIGHT: { if (turnState_ == NONE) { turnState_ = RIGHT; } else { turnState_ = NONE; } break; } case FORWARD: { if (moveState_ == NONE) { moveState_ = FORWARD; } else { moveState_ = NONE; } break; } case BACKWARD: { if (moveState_ == NONE) { moveState_ = BACKWARD; } else { moveState_ = NONE; } break; } } } void Player::stop() { turnState_ = NONE; moveState_ = NONE; } void Player::stop(Direction d) { switch (d) { case LEFT: { if (turnState_ != NONE) { turnState_ = NONE; } else { turnState_ = RIGHT; } break; } case RIGHT: { if (turnState_ != NONE) { turnState_ = NONE; } else { turnState_ = LEFT; } break; } case FORWARD: { if (moveState_ != NONE) { moveState_ = NONE; } else { moveState_ = BACKWARD; } break; } case BACKWARD: { if (moveState_ != NONE) { moveState_ = NONE; } else { moveState_ = FORWARD; } break; } } }