1152 lines
35 KiB
C++
1152 lines
35 KiB
C++
|
|
|
|
#include "stdfx/shaderfx.h"
|
|
|
|
// TnzStdfx includes
|
|
#include "stdfx.h"
|
|
#include "stdfx/shaderinterface.h"
|
|
#include "stdfx/shadingcontext.h"
|
|
|
|
// TnzBase includes
|
|
#include "tfxparam.h"
|
|
#include "tparamset.h"
|
|
#include "trenderresourcemanager.h"
|
|
#include "trenderer.h"
|
|
|
|
// TnzCore includes
|
|
#include "tthread.h"
|
|
#include "tfilepath.h"
|
|
#include "tstream.h"
|
|
#include "tfunctorinvoker.h"
|
|
#include "tmsgcore.h"
|
|
|
|
// Qt includes
|
|
#include <QDir>
|
|
#include <QGLShaderProgram>
|
|
#include <QCoreApplication>
|
|
|
|
// Glew include
|
|
#include <GL/glew.h>
|
|
|
|
// tcg includes
|
|
#include "tcg/tcg_function_types.h"
|
|
#include "tcg/tcg_unique_ptr.h"
|
|
|
|
// Boost includes
|
|
#include <boost/any.hpp>
|
|
#include <boost/iterator/transform_iterator.hpp>
|
|
#include <boost/ptr_container/ptr_vector.hpp>
|
|
|
|
// Diagnostics include
|
|
//#define DIAGNOSTICS
|
|
#ifdef DIAGNOSTICS
|
|
#include "diagnostics.h"
|
|
#endif
|
|
|
|
//===================================================
|
|
|
|
// Forward Declarations
|
|
|
|
class ShaderFxDeclaration;
|
|
|
|
//===================================================
|
|
|
|
//****************************************************************************
|
|
// Local Namespace stuff
|
|
//****************************************************************************
|
|
|
|
namespace
|
|
{
|
|
|
|
// Classes
|
|
|
|
struct ContextLocker {
|
|
ShadingContext &m_ctx;
|
|
bool m_locked;
|
|
|
|
public:
|
|
ContextLocker(ShadingContext &ctx)
|
|
: m_ctx(ctx), m_locked(false) { relock(); }
|
|
~ContextLocker()
|
|
{
|
|
if (m_locked)
|
|
unlock();
|
|
}
|
|
|
|
void relock()
|
|
{
|
|
assert(!m_locked), m_locked = true;
|
|
m_ctx.makeCurrent();
|
|
}
|
|
|
|
void unlock()
|
|
{
|
|
assert(m_locked), m_locked = false;
|
|
m_ctx.doneCurrent();
|
|
}
|
|
};
|
|
|
|
struct ProgramBinder {
|
|
QGLShaderProgram *m_prog;
|
|
|
|
public:
|
|
ProgramBinder(QGLShaderProgram *prog) : m_prog(prog) { m_prog->bind(); }
|
|
~ProgramBinder() { m_prog->release(); }
|
|
};
|
|
|
|
struct RectF {
|
|
GLfloat m_val[4];
|
|
RectF(GLfloat x0, GLfloat y0, GLfloat x1, GLfloat y1) { m_val[0] = x0, m_val[1] = y0, m_val[2] = x1, m_val[3] = y1; }
|
|
RectF(const TRectD &rect) { m_val[0] = rect.x0, m_val[1] = rect.y0, m_val[2] = rect.x1, m_val[3] = rect.y1; }
|
|
|
|
operator TRectD() const { return TRectD(m_val[0], m_val[1], m_val[2], m_val[3]); }
|
|
bool operator==(const RectF &rect) const { return (memcmp(m_val, rect.m_val, sizeof(this)) == 0); }
|
|
};
|
|
|
|
struct AffineF {
|
|
GLfloat m_val[9];
|
|
operator TAffine() const { return TAffine(m_val[0], m_val[3], m_val[6], m_val[1], m_val[4], m_val[7]); }
|
|
// Observe that mat3 from GLSL stores elements column-wise; this explains the weird indexing
|
|
};
|
|
|
|
// Global Variables
|
|
|
|
typedef std::map<QString, ShaderFxDeclaration *> FxDeclarationsMap;
|
|
FxDeclarationsMap l_shaderFxDeclarations;
|
|
|
|
enum Measures { NONE,
|
|
PERCENT,
|
|
LENGTH,
|
|
ANGLE,
|
|
MEASURESCOUNT };
|
|
|
|
static const std::string l_measureNames[MEASURESCOUNT] = {
|
|
"", "percentage", "fxLength", "angle"};
|
|
|
|
static const TParamUIConcept::Type l_conceptTypes[ShaderInterface::CONCEPTSCOUNT - ShaderInterface::UI_CONCEPTS] = {
|
|
TParamUIConcept::RADIUS, TParamUIConcept::WIDTH, TParamUIConcept::ANGLE, TParamUIConcept::POINT,
|
|
TParamUIConcept::POINT_2, TParamUIConcept::VECTOR, TParamUIConcept::POLAR, TParamUIConcept::SIZE,
|
|
TParamUIConcept::QUAD, TParamUIConcept::RECT};
|
|
|
|
// Functions
|
|
|
|
inline bool isObsolete(const TFilePath &fp, const QDateTime &lastModified)
|
|
{
|
|
QFileInfo fInfo(QString::fromStdWString(fp.getWideString()));
|
|
return (lastModified != fInfo.lastModified());
|
|
}
|
|
|
|
inline TRectD tileRect(const TTile &tile)
|
|
{
|
|
const TDimension &dim = tile.getRaster()->getSize();
|
|
return TRectD(tile.m_pos, TDimensionD(dim.lx, dim.ly));
|
|
}
|
|
|
|
inline void ceilRect(TRectD &rect)
|
|
{
|
|
rect.x0 = tfloor(rect.x0), rect.y0 = tfloor(rect.y0);
|
|
rect.x1 = tceil(rect.x1), rect.y1 = tceil(rect.y1);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
//****************************************************************************
|
|
// Shader Fx declaration
|
|
//****************************************************************************
|
|
|
|
class ShaderFx : public TStandardZeraryFx
|
|
{
|
|
FX_PLUGIN_DECLARATION(ShaderFx)
|
|
|
|
const ShaderInterface *m_shaderInterface; //!< Shader fx 'description'.
|
|
std::vector<boost::any> m_params; //!< Parameters for the shader fx. The actual parameter
|
|
//!< type depends on the shader interface declaration.
|
|
std::vector<TParamUIConcept> m_uiConcepts; //!< UI concepts related to m_params.
|
|
boost::ptr_vector<TRasterFxPort> m_inputPorts; //!< Input ports for the shader fx.
|
|
|
|
public:
|
|
ShaderFx() : m_shaderInterface() { assert(false); } // Necessary due to TPersist inheritance, but must NOT be used
|
|
ShaderFx(const ShaderInterface *shaderInterface)
|
|
: m_shaderInterface(shaderInterface) { initialize(); }
|
|
|
|
//void setShaderInterface(const ShaderInterface& shaderInterface);
|
|
void initialize();
|
|
|
|
void getParamUIs(TParamUIConcept *¶ms, int &length);
|
|
bool doGetBBox(double frame, TRectD &bBox, const TRenderSettings &info);
|
|
bool canHandle(const TRenderSettings &info, double frame);
|
|
|
|
void doDryCompute(TRectD &rect, double frame, const TRenderSettings &ri);
|
|
void doCompute(TTile &tile, double frame, const TRenderSettings &ri);
|
|
|
|
private:
|
|
QGLShaderProgram *touchShaderProgram(
|
|
const ShaderInterface::ShaderData &sd,
|
|
ShadingContext &context,
|
|
int varyingsCount = 0, const GLchar **varyings = 0);
|
|
|
|
void bindParameters(QGLShaderProgram *shaderProgram, double frame);
|
|
|
|
void bindWorldTransform(QGLShaderProgram *shaderProgram, const TAffine &worldToDst);
|
|
|
|
void getInputData(const TRectD &rect, double frame, const TRenderSettings &ri,
|
|
std::vector<TRectD> &inputRects, std::vector<TAffine> &inputAffines,
|
|
ShadingContext &context);
|
|
};
|
|
|
|
//****************************************************************************
|
|
// ShaderFxDeclaration definition
|
|
//****************************************************************************
|
|
|
|
class ShaderFxDeclaration : public TFxDeclaration
|
|
{
|
|
ShaderInterface m_shaderInterface;
|
|
|
|
public:
|
|
ShaderFxDeclaration(const ShaderInterface &shaderInterface)
|
|
: TFxDeclaration(TFxInfo(
|
|
shaderInterface.mainShader().m_name.toStdString(), false)),
|
|
m_shaderInterface(shaderInterface) {}
|
|
|
|
TPersist *create() const
|
|
{
|
|
return new ShaderFx(&m_shaderInterface);
|
|
}
|
|
};
|
|
|
|
//****************************************************************************
|
|
// ShadingContextManager definition
|
|
//****************************************************************************
|
|
|
|
class ShadingContextManager : public QObject
|
|
{
|
|
mutable QMutex m_mutex;
|
|
|
|
ShadingContext m_shadingContext;
|
|
TAtomicVar m_activeRenderInstances;
|
|
|
|
public:
|
|
ShadingContextManager()
|
|
{
|
|
/*
|
|
The ShadingContext's QGLPixelBuffer must be destroyed *before* the global QApplication
|
|
is. So, we will attach to a suitable parent object whose lifespan is shorter.
|
|
|
|
FYI - yes, this approach was adopted after a long and PAINFUL wrestling session with Qt.
|
|
Suggestions are welcome as this is a tad beyond ridiculous...
|
|
*/
|
|
|
|
QObject *mainScopeBoundObject = QCoreApplication::instance()->findChild<QObject *>("mainScope");
|
|
|
|
assert(thread() == mainScopeBoundObject->thread()); // Parent object must be in the same thread,
|
|
setParent(mainScopeBoundObject); // otherwise reparenting fails
|
|
}
|
|
|
|
static ShadingContextManager *instance()
|
|
{
|
|
static ShadingContextManager *theManager = new ShadingContextManager;
|
|
return theManager;
|
|
}
|
|
|
|
QMutex *mutex() const { return &m_mutex; }
|
|
|
|
const ShadingContext &shadingContext() const { return m_shadingContext; }
|
|
ShadingContext &shadingContext() { return m_shadingContext; }
|
|
|
|
void onRenderInstanceStart()
|
|
{
|
|
++m_activeRenderInstances;
|
|
}
|
|
|
|
void onRenderInstanceEnd()
|
|
{
|
|
if (--m_activeRenderInstances == 0) {
|
|
QMutexLocker mLocker(&m_mutex);
|
|
|
|
// Release the shading context's output buffer
|
|
::ContextLocker cLocker(m_shadingContext);
|
|
m_shadingContext.resize(0, 0);
|
|
|
|
#ifdef DIAGNOSTICS
|
|
DIAGNOSTICS_DUMP("ShaderLogs");
|
|
DIAGNOSTICS_CLEAR;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
ShadingContext::Support touchSupport()
|
|
{
|
|
struct {
|
|
ShadingContextManager *m_this;
|
|
ShadingContext::Support support()
|
|
{
|
|
QMutexLocker mLocker(&m_this->m_mutex);
|
|
::ContextLocker cLocker(m_this->m_shadingContext);
|
|
|
|
return ShadingContext::support();
|
|
}
|
|
} locals = {this};
|
|
|
|
static ShadingContext::Support sup = locals.support();
|
|
|
|
static bool sentMsg = false;
|
|
if (!sentMsg) {
|
|
switch (sup) {
|
|
case ShadingContext::NO_PIXEL_BUFFER:
|
|
DVGui::warning(QGLShaderProgram::tr(
|
|
"This system configuration does not support OpenGL Pixel Buffers. Shader Fxs will not be able to render."));
|
|
|
|
CASE ShadingContext::NO_SHADERS : DVGui::warning(QGLShaderProgram::tr(
|
|
"This system configuration does not support OpenGL Shader Programs. Shader Fxs will not be able to render."));
|
|
|
|
DEFAULT:;
|
|
}
|
|
|
|
sentMsg = true;
|
|
}
|
|
|
|
return sup;
|
|
}
|
|
};
|
|
|
|
template class DV_EXPORT_API TFxDeclarationT<ShaderFx>;
|
|
|
|
//****************************************************************************
|
|
// ShadingContextManagerDelegate definition
|
|
//****************************************************************************
|
|
|
|
class MessageCreateContext : public TThread::Message
|
|
{
|
|
ShadingContextManager *man;
|
|
|
|
public:
|
|
MessageCreateContext(ShadingContextManager *ctx)
|
|
: man(ctx) {}
|
|
|
|
void onDeliver()
|
|
{
|
|
man->onRenderInstanceEnd();
|
|
}
|
|
|
|
TThread::Message *clone() const
|
|
{
|
|
return new MessageCreateContext(*this);
|
|
}
|
|
};
|
|
|
|
class SCMDelegate : public TRenderResourceManager
|
|
{
|
|
T_RENDER_RESOURCE_MANAGER
|
|
|
|
void onRenderInstanceStart(unsigned long id)
|
|
{
|
|
ShadingContextManager::instance()->onRenderInstanceStart();
|
|
}
|
|
|
|
void onRenderInstanceEnd(unsigned long id)
|
|
{
|
|
if (!TThread::isMainThread()) {
|
|
/* tofflinegl のときとは逆で main thread に dispatch する */
|
|
MessageCreateContext(ShadingContextManager::instance()).sendBlocking();
|
|
} else {
|
|
ShadingContextManager::instance()->onRenderInstanceEnd();
|
|
}
|
|
}
|
|
};
|
|
|
|
//-------------------------------------------------------------------
|
|
|
|
class SCMDelegateGenerator : public TRenderResourceManagerGenerator
|
|
{
|
|
public:
|
|
SCMDelegateGenerator() : TRenderResourceManagerGenerator(false)
|
|
{
|
|
/*
|
|
Again, this has to do with the manager's lifetime issue.
|
|
The SCM must be created in the MAIN THREAD, but NOT BEFORE the
|
|
QCoreApplication itself has been created. The easiest way to do so
|
|
is scheduling a slot to be executed as soon as event processing starts.
|
|
*/
|
|
|
|
struct InstanceSCM : public TFunctorInvoker::BaseFunctor {
|
|
void operator()() { ShadingContextManager::instance(); }
|
|
};
|
|
|
|
TFunctorInvoker::instance()->invokeQueued(new InstanceSCM);
|
|
}
|
|
|
|
TRenderResourceManager *operator()() { return new SCMDelegate; }
|
|
};
|
|
|
|
MANAGER_FILESCOPE_DECLARATION(SCMDelegate, SCMDelegateGenerator)
|
|
|
|
//****************************************************************************
|
|
// Shader Fx implementation
|
|
//****************************************************************************
|
|
|
|
void ShaderFx::initialize()
|
|
{
|
|
struct {
|
|
ShaderFx *m_this;
|
|
|
|
inline void addUiConcept(const ShaderInterface::Parameter &siParam, const TParamP ¶m)
|
|
{
|
|
if (siParam.m_concept.m_type >= ShaderInterface::UI_CONCEPTS &&
|
|
siParam.m_concept.m_type < ShaderInterface::CONCEPTSCOUNT) {
|
|
m_this->m_uiConcepts.push_back(TParamUIConcept());
|
|
|
|
TParamUIConcept &uiConcept = m_this->m_uiConcepts.back();
|
|
uiConcept.m_type = ::l_conceptTypes[siParam.m_concept.m_type - ShaderInterface::UI_CONCEPTS];
|
|
uiConcept.m_label = siParam.m_concept.m_label.toStdString();
|
|
uiConcept.m_params.push_back(param);
|
|
}
|
|
}
|
|
|
|
inline void addUiConcept(const ShaderInterface::ParameterConcept &concept)
|
|
{
|
|
if (!concept.isUI() || concept.m_parameterNames.empty())
|
|
return;
|
|
|
|
TParamUIConcept uiConcept = {
|
|
::l_conceptTypes[concept.m_type - ShaderInterface::UI_CONCEPTS],
|
|
concept.m_label.toStdString()};
|
|
|
|
int n, nCount = int(concept.m_parameterNames.size());
|
|
for (n = 0; n != nCount; ++n) {
|
|
TParam *param = m_this->getParams()->getParam(concept.m_parameterNames[n].toStdString());
|
|
if (!param)
|
|
break;
|
|
|
|
uiConcept.m_params.push_back(param);
|
|
}
|
|
|
|
if (uiConcept.m_params.size() == concept.m_parameterNames.size())
|
|
m_this->m_uiConcepts.push_back(uiConcept);
|
|
}
|
|
|
|
} locals = {this};
|
|
|
|
assert(m_params.empty()); // Interfaces should not be re-set
|
|
|
|
// Allocate parameters following the specified interface
|
|
const std::vector<ShaderInterface::Parameter> &siParams =
|
|
m_shaderInterface->parameters();
|
|
|
|
int p, pCount = int(siParams.size());
|
|
m_params.reserve(pCount);
|
|
|
|
for (p = 0; p != pCount; ++p) {
|
|
const ShaderInterface::Parameter &siParam = siParams[p];
|
|
|
|
switch (siParam.m_type) {
|
|
case ShaderInterface::BOOL: {
|
|
TBoolParamP param(siParam.m_default.m_bool);
|
|
|
|
m_params.push_back(param);
|
|
bindParam(this, siParam.m_name.toStdString(),
|
|
*boost::unsafe_any_cast<TBoolParamP>(&m_params.back()));
|
|
}
|
|
|
|
CASE ShaderInterface::FLOAT:
|
|
{
|
|
TDoubleParamP param(siParam.m_default.m_float);
|
|
param->setValueRange(siParam.m_range[0].m_float, siParam.m_range[1].m_float);
|
|
|
|
locals.addUiConcept(siParam, param);
|
|
|
|
switch (siParam.m_concept.m_type) {
|
|
case ShaderInterface::PERCENT:
|
|
param->setMeasureName(l_measureNames[PERCENT]);
|
|
|
|
CASE ShaderInterface::LENGTH : case ShaderInterface::RADIUS_UI : case ShaderInterface::WIDTH_UI : case ShaderInterface::SIZE_UI : param->setMeasureName(l_measureNames[LENGTH]);
|
|
|
|
CASE ShaderInterface::ANGLE : case ShaderInterface::ANGLE_UI : param->setMeasureName(l_measureNames[ANGLE]);
|
|
|
|
DEFAULT:;
|
|
}
|
|
|
|
m_params.push_back(param);
|
|
bindParam(this, siParam.m_name.toStdString(),
|
|
*boost::unsafe_any_cast<TDoubleParamP>(&m_params.back()));
|
|
}
|
|
|
|
CASE ShaderInterface::VEC2:
|
|
{
|
|
TPointParamP param(TPointD(
|
|
siParam.m_default.m_vec2[0], siParam.m_default.m_vec2[1]));
|
|
|
|
param->getX()->setValueRange(siParam.m_range[0].m_vec2[0], siParam.m_range[1].m_vec2[0]);
|
|
param->getY()->setValueRange(siParam.m_range[0].m_vec2[1], siParam.m_range[1].m_vec2[1]);
|
|
|
|
locals.addUiConcept(siParam, param);
|
|
|
|
switch (siParam.m_concept.m_type) {
|
|
case ShaderInterface::PERCENT:
|
|
param->getX()->setMeasureName(l_measureNames[PERCENT]);
|
|
param->getY()->setMeasureName(l_measureNames[PERCENT]);
|
|
|
|
CASE ShaderInterface::LENGTH : case ShaderInterface::POINT : case ShaderInterface::POINT_UI : case ShaderInterface::VECTOR_UI : case ShaderInterface::WIDTH_UI : case ShaderInterface::SIZE_UI : param->getX()->setMeasureName(l_measureNames[LENGTH]);
|
|
param->getY()->setMeasureName(l_measureNames[LENGTH]);
|
|
|
|
CASE ShaderInterface::ANGLE : case ShaderInterface::ANGLE_UI : param->getX()->setMeasureName(l_measureNames[ANGLE]);
|
|
param->getY()->setMeasureName(l_measureNames[ANGLE]);
|
|
|
|
DEFAULT:;
|
|
}
|
|
|
|
m_params.push_back(param);
|
|
bindParam(this, siParam.m_name.toStdString(),
|
|
*boost::unsafe_any_cast<TPointParamP>(&m_params.back()));
|
|
}
|
|
|
|
CASE ShaderInterface::INT:
|
|
{
|
|
TIntParamP param(siParam.m_default.m_int);
|
|
param->setValueRange(siParam.m_range[0].m_int, siParam.m_range[1].m_int);
|
|
|
|
m_params.push_back(param);
|
|
bindParam(this, siParam.m_name.toStdString(),
|
|
*boost::unsafe_any_cast<TIntParamP>(&m_params.back()));
|
|
}
|
|
|
|
CASE ShaderInterface::RGBA:
|
|
{
|
|
TPixelParamP param(TPixel32(
|
|
siParam.m_default.m_rgba[0], siParam.m_default.m_rgba[1],
|
|
siParam.m_default.m_rgba[2], siParam.m_default.m_rgba[3]));
|
|
|
|
m_params.push_back(param);
|
|
bindParam(this, siParam.m_name.toStdString(),
|
|
*boost::unsafe_any_cast<TPixelParamP>(&m_params.back()));
|
|
}
|
|
|
|
CASE ShaderInterface::RGB:
|
|
{
|
|
TPixelParamP param(TPixel32(
|
|
siParam.m_default.m_rgb[0], siParam.m_default.m_rgb[1],
|
|
siParam.m_default.m_rgb[2]));
|
|
|
|
param->enableMatte(false);
|
|
|
|
m_params.push_back(param);
|
|
bindParam(this, siParam.m_name.toStdString(),
|
|
*boost::unsafe_any_cast<TPixelParamP>(&m_params.back()));
|
|
}
|
|
|
|
DEFAULT:;
|
|
}
|
|
}
|
|
|
|
// Add composite UI concepts
|
|
const std::vector<ShaderInterface::ParameterConcept> &parConcepts = m_shaderInterface->m_parConcepts;
|
|
|
|
int c, cCount = int(parConcepts.size());
|
|
for (c = 0; c != cCount; ++c)
|
|
locals.addUiConcept(parConcepts[c]);
|
|
|
|
// Add input ports
|
|
const std::vector<QString> &inputPorts = m_shaderInterface->inputPorts();
|
|
|
|
int i, iCount = int(inputPorts.size());
|
|
m_inputPorts.reserve(iCount);
|
|
|
|
for (i = 0; i != iCount; ++i) {
|
|
m_inputPorts.push_back(new TRasterFxPort);
|
|
addInputPort(inputPorts[i].toStdString(), m_inputPorts[i]);
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
|
|
void ShaderFx::getParamUIs(TParamUIConcept *¶ms, int &length)
|
|
{
|
|
length = int(m_uiConcepts.size());
|
|
params = new TParamUIConcept[length];
|
|
|
|
std::copy(m_uiConcepts.begin(), m_uiConcepts.end(), params);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
|
|
bool ShaderFx::doGetBBox(double frame, TRectD &bbox, const TRenderSettings &info)
|
|
{
|
|
static const ::RectF infiniteRectF(
|
|
-(std::numeric_limits<GLfloat>::max)(), -(std::numeric_limits<GLfloat>::max)(),
|
|
(std::numeric_limits<GLfloat>::max)(), (std::numeric_limits<GLfloat>::max)());
|
|
|
|
bbox = TConsts::infiniteRectD;
|
|
|
|
const ShaderInterface::ShaderData &sd = m_shaderInterface->bboxShader();
|
|
if (!sd.isValid())
|
|
return true;
|
|
|
|
ShadingContextManager *manager = ShadingContextManager::instance();
|
|
if (manager->touchSupport() != ShadingContext::OK)
|
|
return true;
|
|
|
|
//Remember: info.m_affine MUST NOT BE CONSIDERED in doGetBBox's implementation
|
|
::RectF bboxF(infiniteRectF);
|
|
|
|
QMutexLocker mLocker(manager->mutex());
|
|
|
|
//ShadingContext& context = manager->shadingContext();
|
|
std::shared_ptr<ShadingContext> shadingContextPtr(new ShadingContext);
|
|
ShadingContext &context = *shadingContextPtr.get();
|
|
|
|
::ContextLocker cLocker(context);
|
|
|
|
// Build the varyings data
|
|
QGLShaderProgram *prog = 0;
|
|
{
|
|
const GLchar *varyingNames[] = {"outputBBox"};
|
|
prog = touchShaderProgram(sd, context, 1, &varyingNames[0]);
|
|
}
|
|
|
|
int pCount = getInputPortCount();
|
|
|
|
std::vector<RectF> inputBBoxes(pCount, ::RectF(TRectD()));
|
|
|
|
for (int p = 0; p != pCount; ++p) {
|
|
TRasterFxPort &port = m_inputPorts[p];
|
|
if (port.isConnected()) {
|
|
TRectD inputBBox;
|
|
|
|
cLocker.unlock();
|
|
mLocker.unlock();
|
|
|
|
if (port->doGetBBox(frame, inputBBox, info))
|
|
inputBBoxes[p] = (inputBBox == TConsts::infiniteRectD) ? infiniteRectF : ::RectF(inputBBox);
|
|
|
|
mLocker.relock();
|
|
cLocker.relock();
|
|
}
|
|
}
|
|
|
|
{
|
|
ProgramBinder progBinder(prog);
|
|
|
|
// Bind uniform parameters
|
|
bindParameters(prog, frame);
|
|
|
|
prog->setUniformValue("infiniteRect",
|
|
infiniteRectF.m_val[0], infiniteRectF.m_val[1], infiniteRectF.m_val[2], infiniteRectF.m_val[3]);
|
|
|
|
prog->setUniformValueArray("inputBBox", inputBBoxes[0].m_val,
|
|
int(inputBBoxes.size()), 4);
|
|
|
|
// Perform transform feedback
|
|
const GLsizeiptr varyingSizes[] = {sizeof(::RectF)};
|
|
GLvoid *bufs[] = {bboxF.m_val};
|
|
|
|
context.transformFeedback(1, varyingSizes, bufs);
|
|
}
|
|
|
|
// Finalize output
|
|
bbox = (bboxF == infiniteRectF) ? TConsts::infiniteRectD : TRectD(bboxF);
|
|
return true;
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
|
|
bool ShaderFx::canHandle(const TRenderSettings &info, double frame)
|
|
{
|
|
return (m_shaderInterface->hwtType() == ShaderInterface::ANY) ? true : isAlmostIsotropic(info.m_affine);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
|
|
QGLShaderProgram *ShaderFx::touchShaderProgram(
|
|
const ShaderInterface::ShaderData &sd,
|
|
ShadingContext &context,
|
|
int varyingsCount, const GLchar **varyings)
|
|
{
|
|
typedef std::pair<QGLShaderProgram *, QDateTime> CompiledShader;
|
|
|
|
struct locals {
|
|
inline static void logCompilation(QGLShaderProgram *program)
|
|
{
|
|
// Log shaders - observe that we'll look into the program's *children*, not its
|
|
// shaders. This is necessary as uncompiled shaders are not added to the program.
|
|
const QObjectList &children = program->children();
|
|
|
|
int c, cCount = children.size();
|
|
for (c = 0; c != cCount; ++c) {
|
|
if (QGLShader *shader = dynamic_cast<QGLShader *>(children[c])) {
|
|
const QString &log = shader->log();
|
|
if (!log.isEmpty())
|
|
DVGui::info(log);
|
|
}
|
|
}
|
|
|
|
// ShaderProgram linking logs
|
|
const QString &log = program->log();
|
|
if (!log.isEmpty())
|
|
DVGui::info(log);
|
|
}
|
|
}; // locals
|
|
|
|
//ShadingContext& context = ShadingContextManager::instance()->shadingContext();
|
|
|
|
CompiledShader cs = context.shaderData(sd.m_name);
|
|
if (!cs.first || ::isObsolete(sd.m_path, cs.second)) {
|
|
cs = m_shaderInterface->makeProgram(sd, varyingsCount, varyings);
|
|
context.addShaderProgram(sd.m_name, cs.first, cs.second);
|
|
|
|
locals::logCompilation(cs.first);
|
|
}
|
|
|
|
assert(cs.first);
|
|
return cs.first;
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
|
|
void ShaderFx::bindParameters(
|
|
QGLShaderProgram *program, double frame)
|
|
{
|
|
// Bind fx parameters
|
|
const std::vector<ShaderInterface::Parameter> &siParams =
|
|
m_shaderInterface->parameters();
|
|
|
|
assert(siParams.size() == m_params.size());
|
|
|
|
int p, pCount = int(siParams.size());
|
|
for (p = 0; p != pCount; ++p) {
|
|
const ShaderInterface::Parameter &siParam = siParams[p];
|
|
|
|
switch (siParam.m_type) {
|
|
case ShaderInterface::BOOL: {
|
|
const TBoolParamP ¶m = *boost::unsafe_any_cast<TBoolParamP>(&m_params[p]);
|
|
program->setUniformValue(siParam.m_name.toUtf8().data(), (GLboolean)param->getValue());
|
|
}
|
|
|
|
CASE ShaderInterface::FLOAT:
|
|
{
|
|
const TDoubleParamP ¶m = *boost::unsafe_any_cast<TDoubleParamP>(&m_params[p]);
|
|
program->setUniformValue(siParam.m_name.toUtf8().data(),
|
|
(GLfloat)param->getValue(frame));
|
|
}
|
|
|
|
CASE ShaderInterface::VEC2:
|
|
{
|
|
const TPointParamP ¶m = *boost::unsafe_any_cast<TPointParamP>(&m_params[p]);
|
|
|
|
const TPointD &value = param->getValue(frame);
|
|
program->setUniformValue(siParam.m_name.toUtf8().data(),
|
|
(GLfloat)value.x, (GLfloat)value.y);
|
|
}
|
|
|
|
CASE ShaderInterface::INT:
|
|
{
|
|
const TIntParamP ¶m = *boost::unsafe_any_cast<TIntParamP>(&m_params[p]);
|
|
program->setUniformValue(siParam.m_name.toUtf8().data(), (GLint)param->getValue());
|
|
}
|
|
|
|
CASE ShaderInterface::RGBA : case ShaderInterface::RGB:
|
|
{
|
|
const TPixelParamP ¶m = *boost::unsafe_any_cast<TPixelParamP>(&m_params[p]);
|
|
|
|
const TPixel32 &value = param->getValue(frame);
|
|
program->setUniformValue(siParam.m_name.toUtf8().data(),
|
|
(GLfloat)value.r / 255.0f, (GLfloat)value.g / 255.0f,
|
|
(GLfloat)value.b / 255.0f, (GLfloat)value.m / 255.0f);
|
|
}
|
|
|
|
DEFAULT:;
|
|
}
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
|
|
void ShaderFx::bindWorldTransform(QGLShaderProgram *program, const TAffine &worldToDst)
|
|
{
|
|
// Bind transformation affine
|
|
#if QT_VERSION >= 0x050500
|
|
float qwToD[9] = {static_cast<float>(worldToDst.a11), static_cast<float>(worldToDst.a12), static_cast<float>(worldToDst.a13),
|
|
static_cast<float>(worldToDst.a21), static_cast<float>(worldToDst.a22), static_cast<float>(worldToDst.a23),
|
|
0.0f, 0.0f, 1.0f};
|
|
#else
|
|
qreal qwToD[9] = {worldToDst.a11, worldToDst.a12, worldToDst.a13,
|
|
worldToDst.a21, worldToDst.a22, worldToDst.a23,
|
|
0.0, 0.0, 1.0};
|
|
#endif
|
|
program->setUniformValue("worldToOutput", QMatrix3x3(qwToD));
|
|
|
|
const TAffine &dToW = worldToDst.inv();
|
|
#if QT_VERSION >= 0x050500
|
|
float qdToW[9] = {static_cast<float>(dToW.a11), static_cast<float>(dToW.a12), static_cast<float>(dToW.a13),
|
|
static_cast<float>(dToW.a21), static_cast<float>(dToW.a22), static_cast<float>(dToW.a23),
|
|
0.0f, 0.0f, 1.0f};
|
|
#else
|
|
qreal qdToW[9] = {dToW.a11, dToW.a12, dToW.a13,
|
|
dToW.a21, dToW.a22, dToW.a23,
|
|
0.0, 0.0, 1.0};
|
|
#endif
|
|
program->setUniformValue("outputToWorld", QMatrix3x3(qdToW));
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
|
|
void ShaderFx::getInputData(const TRectD &rect, double frame, const TRenderSettings &ri,
|
|
std::vector<TRectD> &inputRects, std::vector<TAffine> &inputAffines,
|
|
ShadingContext &context)
|
|
{
|
|
struct locals {
|
|
static inline void addNames(std::vector<std::string> &names, const char *prefix, int pCount)
|
|
{
|
|
for (int p = 0; p != pCount; ++p)
|
|
names.push_back((prefix + QString("[%1]").arg(p)).toStdString());
|
|
}
|
|
};
|
|
|
|
const ShaderInterface::ShaderData &sd = m_shaderInterface->inputPortsShader();
|
|
if (!sd.isValid()) {
|
|
inputRects.resize(getInputPortCount());
|
|
std::fill(inputRects.begin(), inputRects.end(), rect);
|
|
|
|
inputAffines.resize(getInputPortCount());
|
|
std::fill(inputAffines.begin(), inputAffines.end(), ri.m_affine);
|
|
|
|
return;
|
|
}
|
|
|
|
//ShadingContext& context = ShadingContextManager::instance()->shadingContext();
|
|
|
|
std::vector<GLfloat> buf;
|
|
int pCount = getInputPortCount();
|
|
|
|
// Build the varyings data
|
|
QGLShaderProgram *prog = 0;
|
|
{
|
|
// Unsubscripted varying arrays on transform feedback seems to be unsupported
|
|
// on ATI cards. We have to declare EACH array name - e.g. inputRect[0], intputRect[1], etc..
|
|
|
|
const GLchar *varyingPrefixes[] = {"inputRect", "worldToInput"};
|
|
const int varyingsCount = sizeof(varyingPrefixes) / sizeof(GLchar *);
|
|
|
|
std::vector<std::string> varyingStrings;
|
|
varyingStrings.reserve(varyingsCount);
|
|
|
|
for (int v = 0; v != varyingsCount; ++v)
|
|
locals::addNames(varyingStrings, varyingPrefixes[v], pCount);
|
|
|
|
#if defined(__APPLE_CC__)
|
|
/* OSX10.8 の clang -stdlib=libc++ だと link 時 &std::string::c_str が undefined になってしまう */
|
|
std::vector<const GLchar *> varyingNames(varyingStrings.size());
|
|
auto conv = [](const std::string &i) { return i.c_str(); };
|
|
std::transform(varyingStrings.begin(), varyingStrings.end(), varyingNames.begin(), conv);
|
|
#else
|
|
tcg::function<const char *(std::string::*)() const,
|
|
&std::string::c_str> c_str;
|
|
|
|
std::vector<const GLchar *> varyingNames(
|
|
boost::make_transform_iterator(varyingStrings.begin(), std::mem_fun_ref(&std::string::c_str)),
|
|
boost::make_transform_iterator(varyingStrings.end(), std::mem_fun_ref(&std::string::c_str)));
|
|
#endif
|
|
prog = touchShaderProgram(sd, context, int(varyingNames.size()), &varyingNames[0]);
|
|
}
|
|
|
|
{
|
|
ProgramBinder progBinder(prog);
|
|
|
|
// Build varying buffers
|
|
int bufFloatsCount = pCount * (sizeof(RectF) + sizeof(AffineF)) / sizeof(GLfloat);
|
|
buf.resize(bufFloatsCount);
|
|
|
|
// Bind uniform parameters
|
|
bindParameters(prog, frame);
|
|
bindWorldTransform(prog, ri.m_affine);
|
|
|
|
prog->setUniformValue("outputRect",
|
|
(GLfloat)rect.x0, (GLfloat)rect.y0, (GLfloat)rect.x1, (GLfloat)rect.y1);
|
|
|
|
// Perform transform feedback
|
|
const GLsizeiptr varyingSizes[] = {static_cast<GLsizeiptr>(bufFloatsCount * sizeof(GLfloat))};
|
|
GLvoid *bufs[] = {&buf[0]};
|
|
|
|
context.transformFeedback(1, varyingSizes, bufs);
|
|
|
|
#ifdef TRANSFORM_FEEDBACK_COUT
|
|
std::cout << "trFeedback: ";
|
|
for (int f = 0; f != bufFloatsCount; ++f)
|
|
std::cout << buf[f] << " ";
|
|
std::cout << "\n" << std::endl;
|
|
#endif
|
|
}
|
|
|
|
// Finalize output
|
|
const RectF *rBufBegin(reinterpret_cast<const RectF *>(&buf[0])), *rBufEnd(rBufBegin + pCount);
|
|
std::copy(rBufBegin, rBufEnd, &inputRects[0]);
|
|
|
|
const AffineF *aBufBegin(reinterpret_cast<const AffineF *>(rBufEnd)), *aBufEnd(aBufBegin + pCount);
|
|
std::copy(aBufBegin, aBufEnd, &inputAffines[0]);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
|
|
void ShaderFx::doCompute(TTile &tile, double frame, const TRenderSettings &info)
|
|
{
|
|
struct locals {
|
|
|
|
struct TexturesStorage {
|
|
ShadingContext &m_ctx;
|
|
std::vector<GLuint> m_texIds;
|
|
|
|
TexturesStorage(ShadingContext &ctx, int pCount)
|
|
: m_ctx(ctx)
|
|
{
|
|
m_texIds.reserve(pCount);
|
|
}
|
|
|
|
~TexturesStorage()
|
|
{
|
|
typedef tcg::function<void (ShadingContext::*)(GLuint),
|
|
&ShadingContext::unloadTexture> UnloadFunc;
|
|
|
|
std::for_each(m_texIds.begin(), m_texIds.end(),
|
|
tcg::bind1st(UnloadFunc(), m_ctx));
|
|
}
|
|
|
|
void load(const TRasterP &ras, GLuint texUnit)
|
|
{
|
|
if (ras)
|
|
m_texIds.push_back(m_ctx.loadTexture(ras, texUnit));
|
|
}
|
|
};
|
|
|
|
inline static QGLFramebufferObjectFormat makeFormat(int bpp)
|
|
{
|
|
QGLFramebufferObjectFormat fmt;
|
|
if (bpp == 64)
|
|
fmt.setInternalTextureFormat(GL_RGBA16);
|
|
return fmt;
|
|
}
|
|
|
|
inline static void touchOutputSize(ShadingContext &context, const TDimension &size, int bpp)
|
|
{
|
|
const QGLFramebufferObjectFormat &fmt = makeFormat(bpp);
|
|
|
|
const TDimension ¤tSize = context.size();
|
|
const QGLFramebufferObjectFormat ¤tFmt = context.format();
|
|
|
|
if (currentSize.lx < size.lx || currentSize.ly < size.ly ||
|
|
currentFmt != fmt)
|
|
context.resize(tmax(size.lx, currentSize.lx), tmax(size.ly, currentSize.ly), fmt);
|
|
}
|
|
}; // locals
|
|
|
|
ShadingContextManager *manager = ShadingContextManager::instance();
|
|
if (manager->touchSupport() != ShadingContext::OK)
|
|
return;
|
|
|
|
QMutexLocker mLocker(manager->mutex()); // As GPU access can be considered sequential anyway,
|
|
// lock the full-scale mutex
|
|
|
|
std::shared_ptr<ShadingContext> shadingContextPtr(new ShadingContext);
|
|
ShadingContext &context = *shadingContextPtr.get();
|
|
// ShadingContext& context = manager->shadingContext();
|
|
|
|
int pCount = getInputPortCount();
|
|
|
|
const TRectD &tileRect = ::tileRect(tile);
|
|
|
|
std::vector<TRectD> inputRects(pCount);
|
|
std::vector<TAffine> inputAffines(pCount);
|
|
|
|
// Calculate input tiles
|
|
::ContextLocker cLocker(context);
|
|
|
|
tcg::unique_ptr<TTile[]> inTiles(new TTile[pCount]); // NOTE: Input tiles must be STORED - they cannot
|
|
// be passed immediately to OpenGL, since *other shader
|
|
if (pCount > 0) // fxs*, with the very same host context, could lie
|
|
{ // inside this fx's input branches...
|
|
getInputData(tileRect, frame, info, inputRects, inputAffines, context);
|
|
|
|
// Release context and mutex
|
|
cLocker.unlock();
|
|
mLocker.unlock();
|
|
|
|
for (int p = 0; p != pCount; ++p) {
|
|
TRasterFxPort &port = m_inputPorts[p];
|
|
if (port.isConnected()) {
|
|
// Compute input tile
|
|
TRectD &inRect = inputRects[p];
|
|
if (inRect.getLx() > 0.0 && inRect.getLy() > 0.0) {
|
|
::ceilRect(inRect);
|
|
|
|
TRenderSettings inputInfo(info);
|
|
inputInfo.m_affine = inputAffines[p];
|
|
|
|
#ifdef TRANSFORM_FEEDBACK_COUT
|
|
const TAffine &inAff = inputAffines[p];
|
|
std::cout << "inRect " << p << ": " << inRect.x0 << " " << inRect.y0 << " " << inRect.x1 << " " << inRect.y1 << "\n";
|
|
std::cout << "inAff " << p << ": " << inAff.a11 << " " << inAff.a12 << " " << inAff.a13 << "\n";
|
|
std::cout << " " << inAff.a21 << " " << inAff.a22 << " " << inAff.a23 << "\n" << std::endl;
|
|
#endif
|
|
|
|
port->allocateAndCompute(inTiles[p], inRect.getP00(),
|
|
TDimension(tround(inRect.getLx()), tround(inRect.getLy())), tile.getRaster(), frame, inputInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load input tiles on the GPU as textures
|
|
mLocker.relock();
|
|
cLocker.relock();
|
|
|
|
// Input tiles are NOT supplied to OpenGL here - but rather just before drawing.
|
|
// It's probably beacuse a uniform integer variable must have already been bound
|
|
// to prepare the associated sampler variable in the linkes program...
|
|
}
|
|
|
|
// Perform the actual fragment shading
|
|
{
|
|
locals::touchOutputSize(context, tile.getRaster()->getSize(), info.m_bpp);
|
|
|
|
QGLShaderProgram *program = touchShaderProgram(m_shaderInterface->mainShader(), context);
|
|
{
|
|
ProgramBinder binder(program);
|
|
|
|
// Bind parameters and textures
|
|
bindParameters(program, frame);
|
|
bindWorldTransform(program, TTranslation(-tile.m_pos) * info.m_affine);
|
|
|
|
// Setup input data, if any
|
|
locals::TexturesStorage texStorage(context, pCount);
|
|
|
|
if (pCount > 0) {
|
|
std::vector<GLint> inputs(pCount);
|
|
std::vector<QMatrix3x3> screenToInput(pCount);
|
|
std::vector<QMatrix3x3> inputToScreen(pCount);
|
|
|
|
for (int p = 0; p != pCount; ++p) {
|
|
TAffine iToS(
|
|
TTranslation(-tile.m_pos) * // Output to Screen
|
|
info.m_affine * // World to Output
|
|
inputAffines[p].inv() * // Input to World
|
|
TTranslation(inputRects[p].getP00()) * // Texture to Input
|
|
TScale(inputRects[p].getLx(), inputRects[p].getLy())); //
|
|
|
|
TAffine sToI(iToS.inv());
|
|
|
|
#if QT_VERSION >= 0x050500
|
|
float qiToS[9] = {static_cast<float>(iToS.a11), static_cast<float>(iToS.a12), static_cast<float>(iToS.a13),
|
|
static_cast<float>(iToS.a21), static_cast<float>(iToS.a22), static_cast<float>(iToS.a23),
|
|
0.0f, 0.0f, 1.0f};
|
|
float qsToI[9] = {static_cast<float>(sToI.a11), static_cast<float>(sToI.a12), static_cast<float>(sToI.a13),
|
|
static_cast<float>(sToI.a21), static_cast<float>(sToI.a22), static_cast<float>(sToI.a23),
|
|
0.0f, 0.0f, 1.0f};
|
|
#else
|
|
qreal qiToS[9] = {iToS.a11, iToS.a12, iToS.a13,
|
|
iToS.a21, iToS.a22, iToS.a23,
|
|
0.0, 0.0, 1.0};
|
|
|
|
qreal qsToI[9] = {sToI.a11, sToI.a12, sToI.a13,
|
|
sToI.a21, sToI.a22, sToI.a23,
|
|
0.0, 0.0, 1.0};
|
|
#endif
|
|
inputs[p] = p, screenToInput[p] = QMatrix3x3(qsToI), inputToScreen[p] = QMatrix3x3(qiToS);
|
|
}
|
|
|
|
program->setUniformValueArray("inputImage", &inputs[0], pCount);
|
|
program->setUniformValueArray("outputToInput", &screenToInput[0], pCount);
|
|
program->setUniformValueArray("inputToOutput", &inputToScreen[0], pCount);
|
|
|
|
// Load textures
|
|
for (int p = 0; p != pCount; ++p)
|
|
texStorage.load(inTiles[p].getRaster(), p);
|
|
}
|
|
|
|
#ifdef DIAGNOSTICS
|
|
DIAGNOSTICS_TIMER("Shader Overall Times | " + m_shaderInterface->m_mainShader.m_name);
|
|
#endif
|
|
|
|
context.draw(tile.getRaster());
|
|
}
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
|
|
void ShaderFx::doDryCompute(TRectD &rect, double frame, const TRenderSettings &info)
|
|
{
|
|
ShadingContextManager *manager = ShadingContextManager::instance();
|
|
if (manager->touchSupport() != ShadingContext::OK)
|
|
return;
|
|
|
|
QMutexLocker mLocker(manager->mutex());
|
|
|
|
//ShadingContext& context = manager->shadingContext();
|
|
std::shared_ptr<ShadingContext> shadingContextPtr(new ShadingContext);
|
|
ShadingContext &context = *shadingContextPtr.get();
|
|
|
|
int pCount = getInputPortCount();
|
|
if (pCount > 0) {
|
|
::ContextLocker cLocker(context);
|
|
|
|
std::vector<TRectD> inputRects(pCount);
|
|
std::vector<TAffine> inputAffines(pCount);
|
|
|
|
getInputData(rect, frame, info, inputRects, inputAffines, context);
|
|
|
|
for (int p = 0; p != pCount; ++p) {
|
|
TRasterFxPort &port = m_inputPorts[p];
|
|
if (port.isConnected()) {
|
|
TRectD &inRect = inputRects[p];
|
|
if (inRect.getLx() > 0.0 && inRect.getLy() > 0.0) {
|
|
::ceilRect(inRect);
|
|
|
|
TRenderSettings inputInfo(info);
|
|
inputInfo.m_affine = inputAffines[p];
|
|
|
|
cLocker.unlock();
|
|
mLocker.unlock();
|
|
|
|
port->dryCompute(inRect, frame, inputInfo);
|
|
|
|
mLocker.relock();
|
|
cLocker.relock();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
const TPersistDeclaration *ShaderFx::getDeclaration() const
|
|
{
|
|
FxDeclarationsMap::iterator it = ::l_shaderFxDeclarations.find(
|
|
m_shaderInterface->mainShader().m_name);
|
|
|
|
return (it == ::l_shaderFxDeclarations.end()) ? 0 : it->second;
|
|
}
|
|
|
|
//****************************************************************************
|
|
// Shader Interfaces loading function
|
|
//****************************************************************************
|
|
|
|
void loadShaderInterfaces(const TFilePath &shadersFolder)
|
|
{
|
|
// Scan the shaders folder for xml (shader interface) files
|
|
QDir shadersDir(QString::fromStdWString(shadersFolder.getWideString()));
|
|
|
|
QStringList namesFilter("*.xml");
|
|
QStringList files = shadersDir.entryList(namesFilter, QDir::Files, QDir::Name | QDir::LocaleAware);
|
|
|
|
int f, fCount = files.size();
|
|
for (f = 0; f != fCount; ++f) {
|
|
TIStream is(shadersFolder + TFilePath(files[f].toStdWString()));
|
|
|
|
// Try to load a ShaderInterface instance for the file
|
|
ShaderInterface shaderInterface;
|
|
is >> shaderInterface;
|
|
|
|
if (shaderInterface.isValid()) {
|
|
// Store a ShaderFx factory for the interface
|
|
::l_shaderFxDeclarations.insert(std::make_pair(
|
|
shaderInterface.mainShader().m_name, new ShaderFxDeclaration(shaderInterface)));
|
|
}
|
|
}
|
|
}
|