org.lwjgl.demo.game.SpaceGame.java Source code

Java tutorial

Introduction

Here is the source code for org.lwjgl.demo.game.SpaceGame.java

Source

/*
 * Copyright LWJGL. All rights reserved.
 * License terms: http://lwjgl.org/license.php
 */
package org.lwjgl.demo.game;

import org.lwjgl.BufferUtils;
import org.lwjgl.PointerBuffer;
import org.lwjgl.demo.opengl.util.WavefrontMeshLoader;
import org.lwjgl.demo.opengl.util.WavefrontMeshLoader.Mesh;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GLCapabilities;
import org.lwjgl.opengl.GLUtil;
import org.lwjgl.system.Callback;
import org.joml.FrustumIntersection;
import org.joml.GeometryUtils;
import org.joml.Intersectiond;
import org.joml.Intersectionf;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
import org.joml.Vector3d;
import org.joml.Vector3f;
import org.joml.Vector4d;
import org.joml.Vector4f;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import static org.lwjgl.opengl.ARBSeamlessCubeMap.*;
import static org.lwjgl.demo.opengl.util.DemoUtils.*;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL13.*;
import static org.lwjgl.opengl.GL14.*;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.stb.STBEasyFont.stb_easy_font_print;
import static org.lwjgl.stb.STBImage.*;
import static org.lwjgl.system.MemoryUtil.*;

/**
 * A little 3D space shooter.
 * 
 * @author Kai Burjack
 */
public class SpaceGame {
    private static class SpaceCamera {
        public Vector3f linearAcc = new Vector3f();
        public Vector3f linearVel = new Vector3f();
        public float linearDamping = 0.08f;

        /** ALWAYS rotation about the local XYZ axes of the camera! */
        public Vector3f angularAcc = new Vector3f();
        public Vector3f angularVel = new Vector3f();
        public float angularDamping = 0.5f;

        public Vector3d position = new Vector3d(0, 0, 10);
        public Quaternionf rotation = new Quaternionf();

        public SpaceCamera update(float dt) {
            // update linear velocity based on linear acceleration
            linearVel.fma(dt, linearAcc);
            // update angular velocity based on angular acceleration
            angularVel.fma(dt, angularAcc);
            // update the rotation based on the angular velocity
            rotation.integrate(dt, angularVel.x, angularVel.y, angularVel.z);
            angularVel.mul(1.0f - angularDamping * dt);
            // update position based on linear velocity
            position.fma(dt, linearVel);
            linearVel.mul(1.0f - linearDamping * dt);
            return this;
        }

        public Vector3f right(Vector3f dest) {
            return rotation.positiveX(dest);
        }

        public Vector3f up(Vector3f dest) {
            return rotation.positiveY(dest);
        }

        public Vector3f forward(Vector3f dest) {
            return rotation.positiveZ(dest).negate();
        }
    }

    private static class Ship {
        public double x, y, z;
        public long lastShotTime;
    }

    private static class Asteroid {
        public double x, y, z;
        public float scale;
    }

    private static float shotVelocity = 450.0f;
    private static float shotSeparation = 0.8f;
    private static int shotMilliseconds = 80;
    private static int shotOpponentMilliseconds = 200;
    private static float straveThrusterAccFactor = 20.0f;
    private static float mainThrusterAccFactor = 50.0f;
    private static float maxLinearVel = 200.0f;
    private static float maxShotLifetime = 30.0f;
    private static float maxParticleLifetime = 1.0f;
    private static float shotSize = 0.5f;
    private static float particleSize = 1.0f;
    private static final int explosionParticles = 60;
    private static final int maxParticles = 4096;
    private static final int maxShots = 1024;

    private long window;
    private int width = 800;
    private int height = 600;

    private int cubemapProgram;
    private int cubemap_invViewProjUniform;

    private int shipProgram;
    private int ship_viewUniform;
    private int ship_projUniform;
    private int ship_modelUniform;

    private int shotProgram;
    private int shot_projUniform;

    private int particleProgram;
    private int particle_projUniform;

    private ByteBuffer quadVertices;
    private Mesh ship;
    private int shipPositionVbo;
    private int shipNormalVbo;
    private Mesh sphere;
    private Mesh asteroid;
    private int asteroidPositionVbo;
    private int asteroidNormalVbo;
    private int shipCount = 128;
    private int asteroidCount = 512;
    private float maxAsteroidRadius = 20.0f;
    private static float shipSpread = 1000.0f;
    private static float shipRadius = 4.0f;
    private Ship[] ships = new Ship[shipCount];
    {
        for (int i = 0; i < ships.length; i++) {
            Ship ship = new Ship();
            ship.x = (Math.random() - 0.5) * shipSpread;
            ship.y = (Math.random() - 0.5) * shipSpread;
            ship.z = (Math.random() - 0.5) * shipSpread;
            ships[i] = ship;
        }
    }
    private Asteroid[] asteroids = new Asteroid[asteroidCount];
    {
        for (int i = 0; i < asteroids.length; i++) {
            Asteroid asteroid = new Asteroid();
            float scale = (float) ((Math.random() * 0.5 + 0.5) * maxAsteroidRadius);
            asteroid.x = (Math.random() - 0.5) * shipSpread;
            asteroid.y = (Math.random() - 0.5) * shipSpread;
            asteroid.z = (Math.random() - 0.5) * shipSpread;
            asteroid.scale = scale;
            asteroids[i] = asteroid;
        }
    }

