├── 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 |
--------------------------------------------------------------------------------