从纹理创建法线贴图的逻辑是什么?

3
我在谷歌上搜索,但唯一找到的是使用Photoshop创建bump map的教程。我对此不感兴趣!我需要它背后的逻辑。 (我不需要“使用”bump map的逻辑,我想知道如何“制作”它!)
我正在编写自己的HLSL着色器,已经意识到两个像素之间存在某种梯度,将显示其法线-因此可以根据光的位置进行照明。
我希望实时完成这项工作,以便当纹理发生变化时,bump map也会随之改变。
谢谢

我不确定是否可能。你想要的实际上是创建一个2D纹理的3D模型(法线贴图)。 - Ondra
4个回答

3
我意识到我迟到了,但我最近也遇到了同样的情况,当我尝试为3ds max编写自己的法线贴图生成器时。有笨重且不必要的C#库,但没有简单的、基于数学的解决方案。
所以我采用了转换背后的数学:Sobel算子。这就是你想在着色器脚本中使用的东西。
下面的类是我见过的最简单的C#实现。它完全做到了它应该做的,并实现了所需的效果:基于高度图、纹理甚至是您提供的程序生成过程的法线贴图。
正如您在代码中看到的,我实现了if/else来减轻在边缘检测宽度和高度限制上引发的异常。
它的作用是:采样每个像素/相邻像素的HSB亮度,以确定随后转换为RGB进行SetPixel操作的输出色调/饱和度值的比例。
另外:您可以实现一个输入控件来缩放输出色调/饱和度值的强度,以缩放输出法线贴图将为您的几何体/照明提供的后续影响。
就是这样。不再需要处理那个已经过时的、只有小窗口的PhotoShop插件。天空是极限。

C# WinForms实现的截图(源代码/输出):

enter image description here

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

2
一个用于读取高度图或深度图的采样器。
/// 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 内置函数生成法线,而无需重新采样源纹理。显然,您会在预处理步骤中执行此操作,读取生成的法线贴图,然后将其用作后续阶段的输入。

无论如何,这对我来说已经足够快了。


0
简短回答是:没有可靠可产生良好结果的方法来区分由于凹凸不平而导致颜色/亮度变化的散射纹理和表面实际上在该点具有不同颜色/亮度的散射纹理。
更详细地说:
如果您假设表面实际上是恒定颜色,那么颜色或亮度的任何变化都必须是由于凹凸效应造成的遮挡效果。计算每个像素从实际表面颜色增加/减少多少亮度;较亮的值表示表面的部分朝向光源,而较暗的值表示表面的部分背离光源。如果您还指定了光线的方向,则可以在纹理上每个点计算出一个表面法线,以使其产生您计算的遮挡值。
这是基本理论。当然,在现实中,表面几乎从不是恒定颜色,这就是为什么使用纯粹的散射纹理作为输入的方法往往效果不佳的原因。我不确定CrazyBump之类的东西是如何完成的,但我认为它们正在执行类似于对图像的局部部分进行颜色平均而不是整个纹理的操作。
通常,法线贴图是从实际的三维模型表面“投射”到低分辨率几何体上创建的。毕竟,法线贴图只是一种伪造高分辨率几何体的技术。

-1
快速回答:这是不可能的。 一个简单的通用(扩散)纹理根本不包含这些信息。我没有仔细研究过Photoshop是如何做到的(只见过一位艺术家使用过),但我认为他们只是简单地执行类似于“深度=红+绿+蓝+透明度”的操作,基本上返回一个高度图/渐变图。然后使用简单的边缘检测效果将高度图转换为法线贴图,以获取切线空间法线贴图。
请记住,在大多数情况下,您使用法线贴图来模拟高分辨率的3D几何网格,因为它填补了顶点法线留下的空白区域。如果您的场景严重依赖光照,那么这是行不通的,但如果只是一个简单的定向光源,这个方法“可能”有效。 当然,这只是我的经验,你可能正在从事完全不同类型的项目。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接