Why is UnlitMaterial useful - michaliskambi/x3d-tests GitHub Wiki

I propose adding a new node UnlitMaterial to the X3D v4.

Table of Contents:

What is it?

It's a material node that is always "unlit" (unaffected by lighting sources).

It has only emissive parameter (which can be specified using emissiveColor and optionally varied using emissiveTexture).

It interacts in a natural way with Color and ColorRGBA nodes, and with texture in Appearance.texture (read on to know why it matters).

How and where it is done?

I already included the UnlitMaterial node:

The initial tests are in https://github.com/michaliskambi/x3d-tests/tree/master/pbr/unlit_material . You can use view3dscene from snapshot to see the "reference output" for these tests now.

Why

General reason

The general reason for having "unlit" rendering is that it's very useful for cartoon/unrealistic rendering. Artist can just explicitly apply colors to make an exact desired look, completely ignoring any possible physics or realistic lighting. For some games (see our The Unholy Society or Escape from the Universe) this is the only way to achieve the desired look.

Ways to get "unlit" in X3D 3

In X3D 3, one way to achieve the "unlit" look is to just let Appearance.material be NULL. While it works, the resulting look is not customizable -- there's no place to change emissiveColor, it's always pure white.

Another way to achieve the "unlit" look in X3D 3 is to use Material with all parameters (except emissiveColor) set to zero. I call this approach a "poor way of simulating unlit material" in text below. It looks like this:

Material {
  emissiveColor 1 0 0 # anything non-zero
  diffuseColor 0 0 0
  specularColor 0 0 0
  ambientIntensity 0
}

However the above solution (Material with all parameters except "emissive" set to zero) has problems. I point them below in more details.

Interaction with Appearance.texture

This is one problem with "poor way of simulating unlit material":

In X3D 3 lighting equations, we say "use Appearance.texture for the diffuse factor".

