Parallax Occlusion Mapping

The purpose was to make terrain in my Game Engine. In order to make terrain, you need to multiply the height scale based on Height Mapping for each model mesh in the vertex shader. However, one of my instincts was basically telling me “Would it be efficient to use height mapping if there are too many vertices, and transform those vertices according to displacement texture to show realism?” So, I’ve found that there is something called “Parallax Mapping”, “Parallax Steep Mapping”, and “Parallax Occlusion Mapping”. These methods originate from Per-Pixel Displacement Mapping. These methods give a sense of depth or illusion, and this approximation technique can be done in the fragment shader. Let’s take a look at Parallax Mapping first, then move on to Parallax Occlusion Mapping because steep mapping is an addition of steps - other than that, it’s similar to Parallax Mapping.

Idea

The idea is to think differently about the texture coordinate, considering that the fragment’s surface is higher or lower than it actually is. So the UV coordinate is literally above or below the actual vertices from the vertex shader. If you take a look at the image below, what we want to find is Point B, rather than A. But A is what we actually see (in fragment space). Then how would you calculate Point B? You can think of it as similar to ray casting. Since you have the view direction, you can figure out the P vector by using the height (displacement) map, then we can approximate the displacement to Point B. But it won’t always work - I will explain this in the limitations.

alt text

In terms of implementation, one important thing to note is to send the normal and tangent vectors. For each vertex, depending on where the surface is directing, you can set normal and tangent, and pass these through the render pass. In detail, you should calculate the Parallax mapping on tangent space, which means you need to transform the view direction to tangent space multiplying TBN matrix.

Okay! Let’s look at the calculation in detail. We think that we are looking at the surface (at height 0.0), and the camera is pointing at A, but we want to figure out Point B. We can calculate the vector P from Point A using the view direction. The key insight is that we use the height value H(A) sampled from the height map at point A to determine how far to offset our texture coordinates along the projected view ray. Then we sample through along the Vector P with the length H(A) to A. Then we can get the end of Point from Vector P and corresponding H(P) from height map. Like i said this is approximation. In order to make it more accurate, you can certainly use steep method where it takes multiple sample by dividing total depth range into multiple layers. The details are shown in this link.

alt text

Finally, one more step is needed after steep parallax occlusion mapping. For each step we found T3 and T2, then we sample H(T3) and H(T2). Then we interpolatate those two points, treating like flat surface, if the depth is incorrect.

alt text

Results

alt text

alt text

Limitation

There are some limitation or usages I can mention by doing some experiments. The big issues is an aliasing exist when height map of that texture dramatically changes over a surface. You can test on this repo. I guess that’s why the usecase might be cave, stairs, bricks, some texture that have consistent height values. I can mentioned that each methods have a “bad effect”; it looks distorted in some angles in Parallax Mapping, and you can see the steep if the sample rate is very low in Steep Parallax Mapping. But if we know that why these downside appears to be true when you implementing, it all makes sense.

Conclusion

Interestingly, we found the way to show the depth rather creating a lot of triangles. Of course the goal was to implement the terrain (height + tesselation), but it was good to develop new things!

Resource

Physically Based Rendering (Lighting & Shading)

Okay, this is a very difficult topics I could say from the point of muggles. (when I say muggle, it’s not like dumb people, just people who don’t know the background of physical rendering which include me). But we can simply narrow down about what we know. In order to fully understand physical based rendering, we need to understand how light behaves in real world. Basically we need to go over the physics in Light.

Physics

First of all, the one of the principle of physics is energy conservation. What !? where this idea coming from? When we think of the physics of light. Light waves carry the energy, basically saying that the density of the energy flow is equal to the product of the magnitudes of the electric and magnetic field.

alt text

Rendering, we care about the average energy flow over time, which is proportional to the squared wave amplitude, and this “average energy flow density” is also called irradiance. Summation / Subtraction of wave can be described as constructive and destructive interference, and also called coherent addition. Since those are not the most often case. If the waves are mutually incoherent, which means there wave’s phase can be random. We can simply say that they can interfere each other resulting almost “zero” amplitude or “some amplitude” in different location. This basically tells us that the energy gained via constructive interference and the energy lost via destructive interference always cancel out, and the energy is conserved.

alt text

Above information, in rendering scenario, using the average energy flow density is plausible, Then we can talk about the light interaction with molecule. Basically, when light hits the matter, then it separates the positive and negative charges, forms dipoles, then this matter itself radiates the energy back out as form of heat or form of new waves (scattered light) in new direction. So far, in reality, it is hard to simulate all these behavior.

While these molecular-level interaction are fascinating from a physics perspective, and implementing this simulation in rendering is computative expensive. When implementing rendering system, we don’t work with individual molecules - instead, we deal with surfaces composed of countlesss molecules interacting together. This collecttive behavior creates some interesting effects:

  1. Group Behavior: Lights interactions with a cluster of molecule behave differently than with single molecules in isolation.
  2. Wave Coherence: When light waves scatter from molecules that are close together:
    • They maintain a coherent relationship since they come from the same source wave.
    • This leads to interference patterns between the scattered wave between scattered waves.
    • These interference patterns significantly affect the final appearance material

To make these complex light interactions more manageable for real-time rendering, we can leverage fundamental principles from optics. Our foundation begins with the concept of homogeneous media - materials that maintain uniform optical properties throughout their volume.

The key characteristic of a homogeneous medium is its Index of Refraction (IoR), a property familiar from basic physics. There are two numbers associated with IOR, one part is to describe the speed of light through the medium, and the other part is to describe how much light are absorbed in medium. But simply put how it bends when crossing boundaries between different materials like we learn in science class.

Then, we assume that there are many molecules or isolated molecules inside of, what we called “Scattering Particle”. These bascially behave similarly as what we mentioned earlier. The right below image basically shows overall combination of absoprtion and scattering property. There are different types of scattering “Rayleigh Scattering” for atmospheric particles, and “Tryndall Scattering” in particle embedded in solids. Also, mie scattering when particle size goes beyond the wavelength.

alt text

