LoginSignup
2
2

More than 3 years have passed since last update.

社内勉強会「レンダリング合宿1」 ソフトウェアラスタライザー基本実装編 06.テクスチャマッピング

Last updated at Posted at 2019-11-06

ソフトウェアラスタライザー基本実装編 06.テクスチャマッピング

前回まででポリゴンをそれなりに正確に画面に描画するといったところまでは終わりました。
今回からはテクスチャマッピングに関して扱っていきます。

リポジトリ

今回のビルド結果です。
テクスチャが貼られています。
top.png

テクスチャの準備

なにはともあれテクスチャを読み込む必要があります。
今回からTexture.hとTexture.cppが追加されています。
このファイルでテクスチャを扱います。

ミップマップ付きのRGBA32bitのフォーマットのDDSファイルの読み込みができます。
他のフォーマットは読み込みできません。

実際に読み込みをしている箇所が以下になります。

Application.cpp
MeshFileBinary MeshBin;
::ReadFile(hFile, &MeshBin, sizeof(MeshBin), &ReadedBytes, nullptr);

// テクスチャの読み込み
Dst._Texture.Load((Dir + MeshBin.TextureName + ".dds").c_str());

// ジオメトリデータ読み込み
std::vector<VertexData> VertexDatas(MeshBin.TriangleVertexCount);
::ReadFile(hFile, &(VertexDatas[0]), sizeof(VertexData) * MeshBin.TriangleVertexCount, &ReadedBytes, nullptr);

メッシュのデータもテクスチャのUVを読み込むようになっています。

Renderer.h
struct IMeshData
{
    virtual const Texture* GetTexture() const = 0;

    virtual const int32 GetVertexCount() const = 0;
    virtual const int32 GetIndexCount() const = 0;

    virtual const Vector3* const GetPosition() const = 0;
    virtual const Vector3* const GetNormal() const = 0;
    virtual const Vector2* const GetTexCoord() const = 0;
    virtual const uint16* const GetIndex() const = 0;
};

テクスチャを貼る

データの準備はできたので実際にテクスチャマッピングをする個所を確認していきます。
ラスタライズの処理は前回と変わっていませんのでこの場所にテクスチャの処理を追加していきます。

Renderer.cpp
void Renderer::RasterizeTriangle(uint16 TextureId, InternalVertex v0, InternalVertex v1, InternalVertex v2)
{
    // 三角形の各位置
    auto& p0 = v0.Position;
    auto& p1 = v1.Position;
    auto& p2 = v2.Position;

    // 三角形の各法線
    auto& n0 = v0.Normal;
    auto& n1 = v1.Normal;
    auto& n2 = v2.Normal;

    // 三角形の各UV
    auto& t0 = v0.TexCoord;
    auto& t1 = v1.TexCoord;
    auto& t2 = v2.TexCoord;

    // 外積から面の向きを求めて、裏向きなら破棄する(backface-culling)
    const auto Denom = EdgeFunc(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y);
    if (Denom <= 0.0f) return;

    const auto InvDenom = 1.0f / Denom;
    p0.z /= p0.w;
    p1.z /= p1.w;
    p2.z /= p2.w;

    // ポリゴンのバウンディングを求める
    auto bbMinX = p0.x, bbMinY = p0.y, bbMaxX = p0.x, bbMaxY = p0.y;
    if (bbMaxX < p1.x) bbMaxX = p1.x;
    if (bbMaxX < p2.x) bbMaxX = p2.x;
    if (bbMaxY < p1.y) bbMaxY = p1.y;
    if (bbMaxY < p2.y) bbMaxY = p2.y;
    if (bbMinX > p1.x) bbMinX = p1.x;
    if (bbMinX > p2.x) bbMinX = p2.x;
    if (bbMinY > p1.y) bbMinY = p1.y;
    if (bbMinY > p2.y) bbMinY = p2.y;

    const auto x0 = int16(bbMinX);
    const auto x1 = int16(bbMaxX);
    const auto y0 = int16(bbMinY);
    const auto y1 = int16(bbMaxY);

    auto pDepthBuffer = _pDepthBuffer->GetPixelPointer(x0, y0);
    auto pColorBuffer = _pColorBuffer->GetPixelPointer(x0, y0);

    // 求めたバウンディング内をforで回す
    fp32 py = fp32(y0) + 0.5f;
    for (int32 y = y0; y <= y1; ++y, py += 1.0f)
    {
        int32 x_offset = 0;
        fp32 px = fp32(x0) + 0.5f;
        for (auto x = x0; x <= x1; ++x, px += 1.0f, ++x_offset)
        {
            // 辺1-2に対して内外判定
            auto b0 = EdgeFunc(p1.x, p1.y, p2.x, p2.y, px, py);
            if (b0 < 0.0f) continue;
            // 辺2-0に対して内外判定
            auto b1 = EdgeFunc(p2.x, p2.y, p0.x, p0.y, px, py);
            if (b1 < 0.0f) continue;
            // 辺0-1に対して内外判定
            auto b2 = EdgeFunc(p0.x, p0.y, p1.x, p1.y, px, py);
            if (b2 < 0.0f) continue;

            b0 *= InvDenom;
            b1 *= InvDenom;
            b2 *= InvDenom;

            // 重心座標系でz値を求める
            const auto Depth = (b0 * p0.z) + (b1 * p1.z) + (b2 * p2.z);
            auto& DepthBuf = pDepthBuffer[x_offset];
            if (Depth >= DepthBuf) continue;
            DepthBuf = Depth;

            // 重心座標系で法線を求める
            Vector3 Normal = {
                ((b0 * n0.x) + (b1 * n1.x) + (b2 * n2.x)),
                ((b0 * n0.y) + (b1 * n1.y) + (b2 * n2.y)),
                ((b0 * n0.z) + (b1 * n1.z) + (b2 * n2.z)),
            };

            // 適当な感じでライティングする
            Vector_Normalize(Normal, Normal);
            const auto NdotL = Vector_DotProduct(Normal, _DirectionalLight) * 0.25f + 0.75f;

            const auto Brightness = uint32(NdotL * 128.0f);

            // 重心座標系でUVを求める
            Vector2 TexCoord = {
                ((b0 * t0.x) + (b1 * t1.x) + (b2 * t2.x)),
                ((b0 * t0.y) + (b1 * t1.y) + (b2 * t2.y)),
            };

            // 求めたUVからテクスチャの色をとってくる
            auto pTexture = _Textures[TextureId];
            Color texel = pTexture->Sample(TexCoord.x, TexCoord.y);

            // テクスチャの色にライtの結果を乗算
            auto& ColorBuff = pColorBuffer[x_offset];
            ColorBuff.r = (texel.r * Brightness) >> 7;
            ColorBuff.g = (texel.g * Brightness) >> 7;
            ColorBuff.b = (texel.b * Brightness) >> 7;
        }

        pDepthBuffer += SCREEN_WIDTH;
        pColorBuffer += SCREEN_WIDTH;
    }
}

