WIP : shader Worker
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import noise from "~/assets/shaders/noise.frag?raw"
|
||||
// @ts-ignore Import module
|
||||
import resolveLygia from "https://lygia.xyz/resolve.esm.js"
|
||||
|
||||
|
||||
declare const resolveLygia: (source: string) => string
|
||||
|
||||
let interval: number
|
||||
|
||||
const shaderOptions = { quality: 6, framerate: 1000 / 60 }
|
||||
const bounds = { left: 0, bottom: 0 }
|
||||
const screen = { width: 0, height: 0, devicePixelRatio: 0 }
|
||||
|
||||
let fragmentShader = resolveLygia(noise);
|
||||
|
||||
let canvas: OffscreenCanvas
|
||||
|
||||
async function main(canvas: OffscreenCanvas, fragmentShader2: string, _colorStart: [number, number, number], _colorEnd: [number, number, number]) {
|
||||
const gl = canvas.getContext("webgl");
|
||||
if (!gl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// setup GLSL program
|
||||
const program = gl.createProgram()
|
||||
if (!program) { return }
|
||||
|
||||
const fragment = gl.createShader(gl.FRAGMENT_SHADER);
|
||||
if (!fragment) { return }
|
||||
gl.shaderSource(fragment, fragmentShader2)
|
||||
gl.compileShader(fragment);
|
||||
if (!gl.getShaderParameter(fragment, gl.COMPILE_STATUS)) {
|
||||
const lastError = gl.getShaderInfoLog(fragment);
|
||||
console.error(lastError)
|
||||
gl.deleteShader(fragment);
|
||||
return
|
||||
}
|
||||
|
||||
const vertex = gl.createShader(gl.VERTEX_SHADER);
|
||||
if (!vertex) { return }
|
||||
gl.shaderSource(vertex, `attribute vec4 a_position;void main() {gl_Position = a_position;}`)
|
||||
gl.compileShader(vertex);
|
||||
if (!gl.getShaderParameter(vertex, gl.COMPILE_STATUS)) {
|
||||
const lastError = gl.getShaderInfoLog(vertex);
|
||||
console.error(lastError)
|
||||
gl.deleteShader(vertex);
|
||||
return
|
||||
}
|
||||
|
||||
gl.attachShader(program, fragment)
|
||||
gl.attachShader(program, vertex)
|
||||
gl.linkProgram(program)
|
||||
const linked = gl.getProgramParameter(program, gl.LINK_STATUS);
|
||||
if (!linked) { return }
|
||||
// const program = webglUtils.createProgramFromSources(gl, [vs, fragmentShader2]);
|
||||
|
||||
// look up where the vertex data needs to go.
|
||||
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
|
||||
|
||||
// look up uniform locations
|
||||
const offset = gl.getUniformLocation(program, "u_offset");
|
||||
const resolutionLocation = gl.getUniformLocation(program, "u_resolution");
|
||||
const colorStart = gl.getUniformLocation(program, "u_color_start");
|
||||
const quality = gl.getUniformLocation(program, "u_quality");
|
||||
const colorEnd = gl.getUniformLocation(program, "u_color_end");
|
||||
const timeLocation = gl.getUniformLocation(program, "u_time");
|
||||
|
||||
// Create a buffer to put three 2d clip space points in
|
||||
const positionBuffer = gl.createBuffer();
|
||||
|
||||
// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
|
||||
// fill it with a 2 triangles that cover clipspace
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
||||
-1, -1, // first triangle
|
||||
1, -1,
|
||||
-1, 1,
|
||||
-1, 1, // second triangle
|
||||
1, -1,
|
||||
1, 1,
|
||||
]), gl.STATIC_DRAW);
|
||||
|
||||
|
||||
const gl2 = gl
|
||||
function render(time: number) {
|
||||
time *= 0.001; // convert to seconds
|
||||
// Tell WebGL how to convert from clip space to pixels
|
||||
gl2.viewport(0, 0, gl2.canvas.width, gl2.canvas.height);
|
||||
|
||||
// Tell it to use our program (pair of shaders)
|
||||
gl2.useProgram(program);
|
||||
|
||||
// Turn on the attribute
|
||||
gl2.enableVertexAttribArray(positionAttributeLocation);
|
||||
|
||||
// Bind the position buffer.
|
||||
gl2.bindBuffer(gl2.ARRAY_BUFFER, positionBuffer);
|
||||
|
||||
// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
|
||||
gl2.vertexAttribPointer(
|
||||
positionAttributeLocation,
|
||||
2, // 2 components per iteration
|
||||
gl2.FLOAT, // the data is 32bit floats
|
||||
false, // don't normalize the data
|
||||
0, // 0 = move forward size * sizeof(type) each iteration to get the next position
|
||||
0, // start at the beginning of the buffer
|
||||
);
|
||||
|
||||
|
||||
const aspectRatio = Math.max(screen.width, screen.height) * screen.devicePixelRatio
|
||||
|
||||
gl2.uniform2f(offset, bounds.left, -bounds.bottom);
|
||||
// console.log(canvas.getBoundingClientRect().left * window.devicePixelRatio)
|
||||
gl2.uniform1i(quality, shaderOptions.quality)
|
||||
gl2.uniform2f(resolutionLocation, aspectRatio, aspectRatio);
|
||||
gl2.uniform3f(colorStart, _colorStart[0], _colorStart[1], _colorStart[2]);
|
||||
gl2.uniform3f(colorEnd, _colorEnd[0], _colorEnd[1], _colorEnd[2]);
|
||||
gl2.uniform1f(timeLocation, time);
|
||||
gl2.drawArrays(
|
||||
gl2.TRIANGLES,
|
||||
0, // offset
|
||||
6, // num vertices to process
|
||||
);
|
||||
}
|
||||
self.clearInterval(interval)
|
||||
interval = self.setInterval(() => requestAnimationFrame(render), shaderOptions.framerate)
|
||||
}
|
||||
|
||||
self.addEventListener("message", (e) => {
|
||||
const data = e.data
|
||||
if (data.command == "initShader") {
|
||||
canvas = data.canvas
|
||||
} else if (data.command == "killShader") {
|
||||
self.clearInterval(interval)
|
||||
} else {
|
||||
bounds.bottom = data.bounds.bottom
|
||||
bounds.left = data.bounds.left
|
||||
screen.devicePixelRatio = data.screen.devicePixelRatio
|
||||
screen.width = data.screen.width
|
||||
screen.height = data.screen.height
|
||||
if (data.command == "reloadShader") {
|
||||
shaderOptions.quality = data.shaderOptions.quality
|
||||
shaderOptions.framerate = data.shaderOptions.framerate
|
||||
|
||||
reloadShader(data.canvas, data.colorStart, data.colorEnd)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function reloadShader(canvas: OffscreenCanvas, _colorStart: [number, number, number], _colorEnd: [number, number, number]) {
|
||||
if (!canvas) { return }
|
||||
let fragmentShader2 = fragmentShader.replace("#define FBM_OCTAVES 6", "#define FBM_OCTAVES " + shaderOptions.quality);
|
||||
self.clearInterval(interval)
|
||||
main(canvas, fragmentShader2, _colorStart, _colorEnd);
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import noise from "~/assets/shaders/noise.frag?raw"
|
||||
const settingsStore = useSettingsStore()
|
||||
import { off } from 'process';
|
||||
import ShaderWorker from '~/assets/workers/ShaderWorker?worker'
|
||||
const worker = new ShaderWorker()
|
||||
|
||||
const canvas = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
declare const webglUtils: {
|
||||
createProgramFromSources: (gl: WebGLRenderingContext, array: [string, string]) => WebGLProgram,
|
||||
resizeCanvasToDisplaySize: (e: HTMLCanvasElement | OffscreenCanvas) => any
|
||||
}
|
||||
declare const resolveLygiaAsync: (source: string) => Promise<string>
|
||||
|
||||
export interface Props {
|
||||
colorStart?: [number, number, number]
|
||||
colorEnd?: [number, number, number]
|
||||
@@ -20,122 +15,66 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
colorEnd: () => [0, 1, 0]
|
||||
})
|
||||
|
||||
const targetFramerate = 1000 / settingsStore.shaderFramerate
|
||||
|
||||
const interval = ref<ReturnType<typeof setInterval>>()
|
||||
|
||||
let fragmentShader = await resolveLygiaAsync(noise);
|
||||
|
||||
async function main(canvas: HTMLCanvasElement, fragmentShader2: string) {
|
||||
// Get A WebGL context
|
||||
/** @type {HTMLCanvasElement} */
|
||||
const gl = canvas.getContext("webgl");
|
||||
if (!gl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vs = `attribute vec4 a_position;void main() {gl_Position = a_position;}`;
|
||||
|
||||
// setup GLSL program
|
||||
const program = webglUtils.createProgramFromSources(gl, [vs, fragmentShader2]);
|
||||
|
||||
// look up where the vertex data needs to go.
|
||||
const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
|
||||
|
||||
// look up uniform locations
|
||||
const offset = gl.getUniformLocation(program, "u_offset");
|
||||
const resolutionLocation = gl.getUniformLocation(program, "u_resolution");
|
||||
const colorStart = gl.getUniformLocation(program, "u_color_start");
|
||||
const quality = gl.getUniformLocation(program, "u_quality");
|
||||
const colorEnd = gl.getUniformLocation(program, "u_color_end");
|
||||
const timeLocation = gl.getUniformLocation(program, "u_time");
|
||||
|
||||
// Create a buffer to put three 2d clip space points in
|
||||
const positionBuffer = gl.createBuffer();
|
||||
|
||||
// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
|
||||
// fill it with a 2 triangles that cover clipspace
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
||||
-1, -1, // first triangle
|
||||
1, -1,
|
||||
-1, 1,
|
||||
-1, 1, // second triangle
|
||||
1, -1,
|
||||
1, 1,
|
||||
]), gl.STATIC_DRAW);
|
||||
|
||||
|
||||
const gl2 = gl
|
||||
function render(time: number) {
|
||||
time *= 0.001; // convert to seconds
|
||||
webglUtils.resizeCanvasToDisplaySize(gl2.canvas);
|
||||
|
||||
// Tell WebGL how to convert from clip space to pixels
|
||||
gl2.viewport(0, 0, gl2.canvas.width, gl2.canvas.height);
|
||||
|
||||
// Tell it to use our program (pair of shaders)
|
||||
gl2.useProgram(program);
|
||||
|
||||
// Turn on the attribute
|
||||
gl2.enableVertexAttribArray(positionAttributeLocation);
|
||||
|
||||
// Bind the position buffer.
|
||||
gl2.bindBuffer(gl2.ARRAY_BUFFER, positionBuffer);
|
||||
|
||||
// Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
|
||||
gl2.vertexAttribPointer(
|
||||
positionAttributeLocation,
|
||||
2, // 2 components per iteration
|
||||
gl2.FLOAT, // the data is 32bit floats
|
||||
false, // don't normalize the data
|
||||
0, // 0 = move forward size * sizeof(type) each iteration to get the next position
|
||||
0, // start at the beginning of the buffer
|
||||
);
|
||||
|
||||
const bounds = canvas.getBoundingClientRect()
|
||||
const aspectRatio = Math.max(window.screen.width, window.screen.height) * devicePixelRatio
|
||||
|
||||
gl2.uniform2f(offset, bounds.left, -bounds.bottom);
|
||||
// console.log(canvas.getBoundingClientRect().left * window.devicePixelRatio)
|
||||
gl2.uniform1i(quality, settingsStore.shaderQuality)
|
||||
gl2.uniform2f(resolutionLocation, aspectRatio, aspectRatio);
|
||||
gl2.uniform3f(colorStart, props.colorStart[0], props.colorStart[1], props.colorStart[2]);
|
||||
gl2.uniform3f(colorEnd, props.colorEnd[0], props.colorEnd[1], props.colorEnd[2]);
|
||||
gl2.uniform1f(timeLocation, time);
|
||||
gl2.drawArrays(
|
||||
gl2.TRIANGLES,
|
||||
0, // offset
|
||||
6, // num vertices to process
|
||||
);
|
||||
}
|
||||
clearInterval(interval.value)
|
||||
interval.value = setInterval(() => requestAnimationFrame(render), targetFramerate)
|
||||
function initShader() {
|
||||
if (!canvas.value) { return }
|
||||
const offscreen = canvas.value?.transferControlToOffscreen()
|
||||
worker.postMessage({
|
||||
command: "initShader",
|
||||
canvas: offscreen
|
||||
}, [offscreen])
|
||||
}
|
||||
|
||||
|
||||
async function reloadShader() {
|
||||
nextTick(() => {
|
||||
if (!canvas.value) { return }
|
||||
let fragmentShader2 = fragmentShader.replace("#define FBM_OCTAVES 6", "#define FBM_OCTAVES " + settingsStore.shaderQuality);
|
||||
clearInterval(interval.value)
|
||||
main(canvas.value, fragmentShader2);
|
||||
})
|
||||
|
||||
}
|
||||
watch(() => settingsStore.shaderQuality, (newValue, oldValue) => {
|
||||
function safeReloadShader() {
|
||||
reloadShader()
|
||||
})
|
||||
}
|
||||
|
||||
function reloadShader() {
|
||||
const bounds = canvas.value?.getBoundingClientRect()
|
||||
if (!bounds) { return }
|
||||
worker.postMessage({
|
||||
command: 'reloadShader',
|
||||
quality: settingsStore.shaderQuality,
|
||||
framerate: settingsStore.shaderFramerate,
|
||||
bounds: { left: bounds.left, bottom: bounds.bottom },
|
||||
screen: { height: window.screen.height, width: window.screen.width, devicePixelRatio },
|
||||
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate },
|
||||
colorStart: props.colorStart,
|
||||
colorEnd: props.colorEnd
|
||||
})
|
||||
}
|
||||
|
||||
function resizeShader() {
|
||||
const bounds = canvas.value?.getBoundingClientRect()
|
||||
if (!bounds) { return }
|
||||
worker.postMessage({
|
||||
command: 'changeBounds',
|
||||
bounds: { left: bounds.left, bottom: bounds.bottom },
|
||||
screen: { height: window.screen.height, width: window.screen.width, devicePixelRatio },
|
||||
})
|
||||
}
|
||||
|
||||
function endShader() {
|
||||
worker.postMessage({
|
||||
command: 'killShader'
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => [settingsStore.shaderQuality, settingsStore.shaderFramerate, props.colorEnd, props.colorStart], () => {
|
||||
safeReloadShader()
|
||||
})
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
reloadShader()
|
||||
nextTick(() => {
|
||||
initShader()
|
||||
window.addEventListener("resize", resizeShader, true)
|
||||
safeReloadShader()
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(interval.value)
|
||||
window.removeEventListener("resize", resizeShader)
|
||||
endShader()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -12,13 +12,4 @@ export default defineNuxtConfig({
|
||||
'@pinia/nuxt',
|
||||
'@pinia-plugin-persistedstate/nuxt',
|
||||
],
|
||||
app: {
|
||||
head: {
|
||||
script: [{
|
||||
src: "/webgl-utils.js"
|
||||
}, {
|
||||
src: "https://lygia.xyz/resolve.js"
|
||||
}]
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user