Skip to content

Model Scene

Swifter edited this page May 6, 2025 · 40 revisions

Prerequisites

General Usage

A ModelScene is used to easily manage mass environment/geometry object creation and recycling, as well as position objects that have a track. It can work in tandem with the remapper blender exporter to manage object positions in Blender.

Settings

Every ModelScene must be created with ModelSceneSettings.

The settings provide information about how to translate your model into beat saber objects. More specifically, it contains all of the model groups.

Here's what creating settings might look like:

// Create settings
const sceneSettings = new rm.ModelSceneSettings()

// Provide object for the default group
sceneSettings.setDefaultObjectGroup((d) => rm.geometry(d, {}))

Here, we're just creating a ModelSceneSettings object and then setting the default group to a geometry object.


Additionally, there are some parameters we can adjust on the settings object:

  • shouldInitializeObjects (boolean) This determines whether objects will be in-place at beat 0 of the map, even if the first scene isn't until later.

  • throwOnMissingGroup (boolean) This determines whether to throw an exception if a model object is calling for a group that isn't in the settings.

Builder Functions

Functions to create a ModelScene class are stored under a modelScene namespace for organization purposes:

image

There are 3 builder functions:

  • static
  • multipleAnimated
  • singleAnimated

The builder functions take in different parameters, but in general you are providing the information about the model and how it will be used (static, animated, etc).

You are also always providing settings for the scene to be created with.


  • static requests only a model input. This scene is intended to stay the entire map. If the model is animated, the very start of the animation is used.
const scene = rm.modelScene.static(sceneSettings, 'my_model')

  • multipleAnimated requests an array of objects called SceneSwitch, which essentially define a when a model will take place. The model property on it is a model input.
const scene = rm.modelScene.multipleAnimated(sceneSettings, [
    {
        model: 'my_model_1',
        beat: 0
    },
    {
        model: 'my_model_2',
        beat: 2
    }
])

There are also other properties on a SceneSwitch to describe how the animation is controlled, such as animationDuration, animationOffset, and loop.

// Switch to the scene at beat 1, start the animation at beat 3, and until beat 6, play the animation 3 times.

const scene = rm.modelScene.multipleAnimated(sceneSettings, [
    {
        model: 'my_model_1',
        beat: 1,
        animationOffset: 2,
        animationDuration: 3,
        loop: 3
    }
])

  • singleAnimated requests a model input, a time to start the animation, and the duration of the animation.
const scene = rm.modelScene.singleAnimated(sceneSettings, 'my_model', 2, 5)

Internally, this also uses the multi-scene approach, but with one SceneSwitch. If you want to access the SceneSwitch that is used, you can provide a callback.

const scene = rm.modelScene.singleAnimated(sceneSettings, 'my_model', 2, 5, (sceneSwitch) => {
    sceneSwitch.loop = 20
})

Model Input

Whenever a ModelScene builder function requests a "model input" (whether that be StaticModelInput or AnimatedModelInput), it's requesting the model and/or information on how to process the model.

The most simple case is just a path to a .rmmodel file (with the .rmmodel extension being optional).

const scene = rm.modelScene.static(sceneSettings, 'my_model')

Note: .rmmodel files are basically just JSON. They are outputted by remapper blender exporter.

You can also provide an array of model objects:

const scene = rm.modelScene.static(sceneSettings, [
    {
        position: [0, 0, 0],
        rotation: [0, 0, 0],
        scale: [1, 1, 1]
    },
    ...
])

Finally you can provide an object {}. The input property can be either a file path or model objects like before. The object also has other parameters you can use to process the model differently.

const scene = rm.modelScene.static(sceneSettings, {
    input: 'my_model',
    reverseAnimation: true,
    mirrorAnimation: true
})

Instantiating

Once you have a ModelScene, you can call instantiate on it to actually place the objects in a difficulty.

// Create settings
const sceneSettings = new rm.ModelSceneSettings()

// Provide object for the default group
sceneSettings.setDefaultObjectGroup((d) => rm.geometry(d, {}))

// Create ModelScene based on settings
const scene = rm.modelScene.static(sceneSettings, 'my_model')

// Create scene in our difficulty
scene.instantiate(map)

This is an asynchronous function and awaiting it will give you a report of the scene.

const scene = rm.modelScene.static('my_model')

...

const sceneInfo = await scene.instantiate(map)

Groups

Every time a model object references a group, it is expected to be "represented" by that group. Here are the different types of ways model objects can be represented:

  • Track groups: Animate a track with the model object's movement. There should only be one object per model with this group name.
  • Object groups: Create and animate a geometry/environment object with the model object's movement.

Creating Track Groups

To add a track group to the scene, simply provide the track you wish to effect.

scene.setTrackGroup('myTrack')

The group name is "myTrack", but it will also create an AnimateTrack event that animates "myTrack".

You also have the option to provide a transform to apply to the animation.

sceneSettings.setTrackGroup('myTrack', {
    position: [0, 10, 0] // move the track 10 meters up
})

Creating Object Groups

Let's say we have some group "cubes" that we want to be represented with a geometry cube.

We can call the setObjectGroup function to tell the scene to place geometry cubes for the "cubes" group.

function createGeometryCube(difficulty: rm.AbstractDifficulty) {
    return rm.geometry(difficulty, {
        type: 'Cube'
    })
}
sceneSettings.setObjectGroup('cubes', createGeometryCube)

You also have the option to provide a transform to apply to the object when it's spawned.

sceneSettings.setObjectGroup('cubes', createGeometryCube, {
    position: [0, 10, 0] // move the cube 10 meters up
})

Object groups also accept EnvironmentModelPieces, which summarize an environment object's id, lookupMethod required to create it, and the transform to fit it to a unit cube. Some are available in the rm.ENVIRONMENT_MODEL_PIECES constant.

sceneSettings.setObjectGroup('cubes', rm.ENVIRONMENT_MODEL_PIECES.BTS.PILLAR)

Default Group

The default group (when the group property on a ModelObject isn't defined) is an object group, and you can call setDefaultObjectGroup to set it.

function createGeometrySphere(difficulty: rm.AbstractDifficulty) {
    return rm.geometry(difficulty, {
        type: 'Sphere'
    })
}
sceneSettings.setDefaultObjectGroup(createGeometrySphere)

Fix Objects Not Being The Right Size

If you would like to help fit your geometry/environment objects to a unit cube, use the rm.debugFitObjectToUnitCube function which will spawn the object and let you inspect how closely each side fits to each face of the cube.

// Let's try to fit the BTS pillar to a unit cube.
function createBTSPillar(difficulty: rm.AbstractDifficulty) {
    return rm.environment(difficulty, {
        id: rm.regex().start().add('PillarPair').separate().add('PillarL').separate().add('Pillar').end(),
        lookupMethod: 'Regex',
    })
}

// I have adjusted this transform until it looked right
const transform: rm.Transform = {
    scale: [0.285714, 0.008868, 0.285714],
    position: [0, 0.4999, 0],
}

rm.debugFitObjectToUnitCube(map, createBTSPillar, transform)

image

Once the transform seems to fit properly, you can plug the object factory and transform into the setObjectGroup function.

Clone this wiki locally