In physically-based rendering, we need to design algorithms that simulate how light realistically interacts with surfaces—whether it’s glossy glass, brushed metal, or frosted plastic. When light hits a surface, two major factors influence the outcome:

  • The substances on either side of the surface
  • The surface geometry

The first factor—the materials on either side of the boundary—is governed by the index of refraction (IoR). When a light ray encounters a boundary between two media (e.g., air and glass), it bends according to Snell’s Law:

sin(θt) = (n1 / n2) * sin(θi)

Here, n1 and n2 are the indices of refraction for the “outside” and “inside” media, respectively. Assuming the surface is perfectly flat, Snell’s Law predicts the direction of the refracted ray. This kind of behavior explains how materials like glass or water bend light in a physically accurate way.

According to video, I’ve watched they talk about more geometry of the surface, so we’re going to talk about that! In terms of surface, we can mention nanogeometry in terms of atomic level (smaller than wavelength). What we see the image below is basically diffraction in atomic level creating waves by the Huygens Law.

alt text

When light hits the surface, there are two parts, reflection & refraction. Depending on the surface normal, the direction for reflection can be varied as well as refraction. Such as shown below.

alt text

The great example would be the shown below. Even though we see and percieve the surface as “Similar” shape, but in microscopic level, these have different reflection and refraction. The one above seems to be very reflective, which means the roughness is relatively lower than below, and the other seeems to be relatively blurred.

alt text

The behavior of refracted light depends heavily on the type of material the medium is made of. Broadly, we can divide materials into two categories:

  • Metals (Conductors)
  • Dielectrics (Insulators)

  • Metals (Conductors) In metals, the refracted light doesn’t travel far into the material. Instead, it is quickly absorbed due to the presence of free electrons. These electrons interact with the incoming light, converting much of the refracted energy into heat or re-emitting it as reflected light. This is why metals are highly reflective and often appear shiny, but not transparent.

  • Dielectrics (Insulators) In contrast, dielectrics—like water, glass, or plastic—allow light to enter and travel through the medium. However, as light moves through a dielectric, part of its energy is absorbed or scattered inside the material. For example, if you shine a light above a cup of water, you’ll notice that:

Some of the light reflects off the surface, creating visible highlights. The rest penetrates into the material, where it becomes attenuated due to absorption and scattering within the medium. This process is responsible for effects like subsurface scattering and volumetric absorption, which are essential for rendering realistic materials such as skin, wax, milk, or water.

A common real-world example is holding your finger up to sunlight. You’ll notice a glowing red edge around the silhouette of your finger—this is light scattering beneath the surface of the skin and exiting at different points. It demonstrates how light can enter a translucent material, bounce around internally, and emerge with a diffused, softened appearance. All these are called subsurface scattering.

alt text

If the area below picture are smaller than one pixel, then we can treat them as a local point, treating like one particle as shown on next image.

alt text

alt text

Mathematics

Radiance is a physical quantity that measures the intensity of light traveling along a specific direction — essentially, how much light energy is flowing through a point in a specific direction. In rendering, this typically corresponds to the light that reaches the camera through a pixel along a ray.

Radiance is spectrally varying, meaning it can be described across different wavelengths (or as RGB in discrete form).

The unit of radiance is: W / (m²·sr)

BRDF

The Bidirectional Reflectance Distribution Function (BRDF) defines how light is reflected at an opaque surface. The BRDF is defined as the ratio of the reflected radiance in a specific outgoing direction to the incident irradiance from a specific incoming direction, for given azimuth and zenith angles of both incidence and reflection.

Intuitively, the way a surface appears depends on two things:

  • The direction of incoming light (where the light is shining from)
  • The viewing direction (where the camera or eye is positioned)

alt text

l represent the light direction, and v as a view direction. If you take a closer look on image below, we can actually calculate how much lights are coming through that patch into one points.

alt text

Now, we can truly define the what is really the definition of BRDF. Suppose we are given an incoming light direction ωᵢ (a unit vector pointing toward the surface), and an outgoing/viewing direction: ωₒ (a unit vector pointing away from the surface, typically toward the camera). BRDF can be defined as the ratio of the quantity of relfected light in direction ωₒ, to the amount of light that reaches the surface from direction ωᵢ. Which means, the quqntity of light reflected from the surface in direction ωₒ. Lo, and the amount of light arriving from direction ωᵢ, Ei.

alt text \[f_r(\omega_i, \omega_o) = \frac{dL_o(\omega_o)}{dE_i(\omega_i)}\]

The BRDF is the ratio of the differential outgoing radiance 𝑑𝐿𝑜(𝜔𝑜) in direction 𝜔𝑜 to the differential incoming irradiance 𝑑𝐸𝑖 from direction 𝜔𝑖.

There are two classes of BRDFs and two important properties. BRDFs can be classified into two classes, isotropic BRDFs and anistorpics BRDFs. The two important properties of BRDFs are reciprocity and conservation of energy. Reciprocity states that the BRDF remains unchanged when the directions of incoming and outgoing light are swapped. In other words, the ratio of reflected radiance in one direction to the irradiance from another direction is the same, regardless of whether the directions are reversed:

alt text \[\text{BRDF}_{\lambda}(\theta_i, \phi_i, \theta_o, \phi_o) = \text{BRDF}_{\lambda}(\theta_o, \phi_o, \theta_i, \phi_i)\]

Conservation of energy ensures that the total reflected energy from a surface cannot exceed the total incoming energy. That is, the BRDF must not allow more light to be reflected than is received, ensuring energy is preserved in the system as shown below

alt text

Reflectance Equation

All of the things we cover now reveal as one equation called reflectance equation. Outgoing Radiance from a point equals to the integral of incoming radiance times BRDF times the sign Factor over the hemisphere of incoming directions. Note that it is component-wise RGB multiplication. In detail, Lo is basically what we want to calculate in the pixel shader. Li(l) is the amount of incoming light (RGB). ndotl is the factor that increase and decrease the light power. f(l,v) is the BRDF term, then we integral all that terms. Unit-wise, Lo(V) is RGB, f(l,v) is RGB, Li(l) is also RGB with scalar. So output must be RGB.

alt text

