├── build.py ├── exit-fullscreen.png ├── framework.coffee ├── framework.js ├── fullscreen.png ├── hard-shadow.coffee ├── hard-shadow.js ├── hard-shadow.png ├── index.html ├── jquery.js ├── lerp-shadow.coffee ├── lerp-shadow.js ├── lerp-shadow.png ├── markup ├── meshes.coffee ├── meshes.js ├── no-shadow.coffee ├── no-shadow.js ├── no-shadow.png ├── pcf-lerp-shadow.coffee ├── pcf-lerp-shadow.js ├── pcf-lerp-shadow.png ├── pcf-shadow.coffee ├── pcf-shadow.js ├── pcf-shadow.png ├── vsm-filtered-shadow.coffee ├── vsm-filtered-shadow.js ├── vsm-filtered-shadow.png ├── vsm-shadow.coffee ├── vsm-shadow.js ├── vsm-shadow.png ├── webgl-nuke-vendor-prefix.coffee ├── webgl-nuke-vendor-prefix.js ├── webgl-texture-float-extension-shims.coffee └── webgl-texture-float-extension-shims.js /build.py: -------------------------------------------------------------------------------- 1 | from marshal import loads, dumps 2 | from os import walk, stat 3 | from os.path import exists, join, realpath, dirname, splitext, basename 4 | from stat import ST_MTIME 5 | from subprocess import Popen, PIPE 6 | from datetime import datetime 7 | from sys import argv, stdout 8 | from tarfile import TarFile 9 | 10 | try: 11 | import json 12 | jsonEncode = json.dumps 13 | except ImportError: 14 | try: 15 | import cjson 16 | jsonEncode = cjson.encode 17 | except ImportError: 18 | try: 19 | import simplejson 20 | jsonEncode = simplejson.dumps 21 | except ImportError: 22 | sys.exit(-1) 23 | 24 | __dir__ = dirname(realpath(__file__)) 25 | 26 | message_count = 0 27 | 28 | class CoffeeError(Exception): pass 29 | 30 | def message(text): 31 | global message_count 32 | now = datetime.now().strftime('%H:%M:%S') 33 | print '[%04i %s] %s' % (message_count, now, text) 34 | message_count+=1 35 | 36 | def error(text): 37 | stdout.write('\x1b[31m%s\x1b[39m' % text) 38 | stdout.flush() 39 | 40 | def modified(path): 41 | return stat(path)[ST_MTIME] 42 | 43 | def suffix(items, suffix): 44 | result = [] 45 | for item in items: 46 | if item.endswith(suffix): 47 | result.append(item) 48 | return result 49 | 50 | def prefix(items, prefix): 51 | result = [] 52 | for item in items: 53 | if item.startswith(prefix): 54 | result.append(item) 55 | return result 56 | 57 | def files(directory): 58 | result = [] 59 | for root, dirs, files in walk(directory): 60 | for file in files: 61 | result.append(join(root, file)) 62 | return result 63 | 64 | def preprocess(source, name): 65 | result = [] 66 | for lineno, line in enumerate(source.split('\n')): 67 | line = line.replace('//essl', '#line %i %s' % (lineno+1, basename(name))) 68 | result.append(line) 69 | return '\n'.join(result) 70 | 71 | def coffee_compile(name): 72 | message('compiling: %s' % name) 73 | source = open(name).read() 74 | source = preprocess(source, name) 75 | output_name = splitext(name)[0] + '.js' 76 | command = ['coffee', '--stdio', '--print'] 77 | process = Popen(command, stdin=PIPE, stdout=PIPE, stderr=PIPE) 78 | out, err = process.communicate(source) 79 | if process.returncode: 80 | error(err) 81 | raise CoffeeError(err) 82 | else: 83 | outfile = open(output_name, 'w') 84 | outfile.write(out) 85 | outfile.close() 86 | 87 | def coffee_cache(filelist): 88 | coffees = set(suffix(filelist, '.coffee')) 89 | for name in coffees: 90 | coffee_compile(name) 91 | 92 | if __name__ == '__main__': 93 | filelist = files(__dir__) 94 | cache = coffee_cache(filelist) 95 | -------------------------------------------------------------------------------- /exit-fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyalot/soft-shadow-mapping/e425c99c93a11f2b46f01b37bf47e10069133144/exit-fullscreen.png -------------------------------------------------------------------------------- /framework.coffee: -------------------------------------------------------------------------------- 1 | pi = Math.PI 2 | tau = 2*pi 3 | deg = 360/tau 4 | arc = tau/360 5 | 6 | class Mat3 7 | constructor: (@data) -> 8 | @data ?= new Float32Array 9 9 | @ident() 10 | 11 | ident: -> 12 | d = @data 13 | d[0] = 1; d[1] =0; d[2] = 0 14 | d[3] = 0; d[4] =1; d[5] = 0 15 | d[6] = 0; d[7] =0; d[8] = 1 16 | return @ 17 | 18 | transpose: -> 19 | d = @data 20 | a01 = d[1]; a02 = d[2]; a12 = d[5] 21 | 22 | d[1] = d[3] 23 | d[2] = d[6] 24 | d[3] = a01 25 | d[5] = d[7] 26 | d[6] = a02 27 | d[7] = a12 28 | return @ 29 | 30 | mulVec3: (vec, dst=vec) -> 31 | @mulVal3 vec.x, vec.y, vec.z, dst 32 | return dst 33 | 34 | mulVal3: (x, y, z, dst) -> 35 | dst = dst.data 36 | d = @data 37 | dst[0] = d[0]*x + d[3]*y + d[6]*z 38 | dst[1] = d[1]*x + d[4]*y + d[7]*z 39 | dst[2] = d[2]*x + d[5]*y + d[8]*z 40 | 41 | return @ 42 | 43 | rotatex: (angle) -> 44 | s = Math.sin angle*arc 45 | c = Math.cos angle*arc 46 | return @amul( 47 | 1, 0, 0, 48 | 0, c, s, 49 | 0, -s, c 50 | ) 51 | 52 | rotatey: (angle) -> 53 | s = Math.sin angle*arc 54 | c = Math.cos angle*arc 55 | return @amul( 56 | c, 0, -s, 57 | 0, 1, 0, 58 | s, 0, c 59 | ) 60 | 61 | rotatez: (angle) -> 62 | s = Math.sin angle*arc 63 | c = Math.cos angle*arc 64 | return @amul( 65 | c, s, 0, 66 | -s, c, 0, 67 | 0, 0, 1 68 | ) 69 | 70 | amul: ( 71 | b00, b10, b20, 72 | b01, b11, b21, 73 | b02, b12, b22, 74 | b03, b13, b23 75 | ) -> 76 | a = @data 77 | 78 | a00 = a[0] 79 | a10 = a[1] 80 | a20 = a[2] 81 | 82 | a01 = a[3] 83 | a11 = a[4] 84 | a21 = a[5] 85 | 86 | a02 = a[6] 87 | a12 = a[7] 88 | a22 = a[8] 89 | 90 | a[0] = a00*b00 + a01*b10 + a02*b20 91 | a[1] = a10*b00 + a11*b10 + a12*b20 92 | a[2] = a20*b00 + a21*b10 + a22*b20 93 | 94 | a[3] = a00*b01 + a01*b11 + a02*b21 95 | a[4] = a10*b01 + a11*b11 + a12*b21 96 | a[5] = a20*b01 + a21*b11 + a22*b21 97 | 98 | a[6] = a00*b02 + a01*b12 + a02*b22 99 | a[7] = a10*b02 + a11*b12 + a12*b22 100 | a[8] = a20*b02 + a21*b12 + a22*b22 101 | 102 | return @ 103 | 104 | fromMat4Rot: (source) -> source.toMat3Rot @ 105 | 106 | log: -> 107 | d = @data 108 | console.log '%f, %f, %f,\n%f, %f, %f, \n%f, %f, %f, ', d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8] 109 | 110 | class Mat4 111 | constructor: (@data) -> 112 | @data ?= new Float32Array 16 113 | @ident() 114 | 115 | ident: -> 116 | d = @data 117 | d[0] = 1; d[1] =0; d[2] = 0; d[3] = 0 118 | d[4] = 0; d[5] =1; d[6] = 0; d[7] = 0 119 | d[8] = 0; d[9] =0; d[10] = 1; d[11] = 0 120 | d[12] = 0; d[13] =0; d[14] = 0; d[15] = 1 121 | return @ 122 | 123 | zero: -> 124 | d = @data 125 | d[0] = 0; d[1] =0; d[2] = 0; d[3] = 0 126 | d[4] = 0; d[5] =0; d[6] = 0; d[7] = 0 127 | d[8] = 0; d[9] =0; d[10] = 0; d[11] = 0 128 | d[12] = 0; d[13] =0; d[14] = 0; d[15] = 0 129 | return @ 130 | 131 | copy: (dest) -> 132 | src = @data 133 | dst = dest.data 134 | dst[0] = src[0] 135 | dst[1] = src[1] 136 | dst[2] = src[2] 137 | dst[3] = src[3] 138 | dst[4] = src[4] 139 | dst[5] = src[5] 140 | dst[6] = src[6] 141 | dst[7] = src[7] 142 | dst[8] = src[8] 143 | dst[9] = src[9] 144 | dst[10] = src[10] 145 | dst[11] = src[11] 146 | dst[12] = src[12] 147 | dst[13] = src[13] 148 | dst[14] = src[14] 149 | dst[15] = src[15] 150 | return dest 151 | 152 | toMat3: (dest) -> 153 | src = @data 154 | dst = dest.data 155 | dst[0] = src[0] 156 | dst[1] = src[1] 157 | dst[2] = src[2] 158 | dst[3] = src[4] 159 | dst[4] = src[5] 160 | dst[5] = src[6] 161 | dst[6] = src[8] 162 | dst[7] = src[9] 163 | dst[8] = src[10] 164 | 165 | return dest 166 | 167 | toMat3Rot: (dest) -> 168 | dst = dest.data 169 | src = @data 170 | a00 = src[0]; a01 = src[1]; a02 = src[2] 171 | a10 = src[4]; a11 = src[5]; a12 = src[6] 172 | a20 = src[8]; a21 = src[9]; a22 = src[10] 173 | 174 | b01 = a22 * a11 - a12 * a21 175 | b11 = -a22 * a10 + a12 * a20 176 | b21 = a21 * a10 - a11 * a20 177 | 178 | d = a00 * b01 + a01 * b11 + a02 * b21 179 | id = 1 / d 180 | 181 | dst[0] = b01 * id 182 | dst[3] = (-a22 * a01 + a02 * a21) * id 183 | dst[6] = (a12 * a01 - a02 * a11) * id 184 | dst[1] = b11 * id 185 | dst[4] = (a22 * a00 - a02 * a20) * id 186 | dst[7] = (-a12 * a00 + a02 * a10) * id 187 | dst[2] = b21 * id 188 | dst[5] = (-a21 * a00 + a01 * a20) * id 189 | dst[8] = (a11 * a00 - a01 * a10) * id 190 | 191 | return dest 192 | 193 | perspective: ({fov, aspect, near, far}) -> 194 | fov ?= 60 195 | aspect ?= 1 196 | near ?= 0.01 197 | far ?= 100 198 | 199 | @zero() 200 | d = @data 201 | top = near * Math.tan(fov*Math.PI/360) 202 | right = top*aspect 203 | left = -right 204 | bottom = -top 205 | 206 | d[0] = (2*near)/(right-left) 207 | d[5] = (2*near)/(top-bottom) 208 | d[8] = (right+left)/(right-left) 209 | d[9] = (top+bottom)/(top-bottom) 210 | d[10] = -(far+near)/(far-near) 211 | d[11] = -1 212 | d[14] = -(2*far*near)/(far-near) 213 | 214 | return @ 215 | 216 | inversePerspective: (fov, aspect, near, far) -> 217 | @zero() 218 | dst = @data 219 | top = near * Math.tan(fov*Math.PI/360) 220 | right = top*aspect 221 | left = -right 222 | bottom = -top 223 | 224 | dst[0] = (right-left)/(2*near) 225 | dst[5] = (top-bottom)/(2*near) 226 | dst[11] = -(far-near)/(2*far*near) 227 | dst[12] = (right+left)/(2*near) 228 | dst[13] = (top+bottom)/(2*near) 229 | dst[14] = -1 230 | dst[15] = (far+near)/(2*far*near) 231 | 232 | return @ 233 | 234 | ortho: (near=-1, far=1, top=-1, bottom=1, left=-1, right=1) -> 235 | rl = right-left 236 | tb = top - bottom 237 | fn = far - near 238 | 239 | return @set( 240 | 2/rl, 0, 0, -(left+right)/rl, 241 | 0, 2/tb, 0, -(top+bottom)/tb, 242 | 0, 0, -2/fn, -(far+near)/fn, 243 | 0, 0, 0, 1, 244 | ) 245 | 246 | inverseOrtho: (near=-1, far=1, top=-1, bottom=1, left=-1, right=1) -> 247 | a = (right-left)/2 248 | b = (right+left)/2 249 | c = (top-bottom)/2 250 | d = (top+bottom)/2 251 | e = (far-near)/-2 252 | f = (near+far)/2 253 | g = 1 254 | 255 | return @set( 256 | a, 0, 0, b, 257 | 0, c, 0, d, 258 | 0, 0, e, f, 259 | 0, 0, 0, g 260 | ) 261 | 262 | fromRotationTranslation: (quat, vec) -> 263 | x = quat.x; y = quat.y; z = quat.z; w = quat.w 264 | x2 = x + x 265 | y2 = y + y 266 | z2 = z + z 267 | 268 | xx = x * x2 269 | xy = x * y2 270 | xz = x * z2 271 | yy = y * y2 272 | yz = y * z2 273 | zz = z * z2 274 | wx = w * x2 275 | wy = w * y2 276 | wz = w * z2 277 | 278 | dest = @data 279 | 280 | dest[0] = 1 - (yy + zz) 281 | dest[1] = xy + wz 282 | dest[2] = xz - wy 283 | dest[3] = 0 284 | dest[4] = xy - wz 285 | dest[5] = 1 - (xx + zz) 286 | dest[6] = yz + wx 287 | dest[7] = 0 288 | dest[8] = xz + wy 289 | dest[9] = yz - wx 290 | dest[10] = 1 - (xx + yy) 291 | dest[11] = 0 292 | 293 | dest[12] = vec.x 294 | dest[13] = vec.y 295 | dest[14] = vec.z 296 | dest[15] = 1 297 | 298 | return @ 299 | 300 | trans: (x, y, z) -> 301 | d = @data 302 | a00 = d[0]; a01 = d[1]; a02 = d[2]; a03 = d[3] 303 | a10 = d[4]; a11 = d[5]; a12 = d[6]; a13 = d[7] 304 | a20 = d[8]; a21 = d[9]; a22 = d[10]; a23 = d[11] 305 | 306 | d[12] = a00 * x + a10 * y + a20 * z + d[12] 307 | d[13] = a01 * x + a11 * y + a21 * z + d[13] 308 | d[14] = a02 * x + a12 * y + a22 * z + d[14] 309 | d[15] = a03 * x + a13 * y + a23 * z + d[15] 310 | 311 | return @ 312 | 313 | rotatex: (angle) -> 314 | d = @data 315 | rad = tau*(angle/360) 316 | s = Math.sin rad 317 | c = Math.cos rad 318 | 319 | a10 = d[4] 320 | a11 = d[5] 321 | a12 = d[6] 322 | a13 = d[7] 323 | a20 = d[8] 324 | a21 = d[9] 325 | a22 = d[10] 326 | a23 = d[11] 327 | 328 | d[4] = a10 * c + a20 * s 329 | d[5] = a11 * c + a21 * s 330 | d[6] = a12 * c + a22 * s 331 | d[7] = a13 * c + a23 * s 332 | 333 | d[8] = a10 * -s + a20 * c 334 | d[9] = a11 * -s + a21 * c 335 | d[10] = a12 * -s + a22 * c 336 | d[11] = a13 * -s + a23 * c 337 | 338 | return @ 339 | 340 | rotatey: (angle) -> 341 | d = @data 342 | rad = tau*(angle/360) 343 | s = Math.sin rad 344 | c = Math.cos rad 345 | 346 | a00 = d[0] 347 | a01 = d[1] 348 | a02 = d[2] 349 | a03 = d[3] 350 | a20 = d[8] 351 | a21 = d[9] 352 | a22 = d[10] 353 | a23 = d[11] 354 | 355 | d[0] = a00 * c + a20 * -s 356 | d[1] = a01 * c + a21 * -s 357 | d[2] = a02 * c + a22 * -s 358 | d[3] = a03 * c + a23 * -s 359 | 360 | d[8] = a00 * s + a20 * c 361 | d[9] = a01 * s + a21 * c 362 | d[10] = a02 * s + a22 * c 363 | d[11] = a03 * s + a23 * c 364 | 365 | return @ 366 | 367 | rotatez: (angle) -> 368 | d = @data 369 | rad = tau*(angle/360) 370 | s = Math.sin rad 371 | c = Math.cos rad 372 | 373 | a00 = d[0] 374 | a01 = d[1] 375 | a02 = d[2] 376 | a03 = d[3] 377 | a10 = d[4] 378 | a11 = d[5] 379 | a12 = d[6] 380 | a13 = d[7] 381 | 382 | d[0] = a00 * c + a10 * s 383 | d[1] = a01 * c + a11 * s 384 | d[2] = a02 * c + a12 * s 385 | d[3] = a03 * c + a13 * s 386 | d[4] = a00 * -s + a10 * c 387 | d[5] = a01 * -s + a11 * c 388 | d[6] = a02 * -s + a12 * c 389 | d[7] = a03 * -s + a13 * c 390 | 391 | return @ 392 | 393 | scale: (scalar) -> 394 | d = @data 395 | 396 | a00 = d[0]; a01 = d[1]; a02 = d[2]; a03 = d[3] 397 | a10 = d[4]; a11 = d[5]; a12 = d[6]; a13 = d[7] 398 | a20 = d[8]; a21 = d[9]; a22 = d[10]; a23 = d[11] 399 | 400 | d[0] = a00 * scalar 401 | d[1] = a01 * scalar 402 | d[2] = a02 * scalar 403 | d[3] = a03 * scalar 404 | 405 | d[4] = a10 * scalar 406 | d[5] = a11 * scalar 407 | d[6] = a12 * scalar 408 | d[7] = a13 * scalar 409 | 410 | d[8] = a20 * scalar 411 | d[9] = a21 * scalar 412 | d[10] = a22 * scalar 413 | d[11] = a23 * scalar 414 | 415 | return @ 416 | 417 | mulMat4: (other, dst=@) -> 418 | dest = dst.data 419 | mat = @data 420 | mat2 = other.data 421 | 422 | a00 = mat[ 0]; a01 = mat[ 1]; a02 = mat[ 2]; a03 = mat[3] 423 | a10 = mat[ 4]; a11 = mat[ 5]; a12 = mat[ 6]; a13 = mat[7] 424 | a20 = mat[ 8]; a21 = mat[ 9]; a22 = mat[10]; a23 = mat[11] 425 | a30 = mat[12]; a31 = mat[13]; a32 = mat[14]; a33 = mat[15] 426 | 427 | b0 = mat2[0]; b1 = mat2[1]; b2 = mat2[2]; b3 = mat2[3] 428 | dest[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30 429 | dest[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31 430 | dest[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32 431 | dest[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33 432 | 433 | b0 = mat2[4] 434 | b1 = mat2[5] 435 | b2 = mat2[6] 436 | b3 = mat2[7] 437 | dest[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30 438 | dest[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31 439 | dest[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32 440 | dest[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33 441 | 442 | b0 = mat2[8] 443 | b1 = mat2[9] 444 | b2 = mat2[10] 445 | b3 = mat2[11] 446 | dest[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30 447 | dest[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31 448 | dest[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32 449 | dest[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33 450 | 451 | b0 = mat2[12] 452 | b1 = mat2[13] 453 | b2 = mat2[14] 454 | b3 = mat2[15] 455 | dest[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30 456 | dest[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31 457 | dest[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32 458 | dest[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33 459 | 460 | return dst 461 | 462 | mulVec3: (vec, dst=vec) -> 463 | return @mulVal3 vec.x, vec.y, vec.z, dst 464 | 465 | mulVal3: (x, y, z, dst) -> 466 | dst = dst.data 467 | d = @data 468 | dst[0] = d[0]*x + d[4]*y + d[8] *z 469 | dst[1] = d[1]*x + d[5]*y + d[9] *z 470 | dst[2] = d[2]*x + d[6]*y + d[10]*z 471 | 472 | return dst 473 | 474 | mulVec4: (vec, dst) -> 475 | dst ?= vec 476 | return @mulVal4 vec.x, vec.y, vec.z, vec.w, dst 477 | 478 | mulVal4: (x, y, z, w, dst) -> 479 | dst = dst.data 480 | d = @data 481 | dst[0] = d[0]*x + d[4]*y + d[8] *z + d[12]*w 482 | dst[1] = d[1]*x + d[5]*y + d[9] *z + d[13]*w 483 | dst[2] = d[2]*x + d[6]*y + d[10]*z + d[14]*w 484 | dst[3] = d[3]*x + d[7]*y + d[11]*z + d[15]*w 485 | 486 | return dst 487 | 488 | invert: (dst=@) -> 489 | mat = @data 490 | dest = dst.data 491 | 492 | a00 = mat[0]; a01 = mat[1]; a02 = mat[2]; a03 = mat[3] 493 | a10 = mat[4]; a11 = mat[5]; a12 = mat[6]; a13 = mat[7] 494 | a20 = mat[8]; a21 = mat[9]; a22 = mat[10]; a23 = mat[11] 495 | a30 = mat[12]; a31 = mat[13]; a32 = mat[14]; a33 = mat[15] 496 | 497 | b00 = a00 * a11 - a01 * a10 498 | b01 = a00 * a12 - a02 * a10 499 | b02 = a00 * a13 - a03 * a10 500 | b03 = a01 * a12 - a02 * a11 501 | b04 = a01 * a13 - a03 * a11 502 | b05 = a02 * a13 - a03 * a12 503 | b06 = a20 * a31 - a21 * a30 504 | b07 = a20 * a32 - a22 * a30 505 | b08 = a20 * a33 - a23 * a30 506 | b09 = a21 * a32 - a22 * a31 507 | b10 = a21 * a33 - a23 * a31 508 | b11 = a22 * a33 - a23 * a32 509 | 510 | d = (b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06) 511 | 512 | if d==0 then return 513 | invDet = 1 / d 514 | 515 | dest[0] = (a11 * b11 - a12 * b10 + a13 * b09) * invDet 516 | dest[1] = (-a01 * b11 + a02 * b10 - a03 * b09) * invDet 517 | dest[2] = (a31 * b05 - a32 * b04 + a33 * b03) * invDet 518 | dest[3] = (-a21 * b05 + a22 * b04 - a23 * b03) * invDet 519 | dest[4] = (-a10 * b11 + a12 * b08 - a13 * b07) * invDet 520 | dest[5] = (a00 * b11 - a02 * b08 + a03 * b07) * invDet 521 | dest[6] = (-a30 * b05 + a32 * b02 - a33 * b01) * invDet 522 | dest[7] = (a20 * b05 - a22 * b02 + a23 * b01) * invDet 523 | dest[8] = (a10 * b10 - a11 * b08 + a13 * b06) * invDet 524 | dest[9] = (-a00 * b10 + a01 * b08 - a03 * b06) * invDet 525 | dest[10] = (a30 * b04 - a31 * b02 + a33 * b00) * invDet 526 | dest[11] = (-a20 * b04 + a21 * b02 - a23 * b00) * invDet 527 | dest[12] = (-a10 * b09 + a11 * b07 - a12 * b06) * invDet 528 | dest[13] = (a00 * b09 - a01 * b07 + a02 * b06) * invDet 529 | dest[14] = (-a30 * b03 + a31 * b01 - a32 * b00) * invDet 530 | dest[15] = (a20 * b03 - a21 * b01 + a22 * b00) * invDet 531 | 532 | return dst 533 | 534 | set: ( 535 | a00, a10, a20, a30, 536 | a01, a11, a21, a31, 537 | a02, a12, a22, a32, 538 | a03, a13, a23, a33, 539 | ) -> 540 | d = @data 541 | d[0]=a00; d[4]=a10; d[8]=a20; d[12]=a30 542 | d[1]=a01; d[5]=a11; d[9]=a21; d[13]=a31 543 | d[2]=a02; d[6]=a12; d[10]=a22; d[14]=a32 544 | d[3]=a03; d[7]=a13; d[11]=a23; d[15]=a33 545 | 546 | return @ 547 | 548 | class Shader 549 | boilerplate = ''' 550 | #ifdef GL_FRAGMENT_PRECISION_HIGH 551 | precision highp int; 552 | precision highp float; 553 | #else 554 | precision mediump int; 555 | precision mediump float; 556 | #endif 557 | #define PI 3.141592653589793 558 | #define TAU 6.283185307179586 559 | #define PIH 1.5707963267948966 560 | ''' 561 | constructor: (@framework, {common, vertex, fragment}) -> 562 | @gl = @framework.gl 563 | 564 | @program = @gl.createProgram() 565 | @vs = @gl.createShader @gl.VERTEX_SHADER 566 | @fs = @gl.createShader @gl.FRAGMENT_SHADER 567 | @gl.attachShader @program, @vs 568 | @gl.attachShader @program, @fs 569 | 570 | common ?= '' 571 | @compileShader @vs, [common, vertex].join('\n') 572 | @compileShader @fs, [common, fragment].join('\n') 573 | @link() 574 | 575 | @uniformCache = {} 576 | @attributeCache = {} 577 | @samplers = {} 578 | @unitCounter = 0 579 | 580 | compileShader: (shader, source) -> 581 | source = [boilerplate, source].join('\n') 582 | [source, lines] = @preprocess source 583 | 584 | @gl.shaderSource shader, source 585 | @gl.compileShader shader 586 | 587 | if not @gl.getShaderParameter shader, @gl.COMPILE_STATUS 588 | error = @gl.getShaderInfoLog(shader) 589 | throw @translateError error, lines 590 | 591 | preprocess: (source) -> 592 | lines = [] 593 | result = [] 594 | filename = 'no file' 595 | lineno = 1 596 | for line in source.split('\n') 597 | match = line.match /#line (\d+) (.*)/ 598 | if match 599 | lineno = parseInt(match[1], 10)+1 600 | filename = match[2] 601 | else 602 | lines.push 603 | source: line 604 | lineno: lineno 605 | filename: filename 606 | result.push line 607 | lineno += 1 608 | return [result.join('\n'), lines] 609 | 610 | translateError: (error, lines) -> 611 | result = ['Shader Compile Error'] 612 | for line, i in error.split('\n') 613 | match = line.match /ERROR: \d+:(\d+): (.*)/ 614 | if match 615 | lineno = parseFloat(match[1])-1 616 | message = match[2] 617 | sourceline = lines[lineno] 618 | result.push "File \"#{sourceline.filename}\", Line #{sourceline.lineno}, #{message}" 619 | result.push " #{sourceline.source}" 620 | else 621 | result.push line 622 | return result.join('\n') 623 | 624 | link: -> 625 | @gl.linkProgram @program 626 | 627 | if not @gl.getProgramParameter @program, @gl.LINK_STATUS 628 | throw "Shader Link Error: #{@gl.getProgramInfoLog(@program)}" 629 | 630 | attributeLocation: (name) -> 631 | location = @attributeCache[name] 632 | if location is undefined 633 | location = @attributeCache[name] = @gl.getAttribLocation @program, name 634 | return location 635 | 636 | uniformLocation: (name) -> 637 | location = @uniformCache[name] 638 | if location is undefined 639 | location = @uniformCache[name] = @gl.getUniformLocation @program, name 640 | return location 641 | 642 | use: -> 643 | if @framework.currentShader isnt @ 644 | @framework.currentShader = @ 645 | @gl.useProgram @program 646 | return @ 647 | 648 | draw: (drawable) -> 649 | drawable.setPointersForShader(@).draw() 650 | return @ 651 | 652 | int: (name, value) -> 653 | loc = @uniformLocation name 654 | @gl.uniform1i loc, value if loc 655 | return @ 656 | 657 | sampler: (name, texture) -> 658 | unit = @samplers[name] 659 | if unit is undefined 660 | unit = @samplers[name] = @unitCounter++ 661 | texture.bind(unit) 662 | @int name, unit 663 | return @ 664 | 665 | vec2: (name, a, b) -> 666 | loc = @uniformLocation name 667 | @gl.uniform2f loc, a, b if loc 668 | return @ 669 | 670 | vec3: (name, a, b, c) -> 671 | loc = @uniformLocation name 672 | @gl.uniform3f loc, a, b, f if loc 673 | return @ 674 | 675 | mat4: (name, value) -> 676 | loc = @uniformLocation name 677 | if loc 678 | if value instanceof Mat4 679 | @gl.uniformMatrix4fv loc, @gl.FALSE, value.data 680 | else 681 | @gl.uniformMatrix4fv loc, @gl.FALSE, value 682 | return @ 683 | 684 | mat3: (name, value) -> 685 | loc = @uniformLocation name 686 | if loc 687 | if value instanceof Mat3 688 | @gl.uniformMatrix3fv loc, @gl.FALSE, value.data 689 | else 690 | @gl.uniformMatrix3fv loc, @gl.FALSE, value 691 | return @ 692 | 693 | float: (name, value) -> 694 | loc = @uniformLocation name 695 | @gl.uniform1f loc, value if loc 696 | return @ 697 | 698 | class Drawable 699 | float_size = Float32Array.BYTES_PER_ELEMENT 700 | 701 | constructor: (@framework, {@pointers, vertices, @mode}) -> 702 | @gl = @framework.gl 703 | @buffer = @gl.createBuffer() 704 | @mode ?= @gl.TRIANGLES 705 | 706 | @vertexSize = 0 707 | for pointer in @pointers 708 | @vertexSize += pointer.size 709 | 710 | @upload vertices 711 | 712 | upload: (vertices) -> 713 | if vertices instanceof Array 714 | data = new Float32Array vertices 715 | else 716 | data = vertices 717 | 718 | @size = data.length/@vertexSize 719 | @gl.bindBuffer @gl.ARRAY_BUFFER, @buffer 720 | @gl.bufferData @gl.ARRAY_BUFFER, data, @gl.STATIC_DRAW 721 | @gl.bindBuffer @gl.ARRAY_BUFFER, null 722 | 723 | setPointer: (shader, pointer, idx) -> 724 | location = shader.attributeLocation pointer.name 725 | if location >= 0 726 | unit = @framework.vertexUnits[location] 727 | if not unit.enabled 728 | unit.enabled = true 729 | @gl.enableVertexAttribArray location 730 | 731 | if unit.drawable isnt @ or unit.idx != idx 732 | unit.idx = idx 733 | unit.drawable = @ 734 | @gl.vertexAttribPointer( 735 | location, 736 | pointer.size, 737 | @gl.FLOAT, 738 | false, 739 | pointer.stride*float_size, 740 | pointer.offset*float_size 741 | ) 742 | return @ 743 | 744 | setPointersForShader: (shader) -> 745 | @gl.bindBuffer @gl.ARRAY_BUFFER, @buffer 746 | for pointer, i in @pointers 747 | @setPointer shader, pointer, i 748 | return @ 749 | 750 | draw: (first=0, size=@size, mode=@mode) -> 751 | @gl.drawArrays mode, first, size 752 | return @ 753 | 754 | class Texture 755 | constructor: (@framework, params={}) -> 756 | @gl = @framework.gl 757 | @channels = @gl[(params.channels ? 'rgb').toUpperCase()] 758 | 759 | if typeof(params.type) == 'number' 760 | @type = params.type 761 | else 762 | @type = @gl[(params.type ? 'unsigned_byte').toUpperCase()] 763 | 764 | @target = @gl.TEXTURE_2D 765 | @handle = @gl.createTexture() 766 | 767 | destroy: -> 768 | @gl.deleteTexture @handle 769 | 770 | bind: (unit=0) -> 771 | if unit > 15 772 | throw 'Texture unit too large: ' + unit 773 | 774 | @gl.activeTexture @gl.TEXTURE0+unit 775 | @gl.bindTexture @target, @handle 776 | 777 | return @ 778 | 779 | setSize: (@width, @height) -> 780 | @gl.texImage2D @target, 0, @channels, @width, @height, 0, @channels, @type, null 781 | return @ 782 | 783 | linear: -> 784 | @gl.texParameteri @target, @gl.TEXTURE_MAG_FILTER, @gl.LINEAR 785 | @gl.texParameteri @target, @gl.TEXTURE_MIN_FILTER, @gl.LINEAR 786 | return @ 787 | 788 | nearest: -> 789 | @gl.texParameteri @target, @gl.TEXTURE_MAG_FILTER, @gl.NEAREST 790 | @gl.texParameteri @target, @gl.TEXTURE_MIN_FILTER, @gl.NEAREST 791 | return @ 792 | 793 | clampToEdge: -> 794 | @gl.texParameteri @target, @gl.TEXTURE_WRAP_S, @gl.CLAMP_TO_EDGE 795 | @gl.texParameteri @target, @gl.TEXTURE_WRAP_T, @gl.CLAMP_TO_EDGE 796 | return @ 797 | 798 | repeat: -> 799 | @gl.texParameteri @target, @gl.TEXTURE_WRAP_S, @gl.REPEAT 800 | @gl.texParameteri @target, @gl.TEXTURE_WRAP_T, @gl.REPEAT 801 | return @ 802 | 803 | class Framebuffer 804 | constructor: (@framework) -> 805 | @gl = @framework.gl 806 | @buffer = @gl.createFramebuffer() 807 | @ownDepth = false 808 | 809 | destroy: -> 810 | @gl.deleteFRamebuffer @buffer 811 | 812 | bind: -> 813 | @gl.bindFramebuffer @gl.FRAMEBUFFER, @buffer 814 | return @ 815 | 816 | unbind: -> 817 | @gl.bindFramebuffer @gl.FRAMEBUFFER, null 818 | return @ 819 | 820 | check: -> 821 | result = @gl.checkFramebufferStatus @gl.FRAMEBUFFER 822 | switch result 823 | when @gl.FRAMEBUFFER_UNSUPPORTED 824 | throw 'Framebuffer is unsupported' 825 | when @gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT 826 | throw 'Framebuffer incomplete attachment' 827 | when @gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS 828 | throw 'Framebuffer incomplete dimensions' 829 | when @gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT 830 | throw 'Framebuffer incomplete missing attachment' 831 | return @ 832 | 833 | color: (@colorTexture) -> 834 | @gl.framebufferTexture2D @gl.FRAMEBUFFER, @gl.COLOR_ATTACHMENT0, @colorTexture.target, @colorTexture.handle, 0 835 | @check() 836 | return @ 837 | 838 | depth: (@depthBuffer) -> 839 | if @depthBuffer is undefined 840 | if @colorTexture is undefined 841 | throw 'Cannot create implicit depth buffer without a color texture' 842 | else 843 | @ownDepth = true 844 | @depthBuffer = @framework.depthbuffer().bind().setSize(@colorTexture.width, @colorTexture.height) 845 | @gl.framebufferRenderbuffer @gl.FRAMEBUFFER, @gl.DEPTH_ATTACHMENT, @gl.RENDERBUFFER, @depthBuffer.id 846 | @check() 847 | return @ 848 | 849 | destroy: -> 850 | @gl.deleteFramebuffer @buffer 851 | if @ownDepth 852 | @depthBuffer.destroy() 853 | 854 | class Renderbuffer 855 | constructor: (@framework) -> 856 | @gl = @framework.gl 857 | @id = @gl.createRenderbuffer() 858 | 859 | bind: -> 860 | @gl.bindRenderbuffer @gl.RENDERBUFFER, @id 861 | return @ 862 | 863 | setSize: (@width, @height) -> 864 | @bind() 865 | @gl.renderbufferStorage @gl.RENDERBUFFER, @gl[@format], @width, @height 866 | @unbind() 867 | 868 | unbind: -> 869 | @gl.bindRenderbuffer @gl.RENDERBUFFER, null 870 | return @ 871 | 872 | destroy: -> 873 | @gl.deleteRenderbuffer @id 874 | 875 | Depthbuffer = class extends Renderbuffer 876 | format: 'DEPTH_COMPONENT16' 877 | 878 | raf = ( 879 | window.requestAnimationFrame or 880 | window.mozRequestAnimationFrame or 881 | window.webkitRequestAnimationFrame or 882 | window.oRequestAnimationFrame 883 | ) 884 | 885 | performance.now = ( 886 | performance.now or 887 | performance.mozNow or 888 | performance.webkitNow or 889 | performance.oNow 890 | ) 891 | 892 | window.WebGLFramework = class WebGLFramework 893 | constructor: (@canvas, params) -> 894 | try 895 | @gl = @canvas.getContext('experimental-webgl', params) 896 | if @gl == null 897 | @gl = @canvas.getContext('webgl', params) 898 | if @gl == null 899 | throw 'WebGL not supported' 900 | catch error 901 | throw 'WebGL not supported' 902 | 903 | @textureUnits = [] 904 | for _ in [0...16] 905 | @textureUnits.push(null) 906 | 907 | @vertexUnits = [] 908 | for _ in [0...16] 909 | @vertexUnits.push(enabled:false, drawable:null, idx:null) 910 | 911 | @currentShader = null 912 | 913 | shader: (params) -> new Shader @, params 914 | drawable: (params) -> new Drawable @, params 915 | texture: (params) -> new Texture @, params 916 | framebuffer: -> new Framebuffer @ 917 | depthbuffer: -> new Depthbuffer @ 918 | 919 | mat3: (data) -> new Mat3 data 920 | mat4: (data) -> new Mat4 data 921 | 922 | clearColor: (r=0, g=0, b=0, a=1) -> 923 | @gl.clearColor r, g, b, a 924 | @gl.clear @gl.COLOR_BUFFER_BIT 925 | return @ 926 | 927 | clearDepth: (depth=1) -> 928 | @gl.clearDepth depth 929 | @gl.clear @gl.DEPTH_BUFFER_BIT 930 | return @ 931 | 932 | adjustSize: -> 933 | canvasWidth = @canvas.offsetWidth or 2 934 | canvasHeight = @canvas.offsetHeight or 2 935 | 936 | if @width isnt canvasWidth or @height isnt canvasHeight 937 | @canvas.width = canvasWidth 938 | @canvas.height = canvasHeight 939 | @width = canvasWidth 940 | @height = canvasHeight 941 | @aspect = @width/@height 942 | 943 | return @ 944 | 945 | viewport: (left=0, top=0, width=@width, height=@height) -> 946 | @gl.viewport left, top, width, height 947 | return @ 948 | 949 | depthTest: (value=true) -> 950 | if value then @gl.enable @gl.DEPTH_TEST 951 | else @gl.disable @gl.DEPTH_TEST 952 | return @ 953 | 954 | animationInterval: (callback) => 955 | interval = -> 956 | callback() 957 | raf interval 958 | raf interval 959 | 960 | now: -> performance.now()/1000 961 | 962 | getExt: (name, throws=true) -> 963 | ext = @gl.getExtension name 964 | if not ext and throws 965 | throw "WebGL Extension not supported: #{name}" 966 | return ext 967 | 968 | requestFullscreen: (elem=@canvas) -> 969 | if elem.mozRequestFullScreen then elem.mozRequestFullScreen() 970 | else if elem.webkitRequestFullScreen then elem.webkitRequestFullScreen() 971 | else if elem.oRequestFullScreen then elem.oRequestFullScreen() 972 | return @ 973 | 974 | isFullscreen: -> 975 | a = getVendorAttrib(document, 'fullscreenElement') 976 | b = getVendorAttrib(document, 'fullScreenElement') 977 | if a or b 978 | return true 979 | else 980 | return false 981 | 982 | onFullscreenChange: (fun) -> 983 | callback = => 984 | fun(@isFullscreen()) 985 | for vendor in vendors 986 | document.addEventListener vendor + 'fullscreenchange', callback, false 987 | return @ 988 | 989 | exitFullscreen: -> 990 | document.cancelFullscreen() 991 | return @ 992 | 993 | toggleFullscreen: (elem=@canvas) -> 994 | if @isFullscreen() then @exitFullscreen() 995 | else @requestFullscreen(elem) 996 | 997 | getFloatExtension: (spec) -> @gl.getFloatExtension(spec) 998 | 999 | cullFace: (value='back') -> 1000 | if value 1001 | @gl.enable @gl.CULL_FACE 1002 | @gl.cullFace @gl[value.toUpperCase()] 1003 | else 1004 | @gl.disable @gl.CULL_FACE 1005 | return @ 1006 | 1007 | ## shims ## 1008 | vendors = [null, 'webkit', 'apple', 'moz', 'o', 'xv', 'ms', 'khtml', 'atsc', 'wap', 'prince', 'ah', 'hp', 'ro', 'rim', 'tc'] 1009 | 1010 | vendorName = (name, vendor) -> 1011 | if vendor == null 1012 | return name 1013 | else 1014 | return vendor + name[0].toUpperCase() + name.substr(1) 1015 | 1016 | getVendorAttrib = (obj, name, def) -> 1017 | if obj 1018 | for vendor in vendors 1019 | attrib_name = vendorName(name, vendor) 1020 | attrib = obj[attrib_name] 1021 | if attrib != undefined 1022 | return attrib 1023 | return def 1024 | 1025 | document.fullscreenEnabled = getVendorAttrib document, 'fullscreenEnabled' 1026 | document.cancelFullscreen = getVendorAttrib document, 'cancelFullScreen' 1027 | -------------------------------------------------------------------------------- /fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyalot/soft-shadow-mapping/e425c99c93a11f2b46f01b37bf47e10069133144/fullscreen.png -------------------------------------------------------------------------------- /hard-shadow.coffee: -------------------------------------------------------------------------------- 1 | ## add the canvas to the dom ## 2 | name = 'hard-shadow' 3 | document.write "
" 4 | container = $('#' + name) 5 | canvas = $('').appendTo(container)[0] 6 | 7 | ## setup the framework ## 8 | try 9 | gl = new WebGLFramework(canvas) 10 | .depthTest() 11 | 12 | floatExt = gl.getFloatExtension( 13 | require: ['renderable'] 14 | prefer: ['filterable', 'half'] 15 | ) 16 | 17 | catch error 18 | container.empty() 19 | $('
').text(error).appendTo(container) 20 | $('
').text('(screenshot instead)').appendTo(container) 21 | $("").appendTo(container) 22 | return 23 | 24 | ## fullscreen handling ## 25 | fullscreenImg = $('') 26 | .appendTo(container) 27 | .click -> gl.toggleFullscreen(container[0]) 28 | 29 | gl.onFullscreenChange (isFullscreen) -> 30 | if isFullscreen 31 | container.addClass('fullscreen') 32 | fullscreenImg.attr('src', 'exit-fullscreen.png') 33 | else 34 | container.removeClass('fullscreen') 35 | fullscreenImg.attr('src', 'fullscreen.png') 36 | 37 | ## handle mouse over ## 38 | hover = false 39 | container.hover (-> hover = true), (-> hover = false) 40 | 41 | ## animation control ## 42 | animate = true 43 | controls = $('
') 44 | .appendTo(container) 45 | $('').appendTo(controls) 46 | $('') 47 | .appendTo(controls) 48 | .change -> 49 | animate = @checked 50 | 51 | ## create webgl objects ## 52 | cubeGeom = gl.drawable meshes.cube 53 | planeGeom = gl.drawable meshes.plane(50) 54 | displayShader = gl.shader 55 | common: '''//essl 56 | varying vec3 vWorldNormal; varying vec4 vWorldPosition; 57 | uniform mat4 camProj, camView; 58 | uniform mat4 lightProj, lightView; uniform mat3 lightRot; 59 | uniform mat4 model; 60 | ''' 61 | vertex: '''//essl 62 | attribute vec3 position, normal; 63 | 64 | void main(){ 65 | vWorldNormal = normal; 66 | vWorldPosition = model * vec4(position, 1.0); 67 | gl_Position = camProj * camView * vWorldPosition; 68 | } 69 | ''' 70 | fragment: '''//essl 71 | uniform sampler2D sLightDepth; 72 | 73 | float attenuation(vec3 dir){ 74 | float dist = length(dir); 75 | float radiance = 1.0/(1.0+pow(dist/10.0, 2.0)); 76 | return clamp(radiance*10.0, 0.0, 1.0); 77 | } 78 | 79 | float influence(vec3 normal, float coneAngle){ 80 | float minConeAngle = ((360.0-coneAngle-10.0)/360.0)*PI; 81 | float maxConeAngle = ((360.0-coneAngle)/360.0)*PI; 82 | return smoothstep(minConeAngle, maxConeAngle, acos(normal.z)); 83 | } 84 | 85 | float lambert(vec3 surfaceNormal, vec3 lightDirNormal){ 86 | return max(0.0, dot(surfaceNormal, lightDirNormal)); 87 | } 88 | 89 | vec3 skyLight(vec3 normal){ 90 | return vec3(smoothstep(0.0, PI, PI-acos(normal.y)))*0.4; 91 | } 92 | 93 | vec3 gamma(vec3 color){ 94 | return pow(color, vec3(2.2)); 95 | } 96 | 97 | void main(){ 98 | vec3 worldNormal = normalize(vWorldNormal); 99 | 100 | vec3 camPos = (camView * vWorldPosition).xyz; 101 | vec3 lightPos = (lightView * vWorldPosition).xyz; 102 | vec3 lightPosNormal = normalize(lightPos); 103 | vec3 lightSurfaceNormal = lightRot * worldNormal; 104 | vec4 lightDevice = lightProj * vec4(lightPos, 1.0); 105 | vec2 lightDeviceNormal = lightDevice.xy/lightDevice.w; 106 | vec2 lightUV = lightDeviceNormal*0.5+0.5; 107 | 108 | // shadow calculation 109 | float lightDepth1 = texture2D(sLightDepth, lightUV).r; 110 | float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0); 111 | float bias = 0.001; 112 | float illuminated = step(lightDepth2, lightDepth1+bias); 113 | 114 | vec3 excident = ( 115 | skyLight(worldNormal) + 116 | lambert(lightSurfaceNormal, -lightPosNormal) * 117 | influence(lightPosNormal, 55.0) * 118 | attenuation(lightPos) * 119 | illuminated 120 | ); 121 | gl_FragColor = vec4(gamma(excident), 1.0); 122 | } 123 | ''' 124 | 125 | lightShader = gl.shader 126 | common: '''//essl 127 | varying vec3 vWorldNormal; varying vec4 vWorldPosition; 128 | uniform mat4 lightProj, lightView; uniform mat3 lightRot; 129 | uniform mat4 model; 130 | ''' 131 | vertex: '''//essl 132 | attribute vec3 position, normal; 133 | 134 | void main(){ 135 | vWorldNormal = normal; 136 | vWorldPosition = model * vec4(position, 1.0); 137 | gl_Position = lightProj * lightView * vWorldPosition; 138 | } 139 | ''' 140 | fragment: '''//essl 141 | void main(){ 142 | vec3 worldNormal = normalize(vWorldNormal); 143 | vec3 lightPos = (lightView * vWorldPosition).xyz; 144 | float depth = clamp(length(lightPos)/40.0, 0.0, 1.0); 145 | gl_FragColor = vec4(vec3(depth), 1.0); 146 | } 147 | ''' 148 | 149 | lightDepthTexture = gl.texture(type:floatExt.type, channels:'rgba').bind().setSize(64, 64).clampToEdge() 150 | if floatExt.filterable 151 | lightDepthTexture.linear() 152 | else 153 | lightDepthTexture.nearest() 154 | 155 | lightFramebuffer = gl.framebuffer().bind().color(lightDepthTexture).depth().unbind() 156 | 157 | ## matrix setup ## 158 | camProj = gl.mat4() 159 | camView = gl.mat4() 160 | lightProj = gl.mat4().perspective(fov:60, 1, near:0.01, far:100) 161 | lightView = gl.mat4().trans(0, 0, -6).rotatex(30).rotatey(110) 162 | lightRot = gl.mat3().fromMat4Rot(lightView) 163 | model = gl.mat4() 164 | 165 | ## state variables ## 166 | counter = -Math.PI*0.5 167 | offset = 0 168 | camDist = 10 169 | camRot = 55 170 | camPitch = 41 171 | 172 | ## mouse handling ## 173 | mouseup = -> $(document).unbind('mousemove', mousemove).unbind('mouseup', mouseup) 174 | mousemove = ({originalEvent}) -> 175 | x = originalEvent.movementX ? originalEvent.webkitMovementX ? originalEvent.mozMovementX ? originalEvent.oMovementX 176 | y = originalEvent.movementY ? originalEvent.webkitMovementY ? originalEvent.mozMovementY ? originalEvent.oMovementY 177 | camRot += x 178 | camPitch += y 179 | if camPitch > 85 then camPitch = 85 180 | else if camPitch < 1 then camPitch = 1 181 | 182 | $(canvas) 183 | .bind 'mousedown', -> 184 | $(document).bind('mousemove', mousemove).bind('mouseup', mouseup) 185 | return false 186 | 187 | .bind 'mousewheel', ({originalEvent}) -> 188 | camDist -= originalEvent.wheelDeltaY/250 189 | return false 190 | .bind 'DOMMouseScroll', ({originalEvent}) -> 191 | camDist += originalEvent.detail/5 192 | return false 193 | 194 | ## drawing methods ## 195 | drawScene = (shader) -> 196 | shader 197 | .mat4('model', model.ident().trans(0, 0, 0)) 198 | .draw(planeGeom) 199 | .mat4('model', model.ident().trans(0, 1+offset, 0)) 200 | .draw(cubeGeom) 201 | .mat4('model', model.ident().trans(5, 1, -1)) 202 | .draw(cubeGeom) 203 | 204 | drawLight = -> 205 | lightFramebuffer.bind() 206 | gl 207 | .viewport(0, 0, lightDepthTexture.width, lightDepthTexture.height) 208 | .clearColor(1,1,1,1) 209 | .clearDepth(1) 210 | .cullFace('front') 211 | lightShader.use() 212 | .mat4('lightView', lightView) 213 | .mat4('lightProj', lightProj) 214 | .mat3('lightRot', lightRot) 215 | drawScene lightShader 216 | lightFramebuffer.unbind() 217 | 218 | drawCamera = -> 219 | gl 220 | .adjustSize() 221 | .viewport() 222 | .cullFace('back') 223 | .clearColor(0,0,0,0) 224 | .clearDepth(1) 225 | 226 | camProj.perspective(fov:60, aspect:gl.aspect, near:0.01, far:100) 227 | camView.ident().trans(0, -1, -camDist).rotatex(camPitch).rotatey(camRot) 228 | 229 | displayShader.use() 230 | .mat4('camProj', camProj) 231 | .mat4('camView', camView) 232 | .mat4('lightView', lightView) 233 | .mat4('lightProj', lightProj) 234 | .mat3('lightRot', lightRot) 235 | .sampler('sLightDepth', lightDepthTexture) 236 | drawScene displayShader 237 | 238 | draw = -> 239 | drawLight() 240 | drawCamera() 241 | 242 | ## mainloop ## 243 | draw() 244 | gl.animationInterval -> 245 | if hover 246 | if animate 247 | offset = 1 + Math.sin(counter) 248 | counter += 1/30 249 | else 250 | offset = 0 251 | draw() 252 | -------------------------------------------------------------------------------- /hard-shadow.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var animate, camDist, camPitch, camProj, camRot, camView, canvas, container, controls, counter, cubeGeom, displayShader, draw, drawCamera, drawLight, drawScene, floatExt, fullscreenImg, gl, hover, lightDepthTexture, lightFramebuffer, lightProj, lightRot, lightShader, lightView, model, mousemove, mouseup, name, offset, planeGeom; 3 | 4 | name = 'hard-shadow'; 5 | 6 | document.write("
"); 7 | 8 | container = $('#' + name); 9 | 10 | canvas = $('').appendTo(container)[0]; 11 | 12 | try { 13 | gl = new WebGLFramework(canvas).depthTest(); 14 | floatExt = gl.getFloatExtension({ 15 | require: ['renderable'], 16 | prefer: ['filterable', 'half'] 17 | }); 18 | } catch (error) { 19 | container.empty(); 20 | $('
').text(error).appendTo(container); 21 | $('
').text('(screenshot instead)').appendTo(container); 22 | $("").appendTo(container); 23 | return; 24 | } 25 | 26 | fullscreenImg = $('').appendTo(container).click(function() { 27 | return gl.toggleFullscreen(container[0]); 28 | }); 29 | 30 | gl.onFullscreenChange(function(isFullscreen) { 31 | if (isFullscreen) { 32 | container.addClass('fullscreen'); 33 | return fullscreenImg.attr('src', 'exit-fullscreen.png'); 34 | } else { 35 | container.removeClass('fullscreen'); 36 | return fullscreenImg.attr('src', 'fullscreen.png'); 37 | } 38 | }); 39 | 40 | hover = false; 41 | 42 | container.hover((function() { 43 | return hover = true; 44 | }), (function() { 45 | return hover = false; 46 | })); 47 | 48 | animate = true; 49 | 50 | controls = $('
').appendTo(container); 51 | 52 | $('').appendTo(controls); 53 | 54 | $('').appendTo(controls).change(function() { 55 | return animate = this.checked; 56 | }); 57 | 58 | cubeGeom = gl.drawable(meshes.cube); 59 | 60 | planeGeom = gl.drawable(meshes.plane(50)); 61 | 62 | displayShader = gl.shader({ 63 | common: '#line 55 hard-shadow.coffee\nvarying vec3 vWorldNormal; varying vec4 vWorldPosition;\nuniform mat4 camProj, camView;\nuniform mat4 lightProj, lightView; uniform mat3 lightRot;\nuniform mat4 model;', 64 | vertex: '#line 61 hard-shadow.coffee\nattribute vec3 position, normal;\n\nvoid main(){\n vWorldNormal = normal;\n vWorldPosition = model * vec4(position, 1.0);\n gl_Position = camProj * camView * vWorldPosition;\n}', 65 | fragment: '#line 70 hard-shadow.coffee\nuniform sampler2D sLightDepth;\n\nfloat attenuation(vec3 dir){\n float dist = length(dir);\n float radiance = 1.0/(1.0+pow(dist/10.0, 2.0));\n return clamp(radiance*10.0, 0.0, 1.0);\n}\n\nfloat influence(vec3 normal, float coneAngle){\n float minConeAngle = ((360.0-coneAngle-10.0)/360.0)*PI;\n float maxConeAngle = ((360.0-coneAngle)/360.0)*PI;\n return smoothstep(minConeAngle, maxConeAngle, acos(normal.z));\n}\n\nfloat lambert(vec3 surfaceNormal, vec3 lightDirNormal){\n return max(0.0, dot(surfaceNormal, lightDirNormal));\n}\n\nvec3 skyLight(vec3 normal){\n return vec3(smoothstep(0.0, PI, PI-acos(normal.y)))*0.4;\n}\n\nvec3 gamma(vec3 color){\n return pow(color, vec3(2.2));\n}\n\nvoid main(){\n vec3 worldNormal = normalize(vWorldNormal);\n\n vec3 camPos = (camView * vWorldPosition).xyz;\n vec3 lightPos = (lightView * vWorldPosition).xyz;\n vec3 lightPosNormal = normalize(lightPos);\n vec3 lightSurfaceNormal = lightRot * worldNormal;\n vec4 lightDevice = lightProj * vec4(lightPos, 1.0);\n vec2 lightDeviceNormal = lightDevice.xy/lightDevice.w;\n vec2 lightUV = lightDeviceNormal*0.5+0.5;\n\n // shadow calculation\n float lightDepth1 = texture2D(sLightDepth, lightUV).r;\n float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0);\n float bias = 0.001;\n float illuminated = step(lightDepth2, lightDepth1+bias);\n \n vec3 excident = (\n skyLight(worldNormal) +\n lambert(lightSurfaceNormal, -lightPosNormal) *\n influence(lightPosNormal, 55.0) *\n attenuation(lightPos) *\n illuminated\n );\n gl_FragColor = vec4(gamma(excident), 1.0);\n}' 66 | }); 67 | 68 | lightShader = gl.shader({ 69 | common: '#line 126 hard-shadow.coffee\nvarying vec3 vWorldNormal; varying vec4 vWorldPosition;\nuniform mat4 lightProj, lightView; uniform mat3 lightRot;\nuniform mat4 model;', 70 | vertex: '#line 131 hard-shadow.coffee\nattribute vec3 position, normal;\n\nvoid main(){\n vWorldNormal = normal;\n vWorldPosition = model * vec4(position, 1.0);\n gl_Position = lightProj * lightView * vWorldPosition;\n}', 71 | fragment: '#line 140 hard-shadow.coffee\nvoid main(){\n vec3 worldNormal = normalize(vWorldNormal);\n vec3 lightPos = (lightView * vWorldPosition).xyz;\n float depth = clamp(length(lightPos)/40.0, 0.0, 1.0);\n gl_FragColor = vec4(vec3(depth), 1.0);\n}' 72 | }); 73 | 74 | lightDepthTexture = gl.texture({ 75 | type: floatExt.type, 76 | channels: 'rgba' 77 | }).bind().setSize(64, 64).clampToEdge(); 78 | 79 | if (floatExt.filterable) { 80 | lightDepthTexture.linear(); 81 | } else { 82 | lightDepthTexture.nearest(); 83 | } 84 | 85 | lightFramebuffer = gl.framebuffer().bind().color(lightDepthTexture).depth().unbind(); 86 | 87 | camProj = gl.mat4(); 88 | 89 | camView = gl.mat4(); 90 | 91 | lightProj = gl.mat4().perspective({ 92 | fov: 60 93 | }, 1, { 94 | near: 0.01, 95 | far: 100 96 | }); 97 | 98 | lightView = gl.mat4().trans(0, 0, -6).rotatex(30).rotatey(110); 99 | 100 | lightRot = gl.mat3().fromMat4Rot(lightView); 101 | 102 | model = gl.mat4(); 103 | 104 | counter = -Math.PI * 0.5; 105 | 106 | offset = 0; 107 | 108 | camDist = 10; 109 | 110 | camRot = 55; 111 | 112 | camPitch = 41; 113 | 114 | mouseup = function() { 115 | return $(document).unbind('mousemove', mousemove).unbind('mouseup', mouseup); 116 | }; 117 | 118 | mousemove = function(_arg) { 119 | var originalEvent, x, y, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; 120 | originalEvent = _arg.originalEvent; 121 | x = (_ref = (_ref1 = (_ref2 = originalEvent.movementX) != null ? _ref2 : originalEvent.webkitMovementX) != null ? _ref1 : originalEvent.mozMovementX) != null ? _ref : originalEvent.oMovementX; 122 | y = (_ref3 = (_ref4 = (_ref5 = originalEvent.movementY) != null ? _ref5 : originalEvent.webkitMovementY) != null ? _ref4 : originalEvent.mozMovementY) != null ? _ref3 : originalEvent.oMovementY; 123 | camRot += x; 124 | camPitch += y; 125 | if (camPitch > 85) { 126 | return camPitch = 85; 127 | } else if (camPitch < 1) { 128 | return camPitch = 1; 129 | } 130 | }; 131 | 132 | $(canvas).bind('mousedown', function() { 133 | $(document).bind('mousemove', mousemove).bind('mouseup', mouseup); 134 | return false; 135 | }).bind('mousewheel', function(_arg) { 136 | var originalEvent; 137 | originalEvent = _arg.originalEvent; 138 | camDist -= originalEvent.wheelDeltaY / 250; 139 | return false; 140 | }).bind('DOMMouseScroll', function(_arg) { 141 | var originalEvent; 142 | originalEvent = _arg.originalEvent; 143 | camDist += originalEvent.detail / 5; 144 | return false; 145 | }); 146 | 147 | drawScene = function(shader) { 148 | return shader.mat4('model', model.ident().trans(0, 0, 0)).draw(planeGeom).mat4('model', model.ident().trans(0, 1 + offset, 0)).draw(cubeGeom).mat4('model', model.ident().trans(5, 1, -1)).draw(cubeGeom); 149 | }; 150 | 151 | drawLight = function() { 152 | lightFramebuffer.bind(); 153 | gl.viewport(0, 0, lightDepthTexture.width, lightDepthTexture.height).clearColor(1, 1, 1, 1).clearDepth(1).cullFace('front'); 154 | lightShader.use().mat4('lightView', lightView).mat4('lightProj', lightProj).mat3('lightRot', lightRot); 155 | drawScene(lightShader); 156 | return lightFramebuffer.unbind(); 157 | }; 158 | 159 | drawCamera = function() { 160 | gl.adjustSize().viewport().cullFace('back').clearColor(0, 0, 0, 0).clearDepth(1); 161 | camProj.perspective({ 162 | fov: 60, 163 | aspect: gl.aspect, 164 | near: 0.01, 165 | far: 100 166 | }); 167 | camView.ident().trans(0, -1, -camDist).rotatex(camPitch).rotatey(camRot); 168 | displayShader.use().mat4('camProj', camProj).mat4('camView', camView).mat4('lightView', lightView).mat4('lightProj', lightProj).mat3('lightRot', lightRot).sampler('sLightDepth', lightDepthTexture); 169 | return drawScene(displayShader); 170 | }; 171 | 172 | draw = function() { 173 | drawLight(); 174 | return drawCamera(); 175 | }; 176 | 177 | draw(); 178 | 179 | gl.animationInterval(function() { 180 | if (hover) { 181 | if (animate) { 182 | offset = 1 + Math.sin(counter); 183 | counter += 1 / 30; 184 | } else { 185 | offset = 0; 186 | } 187 | return draw(); 188 | } 189 | }); 190 | 191 | }).call(this); 192 | -------------------------------------------------------------------------------- /hard-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyalot/soft-shadow-mapping/e425c99c93a11f2b46f01b37bf47e10069133144/hard-shadow.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |

No Shadow

75 | 76 |

Hard

77 | 78 |

Interpolated

79 | 80 |

PCF

81 | 82 |

PCF and Interpolated

83 | 84 |

VSM

85 | 86 |

Antialiased and Blurred VSM

87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /lerp-shadow.coffee: -------------------------------------------------------------------------------- 1 | ## add the canvas to the dom ## 2 | name = 'lerp-shadow' 3 | document.write "
" 4 | container = $('#' + name) 5 | canvas = $('').appendTo(container)[0] 6 | 7 | ## setup the framework ## 8 | try 9 | gl = new WebGLFramework(canvas) 10 | .depthTest() 11 | 12 | floatExt = gl.getFloatExtension( 13 | require: ['renderable'] 14 | ) 15 | 16 | catch error 17 | container.empty() 18 | $('
').text(error).appendTo(container) 19 | $('
').text('(screenshot instead)').appendTo(container) 20 | $("").appendTo(container) 21 | return 22 | 23 | ## fullscreen handling ## 24 | fullscreenImg = $('') 25 | .appendTo(container) 26 | .click -> gl.toggleFullscreen(container[0]) 27 | 28 | gl.onFullscreenChange (isFullscreen) -> 29 | if isFullscreen 30 | container.addClass('fullscreen') 31 | fullscreenImg.attr('src', 'exit-fullscreen.png') 32 | else 33 | container.removeClass('fullscreen') 34 | fullscreenImg.attr('src', 'fullscreen.png') 35 | 36 | ## handle mouse over ## 37 | hover = false 38 | container.hover (-> hover = true), (-> hover = false) 39 | 40 | ## animation control ## 41 | animate = true 42 | controls = $('
') 43 | .appendTo(container) 44 | $('').appendTo(controls) 45 | $('') 46 | .appendTo(controls) 47 | .change -> 48 | animate = @checked 49 | 50 | ## create webgl objects ## 51 | cubeGeom = gl.drawable meshes.cube 52 | planeGeom = gl.drawable meshes.plane(50) 53 | displayShader = gl.shader 54 | common: '''//essl 55 | varying vec3 vWorldNormal; varying vec4 vWorldPosition; 56 | uniform mat4 camProj, camView; 57 | uniform mat4 lightProj, lightView; uniform mat3 lightRot; 58 | uniform mat4 model; 59 | ''' 60 | vertex: '''//essl 61 | attribute vec3 position, normal; 62 | 63 | void main(){ 64 | vWorldNormal = normal; 65 | vWorldPosition = model * vec4(position, 1.0); 66 | gl_Position = camProj * camView * vWorldPosition; 67 | } 68 | ''' 69 | fragment: '''//essl 70 | uniform sampler2D sLightDepth; 71 | uniform vec2 lightDepthSize; 72 | 73 | float texture2DCompare(sampler2D depths, vec2 uv, float compare){ 74 | float depth = texture2D(depths, uv).r; 75 | return step(compare, depth); 76 | } 77 | 78 | float texture2DShadowLerp(sampler2D depths, vec2 size, vec2 uv, float compare){ 79 | vec2 texelSize = vec2(1.0)/size; 80 | vec2 f = fract(uv*size+0.5); 81 | vec2 centroidUV = floor(uv*size+0.5)/size; 82 | 83 | float lb = texture2DCompare(depths, centroidUV+texelSize*vec2(0.0, 0.0), compare); 84 | float lt = texture2DCompare(depths, centroidUV+texelSize*vec2(0.0, 1.0), compare); 85 | float rb = texture2DCompare(depths, centroidUV+texelSize*vec2(1.0, 0.0), compare); 86 | float rt = texture2DCompare(depths, centroidUV+texelSize*vec2(1.0, 1.0), compare); 87 | float a = mix(lb, lt, f.y); 88 | float b = mix(rb, rt, f.y); 89 | float c = mix(a, b, f.x); 90 | return c; 91 | } 92 | 93 | float attenuation(vec3 dir){ 94 | float dist = length(dir); 95 | float radiance = 1.0/(1.0+pow(dist/10.0, 2.0)); 96 | return clamp(radiance*10.0, 0.0, 1.0); 97 | } 98 | 99 | float influence(vec3 normal, float coneAngle){ 100 | float minConeAngle = ((360.0-coneAngle-10.0)/360.0)*PI; 101 | float maxConeAngle = ((360.0-coneAngle)/360.0)*PI; 102 | return smoothstep(minConeAngle, maxConeAngle, acos(normal.z)); 103 | } 104 | 105 | float lambert(vec3 surfaceNormal, vec3 lightDirNormal){ 106 | return max(0.0, dot(surfaceNormal, lightDirNormal)); 107 | } 108 | 109 | vec3 skyLight(vec3 normal){ 110 | return vec3(smoothstep(0.0, PI, PI-acos(normal.y)))*0.4; 111 | } 112 | 113 | vec3 gamma(vec3 color){ 114 | return pow(color, vec3(2.2)); 115 | } 116 | 117 | void main(){ 118 | vec3 worldNormal = normalize(vWorldNormal); 119 | 120 | vec3 camPos = (camView * vWorldPosition).xyz; 121 | vec3 lightPos = (lightView * vWorldPosition).xyz; 122 | vec3 lightPosNormal = normalize(lightPos); 123 | vec3 lightSurfaceNormal = lightRot * worldNormal; 124 | vec4 lightDevice = lightProj * vec4(lightPos, 1.0); 125 | vec2 lightDeviceNormal = lightDevice.xy/lightDevice.w; 126 | vec2 lightUV = lightDeviceNormal*0.5+0.5; 127 | 128 | // shadow calculation 129 | float bias = 0.001; 130 | float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0)-bias; 131 | float illuminated = texture2DShadowLerp(sLightDepth, lightDepthSize, lightUV, lightDepth2); 132 | 133 | vec3 excident = ( 134 | skyLight(worldNormal) + 135 | lambert(lightSurfaceNormal, -lightPosNormal) * 136 | influence(lightPosNormal, 55.0) * 137 | attenuation(lightPos) * 138 | illuminated 139 | ); 140 | gl_FragColor = vec4(gamma(excident), 1.0); 141 | } 142 | ''' 143 | 144 | lightShader = gl.shader 145 | common: '''//essl 146 | varying vec3 vWorldNormal; varying vec4 vWorldPosition; 147 | uniform mat4 lightProj, lightView; uniform mat3 lightRot; 148 | uniform mat4 model; 149 | ''' 150 | vertex: '''//essl 151 | attribute vec3 position, normal; 152 | 153 | void main(){ 154 | vWorldNormal = normal; 155 | vWorldPosition = model * vec4(position, 1.0); 156 | gl_Position = lightProj * lightView * vWorldPosition; 157 | } 158 | ''' 159 | fragment: '''//essl 160 | void main(){ 161 | vec3 worldNormal = normalize(vWorldNormal); 162 | vec3 lightPos = (lightView * vWorldPosition).xyz; 163 | float depth = clamp(length(lightPos)/40.0, 0.0, 1.0); 164 | gl_FragColor = vec4(vec3(depth), 1.0); 165 | } 166 | ''' 167 | lightDepthTexture = gl.texture(type:'float', channels:'rgba').bind().setSize(64, 64).nearest().clampToEdge() 168 | lightFramebuffer = gl.framebuffer().bind().color(lightDepthTexture).depth().unbind() 169 | 170 | ## matrix setup ## 171 | camProj = gl.mat4() 172 | camView = gl.mat4() 173 | lightProj = gl.mat4().perspective(fov:60, 1, near:0.01, far:100) 174 | lightView = gl.mat4().trans(0, 0, -6).rotatex(30).rotatey(110) 175 | lightRot = gl.mat3().fromMat4Rot(lightView) 176 | model = gl.mat4() 177 | 178 | ## state variables ## 179 | counter = -Math.PI*0.5 180 | offset = 0 181 | camDist = 10 182 | camRot = 55 183 | camPitch = 41 184 | 185 | ## mouse handling ## 186 | mouseup = -> $(document).unbind('mousemove', mousemove).unbind('mouseup', mouseup) 187 | mousemove = ({originalEvent}) -> 188 | x = originalEvent.movementX ? originalEvent.webkitMovementX ? originalEvent.mozMovementX ? originalEvent.oMovementX 189 | y = originalEvent.movementY ? originalEvent.webkitMovementY ? originalEvent.mozMovementY ? originalEvent.oMovementY 190 | camRot += x 191 | camPitch += y 192 | if camPitch > 85 then camPitch = 85 193 | else if camPitch < 1 then camPitch = 1 194 | 195 | $(canvas) 196 | .bind 'mousedown', -> 197 | $(document).bind('mousemove', mousemove).bind('mouseup', mouseup) 198 | return false 199 | 200 | .bind 'mousewheel', ({originalEvent}) -> 201 | camDist -= originalEvent.wheelDeltaY/250 202 | return false 203 | .bind 'DOMMouseScroll', ({originalEvent}) -> 204 | camDist += originalEvent.detail/5 205 | return false 206 | 207 | ## drawing methods ## 208 | drawScene = (shader) -> 209 | shader 210 | .mat4('model', model.ident().trans(0, 0, 0)) 211 | .draw(planeGeom) 212 | .mat4('model', model.ident().trans(0, 1+offset, 0)) 213 | .draw(cubeGeom) 214 | .mat4('model', model.ident().trans(5, 1, -1)) 215 | .draw(cubeGeom) 216 | 217 | drawLight = -> 218 | lightFramebuffer.bind() 219 | gl 220 | .viewport(0, 0, lightDepthTexture.width, lightDepthTexture.height) 221 | .clearColor(1,1,1,1) 222 | .clearDepth(1) 223 | .cullFace('front') 224 | 225 | lightShader.use() 226 | .mat4('lightView', lightView) 227 | .mat4('lightProj', lightProj) 228 | .mat3('lightRot', lightRot) 229 | drawScene lightShader 230 | lightFramebuffer.unbind() 231 | 232 | drawCamera = -> 233 | gl 234 | .adjustSize() 235 | .viewport() 236 | .cullFace('back') 237 | .clearColor(0,0,0,0) 238 | .clearDepth(1) 239 | 240 | camProj.perspective(fov:60, aspect:gl.aspect, near:0.01, far:100) 241 | camView.ident().trans(0, -1, -camDist).rotatex(camPitch).rotatey(camRot) 242 | 243 | displayShader.use() 244 | .mat4('camProj', camProj) 245 | .mat4('camView', camView) 246 | .mat4('lightView', lightView) 247 | .mat4('lightProj', lightProj) 248 | .mat3('lightRot', lightRot) 249 | .sampler('sLightDepth', lightDepthTexture) 250 | .vec2('lightDepthSize', lightDepthTexture.width, lightDepthTexture.height) 251 | drawScene displayShader 252 | 253 | draw = -> 254 | drawLight() 255 | drawCamera() 256 | 257 | ## mainloop ## 258 | draw() 259 | gl.animationInterval -> 260 | if hover 261 | if animate 262 | offset = 1 + Math.sin(counter) 263 | counter += 1/30 264 | else 265 | offset = 0 266 | draw() 267 | -------------------------------------------------------------------------------- /lerp-shadow.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var animate, camDist, camPitch, camProj, camRot, camView, canvas, container, controls, counter, cubeGeom, displayShader, draw, drawCamera, drawLight, drawScene, floatExt, fullscreenImg, gl, hover, lightDepthTexture, lightFramebuffer, lightProj, lightRot, lightShader, lightView, model, mousemove, mouseup, name, offset, planeGeom; 3 | 4 | name = 'lerp-shadow'; 5 | 6 | document.write("
"); 7 | 8 | container = $('#' + name); 9 | 10 | canvas = $('').appendTo(container)[0]; 11 | 12 | try { 13 | gl = new WebGLFramework(canvas).depthTest(); 14 | floatExt = gl.getFloatExtension({ 15 | require: ['renderable'] 16 | }); 17 | } catch (error) { 18 | container.empty(); 19 | $('
').text(error).appendTo(container); 20 | $('
').text('(screenshot instead)').appendTo(container); 21 | $("").appendTo(container); 22 | return; 23 | } 24 | 25 | fullscreenImg = $('').appendTo(container).click(function() { 26 | return gl.toggleFullscreen(container[0]); 27 | }); 28 | 29 | gl.onFullscreenChange(function(isFullscreen) { 30 | if (isFullscreen) { 31 | container.addClass('fullscreen'); 32 | return fullscreenImg.attr('src', 'exit-fullscreen.png'); 33 | } else { 34 | container.removeClass('fullscreen'); 35 | return fullscreenImg.attr('src', 'fullscreen.png'); 36 | } 37 | }); 38 | 39 | hover = false; 40 | 41 | container.hover((function() { 42 | return hover = true; 43 | }), (function() { 44 | return hover = false; 45 | })); 46 | 47 | animate = true; 48 | 49 | controls = $('
').appendTo(container); 50 | 51 | $('').appendTo(controls); 52 | 53 | $('').appendTo(controls).change(function() { 54 | return animate = this.checked; 55 | }); 56 | 57 | cubeGeom = gl.drawable(meshes.cube); 58 | 59 | planeGeom = gl.drawable(meshes.plane(50)); 60 | 61 | displayShader = gl.shader({ 62 | common: '#line 54 lerp-shadow.coffee\nvarying vec3 vWorldNormal; varying vec4 vWorldPosition;\nuniform mat4 camProj, camView;\nuniform mat4 lightProj, lightView; uniform mat3 lightRot;\nuniform mat4 model;', 63 | vertex: '#line 60 lerp-shadow.coffee\nattribute vec3 position, normal;\n\nvoid main(){\n vWorldNormal = normal;\n vWorldPosition = model * vec4(position, 1.0);\n gl_Position = camProj * camView * vWorldPosition;\n}', 64 | fragment: '#line 69 lerp-shadow.coffee\nuniform sampler2D sLightDepth;\nuniform vec2 lightDepthSize;\n\nfloat texture2DCompare(sampler2D depths, vec2 uv, float compare){\n float depth = texture2D(depths, uv).r;\n return step(compare, depth);\n}\n\nfloat texture2DShadowLerp(sampler2D depths, vec2 size, vec2 uv, float compare){\n vec2 texelSize = vec2(1.0)/size;\n vec2 f = fract(uv*size+0.5);\n vec2 centroidUV = floor(uv*size+0.5)/size;\n\n float lb = texture2DCompare(depths, centroidUV+texelSize*vec2(0.0, 0.0), compare);\n float lt = texture2DCompare(depths, centroidUV+texelSize*vec2(0.0, 1.0), compare);\n float rb = texture2DCompare(depths, centroidUV+texelSize*vec2(1.0, 0.0), compare);\n float rt = texture2DCompare(depths, centroidUV+texelSize*vec2(1.0, 1.0), compare);\n float a = mix(lb, lt, f.y);\n float b = mix(rb, rt, f.y);\n float c = mix(a, b, f.x);\n return c;\n}\n\nfloat attenuation(vec3 dir){\n float dist = length(dir);\n float radiance = 1.0/(1.0+pow(dist/10.0, 2.0));\n return clamp(radiance*10.0, 0.0, 1.0);\n}\n\nfloat influence(vec3 normal, float coneAngle){\n float minConeAngle = ((360.0-coneAngle-10.0)/360.0)*PI;\n float maxConeAngle = ((360.0-coneAngle)/360.0)*PI;\n return smoothstep(minConeAngle, maxConeAngle, acos(normal.z));\n}\n\nfloat lambert(vec3 surfaceNormal, vec3 lightDirNormal){\n return max(0.0, dot(surfaceNormal, lightDirNormal));\n}\n\nvec3 skyLight(vec3 normal){\n return vec3(smoothstep(0.0, PI, PI-acos(normal.y)))*0.4;\n}\n\nvec3 gamma(vec3 color){\n return pow(color, vec3(2.2));\n}\n\nvoid main(){\n vec3 worldNormal = normalize(vWorldNormal);\n\n vec3 camPos = (camView * vWorldPosition).xyz;\n vec3 lightPos = (lightView * vWorldPosition).xyz;\n vec3 lightPosNormal = normalize(lightPos);\n vec3 lightSurfaceNormal = lightRot * worldNormal;\n vec4 lightDevice = lightProj * vec4(lightPos, 1.0);\n vec2 lightDeviceNormal = lightDevice.xy/lightDevice.w;\n vec2 lightUV = lightDeviceNormal*0.5+0.5;\n\n // shadow calculation\n float bias = 0.001;\n float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0)-bias;\n float illuminated = texture2DShadowLerp(sLightDepth, lightDepthSize, lightUV, lightDepth2);\n \n vec3 excident = (\n skyLight(worldNormal) +\n lambert(lightSurfaceNormal, -lightPosNormal) *\n influence(lightPosNormal, 55.0) *\n attenuation(lightPos) *\n illuminated\n );\n gl_FragColor = vec4(gamma(excident), 1.0);\n}' 65 | }); 66 | 67 | lightShader = gl.shader({ 68 | common: '#line 145 lerp-shadow.coffee\nvarying vec3 vWorldNormal; varying vec4 vWorldPosition;\nuniform mat4 lightProj, lightView; uniform mat3 lightRot;\nuniform mat4 model;', 69 | vertex: '#line 150 lerp-shadow.coffee\nattribute vec3 position, normal;\n\nvoid main(){\n vWorldNormal = normal;\n vWorldPosition = model * vec4(position, 1.0);\n gl_Position = lightProj * lightView * vWorldPosition;\n}', 70 | fragment: '#line 159 lerp-shadow.coffee\nvoid main(){\n vec3 worldNormal = normalize(vWorldNormal);\n vec3 lightPos = (lightView * vWorldPosition).xyz;\n float depth = clamp(length(lightPos)/40.0, 0.0, 1.0);\n gl_FragColor = vec4(vec3(depth), 1.0);\n}' 71 | }); 72 | 73 | lightDepthTexture = gl.texture({ 74 | type: 'float', 75 | channels: 'rgba' 76 | }).bind().setSize(64, 64).nearest().clampToEdge(); 77 | 78 | lightFramebuffer = gl.framebuffer().bind().color(lightDepthTexture).depth().unbind(); 79 | 80 | camProj = gl.mat4(); 81 | 82 | camView = gl.mat4(); 83 | 84 | lightProj = gl.mat4().perspective({ 85 | fov: 60 86 | }, 1, { 87 | near: 0.01, 88 | far: 100 89 | }); 90 | 91 | lightView = gl.mat4().trans(0, 0, -6).rotatex(30).rotatey(110); 92 | 93 | lightRot = gl.mat3().fromMat4Rot(lightView); 94 | 95 | model = gl.mat4(); 96 | 97 | counter = -Math.PI * 0.5; 98 | 99 | offset = 0; 100 | 101 | camDist = 10; 102 | 103 | camRot = 55; 104 | 105 | camPitch = 41; 106 | 107 | mouseup = function() { 108 | return $(document).unbind('mousemove', mousemove).unbind('mouseup', mouseup); 109 | }; 110 | 111 | mousemove = function(_arg) { 112 | var originalEvent, x, y, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; 113 | originalEvent = _arg.originalEvent; 114 | x = (_ref = (_ref1 = (_ref2 = originalEvent.movementX) != null ? _ref2 : originalEvent.webkitMovementX) != null ? _ref1 : originalEvent.mozMovementX) != null ? _ref : originalEvent.oMovementX; 115 | y = (_ref3 = (_ref4 = (_ref5 = originalEvent.movementY) != null ? _ref5 : originalEvent.webkitMovementY) != null ? _ref4 : originalEvent.mozMovementY) != null ? _ref3 : originalEvent.oMovementY; 116 | camRot += x; 117 | camPitch += y; 118 | if (camPitch > 85) { 119 | return camPitch = 85; 120 | } else if (camPitch < 1) { 121 | return camPitch = 1; 122 | } 123 | }; 124 | 125 | $(canvas).bind('mousedown', function() { 126 | $(document).bind('mousemove', mousemove).bind('mouseup', mouseup); 127 | return false; 128 | }).bind('mousewheel', function(_arg) { 129 | var originalEvent; 130 | originalEvent = _arg.originalEvent; 131 | camDist -= originalEvent.wheelDeltaY / 250; 132 | return false; 133 | }).bind('DOMMouseScroll', function(_arg) { 134 | var originalEvent; 135 | originalEvent = _arg.originalEvent; 136 | camDist += originalEvent.detail / 5; 137 | return false; 138 | }); 139 | 140 | drawScene = function(shader) { 141 | return shader.mat4('model', model.ident().trans(0, 0, 0)).draw(planeGeom).mat4('model', model.ident().trans(0, 1 + offset, 0)).draw(cubeGeom).mat4('model', model.ident().trans(5, 1, -1)).draw(cubeGeom); 142 | }; 143 | 144 | drawLight = function() { 145 | lightFramebuffer.bind(); 146 | gl.viewport(0, 0, lightDepthTexture.width, lightDepthTexture.height).clearColor(1, 1, 1, 1).clearDepth(1).cullFace('front'); 147 | lightShader.use().mat4('lightView', lightView).mat4('lightProj', lightProj).mat3('lightRot', lightRot); 148 | drawScene(lightShader); 149 | return lightFramebuffer.unbind(); 150 | }; 151 | 152 | drawCamera = function() { 153 | gl.adjustSize().viewport().cullFace('back').clearColor(0, 0, 0, 0).clearDepth(1); 154 | camProj.perspective({ 155 | fov: 60, 156 | aspect: gl.aspect, 157 | near: 0.01, 158 | far: 100 159 | }); 160 | camView.ident().trans(0, -1, -camDist).rotatex(camPitch).rotatey(camRot); 161 | displayShader.use().mat4('camProj', camProj).mat4('camView', camView).mat4('lightView', lightView).mat4('lightProj', lightProj).mat3('lightRot', lightRot).sampler('sLightDepth', lightDepthTexture).vec2('lightDepthSize', lightDepthTexture.width, lightDepthTexture.height); 162 | return drawScene(displayShader); 163 | }; 164 | 165 | draw = function() { 166 | drawLight(); 167 | return drawCamera(); 168 | }; 169 | 170 | draw(); 171 | 172 | gl.animationInterval(function() { 173 | if (hover) { 174 | if (animate) { 175 | offset = 1 + Math.sin(counter); 176 | counter += 1 / 30; 177 | } else { 178 | offset = 0; 179 | } 180 | return draw(); 181 | } 182 | }); 183 | 184 | }).call(this); 185 | -------------------------------------------------------------------------------- /lerp-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyalot/soft-shadow-mapping/e425c99c93a11f2b46f01b37bf47e10069133144/lerp-shadow.png -------------------------------------------------------------------------------- /markup: -------------------------------------------------------------------------------- 1 | :meta 2 | title Soft Shadow Mapping 3 | tags webgl 3d howto 4 | created 2013-2-15 16:30 5 | 6 | :html 7 | 70 | 71 | 72 | 73 | Shadow mapping is one of those things that a lot of people struggle with. It is also a very old shadowing technique that has been improved in a variety of ways. I'd like to make a brief trip trough the history of shadow mapping hopefully shedding some light on the topic and introduce you to some very nice techniques. 74 | 75 | :toc2 76 | 77 | Preface 78 | ======= 79 | 80 | The examples in this article use [http://www.khronos.org/webgl/ WebGL] and a set of specific capabilities such as *floating point texture render targets*. You might not have support for these features. In that case the illustrations are non-interactive screenshots. 81 | 82 | Scope 83 | ----- 84 | 85 | This blog post can't cover all shadowing topics, or even most optimizations you can apply. It will cover the basics of several shadow mapping techniques in a very simple and not optimized setup. 86 | 87 | Syntax 88 | ------ 89 | 90 | All examples and supporting code is written in [http://coffeescript.org/ CoffeeScript]. The reason is that I find CoffeeScript pleasant, it allows me to represent the subject matter clearly and it supports multiline strings (unlike javascript) which makes writing shaders much easier. 91 | 92 | Debugging 93 | --------- 94 | 95 | To aid shader debugging I have introduced a *custom build* step that seeks out "//essl" and replaces it with a #line directive indicating a sourceline and file. This is not necessary to run, since "//essl" would *just be a comment in essl* otherwise. However if you plan to *toy with the code*, I recommend you *use that buildstep* as it makes debugging shaders *much easier*. 96 | 97 | Code 98 | ---- 99 | 100 | You can obtain the code for all examples on github. 101 | 102 | Play 103 | ---- 104 | 105 | All illustrations are interactive. 106 | 107 | * Left click+drag for changing viewing angles 108 | * Scroll for zoom in/out 109 | * Stop/Start animations (button top right in the example viewport) 110 | * Make fullscreen (button bottom right in the example viewport) 111 | 112 | Recommended Reading 113 | ------------------- 114 | 115 | You can read everything I'm going to explain here and much more in the [http://www.amazon.com/gp/product/1568814380/ref=as_li_qf_sp_asin_il_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=1568814 book real-time shadows] 116 | 117 | :html 118 | 119 | 120 | The [https://developer.nvidia.com/gpu-gems GPU Gems series] by nvidia also has a wealth of shadowing information (freely available online) 121 | 122 | * GPU Gems 1, Chapter 9: [http://http.developer.nvidia.com/GPUGems/gpugems_ch09.html Efficient Shadow Volume Rendering] 123 | * GPU Gems 1, Chapter 11: [http://http.developer.nvidia.com/GPUGems/gpugems_ch11.html Shadow Map Antialiasing] 124 | * GPU Gems 1, Chapter 12: [http://http.developer.nvidia.com/GPUGems/gpugems_ch12.html Omnidirectional Shadow Mapping] 125 | * GPU Gems 1, Chapter 13: [http://http.developer.nvidia.com/GPUGems/gpugems_ch13.html Generating Soft Shadows Using Occlusion Interval Maps] 126 | * GPU Gems 1, Chapter 14: [http://http.developer.nvidia.com/GPUGems/gpugems_ch14.html Perspective Shadow Maps: Care and Feeding] 127 | * GPU Gems 2, Chapter 17: [http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter17.html Efficient Soft-Edged Shadows Using Pixel Shader Branching] 128 | * GPU Gems 3, Chapter 8: [http://http.developer.nvidia.com/GPUGems3/gpugems3_ch08.html Summed-Area Variance Shadow Maps] 129 | * GPU Gems 3, Chapter 10: [http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html Parallel-Split Shadow Maps on Programmable GPUs] 130 | * GPU Gems 3, Chapter 11: [http://http.developer.nvidia.com/GPUGems3/gpugems3_ch11.html Efficient and Robust Shadow Volumes Using Hierarchical Occlusion Culling and Geometry Shaders] 131 | 132 | Test scene without shadow 133 | ========================= 134 | 135 | :html 136 | 137 | 138 | In this [no-shadow.coffee example] a simple spotlight is setup that has the following characteristics: 139 | 140 | * Attenuation with distance 141 | * Influence area of 55 degrees 142 | * 10 degree smoothing for the influence 143 | * A simple lambertian surface radiance 144 | * A 64x64 pixel shadow map is used (that is a low resolution). The purpose is to visualize error well. 145 | 146 | This setup will be the basis for the further examples. 147 | 148 | Conventions and Spaces 149 | ---------------------- 150 | 151 | Since shadow mapping is a variant of projective texturing, it is important to have a clear convention in what "space" a given data point is expressed. I use these conventions. 152 | 153 | * World: The world space, positions and normals in this space are *independent of viewpoint or model transformations*. I prefix variables in this space with "world". 154 | * Camera: This space expresses things in relation to the *observers viewpoint*. The prefix "cam" is used. 155 | * Light: This space expresses things in relation to the *light viewpoint*, the prefix "light" is used. 156 | 157 | Furthermore each space might have distinctive variants these are: 158 | 159 | * View: The *translational/rotational* transformation to this space (such as camView, lightView). 160 | * Projection: The transform to device coordinates, using a projective matrix (usually perspective, such as camProj, lightProj). 161 | * UV: The *texture coordinates* of a data point (obviously important for shadow mapping). 162 | 163 | Preferred space for calculations 164 | -------------------------------- 165 | 166 | Lighting *calculations* in this tutorial are done *in light space*. The obvious benefit of this is to *avoid convoluted transformations* back and forth between light space and camera space for shadow mapping. As a consequence the light is defined in terms of a transform/rotation matrix and projection, rather than as a light position and direction. 167 | 168 | Hard Shadow Mapping 169 | =================== 170 | 171 | :html 172 | 173 | 174 | This [hard-shadow.coffee example] looks a bit better. 175 | 176 | Method 177 | ------ 178 | 179 | The *depth from the lights* point of view is rendered into a texture. This texture is then looked up in the shader and compared to the calculated depth in the camera pass in lightspace. 180 | 181 | :code glsl 182 | float lightDepth1 = texture2D(sLightDepth, lightUV).r; 183 | float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0); 184 | float bias = 0.001; 185 | float illuminated = step(lightDepth2, lightDepth1+bias); 186 | 187 | If lightDepth1+bias is bigger than lightDepth2 then an area is considered to be illuminated. 188 | 189 | The *depth* value is *linear* and clamped to 0 and 1. In hard shadow mapping this serves no immediate purpose but it will become important later on. The *value 40* is chosen because at that distance given the light attenuation (and using a gamma of 2.2) the *observable radiance* has fallen below 0.5/256th and is hence *insignificant*. It is in fact the *far range of the light*. 190 | 191 | Shading vs. Shadowing 192 | --------------------- 193 | 194 | It deserves mention that the example code does shading (attenuation with distance, influence on the cone, surface radiance evaluation) at the same time as it computes the shadow. 195 | 196 | The reason to do it this way is that *shadow mapping* algorithms have *artifacts*. But a lot of these artifacts are actually *not visible* once a scene *is shaded*. Hence it is good practise to evaluate them together. 197 | 198 | Drawbacks 199 | --------- 200 | 201 | There are some obvious problems with this method: 202 | 203 | * Aliasing is visible from the light depth compare. 204 | * The shadow border is very hard. 205 | 206 | However it has the advantage of being fairly fast. 207 | 208 | Interpolated shadowing 209 | ====================== 210 | 211 | :html 212 | 213 | 214 | The idea of this [lerp-shadow.coffee example] is to linear interpolate shadow lookup. This functionality is present in actual OpenGL as texture2DShadow. We don't have that in WebGL, so let's reimplement it. 215 | 216 | Method 217 | ------ 218 | 219 | I introduce a new texturing function that does the same thing as in hard shadow mapping. 220 | 221 | :code glsl 222 | float texture2DCompare(sampler2D depths, vec2 uv, float compare){ 223 | float depth = texture2D(depths, uv).r; 224 | return step(compare, depth); 225 | } 226 | 227 | Then this function is used to perform 4 lookups into the surrounding 4 texels, hence 4 compares are done. 228 | 229 | :code glsl 230 | float texture2DShadowLerp(sampler2D depths, vec2 size, vec2 uv, float compare){ 231 | vec2 texelSize = vec2(1.0)/size; 232 | vec2 f = fract(uv*size+0.5); 233 | vec2 centroidUV = floor(uv*size+0.5)/size; 234 | 235 | float lb = texture2DCompare(depths, centroidUV+texelSize*vec2(0.0, 0.0), compare); 236 | float lt = texture2DCompare(depths, centroidUV+texelSize*vec2(0.0, 1.0), compare); 237 | float rb = texture2DCompare(depths, centroidUV+texelSize*vec2(1.0, 0.0), compare); 238 | float rt = texture2DCompare(depths, centroidUV+texelSize*vec2(1.0, 1.0), compare); 239 | float a = mix(lb, lt, f.y); 240 | float b = mix(rb, rt, f.y); 241 | float c = mix(a, b, f.x); 242 | return c; 243 | } 244 | 245 | The resulting illumination results are bilinearly interpolated and returned. 246 | 247 | Drawbacks 248 | --------- 249 | 250 | * It does *not* offer *a much improvement*, it just smooths the shadowing a bit between texels. 251 | * The *cost has gone up* as 4 lookups are performed now. They are expensive because shadow lookups are not vram/cache coherent. 252 | * The bilinear interpolation *introduces artifacts* of its own such as the typical diamond pattern. 253 | * There can be depth error artifacts because now linear interpolation isn't a depth estimator 254 | * Aliasing is still clearly visible 255 | 256 | Percentage Closer Filtering (PCF) 257 | ================================= 258 | 259 | :html 260 | 261 | 262 | The idea behind this [pcf-shadow.coffee example] is to simply *average the result of the compare* over a patch surrounding the uv coordinate. It offers a similar result to linear interpolation, but up close it looks pretty horrible. 263 | 264 | Method 265 | ------ 266 | 267 | We replace the texture2DShadowLerp function with the PCF function. This looks up the shadow compare for a 5x5 region with the UV coordinate at the center, then divides the result by 25. 268 | 269 | :code glsl 270 | float PCF(sampler2D depths, vec2 size, vec2 uv, float compare){ 271 | float result = 0.0; 272 | for(int x=-2; x<=2; x++){ 273 | for(int y=-2; y<=2; y++){ 274 | vec2 off = vec2(x,y)/size; 275 | result += texture2DCompare(depths, uv+off, compare); 276 | } 277 | } 278 | return result/25.0; 279 | } 280 | 281 | Drawbacks 282 | --------- 283 | 284 | * It is even *more expensive* than linear interpolation. 285 | * It introduces a banding artifacts over the sample kernel. 286 | 287 | PCF and Interpolation 288 | ===================== 289 | 290 | :html 291 | 292 | 293 | This [pcf-lerp-shadow.coffee example] combines linear interpolation and PCF. This is starting to look fairly acceptable. 294 | 295 | Method 296 | ------ 297 | 298 | :code glsl 299 | float PCF(sampler2D depths, vec2 size, vec2 uv, float compare){ 300 | float result = 0.0; 301 | for(int x=-1; x<=1; x++){ 302 | for(int y=-1; y<=1; y++){ 303 | vec2 off = vec2(x,y)/size; 304 | result += texture2DShadowLerp(depths, size, uv+off, compare); 305 | } 306 | } 307 | return result/9.0; 308 | } 309 | 310 | The size of the kernel is reduced since with linear interpolation it does not have to be very big. The kernel banding is mostly gone. Some artifacts of aliasing are still visible, but much less so than previously. The quality of this method is quite good, of course at a cost. 311 | 312 | Drawbacks 313 | --------- 314 | 315 | * It's even more expensive than PCF alone. Although if there was a texture2DShadow function built in, that would obviously be a faster than reimplementing it in ESSL. 316 | 317 | Variance Shadow Mapping (VSM) 318 | ============================= 319 | 320 | :html 321 | 322 | 323 | The idea behind [vsm-shadow.coffee this] is to statistically measure the *likelyhood of occlusion* based on [http://en.wikipedia.org/wiki/Variance variance]. [http://en.wikipedia.org/wiki/Chebyshev's_inequality Chebeyshevs inequality] is used to compute an upper bound for the occlusion. It looks very similar to linear interpolated shadow mapping. 324 | 325 | There several advantages to the technique. 326 | 327 | * It is *very cheap* (just one lookup per fragment) 328 | * It makes it possible to *pre-filter the depths* 329 | 330 | Important: VSM only works with linear depths starting at 0 near the light and going to 1 to the far range of the light. 331 | 332 | Method 333 | ------ 334 | 335 | First we need to compute the moments of the depths. This is done during light depth rendering: 336 | 337 | :code glsl 338 | float dx = dFdx(depth); 339 | float dy = dFdy(depth); 340 | gl_FragColor = vec4(depth, pow(depth, 2.0) + 0.25*(dx*dx + dy*dy), 0.0, 1.0); 341 | 342 | In order to write the variance function we need a linstep function analogous to smoothstep: 343 | 344 | :code glsl 345 | float linstep(float low, float high, float v){ 346 | return clamp((v-low)/(high-low), 0.0, 1.0); 347 | } 348 | 349 | Then the variance has to be used to compute the shadowing function: 350 | 351 | :code glsl 352 | float VSM(sampler2D depths, vec2 uv, float compare){ 353 | vec2 moments = texture2D(depths, uv).xy; 354 | float p = smoothstep(compare-0.02, compare, moments.x); 355 | float variance = max(moments.y - moments.x*moments.x, -0.001); 356 | float d = compare - moments.x; 357 | float p_max = linstep(0.2, 1.0, variance / (variance + d*d)); 358 | return clamp(max(p, p_max), 0.0, 1.0); 359 | } 360 | 361 | There are a couple of noteworthy ways in which this works. 362 | 363 | * "p" holds a hard shadow comparision, however the bias is applied softly via a smoothstep 364 | * p_max is stepped between 0.2 and 1.0, this reduces an artifact known as light bleeding. 365 | 366 | Drawbacks 367 | --------- 368 | 369 | * In its plain form (unfiltered) VSM isn't better than linear interpolated shadows. 370 | * Due to a need to sample the front surfaces, there can be *more depth error banding issues*. 371 | 372 | Antialiased and Filtered VSM 373 | ============================ 374 | 375 | :html 376 | 377 | 378 | The idea of this [vsm-filtered-shadow.coffee example] is to *antialias* the shadow depths first and then *blur* them slightly. The result is *substantially better* than anything so far. 379 | 380 | Antialias 381 | --------- 382 | 383 | If there was Framebuffer MSAA or similar in WebGL we could use this. As it is, this is not a choice, so let's reimplement anti-aliasing in a fast brute force method. The light depth is rendered at 256x256 resolution and then supersampled efficiently with linear interpolation first to 128x128 and then to 64x64. This is equivalent to *4x4 MSAA*. 384 | 385 | The definition of the filters: 386 | 387 | :code coffeescript 388 | downsample128 = new Filter 128, '''//essl 389 | return get(0.0, 0.0); 390 | ''' 391 | 392 | downsample64 = new Filter 64, '''//essl 393 | return get(0.0, 0.0); 394 | ''' 395 | 396 | Applying them after rendering the light depth: 397 | 398 | :code coffeescript 399 | downsample128.apply lightDepthTexture 400 | downsample64.apply downsample128 401 | 402 | Blur 403 | ---- 404 | 405 | A simple 3x3 box filter is then used on the downsampled 64x64 light depths. 406 | 407 | :code coffeescript 408 | boxFilter = new Filter 64, '''//essl 409 | vec3 result = vec3(0.0); 410 | for(int x=-1; x<=1; x++){ 411 | for(int y=-1; y<=1; y++){ 412 | result += get(x,y); 413 | } 414 | } 415 | return result/9.0; 416 | ''' 417 | 418 | And applying after downsampling: 419 | 420 | :code coffeescript 421 | boxFilter.apply downsample64 422 | 423 | Now instead of passing in the lightDepthTexture for VSM, we pass in the boxFilter texture, the VSM code is unchanged: 424 | 425 | :code coffeescript 426 | .sampler('sLightDepth', boxFilter) 427 | 428 | Advantage 429 | --------- 430 | 431 | Unlike when filtering at shadow application, the filtering with VSM can be done prior at light depth texture resolution, this offers the following advantages: 432 | 433 | * Filtering the light depth texture is *VRAM/cache coherent*. 434 | * The *resolution* of the light depth texture is usually *smaller* than fragments on screen (this example only uses 64x64 light depth texels) 435 | * In forward shading there *might be overdraw*, which would cause multiple lookups into the light depth texture that are never used. 436 | -------------------------------------------------------------------------------- /meshes.coffee: -------------------------------------------------------------------------------- 1 | modelPointers =[ 2 | {name: 'position', size: 3, offset: 0, stride: 6}, 3 | {name: 'normal', size: 3, offset: 3, stride: 6}, 4 | ] 5 | 6 | window.meshes = 7 | quad: 8 | pointers: [ 9 | {name: 'position', size: 2, offset: 0, stride: 2}, 10 | ] 11 | vertices: [ 12 | -1, -1, 1, -1, 1, 1, 13 | -1, 1, -1, -1, 1, 1, 14 | ] 15 | 16 | plane: (s) -> 17 | pointers: modelPointers 18 | vertices: [ 19 | -s, 0, -s, 0, 1, 0, 20 | -s, 0, s, 0, 1, 0, 21 | s, 0, s, 0, 1, 0, 22 | s, 0, -s, 0, 1, 0, 23 | -s, 0, -s, 0, 1, 0, 24 | s, 0, s, 0, 1, 0, 25 | ] 26 | 27 | cube: 28 | pointers: modelPointers 29 | vertices: [ 30 | -1, -1, -1, 0, 0, -1, 31 | -1, 1, -1, 0, 0, -1, 32 | 1, 1, -1, 0, 0, -1, 33 | 1, -1, -1, 0, 0, -1, 34 | -1, -1, -1, 0, 0, -1, 35 | 1, 1, -1, 0, 0, -1, 36 | 37 | 1, 1, 1, 0, 0, 1, 38 | -1, 1, 1, 0, 0, 1, 39 | -1, -1, 1, 0, 0, 1, 40 | 1, 1, 1, 0, 0, 1, 41 | -1, -1, 1, 0, 0, 1, 42 | 1, -1, 1, 0, 0, 1, 43 | 44 | -1, 1, -1, 0, 1, 0, 45 | -1, 1, 1, 0, 1, 0, 46 | 1, 1, 1, 0, 1, 0, 47 | 1, 1, -1, 0, 1, 0, 48 | -1, 1, -1, 0, 1, 0, 49 | 1, 1, 1, 0, 1, 0, 50 | 51 | 1, -1, 1, 0, -1, 0, 52 | -1, -1, 1, 0, -1, 0, 53 | -1, -1, -1, 0, -1, 0, 54 | 1, -1, 1, 0, -1, 0, 55 | -1, -1, -1, 0, -1, 0, 56 | 1, -1, -1, 0, -1, 0, 57 | 58 | -1, -1, -1, -1, 0, 0, 59 | -1, -1, 1, -1, 0, 0, 60 | -1, 1, 1, -1, 0, 0, 61 | -1, 1, -1, -1, 0, 0, 62 | -1, -1, -1, -1, 0, 0, 63 | -1, 1, 1, -1, 0, 0, 64 | 65 | 1, 1, 1, 1, 0, 0, 66 | 1, -1, 1, 1, 0, 0, 67 | 1, -1, -1, 1, 0, 0, 68 | 1, 1, 1, 1, 0, 0, 69 | 1, -1, -1, 1, 0, 0, 70 | 1, 1, -1, 1, 0, 0, 71 | ] 72 | -------------------------------------------------------------------------------- /meshes.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var modelPointers; 3 | 4 | modelPointers = [ 5 | { 6 | name: 'position', 7 | size: 3, 8 | offset: 0, 9 | stride: 6 10 | }, { 11 | name: 'normal', 12 | size: 3, 13 | offset: 3, 14 | stride: 6 15 | } 16 | ]; 17 | 18 | window.meshes = { 19 | quad: { 20 | pointers: [ 21 | { 22 | name: 'position', 23 | size: 2, 24 | offset: 0, 25 | stride: 2 26 | } 27 | ], 28 | vertices: [-1, -1, 1, -1, 1, 1, -1, 1, -1, -1, 1, 1] 29 | }, 30 | plane: function(s) { 31 | return { 32 | pointers: modelPointers, 33 | vertices: [-s, 0, -s, 0, 1, 0, -s, 0, s, 0, 1, 0, s, 0, s, 0, 1, 0, s, 0, -s, 0, 1, 0, -s, 0, -s, 0, 1, 0, s, 0, s, 0, 1, 0] 34 | }; 35 | }, 36 | cube: { 37 | pointers: modelPointers, 38 | vertices: [-1, -1, -1, 0, 0, -1, -1, 1, -1, 0, 0, -1, 1, 1, -1, 0, 0, -1, 1, -1, -1, 0, 0, -1, -1, -1, -1, 0, 0, -1, 1, 1, -1, 0, 0, -1, 1, 1, 1, 0, 0, 1, -1, 1, 1, 0, 0, 1, -1, -1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, -1, -1, 1, 0, 0, 1, 1, -1, 1, 0, 0, 1, -1, 1, -1, 0, 1, 0, -1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, -1, 0, 1, 0, -1, 1, -1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, -1, 1, 0, -1, 0, -1, -1, 1, 0, -1, 0, -1, -1, -1, 0, -1, 0, 1, -1, 1, 0, -1, 0, -1, -1, -1, 0, -1, 0, 1, -1, -1, 0, -1, 0, -1, -1, -1, -1, 0, 0, -1, -1, 1, -1, 0, 0, -1, 1, 1, -1, 0, 0, -1, 1, -1, -1, 0, 0, -1, -1, -1, -1, 0, 0, -1, 1, 1, -1, 0, 0, 1, 1, 1, 1, 0, 0, 1, -1, 1, 1, 0, 0, 1, -1, -1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, -1, -1, 1, 0, 0, 1, 1, -1, 1, 0, 0] 39 | } 40 | }; 41 | 42 | }).call(this); 43 | -------------------------------------------------------------------------------- /no-shadow.coffee: -------------------------------------------------------------------------------- 1 | ## add the canvas to the dom ## 2 | name = 'no-shadow' 3 | document.write "
" 4 | container = $('#' + name) 5 | canvas = $('').appendTo(container)[0] 6 | 7 | ## setup the framework ## 8 | try 9 | gl = new WebGLFramework(canvas) 10 | .depthTest() 11 | catch error 12 | container.empty() 13 | $('
').text(error).appendTo(container) 14 | $('
').text('(screenshot instead)').appendTo(container) 15 | $("").appendTo(container) 16 | return 17 | 18 | ## fullscreen handling ## 19 | fullscreenImg = $('') 20 | .appendTo(container) 21 | .click -> gl.toggleFullscreen(container[0]) 22 | 23 | gl.onFullscreenChange (isFullscreen) -> 24 | if isFullscreen 25 | container.addClass('fullscreen') 26 | fullscreenImg.attr('src', 'exit-fullscreen.png') 27 | else 28 | container.removeClass('fullscreen') 29 | fullscreenImg.attr('src', 'fullscreen.png') 30 | 31 | ## handle mouse over ## 32 | hover = false 33 | container.hover (-> hover = true), (-> hover = false) 34 | 35 | ## animation control ## 36 | animate = true 37 | controls = $('
') 38 | .appendTo(container) 39 | $('').appendTo(controls) 40 | $('') 41 | .appendTo(controls) 42 | .change -> 43 | animate = @checked 44 | 45 | ## create webgl objects ## 46 | cubeGeom = gl.drawable meshes.cube 47 | planeGeom = gl.drawable meshes.plane(50) 48 | displayShader = gl.shader 49 | common: '''//essl 50 | varying vec3 vWorldNormal; varying vec4 vWorldPosition; 51 | uniform mat4 camProj, camView; 52 | uniform mat4 lightProj, lightView; uniform mat3 lightRot; 53 | uniform mat4 model; 54 | ''' 55 | vertex: '''//essl 56 | attribute vec3 position, normal; 57 | 58 | void main(){ 59 | vWorldNormal = normal; 60 | vWorldPosition = model * vec4(position, 1.0); 61 | gl_Position = camProj * camView * vWorldPosition; 62 | } 63 | ''' 64 | fragment: '''//essl 65 | float attenuation(vec3 dir){ 66 | float dist = length(dir); 67 | float radiance = 1.0/(1.0+pow(dist/10.0, 2.0)); 68 | return clamp(radiance*10.0, 0.0, 1.0); 69 | } 70 | 71 | float influence(vec3 normal, float coneAngle){ 72 | float minConeAngle = ((360.0-coneAngle-10.0)/360.0)*PI; 73 | float maxConeAngle = ((360.0-coneAngle)/360.0)*PI; 74 | return smoothstep(minConeAngle, maxConeAngle, acos(normal.z)); 75 | } 76 | 77 | float lambert(vec3 surfaceNormal, vec3 lightDirNormal){ 78 | return max(0.0, dot(surfaceNormal, lightDirNormal)); 79 | } 80 | 81 | vec3 skyLight(vec3 normal){ 82 | return vec3(smoothstep(0.0, PI, PI-acos(normal.y)))*0.4; 83 | } 84 | 85 | vec3 gamma(vec3 color){ 86 | return pow(color, vec3(2.2)); 87 | } 88 | 89 | void main(){ 90 | vec3 worldNormal = normalize(vWorldNormal); 91 | 92 | vec3 camPos = (camView * vWorldPosition).xyz; 93 | vec3 lightPos = (lightView * vWorldPosition).xyz; 94 | vec3 lightPosNormal = normalize(lightPos); 95 | vec3 lightSurfaceNormal = lightRot * worldNormal; 96 | 97 | vec3 excident = ( 98 | skyLight(worldNormal) + 99 | lambert(lightSurfaceNormal, -lightPosNormal) * 100 | influence(lightPosNormal, 55.0) * 101 | attenuation(lightPos) 102 | ); 103 | gl_FragColor = vec4(gamma(excident), 1.0); 104 | } 105 | ''' 106 | 107 | ## matrix setup ## 108 | camProj = gl.mat4() 109 | camView = gl.mat4() 110 | lightProj = gl.mat4().perspective(fov:60, 1, near:0.01, far:100) 111 | lightView = gl.mat4().trans(0, 0, -6).rotatex(30).rotatey(110) 112 | lightRot = gl.mat3().fromMat4Rot(lightView) 113 | model = gl.mat4() 114 | 115 | ## state variables ## 116 | counter = -Math.PI*0.5 117 | offset = 0 118 | camDist = 10 119 | camRot = 55 120 | camPitch = 41 121 | 122 | ## mouse handling ## 123 | mouseup = -> $(document).unbind('mousemove', mousemove).unbind('mouseup', mouseup) 124 | mousemove = ({originalEvent}) -> 125 | x = originalEvent.movementX ? originalEvent.webkitMovementX ? originalEvent.mozMovementX ? originalEvent.oMovementX 126 | y = originalEvent.movementY ? originalEvent.webkitMovementY ? originalEvent.mozMovementY ? originalEvent.oMovementY 127 | camRot += x 128 | camPitch += y 129 | if camPitch > 85 then camPitch = 85 130 | else if camPitch < 1 then camPitch = 1 131 | 132 | $(canvas) 133 | .bind 'mousedown', -> 134 | $(document).bind('mousemove', mousemove).bind('mouseup', mouseup) 135 | return false 136 | .bind 'mousewheel', ({originalEvent}) -> 137 | camDist -= originalEvent.wheelDeltaY/250 138 | return false 139 | .bind 'DOMMouseScroll', ({originalEvent}) -> 140 | camDist += originalEvent.detail/5 141 | return false 142 | 143 | ## drawing methods ## 144 | draw = -> 145 | gl 146 | .adjustSize() 147 | .viewport() 148 | .cullFace('back') 149 | .clearColor(0,0,0,0) 150 | .clearDepth(1) 151 | 152 | camProj.perspective(fov:60, aspect:gl.aspect, near:0.01, far:100) 153 | camView.ident().trans(0, -1, -camDist).rotatex(camPitch).rotatey(camRot) 154 | 155 | displayShader.use() 156 | .mat4('camProj', camProj) 157 | .mat4('camView', camView) 158 | .mat4('lightView', lightView) 159 | .mat4('lightProj', lightProj) 160 | .mat3('lightRot', lightRot) 161 | .mat4('model', model.ident().trans(0, 0, 0)) 162 | .draw(planeGeom) 163 | .mat4('model', model.ident().trans(0, 1+offset, 0)) 164 | .draw(cubeGeom) 165 | .mat4('model', model.ident().trans(5, 1, -1)) 166 | .draw(cubeGeom) 167 | 168 | ## mainloop ## 169 | draw() 170 | gl.animationInterval -> 171 | if hover 172 | if animate 173 | offset = 1 + Math.sin(counter) 174 | counter += 1/30 175 | else 176 | offset = 0 177 | draw() 178 | -------------------------------------------------------------------------------- /no-shadow.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var animate, camDist, camPitch, camProj, camRot, camView, canvas, container, controls, counter, cubeGeom, displayShader, draw, fullscreenImg, gl, hover, lightProj, lightRot, lightView, model, mousemove, mouseup, name, offset, planeGeom; 3 | 4 | name = 'no-shadow'; 5 | 6 | document.write("
"); 7 | 8 | container = $('#' + name); 9 | 10 | canvas = $('').appendTo(container)[0]; 11 | 12 | try { 13 | gl = new WebGLFramework(canvas).depthTest(); 14 | } catch (error) { 15 | container.empty(); 16 | $('
').text(error).appendTo(container); 17 | $('
').text('(screenshot instead)').appendTo(container); 18 | $("").appendTo(container); 19 | return; 20 | } 21 | 22 | fullscreenImg = $('').appendTo(container).click(function() { 23 | return gl.toggleFullscreen(container[0]); 24 | }); 25 | 26 | gl.onFullscreenChange(function(isFullscreen) { 27 | if (isFullscreen) { 28 | container.addClass('fullscreen'); 29 | return fullscreenImg.attr('src', 'exit-fullscreen.png'); 30 | } else { 31 | container.removeClass('fullscreen'); 32 | return fullscreenImg.attr('src', 'fullscreen.png'); 33 | } 34 | }); 35 | 36 | hover = false; 37 | 38 | container.hover((function() { 39 | return hover = true; 40 | }), (function() { 41 | return hover = false; 42 | })); 43 | 44 | animate = true; 45 | 46 | controls = $('
').appendTo(container); 47 | 48 | $('').appendTo(controls); 49 | 50 | $('').appendTo(controls).change(function() { 51 | return animate = this.checked; 52 | }); 53 | 54 | cubeGeom = gl.drawable(meshes.cube); 55 | 56 | planeGeom = gl.drawable(meshes.plane(50)); 57 | 58 | displayShader = gl.shader({ 59 | common: '#line 49 no-shadow.coffee\nvarying vec3 vWorldNormal; varying vec4 vWorldPosition;\nuniform mat4 camProj, camView;\nuniform mat4 lightProj, lightView; uniform mat3 lightRot;\nuniform mat4 model;', 60 | vertex: '#line 55 no-shadow.coffee\nattribute vec3 position, normal;\n\nvoid main(){\n vWorldNormal = normal;\n vWorldPosition = model * vec4(position, 1.0);\n gl_Position = camProj * camView * vWorldPosition;\n}', 61 | fragment: '#line 64 no-shadow.coffee\nfloat attenuation(vec3 dir){\n float dist = length(dir);\n float radiance = 1.0/(1.0+pow(dist/10.0, 2.0));\n return clamp(radiance*10.0, 0.0, 1.0);\n}\n\nfloat influence(vec3 normal, float coneAngle){\n float minConeAngle = ((360.0-coneAngle-10.0)/360.0)*PI;\n float maxConeAngle = ((360.0-coneAngle)/360.0)*PI;\n return smoothstep(minConeAngle, maxConeAngle, acos(normal.z));\n}\n\nfloat lambert(vec3 surfaceNormal, vec3 lightDirNormal){\n return max(0.0, dot(surfaceNormal, lightDirNormal));\n}\n\nvec3 skyLight(vec3 normal){\n return vec3(smoothstep(0.0, PI, PI-acos(normal.y)))*0.4;\n}\n\nvec3 gamma(vec3 color){\n return pow(color, vec3(2.2));\n}\n\nvoid main(){\n vec3 worldNormal = normalize(vWorldNormal);\n\n vec3 camPos = (camView * vWorldPosition).xyz;\n vec3 lightPos = (lightView * vWorldPosition).xyz;\n vec3 lightPosNormal = normalize(lightPos);\n vec3 lightSurfaceNormal = lightRot * worldNormal;\n\n vec3 excident = (\n skyLight(worldNormal) +\n lambert(lightSurfaceNormal, -lightPosNormal) *\n influence(lightPosNormal, 55.0) *\n attenuation(lightPos)\n );\n gl_FragColor = vec4(gamma(excident), 1.0);\n}' 62 | }); 63 | 64 | camProj = gl.mat4(); 65 | 66 | camView = gl.mat4(); 67 | 68 | lightProj = gl.mat4().perspective({ 69 | fov: 60 70 | }, 1, { 71 | near: 0.01, 72 | far: 100 73 | }); 74 | 75 | lightView = gl.mat4().trans(0, 0, -6).rotatex(30).rotatey(110); 76 | 77 | lightRot = gl.mat3().fromMat4Rot(lightView); 78 | 79 | model = gl.mat4(); 80 | 81 | counter = -Math.PI * 0.5; 82 | 83 | offset = 0; 84 | 85 | camDist = 10; 86 | 87 | camRot = 55; 88 | 89 | camPitch = 41; 90 | 91 | mouseup = function() { 92 | return $(document).unbind('mousemove', mousemove).unbind('mouseup', mouseup); 93 | }; 94 | 95 | mousemove = function(_arg) { 96 | var originalEvent, x, y, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; 97 | originalEvent = _arg.originalEvent; 98 | x = (_ref = (_ref1 = (_ref2 = originalEvent.movementX) != null ? _ref2 : originalEvent.webkitMovementX) != null ? _ref1 : originalEvent.mozMovementX) != null ? _ref : originalEvent.oMovementX; 99 | y = (_ref3 = (_ref4 = (_ref5 = originalEvent.movementY) != null ? _ref5 : originalEvent.webkitMovementY) != null ? _ref4 : originalEvent.mozMovementY) != null ? _ref3 : originalEvent.oMovementY; 100 | camRot += x; 101 | camPitch += y; 102 | if (camPitch > 85) { 103 | return camPitch = 85; 104 | } else if (camPitch < 1) { 105 | return camPitch = 1; 106 | } 107 | }; 108 | 109 | $(canvas).bind('mousedown', function() { 110 | $(document).bind('mousemove', mousemove).bind('mouseup', mouseup); 111 | return false; 112 | }).bind('mousewheel', function(_arg) { 113 | var originalEvent; 114 | originalEvent = _arg.originalEvent; 115 | camDist -= originalEvent.wheelDeltaY / 250; 116 | return false; 117 | }).bind('DOMMouseScroll', function(_arg) { 118 | var originalEvent; 119 | originalEvent = _arg.originalEvent; 120 | camDist += originalEvent.detail / 5; 121 | return false; 122 | }); 123 | 124 | draw = function() { 125 | gl.adjustSize().viewport().cullFace('back').clearColor(0, 0, 0, 0).clearDepth(1); 126 | camProj.perspective({ 127 | fov: 60, 128 | aspect: gl.aspect, 129 | near: 0.01, 130 | far: 100 131 | }); 132 | camView.ident().trans(0, -1, -camDist).rotatex(camPitch).rotatey(camRot); 133 | return displayShader.use().mat4('camProj', camProj).mat4('camView', camView).mat4('lightView', lightView).mat4('lightProj', lightProj).mat3('lightRot', lightRot).mat4('model', model.ident().trans(0, 0, 0)).draw(planeGeom).mat4('model', model.ident().trans(0, 1 + offset, 0)).draw(cubeGeom).mat4('model', model.ident().trans(5, 1, -1)).draw(cubeGeom); 134 | }; 135 | 136 | draw(); 137 | 138 | gl.animationInterval(function() { 139 | if (hover) { 140 | if (animate) { 141 | offset = 1 + Math.sin(counter); 142 | counter += 1 / 30; 143 | } else { 144 | offset = 0; 145 | } 146 | return draw(); 147 | } 148 | }); 149 | 150 | }).call(this); 151 | -------------------------------------------------------------------------------- /no-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyalot/soft-shadow-mapping/e425c99c93a11f2b46f01b37bf47e10069133144/no-shadow.png -------------------------------------------------------------------------------- /pcf-lerp-shadow.coffee: -------------------------------------------------------------------------------- 1 | ## add the canvas to the dom ## 2 | name = 'pcf-lerp-shadow' 3 | document.write "
" 4 | container = $('#' + name) 5 | canvas = $('').appendTo(container)[0] 6 | 7 | ## setup the framework ## 8 | try 9 | gl = new WebGLFramework(canvas) 10 | .depthTest() 11 | 12 | floatExt = gl.getFloatExtension 13 | require: ['renderable'] 14 | 15 | catch error 16 | container.empty() 17 | $('
').text(error).appendTo(container) 18 | $('
').text('(screenshot instead)').appendTo(container) 19 | $("").appendTo(container) 20 | return 21 | 22 | ## fullscreen handling ## 23 | fullscreenImg = $('') 24 | .appendTo(container) 25 | .click -> gl.toggleFullscreen(container[0]) 26 | 27 | gl.onFullscreenChange (isFullscreen) -> 28 | if isFullscreen 29 | container.addClass('fullscreen') 30 | fullscreenImg.attr('src', 'exit-fullscreen.png') 31 | else 32 | container.removeClass('fullscreen') 33 | fullscreenImg.attr('src', 'fullscreen.png') 34 | 35 | ## handle mouse over ## 36 | hover = false 37 | container.hover (-> hover = true), (-> hover = false) 38 | 39 | ## animation control ## 40 | animate = true 41 | controls = $('
') 42 | .appendTo(container) 43 | $('').appendTo(controls) 44 | $('') 45 | .appendTo(controls) 46 | .change -> 47 | animate = @checked 48 | 49 | ## create webgl objects ## 50 | cubeGeom = gl.drawable meshes.cube 51 | planeGeom = gl.drawable meshes.plane(50) 52 | displayShader = gl.shader 53 | common: '''//essl 54 | varying vec3 vWorldNormal; varying vec4 vWorldPosition; 55 | uniform mat4 camProj, camView; 56 | uniform mat4 lightProj, lightView; uniform mat3 lightRot; 57 | uniform mat4 model; 58 | ''' 59 | vertex: '''//essl 60 | attribute vec3 position, normal; 61 | 62 | void main(){ 63 | vWorldNormal = normal; 64 | vWorldPosition = model * vec4(position, 1.0); 65 | gl_Position = camProj * camView * vWorldPosition; 66 | } 67 | ''' 68 | fragment: '''//essl 69 | uniform sampler2D sLightDepth; 70 | uniform vec2 lightDepthSize; 71 | 72 | float texture2DCompare(sampler2D depths, vec2 uv, float compare){ 73 | float depth = texture2D(depths, uv).r; 74 | return step(compare, depth); 75 | } 76 | 77 | float texture2DShadowLerp(sampler2D depths, vec2 size, vec2 uv, float compare){ 78 | vec2 texelSize = vec2(1.0)/size; 79 | vec2 f = fract(uv*size+0.5); 80 | vec2 centroidUV = floor(uv*size+0.5)/size; 81 | 82 | float lb = texture2DCompare(depths, centroidUV+texelSize*vec2(0.0, 0.0), compare); 83 | float lt = texture2DCompare(depths, centroidUV+texelSize*vec2(0.0, 1.0), compare); 84 | float rb = texture2DCompare(depths, centroidUV+texelSize*vec2(1.0, 0.0), compare); 85 | float rt = texture2DCompare(depths, centroidUV+texelSize*vec2(1.0, 1.0), compare); 86 | float a = mix(lb, lt, f.y); 87 | float b = mix(rb, rt, f.y); 88 | float c = mix(a, b, f.x); 89 | return c; 90 | } 91 | 92 | float PCF(sampler2D depths, vec2 size, vec2 uv, float compare){ 93 | float result = 0.0; 94 | for(int x=-1; x<=1; x++){ 95 | for(int y=-1; y<=1; y++){ 96 | vec2 off = vec2(x,y)/size; 97 | result += texture2DShadowLerp(depths, size, uv+off, compare); 98 | } 99 | } 100 | return result/9.0; 101 | } 102 | 103 | float attenuation(vec3 dir){ 104 | float dist = length(dir); 105 | float radiance = 1.0/(1.0+pow(dist/10.0, 2.0)); 106 | return clamp(radiance*10.0, 0.0, 1.0); 107 | } 108 | 109 | float influence(vec3 normal, float coneAngle){ 110 | float minConeAngle = ((360.0-coneAngle-10.0)/360.0)*PI; 111 | float maxConeAngle = ((360.0-coneAngle)/360.0)*PI; 112 | return smoothstep(minConeAngle, maxConeAngle, acos(normal.z)); 113 | } 114 | 115 | float lambert(vec3 surfaceNormal, vec3 lightDirNormal){ 116 | return max(0.0, dot(surfaceNormal, lightDirNormal)); 117 | } 118 | 119 | vec3 skyLight(vec3 normal){ 120 | return vec3(smoothstep(0.0, PI, PI-acos(normal.y)))*0.4; 121 | } 122 | 123 | vec3 gamma(vec3 color){ 124 | return pow(color, vec3(2.2)); 125 | } 126 | 127 | void main(){ 128 | vec3 worldNormal = normalize(vWorldNormal); 129 | 130 | vec3 camPos = (camView * vWorldPosition).xyz; 131 | vec3 lightPos = (lightView * vWorldPosition).xyz; 132 | vec3 lightPosNormal = normalize(lightPos); 133 | vec3 lightSurfaceNormal = lightRot * worldNormal; 134 | vec4 lightDevice = lightProj * vec4(lightPos, 1.0); 135 | vec2 lightDeviceNormal = lightDevice.xy/lightDevice.w; 136 | vec2 lightUV = lightDeviceNormal*0.5+0.5; 137 | 138 | // shadow calculation 139 | float bias = 0.001; 140 | float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0)-bias; 141 | float illuminated = PCF(sLightDepth, lightDepthSize, lightUV, lightDepth2); 142 | 143 | vec3 excident = ( 144 | skyLight(worldNormal) + 145 | lambert(lightSurfaceNormal, -lightPosNormal) * 146 | influence(lightPosNormal, 55.0) * 147 | attenuation(lightPos) * 148 | illuminated 149 | ); 150 | gl_FragColor = vec4(gamma(excident), 1.0); 151 | } 152 | ''' 153 | 154 | lightShader = gl.shader 155 | common: '''//essl 156 | varying vec3 vWorldNormal; varying vec4 vWorldPosition; 157 | uniform mat4 lightProj, lightView; uniform mat3 lightRot; 158 | uniform mat4 model; 159 | ''' 160 | vertex: '''//essl 161 | attribute vec3 position, normal; 162 | 163 | void main(){ 164 | vWorldNormal = normal; 165 | vWorldPosition = model * vec4(position, 1.0); 166 | gl_Position = lightProj * lightView * vWorldPosition; 167 | } 168 | ''' 169 | fragment: '''//essl 170 | void main(){ 171 | vec3 worldNormal = normalize(vWorldNormal); 172 | vec3 lightPos = (lightView * vWorldPosition).xyz; 173 | float depth = clamp(length(lightPos)/40.0, 0.0, 1.0); 174 | gl_FragColor = vec4(vec3(depth), 1.0); 175 | } 176 | ''' 177 | lightDepthTexture = gl.texture(type:'float', channels:'rgba').bind().setSize(64, 64).nearest().clampToEdge() 178 | lightFramebuffer = gl.framebuffer().bind().color(lightDepthTexture).depth().unbind() 179 | 180 | ## matrix setup ## 181 | camProj = gl.mat4() 182 | camView = gl.mat4() 183 | lightProj = gl.mat4().perspective(fov:60, 1, near:0.01, far:100) 184 | lightView = gl.mat4().trans(0, 0, -6).rotatex(30).rotatey(110) 185 | lightRot = gl.mat3().fromMat4Rot(lightView) 186 | model = gl.mat4() 187 | 188 | ## state variables ## 189 | counter = -Math.PI*0.5 190 | offset = 0 191 | camDist = 10 192 | camRot = 55 193 | camPitch = 41 194 | 195 | ## mouse handling ## 196 | mouseup = -> $(document).unbind('mousemove', mousemove).unbind('mouseup', mouseup) 197 | mousemove = ({originalEvent}) -> 198 | x = originalEvent.movementX ? originalEvent.webkitMovementX ? originalEvent.mozMovementX ? originalEvent.oMovementX 199 | y = originalEvent.movementY ? originalEvent.webkitMovementY ? originalEvent.mozMovementY ? originalEvent.oMovementY 200 | camRot += x 201 | camPitch += y 202 | if camPitch > 85 then camPitch = 85 203 | else if camPitch < 1 then camPitch = 1 204 | 205 | $(canvas) 206 | .bind 'mousedown', -> 207 | $(document).bind('mousemove', mousemove).bind('mouseup', mouseup) 208 | return false 209 | 210 | .bind 'mousewheel', ({originalEvent}) -> 211 | camDist -= originalEvent.wheelDeltaY/250 212 | return false 213 | .bind 'DOMMouseScroll', ({originalEvent}) -> 214 | camDist += originalEvent.detail/5 215 | return false 216 | 217 | ## drawing methods ## 218 | drawScene = (shader) -> 219 | shader 220 | .mat4('model', model.ident().trans(0, 0, 0)) 221 | .draw(planeGeom) 222 | .mat4('model', model.ident().trans(0, 1+offset, 0)) 223 | .draw(cubeGeom) 224 | .mat4('model', model.ident().trans(5, 1, -1)) 225 | .draw(cubeGeom) 226 | 227 | drawLight = -> 228 | lightFramebuffer.bind() 229 | gl 230 | .viewport(0, 0, lightDepthTexture.width, lightDepthTexture.height) 231 | .clearColor(1,1,1,1) 232 | .clearDepth(1) 233 | .cullFace('front') 234 | 235 | lightShader.use() 236 | .mat4('lightView', lightView) 237 | .mat4('lightProj', lightProj) 238 | .mat3('lightRot', lightRot) 239 | drawScene lightShader 240 | lightFramebuffer.unbind() 241 | 242 | drawCamera = -> 243 | gl 244 | .adjustSize() 245 | .viewport() 246 | .cullFace('back') 247 | .clearColor(0,0,0,0) 248 | .clearDepth(1) 249 | 250 | camProj.perspective(fov:60, aspect:gl.aspect, near:0.01, far:100) 251 | camView.ident().trans(0, -1, -camDist).rotatex(camPitch).rotatey(camRot) 252 | 253 | displayShader.use() 254 | .mat4('camProj', camProj) 255 | .mat4('camView', camView) 256 | .mat4('lightView', lightView) 257 | .mat4('lightProj', lightProj) 258 | .mat3('lightRot', lightRot) 259 | .sampler('sLightDepth', lightDepthTexture) 260 | .vec2('lightDepthSize', lightDepthTexture.width, lightDepthTexture.height) 261 | drawScene displayShader 262 | 263 | draw = -> 264 | drawLight() 265 | drawCamera() 266 | 267 | ## mainloop ## 268 | draw() 269 | gl.animationInterval -> 270 | if hover 271 | if animate 272 | offset = 1 + Math.sin(counter) 273 | counter += 1/30 274 | else 275 | offset = 0 276 | draw() 277 | -------------------------------------------------------------------------------- /pcf-lerp-shadow.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var animate, camDist, camPitch, camProj, camRot, camView, canvas, container, controls, counter, cubeGeom, displayShader, draw, drawCamera, drawLight, drawScene, floatExt, fullscreenImg, gl, hover, lightDepthTexture, lightFramebuffer, lightProj, lightRot, lightShader, lightView, model, mousemove, mouseup, name, offset, planeGeom; 3 | 4 | name = 'pcf-lerp-shadow'; 5 | 6 | document.write("
"); 7 | 8 | container = $('#' + name); 9 | 10 | canvas = $('').appendTo(container)[0]; 11 | 12 | try { 13 | gl = new WebGLFramework(canvas).depthTest(); 14 | floatExt = gl.getFloatExtension({ 15 | require: ['renderable'] 16 | }); 17 | } catch (error) { 18 | container.empty(); 19 | $('
').text(error).appendTo(container); 20 | $('
').text('(screenshot instead)').appendTo(container); 21 | $("").appendTo(container); 22 | return; 23 | } 24 | 25 | fullscreenImg = $('').appendTo(container).click(function() { 26 | return gl.toggleFullscreen(container[0]); 27 | }); 28 | 29 | gl.onFullscreenChange(function(isFullscreen) { 30 | if (isFullscreen) { 31 | container.addClass('fullscreen'); 32 | return fullscreenImg.attr('src', 'exit-fullscreen.png'); 33 | } else { 34 | container.removeClass('fullscreen'); 35 | return fullscreenImg.attr('src', 'fullscreen.png'); 36 | } 37 | }); 38 | 39 | hover = false; 40 | 41 | container.hover((function() { 42 | return hover = true; 43 | }), (function() { 44 | return hover = false; 45 | })); 46 | 47 | animate = true; 48 | 49 | controls = $('
').appendTo(container); 50 | 51 | $('').appendTo(controls); 52 | 53 | $('').appendTo(controls).change(function() { 54 | return animate = this.checked; 55 | }); 56 | 57 | cubeGeom = gl.drawable(meshes.cube); 58 | 59 | planeGeom = gl.drawable(meshes.plane(50)); 60 | 61 | displayShader = gl.shader({ 62 | common: '#line 53 pcf-lerp-shadow.coffee\nvarying vec3 vWorldNormal; varying vec4 vWorldPosition;\nuniform mat4 camProj, camView;\nuniform mat4 lightProj, lightView; uniform mat3 lightRot;\nuniform mat4 model;', 63 | vertex: '#line 59 pcf-lerp-shadow.coffee\nattribute vec3 position, normal;\n\nvoid main(){\n vWorldNormal = normal;\n vWorldPosition = model * vec4(position, 1.0);\n gl_Position = camProj * camView * vWorldPosition;\n}', 64 | fragment: '#line 68 pcf-lerp-shadow.coffee\nuniform sampler2D sLightDepth;\nuniform vec2 lightDepthSize;\n\nfloat texture2DCompare(sampler2D depths, vec2 uv, float compare){\n float depth = texture2D(depths, uv).r;\n return step(compare, depth);\n}\n\nfloat texture2DShadowLerp(sampler2D depths, vec2 size, vec2 uv, float compare){\n vec2 texelSize = vec2(1.0)/size;\n vec2 f = fract(uv*size+0.5);\n vec2 centroidUV = floor(uv*size+0.5)/size;\n\n float lb = texture2DCompare(depths, centroidUV+texelSize*vec2(0.0, 0.0), compare);\n float lt = texture2DCompare(depths, centroidUV+texelSize*vec2(0.0, 1.0), compare);\n float rb = texture2DCompare(depths, centroidUV+texelSize*vec2(1.0, 0.0), compare);\n float rt = texture2DCompare(depths, centroidUV+texelSize*vec2(1.0, 1.0), compare);\n float a = mix(lb, lt, f.y);\n float b = mix(rb, rt, f.y);\n float c = mix(a, b, f.x);\n return c;\n}\n\nfloat PCF(sampler2D depths, vec2 size, vec2 uv, float compare){\n float result = 0.0;\n for(int x=-1; x<=1; x++){\n for(int y=-1; y<=1; y++){\n vec2 off = vec2(x,y)/size;\n result += texture2DShadowLerp(depths, size, uv+off, compare);\n }\n }\n return result/9.0;\n}\n\nfloat attenuation(vec3 dir){\n float dist = length(dir);\n float radiance = 1.0/(1.0+pow(dist/10.0, 2.0));\n return clamp(radiance*10.0, 0.0, 1.0);\n}\n\nfloat influence(vec3 normal, float coneAngle){\n float minConeAngle = ((360.0-coneAngle-10.0)/360.0)*PI;\n float maxConeAngle = ((360.0-coneAngle)/360.0)*PI;\n return smoothstep(minConeAngle, maxConeAngle, acos(normal.z));\n}\n\nfloat lambert(vec3 surfaceNormal, vec3 lightDirNormal){\n return max(0.0, dot(surfaceNormal, lightDirNormal));\n}\n\nvec3 skyLight(vec3 normal){\n return vec3(smoothstep(0.0, PI, PI-acos(normal.y)))*0.4;\n}\n\nvec3 gamma(vec3 color){\n return pow(color, vec3(2.2));\n}\n\nvoid main(){\n vec3 worldNormal = normalize(vWorldNormal);\n\n vec3 camPos = (camView * vWorldPosition).xyz;\n vec3 lightPos = (lightView * vWorldPosition).xyz;\n vec3 lightPosNormal = normalize(lightPos);\n vec3 lightSurfaceNormal = lightRot * worldNormal;\n vec4 lightDevice = lightProj * vec4(lightPos, 1.0);\n vec2 lightDeviceNormal = lightDevice.xy/lightDevice.w;\n vec2 lightUV = lightDeviceNormal*0.5+0.5;\n\n // shadow calculation\n float bias = 0.001;\n float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0)-bias;\n float illuminated = PCF(sLightDepth, lightDepthSize, lightUV, lightDepth2);\n \n vec3 excident = (\n skyLight(worldNormal) +\n lambert(lightSurfaceNormal, -lightPosNormal) *\n influence(lightPosNormal, 55.0) *\n attenuation(lightPos) *\n illuminated\n );\n gl_FragColor = vec4(gamma(excident), 1.0);\n}' 65 | }); 66 | 67 | lightShader = gl.shader({ 68 | common: '#line 155 pcf-lerp-shadow.coffee\nvarying vec3 vWorldNormal; varying vec4 vWorldPosition;\nuniform mat4 lightProj, lightView; uniform mat3 lightRot;\nuniform mat4 model;', 69 | vertex: '#line 160 pcf-lerp-shadow.coffee\nattribute vec3 position, normal;\n\nvoid main(){\n vWorldNormal = normal;\n vWorldPosition = model * vec4(position, 1.0);\n gl_Position = lightProj * lightView * vWorldPosition;\n}', 70 | fragment: '#line 169 pcf-lerp-shadow.coffee\nvoid main(){\n vec3 worldNormal = normalize(vWorldNormal);\n vec3 lightPos = (lightView * vWorldPosition).xyz;\n float depth = clamp(length(lightPos)/40.0, 0.0, 1.0);\n gl_FragColor = vec4(vec3(depth), 1.0);\n}' 71 | }); 72 | 73 | lightDepthTexture = gl.texture({ 74 | type: 'float', 75 | channels: 'rgba' 76 | }).bind().setSize(64, 64).nearest().clampToEdge(); 77 | 78 | lightFramebuffer = gl.framebuffer().bind().color(lightDepthTexture).depth().unbind(); 79 | 80 | camProj = gl.mat4(); 81 | 82 | camView = gl.mat4(); 83 | 84 | lightProj = gl.mat4().perspective({ 85 | fov: 60 86 | }, 1, { 87 | near: 0.01, 88 | far: 100 89 | }); 90 | 91 | lightView = gl.mat4().trans(0, 0, -6).rotatex(30).rotatey(110); 92 | 93 | lightRot = gl.mat3().fromMat4Rot(lightView); 94 | 95 | model = gl.mat4(); 96 | 97 | counter = -Math.PI * 0.5; 98 | 99 | offset = 0; 100 | 101 | camDist = 10; 102 | 103 | camRot = 55; 104 | 105 | camPitch = 41; 106 | 107 | mouseup = function() { 108 | return $(document).unbind('mousemove', mousemove).unbind('mouseup', mouseup); 109 | }; 110 | 111 | mousemove = function(_arg) { 112 | var originalEvent, x, y, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; 113 | originalEvent = _arg.originalEvent; 114 | x = (_ref = (_ref1 = (_ref2 = originalEvent.movementX) != null ? _ref2 : originalEvent.webkitMovementX) != null ? _ref1 : originalEvent.mozMovementX) != null ? _ref : originalEvent.oMovementX; 115 | y = (_ref3 = (_ref4 = (_ref5 = originalEvent.movementY) != null ? _ref5 : originalEvent.webkitMovementY) != null ? _ref4 : originalEvent.mozMovementY) != null ? _ref3 : originalEvent.oMovementY; 116 | camRot += x; 117 | camPitch += y; 118 | if (camPitch > 85) { 119 | return camPitch = 85; 120 | } else if (camPitch < 1) { 121 | return camPitch = 1; 122 | } 123 | }; 124 | 125 | $(canvas).bind('mousedown', function() { 126 | $(document).bind('mousemove', mousemove).bind('mouseup', mouseup); 127 | return false; 128 | }).bind('mousewheel', function(_arg) { 129 | var originalEvent; 130 | originalEvent = _arg.originalEvent; 131 | camDist -= originalEvent.wheelDeltaY / 250; 132 | return false; 133 | }).bind('DOMMouseScroll', function(_arg) { 134 | var originalEvent; 135 | originalEvent = _arg.originalEvent; 136 | camDist += originalEvent.detail / 5; 137 | return false; 138 | }); 139 | 140 | drawScene = function(shader) { 141 | return shader.mat4('model', model.ident().trans(0, 0, 0)).draw(planeGeom).mat4('model', model.ident().trans(0, 1 + offset, 0)).draw(cubeGeom).mat4('model', model.ident().trans(5, 1, -1)).draw(cubeGeom); 142 | }; 143 | 144 | drawLight = function() { 145 | lightFramebuffer.bind(); 146 | gl.viewport(0, 0, lightDepthTexture.width, lightDepthTexture.height).clearColor(1, 1, 1, 1).clearDepth(1).cullFace('front'); 147 | lightShader.use().mat4('lightView', lightView).mat4('lightProj', lightProj).mat3('lightRot', lightRot); 148 | drawScene(lightShader); 149 | return lightFramebuffer.unbind(); 150 | }; 151 | 152 | drawCamera = function() { 153 | gl.adjustSize().viewport().cullFace('back').clearColor(0, 0, 0, 0).clearDepth(1); 154 | camProj.perspective({ 155 | fov: 60, 156 | aspect: gl.aspect, 157 | near: 0.01, 158 | far: 100 159 | }); 160 | camView.ident().trans(0, -1, -camDist).rotatex(camPitch).rotatey(camRot); 161 | displayShader.use().mat4('camProj', camProj).mat4('camView', camView).mat4('lightView', lightView).mat4('lightProj', lightProj).mat3('lightRot', lightRot).sampler('sLightDepth', lightDepthTexture).vec2('lightDepthSize', lightDepthTexture.width, lightDepthTexture.height); 162 | return drawScene(displayShader); 163 | }; 164 | 165 | draw = function() { 166 | drawLight(); 167 | return drawCamera(); 168 | }; 169 | 170 | draw(); 171 | 172 | gl.animationInterval(function() { 173 | if (hover) { 174 | if (animate) { 175 | offset = 1 + Math.sin(counter); 176 | counter += 1 / 30; 177 | } else { 178 | offset = 0; 179 | } 180 | return draw(); 181 | } 182 | }); 183 | 184 | }).call(this); 185 | -------------------------------------------------------------------------------- /pcf-lerp-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyalot/soft-shadow-mapping/e425c99c93a11f2b46f01b37bf47e10069133144/pcf-lerp-shadow.png -------------------------------------------------------------------------------- /pcf-shadow.coffee: -------------------------------------------------------------------------------- 1 | ## add the canvas to the dom ## 2 | name = 'pcf-shadow' 3 | document.write "
" 4 | container = $('#' + name) 5 | canvas = $('').appendTo(container)[0] 6 | 7 | ## setup the framework ## 8 | try 9 | gl = new WebGLFramework(canvas) 10 | .depthTest() 11 | 12 | floatExt = gl.getFloatExtension( 13 | require: ['renderable'] 14 | prefer: ['filterable', 'half'] 15 | ) 16 | 17 | catch error 18 | container.empty() 19 | $('
').text(error).appendTo(container) 20 | $('
').text('(screenshot instead)').appendTo(container) 21 | $("").appendTo(container) 22 | return 23 | 24 | ## fullscreen handling ## 25 | fullscreenImg = $('') 26 | .appendTo(container) 27 | .click -> gl.toggleFullscreen(container[0]) 28 | 29 | gl.onFullscreenChange (isFullscreen) -> 30 | if isFullscreen 31 | container.addClass('fullscreen') 32 | fullscreenImg.attr('src', 'exit-fullscreen.png') 33 | else 34 | container.removeClass('fullscreen') 35 | fullscreenImg.attr('src', 'fullscreen.png') 36 | 37 | ## handle mouse over ## 38 | hover = false 39 | container.hover (-> hover = true), (-> hover = false) 40 | 41 | ## animation control ## 42 | animate = true 43 | controls = $('
') 44 | .appendTo(container) 45 | $('').appendTo(controls) 46 | $('') 47 | .appendTo(controls) 48 | .change -> 49 | animate = @checked 50 | 51 | ## create webgl objects ## 52 | cubeGeom = gl.drawable meshes.cube 53 | planeGeom = gl.drawable meshes.plane(50) 54 | displayShader = gl.shader 55 | common: '''//essl 56 | varying vec3 vWorldNormal; varying vec4 vWorldPosition; 57 | uniform mat4 camProj, camView; 58 | uniform mat4 lightProj, lightView; uniform mat3 lightRot; 59 | uniform mat4 model; 60 | ''' 61 | vertex: '''//essl 62 | attribute vec3 position, normal; 63 | 64 | void main(){ 65 | vWorldNormal = normal; 66 | vWorldPosition = model * vec4(position, 1.0); 67 | gl_Position = camProj * camView * vWorldPosition; 68 | } 69 | ''' 70 | fragment: '''//essl 71 | uniform sampler2D sLightDepth; 72 | uniform vec2 lightDepthSize; 73 | 74 | float texture2DCompare(sampler2D depths, vec2 uv, float compare){ 75 | float depth = texture2D(depths, uv).r; 76 | return step(compare, depth); 77 | } 78 | 79 | float PCF(sampler2D depths, vec2 size, vec2 uv, float compare){ 80 | float result = 0.0; 81 | for(int x=-2; x<=2; x++){ 82 | for(int y=-2; y<=2; y++){ 83 | vec2 off = vec2(x,y)/size; 84 | result += texture2DCompare(depths, uv+off, compare); 85 | } 86 | } 87 | return result/25.0; 88 | } 89 | 90 | float attenuation(vec3 dir){ 91 | float dist = length(dir); 92 | float radiance = 1.0/(1.0+pow(dist/10.0, 2.0)); 93 | return clamp(radiance*10.0, 0.0, 1.0); 94 | } 95 | 96 | float influence(vec3 normal, float coneAngle){ 97 | float minConeAngle = ((360.0-coneAngle-10.0)/360.0)*PI; 98 | float maxConeAngle = ((360.0-coneAngle)/360.0)*PI; 99 | return smoothstep(minConeAngle, maxConeAngle, acos(normal.z)); 100 | } 101 | 102 | float lambert(vec3 surfaceNormal, vec3 lightDirNormal){ 103 | return max(0.0, dot(surfaceNormal, lightDirNormal)); 104 | } 105 | 106 | vec3 skyLight(vec3 normal){ 107 | return vec3(smoothstep(0.0, PI, PI-acos(normal.y)))*0.4; 108 | } 109 | 110 | vec3 gamma(vec3 color){ 111 | return pow(color, vec3(2.2)); 112 | } 113 | 114 | void main(){ 115 | vec3 worldNormal = normalize(vWorldNormal); 116 | 117 | vec3 camPos = (camView * vWorldPosition).xyz; 118 | vec3 lightPos = (lightView * vWorldPosition).xyz; 119 | vec3 lightPosNormal = normalize(lightPos); 120 | vec3 lightSurfaceNormal = lightRot * worldNormal; 121 | vec4 lightDevice = lightProj * vec4(lightPos, 1.0); 122 | vec2 lightDeviceNormal = lightDevice.xy/lightDevice.w; 123 | vec2 lightUV = lightDeviceNormal*0.5+0.5; 124 | 125 | // shadow calculation 126 | float bias = 0.001; 127 | float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0)-bias; 128 | float illuminated = PCF(sLightDepth, lightDepthSize, lightUV, lightDepth2); 129 | 130 | vec3 excident = ( 131 | skyLight(worldNormal) + 132 | lambert(lightSurfaceNormal, -lightPosNormal) * 133 | influence(lightPosNormal, 55.0) * 134 | attenuation(lightPos) * 135 | illuminated 136 | ); 137 | gl_FragColor = vec4(gamma(excident), 1.0); 138 | } 139 | ''' 140 | 141 | lightShader = gl.shader 142 | common: '''//essl 143 | varying vec3 vWorldNormal; varying vec4 vWorldPosition; 144 | uniform mat4 lightProj, lightView; uniform mat3 lightRot; 145 | uniform mat4 model; 146 | ''' 147 | vertex: '''//essl 148 | attribute vec3 position, normal; 149 | 150 | void main(){ 151 | vWorldNormal = normal; 152 | vWorldPosition = model * vec4(position, 1.0); 153 | gl_Position = lightProj * lightView * vWorldPosition; 154 | } 155 | ''' 156 | fragment: '''//essl 157 | void main(){ 158 | vec3 worldNormal = normalize(vWorldNormal); 159 | vec3 lightPos = (lightView * vWorldPosition).xyz; 160 | float depth = clamp(length(lightPos)/40.0, 0.0, 1.0); 161 | gl_FragColor = vec4(vec3(depth), 1.0); 162 | } 163 | ''' 164 | 165 | lightDepthTexture = gl.texture(type:floatExt.type, channels:'rgba').bind().setSize(64, 64).clampToEdge() 166 | if floatExt.filterable 167 | lightDepthTexture.linear() 168 | else 169 | lightDepthTexture.nearest() 170 | 171 | lightFramebuffer = gl.framebuffer().bind().color(lightDepthTexture).depth().unbind() 172 | 173 | ## matrix setup ## 174 | camProj = gl.mat4() 175 | camView = gl.mat4() 176 | lightProj = gl.mat4().perspective(fov:60, 1, near:0.01, far:100) 177 | lightView = gl.mat4().trans(0, 0, -6).rotatex(30).rotatey(110) 178 | lightRot = gl.mat3().fromMat4Rot(lightView) 179 | model = gl.mat4() 180 | 181 | ## state variables ## 182 | counter = -Math.PI*0.5 183 | offset = 0 184 | camDist = 10 185 | camRot = 55 186 | camPitch = 41 187 | 188 | ## mouse handling ## 189 | mouseup = -> $(document).unbind('mousemove', mousemove).unbind('mouseup', mouseup) 190 | mousemove = ({originalEvent}) -> 191 | x = originalEvent.movementX ? originalEvent.webkitMovementX ? originalEvent.mozMovementX ? originalEvent.oMovementX 192 | y = originalEvent.movementY ? originalEvent.webkitMovementY ? originalEvent.mozMovementY ? originalEvent.oMovementY 193 | camRot += x 194 | camPitch += y 195 | if camPitch > 85 then camPitch = 85 196 | else if camPitch < 1 then camPitch = 1 197 | 198 | $(canvas) 199 | .bind 'mousedown', -> 200 | $(document).bind('mousemove', mousemove).bind('mouseup', mouseup) 201 | return false 202 | 203 | .bind 'mousewheel', ({originalEvent}) -> 204 | camDist -= originalEvent.wheelDeltaY/250 205 | return false 206 | .bind 'DOMMouseScroll', ({originalEvent}) -> 207 | camDist += originalEvent.detail/5 208 | return false 209 | 210 | ## drawing methods ## 211 | drawScene = (shader) -> 212 | shader 213 | .mat4('model', model.ident().trans(0, 0, 0)) 214 | .draw(planeGeom) 215 | .mat4('model', model.ident().trans(0, 1+offset, 0)) 216 | .draw(cubeGeom) 217 | .mat4('model', model.ident().trans(5, 1, -1)) 218 | .draw(cubeGeom) 219 | 220 | drawLight = -> 221 | lightFramebuffer.bind() 222 | gl 223 | .viewport(0, 0, lightDepthTexture.width, lightDepthTexture.height) 224 | .clearColor(1,1,1,1) 225 | .clearDepth(1) 226 | .cullFace('front') 227 | 228 | lightShader.use() 229 | .mat4('lightView', lightView) 230 | .mat4('lightProj', lightProj) 231 | .mat3('lightRot', lightRot) 232 | drawScene lightShader 233 | lightFramebuffer.unbind() 234 | 235 | drawCamera = -> 236 | gl 237 | .adjustSize() 238 | .viewport() 239 | .cullFace('back') 240 | .clearColor(0,0,0,0) 241 | .clearDepth(1) 242 | 243 | camProj.perspective(fov:60, aspect:gl.aspect, near:0.01, far:100) 244 | camView.ident().trans(0, -1, -camDist).rotatex(camPitch).rotatey(camRot) 245 | 246 | displayShader.use() 247 | .mat4('camProj', camProj) 248 | .mat4('camView', camView) 249 | .mat4('lightView', lightView) 250 | .mat4('lightProj', lightProj) 251 | .mat3('lightRot', lightRot) 252 | .sampler('sLightDepth', lightDepthTexture) 253 | .vec2('lightDepthSize', lightDepthTexture.width, lightDepthTexture.height) 254 | drawScene displayShader 255 | 256 | draw = -> 257 | drawLight() 258 | drawCamera() 259 | 260 | ## mainloop ## 261 | draw() 262 | gl.animationInterval -> 263 | if hover 264 | if animate 265 | offset = 1 + Math.sin(counter) 266 | counter += 1/30 267 | else 268 | offset = 0 269 | draw() 270 | -------------------------------------------------------------------------------- /pcf-shadow.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var animate, camDist, camPitch, camProj, camRot, camView, canvas, container, controls, counter, cubeGeom, displayShader, draw, drawCamera, drawLight, drawScene, floatExt, fullscreenImg, gl, hover, lightDepthTexture, lightFramebuffer, lightProj, lightRot, lightShader, lightView, model, mousemove, mouseup, name, offset, planeGeom; 3 | 4 | name = 'pcf-shadow'; 5 | 6 | document.write("
"); 7 | 8 | container = $('#' + name); 9 | 10 | canvas = $('').appendTo(container)[0]; 11 | 12 | try { 13 | gl = new WebGLFramework(canvas).depthTest(); 14 | floatExt = gl.getFloatExtension({ 15 | require: ['renderable'], 16 | prefer: ['filterable', 'half'] 17 | }); 18 | } catch (error) { 19 | container.empty(); 20 | $('
').text(error).appendTo(container); 21 | $('
').text('(screenshot instead)').appendTo(container); 22 | $("").appendTo(container); 23 | return; 24 | } 25 | 26 | fullscreenImg = $('').appendTo(container).click(function() { 27 | return gl.toggleFullscreen(container[0]); 28 | }); 29 | 30 | gl.onFullscreenChange(function(isFullscreen) { 31 | if (isFullscreen) { 32 | container.addClass('fullscreen'); 33 | return fullscreenImg.attr('src', 'exit-fullscreen.png'); 34 | } else { 35 | container.removeClass('fullscreen'); 36 | return fullscreenImg.attr('src', 'fullscreen.png'); 37 | } 38 | }); 39 | 40 | hover = false; 41 | 42 | container.hover((function() { 43 | return hover = true; 44 | }), (function() { 45 | return hover = false; 46 | })); 47 | 48 | animate = true; 49 | 50 | controls = $('
').appendTo(container); 51 | 52 | $('').appendTo(controls); 53 | 54 | $('').appendTo(controls).change(function() { 55 | return animate = this.checked; 56 | }); 57 | 58 | cubeGeom = gl.drawable(meshes.cube); 59 | 60 | planeGeom = gl.drawable(meshes.plane(50)); 61 | 62 | displayShader = gl.shader({ 63 | common: '#line 55 pcf-shadow.coffee\nvarying vec3 vWorldNormal; varying vec4 vWorldPosition;\nuniform mat4 camProj, camView;\nuniform mat4 lightProj, lightView; uniform mat3 lightRot;\nuniform mat4 model;', 64 | vertex: '#line 61 pcf-shadow.coffee\nattribute vec3 position, normal;\n\nvoid main(){\n vWorldNormal = normal;\n vWorldPosition = model * vec4(position, 1.0);\n gl_Position = camProj * camView * vWorldPosition;\n}', 65 | fragment: '#line 70 pcf-shadow.coffee\nuniform sampler2D sLightDepth;\nuniform vec2 lightDepthSize;\n\nfloat texture2DCompare(sampler2D depths, vec2 uv, float compare){\n float depth = texture2D(depths, uv).r;\n return step(compare, depth);\n}\n\nfloat PCF(sampler2D depths, vec2 size, vec2 uv, float compare){\n float result = 0.0;\n for(int x=-2; x<=2; x++){\n for(int y=-2; y<=2; y++){\n vec2 off = vec2(x,y)/size;\n result += texture2DCompare(depths, uv+off, compare);\n }\n }\n return result/25.0;\n}\n\nfloat attenuation(vec3 dir){\n float dist = length(dir);\n float radiance = 1.0/(1.0+pow(dist/10.0, 2.0));\n return clamp(radiance*10.0, 0.0, 1.0);\n}\n\nfloat influence(vec3 normal, float coneAngle){\n float minConeAngle = ((360.0-coneAngle-10.0)/360.0)*PI;\n float maxConeAngle = ((360.0-coneAngle)/360.0)*PI;\n return smoothstep(minConeAngle, maxConeAngle, acos(normal.z));\n}\n\nfloat lambert(vec3 surfaceNormal, vec3 lightDirNormal){\n return max(0.0, dot(surfaceNormal, lightDirNormal));\n}\n\nvec3 skyLight(vec3 normal){\n return vec3(smoothstep(0.0, PI, PI-acos(normal.y)))*0.4;\n}\n\nvec3 gamma(vec3 color){\n return pow(color, vec3(2.2));\n}\n\nvoid main(){\n vec3 worldNormal = normalize(vWorldNormal);\n\n vec3 camPos = (camView * vWorldPosition).xyz;\n vec3 lightPos = (lightView * vWorldPosition).xyz;\n vec3 lightPosNormal = normalize(lightPos);\n vec3 lightSurfaceNormal = lightRot * worldNormal;\n vec4 lightDevice = lightProj * vec4(lightPos, 1.0);\n vec2 lightDeviceNormal = lightDevice.xy/lightDevice.w;\n vec2 lightUV = lightDeviceNormal*0.5+0.5;\n\n // shadow calculation\n float bias = 0.001;\n float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0)-bias;\n float illuminated = PCF(sLightDepth, lightDepthSize, lightUV, lightDepth2);\n \n vec3 excident = (\n skyLight(worldNormal) +\n lambert(lightSurfaceNormal, -lightPosNormal) *\n influence(lightPosNormal, 55.0) *\n attenuation(lightPos) *\n illuminated\n );\n gl_FragColor = vec4(gamma(excident), 1.0);\n}' 66 | }); 67 | 68 | lightShader = gl.shader({ 69 | common: '#line 142 pcf-shadow.coffee\nvarying vec3 vWorldNormal; varying vec4 vWorldPosition;\nuniform mat4 lightProj, lightView; uniform mat3 lightRot;\nuniform mat4 model;', 70 | vertex: '#line 147 pcf-shadow.coffee\nattribute vec3 position, normal;\n\nvoid main(){\n vWorldNormal = normal;\n vWorldPosition = model * vec4(position, 1.0);\n gl_Position = lightProj * lightView * vWorldPosition;\n}', 71 | fragment: '#line 156 pcf-shadow.coffee\nvoid main(){\n vec3 worldNormal = normalize(vWorldNormal);\n vec3 lightPos = (lightView * vWorldPosition).xyz;\n float depth = clamp(length(lightPos)/40.0, 0.0, 1.0);\n gl_FragColor = vec4(vec3(depth), 1.0);\n}' 72 | }); 73 | 74 | lightDepthTexture = gl.texture({ 75 | type: floatExt.type, 76 | channels: 'rgba' 77 | }).bind().setSize(64, 64).clampToEdge(); 78 | 79 | if (floatExt.filterable) { 80 | lightDepthTexture.linear(); 81 | } else { 82 | lightDepthTexture.nearest(); 83 | } 84 | 85 | lightFramebuffer = gl.framebuffer().bind().color(lightDepthTexture).depth().unbind(); 86 | 87 | camProj = gl.mat4(); 88 | 89 | camView = gl.mat4(); 90 | 91 | lightProj = gl.mat4().perspective({ 92 | fov: 60 93 | }, 1, { 94 | near: 0.01, 95 | far: 100 96 | }); 97 | 98 | lightView = gl.mat4().trans(0, 0, -6).rotatex(30).rotatey(110); 99 | 100 | lightRot = gl.mat3().fromMat4Rot(lightView); 101 | 102 | model = gl.mat4(); 103 | 104 | counter = -Math.PI * 0.5; 105 | 106 | offset = 0; 107 | 108 | camDist = 10; 109 | 110 | camRot = 55; 111 | 112 | camPitch = 41; 113 | 114 | mouseup = function() { 115 | return $(document).unbind('mousemove', mousemove).unbind('mouseup', mouseup); 116 | }; 117 | 118 | mousemove = function(_arg) { 119 | var originalEvent, x, y, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; 120 | originalEvent = _arg.originalEvent; 121 | x = (_ref = (_ref1 = (_ref2 = originalEvent.movementX) != null ? _ref2 : originalEvent.webkitMovementX) != null ? _ref1 : originalEvent.mozMovementX) != null ? _ref : originalEvent.oMovementX; 122 | y = (_ref3 = (_ref4 = (_ref5 = originalEvent.movementY) != null ? _ref5 : originalEvent.webkitMovementY) != null ? _ref4 : originalEvent.mozMovementY) != null ? _ref3 : originalEvent.oMovementY; 123 | camRot += x; 124 | camPitch += y; 125 | if (camPitch > 85) { 126 | return camPitch = 85; 127 | } else if (camPitch < 1) { 128 | return camPitch = 1; 129 | } 130 | }; 131 | 132 | $(canvas).bind('mousedown', function() { 133 | $(document).bind('mousemove', mousemove).bind('mouseup', mouseup); 134 | return false; 135 | }).bind('mousewheel', function(_arg) { 136 | var originalEvent; 137 | originalEvent = _arg.originalEvent; 138 | camDist -= originalEvent.wheelDeltaY / 250; 139 | return false; 140 | }).bind('DOMMouseScroll', function(_arg) { 141 | var originalEvent; 142 | originalEvent = _arg.originalEvent; 143 | camDist += originalEvent.detail / 5; 144 | return false; 145 | }); 146 | 147 | drawScene = function(shader) { 148 | return shader.mat4('model', model.ident().trans(0, 0, 0)).draw(planeGeom).mat4('model', model.ident().trans(0, 1 + offset, 0)).draw(cubeGeom).mat4('model', model.ident().trans(5, 1, -1)).draw(cubeGeom); 149 | }; 150 | 151 | drawLight = function() { 152 | lightFramebuffer.bind(); 153 | gl.viewport(0, 0, lightDepthTexture.width, lightDepthTexture.height).clearColor(1, 1, 1, 1).clearDepth(1).cullFace('front'); 154 | lightShader.use().mat4('lightView', lightView).mat4('lightProj', lightProj).mat3('lightRot', lightRot); 155 | drawScene(lightShader); 156 | return lightFramebuffer.unbind(); 157 | }; 158 | 159 | drawCamera = function() { 160 | gl.adjustSize().viewport().cullFace('back').clearColor(0, 0, 0, 0).clearDepth(1); 161 | camProj.perspective({ 162 | fov: 60, 163 | aspect: gl.aspect, 164 | near: 0.01, 165 | far: 100 166 | }); 167 | camView.ident().trans(0, -1, -camDist).rotatex(camPitch).rotatey(camRot); 168 | displayShader.use().mat4('camProj', camProj).mat4('camView', camView).mat4('lightView', lightView).mat4('lightProj', lightProj).mat3('lightRot', lightRot).sampler('sLightDepth', lightDepthTexture).vec2('lightDepthSize', lightDepthTexture.width, lightDepthTexture.height); 169 | return drawScene(displayShader); 170 | }; 171 | 172 | draw = function() { 173 | drawLight(); 174 | return drawCamera(); 175 | }; 176 | 177 | draw(); 178 | 179 | gl.animationInterval(function() { 180 | if (hover) { 181 | if (animate) { 182 | offset = 1 + Math.sin(counter); 183 | counter += 1 / 30; 184 | } else { 185 | offset = 0; 186 | } 187 | return draw(); 188 | } 189 | }); 190 | 191 | }).call(this); 192 | -------------------------------------------------------------------------------- /pcf-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyalot/soft-shadow-mapping/e425c99c93a11f2b46f01b37bf47e10069133144/pcf-shadow.png -------------------------------------------------------------------------------- /vsm-filtered-shadow.coffee: -------------------------------------------------------------------------------- 1 | ## add the canvas to the dom ## 2 | name = 'vsm-filtered-shadow' 3 | document.write "
" 4 | container = $('#' + name) 5 | canvas = $('').appendTo(container)[0] 6 | 7 | ## setup the framework ## 8 | try 9 | gl = new WebGLFramework(canvas) 10 | .depthTest() 11 | 12 | floatExt = gl.getFloatExtension 13 | require: ['renderable', 'filterable'] 14 | 15 | gl.getExt('OES_standard_derivatives') 16 | catch error 17 | container.empty() 18 | $('
').text(error).appendTo(container) 19 | $('
').text('(screenshot instead)').appendTo(container) 20 | $("").appendTo(container) 21 | return 22 | 23 | ## fullscreen handling ## 24 | fullscreenImg = $('') 25 | .appendTo(container) 26 | .click -> gl.toggleFullscreen(container[0]) 27 | 28 | gl.onFullscreenChange (isFullscreen) -> 29 | if isFullscreen 30 | container.addClass('fullscreen') 31 | fullscreenImg.attr('src', 'exit-fullscreen.png') 32 | else 33 | container.removeClass('fullscreen') 34 | fullscreenImg.attr('src', 'fullscreen.png') 35 | 36 | ## handle mouse over ## 37 | hover = false 38 | container.hover (-> hover = true), (-> hover = false) 39 | 40 | ## animation control ## 41 | animate = true 42 | controls = $('
') 43 | .appendTo(container) 44 | $('').appendTo(controls) 45 | $('') 46 | .appendTo(controls) 47 | .change -> 48 | animate = @checked 49 | 50 | ## create webgl objects ## 51 | cubeGeom = gl.drawable meshes.cube 52 | planeGeom = gl.drawable meshes.plane(50) 53 | quad = gl.drawable meshes.quad 54 | 55 | displayShader = gl.shader 56 | common: '''//essl 57 | varying vec3 vWorldNormal; varying vec4 vWorldPosition; 58 | uniform mat4 camProj, camView; 59 | uniform mat4 lightProj, lightView; uniform mat3 lightRot; 60 | uniform mat4 model; 61 | ''' 62 | vertex: '''//essl 63 | attribute vec3 position, normal; 64 | 65 | void main(){ 66 | vWorldNormal = normal; 67 | vWorldPosition = model * vec4(position, 1.0); 68 | gl_Position = camProj * camView * vWorldPosition; 69 | } 70 | ''' 71 | fragment: '''//essl 72 | uniform sampler2D sLightDepth; 73 | 74 | float linstep(float low, float high, float v){ 75 | return clamp((v-low)/(high-low), 0.0, 1.0); 76 | } 77 | 78 | float VSM(sampler2D depths, vec2 uv, float compare){ 79 | vec2 moments = texture2D(depths, uv).xy; 80 | float p = smoothstep(compare-0.02, compare, moments.x); 81 | float variance = max(moments.y - moments.x*moments.x, -0.001); 82 | float d = compare - moments.x; 83 | float p_max = linstep(0.2, 1.0, variance / (variance + d*d)); 84 | return clamp(max(p, p_max), 0.0, 1.0); 85 | } 86 | 87 | float attenuation(vec3 dir){ 88 | float dist = length(dir); 89 | float radiance = 1.0/(1.0+pow(dist/10.0, 2.0)); 90 | return clamp(radiance*10.0, 0.0, 1.0); 91 | } 92 | 93 | float influence(vec3 normal, float coneAngle){ 94 | float minConeAngle = ((360.0-coneAngle-10.0)/360.0)*PI; 95 | float maxConeAngle = ((360.0-coneAngle)/360.0)*PI; 96 | return smoothstep(minConeAngle, maxConeAngle, acos(normal.z)); 97 | } 98 | 99 | float lambert(vec3 surfaceNormal, vec3 lightDirNormal){ 100 | return max(0.0, dot(surfaceNormal, lightDirNormal)); 101 | } 102 | 103 | vec3 skyLight(vec3 normal){ 104 | return vec3(smoothstep(0.0, PI, PI-acos(normal.y)))*0.4; 105 | } 106 | 107 | vec3 gamma(vec3 color){ 108 | return pow(color, vec3(2.2)); 109 | } 110 | 111 | void main(){ 112 | vec3 worldNormal = normalize(vWorldNormal); 113 | 114 | vec3 camPos = (camView * vWorldPosition).xyz; 115 | vec3 lightPos = (lightView * vWorldPosition).xyz; 116 | vec3 lightPosNormal = normalize(lightPos); 117 | vec3 lightSurfaceNormal = lightRot * worldNormal; 118 | vec4 lightDevice = lightProj * vec4(lightPos, 1.0); 119 | vec2 lightDeviceNormal = lightDevice.xy/lightDevice.w; 120 | vec2 lightUV = lightDeviceNormal*0.5+0.5; 121 | 122 | // shadow calculation 123 | float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0); 124 | float illuminated = VSM(sLightDepth, lightUV, lightDepth2); 125 | 126 | vec3 excident = ( 127 | skyLight(worldNormal) + 128 | lambert(lightSurfaceNormal, -lightPosNormal) * 129 | influence(lightPosNormal, 55.0) * 130 | attenuation(lightPos) * 131 | illuminated 132 | ); 133 | gl_FragColor = vec4(gamma(excident), 1.0); 134 | } 135 | ''' 136 | 137 | lightShader = gl.shader 138 | common: '''//essl 139 | varying vec3 vWorldNormal; varying vec4 vWorldPosition; 140 | uniform mat4 lightProj, lightView; uniform mat3 lightRot; 141 | uniform mat4 model; 142 | ''' 143 | vertex: '''//essl 144 | attribute vec3 position, normal; 145 | 146 | void main(){ 147 | vWorldNormal = normal; 148 | vWorldPosition = model * vec4(position, 1.0); 149 | gl_Position = lightProj * lightView * vWorldPosition; 150 | } 151 | ''' 152 | fragment: '''//essl 153 | #extension GL_OES_standard_derivatives : enable 154 | void main(){ 155 | vec3 worldNormal = normalize(vWorldNormal); 156 | vec3 lightPos = (lightView * vWorldPosition).xyz; 157 | float depth = clamp(length(lightPos)/40.0, 0.0, 1.0); 158 | float dx = dFdx(depth); 159 | float dy = dFdy(depth); 160 | gl_FragColor = vec4(depth, pow(depth, 2.0) + 0.25*(dx*dx + dy*dy), 0.0, 1.0); 161 | } 162 | ''' 163 | 164 | lightDepthTexture = gl.texture(type:floatExt.type, channels:'rgba').bind().setSize(256, 256).linear().clampToEdge() 165 | lightFramebuffer = gl.framebuffer().bind().color(lightDepthTexture).depth().unbind() 166 | 167 | ## filtering helper ## 168 | class Filter 169 | constructor: (@size, filter) -> 170 | @output = gl.texture(type:floatExt.type, channels:'rgba') 171 | .bind().setSize(@size, @size).linear().clampToEdge() 172 | @framebuffer = gl.framebuffer().bind().color(@output).unbind() 173 | @shader = gl.shader 174 | common: '''//essl 175 | varying vec2 texcoord; 176 | ''' 177 | vertex: '''//essl 178 | attribute vec2 position; 179 | 180 | void main(){ 181 | texcoord = position*0.5+0.5; 182 | gl_Position = vec4(position, 0.0, 1.0); 183 | } 184 | ''' 185 | fragment: 186 | """//essl 187 | uniform vec2 viewport; 188 | uniform sampler2D source; 189 | 190 | vec3 get(float x, float y){ 191 | vec2 off = vec2(x, y); 192 | return texture2D(source, texcoord+off/viewport).rgb; 193 | } 194 | vec3 get(int x, int y){ 195 | vec2 off = vec2(x, y); 196 | return texture2D(source, texcoord+off/viewport).rgb; 197 | } 198 | vec3 filter(){ 199 | #{filter} 200 | } 201 | void main(){ 202 | gl_FragColor = vec4(filter(), 1.0); 203 | } 204 | """ 205 | bind: (unit) -> @output.bind unit 206 | apply: (source) -> 207 | @framebuffer.bind() 208 | gl.viewport 0, 0, @size, @size 209 | @shader 210 | .use() 211 | .vec2('viewport', @size, @size) 212 | .sampler('source', source) 213 | .draw(quad) 214 | @framebuffer.unbind() 215 | 216 | downsample128 = new Filter 128, '''//essl 217 | return get(0.0, 0.0); 218 | ''' 219 | 220 | downsample64 = new Filter 64, '''//essl 221 | return get(0.0, 0.0); 222 | ''' 223 | 224 | boxFilter = new Filter 64, '''//essl 225 | vec3 result = vec3(0.0); 226 | for(int x=-1; x<=1; x++){ 227 | for(int y=-1; y<=1; y++){ 228 | result += get(x,y); 229 | } 230 | } 231 | return result/9.0; 232 | ''' 233 | 234 | ## matrix setup ## 235 | camProj = gl.mat4() 236 | camView = gl.mat4() 237 | lightProj = gl.mat4().perspective(fov:60, 1, near:0.01, far:100) 238 | lightView = gl.mat4().trans(0, 0, -6).rotatex(30).rotatey(110) 239 | lightRot = gl.mat3().fromMat4Rot(lightView) 240 | model = gl.mat4() 241 | 242 | ## state variables ## 243 | counter = -Math.PI*0.5 244 | offset = 0 245 | camDist = 10 246 | camRot = 55 247 | camPitch = 41 248 | 249 | ## mouse handling ## 250 | mouseup = -> $(document).unbind('mousemove', mousemove).unbind('mouseup', mouseup) 251 | mousemove = ({originalEvent}) -> 252 | x = originalEvent.movementX ? originalEvent.webkitMovementX ? originalEvent.mozMovementX ? originalEvent.oMovementX 253 | y = originalEvent.movementY ? originalEvent.webkitMovementY ? originalEvent.mozMovementY ? originalEvent.oMovementY 254 | camRot += x 255 | camPitch += y 256 | if camPitch > 85 then camPitch = 85 257 | else if camPitch < 1 then camPitch = 1 258 | 259 | $(canvas) 260 | .bind 'mousedown', -> 261 | $(document).bind('mousemove', mousemove).bind('mouseup', mouseup) 262 | return false 263 | 264 | .bind 'mousewheel', ({originalEvent}) -> 265 | camDist -= originalEvent.wheelDeltaY/250 266 | return false 267 | .bind 'DOMMouseScroll', ({originalEvent}) -> 268 | camDist += originalEvent.detail/5 269 | return false 270 | 271 | ## drawing methods ## 272 | drawScene = (shader) -> 273 | shader 274 | .mat4('model', model.ident().trans(0, 0, 0)) 275 | .draw(planeGeom) 276 | .mat4('model', model.ident().trans(0, 1+offset, 0)) 277 | .draw(cubeGeom) 278 | .mat4('model', model.ident().trans(5, 1, -1)) 279 | .draw(cubeGeom) 280 | 281 | drawLight = -> 282 | lightFramebuffer.bind() 283 | gl 284 | .viewport(0, 0, lightDepthTexture.width, lightDepthTexture.height) 285 | .clearColor(1,1,1,1) 286 | .clearDepth(1) 287 | .cullFace('back') 288 | 289 | lightShader.use() 290 | .mat4('lightView', lightView) 291 | .mat4('lightProj', lightProj) 292 | .mat3('lightRot', lightRot) 293 | drawScene lightShader 294 | lightFramebuffer.unbind() 295 | 296 | downsample128.apply lightDepthTexture 297 | downsample64.apply downsample128 298 | boxFilter.apply downsample64 299 | 300 | drawCamera = -> 301 | gl 302 | .adjustSize() 303 | .viewport() 304 | .cullFace('back') 305 | .clearColor(0,0,0,0) 306 | .clearDepth(1) 307 | 308 | camProj.perspective(fov:60, aspect:gl.aspect, near:0.01, far:100) 309 | camView.ident().trans(0, -1, -camDist).rotatex(camPitch).rotatey(camRot) 310 | 311 | displayShader.use() 312 | .mat4('camProj', camProj) 313 | .mat4('camView', camView) 314 | .mat4('lightView', lightView) 315 | .mat4('lightProj', lightProj) 316 | .mat3('lightRot', lightRot) 317 | .sampler('sLightDepth', boxFilter) 318 | drawScene displayShader 319 | 320 | draw = -> 321 | drawLight() 322 | drawCamera() 323 | 324 | ## mainloop ## 325 | draw() 326 | gl.animationInterval -> 327 | if hover 328 | if animate 329 | offset = 1 + Math.sin(counter) 330 | counter += 1/30 331 | else 332 | offset = 0 333 | draw() 334 | -------------------------------------------------------------------------------- /vsm-filtered-shadow.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Filter, animate, boxFilter, camDist, camPitch, camProj, camRot, camView, canvas, container, controls, counter, cubeGeom, displayShader, downsample128, downsample64, draw, drawCamera, drawLight, drawScene, floatExt, fullscreenImg, gl, hover, lightDepthTexture, lightFramebuffer, lightProj, lightRot, lightShader, lightView, model, mousemove, mouseup, name, offset, planeGeom, quad; 3 | 4 | name = 'vsm-filtered-shadow'; 5 | 6 | document.write("
"); 7 | 8 | container = $('#' + name); 9 | 10 | canvas = $('').appendTo(container)[0]; 11 | 12 | try { 13 | gl = new WebGLFramework(canvas).depthTest(); 14 | floatExt = gl.getFloatExtension({ 15 | require: ['renderable', 'filterable'] 16 | }); 17 | gl.getExt('OES_standard_derivatives'); 18 | } catch (error) { 19 | container.empty(); 20 | $('
').text(error).appendTo(container); 21 | $('
').text('(screenshot instead)').appendTo(container); 22 | $("").appendTo(container); 23 | return; 24 | } 25 | 26 | fullscreenImg = $('').appendTo(container).click(function() { 27 | return gl.toggleFullscreen(container[0]); 28 | }); 29 | 30 | gl.onFullscreenChange(function(isFullscreen) { 31 | if (isFullscreen) { 32 | container.addClass('fullscreen'); 33 | return fullscreenImg.attr('src', 'exit-fullscreen.png'); 34 | } else { 35 | container.removeClass('fullscreen'); 36 | return fullscreenImg.attr('src', 'fullscreen.png'); 37 | } 38 | }); 39 | 40 | hover = false; 41 | 42 | container.hover((function() { 43 | return hover = true; 44 | }), (function() { 45 | return hover = false; 46 | })); 47 | 48 | animate = true; 49 | 50 | controls = $('
').appendTo(container); 51 | 52 | $('').appendTo(controls); 53 | 54 | $('').appendTo(controls).change(function() { 55 | return animate = this.checked; 56 | }); 57 | 58 | cubeGeom = gl.drawable(meshes.cube); 59 | 60 | planeGeom = gl.drawable(meshes.plane(50)); 61 | 62 | quad = gl.drawable(meshes.quad); 63 | 64 | displayShader = gl.shader({ 65 | common: '#line 56 vsm-filtered-shadow.coffee\nvarying vec3 vWorldNormal; varying vec4 vWorldPosition;\nuniform mat4 camProj, camView;\nuniform mat4 lightProj, lightView; uniform mat3 lightRot;\nuniform mat4 model;', 66 | vertex: '#line 62 vsm-filtered-shadow.coffee\nattribute vec3 position, normal;\n\nvoid main(){\n vWorldNormal = normal;\n vWorldPosition = model * vec4(position, 1.0);\n gl_Position = camProj * camView * vWorldPosition;\n}', 67 | fragment: '#line 71 vsm-filtered-shadow.coffee\nuniform sampler2D sLightDepth;\n\nfloat linstep(float low, float high, float v){\n return clamp((v-low)/(high-low), 0.0, 1.0);\n}\n\nfloat VSM(sampler2D depths, vec2 uv, float compare){\n vec2 moments = texture2D(depths, uv).xy;\n float p = smoothstep(compare-0.02, compare, moments.x);\n float variance = max(moments.y - moments.x*moments.x, -0.001);\n float d = compare - moments.x;\n float p_max = linstep(0.2, 1.0, variance / (variance + d*d));\n return clamp(max(p, p_max), 0.0, 1.0);\n}\n\nfloat attenuation(vec3 dir){\n float dist = length(dir);\n float radiance = 1.0/(1.0+pow(dist/10.0, 2.0));\n return clamp(radiance*10.0, 0.0, 1.0);\n}\n\nfloat influence(vec3 normal, float coneAngle){\n float minConeAngle = ((360.0-coneAngle-10.0)/360.0)*PI;\n float maxConeAngle = ((360.0-coneAngle)/360.0)*PI;\n return smoothstep(minConeAngle, maxConeAngle, acos(normal.z));\n}\n\nfloat lambert(vec3 surfaceNormal, vec3 lightDirNormal){\n return max(0.0, dot(surfaceNormal, lightDirNormal));\n}\n\nvec3 skyLight(vec3 normal){\n return vec3(smoothstep(0.0, PI, PI-acos(normal.y)))*0.4;\n}\n\nvec3 gamma(vec3 color){\n return pow(color, vec3(2.2));\n}\n\nvoid main(){\n vec3 worldNormal = normalize(vWorldNormal);\n\n vec3 camPos = (camView * vWorldPosition).xyz;\n vec3 lightPos = (lightView * vWorldPosition).xyz;\n vec3 lightPosNormal = normalize(lightPos);\n vec3 lightSurfaceNormal = lightRot * worldNormal;\n vec4 lightDevice = lightProj * vec4(lightPos, 1.0);\n vec2 lightDeviceNormal = lightDevice.xy/lightDevice.w;\n vec2 lightUV = lightDeviceNormal*0.5+0.5;\n\n // shadow calculation\n float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0);\n float illuminated = VSM(sLightDepth, lightUV, lightDepth2);\n \n vec3 excident = (\n skyLight(worldNormal) +\n lambert(lightSurfaceNormal, -lightPosNormal) *\n influence(lightPosNormal, 55.0) *\n attenuation(lightPos) *\n illuminated\n );\n gl_FragColor = vec4(gamma(excident), 1.0);\n}' 68 | }); 69 | 70 | lightShader = gl.shader({ 71 | common: '#line 138 vsm-filtered-shadow.coffee\nvarying vec3 vWorldNormal; varying vec4 vWorldPosition;\nuniform mat4 lightProj, lightView; uniform mat3 lightRot;\nuniform mat4 model;', 72 | vertex: '#line 143 vsm-filtered-shadow.coffee\nattribute vec3 position, normal;\n\nvoid main(){\n vWorldNormal = normal;\n vWorldPosition = model * vec4(position, 1.0);\n gl_Position = lightProj * lightView * vWorldPosition;\n}', 73 | fragment: '#line 152 vsm-filtered-shadow.coffee\n#extension GL_OES_standard_derivatives : enable\nvoid main(){\n vec3 worldNormal = normalize(vWorldNormal);\n vec3 lightPos = (lightView * vWorldPosition).xyz;\n float depth = clamp(length(lightPos)/40.0, 0.0, 1.0);\n float dx = dFdx(depth);\n float dy = dFdy(depth);\n gl_FragColor = vec4(depth, pow(depth, 2.0) + 0.25*(dx*dx + dy*dy), 0.0, 1.0);\n}' 74 | }); 75 | 76 | lightDepthTexture = gl.texture({ 77 | type: floatExt.type, 78 | channels: 'rgba' 79 | }).bind().setSize(256, 256).linear().clampToEdge(); 80 | 81 | lightFramebuffer = gl.framebuffer().bind().color(lightDepthTexture).depth().unbind(); 82 | 83 | Filter = (function() { 84 | 85 | function Filter(size, filter) { 86 | this.size = size; 87 | this.output = gl.texture({ 88 | type: floatExt.type, 89 | channels: 'rgba' 90 | }).bind().setSize(this.size, this.size).linear().clampToEdge(); 91 | this.framebuffer = gl.framebuffer().bind().color(this.output).unbind(); 92 | this.shader = gl.shader({ 93 | common: '#line 174 vsm-filtered-shadow.coffee\nvarying vec2 texcoord;', 94 | vertex: '#line 177 vsm-filtered-shadow.coffee\nattribute vec2 position;\n\nvoid main(){\n texcoord = position*0.5+0.5;\n gl_Position = vec4(position, 0.0, 1.0);\n}', 95 | fragment: "#line 186 vsm-filtered-shadow.coffee\nuniform vec2 viewport;\nuniform sampler2D source;\n\nvec3 get(float x, float y){\n vec2 off = vec2(x, y);\n return texture2D(source, texcoord+off/viewport).rgb;\n}\nvec3 get(int x, int y){\n vec2 off = vec2(x, y);\n return texture2D(source, texcoord+off/viewport).rgb;\n}\nvec3 filter(){\n " + filter + "\n}\nvoid main(){\n gl_FragColor = vec4(filter(), 1.0);\n}" 96 | }); 97 | } 98 | 99 | Filter.prototype.bind = function(unit) { 100 | return this.output.bind(unit); 101 | }; 102 | 103 | Filter.prototype.apply = function(source) { 104 | this.framebuffer.bind(); 105 | gl.viewport(0, 0, this.size, this.size); 106 | this.shader.use().vec2('viewport', this.size, this.size).sampler('source', source).draw(quad); 107 | return this.framebuffer.unbind(); 108 | }; 109 | 110 | return Filter; 111 | 112 | })(); 113 | 114 | downsample128 = new Filter(128, '#line 216 vsm-filtered-shadow.coffee\nreturn get(0.0, 0.0);'); 115 | 116 | downsample64 = new Filter(64, '#line 220 vsm-filtered-shadow.coffee\nreturn get(0.0, 0.0);'); 117 | 118 | boxFilter = new Filter(64, '#line 224 vsm-filtered-shadow.coffee\nvec3 result = vec3(0.0);\nfor(int x=-1; x<=1; x++){\n for(int y=-1; y<=1; y++){\n result += get(x,y);\n }\n}\nreturn result/9.0;'); 119 | 120 | camProj = gl.mat4(); 121 | 122 | camView = gl.mat4(); 123 | 124 | lightProj = gl.mat4().perspective({ 125 | fov: 60 126 | }, 1, { 127 | near: 0.01, 128 | far: 100 129 | }); 130 | 131 | lightView = gl.mat4().trans(0, 0, -6).rotatex(30).rotatey(110); 132 | 133 | lightRot = gl.mat3().fromMat4Rot(lightView); 134 | 135 | model = gl.mat4(); 136 | 137 | counter = -Math.PI * 0.5; 138 | 139 | offset = 0; 140 | 141 | camDist = 10; 142 | 143 | camRot = 55; 144 | 145 | camPitch = 41; 146 | 147 | mouseup = function() { 148 | return $(document).unbind('mousemove', mousemove).unbind('mouseup', mouseup); 149 | }; 150 | 151 | mousemove = function(_arg) { 152 | var originalEvent, x, y, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; 153 | originalEvent = _arg.originalEvent; 154 | x = (_ref = (_ref1 = (_ref2 = originalEvent.movementX) != null ? _ref2 : originalEvent.webkitMovementX) != null ? _ref1 : originalEvent.mozMovementX) != null ? _ref : originalEvent.oMovementX; 155 | y = (_ref3 = (_ref4 = (_ref5 = originalEvent.movementY) != null ? _ref5 : originalEvent.webkitMovementY) != null ? _ref4 : originalEvent.mozMovementY) != null ? _ref3 : originalEvent.oMovementY; 156 | camRot += x; 157 | camPitch += y; 158 | if (camPitch > 85) { 159 | return camPitch = 85; 160 | } else if (camPitch < 1) { 161 | return camPitch = 1; 162 | } 163 | }; 164 | 165 | $(canvas).bind('mousedown', function() { 166 | $(document).bind('mousemove', mousemove).bind('mouseup', mouseup); 167 | return false; 168 | }).bind('mousewheel', function(_arg) { 169 | var originalEvent; 170 | originalEvent = _arg.originalEvent; 171 | camDist -= originalEvent.wheelDeltaY / 250; 172 | return false; 173 | }).bind('DOMMouseScroll', function(_arg) { 174 | var originalEvent; 175 | originalEvent = _arg.originalEvent; 176 | camDist += originalEvent.detail / 5; 177 | return false; 178 | }); 179 | 180 | drawScene = function(shader) { 181 | return shader.mat4('model', model.ident().trans(0, 0, 0)).draw(planeGeom).mat4('model', model.ident().trans(0, 1 + offset, 0)).draw(cubeGeom).mat4('model', model.ident().trans(5, 1, -1)).draw(cubeGeom); 182 | }; 183 | 184 | drawLight = function() { 185 | lightFramebuffer.bind(); 186 | gl.viewport(0, 0, lightDepthTexture.width, lightDepthTexture.height).clearColor(1, 1, 1, 1).clearDepth(1).cullFace('back'); 187 | lightShader.use().mat4('lightView', lightView).mat4('lightProj', lightProj).mat3('lightRot', lightRot); 188 | drawScene(lightShader); 189 | lightFramebuffer.unbind(); 190 | downsample128.apply(lightDepthTexture); 191 | downsample64.apply(downsample128); 192 | return boxFilter.apply(downsample64); 193 | }; 194 | 195 | drawCamera = function() { 196 | gl.adjustSize().viewport().cullFace('back').clearColor(0, 0, 0, 0).clearDepth(1); 197 | camProj.perspective({ 198 | fov: 60, 199 | aspect: gl.aspect, 200 | near: 0.01, 201 | far: 100 202 | }); 203 | camView.ident().trans(0, -1, -camDist).rotatex(camPitch).rotatey(camRot); 204 | displayShader.use().mat4('camProj', camProj).mat4('camView', camView).mat4('lightView', lightView).mat4('lightProj', lightProj).mat3('lightRot', lightRot).sampler('sLightDepth', boxFilter); 205 | return drawScene(displayShader); 206 | }; 207 | 208 | draw = function() { 209 | drawLight(); 210 | return drawCamera(); 211 | }; 212 | 213 | draw(); 214 | 215 | gl.animationInterval(function() { 216 | if (hover) { 217 | if (animate) { 218 | offset = 1 + Math.sin(counter); 219 | counter += 1 / 30; 220 | } else { 221 | offset = 0; 222 | } 223 | return draw(); 224 | } 225 | }); 226 | 227 | }).call(this); 228 | -------------------------------------------------------------------------------- /vsm-filtered-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyalot/soft-shadow-mapping/e425c99c93a11f2b46f01b37bf47e10069133144/vsm-filtered-shadow.png -------------------------------------------------------------------------------- /vsm-shadow.coffee: -------------------------------------------------------------------------------- 1 | ## add the canvas to the dom ## 2 | name = 'vsm-shadow' 3 | document.write "
" 4 | container = $('#' + name) 5 | canvas = $('').appendTo(container)[0] 6 | 7 | ## setup the framework ## 8 | try 9 | gl = new WebGLFramework(canvas) 10 | .depthTest() 11 | 12 | floatExt = gl.getFloatExtension 13 | require: ['renderable', 'filterable'] 14 | 15 | gl.getExt('OES_standard_derivatives') 16 | catch error 17 | container.empty() 18 | $('
').text(error).appendTo(container) 19 | $('
').text('(screenshot instead)').appendTo(container) 20 | $("").appendTo(container) 21 | return 22 | 23 | ## fullscreen handling ## 24 | fullscreenImg = $('') 25 | .appendTo(container) 26 | .click -> gl.toggleFullscreen(container[0]) 27 | 28 | gl.onFullscreenChange (isFullscreen) -> 29 | if isFullscreen 30 | container.addClass('fullscreen') 31 | fullscreenImg.attr('src', 'exit-fullscreen.png') 32 | else 33 | container.removeClass('fullscreen') 34 | fullscreenImg.attr('src', 'fullscreen.png') 35 | 36 | ## handle mouse over ## 37 | hover = false 38 | container.hover (-> hover = true), (-> hover = false) 39 | 40 | ## animation control ## 41 | animate = true 42 | controls = $('
') 43 | .appendTo(container) 44 | $('').appendTo(controls) 45 | $('') 46 | .appendTo(controls) 47 | .change -> 48 | animate = @checked 49 | 50 | ## create webgl objects ## 51 | cubeGeom = gl.drawable meshes.cube 52 | planeGeom = gl.drawable meshes.plane(50) 53 | displayShader = gl.shader 54 | common: '''//essl 55 | varying vec3 vWorldNormal; varying vec4 vWorldPosition; 56 | uniform mat4 camProj, camView; 57 | uniform mat4 lightProj, lightView; uniform mat3 lightRot; 58 | uniform mat4 model; 59 | ''' 60 | vertex: '''//essl 61 | attribute vec3 position, normal; 62 | 63 | void main(){ 64 | vWorldNormal = normal; 65 | vWorldPosition = model * vec4(position, 1.0); 66 | gl_Position = camProj * camView * vWorldPosition; 67 | } 68 | ''' 69 | fragment: '''//essl 70 | uniform sampler2D sLightDepth; 71 | 72 | float linstep(float low, float high, float v){ 73 | return clamp((v-low)/(high-low), 0.0, 1.0); 74 | } 75 | 76 | float VSM(sampler2D depths, vec2 uv, float compare){ 77 | vec2 moments = texture2D(depths, uv).xy; 78 | float p = smoothstep(compare-0.02, compare, moments.x); 79 | float variance = max(moments.y - moments.x*moments.x, -0.001); 80 | float d = compare - moments.x; 81 | float p_max = linstep(0.2, 1.0, variance / (variance + d*d)); 82 | return clamp(max(p, p_max), 0.0, 1.0); 83 | } 84 | 85 | float attenuation(vec3 dir){ 86 | float dist = length(dir); 87 | float radiance = 1.0/(1.0+pow(dist/10.0, 2.0)); 88 | return clamp(radiance*10.0, 0.0, 1.0); 89 | } 90 | 91 | float influence(vec3 normal, float coneAngle){ 92 | float minConeAngle = ((360.0-coneAngle-10.0)/360.0)*PI; 93 | float maxConeAngle = ((360.0-coneAngle)/360.0)*PI; 94 | return smoothstep(minConeAngle, maxConeAngle, acos(normal.z)); 95 | } 96 | 97 | float lambert(vec3 surfaceNormal, vec3 lightDirNormal){ 98 | return max(0.0, dot(surfaceNormal, lightDirNormal)); 99 | } 100 | 101 | vec3 skyLight(vec3 normal){ 102 | return vec3(smoothstep(0.0, PI, PI-acos(normal.y)))*0.4; 103 | } 104 | 105 | vec3 gamma(vec3 color){ 106 | return pow(color, vec3(2.2)); 107 | } 108 | 109 | void main(){ 110 | vec3 worldNormal = normalize(vWorldNormal); 111 | 112 | vec3 camPos = (camView * vWorldPosition).xyz; 113 | vec3 lightPos = (lightView * vWorldPosition).xyz; 114 | vec3 lightPosNormal = normalize(lightPos); 115 | vec3 lightSurfaceNormal = lightRot * worldNormal; 116 | vec4 lightDevice = lightProj * vec4(lightPos, 1.0); 117 | vec2 lightDeviceNormal = lightDevice.xy/lightDevice.w; 118 | vec2 lightUV = lightDeviceNormal*0.5+0.5; 119 | 120 | // shadow calculation 121 | float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0); 122 | float illuminated = VSM(sLightDepth, lightUV, lightDepth2); 123 | 124 | vec3 excident = ( 125 | skyLight(worldNormal) + 126 | lambert(lightSurfaceNormal, -lightPosNormal) * 127 | influence(lightPosNormal, 55.0) * 128 | attenuation(lightPos) * 129 | illuminated 130 | ); 131 | gl_FragColor = vec4(gamma(excident), 1.0); 132 | } 133 | ''' 134 | 135 | lightShader = gl.shader 136 | common: '''//essl 137 | varying vec3 vWorldNormal; varying vec4 vWorldPosition; 138 | uniform mat4 lightProj, lightView; uniform mat3 lightRot; 139 | uniform mat4 model; 140 | ''' 141 | vertex: '''//essl 142 | attribute vec3 position, normal; 143 | 144 | void main(){ 145 | vWorldNormal = normal; 146 | vWorldPosition = model * vec4(position, 1.0); 147 | gl_Position = lightProj * lightView * vWorldPosition; 148 | } 149 | ''' 150 | fragment: '''//essl 151 | #extension GL_OES_standard_derivatives : enable 152 | void main(){ 153 | vec3 worldNormal = normalize(vWorldNormal); 154 | vec3 lightPos = (lightView * vWorldPosition).xyz; 155 | float depth = clamp(length(lightPos)/40.0, 0.0, 1.0); 156 | float dx = dFdx(depth); 157 | float dy = dFdy(depth); 158 | gl_FragColor = vec4(depth, pow(depth, 2.0) + 0.25*(dx*dx + dy*dy), 0.0, 1.0); 159 | } 160 | ''' 161 | lightDepthTexture = gl.texture(type:floatExt.type, channels:'rgba').bind().setSize(64, 64).linear().clampToEdge() 162 | lightFramebuffer = gl.framebuffer().bind().color(lightDepthTexture).depth().unbind() 163 | 164 | ## matrix setup ## 165 | camProj = gl.mat4() 166 | camView = gl.mat4() 167 | lightProj = gl.mat4().perspective(fov:60, 1, near:0.01, far:100) 168 | lightView = gl.mat4().trans(0, 0, -6).rotatex(30).rotatey(110) 169 | lightRot = gl.mat3().fromMat4Rot(lightView) 170 | model = gl.mat4() 171 | 172 | ## state variables ## 173 | counter = -Math.PI*0.5 174 | offset = 0 175 | camDist = 10 176 | camRot = 55 177 | camPitch = 41 178 | 179 | ## mouse handling ## 180 | mouseup = -> $(document).unbind('mousemove', mousemove).unbind('mouseup', mouseup) 181 | mousemove = ({originalEvent}) -> 182 | x = originalEvent.movementX ? originalEvent.webkitMovementX ? originalEvent.mozMovementX ? originalEvent.oMovementX 183 | y = originalEvent.movementY ? originalEvent.webkitMovementY ? originalEvent.mozMovementY ? originalEvent.oMovementY 184 | camRot += x 185 | camPitch += y 186 | if camPitch > 85 then camPitch = 85 187 | else if camPitch < 1 then camPitch = 1 188 | 189 | $(canvas) 190 | .bind 'mousedown', -> 191 | $(document).bind('mousemove', mousemove).bind('mouseup', mouseup) 192 | return false 193 | .bind 'mousewheel', ({originalEvent}) -> 194 | camDist -= originalEvent.wheelDeltaY/250 195 | return false 196 | .bind 'DOMMouseScroll', ({originalEvent}) -> 197 | camDist += originalEvent.detail/5 198 | return false 199 | 200 | ## drawing methods ## 201 | drawScene = (shader) -> 202 | shader 203 | .mat4('model', model.ident().trans(0, 0, 0)) 204 | .draw(planeGeom) 205 | .mat4('model', model.ident().trans(0, 1+offset, 0)) 206 | .draw(cubeGeom) 207 | .mat4('model', model.ident().trans(5, 1, -1)) 208 | .draw(cubeGeom) 209 | 210 | drawLight = -> 211 | lightFramebuffer.bind() 212 | gl 213 | .viewport(0, 0, lightDepthTexture.width, lightDepthTexture.height) 214 | .clearColor(1,1,1,1) 215 | .clearDepth(1) 216 | .cullFace('back') 217 | 218 | lightShader.use() 219 | .mat4('lightView', lightView) 220 | .mat4('lightProj', lightProj) 221 | .mat3('lightRot', lightRot) 222 | drawScene lightShader 223 | lightFramebuffer.unbind() 224 | 225 | drawCamera = -> 226 | gl 227 | .adjustSize() 228 | .viewport() 229 | .cullFace('back') 230 | .clearColor(0,0,0,0) 231 | .clearDepth(1) 232 | 233 | camProj.perspective(fov:60, aspect:gl.aspect, near:0.01, far:100) 234 | camView.ident().trans(0, -1, -camDist).rotatex(camPitch).rotatey(camRot) 235 | 236 | displayShader.use() 237 | .mat4('camProj', camProj) 238 | .mat4('camView', camView) 239 | .mat4('lightView', lightView) 240 | .mat4('lightProj', lightProj) 241 | .mat3('lightRot', lightRot) 242 | .sampler('sLightDepth', lightDepthTexture) 243 | drawScene displayShader 244 | 245 | draw = -> 246 | drawLight() 247 | drawCamera() 248 | 249 | ## mainloop ## 250 | draw() 251 | gl.animationInterval -> 252 | if hover 253 | if animate 254 | offset = 1 + Math.sin(counter) 255 | counter += 1/30 256 | else 257 | offset = 0 258 | draw() 259 | -------------------------------------------------------------------------------- /vsm-shadow.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var animate, camDist, camPitch, camProj, camRot, camView, canvas, container, controls, counter, cubeGeom, displayShader, draw, drawCamera, drawLight, drawScene, floatExt, fullscreenImg, gl, hover, lightDepthTexture, lightFramebuffer, lightProj, lightRot, lightShader, lightView, model, mousemove, mouseup, name, offset, planeGeom; 3 | 4 | name = 'vsm-shadow'; 5 | 6 | document.write("
"); 7 | 8 | container = $('#' + name); 9 | 10 | canvas = $('').appendTo(container)[0]; 11 | 12 | try { 13 | gl = new WebGLFramework(canvas).depthTest(); 14 | floatExt = gl.getFloatExtension({ 15 | require: ['renderable', 'filterable'] 16 | }); 17 | gl.getExt('OES_standard_derivatives'); 18 | } catch (error) { 19 | container.empty(); 20 | $('
').text(error).appendTo(container); 21 | $('
').text('(screenshot instead)').appendTo(container); 22 | $("").appendTo(container); 23 | return; 24 | } 25 | 26 | fullscreenImg = $('').appendTo(container).click(function() { 27 | return gl.toggleFullscreen(container[0]); 28 | }); 29 | 30 | gl.onFullscreenChange(function(isFullscreen) { 31 | if (isFullscreen) { 32 | container.addClass('fullscreen'); 33 | return fullscreenImg.attr('src', 'exit-fullscreen.png'); 34 | } else { 35 | container.removeClass('fullscreen'); 36 | return fullscreenImg.attr('src', 'fullscreen.png'); 37 | } 38 | }); 39 | 40 | hover = false; 41 | 42 | container.hover((function() { 43 | return hover = true; 44 | }), (function() { 45 | return hover = false; 46 | })); 47 | 48 | animate = true; 49 | 50 | controls = $('
').appendTo(container); 51 | 52 | $('').appendTo(controls); 53 | 54 | $('').appendTo(controls).change(function() { 55 | return animate = this.checked; 56 | }); 57 | 58 | cubeGeom = gl.drawable(meshes.cube); 59 | 60 | planeGeom = gl.drawable(meshes.plane(50)); 61 | 62 | displayShader = gl.shader({ 63 | common: '#line 54 vsm-shadow.coffee\nvarying vec3 vWorldNormal; varying vec4 vWorldPosition;\nuniform mat4 camProj, camView;\nuniform mat4 lightProj, lightView; uniform mat3 lightRot;\nuniform mat4 model;', 64 | vertex: '#line 60 vsm-shadow.coffee\nattribute vec3 position, normal;\n\nvoid main(){\n vWorldNormal = normal;\n vWorldPosition = model * vec4(position, 1.0);\n gl_Position = camProj * camView * vWorldPosition;\n}', 65 | fragment: '#line 69 vsm-shadow.coffee\nuniform sampler2D sLightDepth;\n\nfloat linstep(float low, float high, float v){\n return clamp((v-low)/(high-low), 0.0, 1.0);\n}\n\nfloat VSM(sampler2D depths, vec2 uv, float compare){\n vec2 moments = texture2D(depths, uv).xy;\n float p = smoothstep(compare-0.02, compare, moments.x);\n float variance = max(moments.y - moments.x*moments.x, -0.001);\n float d = compare - moments.x;\n float p_max = linstep(0.2, 1.0, variance / (variance + d*d));\n return clamp(max(p, p_max), 0.0, 1.0);\n}\n\nfloat attenuation(vec3 dir){\n float dist = length(dir);\n float radiance = 1.0/(1.0+pow(dist/10.0, 2.0));\n return clamp(radiance*10.0, 0.0, 1.0);\n}\n\nfloat influence(vec3 normal, float coneAngle){\n float minConeAngle = ((360.0-coneAngle-10.0)/360.0)*PI;\n float maxConeAngle = ((360.0-coneAngle)/360.0)*PI;\n return smoothstep(minConeAngle, maxConeAngle, acos(normal.z));\n}\n\nfloat lambert(vec3 surfaceNormal, vec3 lightDirNormal){\n return max(0.0, dot(surfaceNormal, lightDirNormal));\n}\n\nvec3 skyLight(vec3 normal){\n return vec3(smoothstep(0.0, PI, PI-acos(normal.y)))*0.4;\n}\n\nvec3 gamma(vec3 color){\n return pow(color, vec3(2.2));\n}\n\nvoid main(){\n vec3 worldNormal = normalize(vWorldNormal);\n\n vec3 camPos = (camView * vWorldPosition).xyz;\n vec3 lightPos = (lightView * vWorldPosition).xyz;\n vec3 lightPosNormal = normalize(lightPos);\n vec3 lightSurfaceNormal = lightRot * worldNormal;\n vec4 lightDevice = lightProj * vec4(lightPos, 1.0);\n vec2 lightDeviceNormal = lightDevice.xy/lightDevice.w;\n vec2 lightUV = lightDeviceNormal*0.5+0.5;\n\n // shadow calculation\n float lightDepth2 = clamp(length(lightPos)/40.0, 0.0, 1.0);\n float illuminated = VSM(sLightDepth, lightUV, lightDepth2);\n \n vec3 excident = (\n skyLight(worldNormal) +\n lambert(lightSurfaceNormal, -lightPosNormal) *\n influence(lightPosNormal, 55.0) *\n attenuation(lightPos) *\n illuminated\n );\n gl_FragColor = vec4(gamma(excident), 1.0);\n}' 66 | }); 67 | 68 | lightShader = gl.shader({ 69 | common: '#line 136 vsm-shadow.coffee\nvarying vec3 vWorldNormal; varying vec4 vWorldPosition;\nuniform mat4 lightProj, lightView; uniform mat3 lightRot;\nuniform mat4 model;', 70 | vertex: '#line 141 vsm-shadow.coffee\nattribute vec3 position, normal;\n\nvoid main(){\n vWorldNormal = normal;\n vWorldPosition = model * vec4(position, 1.0);\n gl_Position = lightProj * lightView * vWorldPosition;\n}', 71 | fragment: '#line 150 vsm-shadow.coffee\n#extension GL_OES_standard_derivatives : enable\nvoid main(){\n vec3 worldNormal = normalize(vWorldNormal);\n vec3 lightPos = (lightView * vWorldPosition).xyz;\n float depth = clamp(length(lightPos)/40.0, 0.0, 1.0);\n float dx = dFdx(depth);\n float dy = dFdy(depth);\n gl_FragColor = vec4(depth, pow(depth, 2.0) + 0.25*(dx*dx + dy*dy), 0.0, 1.0);\n}' 72 | }); 73 | 74 | lightDepthTexture = gl.texture({ 75 | type: floatExt.type, 76 | channels: 'rgba' 77 | }).bind().setSize(64, 64).linear().clampToEdge(); 78 | 79 | lightFramebuffer = gl.framebuffer().bind().color(lightDepthTexture).depth().unbind(); 80 | 81 | camProj = gl.mat4(); 82 | 83 | camView = gl.mat4(); 84 | 85 | lightProj = gl.mat4().perspective({ 86 | fov: 60 87 | }, 1, { 88 | near: 0.01, 89 | far: 100 90 | }); 91 | 92 | lightView = gl.mat4().trans(0, 0, -6).rotatex(30).rotatey(110); 93 | 94 | lightRot = gl.mat3().fromMat4Rot(lightView); 95 | 96 | model = gl.mat4(); 97 | 98 | counter = -Math.PI * 0.5; 99 | 100 | offset = 0; 101 | 102 | camDist = 10; 103 | 104 | camRot = 55; 105 | 106 | camPitch = 41; 107 | 108 | mouseup = function() { 109 | return $(document).unbind('mousemove', mousemove).unbind('mouseup', mouseup); 110 | }; 111 | 112 | mousemove = function(_arg) { 113 | var originalEvent, x, y, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; 114 | originalEvent = _arg.originalEvent; 115 | x = (_ref = (_ref1 = (_ref2 = originalEvent.movementX) != null ? _ref2 : originalEvent.webkitMovementX) != null ? _ref1 : originalEvent.mozMovementX) != null ? _ref : originalEvent.oMovementX; 116 | y = (_ref3 = (_ref4 = (_ref5 = originalEvent.movementY) != null ? _ref5 : originalEvent.webkitMovementY) != null ? _ref4 : originalEvent.mozMovementY) != null ? _ref3 : originalEvent.oMovementY; 117 | camRot += x; 118 | camPitch += y; 119 | if (camPitch > 85) { 120 | return camPitch = 85; 121 | } else if (camPitch < 1) { 122 | return camPitch = 1; 123 | } 124 | }; 125 | 126 | $(canvas).bind('mousedown', function() { 127 | $(document).bind('mousemove', mousemove).bind('mouseup', mouseup); 128 | return false; 129 | }).bind('mousewheel', function(_arg) { 130 | var originalEvent; 131 | originalEvent = _arg.originalEvent; 132 | camDist -= originalEvent.wheelDeltaY / 250; 133 | return false; 134 | }).bind('DOMMouseScroll', function(_arg) { 135 | var originalEvent; 136 | originalEvent = _arg.originalEvent; 137 | camDist += originalEvent.detail / 5; 138 | return false; 139 | }); 140 | 141 | drawScene = function(shader) { 142 | return shader.mat4('model', model.ident().trans(0, 0, 0)).draw(planeGeom).mat4('model', model.ident().trans(0, 1 + offset, 0)).draw(cubeGeom).mat4('model', model.ident().trans(5, 1, -1)).draw(cubeGeom); 143 | }; 144 | 145 | drawLight = function() { 146 | lightFramebuffer.bind(); 147 | gl.viewport(0, 0, lightDepthTexture.width, lightDepthTexture.height).clearColor(1, 1, 1, 1).clearDepth(1).cullFace('back'); 148 | lightShader.use().mat4('lightView', lightView).mat4('lightProj', lightProj).mat3('lightRot', lightRot); 149 | drawScene(lightShader); 150 | return lightFramebuffer.unbind(); 151 | }; 152 | 153 | drawCamera = function() { 154 | gl.adjustSize().viewport().cullFace('back').clearColor(0, 0, 0, 0).clearDepth(1); 155 | camProj.perspective({ 156 | fov: 60, 157 | aspect: gl.aspect, 158 | near: 0.01, 159 | far: 100 160 | }); 161 | camView.ident().trans(0, -1, -camDist).rotatex(camPitch).rotatey(camRot); 162 | displayShader.use().mat4('camProj', camProj).mat4('camView', camView).mat4('lightView', lightView).mat4('lightProj', lightProj).mat3('lightRot', lightRot).sampler('sLightDepth', lightDepthTexture); 163 | return drawScene(displayShader); 164 | }; 165 | 166 | draw = function() { 167 | drawLight(); 168 | return drawCamera(); 169 | }; 170 | 171 | draw(); 172 | 173 | gl.animationInterval(function() { 174 | if (hover) { 175 | if (animate) { 176 | offset = 1 + Math.sin(counter); 177 | counter += 1 / 30; 178 | } else { 179 | offset = 0; 180 | } 181 | return draw(); 182 | } 183 | }); 184 | 185 | }).call(this); 186 | -------------------------------------------------------------------------------- /vsm-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyalot/soft-shadow-mapping/e425c99c93a11f2b46f01b37bf47e10069133144/vsm-shadow.png -------------------------------------------------------------------------------- /webgl-nuke-vendor-prefix.coffee: -------------------------------------------------------------------------------- 1 | if window.WebGLRenderingContext? 2 | vendors = ['WEBKIT', 'MOZ', 'MS', 'O'] 3 | vendorRe = /^WEBKIT_(.*)|MOZ_(.*)|MS_(.*)|O_(.*)/ 4 | 5 | getExtension = WebGLRenderingContext.prototype.getExtension 6 | WebGLRenderingContext.prototype.getExtension = (name) -> 7 | match = name.match vendorRe 8 | if match != null 9 | name = match[1] 10 | 11 | extobj = getExtension.call @, name 12 | if extobj == null 13 | for vendor in vendors 14 | extobj = getExtension.call @, vendor + '_' + name 15 | if extobj != null 16 | return extobj 17 | return null 18 | else 19 | return extobj 20 | 21 | getSupportedExtensions = WebGLRenderingContext.prototype.getSupportedExtensions 22 | WebGLRenderingContext.prototype.getSupportedExtensions = -> 23 | supported = getSupportedExtensions.call @ 24 | result = [] 25 | 26 | for extension in supported 27 | match = extension.match vendorRe 28 | if match != null 29 | extension = match[1] 30 | 31 | if extension not in result 32 | result.push extension 33 | 34 | return result 35 | -------------------------------------------------------------------------------- /webgl-nuke-vendor-prefix.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var getExtension, getSupportedExtensions, vendorRe, vendors, 3 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 4 | 5 | if (window.WebGLRenderingContext != null) { 6 | vendors = ['WEBKIT', 'MOZ', 'MS', 'O']; 7 | vendorRe = /^WEBKIT_(.*)|MOZ_(.*)|MS_(.*)|O_(.*)/; 8 | getExtension = WebGLRenderingContext.prototype.getExtension; 9 | WebGLRenderingContext.prototype.getExtension = function(name) { 10 | var extobj, match, vendor, _i, _len; 11 | match = name.match(vendorRe); 12 | if (match !== null) { 13 | name = match[1]; 14 | } 15 | extobj = getExtension.call(this, name); 16 | if (extobj === null) { 17 | for (_i = 0, _len = vendors.length; _i < _len; _i++) { 18 | vendor = vendors[_i]; 19 | extobj = getExtension.call(this, vendor + '_' + name); 20 | if (extobj !== null) { 21 | return extobj; 22 | } 23 | } 24 | return null; 25 | } else { 26 | return extobj; 27 | } 28 | }; 29 | getSupportedExtensions = WebGLRenderingContext.prototype.getSupportedExtensions; 30 | WebGLRenderingContext.prototype.getSupportedExtensions = function() { 31 | var extension, match, result, supported, _i, _len; 32 | supported = getSupportedExtensions.call(this); 33 | result = []; 34 | for (_i = 0, _len = supported.length; _i < _len; _i++) { 35 | extension = supported[_i]; 36 | match = extension.match(vendorRe); 37 | if (match !== null) { 38 | extension = match[1]; 39 | } 40 | if (__indexOf.call(result, extension) < 0) { 41 | result.push(extension); 42 | } 43 | } 44 | return result; 45 | }; 46 | } 47 | 48 | }).call(this); 49 | -------------------------------------------------------------------------------- /webgl-texture-float-extension-shims.coffee: -------------------------------------------------------------------------------- 1 | createSourceCanvas = -> 2 | canvas = document.createElement 'canvas' 3 | canvas.width = 2 4 | canvas.height = 2 5 | ctx = canvas.getContext '2d' 6 | imageData = ctx.getImageData(0, 0, 2, 2) 7 | imageData.data.set(new Uint8ClampedArray([ 8 | 0,0,0,0, 9 | 255,255,255,255, 10 | 0,0,0,0, 11 | 255,255,255,255, 12 | ])) 13 | ctx.putImageData(imageData, 0, 0) 14 | return canvas 15 | 16 | createSourceCanvas() 17 | 18 | checkFloatLinear = (gl, sourceType) -> 19 | ## drawing program ## 20 | program = gl.createProgram() 21 | vertexShader = gl.createShader(gl.VERTEX_SHADER) 22 | gl.attachShader(program, vertexShader) 23 | gl.shaderSource(vertexShader, ''' 24 | attribute vec2 position; 25 | void main(){ 26 | gl_Position = vec4(position, 0.0, 1.0); 27 | } 28 | ''') 29 | 30 | gl.compileShader(vertexShader) 31 | if not gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS) 32 | throw gl.getShaderInfoLog(vertexShader) 33 | 34 | fragmentShader = gl.createShader(gl.FRAGMENT_SHADER) 35 | gl.attachShader(program, fragmentShader) 36 | gl.shaderSource(fragmentShader, ''' 37 | uniform sampler2D source; 38 | void main(){ 39 | gl_FragColor = texture2D(source, vec2(1.0, 1.0)); 40 | } 41 | ''') 42 | gl.compileShader(fragmentShader) 43 | if not gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS) 44 | throw gl.getShaderInfoLog(fragmentShader) 45 | 46 | gl.linkProgram(program) 47 | if not gl.getProgramParameter(program, gl.LINK_STATUS) 48 | throw gl.getProgramInfoLog(program) 49 | 50 | gl.useProgram(program) 51 | 52 | cleanup = -> 53 | gl.deleteShader(fragmentShader) 54 | gl.deleteShader(vertexShader) 55 | gl.deleteProgram(program) 56 | gl.deleteBuffer(buffer) 57 | gl.deleteTexture(source) 58 | gl.deleteTexture(target) 59 | gl.deleteFramebuffer(framebuffer) 60 | 61 | gl.bindBuffer(gl.ARRAY_BUFFER, null) 62 | gl.useProgram(null) 63 | gl.bindTexture(gl.TEXTURE_2D, null) 64 | gl.bindFramebuffer(gl.FRAMEBUFFER, null) 65 | 66 | ## target FBO ## 67 | target = gl.createTexture() 68 | gl.bindTexture(gl.TEXTURE_2D, target) 69 | gl.texImage2D( 70 | gl.TEXTURE_2D, 71 | 0, 72 | gl.RGBA, 73 | 2, 2, 74 | 0, 75 | gl.RGBA, 76 | gl.UNSIGNED_BYTE, 77 | null, 78 | ) 79 | 80 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) 81 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) 82 | 83 | framebuffer = gl.createFramebuffer() 84 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer) 85 | gl.framebufferTexture2D( 86 | gl.FRAMEBUFFER, 87 | gl.COLOR_ATTACHMENT0, 88 | gl.TEXTURE_2D, 89 | target, 90 | 0 91 | ) 92 | 93 | ## source texture ## 94 | sourceCanvas = createSourceCanvas() 95 | source = gl.createTexture() 96 | gl.bindTexture(gl.TEXTURE_2D, source) 97 | gl.texImage2D( 98 | gl.TEXTURE_2D, 99 | 0, 100 | gl.RGBA, 101 | gl.RGBA, 102 | sourceType, 103 | sourceCanvas, 104 | ) 105 | 106 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) 107 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) 108 | 109 | ## create VBO ## 110 | vertices = new Float32Array([ 111 | 1, 1, 112 | -1, 1, 113 | -1, -1, 114 | 115 | 1, 1, 116 | -1, -1, 117 | 1, -1, 118 | ]) 119 | buffer = gl.createBuffer() 120 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer) 121 | gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW) 122 | positionLoc = gl.getAttribLocation(program, 'position') 123 | sourceLoc = gl.getUniformLocation(program, 'source') 124 | gl.enableVertexAttribArray(positionLoc) 125 | gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0) 126 | gl.uniform1i(sourceLoc, 0) 127 | gl.drawArrays(gl.TRIANGLES, 0, 6) 128 | 129 | readBuffer = new Uint8Array(4*4) 130 | gl.readPixels(0, 0, 2, 2, gl.RGBA, gl.UNSIGNED_BYTE, readBuffer) 131 | 132 | result = Math.abs(readBuffer[0] - 127) < 10 133 | 134 | cleanup() 135 | return result 136 | 137 | checkTexture = (gl, targetType) -> 138 | target = gl.createTexture() 139 | gl.bindTexture(gl.TEXTURE_2D, target) 140 | gl.texImage2D( 141 | gl.TEXTURE_2D, 142 | 0, 143 | gl.RGBA, 144 | 2, 2, 145 | 0, 146 | gl.RGBA, 147 | targetType, 148 | null, 149 | ) 150 | 151 | if gl.getError() == 0 152 | gl.deleteTexture(target) 153 | return true 154 | else 155 | gl.deleteTexture(target) 156 | return false 157 | 158 | checkColorBuffer = (gl, targetType) -> 159 | target = gl.createTexture() 160 | gl.bindTexture(gl.TEXTURE_2D, target) 161 | gl.texImage2D( 162 | gl.TEXTURE_2D, 163 | 0, 164 | gl.RGBA, 165 | 2, 2, 166 | 0, 167 | gl.RGBA, 168 | targetType, 169 | null, 170 | ) 171 | 172 | framebuffer = gl.createFramebuffer() 173 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer) 174 | gl.framebufferTexture2D( 175 | gl.FRAMEBUFFER, 176 | gl.COLOR_ATTACHMENT0, 177 | gl.TEXTURE_2D, 178 | target, 179 | 0 180 | ) 181 | 182 | check = gl.checkFramebufferStatus(gl.FRAMEBUFFER) 183 | 184 | gl.deleteTexture(target) 185 | gl.deleteFramebuffer(framebuffer) 186 | gl.bindTexture(gl.TEXTURE_2D, null) 187 | gl.bindFramebuffer(gl.FRAMEBUFFER, null) 188 | 189 | if check == gl.FRAMEBUFFER_COMPLETE 190 | return true 191 | else 192 | return false 193 | 194 | shimExtensions = [] 195 | shimLookup = {} 196 | unshimExtensions = [] 197 | 198 | checkSupport = -> 199 | canvas = document.createElement 'canvas' 200 | gl = null 201 | try 202 | gl = canvas.getContext 'experimental-webgl' 203 | if(gl == null) 204 | gl = canvas.getContext 'webgl' 205 | 206 | if gl? 207 | singleFloatExt = gl.getExtension 'OES_texture_float' 208 | if singleFloatExt == null 209 | if checkTexture gl, gl.FLOAT 210 | singleFloatTexturing = true 211 | shimExtensions.push 'OES_texture_float' 212 | shimLookup.OES_texture_float = {shim:true} 213 | else 214 | singleFloatTexturing = false 215 | unshimExtensions.push 'OES_texture_float' 216 | else 217 | if checkTexture gl, gl.FLOAT 218 | singleFloatTexturing = true 219 | shimExtensions.push 'OES_texture_float' 220 | else 221 | singleFloatTexturing = false 222 | unshimExtensions.push 'OES_texture_float' 223 | 224 | if singleFloatTexturing 225 | extobj = gl.getExtension 'WEBGL_color_buffer_float' 226 | if extobj == null 227 | if checkColorBuffer gl, gl.FLOAT 228 | shimExtensions.push 'WEBGL_color_buffer_float' 229 | shimLookup.WEBGL_color_buffer_float = { 230 | shim: true 231 | RGBA32F_EXT: 0x8814 232 | RGB32F_EXT: 0x8815 233 | FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT: 0x8211 234 | UNSIGNED_NORMALIZED_EXT: 0x8C17 235 | } 236 | else 237 | unshimExtensions.push 'WEBGL_color_buffer_float' 238 | else 239 | if checkColorBuffer gl, gl.FLOAT 240 | shimExtensions.push 'WEBGL_color_buffer_float' 241 | else 242 | unshimExtensions.push 'WEBGL_color_buffer_float' 243 | 244 | extobj = gl.getExtension 'OES_texture_float_linear' 245 | if extobj == null 246 | if checkFloatLinear gl, gl.FLOAT 247 | shimExtensions.push 'OES_texture_float_linear' 248 | shimLookup.OES_texture_float_linear = {shim:true} 249 | else 250 | unshimExtensions.push 'OES_texture_float_linear' 251 | else 252 | if checkFloatLinear gl, gl.FLOAT 253 | shimExtensions.push 'OES_texture_float_linear' 254 | else 255 | unshimExtensions.push 'OES_texture_float_linear' 256 | 257 | halfFloatExt = gl.getExtension 'OES_texture_half_float' 258 | if halfFloatExt == null 259 | if checkTexture(gl, 0x8D61) 260 | halfFloatTexturing = true 261 | shimExtensions.push 'OES_texture_half_float' 262 | halfFloatExt = shimLookup.OES_texture_half_float = { 263 | HALF_FLOAT_OES: 0x8D61 264 | shim:true 265 | } 266 | else 267 | halfFloatTexturing = false 268 | unshimExtensions.push 'OES_texture_half_float' 269 | else 270 | if checkTexture(gl, halfFloatExt.HALF_FLOAT_OES) 271 | halfFloatTexturing = true 272 | shimExtensions.push 'OES_texture_half_float' 273 | else 274 | halfFloatTexturing = false 275 | unshimExtensions.push 'OES_texture_half_float' 276 | 277 | if halfFloatTexturing 278 | extobj = gl.getExtension 'EXT_color_buffer_half_float' 279 | if extobj == null 280 | if checkColorBuffer gl, halfFloatExt.HALF_FLOAT_OES 281 | shimExtensions.push 'EXT_color_buffer_half_float' 282 | shimLookup.EXT_color_buffer_half_float = { 283 | shim: true 284 | RGBA16F_EXT: 0x881A 285 | RGB16F_EXT: 0x881B 286 | FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT: 0x8211 287 | UNSIGNED_NORMALIZED_EXT: 0x8C17 288 | } 289 | else 290 | unshimExtensions.push 'EXT_color_buffer_half_float' 291 | else 292 | if checkColorBuffer gl, halfFloatExt.HALF_FLOAT_OES 293 | shimExtensions.push 'EXT_color_buffer_half_float' 294 | else 295 | unshimExtensions.push 'EXT_color_buffer_half_float' 296 | 297 | extobj = gl.getExtension 'OES_texture_half_float_linear' 298 | if extobj == null 299 | if checkFloatLinear gl, halfFloatExt.HALF_FLOAT_OES 300 | shimExtensions.push 'OES_texture_half_float_linear' 301 | shimLookup.OES_texture_half_float_linear = {shim:true} 302 | else 303 | unshimExtensions.push 'OES_texture_half_float_linear' 304 | else 305 | if checkFloatLinear gl, halfFloatExt.HALF_FLOAT_OES 306 | shimExtensions.push 'OES_texture_half_float_linear' 307 | else 308 | unshimExtensions.push 'OES_texture_half_float_linear' 309 | 310 | if window.WebGLRenderingContext? 311 | checkSupport() 312 | 313 | unshimLookup = {} 314 | for name in unshimExtensions 315 | unshimLookup[name] = true 316 | 317 | getExtension = WebGLRenderingContext.prototype.getExtension 318 | WebGLRenderingContext.prototype.getExtension = (name) -> 319 | extobj = shimLookup[name] 320 | if extobj == undefined 321 | if unshimLookup[name] 322 | return null 323 | else 324 | return getExtension.call @, name 325 | else 326 | return extobj 327 | 328 | getSupportedExtensions = WebGLRenderingContext.prototype.getSupportedExtensions 329 | WebGLRenderingContext.prototype.getSupportedExtensions = -> 330 | supported = getSupportedExtensions.call(@) 331 | result = [] 332 | 333 | for extension in supported 334 | if unshimLookup[extension] == undefined 335 | result.push(extension) 336 | 337 | for extension in shimExtensions 338 | if extension not in result 339 | result.push extension 340 | 341 | return result 342 | 343 | WebGLRenderingContext.prototype.getFloatExtension = (spec) -> 344 | spec.prefer ?= ['half'] 345 | spec.require ?= [] 346 | spec.throws ?= true 347 | 348 | singleTexture = @getExtension 'OES_texture_float' 349 | halfTexture = @getExtension 'OES_texture_half_float' 350 | singleFramebuffer = @getExtension 'WEBGL_color_buffer_float' 351 | halfFramebuffer = @getExtension 'EXT_color_buffer_half_float' 352 | singleLinear = @getExtension 'OES_texture_float_linear' 353 | halfLinear = @getExtension 'OES_texture_half_float_linear' 354 | 355 | single = { 356 | texture: singleTexture != null 357 | filterable: singleLinear != null 358 | renderable: singleFramebuffer != null 359 | score: 0 360 | precision: 'single' 361 | half: false 362 | single: true 363 | type: @FLOAT 364 | } 365 | 366 | half = { 367 | texture: halfTexture != null 368 | filterable: halfLinear != null 369 | renderable: halfFramebuffer != null 370 | score: 0 371 | precision: 'half' 372 | half: true 373 | single: false 374 | type: halfTexture?.HALF_FLOAT_OES ? null 375 | } 376 | 377 | candidates = [] 378 | if single.texture 379 | candidates.push(single) 380 | if half.texture 381 | candidates.push(half) 382 | 383 | result = [] 384 | for candidate in candidates 385 | use = true 386 | for name in spec.require 387 | if candidate[name] == false 388 | use = false 389 | if use 390 | result.push candidate 391 | 392 | for candidate in result 393 | for preference, i in spec.prefer 394 | importance = Math.pow 2, spec.prefer.length - i - 1 395 | if candidate[preference] 396 | candidate.score += importance 397 | 398 | result.sort (a, b) -> 399 | if a.score == b.score then 0 400 | else if a.score < b.score then 1 401 | else if a.score > b.score then -1 402 | 403 | if result.length == 0 404 | if spec.throws 405 | throw 'No floating point texture support that is ' + spec.require.join(', ') 406 | else 407 | return null 408 | else 409 | result = result[0] 410 | return { 411 | filterable: result.filterable 412 | renderable: result.renderable 413 | type: result.type 414 | precision: result.precision 415 | } 416 | -------------------------------------------------------------------------------- /webgl-texture-float-extension-shims.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var checkColorBuffer, checkFloatLinear, checkSupport, checkTexture, createSourceCanvas, getExtension, getSupportedExtensions, name, shimExtensions, shimLookup, unshimExtensions, unshimLookup, _i, _len, 3 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 4 | 5 | createSourceCanvas = function() { 6 | var canvas, ctx, imageData; 7 | canvas = document.createElement('canvas'); 8 | canvas.width = 2; 9 | canvas.height = 2; 10 | ctx = canvas.getContext('2d'); 11 | imageData = ctx.getImageData(0, 0, 2, 2); 12 | imageData.data.set(new Uint8ClampedArray([0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255])); 13 | ctx.putImageData(imageData, 0, 0); 14 | return canvas; 15 | }; 16 | 17 | createSourceCanvas(); 18 | 19 | checkFloatLinear = function(gl, sourceType) { 20 | var buffer, cleanup, fragmentShader, framebuffer, positionLoc, program, readBuffer, result, source, sourceCanvas, sourceLoc, target, vertexShader, vertices; 21 | program = gl.createProgram(); 22 | vertexShader = gl.createShader(gl.VERTEX_SHADER); 23 | gl.attachShader(program, vertexShader); 24 | gl.shaderSource(vertexShader, 'attribute vec2 position;\nvoid main(){\n gl_Position = vec4(position, 0.0, 1.0);\n}'); 25 | gl.compileShader(vertexShader); 26 | if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { 27 | throw gl.getShaderInfoLog(vertexShader); 28 | } 29 | fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 30 | gl.attachShader(program, fragmentShader); 31 | gl.shaderSource(fragmentShader, 'uniform sampler2D source;\nvoid main(){\n gl_FragColor = texture2D(source, vec2(1.0, 1.0));\n}'); 32 | gl.compileShader(fragmentShader); 33 | if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { 34 | throw gl.getShaderInfoLog(fragmentShader); 35 | } 36 | gl.linkProgram(program); 37 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 38 | throw gl.getProgramInfoLog(program); 39 | } 40 | gl.useProgram(program); 41 | cleanup = function() { 42 | gl.deleteShader(fragmentShader); 43 | gl.deleteShader(vertexShader); 44 | gl.deleteProgram(program); 45 | gl.deleteBuffer(buffer); 46 | gl.deleteTexture(source); 47 | gl.deleteTexture(target); 48 | gl.deleteFramebuffer(framebuffer); 49 | gl.bindBuffer(gl.ARRAY_BUFFER, null); 50 | gl.useProgram(null); 51 | gl.bindTexture(gl.TEXTURE_2D, null); 52 | return gl.bindFramebuffer(gl.FRAMEBUFFER, null); 53 | }; 54 | target = gl.createTexture(); 55 | gl.bindTexture(gl.TEXTURE_2D, target); 56 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 57 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 58 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 59 | framebuffer = gl.createFramebuffer(); 60 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 61 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, target, 0); 62 | sourceCanvas = createSourceCanvas(); 63 | source = gl.createTexture(); 64 | gl.bindTexture(gl.TEXTURE_2D, source); 65 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, sourceType, sourceCanvas); 66 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 67 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 68 | vertices = new Float32Array([1, 1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1]); 69 | buffer = gl.createBuffer(); 70 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 71 | gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); 72 | positionLoc = gl.getAttribLocation(program, 'position'); 73 | sourceLoc = gl.getUniformLocation(program, 'source'); 74 | gl.enableVertexAttribArray(positionLoc); 75 | gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0); 76 | gl.uniform1i(sourceLoc, 0); 77 | gl.drawArrays(gl.TRIANGLES, 0, 6); 78 | readBuffer = new Uint8Array(4 * 4); 79 | gl.readPixels(0, 0, 2, 2, gl.RGBA, gl.UNSIGNED_BYTE, readBuffer); 80 | result = Math.abs(readBuffer[0] - 127) < 10; 81 | cleanup(); 82 | return result; 83 | }; 84 | 85 | checkTexture = function(gl, targetType) { 86 | var target; 87 | target = gl.createTexture(); 88 | gl.bindTexture(gl.TEXTURE_2D, target); 89 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, targetType, null); 90 | if (gl.getError() === 0) { 91 | gl.deleteTexture(target); 92 | return true; 93 | } else { 94 | gl.deleteTexture(target); 95 | return false; 96 | } 97 | }; 98 | 99 | checkColorBuffer = function(gl, targetType) { 100 | var check, framebuffer, target; 101 | target = gl.createTexture(); 102 | gl.bindTexture(gl.TEXTURE_2D, target); 103 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, targetType, null); 104 | framebuffer = gl.createFramebuffer(); 105 | gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); 106 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, target, 0); 107 | check = gl.checkFramebufferStatus(gl.FRAMEBUFFER); 108 | gl.deleteTexture(target); 109 | gl.deleteFramebuffer(framebuffer); 110 | gl.bindTexture(gl.TEXTURE_2D, null); 111 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 112 | if (check === gl.FRAMEBUFFER_COMPLETE) { 113 | return true; 114 | } else { 115 | return false; 116 | } 117 | }; 118 | 119 | shimExtensions = []; 120 | 121 | shimLookup = {}; 122 | 123 | unshimExtensions = []; 124 | 125 | checkSupport = function() { 126 | var canvas, extobj, gl, halfFloatExt, halfFloatTexturing, singleFloatExt, singleFloatTexturing; 127 | canvas = document.createElement('canvas'); 128 | gl = null; 129 | try { 130 | gl = canvas.getContext('experimental-webgl'); 131 | if (gl === null) { 132 | gl = canvas.getContext('webgl'); 133 | } 134 | } catch (_error) {} 135 | if (gl != null) { 136 | singleFloatExt = gl.getExtension('OES_texture_float'); 137 | if (singleFloatExt === null) { 138 | if (checkTexture(gl, gl.FLOAT)) { 139 | singleFloatTexturing = true; 140 | shimExtensions.push('OES_texture_float'); 141 | shimLookup.OES_texture_float = { 142 | shim: true 143 | }; 144 | } else { 145 | singleFloatTexturing = false; 146 | unshimExtensions.push('OES_texture_float'); 147 | } 148 | } else { 149 | if (checkTexture(gl, gl.FLOAT)) { 150 | singleFloatTexturing = true; 151 | shimExtensions.push('OES_texture_float'); 152 | } else { 153 | singleFloatTexturing = false; 154 | unshimExtensions.push('OES_texture_float'); 155 | } 156 | } 157 | if (singleFloatTexturing) { 158 | extobj = gl.getExtension('WEBGL_color_buffer_float'); 159 | if (extobj === null) { 160 | if (checkColorBuffer(gl, gl.FLOAT)) { 161 | shimExtensions.push('WEBGL_color_buffer_float'); 162 | shimLookup.WEBGL_color_buffer_float = { 163 | shim: true, 164 | RGBA32F_EXT: 0x8814, 165 | RGB32F_EXT: 0x8815, 166 | FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT: 0x8211, 167 | UNSIGNED_NORMALIZED_EXT: 0x8C17 168 | }; 169 | } else { 170 | unshimExtensions.push('WEBGL_color_buffer_float'); 171 | } 172 | } else { 173 | if (checkColorBuffer(gl, gl.FLOAT)) { 174 | shimExtensions.push('WEBGL_color_buffer_float'); 175 | } else { 176 | unshimExtensions.push('WEBGL_color_buffer_float'); 177 | } 178 | } 179 | extobj = gl.getExtension('OES_texture_float_linear'); 180 | if (extobj === null) { 181 | if (checkFloatLinear(gl, gl.FLOAT)) { 182 | shimExtensions.push('OES_texture_float_linear'); 183 | shimLookup.OES_texture_float_linear = { 184 | shim: true 185 | }; 186 | } else { 187 | unshimExtensions.push('OES_texture_float_linear'); 188 | } 189 | } else { 190 | if (checkFloatLinear(gl, gl.FLOAT)) { 191 | shimExtensions.push('OES_texture_float_linear'); 192 | } else { 193 | unshimExtensions.push('OES_texture_float_linear'); 194 | } 195 | } 196 | } 197 | halfFloatExt = gl.getExtension('OES_texture_half_float'); 198 | if (halfFloatExt === null) { 199 | if (checkTexture(gl, 0x8D61)) { 200 | halfFloatTexturing = true; 201 | shimExtensions.push('OES_texture_half_float'); 202 | halfFloatExt = shimLookup.OES_texture_half_float = { 203 | HALF_FLOAT_OES: 0x8D61, 204 | shim: true 205 | }; 206 | } else { 207 | halfFloatTexturing = false; 208 | unshimExtensions.push('OES_texture_half_float'); 209 | } 210 | } else { 211 | if (checkTexture(gl, halfFloatExt.HALF_FLOAT_OES)) { 212 | halfFloatTexturing = true; 213 | shimExtensions.push('OES_texture_half_float'); 214 | } else { 215 | halfFloatTexturing = false; 216 | unshimExtensions.push('OES_texture_half_float'); 217 | } 218 | } 219 | if (halfFloatTexturing) { 220 | extobj = gl.getExtension('EXT_color_buffer_half_float'); 221 | if (extobj === null) { 222 | if (checkColorBuffer(gl, halfFloatExt.HALF_FLOAT_OES)) { 223 | shimExtensions.push('EXT_color_buffer_half_float'); 224 | shimLookup.EXT_color_buffer_half_float = { 225 | shim: true, 226 | RGBA16F_EXT: 0x881A, 227 | RGB16F_EXT: 0x881B, 228 | FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT: 0x8211, 229 | UNSIGNED_NORMALIZED_EXT: 0x8C17 230 | }; 231 | } else { 232 | unshimExtensions.push('EXT_color_buffer_half_float'); 233 | } 234 | } else { 235 | if (checkColorBuffer(gl, halfFloatExt.HALF_FLOAT_OES)) { 236 | shimExtensions.push('EXT_color_buffer_half_float'); 237 | } else { 238 | unshimExtensions.push('EXT_color_buffer_half_float'); 239 | } 240 | } 241 | extobj = gl.getExtension('OES_texture_half_float_linear'); 242 | if (extobj === null) { 243 | if (checkFloatLinear(gl, halfFloatExt.HALF_FLOAT_OES)) { 244 | shimExtensions.push('OES_texture_half_float_linear'); 245 | return shimLookup.OES_texture_half_float_linear = { 246 | shim: true 247 | }; 248 | } else { 249 | return unshimExtensions.push('OES_texture_half_float_linear'); 250 | } 251 | } else { 252 | if (checkFloatLinear(gl, halfFloatExt.HALF_FLOAT_OES)) { 253 | return shimExtensions.push('OES_texture_half_float_linear'); 254 | } else { 255 | return unshimExtensions.push('OES_texture_half_float_linear'); 256 | } 257 | } 258 | } 259 | } 260 | }; 261 | 262 | if (window.WebGLRenderingContext != null) { 263 | checkSupport(); 264 | unshimLookup = {}; 265 | for (_i = 0, _len = unshimExtensions.length; _i < _len; _i++) { 266 | name = unshimExtensions[_i]; 267 | unshimLookup[name] = true; 268 | } 269 | getExtension = WebGLRenderingContext.prototype.getExtension; 270 | WebGLRenderingContext.prototype.getExtension = function(name) { 271 | var extobj; 272 | extobj = shimLookup[name]; 273 | if (extobj === void 0) { 274 | if (unshimLookup[name]) { 275 | return null; 276 | } else { 277 | return getExtension.call(this, name); 278 | } 279 | } else { 280 | return extobj; 281 | } 282 | }; 283 | getSupportedExtensions = WebGLRenderingContext.prototype.getSupportedExtensions; 284 | WebGLRenderingContext.prototype.getSupportedExtensions = function() { 285 | var extension, result, supported, _j, _k, _len1, _len2; 286 | supported = getSupportedExtensions.call(this); 287 | result = []; 288 | for (_j = 0, _len1 = supported.length; _j < _len1; _j++) { 289 | extension = supported[_j]; 290 | if (unshimLookup[extension] === void 0) { 291 | result.push(extension); 292 | } 293 | } 294 | for (_k = 0, _len2 = shimExtensions.length; _k < _len2; _k++) { 295 | extension = shimExtensions[_k]; 296 | if (__indexOf.call(result, extension) < 0) { 297 | result.push(extension); 298 | } 299 | } 300 | return result; 301 | }; 302 | WebGLRenderingContext.prototype.getFloatExtension = function(spec) { 303 | var candidate, candidates, half, halfFramebuffer, halfLinear, halfTexture, i, importance, preference, result, single, singleFramebuffer, singleLinear, singleTexture, use, _j, _k, _l, _len1, _len2, _len3, _len4, _m, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; 304 | if ((_ref = spec.prefer) == null) { 305 | spec.prefer = ['half']; 306 | } 307 | if ((_ref1 = spec.require) == null) { 308 | spec.require = []; 309 | } 310 | if ((_ref2 = spec.throws) == null) { 311 | spec.throws = true; 312 | } 313 | singleTexture = this.getExtension('OES_texture_float'); 314 | halfTexture = this.getExtension('OES_texture_half_float'); 315 | singleFramebuffer = this.getExtension('WEBGL_color_buffer_float'); 316 | halfFramebuffer = this.getExtension('EXT_color_buffer_half_float'); 317 | singleLinear = this.getExtension('OES_texture_float_linear'); 318 | halfLinear = this.getExtension('OES_texture_half_float_linear'); 319 | single = { 320 | texture: singleTexture !== null, 321 | filterable: singleLinear !== null, 322 | renderable: singleFramebuffer !== null, 323 | score: 0, 324 | precision: 'single', 325 | half: false, 326 | single: true, 327 | type: this.FLOAT 328 | }; 329 | half = { 330 | texture: halfTexture !== null, 331 | filterable: halfLinear !== null, 332 | renderable: halfFramebuffer !== null, 333 | score: 0, 334 | precision: 'half', 335 | half: true, 336 | single: false, 337 | type: (_ref3 = halfTexture != null ? halfTexture.HALF_FLOAT_OES : void 0) != null ? _ref3 : null 338 | }; 339 | candidates = []; 340 | if (single.texture) { 341 | candidates.push(single); 342 | } 343 | if (half.texture) { 344 | candidates.push(half); 345 | } 346 | result = []; 347 | for (_j = 0, _len1 = candidates.length; _j < _len1; _j++) { 348 | candidate = candidates[_j]; 349 | use = true; 350 | _ref4 = spec.require; 351 | for (_k = 0, _len2 = _ref4.length; _k < _len2; _k++) { 352 | name = _ref4[_k]; 353 | if (candidate[name] === false) { 354 | use = false; 355 | } 356 | } 357 | if (use) { 358 | result.push(candidate); 359 | } 360 | } 361 | for (_l = 0, _len3 = result.length; _l < _len3; _l++) { 362 | candidate = result[_l]; 363 | _ref5 = spec.prefer; 364 | for (i = _m = 0, _len4 = _ref5.length; _m < _len4; i = ++_m) { 365 | preference = _ref5[i]; 366 | importance = Math.pow(2, spec.prefer.length - i - 1); 367 | if (candidate[preference]) { 368 | candidate.score += importance; 369 | } 370 | } 371 | } 372 | result.sort(function(a, b) { 373 | if (a.score === b.score) { 374 | return 0; 375 | } else if (a.score < b.score) { 376 | return 1; 377 | } else if (a.score > b.score) { 378 | return -1; 379 | } 380 | }); 381 | if (result.length === 0) { 382 | if (spec.throws) { 383 | throw 'No floating point texture support that is ' + spec.require.join(', '); 384 | } else { 385 | return null; 386 | } 387 | } else { 388 | result = result[0]; 389 | return { 390 | filterable: result.filterable, 391 | renderable: result.renderable, 392 | type: result.type, 393 | precision: result.precision 394 | }; 395 | } 396 | }; 397 | } 398 | 399 | }).call(this); 400 | --------------------------------------------------------------------------------