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