Microfacet Theory

To achieve a visually immersive experience, both the diffuse and specular components of light reflection are important. Let’s begin by examining the specular term.

alt text

Microfacet Theory provides a framework for modeling surface reflection on rough or non-optically-flat surfaces. Rather than treating a surface as perfectly smooth, it assumes the surface is composed of many tiny, flat facets—each acting like a perfect mirror. Depending on the surfaces, BRDFs output term can be varied.

alt text

By zooming in on a small surface region, we approximate it as a collection of microscopic facets, each with its own orientation. At any given point, light may be reflected or refracted depending on the orientation of the microfacets. This interpretation allows us to derive realistic BRDFs that account for the surface roughness and the distribution of microfacet normals.

The half vector is simply the direction that the microfacets must be aligned with in order to reflect light from the light direction L into the view direction V.

In other words, the proportion of light that is reflected from L to V depends on how many microfacets have their surface normals aligned with this half vector. These microfacets act like tiny mirrors that perfectly reflect light when their normals match the half vector. The more microfacets oriented in the direction of H, the stronger the specular reflection in the view direction V. This is basically halfway vector calculation in phong model.

alt text

Depending on the surface geometry, not all microfacets aligned with the half vector can contribute to reflection. For example, if a nearby facet is taller and blocks the incoming light from reaching a microfacet, this is known as shadowing. On the other hand, if the reflected light from a microfacet is blocked in the direction of the viewer, this is referred to as masking. In reality, the some light can reach into shadow area, but Micro BRDFs ignore all that.

alt text

Microfacet Specular BRDF

This is the general form of Microfacet Specular BRDF.

alt text


F(l,h) is Fresenel Reflectance. This basically show depending on the view direction(incident angle) and surface normal, and the RoI. The vale of frasnel can be varied as shown below.

alt text

Since the barely change parts is kind of like a starting point (parameter), so that we can tweak them from there. The image below shows each fresnel value for each metal and dielectric.

alt text

alt text

The speaker mentioned that Schlick Approximiation is good enough to implement.

alt text


The normal distribution D(h), describes how densely microfacet normals are aligned with a given half-vector direction h. In simple terms, it tells us how many microfacets are oriented in a way that would reflect light from the incoming direction L toward the outgoing view direction V. Since perfect specular reflection happens only when the microfacet normal matches the half-vector (the vector halfway between light direction L and view direction V), D(h) essentially controls the shape and sharpness of the specular highlight. You can actually see all those functions in Cook-Torrance


Finally, the geometry function, often denoted as G(l, v, h), accounts for shadowing and masking effects caused by the microgeometry of a surface. Shadowing occurs when incoming light (from direction l) is blocked by parts of the surface before it can reach a microfacet. Masking happens when the reflected light (toward the view direction v) is blocked by other parts of the surface, preventing it from escaping.

So, G(l, v, h) tells us how much of the microfacet reflection is actually visible, based on how the surface self-occludes due to its roughness.

The image below is commonly used, smith function, to illustrate the concept of the geometry function, as it has been both mathematically and physically validated:

alt text


Putting It All Together

  • D(h) (Normal Distribution Function): Describes how many microfacets are oriented in the direction of the half-vector h, which is the direction needed to reflect light from L to V. It essentially tells us how aligned the surface microfacets are with the ideal reflection direction.

  • F (Fresnel Term): Tells us how reflective each of those microfacets are, depending on the viewing angle and material properties.

  • G(l, v, h) (Geometry Function): Tells us how many of those microfacets are visible and not occluded, meaning they can actually participate in reflecting light from the light direction L to the view direction


In physically based rendering (PBR), we often split surface reflection into diffuse and specular components. The diffuse term accounts for the light that enters a surface, scatters beneath it, and then exits in a different direction.

alt text

Lambertian Diffuse Model assumes light is scattered equally in all directions, and the surface looks the same from all viewing angles. Fairly simple and works well for matte surfaces like a paper.

Diffuse BRDF (Lambert): 𝑓𝑑 = C𝑑 / 𝜋, where the C𝑑 is the diffuse color. In real world, the rougher surfaces doen’t scatter light perfectly evenly. For example, skin, cloth have subsurface scattering and edge darkening. A specular reflection becomes sharper, the diffuse should became broader (vice versa). Thus Lambertian diffuse isn’t always phsycially accurate when the rough microfacet is applied.

The speaker also mentioned that Diffuse Roughness = Specular Roughness. It’s because of the assumption. “If the surface is rough for specular, it must also be rough for diffuse” but the assumption is wrong because specular roughness comes from surface microgeometry, and diffuse roughness is caused by the subsurface scattering, material properties, and light diffuion inside the medium..

Results

Of course, this is a summary based on content from SIGGRAPH and various other sources, so it might not be perfect or comprehensive. However, I thought it would be helpful for the understanding stage, so I put together a blog post to organize the information.

Resource

DirectX11 - Image Based Lighting

이미지 기반 Lighting 도 결국엔 Physically Based Rendering 과 비슷한 Technique 이다. Unreal Engine 에서도 사용이 된다고 한다. 결국에는 “Lighting Based on Data Stored in An Image” 라고 생각을 하면 된다.

Physically Based on Shading 안에서 일단, Lighting 에는 두개의 정보가 있다. Direct Diffuse, Direct Specular, Indirect Diffuse, Indirect Specular Term 이 있다. 일단 Diffuse 자체는 얼마나 빛이 Scatter 되는지를 나타낸다. (그래서 Labertian Reflectance) 하지만 아래의 그림 처럼 Material Definition 을 정확하게는 알지못한다. (하지만 Roughness 과 matte 는 판단 가능하지만, Glossy 는 모른다.)

alt text

이걸 판단하기 위해서는, Specular Reflectance 가 필요하다. 아래처럼 matte 같은 경우에는 대부분 Bounce 되지만, Glossy 인 물체에는 Camera Align 되면서 Single Direction 인 빛을 반사한다. 이말은 결국에는 카메라 시점에 따라서 빛이 물체에 어디에 빛을 비추는지가 Tracking 이 된다는 소리이다.

alt text

