Skip to content

Commit

Permalink
Initial list of attached entities in EntityNode
Browse files Browse the repository at this point in the history
EntityNode maintains a list of entity node pointers representing the attached
entities, which are submitted at render time. The basic rendering is working
and covered by a unit test, but the attached entity appears at the origin since
there is not yet any handling of the localToWorld matrix.
  • Loading branch information
Matthew Mott committed Feb 9, 2021
1 parent 3e8f58a commit 3bd99f9
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 25 deletions.
49 changes: 43 additions & 6 deletions radiantcore/entity/EntityNode.cpp
Expand Up @@ -22,7 +22,9 @@ EntityNode::EntityNode(const IEntityClassPtr& eclass) :
_keyObservers(_spawnArgs),
_shaderParms(_keyObservers, _colourKey),
_direction(1,0,0)
{}
{
createAttachedEntities();
}

EntityNode::EntityNode(const EntityNode& other) :
IEntityNode(other),
Expand All @@ -41,7 +43,9 @@ EntityNode::EntityNode(const EntityNode& other) :
_keyObservers(_spawnArgs),
_shaderParms(_keyObservers, _colourKey),
_direction(1,0,0)
{}
{
createAttachedEntities();
}

EntityNode::~EntityNode()
{
Expand Down Expand Up @@ -112,6 +116,25 @@ void EntityNode::destruct()
TargetableNode::destruct();
}

void EntityNode::createAttachedEntities()
{
_spawnArgs.forEachAttachment(
[this](const Entity::Attachment& a)
{
auto cls = GlobalEntityClassManager().findClass(a.eclass);
if (!cls)
{
rWarning() << "EntityNode [" << _eclass->getName()
<< "]: cannot attach non-existent entity class '"
<< a.eclass << "'\n";
return;
}

_attachedEnts.push_back(GlobalEntityModule().createEntity(cls));
}
);
}

void EntityNode::onEntityClassChanged()
{
// By default, we notify the KeyObservers attached to this entity
Expand Down Expand Up @@ -256,9 +279,13 @@ scene::INode::Type EntityNode::getNodeType() const
return Type::Entity;
}

void EntityNode::renderSolid(RenderableCollector& collector, const VolumeTest& volume) const
void EntityNode::renderSolid(RenderableCollector& collector,
const VolumeTest& volume) const
{
// Nothing here
// Render any attached entities
renderAttachments(
[&](const scene::INodePtr& n) { n->renderSolid(collector, volume); }
);
}

void EntityNode::renderWireframe(RenderableCollector& collector,
Expand All @@ -267,8 +294,14 @@ void EntityNode::renderWireframe(RenderableCollector& collector,
// Submit renderable text name if required
if (EntitySettings::InstancePtr()->getRenderEntityNames())
{
collector.addRenderable(*getWireShader(), _renderableName, localToWorld());
}
collector.addRenderable(*getWireShader(), _renderableName,
localToWorld());
}

// Render any attached entities
renderAttachments(
[&](const scene::INodePtr& n) { n->renderWireframe(collector, volume); }
);
}

void EntityNode::acquireShaders()
Expand Down Expand Up @@ -298,6 +331,10 @@ void EntityNode::setRenderSystem(const RenderSystemPtr& renderSystem)

// The colour key is maintaining a shader object as well
_colourKey.setRenderSystem(renderSystem);

// Make sure any attached entities have a render system too
for (IEntityNodePtr node: _attachedEnts)
node->setRenderSystem(renderSystem);
}

std::size_t EntityNode::getHighlightFlags()
Expand Down
47 changes: 45 additions & 2 deletions radiantcore/entity/EntityNode.h
Expand Up @@ -75,7 +75,20 @@ class EntityNode :

sigc::connection _eclassChangedConn;

protected:
// List of attached sub-entities that we will submit for rendering (but are
// otherwise non-interactable).
//
// Although scene::Node already has the ability to store children, this is a
// separate list of entity nodes for two reasons: (1) there is a lot of
// other code which walks the scene graph for various reasons (e.g. map
// saving), and I don't want to have to audit the entire codebase to make
// sure that everything will play nicely with entities as children of other
// entities, and (2) storing entity node pointers instead of generic node
// pointers avoids some extra dynamic_casting.
using AttachedEntities = std::list<IEntityNodePtr>;
AttachedEntities _attachedEnts;

protected:
// The Constructor needs the eclass
EntityNode(const IEntityClassPtr& eclass);

Expand Down Expand Up @@ -166,6 +179,36 @@ class EntityNode :

void acquireShaders();
void acquireShaders(const RenderSystemPtr& renderSystem);
};

// Create entity nodes for all attached entities
void createAttachedEntities();

// Render all attached entities
template <typename RenderFunc> void renderAttachments(RenderFunc func) const
{
for (const IEntityNodePtr& ent: _attachedEnts)
{
// Attached entities might themselves have child nodes (e.g. func_static
// which has its model as a child node), so we must traverse() the
// attached entities, not just render them alone
struct ChildRenderer : public scene::NodeVisitor
{
RenderFunc _func;
ChildRenderer(RenderFunc f): _func(f)
{}

bool pre(const scene::INodePtr& node)
{
_func(node);
return true;
}
};

ChildRenderer cr(func);
ent->traverse(cr);
}
}

};

} // namespace entity
14 changes: 8 additions & 6 deletions radiantcore/rendersystem/backend/OpenGLShaderPass.cpp
Expand Up @@ -472,7 +472,7 @@ void OpenGLShaderPass::applyState(OpenGLState& current,
} // end of changingBitsMask-dependent changes

