diff --git a/README.md b/README.md index f911210..62c8e7a 100644 --- a/README.md +++ b/README.md @@ -80,3 +80,4 @@ The following projects and individuals have contributed significantly to the pro * **[GLFW for Go](https://github.com/go-gl/glfw)** for making it possible to use GLFW and OpenGL in Go. * **[Bo0mer](https://github.com/Bo0mer)** for the panoramic image that was used to generate the individual `city` skybox images. +* **[Erin Catto](https://github.com/erincatto)** for all the presentations and articles that were used as reference. diff --git a/cmd/rallymka/internal/ecs/car.go b/cmd/rallymka/internal/ecs/car.go new file mode 100644 index 0000000..6286881 --- /dev/null +++ b/cmd/rallymka/internal/ecs/car.go @@ -0,0 +1,106 @@ +package ecs + +import ( + "time" + + "github.com/mokiat/gomath/sprec" +) + +const ( + // TODO: Move constants as part of car descriptor + carMaxSteeringAngle = 40 + carSteeringSpeed = 80 + carSteeringRestoreSpeed = 150 + + carFrontAcceleration = 160 + carRearAcceleration = 80 + carReverseAccelerationRatio = 0.75 + + carFrontBrakeRatio = 0.1 + carRearBrakeRatio = 0.1 +) + +func NewCarSystem(ecsManager *Manager) *CarSystem { + return &CarSystem{ + ecsManager: ecsManager, + } +} + +type CarSystem struct { + ecsManager *Manager +} + +func (s *CarSystem) Update(elapsedTime time.Duration, input CarInput) { + elapsedSeconds := float32(elapsedTime.Seconds()) + + for _, entity := range s.ecsManager.Entities() { + if car := entity.Car; car != nil && entity.HumanInput { + s.updateCarSteering(car, elapsedSeconds, input) + s.updateCarAcceleration(car, elapsedSeconds, input) + } + } +} + +func (s *CarSystem) updateCarSteering(car *Car, elapsedSeconds float32, input CarInput) { + actualSteeringAngle := carMaxSteeringAngle / (1.0 + 0.03*car.Chassis.Velocity.Length()) + switch { + case input.TurnLeft == input.TurnRight: + if car.SteeringAngle > 0.001 { + if car.SteeringAngle -= sprec.Degrees(elapsedSeconds * carSteeringRestoreSpeed); car.SteeringAngle < 0.0 { + car.SteeringAngle = 0.0 + } + } + if car.SteeringAngle < -0.001 { + if car.SteeringAngle += sprec.Degrees(elapsedSeconds * carSteeringRestoreSpeed); car.SteeringAngle > 0.0 { + car.SteeringAngle = 0.0 + } + } + case input.TurnLeft: + if car.SteeringAngle += sprec.Degrees(elapsedSeconds * carSteeringSpeed); car.SteeringAngle > sprec.Degrees(actualSteeringAngle) { + car.SteeringAngle = sprec.Degrees(actualSteeringAngle) + } + case input.TurnRight: + if car.SteeringAngle -= sprec.Degrees(elapsedSeconds * carSteeringSpeed); car.SteeringAngle < -sprec.Degrees(actualSteeringAngle) { + car.SteeringAngle = -sprec.Degrees(actualSteeringAngle) + } + } + + rotationQuat := sprec.RotationQuat(car.SteeringAngle, sprec.BasisYVec3()) + car.FLWheelRotation.FirstBodyAxis = sprec.QuatVec3Rotation(rotationQuat, sprec.BasisXVec3()) + car.FRWheelRotation.FirstBodyAxis = sprec.QuatVec3Rotation(rotationQuat, sprec.BasisXVec3()) +} + +func (s *CarSystem) updateCarAcceleration(car *Car, elapsedSeconds float32, input CarInput) { + // TODO: Remove, just for debugging + if input.Handbrake { + car.Chassis.AngularVelocity = sprec.Vec3Sum(car.Chassis.AngularVelocity, sprec.NewVec3(0.0, 0.0, 0.1)) + car.Chassis.Velocity = sprec.Vec3Sum(car.Chassis.Velocity, sprec.NewVec3(0.0, 0.2, 0.0)) + } + + if input.Forward { + if sprec.Vec3Dot(car.Chassis.Velocity, car.Chassis.Orientation.OrientationZ()) < -5.0 { + car.FLWheel.AngularVelocity = sprec.Vec3Prod(car.FLWheel.AngularVelocity, 1.0-carFrontBrakeRatio) + car.FRWheel.AngularVelocity = sprec.Vec3Prod(car.FRWheel.AngularVelocity, 1.0-carFrontBrakeRatio) + car.BLWheel.AngularVelocity = sprec.Vec3Prod(car.BLWheel.AngularVelocity, 1.0-carRearBrakeRatio) + car.BRWheel.AngularVelocity = sprec.Vec3Prod(car.BRWheel.AngularVelocity, 1.0-carRearBrakeRatio) + } else { + car.FLWheel.AngularVelocity = sprec.Vec3Sum(car.FLWheel.AngularVelocity, sprec.Vec3Prod(car.FLWheel.Orientation.OrientationX(), carFrontAcceleration*elapsedSeconds)) + car.FRWheel.AngularVelocity = sprec.Vec3Sum(car.FRWheel.AngularVelocity, sprec.Vec3Prod(car.FRWheel.Orientation.OrientationX(), carFrontAcceleration*elapsedSeconds)) + car.BLWheel.AngularVelocity = sprec.Vec3Sum(car.BLWheel.AngularVelocity, sprec.Vec3Prod(car.BLWheel.Orientation.OrientationX(), carRearAcceleration*elapsedSeconds)) + car.BRWheel.AngularVelocity = sprec.Vec3Sum(car.BRWheel.AngularVelocity, sprec.Vec3Prod(car.BRWheel.Orientation.OrientationX(), carRearAcceleration*elapsedSeconds)) + } + } + if input.Backward { + if sprec.Vec3Dot(car.Chassis.Velocity, car.Chassis.Orientation.OrientationZ()) > 5.0 { + car.FLWheel.AngularVelocity = sprec.Vec3Prod(car.FLWheel.AngularVelocity, 1.0-carFrontBrakeRatio) + car.FRWheel.AngularVelocity = sprec.Vec3Prod(car.FRWheel.AngularVelocity, 1.0-carFrontBrakeRatio) + car.BLWheel.AngularVelocity = sprec.Vec3Prod(car.BLWheel.AngularVelocity, 1.0-carRearBrakeRatio) + car.BRWheel.AngularVelocity = sprec.Vec3Prod(car.BRWheel.AngularVelocity, 1.0-carRearBrakeRatio) + } else { + car.FLWheel.AngularVelocity = sprec.Vec3Sum(car.FLWheel.AngularVelocity, sprec.Vec3Prod(car.FLWheel.Orientation.OrientationX(), -carFrontAcceleration*carReverseAccelerationRatio*elapsedSeconds)) + car.FRWheel.AngularVelocity = sprec.Vec3Sum(car.FRWheel.AngularVelocity, sprec.Vec3Prod(car.FRWheel.Orientation.OrientationX(), -carFrontAcceleration*carReverseAccelerationRatio*elapsedSeconds)) + car.BLWheel.AngularVelocity = sprec.Vec3Sum(car.BLWheel.AngularVelocity, sprec.Vec3Prod(car.BLWheel.Orientation.OrientationX(), -carRearAcceleration*carReverseAccelerationRatio*elapsedSeconds)) + car.BRWheel.AngularVelocity = sprec.Vec3Sum(car.BRWheel.AngularVelocity, sprec.Vec3Prod(car.BRWheel.Orientation.OrientationX(), -carRearAcceleration*carReverseAccelerationRatio*elapsedSeconds)) + } + } +} diff --git a/cmd/rallymka/internal/ecs/entity.go b/cmd/rallymka/internal/ecs/entity.go index 0bca83a..d6454a0 100644 --- a/cmd/rallymka/internal/ecs/entity.go +++ b/cmd/rallymka/internal/ecs/entity.go @@ -4,27 +4,27 @@ import ( "github.com/mokiat/gomath/sprec" "github.com/mokiat/rally-mka/cmd/rallymka/internal/stream" "github.com/mokiat/rally-mka/internal/engine/graphics" + "github.com/mokiat/rally-mka/internal/engine/physics" ) type Entity struct { - Transform *TransformComponent - RenderMesh *RenderMesh - RenderModel *RenderModel + Physics *PhysicsComponent + Render *RenderComponent RenderSkybox *RenderSkybox - Vehicle *Vehicle - Wheel *Wheel - Input *Input + Car *Car CameraStand *CameraStand + HumanInput bool } -type RenderModel struct { - Model *stream.Model - GeomProgram *graphics.Program +type PhysicsComponent struct { + Body *physics.Body } -type RenderMesh struct { - Mesh *stream.Mesh +type RenderComponent struct { GeomProgram *graphics.Program + Model *stream.Model + Mesh *stream.Mesh + Matrix sprec.Mat4 } type RenderSkybox struct { @@ -33,39 +33,25 @@ type RenderSkybox struct { Mesh *stream.Mesh } -type Vehicle struct { - SteeringAngle sprec.Angle - Acceleration float32 - HandbrakePulled bool - - Position sprec.Vec3 - Orientation Orientation - Velocity sprec.Vec3 - AngularVelocity sprec.Vec3 - - FLWheel *Entity - FRWheel *Entity - BLWheel *Entity - BRWheel *Entity +type CarInput struct { + Forward bool + Backward bool + TurnLeft bool + TurnRight bool + Handbrake bool } -type Wheel struct { - SteeringAngle sprec.Angle - RotationAngle sprec.Angle - - AnchorPosition sprec.Vec3 - - IsDriven bool - - Length float32 - Radius float32 - - SuspensionLength float32 - IsGrounded bool +type Car struct { + SteeringAngle sprec.Angle + Chassis *physics.Body + FLWheelRotation *physics.MatchAxisConstraint + FRWheelRotation *physics.MatchAxisConstraint + FLWheel *physics.Body + FRWheel *physics.Body + BLWheel *physics.Body + BRWheel *physics.Body } -type Input struct{} - type CameraStand struct { Target *Entity AnchorPosition sprec.Vec3 diff --git a/cmd/rallymka/internal/ecs/renderer.go b/cmd/rallymka/internal/ecs/renderer.go index 535e874..4c6fe28 100644 --- a/cmd/rallymka/internal/ecs/renderer.go +++ b/cmd/rallymka/internal/ecs/renderer.go @@ -17,31 +17,32 @@ type Renderer struct { } func (r *Renderer) Render(sequence *graphics.Sequence) { - r.renderRenderMeshes(sequence) - r.renderRenderModels(sequence) + r.renderRender(sequence) r.renderRenderSkyboxes(sequence) } -func (r *Renderer) renderRenderMeshes(sequence *graphics.Sequence) { +func (r *Renderer) renderRender(sequence *graphics.Sequence) { for _, entity := range r.ecsManager.Entities() { - transformComp := entity.Transform - renderMeshComp := entity.RenderMesh - if transformComp == nil || renderMeshComp == nil { + render := entity.Render + if render == nil { continue } - r.renderMesh(sequence, renderMeshComp.GeomProgram, transformComp.Matrix(), renderMeshComp.Mesh) - } -} - -func (r *Renderer) renderRenderModels(sequence *graphics.Sequence) { - for _, entity := range r.ecsManager.Entities() { - transformComp := entity.Transform - renderModelComp := entity.RenderModel - if transformComp == nil || renderModelComp == nil { - continue + if entity.Physics != nil { + body := entity.Physics.Body + render.Matrix = sprec.TransformationMat4( + body.Orientation.OrientationX(), + body.Orientation.OrientationY(), + body.Orientation.OrientationZ(), + body.Position, + ) + } + if render.Model != nil { + for _, node := range render.Model.Nodes { + r.renderModelNode(sequence, render.GeomProgram, render.Matrix, node) + } } - for _, node := range renderModelComp.Model.Nodes { - r.renderModelNode(sequence, renderModelComp.GeomProgram, transformComp.Matrix(), node) + if render.Mesh != nil { + r.renderMesh(sequence, render.GeomProgram, render.Matrix, render.Mesh) } } } diff --git a/cmd/rallymka/internal/ecs/stand.go b/cmd/rallymka/internal/ecs/stand.go index 219d9af..41ad885 100644 --- a/cmd/rallymka/internal/ecs/stand.go +++ b/cmd/rallymka/internal/ecs/stand.go @@ -20,10 +20,18 @@ func (s *CameraStandSystem) Update() { } } +var angleSpeed sprec.Angle = sprec.Degrees(0.0) +var angle sprec.Angle = sprec.Degrees(0) + func (s *CameraStandSystem) updateCameraStand(cameraStand *CameraStand) { + angle += angleSpeed + var targetPosition sprec.Vec3 - if cameraStand.Target.Vehicle != nil { - targetPosition = cameraStand.Target.Vehicle.Position + switch { + case cameraStand.Target.Physics != nil: + targetPosition = cameraStand.Target.Physics.Body.Position + case cameraStand.Target.Render != nil: + targetPosition = cameraStand.Target.Render.Matrix.Translation() } // we use a camera anchor to achieve the smooth effect of a // camera following the target @@ -45,6 +53,7 @@ func (s *CameraStandSystem) updateCameraStand(cameraStand *CameraStand) { sprec.UnitVec3(cameraVectorZ), sprec.ZeroVec3(), ), + sprec.RotationMat4(angle, 0.0, 1.0, 0.0), sprec.RotationMat4(sprec.Degrees(-25.0), 1.0, 0.0, 0.0), sprec.TranslationMat4(0.0, 0.0, cameraStand.CameraDistance), )) diff --git a/cmd/rallymka/internal/ecs/transform.go b/cmd/rallymka/internal/ecs/transform.go deleted file mode 100644 index d7193e9..0000000 --- a/cmd/rallymka/internal/ecs/transform.go +++ /dev/null @@ -1,60 +0,0 @@ -package ecs - -import "github.com/mokiat/gomath/sprec" - -type TransformComponent struct { - Position sprec.Vec3 - Orientation Orientation -} - -func (c TransformComponent) Matrix() sprec.Mat4 { - return sprec.TransformationMat4( - c.Orientation.VectorX, - c.Orientation.VectorY, - c.Orientation.VectorZ, - c.Position, - ) -} - -func NewOrientation() Orientation { - return Orientation{ - VectorX: sprec.BasisXVec3(), - VectorY: sprec.BasisYVec3(), - VectorZ: sprec.BasisZVec3(), - } -} - -type Orientation struct { - VectorX sprec.Vec3 - VectorY sprec.Vec3 - VectorZ sprec.Vec3 -} - -func (o *Orientation) Rotate(rotation sprec.Vec3) { - length := rotation.Length() - if length < 0.00001 { - return - } - matrix := sprec.RotationMat4(sprec.Radians(length), rotation.X, rotation.Y, rotation.Z) - orientationMatrix := sprec.TransformationMat4( - o.VectorX, - o.VectorY, - o.VectorZ, - sprec.ZeroVec3(), - ) - result := sprec.Mat4Prod(matrix, orientationMatrix) - o.VectorX = sprec.NewVec3(result.M11, result.M21, result.M31) - o.VectorY = sprec.NewVec3(result.M12, result.M22, result.M32) - o.VectorZ = sprec.NewVec3(result.M13, result.M23, result.M33) -} - -func (o Orientation) MulVec3(vec sprec.Vec3) sprec.Vec3 { - matrix := sprec.TransformationMat4( - o.VectorX, - o.VectorY, - o.VectorZ, - sprec.ZeroVec3(), - ) - result := sprec.Mat4Vec4Prod(matrix, sprec.NewVec4(vec.X, vec.Y, vec.Z, 1.0)) - return result.VecXYZ() -} diff --git a/cmd/rallymka/internal/ecs/vehicle.go b/cmd/rallymka/internal/ecs/vehicle.go deleted file mode 100644 index 1fed31c..0000000 --- a/cmd/rallymka/internal/ecs/vehicle.go +++ /dev/null @@ -1,308 +0,0 @@ -package ecs - -import ( - "time" - - "github.com/mokiat/gomath/sprec" - "github.com/mokiat/rally-mka/internal/engine/collision" -) - -const ( - gravity = 0.01 - wheelFriction = 99.0 / 100.0 - speedFriction = 99.0 / 100.0 - speedFriction2 = 1.0 - (99.0 / 100.0) - rotationFriction = 60.0 / 100.0 - rotationFriction2 = 1.0 - 60.0/100.0 - maxSuspensionLength = 0.4 - skidWheelSpeed = 0.03 -) - -type CarInput struct { - Forward bool - Backward bool - TurnLeft bool - TurnRight bool - Handbrake bool -} - -type Stage interface { - CheckCollision(line collision.Line) (collision.LineCollision, bool) -} - -func NewVehicleSystem(ecsManager *Manager, stage Stage) *VehicleSystem { - return &VehicleSystem{ - ecsManager: ecsManager, - stage: stage, - } -} - -type VehicleSystem struct { - ecsManager *Manager - stage Stage -} - -func (s *VehicleSystem) Update(elapsedTime time.Duration, input CarInput) { - for _, entity := range s.ecsManager.Entities() { - if vehicle := entity.Vehicle; vehicle != nil { - if entity.Input != nil { - s.updateVehicleInput(entity, elapsedTime, input) - } - s.updateVehicle(entity, elapsedTime) - } - } -} - -func (s *VehicleSystem) updateVehicleInput(entity *Entity, elapsedTime time.Duration, input CarInput) { - // TODO: Move constants as part of car descriptor - const turnSpeed = 100 // FIXME ORIGINAL: 120 - const returnSpeed = 50 // FIXME ORIGINAL: 60 - const maxWheelAngle = 30 // FIXME ORIGINAL: 30 - const maxAcceleration = 0.6 // FIXME ORIGINAL: 0.01 - const maxDeceleration = 0.3 // FIXME ORIGINAL: 0.005 - - elapsedSeconds := float32(elapsedTime.Seconds()) - vehicle := entity.Vehicle - - switch { - case input.TurnLeft == input.TurnRight: - if vehicle.SteeringAngle > 0.001 { - if vehicle.SteeringAngle -= sprec.Degrees(elapsedSeconds * returnSpeed); vehicle.SteeringAngle < 0.0 { - vehicle.SteeringAngle = 0.0 - } - } - if vehicle.SteeringAngle < -0.001 { - if vehicle.SteeringAngle += sprec.Degrees(elapsedSeconds * returnSpeed); vehicle.SteeringAngle > 0.0 { - vehicle.SteeringAngle = 0.0 - } - } - case input.TurnLeft: - if vehicle.SteeringAngle += sprec.Degrees(elapsedSeconds * turnSpeed); vehicle.SteeringAngle > sprec.Degrees(maxWheelAngle) { - vehicle.SteeringAngle = sprec.Degrees(maxWheelAngle) - } - case input.TurnRight: - if vehicle.SteeringAngle -= sprec.Degrees(elapsedSeconds * turnSpeed); vehicle.SteeringAngle < -sprec.Degrees(maxWheelAngle) { - vehicle.SteeringAngle = -sprec.Degrees(maxWheelAngle) - } - } - vehicle.Acceleration = 0.0 - if input.Forward { - vehicle.Acceleration = maxAcceleration * elapsedSeconds - } - if input.Backward { - vehicle.Acceleration = -maxDeceleration * elapsedSeconds - } - vehicle.HandbrakePulled = input.Handbrake -} - -func (s *VehicleSystem) updateVehicle(entity *Entity, elapsedTime time.Duration) { - car := entity.Vehicle - flWheel := entity.Vehicle.FLWheel.Wheel - frWheel := entity.Vehicle.FRWheel.Wheel - blWheel := entity.Vehicle.BLWheel.Wheel - brWheel := entity.Vehicle.BRWheel.Wheel - - flWheel.SteeringAngle = car.SteeringAngle - frWheel.SteeringAngle = car.SteeringAngle - - s.calculateWheelRotation(car, flWheel) - s.calculateWheelRotation(car, frWheel) - s.calculateWheelRotation(car, blWheel) - s.calculateWheelRotation(car, brWheel) - s.accelerateCar(car, flWheel, frWheel, blWheel, brWheel) - s.translateCar(car) - - s.checkWheelSideCollision(car, flWheel, sprec.InverseVec3(car.Orientation.VectorX), sprec.InverseVec3(car.Orientation.VectorZ)) - s.checkWheelSideCollision(car, frWheel, car.Orientation.VectorX, sprec.InverseVec3(car.Orientation.VectorZ)) - s.checkWheelSideCollision(car, blWheel, sprec.InverseVec3(car.Orientation.VectorX), car.Orientation.VectorZ) - s.checkWheelSideCollision(car, brWheel, car.Orientation.VectorX, car.Orientation.VectorZ) - s.checkWheelBottomCollision(car, flWheel) - s.checkWheelBottomCollision(car, frWheel) - s.checkWheelBottomCollision(car, blWheel) - s.checkWheelBottomCollision(car, brWheel) - - s.updateCarModelMatrix(entity) - s.updateWheelModelMatrix(entity, entity.Vehicle.FLWheel) - s.updateWheelModelMatrix(entity, entity.Vehicle.FRWheel) - s.updateWheelModelMatrix(entity, entity.Vehicle.BLWheel) - s.updateWheelModelMatrix(entity, entity.Vehicle.BRWheel) -} - -func (s *VehicleSystem) calculateWheelRotation(car *Vehicle, wheel *Wheel) { - // Handbrake locks all wheels - if wheel.IsGrounded && !car.HandbrakePulled { - wheelOrientation := car.Orientation - wheelOrientation.Rotate(sprec.ResizedVec3(wheelOrientation.VectorY, wheel.SteeringAngle.Radians())) - wheelTravelDistance := sprec.Vec3Dot(car.Velocity, wheelOrientation.VectorZ) - s.rotateWheel(wheel, wheelTravelDistance) - } - - // Add some wheel slip if we are accelerating - if sprec.Abs(car.Acceleration) > 0.0001 && wheel.IsDriven { - s.rotateWheel(wheel, sprec.Sign(car.Acceleration)*skidWheelSpeed) - } -} - -func (s *VehicleSystem) accelerateCar(car *Vehicle, flWheel, frWheel, blWheel, brWheel *Wheel) { - shouldAccelerate := - (flWheel.IsDriven && flWheel.IsGrounded) || - (frWheel.IsDriven && frWheel.IsGrounded) || - (blWheel.IsDriven && blWheel.IsGrounded) || - (brWheel.IsDriven && brWheel.IsGrounded) - - if shouldAccelerate { - acceleration := sprec.ResizedVec3(car.Orientation.VectorZ, car.Acceleration) - car.Velocity = sprec.Vec3Sum(car.Velocity, acceleration) - } - - shouldDecelerate := car.HandbrakePulled && - (flWheel.IsGrounded || frWheel.IsGrounded || blWheel.IsGrounded || brWheel.IsGrounded) - - if shouldDecelerate { - car.Velocity = sprec.Vec3Prod(car.Velocity, wheelFriction) - } - - car.Velocity = sprec.Vec3Diff(car.Velocity, sprec.NewVec3(0.0, gravity, 0.0)) - car.Velocity = sprec.Vec3Diff(car.Velocity, sprec.Vec3Prod(car.Velocity, speedFriction2)) - - // no idea how I originally go to this - magicVelocity := sprec.Vec3Dot(car.Velocity, car.Orientation.VectorZ) + sprec.Vec3Dot(car.Velocity, car.Orientation.VectorX)*sprec.Sin(car.SteeringAngle) - var turnAcceleration float32 - if !car.HandbrakePulled { - turnAcceleration = car.SteeringAngle.Degrees() * magicVelocity / (100.0 + 2.0*magicVelocity*magicVelocity) - } - turnAcceleration += car.SteeringAngle.Degrees() * car.Acceleration * 0.6 - turnAcceleration /= 2.0 - - if flWheel.IsGrounded || frWheel.IsGrounded { - car.AngularVelocity = sprec.Vec3Sum(car.AngularVelocity, sprec.ResizedVec3(car.Orientation.VectorY, turnAcceleration)) - } - car.AngularVelocity = sprec.Vec3Prod(car.AngularVelocity, rotationFriction) - car.AngularVelocity = sprec.Vec3Diff(car.AngularVelocity, sprec.Vec3Prod(car.AngularVelocity, rotationFriction2)) -} - -func (s *VehicleSystem) translateCar(car *Vehicle) { - car.Position = sprec.Vec3Sum(car.Position, car.Velocity) - car.Orientation.Rotate(car.AngularVelocity) -} - -func (s *VehicleSystem) checkWheelSideCollision(car *Vehicle, wheel *Wheel, dirX sprec.Vec3, dirZ sprec.Vec3) { - pa := s.wheelAbsolutePosition(car, wheel) - - p2 := sprec.Vec3Diff(pa, sprec.ResizedVec3(dirX, wheel.Length)) - p1 := sprec.Vec3Sum(pa, sprec.ResizedVec3(dirX, 1.2)) - result, active := s.stage.CheckCollision(collision.MakeLine(p1, p2)) - if active { - f := sprec.ResizedVec3(result.Normal(), sprec.Abs(result.BottomHeight())) - car.Position = sprec.Vec3Sum(car.Position, f) - collisionVelocity := sprec.Vec3Dot(result.Normal(), car.Velocity) - car.Velocity = sprec.Vec3Diff(car.Velocity, sprec.Vec3Prod(result.Normal(), collisionVelocity)) - } - - p2 = sprec.Vec3Sum(pa, sprec.ResizedVec3(dirZ, sprec.Abs(wheel.Radius))) - p1 = sprec.Vec3Diff(pa, sprec.ResizedVec3(dirZ, 1.2)) - result, active = s.stage.CheckCollision(collision.MakeLine(p1, p2)) - if active { - f := sprec.ResizedVec3(result.Normal(), sprec.Abs(result.BottomHeight())) - car.Position = sprec.Vec3Sum(car.Position, f) - collisionVelocity := sprec.Vec3Dot(result.Normal(), car.Velocity) - car.Velocity = sprec.Vec3Diff(car.Velocity, sprec.Vec3Prod(result.Normal(), collisionVelocity)) - } -} - -func (s *VehicleSystem) checkWheelBottomCollision(car *Vehicle, wheel *Wheel) { - wheel.IsGrounded = false - - pa := sprec.Vec3Sum(car.Position, car.Orientation.MulVec3(wheel.AnchorPosition)) - p1 := sprec.Vec3Sum(pa, sprec.ResizedVec3(car.Orientation.VectorY, 3.6)) - p2 := sprec.Vec3Diff(pa, sprec.ResizedVec3(car.Orientation.VectorY, wheel.Radius+maxSuspensionLength)) - - result, active := s.stage.CheckCollision(collision.MakeLine(p1, p2)) - if !active { - wheel.SuspensionLength = maxSuspensionLength - return - } - - dis := sprec.Vec3Diff(result.Intersection(), p1).Length() - if dis > 3.6+wheel.Radius { - wheel.SuspensionLength = dis - (3.6 + wheel.Radius) - } else { - wheel.SuspensionLength = 0.0 - } - wheel.IsGrounded = true - - if dis < 3.6+wheel.Radius { - dis2 := sprec.Vec3Dot(result.Normal(), car.Velocity) - f := sprec.ResizedVec3(result.Normal(), dis2) - car.Velocity = sprec.Vec3Diff(car.Velocity, f) - f = sprec.ResizedVec3(car.Orientation.VectorY, 3.6+wheel.Radius-dis) - car.Position = sprec.Vec3Sum(car.Position, f) - } - - wheelAbsolutePosition := s.wheelAbsolutePosition(car, wheel) - relativePosition := sprec.Vec3Diff(wheelAbsolutePosition, car.Position) - cross := sprec.Vec3Cross(result.Normal(), sprec.InverseVec3(relativePosition)) - cross = sprec.UnitVec3(cross) - - koef := sprec.ZeroVec2() - koef.Y = sprec.Vec3Dot(sprec.InverseVec3(result.Intersection()), result.Intersection()) - tmp := sprec.InverseVec3(result.Intersection()).Length() - koef.X = sprec.Sqrt(tmp*tmp - koef.Y*koef.Y) - - if koef.Length() > 0.0000001 { - koef = sprec.UnitVec2(koef) - } else { - koef.X = 1.0 - koef.Y = 0.0 - } - - if sprec.Abs(koef.X) > 0.0000001 { - cross = sprec.ResizedVec3(cross, koef.X*koef.X*(1.0-(dis-(3.6+wheel.Radius))/maxSuspensionLength)/10) - car.AngularVelocity = sprec.Vec3Sum(car.AngularVelocity, cross) - } -} - -func (s *VehicleSystem) wheelAbsolutePosition(car *Vehicle, wheel *Wheel) sprec.Vec3 { - worldPosition := car.Position - worldPosition = sprec.Vec3Sum(worldPosition, car.Orientation.MulVec3(wheel.AnchorPosition)) - worldPosition = sprec.Vec3Sum(worldPosition, car.Orientation.MulVec3(sprec.NewVec3(0.0, -wheel.SuspensionLength, 0.0))) - return worldPosition -} - -func (s *VehicleSystem) rotateWheel(wheel *Wheel, speed float32) { - wheel.RotationAngle += sprec.Radians(speed / wheel.Radius) -} - -func (s *VehicleSystem) updateCarModelMatrix(car *Entity) { - vehicleComp := car.Vehicle - if car.Transform != nil { - car.Transform.Position = vehicleComp.Position - car.Transform.Orientation = vehicleComp.Orientation - } -} - -func (s *VehicleSystem) updateWheelModelMatrix(car *Entity, wheel *Entity) { - vehicleComp := car.Vehicle - wheelComp := wheel.Wheel - - modelMatrix := sprec.Mat4MultiProd( - sprec.TransformationMat4( - vehicleComp.Orientation.VectorX, - vehicleComp.Orientation.VectorY, - vehicleComp.Orientation.VectorZ, - vehicleComp.Position, - ), - sprec.TranslationMat4(wheelComp.AnchorPosition.X, wheelComp.AnchorPosition.Y-wheelComp.SuspensionLength, wheelComp.AnchorPosition.Z), - sprec.RotationMat4(wheelComp.SteeringAngle, 0.0, 1.0, 0.0), - sprec.RotationMat4(wheelComp.RotationAngle, 1.0, 0.0, 0.0), - ) - - if wheel.Transform != nil { - wheel.Transform.Position = modelMatrix.Translation() - wheel.Transform.Orientation = Orientation{ - VectorX: modelMatrix.OrientationX(), - VectorY: modelMatrix.OrientationY(), - VectorZ: modelMatrix.OrientationZ(), - } - } -} diff --git a/cmd/rallymka/internal/game/app.go b/cmd/rallymka/internal/game/app.go index 55c8cdd..f04cfdb 100644 --- a/cmd/rallymka/internal/game/app.go +++ b/cmd/rallymka/internal/game/app.go @@ -3,7 +3,6 @@ package game import ( "fmt" "os" - "path/filepath" "time" "github.com/go-gl/gl/v4.1-core/gl" @@ -42,12 +41,7 @@ func (a Application) Run() error { return fmt.Errorf("failed to initialize opengl: %w", err) } - assetsDir := filepath.Join(filepath.Dir(os.Args[0]), "..", "Resources", "assets") - if !dirExists(assetsDir) { - assetsDir = "assets" - } - - controller := NewController(assetsDir) + controller := NewController() controller.OnGLInit() go func() { diff --git a/cmd/rallymka/internal/game/controller.go b/cmd/rallymka/internal/game/controller.go index 4cca18c..d3e6054 100644 --- a/cmd/rallymka/internal/game/controller.go +++ b/cmd/rallymka/internal/game/controller.go @@ -1,6 +1,8 @@ package game import ( + "os" + "path/filepath" "time" "github.com/mokiat/rally-mka/cmd/rallymka/internal/game/input" @@ -30,7 +32,12 @@ type View interface { Render(pipeline *graphics.Pipeline) } -func NewController(assetsDir string) *Controller { +func NewController() *Controller { + assetsDir := filepath.Join(filepath.Dir(os.Args[0]), "..", "Resources", "assets") + if !dirExists(assetsDir) { + assetsDir = "assets" + } + resWorker := resource.NewWorker(maxQueuedResources) resRegistry := resource.NewRegistry(resWorker, maxResources, maxEvents) gfxWorker := graphics.NewWorker() diff --git a/cmd/rallymka/internal/game/simulation/view.go b/cmd/rallymka/internal/game/simulation/view.go index fae74ff..f91d9df 100644 --- a/cmd/rallymka/internal/game/simulation/view.go +++ b/cmd/rallymka/internal/game/simulation/view.go @@ -15,7 +15,7 @@ func NewView(registry *resource.Registry, gfxWorker *graphics.Worker) *View { return &View{ gameData: scene.NewData(registry, gfxWorker), camera: ecs.NewCamera(), - stage: scene.NewStage(), + stage: scene.NewStage(gfxWorker), } } diff --git a/cmd/rallymka/internal/scene/car/chassis.go b/cmd/rallymka/internal/scene/car/chassis.go new file mode 100644 index 0000000..53ecd52 --- /dev/null +++ b/cmd/rallymka/internal/scene/car/chassis.go @@ -0,0 +1,80 @@ +package car + +import ( + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/rally-mka/cmd/rallymka/internal/ecs" + "github.com/mokiat/rally-mka/cmd/rallymka/internal/stream" + "github.com/mokiat/rally-mka/internal/engine/graphics" + "github.com/mokiat/rally-mka/internal/engine/physics" + "github.com/mokiat/rally-mka/internal/engine/shape" +) + +const ( + chassisRadius = 2 + chassisMass = 1300.0 / 5.0 + // chassisMass = 1300.0 / 10.0 + chassisMomentOfInertia = chassisMass * chassisRadius * chassisRadius / 2.0 + chassisDragFactor = 0.0 // 0.5 * 6.8 * 1.0 + chassisAngularDragFactor = 0.0 // 0.5 * 6.8 * 1.0 + chassisRestitutionCoef = 0.3 +) + +func Chassis(program *graphics.Program, model *stream.Model) *ChassisBuilder { + return &ChassisBuilder{ + program: program, + model: model, + } +} + +type ChassisBuilder struct { + program *graphics.Program + model *stream.Model + modifiers []func(entity *ecs.Entity) +} + +func (b *ChassisBuilder) WithName(name string) *ChassisBuilder { + b.modifiers = append(b.modifiers, func(entity *ecs.Entity) { + entity.Physics.Body.Name = name + }) + return b +} + +func (b *ChassisBuilder) WithPosition(position sprec.Vec3) *ChassisBuilder { + b.modifiers = append(b.modifiers, func(entity *ecs.Entity) { + entity.Physics.Body.Position = position + }) + return b +} + +func (b *ChassisBuilder) Build(ecsManager *ecs.Manager) *ecs.Entity { + bodyNode, _ := b.model.FindNode("body") + + entity := ecsManager.CreateEntity() + entity.Physics = &ecs.PhysicsComponent{ + Body: &physics.Body{ + Position: sprec.ZeroVec3(), + Orientation: sprec.IdentityQuat(), + Mass: chassisMass, + MomentOfInertia: physics.SymmetricMomentOfInertia(chassisMomentOfInertia), + DragFactor: chassisDragFactor, + AngularDragFactor: chassisAngularDragFactor, + RestitutionCoef: chassisRestitutionCoef, + CollisionShapes: []shape.Placement{ + { + Position: sprec.NewVec3(0.0, 0.2, -0.3), + Orientation: sprec.IdentityQuat(), + Shape: shape.NewStaticBox(1.6, 1.2, 3.8), + }, + }, + }, + } + entity.Render = &ecs.RenderComponent{ + GeomProgram: b.program, + Mesh: bodyNode.Mesh, + Matrix: sprec.IdentityMat4(), + } + for _, modifier := range b.modifiers { + modifier(entity) + } + return entity +} diff --git a/cmd/rallymka/internal/scene/car/wheel.go b/cmd/rallymka/internal/scene/car/wheel.go new file mode 100644 index 0000000..f1181fd --- /dev/null +++ b/cmd/rallymka/internal/scene/car/wheel.go @@ -0,0 +1,93 @@ +package car + +import ( + "fmt" + + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/rally-mka/cmd/rallymka/internal/ecs" + "github.com/mokiat/rally-mka/cmd/rallymka/internal/stream" + "github.com/mokiat/rally-mka/internal/engine/graphics" + "github.com/mokiat/rally-mka/internal/engine/physics" + "github.com/mokiat/rally-mka/internal/engine/shape" +) + +const ( + wheelRadius = 0.3 + wheelMass = 20.0 // wheel: ~12kg; rim: ~8kg + wheelMomentOfInertia = wheelMass * wheelRadius * wheelRadius / 2.0 // using cylinder as approximation + wheelDragFactor = 0.0 // 0.5 * 0.3 * 0.8 + wheelAngularDragFactor = 0.0 // 0.5 * 0.3 * 0.8 + wheelRestitutionCoef = 0.5 +) + +type WheelLocation string + +const ( + FrontLeftWheelLocation WheelLocation = "front_left" + FrontRightWheelLocation WheelLocation = "front_right" + BackLeftWheelLocation WheelLocation = "back_left" + BackRightWheelLocation WheelLocation = "back_right" +) + +func Wheel(program *graphics.Program, model *stream.Model, location WheelLocation) *WheelBuilder { + return &WheelBuilder{ + program: program, + model: model, + location: location, + } +} + +type WheelBuilder struct { + program *graphics.Program + model *stream.Model + location WheelLocation + modifiers []func(entity *ecs.Entity) +} + +func (b *WheelBuilder) WithName(name string) *WheelBuilder { + b.modifiers = append(b.modifiers, func(entity *ecs.Entity) { + entity.Physics.Body.Name = name + }) + return b +} + +func (b *WheelBuilder) WithPosition(position sprec.Vec3) *WheelBuilder { + b.modifiers = append(b.modifiers, func(entity *ecs.Entity) { + entity.Physics.Body.Position = position + }) + return b +} + +func (b *WheelBuilder) Build(ecsManager *ecs.Manager) *ecs.Entity { + modelNode, _ := b.model.FindNode(fmt.Sprintf("wheel_%s", b.location)) + + entity := ecsManager.CreateEntity() + entity.Physics = &ecs.PhysicsComponent{ + Body: &physics.Body{ + Position: sprec.ZeroVec3(), + Orientation: sprec.IdentityQuat(), + Mass: wheelMass, + MomentOfInertia: physics.SymmetricMomentOfInertia(wheelMomentOfInertia), + DragFactor: wheelDragFactor, + AngularDragFactor: wheelAngularDragFactor, + RestitutionCoef: wheelRestitutionCoef, + // using sphere shape at is easier to do in physics engine at the moment + CollisionShapes: []shape.Placement{ + { + Position: sprec.ZeroVec3(), + Orientation: sprec.IdentityQuat(), + Shape: shape.NewStaticSphere(0.3), + }, + }, + }, + } + entity.Render = &ecs.RenderComponent{ + GeomProgram: b.program, + Mesh: modelNode.Mesh, + Matrix: sprec.IdentityMat4(), + } + for _, modifier := range b.modifiers { + modifier(entity) + } + return entity +} diff --git a/cmd/rallymka/internal/scene/data.go b/cmd/rallymka/internal/scene/data.go index a674502..81ccb8b 100644 --- a/cmd/rallymka/internal/scene/data.go +++ b/cmd/rallymka/internal/scene/data.go @@ -22,6 +22,7 @@ func NewData(registry *resource.Registry, gfxWorker *graphics.Worker) *Data { DeferredGeometryProgram: stream.GetProgram(registry, "deferred-geometry"), DeferredLightingProgram: stream.GetProgram(registry, "deferred-lighting"), QuadMesh: stream.GetMesh(registry, "quad"), + DebugProgram: stream.GetProgram(registry, "debug"), GeometryFramebuffer: &graphics.Framebuffer{}, LightingFramebuffer: &graphics.Framebuffer{}, } @@ -43,6 +44,8 @@ type Data struct { DeferredLightingProgram stream.ProgramHandle QuadMesh stream.MeshHandle + DebugProgram stream.ProgramHandle + gfxTask *graphics.Task GeometryFramebuffer *graphics.Framebuffer LightingFramebuffer *graphics.Framebuffer @@ -59,6 +62,7 @@ func (d *Data) Request() { d.registry.Request(d.DeferredGeometryProgram.Handle) d.registry.Request(d.DeferredLightingProgram.Handle) d.registry.Request(d.QuadMesh.Handle) + d.registry.Request(d.DebugProgram.Handle) d.gfxTask = d.gfxWorker.Schedule(func() error { geometryFramebufferData := graphics.FramebufferData{ Width: framebufferWidth, @@ -94,6 +98,7 @@ func (d *Data) Dismiss() { d.registry.Dismiss(d.DeferredGeometryProgram.Handle) d.registry.Dismiss(d.DeferredLightingProgram.Handle) d.registry.Dismiss(d.QuadMesh.Handle) + d.registry.Dismiss(d.DebugProgram.Handle) geometryFramebuffer := d.GeometryFramebuffer lightingFramebuffer := d.LightingFramebuffer @@ -120,5 +125,6 @@ func (d *Data) IsAvailable() bool { d.DeferredGeometryProgram.IsAvailable() && d.DeferredLightingProgram.IsAvailable() && d.QuadMesh.IsAvailable() && + d.DebugProgram.IsAvailable() && (d.gfxTask != nil && d.gfxTask.Done()) } diff --git a/cmd/rallymka/internal/scene/stage.go b/cmd/rallymka/internal/scene/stage.go index 1c7b02b..4a28291 100644 --- a/cmd/rallymka/internal/scene/stage.go +++ b/cmd/rallymka/internal/scene/stage.go @@ -5,9 +5,12 @@ import ( "github.com/mokiat/gomath/sprec" "github.com/mokiat/rally-mka/cmd/rallymka/internal/ecs" + "github.com/mokiat/rally-mka/cmd/rallymka/internal/scene/car" "github.com/mokiat/rally-mka/cmd/rallymka/internal/stream" - "github.com/mokiat/rally-mka/internal/engine/collision" + "github.com/mokiat/rally-mka/internal/data" "github.com/mokiat/rally-mka/internal/engine/graphics" + "github.com/mokiat/rally-mka/internal/engine/physics" + "github.com/mokiat/rally-mka/internal/engine/shape" ) const ( @@ -16,9 +19,10 @@ const ( ) const ( - carDropHeight = 1.6 - anchorDistance = 4.0 - cameraDistance = 8.0 + carDropHeight = 1.6 + entityVisualHeight = 5.0 + anchorDistance = 4.0 + cameraDistance = 8.0 ) type CarInput struct { @@ -29,23 +33,51 @@ type CarInput struct { Handbrake bool } -func NewStage() *Stage { +const maxDebugLines = 1024 * 6 + +var arrayTask *graphics.Task + +func NewStage(gfxWorker *graphics.Worker) *Stage { + indexData := make([]byte, maxDebugLines*2) + for i := 0; i < maxDebugLines; i++ { + data.Buffer(indexData).SetUInt16(i*2, uint16(i)) + } + vertexData := make([]byte, maxDebugLines*4*7*2) + debugVertexArrayData := graphics.VertexArrayData{ + VertexData: vertexData, + VertexStride: 4 * 7, + CoordOffset: 0, + ColorOffset: 4 * 3, + IndexData: indexData, + } + debugVertexArray := &graphics.VertexArray{} + arrayTask = gfxWorker.Schedule(func() error { + if err := debugVertexArray.Allocate(debugVertexArrayData); err != nil { + panic(err) + } + return nil + }) // FIXME: Race condition + ecsManager := ecs.NewManager() stage := &Stage{ ecsManager: ecsManager, ecsRenderer: ecs.NewRenderer(ecsManager), + ecsCarSystem: ecs.NewCarSystem(ecsManager), ecsCameraStandSystem: ecs.NewCameraStandSystem(ecsManager), + physicsEngine: physics.NewEngine(15 * time.Millisecond), screenFramebuffer: &graphics.Framebuffer{}, + debugVertexArray: debugVertexArray, + debugVertexArrayData: debugVertexArrayData, } - stage.ecsVehicleSystem = ecs.NewVehicleSystem(ecsManager, stage) return stage } type Stage struct { ecsManager *ecs.Manager ecsRenderer *ecs.Renderer - ecsVehicleSystem *ecs.VehicleSystem + ecsCarSystem *ecs.CarSystem ecsCameraStandSystem *ecs.CameraStandSystem + physicsEngine *physics.Engine geometryFramebuffer *graphics.Framebuffer lightingFramebuffer *graphics.Framebuffer @@ -53,12 +85,19 @@ type Stage struct { lightingProgram *graphics.Program quadMesh *stream.Mesh - collisionMeshes []*collision.Mesh + debugProgram *graphics.Program + debugVertexArray *graphics.VertexArray + debugVertexArrayData graphics.VertexArrayData + debugLines []DebugLine } +var targetEntity *ecs.Entity + func (s *Stage) Init(data *Data, camera *ecs.Camera) { level := data.Level.Get() + s.debugProgram = data.DebugProgram.Get() + s.geometryFramebuffer = data.GeometryFramebuffer s.lightingFramebuffer = data.LightingFramebuffer @@ -67,92 +106,55 @@ func (s *Stage) Init(data *Data, camera *ecs.Camera) { for _, staticMesh := range level.StaticMeshes { entity := s.ecsManager.CreateEntity() - entity.Transform = &ecs.TransformComponent{ - Position: sprec.ZeroVec3(), - Orientation: ecs.NewOrientation(), - } - entity.RenderMesh = &ecs.RenderMesh{ + entity.Render = &ecs.RenderComponent{ GeomProgram: data.TerrainProgram.Get(), Mesh: staticMesh, + Matrix: sprec.IdentityMat4(), } } - s.collisionMeshes = level.CollisionMeshes + for _, collisionMesh := range level.CollisionMeshes { + s.physicsEngine.AddBody(&physics.Body{ + Position: sprec.ZeroVec3(), + Orientation: sprec.IdentityQuat(), + IsStatic: true, + RestitutionCoef: 1.0, + CollisionShapes: []shape.Placement{collisionMesh}, + }) + } for _, staticEntity := range level.StaticEntities { entity := s.ecsManager.CreateEntity() - entity.Transform = &ecs.TransformComponent{ - Position: staticEntity.Matrix.Translation(), - Orientation: ecs.Orientation{ - VectorX: staticEntity.Matrix.OrientationX(), - VectorY: staticEntity.Matrix.OrientationY(), - VectorZ: staticEntity.Matrix.OrientationZ(), - }, - } - entity.RenderModel = &ecs.RenderModel{ + entity.Render = &ecs.RenderComponent{ GeomProgram: data.EntityProgram.Get(), Model: staticEntity.Model.Get(), + Matrix: staticEntity.Matrix, } } carProgram := data.CarProgram.Get() carModel := data.CarModel.Get() - // spawn car - { - bodyNode, _ := carModel.FindNode("body") - flWheelNode, _ := carModel.FindNode("wheel_front_left") - frWheelNode, _ := carModel.FindNode("wheel_front_right") - blWheelNode, _ := carModel.FindNode("wheel_back_left") - brWheelNode, _ := carModel.FindNode("wheel_back_right") - - createWheelEntity := func(node *stream.Node, isDriven bool) *ecs.Entity { - wheelEntity := s.ecsManager.CreateEntity() - wheelEntity.Transform = &ecs.TransformComponent{ - Position: sprec.ZeroVec3(), - Orientation: ecs.NewOrientation(), - } - wheelEntity.Wheel = &ecs.Wheel{ - IsDriven: isDriven, - Length: 0.4, - Radius: 0.3, - AnchorPosition: sprec.Vec3Diff(node.Matrix.Translation(), bodyNode.Matrix.Translation()), - } - wheelEntity.RenderMesh = &ecs.RenderMesh{ - GeomProgram: carProgram, - Mesh: node.Mesh, - } - return wheelEntity - } + // targetEntity = + // s.setupChandelierDemo(carProgram, carModel, sprec.NewVec3(0.0, 10.0, 0.0)) - carEntity := s.ecsManager.CreateEntity() - carEntity.Transform = &ecs.TransformComponent{ - Position: sprec.ZeroVec3(), - Orientation: ecs.NewOrientation(), - } - carEntity.RenderMesh = &ecs.RenderMesh{ - GeomProgram: carProgram, - Mesh: bodyNode.Mesh, - } - carEntity.Input = &ecs.Input{} - carEntity.Vehicle = &ecs.Vehicle{ - Position: sprec.NewVec3(0.0, carDropHeight, 0.0), - Orientation: ecs.NewOrientation(), - - FLWheel: createWheelEntity(flWheelNode, true), - FRWheel: createWheelEntity(frWheelNode, true), - BLWheel: createWheelEntity(blWheelNode, false), - BRWheel: createWheelEntity(brWheelNode, false), - } + // targetEntity = + // s.setupRodDemo(carProgram, carModel, sprec.NewVec3(0.0, 10.0, 5.0)) - standEntity := s.ecsManager.CreateEntity() - standEntity.CameraStand = &ecs.CameraStand{ - Target: carEntity, - Camera: camera, - AnchorPosition: sprec.NewVec3(0.0, carDropHeight, -cameraDistance), - AnchorDistance: anchorDistance, - CameraDistance: cameraDistance, - } + // targetEntity = + // s.setupCoiloverDemo(carProgram, carModel, sprec.NewVec3(0.0, 10.0, -5.0)) + + targetEntity = + s.setupCarDemo(carProgram, carModel, sprec.NewVec3(0.0, 2.0, 10.0)) + + standTarget := targetEntity + standEntity := s.ecsManager.CreateEntity() + standEntity.CameraStand = &ecs.CameraStand{ + Target: standTarget, + Camera: camera, + AnchorPosition: sprec.Vec3Sum(standTarget.Physics.Body.Position, sprec.NewVec3(0.0, 0.0, -cameraDistance)), + AnchorDistance: anchorDistance, + CameraDistance: cameraDistance, } { @@ -165,17 +167,274 @@ func (s *Stage) Init(data *Data, camera *ecs.Camera) { } } +func (s *Stage) setupChandelierDemo(program *graphics.Program, model *stream.Model, position sprec.Vec3) *ecs.Entity { + fakeFixtureWheel := car.Wheel(program, model, car.FrontRightWheelLocation). + WithPosition(position). + Build(s.ecsManager) + s.physicsEngine.AddBody(fakeFixtureWheel.Physics.Body) + s.physicsEngine.AddConstraint(physics.FixedTranslationConstraint{ + Fixture: position, + Body: fakeFixtureWheel.Physics.Body, + }) + + playWheel := car.Wheel(program, model, car.FrontRightWheelLocation). + WithPosition(sprec.Vec3Sum(position, sprec.NewVec3(-2.3, 0.0, 0.0))). + Build(s.ecsManager) + s.physicsEngine.AddBody(playWheel.Physics.Body) + s.physicsEngine.AddConstraint(physics.ChandelierConstraint{ + Fixture: position, + Body: playWheel.Physics.Body, + BodyAnchor: sprec.NewVec3(0.3, 0.0, 0.0), + Length: 2.0, + }) + + return fakeFixtureWheel +} + +func (s *Stage) setupCoiloverDemo(program *graphics.Program, model *stream.Model, position sprec.Vec3) *ecs.Entity { + fixtureWheel := car.Wheel(program, model, car.FrontRightWheelLocation). + WithPosition(position). + Build(s.ecsManager) + s.physicsEngine.AddBody(fixtureWheel.Physics.Body) + s.physicsEngine.AddConstraint(physics.FixedTranslationConstraint{ + Fixture: position, + Body: fixtureWheel.Physics.Body, + }) + + fallingWheel := car.Wheel(program, model, car.FrontRightWheelLocation). + WithPosition(sprec.Vec3Sum(position, sprec.NewVec3(0.0, 2.0, 0.0))). + Build(s.ecsManager) + s.physicsEngine.AddBody(fallingWheel.Physics.Body) + s.physicsEngine.AddConstraint(&physics.CoiloverConstraint{ + FirstBody: fixtureWheel.Physics.Body, + SecondBody: fallingWheel.Physics.Body, + FrequencyHz: 4.5, + DampingRatio: 0.1, + }) + + return fixtureWheel +} + +func (s *Stage) setupRodDemo(program *graphics.Program, model *stream.Model, position sprec.Vec3) *ecs.Entity { + topWheelPosition := position + topWheel := car.Wheel(program, model, car.FrontRightWheelLocation). + WithPosition(topWheelPosition). + Build(s.ecsManager) + s.physicsEngine.AddBody(topWheel.Physics.Body) + s.physicsEngine.AddConstraint(physics.FixedTranslationConstraint{ + Fixture: topWheelPosition, + Body: topWheel.Physics.Body, + }) + + middleWheelPosition := sprec.Vec3Sum(topWheelPosition, sprec.NewVec3(1.4, 0.0, 0.0)) + middleWheel := car.Wheel(program, model, car.FrontRightWheelLocation). + WithPosition(middleWheelPosition). + Build(s.ecsManager) + s.physicsEngine.AddBody(middleWheel.Physics.Body) + s.physicsEngine.AddConstraint(physics.HingedRodConstraint{ + FirstBody: topWheel.Physics.Body, + FirstBodyAnchor: sprec.NewVec3(0.2, 0.0, 0.0), + SecondBody: middleWheel.Physics.Body, + SecondBodyAnchor: sprec.NewVec3(-0.2, 0.0, 0.0), + Length: 1.0, + }) + + bottomWheelPosition := sprec.Vec3Sum(middleWheelPosition, sprec.NewVec3(1.4, 0.0, 0.0)) + bottomWheel := car.Wheel(program, model, car.FrontRightWheelLocation). + WithPosition(bottomWheelPosition). + Build(s.ecsManager) + s.physicsEngine.AddBody(bottomWheel.Physics.Body) + s.physicsEngine.AddConstraint(physics.HingedRodConstraint{ + FirstBody: middleWheel.Physics.Body, + FirstBodyAnchor: sprec.NewVec3(0.2, 0.0, 0.0), + SecondBody: bottomWheel.Physics.Body, + SecondBodyAnchor: sprec.NewVec3(-0.2, 0.0, 0.0), + Length: 1.0, + }) + + return topWheel +} + +func (s *Stage) setupCarDemo(program *graphics.Program, model *stream.Model, position sprec.Vec3) *ecs.Entity { + chasis := car.Chassis(program, model). + WithName("chasis"). + WithPosition(position). + Build(s.ecsManager) + s.physicsEngine.AddBody(chasis.Physics.Body) + + suspensionEnabled := true + suspensionWidth := float32(1.0) + suspensionLength := float32(0.3) + suspensionFrequencyHz := float32(4.5) + suspensionDampingRatio := float32(1.0) + + flWheelRelativePosition := sprec.NewVec3(suspensionWidth, -0.6-suspensionLength/2.0, 1.25) + flWheel := car.Wheel(program, model, car.FrontLeftWheelLocation). + WithName("front-left-wheel"). + WithPosition(sprec.Vec3Sum(position, flWheelRelativePosition)). + Build(s.ecsManager) + s.physicsEngine.AddBody(flWheel.Physics.Body) + s.physicsEngine.AddConstraint(physics.MatchTranslationConstraint{ + FirstBody: chasis.Physics.Body, + FirstBodyAnchor: flWheelRelativePosition, + SecondBody: flWheel.Physics.Body, + IgnoreY: suspensionEnabled, + }) + s.physicsEngine.AddConstraint(physics.LimitTranslationConstraint{ + FirstBody: chasis.Physics.Body, + SecondBody: flWheel.Physics.Body, + MaxY: -0.5, + MinY: -1.0, + }) + flRotation := &physics.MatchAxisConstraint{ + FirstBody: chasis.Physics.Body, + FirstBodyAxis: sprec.BasisXVec3(), + SecondBody: flWheel.Physics.Body, + SecondBodyAxis: sprec.BasisXVec3(), + } + s.physicsEngine.AddConstraint(flRotation) + s.physicsEngine.AddConstraint(&physics.CoiloverConstraint{ + FirstBody: chasis.Physics.Body, + FirstBodyAnchor: flWheelRelativePosition, + SecondBody: flWheel.Physics.Body, + FrequencyHz: suspensionFrequencyHz, + DampingRatio: suspensionDampingRatio, + }) + + frWheelRelativePosition := sprec.NewVec3(-suspensionWidth, -0.6-suspensionLength/2.0, 1.25) + frWheel := car.Wheel(program, model, car.FrontRightWheelLocation). + WithName("front-right-wheel"). + WithPosition(sprec.Vec3Sum(position, frWheelRelativePosition)). + Build(s.ecsManager) + s.physicsEngine.AddBody(frWheel.Physics.Body) + s.physicsEngine.AddConstraint(physics.MatchTranslationConstraint{ + FirstBody: chasis.Physics.Body, + FirstBodyAnchor: frWheelRelativePosition, + SecondBody: frWheel.Physics.Body, + IgnoreY: suspensionEnabled, + }) + s.physicsEngine.AddConstraint(physics.LimitTranslationConstraint{ + FirstBody: chasis.Physics.Body, + SecondBody: frWheel.Physics.Body, + MaxY: -0.5, + MinY: -1.0, + }) + frRotation := &physics.MatchAxisConstraint{ + FirstBody: chasis.Physics.Body, + FirstBodyAxis: sprec.BasisXVec3(), + SecondBody: frWheel.Physics.Body, + SecondBodyAxis: sprec.BasisXVec3(), + } + s.physicsEngine.AddConstraint(frRotation) + s.physicsEngine.AddConstraint(&physics.CoiloverConstraint{ + FirstBody: chasis.Physics.Body, + FirstBodyAnchor: frWheelRelativePosition, + SecondBody: frWheel.Physics.Body, + FrequencyHz: suspensionFrequencyHz, + DampingRatio: suspensionDampingRatio, + }) + + blWheelRelativePosition := sprec.NewVec3(suspensionWidth, -0.6-suspensionLength/2.0, -1.45) + blWheel := car.Wheel(program, model, car.BackLeftWheelLocation). + WithName("back-left-wheel"). + WithPosition(sprec.Vec3Sum(position, blWheelRelativePosition)). + Build(s.ecsManager) + s.physicsEngine.AddBody(blWheel.Physics.Body) + s.physicsEngine.AddConstraint(physics.MatchTranslationConstraint{ + FirstBody: chasis.Physics.Body, + FirstBodyAnchor: blWheelRelativePosition, + SecondBody: blWheel.Physics.Body, + IgnoreY: suspensionEnabled, + }) + s.physicsEngine.AddConstraint(physics.LimitTranslationConstraint{ + FirstBody: chasis.Physics.Body, + SecondBody: blWheel.Physics.Body, + MaxY: -0.5, + MinY: -1.0, + }) + s.physicsEngine.AddConstraint(physics.MatchAxisConstraint{ + FirstBody: chasis.Physics.Body, + FirstBodyAxis: sprec.BasisXVec3(), + SecondBody: blWheel.Physics.Body, + SecondBodyAxis: sprec.BasisXVec3(), + }) + s.physicsEngine.AddConstraint(&physics.CoiloverConstraint{ + FirstBody: chasis.Physics.Body, + FirstBodyAnchor: blWheelRelativePosition, + SecondBody: blWheel.Physics.Body, + FrequencyHz: suspensionFrequencyHz, + DampingRatio: suspensionDampingRatio, + }) + + brWheelRelativePosition := sprec.NewVec3(-suspensionWidth, -0.6-suspensionLength/2.0, -1.45) + brWheel := car.Wheel(program, model, car.BackRightWheelLocation). + WithName("back-right-wheel"). + WithPosition(sprec.Vec3Sum(position, brWheelRelativePosition)). + Build(s.ecsManager) + s.physicsEngine.AddBody(brWheel.Physics.Body) + s.physicsEngine.AddConstraint(physics.MatchTranslationConstraint{ + FirstBody: chasis.Physics.Body, + FirstBodyAnchor: brWheelRelativePosition, + SecondBody: brWheel.Physics.Body, + IgnoreY: suspensionEnabled, + }) + s.physicsEngine.AddConstraint(physics.LimitTranslationConstraint{ + FirstBody: chasis.Physics.Body, + SecondBody: brWheel.Physics.Body, + MaxY: -0.5, + MinY: -1.0, + }) + s.physicsEngine.AddConstraint(physics.MatchAxisConstraint{ + FirstBody: chasis.Physics.Body, + FirstBodyAxis: sprec.BasisXVec3(), + SecondBody: brWheel.Physics.Body, + SecondBodyAxis: sprec.BasisXVec3(), + }) + s.physicsEngine.AddConstraint(&physics.CoiloverConstraint{ + FirstBody: chasis.Physics.Body, + FirstBodyAnchor: brWheelRelativePosition, + SecondBody: brWheel.Physics.Body, + FrequencyHz: suspensionFrequencyHz, + DampingRatio: suspensionDampingRatio, + }) + + car := s.ecsManager.CreateEntity() + car.Car = &ecs.Car{ + Chassis: chasis.Physics.Body, + FLWheelRotation: flRotation, + FLWheel: flWheel.Physics.Body, + FRWheelRotation: frRotation, + FRWheel: frWheel.Physics.Body, + BLWheel: blWheel.Physics.Body, + BRWheel: brWheel.Physics.Body, + } + car.HumanInput = true + + return chasis +} + func (s *Stage) Resize(width, height int) { s.screenFramebuffer.Width = int32(width) s.screenFramebuffer.Height = int32(height) } func (s *Stage) Update(elapsedTime time.Duration, camera *ecs.Camera, input ecs.CarInput) { - s.ecsVehicleSystem.Update(elapsedTime, input) + s.physicsEngine.Update(elapsedTime) + s.ecsCarSystem.Update(elapsedTime, input) s.ecsCameraStandSystem.Update() } func (s *Stage) Render(pipeline *graphics.Pipeline, camera *ecs.Camera) { + if !arrayTask.Done() { + panic("NOT DONE!") + } + + pipeline.SchedulePreRender(func() { + if err := s.debugVertexArray.Update(s.debugVertexArrayData); err != nil { + panic(err) + } + }) + geometrySequence := pipeline.BeginSequence() geometrySequence.TargetFramebuffer = s.geometryFramebuffer geometrySequence.BackgroundColor = sprec.NewVec4(0.0, 0.6, 1.0, 1.0) @@ -184,6 +443,8 @@ func (s *Stage) Render(pipeline *graphics.Pipeline, camera *ecs.Camera) { geometrySequence.DepthFunc = graphics.DepthFuncLessOrEqual geometrySequence.ProjectionMatrix = camera.ProjectionMatrix() geometrySequence.ViewMatrix = camera.InverseViewMatrix() + // s.refreshDebugLines() + s.renderDebugLines(geometrySequence) s.ecsRenderer.Render(geometrySequence) pipeline.EndSequence(geometrySequence) @@ -209,17 +470,117 @@ func (s *Stage) Render(pipeline *graphics.Pipeline, camera *ecs.Camera) { pipeline.EndSequence(screenSequence) } -func (s *Stage) CheckCollision(line collision.Line) (bestCollision collision.LineCollision, found bool) { - closestDistance := line.LengthSquared() - for _, mesh := range s.collisionMeshes { - if lineCollision, ok := mesh.LineCollision(line); ok { - found = true - distanceVector := sprec.Vec3Diff(lineCollision.Intersection(), line.Start()) - if distance := distanceVector.SqrLength(); distance < closestDistance { - closestDistance = distance - bestCollision = lineCollision - } +type DebugLine struct { + A sprec.Vec3 + B sprec.Vec3 + Color sprec.Vec4 +} + +func (s *Stage) refreshDebugLines() { + s.debugLines = s.debugLines[:0] + for _, body := range s.physicsEngine.Bodies() { + color := sprec.NewVec4(1.0, 1.0, 1.0, 1.0) + if body.InCollision { + color = sprec.NewVec4(1.0, 0.0, 0.0, 1.0) } + for _, placement := range body.CollisionShapes { + placementWS := placement.Transformed(body.Position, body.Orientation) + s.renderDebugPlacement(placementWS, color) + } + } +} + +func (s *Stage) renderDebugPlacement(placement shape.Placement, color sprec.Vec4) { + switch shape := placement.Shape.(type) { + case shape.StaticSphere: + s.renderDebugSphere(placement, shape, color) + case shape.StaticBox: + s.renderDebugBox(placement, shape, color) + case shape.StaticMesh: + s.renderDebugMesh(placement, shape, color) } - return +} + +func (s *Stage) renderDebugSphere(placement shape.Placement, sphere shape.StaticSphere, color sprec.Vec4) { + // FIXME: Draw sphere, not box! + box := shape.NewStaticBox(sphere.Radius()*2.0, sphere.Radius()*2.0, sphere.Radius()*2.0) + s.renderDebugBox(placement, box, color) +} + +func (s *Stage) renderDebugBox(placement shape.Placement, box shape.StaticBox, color sprec.Vec4) { + minX := sprec.Vec3Prod(placement.Orientation.OrientationX(), -box.Width()/2.0) + maxX := sprec.Vec3Prod(placement.Orientation.OrientationX(), box.Width()/2.0) + minY := sprec.Vec3Prod(placement.Orientation.OrientationY(), -box.Height()/2.0) + maxY := sprec.Vec3Prod(placement.Orientation.OrientationY(), box.Height()/2.0) + minZ := sprec.Vec3Prod(placement.Orientation.OrientationZ(), -box.Length()/2.0) + maxZ := sprec.Vec3Prod(placement.Orientation.OrientationZ(), box.Length()/2.0) + + p1 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(placement.Position, minX), minZ), maxY) + p2 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(placement.Position, minX), maxZ), maxY) + p3 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(placement.Position, maxX), maxZ), maxY) + p4 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(placement.Position, maxX), minZ), maxY) + p5 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(placement.Position, minX), minZ), minY) + p6 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(placement.Position, minX), maxZ), minY) + p7 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(placement.Position, maxX), maxZ), minY) + p8 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(placement.Position, maxX), minZ), minY) + + s.addDebugLine(p1, p2, color) + s.addDebugLine(p2, p3, color) + s.addDebugLine(p3, p4, color) + s.addDebugLine(p4, p1, color) + + s.addDebugLine(p5, p6, color) + s.addDebugLine(p6, p7, color) + s.addDebugLine(p7, p8, color) + s.addDebugLine(p8, p5, color) + + s.addDebugLine(p1, p5, color) + s.addDebugLine(p2, p6, color) + s.addDebugLine(p3, p7, color) + s.addDebugLine(p4, p8, color) +} + +func (s *Stage) renderDebugMesh(placement shape.Placement, mesh shape.StaticMesh, color sprec.Vec4) { + for _, triangle := range mesh.Triangles() { + triangleWS := triangle.Transformed(placement.Position, placement.Orientation) + s.addDebugLine(triangleWS.A(), triangleWS.B(), color) + s.addDebugLine(triangleWS.B(), triangleWS.C(), color) + s.addDebugLine(triangleWS.C(), triangleWS.A(), color) + } +} + +func (s *Stage) addDebugLine(a, b sprec.Vec3, color sprec.Vec4) { + s.debugLines = append(s.debugLines, DebugLine{ + A: a, + B: b, + Color: color, + }) +} + +func (s *Stage) renderDebugLines(sequence *graphics.Sequence) { + for i, line := range s.debugLines { + vertexStride := 4 * 7 * 2 + data.Buffer(s.debugVertexArrayData.VertexData).SetFloat32(vertexStride*i+0, line.A.X) + data.Buffer(s.debugVertexArrayData.VertexData).SetFloat32(vertexStride*i+4, line.A.Y) + data.Buffer(s.debugVertexArrayData.VertexData).SetFloat32(vertexStride*i+8, line.A.Z) + data.Buffer(s.debugVertexArrayData.VertexData).SetFloat32(vertexStride*i+12, line.Color.X) + data.Buffer(s.debugVertexArrayData.VertexData).SetFloat32(vertexStride*i+16, line.Color.Y) + data.Buffer(s.debugVertexArrayData.VertexData).SetFloat32(vertexStride*i+20, line.Color.Z) + data.Buffer(s.debugVertexArrayData.VertexData).SetFloat32(vertexStride*i+24, line.Color.W) + data.Buffer(s.debugVertexArrayData.VertexData).SetFloat32(vertexStride*i+28, line.B.X) + data.Buffer(s.debugVertexArrayData.VertexData).SetFloat32(vertexStride*i+32, line.B.Y) + data.Buffer(s.debugVertexArrayData.VertexData).SetFloat32(vertexStride*i+36, line.B.Z) + data.Buffer(s.debugVertexArrayData.VertexData).SetFloat32(vertexStride*i+40, line.Color.X) + data.Buffer(s.debugVertexArrayData.VertexData).SetFloat32(vertexStride*i+44, line.Color.Y) + data.Buffer(s.debugVertexArrayData.VertexData).SetFloat32(vertexStride*i+48, line.Color.Z) + data.Buffer(s.debugVertexArrayData.VertexData).SetFloat32(vertexStride*i+52, line.Color.W) + } + + item := sequence.BeginItem() + item.Primitive = graphics.RenderPrimitiveLines + item.Program = s.debugProgram + item.ModelMatrix = sprec.IdentityMat4() + item.VertexArray = s.debugVertexArray + item.IndexCount = int32(len(s.debugLines) * 2) + sequence.EndItem(item) } diff --git a/cmd/rallymka/internal/stream/level.go b/cmd/rallymka/internal/stream/level.go index 541861d..226a8a7 100644 --- a/cmd/rallymka/internal/stream/level.go +++ b/cmd/rallymka/internal/stream/level.go @@ -5,9 +5,9 @@ import ( "github.com/mokiat/gomath/sprec" "github.com/mokiat/rally-mka/internal/data/asset" - "github.com/mokiat/rally-mka/internal/engine/collision" "github.com/mokiat/rally-mka/internal/engine/graphics" "github.com/mokiat/rally-mka/internal/engine/resource" + "github.com/mokiat/rally-mka/internal/engine/shape" ) const levelResourceType = "level" @@ -33,8 +33,8 @@ func (h LevelHandle) IsAvailable() bool { type Level struct { Waypoints []sprec.Vec3 SkyboxTexture CubeTextureHandle - CollisionMeshes []*collision.Mesh - StartCollisionMesh *collision.Mesh + CollisionMeshes []shape.Placement + StartCollisionMesh shape.Placement StaticMeshes []*Mesh StaticEntities []*Entity } @@ -105,19 +105,43 @@ func (o *LevelOperator) Allocate(registry *resource.Registry, name string) (reso registry.Request(skyboxTexture.Handle) level.SkyboxTexture = skyboxTexture - convertCollisionMesh := func(collisionMeshAsset asset.LevelCollisionMesh) *collision.Mesh { - var triangles []collision.Triangle + trianglesCenter := func(triangles []shape.StaticTriangle) sprec.Vec3 { + var center sprec.Vec3 + count := 0 + for _, triangle := range triangles { + center = sprec.Vec3Sum(center, triangle.A()) + center = sprec.Vec3Sum(center, triangle.B()) + center = sprec.Vec3Sum(center, triangle.C()) + count += 3 + } + return sprec.Vec3Quot(center, float32(count)) + } + + convertCollisionMesh := func(collisionMeshAsset asset.LevelCollisionMesh) shape.Placement { + var triangles []shape.StaticTriangle for _, triangleAsset := range collisionMeshAsset.Triangles { - triangles = append(triangles, collision.MakeTriangle( + triangles = append(triangles, shape.NewStaticTriangle( sprec.NewVec3(triangleAsset[0][0], triangleAsset[0][1], triangleAsset[0][2]), sprec.NewVec3(triangleAsset[1][0], triangleAsset[1][1], triangleAsset[1][2]), sprec.NewVec3(triangleAsset[2][0], triangleAsset[2][1], triangleAsset[2][2]), )) } - return collision.NewMesh(triangles) + center := trianglesCenter(triangles) + for i := range triangles { + triangles[i] = shape.NewStaticTriangle( + sprec.Vec3Diff(triangles[i].A(), center), + sprec.Vec3Diff(triangles[i].B(), center), + sprec.Vec3Diff(triangles[i].C(), center), + ) + } + return shape.Placement{ + Position: center, + Orientation: sprec.IdentityQuat(), + Shape: shape.NewStaticMesh(triangles), + } } - collisionMeshes := make([]*collision.Mesh, len(levelAsset.CollisionMeshes)) + collisionMeshes := make([]shape.Placement, len(levelAsset.CollisionMeshes)) for i, collisionMeshAsset := range levelAsset.CollisionMeshes { collisionMeshes[i] = convertCollisionMesh(collisionMeshAsset) } diff --git a/go.mod b/go.mod index ac89fe3..6dc46d2 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 - github.com/mokiat/gomath v0.0.0-20200331200621-213f6758ab70 + github.com/mokiat/gomath v0.0.0-20200423212154-8a093a9f6eff github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/urfave/cli/v2 v2.2.0 ) diff --git a/go.sum b/go.sum index cf6028a..96c7a5e 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/mokiat/gomath v0.0.0-20200331200621-213f6758ab70 h1:cdB3Bwwu5q8kKMZxQAw8bw+Fd9DQIZ9s98+ODWJyVtQ= -github.com/mokiat/gomath v0.0.0-20200331200621-213f6758ab70/go.mod h1:t5I5hXzSNOgdr3TRQTTxmvp0KbwNgk8atDcxA2yjxDE= +github.com/mokiat/gomath v0.0.0-20200423212154-8a093a9f6eff h1:fcg8e2m6iu9sX79QPFF543uYoz84Mmwm20HttG3SglY= +github.com/mokiat/gomath v0.0.0-20200423212154-8a093a9f6eff/go.mod h1:t5I5hXzSNOgdr3TRQTTxmvp0KbwNgk8atDcxA2yjxDE= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/internal/engine/collision/line.go b/internal/engine/collision/line.go deleted file mode 100644 index f145ff5..0000000 --- a/internal/engine/collision/line.go +++ /dev/null @@ -1,58 +0,0 @@ -package collision - -import ( - "github.com/mokiat/gomath/sprec" -) - -func MakeLine(start, end sprec.Vec3) Line { - return Line{ - start: start, - end: end, - length: sprec.Vec3Diff(end, start).Length(), - } -} - -type Line struct { - start sprec.Vec3 - end sprec.Vec3 - length float32 -} - -func (l Line) Start() sprec.Vec3 { - return l.start -} - -func (l Line) End() sprec.Vec3 { - return l.end -} - -func (l Line) Length() float32 { - return l.length -} - -func (l Line) LengthSquared() float32 { - return l.length * l.length -} - -type LineCollision struct { - intersection sprec.Vec3 - normal sprec.Vec3 - topHeight float32 - bottomHeight float32 -} - -func (c LineCollision) Intersection() sprec.Vec3 { - return c.intersection -} - -func (c LineCollision) Normal() sprec.Vec3 { - return c.normal -} - -func (c LineCollision) TopHeight() float32 { - return c.topHeight -} - -func (c LineCollision) BottomHeight() float32 { - return c.bottomHeight -} diff --git a/internal/engine/collision/mesh.go b/internal/engine/collision/mesh.go deleted file mode 100644 index e6a0104..0000000 --- a/internal/engine/collision/mesh.go +++ /dev/null @@ -1,74 +0,0 @@ -package collision - -import ( - "github.com/mokiat/gomath/sprec" -) - -func NewMesh(triangles []Triangle) *Mesh { - mesh := &Mesh{ - triangles: triangles, - } - mesh.evaluateCenter() - mesh.evaluateRadius() - return mesh -} - -type Mesh struct { - center sprec.Vec3 - radius float32 - triangles []Triangle -} - -func (m *Mesh) Triangles() []Triangle { - return m.triangles -} - -func (m *Mesh) LineCollision(line Line) (bestCollision LineCollision, found bool) { - if startDistance := sprec.Vec3Diff(m.center, line.start).Length(); startDistance > line.Length()+m.radius { - return - } - if endDistance := sprec.Vec3Diff(m.center, line.end).Length(); endDistance > line.Length()+m.radius { - return - } - - closestDistance := line.LengthSquared() - for _, triangle := range m.triangles { - if lineCollision, ok := triangle.LineCollision(line); ok { - found = true - distanceVector := sprec.Vec3Diff(lineCollision.intersection, line.start) - distance := distanceVector.SqrLength() - if distance < closestDistance { - closestDistance = distance - bestCollision = lineCollision - } - } - } - return -} - -func (m *Mesh) evaluateCenter() { - m.center = sprec.ZeroVec3() - count := 0 - for _, triangle := range m.triangles { - m.center = sprec.Vec3Sum(m.center, triangle.a) - m.center = sprec.Vec3Sum(m.center, triangle.b) - m.center = sprec.Vec3Sum(m.center, triangle.c) - count += 3 - } - m.center = sprec.Vec3Quot(m.center, float32(count)) -} - -func (m *Mesh) evaluateRadius() { - m.radius = 0.0 - for _, triangle := range m.triangles { - if radius := sprec.Vec3Diff(m.center, triangle.a).Length(); radius > m.radius { - m.radius = radius - } - if radius := sprec.Vec3Diff(m.center, triangle.b).Length(); radius > m.radius { - m.radius = radius - } - if radius := sprec.Vec3Diff(m.center, triangle.c).Length(); radius > m.radius { - m.radius = radius - } - } -} diff --git a/internal/engine/collision/triangle.go b/internal/engine/collision/triangle.go deleted file mode 100644 index 40a3e8a..0000000 --- a/internal/engine/collision/triangle.go +++ /dev/null @@ -1,69 +0,0 @@ -package collision - -import ( - "github.com/mokiat/gomath/sprec" -) - -func MakeTriangle(a, b, c sprec.Vec3) Triangle { - return Triangle{ - a: a, - b: b, - c: c, - normal: getNormal(a, b, c), - } -} - -type Triangle struct { - a sprec.Vec3 - b sprec.Vec3 - c sprec.Vec3 - normal sprec.Vec3 -} - -func (t Triangle) LineCollision(line Line) (LineCollision, bool) { - surfaceToStart := sprec.Vec3Diff(line.start, t.a) - surfaceToEnd := sprec.Vec3Diff(line.end, t.a) - - topHeight := sprec.Vec3Dot(t.normal, surfaceToStart) - bottomHeight := sprec.Vec3Dot(t.normal, surfaceToEnd) - if topHeight < 0 || bottomHeight > 0 { - return LineCollision{}, false - } - - height := topHeight - bottomHeight - if sprec.Abs(height) < 0.00001 { - return LineCollision{}, false - } - - factor := topHeight / height - delta := sprec.Vec3Diff(line.end, line.start) - intersection := sprec.Vec3Sum(sprec.Vec3Prod(delta, factor), line.start) - - if !isInTriangle(intersection, t.a, t.b, t.c, t.normal) { - return LineCollision{}, false - } - return LineCollision{ - intersection: intersection, - normal: t.normal, - topHeight: topHeight, - bottomHeight: bottomHeight, - }, true -} - -func isInTriangle(vertex, a, b, c, normal sprec.Vec3) bool { - return isCounterClockwise(a, b, vertex, normal) && - isCounterClockwise(b, c, vertex, normal) && - isCounterClockwise(c, a, vertex, normal) -} - -func isCounterClockwise(a, b, c, normal sprec.Vec3) bool { - evaluatedNormal := getNormal(a, b, c) - return sprec.Vec3Dot(normal, evaluatedNormal) > 0.0 -} - -func getNormal(a, b, c sprec.Vec3) sprec.Vec3 { - vector1 := sprec.Vec3Diff(a, c) - vector2 := sprec.Vec3Diff(b, c) - direction := sprec.Vec3Cross(vector1, vector2) - return sprec.ResizedVec3(direction, 1.0) -} diff --git a/internal/engine/graphics/twod_texture.go b/internal/engine/graphics/twod_texture.go index 85a0e19..b342faa 100644 --- a/internal/engine/graphics/twod_texture.go +++ b/internal/engine/graphics/twod_texture.go @@ -22,8 +22,8 @@ func (t *TwoDTexture) Allocate(data TwoDTextureData) error { return fmt.Errorf("failed to allocate texture") } gl.BindTexture(gl.TEXTURE_2D, t.ID) - gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) - gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT) gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, data.Width, data.Height, 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(data.Data)) diff --git a/internal/engine/graphics/vertex_array.go b/internal/engine/graphics/vertex_array.go index be14652..e071f7a 100644 --- a/internal/engine/graphics/vertex_array.go +++ b/internal/engine/graphics/vertex_array.go @@ -18,6 +18,7 @@ type VertexArrayData struct { CoordOffset int NormalOffset int TexCoordOffset int + ColorOffset int IndexData []byte } @@ -33,7 +34,7 @@ func (a *VertexArray) Allocate(data VertexArrayData) error { return fmt.Errorf("failed to allocate vertex buffer") } gl.BindBuffer(gl.ARRAY_BUFFER, a.VertexBufferID) - gl.BufferData(gl.ARRAY_BUFFER, len(data.VertexData), gl.Ptr(data.VertexData), gl.STATIC_DRAW) + gl.BufferData(gl.ARRAY_BUFFER, len(data.VertexData), gl.Ptr(data.VertexData), gl.DYNAMIC_DRAW) gl.EnableVertexAttribArray(0) gl.VertexAttribPointer(0, 3, gl.FLOAT, false, data.VertexStride, gl.PtrOffset(data.CoordOffset)) @@ -45,6 +46,10 @@ func (a *VertexArray) Allocate(data VertexArrayData) error { gl.EnableVertexAttribArray(2) gl.VertexAttribPointer(2, 2, gl.FLOAT, false, data.VertexStride, gl.PtrOffset(data.TexCoordOffset)) } + if data.ColorOffset != 0 { + gl.EnableVertexAttribArray(3) + gl.VertexAttribPointer(3, 4, gl.FLOAT, false, data.VertexStride, gl.PtrOffset(data.ColorOffset)) + } gl.GenBuffers(1, &a.IndexBufferID) if a.IndexBufferID == 0 { @@ -55,6 +60,12 @@ func (a *VertexArray) Allocate(data VertexArrayData) error { return nil } +func (a *VertexArray) Update(data VertexArrayData) error { + gl.BindBuffer(gl.ARRAY_BUFFER, a.VertexBufferID) + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(data.VertexData), gl.Ptr(data.VertexData)) + return nil +} + func (a *VertexArray) Release() error { gl.DeleteBuffers(1, &a.IndexBufferID) gl.DeleteBuffers(1, &a.VertexBufferID) diff --git a/internal/engine/physics/body.go b/internal/engine/physics/body.go new file mode 100644 index 0000000..7f805b4 --- /dev/null +++ b/internal/engine/physics/body.go @@ -0,0 +1,109 @@ +package physics + +import ( + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/rally-mka/internal/engine/shape" +) + +type Body struct { + Name string + + Position sprec.Vec3 + Orientation sprec.Quat + IsStatic bool + + Mass float32 + MomentOfInertia sprec.Mat3 + + Acceleration sprec.Vec3 + AngularAcceleration sprec.Vec3 + + Velocity sprec.Vec3 + AngularVelocity sprec.Vec3 + + DragFactor float32 + AngularDragFactor float32 + + RestitutionCoef float32 + CollisionShapes []shape.Placement + InCollision bool +} + +func (b *Body) Translate(offset sprec.Vec3) { + b.Position = sprec.Vec3Sum(b.Position, offset) +} + +func (b *Body) Rotate(vector sprec.Vec3) { + if radians := vector.Length(); sprec.Abs(radians) > 0.00001 { + b.Orientation = sprec.QuatProd(sprec.RotationQuat(sprec.Radians(radians), vector), b.Orientation) + } +} + +func (b *Body) ApplyNudge(nudge sprec.Vec3) { + b.Translate(sprec.Vec3Quot(nudge, b.Mass)) +} + +func (b *Body) ApplyAngularNudge(nudge sprec.Vec3) { + // FIXME: the moment of intertia is in local space, whereas the torque is in world space + b.Rotate(sprec.Mat3Vec3Prod(sprec.InverseMat3(b.MomentOfInertia), nudge)) +} + +func (b *Body) ResetAcceleration() { + b.Acceleration = sprec.ZeroVec3() +} + +func (b *Body) ResetAngularAcceleration() { + b.AngularAcceleration = sprec.ZeroVec3() +} + +func (b *Body) AddAcceleration(amount sprec.Vec3) { + b.Acceleration = sprec.Vec3Sum(b.Acceleration, amount) +} + +func (b *Body) AddAngularAcceleration(amount sprec.Vec3) { + b.AngularAcceleration = sprec.Vec3Sum(b.AngularAcceleration, amount) +} + +func (b *Body) AddVelocity(amount sprec.Vec3) { + b.Velocity = sprec.Vec3Sum(b.Velocity, amount) +} + +func (b *Body) AddAngularVelocity(amount sprec.Vec3) { + b.AngularVelocity = sprec.Vec3Sum(b.AngularVelocity, amount) +} + +func (b *Body) ApplyForce(force sprec.Vec3) { + b.AddAcceleration(sprec.Vec3Quot(force, b.Mass)) +} + +func (b *Body) ApplyTorque(torque sprec.Vec3) { + // FIXME: the moment of intertia is in local space, whereas the torque is in world space + b.AddAngularAcceleration(sprec.Mat3Vec3Prod(sprec.InverseMat3(b.MomentOfInertia), torque)) +} + +func (b *Body) ApplyOffsetForce(offset, force sprec.Vec3) { + b.ApplyForce(force) + b.ApplyTorque(sprec.Vec3Cross(offset, force)) +} + +func (b *Body) ApplyImpulse(impulse sprec.Vec3) { + b.Velocity = sprec.Vec3Sum(b.Velocity, sprec.Vec3Quot(impulse, b.Mass)) +} + +func (b *Body) ApplyAngularImpulse(impulse sprec.Vec3) { + // FIXME: the moment of intertia is in local space, whereas the impulse is in world space + b.AddAngularVelocity(sprec.Mat3Vec3Prod(sprec.InverseMat3(b.MomentOfInertia), impulse)) +} + +func (b *Body) ApplyOffsetImpulse(offset, impulse sprec.Vec3) { + b.ApplyImpulse(impulse) + b.ApplyAngularImpulse(sprec.Vec3Cross(offset, impulse)) +} + +func SymmetricMomentOfInertia(value float32) sprec.Mat3 { + return sprec.NewMat3( + value, 0.0, 0.0, + 0.0, value, 0.0, + 0.0, 0.0, value, + ) +} diff --git a/internal/engine/physics/chandelier.go b/internal/engine/physics/chandelier.go new file mode 100644 index 0000000..d9bc8da --- /dev/null +++ b/internal/engine/physics/chandelier.go @@ -0,0 +1,56 @@ +package physics + +import "github.com/mokiat/gomath/sprec" + +type ChandelierConstraint struct { + NilConstraint + Fixture sprec.Vec3 + Body *Body + BodyAnchor sprec.Vec3 + Length float32 +} + +func (c ChandelierConstraint) ApplyImpulse(ctx Context) { + result := c.Calculate() + if sprec.Abs(result.Drift) > 0.0001 { + result.Jacobian.CorrectVelocity(c.Body) + } +} + +func (c ChandelierConstraint) ApplyNudge(ctx Context) { + result := c.Calculate() + if sprec.Abs(result.Drift) > 0.0001 { + result.Jacobian.CorrectPosition(c.Body, result.Drift) + } +} + +func (c ChandelierConstraint) Calculate() ChandelierConstraintResult { + anchorWS := sprec.Vec3Sum(c.Body.Position, sprec.QuatVec3Rotation(c.Body.Orientation, c.BodyAnchor)) + radiusWS := sprec.Vec3Diff(anchorWS, c.Body.Position) + deltaPosition := sprec.Vec3Diff(anchorWS, c.Fixture) + normal := sprec.BasisXVec3() + if deltaPosition.SqrLength() > 0.000001 { + normal = sprec.UnitVec3(deltaPosition) + } + + return ChandelierConstraintResult{ + Jacobian: Jacobian{ + SlopeVelocity: sprec.NewVec3( + normal.X, + normal.Y, + normal.Z, + ), + SlopeAngularVelocity: sprec.NewVec3( + normal.Z*radiusWS.Y-normal.Y*radiusWS.Z, + normal.X*radiusWS.Z-normal.Z*radiusWS.X, + normal.Y*radiusWS.X-normal.X*radiusWS.Y, + ), + }, + Drift: deltaPosition.Length() - c.Length, + } +} + +type ChandelierConstraintResult struct { + Jacobian Jacobian + Drift float32 +} diff --git a/internal/engine/physics/coilover.go b/internal/engine/physics/coilover.go new file mode 100644 index 0000000..a49e3d0 --- /dev/null +++ b/internal/engine/physics/coilover.go @@ -0,0 +1,72 @@ +package physics + +import ( + "github.com/mokiat/gomath/sprec" +) + +type CoiloverConstraint struct { + NilConstraint + + FirstBody *Body + FirstBodyAnchor sprec.Vec3 + SecondBody *Body + FrequencyHz float32 + DampingRatio float32 + + appliedLambda float32 +} + +func (c *CoiloverConstraint) Reset() { + c.appliedLambda = 0.0 +} + +func (c *CoiloverConstraint) ApplyImpulse(ctx Context) { + firstRadiusWS := sprec.QuatVec3Rotation(c.FirstBody.Orientation, c.FirstBodyAnchor) + firstAnchorWS := sprec.Vec3Sum(c.FirstBody.Position, firstRadiusWS) + secondAnchorWS := c.SecondBody.Position + deltaPosition := sprec.Vec3Diff(secondAnchorWS, firstAnchorWS) + if deltaPosition.Length() < 0.0001 { + return + } + drift := deltaPosition.Length() + normal := sprec.BasisXVec3() + if drift > 0.0001 { + normal = sprec.UnitVec3(deltaPosition) + } + + jacobian := PairJacobian{ + First: Jacobian{ + SlopeVelocity: sprec.NewVec3( + -normal.X, + -normal.Y, + -normal.Z, + ), + SlopeAngularVelocity: sprec.NewVec3( + -(normal.Z*firstRadiusWS.Y - normal.Y*firstRadiusWS.Z), + -(normal.X*firstRadiusWS.Z - normal.Z*firstRadiusWS.X), + -(normal.Y*firstRadiusWS.X - normal.X*firstRadiusWS.Y), + ), + }, + Second: Jacobian{ + SlopeVelocity: sprec.NewVec3( + normal.X, + normal.Y, + normal.Z, + ), + SlopeAngularVelocity: sprec.ZeroVec3(), + }, + } + + invertedEffectiveMass := jacobian.InverseEffectiveMass(c.FirstBody, c.SecondBody) + w := 2.0 * sprec.Pi * c.FrequencyHz + dc := 2.0 * c.DampingRatio * w / invertedEffectiveMass + k := w * w / invertedEffectiveMass + + gamma := 1.0 / (ctx.ElapsedSeconds * (dc + ctx.ElapsedSeconds*k)) + beta := ctx.ElapsedSeconds * k * gamma + + velocityLambda := jacobian.EffectiveVelocity(c.FirstBody, c.SecondBody) + lambda := -(velocityLambda + beta*drift + gamma*c.appliedLambda) / (invertedEffectiveMass + gamma) + c.appliedLambda += lambda + jacobian.ApplyImpulse(c.FirstBody, c.SecondBody, lambda) +} diff --git a/internal/engine/physics/collision.go b/internal/engine/physics/collision.go new file mode 100644 index 0000000..8422559 --- /dev/null +++ b/internal/engine/physics/collision.go @@ -0,0 +1,54 @@ +package physics + +import ( + "github.com/mokiat/gomath/sprec" +) + +type GroundCollisionConstraint struct { + NilConstraint + Body *Body + Normal sprec.Vec3 + ContactPoint sprec.Vec3 + Depth float32 +} + +func (c GroundCollisionConstraint) ApplyImpulse(ctx Context) { + contactRadiusWS := sprec.Vec3Diff(c.ContactPoint, c.Body.Position) + contactVelocity := sprec.Vec3Sum(c.Body.Velocity, sprec.Vec3Cross(c.Body.AngularVelocity, contactRadiusWS)) + verticalVelocity := sprec.Vec3Dot(c.Normal, contactVelocity) + + normal := sprec.InverseVec3(c.Normal) + normalVelocity := sprec.Vec3Dot(c.Normal, contactVelocity) + if normalVelocity > 0 { + return // moving away from ground + } + + // restitutionClamp := float32(1.0) + // if sprec.Abs(normalVelocity) < 2 { + // restitutionClamp = 0.1 + // } + // if sprec.Abs(normalVelocity) < 1 { + // restitutionClamp = 0.05 + // } + // if sprec.Abs(normalVelocity) < 0.5 { + // restitutionClamp = 0.0 + // } + + restitutionClamp := float32(0.0) // TODO: Delete, use above one + + totalMass := (1 + c.Body.RestitutionCoef*restitutionClamp) / ((1.0 / c.Body.Mass) + sprec.Vec3Dot(sprec.Mat3Vec3Prod(sprec.InverseMat3(c.Body.MomentOfInertia), sprec.Vec3Cross(contactRadiusWS, normal)), sprec.Vec3Cross(contactRadiusWS, normal))) + pureImpulseStrength := totalMass * sprec.Vec3Dot(normal, contactVelocity) + impulseStrength := pureImpulseStrength + totalMass*c.Depth // FIXME + c.Body.ApplyOffsetImpulse(contactRadiusWS, sprec.InverseVec3(sprec.Vec3Prod(normal, impulseStrength))) + + frictionCoef := float32(0.8) + lateralVelocity := sprec.Vec3Diff(contactVelocity, sprec.Vec3Prod(c.Normal, verticalVelocity)) + if lateralVelocity.SqrLength() > 0.000001 { + lateralImpulseStrength := totalMass * lateralVelocity.Length() + if lateralImpulseStrength > sprec.Abs(impulseStrength)*frictionCoef { + lateralImpulseStrength = sprec.Abs(impulseStrength) * frictionCoef + } + lateralDir := sprec.UnitVec3(lateralVelocity) + c.Body.ApplyOffsetImpulse(contactRadiusWS, sprec.Vec3Prod(lateralDir, -lateralImpulseStrength)) + } +} diff --git a/internal/engine/physics/constraint.go b/internal/engine/physics/constraint.go new file mode 100644 index 0000000..4f0863e --- /dev/null +++ b/internal/engine/physics/constraint.go @@ -0,0 +1,23 @@ +package physics + +type Context struct { + ElapsedSeconds float32 + ImpulseIterations int + NudgeIterations int +} + +type Constraint interface { + Reset() + ApplyImpulse(ctx Context) + ApplyNudge(ctx Context) +} + +var _ Constraint = NilConstraint{} + +type NilConstraint struct{} + +func (NilConstraint) Reset() {} + +func (NilConstraint) ApplyImpulse(Context) {} + +func (NilConstraint) ApplyNudge(Context) {} diff --git a/internal/engine/physics/engine.go b/internal/engine/physics/engine.go new file mode 100644 index 0000000..3e42fc7 --- /dev/null +++ b/internal/engine/physics/engine.go @@ -0,0 +1,224 @@ +package physics + +import ( + "time" + + "github.com/mokiat/gomath/sprec" + "github.com/mokiat/rally-mka/internal/engine/shape" +) + +const ( + gravity = 9.8 + windDensity = 1.2 + + impulseIterations = 100 + nudgeIterations = 100 +) + +func NewEngine(step time.Duration) *Engine { + return &Engine{ + step: step, + accumulatedTime: 0, + + gravity: sprec.NewVec3(0.0, -gravity, 0.0), + windVelocity: sprec.NewVec3(0.0, 0.0, 0.0), + windDensity: windDensity, + + intersectionSet: shape.NewIntersectionResultSet(128), + } +} + +type Engine struct { + step time.Duration + accumulatedTime time.Duration + + gravity sprec.Vec3 + windVelocity sprec.Vec3 + windDensity float32 + + bodies []*Body + constraints []Constraint + collisionConstraints []Constraint + + intersectionSet *shape.IntersectionResultSet +} + +func (e *Engine) Bodies() []*Body { + return e.bodies +} + +func (e *Engine) Update(elapsedTime time.Duration) { + e.accumulatedTime += elapsedTime + for e.accumulatedTime > e.step { + e.accumulatedTime -= e.step + e.runSimulation(Context{ + ElapsedSeconds: float32(e.step.Seconds()), + ImpulseIterations: impulseIterations, + NudgeIterations: nudgeIterations, + }) + } +} + +func (e *Engine) Add(aspect interface{}) { + if body, ok := aspect.(*Body); ok { + e.AddBody(body) + } + if constraint, ok := aspect.(Constraint); ok { + e.AddConstraint(constraint) + } +} + +func (e *Engine) AddBody(body *Body) { + e.bodies = append(e.bodies, body) +} + +func (e *Engine) AddConstraint(constraint Constraint) { + e.constraints = append(e.constraints, constraint) +} + +func (e *Engine) runSimulation(ctx Context) { + e.detectCollisions() + e.resetConstraints() + e.applyForces() + e.integrate(ctx) + e.applyImpulses(ctx) + e.applyMotion(ctx) + e.applyNudges(ctx) +} + +func (e *Engine) resetConstraints() { + for _, constraint := range e.constraints { + constraint.Reset() + } + for _, constraint := range e.collisionConstraints { + constraint.Reset() + } +} + +func (e *Engine) applyForces() { + for _, body := range e.bodies { + if body.IsStatic { + continue + } + body.ResetAcceleration() + body.ResetAngularAcceleration() + + body.AddAcceleration(e.gravity) + deltaWindVelocity := sprec.Vec3Diff(e.windVelocity, body.Velocity) + body.ApplyForce(sprec.Vec3Prod(deltaWindVelocity, e.windDensity*body.DragFactor*deltaWindVelocity.Length())) + body.ApplyTorque(sprec.Vec3Prod(body.AngularVelocity, -e.windDensity*body.AngularDragFactor*body.AngularVelocity.Length())) + } + + // TODO: Restrict max linear + angular accelerations +} + +func (e *Engine) integrate(ctx Context) { + for _, body := range e.bodies { + if body.IsStatic { + continue + } + deltaVelocity := sprec.Vec3Prod(body.Acceleration, ctx.ElapsedSeconds) + body.AddVelocity(deltaVelocity) + deltaAngularVelocity := sprec.Vec3Prod(body.AngularAcceleration, ctx.ElapsedSeconds) + body.AddAngularVelocity(deltaAngularVelocity) + + // TODO: Restrict max linear + angular velocities + } +} + +func (e *Engine) applyImpulses(ctx Context) { + for i := 0; i < ctx.ImpulseIterations; i++ { + for _, constraint := range e.constraints { + constraint.ApplyImpulse(ctx) + } + for _, constraint := range e.collisionConstraints { + constraint.ApplyImpulse(ctx) + } + } +} + +func (e *Engine) applyMotion(ctx Context) { + for _, body := range e.bodies { + deltaPosition := sprec.Vec3Prod(body.Velocity, ctx.ElapsedSeconds) + body.Translate(deltaPosition) + deltaRotation := sprec.Vec3Prod(body.AngularVelocity, ctx.ElapsedSeconds) + body.Rotate(deltaRotation) + } +} + +func (e *Engine) applyNudges(ctx Context) { + for i := 0; i < ctx.NudgeIterations; i++ { + for _, constraint := range e.constraints { + constraint.ApplyNudge(ctx) + } + for _, constraint := range e.collisionConstraints { + constraint.ApplyNudge(ctx) + } + } +} + +func (e *Engine) detectCollisions() { + for _, body := range e.bodies { + body.InCollision = false + } + + e.collisionConstraints = e.collisionConstraints[:0] + for i := 0; i < len(e.bodies); i++ { + for j := i + 1; j < len(e.bodies); j++ { + first := e.bodies[i] + second := e.bodies[j] + e.checkCollisionTwoBodies(first, second) + } + } +} + +func (e *Engine) checkCollisionTwoBodies(first, second *Body) { + if first.IsStatic && second.IsStatic { + return + } + + // FIXME: Temporary, to prevent non-static entities from colliding for now + // Currently, only static to non-static is supported + if !first.IsStatic && !second.IsStatic { + return + } + + for _, firstPlacement := range first.CollisionShapes { + firstPlacementWS := firstPlacement.Transformed(first.Position, first.Orientation) + + for _, secondPlacement := range second.CollisionShapes { + secondPlacementWS := secondPlacement.Transformed(second.Position, second.Orientation) + + e.intersectionSet.Reset() + shape.CheckIntersection(firstPlacementWS, secondPlacementWS, e.intersectionSet) + + if e.intersectionSet.Found() { + first.InCollision = true + second.InCollision = true + } + + for _, intersection := range e.intersectionSet.Intersections() { + // TODO: Once both non-static are supported, a dual-body collision constraint + // should be used instead of individual uni-body constraints + + if !first.IsStatic { + e.collisionConstraints = append(e.collisionConstraints, GroundCollisionConstraint{ + Body: first, + Normal: intersection.FirstDisplaceNormal, + ContactPoint: intersection.FirstContact, + Depth: intersection.Depth, + }) + } + + if !second.IsStatic { + e.collisionConstraints = append(e.collisionConstraints, GroundCollisionConstraint{ + Body: second, + Normal: intersection.SecondDisplaceNormal, + ContactPoint: intersection.SecondContact, + Depth: intersection.Depth, + }) + } + } + } + } +} diff --git a/internal/engine/physics/fixed_translation.go b/internal/engine/physics/fixed_translation.go new file mode 100644 index 0000000..ca5ffb2 --- /dev/null +++ b/internal/engine/physics/fixed_translation.go @@ -0,0 +1,48 @@ +package physics + +import "github.com/mokiat/gomath/sprec" + +type FixedTranslationConstraint struct { + NilConstraint + Fixture sprec.Vec3 + Body *Body +} + +func (c FixedTranslationConstraint) ApplyImpulse(ctx Context) { + result := c.Calculate() + if sprec.Abs(result.Drift) > 0.0001 { + result.Jacobian.CorrectVelocity(c.Body) + } +} + +func (c FixedTranslationConstraint) ApplyNudge(ctx Context) { + result := c.Calculate() + if sprec.Abs(result.Drift) > 0.0001 { + result.Jacobian.CorrectPosition(c.Body, result.Drift) + } +} + +func (c FixedTranslationConstraint) Calculate() FixedTranslationConstraintResult { + deltaPosition := sprec.Vec3Diff(c.Body.Position, c.Fixture) + normal := sprec.BasisXVec3() + if deltaPosition.SqrLength() > 0.000001 { + normal = sprec.UnitVec3(deltaPosition) + } + + return FixedTranslationConstraintResult{ + Jacobian: Jacobian{ + SlopeVelocity: sprec.NewVec3( + normal.X, + normal.Y, + normal.Z, + ), + SlopeAngularVelocity: sprec.ZeroVec3(), + }, + Drift: deltaPosition.Length(), + } +} + +type FixedTranslationConstraintResult struct { + Jacobian Jacobian + Drift float32 +} diff --git a/internal/engine/physics/hinged_rod.go b/internal/engine/physics/hinged_rod.go new file mode 100644 index 0000000..12a021e --- /dev/null +++ b/internal/engine/physics/hinged_rod.go @@ -0,0 +1,73 @@ +package physics + +import "github.com/mokiat/gomath/sprec" + +type HingedRodConstraint struct { + NilConstraint + FirstBody *Body + FirstBodyAnchor sprec.Vec3 + SecondBody *Body + SecondBodyAnchor sprec.Vec3 + Length float32 +} + +func (c HingedRodConstraint) ApplyImpulse(ctx Context) { + result := c.Calculate() + if sprec.Abs(result.Drift) > 0.0001 { + result.Jacobian.CorrectVelocity(c.FirstBody, c.SecondBody) + } +} + +func (c HingedRodConstraint) ApplyNudge(ctx Context) { + result := c.Calculate() + if sprec.Abs(result.Drift) > 0.0001 { + result.Jacobian.CorrectPosition(c.FirstBody, c.SecondBody, result.Drift) + } +} + +func (c HingedRodConstraint) Calculate() HingedRodConstraintResult { + firstRadiusWS := sprec.QuatVec3Rotation(c.FirstBody.Orientation, c.FirstBodyAnchor) + secondRadiusWS := sprec.QuatVec3Rotation(c.SecondBody.Orientation, c.SecondBodyAnchor) + firstAnchorWS := sprec.Vec3Sum(c.FirstBody.Position, firstRadiusWS) + secondAnchorWS := sprec.Vec3Sum(c.SecondBody.Position, secondRadiusWS) + deltaPosition := sprec.Vec3Diff(secondAnchorWS, firstAnchorWS) + normal := sprec.BasisXVec3() + if deltaPosition.SqrLength() > 0.000001 { + normal = sprec.UnitVec3(deltaPosition) + } + + return HingedRodConstraintResult{ + Jacobian: PairJacobian{ + First: Jacobian{ + SlopeVelocity: sprec.NewVec3( + -normal.X, + -normal.Y, + -normal.Z, + ), + SlopeAngularVelocity: sprec.NewVec3( + -(normal.Z*firstRadiusWS.Y - normal.Y*firstRadiusWS.Z), + -(normal.X*firstRadiusWS.Z - normal.Z*firstRadiusWS.X), + -(normal.Y*firstRadiusWS.X - normal.X*firstRadiusWS.Y), + ), + }, + Second: Jacobian{ + SlopeVelocity: sprec.NewVec3( + normal.X, + normal.Y, + normal.Z, + ), + SlopeAngularVelocity: sprec.NewVec3( + normal.Z*secondRadiusWS.Y-normal.Y*secondRadiusWS.Z, + normal.X*secondRadiusWS.Z-normal.Z*secondRadiusWS.X, + normal.Y*secondRadiusWS.X-normal.X*secondRadiusWS.Y, + ), + }, + }, + Drift: deltaPosition.Length() - c.Length, + } +} + +type HingedRodConstraintResult struct { + Jacobian PairJacobian + Drift float32 +} diff --git a/internal/engine/physics/jacobian.go b/internal/engine/physics/jacobian.go new file mode 100644 index 0000000..efd33af --- /dev/null +++ b/internal/engine/physics/jacobian.go @@ -0,0 +1,77 @@ +package physics + +import "github.com/mokiat/gomath/sprec" + +const driftCorrectionAmount = float32(0.01) // TODO: Configurable? + +type Jacobian struct { + SlopeVelocity sprec.Vec3 + SlopeAngularVelocity sprec.Vec3 +} + +func (j Jacobian) EffectiveVelocity(body *Body) float32 { + return sprec.Vec3Dot(j.SlopeVelocity, body.Velocity) + + sprec.Vec3Dot(j.SlopeAngularVelocity, body.AngularVelocity) +} + +func (j Jacobian) InverseEffectiveMass(body *Body) float32 { + return sprec.Vec3Dot(j.SlopeVelocity, j.SlopeVelocity)/body.Mass + + sprec.Vec3Dot(sprec.Mat3Vec3Prod(sprec.InverseMat3(body.MomentOfInertia), j.SlopeAngularVelocity), j.SlopeAngularVelocity) +} + +func (j Jacobian) ApplyImpulse(body *Body, lambda float32) { + body.ApplyImpulse(sprec.Vec3Prod(j.SlopeVelocity, lambda)) + body.ApplyAngularImpulse(sprec.Vec3Prod(j.SlopeAngularVelocity, lambda)) +} + +func (j Jacobian) ApplyNudge(body *Body, lambda float32) { + body.ApplyNudge(sprec.Vec3Prod(j.SlopeVelocity, lambda)) + body.ApplyAngularNudge(sprec.Vec3Prod(j.SlopeAngularVelocity, lambda)) +} + +func (j Jacobian) CorrectVelocity(body *Body) { + lambda := -j.EffectiveVelocity(body) / j.InverseEffectiveMass(body) + j.ApplyImpulse(body, lambda) +} + +func (j Jacobian) CorrectPosition(body *Body, drift float32) { + lambda := -driftCorrectionAmount * drift / j.InverseEffectiveMass(body) + j.ApplyNudge(body, lambda) +} + +type PairJacobian struct { + First Jacobian + Second Jacobian +} + +func (j PairJacobian) EffectiveVelocity(firstBody, secondBody *Body) float32 { + return j.First.EffectiveVelocity(firstBody) + j.Second.EffectiveVelocity(secondBody) +} + +func (j PairJacobian) InverseEffectiveMass(firstBody, secondBody *Body) float32 { + return j.First.InverseEffectiveMass(firstBody) + j.Second.InverseEffectiveMass(secondBody) +} + +func (j PairJacobian) ApplyImpulse(firstBody, secondBody *Body, lambda float32) { + firstBody.ApplyImpulse(sprec.Vec3Prod(j.First.SlopeVelocity, lambda)) + firstBody.ApplyAngularImpulse(sprec.Vec3Prod(j.First.SlopeAngularVelocity, lambda)) + secondBody.ApplyImpulse(sprec.Vec3Prod(j.Second.SlopeVelocity, lambda)) + secondBody.ApplyAngularImpulse(sprec.Vec3Prod(j.Second.SlopeAngularVelocity, lambda)) +} + +func (j PairJacobian) ApplyNudge(firstBody, secondBody *Body, lambda float32) { + firstBody.ApplyNudge(sprec.Vec3Prod(j.First.SlopeVelocity, lambda)) + firstBody.ApplyAngularNudge(sprec.Vec3Prod(j.First.SlopeAngularVelocity, lambda)) + secondBody.ApplyNudge(sprec.Vec3Prod(j.Second.SlopeVelocity, lambda)) + secondBody.ApplyAngularNudge(sprec.Vec3Prod(j.Second.SlopeAngularVelocity, lambda)) +} + +func (j PairJacobian) CorrectVelocity(firstBody, secondBody *Body) { + lambda := -j.EffectiveVelocity(firstBody, secondBody) / j.InverseEffectiveMass(firstBody, secondBody) + j.ApplyImpulse(firstBody, secondBody, lambda) +} + +func (j PairJacobian) CorrectPosition(firstBody, secondBody *Body, drift float32) { + lambda := -driftCorrectionAmount * drift / j.InverseEffectiveMass(firstBody, secondBody) + j.ApplyNudge(firstBody, secondBody, lambda) +} diff --git a/internal/engine/physics/limit_translation.go b/internal/engine/physics/limit_translation.go new file mode 100644 index 0000000..65b7468 --- /dev/null +++ b/internal/engine/physics/limit_translation.go @@ -0,0 +1,44 @@ +package physics + +import ( + "github.com/mokiat/gomath/sprec" +) + +type LimitTranslationConstraint struct { + NilConstraint + FirstBody *Body + SecondBody *Body + MinY float32 + MaxY float32 +} + +func (c LimitTranslationConstraint) ApplyImpulse(ctx Context) { + deltaPosition := sprec.Vec3Diff(c.SecondBody.Position, c.FirstBody.Position) + if deltaPosition.SqrLength() < 0.000001 { + return + } + + deltaY := sprec.Vec3Dot(c.FirstBody.Orientation.OrientationY(), deltaPosition) + normalY := sprec.Vec3Prod(c.FirstBody.Orientation.OrientationY(), deltaY) + + deltaVelocity := sprec.Vec3Diff(c.SecondBody.Velocity, sprec.Vec3Sum(c.FirstBody.Velocity, sprec.Vec3Cross(c.FirstBody.AngularVelocity, deltaPosition))) + contactVelocity := sprec.Vec3Dot(normalY, deltaVelocity) + + if deltaY > c.MaxY && contactVelocity < 0 { + firstInverseMass := (1.0 / c.FirstBody.Mass) + sprec.Vec3Dot(sprec.Mat3Vec3Prod(sprec.InverseMat3(c.FirstBody.MomentOfInertia), sprec.Vec3Cross(deltaPosition, normalY)), sprec.Vec3Cross(deltaPosition, normalY)) + secondInverseMass := (1.0 / c.SecondBody.Mass) + totalMass := 1.0 / (firstInverseMass + secondInverseMass) + impulseStrength := totalMass * contactVelocity + c.FirstBody.ApplyOffsetImpulse(deltaPosition, sprec.Vec3Prod(normalY, impulseStrength)) + c.SecondBody.ApplyImpulse(sprec.Vec3Prod(normalY, -impulseStrength)) + } + + if deltaY < c.MinY && contactVelocity > 0 { + firstInverseMass := (1.0 / c.FirstBody.Mass) + sprec.Vec3Dot(sprec.Mat3Vec3Prod(sprec.InverseMat3(c.FirstBody.MomentOfInertia), sprec.Vec3Cross(deltaPosition, normalY)), sprec.Vec3Cross(deltaPosition, normalY)) + secondInverseMass := (1.0 / c.SecondBody.Mass) + totalMass := 1.0 / (firstInverseMass + secondInverseMass) + impulseStrength := totalMass * contactVelocity + c.FirstBody.ApplyOffsetImpulse(deltaPosition, sprec.Vec3Prod(normalY, impulseStrength)) + c.SecondBody.ApplyImpulse(sprec.Vec3Prod(normalY, -impulseStrength)) + } +} diff --git a/internal/engine/physics/match_axis.go b/internal/engine/physics/match_axis.go new file mode 100644 index 0000000..8e4efe1 --- /dev/null +++ b/internal/engine/physics/match_axis.go @@ -0,0 +1,50 @@ +package physics + +import "github.com/mokiat/gomath/sprec" + +type MatchAxisConstraint struct { + NilConstraint + FirstBody *Body + FirstBodyAxis sprec.Vec3 + SecondBody *Body + SecondBodyAxis sprec.Vec3 +} + +func (c MatchAxisConstraint) ApplyImpulse(ctx Context) { + result := c.Calculate() + if sprec.Abs(result.Drift) > 0.0001 { + result.Jacobian.CorrectVelocity(c.FirstBody, c.SecondBody) + } +} + +func (c MatchAxisConstraint) ApplyNudge(ctx Context) { + result := c.Calculate() + if sprec.Abs(result.Drift) > 0.0001 { + result.Jacobian.CorrectPosition(c.FirstBody, c.SecondBody, result.Drift) + } +} + +func (c MatchAxisConstraint) Calculate() MatchAxisConstraintResult { + // FIXME: Does not handle when axis are pointing in opposite directions + firstAxisWS := sprec.QuatVec3Rotation(c.FirstBody.Orientation, c.FirstBodyAxis) + secondAxisWS := sprec.QuatVec3Rotation(c.SecondBody.Orientation, c.SecondBodyAxis) + cross := sprec.Vec3Cross(firstAxisWS, secondAxisWS) + return MatchAxisConstraintResult{ + Jacobian: PairJacobian{ + First: Jacobian{ + SlopeVelocity: sprec.ZeroVec3(), + SlopeAngularVelocity: sprec.InverseVec3(cross), + }, + Second: Jacobian{ + SlopeVelocity: sprec.ZeroVec3(), + SlopeAngularVelocity: cross, + }, + }, + Drift: cross.Length(), + } +} + +type MatchAxisConstraintResult struct { + Jacobian PairJacobian + Drift float32 +} diff --git a/internal/engine/physics/match_rotation.go b/internal/engine/physics/match_rotation.go new file mode 100644 index 0000000..005a375 --- /dev/null +++ b/internal/engine/physics/match_rotation.go @@ -0,0 +1,37 @@ +package physics + +import "github.com/mokiat/gomath/sprec" + +type MatchRotationConstraint struct { + NilConstraint + FirstBody *Body + SecondBody *Body +} + +func (c MatchRotationConstraint) ApplyImpulse(ctx Context) { + c.yConstraint().ApplyImpulse(ctx) + c.zConstraint().ApplyImpulse(ctx) +} + +func (c MatchRotationConstraint) ApplyNudge(ctx Context) { + c.yConstraint().ApplyNudge(ctx) + c.zConstraint().ApplyNudge(ctx) +} + +func (c MatchRotationConstraint) yConstraint() MatchAxisConstraint { + return MatchAxisConstraint{ + FirstBody: c.FirstBody, + FirstBodyAxis: sprec.BasisYVec3(), + SecondBody: c.SecondBody, + SecondBodyAxis: sprec.BasisYVec3(), + } +} + +func (c MatchRotationConstraint) zConstraint() MatchAxisConstraint { + return MatchAxisConstraint{ + FirstBody: c.FirstBody, + FirstBodyAxis: sprec.BasisZVec3(), + SecondBody: c.SecondBody, + SecondBodyAxis: sprec.BasisZVec3(), + } +} diff --git a/internal/engine/physics/match_translation.go b/internal/engine/physics/match_translation.go new file mode 100644 index 0000000..1eddf20 --- /dev/null +++ b/internal/engine/physics/match_translation.go @@ -0,0 +1,77 @@ +package physics + +import "github.com/mokiat/gomath/sprec" + +type MatchTranslationConstraint struct { + NilConstraint + FirstBody *Body + FirstBodyAnchor sprec.Vec3 + SecondBody *Body + IgnoreX bool + IgnoreY bool + IgnoreZ bool +} + +func (c MatchTranslationConstraint) ApplyImpulse(ctx Context) { + result := c.Calculate() + if sprec.Abs(result.Drift) > 0.0001 { + result.Jacobian.CorrectVelocity(c.FirstBody, c.SecondBody) + } +} + +func (c MatchTranslationConstraint) ApplyNudge(ctx Context) { + result := c.Calculate() + if sprec.Abs(result.Drift) > 0.0001 { + result.Jacobian.CorrectPosition(c.FirstBody, c.SecondBody, result.Drift) + } +} + +func (c MatchTranslationConstraint) Calculate() MatchTranslationResultConstraint { + firstRadiusWS := sprec.QuatVec3Rotation(c.FirstBody.Orientation, c.FirstBodyAnchor) + firstAnchorWS := sprec.Vec3Sum(c.FirstBody.Position, firstRadiusWS) + deltaPosition := sprec.Vec3Diff(c.SecondBody.Position, firstAnchorWS) + if c.IgnoreX { + deltaPosition = sprec.Vec3Diff(deltaPosition, sprec.Vec3Prod(c.FirstBody.Orientation.OrientationX(), sprec.Vec3Dot(deltaPosition, c.FirstBody.Orientation.OrientationX()))) + } + if c.IgnoreY { + deltaPosition = sprec.Vec3Diff(deltaPosition, sprec.Vec3Prod(c.FirstBody.Orientation.OrientationY(), sprec.Vec3Dot(deltaPosition, c.FirstBody.Orientation.OrientationY()))) + } + if c.IgnoreZ { + deltaPosition = sprec.Vec3Diff(deltaPosition, sprec.Vec3Prod(c.FirstBody.Orientation.OrientationZ(), sprec.Vec3Dot(deltaPosition, c.FirstBody.Orientation.OrientationZ()))) + } + normal := sprec.BasisXVec3() + if deltaPosition.SqrLength() > 0.000001 { + normal = sprec.UnitVec3(deltaPosition) + } + + return MatchTranslationResultConstraint{ + Jacobian: PairJacobian{ + First: Jacobian{ + SlopeVelocity: sprec.NewVec3( + -normal.X, + -normal.Y, + -normal.Z, + ), + SlopeAngularVelocity: sprec.NewVec3( + -(normal.Z*firstRadiusWS.Y - normal.Y*firstRadiusWS.Z), + -(normal.X*firstRadiusWS.Z - normal.Z*firstRadiusWS.X), + -(normal.Y*firstRadiusWS.X - normal.X*firstRadiusWS.Y), + ), + }, + Second: Jacobian{ + SlopeVelocity: sprec.NewVec3( + normal.X, + normal.Y, + normal.Z, + ), + SlopeAngularVelocity: sprec.ZeroVec3(), + }, + }, + Drift: deltaPosition.Length(), + } +} + +type MatchTranslationResultConstraint struct { + Jacobian PairJacobian + Drift float32 +} diff --git a/internal/engine/shape/box.go b/internal/engine/shape/box.go new file mode 100644 index 0000000..ac708a7 --- /dev/null +++ b/internal/engine/shape/box.go @@ -0,0 +1,27 @@ +package shape + +func NewStaticBox(width, height, length float32) StaticBox { + return StaticBox{ + width: width, + height: height, + length: length, + } +} + +type StaticBox struct { + width float32 + height float32 + length float32 +} + +func (b StaticBox) Width() float32 { + return b.width +} + +func (b StaticBox) Height() float32 { + return b.height +} + +func (b StaticBox) Length() float32 { + return b.length +} diff --git a/internal/engine/shape/intersection.go b/internal/engine/shape/intersection.go new file mode 100644 index 0000000..796c80a --- /dev/null +++ b/internal/engine/shape/intersection.go @@ -0,0 +1,226 @@ +package shape + +import "github.com/mokiat/gomath/sprec" + +type Intersection struct { + Depth float32 + FirstContact sprec.Vec3 + FirstDisplaceNormal sprec.Vec3 + SecondContact sprec.Vec3 + SecondDisplaceNormal sprec.Vec3 +} + +func (i Intersection) Flipped() Intersection { + i.FirstContact, i.SecondContact = i.SecondContact, i.FirstContact + i.FirstDisplaceNormal, i.SecondDisplaceNormal = i.SecondDisplaceNormal, i.FirstDisplaceNormal + return i +} + +func NewIntersectionResultSet(capacity int) *IntersectionResultSet { + return &IntersectionResultSet{ + intersections: make([]Intersection, 0, capacity), + flipped: false, + } +} + +type IntersectionResultSet struct { + intersections []Intersection + flipped bool +} + +func (s *IntersectionResultSet) Reset() { + s.intersections = s.intersections[:0] +} + +func (s *IntersectionResultSet) AddFlipped(flipped bool) { + s.flipped = true +} + +func (s *IntersectionResultSet) Add(intersection Intersection) { + if s.flipped { + s.intersections = append(s.intersections, intersection.Flipped()) + } else { + s.intersections = append(s.intersections, intersection) + } +} + +func (s *IntersectionResultSet) Found() bool { + return len(s.intersections) > 0 +} + +func (s *IntersectionResultSet) Intersections() []Intersection { + return s.intersections +} + +func CheckIntersection(first, second Placement, resultSet *IntersectionResultSet) { + switch first.Shape.(type) { + case StaticSphere: + CheckIntersectionSphereUnknown(first, second, resultSet) + case StaticBox: + CheckIntersectionBoxUnknown(first, second, resultSet) + case StaticMesh: + CheckIntersectionMeshUnknown(first, second, resultSet) + } +} + +func CheckIntersectionSphereUnknown(first, second Placement, resultSet *IntersectionResultSet) { + switch second.Shape.(type) { + case StaticSphere: + resultSet.AddFlipped(false) + CheckIntersectionSphereSphere(first, second, resultSet) + case StaticBox: + resultSet.AddFlipped(false) + CheckIntersectionSphereBox(first, second, resultSet) + case StaticMesh: + resultSet.AddFlipped(false) + CheckIntersectionSphereMesh(first, second, resultSet) + } +} + +func CheckIntersectionBoxUnknown(first, second Placement, resultSet *IntersectionResultSet) { + switch second.Shape.(type) { + case StaticSphere: + resultSet.AddFlipped(true) + CheckIntersectionSphereBox(second, first, resultSet) + case StaticBox: + resultSet.AddFlipped(false) + CheckIntersectionBoxBox(first, second, resultSet) + case StaticMesh: + resultSet.AddFlipped(false) + CheckIntersectionBoxMesh(first, second, resultSet) + } +} + +func CheckIntersectionMeshUnknown(first, second Placement, resultSet *IntersectionResultSet) { + switch second.Shape.(type) { + case StaticSphere: + resultSet.AddFlipped(true) + CheckIntersectionSphereMesh(second, first, resultSet) + case StaticBox: + resultSet.AddFlipped(true) + CheckIntersectionBoxMesh(second, first, resultSet) + case StaticMesh: + resultSet.AddFlipped(false) + CheckIntersectionMeshMesh(first, second, resultSet) + } +} + +func CheckIntersectionSphereSphere(first, second Placement, resultSet *IntersectionResultSet) { +} + +func CheckIntersectionSphereBox(first, second Placement, resultSet *IntersectionResultSet) { +} + +func CheckIntersectionSphereMesh(spherePlacement, meshPlacement Placement, resultSet *IntersectionResultSet) { + sphere := spherePlacement.Shape.(StaticSphere) + mesh := meshPlacement.Shape.(StaticMesh) + + // broad phase + deltaPosition := sprec.Vec3Diff(meshPlacement.Position, spherePlacement.Position) + if deltaPosition.Length() > sphere.Radius()+mesh.BoundingSphereRadius() { + return + } + + // narrow phase + for _, triangle := range mesh.Triangles() { + triangleWS := triangle.Transformed(meshPlacement.Position, meshPlacement.Orientation) + height := sprec.Vec3Dot(triangleWS.Normal(), sprec.Vec3Diff(spherePlacement.Position, triangleWS.A())) + if height > sphere.Radius() || height < -sphere.Radius() { + continue + } + + projectedPoint := sprec.Vec3Diff(spherePlacement.Position, sprec.Vec3Prod(triangle.Normal(), height)) + if triangleWS.ContainsPoint(projectedPoint) { + resultSet.Add(Intersection{ + Depth: sphere.Radius() - height, + FirstContact: projectedPoint, // TODO: Extrude to equal radius length + FirstDisplaceNormal: triangle.Normal(), + SecondContact: projectedPoint, + SecondDisplaceNormal: sprec.InverseVec3(triangle.Normal()), + }) + } + } +} + +func CheckIntersectionBoxBox(first, second Placement, resultSet *IntersectionResultSet) { +} + +func CheckIntersectionBoxMesh(boxPlacement, meshPlacement Placement, resultSet *IntersectionResultSet) { + box := boxPlacement.Shape.(StaticBox) + mesh := meshPlacement.Shape.(StaticMesh) + + // broad phase + deltaPosition := sprec.Vec3Diff(meshPlacement.Position, boxPlacement.Position) + boxBoundingSphereRadius := sprec.Sqrt(box.Width()*box.Width()+box.Height()*box.Height()+box.Length()*box.Length()) / 2.0 + if deltaPosition.Length() > boxBoundingSphereRadius+mesh.BoundingSphereRadius() { + return + } + + minX := sprec.Vec3Prod(boxPlacement.Orientation.OrientationX(), -box.Width()/2.0) + maxX := sprec.Vec3Prod(boxPlacement.Orientation.OrientationX(), box.Width()/2.0) + minY := sprec.Vec3Prod(boxPlacement.Orientation.OrientationY(), -box.Height()/2.0) + maxY := sprec.Vec3Prod(boxPlacement.Orientation.OrientationY(), box.Height()/2.0) + minZ := sprec.Vec3Prod(boxPlacement.Orientation.OrientationZ(), -box.Length()/2.0) + maxZ := sprec.Vec3Prod(boxPlacement.Orientation.OrientationZ(), box.Length()/2.0) + + p1 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(boxPlacement.Position, minX), minZ), maxY) + p2 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(boxPlacement.Position, minX), maxZ), maxY) + p3 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(boxPlacement.Position, maxX), maxZ), maxY) + p4 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(boxPlacement.Position, maxX), minZ), maxY) + p5 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(boxPlacement.Position, minX), minZ), minY) + p6 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(boxPlacement.Position, minX), maxZ), minY) + p7 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(boxPlacement.Position, maxX), maxZ), minY) + p8 := sprec.Vec3Sum(sprec.Vec3Sum(sprec.Vec3Sum(boxPlacement.Position, maxX), minZ), minY) + + checkLineIntersection := func(line StaticLine, triangle StaticTriangle) { + heightA := sprec.Vec3Dot(triangle.Normal(), sprec.Vec3Diff(line.A(), triangle.A())) + heightB := sprec.Vec3Dot(triangle.Normal(), sprec.Vec3Diff(line.B(), triangle.A())) + if heightA > 0.0 && heightB > 0.0 { + return + } + if heightA < 0.0 && heightB < 0.0 { + return + } + if heightA < 0.0 { + line = NewStaticLine(line.B(), line.A()) + heightA, heightB = heightB, heightA + } + + projectedPoint := sprec.Vec3Sum( + sprec.Vec3Prod(line.A(), -heightB/(heightA-heightB)), + sprec.Vec3Prod(line.B(), heightA/(heightA-heightB)), + ) + + if triangle.ContainsPoint(projectedPoint) { + resultSet.Add(Intersection{ + Depth: -heightB, + FirstContact: projectedPoint, + FirstDisplaceNormal: triangle.Normal(), + SecondContact: projectedPoint, + SecondDisplaceNormal: sprec.InverseVec3(triangle.Normal()), + }) + } + } + + // narrow phase + for _, triangle := range mesh.Triangles() { + triangleWS := triangle.Transformed(meshPlacement.Position, meshPlacement.Orientation) + checkLineIntersection(NewStaticLine(p1, p2), triangleWS) + checkLineIntersection(NewStaticLine(p2, p3), triangleWS) + checkLineIntersection(NewStaticLine(p3, p4), triangleWS) + checkLineIntersection(NewStaticLine(p4, p1), triangleWS) + + checkLineIntersection(NewStaticLine(p5, p6), triangleWS) + checkLineIntersection(NewStaticLine(p6, p7), triangleWS) + checkLineIntersection(NewStaticLine(p7, p8), triangleWS) + checkLineIntersection(NewStaticLine(p8, p5), triangleWS) + + checkLineIntersection(NewStaticLine(p1, p5), triangleWS) + checkLineIntersection(NewStaticLine(p2, p6), triangleWS) + checkLineIntersection(NewStaticLine(p3, p7), triangleWS) + checkLineIntersection(NewStaticLine(p4, p8), triangleWS) + } +} + +func CheckIntersectionMeshMesh(first, second Placement, resultSet *IntersectionResultSet) { +} diff --git a/internal/engine/shape/line.go b/internal/engine/shape/line.go new file mode 100644 index 0000000..4e7cfe4 --- /dev/null +++ b/internal/engine/shape/line.go @@ -0,0 +1,31 @@ +package shape + +import "github.com/mokiat/gomath/sprec" + +func NewStaticLine(a, b sprec.Vec3) StaticLine { + return StaticLine{ + a: a, + b: b, + } +} + +type StaticLine struct { + a sprec.Vec3 + b sprec.Vec3 +} + +func (l StaticLine) A() sprec.Vec3 { + return l.a +} + +func (l StaticLine) B() sprec.Vec3 { + return l.b +} + +func (l StaticLine) SqrLength() float32 { + return sprec.Vec3Diff(l.b, l.a).SqrLength() +} + +func (l StaticLine) Length() float32 { + return sprec.Vec3Diff(l.b, l.a).Length() +} diff --git a/internal/engine/shape/mesh.go b/internal/engine/shape/mesh.go new file mode 100644 index 0000000..0ea5895 --- /dev/null +++ b/internal/engine/shape/mesh.go @@ -0,0 +1,37 @@ +package shape + +func NewStaticMesh(triangles []StaticTriangle) StaticMesh { + return StaticMesh{ + triangles: triangles, + radius: triangleListBoundingSphereRadius(triangles), + } +} + +type StaticMesh struct { + triangles []StaticTriangle + radius float32 +} + +func (m StaticMesh) Triangles() []StaticTriangle { + return m.triangles +} + +func (m StaticMesh) BoundingSphereRadius() float32 { + return m.radius +} + +func triangleListBoundingSphereRadius(triangles []StaticTriangle) float32 { + var radius float32 + for _, triangle := range triangles { + if pointDistance := triangle.A().Length(); pointDistance > radius { + radius = pointDistance + } + if pointDistance := triangle.B().Length(); pointDistance > radius { + radius = pointDistance + } + if pointDistance := triangle.C().Length(); pointDistance > radius { + radius = pointDistance + } + } + return radius +} diff --git a/internal/engine/shape/placement.go b/internal/engine/shape/placement.go new file mode 100644 index 0000000..6c7e497 --- /dev/null +++ b/internal/engine/shape/placement.go @@ -0,0 +1,15 @@ +package shape + +import "github.com/mokiat/gomath/sprec" + +type Placement struct { + Position sprec.Vec3 + Orientation sprec.Quat + Shape interface{} +} + +func (p Placement) Transformed(translation sprec.Vec3, rotation sprec.Quat) Placement { + p.Position = sprec.Vec3Sum(translation, sprec.QuatVec3Rotation(rotation, p.Position)) + p.Orientation = sprec.QuatProd(rotation, p.Orientation) + return p +} diff --git a/internal/engine/shape/sphere.go b/internal/engine/shape/sphere.go new file mode 100644 index 0000000..b1d30ed --- /dev/null +++ b/internal/engine/shape/sphere.go @@ -0,0 +1,15 @@ +package shape + +func NewStaticSphere(radius float32) StaticSphere { + return StaticSphere{ + radius: radius, + } +} + +type StaticSphere struct { + radius float32 +} + +func (s StaticSphere) Radius() float32 { + return s.radius +} diff --git a/internal/engine/shape/triangle.go b/internal/engine/shape/triangle.go new file mode 100644 index 0000000..a1cc250 --- /dev/null +++ b/internal/engine/shape/triangle.go @@ -0,0 +1,67 @@ +package shape + +import "github.com/mokiat/gomath/sprec" + +func NewStaticTriangle(a, b, c sprec.Vec3) StaticTriangle { + return StaticTriangle{ + a: a, + b: b, + c: c, + } +} + +type StaticTriangle struct { + a sprec.Vec3 + b sprec.Vec3 + c sprec.Vec3 +} + +func (t StaticTriangle) Transformed(translation sprec.Vec3, rotation sprec.Quat) StaticTriangle { + return StaticTriangle{ + a: sprec.Vec3Sum(translation, sprec.QuatVec3Rotation(rotation, t.a)), + b: sprec.Vec3Sum(translation, sprec.QuatVec3Rotation(rotation, t.b)), + c: sprec.Vec3Sum(translation, sprec.QuatVec3Rotation(rotation, t.c)), + } +} + +func (t StaticTriangle) A() sprec.Vec3 { + return t.a +} + +func (t StaticTriangle) B() sprec.Vec3 { + return t.b +} + +func (t StaticTriangle) C() sprec.Vec3 { + return t.c +} + +func (t StaticTriangle) Normal() sprec.Vec3 { + vecAB := sprec.Vec3Diff(t.b, t.a) + vecAC := sprec.Vec3Diff(t.c, t.a) + return sprec.UnitVec3(sprec.Vec3Cross(vecAB, vecAC)) +} + +func (t StaticTriangle) Area() float32 { + vecAB := sprec.Vec3Diff(t.b, t.a) + vecAC := sprec.Vec3Diff(t.c, t.a) + return sprec.Vec3Cross(vecAB, vecAC).Length() / 2.0 +} + +func (t StaticTriangle) IsLookingTowards(direction sprec.Vec3) bool { + return sprec.Vec3Dot(t.Normal(), direction) > 0.0 +} + +func (t StaticTriangle) ContainsPoint(point sprec.Vec3) bool { + normal := t.Normal() + if triangleABP := NewStaticTriangle(t.a, t.b, point); !triangleABP.IsLookingTowards(normal) { + return false + } + if triangleBCP := NewStaticTriangle(t.b, t.c, point); !triangleBCP.IsLookingTowards(normal) { + return false + } + if triangleCAP := NewStaticTriangle(t.c, t.a, point); !triangleCAP.IsLookingTowards(normal) { + return false + } + return true +} diff --git a/resources/levels/playground.json b/resources/levels/playground.json index 9bee18a..ad38ba1 100644 --- a/resources/levels/playground.json +++ b/resources/levels/playground.json @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49a5eaf940fc90bbe4106289d51bd132ca5a0250d2fd884512d3e5ec3c3d4653 -size 1117 +oid sha256:8fee344296eabe0438adffb61843ecfc7caddd6fd56f3b5d9a1c2edbfa632cf0 +size 1149 diff --git a/resources/shaders/debug.frag b/resources/shaders/debug.frag index d3a1fd5..d8987f1 100644 --- a/resources/shaders/debug.frag +++ b/resources/shaders/debug.frag @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b1c6ea120843ceeae1c586e85f3a0be7f1f952cec599580af3738427f506223 -size 131 +oid sha256:a012d8a49ca5991951d7c8b65a2a9bfd4f6291e30ce663babea090ebde1eebca +size 229 diff --git a/resources/shaders/debug.vert b/resources/shaders/debug.vert index e645cfc..d7befdc 100644 --- a/resources/shaders/debug.vert +++ b/resources/shaders/debug.vert @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6760035e40b8337517d928d96adcdca75240b03071469aa779930c040e35b851 -size 338 +oid sha256:041933a2eef1458d4a3449e99cc8457c3df6ec87f8d6fcb06c990c63b6f0d34f +size 292