From 69714a89ca0b748fc290f3e7d733805133bbb2c3 Mon Sep 17 00:00:00 2001
From: HifiExperiments <thingsandstuffblog@gmail.com>
Date: Tue, 26 Nov 2019 14:48:43 -0800
Subject: [PATCH] material entities support cullFaceMode and support gltf
 doubleSided

---
 interface/src/graphics/WorldBox.cpp           |   2 +-
 .../src/avatars-renderer/Avatar.cpp           |   6 +-
 .../src/RenderableGizmoEntityItem.cpp         |   2 +-
 .../src/RenderableLineEntityItem.cpp          |   2 +-
 .../src/RenderableShapeEntityItem.cpp         |   3 +-
 libraries/fbx/src/GLTFSerializer.cpp          |   4 +
 .../src/graphics-scripting/Forward.h          |   2 +
 .../GraphicsScriptingInterface.cpp            |   5 +
 .../graphics-scripting/ScriptableModel.cpp    |   3 +
 libraries/graphics/src/graphics/Material.cpp  |  21 ++-
 libraries/graphics/src/graphics/Material.h    |  20 +++
 .../procedural/ProceduralMaterialCache.cpp    |  24 +++-
 libraries/render-utils/src/GeometryCache.cpp  | 121 ++++++++++--------
 libraries/render-utils/src/GeometryCache.h    |  21 +--
 .../render-utils/src/MeshPartPayload.cpp      |   6 +
 libraries/render-utils/src/Model.cpp          |   2 +-
 .../render-utils/src/RenderPipelines.cpp      |  78 +++++------
 libraries/render/src/render/ShapePipeline.h   |  51 +++++++-
 18 files changed, 256 insertions(+), 117 deletions(-)

diff --git a/interface/src/graphics/WorldBox.cpp b/interface/src/graphics/WorldBox.cpp
index 648d6d3177..0e15d9da86 100644
--- a/interface/src/graphics/WorldBox.cpp
+++ b/interface/src/graphics/WorldBox.cpp
@@ -22,7 +22,7 @@ namespace render {
             PerformanceTimer perfTimer("worldBox");
 
             auto& batch = *args->_batch;
-            DependencyManager::get<GeometryCache>()->bindSimpleProgram(batch, false, false, true, false, false, true, args->_renderMethod == Args::RenderMethod::FORWARD);
+            DependencyManager::get<GeometryCache>()->bindSimpleProgram(batch, false, false, false, false, true, args->_renderMethod == Args::RenderMethod::FORWARD);
             WorldBoxRenderData::renderWorldBox(args, batch);
         }
     }
diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
index 5d5d809e15..0ba8705482 100644
--- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
+++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
@@ -795,7 +795,7 @@ void Avatar::render(RenderArgs* renderArgs) {
                 pointerTransform.setTranslation(position);
                 pointerTransform.setRotation(rotation);
                 batch.setModelTransform(pointerTransform);
-                geometryCache->bindSimpleProgram(batch, false, false, true, false, false, true, renderArgs->_renderMethod == render::Args::FORWARD);
+                geometryCache->bindSimpleProgram(batch, false, false, false, false, true, renderArgs->_renderMethod == render::Args::FORWARD);
                 geometryCache->renderLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, laserLength, 0.0f), laserColor, _leftPointerGeometryID);
             }
         }
@@ -819,7 +819,7 @@ void Avatar::render(RenderArgs* renderArgs) {
                 pointerTransform.setTranslation(position);
                 pointerTransform.setRotation(rotation);
                 batch.setModelTransform(pointerTransform);
-                geometryCache->bindSimpleProgram(batch, false, false, true, false, false, true, renderArgs->_renderMethod == render::Args::FORWARD);
+                geometryCache->bindSimpleProgram(batch, false, false, false, false, true, renderArgs->_renderMethod == render::Args::FORWARD);
                 geometryCache->renderLine(batch, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, laserLength, 0.0f), laserColor, _rightPointerGeometryID);
             }
         }
@@ -1107,7 +1107,7 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& view, const
 
         {
             PROFILE_RANGE_BATCH(batch, __FUNCTION__":renderBevelCornersRect");
-            DependencyManager::get<GeometryCache>()->bindSimpleProgram(batch, false, false, true, true, true, true, forward);
+            DependencyManager::get<GeometryCache>()->bindSimpleProgram(batch, false, false, true, true, true, forward);
             DependencyManager::get<GeometryCache>()->renderBevelCornersRect(batch, left, bottom, width, height,
                 bevelDistance, backgroundColor, _nameRectGeometryID);
         }
diff --git a/libraries/entities-renderer/src/RenderableGizmoEntityItem.cpp b/libraries/entities-renderer/src/RenderableGizmoEntityItem.cpp
index a066107a15..9081d0727f 100644
--- a/libraries/entities-renderer/src/RenderableGizmoEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableGizmoEntityItem.cpp
@@ -253,7 +253,7 @@ void GizmoEntityRenderer::doRender(RenderArgs* args) {
         });
 
         bool wireframe = render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES;
-        geometryCache->bindSimpleProgram(batch, false, isTransparent(), false, wireframe, true, true, forward);
+        geometryCache->bindSimpleProgram(batch, false, isTransparent(), wireframe, true, true, forward, graphics::MaterialKey::CULL_NONE);
 
         batch.setModelTransform(transform);
 
