thothbot.parallax.core.client.renderers.RaytracingRenderer.java Source code

Java tutorial

Introduction

Here is the source code for thothbot.parallax.core.client.renderers.RaytracingRenderer.java

Source

/*
 * Copyright 2012 Alex Usachev, thothbot@gmail.com
 * 
 * This file is part of Parallax project.
 * 
 * Parallax is free software: you can redistribute it and/or modify it 
 * under the terms of the Creative Commons Attribution 3.0 Unported License.
 * 
 * Parallax is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE. See the Creative Commons Attribution 
 * 3.0 Unported License. for more details.
 * 
 * You should have received a copy of the the Creative Commons Attribution 
 * 3.0 Unported License along with Parallax. 
 * If not, see http://creativecommons.org/licenses/by/3.0/.
 */

package thothbot.parallax.core.client.renderers;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.gwt.animation.client.AnimationScheduler;
import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle;
import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.canvas.dom.client.CanvasPixelArray;
import com.google.gwt.canvas.dom.client.Context2d;
import com.google.gwt.canvas.dom.client.ImageData;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.ui.RootPanel;

import thothbot.parallax.core.shared.Log;
import thothbot.parallax.core.shared.cameras.Camera;
import thothbot.parallax.core.shared.cameras.PerspectiveCamera;
import thothbot.parallax.core.shared.core.Face3;
import thothbot.parallax.core.shared.core.FastMap;
import thothbot.parallax.core.shared.core.Geometry;
import thothbot.parallax.core.shared.core.GeometryObject;
import thothbot.parallax.core.shared.core.Object3D;
import thothbot.parallax.core.shared.core.Object3D.Traverse;
import thothbot.parallax.core.shared.core.Raycaster;
import thothbot.parallax.core.shared.core.Raycaster.Intersect;
import thothbot.parallax.core.shared.helpers.HasRaytracingPhysicalAttenuation;
import thothbot.parallax.core.shared.lights.HasIntensity;
import thothbot.parallax.core.shared.lights.Light;
import thothbot.parallax.core.shared.materials.HasColor;
import thothbot.parallax.core.shared.materials.HasRaytracingGlass;
import thothbot.parallax.core.shared.materials.HasRaytracingMirror;
import thothbot.parallax.core.shared.materials.HasShading;
import thothbot.parallax.core.shared.materials.HasVertexColors;
import thothbot.parallax.core.shared.materials.Material;
import thothbot.parallax.core.shared.materials.MeshBasicMaterial;
import thothbot.parallax.core.shared.materials.MeshLambertMaterial;
import thothbot.parallax.core.shared.materials.MeshPhongMaterial;
import thothbot.parallax.core.shared.math.Color;
import thothbot.parallax.core.shared.math.Mathematics;
import thothbot.parallax.core.shared.math.Matrix3;
import thothbot.parallax.core.shared.math.Matrix4;
import thothbot.parallax.core.shared.math.Ray;
import thothbot.parallax.core.shared.math.Vector3;
import thothbot.parallax.core.shared.scenes.Scene;

public class RaytracingRenderer extends AbstractRenderer {

    private static final int maxRecursionDepth = 3;
    private static final int blockSize = 64;

    private static class ObjectMatrixes {
        public Matrix3 normalMatrix;
        public Matrix4 inverseMatrix;

        public ObjectMatrixes() {
            this.normalMatrix = new Matrix3();
            this.inverseMatrix = new Matrix4();
        }
    }

    Canvas canvas;
    Context2d context;

    Vector3 origin = new Vector3();
    Vector3 direction = new Vector3();

    Vector3 cameraPosition = new Vector3();

    Raycaster raycaster = new Raycaster(origin, direction);
    Raycaster raycasterLight = new Raycaster();

    double perspective;
    Matrix4 modelViewMatrix = new Matrix4();
    Matrix3 cameraNormalMatrix = new Matrix3();

    List<Object3D> objects;
    List<Light> lights = new ArrayList<Light>();

    AnimationHandle animationHandler;

    Map<String, ObjectMatrixes> cache = GWT.isScript() ? new FastMap<ObjectMatrixes>()
            : new HashMap<String, ObjectMatrixes>();

    Canvas canvasBlock;
    ImageData imagedata = null;

