/** * NormalMap.java * * Copyright (c) 2013-2016, F(X)yz * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 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. * * Neither the name of F(X)yz, any associated website, 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 F(X)yz 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 org.fxyz3d.tools; import java.util.Random; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.geometry.Point3D; import javafx.scene.image.Image; import javafx.scene.image.PixelFormat; import javafx.scene.image.PixelReader; import javafx.scene.image.PixelWriter; import javafx.scene.image.WritableImage; /** * Class represent a Normal Map Image. * When it comes to Scaling think of intensity as the Macro, * and intensity scale as the micro Also if you recieve a lot of "Bright" pixeling viewing object from the side, Apply a SMALL amount of blur to the Image PRIOR to creating this Image. It should help smooth things out. * @author Jason Pollastrini aka jdub1581 */ public class NormalMap extends WritableImage { private final double DEFAULT_INTENSITY = 5.0, DEFAULT_INTENSITY_SCALE = 5.0; private final boolean DEFAULT_INVERTED = new Random().nextBoolean(); protected PixelReader pReader; private final PixelWriter pWriter; private final Image srcImage; public NormalMap(final Image src){ super(src.getPixelReader(),0,0, (int)src.getWidth(), (int)src.getHeight()); this.srcImage = src; this.pWriter = getPixelWriter(); this.buildNormalMap(DEFAULT_INTENSITY, DEFAULT_INTENSITY_SCALE, DEFAULT_INVERTED); } public NormalMap(final double i, final double is, final boolean inv, final Image src){ this(src); this.buildNormalMap(i, is, inv); } private void buildNormalMap(double scale, double scaleFactor, boolean invert) { pReader = srcImage.getPixelReader(); final int w = (int) srcImage.getWidth(); final int h = (int) srcImage.getHeight(); final WritableImage gray = new WritableImage(w, h); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { gray.getPixelWriter().setColor(x, y, pReader.getColor(x, y).grayscale()); } } final byte[] heightPixels = new byte[w * h * 4]; final byte[] normalPixels = new byte[w * h * 4]; // get pixels pReader = gray.getPixelReader(); pReader.getPixels(0, 0, w, h, PixelFormat.getByteBgraInstance(), heightPixels, 0, w * 4); if (invert) { for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { final int pixelIndex = (y * w * 4) + (x * 4); heightPixels[pixelIndex + 0] = (byte) (255 - Byte.toUnsignedInt(heightPixels[pixelIndex])); heightPixels[pixelIndex + 1] = (byte) (255 - Byte.toUnsignedInt(heightPixels[pixelIndex + 1])); heightPixels[pixelIndex + 2] = (byte) (255 - Byte.toUnsignedInt(heightPixels[pixelIndex + 2])); heightPixels[pixelIndex + 3] = (byte) (heightPixels[pixelIndex + 3]); } } } // generate normal map for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { final int yAbove = Math.max(0, y - 1); final int yBelow = Math.min(h - 1, y + 1); final int xLeft = Math.max(0, x - 1); final int xRight = Math.min(w - 1, x + 1); final int pixelIndex = (y * w * 4) + (x * 4); final int pixelAboveIndex = (yAbove * w * 4) + (x * 4); final int pixelBelowIndex = (yBelow * w * 4) + (x * 4); final int pixelLeftIndex = (y * w * 4) + (xLeft * 4); final int pixelRightIndex = (y * w * 4) + (xRight * 4); final int pixelAboveHeight = Byte.toUnsignedInt(heightPixels[pixelAboveIndex]); final int pixelBelowHeight = Byte.toUnsignedInt(heightPixels[pixelBelowIndex]); final int pixelLeftHeight = Byte.toUnsignedInt(heightPixels[pixelLeftIndex]); final int pixelRightHeight = Byte.toUnsignedInt(heightPixels[pixelRightIndex]); Point3D pixelAbove = new Point3D(x, yAbove, pixelAboveHeight); Point3D pixelBelow = new Point3D(x, yBelow, pixelBelowHeight); Point3D pixelLeft = new Point3D(xLeft, y, pixelLeftHeight); Point3D pixelRight = new Point3D(xRight, y, pixelRightHeight); Point3D H = pixelLeft.subtract(pixelRight); Point3D V = pixelAbove.subtract(pixelBelow); Point3D normal = H.crossProduct(V); normal = new Point3D( (normal.getX() / w), (normal.getY() / h), (1 / normal.getZ()) / (Math.max(1.0, scale) / (scaleFactor)) ).normalize(); normalPixels[pixelIndex + 0] = (byte) (255 - (normal.getZ())); //Blue normalPixels[pixelIndex + 1] = (byte) (128 + (normal.getY() * 128.0));//Green normalPixels[pixelIndex + 2] = (byte) (128 + (normal.getX() * 128.0));//Red normalPixels[pixelIndex + 3] = (byte) (255); //alpha } } // create output image pWriter.setPixels(0, 0, w, h, PixelFormat.getByteBgraPreInstance(), normalPixels, 0, w * 4); } /*========================================================================== Properties */ /** * */ private final DoubleProperty intensity = new SimpleDoubleProperty(this, "intensity" , DEFAULT_INTENSITY){ }; /** * * @return */ public double getIntensity() { return intensity.get(); } /** * * @param value */ public void setIntensity(double value) { intensity.set(value); } /** * * @return */ public DoubleProperty intensityProperty() { return intensity; } //========================================= /** * */ private final DoubleProperty intensityScale = new SimpleDoubleProperty(this, "intensityScale" , DEFAULT_INTENSITY_SCALE){ }; /** * * @return */ public double getIntensityScale() { return intensityScale.get(); } /** * * @param value */ public void setIntensityScale(double value) { intensityScale.set(value); } /** * * @return */ public DoubleProperty intensityScaleProperty() { return intensityScale; } //========================================== /** * */ private final BooleanProperty invertNormals = new SimpleBooleanProperty(this, "inverted" , DEFAULT_INVERTED){ }; /** * * @return */ public boolean isInvertNormals() { return invertNormals.get(); } /** * * @param value */ public void setInvertNormals(boolean value) { invertNormals.set(value); } /** * * @return */ public BooleanProperty invertNormalsProperty() { return invertNormals; } }