Skip to content

Instantly share code, notes, and snippets.

@volfegan
Last active April 30, 2022 04:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save volfegan/0fede2a0f1c3ae2341c04c3664db36e2 to your computer and use it in GitHub Desktop.
Save volfegan/0fede2a0f1c3ae2341c04c3664db36e2 to your computer and use it in GitHub Desktop.
Pseudo 3D Illumination effect using 2D normal map images
//Selects a normal map image with naming convection: image_normal.xxx
//Tries to find the original image (image.xxx) by removing "_normal" if any exists.
//References:
//https://en.wikipedia.org/wiki/Normal_mapping
//https://github.com/leonardo-ono/Java2DNormalMapEffectTest/blob/main/src/Test.java
//https://learnopengl.com/Advanced-Lighting/Normal-Mapping
//https://ogldev.org/www/tutorial26/tutorial26.html
//Create normal maps:
//https://beta.friendlyshade.com/normalizer
//https://www.gimp.org/downloads/
//https://www.youtube.com/watch?v=77Nd7yXuSgg
//http://www.zarria.net/nrmphoto/nrmphoto.html
//https://www.katsbits.com/tutorials/textures/making-normal-maps-from-photographs.php
PImage img_normal;
PImage img;
//there is no file validation, so any non-img_normal selected will crash the program
void fileSelected(File selection) {
if (selection == null) {
println("No image file selected.");
exit();
} else {
String filepath = selection.getAbsolutePath();
String filename = selection.getName();
int pos = filename.lastIndexOf(".");
String fileExtension = filename.substring(pos, filename.length()); //still has dot extension
if (pos != -1) filename = filename.substring(0, pos); //remove extension
println("File selected " + filepath);
//println("Filename: " + filename);
// load file here
img_normal = loadImage(filepath);
println(fileExtension);
try {
//get the image from the normal map without the "_normal" part
pos = filepath.lastIndexOf("_normal");
String new_filepath = filepath.substring(0, pos);
new_filepath += fileExtension;
println(new_filepath);
img = loadImage(new_filepath);
}
catch(Exception e) {
img = null;
}
}
}
void interrupt() {
while (img_normal==null) delay(200);
}
public void settings() {
selectInput("Select the normal map image file to process:", "fileSelected");
interrupt(); //interrupt process until img_normal is selected
//for testing
//img_normal = loadImage("cat_normal.jpg");
width = img_normal.width;
height = img_normal.height;
//the canvas window size will be according to the img_normal size
//if the img_normal is bigger, it will be resized to 80% of display
if (width > displayWidth) {
float resizer = width / (displayWidth * 0.8);
width = (int)((float)displayWidth * 0.8);
height = (int)((float)height / resizer);
img_normal.resize(width, height);
}
if (height > displayHeight) {
float resizer = height / (displayHeight * 0.8);
height = (int)((float)displayHeight * 0.8);
width = (int)((float)width / resizer);
img_normal.resize(width, height);
}
size(width, height);
}
public void setup() {
noStroke();
}
void draw() {
clear();
loadPixels();
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
color normalMapPixel = img_normal.get(x, y);
//Get the RGB values from the normal map pixel
int r = (normalMapPixel >> 16) & 0xFF;
int g = (normalMapPixel >> 8) & 0xFF;
int b = normalMapPixel & 0xFF;
// ref: https://en.wikipedia.org/wiki/Normal_mapping#Calculation
// X: -1 to +1 : Red: 0 to 255
// Y: -1 to +1 : Green: 0 to 255
// Z: 0 to -1 : Blue: 128 to 255
PVector normal = new PVector();
normal.x = map(r, 0, 255, -1, 1);
normal.y = map(g, 0, 255, 1, -1);// y is inverted because screen space
normal.z = map(b, 128, 255, 0, -1);
normal.normalize();
PVector pixelPosision = new PVector(x, y, 0);
//Get the light direction from the current pixel position to mouse location and normalize it
//The negative z values convention guarantees the light vector and normal vector are coincident
float lightPower = 25 * -1;
PVector lightDirection = new PVector(mouseX, mouseY, lightPower);
lightDirection.sub(pixelPosision);
lightDirection.normalize();
float intensity = normal.dot(lightDirection);
int brightness = (int) (255 * intensity);
color colour = color(brightness, brightness, brightness);
pixels[x+y*width] = colour;
//pixels[x+y*width] = img_normal.get(x, y);
}
}
updatePixels();
//simple overlay with colour image
if (img != null) {
tint(255,128);
image(img,0,0);
}
//a white ball of light
for (int r = 0; r < 69; r+=1) {
fill(255, 5);
circle(mouseX, mouseY, r);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment