I defined a make-shift texture class and decided that the exact needed interface would be determined later. I decided that, for now, the sampler would be a part of the Texture class for the sake of simplicity.
The C++11 feature of const-expr template parameters allowed me to determine the need for a Texture plugin in the hlslModularProgram to also automatically activate the usesTexUvs feature. See the bottom of the post for that nice template trick.
The actual shader was rather simple, its effects below... After figuring out the interface of a TextureManager, creating the class that will actually load, create, configure, unload the texture and sampler, the actual shader code was disappointingly simple. Just two declarations in the Fragment Shader, and a sampling of the texture multiplied by the object material.
The spotlight also proved to be a very easy addition, but it was the driving factor behind the implementation of a lot of changes to get code simplicity and reusability to the point I wanted. While the feature itself is 5 lines of glsl code, having these 5 lines duplicated everywhere was driving me crazy.
![]() | ![]() |
So, I had to implement a simple glsl - include feature to my shader loader - compiler: The parser became recursive, following the #pragma include xxxxxxx, lines into other files, including it in the actual code that will be compiled.
Remember, functions in GLSL are always inline, but take care to define them in such a way as to REALLY not cause you unnecessary overhead, as it is easy to rewrite a shader around a function when in truth the function takes different arguments than you really need.
For example, the Spotlight pragma include is the following snippet:
//Returns the attenuation caused by a spotlight, from 1 (inside inner) to 0 (not touched by light)
float calc_spotlight(vec3 vertexToLightDir, vec3 spotdir, float spotangle_inner, float spotangle_outer)
{
float cosLightToSpot = dot(-vertexToLightDir, spotdir);
float coneInner = clamp(spotangle_inner_cos, -1, 1);
float coneOuter = clamp(spotangle_outer_cos, -1, 1);
float conediff = coneInner-coneOuter;
float conecoeff = clamp((cosLightToSpot-coneOuter)/conediff, 0,1);
return conecoeff;
}
Matrices, lights and material definition uniform buffers, as well as specular lighting equations went the same road of #pragma includes, minimizing code duplication.
Yes, I hate code duplication with great passion. Copy pasting is just asking for trouble down the road.
The promised template trick : This is the code of the templated configuration object of an xlslModularProgram - the one you need to fill and then pass back to the shader's init() function. Observe that by inheriting from all the actual setup classes, it will only contain its necessary fields for its setup. Also, by virtue of that inheritance scheme, it can pass a reference to each plugin's init() function, and in the same way the plugin expects the correct object depending on if it is activated (<true>) or deactivated (<false>).
template < grWin32::GraphicsApi _api, bool usesMatrices, typename TextureManagerType, typename FrameBufferObjectType, typename MatrixBufferObjectType, bool usesVertexPositions, bool usesVertexNormals,
bool usesMaterials, bool usesLights, bool usesTextures,
bool usesNormalmaps, bool usesVertexTangents , bool usesVertexBitangents,
bool usesShadowmaps >
struct DynamicShaderConfig :
public MatrixSetup<_api, usesMatrices> ,
public VertexPosSetup<usesVertexPositions>,
public VertexNormalSetup<usesVertexNormals>,
public MaterialSetup<_api, usesMaterials>,
public LightSetup<_api, usesLights>,
//The following line activates the configuration object fields that are required
//to configure texture coordinates IF a feature that will need texture coordinates is activated.
public VertexUvSetup < usesTextures || usesNormalmaps > ,
public TextureSetup<_api, TextureManagerType, usesTextures>,
public NormalMapSetup<_api, TextureManagerType, usesNormalmaps>,
public VertexTangentSetup<usesVertexTangents>,
public VertexBitangentSetup<usesVertexBitangents>,
public ShadowmapSetup<_api, FrameBufferObjectType, MatrixBufferObjectType, usesShadowmaps> {
grGraphics::EntityManager* core_EntityManager;
};
In turn, a configuration object might look like:
template<grWin32::GraphicsApi _api, bool supportsLights> struct LightSetup { LightSetup() { static_assert(0, "THIS LIGHT CONFIGURATION HAS NOT BEEN DEFINED YET"); } };
//Partial specialization: if the second parameter is false: no lights, so the object is empty
template<grWin32::GraphicsApi _api> struct LightSetup<_api, false> {};
//Specialization for OpenGL flavor, with lights. Needed parameters include the name of the light block in the shader, and the coordinate space the shader expects it in.
template<> struct LightSetup<grWin32::GraphicsApi::OPENGL, true> {
CoordinateSpaces light_CoordinateSpace;
const char* light_UniformBlockName;
LightSetup(): light_CoordinateSpace(CoordinateSpaces::_UNSET), light_UniformBlockName(0) {}
};
The actual plugins (which is a field in the xlslModularProgram) would look like:
//EMPTY VERSION: No lights exist, but the xlslModularprogram will call the functions anyway. Make sure to forceinline them just in case the compiler is stupid enough not to realize they are not empty...
template<bool usesLights> struct LightPlugin {
LightPlugin(glslProgram& program, grg::EntityManager& entityManager) { }
__forceinline void init(const grg::LightSetup<grWin32::GraphicsApi::OPENGL, usesLights>& config, GLuint globalUniformBindingIndex) { }
__forceinline void render_starting() { }
};
//NORMAL VERSION:
template<> struct LightPlugin<true> {
glslProgram& program;
GLint uniformBlockIndex;
... other needed fields//Observe that the empty and the full version need the exact same signatures.
LightPlugin(glslProgram& program): program(program) {}
//Notice, that the configuration object is passed by reference, and the reference actually holds an entire DynamicShaderConfig. Polymorphism to the rescue void init(const grg::LightSetup<grWin32::GraphicsApi::OPENGL, true>& config, grg::EntityManager& entityManager, GLuint globalUniformBindingIndex) {
this->entityManager = &entityManager;
... Assert correctly filled config options, retrieve uniform locations etc...
}
__forceinline void render_starting() {
if (entityManager->lights.size()) {
... Transform the lights to the correct coordinate spaces, upload them to the buffers using the info retrieved from init...
}
}
};
No comments:
Post a Comment