1/*
2 * Copyright (c) 2009-2012 jMonkeyEngine
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 * * Redistributions of source code must retain the above copyright
10 *   notice, this list of conditions and the following disclaimer.
11 *
12 * * Redistributions in binary form must reproduce the above copyright
13 *   notice, this list of conditions and the following disclaimer in the
14 *   documentation and/or other materials provided with the distribution.
15 *
16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17 *   may be used to endorse or promote products derived from this software
18 *   without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32package com.jme3.shadow;
33
34import com.jme3.bounding.BoundingBox;
35import com.jme3.bounding.BoundingVolume;
36import com.jme3.math.Matrix4f;
37import com.jme3.math.Transform;
38import com.jme3.math.Vector2f;
39import com.jme3.math.Vector3f;
40import com.jme3.renderer.Camera;
41import com.jme3.renderer.queue.GeometryList;
42import com.jme3.scene.Geometry;
43import static java.lang.Math.max;
44import static java.lang.Math.min;
45import java.util.ArrayList;
46import java.util.List;
47
48/**
49 * Includes various useful shadow mapping functions.
50 *
51 * @see
52 * <ul>
53 * <li><a href="http://appsrv.cse.cuhk.edu.hk/~fzhang/pssm_vrcia/">http://appsrv.cse.cuhk.edu.hk/~fzhang/pssm_vrcia/</a></li>
54 * <li><a href="http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html">http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html</a></li>
55 * </ul>
56 * for more info.
57 */
58public class ShadowUtil {
59
60    /**
61     * Updates a points arrays with the frustum corners of the provided camera.
62     * @param viewCam
63     * @param points
64     */
65    public static void updateFrustumPoints2(Camera viewCam, Vector3f[] points) {
66        int w = viewCam.getWidth();
67        int h = viewCam.getHeight();
68        float n = viewCam.getFrustumNear();
69        float f = viewCam.getFrustumFar();
70
71        points[0].set(viewCam.getWorldCoordinates(new Vector2f(0, 0), n));
72        points[1].set(viewCam.getWorldCoordinates(new Vector2f(0, h), n));
73        points[2].set(viewCam.getWorldCoordinates(new Vector2f(w, h), n));
74        points[3].set(viewCam.getWorldCoordinates(new Vector2f(w, 0), n));
75
76        points[4].set(viewCam.getWorldCoordinates(new Vector2f(0, 0), f));
77        points[5].set(viewCam.getWorldCoordinates(new Vector2f(0, h), f));
78        points[6].set(viewCam.getWorldCoordinates(new Vector2f(w, h), f));
79        points[7].set(viewCam.getWorldCoordinates(new Vector2f(w, 0), f));
80    }
81
82    /**
83     * Updates the points array to contain the frustum corners of the given
84     * camera. The nearOverride and farOverride variables can be used
85     * to override the camera's near/far values with own values.
86     *
87     * TODO: Reduce creation of new vectors
88     *
89     * @param viewCam
90     * @param nearOverride
91     * @param farOverride
92     */
93    public static void updateFrustumPoints(Camera viewCam,
94            float nearOverride,
95            float farOverride,
96            float scale,
97            Vector3f[] points) {
98
99        Vector3f pos = viewCam.getLocation();
100        Vector3f dir = viewCam.getDirection();
101        Vector3f up = viewCam.getUp();
102
103        float depthHeightRatio = viewCam.getFrustumTop() / viewCam.getFrustumNear();
104        float near = nearOverride;
105        float far = farOverride;
106        float ftop = viewCam.getFrustumTop();
107        float fright = viewCam.getFrustumRight();
108        float ratio = fright / ftop;
109
110        float near_height;
111        float near_width;
112        float far_height;
113        float far_width;
114
115        if (viewCam.isParallelProjection()) {
116            near_height = ftop;
117            near_width = near_height * ratio;
118            far_height = ftop;
119            far_width = far_height * ratio;
120        } else {
121            near_height = depthHeightRatio * near;
122            near_width = near_height * ratio;
123            far_height = depthHeightRatio * far;
124            far_width = far_height * ratio;
125        }
126
127        Vector3f right = dir.cross(up).normalizeLocal();
128
129        Vector3f temp = new Vector3f();
130        temp.set(dir).multLocal(far).addLocal(pos);
131        Vector3f farCenter = temp.clone();
132        temp.set(dir).multLocal(near).addLocal(pos);
133        Vector3f nearCenter = temp.clone();
134
135        Vector3f nearUp = temp.set(up).multLocal(near_height).clone();
136        Vector3f farUp = temp.set(up).multLocal(far_height).clone();
137        Vector3f nearRight = temp.set(right).multLocal(near_width).clone();
138        Vector3f farRight = temp.set(right).multLocal(far_width).clone();
139
140        points[0].set(nearCenter).subtractLocal(nearUp).subtractLocal(nearRight);
141        points[1].set(nearCenter).addLocal(nearUp).subtractLocal(nearRight);
142        points[2].set(nearCenter).addLocal(nearUp).addLocal(nearRight);
143        points[3].set(nearCenter).subtractLocal(nearUp).addLocal(nearRight);
144
145        points[4].set(farCenter).subtractLocal(farUp).subtractLocal(farRight);
146        points[5].set(farCenter).addLocal(farUp).subtractLocal(farRight);
147        points[6].set(farCenter).addLocal(farUp).addLocal(farRight);
148        points[7].set(farCenter).subtractLocal(farUp).addLocal(farRight);
149
150        if (scale != 1.0f) {
151            // find center of frustum
152            Vector3f center = new Vector3f();
153            for (int i = 0; i < 8; i++) {
154                center.addLocal(points[i]);
155            }
156            center.divideLocal(8f);
157
158            Vector3f cDir = new Vector3f();
159            for (int i = 0; i < 8; i++) {
160                cDir.set(points[i]).subtractLocal(center);
161                cDir.multLocal(scale - 1.0f);
162                points[i].addLocal(cDir);
163            }
164        }
165    }
166
167    /**
168     * Compute bounds of a geomList
169     * @param list
170     * @param transform
171     * @return
172     */
173    public static BoundingBox computeUnionBound(GeometryList list, Transform transform) {
174        BoundingBox bbox = new BoundingBox();
175        for (int i = 0; i < list.size(); i++) {
176            BoundingVolume vol = list.get(i).getWorldBound();
177            BoundingVolume newVol = vol.transform(transform);
178            //Nehon : prevent NaN and infinity values to screw the final bounding box
179            if (!Float.isNaN(newVol.getCenter().x) && !Float.isInfinite(newVol.getCenter().x)) {
180                bbox.mergeLocal(newVol);
181            }
182        }
183        return bbox;
184    }
185
186    /**
187     * Compute bounds of a geomList
188     * @param list
189     * @param mat
190     * @return
191     */
192    public static BoundingBox computeUnionBound(GeometryList list, Matrix4f mat) {
193        BoundingBox bbox = new BoundingBox();
194        BoundingVolume store = null;
195        for (int i = 0; i < list.size(); i++) {
196            BoundingVolume vol = list.get(i).getWorldBound();
197            store = vol.clone().transform(mat, null);
198            //Nehon : prevent NaN and infinity values to screw the final bounding box
199            if (!Float.isNaN(store.getCenter().x) && !Float.isInfinite(store.getCenter().x)) {
200                bbox.mergeLocal(store);
201            }
202        }
203        return bbox;
204    }
205
206    /**
207     * Computes the bounds of multiple bounding volumes
208     * @param bv
209     * @return
210     */
211    public static BoundingBox computeUnionBound(List<BoundingVolume> bv) {
212        BoundingBox bbox = new BoundingBox();
213        for (int i = 0; i < bv.size(); i++) {
214            BoundingVolume vol = bv.get(i);
215            bbox.mergeLocal(vol);
216        }
217        return bbox;
218    }
219
220    /**
221     * Compute bounds from an array of points
222     * @param pts
223     * @param transform
224     * @return
225     */
226    public static BoundingBox computeBoundForPoints(Vector3f[] pts, Transform transform) {
227        Vector3f min = new Vector3f(Vector3f.POSITIVE_INFINITY);
228        Vector3f max = new Vector3f(Vector3f.NEGATIVE_INFINITY);
229        Vector3f temp = new Vector3f();
230        for (int i = 0; i < pts.length; i++) {
231            transform.transformVector(pts[i], temp);
232
233            min.minLocal(temp);
234            max.maxLocal(temp);
235        }
236        Vector3f center = min.add(max).multLocal(0.5f);
237        Vector3f extent = max.subtract(min).multLocal(0.5f);
238        return new BoundingBox(center, extent.x, extent.y, extent.z);
239    }
240
241    /**
242     * Compute bounds from an array of points
243     * @param pts
244     * @param mat
245     * @return
246     */
247    public static BoundingBox computeBoundForPoints(Vector3f[] pts, Matrix4f mat) {
248        Vector3f min = new Vector3f(Vector3f.POSITIVE_INFINITY);
249        Vector3f max = new Vector3f(Vector3f.NEGATIVE_INFINITY);
250        Vector3f temp = new Vector3f();
251
252        for (int i = 0; i < pts.length; i++) {
253            float w = mat.multProj(pts[i], temp);
254
255            temp.x /= w;
256            temp.y /= w;
257            // Why was this commented out?
258            temp.z /= w;
259
260            min.minLocal(temp);
261            max.maxLocal(temp);
262        }
263
264        Vector3f center = min.add(max).multLocal(0.5f);
265        Vector3f extent = max.subtract(min).multLocal(0.5f);
266        //Nehon 08/18/2010 : Added an offset to the extend to avoid banding artifacts when the frustum are aligned
267        return new BoundingBox(center, extent.x + 2.0f, extent.y + 2.0f, extent.z + 2.5f);
268    }
269
270    /**
271     * Updates the shadow camera to properly contain the given
272     * points (which contain the eye camera frustum corners)
273     *
274     * @param shadowCam
275     * @param points
276     */
277    public static void updateShadowCamera(Camera shadowCam, Vector3f[] points) {
278        boolean ortho = shadowCam.isParallelProjection();
279        shadowCam.setProjectionMatrix(null);
280
281        if (ortho) {
282            shadowCam.setFrustum(-1, 1, -1, 1, 1, -1);
283        } else {
284            shadowCam.setFrustumPerspective(45, 1, 1, 150);
285        }
286
287        Matrix4f viewProjMatrix = shadowCam.getViewProjectionMatrix();
288        Matrix4f projMatrix = shadowCam.getProjectionMatrix();
289
290        BoundingBox splitBB = computeBoundForPoints(points, viewProjMatrix);
291
292        Vector3f splitMin = splitBB.getMin(null);
293        Vector3f splitMax = splitBB.getMax(null);
294
295//        splitMin.z = 0;
296
297        // Create the crop matrix.
298        float scaleX, scaleY, scaleZ;
299        float offsetX, offsetY, offsetZ;
300
301        scaleX = 2.0f / (splitMax.x - splitMin.x);
302        scaleY = 2.0f / (splitMax.y - splitMin.y);
303        offsetX = -0.5f * (splitMax.x + splitMin.x) * scaleX;
304        offsetY = -0.5f * (splitMax.y + splitMin.y) * scaleY;
305        scaleZ = 1.0f / (splitMax.z - splitMin.z);
306        offsetZ = -splitMin.z * scaleZ;
307
308        Matrix4f cropMatrix = new Matrix4f(scaleX, 0f, 0f, offsetX,
309                0f, scaleY, 0f, offsetY,
310                0f, 0f, scaleZ, offsetZ,
311                0f, 0f, 0f, 1f);
312
313
314        Matrix4f result = new Matrix4f();
315        result.set(cropMatrix);
316        result.multLocal(projMatrix);
317
318        shadowCam.setProjectionMatrix(result);
319    }
320
321    /**
322     * Updates the shadow camera to properly contain the given
323     * points (which contain the eye camera frustum corners) and the
324     * shadow occluder objects.
325     *
326     * @param occluders
327     * @param receivers
328     * @param shadowCam
329     * @param points
330     */
331    public static void updateShadowCamera(GeometryList occluders,
332            GeometryList receivers,
333            Camera shadowCam,
334            Vector3f[] points) {
335        updateShadowCamera(occluders, receivers, shadowCam, points, null);
336    }
337
338    /**
339     * Updates the shadow camera to properly contain the given
340     * points (which contain the eye camera frustum corners) and the
341     * shadow occluder objects.
342     *
343     * @param occluders
344     * @param shadowCam
345     * @param points
346     */
347    public static void updateShadowCamera(GeometryList occluders,
348            GeometryList receivers,
349            Camera shadowCam,
350            Vector3f[] points,
351            GeometryList splitOccluders) {
352
353        boolean ortho = shadowCam.isParallelProjection();
354
355        shadowCam.setProjectionMatrix(null);
356
357        if (ortho) {
358            shadowCam.setFrustum(-1, 1, -1, 1, 1, -1);
359        } else {
360            shadowCam.setFrustumPerspective(45, 1, 1, 150);
361        }
362
363        // create transform to rotate points to viewspace
364        Matrix4f viewProjMatrix = shadowCam.getViewProjectionMatrix();
365
366        BoundingBox splitBB = computeBoundForPoints(points, viewProjMatrix);
367
368        ArrayList<BoundingVolume> visRecvList = new ArrayList<BoundingVolume>();
369        for (int i = 0; i < receivers.size(); i++) {
370            // convert bounding box to light's viewproj space
371            Geometry receiver = receivers.get(i);
372            BoundingVolume bv = receiver.getWorldBound();
373            BoundingVolume recvBox = bv.transform(viewProjMatrix, null);
374
375            if (splitBB.intersects(recvBox)) {
376                visRecvList.add(recvBox);
377            }
378        }
379
380        ArrayList<BoundingVolume> visOccList = new ArrayList<BoundingVolume>();
381        for (int i = 0; i < occluders.size(); i++) {
382            // convert bounding box to light's viewproj space
383            Geometry occluder = occluders.get(i);
384            BoundingVolume bv = occluder.getWorldBound();
385            BoundingVolume occBox = bv.transform(viewProjMatrix, null);
386
387            boolean intersects = splitBB.intersects(occBox);
388            if (!intersects && occBox instanceof BoundingBox) {
389                BoundingBox occBB = (BoundingBox) occBox;
390                //Kirill 01/10/2011
391                // Extend the occluder further into the frustum
392                // This fixes shadow dissapearing issues when
393                // the caster itself is not in the view camera
394                // but its shadow is in the camera
395                //      The number is in world units
396                occBB.setZExtent(occBB.getZExtent() + 50);
397                occBB.setCenter(occBB.getCenter().addLocal(0, 0, 25));
398                if (splitBB.intersects(occBB)) {
399                    // To prevent extending the depth range too much
400                    // We return the bound to its former shape
401                    // Before adding it
402                    occBB.setZExtent(occBB.getZExtent() - 50);
403                    occBB.setCenter(occBB.getCenter().subtractLocal(0, 0, 25));
404                    visOccList.add(occBox);
405                    if (splitOccluders != null) {
406                        splitOccluders.add(occluder);
407                    }
408                }
409            } else if (intersects) {
410                visOccList.add(occBox);
411                if (splitOccluders != null) {
412                    splitOccluders.add(occluder);
413                }
414            }
415        }
416
417        BoundingBox casterBB = computeUnionBound(visOccList);
418        BoundingBox receiverBB = computeUnionBound(visRecvList);
419
420        //Nehon 08/18/2010 this is to avoid shadow bleeding when the ground is set to only receive shadows
421        if (visOccList.size() != visRecvList.size()) {
422            casterBB.setXExtent(casterBB.getXExtent() + 2.0f);
423            casterBB.setYExtent(casterBB.getYExtent() + 2.0f);
424            casterBB.setZExtent(casterBB.getZExtent() + 2.0f);
425        }
426
427        Vector3f casterMin = casterBB.getMin(null);
428        Vector3f casterMax = casterBB.getMax(null);
429
430        Vector3f receiverMin = receiverBB.getMin(null);
431        Vector3f receiverMax = receiverBB.getMax(null);
432
433        Vector3f splitMin = splitBB.getMin(null);
434        Vector3f splitMax = splitBB.getMax(null);
435
436        splitMin.z = 0;
437
438        if (!ortho) {
439            shadowCam.setFrustumPerspective(45, 1, 1, splitMax.z);
440        }
441
442        Matrix4f projMatrix = shadowCam.getProjectionMatrix();
443
444        Vector3f cropMin = new Vector3f();
445        Vector3f cropMax = new Vector3f();
446
447        // IMPORTANT: Special handling for Z values
448        cropMin.x = max(max(casterMin.x, receiverMin.x), splitMin.x);
449        cropMax.x = min(min(casterMax.x, receiverMax.x), splitMax.x);
450
451        cropMin.y = max(max(casterMin.y, receiverMin.y), splitMin.y);
452        cropMax.y = min(min(casterMax.y, receiverMax.y), splitMax.y);
453
454        cropMin.z = min(casterMin.z, splitMin.z);
455        cropMax.z = min(receiverMax.z, splitMax.z);
456
457
458        // Create the crop matrix.
459        float scaleX, scaleY, scaleZ;
460        float offsetX, offsetY, offsetZ;
461
462        scaleX = (2.0f) / (cropMax.x - cropMin.x);
463        scaleY = (2.0f) / (cropMax.y - cropMin.y);
464
465        offsetX = -0.5f * (cropMax.x + cropMin.x) * scaleX;
466        offsetY = -0.5f * (cropMax.y + cropMin.y) * scaleY;
467
468        scaleZ = 1.0f / (cropMax.z - cropMin.z);
469        offsetZ = -cropMin.z * scaleZ;
470
471
472
473        Matrix4f cropMatrix = new Matrix4f(scaleX, 0f, 0f, offsetX,
474                0f, scaleY, 0f, offsetY,
475                0f, 0f, scaleZ, offsetZ,
476                0f, 0f, 0f, 1f);
477
478
479        Matrix4f result = new Matrix4f();
480        result.set(cropMatrix);
481        result.multLocal(projMatrix);
482
483        shadowCam.setProjectionMatrix(result);
484
485    }
486}
487