我试图在WebGL中使用缩放着色器(特别是super-xbr),但是我无法正确地将数据传递给着色器,因此它进行了正确的插值。我已经将GLSL着色器转换为相应的顶点和片段着色器,并且在我尝试使用它们进行缩放时,它们都运行良好。
这就是我定义gl_Position以获得两倍大的图像(这可能是我犯错的地方):
// Added 2x multiplication to the original code
vec2 zeroToOne = a_position / u_resolution * 2.0;
vec2 zeroToTwo = zeroToOne * 2.0;
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
这可以完成工作,但着色器不会插入图像。这是结果(顶部是具有最近邻居插值的源,底部是程序的结果):
这是近似的预期结果:
程序本身基本上是WebGL2 fundamentals tutorial的副本,它执行基本的矩形绘制。我改变的唯一一件事就是让画布大两倍。
下面的完整代码(在高dpi监视器上可以更好地查看结果)
!(function(window, document, undefined) {
"use strict";
function main() {
var image = document.getElementsByTagName('img')[0]; // MUST BE SAME DOMAIN!!!
image.onload = function() {
render(image);
}
}
function render(image) {
// Get A WebGL context
/** @type {HTMLCanvasElement} */
var canvas = document.getElementById("canvas");
canvas.width = image.naturalWidth * 2;
canvas.height = image.naturalHeight * 2;
canvas.style.width = image.naturalWidth + 'px';
canvas.style.height = image.naturalHeight + 'px';
var gl = canvas.getContext("webgl2");
if (!gl) {
return;
}
// setup GLSL program
var program = webglUtils.createProgramFromScripts(gl, ["2d-vertex-shader", "2d-fragment-shader"]);
// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");
var texcoordLocation = gl.getAttribLocation(program, "a_texCoord");
// Create a buffer to put three 2d clip space points in
var positionBuffer = gl.createBuffer();
// Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Set a rectangle the same size as the image.
setRectangle(gl, 0, 0, image.width, image.height);
// provide texture coordinates for the rectangle.
var texcoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
0.0, 1.0,
1.0, 0.0,
1.0, 1.0,
]), gl.STATIC_DRAW);
// Create a texture.
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set the parameters so we can render any size image.
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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// Upload the image into the texture.
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
// lookup uniforms
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
//webglUtils.resizeCanvasToDisplaySize(gl.canvas);
// Tell WebGL how to convert from clip space to pixels
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Clear the canvas
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Tell it to use our program (pair of shaders)
gl.useProgram(program);
// Turn on the position attribute
gl.enableVertexAttribArray(positionLocation);
// Bind the position buffer.
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
// Tell the position attribute how to get data out of positionBuffer (ARRAY_BUFFER)
var size = 2; // 2 components per iteration
var type = gl.FLOAT; // the data is 32bit floats
var normalize = false; // don't normalize the data
var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
var offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(
positionLocation, size, type, normalize, stride, offset)
// Turn on the texcord attribute
gl.enableVertexAttribArray(texcoordLocation);
// Bind the position buffer.
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
// Tell the position attribute how to get data out of positionBuffer (ARRAY_BUFFER)
var size = 2; // 2 components per iteration
var type = gl.FLOAT; // the data is 32bit floats
var normalize = false; // don't normalize the data
var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
var offset = 0; // start at the beginning of the buffer
gl.vertexAttribPointer(
texcoordLocation, size, type, normalize, stride, offset)
// set the resolution
gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
// Draw the rectangle.
var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 6;
gl.drawArrays(primitiveType, offset, count);
}
function setRectangle(gl, x, y, width, height) {
var x1 = x;
var x2 = x + width;
var y1 = y;
var y2 = y + height;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
x1, y1,
x2, y1,
x1, y2,
x1, y2,
x2, y1,
x2, y2,
]), gl.STATIC_DRAW);
}
document.addEventListener('DOMContentLoaded', main);
})(window, document, undefined);
html, body
{
margin: 0;
padding: 0;
}
body
{
display: flex;
flex-direction: column;
}
.container,
.canvas
{
margin: auto;
}
img
{
max-width: 100%;
height: auto;
image-rendering: pixelated;
}
<script src="https://webgl2fundamentals.org/webgl/resources/webgl-utils.js"></script>
<script id="2d-vertex-shader" type="x-shader/x-vertex">#version 300 es
#ifdef GL_ES
#define COMPAT_PRECISION mediump
#else
#define COMPAT_PRECISION
#endif
#if __VERSION__ >= 130
#define COMPAT_VARYING out
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#else
#define COMPAT_VARYING varying
#define COMPAT_ATTRIBUTE attribute
#define COMPAT_TEXTURE texture2D
#endif
COMPAT_ATTRIBUTE vec2 a_position;
COMPAT_ATTRIBUTE vec2 a_texCoord;
COMPAT_VARYING vec4 TEX0;
COMPAT_VARYING vec4 t1;
COMPAT_VARYING vec4 t2;
COMPAT_VARYING vec4 t3;
COMPAT_VARYING vec4 t4;
COMPAT_VARYING vec4 t5;
COMPAT_VARYING vec4 t6;
COMPAT_VARYING vec4 t7;
uniform COMPAT_PRECISION int FrameDirection;
uniform COMPAT_PRECISION int FrameCount;
uniform COMPAT_PRECISION vec2 OutputSize;
uniform COMPAT_PRECISION vec2 TextureSize;
uniform COMPAT_PRECISION vec2 InputSize;
uniform COMPAT_PRECISION vec2 u_resolution;
// compatibility #defines
#define vTexCoord TEX0.xy
#define SourceSize vec4(TextureSize, 1.0 / TextureSize) //either TextureSize or InputSize
#define OutSize vec4(OutputSize, 1.0 / OutputSize)
void main()
{
//gl_Position = MVPMatrix * VertexCoord;
// convert the rectangle from pixels to 0.0 to 1.0
vec2 zeroToOne = a_position / u_resolution * 2.0;
// convert from 0->1 to 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// convert from 0->2 to -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
TEX0.xy = a_texCoord.xy * 1.0004;
vec2 ps = vec2(1.0/ SourceSize.x, 1.0/ SourceSize.y);
float dx = ps.x;
float dy = ps.y;
// A1 B1 C1
// A0 A B C C4
// D0 D E F F4
// G0 G H I I4
// G5 H5 I5
t1 = vTexCoord.xxxy + vec4( -dx, 0, dx,-2.0*dy); // A1 B1 C1
t2 = vTexCoord.xxxy + vec4( -dx, 0, dx, -dy); // A B C
t3 = vTexCoord.xxxy + vec4( -dx, 0, dx, 0); // D E F
t4 = vTexCoord.xxxy + vec4( -dx, 0, dx, dy); // G H I
t5 = vTexCoord.xxxy + vec4( -dx, 0, dx, 2.0*dy); // G5 H5 I5
t6 = vTexCoord.xyyy + vec4(-2.0*dx,-dy, 0, dy); // A0 D0 G0
t7 = vTexCoord.xyyy + vec4( 2.0*dx,-dy, 0, dy); // C4 F4 I4
}
</script>
<script id="2d-fragment-shader" type="x-shader/x-fragment">#version 300 es
#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
#define COMPAT_PRECISION mediump
#else
#define COMPAT_PRECISION
#endif
#define mul(a,b) (b*a)
// Constants used with gamma correction.
#define InputGamma 2.4
#define OutputGamma 2.2
#define GAMMA_IN(color) pow(color, vec3(InputGamma, InputGamma, InputGamma))
#define GAMMA_OUT(color) pow(color, vec3(1.0 / OutputGamma, 1.0 / OutputGamma, 1.0 / OutputGamma))
#if __VERSION__ >= 130
#define COMPAT_VARYING in
#define COMPAT_TEXTURE texture
out COMPAT_PRECISION vec4 FragColor;
#else
#define COMPAT_VARYING varying
#define FragColor gl_FragColor
#define COMPAT_TEXTURE texture2D
#endif
uniform COMPAT_PRECISION int FrameDirection;
uniform COMPAT_PRECISION int FrameCount;
uniform COMPAT_PRECISION vec2 OutputSize;
uniform COMPAT_PRECISION vec2 TextureSize;
uniform COMPAT_PRECISION vec2 InputSize;
uniform sampler2D Texture;
COMPAT_VARYING vec4 TEX0;
COMPAT_VARYING vec4 t1;
COMPAT_VARYING vec4 t2;
COMPAT_VARYING vec4 t3;
COMPAT_VARYING vec4 t4;
COMPAT_VARYING vec4 t5;
COMPAT_VARYING vec4 t6;
COMPAT_VARYING vec4 t7;
// compatibility #defines
#define Source Texture
#define vTexCoord TEX0.xy
#define texture(c, d) COMPAT_TEXTURE(c, d)
#define SourceSize vec4(TextureSize, 1.0 / TextureSize) //either TextureSize or InputSize
#define OutSize vec4(OutputSize, 1.0 / OutputSize)
const float coef = 2.0;
const vec4 eq_threshold = vec4(15.0, 15.0, 15.0, 15.0);
const float y_weight = 1.0;
const float u_weight = 7.0;
const float v_weight = 6.0;
const mat3x3 yuv = mat3x3(0.299, 0.587, 0.114, -0.169, -0.331, 0.499, 0.499, -0.418, -0.0813);
const mat3x3 yuv_weighted = mat3x3(y_weight*yuv[0], u_weight*yuv[1], v_weight*yuv[2]);
const vec4 delta = vec4(0.5, 0.5, 0.5, 0.5);
const float sharpness = 0.65;
float lum(vec3 A, vec3 B)
{
return abs(dot(A-B, yuv_weighted[0]));
}
vec4 df(vec4 A, vec4 B)
{
return vec4(abs(A-B));
}
bvec4 eq(vec4 A, vec4 B)
{
return lessThan(df(A, B) , vec4(15.0, 15.0, 15.0, 15.0));
}
bvec4 eq2(vec4 A, vec4 B)
{
return lessThan(df(A, B) , vec4(2.0, 2.0, 2.0, 2.0));
}
bvec4 eq3(vec4 A, vec4 B)
{
return lessThan(df(A, B) , vec4(5.0, 5.0, 5.0, 5.0));
}
vec4 weighted_distance(vec4 a, vec4 b, vec4 c, vec4 d, vec4 e, vec4 f, vec4 g, vec4 h)
{
return (df(a,b) + df(a,c) + df(d,e) + df(d,f) + 4.0*df(g,h));
}
bvec4 and(bvec4 A, bvec4 B)
{
return bvec4(A.x && B.x, A.y && B.y, A.z && B.z, A.w && B.w);
}
bvec4 or(bvec4 A, bvec4 B)
{
return bvec4(A.x || B.x, A.y || B.y, A.z || B.z, A.w || B.w);
}
void main()
{
bvec4 edr, edr_left, edr_up, px; // px = pixel, edr = edge detection rule
bvec4 interp_restriction_lv1, interp_restriction_lv2_left, interp_restriction_lv2_up;
bvec4 nc, nc30, nc60, nc45; // new_color
vec4 fx, fx_left, fx_up, final_fx; // inequations of straight lines.
vec2 fp = fract(vTexCoord*SourceSize.xy);
vec3 A1 = texture(Source, t1.xw).rgb;
vec3 B1 = texture(Source, t1.yw).rgb;
vec3 C1 = texture(Source, t1.zw).rgb;
vec3 A = texture(Source, t2.xw).rgb;
vec3 B = texture(Source, t2.yw).rgb;
vec3 C = texture(Source, t2.zw).rgb;
vec3 D = texture(Source, t3.xw).rgb;
vec3 E = texture(Source, t3.yw).rgb;
vec3 F = texture(Source, t3.zw).rgb;
vec3 G = texture(Source, t4.xw).rgb;
vec3 H = texture(Source, t4.yw).rgb;
vec3 I = texture(Source, t4.zw).rgb;
vec3 G5 = texture(Source, t5.xw).rgb;
vec3 H5 = texture(Source, t5.yw).rgb;
vec3 I5 = texture(Source, t5.zw).rgb;
vec3 A0 = texture(Source, t6.xy).rgb;
vec3 D0 = texture(Source, t6.xz).rgb;
vec3 G0 = texture(Source, t6.xw).rgb;
vec3 C4 = texture(Source, t7.xy).rgb;
vec3 F4 = texture(Source, t7.xz).rgb;
vec3 I4 = texture(Source, t7.xw).rgb;
vec4 b = mul( mat4x3(B, D, H, F), yuv_weighted[0] );
vec4 c = mul( mat4x3(C, A, G, I), yuv_weighted[0] );
vec4 e = mul( mat4x3(E, E, E, E), yuv_weighted[0] );
vec4 a = c.yzwx;
vec4 d = b.yzwx;
vec4 f = b.wxyz;
vec4 g = c.zwxy;
vec4 h = b.zwxy;
vec4 i = c.wxyz;
vec4 i4 = mul( mat4x3(I4, C1, A0, G5), yuv_weighted[0] );
vec4 i5 = mul( mat4x3(I5, C4, A1, G0), yuv_weighted[0] );
vec4 h5 = mul( mat4x3(H5, F4, B1, D0), yuv_weighted[0] );
vec4 f4 = h5.yzwx;
vec4 Ao = vec4( 1.0, -1.0, -1.0, 1.0 );
vec4 Bo = vec4( 1.0, 1.0, -1.0,-1.0 );
vec4 Co = vec4( 1.5, 0.5, -0.5, 0.5 );
vec4 Ax = vec4( 1.0, -1.0, -1.0, 1.0 );
vec4 Bx = vec4( 0.5, 2.0, -0.5,-2.0 );
vec4 Cx = vec4( 1.0, 1.0, -0.5, 0.0 );
vec4 Ay = vec4( 1.0, -1.0, -1.0, 1.0 );
vec4 By = vec4( 2.0, 0.5, -2.0,-0.5 );
vec4 Cy = vec4( 2.0, 0.0, -1.0, 0.5 );
// These inequations define the line below which interpolation occurs.
fx = (Ao*fp.y+Bo*fp.x);
fx_left = (Ax*fp.y+Bx*fp.x);
fx_up = (Ay*fp.y+By*fp.x);
//this is the only way I can keep these comparisons straight '-_-
bvec4 block1 = and(not(eq(h,h5)) , not(eq(h,i5)));
bvec4 block2 = and(not(eq(f,f4)) , not(eq(f,i4)));
bvec4 block3 = and(not(eq(h, d)) , not(eq(h, g)));
bvec4 block4 = and(not(eq(f, b)) , not(eq(f, c)));
bvec4 block5 = and(eq(e, i) , or(block2 , block1));
bvec4 block_comp = or(block4 , or(block3 , or(block5 , or(eq(e,g) , eq(e,c)))));
interp_restriction_lv1 = and(notEqual(e,f) , and(notEqual(e,h) , block_comp));
interp_restriction_lv2_left = and(notEqual(e,g) , notEqual(d,g));
interp_restriction_lv2_up = and(notEqual(e,c) , notEqual(b,c));
vec4 fx45 = smoothstep(Co - delta, Co + delta, fx);
vec4 fx30 = smoothstep(Cx - delta, Cx + delta, fx_left);
vec4 fx60 = smoothstep(Cy - delta, Cy + delta, fx_up);
edr = and(lessThan((weighted_distance( e, c, g, i, h5, f4, h, f) + 3.5) , weighted_distance( h, d, i5, f, i4, b, e, i)) , interp_restriction_lv1);
edr_left = and(lessThanEqual((coef*df(f,g)) , df(h,c)) , interp_restriction_lv2_left);
edr_up = and(greaterThanEqual(df(f,g) , (coef*df(h,c))) , interp_restriction_lv2_up);
nc45 = and( edr , bvec4(fx45));
nc30 = and( edr , and(edr_left , bvec4(fx30)));
nc60 = and( edr , and(edr_up , bvec4(fx60)));
px = lessThanEqual(df(e,f) , df(e,h));
vec3 res = E;
vec3 n1, n2, n3, n4, s, aa, bb, cc, dd;
n1 = B1; n2 = B; s = E; n3 = H; n4 = H5;
aa = n2-n1; bb = s-n2; cc = n3-s; dd = n4-n3;
vec3 t = (7. * (bb + cc) - 3. * (aa + dd)) / 16.;
vec3 m;
m.x = (s.x < 0.5) ? 2.*s.x : 2.*(1.0-s.x);
m.y = (s.y < 0.5) ? 2.*s.y : 2.*(1.0-s.y);
m.z = (s.z < 0.5) ? 2.*s.z : 2.*(1.0-s.z);
m = min(m, sharpness*abs(bb));
m = min(m, sharpness*abs(cc));
t = clamp(t, -m, m);
vec3 s1 = (2.*fp.y-1.)*t + s;
n1 = D0; n2 = D; s = s1; n3 = F; n4 = F4;
aa = n2-n1; bb = s-n2; cc = n3-s; dd = n4-n3;
t = (7. * (bb + cc) - 3. * (aa + dd)) / 16.;
m.x = (s.x < 0.5) ? 2.*s.x : 2.*(1.0-s.x);
m.y = (s.y < 0.5) ? 2.*s.y : 2.*(1.0-s.y);
m.z = (s.z < 0.5) ? 2.*s.z : 2.*(1.0-s.z);
m = min(m, sharpness*abs(bb));
m = min(m, sharpness*abs(cc));
t = clamp(t, -m, m);
vec3 s0 = (2.*fp.x-1.)*t + s;
nc = or(nc30 , or(nc60 , nc45));
float blend = 0.0;
vec3 pix;
vec4 r1 = mix(e, f, edr);
bool yeseq3=false;
bvec4 yes;
if ( all(eq3(r1,e)) )
{
yeseq3 = true;
pix = res = s0;
}
else
{
pix = res = E;
}
yes = and(bvec4(yeseq3) , eq2(e, mix(f, h, px)));
vec4 final45 = vec4(nc45) * fx45;
vec4 final30 = vec4(nc30) * fx30;
vec4 final60 = vec4(nc60) * fx60;
vec4 maximo = max(max(final30, final60), final45);
if (nc.x) {pix = px.x ? F : H; blend = maximo.x; if (yes.x) pix = res = s0; else res=E;}
else if (nc.y) {pix = px.y ? B : F; blend = maximo.y; if (yes.y) pix = res = s0; else res=E;}
else if (nc.z) {pix = px.z ? D : B; blend = maximo.z; if (yes.z) pix = res = s0; else res=E;}
else if (nc.w) {pix = px.w ? H : D; blend = maximo.w; if (yes.w) pix = res = s0; else res=E;}
res = GAMMA_IN(res);
pix = GAMMA_IN(pix);
res = mix(res, pix, blend);
FragColor = vec4(clamp( GAMMA_OUT(res), 0.0, 1.0 ), 1.0);
}
</script>
<div class="container">
<img src="data:image/jpeg;base64,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" alt="testimage" />
</div>
<div class="canvas">
<canvas id="canvas"></canvas>
</div>