Skip to content

What's new in v0.4.1

1. レンダーテクスチャ

これまで、図形やテクスチャはシーンにしか描画できませんでしたが、プログラムで用意した別のレンダーテクスチャにも描画できるようになりました。RenderTexture を作成し、ScopedRenderTarget2D オブジェクトのコンストラクタにレンダーテクスチャを渡すと、ScopedRenderTarget2D オブジェクトのスコープが有効な間、図形やテクスチャがそのレンダーテクスチャに描画されます(レンダーターゲットの変更)。描画されたレンダーテクスチャは、レンダーターゲットから解除されたあとにテクスチャとして描画に転用できます。

Warning

レンダーターゲットとして設定されている最中のレンダーテクスチャを、描画に使用することはできません。

# include <Siv3D.hpp>

void Main()
{
    // シーンの背景色を淡い水色に設定
    Scene::SetBackground(ColorF(0.8, 0.9, 1.0));

    // 絵文字
    const Texture emoji(Emoji(U"😇"));

    // レンダーテクスチャ
    RenderTexture rt(600, 600, Palette::White);

    while (System::Update())
    {
        // マウスの左ボタンが押されていたら
        if (MouseL.pressed())
        {
            // レンダーターゲットを rt に設定
            ScopedRenderTarget2D target(rt);
            emoji.drawAt(Cursor::Pos());
        }

        rt.draw();
        emoji.drawAt(Cursor::Pos());

        // Clear ボタンが押されたら
        if (SimpleGUI::Button(U"Clear", Vec2(650, 20)))
        {
            // レンダーテクスチャを白でクリア
            rt.clear(Palette::White);
        }
    }
}

2. マルチサンプル・レンダーテクスチャ

通常の RenderTexture への描画ではマルチサンプル・アンチエイリアシングが有効にならないので、図形を描画した際にジャギーが生じます。MSRenderTexture を使うと、通常のシーンへの描画と同じように、マルチサンプル・アンチエイリアシングを有効にして描画できます。ただし、MSRenderTexture に描画された結果を、別の描画で使う際には、Graphics2D::Flush() によってその時点までの描画処理をすべて実行(フラッシュ)して MSRenderTexture に確実に描画したあとに、MSRenderTexture::resolve() を行い、MSRenderTexture 内のマルチサンプル・テクスチャを、描画で使用可能な通常のテクスチャに変換しておく必要があります。

# include <Siv3D.hpp>

void Main()
{
    Scene::SetBackground(ColorF(0.8, 0.9, 1.0));

    // レンダーテクスチャ
    RenderTexture rt(200, 200);

    // マルチサンプル・レンダーテクスチャ
    MSRenderTexture msrt(200, 200);

    while (System::Update())
    {
        rt.clear(ColorF(0.0, 1.0));
        {
            ScopedRenderTarget2D target(rt);

            Rect(Arg::center(100, 100), 80)
                .rotated(Scene::Time() * 30_deg).draw();
        }

        msrt.clear(ColorF(0.0, 1.0));
        {
            {
                ScopedRenderTarget2D target(msrt);

                Rect(Arg::center(100, 100), 80)
                    .rotated(Scene::Time() * 30_deg).draw();
            }

            // 2D 描画をフラッシュ
            Graphics2D::Flush();

            // マルチサンプルテクスチャを描画可能なテクスチャに変換
            msrt.resolve();
        }

        rt.draw(100, 0);

        msrt.draw(400, 0);
    }
}

3. レンダーテクスチャへのシェーダ処理

テクスチャから別のレンダーテクスチャへの様々な変換処理を関数 1 つで実行できます。レンダーステートの変更も不要です。提供される関数は次のとおりです。

void Copy(const TextureRegion& from, RenderTexture& to);

  • from: 入力テクスチャ
  • to: 出力テクスチャ

from のテクスチャの内容を to に描画します。fromto はともに有効なテクスチャで、互いに異なり、領域のサイズが同じでなければなりません。

void Downsample(const TextureRegion& from, RenderTexture& to);

  • from: 入力テクスチャ
  • to: 出力テクスチャ

from のテクスチャの内容を縮小して to に描画します。fromto はともに有効なテクスチャで、互いに異なるテクスチャでなければなりません。

void GaussianBlurH(const TextureRegion& from, RenderTexture& to);

  • from: 入力テクスチャ
  • to: 出力テクスチャ

from のテクスチャの内容に横方向のガウスブラーをかけて to に描画します。fromto はともに有効なテクスチャで、互いに異なり、領域のサイズが同じでなければなりません。

void GaussianBlurV(const TextureRegion& from, RenderTexture& to);

  • from: 入力テクスチャ
  • to: 出力テクスチャ

from のテクスチャの内容に縦方向のガウスブラーをかけて to に描画します。fromto はともに有効なテクスチャで、互いに異なり、領域のサイズが同じでなければなりません。

void GaussianBlur(const TextureRegion& from, RenderTexture& to, const Vec2& direction);

  • from: 入力テクスチャ
  • to: 出力テクスチャ
  • direction: ブラーの方向

from のテクスチャの内容に指定した方向のガウスブラーをかけて to に描画します。fromto はともに有効なテクスチャで、互いに異なり、領域のサイズが同じでなければなりません。

void GaussianBlur(const TextureRegion& from, RenderTexture& internalBuffer, RenderTexture& to);

  • from: 入力テクスチャ
  • internalBuffer: 中間テクスチャ
  • to: 出力テクスチャ

from のテクスチャの内容をに縦方向と横方向のガウスブラーをかけて to に描画します。from, internalBuffer, to はいずれも有効なテクスチャで、隣り合うもの同士は異なり、領域のサイズが同じでなければなりません。
GaussianBlurH(from, internalBuffer); GaussianBlurV(internalBuffer, to); と等価です。

ダウンサンプリング

# include <Siv3D.hpp>

