Merge pull request 'feat-shader-background' (#95) from feat-shader-background into main
release-tag / release-image (push) Successful in 33s

Reviewed-on: #95
This commit was merged in pull request #95.
This commit is contained in:
2024-05-24 17:58:19 +00:00
9 changed files with 336 additions and 142 deletions
+88 -113
View File
@@ -1,151 +1,121 @@
<script setup lang="ts">
import noise from "~/assets/shaders/noise.frag?raw"
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>
import gsap from 'gsap';
import ShaderWorker from '~/assets/workers/ShaderWorker?worker'
import goStore from '~/netcode';
const worker = new ShaderWorker()
const canvas = ref<HTMLCanvasElement | null>(null)
const canvas_resize_temp = ref<HTMLCanvasElement | null>(null)
export interface Props {
colorStart?: [number, number, number]
colorEnd?: [number, number, number]
}
const props = withDefaults(defineProps<Props>(), {
colorStart: () => [0, .3, 0],
colorEnd: () => [0, 1, 0]
})
const targetFramerate = 1000 / settingsStore.shaderFramerate
const _colorStart = reactive(props.colorStart)
const _colorEnd = reactive(props.colorEnd)
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;
function initShader() {
if (!canvas.value) { return }
const offscreen = canvas.value?.transferControlToOffscreen()
worker.postMessage({
command: "initShader",
canvas: offscreen,
screen: { height: window.innerHeight * devicePixelRatio, width: window.innerWidth * devicePixelRatio, devicePixelRatio },
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate },
}, [offscreen])
if (settingsStore.shaderFramerate == 0) {
killShader()
}
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)
resizeShader()
}
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);
function recolorShader() {
worker.postMessage({
command: 'recolorShader',
colorStart: [..._colorStart],
colorEnd: [..._colorEnd]
})
}
watch(() => settingsStore.shaderQuality, (newValue, oldValue) => {
reloadShader()
function resizeShader() {
worker.postMessage({
command: 'resizeShader',
screen: { height: window.innerHeight * devicePixelRatio, width: window.innerWidth * devicePixelRatio, devicePixelRatio },
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate },
})
}
function killShader() {
worker.postMessage({
command: 'killShader'
})
}
function resumeShader() {
worker.postMessage({
command: 'resumeShader',
shaderOptions: { quality: settingsStore.shaderQuality, framerate: settingsStore.shaderFramerate },
})
}
watch(() => [settingsStore.shaderFramerate], () => {
if (settingsStore.shaderFramerate == 0) {
killShader()
return
}
resumeShader()
})
watch(() => settingsStore.shaderFramerate, (newValue, oldValue) => {
reloadShader()
watch(() => [settingsStore.shaderQuality], () => {
resizeShader()
})
watch(() => [goStore.current_game?.current_turn], () => {
if (goStore.current_game?.current_turn != goStore.self?.team) {
gsap.to(_colorStart, { duration: 0.5, 0: 0, 1: .2, 2: .3 })
gsap.to(_colorEnd, { duration: 0.5, 0: 0, 1: 1, 2: 1 })
} else {
gsap.to(_colorStart, { duration: 0.5, 0: 0, 1: .3, 2: 0 })
gsap.to(_colorEnd, { duration: 0.5, 0: 0, 1: 1, 2: 0 })
}
}, { immediate: true })
watch([_colorEnd, _colorStart], () => {
recolorShader()
})
onMounted(async () => {
reloadShader()
nextTick(() => {
recolorShader()
initShader()
window.addEventListener("resize", resizeShader, true)
})
})
onUnmounted(() => {
clearInterval(interval.value)
window.removeEventListener("resize", resizeShader)
killShader()
})
</script>
<template>
<canvas v-if="settingsStore.shaderQuality != 0" ref="canvas" id="background">test</canvas>
<canvas ref="canvas" id="background" :class="{ pixelated: settingsStore.shaderPixelated }" width="480"
height="270">test</canvas>
</template>
<style scoped>
@@ -154,5 +124,10 @@ onUnmounted(() => {
position: absolute;
height: 100%;
width: 100%;
image-rendering: smooth;
}
#background.pixelated {
image-rendering: crisp-edges;
}
</style>
+17 -16
View File
@@ -6,6 +6,9 @@ export interface Props {
isVisible: boolean;
}
const _devicePixelRatio = computed(() => devicePixelRatio)
const _window = computed(() => window)
const props = withDefaults(defineProps<Props>(), {
})
@@ -15,10 +18,6 @@ const close = () => {
emit('close')
}
watch(() => settingsStore.shaderQuality, async () => {
console.log(settingsStore.shaderQuality)
})
</script>
<template>
@@ -40,27 +39,29 @@ watch(() => settingsStore.shaderQuality, async () => {
<span>Shader Quality</span>
</div>
<div class="dialog-body">
<input type="range" min="0" max="10" v-model="settingsStore.shaderQuality">
<input type="range" min="1" max="10" v-model="settingsStore.shaderQuality"
:disabled="settingsStore.shaderFramerate == 0">
</div>
<div class="dialog-footer">
<span>
<template v-if="settingsStore.shaderQuality == 0">
Disabled
</template>
<template v-if="settingsStore.shaderQuality > 0">
{{ settingsStore.shaderQuality }}
</template>
</span>
<span>{{ Math.floor(_window.innerWidth * _devicePixelRatio * settingsStore.shaderQuality / 10) }}x{{
Math.floor(_window.innerHeight *
_devicePixelRatio * settingsStore.shaderQuality / 10) }}</span>
</div>
<div class="dialog-header">
<span>Shader Framerate</span>
</div>
<div class="dialog-body">
<input type="range" min="10" max="60" step="5" v-model="settingsStore.shaderFramerate"
:disabled="settingsStore.shaderQuality == 0">
<input type="range" min="0" max="65" step="5" v-model="settingsStore.shaderFramerate">
</div>
<div class="dialog-footer">
<span>{{ settingsStore.shaderFramerate }} fps</span>
<span v-if="settingsStore.shaderFramerate == 65">vsync</span>
<span v-else>{{ settingsStore.shaderFramerate }} fps</span>
</div>
<div class="dialog-header">
<span>Pixelated</span>
</div>
<div class="dialog-body">
<input type="checkbox" v-model="settingsStore.shaderPixelated">
</div>
</div>
</div>
-1
View File
@@ -60,7 +60,6 @@ function can_buy_fire_gem(): boolean {
function checkEndTurn() {
// TODO: Add a confirmation dialog if resources are left
console.log("Checking end turn")
if (
(!warning.value)
&& has_actions_remaining()) {