Sunday, May 13, 2012

Why coding things takes so long

Work distribution has been... peculiar, this iteration. Observe.

  • Day 1: Completed all the two-box models I had scheduled for the iteration. Yay!
  • Nothing happens for a while.
  • Day 10: I've got so much time on my hands. Maybe I should do more work?
    Yes! I shall complete all the two-box models for the entire condo! Piece of cake!
    (writes down the task in the TODO list, then goes to sleep)
  • Nothing happens for a while.
  • Last day: Frantically creates two-box models for one extra room.
    Two more rooms left. Time's up; game over!

Well, now we know where most of the time went during this project.


For the next iteration, we intend to waste time in a totally different way. Because this time, it's a programming project! When programming, I think we tend to spend a disproportionate amount of time learning new libraries. Case in point: we want to parse OBJ files.

OBJ is a very simple file format for 3D meshes. Nadya asked me to look up how the OBJ format represents points on disk, and to write some pseudo-code for her. Well, I'm afraid I will again be done with my part of the work on the first day, because the format is very, very simple:

# this is a comment

# List of Vertices, with (x,y,z[,w]) coordinates, w is optional and defaults to 1.0.
v 0.123 0.234 0.345 1.0
v ...

The format is so simple that I don't really need to write down pseudo-code, but I will do so anyway, because I have a point to make.

m := create 3DS Max mesh object
for each line is the OBJ file:
  if the first character is 'v':
    split the line into words.
    x := second word
    y := third word
    z := fourth word
    add the point (x, y, z) to m's point list.

And that's it! That's the entire pseudo-code, because for this iteration, we're only interested in importing the points, not the faces.


Now, the algorithm is trivial, but we are still scheduling an entire 3 weeks to actually implement it. Why? Because we are not familiar with 3DS Max's API. We already know that the documentation contains errors. There might be all kinds of catches we haven't thought of and which we'll need to debug through. Really, the algorithm is trivial, but this has no incidence on the amount of time it will take to implement it! Familiarity with the necessary API is the dominant factor.

I feel like this is very representative of the work I do every day as a programmer. The reason coding things takes so long is not the complexity of the algorithms (they are often trivial), it's not that we work with low-level languages which force us to deal with unnecessary details (although we often do), it's mainly that we need to learn how to use new libraries all the time.

Well! I'll have a lot of time to ponder about that problem during the next three weeks. Because I'm responsible for the pseudo-code, and that part is done!


Oh, and I should also finish those two-box models. There are what, two rooms left? I think I'll wait until two days before the deadline...

Sunday, April 29, 2012

C# action item plugin for 3ds Max


It has been a while since the last time when I made some good progress on my 3D project. My guess is that I have had a lack of motivation and clear planning. And recently Sam has got a brilliant idea which should resolve most of my issues! We are doing Agile now!

Having a list of goals for a relatively short period is a good motivation for me: I work very well having visible and reasonable deadlines. And dealing with clear and shorter tasks helps to go forward with "baby steps". This is important given that I work on this project in my spare time.

So, here is my plan for this 3 week iteration! It has only one story: Creating a plugin that imports OBJ files into my scene.

You may want to ask - why do I need to create a whole plugin for a task that can be performed by simply clicking the Import menu? Well, due to our work process, I need a more sophisticated import procedure. I want to update the existing objects rather than delete and re-create them. Sam does modeling and I do texturing, and it is important to bring in his changes without deleting any material that are already in the scene.

So, my plugin will work in the following way. It will look up for the existing objects that are about to be updated and will rename them if necessary. Then it will proceed with the import. After the import is done, it will grab all the materials of the old objects and apply them to the new ones. After that, the old objects can be deleted.

This is the long term task. In this iteration I aim for a simpler goal: a simple, straightforward import of OBJ files. OBJ is a standard 3D object file format which we use to exchange files between our applications. I just want to familiarize myself with writing action item plugins and with the file import SDK.
And it is so super-easy to write these plugins with the new .NET SDK! I will show you how :)

First of all, I downloaded the Microsoft Visual C# 2010 Express from here. It is a free tool from Microsoft to create C# applications.

Then I went to the online SDK help to read how to create .NET plugins.

According to the help, I only needed to create an assembly project with a class that would implement the ICuiActionCommand interface. Once the dll is compiled, it should be placed to the bin\assemblies folder. The next time Max is launched, it will dynamically load it and register an instance of the class as an action in its CUI interface. Very simple!