void Main()
{
    const Texture texture(U"example/windmill.png");

    // 縦、横が 4 分の 1 サイズのレンダーテクスチャ
    RenderTexture rt(texture.size() / 4);

    // ダウンサンプリング
    Shader::Downsample(texture, rt);

    while (System::Update())
    {
        rt.draw();
    }
}

ガウスぼかし

# include <Siv3D.hpp>

void Main()
{
    // ウィンドウを 1280x720 にリサイズ
    Window::Resize(1280, 720);

    // bay.jpg は 2560x1440 なのでサイズを小さくしてロード
    const Texture texture(Image(U"example/bay.jpg").scale(1280, 720));

    // ぼかしを適用する領域のサイズ
    constexpr Size blurAreaSize(480, 320);

    // ガウスぼかしの中間で使うレンダーテクスチャを用意
    RenderTexture rtA(blurAreaSize), rtB(blurAreaSize);
    RenderTexture rtA4(blurAreaSize / 4), rtB4(blurAreaSize / 4);
    RenderTexture rtA8(blurAreaSize / 8), rtB8(blurAreaSize / 8);

    while (System::Update())
    {
        const Point cursorPos = Cursor::Pos();

        // 背景画像のうちぼかしを適用する領域
        const Rect blurArea(cursorPos, blurAreaSize);

        // [オリジナル]->[ガウスぼかし]->[1/4サイズ]->[ガウスぼかし]->[1/8サイズ]->[ガウスぼかし]
        Shader::GaussianBlur(texture(blurArea), rtB, rtA);
        Shader::Downsample(rtA, rtA4);
        Shader::GaussianBlur(rtA4, rtB4, rtA4);
        Shader::Downsample(rtA4, rtA8);
        Shader::GaussianBlur(rtA8, rtB8, rtA8);

        // 背景を描画
        texture.draw();

        // ガウスぼかし後のテクスチャを RoundRect に貼り付けて描画
        RoundRect(cursorPos, blurAreaSize, 40)(rtA8.resized(blurAreaSize)).draw();
    }
}

4. カスタムピクセルシェーダ

2D のテクスチャや図形がレンダーターゲットに描かれるとき、どのような色を出力するかは、「ピクセルシェ―ダ」と呼ばれる、ピクセルごとに GPU 上で実行されるプログラムを通して決定されます。そのプログラムをカスタマイズできるようになりました。

ピクセルシェーダのプログラムは Windows (Direct3D) では「HLSL」、macOS/Linux (OpenGL) では「GLSL」という言語で記述します。カスタムのピクセルシェーダプログラムを書く前に、デフォルトではどのようなシェーダプログラムで図形やテクスチャが描かれているのかを見てみましょう。

図形を描くときに使われる基本シェーダ

HLSL

PS() 関数がピクセルシェーダのエントリーポイントです。頂点シェーダの出力である PSInput 型の値 input.position, .color, .uv メンバ変数のうち .color と定数バッファ g_colorAdd だけを使って、出力するピクセルの RGBA 色 (float4 型) を計算しています。

Rect(100).draw(ColorF(0.3, 0.5, 0.7)) のように描画したとき、input.colorfloat4(0.3, 0.5, 0.7, 1.0) です。なお、ScopedColorMul2D を変更した場合にはその値が乗算済みで渡されます。 g_colorAddScopedAdd2D で指定した値が格納されていて、基本的には flaot4(0.0, 0.0, 0.0, 0.0) です。

したがって、この基本シェーダを使うと、.draw() で指定した色 × ColorMul2D + ColorAdd2D の色で図形が画面に描かれることになります。こうした計算をせずに return float4(1, 0, 0, 1) を返せば、図形は赤く描画されます。

  • engine/shader/2d/default_shape.hlsl
Texture2D       g_texture0 : register(t0);
SamplerState    g_sampler0 : register(s0);

cbuffer PSConstants2D : register(b0)
{
    float4 g_colorAdd;
    float4 g_sdfParam;
    float4 g_internal;
}

struct PSInput
{
    float4 position : SV_POSITION;
    float4 color    : COLOR0;
    float2 uv       : TEXCOORD0;
};

float4 PS(PSInput input) : SV_TARGET
{
    return input.color + g_colorAdd;
}

GLSL

main() 関数がピクセルシェーダのエントリーポイントです。頂点シェーダの出力である vec4 型の値 Color と定数バッファ g_colorAdd だけを使って、出力するピクセルの RGBA 色 (float4 型) を計算し、FragColor に格納しています。

Rect(100).draw(ColorF(0.3, 0.5, 0.7)) のように描画したとき、Colorvec4(0.3, 0.5, 0.7, 1.0) です。なお、ScopedColorMul2D を変更した場合にはその値が乗算済みで渡されます。 g_colorAddScopedAdd2D で指定した値が格納されていて、基本的には vec4(0.0, 0.0, 0.0, 0.0) です。

したがって、この基本シェーダを使うと、.draw() で指定した色 × ColorMul2D + ColorAdd2D の色で図形が画面に描かれることになります。こうした計算をせずに FragColor = vec4(1, 0, 0, 1) とすれば、図形は赤く描画されます。

  • engine/shader/2d/default_shape.frag
#version 410

layout(std140) uniform PSConstants2D
{
    vec4 g_colorAdd;
    vec4 g_sdfParam;
    vec4 g_internal;    
};

//
// PSInput
//
layout(location = 0) in vec4 Color;

//
// PSOutput
//
layout(location = 0) out vec4 FragColor;

void main()
{
    FragColor = Color + g_colorAdd;
}

テクスチャを描くときに使われる基本シェーダ

HLSL

基本的な流れは図形のシェーダプログラムと同じですが、追加でテクスチャ g_texture0 とサンプラー g_sampler0 を使ってテクスチャから色を取得します。UV 座標として頂点シェーダから渡される input.uv を使います。

  • engine/shader/2d/default_texture.hlsl
