com.cyphercove.doublehelix.Particle.java Source code

Java tutorial

Introduction

Here is the source code for com.cyphercove.doublehelix.Particle.java

Source

/*******************************************************************************
 * Copyright 2015 Cypher Cove, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.cyphercove.doublehelix;

import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector3;
import com.cyphercove.doublehelix.points.BillboardDecal;

import java.util.Random;

/**
 * Created by Darren on 9/7/2015.
 */
public class Particle {
    public final Vector3 center = new Vector3();
    public final Vector3 originalCenter = new Vector3();
    public float phase;
    public float frequency;
    public float age;
    private float ageFade;
    public float angle2d;
    final Vector3 baseVelocity = new Vector3();
    final Vector3 velocity = new Vector3();

    final Vector3 touchRotationVelocity = new Vector3();
    final Vector3 touchRotationAxis = new Vector3();
    float touchMagnitude; //if magnitude of new point is bigger, replace these
    float touchAge;

    public final DecalPlus decal;
    public final BillboardDecal point;

    private static final Random RAND = new Random();
    private static final Vector3 TMPV = new Vector3();
    private static final Vector3 TMPV2 = new Vector3();

    private static final float PI4 = MathUtils.PI2 * 2f;

    private static final float BASE_SIZE = 0.015f;
    private static final float BASE_SIZE_SQUARED = BASE_SIZE * BASE_SIZE;
    private static final float POINT_BASE_SIZE = 0.004f;
    private static final float POINT_BASE_SIZE_SQUARED = POINT_BASE_SIZE * POINT_BASE_SIZE;
    private static final float POINT_BASE_SIZE_NO_DOF = 0.004f;
    private static final float BASE_BRIGHTNESS = 1.0f;
    private static final float POINT_BASE_BRIGHTNESS = 2.0f;
    private static final float SPECULAR_POWER = 3f;
    private static final float SPECULAR_SCALE = 30f;
    private static final float SPECULAR_PHASE_SHIFT = MathUtils.PI / 2f;

    private static final float RADIAL_ACCELERATION_MAX = 0.05f;
    private static final float RADIAL_ACCELERATION_EFFECT_EXTENT = 1.3f;
    private static final float RADIAL_ACCELERATION_MAX_CENTER = 1.5f; //radius of the helix
    private static final float DEGREES_ACCLERATION_PER_DIST = 180f;
    private static final float DAMPENING = 6f;

    private static final float LIFE_TIME = 8f;
    private static final float LIFE_FADE_TIME = 0.5f;
    private static final float LIFE_FADE_OUT_TIME = LIFE_TIME - LIFE_FADE_TIME;

    private static final float TOUCH_INFLUENCE = 1F;
    private static final float TOUCH_SPEED_RATIO = 0.15F;
    private static final float TOUCH_DAMPEN_TIME = 1.5F;
    private static final float TOUCH_CHECK_DISTANCE = Particles.TOUCH_DISTANCE + TOUCH_INFLUENCE;

    private static class Power {
        static final int COUNT = 16000;

        final static float[] table = new float[COUNT];

        static {
            float indexToDistance = 1f / COUNT;
            for (int i = 0; i < COUNT; i++) {
                table[i] = (float) Math.pow((i + 0.5f) * indexToDistance, SPECULAR_POWER);
            }
        }
    }

    public Particle(TextureRegion textureRegion, Texture pointTexture) {
        super();
        decal = new DecalPlus();
        decal.setTextureRegion(textureRegion);
        decal.setBlending(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);

        point = new BillboardDecal();
        point.setTexture(pointTexture);
        point.setBlending(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);
    }

    public void reset() {
        center.set(originalCenter);
        age = RAND.nextFloat() * LIFE_TIME;
        phase = RAND.nextFloat() * MathUtils.PI2 * 2f; //need to go up to 4pi because of doubled parameter for the every-other-wave specular cut
        frequency = 3f; //TODO
        angle2d = 360f * RAND.nextFloat();
        touchMagnitude = 0;
    }

