Friday, October 31, 2014

A basic dielectric with refraction

Another quick post. This is also not new, I've done this about a week ago, and its essentially an assembly of the theory posted by Kevin George that describes the Beer's Law of light absorbtion. He only posts some information about the main part of the shader (the logarithmic decay of the light), and so I went ahead and implemented the theory as well as the rest of the basic shader. This is not a physically plausible setup, and I am currently working on implementing a more generic, RSL 2.0 friendly, and physically plausible version that will eventually include some texture mapping support, and maybe other basic features. I am looking to implement a correct BRDF approach, and will see if I can actually utilize the GGX BRDF eventually. It is quite a bit of work, so I wouldn't expect this to be something that will come too soon, but still, its good to aim high!

test dielectric 01 from Dimitry Kachkovski on Vimeo.

Tuesday, October 28, 2014

Henchmen Teaser

I completely forgot to actually post the Henchmen Teaser, our lookdev piece, or a short, that basically sets the design and idea for our upcoming feature animated film with the same name - "Henchmen", which has gotten quite some publicity over the last few months (http://www.hollywoodreporter.com/behind-screen/siggraph-gary-sanchez-productions-produce-725064). If you haven't seen it yet, here is a chance to do so now! Enjoy!

HENCHMEN TEASER 2014 from Bron Studios Inc. on Vimeo.

New logo for BRON Studios!

Aaaaaaand its LIIIIVE! This is our new logo that many people will be seeing with most of the movies that BRON is going to be releasing and producing, and it features a whole bunch of stuff I did, such as the sim/lighting/rendering of the pouring lava, the sims of the steam, and the sims of the sparks. I have also done some finishing tweaks for comping. A huge props to Barry Andres for doing an awesome job lighting and comping most of this shot, and to Matthew Bilton for doing a kick ass previs/layout job (the text reveal is his baby as was all the layout of the environment to make it happen!). Enjoy!

BRON STUDIOS ANIMATED LOGO 2014 from Bron Studios Inc. on Vimeo.

P.S. If not showing here, just go to vimeo to check it out!

Monday, October 20, 2014

A soap bubble shader - first RSL learning experience

So as i start my process in moving towards LookDev and lighting departments, one of the necessary upgrades to my skill-set is learning shader development, specifically using renderman and RSL. So following a nice tutorial on digital tutors, this here is the result of my very first shader! Features included as i was going through the tutorial are raytraced fresnel reflections, thin-film light interference using distance and angular methods based on RGB spectrums (the rainbow effect on the soap bubble), and some noise based procedural displacements. Ah, and some gravity based thickness motion and general thickness variation (also noise based). As i learned all this, i will need to do a separate post on all the details i figured out so i can recap them, and hopefully remember a bit better.

bubble test from Dimitry Kachkovski on Vimeo.

Thursday, April 17, 2014

Dry Ice - Some fun with maya fluids

Was having a bit of fun with Maya's Fluid dynamics. This is so much fun! (And yea, I know, everyone does Dry Ice... That still doesn't stop it from being very fun to do! :D)

fluid test from Dimitry Kachkovski on Vimeo.

Saturday, April 12, 2014

Testing the UV export