    private Vector3d[] projectilePositions = new Vector3d[1024];
    private Vector4f[] projectileVelocities = new Vector4f[1024];
    {
        for (int i = 0; i < projectilePositions.length; i++) {
            Vector3d projectilePosition = new Vector3d(0, 0, 0);
            projectilePositions[i] = projectilePosition;
            Vector4f projectileVelocity = new Vector4f(0, 0, 0, 0);
            projectileVelocities[i] = projectileVelocity;
        }
    }
    private Vector3d[] particlePositions = new Vector3d[maxParticles];
    private Vector4d[] particleVelocities = new Vector4d[maxParticles];
    {
        for (int i = 0; i < particlePositions.length; i++) {
            Vector3d particlePosition = new Vector3d(0, 0, 0);
            particlePositions[i] = particlePosition;
            Vector4d particleVelocity = new Vector4d(0, 0, 0, 0);
            particleVelocities[i] = particleVelocity;
        }
    }
    private FloatBuffer shotsVertices = BufferUtils.createFloatBuffer(6 * 6 * maxShots);
    private FloatBuffer particleVertices = BufferUtils.createFloatBuffer(6 * 6 * maxParticles);
    private FloatBuffer crosshairVertices = BufferUtils.createFloatBuffer(6 * 2);

    private ByteBuffer charBuffer = BufferUtils.createByteBuffer(16 * 270);

    private boolean windowed = false;
    private boolean[] keyDown = new boolean[GLFW.GLFW_KEY_LAST];
    private boolean leftMouseDown = false;
    private boolean rightMouseDown = false;
    private long lastShotTime = 0L;
    private int shootingShip = 0;
    private float mouseX = 0.0f;
    private float mouseY = 0.0f;
    private long lastTime = System.nanoTime();
    private SpaceCamera cam = new SpaceCamera();
    private Vector3d tmp = new Vector3d();
    private Vector3d newPosition = new Vector3d();
    private Vector3f tmp2 = new Vector3f();
    private Vector3f tmp3 = new Vector3f();
    private Vector3f tmp4 = new Vector3f();
    private Matrix4f projMatrix = new Matrix4f();
    private Matrix4f viewMatrix = new Matrix4f();
    private Matrix4f modelMatrix = new Matrix4f();
    private Matrix4f viewProjMatrix = new Matrix4f();
    private Matrix4f invViewMatrix = new Matrix4f();
    private Matrix4f invViewProjMatrix = new Matrix4f();
    private FloatBuffer matrixBuffer = BufferUtils.createFloatBuffer(16);
    private FrustumIntersection frustumIntersection = new FrustumIntersection();

    private GLCapabilities caps;
    private GLFWKeyCallback keyCallback;
    private GLFWCursorPosCallback cpCallback;
    private GLFWMouseButtonCallback mbCallback;
    private GLFWFramebufferSizeCallback fbCallback;
    private Callback debugProc;

    private void init() throws IOException {
        if (!glfwInit())
            throw new IllegalStateException("Unable to initialize GLFW");

        glfwDefaultWindowHints();
        glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
        glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
        glfwWindowHint(GLFW_SAMPLES, 4);

        long monitor = glfwGetPrimaryMonitor();
        GLFWVidMode vidmode = glfwGetVideoMode(monitor);
        if (!windowed) {
            width = vidmode.width();
            height = vidmode.height();
        }
        window = glfwCreateWindow(width, height, "Little Space Shooter Game", !windowed ? monitor : 0L, NULL);
        if (window == NULL) {
            throw new AssertionError("Failed to create the GLFW window");
        }
        glfwSetCursor(window, glfwCreateStandardCursor(GLFW_CROSSHAIR_CURSOR));

        glfwSetFramebufferSizeCallback(window, fbCallback = new GLFWFramebufferSizeCallback() {
            public void invoke(long window, int width, int height) {
                if (width > 0 && height > 0 && (SpaceGame.this.width != width || SpaceGame.this.height != height)) {
                    SpaceGame.this.width = width;
                    SpaceGame.this.height = height;
                }
            }
        });

        System.out.println("Press W/S to move forward/backward");
        System.out.println("Press L.Ctrl/Spacebar to move down/up");
        System.out.println("Press A/D to strafe left/right");
        System.out.println("Press Q/E to roll left/right");
        System.out.println("Hold the left mouse button to shoot");
        System.out.println("Hold the right mouse button to rotate towards the mouse cursor");
        glfwSetKeyCallback(window, keyCallback = new GLFWKeyCallback() {
            public void invoke(long window, int key, int scancode, int action, int mods) {
                if (key == GLFW_KEY_UNKNOWN)
                    return;
                if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
                    glfwSetWindowShouldClose(window, true);
                }
                if (action == GLFW_PRESS || action == GLFW_REPEAT) {
                    keyDown[key] = true;
                } else {
                    keyDown[key] = false;
                }
            }
        });
        glfwSetCursorPosCallback(window, cpCallback = new GLFWCursorPosCallback() {
            public void invoke(long window, double xpos, double ypos) {
                float normX = (float) ((xpos - width / 2.0) / width * 2.0);
                float normY = (float) ((ypos - height / 2.0) / height * 2.0);
                SpaceGame.this.mouseX = Math.max(-width / 2.0f, Math.min(width / 2.0f, normX));
                SpaceGame.this.mouseY = Math.max(-height / 2.0f, Math.min(height / 2.0f, normY));
            }
        });
        glfwSetMouseButtonCallback(window, mbCallback = new GLFWMouseButtonCallback() {
            public void invoke(long window, int button, int action, int mods) {
                if (button == GLFW_MOUSE_BUTTON_LEFT) {
                    if (action == GLFW_PRESS)
                        leftMouseDown = true;
                    else if (action == GLFW_RELEASE)
                        leftMouseDown = false;
                } else if (button == GLFW_MOUSE_BUTTON_RIGHT) {
                    if (action == GLFW_PRESS)
                        rightMouseDown = true;
                    else if (action == GLFW_RELEASE)
                        rightMouseDown = false;
                }
            }
        });
        glfwMakeContextCurrent(window);
        glfwSwapInterval(0);
        glfwShowWindow(window);