Direct 와 Indirect 의 차이를 보자면, 결국엔 DirectLight 과 PointLight 이 대표적인 Source 인것 같다. Indirect 같은 경우에는 DirectLight Source 가 어떠한 물체에 부딫혔을때, 반사광들이 Indirect Light Source 라고 말할수 있다. 아래의 그림을 보자면, Point Light 은 결국엔 Diffuse Direct Light 이며, 물체에 부딫혔을때 반사되는 빛들이 Indirect Light 이라고 할수 있다. 그 다음 이미지가 Indirect Diffuse Lighting 에 의해서 주변이 밝아지는 현상을 뜻한다. (즉 이말은 Directional Light 보다 더 밝게 주변이 빛춰진다.)

alt text alt text

Indirect Specular 같은 경우는 High Glossy 에서 나타내는 현상중에 하나이다. 아래 처럼 glossiness 가 높으면 높을수록 반사되는게 보인다? 반사되는게 보인다는건, Model Mesh 주변(Enviornment) 를 볼수있다 라고 말할수 있다.

alt text

이제 IBL 하고 연관되게 설명을 하자면, 이렇게 Indirect 를 모두 계산을 할수는 있지만 아무래도 한계점이 있다. 그래서 하나의 Light Source 가 있다고 가정하고, 모든 면에서 사진을 찍어서, Lookup Texture 로 만들면 좋지 않을까? 라는 형태가 Cubemap 형태 인것이다.

alt text

그리고 Unreal 에서 사용되는 Image-Based Lighting 의 공식은 아래와 같다. 그리고 이 공식에 대한 HLSL 에 대한 설명은 여길 찾아보면 좋을것 같다.

alt text


위 처럼 결국에는 Cubemap 이 어떻게 생성이됬고, 어떤게 IBL 을 어떻게 계산하는지는 Environmental Mapping 에서 보았다. 그럼 구현 단계를 생각을 해보자.

일단 IBL Texture 같은 경우, 위에서 이야기한것처럼 specular / diffuse IBL DDS Files 예제 들이 존재 할것 이다.

그리고 코드로서는, CubeMapping Shader 에서는 Specular (빛이 잘 표현되는) Texture 만 올리고, Model 을 나타내는 Pixel Shader 에서는 Specular 와 diffuse texture 을 둘다 올리면 된다. (CPU 쪽 Code 는 생략)

그래서 HLSL 에서는, Cube Texture 를 받아서, Sampling 을 하고, 평균을 내서 Specular 는 표현을 하고, Texture 를 사용한다고 했을시에는 아래 처럼 분기처리해서 Diffuse 값에다가 은은하게 표현이 가능하다.

Texture2D g_texture0 : register(t0);        // model texture
TextureCube g_diffuseCube : register(t1);   // cube (diffuse)
TextureCube g_specularCube : register(t2);  // cube (specular)
SamplerState g_sampler : register(s0);

float4 main(PixelShaderInput input) : SV_TARGET
{
    float4 diffuse = g_diffuseCube.Sample(g_sampler, input.normalWorld);
    float4 specular = g_specularCube.Sample(g_sampler, reflect(-toEye, input.normalWorld));
    specular *= pow((specular.x + specular.y + specular.z) / 3.0, material.shininess);
    
    diffuse.xyz *= material.diffuse; // separtae r, g, b   
    specular.xyz *= material.specular;
    
    if (useTexture)
    {
        diffuse *= g_texture0.Sample(g_sampler, input.texcoord);
    }
    return diffuse + specular;
}

결국에는 마지막은 Phong-Shading 처럼 덧셈(aggregate)을 해준다. Parameters 를 잘섞어서, 내가 이 Model 을 잘 표현했다는 결과는 아래와 같다.

alt text

Resource

DirectX11 - Fresnel & Schlick Approximation

“Fresnel Reflection describe the behavior of light when moving between media of differing refractive indices”. 결국 말을 하는건 빛이 다른 굴절률(Refractive Index) 가지고 있는 물질(Media) 에 어떻게 표현하는지를 나타낸다고 한다. 아래의 그림처럼, 다른 n1, n2 물질이 있다고 하면, 빛은 일부는 반사하고, 일부는 굴절한다. 그걸 표현한게 아래의 그림이다. 그리고 Snell’s law 를 통하면, 각 Incident Angle 과 Refractive Angle 의 관계를 표현을 하면 sin(theta(i)) / sin(theta(t)) = n2/n1 표현을 할수 있다.

alt text

그렇다면, 이 식을 어떻게 이제껏 사용했던걸로 사용하자면, Specular(반사광) 에다가 곱해주면 된다. 일단 물질의 고유의 값을 표현할 NameSpace 로 묶어 준다. 그리고 결국엔 이 값들을 Material 값들을 ConstantBuffer 안에다가 같이 넣어주면 된다.

namespace FresnelConstant {
constexpr Vector3 Water(0.02f, 0.02f, 0.02f);
constexpr Vector3 Glass(0.08, 0.08, 0.08);
constexpr Vector3 Plastic(0.05, 0.05, 0.05);
constexpr Vector3 Silver(0.95, 0.93, 0.88);
constexpr Vector3 Copper(0.95, 0.64, 0.54);
}; // namespace FresnelConstant

struct Material {
    Vector3 ambient = Vector3(0.0f);  // 12
    float shininess = 0.01f;           // 4
    Vector3 diffuse = Vector3(0.0f);  // 12
    float dummy1;                     // 4
    Vector3 specular = Vector3(1.0f); // 12
    float dummy2;                     // 4
    Vector3 fresnelR0 = FresnelConstant::Water; // 12
    float dummy3;
};

그이후에 CPU 에서 어떻게 Resource 가 Binding 되는건 생략을 하도록 하자.

HLSL 쪽을 한번 보자. SchlicFresnel 의 공식은 아래와 같다.

alt text

