A Pen by Boris Šehovac on CodePen.
Created
October 23, 2024 01:26
-
-
Save trvswgnr/408dbfc4ac7c9f807266a98eecefab9a to your computer and use it in GitHub Desktop.
WebGL Shader
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ShaderProgram { | |
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 ) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment