Boost Preprocessorでコンストラクタ生成

今回は、下記のように記述すると、その下のコードのように展開してくれるマクロを、Boost Preprocessorを利用して作成してみたいと思います。(実際には改行は生成されません。)

struct Person {
    std::string FirstName;
    std::string LastName;
    int Age;

    CONSTRUCTOR(Person, FirstName, LastName, Age)
};
struct Person {
    std::string FirstName;
    std::string LastName;
    int Age;

    Person(decltype(FirstName) a_FirstName, decltype(LastName) a_LastName, decltype(Age) a_Age) 
        : FirstName(std::move(a_FirstName)), LastName(std::move(a_LastName)), Age(std::move(a_Age)) 
    {}
};


マクロ定義

Variadic引数をSEQに変換し、各要素をBOOST_PP_SEQ_TRANSFORMで変換したのちBOOST_PP_SEQ_ENUMで出力しています。(BOOST_PP_SEQ_ENUMはカンマ区切りで出力されます。)

通常のマクロでは結合はa_##memberのように記述しますが、Boost Preprocessorを利用している場合はBOOST_PP_CATを利用します。(同様に#memberはBOOST_PP_STRINGIZE(member)と記述します。)

#include <boost\preprocessor.hpp>

#define _CONSTRUCTOR_PARAM(s, data, member) \
    decltype(member) BOOST_PP_CAT(a_, member)

#define _CONSTRUCTOR_PARAMS(members) \
    BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(_CONSTRUCTOR_PARAM, , members))

#define _CONSTRUCTOR_INIT(s, data, member) \
    member(std::move(BOOST_PP_CAT(a_, member)))

#define _CONSTRUCTOR_INITS(members) \
    BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(_CONSTRUCTOR_INIT, , members))

#define _CONSTRUCTOR(cls, members) \
    cls(_CONSTRUCTOR_PARAMS(members)) : _CONSTRUCTOR_INITS(members) {}

#define CONSTRUCTOR(cls, ...) \
    _CONSTRUCTOR(cls, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__))


展開結果の確認

cppファイルのプロパティで、Process to a Fileを有効化してファイルをコンパイルすると、出力フォルダに .i ファイルが生成されます。(.objファイルが出力されなくなるため、ビルド時には無効化してください。)

f:id:any-programming:20180116153641p:plain





C++ AMPで画像処理

今回はC++ AMPを利用して画像処理を行ってみたいと思います。

下図のように、赤と青を入れ替える処理を実装していきます。

f:id:any-programming:20170319005947j:plain f:id:any-programming:20170319010003j:plain


アプリケーションコード

concurrency::arrayを利用するとint配列しか受け付けてくれないため、今回はconcurrency::graphics::textureを利用しています。

BMPの読み書きは下記記事のものを利用しています。
Bitmap読み書き - 何でもプログラミング

#include <amp.h>
#include <amp_graphics.h>

int main()
{
    int width, height;
    std::vector<byte> srcPixels;
    LoadBitmap24("Parrots.bmp", &width, &height, &srcPixels);

    // bits_per_scalar_elementに8Uを指定した場合、texture_view<const int>経由でアクセスしないとエラーになります。
    concurrency::graphics::texture<int, 2> srcTexture(height, 3 * width, srcPixels.data(), srcPixels.size(), 8U);
    concurrency::graphics::texture_view<const int, 2> srcView(srcTexture);

    concurrency::graphics::texture<int, 2> dstTexture(height, 3 * width, 8U);
    
    concurrency::extent<2> extent(height, width);
    concurrency::parallel_for_each(extent, [&, srcView](concurrency::index<2> idx) restrict(amp) {
        concurrency::index<2> idx1(idx[0], 3 * idx[1]);
        concurrency::index<2> idx2(idx[0], 3 * idx[1] + 1);
        concurrency::index<2> idx3(idx[0], 3 * idx[1] + 2);
        dstTexture.set(idx1, srcView[idx3]);
        dstTexture.set(idx2, srcView[idx2]);
        dstTexture.set(idx3, srcView[idx1]);
    });

    std::vector<byte> dstPixels(width * height * 3);
    concurrency::graphics::copy(dstTexture, dstPixels.data(), dstPixels.size());

    SaveBitmap24("Parrots2.bmp", width, height, dstPixels.data());

    return 0;
}






OpenCLで画像処理

今回はOpenCLを用いて画像処理を行ってみたいと思います。

下図のように、赤と青を入れ替える処理を実装していきます。

f:id:any-programming:20170319005947j:plain f:id:any-programming:20170319010003j:plain


アプリケーションコード

clCreateImageでもいいですが、今回はclCreateBufferで実装してみました。

BMPの読み書きは下記記事のものを利用しています。
Bitmap読み書き - 何でもプログラミング

#include <cl/cl.h>
#define ASSERT_CL(expr) if (expr != CL_SUCCESS) { throw std::exception(#expr); }

int main()
{
    // 画像読み込み
    int width, height;
    std::vector<byte> srcPixels;
    LoadBitmap24("Parrots.bmp", &width, &height, &srcPixels);

    // device, context作成
    cl_platform_id platform;
    ASSERT_CL(clGetPlatformIDs(1, &platform, nullptr));

    cl_device_id device;
    ASSERT_CL(clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, 1, &device, nullptr));

    cl_context context = clCreateContext(nullptr, 1, &device, nullptr, nullptr, nullptr);
    assert(context);

    // kernel作成
    const char* source =
        "__kernel void main(__global const uchar* src, __global uchar *dst) {     \n"
        "   int i = get_global_id(0);                                             \n"
        "   dst[3 * i]     = src[3 * i + 2];                                      \n"
        "   dst[3 * i + 1] = src[3 * i + 1];                                      \n"
        "   dst[3 * i + 2] = src[3 * i];                                          \n"
        "}                                                                        \n";

    cl_program program = clCreateProgramWithSource(context, 1, &source, nullptr, nullptr);
    assert(program);

    ASSERT_CL(clBuildProgram(program, 1, &device, nullptr, nullptr, nullptr));
    char buildLog[1024];
    ASSERT_CL(clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 1024, buildLog, nullptr));
    printf("build %s\n", buildLog);

    cl_kernel kernel = clCreateKernel(program, "main", nullptr);
    assert(kernel);

    // 引数設定
    int size = (int)srcPixels.size();
    cl_mem srcBuffer = clCreateBuffer(context, CL_MEM_READ_ONLY, size, nullptr, nullptr);
    assert(srcBuffer);
    cl_mem dstBuffer = clCreateBuffer(context, CL_MEM_WRITE_ONLY, size, nullptr, nullptr);
    assert(dstBuffer);

    ASSERT_CL(clSetKernelArg(kernel, 0, sizeof(srcBuffer), &srcBuffer));
    ASSERT_CL(clSetKernelArg(kernel, 1, sizeof(dstBuffer), &dstBuffer));

    // Queue作成
    cl_command_queue queue = clCreateCommandQueue(context, device, 0, nullptr);
    assert(queue);

    ASSERT_CL(clEnqueueWriteBuffer(queue, srcBuffer, CL_TRUE, 0, size, srcPixels.data(), 0, nullptr, nullptr));

    size_t global_work_size = size / 3;
    ASSERT_CL(clEnqueueNDRangeKernel(queue, kernel, 1, nullptr, &global_work_size, nullptr, 0, nullptr, nullptr));

    std::vector<byte> dstPixels(size);
    ASSERT_CL(clEnqueueReadBuffer(queue, dstBuffer, CL_TRUE, 0, size, dstPixels.data(), 0, nullptr, nullptr));

    // 実行
    ASSERT_CL(clFlush(queue));
    ASSERT_CL(clFinish(queue));

    // 画像保存
    SaveBitmap24("Parrots2.bmp", width, height, dstPixels.data());

    return 0;
}


SDKなしでの開発

OpenCLSDKは、IntellやNVIDIAなどから提供されています。

ちょっとしたものであれば、ヘッダファイルだけ取得して動的にdllをロードすることで開発できます。

ヘッダは下記よりダウンロードできます。(最低限、cl.h と cl_platform.h で大丈夫です。)
Khronos OpenCL Registry - The Khronos Group Inc

dllのロードは、下記コードのように行っています。

