OpenSiv3D マスク処理

# はじめに

v0.4.3現在のOpenSiv3Dではユーザーコードとしてステンシルステートが使用できません。

そのため、マスク処理を行う方法としてレンダーテクスチャとピクセルシェーダーを使用することになります。

OpenSiv3Dの公式ページでもサンプルコードがあります
https://siv3d.github.io/ja-jp/news/v041/

しかし、実際にやりたいことは、このサンプルのようにうまくいかなかったりします。。

なので細かいところも調整したマスク処理を実装しました。

# 実装

## マスク用のシルエットどう作るんだ問題

サンプルコードでは以下のようにマスク用のシルエットを作成している。

`const Texture emoji(Emoji::CreateSilhouetteImage(U"🍎"), TextureDesc::Mipped);`

しかし、実際にはもっと複数のテクスチャや図形が描画された**最終結果**に対してマスク処理をしたいことがあるため、このようにシルエットのTextureを事前に作成しておくのは難しそうです。

なので、マスクされるものを描画するレンダーテクスチャとマスク用のシルエットを描画するレンダーテクスチャの2つを使用することで、最終結果に対するマスク処理を実現します。

そのためレンダーテクスチャのサイズはスクリーンサイズと同じにします。

```cpp
        RenderTexture rt(width, height); // スクリーンのサイズにする
```

また、サンプルのシェーダーではシルエット画像のR値をアルファ値として使用しています。


```cpp:multi_texture_mask.hlsl
	float mask = g_texture1.Sample(g_sampler1, input.uv).r;

	texColor.a *= mask;
```

しかし、この方法だと任意のテクスチャをシルエットとして使用したい場合にもとのテクスチャのR値をアルファ値グレイスケールなどで何かしら変換する必要がでてくるため、少し都合が悪いです。
(それ用のシェーダーを書いてもいいですが、基本図形の描画はカスタムピクセルシェーダーが適用されなかったりするので使用する基本図形の場合は描画する色を意識しなければいけなくなる)

そのため、今回はR値ではなく、**アルファ値を使用したシルエット**にしようと思います。
そのシルエット作成にはブレンドステートを使用します。

```cpp



		rt.clear(ColorF(0.0, 0.0)); // アルファ値を採用するのでクリアーカラーもアルファ0
		{
			static BlendState blend{
				true,
				Blend::Zero, // RGBはみないのでなんでもいい
				Blend::Zero,
				BlendOp::Add,
				Blend::One, // アルファ値加算 (srcA *1.0 + destA *1.0)
				Blend::One,
			};
			ScopedRenderStates2D state(blend);
			ScopedRenderTarget2D target(rt);
            // マスク用シルエットを描画する関数
			mask();
		}
```

今回に関しては、アルファ値をシンプルな加算としました。

これでシルエット用のテクスチャが完成しました。

# RenderTexture使用によるアルファ値問題

先にも述べたようにマスクされるものを描画するレンダーテクスチャもスクリーンサイズで用意します。
これはシルエットのテクスチャサイズとマスクする領域のサイズを合わせるためです。

```cpp
        RenderTexture rt2(width, height); // スクリーンのサイズにする
```

見出しのタイトルにも書いたようにレンダーテクスチャを経由して描画すると描画したもののアルファ値が、レンダーテクスチャのクリアカラーに対して計算されてしまうので、想定していない色になってしまうことがあります。

こちらの詳細は省きます

参考
- https://www.slideshare.net/klab-tech/rendertexture
- https://qiita.com/luckin/items/43a8a98a88a3ba20e92b

そのため、これに関しても専用のブレンドステートを用意することで対処します