Texture2D       g_texture0 : register(t0);
SamplerState    g_sampler0 : register(s0);

cbuffer PSConstants2D : register(b0)
{
    float4 g_colorAdd;
    float4 g_sdfParam;
    float4 g_internal;
}

struct PSInput
{
    float4 position : SV_POSITION;
    float4 color    : COLOR0;
    float2 uv       : TEXCOORD0;
};

float4 PS(PSInput input) : SV_TARGET
{
    float4 texColor = g_texture0.Sample(g_sampler0, input.uv);

    return (texColor * input.color) + g_colorAdd;
}

GLSL

基本的な流れは図形のシェーダプログラムと同じですが、追加でテクスチャサンプラー Texture0 から色を取得しています。UV 座標として頂点シェーダから渡される値 UV を使います。

  • engine/shader/2d/default_texture.frag
#version 410

uniform sampler2D Texture0;

layout(std140) uniform PSConstants2D
{
    vec4 g_colorAdd;
    vec4 g_sdfParam;
    vec4 g_internal;    
};

//
// PSInput
//
layout(location = 0) in vec4 Color;
layout(location = 1) in vec2 UV;

//
// PSOutput
//
layout(location = 0) out vec4 FragColor;

void main()
{
    vec4 texColor = texture(Texture0, UV);

    FragColor = (texColor * Color) + g_colorAdd;
}

カスタムピクセルシェーダの使用

シェーダファイルからシェーダプログラムを読み込むには PixelShader クラスを使います。PixelShader のコンストラクタ引数に、読み込みたいシェーダファイルのパスを渡します。このファイルパスは、実行ファイルがあるフォルダ(開発中は App フォルダ)を基準とする相対パスか、絶対パスを使用します。リリース用のアプリを作るときには、のちの章で説明する「リソース」パスの使用を推奨します。クロスプラットフォーム開発では、プラットフォームによって異なるシェーダファイルの拡張子を使い分けるために SIV3D_SELECT_SHADER マクロを使います。 PixelShader のコンストラクタの第二引数には、定数バッファの名前と定数インデックスの対応付けのために { { U"PSConstants2D", 0 } } を記述します。2D 描画のピクセルシェーダにおいては、Siv3D 内部の処理のために、U"PSConstants2D" という名前の定数バッファをインデックス 0 に常に設定しておく必要があります。ロードに失敗したかどうかは if (!ps) で調べられます。

ScopedCustomShader2D オブジェクトのコンストラクタに、ロードしたピクセルシェーダを渡すと、そのオブジェクトのスコープが有効な間、図形やテクスチャがそのカスタムピクセルシェーダによって描画されます。次のサンプルプログラムでは、テクスチャの R 成分と B 成分を入れ替えて描画するカスタムピクセルシェーダを使用します。

# include <Siv3D.hpp>

void Main()
{
    const Texture windmill(U"example/windmill.png");

    // R と B を入れ替えるピクセルシェーダ
    // シェーダファイルの拡張子は、Windows では hlsl, macOS/Linux では frag を選択
    // {} 内は定数バッファの名前と、対応する定数インデックス
    const PixelShader ps(U"example/shader/2d/rgb_to_bgr" SIV3D_SELECT_SHADER(U".hlsl", U".frag"),
        { { U"PSConstants2D", 0 } });

    if (!ps)
    {
        throw Error(U"Failed to load a shader file");
    }

    while (System::Update())
    {
        {
            // R と B を入れ替えるピクセルシェーダを開始
            ScopedCustomShader2D shader(ps);
            windmill.draw(10, 10);
        }
    }
}

コンパイル済みシェーダの使用 (Windows のみ)

Platform::Windows::Shader::CompileHLSLToFile() 関数を使うと、HLSL ファイルをあらかじめコンパイルしておくことができ、実行時の処理を削減することができます。コンパイル済みのシェーダは PixelShader でそのまま読み込めます。アプリケーションをリリースするときには、全てのシェーダをコンパイル済みにしておくことを推奨します。

# include <Siv3D.hpp>

void Main()
{
    const Texture windmill(U"example/windmill.png");

# if SIV3D_PLATFORM(WINDOWS)

    // コンパイル済みシェーダを作って保存する(1 度だけ作成すれば OK)
    //Platform::Windows::Shader::CompileHLSLToFile(
    //  U"example/shader/2d/rgb_to_bgr.hlsl",
    //  U"example/shader/2d/rgb_to_bgr.ps",
    //  ShaderStage::Pixel, U"PS");

# endif

    // R と B を入れ替えるピクセルシェーダ
    // シェーダファイルの拡張子は、Windows では ps, macOS/Linux では frag を選択
    // {} 内は定数バッファの名前と、対応する定数インデックス
    const PixelShader ps(U"example/shader/2d/rgb_to_bgr" SIV3D_SELECT_SHADER(U".ps", U".frag"),
        { { U"PSConstants2D", 0 } });

    if (!ps)
    {
        throw Error(U"Failed to load a shader file");
    }

    while (System::Update())
    {
        {
            // R と B を入れ替えるピクセルシェーダを開始
            ScopedCustomShader2D shader(ps);
            windmill.draw(10, 10);
        }
    }
}

カスタムピクセルシェーダのサンプル

RGB シフト

# include <Siv3D.hpp>

void Main()
{
    const Texture windmill(U"example/windmill.png");

    // RGB シフト用のピクセルシェーダ
    // シェーダファイルの拡張子は、Windows では hlsl, macOS/Linux では frag を選択
    // {} 内は定数バッファの名前と、対応する定数インデックス
    const PixelShader ps(U"example/shader/2d/rgb_shift" SIV3D_SELECT_SHADER(U".hlsl", U".frag"),
        { { U"PSConstants2D", 0 } });

    if (!ps)
    {
        throw Error(U"Failed to load a shader file");
    }

    while (System::Update())
    {
        {
            // RGB シフト用のシェーダを開始
            ScopedCustomShader2D shader(ps);
            windmill.draw(10, 10);
        }
    }
}

グレースケール化

# include <Siv3D.hpp>

void Main()
{
    const Texture windmill(U"example/windmill.png");

    // グレースケール化するピクセルシェーダ
    // シェーダファイルの拡張子は、Windows では hlsl, macOS/Linux では frag を選択
    // {} 内は定数バッファの名前と、対応する定数インデックス
    const PixelShader ps(U"example/shader/2d/grayscale" SIV3D_SELECT_SHADER(U".hlsl", U".frag"),
        { { U"PSConstants2D", 0 } });

    if (!ps)
    {
        throw Error(U"Failed to load a shader file");
    }

    while (System::Update())
    {
        {
            // グレースケール化するピクセルシェーダを開始
            ScopedCustomShader2D shader(ps);
            windmill.draw(10, 10);
        }
    }
}

ポスタライズ

# include <Siv3D.hpp>

void Main()
{
    const Texture windmill(U"example/windmill.png");

    // ポスタライズ化するピクセルシェーダ
    // シェーダファイルの拡張子は、Windows では hlsl, macOS/Linux では frag を選択
    // {} 内は定数バッファの名前と、対応する定数インデックス
    const PixelShader ps(U"example/shader/2d/posterize" SIV3D_SELECT_SHADER(U".hlsl", U".frag"),
        { { U"PSConstants2D", 0 } });

    if (!ps)
    {
        throw Error(U"Failed to load a shader file");
    }

    while (System::Update())
    {
        {
            // ポスタライズ化するピクセルシェーダを開始
            ScopedCustomShader2D shader(ps);
            windmill.draw(10, 10);
        }
    }
}

Poisson-Disk Sampling

定数バッファを使うと、C++ プログラムからシェーダプログラムに値を渡せます。定数バッファにする構造体は 16 の倍数のサイズで用意します。1 つの定数バッファの最大サイズは 64KB です。

カスタムピクセルシェーダで新しい定数バッファを使うには、シェーダプログラムに定数バッファを追加し、PixelShader のコンストラクタの第二引数に、対応する追加の定数バッファの名前とインデックスを追加します。なお、デフォルトで存在する PSConstants2D 定数バッファを変更することはできません。

C++ プログラムでは、ConstantBuffer<> クラステンプレートで定数バッファにする構造体のラッパーを作り、このクラス経由で値を操作します。描画前に、Graphics2D::SetConstantBuffer() によって、適切な定数バッファのインデックスにデータを転送します。

# include <Siv3D.hpp>

// 定数バッファ (PS_1)
struct PoissonDisk
{
    // 1 ピクセルあたりの UV サイズ
    Float2 pixelSize;

    // サンプリング半径
    float diskRadius;

    // 定数バッファのサイズを
    // 16 の倍数にするためのパディング
    float _unused = 0.0f;
};

void Main()
{
    const Texture windmill(U"example/windmill.png");

    // Poisson-Disk Sampling 用のピクセルシェーダ
    // シェーダファイルの拡張子は、Windows では hlsl, macOS/Linux では frag を選択
    // {} 内は定数バッファの名前と、対応する定数インデックス
    const PixelShader ps(U"example/shader/2d/poisson_disk" SIV3D_SELECT_SHADER(U".hlsl", U".frag"),
        { { U"PSConstants2D", 0 }, { U"PoissonDisk", 1 } });

    if (!ps)
    {
        throw Error(U"Failed to load a shader file");
    }

    // 定数バッファ
    ConstantBuffer<PoissonDisk> cb;
    cb->pixelSize = Float2(1.0, 1.0) / windmill.size();

    // サンプリング半径
    double diskRadius = 0.0;

    while (System::Update())
    {
        // サンプリング半径をスライダーで変更
        SimpleGUI::Slider(U"diskRadius", diskRadius, 0.0, 8.0, Vec2(10, 340), 120, 200);

        cb->diskRadius = static_cast<float>(diskRadius);

        {
            // 定数バッファを、ピクセルシェーダの定数バッファインデックス 1 に設定
            Graphics2D::SetConstantBuffer(ShaderStage::Pixel, 1, cb);

            // Poisson-Disk Sampling 用のシェーダを開始
            ScopedCustomShader2D shader(ps);
            windmill.draw(10, 10);
        }
    }
}

渦巻き効果

# include <Siv3D.hpp>

// 定数バッファ (PS_1)
struct Swirl
{
    // 回転
    float angle;

    // 定数バッファのサイズを
    // 16 の倍数にするためのパディング
    Float3 unused = {};
};

void Main()
{
    const Texture windmill(U"example/windmill.png");

    // 渦巻き効果のピクセルシェーダ
    // シェーダファイルの拡張子は、Windows では hlsl, macOS/Linux では frag を選択
    // {} 内は定数バッファの名前と、対応する定数インデックス
    const PixelShader ps(U"example/shader/2d/swirl" SIV3D_SELECT_SHADER(U".hlsl", U".frag"),
        { { U"PSConstants2D", 0 }, { U"Swirl", 1 } });

    if (!ps)
    {
        throw Error(U"Failed to load a shader file");
    }

    // 定数バッファ
    ConstantBuffer<Swirl> cb;

    while (System::Update())
    {
        cb->angle = static_cast<float>(Math::Sin(Scene::Time()) * 720_deg);

        {
            // 定数バッファを設定
            Graphics2D::SetConstantBuffer(ShaderStage::Pixel, 1, cb);

            // 渦巻き効果のシェーダを開始
            ScopedCustomShader2D shader(ps);
            windmill.draw(10, 10);
        }
    }
}

テクスチャのブレンド

