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"] + } +}