ScreenView = class {
constructor(canvas) {
const gl = canvas.getContext("webgl");
const float_texture_ext = gl.getExtension('OES_texture_float');
if (float_texture_ext == null) {
throw new Error("WebGL extension 'OES_texture_float' unavailable");
}
this.canvas = canvas;
this.gl = gl;
this.textures = {};
this.shaders = {};
this.buffers = [];
this.image = null;
this.display = null;
this.imageSampleRate = null;
this.imageBlackLevel = null;
this.imageWhiteLevel = null;
this.imageSubcarrier = null;
this.viewportSize = new Size(0, 0);
this.persistenceTexRect = new Rect(0, 0, 0, 0);
this.configurationChanged = true;
this.imageChanged = true;
}
get image() {
return this._image;
}
set image(image) {
this._image = image;
this.imageChanged = true;
}
set displayConfiguration(displayConfiguration) {
this.display = displayConfiguration;
this.configurationChanged = true;
}
async initOpenGL() {
const gl = this.gl;
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
this.textures = {};
for (let name of TEXTURE_NAMES) {
this.textures[name] = new TextureInfo(0, 0, gl.createTexture());
}
for (let i = 0; i < BUFFER_COUNT; i++) {
this.buffers.push(gl.createBuffer());
}
await this.loadTextures();
gl.pixelStorei(gl.PACK_ALIGNMENT, 1);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
this.loadShaders();
}
freeOpenGL() {
const gl = this.gl;
for (let name of TEXTURE_NAMES) {
gl.deleteTexture(this.textures[name].glTexture);
}
for (let buffer of this.buffers) {
gl.deleteBuffer(buffer);
}
this.deleteShaders();
}
loadTextures() {
return Promise.all([
this.loadTexture("Shadow Mask Triad.png", true, "SHADOWMASK_TRIAD"),
this.loadTexture("Shadow Mask Inline.png", true, "SHADOWMASK_INLINE"),
this.loadTexture("Shadow Mask Aperture.png", true, "SHADOWMASK_APERTURE"),
this.loadTexture("Shadow Mask LCD.png", true, "SHADOWMASK_LCD"),
this.loadTexture("Shadow Mask Bayer.png", true, "SHADOWMASK_BAYER")
]);
}
async loadTexture(path, isMipMap, name) {
const gl = this.gl;
const texInfo = this.textures[name];
const image = await loadImage(path);
gl.bindTexture(gl.TEXTURE_2D, texInfo.glTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
if (isMipMap) {
gl.generateMipmap(gl.TEXTURE_2D);
}
texInfo.width = image.naturalWidth;
texInfo.height = image.naturalHeight;
}
loadShaders() {
this.loadShader("COMPOSITE", COMPOSITE_SHADER, VERTEX_RENDER_SHADER);
this.loadShader("RGB", RGB_SHADER, VERTEX_RENDER_SHADER);
this.loadShader("DISPLAY", DISPLAY_SHADER, VERTEX_DISPLAY_SHADER);
}
loadShader(name, fragmentSource, vertexSource) {
const glVertexShader = createShader(
this.gl,
name,
this.gl.VERTEX_SHADER,
vertexSource
);
const glFragmentShader = createShader(
this.gl,
name,
this.gl.FRAGMENT_SHADER,
fragmentSource
);
const glProgram = createProgram(
this.gl,
name,
glVertexShader,
glFragmentShader
);
this.gl.deleteShader(glVertexShader);
this.gl.deleteShader(glFragmentShader);
this.shaders[name] = glProgram;
}
deleteShaders() {
for (let name of SHADER_NAMES) {
if (this.shaders[name]) {
this.gl.deleteProgram(this.shaders[name]);
this.shaders[name] = false;
}
}
}
vsync() {
const gl = this.gl;
resizeCanvas(this.canvas);
const canvasWidth = this.canvas.width;
const canvasHeight = this.canvas.height;
// if viewport size has changed:
if (
this.viewportSize.width != canvasWidth ||
this.viewportSize.height != this.canvasHeight
) {
this.viewportSize = new Size(canvasWidth, canvasHeight);
gl.viewport(0, 0, canvasWidth, canvasHeight);
this.configurationChanged = true;
}
if (this.imageChanged) {
this.uploadImage();
}
if (this.configurationChanged) {
this.configureShaders();
}
if (this.imageChanged || this.configurationChanged) {
this.renderImage();
}
if (
this.imageChanged ||
this.configurationChanged ||
this.image.displayPersistence != 0
) {
this.drawDisplayCanvas();
}
}
uploadImage() {
const gl = this.gl;
const image = this.image;
this.resizeTexture("IMAGE_IN", image.width, image.height, true);
const texInfoImage = this.textures["IMAGE_IN"];
gl.bindTexture(gl.TEXTURE_2D, texInfoImage.glTexture);
const format = gl.LUMINANCE;
const type = gl.UNSIGNED_BYTE;
const luminance = luminanceData(image.data);
gl.texSubImage2D(
gl.TEXTURE_2D,
0,
0,
0, // xoffset, yoffset
image.data.width,
image.data.height,
format,
type,
luminance
);
// Update configuration
if (
image.sampleRate != this.imageSampleRate ||
image.blackLevel != this.imageBlackLevel ||
image.whiteLevel != this.imageWhiteLevel ||
image.subCarrier != this.imageSubcarrier
) {
this.imageSampleRate = image.sampleRate;
this.imageBlackLevel = image.blackLevel;
this.imageWhiteLevel = image.whiteLevel;
this.imageSubcarrier = image.subCarrier;
this.configurationChanged = true;
}
// Upload phase info
const texHeight = 2 ** Math.ceil(Math.log2(image.height));
const colorBurst = image.colorBurst;
const phaseAlternation = image.phaseAlternation;
const phaseInfo = new Float32Array(3 * texHeight);
for (let x = 0; x < image.height; x++) {
const c = colorBurst[x % colorBurst.length] / 2 / Math.PI;
phaseInfo[3 * x + 0] = c - Math.floor(c);
phaseInfo[3 * x + 1] = phaseAlternation[x % phaseAlternation.length];
}
const texInfoPhase = this.textures["IMAGE_PHASEINFO"];
gl.bindTexture(gl.TEXTURE_2D, texInfoPhase.glTexture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGB,
1,
texHeight,
0,
gl.RGB,
gl.FLOAT,
phaseInfo
);
}
getRenderShader() {
switch (this.display.videoDecoder) {
case "CANVAS_RGB":
case "CANVAS_MONOCHROME":
return [this.shaders["RGB"], "RGB"];
case "CANVAS_YUV":
case "CANVAS_YIQ":
case "CANVAS_CXA2025AS":
return [this.shaders["COMPOSITE"], "COMPOSITE"];
}
const decoder = this.display.videoDecoder;
throw new Error(`unknown displayConfiguration.videoDecoder: ${decoder}`);
}
configureShaders() {
const gl = this.gl;
const [renderShader, renderShaderName] = this.getRenderShader();
const displayShader = this.shaders["DISPLAY"];
const isCompositeDecoder = renderShaderName == "COMPOSITE";
// Render shader
gl.useProgram(renderShader);
// Subcarrier
if (isCompositeDecoder) {
gl.uniform1f(
gl.getUniformLocation(renderShader, "subcarrier"),
this.imageSubcarrier / this.imageSampleRate
);
}
// Filters
const w = Vector.chebyshevWindow(17, 50).normalize();
let wy, wu, wv;
const bandwidth = this.display.videoBandwidth / this.imageSampleRate;
if (isCompositeDecoder) {
let yBandwidth = this.display.videoLumaBandwidth / this.imageSampleRate;
let uBandwidth = this.display.videoChromaBandwidth / this.imageSampleRate;
let vBandwidth = uBandwidth;
if (this.display.videoDecoder == "CANVAS_YIQ")
uBandwidth = uBandwidth + NTSC_IQ_DELTA / this.imageSampleRate;
// Switch to video bandwidth when no subcarrier
if (this.imageSubcarrier == 0.0 || this.display.videoWhiteOnly) {
yBandwidth = bandwidth;
uBandwidth = bandwidth;
vBandwidth = bandwidth;
}
wy = w.mul(Vector.lanczosWindow(17, yBandwidth));
wy = wy.normalize();
wu = w.mul(Vector.lanczosWindow(17, uBandwidth));
wu = wu.normalize().mul(2);
wv = w.mul(Vector.lanczosWindow(17, vBandwidth));
wv = wv.normalize().mul(2);
} else {
wy = w.mul(Vector.lanczosWindow(17, bandwidth));
wu = wv = wy = wy.normalize();
}
gl.uniform3f(
gl.getUniformLocation(renderShader, "c0"),
wy.data[8],
wu.data[8],
wv.data[8]
);
gl.uniform3f(
gl.getUniformLocation(renderShader, "c1"),
wy.data[7],
wu.data[7],
wv.data[7]
);
gl.uniform3f(
gl.getUniformLocation(renderShader, "c2"),
wy.data[6],
wu.data[6],
wv.data[6]
);
gl.uniform3f(
gl.getUniformLocation(renderShader, "c3"),
wy.data[5],
wu.data[5],
wv.data[5]
);
gl.uniform3f(
gl.getUniformLocation(renderShader, "c4"),
wy.data[4],
wu.data[4],
wv.data[4]
);
gl.uniform3f(
gl.getUniformLocation(renderShader, "c5"),
wy.data[3],
wu.data[3],
wv.data[3]
);
gl.uniform3f(
gl.getUniformLocation(renderShader, "c6"),
wy.data[2],
wu.data[2],
wv.data[2]
);
gl.uniform3f(
gl.getUniformLocation(renderShader, "c7"),
wy.data[1],
wu.data[1],
wv.data[1]
);
gl.uniform3f(
gl.getUniformLocation(renderShader, "c8"),
wy.data[0],
wu.data[0],
wv.data[0]
);
// Decoder matrix
let decoderMatrix = new Matrix3(1, 0, 0, 0, 1, 0, 0, 0, 1);
// Encode
if (!isCompositeDecoder) {
// Y'PbPr encoding matrix
decoderMatrix = new Matrix3(
0.299,
-0.168736,
0.5,
0.587,
-0.331264,
-0.418688,
0.114,
0.5,
-0.081312
).mul(decoderMatrix);
}
// Set hue
if (this.display.videoDecoder == "CANVAS_MONOCHROME")
decoderMatrix = new Matrix3(1, 0.5, 0, 0, 0, 0, 0, 0, 0).mul(
decoderMatrix
);
// Disable color decoding when no subcarrier
if (isCompositeDecoder) {
if (this.imageSubcarrier == 0.0 || this.display.videoWhiteOnly) {
decoderMatrix = new Matrix3(1, 0, 0, 0, 0, 0, 0, 0, 0).mul(
decoderMatrix
);
}
}
// Saturation
decoderMatrix = new Matrix3(
1,
0,
0,
0,
this.display.videoSaturation,
0,
0,
0,
this.display.videoSaturation
).mul(decoderMatrix);
// Hue
let hue = 2 * Math.PI * this.display.videoHue;
decoderMatrix = new Matrix3(
1,
0,
0,
0,
Math.cos(hue),
-Math.sin(hue),
0,
Math.sin(hue),
Math.cos(hue)
).mul(decoderMatrix);
// Decode
switch (this.display.videoDecoder) {
case "CANVAS_RGB":
case "CANVAS_MONOCHROME":
// Y'PbPr decoder matrix
decoderMatrix = new Matrix3(
1,
1,
1,
0,
-0.344136,
1.772,
1.402,
-0.714136,
0
).mul(decoderMatrix);
break;
case "CANVAS_YUV":
case "CANVAS_YIQ":
// Y'UV decoder matrix
decoderMatrix = new Matrix3(
1,
1,
1,
0,
-0.394642,
2.032062,
1.139883,
-0.580622,
0
).mul(decoderMatrix);
break;
case "CANVAS_CXA2025AS":
// Exchange I and Q
decoderMatrix = new Matrix3(1, 0, 0, 0, 0, 1, 0, 1, 0).mul(
decoderMatrix
);
// Rotate 33 degrees
hue = (-Math.PI * 33) / 180;
decoderMatrix = new Matrix3(
1,
0,
0,
0,
Math.cos(hue),
-Math.sin(hue),
0,
Math.sin(hue),
Math.cos(hue)
).mul(decoderMatrix);
// CXA2025AS decoder matrix
decoderMatrix = new Matrix3(
1,
1,
1,
1.630,
-0.378,
-1.089,
0.317,
-0.466,
1.677
).mul(decoderMatrix);
break;
default:
throw new Error(`unknown videoDecoder: ${this.display.videoDecoder}`);
}
// Brightness
const brightness = this.display.videoBrightness - this.imageBlackLevel;
let decoderOffset;
if (isCompositeDecoder)
decoderOffset = decoderMatrix.mul(
new Matrix3(brightness, 0, 0, 0, 0, 0, 0, 0, 0)
);
else
decoderOffset = decoderMatrix.mul(
new Matrix3(brightness, 0, 0, brightness, 0, 0, brightness, 0, 0)
);
gl.uniform3f(
gl.getUniformLocation(renderShader, "decoderOffset"),
decoderOffset.at(0, 0),
decoderOffset.at(0, 1),
decoderOffset.at(0, 2)
);
// Contrast
let contrast = this.display.videoContrast;
const videoLevel = this.imageWhiteLevel - this.imageBlackLevel;
if (videoLevel > 0) contrast /= videoLevel;
else contrast = 0;
if (contrast < 0) contrast = 0;
decoderMatrix = decoderMatrix.mul(contrast);
gl.uniformMatrix3fv(
gl.getUniformLocation(renderShader, "decoderMatrix"),
false,
decoderMatrix.data
);
// Display shader
gl.useProgram(displayShader);
// Barrel
gl.uniform1f(
gl.getUniformLocation(displayShader, "barrel"),
this.display.displayBarrel
);
// Shadow mask
gl.uniform1i(gl.getUniformLocation(displayShader, "shadowMask"), 1);
gl.uniform1f(
gl.getUniformLocation(displayShader, "shadowMaskLevel"),
this.display.displayShadowMaskLevel
);
// Persistence
const frameRate = 60;
gl.uniform1f(
gl.getUniformLocation(displayShader, "persistenceLevel"),
this.display.displayPersistence /
(1.0 / frameRate + this.display.displayPersistence)
);
if (this.display.displayPersistence == 0)
this.resizeTexture("IMAGE_PERSISTENCE", 0, 0);
// Center lighting
let centerLighting = this.display.displayCenterLighting;
if (Math.abs(centerLighting) < 0.001) centerLighting = 0.001;
gl.uniform1f(
gl.getUniformLocation(displayShader, "centerLighting"),
1.0 / centerLighting - 1
);
// Luminance gain
gl.uniform1f(
gl.getUniformLocation(displayShader, "luminanceGain"),
this.display.displayLuminanceGain
);
}
renderImage() {
const gl = this.gl;
const [renderShader, renderShaderName] = this.getRenderShader();
const isCompositeDecoder = renderShaderName == "COMPOSITE";
gl.useProgram(renderShader);
const texSize = this.textures["IMAGE_IN"].size;
this.resizeTexture("IMAGE_DECODED", texSize.width, texSize.height);
gl.uniform1i(gl.getUniformLocation(renderShader, "texture"), 0);
gl.uniform2f(
gl.getUniformLocation(renderShader, "textureSize"),
texSize.width,
texSize.height
);
if (isCompositeDecoder) {
gl.uniform1i(gl.getUniformLocation(renderShader, "phaseInfo"), 1);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, this.textures["IMAGE_PHASEINFO"].glTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.activeTexture(gl.TEXTURE0);
}
// Render to the back buffer, to avoid using FBOs
// (support for vanilla OpenGL 2.0 cards)
// I think webgl is rendering to the back buffer anyway, until
// we stop executing javascript statements and give control
// back, at which point it flips. So we might not need
// this. Although truly, I'm not certain what it's doing. If we
// *do* end up needing it, we'll have to go full webgl2.
// glReadBuffer(GL_BACK);
const imageSize = this.image.size;
for (let y = 0; y < this.image.height; y += this.viewportSize.height) {
for (let x = 0; x < this.image.width; x += this.viewportSize.width) {
// Calculate rects
const clipSize = new Size(
this.viewportSize.width,
this.viewportSize.height
);
if (x + clipSize.width > imageSize.width)
clipSize.width = imageSize.width - x;
if (y + clipSize.height > imageSize.height)
clipSize.height = imageSize.height - y;
const textureRect = new Rect(
x / texSize.width,
y / texSize.height,
clipSize.width / texSize.width,
clipSize.height / texSize.height
);
const canvasRect = new Rect(
-1,
-1,
(2 * clipSize.width) / this.viewportSize.width,
(2 * clipSize.height) / this.viewportSize.height
);
// Render
gl.bindTexture(gl.TEXTURE_2D, this.textures["IMAGE_IN"].glTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// What is this, some ancient fixed pipeline nonsense? No way.
// glLoadIdentity();
this.drawRectangle(renderShader, canvasRect, textureRect);
// Copy framebuffer
gl.bindTexture(gl.TEXTURE_2D, this.textures["IMAGE_DECODED"].glTexture);
gl.copyTexSubImage2D(
gl.TEXTURE_2D,
0,
x,
y,
0,
0,
clipSize.width,
clipSize.height
);
}
}
}
drawRectangle(shader, posRect, texRect, texRect2) {
const gl = this.gl;
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const positionLocation = gl.getAttribLocation(shader, "a_position");
const texcoordLocations = [gl.getAttribLocation(shader, "a_texCoord")];
const texRects = [texRect];
if (texRect2) {
texcoordLocations.push(gl.getAttribLocation(shader, "a_texCoord2"));
texRects.push(texRect2);
}
const positionBuffer = this.buffers[0];
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const p_x1 = posRect.l;
const p_x2 = posRect.r;
const p_y1 = posRect.t;
const p_y2 = posRect.b;
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
p_x1,
p_y1,
p_x2,
p_y1,
p_x1,
p_y2,
p_x1,
p_y2,
p_x2,
p_y1,
p_x2,
p_y2
]),
gl.STATIC_DRAW
);
gl.enableVertexAttribArray(positionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
for (let i = 0; i < texRects.length; i++) {
const texcoordBuffer = this.buffers[i + 1];
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
const t_x1 = texRects[i].l;
const t_x2 = texRects[i].r;
const t_y1 = texRects[i].t;
const t_y2 = texRects[i].b;
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
t_x1,
t_y1,
t_x2,
t_y1,
t_x1,
t_y2,
t_x1,
t_y2,
t_x2,
t_y1,
t_x2,
t_y2
]),
gl.STATIC_DRAW
);
gl.enableVertexAttribArray(texcoordLocations[i]);
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
gl.vertexAttribPointer(texcoordLocations[i], 2, gl.FLOAT, false, 0, 0);
}
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
drawDisplayCanvas() {
const gl = this.gl;
const displayShader = this.shaders["DISPLAY"];
// Clear
// (Moved inside drawRectangle)
// gl.clearColor(0, 0, 0, 1);
// gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
if (this.image.width == 0 || this.image.height == 0) {
this.resizeTexture("IMAGE_PERSISTENCE", 0, 0);
return;
}
// Grab common variables
const displayResolution = this.display.displayResolution;
// Vertex rect
const vertexRect = new Rect(-1, -1, 2, 2);
const viewportAspectRatio = this.viewportSize.ratio;
const displayAspectRatio = displayResolution.ratio;
const ratio = viewportAspectRatio / displayAspectRatio;
if (ratio > 1) {
vertexRect.origin.x /= ratio;
vertexRect.size.width /= ratio;
} else {
vertexRect.origin.y *= ratio;
vertexRect.size.height *= ratio;
}
// Base texture rect
const baseTexRect = new Rect(0, 0, 1, 1);
// Canvas texture rect
const interlaceShift = this.image.interlace / this.image.height;
const canvasTexLowerLeft = this.getDisplayCanvasTexPoint(
new Point(-1, -1 + 2 * interlaceShift)
);
const canvasTexUpperRight = this.getDisplayCanvasTexPoint(
new Point(1, 1 + 2 * interlaceShift)
);
const canvasTexRect = new Rect(
canvasTexLowerLeft.x,
canvasTexLowerLeft.y,
canvasTexUpperRight.x - canvasTexLowerLeft.x,
canvasTexUpperRight.y - canvasTexLowerLeft.y
);
const canvasSize = new Size(
0.5 * this.viewportSize.width * vertexRect.size.width,
0.5 * this.viewportSize.height * vertexRect.size.height
);
const canvasVideoSize = new Size(
canvasSize.width * this.display.videoSize.width,
canvasSize.height * this.display.videoSize.height
);
let barrelTexRect;
// Render
const texture = this.textures["IMAGE_DECODED"];
// Set uniforms
gl.useProgram(displayShader);
// Texture
const texSize = texture.size;
gl.uniform1i(gl.getUniformLocation(displayShader, "texture"), 0);
gl.uniform2f(
gl.getUniformLocation(displayShader, "textureSize"),
texSize.width,
texSize.height
);
// Barrel
barrelTexRect = new Rect(
-0.5,
-0.5 / displayAspectRatio,
1.0,
1.0 / displayAspectRatio
);
gl.uniform2f(
gl.getUniformLocation(displayShader, "barrelSize"),
1,
1.0 / displayAspectRatio
);
// Scanlines
const scanlineHeight = canvasVideoSize.height / this.image.height;
let scanlineLevel = this.display.displayScanlineLevel;
scanlineLevel =
scanlineHeight > 2.5
? scanlineLevel
: scanlineHeight < 2
? 0
: ((scanlineHeight - 2) / (2.5 - 2)) * scanlineLevel;
gl.uniform1f(
gl.getUniformLocation(displayShader, "scanlineLevel"),
scanlineLevel
);
// Shadow mask
let shadowMaskTexture;
let shadowMaskAspectRatio;
switch (this.display.displayShadowMask) {
case "SHADOWMASK_TRIAD":
shadowMaskTexture = this.textures["SHADOWMASK_TRIAD"];
shadowMaskAspectRatio = 2 / (274.0 / 240.0);
break;
case "SHADOWMASK_INLINE":
shadowMaskTexture = this.textures["SHADOWMASK_INLINE"];
shadowMaskAspectRatio = 2;
break;
case "SHADOWMASK_APERTURE":
shadowMaskTexture = this.textures["SHADOWMASK_APERTURE"];
shadowMaskAspectRatio = 2;
break;
case "SHADOWMASK_LCD":
shadowMaskTexture = this.textures["SHADOWMASK_LCD"];
shadowMaskAspectRatio = 2;
break;
case "SHADOWMASK_BAYER":
shadowMaskTexture = this.textures["SHADOWMASK_BAYER"];
shadowMaskAspectRatio = 2;
break;
}
let shadowMaskDotPitch = this.display.displayShadowMaskDotPitch;
if (shadowMaskDotPitch <= 0.001) shadowMaskDotPitch = 0.001;
const shadowMaskElemX =
((displayResolution.width / this.display.displayPixelDensity) *
25.4 *
0.5) /
shadowMaskDotPitch;
const shadowMaskSize = new Size(
shadowMaskElemX,
(shadowMaskElemX * shadowMaskAspectRatio) / displayAspectRatio
);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, shadowMaskTexture.glTexture);
gl.uniform2f(
gl.getUniformLocation(displayShader, "shadowMaskSize"),
shadowMaskSize.width,
shadowMaskSize.height
);
// Persistence
gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, this.textures["IMAGE_PERSISTENCE"].glTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.activeTexture(gl.TEXTURE0);
gl.uniform1i(gl.getUniformLocation(displayShader, "persistence"), 2);
gl.uniform2f(
gl.getUniformLocation(displayShader, "persistenceOrigin"),
this.persistenceTexRect.x,
this.persistenceTexRect.y
);
gl.uniform2f(
gl.getUniformLocation(displayShader, "persistenceSize"),
this.persistenceTexRect.width,
this.persistenceTexRect.height
);
// Old fixed pipeline stuff.
// gl.loadIdentity();
// gl.rotatef(180, 1, 0, 0);
gl.bindTexture(gl.TEXTURE_2D, texture.glTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
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);
// Render
this.drawRectangle(displayShader, vertexRect, canvasTexRect, baseTexRect);
// TODO(zellyn): implement
/*
if (displayConfiguration.displayPersistence != 0.0)
{
updateTextureSize(OPENGLCANVAS_IMAGE_PERSISTENCE, viewportSize);
gl.bindTexture(gl.TEXTURE_2D, texture[OPENGLCANVAS_IMAGE_PERSISTENCE]);
gl.readBuffer(gl.BACK);
gl.copyTexSubImage2D(gl.TEXTURE_2D, 0,
0, 0, 0, 0,
viewportSize.width, viewportSize.height);
OESize persistenceTexSize = OEMakeSize(viewportSize.width /
textureSize[OPENGLCANVAS_IMAGE_PERSISTENCE].width,
viewportSize.height /
textureSize[OPENGLCANVAS_IMAGE_PERSISTENCE].height);
persistenceTexRect = OEMakeRect((vertexRect.origin.x + 1) * 0.5F * persistenceTexSize.width,
(vertexRect.origin.y + 1) * 0.5F * persistenceTexSize.height,
vertexRect.size.width * 0.5F * persistenceTexSize.width,
vertexRect.size.height * 0.5F * persistenceTexSize.height);
persistenceTexRect.origin.y += persistenceTexRect.size.height;
persistenceTexRect.size.height = -persistenceTexRect.size.height;
}
*/
}
getDisplayCanvasTexPoint(p) {
const videoCenter = this.display.videoCenter;
const videoSize = this.display.videoSize;
p = new Point(
(p.x - 2 * videoCenter.x) / videoSize.width,
(p.y - 2 * videoCenter.y) / videoSize.height
);
const imageSize = this.image.size;
const texSize = this.textures["IMAGE_IN"].size;
p.x = ((p.x + 1) * 0.5 * imageSize.width) / texSize.width;
p.y = ((p.y + 1) * 0.5 * imageSize.height) / texSize.height;
return p;
}
// Resize the texture with the given name to the next
// highest power of two width and height. Wouldn't be
// necessary with webgl2.
resizeTexture(name, width, height, luminance = false) {
const gl = this.gl;
const texInfo = this.textures[name];
if (!texInfo) {
throw new Error(`Cannot find texture named ${name}`);
}
if (width < 4) width = 4;
if (height < 4) height = 4;
width = 2 ** Math.ceil(Math.log2(width));
height = 2 ** Math.ceil(Math.log2(height));
if (texInfo.width != width || texInfo.height != height) {
texInfo.width = width;
texInfo.height = height;
gl.bindTexture(gl.TEXTURE_2D, texInfo.glTexture);
const length = width * height * (luminance ? 1 : 4);
const dummy = new Uint8Array(length);
const type = luminance ? gl.LUMINANCE : gl.RGBA;
gl.texImage2D(
gl.TEXTURE_2D,
0,
type,
width,
height,
0,
type,
gl.UNSIGNED_BYTE,
dummy
);
}
}
}