This Week In Veloren 13

10 minute read29 April 2019

Authored by AngelOnFira

This week, we have content written by many different contributors! The topics include character animation, GitLab CI, asset loading, and FPS issues. This week's blog was edited by @AngelOnFira and has sections by @Slipped and @imbris. As always, the art section has been curated by @Pfau!

Thanks to this week's contributors, @Zesterer, @Timo, @Slipped, @desttinghim, @Pfau, @robojumper, @AngelOnFira, and @Algorhythm!

Programming

Character Animations by @Slipped


Here is a short clip displaying the new idle and run animations that were merged this week! The big addition to this run from earlier iterations is the velocity tilt, which leans the player forward giving the realistic impression of the character's speed. The change here is a new bone which works a little differently from the rest, but I'll digress to run through the basics of animation as we haven't really covered it in a weekly post yet.

Veloren will animate its mobs, NPCs, and players through a series of "skeletons" that cover specific categories. These categories include anything that needs to be distinctly animated, and the big two, are bipeds and quadrupeds. I've experimented with the quadruped (and also the dragon :P) skeleton mostly for fun and as learning experiences, but the biped has been my recent focus and is by far the most fleshed out. The skeletons work through the placement of 'bones' which usually get assigned a VOX model. They are then placed in space and given a rotation point--we want the foot to rotate as if the character has a leg. So when we add those feet we need to tell it that there's some imaginary "hip" above it where it pivots from.

Here's the movement for the left hand in the run animation:

next.l_hand.offset = Vec3::new(2.0 - wavecos * 2.5, 7.5, 12.0 + wave * 1.5);
next.l_hand.ori = Quaternion::rotation_y(wavecos * 0.9);

Here, we perform a few offsets (the constants) to get pieces in correct positions, but more interestingly we use a negative cosine wave in the X (forward/back) and a positive sine wave in the Z direction (up/down) to create a circle! (trigonometry really works, who knew?) Or more accurately, an oval, since we're multiplying by different constants. Finally, the next line is used to spin the hand back and forth through another wave expression. We use an equation like this for every body part, and we can switch a skeleton's 'state' (thanks @Qutrin!) to another state to get entirely new equations to run, such as in the idle animation.

Finally, the last thing I'll run through is the torso bone, which is what made velocity tilt possible. The torso bone is the only bone that doesn't have a VOX file, we keep it invisible and simply use it as a multiplier for all the other bones, meaning, that we can tell the torso to do something and everything will listen. Here, we simply tell the torso to lean and everything follows, but that's hardly all of its potential. Eventually, we can use the torso to tell the body to jump, or roll, or...hang glide. All exciting possibilities that we can start exploring in the future.

- @Slipped

FPS Counter Slowdown Investigation by @imbris

This week, @imbris and @YuriMomo worked together to solve some issues with FPS slowdowns. The following was written by @imbris.

After the addition of the FPS counter, some users noticed large FPS drops (60 FPS -> 20 FPS) when it was enabled while others were unaffected. Initially, we thought this might be due to an inefficiency in glyph caching since the FPS text is constantly changing and also because it causes the UI to be updated every frame triggering a reprocessing of widgets into draw commands which includes accessing the glyph cache for text widgets.

For context, the UI is redrawn every frame, but the meshes/draw commands used to draw the UI are only regenerated if the UI is updated (i.e. there is a change in one of the widgets). We also discovered that CPU-integrated GPUs did not experience any of this slowdown leading me to theorize that it could be these inefficiently cached glyphs being resent to the GPU's memory every frame. Nevertheless, after drawing the glyph cache to the screen it appeared that there were no changes from frame to frame even if the text on widgets was changing ruling out this as the potential culprit.

Consequently, I extended this idea to the sending of new meshes to the GPU every frame was the issue and wired up the L key to turn off generating new meshes when the UI updates. @YuriMomo then tested this using the key and found that FPS was restored. Upon profiling, @YuriMomo discovered the key culprit which is a function creating the vertex buffers for the meshes that are created when the UI is updated (~20 buffers are recreated when the hud UI is updated with a combined vertex count of ~1400).

The obvious solution, which I will be developing, is to not resend the vertices for unchanged widgets or at least not recreate the vertex buffers which hold the vertices for drawing the UI. However, a mystery still remains. After running the game for ~5 minutes this FPS drop seems to disappear. The only clue is a spike in swap_buffer call time before stabilization of the FPS.

- @imbris

GitLab CI setup by @AngelOnFira

The new Continuous Integration system is finally up and running! This new system makes use of Docker, which gives testing multiple benefits;

  • Every run will always be clean since the containers that run the tests restart every time.
  • Every run will always be predictable since the tests are all running from the same initial image. If it fails on one runner, it won't run on the other runners, and can, therefore, be debugged with relative ease.

You can see an example of what a new build looks like here. As you can see, there are different stages of the pipeline. First, the CI tries to compile the code. If this fails, then the entire pipeline fails. This is because you don't want to allow anyone to push broken builds to master.

Next, many tests are run in the "Test" and "Post-build" sections. Unit tests help prevent regression in the code and benchmark tests make sure that the codebase's performance is stable. These tests need to be written manually, which will be a task that comes up soon.

Clean-code, clippy, and coverage all look over the code base for code best practices. They don't prevent the code from being merged into master as of right now, but hopefully, in the future we enforce all of them passing.

Since we are getting information about builds, we've added some shields to the [Veloren Readme](https://gitlab.com/veloren/veloren)

We are still ironing out some issues with the new system, but if you are interested in donating compute time, we will start looking for volunteer runners next week. Keep an eye out for that!

Asset Loading by @imbris

@imbris has implemented a system to load assets which will allow sharing of assets so that they are not loaded up in different places in voxygen. This will save time loading them and save memory storing multiple copies. Additionally, asset loading code, in general, will be simplified. In the future, this system should abstract away from working with the filesystem and also allow the loading of assets provided by mods. The final goal is for an interface that looks similar to this:

let my_image = common::assets::load::<Image>("core.ui.backgrounds.city")?;

The only part missing is mapping names like "core.ui.backgrounds.city" to the filesystem. So it looks more like this right now:

let my_image = common::assets::load::<Image>("/voxygen/background/city.png")?;

An improvement from:

let my_image = image::load_from_memory(
common::assets::load("/voxygen/background/city.png")
.expect("Error loading file")
.as_slice(),
)?;

A global hash map holds all the loaded assets and ensures that an asset is only loaded once using ARC's (asynchronous reference counted pointer) to hold references to the assets which can be passed around by cloning them.

lazy_static! {
static ref ASSETS: RwLock<HashMap<String, Arc<dyn Any + 'static + Sync + Send>>> =
RwLock::new(HashMap::new());
}

An Asset trait is used to enable the loading of assets. Consequently, any type that implements this trait can be loaded using the same interface via this function:

pub fn load<A: Asset + 'static>(specifier: &str) -> Result<Arc<A>, Error> {
Ok(ASSETS
.write().unwrap()
.entry(specifier.to_string())
.or_insert(Arc::new(A::load(specifier)?))
.clone()
.downcast()?)
}

- @imbris

image_ids! Macro by @imbris

@Timo has created a macro for declaring structs to hold image ids for the UI. This macro removes the tedium involved with updating the image ids struct with a new image or removing an old one by generating the fields of a struct and the function to load all of the images from a much more compact definition. Previously something like this,

pub struct Imgs {
button1: ImgId,
button2: ImgId,
background: ImgId,
blank: ImgId,
}
impl Imgs {
pub fn load(ui: &mut Ui) -> Self {
let load_vox = |ui: &mut UI| {
// .vox loading code here
}
let load_img = |ui: &mut UI| {
// image loading code here
}
Imgs {
button1: load_vox("filename1.vox", ui),
button2: load_vox("filename1.vox", ui),
background: load_img("background.png", ui),
blank: ui.new_graphic(Graphic::Blank),
}
}
}

becomes this, notice there is only one place that needs to be changed here as opposed to the two above

image_ids! {
pub struct Imgs {
<VoxelGraphic>
button1: "filename1.vox",
button2: "filename1.vox",

<ImageGraphic>
background: "background.png",

<BlankGraphic>
blank: (),
}
}

Other work

There have been lots of other work done this week, here is a short recap,

  • @desttinghim worked on the server list UI and loading the config file from the OS config directory instead of the project directory.
  • @robojumper added ambient occlusion, a task that was by no means trivial!
  • @Timo did some code cleanup and fixed a couple of errors with ray casting code.

Current burndown status of 0.2

Veloren Code Challenge III

Make sure to have a go at this week's challenge! Also, take a look at last week's solutions!

You have just calculated the Harmonic Convergences of each day, and have found that there are three days with the same greatest GHC, the 52nd, 53rd, and 54th day. As you figure out what day these are, you realize that today is the 54th day! Knowing that you have very little time to spare, you run down to a plot of farmland that has been prepared for you. This land has been tilled to prepare for normal crops, but the three beans that you have should be planted in the most fertile soil.

Art by @Pfau

Updated UI design applied to the map window

Settings Window in the works

Quick sketch of Dwarven automatronics

WIP for the Worker by @Vechro