diff --git a/libraries/entities-renderer/src/RenderableLineEntityItem.cpp b/libraries/entities-renderer/src/RenderableLineEntityItem.cpp
index aaef0b3f7d..6e2be1b41e 100644
--- a/libraries/entities-renderer/src/RenderableLineEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableLineEntityItem.cpp
@@ -50,7 +50,7 @@ void LineEntityRenderer::doRender(RenderArgs* args) {
     transform.setRotation(modelTransform.getRotation());
     batch.setModelTransform(transform);
     if (_linePoints.size() > 1) {
-        DependencyManager::get<GeometryCache>()->bindSimpleProgram(batch, false, false, true, false, false, true,
+        DependencyManager::get<GeometryCache>()->bindSimpleProgram(batch, false, false, false, false, true,
             _renderLayer != RenderLayer::WORLD || args->_renderMethod == Args::RenderMethod::FORWARD);
         DependencyManager::get<GeometryCache>()->renderVertices(batch, gpu::LINE_STRIP, _lineVerticesID);
     }
diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
index 88cc78b6b6..3cc18ee412 100644
--- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
@@ -200,6 +200,7 @@ ShapeKey ShapeEntityRenderer::getShapeKey() {
         if (drawMaterialKey.isUnlit()) {
             builder.withUnlit();
         }
+        builder.withCullFaceMode(mat->second.getCullFaceMode());
     } else if (pipelineType == Pipeline::PROCEDURAL) {
         builder.withOwnPipeline();
     }
@@ -251,7 +252,7 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) {
         // FIXME, support instanced multi-shape rendering using multidraw indirect
         outColor.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f;
         render::ShapePipelinePointer pipeline = geometryCache->getShapePipelinePointer(outColor.a < 1.0f, false,
-            renderLayer != RenderLayer::WORLD || args->_renderMethod == Args::RenderMethod::FORWARD);
+            renderLayer != RenderLayer::WORLD || args->_renderMethod == Args::RenderMethod::FORWARD, materials.top().material->getCullFaceMode());
         if (render::ShapeKey(args->_globalShapeKey).isWireframe() || primitiveMode == PrimitiveMode::LINES) {
             geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline);
         } else {
diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp
index c75e717609..9d9d16d733 100755
--- a/libraries/fbx/src/GLTFSerializer.cpp
+++ b/libraries/fbx/src/GLTFSerializer.cpp
@@ -1715,6 +1715,10 @@ void GLTFSerializer::setHFMMaterial(HFMMaterial& hfmMat, const GLTFMaterial& mat
         hfmMat._material->setOpacityCutoff(material.alphaCutoff);
     }
 
+    if (material.defined["doubleSided"] && material.doubleSided) {
+        hfmMat._material->setCullFaceMode(graphics::MaterialKey::CullFaceMode::CULL_NONE);
+    }
+
     if (material.defined["emissiveFactor"] && material.emissiveFactor.size() == 3) {
         glm::vec3 emissive = glm::vec3(material.emissiveFactor[0], material.emissiveFactor[1], material.emissiveFactor[2]);
         hfmMat._material->setEmissive(emissive);
diff --git a/libraries/graphics-scripting/src/graphics-scripting/Forward.h b/libraries/graphics-scripting/src/graphics-scripting/Forward.h
index 9efaa0a90d..acef5a5bd4 100644
--- a/libraries/graphics-scripting/src/graphics-scripting/Forward.h
+++ b/libraries/graphics-scripting/src/graphics-scripting/Forward.h
@@ -65,6 +65,7 @@ namespace scriptable {
      * @property {Mat4|string} texCoordTransform1
      * @property {string} lightmapParams
      * @property {string} materialParams
+     * @property {string} cullFaceMode
      * @property {boolean} defaultFallthrough
      * @property {string} procedural
      */
@@ -99,6 +100,7 @@ namespace scriptable {
         QString lightMap;
         QString scatteringMap;
         std::array<glm::mat4, graphics::Material::NUM_TEXCOORD_TRANSFORMS> texCoordTransforms;
+        QString cullFaceMode;
         bool defaultFallthrough;
         std::unordered_map<uint, bool> propertyFallthroughs; // not actually exposed to script
 
diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp
index 4d95709f15..62614ea6e8 100644
--- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp
+++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp
@@ -495,6 +495,11 @@ namespace scriptable {
                 obj.setProperty("materialParams", FALLTHROUGH);
             }
 
+            if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::CULL_FACE_MODE)) {
+                obj.setProperty("cullFaceMode", FALLTHROUGH);
+            } else if (!material.cullFaceMode.isEmpty()) {
+                obj.setProperty("cullFaceMode", material.cullFaceMode);
+            }
         } else if (material.model.toStdString() == graphics::Material::HIFI_SHADER_SIMPLE) {
             obj.setProperty("procedural", material.procedural);
         }
diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp
index 4a56db0d04..28cd49e7c4 100644
--- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp
+++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp
@@ -45,6 +45,7 @@ scriptable::ScriptableMaterial& scriptable::ScriptableMaterial::operator=(const
         occlusionMap = material.occlusionMap;
         lightMap = material.lightMap;
         scatteringMap = material.scatteringMap;
+        cullFaceMode = material.cullFaceMode;
     } else if (model.toStdString() == graphics::Material::HIFI_SHADER_SIMPLE) {
         procedural = material.procedural;
     }
@@ -131,6 +132,8 @@ scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPoint
             for (int i = 0; i < graphics::Material::NUM_TEXCOORD_TRANSFORMS; i++) {
                 texCoordTransforms[i] = material->getTexCoordTransform(i);
             }
+
+            cullFaceMode = QString(graphics::MaterialKey::getCullFaceModeName(material->getCullFaceMode()).c_str());
         } else if (model.toStdString() == graphics::Material::HIFI_SHADER_SIMPLE) {
             procedural = material->getProceduralString();
         }
diff --git a/libraries/graphics/src/graphics/Material.cpp b/libraries/graphics/src/graphics/Material.cpp
index 1b96ed433b..41cd319595 100755
--- a/libraries/graphics/src/graphics/Material.cpp
+++ b/libraries/graphics/src/graphics/Material.cpp
@@ -27,7 +27,7 @@ const float Material::DEFAULT_ROUGHNESS { 1.0f };
 const float Material::DEFAULT_SCATTERING{ 0.0f };
 const MaterialKey::OpacityMapMode Material::DEFAULT_OPACITY_MAP_MODE{ MaterialKey::OPACITY_MAP_OPAQUE };
 const float Material::DEFAULT_OPACITY_CUTOFF { 0.5f };