通常のテクスチャ描画のピクセルシェーダプログラムでは、描画で使うテクスチャがテクスチャインデックス 0 にセットされています。Graphics2D::SetTexture() を使うと、それ以外のスロットに追加のテクスチャをセットして、シェーダプログラムで複数のテクスチャを参照できるようになります。Graphics2D::SetTexture()none を渡すことでスロットにセットしたテクスチャを解除できます。

# include <Siv3D.hpp>

void Main()
{
    const Texture emojiCat(Emoji(U"🐈"));

    const Texture windmill(U"example/windmill.png", TextureDesc::Mipped);

    // マルチテクスチャによるブレンドのピクセルシェーダ
    // シェーダファイルの拡張子は、Windows では hlsl, macOS/Linux では frag を選択
    // {} 内は定数バッファの名前と、対応する定数インデックス
    const PixelShader ps(U"example/shader/2d/multi_texture_blend" SIV3D_SELECT_SHADER(U".hlsl", U".frag"),
        { { U"PSConstants2D", 0 } });

    if (!ps)
    {
        throw Error(U"Failed to load a shader file");
    }

    while (System::Update())
    {
        // windmill を インデックス [1] のテクスチャスロットにセット 
        Graphics2D::SetTexture(1, windmill);
        {
            // マルチテクスチャによるブレンドのシェーダを開始
            ScopedCustomShader2D shader(ps);
            emojiCat.scaled(2).drawAt(Scene::Center());
        }
    }
}

テクスチャのマスク

# include <Siv3D.hpp>

void Main()
{
    // ウィンドウを 960x600 にリサイズ
    Window::Resize(960, 600);

    // シーンの背景色を淡い水色に設定
    Scene::SetBackground(ColorF(0.8, 0.9, 1.0));

    // マルチテクスチャによるマスクのピクセルシェーダ
    // シェーダファイルの拡張子は、Windows では hlsl, macOS/Linux では frag を選択
    // {} 内は定数バッファの名前と、対応する定数インデックス
    const PixelShader ps(U"example/shader/2d/multi_texture_mask" SIV3D_SELECT_SHADER(U".hlsl", U".frag"),
        { { U"PSConstants2D", 0 } });

    if (!ps)
    {
        throw Error(U"Failed to load a shader file");
    }

    // 絵文字のシルエット画像
    const Texture emoji(Emoji::CreateSilhouetteImage(U"🍎"), TextureDesc::Mipped);

    // 風車の写真
    const Texture windmill(U"example/windmill.png");

    // レンダーテクスチャを作成
    RenderTexture rt(480, 320);

    while (System::Update())
    {
        // レンダーテクスチャをクリア
        rt.clear(ColorF(0.0, 1.0));
        {
            // レンダーターゲットを rt に設定
            ScopedRenderTarget2D target(rt);
            emoji.scaled(2).rotated(Scene::Time() * 60_deg).drawAt(rt.size() / 2);
        }

        // 描画された rt を表示
        rt.draw(0, 140);

        // rt を インデックス [1] のテクスチャスロットにセット 
        Graphics2D::SetTexture(1, rt);
        {
            // マルチテクスチャによるマスクのシェーダを開始
            ScopedCustomShader2D shader(ps);
            windmill.draw(480, 140);
        }
    }
}

GPU 上でのライフゲームシミュレーション

ライフゲームをピクセルシェーダで実行するプログラムです。

# include <Siv3D.hpp>

// 定数バッファ (PS_1)
struct GameOfLife
{
    Float2 pixelSize;

    // 定数バッファのサイズを
    // 16 の倍数にするためのパディング    
    Float2 _unused = {};
};

void Main()
{
    // ウィンドウを 1280x720 にリサイズ
    Window::Resize(1280, 720);

    // セルの数 (1280x720)
    constexpr Size FieldSize(1280, 720);

    // ライフゲーム用のピクセルシェーダ
    // シェーダファイルの拡張子は、Windows では hlsl, macOS/Linux では frag を選択
    // {} 内は定数バッファの名前と、対応する定数インデックス
    const PixelShader ps(U"example/shader/2d/game_of_life" SIV3D_SELECT_SHADER(U".hlsl", U".frag"),
        { { U"PSConstants2D", 0 }, { U"GameOfLife", 1 } });

    // 定数バッファ
    const ConstantBuffer<GameOfLife> cb({ Float2(1.0f, 1.0f) / FieldSize });

    // レンダーテクスチャ 0
    RenderTexture rt0(Image(FieldSize, Arg::generator = []() { return Color(RandomBool() * 255); }));

    // レンダーテクスチャ 1
    RenderTexture rt1(FieldSize, ColorF(0.0));

    while (System::Update())
    {
        {
            // テクスチャフィルタなし
            ScopedRenderStates2D sampler(SamplerState::ClampNearest);

            // 現在の状態を画面に描く
            rt0.draw(ColorF(0.0, 1.0, 0.0));

            {
                // ライフゲーム用のシェーダ
                Graphics2D::SetConstantBuffer(ShaderStage::Pixel, 1, cb);
                ScopedCustomShader2D shader(ps);

                // 更新後の状態を描く rt1 に描く
                ScopedRenderTarget2D target(rt1);
                rt0.draw();
            }
        }

        // rt0 と rt1 を入れ替える
        std::swap(rt0, rt1);
    }
}

GPU 上でのライフゲームシミュレーション(2D カメラ操作対応版)

マウスやキーを使った 2D カメラ操作で拡大縮小や移動ができます。

# include <Siv3D.hpp>

// 定数バッファ (PS_1)
struct GameOfLife
{
    Float2 pixelSize;

    // 定数バッファのサイズを
    // 16 の倍数にするためのパディング
    Float2 _unused = {};
};

