#include "lightmanager.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr int ffpMaxLights = 8; void configurePosition(osg::Matrixf& mat, const osg::Vec4& pos) { mat(0, 0) = pos.x(); mat(0, 1) = pos.y(); mat(0, 2) = pos.z(); } void configureAmbient(osg::Matrixf& mat, const osg::Vec4& color) { mat(1, 0) = color.r(); mat(1, 1) = color.g(); mat(1, 2) = color.b(); } void configureDiffuse(osg::Matrixf& mat, const osg::Vec4& color) { mat(2, 0) = color.r(); mat(2, 1) = color.g(); mat(2, 2) = color.b(); } void configureSpecular(osg::Matrixf& mat, const osg::Vec4& color) { mat(3, 0) = color.r(); mat(3, 1) = color.g(); mat(3, 2) = color.b(); mat(3, 3) = color.a(); } void configureAttenuation(osg::Matrixf& mat, float c, float l, float q, float r) { mat(0, 3) = c; mat(1, 3) = l; mat(2, 3) = q; mat(3, 3) = r; } } namespace SceneUtil { namespace { const std::unordered_map lightingMethodSettingMap = { { "legacy", LightingMethod::FFP }, { "shaders compatibility", LightingMethod::PerObjectUniform }, { "shaders", LightingMethod::SingleUBO }, }; } static int sLightId = 0; // Handles a GLSL shared layout by using configured offsets and strides to fill a continuous buffer, making the data // upload to GPU simpler. class LightBuffer : public osg::Referenced { public: enum LayoutOffset { Diffuse, DiffuseSign, Ambient, Specular, Position, AttenuationRadius }; LightBuffer(int count) : mData(new osg::FloatArray(3 * 4 * count)) , mEndian(osg::getCpuByteOrder()) , mCount(count) , mCachedSunPos(osg::Vec4()) { } LightBuffer(const LightBuffer&) = delete; void setDiffuse(int index, const osg::Vec4& value) { // Deal with negative lights (negative diffuse) by passing a sign bit in the unused alpha component auto positiveColor = value; unsigned int signBit = 1; if (value[0] < 0) { positiveColor *= -1.0; signBit = ~0u; } unsigned int packedColor = asRGBA(positiveColor); std::memcpy(&(*mData)[getOffset(index, Diffuse)], &packedColor, sizeof(unsigned int)); std::memcpy(&(*mData)[getOffset(index, DiffuseSign)], &signBit, sizeof(unsigned int)); } void setAmbient(int index, const osg::Vec4& value) { unsigned int packed = asRGBA(value); std::memcpy(&(*mData)[getOffset(index, Ambient)], &packed, sizeof(unsigned int)); } void setSpecular(int index, const osg::Vec4& value) { unsigned int packed = asRGBA(value); std::memcpy(&(*mData)[getOffset(index, Specular)], &packed, sizeof(unsigned int)); } void setPosition(int index, const osg::Vec4& value) { std::memcpy(&(*mData)[getOffset(index, Position)], value.ptr(), sizeof(osg::Vec4f)); } void setAttenuationRadius(int index, const osg::Vec4& value) { std::memcpy(&(*mData)[getOffset(index, AttenuationRadius)], value.ptr(), sizeof(osg::Vec4f)); } auto& getData() { return mData; } void dirty() { mData->dirty(); } static constexpr int queryBlockSize(int sz) { return 3 * osg::Vec4::num_components * sizeof(GLfloat) * sz; } void setCachedSunPos(const osg::Vec4& pos) { mCachedSunPos = pos; } void uploadCachedSunPos(const osg::Matrix& viewMat) { osg::Vec4 viewPos = mCachedSunPos * viewMat; std::memcpy(&(*mData)[getOffset(0, Position)], viewPos.ptr(), sizeof(osg::Vec4f)); } unsigned int asRGBA(const osg::Vec4& value) const { return mEndian == osg::BigEndian ? value.asABGR() : value.asRGBA(); } int getOffset(int index, LayoutOffset slot) const { return mOffsets.get(index, slot); } void configureLayout(int offsetColors, int offsetPosition, int offsetAttenuationRadius, int size, int stride) { configureLayout(Offsets(offsetColors, offsetPosition, offsetAttenuationRadius, stride), size); } void configureLayout(const LightBuffer* other) { mOffsets = other->mOffsets; int size = other->mData->size(); configureLayout(mOffsets, size); } private: class Offsets { public: Offsets() : mStride(12) { mValues[Diffuse] = 0; mValues[Ambient] = 1; mValues[Specular] = 2; mValues[DiffuseSign] = 3; mValues[Position] = 4; mValues[AttenuationRadius] = 8; } Offsets(int offsetColors, int offsetPosition, int offsetAttenuationRadius, int stride) : mStride((offsetAttenuationRadius + sizeof(GLfloat) * osg::Vec4::num_components + stride) / 4) { constexpr auto sizeofFloat = sizeof(GLfloat); const auto diffuseOffset = offsetColors / sizeofFloat; mValues[Diffuse] = diffuseOffset; mValues[Ambient] = diffuseOffset + 1; mValues[Specular] = diffuseOffset + 2; mValues[DiffuseSign] = diffuseOffset + 3; mValues[Position] = offsetPosition / sizeofFloat; mValues[AttenuationRadius] = offsetAttenuationRadius / sizeofFloat; } int get(int index, LayoutOffset slot) const { return mStride * index + mValues[slot]; } private: int mStride; std::array mValues; }; void configureLayout(const Offsets& offsets, int size) { // Copy cloned data using current layout into current data using new layout. // This allows to preserve osg::FloatArray buffer object in mData. const auto data = mData->asVector(); mData->resizeArray(static_cast(size)); for (int i = 0; i < mCount; ++i) { std::memcpy( &(*mData)[offsets.get(i, Diffuse)], data.data() + getOffset(i, Diffuse), sizeof(osg::Vec4f)); std::memcpy( &(*mData)[offsets.get(i, Position)], data.data() + getOffset(i, Position), sizeof(osg::Vec4f)); std::memcpy(&(*mData)[offsets.get(i, AttenuationRadius)], data.data() + getOffset(i, AttenuationRadius), sizeof(osg::Vec4f)); } mOffsets = offsets; } osg::ref_ptr mData; osg::Endian mEndian; int mCount; Offsets mOffsets; osg::Vec4 mCachedSunPos; }; struct LightStateCache { std::vector lastAppliedLight; }; LightStateCache* getLightStateCache(size_t contextid, size_t size = 8) { static std::vector cacheVector; if (cacheVector.size() < contextid + 1) cacheVector.resize(contextid + 1); cacheVector[contextid].lastAppliedLight.resize(size); return &cacheVector[contextid]; } void configureStateSetSunOverride( LightManager* lightManager, const osg::Light* light, osg::StateSet* stateset, int mode) { auto method = lightManager->getLightingMethod(); switch (method) { case LightingMethod::FFP: { break; } case LightingMethod::PerObjectUniform: { osg::Matrixf lightMat; configurePosition(lightMat, light->getPosition()); configureAmbient(lightMat, light->getAmbient()); configureDiffuse(lightMat, light->getDiffuse()); configureSpecular(lightMat, light->getSpecular()); stateset->addUniform(lightManager->generateLightBufferUniform(lightMat), mode); break; } case LightingMethod::SingleUBO: { osg::ref_ptr buffer = new LightBuffer(lightManager->getMaxLightsInScene()); buffer->setDiffuse(0, light->getDiffuse()); buffer->setAmbient(0, light->getAmbient()); buffer->setSpecular(0, light->getSpecular()); buffer->setPosition(0, light->getPosition()); osg::ref_ptr ubo = new osg::UniformBufferObject; buffer->getData()->setBufferObject(ubo); osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), buffer->getData(), 0, buffer->getData()->getTotalDataSize()); stateset->setAttributeAndModes(ubb, mode); break; } } } class DisableLight : public osg::StateAttribute { public: DisableLight() : mIndex(0) { } DisableLight(int index) : mIndex(index) { } DisableLight(const DisableLight& copy, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY) : osg::StateAttribute(copy, copyop) , mIndex(copy.mIndex) { } META_StateAttribute(SceneUtil, DisableLight, osg::StateAttribute::LIGHT) unsigned int getMember() const override { return mIndex; } bool getModeUsage(ModeUsage& usage) const override { usage.usesMode(GL_LIGHT0 + mIndex); return true; } int compare(const StateAttribute& sa) const override { throw std::runtime_error("DisableLight::compare: unimplemented"); } void apply(osg::State& state) const override { int lightNum = GL_LIGHT0 + mIndex; glLightfv(lightNum, GL_AMBIENT, mNullptr.ptr()); glLightfv(lightNum, GL_DIFFUSE, mNullptr.ptr()); glLightfv(lightNum, GL_SPECULAR, mNullptr.ptr()); LightStateCache* cache = getLightStateCache(state.getContextID()); cache->lastAppliedLight[mIndex] = nullptr; } private: size_t mIndex; osg::Vec4f mNullptr; }; class FFPLightStateAttribute : public osg::StateAttribute { public: FFPLightStateAttribute() : mIndex(0) { } FFPLightStateAttribute(size_t index, const std::vector>& lights) : mIndex(index) , mLights(lights) { } FFPLightStateAttribute( const FFPLightStateAttribute& copy, const osg::CopyOp& copyop = osg::CopyOp::SHALLOW_COPY) : osg::StateAttribute(copy, copyop) , mIndex(copy.mIndex) , mLights(copy.mLights) { } unsigned int getMember() const override { return mIndex; } bool getModeUsage(ModeUsage& usage) const override { for (size_t i = 0; i < mLights.size(); ++i) usage.usesMode(GL_LIGHT0 + mIndex + i); return true; } int compare(const StateAttribute& sa) const override { throw std::runtime_error("FFPLightStateAttribute::compare: unimplemented"); } META_StateAttribute(SceneUtil, FFPLightStateAttribute, osg::StateAttribute::LIGHT) void apply(osg::State& state) const override { if (mLights.empty()) return; osg::Matrix modelViewMatrix = state.getModelViewMatrix(); state.applyModelViewMatrix(state.getInitialViewMatrix()); LightStateCache* cache = getLightStateCache(state.getContextID()); for (size_t i = 0; i < mLights.size(); ++i) { osg::Light* current = cache->lastAppliedLight[i + mIndex]; if (current != mLights[i].get()) { applyLight((GLenum)((int)GL_LIGHT0 + i + mIndex), mLights[i].get()); cache->lastAppliedLight[i + mIndex] = mLights[i].get(); } } state.applyModelViewMatrix(modelViewMatrix); } void applyLight(GLenum lightNum, const osg::Light* light) const { glLightfv(lightNum, GL_AMBIENT, light->getAmbient().ptr()); glLightfv(lightNum, GL_DIFFUSE, light->getDiffuse().ptr()); glLightfv(lightNum, GL_SPECULAR, light->getSpecular().ptr()); glLightfv(lightNum, GL_POSITION, light->getPosition().ptr()); // TODO: enable this once spot lights are supported // need to transform SPOT_DIRECTION by the world matrix? // glLightfv(lightNum, GL_SPOT_DIRECTION, light->getDirection().ptr()); // glLightf(lightNum, GL_SPOT_EXPONENT, light->getSpotExponent()); // glLightf(lightNum, GL_SPOT_CUTOFF, light->getSpotCutoff()); glLightf(lightNum, GL_CONSTANT_ATTENUATION, light->getConstantAttenuation()); glLightf(lightNum, GL_LINEAR_ATTENUATION, light->getLinearAttenuation()); glLightf(lightNum, GL_QUADRATIC_ATTENUATION, light->getQuadraticAttenuation()); } private: size_t mIndex; std::vector> mLights; }; struct StateSetGenerator { LightManager* mLightManager; virtual ~StateSetGenerator() {} virtual osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) = 0; virtual void update(osg::StateSet* stateset, const LightManager::LightList& lightList, size_t frameNum) {} osg::Matrix mViewMatrix; }; struct StateSetGeneratorFFP : StateSetGenerator { osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) override { osg::ref_ptr stateset = new osg::StateSet; std::vector> lights; lights.reserve(lightList.size()); for (size_t i = 0; i < lightList.size(); ++i) lights.emplace_back(lightList[i]->mLightSource->getLight(frameNum)); // the first light state attribute handles the actual state setting for all lights // it's best to batch these up so that we don't need to touch the modelView matrix more than necessary // don't use setAttributeAndModes, that does not support light indices! stateset->setAttribute( new FFPLightStateAttribute(mLightManager->getStartLight(), std::move(lights)), osg::StateAttribute::ON); for (size_t i = 0; i < lightList.size(); ++i) stateset->setMode(GL_LIGHT0 + mLightManager->getStartLight() + i, osg::StateAttribute::ON); // need to push some dummy attributes to ensure proper state tracking // lights need to reset to their default when the StateSet is popped for (size_t i = 1; i < lightList.size(); ++i) stateset->setAttribute( mLightManager->getDummies()[i + mLightManager->getStartLight()].get(), osg::StateAttribute::ON); return stateset; } }; struct StateSetGeneratorSingleUBO : StateSetGenerator { osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) override { osg::ref_ptr stateset = new osg::StateSet; osg::ref_ptr indicesUni = new osg::Uniform(osg::Uniform::Type::INT, "PointLightIndex", mLightManager->getMaxLights()); int pointCount = 0; for (size_t i = 0; i < lightList.size(); ++i) { int bufIndex = mLightManager->getLightIndexMap(frameNum)[lightList[i]->mLightSource->getId()]; indicesUni->setElement(pointCount++, bufIndex); } stateset->addUniform(indicesUni); stateset->addUniform(new osg::Uniform("PointLightCount", pointCount)); return stateset; } // Cached statesets must be revalidated in case the light indices change. There is no actual link between // a light's ID and the buffer index it will eventually be assigned (or reassigned) to. void update(osg::StateSet* stateset, const LightManager::LightList& lightList, size_t frameNum) override { int newCount = 0; int oldCount; auto uOldArray = stateset->getUniform("PointLightIndex"); auto uOldCount = stateset->getUniform("PointLightCount"); uOldCount->get(oldCount); // max lights count can change during runtime oldCount = std::min(mLightManager->getMaxLights(), oldCount); auto& lightData = mLightManager->getLightIndexMap(frameNum); for (int i = 0; i < oldCount; ++i) { auto* lightSource = lightList[i]->mLightSource; auto it = lightData.find(lightSource->getId()); if (it != lightData.end()) uOldArray->setElement(newCount++, it->second); } uOldArray->dirty(); uOldCount->set(newCount); } }; struct StateSetGeneratorPerObjectUniform : StateSetGenerator { osg::ref_ptr generate(const LightManager::LightList& lightList, size_t frameNum) override { osg::ref_ptr stateset = new osg::StateSet; osg::ref_ptr data = mLightManager->generateLightBufferUniform(mLightManager->getSunlightBuffer(frameNum)); for (size_t i = 0; i < lightList.size(); ++i) { auto* light = lightList[i]->mLightSource->getLight(frameNum); osg::Matrixf lightMat; configurePosition(lightMat, light->getPosition() * mViewMatrix); configureAmbient(lightMat, light->getAmbient()); configureDiffuse(lightMat, light->getDiffuse()); configureSpecular(lightMat, light->getSpecular()); configureAttenuation(lightMat, light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), lightList[i]->mLightSource->getRadius()); data->setElement(i + 1, lightMat); } stateset->addUniform(data); stateset->addUniform(new osg::Uniform("PointLightCount", static_cast(lightList.size() + 1))); return stateset; } }; LightManager* findLightManager(const osg::NodePath& path) { for (size_t i = 0; i < path.size(); ++i) { if (LightManager* lightManager = dynamic_cast(path[i])) return lightManager; } return nullptr; } // Set on a LightSource. Adds the light source to its light manager for the current frame. // This allows us to keep track of the current lights in the scene graph without tying creation & destruction to the // manager. class CollectLightCallback : public NodeCallback { public: CollectLightCallback() : mLightManager(nullptr) { } CollectLightCallback(const CollectLightCallback& copy, const osg::CopyOp& copyop) : NodeCallback(copy, copyop) , mLightManager(nullptr) { } META_Object(SceneUtil, CollectLightCallback) void operator()(osg::Node* node, osg::NodeVisitor* nv) { if (!mLightManager) { mLightManager = findLightManager(nv->getNodePath()); if (!mLightManager) throw std::runtime_error("can't find parent LightManager"); } mLightManager->addLight( static_cast(node), osg::computeLocalToWorld(nv->getNodePath()), nv->getTraversalNumber()); traverse(node, nv); } private: LightManager* mLightManager; }; // Set on a LightManager. Clears the data from the previous frame. class LightManagerUpdateCallback : public SceneUtil::NodeCallback { public: void operator()(osg::Node* node, osg::NodeVisitor* nv) { LightManager* lightManager = static_cast(node); lightManager->update(nv->getTraversalNumber()); traverse(node, nv); } }; class LightManagerCullCallback : public SceneUtil::NodeCallback { public: LightManagerCullCallback(LightManager* lightManager) { if (!lightManager->getUBOManager()) return; for (size_t i = 0; i < mUBBs.size(); ++i) { auto& buffer = lightManager->getUBOManager()->getLightBuffer(i); mUBBs[i] = new osg::UniformBufferBinding(static_cast(Resource::SceneManager::UBOBinding::LightBuffer), buffer->getData(), 0, buffer->getData()->getTotalDataSize()); } } void operator()(LightManager* node, osgUtil::CullVisitor* cv) { osg::ref_ptr stateset = new osg::StateSet; if (node->getLightingMethod() == LightingMethod::SingleUBO) { const size_t frameId = cv->getTraversalNumber() % 2; stateset->setAttributeAndModes(mUBBs[frameId], osg::StateAttribute::ON); auto& buffer = node->getUBOManager()->getLightBuffer(cv->getTraversalNumber()); if (auto sun = node->getSunlight()) { buffer->setCachedSunPos(sun->getPosition()); buffer->setAmbient(0, sun->getAmbient()); buffer->setDiffuse(0, sun->getDiffuse()); buffer->setSpecular(0, sun->getSpecular()); } } else if (node->getLightingMethod() == LightingMethod::PerObjectUniform) { if (auto sun = node->getSunlight()) { osg::Matrixf lightMat; configurePosition( lightMat, sun->getPosition() * (*cv->getCurrentRenderStage()->getInitialViewMatrix())); configureAmbient(lightMat, sun->getAmbient()); configureDiffuse(lightMat, sun->getDiffuse()); configureSpecular(lightMat, sun->getSpecular()); node->setSunlightBuffer(lightMat, cv->getTraversalNumber()); stateset->addUniform(node->generateLightBufferUniform(lightMat)); } } cv->pushStateSet(stateset); traverse(node, cv); cv->popStateSet(); if (node->getPPLightsBuffer() && cv->getCurrentCamera()->getName() == Constants::SceneCamera) node->getPPLightsBuffer()->updateCount(cv->getTraversalNumber()); } std::array, 2> mUBBs; }; UBOManager::UBOManager(int lightCount) : mDummyProgram(new osg::Program) , mInitLayout(false) , mDirty({ true, true }) , mTemplate(new LightBuffer(lightCount)) { static const std::string dummyVertSource = generateDummyShader(lightCount); // Needed to query the layout of the buffer object. The layout specifier needed to use the std140 layout is not // reliably available, regardless of extensions, until GLSL 140. mDummyProgram->addShader(new osg::Shader(osg::Shader::VERTEX, dummyVertSource)); mDummyProgram->addBindUniformBlock( "LightBufferBinding", static_cast(Resource::SceneManager::UBOBinding::LightBuffer)); for (size_t i = 0; i < mLightBuffers.size(); ++i) { mLightBuffers[i] = new LightBuffer(lightCount); osg::ref_ptr ubo = new osg::UniformBufferObject; ubo->setUsage(GL_STREAM_DRAW); mLightBuffers[i]->getData()->setBufferObject(ubo); } } UBOManager::UBOManager(const UBOManager& copy, const osg::CopyOp& copyop) : osg::StateAttribute(copy, copyop) , mDummyProgram(copy.mDummyProgram) , mInitLayout(copy.mInitLayout) { } void UBOManager::releaseGLObjects(osg::State* state) const { mDummyProgram->releaseGLObjects(state); } int UBOManager::compare(const StateAttribute& sa) const { throw std::runtime_error("LightManagerStateAttribute::compare: unimplemented"); } void UBOManager::apply(osg::State& state) const { unsigned int frame = state.getFrameStamp()->getFrameNumber(); unsigned int index = frame % 2; if (!mInitLayout) { mDummyProgram->apply(state); auto handle = mDummyProgram->getPCP(state)->getHandle(); auto* ext = state.get(); int activeUniformBlocks = 0; ext->glGetProgramiv(handle, GL_ACTIVE_UNIFORM_BLOCKS, &activeUniformBlocks); // wait until the UBO binding is created if (activeUniformBlocks > 0) { initSharedLayout(ext, handle, frame); mInitLayout = true; } } else if (mDirty[index]) { mDirty[index] = false; mLightBuffers[index]->configureLayout(mTemplate); } mLightBuffers[index]->uploadCachedSunPos(state.getInitialViewMatrix()); mLightBuffers[index]->dirty(); } std::string UBOManager::generateDummyShader(int maxLightsInScene) { const std::string define = "@maxLightsInScene"; std::string shader = R"GLSL( #version 120 #extension GL_ARB_uniform_buffer_object : require struct LightData { ivec4 packedColors; vec4 position; vec4 attenuation; }; uniform LightBufferBinding { LightData LightBuffer[@maxLightsInScene]; }; void main() { gl_Position = vec4(0.0); } )GLSL"; shader.replace(shader.find(define), define.length(), std::to_string(maxLightsInScene)); return shader; } void UBOManager::initSharedLayout(osg::GLExtensions* ext, int handle, unsigned int frame) const { constexpr std::array index = { static_cast(Resource::SceneManager::UBOBinding::LightBuffer) }; int totalBlockSize = -1; int stride = -1; ext->glGetActiveUniformBlockiv(handle, 0, GL_UNIFORM_BLOCK_DATA_SIZE, &totalBlockSize); ext->glGetActiveUniformsiv(handle, index.size(), index.data(), GL_UNIFORM_ARRAY_STRIDE, &stride); std::array names = { "LightBuffer[0].packedColors", "LightBuffer[0].position", "LightBuffer[0].attenuation", }; std::vector indices(names.size()); std::vector offsets(names.size()); ext->glGetUniformIndices(handle, names.size(), names.data(), indices.data()); ext->glGetActiveUniformsiv(handle, indices.size(), indices.data(), GL_UNIFORM_OFFSET, offsets.data()); mTemplate->configureLayout(offsets[0], offsets[1], offsets[2], totalBlockSize, stride); } LightingMethod LightManager::getLightingMethodFromString(const std::string& value) { auto it = lightingMethodSettingMap.find(value); if (it != lightingMethodSettingMap.end()) return it->second; constexpr const char* fallback = "shaders compatibility"; Log(Debug::Warning) << "Unknown lighting method '" << value << "', returning fallback '" << fallback << "'"; return LightingMethod::PerObjectUniform; } std::string LightManager::getLightingMethodString(LightingMethod method) { for (const auto& p : lightingMethodSettingMap) if (p.second == method) return p.first; return ""; } LightManager::LightManager(const LightSettings& settings) : mStartLight(0) , mLightingMask(~0u) , mSun(nullptr) , mPointLightRadiusMultiplier(1.f) , mPointLightFadeEnd(0.f) , mPointLightFadeStart(0.f) { osg::GLExtensions* exts = SceneUtil::glExtensionsReady() ? &SceneUtil::getGLExtensions() : nullptr; bool supportsUBO = exts && exts->isUniformBufferObjectSupported; bool supportsGPU4 = exts && exts->isGpuShader4Supported; mSupported[static_cast(LightingMethod::FFP)] = true; mSupported[static_cast(LightingMethod::PerObjectUniform)] = true; mSupported[static_cast(LightingMethod::SingleUBO)] = supportsUBO && supportsGPU4; setUpdateCallback(new LightManagerUpdateCallback); if (settings.mLightingMethod == LightingMethod::FFP) { initFFP(ffpMaxLights); } else { static bool hasLoggedWarnings = false; if (settings.mLightingMethod == LightingMethod::SingleUBO && !hasLoggedWarnings) { if (!supportsUBO) Log(Debug::Warning) << "GL_ARB_uniform_buffer_object not supported: switching to shader " "compatibility lighting mode"; if (!supportsGPU4) Log(Debug::Warning) << "GL_EXT_gpu_shader4 not supported: switching to shader compatibility lighting mode"; hasLoggedWarnings = true; } if (!supportsUBO || !supportsGPU4 || settings.mLightingMethod == LightingMethod::PerObjectUniform) initPerObjectUniform(settings.mMaxLights); else initSingleUBO(settings.mMaxLights); getOrCreateStateSet()->addUniform(new osg::Uniform("PointLightCount", 0)); addCullCallback(new LightManagerCullCallback(this)); } updateSettings(settings.mLightBoundsMultiplier, settings.mMaximumLightDistance, settings.mLightFadeStart); } LightManager::LightManager(const LightManager& copy, const osg::CopyOp& copyop) : osg::Group(copy, copyop) , mStartLight(copy.mStartLight) , mLightingMask(copy.mLightingMask) , mSun(copy.mSun) , mLightingMethod(copy.mLightingMethod) , mPointLightRadiusMultiplier(copy.mPointLightRadiusMultiplier) , mPointLightFadeEnd(copy.mPointLightFadeEnd) , mPointLightFadeStart(copy.mPointLightFadeStart) , mMaxLights(copy.mMaxLights) , mPPLightBuffer(copy.mPPLightBuffer) { } LightingMethod LightManager::getLightingMethod() const { return mLightingMethod; } bool LightManager::usingFFP() const { return mLightingMethod == LightingMethod::FFP; } int LightManager::getMaxLights() const { return mMaxLights; } void LightManager::setMaxLights(int value) { mMaxLights = value; } int LightManager::getMaxLightsInScene() const { static constexpr int max = 16384 / LightBuffer::queryBlockSize(1); return max; } Shader::ShaderManager::DefineMap LightManager::getLightDefines() const { Shader::ShaderManager::DefineMap defines; defines["maxLights"] = std::to_string(getMaxLights()); defines["maxLightsInScene"] = std::to_string(getMaxLightsInScene()); defines["lightingMethodFFP"] = getLightingMethod() == LightingMethod::FFP ? "1" : "0"; defines["lightingMethodPerObjectUniform"] = getLightingMethod() == LightingMethod::PerObjectUniform ? "1" : "0"; defines["lightingMethodUBO"] = getLightingMethod() == LightingMethod::SingleUBO ? "1" : "0"; defines["useUBO"] = std::to_string(getLightingMethod() == LightingMethod::SingleUBO); // exposes bitwise operators defines["useGPUShader4"] = std::to_string(getLightingMethod() == LightingMethod::SingleUBO); defines["getLight"] = getLightingMethod() == LightingMethod::FFP ? "gl_LightSource" : "LightBuffer"; defines["startLight"] = getLightingMethod() == LightingMethod::SingleUBO ? "0" : "1"; defines["endLight"] = getLightingMethod() == LightingMethod::FFP ? defines["maxLights"] : "PointLightCount"; return defines; } void LightManager::processChangedSettings( float lightBoundsMultiplier, float maximumLightDistance, float lightFadeStart) { updateSettings(lightBoundsMultiplier, maximumLightDistance, lightFadeStart); } void LightManager::updateMaxLights(int maxLights) { if (usingFFP()) return; setMaxLights(maxLights); if (getLightingMethod() == LightingMethod::PerObjectUniform) { getStateSet()->removeUniform("LightBuffer"); getStateSet()->addUniform(generateLightBufferUniform(osg::Matrixf())); } for (auto& cache : mStateSetCache) cache.clear(); } void LightManager::updateSettings(float lightBoundsMultiplier, float maximumLightDistance, float lightFadeStart) { mPointLightRadiusMultiplier = lightBoundsMultiplier; mPointLightFadeEnd = maximumLightDistance; if (mPointLightFadeEnd > 0) mPointLightFadeStart = mPointLightFadeEnd * lightFadeStart; } void LightManager::initFFP(int targetLights) { setLightingMethod(LightingMethod::FFP); setMaxLights(targetLights); for (int i = 0; i < getMaxLights(); ++i) mDummies.push_back(new FFPLightStateAttribute(i, std::vector>())); } void LightManager::initPerObjectUniform(int targetLights) { setLightingMethod(LightingMethod::PerObjectUniform); setMaxLights(targetLights); getOrCreateStateSet()->addUniform(generateLightBufferUniform(osg::Matrixf())); } void LightManager::initSingleUBO(int targetLights) { setLightingMethod(LightingMethod::SingleUBO); setMaxLights(targetLights); mUBOManager = new UBOManager(getMaxLightsInScene()); getOrCreateStateSet()->setAttributeAndModes(mUBOManager); } void LightManager::setLightingMethod(LightingMethod method) { mLightingMethod = method; switch (method) { case LightingMethod::FFP: mStateSetGenerator = std::make_unique(); break; case LightingMethod::SingleUBO: mStateSetGenerator = std::make_unique(); break; case LightingMethod::PerObjectUniform: mStateSetGenerator = std::make_unique(); break; } mStateSetGenerator->mLightManager = this; } void LightManager::setLightingMask(size_t mask) { mLightingMask = mask; } size_t LightManager::getLightingMask() const { return mLightingMask; } void LightManager::setStartLight(int start) { mStartLight = start; if (!usingFFP()) return; // Set default light state to zero // This is necessary because shaders don't respect glDisable(GL_LIGHTX) so in addition to disabling // we'll have to set a light state that has no visible effect for (int i = start; i < getMaxLights(); ++i) { osg::ref_ptr defaultLight(new DisableLight(i)); getOrCreateStateSet()->setAttributeAndModes(defaultLight, osg::StateAttribute::OFF); } } int LightManager::getStartLight() const { return mStartLight; } void LightManager::update(size_t frameNum) { if (mPPLightBuffer) mPPLightBuffer->clear(frameNum); getLightIndexMap(frameNum).clear(); mLights.clear(); mLightsInViewSpace.clear(); // Do an occasional cleanup for orphaned lights. for (int i = 0; i < 2; ++i) { if (mStateSetCache[i].size() > 5000) mStateSetCache[i].clear(); } } void LightManager::addLight(LightSource* lightSource, const osg::Matrixf& worldMat, size_t frameNum) { LightSourceTransform l; l.mLightSource = lightSource; l.mWorldMatrix = worldMat; osg::Vec3f pos = osg::Vec3f(worldMat.getTrans().x(), worldMat.getTrans().y(), worldMat.getTrans().z()); lightSource->getLight(frameNum)->setPosition(osg::Vec4f(pos, 1.f)); mLights.push_back(l); } void LightManager::setSunlight(osg::ref_ptr sun) { if (usingFFP()) return; mSun = sun; } osg::ref_ptr LightManager::getSunlight() { return mSun; } size_t LightManager::HashLightIdList::operator()(const LightIdList& lightIdList) const { size_t hash = 0; for (size_t i = 0; i < lightIdList.size(); ++i) Misc::hashCombine(hash, lightIdList[i]); return hash; } osg::ref_ptr LightManager::getLightListStateSet( const LightList& lightList, size_t frameNum, const osg::RefMatrix* viewMatrix) { if (getLightingMethod() == LightingMethod::PerObjectUniform) { mStateSetGenerator->mViewMatrix = *viewMatrix; return mStateSetGenerator->generate(lightList, frameNum); } // possible optimization: return a StateSet containing all requested lights plus some extra lights (if a // suitable one exists) if (getLightingMethod() == LightingMethod::SingleUBO) { for (size_t i = 0; i < lightList.size(); ++i) { auto id = lightList[i]->mLightSource->getId(); if (getLightIndexMap(frameNum).find(id) != getLightIndexMap(frameNum).end()) continue; int index = getLightIndexMap(frameNum).size() + 1; updateGPUPointLight(index, lightList[i]->mLightSource, frameNum, viewMatrix); getLightIndexMap(frameNum).emplace(id, index); } } auto& stateSetCache = mStateSetCache[frameNum % 2]; LightIdList lightIdList; lightIdList.reserve(lightList.size()); std::transform(lightList.begin(), lightList.end(), std::back_inserter(lightIdList), [](const LightSourceViewBound* l) { return l->mLightSource->getId(); }); auto found = stateSetCache.find(lightIdList); if (found != stateSetCache.end()) { mStateSetGenerator->update(found->second, lightList, frameNum); return found->second; } auto stateset = mStateSetGenerator->generate(lightList, frameNum); stateSetCache.emplace(lightIdList, stateset); return stateset; } const std::vector& LightManager::getLightsInViewSpace( osgUtil::CullVisitor* cv, const osg::RefMatrix* viewMatrix, size_t frameNum) { osg::Camera* camera = cv->getCurrentCamera(); osg::observer_ptr camPtr(camera); auto it = mLightsInViewSpace.find(camPtr); if (it == mLightsInViewSpace.end()) { it = mLightsInViewSpace.insert(std::make_pair(camPtr, LightSourceViewBoundCollection())).first; for (const auto& transform : mLights) { osg::Matrixf worldViewMat = transform.mWorldMatrix * (*viewMatrix); float radius = transform.mLightSource->getRadius(); osg::BoundingSphere viewBound(osg::Vec3f(), radius * mPointLightRadiusMultiplier); transformBoundingSphere(worldViewMat, viewBound); if (transform.mLightSource->getLastAppliedFrame() != frameNum && mPointLightFadeEnd != 0.f) { const float fadeDelta = mPointLightFadeEnd - mPointLightFadeStart; const float viewDelta = viewBound.center().length() - mPointLightFadeStart; float fade = 1 - std::clamp(viewDelta / fadeDelta, 0.f, 1.f); if (fade == 0.f) continue; auto* light = transform.mLightSource->getLight(frameNum); light->setDiffuse(light->getDiffuse() * fade); light->setSpecular(light->getSpecular() * fade); transform.mLightSource->setLastAppliedFrame(frameNum); } LightSourceViewBound l; l.mLightSource = transform.mLightSource; l.mViewBound = viewBound; it->second.push_back(l); } const bool fillPPLights = mPPLightBuffer && it->first->getName() == Constants::SceneCamera; const bool sceneLimitReached = getLightingMethod() == LightingMethod::SingleUBO && it->second.size() > static_cast(getMaxLightsInScene() - 1); if (fillPPLights || sceneLimitReached) { auto sorter = [](const LightSourceViewBound& left, const LightSourceViewBound& right) { return left.mViewBound.center().length2() - left.mViewBound.radius2() < right.mViewBound.center().length2() - right.mViewBound.radius2(); }; std::sort(it->second.begin(), it->second.end(), sorter); if (fillPPLights) { osg::CullingSet& cullingSet = cv->getModelViewCullingStack().front(); for (const auto& bound : it->second) { if (bound.mLightSource->getEmpty()) continue; const auto* light = bound.mLightSource->getLight(frameNum); // Ignore negative lights if (light->getDiffuse().x() < 0.f) continue; const float radius = bound.mLightSource->getRadius(); osg::BoundingSphere frustumBound = bound.mViewBound; frustumBound.radius() = radius * 2.f; // Ignore culled lights if (cullingSet.isCulled(frustumBound)) continue; mPPLightBuffer->setLight(frameNum, light, radius); } } if (sceneLimitReached) it->second.resize(getMaxLightsInScene() - 1); } } return it->second; } void LightManager::updateGPUPointLight( int index, LightSource* lightSource, size_t frameNum, const osg::RefMatrix* viewMatrix) { auto* light = lightSource->getLight(frameNum); auto& buf = getUBOManager()->getLightBuffer(frameNum); buf->setDiffuse(index, light->getDiffuse()); buf->setAmbient(index, light->getAmbient()); buf->setSpecular(index, light->getSpecular()); buf->setAttenuationRadius(index, osg::Vec4(light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), lightSource->getRadius())); buf->setPosition(index, light->getPosition() * (*viewMatrix)); } osg::ref_ptr LightManager::generateLightBufferUniform(const osg::Matrixf& sun) { osg::ref_ptr uniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "LightBuffer", getMaxLights()); uniform->setElement(0, sun); return uniform; } void LightManager::setCollectPPLights(bool enabled) { if (enabled) mPPLightBuffer = std::make_shared(); else mPPLightBuffer = nullptr; } LightSource::LightSource() : mRadius(0.f) , mActorFade(1.f) , mLastAppliedFrame(0) { setUpdateCallback(new CollectLightCallback); mId = sLightId++; } LightSource::LightSource(const LightSource& copy, const osg::CopyOp& copyop) : osg::Node(copy, copyop) , mRadius(copy.mRadius) , mActorFade(copy.mActorFade) , mLastAppliedFrame(copy.mLastAppliedFrame) { mId = sLightId++; for (size_t i = 0; i < mLight.size(); ++i) mLight[i] = new osg::Light(*copy.mLight[i].get(), copyop); } void LightListCallback::operator()(osg::Node* node, osgUtil::CullVisitor* cv) { bool pushedState = pushLightState(node, cv); traverse(node, cv); if (pushedState) cv->popStateSet(); } bool LightListCallback::pushLightState(osg::Node* node, osgUtil::CullVisitor* cv) { if (!mLightManager) { mLightManager = findLightManager(cv->getNodePath()); if (!mLightManager) return false; } if (!(cv->getTraversalMask() & mLightManager->getLightingMask())) return false; // Possible optimizations: // - organize lights in a quad tree // Don't use Camera::getViewMatrix, that one might be relative to another camera! const osg::RefMatrix* viewMatrix = cv->getCurrentRenderStage()->getInitialViewMatrix(); // Update light list if necessary // This makes sure we don't update it more than once per frame when rendering with multiple cameras if (mLastFrameNumber != cv->getTraversalNumber()) { mLastFrameNumber = cv->getTraversalNumber(); // Get the node bounds in view space // NB: do not node->getBound() * modelView, that would apply the node's transformation twice osg::BoundingSphere nodeBound; const osg::Transform* transform = node->asTransform(); if (transform) { for (size_t i = 0; i < transform->getNumChildren(); ++i) nodeBound.expandBy(transform->getChild(i)->getBound()); } else nodeBound = node->getBound(); transformBoundingSphere(*cv->getModelViewMatrix(), nodeBound); const std::vector& lights = mLightManager->getLightsInViewSpace(cv, viewMatrix, mLastFrameNumber); mLightList.clear(); for (const LightManager::LightSourceViewBound& light : lights) { if (mIgnoredLightSources.contains(light.mLightSource)) continue; if (light.mViewBound.intersects(nodeBound)) mLightList.push_back(&light); } const size_t maxLights = mLightManager->getMaxLights() - mLightManager->getStartLight(); if (mLightList.size() > maxLights) { // Sort by proximity to object: prefer closer lights with larger radius std::sort(mLightList.begin(), mLightList.end(), [&](const SceneUtil::LightManager::LightSourceViewBound* left, const SceneUtil::LightManager::LightSourceViewBound* right) { const float leftDist = (nodeBound.center() - left->mViewBound.center()).length2(); const float rightDist = (nodeBound.center() - right->mViewBound.center()).length2(); // A tricky way to compare normalized distance. This avoids division by near zero return left->mViewBound.radius() * rightDist > right->mViewBound.radius() * leftDist; }); mLightList.resize(maxLights); } } if (!mLightList.empty()) { cv->pushStateSet(mLightManager->getLightListStateSet(mLightList, mLastFrameNumber, viewMatrix)); return true; } return false; } }