UnityでGraphics.DrawProcedural使ってたけど半透明処理とかで詰まって、CommandBuffer.DrawProceduralに切り替えてうまくいったよという話。


やりたいこと
全レイヤー

レイヤー1でDrawProceduralを使いつつ、一番手前レイヤーの半透明の矩形をうまく表示したかった。


しかし「Graphics.DrawProcedural」を使っていた当初はdamenahou

こんな感じに本来後ろに描画されてほしい粒子が半透明の矩形より手前に描画されてしまっていた。



どういうことかというと、最初から説明すると
全レイヤー

Zはマイナスのほうが「手前」。カメラの位置はz=-10.0
で、レイヤー0、2、3はそれぞれ2DスプライトをGameObjectとしてUnity画面上に配置している。レイヤーというのは頭の中で考えているだけで、実際の配置はUnity側でGameObjectの座標を0.01とか-0.02とかベタ打ちしている。
レイヤー1の流体の粒子だけは特殊で、65536*16個のドットの描画を必要とする。この大量のドットの描画の仕方としてはここを参考にOnRenderObject()内でGraphics.DrawProceduralを使うことで実現していた。いわゆるGPU Instancing機能だ。スクリプト側で各ドットの座標を記憶するCompute Bufferを生成しておき、Compute shaderで座標を更新して、最後にGraphics.DrawProceduralで描画といった形。
このGraphics.DrawProceduralは「即時実行」の命令だ。
https://docs.unity3d.com/ja/2017.4/ScriptReference/Graphics.DrawProcedural.html
(公式リファレンスより「この呼び出しは Graphics.DrawMeshNow のようにすぐに実行されることに注意してください。」)

さて、先ほどOnRenderObject()内でGraphics.DrawProceduralを実行すると言ったがこれはどのタイミングになるのか

http://staff.live2d.com/archives/55264880.html
このサイトを参考にさせて頂くと
Update()

中略

Camera1のレンダリング

中略

OnRenderObject()

・・・・
このようになる。
ということは全てのGameObjectが「Camera1のレンダリング」フェーズで描画された後に「OnRenderObject()」フェーズでGraphics.DrawProceduralが実行されていたことがわかる。これではGPUインスタンスで描画されるものがすべて手前にきてしまう!
damenahou

もう一度よくさっきの失敗画像を見てみると、確かに緑のステージより手前に半透明の矩形は描画されている。そしてその後実行されるGraphics.DrawProceduralでレイヤー1が矩形の上から描画されてしまっている。
今回半透明の矩形描画はZWrite Offとしていたため、背景のz=0.01の情報が残っており描画されてしまったのだろう。ちなみにZWrite Onにしたら今度は矩形の領域だけ粒子が「抜けて」しまった。
saisyo
予想通り。ZTestで負けた粒子は描画されてない。
しかしこれは解決にはならない。

今回のでGraphics.DrawProceduralのスクリプト上での実行位置が悪いことが分かった。これをUpdateの最後にしたらどうか

これは実際にやってみたがうまくいかなかった。
今度はz=0.01の背景が粒子の上から描画されてしまうようだった。背景もGameObjectなので「Camera1のレンダリング」フェーズで描画されるためだ。

じゃあなんとか「Camera1のレンダリング」フェーズでGraphics.DrawProceduralを実行できないのか!?

いろいろ調べたところ、CommandBufferというのを使えばいいらしいことがわかった。
https://forum.unity.com/threads/commandbuffer-drawprocedural-crashes-editor-when-converted-to-drawproceduralindirect.434113/
using UnityEngine;
using UnityEngine.Rendering;
using System.Collections;
using System.Runtime.InteropServices;
 
// Apply Script to Camera GameObject
public class MyCommandBuffer : MonoBehaviour {
 
    public Shader shader;
 
    ComputeBuffer cbPoints;
 
    void Start () {
        Material mat = new Material(this.shader);
        Camera cam = this.GetComponent();
        CommandBuffer cb = new CommandBuffer();
 
        var verts = new Vector4[6] { new Vector4(-1, -1, 0, 1),
                                     new Vector4(1, 1, 0, 1),
                                     new Vector4(1, -1, 0, 1),
                                     new Vector4(1, 1, 0, 1),
                                     new Vector4(-1, -1, 0, 1),
                                     new Vector4(-1, 1, 0, 1) };
 
        this.cbPoints = new ComputeBuffer(6, Marshal.SizeOf(typeof(Vector4)), ComputeBufferType.Default);
        this.cbPoints.SetData(verts);
        mat.SetBuffer("quadVerts", this.cbPoints);
 
        cb.name = "stencil mask";
        cb.DrawProcedural(cam.cameraToWorldMatrix, mat, 0, MeshTopology.Triangles, 6, 1);
        cam.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, cb);
    }
 
    void OnDestroy () {
        if (this.cbPoints != null) this.cbPoints.Release();
        this.cbPoints = null;
    }
}

このソースを参考にして書き直した。
OnRenderObject()で実行していたGraphics.DrawProceduralは消して、Start()内でCommandBuffer.DrawProceduralでコマンドを登録する。あとは何もしなくとも毎フレーム自動的にDrawProceduralが行われる。

また「CameraEvent.BeforeForwardOpaque」
この部分を変えればさらにいろんなタイミングでDrawProceduralが使えるようだ。今回はBeforeForwardOpaqueのままでうまくいった。→うまくなんかいってませんでした。
今回はAfterForwardOpaqueにすることでだいたいの順番を指定することができた。ShaderのTagのQueueでBackground(1000)、Geometry(2000)はOpaqueに相当するらしく、Transparent(3000)、Overlay(4000)はOpaqueに相当しない。なのでこの間に挟むことで初めて任意のタイミングでの実行が保証される
yaritaikoto