In reality it was too, except that back then, when I started working on this, the online help had a bug. It said: "This is done by creating a public class that implements the interface ICuiActionCommand (found in MaxCustomControls.dll)". But it was not true! MaxCustomControls.dll did not have it! I searched all the help from the beginning to the end, and everywhere it was said the same thing. It was  pretty frustrating! Then I decided to text search my class name in the Max .NET assemblies, since they all have a textual description of the interface. And I found my class! It was hiding in the UiViewModels.dll, the namespace UiViewModels::Actions. UiViewModels was not documented at all; fortunately, this bug is fixed now.

I quickly implemented the action item interface, created a simple "Hello, World!" WPF form... And here is my action in 3dsMax!


I put a button with my action on the toolbar and launched the plugin:



Here is the code behind it (I omitted the window code, since it is trivial):


As you see, the code that launches the window is located in the Execute() function.
That's it! I am done with the action item plugin!

Now I need to write a simple import procedure, and this will be the subject of my next post.
Stay tuned!




Sunday, April 22, 2012

Plans for a cube-filled room

We're back! And this time, we've got a plan.

Given how much my other project has progressed since I began planning it using Agile methods, I would like to apply the same ideas to our 3D project. Iterations! Stories! Minimum viable product! Yes!

The first thing to decide is how long our iterations are going to be. This will give us limits for the scope of our first iteration's deliverable, and guide us in scheduling an appropriate number of stories. We chose 3 weeks. If the evolution of my other project is any indication, this length is bound to change after an iteration or two anyway, so the exact length is not very important.


Next, our Minimum Viable Product (MVP). The long-term plan is to model our entire condo in 3D, but that's far too large a scope. Especially given that Nadya, being a photography enthousiast, is aiming for photorealism. But with a deadline in three weeks, something has got to give! Should we favor quality or quantity?

That's a false dilemma. We aren't experienced enough to expect photorealism in any amount of time, and a 4½ condo contains a surprisingly large number of objects. So we shall cut both quality and quantity!

Did I say "cut"? We shall maim quality, and shred it to pieces. Observe:


A maimed room. Next to the sea of white. Low poly-count, affordable rent.


The objects are actually much more recognizable than I expected. What you see above is extreme minimalism: two boxes per object, no more, no less. Nadya will place my minimalist models at their proper location in her model of the condo, and I will refine them later. That's the reason why a single box is not allowed: that would make it hard for her to orient the models properly, and we might only notice the mistake once the models get refined.

For some models, the restriction was both challenging and insightful. The fridge, for example: its most distinguishing feature is the line which separates the freezer from the fridge proper. But how do I recreate this line with only two boxes? Well, I could make the freezer hover over the rest of the fridge, or I could make the top door slightly thicker or thiner than the bottom door, but neither approach gives satisfying results. Instead, I found another feature to highlight: the space between the door and the floor! I had never noticed that space before, but if it wasn't there, the fridge just wouldn't look right.


No, we don't keep our fridge in the bedroom.
It's for size comparison! Or something.


For the first iteration, we restrict our attention to a fraction of the condo, but there are still dozens of objects to model. In order to create those two-boxes models as efficiently as possible, I created a simple tool to align two boxes on top of each other.


Surprisingly, filling all of this takes less time than a
 round-trip to the bedroom to re-take a measurement.


Even though there are just two boxes, there are many ways in which the boxes could be placed relative to each other, and even more ways to take the wrong measure on the real-world object. The numbers above, for example, describe the chair. The first box is the base of the chair, and the second box is its back. To measure the height of this second part, I could have measured from the top of the chair to the floor, or from the top of the chair to its seat. To measure the depth of this second box, I could have measured the back's thickness, or the distance between the seat's front and the back. All of these combinations of measurements are easy to express using my tool, by using negative numbers and "reinterpret data" checkboxes.

Once the boxes have the proper size, I use the "face" menu and the "fit" checkboxes to position the second box relative to the first. The back of the chair, for example, has the same width as the base, and is aligned at the back. Done! Next object. To minimize the round trips even further, I bring my wireless keyboard with me to the bedroom and I blindly type notes remotely into an invisible text document. Spooky action at a distance!


