第17章 影
現在、光が 3D シーン内のオブジェクトにどのように影響するかを表すことができます。より多くの光を受け取るオブジェクトは、光を受けないオブジェクトよりも明るく表示されます。ただし、まだ影を落とすことはできません。影は、3D シーンのリアリズムの度合いを高めます。これが、この章で行うことです。
シャドウ マッピング
ゲームで広く使用されており、エンジンのパフォーマンスに深刻な影響を与えないシャドウ マッピングという手法を使用します。シャドウ マッピングは簡単に理解できるように見えるかもしれませんが、正しく実装するのはやや困難です。または、より正確に言えば、すべての潜在的なケースをカバーし、一貫した結果を生成する一般的な方法で実装することは非常に困難です。
それでは、特定の領域 (実際にはフラグメント) が影にあるかどうかを確認する方法を考えることから始めましょう。その領域を描画しているときに、光線を光源に投射し、衝突せずに光源に到達できる場合、そのピクセルは光の中にあります。そうでない場合、ピクセルは影になっています。
次の図は、ポイント ライトの場合を示しています。ポイント PA はソース ライトに到達できますが、ポイント PB と PC は到達できないため、影になっています。
そのレイを衝突なしでキャストできるかどうかを効率的に確認するにはどうすればよいでしょうか? 光源は理論的に無限の光線を放つことができますが、光線がブロックされているかどうかを確認するにはどうすればよいでしょうか? レイ ライトをキャストする代わりにできることは、ライトの視点から 3D シーンを見て、その場所からシーンをレンダリングすることです。カメラをライトの位置に設定してシーンをレンダリングし、各フラグメントの深度を保存できるようにします。これは、光源までの各フラグメントの距離を計算することと同じです。最後に、光源から見た最小距離をシャドウ マップとして保存します。
次の図は、平面上に浮かんでいる立方体と垂直なライトを示しています。
光の視点から見たシーンは、このようなものになります (色が濃いほど、光源に近くなります)。
その情報を使用して、通常どおり 3D シーンをレンダリングし、保存されている最小距離で光源までの各フラグメントの距離を確認できます。距離がシャドウ マップに格納されている値よりも小さい場合、オブジェクトは明るくなり、それ以外の場合は影になります。同じレイ ライトが当たる可能性のあるオブジェクトを複数持つことができますが、最小距離を保存します。
したがって、シャドウ マッピングは 2 段階のプロセスです。
・最初に、シーンをライト スペースからシャドウ マップにレンダリングして、最小距離を取得します。
・次に、カメラの視点からシーンをレンダリングし、その深度マップを使用して、オブジェクトが影にあるかどうかを計算します。
深度マップをレンダリングするには、深度バッファについて説明する必要があります。シーンをレンダリングすると、すべての深度情報は、明らかに深度バッファ (または z バッファ) という名前のバッファに格納されます。その深度情報は、
レンダリングされる各フラグメントの値。最初の章で、シーンのレンダリング中に行っていたことを思い出すと、ワールド座標からスクリーン座標に変換されます。範囲の座標空間に描画しています0に1にとってxとy軸。オブジェクトが別のオブジェクトよりも離れている場合、これがオブジェクトにどのように影響するかを計算する必要がありますzとy透視投影行列による座標。によっては自動計算されません。z
価値はありますが、当社で行う必要があります。実際に z 座標に格納されるのは、そのフラグメントの深さであり、それ以下でもそれ以上でもありません。
カスケード シャドウ マップ
上記の解決策は、そのままでは、オープン スペースの質の高い結果を生み出しません。その理由は、影の解像度がテクスチャ サイズによって制限されるためです。現在、潜在的に巨大な領域をカバーしており、深度情報を保存するために使用しているテクスチャは、良い結果を得るには十分な解像度がありません。テクスチャの解像度を上げるだけで解決できると思われるかもしれませんが、これだけでは問題を完全に解決するには不十分です。そのためには、巨大なテクスチャが必要になります。したがって、基本を説明したら、単純なシャドウ マップを改良したカスケード シャドウ マップ (CSM) と呼ばれる手法について説明します。
重要な概念は、カメラに近いオブジェクトの影は、遠くのオブジェクトの影よりも高品質である必要があるということです。1 つの方法として、カメラの近くにあるオブジェクトの影をレンダリングすることもできますが、これではシーン内を移動している限り、影が現れたり消えたりします。
カスケード シャドウ マップ (CSM) が使用するアプローチは、視錐台をいくつかの分割に分割することです。カメラに近い分割はより少ない空間をカバーしますが、遠い領域はより広い領域をカバーします。次の図は、3 つの分割に分割された視錐台を示しています。
これらの分割ごとに深度マップがレンダリングされ、各分割に適合するようにライト ビューと投影マトリックスが調整されます。したがって、深度マップを格納するテクスチャは、視錐台の縮小された領域をカバーします。また、カメラに最も近いスプリットがカバーするスペースが少ないため、深度解像度が向上します。
上記の説明から推測できるように、分割と同じ数の深度テクスチャが必要になり、それぞれのライト ビューと投影マトリックスも変更します。したがって、CSM を適用するために実行する手順は次のとおりです。
- 視錐台を n 個の分割に分割します。
- 深度マップのレンダリング中、分割ごとに:
- ライト ビューと投影行列を計算します。
- シーンをライトの視点から別の深度マップにレンダリングします
- シーンのレンダリング中:
- 上記で計算された深度マップを使用します。
- 描画するフラグメントが属する分割を決定します。
- シャドウ マップと同様にシャドウ ファクターを計算します。
のとおり、CSM の主な欠点は、分割ごとにライトの視点からシーンをレンダリングする必要があることです。これが、オープン スペースにのみ使用されることが多い理由です (もちろん、キャッシュをシャドウ計算に適用してオーバーヘッドを削減できます)。
実装
最初に作成するクラスは、ライト パースペクティブからシャドウ マップをレンダリングするために必要なマトリックスを計算します。このクラスには名前が付けられCascadeShadow、特定のカスケード シャドウ スプリット (projViewMatrixアトリビュート) の投影ビュー マトリックス (ライト パースペクティブから) と、その正射影マトリックスのファー プレーン距離(アトリビュート) が格納されsplitDistanceます。
public class CascadeShadow {
public static final int SHADOW_MAP_CASCADE_COUNT = 3;
private Matrix4f projViewMatrix;
private float splitDistance;
public CascadeShadow() {
projViewMatrix = new Matrix4f();
}
...
public Matrix4f getProjViewMatrix() {
return projViewMatrix;
}
public float getSplitDistance() {
return splitDistance;
}
...
}
このCascadeShadowクラスは、カスケード シャドウ インスタンスのリストを という名前の適切な値で初期化する静的メソッドを定義しますupdateCascadeShadows。このメソッドは次のように始まります。
public class CascadeShadow {
...
public static void updateCascadeShadows(List<CascadeShadow> cascadeShadows, Scene scene) {
Matrix4f viewMatrix = scene.getCamera().getViewMatrix();
Matrix4f projMatrix = scene.getProjection().getProjMatrix();
Vector4f lightPos = new Vector4f(scene.getSceneLights().getDirLight().getDirection(), 0);
float cascadeSplitLambda = 0.95f;
float[] cascadeSplits = new float[SHADOW_MAP_CASCADE_COUNT];
float nearClip = projMatrix.perspectiveNear();
float farClip = projMatrix.perspectiveFar();
float clipRange = farClip - nearClip;
float minZ = nearClip;
float maxZ = nearClip + clipRange;
float range = maxZ - minZ;
float ratio = maxZ / minZ;
...
}
...
}
まず、シーンのレンダリングに使用する透視投影の分割データ、ビューと投影の行列、ライトの位置、近距離と遠距離のクリップを計算するために必要な行列を取得します。その情報を使用して、各シャドウ カスケードの分割距離を計算できます。
public class CascadeShadow {
...
public static void updateCascadeShadows(List<CascadeShadow> cascadeShadows, Scene scene) {
...
// Calculate split depths based on view camera frustum
// Based on method presented in https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch10.html
for (int i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
float p = (i + 1) / (float) (SHADOW_MAP_CASCADE_COUNT);
float log = (float) (minZ * java.lang.Math.pow(ratio, p));
float uniform = minZ + range * p;
float d = cascadeSplitLambda * (log - uniform) + uniform;
cascadeSplits[i] = (d - nearClip) / clipRange;
}
...
}
...
}
分割位置の計算に使用されるアルゴリズムは、対数スキーマを使用して距離をより適切に分散します。カスケードを均等に分割する、または事前に設定された比率に従って分割するなど、他のさまざまなアプローチを使用することもできます。対数スキーマの利点は、近くのビューの分割に使用するスペースが少なく、カメラに近い要素の解像度が高くなることです。数学の詳細については、NVIDIA の記事を参照してください。配列には [0, 1] の範囲の値のセットが含まれます。cascadeSplitsこれを後で使用して、必要な計算を実行し、各カスケードの分割距離と射影行列を取得します。
カスケード分割のすべてのデータを計算するループを定義します。そのループでは、最初に NDC (Normalized Device Coordinates) 空間に錐台コーナーを作成します。その後、ビュー マトリックスとパースペクティブ マトリックスの逆数を使用して、これらの座標をワールド空間に投影します。ディレクショナル ライトを使用しているため、シャドウ マップのレンダリングには正投影行列を使用します。これが、NDC 座標として、可視ボリュームを含む立方体の境界のみを設定する理由です (遠くのオブジェクトはレンダリングされません)。透視投影のように小さくなります)。
public class CascadeShadow {
...
public static void updateCascadeShadows(List<CascadeShadow> cascadeShadows, Scene scene) {
...
// Calculate orthographic projection matrix for each cascade
float lastSplitDist = 0.0f;
for (int i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
float splitDist = cascadeSplits[i];
Vector3f[] frustumCorners = new Vector3f[]{
new Vector3f(-1.0f, 1.0f, -1.0f),
new Vector3f(1.0f, 1.0f, -1.0f),
new Vector3f(1.0f, -1.0f, -1.0f),
new Vector3f(-1.0f, -1.0f, -1.0f),
new Vector3f(-1.0f, 1.0f, 1.0f),
new Vector3f(1.0f, 1.0f, 1.0f),
new Vector3f(1.0f, -1.0f, 1.0f),
new Vector3f(-1.0f, -1.0f, 1.0f),
};
// Project frustum corners into world space
Matrix4f invCam = (new Matrix4f(projMatrix).mul(viewMatrix)).invert();
for (int j = 0; j < 8; j++) {
Vector4f invCorner = new Vector4f(frustumCorners[j], 1.0f).mul(invCam);
frustumCorners[j] = new Vector3f(invCorner.x / invCorner.w, invCorner.y / invCorner.w, invCorner.z / invCorner.w);
}
...
}
...
}
...
}
この時点で、frustumCorners変数は可視空間を含む立方体の座標を持っていますが、この特定のカスケード分割にはワールド座標が必要です。したがって、次のステップは、それらの方法の最初に計算されたカスケード距離を機能させることです。事前に計算された距離に従って、この特定の分割の近平面と遠平面の座標を調整します。
public class CascadeShadow {
...
public static void updateCascadeShadows(List<CascadeShadow> cascadeShadows, Scene scene) {
...
for (int i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
...
for (int j = 0; j < 4; j++) {
Vector3f dist = new Vector3f(frustumCorners[j + 4]).sub(frustumCorners[j]);
frustumCorners[j + 4] = new Vector3f(frustumCorners[j]).add(new Vector3f(dist).mul(splitDist));
frustumCorners[j] = new Vector3f(frustumCorners[j]).add(new Vector3f(dist).mul(lastSplitDist));
}
...
}
...
}
...
}
その後、その分割の中心の座標 (引き続きワールド座標で機能します) と、その分割の半径を計算します。
public class CascadeShadow {
...
public static void updateCascadeShadows(List<CascadeShadow> cascadeShadows, Scene scene) {
...
for (int i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
...
// Get frustum center
Vector3f frustumCenter = new Vector3f(0.0f);
for (int j = 0; j < 8; j++) {
frustumCenter.add(frustumCorners[j]);
}
frustumCenter.div(8.0f);
float radius = 0.0f;
for (int j = 0; j < 8; j++) {
float distance = (new Vector3f(frustumCorners[j]).sub(frustumCenter)).length();
radius = java.lang.Math.max(radius, distance);
}
radius = (float) java.lang.Math.ceil(radius * 16.0f) / 16.0f;
...
}
...
}
...
}
その情報を使用して、ライトの視点と正射投影マトリックス、および分割距離 (カメラ ビュー座標) からビュー マトリックスを計算できるようになりました。
public class CascadeShadow {
...
public static void updateCascadeShadows(List<CascadeShadow> cascadeShadows, Scene scene) {
...
for (int i = 0; i < SHADOW_MAP_CASCADE_COUNT; i++) {
...
Vector3f maxExtents = new Vector3f(radius);
Vector3f minExtents = new Vector3f(maxExtents).mul(-1);
Vector3f lightDir = (new Vector3f(lightPos.x, lightPos.y, lightPos.z).mul(-1)).normalize();
Vector3f eye = new Vector3f(frustumCenter).sub(new Vector3f(lightDir).mul(-minExtents.z));
Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f);
Matrix4f lightViewMatrix = new Matrix4f().lookAt(eye, frustumCenter, up);
Matrix4f lightOrthoMatrix = new Matrix4f().ortho
(minExtents.x, maxExtents.x, minExtents.y, maxExtents.y, 0.0f, maxExtents.z - minExtents.z, true);
// Store split distance and matrix in cascade
CascadeShadow cascadeShadow = cascadeShadows.get(i);
cascadeShadow.splitDistance = (nearClip + splitDist * clipRange) * -1.0f;
cascadeShadow.projViewMatrix = lightOrthoMatrix.mul(lightViewMatrix);
lastSplitDist = cascadeSplits[i];
}
...
}
...
}
これで、シャドウ マップのレンダリングに必要な行列を計算するコードが完成しました。したがって、そのレンダリングを実行するために必要なクラスのコーディングを開始できます。この場合、別の画像 (深度画像) にレンダリングします。カスケード マップの分割ごとに 1 つのテクスチャが必要です。それを管理するために、一連のテクスチャを作成するという名前の新しいクラスArrTextureを作成します。これは次のように定義されます。
package org.lwjglb.engine.graph;
import java.nio.ByteBuffer;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL12.GL_CLAMP_TO_EDGE;
import static org.lwjgl.opengl.GL14.GL_TEXTURE_COMPARE_MODE;
public class ArrTexture {
private final int[] ids;
public ArrTexture(int numTextures, int width, int height, int pixelFormat) {
ids = new int[numTextures];
glGenTextures(ids);
for (int i = 0; i < numTextures; i++) {
glBindTexture(GL_TEXTURE_2D, ids[i]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, width, height, 0, pixelFormat, GL_FLOAT, (ByteBuffer) null);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
}
public void cleanup() {
for (int id : ids) {
glDeleteTextures(id);
}
}
public int[] getIds() {
return ids;
}
}
GL_CLAMP_TO_EDGEを超えた場合にテクスチャを繰り返さないため、テクスチャ ラッピング モードを に設定します。[0, 1]範囲
空のテクスチャを作成できるようになったので、それにシーンをレンダリングできるようにする必要があります。そのためには、フレーム バッファ オブジェクト (または FBO) を使用する必要があります。フレーム バッファは、レンダリングの宛先として使用できるバッファのコレクションです。画面にレンダリングしているときは、OpenGL のデフォルト バッファを使用しています。OpenGL では、FBO を使用してユーザー定義のバッファーにレンダリングできます。という名前の新しいクラスを作成することにより、シャドウ マッピング用の FBO を作成するプロセスの残りのコードを分離しますShadowBuffer。これがそのクラスの定義です。
package org.lwjglb.engine.graph;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL13.glActiveTexture;
import static org.lwjgl.opengl.GL30.*;
public class ShadowBuffer {
public static final int SHADOW_MAP_WIDTH = 4096;
public static final int SHADOW_MAP_HEIGHT = SHADOW_MAP_WIDTH;
private final ArrTexture depthMap;
private final int depthMapFBO;
public ShadowBuffer() {
// Create a FBO to render the depth map
depthMapFBO = glGenFramebuffers();
// Create the depth map textures
depthMap = new ArrTexture(CascadeShadow.SHADOW_MAP_CASCADE_COUNT, SHADOW_MAP_WIDTH, SHADOW_MAP_HEIGHT, GL_DEPTH_COMPONENT);
// Attach the the depth map texture to the FBO
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap.getIds()[0], 0);
// Set only depth
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
throw new RuntimeException("Could not create FrameBuffer");
}
// Unbind
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
public void bindTextures(int start) {
for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
glActiveTexture(start + i);
glBindTexture(GL_TEXTURE_2D, depthMap.getIds()[i]);
}
}
public void cleanup() {
glDeleteFramebuffers(depthMapFBO);
depthMap.cleanup();
}
public int getDepthMapFBO() {
return depthMapFBO;
}
public ArrTexture getDepthMapTexture() {
return depthMap;
}
}
このShadowBufferクラスは、深度マップを保持するテクスチャのサイズを決定する 2 つの定数を定義します。また、FBO 用とテクスチャ用の 2 つの属性も定義します。コンストラクターで、新しい FBO とテクスチャーの配列を作成します。その配列の各要素は、各カスケード シャドウ スプリットのシャドウ マップをレンダリングするために使用されます。FBO では、GL_DEPTH_COMPONENT深度値の保存のみに関心があるため、ピクセル形式として定数を使用します。次に、FBO をテクスチャ インスタンスにアタッチします。
行は、FBO が色をレンダリングしないように明示的に設定します。FBO にはカラー バッファが必要ですが、必要ありません。これが、カラー バッファを として使用するように設定した理由GL_NONEです。
で、シャドウ マップをレンダリングするために、以前のすべてのクラスを機能させることができます。ShadowRender次のように始まる名前の新しいクラスでこれを行います。
package org.lwjglb.engine.graph;
import org.lwjglb.engine.scene.*;
import java.util.*;
import static org.lwjgl.opengl.GL30.*;
public class ShadowRender {
private ArrayList<CascadeShadow> cascadeShadows;
private ShaderProgram shaderProgram;
private ShadowBuffer shadowBuffer;
private UniformsMap uniformsMap;
public ShadowRender() {
List<ShaderProgram.ShaderModuleData> shaderModuleDataList = new ArrayList<>();
shaderModuleDataList.add(new ShaderProgram.ShaderModuleData("resources/shaders/shadow.vert", GL_VERTEX_SHADER));
shaderProgram = new ShaderProgram(shaderModuleDataList);
shadowBuffer = new ShadowBuffer();
cascadeShadows = new ArrayList<>();
for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
CascadeShadow cascadeShadow = new CascadeShadow();
cascadeShadows.add(cascadeShadow);
}
createUniforms();
}
public void cleanup() {
shaderProgram.cleanup();
shadowBuffer.cleanup();
}
private void createUniforms() {
uniformsMap = new UniformsMap(shaderProgram.getProgramId());
uniformsMap.createUniform("modelMatrix");
uniformsMap.createUniform("projViewMatrix");
uniformsMap.createUniform("bonesMatrices");
}
public List<CascadeShadow> getCascadeShadows() {
return cascadeShadows;
}
public ShadowBuffer getShadowBuffer() {
return shadowBuffer;
}
...
}
ご覧のとおり、他のレンダリング クラスと非常によく似ています。シェーダー プログラム、必要なユニフォームを作成し、cleanupメソッドを提供します。唯一の例外は次のとおりです。
- 深さの値に関心があるだけなので、フラグメントシェーダーはまったく必要ありません。頂点シェーダーからの深さを含む頂点位置をダンプするだけです-
- CascadeShadowカスケード シャドウ スプリット (クラス インスタンスのインスタンスによってモデル化) を作成します。それに加えて、カスケード シャドウ マップとシャドウ マップをレンダリングするバッファを取得するためのゲッターをいくつか提供します。これらのゲッターは、SceneRenderシャドウ マップ データにアクセスするためにクラスで使用されます。
クラスのrenderメソッドShadowRenderは次のように定義されます。
public class ShadowRender {
...
public void render(Scene scene) {
CascadeShadow.updateCascadeShadows(cascadeShadows, scene);
glBindFramebuffer(GL_FRAMEBUFFER, shadowBuffer.getDepthMapFBO());
glViewport(0, 0, ShadowBuffer.SHADOW_MAP_WIDTH, ShadowBuffer.SHADOW_MAP_HEIGHT);
shaderProgram.bind();
Collection<Model> models = scene.getModelMap().values();
for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowBuffer.getDepthMapTexture().getIds()[i], 0);
glClear(GL_DEPTH_BUFFER_BIT);
CascadeShadow shadowCascade = cascadeShadows.get(i);
uniformsMap.setUniform("projViewMatrix", shadowCascade.getProjViewMatrix());
for (Model model : models) {
List<Entity> entities = model.getEntitiesList();
for (Material material : model.getMaterialList()) {
for (Mesh mesh : material.getMeshList()) {
glBindVertexArray(mesh.getVaoId());
for (Entity entity : entities) {
uniformsMap.setUniform("modelMatrix", entity.getModelMatrix());
AnimationData animationData = entity.getAnimationData();
if (animationData == null) {
uniformsMap.setUniform("bonesMatrices", AnimationData.DEFAULT_BONES_MATRICES);
} else {
uniformsMap.setUniform("bonesMatrices", animationData.getCurrentFrame().boneMatrices());
}
glDrawElements(GL_TRIANGLES, mesh.getNumVertices(), GL_UNSIGNED_INT, 0);
}
}
}
}
}
shaderProgram.unbind();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
}
最初に行うことは、カスケード マップを更新することです。これは、カスケード マップを更新することです。これは、シャドウ マップをレンダリングできるように、各カスケード スプリットの射影行列です (シーンを更新したり、カメラを移動したり、プレーヤーやアニメーションを変更したりできます)。これは、キャッシュして、シーンが変更された場合に再計算したい場合があります。簡単にするために、フレームごとに行います。その後、glBindFramebuffer関数を呼び出してシャドウ マップをレンダリングするフレーム バッファーをバインドします。それをクリアし、さまざまなカスケード シャドウ スプリットを反復処理します。
分割ごとに、次のアクションを実行します。
- を呼び出してカスケード シャドウ スプリットに関連付けられたテクスチャをバインドし、glFramebufferTexture2Dそれをクリアします。
- 現在のカスケード シャドウ スプリットに従って射影行列を更新します。
- クラスで行っていたように、各エンティティをレンダリングしますSceneRender。
shadow.vert次のように定義された新しい頂点シェーダー ( ) が必要です。
#version 330
const int MAX_WEIGHTS = 4;
const int MAX_BONES = 150;
layout (location=0) in vec3 position;
layout (location=1) in vec3 normal;
layout (location=2) in vec3 tangent;
layout (location=3) in vec3 bitangent;
layout (location=4) in vec2 texCoord;
layout (location=5) in vec4 boneWeights;
layout (location=6) in ivec4 boneIndices;
uniform mat4 modelMatrix;
uniform mat4 projViewMatrix;
uniform mat4 bonesMatrices[MAX_BONES];
void main()
{
vec4 initPos = vec4(0, 0, 0, 0);
int count = 0;
for (int i = 0; i < MAX_WEIGHTS; i++) {
float weight = boneWeights[i];
if (weight > 0) {
count++;
int boneIndex = boneIndices[i];
vec4 tmpPos = bonesMatrices[boneIndex] * vec4(position, 1.0);
initPos += weight * tmpPos;
}
}
if (count == 0) {
initPos = vec4(position, 1.0);
}
gl_Position = projViewMatrix * modelMatrix * initPos;
}
設定できるように、シーンの頂点シェーダーと同じ入力属性のセットを受け取ります。位置を投影するだけで、モデル マトリックスとアニメーション データに従って以前に入力された位置を更新します。
SceneRender次に、レンダリング時にカスケード シャドウ マップを使用してシャドウを適切に表示するようにクラスを更新する必要があります。まず、フラグメント シェーダーでテクスチャとしてシャドウ マップにアクセスするため、それらのユニフォームを作成する必要があります。また、頂点の位置に応じてどの分割を使用するかを選択するために、カスケード分割の射影行列と分割距離を渡す必要があります。
public class SceneRender {
...
private void createUniforms() {
...
for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
uniformsMap.createUniform("shadowMap_" + i);
uniformsMap.createUniform("cascadeshadows[" + i + "]" + ".projViewMatrix");
uniformsMap.createUniform("cascadeshadows[" + i + "]" + ".splitDistance");
}
}
...
}
クラスのrenderメソッドではSceneRender、モデルを調整する前にこれらのユニフォームを設定する必要があります。
public class SceneRender {
...
public void render(Scene scene, ShadowRender shadowRender) {
...
uniformsMap.setUniform("txtSampler", 0);
uniformsMap.setUniform("normalSampler", 1);
int start = 2;
List<CascadeShadow> cascadeShadows = shadowRender.getCascadeShadows();
for (int i = 0; i < CascadeShadow.SHADOW_MAP_CASCADE_COUNT; i++) {
uniformsMap.setUniform("shadowMap_" + i, start + i);
CascadeShadow cascadeShadow = cascadeShadows.get(i);
uniformsMap.setUniform("cascadeshadows[" + i + "]" + ".projViewMatrix", cascadeShadow.getProjViewMatrix());
uniformsMap.setUniform("cascadeshadows[" + i + "]" + ".splitDistance", cascadeShadow.getSplitDistance());
}
shadowRender.getShadowBuffer().bindTextures(GL_TEXTURE2);
...
}
...
}
それでは、シーン シェーダーの変更を見てみましょう。頂点シェーダー ( scene.vert) では、モデル座標の頂点位置も fargemnet シェーダーに渡す必要があります (ビュー マトリックスの影響を受けません)。
#version 330
...
out vec3 outNormal;
out vec3 outTangent;
out vec3 outBitangent;
out vec2 outTextCoord;
out vec3 outViewPosition;
out vec4 outWorldPosition;
...
void main()
{
...
outViewPosition = mvPosition.xyz;
outWorldPosition = modelMatrix * initPos;
...
}
ほとんどの変更はフラグメント シェーダーに適用されます ( scene.frag)。
#version 330
...
const int DEBUG_SHADOWS = 0;
...
const float BIAS = 0.0005;
const float SHADOW_FACTOR = 0.25;
...
in vec3 outViewPosition;
in vec4 outWorldPosition;
最初に一連の定数を定義します。
- DEBUG_SHADOWS: これは、割り当てられるカスケード スプリットを識別するためにフラグメントに色を適用するかどうかを制御します (これ1を有効にするには値が必要です)。
- SHADOW_FACTOR: 影にあるときにフラグメントに適用される塗りつぶしの暗化係数。
- BIAS: フラグメントが影の影響を受けているかどうかを推定するときに適用する深度バイアス。これは、シャドウ アクネなどのシャドウ アーティファクトを削減するために使用されます。TS シャドウ アクネは、奇妙なアーティファクトを生成する深度マップを格納するテクスチャの解像度が制限されているために生成されます。精度の問題を軽減するしきい値を設定することで、この問題を解決します。
その後、カスケード スプリットとシャドウ マップのテクスチャを格納する新しいユニフォームを定義します。また、シェーダーに逆ビュー マトリックスを渡す必要があります。前の章では、射影行列の逆行列を使用して、ビュー座標でのフラグメントの位置を取得しました。この場合、一歩先に進み、ワールド座標でもフラグメントの位置を取得する必要があります。逆ビュー行列にビュー座標のフラグメントの位置を掛けると、ワールド座標が得られます。それに加えて、カスケード スプリットの射影ビュー マトリックスとそれらのスプリット距離が必要です。
...
struct CascadeShadow {
mat4 projViewMatrix;
float splitDistance;
};
...
uniform CascadeShadow cascadeshadows[NUM_CASCADES];
uniform sampler2D shadowMap_0;
uniform sampler2D shadowMap_1;
uniform sampler2D shadowMap_2;
...
という名前の新しい関数を作成します。この関数はcalcShadow、ワールド位置とカスケード分割インデックスを指定すると、最終的なフラグメント カラーに適用されるシャドウ ファクターを返します。フラグメントが影の影響を受けていない場合、結果は になり1、最終的な色には影響しません。
...
float calcShadow(vec4 worldPosition, int idx) {
vec4 shadowMapPosition = cascadeshadows[idx].projViewMatrix * worldPosition;
float shadow = 1.0;
vec4 shadowCoord = (shadowMapPosition / shadowMapPosition.w) * 0.5 + 0.5;
shadow = textureProj(shadowCoord, vec2(0, 0), idx);
return shadow;
}
...
この関数は、正投影を使用して、特定のカスケード スプリットのために、ワールド座標空間からディレクショナル ライトの NDC 空間に変換します。つまり、ワールド空間に、指定されたカスケード スプリットの射影ビュー マトリックスを乗算します。その後、これらの座標をテクスチャ座標 (左上隅から [0, 1] の範囲) に変換する必要があります。その情報を使用textureProjして、使用する適切なシャドウ マップ テクスチャを選択する関数を使用し、結果の値に応じてシャドウ ファクターを適用します。
...
float textureProj(vec4 shadowCoord, vec2 offset, int idx) {
float shadow = 1.0;
if (shadowCoord.z > -1.0 && shadowCoord.z < 1.0) {
float dist = 0.0;
if (idx == 0) {
dist = texture(shadowMap_0, vec2(shadowCoord.xy + offset)).r;
} else if (idx == 1) {
dist = texture(shadowMap_1, vec2(shadowCoord.xy + offset)).r;
} else {
dist = texture(shadowMap_2, vec2(shadowCoord.xy + offset)).r;
}
if (shadowCoord.w > 0 && dist < shadowCoord.z - BIAS) {
shadow = SHADOW_FACTOR;
}
}
return shadow;
}
...
このmain関数では、入力としてビュー位置を取り、カスケード分割ごとに計算された分割距離を反復処理して、このフラグメントが属するカスケード インデックスを決定し、シャドウ ファクターを計算します。
...
void main() {
...
...
vec4 diffuseSpecularComp = calcDirLight(diffuse, specular, dirLight, outViewPosition, normal);
int cascadeIndex;
for (int i=0; i<NUM_CASCADES - 1; i++) {
if (outViewPosition.z < cascadeshadows[i].splitDistance) {
cascadeIndex = i + 1;
break;
}
}
float shadowFactor = calcShadow(outWorldPosition, cascadeIndex);
for (int i=0; i<MAX_POINT_LIGHTS; i++) {
if (pointLights[i].intensity > 0) {
diffuseSpecularComp += calcPointLight(diffuse, specular, pointLights[i], outViewPosition, normal);
}
}
for (int i=0; i<MAX_SPOT_LIGHTS; i++) {
if (spotLights[i].pl.intensity > 0) {
diffuseSpecularComp += calcSpotLight(diffuse, specular, spotLights[i], outViewPosition, normal);
}
}
fragColor = ambient + diffuseSpecularComp;
fragColor.rgb = fragColor.rgb * shadowFactor;
if (fog.activeFog == 1) {
fragColor = calcFog(outViewPosition, fragColor, fog, ambientLight.color, dirLight);
}
if (DEBUG_SHADOWS == 1) {
switch (cascadeIndex) {
case 0:
fragColor.rgb *= vec3(1.0f, 0.25f, 0.25f);
break;
case 1:
fragColor.rgb *= vec3(0.25f, 1.0f, 0.25f);
break;
case 2:
fragColor.rgb *= vec3(0.25f, 0.25f, 1.0f);
break;
default :
fragColor.rgb *= vec3(1.0f, 1.0f, 0.25f);
break;
}
}
}
最終的なフラグメントの色は、シャドウ ファクターによって調整されます。最後に、デバッグ モードが有効になっている場合は、そのフラグメントに色を適用して、使用しているカスケードを識別します。
最後に、Renderクラスをインスタンス化して使用するようにクラスを更新する必要がありますShadowRender。また、ブレンディング アクティベーション コードをこのクラスに移動します。
public class Render {
...
private ShadowRender shadowRender;
...
public Render(Window window) {
...
// Support for transparencies
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
...
shadowRender = new ShadowRender();
}
public void cleanup() {
...
shadowRender.cleanup();
}
public void render(Window window, Scene scene) {
shadowRender.render(scene);
...
sceneRender.render(scene, shadowRender);
...
}
...
}
クラスではMain、サウンド コードを削除するだけです。最後に、次のようなものが表示されます。
DEBUG_SHADOWS定数を に設定する1と、カスケード シャドウがどのように分割されるかがわかります