// Set depth function
if (requiredState & RENDER_DEPTHTEST
if (requiredState & RENDER_DEPTHTEST
&& _glState.getDepthFunc() != current.getDepthFunc())
{
glDepthFunc(_glState.getDepthFunc());
Expand Down Expand Up @@ -649,11 +649,13 @@ void OpenGLShaderPass::setUpLightingCalculation(OpenGLState& current,
const Matrix4& objTransform,
std::size_t time)
{
// Get the light shader and examine its first (and only valid) layer
assert(light);
ShaderPtr shader = light->getShader();
assert(shader);

// Get the light shader and examine its first (and only valid) layer
const MaterialPtr& lightShader = light->getShader()->getMaterial();
ShaderLayer* layer = lightShader ? lightShader->firstLayer() : nullptr;
const MaterialPtr& lightMat = shader->getMaterial();
ShaderLayer* layer = lightMat ? lightMat->firstLayer() : nullptr;
if (!layer) return;

// Calculate viewer location in object space
Expand All @@ -665,7 +667,7 @@ void OpenGLShaderPass::setUpLightingCalculation(OpenGLState& current,

// Get the XY and Z falloff texture numbers.
GLuint attenuation_xy = layer->getTexture()->getGLTexNum();
GLuint attenuation_z = lightShader->lightFalloffImage()->getGLTexNum();
GLuint attenuation_z = lightMat->lightFalloffImage()->getGLTexNum();

// Bind the falloff textures
assert(current.testRenderFlag(RENDER_TEXTURE_2D));
Expand All @@ -689,7 +691,7 @@ void OpenGLShaderPass::setUpLightingCalculation(OpenGLState& current,
GLProgram::Params parms(
light->getLightOrigin(), layer->getColour(), world2light
);
parms.ambientFactor = lightShader->isAmbientLight() ? 1.0f : 0.0f;
parms.ambientFactor = lightMat->isAmbientLight() ? 1.0f : 0.0f;
parms.invertVertexColour = _glState.isColourInverted();

assert(current.glProgram);
Expand Down
49 changes: 38 additions & 11 deletions test/Entity.cpp
Expand Up @@ -319,14 +319,29 @@ namespace
render::NopVolumeTest volumeTest;
TestRenderableCollector collector;

// Whether to render solid or wireframe
const bool renderSolid;

// Keep track of nodes visited
int nodesVisited = 0;

// Construct
RenderFixture(bool solid = false): renderSolid(solid)
{}

// NodeVisitor implementation
bool pre(const scene::INodePtr& node) override
{
// Count the node itself
++nodesVisited;
node->renderWireframe(collector, volumeTest);

// Render the node in appropriate mode
if (renderSolid)
node->renderSolid(collector, volumeTest);
else
node->renderWireframe(collector, volumeTest);

// Continue traversing
return true;
}
};
Expand Down Expand Up @@ -450,20 +465,32 @@ TEST_F(EntityTest, RenderAttachedLightEntity)
auto& spawnArgs = torch->getEntity();
EXPECT_EQ(spawnArgs.getKeyValue("model"), "models/torch.lwo");

RenderFixture rf;
// We must render in solid mode to get the light source
RenderFixture rf(true);
torch->setRenderSystem(rf.backend);

// The entity node itself does not render the model; it is a parent node
// with the model as a child (e.g. as a StaticModelNode). Therefore we must
// traverse our "mini-scenegraph" to render the model.
torch->traverseChildren(rf);

// The node visitor should have visited one child node (a static model) and
// collected 3 renderables in total (because the torch model has several
// surfaces).
EXPECT_EQ(rf.nodesVisited, 1);
EXPECT_EQ(rf.collector.renderables, 3);
EXPECT_EQ(rf.collector.lights, 0);
// traverse our "mini-scenegraph" to render the model as well as the
// attached entities.
torch->traverse(rf);

// The node visitor should have visited the entity itself and one child node (a
// static model)
EXPECT_EQ(rf.nodesVisited, 2);

// There should be 3 renderables from the torch (because the entity has a
// shadowmesh and a collision mesh as well as the main model) and one from
// the light (the origin diamond).
EXPECT_EQ(rf.collector.renderables, 4);

// The attached light should have been submitted as a light source
EXPECT_EQ(rf.collector.lights, 1);

// The submitted light should be fully realised with a light shader
const RendererLight* rLight = rf.collector.lightPtrs.front();
ASSERT_TRUE(rLight);
EXPECT_TRUE(rLight->getShader());
}

}
22 changes: 22 additions & 0 deletions test/resources/tdm/def/lights.def
Expand Up @@ -111,3 +111,25 @@ entitydef light_torchflame_small
"light_radius" "240 240 240" //larger than other similar flames to be consistent with older maps
}

entitydef light_cageflame_small // casts shadows with bars
{
"inherit" "light_extinguishable"

"mins" "-6 -6 -6"
"maxs" "6 6 24"

"editor_displayFolder" "Lights/Light Sources/Torch Flames"
"editor_usage" "Torch-sized flame for cage-lights, casts faint bar shadows. Light pulses but is static. For moving light, add 'inherit' 'light_extinguishable_moving' to entity. "

"model_lit" "torchflame_new01_small.prt"
"model_extinguished" "tdm_smoke_torchout.prt"

"snd_lit" "fire_torch_small"
"snd_extinguished" "machine_steam01"

"falloff" "0"
"texture" "lights/8pt_cageflicker"

"_color" "0.9 0.6 0.40"
"light_radius" "230 230 250"
}

0 comments on commit 3bd99f9

Please sign in to comment.