Hi all,
I try to have a behavior like the Matte / Shadow material in 3DSMax with Away3d 4.x, i.e. a 3d mask, in order to incrust 3D models in a 2D background.
And I really start to bang my head against the walls ‘^^
The way I try to implement it is to use the stencil actions.
Basically, I want the mask objects be invisible. If a masked object is in front of a mask object, it is visible.
If the masked object is behind a mask object, the parts of the masked object that overlay the parts of a mask object are invisible.
This example illustrate the concept.
Note that the robot (3D) is masked when it’s behind by the floor lamp (2D), but isn’t masked when it’s in front of the floor lamp.
So far, I managed to mask objects with the Context3D.setStencilActions() method, but the objects are masked even if they are in front of a mask object.
In pseudo-code, I would have :
if(pixel depth masked > pixel depth mask) {
return pixel
}
else{
do stencilAction;
}
For testing purpose, I’ve created 2 objects (a sphere and a cube) and added a custom property in their material (material.extra = {mask:masked or mask}).
Here is the code :
public function SimpleTest()
{
var bgImage:Sprite = new McBackgroundImage();
var bdBackgroundImage:BitmapData = new BitmapData(512, 512);
bdBackgroundImage.draw(bgImage);
//create a view with MaskRenderer
_view = new View3D(null,null,new MaskRenderer());
//add a background
_view.background = new BitmapTexture(bdBackgroundImage);
_view.antiAlias = 2;
addChild(_view);
var maskMaterial:ColorMaterial = new ColorMaterial(0xff0000);
var maskedMaterial:ColorMaterial = new ColorMaterial(0x00ff00);
var cube:CubeGeometry = new CubeGeometry();
var sphere:SphereGeometry = new SphereGeometry();
_cubeMesh = new Mesh(cube, maskMaterial);
_sphereMesh = new Mesh(sphere, maskedMaterial);
_cubeMesh.material.extra = {mask:"mask"};
_sphereMesh.material.extra = { mask:"masked" };
_view.scene.addChild(_cubeMesh);
_view.scene.addChild(_sphereMesh);
addEventListener(Event.ENTER_FRAME, handleEnterFrame);
}
private function handleEnterFrame(e : Event) : void
{
//rotate the cube
_cubeMesh.rotationY += .5;
_cubeMesh.rotationX += .5;
_cubeMesh.rotationZ += .5;
//rotate the sphere
_sphereMesh.z = 2 * Math.cos( getTimer() / 700 ) * 100;
_sphereMesh.x = 2 * Math.sin( getTimer() / 700 ) * 100;
_view.render();
}
}
Then, I’ve created a MaskRenderer that extends DefaultRenderer and override the draw() function. I created one EntityCollector for the mask objects and one for the masked objects.
Before drawing the mask objects, I set the StencilReferenceValue to 1 and set the stencilActions so that the objects aren’t drawn.
Then I set the stencilActions again to draw the parts of the masked objects that are outside the parts of the mask objects.
Here is the code of the draw() method (the other methods of DefaultRenderer are the same):
override protected function draw(entityCollector : EntityCollector, target : TextureBase) : void
{
// TODO: not used
target = target;
_context.setDepthTest(true, Context3DCompareMode.LESS);
_context.setBlendFactors(Context3DBlendFactor.ONE, Context3DBlendFactor.ZERO);
var entityCollectorMask:EntityCollector = new EntityCollector();
var entityCollectorMasked:EntityCollector = new EntityCollector();
//not optimized, just for testing purpose
if (entityCollector.opaqueRenderableHead.renderable.material.extra.mask == "mask") {
entityCollectorMask.applyRenderable( entityCollector.opaqueRenderableHead.renderable);
entityCollectorMasked.applyRenderable(entityCollector.opaqueRenderableHead.next.renderable);
}
else {
entityCollectorMasked.applyRenderable( entityCollector.opaqueRenderableHead.renderable);
entityCollectorMask.applyRenderable(entityCollector.opaqueRenderableHead.next.renderable);
}
// set a value to draw into the stencil mask buffer for each pixel.
_context.setStencilReferenceValue(1);
_context.setStencilActions( "frontAndBack", "never", "set", "set","set");
// draw the cube, and for each pixel of the cube, will be set to a value of 1 on the mask buffer.
drawRenderables(entityCollectorMask.opaqueRenderableHead, entityCollectorMask);
// change the stencil action to only draw the sphere if the pixel hasn't a value of 1.
_context.setStencilActions( "frontAndBack", "notEqual", "set", "set","set");
drawRenderables(entityCollectorMasked.opaqueRenderableHead, entityCollectorMasked);
// reset the stencil action to the default values to keep drawing the sceen normally.
_context.setStencilActions();
_context.setDepthTest(false, Context3DCompareMode.LESS);
if (entityCollector.skyBox) {
if (_activeMaterial) _activeMaterial.deactivate(_stage3DProxy);
_activeMaterial = null;
drawSkyBox(entityCollector);
}
drawRenderables(entityCollector.blendedRenderableHead, entityCollector);
if (_activeMaterial) _activeMaterial.deactivate(_stage3DProxy);
_activeMaterial = null;
}
It’s (strongly) inspired by that entry in the flare3d wiki.
Here is the result.
Is it possible to test the depth of a pixel before the stencil tests ?
Or is it just a matter of parameters in the Context3D.setStencilActions() method ?
Has someone already implement this behavior ?
Is this the right way to do it ?
Of course, it would be great if the mask objects can receive and cast shadows, but that’s another challenge ‘^^
Any help or hint will be much appreciated !
Thanks a lot !