diff --git a/TODO.md b/TODO.md
index 3047747..7429db2 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,2 +1,2 @@
- fix mouse, add button, drag
-
+- jsdoc
diff --git a/demo/3dt.fs b/demo/3dt.fs
new file mode 100644
index 0000000..846e76e
--- /dev/null
+++ b/demo/3dt.fs
@@ -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);
+}
diff --git a/demo/3dt.html b/demo/3dt.html
new file mode 100644
index 0000000..879078d
--- /dev/null
+++ b/demo/3dt.html
@@ -0,0 +1,76 @@
+
+
+
+
+ <repa-shader> demo
+
+
+
+
+
+
+
+
+
+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);
+}
+
+
+
+
+
+
+
+
diff --git a/demo/3dt.png b/demo/3dt.png
new file mode 100644
index 0000000..c42bd6a
Binary files /dev/null and b/demo/3dt.png differ
diff --git a/demo/index.html b/demo/simple.html
similarity index 100%
rename from demo/index.html
rename to demo/simple.html
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..df5abde
--- /dev/null
+++ b/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+ <repa-shader> demo
+
+
+ repa-shader demos
+
+
+
diff --git a/src/repa-shader.js b/src/repa-shader.js
index 6c6244f..0b89058 100644
--- a/src/repa-shader.js
+++ b/src/repa-shader.js
@@ -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 {} - 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('');
}
diff --git a/src/repa-texture.js b/src/repa-texture.js
index dedc752..38040cf 100644
--- a/src/repa-texture.js
+++ b/src/repa-texture.js
@@ -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');
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..0b7125b
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "include": ["src/**/*"],
+
+ "compilerOptions": {
+ "allowJs": true,
+ "declaration": true,
+ "emitDeclarationOnly": true,
+ "outDir": "dist",
+ "declarationMap": true,
+ "module": "ES2020",
+ "lib": ["ES2020", "DOM"]
+ }
+}