xxxxxxxxxx
/* @pjs preload="n2.gif,c2.gif,nn2.png,dd2.png,heightmap.png,rdn.png" */
/*
Andor Salga
Blog: (includes some really dumb mistakes, which are corrected in this sketch)
https://asalga.wordpress.com/2012/09/26/experimenting-with-normal-mapping/
Performance notes:
- Avoid using PVector.sub because it is too slow
- Avoid using 'new' in the calculation hotloop because that's way too slow
- Avoid calling PImage.get() to get the pixel data, that's too slow. Use a data array instead.
*/
// colorImage is the original image the user wants to light
// targetImage will hold the result of blending the
// colorImage with the lighting.
PImage colorImage, targetImage, normalImage;
// normalMap holds our 2D array of normal vectors.
// It will be the same dimensions as our colorImage
// since the lighting is per-pixel.
PVector normalMap[][];
PVector diffuseMap[][];
// shine will be used in specular reflection calculations
// The higher the shine value, the shinier our object will be
float shine = 20.0f;
float specCol[] = {255, 128, 50};
// rayOfLight will represent a vector from the current
// pixel to the light source (cursor coords);
PVector rayOfLight = new PVector(0, 0, 0);
PVector view = new PVector(0, 0, 1);
PVector specRay = new PVector(0, 0, 0);
PVector reflection = new PVector(0, 0, 0);
// These will hold our calculated lighting values
// diffuse will be white, so we only need 1 value
// Specular is orange, so we need all three components
float finalDiffuse = 0;
float finalSpec[] = {0, 0, 0};
// animation stuff
float xOffset = 0;
int lastTime = 0;
// nDotL = Normal dot Light. This is calculated once
// per pixel in the diffuse part of the algorithm, but we may
// want to reuse it if the user wants specular reflection
// Define it here to avoid calculating it twice per pixel
float nDotL;
// These will map to keyboard keys to change the sketch state
boolean debugOn = false;
float flipNormalZ = -1.0;
boolean calculateLighting = true;
boolean isAnimating = false;
PFont font;
let cvs,
ctx;
let _diffuseImageData,
_diffuseArrBuff,
_normalArrBuff,
_targetArrBuff,
_diffuseDataView,
_normalDataView,
_targetDataView;
let uint8;
void setup(){
size(512, 512);
cvs = document.getElementById('pjsCanvas');
console.log(cvs);
ctx = cvs.getContext('2d');
font = createFont("arial", 24);
textFont(font);
fill(0);
colorImage = loadImage("heightmap.png");
normalImage = loadImage("rdn.png");
targetImage = createImage(width, height, RGB);
_diffuseImageData = ctx.createImageData(width, height);
let len = _diffuseImageData.data.length;
_diffuseArrBuff = new ArrayBuffer(len);
_normalArrBuff = new ArrayBuffer(len*3);
_targetArrBuff = new ArrayBuffer(len);
_diffuseDataView = new Uint32Array(_diffuseArrBuff);
_normalDataView = new Float32Array(_normalArrBuff);
_targetDataView = new Uint32Array(_targetArrBuff);
uint8 = new Uint8Array(_targetArrBuff);
loadNormal();
}
void keyPressed(){
// N key for toggling normal mapping
if(keyCode == 78){
calculateLighting = !calculateLighting;
}
// D key for toggling debug info
if(keyCode == 68){
debugOn = !debugOn;
}
// F key for flipping normals
if(keyCode == 70){
flipNormalZ *= -1;
}
// A key for animation
if(keyCode == 65){
isAnimating = !isAnimating;
}
// R key for random specular color
if(keyCode == 82){
specCol[0] = random(0, 255);
specCol[1] = random(0, 255);
specCol[2] = random(0, 255);
}
}
void loadNormal(){
diffuseMap = new PVector[width][height];
normalMap = new PVector[width][height];
// i indexes into the 1D array of pixels in the normal map
int i;
normalImage.loadPixels();
color col;
for(int x = 0; x < width; x++){
for(int y = 0; y < height; y++){
i = y * width + x;
// Convert the RBG values to XYZ
col = normalImage.pixels[i];
float r = red(col) - 127.0;
float g = green(col) - 127.0;
float b = blue(col) - 0.0;
normalMap[x][y] = new PVector(r, g, b);
// Normal needs to be normalized because Z
// ranged from 127-255
normalMap[x][y].normalize();
_normalDataView[i*3 + 0] = normalMap[x][y].x;
_normalDataView[i*3 + 1] = normalMap[x][y].y;
_normalDataView[i*3 + 2] = normalMap[x][y].z;
}
}
colorImage.loadPixels();
for(let i = 0; i < width * height; i++){
_diffuseDataView[i] =
255 << 24 |
blue(colorImage.pixels[i]) << 16 |
green(colorImage.pixels[i]) << 8 |
red(colorImage.pixels[i]);
}
}
void draw(){
// When the user is no longer holding down the mouse button,
// the specular highlights aren't used. So reset the values
// every frame here and set them only if necessary
finalSpec[0] = 0;
finalSpec[1] = 0;
finalSpec[2] = 0;
int xPos = (int)xOffset;
// Per frame we iterate over every pixel. We are performing
// per-pixel lighting.
if(calculateLighting){
for(int x = 0; x < width; x++, xPos++){
xPos %= width;
for(int y = 0; y < height; y++){
// We're going to create a point light instead of
// a directional light because points lights will
// provide a more interesting simulation.
// Because we'll be using point lights, we need to calculate
// the ray of light from each point to the actual light
// Here we avoid using the .sub() PVector method because
// it is much slower than simply doing the calculations ourselves.
// The ray of light then will go from the current pixel to the cursor
// coordinates.
rayOfLight.x = x - mouseX;
rayOfLight.y = y - mouseY;
// We only have two dimensions with the mouse, so we
// have to create/make up a third component ourselves.
// We pick width 'pixels' down +Z. Experiment with this
// value for interesting results.
rayOfLight.z = 100 * flipNormalZ;
// Normalize the ray it can be used in a dot product
// operation to get a sensible values(-1 to 1).
// The normal will point towards the viewer
// And the ray will be pointing from the pixel to the viewer
// It's a kind of strange backwards way of thinking about it,
// but it helps ease the math.
// When the two vectors are pointing in the same direciton
// the dot product will return 1
// if they are 90 degree apart 0
// if they are in opposite directions -1
rayOfLight.normalize();
// get the absolute value since we allow flipping the normal
let i = y * width + x;
let nx = _normalDataView[i * 3 ];
let ny = _normalDataView[i * 3 + 1];
let nz = _normalDataView[i * 3 + 2];
PVector _test = new PVector(nx, ny, nz);
nDotL = abs(rayOfLight.dot(_test));
// Avoid more processing by only calculating
// specular lighting if the users wants to do it.
// It is fairly processor intensive.
if(true){
// The next few lines calculates the reflection vector
// using Phong specular illumination. I've written
// a detailed blog about how this works:
// http://asalga.wordpress.com/2012/09/23/understanding-vector-reflection-visually/
// Also, when we have to perform vector subtraction
// as part of calculating the reflection vector,
// do it manually since calling sub() is slow.
reflection.x = _normalDataView[y * width + x + 0]; //normalMap[xPos][y].x;
reflection.y = _normalDataView[y * width + x + 1]; //normalMap[xPos][y].y;
reflection.z = _normalDataView[y * width + x + 2]; //normalMap[xPos][y].z;
reflection.mult(2.0 * nDotL);
reflection.x -= rayOfLight.x;
reflection.y -= rayOfLight.y;
reflection.z -= rayOfLight.z * flipNormalZ;
// The view vector points down (0, 0, 1) that is,
// directly towards the viewer. The dot product
// of two normalized vector returns a value from
// (-1 to 1). However, none of the normal vectors
// point away from the user, so we don't have to
// deal with making sure the result of the dot product
// is negative and thus a negative specular intensity.
// Raise the result of that dot product value to the
// power of shine. The higher shine is, the shinier
// the surface will appear.
float specIntensity = pow(reflection.dot(view), shine);
finalSpec[0] = specIntensity * specCol[0];
finalSpec[1] = specIntensity * specCol[1];
finalSpec[2] = specIntensity * specCol[2];
}
// Now that the specular and diffuse lighting are
// calculated, they need to be blended together
// with the original image and placed in the
// target image. Since blend() is too slow,
// perform our own blending operation for diffuse.
// targetImage.pixels[y*width + x] =
// color(finalSpec[0] + (nDotL * diffuseMap[xPos][y].x ),
// finalSpec[1] + (nDotL * diffuseMap[xPos][y].y ),
// finalSpec[2] + (nDotL * diffuseMap[xPos][y].z ));
var diffu = _diffuseDataView[y * width + x];
_targetDataView[y * width + x] =
255 << 24 |
(nDotL * ((diffu & 0x00ff0000) >> 16)) << 16 |
(nDotL * ((diffu & 0x0000ff00) >> 8)) << 8 |
(nDotL * (diffu & 0x000000ff));
}
}
_diffuseImageData.data.set(uint8);
ctx.putImageData(_diffuseImageData, 0, 0);
}
else{
xPos %= width;
image(colorImage, -xPos, 0 );
image(colorImage, width-xPos, 0 );
}
if(isAnimating){
xOffset += (millis() - lastTime) / 50.0f;
}
drawDebugInfo();
lastTime = millis();
}
// I try not to crowd the sketch too much with text output
void drawDebugInfo(){
if(debugOn){
text("FPS: " + floor(frameRate), 10, 20);
}
}
Image not found at n2.gif
Processing.js: Image not found at n2.gif
Image not found at c2.gif
Processing.js: Image not found at c2.gif
Image not found at nn2.png
Processing.js: Image not found at nn2.png
Image not found at dd2.png
Processing.js: Image not found at dd2.png
Image not found at heightmap.png
Processing.js: Image not found at heightmap.png