5
\$\begingroup\$

I made this Renderer class recently, to simplify the user interface of my library's API. I would like to ask advice about the move semantics (copy constructor, std::unique_ptr, ...), or any other things you can point out to improve my code.

As you can see, I take in parameters in RenderObject in the form of a pointer. This is simply because Material uses a Texture class, whose assignment operators are disabled. It only has a move constructor (C++11), but Material cannot use it.

RenderObject.h

#pragma once

#include "alpha/Mesh.h"
#include "alpha/Texture.h"
#include "alpha/Light.h"
#include "alpha/Transform.h"

class RenderObject
{
public:
    RenderObject(
        Mesh* mesh, Material* material,
        Transform transform = Transform()
        )
        :
            m_pmesh(mesh),
            m_pmaterial(material),
            m_transform(transform)
        {}
    ~RenderObject(){}

    void SetTransform(Transform transform){
        m_transform = transform;
    }
    Transform GetTransform(){
        return m_transform;
    }
    Mesh* GetMesh(){
        return m_pmesh;
    }
    void SetMesh(Mesh* mesh){
        m_pmesh = mesh;
    }
    Material* GetMaterial(){
        return m_pmaterial;
    }
    void SetMaterial(Material* material){
        m_pmaterial = material;
    }
    void SetPosition(glm::vec3 position){
        m_transform.SetPos(position);
    }
    void SetRotation(glm::vec3 rotation){
        m_transform.SetRot(rotation);
    }
    void SetScale(glm::vec3 scale){
        m_transform.SetScale(scale);
    }
    glm::vec3 GetPosition(){
        return m_transform.GetPos();
    }
    glm::vec3 GetRotation(){
        return m_transform.GetRot();
    }
    glm::vec3 GetScale(){
        return m_transform.GetScale();
    }

private:
    Transform m_transform;
    Mesh* m_pmesh = nullptr;
    Material* m_pmaterial = nullptr;
};

Material.h

class Material{
public:
    template<typename T>
    Material(T&& texture, float shininess,
        const glm::vec3& specularColor = glm::vec3(0.5, 1.0, 1.5),
        const glm::vec3& emissiveColor = glm::vec3(0.0, 0.0, 0.0)
        )
            :
                m_texture(std::forward<T>(texture)),
                m_shininess(shininess),
                m_specularColor(specularColor),
                m_emissiveColor(emissiveColor)
    {}
    ~Material(){
        m_texture.Dispose(); ///not necessary... ressources are automatically disposed in texture !
    }
    void Bind(unsigned unit = 0){
        this->m_texture.Bind(unit);
    }

    void SetUniforms(Shader& shader) const{
        shader.UpdateUniform3fv("materialSpecularColor" ,glm::value_ptr(this->m_specularColor));
        shader.UpdateUniform1f("materialShininess", this->m_shininess);
        shader.UpdateUniform3fv("materialEmissiveColor", glm::value_ptr(this->m_emissiveColor));
    }

    void SetShininess(float shininess){
        m_shininess = shininess;
    }
    void SetSpecularColor(glm::vec3 specularColor){
        m_specularColor = specularColor;
    }
    void SetEmissiveColor(glm::vec3 emissiveColor){
        m_emissiveColor = emissiveColor;
    }
    float GetShininess(){
        return m_shininess;
    }
    glm::vec3 GetSpecularColor(){
        return m_specularColor;
    }
    glm::vec3 GetEmissiveColor(){
        return m_emissiveColor;
    }
    Texture& GetTexture(){
        return m_texture;
    }


    Material& operator=(const Material& other) = delete;
    Material(const Material& other) = delete;
protected:
    float m_shininess;
    glm::vec3 m_specularColor;
    glm::vec3 m_emissiveColor;
    Texture m_texture;
    ///more lighting... normal maps...
};

Renderer.h

#pragma once

#include <vector>
#include "alpha/Light.h"
#include "alpha/Mesh.h"
#include "alpha/Texture.h"
#include "alpha/Light.h"
#include "alpha/Transform.h"
#include "alpha/RenderObject.h"

class ForwardRenderer
{
public:
    ForwardRenderer(){}///default ctor

    ForwardRenderer(std::vector<RenderObject>* objects)
        :
            m_pobjects(objects),
            m_shader("res/basicShader.glslv", "res/phongShader.glslf")

    {}
    ForwardRenderer(
        std::vector<RenderObject>* objects,
        PhongLight* light
    )
        :
            m_pobjects(objects),
            m_plight(light),
            m_shader("res/basicShader.glslv", "res/phongShader.glslf")
    {}
    ~ForwardRenderer(){}///default dtor

    void RenderAll(Camera& camera){
        m_shader.Bind();
            for(auto it = m_pobjects->begin(); it != m_pobjects->end(); ++it){
                ///Set Uniforms
                camera.SetUniforms(m_shader, it->GetTransform());
                it->GetMaterial()->SetUniforms(m_shader);
                m_plight->SetUniforms(m_shader);

                ///Render
                it->GetMaterial()->Bind(0);///sampler slot = 0;
                    it->GetMesh()->Draw();

                camera.SetUniforms(m_shader, Transform());

            }
    }

    ForwardRenderer& operator=(const ForwardRenderer& other) = delete;
    ForwardRenderer(const ForwardRenderer& other) = delete;

private:
    std::vector<RenderObject>* m_pobjects = nullptr;
    PhongLight* m_plight = nullptr;
    Shader m_shader;
};

typedef ForwardRenderer BasicRenderer;
\$\endgroup\$
5
\$\begingroup\$

Resource management:

I general, the code seems alright to me, but one thing you continue to do, if I recall from your previous questions, is to use raw pointers and manual resource/memory management. So I'd like to urge you to look into the standard smart pointers and start using them.

