尽管禁用 mipmaping 和半像素校正,但纹理渗色
Texture Bleeding despite disable mipmaping and half pixel correction
我已经应用了这个答案 https://gamedev.stackexchange.com/questions/46963/how-to-avoid-texture-bleeding-in-a-texture-atlas 中给出的两个必要步骤,但我仍然出现纹理渗色。
我有一个在边界处填充了纯色的图集:x y w h: 0 0 32 32, 0 32 32 32, 0 64 32 32, 0 32 * 3 32 32
我想使用 webgl 显示这些帧中的每一帧,而不会出现纹理渗色,只显示纯色。
我已经禁用了 mipmaping:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
//gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
我应用了半像素校正:
const uvs = (src, frame) => {
const tw = src.width,
th = src.height;
const getTexelCoords = (x, y) => {
return [(x + 0.5) / tw, (y + 0.5) / th];
};
let frameLeft = frame[0],
frameRight = frame[0] + frame[2],
frameTop = frame[1],
frameBottom = frame[1] + frame[3];
let p0 = getTexelCoords(frameLeft, frameTop),
p1 = getTexelCoords(frameRight, frameTop),
p2 = getTexelCoords(frameRight, frameBottom),
p3 = getTexelCoords(frameLeft, frameBottom);
return [
p0[0], p0[1],
p1[0], p1[1],
p3[0], p3[1],
p2[0], p2[1]
];
};
但我仍然出现纹理渗色。起初我尝试使用 pixi.js,但我也有纹理渗出,然后我尝试使用 vanilla js。
我已经通过更改这些行解决了这个问题:
let frameLeft = frame[0],
frameRight = frame[0] + frame[2] - 1,
frameTop = frame[1],
frameBottom = frame[1] + frame[3] - 1;
如您所见,我从右侧和底部边缘减去 1。以前这些索引是 32,这意味着另一帧的开始,它必须是 31。我不知道这是否是正确的解决方案。
您的解决方案是正确的。
假设我们有一个 4x2 的纹理和两个 2x2 像素的精灵
+-------+-------+-------+-------+
| | | | |
| E | F | G | H |
| | | | |
+-------+-------+-------+-------+
| | | | |
| A | B | C | D |
| | | | |
+-------+-------+-------+-------+
字母代表纹理中像素的中心。
(pixelCoord + 0.5) / textureDimensions
以 A、B、E、F 处的 2x2 精灵为例。如果您的纹理坐标位于 B 和 C 之间的任何位置,那么如果您启用了纹理过滤,则会混合一些 C。
最初您计算的是坐标 A,A + 宽度,其中宽度 = 2。这会引导您从 A 到 C。通过添加 -1,您只得到 A 到 B。
不幸的是,您遇到了一个新问题,即您只显示了 A 和 B 的一半。您可以通过填充精灵来解决这个问题。例如,将其设置为 6x2,像素之间的像素重复
+-------+-------+-------+-------+-------+-------+
| | | | | | |
| E | F | Fr | Gr | G | H |
| | | | | | |
+-------+-------+-------+-------+-------+-------+
| | | | | | |
| A | B | Br | Cr | C | D |
| | | | | | |
+-------+-------+-------+-------+-------+-------+
上面Br是B重复,Cr是C重复。将 repeat 设置为 gl.CLAMP_TO_EDGE
将为您重复 A 和 D。现在你可以使用边缘了。
Sprite CDGH 的坐标是
p0 = 4 / texWidth
p1 = 0 / texHeigth
p2 = (4 + spriteWidth) / texWidth
p3 = (0 + spriteHeigth) / texHeight
查看差异的最佳方法是使用两种技术(未填充和填充)绘制 2 个大精灵。
const numSprites = 3;
const spriteSize = 16;
const m4 = twgl.m4;
const gl = document.querySelector('canvas').getContext('webgl');
const spriteElem = makeSprites();
log('sprites');
document.body.appendChild(spriteElem);
const paddedSpriteElem = padSprites(spriteElem);
log('padded sprites');
document.body.appendChild(paddedSpriteElem);
const unpaddedTex = twgl.createTexture(gl, {src: spriteElem, wrap: gl.CLAMP_TO_EDGE, minMax: gl.LINEAR});
const paddedTex = twgl.createTexture(gl, {src: paddedSpriteElem, wrap: gl.CLAMP_TO_EDGE, minMax: gl.LINEAR});
const vs = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
uniform mat4 matrix;
void main() {
gl_Position = matrix * position;
v_texcoord = texcoord;
}
`;
const fs = `
precision highp float;
varying vec2 v_texcoord;
uniform sampler2D tex;
void main() {
gl_FragColor = texture2D(tex, v_texcoord);
}
`;
const program = twgl.createProgram(gl, [vs, fs]);
gl.useProgram(program);
const ploc = gl.getAttribLocation(program, 'position');
const tloc = gl.getAttribLocation(program, 'texcoord');
const mloc = gl.getUniformLocation(program, 'matrix');
// no need to look up 'tex' as we're only using 1 texture and uniforms default
// to 0 so it will use texture unit 0, the default
const pbuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, pbuf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 1, 1, 1, 0, 0, 1, 0]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(ploc);
gl.vertexAttribPointer(ploc, 2, gl.FLOAT, false, 0, 0);
const tbuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, tbuf);
gl.enableVertexAttribArray(tloc);
gl.vertexAttribPointer(tloc, 2, gl.FLOAT, false, 0, 0);
const ibuf = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuf);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array([0, 1, 2, 2, 1, 3]), gl.STATIC_DRAW);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// un paddded using rect from centers
{
const spriteId = 1;
const texWidth = spriteElem.width;
const texHeight = spriteElem.height;
const x = spriteId * spriteSize;
const y = 0;
const p0x = (x + 0.5) / texWidth;
const p0y = (y + 0.5) / texHeight;
const p1x = (x + spriteSize - 1 + 0.5) / texWidth;
const p1y = (y + spriteSize - 1 + 0.5) / texHeight;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
p0x, p0y,
p1x, p0y,
p0x, p1y,
p1x, p1y,
]), gl.STATIC_DRAW);
gl.bindTexture(gl.TEXTURE_2D, unpaddedTex);
let m = m4.ortho(0, gl.canvas.width, 0, gl.canvas.height, -1, 1);
m = m4.translate(m, [2, 5, 0]);
m = m4.scale(m, [96, 96, 1]);
gl.uniformMatrix4fv(mloc, false, m);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);
}
// paddded using rect from edges
{
const spriteId = 1;
const texWidth = paddedSpriteElem.width;
const texHeight = paddedSpriteElem.height;
const x = spriteId * (spriteSize + 2);
const y = 0;
const p0x = (x) / texWidth;
const p0y = (y) / texHeight;
const p1x = (x + spriteSize) / texWidth;
const p1y = (y + spriteSize) / texHeight;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
p0x, p0y,
p1x, p0y,
p0x, p1y,
p1x, p1y,
]), gl.STATIC_DRAW);
gl.bindTexture(gl.TEXTURE_2D, paddedTex);
let m = m4.ortho(0, gl.canvas.width, 0, gl.canvas.height, -1, 1);
m = m4.translate(m, [102, 5, 0]);
m = m4.scale(m, [96, 96, 1]);
gl.uniformMatrix4fv(mloc, false, m);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);
}
// unpaddded using rect from edges (bleeding)
{
const spriteId = 1;
const texWidth = spriteElem.width;
const texHeight = spriteElem.height;
const x = spriteId * spriteSize;
const y = 0;
const p0x = (x) / texWidth;
const p0y = (y) / texHeight;
const p1x = (x + spriteSize) / texWidth;
const p1y = (y + spriteSize) / texHeight;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
p0x, p0y,
p1x, p0y,
p0x, p1y,
p1x, p1y,
]), gl.STATIC_DRAW);
gl.bindTexture(gl.TEXTURE_2D, unpaddedTex);
let m = m4.ortho(0, gl.canvas.width, 0, gl.canvas.height, -1, 1);
m = m4.translate(m, [202, 5, 0]);
m = m4.scale(m, [96, 96, 1]);
gl.uniformMatrix4fv(mloc, false, m);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);
}
function padSprites(elem) {
const canvas = document.createElement('canvas');
canvas.className = 'zoom';
canvas.width = numSprites * spriteSize + (2 * numSprites - 1);
canvas.height = spriteSize;
const ctx = canvas.getContext('2d');
let dstX = 0;
const offsets = [
// corners
[-1, -1],
[ 1, -1],
[-1, 1],
[ 1, 1],
// edges
[-1, 0],
[ 1, 0],
[ 0, -1],
[ 0, 1],
// middle
[ 0, 0],
];
for (let i = 0; i < numSprites; ++i) {
const srcX = i * spriteSize;
for (const offset of offsets) {
ctx.drawImage(
elem,
srcX, 0, spriteSize, spriteSize,
dstX + offset[0], offset[1], spriteSize, spriteSize,
);
}
dstX += spriteSize + 2;
}
return canvas;
}
function makeSprites() {
const canvas = document.createElement('canvas');
canvas.width = numSprites * spriteSize;
canvas.height = spriteSize;
canvas.className = 'zoom';
const ctx = canvas.getContext('2d');
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (let i = 0; i < numSprites; ++i) {
const x = spriteSize * i;
const h = i / numSprites;
ctx.fillStyle = hsl(h, 1, 0.4);
ctx.fillRect(x, 0, spriteSize, spriteSize);
ctx.fillStyle = hsl(h, 1, 0.85);
for (let j = 0; j < spriteSize; j += 4) {
ctx.fillRect(x + j, 0, 2, 2);
ctx.fillRect(x, j, 2, 2);
ctx.fillRect(x + j, spriteSize - 2, 2, 2);
ctx.fillRect(x + spriteSize - 2, j, 2, 2);
}
ctx.fillStyle = hsl(h + 0.5, 1, 0.5);
ctx.fillRect(x + 1, 1, spriteSize - 2, spriteSize - 2);
ctx.fillStyle = hsl(h, 1, 0.5);
ctx.fillText(String.fromCharCode(65 + i), x + spriteSize / 2, spriteSize / 2);
}
return canvas;
}
function hsl(h, s, l) {
return `hsl(${h * 360},${s * 100}%,${l * 100}%)`;
}
function log(...args) {
const elem = document.createElement('pre');
elem.textContent = [...args].join(' ');
document.body.appendChild(elem);
}
canvas {
display: block;
image-rendering: pixelated;
padding: 5px;
}
.zoom {
zoom: 4;
}
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<pre>left : rect from centers using unpadded texture
middle: rect from edges using padded texture
right : rect from edges using unpadded texture (bleeding)
(note the red at the bottom left edge)</pre>
<canvas></canvas>
我已经应用了这个答案 https://gamedev.stackexchange.com/questions/46963/how-to-avoid-texture-bleeding-in-a-texture-atlas 中给出的两个必要步骤,但我仍然出现纹理渗色。
我有一个在边界处填充了纯色的图集:x y w h: 0 0 32 32, 0 32 32 32, 0 64 32 32, 0 32 * 3 32 32
我想使用 webgl 显示这些帧中的每一帧,而不会出现纹理渗色,只显示纯色。
我已经禁用了 mipmaping:
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
//gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
我应用了半像素校正:
const uvs = (src, frame) => {
const tw = src.width,
th = src.height;
const getTexelCoords = (x, y) => {
return [(x + 0.5) / tw, (y + 0.5) / th];
};
let frameLeft = frame[0],
frameRight = frame[0] + frame[2],
frameTop = frame[1],
frameBottom = frame[1] + frame[3];
let p0 = getTexelCoords(frameLeft, frameTop),
p1 = getTexelCoords(frameRight, frameTop),
p2 = getTexelCoords(frameRight, frameBottom),
p3 = getTexelCoords(frameLeft, frameBottom);
return [
p0[0], p0[1],
p1[0], p1[1],
p3[0], p3[1],
p2[0], p2[1]
];
};
但我仍然出现纹理渗色。起初我尝试使用 pixi.js,但我也有纹理渗出,然后我尝试使用 vanilla js。
我已经通过更改这些行解决了这个问题:
let frameLeft = frame[0],
frameRight = frame[0] + frame[2] - 1,
frameTop = frame[1],
frameBottom = frame[1] + frame[3] - 1;
如您所见,我从右侧和底部边缘减去 1。以前这些索引是 32,这意味着另一帧的开始,它必须是 31。我不知道这是否是正确的解决方案。
您的解决方案是正确的。
假设我们有一个 4x2 的纹理和两个 2x2 像素的精灵
+-------+-------+-------+-------+
| | | | |
| E | F | G | H |
| | | | |
+-------+-------+-------+-------+
| | | | |
| A | B | C | D |
| | | | |
+-------+-------+-------+-------+
字母代表纹理中像素的中心。
(pixelCoord + 0.5) / textureDimensions
以 A、B、E、F 处的 2x2 精灵为例。如果您的纹理坐标位于 B 和 C 之间的任何位置,那么如果您启用了纹理过滤,则会混合一些 C。
最初您计算的是坐标 A,A + 宽度,其中宽度 = 2。这会引导您从 A 到 C。通过添加 -1,您只得到 A 到 B。
不幸的是,您遇到了一个新问题,即您只显示了 A 和 B 的一半。您可以通过填充精灵来解决这个问题。例如,将其设置为 6x2,像素之间的像素重复
+-------+-------+-------+-------+-------+-------+
| | | | | | |
| E | F | Fr | Gr | G | H |
| | | | | | |
+-------+-------+-------+-------+-------+-------+
| | | | | | |
| A | B | Br | Cr | C | D |
| | | | | | |
+-------+-------+-------+-------+-------+-------+
上面Br是B重复,Cr是C重复。将 repeat 设置为 gl.CLAMP_TO_EDGE
将为您重复 A 和 D。现在你可以使用边缘了。
Sprite CDGH 的坐标是
p0 = 4 / texWidth
p1 = 0 / texHeigth
p2 = (4 + spriteWidth) / texWidth
p3 = (0 + spriteHeigth) / texHeight
查看差异的最佳方法是使用两种技术(未填充和填充)绘制 2 个大精灵。
const numSprites = 3;
const spriteSize = 16;
const m4 = twgl.m4;
const gl = document.querySelector('canvas').getContext('webgl');
const spriteElem = makeSprites();
log('sprites');
document.body.appendChild(spriteElem);
const paddedSpriteElem = padSprites(spriteElem);
log('padded sprites');
document.body.appendChild(paddedSpriteElem);
const unpaddedTex = twgl.createTexture(gl, {src: spriteElem, wrap: gl.CLAMP_TO_EDGE, minMax: gl.LINEAR});
const paddedTex = twgl.createTexture(gl, {src: paddedSpriteElem, wrap: gl.CLAMP_TO_EDGE, minMax: gl.LINEAR});
const vs = `
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
uniform mat4 matrix;
void main() {
gl_Position = matrix * position;
v_texcoord = texcoord;
}
`;
const fs = `
precision highp float;
varying vec2 v_texcoord;
uniform sampler2D tex;
void main() {
gl_FragColor = texture2D(tex, v_texcoord);
}
`;
const program = twgl.createProgram(gl, [vs, fs]);
gl.useProgram(program);
const ploc = gl.getAttribLocation(program, 'position');
const tloc = gl.getAttribLocation(program, 'texcoord');
const mloc = gl.getUniformLocation(program, 'matrix');
// no need to look up 'tex' as we're only using 1 texture and uniforms default
// to 0 so it will use texture unit 0, the default
const pbuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, pbuf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 1, 1, 1, 0, 0, 1, 0]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(ploc);
gl.vertexAttribPointer(ploc, 2, gl.FLOAT, false, 0, 0);
const tbuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, tbuf);
gl.enableVertexAttribArray(tloc);
gl.vertexAttribPointer(tloc, 2, gl.FLOAT, false, 0, 0);
const ibuf = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibuf);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array([0, 1, 2, 2, 1, 3]), gl.STATIC_DRAW);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// un paddded using rect from centers
{
const spriteId = 1;
const texWidth = spriteElem.width;
const texHeight = spriteElem.height;
const x = spriteId * spriteSize;
const y = 0;
const p0x = (x + 0.5) / texWidth;
const p0y = (y + 0.5) / texHeight;
const p1x = (x + spriteSize - 1 + 0.5) / texWidth;
const p1y = (y + spriteSize - 1 + 0.5) / texHeight;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
p0x, p0y,
p1x, p0y,
p0x, p1y,
p1x, p1y,
]), gl.STATIC_DRAW);
gl.bindTexture(gl.TEXTURE_2D, unpaddedTex);
let m = m4.ortho(0, gl.canvas.width, 0, gl.canvas.height, -1, 1);
m = m4.translate(m, [2, 5, 0]);
m = m4.scale(m, [96, 96, 1]);
gl.uniformMatrix4fv(mloc, false, m);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);
}
// paddded using rect from edges
{
const spriteId = 1;
const texWidth = paddedSpriteElem.width;
const texHeight = paddedSpriteElem.height;
const x = spriteId * (spriteSize + 2);
const y = 0;
const p0x = (x) / texWidth;
const p0y = (y) / texHeight;
const p1x = (x + spriteSize) / texWidth;
const p1y = (y + spriteSize) / texHeight;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
p0x, p0y,
p1x, p0y,
p0x, p1y,
p1x, p1y,
]), gl.STATIC_DRAW);
gl.bindTexture(gl.TEXTURE_2D, paddedTex);
let m = m4.ortho(0, gl.canvas.width, 0, gl.canvas.height, -1, 1);
m = m4.translate(m, [102, 5, 0]);
m = m4.scale(m, [96, 96, 1]);
gl.uniformMatrix4fv(mloc, false, m);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);
}
// unpaddded using rect from edges (bleeding)
{
const spriteId = 1;
const texWidth = spriteElem.width;
const texHeight = spriteElem.height;
const x = spriteId * spriteSize;
const y = 0;
const p0x = (x) / texWidth;
const p0y = (y) / texHeight;
const p1x = (x + spriteSize) / texWidth;
const p1y = (y + spriteSize) / texHeight;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
p0x, p0y,
p1x, p0y,
p0x, p1y,
p1x, p1y,
]), gl.STATIC_DRAW);
gl.bindTexture(gl.TEXTURE_2D, unpaddedTex);
let m = m4.ortho(0, gl.canvas.width, 0, gl.canvas.height, -1, 1);
m = m4.translate(m, [202, 5, 0]);
m = m4.scale(m, [96, 96, 1]);
gl.uniformMatrix4fv(mloc, false, m);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0);
}
function padSprites(elem) {
const canvas = document.createElement('canvas');
canvas.className = 'zoom';
canvas.width = numSprites * spriteSize + (2 * numSprites - 1);
canvas.height = spriteSize;
const ctx = canvas.getContext('2d');
let dstX = 0;
const offsets = [
// corners
[-1, -1],
[ 1, -1],
[-1, 1],
[ 1, 1],
// edges
[-1, 0],
[ 1, 0],
[ 0, -1],
[ 0, 1],
// middle
[ 0, 0],
];
for (let i = 0; i < numSprites; ++i) {
const srcX = i * spriteSize;
for (const offset of offsets) {
ctx.drawImage(
elem,
srcX, 0, spriteSize, spriteSize,
dstX + offset[0], offset[1], spriteSize, spriteSize,
);
}
dstX += spriteSize + 2;
}
return canvas;
}
function makeSprites() {
const canvas = document.createElement('canvas');
canvas.width = numSprites * spriteSize;
canvas.height = spriteSize;
canvas.className = 'zoom';
const ctx = canvas.getContext('2d');
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (let i = 0; i < numSprites; ++i) {
const x = spriteSize * i;
const h = i / numSprites;
ctx.fillStyle = hsl(h, 1, 0.4);
ctx.fillRect(x, 0, spriteSize, spriteSize);
ctx.fillStyle = hsl(h, 1, 0.85);
for (let j = 0; j < spriteSize; j += 4) {
ctx.fillRect(x + j, 0, 2, 2);
ctx.fillRect(x, j, 2, 2);
ctx.fillRect(x + j, spriteSize - 2, 2, 2);
ctx.fillRect(x + spriteSize - 2, j, 2, 2);
}
ctx.fillStyle = hsl(h + 0.5, 1, 0.5);
ctx.fillRect(x + 1, 1, spriteSize - 2, spriteSize - 2);
ctx.fillStyle = hsl(h, 1, 0.5);
ctx.fillText(String.fromCharCode(65 + i), x + spriteSize / 2, spriteSize / 2);
}
return canvas;
}
function hsl(h, s, l) {
return `hsl(${h * 360},${s * 100}%,${l * 100}%)`;
}
function log(...args) {
const elem = document.createElement('pre');
elem.textContent = [...args].join(' ');
document.body.appendChild(elem);
}
canvas {
display: block;
image-rendering: pixelated;
padding: 5px;
}
.zoom {
zoom: 4;
}
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<pre>left : rect from centers using unpadded texture
middle: rect from edges using padded texture
right : rect from edges using unpadded texture (bleeding)
(note the red at the bottom left edge)</pre>
<canvas></canvas>