그리고 HLSL 에서 표현을 하면, 아래와 같이 표현이 가능하다. 여기에서 1 - cosTheta 가 결국에는 내가 바라보는 각도에따라서 Normal 과 90 도라고 한다면, 0 이 되므로, 가장자리쪽이고, 0 에 가까우면 가장자리가 아니라는것이다, 즉 가장자리라고 함은 빛을 더많이 받고, 그렇지 않는 경우에는 값이 작은 값들이 들어오고, Cubemap 에 있는걸 그대로 빛출것이다.

float3 SchlickFresnel(float3 fresnelR0, float3 normal, float3 toEye)
{
    float cosTheta = saturate(dot(normal, toEye));
    return fresnelR0 + (1.0 - fresnelR0) * pow(1 - cosTheta, 5.0);
}

결과는 아래와 같다. 아래의 그림처럼, 가장자리 쪽에는 굉장히 많은 빛을 받아서, Specular 값이 쎄고, 안쪽은 빛을 덜받기에 약간의 Diffuse 를 얻는것을 확인 할 수 있다.

alt text

Resource

DirectX11 - Cube Mapping

환경적 요소를 다루기 위한 한걸음인것 같다. Cube Mapping 이란건 쉽게 말해서 카메라가 거대한 상자 안에 있는것과 같다. 즉 아래의 그림처럼, 거대한상자는 십자가 패턴으로, 총 6 개의 Textures 로 이루어져있다. 일반 Texture 와 다른 부분이 하나 있다. 전에 같은 경우는 Texture 를 입힌다라고 했을때 Mesh 에다가 Texture 좌표계를 입혔어야했다. Cube Mapping 같은 경우에는 DirectX 내부에서 알아서 해준다고 볼수 있다.

alt text

그러면 DirectX 에서 어떻게 Cube Mapping 을 하는지 해보자.

CPU Side


auto skyBox = L"./CubemapTextures/skybox.dds"
ComPtr<ID3D11ShaderResourceView> cubemapResourceView; 
ComPtr<ID3D11Texture2D> texture;

HRESULT hr = CreateDDSTextureFromFileEx(
    m_device.Get(), filename, 0, D3D11_USAGE_DEFAULT,
    D3D11_BIND_SHADER_RESOURCE, 0,
    D3D11_RESOURCE_MISC_TEXTURECUBE,
    DDS_LOADER_FLAGS(false), (ID3D11Resource **)texture.GetAddressOf(),
    cubemapResourceView.GetAddressOf(), nullptr);

if (FAILED(hr)) {
    std::cout << "Failed To Create DDSTexture " << std::endl;
}
// -----

// ConstantBuffer
BasicVertexConstantBuffer {
    Matrix model;
    Matrix inverseTranspose;
    Matrix view;
    Matrix projection;
};

static_assert((sizeof(BasicVertexConstantBuffer) % 16) == 0,
              "Constant Buffer size must be 16-byte aligned");

std::shared_ptr<Mesh> cubeMesh;
cubMesh = std::make_shared<Mesh>();
BasicVertexConstantBuffer m_BasicVertexConstantBufferData;
m_BasicVertexConstantBufferData.model = Matrix();
m_BasicVertexConstantBufferData.view = Matrix();
m_BasicVertexConstantBufferData.projection = Matrix();

ComPtr<ID3D11Buffer> vertexConstantBuffer;
// This is custom Method
CreateConstantBuffer(m_BasicVertexConstantBufferData, cubeMesh->vertexConstantBuffer);

MeshData cubeMeshData = MeshGenerator::MakeBox(20.0f);
std::reverse(cubeMeshData.indicies.begin(), cubeMeshData.indicies.end());

CreateVertexBuffer(cubeMeshData.vertices, cubeMesh->vertexBuffer);
CreateIndexBuffer(cubeMeshData.indices, cubeMesh->indexBuffer);

ComPtr<ID3D11InputLayout> inputLayout;
ComPtr<ID3D11VertexShader> vertexShader;
ComPtr<ID3D11PixelShader> pixelShader;

vector<D3D11_INPUT_ELEMENT_DESC> basicInputElements = {
    {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
     D3D11_INPUT_PER_VERTEX_DATA, 0},
};


CreateVertexShaderAndInputLayout(
    L"CubeMappingVertexShader.hlsl", basicInputElements,
    vertexShader, inputLayout);

CreatePixelShader(L"CubeMappingPixelShader.hlsl", pixelShader);

// Render omit: But this is just update the constant buffer.

// Update: Pipeline for cube mapping
m_context->IASetInputLayout(inputLayout.Get());
m_context->IASetVertexBuffer(0, 1, cubeMesh->vertexBuffer.GetAddressOf(), &stride, &offset);
m_context->IASetIndexBuffer(cubeMesh->indexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);
m_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
m_context->VSSetShader(vertexShader.Get(), 0, 0);
m_context->VSSetConstantBuffers(0, 1, cubeMesh->vertexConstantBuffer.GetAddressOf());
ID3D11ShaderResourceView *views[1] = { cubemapResourceView.Get() };
m_context->PSSetShaderResources(0, 1, views);
m_context->PSSetShader(pixelShader.Get(), 0, 0);
m_context->PSSetSamplers(0, 1, m_samplerState.GetAddressOf());