そのあともいろいろ実験したけど、確かに粒子のz座標を変えるだけで手前にきたり奥にいったり。うまくいっているようだ。→ように見えただけ。zバッファ書き込みonにしてたため粒子の描画が先に来てもぱっと見問題ないように見えただけでした・・・
ただ今回すべての粒子をz=0.00としてやっているのでうまくいっているだけなのかもしれない。というわけでshader側で粒子のz座標を-1.0と0.0の2パターンにランダムにばらけさせて実験してみたところ、ちゃんとz==-1.0の粒子だけ灰色の矩形より手前に表示された!
完璧だった

結論
Graphics.DrawProceduralよりCommandBuffer.DrawProceduralを使うべき
それでも完全に任意のタイミングでDrawProceduralを行うには、ほかの全てのSprite等でタイミング指定記述(Queue=???)が必要。さらに2つ以上CommandBuffer.DrawProceduralでAfterForwardOpaqueを指定した場合どちらが先に描画されるかは不明・・・





今回使ったshaderのコード(いろいろ汚いです。ごめんなさい)





レイヤー1 粒子のほう
Shader "MenyBulletsShader" {
	Properties{
		_Intensity("色の強さ", float) = 1.0
	}

	SubShader{
		Pass{
		ZWrite Off
		Blend One One
		CGPROGRAM

		
#pragma target 5.0// シェーダーモデルは5.0を指定
#pragma vertex vert
//#pragma geometry geom
#pragma fragment frag

#define WX (uint)(192)
#define WY (uint)(144)

#include "UnityCG.cginc"

	uniform fixed _Intensity;

	// 弾の構造体
	struct Bullet
	{
		float2 pos;
		uint col;
	};
	
	// 弾の構造化バッファ
	StructuredBuffer<Bullet> Bullets;
	//粒子の位置
	StructuredBuffer<float2> RYS;
	//StructuredBuffer<uint> RYS;
	//粒子の色
	StructuredBuffer<uint> RYc;

	// 頂点シェーダからの出力
	struct VSOut {
		float4 pos : SV_POSITION;
		float4 col : COLOR;
	};

	// 頂点シェーダ
	VSOut vert(uint id : SV_VertexID)
	{
		// idを元に、弾の情報を取得
		VSOut output;
		uint di = RYS[id];
		//float xx = 0.00390625*(float)(di % 65536);
		//float yy = 0.00390625*(float)(di / 65536);
		float2 rysxy=RYS[id];
		float xx=rysxy.x;
		float yy=rysxy.y;

		float3 worldPos = float3(10.0f / WY *(xx- 0.5f*WX),10.0f/WY*(0.5f*WY -yy),0.0);//0.078125は10.0/128。RYSを0を中心に+-5以内に収める処理
		output.pos = mul(UNITY_MATRIX_VP, float4(worldPos, 1.0f));
		uint uintcol = RYc[id];
		output.col.r =  (float)(uintcol % 256) / 255.0;
		output.col.g =  (float)((uintcol / 256) % 256) / 255.0;
		output.col.b =  (float)((uintcol / 65536) % 256) / 255.0;
		output.col.a = 1.0f;//加算合成でのレンダリングのせいなのかαは0.0でも1.0として計算される
		return output;
	}

	// ピクセルシェーダー
	float4 frag(VSOut i) : COLOR
	{
		return i.col*_Intensity;
	}

		ENDCG
	}
}







レイヤー3 灰色のほう
Shader "mynormalcopy"{
	Properties{
		_MainTex("テクスチャ", 2D) = "white" { }
		_Intensity("色の強さ", Vector) = (1.0,1.0,1.0,1.0)
	}

		SubShader{
		Tags{ "Queue" = "Transparent" }

		Pass{
		ZWrite Off
		Blend SrcAlpha OneMinusSrcAlpha
		CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

	uniform sampler2D _MainTex;
	uniform fixed4 _MainTex_ST;//これもなぜかいる
	uniform fixed4 _Intensity;
	

	struct appdata {
		float4 vertex   : POSITION;
		float4 texcoord : TEXCOORD0;
	};

	struct v2f {
		float4 pos : SV_POSITION;
		float2 uv  : TEXCOORD0;
	};

	v2f vert(appdata v) {
		v2f o;
		o.pos = UnityObjectToClipPos(v.vertex);
		o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
		return o;
	}

	fixed4 frag(v2f v) : SV_Target{
		return tex2D(_MainTex, v.uv)*_Intensity;
	}
		ENDCG
	}
	}
}




C#側は全部乗せるとえらく長くなるので重要な部分のみ
using UnityEngine;
using System.Runtime.InteropServices;
using System;
using System.IO;
using UnityEngine.Rendering;

public class MenyBullets : MonoBehaviour
{
    public Shader bulletsShader;///MenyBulletsShader.shader 粒子をレンダリングするシェーダー//
    Material bulletsMaterial;///粒子のマテリアル bulletsShaderと紐づけされる
    public ComputeShader bulletsComputeShader;///NS.compute 流体の更新を行うコンピュートシェーダー 
    CommandBuffer commandb;
//中略
    void Start()
    {
	//中略
        bulletsMaterial = new Material(bulletsShader);
	//中略
        //コマンドバッファ系
        commandb = new CommandBuffer();
        Camera cam = GameObject.Find("Main Camera").GetComponent<Camera>();//コンポーネント
        commandb.name = "gpu instanse";
        commandb.DrawProcedural(cam.cameraToWorldMatrix, bulletsMaterial, 0, MeshTopology.Points, dtprtcomp.RYS.count, 1);//RYS.countはインスタンス化する数
cam.AddCommandBuffer(CameraEvent.AfterForwardOpaque, commandb); //中略 }