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 */ 32 package com.jme3.shadow; 33 34 import com.jme3.bounding.BoundingBox; 35 import com.jme3.bounding.BoundingVolume; 36 import com.jme3.math.Matrix4f; 37 import com.jme3.math.Transform; 38 import com.jme3.math.Vector2f; 39 import com.jme3.math.Vector3f; 40 import com.jme3.renderer.Camera; 41 import com.jme3.renderer.queue.GeometryList; 42 import com.jme3.scene.Geometry; 43 import static java.lang.Math.max; 44 import static java.lang.Math.min; 45 import java.util.ArrayList; 46 import 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 */ 58 public class ShadowUtil { 59 60 /** 61 * Updates a points arrays with the frustum corners of the provided camera. 62 * @param viewCam 63 * @param points 64 */ updateFrustumPoints2(Camera viewCam, Vector3f[] points)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 */ updateFrustumPoints(Camera viewCam, float nearOverride, float farOverride, float scale, Vector3f[] points)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 */ computeUnionBound(GeometryList list, Transform transform)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 */ computeUnionBound(GeometryList list, Matrix4f mat)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 */ computeUnionBound(List<BoundingVolume> bv)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 */ computeBoundForPoints(Vector3f[] pts, Transform transform)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 */ computeBoundForPoints(Vector3f[] pts, Matrix4f mat)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 */ updateShadowCamera(Camera shadowCam, Vector3f[] points)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 */ updateShadowCamera(GeometryList occluders, GeometryList receivers, Camera shadowCam, Vector3f[] points)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 */ updateShadowCamera(GeometryList occluders, GeometryList receivers, Camera shadowCam, Vector3f[] points, GeometryList splitOccluders)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