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

问题描述 投票:3回答:4

我看过google,但我唯一能找到的就是如何使用photoshop创建一个教程。不感兴趣!我需要它背后的逻辑。 (我不需要如何'使用'凹凸贴图的逻辑,我想知道如何'制作'一个!)

我正在编写自己的HLSL着色器,并且已经意识到在两个像素之间存在某种渐变将显示其正常 - 因此可以随意点亮灯的位置。

我想实时这样做,以便当纹理改变时,凹凸贴图也会这样做。

谢谢

c# xna textures hlsl
4个回答
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
投票

快速回答:这是不可能的。 简单的通用(漫反射)纹理根本不包含此信息。我没有看到Photoshop究竟是如何做到的(看过它曾经被艺术家使用过),但我认为它们只是做了类似'depth = r + g + b + a'的东西,它基本上会返回一个高度图/渐变。然后使用简单的边缘检测效果将高度图转换为法线贴图,以获得切线空间法线贴图。

请记住,在大多数情况下,您使用法线贴图来模拟高分辨率3D几何网格,因为它填充了空白点顶点法线留下。如果您的场景严重依赖于照明,这是不可取的,但如果它是一个简单的定向灯,这可能会起作用。当然,这只是我的经验,你也可能正在开发一个完全不同类型的项目。


0
投票

简短的回答是:没有办法可靠地做到这一点,从而产生良好的效果,因为没有办法区分由于凹凸而导致颜色/亮度发生变化的漫反射纹理与颜色变化的漫反射纹理/亮度,因为表面实际上是不同的颜色/亮度。

更长的回答:

如果你假设表面实际上是一个恒定的颜色,那么颜色或亮度的任何变化都必须归因于凹凸造成的阴影效果。计算每个像素与实际表面颜色相比有多亮/暗;较亮的值表示面向“朝向”光源的表面部分,而较暗的值表示表面“远离”光源的部分。如果还指定了光源的方向,则可以计算纹理上每个点的曲面法线,使其产生您计算的着色值。

这是基本理论。当然,实际上,表面几乎从不是恒定的颜色,这就是为什么这种纯粹使用漫反射纹理作为输入的方法往往不能很好地工作。我不确定像CrazyBump这样的东西是怎么做的,但我认为他们正在做的事情就是在图像的局部部分而不是整个纹理上平均颜色。

通常,法线贴图是从表面的实际3D模型创建的,这些3D模型被“投影”到较低分辨率的几何体上。毕竟,法线贴图只是伪造高分辨率几何体的一种技术。


0
投票

我意识到我在这个派对上已经很晚了但是我也在最近尝试为3ds max编写我自己的法线贴图生成器时遇到了同样的情况。 C#有笨重且不必要的库,但没有任何简单的基于数学的解决方案。

所以我跟着转换背后的数学运算:Sobel运算符。这就是您希望在着色器脚本中使用的内容。

以下类是关于我在C#中看到的最简单的实现。它确实完成了它应该做的事情并完全达到了预期的效果:基于高度图,纹理甚至是您提供的以编程方式生成的程序的法线贴图。

正如您在代码中看到的那样,我已经实现了if / else来减轻在边缘检测宽度和高度限制上抛出的异常。

作用:对每个像素/相邻像素的HSB亮度进行采样,以确定输出色相/饱和度值的比例,随后将其转换为RGB以用于SetPixel操作。

另外:您可以实现输入控件来缩放输出色相/饱和度值的强度,以缩放输出法线贴图将为您的几何/光照提供的后续效果。

就是这样。不再需要处理那个已弃用的小窗口PhotoShop插件。天空是极限。

using System.Drawing;
using System.Windows.Forms;

namespace heightmap.Class
{
    class Normal
    {
        public void calculate(Bitmap image, PictureBox pic_normal)
        {
            #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; y++)
            {
                for (int x = 0; x < h; 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;
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.