Mimic SketchUp face shading

Hi all,

I’m working on a Ruby tool that modifies faces. Before altering the physical face an interactive preview is shown. To make the preview look more 3D-dimensional and better fit into the SketchUp UX I want to shade the face color just as SketchUp shades faces but have little success. Has anyone done this before?

My tests show the following:

  • Primary colors and secondary colors keep hue when shaded.
  • Other colors change hue slightly (!) meaning the shading isn’t applied to the lightness in HLS space or B in HBS space. Or maybe it is but the hue changes when rounding the values when converting to and from RGB space.
  • Colors stay the same regardless of shading when Shadow Light is set to 0 and Shadow Dark is set to 80.
  • The Shadow Light and Shadow Dark value can’t be altered when not using sun for shading but the constant values seem to correspond to 81 and 20 with use sun for shading.
  • When using Sun for Shading the shading differs between faces facing the sun directly and faces with the normal being perpendicular to the sun vector. When the face faces away from the sun the shading is identical regardless of how much it faces away from the sun.

This is the code I’ve got so far:

# Returns Float in interval 0.0 to 1.0 of how much face should be shaded,
# 0.0 being the darkest and 1.0 lightest.
def shade_value(normal, view)
  reference =
    if view.model.shadow_info["UseSunForAllShading"]
      view.model.shadow_info["SunDirection"]
    else
      (view.camera.eye - view.camera.target).normalize
    end

    [normal % reference, 0].max
end

# Select a face and run:
model = Sketchup.active_model
shade_value(model.selection.first.normal, model.active_view)

Does anyone know how to go from here? Getting the RGB color from a face is fairly easy but how do I apply the shade_value to the color?

Thanks!

Shade test.skp (171.9 KB)
If anyone is interested this is the model I’ve conducted the tests on. I’ve aligned the camera in the second scene and the component shown in that scene with the sun vector to easier conduct tests.

I don’t have anything to contribute, but I’m interested as well. We’d like to mimic SketchUp’s shading in a webGL preview.

1 Like

I haven’t tested the code myself but I think each RGB channel is multiplied by

0.2 + dark + shade*light

where dark, shade and light are all in the range of 0 to 1, and then capped to 255. Capping individual channels would explain why the hue changes when the color is tinted very light.

If I’m not mistaken any value for dark and 0 for light makes the color consistent regardless of shading as observed. A dark value of 0.8 and a light value still at 0 would show the color on screen exactly as defined in the Material window, as observed.

I haven’t written the actual code and maybe light and dark values need to be remapped from an interval of 0 to 100 when taken from the ShadowInfo but I think I have found the formula!

Edit: Adding new test model.color test.skp (134.4 KB)

A few years ago when I was researching SUbD I did something like this. I was experimenting with subdivisions in pure Ruby and I was looking for ways to make the subdivision preview faster.

I extracted the preview tool from my experiment. (It won’t run without tweaks, but you can find a lot of logic that might be of use.)

1 Like

I had a feeling someone had experimented with this before!

I’l look into it more when I get home.

A small breakdown of what my code do:

You will find in initialize that I assign to @light two times.
The first one will color by axes. https://gist.github.com/thomthom/312b87cd4c6bb636347124ea3b02c28f#file-su_shading-rb-L31

It will have no effect unless you comment out line 40. https://gist.github.com/thomthom/312b87cd4c6bb636347124ea3b02c28f#file-su_shading-rb-L40

face_shading is computing the final color for the given face. https://gist.github.com/thomthom/312b87cd4c6bb636347124ea3b02c28f#file-su_shading-rb-L46

get_ambient computes the ambient light based on shadow info and some magic values from SketchUp. https://gist.github.com/thomthom/312b87cd4c6bb636347124ea3b02c28f#file-su_shading-rb-L67

update is what I called whenever the scene changed (view or model). It recomputes all the GL data needed to draw and cache it. Each face with similar color is grouped in an attempt to make drawing faster. https://gist.github.com/thomthom/312b87cd4c6bb636347124ea3b02c28f#file-su_shading-rb-L73

update_outlines will compute the Outline edges. (Called when the current Style have Outlines enabled): https://gist.github.com/thomthom/312b87cd4c6bb636347124ea3b02c28f#file-su_shading-rb-L73

draw will draw all the geometry based on the current rendering options.

Note that to draw edges they are offset slightly towards the camera in order to avoid z-fighting. This is currently done in the draw method - but could and probably should be cached as well.

1 Like

I think I noticed this behavior in SketchUp’s native drawing even before I started writing plugins. It’s also the reason why edges sometimes bleed through faces when they are placed really close behind relative to the distance to the camera. I didn’t get around to do it myself until yesterday though :stuck_out_tongue: . My upcoming plugin may be the one that feels most native to SketchUp of all my plugins yet :smiley: .

1 Like

I’ve successfully managed to replicate SketchUp’s own shading!

I’ve also written a really simple test tool that draws points on face centroids with the same color as the face is drawn with. Points are drawn on all faces but can’t be seen except when they represent a face further back, when they don’t fit into the face or when there is a shadow on the face.

2017-11-07_18h55_31
2017-11-07_18h55_37
2017-11-07_18h55_50

Here is my code, including the test tool:

module FaceShading

  # Get color of face (not shaded).
  # If face has texture, simply return the average color.
  # For now material inherited from parent group/component is ignored.
  #
  # face - A Face Entity.
  #
  # Returns Color object.
  def self.face_color(face)
    if view_back_face?(face)
      if face.back_material
        face.back_material.color
      else
        face.model.rendering_options["FaceBackColor"]
      end
    else
      if face.material
        face.material.color
      else
        face.model.rendering_options["FaceFrontColor"]
      end
    end
  end

  # Shade a color as it would be shaded if drawn to a face with a certain normal
  # and shown in a certain view.
  #
  # color  - Color object to base shaded color of.
  # normal - Vector3d.
  # view   - View object.
  def self.shade_color(color, normal, view)
    si = view.model.shadow_info
    light = sun_for_shading?(si) ? si["Light"]/100.0 : 0.81
    dark = sun_for_shading?(si) ? si["Dark"]/100.0 : 0.2
    shading = shade_value(normal, view)
    shift = 0.2 + dark + shading*light

    Sketchup::Color.new(*color.to_a[0, 3].map { |c| [(c*shift).to_i, 255].min })
  end

  # Get the shaded color of a face.
  #
  # face - A Face.
  #
  # Returns Color.
  def self.shaded_face_color(face)
    shade_color(face_color(face), face.normal, face.model.active_view)
  end

  # Check what side of face is being viewed.
  # Assume face is in same coordinate system as camera.
  #
  # face - A Face entity.
  # camera - A Camera object (default: the camera of the same model fce is in).
  #
  # Returns Boolean.
  def self.view_back_face?(face, camera = nil)
    camera ||= face.model.active_view.camera
    (camera.eye - camera.eye.project_to_plane(face.plane)) % face.normal < 0
  end

  private

  # Returns Float in interval 0.0 to 1.0 of how much face should be shaded,
  # 0.0 being the darkest and 1.0 lightest.
  #
  # normal - Normal Vector3d.
  # view   - Sketchup::View object.
  #
  # Examples
  #
  #   model = Sketchup.active_model
  #   shade_value(model.selection.first.normal, model.active_view)
  #
  # Returns float.
  def self.shade_value(normal, view)
    si = view.model.shadow_info
    reference =
      if sun_for_shading?(si)
        si["SunDirection"]
      else
        (view.camera.eye - view.camera.target).normalize
      end
      value = normal % reference

      sun_for_shading?(si) ? [value, 0].max : value.abs
  end

  def self.sun_for_shading?(si)
    # If shadows are enabled SketchUp uses sun for shading regardless of
    # the UseSunForAllShading setting.
    si["UseSunForAllShading"] || si["DisplayShadows"]
  end

end

# Test code to see that calculated color really matches the one SketchUp
# uses for face shading.
#
# If customs hading code is successful the points drawn by TestTool should being
# invisible unless they represent a face hidden behind another face or cover
# their faces' edges.
class TestTool

  def activate
    Sketchup.active_model.active_view.invalidate
  end

  def draw(view)
    view.model.active_entities.each do |face|
      next unless face.is_a?(Sketchup::Face)
      view.draw_points([face.bounds.center], 10, 2, FaceShading.shaded_face_color(face))
    end
  end

  def resume(view)
    view.invalidate
  end

end
Sketchup.active_model.select_tool(TestTool.new)
2 Likes

@Fredo6: Maybe your Joint Push Pull could benefit of face shading to make the preview faces look more 3D?

This is perfect, thanks a lot for sharing!

1 Like

You’re welcome! It was surprisingly simple (once had figured it out :stuck_out_tongue: ).

By the way does anyone know some quick way to disable any shading in SketchUp? For now it is possible to set time to midnight, set both light and shadow to 100 and enable sun for shading.
disable shading
But maybe it is possible just to set some single option somewhere in order to achieve the same result. It is rarely needed but sometimes for some orthogonal scenes it is necessary to display a color of each face as is regardless of its orientation relatively to a camera view direction or sun rays direction.

Use sun for shading, set Light to 0 and dark to 80 and all colors drawn on screen will be exactly as defined in the Materials.

Yes, for some reason dark should be 80 (not 100 as I expected) in order to display colors as in the Materials. I was wondering if there is a faster way to achieve the same result (like some option anywhere in display styles, which just turns off any shadows and displays original color of faces without changing of shadows settings). I thought such option could be useful if any.

An extra 20 is added internally in SketchUp, otherwise a face not hit by the sun would be pitch black. That’s why the slider needs to be at 80 to add up to 100. I don’t think there is a faster way to set this up. If there is a renderingoption for it it’s not exposed to the UI anyway.

Yes you can save shadow settings in a scene and then switch to that scene tab whenever you want to see the model this way.

I’ve just open sourced this under the MIT license and put on GitHub.

4 Likes

Thanks!

Amazing! Thanks.

1 Like