我有点难以理解这个看似简单的任务,即以最近邻格式升级画布渲染,我在这里问:
How can I properly write this shader function in JS?
目标是转换3D渲染输出,如下所示:
像pixelart这样:
但在这个问题中,我正在询问如何正确实现我选择的解决方案(基本上使用着色器来处理扩展)。也许我应该问:有更简单(并且仍然高效)的方法吗?
答案 0 :(得分:4)
我可以提供两种方法,可以使用最近邻居有效地向上或向下缩放图像。
要手动执行此操作,您应该遍历新缩放图像的每个像素,并使用旧尺寸与新尺寸的比率计算原始像素应使用的像素。
(我的代码片段使用.toDataURL(),因此它们可能无法在chrome中使用。)
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
#input {
display: none;
}
body {
background-color: black;
}
body > * {
display: block;
margin-top: 10px;
margin-left: auto;
margin-right: auto;
}
img {
background-color: gray;
border: solid 1px white;
border-radius: 10px;
image-rendering: optimizeSpeed;
}
label {
transition: 0.1s;
cursor: pointer;
text-align: center;
font-size: 15px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
width: 130px;
height: 40px;
line-height: 40px;
border-radius: 10px;
color: white;
background-color: #005500;
box-shadow: 0px 4px #555555;
}
label:hover {
background-color: #007700;
}
label:active {
box-shadow: 0px 1px #555555;
transform: translateY(3px);
}
script {
display: none;
}
</style>
</head>
<body>
<img id="unscaledImage"></img>
<img id="scaledImage"></img>
<input id="scale" type="range" min="1" max="100" value="50"></input>
<label for="input">Upload Image</label>
<input id="input" type="file"></input>
<script type="application/javascript">
void function() {
"use strict";
var unscaledImage = null;
var scaledImage = null;
var scale = null;
var input = null;
var canvas = null;
var ctx = null;
var hasImage = false;
function scaleImage(img,scale) {
var newWidth = (img.width * scale) | 0;
var newHeight = (img.height * scale) | 0;
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img,0,0);
var unscaledData = ctx.getImageData(0,0,img.width,img.height);
var scaledData = ctx.createImageData(newWidth,newHeight);
var unscaledBitmap = unscaledData.data;
var scaledBitmap = scaledData.data;
var xScale = img.width / newWidth;
var yScale = img.height / newHeight;
for (var x = 0; x < newWidth; ++x) {
for (var y = 0; y < newHeight; ++y) {
var _x = (x * xScale) | 0;
var _y = (y * yScale) | 0;
var scaledIndex = (x + y * newWidth) * 4;
var unscaledIndex = (_x + _y * img.width) * 4;
scaledBitmap[scaledIndex] = unscaledBitmap[unscaledIndex];
scaledBitmap[scaledIndex + 1] = unscaledBitmap[unscaledIndex + 1];
scaledBitmap[scaledIndex + 2] = unscaledBitmap[unscaledIndex + 2];
scaledBitmap[scaledIndex + 3] = 255;
}
}
ctx.clearRect(0,0,canvas.width,canvas.height);
canvas.width = newWidth;
canvas.height = newHeight;
ctx.putImageData(scaledData,0,0);
return canvas.toDataURL();
}
function onImageLoad() {
URL.revokeObjectURL(this.src);
scaledImage.src = scaleImage(this,scale.value * 0.01);
scaledImage.style.width = this.width + "px";
scaledImage.style.height = this.height + "px";
hasImage = true;
}
function onImageError() {
URL.revokeObjectURL(this.src);
}
function onScaleChanged() {
if (hasImage) {
scaledImage.src = scaleImage(unscaledImage,this.value * 0.01);
}
}
function onImageSelected() {
if (this.files[0]) {
unscaledImage.src = URL.createObjectURL(this.files[0]);
}
}
onload = function() {
unscaledImage = document.getElementById("unscaledImage");
scaledImage = document.getElementById("scaledImage");
scale = document.getElementById("scale");
input = document.getElementById("input");
canvas = document.createElement("canvas");
ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = false;
unscaledImage.onload = onImageLoad;
unscaledImage.onerror = onImageError;
scale.onmouseup = onScaleChanged;
input.oninput = onImageSelected;
}
}();
</script>
</body>
</html>
&#13;
另一种使用着色器的方法更快,就是将图像添加到设置为使用最近邻居过滤的纹理并将其绘制到四边形上。在绘制之前,可以通过gl.viewport控制四边形的大小。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
#file {
display: none;
}
body {
background-color: black;
}
body > * {
display: block;
margin-top: 10px;
margin-left: auto;
margin-right: auto;
}
img {
background-color: gray;
border: solid 1px white;
border-radius: 10px;
image-rendering: optimizeSpeed;
}
label {
transition: 0.1s;
cursor: pointer;
text-align: center;
font-size: 15px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
width: 130px;
height: 40px;
line-height: 40px;
border-radius: 10px;
color: white;
background-color: #005500;
box-shadow: 0px 4px #555555;
}
label:hover {
background-color: #007700;
}
label:active {
box-shadow: 0px 1px #555555;
transform: translateY(3px);
}
script {
display: none;
}
</style>
</head>
<body>
<img id="unscaledImage"></img>
<img id="scaledImage"></img>
<input id="scale" type="range" min="1" max="100" value="50"></input>
<input id="file" type="file"></input>
<label for="file">Upload Image</label>
<script type="application/javascript">
void function() {
"use strict";
// DOM
var unscaledImage = document.getElementById("unscaledImage");
var scaledImage = document.getElementById("scaledImage");
var scale = document.getElementById("scale");
var file = document.getElementById("file");
var imageUploaded = false;
function onScaleChanged() {
if (imageUploaded) {
scaledImage.src = scaleOnGPU(this.value * 0.01);
}
}
function onImageLoad() {
URL.revokeObjectURL(this.src);
uploadImageToGPU(this);
scaledImage.src = scaleOnGPU(scale.value * 0.01);
scaledImage.style.width = this.width + "px";
scaledImage.style.height = this.height + "px";
imageUploaded = true;
}
function onImageError() {
URL.revokeObjectURL(this.src);
}
function onImageSubmitted() {
if (this.files[0]) {
unscaledImage.src = URL.createObjectURL(this.files[0]);
}
}
// GL
var canvas = document.createElement("canvas");
var gl = canvas.getContext("webgl",{ preserveDrawingBuffer: true })
var program = null;
var buffer = null;
var texture = null;
function uploadImageToGPU(img) {
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,img);
}
function scaleOnGPU(scale) {
canvas.width = (unscaledImage.width * scale) | 0;
canvas.height = (unscaledImage.height * scale) | 0;
gl.viewport(0,0,canvas.width,canvas.height);
gl.drawArrays(gl.TRIANGLES,0,6);
return canvas.toDataURL();
}
// Entry point
onload = function() {
// DOM setup
unscaledImage.onload = onImageLoad;
unscaledImage.onerror = onImageError;
scale.onmouseup = onScaleChanged;
file.oninput = onImageSubmitted;
// GL setup
// Program (shaders)
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
program = gl.createProgram();
gl.shaderSource(vertexShader,`
precision mediump float;
attribute vec2 aPosition;
attribute vec2 aUV;
varying vec2 vUV;
void main() {
vUV = aUV;
gl_Position = vec4(aPosition,0.0,1.0);
}
`);
gl.shaderSource(fragmentShader,`
precision mediump float;
varying vec2 vUV;
uniform sampler2D uTexture;
void main() {
gl_FragColor = texture2D(uTexture,vUV);
}
`);
gl.compileShader(vertexShader);
gl.compileShader(fragmentShader);
gl.attachShader(program,vertexShader);
gl.attachShader(program,fragmentShader);
gl.linkProgram(program);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
gl.useProgram(program);
// Buffer
buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([
1.0, 1.0, 1.0, 0.0,
-1.0, 1.0, 0.0, 0.0,
-1.0,-1.0, 0.0, 1.0,
1.0, 1.0, 1.0, 0.0,
-1.0,-1.0, 0.0, 1.0,
1.0,-1.0, 1.0, 1.0
]),gl.STATIC_DRAW);
gl.vertexAttribPointer(0,2,gl.FLOAT,gl.FALSE,16,0);
gl.vertexAttribPointer(1,2,gl.FLOAT,gl.FALSE,16,8);
gl.enableVertexAttribArray(0);
gl.enableVertexAttribArray(1);
// Texture
texture = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D,texture);
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);
}
onunload = function() {
gl.deleteProgram(program);
gl.deleteBuffer(buffer);
gl.deleteTexture(texture);
}
}();
</script>
</body>
</html>
&#13;
编辑: 为了更好地说明在实际渲染器中它的外观,我创建了另一个示例,将场景绘制到低分辨率帧缓冲区,然后将其缩放到画布(关键是设置min&amp; ; mag过滤到最近邻居。)
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
background-color: black;
}
.center {
display: block;
margin-top: 30px;
margin-left: auto;
margin-right: auto;
border: solid 1px white;
border-radius: 10px;
}
script {
display: none;
}
</style>
</head>
<body>
<canvas id="canvas" class="center"></canvas>
<input id="scale" type="range" min="1" max="100" value="100" class="center"></input>
<script type="application/javascript">
void function() {
"use strict";
// DOM
var canvasWidth = 180 << 1;
var canvasHeight = 160 << 1;
var canvas = document.getElementById("canvas");
var scale = document.getElementById("scale");
function onScaleChange() {
var scale = this.value * 0.01;
internalWidth = (canvasWidth * scale) | 0;
internalHeight = (canvasHeight * scale) | 0;
gl.uniform1f(uAspectRatio,1.0 / (internalWidth / internalHeight));
gl.deleteFramebuffer(framebuffer);
gl.deleteTexture(framebufferTexture);
[framebuffer,framebufferTexture] = createFramebuffer(internalWidth,internalHeight);
}
// GL
var internalWidth = canvasWidth;
var internalHeight = canvasHeight;
var currentCubeAngle = -0.5;
var gl = canvas.getContext("webgl",{ preserveDrawingBuffer: true, antialias: false }) || console.warn("WebGL Not Supported.");
var cubeProgram = null; // Shaders to draw 3D cube
var scaleProgram = null; // Shaders to scale the frame
var uAspectRatio = null; // Aspect ratio for projection matrix
var uCubeRotation = null; // uniform location for cube program
var cubeBuffer = null; // cube model (attributes)
var scaleBuffer = null; // quad position & UV's
var framebuffer = null; // render target
var framebufferTexture = null; // textured that is rendered to. (The cube is drawn on this)
function createProgram(vertexCode,fragmentCode) {
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(vertexShader,vertexCode);
gl.shaderSource(fragmentShader,fragmentCode);
gl.compileShader(vertexShader);
gl.compileShader(fragmentShader);
try {
if (!gl.getShaderParameter(vertexShader,gl.COMPILE_STATUS)) { throw "VS: " + gl.getShaderInfoLog(vertexShader); }
if (!gl.getShaderParameter(fragmentShader,gl.COMPILE_STATUS)) { throw "FS: " + gl.getShaderInfoLog(fragmentShader); }
} catch(error) {
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
console.error(error);
}
var program = gl.createProgram();
gl.attachShader(program,vertexShader);
gl.attachShader(program,fragmentShader);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
gl.linkProgram(program);
return program;
}
function createBuffer(data) {
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
gl.bufferData(gl.ARRAY_BUFFER,Float32Array.from(data),gl.STATIC_DRAW);
return buffer;
}
function createFramebuffer(width,height) {
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D,texture);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_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.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,width,height,0,gl.RGBA,gl.UNSIGNED_BYTE,null);
var _framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER,_framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,texture,0);
gl.bindTexture(gl.TEXTURE_2D,null);
gl.bindFramebuffer(gl.FRAMEBUFFER,null);
return [_framebuffer,texture];
}
function loop() {
//
currentCubeAngle += 0.01;
if (currentCubeAngle > 2.0 * Math.PI) {
currentCubeAngle = 0.0;
}
//
gl.bindFramebuffer(gl.FRAMEBUFFER,framebuffer);
gl.bindTexture(gl.TEXTURE_2D,null);
gl.viewport(0,0,internalWidth,internalHeight);
gl.useProgram(cubeProgram);
gl.uniform1f(uCubeRotation,currentCubeAngle);
gl.bindBuffer(gl.ARRAY_BUFFER,cubeBuffer);
gl.vertexAttribPointer(0,3,gl.FLOAT,gl.FALSE,36,0);
gl.vertexAttribPointer(1,3,gl.FLOAT,gl.FALSE,36,12);
gl.vertexAttribPointer(2,3,gl.FLOAT,gl.FALSE,36,24);
gl.enableVertexAttribArray(2);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES,0,24);
gl.bindFramebuffer(gl.FRAMEBUFFER,null);
gl.bindTexture(gl.TEXTURE_2D,framebufferTexture);
gl.viewport(0,0,canvasWidth,canvasHeight);
gl.useProgram(scaleProgram);
gl.bindBuffer(gl.ARRAY_BUFFER,scaleBuffer);
gl.vertexAttribPointer(0,2,gl.FLOAT,gl.FALSE,16,0);
gl.vertexAttribPointer(1,2,gl.FLOAT,gl.FALSE,16,8);
gl.disableVertexAttribArray(2);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES,0,6);
//
requestAnimationFrame(loop);
}
// Entry Point
onload = function() {
// DOM
canvas.width = canvasWidth;
canvas.height = canvasHeight;
scale.onmouseup = onScaleChange;
// GL
gl.clearColor(0.5,0.5,0.5,1.0);
gl.enable(gl.CULL_FACE);
gl.enableVertexAttribArray(0);
gl.enableVertexAttribArray(1);
cubeProgram = createProgram(`
precision mediump float;
const float LIGHT_ANGLE = 0.5;
const vec3 LIGHT_DIR = vec3(sin(LIGHT_ANGLE),0.0,cos(LIGHT_ANGLE));
const mat4 OFFSET = mat4(
1.0,0.0,0.0,0.0,
0.0,1.0,0.0,0.0,
0.0,0.0,1.0,0.0,
0.0,0.0,-5.0,1.0
);
const float FOV = 0.698132;
const float Z_NEAR = 1.0;
const float Z_FAR = 20.0;
const float COT_FOV = 1.0 / tan(FOV * 0.5);
const float Z_FACTOR_1 = -(Z_FAR / (Z_FAR - Z_NEAR));
const float Z_FACTOR_2 = -((Z_NEAR * Z_FAR) / (Z_FAR - Z_NEAR));
attribute vec3 aPosition;
attribute vec3 aNormal;
attribute vec3 aColour;
varying vec3 vColour;
uniform float uAspectRatio;
uniform float uRotation;
void main() {
float s = sin(uRotation);
float c = cos(uRotation);
mat4 PROJ = mat4(
COT_FOV * uAspectRatio,0.0,0.0,0.0,
0.0,COT_FOV,0.0,0.0,
0.0,0.0,Z_FACTOR_1,Z_FACTOR_2,
0.0,0.0,-1.0,0.0
);
mat4 rot = mat4(
c ,0.0,-s ,0.0,
0.0,1.0,0.0,0.0,
s ,0.0,c ,0.0,
0.0,0.0,0.0,1.0
);
vec3 normal = (vec4(aNormal,0.0) * rot).xyz;
vColour = aColour * max(0.4,dot(normal,LIGHT_DIR));
gl_Position = PROJ * OFFSET * rot * vec4(aPosition,1.0);
}
`,`
precision mediump float;
varying vec3 vColour;
void main() {
gl_FragColor = vec4(vColour,1.0);
}
`);
uAspectRatio = gl.getUniformLocation(cubeProgram,"uAspectRatio");
uCubeRotation = gl.getUniformLocation(cubeProgram,"uRotation");
gl.useProgram(cubeProgram);
gl.uniform1f(uAspectRatio,1.0 / (internalWidth / internalHeight));
scaleProgram = createProgram(`
precision mediump float;
attribute vec2 aPosition;
attribute vec2 aUV;
varying vec2 vUV;
void main() {
vUV = aUV;
gl_Position = vec4(aPosition,0.0,1.0);
}
`,`
precision mediump float;
varying vec2 vUV;
uniform sampler2D uTexture;
void main() {
gl_FragColor = texture2D(uTexture,vUV);
}
`);
cubeBuffer = createBuffer([
// Position Normal Colour
// Front
1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0,0.0,0.6,
-1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0,0.0,0.6,
-1.0,-1.0, 1.0, 0.0, 0.0, 1.0, 0.0,0.0,0.6,
-1.0,-1.0, 1.0, 0.0, 0.0, 1.0, 0.0,0.0,0.6,
1.0,-1.0, 1.0, 0.0, 0.0, 1.0, 0.0,0.0,0.6,
1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0,0.0,0.6,
// Back
-1.0,-1.0,-1.0, 0.0, 0.0,-1.0, 0.0,0.0,0.6,
-1.0, 1.0,-1.0, 0.0, 0.0,-1.0, 0.0,0.0,0.6,
1.0, 1.0,-1.0, 0.0, 0.0,-1.0, 0.0,0.0,0.6,
1.0, 1.0,-1.0, 0.0, 0.0,-1.0, 0.0,0.0,0.6,
1.0,-1.0,-1.0, 0.0, 0.0,-1.0, 0.0,0.0,0.6,
-1.0,-1.0,-1.0, 0.0, 0.0,-1.0, 0.0,0.0,0.6,
// Left
-1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0,0.0,0.6,
-1.0,-1.0,-1.0, 1.0, 0.0, 0.0, 0.0,0.0,0.6,
-1.0,-1.0, 1.0, 1.0, 0.0, 0.0, 0.0,0.0,0.6,
-1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0,0.0,0.6,
-1.0, 1.0,-1.0, 1.0, 0.0, 0.0, 0.0,0.0,0.6,
-1.0,-1.0,-1.0, 1.0, 0.0, 0.0, 0.0,0.0,0.6,
// Right
1.0,-1.0, 1.0, -1.0, 0.0, 0.0, 0.0,0.0,0.6,
1.0,-1.0,-1.0, -1.0, 0.0, 0.0, 0.0,0.0,0.6,
1.0, 1.0, 1.0, -1.0, 0.0, 0.0, 0.0,0.0,0.6,
1.0,-1.0,-1.0, -1.0, 0.0, 0.0, 0.0,0.0,0.6,
1.0, 1.0,-1.0, -1.0, 0.0, 0.0, 0.0,0.0,0.6,
1.0, 1.0, 1.0, -1.0, 0.0, 0.0, 0.0,0.0,0.6
]);
scaleBuffer = createBuffer([
// Position UV
1.0, 1.0, 1.0,1.0,
-1.0, 1.0, 0.0,1.0,
-1.0,-1.0, 0.0,0.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
]);
[framebuffer,framebufferTexture] = createFramebuffer(internalWidth,internalHeight);
loop();
}
// Exit point
onunload = function() {
gl.deleteProgram(cubeProgram);
gl.deleteProgram(scaleProgram);
gl.deleteBuffer(cubeBuffer);
gl.deleteBuffer(scaleBuffer);
gl.deleteFramebuffer(framebuffer);
gl.deleteTexture(framebufferTexture);
}
}();
</script>
</body>
</html>
&#13;