001/* 002Copyright 2006 Jerry Huxtable 003 004Licensed under the Apache License, Version 2.0 (the "License"); 005you may not use this file except in compliance with the License. 006You may obtain a copy of the License at 007 008 http://www.apache.org/licenses/LICENSE-2.0 009 010Unless required by applicable law or agreed to in writing, software 011distributed under the License is distributed on an "AS IS" BASIS, 012WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013See the License for the specific language governing permissions and 014limitations under the License. 015*/ 016 017package com.jhlabs.image; 018 019import java.awt.image.*; 020import com.jhlabs.math.*; 021import com.jhlabs.vecmath.*; 022import java.awt.*; 023import java.io.*; 024import java.util.*; 025 026/** 027 * A filter which produces lighting and embossing effects. 028 */ 029public class LightFilter extends WholeImageFilter { 030 031 /** 032 * Take the output colors from the input image. 033 */ 034 public final static int COLORS_FROM_IMAGE = 0; 035 036 /** 037 * Use constant material color. 038 */ 039 public final static int COLORS_CONSTANT = 1; 040 041 /** 042 * Use the input image brightness as the bump map. 043 */ 044 public final static int BUMPS_FROM_IMAGE = 0; 045 046 /** 047 * Use the input image alpha as the bump map. 048 */ 049 public final static int BUMPS_FROM_IMAGE_ALPHA = 1; 050 051 /** 052 * Use a separate image alpha channel as the bump map. 053 */ 054 public final static int BUMPS_FROM_MAP = 2; 055 056 /** 057 * Use a custom function as the bump map. 058 */ 059 public final static int BUMPS_FROM_BEVEL = 3; 060 061 private float bumpHeight; 062 private float bumpSoftness; 063 private int bumpShape; 064 private float viewDistance = 10000.0f; 065 Material material; 066 private Vector lights; 067 private int colorSource = COLORS_FROM_IMAGE; 068 private int bumpSource = BUMPS_FROM_IMAGE; 069 private Function2D bumpFunction; 070 private Image environmentMap; 071 private int[] envPixels; 072 private int envWidth = 1, envHeight = 1; 073 074 // Temporary variables used to avoid per-pixel memory allocation while filtering 075 private Vector3f l; 076 private Vector3f v; 077 private Vector3f n; 078 private Color4f shadedColor; 079 private Color4f diffuse_color; 080 private Color4f specular_color; 081 private Vector3f tmpv, tmpv2; 082 083 public LightFilter() { 084 lights = new Vector(); 085 addLight(new DistantLight()); 086 bumpHeight = 1.0f; 087 bumpSoftness = 5.0f; 088 bumpShape = 0; 089 material = new Material(); 090 l = new Vector3f(); 091 v = new Vector3f(); 092 n = new Vector3f(); 093 shadedColor = new Color4f(); 094 diffuse_color = new Color4f(); 095 specular_color = new Color4f(); 096 tmpv = new Vector3f(); 097 tmpv2 = new Vector3f(); 098 } 099 100 public void setMaterial( Material material ) { 101 this.material = material; 102 } 103 104 public Material getMaterial() { 105 return material; 106 } 107 108 public void setBumpFunction(Function2D bumpFunction) { 109 this.bumpFunction = bumpFunction; 110 } 111 112 public Function2D getBumpFunction() { 113 return bumpFunction; 114 } 115 116 public void setBumpHeight(float bumpHeight) { 117 this.bumpHeight = bumpHeight; 118 } 119 120 public float getBumpHeight() { 121 return bumpHeight; 122 } 123 124 public void setBumpSoftness(float bumpSoftness) { 125 this.bumpSoftness = bumpSoftness; 126 } 127 128 public float getBumpSoftness() { 129 return bumpSoftness; 130 } 131 132 public void setBumpShape(int bumpShape) { 133 this.bumpShape = bumpShape; 134 } 135 136 public int getBumpShape() { 137 return bumpShape; 138 } 139 140 public void setViewDistance(float viewDistance) { 141 this.viewDistance = viewDistance; 142 } 143 144 public float getViewDistance() { 145 return viewDistance; 146 } 147 148 public void setEnvironmentMap(BufferedImage environmentMap) { 149 this.environmentMap = environmentMap; 150 if (environmentMap != null) { 151 envWidth = environmentMap.getWidth(); 152 envHeight = environmentMap.getHeight(); 153 envPixels = getRGB( environmentMap, 0, 0, envWidth, envHeight, null ); 154 } else { 155 envWidth = envHeight = 1; 156 envPixels = null; 157 } 158 } 159 160 public Image getEnvironmentMap() { 161 return environmentMap; 162 } 163 164 public void setColorSource(int colorSource) { 165 this.colorSource = colorSource; 166 } 167 168 public int getColorSource() { 169 return colorSource; 170 } 171 172 public void setBumpSource(int bumpSource) { 173 this.bumpSource = bumpSource; 174 } 175 176 public int getBumpSource() { 177 return bumpSource; 178 } 179 180 public void setDiffuseColor(int diffuseColor) { 181 material.diffuseColor = diffuseColor; 182 } 183 184 public int getDiffuseColor() { 185 return material.diffuseColor; 186 } 187 188 public void addLight(Light light) { 189 lights.addElement(light); 190 } 191 192 public void removeLight(Light light) { 193 lights.removeElement(light); 194 } 195 196 public Vector getLights() { 197 return lights; 198 } 199 200 protected final static float r255 = 1.0f/255.0f; 201 202 protected void setFromRGB( Color4f c, int argb ) { 203 c.set( ((argb >> 16) & 0xff) * r255, ((argb >> 8) & 0xff) * r255, (argb & 0xff) * r255, ((argb >> 24) & 0xff) * r255 ); 204 } 205 206 protected int[] filterPixels( int width, int height, int[] inPixels, Rectangle transformedSpace ) { 207 int index = 0; 208 int[] outPixels = new int[width * height]; 209 float width45 = Math.abs(6.0f * bumpHeight); 210 boolean invertBumps = bumpHeight < 0; 211 Vector3f position = new Vector3f(0.0f, 0.0f, 0.0f); 212 Vector3f viewpoint = new Vector3f((float)width / 2.0f, (float)height / 2.0f, viewDistance); 213 Vector3f normal = new Vector3f(); 214 Color4f envColor = new Color4f(); 215 Color4f diffuseColor = new Color4f( new Color(material.diffuseColor) ); 216 Color4f specularColor = new Color4f( new Color(material.specularColor) ); 217 Function2D bump = bumpFunction; 218 219 // Apply the bump softness 220 if (bumpSource == BUMPS_FROM_IMAGE || bumpSource == BUMPS_FROM_IMAGE_ALPHA || bumpSource == BUMPS_FROM_MAP || bump == null) { 221 if ( bumpSoftness != 0 ) { 222 int bumpWidth = width; 223 int bumpHeight = height; 224 int[] bumpPixels = inPixels; 225 if ( bumpSource == BUMPS_FROM_MAP && bumpFunction instanceof ImageFunction2D ) { 226 ImageFunction2D if2d = (ImageFunction2D)bumpFunction; 227 bumpWidth = if2d.getWidth(); 228 bumpHeight = if2d.getHeight(); 229 bumpPixels = if2d.getPixels(); 230 } 231 int [] tmpPixels = new int[bumpWidth * bumpHeight]; 232 int [] softPixels = new int[bumpWidth * bumpHeight]; 233/* 234 for (int i = 0; i < 3; i++ ) { 235 BoxBlurFilter.blur( bumpPixels, tmpPixels, bumpWidth, bumpHeight, (int)bumpSoftness ); 236 BoxBlurFilter.blur( tmpPixels, softPixels, bumpHeight, bumpWidth, (int)bumpSoftness ); 237 } 238*/ 239 Kernel kernel = GaussianFilter.makeKernel( bumpSoftness ); 240 GaussianFilter.convolveAndTranspose( kernel, bumpPixels, tmpPixels, bumpWidth, bumpHeight, true, false, false, GaussianFilter.WRAP_EDGES ); 241 GaussianFilter.convolveAndTranspose( kernel, tmpPixels, softPixels, bumpHeight, bumpWidth, true, false, false, GaussianFilter.WRAP_EDGES ); 242 bump = new ImageFunction2D(softPixels, bumpWidth, bumpHeight, ImageFunction2D.CLAMP, bumpSource == BUMPS_FROM_IMAGE_ALPHA); 243final Function2D bbump = bump; 244if ( bumpShape != 0 ) { 245 bump = new Function2D() { 246 private Function2D original = bbump; 247 248 public float evaluate(float x, float y) { 249 float v = original.evaluate( x, y ); 250 switch ( bumpShape ) { 251 case 1: 252// v = v > 0.5f ? 0.5f : v; 253 v *= ImageMath.smoothStep( 0.45f, 0.55f, v ); 254 break; 255 case 2: 256 v = v < 0.5f ? 0.5f : v; 257 break; 258 case 3: 259 v = ImageMath.triangle( v ); 260 break; 261 case 4: 262 v = ImageMath.circleDown( v ); 263 break; 264 case 5: 265 v = ImageMath.gain( v, 0.75f ); 266 break; 267 } 268 return v; 269 } 270 }; 271} 272 } else if ( bumpSource != BUMPS_FROM_MAP ) 273 bump = new ImageFunction2D(inPixels, width, height, ImageFunction2D.CLAMP, bumpSource == BUMPS_FROM_IMAGE_ALPHA); 274 } 275 276 float reflectivity = material.reflectivity; 277 float areflectivity = (1-reflectivity); 278 Vector3f v1 = new Vector3f(); 279 Vector3f v2 = new Vector3f(); 280 Vector3f n = new Vector3f(); 281 Light[] lightsArray = new Light[lights.size()]; 282 lights.copyInto(lightsArray); 283 for (int i = 0; i < lightsArray.length; i++) 284 lightsArray[i].prepare(width, height); 285 286 float[][] heightWindow = new float[3][width]; 287 for (int x = 0; x < width; x++) 288 heightWindow[1][x] = width45*bump.evaluate(x, 0); 289 290 // Loop through each source pixel 291 for (int y = 0; y < height; y++) { 292 boolean y0 = y > 0; 293 boolean y1 = y < height-1; 294 position.y = y; 295 for (int x = 0; x < width; x++) 296 heightWindow[2][x] = width45*bump.evaluate(x, y+1); 297 for (int x = 0; x < width; x++) { 298 boolean x0 = x > 0; 299 boolean x1 = x < width-1; 300 301 // Calculate the normal at this point 302 if (bumpSource != BUMPS_FROM_BEVEL) { 303 // Complicated and slower method 304 // Calculate four normals using the gradients in +/- X/Y directions 305 int count = 0; 306 normal.x = normal.y = normal.z = 0; 307 float m0 = heightWindow[1][x]; 308 float m1 = x0 ? heightWindow[1][x-1]-m0 : 0; 309 float m2 = y0 ? heightWindow[0][x]-m0 : 0; 310 float m3 = x1 ? heightWindow[1][x+1]-m0 : 0; 311 float m4 = y1 ? heightWindow[2][x]-m0 : 0; 312 313 if (x0 && y1) { 314 v1.x = -1.0f; v1.y = 0.0f; v1.z = m1; 315 v2.x = 0.0f; v2.y = 1.0f; v2.z = m4; 316 n.cross(v1, v2); 317 n.normalize(); 318 if (n.z < 0.0) 319 n.z = -n.z; 320 normal.add(n); 321 count++; 322 } 323 324 if (x0 && y0) { 325 v1.x = -1.0f; v1.y = 0.0f; v1.z = m1; 326 v2.x = 0.0f; v2.y = -1.0f; v2.z = m2; 327 n.cross(v1, v2); 328 n.normalize(); 329 if (n.z < 0.0) 330 n.z = -n.z; 331 normal.add(n); 332 count++; 333 } 334 335 if (y0 && x1) { 336 v1.x = 0.0f; v1.y = -1.0f; v1.z = m2; 337 v2.x = 1.0f; v2.y = 0.0f; v2.z = m3; 338 n.cross(v1, v2); 339 n.normalize(); 340 if (n.z < 0.0) 341 n.z = -n.z; 342 normal.add(n); 343 count++; 344 } 345 346 if (x1 && y1) { 347 v1.x = 1.0f; v1.y = 0.0f; v1.z = m3; 348 v2.x = 0.0f; v2.y = 1.0f; v2.z = m4; 349 n.cross(v1, v2); 350 n.normalize(); 351 if (n.z < 0.0) 352 n.z = -n.z; 353 normal.add(n); 354 count++; 355 } 356 357 // Average the four normals 358 normal.x /= count; 359 normal.y /= count; 360 normal.z /= count; 361 } 362 if (invertBumps) { 363 normal.x = -normal.x; 364 normal.y = -normal.y; 365 } 366 position.x = x; 367 368 if (normal.z >= 0) { 369 // Get the material colour at this point 370 if (colorSource == COLORS_FROM_IMAGE) 371 setFromRGB(diffuseColor, inPixels[index]); 372 else 373 setFromRGB(diffuseColor, material.diffuseColor); 374 if (reflectivity != 0 && environmentMap != null) { 375 //FIXME-too much normalizing going on here 376 tmpv2.set(viewpoint); 377 tmpv2.sub(position); 378 tmpv2.normalize(); 379 tmpv.set(normal); 380 tmpv.normalize(); 381 382 // Reflect 383 tmpv.scale( 2.0f*tmpv.dot(tmpv2) ); 384 tmpv.sub(v); 385 386 tmpv.normalize(); 387 setFromRGB(envColor, getEnvironmentMap(tmpv, inPixels, width, height));//FIXME-interpolate() 388 diffuseColor.x = reflectivity*envColor.x + areflectivity*diffuseColor.x; 389 diffuseColor.y = reflectivity*envColor.y + areflectivity*diffuseColor.y; 390 diffuseColor.z = reflectivity*envColor.z + areflectivity*diffuseColor.z; 391 } 392 // Shade the pixel 393 Color4f c = phongShade(position, viewpoint, normal, diffuseColor, specularColor, material, lightsArray); 394 int alpha = inPixels[index] & 0xff000000; 395 int rgb = ((int)(c.x * 255) << 16) | ((int)(c.y * 255) << 8) | (int)(c.z * 255); 396 outPixels[index++] = alpha | rgb; 397 } else 398 outPixels[index++] = 0; 399 } 400 float[] t = heightWindow[0]; 401 heightWindow[0] = heightWindow[1]; 402 heightWindow[1] = heightWindow[2]; 403 heightWindow[2] = t; 404 } 405 return outPixels; 406 } 407 408 protected Color4f phongShade(Vector3f position, Vector3f viewpoint, Vector3f normal, Color4f diffuseColor, Color4f specularColor, Material material, Light[] lightsArray) { 409 shadedColor.set(diffuseColor); 410 shadedColor.scale(material.ambientIntensity); 411 412 for (int i = 0; i < lightsArray.length; i++) { 413 Light light = lightsArray[i]; 414 n.set(normal); 415 l.set(light.position); 416 if (light.type != DISTANT) 417 l.sub(position); 418 l.normalize(); 419 float nDotL = n.dot(l); 420 if (nDotL >= 0.0) { 421 float dDotL = 0; 422 423 v.set(viewpoint); 424 v.sub(position); 425 v.normalize(); 426 427 // Spotlight 428 if (light.type == SPOT) { 429 dDotL = light.direction.dot(l); 430 if (dDotL < light.cosConeAngle) 431 continue; 432 } 433 434 n.scale(2.0f * nDotL); 435 n.sub(l); 436 float rDotV = n.dot(v); 437 438 float rv; 439 if (rDotV < 0.0) 440 rv = 0.0f; 441 else 442// rv = (float)Math.pow(rDotV, material.highlight); 443 rv = rDotV / (material.highlight - material.highlight*rDotV + rDotV); // Fast approximation to pow 444 445 // Spotlight 446 if (light.type == SPOT) { 447 dDotL = light.cosConeAngle/dDotL; 448 float e = dDotL; 449 e *= e; 450 e *= e; 451 e *= e; 452 e = (float)Math.pow(dDotL, light.focus*10)*(1 - e); 453 rv *= e; 454 nDotL *= e; 455 } 456 457 diffuse_color.set(diffuseColor); 458 diffuse_color.scale(material.diffuseReflectivity); 459 diffuse_color.x *= light.realColor.x * nDotL; 460 diffuse_color.y *= light.realColor.y * nDotL; 461 diffuse_color.z *= light.realColor.z * nDotL; 462 specular_color.set(specularColor); 463 specular_color.scale(material.specularReflectivity); 464 specular_color.x *= light.realColor.x * rv; 465 specular_color.y *= light.realColor.y * rv; 466 specular_color.z *= light.realColor.z * rv; 467 diffuse_color.add(specular_color); 468 diffuse_color.clamp( 0, 1 ); 469 shadedColor.add(diffuse_color); 470 } 471 } 472 shadedColor.clamp( 0, 1 ); 473 return shadedColor; 474 } 475 476 private int getEnvironmentMap(Vector3f normal, int[] inPixels, int width, int height) { 477 if (environmentMap != null) { 478 float angle = (float)Math.acos(-normal.y); 479 480 float x, y; 481 y = angle/ImageMath.PI; 482 483 if (y == 0.0f || y == 1.0f) 484 x = 0.0f; 485 else { 486 float f = normal.x/(float)Math.sin(angle); 487 488 if (f > 1.0f) 489 f = 1.0f; 490 else if (f < -1.0f) 491 f = -1.0f; 492 493 x = (float)Math.acos(f)/ImageMath.PI; 494 } 495 // A bit of empirical scaling.... 496 x = ImageMath.clamp(x * envWidth, 0, envWidth-1); 497 y = ImageMath.clamp(y * envHeight, 0, envHeight-1); 498 int ix = (int)x; 499 int iy = (int)y; 500 501 float xWeight = x-ix; 502 float yWeight = y-iy; 503 int i = envWidth*iy + ix; 504 int dx = ix == envWidth-1 ? 0 : 1; 505 int dy = iy == envHeight-1 ? 0 : envWidth; 506 return ImageMath.bilinearInterpolate( xWeight, yWeight, envPixels[i], envPixels[i+dx], envPixels[i+dy], envPixels[i+dx+dy] ); 507 } 508 return 0; 509 } 510 511 public String toString() { 512 return "Stylize/Light Effects..."; 513 } 514 515 /** 516 * A class representing material properties. 517 */ 518 public static class Material { 519 int diffuseColor; 520 int specularColor; 521 float ambientIntensity; 522 float diffuseReflectivity; 523 float specularReflectivity; 524 float highlight; 525 float reflectivity; 526 float opacity = 1; 527 528 public Material() { 529 ambientIntensity = 0.5f; 530 diffuseReflectivity = 1.0f; 531 specularReflectivity = 1.0f; 532 highlight = 3.0f; 533 reflectivity = 0.0f; 534 diffuseColor = 0xff888888; 535 specularColor = 0xffffffff; 536 } 537 538 public void setDiffuseColor(int diffuseColor) { 539 this.diffuseColor = diffuseColor; 540 } 541 542 public int getDiffuseColor() { 543 return diffuseColor; 544 } 545 546 public void setOpacity( float opacity ) { 547 this.opacity = opacity; 548 } 549 550 public float getOpacity() { 551 return opacity; 552 } 553 554 } 555 556 public final static int AMBIENT = 0; 557 public final static int DISTANT = 1; 558 public final static int POINT = 2; 559 public final static int SPOT = 3; 560 561 /** 562 * A class representing a light. 563 */ 564 public static class Light implements Cloneable { 565 566 int type = AMBIENT; 567 Vector3f position; 568 Vector3f direction; 569 Color4f realColor = new Color4f(); 570 int color = 0xffffffff; 571 float intensity; 572 float azimuth; 573 float elevation; 574 float focus = 0.5f; 575 float centreX = 0.5f, centreY = 0.5f; 576 float coneAngle = ImageMath.PI/6; 577 float cosConeAngle; 578 float distance = 100.0f; 579 580 public Light() { 581 this(270*ImageMath.PI/180.0f, 0.5235987755982988f, 1.0f); 582 } 583 584 public Light(float azimuth, float elevation, float intensity) { 585 this.azimuth = azimuth; 586 this.elevation = elevation; 587 this.intensity = intensity; 588 } 589 590 public void setAzimuth(float azimuth) { 591 this.azimuth = azimuth; 592 } 593 594 public float getAzimuth() { 595 return azimuth; 596 } 597 598 public void setElevation(float elevation) { 599 this.elevation = elevation; 600 } 601 602 public float getElevation() { 603 return elevation; 604 } 605 606 public void setDistance(float distance) { 607 this.distance = distance; 608 } 609 610 public float getDistance() { 611 return distance; 612 } 613 614 public void setIntensity(float intensity) { 615 this.intensity = intensity; 616 } 617 618 public float getIntensity() { 619 return intensity; 620 } 621 622 public void setConeAngle(float coneAngle) { 623 this.coneAngle = coneAngle; 624 } 625 626 public float getConeAngle() { 627 return coneAngle; 628 } 629 630 public void setFocus(float focus) { 631 this.focus = focus; 632 } 633 634 public float getFocus() { 635 return focus; 636 } 637 638 public void setColor(int color) { 639 this.color = color; 640 } 641 642 public int getColor() { 643 return color; 644 } 645 646 /** 647 * Set the centre of the light in the X direction as a proportion of the image size. 648 * @param centreX the center 649 * @see #getCentreX 650 */ 651 public void setCentreX(float x) { 652 centreX = x; 653 } 654 655 /** 656 * Get the centre of the light in the X direction as a proportion of the image size. 657 * @return the center 658 * @see #setCentreX 659 */ 660 public float getCentreX() { 661 return centreX; 662 } 663 664 /** 665 * Set the centre of the light in the Y direction as a proportion of the image size. 666 * @param centreY the center 667 * @see #getCentreY 668 */ 669 public void setCentreY(float y) { 670 centreY = y; 671 } 672 673 /** 674 * Get the centre of the light in the Y direction as a proportion of the image size. 675 * @return the center 676 * @see #setCentreY 677 */ 678 public float getCentreY() { 679 return centreY; 680 } 681 682 /** 683 * Prepare the light for rendering. 684 * @param width the output image width 685 * @param height the output image height 686 */ 687 public void prepare(int width, int height) { 688 float lx = (float)(Math.cos(azimuth) * Math.cos(elevation)); 689 float ly = (float)(Math.sin(azimuth) * Math.cos(elevation)); 690 float lz = (float)Math.sin(elevation); 691 direction = new Vector3f(lx, ly, lz); 692 direction.normalize(); 693 if (type != DISTANT) { 694 lx *= distance; 695 ly *= distance; 696 lz *= distance; 697 lx += width * centreX; 698 ly += height * centreY; 699 } 700 position = new Vector3f(lx, ly, lz); 701 realColor.set( new Color(color) ); 702 realColor.scale(intensity); 703 cosConeAngle = (float)Math.cos(coneAngle); 704 } 705 706 public Object clone() { 707 try { 708 Light copy = (Light)super.clone(); 709 return copy; 710 } 711 catch (CloneNotSupportedException e) { 712 return null; 713 } 714 } 715 716 public String toString() { 717 return "Light"; 718 } 719 720 } 721 722 public class AmbientLight extends Light { 723 public String toString() { 724 return "Ambient Light"; 725 } 726 } 727 728 public class PointLight extends Light { 729 public PointLight() { 730 type = POINT; 731 } 732 733 public String toString() { 734 return "Point Light"; 735 } 736 } 737 738 public class DistantLight extends Light { 739 public DistantLight() { 740 type = DISTANT; 741 } 742 743 public String toString() { 744 return "Distant Light"; 745 } 746 } 747 748 public class SpotLight extends Light { 749 public SpotLight() { 750 type = SPOT; 751 } 752 753 public String toString() { 754 return "Spotlight"; 755 } 756 } 757}