        IntBuffer framebufferSize = BufferUtils.createIntBuffer(2);
        nglfwGetFramebufferSize(window, memAddress(framebufferSize), memAddress(framebufferSize) + 4);
        width = framebufferSize.get(0);
        height = framebufferSize.get(1);
        caps = GL.createCapabilities();
        if (!caps.OpenGL20) {
            throw new AssertionError("This demo requires OpenGL 2.0.");
        }
        debugProc = GLUtil.setupDebugMessageCallback();

        /* Create all needed GL resources */
        createCubemapTexture();
        createFullScreenQuad();
        createCubemapProgram();
        createShipProgram();
        createParticleProgram();
        createShip();
        createAsteroid();
        createShotProgram();
        createSphere();

        glEnableClientState(GL_VERTEX_ARRAY);
        glEnable(GL_DEPTH_TEST);
        glEnable(GL_CULL_FACE);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    }

    private void createFullScreenQuad() {
        quadVertices = BufferUtils.createByteBuffer(4 * 2 * 6);
        FloatBuffer fv = quadVertices.asFloatBuffer();
        fv.put(-1.0f).put(-1.0f);
        fv.put(1.0f).put(-1.0f);
        fv.put(1.0f).put(1.0f);
        fv.put(1.0f).put(1.0f);
        fv.put(-1.0f).put(1.0f);
        fv.put(-1.0f).put(-1.0f);
    }

    private void createShip() throws IOException {
        WavefrontMeshLoader loader = new WavefrontMeshLoader();
        ship = loader.loadMesh("org/lwjgl/demo/game/ship.obj.zip");
        shipPositionVbo = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, shipPositionVbo);
        glBufferData(GL_ARRAY_BUFFER, ship.positions, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        shipNormalVbo = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, shipNormalVbo);
        glBufferData(GL_ARRAY_BUFFER, ship.normals, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }

    private void createAsteroid() throws IOException {
        WavefrontMeshLoader loader = new WavefrontMeshLoader();
        asteroid = loader.loadMesh("org/lwjgl/demo/game/asteroid.obj.zip");
        asteroidPositionVbo = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, asteroidPositionVbo);
        glBufferData(GL_ARRAY_BUFFER, asteroid.positions, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        asteroidNormalVbo = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, asteroidNormalVbo);
        glBufferData(GL_ARRAY_BUFFER, asteroid.normals, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }

    private void createSphere() throws IOException {
        WavefrontMeshLoader loader = new WavefrontMeshLoader();
        sphere = loader.loadMesh("org/lwjgl/demo/game/sphere.obj.zip");
    }

    private static int createShader(String resource, int type) throws IOException {
        int shader = glCreateShader(type);
        ByteBuffer source = ioResourceToByteBuffer(resource, 1024);
        PointerBuffer strings = BufferUtils.createPointerBuffer(1);
        IntBuffer lengths = BufferUtils.createIntBuffer(1);
        strings.put(0, source);
        lengths.put(0, source.remaining());
        glShaderSource(shader, strings, lengths);
        glCompileShader(shader);
        int compiled = glGetShaderi(shader, GL_COMPILE_STATUS);
        String shaderLog = glGetShaderInfoLog(shader);
        if (shaderLog.trim().length() > 0) {
            System.err.println(shaderLog);
        }
        if (compiled == 0) {
            throw new AssertionError("Could not compile shader");
        }
        return shader;
    }

    private static int createProgram(int vshader, int fshader) {
        int program = glCreateProgram();
        glAttachShader(program, vshader);
        glAttachShader(program, fshader);
        glLinkProgram(program);
        int linked = glGetProgrami(program, GL_LINK_STATUS);
        String programLog = glGetProgramInfoLog(program);
        if (programLog.trim().length() > 0) {
            System.err.println(programLog);
        }
        if (linked == 0) {
            throw new AssertionError("Could not link program");
        }
        return program;
    }

    private void createCubemapProgram() throws IOException {
        int vshader = createShader("org/lwjgl/demo/game/cubemap.vs", GL_VERTEX_SHADER);
        int fshader = createShader("org/lwjgl/demo/game/cubemap.fs", GL_FRAGMENT_SHADER);
        int program = createProgram(vshader, fshader);
        glUseProgram(program);
        int texLocation = glGetUniformLocation(program, "tex");
        glUniform1i(texLocation, 0);
        cubemap_invViewProjUniform = glGetUniformLocation(program, "invViewProj");
        glUseProgram(0);
        cubemapProgram = program;
    }

    private void createShipProgram() throws IOException {
        int vshader = createShader("org/lwjgl/demo/game/ship.vs", GL_VERTEX_SHADER);
        int fshader = createShader("org/lwjgl/demo/game/ship.fs", GL_FRAGMENT_SHADER);
        int program = createProgram(vshader, fshader);
        glUseProgram(program);
        ship_viewUniform = glGetUniformLocation(program, "view");
        ship_projUniform = glGetUniformLocation(program, "proj");
        ship_modelUniform = glGetUniformLocation(program, "model");
        glUseProgram(0);
        shipProgram = program;
    }

    private void createParticleProgram() throws IOException {
        int vshader = createShader("org/lwjgl/demo/game/particle.vs", GL_VERTEX_SHADER);
        int fshader = createShader("org/lwjgl/demo/game/particle.fs", GL_FRAGMENT_SHADER);
        int program = createProgram(vshader, fshader);
        glUseProgram(program);
        particle_projUniform = glGetUniformLocation(program, "proj");
        glUseProgram(0);
        particleProgram = program;
    }

    private void createShotProgram() throws IOException {
        int vshader = createShader("org/lwjgl/demo/game/shot.vs", GL_VERTEX_SHADER);
        int fshader = createShader("org/lwjgl/demo/game/shot.fs", GL_FRAGMENT_SHADER);
        int program = createProgram(vshader, fshader);
        glUseProgram(program);
        shot_projUniform = glGetUniformLocation(program, "proj");
        glUseProgram(0);
        shotProgram = program;
    }

    private void createCubemapTexture() throws IOException {
        int tex = glGenTextures();
        glBindTexture(GL_TEXTURE_CUBE_MAP, tex);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        ByteBuffer imageBuffer;
        IntBuffer w = BufferUtils.createIntBuffer(1);
        IntBuffer h = BufferUtils.createIntBuffer(1);
        IntBuffer comp = BufferUtils.createIntBuffer(1);
        String[] names = { "right", "left", "top", "bottom", "front", "back" };
        ByteBuffer image;
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_GENERATE_MIPMAP, GL_TRUE);
        for (int i = 0; i < 6; i++) {
            imageBuffer = ioResourceToByteBuffer("org/lwjgl/demo/space_" + names[i] + (i + 1) + ".jpg", 8 * 1024);
            if (!stbi_info_from_memory(imageBuffer, w, h, comp))
                throw new IOException("Failed to read image information: " + stbi_failure_reason());
            image = stbi_load_from_memory(imageBuffer, w, h, comp, 0);
            if (image == null)
                throw new IOException("Failed to load image: " + stbi_failure_reason());
            glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB8, w.get(0), h.get(0), 0, GL_RGB,
                    GL_UNSIGNED_BYTE, image);
            stbi_image_free(image);
        }
        if (caps.OpenGL32 || caps.GL_ARB_seamless_cube_map) {
            glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
        }
    }

    private void update() {
        long thisTime = System.nanoTime();
        float dt = (thisTime - lastTime) / 1E9f;
        lastTime = thisTime;
        updateShots(dt);
        updateParticles(dt);
        cam.update(dt);

        projMatrix.setPerspective((float) Math.toRadians(40.0f), (float) width / height, 0.1f, 5000.0f);
        viewMatrix.set(cam.rotation).invert(invViewMatrix);
        viewProjMatrix.set(projMatrix).mul(viewMatrix).invert(invViewProjMatrix);
        frustumIntersection.set(viewProjMatrix);

        /* Update the background shader */
        glUseProgram(cubemapProgram);
        glUniformMatrix4fv(cubemap_invViewProjUniform, false, invViewProjMatrix.get(matrixBuffer));

        /* Update the ship shader */
        glUseProgram(shipProgram);
        glUniformMatrix4fv(ship_viewUniform, false, viewMatrix.get(matrixBuffer));
        glUniformMatrix4fv(ship_projUniform, false, projMatrix.get(matrixBuffer));

        /* Update the shot shader */
        glUseProgram(shotProgram);
        glUniformMatrix4fv(shot_projUniform, false, matrixBuffer);

        /* Update the particle shader */
        glUseProgram(particleProgram);
        glUniformMatrix4fv(particle_projUniform, false, matrixBuffer);

        updateControls();

        /* Let the player shoot a bullet */
        if (leftMouseDown && (thisTime - lastShotTime >= 1E6 * shotMilliseconds)) {
            shoot();
            lastShotTime = thisTime;
        }
        /* Let the opponent shoot a bullet */
        shootFromShip(thisTime, shootingShip);
    }

    private void updateControls() {
        cam.linearAcc.zero();
        float rotZ = 0.0f;
        if (keyDown[GLFW_KEY_W])
            cam.linearAcc.fma(mainThrusterAccFactor, cam.forward(tmp2));
        if (keyDown[GLFW_KEY_S])
            cam.linearAcc.fma(-mainThrusterAccFactor, cam.forward(tmp2));
        if (keyDown[GLFW_KEY_D])
            cam.linearAcc.fma(straveThrusterAccFactor, cam.right(tmp2));
        if (keyDown[GLFW_KEY_A])
            cam.linearAcc.fma(-straveThrusterAccFactor, cam.right(tmp2));
        if (keyDown[GLFW_KEY_Q])
            rotZ = -1.0f;
        if (keyDown[GLFW_KEY_E])
            rotZ = +1.0f;
        if (keyDown[GLFW_KEY_SPACE])
            cam.linearAcc.fma(straveThrusterAccFactor, cam.up(tmp2));
        if (keyDown[GLFW_KEY_LEFT_CONTROL])
            cam.linearAcc.fma(-straveThrusterAccFactor, cam.up(tmp2));
        if (rightMouseDown)
            cam.angularAcc.set(2.0f * mouseY * mouseY * mouseY, 2.0f * mouseX * mouseX * mouseX, rotZ);
        else if (!rightMouseDown)
            cam.angularAcc.set(0, 0, rotZ);
        double linearVelAbs = cam.linearVel.length();
        if (linearVelAbs > maxLinearVel)
            cam.linearVel.normalize().mul(maxLinearVel);
    }

    private static Vector3f intercept(Vector3d shotOrigin, float shotSpeed, Vector3d targetOrigin,
            Vector3f targetVel, Vector3f out) {
        float dirToTargetX = (float) (targetOrigin.x - shotOrigin.x);
        float dirToTargetY = (float) (targetOrigin.y - shotOrigin.y);
        float dirToTargetZ = (float) (targetOrigin.z - shotOrigin.z);
        float len = (float) Math
                .sqrt(dirToTargetX * dirToTargetX + dirToTargetY * dirToTargetY + dirToTargetZ * dirToTargetZ);
        dirToTargetX /= len;
        dirToTargetY /= len;
        dirToTargetZ /= len;
        float targetVelOrthDot = targetVel.x * dirToTargetX + targetVel.y * dirToTargetY
                + targetVel.z * dirToTargetZ;
        float targetVelOrthX = dirToTargetX * targetVelOrthDot;
        float targetVelOrthY = dirToTargetY * targetVelOrthDot;
        float targetVelOrthZ = dirToTargetZ * targetVelOrthDot;
        float targetVelTangX = targetVel.x - targetVelOrthX;
        float targetVelTangY = targetVel.y - targetVelOrthY;
        float targetVelTangZ = targetVel.z - targetVelOrthZ;
        float shotVelSpeed = (float) Math.sqrt(targetVelTangX * targetVelTangX + targetVelTangY * targetVelTangY
                + targetVelTangZ * targetVelTangZ);
        if (shotVelSpeed > shotSpeed) {
            return null;
        }
        float shotSpeedOrth = (float) Math.sqrt(shotSpeed * shotSpeed - shotVelSpeed * shotVelSpeed);
        float shotVelOrthX = dirToTargetX * shotSpeedOrth;
        float shotVelOrthY = dirToTargetY * shotSpeedOrth;
        float shotVelOrthZ = dirToTargetZ * shotSpeedOrth;
        return out.set(shotVelOrthX + targetVelTangX, shotVelOrthY + targetVelTangY, shotVelOrthZ + targetVelTangZ)
                .normalize();
    }

    private void shootFromShip(long thisTime, int index) {
        Ship ship = ships[index];
        if (ship == null)
            return;
        if (thisTime - ship.lastShotTime < 1E6 * shotOpponentMilliseconds) {
            return;
        }
        ship.lastShotTime = thisTime;
        Vector3d shotPos = tmp.set(ship.x, ship.y, ship.z).sub(cam.position).negate().normalize()
                .mul(1.01f * shipRadius).add(ship.x, ship.y, ship.z);
        Vector3f icept = intercept(shotPos, shotVelocity, cam.position, cam.linearVel, tmp2);
        if (icept == null)
            return;
        // jitter the direction a bit
        GeometryUtils.perpendicular(icept, tmp3, tmp4);
        icept.fma(((float) Math.random() * 2.0f - 1.0f) * 0.01f, tmp3);
        icept.fma(((float) Math.random() * 2.0f - 1.0f) * 0.01f, tmp4);
        icept.normalize();
        for (int i = 0; i < projectilePositions.length; i++) {
            Vector3d projectilePosition = projectilePositions[i];
            Vector4f projectileVelocity = projectileVelocities[i];
            if (projectileVelocity.w <= 0.0f) {
                projectilePosition.set(shotPos);
                projectileVelocity.x = tmp2.x * shotVelocity;
                projectileVelocity.y = tmp2.y * shotVelocity;
                projectileVelocity.z = tmp2.z * shotVelocity;
                projectileVelocity.w = 0.01f;
                break;
            }
        }
    }

    private void shoot() {
        boolean firstShot = false;
        for (int i = 0; i < projectilePositions.length; i++) {
            Vector3d projectilePosition = projectilePositions[i];
            Vector4f projectileVelocity = projectileVelocities[i];
            invViewProjMatrix.transformProject(tmp2.set(mouseX, -mouseY, 1.0f)).normalize();
            if (projectileVelocity.w <= 0.0f) {
                projectileVelocity.x = cam.linearVel.x + tmp2.x * shotVelocity;
                projectileVelocity.y = cam.linearVel.y + tmp2.y * shotVelocity;
                projectileVelocity.z = cam.linearVel.z + tmp2.z * shotVelocity;
                projectileVelocity.w = 0.01f;
                if (!firstShot) {
                    projectilePosition.set(cam.right(tmp3)).mul(shotSeparation).add(cam.position);
                    firstShot = true;
                } else {
                    projectilePosition.set(cam.right(tmp3)).mul(-shotSeparation).add(cam.position);
                    break;
                }
            }
        }
    }

    private void drawCubemap() {
        glUseProgram(cubemapProgram);
        glVertexPointer(2, GL_FLOAT, 0, quadVertices);
        glDrawArrays(GL_TRIANGLES, 0, 6);
    }

    private void drawShips() {
        glUseProgram(shipProgram);
        glBindBuffer(GL_ARRAY_BUFFER, shipPositionVbo);
        glVertexPointer(3, GL_FLOAT, 0, 0);
        glEnableClientState(GL_NORMAL_ARRAY);
        glBindBuffer(GL_ARRAY_BUFFER, shipNormalVbo);
        glNormalPointer(GL_FLOAT, 0, 0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        for (int i = 0; i < ships.length; i++) {
            Ship ship = ships[i];
            if (ship == null)
                continue;
            float x = (float) (ship.x - cam.position.x);
            float y = (float) (ship.y - cam.position.y);
            float z = (float) (ship.z - cam.position.z);
            if (frustumIntersection.testSphere(x, y, z, shipRadius)) {
                modelMatrix.translation(x, y, z);
                modelMatrix.scale(shipRadius);
                glUniformMatrix4fv(ship_modelUniform, false, modelMatrix.get(matrixBuffer));
                glDrawArrays(GL_TRIANGLES, 0, this.ship.numVertices);
            }
        }
        glDisableClientState(GL_NORMAL_ARRAY);
    }

    private void drawAsteroids() {
        glUseProgram(shipProgram);
        glBindBuffer(GL_ARRAY_BUFFER, asteroidPositionVbo);
        glVertexPointer(3, GL_FLOAT, 0, 0);
        glEnableClientState(GL_NORMAL_ARRAY);
        glBindBuffer(GL_ARRAY_BUFFER, asteroidNormalVbo);
        glNormalPointer(GL_FLOAT, 0, 0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        for (int i = 0; i < asteroids.length; i++) {
            Asteroid asteroid = asteroids[i];
            if (asteroid == null)
                continue;
            float x = (float) (asteroid.x - cam.position.x);
            float y = (float) (asteroid.y - cam.position.y);
            float z = (float) (asteroid.z - cam.position.z);
            if (frustumIntersection.testSphere(x, y, z, asteroid.scale)) {
                modelMatrix.translation(x, y, z);
                modelMatrix.scale(asteroid.scale);
                glUniformMatrix4fv(ship_modelUniform, false, modelMatrix.get(matrixBuffer));
                glDrawArrays(GL_TRIANGLES, 0, this.asteroid.numVertices);
            }
        }
        glDisableClientState(GL_NORMAL_ARRAY);
    }

    private void drawParticles() {
        particleVertices.clear();
        int num = 0;
        for (int i = 0; i < particlePositions.length; i++) {
            Vector3d particlePosition = particlePositions[i];
            Vector4d particleVelocity = particleVelocities[i];
            if (particleVelocity.w > 0.0f) {
                float x = (float) (particlePosition.x - cam.position.x);
                float y = (float) (particlePosition.y - cam.position.y);
                float z = (float) (particlePosition.z - cam.position.z);
                if (frustumIntersection.testPoint(x, y, z)) {
                    float w = (float) particleVelocity.w;
                    viewMatrix.transformPosition(tmp2.set(x, y, z));
                    particleVertices.put(tmp2.x - particleSize).put(tmp2.y - particleSize).put(tmp2.z).put(w)
                            .put(-1).put(-1);
                    particleVertices.put(tmp2.x + particleSize).put(tmp2.y - particleSize).put(tmp2.z).put(w).put(1)
                            .put(-1);
                    particleVertices.put(tmp2.x + particleSize).put(tmp2.y + particleSize).put(tmp2.z).put(w).put(1)
                            .put(1);
                    particleVertices.put(tmp2.x + particleSize).put(tmp2.y + particleSize).put(tmp2.z).put(w).put(1)
                            .put(1);
                    particleVertices.put(tmp2.x - particleSize).put(tmp2.y + particleSize).put(tmp2.z).put(w)
                            .put(-1).put(1);
                    particleVertices.put(tmp2.x - particleSize).put(tmp2.y - particleSize).put(tmp2.z).put(w)
                            .put(-1).put(-1);
                    num++;
                }
            }
        }
        particleVertices.flip();
        if (num > 0) {
            glUseProgram(particleProgram);
            glDepthMask(false);
            glEnable(GL_BLEND);
            glVertexPointer(4, GL_FLOAT, 6 * 4, particleVertices);
            particleVertices.position(4);
            glTexCoordPointer(2, GL_FLOAT, 6 * 4, particleVertices);
            particleVertices.position(0);
            glEnableClientState(GL_TEXTURE_COORD_ARRAY);
            glDrawArrays(GL_TRIANGLES, 0, num * 6);
            glDisableClientState(GL_TEXTURE_COORD_ARRAY);
            glDisable(GL_BLEND);
            glDepthMask(true);
        }
    }

    private void drawShots() {
        shotsVertices.clear();
        int num = 0;
        for (int i = 0; i < projectilePositions.length; i++) {
            Vector3d projectilePosition = projectilePositions[i];
            Vector4f projectileVelocity = projectileVelocities[i];
            if (projectileVelocity.w > 0.0f) {
                float x = (float) (projectilePosition.x - cam.position.x);
                float y = (float) (projectilePosition.y - cam.position.y);
                float z = (float) (projectilePosition.z - cam.position.z);
                if (frustumIntersection.testPoint(x, y, z)) {
                    float w = projectileVelocity.w;
                    viewMatrix.transformPosition(tmp2.set(x, y, z));
                    shotsVertices.put(tmp2.x - shotSize).put(tmp2.y - shotSize).put(tmp2.z).put(w).put(-1).put(-1);
                    shotsVertices.put(tmp2.x + shotSize).put(tmp2.y - shotSize).put(tmp2.z).put(w).put(1).put(-1);
                    shotsVertices.put(tmp2.x + shotSize).put(tmp2.y + shotSize).put(tmp2.z).put(w).put(1).put(1);
                    shotsVertices.put(tmp2.x + shotSize).put(tmp2.y + shotSize).put(tmp2.z).put(w).put(1).put(1);
                    shotsVertices.put(tmp2.x - shotSize).put(tmp2.y + shotSize).put(tmp2.z).put(w).put(-1).put(1);
                    shotsVertices.put(tmp2.x - shotSize).put(tmp2.y - shotSize).put(tmp2.z).put(w).put(-1).put(-1);
                    num++;
                }
            }
        }
        shotsVertices.flip();
        if (num > 0) {
            glUseProgram(shotProgram);
            glDepthMask(false);
            glEnable(GL_BLEND);
            glVertexPointer(4, GL_FLOAT, 6 * 4, shotsVertices);
            shotsVertices.position(4);
            glTexCoordPointer(2, GL_FLOAT, 6 * 4, shotsVertices);
            shotsVertices.position(0);
            glEnableClientState(GL_TEXTURE_COORD_ARRAY);
            glDrawArrays(GL_TRIANGLES, 0, num * 6);
            glDisableClientState(GL_TEXTURE_COORD_ARRAY);
            glDisable(GL_BLEND);
            glDepthMask(true);
        }
    }

    private void drawVelocityCompass() {
        glUseProgram(0);
        glEnable(GL_BLEND);
        glVertexPointer(3, GL_FLOAT, 0, sphere.positions);
        glEnableClientState(GL_NORMAL_ARRAY);
        glNormalPointer(GL_FLOAT, 0, sphere.normals);
        glMatrixMode(GL_PROJECTION);
        glPushMatrix();
        glLoadMatrixf(projMatrix.get(matrixBuffer));
        glMatrixMode(GL_MODELVIEW);
        glPushMatrix();
        glLoadIdentity();
        glTranslatef(0, -1, -4);
        glMultMatrixf(viewMatrix.get(matrixBuffer));
        glScalef(0.3f, 0.3f, 0.3f);
        glColor4f(0.1f, 0.1f, 0.1f, 0.2f);
        glDisable(GL_DEPTH_TEST);
        glDrawArrays(GL_TRIANGLES, 0, sphere.numVertices);
        glEnable(GL_DEPTH_TEST);
        glBegin(GL_LINES);
        glColor4f(1, 0, 0, 1);
        glVertex3f(0, 0, 0);
        glVertex3f(1, 0, 0);
        glColor4f(0, 1, 0, 1);
        glVertex3f(0, 0, 0);
        glVertex3f(0, 1, 0);
        glColor4f(0, 0, 1, 1);
        glVertex3f(0, 0, 0);
        glVertex3f(0, 0, 1);
        glColor4f(1, 1, 1, 1);
        glVertex3f(0, 0, 0);
        glVertex3f(cam.linearVel.x / maxLinearVel, cam.linearVel.y / maxLinearVel, cam.linearVel.z / maxLinearVel);
        glEnd();
        glPopMatrix();
        glMatrixMode(GL_PROJECTION);
        glPopMatrix();
        glMatrixMode(GL_MODELVIEW);
        glDisableClientState(GL_NORMAL_ARRAY);
        glDisable(GL_BLEND);
    }

    private void drawHudShotDirection() {
        glUseProgram(0);
        Ship enemyShip = ships[shootingShip];
        if (enemyShip == null)
            return;
        Vector3d targetOrigin = tmp;
        targetOrigin.set(enemyShip.x, enemyShip.y, enemyShip.z);
        Vector3f interceptorDir = intercept(cam.position, shotVelocity, targetOrigin,
                tmp3.set(cam.linearVel).negate(), tmp2);
        viewMatrix.transformDirection(interceptorDir);
        if (interceptorDir.z > 0.0)
            return;
        projMatrix.transformProject(interceptorDir);
        float crosshairSize = 0.01f;
        float xs = crosshairSize * height / width;
        float ys = crosshairSize;
        crosshairVertices.clear();
        crosshairVertices.put(interceptorDir.x - xs).put(interceptorDir.y - ys);
        crosshairVertices.put(interceptorDir.x + xs).put(interceptorDir.y - ys);
        crosshairVertices.put(interceptorDir.x + xs).put(interceptorDir.y + ys);
        crosshairVertices.put(interceptorDir.x - xs).put(interceptorDir.y + ys);
        crosshairVertices.flip();
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
        glVertexPointer(2, GL_FLOAT, 0, crosshairVertices);
        glDrawArrays(GL_QUADS, 0, 4);
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    }

    private void drawHudShip() {
        glUseProgram(0);
        Ship enemyShip = ships[shootingShip];
        if (enemyShip == null)
            return;
        Vector3f targetOrigin = tmp2;
        targetOrigin.set((float) (enemyShip.x - cam.position.x), (float) (enemyShip.y - cam.position.y),
                (float) (enemyShip.z - cam.position.z));
        tmp3.set(tmp2);
        viewMatrix.transformPosition(targetOrigin);
        boolean backward = targetOrigin.z > 0.0f;
        if (backward)
            return;
        projMatrix.transformProject(targetOrigin);
        if (targetOrigin.x < -1.0f)
            targetOrigin.x = -1.0f;
        if (targetOrigin.x > 1.0f)
            targetOrigin.x = 1.0f;
        if (targetOrigin.y < -1.0f)
            targetOrigin.y = -1.0f;
        if (targetOrigin.y > 1.0f)
            targetOrigin.y = 1.0f;
        float crosshairSize = 0.03f;
        float xs = crosshairSize * height / width;
        float ys = crosshairSize;
        crosshairVertices.clear();
        crosshairVertices.put(targetOrigin.x - xs).put(targetOrigin.y - ys);
        crosshairVertices.put(targetOrigin.x + xs).put(targetOrigin.y - ys);
        crosshairVertices.put(targetOrigin.x + xs).put(targetOrigin.y + ys);
        crosshairVertices.put(targetOrigin.x - xs).put(targetOrigin.y + ys);
        crosshairVertices.flip();
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
        glVertexPointer(2, GL_FLOAT, 0, crosshairVertices);
        glDrawArrays(GL_QUADS, 0, 4);
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
        // Draw distance text of enemy
        int quads = stb_easy_font_print(0, 0, Integer.toString((int) (tmp3.length())), null, charBuffer);
        glVertexPointer(2, GL_FLOAT, 16, charBuffer);
        glPushMatrix();
        // Scroll
        glTranslatef(targetOrigin.x, targetOrigin.y - crosshairSize * 1.1f, 0f);
        float aspect = (float) width / height;
        glScalef(1.0f / 500.0f, -1.0f / 500.0f * aspect, 0.0f);
        glDrawArrays(GL_QUADS, 0, quads * 4);
        glPopMatrix();
    }

    private boolean narrowphase(FloatBuffer data, double x, double y, double z, float scale, Vector3d pOld,
            Vector3d pNew, Vector3d intersectionPoint, Vector3f normal) {
        tmp2.set(tmp.set(pOld).sub(x, y, z)).div(scale);
        tmp3.set(tmp.set(pNew).sub(x, y, z)).div(scale);
        data.clear();
        boolean intersects = false;
        while (data.hasRemaining() && !intersects) {
            float v0X = data.get();
            float v0Y = data.get();
            float v0Z = data.get();
            float v1X = data.get();
            float v1Y = data.get();
            float v1Z = data.get();
            float v2X = data.get();
            float v2Y = data.get();
            float v2Z = data.get();
            if (Intersectionf.intersectLineSegmentTriangle(tmp2.x, tmp2.y, tmp2.z, tmp3.x, tmp3.y, tmp3.z, v0X, v0Y,
                    v0Z, v1X, v1Y, v1Z, v2X, v2Y, v2Z, 1E-6f, tmp2)) {
                intersectionPoint.x = tmp2.x * scale + x;
                intersectionPoint.y = tmp2.y * scale + y;
                intersectionPoint.z = tmp2.z * scale + z;
                GeometryUtils.normal(v0X, v0Y, v0Z, v1X, v1Y, v1Z, v2X, v2Y, v2Z, normal);
                intersects = true;
            }
        }
        data.clear();
        return intersects;
    }

    private static boolean broadphase(double x, double y, double z, float boundingRadius, float scale,
            Vector3d pOld, Vector3d pNew) {
        return Intersectiond.testLineSegmentSphere(pOld.x, pOld.y, pOld.z, pNew.x, pNew.y, pNew.z, x, y, z,
                boundingRadius * boundingRadius * scale * scale);
    }

    private void updateParticles(float dt) {
        for (int i = 0; i < particlePositions.length; i++) {
            Vector4d particleVelocity = particleVelocities[i];
            if (particleVelocity.w <= 0.0f)
                continue;
            particleVelocity.w += dt;
            Vector3d particlePosition = particlePositions[i];
            newPosition.set(particleVelocity.x, particleVelocity.y, particleVelocity.z).mul(dt)
                    .add(particlePosition);
            if (particleVelocity.w > maxParticleLifetime) {
                particleVelocity.w = 0.0f;
                continue;
            }
            particlePosition.set(newPosition);
        }
    }

    private void updateShots(float dt) {
        projectiles: for (int i = 0; i < projectilePositions.length; i++) {
            Vector4f projectileVelocity = projectileVelocities[i];
            if (projectileVelocity.w <= 0.0f)
                continue;
            projectileVelocity.w += dt;
            Vector3d projectilePosition = projectilePositions[i];
            newPosition.set(projectileVelocity.x, projectileVelocity.y, projectileVelocity.z).mul(dt)
                    .add(projectilePosition);
            if (projectileVelocity.w > maxShotLifetime) {
                projectileVelocity.w = 0.0f;
                continue;
            }
            /* Test against ships */
            for (int r = 0; r < shipCount; r++) {
                Ship ship = ships[r];
                if (ship == null)
                    continue;
                if (broadphase(ship.x, ship.y, ship.z, this.ship.boundingSphereRadius, shipRadius,
                        projectilePosition, newPosition)
                        && narrowphase(this.ship.positions, ship.x, ship.y, ship.z, shipRadius, projectilePosition,
                                newPosition, tmp, tmp2)) {
                    emitExplosion(tmp, null);
                    ships[r] = null;
                    projectileVelocity.w = 0.0f;
                    if (r == shootingShip) {
                        for (int sr = 0; sr < shipCount; sr++) {
                            if (ships[sr] != null) {
                                shootingShip = sr;
                                break;
                            }
                        }
                    }
                    continue projectiles;
                }
            }
            /* Test against asteroids */
            for (int r = 0; r < asteroidCount; r++) {
                Asteroid asteroid = asteroids[r];
                if (asteroid == null)
                    continue;
                if (broadphase(asteroid.x, asteroid.y, asteroid.z, this.asteroid.boundingSphereRadius,
                        asteroid.scale, projectilePosition, newPosition)
                        && narrowphase(this.asteroid.positions, asteroid.x, asteroid.y, asteroid.z, asteroid.scale,
                                projectilePosition, newPosition, tmp, tmp2)) {
                    emitExplosion(tmp, tmp2);
                    projectileVelocity.w = 0.0f;
                    continue projectiles;
                }
            }
            projectilePosition.set(newPosition);
        }
    }

    private void emitExplosion(Vector3d p, Vector3f normal) {
        int c = explosionParticles;
        if (normal != null)
            GeometryUtils.perpendicular(normal, tmp4, tmp3);
        for (int i = 0; i < particlePositions.length; i++) {
            Vector3d particlePosition = particlePositions[i];
            Vector4d particleVelocity = particleVelocities[i];
            if (particleVelocity.w <= 0.0f) {
                if (normal != null) {
                    float r1 = (float) Math.random() * 2.0f - 1.0f;
                    float r2 = (float) Math.random() * 2.0f - 1.0f;
                    particleVelocity.x = normal.x + r1 * tmp4.x + r2 * tmp3.x;
                    particleVelocity.y = normal.y + r1 * tmp4.y + r2 * tmp3.y;
                    particleVelocity.z = normal.z + r1 * tmp4.z + r2 * tmp3.z;
                } else {
                    float x = (float) Math.random() * 2.0f - 1.0f;
                    float y = (float) Math.random() * 2.0f - 1.0f;
                    float z = (float) Math.random() * 2.0f - 1.0f;
                    particleVelocity.x = x;
                    particleVelocity.y = y;
                    particleVelocity.z = z;
                }
                particleVelocity.normalize3();
                particleVelocity.mul(140);
                particleVelocity.w = 0.01f;
                particlePosition.set(p);
                if (c-- == 0)
                    break;
            }
        }
    }

    private void render() {
        glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
        drawShips();
        drawAsteroids();
        drawCubemap();
        drawShots();
        drawParticles();
        drawHudShotDirection();
        drawHudShip();
        drawVelocityCompass();
    }

    private void loop() {
        while (!glfwWindowShouldClose(window)) {
            glfwPollEvents();
            glViewport(0, 0, width, height);
            update();
            render();
            glfwSwapBuffers(window);
        }
    }

    private void run() {
        try {
            init();
            loop();

            if (debugProc != null)
                debugProc.free();

            keyCallback.free();
            cpCallback.free();
            mbCallback.free();
            fbCallback.free();
            glfwDestroyWindow(window);
        } catch (Throwable t) {
            t.printStackTrace();
        } finally {
            glfwTerminate();
        }
    }

    public static void main(String[] args) {
        new SpaceGame().run();
    }

}