```cpp
		rt2.clear(ColorF(0.0, 1.0));
		{
            // レンダーテクスチャに描画するもの
			static BlendState blend {
				true, 
				Blend::SrcAlpha,
				Blend::InvSrcAlpha, 
				BlendOp::Add,
				Blend::Zero,
				Blend::InvSrcAlpha
			};
			ScopedRenderStates2D state(blend);
			ScopedRenderTarget2D target(rt2);
			drawer(); // マスクされるものを描画する関数
		}

		{
            // レンダーテクスチャの描画
			static BlendState blend{
				true,
				Blend::One,
				Blend::SrcAlpha
			};
			ScopedRenderStates2D state(blend);
			rt2.draw();
		}
```

## シェーダーによるマスク処理

2枚のレンダーテクスチャが準備できたのでシェーダーでマスク処理を行います。

### hlsl

```cpp:mask.hlsl
Texture2D		g_texture0 : register(t0);
Texture2D		g_texture1 : register(t1);
SamplerState	g_sampler0 : register(s0);
SamplerState	g_sampler1 : register(s1);

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

cbuffer ShaderParam : register(b1)
{
	int g_isEqual;
}

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);
	
	float mask = g_texture1.Sample(g_sampler1, input.uv).a;
	if (!g_isEqual) {
		mask = 1 - mask;
	}
	texColor.r *= mask;
	texColor.g *= mask;
	texColor.b *= mask;

	float invA = 1 - texColor.a;
	texColor.a = 1 - invA * mask;

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

全体としてはこんな感じです。

```cpp
    float mask = g_texture1.Sample(g_sampler1, input.uv).a;
    if (!g_isEqual) {
        mask = 1 - mask;
    }
```

定数バッファでフラグを渡しておくことで重なるところを描画するのか、描画しないのか切り替えれるようにしています。
また、先ほど述べたようにR値ではなくアルファ値をしようしてマスク処理を計算します。

```cpp
    texColor.r *= mask;
    texColor.g *= mask;
    texColor.b *= mask;

    float invA = 1 - texColor.a;
    texColor.a = 1 - invA * mask;
```

もともと `texColor.a *= mask` だったのが何故こうなってしまたかというと、先ほどのレンダーテクスチャによるアルファ問題解決のために使用したブレンドによって、アルファ値がずれてしまったりしているからです。

この時点でテクスチャは、**RGBはアルファ考慮済み、アルファ値は 1-元のアルファ値**となっているので

```haskell
RGB: srcRGB * srcA
A: 1 - srcA
```

RGBにmaskをかけることで、改めてRGBをアルファ考慮済みとし、アルファ値を元に戻した`invA`にmask値をかけたうえで再度反転しています。

### cpp

```cpp
    // マスク処理を切り替えるための列挙体
    enum class MaskFunc
    {
        Equal,
        NotEqual,
    };
```

```cpp
    const PixelShader maskPs(U"mask.hlsl", { { U"PSConstants2D", 0 } });
    struct ShaderParam
    {
        int isEqual;
        Float3 _unused;
    };
    ConstantBuffer<ShaderParam> cb;
```

```cpp
		rt2.clear(ColorF(0.0, 1.0));
		{
            // レンダーテクスチャに描画するもの
            省略
		}
        // シルエットテクスチャをセット
		Graphics2D::SetTexture(1, rt);
        // 定数バッファをセット
        m_cb->isEqual = func == MaskFunc::Equal ? 1 : 0;
		Graphics2D::SetConstantBuffer(ShaderStage::Pixel, 1, cb);
		{
			static BlendState blend{
				true,
				Blend::One,
				Blend::SrcAlpha
			};
			ScopedRenderStates2D state(blend);
            // マスク用のシェーダー使用
			ScopedCustomShader2D shader(maskPs);
			rt2.draw();
		}