Using this technique, I can create two-boxes placeholders for a large number of objects in a small amount of time. I still can't keep up with Nadya, though, who says that placing all of those models at their appropriate location is too easy. For this reason, she also scheduled a story for writing a plugin to import data from Houdini to 3DS Max! I can't wait to see how this pans out. More details in her next post...

Thursday, February 2, 2012

How to blend shapes along an axis. Also, a chair!

Today, I modelled a chair!

Nadya just loves this chair. After spotting it in a second-hand store,
she worried that if we did not hurry, somebody else might buy it first.

Fortunately, she is the only one who thinks this chair is awesome.


Ok, so it's only a chair, but just because you see chairs every day doesn't mean it's easy to model one. The most challenging part, for me, was the seat. The front of the seat curves downwards in order to wrap the legs, while the remainder of the seat curves inwards in order to wrap Nadya's ass. Important stuff! But easier said than done.

Thanks to the SoftTransform operator, adding a single curve to a planar surface is straightforward. Adding the second curve, however, is harder because the curves can interact with each other. To bypass the problem, I decided to model two single-curve seats, assuming that I could somehow blend one variant into the other.

Well, turns out blending is complicated enough that I can write an entire blog post on the subject. Enjoy.


Which seat would you rather sit on?

Answer: Sit on your own damn seat. This one is for Nadya's ass only!

I love Houdini, but I was a bit disappointed to discover that its BlendShape operator isn't flexible enough to perform the blending I needed. I will explain why in a moment, but before I get to that, let me explain what it is that I needed.

Blending, you see, involves interpolating the position of each point so that it lies between the position it would have in the first variant, and the position it would have in the second variant. This interpolation depends on a parameter, the blending parameter, which can vary between zero and one. Animating the parameter from zero to one gives the impression that the first shape is morphing into the second shape, and Houdini's BlendShape operator clearly has this use case in mind.

Above: Sphericubes!


Being able to create intermediate shapes is nice, but that's not what I had in mind. An intermediate between my two curved seats would be a seat which curves both downwards and inwards, simultaneously. That's not what I want! The seat I want to model has a downwards curve at the front, and an inwards curve at the back.

To use a sphericube-style example, I don't want a cube with bulging faces, I want the left side of a cube, the right side of a sphere, and something intermediate in the middle, whatever that may be.

Above: A cubisphere, whose "something intermediate in the
middle" turned out to look a lot less awkward than I expected.

Sadly, the BlendShape operator can create sphericubes, but not cubispheres.

It's a shame, really.


Well, punchline! I found a way to create them anyway. Yes! The screenshots are real!


Houdini has a very flexible operator called, simply, "Point". It loops over all the points of your shape, manipulating them according to a formula you write. Or rather, three formulas, one per axis.

Suppose you wanted to translate your shape to the right by one unit. The formula relating the original point positions to the translated positions is as follows.

(x, y, z) ↦ (x + 1, y, z)

Using the Point operator, you would write this instead.

$TX + 1
$TY
$TZ

And your points would be translated. Of course, you wouldn't use the Point operator for a simple translation; you would use a Transform operator, because its interface is simpler and its implementation is probably faster. You would only use the Point operator when, say, the existing operators let you down. I'm looking at you, BlendShape.

Above: Math. Also, a blending operator which blends along the Z axis!


Maybe I am being too harsh with BlendShape. Its limitation is not surprising. In fact, most operators, including the Transform operator, share this limitation: parameter values cannot vary from point to point. For example, when I translate a shape, I cannot tell Transform that the top of the shape should move to the right, while the bottom of the shape should move to the left. Outrage! Cripplingly limiting! Annoying, perhaps, but entirely unsurprising if your favorite 3D software is something more conventional, like 3DS Max.

As a Houdini user, however, I am rightfully surprised!


You see, the parameters in Houdini are not mere numbers. They are formulas, computing the right number from attributes of other nodes. It's very easy, for example, to scale a hat so that its new width coincides with the width of a head you have already created. The idea is that if you change your mind about the width of the head, you won't have to also adjust the hat.

At Houdini's, we sell hats which fit heads of all sizes.

Parameter formulas are also convenient for creating animations, by writing equations involving the current frame number. The point is that in Houdini, varying parameters according to stuff is common and expected. One subtlety to keep in mind, however, is that the node can't actually see the formula; instead, the formula is used to compute a number, and the node can only see that number.