ヘッダに関数郡がすでに定義されているため、一段namespaceで包んで関数をロードしています。

#include <cl/cl.h>
namespace App {
    HMODULE hOpenCL = LoadLibraryA("opencl.dll");

#define GET_PROC(name) decltype(::name)* name = (decltype(::name)*)GetProcAddress(hOpenCL, #name)
    GET_PROC(clGetPlatformIDs);
    GET_PROC(clGetDeviceIDs);
    GET_PROC(clCreateContext);
    ...

    int main()
    {
        ....
    }
}

int main() { return App::main(); }






WebGL2.0でボリュームレンダリング(TypeScript)

下記記事にて、Direct3D11を用いてボリュームレンダリングを実装してみました。
Direct3D11でボリュームレンダリング - 何でもプログラミング

今回はWebGL2.0を利用して同様のものを実装してみたいと思います。

ボリュームレンダリングの手法やデータは上記記事を参照してみてください。

※現時点ではWebGL2.0はChromeFirefoxでしかサポートされていません。

HTML

表示のため、下記のhtmlを用意しました。

canvasWebGLレンタリング用で、file inputはデータを読み込むためのものです。

最終的に下図のような表示になります。
f:id:any-programming:20171226133944p:plain

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>webGL</title>
  </head>
  <body>
    <canvas id="gl" width="200" height="200"></canvas>
    <input id="file" type="file">
    <script src="script.js"></script>
  </body>
</html>


Canvasのサイズ

htmlのcanvas要素のwidthとheightはバックバッファのサイズ、cssで設定するwidthとheightは実際の表示サイズとして利用されます。

WebGL2.d.ts

WebGL2の定義ファイルはデフォルトで含まれていないため、下記のものをダウンロードして利用しました。
GitHub - MaxGraey/WebGL2-TypeScript: WebGL2 bindings for TypeScript

いくつかWebGLRenderingContextの定義とかち合う関数があるので、適宜コメントアウトして利用してください。

main関数

Direct3Dの時と同様に、三方向のスライスセットを準備し適宜切り替えています。

また陰影もボリューム値の勾配を法線として算出しています。

function main() : void {
    // context取得
    const canvas = <HTMLCanvasElement>document.getElementById("gl");
    const gl = canvas.getContext("webgl2")!;

    // Shader作成
    const vertexCode = `#version 300 es
        layout(location = 0) in vec4 position;
        layout(location = 1) in vec3 texcoord;
        uniform mat4 matrix;
        out vec3 v_texcoord;
        void main() {
            gl_Position = matrix * position;
            v_texcoord = texcoord;
        }    
    `;

    const fragmentCode = `#version 300 es
        precision mediump float;
        precision mediump sampler3D;
        in vec3 v_texcoord;
        uniform sampler3D tex;
        out vec4 fragColor;
        void main() {
            if (texture(tex, v_texcoord).r < 0.5)
                discard;
            
            float dx = textureOffset(tex, v_texcoord, ivec3(1, 0, 0)).r - textureOffset(tex, v_texcoord, ivec3(-1,  0,  0)).r;
            float dy = textureOffset(tex, v_texcoord, ivec3(0, 1, 0)).r - textureOffset(tex, v_texcoord, ivec3( 0, -1,  0)).r;
            float dz = textureOffset(tex, v_texcoord, ivec3(0, 0, 1)).r - textureOffset(tex, v_texcoord, ivec3( 0,  0, -1)).r;
            vec3 normal = normalize(vec3(dx, dy, dz));

            vec3 light = normalize(vec3(1, 1, 1));
            float gray = abs(dot(normal, light));
            fragColor = vec4(gray, gray, gray, 1);
        }
    `;

    const program = createProgram(gl, vertexCode, fragmentCode);

    // Texture3D、VertexBuffer、IndexBuffer作成(Fileが読み込まれた際)
    let texture : WebGLTexture | undefined;
    let slices : VolumeSlices | undefined;
    const fileInput = <HTMLInputElement>document.getElementById("file");
    fileInput.onchange = ev =>
    {
        const file = (<HTMLInputElement>ev.target).files![0];
        const reader = new FileReader();
        reader.onload = _ev =>
        {
            const buf = new Uint8Array(<ArrayBuffer>reader.result);
            texture = createTexture3D(gl, 512, 512, 360, buf);
            slices = new VolumeSlices(gl, 512, 512, 360);
        };
        reader.readAsArrayBuffer(file);
    };

    // 固定の設定
    gl.useProgram(program);
    gl.enableVertexAttribArray(0);
    gl.enableVertexAttribArray(1);
    const matrixUniform = gl.getUniformLocation(program, "matrix")!;
    gl.enable(gl.DEPTH_TEST);

    // 描画関数
    function draw(rotX : number, rotY : number) : void {
        gl.clearColor(0.5, 0.6, 1.0, 1);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        if (slices == undefined || texture == undefined)
            return;

        // 行列設定
        const mat = Matrix4.rotate(rotY, new Vector3(0, 1, 0))
            .mul(Matrix4.rotate(rotX, new Vector3(1, 0, 0)))
            .mul(Matrix4.scale(1.5 / 512))
            .mul(Matrix4.translate(new Vector3(-512.0 / 2, -512.0 / 2, -360.0 / 2)));
        gl.uniformMatrix4fv(matrixUniform, false, mat.transpose().data);

        // VertexBuffer、IndexBuffer設定
        const { vertexBuffer, indexBuffer } = slices.getBuffer(mat);
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
        gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0);
        gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

        // 描画
        const indexCount = gl.getBufferParameter(gl.ELEMENT_ARRAY_BUFFER, gl.BUFFER_SIZE) / 2;
        gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_SHORT, 0);
    }

    // MouseMoveで描画
    canvas.onmousemove = ev => {
        const rotX = 2 * Math.PI * ev.y / canvas.height;
        const rotY = 2 * Math.PI * ev.x / canvas.width;
        draw(rotX, rotY);
    };
}


Slices

三方向のスライスセットを作成、適切なものを取得できるクラスになります。

class VolumeSlices {
    private vertexBufferX : WebGLBuffer;
    private indexBufferX  : WebGLBuffer;
    private vertexBufferY : WebGLBuffer;
    private indexBufferY  : WebGLBuffer;
    private vertexBufferZ : WebGLBuffer;
    private indexBufferZ  : WebGLBuffer;
    constructor(gl : WebGLRenderingContext, width : number, height : number, depth : number) {
        const verticesZ = normalizedRange(depth, 0, 1)
            .concatMap(t => [
                0,     0,      t * depth, 0, 0, t,
                width, 0,      t * depth, 1, 0, t,
                0,     height, t * depth, 0, 1, t,
                width, height, t * depth, 1, 1, t,
            ]);

        const verticesY = normalizedRange(height, 0, 1)
            .concatMap(t => [
                0,     t * height, 0,     0, t, 0,
                width, t * height, 0,     1, t, 0,
                0,     t * height, depth, 0, t, 1,
                width, t * height, depth, 1, t, 1,
            ]);

        const verticesX = normalizedRange(width, 0, 1)
            .concatMap(t => [
                t * width, 0,      0,     t, 0, 0,
                t * width, height, 0,     t, 1, 0,
                t * width, 0,      depth, t, 0, 1,
                t * width, height, depth, t, 1, 1,
            ]);

        const createIndices = (count : number) =>
            range(count).concatMap<number>(i => [0, 2, 1, 1, 2, 3].map<number>(x => x + 4 * i));

        this.vertexBufferX = createVertexBuffer(gl, new Float32Array(verticesX));
        this.vertexBufferY = createVertexBuffer(gl, new Float32Array(verticesY));
        this.vertexBufferZ = createVertexBuffer(gl, new Float32Array(verticesZ));
        this.indexBufferX = createIndexBuffer(gl, new Uint16Array(createIndices(width)));
        this.indexBufferY = createIndexBuffer(gl, new Uint16Array(createIndices(height)));
        this.indexBufferZ = createIndexBuffer(gl, new Uint16Array(createIndices(depth)));
    }
    getBuffer(m : Matrix4) : { vertexBuffer : WebGLBuffer, indexBuffer : WebGLBuffer } {
        const dotX = Math.abs(m.transform3x3(Vector3.ex).z);
        const dotY = Math.abs(m.transform3x3(Vector3.ey).z);
        const dotZ = Math.abs(m.transform3x3(Vector3.ez).z);
        if (dotX < dotZ && dotY < dotZ)
            return { vertexBuffer : this.vertexBufferZ, indexBuffer : this.indexBufferZ };
        else if (dotX < dotY)
            return { vertexBuffer : this.vertexBufferY, indexBuffer : this.indexBufferY };
        else
            return { vertexBuffer : this.vertexBufferX, indexBuffer : this.indexBufferX };
    }
}


