diff --git a/src/visual/GratingStim.js b/src/visual/GratingStim.js index 88ad1ae..b5a222b 100644 --- a/src/visual/GratingStim.js +++ b/src/visual/GratingStim.js @@ -14,6 +14,7 @@ import { to_pixiPoint } from "../util/Pixi.js"; import * as util from "../util/Util.js"; import { VisualStim } from "./VisualStim.js"; import defaultQuadVert from "./shaders/defaultQuad.vert"; +import imageShader from "./shaders/imageShader.frag"; import sinShader from "./shaders/sinShader.frag"; import sqrShader from "./shaders/sqrShader.frag"; import sawShader from "./shaders/sawShader.frag"; @@ -60,6 +61,13 @@ export class GratingStim extends VisualStim * Shader source code is later used for construction of shader programs to create respective visual stimuli. * @name module:visual.GratingStim.#SHADERS * @type {Object} + * + * @property {Object} imageShader - Renders provided image with applied effects (coloring, phase, frequency). + * @property {String} imageShader.shader - shader source code for the image based grating stimuli. + * @property {Object} imageShader.uniforms - default uniforms for the image based shader. + * @property {float} imageShader.uniforms.uFreq=1.0 - how much times image repeated within grating stimuli. + * @property {float} imageShader.uniforms.uPhase=0.0 - offset of the image along X axis. + * * @property {Object} sin - Creates 2d sine wave image as if 1d sine graph was extended across Z axis and observed from above. * {@link https://en.wikipedia.org/wiki/Sine_wave} * @property {String} sin.shader - shader source code for the sine wave stimuli @@ -137,12 +145,22 @@ export class GratingStim extends VisualStim * @property {float} raisedCos.uniforms.uPeriod=0.625 - reciprocal of the symbol-rate (see link). */ static #SHADERS = { + imageShader: { + shader: imageShader, + uniforms: { + uFreq: 1.0, + uPhase: 0.0, + uColor: [1., 1., 1.], + uAlpha: 1.0 + } + }, sin: { shader: sinShader, uniforms: { uFreq: 1.0, uPhase: 0.0, - uColor: [1., 1., 1.] + uColor: [1., 1., 1.], + uAlpha: 1.0 } }, sqr: { @@ -150,7 +168,8 @@ export class GratingStim extends VisualStim uniforms: { uFreq: 1.0, uPhase: 0.0, - uColor: [1., 1., 1.] + uColor: [1., 1., 1.], + uAlpha: 1.0 } }, saw: { @@ -158,7 +177,8 @@ export class GratingStim extends VisualStim uniforms: { uFreq: 1.0, uPhase: 0.0, - uColor: [1., 1., 1.] + uColor: [1., 1., 1.], + uAlpha: 1.0 } }, tri: { @@ -167,7 +187,8 @@ export class GratingStim extends VisualStim uFreq: 1.0, uPhase: 0.0, uPeriod: 1.0, - uColor: [1., 1., 1.] + uColor: [1., 1., 1.], + uAlpha: 1.0 } }, sinXsin: { @@ -175,7 +196,8 @@ export class GratingStim extends VisualStim uniforms: { uFreq: 1.0, uPhase: 0.0, - uColor: [1., 1., 1.] + uColor: [1., 1., 1.], + uAlpha: 1.0 } }, sqrXsqr: { @@ -183,14 +205,16 @@ export class GratingStim extends VisualStim uniforms: { uFreq: 1.0, uPhase: 0.0, - uColor: [1., 1., 1.] + uColor: [1., 1., 1.], + uAlpha: 1.0 } }, circle: { shader: circleShader, uniforms: { uRadius: 1.0, - uColor: [1., 1., 1.] + uColor: [1., 1., 1.], + uAlpha: 1.0 } }, gauss: { @@ -199,21 +223,24 @@ export class GratingStim extends VisualStim uA: 1.0, uB: 0.0, uC: 0.16, - uColor: [1., 1., 1.] + uColor: [1., 1., 1.], + uAlpha: 1.0 } }, cross: { shader: crossShader, uniforms: { uThickness: 0.2, - uColor: [1., 1., 1.] + uColor: [1., 1., 1.], + uAlpha: 1.0 } }, radRamp: { shader: radRampShader, uniforms: { uSqueeze: 1.0, - uColor: [1., 1., 1.] + uColor: [1., 1., 1.], + uAlpha: 1.0 } }, raisedCos: { @@ -221,7 +248,8 @@ export class GratingStim extends VisualStim uniforms: { uBeta: 0.25, uPeriod: 0.625, - uColor: [1., 1., 1.] + uColor: [1., 1., 1.], + uAlpha: 1.0 } } }; @@ -469,11 +497,11 @@ export class GratingStim extends VisualStim * @name module:visual.GratingStim#_getPixiMeshFromPredefinedShaders * @function * @protected - * @param {String} funcName - name of the shader function. Must be one of the SHADERS + * @param {String} shaderName - name of the shader. Must be one of the SHADERS * @param {Object} uniforms - a set of uniforms to supply to the shader. Mixed together with default uniform values. * @return {Pixi.Mesh} Pixi.Mesh object that represents shader and later added to the scene. */ - _getPixiMeshFromPredefinedShaders (funcName = "", uniforms = {}) { + _getPixiMeshFromPredefinedShaders (shaderName = "", uniforms = {}) { const geometry = new PIXI.Geometry(); geometry.addAttribute( "aVertexPosition", @@ -492,8 +520,8 @@ export class GratingStim extends VisualStim ); geometry.addIndex([0, 1, 2, 0, 2, 3]); const vertexSrc = defaultQuadVert; - const fragmentSrc = GratingStim.#SHADERS[funcName].shader; - const uniformsFinal = Object.assign({}, GratingStim.#SHADERS[funcName].uniforms, uniforms); + const fragmentSrc = GratingStim.#SHADERS[shaderName].shader; + const uniformsFinal = Object.assign({}, GratingStim.#SHADERS[shaderName].uniforms, uniforms); const shader = PIXI.Shader.from(vertexSrc, fragmentSrc, uniformsFinal); return new PIXI.Mesh(geometry, shader); } @@ -509,9 +537,7 @@ export class GratingStim extends VisualStim setPhase (phase, log = false) { this._setAttribute("phase", phase, log); if (this._pixi instanceof PIXI.Mesh) { - this._pixi.shader.uniforms.uPhase = phase; - } else if (this._pixi instanceof PIXI.TilingSprite) { - this._pixi.tilePosition.x = -phase * (this._size_px[0] * this._pixi.tileScale.x) / (2 * Math.PI) + this._pixi.shader.uniforms.uPhase = -phase; } } @@ -547,8 +573,21 @@ export class GratingStim extends VisualStim this._setAttribute("color", colorObj, log); if (this._pixi instanceof PIXI.Mesh) { this._pixi.shader.uniforms.uColor = colorObj.rgbFull; - } else if (this._pixi instanceof PIXI.TilingSprite) { - + } + } + + /** + * Determines how visible the stimulus is relative to background. + * + * @name module:visual.GratingStim#setOpacity + * @public + * @param {number} [opacity=1] opacity - The value should be a single float ranging 1.0 (opaque) to 0.0 (transparent). + * @param {boolean} [log= false] - whether of not to log + */ + setOpacity (opacity = 1, log = false) { + this._setAttribute("opacity", opacity, log); + if (this._pixi instanceof PIXI.Mesh) { + this._pixi.shader.uniforms.uAlpha = opacity; } } @@ -564,13 +603,6 @@ export class GratingStim extends VisualStim this._setAttribute("SF", sf, log); if (this._pixi instanceof PIXI.Mesh) { this._pixi.shader.uniforms.uFreq = sf; - } else if (this._pixi instanceof PIXI.TilingSprite) { - // tileScale units are pixels, so converting function frequency to pixels - // and also taking into account possible size difference between used texture and requested stim size - this._pixi.tileScale.x = (1 / sf) * (this._pixi.width / this._pixi.texture.width); - // since most functions defined in SHADERS assume spatial frequency change along X axis - // we assume desired effect for image based stims to be the same so tileScale.y is not affected by spatialFrequency - this._pixi.tileScale.y = this._pixi.height / this._pixi.texture.height; } } @@ -607,12 +639,11 @@ export class GratingStim extends VisualStim */ setInterpolate (interpolate = false, log = false) { this._setAttribute("interpolate", interpolate, log); - if (this._pixi instanceof PIXI.Mesh) { - - } else if (this._pixi instanceof PIXI.TilingSprite) { - this._pixi.texture.baseTexture.scaleMode = interpolate ? PIXI.SCALE_MODES.LINEAR : PIXI.SCALE_MODES.NEAREST; - this._pixi.texture.baseTexture.update(); + if (this._pixi === undefined || !this._pixi.shader.uniforms.uTex) { + return; } + this._pixi.shader.uniforms.uTex.baseTexture.scaleMode = interpolate ? PIXI.SCALE_MODES.LINEAR : PIXI.SCALE_MODES.NEAREST; + this._pixi.shader.uniforms.uTex.baseTexture.update(); } /** @@ -633,6 +664,8 @@ export class GratingStim extends VisualStim if (this._needPixiUpdate) { this._needPixiUpdate = false; + let shaderName; + let shaderUniforms; let currentUniforms = {}; if (typeof this._pixi !== "undefined") { @@ -651,25 +684,28 @@ export class GratingStim extends VisualStim if (this._tex instanceof HTMLImageElement) { - this._pixi = PIXI.TilingSprite.from(this._tex, { - scaleMode: this._interpolate ? PIXI.SCALE_MODES.LINEAR : PIXI.SCALE_MODES.NEAREST, - width: this._size_px[0], - height: this._size_px[1] + shaderName = "imageShader"; + let shaderTex = PIXI.Texture.from(this._tex, { + wrapMode: PIXI.WRAP_MODES.REPEAT, + scaleMode: this._interpolate ? PIXI.SCALE_MODES.LINEAR : PIXI.SCALE_MODES.NEAREST }); - this.setPhase(this._phase); - this.setSF(this._SF); + shaderUniforms = { + uTex: shaderTex, + uFreq: this._SF, + uPhase: this._phase, + uColor: this._color.rgbFull + }; } else { - this._pixi = this._getPixiMeshFromPredefinedShaders( - this._tex, - Object.assign({ - uFreq: this._SF, - uPhase: this._phase, - uColor: this._color.rgbFull - }, currentUniforms) - ); + shaderName = this._tex; + shaderUniforms = { + uFreq: this._SF, + uPhase: this._phase, + uColor: this._color.rgbFull + }; } + this._pixi = this._getPixiMeshFromPredefinedShaders(shaderName, Object.assign(shaderUniforms, currentUniforms)); this._pixi.pivot.set(this._pixi.width * 0.5, this._pixi.width * 0.5); this._pixi.filters = [this._adjustmentFilter]; @@ -712,7 +748,7 @@ export class GratingStim extends VisualStim } this._pixi.zIndex = this._depth; - this._pixi.alpha = this.opacity; + this.opacity = this._opacity; // set the scale: const displaySize = this._getDisplaySize(); diff --git a/src/visual/shaders/circleShader.frag b/src/visual/shaders/circleShader.frag index 87addc6..4a2d741 100644 --- a/src/visual/shaders/circleShader.frag +++ b/src/visual/shaders/circleShader.frag @@ -17,11 +17,12 @@ out vec4 shaderOut; #define M_PI 3.14159265358979 uniform float uRadius; uniform vec3 uColor; +uniform float uAlpha; void main() { vec2 uv = vUvs; // converting first to [-1, 1] space to get the proper color functionality // then back to [0, 1] float s = (1. - step(uRadius, length(uv * 2. - 1.))) * 2. - 1.; - shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0); + shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; } diff --git a/src/visual/shaders/crossShader.frag b/src/visual/shaders/crossShader.frag index 8518f91..36c71f4 100644 --- a/src/visual/shaders/crossShader.frag +++ b/src/visual/shaders/crossShader.frag @@ -17,6 +17,7 @@ out vec4 shaderOut; #define M_PI 3.14159265358979 uniform float uThickness; uniform vec3 uColor; +uniform float uAlpha; void main() { vec2 uv = vUvs; @@ -25,5 +26,5 @@ void main() { // converting first to [-1, 1] space to get the proper color functionality // then back to [0, 1] float s = (1. - sx * sy) * 2. - 1.; - shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0); + shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; } diff --git a/src/visual/shaders/gaussShader.frag b/src/visual/shaders/gaussShader.frag index deb1de3..3600fe8 100644 --- a/src/visual/shaders/gaussShader.frag +++ b/src/visual/shaders/gaussShader.frag @@ -19,6 +19,7 @@ uniform float uA; uniform float uB; uniform float uC; uniform vec3 uColor; +uniform float uAlpha; #define M_PI 3.14159265358979 @@ -29,5 +30,5 @@ void main() { // converting first to [-1, 1] space to get the proper color functionality // then back to [0, 1] float g = uA * exp(-pow(x - uB, 2.) / c2 * .5) * 2. - 1.; - shaderOut = vec4(vec3(g) * uColor * .5 + .5, 1.); + shaderOut = vec4(vec3(g) * uColor * .5 + .5, 1.) * uAlpha; } diff --git a/src/visual/shaders/imageShader.frag b/src/visual/shaders/imageShader.frag new file mode 100644 index 0000000..2871eeb --- /dev/null +++ b/src/visual/shaders/imageShader.frag @@ -0,0 +1,31 @@ +/** + * Image shader. + * + * @author Nikita Agafonov + * @copyright (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) + * @license Distributed under the terms of the MIT License + * @description Renders passed in image with applied effects. + * @usedby GratingStim.js + */ + +#version 300 es +precision mediump float; + +in vec2 vUvs; +out vec4 shaderOut; + +#define M_PI 3.14159265358979 +uniform sampler2D uTex; +uniform float uFreq; +uniform float uPhase; +uniform vec3 uColor; +uniform float uAlpha; + +void main() { + vec2 uv = vUvs; + // converting first to [-1, 1] space to get the proper color functionality + // then back to [0, 1] + vec4 s = texture(uTex, vec2(uv.x * uFreq + uPhase, uv.y)); + s.xyz = s.xyz * 2. - 1.; + shaderOut = vec4(s.xyz * uColor * .5 + .5, s.a) * uAlpha; +} diff --git a/src/visual/shaders/radRampShader.frag b/src/visual/shaders/radRampShader.frag index 0ea424d..5aed76c 100644 --- a/src/visual/shaders/radRampShader.frag +++ b/src/visual/shaders/radRampShader.frag @@ -15,6 +15,7 @@ in vec2 vUvs; out vec4 shaderOut; uniform float uSqueeze; uniform vec3 uColor; +uniform float uAlpha; #define M_PI 3.14159265358979 @@ -23,5 +24,5 @@ void main() { // converting first to [-1, 1] space to get the proper color functionality // then back to [0, 1] float s = (1. - length(uv * 2. - 1.) * uSqueeze) * 2. - 1.; - shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0); + shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; } diff --git a/src/visual/shaders/raisedCosShader.frag b/src/visual/shaders/raisedCosShader.frag index 955d9f8..9ec0129 100644 --- a/src/visual/shaders/raisedCosShader.frag +++ b/src/visual/shaders/raisedCosShader.frag @@ -19,6 +19,7 @@ out vec4 shaderOut; uniform float uBeta; uniform float uPeriod; uniform vec3 uColor; +uniform float uAlpha; void main() { vec2 uv = vUvs; @@ -35,5 +36,5 @@ void main() { // converting first to [-1, 1] space to get the proper color functionality // then back to [0, 1] s = s * 2. - 1.; - shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0); + shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; } diff --git a/src/visual/shaders/sawShader.frag b/src/visual/shaders/sawShader.frag index 0802154..510a491 100644 --- a/src/visual/shaders/sawShader.frag +++ b/src/visual/shaders/sawShader.frag @@ -19,6 +19,7 @@ out vec4 shaderOut; uniform float uFreq; uniform float uPhase; uniform vec3 uColor; +uniform float uAlpha; void main() { vec2 uv = vUvs; @@ -26,5 +27,5 @@ void main() { // converting first to [-1, 1] space to get the proper color functionality // then back to [0, 1] s = mod(s, 1.) * 2. - 1.; - shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0); + shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; } diff --git a/src/visual/shaders/sinShader.frag b/src/visual/shaders/sinShader.frag index 37b31a0..a95267d 100644 --- a/src/visual/shaders/sinShader.frag +++ b/src/visual/shaders/sinShader.frag @@ -19,10 +19,11 @@ out vec4 shaderOut; uniform float uFreq; uniform float uPhase; uniform vec3 uColor; +uniform float uAlpha; void main() { - vec2 uv = vUvs; + vec2 uv = vUvs - .25; float s = sin((uFreq * uv.x + uPhase) * 2. * M_PI); - // it's important to convert to [0, 1] while multiplication to uColor, not before, to preserve desired coloring functionality - shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0); + // it's important to convert to [0, 1] while multiplying to uColor, not before, to preserve desired coloring functionality + shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; } diff --git a/src/visual/shaders/sinXsinShader.frag b/src/visual/shaders/sinXsinShader.frag index 58cf0b9..45b7353 100644 --- a/src/visual/shaders/sinXsinShader.frag +++ b/src/visual/shaders/sinXsinShader.frag @@ -20,12 +20,13 @@ out vec4 shaderOut; uniform float uFreq; uniform float uPhase; uniform vec3 uColor; +uniform float uAlpha; void main() { - vec2 uv = vUvs; + vec2 uv = vec2(vUvs.x - .25, vUvs.y * -1. - .25); float sx = sin((uFreq * uv.x + uPhase) * PI2); float sy = sin((uFreq * uv.y + uPhase) * PI2); float s = sx * sy; - // it's important to convert to [0, 1] while multiplication to uColor, not before, to preserve desired coloring functionality - shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0); + // it's important to convert to [0, 1] while multiplying to uColor, not before, to preserve desired coloring functionality + shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; } diff --git a/src/visual/shaders/sqrShader.frag b/src/visual/shaders/sqrShader.frag index dc7e34b..6198c5a 100644 --- a/src/visual/shaders/sqrShader.frag +++ b/src/visual/shaders/sqrShader.frag @@ -19,10 +19,11 @@ out vec4 shaderOut; uniform float uFreq; uniform float uPhase; uniform vec3 uColor; +uniform float uAlpha; void main() { - vec2 uv = vUvs; + vec2 uv = vUvs - .25; float s = sign(sin((uFreq * uv.x + uPhase) * 2. * M_PI)); - // it's important to convert to [0, 1] while multiplication to uColor, not before, to preserve desired coloring functionality - shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0); + // it's important to convert to [0, 1] while multiplying to uColor, not before, to preserve desired coloring functionality + shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; } diff --git a/src/visual/shaders/sqrXsqrShader.frag b/src/visual/shaders/sqrXsqrShader.frag index 7542208..2b3af20 100644 --- a/src/visual/shaders/sqrXsqrShader.frag +++ b/src/visual/shaders/sqrXsqrShader.frag @@ -20,12 +20,13 @@ out vec4 shaderOut; uniform float uFreq; uniform float uPhase; uniform vec3 uColor; +uniform float uAlpha; void main() { - vec2 uv = vUvs; + vec2 uv = vec2(vUvs.x - .25, vUvs.y * -1. - .25); float sx = sign(sin((uFreq * uv.x + uPhase) * PI2)); float sy = sign(sin((uFreq * uv.y + uPhase) * PI2)); float s = sx * sy; - // it's important to convert to [0, 1] while multiplication to uColor, not before, to preserve desired coloring functionality - shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0); + // it's important to convert to [0, 1] while multiplying to uColor, not before, to preserve desired coloring functionality + shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; } diff --git a/src/visual/shaders/triShader.frag b/src/visual/shaders/triShader.frag index c74260c..adf38f1 100644 --- a/src/visual/shaders/triShader.frag +++ b/src/visual/shaders/triShader.frag @@ -20,6 +20,7 @@ uniform float uFreq; uniform float uPhase; uniform float uPeriod; uniform vec3 uColor; +uniform float uAlpha; void main() { vec2 uv = vUvs; @@ -27,5 +28,5 @@ void main() { // converting first to [-1, 1] space to get the proper color functionality // then back to [0, 1] s = (2. * abs(s / uPeriod - floor(s / uPeriod + .5))) * 2. - 1.; - shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0); + shaderOut = vec4(vec3(s) * uColor * .5 + .5, 1.0) * uAlpha; }