- Home /
How to make a localToWorld matrix for shader parameters?
Hi, I would like to make a matrix which is passed to a shader like "_Projector" matrix. It was not so difficult if localToWorld matrix had no scale. However, if localToWorld matrix had scale, it seemed like the scale parameters were applied twice to the vertices. To check this problem, I tested the following script:
using UnityEngine;
public class ManualTransform : MonoBehaviour {
void OnWillRenderObject()
{
//Matrix4x4 m = renderer.localToWorldMatrix;
Matrix4x4 m = Matrix4x4.identity;
m.SetColumn(3, renderer.localToWorldMatrix.GetColumn(3));
renderer.material.SetMatrix("mL2W", m);
}
}
And draw a sphere with this shader:
Shader "Custom/ManualTransform" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4x4 mL2W;
float4 vert(float4 vertex : POSITION) : SV_POSITION
{
return mul(UNITY_MATRIX_VP, mul(mL2W, vertex));
}
fixed4 frag() : COLOR
{
return fixed4(1,0,0,1);
}
ENDCG
}
}
}
Even though, the matrix "mL2W" was identity, the sphere was deformed when I set some scale. It seems like the vertices are already scaled before they are passed to the vertex shader. So, if I use the code in the line comment in OnWillRenderObject, scale parameters will be applied twice.
Here is a workaround:
public class ManualTransform : MonoBehaviour {
void OnWillRenderObject()
{
Vector3 scale = transform.localScale;
Matrix4x4 m = renderer.localToWorldMatrix * Matrix4x4.Scale(new Vector3(1.0f/scale.x, 1.0f/scale.y, 1.0f/scale.z));
renderer.material.SetMatrix("mL2W", m);
}
}
However, it doesn't look a good idea, because I'm not sure if the vertices are always scaled before vertex shader.
So, my questions are:
Is it true that vertices of a mesh are scaled before they are passed to a vertex shader?
If so, does it happen always, or under some conditions? Is there any way to check if the vertices are already scaled or not, like renderer.isPartOfStaticBatch?
Shouldn't renderer.localToWorldMatrix have no scale, if vertices are already scaled?
Is there a better way to make a matrix which contains localToWorld matrix with scale?
It would be appreciated if I could get answers for any of them. Thanks in advance!
Answer by _dns_ · Nov 29, 2014 at 06:10 PM
Hi, those vertices are pre-scaled because you must be using non-uniform scale on the mesh. Unity applies the scale before passing vertices to the shader. Try setting the same scale for all axis to see if this changes something. Also, note that Unity 5 will remove this behavior (info about this in the Unity blog)
You already have access to some transform matrices in the shader, check this page: http://docs.unity3d.com/Manual/SL-BuiltinValues.html.
Also, another effect you may see is the merge of meshes due to dynamic batching: this will change the origin & coordinates of the vertices of batched meshes.
Lastly, if you still have to pass parameters to a shader during play, you may want to check http://docs.unity3d.com/ScriptReference/Renderer.SetPropertyBlock.html. It's faster than using the material and is done on one object/renderer only. Using renderer.material like you do duplicates the material for this object, leading to un-batchable meshes, more memory usage etc...
Thank you for the answer! I tried uniform scale, then vertices were not pre-scaled! Also, I tried nearly uniform scale, like (10, 10, 9.99999), and vertices were not pre-scaled either. If uniform scale was negative, vertices were pre-scaled. So, I might need to do like this:
$$anonymous$$aterialPropertyBlock m_propertyBlock;
void Awake()
{
m_propertyBlock = new $$anonymous$$aterialPropertyBlock();
}
void LateUpdate()
{
$$anonymous$$atrix4x4 m = renderer.localToWorld$$anonymous$$atrix;
if (IsVertexPrescaled()) {
Vector3 scale = transform.localScale;
m = m * $$anonymous$$atrix4x4.Scale(new Vector3(1.0f/scale.x, 1.0f/scale.y, 1.0f/scale.z));
}
m_propertyBlock.Clear();
m_propertyBlock.Add$$anonymous$$atrix("mL2W", m);
renderer.SetPropertyBlock(m_propertyBlock);
}
bool IsVertexPrescaled()
{
#if (UNITY_4_3 || UNITY_4_4 || UNITY_4_5 || UNITY_4_6) // support Unity 4.3 or later
Vector3 scale = transform.localScale;
float max = $$anonymous$$athf.$$anonymous$$ax (scale.x, $$anonymous$$athf.$$anonymous$$ax (scale.y, scale.z));
float $$anonymous$$ = $$anonymous$$athf.$$anonymous$$in (scale.x, $$anonymous$$athf.$$anonymous$$in (scale.y, scale.z));
return 0.00001f * max < (max - $$anonymous$$);
#else
return false;
#endif
}
Of course, it is safer to use builtin matrices, like Object2World and UNITY$$anonymous$$ATRIX_$$anonymous$$VP, but I believe using localToWorld matrix is more efficient. What I actually want to do is making a uv projection matrix, like
$$anonymous$$atrix4x4 m = uvProjection * renderer.localToWorld$$anonymous$$atrix;
I can do this matrix multiplication in vertex shader, but that has GPU overheads. Also I can use UNITY_$$anonymous$$ATRIX_$$anonymous$$VP, and make the matrix m like this:
$$anonymous$$atrix4x4 m = Camera.current.projection$$anonymous$$atrix * Camera.current.worldToCamera$$anonymous$$atrix;
m = uvProjection * m.inverse;
It won't have GPU overheads, but needs to update the matrix property for each camera.
I also worried about dynamic batching as you mentioned, and I think, batching is the reason why renderer.localToWorld$$anonymous$$atrix must be used ins$$anonymous$$d of transform.localToWorld$$anonymous$$atrix. I wonder why renderer.localToWorld$$anonymous$$atrix doesn't take into account vertex pre-scale.
Anyway, your answer made the things clearer. Thanks!