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}