-
+const MaterialKey::CullFaceMode Material::DEFAULT_CULL_FACE_MODE { MaterialKey::CULL_BACK };
 
 std::string MaterialKey::getOpacityMapModeName(OpacityMapMode mode) {
     const std::string names[3] = { "OPACITY_MAP_OPAQUE", "OPACITY_MAP_MASK", "OPACITY_MAP_BLEND" };
@@ -44,6 +44,21 @@ bool MaterialKey::getOpacityMapModeFromName(const std::string& modeName, Materia
     return false;
 }
 
+std::string MaterialKey::getCullFaceModeName(CullFaceMode mode) {
+    const std::string names[3] = { "CULL_NONE", "CULL_FRONT", "CULL_BACK" };
+    return names[mode];
+}
+
+bool MaterialKey::getCullFaceModeFromName(const std::string& modeName, CullFaceMode& mode) {
+    for (int i = CULL_NONE; i < NUM_CULL_FACE_MODES; i++) {
+        mode = (CullFaceMode)i;
+        if (modeName == getCullFaceModeName(mode)) {
+            return true;
+        }
+    }
+    return false;
+}
+
 const std::string Material::HIFI_PBR { "hifi_pbr" };
 const std::string Material::HIFI_SHADER_SIMPLE { "hifi_shader_simple" };
 
@@ -67,6 +82,7 @@ Material::Material(const Material& material) :
     _texcoordTransforms(material._texcoordTransforms),
     _lightmapParams(material._lightmapParams),
     _materialParams(material._materialParams),
+    _cullFaceMode(material._cullFaceMode),
     _textureMaps(material._textureMaps),
     _defaultFallthrough(material._defaultFallthrough),
     _propertyFallthroughs(material._propertyFallthroughs)
@@ -89,6 +105,7 @@ Material& Material::operator=(const Material& material) {
     _texcoordTransforms = material._texcoordTransforms;
     _lightmapParams = material._lightmapParams;
     _materialParams = material._materialParams;
+    _cullFaceMode = material._cullFaceMode;
     _textureMaps = material._textureMaps;
 
     _defaultFallthrough = material._defaultFallthrough;
@@ -144,7 +161,7 @@ void Material::setOpacityMapMode(MaterialKey::OpacityMapMode opacityMapMode) {
     _key.setOpacityMapMode(opacityMapMode);
 }
 
-MaterialKey::OpacityMapMode  Material::getOpacityMapMode() const {
+MaterialKey::OpacityMapMode Material::getOpacityMapMode() const {
     return _key.getOpacityMapMode();
 }
 
diff --git a/libraries/graphics/src/graphics/Material.h b/libraries/graphics/src/graphics/Material.h
index 48ab8151c5..7a411e5b2c 100755
--- a/libraries/graphics/src/graphics/Material.h
+++ b/libraries/graphics/src/graphics/Material.h
@@ -83,6 +83,16 @@ public:
     // find the enum value from a string, return true if match found
     static bool getOpacityMapModeFromName(const std::string& modeName, OpacityMapMode& mode);
 
+    enum CullFaceMode {
+        CULL_NONE = 0,
+        CULL_FRONT,
+        CULL_BACK,
+
+        NUM_CULL_FACE_MODES
+    };
+    static std::string getCullFaceModeName(CullFaceMode mode);
+    static bool getCullFaceModeFromName(const std::string& modeName, CullFaceMode& mode);
+
     // The signature is the Flags
     Flags _flags;
 
@@ -349,6 +359,10 @@ public:
     void setOpacityCutoff(float opacityCutoff);
     float getOpacityCutoff() const { return _opacityCutoff; }
 
+    static const MaterialKey::CullFaceMode DEFAULT_CULL_FACE_MODE;
+    void setCullFaceMode(MaterialKey::CullFaceMode cullFaceMode) { _cullFaceMode = cullFaceMode; }
+    MaterialKey::CullFaceMode getCullFaceMode() const { return _cullFaceMode; }
+
     void setUnlit(bool value);
     bool isUnlit() const { return _key.isUnlit(); }
 
@@ -403,6 +417,7 @@ public:
         TEXCOORDTRANSFORM1,
         LIGHTMAP_PARAMS,
         MATERIAL_PARAMS,
+        CULL_FACE_MODE,
 
         NUM_TOTAL_FLAGS
     };
@@ -436,6 +451,7 @@ private:
     std::array<glm::mat4, NUM_TEXCOORD_TRANSFORMS> _texcoordTransforms;
     glm::vec2 _lightmapParams { 0.0, 1.0 };
     glm::vec2 _materialParams { 0.0, 1.0 };
+    MaterialKey::CullFaceMode _cullFaceMode { DEFAULT_CULL_FACE_MODE };
     TextureMaps _textureMaps;
 
     bool _defaultFallthrough { false };
@@ -524,6 +540,9 @@ public:
     graphics::MaterialKey getMaterialKey() const { return graphics::MaterialKey(_schemaBuffer.get<graphics::MultiMaterial::Schema>()._key); }
     const gpu::TextureTablePointer& getTextureTable() const { return _textureTable; }
 
+    void setCullFaceMode(graphics::MaterialKey::CullFaceMode cullFaceMode) { _cullFaceMode = cullFaceMode; }
+    graphics::MaterialKey::CullFaceMode getCullFaceMode() const { return _cullFaceMode; }
+
     void setNeedsUpdate(bool needsUpdate) { _needsUpdate = needsUpdate; }
     void setTexturesLoading(bool value) { _texturesLoading = value; }
     void setInitialized() { _initialized = true; }
@@ -536,6 +555,7 @@ public:
 
 private:
     gpu::BufferView _schemaBuffer;
+    graphics::MaterialKey::CullFaceMode _cullFaceMode { graphics::Material::DEFAULT_CULL_FACE_MODE };
     gpu::TextureTablePointer _textureTable { std::make_shared<gpu::TextureTable>() };
     bool _needsUpdate { false };
     bool _texturesLoading { false };
diff --git a/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp b/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp
index b9611358e7..a97cb294b4 100644
--- a/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp
+++ b/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp
@@ -143,7 +143,7 @@ NetworkMaterialResource::ParsedMaterials NetworkMaterialResource::parseJSONMater
  * @property {string} opacityMap - The URL of the opacity texture image. Set the value the same as the <code>albedoMap</code> 
  *     value for transparency. 
  *     <code>"hifi_pbr"</code> model only.
- * @property {number|string} opacityMapMode - The mode defining the interpretation of the opacity map. Values can be:
+ * @property {string} opacityMapMode - The mode defining the interpretation of the opacity map. Values can be:
  *     <code>"OPACITY_MAP_OPAQUE"</code> for ignoring the opacity map information.
  *     <code>"OPACITY_MAP_MASK"</code> for using the opacity map as a mask, where only the texel greater than opacityCutoff are visible and rendered opaque.
  *     <code>"OPACITY_MAP_BLEND"</code> for using the opacity map for alpha blending the material surface with the background.
@@ -151,6 +151,13 @@ NetworkMaterialResource::ParsedMaterials NetworkMaterialResource::parseJSONMater
  * @property {number|string} opacityCutoff - The opacity cutoff threshold used to determine the opaque texels of the Opacity map
  *     when opacityMapMode is "OPACITY_MAP_MASK", range <code>0.0</code> &ndash; <code>1.0</code>.
  *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ * @property {string} cullFaceMode - The mode defining which side of the geometry should be rendered. Values can be:
+ *     <ul>
+ *         <li><code>"CULL_NONE"</code> for rendering both sides of the geometry.</li>
+ *         <li><code>"CULL_FRONT"</code> for culling the front faces of the geometry.</li>
+ *         <li><code>"CULL_BACK"</code> (the default) for culling the back faces of the geometry.</li>
+ *     </ul>
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
  * @property {string} roughnessMap - The URL of the roughness texture image. You can use this or <code>glossMap</code>, but not 
  *     both. 
  *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
@@ -285,7 +292,20 @@ std::pair<std::string, std::shared_ptr<NetworkMaterial>> NetworkMaterialResource
                 } else if (value.isDouble()) {
                     material->setOpacityCutoff(value.toDouble());
                 }
-            } else if (key == "scattering") {
+            } else if (key == "cullFaceMode") {
+                auto value = materialJSON.value(key);
+                if (value.isString()) {
+                    auto valueString = value.toString();
+                    if (valueString == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(graphics::Material::ExtraFlagBit::CULL_FACE_MODE);
+                    } else {
+                        graphics::MaterialKey::CullFaceMode mode;
+                        if (graphics::MaterialKey::getCullFaceModeFromName(valueString.toStdString(), mode)) {
+                            material->setCullFaceMode(mode);
+                        }
+                    }
+                }
+           } else if (key == "scattering") {
                 auto value = materialJSON.value(key);
                 if (value.isString() && value.toString() == FALLTHROUGH) {
                     material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::SCATTERING_VAL_BIT);
diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp
index 621c20227c..e0d23535e4 100644
--- a/libraries/render-utils/src/GeometryCache.cpp
+++ b/libraries/render-utils/src/GeometryCache.cpp
@@ -723,7 +723,7 @@ gpu::ShaderPointer GeometryCache::_forwardUnlitShader;
 gpu::ShaderPointer GeometryCache::_forwardSimpleFadeShader;
 gpu::ShaderPointer GeometryCache::_forwardUnlitFadeShader;
 
-std::map<std::tuple<bool, bool, bool>, render::ShapePipelinePointer> GeometryCache::_shapePipelines;
+std::map<std::tuple<bool, bool, bool, graphics::MaterialKey::CullFaceMode>, render::ShapePipelinePointer> GeometryCache::_shapePipelines;
 
 GeometryCache::GeometryCache() :
 _nextID(0) {
@@ -776,15 +776,18 @@ void GeometryCache::initializeShapePipelines() {
             bool transparent = i & 1;
             bool unlit = i & 2;
             bool forward = i & 4;
-            _shapePipelines[std::make_tuple(transparent, unlit, forward)] = getShapePipeline(false, transparent, true, unlit, false, forward);
+            for (int cullFaceMode = graphics::MaterialKey::CullFaceMode::CULL_NONE; cullFaceMode < graphics::MaterialKey::CullFaceMode::NUM_CULL_FACE_MODES; cullFaceMode++) {
+                auto cullMode = (graphics::MaterialKey::CullFaceMode)cullFaceMode;
+                _shapePipelines[std::make_tuple(transparent, unlit, forward, cullMode)] = getShapePipeline(false, transparent, unlit, false, forward, cullMode);
+            }
         }
     }
 }
 