テクスチャの処理をしている箇所は以下の部分です。

Renderer.cpp
// 重心座標系でUVを求める
Vector2 TexCoord = {
    ((b0 * t0.x) + (b1 * t1.x) + (b2 * t2.x)),
    ((b0 * t0.y) + (b1 * t1.y) + (b2 * t2.y)),
};

// 求めたUVからテクスチャの色をとってくる
auto pTexture = _Textures[TextureId];
Color texel = pTexture->Sample(TexCoord.x, TexCoord.y);

// テクスチャの色にライtの結果を乗算
auto& ColorBuff = pColorBuffer[x_offset];
ColorBuff.r = (texel.r * Brightness) >> 7;
ColorBuff.g = (texel.g * Brightness) >> 7;
ColorBuff.b = (texel.b * Brightness) >> 7;

深度や法線と同様に重心座標系でピクセルのUVを求めて、そのUVに該当するテクセルをテクスチャから取得しています。
あとは取得した色とライティングの結果を合わせてカラーバッファに出力しています。

テクスチャのサンプリング

UV値からテクスチャの色をサンプリングしてくる部分を確認してみます。

Texture.cpp
Color Texture::Sample(fp32 u, fp32 v) const
{
    u += 256.0f;
    v += 256.0f;

    const auto& Src = _Surface[0];

    const auto uf = u * fp32(Src.Width);
    const auto vf = v * fp32(Src.Height);
    const auto ui = int32(uf);
    const auto vi = int32(vf);

    const auto mui = ui % Src.Width;
    const auto mvi = vi % Src.Height;

    return Src.Color[mui + (mvi * Src.Width)];
}

まず最初にUVそれぞれに +256 していますが、これはマイナスの場合におかしくなるのを回避するためにこのようにしています。
まぁ -256 以下のような数値が入ってることはまずないだろうという雑な前提で今回は組んでいます。

正確に処理するなら以下のような感じでしょうか。

u -= floorf(u);
v -= floorf(v);

UV値は1.0が画像サイズとなりますので、UVそれぞれに画像のサイズをかけ実際のピクセル座標を求めます。
この場合に1.0以上の場合は画像からはみ出してしまいますのでサイズで%で余剰を求めています。
この際にマイナスだと都合が悪くなるので事前にマイナスを消しています。

あとは求めた座標の画像の色を返して終わります。

ここでは余剰でリピート処理をしていますが、端でクランプする場合や反転してミラーにするなどもあります。
処理的に一番軽いのがリピートなので今回はリピートのみ対応します。

最後に

今回はテクスチャマッピングを行いました。
実行してカメラを動かしてみると直ぐにわかるかと思いますがテクスチャが歪んでいます。
これはテクスチャのUVを2D的な画像変換処理をしているのが原因です。

こういった見た目は初代PlayStationやWindows98とかに入ってたスクリーンセーバーでよく見かけた気がします。

昔はこういった歪みを軽減するためにポリゴンの割り方で工夫をしたりカメラアングルを気を付けたりといった事をしていました。

この問題の解決方法は奥行きを考慮した3D的な画像変換処理を行う事です。
次回はこの辺に関してやっていきたいと思います。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2