Which means that Appearance.texture should not modify a material with diffuseColor = zero. (In Phong shading; in Gouraud shading, Appearance.texture modifies everything, but this is not correct with respect to X3D 3 lighting equations, it's just a necessary consequence of the Gouraud shading optimization.)

Example: If you use a correct X3D browser, that does Phong shading and follows X3D (v3 and v4) specification correctly, then the shape below should be pure yellow (not affected by the my_texture.png , at least by the RGB colors in that texture):

Appearance {
  texture ImageTexture {
    url "my_texture.png"
  }
  material Material {
    emissiveColor 1 0 0 # anything non-zero
    diffuseColor 0 0 0
    specularColor 0 0 0
    ambientIntensity 0
  }
}

Note: if you're going to test it, make sure to request "Phong shading" from your X3D browser. In Gouraud shading, which is often default, "my_texture.png" necessarily affects everything.

Note that only Appearance.texture supports MultiTexture (because MultiTexture is a pain to implement) in X3D v4. So Appearance.texture is still useful in X3D v4, even though we introduce other ways to provide textures, by material xxxTexture fields.

Note that Appearance.texture works already intuitively in case of material=NULL case. Specification has special wording to account for it. In this case, texture obviously multiplies the emission.

So this is a problem with "poor way of simulating unlit material": texture RGB in Appearance.texture is multiplied by Material.diffuseColor, so the texture is effectively ignored when Material.diffuseColor is zero.

Using UnlitMaterial, the solution is easy: in case of UnlitMaterial, we can say that Appearance.texture acts as a emissiveTexture. In general, X3D v4 (after my modifications) accounts for the Appearance.texture like this:

  • Appearance.texture acts as Material.diffuseTexture (if you use Material with diffuseTexture=NULL)
  • Appearance.texture acts as UnlitMaterial.emissiveTexture (if you use UnlitMaterial with emissiveTexture=NULL)
  • Appearance.texture acts as PhysicalMaterial.baseTexture (if you use PhysicalMaterial with baseTexture=NULL)

Interaction with Color and ColorRGBA nodes

Color and ColorRGBA nodes affect (replace) the diffuse material parameter.

Which means that combining these nodes with a "poor way of simulating unlit material" makes the resulting material... lit. The below X3D shape is actually "lit", i.e. it has yellow emissive + white diffuse color. The blue channel of the resulting pixels is affected by the light sources.

Shape {
  appearance Appearance {
    material Material {
      emissiveColor 1 1 0
      diffuseColor 0 0 0
      specularColor 0 0 0
      ambientIntensity 0
    }
  }
  geometry IndexedFaceSet {
    color Color {
      color [ 1 1 1, ... ]
    }
    coordIndex ...
    coord ...
  }
}

And we cannot really change it, it would break backward-compatibility.

Note that Color and ColorRGBA work already intuitively in case of material=NULL case. Specification has special wording to account for it.

So this is a problem with "poor way of simulating unlit material": using per-vertex colors, in Color / ColorRGBA nodes, turns unlit materials into lit. This is quite unintuitive.

Using UnlitMaterial, the solution is easy: in case of UnlitMaterial, we can say that Color / ColorRGBA replace the emissiveColor. In general, X3D v4 (after my modifications) accounts for the Color / ColorRGBA like this:

  • Color / ColorRGBA replace Material.diffuseColor, if Material is used
  • Color / ColorRGBA replace UnlitMaterial.emissiveColor, if UnlitMaterial is used
  • Color / ColorRGBA replace PhysicalMaterial.baseColor, if PhysicalMaterial is used

Note: If you want per-vertex colors to multiply (instead of replace) the colors, use our X3DColorNode.mode field (extension to X3D), like this: Color { mode "MODULATE" color [ 1 1 1, ... ] }. In effect: we modulate (multiply) the same color than would be replaced, so we multiply the emissive color when UnlitMaterial was used.

Where is opacity (alpha)

Another problem with "poor way of simulating unlit material" is that diffuseTexture alpha would be used for opacity in this case, even though emissiveTexture would be used for RGB colors. That is, consider this X3D node:

material Material {
  emissiveColor 1 1 0
  emissiveTexture ImageTexture { "my_emission.png" }
  diffuseColor 0 0 0
  diffuseTexture ImageTexture { "my_diffuse.png" }
  specularColor 0 0 0
  ambientIntensity 0
}

The diffuseTexture acts as the main texture, so it also determines the alpha (opacity). This is consistent with PhysicalMaterial.baseTexture, with glTF material (where one texture affects base color and opacity), and in general with most software: there is usually a "main" texture whose alpha channel determines opacity.

So my_diffuse.png RGB channel is ignored, but my_diffuse.png alpha is used.

And my_emissive.png RGB channel is used, but my_emissive.png alpha is ignored.

Weird, right?

Using UnlitMaterial solves this: alpha just comes from emissiveTexture then, so everything is simple and intuitive.

What to do in Gouraud shading in X3D v4

Once we add textures to configure material slots, we need to say in spec what happens for Gouraud shading.

For Material, the diffuseTexture (or Appearance.texture) should be used, this is natural. Which means that emissiveTexture should be ignored in Gouraud shading. Which means that "poor way of simulating unlit material" will fail again. Which is weird, since Gouraud/Phong shading should not actually matter at all for unlit calculations (but it matters, if we use "poor way of simulating unlit material" as special subcase of "Phong lighting").

I initially tried to workaround it in X3D v4 by a rule "use emissiveTexture instead of diffuseTexture when emissiveColor > diffuseColor (on every component)". But this was very unclean -- we make a heuristics "when emissive is more important than diffuse". Instead, user should explicitly say "this material is unlit".

It would also make implementation complicated.

  • For Phong shading -- it would choose as "main texture" (the one affected by Appearance.texture) the diffuse. (and forcing it to be diffuse or emissive is, again, adding a heuristic to a place that should be clear)

  • For Gouraud shading -- it would choose as "main texture" (the one affected by Appearance.texture) either diffuse or emissive.

So this felt like a very "shaky" definition, causing surprising behaviour for users. So I rejected it.

With UnlitMaterial, it is solved: It is very clear what is the primary texture:

  • Material.diffuseTexture
  • UnlitMaterial.emissiveTexture
  • PhysicalMaterial.baseTexture
  • So Gouraud shading uses one of them (or Appearance.texture if it is nil).

Ugly alternatives

It would be possible to add a boolean field like unlit to Material. But this feels ugly -- we would introduce a field to deactivate most of the node's functionality. For both the implementation (simplicity, optimization) and the author (easy to grasp concept) it is more natural to have a dedicated node for this.

This is also why "poor way of simulating unlit material" feels unclean. It feels weird that we need to request Phong lighting (Material) and then set all Phong parameters to zero to actually achieve "unlit". It just feels wrong that we cannot explictly state our intention: "this is unlit".

Minor points

These are just extra benefits of introducing UnlitMaterial, and other confirmations that "it is a good idea".

Getting PointSet and LineSet with customizable colors, but still unlit, in X3D v4

In X3D v4 we want to allow lit PointSet and LineSet. This should be recognized by them having a material assigned.

This breaks compatibility with X3D 3, that tells for PointSet: """If the color field is NULL and there is a Material node defined for the Appearance node affecting this PointSet node, the emissiveColor of the Material node shall be used to draw the points."""

So in X3D 3, you could use this to have yellow unlit points:

  Shape {
    appearance Appearance {
      material Material {
        emissiveColor 1 1 0
      }
    }
    geometry PoinSet { ... }
  }

In X3D 4, you will have to write explicitly that diffuseColor, specularColor etc. are zero:

  Shape {
    appearance Appearance {
      material Material {
        emissiveColor 1 1 0
        diffuseColor 0 0 0
        specularColor 0 0 0
        ambientIntensity 0
      }
    }
    geometry PoinSet { ... }
  }

... or use UnlitMaterial, which looks much simpler:

  Shape {
    appearance Appearance {
      material UnlitMaterial {
        emissiveColor 1 1 0
      }
    }
    geometry PoinSet { ... }
  }

Adding "unlit" option doesn't complicate the spec

The node to define it is practically ready (after my PBR changes): it's just X3DMaterialNode without anything extra. X3DMaterialNode already has emissiveColor, emissiveTexture.

We also already have equations for it: they are in section """Lighting "off" """ of the X3D 3 spec. We only need to multiply them by emissive RGB.

Adding "unlit" option may even simplify specification a bit: The case of material=NULL can be now expressed easier:

""" when material field is NULL, it is always, 100% cases, equivalent to "UnlitMaterial with emissiveColor = 1 1 1" """

glTF has such material too

https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit . I can guess they reached the same conclusions as me, while writing spec and implementing.

Athough their definition of it is has weird naming for fields ("unlit" material uses "baseColor" (for a purpose other than standard material); the "emissiveColor" is summed with "baseColor" -- although it wasn't clear earlier, and it seems some browsers just ignored emissiveColor for unlit material). In X3D we can do it better/simpler.

Note that unlit material in glTF is affected by the vertex color (like our Color/ColorRGBA). So it's a good thing we addressed it above.

Having UnlitMaterial allows to use the "main texture" concept in the implementation very consistently

  • For Phong shading, "main texture" is the texture:

    • diffuseTexture for Material
    • emissiveTexture for UnlitMaterial
    • baseTexture for PhysicalMaterial
  • This is the texture affected by Apperance.texture (e.g. if you use Material, but Material.diffuseTexture is NULL, we use Apperance.texture).

  • This is the texture used when rendering using Gouraud shading.

  • This is also the texture that determines the opacity of the material.