diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 9c60139d06..dd9a10a237 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -2107,6 +2107,23 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
         }
         return false;
     });
+    EntityTree::setGetUnscaledDimensionsForIDOperator([this](const QUuid& id) {
+        if (_aboutToQuit) {
+            return glm::vec3(1.0f);
+        }
+
+        auto entity = getEntities()->getEntity(id);
+        if (entity) {
+            return entity->getUnscaledDimensions();
+        }
+
+        auto avatarManager = DependencyManager::get<AvatarManager>();
+        auto avatar = static_pointer_cast<Avatar>(avatarManager->getAvatarBySessionID(id));
+        if (avatar) {
+            return avatar->getSNScale();
+        }
+        return glm::vec3(1.0f);
+    });
     Procedural::opaqueStencil = [](gpu::StatePointer state) { PrepareStencil::testMaskDrawShape(*state); };
     Procedural::transparentStencil = [](gpu::StatePointer state) { PrepareStencil::testMask(*state); };
 
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 9b8ce8cf34..868d49bb5c 100644
--- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
+++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
@@ -791,7 +791,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);
             }
         }
@@ -815,7 +815,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);
             }
         }
@@ -1103,7 +1103,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);
         }
@@ -2121,6 +2121,8 @@ void Avatar::updateAttachmentRenderIDs() {
 
 void Avatar::updateDescendantRenderIDs() {
     _subItemLock.withWriteLock([&] {
+        auto oldRenderingDescendantEntityIDs = _renderingDescendantEntityIDs;
+        _renderingDescendantEntityIDs.clear();
         _descendantRenderIDs.clear();
         auto entityTreeRenderer = DependencyManager::get<EntityTreeRenderer>();
         EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr;
@@ -2130,7 +2132,12 @@ void Avatar::updateDescendantRenderIDs() {
                     if (object && object->getNestableType() == NestableType::Entity) {
                         EntityItemPointer entity = std::static_pointer_cast<EntityItem>(object);
                         if (entity->isVisible()) {
-                            auto renderer = entityTreeRenderer->renderableForEntityId(object->getID());
+                            auto id = object->getID();
+                            _renderingDescendantEntityIDs.insert(id);
+                            oldRenderingDescendantEntityIDs.erase(id);
+                            entity->setCullWithParent(true);
+
+                            auto renderer = entityTreeRenderer->renderableForEntityId(id);
                             if (renderer) {
                                 render::ItemIDs renderableSubItems;
                                 uint32_t numRenderableSubItems = renderer->metaFetchMetaSubItems(renderableSubItems);
@@ -2141,6 +2148,13 @@ void Avatar::updateDescendantRenderIDs() {
                         }
                     }
                 });
+
+                for (auto& oldRenderingDescendantEntityID : oldRenderingDescendantEntityIDs) {
+                    auto entity = entityTree->findEntityByEntityItemID(oldRenderingDescendantEntityID);
+                    if (entity) {
+                        entity->setCullWithParent(false);
+                    }
+                }
             });
         }
     });
diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
index 031b2f1309..6a740a793d 100644
--- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
+++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
@@ -782,6 +782,7 @@ protected:
     render::ItemIDs _attachmentRenderIDs;
     void updateDescendantRenderIDs();
     render::ItemIDs _descendantRenderIDs;
+    std::unordered_set<EntityItemID> _renderingDescendantEntityIDs;
     uint32_t _lastAncestorChainRenderableVersion { 0 };
 };
 
diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp
index fb3d2f1bf5..ae56a8fc47 100644
--- a/libraries/entities-renderer/src/RenderableEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp
@@ -168,7 +168,8 @@ render::hifi::Layer EntityRenderer::getHifiRenderLayer() const {
 }
 
 ItemKey EntityRenderer::getKey() {
-    ItemKey::Builder builder = ItemKey::Builder().withTypeShape().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
+    ItemKey::Builder builder =
+        ItemKey::Builder().withTypeShape().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
 
     if (isTransparent()) {
         builder.withTransparent();
@@ -176,6 +177,10 @@ ItemKey EntityRenderer::getKey() {
         builder.withShadowCaster();
     }
 
+    if (_cullWithParent) {
+        builder.withSubMetaCulled();
+    }
+
     if (!_visible) {
         builder.withInvisible();
     }
@@ -419,6 +424,7 @@ void EntityRenderer::doRenderUpdateSynchronous(const ScenePointer& scene, Transa
         setRenderLayer(entity->getRenderLayer());
         setPrimitiveMode(entity->getPrimitiveMode());
         _canCastShadow = entity->getCanCastShadow();
+        setCullWithParent(entity->getCullWithParent());
         _cauterized = entity->getCauterized();
         entity->setNeedsRenderUpdate(false);
     });
diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h
index d11f6dd6f4..227eb6a018 100644
--- a/libraries/entities-renderer/src/RenderableEntityItem.h
+++ b/libraries/entities-renderer/src/RenderableEntityItem.h
@@ -108,6 +108,16 @@ protected:
     virtual void setIsVisibleInSecondaryCamera(bool value) { _isVisibleInSecondaryCamera = value; }
     virtual void setRenderLayer(RenderLayer value) { _renderLayer = value; }
     virtual void setPrimitiveMode(PrimitiveMode value) { _primitiveMode = value; }
+    virtual void setCullWithParent(bool value) { _cullWithParent = value; }
+
+    template <typename F, typename T>
+    T withReadLockResult(const std::function<T()>& f) {
+        T result;
+        withReadLock([&] {
+            result = f();
+        });
+        return result;
+    }
 
 signals:
     void requestRenderUpdate();
@@ -130,6 +140,7 @@ protected:
     bool _visible { false };
     bool _isVisibleInSecondaryCamera { false };
     bool _canCastShadow { false };
+    bool _cullWithParent { false };
     RenderLayer _renderLayer { RenderLayer::WORLD };
     PrimitiveMode _primitiveMode { PrimitiveMode::SOLID };
     bool _cauterized { false };
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/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp
index a013ff75b7..c1b024a478 100644
--- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp
@@ -153,13 +153,13 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo
 
         if (urlChanged && !usingMaterialData) {
             _networkMaterial = DependencyManager::get<MaterialCache>()->getMaterial(_materialURL);
-            auto onMaterialRequestFinished = [this, oldParentID, oldParentMaterialName, newCurrentMaterialName](bool success) {
+            auto onMaterialRequestFinished = [this, entity, oldParentID, oldParentMaterialName, newCurrentMaterialName](bool success) {
                 if (success) {
                     deleteMaterial(oldParentID, oldParentMaterialName);
                     _texturesLoaded = false;
                     _parsedMaterials = _networkMaterial->parsedMaterials;
                     setCurrentMaterialName(newCurrentMaterialName);
-                    applyMaterial();
+                    applyMaterial(entity);
                 } else {
                     deleteMaterial(oldParentID, oldParentMaterialName);
                     _retryApply = false;
@@ -183,13 +183,13 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo
             _parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument::fromJson(_materialData.toUtf8()), _materialURL);
             // Since our material changed, the current name might not be valid anymore, so we need to update
             setCurrentMaterialName(newCurrentMaterialName);
-            applyMaterial();
+            applyMaterial(entity);
         } else {
             if (deleteNeeded) {
                 deleteMaterial(oldParentID, oldParentMaterialName);
             }
             if (addNeeded) {
-                applyMaterial();
+                applyMaterial(entity);
             }
         }
 
@@ -210,8 +210,7 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo
 }
 
 ItemKey MaterialEntityRenderer::getKey() {
-    ItemKey::Builder builder;
-    builder.withTypeShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
+    auto builder = ItemKey::Builder().withTypeShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
 
     if (!_visible) {
         builder.withInvisible();
@@ -382,7 +381,7 @@ void MaterialEntityRenderer::applyTextureTransform(std::shared_ptr<NetworkMateri
     material->setTextureTransforms(textureTransform, _materialMappingMode, _materialRepeat);
 }
 
-void MaterialEntityRenderer::applyMaterial() {
+void MaterialEntityRenderer::applyMaterial(const TypedEntityPointer& entity) {
     _retryApply = false;
 
     std::shared_ptr<NetworkMaterial> material = getMaterial();
@@ -396,6 +395,12 @@ void MaterialEntityRenderer::applyMaterial() {
 
     graphics::MaterialLayer materialLayer = graphics::MaterialLayer(material, _priority);
 
+    if (material->isProcedural()) {
+        auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(material);
+        procedural->setBoundOperator([this] { return getBound(); });
+        entity->setHasVertexShader(procedural->hasVertexShader());
+    }
+
     // Our parent could be an entity or an avatar
     std::string parentMaterialName = _parentMaterialName.toStdString();
     if (EntityTreeRenderer::addMaterialToEntity(parentID, materialLayer, parentMaterialName)) {
diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.h b/libraries/entities-renderer/src/RenderableMaterialEntityItem.h
index ff7367a44e..3a73c988eb 100644
--- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.h
+++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.h
@@ -56,7 +56,7 @@ private:
     void setCurrentMaterialName(const std::string& currentMaterialName);
 
     void applyTextureTransform(std::shared_ptr<NetworkMaterial>& material);
-    void applyMaterial();
+    void applyMaterial(const TypedEntityPointer& entity);
     void deleteMaterial(const QUuid& oldParentID, const QString& oldParentMaterialName);
 
     NetworkMaterialResourcePointer _networkMaterial;
diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
index 1f2be0c488..5fbbdfa0b8 100644
--- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
@@ -1057,8 +1057,10 @@ ModelEntityRenderer::ModelEntityRenderer(const EntityItemPointer& entity) : Pare
 void ModelEntityRenderer::setKey(bool didVisualGeometryRequestSucceed) {
     auto builder = ItemKey::Builder().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
 
-    if (_model && _model->isGroupCulled()) {
+    if (!_cullWithParent && _model && _model->isGroupCulled()) {
         builder.withMetaCullGroup();
+    } else if (_cullWithParent) {
+        builder.withSubMetaCulled();
     }
 
     if (didVisualGeometryRequestSucceed) {
@@ -1497,6 +1499,14 @@ void ModelEntityRenderer::setPrimitiveMode(PrimitiveMode value) {
     }
 }
 
+void ModelEntityRenderer::setCullWithParent(bool value) {
+    Parent::setCullWithParent(value);
+    setKey(_didLastVisualGeometryRequestSucceed);
+    if (_model) {
+        _model->setCullWithParent(_cullWithParent);
+    }
+}
+
 // NOTE: this only renders the "meta" portion of the Model, namely it renders debugging items
 void ModelEntityRenderer::doRender(RenderArgs* args) {
     DETAILED_PROFILE_RANGE(render_detail, "MetaModelRender");
diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h
index c32dad901f..0119c7bc26 100644
--- a/libraries/entities-renderer/src/RenderableModelEntityItem.h
+++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h
@@ -164,6 +164,7 @@ protected:
     void setIsVisibleInSecondaryCamera(bool value) override;
     void setRenderLayer(RenderLayer value) override;
     void setPrimitiveMode(PrimitiveMode value) override;
+    void setCullWithParent(bool value) override;
 
 private:
     void animate(const TypedEntityPointer& entity);
diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp
index e3528e2291..f34eb85230 100644
--- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp
@@ -151,11 +151,17 @@ void ParticleEffectEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEn
 
 ItemKey ParticleEffectEntityRenderer::getKey() {
     // FIXME: implement isTransparent() for particles and an opaque pipeline
-    if (_visible) {
-        return ItemKey::Builder::transparentShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
-    } else {
-        return ItemKey::Builder().withInvisible().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()).build();
+    auto builder = ItemKey::Builder::transparentShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
+
+    if (!_visible) {
+        builder.withInvisible();
     }
+
+    if (_cullWithParent) {
+        builder.withSubMetaCulled();
+    }
+
+    return builder.build();
 }
 
 ShapeKey ParticleEffectEntityRenderer::getShapeKey() {
diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp
index 9a762b3b3a..e75f8593d6 100644
--- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp
@@ -97,9 +97,17 @@ void PolyLineEntityRenderer::buildPipelines() {
 }
 
 ItemKey PolyLineEntityRenderer::getKey() {
-    return isTransparent() ?
-        ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()) :
-        ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
+    auto builder = ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
+
+    if (isTransparent()) {
+        builder.withTransparent();
+    }
+
+    if (_cullWithParent) {
+        builder.withSubMetaCulled();
+    }
+
+    return builder.build();
 }
 
 ShapeKey PolyLineEntityRenderer::getShapeKey() {
diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp
index 853e36b45b..1555604604 100644
--- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp
@@ -1783,6 +1783,16 @@ PolyVoxEntityRenderer::PolyVoxEntityRenderer(const EntityItemPointer& entity) :
     _params = std::make_shared<gpu::Buffer>(sizeof(glm::vec4), nullptr);
 }
 
+ItemKey PolyVoxEntityRenderer::getKey() {
+    auto builder = ItemKey::Builder::opaqueShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
+
+    if (_cullWithParent) {
+        builder.withSubMetaCulled();
+    }
+
+    return builder.build();
+}
+
 ShapeKey PolyVoxEntityRenderer::getShapeKey() {
     auto builder = ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER);
     if (_primitiveMode == PrimitiveMode::LINES) {
diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h
index c322959f86..825b4429cd 100644
--- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h
+++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h
@@ -209,7 +209,7 @@ public:
     }
 
 protected:
-    virtual ItemKey getKey() override { return ItemKey::Builder::opaqueShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()); }
+    virtual ItemKey getKey() override;
     virtual ShapeKey getShapeKey() override;
     virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override;
     virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
index 88cc78b6b6..d5a0e22199 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();
     }
@@ -207,6 +208,18 @@ ShapeKey ShapeEntityRenderer::getShapeKey() {
     return builder.build();
 }
 
+Item::Bound ShapeEntityRenderer::getBound() {
+    auto mat = _materials.find("0");
+    if (mat != _materials.end() && mat->second.top().material && mat->second.top().material->isProcedural() &&
+        mat->second.top().material->isReady()) {
+        auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(mat->second.top().material);
+        if (procedural->hasVertexShader() && procedural->hasBoundOperator()) {
+           return procedural->getBound();
+        }
+    }
+    return Parent::getBound();
+}
+
 void ShapeEntityRenderer::doRender(RenderArgs* args) {
     PerformanceTimer perfTimer("RenderableShapeEntityItem::render");
     Q_ASSERT(args->_batch);
@@ -251,7 +264,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/entities-renderer/src/RenderableShapeEntityItem.h b/libraries/entities-renderer/src/RenderableShapeEntityItem.h
index 6061526f75..5bc61606ad 100644
--- a/libraries/entities-renderer/src/RenderableShapeEntityItem.h
+++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.h
@@ -26,6 +26,7 @@ public:
 
 protected:
     ShapeKey getShapeKey() override;
+    Item::Bound getBound() override;
 
 private:
     virtual bool needsRenderUpdate() const override;
diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp
index 598ac17510..cc215d3528 100644
--- a/libraries/entities/src/EntityItem.cpp
+++ b/libraries/entities/src/EntityItem.cpp
@@ -3025,6 +3025,17 @@ void EntityItem::setCanCastShadow(bool value) {
     });
 }
 
+bool EntityItem::getCullWithParent() const {
+    return _cullWithParent;
+}
+
+void EntityItem::setCullWithParent(bool value) {
+    withWriteLock([&] {
+        _needsRenderUpdate |= _cullWithParent != value;
+        _cullWithParent = value;
+    });
+}
+
 bool EntityItem::isChildOfMyAvatar() const {
     QUuid ancestorID = findAncestorOfType(NestableType::Avatar);
     return !ancestorID.isNull() && (ancestorID == Physics::getSessionUUID() || ancestorID == AVATAR_SELF_ID);
diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h
index 3274379ee9..14b8b259cc 100644
--- a/libraries/entities/src/EntityItem.h
+++ b/libraries/entities/src/EntityItem.h
@@ -305,6 +305,9 @@ public:
     bool getCanCastShadow() const;
     void setCanCastShadow(bool value);
 
+    bool getCullWithParent() const;
+    void setCullWithParent(bool value);
+
     void setCauterized(bool value);
     bool getCauterized() const;
 
@@ -762,6 +765,8 @@ protected:
 
     QHash<QUuid, EntityDynamicPointer> _grabActions;
 
+    bool _cullWithParent { false };
+    
     mutable bool _needsRenderUpdate { false };
 
 private:
diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp
index 6c12c6d019..41008625e8 100644
--- a/libraries/entities/src/EntityTree.cpp
+++ b/libraries/entities/src/EntityTree.cpp
@@ -3113,6 +3113,7 @@ std::function<QObject*(const QUuid&)> EntityTree::_getEntityObjectOperator = nul
 std::function<QSizeF(const QUuid&, const QString&)> EntityTree::_textSizeOperator = nullptr;
 std::function<bool()> EntityTree::_areEntityClicksCapturedOperator = nullptr;
 std::function<void(const QUuid&, const QVariant&)> EntityTree::_emitScriptEventOperator = nullptr;
+std::function<glm::vec3(const QUuid&)> EntityTree::_getUnscaledDimensionsForIDOperator = nullptr;
 
 QObject* EntityTree::getEntityObject(const QUuid& id) {
     if (_getEntityObjectOperator) {
@@ -3141,6 +3142,13 @@ void EntityTree::emitScriptEvent(const QUuid& id, const QVariant& message) {
     }
 }
 
+glm::vec3 EntityTree::getUnscaledDimensionsForID(const QUuid& id) {
+    if (_getUnscaledDimensionsForIDOperator) {
+        return _getUnscaledDimensionsForIDOperator(id);
+    }
+    return glm::vec3(1.0f);
+}
+
 void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender,
                                                MovingEntitiesOperator& moveOperator, bool force, bool tellServer) {
     // if the queryBox has changed, tell the entity-server
diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h
index 9108f8d8d2..a0fcbd3244 100644
--- a/libraries/entities/src/EntityTree.h
+++ b/libraries/entities/src/EntityTree.h
@@ -271,6 +271,9 @@ public:
     static void setEmitScriptEventOperator(std::function<void(const QUuid&, const QVariant&)> emitScriptEventOperator) { _emitScriptEventOperator = emitScriptEventOperator; }
     static void emitScriptEvent(const QUuid& id, const QVariant& message);
 
+    static void setGetUnscaledDimensionsForIDOperator(std::function<glm::vec3(const QUuid&)> getUnscaledDimensionsForIDOperator) { _getUnscaledDimensionsForIDOperator = getUnscaledDimensionsForIDOperator; }
+    static glm::vec3 getUnscaledDimensionsForID(const QUuid& id);
+
     std::map<QString, QString> getNamedPaths() const { return _namedPaths; }
 
     void updateEntityQueryAACube(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender,
@@ -386,6 +389,7 @@ private:
     static std::function<QSizeF(const QUuid&, const QString&)> _textSizeOperator;
     static std::function<bool()> _areEntityClicksCapturedOperator;
     static std::function<void(const QUuid&, const QVariant&)> _emitScriptEventOperator;
+    static std::function<glm::vec3(const QUuid&)> _getUnscaledDimensionsForIDOperator;
 
     std::vector<int32_t> _staleProxies;
 
diff --git a/libraries/entities/src/MaterialEntityItem.cpp b/libraries/entities/src/MaterialEntityItem.cpp
index 1a7c3c601b..73bebfc403 100644
--- a/libraries/entities/src/MaterialEntityItem.cpp
+++ b/libraries/entities/src/MaterialEntityItem.cpp
@@ -139,10 +139,10 @@ void MaterialEntityItem::debugDump() const {
 
 void MaterialEntityItem::setUnscaledDimensions(const glm::vec3& value) {
     _desiredDimensions = value;
-    if (_materialMappingMode == MaterialMappingMode::UV) {
-        EntityItem::setUnscaledDimensions(ENTITY_ITEM_DEFAULT_DIMENSIONS);
-    } else if (_materialMappingMode == MaterialMappingMode::PROJECTED) {
+    if (_hasVertexShader || _materialMappingMode == MaterialMappingMode::PROJECTED) {
         EntityItem::setUnscaledDimensions(value);
+    } else if (_materialMappingMode == MaterialMappingMode::UV) {
+        EntityItem::setUnscaledDimensions(ENTITY_ITEM_DEFAULT_DIMENSIONS);
     }
 }
 
@@ -264,6 +264,13 @@ void MaterialEntityItem::setMaterialRepeat(bool value) {
     });
 }
 
+void MaterialEntityItem::setParentID(const QUuid& parentID) {
+    if (parentID != getParentID()) {
+        EntityItem::setParentID(parentID);
+        _hasVertexShader = false;
+    }
+}
+
 AACube MaterialEntityItem::calculateInitialQueryAACube(bool& success) {
     AACube aaCube = EntityItem::calculateInitialQueryAACube(success);
     // A Material entity's queryAACube contains its parent's queryAACube
@@ -278,3 +285,16 @@ AACube MaterialEntityItem::calculateInitialQueryAACube(bool& success) {
     }
     return aaCube;
 }
+
+void MaterialEntityItem::setHasVertexShader(bool hasVertexShader) {
+    bool prevHasVertexShader = _hasVertexShader;
+    _hasVertexShader = hasVertexShader;
+
+    if (hasVertexShader && !prevHasVertexShader) {
+        setLocalPosition(glm::vec3(0.0f));
+        setLocalOrientation(glm::quat());
+        setUnscaledDimensions(EntityTree::getUnscaledDimensionsForID(getParentID()));
+    } else if (!hasVertexShader && prevHasVertexShader) {
+        setUnscaledDimensions(_desiredDimensions);
+    }
+}
\ No newline at end of file
diff --git a/libraries/entities/src/MaterialEntityItem.h b/libraries/entities/src/MaterialEntityItem.h
index 3f32486f0b..d8de8c3bc6 100644
--- a/libraries/entities/src/MaterialEntityItem.h
+++ b/libraries/entities/src/MaterialEntityItem.h
@@ -64,6 +64,8 @@ public:
     QString getParentMaterialName() const;
     void setParentMaterialName(const QString& parentMaterialName);
 
+    void setParentID(const QUuid& parentID) override;
+
     glm::vec2 getMaterialMappingPos() const;
     void setMaterialMappingPos(const glm::vec2& materialMappingPos);
     glm::vec2 getMaterialMappingScale() const;
@@ -73,6 +75,8 @@ public:
 
     AACube calculateInitialQueryAACube(bool& success) override;
 
+    void setHasVertexShader(bool hasVertexShader);
+
 private:
     // URL for this material.  Currently, only JSON format is supported.  Set to "materialData" to use the material data to live edit a material.
     // The following fields are supported in the JSON:
@@ -108,6 +112,8 @@ private:
     float _materialMappingRot { 0 };
     QString _materialData;
 
+    bool _hasVertexShader { false };
+
 };
 
 #endif // hifi_MaterialEntityItem_h
diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp
index 5d4daf53f7..e117e1f211 100755
--- a/libraries/fbx/src/GLTFSerializer.cpp
+++ b/libraries/fbx/src/GLTFSerializer.cpp
@@ -225,18 +225,17 @@ int GLTFSerializer::getAccessorType(const QString& type)
     return GLTFAccessorType::SCALAR;
 }
 
-int GLTFSerializer::getMaterialAlphaMode(const QString& type)
-{
+graphics::MaterialKey::OpacityMapMode GLTFSerializer::getMaterialAlphaMode(const QString& type) {
     if (type == "OPAQUE") {
-        return GLTFMaterialAlphaMode::OPAQUE;
+        return graphics::MaterialKey::OPACITY_MAP_OPAQUE;
     }
     if (type == "MASK") {
-        return GLTFMaterialAlphaMode::MASK;
+        return graphics::MaterialKey::OPACITY_MAP_MASK;
     }
     if (type == "BLEND") {
-        return GLTFMaterialAlphaMode::BLEND;
+        return graphics::MaterialKey::OPACITY_MAP_BLEND;
     }
-    return GLTFMaterialAlphaMode::OPAQUE;
+    return graphics::MaterialKey::OPACITY_MAP_BLEND;
 }
 
 int GLTFSerializer::getCameraType(const QString& type)
@@ -484,9 +483,9 @@ bool GLTFSerializer::addMaterial(const QJsonObject& object) {
     getIndexFromObject(object, "normalTexture", material.normalTexture, material.defined);
     getIndexFromObject(object, "occlusionTexture", material.occlusionTexture, material.defined);
     getBoolVal(object, "doubleSided", material.doubleSided, material.defined);
-    QString alphamode;
-    if (getStringVal(object, "alphaMode", alphamode, material.defined)) {
-        material.alphaMode = getMaterialAlphaMode(alphamode);
+    QString alphaMode;
+    if (getStringVal(object, "alphaMode", alphaMode, material.defined)) {
+        material.alphaMode = getMaterialAlphaMode(alphaMode);
     }
     getDoubleVal(object, "alphaCutoff", material.alphaCutoff, material.defined);
     QJsonObject jsMetallicRoughness;
@@ -1764,62 +1763,72 @@ HFMTexture GLTFSerializer::getHFMTexture(const GLTFTexture& texture) {
     return fbxtex;
 }
 
-void GLTFSerializer::setHFMMaterial(HFMMaterial& fbxmat, const GLTFMaterial& material) {
+void GLTFSerializer::setHFMMaterial(HFMMaterial& hfmMat, const GLTFMaterial& material) {
+    if (material.defined["alphaMode"]) {
+        hfmMat._material->setOpacityMapMode(material.alphaMode);
+    } else {
+        hfmMat._material->setOpacityMapMode(graphics::MaterialKey::OPACITY_MAP_OPAQUE); // GLTF defaults to opaque
+    }
 
+    if (material.defined["alphaCutoff"]) {
+        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]);
-        fbxmat._material->setEmissive(emissive);
+        glm::vec3 emissive = glm::vec3(material.emissiveFactor[0], material.emissiveFactor[1], material.emissiveFactor[2]);
+        hfmMat._material->setEmissive(emissive);
     }
 
     if (material.defined["emissiveTexture"]) {
-        fbxmat.emissiveTexture = getHFMTexture(_file.textures[material.emissiveTexture]);
-        fbxmat.useEmissiveMap = true;
+        hfmMat.emissiveTexture = getHFMTexture(_file.textures[material.emissiveTexture]);
+        hfmMat.useEmissiveMap = true;
     }
     
     if (material.defined["normalTexture"]) {
-        fbxmat.normalTexture = getHFMTexture(_file.textures[material.normalTexture]);
-        fbxmat.useNormalMap = true;
+        hfmMat.normalTexture = getHFMTexture(_file.textures[material.normalTexture]);
+        hfmMat.useNormalMap = true;
     }
     
     if (material.defined["occlusionTexture"]) {
-        fbxmat.occlusionTexture = getHFMTexture(_file.textures[material.occlusionTexture]);
-        fbxmat.useOcclusionMap = true;
+        hfmMat.occlusionTexture = getHFMTexture(_file.textures[material.occlusionTexture]);
+        hfmMat.useOcclusionMap = true;
     }
 
     if (material.defined["pbrMetallicRoughness"]) {
-        fbxmat.isPBSMaterial = true;
-        
+        hfmMat.isPBSMaterial = true;
+
         if (material.pbrMetallicRoughness.defined["metallicFactor"]) {
-            fbxmat.metallic = material.pbrMetallicRoughness.metallicFactor;
+            hfmMat.metallic = material.pbrMetallicRoughness.metallicFactor;
         }
         if (material.pbrMetallicRoughness.defined["baseColorTexture"]) {
-            fbxmat.opacityTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]);
-            fbxmat.albedoTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]);
-            fbxmat.useAlbedoMap = true;
+            hfmMat.opacityTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]);
+            hfmMat.albedoTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]);
+            hfmMat.useAlbedoMap = true;
         }
         if (material.pbrMetallicRoughness.defined["metallicRoughnessTexture"]) {
-            fbxmat.roughnessTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]);
-            fbxmat.roughnessTexture.sourceChannel = image::ColorChannel::GREEN;
-            fbxmat.useRoughnessMap = true;
-            fbxmat.metallicTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]);
-            fbxmat.metallicTexture.sourceChannel = image::ColorChannel::BLUE;
-            fbxmat.useMetallicMap = true;
+            hfmMat.roughnessTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]);
+            hfmMat.roughnessTexture.sourceChannel = image::ColorChannel::GREEN;
+            hfmMat.useRoughnessMap = true;
+            hfmMat.metallicTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]);
+            hfmMat.metallicTexture.sourceChannel = image::ColorChannel::BLUE;
+            hfmMat.useMetallicMap = true;
         }
         if (material.pbrMetallicRoughness.defined["roughnessFactor"]) {
-            fbxmat._material->setRoughness(material.pbrMetallicRoughness.roughnessFactor);
+            hfmMat._material->setRoughness(material.pbrMetallicRoughness.roughnessFactor);
         }
         if (material.pbrMetallicRoughness.defined["baseColorFactor"] && 
             material.pbrMetallicRoughness.baseColorFactor.size() == 4) {
-            glm::vec3 dcolor =  glm::vec3(material.pbrMetallicRoughness.baseColorFactor[0], 
-                                          material.pbrMetallicRoughness.baseColorFactor[1], 
-                                          material.pbrMetallicRoughness.baseColorFactor[2]);
-            fbxmat.diffuseColor = dcolor;
-            fbxmat._material->setAlbedo(dcolor);
-            fbxmat._material->setOpacity(material.pbrMetallicRoughness.baseColorFactor[3]);
-        }   
+            glm::vec3 dcolor =
+                glm::vec3(material.pbrMetallicRoughness.baseColorFactor[0], material.pbrMetallicRoughness.baseColorFactor[1],
+                          material.pbrMetallicRoughness.baseColorFactor[2]);
+            hfmMat.diffuseColor = dcolor;
+            hfmMat._material->setAlbedo(dcolor);
+            hfmMat._material->setOpacity(material.pbrMetallicRoughness.baseColorFactor[3]);
+        }
     }
 
 }
diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h
index 4d72805863..b1020f7154 100755
--- a/libraries/fbx/src/GLTFSerializer.h
+++ b/libraries/fbx/src/GLTFSerializer.h
@@ -416,21 +416,13 @@ struct GLTFpbrMetallicRoughness {
     }
 };
 
-namespace GLTFMaterialAlphaMode {
-    enum Values {
-        OPAQUE = 0,
-        MASK,
-        BLEND
-    };
-};
-
 struct GLTFMaterial {
     QString name;
     QVector<double> emissiveFactor;
     int emissiveTexture;
     int normalTexture;
     int occlusionTexture;
-    int alphaMode;
+    graphics::MaterialKey::OpacityMapMode alphaMode;
     double alphaCutoff;
     bool doubleSided;
     GLTFpbrMetallicRoughness pbrMetallicRoughness;
@@ -451,6 +443,12 @@ struct GLTFMaterial {
         if (defined["emissiveFactor"]) {
             qCDebug(modelformat) << "emissiveFactor: " << emissiveFactor;
         }
+        if (defined["alphaMode"]) {
+            qCDebug(modelformat) << "alphaMode: " << alphaMode;
+        }
+        if (defined["alphaCutoff"]) {
+            qCDebug(modelformat) << "alphaCutoff: " << alphaCutoff;
+        }
         if (defined["pbrMetallicRoughness"]) {
             pbrMetallicRoughness.dump();
         }
@@ -800,7 +798,7 @@ private:
 
     hifi::ByteArray setGLBChunks(const hifi::ByteArray& data);
     
-    int getMaterialAlphaMode(const QString& type);
+    graphics::MaterialKey::OpacityMapMode getMaterialAlphaMode(const QString& type);
     int getAccessorType(const QString& type);
     int getAnimationSamplerInterpolation(const QString& interpolation);
     int getCameraType(const QString& type);
@@ -854,7 +852,7 @@ private:
     bool doesResourceExist(const QString& url);
 
 
-    void setHFMMaterial(HFMMaterial& fbxmat, const GLTFMaterial& material);
+    void setHFMMaterial(HFMMaterial& hfmMat, const GLTFMaterial& material);
     HFMTexture getHFMTexture(const GLTFTexture& texture);
     void glTFDebugDump();
     void hfmDebugDump(const HFMModel& hfmModel);
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 d76c1fcbd2..c0a2d4bf25 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 d80c3adfbc..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();
 }
 
@@ -209,8 +226,7 @@ bool Material::resetOpacityMap() const {
             }
         }
     }