WebGL関数群

main関数内で利用されているWebGL関連の関数は下記のように実装しています。

function compileShader(gl : WebGLRenderingContext, shader : WebGLShader, code : string) : void {
    gl.shaderSource(shader, code);
    gl.compileShader(shader);

    console.log(gl.getShaderInfoLog(shader));
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
        throw new Error("compile error");
}

function createProgram(gl : WebGLRenderingContext, vertexCode : string, fragmentCode : string) : WebGLProgram {
    const vertexShader = gl.createShader(gl.VERTEX_SHADER)!;
    compileShader(gl, vertexShader, vertexCode);

    const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)!;
    compileShader(gl, fragmentShader, fragmentCode);

    const program = gl.createProgram()!;
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);

    console.log(gl.getProgramInfoLog(program));
    if (!gl.getProgramParameter(program, gl.LINK_STATUS))
        throw new Error("program error");

    return program;
}
function createVertexBuffer(gl : WebGLRenderingContext, vertices : Float32Array) : WebGLBuffer {
    const buffer = gl.createBuffer()!;

    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

    return buffer;
}
function createIndexBuffer(gl : WebGLRenderingContext, indices : Uint16Array) : WebGLBuffer {
    const buffer = gl.createBuffer()!;

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

    return buffer;
}
function createTexture3D(gl : WebGL2RenderingContext, width : number, height : number, depth : number, voxels : Uint8Array) : WebGLTexture {
    const texture = gl.createTexture()!;

    gl.bindTexture(gl.TEXTURE_3D, texture);
    gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
    gl.texImage3D(gl.TEXTURE_3D, 0, gl.R8, width, height, depth, 0, gl.RED, gl.UNSIGNED_BYTE, voxels);
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);

    return texture;
}


Vector3、Matrix4

動作確認レベルの実装ですので、お好みの行列ライブラリで置き換えてください。

class Vector3 {
    constructor(
        readonly x : number,
        readonly y : number,
        readonly z : number,
    ) {}
    add(v : Vector3) : Vector3 {
        return new Vector3(this.x + v.x, this.y + v.y, this.z + v.z);
    }
    sub(v : Vector3) : Vector3 {
        return new Vector3(this.x - v.x, this.y - v.y, this.z - v.z);
    }
    mul(s : number) : Vector3 {
        return new Vector3(this.x * s, this.y * s, this.z * s);
    }
    div(s : number) : Vector3 {
        return new Vector3(this.x / s, this.y / s, this.z / s);
    }
    length() : number {
        return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
    }
    normalize() : Vector3 {
        return this.div(this.length());
    }
    dot(v : Vector3) : number {
        return this.x * v.x + this.y * v.y + this.z * v.z;
    }
    cross(v : Vector3) : Vector3 {
        return new Vector3(this.y * v.z - this.z * v.y, this.z * v.x - this.x * v.z, this.x * v.y - this.y * v.x);
    }
    static readonly ex = new Vector3(1, 0, 0);
    static readonly ey = new Vector3(0, 1, 0);
    static readonly ez = new Vector3(0, 0, 1);
}
class Matrix4 {
    constructor(readonly data: number[])
    { }
    mul(m : Matrix4) : Matrix4 {
        return new Matrix4([
            this.data[0] * m.data[0] + this.data[1] * m.data[4] + this.data[2] * m.data[8]  + this.data[3] * m.data[12],
            this.data[0] * m.data[1] + this.data[1] * m.data[5] + this.data[2] * m.data[9]  + this.data[3] * m.data[13],
            this.data[0] * m.data[2] + this.data[1] * m.data[6] + this.data[2] * m.data[10] + this.data[3] * m.data[14],
            this.data[0] * m.data[3] + this.data[1] * m.data[7] + this.data[2] * m.data[11] + this.data[3] * m.data[15],

            this.data[4] * m.data[0] + this.data[5] * m.data[4] + this.data[6] * m.data[8]  + this.data[7] * m.data[12],
            this.data[4] * m.data[1] + this.data[5] * m.data[5] + this.data[6] * m.data[9]  + this.data[7] * m.data[13],
            this.data[4] * m.data[2] + this.data[5] * m.data[6] + this.data[6] * m.data[10] + this.data[7] * m.data[14],
            this.data[4] * m.data[3] + this.data[5] * m.data[7] + this.data[6] * m.data[11] + this.data[7] * m.data[15],

            this.data[8] * m.data[0] + this.data[9] * m.data[4] + this.data[10] * m.data[8]  + this.data[11] * m.data[12],
            this.data[8] * m.data[1] + this.data[9] * m.data[5] + this.data[10] * m.data[9]  + this.data[11] * m.data[13],
            this.data[8] * m.data[2] + this.data[9] * m.data[6] + this.data[10] * m.data[10] + this.data[11] * m.data[14],
            this.data[8] * m.data[3] + this.data[9] * m.data[7] + this.data[10] * m.data[11] + this.data[11] * m.data[15],

            this.data[12] * m.data[0] + this.data[13] * m.data[4] + this.data[14] * m.data[8]  + this.data[15] * m.data[12],
            this.data[12] * m.data[1] + this.data[13] * m.data[5] + this.data[14] * m.data[9]  + this.data[15] * m.data[13],
            this.data[12] * m.data[2] + this.data[13] * m.data[6] + this.data[14] * m.data[10] + this.data[15] * m.data[14],
            this.data[12] * m.data[3] + this.data[13] * m.data[7] + this.data[14] * m.data[11] + this.data[15] * m.data[15],
        ]);
    }
    transform(v : Vector3) : Vector3 {
        const x = this.data[0]  * v.x + this.data[1]  * v.y + this.data[2]  * v.z + this.data[3];
        const y = this.data[4]  * v.x + this.data[5]  * v.y + this.data[6]  * v.z + this.data[7];
        const z = this.data[8]  * v.x + this.data[9]  * v.y + this.data[10] * v.z + this.data[11];
        const w = this.data[12] * v.x + this.data[13] * v.y + this.data[14] * v.z + this.data[15];
        return new Vector3(x / w, y / w, z / w);
    }
    transform3x3(v : Vector3) : Vector3 {
        return new Vector3(
            this.data[0] * v.x + this.data[1] * v.y + this.data[2]  * v.z,
            this.data[4] * v.x + this.data[5] * v.y + this.data[6]  * v.z,
            this.data[8] * v.x + this.data[9] * v.y + this.data[10] * v.z,
        );
    }
    transpose() : Matrix4 {
        return new Matrix4([
            this.data[0], this.data[4], this.data[8],  this.data[12],
            this.data[1], this.data[5], this.data[9],  this.data[13],
            this.data[2], this.data[6], this.data[10], this.data[14],
            this.data[3], this.data[7], this.data[11], this.data[15],
        ]);
    }
    getMatrix3Data() : number[] {
        return [
            this.data[0], this.data[1], this.data[2],
            this.data[4], this.data[5], this.data[6],
            this.data[8], this.data[9], this.data[10],
        ];
    }
    static readonly identity =
        new Matrix4([
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 1, 0,
            0, 0, 0, 1
        ]);
    static translate(v : Vector3) : Matrix4 {
        return new Matrix4([
            1, 0, 0, v.x,
            0, 1, 0, v.y,
            0, 0, 1, v.z,
            0, 0, 0, 1
        ]);
    }
    static rotate(radian : number, axis : Vector3) : Matrix4 {
        const x = axis.x, y = axis.y, z = axis.z;
        const s = Math.sin(radian);
        const c = Math.cos(radian);
        return new Matrix4([
            x * x * (1 - c) + c,     x * y * (1 - c) - z * s, z * x * (1 - c) + y * s, 0,
            x * y * (1 - c) + z * s, y * y * (1 - c) + c,     y * z * (1 - c) - x * s, 0,
            z * x * (1 - c) - y * s, y * z * (1 - c) + x * s, z * z * (1 - c) + c,     0,
            0,                       0,                       0,                       1
        ]);
    }
    static scale(s : number) : Matrix4 {
        return new Matrix4([
            s, 0, 0, 0,
            0, s, 0, 0,
            0, 0, s, 0,
            0, 0, 0, 1
        ]);
    }
    static ortho(width : number, height : number, near : number, far : number) : Matrix4 {
        return new Matrix4([
            2.0 / width, 0,            0,                   0,
            0,           2.0 / height, 0,                   0,
            0,           0,            -2.0 / (far - near), -(far + near) / (far - near),
            0,           0,            0,                   1
        ]);
    }
    static perspective(fovy : number, aspect : number, near : number, far : number) : Matrix4 {
        const f = 1.0 / Math.tan(fovy / 2.0);
        return new Matrix4([
            f / aspect, 0, 0,                           0,
            0,          f, 0,                           0,
            0,          0, (far + near) / (near - far), 2 * far * near / (near - far),
            0,          0, -1,                          0
        ]);
    }
}