The basic usage is:

  • Resource is shared between instances of different objects: Frequent case for things like textures, materials and shaders. Use a shared_ptr for them.

  • Resource is explicitly owned by an instance of an object: In a renderer, this could apply to many types of things, from light sources to object transforms. Use a unique_ptr for those.

A few other things:

  1. RenderObject is just a data container with a bunch of accessors. When you get to this point, it might be worth considering just making it a plain struct with all fields public, since there is no difference regarding encapsulation when you can freely get/set all fields. In such cases, the accessors only add a level of unnecessary indirection. However, you might also consider a redesign altogether, since the heavy use of accessors indicate that all the algorithms involving a RenderObject are outside the class, which in turn indicate that it is probably strongly coupled with other areas of the code and systems.

  2. I'm wondering what this is about?

     ~Material(){
         m_texture.Dispose(); ///not necessary... ressources are automatically disposed in texture !
     }
    

    So, if it is not necessary, as you say, why is it there? It would be safer indeed if the destructor of Texture did the cleanup, so you should probably avoid exposing a public Dispose() method anyways, to prevent making the mistake of having a disposed/invalid object in your hands. This is specially true for library code, make it as hard as possible for the user to shoot himself in the foot (dedicated users will still manage, but don't facilitate ;)).

  3. The fact that the constructor of Material is templated for a texture type puzzles me:

    template<typename T>
    Material(T&& texture, float shininess, ...
    

    The member m_texture is of type Texture, so unless there's some polymorphic conversion going on here, the template parameter should just be a Texture &&.

  4. Don't this-> qualify member variables, ever! We have a very amusing example right here on CR of how using it can lead to some pretty hilarious bugs.

  5. Avoid writing the empty constructors/destructors and let the compiler do its job providing the empty defaults. But in particular, don't do this:

    ~ForwardRenderer(){}///default dtor
    

    Yep, anyone that has been programming in C++ for more than two days knows what a destructor looks like. Unless you're using the comment for trolling purposes[*], trim it down.

  6. std::vector<RenderObject>* m_pobjects in ForwardRenderer seems like it could be declared by value. Then change the constructor to move the parameter or take it by && move-reference. The less pointer chasing the better when it comes to performance critical applications like real-time rendering. Make good use your data cache.


[*]: I recall once reading a comment just above a class destructor that said something like "If you don't know what this is, then you're probably going to get fired..." -- Obviously the author was being funny, and this kind of internal jokes can help bootring the morale of a team, but otherwise, never comment explaining obvious aspects of the language.

\$\endgroup\$
  • \$\begingroup\$ Actually, Texture has a Dispose() method, which gets called automatically called by the destructor.... but, the user can call it manually, and then, the destructor will not call Dispose() twice ! -- This is basically usefull if the user wants to delete a big object (like a Shader) before the variable goes out of scope. \$\endgroup\$ – MattMatt Jul 26 '15 at 8:18
  • \$\begingroup\$ 2) About smart pointers... All my opengl classes have their assignment operators deleted; but they have move constructors. So the question would be that should I typedef all my opengl classes to a unique_ptr ? like typedef shared_ptr<__classMesh> Mesh; this would be ton of work though... \$\endgroup\$ – MattMatt Jul 26 '15 at 8:20
  • \$\begingroup\$ 3) ///dtor is generated by my IDE :) \$\endgroup\$ – MattMatt Jul 26 '15 at 8:41
  • \$\begingroup\$ 2) bis : or I can put all my opengl classes in a namespace detail::, and put the shared_ptr typdef in the global library namespace... \$\endgroup\$ – MattMatt Jul 26 '15 at 8:50
3
\$\begingroup\$
~RenderObject(){}

Empty methods should be commented to let the maintainer know that

  1. This was intentional.
  2. The reason why this was done.

Otherwise, some maintainer like me is going to come behind you and remove what looks like dead code.

Now, there seems to be some debate about whether or not to declare empty finalizers, but I found a great analysis of the topic that you may be interested in. The TL;DR version of that piece is below.

To be able to quickly decide whether a class needs an empty non-inline destructor definition I condensed the above analysis into a short checklist. When designing a class interface, ask yourself the following three questions:

  1. Do you need to anchor the vtbl (doesn’t apply to class templates)?
  2. Does proper destruction of data members require additional declarations or functionality that is not available in the class interface? Does the destruction need to be done consistently with construction (e.g., using the same runtime)?
  3. Do you need to define a stable interface and chances are that later you may have to add some functionality to the destructor?

If the answers to all these questions are “No”, then let the compiler provide the default implementation of the destructor.

\$\endgroup\$
  • \$\begingroup\$ Hehe, I realize this is an extended comment to my answer, but bare in mid that I was explicitly talking about the constructor/destructor pair. If it was a normal method instead left empty, the comment would be just fine. I shall read the link you've provided in more detail. Thanks for bringing it up ;) \$\endgroup\$ – glampert Jul 26 '15 at 2:14
  • \$\begingroup\$ Actually, I started to write this before I saw your answer @glampert. Lol \$\endgroup\$ – RubberDuck Jul 26 '15 at 2:15
  • \$\begingroup\$ Oh. @glampert my point wasn't bad comments, but that if there's a reason to have an empty method, you have to leave a comment explaining why. Otherwise, someone like me will see this and go "Wtf? Dead code." And then poof it's gone. \$\endgroup\$ – RubberDuck Jul 26 '15 at 2:17
  • 1
    \$\begingroup\$ Yep, I also got ahead of myself :P, but good point, for sure. \$\endgroup\$ – glampert Jul 26 '15 at 2:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.