Last active
July 5, 2025 14:00
-
-
Save bigmistqke/efcaa9e8dd64425c69480bf0e0baa94c to your computer and use it in GitHub Desktop.
view.gl
This file contains hidden or 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
import type { | |
AttributeKind, | |
AttributeMethods, | |
AttributeSchema, | |
BufferSchema, | |
FramebufferSchema, | |
GL, | |
InferAttributeView, | |
InferBuffers, | |
InferFramebuffers, | |
InferInterleavedAttributes, | |
InferTextures, | |
InferUniforms as InferUniformView, | |
InterleavedAttributeSchema, | |
RemoveSuffix, | |
TexImage2DOptions, | |
TextureParameters, | |
TextureSchema, | |
UniformSchema, | |
View, | |
ViewSchema, | |
} from './types.ts' | |
import { createTexture } from './utils.ts' | |
function mapObject<T extends Record<string, any>, TReturn>( | |
value: T, | |
callback: (value: T[keyof T], key: Extract<keyof T, string>, index: number) => TReturn, | |
): { [TKey in keyof T]: TReturn } { | |
return Object.fromEntries( | |
Object.entries(value).map(([key, value], index) => [key, callback(value, key, index)]), | |
) | |
} | |
function assertedNotNullish<T>(value: T, message?: string): NonNullable<T> { | |
if (value === undefined || value === null) throw new Error(message) | |
return value | |
} | |
/**********************************************************************************/ | |
/* */ | |
/* Constants */ | |
/* */ | |
/**********************************************************************************/ | |
const SIZE_MAP = { | |
'1f': 1, | |
'2f': 2, | |
'3f': 3, | |
'4f': 4, | |
'1i': 1, | |
'2i': 2, | |
'3i': 3, | |
'4i': 4, | |
mat3: 9, | |
mat4: 16, | |
} as const | |
const FRAMEBUFFER_ATTACHMENT_MAP = { | |
color: 'COLOR_ATTACHMENT0', | |
depth: 'DEPTH_ATTACHMENT', | |
stencil: 'STENCIL_ATTACHMENT', | |
depthStencil: 'DEPTH_STENCIL_ATTACHMENT', | |
} as const | |
/**********************************************************************************/ | |
/* */ | |
/* Features */ | |
/* */ | |
/**********************************************************************************/ | |
const INSTANCED_ARRAYS_WRAPPER_MAP = new WeakMap< | |
GL, | |
RemoveSuffix< | |
Pick< | |
ANGLE_instanced_arrays, | |
'drawArraysInstancedANGLE' | 'drawElementsInstancedANGLE' | 'vertexAttribDivisorANGLE' | |
>, | |
'ANGLE' | |
> | null | |
>() | |
function getInstancedArrays(gl: GL) { | |
if (gl instanceof WebGL2RenderingContext) return gl | |
const cached = INSTANCED_ARRAYS_WRAPPER_MAP.get(gl) | |
if (cached) return cached | |
const ext = gl.getExtension('ANGLE_instanced_arrays') | |
if (!ext) return undefined | |
const wrapper = { | |
drawArraysInstanced: ext.drawArraysInstancedANGLE.bind(ext), | |
drawElementsInstanced: ext.drawElementsInstancedANGLE.bind(ext), | |
vertexAttribDivisor: ext.vertexAttribDivisorANGLE.bind(ext), | |
} | |
INSTANCED_ARRAYS_WRAPPER_MAP.set(gl, wrapper) | |
return wrapper | |
} | |
const VERTEX_ARRAY_OBJECT_WRAPPER_MAP = new WeakMap< | |
GL, | |
RemoveSuffix< | |
Pick< | |
OES_vertex_array_object, | |
'bindVertexArrayOES' | 'createVertexArrayOES' | 'deleteVertexArrayOES' | |
>, | |
'OES' | |
> | null | |
>() | |
function getVertexArrayObject(gl: GL) { | |
if (gl instanceof WebGL2RenderingContext) return gl | |
const cached = VERTEX_ARRAY_OBJECT_WRAPPER_MAP.get(gl) | |
if (cached) return cached | |
const ext = gl.getExtension('OES_vertex_array_object') | |
if (!ext) return null | |
const wrapper = { | |
bindVertexArray: ext.bindVertexArrayOES.bind(ext), | |
createVertexArray: ext.createVertexArrayOES.bind(ext), | |
deleteVertexArray: ext.deleteVertexArrayOES.bind(ext), | |
} | |
VERTEX_ARRAY_OBJECT_WRAPPER_MAP.set(gl, wrapper) | |
return wrapper | |
} | |
/**********************************************************************************/ | |
/* */ | |
/* View */ | |
/* */ | |
/**********************************************************************************/ | |
export function view<T extends ViewSchema>(gl: GL, program: WebGLProgram, config: T): View<T> { | |
const uniforms = !config.uniforms ? undefined : uniformView(gl, program, config.uniforms) | |
const [attributes, disposeAttributes] = !config.attributes | |
? [] | |
: attributeView(gl, program, config.attributes) | |
const [interleavedAttributes, disposeInterleavedAttributes] = !config.interleavedAttributes | |
? [] | |
: interleavedAttributeView(gl, program, config.interleavedAttributes) | |
const [buffers, disposeBuffers] = !config.buffers ? [] : bufferView(gl, config.buffers) | |
const [framebuffers, disposeFramebuffers] = !config.framebuffers | |
? [] | |
: framebufferView(gl, config.framebuffers) | |
const [textures, disposeTextures] = !config.textures ? [] : textureView(gl, config.textures) | |
return [ | |
{ uniforms, attributes, interleavedAttributes, buffers, framebuffers, textures }, | |
function dispose() { | |
disposeAttributes?.() | |
disposeInterleavedAttributes?.() | |
disposeBuffers?.() | |
disposeFramebuffers?.() | |
disposeTextures?.() | |
}, | |
] as View<T> | |
} | |
/**********************************************************************************/ | |
/* */ | |
/* Uniform View */ | |
/* */ | |
/**********************************************************************************/ | |
export function uniformView<T extends UniformSchema>(gl: GL, program: WebGLProgram, config: T) { | |
return mapObject(config, (kind, name) => { | |
const location = assertedNotNullish(gl.getUniformLocation(program, name)) | |
return { | |
set(...args: any[]) { | |
if (kind.startsWith('mat')) { | |
gl[`uniformMatrix${kind.replace('mat', '')}fv`](location, false, args[0]) | |
} | |
if (kind.startsWith('sampler')) { | |
gl.uniform1i(location, args[0]) | |
} else { | |
// @ts-expect-error | |
gl[`uniform${kind}`](location, ...args) | |
} | |
}, | |
} | |
}) as InferUniformView<T> | |
} | |
/**********************************************************************************/ | |
/* */ | |
/* Attribute View */ | |
/* */ | |
/**********************************************************************************/ | |
// Shared attribute helper functions | |
// between attributeView and interleavedAttributeView | |
function handleAttribute( | |
gl: GL, | |
location: number, | |
size: number, | |
stride: number, | |
offset: number, | |
kind: AttributeKind, | |
instanced?: boolean, | |
) { | |
gl.enableVertexAttribArray(location) | |
gl.vertexAttribPointer( | |
location, | |
size, | |
gl[kind.endsWith('i') ? 'INT' : 'FLOAT'], | |
false, | |
stride, | |
offset, | |
) | |
if (instanced) { | |
// Get instanced-arrays-feature: extension if webgl, gl if webgl2 | |
assertedNotNullish(getInstancedArrays(gl)).vertexAttribDivisor(location, 1) | |
} | |
} | |
export function attributeView<T extends AttributeSchema>(gl: GL, program: WebGLProgram, config: T) { | |
const attributes = mapObject(config, ({ kind, instanced }, name): AttributeMethods => { | |
const location = gl.getAttribLocation(program, name) | |
if (location < 0) { | |
throw new Error(`Attribute '${name}' not found`) | |
} | |
const buffer = assertedNotNullish(gl.createBuffer()) | |
const size = SIZE_MAP[kind] | |
return { | |
bind() { | |
gl.bindBuffer(gl.ARRAY_BUFFER, buffer) | |
handleAttribute(gl, location, size, 0, 0, kind, instanced) | |
}, | |
dispose() { | |
gl.deleteBuffer(buffer) | |
}, | |
set(data, usage = 'STATIC_DRAW') { | |
gl.bindBuffer(gl.ARRAY_BUFFER, buffer) | |
gl.bufferData(gl.ARRAY_BUFFER, data, gl[usage]) | |
return this | |
}, | |
} | |
}) | |
return [ | |
attributes, | |
function dispose() { | |
for (const name in attributes) { | |
attributes[name].dispose() | |
} | |
}, | |
] as InferAttributeView<T> | |
} | |
/**********************************************************************************/ | |
/* */ | |
/* Interleaved Attribute View */ | |
/* */ | |
/**********************************************************************************/ | |
export function interleavedAttributeView<T extends InterleavedAttributeSchema>( | |
gl: GL, | |
program: WebGLProgram, | |
schema: T, | |
) { | |
// Initialize interleaved attributes | |
const interleavedAttributes = mapObject(schema, ({ layout, instanced }) => { | |
// Increment number to keep track of offset | |
let index = 0 | |
// Calculate layout information | |
const handles = layout.map(layout => { | |
const location = gl.getAttribLocation(program, layout.name) | |
if (location < 0) { | |
throw new Error(`Attribute '${layout.name}' not found`) | |
} | |
const size = SIZE_MAP[layout.kind] | |
const offset = index | |
index += size * 4 | |
return () => handleAttribute(gl, location, size, stride, offset, layout.kind, instanced) | |
}) | |
// Set stride to final index | |
const stride = index | |
// Create a buffer | |
const buffer = assertedNotNullish(gl.createBuffer()) | |
// Create VAO to cache attribute state | |
let vao: { bind(): void; dispose(): void } | undefined = undefined | |
// Get VAO-feature: extension if webgl1, gl if webgl2 | |
const feature = getVertexArrayObject(gl) | |
if (feature) { | |
const vertexArray = feature.createVertexArray() | |
feature.bindVertexArray(vertexArray) | |
gl.bindBuffer(gl.ARRAY_BUFFER, buffer) | |
for (const handle of handles) { | |
handle() | |
} | |
feature.bindVertexArray(null) | |
vao = { | |
dispose() { | |
feature.deleteVertexArray(vertexArray) | |
}, | |
bind() { | |
feature.bindVertexArray(vertexArray) | |
}, | |
} | |
} | |
return { | |
bind() { | |
if (vao) { | |
vao.bind() | |
} else { | |
// Fallback: manual attribute setup | |
gl.bindBuffer(gl.ARRAY_BUFFER, buffer) | |
for (const handle of handles) { | |
handle() | |
} | |
} | |
}, | |
dispose() { | |
gl.deleteBuffer(buffer) | |
if (vao) { | |
vao.dispose() | |
} | |
}, | |
set(value, usage = 'STATIC_DRAW') { | |
gl.bindBuffer(gl.ARRAY_BUFFER, buffer) | |
gl.bufferData(gl.ARRAY_BUFFER, value, gl[usage]) | |
}, | |
} | |
}) | |
return [ | |
interleavedAttributes, | |
function dispose() { | |
for (const name in interleavedAttributes) { | |
interleavedAttributes[name].dispose() | |
} | |
}, | |
] as InferInterleavedAttributes<T> | |
} | |
/**********************************************************************************/ | |
/* */ | |
/* Buffer View */ | |
/* */ | |
/**********************************************************************************/ | |
export function bufferView<T extends BufferSchema>(gl: GL, config: T) { | |
// Initialize buffers | |
const buffers = mapObject(config, ({ target = 'ARRAY_BUFFER', usage = 'STATIC_DRAW' }) => { | |
const buffer = assertedNotNullish(gl.createBuffer()) | |
return { | |
bind() { | |
gl.bindBuffer(gl[target], buffer) | |
}, | |
dispose() { | |
gl.deleteBuffer(buffer) | |
}, | |
set(data) { | |
gl.bindBuffer(gl[target], buffer) | |
gl.bufferData(gl[target], data, gl[usage]) | |
}, | |
} | |
}) | |
return [ | |
buffers, | |
function dispose() { | |
for (const name in buffers) { | |
buffers[name].dispose() | |
} | |
}, | |
] as InferBuffers<T> | |
} | |
/**********************************************************************************/ | |
/* */ | |
/* Framebuffer View */ | |
/* */ | |
/**********************************************************************************/ | |
class FramebufferError extends Error { | |
constructor(gl: GL, name: string, status: number) { | |
let errorMessage = `Framebuffer '${name}' not complete. Status: ` | |
switch (status) { | |
case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: | |
errorMessage += 'FRAMEBUFFER_INCOMPLETE_ATTACHMENT' | |
break | |
case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: | |
errorMessage += 'FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT' | |
break | |
case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: | |
errorMessage += 'FRAMEBUFFER_INCOMPLETE_DIMENSIONS' | |
break | |
case gl.FRAMEBUFFER_UNSUPPORTED: | |
errorMessage += 'FRAMEBUFFER_UNSUPPORTED' | |
break | |
default: | |
errorMessage += `Unknown (${status})` | |
} | |
super(errorMessage) | |
} | |
} | |
export function framebufferView<T extends FramebufferSchema>(gl: GL, config: T) { | |
if (!config) return [] as InferFramebuffers<T> | |
// Initialize framebuffers | |
const framebuffers = mapObject(config, ({ attachment, ...options }, name) => { | |
// Create framebuffer | |
const framebuffer = assertedNotNullish( | |
gl.createFramebuffer(), | |
`Failed to create framebuffer: ${name}`, | |
) | |
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer) | |
let texture: WebGLTexture | |
try { | |
// Create texture for the framebuffer | |
texture = createTexture(gl, options) | |
} catch { | |
throw new Error(`Failed to create texture: ${name}`) | |
} | |
gl.framebufferTexture2D( | |
gl.FRAMEBUFFER, | |
// Determine attachment point based on attachment kind | |
gl[FRAMEBUFFER_ATTACHMENT_MAP[attachment]], | |
gl.TEXTURE_2D, | |
texture, | |
0, | |
) | |
// Check framebuffer completeness | |
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER) | |
if (status !== gl.FRAMEBUFFER_COMPLETE) { | |
throw new FramebufferError(gl, name, status) | |
} | |
return { | |
bind() { | |
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer) | |
}, | |
dispose() { | |
gl.deleteFramebuffer(framebuffer) | |
gl.deleteTexture(texture) | |
}, | |
} | |
}) | |
// Restore default framebuffer | |
gl.bindFramebuffer(gl.FRAMEBUFFER, null) | |
return [ | |
framebuffers, | |
function dispose() { | |
for (const name in framebuffers) { | |
framebuffers[name].dispose() | |
} | |
}, | |
] as InferFramebuffers<T> | |
} | |
/**********************************************************************************/ | |
/* */ | |
/* Texture View */ | |
/* */ | |
/**********************************************************************************/ | |
export function textureView<T extends TextureSchema>(gl: GL, schema: T) { | |
const textures = mapObject(schema, ({ target = 'TEXTURE_2D' }, name) => { | |
const texture = assertedNotNullish(gl.createTexture(), `Failed to create texture '${name}'`) | |
return { | |
bind(unit = 0) { | |
gl.activeTexture(gl.TEXTURE0 + unit) | |
gl.bindTexture(gl[target], texture) | |
}, | |
dispose() { | |
gl.deleteTexture(texture) | |
}, | |
set( | |
source: ImageBufferSource | null, | |
{ | |
level = 0, | |
internalFormat = 'RGBA', | |
width = 1, | |
height = 1, | |
border = 0, | |
format = 'RGBA', | |
type = 'UNSIGNED_BYTE', | |
}: Partial<TexImage2DOptions> = {}, | |
) { | |
gl.bindTexture(gl[target], texture) | |
if (!source) { | |
gl.texImage2D( | |
gl[target], | |
level, | |
gl[internalFormat], | |
width, | |
height, | |
border, | |
gl[format], | |
gl[type], | |
null, | |
) | |
return | |
} | |
if ( | |
source instanceof ImageBitmap || | |
source instanceof HTMLImageElement || | |
source instanceof HTMLCanvasElement || | |
source instanceof HTMLVideoElement | |
) { | |
gl.texImage2D(gl[target], level, gl[internalFormat], gl[format], gl[type], source) | |
return | |
} | |
throw new Error(`Unsupported image source for texture '${name}'`) | |
}, | |
parameters(params: TextureParameters) { | |
gl.bindTexture(gl[target], texture) | |
for (const [pname, value] of Object.entries(params)) { | |
gl.texParameteri(gl[target], gl[pname], gl[value]) | |
} | |
}, | |
} | |
}) | |
return [ | |
textures, | |
function dispose() { | |
for (const name in textures) { | |
textures[name].dispose() | |
} | |
}, | |
] as InferTextures<T> | |
} |
This file contains hidden or 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
export type GL = WebGLRenderingContext | WebGL2RenderingContext; | |
/**********************************************************************************/ | |
/* */ | |
/* Constants */ | |
/* */ | |
/**********************************************************************************/ | |
type GLTarget = 'ARRAY_BUFFER' | 'ELEMENT_ARRAY_BUFFER'; | |
type GLUsage = 'STATIC_DRAW' | 'DYNAMIC_DRAW' | 'STREAM_DRAW'; | |
type GLTextureTarget = 'TEXTURE_2D' | 'TEXTURE_CUBE_MAP'; | |
type GLTextureFormat = | |
| 'RGBA' | |
| 'RGB' | |
| 'RGBA32F' | |
| 'RGB32F' | |
| 'RGBA16F' | |
| 'RGB16F'; | |
type GLTextureType = 'UNSIGNED_BYTE' | 'FLOAT' | 'HALF_FLOAT'; | |
type GLTextureFilter = 'NEAREST' | 'LINEAR'; | |
type GLTextureWrap = 'CLAMP_TO_EDGE' | 'REPEAT' | 'MIRRORED_REPEAT'; | |
/**********************************************************************************/ | |
/* */ | |
/* Uniform */ | |
/* */ | |
/**********************************************************************************/ | |
export type UniformKind = | |
| '1f' | |
| '2f' | |
| '3f' | |
| '4f' | |
| '1i' | |
| '2i' | |
| '3i' | |
| '4i' | |
| 'mat3' | |
| 'mat4'; | |
interface UniformKindMap { | |
'1f': [number]; | |
'2f': [number, number]; | |
'3f': [number, number, number]; | |
'4f': [number, number, number, number]; | |
'1i': [number]; | |
'2i': [number, number]; | |
'3i': [number, number, number]; | |
'4i': [number, number, number, number]; | |
mat3: | |
| [Float32Array] | |
| [number, number, number, number, number, number, number, number, number]; | |
mat4: [ | |
| Float32Array | |
| [ | |
number, | |
number, | |
number, | |
number, | |
number, | |
number, | |
number, | |
number, | |
number, | |
number, | |
number, | |
number, | |
number, | |
number, | |
number, | |
number | |
] | |
]; | |
} | |
export interface UniformMethods<TKind extends UniformKind> { | |
set(...args: UniformKindMap[TKind]): void; | |
} | |
export type InferUniforms< | |
T extends UniformConfig | undefined = Record<string, any> | |
> = T extends UniformConfig | |
? { | |
[K in keyof T]: UniformMethods<T[K]>; | |
} | |
: undefined; | |
/**********************************************************************************/ | |
/* */ | |
/* Attributes */ | |
/* */ | |
/**********************************************************************************/ | |
export type AttributeKind = '1f' | '2f' | '3f' | '4f'; | |
export interface AttributeSchema { | |
location: number; | |
size: number; | |
stride: number; | |
offset: number; | |
instanced?: boolean; | |
} | |
export interface AttributeMethods { | |
buffer: WebGLBuffer; | |
bind(): void; | |
set(data: Float32Array, usage?: GLUsage): AttributeMethods; | |
} | |
export type InferAttributes< | |
T extends AttributeConfig | undefined = Record<string, any> | |
> = T extends AttributeConfig | |
? { | |
dispose(): void; | |
methods: { | |
[K in keyof T]: AttributeMethods; | |
}; | |
} | |
: undefined; | |
/**********************************************************************************/ | |
/* */ | |
/* Interleaved Attributes */ | |
/* */ | |
/**********************************************************************************/ | |
export interface InterleavedAttributeOptions { | |
name: string; | |
kind: AttributeKind; | |
} | |
export interface InterleavedAttributeConfig { | |
layout: InterleavedAttributeOptions[]; | |
instanced?: boolean; | |
} | |
export interface InterleavedAttributeMethods { | |
vao?: WebGLVertexArrayObject; | |
buffer: WebGLBuffer; | |
bind(): void; | |
set(data: Float32Array, usage?: GLUsage): InterleavedAttributeMethods; | |
} | |
export type InferInterleavedAttributes< | |
T extends Record<string, InterleavedAttributeConfig> | undefined = Record< | |
string, | |
any | |
> | |
> = | |
T extends Record<string, InterleavedAttributeConfig> | |
? { | |
dispose(): void; | |
methods: { | |
[K in keyof T]: InterleavedAttributeMethods; | |
}; | |
} | |
: undefined; | |
/**********************************************************************************/ | |
/* */ | |
/* Buffers */ | |
/* */ | |
/**********************************************************************************/ | |
export interface BufferMethods { | |
set(data: Float32Array | Uint16Array | Uint32Array): BufferMethods; | |
buffer: WebGLBuffer; | |
bind(): void; | |
} | |
export type InferBuffers< | |
T extends BufferConfig | undefined = Record<string, any> | |
> = T extends BufferConfig | |
? { | |
dispose(): void; | |
methods: { | |
[K in keyof T]: BufferMethods; | |
}; | |
} | |
: undefined; | |
/**********************************************************************************/ | |
/* */ | |
/* Framebuffers */ | |
/* */ | |
/**********************************************************************************/ | |
export interface FramebufferOptions extends Omit<TextureOptions, 'data'> { | |
attachment: 'color' | 'depth' | 'stencil' | 'depthStencil'; | |
} | |
export interface TextureOptions { | |
target?: GLTextureTarget; | |
width: number; | |
height: number; | |
internalFormat?: GLTextureFormat; | |
format?: GLTextureFormat; | |
type?: GLTextureType; | |
minFilter?: GLTextureFilter; | |
magFilter?: GLTextureFilter; | |
wrapS?: GLTextureWrap; | |
wrapT?: GLTextureWrap; | |
data?: ArrayBufferView | null; | |
} | |
export interface FramebufferMethods { | |
framebuffer: WebGLFramebuffer; | |
texture: WebGLTexture; | |
bind(): void; | |
} | |
export type InferFramebuffers< | |
T extends FramebufferConfig | undefined = Record<string, any> | |
> = T extends FramebufferConfig | |
? { | |
dispose(): void; | |
methods: { | |
[K in keyof T]: FramebufferMethods; | |
}; | |
} | |
: undefined; | |
export interface BufferOptions { | |
target?: GLTarget; | |
usage?: GLUsage; | |
} | |
/**********************************************************************************/ | |
/* */ | |
/* Config */ | |
/* */ | |
/**********************************************************************************/ | |
export interface ShaderConfig { | |
gl: GL; | |
vertex: string; | |
fragment: string; | |
} | |
export type UniformConfig = Record<string, UniformKind>; | |
export interface AttributeOptions { | |
kind: AttributeKind; | |
instanced?: boolean; | |
} | |
export type AttributeConfig = Record<string, AttributeOptions>; | |
export type BufferConfig = Record<string, BufferOptions>; | |
export type TextureConfig = Record<string, TextureOptions>; | |
export type FramebufferConfig = Record<string, FramebufferOptions>; | |
export interface ProgramConfig { | |
uniforms?: UniformConfig; | |
attributes?: AttributeConfig; | |
interleavedAttributes?: Record<string, InterleavedAttributeConfig>; | |
buffers?: BufferConfig; | |
textures?: TextureConfig; | |
framebuffers?: FramebufferConfig; | |
} | |
/**********************************************************************************/ | |
/* */ | |
/* Program State */ | |
/* */ | |
/**********************************************************************************/ | |
export type ProgramState = | |
| { | |
gl: WebGLRenderingContext; | |
isWebGL2: false; | |
program: WebGLProgram; | |
instancedArraysExt: ANGLE_instanced_arrays | null; | |
} | |
| { | |
gl: WebGL2RenderingContext; | |
isWebGL2: true; | |
program: WebGLProgram; | |
instancedArraysExt?: never; | |
}; | |
/**********************************************************************************/ | |
/* */ | |
/* Program */ | |
/* */ | |
/**********************************************************************************/ | |
export interface Program<T extends ProgramConfig = ProgramConfig> | |
extends Resources<T> { | |
dispose(): void; | |
drawArrays(mode: number, first: number, count: number): void; | |
drawArraysInstanced( | |
mode: number, | |
first: number, | |
count: number, | |
instanceCount: number | |
): void; | |
drawElements(mode: number, count: number, type: number, offset: number): void; | |
drawElementsInstanced( | |
mode: number, | |
count: number, | |
type: number, | |
offset: number, | |
instanceCount: number | |
): void; | |
use(): void; | |
} | |
export interface Resources<T extends ProgramConfig = ProgramConfig> { | |
attributes: PickMaybe<InferAttributes<T['attributes']>, 'methods'>; | |
interleavedAttributes: PickMaybe< | |
InferInterleavedAttributes<T['interleavedAttributes']>, | |
'methods' | |
>; | |
buffers: PickMaybe<InferBuffers<T['buffers']>, 'methods'>; | |
framebuffers: PickMaybe<InferFramebuffers<T['framebuffers']>, 'methods'>; | |
uniforms: InferUniforms<T['uniforms']>; | |
} | |
type PickMaybe< | |
T extends Record<string, any> | undefined, | |
U extends keyof T | (string & {}) | |
> = T extends Record<string, any> ? T[U] : undefined; |
This file contains hidden or 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
import { GL } from './types' | |
function createGLShader(gl: GL, type: number, source: string): WebGLShader { | |
const shader = gl.createShader(type) | |
if (!shader) throw new Error('Failed to create shader') | |
gl.shaderSource(shader, source) | |
gl.compileShader(shader) | |
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { | |
const info = gl.getShaderInfoLog(shader) | |
gl.deleteShader(shader) | |
throw new Error( | |
`Failed to compile ${type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader: ${info}`, | |
) | |
} | |
return shader | |
} | |
export function createGLProgram( | |
gl: GL, | |
vertexSource: string, | |
fragmentSource: string, | |
): WebGLProgram { | |
const program = gl.createProgram() | |
if (!program) throw new Error('Failed to create WebGL program') | |
const vertexShader = createGLShader(gl, gl.VERTEX_SHADER, vertexSource) | |
const fragmentShader = createGLShader(gl, gl.FRAGMENT_SHADER, fragmentSource) | |
gl.attachShader(program, vertexShader) | |
gl.attachShader(program, fragmentShader) | |
gl.linkProgram(program) | |
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { | |
const info = gl.getProgramInfoLog(program) | |
gl.deleteProgram(program) | |
gl.deleteShader(vertexShader) | |
gl.deleteShader(fragmentShader) | |
throw new Error(`Failed to link program: ${info}`) | |
} | |
// Clean up shaders after linking | |
gl.deleteShader(vertexShader) | |
gl.deleteShader(fragmentShader) | |
return program | |
} | |
export function createTexture( | |
gl: GL, | |
{ | |
target = 'TEXTURE_2D', | |
internalFormat = 'RGBA', | |
format = 'RGBA', | |
type = 'UNSIGNED_BYTE', | |
minFilter = 'NEAREST', | |
magFilter = 'NEAREST', | |
wrapS = 'CLAMP_TO_EDGE', | |
wrapT = 'CLAMP_TO_EDGE', | |
width, | |
height, | |
}: TextureOptions, | |
data?: ArrayBufferView | null, | |
): WebGLTexture { | |
const texture = gl.createTexture() | |
function getTextureConstant(name: string) { | |
if (!(name in gl)) { | |
throw new Error(`Attempted to create webgl2-only texture (${name}) in webgl1`) | |
} | |
return gl[name] | |
} | |
gl.bindTexture(gl[target], texture) | |
gl.texImage2D( | |
gl[target], | |
0, | |
getTextureConstant(internalFormat), | |
width, | |
height, | |
0, | |
getTextureConstant(format), | |
getTextureConstant(type), | |
data ?? data ?? null, | |
) | |
// Set texture parameters | |
gl.texParameteri(gl[target], gl.TEXTURE_MIN_FILTER, gl[minFilter]) | |
gl.texParameteri(gl[target], gl.TEXTURE_MAG_FILTER, gl[magFilter]) | |
gl.texParameteri(gl[target], gl.TEXTURE_WRAP_S, gl[wrapS]) | |
gl.texParameteri(gl[target], gl.TEXTURE_WRAP_T, gl[wrapT]) | |
return texture | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment