Last active
April 30, 2022 04:53
Pseudo 3D Illumination effect using 2D normal map images
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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