我在谷歌上看过,但我唯一能找到的是一个关于如何使用 Photoshop 创建一个的教程。不感兴趣!我需要它背后的逻辑。(而且我不需要如何“使用”凹凸贴图的逻辑,我想知道如何“制作”一个!)
我正在编写自己的 HLSL 着色器,并且已经意识到在两个像素之间存在某种渐变,这将显示其正常 - 因此可以根据光线的位置进行照明。
我想实时执行此操作,以便当纹理发生变化时,凹凸贴图也会发生变化。
谢谢
我意识到我参加这个聚会已经很晚了,但我最近在尝试为 3ds max 编写自己的法线贴图生成器时也遇到了同样的情况。C# 有庞大且不必要的库,但没有任何基于数学的简单解决方案。
所以我运行了转换背后的数学:Sobel 算子。这就是您希望在着色器脚本中使用的内容。
下面的类是关于我见过的 C# 最简单的实现。它完全完成了它应该做的事情并准确地实现了所期望的:基于高度图、纹理甚至是您提供的以编程方式生成的程序的法线贴图。
正如您在代码中看到的那样,我已经实现了 if / else 以减轻边缘检测宽度和高度限制引发的异常。
它的作用:对每个像素/相邻像素的 HSB 亮度进行采样,以确定输出色调/饱和度值的比例,这些值随后转换为 RGB 以进行 SetPixel 操作。
顺便说一句:您可以实现输入控件来缩放输出色相/饱和度值的强度,以缩放输出法线贴图将提供几何/照明的后续影响。
就是这样。不再需要处理那个已弃用的小窗口 PhotoShop 插件。天空是极限。
C# winforms 实现截图(源码/输出):
C# 类从源图像实现基于 Sobel 的法线贴图:
using System.Drawing;
using System.Windows.Forms;
namespace heightmap.Class
{
class Normal
{
public void calculate(Bitmap image, PictureBox pic_normal)
{
Bitmap image = (Bitmap) Bitmap.FromFile(@"yourpath/yourimage.jpg");
#region Global Variables
int w = image.Width - 1;
int h = image.Height - 1;
float sample_l;
float sample_r;
float sample_u;
float sample_d;
float x_vector;
float y_vector;
Bitmap normal = new Bitmap(image.Width, image.Height);
#endregion
for (int y = 0; y < w + 1; y++)
{
for (int x = 0; x < h + 1; x++)
{
if (x > 0) { sample_l = image.GetPixel(x - 1, y).GetBrightness(); }
else { sample_l = image.GetPixel(x, y).GetBrightness(); }
if (x < w) { sample_r = image.GetPixel(x + 1, y).GetBrightness(); }
else { sample_r = image.GetPixel(x, y).GetBrightness(); }
if (y > 1) { sample_u = image.GetPixel(x, y - 1).GetBrightness(); }
else { sample_u = image.GetPixel(x, y).GetBrightness(); }
if (y < h) { sample_d = image.GetPixel(x, y + 1).GetBrightness(); }
else { sample_d = image.GetPixel(x, y).GetBrightness(); }
x_vector = (((sample_l - sample_r) + 1) * .5f) * 255;
y_vector = (((sample_u - sample_d) + 1) * .5f) * 255;
Color col = Color.FromArgb(255, (int)x_vector, (int)y_vector, 255);
normal.SetPixel(x, y, col);
}
}
pic_normal.Image = normal; // set as PictureBox image
}
}
}
用于读取高度或深度图的采样器。
/// same data as HeightMap, but in a format that the pixel shader can read
/// the pixel shader dynamically generates the surface normals from this.
extern Texture2D HeightMap;
sampler2D HeightSampler = sampler_state
{
Texture=(HeightMap);
AddressU=CLAMP;
AddressV=CLAMP;
Filter=LINEAR;
};
请注意,我的输入图是 512x512 单分量灰度纹理。从中计算法线非常简单:
#define HALF2 ((float2)0.5)
#define GET_HEIGHT(heightSampler,texCoord) (tex2D(heightSampler,texCoord+HALF2))
///calculate a normal for the given location from the height map
/// basically, this calculates the X- and Z- surface derivatives and returns their
/// cross product. Note that this assumes the heightmap is a 512 pixel square for no particular
/// reason other than that my test map is 512x512.
float3 GetNormal(sampler2D heightSampler, float2 texCoord)
{
/// normalized size of one texel. this would be 1/1024.0 if using 1024x1024 bitmap.
float texelSize=1/512.0;
float n = GET_HEIGHT(heightSampler,texCoord+float2(0,-texelSize));
float s = GET_HEIGHT(heightSampler,texCoord+float2(0,texelSize));
float e = GET_HEIGHT(heightSampler,texCoord+float2(-texelSize,0));
float w = GET_HEIGHT(heightSampler,texCoord+float2(texelSize,0));
float3 ew = normalize(float3(2*texelSize,e-w,0));
float3 ns = normalize(float3(0,s-n,2*texelSize));
float3 result = cross(ew,ns);
return result;
}
和一个像素着色器来调用它:
#define LIGHT_POSITION (float3(0,2,0))
float4 SolidPS(float3 worldPosition : NORMAL0, float2 texCoord : TEXCOORD0) : COLOR0
{
/// calculate a normal from the height map
float3 normal = GetNormal(HeightSampler,texCoord);
/// return it as a color. (Since the normal components can range from -1 to +1, this
/// will probably return a lot of "black" pixels if rendered as-is to screen.
return float3(normal,1);
}
LIGHT_POSITION
可以(并且可能应该)从您的主机代码输入,尽管我在这里作弊并使用了一个常量。
请注意,此方法每个法线需要 4 次纹理查找,而不是计算一次来获取颜色。这对你来说可能不是问题(取决于你在做什么)。如果这对性能造成太大影响,您可以在纹理更改时调用它,渲染到目标,然后将结果捕获为法线贴图。
一种替代方法是将具有高度图纹理的屏幕对齐四边形绘制到渲染目标,并使用ddx
/ ddy
HLSL 内在函数来生成法线,而无需重新采样源纹理。显然,您将在预传递步骤中执行此操作,读取生成的法线贴图,然后将其用作后续阶段的输入。
无论如何,这对我来说已经足够快了。
简短的回答是:没有办法可靠地做到这一点,从而产生良好的结果,因为没有办法区分由于凹凸而导致颜色/亮度变化的漫反射纹理与颜色/亮度变化的漫反射纹理之间的区别。亮度,因为此时表面实际上是不同的颜色/亮度。
更长的答案:
如果您假设表面实际上是恒定的颜色,那么颜色或亮度的任何变化都必须是由于凹凸造成的阴影效果。根据实际表面颜色计算每个像素的亮/暗程度;较亮的值表示表面“朝向”光源的部分,而较暗的值表示表面“远离”光源的部分。如果您还指定了光线来自的方向,您可以计算纹理上每个点的表面法线,这样它就会产生您计算的着色值。
这就是基本理论。当然,在现实中,表面几乎从来都不是一个恒定的颜色,这就是为什么这种纯粹使用漫反射纹理作为输入的方法往往效果不佳的原因。我不确定像 CrazyBump 这样的事情是如何做到的,但我认为他们正在做一些事情,比如在图像的局部部分而不是整个纹理上平均颜色。
通常,法线贴图是从表面的实际 3D 模型创建的,这些模型“投影”到较低分辨率的几何体上。毕竟,法线贴图只是一种伪造高分辨率几何图形的技术。
快速回答:这是不可能的。
一个简单的通用(漫反射)纹理根本不包含此信息。我还没有仔细研究 Photoshop 是如何做到的(曾经被艺术家使用过),但我认为他们只是简单地执行了类似“depth=r+g+b+a”之类的操作,它基本上返回了一个高度图/渐变。然后使用简单的边缘检测效果将高度图转换为法线贴图以获得切线空间法线贴图。
请记住,在大多数情况下,您使用法线贴图来模拟高分辨率 3D 几何网格,因为它填充了顶点法线留下的空白点。如果您的场景严重依赖照明,这是不行的,但如果它是一个简单的定向光,这“可能”工作。当然,这只是我的经验,你也可以从事一个完全不同类型的项目。