    private void updateCommon(float delta, boolean haveTouchVelocity, Vector3 touchPosition, Vector3 touchVelocity,
            float cameraDistance) {
        age += delta;
        if (age < LIFE_FADE_TIME)
            ageFade = Interpolation.fade.apply(age / LIFE_FADE_TIME);
        else if (age > LIFE_FADE_OUT_TIME)
            ageFade = Interpolation.fade.apply((LIFE_TIME - age) / LIFE_FADE_TIME);
        else
            ageFade = 1;

        TMPV.set(center.x, center.y, 0);
        float distFromAccelerationCenter = TMPV.len() - RADIAL_ACCELERATION_MAX_CENTER;
        float radialAcceleration = RADIAL_ACCELERATION_MAX * (1F - Math.min(1f, Interpolation.fade
                .apply(Math.abs(distFromAccelerationCenter) / RADIAL_ACCELERATION_EFFECT_EXTENT)));
        TMPV.nor().rotate(Vector3.Z, 90f - (distFromAccelerationCenter * DEGREES_ACCLERATION_PER_DIST)); //motion direction
        TMPV.scl(radialAcceleration); //acceleration
        baseVelocity.add(TMPV.scl(delta)); //apply acceleration

        float dampenAmount = DAMPENING * baseVelocity.len() * delta;
        if (dampenAmount >= 1f)
            baseVelocity.set(0, 0, 0);
        else
            baseVelocity.add(TMPV.set(baseVelocity).scl(-dampenAmount));

        velocity.set(baseVelocity);

        if (haveTouchVelocity && cameraDistance <= TOUCH_CHECK_DISTANCE) {
            float dst = TMPV.set(center).sub(touchPosition).len();
            if (dst < TOUCH_INFLUENCE) {
                float mag = 1f - Math.min(1f, dst / TOUCH_INFLUENCE);
                if (mag > touchMagnitude) {
                    touchMagnitude = mag;
                    //float radialDst = distanceToAxis(center, touchPosition, touchVelocity);
                    touchAge = 0;

                    touchRotationVelocity.set(touchVelocity).scl(TOUCH_SPEED_RATIO * mag);
                    touchRotationAxis.set(TMPV).crs(touchVelocity).nor();
                }
            }
        }

        if (touchMagnitude > 0) {
            touchAge += delta;

            if (touchAge > TOUCH_DAMPEN_TIME) {
                touchMagnitude = 0;
                TMPV.set(touchRotationVelocity).scl(0.5f);
                TMPV.rotate(touchRotationAxis, -45);
                baseVelocity.add(TMPV);
                velocity.set(baseVelocity);
            } else {
                float lerp = touchAge / TOUCH_DAMPEN_TIME;
                TMPV.set(touchRotationVelocity).scl(1f - 0.5f * Interpolation.pow2Out.apply(lerp));
                TMPV.rotate(touchRotationAxis, -Interpolation.pow3In.apply(lerp) * 45);
                velocity.add(TMPV);
            }
        }

        if (age >= LIFE_TIME) {
            center.set(originalCenter);
            age = 0;
        } else {
            center.add(TMPV.set(velocity).scl(delta));
        }
    }

    float distanceToAxis(Vector3 point, Vector3 pointOnAxis, Vector3 axisDirection) {
        TMPV.set(point).sub(pointOnAxis);
        float c1 = TMPV.dot(axisDirection);
        float c2 = axisDirection.dot(axisDirection);
        TMPV.set(axisDirection).scl(c1 / c2).add(pointOnAxis);
        return TMPV.dst(point);
    }

    public void updateFlake(float delta, DepthOfFieldCamera camera, Vector3 projectedLight,
            boolean haveTouchVelocity, Vector3 touchPosition, Vector3 touchVelocity) {
        float cameraDistance = center.dst(camera.position);
        decal.cameraDistance = cameraDistance;
        updateCommon(delta, haveTouchVelocity, touchPosition, touchVelocity, cameraDistance);

        float circleOfConfusion = camera.calculateUnprojectedCircleOfConfusion(cameraDistance);
        float size = BASE_SIZE + circleOfConfusion;
        float focus = 1f - Math.min(1f, circleOfConfusion * 6.0f); // Focus is from 0 to 1. 0 is full bokeh. Last parameter experimentally determined (it's a factor of CoC).
        focus = focus * focus * focus;
        float unfocus = 1f - focus;

        //Fake rotation by squeezing vertically. Reduce amount of squeeze by unfocus to keep bokeh
        //blur. At full unfocus, there is no squeezing.
        float squeeze = 1f - focus * (0.5f + 0.5f * MathUtils.sin(frequency * age + phase));
        float squeezedBaseSize = BASE_SIZE * squeeze;
        decal.setDimensions(size, size * squeeze);

        camera.project(TMPV.set(center)); //screen position of cell
        TMPV.sub(projectedLight).nor(); //screen space light-to-cell vector
        TMPV2.set(0, 1f, 0).rotate(angle2d, 0, 0, -1f); //sprite angle in 2D (-1 z matches camera.direction used to rotate decal at end of this method)
        float specularAlignment = Math.abs(TMPV.x * TMPV2.x + TMPV.y * TMPV2.y); //dot product of rotation direction and vector to light
        specularAlignment = Power.table[(int) Math.min(Power.COUNT - 1, specularAlignment * Power.COUNT)];//(float)Math.pow(specularAlignment, SPECULAR_POWER);

        float specularSineParameter = frequency * age + phase + SPECULAR_PHASE_SHIFT;
        float specular = (specularSineParameter - 1.5f * MathUtils.PI) % PI4 < MathUtils.PI2
                ? specularAlignment * Power.table[(int) Math.min(Power.COUNT - 1,
                        Math.max(0, 0.5f + 0.5f * MathUtils.sin(specularSineParameter)) * Power.COUNT)]
                : 0; //Ternary expression makes it every other wave
        float totalBrightness = BASE_BRIGHTNESS + specular * SPECULAR_SCALE;

        //alpha is only used as the decal goes unfocused. When fully unfocused, the brightness is
        //evenly spread across the larger area of the sprite. Brightness can be more than 1 before
        //clamping because of HDR specular.
        float alpha = ageFade * Math.min(1f, //Clamp color down to one
                Math.max(1, totalBrightness) * //HDR brightness
                        Interpolation.linear.apply(BASE_SIZE_SQUARED, (squeezedBaseSize * squeezedBaseSize),
                                unfocus)
                        / (size * size)); //size ratio of focused to unfocused sprite.
        // ^^Squeezed base size is used for unfocused sprites because the proper base size is dependent on squeeze

        decal.setColor(Math.min(1f, specular), 0f, unfocus, alpha); //whiteness, unused, unfocus, alpha.
        decal.setPosition(center);
        decal.lookAt(camera.position, TMPV.set(camera.up).rotate(camera.direction, angle2d));
    }