-    auto newious = _key.getOpacityMapMode();
-    if (previous != newious) {
+    if (previous != _key.getOpacityMapMode()) {
         //opacity change detected for this material
         return true;
     }
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/graphics/src/graphics/MaterialTextures.slh b/libraries/graphics/src/graphics/MaterialTextures.slh
index 2a291e5d57..cb83f7d9cf 100644
--- a/libraries/graphics/src/graphics/MaterialTextures.slh
+++ b/libraries/graphics/src/graphics/MaterialTextures.slh
@@ -214,18 +214,18 @@ vec3 fetchLightMap(vec2 uv) {
 }
 <@endfunc@>
 
-<@func evalMaterialOpacityMask(fetchedOpacity, materialOpacityCutoff, opacity)@>
+<@func evalMaterialOpacityMask(fetchedOpacity, materialOpacityCutoff, materialOpacity, matKey, opacity)@>
 {
-    // This path only valid for opaque or texel opaque material 
-    <$opacity$> = step(<$materialOpacityCutoff$>, <$fetchedOpacity$>);
+    // This path only valid for opaque or texel opaque material
+    <$opacity$> = mix(<$materialOpacity$>,
+                      step(<$materialOpacityCutoff$>, <$fetchedOpacity$>),
+                      float((<$matKey$> & OPACITY_MASK_MAP_BIT) != 0));
 }
 <@endfunc@>
 
-
 <@func evalMaterialOpacity(fetchedOpacity, materialOpacityCutoff, materialOpacity, matKey, opacity)@>
 {
     // This path only valid for transparent material
-    // Assert that float((<$matKey$> & (OPACITY_TRANSLUCENT_MAP_BIT | OPACITY_MASK_MAP_BIT)) != 0)) == 1.0
     <$opacity$> = mix(<$fetchedOpacity$>,
                           step(<$materialOpacityCutoff$>, <$fetchedOpacity$>),
                           float((<$matKey$> & OPACITY_MASK_MAP_BIT) != 0))
diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h
index 888e562bca..7111ad2e65 100644
--- a/libraries/hfm/src/hfm/HFM.h
+++ b/libraries/hfm/src/hfm/HFM.h
@@ -173,24 +173,27 @@ public:
     void getTextureNames(QSet<QString>& textureList) const;
     void setMaxNumPixelsPerTexture(int maxNumPixels);
 
-    glm::vec3 diffuseColor{ 1.0f };
-    float diffuseFactor{ 1.0f };
-    glm::vec3 specularColor{ 0.02f };
-    float specularFactor{ 1.0f };
+    glm::vec3 diffuseColor { 1.0f };
+    float diffuseFactor { 1.0f };
+    glm::vec3 specularColor { 0.02f };
+    float specularFactor { 1.0f };
 
-    glm::vec3 emissiveColor{ 0.0f };
-    float emissiveFactor{ 0.0f };
+    glm::vec3 emissiveColor { 0.0f };
+    float emissiveFactor { 0.0f };
 
-    float shininess{ 23.0f };
-    float opacity{ 1.0f };
+    float shininess { 23.0f };
+    float opacity { 1.0f };
 
-    float metallic{ 0.0f };
-    float roughness{ 1.0f };
-    float emissiveIntensity{ 1.0f };
-    float ambientFactor{ 1.0f };
+    float metallic { 0.0f };
+    float roughness { 1.0f };
+    float emissiveIntensity { 1.0f };
+    float ambientFactor { 1.0f };
 
     float bumpMultiplier { 1.0f }; // TODO: to be implemented
 
+    graphics::MaterialKey::OpacityMapMode alphaMode { graphics::MaterialKey::OPACITY_MAP_BLEND };
+    float alphaCutoff { 0.5f };
+
     QString materialID;
     QString name;
     QString shadingModel;
@@ -207,19 +210,19 @@ public:
     Texture occlusionTexture;
     Texture scatteringTexture;
     Texture lightmapTexture;
-    glm::vec2 lightmapParams{ 0.0f, 1.0f };
+    glm::vec2 lightmapParams { 0.0f, 1.0f };
 
 
-    bool isPBSMaterial{ false };
+    bool isPBSMaterial { false };
     // THe use XXXMap are not really used to drive which map are going or not, debug only
-    bool useNormalMap{ false };
-    bool useAlbedoMap{ false };
-    bool useOpacityMap{ false };
-    bool useRoughnessMap{ false };
-    bool useSpecularMap{ false };
-    bool useMetallicMap{ false };
-    bool useEmissiveMap{ false };
-    bool useOcclusionMap{ false };
+    bool useNormalMap { false };
+    bool useAlbedoMap { false };
+    bool useOpacityMap { false };
+    bool useRoughnessMap { false };
+    bool useSpecularMap { false };
+    bool useMetallicMap { false };
+    bool useEmissiveMap { false };
+    bool useOcclusionMap { false };
 
     bool needTangentSpace() const;
 };
diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h
index fb18866001..505bbb94b5 100644
--- a/libraries/networking/src/DomainHandler.h
+++ b/libraries/networking/src/DomainHandler.h
@@ -12,6 +12,8 @@
 #ifndef hifi_DomainHandler_h
 #define hifi_DomainHandler_h
 
+#include <QProcessEnvironment>
+
 #include <QtCore/QJsonObject>
 #include <QtCore/QObject>
 #include <QtCore/QTimer>
@@ -30,10 +32,37 @@
 #include "ReceivedMessage.h"
 #include "NetworkingConstants.h"
 
-const unsigned short DEFAULT_DOMAIN_SERVER_PORT = 40102;
-const unsigned short DEFAULT_DOMAIN_SERVER_DTLS_PORT = 40103;
-const quint16 DOMAIN_SERVER_HTTP_PORT = 40100;
-const quint16 DOMAIN_SERVER_HTTPS_PORT = 40101;
+const unsigned short DEFAULT_DOMAIN_SERVER_PORT = 
+    QProcessEnvironment::systemEnvironment()
+    .contains("HIFI_DOMAIN_SERVER_PORT")
+        ? QProcessEnvironment::systemEnvironment()
+            .value("HIFI_DOMAIN_SERVER_PORT")
+            .toUShort()
+        : 40102;
+
+const unsigned short DEFAULT_DOMAIN_SERVER_DTLS_PORT = 
+    QProcessEnvironment::systemEnvironment()
+    .contains("HIFI_DOMAIN_SERVER_DTLS_PORT")
+        ? QProcessEnvironment::systemEnvironment()
+            .value("HIFI_DOMAIN_SERVER_DTLS_PORT")
+            .toUShort()
+        : 40103;
+
+const quint16 DOMAIN_SERVER_HTTP_PORT = 
+    QProcessEnvironment::systemEnvironment()
+    .contains("HIFI_DOMAIN_SERVER_HTTP_PORT")
+        ? QProcessEnvironment::systemEnvironment()
+            .value("HIFI_DOMAIN_SERVER_HTTP_PORT")
+            .toUInt()
+        : 40100;
+
+const quint16 DOMAIN_SERVER_HTTPS_PORT = 
+    QProcessEnvironment::systemEnvironment()
+    .contains("HIFI_DOMAIN_SERVER_HTTPS_PORT")
+        ? QProcessEnvironment::systemEnvironment()
+            .value("HIFI_DOMAIN_SERVER_HTTPS_PORT")
+            .toUInt()
+        : 40101;
 
 const int MAX_SILENT_DOMAIN_SERVER_CHECK_INS = 5;
 
diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp
index 43c6b25dcb..22bfc3f1ad 100644
--- a/libraries/procedural/src/procedural/Procedural.cpp
+++ b/libraries/procedural/src/procedural/Procedural.cpp
@@ -29,6 +29,7 @@ Q_LOGGING_CATEGORY(proceduralLog, "hifi.gpu.procedural")
 
 // User-data parsing constants
 static const QString PROCEDURAL_USER_DATA_KEY = "ProceduralEntity";
+static const QString VERTEX_URL_KEY = "vertexShaderURL";
 static const QString FRAGMENT_URL_KEY = "fragmentShaderURL";
 static const QString URL_KEY = "shaderUrl";
 static const QString VERSION_KEY = "version";
@@ -42,6 +43,7 @@ static const std::string PROCEDURAL_VERSION = "//PROCEDURAL_VERSION";
 bool operator==(const ProceduralData& a, const ProceduralData& b) {
     return ((a.version == b.version) &&
             (a.fragmentShaderUrl == b.fragmentShaderUrl) &&
+            (a.vertexShaderUrl == b.vertexShaderUrl) &&
             (a.uniforms == b.uniforms) &&
             (a.channels == b.channels));
 }
@@ -101,9 +103,9 @@ void ProceduralData::parse(const QJsonObject& proceduralData) {
         }
     }
 
