Chapter 9. Efficient Shadow Volume Rendering
원문: http://http.developer.nvidia.com/GPUGems/gpugems_ch09.html
Chapter 9. Efficient Shadow Volume Rendering
Morgan McGuire
Brown University
9.1 Introduction
A security guard's shadow precedes him into a vault—enough advance warning to let the thief hide on the ceiling. Ready to pounce on an unwary space marine, the alien predator clings to a wall, concealed in the shadow of a nearby gun turret. Yellow and red shadows of ancient marbled gods flicker on the walls of a tomb when the knight's torch and the druid's staff illuminate the statues inside. These are just a few vivid examples of how real-time shadows are used today in gaming.
경비원의 그림자가 둥근천장으로 그를 앞선다-도둑을 천장에 숨기게 하는 충분한 사전 예고이다. 방심한 우주 해병을 덮치려고 하는 외계 포식 동물이 포탑 근처의 그림자에 숨어 벽에 달라붙어있다. 기사의 횟불과 드루이드의 지팡이가 조각상 내부를 비출 때 고대 신들의 대리석상의 노랗고 빨간 그림자가 무덤의 벽에 깜박인다. 이것들은 오늘날 실시간 그림자가 게임에서 어떻게 사용되는지에 대한 단지 몇가지 예일 뿐이다.
Real-time shadows are now required for new 3D games. Gamers are accustomed to the perceptual, strategic, and cinematic benefits of realistic lighting. Unlike other effects, shadows aren't rendered objects. Instead, they are areas of the screen that are darker than others because they receive less light during illumination calculations. The hard part of adding shadows to a rendering engine is finding those areas in real time. This chapter describes how to use shadow volumes, the shadowing method used in games such as id Software's Doom 3, to mark shadowed pixels in the stencil buffer. See Figure 9-1. once each pixel is classified as shadowed or illuminated, it's simple to modify the pixel program responsible for lighting in order to zero out the illumination contribution at shadowed pixels.
실시간 그림자는 이제 새로운 3D 게임에 요구된다. 게이머들은 실제 같은 조명의 지각적이고 전략적이고 영화적인 이점에 익숙해져있다. 다른 효과와 달리, 그림자는 물체를 렌더링하지 않는다. 대신, 조명 계산동안 더 적은 빛을 받아서 다른 지역보다 더 어두운 스크린의 영역이다. 렌더링 엔진에 그림자를 추가하는 것의 어려운 부분은 실시간으로 그런 지역을 찾아내는 것이다. 이 챕터는 id Software의 Doom 3같은 게임에서 사용된 그림자 방식인, 스텐실 버퍼에 그림자 지역 픽셀을 표시하는 그림자 볼륨을 어떻게 사용하는지 설명한다. 그림 9-1을 보라. 일단 각 픽셀이 그림자가 드리워지는지 빛을 받는지 분류가 되면, 그림자 영역의 픽셀에 조명 기여도를 없애기 위해 조명을 책임지는 픽셀 프로그램을 수정하는 것은 간단하다.
9.1.1 Where to Use Shadow Volumes
The shadow volume technique creates sharp, per-pixel accurate shadows from point, spot, and directional lights. A single object can be lit by multiple lights, and the lights can have arbitrary colors and attenuation. The shadows are cast from triangle meshes onto whatever is in the depth buffer. This means that the objects being shadowed can be meshes, billboards, particle systems, or even prerendered scenes with depth buffers.
그림자 볼륨 테크닉은 점, 스포트라이트, 방향성 광원으로부터 날카롭고 픽셀당 정확도의 그림자를 생성한다. 하나의 물체는 여러 개의 빛을 받을 수 있고, 빛은 임의의 색과 감쇠를 가질 수 있다. 그림자는 깊이 버퍼에 있는 어떤 것이든지 그 위에 삼각형 메쉬로부터 캐스트된다. 이것은 그림자가 드리워진 물체는 깊이 버퍼에 있는 메쉬, 빌보드, 파티클 시스템, 또는 미리 렌더링된 장면까지도 될 수 있다는 것을 의미한다.
Compared to other algorithms, shadow volumes can handle many difficult-to-shadow scenes well. Figure 9-2 shows one such problematic scene. The only light source is a point light inside the jack-o'-lantern. The entire scene is in shadow except for the triangular patches of ground illuminated by light that shines out through the holes in the pumpkin. This is a hard case for several reasons. It inverts our usual assumption that most of the scene is lit and shadows are small—rarely do shadows enclose the entire scene. The lit areas are very large compared to the holes in the pumpkin that create them. Although light shines out through only the front and the bottom, the light is omnidirectional and shadows must be considered from all angles. Finally, the shadow caster is more than close to the light source: it surrounds it.
다른 알고리즘과 비교하면, 그림자 볼륨은 많은 그림자지기 어려운 장면을 잘 다룰 수 있다.그림 9-2는 그러한 문제의 장면을 하나 보여준다. 유일한 광원은 잭오랜턴 안에 있는 점 광원이다. 전체 장면은 호박의 구멍을 통해 비치는 조명에 의해 빛을 받는 바닥의 삼각형 조각을 제외하고는 그림자 안에 있다. 이것은 여러 이유로 어려운 경우이다. 이것은 장면의 대부분은 빛을 받고 그림자는 작고 좀처럼 전체 장면을 에워싸지 않는다는 보통의 가정을 뒤엎는 것이다. 빛을 받는 지역은 그것을 만드는 호박의 구멍과 비교해보면 아주 크다. 비록 조명이 정면과 바닥을 통해서만 비치더라도, 조명은 전방향이고 그림자는 모든 각으로부터 고려되야 한다. 결국, 그림자 캐스터는 광원에 더 가까이에 있다: 그것은 그것을 둘러싸고 있다.
Shadow volumes are not ideal for all scenes. The technique involves constructing a 3D volume that encloses all shadows cast by an object. This volume is constructed from the shadow caster's mesh; however, some shadow casters do not have a mesh that accurately represents their shape. Examples include a billboard, a particle system, or a mesh textured with an alpha matte (such as a tree leaf). These casters produce shadows based on their actual meshes, which do not match how the objects really appear. For example, a billboard smoke cloud casts a rectangular shadow.
그림자 볼륨은 모든 장면에 대해 이상적인 것은 아니다. 이 테크닉은 물체에 의해 캐스트되는 모든 그림자를 둘러싸는 3D 볼륨을 만드는 것을 포함하고 있다. 이 볼륨은 그림자 캐스터 메쉬로부터 만들어진다; 그러나, 어떤 그림자 캐스터는 자신의 모양을 나타내는 정확한 메쉬를 가지고 있지 않다. 예를 들면, 빌보드, 파티클 시스템, 알파 매트를 가지는 텍스쳐된 메쉬(나뭇잎 같은) 같은 것들이다. 이들 캐스터는 물체가 실제로 어떻게 나타나는지에 맞지 않은 현재의 메쉬에 기반하여 그림자를 만든다. 예를 들면, 빌보드 연기 구름은 사각형 그림자를 캐스트한다.
Another problem object is a mesh containing edges that have only a single adjacent face, commonly known as a crack. In the real world, if you look into a crack, you see the inside of the object. Of course, in a rendering engine, you'll see through the object and out the other side because the inside is lined with back-facing polygons culled during rendering. This object is nonsensical as a shadow caster. From some angles, it casts a solid shadow; from other angles, light peeks through the hole and shines out the other side. Even worse, an optimization for the shadow volume breaks when using these objects, creating a large streak of darkness hanging in empty space, as shown in Figure 9-3.
또 다른 문제가 되는 물체는 보통 크랙으로 알려진, 하나의 이웃 면만 가지고 있는 모서리를 포함하고 있는 메쉬다. 실세계에서, 크랙 안을 보면 물체의 내부를 볼 수 있다. 물론, 렌더링 엔진에서는 물체를 통해서 다른 면을 보게 될 것이다. 왜냐하면 내부는 렌더링동안 컬링된 후면 폴리곤으로 채워져있기 때문이다. 이 물체는 그림자 캐스터로서 무의미하다. 어떤 각도에서는 꽉 찬 그림자를 캐스트한다; 다른 각도에서는 빛은 구멍으로 통해 빠져나가 다른 면을 비춘다. 더 심각한 것은, 이런 물체를 사용했을 때 그림 9-3에서 보는 것과 같이 빈 지역에 어둡게 튀어나온 큰 줄을 만들어 그림자 볼륨을 위한 최적화를 깨트린다.
Another potential limitation of the approach is that it requires that everything in a scene cast shadows. When a character's shadow is cast on a wall, it is also cast on everything behind the wall. The only reason the viewer doesn't see the shadow on the other side of the wall is that the wall casts its own shadow that overlaps it. If you cast shadows from characters but not from scene geometry, the shadows appear to go through solid objects.
이 접근방식의 또 다른 잠재적 제한사항은 장면에 있는 모든 것이 그림자를 캐스트하도록 요구한다는 것이다. 캐릭터의 그림자가 벽에 캐스트될 때, 벽 뒤에 있는 모든 것에도 캐스트된다. 관찰자가 벽의 다른 쪽에 있는 그림자를 보지 않는 유일한 이유는 벽이 그것과 겹치는 자신의 그림자를 캐스트하기 때문이다. 만약 장면 기하가 아니라 캐릭터로부터 그림자를 캐스트하면, 그림자는 고형물체를 통과하여 나타난다.
The ideal scene for shadow volume performance is a top view, such as those found in many real-time strategy, sports, and above-ground adventure games. Such a scene is lit from a few downward-pointing directional lights, and the camera is above all the objects, looking down at the ground. The worst case for performance is a scene with multiple point lights in the middle of a large number of shadow-casting objects—such as a large party of torch-wielding adventurers in an underground room with pillars.
그림자 볼륨의 수행에 대한 이상적인 장면은 많은 실시간 전략, 스포츠, 지상 어드벤쳐 게임에서 볼 수 있는 탑뷰이다. 그런 장면은 몇몇 아래를 향하는 방향성 조명으로부터 빛을 받고, 카메라는 모든 물체의 위에서 땅을 내려다보고 있다. 가장 안 좋은 경우는 기둥이 있는 지하실에서 횃불을 사용하는 모험가의 큰 파티 같은, 많은 수의 그림자를 캐스트하는 물체 중간에 여러 개의 점 광원이 있는 장면이다.
9.2 Program Structure
The shadow volume technique consists of two parts: constructing the volumes from silhouette edges and rendering them into the stencil buffer. These parts are repeated for each light source, and the resulting images are added together to create a final frame (a process called multipass rendering). The basic algorithm is easy to understand and implement, but it is slow for big scenes. To address this, a series of optimizations reduces the geometry-processing and fill-rate requirements.
그림자 볼륨 테크닉은 실루엣 모서리로부터 볼륨을 만드는 것과 그것을 스텐실 버퍼에 렌더링하는 것, 2 부분으로 구성되어 있다. 이 부분들은 각 광원마다 반복되고 결과 이미지는 최종 프레임을 만들기 위해 함께 추가된다(다중패스 렌더링으로 불리는 과정). 기본 알고리즘은 이해하고 구현하기 쉽다. 하지만 큰 장면에서는 느리다. 이것을 처리하기 위해, 일련의 최적화가 기하-프로세싱과 필레이트 요구를 줄인다.
We begin with a high-level view of the program structure. We follow up with a detailed discussion of each step, and then we look at several optimizations. Finally, we peek into the future by examining several research projects on shadow volumes.
우리는 프로그램 구조의 고수준 관점으로 시작한다. 각 단계의 자세한 검토를 따라가고, 그런 다음 여러 최적화를 볼 것이다. 마지막으로, 그림자 볼륨에 대한 여러 연구 프로젝트를 설명하여 미래를 살짝 들여다본다.
9.2.1 Multipass Rendering
Mathematically, the illumination at a point is the sum of several terms. We see this in the Phong illumination equation for a single light, which is the sum of ambient, emissive (internal glow), diffuse, and specular components. A scene with multiple lights has a single ambient term and a single emissive term, but it has one diffuse term and one specular term for each light. When rendering without shadows, multiple lights can all be rendered in a single pass. This is typically done by enabling multiple hardware light sources or implementing a pixel shader with code for each light.
수학적으로, 한 점에서의 조명은 여러 항목의 합이다. 우리는 주변, 방사(내부 글로우), 디퓨즈, 정반사 성분의 합인, 하나의 조명에 대한 퐁 조명 방정식에서 이것을 볼 수 있다. 여러 조명이 있는 장면은 하나의 주변 항과 방사 항을 가지지만, 디퓨즈 항과 정반사 항은 각 조명마다 하나씩 가지고 있다. 그림자없이 렌더링할 때, 다수의 조명은 하나의 패스에서 모두 렌더링될 수 있다. 이것은 일반적으로 다중 하드웨어 광원을 활성화시키거나 각 조명에 대한 코드로 픽셀 쉐이더를 실행하여 수행한다.
When rendering with shadows, the contribution from a given light is zero at some points because those points are shadowed. To account for this, the diffuse and specular contribution from each light is computed in a separate rendering pass. The final image is the sum of an initial pass that computes ambient and emissive illumination and the individual lighting passes. Because the initial pass writes depth values into the z-buffer, the additional passes have zero overdraw and can be substantially cheaper in terms of fill rate. Objects rendered in the additional passes are also good candidates for occlusion culling.
그림자와 같이 렌더링할 때, 주어진 조명으로부터의 기여도는 어떤 점에서는 0이 된다. 왜냐하면 그 점은 그림자 내부에 있기 때문이다. 이것을 설명하기 위해, 각 조명으로부터의 디퓨즈와 정반사 기여도는 분리된 렌더링 패스에서 계산된다. 최종 이미지는 주변과 방사 조명을 계산한 처음 패스와 각각의 조명 패스들의 합한 것이다. 처음 패스는 z-버퍼에 깊이 값을 쓰기 때문에, 추가적인 패스는 0 overdraw를 가지고, 필레이트 측면에서 충분히 더 비용이 적게 들 수 있다. 추가적인 패스에서 렌더링된 물체들은 또한 폐색 컬링을 위한 좋은 후보들이다.
Although shadow volumes do not create the soft shadows cast by area sources, multiple passes can be exploited to create a similar effect by distributing multiple, dim spotlights over the surface of an area light. Unfortunately, for a complex scene having enough lights to make this look good, this method is too slow to be practical. (A new research technique, described in Assarsson et al. 2003, suggests a more efficient way of rendering soft shadows with shadow volumes.)
비록 그림자 조명이 지역 근원을 통해 부드러운 그림자 캐스트를 만들지는 못하지만, 지역 조명의 표면 위에 여러 개의 어두운 스포트라이트를 분포하여 비슷한 효과를 만들기 위해 다중 패스를 활용할 수 있다. 불행히도, 이것을 보기 좋게 만드는 충분한 조명이 있는 복잡한 장면에 대해서는, 이 방법은 사용되기에 너무 느리다. (Assarsson et al. 2003에서 설명된 새로운 연구 테크닉은 그림자 볼륨으로 부드러운 그림자를 렌더링하는 더 효율적인 방법을 제안한다.)
The individual lighting passes are combined using alpha blending. To do this, render the ambient/emissive pass to the back buffer with depth writing enabled and the blending function set to glBlendFunc(GL_ONE, GL_ZERO). This initializes the depth buffer and creates the base illumination.
개별 조명 패스는 알파 블렌딩을 사용하여 결합된다. 이것을 하기 위해, 깊이 값 쓰기를 활성화하여 후면 버퍼에 주변/방사 패스를 렌더링하고 블렌딩 함수는 glBlendFunc(GL_ONE, GL_ZERO)으로 설정하라. 이것은 깊이 버퍼를 초기화하고 기본 조명을 생성한다.
Then for the light passes, disable depth writing and change the blending function to glBlendFunc(GL_ONE, GL_ONE). This blending mode adds newly rendered pixels to the ones already there. The pre-initialized depth buffer prevents overdraw. Also, be sure to set the depth test to glDepthFunc(GL_LEQUAL) to avoid z-fighting between subsequent passes.
그런 다음, 조명 패스에 대해서, 깊이 값 쓰기를 비활성화시키고 블렌딩 함수를 glBlendFunc(GL_ONE, GL_ONE)으로 변경한다. 이 블렌딩 모드는 새로 랜더링된 픽셀을 기존의 것에 추가한다. 미리 초기화된 깊이 버퍼는 overdraw를 방지한다. 또한, 이어지는 패스 사이에 z-다툼을 피하기 위해 깊이 테스트를 glDepthFunc(GL_LEQUAL)으로 설정했는지 확인하라.
With these settings, make one pass for each light source. Each pass clears the stencil buffer, marks shadowed areas in it, and then computes the illumination in nonshadowed areas and adds them to the frame buffer.
이 세팅으로, 각 광원마다 하나의 패스를 만들어라. 각 패스는 스텐실 버퍼를 비우고 그 안에 그림자 지역을 표시한다. 그런 다음 그림자가 없는 지역의 조명을 계산하고 그것들을 프레임 버퍼에 추가한다.
The overall structure of the rendering part of the program is shown in Figure 9-4.
프로그램의 렌더링 부분의 전체적인 구조가 그림 9-4에 나타나있다.
A simplified version of this procedure appears in Listing 9-1. The simplification is that the "mark shadows" step is reduced to the worst case, in which every one of the conditionals in the diagram returns true. After walking through the code in detail, we'll put the shorter paths back in as optimizations. The sections of code that will be changed by these optimizations are highlighted to make them easy to find later.
이 절차의 단순화된 버전이 Listing 9-1에 나와있다. 단순화는 도표에 있는 모든 조건문이 true를 반환하는 최악의 경우에 “그림자 표시” 단계가 줄어드는 것이다. 코드를 자세히 살펴본 후에, 더 짧은 경로를 최적화에 돌려놓을 것이다. 이 최적화로 인해 변경될 코드 부분은 후에 찾기 쉽게 강조해놓았다.
Example 9-1. Program Structure Pseudocode
static const float black[] = {0.0f, 0.0f, 0.0f, 0.0f}; glPushAttrib(GL_ALL_ATTRIB_BITS); setupCamera(); // -- Ambient + emissive pass -- // Clear depth and color buffers glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); glBlendFunc(GL_ONE, GL_ZERO); glEnable(GL_BLEND_FUNC); glDepthMask(0xFF); glDepthFunc(GL_LEQUAL); glEnable(GL_LIGHTING); glDisable(GL_LIGHT0); glLightModelfv(LIGHT_MODEL_AMBIENT, globalAmbient); drawScene(); // Light passes glLightModelfv(LIGHT_MODEL_AMBIENT, black); glEnable(GL_LIGHT0); glBlend(GL_ONE, GL_ZERO); glDepthMask(0x00); glEnable(GL_LIGHT0); glEnable(GL_STENCIL_TEST); glEnable(GL_STENCIL_TEST_TWO_SIDE_EXT); for (int i = numLights - 1; i >= 0; --i) { // (The "XY" clipping optimizations set the scissor // region here.) //-- Mark shadows from all casters -- // Clear stencil buffer and switch to stencil-only rendering glClear(GL_STENCIL_BUFFER_BIT); glColorMask(0, 0, 0, 0); glDisable(GL_LIGHTING); glStencilFunc(GL_ALWAYS, 0, ~0); glStencilMask(~0); loadVertexShader(); for (int c = 0; c < numCasters; ++c) { // (The "point and spot" optimization marks shadows // only for casters inside the light's range) setVertexParam("L", object->cframe.inv() * light[i]); object[c]->markShadows(light[i].direction); } unloadVertexShader(); //-- Add illumination - // Configure lighting configureLight(light[i]); glEnable(GL_LIGHTING); glStencilFunc(GL_EQUAL, 0, ~0); glActiveStencilFaceEXT(GL_FRONT); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); glActiveStencilFaceEXT(GL_BACK); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); glDepthFunc(GL_EQUAL); glColorMask(1, 1, 1, 1); glCullFace(GL_BACK); // (The "point and spot" optimization adds illumination // only for objects inside the light's range) drawScene(); } glPopAttrib();
9.2.2 Vertex Buffer Structure
The shadow of a mesh is cast by its silhouette. To quickly find the silhouette edges and extrude them into a shadow volume, meshes need more information than what's needed in a traditional rendering framework that uses only face triangles.
메쉬에 그림자는 그것의 실루엣에 의해 캐스트된다. 실루엣 모서리를 빨리 찾아내서 그림자 볼륨 안으로 돌출시키기 위해, 표면 삼각형만 사용하는 전통적인 렌더링 프레임워크에서 필요한 것보다 더 많은 정보가 필요하다.
In our system, the vertex buffer for a mesh contains two copies of each vertex. Say there are n original vertices. Elements 0 through n - 1 of the vertex buffer contain typical vertices, of the form (x, y, z, 1). Elements n through 2n - 1 are copies of the first set but have the form (x, y, z, 0). The first set can be used for normal rendering. Both sets will be used for shadow determination, where a vertex shader will transform the second set to infinity.
우리 시스템에서, 메쉬를 위한 정점 버퍼는 각 정점의 2개의 복사본을 포함하고 있다. n 개의 원본 정점이 있다고 하자. 정점 버퍼의 0~ n-1 성분은 (x, y, z, 1)형태의 전형적인 정점을 포함하고 있다. n 부터 2n - 1까지의 성분은 처음 세트의 복사본이지만 (x, y, z, 0)형태이다. 처음 세트는 보통의 렌더링에 사용될 수 있다. 두 세트는 정점 쉐이더가 두번째 세트를 무한대로 변경시킬, 그림자 결정을 위해 사용될 것이다.
Objects also must have adjacency information and per-face normals. For every face, we need to know the three counterclockwise vertex indices and the surface normal. For every edge, we need the indices of the two adjacent faces and the indices of the two vertices. As mentioned previously, the model must be closed so it has no cracks. In terms of adjacency information, this means that every edge has exactly two adjacent faces that contain the same vertices but in opposite order. By convention, let the first face index of an edge be the one in which the edge vertices are traversed in order, and let the second index be the face in which the vertices are traversed in the opposite order. Note that there may be vertices in the model that are not in any edge or face. This is because it is a common practice when creating 3D models to collocate vertices with different texture coordinates. For adjacency information, we care only about the raw geometry and ignore the texture coordinates, normals, vertex colors, and so on that are stored with a model for rendering purposes.
물체는 이웃 정보와 표면당 법선 또한 가지고 있어야 한다. 모든 표면에 대해, 3개의 반시계방향 정점 인덱스와 표면 법선을 알 필요가 있다. 모든 모서리에 대해, 두 이웃면의 인덱스와 두 정점의 인덱스가 필요하다. 이전에 말했듯이, 모델은 닫혀있어야 하고, 그로 인해 크랙이 없어야 한다. 이웃 정보를 따져보면, 이것은 모든 모서리는 정확히 같은 정점을 가지고 있지만 방향이 반대인 2개의 이웃 표면을 가진다는 것을 의미한다. 관습상, 모서리의 첫번째 표면 인덱스는 모서리 정점이 순서대로 가로지르는 것이 되게 하고, 두번째 표면 인덱스는 정점이 반대 방향으로 가로지르는 표면이 되게 하라. 모델에 어떤 모서리나 면에도 있지 않는 정점이 있을 수 있다는 것을 주의하라. 이것은 3D 모델을 만들 때 다른 텍스쳐 좌표를 가지는 정점을 배치하는 것이 일반적인 습관이기 때문이다. 이웃 정보를 위해, 그대로의(raw) 기하에 대해서만 관심을 가지고 텍스쳐 좌표, 법선, 정점 색 등등의 렌더링 목적을 위해 모델에 저장된 것은 무시한다.
9.2.3 Working at Infinity
Unlike other OpenGL programs you may have written, shadow volumes make extensive use of coordinates at infinity. Shadow volumes themselves consist of both finite geometry and geometry at infinity. The algorithm is implemented for point light sources, and directional lights are handled as point lights at infinity. The far clipping plane must be at infinity so that it will not cut off the infinite shadow volumes, and the perspective projection must be configured to take this into account.
당신이 작성해봤을 다른 OpenGL 프로그램과 달리, 그림자 볼륨은 무한대에 좌표의 광대한 사용을 만들어낸다. 그림자 볼륨은 스스로가 유한한 기하와 무한한 기하를 둘 다 가지고 있다. 알고리즘은 점 광원에 대해 수행되고, 방향성 조명은 무한대에 있는 점 광원으로써 다루어진다. 원절단면은 무한대에 있어야 하기 때문에, 무한 그림자 볼륨을 잘라내지 않을 것이다. 그리고 원근투영은 이것을 고려하도록 설정되어야 한다.
OpenGL provides full support for working at infinity using homogeneous coordinates. This section reviews homogeneous vertices (for geometry and light sources) and shows how to configure an infinite-perspective matrix.
OpenGL은 동차 좌표를 사용하여 무한대에 작동하는데 완전한 지원을 제공한다. 이 섹션은 동차 정점(기하와 광원을 위한)을 살펴보고 무한-원근 행렬을 어떻게 설정하는지 보여준다.
Finite homogeneous points are represented as (x, y, z, 1); that is, the w component is equal to 1. This implicitly means the point at 3D position (x/1, y/1, z/1). Perspective projection matrices use the w component to divide through by a nonunit value, creating vertices such as (x, y, z, -z) that become (x/-z, y/-z, -1) after the homogeneous divide. What happens when the w component is zero? We get a point that has the form (x/0, y/0, z/0). This point is "at infinity." Of course, if we actually divided each component by 0 and computed the point, it would become (,
,
), which throws away important information—the direction in which the point went to infinity. The (x, y, z, 0) representation uses w = 0 to flag the point as "at infinity" but retains the directional information (x, y, z).
유한 동차 점은 (x, y, z, 1)로 나타낸다; 즉, w 성분은 1이다. 이것은 함축적으로 3D 위치 (x/1, y/1, z/1)에 있는 점을 의미한다. 원근 투영 행렬은 w 성분을 사용하여 비단위 값을 나눈다. (x, y, z, -z) 같은 정점은 동차 나눗셈 후에 (x/-z, y/-z, -1)가 된다. w 성분이 0이면 어떻게 될까? (x/0, y/0, z/0) 형태의 점이 있다고 하자. 이 점은 “무한에” 있다. 물론, 각 성분을 실제로 0으로 나누어 그 점을 계산했다면, 중요한 정보인 점이 무한대로 간 방향을 알려주는 ( , , )가 될 것이다. (x, y, z, 0) 표현은 점이 “무한에” 있는 점이라는 걸 표시하기 위해 w = 0을 사용하지만, 방향 정보 (x, y, z)는 유지하고 있다.
Intuitively, a point at infinity acts as if it is very far away, regardless of the physical dimensions of the scene. Like stars in the night sky, points at infinity stay fixed as the viewer's position changes, but they rotate according to the viewer's orientation. OpenGL renders points with w = 0 correctly. Again like stars, they appear as if rendered on a sphere "at infinity" centered on the viewer. Note that for a point at infinity, only the direction (x, y, z) is important, not the magnitude of the individual components. It is not surprising that OpenGL therefore uses w = 0 to represent a directional light as a point light whose position is the vector to the light: a directional light is a point light that has been moved to infinity along a specific direction.
직관적으로, 무한에 있는 점은 장면의 물리적 넓이에 관계없이 아주 멀리 있는 것처럼 행동한다. 밤하늘의 별처럼, 무한에 있는 점은 관찰자의 위치 변화와 관계없이 고정된 위치에 있지만 관찰자의 방위에 따라 회전한다. OpenGL은 w = 0로 정확하게 점을 렌더링한다. 별과 같이, 점들은 관찰자를 중심으로한 “무한에 있는” 구 위에 렌더링되는 것처럼 보인다. 무한에 있는 점에 있어서 방향 (x, y, z)만이 중요하고 개별적인 성분의 크기는 중요하지 않다는 것을 주의하라. 그러므로 OpenGL이 방향성 조명을 위치가 빛으로의 벡터인 점 조명으로 나타내기 위해 w = 0을 사용하는 것을 놀라운 것이 아니다: 방향성 조명은 특정 방향을 따라 무한으로 움직이는 점 조명이다.
Throughout this chapter, we use w = 0 to represent points at infinity. We'll not only use point lights at infinity, but also extrude shadow volumes to infinity. In the previous section, we used w = 0 as a notation in the second half of the vertex buffer. This was because those vertices will be moved to infinity (they are the infinite end of the shadow volume). The vertex shader will move them relative to the light before they are actually transformed to infinity, however.
이 챕터에서는, 무한에 점을 나타내기 위해 w = 0를 사용한다. 우리는 무한에 점 조명을 사용할 뿐 아니라, 그림자 볼륨을 무한으로 돌출시킬 것이다. 이전 섹션에서, 정점 버퍼의 두번째 반에서 표기법으로 w = 0을 사용했다. 이것은 그 정점들이 무한으로 움직일 것이기 때문이었다(그것들은 그림자 볼륨의 무한 끝이다). 정점들이 실제로 무한으로 변형되기 전에 정점 쉐이더가 조명에 관련된 그것들을 움직일 것이다.
When rendering all of these objects at infinity, we can't have them clipped by the far plane. Therefore, we need to move the far clipping plane to infinity. This is done by computing the limit of the standard projection matrix as the far plane moves to infinity:
이 모든 물체들에 무한에 렌더링될 때, 그것들을 원 평면에 의해 잘려지게 할 수 없다. 그러므로, 원절단평면을 무한으로 움직일 필요가 있다. 이것은 표준 투영 행렬의 한계를 계산해서 원평면을 무한으로 이동시킴으로써 행해진다.
In code, this is a minor change to the way we compute the perspective projection matrix. Just create the projection matrix as shown in Listing 9-2 instead of using glFrustum.
코드에서, 이것은 원근 투영 행렬을 계산하는 방법에서의 작은 변화이다. glFrustum을 사용하는 대신에 Listing 9-2에서 보여지는 것처럼 투영 행렬을 만들어라.
Example 9-2. An Infinite Projection Matrix in the Style of glFrustum
void perspectiveProjectionMatrix(double left, double right, double bottom, double top, double nearval, double farval) { double x, y, a, b, c, d; x = (2.0 * nearval) / (right - left); y = (2.0 * nearval) / (top - bottom); a = (right + left) / (right - left); b = (top + bottom) / (top - bottom); if ((float)farval >= (float)inf) { // Infinite view frustum c = -1.0; d = -2.0 * nearval; } else { c = -(farval + nearval) / (farval - nearval); d = -(2.0 * farval * nearval) / (farval - nearval); } double m[] = {x, 0, 0, 0, 0, y, 0, 0, a, b, c, -1, 0, 0, d, 0}; glLoadMatrixd(m); }
The Cg vertex shader from Listing 9-3 transforms points with w = 1 normally and sends points with w = 0 to infinity away from the light.
Listing 9-3의 Cg 정점 쉐이더는 점을 w = 1으로 정상적으로 변형하고 w = 0으로 조명으로부터 무한으로 보낸다.
Example 9-3. A Vertex Shader for Extruding w = 0 Vertices Away from the Light
VOut main(const float4x4 uniform in MVP, const float4 uniform in L, const VIn in vin) { VOut vout; // (The "directional" optimization eliminates the vertex shader // by using different rendering loops for point and directional // lights.) vout.pos = MVP * (vin.pos.w == 0 ? float4(vin.pos.xyz * L.w - L.xyz, 0) : vin.posvin.pos); return vout; }
The branch operator (?) can be replaced with a call to the lerp function on older graphics cards that don't support branching in vertex shaders. Note that multiplying the point position by L.w in the middle line makes the point's position irrelevant for a directional light. This is because the vector from the light to a point is independent of the point position for a directional light. In Listing 9-1, the call to setVertexParam sets the object-space light vector. The implementations of loadVertexProgram, unloadVertexProgram, and setVertexParam depend on the vertex shader runtime used.
분기 연산자(?)는 정점 쉐이더에서 분기를 지원하지 않는 오래된 그래픽 카드에서 lerp 함수를 호출하는 것으로 대체할 수 있다. 중간 라인에서 점 위치를 L.w로 곱하는 것은 점의 위치를 방향성 조명과 무관하게 만든다는 것을 주목하라. Listing 9-1에서, setVertexParam을 호출하는 것은 물체-공간 조명 백터를 설정한다. loadVertexProgram, unloadVertexProgram, setVertexParam의 실행은 사용된 정점 쉐이더 런타임에 달려 있다.
9.3 Detailed Discussion
The goal of markShadows is to set the stencil buffer to zero for illuminated pixels and to a nonzero number for shadowed pixels. It does this by constructing a shadow volume—the geometry that bounds the shadow regions—and rendering it into the stencil buffer. Here we briefly look at the mathematical justification for this, and then we cover the implementation in detail.
markShadows의 목표는 스텐실 버퍼를 조명이 비치는 픽섹은 0으로, 그림자 지역의 픽셀은 0이 아닌 수로 설정하는 것이다. 이것은 그림자 지역을 감싸고 있는 기하인 그림자 볼륨을 구성하고 이것을 스텐실 버퍼로 렌더링하여 수행한다. 여기서 우리는 이것에 대한 수학적인 정당성을 간략히 살펴보고 나서 구체적인 구현을 알아볼 것이다.
9.3.1 The Math
Figure 9-5 shows a simple scene with a single point light (the light bulb icon), a shadow-casting box, a shadow-receiving ground plane, and a viewer on the left. The line in front of the viewer represents the image plane, which is important to the discussion in Section 9.5. The blue arrows represent light rays from the source (for clarity, only a few are shown). The ground plane is bright where the leftmost and rightmost rays strike it. The center rays hit the shadow caster instead and are blocked. The ground plane is dark (shadowed) underneath the caster where these rays are blocked. Somewhere between the outer and the inner rays in the diagram are critical lines, shown dashed. These lines mark the edges of the shadow. Note that they pass through the center of the light and the edges of the shadow caster. The diagram is 2D; in 3D, these are not lines but quadrilaterals. These lines are the sides of the shadow volume. Everything farther than the shadow caster and between them is shadowed. All other points are illuminated.
그림 9-5는 간단한 점 광원(전구 아이콘), 그림자-캐스팅 상자, 그림자를 받는 지면, 왼쪽의 관찰자가 있는 간단한 장면을 보여준다. 관찰자 앞에 있는 선은 섹션 9.5에서 중요하게 다루어지는 이미지 평면을 나타낸다. 파란 화살표는 광원으로부터의 조명 광선을 나타낸다(알아보기 쉽게 몇 개만 나타낸다). 지면은 조명 광선이 부딪히는 왼쪽과 오른쪽 부분이 밝아진다. 가운데 광선은 그림자 캐스터를 충돌하여 막히게 된다. 이 광선에 막힌 캐스터 아래의 지면은 어둡다(그림자가 드리워진다). 도표에 있는 바깥쪽 광선과 안쪽 광선 사이에 점선으로 표시된 중요한 선이 있다. 이 선은 그림자의 가장자리를 표시한다. 그것은 조명의 중심을 지나 그림자 캐스터의 가장자리를 지난다는 것을 주목하라. 도표는 2D이다; 3D에서는 이것은 선이 아니라 4변형이다. 이 선들은 그림자 볼륨의 측면이다. 그림자 캐스터보다 멀리 있고 이 선들의 사이에 있는 모든 것은 그림자가 진다. 다른 모든 점들은 조명을 받는다.
Figure 9-6 shows the shadow volume explicitly. The shadow volume has three pieces.
그림 9-6은 그림자 볼륨을 명확하게 보여준다. 그림자 볼륨에는 3부분이 있다.
- The sides are constructed by extruding the edges of the caster away to infinity to form quads. Objects between the caster and the light should not be shadowed, so we have to close the volume on the top. Because an object casts the same shadow as its silhouette, we need to extrude only the silhouette edges. These are edges where, from the point of view of the light, one adjacent face is a back face (N · L < 0) and one adjacent face is a front face (N · L > 0).
측면은 캐스터의 모서리를 사각형 형태로 무한으로 돌출시켜 만들어진다. 캐스터와 조명 사이의 물체는 그림자가 드리워지면 안된다. 그래서 볼륨을 꼭대기에 가깝게 해야한다. 물체는 모서리에 따라 동일한 그림자를 캐스트하기 때문에 실루엣 모서리만 돌출시킬 필요가 있다. 실루엣 모서리는 조명의 뷰 포인트로부터, 한 이웃 면은 후면(N · L < 0)이고 한 이웃면은 전면(N · L > 0)인 모서리이다.
- The light cap is the geometry that closes the volume on the side near the light. It is composed of the caster's light front-facing polygons (that is, polygons that are front faces from the light's point of view). Shadows extend infinitely away from the light, but mathematically we have to close the geometry at the back end to make it a volume.
밝은 뚜껑은 조명에 가까운 쪽에서 볼륨을 닫는 기하이다. 이것은 캐스터의 조명 전면 폴리곤으로 구성되어 있다(즉, 조명의 뷰 포인트로부터 전면인 폴리곤). 그림자는 조명으로부터 무한으로 뻗어나가지만, 수학적으로 볼륨을 만들기 위해 후미에서 기하를 닫아야 한다.
- The dark cap is composed of the caster's light back-facing polygons (that is, polygons that are back faces from the light's point of view) expanded to infinity. In Figure 9-6, they are shown as a curve, because polygons at infinity can be thought of as lying on a very large sphere surrounding the entire scene.
어두운 뚜껑은 무한으로 뻗은, 캐스터의 조명 후면 폴리곤으로 구성된다(즉, 조명의 뷰 포인트로부터 후면인 폴리곤). 그림 9-6에서, 그것들은 곡선으로 보여진다. 왜냐하면 무한에 있는 폴리곤은 전체 장면 주위의 아주 큰 구에 놓여있는 것으로 생각할 수 있기 때문이다.
Although the figures show a 2D diagram of a simple scene, keep in mind that the shadow volumes are in 3D and may have complicated geometry if the shadow caster has a complicated shape. For comparison, the geometry of real 3D shadow volumes is shown in Figures 9-2, 9-7, and 9-10. If there are multiple shadow casters (and there usually are), the shadow volume will have many separate parts. These parts might even overlap. None of this is a problem; the algorithm handles a triangle or a complete scene equally well without any special work on our part.
비록 그림이 간단한 장면의 2D 도표를 보여주지만, 그림자 볼륨은 3D에 있고 그림자 캐스터가 복잡한 형태이면 그림자 볼륨의 기하가 복잡해질 수도 있다는 것을 염두하라. 비교를 위해, 실제 3D 그림자 볼륨의 기하가 그림 9-2, 9-7, 9-10에 나타나 있다. 그림자 캐스터가 여러 개가 있다면(보통 그렇다), 그림자 볼륨은 많은 개별적인 부분을 가질 것이다. 이 부분은 겹칠지도 모른다. 이것이 문제가 될 수 있다는 것을 유념하라; 알고리즘은 우리의 부분에 어떤 특별한 작업없이 삼각형이나 완전한 장면을 동일하게 다룬다.
Here's a mathematical strategy for performing shadow determination using the shadow volume. When rendering, each pixel corresponds to a point in 3D space. We want to set the stencil buffer to a nonzero value (shadowed) at that pixel if the point is inside the shadow volume; otherwise, we'll set it to zero (illuminated). Call the point in question P. Consider intersections between the ray that starts at P and travels to infinity along the negative view vector, -V, and the shadow volume. There are two kinds of intersections. An entering intersection occurs when the ray moves from outside the shadow volume to inside. Let M be the surface normal to the shadow face intersected. At an entering intersection, M · V > 0. An exiting intersection occurs when the ray leaves a shadow volume and has M · V < 0 (ignore glancing intersections where M · V = 0). The key idea is to count the number of occurrences of each kind of intersection:
그림자 볼륨을 사용한 그림자 결정을 수행하기 위한 수학적 전략이 있다. 렌더링할 때, 각 픽셀은 3D 공간에 있는 점에 대응한다. 우리는 만약 점이 그림자 볼륨 안쪽에 있을 때 그 픽셀에 스텐실 버퍼를 0이 아닌 값(그림자 지역)으로 설정하길 원한다; 그렇지 않으면 0으로 설정할 것이다(조명이 비치는 지역). 점을 문제 P 로 부르자. P 에서 시작하여 시선의 역백터 -V 를 따라 움직이는 광선과 그림자 볼륨 사이의 교점을 생각하라. 2종류의 교점이 있다. 진입 교점은 광선이 그림자 볼륨의 바깥쪽에서 안쪽으로 움직일 때 생긴다. M 을 교차하는 그림자 면에서의 표면 법선이라고 하자. 진입 교점에서는 M · V > 0이다. 진출 교점은 광선이 그림자 볼륨을 나갈 때 생기고 M · V < 0이다(M · V = 0인 교점은 무시한다). 주요 아이디어는 각 종류의 교점의 빈도수를 세는 것이다.
Point P is in shadow if and only if there were more entering intersections than exiting intersections along a ray to infinity.
점 P는 무한으로 광선을 따라 진출 교점보다 진입 교점이 더 많을 때 그림자에 있다.
Rays that travel along the negative view vector lie within exactly one pixel under perspective projection. We exploit this fact to perform the intersection counts in hardware using the stencil buffer, which makes the method fast.
시선의 역백터를 따라 움직이는 광선은 정확하게 원근 투영 아래에 한 픽셀에 있다. 우리는 더 빠른 방법인, 스텐실 버퍼를 사용하여 하드웨어에서 교점의 수를 계산하는데에 이 사실을 활용한다.
9.3.2 The Code
Here's how to implement our observations efficiently in hardware. Initialize the stencil buffer to zero and enable wrapping increment and decrement operations, if supported on the graphics card. (If wrapping is not supported, initialize all stencil values to 128 or some other value to avoid underflow.) Disable color rendering and render the shadow volume geometry to the stencil buffer. Because we're counting intersections with the ray that starts at each visible point and travels away from the viewer, set up the hardware to change the stencil value when the depth test fails. The stencil buffer is decremented for each front-face pixel that fails the depth test and incremented for each back-face pixel that fails the depth test.
우리의 관찰을 하드웨어에서 어떻게 효율적으로 구현할지 알아보자. 스텐실 버퍼를 0으로 초기화하고 그래픽 카드에서 지원된다면 랩핑 증감 연산을 활성화시킨다(만약 랩핑을 지원하지 않는다면, 언더플로를 피하기 위해 모든 스텐실 값을 128이나 다른 수로 초기화하라). 색 렌더링을 비활성화시키고 그림자 볼륨 기하를 스텐실 버퍼에 렌더링하라. 각각의 눈에 보이는 점에서 출발하여 관찰자로부터 움직이는 광선으로 교점의 수를 세고 있기 때문에, 깊이 테스트가 실패할 때 스텐실 값을 바꾸도록 하드웨어를 설정하라. 스텐실 버퍼는 깊이 테스트가 실패한 각 전면 픽셀에 대해 감소하고, 깊이 테스트에 실패한 각 후면 픽셀에 대해 증가한다.
Note that we disabled color rendering immediately before rendering shadow volumes, and we disabled depth writing a while ago, after the ambient illumination pass. Because both color and depth writing are disabled, rendering shadow volumes affects only the stencil buffer. Color writing must be disabled because we don't want to see the shadow volumes in the final image, just the shadows (which are based on the stencil counts). Depth writing needs to be disabled because we assumed that the depth values in the z-buffer represent the depths of visible surfaces (and not shadow volumes). Because depth writing is disabled, shadow faces do not interact with each other, and so the order in which they are rendered does not matter.
그림자 볼륨을 렌더링하기 바로 전에 색 랜더링을 비활성화했고, 주변 조명 패스 후, 조금 전에 깊이 쓰기를 비활성했다는 것을 주목하라. 색과 깊이 쓰기를 둘 다 비활성화했기 때문에, 그림자 볼륨을 렌더링하는 것은 스텐실 버퍼에만 영향을 준다. 색 쓰기는 비활성화되어야 한다. 왜냐하면 우리는 최종 이미지에서 그림자 볼륨은 보이지 않고 그림자(스텐실 버퍼에 기반한)만 보이기를 원하기 때문이다. z-버퍼에 있는 깊이 값은 눈에 보이는 표면(그림자 볼륨은 아니다)의 깊이를 나타낸다고 가정하기 때문에 깊이 쓰기는 비활성화될 필요가 있다. 깊이 쓰기가 비활성화되었기 때문에, 그림자 면은 서로서로 영향을 주지 않기 때문에, 렌더링하는 순서는 중요하지 않다.
After rendering, the stencil value at a pixel will be zero if the same number of front and back faces were rendered, and the value will be nonzero if the counts differ. Entering intersections are always created by front faces, and exiting intersections are always created by back faces. The stencil count after rendering is therefore the number of entering intersections minus the number of exiting intersections—precisely the result we want for shadow determination.
렌더링 후에, 같은 수의 전후면이 렌더링되었으면 픽셀에 스텐실 값은 0이 될것이고, 렌더링된 전후면의 수가 다르면 0이 아닌 값이 될 것이다. 진입 교점은 항상 전면에 의해 만들어지고, 진출 교점은 항상 후면에 의해 만들어진다. 그러므로 렌더링 후의 스텐실 값은 진입 교점의 수에서 진출 교점의 수를 뺀 것이다-정확하게 우리가 그림자 결정을 위해 원하던 결과이다.
9.3.3 The markShadows Method
The code for the markShadows method on the Object class is shown in Listing 9-4.
Object 클래스에서 makrShadows 방식을 위한 코드가 Listing 9-4에 나와있다.
First, we take the light vector from world space to object space. For a point light or spotlight, this vector is the position (x, y, z, 1). For a directional light, it has the form (x, y, z, 0), where (x, y, z) is the vector to the light source. In general, a homogeneous vector with w = 0 can be thought of as a point on a sphere at infinity. A directional light is therefore the same as a point light at infinity.
우선, 우리는 조명 백터를 월드 공간에서 물체 공간으로 가져간다. 점 조명이나 스포트라이트 조명을 위해서는, 이 백터는 위치가 (x, y, z, 1)이다. 방향성 조명을 위해서는, (x, y, z)가 광원으로의 백터인, (x, y, z, 0)형태를 가진다. 일반적으로, w = 0인 동차 백터는 무한에 있는 구에 있는 점으로써 생각될 수 있다. 그러므로 방향성 조명은 무한에 있는 점 조명과 같다.
Example 9-4. The markShadows Method
// isBackface[f] = true if face f faces away from the light std::vector<bool> backface; void Object::markShadows(const Vector4& wsL) { // (When the viewport is not shadowed by this object, this // section is changed by the "uncapping" optimization.) // Decrement on front faces; increment on back faces // (a.k.a. z-fail rendering) glActiveStencilFaceEXT(GL_FRONT); glStencilOp(GL_KEEP, GL_DECR_WRAP_EXT, GL_KEEP); glActiveStencilFaceEXT(GL_BACK); glStencilOp(GL_KEEP, GL_INCR_WRAP_EXT, GL_KEEP); glCullFace(GL_NONE); // (The "Z bounds" optimization sets the depth bounds here.) // Take light to object space and compute light back faces obj->findBackfaces(cframe.inv() * wsL); // Set up for vertex buffer rendering glVertexBuffer(vertexBuffer); renderShadowCaps(); renderShadowSides(); glVertexBuffer(NULL); }
With this object-space light vector, we compute the light front faces and light back faces. The facing directions are needed only temporarily and are stored in a global array. The (double-length) vertex buffer is then selected, and we render the shadow light and dark caps as triangles. Finally, the sides of the shadow volume are rendered as quads.
이 물체-공간 조명 백터를 가지고, 우리는 조명 전면과 조명 후면을 계산한다. 지향 방향은 단지 일시적으로만 필요하고 전역 배열에 저장된다. 그런 다음 2배 길이의 정점 버퍼가 선택되고, 그림자의 밝은 뚜껑과 어두운 뚜껑을 삼각형으로 렌더링한다. 마지막으로, 그림자 볼륨의 측면이 사각형으로 렌더링된다.
9.3.4 The findBackfaces Method
The findBackfaces method iterates over each face and computes N · L, as shown in Listing 9-5.
findBackFaces 방식은 Listing 9-5에서 보는 것과 같이, 각 표면에 걸쳐 반복되고 N · L 을 계산한다.
Example 9-5. The findBackfaces Method
void Object::findBackfaces(const Vector4& osL) // Object-space light // vector { backface.resize(face.size()); for (int f = 0; f < face.size(); ++f) { Vector3 L = L.xyz() - vertex[face[f].vertex[0]] * L.w; backface[f] = dot(face[f].normal, L) < 0; } }
For a finite point light, the vector to the specific polygon is needed, so we subtract the position of one face vertex from the light position. For directional lights, the light direction is used unchanged. For performance, these cases can be handled in separate loops; they are combined in this example only for brevity. Note that none of the vectors needs to have unit length, because we're interested in only the sign of N · L, not the magnitude.
유한 점 광원을 위해, 특정 폴리곤에 백터가 필요해진다. 그래서 우리는 조명 위치에서 한 면의 정점의 위치를 뺀다. 방향성 조명에 대해서는, 조명 방향은 바뀌지 않는다. 수행 성능을 위해, 이 경우들은 분리된 루프에서 다루어질 수 있다; 그것들은 이 예제에서 잠깐동안만 결합된다. 어떤 백터도 단위 길이를 가질 필요는 없다는 것을 유념하라. 왜냐하면 우리는 크기가 아니라 단지 N · L 의 부호에만 흥미가 있기 때문이다.
If the model is animated, the face normals must be recomputed from the animated vertices for every frame. This precludes the use of matrix skinning or vertex blending in hardware, because the modified geometry would then not be available on the CPU. At the end of this chapter, we discuss some proposed techniques for combining shadow volumes with hardware vertex displacement.
모델이 애니메이션되면, 표면 법선은 매프레임마다 애니메이션된 정점으로부터 다시 계산되어야 한다. 이것은 하드웨어에서 행렬 스키닝이나 정점 블렌딩을 사용하는 것을 배제한다. 왜냐하면 수정된 기하는 그 때에는 CPU에서 유용하지 않을 것이기 때문이다. 이 챕터의 끝에서, 그림자 볼륨을 하드웨어 정점 변경과 결합하기 위해 제안된 테크닉에 대해서 논의한다.
9.3.5 Light and Dark Caps
Given the back face array, we can compute the caps and shadow volume sides. In each case, we will accumulate a list of vertex indices and then render the indices from the vertex buffer with glDrawElements. The indices are temporarily stored in another global array, called index.
후면 배열이 주어지면, 그림자 볼륨의 뚜껑과 측면을 계산할 수 있다. 각 경우에서, 우리는 정점 인덱스의 리스트를 모으고 나서 glDrawElements를 가지고 정점 버퍼로부터 인덱스를 렌더링한다. 인덱스는 임시적으로 index라고 불리는 또 다른 전역 배열에 저장된다.
The code for the light and dark caps is shown in Listing 9-6.
밝은 뚜껑과 어두운 뚜껑을 위한 코드가 Listing 9-6에 나타나있다.
Example 9-6. The renderShadowCaps Method
// Indices into vertex buffer std::vector<unsigned int> index; void Object::renderShadowCaps() { // (The "Culling" optimization changes this method // to try to cull the light and dark caps separately.) index.resize(0); for (int f = face.size() - 1; f >= 0; --f) { if (backface[f]) { // Dark cap (same vertices but at infinity) for (int v = 0; v < 3; ++v) { index.pushBack(face[f].vertex[v] + n); } } else { // Light cap for (int v = 0; v < 3; ++v) { index.pushBack(face[f].vertex[v]); } } } glDrawElements(GL_TRIANGLES, index.size(), GL_UNSIGNED_INT, index.begin()); }
Light caps are simply polygons that face the light. To create dark caps, we take the light back faces and send them away from the light, to infinity. To do this, we render from the second set of vertices, which the vertex shader sends to infinity for us.
밝은 뚜껑은 단숞히 빛을 향하고 있는 폴리곤이다. 어두운 뚜껑을 만들기 위해, 우리는 조명 후면을 가져와서 그것들을 빛으로부터 무한으로 보낸다. 이것을 하기 위해, 정점 쉐이더가 우리를 위해 무한으로 보내는, 정점의 두번째 세트로부터 렌더링한다.
Figure 9-7 shows an animated Quake 3 character standing on white ground lit by a white point light. The shadow volumes of the character are shown in yellow on the right side of the figure. Note that the shape of the dark cap, which is the part of the shadow volume far from the character, is the same as that of the character, but it is enlarged. The light cap is not visible because it is inside the character. The polygons stretching between the light and dark caps are the sides, which are constructed from silhouette edges.
그림 9-7은 흰 점 조명에 의해 밝혀지는 흰 지면에 서있는 애니메이션되는 Quake 3 캐릭터를 보여준다. 캐릭터의 그림자 볼륨은 그림의 오른쪽에 노란색으로 보여진다. 캐릭터로부터 멀리 있는 그림자 볼륨의 부분인, 어두운 뚜껑의 형태는 캐릭터의 형태와 동일하지만 넓어진 것을 주목하라. 밝은 뚜껑은 캐릭터 안에 있기 때문에 보이지 않는다. 밝은 뚜껑과 어두운 뚜껑 사이에 뻗은 폴리곤은 실루엣 모서리로부터 형성된 측면이다.
9.3.6 Sides
The sides of the shadow volume are quadrilaterals between the first and second sets of vertices—that is, between the object and infinity. We iterate over the edges of the mesh. Recall that only those edges on the silhouette need be extruded into quads; the other edges do not affect the shadow volume.
그림자 볼륨의 측면은 정점의 첫 세트와 두번째 세트 사이의 사변형이다-즉, 물체와 무한 사이이다. 우리는 메쉬의 모서리에 걸쳐 반복한다. 실루엣에 있는 모서리만 사각형으로 돌출할 필요가 있다는 것을 상기하라. 다른 모서리들은 그림자 볼륨에 영향을 주지 않는다.
A silhouette edge occurs where an object's light back face meets one of its light front faces. All of the information to make such a classification is available to us. The edges store the indices of the two adjacent faces, and the back-face array tells us which face indices correspond to light back faces. See Listing 9-7.
실루엣 모서리는 물체의 조명 후면이 조명 전면과 만나는 곳에서 생긴다. 그런 분류를 하기 위한 모든 정보는 우리에게 유용하다. 모서리를 두 인접 표면의 인덱스를 저장하고, 후면 배열은 우리에게 어떤 표면 인덱스가 조명 후면에 해당하는지 알려준다. Listing 9-7을 보라.
It is important to construct edge information for the mesh with consistent edge orientations, so that the resulting shadow-face quads have correct winding directions. on the shadow faces, the vertices must wind counterclockwise, so that the surface normal points out of the shadow volume. To ensure this, we use a convention in which the directed edge from vertex v0 = edge[e].vertex[0] to vertex v1 = edge[e].vertex[1] is counterclockwise in the mesh face with index edge[e].face[0] and clockwise (backward) in the mesh face with index edge[e].face[1].
메쉬에 대해 일관된 모서리 방위로 모서리 정보를 구성하는 것이 중요하다. 그로 인해 결과 그림자-표면 사각형은 올바른 감기 방향을 가진다. 그림자 면에서, 정점들은 반시계방향으로 감겨야 한다. 그래야 표면 법선이 그림자 볼륨의 밖을 가르킨다. 이것을 확실하게 하기 위해, 정점 v0 = edge[e].vertex[0]에서 정점 v1 = edge[e].vertex[1] 방향의 모서리는 인덱스가 edge[e].face[0]인 메쉬 표면에서는 반시계 방향이고 인덱스가 edge[e].face[1]인 메쉬 표면에서는 시계방향(뒤쪽)이다.
The shadow quad must contain the edge directed in the same way as the back face. Therefore, if face edge[e].face[0] is a back face, the shadow face contains the edge from v0 to v1. Otherwise, it contains the edge from v1 to v0. Figure 9-8 shows the winding direction for the light front face and the shadow quad at an edge directed from v0 to v1.
그림자 사각형은 후면과 같은 방식으로 향하는 모서리를 포함해야 한다. 그러므로, 표면 edge[e].face[0]이 후면이라면 그림자 표면은 v0 에서 v1인 모서리를 포함한다. 그렇지 않으면 v1에서 v0인 모서리를 포함한다. 그림 9-8은 v0 에서 v1 방향인 모서리에서 조명 전면과 그림자 사각형을 위한 감기 방향을 보여준다.
Example 9-7. The renderShadowSides Method
void Object::renderShadowSides() { index.resize(0); for (int e = edges.size() - 1; e >= 0; --e) { if (backface[edge[e].face[0]] != backface[edge[e].face[1]) { // This is a silhouette edge int v0, v1; if (backface[edge[e].face[0])) { // Wind the same way as face 0 v0 = edge[e].vertex[0]; v1 = edge[e].vertex[1]; } else { // Wind the same way as face 1 v1 = edge[e].vertex[0]; v0 = edge[e].vertex[1]; } // (The "directional" optimization changes this code.) index.pushBack(v0); index.pushBack(v1); index.pushBack(v1 + n); index.pushBack(v0 + n); } } // (The "directional" optimization changes this to use // GL_TRIANGLES instead of GL_QUADS.) glDrawElements(GL_QUADS, index.size(), GL_UNSIGNED_INT, index.begin()); }
We've now walked through the entire shadow-rendering procedure. We've built a system that classifies pixels as shadowed or unshadowed in the stencil buffer and then adds illumination to only the unshadowed pixels. This system can handle many different kinds of light sources and complex shadow-caster geometry. It can also interoperate with other shadow algorithms such as projective shadows and shadow maps. The program can be altered to add illumination only to those areas that pass all the shadow tests.
우리는 이제 전체적인 그림자-렌더링 절차를 대략적으로 알아보고 있다. 우리는 스텐실 버퍼에서 그림자거나 그림자가 아닌 픽셀을 구분해서 그림자가 아닌 픽셀에만 조명을 추가하는 시스템을 만들고 있다. 이 시스템은 많은 다른 종류의 광원과 복잡한 그림자-케스터 기하를 다룰 수 있다. 그것은 또한 투영 그림자나 그림자맵 같은 다른 그림자 알고리즘와 같이 사용될 수 있다. 프로그램은 모든 그림자 테스트를 통과한 지역에만 조명을 추가하게 수정될 수 있다.
By taking advantage of some common cases where the shadow volume algorithm is simplified, we can significantly speed up the process. The remainder of this chapter describes ways of speeding up shadow volume creation and rendering. In practice, the following methods can quadruple the speed of the base algorithm.
몇몇 공통적인 경우의 이점을 취하여 그림자 볼륨 알고리즘은 단순화되고, 처리 속도를 상당히 높일 수 있다. 이 챕터의 나머지 부분은 그림자 볼륨 생성과 렌더링의 속도를 높일 수 있는 방법을 소개한다. 실제로, 다음의 방법들은 기본 알고리즘의 속도를 4배로 할 수 있다.
9.4 Debugging
To see if you are generating the shadow volumes correctly, temporarily enable color rendering and then draw shadow volumes with additive alpha blending. Turn on face culling and use one color for front faces and another for back faces. These shapes have other uses beyond debugging: you might want to render visible shadow volumes during gameplay for effects such as light rays passing through clouds or trees.
그림자 볼륨을 제대로 생성했는지 보기 위해, 임시적으로 색 렌더링을 활성화해서 그림자 볼륨을 부가적인 알파 블렌딩으로 그려라. 표면 컬링을 키고 전면에 한가지 색을 사용하고 후면에 다른 색을 사용하라. 이런 형태는 디버깅 이외에도 다른 용도를 가진다: 구름이나 나무를 통해 지나는 빛 광선 같은 효과를 위해 게임 플레이동안 그림자 볼륨을 보이게 렌더링하길 원할 수도 있다.
Remember that OpenGL requires the stencil test to be enabled, even if it is set to GL_ALWAYS_PASS, when using a stencil operation. Also, don't forget the stencil mask: glStencilMask(~0). If you forget either of these prerequisites, your write operations will be ignored.
스텐실 연산을 사용할 때 GL_ALWAYS_PASS 으로 설정되어 있다고 하더라도 OpenGL은 스텐실 테스트가 활성화되는 것을 요구한다는 것을 기억하라. 또한, 스텐실 마스크를 잊지 마라: glStencilMask(~0). 이런 필요 조건 중 하나라도 잊어버린다면, 쓰기 연산은 무시될 것이다.
Use assertions to check that every edge has exactly two adjacent faces. If you have cracks in a model, you'll get shadow streaks in the air like those we saw in Figure 9-3. Software modelers such as 3ds max have tools to fix cracks (called welding vertices) automatically—use them!
모든 모서리가 정확히 2개의 이웃 표면을 가지고 있다는 것을 체크하기 위해 assertion을 사용하라. 모델이 크랙이 있으면 그림 9-3에서 본 것과 같은 그림자 띠가 생길 것이다. 3ds max같은 소프트웨어 모델러는 크랙을 자동으로 수정하는 툴(정점 용접으로 불리는)을 가지고 있다-그것을 사용하라!
9.5 Geometry Optimizations
For clarity and simplicity, the base shadow-volume algorithm was described in the first half of the chapter in generic form, with directional lights, point lights, and spotlights treated the same. We used the mathematical trick L.xyz() - V * L.w in Listing 9-5 and a similar one in the vertex shader in Listing 9-3. These listings compute the light vector for both types of light with a single expression. We can improve performance by treating them separately in the vertex shader and throughout the process of generating shadow volumes. The shadow volume created by a directional light is simpler than that created by a point light, so this can turn into a big savings (at the expense of code complexity).
명쾌하고 간단하게, 이 챕터의 처음 반에서는 방향성 조명, 점 조명, 스포트라이트 조명을 동일하게 다루는 일반적은 형태로 기본 그림자-볼륨 알고리즘을 설명했다. 우리는 Listing 9-5에서 수학적 트릭 L.xyz() - V * L.w 을 사용했고 Listing 9-3에서는 정점 쉐이더에 비슷한 것을 사용했다. 이 listing들은 하나의 식으로 2가지 형태의 조명에 대한 조명 백터를 계산한다. 그것들을 정점 쉐이더에서 개별적으로 그림자 볼륨 생성 과정 내내 다루면 성능을 향상시킬 수 있다. 방향성 조명에 의해 만들어진 그림자 볼륨은 점 광원에 의해 생성된 것보다 더 간단한다. 그래서 이것은 큰 절약이 될 수 있다(코드 복잡성의 비용에).
We can also improve geometry processing performance by using conservative bounding volumes to cull shadow geometry. This section describes these optimizations.
또한 그림자 기하를 컬링하는 전통적인 바운딩 볼륨을 사용하여 기하 처리 성능을 향상시킬 수 있다. 이 섹션은 이 최적화를 설명한다.
9.5.1 Directional Lights
For a directional light, the light vector is just L.xyz. Because the light vector is the same at all vertices, all vertices in the dark cap are at the same point, which is -L. This means there is no dark cap: the (parallel) sides of the shadow volume converge at infinity to a single point, and so the cap is unnecessary.
방향성 조명의 경우, 조명 백터는 단지 L.xyz이다. 조명 백터는 모든 정점에서 동일하기 때문에, 어두운 뚜껑에 있는 모든 정점은 –L인 동일한 점에 있다. 이것은 어두운 뚜껑이 없다는 것을 의미한다: 그림자 볼륨의 (평형)측면은 무한에 한 점에 모인다. 그래서 뚜껑이 필요가 없다.
Because they converge to a point, the sides are triangles, not quads. The push statements in renderShadowSides (Listing 9-7) become:
그것들은 한 점에 모이기 때문에, 측면은 사각형이 아니라 삼각형이다. renderShadowSides (Listing 9-7)에서 밀기 명령문은 다음과 같이 된다:
index.pushBack(v0); index.pushBack(v1); index.pushBack(n);
These statements not only have fewer indices, but they are more friendly to the vertex cache. That's because the same vertex number n is transferred multiple times (we could transfer any one vertex with index greater than or equal to n, because they all transform to the same point). Alternatively, we could eliminate the vertex shader altogether and add one more vertex with index 2n that is set to the negative light vector before each shadow pass.
이 명령문은 더 적은 인덱스를 가질뿐만 아니라 정점 캐쉬에 더 친숙하다. 그것은 동일한 정점 수 n 가 여러 번 이동되기 때문이다(n 보다 크거나 같은 인덱스로 어떤 한 정점이라도 이동시킬 수 있다. 왜냐하면 그것들은 모두 같은 점으로 변환되기 때문이다). 그 대신에, 정점 쉐이더를 완전히 없애고 각 그림자 패스 전에 조명 역백터에 놓이는, 인덱스가 2n 인 정점을 하나 더 추가할 수 있다.
9.5.2 Point Lights and Spotlights
Point lights are typically attenuated by distance. After a certain distance, the light given off by a point light is negligible (when it drops below 1/255, we can't even see the result in an eight-bit frame buffer). Spotlights are attenuated by angle, and sometimes by distance. Outside the cone of the spotlight, they give no illumination.
점 조명은 일반적으로 거리에 따라 감쇠된다. 특정 거리 이후에는, 점 조명이 내는 빛은 무시해도 좋다(1/255 밑으로 떨어지면, 8-비트 프레임 버퍼에서 그 결과를 볼 수 없다). 스포트라이트는 각에 따라, 때때로 거리에 따라 감쇠된다. 스포트라이트의 원뿔 밖에서는 조명을 주지 않는다.
If an object is outside the effective range of either kind of light source, it does not need to cast a shadow, because any object behind it is also outside the range. Detect this case by testing the bounding box of a shadow caster against the bounding sphere of a distance-attenuated light, or against the cone of an angularly attenuated light. When an object is outside the range, don't mark shadows for it. Likewise, no illumination pass is needed for objects outside the light's range.
물체가 각 종류의 광원의 유효한 범위 밖에 있으면, 그림자를 캐스트할 필요가 없다. 왜냐하면 더 뒤에 있는 물체 또한 범위 밖에 있기 때문이다. 그림자 캐스터의 바운딩 박스를 거리에 따라 감쇠되는 조명의 바운딩 구에 대해서나 모나게 감쇠되는 조명의 원뿔의 대해 테스트하여 이런 경우를 탐지하라. 물체가 범위 밖에 있을 때, 그 물체에 대한 그림자를 표시하지 말라. 마찬가지로, 조명의 범위 밖에 있는 물체에 대한 조명 패스 역시 필요하지 않다.
9.5.3 Culling Shadow Volumes
Just as with regular geometry, the vertex-processing rate may be improved for shadow volumes by culling shadow geometry outside the view frustum. Note that the caster may be outside the view frustum and still cast a shadow on visible objects, so culling the caster and the shadow geometry are completely independent.
보통의 기하와 마찬가지로, 뷰 절두체 밖에 있는 그림자 기하를 컬링하여 그림자 볼륨에 대한 정점-처리율을 향상시킬 수 있을 것이다. 캐스터가 뷰 절두체 밖에 있어도 여전히 눈에 보이는 물체에 그림자를 캐스트하고, 그래서 캐스터와 그림자 기하는 완전히 독립적이라는 것을 주의하라.
Cull the sides and cap separately. For each, approximate the shadow geometry with a geometric primitive and cull that primitive against the view frustum. The light cap can use the same bounds as the caster geometry, because the cap is inside the caster. The dark cap uses the same geometry, but sent to infinity away from the light source.
측면과 뚜껑을 개별적으로 컬링하라. 각각에 대해, 그림자 기하를 기하학적인 원형에 근사하고 뷰 절두체에 대해 컬링하라. 밝은 뚜껑은 캐스터 기하와 동일한 경계를 사용할 수 있다. 왜냐하면 뚜껑은 캐스터 안에 있기 때문이다. 어두운 뚜껑은 동일한 기하를 사용하지만 광원으로부터 무한으로 보내진다.
For example, say a bounding box is available for the caster. Transform each vertex, v, of the bounding box to infinity using the equation v' = MV * (v * L w - L xyz where L is the object-space light vector and MV is the modelview matrix. Then test the transformed bounding box against the view frustum. If the box is culled, the dark cap can also be culled. The shadow volume sides are most easily bounded by a cylinder for directional lights and by a truncated cone for point lights.
예를 들어, 바운딩 박스가 캐스터에 유용하다고 하자. 바운딩 박스의 각 정점 v을 방정식 v' = MV * (v * L w - L xyz)을 사용하여 무한으로 변형하라. L 은 물체-공간 조명 백터이고 MV은 모델뷰 행렬이다. 그런 다음 변형된 바운딩 박스를 뷰 절두체에 대해 테스트하라. 만약 박스가 컬링되면, 어두운 뚜껑 또한 컬링될 수 있다. 그림자 볼륨 측면은 방향성 조명을 위한 실린더와 점 조명을 위한 끝이 잘린 원뿔에 의해 가장 쉽게 바운드된다.
Although any culling is helpful, culling the caps particularly speeds up vertex processing because caps have many more vertices than sides. For point lights, the dark cap is potentially huge; culling it can also save a lot of fill rate. This is the effect we see in cartoons when a kitten casts a lion's shadow by standing in front of a flashlight. This magnifying effect was illustrated in Figure 9-7, where the dark cap for the model is several times larger than the model itself.
어떤 컬링이든 도움이 되긴 하지만, 측면보다는 뚜껑이 훨씬 많은 정점을 가지고 있기 때문에 뚜껑을 컬링하는 것은 특히 정점 처리 속도를 향상시킬 수 있다. 점 광원의 경우 어두운 뚜껑은 거대할 가능성이 있다; 그것을 컬링하면 많은 필레이트를 절약할 수 있다. 이것은 만화에서 새끼 고양이가 손전등 앞에 서있으면 사자의 그림자가 생기는 것과 같은 효과이다. 이런 확대 효과는 그림 9-7에 나타나있다. 모델의 어두운 뚜껑은 모델 자신보다 몇 배는 더 크다.
9.5.4 Uncapped Performance
Even when the caps would otherwise be unculled, we can use another technique to remove the caps for a special case in which the viewport is unshadowed.
비록 뚜껑이 컬링되지 않을 때라도, 뷰포트가 그림자가 지지않는 특별한 경우에 뚜껑을 제거하는 또 다른 테크닉을 사용할 수 있다.
In the mathematical formulation, we used rays from a point to infinity away from the viewer. In the implementation, these rays were simulated by rendering polygons to the stencil buffer. We moved the far clipping plane to infinity and sent rays away from the viewer so that we wouldn't miss any intersections between the point and infinity because of clipping.
수학상 공식에서, 우리는 점으로부터 관찰자에서 무한으로 멀어지는 광선을 사용했다. 구현에서, 이 광선들은 스텐실 버퍼에 폴리곤을 렌더링함으로써 시뮬레이션되었다. 우리는 원절단면을 무한으로 이동하고 관찰자로부터 광선을 내보냈다. 그래서 클리핑 때문에 점과 무한 사이에 교점을 놓치지는 않을 것이다.
It's possible to count in the other direction. To count away from the viewer, increment or decrement the stencil buffer when the depth test fails. To count toward the viewer, increment or decrement when the depth test passes. When the viewport is not in a shadow volume, the number of intersections along a line segment from an unshadowed point to the image plane is zero. This is because the line had to pass through exactly the same number of entering and exiting intersections to get from an unshadowed point to an unshadowed viewport. If the point is shadowed, the number of intersections will be nonzero. Of course, we can count in this direction only if the viewport is not in a shadow itself; otherwise, the count will be off by the number of shadow volumes enclosing each viewport pixel. Figure 9-7 showed a case where this optimization can be used because the shadows, which stretch back into the scene, do not enclose the viewport. Figure 9-2 showed an example where it cannot be used, because the viewport is in the shadow cast by the pumpkin—in fact, everything in the scene is in shadow, except the triangles of ground plane, where light shines out of the eyes.
다른 방향으로 카운트하는 것이 가능하다. 관찰자로부터 시작하여 카운트하기 위해, 깊이 테스트가 실패했을 때 스텐실 버퍼를 증가 혹은 감소시켜라. 관찰자쪽으로 카운트하기 위해서는, 깊이 테스트를 통과했을 때 스텐실 버퍼를 증가, 감소시켜라. 뷰포트가 그림자 볼륨 안에 있지 않을 때, 그림자 지역이 아닌 점에서 이미지 평면으로의 선분을 따라 있는 교점의 수는 0이다. 이것은 그림자가 지지 않은 점과 그림자가 지지 않은 뷰포트 사이에서 얻은 진입, 진출 교점의 수가 정확히 같기 때문이다. 점이 그림자 지역이라면 교점의 수는 0이 아닐 것이다. 물론, 뷰포트 자신이 그림자 안에 있지 않을 때만 이 방향으로 카운트할 수 있다; 만약 그렇지 않으면, 카운트는 각 뷰포트 픽셀을 둘러싼 그림자 볼륨의 수만큼 빠질 것이다. 그림 9-7은 장면에서 뒤로 뻗는 그림자가 뷰포트를 둘러싸지 않기 때문에 이 최적화가 사용될 수 있는 경우를 보여줬다. 그림 9-2는 뷰포트가 호박에 의해 캐스트된 그림자 안에 있기 때문에 이것이 사용될 수 없는 예를 보여주었다-사실, 눈에서 나온 빛이 비추는 지면의 삼각형을 제외하고는 장면에 있는 모든 것은 그림자 안에 있다.
The advantage of counting toward the viewer is that we don't need to render the light and dark caps. The light cap will always fail the depth test, because it is inside the shadow caster, so there is no reason to render it. Because we're counting from visible points to the viewer, there is no way for the dark cap (which is at infinity) to create intersections, and so we don't need to render it, because it can't change the result.
관찰자를 향한 방향으로 카운트하는 것의 이 점은 조명과 밝고 어두운 뚜껑을 렌더링할 필요가 없다는 거이다. 밝은 뚜껑은 그림자 캐스터 안에 있기 때문에 항상 깊이 테스트에 실패할 것이다. 그래서 그것을 렌더링할 이유가 없다. 눈에 보이는 점에서 관찰자 쪽으로의 방향으로 카운트하고 있기 때문에, (무한에 있는) 어두운 뚜껑이 교점을 만들 수 있는 아무런 방법도 없다. 그래서 어두운 뚜껑은 결과를 바꿀 수 없기 때문에 렌더링할 필요가 없다.
This optimization requires two changes to the code:
이 최적화는 코드에서 두 군데를 변경해야한다.
- We need to test whether the viewport is (conservatively) in a shadow volume. This test is performed separately for each shadow caster; we can choose our counting direction independently for each caster and still get a correct result.
뷰포트가 그림자 볼륨 안에 있는지 (어림잡아) 테스트할 필요가 있다. 이 테스트는 각 그림자 캐스터에 개별적으로 수행된다; 각 캐스터에 독립적으로 카운팅 방향을 선택할 수 있고 여전이 정확한 결과를 얻는다.
- If the viewport is not in a shadow volume, we need to reverse the increment/decrement sense of the stencil operations (for that caster only).
뷰포트가 그림자 볼륨 안에 있지 않다면, 스텐실 연산의 증감 분별를 반대로할 필요가 있다(그 캐스터에 대해서만).
Figure 9-9 shows the occlusion pyramid of the viewport. The tip is at the light source (which is at infinity if it is a directional light), and the base is the viewport. If the bounding box of the shadow caster intersects this pyramid, the viewport may be shadowed and the optimization cannot be used. In that case, we must render with the normal depth-fail operations and draw both caps, if visible. If the bounding box does not intersect the pyramid, we can change the stencil operations.
그림 9-9는 뷰포트의 폐색 피라미드를 보여준다. 팁은 (방향성 조명이라면 무한에 있는)광원에 있다. 그리고 토대는 뷰포트이다. 그림자 캐스트의 바운딩 박스가 이 피라미드와 교차하면, 뷰포트는 그림자가 질 수도 있고 최적화를 사용할 수 없다. 이 경우에, 보통의 깊이-실패 연산으로 렌더링하고 두 뚜껑이 보인다면 그린다. 바운딩 박스가 피라미드와 교차하지 않으면, 스텐실 연산을 바꿀 수 있다.
The occlusion pyramid can be on either side of the viewport. If the shadow caster intersects the green pyramid, the "uncapped" optimization cannot be used.
폐색 피라미드는 뷰포트의 어떤 쪽에도 있을 수 있다. 만약 그림자 캐스터가 녹색 피라미드와 교차한다면, “뚜껑이 없는” 최적화는 사용할 수 없다.
For counting toward the viewer, set the stencil operations as follows:
관찰자를 향한 방향의 카운트를 위해, 스텐실 연산을 다음과 같이 설정하라:
// Increment on front faces, decrement // on back faces (a.k.a. z-pass rendering) glActiveStencilFaceEXT(GL_FRONT); glStencilOp(GL_KEEP, GL_KEEP, GL_INCR_WRAP_EXT); glActiveStencilFaceEXT(GL_BACK); glStencilOp(GL_KEEP, GL_KEEP, GL_DECR_WRAP_EXT);
Because this is "uncapped" rendering, omit the code to render shadow volume caps entirely from this case.
이것은 “뚜껑이 없는” 렌더링이기 때문에, 이 경우에 그림자 볼륨 뚜껑을 렌더링하는 코드를 완전히 생략한다.
9.6 Fill-Rate Optimizations
Fill rate is the Achilles heel of shadow volumes. Shadow volumes cover many pixels and have a lot of overdraw. This is particularly troublesome for point lights, which create shadows that get bigger the farther they are from the caster. Fortunately, point lights also have great optimization potential, because their attenuation creates a range beyond which illumination is practically zero. We've already discussed not marking shadows for casters outside this range and not rendering illumination on objects outside the range. Now we'll look at three ways to reduce the fill rate required for casters inside the range: finite volumes, XY clipping, and z-bounds.
필레이트는 그림자 볼륨의 아킬레스건이다. 그림자 볼륨은 많은 픽셀을 덮고 많은 overdraw를 가지고 있다. 이것은 그림자가 캐스터로부터 멀어질수록 더 커지는 점 조명의 경우에 특히 문제가 된다. 다행히도, 점 조명은 탁월한 최적화 가능성 또한 가지고 있다. 왜냐하면 점 조명의 감쇠는 조명이 거의 0이 되는 범위를 생성할 수 있기 때문이다. 우리는 이미 이 범위 밖에 있는 캐스터의 그림자를 표시하지 않고 이 범위 밖에 있는 물체에 조명을 렌더링하지 않는 것을 논의했다. 이제 범위 안에 있는 캐스터를 위해 요구되는 필레이트를 줄이는 3가지 방법 제한된 볼륨, XY 클리핑, z-바운드를 볼 것이다.
9.6.1 Finite Volumes
The range of a point light forms a sphere. Objects outside this sphere don't receive illumination, so there is no need to cast shadows beyond the sphere. Instead of extruding shadow volumes to infinity, we can extend them by the radius of the sphere and save the fill rate of rendering infinite polygons. This is a straightforward change to the vertex shader that can recoup significant fill rate. Because the dark cap is more likely to be on-screen under this method, it may increase the geometry processing because the dark cap is less likely to be culled.
점 조명의 범위는 구 형태이다. 이 구 밖에 있는 물체는 조명을 받지 않는다. 그래서 구 너머에 그림자를 캐스트할 필요가 없다. 그림자 볼륨을 무한으로 밀어내는 대신에, 그림자 볼륨을 구의 반지름만큼 뻗어서 무한 폴리곤 렌더링의 필레이트를 절약할 수 있다. 이것은 필레이트를 상당히 되찾을 수 있는 정점 쉐이더에 직접적인 변화이다. 어두운 뚜껑은 이 방법에서 거의 스크린 안에 있기 때문에, 어두운 뚜껑이 거의 컬링되지 않아서 기하 처리가 증가할 수도 있다.
An alternative is to still create polygons that stretch to infinity, but clip them to the light radius in 2D, as described in the next optimization.
다른 방법은 여전히 무한으로 뻗은 폴리곤을 만들기는 하지만 다음 최적화에 설명한 것과 같이 2D에서 조명 반지름으로 그것들을 자르는 것이다.
9.6.2 XY Clipping
The range sphere projects to an ellipse on screen. only pixels within that ellipse can be illuminated. We don't need to render shadow polygons or illumination outside of this ellipse. However, hardware supports a rectangular clipping region, not an elliptical one. We could compute the bounding box of the projected ellipse, but it is more convenient to use the 2D bounding box of the projected 3D bounding box surrounding the light range. Although the fixed-function pipeline supports only radial attenuation, artists can achieve more controlled effects by specifying an arbitrary attenuation function over the cubic volume about a light, as done in Doom 3. Attenuation can fall off arbitrarily within a box, so we just use that box as the light range. Clip the light's box to the view frustum. If it is not entirely clipped, project all vertices of the remaining polyhedron onto the viewport and bound them. That final 2D bound is used as the rectangular clipping region. Set the clipping region with the glScissor command:
범위 구는 스크린에 타원으로 투영된다. 그 타원안에 있는 픽셀만이 조명을 받을 수 있다. 이 타원 밖에 있는 그림자 폴리곤이나 조명을 렌더링할 필요가 없다. 그러나, 하드웨어는 타원이 아니라 사각형 클리핑 지역을 지원한다. 투영된 타원의 바운딩 박스를 계산할 수는 있지만 조명 범위 주위에 투영된 3D 바운딩 박스의 2D 바운딩 박스를 사용하는 것이 편리하다. 고정-기능 파이프라인이 반지름 감쇠만을 지원한다고 할지라도, 아티스트는 Doom 3에서 한 것과 같이 조명에 대해 입방체 볼륨 위에 임의의 감쇠 함수를 지정하여 더 통제되는 효과를 달성할 수 있다. 감쇠는 박스 안에서 임의로 떨어질 수 있다. 그래서 우리는 그 박스를 조명 범위로 사용한다. 조명 박스를 뷰 절두체에 클리핑하라. 그것이 완전히 클리핑되지 않으면, 나머지 다면체의 모든 정점을 뷰포트 위에 투영하고 그것들을 바운드하라. 그 최종 2D 바운드는 사각형 클리핑 지역으로써 사용된다. 클리핑 지역을 glScissor 명령어로 설정하라.
glScissor(left, top, width, height); glEnable(GL_SCISSOR_TEST);
Figure 9-10 shows a Quake 3 character standing outside a building. This is the scene from Figure 9-7, but now the camera has moved backward. The single point light creates shadow volumes from the character and the building (shown in yellow), which would fill the screen were they not clipped to the effective bounds of the light. The scissor region is shown in the right half of the figure as a white box. The left half of the figure shows the visible scene, where the effect of clipping is not apparent because the light does not illuminate the distant parts of the building. For this scene, rendering only the shadow volume pixels within the scissor region cuts the fill-rate cost in half.
그림 9-10은 빌딩 밖에 서있는 Quake 3캐릭터를 보여준다. 이것은 그림 9-7의 장면이다. 하지만 이제 카메라는 뒤로 움직였다. 하나의 점 조명이 조명의 효과적인 경계에 클리핑되지 않은 것들로 스크린을 채울 캐릭터와 빌딩으로부터 그림자 볼륨(노란색으로 보여지는 것)을 생성한다. 잘린 지역은 오른쪽 그림에 흰 박스로 나타나있다. 왼쪽 그림은 조명이 빌딩의 먼 부분을 비추지 않기 때문에 클리핑의 효과가 뚜렷하지 않는 장면을 보여준다. 이 장면에서, 잘린 지역 안에 있는 그림자 볼륨 픽셀만 렌더링하는 것은 필레이트 비용을 반으로 줄인다.

Figure 9-10 Clipping in Quake3
9.6.3 Z-Bounds
If the point at a given pixel is outside of the light range—because it is either closer to the viewer or farther from the viewer than the range bounds—that point cannot be illuminated, so we don't need to make a shadow-marking or illumination pass over that pixel. Restricting those passes to a specific depth range means that we pay fill rate for only those pixels actually affected by the light, which is potentially fewer pixels than those within the 2D bounds of the light.
주어진 픽셀에 점이 조명 범위 밖에 있다면 – 그것은 범위 경계보다 관찰자에 더 가까이 있거나 관찰자로부터 더 멀리 있기 때문에 – 그 점은 조명이 비춰질 수 없다. 그래서 그 픽셀에 그림자-표시나 조명 패스를 만들 필요가 없다. 특정 깊이 범위에 그런 패스들을 제한하는 것은 빛에 의해 실제로 영향을 받는 픽셀들에만 필레이트를 소비한다는 것을 의미한다. 그러한 픽셀들은 잠재적으로 조명의 2D 경계 안에 있는 것보다 더 적다.
The glDepthBoundsEXT function lets us set this behavior:
glDepthBoundsEXT 함수는 이런 행동을 설정한다.
glEnable(GL_DEPTH_BOUNDS_TEST_EXT); glDepthBoundsEXT(zMin, zMax);
This setting prevents rendering a pixel where the depth buffer already has a value outside the range [zMin, zMax]—that is, where the point visible at that pixel (rendered in the ambient pass) is outside the range. This is not the same as a clipping plane, which prevents rendering new pixels from polygons past a bound.
이 설정은 깊이 버퍼가 이미 [zMin, zMax]범위 밖의 값을 가지고 있는 픽셀을 렌더링하는 것을 방지한다 – 즉, 그 픽셀에서 보이는 점(ambient 패스에서 렌더링된)은 범위 밖에 있다. 이것은 경계를 지나간 폴리곤으로부터 새로운 픽셀들을 렌더링하는 것을 막는 클리핑 평면과는 다른 것이다.
Figure 9-11 shows a viewer looking at an object illuminated from a point light. The caster's shadow projects downward toward the rugged ground slope. The bold green portion of the ground can't possibly be shadowed by the caster. The depth-bounds test saves the fill rate of rendering the orange parts of the shadow volume because the visible pixels behind them (the bold green ones) are outside the bounds. Notice that the shadow volume itself is inside the bounds, but this is irrelevant—the depth bound applies to the pixel rendered in the ambient pass, not to the shadow geometry.
그릠 9-11은 관찰자가 점 조명으로부터 빛을 받는 물체를 보는 것을 보여준다. 캐스터의 그림자는 울퉁불퉁한 지면 경사를 향해 아래쪽으로 투영한다. 지면의 굵은 녹색 부분은 캐스터에 의해 어떻게든 그림자가 질 수 없다. 깊이-경계 테스트는 그림자 볼륨의 오렌지색 부분을 렌더링하는 필레이트를 절약한다. 왜냐하면 그것들 뒤에 보이는 픽셀들(굵은 녹색)은 경계 밖에 있기 때문이다. 그림자 볼륨 자신은 경계 안에 있지만 무의미하다는 것을 주목하라 – 깊이 경계는 그림자 기하가 아니라 ambient 패스에 렌더링된 픽셀에 적용되기 때문이다.
Note that the depth bounds are more restrictive than just the light range. It is the depth range defined by the intersection of the view frustum, the light range, and the shadow volume bounds. The arguments to the OpenGL function are post-projective camera-space values. If the geometry of the intersection is defined by a polyhedron whose vertices are stored in an array std::vector<Vector4> boundVert, the arguments are computed as:
깊이 경계는 조명 범위보다 더 제한적이라는 것을 주의하라. 그것은 뷰 절두체, 조명 범위, 그림자 볼륨 경계의 교차에 의해 정의된 깊이 범위이다. OpenGL 함수에서 인자는 후-투영 카메라-공간 값이다. 교차 기하가 정점이 배열 std::vector<Vector4> boundVert 에 저장된 다면체에 의해 정의된다면, 인자는 다음처럼 계산된다:
float zMin = 1.0f; float zMax = 0.0f; for (int v = boundVertex.size() - 1; v >= 0; --v) { float z = 1.0f / (projectionMatrix * boundVert[v]).w; zMin = min(zMin, z); zMax = max(zMax, z); }
9.7 Future Shadows
The current, highly optimized shadow volume method is the result of contributions from industry and academia over the past several decades. The basic method was introduced by Frank Crow at SIGGRAPH 1977 and has matured into the method described in this chapter. The history of shadow volumes and the individual contributions of several researchers and developers are summarized in technical reports available on the NVIDIA Developer Web site (Everitt and Kilgard 2002, McGuire et al. 2003). McGuire et al. 2003 gives a formal description and analysis of the method presented in this chapter.
현재, 대단히 최적화된 그림자 볼륨 방식은 지난 수십년에 걸쳐 기업과 학계로부터의 기여의 결과이다. 기본 방식은 SIGGRAPH 1977에서 Frank Crow에 의해 소개되었고 이 챕터에서 설명한 방식으로 발달되었다. 그림자 볼륨의 역사와 여러 연구원과 개발자들의 개별적인 기여가 NVIDIA 개발자 웹사이트에서 이용할 수 있는 테크닉 리포트에 요약되어있다(Everitt and Kilgard 2002, McGuire et al. 2003). McGuire et al. 2003 은 이 챕터에서 소개된 방법의 정식 서술과 분석을 제공한다.
Improving the performance of shadow volume generation through new optimizations continues to be an active research area. Silhouette determination has always been performed on the CPU, which is a major limitation. It precludes the use of matrix skinning or other deformations in the vertex shader and otherwise serializes rendering on CPU operations.
새로운 최적화를 통해 그림자 볼륨 생성의 성능을 향상시키는 것은 계속해서 활동적인 연구 영역으로 남아있다. 실루엣 결정은 많은 제한이 있는 CPU에서 항상 수행된다. 그것은 정점에서 행렬 스키닝이나 다른 변형의 사용을 막는다.
Several solutions have been proposed for performing silhouette determination directly on programmable graphics hardware. Michael McCool (2001) proposed a method for computing the caster silhouettes from a shadow map. Brabec and Seidel (2003) push geometry encoded as colors through the pixel processor, where they compute silhouettes. They then read back the frame buffer and use it as a vertex buffer for shadow rendering. John Hughes and I recently described how to find silhouettes and extrude them into shadow volume sides entirely in a vertex shader using a specially precomputed mesh (McGuire and Hughes 2003).
프로그래밍 가능한 그래픽 하드웨어에서 직접적으로 실루엣 결정을 수행하는 여러 해결책들이 소개되고 있다. Michael McCool (2001)은 캐스터 실루엣을 그림자 맵으로부터 계산하는 방법을 소개했다. Brabec과 Seidel(2003)은 실루엣을 계산하는 픽셀 프로세서를 통해 기하를 색으로 인코딩했다. 그런 다음 프레임 버퍼를 다시 읽어와서 그것을 그림자 렌더링을 위한 정점 버퍼로 사용했다. John Hughes와 나는 최근 특별히 미리 계산된 메쉬를 사용하여 어떻게 정점 쉐이더에서 전체적으로 실루엣을 찾아서 그림자 볼륨 측면으로 그것들을 밀어내는지를 설명했다(McGuire와 Hughes 2003).
Getting good-looking, high-performance soft shadows from area light sources with shadow volumes is another open research topic. Ulf Assarsson and Tomas Akenine-Möller have worked on this problem for some time. Their most recent paper, with Michael Dougherty and Michael Mounier (Assarsson et al. 2003), describes how to construct explicit geometry for the interior and exterior edges of the penumbra (the soft-shadow region) and makes heavy use of programmable hardware.
보기 좋은 장면을 얻기 위해, 그림자 볼륨을 가지는 영역 광원으로부터 고성능 부드러운 그림자를 만다는 것은 또 다른 열린 연구 화제이다. Ulf Assarsson과 Tomas Akenine- Möller 은 한동안 이 문제를 연구했다. 그들의 가장 최근 문서 Michael Dougherty and Michael Mounier (Assarsson et al. 2003) 는 반그림자(부드러운 그림자 영역)의 내외부를 위한 명시적인 기하를 어떻게 만들고 프로그래밍 가능한 하드웨어의 사용을 높이는지 설명한다.
Several people have proposed joining the individual silhouette edges into connected strips so that quad strips (for point lights) and triangle fans (for directional lights) can be used to render the shadow volume sides. Alex Vlachos and Drew Card (2002) have been working on another simplification idea: culling and clipping nested shadow volumes, because they won't affect the final result.
여러 사람들이 개별적인 실루엣 모서리를 연결된 띠에 결합해서 (점 조명을 위한)사각형 띠와 (방향성 조명을 위한)삼각형 팬이 그림자 볼륨 측면을 렌더링하는데 사용될 수 있도록 하는 것을 소개하고 있다. Alex Vlachos와 Drew Card (2002)는 또 다른 단순화 아이디어를 연구하고 있다: 겹쳐진 그림자 볼륨은 최종 결과에 영향을 주지 않을 것이기 때문에 그것들을 컬링하고 클리핑한다.
All of these methods are experimental and have yet to be refined and proven in an actual game engine. If you are interested in moving beyond the capabilities of the current shadow volume method, these are good starting points. Hopefully, future research and graphics hardware will improve and accelerate these methods.
이 모든 방법들은 실험적이고 아직 실제 게임 엔진에 맞춰지고 시험되지 않았다. 현재 그림자 볼륨 방식의 능력을 넘는데에 관심이 있다면, 이것들은 좋은 출발점이다. 아마, 미래의 연구와 그래픽 하드웨어는 이 방법들을 향상시키고 가속화시킬 것이다.
9.8 References
Everitt, Cass, and Mark Kilgard. 2002. "Practical and Robust Stenciled Shadow Volumes for Hardware-Accelerated Rendering." NVIDIA Corporation. Available online at http://developer.nvidia.com/object/robust_shadow_volumes.html
McGuire, Morgan, John F. Hughes, Kevin Egan, Mark Kilgard, and Cass Everitt. 2003. "Fast, Practical and Robust Shadows." Available online at http://developer.nvidia.com/object/fast_shadow_volumes.html . An early version appeared as Brown Univ. Tech. Report CS03-19.
Tekkaman Blade robot model by Michael Mellor (mellor@iaccess.com.au); Tick model by Carl Schell (carl@cschell.com). Both available for download at http://www.polycount.com. Cathedral model by Sam Howell (sam@themightyradish.com), courtesy Sam Howell and Morgan McGuire. "The Tick" character is a trademark of New England Comics. Quake 2, Quake 3, and Doom 3 are trademarks of id Software.
Copyright
Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and Addison-Wesley was aware of a trademark claim, the designations have been printed with initial capital letters or in all capitals.
The authors and publisher have taken care in the preparation of this book, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein.
The publisher offers discounts on this book when ordered in quantity for bulk purchases and special sales. For more information, please contact:
U.S. Corporate and Government Sales
(800) 382-3419
corpsales@pearsontechgroup.com
For sales outside of the U.S., please contact:
International Sales
international@pearsoned.com
Visit Addison-Wesley on the Web: www.awprofessional.com
Library of Congress Control Number: 2004100582
GeForce™ and NVIDIA Quadro® are trademarks or registered trademarks of NVIDIA Corporation.
RenderMan® is a registered trademark of Pixar Animation Studios.
"Shadow Map Antialiasing" © 2003 NVIDIA Corporation and Pixar Animation Studios.
"Cinematic Lighting" © 2003 Pixar Animation Studios.
Dawn images © 2002 NVIDIA Corporation. Vulcan images © 2003 NVIDIA Corporation.
Copyright © 2004 by NVIDIA Corporation.
All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form, or by any means, electronic, mechanical, photocopying, recording, or otherwise, without the prior consent of the publisher. Printed in the United States of America. Published simultaneously in Canada.
For information on obtaining permission for use of material from this work, please submit a written request to:
Pearson Education, Inc.
Rights and Contracts Department
One Lake Street
Upper Saddle River, NJ 07458
Text printed on recycled and acid-free paper.
5 6 7 8 9 10 QWT 09 08 07
5th Printing September 2007