void Main()
{
    // ウィンドウを 1280x720 にリサイズ
    Window::Resize(1280, 720);

    // シーンの背景色をグレーに設定
    Scene::SetBackground(ColorF(0.5));

    // セルの数 (2048x2048), 最大 (8192x8192)
    constexpr Size FieldSize(2048, 2048);

    // ライフゲーム用のピクセルシェーダ
    // シェーダファイルの拡張子は、Windows では hlsl, macOS/Linux では frag を選択
    // {} 内は定数バッファの名前と、対応する定数インデックス
    const PixelShader ps(U"example/shader/2d/game_of_life" SIV3D_SELECT_SHADER(U".hlsl", U".frag"),
        { { U"PSConstants2D", 0 }, { U"GameOfLife", 1 } });

    // 定数バッファ
    const ConstantBuffer<GameOfLife> cb({ Float2(1.0f, 1.0f) / FieldSize });

    // レンダーテクスチャ 0
    RenderTexture rt0(Image(FieldSize, Arg::generator = []() { return Color(RandomBool() * 255); }));

    // レンダーテクスチャ 1
    RenderTexture rt1(FieldSize, ColorF(0.0));

    // 2D カメラ
    Camera2D camera(Vec2(0, 0), 4);

    while (System::Update())
    {
        // 2D カメラを更新
        camera.update();
        {
            // テクスチャフィルタなし
            ScopedRenderStates2D sampler(SamplerState::ClampNearest);

            {
                // 2D カメラの設定から Transformer2D を作成
                const auto t = camera.createTransformer();

                // 現在の状態を画面に描く
                rt0.draw(ColorF(0.0, 1.0, 0.0));

                // 2D カメラの UI を描画
                camera.draw(Palette::Orange);
            }

            {
                // ライフゲーム用のシェーダ
                Graphics2D::SetConstantBuffer(ShaderStage::Pixel, 1, cb);
                ScopedCustomShader2D shader(ps);

                // 更新後の状態を描く rt1 に描く
                ScopedRenderTarget2D target(rt1);
                rt0.draw();
            }
        }

        // rt0 と rt1 を入れ替える
        std::swap(rt0, rt1);
    }
}

ゲーム画面に後処理としてシェーダを適用

レンダーテクスチャにゲームのグラフィックスを描画し、そのテクスチャをシーンに描画する際にシェーダを適用することで、ゲーム画面全体に後処理としてカスタムピクセルシェーダを適用できます。

# include <Siv3D.hpp>

// 渦巻き効果のピクセルシェーダ用の
// 定数バッファ (PS_1)
struct Swirl
{
    // 回転
    float angle;

    // 定数バッファのサイズを
    // 16 の倍数にするためのパディング
    Float3 unused = {};
};

void Main()
{
    // ゲームの描画用のレンダーテクスチャ
    MSRenderTexture rt(Scene::Size());

    // グレースケール化するピクセルシェーダ
    // シェーダファイルの拡張子は、Windows では hlsl, macOS/Linux では frag を選択
    // {} 内は定数バッファの名前と、対応する定数インデックス
    const PixelShader psGrayscale(U"example/shader/2d/grayscale" SIV3D_SELECT_SHADER(U".hlsl", U".frag"),
        { { U"PSConstants2D", 0 } });
    if (!psGrayscale)
    {
        throw Error(U"Failed to load a shader file");
    }

    // 渦巻き効果のピクセルシェーダ
    // シェーダファイルの拡張子は、Windows では hlsl, macOS/Linux では frag を選択
    // {} 内は定数バッファの名前と、対応する定数インデックス
    const PixelShader psSwirl(U"example/shader/2d/swirl" SIV3D_SELECT_SHADER(U".hlsl", U".frag"),
        { { U"PSConstants2D", 0 }, { U"Swirl", 1 } });
    if (!psSwirl)
    {
        throw Error(U"Failed to load a shader file");
    }

    // 渦巻き効果のピクセルシェーダ用の定数バッファ
    ConstantBuffer<Swirl> cb;

    // ガウスぼかしの中間で使うレンダーテクスチャ
    RenderTexture rtA(rt.size()), rtB(rt.size());
    RenderTexture rtA4(rt.size() / 4), rtB4(rt.size() / 4);
    RenderTexture rtA8(rt.size() / 8), rtB8(rt.size() / 8);

    // ゲーム画面に適用するエフェクト
    size_t effectIndex = 0;

    // 背景色
    constexpr ColorF backgroundColor(0.3, 0.4, 0.5);

    // ブロックのサイズ
    constexpr Size blockSize(40, 20);

    // ブロックの配列
    Array<Rect> blocks;

    // 横 (Scene::Width() / blockSize.x) 個、縦 5 個のブロックを配列に追加する
    for (auto p : step(Size((Scene::Width() / blockSize.x), 5)))
    {
        blocks << Rect(p.x * blockSize.x, 60 + p.y * blockSize.y, blockSize);
    }

    // ボールの速さ
    constexpr double speed = 480.0;

    // ボールの速度
    Vec2 ballVelocity(0, -speed);

    // ボール
    Circle ball(400, 400, 8);

    // 自動プレイ用のパラメータ
    double paddleCenter = 400;
    double randomOffset = 0.0;

    while (System::Update())
    {
        // 自動プレイ
        paddleCenter = Math::Damp(paddleCenter, ball.x + ballVelocity.x*1.2 + randomOffset, 0.9, Scene::DeltaTime());

        // パドル
        const RectF paddle(Arg::center(paddleCenter, 500), 120, 10);

        // ボールを移動
        ball.moveBy(ballVelocity * Scene::DeltaTime());

        // ブロックを順にチェック
        for (auto it = blocks.begin(); it != blocks.end(); ++it)
        {
            // ボールとブロックが交差していたら
            if (it->intersects(ball))
            {
                // ボールの向きを反転する
                (it->bottom().intersects(ball) || it->top().intersects(ball) ? ballVelocity.y : ballVelocity.x) *= -1;

                // ブロックを配列から削除(イテレータが無効になるので注意)
                blocks.erase(it);

                // これ以上チェックしない  
                break;
            }
        }

        // 天井にぶつかったらはね返る
        if (ball.y < 0 && ballVelocity.y < 0)
        {
            ballVelocity.y *= -1;
        }

        // 左右の壁にぶつかったらはね返る
        if ((ball.x < 0 && ballVelocity.x < 0) || (Scene::Width() < ball.x && ballVelocity.x > 0))
        {
            ballVelocity.x *= -1;
        }

        // パドルにあたったらはね返る
        if (ballVelocity.y > 0 && paddle.intersects(ball))
        {
            // パドルの中心からの距離に応じてはね返る向きを変える
            ballVelocity = Vec2((ball.x - paddle.center().x) * 10, -ballVelocity.y).setLength(speed);
            randomOffset = Random(-40, 40);
        }

        // レンダーテクスチャをクリア
        rt.clear(backgroundColor);
        {
            // レンダーターゲットを rt に設定
            ScopedRenderTarget2D target(rt);

            for (auto y : Range(1, 5))
            {
                Line(0, y * 100, 800, y * 100).draw(1, Palette::Gray);
            }

            for (auto x : Range(1, 7))
            {
                Line(x * 100, 0, x * 100, 600).draw(1, Palette::Gray);
            }

            // すべてのブロックを描画する
            for (const auto& block : blocks)
            {
                block.stretched(-1).draw(HSV(block.y - 40));
            }

            // ボールを描く
            ball.draw();

            // パドルを描く
            paddle.draw();
        }

        // resolve のために描画を完了させる
        Graphics2D::Flush();

        // multi-sample のテクスチャを resolve して
        // multi-sample ではない、描画可能なテクスチャを得る
        rt.resolve();

        if (effectIndex == 0) // ゲーム画面をそのまま描画
        {
            rt.draw();
        }
        else if (effectIndex == 1) // ゲーム画面をグレースケール化して描画
        {
            // グレースケール化するピクセルシェーダを開始
            ScopedCustomShader2D shader(psGrayscale);

            rt.draw();
        }
        else if (effectIndex == 2) // ゲーム画面を渦巻き効果で描画
        {
            cb->angle = static_cast<float>(Math::Sin(Scene::Time()) * 240_deg);

            // 定数バッファを設定
            Graphics2D::SetConstantBuffer(ShaderStage::Pixel, 1, cb);

            // 渦巻き効果のシェーダを開始
            ScopedCustomShader2D shader(psSwirl);

            rt.draw();
        }
        else if (effectIndex == 3) // ゲーム画面をガウスぼかしで描画
        {
            // [オリジナル]->[ガウスぼかし]->[1/4サイズ]->[ガウスぼかし]
            Shader::GaussianBlur(rt, rtB, rtA);
            Shader::Downsample(rtA, rtA4);
            Shader::GaussianBlur(rtA4, rtB4, rtA4);
            Shader::Downsample(rtA4, rtA8);
            Shader::GaussianBlur(rtA8, rtB8, rtA8);

            rtA8.scaled(8).draw();
        }

        // エフェクトの種類の選択
        SimpleGUI::RadioButtons(effectIndex, { U"Default", U"Grayscale", U"Swirl", U"GaussianBlur" }, Vec2(10, 10));
    }
}

