A minimal but powerful WebGL2 framework designed for creative coding, visualizations, and rapid WebGL prototyping.
- Lightweight and optimized WebGL2 wrapper for modern graphics programming
- Simplified shader creation with automatic uniform parsing and type detection
- Transform Feedback support for GPU-based simulations and particle systems
- Framebuffer abstraction with mipmap support for post-processing effects
- Multiple Render Targets (MRT) for deferred rendering and G-Buffers
- Uniform Buffer Objects (UBO) for efficient uniform management
- Particle system capabilities with ping-pong buffer optimization
- Audio synthesis with GPU acceleration
- Automatic texture unit management
Include the module in your project:
<script type="module">
import { chottoGL } from './esChottoGL.js';
// Your code here
</script>import { chottoGL } from './esChottoGL.js';
// Initialize canvas
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Initialize chottoGL
const chotto = chottoGL(canvas);
// Create a shader with custom fragment shader
// Boilerplate (#version, precision, vTexCoord, fragColor) is auto-inserted
const shader = chotto.createShader({
fragment: `
uniform float uTime;
void main() {
vec2 uv = vTexCoord * 2.0 - 1.0;
float radius = length(uv);
float angle = atan(uv.y, uv.x);
float r = 0.5 + 0.5 * sin(angle * 3.0 + uTime);
float g = 0.5 + 0.5 * sin(radius * 5.0 - uTime * 0.5);
float b = 0.5 + 0.5 * sin(radius * 10.0 + angle + uTime * 2.0);
fragColor = vec4(r, g, b, 1.0);
}`,
});
// Animation loop
let time = 0;
function render() {
time += 0.01;
chotto.clear(0.1, 0.1, 0.1, 1.0);
shader.use().setUniform('uTime', time).draw();
requestAnimationFrame(render);
}
render();const chotto = chottoGL(canvas, options);canvas: HTMLCanvasElement to render tooptions: (Optional) WebGL context options
const shader = chotto.createShader({
vertex: '...', // Optional, uses default if omitted
fragment: '...', // Custom fragment shader
transformFeedbackVaryings: [...], // Optional for transform feedback
transformFeedbackMode: chotto.gl.SEPARATE_ATTRIBS, // Optional mode
raw: false // Set to true to disable auto-insertion of boilerplate
});Shader Boilerplate Auto-Insertion:
Fragment shaders automatically get the following boilerplate prepended:
#version 300 es
precision highp float;
in vec2 vTexCoord;
out vec4 fragColor;This means you can write minimal shaders like:
const shader = chotto.createShader({
fragment: `
uniform float uTime;
void main() {
fragColor = vec4(vTexCoord, sin(uTime), 1.0);
}`
});To disable auto-insertion:
- Use
raw: trueoption - Or start your shader with
#versionfor full control
shader.use()
.setUniform('uniformName', value) // Supports float, int, bool, vectors, matrices
.setUniform({ uniform1: value1, uniform2: value2 }) // Batch setting
.setTexture('textureName', textureObject) // Automatic texture unit management
.draw(); // Draw fullscreen quad
// Custom geometry (particles, meshes, etc.)
shader.draw(vao, gl.POINTS, count); // Draw with custom VAO, mode, and countUniform Type Support:
- Scalars:
float,int,bool - Vectors:
vec2,vec3,vec4,ivec2,ivec3,ivec4 - Matrices:
mat2,mat3,mat4 - Samplers:
sampler2D,sampler3D,samplerCube,sampler2DArray - Arrays: All types support array notation
- Flexible input: Accepts scalars, arrays, or typed arrays
- Boolean conversion:
true→1,false→0
const fbo = chotto.createFramebuffer(width, height, data, options);
// Options include mipmap support
const fboWithMipmap = chotto.createFramebuffer(width, height, null, {
mipmap: true,
minFilter: chotto.gl.LINEAR_MIPMAP_LINEAR
});
// Using the framebuffer
fbo.bind().clear(0, 0, 0, 1);
// Draw operations...
fbo.unbind();
// Update texture data
fbo.updateTexture(xOffset, yOffset, width, height, data, updateMipmap);
// Resize framebuffer
fbo.resize(newWidth, newHeight);
// Generate mipmaps
fbo.updateMipmap();For deferred rendering, G-Buffers, or any effect requiring multiple outputs:
// Numeric: N targets with same format
const gbuffer = chotto.createFramebuffer(width, height, null, {
targets: 3,
depth: true
});
// Array: per-target format configuration
const gbuffer = chotto.createFramebuffer(width, height, null, {
targets: [
{ internalFormat: gl.RGBA, format: gl.RGBA, type: gl.UNSIGNED_BYTE },
{ internalFormat: gl.RGBA16F, format: gl.RGBA, type: gl.FLOAT },
{ internalFormat: gl.RGBA16F, format: gl.RGBA, type: gl.FLOAT }
]
});
// Access textures
gbuffer.textures[0] // First target
gbuffer.textures[1] // Second target
gbuffer.texture // Alias for textures[0]
gbuffer.targetCount // Number of targetsFragment shader for MRT (start with #version 300 es to skip auto-insertion):
#version 300 es
precision highp float;
in vec2 vTexCoord;
layout(location = 0) out vec4 gAlbedo;
layout(location = 1) out vec4 gNormal;
layout(location = 2) out vec4 gPosition;
void main() {
gAlbedo = vec4(1.0, 0.0, 0.0, 1.0);
gNormal = vec4(0.0, 1.0, 0.0, 1.0);
gPosition = vec4(vTexCoord, 0.0, 1.0);
}Important: Always call fbo.unbind() before sampling from MRT textures to avoid feedback loops.
// Particle buffer with ping-pong for GPU particle simulation
const particles = chotto.createParticleBuffer([positions, velocities]);
// Process in update loop (auto-swaps buffers)
particles.process(updateShader, () => {
updateShader.setUniform('uTime', time);
});
// Render particles
particles.draw(renderShader, { uSize: 5.0 });
// Audio buffer for GPU audio synthesis
const audio = chotto.createAudioBuffer(2048, 2); // size, channels
audio.process(audioShader, () => {
audioShader.setUniform('u_sampleRate', 44100);
});
// Read back audio data
audio.readBack(outputBuffer);Create a simple animated shader:
import { chottoGL } from './esChottoGL.js';
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const chotto = chottoGL(canvas);
const shader = chotto.createShader({
fragment: `
uniform float uTime;
void main() {
vec2 uv = vTexCoord * 2.0 - 1.0;
float radius = length(uv);
float angle = atan(uv.y, uv.x);
float r = 0.5 + 0.5 * sin(angle * 3.0 + uTime);
float g = 0.5 + 0.5 * sin(radius * 5.0 - uTime * 0.5);
float b = 0.5 + 0.5 * sin(radius * 10.0 + angle + uTime * 2.0);
fragColor = vec4(r, g, b, 1.0);
}`,
});
let time = 0;
function render() {
time += 0.01;
chotto.clear(0.1, 0.1, 0.1, 1.0);
shader.use().setUniform('uTime', time).draw();
requestAnimationFrame(render);
}
render();Create a scene and apply post-processing effects:
import { chottoGL } from './esChottoGL.js';
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const chotto = chottoGL(canvas);
// Scene rendering shader
const sceneShader = chotto.createShader({
fragment: `
uniform float uTime;
uniform vec2 resolution;
void main() {
vec2 uv = vTexCoord * 2.0 - 1.0;
uv.x *= resolution.x / resolution.y;
vec3 color = vec3(0.5 + 0.5 * sin(uv.x * 10.0 + uTime),
0.5 + 0.5 * sin(uv.y * 10.0 + uTime * 0.7),
0.5 + 0.5 * sin((uv.x + uv.y) * 5.0 + uTime * 1.3));
fragColor = vec4(color, 1.0);
}`
});
// Post-processing shader
const postShader = chotto.createShader({
fragment: `
uniform sampler2D uTexture;
uniform float uTime;
void main() {
vec2 uv = vTexCoord;
float distortion = sin(uv.y * 40.0 + uTime * 2.0) * 0.003;
vec2 distortedUV = vec2(uv.x + distortion, uv.y);
vec3 color = texture(uTexture, distortedUV).rgb;
float vignette = 1.0 - smoothstep(0.5, 0.8, length(uv - 0.5));
color *= vignette;
fragColor = vec4(color, 1.0);
}`
});
// Create framebuffer for scene rendering
const fbo = chotto.createFramebuffer(canvas.width, canvas.height);
let time = 0;
function render() {
time += 0.01;
// Render scene to framebuffer
fbo.bind().clear(0.0, 0.0, 0.0, 1.0);
sceneShader.use()
.setUniform('uTime', time)
.setUniform('resolution', [canvas.width, canvas.height])
.draw();
// Render framebuffer to screen with post-processing
fbo.unbind();
chotto.clear(0.0, 0.0, 0.0, 1.0);
postShader.use()
.setUniform('uTime', time)
.setTexture('uTexture', fbo.texture)
.draw();
requestAnimationFrame(render);
}
render();Create a GPU-based particle system using Transform Feedback:
import { chottoGL } from './esChottoGL.js';
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const chotto = chottoGL(canvas);
const PARTICLE_COUNT = 10000;
// Generate initial particle data
function generateParticles() {
const positions = new Float32Array(PARTICLE_COUNT * 4);
const velocities = new Float32Array(PARTICLE_COUNT * 4);
for (let i = 0; i < PARTICLE_COUNT; i++) {
const i4 = i * 4;
// Position: random circular distribution
const angle = Math.random() * Math.PI * 2;
const radius = 0.1 + Math.random() * 0.3;
positions[i4 + 0] = Math.cos(angle) * radius;
positions[i4 + 1] = Math.sin(angle) * radius;
positions[i4 + 2] = 0.0;
positions[i4 + 3] = Math.random(); // Lifetime
// Velocity: slightly toward center
const vAngle = angle + Math.PI + (Math.random() - 0.5) * 1.0;
const vMag = 0.001 + Math.random() * 0.002;
velocities[i4 + 0] = Math.cos(vAngle) * vMag;
velocities[i4 + 1] = Math.sin(vAngle) * vMag;
velocities[i4 + 2] = 0.0;
velocities[i4 + 3] = 0.003 + Math.random() * 0.008; // Decay rate
}
return { positions, velocities };
}
const particles = generateParticles();
// Update shader for particle simulation
const updateShader = chotto.createShader({
vertex: `#version 300 es
precision highp float;
// Input attributes (current state)
in vec4 aPosition; // (x, y, z, lifetime)
in vec4 aVelocity; // (vx, vy, vz, decay)
// Output varying (captured by Transform Feedback)
out vec4 vPosition;
out vec4 vVelocity;
// Simulation parameters
uniform float uTime;
// Simulation logic...
void main() {
// Current position and velocity
vec3 position = aPosition.xyz;
float life = aPosition.w;
vec3 velocity = aVelocity.xyz;
float decay = aVelocity.w;
// Update position
position += velocity;
// Update lifetime
life -= decay;
// Reset particle if lifetime expired
if (life <= 0.0) {
float angle = fract(sin(float(gl_VertexID) * 78.233) * 43758.5453 + uTime) * 6.283;
float radius = 0.1 + fract(cos(float(gl_VertexID) * 10.873) * 13758.5453) * 0.3;
position.x = cos(angle) * radius;
position.y = sin(angle) * radius;
position.z = 0.0;
// Reset lifetime
life = 0.8 + fract(sin(float(gl_VertexID) * 32.373) * 13758.5453) * 0.4;
}
// Output for Transform Feedback
vPosition = vec4(position, life);
vVelocity = vec4(velocity, decay);
// Required but not used
gl_Position = vec4(position, 1.0);
}
`,
fragment: `#version 300 es
precision highp float;
out vec4 fragColor;
void main() { fragColor = vec4(0.0); }
`,
transformFeedbackVaryings: ['vPosition', 'vVelocity'],
transformFeedbackMode: chotto.gl.SEPARATE_ATTRIBS
});
// Render shader for particle visualization
const renderShader = chotto.createShader({
vertex: `#version 300 es
precision highp float;
in vec4 aPosition;
out float vLife;
uniform mat4 uProjection;
uniform float uParticleSize;
void main() {
vLife = aPosition.w; // Pass lifetime to fragment shader
gl_Position = uProjection * vec4(aPosition.xyz, 1.0);
gl_PointSize = uParticleSize * vLife;
}
`,
fragment: `#version 300 es
precision highp float;
in float vLife;
out vec4 fragColor;
uniform vec3 uParticleColor;
void main() {
// Create circular particles
vec2 coord = gl_PointCoord - vec2(0.5);
float d = length(coord);
float alpha = smoothstep(0.5, 0.35, d);
if (alpha < 0.01) discard;
vec3 color = uParticleColor;
fragColor = vec4(color, 1.0);
}
`
});
// Create particle buffer for ping-pong simulation
const tfPair = chotto.createParticleBuffer([particles.positions, particles.velocities]);
// Create projection matrix
function createProjectionMatrix() {
const aspect = canvas.width / canvas.height;
const scaleX = 1.0 / aspect;
const scaleY = 1.0;
return new Float32Array([
scaleX, 0, 0, 0,
0, scaleY, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]);
}
// Animation settings
let time = 0;
const particleSize = 5.0;
const particleColor = { r: 1.0, g: 1.0, b: 1.0 };
// Render loop
function render(timestamp) {
time = timestamp * 0.001;
chotto.clear(0.0, 0.0, 0.0, 1.0);
// Update particles with transform feedback
tfPair.process(updateShader, () => {
updateShader.setUniform('uTime', time);
// Set other uniforms as needed
});
// Render updated particles
tfPair.draw(renderShader, {
uProjection: createProjectionMatrix(),
uParticleSize: particleSize,
uParticleColor: [particleColor.r, particleColor.g, particleColor.b]
});
requestAnimationFrame(render);
}
requestAnimationFrame(render);Generate audio in real-time using WebGL:
import { chottoGL } from './esChottoGL.js';
const glCanvas = document.getElementById('glCanvas');
const chotto = chottoGL(glCanvas);
// Constants
const SAMPLE_RATE = 44100;
const BUFFER_SIZE = 2048;
// Create audio shader
const soundShader = chotto.createShader({
vertex: `#version 300 es
// Uniforms
uniform float u_sampleRate;
uniform float u_blockOffset;
uniform float u_bpm;
// Outputs (transform feedback varyings)
out vec2 o_sound;
// Utility functions
float sine(float phase) {
return sin(phase * 6.28318530718);
}
float saw(float phase) {
return 2.0 * fract(phase) - 1.0;
}
float square(float phase) {
return fract(phase) < 0.5 ? 1.0 : -1.0;
}
// Sound generating functions
float kick(float time) {
float amp = exp(-5.0 * time);
float phase = 50.0 * time - 10.0 * exp(-70.0 * time);
return amp * sine(phase);
}
float hihat(float time) {
return exp(-30.0 * time) * (2.0 * fract(747.0 * time) - 1.0);
}
// Main sound generator
vec2 mainSound(float time) {
float beat = time / 60.0 * u_bpm;
float kickTime = mod(beat, 1.0) / (u_bpm / 60.0);
float hihatTime = mod(beat, 0.25) / (u_bpm / 60.0);
vec2 out = vec2(0.0);
out += vec2(kick(kickTime));
out += vec2(0.7, 1.3) * vec2(hihat(hihatTime) * 0.5);
return out;
}
void main() {
float time = u_blockOffset + float(gl_VertexID) / u_sampleRate;
o_sound = mainSound(time);
}
`,
fragment: `#version 300 es
precision highp float;
out vec4 fragColor;
void main() { fragColor = vec4(1.0); }
`,
transformFeedbackVaryings: ['o_sound'],
transformFeedbackMode: chotto.gl.SEPARATE_ATTRIBS
});
// Create audio buffer for GPU synthesis
const audioTF = chotto.createAudioBuffer(BUFFER_SIZE, 2);
// Setup Web Audio API
let audioContext, gainNode, analyser;
const audioBuffer = new Float32Array(BUFFER_SIZE * 2);
async function setupAudio() {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
gainNode = audioContext.createGain();
analyser = audioContext.createAnalyser();
analyser.connect(gainNode);
gainNode.connect(audioContext.destination);
// Create audio worklet
await audioContext.audioWorklet.addModule('path-to-audio-processor.js');
const workletNode = new AudioWorkletNode(audioContext, 'glsl-sound-processor');
workletNode.port.onmessage = (event) => {
if (event.data.type === 'needBuffer') {
generateAudioBuffer(currentBlockOffset);
currentBlockOffset += BUFFER_SIZE / SAMPLE_RATE;
workletNode.port.postMessage({
type: 'buffer',
buffer: audioBuffer.slice()
}, [audioBuffer.slice().buffer]);
}
};
workletNode.connect(analyser);
}
// Generate audio data using WebGL
function generateAudioBuffer(blockOffset) {
audioTF.process(soundShader, () => {
soundShader.use()
.setUniform('u_sampleRate', SAMPLE_RATE)
.setUniform('u_blockOffset', blockOffset)
.setUniform('u_bpm', 120);
});
audioTF.readBack(audioBuffer);
}
// UI and startup code...UBOs provide efficient uniform management for complex shaders with many uniforms:
const ubo = chotto.createUniformBuffer('LightBlock', 0, {
'lightPosition': { type: 'vec3', value: [0, 5, 10] },
'lightColor': { type: 'vec3', value: [1, 0.9, 0.8] },
'lightIntensity': { type: 'float', value: 1.0 }
});
// Link to shader
ubo.linkToShader(shader.program);
// Update values (more efficient than individual setUniform calls)
ubo.update({
'lightPosition': { value: [10, 5, 3] },
'lightIntensity': { value: 0.8 }
});Supported UBO types: float, vec2, vec3, vec4
Automatic memory layout: Handles std140 alignment and padding automatically
const texture = chotto.loadTexture('path/to/image.jpg', {
minFilter: chotto.gl.LINEAR_MIPMAP_LINEAR,
magFilter: chotto.gl.LINEAR,
wrapS: chotto.gl.REPEAT,
wrapT: chotto.gl.REPEAT,
generateMipmap: true // Default is true
});
// Disable mipmap generation
const textureNoMipmap = chotto.loadTexture('image.jpg', {
generateMipmap: false,
minFilter: chotto.gl.LINEAR
});Texture options:
internalFormat: Defaultchotto.gl.RGBAformat: Defaultchotto.gl.RGBAtype: Defaultchotto.gl.UNSIGNED_BYTEminFilter: Defaultchotto.gl.LINEARmagFilter: Defaultchotto.gl.LINEARwrapS: Defaultchotto.gl.CLAMP_TO_EDGEwrapT: Defaultchotto.gl.CLAMP_TO_EDGEgenerateMipmap: Defaulttrue
chottoGL requires browsers with WebGL2 support, which includes:
- Chrome 56+
- Firefox 51+
- Safari 14.1+
- Edge 79+
MIT License