    public RaytracingRenderer(int width, int height) {
        canvas = Canvas.createIfSupported();
        canvas.ensureDebugId("canvas2d");

        setSize(width, height);

        context = canvas.getContext2d();
        context.setFillStyle("#FFFFFF");

        canvasBlock = Canvas.createIfSupported();
        canvasBlock.setCoordinateSpaceWidth(blockSize);
        canvasBlock.setCoordinateSpaceHeight(blockSize);

        RootPanel.get().add(canvasBlock, -10000, 0);

        Context2d contextBlock = canvasBlock.getContext2d();
        imagedata = contextBlock.getImageData(0, 0, blockSize, blockSize);
    }

    public Canvas getCanvas() {
        return this.canvas;
    }

    @Override
    public void setSize(int width, int height) {
        super.setSize(width, height);

        canvas.setCoordinateSpaceWidth(width);
        canvas.setCoordinateSpaceHeight(height);
    }

    @Override
    public void setClearColor(Color color, double alpha) {
        this.clearColor.copy(color);
    }

    @Override
    public void clear() {
        // TODO Auto-generated method stub

    }

    @Override
    public void render(Scene scene, final Camera camera) {

        if (isAutoClear() == true)
            this.clear();

        //      cancelAnimationFrame( animationFrameId );

        // update scene graph

        if (scene.isAutoUpdate() == true)
            scene.updateMatrixWorld(false);

        // update camera matrices

        if (camera.getParent() == null)
            camera.updateMatrixWorld(false);

        camera.getMatrixWorldInverse().getInverse(camera.getMatrixWorld());
        cameraPosition.setFromMatrixPosition(camera.getMatrixWorld());

        //

        cameraNormalMatrix.getNormalMatrix(camera.getMatrixWorld());
        origin.copy(cameraPosition);

        perspective = 0.5 / Math.tan(Mathematics.degToRad(((PerspectiveCamera) camera).getFov() * 0.5))
                * getAbsoluteHeight();

        objects = scene.getChildren();

        // collect lights and set up object matrices

        lights = new ArrayList<Light>();

        scene.traverse(new Traverse() {

            @Override
            public void callback(Object3D object) {
                if (object instanceof Light) {

                    lights.add((Light) object);

                }

                if (!cache.containsKey(object.getId() + "")) {
                    cache.put(object.getId() + "", new ObjectMatrixes());
                }

                modelViewMatrix.multiply(camera.getMatrixWorldInverse(), object.getMatrixWorld());

                ObjectMatrixes _object = cache.get(object.getId() + "");

                _object.normalMatrix.getNormalMatrix(modelViewMatrix);
                _object.inverseMatrix.getInverse(object.getMatrixWorld());

            }
        });

        renderBlock(0, 0);
    }

    private void renderBlock(int blockX, int blockY) {
        Log.debug("Raytracing -- Render block: " + blockX + ", " + blockY);
        Color pixelColor = new Color();

        //

        int index = 0;

        for (int y = 0; y < blockSize; y++) {

            for (int x = 0; x < blockSize; x++, index += 4) {

                // spawn primary ray at pixel position

                origin.copy(cameraPosition);

                direction.set(x + blockX - getAbsoluteWidth() / 2, -(y + blockY - getAbsoluteHeight() / 2),
                        -perspective);
                direction.apply(cameraNormalMatrix).normalize();

                spawnRay(origin, direction, pixelColor, 0);

                // convert from linear to gamma

                imagedata.getData().set(index, (int) (Math.sqrt(pixelColor.getR()) * 255));
                imagedata.getData().set(index + 1, (int) (Math.sqrt(pixelColor.getG()) * 255));
                imagedata.getData().set(index + 2, (int) (Math.sqrt(pixelColor.getB()) * 255));

                imagedata.getData().set(index + 3, 255); // alpha
            }

        }

        context.putImageData(imagedata, blockX, blockY);

        blockX += blockSize;

        if (blockX >= getAbsoluteWidth()) {

            blockX = 0;
            blockY += blockSize;

            if (blockY >= getAbsoluteHeight())
                return;

        }

        context.fillRect(blockX, blockY, blockSize, blockSize);

        final int _blockX = blockX;
        final int _blockY = blockY;

        animationHandler = AnimationScheduler.get().requestAnimationFrame(new AnimationCallback() {

            @Override
            public void execute(double timestamp) {

                renderBlock(_blockX, _blockY);

            }
        });

    }

    Color diffuseColor = new Color();
    Color specularColor = new Color();
    Color lightColor = new Color();
    Color schlick = new Color();

    Color lightContribution = new Color();

    Vector3 eyeVector = new Vector3();
    Vector3 lightVector = new Vector3();
    Vector3 normalVector = new Vector3();
    Vector3 halfVector = new Vector3();

    Vector3 localPoint = new Vector3();
    Vector3 reflectionVector = new Vector3();

    Vector3 tmpVec = new Vector3();

    Color[] tmpColor = null;

    private void spawnRay(Vector3 rayOrigin, Vector3 rayDirection, Color outputColor, int recursionDepth) {
        // Init the tmp array
        if (tmpColor == null) {
            tmpColor = new Color[maxRecursionDepth];
            for (int i = 0; i < maxRecursionDepth; i++)
                tmpColor[i] = new Color();
        }

        Ray ray = raycaster.getRay();

        ray.setOrigin(rayOrigin);
        ray.setDirection(rayDirection);

        //

        Ray rayLight = raycasterLight.getRay();

        //

        outputColor.setRGB(0, 0, 0);

        //

        List<Intersect> intersections = raycaster.intersectObjects(objects, true);

        // ray didn't find anything
        // (here should come setting of background color?)

        if (intersections.size() == 0) {

            return;

        }

        // ray hit

        Intersect intersection = intersections.get(0);

        Vector3 point = intersection.point;
        GeometryObject object = intersection.object;
        Material material = object.getMaterial();
        Face3 face = intersection.face;

        List<Vector3> vertices = ((Geometry) object.getGeometry()).getVertices();

        //

        ObjectMatrixes _object = cache.get(object.getId() + "");

        localPoint.copy(point).apply(_object.inverseMatrix);
        eyeVector.sub(raycaster.getRay().getOrigin(), point).normalize();

        // resolve pixel diffuse color

        if (material instanceof MeshLambertMaterial || material instanceof MeshPhongMaterial
                || material instanceof MeshBasicMaterial) {
            diffuseColor.copyGammaToLinear(((HasColor) material).getColor());
        } else {
            diffuseColor.setRGB(1, 1, 1);
        }

        if (material instanceof HasVertexColors
                && ((HasVertexColors) material).isVertexColors() == Material.COLORS.FACE) {
            diffuseColor.multiply(face.getColor());
        }

        // compute light shading

        rayLight.getOrigin().copy(point);

        if (material instanceof MeshBasicMaterial) {
            for (int i = 0, l = lights.size(); i < l; i++) {

                Light light = lights.get(i);

                lightVector.setFromMatrixPosition(light.getMatrixWorld());
                lightVector.sub(point);

                rayLight.getDirection().copy(lightVector).normalize();

                List<Intersect> intersections2 = raycasterLight.intersectObjects(objects, true);

                // point in shadow

                if (intersections2.size() > 0)
                    continue;

                // point visible

                outputColor.add(diffuseColor);

            }

        } else if (material instanceof MeshLambertMaterial || material instanceof MeshPhongMaterial) {

            boolean normalComputed = false;

            for (int i = 0, l = lights.size(); i < l; i++) {

                Light light = lights.get(i);

                lightColor.copyGammaToLinear(light.getColor());

                lightVector.setFromMatrixPosition(light.getMatrixWorld());
                lightVector.sub(point);

                rayLight.getDirection().copy(lightVector).normalize();

                List<Intersect> intersections3 = raycasterLight.intersectObjects(objects, true);

                // point in shadow

                if (intersections3.size() > 0)
                    continue;

                // point lit

                if (normalComputed == false) {

                    // the same normal can be reused for all lights
                    // (should be possible to cache even more)

                    computePixelNormal(normalVector, localPoint, ((HasShading) material).getShading(), face,
                            vertices);
                    normalVector.apply(_object.normalMatrix).normalize();

                    normalComputed = true;

                }

                // compute attenuation

                double attenuation = 1.0;

                if (light instanceof HasRaytracingPhysicalAttenuation
                        && ((HasRaytracingPhysicalAttenuation) light).isPhysicalAttenuation() == true) {

                    attenuation = lightVector.length();
                    attenuation = 1.0 / (attenuation * attenuation);

                }

                lightVector.normalize();

                // compute diffuse

                double dot = Math.max(normalVector.dot(lightVector), 0);
                double diffuseIntensity = dot * ((HasIntensity) light).getIntensity();

                lightContribution.copy(diffuseColor);
                lightContribution.multiply(lightColor);
                lightContribution.multiply(diffuseIntensity * attenuation);

                outputColor.add(lightContribution);

                // compute specular

                if (material instanceof MeshPhongMaterial) {

                    halfVector.add(lightVector, eyeVector).normalize();

                    double dotNormalHalf = Math.max(normalVector.dot(halfVector), 0.0);
                    double specularIntensity = Math
                            .max(Math.pow(dotNormalHalf, ((MeshPhongMaterial) material).getShininess()), 0.0)
                            * diffuseIntensity;

                    double specularNormalization = (((MeshPhongMaterial) material).getShininess() + 2.0) / 8.0;

                    specularColor.copyGammaToLinear(((MeshPhongMaterial) material).getSpecular());

                    double alpha = Math.pow(Math.max(1.0 - lightVector.dot(halfVector), 0.0), 5.0);

                    schlick.setR(specularColor.getR() + (1.0 - specularColor.getR()) * alpha);
                    schlick.setG(specularColor.getG() + (1.0 - specularColor.getG()) * alpha);
                    schlick.setB(specularColor.getB() + (1.0 - specularColor.getB()) * alpha);

                    lightContribution.copy(schlick);

                    lightContribution.multiply(lightColor);
                    lightContribution.multiply(specularNormalization * specularIntensity * attenuation);

                    outputColor.add(lightContribution);
                }

            }

        }

        // reflection / refraction

        double reflectivity = ((MeshPhongMaterial) material).getReflectivity();

        if ((((HasRaytracingMirror) material).isMirror() || ((HasRaytracingGlass) material).isGlass())
                && reflectivity > 0 && recursionDepth < maxRecursionDepth) {

            if (((HasRaytracingMirror) material).isMirror()) {

                reflectionVector.copy(rayDirection);
                reflectionVector.reflect(normalVector);

            } else if (((HasRaytracingGlass) material).isGlass()) {

                double eta = ((MeshPhongMaterial) material).getRefractionRatio();

                double dotNI = rayDirection.dot(normalVector);
                double k = 1.0 - eta * eta * (1.0 - dotNI * dotNI);

                if (k < 0.0) {

                    reflectionVector.set(0, 0, 0);

                } else {

                    reflectionVector.copy(rayDirection);
                    reflectionVector.multiply(eta);

                    double alpha = eta * dotNI + Math.sqrt(k);
                    tmpVec.copy(normalVector);
                    tmpVec.multiply(alpha);
                    reflectionVector.sub(tmpVec);

                }

            }

            double theta = Math.max(eyeVector.dot(normalVector), 0.0);
            double rf0 = reflectivity;
            double fresnel = rf0 + (1.0 - rf0) * Math.pow((1.0 - theta), 5.0);

            double weight = fresnel;

            Color zColor = tmpColor[recursionDepth];

            spawnRay(point, reflectionVector, zColor, recursionDepth + 1);

            if (((MeshPhongMaterial) material).getSpecular() != null) {

                zColor.multiply(((MeshPhongMaterial) material).getSpecular());

            }

            zColor.multiply(weight);
            outputColor.multiply(1.0 - weight);
            outputColor.add(zColor);
        }

    }

    Vector3 tmpVec1 = new Vector3();
    Vector3 tmpVec2 = new Vector3();
    Vector3 tmpVec3 = new Vector3();

    private void computePixelNormal(Vector3 outputVector, Vector3 point, Material.SHADING shading, Face3 face,
            List<Vector3> vertices) {

        Vector3 faceNormal = face.getNormal();
        List<Vector3> vertexNormals = face.getVertexNormals();

        if (shading == Material.SHADING.FLAT) {
            outputVector.copy(faceNormal);

        } else if (shading == Material.SHADING.SMOOTH) {
            // compute barycentric coordinates

            Vector3 vA = vertices.get(face.getA());
            Vector3 vB = vertices.get(face.getB());
            Vector3 vC = vertices.get(face.getC());

            tmpVec3.cross(tmpVec1.sub(vB, vA), tmpVec2.sub(vC, vA));
            double areaABC = faceNormal.dot(tmpVec3);

            tmpVec3.cross(tmpVec1.sub(vB, point), tmpVec2.sub(vC, point));
            double areaPBC = faceNormal.dot(tmpVec3);
            double a = areaPBC / areaABC;

            tmpVec3.cross(tmpVec1.sub(vC, point), tmpVec2.sub(vA, point));
            double areaPCA = faceNormal.dot(tmpVec3);
            double b = areaPCA / areaABC;

            double c = 1.0 - a - b;

            // compute interpolated vertex normal

            tmpVec1.copy(vertexNormals.get(0));
            tmpVec1.multiply(a);

            tmpVec2.copy(vertexNormals.get(1));
            tmpVec2.multiply(b);

            tmpVec3.copy(vertexNormals.get(2));
            tmpVec3.multiply(c);

            outputVector.add(tmpVec1, tmpVec2);
            outputVector.add(tmpVec3);

        }

    }

}