Scriptable Render Pipeline-Transparency

https://catlikecoding.com/unity/tutorials/scriptable-render-pipeline/transparency/

1 alpha clipping
as explained in rendering 11, transparency, it is possible to cut holes in geometry by discarding fragments based on an alpha map. this technique is known as alpha clipping, alpha testing, or cutout rendering. besides that, it is exactly the same as rendering opaque geometry. so so support alpha clipping we only have to adjust our shader.

1.1 alpha maps
alpha clipping is only useful when a material’s alpha varies across its surface. the most straightforward way to achieve this is with an alpha map. here are two texture for that, one for square geometry like quads and cubes, and one for spheres.
在这里插入图片描述
import these textures and indicate that their alpha channel represents transparency. their RGB channels are uniform white so will not affects the material’s appearance.

1.2 texturing

add a main texture property to the lit shader. we will use is as the source for albedo and alpha, with solid white as the default.

Properties {
		_Color ("Color", Color) = (1, 1, 1, 1)
		_MainTex("Albedo & Alpha", 2D) = "white" {}
	}

create two new materials, one for lit alpha-clipped spheres and one for lit clipped squares, using the appropriate textures.

in the lit include file, add declarations for the main texture and its sampler state. this works like for the shadow map, but uses the TEXTURE2D and SAMPLER macros instead.

TEXTURE2D_SHADOW(_CascadedShadowMap);
SAMPLER_CMP(sampler_CascadedShadowMap);

TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);

we need uv texture coordinates for sampling, which are part of the mesh data. so add them to the vertex input and output structs.

