rustarok icon indicating copy to clipboard operation
rustarok copied to clipboard

Graphics refactor

Open bbodi opened this issue 4 years ago • 1 comments

Graphics

Here, the term entity is used as in Entity Component Systems.

Who should initiate rendering sprites/effects

Currently, utilizing the Entity Component System, specific entities are responsible for rendering Sprites or Effects. E.g. a skill, which wish to render a lightning bolt on an area, has to create an entity with the details of the animation (when it started, what is the effect id, when it ends etc), then a system will pick up that entity and render it.

impl PushBackWallSkill {
    pub fn new(...) {
		let effect_comp = StrEffectComponent {
                effect: "StrEffect::FireWall".to_owned(),
                pos: effect_coords,
                start_time: system_time,
                die_at: system_time.add_seconds(3.0)
        };
        let effect_entity = entities.create();
        updater.insert(effect_entity, effect_comp);
}

Later, the skill is responsible for removing the effect entity:

impl SkillManifestation for PushBackWallSkill {
  ..
  fn update(&mut self,
            self_entity_id: Entity,
            updater: &mut specs::Write<LazyUpdate>,
            ...) {
        if self.die_at.has_passed(system_vars.time) {
            updater.remove::<SkillManifestationComponent>(self_entity_id);
            for effect_id in &self.effect_ids {
                updater.remove::<StrEffectComponent>(*effect_id);
            }

The other option would be to initiate rendering directly in the update function of the skill. e.g.

impl SkillManifestation for PushBackWallSkill {
	fn update(..) {
        ...
        render_sys.render_effect(
            "StrEffect::FireWall",
            effect_coords,
            self.effect_started,
            self.effect_ends_at
        );
	}
}

One advantage of the first approach is cache utilization. StrEffectComponent can be stored in a Vec, so the system who will render those components can iterate through them easily, achieving great cache utilization. An other advantage is "fire and forget" rendering: It is enough to add an effect to the system once, and the effect will appear and be animated without any further effort. The disadvantage of it is that the skill- and its visualization logic are separated.

  • The skill has to clean up after itself. It is possible to forget to remove an effect component.
  • That "fire and forget" makes it more difficult to understand the code. The update function of a skill won't reflect the fact that an animation is rendered for that skill.

The advantage is not that obvious anymore if we consider real world scenarios, like

  • What if the effect requires special positioning, like moving in a direction in every frame. With this approach the update code of the skill has to get the EffectComponent and modify its position.
  • Many effects are put on players/monsters, and the effect have to follow their positions, which means the rendering system has to access other additional components too for their positions.
  • I am planning to redesign and refactor the graphics module (see later), which basically will translate graphic calls to Command structures, then the rendering system can optimize and render those Commands, so cache utilization advantage would be moved to the the render system level anyway with either approach.

Personally I like the second approach more. It feels that effects/sprites in this situation belongs to the skill, and it is the skill's responsibility to manage them. However I would keep StrEffectComponent, but for simpler cases, like when I want to put an effect on the map, which does not have any "owner", just stands there alone (e.g. for debugging purposes, to check how an effect look like on the map).

As an explanation on why I chose to put rendering data into the Skill, but I don't do it in case of Characters for example, is that Skills are stored as a Box object anyway. Almost every Skill has a different data structure and update logic, and their living instance numbers will be very few, so it is impossible to store them in a Vec. Which means I don't get any benefit separating the rendering logic from the Skills. But in the case of Characters, both the Characters and their rendering data (SpriteRenderDescriptorComponent) have a homogene structure and stored in a Vec as components. And there are systems in the code which uses solely either one or the other, so they are not coupled strongly.

Graphics refactor

Systematic Refactor

Right now there isn't a separate graphic module in the code, different systems access OpenGL to render whatever they want.

  • It leads to ugly code. Systems have to be aware of Shaders and other rendering-related objects, which pollute their code.
  • To render some special cases, the rendered objects has to be prepared for it (e.g. for rendering transparent objects, they have to be sorted by their distance to the camera). It is difficult to do it if rendering logic is scattered throughout the code, or even more, if one system which render things does not even know about an other system who do the same.
  • As I wrote previously, I want to put rendering logic into more code (code of skills for example), so I will need some rendering system which provides a simple, high level API for those areas for rendering whatever they want.
  • It makes it more difficult to optimize rendering. 'Context switching' in rendering is expensive, so it is beneficial to render as many things with the same rendering attributes (texture, shader, vertex array etc) as possible at once. It is only possible if the objects to be rendered are collected at one place and grouped by rendering attributes.
  • The same with culling (not rendering things which are not seen by the camera)
  • A new, unified rendering module would make it possible to measure some characteristics, like the amount of rendered vertices, textures, sprites etc etc.
  • An experimental idea: (I haven't mentioned yet, but the original goal for solving the multiplayer part of the game is to stream the render outpout + sound to browser clients). If it turn out that sending pixels are too expensive, sending these high level rendering commands from the rendering system to the clients could be cheaper.

So the idea is to have one rendering layer which collects high level rendering commands (e.g. "draw a sprite here" or "draw a circle there"). It then processes, prepares and renders those commands.

Implementation

pub struct RenderCommandCollector This will be a component, and every ControllerComponent (a controller represents a human player) will own an instance from it. It will be responsible for collecting rendering commands from the systems. Currently I don't plan to put any preprocessing logic into this struct. The sorting will happen in the RenderSystem, before the rendering, not by this struct.
It will contain several Vecs to store specific commands, one for rendering models, one for sprites, one for circles etc.

pub struct RenderSystem

This is the system which is responsible for processing and rendering the commands obtained from RenderCommandCollectors.

It will have many cached VertexBuffers for dynamic objects.

Missing features

  • Cull ground (now all the ground is rendered) (however it does not seem something as heavy, measure first)
  • Experimenting with removing back face of models
  • Sort models by their Z coordinate on startup
  • Sort sprites and effects by their Z coordinate
  • Render dead sprites first
  • Group model rendering by their textures
  • Efficient primitive (circle, rectangle) drawing

Achievable goals:

  • 100 fire effect should be rendered without massive FPS drop
  • 100 flying number should be rendered without massive FPS drop
  • 100 mixed (heal, damage, crit etc) flying number should be rendered without massive FPS drop
  • 100 sprites should be rendered without massive FPS drop

bbodi avatar Jul 25 '19 05:07 bbodi

StrEffectComponent and 'fire and forget' rendering is definitely useful for Statuses which does not have update functions, e.g. "AGI up" increases your walking speed and draw a short animation on the target entity on casting.
Managing animation in the status code would be messy and a waste of resources (since the animation itself is really short, but the status can stay for minutes on the target entity).

bbodi avatar Jul 25 '19 07:07 bbodi