是否有更简单(并且仍然高效)的方式来升级具有最近邻居重新采样的画布渲染? [英] Is there a simpler (and still performant) way to upscale a canvas render with nearest neighbor resampling?

查看:87
本文介绍了是否有更简单(并且仍然高效)的方式来升级具有最近邻居重新采样的画布渲染?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有点难过这个看似简单的任务,即以最近邻格式升级画布渲染,我在这里问:





像pixelart这样:





但在这个问题中,我正在问如何正确实现我选择的解决方案(基本上使用着色器来处理扩展)。也许我应该问:有更简单(并且仍然有效)的方法吗?

解决方案

我可以提供两种方法,可以使用最近邻居有效地向上或向下缩放图像。



要手动完成,你应该遍历新的每个像素缩放图像并使用旧尺寸与新尺寸的比率来计算他们应该使用的原始像素。



(我的代码片段使用.toDataURL()所以他们可能不适用于chrome。)



 <! doctype html>< html> < HEAD> < meta charset =utf-8> <风格> #input {display:none; } body {background-color:black; } body> * {display:block; margin-top:10px; margin-left:auto; margin-right:auto; } img {background-color:grey;边框:实心1px白色; border-radius:10px;图像渲染:optimizeSpeed; } label {transition:0.1s;游标:指针; 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;宽度:130px;身高:40px; line-height:40px; border-radius:10px;白颜色; 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; }< /风格> < /头> <身体GT; < img id =unscaledImage>< / img> < img id =scaledImage>< / img> < input id =scaletype =rangemin =1max =100value =50>< / input> < label for =input>上传图片< / label> < input id =inputtype =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; }}(); < /脚本> < / body>< / html>  



使用着色器的更快方法是将图像添加到设置为使用最近邻居过滤的纹理并将其绘制到四边形上。在绘制之前,可以通过gl.viewport控制四边形的大小。



 <!doctype html>< html> < HEAD> < meta charset =utf-8> <风格> #file {display:none; } body {background-color:black; } body> * {display:block; margin-top:10px; margin-left:auto; margin-right:auto; } img {background-color:grey;边框:实心1px白色; border-radius:10px;图像渲染:optimizeSpeed; } label {transition:0.1s;游标:指针; 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;宽度:130px;身高:40px; line-height:40px; border-radius:10px;白颜色; 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; }< /风格> < /头> <身体GT; < img id =unscaledImage>< / img> < img id =scaledImage>< / img> < input id =scaletype =rangemin =1max =100value =50>< / input> < input id =filetype =file>< / input> < label for =file>上传图片< / 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(本); 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(); } //入口点onload = function(){// DOM设置unscaledImage.onload = onImageLoad; unscaledImage.onerror = onImageError; scale.onmouseup = onScaleChanged; file.oninput = onImageSubmitted; // GL setup //程序(着色器)var vertexShader = gl.createShader(gl.VERTEX_SHADER); var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); program = gl.createProgram(); gl.shaderSource(vertexShader,`precision mediump float;属性vec2 aPosition;属性vec2 aUV;变化vec2 vUV; void main(){vUV = aUV; gl_Position = vec4(aPosition,0.0,1.0);}`); gl.shaderSource(fragmentShader,`precision mediump float; vary vec2 vUV; uniform sampler2D uTexture; void main(){gl_FragColor = texture2D(uTexture,vUV);}`); gl.compileShader(vertexShader); gl.compileShader(fragmentShader); gl.attachShader(程序,vertexShader); gl.attachShader(程序,fragmentShader); gl.linkProgram(程序); gl.deleteShader(vertexShader); gl.deleteShader(fragmentShader); gl.useProgram(程序); // Buffer buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_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); //纹理纹理= gl.createTexture(); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,纹理); 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(缓冲液); gl.deleteTexture(纹理); }}(); < /脚本> < / body>< / html>  



编辑:
为了更好地说明实际渲染器中的内容,我创建了另一个示例,将场景绘制到低分辨率帧缓冲区,然后将其缩放到画布(关键是设置最小值) & mag filter to nearest neighbor)。



 < ;!doctype html>< html> < HEAD> < meta charset =utf-8> <风格>身体{背景颜色:黑色; } .center {display:block; margin-top:30px; margin-left:auto; margin-right:auto;边框:实心1px白色; border-radius:10px; } script {display:none; }< /风格> < /头> <身体GT; < canvas id =canvasclass =center>< / canvas> < input id =scaletype =rangemin =1max =100value =100class =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(帧缓冲器); 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不支持。); var cubeProgram = null; //用于绘制3D立方体的着色器var scaleProgram = null; //用于缩放帧的着色器var uAspectRatio = null; //投影矩阵的宽高比var uCubeRotation = null; //多维数据集程序的统一位置var cubeBuffer = null; // cube model(attributes)var scaleBuffer = null; //四位置和UV的var framebuffer = null; // render target var framebufferTexture = null; //呈现的纹理。 (在此上绘制立方体)函数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)){throwVS:+ gl.getShaderInfoLog(vertexShader); } if(!gl.getShaderParameter(fragmentShader,gl.COMPILE_STATUS)){throwFS:+ gl.getShaderInfoLog(fragmentShader); catch(error){gl.deleteShader(vertexShader); gl.deleteShader(fragmentShader); console.error(误差); } var program = gl.createProgram(); gl.attachShader(程序,vertexShader); gl.attachShader(程序,fragmentShader); gl.deleteShader(vertexShader); gl.deleteShader(fragmentShader); gl.linkProgram(程序);回归计划; function createBuffer(data){var buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER,缓冲液); gl.bufferData(gl.ARRAY_BUFFER,Float32Array.from(数据),gl.STATIC_DRAW);返回缓冲区; function createFramebuffer(width,height){var texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D,纹理); 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,宽度,高度,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,纹理,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,帧缓冲器); 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(环); } // 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));属性vec3 aPosition;属性vec3 aNormal;属性vec3 aColour ;变化vec3 vColour;均匀浮动uAspectRatio;均匀浮动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_FACT OR_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); },``精确的mediump浮动;变化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; vary vec2 vUV; void main(){vUV = aUV; gl_Position = vec4(aPosition,0.0,1.0);}`,`precision mediump float; vary vec2 vUV; uniform sampler2D uTexture; void main(){gl_FragColor = texture2D(uTexture,vUV);}`); cubeBuffer = createBuffer([// Position Normal Color // 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,//返回-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,//左-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,//右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([//位置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);循环(); } //退出点onunload = function(){gl.deleteProgram(cubeProgram); gl.deleteProgram(scaleProgram); gl.deleteBuffer(cubeBuffer); gl.deleteBuffer(scaleBuffer); gl.deleteFramebuffer(帧缓冲器); gl.deleteTexture(framebufferTexture); }}(); < /脚本> < / body>< / html>  


I'm kinda stumped about this seemingly simple task of upscaling a canvas render in nearest neighbor format, which I asked here:

How can I properly write this shader function in JS?

The goal being to convert a 3D render output like this:

to pixelart like this:

But in that question I'm asking how to implement my chosen solution (using essentially a shader to handle the up-scaling) correctly. Perhaps instead I should ask: Is there a simpler (and still performant) way to do this?

解决方案

I can offer two approaches that can both efficiently scale an image up or down using nearest neighbor.

To do it manually, you should iterate through each pixel of your new scaled image and calculate what pixel from the original they should use using ratios of the old size compared to the new size.

(My code snippets use .toDataURL() so they may not work in 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>

Alternatively a much faster way using shaders, is to add your image to a texture that is set to use nearest neighbor filtering and draw it onto a quad. The size of the quad can be controlled via gl.viewport before drawing.

<!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>

Edit: To give better clarification on what this could look like in an actual renderer, I've created another example that draws a scene to a low resolution frame buffer and then scales it up to the canvas (The key is to set the min & mag filter to nearest neighbor).

<!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>

这篇关于是否有更简单(并且仍然高效)的方式来升级具有最近邻居重新采样的画布渲染?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