-    // Empty shader URL isn't valid
-    if (fragmentShaderUrl.isEmpty()) {
-        return;
+    {  // Vertex shader URL
+        auto rawShaderUrl = proceduralData[VERTEX_URL_KEY].toString();
+        vertexShaderUrl = DependencyManager::get<ResourceManager>()->normalizeURL(rawShaderUrl);
     }
 
     uniforms = proceduralData[UNIFORMS_KEY].toObject();
@@ -172,29 +174,57 @@ void Procedural::setProceduralData(const ProceduralData& proceduralData) {
 
     if (proceduralData.fragmentShaderUrl != _data.fragmentShaderUrl) {
         _data.fragmentShaderUrl = proceduralData.fragmentShaderUrl;
-        const auto& shaderUrl = _data.fragmentShaderUrl;
 
         _shaderDirty = true;
         _networkFragmentShader.reset();
         _fragmentShaderPath.clear();
         _fragmentShaderSource.clear();
 
-        if (shaderUrl.isEmpty() || !shaderUrl.isValid()) {
+        if (!_data.fragmentShaderUrl.isValid()) {
+            qCWarning(proceduralLog) << "Invalid fragment shader URL: " << _data.fragmentShaderUrl;
             return;
         }
 
-        if (shaderUrl.isLocalFile()) {
-            if (!QFileInfo(shaderUrl.toLocalFile()).exists()) {
+        if (_data.fragmentShaderUrl.isLocalFile()) {
+            if (!QFileInfo(_data.fragmentShaderUrl.toLocalFile()).exists()) {
+                qCWarning(proceduralLog) << "Invalid fragment shader URL, missing local file: " << _data.fragmentShaderUrl;
                 return;
             }
-            _fragmentShaderPath = shaderUrl.toLocalFile();
-        } else if (shaderUrl.scheme() == URL_SCHEME_QRC) {
-            _fragmentShaderPath = ":" + shaderUrl.path();
+            _fragmentShaderPath = _data.fragmentShaderUrl.toLocalFile();
+        } else if (_data.fragmentShaderUrl.scheme() == URL_SCHEME_QRC) {
+            _fragmentShaderPath = ":" + _data.fragmentShaderUrl.path();
         } else {
-            _networkFragmentShader = ShaderCache::instance().getShader(shaderUrl);
+            _networkFragmentShader = ShaderCache::instance().getShader(_data.fragmentShaderUrl);
         }
     }
 
+    if (proceduralData.vertexShaderUrl != _data.vertexShaderUrl) {
+        _data.vertexShaderUrl = proceduralData.vertexShaderUrl;
+
+        _shaderDirty = true;
+        _networkVertexShader.reset();
+        _vertexShaderPath.clear();
+        _vertexShaderSource.clear();
+
+        if (!_data.vertexShaderUrl.isValid()) {
+            qCWarning(proceduralLog) << "Invalid vertex shader URL: " << _data.vertexShaderUrl;
+            return;
+        }
+
+        if (_data.vertexShaderUrl.isLocalFile()) {
+            if (!QFileInfo(_data.vertexShaderUrl.toLocalFile()).exists()) {
+                qCWarning(proceduralLog) << "Invalid vertex shader URL, missing local file: " << _data.vertexShaderUrl;
+                return;
+            }
+            _vertexShaderPath = _data.vertexShaderUrl.toLocalFile();
+        } else if (_data.vertexShaderUrl.scheme() == URL_SCHEME_QRC) {
+            _vertexShaderPath = ":" + _data.vertexShaderUrl.path();
+        } else {
+            _networkVertexShader = ShaderCache::instance().getShader(_data.vertexShaderUrl);
+        }
+
+    }
+
     _enabled = true;
 }
 
@@ -213,8 +243,12 @@ bool Procedural::isReady() const {
         _fadeStartTime = usecTimestampNow();
     }
 
-    // Do we have a network or local shader, and if so, is it loaded?
-    if (_fragmentShaderPath.isEmpty() && (!_networkFragmentShader || !_networkFragmentShader->isLoaded())) {
+    // We need to have at least one shader, and whichever ones we have need to be loaded
+    bool hasFragmentShader = !_fragmentShaderPath.isEmpty() || _networkFragmentShader;
+    bool fragmentShaderLoaded = !_fragmentShaderPath.isEmpty() || (_networkFragmentShader && _networkFragmentShader->isLoaded());
+    bool hasVertexShader = !_vertexShaderPath.isEmpty() || _networkVertexShader;
+    bool vertexShaderLoaded = !_vertexShaderPath.isEmpty() || (_networkVertexShader && _networkVertexShader->isLoaded());
+    if ((!hasFragmentShader && !hasVertexShader) || (hasFragmentShader && !fragmentShaderLoaded) || (hasVertexShader && !vertexShaderLoaded)) {
         return false;
     }
 
@@ -258,6 +292,20 @@ void Procedural::prepare(gpu::Batch& batch,
         _shaderDirty = true;
     }
 
+    if (!_vertexShaderPath.isEmpty()) {
+        auto lastModified = (uint64_t)QFileInfo(_vertexShaderPath).lastModified().toMSecsSinceEpoch();
+        if (lastModified > _vertexShaderModified) {
+            QFile file(_vertexShaderPath);
+            file.open(QIODevice::ReadOnly);
+            _vertexShaderSource = QTextStream(&file).readAll();
+            _shaderDirty = true;
+            _vertexShaderModified = lastModified;
+        }
+    } else if (_vertexShaderSource.isEmpty() && _networkVertexShader && _networkVertexShader->isLoaded()) {
+        _vertexShaderSource = _networkVertexShader->_source;
+        _shaderDirty = true;
+    }
+
     if (_shaderDirty) {
         _proceduralPipelines.clear();
     }
@@ -276,25 +324,42 @@ void Procedural::prepare(gpu::Batch& batch,
 
         gpu::Shader::Source& fragmentSource = (key.isTransparent() && _transparentFragmentSource.valid()) ? _transparentFragmentSource : _opaqueFragmentSource;
 
-        // Build the fragment shader
+        // Build the fragment and vertex shaders
+        auto versionDefine = "#define PROCEDURAL_V" + std::to_string(_data.version);
         fragmentSource.replacements.clear();
-        fragmentSource.replacements[PROCEDURAL_VERSION] = "#define PROCEDURAL_V" + std::to_string(_data.version);
-        fragmentSource.replacements[PROCEDURAL_BLOCK] = _fragmentShaderSource.toStdString();
+        fragmentSource.replacements[PROCEDURAL_VERSION] = versionDefine;
+        if (!_fragmentShaderSource.isEmpty()) {
+            fragmentSource.replacements[PROCEDURAL_BLOCK] = _fragmentShaderSource.toStdString();
+        }
+        vertexSource.replacements.clear();
+        vertexSource.replacements[PROCEDURAL_VERSION] = versionDefine;
+        if (!_vertexShaderSource.isEmpty()) {
+            vertexSource.replacements[PROCEDURAL_BLOCK] = _vertexShaderSource.toStdString();
+        }
 
         // Set any userdata specified uniforms (if any)
         if (!_data.uniforms.empty()) {
-            // First grab all the possible dialect/variant/Reflections
-            std::vector<shader::Reflection*> allReflections;
+            // First grab all the possible dialect/variant/reflections
+            std::vector<shader::Reflection*> allFragmentReflections;
             for (auto dialectIt = fragmentSource.dialectSources.begin(); dialectIt != fragmentSource.dialectSources.end(); ++dialectIt) {
                 for (auto variantIt = (*dialectIt).second.variantSources.begin(); variantIt != (*dialectIt).second.variantSources.end(); ++variantIt) {
-                    allReflections.push_back(&(*variantIt).second.reflection);
+                    allFragmentReflections.push_back(&(*variantIt).second.reflection);
+                }
+            }
+            std::vector<shader::Reflection*> allVertexReflections;
+            for (auto dialectIt = vertexSource.dialectSources.begin(); dialectIt != vertexSource.dialectSources.end(); ++dialectIt) {
+                for (auto variantIt = (*dialectIt).second.variantSources.begin(); variantIt != (*dialectIt).second.variantSources.end(); ++variantIt) {
+                    allVertexReflections.push_back(&(*variantIt).second.reflection);
                 }
             }
             // Then fill in every reflections the new custom bindings
             int customSlot = procedural::slot::uniform::Custom;
             for (const auto& key : _data.uniforms.keys()) {
                 std::string uniformName = key.toLocal8Bit().data();
-                for (auto reflection : allReflections) {
+                for (auto reflection : allFragmentReflections) {
+                    reflection->uniforms[uniformName] = customSlot;
+                }
+                for (auto reflection : allVertexReflections) {
                     reflection->uniforms[uniformName] = customSlot;
                 }
                 ++customSlot;
@@ -303,6 +368,7 @@ void Procedural::prepare(gpu::Batch& batch,
 
         // Leave this here for debugging
         //qCDebug(proceduralLog) << "FragmentShader:\n" << fragmentSource.getSource(shader::Dialect::glsl450, shader::Variant::Mono).c_str();
+        //qCDebug(proceduralLog) << "VertexShader:\n" << vertexSource.getSource(shader::Dialect::glsl450, shader::Variant::Mono).c_str();
 
         gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(vertexSource);
         gpu::ShaderPointer fragmentShader = gpu::Shader::createPixel(fragmentSource);
@@ -453,6 +519,11 @@ glm::vec4 Procedural::getColor(const glm::vec4& entityColor) const {
     return entityColor;
 }
 
+bool Procedural::hasVertexShader() const {
+    std::lock_guard<std::mutex> lock(_mutex);
+    return !_data.vertexShaderUrl.isEmpty();
+}
+
 void graphics::ProceduralMaterial::initializeProcedural() {
     _procedural._vertexSource = gpu::Shader::getVertexShaderSource(shader::render_utils::vertex::simple_procedural);
     _procedural._vertexSourceSkinned = gpu::Shader::getVertexShaderSource(shader::render_utils::vertex::simple_procedural_deformed);
diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h
index aac353bf7c..89f21218e6 100644
--- a/libraries/procedural/src/procedural/Procedural.h
+++ b/libraries/procedural/src/procedural/Procedural.h
@@ -36,6 +36,8 @@ const size_t MAX_PROCEDURAL_TEXTURE_CHANNELS{ 4 };
  * The data used to define a Procedural shader material.
  * @typedef {object} ProceduralData
  * @property {number} version=1 - The version of the procedural shader.
+ * @property {string} vertexShaderURL - A link to a vertex shader.  Currently, only GLSL shaders are supported.  The shader must implement a different method depending on the version.
+ *     If a procedural material contains a vertex shader, the bounding box of the material entity is used to cull the object to which the material is applied.
  * @property {string} fragmentShaderURL - A link to a fragment shader.  Currently, only GLSL shaders are supported.  The shader must implement a different method depending on the version.
  *     <code>shaderUrl</code> is an alias.
  * @property {string[]} channels=[] - An array of input texture URLs.  Currently, up to 4 are supported.
@@ -50,6 +52,7 @@ struct ProceduralData {
     // Rendering object descriptions, from userData
     uint8_t version { 0 };
     QUrl fragmentShaderUrl;
+    QUrl vertexShaderUrl;
     QJsonObject uniforms;
     QJsonArray channels;
 };
@@ -110,6 +113,11 @@ public:
     void setIsFading(bool isFading) { _isFading = isFading; }
     void setDoesFade(bool doesFade) { _doesFade = doesFade; }
 
+    bool hasVertexShader() const;
+    void setBoundOperator(const std::function<AABox()>& boundOperator) { _boundOperator = boundOperator; }
+    bool hasBoundOperator() const { return (bool)_boundOperator; }
+    AABox getBound() { return _boundOperator(); }
+
     gpu::Shader::Source _vertexSource;
     gpu::Shader::Source _vertexSourceSkinned;
     gpu::Shader::Source _vertexSourceSkinnedDQ;
@@ -156,7 +164,11 @@ protected:
     uint64_t _firstCompile { 0 };
     int32_t _frameCount { 0 };
 
-    // Rendering object descriptions, from userData
+    // Rendering object descriptions
+    QString _vertexShaderSource;
+    QString _vertexShaderPath;
+    uint64_t _vertexShaderModified { 0 };
+    NetworkShaderPointer _networkVertexShader;
     QString _fragmentShaderSource;
     QString _fragmentShaderPath;
     uint64_t _fragmentShaderModified { 0 };
@@ -187,6 +199,9 @@ private:
     mutable bool _isFading { false };
     bool _doesFade { true };
     ProceduralProgramKey _prevKey;
+
+    std::function<AABox()> _boundOperator { nullptr };
+
     mutable std::mutex _mutex;
 };
 
@@ -210,6 +225,7 @@ public:
     bool isFading() const { return _procedural.isFading(); }
     void setIsFading(bool isFading) { _procedural.setIsFading(isFading); }
     uint64_t getFadeStartTime() const { return _procedural.getFadeStartTime(); }
+    bool hasVertexShader() const { return _procedural.hasVertexShader(); }
     void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation,
                  const uint64_t& created, const ProceduralProgramKey key = ProceduralProgramKey()) {
         _procedural.prepare(batch, position, size, orientation, created, key);
@@ -217,6 +233,10 @@ public:
 
     void initializeProcedural();
 
+    void setBoundOperator(const std::function<AABox()>& boundOperator) { _procedural.setBoundOperator(boundOperator); }
+    bool hasBoundOperator() const { return _procedural.hasBoundOperator(); }
+    AABox getBound() { return _procedural.getBound(); }
+
 private:
     QString _proceduralString;
     Procedural _procedural;
diff --git a/libraries/procedural/src/procedural/ProceduralCommon.slh b/libraries/procedural/src/procedural/ProceduralCommon.slh
index 2915f096e6..e2344dc14e 100644
--- a/libraries/procedural/src/procedural/ProceduralCommon.slh
+++ b/libraries/procedural/src/procedural/ProceduralCommon.slh
@@ -60,6 +60,17 @@ LAYOUT_STD140(binding=PROCEDURAL_BUFFER_INPUTS) uniform standardInputsBuffer {
 #define iChannelResolution standardInputs.channelResolution
 #define iWorldOrientation standardInputs.worldOrientation
 
+struct ProceduralVertexData {
+    vec4 position;
+    vec4 nonSkinnedPosition;    // input only
+    vec3 normal;
+    vec3 nonSkinnedNormal;      // input only
+    vec3 tangent;               // input only
+    vec3 nonSkinnedTangent;     // input only
+    vec4 color;
+    vec2 texCoord0;
+};
+
 struct ProceduralFragment {
     vec3 normal;
     vec3 diffuse;
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 997f87b8d6..ea66ac19ec 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 b474e6c712..179d49c076 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 6359c14896..e82af5395f 100644
--- a/libraries/render-utils/src/MeshPartPayload.cpp
+++ b/libraries/render-utils/src/MeshPartPayload.cpp
@@ -102,6 +102,10 @@ void MeshPartPayload::updateKey(const render::ItemKey& key) {
         builder.withTransparent();
     }
 
+    if (_cullWithParent) {
+        builder.withSubMetaCulled();
+    }
+
     _itemKey = builder.build();
 }
 
@@ -110,6 +114,13 @@ ItemKey MeshPartPayload::getKey() const {
 }
 
 Item::Bound MeshPartPayload::getBound() const {
+    graphics::MaterialPointer material = _drawMaterials.empty() ? nullptr : _drawMaterials.top().material;
+    if (material && material->isProcedural() && material->isReady()) {
+        auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(_drawMaterials.top().material);
+        if (procedural->hasVertexShader() && procedural->hasBoundOperator()) {
+           return procedural->getBound();
+        }
+    }
     return _worldBound;
 }
 
@@ -136,6 +147,9 @@ ShapeKey MeshPartPayload::getShapeKey() const {
         if (drawMaterialKey.isUnlit()) {
             builder.withUnlit();
         }
+        if (material) {
+            builder.withCullFaceMode(material->getCullFaceMode());
+        }
     }
 
     return builder.build();
@@ -175,6 +189,9 @@ void MeshPartPayload::render(RenderArgs* args) {
 
     if (!_drawMaterials.empty() && _drawMaterials.top().material && _drawMaterials.top().material->isProcedural() &&
             _drawMaterials.top().material->isReady()) {
+        if (!enableMaterialProceduralShaders) {
+            return;
+        }
         auto procedural = std::static_pointer_cast<graphics::ProceduralMaterial>(_drawMaterials.top().material);
         auto& schema = _drawMaterials.getSchemaBuffer().get<graphics::MultiMaterial::Schema>();
         glm::vec4 outColor = glm::vec4(ColorUtils::tosRGBVec3(schema._albedo), schema._opacity);
@@ -370,6 +387,10 @@ void ModelMeshPartPayload::updateKey(const render::ItemKey& key) {
         builder.withTransparent();
     }
 
+    if (_cullWithParent) {
+        builder.withSubMetaCulled();
+    }
+
     _itemKey = builder.build();
 }
 
@@ -424,6 +445,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/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h
index 4d9510e4a2..ee205bd778 100644
--- a/libraries/render-utils/src/MeshPartPayload.h
+++ b/libraries/render-utils/src/MeshPartPayload.h
@@ -43,7 +43,7 @@ public:
     // Render Item interface
     virtual render::ItemKey getKey() const;
     virtual render::Item::Bound getBound() const;
-    virtual render::ShapeKey getShapeKey() const; // shape interface
+    virtual render::ShapeKey getShapeKey() const;
     virtual void render(RenderArgs* args);
 
     // ModelMeshPartPayload functions to perform render
@@ -73,10 +73,13 @@ public:
     void addMaterial(graphics::MaterialLayer material);
     void removeMaterial(graphics::MaterialPointer material);
 
+    void setCullWithParent(bool value) { _cullWithParent = value; }
+
     static bool enableMaterialProceduralShaders;
 
 protected:
     render::ItemKey _itemKey{ render::ItemKey::Builder::opaqueShape().build() };
+    bool _cullWithParent { false };
     uint64_t _created;
 };
 
@@ -106,7 +109,7 @@ public:
     void updateTransformForSkinnedMesh(const Transform& renderTransform, const Transform& boundTransform);
 
     // Render Item interface
-    render::ShapeKey getShapeKey() const override; // shape interface
+    render::ShapeKey getShapeKey() const override;
     void render(RenderArgs* args) override;
 
     void setShapeKey(bool invalidateShapeKey, PrimitiveMode primitiveMode, bool useDualQuaternionSkinning);
diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp
index 3a2e450d4b..d8172112ff 100644
--- a/libraries/render-utils/src/Model.cpp
+++ b/libraries/render-utils/src/Model.cpp
@@ -154,7 +154,7 @@ void Model::setOffset(const glm::vec3& offset) {
 }
 
 void Model::calculateTextureInfo() {
-    if (!_hasCalculatedTextureInfo && isLoaded() && getGeometry()->areTexturesLoaded() && !_modelMeshRenderItemsMap.isEmpty()) {
+    if (!_hasCalculatedTextureInfo && isLoaded() && getGeometry()->areTexturesLoaded() && !_modelMeshRenderItems.isEmpty()) {
         size_t textureSize = 0;
         int textureCount = 0;
         bool allTexturesLoaded = true;
@@ -960,6 +960,22 @@ void Model::setCauterized(bool cauterized, const render::ScenePointer& scene) {
     }
 }
 
+void Model::setCullWithParent(bool cullWithParent) {
+    if (_cullWithParent != cullWithParent) {
+        _cullWithParent = cullWithParent;
+
+        render::Transaction transaction;
+        auto renderItemsKey = _renderItemKeyGlobalFlags;
+        for(auto item : _modelMeshRenderItemIDs) {
+            transaction.updateItem<ModelMeshPartPayload>(item, [cullWithParent, renderItemsKey](ModelMeshPartPayload& data) {
+                data.setCullWithParent(cullWithParent);
+                data.updateKey(renderItemsKey);
+            });
+        }
+        AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction);
+    }
+}
+
 const render::ItemKey Model::getRenderItemKeyGlobalFlags() const {
     return _renderItemKeyGlobalFlags;
 }
@@ -1040,7 +1056,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/Model.h b/libraries/render-utils/src/Model.h
index 597fe552f7..b677861e9f 100644
--- a/libraries/render-utils/src/Model.h
+++ b/libraries/render-utils/src/Model.h
@@ -129,6 +129,8 @@ public:
     bool isCauterized() const { return _cauterized; }
     void setCauterized(bool value, const render::ScenePointer& scene);
 
+    void setCullWithParent(bool value);
+
     // Access the current RenderItemKey Global Flags used by the model and applied to the render items  representing the parts of the model.
     const render::ItemKey getRenderItemKeyGlobalFlags() const;
 
@@ -503,6 +505,7 @@ protected:
     //  
     render::ItemKey _renderItemKeyGlobalFlags;
     bool _cauterized { false };
+    bool _cullWithParent { false };
 
     bool shouldInvalidatePayloadShapeKey(int meshIndex);
 
diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp
index 2b0d197819..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);
     }
 }
 
@@ -387,8 +385,10 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial
     std::call_once(once, [] {
         for (int i = 0; i < graphics::Material::NUM_TOTAL_FLAGS; i++) {
             // The opacity mask/map are derived from the albedo map
+            // FIXME: OPACITY_MAP_MODE_BIT is supposed to support fallthrough
             if (i != graphics::MaterialKey::OPACITY_MASK_MAP_BIT &&
-                    i != graphics::MaterialKey::OPACITY_TRANSLUCENT_MAP_BIT) {
+                    i != graphics::MaterialKey::OPACITY_TRANSLUCENT_MAP_BIT &&
+                    i != graphics::MaterialKey::OPACITY_MAP_MODE_BIT) {
                 allFlags.insert(i);
             }
         }
@@ -642,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;
             }
@@ -683,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-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp
index 60e11f5301..fe005df2d4 100755
--- a/libraries/render-utils/src/RenderShadowTask.cpp
+++ b/libraries/render-utils/src/RenderShadowTask.cpp
@@ -61,12 +61,12 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende
     const auto currentKeyLight = setupOutput.getN<RenderShadowSetup::Output>(4);
     // Fetch and cull the items from the scene
 
-    static const auto shadowCasterReceiverFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(tagBits, tagMask);
+    static const auto shadowCasterReceiverFilter = ItemFilter::Builder::visibleWorldItems().withOpaque().withoutLayered().withTagBits(tagBits, tagMask);
 
     const auto fetchInput = FetchSpatialTree::Inputs(shadowCasterReceiverFilter, queryResolution).asVarying();
     const auto shadowSelection = task.addJob<FetchSpatialTree>("FetchShadowTree", fetchInput);
-    const auto selectionInputs = FilterSpatialSelection::Inputs(shadowSelection, shadowCasterReceiverFilter).asVarying();
-    const auto shadowItems = task.addJob<FilterSpatialSelection>("FilterShadowSelection", selectionInputs);
+    const auto selectionInputs = CullSpatialSelection::Inputs(shadowSelection, shadowCasterReceiverFilter).asVarying();
+    const auto shadowItems = task.addJob<CullSpatialSelection>("FilterShadowSelection", selectionInputs, nullptr, true, RenderDetails::SHADOW);
 
     // Cull objects that are not visible in camera view. Hopefully the cull functor only performs LOD culling, not
     // frustum culling or this will make shadow casters out of the camera frustum disappear.
diff --git a/libraries/render-utils/src/model.slf b/libraries/render-utils/src/model.slf
index bacc6b0ab1..a6cc82e335 100644
--- a/libraries/render-utils/src/model.slf
+++ b/libraries/render-utils/src/model.slf
@@ -110,7 +110,7 @@ void main(void) {
     <@else@>
         float cutoff = getMaterialOpacityCutoff(mat);
         float opacity = 1.0;
-        <$evalMaterialOpacityMask(albedoTex.a, cutoff, opacity)$>;
+        <$evalMaterialOpacityMask(albedoTex.a, cutoff, opacity, matKey, opacity)$>;
         <$discardTransparent(opacity)$>;
     <@endif@>
 
@@ -162,7 +162,7 @@ void main(void) {
     <@else@>
         float cutoff = getMaterialOpacityCutoff(mat);
         float opacity = 1.0;
-        <$evalMaterialOpacityMask(albedoTex.a, cutoff, opacity)$>;
+        <$evalMaterialOpacityMask(albedoTex.a, cutoff, opacity, matKey, opacity)$>;
         <$discardTransparent(opacity)$>;
     <@endif@>
 
diff --git a/libraries/render-utils/src/simple_procedural.slv b/libraries/render-utils/src/simple_procedural.slv
index a8d494f72d..70bce451d3 100644
--- a/libraries/render-utils/src/simple_procedural.slv
+++ b/libraries/render-utils/src/simple_procedural.slv
@@ -20,9 +20,9 @@
 <@if HIFI_USE_DEFORMED or HIFI_USE_DEFORMEDDQ@>
     <@include MeshDeformer.slh@>
     <@if HIFI_USE_DEFORMED@>
-        <$declareMeshDeformer(1, _SCRIBE_NULL, 1, _SCRIBE_NULL, 1)$>
+        <$declareMeshDeformer(1, 1, 1, _SCRIBE_NULL, 1)$>
     <@else@>
-        <$declareMeshDeformer(1, _SCRIBE_NULL, 1, 1, 1)$>
+        <$declareMeshDeformer(1, 1, 1, 1, 1)$>
     <@endif@>
     <$declareMeshDeformerActivation(1, 1)$>
 <@endif@>
@@ -34,24 +34,56 @@ layout(location=RENDER_UTILS_ATTR_NORMAL_WS) out vec3 _normalWS;
 layout(location=RENDER_UTILS_ATTR_COLOR) out vec4 _color;
 layout(location=RENDER_UTILS_ATTR_TEXCOORD01) out vec4 _texCoord01;
 
+<@include procedural/ProceduralCommon.slh@>
+
+#line 1001
+//PROCEDURAL_BLOCK_BEGIN
+
+void getProceduralVertex(inout ProceduralVertexData proceduralData) {}
+
+//PROCEDURAL_BLOCK_END
+
+#line 2030
 void main(void) {
     vec4 positionMS = inPosition;
     vec3 normalMS = inNormal.xyz;
+    vec3 tangentMS = inTangent.xyz;
+    vec4 color = color_sRGBAToLinear(inColor);
+    vec2 texCoord0 = inTexCoord0.st;
 
 <@if HIFI_USE_DEFORMED or HIFI_USE_DEFORMEDDQ@>
-        evalMeshDeformer(inPosition, positionMS, inNormal.xyz, normalMS,
+        evalMeshDeformer(inPosition, positionMS, inNormal.xyz, normalMS, inTangent.xyz, tangentMS,
                          meshDeformer_doSkinning(_drawCallInfo.y), inSkinClusterIndex, inSkinClusterWeight,
                          meshDeformer_doBlendshape(_drawCallInfo.y), gl_VertexID);
 <@endif@>
 
+#if defined(PROCEDURAL_V1) || defined(PROCEDURAL_V2) || defined(PROCEDURAL_V3)
+    ProceduralVertexData proceduralData = ProceduralVertexData(
+        positionMS,
+        inPosition,
+        normalMS,
+        inNormal.xyz,
+        tangentMS,
+        inTangent.xyz,
+        color,
+        texCoord0
+    );
+
+    getProceduralVertex(proceduralData);
+
+    positionMS = proceduralData.position;
+    normalMS = proceduralData.normal;
+    color = proceduralData.color;
+    texCoord0 = proceduralData.texCoord0;
+#endif
+
     _positionMS = positionMS;
     _normalMS = normalMS;
+    _color = color;
+    _texCoord01 = vec4(texCoord0, 0.0, 0.0);
 
     TransformCamera cam = getTransformCamera();
     TransformObject obj = getTransformObject();
     <$transformModelToEyeAndClipPos(cam, obj, positionMS, _positionES, gl_Position)$>
     <$transformModelToWorldDir(cam, obj, normalMS, _normalWS)$>
-
-    _color = color_sRGBAToLinear(inColor);
-    _texCoord01 = vec4(inTexCoord0.st, 0.0, 0.0);
 }
\ No newline at end of file
diff --git a/libraries/render/src/render/CullTask.cpp b/libraries/render/src/render/CullTask.cpp
index 5e5c6b4c6e..068d6f9851 100644
--- a/libraries/render/src/render/CullTask.cpp
+++ b/libraries/render/src/render/CullTask.cpp
@@ -172,7 +172,7 @@ void FetchSpatialTree::run(const RenderContextPointer& renderContext, const Inpu
 void CullSpatialSelection::configure(const Config& config) {
     _justFrozeFrustum = _justFrozeFrustum || (config.freezeFrustum && !_freezeFrustum);
     _freezeFrustum = config.freezeFrustum;
-    _skipCulling = config.skipCulling;
+    _overrideSkipCulling = config.skipCulling;
 }
 
 void CullSpatialSelection::run(const RenderContextPointer& renderContext,
@@ -209,7 +209,7 @@ void CullSpatialSelection::run(const RenderContextPointer& renderContext,
         // filter individually against the _filter
         // visibility cull if partially selected ( octree cell contianing it was partial)
         // distance cull if was a subcell item ( octree cell is way bigger than the item bound itself, so now need to test per item)
-        if (_skipCulling) {
+        if (_skipCulling || _overrideSkipCulling) {
             // inside & fit items: filter only, culling is disabled
             {
                 PerformanceTimer perfTimer("insideFitItems");
@@ -444,69 +444,3 @@ void ApplyCullFunctorOnItemBounds::run(const RenderContextPointer& renderContext
         args->popViewFrustum();
     }
 }
-
-void FilterSpatialSelection::run(const RenderContextPointer& renderContext,
-                               const Inputs& inputs, ItemBounds& outItems) {
-    assert(renderContext->args);
-    auto& scene = renderContext->_scene;
-    auto& inSelection = inputs.get0();
-
-    // Now we have a selection of items to render
-    outItems.clear();
-    outItems.reserve(inSelection.numItems());
-
-    const auto filter = inputs.get1();
-    if (!filter.selectsNothing()) {
-        // Now get the bound, and
-        // filter individually against the _filter
-
-        // inside & fit items: filter only
-        {
-            PerformanceTimer perfTimer("insideFitItems");
-            for (auto id : inSelection.insideItems) {
-                auto& item = scene->getItem(id);
-                if (filter.test(item.getKey())) {
-                    ItemBound itemBound(id, item.getBound());
-                    outItems.emplace_back(itemBound);
-                }
-            }
-        }
-
-        // inside & subcell items: filter only
-        {
-            PerformanceTimer perfTimer("insideSmallItems");
-            for (auto id : inSelection.insideSubcellItems) {
-                auto& item = scene->getItem(id);
-                if (filter.test(item.getKey())) {
-                    ItemBound itemBound(id, item.getBound());
-                    outItems.emplace_back(itemBound);
-
-                }
-            }
-        }
-
-        // partial & fit items: filter only
-        {
-            PerformanceTimer perfTimer("partialFitItems");
-            for (auto id : inSelection.partialItems) {
-                auto& item = scene->getItem(id);
-                if (filter.test(item.getKey())) {
-                    ItemBound itemBound(id, item.getBound());
-                    outItems.emplace_back(itemBound);
-                }
-            }
-        }
-
-        // partial & subcell items: filter only
-        {
-            PerformanceTimer perfTimer("partialSmallItems");
-            for (auto id : inSelection.partialSubcellItems) {
-                auto& item = scene->getItem(id);
-                if (filter.test(item.getKey())) {
-                    ItemBound itemBound(id, item.getBound());
-                    outItems.emplace_back(itemBound);
-                }
-            }
-        }
-    }
-}
diff --git a/libraries/render/src/render/CullTask.h b/libraries/render/src/render/CullTask.h
index 99ca7abe6c..e67edd6666 100644
--- a/libraries/render/src/render/CullTask.h
+++ b/libraries/render/src/render/CullTask.h
@@ -100,26 +100,25 @@ namespace render {
     };
 
     class CullSpatialSelection {
-        bool _freezeFrustum{ false }; // initialized by Config
-        bool _justFrozeFrustum{ false };
-        bool _skipCulling{ false };
-        ViewFrustum _frozenFrustum;
     public:
         using Config = CullSpatialSelectionConfig;
         using Inputs = render::VaryingSet2<ItemSpatialTree::ItemSelection, ItemFilter>;
         using JobModel = Job::ModelIO<CullSpatialSelection, Inputs, ItemBounds, Config>;
 
-        CullSpatialSelection(CullFunctor cullFunctor, RenderDetails::Type type) :
-            _cullFunctor{ cullFunctor },
+        CullSpatialSelection(CullFunctor cullFunctor, bool skipCulling, RenderDetails::Type type) :
+            _cullFunctor(cullFunctor),
+            _skipCulling(skipCulling),
             _detailType(type) {}
 
-        CullSpatialSelection(CullFunctor cullFunctor) :
-            _cullFunctor{ cullFunctor } {
-        }
-
         CullFunctor _cullFunctor;
+        bool _skipCulling { false };
         RenderDetails::Type _detailType{ RenderDetails::OTHER };
 
+        bool _freezeFrustum { false }; // initialized by Config
+        bool _justFrozeFrustum { false };
+        bool _overrideSkipCulling { false };
+        ViewFrustum _frozenFrustum;
+
         void configure(const Config& config);
         void run(const RenderContextPointer& renderContext, const Inputs& inputs, ItemBounds& outItems);
     };
@@ -147,15 +146,6 @@ namespace render {
 
     };
 
-    class FilterSpatialSelection {
-    public:
-        using Inputs = render::VaryingSet2<ItemSpatialTree::ItemSelection, ItemFilter>;
-        using JobModel = Job::ModelIO<FilterSpatialSelection, Inputs, ItemBounds>;
-
-        FilterSpatialSelection() {}
-        void run(const RenderContextPointer& renderContext, const Inputs& inputs, ItemBounds& outItems);
-    };
-
     class ApplyCullFunctorOnItemBounds {
     public:
         using Inputs = render::VaryingSet2<ItemBounds, ViewFrustumPointer>;
diff --git a/libraries/render/src/render/Item.h b/libraries/render/src/render/Item.h
index 6b66e844c0..3383101b5b 100644
--- a/libraries/render/src/render/Item.h
+++ b/libraries/render/src/render/Item.h
@@ -625,7 +625,7 @@ template <> const ShapeKey shapeGetShapeKey(const PayloadProxyInterface::Pointer
 
 
 typedef Item::PayloadPointer PayloadPointer;
-typedef std::vector< PayloadPointer > Payloads;
+typedef std::vector<PayloadPointer> Payloads;
 
 // A map of items by ShapeKey to optimize rendering pipeline assignments
 using ShapeBounds = std::unordered_map<ShapeKey, ItemBounds, ShapeKey::Hash, ShapeKey::KeyEqual>;
diff --git a/libraries/render/src/render/RenderFetchCullSortTask.cpp b/libraries/render/src/render/RenderFetchCullSortTask.cpp
index 6b1a57ed88..ebcf5a432b 100644
--- a/libraries/render/src/render/RenderFetchCullSortTask.cpp
+++ b/libraries/render/src/render/RenderFetchCullSortTask.cpp
@@ -27,7 +27,7 @@ void RenderFetchCullSortTask::build(JobModel& task, const Varying& input, Varyin
     const auto fetchInput = FetchSpatialTree::Inputs(filter, glm::ivec2(0,0)).asVarying();
     const auto spatialSelection = task.addJob<FetchSpatialTree>("FetchSceneSelection", fetchInput);
     const auto cullInputs = CullSpatialSelection::Inputs(spatialSelection, spatialFilter).asVarying();
-    const auto culledSpatialSelection = task.addJob<CullSpatialSelection>("CullSceneSelection", cullInputs, cullFunctor, RenderDetails::ITEM);
+    const auto culledSpatialSelection = task.addJob<CullSpatialSelection>("CullSceneSelection", cullInputs, cullFunctor, false, RenderDetails::ITEM);
 
     // Layered objects are not culled
     const ItemFilter layeredFilter = ItemFilter::Builder::visibleWorldItems().withTagBits(tagBits, tagMask);
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]; }