Inherit Texel Color From Emitter |
Particle
Flow does not provide a direct way to "paint" the color of the particles
based on the emitter's material,
something that is possible in the old 3dsmax particle systems.
The following
technique should be officially considered a "hack", but it can give some nice
results when used correctly.
In this tutorial, we will "steal" a single color from the underlying Emitter
geometry.
Natural Language - Setup and Scripts |
Generate Particles on the surface of a mesh object emitter using either a
Birth Operator plus Position Object operator, or a Birth Script.
Give the
Particle an initial speed along the emitter's normals using the Speed By
Surface operator
Assign to
the particles the same material used by the emitter mesh.
Assign a Shape to the particles (by any available method)
Add a Script Operator which:
Prepares a temp. Editable_Mesh in the scene for UV manipulations
Acquires the current Shape of every particle and assigns it to the temp. Editable_Mesh in the scene
Generates new UV coordinates for the temp. Editable_Mesh with a single texture vertex
Reads the velocity (and thus inquires the surface normal of the current particle!)
Calculates a ray going back along the normal towards the surface
Using an IntersectRayEx call, hits the surface of the Emitter from the position of the particle using the Ray
Using the Barycentric coordinates returned by the above call, gets the UV coordinates of the intersection point.
Assigns the UV coordinate to the single texture vertex of the temp. Editable_Mesh.
Assigns Texture Faces all pointing at the single texture vertex
Assigns the mesh of the temp. Editable_Mesh back to the particle's shape.
Particle View Setup |
In this sample setup, a Teapot called Teapot01 is used as the Emitter.
The same material (7 - Default) is assigned to both the Teapot and to the Particles in the flow using a Material Static Operator.
A Cache in the PF Source ensures faster updates
The Event 01 contains:
A Birth operator which creates 2000 particles on frame 0.
A Position Object operator which places the particles on the surface of the Teapot
A Speed By
Surface operator which references the same Teapot and accelerates the
particles along the surface normals.
Note that you could add a Speed operator right after the Script Operator to
stop the particles if you want them to stay on the surface.
The first Speed By Surface is vital for the script as it supplies
information about the surface normals of the Emitter!
A Shape
operator assigns a Sphere to every particle.
You can assign instanced geometry or use a Script Operator to assign any
shape to the particles.
Obviously, you need a mesh shape to be able to assign the texture
coordinates to it...
The Script
Operator (which contains the script listed below) changes the texture
coordinates of the shapes assigned to the particles.
It is VERY important to define the position, the speed and the shape of the
particles BEFORE the Script Operator!
Finally, there is a Display operator set to display Geometry, so you can see the texture on the particles
Complete Script |
on ChannelsUsed pCont
do
(
pCont.usePosition = true
pCont.useShape = true
pCont.useAge = true
pCont.useSpeed = true
)
on Init pCont do
(
global temp_emesh =
$Temp_Emesh01
if temp_emesh == undefined do
(
temp_emesh = Editable_mesh()
temp_emesh.name =
"Temp_Emesh01"
temp_emesh.renderable = false
)
)
on Proceed pCont do
(
count = pCont.NumParticles()
theChannel = 1
the_source = $Teapot01
for i in
1 to count do
(
pCont.particleIndex = i
if
pCont.particleAge == 0 do
(
temp_emesh.mesh = pCont.particleShape
meshop.setMapSupport temp_emesh theChannel true
meshop.setNumMapVerts temp_emesh theChannel 1
tfcount =
meshop.getNumMapFaces temp_emesh theChannel
pVel =
pCont.particleSpeed
thePos =
pCont.particlePosition + pVel
theRay = Ray
thePos -pVel
arr =
(intersectRayEx the_source theRay)
if arr != undefined do
(
tf = meshop.getMapFace the_source theChannel arr[2]
tv1 = meshop.getMapVert the_source theChannel tf.x
tv2 = meshop.getMapVert the_source theChannel tf.y
tv3 = meshop.getMapVert the_source theChannel tf.z
tv = tv1*arr[3].x + tv2*arr[3].y + tv3*arr[3].z
meshop.setMapVert temp_emesh theChannel 1 tv
for f = 1 to tfcount do
meshop.setMapFace temp_emesh theChannel f [1,1,1]
)
pCont.particleShape = temp_emesh.mesh
)--end if age
)--end i loop
)--end on
on Release pCont do
(
)
Final Result |
Click here for a preview AVI (1.2 MB, Cinepak Codec)
Click here for a 3dsmax 5.1 scene (55K, will load in 3ds max 6 and above)
Below are some experimental AVIs generated while developing this technique.
Teapot using Box particles and a procedural Cellular texture.
In this example, there are 40x40 boxes using the same code described in this
tutorial
This causes the resulting "particle image" to be vert low
resolution. Compare with the next one...
A plane with a box particles created on all Emitter vertices.
This time, using a slightly modified version of the script, each VERTEX of the
shape is assigned a corresponding texture coordinate
which leads to a very clear picture of the emitter texure projected onto the
particles!
A similar setup, but this time the particles are spheres
Step-By-Step Comments |
on ChannelsUsed pCont do
(
The ChannelsUsed handler defines the channels to be used by the Script Operator - you cannot get or set particle related values from the particle container without specifying which properties you need access to. This way, Particle Flow does not have to provide the Script Operator with all possible channels (and there can be an arbitrary number of channels in Particle Flow) but only with those that are actually needed. This conserves memory!
The parameter pCont contains the Particle Container.
pCont.usePosition = true
We will need the position of each particle in order to acquire the texture coordinates at the respective surface point of the Emitter mesh.
pCont.useShape = true
We are going to manipulate the texture coordinates of the particle shape, so we need access to it!
pCont.useAge = true
We will calculate texture coordinates only for new - born particles, so we have to read the age...
pCont.useSpeed = true
And finally we will use the speed of the particles to determine the normal vector at the Emitter's surface.
)
on Init pCont do
(
The Init handler is used to initialize the Script Operator.
The parameter pCont contains the Particle Container.Here, we will prepare a temporary scene object which will be used as a depot for the shape mesh to be manipulated
global temp_emesh = $Temp_Emesh01
We define a global variable called temp_mesh and assign a scene object to it.
If this scene object does not exist, the variable will contain the value undefined, otherwise it will point at the scene object.
if temp_emesh
== undefined do
(
If this scene object does not exist, the variable will contain the value undefined, so we will have to create the object!
temp_emesh = Editable_mesh()
We create a new Editable Mesh object in the scene and assign to the same global variable.
temp_emesh.name = "Temp_Emesh01"
Then we assign the name to the object...
temp_emesh.renderable = false
...make the object non-renderable...
hide temp_emesh
...and finally hide the object to make it invisible to the viewer. Note that MAXScript does not care much about visibility of objects and
can access and manipulate all objects in the 3dsmax scene database regardless of their hidden state!
)
This is the end of the if context
)
This is the end of the Init handler.
on Proceed pCont do
(
The Proceed handler is called every time the Script Operator is evaluated by Particle Flow.
It contains the actual body of the script.
The parameter pCont contains the Particle Container which contains all particles in the current Event the Operator can be applied to.
count = pCont.NumParticles()
Here we read the current number of particles in the particle container.
We will access every one of them in the following loop.
The reason we assign the value to a variable is that in the for loop that follows, the to limit is evaluated after each cycle of the loop to decide whether the i variable is greater than the limit.
Using the pCont.NumParticles() method call inside the for loop would call the method n times where n is the number of particles.
With the current code though, the method will be called just once and this will make the script faster!
theChannel = 1
The variable theChannel contains the index of the texture channel to be accessed on both the Emitter and the particles.
If you want to use a different channel, you should make sure that the Emitter has UV coordinates in the new channel, and that this variable uses the same value!
the_source = $Teapot01
The variable the_source contains the mesh object referenced by the Position Object and Speed By Surface Operators.
You could even access the actual object found in one of these Operators here in order to update the script automatically whenever
the Mesh Source changes. If you want to switch from Teapot01 to Sphere02, you should disable the Particle Flow, replace the mesh
in both Operators AND edit the above line to reference the correct object. Then you can enable the Particle Flow again.
for i in 1
to count do
(
Now we repeat the following code block for every single particle by using a for loop which counts from 1 to the number of particles. The variable i will contain the current particle index.
pCont.particleIndex = i
In order to work with multiple particles, we have to specify the current particle to access.
Setting the .particleIndex property of the Particle Container to the i variable will make the i-th particle the current one.
Any subsequent particle property access calls will be directed to that particle!
if pCont.particleAge == 0 do
(
The following code should only be applied to newly born particles - those whose Age is still 0.
temp_emesh.mesh = pCont.particleShape
We snapshot the current shape of the current particle and assign it to the .mesh property of the temporary Editable Mesh object we created in the scene.
NOTE that for this setup to work, the Shape Operator MUST be placed in the Event BEFORE the Script Operator, so we can get a valid shape...
meshop.setMapSupport temp_emesh theChannel true
Now we enable texture coordinates support for the temporary mesh in the scene.
Note that this will automatically generate default texture faces in the mesh!
meshop.setNumMapVerts temp_emesh theChannel 1
Next we tell the mesh that there is one single texture vertex in the map channel.
tfcount = meshop.getNumMapFaces temp_emesh theChannel
We also read the number of map faces, which will always be equal to the number of faces in the mesh.
pVel = pCont.particleSpeed
Since we assigned a Speed By Surface operator after the Birth but BEFORE the Script Operator, the particle already knows its velocity.
And because the Speed By Surface is set to use the Normals, the velocity basically contains a vector parallel to the emitter's surface at the point of birth!
thePos = pCont.particlePosition + pVel
Knowing the normal vector of the surface at the point the particle was born, we define a position that is defined by the particles's position, but offset by the velocity.
This is basically the position the particle projected in the future :o)
theRay = Ray thePos -pVel
From that future point which would lie above the surface, we generate a Ray value which points back along the inverted normal at the surface.
Obviously, this is a ray that hits the surface at the point the particle was born!
arr = intersectRayEx the_source theRay
Now we intersect the surface of the Emitter with the ray we just defined.
The intersectRayEx function is very useful as it returns the barycentric coordinates of the intersection point.
These coordinates are the point expressed as an equation containing the 3 vertices of the face the point lies in.
The result arr is an array of values which containing the face that was hit and the normal at the point.
if arr != undefined do
(
The intersectRayEx will return undefined if the mesh was not hit by the ray. In this case, we would do nothing.
If the mesh was hit, we go on...
tf = meshop.getMapFace the_source theChannel arr[2]
The face the intersection point lies in is the second value in the array.
Knowing that face indices and texture face indices have a one-to-one correspondence,
we use that index to get the definition of the texture face.
The definition is a Point3 value containing the indices of the 3 texture vertices used by the 3 mesh vertices.
tv1 =
meshop.getMapVert the_source theChannel tf.x
tv2 = meshop.getMapVert the_source theChannel tf.y
tv3 = meshop.getMapVert the_source theChannel tf.z
Using the definition of the texture face, we collect the 3 texture vertices from the source.
These values are also Point3 values containing texture coordinates.
tv = tv1*arr[3].x + tv2*arr[3].y + tv3*arr[3].z
Using the barycentric coordinates which are stored in the 3rd element of the intersectionRayEx result array,
we calculate the texture coordinate corresponding to the intersection point.
meshop.setMapVert temp_emesh theChannel 1 tv
Now we can assign this value to the single texture vertex in the particles's shape currently sitting in the temporary Editable Mesh in the scene.
for f = 1 to tfcount do
meshop.setMapFace temp_emesh theChannel f [1,1,1]
Then we redefine all texture faces to use only that single vertex.
This means that the COMPLETE mesh of the particle shape will point at the same texture coordinates and thus show a contant color
which is the color of the texel corresponding to the acquired texture coordinates at the intersection point!
)
pCont.particleShape = temp_emesh.mesh
Finally, we copy the trimesh value from the mesh in the scene to the current particle shape.
Since the particle system was assigned the same material as the emitter, the particle should have the same color
as the texel on the Emitter's surface below it!
)--end
if age
)--end i loop
)--end on
on Release pCont do
(
The Release handler is usually needed to do cleanup work, but we do not need it this time around.
)
Copyright © 2004 by Borislav 'Bobo' Petrov. |