```

## Transformを考慮する

Transformが変換されている場合、レンダーテクスチャに描画されるものも適用後で描画されます。
これ自体は問題ありませんが、変換後で描画されたレンダーテクスチャを最終的に描画する際に、このレンダーテクスチャまで変換がかかってしてしまうと2重にかかってしまいます。

そのため、最終的なレンダーテクスチャの描画時の変換行列は単位行列にしておくとよいです。

```cpp

		{
			static BlendState blend{
				true,
				Blend::One,
				Blend::SrcAlpha
			};
			ScopedRenderStates2D state(blend);
            // マスク用のシェーダー使用
			ScopedCustomShader2D shader(maskPs);
            // 変換行列を単位行列に戻しておくことで二重に変換されないようにする
            Transformer2D transLocal(Mat3x2::Identity(), Transformer2D::Target::SetLocal);
            Transformer2D transCamera(Mat3x2::Identity(), Transformer2D::Target::SetCamera);

			rt2.draw();
		}
```

## まとめ

一通りの処理をまとめた関数を作ります。
本来シェーダー等はstaticにすべきではない気がしますが、今回は処理の流れを見やすくするためと、楽なのでそうします。

```cpp
    enum class MaskFunc
    {
        Equal,
        NotEqual,
    };

	void MaskDraw(
        std::function<void()> mask,
        std::function<void()> drawer,
        MaskFunc func
    ) {
        // スクリーンサイズ任意の値
        constexpr uint32 width = 960;
        constexpr uint32 height = 540;

        static RenderTexture rt(width, height);
        static RenderTexture rt2(width, height);
        static const PixelShader maskPs(U"mask.hlsl", { { U"PSConstants2D", 0 } });

		struct ShaderParam
		{
			s3d::int32 isEqual;
			Float3 _unused;
		};
		static ConstantBuffer<ShaderParam> cb;
		cb->isEqual = func == MaskFunc::Equal;

		rt.clear(ColorF(0.0, 0.0));
		{
			static BlendState blend{
				true,
				Blend::Zero,
				Blend::Zero,
				BlendOp::Add,
				Blend::One,
				Blend::One,
			};
			ScopedRenderStates2D state(blend);
			ScopedRenderTarget2D target(rt);
			mask();
		}
		rt2.clear(ColorF(0.0, 1.0));
		{
			static BlendState blend {
				true, 
				Blend::SrcAlpha,
				Blend::InvSrcAlpha, 
				BlendOp::Add,
				Blend::Zero,
				Blend::InvSrcAlpha
			};
			ScopedRenderStates2D state(blend);
			ScopedRenderTarget2D target(rt2);
			drawer();
		}
		Graphics2D::SetTexture(1, rt);
		Graphics2D::SetConstantBuffer(ShaderStage::Pixel, 1, cb);
		{
			static BlendState blend{
				true,
				Blend::One,
				Blend::SrcAlpha
			};
			ScopedRenderStates2D state(blend);
			ScopedCustomShader2D shader(maskPs);
			Transformer2D transLocal(Mat3x2::Identity(), Transformer2D::Target::SetLocal);
			Transformer2D transCamera(Mat3x2::Identity(), Transformer2D::Target::SetCamera);
			rt2.draw();
		}
    }
```

# おまけ

Scoped版作ってみた

```cpp
        MaskShader mask(960, 540); // 任意のサイズ
        {
            auto scopedMask = mask.notEqual([] {
                // something mask draw
            });

            // something draw
        }
```

## 実装

<details>
<summary>実装</summary>
<div>

```cpp:MaskUtil.hpp
#pragma once
#include <functional>
#include <memory>
#include <Siv3D.hpp>

    enum class MaskFunc
    {
        Equal,
        NotEqual,
    };

    class MaskShader
    {
        class Impl;

        class ScopedMask
        {
            std::shared_ptr<Impl> m_parentImpl;
            MaskFunc m_func;
            std::function<void()> m_mask;

            s3d::BlendState m_oldBlend;
            s3d::Optional<s3d::RenderTexture> m_oldTarget;
            s3d::Optional <s3d::Rect> m_oldViewport;
        public:
            ScopedMask(const std::shared_ptr<Impl>& parentImpl, MaskFunc func, const std::function<void()>& mask);
            ~ScopedMask();
        };
    public:

        std::shared_ptr<Impl> m_pImpl;

    public:
        MaskShader(const Size& size);
        MaskShader(uint32 width, uint32 height);

        ScopedMask equal(const std::function<void()>& mask)const;
        ScopedMask notEqual(const std::function<void()>& mask)const;

    };
