1 /* $RCSfile$
2  * $Author: hansonr $
3  * $Date: 2015-01-12 22:32:00 -0600 (Mon, 12 Jan 2015) $
4  * $Revision: 20187 $
5  *
6  * Copyright (C) 2003-2005  The Jmol Development Team
7  *
8  * Contact: jmol-developers@lists.sf.net
9  *
10  *  This library is free software; you can redistribute it and/or
11  *  modify it under the terms of the GNU Lesser General Public
12  *  License as published by the Free Software Foundation; either
13  *  version 2.1 of the License, or (at your option) any later version.
14  *
15  *  This library is distributed in the hope that it will be useful,
16  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18  *  Lesser General Public License for more details.
19  *
20  *  You should have received a copy of the GNU Lesser General Public
21  *  License along with this library; if not, write to the Free Software
22  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
23  */
24 
25 package org.jmol.util;
26 
27 import javajs.util.AU;
28 import javajs.util.CU;
29 import javajs.util.M4;
30 import javajs.util.V3;
31 
32 
33 /**
34  *<p>
35  * All functions.
36  * Implements the shading of RGB values to support shadow and lighting
37  * highlights.
38  *</p>
39  *<p>
40  * Each RGB value has 64 shades. shade[0] represents ambient lighting.
41  * shade[63] is white ... a full specular highlight.
42  *</p>
43  *
44  * @author Miguel, miguel@jmol.org
45  * @author Bob Hanson, hansonr@stolaf.edu
46  * @author N David Brown -- cel shading
47  *
48  */
49 public class Shader {
50 
51   // there are 64 shades of a given color
52   // 0 = ambient
53   // 52 = normal
54   // 56 = max for
55   // 63 = specular
56   private final static int SHADE_INDEX_MAX = 64;
57   public final static int SHADE_INDEX_LAST = SHADE_INDEX_MAX - 1;
58   public final static byte SHADE_INDEX_NORMAL = 52;
59   public final static byte SHADE_INDEX_NOISY_LIMIT = 56;
60 
61   // the vwr vector is always {0 0 1}
62   // the light source vector normalized
63 
64   private float xLight, yLight, zLight;
65 
Shader()66   public Shader() {
67     setLightSource(-1f, -1f, 2.5f);
68   }
69 
70   V3 lightSource = new V3();
71 
setLightSource(float x, float y, float z)72   private void setLightSource(float x, float y, float z) {
73     lightSource.set(x, y, z);
74     lightSource.normalize();
75     xLight = lightSource.x;
76     yLight = lightSource.y;
77     zLight = lightSource.z;
78   }
79 
80   boolean specularOn = true;
81   boolean usePhongExponent = false;
82 
83   //fractional distance from black for ambient color
84   int ambientPercent = 45;
85 
86   // df in I = df * (N dot L) + sf * (R dot V)^p
87   int diffusePercent = 84;
88 
89   // log_2(p) in I = df * (N dot L) + sf * (R dot V)^p
90   // for faster calculation of shades
91   int specularExponent = 6;
92 
93   // sf in I = df * (N dot L) + sf * (R dot V)^p
94   // not a percent of anything, really
95   int specularPercent = 22;
96 
97   // fractional distance to white for specular dot
98   int specularPower = 40;
99 
100   // p in I = df * (N dot L) + sf * (R dot V)^p
101   int phongExponent = 64;
102 
103   float ambientFraction = ambientPercent / 100f;
104   float diffuseFactor = diffusePercent / 100f;
105   float intenseFraction = specularPower / 100f;
106   float specularFactor = specularPercent / 100f;
107 
108   private int[][] ashades = AU.newInt2(128);
109   private int[][] ashadesGreyscale;
110   boolean celOn;
111   int celPower = 10;
112   private int celRGB;
113   private float celZ;
114   private boolean useLight;
115 
setCel(boolean celShading, int celShadingPower, int argb)116   void setCel(boolean celShading, int celShadingPower, int argb) {
117     celShading = celShading && celShadingPower != 0;
118     argb = C.getArgb(C.getBgContrast(argb));
119  // problem here is with antialiasDisplay on white background
120     argb = (argb == 0xFF000000 ? 0xFF040404
121           : argb == -1 ? -2
122           : argb + 1);
123     if (celOn == celShading && celRGB == argb && celPower == celShadingPower)
124       return;
125     celOn = celShading;
126     celPower = celShadingPower;
127     useLight = (!celOn || celShadingPower > 0);
128     celZ = 1 - (float) Math.pow(2, -Math.abs(celShadingPower)/10f);
129     celRGB = argb;
130     flushCaches();
131   }
132 
flushCaches()133   void flushCaches() {
134     checkShades(C.colixMax);
135     for (int i = C.colixMax; --i >= 0; )
136       ashades[i] = null;
137     calcSphereShading();
138     for (int i =  maxSphereCache; --i >= 0;)
139       sphereShapeCache[i] = null;
140     ellipsoidShades = null;
141   }
142 
setLastColix(int argb, boolean asGrey)143   public void setLastColix(int argb, boolean asGrey) {
144     C.allocateColix(argb, true);
145     checkShades(C.LAST_AVAILABLE_COLIX);
146     if (asGrey)
147       C.setLastGrey(argb);
148     ashades[C.LAST_AVAILABLE_COLIX] = getShades2(argb, false);
149   }
150 
getShades(short colix)151   int[] getShades(short colix) {
152     checkShades(C.colixMax);
153     colix &= C.OPAQUE_MASK;
154     int[] shades = ashades[colix];
155     if (shades == null)
156       shades = ashades[colix] = getShades2(C.argbs[colix], false);
157     return shades;
158   }
159 
getShadesG(short colix)160   int[] getShadesG(short colix) {
161     checkShades(C.colixMax);
162     colix &= C.OPAQUE_MASK;
163     if (ashadesGreyscale == null)
164       ashadesGreyscale = AU.newInt2(ashades.length);
165     int[] shadesGreyscale = ashadesGreyscale[colix];
166     if (shadesGreyscale == null)
167       shadesGreyscale = ashadesGreyscale[colix] =
168         getShades2(C.argbs[colix], true);
169     return shadesGreyscale;
170   }
171 
checkShades(int n)172   private void checkShades(int n) {
173     if (ashades != null && ashades.length >= n)
174       return;
175     if (n == C.LAST_AVAILABLE_COLIX)
176       n++;
177     ashades = AU.arrayCopyII(ashades, n);
178     if (ashadesGreyscale != null)
179       ashadesGreyscale = AU.arrayCopyII(ashadesGreyscale, n);
180   }
181 
182   /*
183    * intensity calculation:
184    *
185    * af ambientFraction (from ambient percent)
186    * if intenseFraction (from specular power)
187    *
188    * given a color rr gg bb, consider one of these components x:
189    *
190    * int[0:63] shades   [0 .......... 52(normal) ........ 63]
191    *                     af*x........ x ..............x+(255-x)*if
192    *              black  <---ambient%--x---specular power---->  white
193    */
194 
getShades2(int rgb, boolean greyScale)195   private int[] getShades2(int rgb, boolean greyScale) {
196     int[] shades = new int[SHADE_INDEX_MAX];
197     if (rgb == 0)
198       return shades;
199 
200     float red0 = ((rgb >> 16) & 0xFF);
201     float grn0 = ((rgb >> 8) & 0xFF);
202     float blu0 = (rgb & 0xFF);
203 
204     float red = 0;
205     float grn = 0;
206     float blu = 0;
207 
208     float f = ambientFraction;
209 
210     while (true) {
211       red = red0 * f + 0.5f;
212       grn = grn0 * f + 0.5f;
213       blu = blu0 * f + 0.5f;
214       if (f > 0 && red < 4 && grn < 4 && blu < 4) {
215         // with antialiasing, black shades with all
216         // components less than 4 will be considered 0
217         // so we must adjust things just a bit.
218         red0++;
219         grn0++;
220         blu0++;
221         if (f < 0.1f)
222           f += 0.1f;
223         rgb = CU.rgb((int) Math.floor(red0), (int) Math.floor(grn0),
224             (int) Math.floor(blu0));
225         continue;
226       }
227       break;
228     }
229 
230     int i = 0;
231 
232     f = (1 - f) / SHADE_INDEX_NORMAL;
233 
234     float redStep = red0 * f;
235     float grnStep = grn0 * f;
236     float bluStep = blu0 * f;
237 
238     if (celOn) {
239 
240       int max = SHADE_INDEX_MAX / 2;
241 
242       int _rgb = CU.rgb((int) Math.floor(red), (int) Math.floor(grn),
243           (int) Math.floor(blu));
244       if (celPower >= 0)
245         for (; i < max; ++i)
246           shades[i] = _rgb;
247 
248       red += redStep * max;
249       grn += grnStep * max;
250       blu += bluStep * max;
251       _rgb = CU.rgb((int) Math.floor(red), (int) Math.floor(grn),
252           (int) Math.floor(blu));
253       for (; i < SHADE_INDEX_MAX; i++)
254         shades[i] = _rgb;
255 
256       // Min r,g,b is 4,4,4 or else antialiasing bleeds background colour into edges.
257       // 0 and 1 here prevents dithering of the outline
258         shades[0] = shades[1] = celRGB;
259     } else {
260 
261       for (; i < SHADE_INDEX_NORMAL; ++i) {
262         shades[i] = CU.rgb((int) Math.floor(red), (int) Math.floor(grn),
263             (int) Math.floor(blu));
264         red += redStep;
265         grn += grnStep;
266         blu += bluStep;
267       }
268 
269       shades[i++] = rgb;
270 
271       f = intenseFraction / (SHADE_INDEX_MAX - i);
272       redStep = (255.5f - red) * f;
273       grnStep = (255.5f - grn) * f;
274       bluStep = (255.5f - blu) * f;
275 
276       for (; i < SHADE_INDEX_MAX; i++) {
277         red += redStep;
278         grn += grnStep;
279         blu += bluStep;
280         shades[i] = CU.rgb((int) Math.floor(red), (int) Math.floor(grn),
281             (int) Math.floor(blu));
282       }
283     }
284     if (greyScale)
285       for (; --i >= 0;)
286         shades[i] = CU.toFFGGGfromRGB(shades[i]);
287     return shades;
288   }
289 
getShadeIndex(float x, float y, float z)290   public int getShadeIndex(float x, float y, float z) {
291     // from Cylinder3D.calcArgbEndcap and renderCone
292     // from GData.getShadeIndex and getShadeIndex
293     double magnitude = Math.sqrt(x * x + y * y + z * z);
294     return Math.round(getShadeF((float) (x / magnitude),
295         (float) (y / magnitude), (float) (z / magnitude))
296         * SHADE_INDEX_LAST);
297   }
298 
getShadeB(float x, float y, float z)299   public byte getShadeB(float x, float y, float z) {
300     //from Normix3D.setRotationMatrix
301     return (byte) Math.round (getShadeF(x, y, z)
302                   * SHADE_INDEX_LAST);
303   }
304 
getShadeFp8(float x, float y, float z)305   public int getShadeFp8(float x, float y, float z) {
306     //from calcDitheredNoisyShadeIndex (not utilized)
307     //and Cylinder.calcRotatedPoint
308     double magnitude = Math.sqrt(x*x + y*y + z*z);
309     return (int) Math.floor(getShadeF((float)(x/magnitude),
310                                               (float)(y/magnitude),
311                                               (float)(z/magnitude))
312                  * SHADE_INDEX_LAST * (1 << 8));
313   }
314 
getShadeF(float x, float y, float z)315   private float getShadeF(float x, float y, float z) {
316     float NdotL = (useLight ? x * xLight + y * yLight + z * zLight : z);
317     if (NdotL <= 0)
318       return 0;
319     // I = k_diffuse * f_diffuse + k_specular * f_specular
320     // where
321     // k_diffuse = (N dot L)
322     // k_specular = {[(2(N dot L)N - L] dot V}^p
323     //
324     // and in our case V = {0 0 1} so the z component of that is:
325     //
326     // k_specular = ( 2 * NdotL * z - zLight )^p
327     //
328     // HOWEVER -- Jmol's "specularExponent is log_2(phongExponent)
329     //
330     // "specularExponent" phong_exponent
331     // 0 1
332     // 1 2
333     // 2 4
334     // 3 8
335     // 4 16
336     // 5 32
337     // 5.322 40
338     // 6 64
339     // 7 128
340     // 8 256
341     // 9 512
342     // 10 1024
343     float intensity = NdotL * diffuseFactor;
344     if (specularOn) {
345       float k_specular = 2 * NdotL * z - zLight;
346       if (k_specular > 0) {
347         if (usePhongExponent) {
348           k_specular = (float) Math.pow(k_specular, phongExponent);
349         } else {
350           for (int n = specularExponent; --n >= 0
351               && k_specular > .0001f;)
352             k_specular *= k_specular;
353         }
354         intensity += k_specular * specularFactor;
355       }
356     }
357     return (celOn && z < celZ ? 0f : intensity > 1 ? 1f : intensity);
358   }
359 
360   /*
361    byte getDitheredShadeIndex(float x, float y, float z) {
362    //not utilized
363    // add some randomness to prevent banding
364    int fp8ShadeIndex = getFp8ShadeIndex(x, y, z);
365    int shadeIndex = fp8ShadeIndex >> 8;
366    // this cannot overflow because the if the float shadeIndex is 1.0
367    // then shadeIndex will be == shadeLast
368    // but there will be no fractional component, so the next test will fail
369    if ((fp8ShadeIndex & 0xFF) > nextRandom8Bit())
370    ++shadeIndex;
371    int random16bit = seed & 0xFFFF;
372    if (random16bit < 65536 / 3 && shadeIndex > 0)
373    --shadeIndex;
374    else if (random16bit > 65536 * 2 / 3 && shadeIndex < shadeLast)
375    ++shadeIndex;
376    return (byte)shadeIndex;
377    }
378    */
379 
getShadeN(float x, float y, float z, float r)380   public byte getShadeN(float x, float y, float z, float r) {
381     // from Sphere3D only
382     // add some randomness to prevent banding
383     int fp8ShadeIndex = (int) Math.floor(getShadeF(x / r, y / r, z / r)
384         * SHADE_INDEX_LAST * (1 << 8));
385     int shadeIndex = fp8ShadeIndex >> 8;
386     // this cannot overflow because the if the float shadeIndex is 1.0
387     // then shadeIndex will be == shadeLast
388     // but there will be no fractional component, so the next test will fail
389     if (!useLight)
390       return (byte) shadeIndex;
391     if ((fp8ShadeIndex & 0xFF) > nextRandom8Bit())
392       ++shadeIndex;
393     int random16bit = seed & 0xFFFF;
394     if (random16bit < 65536 / 3 && shadeIndex > 0)
395       --shadeIndex;
396     else if (random16bit > 65536 * 2 / 3 && shadeIndex < SHADE_INDEX_LAST)
397       ++shadeIndex;
398     return (byte) shadeIndex;
399   }
400 
401   /*
402     This is a linear congruential pseudorandom number generator,
403     as defined by D. H. Lehmer and described by Donald E. Knuth in
404     The Art of Computer Programming,
405     Volume 2: Seminumerical Algorithms, section 3.2.1.
406 
407   long seed = 1;
408   int nextRandom8Bit() {
409     seed = (seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1);
410     //    return (int)(seed >>> (48 - bits));
411     return (int)(seed >>> 40);
412   }
413   */
414 
415 
416   ////////////////////////////////////////////////////////////////
417   // Sphere shading cache for Large spheres
418   ////////////////////////////////////////////////////////////////
419 
420   public byte[] sphereShadeIndexes = new byte[256 * 256];
421 
calcSphereShading()422   private synchronized void calcSphereShading() {
423     float xF = -127.5f;
424     float r2 = 130 * 130;
425     for (int i = 0; i < 256; ++xF, ++i) {
426       float yF = -127.5f;
427       float xF2 = xF * xF;
428       for (int j = 0; j < 256; ++yF, ++j) {
429         byte shadeIndex = 0;
430         float z2 = r2 - xF2 - yF * yF;
431         if (z2 > 0) {
432           float z = (float) Math.sqrt(z2);
433           shadeIndex = getShadeN(xF, yF, z, 130);
434         }
435         sphereShadeIndexes[(j << 8) + i] = shadeIndex;
436       }
437     }
438   }
439 
440   /*
441   byte getSphereshadeIndex(int x, int y, int r) {
442     int d = 2*r + 1;
443     x += r;
444     if (x < 0)
445       x = 0;
446     int x8 = (x << 8) / d;
447     if (x8 > 0xFF)
448       x8 = 0xFF;
449     y += r;
450     if (y < 0)
451       y = 0;
452     int y8 = (y << 8) / d;
453     if (y8 > 0xFF)
454       y8 = 0xFF;
455     return sphereShadeIndexes[(y8 << 8) + x8];
456   }
457   */
458 
459   // this doesn't really need to be synchronized
460   // no serious harm done if two threads write seed at the same time
461   private int seed = 0x12345679; // turn lo bit on
462   /**
463    *<p>
464    * Implements RANDU algorithm for random noise in lighting/shading.
465    *</p>
466    *<p>
467    * RANDU is the classic example of a poor random number generator.
468    * But it is very cheap to calculate and is good enough for our purposes.
469    *</p>
470    *
471    * @return Next random
472    */
nextRandom8Bit()473   public int nextRandom8Bit() {
474     int t = seed;
475     seed = t = ((t << 16) + (t << 1) + t) & 0x7FFFFFFF;
476     return t >> 23;
477   }
478 
479   private final static int SLIM = 20;
480   private final static int SDIM = SLIM * 2;
481   public final static int maxSphereCache = 128;
482   public int[][] sphereShapeCache = AU.newInt2(maxSphereCache);
483   public byte[][][] ellipsoidShades;
484   public int nOut;
485   public int nIn;
486 
487   // Cel shading.
488   // @author N David Brown
489 
getEllipsoidShade(float x, float y, float z, int radius, M4 mDeriv)490   public int getEllipsoidShade(float x, float y, float z, int radius,
491                                        M4 mDeriv) {
492     float tx = mDeriv.m00 * x + mDeriv.m01 * y + mDeriv.m02 * z + mDeriv.m03;
493     float ty = mDeriv.m10 * x + mDeriv.m11 * y + mDeriv.m12 * z + mDeriv.m13;
494     float tz = mDeriv.m20 * x + mDeriv.m21 * y + mDeriv.m22 * z + mDeriv.m23;
495     float f = Math.min(radius/2f, 45) /
496         (float) Math.sqrt(tx * tx + ty * ty + tz * tz);
497     // optimized for about 30-100% inclusion
498     int i = (int) (-tx * f);
499     int j = (int) (-ty * f);
500     int k = (int) (tz * f);
501     boolean outside = i < -SLIM || i >= SLIM || j < -SLIM || j >= SLIM
502         || k < 0 || k >= SDIM;
503     if (outside) {
504       while (i % 2 == 0 && j % 2 == 0 && k % 2 == 0 && i + j + k > 0) {
505         i >>= 1;
506         j >>= 1;
507         k >>= 1;
508       }
509       outside = i < -SLIM || i >= SLIM || j < -SLIM || j >= SLIM || k < 0
510           || k >= SDIM;
511     }
512 
513     if (outside)
514       nOut++;
515     else
516       nIn++;
517 
518     return (outside ? getShadeIndex(i, j, k)
519         : ellipsoidShades[i + SLIM][j + SLIM][k]);
520   }
521 
createEllipsoidShades()522   public void createEllipsoidShades() {
523 
524     // we don't need to cache rear-directed normals (kk < 0)
525 
526     ellipsoidShades = new byte[SDIM][SDIM][SDIM];
527     for (int ii = 0; ii < SDIM; ii++)
528       for (int jj = 0; jj < SDIM; jj++)
529         for (int kk = 0; kk < SDIM; kk++)
530           ellipsoidShades[ii][jj][kk] = (byte) getShadeIndex(ii - SLIM, jj
531               - SLIM, kk);
532   }
533 
534 //  /**
535 //   * not implemented; just experimenting here
536 //   * @param pbuf
537 //   * @param zbuf
538 //   * @param aobuf
539 //   * @param width
540 //   * @param height
541 //   * @param ambientOcclusion
542 //   */
543 //  public void occludePixels(int[] pbuf, int[] zbuf, int[] aobuf, int width,
544 //                            int height, int ambientOcclusion) {
545 //    int n = zbuf.length;
546 //    for (int x = 0, y = 0, offset = 0; offset < n; offset++) {
547 //      int z = zbuf[offset];
548 //      int xymax = Math.min(z >> 5, 0);
549 //      if (xymax == 0)
550 //        continue;
551 //      int r2max = xymax * xymax;
552 //      int pxmax = Math.min(width, x + xymax);
553 //      int pymax = Math.min(height, y + xymax);
554 //      for (int px = Math.max(0, x - xymax); px < pxmax; px++) {
555 //        for (int py = Math.max(0, y - xymax); py < pymax; py++) {
556 //          int dx = px - x;
557 //          int dy = py - y;
558 //          int r2 = dx * dx + dy * dy;
559 //          if (r2 > r2max)
560 //            continue;
561 //          int pt = offset + width * dy + dx;
562 //          int dz = zbuf[pt] - z;
563 //          if(dz <= z || dz * dz > r2)
564 //            continue;
565 //          // TODO
566 //        }
567 //      }
568 //
569 //
570 //      if (++x == width) {
571 //        x = 0;
572 //        y++;
573 //      }
574 //    }
575 //  }
576 
577 }
578