webgl fallback
This commit is contained in:
11
index.html
11
index.html
@@ -83,7 +83,14 @@
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
max-width: min(90vw, 720px);
|
||||
max-height: 76vh;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 2rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255,255,255,0.15) transparent;
|
||||
}
|
||||
#scene-content::-webkit-scrollbar { width: 5px; }
|
||||
#scene-content::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }
|
||||
#scene-title {
|
||||
font-size: clamp(1.6rem, 5vw, 3rem);
|
||||
margin: 0;
|
||||
@@ -478,8 +485,8 @@
|
||||
</div>
|
||||
<div id="fallback">
|
||||
<div>
|
||||
<h2>WebGPU not available</h2>
|
||||
<p>Open this page in a recent Chrome/Edge (or any WebGPU-enabled browser).</p>
|
||||
<h2>Graphics not available</h2>
|
||||
<p>Your browser doesn't support WebGPU or WebGL. Open this page in a recent Chrome, Edge, or Firefox.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
|
||||
10
src/app.ts
10
src/app.ts
@@ -1,4 +1,6 @@
|
||||
import { Renderer, UiState } from './engine/renderer'
|
||||
import { GlRenderer } from './engine/gl-renderer'
|
||||
import type { RendererLike } from './engine/types'
|
||||
import { Input } from './ui/input'
|
||||
import { DomOverlay } from './ui/overlay'
|
||||
import { Router, Route } from './router'
|
||||
@@ -9,7 +11,7 @@ const DPR_CAP = 2
|
||||
|
||||
export class App {
|
||||
private canvas: HTMLCanvasElement
|
||||
private renderer = new Renderer()
|
||||
private renderer: RendererLike = new Renderer()
|
||||
private input!: Input
|
||||
private overlay!: DomOverlay
|
||||
private router = new Router()
|
||||
@@ -47,7 +49,11 @@ export class App {
|
||||
this.router.replace(Router.sceneToBasePath(this.scene))
|
||||
|
||||
this.resize()
|
||||
const ok = await this.renderer.init(this.canvas)
|
||||
let ok = await this.renderer.init(this.canvas)
|
||||
if (!ok) {
|
||||
this.renderer = new GlRenderer()
|
||||
ok = await this.renderer.init(this.canvas)
|
||||
}
|
||||
if (!ok) return false
|
||||
this.startTime = performance.now()
|
||||
this.running = true
|
||||
|
||||
127
src/engine/gl-renderer.ts
Normal file
127
src/engine/gl-renderer.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import glslSource from './shaders/fullscreen.glsl?raw'
|
||||
import type { UiState } from './renderer'
|
||||
import type { RendererLike } from './types'
|
||||
|
||||
const UNIFORM_FLOATS = 48
|
||||
const UNIFORM_BYTES = UNIFORM_FLOATS * 4
|
||||
|
||||
function compileShader(gl: WebGL2RenderingContext, type: number, source: string): WebGLShader | null {
|
||||
const shader = gl.createShader(type)
|
||||
if (!shader) return null
|
||||
gl.shaderSource(shader, source)
|
||||
gl.compileShader(shader)
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
console.error('GL shader compile error:', gl.getShaderInfoLog(shader))
|
||||
gl.deleteShader(shader)
|
||||
return null
|
||||
}
|
||||
return shader
|
||||
}
|
||||
|
||||
function linkProgram(gl: WebGL2RenderingContext, vs: WebGLShader, fs: WebGLShader): WebGLProgram | null {
|
||||
const program = gl.createProgram()
|
||||
if (!program) return null
|
||||
gl.attachShader(program, vs)
|
||||
gl.attachShader(program, fs)
|
||||
gl.linkProgram(program)
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||
console.error('GL program link error:', gl.getProgramInfoLog(program))
|
||||
gl.deleteProgram(program)
|
||||
return null
|
||||
}
|
||||
return program
|
||||
}
|
||||
|
||||
function splitShaderSource(source: string): { vs: string; fs: string } {
|
||||
const parts = source.split('//---FRAGMENT---')
|
||||
if (parts.length !== 2) throw new Error('GLSL source must contain //---VERTEX--- and //---FRAGMENT--- markers')
|
||||
const vs = parts[0].replace('//---VERTEX---', '').trim()
|
||||
const fs = parts[1].trim()
|
||||
return { vs, fs }
|
||||
}
|
||||
|
||||
export class GlRenderer implements RendererLike {
|
||||
private gl: WebGL2RenderingContext | null = null
|
||||
private program: WebGLProgram | null = null
|
||||
private ubo: WebGLBuffer | null = null
|
||||
private uniformData = new Float32Array(UNIFORM_FLOATS)
|
||||
private _ready = false
|
||||
|
||||
get ready(): boolean {
|
||||
return this._ready
|
||||
}
|
||||
|
||||
async init(canvas: HTMLCanvasElement): Promise<boolean> {
|
||||
const gl = canvas.getContext('webgl2', {
|
||||
alpha: false,
|
||||
antialias: false,
|
||||
depth: false,
|
||||
stencil: false,
|
||||
preserveDrawingBuffer: false,
|
||||
})
|
||||
if (!gl) return false
|
||||
|
||||
this.gl = gl
|
||||
|
||||
const { vs: vsSrc, fs: fsSrc } = splitShaderSource(glslSource)
|
||||
|
||||
const vs = compileShader(gl, gl.VERTEX_SHADER, vsSrc)
|
||||
if (!vs) return false
|
||||
|
||||
const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsSrc)
|
||||
if (!fs) {
|
||||
gl.deleteShader(vs)
|
||||
return false
|
||||
}
|
||||
|
||||
const program = linkProgram(gl, vs, fs)
|
||||
gl.deleteShader(vs)
|
||||
gl.deleteShader(fs)
|
||||
if (!program) return false
|
||||
|
||||
this.program = program
|
||||
|
||||
const ubo = gl.createBuffer()
|
||||
if (!ubo) return false
|
||||
gl.bindBuffer(gl.UNIFORM_BUFFER, ubo)
|
||||
gl.bufferData(gl.UNIFORM_BUFFER, UNIFORM_BYTES, gl.DYNAMIC_DRAW)
|
||||
|
||||
const blockIndex = gl.getUniformBlockIndex(program, 'Uniforms')
|
||||
if (blockIndex === gl.INVALID_INDEX) {
|
||||
console.error('Uniform block "Uniforms" not found in GLSL program')
|
||||
return false
|
||||
}
|
||||
gl.uniformBlockBinding(program, blockIndex, 0)
|
||||
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, ubo)
|
||||
|
||||
this.ubo = ubo
|
||||
this._ready = true
|
||||
return true
|
||||
}
|
||||
|
||||
resize(): void {
|
||||
if (!this.gl) return
|
||||
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height)
|
||||
}
|
||||
|
||||
render(state: UiState): void {
|
||||
const gl = this.gl
|
||||
if (!gl || !this.ubo || !this._ready) return
|
||||
|
||||
const d = this.uniformData
|
||||
d[0] = state.resolution[0]
|
||||
d[1] = state.resolution[1]
|
||||
d[2] = state.time
|
||||
d[3] = state.scene
|
||||
d[4] = state.mouse[0]
|
||||
d[5] = state.mouse[1]
|
||||
d[8] = state.dim
|
||||
d.set(state.stemMultipliers, 16)
|
||||
|
||||
gl.bindBuffer(gl.UNIFORM_BUFFER, this.ubo)
|
||||
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, this.uniformData)
|
||||
|
||||
gl.useProgram(this.program)
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 3)
|
||||
}
|
||||
}
|
||||
110
src/engine/shaders/fullscreen.glsl
Normal file
110
src/engine/shaders/fullscreen.glsl
Normal file
@@ -0,0 +1,110 @@
|
||||
//---VERTEX---
|
||||
#version 300 es
|
||||
|
||||
const vec2 POSITIONS[3] = vec2[](
|
||||
vec2(-1.0, -1.0),
|
||||
vec2(-1.0, 3.0),
|
||||
vec2( 3.0, -1.0)
|
||||
);
|
||||
|
||||
out vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vec2 pos = POSITIONS[gl_VertexID];
|
||||
gl_Position = vec4(pos, 0.0, 1.0);
|
||||
vUv = pos;
|
||||
}
|
||||
|
||||
//---FRAGMENT---
|
||||
#version 300 es
|
||||
precision highp float;
|
||||
|
||||
layout(std140) uniform Uniforms {
|
||||
vec2 resolution;
|
||||
float time;
|
||||
float scene;
|
||||
vec2 mouse;
|
||||
vec4 date;
|
||||
vec4 sampleRate;
|
||||
vec4 stemMultipliers[8];
|
||||
};
|
||||
|
||||
in vec2 vUv;
|
||||
out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
vec2 fragCoord = (vUv * 0.5 + 0.5) * resolution;
|
||||
vec2 uv = (fragCoord - 0.5 * resolution) / resolution.y;
|
||||
|
||||
float slowTime = time * 0.2;
|
||||
|
||||
vec3 bgGrey = vec3(0.11, 0.11, 0.12);
|
||||
vec3 cardGrey = vec3(0.30, 0.32, 0.36);
|
||||
vec3 coreWhite = vec3(0.90, 0.95, 0.90);
|
||||
|
||||
vec3 scene0 = vec3(0.20, 0.75, 0.20);
|
||||
vec3 scene1 = vec3(0.85, 0.55, 0.20);
|
||||
vec3 scene2 = vec3(0.25, 0.60, 0.95);
|
||||
vec3 scene3 = vec3(0.90, 0.35, 0.55);
|
||||
|
||||
int sFloor = int(clamp(floor(scene), 0.0, 3.0));
|
||||
int sCeil = int(clamp(ceil(scene), 0.0, 3.0));
|
||||
|
||||
vec3 c0;
|
||||
if (sFloor == 0) c0 = scene0;
|
||||
else if (sFloor == 1) c0 = scene1;
|
||||
else if (sFloor == 2) c0 = scene2;
|
||||
else c0 = scene3;
|
||||
|
||||
vec3 c1;
|
||||
if (sCeil == 0) c1 = scene0;
|
||||
else if (sCeil == 1) c1 = scene1;
|
||||
else if (sCeil == 2) c1 = scene2;
|
||||
else c1 = scene3;
|
||||
|
||||
float tBlend = scene - floor(scene);
|
||||
float smoothT = tBlend * tBlend * (3.0 - 2.0 * tBlend);
|
||||
vec3 sceneAccent = mix(c0, c1, smoothT);
|
||||
|
||||
vec2 mousePos = (mouse - 0.5 * resolution) / resolution.y;
|
||||
float distMouse = length(uv - mousePos);
|
||||
float influence = exp(-distMouse * 7.0);
|
||||
uv += (mousePos - uv) * influence * 0.12;
|
||||
|
||||
float angle = atan(uv.y, uv.x);
|
||||
float radius = length(uv);
|
||||
|
||||
float baseSpiral = angle + (radius * 8.0) - slowTime;
|
||||
float ripple = sin(radius * 15.0 - (time * 0.5)) * 0.08;
|
||||
float distortedSpiral = baseSpiral + ripple;
|
||||
|
||||
float armCount = 2.0;
|
||||
float rChannel = smoothstep(0.05, 0.55, abs(sin((distortedSpiral + 0.04) * armCount)));
|
||||
float gChannel = smoothstep(0.05, 0.55, abs(sin(distortedSpiral * armCount)));
|
||||
float bChannel = smoothstep(0.05, 0.55, abs(sin((distortedSpiral - 0.04) * armCount)));
|
||||
vec3 patternChannels = vec3(rChannel, gChannel, bChannel);
|
||||
|
||||
vec3 coreNeon = vec3(0.02 / (patternChannels + 0.025));
|
||||
vec3 spiralColor = mix(cardGrey, sceneAccent, patternChannels.g);
|
||||
vec3 primarySpiral = coreNeon * spiralColor;
|
||||
|
||||
float ringWave = sin(radius * 12.0 - (time * 1.5));
|
||||
float sharpRing = smoothstep(0.85, 0.95, ringWave);
|
||||
float ringMask = sharpRing * smoothstep(0.7, 0.1, radius);
|
||||
vec3 secondaryRings = cardGrey * ringMask * 0.4;
|
||||
|
||||
float vignette = smoothstep(0.95, 0.4, radius);
|
||||
|
||||
vec3 finalColor = bgGrey + secondaryRings;
|
||||
finalColor = mix(finalColor, primarySpiral, 0.85);
|
||||
|
||||
float centerLight = 0.015 / (radius + 0.02);
|
||||
finalColor += centerLight * coreWhite;
|
||||
|
||||
finalColor += exp(-distMouse * 4.0) * 0.03 * sceneAccent;
|
||||
|
||||
float dim = date.x;
|
||||
finalColor = mix(finalColor, vec3(0.0), dim);
|
||||
|
||||
outColor = vec4(finalColor * vignette, 1.0);
|
||||
}
|
||||
8
src/engine/types.ts
Normal file
8
src/engine/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { UiState } from './renderer'
|
||||
|
||||
export interface RendererLike {
|
||||
readonly ready: boolean
|
||||
init(canvas: HTMLCanvasElement): Promise<boolean>
|
||||
render(state: UiState): void
|
||||
resize(): void
|
||||
}
|
||||
Reference in New Issue
Block a user