如何修正JS着色器函数以实现像素画风格的最近邻放大?
需求说明
为测试自研游戏美术风格,我希望将3D世界渲染为像素画形式。例如,将如下场景(需先调整色彩/风格以适配像素化效果):
转换为类似这样的效果:
通过调整3D源素材的风格,像素化输出效果会更理想。常规做法是先将图像缩小至约80p,再用最近邻重采样放大到1080p,但直接渲染到80p画布再放大效率更高。
虽然着色器通常不用于最近邻格式的位图缩放,但这种方式的实时转换性能优于我尝试过的其他方法。
我的代码
位图缓冲区以行优先(row major)格式存储,即r1, g1, b1, a1, r2, g2, b2, a2...,我使用gpu.js将JS函数转换为着色器。目标是将位图按最近邻方式放大,使每个像素变为2x2或3x3等方块。假设inputBuffer是输出尺寸按比例缩小后的缓冲区。
var pixelateMatrix = gpu.createKernel(function(inputBuffer, width, height, scale) { var y = Math.floor((this.thread.x / (width[0] * 4)) / scale[0]); var x = Math.floor((this.thread.x % (width[0] * 4)) / scale[0]); var remainder = this.thread.x % 4; return inputBuffer[(x * y) + remainder]; }).setOutput([width * height * 4]);
注意:代码遍历的是全尺寸输出的新缓冲区,需根据
outputBuffer的当前索引(库中通过this.thread.x暴露)找到sourceBuffer中的对应坐标。
问题现象
代码未实现最近邻放大,反而生成了彩虹效果(上方是正常小尺寸渲染图,下方是着色器输出结果,右侧为输入输出缓冲区的调试日志):
请问我的代码哪里出错了?
你代码里的核心问题是输入缓冲区的索引计算逻辑完全错误,导致颜色通道和像素位置混乱,才出现了彩虹色的异常效果。咱们一步步拆解问题并修正:
1. 错误点拆解
首先要明确几个关键概念:
this.thread.x是输出缓冲区的全局索引(每个像素占4个通道,所以范围是0到outputWidth*outputHeight*4 - 1)- 行优先缓冲区的像素索引公式是:
(y * 宽度 + x) * 4 + 通道索引,你的代码完全违背了这个规则:- 用输入宽度计算输出像素的坐标,混淆了输入输出尺寸的对应关系
- 最后计算输入索引时用
(x * y) + remainder,完全错误地拼接了坐标和通道
2. 修正后的代码
我们重新梳理正确的映射逻辑:
- 从全局线程索引分离出当前处理的颜色通道
- 将全局索引转换为输出像素的坐标
- 把输出坐标按放大比例映射回输入像素的坐标(最近邻就是直接取整)
- 用行优先规则计算输入缓冲区的正确索引
修正后的代码如下:
var pixelateMatrix = gpu.createKernel(function(inputBuffer, inputWidth, inputHeight, scale) { // 1. 获取当前处理的颜色通道(0=r,1=g,2=b,3=a) const channel = this.thread.x % 4; // 2. 计算当前输出像素的全局索引(剔除通道维度) const outputPixelIdx = Math.floor(this.thread.x / 4); // 3. 计算输出图像的宽高 const outputWidth = inputWidth * scale; // 4. 计算输出像素的坐标 const outputX = outputPixelIdx % outputWidth; const outputY = Math.floor(outputPixelIdx / outputWidth); // 5. 映射到输入图像的对应像素坐标(最近邻采样) const inputX = Math.floor(outputX / scale); const inputY = Math.floor(outputY / scale); // 6. 计算输入缓冲区的正确索引(行优先规则) const inputIndex = (inputY * inputWidth + inputX) * 4 + channel; return inputBuffer[inputIndex]; }) .setOutput([inputWidth * inputHeight * scale * scale * 4]); // 输出尺寸为输入的scale²倍,每个像素4通道
3. 额外注意事项
- 调用kernel时,确保传入的
inputWidth/inputHeight是输入缓冲区的实际宽高,scale是放大倍数(如2、3) - 输出缓冲区的尺寸计算必须准确:
输入像素数 × scale² × 4,对应放大后的所有像素通道 - 如果你的参数习惯用数组形式(比如原代码的
width[0]),只需把变量名对应替换即可
这样修改后,就能正确实现最近邻放大,每个输入像素会被放大成scale×scale的纯色方块,不会再出现颜色混乱的彩虹效果了。
内容的提问来源于stack exchange,提问作者john doe

