This commit is contained in:
Michaël Lemaire 2024-12-17 17:32:34 +01:00
parent 51a539b73b
commit 850304b6f7
8 changed files with 190 additions and 46 deletions

9
.editorconfig Normal file
View file

@ -0,0 +1,9 @@
root = true
[*.{ts,json}]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

6
.gitignore vendored
View file

@ -1,3 +1,5 @@
node_modules
dist
deno.d.ts
.vscode
.local
.output
web/*.js

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# typescript/shaderview
[![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/shaderview?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=shaderview)

19
run Executable file
View file

@ -0,0 +1,19 @@
#!/bin/sh
# Simplified run tool for deno commands
if test $# -eq 0
then
echo "Usage: $0 [file or command]"
exit 1
elif echo $1 | grep -q '.*.ts'
then
denocmd=run
denoargs=$1
shift
else
denocmd=$1
shift
fi
denoargs="$(cat config/$denocmd.flags 2> /dev/null) $denoargs $@"
exec deno $denocmd $denoargs

54
src/shaderview.test.ts Normal file
View file

@ -0,0 +1,54 @@
import { assertEquals } from "jsr:@std/assert";
import { ShaderView } from "./shaderview.ts";
Deno.test("list uniforms from shader code", () => {
assertEquals(
new ShaderView("void main() {}", { noRender: true }).listUniforms(),
{
iFrame: {
type: "int",
},
iMouse: {
type: "vec4",
},
iResolution: {
type: "vec3",
},
iTime: {
type: "float",
},
iTimeDelta: {
type: "float",
},
},
);
assertEquals(
new ShaderView(
"uniform vec3 test1 ;\nuniform float test2; void main() {}",
{ noRender: true },
).listUniforms(),
{
iFrame: {
type: "int",
},
iMouse: {
type: "vec4",
},
iResolution: {
type: "vec3",
},
iTime: {
type: "float",
},
iTimeDelta: {
type: "float",
},
test1: {
type: "vec3",
},
test2: {
type: "float",
},
},
);
});

View file

@ -1,34 +1,24 @@
export class ShaderView {
readonly finalShader: string;
uniforms: Record<string, any> = {};
canvas: HTMLCanvasElement;
gl: WebGLRenderingContext;
vertexShader: WebGLShader;
fragmentShader: WebGLShader;
program: WebGLProgram;
vertices: Float32Array;
buffer: WebGLBuffer;
mousedown = false;
lastTime = 0;
render?: ShaderViewRender;
constructor(
shaderString: string,
options?: {
parent?: HTMLElement;
noRender?: boolean;
noAutoStart?: boolean;
size?: { width: number; height: number };
},
) {
// shadertoy differences
const ioTest = /\(\s*out\s+vec4\s+(\S+)\s*,\s*in\s+vec2\s+(\S+)\s*\)/;
const io = shaderString.match(ioTest);
shaderString = shaderString.replace("mainImage", "main");
shaderString = shaderString.replace(ioTest, "()");
const size = options?.size ?? { width: 128, height: 128 };
// shadertoy built in uniforms
const uniforms = this.uniforms = {
this.uniforms = {
iResolution: {
type: "vec3",
value: [window.innerWidth, window.innerHeight, 0],
value: [size.width, size.height, 0],
},
iTime: {
type: "float",
@ -48,21 +38,97 @@ export class ShaderView {
},
};
this.finalShader = this.preprocessShaderCode(shaderString);
if (!options?.noRender) {
this.render = new ShaderViewRender(this.finalShader, this.uniforms);
}
// attach to parent
if (options?.parent) {
this.setParent(options.parent);
}
// initial size
this.resize(size.width, size.height);
// auto render unless otherwise specified
if (this.render && !options?.noAutoStart) {
this.render.start();
}
}
preprocessShaderCode(shaderString: string): string {
// shadertoy differences
const ioTest = /\(\s*out\s+vec4\s+(\S+)\s*,\s*in\s+vec2\s+(\S+)\s*\)/;
const io = shaderString.match(ioTest);
shaderString = shaderString.replace("mainImage", "main");
shaderString = shaderString.replace(ioTest, "()");
// create default string values
shaderString =
(io
? `#define ${io[1]} gl_FragColor\n#define ${io[2]} gl_FragCoord.xy\n`
: "") + shaderString;
shaderString = Object.keys(uniforms)
shaderString = Object.keys(this.uniforms)
.map((key) => ({
name: key,
type: uniforms[key].type,
type: this.uniforms[key].type,
}))
.reduce((a, uniform) => (
a + `uniform ${uniform.type} ${uniform.name};\n`
), "") + shaderString;
shaderString = "precision highp float;\n" + shaderString;
return shaderString;
}
listUniforms(): Record<string, { type: string }> {
const result: Record<string, { type: string }> = {};
for (
const uniform of this.finalShader.match(/\buniform\s+\w+\s+\w+/g) ?? []
) {
const [type, name] = uniform.replaceAll(/\s+/g, " ").split(" ").slice(1);
result[name] = { type };
}
return result;
}
setUniform(name: string, value: any): void {
this.uniforms[name];
}
setParent(parent: HTMLElement): void {
if (this.render) {
parent.appendChild(this.render.canvas);
}
}
resize(width: number, height: number) {
this.uniforms.iResolution.value[0] = width;
this.uniforms.iResolution.value[1] = height;
if (this.render) {
this.render.resize(width, height);
}
}
}
class ShaderViewRender {
canvas: HTMLCanvasElement;
gl: WebGLRenderingContext;
vertexShader: WebGLShader;
fragmentShader: WebGLShader;
program: WebGLProgram;
vertices: Float32Array;
buffer: WebGLBuffer;
mousedown = false;
lastTime = 0;
constructor(
readonly shaderCode: string,
readonly uniforms: Record<string, any> = {},
) {
// create canvas
const canvas = this.canvas = document.createElement("canvas");
@ -89,7 +155,7 @@ export class ShaderView {
const fragmentShader = this.fragmentShader = check(gl.createShader(
gl.FRAGMENT_SHADER,
));
gl.shaderSource(fragmentShader, shaderString);
gl.shaderSource(fragmentShader, this.shaderCode);
gl.compileShader(fragmentShader);
// make program from shaders
@ -136,25 +202,6 @@ export class ShaderView {
canvas.addEventListener("mousedown", (ev) => this.mouseDown(ev));
canvas.addEventListener("mousemove", (ev) => this.mouseDown(ev));
canvas.addEventListener("mouseup", (ev) => this.mouseUp(ev));
// attach to parent
if (options?.parent) {
this.setParent(options.parent);
}
// initial size
if (options?.size) {
this.resize(options.size.width, options.size.height);
}
// auto render unless otherwise specified
if (!options?.noAutoStart) {
this.start();
}
}
setParent(parent: HTMLElement): void {
parent.appendChild(this.canvas);
}
mouseDown(e) {
@ -229,10 +276,10 @@ export class ShaderView {
}
resize(width: number, height: number) {
this.canvas.width = this.uniforms.iResolution.value[0] = width;
this.canvas.height = this.uniforms.iResolution.value[1] = height;
this.canvas.width = width;
this.canvas.height = height;
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
this.gl.viewport(0, 0, width, height);
}
}

11
tsconfig.json Normal file
View file

@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "esnext",
"target": "ESNext",
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"preserveConstEnums": true,
"lib": ["dom"]
}
}

View file

@ -4,7 +4,7 @@
<script type="module">
import { ShaderView } from "./mod.js";
const shader = `
void main()
void main()
{
vec2 uv = (gl_FragCoord.xy * 2. - iResolution.xy) / iResolution.y;
float d = length(uv);
@ -12,8 +12,7 @@
d = smoothstep(0.95, 1., 1. - d);
vec3 color = d * vec3(1., 2., 3.);
gl_FragColor = vec4(color, 1.0);
}
`.trim();
}`.trim();
new ShaderView(shader, {
parent: document.getElementById("container"),
size: { width: 512, height: 512 },