By Chad Austin
For clarity, I slightly oversimplified my previous discussion on efficiently rendering Flash in a 3D scene. The sticky bit is extracting transparency information from the Flash framebuffer so we can composite the overlay into the scene.
Flash does not give you direct access to its framebuffer. It does, with IViewObject::Draw, allow you to composite the Flash framebuffer onto a DIB section of your choice.
Remembering your Porter-Duff, composition of source over dest is:
Color = SourceColor * SourceAlpha + DestColor * (1 – SourceAlpha)
If the source color is premultiplied, you get:
Color = SourceColor + DestColor * (1 - SourceAlpha)
Assuming we want premultiplied color and alpha from Flash for efficient rendering in the 3D scene, applying the above requires solving for FlashAlpha and FlashColor:
RenderedColor = FlashColor * FlashAlpha + DestColor * (1 - FlashAlpha) RenderedColor = FlashColor * FlashAlpha + DestColor - DestColor * FlashAlpha RenderedColor - DestColor = FlashColor * FlashAlpha - DestColor * FlashAlpha RenderedColor - DestColor = FlashAlpha * (FlashColor - DestColor) FlashAlpha = (RenderedColor - DestColor) / (FlashColor - DestColor)
If FlashColor and DestColor are equal, then FlashAlpha is undefined. Intuitively, this makes sense. If you render a translucent black SWF on a black background, you can’t know the transparency data because all of the pixels are still black. This doesn’t matter, as I’ll show in a moment.
FlashColor is trivial:
RenderedColor = FlashColor * FlashAlpha + DestColor * (1 - FlashAlpha) RenderedColor - DestColor * (1 - FlashAlpha) = FlashColor * FlashAlpha FlashColor = (RenderedColor - DestColor * (1 - FlashAlpha)) / FlashAlpha
FlashColor is undefined if FlashAlpha is 0. Transparency has no color.
What do these equations give us? We know RenderedColor, since it’s the result of calling IViewObject::Draw. We have control over DestColor, since we configure the DIB Flash is drawn atop. What happens if we set DestColor to black (0)?
FlashColor = (RenderedColorOnBlack) / FlashAlpha
What happens if we set it to white (1)?
FlashColor = (RenderedColorOnWhite - (1 - FlashAlpha)) / FlashAlpha
Now we’re getting somewhere! Since FlashColor and FlashAlpha are constant, we can define a relationship between FlashAlpha and RenderedColorOnBlack and RenderedColorOnWhite:
(RenderedColorOnBlack) / FlashAlpha = (RenderedColorOnWhite - (1 - FlashAlpha)) / FlashAlpha RenderedColorOnBlack = RenderedColorOnWhite - 1 + FlashAlpha FlashAlpha = RenderedColorOnBlack - RenderedColorOnWhite + 1 FlashAlpha = RenderedColorOnWhite - RenderedColorOnBlack
So all we have to do is render the SWF on a white background and a black background and subtract the two to extract the alpha channel.
Now what about color? Just plug the calculated FlashAlpha into the following when rendering on black.
FlashColor = (RenderedColor - DestColor * (1 - FlashAlpha)) / FlashAlpha FlashColor = RenderedColorOnBlack / FlashAlpha
Since we want premultiplied alpha:
FlashColor = RenderedColorOnBlack
Now that we know FlashColor and FlashAlpha for the overlay, we can copy it into a texture and render the scene!
By Chad Austin
In my previous post, I talked about how to embed Flash into your desktop application, for UI flexibility and development speed. This time, I’ll discuss efficient rendering into a 3D scene.
Rendering Flash as a 3D Overlay (The Naive Way)
At first blush, rendering Flash on top of a 3D scene sounds easy. Every frame:
- Create a DIB section the size of your 3D viewport
- Render Flash into the DIB section with IViewObject::Draw
- Copy the DIB section into an IDirect3DTexture9
- Render the texture on the top of the scene
Ta da! But your frame rate dropped to 2 frames per second? Ouch. It turns out this implementation is horribly slow. There are a couple reasons.
First, asking the Adobe flash player to render into a DIB isn’t a cheap operation. In our measurements, drawing even a simple SWF takes on the order of 10 milliseconds. Since most UI doesn’t animate every frame, we should be able to cache the captured framebuffer.
Second, main memory and graphics memory are on different components in your computer. You want to avoid wasting time and bus traffic by unnecessarily copying data from the CPU to the GPU every frame. If only the lower-right corner of a SWF changes, we should limit our memory copies to that region.
Third, modern GPUs are fast, but not everyone has them. Let’s say you have a giant mostly-empty SWF and want to render it on top of your 3D scene. On slower GPUs, it would be ideal if you could limit your texture draws to the region of the screen that are non-transparent.
Rendering Flash as a 3D Overlay (The Fast Way)
Disclaimer: I can’t take credit for these algorithms. They were jointly developed over years by many smart engineers at IMVU.
First, let’s reduce an embedded Flash player to its principles:
- Flash exposes an IShockwaveFlash [link] interface through which you can load and play movies.
- Flash maintains its own frame buffer. You can read these pixels with IViewObject::Draw.
- When a SWF updates regions of the frame buffer, it notifies you through IOleInPlaceSiteWindowless::InvalidateRect.
In addition, we’d like the Flash overlay system to fit within these performance constraints:
- Each SWF is rendered over the entire window. For example, implementing a ball that bounces around the screen or a draggable UI component should not require any special IMVU APIs.
- If a SWF is not animating, we do not copy its pixels to the GPU every frame.
- We do not render the overlay in transparent regions. That is, if no Flash content is visible, rendering is free.
- Memory consumption (ignoring memory used by individual SWFs) for the overlay usage is O(framebuffer), not O(framebuffer * SWFs). That is, loading three SWFs should not require allocation of three screen-sized textures.
- If Flash notifies of multiple changed regions per frame, only call IViewObject::Draw once.
Without further ado, let’s look at the fast algorithm:
Flash notifies us of visual changes via IOleInPlaceSiteWindowless::InvalidateRect. We take any updated rectangles and add them to a per-frame dirty region. When it’s time to render a frame, there are four possibilities:
- The dirty region is empty and the opaque region is empty. This case is basically free, because nothing need be drawn.
- The dirty region is empty and the opaque region is nonempty. In this case, we just need to render our cached textures for the non-opaque regions of the screen. This case is the most common. Since a video memory blit is fast, there’s not much we could do to further speed it up.
- The dirty region is nonempty. We must IViewObject::Draw into our Overlay DIB, with one tricky bit. Since we’re only storing one overlay texture, we need to render each loaded Flash overlay SWF into the DIB, not just the one that changed. Imagine an animating SWF underneath another translucent SWF. The top SWF must be composited with the bottom SWF’s updates. After rendering each SWF, we scan the updated DIB for a minimalish opaque region. Why not just render the dirty region? Imagine a SWF with a bouncing ball. If we naively rendered every dirty rectangle, eventually we’d be rendering the entire screen. Scanning for minimal opaque regions enables recalculation of what’s actually visible.
- The dirty region is nonempty, but the updated pixels are all transparent. If this occurs, we no longer need to render anything at all until Flash content reappears.
This algorithm has proven efficient. It supports multiple overlapping SWFs while minimizing memory consumption and CPU/GPU draw calls per frame. Until recently, we used Flash for several of our UI components, giving us a standard toolchain and a great deal of flexibility. Flash was the bridge that took us from the dark ages of C++ UI code to UI on which we could actually iterate.
By Chad Austin
Writing user interfaces is hard. Writing usable interfaces is harder. Yet, the design of your interface is your product.
Products are living entities. They always want to grow, adapting to their users as users adapt to them. In that light, why build your user interface in a static technology like C++ or Java? It won’t be perfect the first time you build it, so prepare for change.
IMVU employs two technologies for rapidly iterating on and refining our client UIs: Flash and Gecko/HTML. Sure, integrating these technologies has a sizable up-front cost, but the iteration speed they provide easily pays for them.
Rapid iteration has some obvious benefits:
- reduces development cost
- reduces time to market
and some less-obvious benefits:
- better product/market fit: when you can change your UI, you will.
- improved product quality: little details distinguish mediocre products from great products. make changing details cheap and your Pinto will become a Cadillac.
- improved morale: both engineers and designers love watching their creations appear on the screen right before them. it’s why so many programmers create games!
I will show you how integrating Flash into a 3D application is easier than it sounds.
Should I use Adobe Flash or Scaleform GFx?
The two most common Flash implementations are Adobe’s ActiveX control (which has a 97% installed base!) and Scaleform GFx.
Adobe’s control has perfect compatibility with their tool chain (go figure!) but is closed-source and good luck getting help from Adobe.
Scaleform GFx is an alternate implementation of Flash designed to be embedded in 3D applications, but, last I checked, is not efficient on machines without GPUs. (Disclaimer: this information is two years old, so I encourage you to make your own evaluation.)
IMVU chose to embed Adobe’s player.
Deploying the Flash Runtime
Assuming you’re using Adobe’s Flash player, how will you deploy their runtime? Well, given Flash’s install base, you can get away with loading the Flash player already installed on the user’s computer. If they don’t have Flash, just require that they install it from your download page. Simple and easy.
Down the road, when Flash version incompatibilities and that last 5% of your possible market becomes important, you can request permission from Adobe to deploy the Flash player with your application.
IMVU displays Flash in two contexts: traditional HWND windows and 2D overlays atop the 3D scene.
If you want to have something up and running in a day, buy f_in_box. Besides its awesome name, it’s cheap, comes with source code, and the support forums are fantastic. It’s a perfect way to bootstrap. After a weekend of playing with f_in_box, Dusty and I had a YouTube video playing in a texture on top of our 3D scene.
Once you run into f_in_box’s limitations, you can use the IShockwaveFlash and IOleInPlaceObjectWindowless COM interfaces directly. See Igor Makarav’s excellent tutorial and CFlashWnd class.
Rendering Flash as an HWND
For top-level UI elements use f_in_box or CFlashWnd directly. They’re perfectly suited for this. Seriously, it’s just a few lines of code. Look at their samples and go.
Rendering Flash as a 3D Overlay
Rendering Flash to a 3D window gets a bit tricky… Check out my next post!