1 /*
  2  * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 package normmap;
 25 
 26 import javax.imageio.ImageIO;
 27 import javax.swing.JFrame;
 28 
 29 import java.awt.Graphics;
 30 import java.awt.Graphics2D;
 31 import java.awt.Color;
 32 import java.awt.image.BufferedImage;
 33 import java.awt.image.DataBufferInt;
 34 import java.io.IOException;
 35 import java.util.Random;
 36 import javax.swing.JPanel;
 37 import java.awt.Font;
 38 
 39 import java.util.Arrays;
 40 
 41 /**
 42  * Based on a demo presented at JVMLS 2025 conference by Emanuel Peter, when giving
 43  * The rest of this comment is based on Emanuel's original code.
 44  *
 45  * A talk about Auto-Vectorization in HotSpot, see:
 46  *   https://inside.java/2025/08/16/jvmls-hotspot-auto-vectorization/
 47  *
 48  * If you want to disable the auto-vectorizer, you can run:
 49  *   java -XX:-UseSuperWord NormalMapping.java
 50  *
 51  * On x86, you can also play with the UseAVX flag:
 52  *   java -XX:UseAVX=1 NormalMapping.java
 53  *
 54  * The motivation for JVMLS 2025 was to present something that vectorizes
 55  * in an "embarassingly parallel" way. It should be something that C2's
 56  * SuperWord Auto Vectorizer could already do for many JDK releases,
 57  * and also has some visual appeal. I decided to use normal mapping, see:
 58  *   https://en.wikipedia.org/wiki/Normal_mapping
 59  *
 60  * At the conference, I only had the version that loads a normal map
 61  * from an image. I now also added some "generated" cases, which are
 62  * created from 2d height functions, and then converted to normal
 63  * maps. This allows us to show more "surfaces" without having to
 64  * store the images for all those cases.
 65  *
 66  * If you are interested in understanding the components, then look at these:
 67  * - computeLight: the normal mapping "shader / kernel".
 68  * - generateNormals / computeNormals: computing normals from height functions.
 69  * - main: setup and endless-loop that triggers normals to be swapped periodically.
 70  * - MyDrawingPanel: drawing all the parts to the screen.
 71  */
 72 public class Main {
 73     public static Random RANDOM = new Random();
 74 
 75     static void main(String[] args) {
 76         System.out.println("Welcome to the Normal Mapping Demo!");
 77         // Create an application state with 5 lights.
 78         State state = new State(5);
 79 
 80         // Set up a panel we can draw on, and put it in a window.
 81         System.out.println("Setting up Window...");
 82         MyDrawingPanel panel = new MyDrawingPanel(state);
 83         JFrame frame = new JFrame("Normal Mapping Demo (Auto-Vectorization)");
 84         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 85         frame.setSize(2000, 1000);
 86         frame.add(panel);
 87         frame.setVisible(true);
 88         System.out.println("Running Demo...");
 89 
 90         try {
 91             // Tight loop where we redraw the panel as fast as possible.
 92             int count = 0;
 93             while (true) {
 94                 Thread.sleep(1);
 95                 state.update();
 96                 panel.repaint();
 97                 if (count++ > 500) {
 98                     count = 0;
 99                     state.nextNormals();
100                 }
101             }
102         } catch (InterruptedException e) {
103             System.out.println("Interrupted, terminating demo.");
104         } finally {
105             System.out.println("Shut down demo.");
106             frame.setVisible(false);
107             frame.dispose();
108         }
109     }
110 
111 /*    public static File getLocalFile(String name) {
112         // If we are in JTREG IR testing mode, we have to get the path via system property,
113         // if it is run in stand-alone that property is not available, and we can load
114         // via getResource.
115         String file = //System.getProperty("test.src",
116             "/Users/grfrost/github/babylon-grfrost-fork/hat/examples/normmap/src/main/resources/images/"+name;
117         System.out.println("file = "+file);
118         return new File(file);
119 
120     } */
121 
122     public static BufferedImage loadImage(String resourcePath) {
123         try {
124             var inputStream = Main.class.getResourceAsStream(resourcePath);
125             return ImageIO.read(inputStream);
126            // return ImageIO.read(file);
127         } catch (IOException e) {
128             throw new RuntimeException("Could not load: ", e);
129         }
130     }
131 
132     public static class Light {
133         public float x = 0.5f;
134         public float y = 0.5f;
135         private float dx;
136         private float dy;
137 
138         private float h;
139         public float r;
140         public float g;
141         public float b;
142 
143         Light() {
144             this.h = RANDOM.nextFloat();
145         }
146 
147         // Random movement of the Light
148         public void update() {
149             // Random acceleration with dampening.
150             dx *= 0.99;
151             dy *= 0.99;
152             dx += RANDOM.nextFloat() * 0.001 - 0.0005;
153             dy += RANDOM.nextFloat() * 0.001 - 0.0005;
154             x += dx;
155             y += dy;
156 
157             // Boounce off the walls.
158             if (x < 0) { dx = +Math.abs(dx); }
159             if (x > 1) { dx = -Math.abs(dx); }
160             if (y < 0) { dy = +Math.abs(dy); }
161             if (y > 1) { dy = -Math.abs(dy); }
162 
163             // Rotate the hue -> gets us nice rainbow colors.
164             h += 0.001 + RANDOM.nextFloat() * 0.0002;
165             Color c = Color.getHSBColor(h, 1f, 1f);
166             r = (1f / 256f) * c.getRed();
167             g = (1f / 256f) * c.getGreen();
168             b = (1f / 256f) * c.getBlue();
169         }
170     }
171 
172     public static class State {
173         private static final int sizeX = 1000;
174         private static final int sizeY = 1000;
175 
176         public Light[] lights;
177         private int nextNormalsId = 0;
178 
179         public BufferedImage normals;
180         public float[] coordsX;
181         public float[] coordsY;
182         public float[] normalsX;
183         public float[] normalsY;
184         public float[] normalsZ;
185 
186         public BufferedImage output;
187         public BufferedImage output_2;
188         public int[] outputRGB;
189         public int[] outputRGB_2;
190 
191         public long lastTime;
192         public float fps;
193 
194         float luminosityCorrection = 1f;
195 
196         public State(int numberOfLights) {
197             lights = new Light[numberOfLights];
198             for (int i = 0; i < lights.length; i++) {
199                 lights[i] = new Light();
200             }
201 
202             // Coordinates
203             this.coordsX = new float[sizeX * sizeY];
204             this.coordsY = new float[sizeX * sizeY];
205             for (int y = 0; y < sizeY; y++) {
206                 for (int x = 0; x < sizeX; x++) {
207                     this.coordsX[y * sizeX + x] = x * (1f / sizeX);
208                     this.coordsY[y * sizeX + x] = y * (1f / sizeY);
209                 }
210             }
211 
212             nextNormals();
213 
214             // Double buffered output images, where we render to.
215             // Without double buffering, we would get some flickering effects,
216             // because we would be concurrently updating the buffer and drawing it.
217             this.output   = new BufferedImage(sizeX, sizeY, BufferedImage.TYPE_INT_RGB);
218             this.output_2 = new BufferedImage(sizeX, sizeY, BufferedImage.TYPE_INT_RGB);
219             this.outputRGB   = ((DataBufferInt) output.getRaster().getDataBuffer()).getData();
220             this.outputRGB_2 = ((DataBufferInt) output_2.getRaster().getDataBuffer()).getData();
221 
222             // Set up the FPS tracker
223             lastTime = System.nanoTime();
224         }
225 
226         public void nextNormals() {
227             switch (nextNormalsId) {
228                 case 0 -> setNormals(loadNormals("normal_map.png"));
229                 case 1 -> setNormals(generateNormals("heart"));
230                 case 2 -> setNormals(generateNormals("hex"));
231                 case 3 -> setNormals(generateNormals("cone"));
232                 case 4 -> setNormals(generateNormals("ripple"));
233                 case 5 -> setNormals(generateNormals("hill"));
234                 case 6 -> setNormals(generateNormals("ripple2"));
235                 case 7 -> setNormals(generateNormals("cones"));
236                 case 8 -> setNormals(generateNormals("spheres"));
237                 case 9 -> setNormals(generateNormals("donut"));
238                 default -> throw new RuntimeException();
239             }
240             nextNormalsId = (nextNormalsId + 1) % 10;
241         }
242 
243         public BufferedImage loadNormals(String name) {
244             // Extract normal values from RGB image
245             // The loaded image may not have the desired INT_RGB format, so first convert it
246             BufferedImage normalsLoaded = loadImage("/images/"+name);
247             BufferedImage buf = new BufferedImage(sizeX, sizeY, BufferedImage.TYPE_INT_RGB);
248             buf.getGraphics().drawImage(normalsLoaded, 0, 0, null);
249             return buf;
250         }
251 
252         public void setNormals(BufferedImage buf) {
253             this.normals = buf;
254 
255             int[] normalsRGB = ((DataBufferInt) this.normals.getRaster().getDataBuffer()).getData();
256             this.normalsX = new float[sizeX * sizeY];
257             this.normalsY = new float[sizeX * sizeY];
258             this.normalsZ = new float[sizeX * sizeY];
259             for (int y = 0; y < sizeY; y++) {
260                 for (int x = 0; x < sizeX; x++) {
261                     this.coordsY[y * sizeX + x] = y * (1f / sizeY);
262                     int normal = normalsRGB[y * sizeX + x];
263                     // RGB values in range [0 ... 255]
264                     int nr = (normal >> 16) & 0xff;
265                     int ng = (normal >>  8) & 0xff;
266                     int nb = (normal >>  0) & 0xff;
267 
268                     // Map range [0..255] -> [-1 .. 1]
269                     float nx = ((float)nr) * (1f / 128f) - 1f;
270                     float ny = ((float)ng) * (1f / 128f) - 1f;
271                     float nz = ((float)nb) * (1f / 128f) - 1f;
272 
273                     this.normalsX[y * sizeX + x] = -nx;
274                     this.normalsY[y * sizeX + x] = ny;
275                     this.normalsZ[y * sizeX + x] = nz;
276                 }
277             }
278         }
279 
280         interface HeightFunction {
281             // x and y should be in [0..1]
282             double call(double x, double y);
283         }
284 
285         public BufferedImage generateNormals(String name) {
286             System.out.println("  generate normals for: " + name);
287             return computeNormals((double x, double y) -> {
288                 // Scale out, so we see a little more
289                 x = 10 * (x - 0.5);
290                 y = 10 * (y - 0.5);
291 
292                 // A selection of "height functions":
293                 return switch (name) {
294                     case "cone" -> 0.1 * Math.max(0, 2 - Math.sqrt(x * x + y * y));
295                     case "heart" -> {
296                         double heart = Math.abs(Math.pow(x * x + y * y - 1, 3) - x * x * Math.pow(-y, 3));
297                         double decay = Math.exp(-(x * x + y * y));
298                         yield 0.1 * heart * decay;
299                     }
300                     case "hill" ->    0.5 * Math.exp(-(x * x + y * y));
301                     case "ripple" ->  0.01 * Math.sin(x * x + y * y);
302                     case "ripple2" -> 0.3 * Math.sin(x) * Math.sin(y);
303                     case "donut" -> {
304                         double d = Math.sqrt(x * x + y * y) - 2;
305                         double i = 1 - d*d;
306                         yield (i >= 0) ? 0.1 * Math.sqrt(i) : 0;
307                     }
308                     case "hex" -> {
309                         double f = 3.0;
310                         double a = Math.cos(f * x);
311                         double b = Math.cos(f * (-0.5 * x + Math.sqrt(3) / 2.0 * y));
312                         double c = Math.cos(f * (-0.5 * x - Math.sqrt(3) / 2.0 * y));
313                         yield 0.03 * (a + b + c);
314                     }
315                     case "cones" -> {
316                         double scale = 2.0;
317                         double r = 0.8;
318                         double cx = scale * (Math.floor(x / scale) + 0.5);
319                         double cy = scale * (Math.floor(y / scale) + 0.5);
320                         double dx = x - cx;
321                         double dy = y - cy;
322                         double d = Math.sqrt(dx * dx + dy * dy);
323                         yield 0.1 * Math.max(0, 0.8 - d);
324                     }
325                     case "spheres" -> {
326                         double scale = 2.0;
327                         double r = 0.8;
328                         double cx = scale * (Math.floor(x / scale) + 0.5);
329                         double cy = scale * (Math.floor(y / scale) + 0.5);
330                         double dx = x - cx;
331                         double dy = y - cy;
332                         double d2 = dx * dx + dy * dy;
333                         if (d2 <= r * r) {
334                             yield 0.03 * Math.sqrt(r * r - d2);
335                         }
336                         yield 0.0;
337                     }
338                     default -> throw new RuntimeException("not supported: " + name);
339                 };
340             });
341         }
342 
343         public static BufferedImage computeNormals(HeightFunction fun) {
344             BufferedImage out = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_RGB);
345             int[] arr = ((DataBufferInt) out.getRaster().getDataBuffer()).getData();
346             int sx = out.getWidth();
347             int sy = out.getHeight();
348 
349             double delta = 0.00001;
350             double dxx = 1.0 / sx;
351             double dyy = 1.0 / sy;
352             for (int yy = 0; yy < sy; yy++) {
353                 int nStart = sy * yy;
354                 for (int xx = 0; xx < sx; xx++) {
355                     double x = xx * dxx;
356                     double y = yy * dyy;
357 
358                     // Compute the partial derivatives in x and y direction;
359                     double fdx = fun.call(x + delta, y) - fun.call(x - delta, y);
360                     double fdy = fun.call(x, y + delta) - fun.call(x, y - delta);
361                     // We can compute the normal from the cross product of:
362                     //
363                     //  df/dx  x  df/dy = [2*delta, 0, fdx]  x  [0, 2*delta, fdy]
364                     //                  = [0*fdy - fdx*2*delta, fdx*0 - 2*delta*fdy, 2*delta*2*delta - 0*0]
365                     double nx = -fdx * 2 * delta;
366                     double ny = -2 * delta * fdy;
367                     double nz = 2 * delta * 2 * delta;
368 
369                     // normalize
370                     float dist = (float)Math.sqrt(nx * nx + ny * ny + nz * nz);
371                     nx /= dist;
372                     ny /= dist;
373                     nz /= dist;
374 
375                     // Now transform [-1..1] -> [0..255]
376                     int r = (int)(nx * 127f + 127f) & 0xff;
377                     int g = (int)(ny * 127f + 127f) & 0xff;
378                     int b = (int)(nz * 127f + 127f) & 0xff;
379                     int c = (r << 16) + (g << 8) + b;
380                     arr[nStart + xx] = c;
381                 }
382             }
383             return out;
384         }
385 
386         public void update() {
387             long nowTime = System.nanoTime();
388             float newFPS = 1e9f / (nowTime - lastTime);
389             fps = 0.99f * fps + 0.01f * newFPS;
390             lastTime = nowTime;
391 
392             for (Light light : lights) {
393                 light.update();
394             }
395 
396             // Reset the buffer
397             int[] outputArray = ((DataBufferInt) output.getRaster().getDataBuffer()).getData();
398             Arrays.fill(outputArray, 0);
399 
400             // Add in the contribution of each light
401             for (Light l : lights) {
402                 computeLight(l);
403             }
404             computeLuminosityCorrection();
405 
406             // Swap the buffers for double buffering.
407             var outputTmp = output;
408             output = output_2;
409             output_2 = outputTmp;
410 
411             var outputRGBTmp = outputRGB;
412             outputRGB = outputRGB_2;
413             outputRGB_2 = outputRGBTmp;
414         }
415 
416         public void computeLight(Light l) {
417             for (int i = 0; i < outputRGB.length; i++) {
418                 float x = coordsX[i];
419                 float y = coordsY[i];
420                 float nx = normalsX[i];
421                 float ny = normalsY[i];
422                 float nz = normalsZ[i];
423 
424                 // Compute distance vector between the light and the pixel
425                 float dx = x - l.x;
426                 float dy = y - l.y;
427                 float dz = 0.2f; // how much the lights float above the scene
428 
429                 // Compute the distance (dot product of d with itself)
430                 float d2 = dx * dx + dy * dy + dz * dz;
431                 float d = (float)Math.sqrt(d2);
432                 float d3 = d * d2;
433 
434                 // Compute dot-product between distance and normal vector
435                 float dotProduct = nx * dx + ny * dy + nz * dz;
436 
437                 // If the dot-product is negative:
438                 //   Light on wrong side -> 0
439                 // If the dot-product is positive:
440                 //   There should be light normalize by distance (d), and divide by the
441                 //   squared distance (d2) to have physically accurately decaying light.
442                 //   Correct the luminosity so the RGB values are going to be close
443                 //   to 255, but not over.
444                 float luminosity = Math.max(0, dotProduct / d3) * luminosityCorrection;
445 
446                 // Now we compute the color values that hopefully end up in the range
447                 // [0..255]. If the hack/trick with luminosityCorrection fails, we may
448                 // occasionally go out of the range and generate an overflow in the masking.
449                 // This can lead to some funky visual artifacts around the lights, but it
450                 // is quite rare.
451                 //
452                 // Feel free to play with the targetExposure below, and see if you can
453                 // observe the artefacts.
454                 int r = (int)(luminosity * l.r) & 0xff;
455                 int g = (int)(luminosity * l.g) & 0xff;
456                 int b = (int)(luminosity * l.b) & 0xff;
457                 int c = (r << 16) + (g << 8) + b;
458                 outputRGB[i] += c;
459             }
460         }
461 
462         // This is a bit of a horrible hack, but it mostly works.
463         // Essentially, it tries to solve the "exposure" problem:
464         // It is hard to know how much light a pixel will receive at most, and
465         // we have to convert this value to a byte [0..255] at some point.
466         // If we chose the "exposure" too low, we get a very dark picture
467         // that is not very exciting to look at. If we over-expose, then we
468         // may overflow/clip the range [0..255], leading to unpleasant visual
469         // artifacts.
470         public void computeLuminosityCorrection() {
471             // Find maximum R, G, and B value.
472             float maxR = 0;
473             float maxG = 0;
474             float maxB = 0;
475             for (int i = 0; i < outputRGB.length; i++) {
476                 int c = outputRGB[i];
477                 int cr = (c >> 16) & 0xff;
478                 int cg = (c >>  8) & 0xff;
479                 int cb = (c >>  0) & 0xff;
480 
481                 maxR = Math.max(maxR, cr);
482                 maxG = Math.max(maxG, cg);
483                 maxB = Math.max(maxB, cb);
484             }
485 
486             float maxC = Math.max(Math.max(maxR, maxG), maxB);
487 
488             // Correct the maximum value to be 230, so we are safely in range 0..255
489             // Setting it instead to 255 will make the image brighter, but most likely
490             // it will give you some funky artefacts.
491             // Setting it to 100 will make the image darker.
492             float targetExposure = 230f;
493             luminosityCorrection *= targetExposure / maxC;
494         }
495     }
496 
497     public static class MyDrawingPanel extends JPanel {
498         private final State state;
499 
500         public MyDrawingPanel(State state) {
501             this.state = state;
502         }
503 
504         @Override
505         protected void paintComponent(Graphics g) {
506             super.paintComponent(g);
507             Graphics2D g2d = (Graphics2D) g;
508 
509             // Draw color output
510             g2d.drawImage(state.output_2, 0, 0, null);
511 
512             // Draw position of lights
513             for (Light l : state.lights) {
514                 g2d.setColor(new Color(l.r, l.g, l.b));
515                 g2d.fillRect((int)(1000f * l.x) - 3, (int)(1000f * l.y) - 3, 6, 6);
516             }
517 
518             g2d.setColor(new Color(0, 0, 0));
519             g2d.fillRect(0, 0, 150, 35);
520             g2d.setColor(new Color(255, 255, 255));
521             g2d.setFont(new Font("Consolas", Font.PLAIN, 30));
522             g2d.drawString("FPS: " + (int)Math.floor(state.fps), 0, 30);
523 
524             g2d.drawImage(state.normals, 1000, 0, null);
525         }
526     }
527 }