I’m trying to apply vertex displacement on a planar mesh particle (shuriken) via a shader. The method I have been trying adjusts the vertex position in the hlsl shader by changing e.g the z parameter of the particle’s position in object-space. It seems that the object-space for particles is the world-space (?), so the displacement happens always at the direction of Z world-coordinate, which is messing the desired effect when the particle has been rotated.
Is there an easy method, like by using a certain vector which is updated with the particle’s rotation and which can be used to always align the vertex displacement to the particle’s z “local” axis -as if the particle’s rotation was (0,0,0)?
Or do I need to apply some kind of rotation matrix? If so, any clue of how to achieve this?
My guess is that I misunderstand something fundamental here, since no matter how much (and it’s been a lot) I investigated and experimented on this task, I did not succeed.
As a side-note, I have managed perpendicular vertex displacement regardless the particle’s rotation (or better-said in relation to the particle’s rotation), by multiplying the displacement amount with the vertex normal vector (as it is passed by the normal vertex stream). If I however want to apply displacement only parallel to the mesh I fail to accomplish what I need to.
FYI all coords in particle shaders are in world space - there is no object space. this is due to how we batch the particle draw calls.
If you need to know about the object space of the system, you’ll need to set a matrix param on the material using transform.localToWorldMatrix
.
Other than that, as others have already suggested, you are correct to try and use the Custom Vertex Streams feature to solve this. The Center stream should be particularly useful to you. Just remember it’s in world space
If you want to construct the “local space of the particle”, you’ll need to pass the center and rotation vertex streams, and construct a matrix. It’s a bit tricky, but here is some pseudocode that should work:
float3 position = IN.center;
float3 rotation = IN.rotation;
rotation.z = -rotation.z; // unityt mesh particles have negated Z rotation due to legacy bug/behaviour
float4x4 transformMatrix = PositionAndEulerToMatrix(position, rotation);
float4x4 PositionAndEulerToMatrix(float3 p, float3 v)
{
float cx = cos(v.x);
float sx = sin(v.x);
float cy = cos(v.y);
float sy = sin(v.y);
float cz = cos(-v.z);
float sz = sin(-v.z);
float4x4 result;
result[0][0] = cy * cz + sx * sy * sz;
result[0][1] = cz * sx * sy - cy * sz;
result[0][2] = cx * sy;
result[0][3] = 0.0f;
result[1][0] = cx * sz;
result[1][1] = cx * cz;
result[1][2] = -sx;
result[1][3] = 0.0f;
result[2][0] = -cz * sy + cy * sx * sz;
result[2][1] = cy * cz * sx + sy * sz;
result[2][2] = cx * cy;
result[2][3] = 0.0f;
result[3][0] = p.x;
result[3][1] = p.y;
result[3][2] = p.z;
result[3][3] = 1.0f;
return result;
}
If you aren’t using 3D rotation in your particle system though, you’ll need to pass the AxisOfRotation vertex stream instead (only available in latest 2022.3 onward) together with the Rotation stream, and use a different function to build the rotation part of the matrix:
float3x3 AxisAngleToMatrix(float3 vec, float radians)
{
float s = sin(radians);
float c = cos(radians);
float vx = vec.x;
float vy = vec.y;
float vz = vec.z;
float xx = vx * vx;
float yy = vy * vy;
float zz = vz * vz;
float xy = vx * vy;
float yz = vy * vz;
float zx = vz * vx;
float xs = vx * s;
float ys = vy * s;
float zs = vz * s;
float one_c = 1.0f - c;
float3x3 result;
result[0][0] = (one_c * xx) + c;
result[1][0] = (one_c * xy) - zs;
result[2][0] = (one_c * zx) + ys;
result[0][1] = (one_c * xy) + zs;
result[1][1] = (one_c * yy) + c;
result[2][1] = (one_c * yz) - xs;
result[0][2] = (one_c * zx) - ys;
result[1][2] = (one_c * yz) + xs;
result[2][2] = (one_c * zz) + c;
return result;
}
(you can adapt that for the float4x4)
Finally, if you need the inverse of this matrix, you can multiply the IN.positions into local particle space, you can probably use this from our particle instancing shader:
// inverse transform matrix
float3x3 w2oRotation;
w2oRotation[0] = objectToWorld[1].yzx * objectToWorld[2].zxy - objectToWorld[1].zxy * objectToWorld[2].yzx;
w2oRotation[1] = objectToWorld[0].zxy * objectToWorld[2].yzx - objectToWorld[0].yzx * objectToWorld[2].zxy;
w2oRotation[2] = objectToWorld[0].yzx * objectToWorld[1].zxy - objectToWorld[0].zxy * objectToWorld[1].yzx;
float det = dot(objectToWorld[0].xyz, w2oRotation[0]);
w2oRotation = transpose(w2oRotation);
w2oRotation *= rcp(det);
float3 w2oPosition = mul(w2oRotation, -objectToWorld._14_24_34);
worldToObject._11_21_31_41 = float4(w2oRotation._11_21_31, 0.0f);
worldToObject._12_22_32_42 = float4(w2oRotation._12_22_32, 0.0f);
worldToObject._13_23_33_43 = float4(w2oRotation._13_23_33, 0.0f);
worldToObject._14_24_34_44 = float4(w2oPosition, 1.0f);
and use it like so:
float3 localPos = mul(worldToObject, float4(IN.position.xyz, 1.0f)).xyz;
localPos *= 0.5f; // scale particle by 50%
outWorldPos = mul(transformMatrix, float4(localPos, 1.0f)).xyz;
This is all pseudocode, though I have taken most of it from working projects, so it should be pretty close to correct. There is a chance you need to swap where I put the position.xyz with the part where i hardcoded 0.0f, in the result matrix. I can’t remember the row/column layout off the top of my head.
EDIT: i just realized, if all you want is to move particle towards its local position, which is effectively a simple scale, just pass Center stream and do:
float3 outPos = lerp(inPos, inCenter, scaleFactor);`
You only need all the matrix math if you want to do more advanced things than simple scaling, in local particle space. Sorry for the wall of text!
Can you describe exactly what kind of deformation do you want to achieve?
Particle system is rendered as a single mesh, usually, but perhaps what you want can be achieved… but we need specifics.
Thank you Pangamini, you’re right! I should specifically explain my end-goal. I have the following mesh to represent a butterfly particle.
I want to displace the vertices of the mesh towards the directions of cyan and magenta arrows. Cyan is for wing flapping, magenta contracts towards the centre, in order to reduce texture stretching. I’m now at the point where I need to move the vertices along the magenta arrows, regardless the rotation of the mesh(particle). The arrows are smaller towards the middle, because I apply a mask to the displacement.
So far I have managed to properly do the cyan movement, even when the particle has been rotated and always perpendicular to the mesh by multiplying the vertex’s position vector by the vertex’s normal vector.
positionOS += flapping * normalOS
But with the magenta displacement I have problem, because I don’t know if there is “built-in” vector, tangent to the mesh surface and one that always point from the vertex towards the central axis regardless of the rotation (like what the normal does for the perpendicular displacement). I think I found out that the tangent vector taken from the vertex stream is also not suitable. And more I read I realise that you are right and the problem lies to the batching of particle systems. So there is no ‘actual’ object-space for each particle, only for the whole mesh, which is always aligned to the world space. I guess then my option is to ‘manually’ create the vector which is always parallel to the particle’s plane (given the rotation3D values and by applying a rotation matrix?) in a c# script and pass it to the shader, but (I tried and) I don’t know how.
Therefore, first and foremost, I need the coplanar vector and as an end-task (which I have not mentioned in the original post) I need to be able to define the direction of that vector to always point to the central “x” local axis -as shown in the image- of the mesh, depending on whether the vertex is “left” or “right” of that axis. When the particle has no rotation I can use the -1 * sign(positionOS.z)
switch, but if let’s say the rotation3D value around x axis is 90°, then the positionOS.z has become the positionOS.x value of the zero-rotated particle, since the particle’s object-space has not been aligned to the rotation. I hope I don’t make my description unclear…
PS: I’m sorry but I could not figure out how to post the above as a comment to your answer!
Could you add a custom vertex stream CENTER to your ps, then in shader calculate the vertex position in world, then subtract the particle’s world position from this custom stream to get the local space position?
Not sure if local position in world orientation is enough for you, but the custom stream options contain tangents, rotation and many other useful things, I am not sure if you are aware of those.
There are multiple rotation options. Choose one that matches yohr particle rotation mode, then you should be able to get the rotated vector.
I am not sure which are these.
Other than this, I already have used the ‘center’ stream and I don’t have a problem applying the animation in regard to the position of the particle. The problem starts when it is rotated. And while the perpendicular to the mesh plane displacement is fine by using the normal vector, I cannot figure out a method to apply the parallel to the mesh plane displacement. The shader pass is something like the following snippet (I only include the vertex step, where everything related to the displacement takes place):
struct Attributes {
float3 positionOS : POSITION;
float3 normalOS : NORMAL;
float4 custom : TEXCOORD1;
float3 center : TEXCOORD2;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
Varyings UnlitPassVertex (Attributes input) {
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
//Butterfly Vertex Displacement Data
input.positionOS -= input.center;
float time = _Time.y;
float disAmount = GetDisplacementAmount();
float disSpeed = input.custom.y;
float mask = 1 - sin(PI * input.baseUV.x) - 0.06;
float s = sin(time * disSpeed); // for butterfly
float flapping = (s + 0.5) * disAmount * mask;
float wingContraction = saturate(s * 0.3) * saturate(mask) * -1.0 * input.positionOS.z;
input.positionOS += flapping * input.normalOS; //cyan displacement
input.positionOS.z += wingContraction; // * is there a vector to secure displacement along the magenta direction?
input.positionOS += input.center;
float3 positionWS = TransformObjectToWorld(input.positionOS);
output.positionCS_SS = TransformWorldToHClip(positionWS);
return output;
I do the ‘center’ subtraction from the position in object-space(?) and in the end I transform the position to world-space. I did however tried as you suggest but without the desired results. Nevertheless, the mesh deformation is fine with the above code as long as the rotation of the particle is (0,0,0). If it is rotated, it is still fine with the cyan displacement and by taking the normal vector into consideration, but the magenta displacement is wrong because it is z-coordinate dependent, and the ‘local’ coordinate system of each particle does not rotate together with the particle’s rotation. It is always aligned to the world and I cannot use the exact above lines of wingContraction and input.positionOS.z.
I appreciate your help and I’m sorry if my limited knowledge does not help to better explain the problem or point out the right cause of it.
What kind of particles are you rendering? Are they screen oriented quads, or custom meshes? Are you rotating particles in one axis, or in 3D?
Can’t you use Rotation or Rotation3D vertex streams?
To start with, thank you very much for your time! The particles are custom meshes like in the image above (8 vertices, 6 triangles). I’m trying to fold the mesh in the middle like a book to imitate the butterfly’s wing flapping. If it will help I could try to upload a gif (my bad I did not overlay the butterfly texture to make things more obvious).
Anyway, the particles are flying around by the noise module of the particle system and they change orientation by a c# script that sets the rotation3D values of each particle in order to align them according to the animatedVelocity introduced by the noise (that is rotation around all three axes). The result is that the particles are not parallel to the ground ((0,0,0) 3D rotation, the case when the above formulas work), but they are tilted (the case when the position.z coordinate is not useful, since it does not affect the vertices towards the real particle’s local z, but the world’s Z). Sure I could make use of the rotation3D values, but I’m not sure how to apply them, in order to have displacement towards the magenta direction, as represented in the image above.
PS: Please feel free to ask me to upload some images to communicate the situation better, if needed.
Doc says that rotation3D is “the inverse of the particle’s Euler rotation in degrees, on each axis.”. So I assume you could construct a 3x3 transform matrix from it to convert your magenta vector from world to local space.
I’m not sure if it can help, but I went ahead and created the following .gif:
I have commented out the perpendicular displacement, so that we see only the animation that is created by the two lines of code:
float wingContraction = saturate(s * 0.3) * saturate(mask) * -1.0 * input.positionOS.z;
input.positionOS.z += wingContraction; // * is there a vector to secure displacement along the magenta direction?
At the very beginning of the video the rotation of the particle is at (0,0,0) and the displacement works as intended from the edge of the wings towards the body of the butterfly.
When I add 90° around the z axis the mesh ‘z’ is still aligned with the world’s Z,so the animation is still correct.
If I however add another 90° rotation around the x axis then the vertices still move along the world’s z which is particle’s x coordinate anymore and the animation is not the desired one.
Adding another 90° rotation around the Y axis and the animation stops because now the z axis is perpendicular to the mesh plane and all vertices have zero z-value, thus the wingContraction value equals zero.
Since the gif is too fast, I include four images of the cue points corresponding to the gif’s applied rotation values.
Just saw your reply. I am not familiar enough with the rotation matrices implementation and I have experimented a lot by the trial-error method without any useful results. Any hint towards the right direction? Ideally I would prefer to create the magenta vector in the c# and pass it to the shader as a custom stream.
But the vector is unique per particle. I am not saying it’s not possible, but I haven’t been adding custom data to particles before.
About the rotation matrices, I am sure you can google something like “rotation matrix from euler angles”. I think that all the data you need is provided by unity (if you add this extra stream to be produced by the particle system).
Thanks @Chromatism! I’ll try my best and let us know if I can manage to yield something useful.
FYI all coords in particle shaders are in world space - there is no object space. this is due to how we batch the particle draw calls.
If you need to know about the object space of the system, you’ll need to set a matrix param on the material using transform.localToWorldMatrix
.
Other than that, as others have already suggested, you are correct to try and use the Custom Vertex Streams feature to solve this. The Center stream should be particularly useful to you. Just remember it’s in world space
If you want to construct the “local space of the particle”, you’ll need to pass the center and rotation vertex streams, and construct a matrix. It’s a bit tricky, but here is some pseudocode that should work:
float3 position = IN.center;
float3 rotation = IN.rotation;
rotation.z = -rotation.z; // unityt mesh particles have negated Z rotation due to legacy bug/behaviour
float4x4 transformMatrix = PositionAndEulerToMatrix(position, rotation);
float4x4 PositionAndEulerToMatrix(float3 p, float3 v)
{
float cx = cos(v.x);
float sx = sin(v.x);
float cy = cos(v.y);
float sy = sin(v.y);
float cz = cos(-v.z);
float sz = sin(-v.z);
float4x4 result;
result[0][0] = cy * cz + sx * sy * sz;
result[0][1] = cz * sx * sy - cy * sz;
result[0][2] = cx * sy;
result[0][3] = 0.0f;
result[1][0] = cx * sz;
result[1][1] = cx * cz;
result[1][2] = -sx;
result[1][3] = 0.0f;
result[2][0] = -cz * sy + cy * sx * sz;
result[2][1] = cy * cz * sx + sy * sz;
result[2][2] = cx * cy;
result[2][3] = 0.0f;
result[3][0] = p.x;
result[3][1] = p.y;
result[3][2] = p.z;
result[3][3] = 1.0f;
return result;
}
If you aren’t using 3D rotation in your particle system though, you’ll need to pass the AxisOfRotation vertex stream instead (only available in latest 2022.3 onward) together with the Rotation stream, and use a different function to build the rotation part of the matrix:
float3x3 AxisAngleToMatrix(float3 vec, float radians)
{
float s = sin(radians);
float c = cos(radians);
float vx = vec.x;
float vy = vec.y;
float vz = vec.z;
float xx = vx * vx;
float yy = vy * vy;
float zz = vz * vz;
float xy = vx * vy;
float yz = vy * vz;
float zx = vz * vx;
float xs = vx * s;
float ys = vy * s;
float zs = vz * s;
float one_c = 1.0f - c;
float3x3 result;
result[0][0] = (one_c * xx) + c;
result[1][0] = (one_c * xy) - zs;
result[2][0] = (one_c * zx) + ys;
result[0][1] = (one_c * xy) + zs;
result[1][1] = (one_c * yy) + c;
result[2][1] = (one_c * yz) - xs;
result[0][2] = (one_c * zx) - ys;
result[1][2] = (one_c * yz) + xs;
result[2][2] = (one_c * zz) + c;
return result;
}
(you can adapt that for the float4x4)
Finally, if you need the inverse of this matrix, you can multiply the IN.positions into local particle space, you can probably use this from our particle instancing shader:
// inverse transform matrix
float3x3 w2oRotation;
w2oRotation[0] = objectToWorld[1].yzx * objectToWorld[2].zxy - objectToWorld[1].zxy * objectToWorld[2].yzx;
w2oRotation[1] = objectToWorld[0].zxy * objectToWorld[2].yzx - objectToWorld[0].yzx * objectToWorld[2].zxy;
w2oRotation[2] = objectToWorld[0].yzx * objectToWorld[1].zxy - objectToWorld[0].zxy * objectToWorld[1].yzx;
float det = dot(objectToWorld[0].xyz, w2oRotation[0]);
w2oRotation = transpose(w2oRotation);
w2oRotation *= rcp(det);
float3 w2oPosition = mul(w2oRotation, -objectToWorld._14_24_34);
worldToObject._11_21_31_41 = float4(w2oRotation._11_21_31, 0.0f);
worldToObject._12_22_32_42 = float4(w2oRotation._12_22_32, 0.0f);
worldToObject._13_23_33_43 = float4(w2oRotation._13_23_33, 0.0f);
worldToObject._14_24_34_44 = float4(w2oPosition, 1.0f);
and use it like so:
float3 localPos = mul(worldToObject, float4(IN.position.xyz, 1.0f)).xyz;
localPos *= 0.5f; // scale particle by 50%
outWorldPos = mul(transformMatrix, float4(localPos, 1.0f)).xyz;
This is all pseudocode, though I have taken most of it from working projects, so it should be pretty close to correct. There is a chance you need to swap where I put the position.xyz with the part where i hardcoded 0.0f, in the result matrix. I can’t remember the row/column layout off the top of my head.
EDIT: i just realized, if all you want is to move particle towards its local position, which is effectively a simple scale, just pass Center stream and do:
float3 outPos = lerp(inPos, inCenter, scaleFactor);`
You only need all the matrix math if you want to do more advanced things than simple scaling, in local particle space. Sorry for the wall of text!
The part with the rotation matrix "If you want to construct the “local space of the particle”, you’ll need to pass the center and rotation vertex streams, and construct a matrix. It’s a bit tricky, but here is some pseudocode that should work:..." has solved my main inquiry so I mark this as the solution.
– Deyoung
Hey Richard, thanks for the helping hand and hyper thank you for the wall of text! It’s really what I need and I’m sure it will be proved useful. It will take quite some time for me to start making sense of it, but I know it will worth it.
For the time being and encouraged by Pangamini I was able to introduce a 3d rotation matrix in the shader’s code, which indeed uses the rotation3D values, and managed to get proper “local-z” animation, regardless the particle’s rotation. I was successful as longs the particles were static and I would set the rotation manually via the editor, but when in play mode, where the particles are affected by noise and fly around (remember? you helped me in the other thread to orient them according to their animatedVelocity ), the results have not been that beautiful/ properly aligned. So I’m now trying to move the calculations from the shader to the cpu side (c# script) and only pass the desired direction vector back to the shader via a custom vertex stream, but currently I’m puzzled of what’s going on. That’s the reason I have not posted anything yet. Maybe your new input will come to rescue. We’ll see…
A couple of observations, although most likely I might be mistaken.
- The matrix I used was for normal objects rotation (not particles) and worked without the negation of the Z rotation. Maybe I’ve done it somehow/somewhere without now remembering or being aware of doing so.
- Although the rotation3D values of the particles are in Euler angles, when they are passed via the rotation3D vertex stream to the shader (I think) they are in radians. The documentation does not specify it and it was a bit confusing for someone like me who does not know how to debug properly. Again, maybe I am mistaken.
The challenge has been hard, especially due to my lack of good understanding for the various coordination spaces that renders me incapable to visualise (either mentally or physically) all these vectors and their translation from one space to the next. I wish I had found a good visual tutorial to help me start making sense out of it. Of course the other way is if I would focus on a proper learning curve about matrices and all, instead of trying to stitch together patches of -nevertheless great- information I find around. I even thought at some point to try and visualize the required vectors in Unity as a debugging method!
Anyway, I now understand that it is not as easy as to get a one-way solution, since everything is depended on my specific setup, what I’m trying to do, the pivot orientation of the custom -no matter how simple- mesh for the particles I’m using and so on. So I’ll be patient and persistent. Naturally, I’ll post the result when I have it for feedback.
I apologise for I have messed up the thread by writing stuff as replies though they are not. Is it easy for the moderators to move it to the forums section?
Also I’d like actually to go ahead and ask a couple of certain questions (not necessarily need an answer, since they might not have a straight one):
-
I understand you just posted pseudocode, but I see it is in hlsl. My thinking was to move the matrix calculations to the c#. Do you indirectly recommend that is a good practice to do them in the shader or is it relevant to any other factors, (that you don’t need to explain here of course)?
-
ATM the process I follow is like this:
The particles are rendered aligned to local space and they get motion by the Noise module.
From the animatedVelocity vector I get the rotation values and orient them accordingly by setting the particle.rotation3D.
Those same rotation values I pass (via a custom vertex stream) to the shader in order to create the particle “local-space”.
For some reason though, the vertex animation is not well adjusted, although by manually setting the rotation3D values in the editor works fine.
I don’t want you to go into details, but do you think I’m doing it wrong? For example: Although ‘align to velocity’ makes the particles to properly follow the noise motion without the need to orient them in the script (something that I formerly thought was not working), the reason I don’t render them this way is because I think the vertex animation by the shader is not playing well, when they are not aligned to local space. Should I try to figure out how to do it anyway? I mean, when the ‘align to velocity’ rotates the particle, is theparticle.rotation3d
at this exact frame the updated rotation introduced by the alignment method or could it be offset and probably this is the reason that the vertex displacement by the shader is not as expected?
If you are doing it in script now, be sure to do it in LateUpdate, so that the noise module etc has already run.
Also, the particles may have a physics velocity as well as an animated velocity. you should add the 2 together to get the total velocity. Or from script, it’s Unity - Scripting API: ParticleSystem.Particle.totalVelocity
So cool Richard!! Your rotation matrix worked out of the box! I did not investigate to find what’s different from the one I had been using:
float3x3 rotationMatrix = float3x3(
cosY * cosZ, -cosY * sinZ, sinY,
cosX * sinZ + sinX * sinY * cosZ, cosX * cosZ - sinX * sinY * sinZ, -sinX * cosY,
sinX * sinZ - cosX * sinY * cosZ, sinX * cosZ + cosX * sinY * sinZ, cosX * cosY
);
Well I did not look into the rest of your advice.Especially the inverted matrices part I did not even understand it. The important thing is that I double-checked and I see no misalignment, which means that everything (shader & c#) work as intended:
Thank you also for the new tips, which I have already taken use of. So, yes, I have used totalVelocity
, since I also try to contain the butterflies in a volume and I affect their velocity and, yes, I have used LateUpdate
, because I had found this recommendation online.
What I found out is not playing well (where “well” is vertex animation ‘sychronized’ with particle orientation) is when I select Render Alignment : Velocity
. The orientation is fine, but the vertex displacement is not following by using the same method for some reason. Local alignment combined with the code you gave me the other day for velocity-aligned particles is the solution however.
Now I’d like to move the matrix transformation from the shader to the c# script. Do you think performance-wise it is worth it? There are just 8 vertices per mesh and I don’t intent to do more than 10-15 of them per emission. The transformations though are performed every frame and I read is better employ the cpu in such cases.
As a final touch, I will try to use an animation sheet to give more textures to the butterflies (hopefully it will go smoothly) and that’s it. Until then, I will do some clean up and post the code we’ve discussed about, as a reply here, plus I intend to post the final result for an overall feedback.
So, after being successful to get the deformation right by performing the matrix transformations on the shader, I found some time and tried to move the calculations to a c# script, but I was not able to repeat the same effect. The working method is using the following code (I only include the parts, relevant to the vertex animation):
Shader:
#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED
struct Attributes {
float3 positionOS : POSITION;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
float4 color : COLOR;
float4 custom : TEXCOORD1;
float4 custom2 : TEXCOORD2;
float4 custom3 : TEXCOORD3;
float4 custom4 : TEXCOORD4;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
Varyings UnlitPassVertex (Attributes input) {
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
//Butterfly Vertex Displacement Data
float3 center = input.custom3.xyz;
input.positionOS -= center;
float time = _Time.y;
float disAmount = GetDisplacementAmount();
float disSpeed = input.custom.y;
float mask = 1 - sin(PI * input.baseUV.x) - 0.06;
float s = sin(time * disSpeed); // for butterfly
// or : float s = sin(_Time.y * 10.0 + mask) // for bird
float flapping = (s + 0.5) * disAmount * mask;
input.positionOS += flapping * input.normalOS; //cyan (perpendicular) displacement
float wingContraction = saturate(s * 0.3) * saturate(mask) * -1.0 * 2 * (0.5 - sin(0.5 * PI * input.baseUV.x));
float3 rotation = float3(input.custom3.w, input.custom4.xy);
rotation.z = -rotation.z; // unity mesh particles have negated Z rotation due to legacy bug/behaviour
float4x4 transformMatrix = EulerToMatrix(rotation);
float3 defaultLocalRight = 1.5 * float3(1, 0, 0); //1.5 factor just suits better in my setup
float3 rotatedLocalRight = mul(transformMatrix, defaultLocalRight);
//float3 rotatedLocalRight = input.custom2.xyz; //Tried to create the rotated vector in c# and pass it here, but I failed! :(
input.positionOS -= wingContraction * rotatedLocalRight; //magenta (parallel) displacement
input.positionOS += center;
float3 positionWS = TransformObjectToWorld(input.positionOS);
output.positionCS_SS = TransformWorldToHClip(positionWS);
where EulerToMatrix
is the matrix given by @Handtohand above, edited to 3x3, by removing the 4th row and column, to keep only the rotation part.
The c# script:
var customDataModule = ps.customData;
customDataModule.enabled = false;
for (int i = 0; i < numParticlesAlive; I++)
{
if (particles[i].totalVelocity != Vector3.zero)
{
//Rotation aligned to velocity
Vector3 currentRotation = particles[i].rotation3D;
Vector3 n = particles[i].totalVelocity.normalized;
Vector3 x = new Vector3(n.z, 0.0f, -n.x).normalized;
float y = n.z * x.x - n.x * x.z;
//float rotX = Mathf.Clamp(Mathf.Atan2(-n.y, y) * Mathf.Rad2Deg, 0f, 45f);
float rotX = Mathf.Atan2(-n.y, y);
float rotY = Mathf.Atan2(-x.z, -x.x);
//float rotZ = Mathf.Clamp(Mathf.Atan2(-n.y, y) * Mathf.Rad2Deg, 0f, 45f);
float rotZ = Mathf.Atan2(-n.y, y);
Vector3 alignmentRotation = new Vector3(rotX, rotY, rotZ);
Vector3 eulerRotation = alignmentRotation * Mathf.Rad2Deg;
particles[i].rotation3D = Vector3.Lerp(
currentRotation, new Vector3(
eulerRotation.x, eulerRotation.y, eulerRotation.z), Time.deltaTime);
}
}
I guess what is done here is that the particles are rotated via the c# script to look at the totalVelocity
vector and the rotation values are passed via the rotation3d
vertex stream to the shader, where they feed the matrix. Finally the vertices are displaced along the normal vector’s direction for the perpendicular movement (the cyan arrow in the image above) and along the rotated default right
or float3(1,0,0)
vector for the parallel movement (magenta arrow).
If I however try the same calculations in the c# script and pass the rotated right
vector in a custom vertex stream the animation is messed up. I don’t understand why. Any suggestions or improvements? I don’t know if I should mark the answer as correct at the moment…
if it works in the shader, i would keep it there. it will probably be more efficient there :)
– Handtohand© 2022 - 2025 — McMap. All rights reserved.
The part with the rotation matrix "If you want to construct the “local space of the particle”, you’ll need to pass the center and rotation vertex streams, and construct a matrix. It’s a bit tricky, but here is some pseudocode that should work:..." has solved my main inquiry so I mark this as the solution.
– Deyoung