struct VertexInput {
	float4 pos : POSITION;
	float3 normal : NORMAL;
	float2 uv : TEXCOORD0;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct VertexOutput {
	float4 clipPos : SV_POSITION;
	float3 normal : TEXCOORD0;
	float3 worldPos : TEXCOORD1;
	float3 vertexLighting : TEXCOORD2;
	float2 uv : TEXCOORD3;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

to apply the tiling and offset of the texture, add the required _MainTex_ST shader variable, in a UnityPerMaterial buffer. then we can use the TRANSFORM_TEX macro when transferring the uv coordinates in LitPassVertex.

CBUFFER_START(UnityPerMaterial)
	float4 _MainTex_ST;
CBUFFER_END

…

VertexOutput LitPassVertex (VertexInput input) {
	…
	
	output.uv = TRANSFORM_TEX(input.uv, _MainTex);
	return output;
}

we can now sample the main map in LitPassFragment with the SAMPLE_TEXTURE2D macro to retrieve the albedo and alpha data, which we when multiply with the color data. we will also return the alpha value from now on. that is not needed right now,but will be used later.

float4 LitPassFragment (VertexOutput input) : SV_TARGET {
	UNITY_SETUP_INSTANCE_ID(input);
	input.normal = normalize(input.normal);
	//float3 albedo = UNITY_ACCESS_INSTANCED_PROP(PerInstance, _Color).rgb;
	float4 albedoAlpha = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
	albedoAlpha *= UNITY_ACCESS_INSTANCED_PROP(PerInstance, _Color);
	
	…
	float3 color = diffuseLight * albedoAlpha.rgb;
	return float4(color, albedoAlpha.a);
}

1.3 discarding fragments

alpha clipping is done by discarding fragments when their alpha value falls below some cutoff threshold. the cutoff value lies between 0 and 1 and is configurable, so add a shader property for it, with 1/2 as the default.

Properties {
		_Color ("Color", Color) = (1, 1, 1, 1)
		_MainTex("Albedo & Alpha", 2D) = "white" {}
		_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
	}

add the corresponding variable to the UnityPerMaterial buffer. then invoke the clip function with the fragment’s alpha value minus the threshold. that will cause all fragments that end up below the threshold to be discarded, which means that they do not get rendered.

CBUFFER_START(UnityPerMaterial)
	float4 _MainTex_ST;
	float _Cutoff;
CBUFFER_END

…

float4 LitPassFragment (VertexOutput input) : SV_TARGET {
	…
	albedoAlpha *= UNITY_ACCESS_INSTANCED_PROP(PerInstance, _Color);
	
	clip(albedoAlpha.a - _Cutoff);}

objects with an alpa-clipeed material are now rendered with holes in them. the size of the holes depends on the cutoff value. however, that is only true for the object surface itself. the shadows that they cast are still solid, because we have not adjusted those yet.

1.4 clipped shadows
clipping shadows works exactly like clipping in the lit pass, so adjust the ShadowCaster include file accordingly. because the final alpha value depends on both the main map and the material color, we now also have to sample the instanced color in ShadowCasterPassFragment, so we have to pass the instance ID along as well.

CBUFFER_START(UnityPerMaterial)
	float4 _MainTex_ST;
	float _Cutoff;
CBUFFER_END

CBUFFER_START(_ShadowCasterBuffer)
	float _ShadowBias;
CBUFFER_END

TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);

#define UNITY_MATRIX_M unity_ObjectToWorld

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"

UNITY_INSTANCING_BUFFER_START(PerInstance)
	UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_INSTANCING_BUFFER_END(PerInstance)

struct VertexInput {
	float4 pos : POSITION;
	float2 uv : TEXCOORD0;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct VertexOutput {
	float4 clipPos : SV_POSITION;
	float2 uv : TEXCOORD0;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

VertexOutput ShadowCasterPassVertex (VertexInput input) {
	VertexOutput output;
	UNITY_SETUP_INSTANCE_ID(input);
	UNITY_TRANSFER_INSTANCE_ID(input, output);
	…
	
	output.uv = TRANSFORM_TEX(input.uv, _MainTex);
	return output;
}

float4 ShadowCasterPassFragment (VertexOutput input) : SV_TARGET {
	UNITY_SETUP_INSTANCE_ID(input);
	float alpha = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv).a;
	alpha *= UNITY_ACCESS_INSTANCED_PROP(PerInstance, _Color).a;
	clip(alpha - _Cutoff);
	return 0;
}

1.5 double-sized rendering

because only the front size of geometry gets rendered, our alpha-clipped objects are missing their back sides. this is obvious when rotating the view around them. also, their shadows do not match what we see, because only the front side relative to the light source casts a shadow. the solution to this is to render both sides of the geometry, which allows us to see the inside of the object surfaces and makes the inside surface cast shadows.

which sides get rendered is controlled by a shader’s cull mode. either no culling takes place, all front-facing triangles are culled, or all back-facing triangles are culled. we can add a float shader property that respsents an enum value, with 2 as the default, cooresponding to the usual back-face culling.

Properties {
		_Color ("Color", Color) = (1, 1, 1, 1)
		_MainTex("Albedo & Alpha", 2D) = "white" {}
		_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
		_Cull ("Cull", Float) = 2
	}

we can expose this property via an enum popup, by adding the attribute to the property. the desired enum type can be supplied as an argument, which in this case is from the UnityEngine.Rendering namespace.

[Enum(UnityEngine.Rendering.CullMode)] _Cull ("Cull", Float) = 2

although we have defined it as a shader property, the cull mode is not directly used by the shader programs. it is used by the gpu to decide which triangles are passed to the fragment programs and which are discarded. we control this via a Cull statement in the shader pass. if we used a fixed cull mode, then we could suffice with sth. like Cull off, but we can also make it depend on our shader property, by writing Cull[_Cull]. do this for both passes.

Pass 
{
			Cull [_Cull]
			HLSLPROGRAM
			…
			ENDHLSL
}
Pass 
{
			Tags 
			{
				"LightMode" = "ShadowCaster"
			}
			Cull [_Cull]
			HLSLPROGRAM
			…
			ENDHLSL
}

1.6 flipping normals to backfaces

we are now seeing both sides of the geometry, but the inside is not lit correctly. this is easiest to see by having our materials cull the front faces, so we only see the insides.

it turns out that the lighting is flipped. what is lit should be dark, and vice versa. that is because the normal vectors are meant to be used for the outside, not the inside. so we have to negate the normal vectors when rendering back faces.

the gpu can tell the fragment program whether it is shading a fragment of a front or a back face. we can access thie information by adding an additional parameter to LitPassFragment. the type and semantic of this parameter depend on the API, but we can use the FRONT_FACE_TYPE and FRONT_FACE_SEMANTIC macros from the Core library. likewise, we can use the IS_FRONT_VFACE macro to choose between two alternatives based on whether we are dealing with a front or a back face. use this to negate the normal vector when necessary.

float4 LitPassFragment (
	VertexOutput input, FRONT_FACE_TYPE isFrontFace : FRONT_FACE_SEMANTIC
) : SV_TARGET {
	UNITY_SETUP_INSTANCE_ID(input);
	input.normal = normalize(input.normal);
	input.normal = IS_FRONT_VFACE(isFrontFace, input.normal, -input.normal);}

the inside surface now gets shaded correctly, although it still ends up darker than the outside because of self-shadowing.

1.7 optional clipping 有选择的剔除

when alpha clipping is used the gpu can no longer assume that the entire triangle gets rendered, which makes some optimizations impossible. so it is best to only enable alpha clipping when necessary. so we will create two shader variants: one with and one without clipping. we can do that with a shader keyword, like the pipeline contols whether shadows are used, except this time we will control it via a material property.
关键字和属性???两个都可以控制

add a toggle property to control clipping to the shader. it has to be a float, with a default value of zero. give it a Toggle value, which will make it show up as a checkbox. besides that, the attribute can be supplied with a keyword that it enables or disables when the propert is changed. we will use the _CLIPPING keyword.

[Toggle(_CLIPPING)] _Clipping ("Alpha Clipping", Float) = 0
_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5

we can now add another multi-compile statement, but the expectation is that this toggle will not change during play but only when editing material assets. so we do not need to always generate shader variants for both options. we can do that by using the #pragma shader_feature directive instead. in case of a single toggle keyword, we can suffice with just listing that keyword and nothing else. do this for both passes.

#pragma shader_feature _CLIPPING

what’s the advantage of using a shader feature??
all multi-compile variants have to always be included, both when shaders are compiled in the editor and when putting them in a build. the shader feature alternative only includes the variants that are actualy needed, as far as the unity editor can determine. this can significantly reduce shader compilation time and the size of builds.

the only reason to use the multi-compile approach is when which keywords are enabled changes during play. an example of this are the shadow keywords, but can also be true if u configure materials during play.

now we can make sure that we only clip in Lit if the _CLIPPING keyword is defined.

#if defined(_CLIPPING)
		clip(albedoAlpha.a - _Cutoff);
#endif

and the same goes for ShadowCaster.

#if defined(_CLIPPING)
		float alpha = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv).a;
		alpha *= UNITY_ACCESS_INSTANCED_PROP(PerInstance, _Color).a;
		clip(alpha - _Cutoff);
#endif

note that, we could optimize further by eliminating the uv coordinates too, but that optimization is less important so i will not cover that. likewise, 同样地,u could use a shader feature to only check the triangle facing when clipping is off, which is another optimization i skip.

1.8 alpha-test render queue

besides potentially discarding fragments, alpha-clipped rendering works the same as opaque rendering and both can be mixed without issue. but because alpha clipping prevents some gpu optimizations it is typical to first render all purely opaque objects before rendering all alpha-cliped objects. that enables the most gpu optimization, potentially litmits the amout of alpha-clipped fragments as more end up hidden behind opaque geometry, and can also reduce the amount of batches. all this can bone by simply setting the alpha-clipped materials to use a later render queue. the default material inspector exposes the queue, so we can manually change it. the default queue for alpha-clipped materials is 2450, corresponding to the Alpha Test option from the dropdown menu.

2 semi-transparency
if a fragment does not get clipped it is fully opaque. so alpha-clipping can be used to cut holes in objects, but it can not represent semi-transparent surfaces. we have some more work to do before our shader supports semi-transparency.

2.1 blend modes
when sth. is semi-transparent, at least some of what is behind it shines through. to achieve that with a shader we have to change how a fragment’s own color gets blended with the color that got rendered earlier. we can do that by changing the blend mode of the shader.

the blend mode is controlled like the cull mode, but with two weighing options that are used to blend the new and old color. the first is known as the source – what we are rendering now-- and the second as the destination-- what was rendered before. for example, the default blend mode is Blend one zero, which means that the new color completely replaces the old one.

are not there also separate options for the alpha channel??
yes, but those are rarely used. without explicitly specifying blends modes for alpha, all four channels are blended the same way.

add two shader properties for the source and destination blend, just like for culling, except with the BlendMode enum type. set their default values to one and zero.

[Enum(UnityEngine.Rendering.CullMode)] _Cull ("Cull", Float) = 2
[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 1
[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 0

add a blend statement to the lit pass only. the ShadowCaster pass only cares about depth so blend modes do not affect it.

Pass {
		Blend [_SrcBlend] [_DstBlend]
		Cull [_Cull]}

the simplest form of semi-transparency is fading a fragment based on its alpha value. that is done by using the source’s alpha as the weight for the source and one minus the source’s alpha as the weight for the destination. we can select those options from the dropdown menus. do this for new fade materials, and also turn off culling for them.

there are a lot of other blend modes too. most are rarely used but some are used for different kinds of transparency. for example, pre-multiplied blending uses one for the source instead of the source’s alpha. that makes it possible to keep specular reflections-- to represent surfaces like glass – but requires some shader changes too which i will not cover here.

2.2 transparent render queue
fading only works if there is already sth. behind what is getting rendered. our pipeline already takes care of that, first rendering the opaque queues, then the skybox, and finally the transparent queues. our fade materials just have to use the correct queue. the default Transparent option is fine.

2.3 not writing depth

semi-transparency now sometimes works as it should, but also produces weird results. this is especially noticeable because we are still casting shadows as if the surfaces were opaque. this happens because we are not culling, so both sides of the surfaces get rendered. which part gets rendered first depends on the triangle order of the mesh. when a front-facing triangle gets rendered first, there is not a back side to blend with yet. and the back will not get rendered because it is behind sth. that already got rendered.

the same problem also happens when two separate transparent objects are close to each other. unity sorts transparent object back-to-front, which is correct but can only consider the object position, not shape. part of an object that is drawn first can still end up in front of an object that gets drawn later. for example, put two mostly-overlapping quads in the scene, one a bit above the other, and adjust the view until the top one gets rendered first.

we can not avoid this except by carefully controlling the placement of the semitransparent objects or using materials with different render queues. in case of intersecting objects or a double-sided material with arbitrary triangle order, it will always go wrong. but what we can do is disable writing to the depth buffer for transparent materials. that way what gets rendered first will never block what gets rendered later.

add another float shader property to control z writing, which is only by default. we could again use a toggle, but that will always produces a keyword, which we do not need in this case. so instead we will make it a custom enumeration with an off and on state, by writing[Enum(off, 0, on, 1)].

[Enum(Off, 0, On, 1)] _ZWrite ("Z Write", Float) = 1

add a ZWrite control to the lit pass only, as once again this does not concern shadows.

ZWrite [_ZWrite]

now both quads get fully rendered, even when their draw order is incorrect. however, the bottom quad still gets drawn after the top quad, so it is still not correct. this is exacerbated by the solid shadows of the quads. it is also very obvious when the draw order flips. this is a limitation of transparent rendering that u have to keep in mind when designing a scene.

2.4 double-sided with semi-transparency

with z writing disabled, the insides of objects always get rendered when culling is off. however, the draw order is still determined by the triangle oder of the mesh. this is guaranteed to produce incorrect results when using the default sphere and cube.

with an arbitrary mesh the only way to ensure that the back faces are drawn first is to duplicate the object and use two materials, one that culls front and another that culls back. then adjust the render queues so that the inside is drawn first.

that works for an individual object, but not when multiple such objects are visualy overlapping. in that case all outsides gets drawn on top of all insides.

2.5 making a double-sided mesh

the best way to render double-side semi-transparent surfaces is to use a mesh specifically created for this purpose. the mesh must contain separate triangles for its inside and outside, ordered so that the inside is draw first. even then, this only reliably works for concage 凹的 objects that never visually overlap themselves.

u can create a double-sided mesh with a separate 3D modeler, but we can also make a simple tool in unity to quickly generate a double-sided variant of any source mesh. to do so, create a static DoubleSidedMeshMenuItem class and put its asset file in an Editor folder. we will use to it add the Assets/Create/Double-Sided Mesh item to unity’s menu. that is done by adding the MenuItem attribute to a static method, with the desired item path as an argument.

using UnityEditor;
using UnityEngine;

public static class DoubleSidedMeshMenuItem {

	[MenuItem("Assets/Create/Double-Sided Mesh")]
	static void MakeDoubleSidedMeshAsset () {}
}

the idea is that the user first selects a mesh and then activates the menu item, then we will create its double-sided equivalent. so the first step is to get a reference to the selected mesh, which is done via Selection.activeObject. if there is not a selected mesh, instruct the user to select one and abort.

static void MakeDoubleSidedMeshAsset () {
		var sourceMesh = Selection.activeObject as Mesh;
		if (sourceMesh == null) {
			Debug.Log("You must have a mesh asset selected.");
			return;
		}
	}

we begin by creating the inside portion of the mesh. clone the source mesh by instantiating it, retrieve its triangles, reverse the order via System.Array.Reverse, and assign the result back to it. that flips the facing of all triangles.

if (sourceMesh == null) {
			Debug.Log("You must have a mesh asset selected.");
			return;
		}
		
		Mesh insideMesh = Object.Instantiate(sourceMesh);
		int[] triangles = insideMesh.triangles;
		System.Array.Reverse(triangles);
		insideMesh.triangles = triangles;

next, retrieve the normals, negate them, and assign them back.

insideMesh.triangles = triangles;
		
		Vector3[] normals = insideMesh.normals;
		for (int i = 0; i < normals.Length; i++) {
			normals[i] = -normals[i];
		}
		insideMesh.normals = normals;

then create a new mesh and invoke CombineMesh on it. its first argument is an array of CombineInstance structs, which just need a reference to relevant mesh. first comes the inside mesh, then the source mesh. that guarantees that the inside triangles get drawn first. after that come three boolean arguments. the first needs to be true, indicating that the meshes must be merged into a single mesh, instead of defining multiple sub-meshes. the other two refer to matrices and lightmap data, which we do not need.

insideMesh.normals = normals;

		var combinedMesh = new Mesh();
		combinedMesh.CombineMeshes(
			new CombineInstance[] {
				new CombineInstance { mesh = insideMesh },
				new CombineInstance { mesh = sourceMesh }
			},
			true, false, false
		);

once that is done we no longer need the inside mesh, so destroy it immediately.

	combinedMesh.CombineMeshes();

		Object.DestroyImmediate(insideMesh);

finally, create a mesh asset by invoking AssetDatabase.CreateAsset. its first argument is the combined mesh and the second its asset path. we will simply put it in the asset root folder and give it the same name as the source mesh with Double-Sided appended to it. the path and file name can be combined via the System.IO.Path.Combine method, so it works no matter which path separator your operating system uses. and we have to use asset as the file extension.

AssetDatabase.CreateAsset(
			combinedMesh,
			System.IO.Path.Combine(
				"Assets", sourceMesh.name + " Double-Sided.asset"
			)
		);

2.6 alpha-clipped shadows

up to this point we have ignored shadows, so our semi-transparent objects still cast shadows as if they were opaque. they also receive shadows, but that is fine.

can transparent objects receive shadows??

yes. all that’s needed to receive shadows is to determine whether there is a shadow caster between a fragment and the light source, which the shadow map tells us. whether the fragment is for an opaque or transparent surface is irrelevant. having said that, unity does not support shadow-receiving for transparent surfaces in combination with cascaded shadow maps. that is because unity samples the cascaded shadow map in a separate full-screen pass, which relies on the depth buffer, thus can not work in combination with transparency. as we sample all shadows per fragment we do have that limitation.

shadow maps cannot represent partial shadows. the best that we can do is use alpha-clipped shadows. currently, alpha clipping can be enabled for a transparent material, but that also affects the surface itself.

it is possible to only perform alpha clipping for shadows. we can support that by replacing the clipping toggle with three options: off, on, and shadows. first, turn off clipping for all materials that current use it, so the _CLIPPING keywords gets cleared. then replace the toggle with a KeywordEnum with the three options as arguments.

[KeywordEnum(Off, On, Shadows)] _Clipping ("Alpha Clipping", Float) = 0

now u can turn clipping back on. we did that because KeywordEnum uses different keywords. the keywords that we now use are formed by taking the shader property name followed by an underscore and then each option separately, all uppercase. so in the lit pass we have to change our shader feature to rely on _CLIPPING_ON instead.

#pragma shader_feature _CLIPPING_ON

adjust the keyword check as well.

#if defined(_CLIPPING_ON)
	clip(albedoAlpha.a - _Cutoff);
#endif

the ShadowCaster pass must now use clipping when it is either on or set to shadows. in other words, it should not clip when it is off. we will use the latter criteria 标准 for the shader feature, so we rely solely on _CLIPPING_OFF

#pragma shader_feature _CLIPPING_OFF

so we must now check whether _CLIPPING_OFF is not defined.

#if !defined(_CLIPPING_OFF)
		float alpha = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv).a;
		alpha *= UNITY_ACCESS_INSTANCED_PROP(PerInstance, _Color).a;
		clip(alpha - _Cutoff);
	#endif

this makes it possible for transparent materials to cast alpha-clipped shadows. it is not a perfect match, but it was easy to support and might be good enough in some cases.

u can turn off shadow casting per object if u do not want them. we will also make that possible per material later.

is not there a way to create semi-transparent shadows??

unity’s legacy pipeline has an option to render semi-transparent shadows, which is described in rendering 12, semitransparent shadows. it fakes semi-transparency by dithering shadows, clipping them based on alpha and a screen-space dither pattern, relying on filtering to smudge the results. it can produce convincing shadows in some limited cases, but in general the results are so bad that it is unusable.

2.7 receiving shadows
transparent surfaces receive shadows just fine, but that might not be desirable. so let us make it optional, by adding a shader property toggle linked to the _RECEIVE_SHADOWS keyword, turned on by default.

[Toggle(_RECEIVE_SHADOWS)] _ReceiveShadows ("Receive Shadows", Float) = 1

add a shader feature for it to the lit pass.

#pragma shader_feature _RECEIVE_SHADOWS

simply return 1 in ShadowAttenuation and CascadedShadowAttenuation when the _RECEIVE_SHADOWS keyword is not defined.

float ShadowAttenuation (int index, float3 worldPos) {
	#if !defined(_RECEIVE_SHADOWS)
		return 1.0;
	#elif !defined(_SHADOWS_HARD) && !defined(_SHADOWS_SOFT)
		return 1.0;
	#endif}float CascadedShadowAttenuation (float3 worldPos) {
	#if !defined(_RECEIVE_SHADOWS) ||
		return 1.0;
	#elif !defined(_CASCADED_SHADOWS_HARD) && !defined(_CASCADED_SHADOWS_SOFT)
		return 1.0;
	#endif}

all shadows will disappear after making these changes, even though the materials all have shadows enabled. that happens because adding a new property to the shader does not automatically enable the relevant keyword. selecting all materials and toggling the option will synchronize the property and its keyword. in contrast, when a new material is created all its keywords linked to attributes will immediately be set correctly.

3 shader gui

while it is now possible to create both opaque and transparent materials with our shader, we have to manually select the correct blend modes and so on. unity’s shader inspector hide these details and instead show a dropdown menu for the supported surface types. we can do sth. similar, by creating a custom shader gui for our material.

3.1 lit shader gui
create a LitShaderGUI class that extends ShaderGUI and put it in an Editor folder.
we need to use the UntiyEditor namespace for that, and also the UnityEngine.Rendering namespace to use the BlendMode and CullMode enum types later.

using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;

public class LitShaderGUI : ShaderGUI {}

ShaderGUI defines an OnGUI method that gets invoked to create a material’s inspector. it has a MaterialEditor parameter, which is the underlying object that tracks the materials that are being edited. it also has a MaterialProperty array parameter that contains references to all the shader properties of the selected materials. we have to override this method to create our own GUI, but we will not replace the default gui, just add to it. so our method will invoke the base OnGUI implementation of ShaderGUI.

public override void OnGUI (
		MaterialEditor materialEditor, MaterialProperty[] properties
	) {
		base.OnGUI(materialEditor, properties);
	}

why is there a separate material editor??
originally, the way to create a custom inspector for a material was by extending MaterialEditor. the ShaderGUI approach came later and is more straightforward and specific to this materials. however, the original editor functionally is still used to generate the inspector. it just invokes the appropriate methods of the shader GUI. the editor is provided via a parameter because it is needed for direct manipulation of the selected materials.

to use our custom gui we have to add a CustomEditor statement to our Lit shader, followed by a string containing the name of our class.

public override void OnGUI (
		MaterialEditor materialEditor, MaterialProperty[] properties
	) {
		base.OnGUI(materialEditor, properties);
	}

to do our custom work we will need to use the editor, properties, and selected materials, so let’s keep track of those with fields. because we will support multi-material editing, we will have to work with an array of selected materials. those can be retrieved via the targets property of the editor. however, because the editor is generic we get an Object array, not a Material array.

MaterialEditor editor;
	Object[] materials;
	MaterialProperty[] properties;
	
	public override void OnGUI (
		MaterialEditor materialEditor, MaterialProperty[] properties
	) {
		base.OnGUI(materialEditor, properties);

		editor = materialEditor;
		materials = materialEditor.targets;
		this.properties = properties;
	}

3.2 casting shadows

let us begin by making it possible to disable shadow casting per material. that is done by disabling the ShadowCaster pass for all selected materials. we can do that by looping through the materials array and invoking SetShaderPassEnabled on each, with a pass name and whether it should be enabled. put that code in a SetPassEnabled method with a pass and the enabled state as parameters.

void SetPassEnabled (string pass, bool enabled) {
		foreach (Material m in materials) {
			m.SetShaderPassEnabled(pass, enabled);
		}
	}

besides that, we need to determine whether shadow casting is enabled. we can not check that by invoking GetShaderPassEnabled on a material. create another method for that, checking the first material of the selection.

bool IsPassEnabled (string pass) 
{
		return ((Material)materials[0]).GetShaderPassEnabled(pass);
}

but if multiple materials are selected we can end up with mixed results. we can not represent that with a single boolean. so let us return a nullable boolean instead. then we can loop through all additional materials and if we find an inconsistency we will return null. 我们遍历所有的material,如果发现一个不一致的,则返回null。

bool? IsPassEnabled (string pass) {
		bool enabled = ((Material)materials[0]).GetShaderPassEnabled(pass);
		for (int i = 1; i < materials.Length; i++) {
			if (enabled != ((Material)materials[i]).GetShaderPassEnabled(pass)) {
				return null;
			}
		}
		return enabled;
	}

now we can create a method that takes care of showing a toggle option for casting shadows. first, check whether the ShadowCaster pass in enabled. if we did not get a value, then set EditorGUI.showMixedValue to true to signal that input controls should draw a mixed-value representation of themselves. we also have to set the enabled state to sth, which can be anything so just use false. and at the end of the method we should disable the mixed value representation.

void CastShadowsToggle () {
		bool? enabled = IsPassEnabled("ShadowCaster");
		if (!enabled.HasValue) {
			EditorGUI.showMixedValue = true;
			enabled = false;
		}
		
		EditorGUI.showMixedValue = false;
	}

the checkbox can be shown by invoking EditorGUILayout.Toggle with the Cast Shadows label and the enabled value. assign its result back to the enabled state.

if (!enabled.HasValue) {
			EditorGUI.showMixedValue = true;
			enabled = false;
		}
		enabled = EditorGUILayout.Toggle("Cast Shadows", enabled.Value);
		EditorGUI.showMixedValue = false;

that only changes our variable. to adjust the materials we have to invoke SetPassEnabled. but we must only do that when the user changed the state. we can ensure that by invoking EditorGUI.BeginChangeCheck before the toggle and EditorGUI.EndChangeCheck after it. the latter invocation returns whether a change was made between. if so, invoke SetPassEnabled.

EditorGUI.BeginChangeCheck();
		enabled = EditorGUILayout.Toggle("Cast Shadows", enabled.Value);
		if (EditorGUI.EndChangeCheck()) {
			SetPassEnabled("ShadowCaster", enabled.Value);
		}

because we directly change the material assets, an undo step will not automatically get enabled. we have to do that ourselves, by invoking RegisterPropertyChangUndo on the editor with a label argument,before making the change.

if (EditorGUI.EndChangeCheck()) {
			editor.RegisterPropertyChangeUndo("Cast Shadows");
			SetPassEnabled("ShadowCaster", enabled.Value);
		}

finally, invoke the method to show the toggle at the end of OnGUI. that causes the toggle to appear at the bottom of our material’s inspector.

public override void OnGUI (
		MaterialEditor materialEditor, MaterialProperty[] properties
	) {CastShadowsToggle();
	}

3.3 setting shader properties
changing from opaque to clipped or faded surfaces requires changing multiple shader properties. we are going to make this convenient by adding setter properties for those that we need. the cull, blend, and z-write properties are straightforward. use the appropriate types for the properties, invoke FindProperty with their name and the properties array as arguments, and assign to their floatValue property.

CullMode Cull {
		set {
			FindProperty("_Cull", properties).floatValue = (float)value;
		}
	}

	BlendMode SrcBlend {
		set {
			FindProperty("_SrcBlend", properties).floatValue = (float)value;
		}
	}

	BlendMode DstBlend {
		set {
			FindProperty("_DstBlend", properties).floatValue = (float)value;
		}
	}

	bool ZWrite {
		set {
			FindProperty("_ZWrite", properties).floatValue = value ? 1 : 0;
		}
	}

the clip mode and shadow-receiving properties require more work, because they needs to be synchronized with shader keywords. a keyword can be enabled for a material by invoking EnableKeyword on the material. disabling it requires invoking DisableKeyword. we have to do that for all selected materials. create a convenient method for this, with a keyword name and enabled state as parameters.

	void SetKeywordEnabled (string keyword, bool enabled) {
		if (enabled) {
			foreach (Material m in materials) {
				m.EnableKeyword(keyword);
			}
		}
		else {
			foreach (Material m in materials) {
				m.DisableKeyword(keyword);
			}
		}
	}

now we can add setter properties for clipping and shadow-receiving, invoking SetKeywordEnabled for al relevant keywords. also create an enum for our custom clip mode.

enum ClipMode {
		Off, On, Shadows
	}

	ClipMode Clipping {
		set {
			FindProperty("_Clipping", properties).floatValue = (float)value;
			SetKeywordEnabled("_CLIPPING_OFF", value == ClipMode.Off);
			SetKeywordEnabled("_CLIPPING_ON", value == ClipMode.On);
			SetKeywordEnabled("_CLIPPING_SHADOWS", value == ClipMode.Shadows);
		}
	}

	bool ReceiveShadows {
		set {
			FindProperty("_ReceiveShadows", properties).floatValue =
				value ? 1 : 0;
			SetKeywordEnabled("_RECEIVE_SHADOWS", value);
		}
	}

the final thing that we need to adjust is the render queue. that can be done by setting a material’s renderQueue property. add a setter property for that with the RenderQueue type, setting the queue of all materials.

RenderQueue RenderQueue {
		set {
			foreach (Material m in materials) {
				m.renderQueue = (int)value;
			}
		}
	}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值