diff --git a/Engine/source/shaderGen/NODE/shaderGenNodes.cpp b/Engine/source/shaderGen/NODE/shaderGenNodes.cpp index 8634bc32d..2ef78ab47 100644 --- a/Engine/source/shaderGen/NODE/shaderGenNodes.cpp +++ b/Engine/source/shaderGen/NODE/shaderGenNodes.cpp @@ -15,11 +15,13 @@ #include "materials/materialFeatureTypes.h" +ImplementFeatureType(SNF_VertexPosition, U32(-1), -1, false); ImplementFeatureType(SNF_DefaultTexCoord, U32(-1), -1, false); ImplementFeatureType(SNF_TextureFeature, U32(-1), -1, false); ImplementFeatureType(SNF_NormalMapFeature, U32(-1), -1, false); ImplementEnumType(ShaderNodeFeature_enum, "Shader node features. Each of thes relates to a specific node for generating a shader.\n\n") + { ShaderNodeFeature_enum::eSNF_VertexPosition, "SNF_VertexPosition", "Setup vertex position." }, { ShaderNodeFeature_enum::eSNF_DefaultTexCoord, "SNF_DefaultTexCoord", "Setup the default texcoord." }, { ShaderNodeFeature_enum::eSNF_TextureFeature, "SNF_TextureFeature", "Sample a Texture - Params: (string,string,GFXSamplerStateData,bool)." }, { ShaderNodeFeature_enum::eSNF_NormalMapFeature,"SNF_NormalMapFeature", "Convert a texture to a normalmap - Params: (string,float,bool,bool)." }, @@ -29,6 +31,7 @@ namespace { void register_node_features(GFXAdapterType type) { + FEATUREMGR->registerFeature(SNF_VertexPosition, new NodeVertexPositionFeature); FEATUREMGR->registerFeature(SNF_DefaultTexCoord, new DefaultTexcoordFeature); FEATUREMGR->registerFeature(SNF_TextureFeature, new TextureFeature, TextureFeature::createFunction); FEATUREMGR->registerFeature(SNF_NormalMapFeature, new NormalMapFeature, NormalMapFeature::createFunction); @@ -51,6 +54,125 @@ MODULE_INIT MODULE_END; +Var* ShaderFeatureNode::getObjTrans(Vector& componentList, + bool useInstancing, + MultiLine* meta) +{ + Var* objTrans = (Var*)LangElement::find("objTrans"); + if (objTrans) + return objTrans; + + if (useInstancing) + { + ShaderConnector* vertStruct = dynamic_cast(componentList[C_VERT_STRUCT]); + Var* instObjTrans = vertStruct->getElement(RT_TEXCOORD, 4, 4); + instObjTrans->setStructName("IN"); + instObjTrans->setName("inst_objectTrans"); + + mInstancingFormat->addElement("objTrans", GFXDeclType_Float4, instObjTrans->constNum + 0); + mInstancingFormat->addElement("objTrans", GFXDeclType_Float4, instObjTrans->constNum + 1); + mInstancingFormat->addElement("objTrans", GFXDeclType_Float4, instObjTrans->constNum + 2); + mInstancingFormat->addElement("objTrans", GFXDeclType_Float4, instObjTrans->constNum + 3); + + objTrans = new Var; + objTrans->setType(GFXSCT_Float4x4); + objTrans->setName("objTrans"); + + Vector matrixVars; + matrixVars.push_back(instObjTrans); + meta->addStatement(new GenOp(" @ = @;", new DecOp(objTrans), new MatrixInitializeOp(objTrans, matrixVars))); + } + else + { + objTrans = new Var; + objTrans->setType(GFXSCT_Float4x4); + objTrans->setName("objTrans"); + objTrans->uniform = true; + objTrans->constSortPos = cspPrimitive; + } + + return objTrans; +} + +Var* ShaderFeatureNode::getModelView( Vector& componentList, + bool useInstancing, + MultiLine* meta) +{ + Var* modelview = (Var*)LangElement::find("modelview"); + if (modelview) + return modelview; + + if (useInstancing) + { + Var* objTrans = getObjTrans(componentList, useInstancing, meta); + + Var* viewProj = (Var*)LangElement::find("viewProj"); + if (!viewProj) + { + viewProj = new Var; + viewProj->setType(GFXSCT_Float4x4); + viewProj->setName("viewProj"); + viewProj->uniform = true; + viewProj->constSortPos = cspPass; + } + + modelview = new Var; + modelview->setType(GFXSCT_Float4x4); + modelview->setName("modelview"); + meta->addStatement(new GenOp(" @ = @; // Instancing!\r\n", new DecOp(modelview), new MatrixMultiplyOp(viewProj, objTrans))); + } + else + { + modelview = new Var; + modelview->setType(GFXSCT_Float4x4); + modelview->setName("modelview"); + modelview->uniform = true; + modelview->constSortPos = cspPrimitive; + } + + return modelview; +} + +Var* ShaderFeatureNode::getWorldView( Vector& componentList, + bool useInstancing, + MultiLine* meta) +{ + Var* worldView = (Var*)LangElement::find("worldViewOnly"); + if (worldView) + return worldView; + + if (useInstancing) + { + Var* objTrans = getObjTrans(componentList, useInstancing, meta); + + Var* worldToCamera = (Var*)LangElement::find("worldToCamera"); + if (!worldToCamera) + { + worldToCamera = new Var; + worldToCamera->setType(GFXSCT_Float4x4); + worldToCamera->setName("worldToCamera"); + worldToCamera->uniform = true; + worldToCamera->constSortPos = cspPass; + } + + worldView = new Var; + worldView->setType(GFXSCT_Float4x4); + worldView->setName("worldViewOnly"); + + meta->addStatement(new GenOp(" @ = @; // Instancing!\r\n", new DecOp(worldView), new MatrixMultiplyOp(worldToCamera, objTrans) )); + } + else + { + worldView = new Var; + worldView->setType(GFXSCT_Float4x4); + worldView->setName("worldViewOnly"); + worldView->uniform = true; + worldView->constSortPos = cspPrimitive; + } + + return worldView; +} + void ShaderFeatureNode::setupTextureSample( const String& samplerName, GFXShaderConstType samplerType, Vector& componentList, @@ -69,43 +191,7 @@ void ShaderFeatureNode::setupTextureSample( const String& samplerName, Var* samplerVar = dynamic_cast(LangElement::find(sampVarName)); - Var* textureVar; - - if (!isGL) // HLSL requires both Texture + SamplerState - { - if (!samplerVar) - { - samplerVar = new Var; - samplerVar->setType(isComparison ? "SamplerComparisonState" : "SamplerState"); - samplerVar->setName(sampVarName); - samplerVar->uniform = true; - samplerVar->sampler = true; - samplerVar->constNum = Var::getTexUnitNum(); - } - - textureVar = dynamic_cast(LangElement::find(texVarName)); - if (!textureVar) - { - textureVar = new Var; - textureVar->setType(LangElement::samplerTypeToString(samplerType)); // Texture2D, TextureCube, etc. - textureVar->setName(texVarName); - textureVar->uniform = true; - textureVar->texture = true; - textureVar->constNum = samplerVar->constNum; - } - } - else - { - if (!samplerVar) - { - samplerVar = new Var; - samplerVar->setType(LangElement::samplerTypeToString(samplerType)); - samplerVar->setName(sampVarName); - samplerVar->uniform = true; - samplerVar->sampler = true; - samplerVar->constNum = Var::getTexUnitNum(); - } - } + Var* textureVar = NULL; // The sampled color variable (e.g. "samplerName_col") should always be new but just in case Var* sampledColor = (Var*)LangElement::find(resultVarName); @@ -123,6 +209,16 @@ void ShaderFeatureNode::setupTextureSample( const String& samplerName, if (isGL) { // ---------------- GLSL Sampling ---------------- + if (!samplerVar) + { + samplerVar = new Var; + samplerVar->setType(LangElement::constTypeToString(samplerType, true)); + samplerVar->setName(sampVarName); + samplerVar->uniform = true; + samplerVar->sampler = true; + samplerVar->constNum = Var::getTexUnitNum(); + } + if (isComparison) { if (useGather) @@ -149,6 +245,27 @@ void ShaderFeatureNode::setupTextureSample( const String& samplerName, else { // ---------------- HLSL Sampling ---------------- + if (!samplerVar) + { + samplerVar = new Var; + samplerVar->setType(isComparison ? "SamplerComparisonState" : "SamplerState"); + samplerVar->setName(sampVarName); + samplerVar->uniform = true; + samplerVar->sampler = true; + samplerVar->constNum = Var::getTexUnitNum(); + } + + textureVar = dynamic_cast(LangElement::find(texVarName)); + if (!textureVar) + { + textureVar = new Var; + textureVar->setType(LangElement::constTypeToString(samplerType, true)); // Texture2D, TextureCube, etc. + textureVar->setName(texVarName); + textureVar->uniform = true; + textureVar->texture = true; + textureVar->constNum = samplerVar->constNum; + } + if (isComparison) { if (useGather) @@ -192,8 +309,7 @@ Var* ShaderFeatureNode::getOutTexCoord(const char* name, GFXShaderConstType type // Statement allows for casting of different types which // eliminates vector truncation problems. - String statement = String::ToString(" @ = (%s)@;\r\n", type); - meta->addStatement(new GenOp(statement, texCoord, inTex)); + meta->addStatement(new GenOp(" @ = (@)@;\r\n", texCoord, new TypeOp(type), inTex)); } @@ -265,6 +381,57 @@ LangElement* ShaderFeatureNode::assignColor(LangElement* elem, Material::BlendOp return NULL; } +//-------------------------------------------------------- +// Vertex position. +//-------------------------------------------------------- + +void NodeVertexPositionFeature::processVert(Vector& componentList, const MaterialFeatureData& fd) +{ + // First check for an input position from a previous feature + // then look for the default vertex position. + Var* inPosition = (Var*)LangElement::find("inPosition"); + if (!inPosition) + inPosition = (Var*)LangElement::find("position"); + + const bool glsl = (GFX->getAdapterType() == OpenGL); + + // grab connector position + ShaderConnector* connectComp = dynamic_cast(componentList[C_CONNECTOR]); + Var* outPosition = connectComp->getElement( glsl ? RT_POSITION : RT_SVPOSITION); + + if (glsl) + { + outPosition->setName("gl_Position"); + } + else + { + outPosition->setName("hpos"); + outPosition->setStructName("OUT"); + } + + MultiLine* meta = new MultiLine; + + Var* modelview = getModelView(componentList, fd.features[MFT_UseInstancing], meta); + + meta->addStatement(new GenOp(" @ = @;\r\n", outPosition, new MatrixMultiplyOp( modelview, new CastOp(inPosition, GFXSCT_Float4, "x;y;z")))); + + output = meta; +} + +void NodeVertexPositionFeature::processPix(Vector& componentList, const MaterialFeatureData& fd) +{ + const bool glsl = (GFX->getAdapterType() == OpenGL); + + if (!glsl) + { + // grab connector position + ShaderConnector* connectComp = dynamic_cast(componentList[C_CONNECTOR]); + Var* outPosition = connectComp->getElement(RT_SVPOSITION); + outPosition->setName("vpos"); + outPosition->setStructName("IN"); + } +} + //-------------------------------------------------------- // Setup the default texcoord //-------------------------------------------------------- @@ -352,10 +519,10 @@ void NormalMapFeature::processPix(Vector& componentList, const meta->addStatement(expandNormalMap(sampledColor, tempNormDecl, tempNorm, fd)); meta->addStatement( - new GenOp(" @.xy *= float2(@, @);\r\n", tempNorm, params->flipX ? -1.0f : 1.0f, params->flipY ? -1.0f : 1.0f)); + new GenOp(" @.xy *= @(@, @);\r\n", tempNorm, new TypeOp(GFXSCT_Float2), params->flipX ? -1.0f : 1.0f, params->flipY ? -1.0f : 1.0f)); - meta->addStatement(new GenOp(" @.xyz = normalize( float3( @.xy * @, @.z ) );\r\n", - tempNorm, tempNorm, params->strength, tempNorm)); + meta->addStatement(new GenOp(" @.xyz = normalize( @( @.xy * @, @.z ) );\r\n", + tempNorm, new TypeOp(GFXSCT_Float3), tempNorm, params->strength, tempNorm)); // write back into our known variable. meta->addStatement(new GenOp(" @ = @;\r\n", sampledColor, tempNorm)); @@ -363,3 +530,4 @@ void NormalMapFeature::processPix(Vector& componentList, const output = meta; } + diff --git a/Engine/source/shaderGen/NODE/shaderGenNodes.h b/Engine/source/shaderGen/NODE/shaderGenNodes.h index b1082f3d8..d93f3ca80 100644 --- a/Engine/source/shaderGen/NODE/shaderGenNodes.h +++ b/Engine/source/shaderGen/NODE/shaderGenNodes.h @@ -20,6 +20,7 @@ #include "gfx/gfxAPI.h" #endif +DeclareFeatureType(SNF_VertexPosition); DeclareFeatureType(SNF_DefaultTexCoord); DeclareFeatureType(SNF_TextureFeature); DeclareFeatureType(SNF_NormalMapFeature); @@ -29,6 +30,7 @@ DeclareFeatureType(SNF_NormalMapFeature); /// enum ShaderNodeFeature_enum { + eSNF_VertexPosition, eSNF_DefaultTexCoord, eSNF_TextureFeature, eSNF_NormalMapFeature, @@ -39,6 +41,18 @@ DefineEnumType(ShaderNodeFeature_enum); class ShaderFeatureNode : public ShaderFeature { public: + + /// + Var* getObjTrans( Vector& componentList, + bool useInstancing, + MultiLine* meta); + + Var* getModelView(Vector& componentList, + bool useInstancing, + MultiLine* meta); + + Var* getWorldView(Vector& componentList, bool useInstancing, MultiLine* meta); + void setupTextureSample(const String& samplerName, GFXShaderConstType samplerType, Vector& componentList, @@ -58,6 +72,19 @@ public: LangElement* assignColor(LangElement* elem, Material::BlendOp blend, LangElement* lerpElem = NULL, ShaderFeature::OutputTarget outputTarget = ShaderFeature::DefaultTarget) override; }; +class NodeVertexPositionFeature : public ShaderFeatureNode +{ + void processVert(Vector& componentList, + const MaterialFeatureData& fd) override; + + void processPix(Vector& componentList, + const MaterialFeatureData& fd) override; + + String getName() override + { + return "NodeVertexPositionFeature"; + } +}; class DefaultTexcoordFeature : public ShaderFeatureNode { diff --git a/Engine/source/shaderGen/shaderFeature.cpp b/Engine/source/shaderGen/shaderFeature.cpp index 00bbd7c9d..6dfd7b553 100644 --- a/Engine/source/shaderGen/shaderFeature.cpp +++ b/Engine/source/shaderGen/shaderFeature.cpp @@ -38,6 +38,73 @@ void ShaderFeature::addDependency( const ShaderDependency *dependsOn ) mDependencies.push_back( dependsOn ); } +Var* ShaderFeature::getVertTexCoord(const String& name) +{ + Var* inTex = NULL; + + for (U32 i = 0; i < LangElement::elementList.size(); i++) + { + if (!String::compare((char*)LangElement::elementList[i]->name, name.c_str())) + { + inTex = dynamic_cast(LangElement::elementList[i]); + if (inTex) + { + // NOTE: This used to do this check... + // + // String::compare( (char*)inTex->structName, "IN" ) + // + // ... to ensure that the var was from the input + // vertex structure, but this kept some features + // ( ie. imposter vert ) from decoding their own + // coords for other features to use. + // + // If we run into issues with collisions between + // IN vars and local vars we may need to revise. + + break; + } + } + } + + return inTex; +} + +LangElement* ShaderFeature::expandNormalMap(LangElement* sampleNormalOp, LangElement* normalDecl, LangElement* normalVar, const MaterialFeatureData& fd) +{ + MultiLine* meta = new MultiLine; + const bool hasBc3 = fd.features.hasFeature(MFT_IsBC3nm, getProcessIndex()); + const bool hasBc5 = fd.features.hasFeature(MFT_IsBC5nm, getProcessIndex()); + + if (hasBc3 || hasBc5) + { + if (fd.features[MFT_ImposterVert]) + { + // The imposter system uses object space normals and + // encodes them with the z axis in the alpha component. + meta->addStatement(new GenOp(" @ = @( normalize( @.xyw * 2.0 - 1.0 ), 0.0 ); // Obj DXTnm\r\n", normalDecl, new TypeOp(GFXSCT_Float4), sampleNormalOp)); + } + else if (hasBc3) + { + // BC3 Swizzle trick + meta->addStatement(new GenOp(" @ = @( @.ag * 2.0 - 1.0, 0.0, 0.0 ); // DXTnm\r\n", normalDecl, new TypeOp(GFXSCT_Float4), sampleNormalOp)); + meta->addStatement(new GenOp(" @.z = sqrt( 1.0 - dot( @.xy, @.xy ) ); // DXTnm\r\n", normalVar, normalVar, normalVar)); + } + else if (hasBc5) + { + // BC5 + meta->addStatement(new GenOp(" @ = @( @.gr * 2.0 - 1.0, 0.0, 0.0 ); // bc5nm\r\n", normalDecl, new TypeOp(GFXSCT_Float4), sampleNormalOp)); + meta->addStatement(new GenOp(" @.z = sqrt( 1.0 - dot( @.xy, @.xy ) ); // bc5nm\r\n", normalVar, normalVar, normalVar)); + } + } + else + { + meta->addStatement(new GenOp(" @ = @;\r\n", normalDecl, sampleNormalOp)); + meta->addStatement(new GenOp(" @.xyz = @.xyz * 2.0 - 1.0;\r\n", normalVar, normalVar)); + } + + return meta; +} + ShaderFeature::Resources ShaderFeature::getResources( const MaterialFeatureData &fd ) { Resources temp; diff --git a/Engine/source/shaderGen/shaderOp.cpp b/Engine/source/shaderGen/shaderOp.cpp index 80e467080..8a514f21c 100644 --- a/Engine/source/shaderGen/shaderOp.cpp +++ b/Engine/source/shaderGen/shaderOp.cpp @@ -26,7 +26,7 @@ #include "shaderOp.h" -bool resolveSourceType(LangElement* elem, Var*& outVar, const ShaderTypeInfo*& outInfo) +bool ShaderOp::resolveSourceType(LangElement* elem, Var*& outVar, const ShaderTypeInfo*& outInfo) { outVar = nullptr; outInfo = nullptr; @@ -35,18 +35,18 @@ bool resolveSourceType(LangElement* elem, Var*& outVar, const ShaderTypeInfo*& o if (Var* v = dynamic_cast(elem)) { outVar = v; - outInfo = LangElement::getTypeInfo(LangElement::stringToConstType((const char*)v->type)); + outInfo = getTypeInfo(stringToConstType((const char*)v->type)); return outInfo != nullptr; } // INDEX OP: arrVar[index] if (IndexOp* idx = dynamic_cast(elem)) { - Var* arr = idx->arrVar; + Var* arr = dynamic_cast(idx->mInput[0]); if (!arr) return false; - const ShaderTypeInfo* arrInfo = LangElement::getTypeInfo(LangElement::stringToConstType((const char*)arr->type)); + const ShaderTypeInfo* arrInfo = getTypeInfo(stringToConstType((const char*)arr->type)); if (!arrInfo) return false; @@ -56,10 +56,23 @@ bool resolveSourceType(LangElement* elem, Var*& outVar, const ShaderTypeInfo*& o return true; } + if (CastOp* cast = dynamic_cast(elem)) + { + Var* castVar = dynamic_cast(cast->mInput[0]); + if (!castVar) + return false; + + const ShaderTypeInfo* castInfo = getTypeInfo(cast->mTargetType);// get the casts target type. + if (!castInfo) + return false; + + outVar = castVar; + outInfo = castInfo; + } + return false; } - //************************************************************************** // Shader Operations //************************************************************************** @@ -117,9 +130,8 @@ void EchoOp::print( Stream &stream ) //************************************************************************** // Index operation //************************************************************************** -IndexOp::IndexOp( Var* var, U32 index ) : Parent( NULL, NULL ) +IndexOp::IndexOp( Var* var, U32 index ) : Parent(var, NULL ) { - arrVar = var; // need to keep hold of it for casts. mInput[0] = var; mIndex = index; } @@ -266,13 +278,13 @@ void CastOp::print(Stream& stream) const ShaderTypeInfo* dstInfo = getTypeInfo(mTargetType); // no info? types match? nothing to do. - if (!srcInfo || !dstInfo || (srcInfo->type == dstInfo->type)) + if (!srcInfo || !dstInfo) { srcElem->print(stream); // print something.... return; } - bool glsl = (GFX->getAdapterType() == OpenGL); + const bool glsl = (GFX->getAdapterType() == OpenGL); const char* dstName = glsl ? dstInfo->glslName : dstInfo->hlslName; U32 srcSize = srcInfo->cols; @@ -322,7 +334,7 @@ void CastOp::print(Stream& stream) } // vector -> vector widening - if (srcSize < dstSize) + if (srcSize <= dstSize) { WRITESTR(dstName); WRITESTR("("); @@ -452,7 +464,18 @@ void MatrixInitializeOp::print(Stream& stream) // If not enough elements → pad with zeros while (count < matSize) { - WRITESTR("0"); + U32 row = count / cols; + U32 col = count % cols; + + if (row == col) + { + WRITESTR("1"); + } + else + { + WRITESTR("0"); + } + count++; if (count < matSize) @@ -472,3 +495,71 @@ void MatrixInitializeOp::print(Stream& stream) } } +MatrixMultiplyOp::MatrixMultiplyOp(LangElement* left, LangElement* right) : Parent(left, right) +{ + mInput[0] = left; + mInput[1] = right; +} + +void MatrixMultiplyOp::print(Stream& stream) +{ + LangElement* leftElem = mInput[0]; + Var* leftVar = nullptr; + const ShaderTypeInfo* leftInfo = nullptr; + + if (!resolveSourceType(leftElem, leftVar, leftInfo)) + { + return; + } + + LangElement* rightElem = mInput[0]; + Var* rightVar = nullptr; + const ShaderTypeInfo* rightInfo = nullptr; + + if (!resolveSourceType(rightElem, rightVar, rightInfo)) + { + return; + } + + if (leftInfo->isMatrix() && rightInfo->isVector()) + { + if (rightInfo->cols != leftInfo->cols) + { + rightElem = new CastOp(rightVar, (GFXShaderConstType)(GFXSCT_Float + (leftInfo->cols - 1))); + } + } + + if (leftInfo->isVector() && rightInfo->isMatrix()) + { + if (rightInfo->cols != leftInfo->cols) + { + leftElem = new CastOp(leftVar, (GFXShaderConstType)(GFXSCT_Float + (rightInfo->cols - 1))); + } + } + + else if (leftInfo->isMatrix() && rightInfo->isMatrix()) + { + if (leftInfo->cols != rightInfo->rows) + Con::warnf("MatrixMultiplyOp: incompatible matrices: (%dx%d) × (%dx%d)", + leftInfo->rows, leftInfo->cols, + rightInfo->rows, rightInfo->cols); + } + + const bool glsl = (GFX->getAdapterType() == OpenGL); + + if (!glsl) + { + WRITESTR("mul("); + leftElem->print(stream); + WRITESTR(", "); + rightElem->print(stream); + WRITESTR(")"); + } + else + { + leftElem->print(stream); + WRITESTR(" * "); + rightElem->print(stream); + } + +} diff --git a/Engine/source/shaderGen/shaderOp.h b/Engine/source/shaderGen/shaderOp.h index 8a42168dd..7fb3da152 100644 --- a/Engine/source/shaderGen/shaderOp.h +++ b/Engine/source/shaderGen/shaderOp.h @@ -45,7 +45,7 @@ */ //************************************************************************** -bool resolveSourceType(LangElement* elem, Var*& outVar, const ShaderTypeInfo*& outInfo); + ///************************************************************************** /// Shader operation base class @@ -56,6 +56,7 @@ protected: LangElement * mInput[2]; public: + bool resolveSourceType(LangElement* elem, Var*& outVar, const ShaderTypeInfo*& outInfo); ShaderOp( LangElement *in1, LangElement *in2 ); }; @@ -118,7 +119,6 @@ class IndexOp : public ShaderOp typedef ShaderOp Parent; U32 mIndex; public: - Var* arrVar; IndexOp( Var* var, U32 index ); void print( Stream &stream ) override; }; @@ -185,10 +185,10 @@ public: class CastOp : public ShaderOp { typedef ShaderOp Parent; - GFXShaderConstType mTargetType; Vector mSwizzle; // "x", "y", "z", "w" Vector mFillValues; // "0", "0", "0", "1" public: + GFXShaderConstType mTargetType; CastOp( LangElement* srcVar, GFXShaderConstType type, const char* swizzleStr = "x;y;z;w", @@ -243,4 +243,13 @@ public: */ //---------------------------------------------------------------------------- +class MatrixMultiplyOp : public ShaderOp +{ + typedef ShaderOp Parent; +public: + MatrixMultiplyOp(LangElement* left, LangElement* right); + + void print(Stream& stream) override; +}; + #endif // _SHADEROP_H_