Skip to content

Instantly share code, notes, and snippets.

@mildsunrise
Last active September 30, 2023 09:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mildsunrise/d21cec18ce1709b0e73ebce3bfdb1760 to your computer and use it in GitHub Desktop.
Save mildsunrise/d21cec18ce1709b0e73ebce3bfdb1760 to your computer and use it in GitHub Desktop.
use WebGL to apply a perspective transform to an image
function assertNotNull<T>(x: T | null, message?: string): T {
if (x === null)
throw Error(message ?? 'unexpected null')
return x
}
const vertexShader = `
precision mediump float;
attribute vec2 position;
void main() {
gl_Position = vec4(position, 1, 1);
}
`
const fragmentShader = `
precision mediump float;
uniform mat3 matrix;
uniform sampler2D texture;
void main() {
gl_FragColor = texture2DProj(texture, matrix * vec3(gl_FragCoord));
}
`
/**
* future improvements: allow initializing a mipmapped texture,
* allow setting different vertexes
*/
export default class Perpsective {
/** creates the necessary resources (canvas, context, shaders, program, VBO) */
constructor(
/** output canvas (default: create) */
public canvas = document.createElement('canvas'),
/** WebGL context to use (default: create) */
public ctx = assertNotNull(canvas.getContext('webgl'), 'WebGL not available'),
) {
// prepare program
this.program = this.compileProgram(vertexShader, fragmentShader)
this.posId = assertNotNull(this.ctx.getAttribLocation(this.program, 'position'))
this.matrixId = assertNotNull(this.ctx.getUniformLocation(this.program, 'matrix'))
this.textureId = assertNotNull(this.ctx.getUniformLocation(this.program, 'texture'))
// prepare VBO
const vertexes = [
[-1,-1], [-1,+1], [+1,+1],
[-1,-1], [+1,-1], [+1,+1],
]
this.vbo = assertNotNull(this.ctx.createBuffer())
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.vbo)
this.ctx.bufferData(this.ctx.ARRAY_BUFFER, Float32Array.from(vertexes.flat()), this.ctx.STATIC_DRAW)
// prepare texture
this.texture = assertNotNull(this.ctx.createTexture(), 'could not create texture')
}
protected vbo: WebGLBuffer
protected program: WebGLProgram
protected posId: number
protected matrixId: WebGLUniformLocation
protected textureId: WebGLUniformLocation
protected texture: WebGLTexture
/**
* prepare the context for drawing. this method must be called before
* before setMatrix() or setImage(). this isn't called automatically by
* the constructor to enable more advanced use cases where the context
* is reused for other things.
*/
init() {
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.vbo)
this.ctx.vertexAttribPointer(this.posId, 2, this.ctx.FLOAT, false, 0, 0)
this.ctx.enableVertexAttribArray(this.posId)
this.ctx.useProgram(this.program)
this.ctx.uniform1i(this.textureId, 0)
this.ctx.activeTexture(this.ctx.TEXTURE0)
this.ctx.bindTexture(this.ctx.TEXTURE_2D, this.texture)
}
/**
* sets the size of the output canvas (can be skipped in advanced use
* cases if you take care of that yourself)
*/
setViewport(width: number, height: number) {
this.canvas.width = width
this.canvas.height = height
this.ctx.viewport(0, 0, width, height)
}
/**
* uploads the supplied image into a texture for rendering.
*/
setImage(
image: TexImageSource,
/** format and type of the uploaded texture */
format = this.ctx.RGBA, type = this.ctx.UNSIGNED_BYTE,
/** interpolation (must be LINEAR or NEAREST) */
interpolation = this.ctx.LINEAR,
) {
const target = this.ctx.TEXTURE_2D
this.ctx.bindTexture(target, this.texture)
this.ctx.texImage2D(target, 0, format, format, type, image)
this.ctx.texParameteri(target, this.ctx.TEXTURE_MAG_FILTER, interpolation)
// we need to disable mipmaps for the texture to render if it's not a power of 2
this.ctx.texParameteri(target, this.ctx.TEXTURE_MIN_FILTER, interpolation)
this.ctx.texParameteri(target, this.ctx.TEXTURE_WRAP_S, this.ctx.CLAMP_TO_EDGE)
this.ctx.texParameteri(target, this.ctx.TEXTURE_WRAP_T, this.ctx.CLAMP_TO_EDGE)
}
/**
* sets the 3x3 perspective matrix (column-by-column).
* this matrix is applied the fragment coordinates (i.e. output (x, y) in pixels)
* and returns the normalized coordinates (i.e. input image (x, y) from 0 to 1) to sample.
*/
setMatrix(matrix: Iterable<number>) {
this.ctx.uniformMatrix3fv(this.matrixId, false, matrix)
}
/** renders the output. requires setMatrix(), setImage() and setViewport() to be called first */
draw() {
// this.ctx.clear(this.ctx.COLOR_BUFFER_BIT)
this.ctx.drawArrays(this.ctx.TRIANGLES, 0, 6)
}
// WebGL generic utils
protected compileShader(type: number, source: string) {
const shader = assertNotNull(
this.ctx.createShader(type),
'failed to create shader')
this.ctx.shaderSource(shader, source)
this.ctx.compileShader(shader)
if (!this.ctx.getShaderParameter(shader, this.ctx.COMPILE_STATUS)) {
const info = this.ctx.getShaderInfoLog(shader)
throw Error(`could not compile WebGL shader:\n${info}`)
}
return shader
}
protected linkProgram(...shaders: WebGLShader[]) {
const program = assertNotNull(this.ctx.createProgram())
shaders.forEach(shader => this.ctx.attachShader(program, shader))
this.ctx.linkProgram(program)
if (!this.ctx.getProgramParameter(program, this.ctx.LINK_STATUS)) {
const info = this.ctx.getProgramInfoLog(program)
throw Error(`could not compile WebGL program:\n${info}`)
}
return program
}
protected compileProgram(vertexShader: string, fragmentShader: string) {
const vertex = this.compileShader(this.ctx.VERTEX_SHADER, vertexShader)
const fragment = this.compileShader(this.ctx.FRAGMENT_SHADER, fragmentShader)
return this.linkProgram(vertex, fragment)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment