blob: 992c4c23d045310afbb080cc8d48f2c62289726b [file]
/**
* 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;
}
`;