我不能说 Three.js 能做什么或不能做什么,因为我真正知道的是它使 3d 资产与画布的集成变得轻而易举。
除此之外,我编写了一个纯 javascript 函数,用于非常有效地从彩色贴图生成法线贴图。但是请记住,这是我大约 4 年前为 C# winforms 编写的函数的 js 的快速移植,它循环遍历给定图像的所有像素以推断转换所需的数据。它很慢。严重的是,缓慢得令人痛苦,这是因为从浏览器的范围内获得漂亮、清晰、准确的法线贴图显然需要花费时间才能方便。
但它完全按照您的意愿行事;从给定的颜色贴图生成一个非常漂亮、干净、精确的法线贴图。
我已将它设置为live demo,这样您就可以在投入使用之前看到/感受到它的实际效果。据我所知,真的没有更快的方法来获取每个像素,计算它的亮度然后构建一个新像素所以,真的,除非你正在处理非常小的法线贴图,否则一个运行在已经很笨重的 js applet浏览器可能不是您的最佳选择。
如果有一种更快的方法可以在浏览器中以达到标准和准确的法线贴图输出的方式迭代数万或数百万像素,那么我会尝试一些狡猾的算法。
我没有实现任何花哨的异步更新,所以你只会知道这是在处理,因为在地图生成完成之前,用于按钮的手形光标不会返回到默认箭头。
我还包含了您的原始图像的 4 个变体供您使用,全部在代码中,4 个中的 3 个被注释掉了。该应用程序以 256x256 开始,因为从中生成法线贴图所需的时间是合理的。它们的大小从 128 到原始的 1024 不等,但我强烈建议不要使用全尺寸变体,因为您的浏览器可能会抱怨操作需要多长时间。
与 C# 变体一样,您可以实现一种方法,客户端可以通过这种方法通过调整亮度参数来控制生成的法线计算的强度。除了 C# 变体之外,这绝对可以作为生成法线贴图的基础,以便在使用 Three.js 应用于几何体时实时可视化。我所说的“实时”是指“无论生成 x 比例尺地图需要多长时间”,因为将完整的地图实际应用到几何体中需要几毫秒。
为了配合现场演示,这里是代码:
归一化.htm
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Normalizer</title>
<link rel="stylesheet" href="css/normalize.css">
</head>
<body onload="startup()">
<canvas id="input" class="canvas"></canvas>
<canvas id="output" class="canvas"></canvas>
<div class="progress">
<input type="button" class="button" onclick="MakeItSo()" value="Normalize!" />
<span id="progress">Ready to rock and / or roll on your command!</span>
</div>
<script src="js/normalize.js"></script>
</body>
</html>
规范化.css
html {
margin: 0px;
padding: 0px;
width: 100vw;
height: 100vh;
}
body {
margin: 0px;
padding: 0px;
width: 100vw;
height: 100vh;
overflow: hidden;
font-family: "Arial Unicode MS";
background: linear-gradient(330deg, rgb(150, 150, 150), rgb(200, 200, 200));
background-color: rgb(200, 200, 200);
display: flex;
align-items: center;
justify-content: center;
}
.canvas {
outline: 1px solid hsla(0, 0%, 0%, 0.25);
}
.progress {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 40px;
display: flex;
}
.progress span {
width: calc(100% - 160px);
height: 40px;
line-height: 40px;
color: hsl(0, 0%, 0%);
text-align: center;
}
input[type="button"] {
margin: 0px;
width: 120px;
height: 40px;
cursor: pointer;
display: inline;
}
标准化.js
// Javascript Normal Map Generator
// Copyright © Brian "BJS3D" Spencer 2022
// Incorporating W3C proposed algorithm to
// calculate pixel brightness in conjunction
// with the Sobel Operator.
window.CP.PenTimer.MAX_TIME_IN_LOOP_WO_EXIT = 60000;
function startup() {
// lets load the color map first to ensure it's there before we start iterating over its pixels.
// Then lets make sure the input and output canvases are sized according to the color map's dimensions.
"use strict";
var input, output, ctx_i, ctx_o, img, w, h;
input = document.getElementById("input");
ctx_i = input.getContext("2d");
ctx_i.clearRect(0, 0,input.width, input.height);
img = new Image();
img.crossOrigin = "Anonymous";
//img.src = "https://i.imgur.com/a4N2Aj4.jpg"; //128x128 - Tiny but fast.
img.src = "https://i.imgur.com/wFe4EG7.jpg"; //256x256 - Takes about a minute.
//img.src = "https://i.imgur.com/bm4pXrn.jpg"; //512x512 - May take 5 or 10 minutes.
//img.src = "https://i.imgur.com/aUIdxHH.jpg"; //original - Don't do it! It'll take hours.
img.onload = function () {
w = img.width;
h = img.height;
input.width = w;
input.height = h;
ctx_i.drawImage(img, 0, 0);
output = document.getElementById("output");
ctx_o = output.getContext("2d");
output.width = w;
output.height = h;
};
}
function MakeItSo(){
document.getElementById("progress").innerHTML = "Normal map generation in progress...";
totallyNormal();
}
function totallyNormal() {
// Now let's prep input to have its pixels violated to calculate their brightness
// and prep output to receive all those totally violated pixels...
"use strict";
var input, output, ctx_i, ctx_o, pixel, x_vector, y_vector, w, h;
input = document.getElementById("input");
ctx_i = input.getContext("2d");
output = document.getElementById("output");
ctx_o = output.getContext("2d");
w = input.width - 1;
h = input.height - 1;
// Let's begin iterating, using the Sobel Operator to get some really nice pixels to violate...
for (var y = 0; y < w + 1; y += 1) {
for (var x = 0; x < h + 1; x += 1) {
var data = [0, 0, 0, 0, x > 0, x < w, y > 1, y < h, x - 1, x + 1, x, x, y, y, y - 1, y + 1];
for (var z = 0; z < 4; z +=1) {
if (data[z + 4]) {
pixel = ctx_i.getImageData(data[z + 8], data[z + 12], 1, 1);
data[z] = ((0.299 * (pixel.data[0] / 100)) + (0.587 * (pixel.data[1] / 100)) + (0.114 * (pixel.data[2] / 100)) / 3);
} else {
pixel = ctx_i.getImageData(x, y, 1, 1);
data[z] = ((0.299 * (pixel.data[0] / 100)) + (0.587 * (pixel.data[1] / 100)) + (0.114 * (pixel.data[2] / 100)) / 3);
}
}
x_vector = parseFloat((Math.abs(data[0] - data[1]) + 1) * 0.5) * 255;
y_vector = parseFloat((Math.abs(data[2] - data[3]) + 1) * 0.5) * 255;
ctx_o.fillStyle = "rgba(" + x_vector + "," + y_vector + ",255,255)";
ctx_o.fillRect(x, y, 1, 1);
}
}
document.getElementById("progress").innerHTML = "Normal map generation complete.";
}