Skip to content

Instantly share code, notes, and snippets.

@jordanranson
Created February 6, 2021 00:44
Show Gist options
  • Save jordanranson/cff865c40d5c00c4f227c8e714ce4eff to your computer and use it in GitHub Desktop.
Save jordanranson/cff865c40d5c00c4f227c8e714ce4eff to your computer and use it in GitHub Desktop.
2D WebGL Pixel Renderer (MIT License)
/* eslint-disable */
function paletteFromImage (src) {
return new Promise((resolve, reject) => {
const image = new Image()
image.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = 16
canvas.height = 16
const context = canvas.getContext('2d')
context.drawImage(image, 0, 0)
const imageData = context.getImageData(0, 0, 16, 16)
resolve(imageData.data)
}
image.onerror = (err) => reject(err)
image.src = src
})
}
const DrawApi = {
clear (color = 0) {
color = Math.round(color)
this.screenBuffer.fill(color)
},
pixel (x, y, color) {
x = Math.round(x)
y = Math.round(y)
color = Math.round(color)
this.setPixel(x, y, color)
},
noise (min = 0, max = 255) {
if (min instanceof Array) {
const colors = min
for (let i = 0; i < this.width * this.height; i++) {
this.screenBuffer[i] = Math.round(colors[Math.floor(Math.random() * colors.length)])
}
} else {
min = Math.round(min)
max = Math.round(max)
max += 1
for (let i = 0; i < this.width * this.height; i++) {
this.screenBuffer[i] = Math.floor(Math.random() * (max - min) + min)
}
}
},
rect (x0, y0, x1, y1, color) {
color = Math.round(color)
const xa = Math.round(Math.min(x0, x1))
const ya = Math.round(Math.min(y0, y1))
const xb = Math.round(Math.max(x0, x1))
const yb = Math.round(Math.max(y0, y1))
let w = xb - xa + 1
let h = yb - ya
this.setRow(xa, ya, w, color)
this.setRow(xa, yb, w, color)
while (h >= 0) {
this.setPixel(xa, ya + h, color)
this.setPixel(xb, ya + h, color)
h--
}
},
rectFill (x0, y0, x1, y1, color) {
color = Math.round(color)
const xa = Math.round(Math.min(x0, x1))
const ya = Math.round(Math.min(y0, y1))
const xb = Math.round(Math.max(x0, x1))
const yb = Math.round(Math.max(y0, y1))
let w = xb - xa + 1
let h = yb - ya
while (h >= 0) {
this.setRow(xa, ya + h, w, color)
h--
}
},
circ (cx, cy, radius, color) {
cx = Math.round(cx)
cy = Math.round(cy)
radius = Math.round(radius)
color = Math.round(color)
let x = radius
let y = 0
let radiusError = 1 - x
while (x >= y) {
this.setPixel(x + cx, y + cy, color)
this.setPixel(y + cx, x + cy, color)
this.setPixel(-x + cx, y + cy, color)
this.setPixel(-y + cx, x + cy, color)
this.setPixel(-x + cx, -y + cy, color)
this.setPixel(-y + cx, -x + cy, color)
this.setPixel(x + cx, -y + cy, color)
this.setPixel(y + cx, -x + cy, color)
y++
if (radiusError < 0) {
radiusError += 2 * y + 1
} else {
x--
radiusError += 2 * (y - x + 1)
}
}
},
circFill (cx, cy, radius, color) {
cx = Math.round(cx)
cy = Math.round(cy)
radius = Math.round(radius)
color = Math.round(color)
for (let y = - radius; y <= radius; y++)
for (let x = - radius; x <= radius; x++)
if (x * x + y * y <= radius * radius)
this.setPixel(cx + x, cy + y, color)
DrawApi.circ.call(this, cx, cy, radius, color)
},
line (x0, y0, x1, y1, color) {
x0 = Math.round(x0)
y0 = Math.round(y0)
x1 = Math.round(x1)
y1 = Math.round(y1)
color = Math.round(color)
const dx = Math.abs(x1 - x0)
const dy = Math.abs(y1 - y0)
const sx = (x0 < x1) ? 1 : -1
const sy = (y0 < y1) ? 1 : -1
let err = dx - dy
while (true) {
this.setPixel(x0, y0, color)
if ((x0 === x1) && (y0 === y1)) break
const e2 = 2 * err
if (e2 > -dy) {
err -= dy
x0 += sx
}
if (e2 < dx) {
err += dx
y0 += sy
}
}
}
}
class PixelRenderer {
get defaultVertexShader () {
return `
attribute vec2 aVertexPosition;
attribute vec2 aTextureCoord;
varying highp vec2 vTextureCoord;
void main() {
gl_Position = vec4(aVertexPosition, 0.0, 1.0);
vTextureCoord = aTextureCoord;
}
`
}
get defaultFragmentShader () {
return `
precision highp float;
varying highp vec2 vTextureCoord;
uniform sampler2D uScreenSampler;
uniform sampler2D uPaletteSampler;
void main () {
float i = floor(texture2D(uScreenSampler, vTextureCoord).a*255.0);
float x = mod(i, 16.0);
float y = floor(i/16.0);
vec4 color = texture2D(uPaletteSampler, vec2(x/15.0, y/15.0));
gl_FragColor = color;
}
`
}
get width () {
return this.glCanvas.width
}
get height () {
return this.glCanvas.height
}
get palette () {
return this.paletteCache
}
set palette (value) {
this.paletteCache = value
this.createPaletteBuffer()
}
constructor () {
this.vertexShader = this.defaultVertexShader;
this.fragmentShader = this.defaultFragmentShader;
this.handlers = []
this.animationFrameId = null
this.canvas = undefined
this.context = undefined
}
run () {
cancelAnimationFrame(this.animationFrameId)
const drawApi = Object.keys(DrawApi).reduce((acc, fnName) => {
acc[fnName] = DrawApi[fnName].bind(this)
return acc
}, {})
const renderLoop = () => {
this.animationFrameId = requestAnimationFrame(renderLoop)
for (const handler of this.handlers) handler.fn(drawApi, this.canvas, this.gl)
this.render()
}
renderLoop()
}
render () {
const { gl } = this
// Clear for redrawing
gl.clear(gl.COLOR_BUFFER_BIT)
// Update array buffers
this.setArrayBuffer(2, 'aVertexPosition', this.screenSurface.position)
this.setArrayBuffer(2, 'aTextureCoord', this.screenSurface.texture)
// Set shader uniforms
this.setUniform('1i', 'uScreenSampler', 0)
this.setUniform('1i', 'uPaletteSampler', 2)
// Bind the screen and palette textures
this.setScreenTexture(this.width, this.height, this.screenBuffer)
this.setPaletteTexture(16, 16, this.paletteBuffer)
// Draw surface
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
}
resize (width, height, scale) {
this.glCanvas.width = width
this.glCanvas.height = height
this.glCanvas.style.transform = `scale(${scale})`
this.glCanvas.style.imageRendering = `pixelated`
this.createScreenTexture()
this.gl.viewport(0, 0, width, height)
}
setPixel (x, y, color) {
if (x < 0 || y < 0 || x >= this.width || y >= this.height) return -1
const i = x + (y * this.width)
this.screenBuffer[i] = color
}
setRow (x, y, w, color) {
if (x < 0 || y < 0 || x + w >= this.width || y >= this.height) return -1
const i = x + (y * this.width)
this.screenBuffer.fill(color, i, i + w)
}
getPixel (x, y) {
const i = x + (y * this.width)
return this.screenBuffer[i]
}
// Texture
setTexture (texture, width, height, pixels, format) {
const { gl } = this
format = format || gl.ALPHA
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.texImage2D(gl.TEXTURE_2D, 0, format, width, height, 0, format, gl.UNSIGNED_BYTE, pixels)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
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)
}
// Screen
createScreenSurface () {
const position = this.createArrayBuffer([
-1.0, 1.0,
1.0, 1.0,
-1.0, -1.0,
1.0, -1.0
])
const texture = this.createArrayBuffer([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
1.0, 1.0
])
this.screenSurface = {
position,
texture
}
}
createScreenTexture () {
this.createScreenBuffer()
this.screenTexture = this.gl.createTexture()
}
createScreenBuffer () {
const length = this.width * this.height
const screenBuffer = new Uint8Array(length)
screenBuffer.fill(0)
this.screenBuffer = screenBuffer
}
setScreenTexture (width, height, pixels) {
const { gl } = this
gl.activeTexture(gl.TEXTURE0 + 0)
this.setTexture(this.screenTexture, width, height, pixels)
}
// Palette
createPaletteTexture () {
this.createPaletteBuffer()
this.paletteTexture = this.gl.createTexture()
}
createPaletteBuffer () {
const paletteBuffer = new Uint8Array(256 * 4)
paletteBuffer.set(this.palette)
this.paletteBuffer = paletteBuffer
}
setPaletteTexture (width, height, pixels) {
const { gl } = this
gl.activeTexture(gl.TEXTURE0 + 2)
this.setTexture(this.paletteTexture, width, height, pixels, gl.RGBA)
}
// Shaders
buildShaderProgram () {
const { gl } = this
const program = gl.createProgram()
let shader
shader = this.compileShader(gl.VERTEX_SHADER, this.vertexShader)
if (shader) gl.attachShader(program, shader)
shader = this.compileShader(gl.FRAGMENT_SHADER, this.fragmentShader)
if (shader) gl.attachShader(program, shader)
gl.linkProgram(program)
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Error linking shader program:')
console.error(gl.getProgramInfoLog(program))
}
gl.useProgram(program)
this.shaderProgram = program
}
compileShader (type, source) {
const { gl } = this
const shader = gl.createShader(type)
gl.shaderSource(shader, source)
gl.compileShader(shader)
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(`Error compiling ${type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:`)
console.error(gl.getShaderInfoLog(shader))
}
return shader
}
// Array buffers
createArrayBuffer (array) {
const { gl } = this
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(array), gl.STATIC_DRAW)
return buffer
}
setArrayBuffer (numComponents, locationName, buffer) {
const { gl } = this
const location = gl.getAttribLocation(this.shaderProgram, locationName)
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.vertexAttribPointer(location, numComponents, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(location)
}
// Uniforms
getUniformLocation (key) {
return this.gl.getUniformLocation(this.shaderProgram, key)
}
setUniform (type, key, value) {
this.gl[`uniform${type}`](this.getUniformLocation(key), value)
}
}
const renderer = new PixelRenderer()
export async function enablePixelRenderer(glCanvas, options = {
paletteSrc: './palette.png',
scale: 3
}) {
if (options.vertexShader) renderer.vertexShader = options.vertexShader
if (options.fragmentShader) renderer.fragmentShader = options.fragmentShader
renderer.handlers = []
renderer.paletteCache = await paletteFromImage(options.paletteSrc)
renderer.glCanvas = glCanvas
renderer.gl = glCanvas.getContext('webgl')
renderer.gl.clearColor(1.0, 1.0, 1.0, 1.0)
renderer.buildShaderProgram()
renderer.createScreenSurface()
renderer.createPaletteTexture()
renderer.resize(glCanvas.width, glCanvas.height, options.scale)
renderer.run()
}
export function usePixelRenderer(handler, options) {
if (handler) {
renderer.handlers.push({
fn: handler,
options
})
renderer.handlers.sort(function (a, b) {
const aOrder = (a.options && typeof a.options.drawOrder === 'number')
? a.options.drawOrder
: 0
const bOrder = (b.options && typeof b.options.drawOrder === 'number')
? b.options.drawOrder
: 0
return aOrder - bOrder
})
}
return [
renderer.context,
renderer.canvas
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment