Skip to content

Making a PBR UI

Guillaume Piolat edited this page Oct 10, 2018 · 4 revisions

Dplug implements a relatively complex "PBR" system for its UI that takes some time to get used too. Filling 8 channels instead of 3 or 4 can seem overwhelming at first.

Here is how you can make the most of this system without scratching your head too much. Though rendering helps a lot, cheating the system is often necessary to get your point across.

The 8 channels you'll have to fill

(An analogy for 3D developers: the three images you get are akin to a "G-buffer" in 3D rendering, except in software. You additionally don't have drawing primitives that write to more than one map.)

In the diffuse map:

  • R, G and B are the red, green and blue albedo aka "base color"
  • A is not opacity but Emissive. When A != 0 this pixel emits light to neighbours coloured with its albedo

In the material map:

  • R is Roughness (0 => rough / 255 => soft)
  • G is Metalness (0 => dielectric / 255 => metal)
  • B is Specular (0 => not shiny at all / 255 => shiny)
  • A is unused since Dplug v8.

In the depth map:

  • the only channel is depth (0 => lowest / 65535 => highest)

You always get these three kinds of maps at once, but not all widgets need to modify them all. For example, a widget could be a slope that only affects depth. A text label will only write to the diffuse map, etc.

Keep in mind the PBR system is not real-time enough. This is a key ingredient to ensure 60 FPS rendering of real-time widgets.

You can also use the 8 existing channels in other ways by making a Compositor subclass. However, builtin widgets were written with the default one in mind.

Respecting dirtyRects

In the onDraw() override, dirtyRects is the list of disjointed boxes that should be drawn onto. You are not supposed to draw anywhere else in box2i(0, 0, position.width, position.height) though that is possible. It WILL create problems if you don't respect dirtyRects.

Widgets are drawn from lower z-order to highest z-order. It is safe to use the current content of the buffers for eg. transparency provided the dirtyRects rule is respected by the stack of widgets on top of each-other.

Whatever is written on the Raw layer is always above what is written on the PBR layer.

Keep UIElement positions minimal

It is best to keep the _position rectangle of widgets to a minimum. Otherwise, their onDraw() method might get called unnecessarily. This is especially expensive for top-level widgets that don't respect dirtyRects. Widgets with no overlap in _position get drawn in parallel.

About the parameters

  • Diffuse RGB is where all the interesting stuff happens. It is the most direct way to give a color to something. It is very rare to need to use the darkest tones.
  • Material map can be filled with much less thought. At first you can leave it entirely uniform and it's OK.
  • Depth is very powerful, but depth maps resources are 8-bits while the runtime depth map is 16-bit. For precise slopes, you can use runtime functions to draw them. Currently depth-induced shadows are quite short so there is no big sense of actual depth though, so you might want to complete with manual shadows in the diffuse map.

Steps for making a new UI

Part 1

  • Use a plain rectangle with default depth (~15000), neutral material ( for example RGBA(128, 128, 128, 255) ), default emissive (0), default physical (255) and a grey diffuse around white (but probably not full white). Create a default skymap.
  • Keep an emissive channel of 0 everywhere. Don't be tempted into making everything emit a bit of light, it is doable but creates problem with transparent widgets who have to overwrite the diffuse map.
  • Put stock widgets a bit randomly. You can move them around with right-click in debug mode and resize them with CTRL + right-click. Have helper functions to shape all widgets at once.
  • Put a background in image resources (PNG and non-progressive JPEG supported). You can reload them pressing RETURN.
  • Keep all widgets, lighting and skybox textures relatively grey at this point. The first goal is to look good in grey.

Part 2

  • Segregate areas of the UI by using a diffuse background, depth background and (early) material background.
  • Spend days creating a "paint" layer with text and other indicators on the diffuse background. It is advisable to keep a multi-layer save of the diffuse background, which can become quite complex. Add colour in this step. Iterate your design a lot!
  • (optional) Manual shadows for knobs look good and are quick to make. Add them in the diffuse.
  • (optional) You probably have widgets that need real-time updates. They key is to only update the Raw layer in real-time update.

Part 3

  • Tweak the skybox. This is the point where you might want to fine-tune the roughness and metalness parameters of your materials.
  • Fine-tune lighting conditions. You may feel the need to tweak the specular channel.
  • Correct colours with Lift-Gamma-Gain curves. Add such a widget.

At this point it should look alright.