Array関連の拡張

Slices作成内で利用しているArray関連の関数は下記のように実装してあります。

interface Array<T> {
    concatMap<U>(f : (value : T) => U[]) : U[];
}
Array.prototype.concatMap = function(f : any) {
    return this.reduce((dst, x) => dst.concat(f(x)), []);
};

function range(count : number) : number[] {
    const ary = Array<number>(count);
    for (let i = 0; i < count; ++i)
        ary[i] = i;
    return ary;
}
function normalizedRange(count : number, min : number, max : number) : number[] {
    return range(count).map<number>(i => ((count - 1 - i) * min + i * max) / (count - 1));
}






Direct3D11でボリュームレンダリング

ボリュームレンダリングの手法はいくつかありますが、今回はもっとも単純な、下図のように長方形のスライスを何枚も重ねて実現したものを実装したいと思います。
f:id:any-programming:20171225171609p:plain

ボリュームデータ

下記ページにある、bunnyのデータを利用いたしました。
The Stanford volume data archive

もとは1ボクセルあたり16bitだったため、8bitに変換して1ファイルにまとめました。

アプリケーションコード

下図のような、マウスの位置によって回転角度の変わるアプリケーションを実装します。
f:id:any-programming:20171225195538p:plain

main関数とWndProcは下記の様に実装しました。

VolumeRenderer renderer;
float rotX = 0;
float rotY = 0;
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_PAINT:
        renderer.Render(rotX, rotY);
        return 0;
    case WM_MOUSEMOVE:
    {
        RECT rect;
        ::GetClientRect(hwnd, &rect);
        int x = LOWORD(lParam);
        int y = HIWORD(lParam);
        rotY = 2 * 3.14f * (float)(x - rect.left) / (rect.right - rect.left);
        rotX = 2 * 3.14f * (float)(y - rect.top) / (rect.bottom - rect.top);
        InvalidateRect(hwnd, nullptr, FALSE);
        return 0;
    }
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}
int main()
{
    HWND hwnd = CreateHWND(WndProc);
    renderer = CreateVolumeRenderer(hwnd);
    Run(hwnd);

    return 0;
}


VolumeRenderer構造体とCreateVolumeRenderer関数

今回はRender関数のみをメンバとして持ったものとしました。

struct VolumeRenderer
{
    std::function<void(float rotX, float rotY)> Render;
};


HWNDを受け取りVolumeRendererを作成する関数を定義します。

複数のスライスを重ねてボリュームを描画する際、横から見てしまうとボリュームにならないため、X、Y、Z三方向のスライスセットを用意しておき随時適切なものを利用して描画するようにしています。

陰影はTextureのX、Y、Z方向の勾配を計算して、それを法線として利用しています。

VolumeRenderer CreateVolumeRenderer(HWND hwnd) {
    // device, context, swapChain作成
    ComPtr<ID3D11Device> device;
    ComPtr<ID3D11DeviceContext> context;
    ComPtr<IDXGISwapChain> swapChain;
    std::tie(device, context, swapChain) = CreateDeviceAndSwapChain(hwnd);

    // RenderTarget、DepthBuffer作成
    ComPtr<ID3D11RenderTargetView> renderTarget = CreateRenderTarget(device.Get(), swapChain.Get());
    SIZE bufferSize = GetBufferSize(swapChain.Get());
    
    ComPtr<ID3D11DepthStencilView> depthBuffer = CreateDepthBuffer(device.Get(), bufferSize.cx, bufferSize.cy);

    // Shader作成
    std::string vertexCode =
        "cbuffer c0{                                                                                \n"
        "    float4x4 Matrix;                                                                       \n"
        "}                                                                                          \n"
        "struct VS_OUTPUT {                                                                         \n"
        "    float4 Position : SV_POSITION;                                                         \n"
        "    float3 Texcoord : TEXCOORD;                                                            \n"
        "};                                                                                         \n"
        "VS_OUTPUT main(float4 position : POSITION, float3 texcoord : TEXCOORD) {                   \n"
        "    VS_OUTPUT output = (VS_OUTPUT)0;                                                       \n"
        "    output.Position = mul(position, Matrix);                                               \n"
        "    output.Texcoord = texcoord;                                                            \n"
        "    return output;                                                                         \n"
        "}                                                                                          \n";
    std::vector<D3D11_INPUT_ELEMENT_DESC> inputDesc =
    {
        { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,                            D3D11_INPUT_PER_VERTEX_DATA, 0 },
        { "TEXCOORD", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }
    };
    ComPtr<ID3D11VertexShader> vertexShader;
    ComPtr<ID3D11InputLayout> inputLayout;
    std::tie(vertexShader, inputLayout) = CreateVertexShader(device.Get(), vertexCode, inputDesc);
    ComPtr<ID3D11Buffer> vertexConstBuffer = CreateConstantBuffer<VertexConst>(device.Get());

    std::string pixelCode =
        "Texture3D texture0 : register(t0);                                                                                                  \n"
        "SamplerState sampler0 {                                                                                                             \n"
        "    Filter = MIN_MAG_MIP_POINT;                                                                                                     \n"
        "    AddressU = Clamp;                                                                                                               \n"
        "    AddressV = Clamp;                                                                                                               \n"
        "    AddressW = Clamp;                                                                                                               \n"
        "};                                                                                                                                  \n"
        "float4 main(float4 position : SV_POSITION, float3 texcoord : TEXCOORD) : SV_TARGET {                                                \n"
        "    if (texture0.Sample(sampler0, texcoord).r < 0.5)                                                                                \n"
        "       discard;                                                                                                                     \n"
        "    float dx = texture0.Sample(sampler0, texcoord, int3(1, 0, 0)).r - texture0.Sample(sampler0, texcoord, int3(-1,  0,  0)).r;      \n"
        "    float dy = texture0.Sample(sampler0, texcoord, int3(0, 1, 0)).r - texture0.Sample(sampler0, texcoord, int3( 0, -1,  0)).r;      \n"
        "    float dz = texture0.Sample(sampler0, texcoord, int3(0, 0, 1)).r - texture0.Sample(sampler0, texcoord, int3( 0,  0, -1)).r;      \n"
        "    float3 normal = normalize(float3(dx, dy, dz));                                                                                  \n"
        "    float3 light = normalize(float3(1, 1, 1));                                                                                      \n"
        "    float gray = abs(dot(normal, light));                                                                                           \n"
        "    return float4(gray, gray, gray, 1.0);                                                                                           \n"
        "}                                                                                                                                   \n";
    ComPtr<ID3D11PixelShader> pixelShader = CreatePixelShader(device.Get(), pixelCode);

    // Texture3D作成
    std::ifstream file("bunny", std::ios::binary | std::ios::ate);
    std::vector<byte> data((size_t)file.tellg());
    file.seekg(0, std::ios::beg);
    file.read((char*)data.data(), data.size());

    ComPtr<ID3D11ShaderResourceView> volumeTexture = CreateTexture3D(device.Get(), 512, 512, data);

    // VertexBuffer, IndexBuffer作成(X、Y、Z三方向のBuffer)
    Slices slices = CreateSlices(device.Get(), 512, 512, 360);

    // culling off用 RasterrizerState作成
    ComPtr<ID3D11RasterizerState> rasterizerState = CreateRasterizerState(device.Get(), [](D3D11_RASTERIZER_DESC& desc) {
        desc.CullMode = D3D11_CULL_NONE;
    });

    // Render関数作成
    auto render = [=](float rotX, float rotY) {
        // Matrix作成
        Matrix4 mat = 
            Matrix4::Rotate(rotY, Vector3{ 0, 1, 0 }) *
            Matrix4::Rotate(rotX, Vector3{ 1, 0, 0 }) *
            Matrix4::Scale(1.5 / 512.0) *
            Matrix4::Translate(Vector3{ -512 / 2, -512 / 2, -360 / 2 });

        // RenderTarget、DepthBuffer設定
        context->OMSetRenderTargets(1, renderTarget.GetAddressOf(), depthBuffer.Get());

        // RasterizerState設定
        context->RSSetState(rasterizerState.Get());

        // Shader設定
        context->VSSetShader(vertexShader.Get(), nullptr, 0);
        context->IASetInputLayout(inputLayout.Get());
        context->PSSetShader(pixelShader.Get(), nullptr, 0);
        
        // Buffer設定
        ID3D11Buffer *vertexBuffer, *indexBuffer;
        std::tie(vertexBuffer, indexBuffer) = GetBuffers(slices, mat);        
        SetVertexBuffer<Vertex>(context.Get(), vertexBuffer);
        context->IASetIndexBuffer(indexBuffer, DXGI_FORMAT_R16_UINT, 0);

        VertexConst vertexConst = { Matrix4::OrthoD3D(2, 2, -1, 1) * mat };
        context->UpdateSubresource(vertexConstBuffer.Get(), 0, nullptr, &vertexConst, 0, 0);
        context->VSSetConstantBuffers(0, 1, vertexConstBuffer.GetAddressOf());

        // Texture3D設定
        context->PSSetShaderResources(0, 1, volumeTexture.GetAddressOf());

        // Viewport設定
        CD3D11_VIEWPORT viewport(0.0f, 0.0f, (float)bufferSize.cx, (float)bufferSize.cy);
        context->RSSetViewports(1, &viewport);

        // 描画
        float clearColor[4] = { 0.5f, 0.6f, 1.0f, 1.0f };
        context->ClearRenderTargetView(renderTarget.Get(), clearColor);
        context->ClearDepthStencilView(depthBuffer.Get(), D3D11_CLEAR_DEPTH, 1.0f, 0);

        context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
        context->DrawIndexed(GetBufferSize<ushort>(indexBuffer), 0, 0);

        AssertHR(swapChain->Present(0, 0));
    };

    return{ render };
}


