Snowfall made with native WebGL shader
A Pen by Boris Šehovac on CodePen.
<div class="snow" count="7000"></div> |
// https://github.com/bsehovac/shader-program | |
class Shader { | |
constructor( holder, options = {} ) { | |
options = Object.assign( { | |
antialias: false, | |
depthTest: false, | |
mousemove: false, | |
autosize: true, | |
side: 'front', | |
vertex: ` | |
precision highp float; | |
attribute vec4 a_position; | |
attribute vec4 a_color; | |
uniform float u_time; | |
uniform vec2 u_resolution; | |
uniform vec2 u_mousemove; | |
uniform mat4 u_projection; | |
varying vec4 v_color; | |
void main() { | |
gl_Position = u_projection * a_position; | |
gl_PointSize = (10.0 / gl_Position.w) * 100.0; | |
v_color = a_color; | |
}`, | |
fragment: ` | |
precision highp float; | |
uniform sampler2D u_texture; | |
uniform int u_hasTexture; | |
varying vec4 v_color; | |
void main() { | |
if ( u_hasTexture == 1 ) { | |
gl_FragColor = v_color * texture2D(u_texture, gl_PointCoord); | |
} else { | |
gl_FragColor = v_color; | |
} | |
}`, | |
uniforms: {}, | |
buffers: {}, | |
camera: {}, | |
texture: null, | |
onUpdate: ( () => {} ), | |
onResize: ( () => {} ), | |
}, options ) | |
const uniforms = Object.assign( { | |
time: { type: 'float', value: 0 }, | |
hasTexture: { type: 'int', value: 0 }, | |
resolution: { type: 'vec2', value: [ 0, 0 ] }, | |
mousemove: { type: 'vec2', value: [ 0, 0 ] }, | |
projection: { type: 'mat4', value: [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] }, | |
}, options.uniforms ) | |
const buffers = Object.assign( { | |
position: { size: 3, data: [] }, | |
color: { size: 4, data: [] }, | |
}, options.buffers ) | |
const camera = Object.assign( { | |
fov: 60, | |
near: 1, | |
far: 10000, | |
aspect: 1, | |
z: 100, | |
perspective: true, | |
}, options.camera ) | |
const canvas = document.createElement( 'canvas' ) | |
const gl = canvas.getContext( 'webgl', { antialias: options.antialias } ) | |
if ( ! gl ) return false | |
this.count = 0 | |
this.gl = gl | |
this.canvas = canvas | |
this.camera = camera | |
this.holder = holder | |
this.onUpdate = options.onUpdate | |
this.onResize = options.onResize | |
this.data = {} | |
holder.appendChild( canvas ) | |
this.createProgram( options.vertex, options.fragment ) | |
this.createBuffers( buffers ) | |
this.createUniforms( uniforms ) | |
this.updateBuffers() | |
this.updateUniforms() | |
this.createTexture( options.texture ) | |
gl.enable( gl.BLEND ) | |
gl.enable( gl.CULL_FACE ) | |
gl.blendFunc( gl.SRC_ALPHA, gl.ONE ) | |
gl[ options.depthTest ? 'enable' : 'disable' ]( gl.DEPTH_TEST ) | |
if ( options.autosize ) | |
window.addEventListener( 'resize', e => this.resize( e ), false ) | |
if ( options.mousemove ) | |
window.addEventListener( 'mousemove', e => this.mousemove( e ), false ) | |
this.resize() | |
this.update = this.update.bind( this ) | |
this.time = { start: performance.now(), old: performance.now() } | |
this.update() | |
} | |
mousemove( e ) { | |
let x = e.pageX / this.width * 2 - 1 | |
let y = e.pageY / this.height * 2 - 1 | |
this.uniforms.mousemove = [ x, y ] | |
} | |
resize( e ) { | |
const holder = this.holder | |
const canvas = this.canvas | |
const gl = this.gl | |
const width = this.width = holder.offsetWidth | |
const height = this.height = holder.offsetHeight | |
const aspect = this.aspect = width / height | |
const dpi = devicePixelRatio | |
canvas.width = width * dpi | |
canvas.height = height * dpi | |
canvas.style.width = width + 'px' | |
canvas.style.height = height + 'px' | |
gl.viewport( 0, 0, width * dpi, height * dpi ) | |
gl.clearColor( 0, 0, 0, 0 ) | |
this.uniforms.resolution = [ width, height ] | |
this.uniforms.projection = this.setProjection( aspect ) | |
this.onResize( width, height, dpi ) | |
} | |
setProjection( aspect ) { | |
const camera = this.camera | |
if ( camera.perspective ) { | |
camera.aspect = aspect | |
const fovRad = camera.fov * ( Math.PI / 180 ) | |
const f = Math.tan( Math.PI * 0.5 - 0.5 * fovRad ) | |
const rangeInv = 1.0 / ( camera.near - camera.far ) | |
const matrix = [ | |
f / camera.aspect, 0, 0, 0, | |
0, f, 0, 0, | |
0, 0, (camera.near + camera.far) * rangeInv, -1, | |
0, 0, camera.near * camera.far * rangeInv * 2, 0 | |
] | |
matrix[ 14 ] += camera.z | |
matrix[ 15 ] += camera.z | |
return matrix | |
} else { | |
return [ | |
2 / this.width, 0, 0, 0, | |
0, -2 / this.height, 0, 0, | |
0, 0, 1, 0, | |
-1, 1, 0, 1, | |
] | |
} | |
} | |
createShader( type, source ) { | |
const gl = this.gl | |
const shader = gl.createShader( type ) | |
gl.shaderSource( shader, source ) | |
gl.compileShader( shader ) | |
if ( gl.getShaderParameter (shader, gl.COMPILE_STATUS ) ) { | |
return shader | |
} else { | |
console.log( gl.getShaderInfoLog( shader ) ) | |
gl.deleteShader( shader ) | |
} | |
} | |
createProgram( vertex, fragment ) { | |
const gl = this.gl | |
const vertexShader = this.createShader( gl.VERTEX_SHADER, vertex ) | |
const fragmentShader = this.createShader( gl.FRAGMENT_SHADER, fragment ) | |
const program = gl.createProgram() | |
gl.attachShader( program, vertexShader ) | |
gl.attachShader( program, fragmentShader ) | |
gl.linkProgram( program ) | |
if ( gl.getProgramParameter( program, gl.LINK_STATUS ) ) { | |
gl.useProgram( program ) | |
this.program = program | |
} else { | |
console.log( gl.getProgramInfoLog( program ) ) | |
gl.deleteProgram( program ) | |
} | |
} | |
createUniforms( data ) { | |
const gl = this.gl | |
const uniforms = this.data.uniforms = data | |
const values = this.uniforms = {} | |
Object.keys( uniforms ).forEach( name => { | |
const uniform = uniforms[ name ] | |
uniform.location = gl.getUniformLocation( this.program, 'u_' + name ) | |
Object.defineProperty( values, name, { | |
set: value => { | |
uniforms[ name ].value = value | |
this.setUniform( name, value ) | |
}, | |
get: () => uniforms[ name ].value | |
} ) | |
} ) | |
} | |
setUniform( name, value ) { | |
const gl = this.gl | |
const uniform = this.data.uniforms[ name ] | |
uniform.value = value | |
switch ( uniform.type ) { | |
case 'int': { | |
gl.uniform1i( uniform.location, value ) | |
break | |
} | |
case 'float': { | |
gl.uniform1f( uniform.location, value ) | |
break | |
} | |
case 'vec2': { | |
gl.uniform2f( uniform.location, ...value ) | |
break | |
} | |
case 'vec3': { | |
gl.uniform3f( uniform.location, ...value ) | |
break | |
} | |
case 'vec4': { | |
gl.uniform4f( uniform.location, ...value ) | |
break | |
} | |
case 'mat2': { | |
gl.uniformMatrix2fv( uniform.location, false, value ) | |
break | |
} | |
case 'mat3': { | |
gl.uniformMatrix3fv( uniform.location, false, value ) | |
break | |
} | |
case 'mat4': { | |
gl.uniformMatrix4fv( uniform.location, false, value ) | |
break | |
} | |
} | |
// ivec2 : uniform2i, | |
// ivec3 : uniform3i, | |
// ivec4 : uniform4i, | |
// sampler2D : uniform1i, | |
// samplerCube : uniform1i, | |
// bool : uniform1i, | |
// bvec2 : uniform2i, | |
// bvec3 : uniform3i, | |
// bvec4 : uniform4i, | |
} | |
updateUniforms() { | |
const gl = this.gl | |
const uniforms = this.data.uniforms | |
Object.keys( uniforms ).forEach( name => { | |
const uniform = uniforms[ name ] | |
this.uniforms[ name ] = uniform.value | |
} ) | |
} | |
createBuffers( data ) { | |
const gl = this.gl | |
const buffers = this.data.buffers = data | |
const values = this.buffers = {} | |
Object.keys( buffers ).forEach( name => { | |
const buffer = buffers[ name ] | |
buffer.buffer = this.createBuffer( 'a_' + name, buffer.size ) | |
Object.defineProperty( values, name, { | |
set: data => { | |
buffers[ name ].data = data | |
this.setBuffer( name, data ) | |
if ( name == 'position' ) | |
this.count = buffers.position.data.length / 3 | |
}, | |
get: () => buffers[ name ].data | |
} ) | |
} ) | |
} | |
createBuffer( name, size ) { | |
const gl = this.gl | |
const program = this.program | |
const index = gl.getAttribLocation( program, name ) | |
const buffer = gl.createBuffer() | |
gl.bindBuffer( gl.ARRAY_BUFFER, buffer ) | |
gl.enableVertexAttribArray( index ) | |
gl.vertexAttribPointer( index, size, gl.FLOAT, false, 0, 0 ) | |
return buffer | |
} | |
setBuffer( name, data ) { | |
const gl = this.gl | |
const buffers = this.data.buffers | |
if ( name == null && ! gl.bindBuffer( gl.ARRAY_BUFFER, null ) ) return | |
gl.bindBuffer( gl.ARRAY_BUFFER, buffers[ name ].buffer ) | |
gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( data ), gl.STATIC_DRAW ) | |
} | |
updateBuffers() { | |
const gl = this.gl | |
const buffers = this.buffers | |
Object.keys( buffers ).forEach( name => | |
buffers[ name ] = buffer.data | |
) | |
this.setBuffer( null ) | |
} | |
createTexture( src ) { | |
const gl = this.gl | |
const texture = gl.createTexture() | |
gl.bindTexture( gl.TEXTURE_2D, texture ) | |
gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array( [ 0, 0, 0, 0 ] ) ) | |
this.texture = texture | |
if ( src ) { | |
this.uniforms.hasTexture = 1 | |
this.loadTexture( src ) | |
} | |
} | |
loadTexture( src ) { | |
const gl = this.gl | |
const texture = this.texture | |
const textureImage = new Image() | |
textureImage.onload = () => { | |
gl.bindTexture( gl.TEXTURE_2D, texture ) | |
gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureImage ) | |
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR ) | |
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR ) | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) | |
} | |
textureImage.src = src | |
} | |
update() { | |
const gl = this.gl | |
const now = performance.now() | |
const elapsed = ( now - this.time.start ) / 5000 | |
const delta = now - this.time.old | |
this.time.old = now | |
this.uniforms.time = elapsed | |
if ( this.count > 0 ) { | |
gl.clear( gl.COLORBUFFERBIT ) | |
gl.drawArrays( gl.POINTS, 0, this.count ) | |
} | |
this.onUpdate( delta ) | |
requestAnimationFrame( this.update ) | |
} | |
} | |
const snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAGAGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDAgNzkuMTYwNDUxLCAyMDE3LzA1LzA2LTAxOjA4OjIxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMTUtMDctMDNUMTg6NTk6MjIrMDI6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDE5LTAxLTEyVDE1OjE0OjQwKzAxOjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDE5LTAxLTEyVDE1OjE0OjQwKzAxOjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIzMzBlMWI0LTk5ZDctNGU2NS05MGQ2LTNmYjFiYmE2ZTE0MCIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjAyNThjNzMxLTQ4ZjQtYTA0MS1hNGFkLTQ4MTA2MTVjY2FlYSIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjJjY2VkMTUyLTRjNzAtNDFlZC1hMzcyLWRlOWY4NjgyZTcwMSI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MmNjZWQxNTItNGM3MC00MWVkLWEzNzItZGU5Zjg2ODJlNzAxIiBzdEV2dDp3aGVuPSIyMDE1LTA3LTAzVDE4OjU5OjIyKzAyOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoTWFjaW50b3NoKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6YjMzMGUxYjQtOTlkNy00ZTY1LTkwZDYtM2ZiMWJiYTZlMTQwIiBzdEV2dDp3aGVuPSIyMDE5LTAxLTEyVDE1OjE0OjQwKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoTWFjaW50b3NoKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz50mbqsAAAToElEQVR4nOVbW49dR5X+VlXtc2v3Ne52N6bTTnAc0u02GRJQwEhYQkJ5QhmhPPOUl0iBB/4AP4JfMA95IEIaJEYiQhlesCZMEE1iGxJHIjK21XbSwX1xd5+zd9Va87BrVdfefWwI4xEaUVJpn7PPPnVqfbUu31pVh0QE/8zN/KMn8I9uTl+8+uqrAAARgYgghIBer4dnnnkGAPDFL34RANDr9eiJJ55AVVUgIhhjwMwYDAbY3Nyk8+fPP/DHdnd3ZWtr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' | |
// const stats = new Stats() | |
// document.body.appendChild( stats.domElement ) | |
const holder = document.querySelector( '.snow' ) | |
const count = parseInt( holder.getAttribute( 'count' ) ) | |
let wind = { | |
current: 0, | |
force: 0.1, | |
target: 0.1, | |
min: 0.1, | |
max: 0.25, | |
easing: 0.005 | |
} | |
const snow = new Shader( holder, { | |
depthTest: false, | |
texture: snowflake, | |
uniforms: { | |
worldSize: { type: 'vec3', value: [ 0, 0, 0 ] }, | |
gravity: { type: 'float', value: 100 }, | |
wind:{ type: 'float', value: 0 }, | |
}, | |
buffers: { | |
size: { size: 1, data: [] }, | |
rotation: { size: 3, data: [] }, | |
speed: { size: 3, data: [] }, | |
}, | |
vertex: ` | |
precision highp float; | |
attribute vec4 a_position; | |
attribute vec4 a_color; | |
attribute vec3 a_rotation; | |
attribute vec3 a_speed; | |
attribute float a_size; | |
uniform float u_time; | |
uniform vec2 u_mousemove; | |
uniform vec2 u_resolution; | |
uniform mat4 u_projection; | |
uniform vec3 u_worldSize; | |
uniform float u_gravity; | |
uniform float u_wind; | |
varying vec4 v_color; | |
varying float v_rotation; | |
void main() { | |
v_color = a_color; | |
v_rotation = a_rotation.x + u_time * a_rotation.y; | |
vec3 pos = a_position.xyz; | |
pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x; | |
pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y; | |
pos.x += sin(u_time * a_speed.z) * a_rotation.z; | |
pos.z += cos(u_time * a_speed.z) * a_rotation.z; | |
gl_Position = u_projection * vec4( pos.xyz, a_position.w ); | |
gl_PointSize = ( a_size / gl_Position.w ) * 100.0; | |
}`, | |
fragment: ` | |
precision highp float; | |
uniform sampler2D u_texture; | |
varying vec4 v_color; | |
varying float v_rotation; | |
void main() { | |
vec2 rotated = vec2( | |
cos(v_rotation) * (gl_PointCoord.x - 0.5) + sin(v_rotation) * (gl_PointCoord.y - 0.5) + 0.5, | |
cos(v_rotation) * (gl_PointCoord.y - 0.5) - sin(v_rotation) * (gl_PointCoord.x - 0.5) + 0.5 | |
); | |
vec4 snowflake = texture2D(u_texture, rotated); | |
gl_FragColor = vec4(snowflake.rgb, snowflake.a * v_color.a); | |
}`, | |
onResize( w, h, dpi ) { | |
const position = [], color = [], size = [], rotation = [], speed = [] | |
// z in range from -80 to 80, camera distance is 100 | |
// max height at z of -80 is 110 | |
const height = 110 | |
const width = w / h * height | |
const depth = 80 | |
Array.from( { length: w / h * count }, snowflake => { | |
position.push( | |
-width + Math.random() * width * 2, | |
-height + Math.random() * height * 2, | |
Math.random() * depth * 2 | |
) | |
speed.push(// 0, 0, 0 ) | |
1 + Math.random(), | |
1 + Math.random(), | |
Math.random() * 10 | |
) // x, y, sinusoid | |
rotation.push( | |
Math.random() * 2 * Math.PI, | |
Math.random() * 20, | |
Math.random() * 10 | |
) // angle, speed, sinusoid | |
color.push( | |
1, | |
1, | |
1, | |
0.1 + Math.random() * 0.2 | |
) | |
size.push( | |
5 * Math.random() * 5 * ( h * dpi / 1000 ) | |
) | |
} ) | |
this.uniforms.worldSize = [ width, height, depth ] | |
this.buffers.position = position | |
this.buffers.color = color | |
this.buffers.rotation = rotation | |
this.buffers.size = size | |
this.buffers.speed = speed | |
}, | |
onUpdate( delta ) { | |
wind.force += ( wind.target - wind.force ) * wind.easing | |
wind.current += wind.force * ( delta * 0.2 ) | |
this.uniforms.wind = wind.current | |
if ( Math.random() > 0.995 ) { | |
wind.target = ( wind.min + Math.random() * ( wind.max - wind.min ) ) * ( Math.random() > 0.5 ? -1 : 1 ) | |
console.log( wind.target ) | |
} | |
// stats.update() | |
}, | |
} ) |
<script src="https://cdnjs.cloudflare.com/ajax/libs/stats.js/r16/Stats.min.js"></script> |
Snowfall made with native WebGL shader
A Pen by Boris Šehovac on CodePen.
html, body { height: 100%; } | |
body { margin: 0; background: #0F2027; background: linear-gradient(to bottom, #0F2027, #080e10); } | |
canvas { display: block; } | |
.snow { position: absolute; left: 0; top: 0; right: 0; bottom: 0; } |