postprocessing lua api extensions

fix/shrink_builds
cody glassman 3 years ago
parent 02cfb7e258
commit 6093cb5f2c

@ -296,7 +296,7 @@ namespace MWGui
auto technique = processor->loadTechnique(name);
if (!technique)
if (!technique || technique->getStatus() == fx::Technique::Status::File_Not_exists)
return;
while (mConfigArea->getChildCount() > 0)
@ -304,9 +304,6 @@ namespace MWGui
mShaderInfo->setCaption("");
if (!technique)
return;
std::ostringstream ss;
const std::string NA = "NA";
@ -322,8 +319,8 @@ namespace MWGui
const auto flags = technique->getFlags();
const auto flag_interior = serializeBool (!(flags & fx::Technique::Flag_Disable_Interiors));
const auto flag_exterior = serializeBool (!(flags & fx::Technique::Flag_Disable_Exteriors));
const auto flag_interior = serializeBool(!(flags & fx::Technique::Flag_Disable_Interiors));
const auto flag_exterior = serializeBool(!(flags & fx::Technique::Flag_Disable_Exteriors));
const auto flag_underwater = serializeBool(!(flags & fx::Technique::Flag_Disable_Underwater));
const auto flag_abovewater = serializeBool(!(flags & fx::Technique::Flag_Disable_Abovewater));
@ -339,14 +336,12 @@ namespace MWGui
<< "#{fontcolourhtml=header} Underwater: #{fontcolourhtml=normal} " << flag_underwater
<< "#{fontcolourhtml=header} Abovewater: #{fontcolourhtml=normal} " << flag_abovewater;
break;
case fx::Technique::Status::File_Not_exists:
ss << "#{fontcolourhtml=negative}Shader Error: #{fontcolourhtml=header} <" << std::string(technique->getFileName()) << ">#{fontcolourhtml=normal} not found." << endl << endl
<< "Ensure the shader file is in a 'Shaders/' sub directory in a data files directory";
break;
case fx::Technique::Status::Parse_Error:
ss << "#{fontcolourhtml=negative}Shader Compile Error: #{fontcolourhtml=normal} <" << std::string(technique->getName()) << "> failed to compile." << endl << endl
<< technique->getLastError();
break;
case fx::Technique::Status::File_Not_exists:
break;
}
mShaderInfo->setCaptionWithReplacing(ss.str());