VertexConstとVertexは下記の様に定義してあります。

struct VertexConst {
    Matrix4 Matrix;
};
struct Vertex {
    Vector3 Position;
    Vector3 Texcoord;
};


Slices作成

一方向のスライスセットでは横から見たときボリュームにならないため、三方向のスライスセットを用意しておきます。

Slices構造体は下記の様に定義します。

struct Slices {
    ComPtr<ID3D11Buffer> VertexBufferX;
    ComPtr<ID3D11Buffer> IndexBufferX;
    ComPtr<ID3D11Buffer> VertexBufferY;
    ComPtr<ID3D11Buffer> IndexBufferY;
    ComPtr<ID3D11Buffer> VertexBufferZ;
    ComPtr<ID3D11Buffer> IndexBufferZ;
};


Slicesを作成する関数を下記の様に実装します。

(0, 0, 0)を原点として、(width, height, depth)のボックスのSlicesを作成しています。

Slices CreateSlices(ID3D11Device* device, int width, int height, int depth) {
    float w = (float)width, h = (float)height, d = (float)depth;

    std::vector<Vertex> verticesZ;
    for (int i = 0; i < depth; ++i) {
        float t = (float)i / (depth - 1);
        verticesZ.push_back({ { 0, 0, t * d }, { 0, 0, t } });
        verticesZ.push_back({ { w, 0, t * d }, { 1, 0, t } });
        verticesZ.push_back({ { 0, h, t * d }, { 0, 1, t } });
        verticesZ.push_back({ { w, h, t * d }, { 1, 1, t } });
    }

    std::vector<Vertex> verticesY;
    for (int i = 0; i < height; ++i) {
        float t = (float)i / (height - 1);
        verticesY.push_back({ { 0, t * h, 0 }, { 0, t, 0 } });
        verticesY.push_back({ { w, t * h, 0 }, { 1, t, 0 } });
        verticesY.push_back({ { 0, t * h, d }, { 0, t, 1 } });
        verticesY.push_back({ { w, t * h, d }, { 1, t, 1 } });
    }

    std::vector<Vertex> verticesX;
    for (int i = 0; i < width; ++i) {
        float t = (float)i / (width - 1);
        verticesX.push_back({ { t * w, 0, 0 }, { t, 0, 0 } });
        verticesX.push_back({ { t * w, h, 0 }, { t, 1, 0 } });
        verticesX.push_back({ { t * w, 0, d }, { t, 0, 1 } });
        verticesX.push_back({ { t * w, h, d }, { t, 1, 1 } });
    }

    auto createIndices = [](int count) {
        std::vector<ushort> indices;
        for (int i = 0; i < count; ++i)
            for (ushort j : { 0, 2, 1, 1, 2, 3 })
                indices.push_back(4 * i + j);
        return indices;
    };

    return{
        CreateVertexBuffer(device, verticesX),
        CreateIndexBuffer(device, createIndices(width)),
        CreateVertexBuffer(device, verticesY),
        CreateIndexBuffer(device, createIndices(height)),
        CreateVertexBuffer(device, verticesZ),
        CreateIndexBuffer(device, createIndices(depth))
    };
}


変換行列から適切なスライスセットを返す関数も定義します。

最も視線方向に向いているスライスセットを選択しています。

std::tuple<ID3D11Buffer*, ID3D11Buffer*> GetBuffers(const Slices& slices, const Matrix4& m) {
    float dotX = std::abs(m.Transform3x3(Vector3{ 1, 0, 0 }).Z);
    float dotY = std::abs(m.Transform3x3(Vector3{ 0, 1, 0 }).Z);
    float dotZ = std::abs(m.Transform3x3(Vector3{ 0, 0, 1 }).Z);
    if (dotX < dotZ && dotY < dotZ)
        return{ slices.VertexBufferZ.Get(), slices.IndexBufferZ.Get() };
    else if (dotX < dotY)
        return{ slices.VertexBufferY.Get(), slices.IndexBufferY.Get() };
    else
        return{ slices.VertexBufferX.Get(), slices.IndexBufferX.Get() };
}


Direct3D11用関数群

コード内で利用したDirect3D11関連の関数は下記の様に実装してあります。