It's a bit as if you were ordering Christmas lights online, and you had to pick a color. You can (and must!) consult your girlfriend, the weather, compare with your other decorations... You can vary your answer according to any number of factors, but your answer still can't vary along the length of the light rope. You must distill all the factors into a single color, say, red, and select "red" on the online form.

It might be possible, in theory, to alternate red and green lights along the rope, but the light rope manufacturer won't ship you one, because all it knows about your order is that you clicked on "red". Similarly, it might be possible to use a 0.1 blend factor on the left of the cubisphere and 0.9 on the right, but the BlendShape operator won't ship you one, because all it knows about your order is that you clicked on "0.5".

The above is purely theoretical.


If you liked my Christmas lights example better than my sphericube example, be sure to check Nadya's post. She is texturing a light rope!

She worked on that labor of love right here, on the properly-blended curves of this very chair.

Wednesday, December 28, 2011

Christmas Lights

The decorative lights the neighbors put on their bushes and houses for Christmas and the last work of Sam (the initials) inspired me to do the following exercise. I decided to write our initials with a LED rope.

Unfortunately I could not use the letters from Sam. I needed single stroke letters, so they could be used as a path for a cylinder which represents a rope.
I was not able to find a single stroke font, so I had to make one myself. Obviously, 3ds Max was a bad choice for font manipulation due to its poor set of vector graphics tools. Instead, I used Inkscape, a perfect app for vector  manipulations.

First, I needed to find a prototype font that would have a fancy shape that would go well with the festive mood of Christmas. Monotype Corsiva seemed to be the best match.



I traced the letters from inside. Then I simplified the resulting path and made the curves more round remembering that the LED rope is rigid and does not bend very well:

(the curly tail of the S letter was eventually given up)


I am sure you've noticed that the paths intersect. The intersection spots required more work. In real life ropes do not go through each other; instead they lay on top of each other. More bezier curve editing after the import - and the paths were not flat anymore:


 


The next step was to make a LED material.

It should be a self-illuminating material, since the rope emits light.
I used Architectural Material. For bump map I used the bump map from Transparent Plastic material.


For diffuse and intensity maps I used a gradient ramp, since it is the base of any light. The light part represented a light bulb, and the dark part was a plastic wrap cord. Since the rope is round, the inside bulbs are visible from any point of view. Thus, I used a linear white-red gradient and mirrored it along its light end. Also I turned the gradient 90 degrees on the VW axis and added some noise to imitate the slightly uneven structure of the plastic wrap.


I rendered my sample LED cylinder, and it looked like a Santa Clause cane, not like a LED rope! The gradient step was way too small.


How to make it bigger? 

I applied the Unwrap UVW modifier and adjusted the UVW map to make the gradient look proper. It was a mistake, since I planned to copy my cylinder around for each letter. Once I started messing up with UVW map, it got converted from being flexible to being fixed. This means that the UVW map got attached to the object vertices. Since every letter cylinder has a different length, its UVW map got streched or shrunk. This made my material be unevenly distributed on each letter (the N letter map distortions were exaggerated a bit to make them more clear):


This approach would require me to adjust a UVW map of the every letter, and the results would not be consistent. Not good.
While looking for a workaround I went again through the Gradient Ramp options. And I discovered the Size field!


It was exactly the value I needed to manipulate in order to adjust the real gradient size.
Here is what I got as a result! A LED rope shaped in the form of our initials!



Happy New Year!

Saturday, December 17, 2011

How hard can it be? Extruded, beveled text in Houdini.

Nadya asked me to create a simple 3D model to help her practice texturing. Specifically, she asked me for the letters "N & S", big and white, like the Hollywood letters on their mountain.

At first, I was a bit disappointed. Extruded letters? That's it? Seems like a waste of my talent. Might as well ask a Photoshop user if she knows how to create a lens flare effect! In fact, extruding text is such a common task that the Extrude operator's icon is an extruded "T".
  1. Lay down a Font operator, changing the text for "S & N".
  2. Lay down an Extrude operator, reducing the depth if necessary.
  3. There is no step three!
3D modelling is rarely that easy.


That was easy, but Nadya was not happy with the result. The edges are way too sharp! Real-world 90 degree angles don't look like that.