// Start Drawing
m_context->DrawIndexed(cubeMesh->m_indexCount, 0, 0);
  1. CubeMap Mesh 를 불러온다. (이때 Cubemap 은 Box Mexh, 단 주의점은 너무 크다보면, Camera 시점에서, Far Point 의 조정이 필요하다.)
  2. 그 이후에는 CubeMap Texture 를 불러온다. 위의 십자가 모형을 만든 Format (.dds) 를 불러온다.
  3. Cubemapping 을 하기위해서는, Shader 가 따로 필요하다 (VertexShader & PixelShader). 그리고 기존에 사용하던 Constant Buffer 도, 일단 수정하지말고 그대로 불러오는 작업을 한다.
  4. Important: CubeMap 같은 경우 카메라가 큐브맵 안에 있으니, 큐브맵이 BackFace 를 바라보는건데, 큐브맵이 Front Face 로 돌려줘야한다. 그러기위해선 기존에, 정점 CCW 로 되어있는게 Front Face 이므로, BackFace(CW) 에 있었던 정점정보들을 CCW 려 돌려줘야한다. (조그만더 정리하자면, 원래 카메라에서 물체를 바라볼때, DirectX 에서는 CCW 로 정점이 생성되서, FrontFace 라고 확인이 되지만 뒤로 봤을때는 이게 CW 로 되어있다. 그래서 Culling 이라는게 존재하는것이다.) 또 다른 방법은 D3D11_CULL_MODE::D3D11_CULL_NONE 을 하면된다.
  5. BackFace 에서 FrontFace 로 변경이후, vertex 와 index 의 정보를 Buffer 에다가 넣어준다.
  6. 새로운 Shader 에서 정의를 하니, InputLayout 도 정의를 하고, VertexShader 와 PixelShader 를 생성해준다.
  7. Render 부분에서는 ConstantBuffer Model 의 Position 만 Update 해주는 방식으로 해주는 대신에, 빈 Matrix 를 넣어줘야한다. 왜냐하면 카메라 시점이 바꾼더라도, Mesh Model 은 변경이 안되어야하므로…
  8. 그리고 Update 하는 부분쪽이서는 모든 Resources 들을 Shader 에서 볼수 있도록 Binding 을 해준다.

GPU

PixelShaderInput main(VertexShaderInput input)
{
    PixelShaderInput output;
    float4 pos = float4(input.posModel, 1.0f);

    pos = mul(pos, model); // Identity
    output.posWorld = pos.xyz;
    pos = mul(pos, view);
    pos = mul(pos, projection);
    output.posProj = pos;
    output.color = float3(1.0, 1.0, 0.0);
    return output;
}

TextureCube g_textureCube0 : register(t0);
SamplerState g_sampler : register(s0);

float4 main(PixelShaderInput input) : SV_TARGET
{
    return g_textureCube0.Sample(g_sampler, input.posWorld.xyz);
}
  • Vertex Shader 에서는 output 에 pos 를 직접 넣어주고, 그리고 Pixel Shader 에서는 Sampling 만 하면 된다.

결과는 아래와 같다. alt text

이러다보면 이상한 점이 있기는한데, 그게 Texture 의 고화질이라고 하면, 사실상 Texture 에 있는 빛처럼 보이는 Source 도 있기 때문에 실제로 어디에 Light Source 가 있는지를 모른다. 그리고, Light Source 로 인해서, Model 의 색깔이나, Shadowing 이런게 잘 보이질 않는다 이걸 해결할수 있는 기법 중에 하나가 Environment Mapping 이다.

DirectX11 - Environment Mapping

결국에는 mapping 을 잘된 결과를 가지고 오기위해서는 Texture 의 Quality 가 좋아야, 그만큼의 조명이 돋보이고, 더 사실적인 Rendering 이 비춰질수 있을것 같다는 생각이든다. 그렇다면 어떻게 하는게 좋을까? 다시 생각을 해보자. 모델이 있다고 하면, 내가 바라보는 시점으로 부터 Model 의 Normal 을 타고 들어간다고 하고, 반사되었을때의 CubeMap 의 Texture 의 값을 가지고 오면 될것 같고, 바라보는 방향과 상관없는것들은 CubeMap 의 Texture 를 그대로 가지고 오면될것 같다.

그러면 변화해야할것들은 무엇인가? 하면, 바로 CubeMap 의 ResourceView 를 넘겨주면 될것 같다. 그러면 다시 CPU 쪽에서 작업을 해주자. 그말은 즉슨 Model 에 우리가 그리는 정보를 줘야하니. Model 에다가 CubeMap 이 그려진 ResourceView 를 Binding 해주면 된다는 의미이다. 그러면 기존의 Pixel Shader 에가 TextureCube 를 넣어주면 된다.

// When Render is Called.
// 1. Drawing the CubeMapping on cubmapResourceView
// 2. For each Mesh, you set pixel shader for binding two resesources.
ID3D11ShaderResourceView *resViews[2] = {
    mesh->textureResourceView.Get(), cubemapResourceView.Get() };
m_context->PSSetShaderResources(0, 2, resViews);

// Updating the constant buffer is omitted for simplicity

// Then on GPU
Texture2D g_texture0 : register(t0);            // Texture on Model
TextureCube g_textureCube0 : register(t1);      // Cubemap Texture
SamplerState g_sampler : register(s0);          // Sampler State

struct PixelShaderInput
{
    float4 posProj : SV_POSITION; // Screen position
    float3 posWorld : POSITION;
    float3 normalWorld : NORMAL;
    float2 texcoord : TEXCOORD;
    float3 color : COLOR;
};

cbuffer BasicPixelConstantBuffer: register(b0)
{
    float3 eyeWorld;
    bool dummy1;
    Material material;
    Light light[MAX_LIGHT];
    float3 rimColor;
    float3 rimPower;
    float3 rimStrength;
    bool dummy2;
}

float4 main(PixelShaderInput input) : SV_TARGET 
{
    float3 toEye = normalize(eyeWorld - input.posWorld);
    // Lighting ... 
    
    return g_textureCube0.Sample(g_sampler, reflect(-toEye, input.normalWorld));
}

위의 방식대로 Sample 을 많이 다루다 보면, Sample 의 두번째 Parameter 는 Location 이다. 즉 GPU 에게 Texture 의 어느 위치를 Sampling 할건지를 물어보는거다. 그리고 reflect(…) 같은 경우는 toEye 가 결국에는 Camera 에서 Model 로 향하는 벡터라고 하면, Incident Vector 로 바꿔줘야한다(-toEye) 그리고 Model 의 Normal 을 World 좌표계에서 보여지는걸 넣어주면, 결국엔 그 반사된 Vector 로 부터 Sampling 을 할수 있다는것 이다.

결과는 아래와 같다. 아래 Cubemap 에 위치해있는, 빛의 일부분이 그대로 반사되서 구에 맺히는걸 볼수 있다. 굉장히 아름다운 Graphics 인것 같다.

alt text

Resource

DirectX11 - How to import Model Import in DirectX11.

처음으로, Model Import 코드를 짜보고 테스트를 해보았아. Library 는 Assimp 로 3D Model 을 Load 를 하면 된다. 일단 기본적으로 Modeling 에 앞서서 Blender 를 확인 해보자.

