changeset 3794:ebe36f2349d1

Added Height Map to Normal Map conversion utility class and app to 3D Viewer.
author "Jasper Potts"
date Mon, 03 Jun 2013 14:12:01 -0700
parents 8df94ef84244
children 2d119fc96ca6
files apps/experiments/3DViewer/src/main/java/com/javafx/experiments/height2normal/Height2NormalApp.java apps/experiments/3DViewer/src/main/java/com/javafx/experiments/height2normal/Height2NormalConverter.java
diffstat 2 files changed, 329 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/height2normal/Height2NormalApp.java	Mon Jun 03 14:12:01 2013 -0700
@@ -0,0 +1,222 @@
+/*
+ * Copyright (c) 2010, 2013 Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * 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 Oracle Corporation 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
+ * OWNER 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.javafx.experiments.height2normal;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import javax.imageio.ImageIO;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.application.Application;
+import javafx.beans.binding.ObjectBinding;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.embed.swing.SwingFXUtils;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.Group;
+import javafx.scene.PerspectiveCamera;
+import javafx.scene.Scene;
+import javafx.scene.SubScene;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.Separator;
+import javafx.scene.control.Slider;
+import javafx.scene.control.ToolBar;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.PhongMaterial;
+import javafx.scene.shape.Box;
+import javafx.scene.transform.Rotate;
+import javafx.scene.transform.Translate;
+import javafx.stage.FileChooser;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+import com.javafx.experiments.jfx3dviewer.AutoScalingGroup;
+
+/**
+ * Gui Util App for converting Heights 2 Normals
+ */
+public class Height2NormalApp  extends Application {
+    private Image testImage;
+    private SimpleObjectProperty<Image> heightImage = new SimpleObjectProperty<>();
+    private SimpleObjectProperty<Image> normalImage = new SimpleObjectProperty<>();
+    private File heightFile;
+    private Stage stage;
+
+    @Override public void start(Stage stage) throws Exception {
+        this.stage = stage;
+
+        // load test image
+        testImage = new Image(Height2NormalApp.class.getResource("javafx-heightmap.jpg").toExternalForm());
+        heightImage.set(testImage);
+
+        // create toolbar
+        ToolBar toolBar = new ToolBar();
+        Button openButton = new Button("Open...");
+        openButton.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent event) {
+                open();
+            }
+        });
+        Button saveButton = new Button("Save...");
+        saveButton.setOnAction(new EventHandler<ActionEvent>() {
+            @Override public void handle(ActionEvent event) {
+                save();
+            }
+        });
+        final CheckBox invertCheckBox = new CheckBox("invert");
+        final Slider scaleSlider = new Slider(1,50,2);
+        toolBar.getItems().addAll(openButton,saveButton,
+                                  new Separator(),
+                                  invertCheckBox,
+                                  new Separator(),
+                                  new Label("Scale:"),scaleSlider);
+
+        // bind creation of normal image
+        normalImage.bind(
+                new ObjectBinding<Image>() {
+                    { bind(heightImage, invertCheckBox.selectedProperty(), scaleSlider.valueProperty()); }
+                    @Override protected Image computeValue() {
+                        return Height2NormalConverter.convertToNormals(heightImage.get(), invertCheckBox.isSelected(), scaleSlider.getValue());
+                    }
+                });
+
+        // build stage
+        VBox root = new VBox();
+        HBox views = new HBox();
+        VBox.setVgrow(views, Priority.ALWAYS);
+        root.getChildren().addAll(toolBar, views);
+
+        ImageView srcImageView = new ImageView();
+        srcImageView.setFitWidth(512);
+        srcImageView.setFitHeight(512);
+        srcImageView.imageProperty().bind(heightImage);
+        ImageView dstImageView = new ImageView();
+        dstImageView.setFitWidth(512);
+        dstImageView.setFitHeight(512);
+        dstImageView.imageProperty().bind(normalImage);
+        views.getChildren().addAll(srcImageView,dstImageView,new View3D().create());
+
+        Scene scene = new Scene(root);
+        stage.setScene(scene);
+        stage.show();
+    }
+
+    public void open() {
+        FileChooser fileChooser = new FileChooser();
+        heightFile = fileChooser.showOpenDialog(stage);
+        if (heightFile != null) {
+            try {
+                heightImage.set(new Image(heightFile.toURI().toURL().toExternalForm()));
+            } catch (MalformedURLException e) {
+                e.printStackTrace();
+            }
+        } else {
+            heightImage.set(testImage);
+        }
+    }
+
+    public void save() {
+        FileChooser fileChooser = new FileChooser();
+        if (heightFile != null) {
+            String filePath = heightFile.getName();
+            fileChooser.setInitialFileName(filePath.substring(0,filePath.lastIndexOf('.'))+"-normal-map.png");
+        } else {
+            fileChooser.setInitialFileName("normal-map.png");
+        }
+        File normalFile = fileChooser.showSaveDialog(stage);
+        if (normalFile != null) {
+            try {
+                ImageIO.write(SwingFXUtils.fromFXImage(normalImage.get(),null),"png",normalFile);
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    public static void main(String[] args) {
+        launch(args);
+    }
+
+    private class View3D {
+        private final Group root3D = new Group();
+        private final PerspectiveCamera camera = new PerspectiveCamera(true);
+        private final Rotate cameraXRotate = new Rotate(-40,0,0,0,Rotate.X_AXIS);
+        private final Rotate cameraYRotate = new Rotate(-20,0,0,0,Rotate.Y_AXIS);
+        private final Rotate cameraLookXRotate = new Rotate(0,0,0,0,Rotate.X_AXIS);
+        private final Rotate cameraLookZRotate = new Rotate(0,0,0,0,Rotate.Z_AXIS);
+        private final Translate cameraPosition = new Translate(0,0,-7);
+        private AutoScalingGroup autoScalingGroup = new AutoScalingGroup(2);
+
+        public SubScene create() {
+            SubScene scene = new SubScene(root3D,512,512,true,false);
+            scene.setFill(Color.ALICEBLUE);
+
+            // CAMERA
+            camera.getTransforms().addAll(
+                    cameraXRotate,
+                    cameraYRotate,
+                    cameraPosition,
+                    cameraLookXRotate,
+                    cameraLookZRotate);
+            camera.setNearClip(0.1);
+            camera.setFarClip(100);
+            scene.setCamera(camera);
+            root3D.getChildren().addAll(camera, autoScalingGroup);
+
+            Box box = new Box(10,0.11,10);
+
+            PhongMaterial material = new PhongMaterial(Color.DODGERBLUE);
+            material.bumpMapProperty().bind(normalImage);
+            box.setMaterial(material);
+
+            autoScalingGroup.getChildren().add(box);
+
+            Timeline timeline = new Timeline(
+                    new KeyFrame(Duration.ZERO, new KeyValue(cameraYRotate.angleProperty(),0)),
+                    new KeyFrame(Duration.seconds(10), new KeyValue(cameraYRotate.angleProperty(),360))
+            );
+            timeline.setCycleCount(Timeline.INDEFINITE);
+            timeline.play();
+
+            return scene;
+        }
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/apps/experiments/3DViewer/src/main/java/com/javafx/experiments/height2normal/Height2NormalConverter.java	Mon Jun 03 14:12:01 2013 -0700
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2010, 2013 Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * 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 Oracle Corporation 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
+ * OWNER 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.javafx.experiments.height2normal;
+
+import javafx.geometry.Point3D;
+import javafx.scene.image.Image;
+import javafx.scene.image.PixelFormat;
+import javafx.scene.image.PixelReader;
+import javafx.scene.image.WritableImage;
+
+/**
+ * Util class to convert height maps into normal maps
+ */
+public class Height2NormalConverter {
+    public static Image convertToNormals(Image heightMap, boolean invert, double scale) {
+        final int w = (int)heightMap.getWidth();
+        final int h = (int)heightMap.getHeight();
+        final byte[] heightPixels = new byte[w*h*4];
+        final byte[] normalPixels = new byte[w*h*4];
+        // get pixels
+        final PixelReader reader = heightMap.getPixelReader();
+        reader.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] = (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] = 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);
+                // normalize normal
+                normal = new Point3D(
+                        normal.getX()/w,
+                        normal.getY()/h,
+                        normal.getZ()
+                );
+                // it seems there is lots of ways to calculate the Z element of normal map, 3 options here
+//                normalPixels[pixelIndex] = (byte)((normal.getZ()*128)+128); // Option 1
+                normalPixels[pixelIndex] = (byte)(255-(normal.getZ() * scale)); // Option 2
+//                normalPixels[pixelIndex] = (byte)255; // Option 3
+                normalPixels[pixelIndex+1] = (byte)((normal.getY()*128)+128);
+                normalPixels[pixelIndex+2] = (byte)((normal.getX()*128)+128);
+                normalPixels[pixelIndex+3] = (byte)255;
+            }
+        }
+        // create output image
+        final WritableImage outImage = new WritableImage(w,h);
+        outImage.getPixelWriter().setPixels(0,0,w,h,PixelFormat.getByteBgraInstance(),normalPixels,0,w*4);
+        return outImage;
+    }
+}