5. 子プロセスの作成

別のプログラムを「子プロセス」として起動、管理できるようになりました。別のアプリケーションを起動したり、別のプログラムと情報をやり取りする際に使えます。

# include <Siv3D.hpp>

void Main()
{
# if SIV3D_PLATFORM(WINDOWS)

    // 子プロセスで実行するファイルのパス
    const FilePath path = U"C:/Windows/System32/notepad.exe";

# elif SIV3D_PLATFORM(MACOS)

    // 子プロセスで実行するファイルのパス
    const FilePath path = U"/Applications/Calculator.app/Contents/MacOS/Calculator";

# endif

    // 子プロセスを作成
    ChildProcess child = Process::Spawn(path);

    if (!child)
    {
        throw Error(U"Failed to create a process");
    }

    while (System::Update())
    {
        ClearPrint();

        // プロセスが実行中かを取得
        Print << child.isRunning();

        // プロセスが終了した場合、その終了コード
        Print << child.getExitCode();

        if (child.isRunning())
        {
            if (SimpleGUI::Button(U"Terminate", Vec2(100, 20)))
            {
                // プロセスを強制終了
                child.terminate();
            }
        }
    }
}

子プロセスとの標準入出力のパイプライン処理

子プロセスとのパイプライン処理によって、一方の標準出力を他方の標準入力とすることができます。次のサンプルでは、"Console" は通常の C++ コンソールプロジェクトとしてビルドします。

# include <iostream>

int main()
{
    int a, b;
    std::cin >> a >> b;
    std::cout << (a + b) << std::endl;
}
# include <Siv3D.hpp>

void Main()
{
# if SIV3D_PLATFORM(WINDOWS)

    // 子プロセスで実行するファイルのパス
    const FilePath path = U"Console.exe";

# else

    // 子プロセスで実行するファイルのパス
    const FilePath path = U"Console";

# endif

    // 子プロセスを作成(パイプライン処理)
    ChildProcess child = Process::Spawn(path, Pipe::StdInOut);

    if (!child)
    {
        throw Error(U"Failed to create a process");
    }

    child.ostream() << 10 << std::endl;
    child.ostream() << 20 << std::endl;

    int32 result;
    child.istream() >> result;
    Print << U"result: " << result;

    while (System::Update())
    {

    }
}

6. 実験的な 3D 描画対応

実験的な 3D 機能が実装されました。ただし、2D 描画で 3D をエミュレートする簡易的なものなので、次のような制約があります。

  • 深度バッファが無いので前後判定ができない
  • 遠近クリップが無いのでカメラに近すぎるオブジェクトが正しく表示されない

