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

 
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:

 

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.