Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Meshes & Models

Live Demo: Prefabs

Built-in primitives

The engine ships with a small set of basic geometric primitives.

#![allow(unused)]
fn main() {
use nightshade::prelude::*;

spawn_cube_at(world, Vec3::new(0.0, 1.0, 0.0));
spawn_sphere_at(world, Vec3::new(2.0, 1.0, 0.0));
spawn_plane_at(world, Vec3::zeros());
spawn_cylinder_at(world, Vec3::new(-2.0, 1.0, 0.0));
}

Loading glTF/GLB models

Basic loading

#![allow(unused)]
fn main() {
use nightshade::ecs::prefab::*;

const MODEL_BYTES: &[u8] = include_bytes!("../assets/character.glb");

fn load_model(world: &mut World) -> Option<Entity> {
    let result = import_gltf_from_bytes(MODEL_BYTES).ok()?;

    // Register textures
    for (name, (rgba_data, width, height)) in result.textures {
        world.queue_command(WorldCommand::LoadTexture {
            name,
            rgba_data,
            width,
            height,
        });
    }

    // Register meshes
    for (name, mesh) in result.meshes {
        mesh_cache_insert(&mut world.resources.mesh_cache, name, mesh);
    }

    // Spawn first prefab
    result.prefabs.first().map(|prefab| {
        spawn_prefab_with_skins(
            world,
            prefab,
            &result.animations,
            &result.skins,
            Vec3::zeros(),
        )
    })
}
}

With custom position/transform

#![allow(unused)]
fn main() {
fn spawn_model_at(world: &mut World, prefab: &Prefab, position: Vec3, scale: f32) -> Entity {
    let entity = spawn_prefab(world, prefab, position);

    if let Some(transform) = world.core.get_local_transform_mut(entity) {
        transform.scale = Vec3::new(scale, scale, scale);
    }

    entity
}
}

Filtered animation channels

A common preprocessing step is to remove root motion from animations so the gameplay code can drive the root transform itself.

#![allow(unused)]
fn main() {
let root_bone_indices: HashSet<usize> = [0, 1, 2, 3].into();

let filtered_animations: Vec<AnimationClip> = result
    .animations
    .iter()
    .map(|clip| AnimationClip {
        name: clip.name.clone(),
        duration: clip.duration,
        channels: clip
            .channels
            .iter()
            .filter(|channel| {
                // Skip translation on all bones
                if channel.target_property == AnimationProperty::Translation {
                    return false;
                }
                // Skip rotation on root bones
                if root_bone_indices.contains(&channel.target_node)
                    && channel.target_property == AnimationProperty::Rotation
                {
                    return false;
                }
                true
            })
            .cloned()
            .collect(),
    })
    .collect();
}

Manual mesh creation

Meshes can be built programmatically. The vertex format is fixed across the engine.

#![allow(unused)]
fn main() {
use nightshade::ecs::mesh::*;

let vertices = vec![
    Vertex {
        position: [0.0, 0.0, 0.0],
        normal: [0.0, 1.0, 0.0],
        tex_coords: [0.0, 0.0],
        ..Default::default()
    },
    Vertex {
        position: [1.0, 0.0, 0.0],
        normal: [0.0, 1.0, 0.0],
        tex_coords: [1.0, 0.0],
        ..Default::default()
    },
    Vertex {
        position: [0.5, 0.0, 1.0],
        normal: [0.0, 1.0, 0.0],
        tex_coords: [0.5, 1.0],
        ..Default::default()
    },
];

let indices = vec![0, 1, 2];

let mesh = Mesh {
    vertices,
    indices,
    ..Default::default()
};

mesh_cache_insert(&mut world.resources.mesh_cache, "triangle".to_string(), mesh);
}

Mesh component

Assigning a mesh to an entity is two writes plus the component mask.

#![allow(unused)]
fn main() {
let entity = world.spawn_entities(
    LOCAL_TRANSFORM | GLOBAL_TRANSFORM | RENDER_MESH | MATERIAL_REF,
    1
)[0];

world.core.set_render_mesh(entity, RenderMesh {
    name: "triangle".to_string(),
    id: None,
});

world.core.set_material_ref(entity, MaterialRef::new("default"));
}

Instanced meshes

For rendering many copies of the same mesh, the instanced path is one draw call instead of N.

#![allow(unused)]
fn main() {
world.core.set_instanced_mesh(entity, InstancedMesh {
    mesh_name: "tree".to_string(),
    instance_count: 1000,
    instance_data: instance_transforms,
});
}

Mesh cache

The cache is keyed by name and shared across the whole world.

#![allow(unused)]
fn main() {
// Check if mesh exists
if world.resources.mesh_cache.contains("cube") {
    // Mesh is available
}

// Get mesh data (for physics, etc.)
if let Some(mesh) = world.resources.mesh_cache.get("terrain") {
    let vertices: Vec<Vec3> = mesh.vertices.iter()
        .map(|v| Vec3::new(v.position[0], v.position[1], v.position[2]))
        .collect();
}
}

Skinned meshes

For animated characters with a skeleton, spawn through the skin-aware path.

#![allow(unused)]
fn main() {
let entity = spawn_prefab_with_skins(
    world,
    &prefab,
    &animations,
    &skins,
    position,
);

// The entity will have Skin and AnimationPlayer components
if let Some(player) = world.core.get_animation_player_mut(entity) {
    player.playing = true;
    player.looping = true;
}
}

Shadow casting

Whether a mesh casts shadows is controlled by the CASTS_SHADOW component.

#![allow(unused)]
fn main() {
// Enable shadow casting
world.core.add_components(entity, CASTS_SHADOW);
world.core.set_casts_shadow(entity, CastsShadow);

// Disable shadow casting (for UI elements, etc.)
world.core.remove_components(entity, CASTS_SHADOW);
}