3D Triangles

# include <Siv3D.hpp>

void Main()
{
    constexpr std::array<Vec3, 8> vertices =
    {
        Vec3(-1, 1, -1),
        Vec3(1, 1, -1),
        Vec3(-1, -1, -1),
        Vec3(1, -1, -1),
        Vec3(1, 1, 1),
        Vec3(-1, 1, 1),
        Vec3(1, -1, 1),
        Vec3(-1, -1, 1),
    };

    constexpr std::array<uint32, 36> indices =
    {
        0, 1, 2, 2, 1, 3,
        5, 4, 0, 0, 4, 1,
        1, 4, 3, 3, 4, 6,
        5, 0, 7, 7, 0, 2,
        4, 5, 6, 6, 5, 7,
        2, 3, 7, 7, 3, 6,
    };

    constexpr double fov = 45_deg;
    constexpr Vec3 focusPosition(0, 0, 0);
    Vec3 eyePosition(0, 4, 0);
    experimental::BasicCamera3D camera(Scene::Size(), fov, eyePosition, focusPosition);

    while (System::Update())
    {
        eyePosition = Cylindrical(8, Scene::Time() * 30_deg, Math::Sin(Scene::Time()) * 4);
        camera.setView(eyePosition, focusPosition);
        const Mat4x4 mat = camera.getMat4x4();

        {
            ScopedRenderStates2D culling(RasterizerState::SolidCullBack);

            for (auto i : step(12))
            {
                const Vec3 p0(vertices[indices[i * 3 + 0]]);
                const Vec3 p1(vertices[indices[i * 3 + 1]]);
                const Vec3 p2(vertices[indices[i * 3 + 2]]);

                experimental::Triangle3D(p0, p1, p2).draw(mat, HSV(i * 30));
            }
        }
    }
}

3D AABB

# include <Siv3D.hpp>

void Main()
{
    constexpr double fov = 45_deg;
    constexpr Vec3 focusPosition(0, 0, 0);
    Vec3 eyePosition(0, 10, 0);
    experimental::BasicCamera3D camera(Scene::Size(), fov, eyePosition, focusPosition);

    while (System::Update())
    {
        eyePosition = Cylindrical(20, Scene::Time() * 30_deg, 8 + Periodic::Sine0_1(4s) * 8);
        camera.setView(eyePosition, focusPosition);
        const Mat4x4 mat = camera.getMat4x4();

        {
            ScopedRenderStates2D culling(RasterizerState::SolidCullBack);

            for (auto i : Range(-10, 10))
            {
                experimental::Line3D(Vec3(-10, 0, i), Vec3(10, 0, i)).draw(mat, ColorF(0.5));
                experimental::Line3D(Vec3(i, 0, -10), Vec3(i, 0, 10)).draw(mat, ColorF(0.5));
            }

            experimental::AABB(Vec3(0, 1, 0), Vec3(2, 2, 2)).draw(mat, Palette::White);
            experimental::AABB(Vec3(-8, 1, 8), Vec3(2, 2, 2)).draw(mat, HSV(0));
            experimental::AABB(Vec3(8, 1, 8), Vec3(2, 2, 2)).draw(mat, HSV(90));
            experimental::AABB(Vec3(8, 1, -8), Vec3(2, 2, 2)).draw(mat, HSV(270));
            experimental::AABB(Vec3(-8, 1, -8), Vec3(2, 2, 2)).draw(mat, HSV(180));
        }
    }
}

3D Terrain

マウスクリックで、左上の高さマップに山を描きます。

# include <Siv3D.hpp>

void Main()
{
    Window::Resize(1280, 720);
    Scene::SetBackground(ColorF(0.05, 0.3, 0.7));

    RenderTexture rt(100, 100, ColorF(0.0), TextureFormat::R32_Float);
    Grid<float> heightMap;
    Grid<Float3> positions;

    constexpr double fov = 45_deg;
    constexpr Vec3 focusPosition(50, 0, -50);
    Vec3 eyePosition(0, 100, 0);
    experimental::BasicCamera3D camera(Scene::Size(), fov, eyePosition, focusPosition);

    while (System::Update())
    {
        eyePosition = Cylindrical(Arg::r = 80, Arg::phi = Scene::Time() * 30_deg, Arg::y = 50) + Vec3(50, 0, -50);
        camera.setView(eyePosition, focusPosition);
        const Mat4x4 mat = camera.getMat4x4();

        rt.read(heightMap);
        {
            positions.resize(heightMap.size());

            for (auto p : step(heightMap.size()))
            {
                positions[p] = Float3(p.x, heightMap[p], -p.y);
            }
        }

        {
            ScopedRenderTarget2D target(rt);
            ScopedRenderStates2D blend(BlendState::Additive);

            if (MouseL.pressed())
            {
                Circle(Cursor::Pos(), 8).draw(ColorF(Scene::DeltaTime() * 24.0));
            }
        }

        if (positions)
        {
            ScopedRenderStates2D culling(RasterizerState::SolidCullBack);

            for (auto x : step(positions.width() - 1))
            {
                for (auto y : step(positions.height()))
                {
                    const Float3 begin = positions[{x, y}];
                    const Float3 end = positions[{x + 1, y}];
                    const ColorF color = HSV(120 - (begin.y + end.y) * 3, 0.75, 0.7);
                    experimental::Line3D(begin, end).draw(mat, color);
                }
            }

            for (auto x : step(positions.width()))
            {
                for (auto y : step(positions.height() - 1))
                {
                    const Float3 begin = positions[{x, y}];
                    const Float3 end = positions[{x, y + 1}];
                    const ColorF color = HSV(120 - (begin.y + end.y) * 3, 0.75, 0.7);
                    experimental::Line3D(begin, end).draw(mat, color);
                }
            }
        }

        rt.draw(ColorF(0.1));
    }
}