std::tuple<ComPtr<ID3D11Device>, ComPtr<ID3D11DeviceContext>, ComPtr<IDXGISwapChain>> CreateDeviceAndSwapChain(HWND hwnd) {
#ifdef _DEBUG
    DWORD createDeviceFlag = D3D11_CREATE_DEVICE_DEBUG;
#else
    DWORD createDeviceFlag = 0;
#endif
   
    DXGI_SWAP_CHAIN_DESC desc = { 0 };
    desc.BufferDesc.Width = 0; // 現在のWindowサイズが自動で設定される
    desc.BufferDesc.Height = 0;
    desc.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
    desc.BufferDesc.RefreshRate.Numerator = 60;
    desc.BufferDesc.RefreshRate.Denominator = 1;
    desc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
    desc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
    desc.SampleDesc.Count = 1;
    desc.SampleDesc.Quality = 0;
    desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    desc.BufferCount = 1;
    desc.OutputWindow = hwnd;
    desc.Windowed = TRUE;
    desc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
    desc.Flags = 0;

    ComPtr<ID3D11Device> device;
    ComPtr<ID3D11DeviceContext> context;
    ComPtr<IDXGISwapChain> swapChain;
    AssertHR(D3D11CreateDeviceAndSwapChain(
        nullptr,
        D3D_DRIVER_TYPE_HARDWARE,
        nullptr,
        createDeviceFlag,
        nullptr,
        0,
        D3D11_SDK_VERSION,
        &desc,
        &swapChain,
        &device,
        nullptr,
        &context
    ));

    return { device, context, swapChain };
}
ComPtr<ID3D11RenderTargetView> CreateRenderTarget(ID3D11Device* device, IDXGISwapChain* swapChain) {
    ComPtr<ID3D11Texture2D> backBuffer;
    AssertHR(swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer)));
    
    ComPtr<ID3D11RenderTargetView> view;
    AssertHR(device->CreateRenderTargetView(backBuffer.Get(), nullptr, &view));
    
    return view;
}
SIZE GetBufferSize(IDXGISwapChain* swapChain) {
    DXGI_SWAP_CHAIN_DESC desc;
    AssertHR(swapChain->GetDesc(&desc));    
    return{ (LONG)desc.BufferDesc.Width, (LONG)desc.BufferDesc.Height };
}
ComPtr<ID3D11DepthStencilView> CreateDepthBuffer(ID3D11Device* device, int width, int height) {
    D3D11_TEXTURE2D_DESC textureDesc = { 0 };
    textureDesc.Width = width;
    textureDesc.Height = height;
    textureDesc.Format = DXGI_FORMAT_D16_UNORM;
    textureDesc.MipLevels = 1;
    textureDesc.ArraySize = 1;
    textureDesc.SampleDesc.Count = 1;
    textureDesc.SampleDesc.Quality = 0;
    textureDesc.Usage = D3D11_USAGE_DEFAULT;
    textureDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
    textureDesc.CPUAccessFlags = 0;

    ComPtr<ID3D11Texture2D> texture;
    AssertHR(device->CreateTexture2D(&textureDesc, nullptr, &texture));

    CD3D11_DEPTH_STENCIL_VIEW_DESC viewDesc(D3D11_DSV_DIMENSION_TEXTURE2D);
    ComPtr<ID3D11DepthStencilView> view;
    AssertHR(device->CreateDepthStencilView(texture.Get(), &viewDesc, &view));

    return view;
}
ComPtr<ID3DBlob> CompileShader(std::string code, std::string target) {
#ifdef _DEBUG
    UINT flags1 = D3DCOMPILE_DEBUG | D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_WARNINGS_ARE_ERRORS;
#else
    UINT flags1 = D3DCOMPILE_OPTIMIZATION_LEVEL3 | D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_WARNINGS_ARE_ERRORS;
#endif
    ComPtr<ID3DBlob> compiled, errorMessage;
    D3DCompile(
        code.c_str(),
        code.size(),
        nullptr,
        nullptr,
        D3D_COMPILE_STANDARD_FILE_INCLUDE,
        "main",
        target.c_str(),
        flags1,
        0,
        &compiled,
        &errorMessage
    );
    if (errorMessage) 
        throw std::exception((char*)errorMessage->GetBufferPointer());

    return compiled;
}
std::tuple<ComPtr<ID3D11VertexShader>, ComPtr<ID3D11InputLayout>> CreateVertexShader(
    ID3D11Device* device, const std::vector<byte>& cso, const std::vector<D3D11_INPUT_ELEMENT_DESC>& inputDesc) {
    ComPtr<ID3D11VertexShader> shader;
    AssertHR(device->CreateVertexShader(cso.data(), cso.size(), NULL, &shader));

    ComPtr<ID3D11InputLayout> inputLayout;
    AssertHR(device->CreateInputLayout(inputDesc.data(), inputDesc.size(), cso.data(), cso.size(), &inputLayout));

    return { shader, inputLayout };
}
ComPtr<ID3D11PixelShader> CreatePixelShader(ID3D11Device* device, const std::vector<byte>& cso) {
    ComPtr<ID3D11PixelShader> shader;
    AssertHR(device->CreatePixelShader(cso.data(), cso.size(), NULL, &shader));
    return shader;
}
std::tuple<ComPtr<ID3D11VertexShader>, ComPtr<ID3D11InputLayout>> CreateVertexShader(
    ID3D11Device* device, std::string code, const std::vector<D3D11_INPUT_ELEMENT_DESC>& inputDesc) {
    auto blob = CompileShader(code, "vs_4_0");
    auto p = (byte*)blob->GetBufferPointer();
    std::vector<byte> cso(p, p + blob->GetBufferSize());
    return CreateVertexShader(device, cso, inputDesc);
}
ComPtr<ID3D11PixelShader> CreatePixelShader(ID3D11Device* device, std::string code) {
    auto blob = CompileShader(code, "ps_4_0");
    auto p = (byte*)blob->GetBufferPointer();
    std::vector<byte> cso(p, p + blob->GetBufferSize());
    return CreatePixelShader(device, cso);
}
template <class T>
ComPtr<ID3D11Buffer> CreateVertexBuffer(ID3D11Device* device, const std::vector<T>& vertices) {
    D3D11_BUFFER_DESC desc = { 0 };
    desc.Usage = D3D11_USAGE_DEFAULT;
    desc.ByteWidth = vertices.size() * sizeof(T);
    desc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
    desc.CPUAccessFlags = 0;

    D3D11_SUBRESOURCE_DATA data = { 0 };
    data.pSysMem = vertices.data();

    ComPtr<ID3D11Buffer> buffer;
    AssertHR(device->CreateBuffer(&desc, &data, &buffer));

    return buffer;
}
template <class T>
void SetVertexBuffer(ID3D11DeviceContext* context, ID3D11Buffer* buffer) {
    UINT stride = sizeof(T);
    UINT offset = 0;
    context->IASetVertexBuffers(0, 1, &buffer, &stride, &offset);
}
template <class T>
ComPtr<ID3D11Buffer> CreateIndexBuffer(ID3D11Device* device, const std::vector<T>& indices) {
    D3D11_BUFFER_DESC desc = { 0 };
    desc.Usage = D3D11_USAGE_DEFAULT;
    desc.ByteWidth = indices.size() * sizeof(T);
    desc.BindFlags = D3D11_BIND_INDEX_BUFFER;
    desc.CPUAccessFlags = 0;

    D3D11_SUBRESOURCE_DATA data = { 0 };
    data.pSysMem = indices.data();

    ComPtr<ID3D11Buffer> buffer;
    AssertHR(device->CreateBuffer(&desc, &data, &buffer));

    return buffer;
}
template<class T>
UINT GetBufferSize(ID3D11Buffer* buffer) {
    D3D11_BUFFER_DESC desc;
    buffer->GetDesc(&desc);
    return desc.ByteWidth / sizeof(T);
}
template <class T>
ComPtr<ID3D11Buffer> CreateConstantBuffer(ID3D11Device* device) {
    D3D11_BUFFER_DESC desc = { 0 };
    desc.Usage = D3D11_USAGE_DEFAULT;
    desc.ByteWidth = sizeof(T);
    desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    desc.CPUAccessFlags = 0;

    ComPtr<ID3D11Buffer> buffer;
    AssertHR(device->CreateBuffer(&desc, nullptr, &buffer));

    return buffer;
}
ComPtr<ID3D11ShaderResourceView> CreateTexture3D(ID3D11Device* device, int width, int height, const std::vector<byte>& voxels) {
    D3D11_TEXTURE3D_DESC desc = { 0 };
    desc.Width = width;
    desc.Height = height;
    desc.Depth = voxels.size() / width / height;
    desc.Format = DXGI_FORMAT_R8_UNORM;
    desc.MipLevels = 1;
    desc.Usage = D3D11_USAGE_DEFAULT;
    desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
    desc.CPUAccessFlags = 0;
    desc.MiscFlags = 0;

    D3D11_SUBRESOURCE_DATA data = { 0 };
    data.pSysMem = voxels.data();
    data.SysMemPitch = width;
    data.SysMemSlicePitch = width * height;

    ComPtr<ID3D11Texture3D> texture;
    AssertHR(device->CreateTexture3D(&desc, &data, &texture));

    ComPtr<ID3D11ShaderResourceView> view;
    AssertHR(device->CreateShaderResourceView(texture.Get(), nullptr, &view));
    
    return view;
}
ComPtr<ID3D11RasterizerState> CreateRasterizerState(ID3D11Device* device, std::function<void(D3D11_RASTERIZER_DESC&)> f) {
    CD3D11_RASTERIZER_DESC desc = CD3D11_RASTERIZER_DESC(CD3D11_DEFAULT());
    f(desc);
    ComPtr<ID3D11RasterizerState> state;
    AssertHR(device->CreateRasterizerState(&desc, &state));
    return state;
}