@ -59,9 +59,44 @@ namespace MWLua
return Misc::StringUtils::format("Shader(%s, %s)", mShader->getName(), mShader->getFileName());
}
bool mQueuedAction = false;
enum { Action_None, Action_Enable, Action_Disable } mQueuedAction = Action_None;
};
template <class T>
auto getSetter(const Context& context)
{
return [context](const Shader& shader, const std::string& name, const T& value) {
context.mLuaManager->addAction(std::make_unique<SetUniformShaderAction<T>>(context.mLua, shader.mShader, name, value));
};
}
template <class T>
auto getArraySetter(const Context& context)
{
return [context](const Shader& shader, const std::string& name, const sol::table& table) {
auto targetSize = MWBase::Environment::get().getWorld()->getPostProcessor()->getUniformSize(shader.mShader, name);
if (!targetSize.has_value())
throw std::runtime_error(Misc::StringUtils::format("Failed setting uniform array '%s'", name));
if (*targetSize != table.size())
throw std::runtime_error(Misc::StringUtils::format("Mismatching uniform array size, got %zu expected %zu", table.size(), *targetSize));
std::vector<T> values;
values.reserve(*targetSize);
for (size_t i = 0; i < *targetSize; ++i)
{
sol::object obj = table[i+1];
if (!obj.is<T>())
throw std::runtime_error("Invalid type for uniform array");
values.push_back(obj.as<T>());
}
context.mLuaManager->addAction(std::make_unique<SetUniformShaderAction<std::vector<T>>>(context.mLua, shader.mShader, name, values));
};
}
sol::table initPostprocessingPackage(const Context& context)
{
sol::table api(context.mLua->sol(), sol::create);
@ -76,62 +111,64 @@ namespace MWLua
pos = optPos.value();
if (shader.mShader && shader.mShader->isValid())
shader.mQueuedAction = true;
shader.mQueuedAction = Shader::Action_Enable;
context.mLuaManager->addAction(
[=] { MWBase::Environment::get().getWorld()->getPostProcessor()->enableTechnique(shader.mShader, pos); },
[=, &shader] {
shader.mQueuedAction = Shader::Action_None;
if (!MWBase::Environment::get().getWorld()->getPostProcessor()->enableTechnique(shader.mShader, pos))
throw std::runtime_error("Failed enabling shader '" + shader.mShader->getName() + "'");
},
"Enable shader " + (shader.mShader ? shader.mShader->getName() : "nil")
);
};
shader["disable"] = [context](Shader& shader)
{
shader.mQueuedAction = false;
shader.mQueuedAction = Shader::Action_Disable;
context.mLuaManager->addAction(
[&] { MWBase::Environment::get().getWorld()->getPostProcessor()->disableTechnique(shader.mShader); },
[&] {
shader.mQueuedAction = Shader::Action_None;
if (!MWBase::Environment::get().getWorld()->getPostProcessor()->disableTechnique(shader.mShader))
throw std::runtime_error("Failed disabling shader '" + shader.mShader->getName() + "'");
},
"Disable shader " + (shader.mShader ? shader.mShader->getName() : "nil")
);
};
shader["isEnabled"] = [](const Shader& shader)
{
return shader.mQueuedAction;
if (shader.mQueuedAction == Shader::Action_Enable)
return true;
else if (shader.mQueuedAction == Shader::Action_Disable)
return false;
return MWBase::Environment::get().getWorld()->getPostProcessor()->isTechniqueEnabled(shader.mShader);
};
shader["setBool"] = [context](const Shader& shader, const std::string& name, bool value)
{
context.mLuaManager->addAction(std::make_unique<SetUniformShaderAction<bool>>(context.mLua, shader.mShader, name, value));
};
shader["setFloat"] = [context](const Shader& shader, const std::string& name, float value)
{
context.mLuaManager->addAction(std::make_unique<SetUniformShaderAction<float>>(context.mLua, shader.mShader, name, value));
};
shader["setInt"] = [context](const Shader& shader, const std::string& name, int value)
{
context.mLuaManager->addAction(std::make_unique<SetUniformShaderAction<int>>(context.mLua, shader.mShader, name, value));
};
shader["setBool"] = getSetter<bool>(context);
shader["setFloat"] = getSetter<float>(context);
shader["setInt"] = getSetter<int>(context);
shader["setVector2"] = getSetter<osg::Vec2f>(context);
shader["setVector3"] = getSetter<osg::Vec3f>(context);
shader["setVector4"] = getSetter<osg::Vec4f>(context);
shader["setVector2"] = [context](const Shader& shader, const std::string& name, const osg::Vec2f& value)
{
context.mLuaManager->addAction(std::make_unique<SetUniformShaderAction<osg::Vec2f>>(context.mLua, shader.mShader, name, value));
};
shader["setFloatArray"] = getArraySetter<float>(context);
shader["setIntArray"] = getArraySetter<int>(context);
shader["setVector2Array"] = getArraySetter<osg::Vec2f>(context);
shader["setVector3Array"] = getArraySetter<osg::Vec3f>(context);
shader["setVector4Array"] = getArraySetter<osg::Vec4f>(context);
shader["setVector3"] = [context](const Shader& shader, const std::string& name, const osg::Vec3f& value)
api["load"] = [](const std::string& name)
{
context.mLuaManager->addAction(std::make_unique<SetUniformShaderAction<osg::Vec3f>>(context.mLua, shader.mShader, name, value));
};
Shader shader{MWBase::Environment::get().getWorld()->getPostProcessor()->loadTechnique(name, false)};
shader["setVector4"] = [context](const Shader& shader, const std::string& name, const osg::Vec4f& value)
{
context.mLuaManager->addAction(std::make_unique<SetUniformShaderAction<osg::Vec4f>>(context.mLua, shader.mShader, name, value));
};
if (!shader.mShader || !shader.mShader->isValid())
throw std::runtime_error(Misc::StringUtils::format("Failed loading shader '%s'", name));
api["load"] = [](const std::string& name)
{
return Shader(MWBase::Environment::get().getWorld()->getPostProcessor()->loadTechnique(name, false));
return shader;
};
return LuaUtil::makeReadOnly(api);

