diff --git a/apps/build.gradle b/apps/build.gradle index cea3fc19..dcd27cd9 100644 --- a/apps/build.gradle +++ b/apps/build.gradle @@ -143,6 +143,10 @@ tasks.register('HelloMotor', JavaExec) { description 'Runs the HelloMotor tutorial app.' mainClass = 'com.github.stephengold.lbjexamples.apps.HelloMotor' } +tasks.register('HelloNewHinge', JavaExec) { + description 'Runs the HelloNewHinge tutorial app.' + mainClass = 'com.github.stephengold.lbjexamples.apps.HelloNewHinge' +} tasks.register('HelloNonUniformGravity', JavaExec) { description 'Runs the HelloNonUniformGravity tutorial app.' mainClass = 'com.github.stephengold.lbjexamples.apps.HelloNonUniformGravity' diff --git a/apps/src/main/java/com/github/stephengold/lbjexamples/AppChooser.java b/apps/src/main/java/com/github/stephengold/lbjexamples/AppChooser.java index a959702c..0ee09b45 100644 --- a/apps/src/main/java/com/github/stephengold/lbjexamples/AppChooser.java +++ b/apps/src/main/java/com/github/stephengold/lbjexamples/AppChooser.java @@ -45,6 +45,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.github.stephengold.lbjexamples.apps.HelloMassDistribution; import com.github.stephengold.lbjexamples.apps.HelloMinkowski; import com.github.stephengold.lbjexamples.apps.HelloMotor; +import com.github.stephengold.lbjexamples.apps.HelloNewHinge; import com.github.stephengold.lbjexamples.apps.HelloNonUniformGravity; import com.github.stephengold.lbjexamples.apps.HelloPin; import com.github.stephengold.lbjexamples.apps.HelloRigidBody; @@ -113,44 +114,45 @@ public static void main(String[] arguments) { apps.add(new HelloDoubleEnded()); apps.add(new HelloGhost()); apps.add(new HelloJoint()); - apps.add(new HelloKinematics()); + apps.add(new HelloKinematics()); apps.add(new HelloLimit()); apps.add(new HelloMadMallet()); apps.add(new HelloMassDistribution()); apps.add(new HelloMinkowski()); - apps.add(new HelloMotor()); + apps.add(new HelloMotor()); + apps.add(new HelloNewHinge()); apps.add(new HelloNonUniformGravity()); apps.add(new HelloPin()); apps.add(new HelloRigidBody()); + apps.add(new HelloServo()); apps.add(new HelloSoftBody()); - apps.add(new HelloSoftRope()); apps.add(new HelloSoftSoft()); apps.add(new HelloSport()); + apps.add(new HelloSpring()); apps.add(new HelloStaticBody()); - apps.add(new HelloVehicle()); apps.add(new HelloWalk()); apps.add(new HelloWind()); + apps.add(new IcosphereTest()); apps.add(new MouseTest()); - apps.add(new MouseTest2()); apps.add(new NewtonsCradle()); apps.add(new OctasphereTest()); + apps.add(new Pachinko()); apps.add(new RainbowTest()); - apps.add(new SplitDemo()); apps.add(new SpriteTest()); apps.add(new TestGearJoint()); + apps.add(new TextureTest()); apps.add(new ThousandCubes()); - apps.add(new Windlass()); new AppChooser(apps); diff --git a/apps/src/main/java/com/github/stephengold/lbjexamples/apps/HelloNewHinge.java b/apps/src/main/java/com/github/stephengold/lbjexamples/apps/HelloNewHinge.java new file mode 100644 index 00000000..31fd8eb8 --- /dev/null +++ b/apps/src/main/java/com/github/stephengold/lbjexamples/apps/HelloNewHinge.java @@ -0,0 +1,284 @@ +/* + Copyright (c) 2020-2024 Stephen Gold and Yanis Boudiaf + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.github.stephengold.lbjexamples.apps; + +import com.github.stephengold.sport.Constants; +import com.github.stephengold.sport.TextureKey; +import com.github.stephengold.sport.input.RotateMode; +import com.github.stephengold.sport.physics.BasePhysicsApp; +import com.jme3.bullet.PhysicsSpace; +import com.jme3.bullet.PhysicsTickListener; +import com.jme3.bullet.collision.shapes.CylinderCollisionShape; +import com.jme3.bullet.collision.shapes.HullCollisionShape; +import com.jme3.bullet.collision.shapes.PlaneCollisionShape; +import com.jme3.bullet.joints.NewHinge; +import com.jme3.bullet.joints.motors.MotorParam; +import com.jme3.bullet.joints.motors.RotationMotor; +import com.jme3.bullet.objects.PhysicsBody; +import com.jme3.bullet.objects.PhysicsRigidBody; +import com.jme3.math.FastMath; +import com.jme3.math.Plane; +import com.jme3.math.Vector3f; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import jme3utilities.math.MyQuaternion; + +/** + * An example of vehicle physics using NewHinge. + *

+ * Builds upon HelloVehicle. + * + * @author Stephen Gold sgold@sonic.net + */ +public class HelloNewHinge + extends BasePhysicsApp + implements PhysicsTickListener { + // ************************************************************************* + // fields + + /** + * wheels for steering + */ + final private static List steer = new ArrayList<>(2); + /** + * drive wheels + */ + final private static List drive = new ArrayList<>(2); + private static PhysicsRigidBody chassis; + // ************************************************************************* + // new methods exposed + + /** + * Main entry point for the HelloNewHinge application. + * + * @param arguments array of command-line arguments (not null) + */ + public static void main(String[] arguments) { + HelloNewHinge application = new HelloNewHinge(); + application.start(); + } + // ************************************************************************* + // BasePhysicsApp methods + + /** + * Create the PhysicsSpace. Invoked once during initialization. + * + * @return a new instance + */ + @Override + public PhysicsSpace createSpace() { + PhysicsSpace result + = new PhysicsSpace(PhysicsSpace.BroadphaseType.DBVT); + + // To enable the callbacks, register the application as a tick listener. + result.addTickListener(this); + + return result; + } + + /** + * Initialize the application. + */ + @Override + public void initialize() { + super.initialize(); + + getCameraInputProcessor().setRotationMode(RotateMode.DragLMB); + setBackgroundColor(Constants.SKY_BLUE); + } + + /** + * Populate the PhysicsSpace. Invoked once during initialization. + */ + @Override + public void populateSpace() { + // Create a wedge-shaped vehicle with a low center of gravity. + // The local forward direction is +Z. + float noseZ = 1.4f; // offset from chassis center + float spoilerY = 0.5f; // offset from chassis center + float tailZ = -0.7f; // offset from chassis center + float undercarriageY = -0.1f; // offset from chassis center + float halfWidth = 0.4f; + Collection cornerLocations = new ArrayList<>(6); + cornerLocations.add(new Vector3f(+halfWidth, undercarriageY, noseZ)); + cornerLocations.add(new Vector3f(-halfWidth, undercarriageY, noseZ)); + cornerLocations.add(new Vector3f(+halfWidth, undercarriageY, tailZ)); + cornerLocations.add(new Vector3f(-halfWidth, undercarriageY, tailZ)); + cornerLocations.add(new Vector3f(+halfWidth, spoilerY, tailZ)); + cornerLocations.add(new Vector3f(-halfWidth, spoilerY, tailZ)); + HullCollisionShape wedgeShape + = new HullCollisionShape(cornerLocations); + float mass = 5f; + chassis = new PhysicsRigidBody(wedgeShape, mass); + chassis.setEnableSleep(false); + physicsSpace.addCollisionObject(chassis); + + // Add 4 wheels, 2 in the front (for steering) and 2 in the rear. + boolean front = true; + boolean rear = false; + float frontAxisZ = 0.7f * noseZ; // offset from chassis center + float rearAxisZ = 0.8f * tailZ; // offset from chassis center + float radius = 0.3f; // of each tire + float restLength = 0.2f; // of the suspension + float xOffset = 0.9f * halfWidth; + Vector3f axleDirection = new Vector3f(-1f, 0f, 0f); + Vector3f suspensionDirection = new Vector3f(0f, -1f, 0f); + addWheel(new Vector3f(-xOffset, 0f, frontAxisZ), + suspensionDirection, axleDirection, restLength, radius, front); + addWheel(new Vector3f(xOffset, 0f, frontAxisZ), + suspensionDirection, axleDirection, restLength, radius, front); + addWheel(new Vector3f(-xOffset, 0f, rearAxisZ), + suspensionDirection, axleDirection, restLength, radius, rear); + addWheel(new Vector3f(xOffset, 0f, rearAxisZ), + suspensionDirection, axleDirection, restLength, radius, rear); + + // Visualize the chassis. + visualizeShape(chassis); + + // Apply a steering angle of 6 degrees left (to the front wheels). + for (RotationMotor motor : steer) { + motor.set(MotorParam.ServoTarget, FastMath.PI / 30f); + } + + // Add a static plane to represent the ground. + float planeY = -radius - 0.35f; + addPlane(planeY); + } + // ************************************************************************* + // PhysicsTickListener methods + + /** + * Callback from Bullet, invoked just before each simulation step. + * + * @param space the space that's about to be stepped (not null) + * @param timeStep the time per simulation step (in seconds, ≥0) + */ + @Override + public void prePhysicsTick(PhysicsSpace space, float timeStep) { + // Apply a constant torque (to the rear wheels). + for (PhysicsRigidBody wheel : drive) { + Vector3f torque = new Vector3f(1f, 0f, 0f); + MyQuaternion.rotate(wheel.getPhysicsRotation(null), torque, torque); + wheel.applyTorque(torque); + } + } + + /** + * Callback from Bullet, invoked just after each simulation step. + * + * @param space the space that was just stepped (not null) + * @param timeStep the time per simulation step (in seconds, ≥0) + */ + @Override + public void physicsTick(PhysicsSpace space, float timeStep) { + // do nothing + } + + /** + * Update the window title. Invoked during each update. + */ + @Override + public void updateWindowTitle() { + // do nothing + } + // ************************************************************************* + // private methods + + /** + * Add a horizontal plane body to the space. + * + * @param y (the desired elevation, in physics-space coordinates) + */ + private void addPlane(float y) { + Plane plane = new Plane(Vector3f.UNIT_Y, y); + PlaneCollisionShape shape = new PlaneCollisionShape(plane); + PhysicsRigidBody body + = new PhysicsRigidBody(shape, PhysicsBody.massForStatic); + + physicsSpace.addCollisionObject(body); + + String resourceName = "/Textures/greenTile.png"; + float maxAniso = 16f; + TextureKey textureKey + = new TextureKey("classpath://" + resourceName, maxAniso); + visualizeShape(body) + .setSpecularColor(Constants.DARK_GRAY) + .setTexture(textureKey); + } + + /** + * Add a cylindrical wheel, joined to the chassis by a NewHinge. + * + * @param connectionPoint the location of the connection point (not null) + * @param suspensionDirection the direction of suspension motion (not null, + * not zero, unaffected) + * @param axle the direction of the axle's axis (not null, not zero, + * unaffected) + * @param restLength the rest length + * @param wheelRadius the desired radius of the wheel + * @param isFrontWheel true for a front/steer wheel, false for a rear/drive + * wheel + */ + private void addWheel(Vector3f connectionPoint, + Vector3f suspensionDirection, Vector3f axle, float restLength, + float wheelRadius, boolean isFrontWheel) { + float thickness = 0.5f * wheelRadius; + CylinderCollisionShape shape = new CylinderCollisionShape( + wheelRadius, thickness, PhysicsSpace.AXIS_X); + float mass = 0.5f; + PhysicsRigidBody body = new PhysicsRigidBody(shape, mass); + body.setEnableSleep(false); + Vector3f center = connectionPoint.add(0f, -restLength, 0f); + body.setPhysicsLocation(center); + physicsSpace.addCollisionObject(body); + + // Visualize the shape of the wheel: + visualizeShape(body); + + NewHinge joint = new NewHinge( + chassis, body, center, suspensionDirection, axle); + if (isFrontWheel) { + RotationMotor motor = joint.getRotationMotor(PhysicsSpace.AXIS_Z); + motor.setMotorEnabled(true); + motor.setServoEnabled(true); + motor.set(MotorParam.TargetVelocity, 1f); + steer.add(motor); + } else { + joint.set(MotorParam.LowerLimit, 3 + PhysicsSpace.AXIS_Z, 0f); + joint.set(MotorParam.UpperLimit, 3 + PhysicsSpace.AXIS_Z, 0f); + drive.add(body); + } + joint.set(MotorParam.Damping, PhysicsSpace.AXIS_Z, 30f); + joint.set(MotorParam.Stiffness, PhysicsSpace.AXIS_Z, 90f); + joint.setCollisionBetweenLinkedBodies(false); + physicsSpace.addJoint(joint); + } +}