-render::ShapePipelinePointer GeometryCache::getShapePipeline(bool textured, bool transparent, bool culled,
-    bool unlit, bool depthBias, bool forward) {
+render::ShapePipelinePointer GeometryCache::getShapePipeline(bool textured, bool transparent, bool unlit, bool depthBias, bool forward,
+        graphics::MaterialKey::CullFaceMode cullFaceMode) {
 
-    return std::make_shared<render::ShapePipeline>(getSimplePipeline(textured, transparent, culled, unlit, depthBias, false, true, forward), nullptr,
+    return std::make_shared<render::ShapePipeline>(getSimplePipeline(textured, transparent, unlit, depthBias, false, true, forward, cullFaceMode), nullptr,
         [](const render::ShapePipeline& pipeline, gpu::Batch& batch, render::Args* args) {
             batch.setResourceTexture(gr::Texture::MaterialAlbedo, DependencyManager::get<TextureCache>()->getWhiteTexture());
             DependencyManager::get<DeferredLightingEffect>()->setupKeyLightBatch(args, batch);
@@ -792,12 +795,12 @@ render::ShapePipelinePointer GeometryCache::getShapePipeline(bool textured, bool
     );
 }
 
-render::ShapePipelinePointer GeometryCache::getFadingShapePipeline(bool textured, bool transparent, bool culled,
-    bool unlit, bool depthBias, bool forward) {
+render::ShapePipelinePointer GeometryCache::getFadingShapePipeline(bool textured, bool transparent, bool unlit, bool depthBias, bool forward,
+        graphics::MaterialKey::CullFaceMode cullFaceMode) {
     auto fadeEffect = DependencyManager::get<FadeEffect>();
     auto fadeBatchSetter = fadeEffect->getBatchSetter();
     auto fadeItemSetter = fadeEffect->getItemUniformSetter();
-    return std::make_shared<render::ShapePipeline>(getSimplePipeline(textured, transparent, culled, unlit, depthBias, true, true, forward), nullptr,
+    return std::make_shared<render::ShapePipeline>(getSimplePipeline(textured, transparent, unlit, depthBias, true, true, forward, cullFaceMode), nullptr,
         [fadeBatchSetter, fadeItemSetter](const render::ShapePipeline& shapePipeline, gpu::Batch& batch, render::Args* args) {
             batch.setResourceTexture(gr::Texture::MaterialAlbedo, DependencyManager::get<TextureCache>()->getWhiteTexture());
             fadeBatchSetter(shapePipeline, batch, args);
@@ -2049,54 +2052,60 @@ void GeometryCache::useGridPipeline(gpu::Batch& batch, GridBuffer gridBuffer, bo
 class SimpleProgramKey {
 public:
     enum FlagBit {
-        IS_TEXTURED_FLAG = 0,
-        IS_TRANSPARENT_FLAG,
-        IS_CULLED_FLAG,
-        IS_UNLIT_FLAG,
-        HAS_DEPTH_BIAS_FLAG,
-        IS_FADING_FLAG,
-        IS_ANTIALIASED_FLAG,
-        IS_FORWARD_FLAG,
+        IS_TEXTURED_BIT = 0,
+        IS_TRANSPARENT_BIT,
+        IS_UNLIT_BIT,
+        IS_DEPTH_BIASED_BIT,
+        IS_FADING_BIT,
+        IS_ANTIALIASED_BIT,
+        IS_FORWARD_BIT,
+        IS_CULL_FACE_NONE_BIT,   // if neither of these are set, we're CULL_FACE_BACK
+        IS_CULL_FACE_FRONT_BIT,
 
         NUM_FLAGS,
     };
+    typedef std::bitset<NUM_FLAGS> Flags;
 
-    enum Flag {
-        IS_TEXTURED = (1 << IS_TEXTURED_FLAG),
-        IS_TRANSPARENT = (1 << IS_TRANSPARENT_FLAG),
-        IS_CULLED = (1 << IS_CULLED_FLAG),
-        IS_UNLIT = (1 << IS_UNLIT_FLAG),
-        HAS_DEPTH_BIAS = (1 << HAS_DEPTH_BIAS_FLAG),
-        IS_FADING = (1 << IS_FADING_FLAG),
-        IS_ANTIALIASED = (1 << IS_ANTIALIASED_FLAG),
-        IS_FORWARD = (1 << IS_FORWARD_FLAG),
-    };
-    typedef unsigned short Flags;
-
-    bool isFlag(short flagNum) const { return bool((_flags & flagNum) != 0); }
-
-    bool isTextured() const { return isFlag(IS_TEXTURED); }
-    bool isTransparent() const { return isFlag(IS_TRANSPARENT); }
-    bool isCulled() const { return isFlag(IS_CULLED); }
-    bool isUnlit() const { return isFlag(IS_UNLIT); }
-    bool hasDepthBias() const { return isFlag(HAS_DEPTH_BIAS); }
-    bool isFading() const { return isFlag(IS_FADING); }
-    bool isAntiAliased() const { return isFlag(IS_ANTIALIASED); }
-    bool isForward() const { return isFlag(IS_FORWARD); }
+    bool isTextured() const { return _flags[IS_TEXTURED_BIT]; }
+    bool isTransparent() const { return _flags[IS_TRANSPARENT_BIT]; }
+    bool isUnlit() const { return _flags[IS_UNLIT_BIT]; }
+    bool hasDepthBias() const { return _flags[IS_DEPTH_BIASED_BIT]; }
+    bool isFading() const { return _flags[IS_FADING_BIT]; }
+    bool isAntiAliased() const { return _flags[IS_ANTIALIASED_BIT]; }
+    bool isForward() const { return _flags[IS_FORWARD_BIT]; }
+    bool isCullFaceNone() const { return _flags[IS_CULL_FACE_NONE_BIT]; }
+    bool isCullFaceFront() const { return _flags[IS_CULL_FACE_FRONT_BIT]; }
 
     Flags _flags = 0;
-#if defined(__clang__)
-    __attribute__((unused))
-#endif
-    short _spare = 0; // Padding
 
-    int getRaw() const { return *reinterpret_cast<const int*>(this); }
+    unsigned long getRaw() const { return _flags.to_ulong(); }
 
+    SimpleProgramKey(bool textured = false, bool transparent = false, bool unlit = false, bool depthBias = false, bool fading = false,
+        bool isAntiAliased = true, bool forward = false, graphics::MaterialKey::CullFaceMode cullFaceMode = graphics::MaterialKey::CULL_BACK) {
+        _flags.set(IS_TEXTURED_BIT, textured);
+        _flags.set(IS_TRANSPARENT_BIT, transparent);
+        _flags.set(IS_UNLIT_BIT, unlit);
+        _flags.set(IS_DEPTH_BIASED_BIT, depthBias);
+        _flags.set(IS_FADING_BIT, fading);
+        _flags.set(IS_ANTIALIASED_BIT, isAntiAliased);
+        _flags.set(IS_FORWARD_BIT, forward);
 
-    SimpleProgramKey(bool textured = false, bool transparent = false, bool culled = true,
-        bool unlit = false, bool depthBias = false, bool fading = false, bool isAntiAliased = true, bool forward = false) {
-        _flags = (textured ? IS_TEXTURED : 0) | (transparent ? IS_TRANSPARENT : 0) | (culled ? IS_CULLED : 0) |
-            (unlit ? IS_UNLIT : 0) | (depthBias ? HAS_DEPTH_BIAS : 0) | (fading ? IS_FADING : 0) | (isAntiAliased ? IS_ANTIALIASED : 0) | (forward ? IS_FORWARD : 0);
+        switch (cullFaceMode) {
+            case graphics::MaterialKey::CullFaceMode::CULL_NONE:
+                _flags.set(IS_CULL_FACE_NONE_BIT);
+                _flags.reset(IS_CULL_FACE_FRONT_BIT);
+                break;
+            case graphics::MaterialKey::CullFaceMode::CULL_FRONT:
+                _flags.reset(IS_CULL_FACE_NONE_BIT);
+                _flags.set(IS_CULL_FACE_FRONT_BIT);
+                break;
+            case graphics::MaterialKey::CullFaceMode::CULL_BACK:
+                _flags.reset(IS_CULL_FACE_NONE_BIT);
+                _flags.reset(IS_CULL_FACE_FRONT_BIT);
+                break;
+            default:
+                break;
+        }
     }
 
     SimpleProgramKey(int bitmask) : _flags(bitmask) {}
@@ -2141,8 +2150,9 @@ gpu::PipelinePointer GeometryCache::getWebBrowserProgram(bool transparent, bool
     return _webPipelines[{ transparent, forward }];
 }
 
-void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool transparent, bool culled, bool unlit, bool depthBiased, bool isAntiAliased, bool forward) {
-    batch.setPipeline(getSimplePipeline(textured, transparent, culled, unlit, depthBiased, false, isAntiAliased, forward));
+void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool transparent, bool unlit, bool depthBiased, bool isAntiAliased,
+        bool forward, graphics::MaterialKey::CullFaceMode cullFaceMode) {
+    batch.setPipeline(getSimplePipeline(textured, transparent, unlit, depthBiased, false, isAntiAliased, forward, cullFaceMode));
 
     // If not textured, set a default albedo map
     if (!textured) {
@@ -2151,8 +2161,9 @@ void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool tra
     }
 }
 
-gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transparent, bool culled, bool unlit, bool depthBiased, bool fading, bool isAntiAliased, bool forward) {
-    SimpleProgramKey config { textured, transparent, culled, unlit, depthBiased, fading, isAntiAliased, forward };
+gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transparent, bool unlit, bool depthBiased, bool fading, bool isAntiAliased,
+        bool forward, graphics::MaterialKey::CullFaceMode cullFaceMode) {
+    SimpleProgramKey config { textured, transparent, unlit, depthBiased, fading, isAntiAliased, forward, cullFaceMode };
 
     // If the pipeline already exists, return it
     auto it = _simplePrograms.find(config);
@@ -2189,10 +2200,12 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp
 
     // If the pipeline did not exist, make it
     auto state = std::make_shared<gpu::State>();
-    if (config.isCulled()) {
-        state->setCullMode(gpu::State::CULL_BACK);
-    } else {
+    if (config.isCullFaceNone()) {
         state->setCullMode(gpu::State::CULL_NONE);
+    } else if (config.isCullFaceFront()) {
+        state->setCullMode(gpu::State::CULL_FRONT);
+    } else {
+        state->setCullMode(gpu::State::CULL_BACK);
     }
     state->setDepthTest(true, true, gpu::LESS_EQUAL);
     if (config.hasDepthBias()) {
diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h
index bfd133183d..03865c6cc7 100644
--- a/libraries/render-utils/src/GeometryCache.h
+++ b/libraries/render-utils/src/GeometryCache.h
@@ -162,18 +162,19 @@ public:
     static const int UNKNOWN_ID;
 
     // Bind the pipeline and get the state to render static geometry
-    void bindSimpleProgram(gpu::Batch& batch, bool textured = false, bool transparent = false, bool culled = true,
-                                          bool unlit = false, bool depthBias = false, bool isAntiAliased = true, bool forward = false);
+    void bindSimpleProgram(gpu::Batch& batch, bool textured = false, bool transparent = false, bool unlit = false, bool depthBias = false,
+        bool isAntiAliased = true, bool forward = false, graphics::MaterialKey::CullFaceMode cullFaceMode = graphics::MaterialKey::CullFaceMode::CULL_BACK);
     // Get the pipeline to render static geometry
-    static gpu::PipelinePointer getSimplePipeline(bool textured = false, bool transparent = false, bool culled = true,
-                                          bool unlit = false, bool depthBias = false, bool fading = false, bool isAntiAliased = true, bool forward = false);
+    static gpu::PipelinePointer getSimplePipeline(bool textured = false, bool transparent = false, bool unlit = false, bool depthBias = false,
+        bool fading = false, bool isAntiAliased = true, bool forward = false, graphics::MaterialKey::CullFaceMode cullFaceMode = graphics::MaterialKey::CullFaceMode::CULL_BACK);
 
     void bindWebBrowserProgram(gpu::Batch& batch, bool transparent, bool forward);
     gpu::PipelinePointer getWebBrowserProgram(bool transparent, bool forward);
     static std::map<std::pair<bool, bool>, gpu::PipelinePointer> _webPipelines;
 
     static void initializeShapePipelines();
-    render::ShapePipelinePointer getShapePipelinePointer(bool transparent, bool unlit, bool forward) { return _shapePipelines[std::make_tuple(transparent, unlit, forward)]; }
+    render::ShapePipelinePointer getShapePipelinePointer(bool transparent, bool unlit, bool forward,
+        graphics::MaterialKey::CullFaceMode cullFaceMode = graphics::MaterialKey::CULL_BACK) { return _shapePipelines[std::make_tuple(transparent, unlit, forward, cullFaceMode)]; }
 
     // Static (instanced) geometry
     void renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer);
@@ -456,13 +457,13 @@ private:
     static gpu::ShaderPointer _forwardSimpleFadeShader;
     static gpu::ShaderPointer _forwardUnlitFadeShader;
 
-    static std::map<std::tuple<bool, bool, bool>, render::ShapePipelinePointer> _shapePipelines;
+    static std::map<std::tuple<bool, bool, bool, graphics::MaterialKey::CullFaceMode>, render::ShapePipelinePointer> _shapePipelines;
     static QHash<SimpleProgramKey, gpu::PipelinePointer> _simplePrograms;
 
-    static render::ShapePipelinePointer getShapePipeline(bool textured = false, bool transparent = false, bool culled = true,
-        bool unlit = false, bool depthBias = false, bool forward = false);
-    static render::ShapePipelinePointer getFadingShapePipeline(bool textured = false, bool transparent = false, bool culled = true,
-        bool unlit = false, bool depthBias = false, bool forward = false);
+    static render::ShapePipelinePointer getShapePipeline(bool textured = false, bool transparent = false, bool unlit = false,
+        bool depthBias = false, bool forward = false, graphics::MaterialKey::CullFaceMode cullFaceMode = graphics::MaterialKey::CullFaceMode::CULL_BACK);
+    static render::ShapePipelinePointer getFadingShapePipeline(bool textured = false, bool transparent = false, bool unlit = false,
+        bool depthBias = false, bool forward = false, graphics::MaterialKey::CullFaceMode cullFaceMode = graphics::MaterialKey::CullFaceMode::CULL_BACK);
 };
 
 #endif // hifi_GeometryCache_h
diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp
index 64140bbb79..8bb406b5c8 100644
--- a/libraries/render-utils/src/MeshPartPayload.cpp
+++ b/libraries/render-utils/src/MeshPartPayload.cpp
@@ -140,6 +140,9 @@ ShapeKey MeshPartPayload::getShapeKey() const {
         if (drawMaterialKey.isUnlit()) {
             builder.withUnlit();
         }
+        if (material) {
+            builder.withCullFaceMode(material->getCullFaceMode());
+        }
     }
 
     return builder.build();
@@ -409,6 +412,9 @@ void ModelMeshPartPayload::setShapeKey(bool invalidateShapeKey, PrimitiveMode pr
         if (isUnlit) {
             builder.withUnlit();
         }
+        if (material) {
+            builder.withCullFaceMode(material->getCullFaceMode());
+        }
     }
 
     _shapeKey = builder.build();
diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp
index da3ae24978..2c33955bd0 100644
--- a/libraries/render-utils/src/Model.cpp
+++ b/libraries/render-utils/src/Model.cpp
@@ -1052,7 +1052,7 @@ void Model::renderDebugMeshBoxes(gpu::Batch& batch, bool forward) {
     Transform meshToWorld(meshToWorldMatrix);
     batch.setModelTransform(meshToWorld);
 
-    DependencyManager::get<GeometryCache>()->bindSimpleProgram(batch, false, false, false, true, true, forward);
+    DependencyManager::get<GeometryCache>()->bindSimpleProgram(batch, false, false, true, true, forward, graphics::MaterialKey::CULL_NONE);
 
     for (auto& meshTriangleSets : _modelSpaceMeshTriangleSets) {
         for (auto &partTriangleSet : meshTriangleSets) {
diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp
index ff2e3610c2..7f1ca4bd71 100644
--- a/libraries/render-utils/src/RenderPipelines.cpp
+++ b/libraries/render-utils/src/RenderPipelines.cpp
@@ -259,46 +259,44 @@ void addPlumberPipeline(ShapePlumber& plumber,
 
     gpu::ShaderPointer program = gpu::Shader::createProgram(programId);
 
-    for (int i = 0; i < 8; i++) {
-        bool isCulled = (i & 1);
-        bool isBiased = (i & 2);
-        bool isWireframed = (i & 4);
+    for (int i = 0; i < 4; i++) {
+        bool isBiased = (i & 1);
+        bool isWireframed = (i & 2);
+        for (int cullFaceMode = graphics::MaterialKey::CullFaceMode::CULL_NONE; cullFaceMode < graphics::MaterialKey::CullFaceMode::NUM_CULL_FACE_MODES; cullFaceMode++) {
+            auto state = std::make_shared<gpu::State>();
+            key.isTranslucent() ? PrepareStencil::testMask(*state) : PrepareStencil::testMaskDrawShape(*state);
 
-        auto state = std::make_shared<gpu::State>();
-        key.isTranslucent() ? PrepareStencil::testMask(*state) : PrepareStencil::testMaskDrawShape(*state);
+            // Depth test depends on transparency
+            state->setDepthTest(true, !key.isTranslucent(), gpu::LESS_EQUAL);
+            state->setBlendFunction(key.isTranslucent(),
+                    gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA,
+                    gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
 
-        // Depth test depends on transparency
-        state->setDepthTest(true, !key.isTranslucent(), gpu::LESS_EQUAL);
-        state->setBlendFunction(key.isTranslucent(),
-                gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA,
-                gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
+            ShapeKey::Builder builder(key);
+            builder.withCullFaceMode((graphics::MaterialKey::CullFaceMode)cullFaceMode);
+            state->setCullMode((gpu::State::CullMode)cullFaceMode);
+            if (isWireframed) {
+                builder.withWireframe();
+                state->setFillMode(gpu::State::FILL_LINE);
+            }
+            if (isBiased) {
+                builder.withDepthBias();
+                state->setDepthBias(1.0f);
+                state->setDepthBiasSlopeScale(1.0f);
+            }
 
-        ShapeKey::Builder builder(key);
-        if (!isCulled) {
-            builder.withoutCullFace();
+            auto baseBatchSetter = (forceLightBatchSetter || key.isTranslucent()) ? &lightBatchSetter : &batchSetter;
+            render::ShapePipeline::BatchSetter finalBatchSetter;
+            if (extraBatchSetter) {
+                finalBatchSetter = [baseBatchSetter, extraBatchSetter](const ShapePipeline& pipeline, gpu::Batch& batch, render::Args* args) {
+                    baseBatchSetter(pipeline, batch, args);
+                    extraBatchSetter(pipeline, batch, args);
+                };
+            } else {
+                finalBatchSetter = baseBatchSetter;
+            }
+            plumber.addPipeline(builder.build(), program, state, finalBatchSetter, itemSetter);
         }
-        state->setCullMode(isCulled ? gpu::State::CULL_BACK : gpu::State::CULL_NONE);
-        if (isWireframed) {
-            builder.withWireframe();
-            state->setFillMode(gpu::State::FILL_LINE);
-        }
-        if (isBiased) {
-            builder.withDepthBias();
-            state->setDepthBias(1.0f);
-            state->setDepthBiasSlopeScale(1.0f);
-        }
-
-        auto baseBatchSetter = (forceLightBatchSetter || key.isTranslucent()) ? &lightBatchSetter : &batchSetter;
-        render::ShapePipeline::BatchSetter finalBatchSetter;
-        if (extraBatchSetter) {
-            finalBatchSetter = [baseBatchSetter, extraBatchSetter](const ShapePipeline& pipeline, gpu::Batch& batch, render::Args* args) {
-                baseBatchSetter(pipeline, batch, args);
-                extraBatchSetter(pipeline, batch, args);
-            };
-        } else {
-            finalBatchSetter = baseBatchSetter;
-        }
-        plumber.addPipeline(builder.build(), program, state, finalBatchSetter, itemSetter);
     }
 }
 
@@ -644,6 +642,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial
                         wasSet = true;
                     }
                     break;
+                case graphics::Material::CULL_FACE_MODE:
+                    if (!fallthrough) {
+                        multiMaterial.setCullFaceMode(material->getCullFaceMode());
+                        wasSet = true;
+                    }
+                    break;
                 default:
                     break;
             }
@@ -685,6 +689,8 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial
             case graphics::Material::MATERIAL_PARAMS:
                 // these are initialized to the correct default values in Schema()
                 break;
+            case graphics::Material::CULL_FACE_MODE:
+                multiMaterial.setCullFaceMode(graphics::Material::DEFAULT_CULL_FACE_MODE);
             case graphics::MaterialKey::ALBEDO_MAP_BIT:
                 if (schemaKey.isAlbedoMap()) {
                     drawMaterialTextures->setTexture(gr::Texture::MaterialAlbedo, textureCache->getWhiteTexture());
diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h
index cf41c85dd9..04b9919140 100644
--- a/libraries/render/src/render/ShapePipeline.h
+++ b/libraries/render/src/render/ShapePipeline.h
@@ -15,6 +15,7 @@
 #include <unordered_set>
 
 #include <gpu/Batch.h>
+#include <graphics/Material.h>
 
 #include "Args.h"
 
@@ -34,8 +35,9 @@ public:
         DUAL_QUAT_SKINNED,
         DEPTH_BIAS,
         WIREFRAME,
-        NO_CULL_FACE,
         FADE,
+        CULL_FACE_NONE, // if neither of these are set, we're CULL_FACE_BACK
+        CULL_FACE_FRONT,
 
         OWN_PIPELINE,
         INVALID,
@@ -81,9 +83,29 @@ public:
         Builder& withDualQuatSkinned() { _flags.set(DUAL_QUAT_SKINNED); return (*this); }
         Builder& withDepthBias() { _flags.set(DEPTH_BIAS); return (*this); }
         Builder& withWireframe() { _flags.set(WIREFRAME); return (*this); }
-        Builder& withoutCullFace() { _flags.set(NO_CULL_FACE); return (*this); }
         Builder& withFade() { _flags.set(FADE); return (*this); }
 
+        Builder& withoutCullFace() { return withCullFaceMode(graphics::MaterialKey::CullFaceMode::CULL_NONE); }
+        Builder& withCullFaceMode(graphics::MaterialKey::CullFaceMode cullFaceMode) {
+            switch (cullFaceMode) {
+                case graphics::MaterialKey::CullFaceMode::CULL_NONE:
+                    _flags.set(CULL_FACE_NONE);
+                    _flags.reset(CULL_FACE_FRONT);
+                    break;
+                case graphics::MaterialKey::CullFaceMode::CULL_FRONT:
+                    _flags.reset(CULL_FACE_NONE);
+                    _flags.set(CULL_FACE_FRONT);
+                    break;
+                case graphics::MaterialKey::CullFaceMode::CULL_BACK:
+                    _flags.reset(CULL_FACE_NONE);
+                    _flags.reset(CULL_FACE_FRONT);
+                    break;
+                default:
+                    break;
+            }
+            return (*this);
+        }
+
         Builder& withOwnPipeline() { _flags.set(OWN_PIPELINE); return (*this); }
         Builder& invalidate() { _flags.set(INVALID); return (*this); }
 
@@ -137,8 +159,27 @@ public:
             Builder& withWireframe() { _flags.set(WIREFRAME); _mask.set(WIREFRAME); return (*this); }
             Builder& withoutWireframe() { _flags.reset(WIREFRAME); _mask.set(WIREFRAME); return (*this); }
 
-            Builder& withCullFace() { _flags.reset(NO_CULL_FACE); _mask.set(NO_CULL_FACE); return (*this); }
-            Builder& withoutCullFace() { _flags.set(NO_CULL_FACE); _mask.set(NO_CULL_FACE); return (*this); }
+            Builder& withCullFaceMode(graphics::MaterialKey::CullFaceMode cullFaceMode) {
+                switch (cullFaceMode) {
+                    case graphics::MaterialKey::CullFaceMode::CULL_NONE:
+                        _flags.set(CULL_FACE_NONE);
+                        _flags.reset(CULL_FACE_FRONT);
+                        break;
+                    case graphics::MaterialKey::CullFaceMode::CULL_FRONT:
+                        _flags.reset(CULL_FACE_NONE);
+                        _flags.set(CULL_FACE_FRONT);
+                        break;
+                    case graphics::MaterialKey::CullFaceMode::CULL_BACK:
+                        _flags.reset(CULL_FACE_NONE);
+                        _flags.reset(CULL_FACE_FRONT);
+                        break;
+                    default:
+                        break;
+                }
+                _mask.set(CULL_FACE_NONE);
+                _mask.set(CULL_FACE_FRONT);
+                return (*this);
+            }
 
             Builder& withFade() { _flags.set(FADE); _mask.set(FADE); return (*this); }
             Builder& withoutFade() { _flags.reset(FADE); _mask.set(FADE); return (*this); }
@@ -168,7 +209,7 @@ public:
     bool isDualQuatSkinned() const { return _flags[DUAL_QUAT_SKINNED]; }
     bool isDepthBiased() const { return _flags[DEPTH_BIAS]; }
     bool isWireframe() const { return _flags[WIREFRAME]; }
-    bool isCullFace() const { return !_flags[NO_CULL_FACE]; }
+    bool isCullFace() const { return !_flags[CULL_FACE_NONE] && !_flags[CULL_FACE_FRONT]; }
     bool isFaded() const { return _flags[FADE]; }
 
     bool hasOwnPipeline() const { return _flags[OWN_PIPELINE]; }