Vector3、Matrix4

動作確認レベルの実装ですので、お好みの行列ライブラリで置き換えてください。

struct Vector3 {
    float X, Y, Z;
    Vector3 operator - () const { 
        return{ -X, -Y, -Z };
    }
    Vector3 operator + (const Vector3& v) const { 
        return{ X + v.X, Y + v.Y, Z + v.Z }; 
    }
    Vector3 operator - (const Vector3& v) const { 
        return{ X - v.X, Y - v.Y, Z - v.Z }; 
    }
    Vector3 operator * (float s) const { 
        return{ s * X, s * Y, s * Z }; 
    }
    Vector3 operator / (float s) const { 
        return{ X / s, Y / s, Z / s }; 
    }
    friend Vector3 operator * (float s, const Vector3& v) { 
        return v * s; 
    }
    float Length() const { 
        return std::sqrt(X * X + Y * Y + Z * Z); 
    }
    Vector3 Normalize() const {
        float length = this->Length();
        return{ X / length, Y / length, Z / length };
    }
    float Dot(const Vector3& v) const { 
        return X * v.X + Y * v.Y + Z * v.Z; 
    }
    Vector3 Cross(const Vector3& v) const { 
        return{ Y * v.Z - Z * v.Y, Z * v.X - X * v.Z, X * v.Y - Y * v.X }; 
    }
};
struct Matrix4 {
    float M00, M01, M02, M03;
    float M10, M11, M12, M13;
    float M20, M21, M22, M23;
    float M30, M31, M32, M33;
    Matrix4 operator * (const Matrix4& m) const {
        return{
            M00 * m.M00 + M01 * m.M10 + M02 * m.M20 + M03 * m.M30,
            M00 * m.M01 + M01 * m.M11 + M02 * m.M21 + M03 * m.M31,
            M00 * m.M02 + M01 * m.M12 + M02 * m.M22 + M03 * m.M32,
            M00 * m.M03 + M01 * m.M13 + M02 * m.M23 + M03 * m.M33,            

            M10 * m.M00 + M11 * m.M10 + M12 * m.M20 + M13 * m.M30,
            M10 * m.M01 + M11 * m.M11 + M12 * m.M21 + M13 * m.M31,
            M10 * m.M02 + M11 * m.M12 + M12 * m.M22 + M13 * m.M32,
            M10 * m.M03 + M11 * m.M13 + M12 * m.M23 + M13 * m.M33,            

            M20 * m.M00 + M21 * m.M10 + M22 * m.M20 + M23 * m.M30,
            M20 * m.M01 + M21 * m.M11 + M22 * m.M21 + M23 * m.M31,
            M20 * m.M02 + M21 * m.M12 + M22 * m.M22 + M23 * m.M32,
            M20 * m.M03 + M21 * m.M13 + M22 * m.M23 + M23 * m.M33,            

            M30 * m.M00 + M31 * m.M10 + M32 * m.M20 + M33 * m.M30,
            M30 * m.M01 + M31 * m.M11 + M32 * m.M21 + M33 * m.M31,
            M30 * m.M02 + M31 * m.M12 + M32 * m.M22 + M33 * m.M32,
            M30 * m.M03 + M31 * m.M13 + M32 * m.M23 + M33 * m.M33
        };
    }
    Vector3 Transform(const Vector3& v) const {
        float x = M00 * v.X + M01 * v.Y + M02 * v.Z + M03;
        float y = M10 * v.X + M11 * v.Y + M12 * v.Z + M13;
        float z = M20 * v.X + M21 * v.Y + M22 * v.Z + M23;
        float w = M30 * v.X + M31 * v.Y + M32 * v.Z + M33;
        return Vector3{x, y, z} / w;
    }
    Vector3 Transform3x3(const Vector3& v) const {
        return Vector3{
            M00 * v.X + M01 * v.Y + M02 * v.Z,
            M10 * v.X + M11 * v.Y + M12 * v.Z,
            M20 * v.X + M21 * v.Y + M22 * v.Z,
        };
    }
    static Matrix4 Identity() {
        return{ 
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 1, 0,
            0, 0, 0, 1 
        };
    }
    static Matrix4 Translate(const Vector3& v) {
        return{
            1, 0, 0, v.X,
            0, 1, 0, v.Y,
            0, 0, 1, v.Z,
            0, 0, 0, 1
        };
    }
    static Matrix4 Rotate(float radian, const Vector3& axis) {
        float x = axis.X, y = axis.Y, z = axis.Z;
        float s = std::sin(radian);
        float c = std::cos(radian);
        return{
            x * x * (1 - c) + c,     x * y * (1 - c) - z * s, z * x * (1 - c) + y * s, 0,
            x * y * (1 - c) + z * s, y * y * (1 - c) + c,     y * z * (1 - c) - x * s, 0,
            z * x * (1 - c) - y * s, y * z * (1 - c) + x * s, z * z * (1 - c) + c,     0,
            0,                       0,                       0,                       1
        };
    }
    static Matrix4 Scale(float s) {
        return{
            s, 0, 0, 0,
            0, s, 0, 0,
            0, 0, s, 0,
            0, 0, 0, 1
        };
    }
    static Matrix4 OrthoD3D(float width, float height, float nearz, float farz) {
        return{
            2 / width, 0,          0,                  0,
            0,         2 / height, 0,                  0,
            0,         0,          1 / (farz - nearz), -nearz / (farz - nearz),
            0,         0,          0,                  1
        };
    }
    static Matrix4 PerspectiveD3D(float fovy, float aspect, float nearz, float farz) {
        float f = 1 / std::tan(fovy / 2.0f);
        return{
            f / aspect, 0, 0,                     0,
            0,          f, 0,                     0,
            0,          0, farz / (farz - nearz), -nearz * farz / (farz - nearz),
            0,          0, 1,                     0
        };
    }
};






WebGL(TypeScript)--- 立方体描画&回転

下記記事にて、WebGLを用いて三角形を描画しました。
WebGL(TypeScript)--- 三角形描画 - 何でもプログラミング

今回は引き続き、立方体を描画&回転を実装してみたいと思います。

上記記事をベースに実装していきますので、解説のない部分は参照をお願いいたします。

Index Buffer作成関数

今回はIndex Bufferを用いて描画を行ってみたいと思います。

ですので、作成関数を準備します。

function createIndexBuffer(gl : WebGLRenderingContext, indices : number[]) : WebGLBuffer {
    const buffer = gl.createBuffer()!;

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

    return buffer;
}


Matrix4クラス

回転を実現するため、最低限の行列クラスを準備します。

回転行列を作成するrotate関数、転置を行うtranspose関数(WebGLは列優先のため)、左上の3x3行列を取り出すgetMatrix3Data関数(法線変換用)を定義しています。

class Matrix4 {
    constructor(readonly data: number[]) { }

    transposed() : Matrix4 {
        return new Matrix4([
            this.data[0], this.data[4], this.data[8],  this.data[12],
            this.data[1], this.data[5], this.data[9],  this.data[13],
            this.data[2], this.data[6], this.data[10], this.data[14],
            this.data[3], this.data[7], this.data[11], this.data[15],
        ]);
    }

    getMatrix3Data() : number[] {
        return [
            this.data[0], this.data[1], this.data[2],
            this.data[4], this.data[5], this.data[6],
            this.data[8], this.data[9], this.data[10],
        ];
    }

    static rotate(degree : number, x : number, y : number, z : number) : Matrix4 {
        const length = Math.sqrt(x * x + y * y + z * z);
        x /= length;
        y /= length;
        z /= length;
        const radian = degree * Math.PI / 180.0;
        const s = Math.sin(radian);
        const c = Math.cos(radian);
        return new Matrix4([
            x * x * (1 - c) + c,     x * y * (1 - c) - z * s, z * x * (1 - c) + y * s, 0,
            x * y * (1 - c) + z * s, y * y * (1 - c) + c,     y * z * (1 - c) - x * s, 0,
            z * x * (1 - c) - y * s, y * z * (1 - c) + x * s, z * z * (1 - c) + c,     0,
            0,                       0,                       0,                       1
        ]);
    }
}