To add to my last post on Alembic, I wanted to also share how you can check if your values got exported. One of the easiest ways is to check this using Alembic's python bindings to traverse you ".abc" file. I'll post an example to show the way to do it using general Alembic python module, but there is also a slightly easier way to access the alembic files via Cask (a module provided with Alembic's examples that helps reading and modifying ".abc" files with python in a more straight forward way). You can find and download it here: http://code.google.com/p/alembic/source/browse/#hg%2Fpython%2Fexamples%2Fcask.

(NOTE - this example is for linux, but can be modified pretty easily for windows)
So lets say you have a file called test_cache.abc located in "/home/userName/alembic_cache/". In the alembic file with have a simple sphere exported, named by default as "pSphere1". The test will look like this:
import imath, alembic

archive = alembic.Abc.IArchive('/home/userName/alembic_cache/test_cache.abc')
      
top = archive.getTop()
mesh = alembic.AbcGeom.IPolyMesh(top.children[0], 'pSphereShape1')
# Should display <class 'alembic.AbcGeom.IPolyMesh'>
print type(mesh)

schema = mesh.getSchema()
# Should display <class 'alembic.AbcGeom.IPolyMeshSchema'>
print type(schema)

arb_params = schema.getArbGeomParams()
# This should show the extra UVs you exported, and, if you did export the
# Vertex colors, those too.
print arb_params.getNumProperties()

# Assuming you didn't export any colors, and only one extra map:
uv_header = arb_params.getPropertyHeader(0)

# Lets make sure that it is indeed the right property, so print the name.
# Should be the same as the UV map in our file (Note, the one that WASN'T the
# active one at the time of export!)
print uv_header.getName()

v2f_param = alembic.AbcGeom.IV2fGeomParam(arb_params, uv_header.getName())

# Lets make sure that what we got back is indeed what we want! Should be "True"
print alembic.AbcGeom.IV2fGeomParam.matches(uv_header)

# Now we get the uv sample that containes the values for the UVs and their
# indices.
uv_sample = v2f_param.getIndexedValue(0)
# Should get <class 'alembic.AbcGeom.IV2fGeomParamSample'>
print type(uv_sample)

uv_values = uv_sample.getVals()
uv_indices = uv_sample.getIndices()

# Lets make sure that what we got back is what it should be.
# Check the type of uv_values: should be <class 'imath.V2fArray'>
print type(uv_values)
# ...and indices: should be <class 'imath.UnsignedIntArray'>
print type(uv_indices)

# Now lets check the values: should get something like V2f(0.432955, 0.681514)
# Note, the values will obviously be different from mine here...
print uv_values[0]
# ...and indices: should just be some integer like 3
print uv_indices[0]
This should get you going and help you test what you exported and see if it worked.

Hope that helps,

Cheers,

DK

Reaping the fruits of "Alembic" proportions

Many have heard about Alembic. It is now becoming the #1 used format for geometry data interchange in the VFX/Animation/Games communities. Created by the joined effort of Sony Imageworks and Industrial Light and Magic, all of us in the industry got a fantastic tool.

In my recent endeavors I had to work on modifying Maya's Alembic Exporter to support multiple UV sets exporting. I will try to document my whole experience here so not to loose the info I learned, and also, perhaps, assist someone later in achieving the same thing easier, and perhaps better then I did.

In essence, the actual theory behind the idea is not overly difficult, and Alembic supports such a possibility pretty straight forward. So, theoretically, I knew exactly what I was to do, or specifically:

  1. Parse if the object contains more then one UV set.
  2. Get those UVs
  3. Convert the data to a format that alembic can read and store.
  4. Store it.
  5. DONE!
Looking around the net for information, I stumbled onto a pretty large topic on the google alembic-discussion board:
From there I gathered some more detailed info on what needed to happen such as:
  • UVs are stored as V2fGeomParam with a scope of varying or facevarying.
  • The original UVs, the default ones, are stored within the PolyMeshSchema of the mesh we are exporting through it's setUVs function, which is able to store only a single V2fGeomParamSample.
  • Any additional Params need to be stored within the arbGeomParams of the PolyMeshSchema.
The biggest challenge I was going to face, as I learned quite quickly, was dealing with Alembic API. For such a popular tool, I have to say, the documentation of it is superbly limited. Having gone through the official documentation on http://code.google.com/p/alembic/, I realized that it was pretty much all the information they had... The rest had to be derived almost entirely from their example code (that had nearly no doc strings) and the actual header files (yea, I built the Doxygen, which, without docstrings, is also not the most useful tool...) .

Having been coding Python almost exclusively, digging in head-first into such a project was a bit of a shock... One really starts to look at code documentation with greater respect... So for the first 2 days I felt very much like this:

Well, I am glad to say that I did finally finish the implementation, and I will share it here. So if anyone is interested in adding the code to their own exporter, they will be able to export multiple UVs themselves. I still need to write the modifications to the AbcImporter for Maya so that the multi UVs will be read and added to the imported meshes, but for the time being this works for things like Maya->Katana pipelines.

So the main change I did was to modify the creator function of the MayaMeshWriter. There are two parts in that creator function that run for a SubD mesh export and a PolyMesh export. I will post the poly mesh one, and will specify what needs to be changed for the SubD stuff.

So you should replace this:
...
Alembic::AbcGeom::OPolyMesh obj(iParent, name.asChar(), iTimeIndex);
mPolySchema = obj.getSchema();

Alembic::AbcGeom::OV2fGeomParam::Sample uvSamp;

if ( mWriteUVs )
{
    getUVs(uvs, indices);
    if (!uvs.empty())
    {

 uvSamp.setScope( Alembic::AbcGeom::kFacevaryingScope );
 uvSamp.setVals(Alembic::AbcGeom::V2fArraySample(
     (const Imath::V2f *) &uvs.front(), uvs.size() / 2));
 if (!indices.empty())
 {
     uvSamp.setIndices(Alembic::Abc::UInt32ArraySample(
  &indices.front(), indices.size()));
 }
    }
}

Alembic::Abc::OCompoundProperty cp;
Alembic::Abc::OCompoundProperty up;
if (AttributesWriter::hasAnyAttr(lMesh, iArgs))
{
    cp = mPolySchema.getArbGeomParams();
    up = mPolySchema.getUserProperties();
}

// set the rest of the props and write to the writer node
mAttrs = AttributesWriterPtr(new AttributesWriter(cp, up, obj, lMesh,
    iTimeIndex, iArgs));

writePoly(uvSamp);
...
With this:
...
Alembic::AbcGeom::OPolyMesh obj(iParent, name.asChar(), iTimeIndex);
mPolySchema = obj.getSchema();
 
std::vector<Alembic::AbcGeom::OV2fGeomParam::Sample> uvSamps;
Alembic::AbcGeom::OV2fGeomParam::Sample defaultUV;
 
std::vector<float> uvs;
std::vector<Alembic::Util::uint32_t> indices; 
MStringArray uvSetNames;
lMesh.getUVSetNames(uvSetNames);
if ( mWriteUVs )
{
    MGlobal::displayInfo("Writing the default UVs");
    getUVs(uvs, indices);
 
    if (!uvs.empty())
    {
         defaultUV.setScope( Alembic::AbcGeom::kFacevaryingScope );
         defaultUV.setVals(Alembic::AbcGeom::V2fArraySample(
              (const Imath::V2f *) &uvs.front(), uvs.size() / 2));
         if (!indices.empty())
         {
              defaultUV.setIndices(Alembic::Abc::UInt32ArraySample(
                   &indices.front(), indices.size()));
         }
    } 
}

Alembic::Abc::OCompoundProperty cp;
Alembic::Abc::OCompoundProperty up;
if (AttributesWriter::hasAnyAttr(lMesh, iArgs))
{
        cp = mPolySchema.getArbGeomParams();
        up = mPolySchema.getUserProperties();
}

// set the rest of the props and write to the writer node
mAttrs = AttributesWriterPtr(new AttributesWriter(cp, up, obj, lMesh,
            iTimeIndex, iArgs));

writePoly(defaultUV);
        
//Write multiple UVs if present
if (uvSetNames.length() > 1)
{
 MGlobal::displayInfo("Will be exporting multiple UVs now!");
 Alembic::Abc::OCompoundProperty arbParams;
 arbParams =  mPolySchema.getArbGeomParams();
 
 MString lastUVSetName = lMesh.currentUVSetName();
 for (int i = 0; i < uvSetNames.length(); i++)
 {
  MString uvSetName = uvSetNames[i];
  
  if (uvSetName == lastUVSetName)
  {
   continue; 
  }
   
  std::string uvSetPropName = uvSetName.asChar();
 
  Alembic::AbcCoreAbstract::MetaData md;

  Alembic::AbcGeom::OV2fGeomParam uvProp(arbParams, uvSetPropName, true,
   Alembic::AbcGeom::kFacevaryingScope, 1, iTimeIndex, md);
  mUVParams.push_back(uvProp);
 }
 std::vector <Alembic::AbcGeom::OV2fGeomParam>::iterator uvIt;
 std::vector <Alembic::AbcGeom::OV2fGeomParam>::iterator uvItEnd;
      
 uvIt = mUVParams.begin();
 uvItEnd = mUVParams.end();

 for(; uvIt != uvItEnd; ++uvIt)
 {
  MString uvSetName(uvIt->getName().c_str());
  lMesh.setCurrentUVSetName(uvSetName);
  Alembic::AbcGeom::OV2fGeomParam::Sample uvSamp;    

  getUVs(uvs, indices);

  if (!uvs.empty())
  {
   uvSamp.setScope( Alembic::AbcGeom::kFacevaryingScope );
   uvSamp.setVals(Alembic::AbcGeom::V2fArraySample(
       (const Imath::V2f *) &uvs.front(), uvs.size() / 2));
   if (!indices.empty())
   {
    Alembic::Abc::UInt32ArraySample vals(&indices.front(), indices.size());
    uvSamp.setIndices(vals);
   }
  }
  uvIt->set(uvSamp);
  uvSamps.push_back(uvSamp);
 }
 lMesh.setCurrentUVSetName(lastUVSetName);
}
...
I am not sure how optimized that is, but it works. I more then welcome comments on how to improve this, btw! Now a few things to add. You need to include a definition of the mUVParams to the MayaMeshWriter.h file:
...
std::vector&ltAlembic::AbcGeom::OV2fGeomParam&gt mUVParams;
...
Now, I made a mistake here, which I learned is a big one. When you define the entry in the header, you will need to recompile most of the plugin to make sure your symbol table matches (I still need to read up more on that to know exactly what the meaning is, unfortunately, but as far as I understand, the mapping of the variables to the memory addresses gets messed up if you don't do the re-compile...). When I forgot to do that as I was testing the implementations, I got very weird memory crashes, until it hit me that perhaps my headers information wasn't being updated with the rest of the plugin.

And finally, the only thing needed to change for that code to suit it for the SubDiv part of MayaMeshWriter::MayaMeshWriter, is to replace all of the mPolySchema variables with mSubDSchema. Note the way OSubD stuff is defined in the code originally, and try to maintain that so that you don't screw up the schema writing process.

Hope that helps!

Cheers,

DK

Tuesday, February 25, 2014

SIGGRAPH University - "The Digital Production Pipeline"

An excellent talk on pipelines across VFX from Sigrgaph 2013.

Debugging Models and Views using ModelTest

If you are like me, and spend quite a bit of time making new tools that always seems to require more and more advanced ways of working with QModels and QViews, you might have ran into issues with Segmentation Faults and general problems where it is very hard to tell where an how you've gone wrong.
QT has a very neat little module in it's C++ library called ModelTest. This is basically a little virtual tester that keeps a watch on your model's activity, and as certain aspects of the model or it's data change, it analyzes the changes, and tests if all the changes go through the expected cycle.
If you are not using QT, but are rather on the side of using PySide or PyQt, there is a Python Version of ModelTest. It saved me a ton of time just recently where I had some very hard-to-track issues that would pop up without recognizable patterns.
Using ModelTest is super simple:
...
from ModelTest import ModelTest
model = MyModel()
parent.setModel(model)
ModelTest(model, self) # Second argument simply needs to be a parent to attach ModelTest to.
...

Happy Coding!

Friday, February 21, 2014

Getting mouse position within QAbstractItemView

Spent a few hours trying to make a presenter for a context menu that I am making within my UI. What I wanted to do was to allow for the context menu to be dynamic to what it appears above, regardless of what is currently selected. The problem I faced was that I needed to get the position of the mouse, and then map that global position to the view that I am using, and get the index of the model that is at the position I am looking at.

In order, here is how I proceeded.

When I create the context menu for a view, there is signal triggered, that emits a QPoint of where the mouse has been clicked:
...
# Connect the signal and the slot
self.view_context_menu = QtGui.QMenu()
self.view.customContextMenuRequested.connect(self.show_view_context_menu)

# The function to show the menu
def show_view_context_menu(self, pos):
    # We need to take the point we got from the signal, and map it to the world
    mapped_point = self.view.viewport().mapToGlobal(pos)
    # Now show the menu in the global space
    self.view_context_menu.exec_(mapped_point)
All seems well and simple, we got our menu, and we displayed it. But now, I want to get the index that might be at that point, and change the context menu accordingly. Lets say for now, I just want to see if the index is a valid one or not.
...
# Connect this to run right before the menu appears
self.view_context_menu.aboutToShow.connect(self.set_menu_action_states)
...

def set_menu_action_states(self):
    point = QtGui.QCursor.pos()
    index = self.get_item_at_point(point)
    print index.isValid()

def get_item_at_point(self, point):
    return self.view.indexAt(point)
Seems straightforward. However, what happens here, the point we get with QtGui.QCursor.pos() is in the world space of our monitor(s). So, whenever you will be trying to run this code, you will pretty much never get a valid index, because, as far as the view is concerned, you are not within it. So to solve this issue one has to transform the cursor position back into the view's coordinate system. But, you have to also remember something - all views in QT inherit the QAbstractItemView class, which in turn inherits the QAbstractScrollArea. What this means, is, the coordinate system of the actual view widget is a bit different then what we get when we are displaying the menu with customContextMenuRequested. The scroll area creates little widgets to drive the area display if some items in your view are hidden, allowing, well, for scrolling. Additionally, there are headers that the view posses, and those are different from what we see within the rest of the view. To be exact, everything we see withing the view, such as the view's items, are all sitting within the view's viewport coordinate system. If one has paid attention when we were showing the menu, we use the self.view.viewport().mapToGlobal() function to determine where the menu should appear. We have to do the same, but in reverse, when we are trying to find the indices under our mouse. So the corrected function looks like this:
def get_item_at_point(self, point):
    mapped_point = self.view.viewport().mapFromGlobal(point)
    return self.view.indexAt(mapped_point)
And that's it!

Tuesday, February 18, 2014

Alembic Maya - Edge Crease Export

Recently I encountered a request from our Rigging TD to see how we may be able to export meshes in Alembic format with edge crease information from Maya. Unfortunately, Alembic is still quite poorly documented in one single place, so it takes a bit of time to put all the information together to figure out how certain things are done. One of the things I ended up having to do was to actually traverse the AlembicExporter for Maya to learn how certain export processes work. The main Object that we need to look at to see how Alembic finds crease data is MayaMeshWrite.cpp. In this Object, if we look at lines 288-324, the constructor of the MayaMeshWriter object, we can see this chunk of code:
// check to see if this poly has been tagged as a SubD
MPlug plug = lMesh.findPlug("SubDivisionMesh");
if ( !plug.isNull() && plug.asBool() )
{
    Alembic::AbcGeom::OSubD obj(iParent, name.asChar(), iTimeIndex);
    mSubDSchema = obj.getSchema();

    Alembic::AbcGeom::OV2fGeomParam::Sample uvSamp;
    if ( mWriteUVs )
    {
        getUVs(uvs, indices);

        if (!uvs.empty())
        {
            uvSamp.setScope( Alembic::AbcGeom::kFacevaryingScope );
            uvSamp.setVals(Alembic::AbcGeom::V2fArraySample(
                (const Imath::V2f *) &uvs.front(), uvs.size() / 2));
            if (!indices.empty())
            {
                uvSamp.setIndices(Alembic::Abc::UInt32ArraySample(
                    &indices.front(), indices.size()));
            }
        }
    }

    Alembic::Abc::OCompoundProperty cp;
    Alembic::Abc::OCompoundProperty up;
    if (AttributesWriter::hasAnyAttr(lMesh, iArgs))
    {
        cp = mSubDSchema.getArbGeomParams();
        up = mSubDSchema.getUserProperties();
    }
    mAttrs = AttributesWriterPtr(new AttributesWriter(cp, up, obj,
        lMesh, iTimeIndex, iArgs));

    writeSubD(uvSamp);
}
In the code, you may notice that the exporter is looking for the plug "SubDivisionMesh" in the mesh that is being parsed. Now, when I looked at this the first time, I thought that the tag might be added when you use an actual SubD meshes created in Maya. But, alas, in Maya 2014, the support for the SubD meshes has been changed, and you no longer can even create Subdivision primitives...

After a few attempts I realized that the SubD meshes that I got converted from PolyMeshes were not containing the "SubDivisonMesh" plug that the exporter was looking for.

Over the weekend the Rigging TD that asked me to look into this, had a look as well, and saw what I was missing this whole time - the flag "SubDivisionMesh" was a bool tag that the user was supposed to add him/herself! And sure enough, as soon as you add the tag to the mesh (NOTE: Add the tag to the Shape node of the mesh, NOT the transform node. But export the Transform node. Makes sense, right??? If so, lucky you, cause I don't get the point...:( ) and export it, the mesh is exported using Alembic's OSubD and OSubDSchema, which is capable of storing the edge creases and corners, as per Alembic docs. The creases are exported in MayaMeshWriter::writeSubD function, in lines 834-862:
std::vector <Alembic::Util::int32_t> creaseIndices;
std::vector <Alembic::Util::int32_t> creaseLengths;
std::vector <float> creaseSharpness;

std::vector <Alembic::Util::int32_t> cornerIndices;
std::vector <float> cornerSharpness;

MUintArray edgeIds;
MDoubleArray creaseData;
if (lMesh.getCreaseEdges(edgeIds, creaseData) == MS::kSuccess)
{
    unsigned int numCreases = creaseData.length();
    creaseIndices.resize(numCreases * 2);
    creaseLengths.resize(numCreases, 2);
    creaseSharpness.resize(numCreases);
    for (unsigned int i = 0; i < numCreases; ++i)
    {
        int verts[2];
        lMesh.getEdgeVertices(edgeIds[i], verts);
        creaseIndices[2 * i] = verts[0];
        creaseIndices[2 * i + 1] = verts[1];
        creaseSharpness[i] = static_cast<float>(creaseData[i]);
    }

    samp.setCreaseIndices(Alembic::Abc::Int32ArraySample(creaseIndices));
    samp.setCreaseLengths(Alembic::Abc::Int32ArraySample(creaseLengths));
    samp.setCreaseSharpnesses(
        Alembic::Abc::FloatArraySample(creaseSharpness));
}
Another thing to note: the "SubDivisionMesh" plug does not need to be added to the export Attributes list in the Alembic Exporter or the script function AbcExport. The plug is searched for in the MayaMeshWriter constructor, and so any mesh object being exported will be checked for the presence of this flag.

Hopefully this saves someone out there the time it took me and my colleague to look all this up!

I'll keep writing more as I dig deeper into Alembic, and Alembics exporter for maya, so perhaps this blog will live again! Yay!