alt text

이러한식으로 Blender 에서, Model 을 Import 할때 여러가지의 Formats 이 보인다. 내가 다뤄봤던건 아래와같다.

  • Standford Rabit (.ply) => Point Cloud Data 옮길때
  • glTF 2.0 (.glb/.gltf) => 이건 기억이 잘안나지만, 이게 최신인걸로 알고 있다.
  • Wavefront (.obj) => 이건 정말 많이 다뤄본것 같다. (차량 Model, Radar Model, etc..)

일단 3D Modeling Import 는 Assimp Library 로 충분하다. 각 모델을 확인해보면 각 Format 별로 Vertex, Index, Normal 값들이 존재하며, 또 Parts 별 Texture 가 존재한다. 가끔씩은 Normal 값이 존재 안할수도 있는데, 이건 따로 처리해야할필요가 있다. 그리고 확실히 std::fileSystem c++17 부터 나와서 훨씬 Path 정리하는게 편하다.

코드는 StackOverflow 나 GameDev 에서 작성하였다.

결과를 한번 보자면 아래와 같다. 이런저런 Free Model 이 있는데, sketchFab, glTF, CesiumGS, and f3d 이렇게 있다.

나는 어렸을때, Dota 를 좋아해서, 진짜 Dota Character 인지는 모르겠지만, F3D 에서 Dota 캐릭터를 가지고 왔다. 충분히 Animation 도 있는것 같은데, 아직 Animation 의 구현은 멀어서,,, 일단 Import 된것 까지 한번 봐보자.

alt text

DirectX11 - Introduction to Rim Effect

이제는 조금은 재밌는 장난을 더해서, Lighting 을 개선해보는게 좋을것 같다. Rim Effect 는 물체의 가장자리 (Edge) 에 빛이 강조되어 반사되는 효과를 뜻한다. 주로 역광 (Backlighting) 상황에서 발생하여, 물체의 윤곽을 강조한다. 즉, 빛이 물체를 직접 비추지 않아도 가장자리에서 빛이 새어 나오는 현상을 뜻한다.

사용되는 용도를 조금 찾아보니, 캐릭터의 윤곽을 강조하거나, 뭔가 환상적인 분위기를 연출하거나, Material 을 표현 할때, 투명하거나 빛을 산란시키는 재질의 특성을 표현한 것이다.

그렇다면 Edge 를 찾으려면 어떻게 해야하는걸까? 일단 생각을 해보면, Model 의 Normal Vector 와 카메라 시점에서 바라본 Vector 의 Normal 값이 90 도가 됬을때, Edge 라고 판별을 할 수 있을것같다. 이제 HLSL 에 적용을 해보자.

이미 이제껏 VertexShader 에 넣어준 ConstantBuffer 의 느낌은 Model, InverseTranspose, view, projection 이 있다. 그리고 각각의 Shader 에서 main 함수의 Parameter 같은 경우는, 아래와 같다.

cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix model;
    matrix inverseTranspose;
    matrix view;
    matrix projection;
};

struct VertexShaderInput {
    float3 posModel : POSITION;
    float3 normalModel : NORMAL;
    float2 texcoord : TEXCOORD0;
}

struct PixelShaderInput {
    float4 posProj : SV_POSITION;
    float3 posWorld : POSITION;
    float3 normalWorld : NORMAL;
    float2 texcoord : TEXCOORD;
    float3 color : COLOR;
}

결국엔 Rasterization 이 끝난 이후 Pixel Shader 에서 색상의 값을 결정해줘야 하므로, Pixel Shader 에서 처리가 가능하다.

cbuffer PixelShaderConstantBuffer : register(b0)
{
    float3 eyeWorld;   // 12 bytes
    bool dummy;        // 4 bytes (16 bytes alignment)
    Material material; // 48 bytes
    Light light[MAX_LIGHTS]; // 3 x 48 bytes = 144 bytes
    float3 rimColor;   // 12 bytes
    float rimPower;    // 4 bytes (16 bytes alignment)
    float rimStrength; // 4 bytes (padding: 12 bytes)
};

float4 main(PixelShaderInput input) : SV_TARGET 
{
    float3 toEye = normalize(eyeWorld - input.posWorld);
    float3 rim = pow(1.0 - saturate(dot(input.normalWorld, toEye)), rimPower);
    // It is optional to use `smoothStep`.
    float3 rimColor = rimStrength * rimColor;
    color += rimColor;
    return float4(color, 1.0);
}

항상 조심해야 하는건, ConstantBuffer 는 16 의 배수여야한다! 그리고 Specular 를 계산을 할때, pow() 라는걸 사용했었다. 그걸 적절하게 사용을하고, 1.0 - dot(input.normalWorld, toEye) 를 하는데에 있어서는, 각도의 90 도 일때, dot product 값은 0 이다. 이걸 확대하게하기 위해서, 1 을 빼주는것이다. 다른 방법으로는 Saturate 대신에, smoothstep 을 사용해도 괜찮은 결과가 나오는것 같았다.

결과는 아래와 같다.

alt text

DirectX11 - Sphere Modeling & Subdivision & face normal

앞에 Post 를 봤더라면, 이제 Sphere 는 cylinder 의 맨위와 아래의 radius 를 묶으면 되지 않느나라는 질문을 할수 있다. 맞다! 그리고 Stakc 이 총 6개라면, 6 개만큼을 아래서부터 각도를 줘서 구처럼 구부리면 될수 있다.

그러면 수식으로 세우면 이렇다, Vector 의 위치 (0, -radius, 0) 부터 시작해서, Z 축 으로 해서 높이를 쌓고, y 축으로 Vertex 를 돌리면 된다. 그말은 첫 Vertex Point 로 부터 Phi 각도를 점차 점차 올라가면서, Transform 을 해주면 된다. 그래서 간단하게 SimpleMath 를 사용한다면 아래의 코드처럼만 변경하고, Indices 와 Normal 은 Cylinder 와 마찬가지로 하면 된다.