main関数

ライティングを行うShaderの作成と、立方体のVertex Buffer&Index Bufferの作成を行い、requestAnimationFrameにて回転させつつ描画を行っています。

実行すると下記のような表示になります。(実際はアニメーションします。)
f:id:any-programming:20171214141158p:plain

let gl : WebGLRenderingContext;

function main() : void {
    const canvas = <HTMLCanvasElement>document.getElementById("gl");
    gl = canvas.getContext("webgl")!;

    // Vertex Shader(頂点と法線を変換して次へ)
    const vertexCode = `
        attribute vec4 position;
        attribute vec3 normal;
        uniform mat4 matrix;
        uniform mat3 normalMatrix;
        varying vec3 v_normal;
        void main() {
            gl_Position = matrix * position;
            v_normal = normalMatrix * normal;
        }    
    `;

    // Fragment Shader(ライティングを行って描画)
    const fragmentCode = `
        precision mediump float;
        varying vec3 v_normal;
        void main() {
            vec3 light = vec3(0.0, 0.0, -1.0);            
            float g = dot(light, normalize(v_normal));
            g = max(0.0, g);
            gl_FragColor = vec4(0.0, g, 0.0, 1.0);
        }
    `;

    const program = createProgram(gl, vertexCode, fragmentCode);

    // 立方体定義(頂点、法線)
    const vertices = [
         0.5,  0.5,  0.5,    0.0,  0.0,  1.0,
        -0.5,  0.5,  0.5,    0.0,  0.0,  1.0,
         0.5, -0.5,  0.5,    0.0,  0.0,  1.0,
        -0.5, -0.5,  0.5,    0.0,  0.0,  1.0,

         0.5,  0.5, -0.5,    0.0,  0.0, -1.0,
        -0.5,  0.5, -0.5,    0.0,  0.0, -1.0,
         0.5, -0.5, -0.5,    0.0,  0.0, -1.0,
        -0.5, -0.5, -0.5,    0.0,  0.0, -1.0,

         0.5,  0.5,  0.5,    1.0,  0.0,  0.0,
         0.5, -0.5,  0.5,    1.0,  0.0,  0.0,
         0.5,  0.5, -0.5,    1.0,  0.0,  0.0,
         0.5, -0.5, -0.5,    1.0,  0.0,  0.0,

        -0.5,  0.5,  0.5,   -1.0,  0.0,  0.0,
        -0.5, -0.5,  0.5,   -1.0,  0.0,  0.0,
        -0.5,  0.5, -0.5,   -1.0,  0.0,  0.0,
        -0.5, -0.5, -0.5,   -1.0,  0.0,  0.0,

         0.5,  0.5,  0.5,    0.0,  1.0,  0.0,
         0.5,  0.5, -0.5,    0.0,  1.0,  0.0,
        -0.5,  0.5,  0.5,    0.0,  1.0,  0.0,
        -0.5,  0.5, -0.5,    0.0,  1.0,  0.0,

         0.5, -0.5,  0.5,    0.0, -1.0,  0.0,
         0.5, -0.5, -0.5,    0.0, -1.0,  0.0,
        -0.5, -0.5,  0.5,    0.0, -1.0,  0.0,
        -0.5, -0.5, -0.5,    0.0, -1.0,  0.0,
    ];
    const vertexBuffer = createVertexBuffer(gl, vertices);

    const indices = [
        0,  2,  1,
        1,  2,  3,
        4,  5,  6,
        5,  7,  6,

        8,  10, 9,
        9,  10, 11,
        12, 13, 14,
        13, 15, 14,

        16, 18, 17,
        17, 18, 19,
        20, 21, 22,
        21, 23, 22,
    ];
    const indexBuffer = createIndexBuffer(gl, indices);

    // 初期設定
    gl.useProgram(program);
    const positionAttrib = gl.getAttribLocation(program, "position");
    const normalAttrib = gl.getAttribLocation(program, "normal");
    const matrixUniform = gl.getUniformLocation(program, "matrix")!;
    const normalMatrixUniform = gl.getUniformLocation(program, "normalMatrix")!;

    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.enableVertexAttribArray(positionAttrib);
    gl.enableVertexAttribArray(normalAttrib);
    gl.vertexAttribPointer(positionAttrib, 3, gl.FLOAT, false, 24, 0);
    gl.vertexAttribPointer(normalAttrib, 3, gl.FLOAT, false, 24, 12);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

    gl.enable(gl.DEPTH_TEST);
    gl.enable(gl.CULL_FACE); // デフォルトで反時計回りが表
    
    // 描画関数
    let rotation = 0;
    function step(time : number) : void {
        rotation++;

        const mat = Matrix4.rotate(rotation, 1, 1, 1).transposed();
        gl.uniformMatrix4fv(matrixUniform, false, mat.data);
        gl.uniformMatrix3fv(normalMatrixUniform, false, mat.getMatrix3Data());

        gl.clearColor(0.8, 0.8, 0.8, 1);
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);

        requestAnimationFrame(step);
    }
    // 描画スタート
    requestAnimationFrame(step);
}

main();






WebGL(TypeScript)--- 三角形描画

今回はWebGLを用いてcanvasに三角形を描画してみたいと思います。

ライブラリを用いず、一から実装してみます。

HTMLの準備

表示のため、下記htmlを用意しました。

canvas要素にWebGLで描画します。

<!DOCTYPE html>
<html>
  <body>
    <canvas id="gl_canvas" width="100" height="100"></canvas>
    <script src="script.js"></script>
  </body>
</html>


Shader作成関数

Vertex ShaderのコードとFragment Shaderのコードから、WebGLProgramを作成する関数を準備します。

function createProgram(gl : WebGLRenderingContext, vertexCode : string, fragmentCode : string) : WebGLProgram {
    // Vertex Shader コンパイル
    const vertexShader = gl.createShader(gl.VERTEX_SHADER)!;
    compileShader(gl, vertexShader, vertexCode);

    // Fragment Shader コンパイル
    const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)!;
    compileShader(gl, fragmentShader, fragmentCode);

    // Program作成
    const program = gl.createProgram()!;
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);

    // エラー確認
    console.log(gl.getProgramInfoLog(program));
    if (!gl.getProgramParameter(program, gl.LINK_STATUS))
        throw new Error("program error");

    return program;
}


上記で利用しているcompileShaderは次のようになります。

function compileShader(gl : WebGLRenderingContext, shader : WebGLShader, code : string) : void {
    gl.shaderSource(shader, code);
    gl.compileShader(shader);

    // エラー確認
    console.log(gl.getShaderInfoLog(shader));
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
        throw new Error("compile error");
}


VertexBuffer作成関数

頂点データの配列からWebGLBufferを作成する関数を準備します。

function createVertexBuffer(gl : WebGLRenderingContext, vertices : number[]) : WebGLBuffer {
    const buffer = gl.createBuffer()!;

    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

    return buffer;
}


main関数

三角形を描画する関数は下記の様になります。

canvasにはこのように描画されます。
f:id:any-programming:20171214011828p:plain

let gl : WebGLRenderingContext;

function main() : void {
    const canvas = <HTMLCanvasElement>document.getElementById("gl_canvas");
    gl = canvas.getContext("webgl")!;

    const vertexCode = `
        attribute vec4 position;
        void main() {
            gl_Position = position;
        }    
    `;

    const fragmentCode = `
        void main() {
            gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
        }
    `;

    const program = createProgram(gl, vertexCode, fragmentCode);

    const vertices = [
         0.0,  1.0, 0.0,
        -1.0, -1.0, 0.0,
         1.0, -1.0, 0.0,
    ];
    const vertexBuffer = createVertexBuffer(gl, vertices);

    gl.useProgram(program);
    const positionAttrib = gl.getAttribLocation(program, "position");
    gl.enableVertexAttribArray(positionAttrib);
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    gl.vertexAttribPointer(positionAttrib, 3, gl.FLOAT, false, 0, 0);

    gl.clearColor(0.3, 0.3, 0.3, 1);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES, 0, 3);
}

main();