```
```cpp:MaskUtil.cpp

#include "MaskUtil.hpp"
#include <Siv3D.hpp>

	class MaskShader::Impl
	{
	private:
		RenderTexture m_rt;
		RenderTexture m_rt2;
		PixelShader m_maskPs;
		struct ShaderParam
		{
			s3d::int32 isEqual;
			Float3 _unused;
		};
		ConstantBuffer<ShaderParam> m_cb;
	public:
		Impl(uint32 width, uint32 height) :
			m_rt(width, height),
			m_rt2(width, height),
			m_maskPs(U"mask.hlsl", { { U"PSConstants2D", 0 } })
		{}

		void draw(MaskFunc func, const std::function<void()> mask)
		{
			m_rt.clear(ColorF(0.0, 0.0));
			{
				static BlendState blend{
					true,
					Blend::Zero,
					Blend::Zero,
					BlendOp::Add,
					Blend::One,
					Blend::One,
				};
				ScopedRenderStates2D state(blend);
				ScopedRenderTarget2D target(m_rt);
				mask();
			}
			Graphics2D::SetTexture(1, m_rt);

			m_cb->isEqual = func == MaskFunc::Equal ? 1 : 0;
			Graphics2D::SetConstantBuffer(ShaderStage::Pixel, 1, m_cb);

			{
				static BlendState blend{
					true,
					Blend::One,
					Blend::SrcAlpha
				};
				ScopedRenderStates2D state(blend);
				ScopedCustomShader2D shader(m_maskPs);

				Transformer2D transLocal(Mat3x2::Identity(), Transformer2D::Target::SetLocal);
				Transformer2D transCamera(Mat3x2::Identity(), Transformer2D::Target::SetCamera);

				m_rt2.draw();
			}
		}

		const RenderTexture& getDrawerTarget()
		{
			return m_rt2;
		}
	};
	MaskShader::MaskShader(const Size& size):
		MaskShader(size.x, size.y)
	{}
	MaskShader::MaskShader(uint32 width, uint32 height):
		m_pImpl(std::make_shared<Impl>(width, height))
	{}
	MaskShader::ScopedMask MaskShader::equal(const std::function<void()> & mask) const
	{
		return ScopedMask(this->m_pImpl, MaskFunc::Equal, mask);
	}
	MaskShader::ScopedMask MaskShader::notEqual(const std::function<void()>& mask) const
	{
		return ScopedMask(this->m_pImpl, MaskFunc::NotEqual, mask);
	}

	MaskShader::ScopedMask::ScopedMask(
		const std::shared_ptr<Impl>& parentImpl,
		MaskFunc func,
		const std::function<void()>& mask
	):
		m_parentImpl(parentImpl),
		m_func(func),
		m_mask(mask),
		m_oldBlend(Graphics2D::GetBlendState()),
		m_oldTarget(Graphics2D::GetRenderTarget()),
		m_oldViewport(Graphics2D::GetViewport())
	{
		static BlendState blend{ true, Blend::SrcAlpha, Blend::InvSrcAlpha, BlendOp::Add, Blend::Zero, Blend::InvSrcAlpha };
		Graphics2D::Internal::SetBlendState(blend);
		const auto& rt = m_parentImpl->getDrawerTarget();
		rt.clear(s3d::Palette::Black);
		Graphics2D::Internal::SetRenderTarget(rt);
		Graphics2D::Internal::SetViewport(Rect(rt.size()));
	}
	MaskShader::ScopedMask::~ScopedMask()
	{
		Graphics2D::Internal::SetBlendState(m_oldBlend);
		Graphics2D::Internal::SetRenderTarget(m_oldTarget);
		Graphics2D::Internal::SetViewport(m_oldViewport);

		m_parentImpl->draw(m_func, m_mask);
	}
```

</div>
</detail>