// loop in stacks
const flaot dTheta = -XM_PI / float(numSlices);
const float dPhi = -XM_PI / float(numStacks);
Vector3 startPoint = Vector3::Transform(Vector3(0.0, -radius, 0.0), Matrix::CreateRotationZ(dPhi * i));

// loop in slice
// from x-z plane, rotate
Vertex v;
v.position = Vector3::Transform(stackPoint, Matrix::CreateRotationY(dTheta * float(j)));

alt text alt text

Subdivision

Subdivision 은 여러 방법이 있을텐데, 제일 기본적인 방법은 Triangle Mesh 를 4 개 만들어주는거다. 즉 Vertex Point 들을 부풀려서 준다고 생각을 하면된다. 근데 굳이? 왜 CPU 단에서 부풀리려고 하냐? 굳이 이렇게 만들 필요가 있냐? 라고 하면, Subdivision 같은 경우는 GPU 에서도 충분히 돌릴수 있다. 그래서 아래의 그림을 보면 모형의 조금더 부드럽게 더 늘려서, 물체의 형태를 실제와 같이 Vertex 를 늘려주는 역활이라고 할수 있다.

alt text

alt text

앞서 말했다 싶이, 제일 기본적인 아이디어는 아래와 같다. 하나의 Triangle Mesh 가 있다고 하면, Vertex 와 Vertex 사이에 중간의 Vertex 를 둬서 다시 연결해줘서 삼각형을 1개에서 4개로 증가 시켜주는 알고리즘이라고 볼수 있다. 그래서 알고리즘은 이렇다. 만약 기존의 Mesh Data 의 Normal 값을 radius 와 곱한다. 즉 vertex position 을 다시 구의 표면위에 작업하는 작업이라고 볼수 있다.

그리고 이러한 방식이 필요한데, 이건 Subdivison 을 할때마다, Texture 들의 좌표가 깨질수도 있으니, 이음매를 엮어주는 형식이다. 그리고 새로운 Mesh Data 에 순서대로 넣어준다고 하고, Indices 들도 정하면, 아래처럼 여러개의 Step 을 돌렸을때 구형이 더 구형처럼 보이게 될것이다.

const float theta = atan2f(v.position.z, v.position.x);
const float phi = acosf(v.position.y / radius);
v.texcoord.x = theta / XM_2PI;
v.texcoord.y = phi / XM_PI;

Result

alt text alt text alt text

Face Normal

사실상 Vertex 를 기준으로해서 즉 하나의 각을 기준으로 해서도 우리는 다른 Normal 을 가진다고 생각을 할수 있다. 즉 각 Vertex 별 Face Normal 이 다르다는 말이다. 이건 구현의 그림으로 보면 편할것 같다. 그리고 삼각형에서 결국에는 Face Normal 을 구하는건 하나의 Vector 에서 다른 하나의 Vector 의 Cross Product 한 결과 값이다.

alt text

Resource

DirectX11 - Sphere Mapping

아마 전에 Sphere Modeling 을 Post 를 봤다고 한다면, 약간의 더러움이 보였을거다. 그게 뭔말이냐면… 삼각형이 균일하게 만들어지지 않았다는 점이다. 그리고 아무리 저번처럼 Texture Mapping 에 필요한 공식을 썻더라고 한더란들, 문제는 해결되지 않는다.

일단 그림 부터 한번 참고를 해보는게 좋을것 같다. 일단 최악의 상황일때, 즉 Subdivision 을 안했을때를 한번 봐보자. 이상태에서 보면 저렇게 끊겨져 있는걸 볼수 있을거다. 그 이유중에 하나는, 바로 Texture 의 삼각형안에서 Interpolation 을 하려고 보니 생기는 이슈이다.

alt text

그래서 다시 Subdivision 을 해서 일단 완벽한 구형의 모델을 만들어보자. Wireframe 을 볼수 있다. 아래처럼 일정하게 삼각형들을 그리게 된다고 하면, 아래처럼 똑같이 나온다.

alt text alt text

이거에 대한 원인은 Cylinder 를 조금 다시 돌아보면 좋을것 같다. Subdivision 을 했을시에 결과값이 Texture 좌표계에서 0 과 1 로 딱떨어지는 경우에는 잘 Mapping 이 될거다. 하지만, 만약에 Texture 좌표에 애매하게 들어가있더라고 하면 0 과 1 사이를 Interpolation 을 시켜버려서, 저렇게 Pixel 값이 흐트러지는 현상이 나온다. 그래서 해결방법은 그렇다. Texture Coordinate 를 Vertex 단위로 Mapping 하는게 아니라, Pixel 단위로 넘기게 된다면 해결이 될문제이다. 즉 다시 uv 값을 계산해서 Pixel Shader 에서 그리게 하면 된다. uv 값을 계산하는식은 전 Post 에 있으니 그 코드를 Shader 코드로 넘기면 된다.

그걸 해결하기위해서는 이제 Shader Programming 으로 들어가는거다. 이제까지 PixelShader 에서 받는 Input 같은 경우에, projection, world, normalworld, texcoord, color 값을 Parameter 로 CPU 에서 GPU 쪽으로 넘기고 있었다. 이때 model 에 대해서 position 값을 같이 넘겨주면 일단 첫번째 Step 이다. 그리고 Pixel Shader 에서 계산을 할때, uv 를 다시 계산해서, 아래처럼 Sampler 을 시키면 해결이 된다.

struct PixelShaderInput
{
    float4 posProj : SV_POSITION;
    float3 posModel : POSITION0;
    float3 posWorld : POSITION1;
    float3 normalWorld : NORMAL;
    float2 texcoord : TEXCOORD;
    float3 color : COLOR; 
};

float2 uv;
uv.x = atan2(input.posModel.z, input.posModel.x) / (3.141592 * 2.0) + 0.5;
uv.y = acos(input.posModel.y / 1.5) / 3.141592;

return float4(color, 1.0) * g_texture0.Sample(g_sampler, uv);

결과는 아래와 같다. 발생했었던 그 끊기는 현상이 깔끔하게 사라지는걸 확인할수 있다.

alt text

Pagination