This is the sharpest-looking corner in the room.
Can you spot the bevel?


If real-world objects had true 90 degree angles, their corners would be dangerously sharp. So while the faces of the table above are at 90 degrees from one another, they do not actually meet at 90 degrees. In fact, the faces themselves do not meet. There is a very thin 45 degrees rectangle beween them, and a tiny triangle at the corner, joining the thin rectangles formed by the three edges. This is a very common setup, called a "bevel".

The bevel should be much easier to spot this time.


By increasing the size of the bevel and adding more intermediate angles than just 45°, corners can be made to look very smooth. Let's add some bevel to our extruded text.

Something doesn't feel right about this bevel.


Yikes, spikes! As you can see, laying down a Bevel operator is not enough. The operator handled the S just fine, but something about the ampersand geometry is messing things up. What is it?

Who put these inner edges on my ampersand?


One of the problems is that the ampersand has holes. Representing an area with holes requires more than one curve: one for the area's perimeter, and one for each of the holes. In a node-based modeller like Houdini, however, this representation has the disadvantage that each operation needs to take the possibility of holes into account. For this reason (or maybe for historical reasons), Houdini uses a simpler representation which assumes that faces never have holes, and can thus always be represented using a single curve.

Faces which do have holes need to be cut in a special way, so that they don't really have holes, yet look like they do.

A convoluted perimeter, yes, but no holes.


This representation is usually adequate. Most operators preserve the shape of the face, so even after many transformations, the fake hole usually continues to look real. There is one operator, however, which does not preserve the shape of the face. This operator is none other than our spike-producing friend, the Bevel operator!

What you want (left) is not always what you get (right).

This is an "inset", the first step towards a bevel.
Houdini doesn't have an Inset operator, but you can easily
make one by playing with the parameters of the Extrude operator.


Now that we understand the cause of the problem, how do we fix it?

I still hope there is a better way, but the solution I found was to make two copies of the ampersand. The inner edges can be moved elsewhere along the face without compromising the look of the hole, so I can place the inner edges at different places on each copy. This way, the bad parts of the beveled ampersand will look fine on the other copy.

I was planning to use a Knife operator to slice away the bad parts of each copy. While playing with the Knife operator, I discovered that slicing a fake hole has the nice side-effect of replacing the fake hole's inner edge with the knifed-in edge. Convenient!

The Knife operator adds, but also replaces inner edges.


Now that we have an easy way to move inner edges around, we are ready to complete the ampersand bevel.

A mother ampersand splits into two baby half-ampersands,
who in turn grow and become 3D half-ampersands. They then
find a mate of the opposite half, with whom they live in symbiosis,
forming another mother ampersand. Thus completing the circle of life.


The result looks good this time. To make it look even better, we can increased the number of points each letter uses to approximate its font curves. Unfortunately, this leads to more problems!

The acute angle at the bottom-right of the ampersand has an artifact which only shows up with the increased point density. Fortunately, this corner is made of straight edges, so we don't actually need the increased point density. Using the multi-purposed Facet operator, we can clean up the polygon so that straight edges don't use so many points, thereby getting rid of the artifact.

Extra divisions are useful to make curved segments smoother.
Extra divisions in the straight segments, on the other hand,
are worse than useless: they can actually produce artifacts!


One last Facet operator to ensure the front faces are completely flat, and voilĂ ! We're ready for a nice, extruded, beveled, boring piece of rendered 3D text.

I guess I should mention somewhere that
"S & N" stands for "Samuel and Nadya".
Yup, that's us!


I wonder what Nadya is going to create using this? To find out, stay tuned!

Sunday, December 11, 2011

Lighting Problem II

See the part I here.

I followed the advice of my friend designer, and replaced Autodesk Hardwood material with Arch & Design Material. The wooden and bump bitmaps were taken from the Autodesk Hardwood.
The radius of the ball was not changed, and it is 19 cm.

This time the rendering results have improved quite significantly.
Here is the result with a bit of glossiness (0.05):





For comparison here is the rendering with 0 glossiness:

Top right corner is not rendered


There is still some parasite lighting on the sides of the ball, but much less than in the previous scene with Hardwood material.


However, perspective rendering with 0 glossiness shows a lack of volume on the picture. The ball looks flat, and it seems to be a 2D image attached to the scene. A specular light is missing: