mirror of
https://github.com/dyuri/repa-shader.git
synced 2025-12-16 03:04:11 +00:00
commit
e5e24fc111
44
demo/3dt.fs
Normal file
44
demo/3dt.fs
Normal file
@ -0,0 +1,44 @@
|
||||
#define STEPSIZE .1
|
||||
#define DENSCALE .1
|
||||
|
||||
void main() {
|
||||
// Compute the pixel's position in view space.
|
||||
vec2 fragCoord = gl_FragCoord.xy / resolution.xy;
|
||||
vec3 viewPos = vec3((fragCoord * 2.0 - 1.0), 0.5);
|
||||
viewPos.y *= -1.0; // Flip Y axis to match WebGL convention.
|
||||
|
||||
vec3 camPos = vec3(.5 + m.x * .5, .5 + m.y * .5, -.25);
|
||||
vec3 camDir = vec3(1., 1., 1.);
|
||||
|
||||
// Convert the pixel's position to world space.
|
||||
vec3 worldPos = camPos + viewPos * length(camDir);
|
||||
|
||||
// Compute the ray direction in world space.
|
||||
vec3 rayDir = normalize(worldPos - camPos);
|
||||
|
||||
// Initialize the color and transparency values.
|
||||
vec4 color = vec4(0.0);
|
||||
float alpha = 0.0;
|
||||
|
||||
// Perform the ray-marching loop.
|
||||
for (float t = 0.0; t < 2.0; t += STEPSIZE) {
|
||||
// Compute the position along the ray.
|
||||
vec3 pos = camPos + rayDir * t;
|
||||
|
||||
// Sample the density at the current position.
|
||||
float density = texture(test3d, pos).x * DENSCALE;
|
||||
|
||||
// Accumulate the color and transparency values.
|
||||
vec4 sampleColor = vec4(1.0, 0.5, 0.2, 1.0);
|
||||
color += (1.0 - alpha) * sampleColor * density;
|
||||
alpha += (1.0 - alpha) * density;
|
||||
|
||||
// Stop marching if the transparency reaches 1.0.
|
||||
if (alpha >= 1.0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Output the final color and transparency.
|
||||
o = vec4(color.rgb, alpha);
|
||||
}
|
||||
76
demo/3dt.html
Normal file
76
demo/3dt.html
Normal file
@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><repa-shader> demo</title>
|
||||
<script type="module" src="../src/repa-texture.js"></script>
|
||||
<script type="module" src="../src/repa-shader.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: repeating-linear-gradient(45deg, #333, #333 10px, #666 10px, #666 20px);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
#fsinput {
|
||||
width: 90vw;
|
||||
height: 50vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<repa-shader id="demoshader" fs-input="fsinput" alpha mouse width=512 height=512>
|
||||
<repa-texture src="avatar.png" name="tex_avatar" wrap="mirrored_repeat"></repa-texture>
|
||||
<repa-texture t3d src="3dt.png" name="test3d" filter="linear" width=32 height=32 depth=32></repa-texture>
|
||||
<repa-texture t3d name="generated" filter="linear" width=32 height=32 depth=32></repa-texture>
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / resolution.xy;
|
||||
vec3 col = texture(tex_avatar, uv).rgb;
|
||||
|
||||
col *= texture(test3d, vec3(uv.xy, mouse.x)).rrr;
|
||||
col *= texture(generated, vec3(uv.xy, mouse.y)).rgb;
|
||||
|
||||
float dist = distance(uv, mouse.xy);
|
||||
float circle = smoothstep(.025, .026, dist) * .5 + .5;
|
||||
vec4 acolor = vec4(col * circle, circle);
|
||||
outColor = vec4(acolor);
|
||||
}
|
||||
</repa-shader>
|
||||
<div>
|
||||
<textarea id="fsinput"></textarea>
|
||||
</div>
|
||||
<button id="update">Update</button>
|
||||
<script>
|
||||
const updateButton = document.getElementById('update');
|
||||
updateButton.addEventListener('click', () => {
|
||||
const fsinput = document.getElementById('fsinput');
|
||||
const shader = document.querySelector('repa-shader');
|
||||
shader.render(fsinput.value);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
// generate texture data
|
||||
const generated3DT = document.querySelector('repa-texture[name="generated"]');
|
||||
const size = 32;
|
||||
const t3data = new Uint8Array(size * size * size * 4);
|
||||
for (let i = 0; i < size; i++) {
|
||||
for (let j = 0; j < size; j++) {
|
||||
for (let k = 0; k < size; k++) {
|
||||
let index = i * size * size + j * size + k;
|
||||
t3data[index*4] = i * 4 % 255;
|
||||
t3data[index*4+1] = j * 4 % 255;
|
||||
t3data[index*4+2] = k * 4 % 255;
|
||||
t3data[index*4+3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generated3DT.content = t3data;
|
||||
}, 100);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
demo/3dt.png
Normal file
BIN
demo/3dt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
19
index.html
Normal file
19
index.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><repa-shader> demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>repa-shader demos</h1>
|
||||
<ul>
|
||||
<li><a href="demo/simple.html">simple</a></li>
|
||||
<li><a href="demo/canvas.html">canvas texture</a></li>
|
||||
<li><a href="demo/video.html">video texture</a></li>
|
||||
<li><a href="demo/audio.html">audio texture</a></li>
|
||||
<li><a href="demo/orientation.html">device orientation uniform</a></li>
|
||||
<li><a href="demo/mrt.html">multi render target</a></li>
|
||||
<li><a href="demo/3dt.html">3d (volumetric) texture</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,3 +1,10 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* createLogger - creates a logger function
|
||||
*
|
||||
* @param {string[]} pfx - logger prefix
|
||||
*/
|
||||
const createLogger = pfx => {
|
||||
return {
|
||||
info: (...args) => console.info(...pfx, ...args),
|
||||
@ -16,6 +23,7 @@ const CHUNKS = {
|
||||
#define t time
|
||||
#define f frame
|
||||
precision highp float;
|
||||
precision highp sampler3D;
|
||||
uniform vec2 resolution;
|
||||
uniform vec3 mouse;
|
||||
uniform vec3 orientation;
|
||||
@ -55,6 +63,9 @@ class RepaShader extends HTMLElement {
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.logger = createLogger(["%c[repa-shader]", "background: #282828; color: #b8bb26"]);
|
||||
this._snippets = {};
|
||||
this._postProgram = null;
|
||||
/** @type {WebGL2RenderingContext} */
|
||||
this._gl = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
@ -66,7 +77,7 @@ class RepaShader extends HTMLElement {
|
||||
|
||||
if (!this._gl) {
|
||||
const glopts = {alpha: this.hasAttribute('alpha'), preserveDrawingBuffer: true};
|
||||
this._gl = this._target.getContext('webgl2', glopts);
|
||||
this._gl = this._target.getContext('webgl2', glopts); // @ts-ignore
|
||||
if (!this._gl) {
|
||||
this.logger.error("WebGL2 not supported");
|
||||
return;
|
||||
@ -100,6 +111,12 @@ class RepaShader extends HTMLElement {
|
||||
// TODO stop animation
|
||||
}
|
||||
|
||||
/**
|
||||
* render - loads the source if not provided and renders the shader
|
||||
*
|
||||
* @param {string} [source] - fragment shader source
|
||||
* @param {Number} [time] - timestamp to render for
|
||||
*/
|
||||
async render(source, time) {
|
||||
if (!source) {
|
||||
source = await this.getFragmentShaderSource();
|
||||
@ -108,6 +125,9 @@ class RepaShader extends HTMLElement {
|
||||
this.reset(time);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
get snippetPrefix() {
|
||||
if (this.hasAttribute('snippet-prefix')) {
|
||||
return this.getAttribute('snippet-prefix');
|
||||
@ -123,6 +143,11 @@ class RepaShader extends HTMLElement {
|
||||
return path + '/snippets';
|
||||
}
|
||||
|
||||
/**
|
||||
* loadSnippet - loads a snippet (prepending `snippetPrefix`)
|
||||
*
|
||||
* @param {string} name - snippet script name
|
||||
*/
|
||||
async loadSnippet(name) {
|
||||
let url = name;
|
||||
if (!url.startsWith('http')) {
|
||||
@ -138,6 +163,12 @@ class RepaShader extends HTMLElement {
|
||||
this._snippets[name] = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* getSnippet - returns a snippet (loading it if necessary)
|
||||
*
|
||||
* @param {string} name
|
||||
* @return {string} - snippet source
|
||||
*/
|
||||
async getSnippet(name) {
|
||||
if (!this._snippets[name]) {
|
||||
await this.loadSnippet(name);
|
||||
@ -146,6 +177,11 @@ class RepaShader extends HTMLElement {
|
||||
return this._snippets[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* _getSnippets - load all the snippets from the `snippets` attribute
|
||||
*
|
||||
* @return {<Promise>} - resolves when all snippets are loaded
|
||||
*/
|
||||
async _getSnippets() {
|
||||
if (!this.hasAttribute('snippets')) {
|
||||
return '';
|
||||
@ -157,6 +193,9 @@ class RepaShader extends HTMLElement {
|
||||
return await Promise.all(promises).then(snippets => snippets.join('\n'));
|
||||
}
|
||||
|
||||
/**
|
||||
* _resizeTarget - resizes the current target (and the GL viewport) canvas based on its current size
|
||||
*/
|
||||
_resizeTarget() {
|
||||
const {width, height} = this._target.getBoundingClientRect();
|
||||
this._target.width = width;
|
||||
@ -164,6 +203,11 @@ class RepaShader extends HTMLElement {
|
||||
this._gl.viewport(0, 0, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* _onOrientationEvent - handles orientation events
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
_onOrientationEvent(e) {
|
||||
this._orientation = [e.alpha, e.beta, e.gamma];
|
||||
}
|
||||
@ -260,10 +304,15 @@ class RepaShader extends HTMLElement {
|
||||
return (format && this._gl[format.toUpperCase().replaceAll('-', '_')]) || this._gl.RGBA;
|
||||
}
|
||||
|
||||
_getType(type) {
|
||||
return (type && this._gl[type.toUpperCase().replaceAll('-', '_')]) || this._gl.UNSIGNED_BYTE;
|
||||
}
|
||||
|
||||
_collectTextures() {
|
||||
this._textures = [];
|
||||
this._textures3d = [];
|
||||
|
||||
this.querySelectorAll('repa-texture').forEach(t => {
|
||||
this.querySelectorAll('repa-texture:not([t3d])').forEach(t => {
|
||||
const texture = this._gl.createTexture();
|
||||
this._gl.bindTexture(this._gl.TEXTURE_2D, texture);
|
||||
|
||||
@ -280,6 +329,23 @@ class RepaShader extends HTMLElement {
|
||||
texElement: t,
|
||||
});
|
||||
});
|
||||
|
||||
this.querySelectorAll('repa-texture[t3d]').forEach(t => {
|
||||
let texture = this._gl.createTexture();
|
||||
this._gl.bindTexture(this._gl.TEXTURE_3D, texture);
|
||||
this._gl.texParameteri(this._gl.TEXTURE_3D, this._gl.TEXTURE_MIN_FILTER, this._getFilter(t.minFilter));
|
||||
this._gl.texParameteri(this._gl.TEXTURE_3D, this._gl.TEXTURE_MAG_FILTER, this._getFilter(t.magFilter));
|
||||
this._gl.texParameteri(this._gl.TEXTURE_3D, this._gl.TEXTURE_WRAP_S, this._getWrap(t.wrapS));
|
||||
this._gl.texParameteri(this._gl.TEXTURE_3D, this._gl.TEXTURE_WRAP_T, this._getWrap(t.wrapT));
|
||||
this._gl.texParameteri(this._gl.TEXTURE_3D, this._gl.TEXTURE_WRAP_R, this._getWrap(t.wrapR));
|
||||
|
||||
this._gl.texImage3D(this._gl.TEXTURE_3D, 0, this._gl.RGBA, 1, 1, 1, 0, this._gl.RGBA, this._gl.UNSIGNED_BYTE, new Uint8Array([64, 255, 128, 255]));
|
||||
|
||||
this._textures3d.push({
|
||||
texture,
|
||||
texElement: t
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async reset(time) {
|
||||
@ -321,12 +387,9 @@ class RepaShader extends HTMLElement {
|
||||
const msg = this._gl.getProgramInfoLog(program);
|
||||
this.logger.error("Program link error: ", msg);
|
||||
// TODO error callback
|
||||
program = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO sound?
|
||||
|
||||
if (this._program) {
|
||||
this._gl.deleteProgram(this._program);
|
||||
}
|
||||
@ -351,6 +414,12 @@ class RepaShader extends HTMLElement {
|
||||
t.texElement.forceUpdate();
|
||||
});
|
||||
|
||||
this._textures3d.forEach((t) => {
|
||||
this._uniLocation[t.texElement.name] = this._gl.getUniformLocation(this.program, t.texElement.name); // texture
|
||||
this._uniLocation[t.texElement.name+'_d'] = this._gl.getUniformLocation(this.program, t.texElement.name+'_d'); // dimensions
|
||||
t.texElement.forceUpdate();
|
||||
});
|
||||
|
||||
this._attLocation = this._gl.getAttribLocation(this.program, 'position');
|
||||
this._mousePosition= [0, 0, 0];
|
||||
this._orientation = [0, 0, 0];
|
||||
@ -399,15 +468,37 @@ class RepaShader extends HTMLElement {
|
||||
// update if needed
|
||||
if (t.texElement.shouldUpdate) {
|
||||
const format = this._getFormat(t.texElement.format);
|
||||
const internalFormat = this._getFormat(t.texElement.internalFormat);
|
||||
const type = this._getType(t.texElement.dataType);
|
||||
|
||||
this._gl.pixelStorei(this._gl.UNPACK_FLIP_Y_WEBGL, t.texElement.flipY);
|
||||
|
||||
this._gl.texImage2D(this._gl.TEXTURE_2D, 0, format, t.texElement.width, t.texElement.height, 0, format, this._gl.UNSIGNED_BYTE, t.texElement.update());
|
||||
this._gl.texImage2D(this._gl.TEXTURE_2D, 0, internalFormat, t.texElement.width, t.texElement.height, 0, format, type, t.texElement.update());
|
||||
}
|
||||
|
||||
this._gl.uniform1i(this._uniLocation[t.texElement.name], i + this.mrt);
|
||||
this._gl.uniform2fv(this._uniLocation[t.texElement.name+'_d'], [t.texElement.width || 1, t.texElement.height || 1]);
|
||||
});
|
||||
|
||||
this._textures3d.forEach((t, i) => {
|
||||
this._gl.activeTexture(this._gl.TEXTURE0 + i + this.mrt + this._textures.length);
|
||||
this._gl.bindTexture(this._gl.TEXTURE_3D, t.texture);
|
||||
|
||||
// update if needed
|
||||
if (t.texElement.shouldUpdate) {
|
||||
const format = this._getFormat(t.texElement.format);
|
||||
const internalFormat = this._getFormat(t.texElement.internalFormat);
|
||||
const type = this._getType(t.texElement.dataType);
|
||||
|
||||
this._gl.pixelStorei(this._gl.UNPACK_FLIP_Y_WEBGL, 0);
|
||||
|
||||
this._gl.texImage3D(this._gl.TEXTURE_3D, 0, internalFormat, t.texElement.width, t.texElement.height, t.texElement.depth, 0, format, type, t.texElement.update());
|
||||
}
|
||||
|
||||
this._gl.uniform1i(this._uniLocation[t.texElement.name], i + this.mrt + this._textures.length);
|
||||
this._gl.uniform3fv(this._uniLocation[t.texElement.name+'_d'], [t.texElement.width || 1, t.texElement.height || 1, t.texElement.depth || 1]);
|
||||
});
|
||||
|
||||
this._gl.drawArrays(this._gl.TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
// fill buffer
|
||||
@ -564,6 +655,12 @@ void main() {
|
||||
return `
|
||||
uniform sampler2D ${t.texElement.name};
|
||||
uniform vec2 ${t.texElement.name}_d;
|
||||
`;
|
||||
}).join('') +
|
||||
this._textures3d.map(t => {
|
||||
return `
|
||||
uniform sampler3D ${t.texElement.name};
|
||||
uniform vec3 ${t.texElement.name}_d;
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ class RepaTexture extends HTMLElement {
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['src', 'type', 'mag-filter', 'min-filter', 'filter', 'wrap-s', 'wrap-t', 'wrap', 'format'];
|
||||
return ['src', 'type', 'mag-filter', 'min-filter', 'filter', 'wrap-s', 'wrap-t', 'wrap-r', 'wrap', 'format'];
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
@ -85,9 +85,9 @@ class RepaTexture extends HTMLElement {
|
||||
this.ready = true;
|
||||
this._forceUpdate = true;
|
||||
} else if (this.textContent) {
|
||||
this.content = JSON.parse(this.textContent);
|
||||
this.simpleContent(JSON.parse(this.textContent));
|
||||
} else {
|
||||
this.logger.error('Source cannot be loaded');
|
||||
this.logger.warn('Texture content cannot be loaded!');
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,27 +212,32 @@ class RepaTexture extends HTMLElement {
|
||||
return null;
|
||||
}
|
||||
|
||||
get flipY() {
|
||||
return this.type !== 'raw';
|
||||
get t3d() {
|
||||
return this.hasAttribute('t3d');
|
||||
}
|
||||
|
||||
setContent(data) {
|
||||
get flipY() {
|
||||
return !this.t3d && this.type !== 'raw';
|
||||
}
|
||||
|
||||
simpleContent(data) {
|
||||
this._format = 'luminance';
|
||||
this._width = data[0].length;
|
||||
this._height = data.length;
|
||||
this._content = new Uint8Array(this._width * this._height);
|
||||
const content = new Uint8Array(this._width * this._height);
|
||||
|
||||
data.forEach((row, y) => {
|
||||
this._content.set(row, y * this._width);
|
||||
content.set(row, y * this._width);
|
||||
});
|
||||
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
set content(data) {
|
||||
this.ready = true;
|
||||
this._type = 'raw';
|
||||
this._format = 'luminance';
|
||||
this._forceUpdate = true;
|
||||
|
||||
this.setContent(data);
|
||||
this._content = data;
|
||||
}
|
||||
|
||||
get content() {
|
||||
@ -281,9 +286,9 @@ class RepaTexture extends HTMLElement {
|
||||
analyser.getByteFrequencyData(this._freqData);
|
||||
analyser.getByteTimeDomainData(this._timeData);
|
||||
|
||||
this.setContent([this._freqData, this._timeData]);
|
||||
this.simpleContent([this._freqData, this._timeData]);
|
||||
} else {
|
||||
this.setContent([[255, 128, 64, 32, 16, 8, 4, 2], [2, 4, 8, 16, 32, 64, 128, 255]]);
|
||||
this.simpleContent([[255, 128, 64, 32, 16, 8, 4, 2], [2, 4, 8, 16, 32, 64, 128, 255]]);
|
||||
}
|
||||
|
||||
return this._content;
|
||||
@ -327,11 +332,15 @@ class RepaTexture extends HTMLElement {
|
||||
}
|
||||
|
||||
get width() {
|
||||
return this._width || this.ref?.videoWidth || this.ref?.width || 0;
|
||||
return +(this._width || this.getAttribute("width") || this.ref?.videoWidth || this.ref?.width || 0);
|
||||
}
|
||||
|
||||
get height() {
|
||||
return this._height || this.ref?.videoHeight || this.ref?.height || 0;
|
||||
return +(this._height || this.getAttribute("height") || this.ref?.videoHeight || this.ref?.height || 0);
|
||||
}
|
||||
|
||||
get depth() {
|
||||
return +(this._depth || this.getAttribute("depth") || this.ref?.depth || 0);
|
||||
}
|
||||
|
||||
get magFilter() {
|
||||
@ -350,10 +359,22 @@ class RepaTexture extends HTMLElement {
|
||||
return this.getAttribute('wrap-t') || this.getAttribute('wrap') || 'clamp-to-edge';
|
||||
}
|
||||
|
||||
get wrapR() {
|
||||
return this.getAttribute('wrap-r') || this.getAttribute('wrap') || 'clamp-to-edge';
|
||||
}
|
||||
|
||||
get format() {
|
||||
return this._format || this.getAttribute('format') || 'rgba';
|
||||
}
|
||||
|
||||
get internalFormat() {
|
||||
return this._internalFormat || this.getAttribute('internal-format') || this.format;
|
||||
}
|
||||
|
||||
get dataType() {
|
||||
return this._dataType || this.getAttribute('data-type') || 'unsigned-byte';
|
||||
}
|
||||
|
||||
get name() {
|
||||
if (!this._name) {
|
||||
let name = this.getAttribute('name');
|
||||
|
||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"include": ["src/**/*"],
|
||||
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "dist",
|
||||
"declarationMap": true,
|
||||
"module": "ES2020",
|
||||
"lib": ["ES2020", "DOM"]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user