| /** | |
| * Constructs a renderer object. | |
| * @param {WebGLRenderingContext} gl The GL context. | |
| * @constructor | |
| */ | |
| var Renderer = function (gl) { | |
| /** | |
| * The GL context. | |
| * @type {WebGLRenderingContext} | |
| * @private | |
| */ | |
| this.gl_ = gl; | |
| /** | |
| * A vertex buffer containing a single quad with xy coordinates from [-1,-1] | |
| * to [1,1] and uv coordinates from [0,0] to [1,1]. | |
| * @private | |
| */ | |
| this.quadVertexBuffer_ = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVertexBuffer_); | |
| var vertices = new Float32Array( | |
| [-1.0, -1.0, 0.0, 1.0, | |
| +1.0, -1.0, 1.0, 1.0, | |
| -1.0, +1.0, 0.0, 0.0, | |
| 1.0, +1.0, 1.0, 0.0]); | |
| // var vertices = new Float32Array( | |
| // [-1.0, -1.0, 0.0, .5, | |
| // +1.0, -1.0, .5, .5, | |
| // -1.0, +1.0, 0.0, 0.0, | |
| // 1.0, +1.0, .5, 0.0]); | |
| gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); | |
| /** | |
| * The default display program (no deblocking). | |
| * @private | |
| */ | |
| this.program_ = this.buildProgram_( | |
| Renderer.vertexShaderSource_, | |
| Renderer.fragmentShaderSource_); | |
| /** | |
| * The deblocking program. | |
| * @private | |
| */ | |
| this.deblockProgram_ = this.buildProgram_( | |
| Renderer.vertexShaderSource_, | |
| Renderer.deblockFragmentShaderSource_); | |
| // Bind the default program so any code relying on state after construction | |
| // sees the same program active as before. | |
| gl.useProgram(this.program_.program); | |
| gl.enableVertexAttribArray(0); | |
| gl.enable(gl.DEPTH_TEST); | |
| gl.disable(gl.CULL_FACE); | |
| }; | |
| /** | |
| * Compiles, links, and introspects a program. Returns an object bundling the | |
| * program handle with its cached uniform and attribute locations. | |
| * @param {string} vertexSource | |
| * @param {string} fragmentSource | |
| * @return {{program: WebGLProgram, uniforms: !Object, attribs: !Object}} | |
| * @private | |
| */ | |
| Renderer.prototype.buildProgram_ = function (vertexSource, fragmentSource) { | |
| var gl = this.gl_; | |
| var program = gl.createProgram(); | |
| var vs = this.compileShader_(vertexSource, gl.VERTEX_SHADER); | |
| var fs = this.compileShader_(fragmentSource, gl.FRAGMENT_SHADER); | |
| gl.attachShader(program, vs); | |
| gl.attachShader(program, fs); | |
| gl.bindAttribLocation(program, 0, 'vert'); | |
| gl.linkProgram(program); | |
| if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { | |
| var log = gl.getProgramInfoLog(program); | |
| console.error('Program link failed:\n' + log); | |
| throw new Error('Program link failed'); | |
| } | |
| var uniforms = {}; | |
| var uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); | |
| for (var i = 0; i < /** @type {number} */(uniformCount); i++) { | |
| var uinfo = gl.getActiveUniform(program, i); | |
| uniforms[uinfo.name] = gl.getUniformLocation(program, uinfo.name); | |
| } | |
| var attribs = {}; | |
| var attribCount = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES); | |
| for (var j = 0; j < /** @type {number} */(attribCount); j++) { | |
| var ainfo = gl.getActiveAttrib(program, j); | |
| attribs[ainfo.name] = gl.getAttribLocation(program, ainfo.name); | |
| } | |
| return { program: program, uniforms: uniforms, attribs: attribs }; | |
| }; | |
| Renderer.prototype.finishInit = function () { | |
| //this.draw(); | |
| }; | |
| /** | |
| * Sets the color space the compositor uses to interpret backbuffer pixels. | |
| * Pure display-side knob: does not modify pixel values, just relabels them. | |
| * No-op (returns false) on browsers/contexts that don't support the property | |
| * or don't accept the requested value. | |
| * @param {string} colorSpace e.g. "srgb" or "display-p3" | |
| * @return {boolean} true if the requested color space is now active. | |
| */ | |
| Renderer.prototype.setCanvasColorSpace = function (colorSpace) { | |
| var gl = this.gl_; | |
| if (typeof gl.drawingBufferColorSpace === 'undefined') return false; | |
| try { | |
| gl.drawingBufferColorSpace = colorSpace; | |
| } catch (e) { | |
| return false; | |
| } | |
| return gl.drawingBufferColorSpace === colorSpace; | |
| }; | |
| Renderer.prototype.createDxtTexture = function (dxtData, width, height, format) { | |
| var gl = this.gl_; | |
| var tex = gl.createTexture(); | |
| gl.bindTexture(gl.TEXTURE_2D, tex); | |
| gl.compressedTexImage2D( | |
| gl.TEXTURE_2D, | |
| 0, | |
| format, | |
| width, | |
| height, | |
| 0, | |
| dxtData); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
| gl.bindTexture(gl.TEXTURE_2D, null); | |
| return tex; | |
| }; | |
| Renderer.prototype.createCompressedTexture = function (data, width, height, format) { | |
| var gl = this.gl_; | |
| var tex = gl.createTexture(); | |
| gl.bindTexture(gl.TEXTURE_2D, tex); | |
| gl.compressedTexImage2D( | |
| gl.TEXTURE_2D, | |
| 0, | |
| format, | |
| width, | |
| height, | |
| 0, | |
| data); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
| gl.bindTexture(gl.TEXTURE_2D, null); | |
| return tex; | |
| }; | |
| Renderer.prototype.createHalfRGBATexture = function (data, width, height, format) { | |
| var gl = this.gl_; | |
| var tex = gl.createTexture(); | |
| gl.bindTexture(gl.TEXTURE_2D, tex); | |
| gl.texImage2D( | |
| gl.TEXTURE_2D, | |
| 0, | |
| gl.RGBA, | |
| width, | |
| height, | |
| 0, | |
| gl.RGBA, | |
| format, | |
| data); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
| gl.bindTexture(gl.TEXTURE_2D, null); | |
| return tex; | |
| }; | |
| // WebGL requires each row of rgb565Data to be aligned on a 4-byte boundary. | |
| Renderer.prototype.createRgb565Texture = function (rgb565Data, width, height) { | |
| var gl = this.gl_; | |
| var tex = gl.createTexture(); | |
| gl.bindTexture(gl.TEXTURE_2D, tex); | |
| gl.texImage2D( | |
| gl.TEXTURE_2D, | |
| 0, | |
| gl.RGB, | |
| width, | |
| height, | |
| 0, | |
| gl.RGB, | |
| gl.UNSIGNED_SHORT_5_6_5, | |
| rgb565Data); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
| gl.bindTexture(gl.TEXTURE_2D, null); | |
| return tex; | |
| }; | |
| Renderer.prototype.createRgbaTexture = function (rgbaData, width, height) { | |
| var gl = this.gl_; | |
| var tex = gl.createTexture(); | |
| gl.bindTexture(gl.TEXTURE_2D, tex); | |
| gl.texImage2D( | |
| gl.TEXTURE_2D, | |
| 0, | |
| gl.RGBA, | |
| width, | |
| height, | |
| 0, | |
| gl.RGBA, | |
| gl.UNSIGNED_BYTE, | |
| rgbaData); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
| gl.bindTexture(gl.TEXTURE_2D, null); | |
| return tex; | |
| }; | |
| /** | |
| * Draws the given texture across the viewport. | |
| * | |
| * Signature is preserved exactly from the original, pre-deblocking Renderer. | |
| * Existing call sites do not need to change. For the deblocking variant, use | |
| * drawTextureDeblocked. | |
| * | |
| * @param {WebGLTexture} texture | |
| * @param {number} width | |
| * @param {number} height | |
| * @param {number} mode 0=normal, 1=RGB only, 2=alpha only, 3/4/5=R/G/B isolate | |
| * @param {number} scale Exposure multiplier applied to RGB. | |
| * @param {boolean} linearToSRGBFlag | |
| * @param {boolean} useLinearFiltering | |
| */ | |
| Renderer.prototype.drawTexture = function (texture, width, height, mode, scale, linearToSRGBFlag, useLinearFiltering) { | |
| var gl = this.gl_; | |
| var prog = this.program_; | |
| gl.useProgram(prog.program); | |
| // draw scene | |
| gl.clearColor(0, 0, 0, 1); | |
| gl.clearDepth(1.0); | |
| gl.viewport(0, 0, width, height); | |
| gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); | |
| gl.activeTexture(gl.TEXTURE0); | |
| gl.bindTexture(gl.TEXTURE_2D, texture); | |
| // Point vs. bilinear sampling (no mipmaps involved here) | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, useLinearFiltering ? gl.LINEAR : gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, useLinearFiltering ? gl.LINEAR : gl.NEAREST); | |
| gl.uniform1i(prog.uniforms.texSampler, 0); | |
| var x = 0.0; | |
| var y = 0.0; | |
| if (mode == 1) | |
| x = 1.0; | |
| else if (mode == 2) | |
| y = 1.0; | |
| gl.uniform4f(prog.uniforms.control, x, y, scale, linearToSRGBFlag ? 1.0 : 0.0); | |
| var a = 1.0 / width; | |
| var b = 1.0 / height; | |
| var c = (mode >= 3) ? (mode - 2) : 0; // mode 3->1(R), 4->2(G), 5->3(B) | |
| var d = 0; | |
| gl.uniform4f(prog.uniforms.control2, a, b, c, d); | |
| gl.enableVertexAttribArray(prog.attribs.vert); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVertexBuffer_); | |
| gl.vertexAttribPointer(prog.attribs.vert, 4, gl.FLOAT, | |
| false, 0, 0); | |
| gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
| }; | |
| /** | |
| * Draws the given texture across the viewport with a block-edge deblocking | |
| * filter applied. Intended for previewing block-compressed textures (DXT, BC, | |
| * ASTC, etc.) where compression blocks can introduce visible seams. | |
| * | |
| * The post-processing knobs (mode, scale, linearToSRGBFlag, useLinearFiltering) | |
| * behave exactly as in drawTexture, and run on the deblocked RGBA. | |
| * | |
| * width/height are the texture's native dimensions (they double as viewport | |
| * size because this renderer always draws 1:1). | |
| * | |
| * @param {WebGLTexture} texture | |
| * @param {number} width | |
| * @param {number} height | |
| * @param {number} mode | |
| * @param {number} scale | |
| * @param {boolean} linearToSRGBFlag | |
| * @param {boolean} useLinearFiltering | |
| * @param {number} blockWidth Block width in texels (e.g. 4 for DXT/BC). | |
| * @param {number} blockHeight Block height in texels. | |
| * @param {boolean} showEdgeWeights Visualize the edge-weight mask instead of | |
| * the deblocked image. | |
| */ | |
| Renderer.prototype.drawTextureDeblocked = function (texture, width, height, mode, scale, linearToSRGBFlag, useLinearFiltering, blockWidth, blockHeight, showEdgeWeights) { | |
| var gl = this.gl_; | |
| var prog = this.deblockProgram_; | |
| gl.useProgram(prog.program); | |
| gl.clearColor(0, 0, 0, 1); | |
| gl.clearDepth(1.0); | |
| gl.viewport(0, 0, width, height); | |
| gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); | |
| gl.activeTexture(gl.TEXTURE0); | |
| gl.bindTexture(gl.TEXTURE_2D, texture); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, useLinearFiltering ? gl.LINEAR : gl.NEAREST); | |
| gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, useLinearFiltering ? gl.LINEAR : gl.NEAREST); | |
| gl.uniform1i(prog.uniforms.texSampler, 0); | |
| var x = 0.0; | |
| var y = 0.0; | |
| if (mode == 1) | |
| x = 1.0; | |
| else if (mode == 2) | |
| y = 1.0; | |
| gl.uniform4f(prog.uniforms.control, x, y, scale, linearToSRGBFlag ? 1.0 : 0.0); | |
| var a = 1.0 / width; | |
| var b = 1.0 / height; | |
| var c = (mode >= 3) ? (mode - 2) : 0; | |
| var d = 0; | |
| gl.uniform4f(prog.uniforms.control2, a, b, c, d); | |
| // texSize: xy = texture dims, zw = block dims. | |
| gl.uniform4f(prog.uniforms.texSize, width, height, blockWidth, blockHeight); | |
| // const0.x: master deblock enable. const0.y: edge-weight visualization. | |
| gl.uniform4f(prog.uniforms.const0, 1.0, showEdgeWeights ? 1.0 : 0.0, 0.0, 0.0); | |
| gl.enableVertexAttribArray(prog.attribs.vert); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, this.quadVertexBuffer_); | |
| gl.vertexAttribPointer(prog.attribs.vert, 4, gl.FLOAT, | |
| false, 0, 0); | |
| gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
| }; | |
| /** | |
| * Compiles a GLSL shader and returns a WebGLShader. | |
| * @param {string} shaderSource The shader source code string. | |
| * @param {number} type Either VERTEX_SHADER or FRAGMENT_SHADER. | |
| * @return {WebGLShader} The new WebGLShader. | |
| * @private | |
| */ | |
| Renderer.prototype.compileShader_ = function (shaderSource, type) { | |
| var gl = this.gl_; | |
| var shader = gl.createShader(type); | |
| gl.shaderSource(shader, shaderSource); | |
| gl.compileShader(shader); | |
| // Check for errors | |
| const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); | |
| if (!compiled) { | |
| const errorLog = gl.getShaderInfoLog(shader); | |
| console.error(`Error compiling ${type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:\n${errorLog}`); | |
| gl.deleteShader(shader); // Cleanup shader object | |
| throw new Error('Shader compilation failed'); | |
| } | |
| return shader; | |
| }; | |
| /** | |
| * @type {string} | |
| * @private | |
| */ | |
| Renderer.vertexShaderSource_ = /* glsl */ ` | |
| attribute vec4 vert; | |
| varying vec2 v_texCoord; | |
| void main() { | |
| gl_Position = vec4(vert.xy, 0.0, 1.0); | |
| v_texCoord = vert.zw; | |
| } | |
| `; | |
| /** | |
| * @type {string} | |
| * @private | |
| */ | |
| Renderer.fragmentShaderSource_ = /* glsl */ ` | |
| precision highp float; | |
| uniform sampler2D texSampler; | |
| uniform vec4 control; | |
| uniform vec4 control2; | |
| varying vec2 v_texCoord; | |
| vec3 linearToSrgb(vec3 linearRGB) { | |
| vec3 srgbLow = linearRGB * 12.92; | |
| vec3 srgbHigh = 1.055 * pow(linearRGB, vec3(1.0 / 2.4)) - 0.055; | |
| return clamp(mix(srgbLow, srgbHigh, step(0.0031308, linearRGB)), 0.0, 1.0); | |
| } | |
| void main() { | |
| vec4 c = texture2D(texSampler, v_texCoord); | |
| c.rgb *= control.z; | |
| if (control.x > 0.0) { | |
| c.w = 1.0; | |
| } else if (control.y > 0.0) { | |
| c.rgb = c.aaa; | |
| c.w = 1.0; | |
| } | |
| if (control2.z == 1.0) { c.rgb = vec3(c.r); c.w = 1.0; } | |
| else if (control2.z == 2.0) { c.rgb = vec3(c.g); c.w = 1.0; } | |
| else if (control2.z == 3.0) { c.rgb = vec3(c.b); c.w = 1.0; } | |
| if (control.w > 0.0) | |
| c.rgb = linearToSrgb(c.rgb); | |
| gl_FragColor = c; | |
| } | |
| `; | |
| /** | |
| * Deblocking fragment shader. Structurally identical to fragmentShaderSource_ | |
| * except the single texture2D(...) fetch at the top is replaced by a block- | |
| * aware 3-tap separable filter that softens texels near block boundaries. The | |
| * existing post-process chain (exposure, alpha-only, channel isolate, | |
| * linear->sRGB) then runs on the deblocked RGBA unchanged. | |
| * | |
| * Uniforms: | |
| * texSize xy = source texture dimensions in texels, | |
| * zw = block dimensions in texels (e.g. 4x4 for DXT/BC7, ASTC sizes | |
| * vary) | |
| * const0 x = master deblock enable (>0.5), | |
| * y = edge-weight visualization (>0.5) | |
| * | |
| * This shader assumes the texture is drawn 2D with no mipmap chain | |
| * (mipScale = 1). If you later add mipmapping, restore the dFdx/dFdy-based | |
| * mipScale derivation and add the OES_standard_derivatives extension. | |
| * @type {string} | |
| * @private | |
| */ | |
| Renderer.deblockFragmentShaderSource_ = /* glsl */ ` | |
| precision highp float; | |
| uniform sampler2D texSampler; | |
| uniform vec4 control; | |
| uniform vec4 control2; | |
| uniform vec4 texSize; // xy = tex dims, zw = block dims | |
| uniform vec4 const0; // x = deblock on, y = show edge weights | |
| varying vec2 v_texCoord; | |
| vec3 linearToSrgb(vec3 linearRGB) { | |
| vec3 srgbLow = linearRGB * 12.92; | |
| vec3 srgbHigh = 1.055 * pow(linearRGB, vec3(1.0 / 2.4)) - 0.055; | |
| return clamp(mix(srgbLow, srgbHigh, step(0.0031308, linearRGB)), 0.0, 1.0); | |
| } | |
| void main() { | |
| vec2 texDim = texSize.xy; | |
| vec2 blockSize = texSize.zw; | |
| vec2 texelStep = 1.0 / texDim; // one texel in mip0 space | |
| vec2 texelPos = (v_texCoord * texDim); // texel coord in mip0 space | |
| vec2 blockPos = mod(texelPos, blockSize); // texel position within the current block | |
| vec4 c = texture2D(texSampler, v_texCoord); | |
| vec4 l1 = texture2D(texSampler, v_texCoord - vec2(texelStep.x, 0.0)); | |
| vec4 r1 = texture2D(texSampler, v_texCoord + vec2(texelStep.x, 0.0)); | |
| vec4 u1 = texture2D(texSampler, v_texCoord - vec2(0.0, texelStep.y)); | |
| vec4 d1 = texture2D(texSampler, v_texCoord + vec2(0.0, texelStep.y)); | |
| if (const0.x > 0.5) { | |
| // Note this shader assumes a quad will be rendered to a viewport the same size as the texture, with point/nearest sampling. If you're going to render with bilinear filtering or mipmapping, this shader isn't the right one. | |
| const float falloff = 1.0; | |
| float leftProx = 1.0 - clamp(blockPos.x / falloff, 0.0, 1.0); | |
| float rightProx = 1.0 - clamp((blockSize.x - blockPos.x) / falloff, 0.0, 1.0); | |
| float topProx = 1.0 - clamp(blockPos.y / falloff, 0.0, 1.0); | |
| float bottomProx = 1.0 - clamp((blockSize.y - blockPos.y) / falloff, 0.0, 1.0); | |
| // Scale weights by 2.0 to ensure the inner ring of samples have 1.0 edge weights (if we knew bilinear sampling was going to be used, this would be 1.0). | |
| const float K = 2.0; | |
| float horizWeight = min(1.0, K*max(leftProx, rightProx)); | |
| float vertWeight = min(1.0, K*max(topProx, bottomProx)); | |
| float edgeWeight = max(horizWeight, vertWeight); | |
| if (edgeWeight > 0.0) { | |
| vec4 c0 = c; | |
| vec4 filteredH = (l1 + c0 + r1) * (1.0 / 3.0); | |
| vec4 filteredV = (u1 + c0 + d1) * (1.0 / 3.0); | |
| float strengthH = horizWeight; | |
| float strengthV = vertWeight; | |
| vec4 horizColor = mix(c0, filteredH, strengthH); | |
| vec4 vertColor = mix(c0, filteredV, strengthV); | |
| float totalW = strengthH + strengthV; | |
| if (totalW > 0.0) | |
| c = (horizColor * strengthH + vertColor * strengthV) / totalW; | |
| } | |
| // Block edge visualization. | |
| if (const0.y > 0.5) { | |
| c = vec4(edgeWeight, edgeWeight, edgeWeight, 1.0); | |
| } | |
| } | |
| // --- Post-process chain (identical to fragmentShaderSource_) --- | |
| c.rgb *= control.z; | |
| if (control.x > 0.0) { | |
| c.w = 1.0; | |
| } else if (control.y > 0.0) { | |
| c.rgb = c.aaa; | |
| c.w = 1.0; | |
| } | |
| if (control2.z == 1.0) { c.rgb = vec3(c.r); c.w = 1.0; } | |
| else if (control2.z == 2.0) { c.rgb = vec3(c.g); c.w = 1.0; } | |
| else if (control2.z == 3.0) { c.rgb = vec3(c.b); c.w = 1.0; } | |
| if (control.w > 0.0) | |
| c.rgb = linearToSrgb(c.rgb); | |
| gl_FragColor = c; | |
| } | |
| `; |