如何在Node.js/C++中实现不同编码纹理图的加权相乘并输出sRGB格式
Node.js 解决方案(优先)
问题根源
sharp 默认将 JPEG 视为 sRGB 编码解码,你处理 Linear 格式的 AO 图时输出偏暗,是因为它把 Linear 数据错误地做了 sRGB 的 gamma 压缩。要实现需求,必须手动处理色彩空间转换。
方案一:使用 sharp 手动处理像素
import sharp from 'sharp'; // sRGB 转 Linear 空间 const srgbToLinear = (value) => { return value <= 0.04045 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4); }; // Linear 转 sRGB 空间 const linearToSrgb = (value) => { return value <= 0.0031308 ? value * 12.92 : 1.055 * Math.pow(value, 1/2.4) - 0.055; }; async function processTextures(albedoPath, aoPath, outputPath) { // 读取两张图的原始像素数据和元信息 const [albedo, ao] = await Promise.all([ sharp(albedoPath).raw().toBuffer({ resolveWithObject: true }), sharp(aoPath).raw().toBuffer({ resolveWithObject: true }) ]); // 校验尺寸一致性 if (albedo.info.width !== ao.info.width || albedo.info.height !== ao.info.height) { throw new Error("两张图像尺寸必须一致"); } const width = albedo.info.width; const height = albedo.info.height; const outputBuffer = Buffer.alloc(width * height * 3); // 遍历处理每个像素 for (let i = 0; i < width * height; i++) { const idx = i * 3; // Albedo:sRGB 转 Linear const albedoR = srgbToLinear(albedo.data[idx] / 255); const albedoG = srgbToLinear(albedo.data[idx + 1] / 255); const albedoB = srgbToLinear(albedo.data[idx + 2] / 255); // AO:将 sharp 误解码的 sRGB 值转回 Linear const aoR = Math.pow(ao.data[idx] / 255, 2.2); const aoG = Math.pow(ao.data[idx + 1] / 255, 2.2); const aoB = Math.pow(ao.data[idx + 2] / 255, 2.2); // 按 0.5 权重执行相乘运算:Albedo * (0.5 + AO * 0.5) const resultR = albedoR * (0.5 + aoR * 0.5); const resultG = albedoG * (0.5 + aoG * 0.5); const resultB = albedoB * (0.5 + aoB * 0.5); // 转回 sRGB 并写入输出缓冲区 outputBuffer[idx] = Math.round(linearToSrgb(resultR) * 255); outputBuffer[idx + 1] = Math.round(linearToSrgb(resultG) * 255); outputBuffer[idx + 2] = Math.round(linearToSrgb(resultB) * 255); } // 输出处理后的图像 await sharp(outputBuffer, { raw: { width, height, channels: 3 } }) .jpeg({ quality: 100 }) .toFile(outputPath); } // 调用示例 processTextures('assets/albedo.jpg', 'assets/ao.jpg', 'assets/combined.jpg') .then(() => console.log("处理完成")) .catch(err => console.error(err));
方案二:使用 jimp(更直观的像素操作)
jimp 对像素级操作的封装更友好,无需手动处理 Buffer:
import Jimp from 'jimp'; // 色彩空间转换函数同前 const srgbToLinear = (value) => { return value <= 0.04045 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4); }; const linearToSrgb = (value) => { return value <= 0.0031308 ? value * 12.92 : 1.055 * Math.pow(value, 1/2.4) - 0.055; }; async function processWithJimp(albedoPath, aoPath, outputPath) { const [albedo, ao] = await Promise.all([ Jimp.read(albedoPath), Jimp.read(aoPath) ]); albedo.scan(0, 0, albedo.bitmap.width, albedo.bitmap.height, (x, y, idx) => { // Albedo 转 Linear const aR = srgbToLinear(albedo.bitmap.data[idx] / 255); const aG = srgbToLinear(albedo.bitmap.data[idx + 1] / 255); const aB = srgbToLinear(albedo.bitmap.data[idx + 2] / 255); // AO 从误解码的 sRGB 转回 Linear const oR = Math.pow(ao.bitmap.data[idx] / 255, 2.2); const oG = Math.pow(ao.bitmap.data[idx + 1] / 255, 2.2); const oB = Math.pow(ao.bitmap.data[idx + 2] / 255, 2.2); // 执行权重运算 const r = aR * (0.5 + oR * 0.5); const g = aG * (0.5 + oG * 0.5); const b = aB * (0.5 + oB * 0.5); // 转回 sRGB 并写入 albedo.bitmap.data[idx] = Math.round(linearToSrgb(r) * 255); albedo.bitmap.data[idx + 1] = Math.round(linearToSrgb(g) * 255); albedo.bitmap.data[idx + 2] = Math.round(linearToSrgb(b) * 255); }); await albedo.writeAsync(outputPath); } processWithJimp('assets/albedo.jpg', 'assets/ao.jpg', 'assets/combined_jimp.jpg') .then(() => console.log("处理完成")) .catch(err => console.error(err));
C++ 解决方案(使用 stb 轻量库)
用 stb_image 和 stb_image_write 实现,无需依赖大型图形库:
代码实现
#define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" #define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h" #include <cmath> #include <cstdio> // sRGB 转 Linear float srgb_to_linear(float value) { return value <= 0.04045f ? value / 12.92f : powf((value + 0.055f) / 1.055f, 2.4f); } // Linear 转 sRGB float linear_to_srgb(float value) { return value <= 0.0031308f ? value * 12.92f : 1.055f * powf(value, 1.0f / 2.4f) - 0.055f; } int main() { int width, height, channels; // 读取 Albedo 图(sRGB) unsigned char* albedo_data = stbi_load("assets/albedo.jpg", &width, &height, &channels, 3); if (!albedo_data) { fprintf(stderr, "加载 Albedo 图失败\n"); return 1; } // 读取 AO 图(Linear,stb 默认按 sRGB 解码) unsigned char* ao_data = stbi_load("assets/ao.jpg", &width, &height, &channels, 3); if (!ao_data) { fprintf(stderr, "加载 AO 图失败\n"); stbi_image_free(albedo_data); return 1; } // 分配输出缓冲区 unsigned char* output_data = new unsigned char[width * height * 3]; // 遍历处理每个像素 for (int i = 0; i < width * height; ++i) { int idx = i * 3; float aR = srgb_to_linear(albedo_data[idx] / 255.0f); float aG = srgb_to_linear(albedo_data[idx+1] / 255.0f); float aB = srgb_to_linear(albedo_data[idx+2] / 255.0f); float oR = powf(ao_data[idx] / 255.0f, 2.2f); float oG = powf(ao_data[idx+1] / 255.0f, 2.2f); float oB = powf(ao_data[idx+2] / 255.0f, 2.2f); float r = aR * (0.5f + oR * 0.5f); float g = aG * (0.5f + oG * 0.5f); float b = aB * (0.5f + oB * 0.5f); output_data[idx] = (unsigned char)roundf(linear_to_srgb(r) * 255.0f); output_data[idx+1] = (unsigned char)roundf(linear_to_srgb(g) * 255.0f); output_data[idx+2] = (unsigned char)roundf(linear_to_srgb(b) * 255.0f); } // 输出结果图 stbi_write_jpg("assets/combined.jpg", width, height, 3, output_data, 100); // 释放资源 stbi_image_free(albedo_data); stbi_image_free(ao_data); delete[] output_data; return 0; }
编译命令
将代码与 stb_image.h、stb_image_write.h 放在同一目录,执行:
g++ -o texture_combine texture_combine.cpp -lm
内容的提问来源于stack exchange,提问作者NeelDigonto
相关产品推荐
相关产品推荐

