diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp index 72470b8c2c..c7a4834299 100644 --- a/components/sceneutil/lightmanager.cpp +++ b/components/sceneutil/lightmanager.cpp @@ -47,55 +47,81 @@ namespace SceneUtil { 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: - LightBuffer(int count) : mData(new osg::Vec4Array(3*count)), mEndian(osg::getCpuByteOrder()) {} + enum LayoutOffset + { + Diffuse, + DiffuseSign, + Ambient, + Specular, + Position, + AttenuationRadius + }; + + LightBuffer(int count) + : mData(new osg::FloatArray(3*4*count)) + , mEndian(osg::getCpuByteOrder()) + , mCount(count) + , mStride(12) + { + mOffsets[Diffuse] = 0; + mOffsets[Ambient] = 1; + mOffsets[Specular] = 2; + mOffsets[DiffuseSign] = 3; + mOffsets[Position] = 4; + mOffsets[AttenuationRadius] = 8; + } + + LightBuffer(const LightBuffer& copy) + : osg::Referenced() + , mData(copy.mData) + , mEndian(copy.mEndian) + , mCount(copy.mCount) + , mStride(copy.mStride) + , mOffsets(copy.mOffsets) + {} void setDiffuse(int index, const osg::Vec4& value) { - auto signedValue = value; + // Deal with negative lights (negative diffuse) by passing a sign bit in the unused alpha component + auto positiveColor = value; float signBit = 1.0; if (value[0] < 0) { - signedValue *= -1.0; + positiveColor *= -1.0; signBit = -1.0; } - *(unsigned int*)(&(*mData)[3*index][0]) = asRGBA(signedValue); - *(int*)(&(*mData)[3*index][3]) = signBit; + *(unsigned int*)(&(*mData)[getOffset(index, Diffuse)]) = asRGBA(positiveColor); + *(int*)(&(*mData)[getOffset(index, DiffuseSign)]) = signBit; } void setAmbient(int index, const osg::Vec4& value) { - *(unsigned int*)(&(*mData)[3*index][1]) = asRGBA(value); + *(unsigned int*)(&(*mData)[getOffset(index, Ambient)]) = asRGBA(value); } void setSpecular(int index, const osg::Vec4& value) { - *(unsigned int*)(&(*mData)[3*index][2]) = asRGBA(value); + *(unsigned int*)(&(*mData)[getOffset(index, Specular)]) = asRGBA(value); } void setPosition(int index, const osg::Vec4& value) { - (*mData)[3*index+1] = value; - } - - void setAttenuation(int index, float c, float l, float q) - { - (*mData)[3*index+2][0] = c; - (*mData)[3*index+2][1] = l; - (*mData)[3*index+2][2] = q; + *(osg::Vec4*)(&(*mData)[getOffset(index, Position)]) = value; } - void setRadius(int index, float value) + void setAttenuationRadius(int index, const osg::Vec4& value) { - (*mData)[3*index+2][3] = value; + *(osg::Vec4*)(&(*mData)[getOffset(index, AttenuationRadius)]) = value; } auto getPosition(int index) { - return (*mData)[3*index+1]; + return *(osg::Vec4*)(&(*mData)[getOffset(index, Position)]); } auto& getData() @@ -118,8 +144,40 @@ namespace SceneUtil return mEndian == osg::BigEndian ? value.asABGR() : value.asRGBA(); } - osg::ref_ptr mData; + int getOffset(int index, LayoutOffset slot) + { + return mStride * index + mOffsets[slot]; + } + + void configureLayout(int offsetColors, int offsetPosition, int offsetAttenuationRadius, int size, int stride) + { + static constexpr auto sizeofVec4 = sizeof(GL_FLOAT) * osg::Vec4::num_components; + static constexpr auto sizeofFloat = sizeof(GL_FLOAT); + + mOffsets[Diffuse] = offsetColors / sizeofFloat; + mOffsets[Ambient] = mOffsets[Diffuse] + 1; + mOffsets[Specular] = mOffsets[Diffuse] + 2; + mOffsets[DiffuseSign] = mOffsets[Diffuse] + 3; + mOffsets[Position] = offsetPosition / sizeofFloat; + mOffsets[AttenuationRadius] = offsetAttenuationRadius / sizeofFloat; + mStride = (offsetAttenuationRadius + sizeofVec4 + stride) / 4; + + // Copy over previous buffers light data. Buffers populate before we know the layout. + LightBuffer oldBuffer = LightBuffer(*this); + for (int i = 0; i < oldBuffer.mCount; ++i) + { + *(osg::Vec4*)(&(*mData)[getOffset(i, Diffuse)]) = *(osg::Vec4*)(&(*mData)[oldBuffer.getOffset(i, Diffuse)]); + *(osg::Vec4*)(&(*mData)[getOffset(i, Position)]) = *(osg::Vec4*)(&(*mData)[oldBuffer.getOffset(i, Position)]); + *(osg::Vec4*)(&(*mData)[getOffset(i, AttenuationRadius)]) = *(osg::Vec4*)(&(*mData)[oldBuffer.getOffset(i, AttenuationRadius)]); + } + } + + private: + osg::ref_ptr mData; osg::Endian mEndian; + int mCount; + int mStride; + std::unordered_map mOffsets; }; class LightStateCache @@ -165,8 +223,8 @@ namespace SceneUtil buffer->setPosition(0, light->getPosition()); osg::ref_ptr ubo = new osg::UniformBufferObject; - buffer->mData->setBufferObject(ubo); - osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), buffer->mData.get(), 0, buffer->mData->getTotalDataSize()); + buffer->getData()->setBufferObject(ubo); + osg::ref_ptr ubb = new osg::UniformBufferBinding(static_cast(Shader::UBOBinding::LightBuffer), buffer->getData().get(), 0, buffer->getData()->getTotalDataSize()); stateset->setAttributeAndModes(ubb, mode); @@ -572,11 +630,24 @@ namespace SceneUtil class LightManagerStateAttribute : public osg::StateAttribute { public: - LightManagerStateAttribute() : mLightManager(nullptr) {} - LightManagerStateAttribute(LightManager* lightManager) : mLightManager(lightManager) {} + LightManagerStateAttribute() + : mLightManager(nullptr) {} - LightManagerStateAttribute(const LightManagerStateAttribute& copy,const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) - : osg::StateAttribute(copy,copyop),mLightManager(copy.mLightManager) {} + LightManagerStateAttribute(LightManager* lightManager) + : mLightManager(lightManager) + , mDummyProgram(new osg::Program) + { + static const std::string dummyVertSource = generateDummyShader(mLightManager->getMaxLightsInScene()); + + mDummyProgram->addShader(new osg::Shader(osg::Shader::VERTEX, dummyVertSource)); + mDummyProgram->addBindUniformBlock("LightBufferBinding", static_cast(Shader::UBOBinding::LightBuffer)); + // 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. + mLightManager->getOrCreateStateSet()->setAttributeAndModes(mDummyProgram, osg::StateAttribute::ON|osg::StateAttribute::PROTECTED); + } + + LightManagerStateAttribute(const LightManagerStateAttribute& copy, const osg::CopyOp& copyop=osg::CopyOp::SHALLOW_COPY) + : osg::StateAttribute(copy,copyop), mLightManager(copy.mLightManager) {} int compare(const StateAttribute &sa) const override { @@ -585,12 +656,76 @@ namespace SceneUtil META_StateAttribute(NifOsg, LightManagerStateAttribute, osg::StateAttribute::LIGHT) + void initSharedLayout(osg::GLExtensions* ext, int handle) const + { + std::vector index = { static_cast(Shader::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::vector 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()); + + for (int i = 0; i < 2; ++i) + { + auto& buf = mLightManager->getLightBuffer(i); + buf->configureLayout(offsets[0], offsets[1], offsets[2], totalBlockSize, stride); + } + } + void apply(osg::State& state) const override { - mLightManager->getLightBuffer(state.getFrameStamp()->getFrameNumber())->dirty(); + static bool init = false; + if (!init) + { + 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); + init = true; + } + } + else + { + mLightManager->getLightBuffer(state.getFrameStamp()->getFrameNumber())->dirty(); + } + } + + private: + + std::string generateDummyShader(int maxLightsInScene) + { + return "#version 120\n" + "#extension GL_ARB_uniform_buffer_object : require\n" + "struct LightData {\n" + " ivec4 packedColors;\n" + " vec4 position;\n" + " vec4 attenuation;\n" + "};\n" + "uniform LightBufferBinding {\n" + " LightData LightBuffer[" + std::to_string(mLightManager->getMaxLightsInScene()) + "];\n" + "};\n" + "void main() { gl_Position = vec4(0.0); }\n"; } LightManager* mLightManager; + osg::ref_ptr mDummyProgram; }; const std::unordered_map LightManager::mLightingMethodSettingMap = { @@ -654,12 +789,15 @@ namespace SceneUtil bool supportsUBO = exts && exts->isUniformBufferObjectSupported; bool supportsGPU4 = exts && exts->isGpuShader4Supported; - if (getLightingMethod() == LightingMethod::SingleUBO) + static bool hasLoggedWarnings = false; + + if (getLightingMethod() == LightingMethod::SingleUBO && !hasLoggedWarnings) { if (!supportsUBO) - Log(Debug::Info) << "GL_ARB_uniform_buffer_object not supported: using fallback uniforms"; - else if (!supportsGPU4) - Log(Debug::Info) << "GL_EXT_gpu_shader4 not supported: using fallback uniforms"; + 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; } int targetLights = Settings::Manager::getInt("max lights", "Shaders"); @@ -978,8 +1116,7 @@ namespace SceneUtil auto& buf = getLightBuffer(frameNum); buf->setDiffuse(index, light->getDiffuse()); buf->setAmbient(index, light->getAmbient()); - buf->setAttenuation(index, light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation()); - buf->setRadius(index, lightSource->getRadius()); + buf->setAttenuationRadius(index, osg::Vec4(light->getConstantAttenuation(), light->getLinearAttenuation(), light->getQuadraticAttenuation(), lightSource->getRadius())); buf->setPosition(index, light->getPosition() * (*viewMatrix)); } diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 2d543303e9..c931789068 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -453,7 +453,7 @@ lighting method = shaders compatibility # Sets the bounding sphere multiplier of light sources, which are used to # determine if an object should receive lighting. Higher values will allow for -# smoother transitions of light sources, but may have a performance cost and +# smoother transitions of light sources, but may carry a performance cost and # requires a higher number of 'max lights' set. It is recommended to keep this # at 1.0 with 'legacy' lighting enabled. light bounds multiplier = 1.75 diff --git a/files/shaders/lighting.glsl b/files/shaders/lighting.glsl index 3678a35704..a3df6666fa 100644 --- a/files/shaders/lighting.glsl +++ b/files/shaders/lighting.glsl @@ -35,15 +35,16 @@ vec4 unpackRGBA(int data) struct LightData { - ivec4 packedColors; // diffuse, ambient, specular + ivec4 packedColors; // diffuse, ambient, specular vec4 position; - vec4 attenuation; // constant, linear, quadratic, radius + vec4 attenuation; // constant, linear, quadratic, radius }; uniform int PointLightIndex[@maxLights]; uniform int PointLightCount; -layout(std140) uniform LightBufferBinding +// Defaults to shared layout. If we ever move to GLSL 140, std140 layout should be considered +uniform LightBufferBinding { LightData LightBuffer[@maxLightsInScene]; }; @@ -56,7 +57,7 @@ struct LightData vec4 diffuse; vec4 ambient; vec4 specular; - vec4 attenuation; // constant, linear, quadratic, radius + vec4 attenuation; // constant, linear, quadratic, radius }; uniform LightData LightBuffer[@maxLights]; @@ -92,7 +93,6 @@ void perLightSun(out vec3 ambientOut, out vec3 diffuseOut, vec3 viewPos, vec3 vi } lambert *= clamp(-8.0 * (1.0 - 0.3) * eyeCosine + 1.0, 0.3, 1.0); #endif - diffuseOut = sunDiffuse * lambert; }