    public void updateFlakeNoDOF(float delta, DepthOfFieldCamera camera, Vector3 projectedLight,
            boolean haveTouchVelocity, Vector3 touchPosition, Vector3 touchVelocity) {
        float cameraDistance = center.dst(camera.position);
        decal.cameraDistance = cameraDistance;
        updateCommon(delta, haveTouchVelocity, touchPosition, touchVelocity, cameraDistance);

        //Fake rotation by squeezing vertically. Reduce amount of squeeze by unfocus to keep bokeh
        //blur. At full unfocus, there is no squeezing.
        float squeeze = 1f - (0.5f + 0.5f * MathUtils.sin(frequency * age + phase));
        decal.setDimensions(BASE_SIZE, BASE_SIZE * squeeze);

        camera.project(TMPV.set(center)); //screen position of cell
        TMPV.sub(projectedLight).nor(); //screen space light-to-cell vector
        TMPV2.set(0, 1f, 0).rotate(angle2d, 0, 0, -1f); //sprite angle in 2D (-1 z matches camera.direction used to rotate decal at end of this method)
        float specularAlignment = Math.abs(TMPV.x * TMPV2.x + TMPV.y * TMPV2.y); //dot product of rotation direction and vector to light
        specularAlignment = Power.table[(int) Math.min(Power.COUNT - 1, specularAlignment * Power.COUNT)];//(float)Math.pow(specularAlignment, SPECULAR_POWER);

        float specularSineParameter = frequency * age + phase + SPECULAR_PHASE_SHIFT;
        float specular = (specularSineParameter - 1.5f * MathUtils.PI) % PI4 < MathUtils.PI2
                ? specularAlignment * Power.table[(int) Math.min(Power.COUNT - 1,
                        Math.max(0, 0.5f + 0.5f * MathUtils.sin(specularSineParameter)) * Power.COUNT)]
                : 0; //Ternary expression makes it every other wave
        //float totalBrightness = BASE_BRIGHTNESS + specular * SPECULAR_SCALE;

        decal.setColor(Math.min(1f, specular), 0f, 1f, ageFade); //whiteness, unused, unfocus, alpha.
        decal.setPosition(center);
        decal.lookAt(camera.position, TMPV.set(camera.up).rotate(camera.direction, angle2d));
    }