@ -406,6 +406,13 @@ namespace MWRender
void PostProcessor::update(size_t frameId)
{
while (!mQueuedTemplates.empty())
{
mTemplates.push_back(std::move(mQueuedTemplates.back()));
mQueuedTemplates.pop_back();
}
updateLiveReload();
reloadIfRequired();
@ -582,7 +589,7 @@ namespace MWRender
if (uniform->mSamplerType) continue;
if (auto type = uniform->getType())
uniform->setUniform(node.mRootStateSet->getOrCreateUniform(uniform->mName, type.value()));
uniform->setUniform(node.mRootStateSet->getOrCreateUniform(uniform->mName.c_str(), *type, uniform->getNumElements()));
}
std::unordered_map<osg::Texture2D*, osg::Texture2D*> renderTargetCache;
@ -643,7 +650,13 @@ namespace MWRender
bool PostProcessor::enableTechnique(std::shared_ptr<fx::Technique> technique, std::optional<int> location)
{
if (!technique || technique->getName() == "main" || (location.has_value() && location.value() <= 0))
if (!isEnabled())
{
Log(Debug::Warning) << "PostProcessing disabled, cannot load technique '" << technique->getName() << "'";
return false;
}
if (!technique || Misc::StringUtils::ciEqual(technique->getName(), "main") || (location.has_value() && location.value() <= 0))
return false;
disableTechnique(technique, false);
@ -753,14 +766,21 @@ namespace MWRender
mHUDCamera->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF);
}
std::shared_ptr<fx::Technique> PostProcessor::loadTechnique(const std::string& name, bool insert)
std::shared_ptr<fx::Technique> PostProcessor::loadTechnique(const std::string& name, bool loadNextFrame)
{
if (!isEnabled())
{
Log(Debug::Warning) << "PostProcessing disabled, cannot load technique '" << name << "'";
return nullptr;
}
for (size_t i = 0; i < mTemplates.size(); ++i)
if (name == mTemplates[i]->getName())
return mTemplates[i];
for (const auto& technique : mTemplates)
if (Misc::StringUtils::ciEqual(technique->getName(), name))
return technique;
for (const auto& technique : mQueuedTemplates)
if (Misc::StringUtils::ciEqual(technique->getName(), name))
return technique;
auto technique = std::make_shared<fx::Technique>(*mVFS, *mRendering.getResourceSystem()->getImageManager(), name, mWidth, mHeight, mUBO, mNormalsSupported);
@ -769,8 +789,11 @@ namespace MWRender
if (technique->getStatus() != fx::Technique::Status::File_Not_exists)
technique->setLastModificationTime(std::filesystem::last_write_time(mTechniqueFileMap[technique->getName()]));
if (!insert)
if (!loadNextFrame)
{
mQueuedTemplates.push_back(technique);
return technique;
}
reloadMainPass(*technique);
@ -779,18 +802,6 @@ namespace MWRender
return mTemplates.back();
}
void PostProcessor::addTemplate(std::shared_ptr<fx::Technique> technique)
{
if (!isEnabled())
return;
for (size_t i = 0; i < mTemplates.size(); ++i)
if (technique.get() == mTemplates[i].get())
return;
mTemplates.push_back(technique);
}
void PostProcessor::reloadTechniques()
{
if (!isEnabled())
@ -810,7 +821,7 @@ namespace MWRender
if (techniqueName.empty())
continue;
if ((&techniqueName != &techniqueStrings.front()) && Misc::StringUtils::ciEqual(techniqueName, "main"))
if ((&techniqueName != &techniqueStrings.front()) && techniqueName == "main")
{
Log(Debug::Warning) << "main.omwfx techniqued specified in chain, this is not allowed. technique file will be ignored if it exists.";
continue;

@ -136,6 +136,16 @@ namespace MWRender
(*it)->setValue(value);
}
std::optional<size_t> getUniformSize(std::shared_ptr<fx::Technique> technique, const std::string& name)
{
auto it = technique->findUniform(name);
if (it == technique->getUniformMap().end())
return std::nullopt;
return (*it)->getNumElements();
}
bool isTechniqueEnabled(const std::shared_ptr<fx::Technique>& technique) const;
void setExteriorFlag(bool exterior) { mExteriorFlag = exterior; }
@ -144,9 +154,7 @@ namespace MWRender
void toggleMode();
std::shared_ptr<fx::Technique> loadTechnique(const std::string& name, bool insert=true);
void addTemplate(std::shared_ptr<fx::Technique> technique);
std::shared_ptr<fx::Technique> loadTechnique(const std::string& name, bool loadNextFrame=true);
bool isEnabled() const { return mUsePostProcessing && mEnabled; }
@ -192,6 +200,7 @@ namespace MWRender
TechniqueList mTechniques;
TechniqueList mTemplates;
TechniqueList mQueuedTemplates;
std::unordered_map<std::string, std::filesystem::path> mTechniqueFileMap;

@ -171,7 +171,7 @@ TestFile repeated_shared_block{R"(
const auto& uniform = mTechnique->getUniformMap().front();
EXPECT_TRUE(uniform->mStatic);
EXPECT_FLOAT_EQ(uniform->mStep, 0.5f);
EXPECT_DOUBLE_EQ(uniform->mStep, 0.5);
EXPECT_EQ(uniform->getDefault<osg::Vec4f>(), osg::Vec4f(0,0,0,0));
EXPECT_EQ(uniform->getMin<osg::Vec4f>(), osg::Vec4f(0,1,0,0));
EXPECT_EQ(uniform->getMax<osg::Vec4f>(), osg::Vec4f(0,0,1,0));

@ -526,7 +526,7 @@ namespace fx
error(Misc::StringUtils::format("redeclaration of uniform '%s'", std::string(mBlockName)));
std::shared_ptr<Types::UniformBase> uniform = std::make_shared<Types::UniformBase>();
Types::Uniform<SrcT> data;
Types::Uniform<SrcT> data = Types::Uniform<SrcT>();
while (!isNext<Lexer::Close_bracket>() && !isNext<Lexer::Eof>())
{
@ -543,11 +543,6 @@ namespace fx
static_assert(isVec || isFloat || isInt || isBool, "Unsupported type");
std::optional<double> step;
if constexpr (isInt)
step = 1.0;
if (key == "default")
{
if constexpr (isVec)
@ -559,6 +554,15 @@ namespace fx
else if constexpr (isBool)
data.mDefault = parseBool();
}
else if (key == "size")
{
if constexpr (isBool)
error("bool arrays currently unsupported");
int size = parseInteger();
if (size > 1)
data.mArray = std::vector<SrcT>(size);
}
else if (key == "min")
{
if constexpr (isVec)
@ -582,7 +586,7 @@ namespace fx
data.mMax = parseBool();
}
else if (key == "step")
step = parseFloat();
uniform->mStep = parseFloat();
else if (key == "static")
uniform->mStatic = parseBool();
else if (key == "description")
@ -598,18 +602,28 @@ namespace fx
else
error(Misc::StringUtils::format("unexpected key '%s'", std::string{key}));
if (step)
uniform->mStep = step.value();
expect<Lexer::SemiColon>();
}
if (data.isArray())
uniform->mStatic = false;
uniform->mName = std::string(mBlockName);
uniform->mData = data;
uniform->mTechniqueName = mName;
if (auto cached = Settings::ShaderManager::get().getValue<SrcT>(mName, uniform->mName))
uniform->setValue<SrcT>(cached.value());
if (data.mArray)
{
if constexpr (!std::is_same_v<bool, SrcT>)
{
if (auto cached = Settings::ShaderManager::get().getValue<std::vector<SrcT>>(mName, uniform->mName))
uniform->setValue(cached.value());
}
}
else if (auto cached = Settings::ShaderManager::get().getValue<SrcT>(mName, uniform->mName))
{
uniform->setValue(cached.value());
}
mDefinedUniforms.emplace_back(std::move(uniform));
}

@ -60,12 +60,24 @@ namespace fx
struct Uniform
{
std::optional<T> mValue;
T mDefault;
std::optional<std::vector<T>> mArray;
T mDefault = {};
T mMin = std::numeric_limits<T>::lowest();
T mMax = std::numeric_limits<T>::max();
using value_type = T;
bool isArray() const
{
return mArray.has_value();
}
const std::vector<T>& getArray() const
{
return *mArray;
}
T getValue() const
{
return mValue.value_or(mDefault);
@ -97,7 +109,7 @@ namespace fx
bool mStatic = true;
std::optional<SamplerType> mSamplerType = std::nullopt;
double mStep;
double mStep = 1.0;
Uniform_t mData;
@ -109,6 +121,11 @@ namespace fx
return value.value_or(std::get<Uniform<T>>(mData).getValue());
}
size_t getNumElements() const
{
return std::visit([&](auto&& arg) { ;return arg.isArray() ? arg.getArray().size() : 1; }, mData);
}
template <class T>
T getMin() const
{
@ -137,8 +154,7 @@ namespace fx
{
arg.mValue = value;
if (mStatic)
Settings::ShaderManager::get().setValue<T>(mTechniqueName, mName, value);
Settings::ShaderManager::get().setValue(mTechniqueName, mName, value);
}
else
{
@ -147,6 +163,28 @@ namespace fx
}, mData);
}
template <class T, class A>
void setValue(const std::vector<T, A>& value)
{
std::visit([&, value](auto&& arg) {
using U = typename std::decay_t<decltype(arg)>::value_type;
if (!arg.isArray() || arg.getArray().size() != value.size())
{
Log(Debug::Error) << "Attempting to set uniform array '" << mName << "' with mismatching array sizes";
return;
}
if constexpr (std::is_same_v<T, U>)
{
arg.mArray = value;
Settings::ShaderManager::get().setValue(mTechniqueName, mName, value);
}
else
Log(Debug::Warning) << "Attempting to set uniform array '" << mName << "' with wrong type";
}, mData);
}
void setUniform(osg::Uniform* uniform)
{
auto type = getType();
@ -155,8 +193,14 @@ namespace fx
std::visit([&](auto&& arg)
{
const auto value = arg.getValue();
uniform->set(value);
if (arg.isArray())
{
for (size_t i = 0; i < arg.getArray().size(); ++i)
uniform->setElement(i, arg.getArray()[i]);
uniform->dirty();
}
else
uniform->set(arg.getValue());
}, mData);
}
@ -197,52 +241,53 @@ namespace fx
}
}
bool useUniform = (Settings::ShaderManager::get().getMode() == Settings::ShaderManager::Mode::Debug || mStatic == false);
return std::visit([&](auto&& arg) -> std::optional<std::string> {
using T = typename std::decay_t<decltype(arg)>::value_type;
auto value = arg.getValue();
const bool useUniform = arg.isArray() || (Settings::ShaderManager::get().getMode() == Settings::ShaderManager::Mode::Debug || mStatic == false);
const std::string uname = arg.isArray() ? Misc::StringUtils::format("%s[%zu]", mName, arg.getArray().size()) : mName;
if constexpr (std::is_same_v<T, osg::Vec2f>)
{
if (useUniform)
return Misc::StringUtils::format("uniform vec2 %s;", mName);
return Misc::StringUtils::format("uniform vec2 %s;", uname);
return Misc::StringUtils::format("const vec2 %s=vec2(%f,%f);", mName, value[0], value[1]);
}
else if constexpr (std::is_same_v<T, osg::Vec3f>)
{
if (useUniform)
return Misc::StringUtils::format("uniform vec3 %s;", mName);
return Misc::StringUtils::format("uniform vec3 %s;", uname);
return Misc::StringUtils::format("const vec3 %s=vec3(%f,%f,%f);", mName, value[0], value[1], value[2]);
}
else if constexpr (std::is_same_v<T, osg::Vec4f>)
{
if (useUniform)
return Misc::StringUtils::format("uniform vec4 %s;", mName);
return Misc::StringUtils::format("uniform vec4 %s;", uname);
return Misc::StringUtils::format("const vec4 %s=vec4(%f,%f,%f,%f);", mName, value[0], value[1], value[2], value[3]);
}
else if constexpr (std::is_same_v<T, float>)
{
if (useUniform)
return Misc::StringUtils::format("uniform float %s;", mName);
return Misc::StringUtils::format("uniform float %s;", uname);
return Misc::StringUtils::format("const float %s=%f;", mName, value);
}
else if constexpr (std::is_same_v<T, int>)
{
if (useUniform)
return Misc::StringUtils::format("uniform int %s;", mName);
return Misc::StringUtils::format("uniform int %s;", uname);
return Misc::StringUtils::format("const int %s=%i;", mName, value);
}
else if constexpr (std::is_same_v<T, bool>)
{
if (useUniform)
return Misc::StringUtils::format("uniform bool %s;", mName);
return Misc::StringUtils::format("uniform bool %s;", uname);
return Misc::StringUtils::format("const bool %s=%s;", mName, value ? "true" : "false");
}

@ -5,6 +5,7 @@
#include <filesystem>
#include <optional>
#include <fstream>
#include <vector>
#include <yaml-cpp/yaml.h>

@ -271,10 +271,10 @@ Below is an example of passing a value through a custom vertex shader to the fra
}
fragment pass {
omw_Out vec2 omw_TexCoord;
omw_In vec2 omw_TexCoord;
// our custom output from the vertex shader is available
omw_Out float noise;
omw_In float noise;
void main()
{
@ -461,6 +461,14 @@ To use the uniform you can reference it in any pass, it should **not** be declar
}
}
You can use uniform arrays as well, but they are restricted to the `Lua API <../lua-scripting/openmw_postprocessing.html>`_ scripts.
These uniform blocks must be defined with the new ``size`` parameter.
.. code-block:: none
uniform_vec3 uArray {
size = 10;
}
``render_target``
*****************

@ -7,7 +7,7 @@ Overview
OpenMW supports a moddable post process framework for creating and
controlling screenspace effects. This is integrated into OpenMW's Lua API, see
`reference <../lua-scripting/openmw_shader.html>`_ for details.
`reference <../lua-scripting/openmw_postprocessing.html>`_ for details.
Basic concepts
==============

@ -1,19 +1,18 @@
---
-- `openmw.postprocessing` is an interface to postprocessing shaders.
-- Can be used only by local scripts, that are attached to a player.
-- @module shader
-- @module postprocessing
-- @usage local postprocessing = require('openmw.postprocessing')
---
-- Load a shader and return its handle.
-- @function [parent=#postprocessing] load
-- @param #string name Name of the shader without its extension
-- @return #Shader
-- @usage
-- If the shader exists and compiles, the shader will still be off by default.
-- It must be enabled to see its effect.
-- -- If the shader exists and compiles, the shader will still be off by default.
-- -- It must be enabled to see its effect.
-- local vignetteShader = postprocessing.load('vignette')
---
@ -72,21 +71,62 @@
-- @function [parent=#Shader] setVector2
-- @param self
-- @param #string name Name of uniform
-- @param #Vector2 value Value of uniform.
-- @param openmw.util#Vector2 value Value of uniform.
---
-- Set a non static Vector3 shader variable.
-- @function [parent=#Shader] setVector3
-- @param self
-- @param #string name Name of uniform
-- @param #Vector3 value Value of uniform.
-- @param openmw.util#Vector3 value Value of uniform.
---
-- Set a non static Vector4 shader variable.
-- @function [parent=#Shader] setVector4
-- @param self
-- @param #string name Name of uniform
-- @param #Vector4 value Value of uniform.
-- @param openmw.util#Vector4 value Value of uniform.
---
-- Set a non static integer array shader variable.
-- @function [parent=#Shader] setIntArray
-- @param self
-- @param #string name Name of uniform
-- @param #table array Contains equal number of #number elements as the uniform array.
---
-- Set a non static float array shader variable.
-- @function [parent=#Shader] setFloatArray
-- @param self
-- @param #string name Name of uniform
-- @param #table array Contains equal number of #number elements as the uniform array.
---
-- Set a non static Vector2 array shader variable.
-- @function [parent=#Shader] setVector2Array
-- @param self
-- @param #string name Name of uniform
-- @param #table array Contains equal number of @{openmw.util#Vector2} elements as the uniform array.
---
-- Set a non static Vector3 array shader variable.
-- @function [parent=#Shader] setVector3Array
-- @param self
-- @param #string name Name of uniform
-- @param #table array Contains equal number of @{openmw.util#Vector3} elements as the uniform array.
---
-- Set a non static Vector4 array shader variable.
-- @function [parent=#Shader] setVector4Array
-- @param self
-- @param #string name Name of uniform
-- @param #table array Contains equal number of @{openmw.util#Vector4} elements as the uniform array.
-- @usage
-- -- Setting an array
-- local shader = postprocessing.load('godrays')
-- -- Toggle shader on
-- shader:enable()
-- -- Set new array uniform which was defined with length 2
-- shader:setVector4Array('myArray', { util.vector4(1,0,0,1), util.vector4(1,0,1,1) })
return nil

Loading…
Cancel
Save