    public void updateBillboard(float delta, DepthOfFieldCamera camera, Vector3 projectedLight,
            boolean haveTouchVelocity, Vector3 touchPosition, Vector3 touchVelocity, float sizeFactor) {
        float cameraDistance = center.dst(camera.position);
        point.floatValue = cameraDistance;
        updateCommon(delta, haveTouchVelocity, touchPosition, touchVelocity, cameraDistance);

        float circleOfConfusion = 0.5f * camera.calculateUnprojectedCircleOfConfusion(cameraDistance);
        float size = POINT_BASE_SIZE + circleOfConfusion;
        float focus = 1f - Math.min(1f, circleOfConfusion * 6.0f); // Focus is from 0 to 1. 0 is full bokeh. Last parameter experimentally determined (it's a factor of CoC).
        focus = focus * focus * focus;
        float unfocus = 1f - focus;

        //not actually squeezing, but this is used for alpha
        float squeeze = 1f - (0.25f + 0.25f * MathUtils.sin(frequency * age + phase)); //note squeeze limited to half as much as with flakes
        float squeezedBaseSize = POINT_BASE_SIZE * squeeze;
        point.setSize(size * sizeFactor / cameraDistance); //size projected to screen

        camera.project(TMPV.set(center)); //screen position of cell
        TMPV.sub(projectedLight).nor(); //screen space light-to-cell vector
        TMPV2.set(0, 1f, 0).rotate(angle2d, 0, 0, -1f); //sprite angle in 2D (-1 z matches camera.direction used to rotate decal at end of this method)
        float specularAlignment = Math.abs(TMPV.x * TMPV2.x + TMPV.y * TMPV2.y); //dot product of rotation direction and vector to light
        specularAlignment = Power.table[(int) Math.min(Power.COUNT - 1, specularAlignment * Power.COUNT)];//(float)Math.pow(specularAlignment, SPECULAR_POWER);

        float specularSineParameter = frequency * age + phase + SPECULAR_PHASE_SHIFT;
        float specular = (specularSineParameter - 1.5f * MathUtils.PI) % PI4 < MathUtils.PI2
                ? specularAlignment * Power.table[(int) Math.min(Power.COUNT - 1,
                        Math.max(0, 0.5f + 0.5f * MathUtils.sin(specularSineParameter)) * Power.COUNT)]
                : 0; //Ternary expression makes it every other wave
        float totalBrightness = POINT_BASE_BRIGHTNESS + specular * SPECULAR_SCALE;

        //alpha is only used as the decal goes unfocused. When fully unfocused, the brightness is
        //evenly spread across the larger area of the sprite. Brightness can be more than 1 before
        //clamping because of HDR specular.
        //TODO this should be calculated from unprojected sizes, not projected sizes.
        float alpha = ageFade * Math.min(1f, //Clamp color down to one
                Math.max(1, totalBrightness) * //HDR brightness
                        squeeze * //simulate rotating spec
                        Interpolation.linear.apply(POINT_BASE_SIZE_SQUARED, (squeezedBaseSize * squeezedBaseSize),
                                unfocus)
                        / (size * size)); //size ratio of focused to unfocused sprite.

        point.setColor(Math.min(1f, specular), 0f, unfocus, alpha); //whiteness, unused, unfocus, alpha.
        point.setPosition(center);
        point.setAngle(angle2d);
    }

    public void updateBillboardNoDOF(float delta, DepthOfFieldCamera camera, Vector3 projectedLight,
            boolean haveTouchVelocity, Vector3 touchPosition, Vector3 touchVelocity, float sizeFactor) {
        float cameraDistance = center.dst(camera.position);
        point.floatValue = cameraDistance;
        updateCommon(delta, haveTouchVelocity, touchPosition, touchVelocity, cameraDistance);

        //not actually squeezing, but this is used for alpha
        float squeeze = 1f - (0.25f + 0.25f * MathUtils.sin(frequency * age + phase)); //note squeeze limited to half as much as with flakes
        point.setSize(POINT_BASE_SIZE_NO_DOF * sizeFactor / cameraDistance); //size projected to screen

        camera.project(TMPV.set(center)); //screen position of cell
        TMPV.sub(projectedLight).nor(); //screen space light-to-cell vector
        TMPV2.set(0, 1f, 0).rotate(angle2d, 0, 0, -1f); //sprite angle in 2D (-1 z matches camera.direction used to rotate decal at end of this method)
        float specularAlignment = Math.abs(TMPV.x * TMPV2.x + TMPV.y * TMPV2.y); //dot product of rotation direction and vector to light
        specularAlignment = Power.table[(int) Math.min(Power.COUNT - 1, specularAlignment * Power.COUNT)];

        float specularSineParameter = frequency * age + phase + SPECULAR_PHASE_SHIFT;
        float specular = (specularSineParameter - 1.5f * MathUtils.PI) % PI4 < MathUtils.PI2
                ? specularAlignment * Power.table[(int) Math.min(Power.COUNT - 1,
                        Math.max(0, 0.5f + 0.5f * MathUtils.sin(specularSineParameter)) * Power.COUNT)]
                : 0; //Ternary expression makes it every other wave

        point.setColor(Math.min(1f, specular), 0f, 1f, ageFade * squeeze); //whiteness, unused, unfocus, alpha.
        point.setPosition(center);
        point.setAngle(angle2d);
    }
}