Quick Reply
Search this Thread
Instructor
Original Poster
#1 Old 11th May 2025 at 6:15 AM Last edited by Butterbot : 11th May 2025 at 6:42 AM.
[Tutorial] Creating New Geostates with S3PE + Blender
I. Introduction

The following describes the creation of a new geostate using S3PE and Blender in an object mesh with no existing geostates. This method presents a marked advantage over TSRW, in that TSRW must copy an existing geostate to add one--it cannot create brand new geostates for objects that do not contain at least one already.

Part of this process will entail adding new mesh groups using S3PE.

Note that the object's scripting (default or modded) must support geostates for these to appear in-game. Although the steps in this tutorial deal with the high LOD mesh only, geostates for the low LOD mesh can be created using essentially the same approach to edit the MODL.

The example object in this tutorial is the Top Shelf Bar Shelving decorative sculpture from Late Night. The idea is to create geostates in this object that will eventually enable it to function as a nectar rack, varying the bottles displayed depending on the fullness of its inventory. The final object is here. (This tutorial will only cover the creation of the necessary geostates, not how to recreate the entire project.)

II. Prerequisites

Working knowledge of:

III. Background + Overview

Geostates are used to control the visibility of mesh groups in an object according to programmatic criteria. This enables an object's geometry to reflect certain states, as determined by the object's scripting. A single geostate can dictate the visibility of more than one mesh group. A mesh group that should always be visible regardless of the object's state does not need to have geostate visibility defined.

For instance, a (theoretical) trophy case object can be made to look empty, half-full, or full, according to how many trophies are stored in its inventory. Its scripting would support three geostates: empty, half-full, and full. The code sets the appropriate geostate as the trophy case inventory is filled or emptied. The trophy case model can be thought of as two components: 1) the case itself, which is always visible and does not change in appearance and 2) the collection of trophies, which will vary depending of whether the case is empty, half-full, or full. The actual object, then, can be composed of three mesh groups: trophy case, half collection, full collection.

Regardless of the geostate being set by the script, the trophy case mesh group is always visible. When the geostate is set to empty, both the half collection and full collection mesh groups are invisible. With the half-full geostate, the half collection mesh group is visible and the full collection mesh group is invisible. When the geostate is set to full, the full collection mesh group is visible while the half collection mesh group is not. This is summarized in the following table:

Mesh Group \ GeostateEmptyHalf-FullFull
Trophy caseVisibleVisibleVisible
Half collection--Visible--
Full collection----Visible
Thus, in the metadata of the half collection and full collection mesh groups, the three geostates should be defined and the settings for "visibility" adjusted accordingly. The trophy case mesh group needs no geostate definition, as it's always visible.

Each new mesh group requires a new VertexBufferIndex and IndexBufferIndex entry, pointing to corresponding new buffer array resources to be added as chunk entries in the MLOD of the object. The TGIBlockIndex of each of these entries is 2 numbers lower (for some objects, it might be 1--examine an existing mesh group within the object to be sure) than the actual indices of the chunk entries containing the new resources. For example, a VertexBufferIndex TGIBlockIndex of 09 would point to ChunkEntry 0B (where the actual vertex buffer array is stored).

(Strictly speaking, multiple mesh groups can share the same buffer arrays. Under these circumstances, geostates can be defined by varying their MinVertexIndex, amongst other parameters. However, since only a starting index position can be specified in this manner, the vertices used in any given geostate must be contiguous [using non-contiguous vertices would require the ability to specify index ranges]. EA's tooling likely makes this a non-issue, but it is problematic for modders since there is no obvious way to ensure that vertices in a given geostate are numbered contiguously with current community-produced meshing tools.)

IV. Procedure

Part 1
  1. Determine the geostate names (or more importantly, their FNV32 hashes) used by the scripting class. This can be done by examining the strings used by the NectarRack.UpdateVisualState method or by looking at the geostate name hashes in EA's nectar rack object. For the second approach:
    1. Clone a nectar rack using S3OC and open the .package file in S3PE.
    2. Highlight the high LOD mesh (0x01D10F34-0x08000000-0x000000000861CA3C in the case of the cheap nectar rack) and click the "Grid" button.
    3. Expand the ChunkEntries row.
    4. Go to [00] ChunkEntries -> RCOLBlock -> Meshes. The mesh groups contained in this MLOD are listed here.
    5. Expand [0] Meshes. Note that GeometryStates contains 3 entries.
    6. Expand each of the GeometryStates entries and note the hashes in the Name row. In this case, they are 0x4CF9B596, 0x8A6C4E39, and 0x95A36FFE. These are the FNV32 hashes of "base", "halffull", and "full", respectively.
  2. Clone the Top Shelf Bar Shelving (sculptureFloorDiveCabinet3x1) using S3OC and open the .package file in S3PE. This is the object for which new geostates will be created.
  3. Examine the existing mesh groups and determine how the overall mesh should be structured (and how many new mesh groups are required) in order to implement geostates:
    1. Right-click the high LOD mesh (0x01D10F34-0x28000000-0x0000000028577A82) and select "MLOD Preview".
    2. To the left of the model viewer is a list of hashes; each is a mesh group contained in this MLOD. Clicking through the list, note how each group represents a part of the whole object's mesh:

      Mesh GroupPart of Object
      0x4958DC82Dropshadow
      0x63B17669Back mirror
      0x3167F41FCabinet + bottles
      0x3C052B84Lights
    3. As a nectar rack, only the bottles on the shelves need to change according to the state of the inventory. The dropshadow, back mirror, lights, and cabinet remain visible at all times, unaffected by the number of nectar bottles stored. From examining EA's nectar rack, it is now clear that the display of bottles can appear in three ways, each corresponding to a geostate supported by the NectarRack scripting class: empty, half-full, full. These can be implemented by separating the bottles from the cabinet mesh group into two new mesh groups, one consisting of half the bottles (rack is half-full), the other one consisting of all the bottles (rack is full); the empty geostate simply entails making both new mesh groups invisible. The whole object's mesh can thus be re-conceptualized as follows:

      Mesh GroupPart of Object
      0x4958DC82Dropshadow
      0x63B17669Back mirror
      0x3167F41FCabinet (without bottles)
      0x3C052B84Lights
      New mesh group 1Half of the bottles
      New mesh group 2All bottles
  4. Exit the model viewer. Highlight the high LOD mesh again and click the "Grid" button.
  5. Go to ChunkEntries -> [00] ChunkEntries -> RCOLBlock -> Meshes. The four mesh groups listed here are the ones visualized in the MLOD preview.
  6. Expand each mesh group entry and note that GeometryStates contains 0 entries in each mesh group as this object currently has no geostates.
  7. Create the two new mesh groups.
    1. Highlight the Meshes row and click the small [...] button to the far right. This will open a new window.
    2. Highlight [2] Mesh on the left side of the window. Check that the hash in its Name row identifies it as the "cabinet + bottles" mesh group: 0x3167F41F.
    3. Click the "Copy" button twice. This copies the "cabinet + bottles" mesh group twice to create two new mesh groups, [4] Mesh and [5] Mesh, which now appear in the left side of the window. Since these new mesh groups are meant to contain the geometry for the bottles, the "cabinet + bottles" mesh group is used as their basis. This is convenient as any unneeded geometry (e.g. the cabinet) can easily be deleted in Blender later.
    4. Change the Name hash of the new mesh groups to something different from the name hashes of the existing mesh groups. In this example, 0x3167F412 is used for [4] Mesh and 0x3167F413 for [5] Mesh. These changes can be arbitrary.
    5. Click the "OK" button to close the window. Click the "Commit" button to apply these changes.

Part 2
  1. Create buffer arrays for the two new mesh groups.
    1. Open the high LOD mesh again in Grid view.
    2. Go to ChunkEntries -> [00] ChunkEntries -> RCOLBlock -> Meshes. There should now be six mesh groups listed.
    3. Expand [2] Mesh. Expand both VertexBufferIndex and IndexBufferIndex; note their TGIBlockIndex entries. In this example, they are 07 and 08, respectively.
    4. VertexBufferIndex and IndexBufferIndex are offset from the actual ChunkEntries containing the buffer arrays. This offset, usually 1 or 2, can vary from object to object. To determine the offset, expand the ChunkEntries most likely to contain the arrays and look for rows labeled SwizzleInfo (for the array referenced by VertexBufferIndex) and Buffer (for both indices). In this example, the ChunkEntries to examine are 08 (07 + offset of 1), 09 (07 + offset of 2; 08 + offset of 1), and 0A (08 + offset of 2). Go to ChunkEntries -> [08]/[09]/[0A] ChunkEntries -> RCOLBlock. Note that [08] ChunkEntries and [09] ChunkEntries contain the buffer-related rows, so the offset in this object is 1. Note also that these are the ChunkEntries that need to be copied in the next few steps for the new mesh groups to have their own buffer arrays.
    5. Highlight the ChunkEntries row and click the small [...] button to the far right. This will open a new window.
    6. Highlight [08] ChunkEntry on the left side of the window and click the "Copy" button. This makes a copy of the vertex buffer array for one of the new mesh groups. Note the newly created row at the bottom of the column: [18] ChunkEntry.
    7. Highlight [09] ChunkEntry on the left side of the window and click the "Copy" button. This makes a copy of the index buffer array for one of the new mesh groups. Note the newly created row at the bottom of the column: [19] ChunkEntry.
    8. Repeat steps 6 and 7 to create the buffer arrays for the second new mesh group. They should occupy [1A] ChunkEntry and [1B] ChunkEntry.
    9. Click the "OK" button to close the window.
    10. Go to ChunkEntries -> [00] ChunkEntries -> RCOLBlock -> Meshes -> [4] Meshes (the first new mesh group).
    11. Expand VertexBufferIndex. The TGIBlockIndex needs to be changed to point to the vertex buffer array ChunkEntry created in step 6. The index position of that ChunkEntry is 18. Knowing that buffer indices are offset by 1 (see step 4), theTGIBlockIndex should be 17 (18 = 17 + offset of 1).
    12. Expand IndexBufferIndex. The TGIBlockIndex needs to be changed to point to the index buffer array ChunkEntry created in step 7. The index position of that ChunkEntry is 19. Knowing that buffer indices are offset by 1 (see step 4), theTGIBlockIndex should be 18 (19 = 18 + offset of 1).
    13. Go to ChunkEntries -> [00] ChunkEntries -> RCOLBlock -> Meshes -> [5] Meshes (the second new mesh group). Repeat steps 11 and 12 using the remaining two new buffer array ChunkEntries. This should produce a VertexBufferIndex TGIBlockIndex of 19 and a IndexBufferIndex TGIBlockIndex of 1A.
    14. Click the "Commit" button to apply these changes.
  2. Determine final mesh group structure and visibility according to geostate.
    1. Right-click the high LOD mesh again and select "MLOD Preview". Examine the hashes (mesh groups) on the left side of the model viewer and click through them to note which part of the whole mesh each represents. There should be three identical mesh groups: 0x3167F41F, 0x3167F412, and 0x3167F413 (the last two being the two new mesh groups copied from the first).
    2. Decide on the role of each mesh group according to the geostates needed. For a nectar rack, the goal is to control the visibility of two collections of bottles independently of each other and of the cabinet, so there should be a mesh group consisting of the cabinet alone, another mesh group of half of the bottles, and a final mesh group of all the bottles. It matters not at this point which role is assigned to which of the three mesh groups, as long as the mesh is edited according to the assigned role. All parts associated to the cabinet (cabinet, dropshadow, mirror, lights) should be visible regardless of the geostate. The two bottles mesh groups will change according to the geostate determined by the number of nectar bottles in the inventory. This tutorial's example object is organized thus:

      Mesh GroupPart of ObjectGeostate: EmptyGeostate: Half-FullGeostate: Full
      0x4958DC82DropshadowVisibleVisibleVisible
      0x63B17669Back mirrorVisibleVisibleVisible
      0x3167F41FAll bottles----Visible
      0x3C052B84LightsVisibleVisibleVisible
      0x3167F412CabinetVisibleVisibleVisible
      0x3167F413Half of the bottles--Visible--
    3. Exit the model viewer.
  3. Export the high LOD mesh to Blender, make the necessary edits, and import it back into S3PE. Verify that the edited mesh groups are OK using MLOD Preview. These are the edits made to the tutorial object:
    • 0x3167F41F (Blender mesh group 02): delete the cabinet
    • 0x3167F412 (Blender mesh group 04): delete all bottles
    • 0x3167F413 (Blender mesh group 05): delete the cabinet and half of the bottles

Part 3
  1. Create the geostates. The only mesh groups that need geostates are those whose visibility vary--the two bottles mesh groups, 0x3167F41F and 0x3167F413, in this example.
    1. Open the newly imported high LOD mesh in Grid view.
    2. Go to ChunkEntries -> [00] ChunkEntries -> RCOLBlock -> Meshes -> [2] Meshes and expand it. Verify that the Name row contains the correct hash: 0x3167F41F.
    3. Highlight the GeometryStates row. Click the small [...] button to the far right. This opens a new window.
    4. Click the "Add" button three times. This adds three entries to the left side of the window.
    5. Click the "OK" button to close the window.
    6. Under GeometryStates, there are now three entries. Expand each of them.
    7. In the Name row of each expanded geostate, enter one of the geostate name hashes gleaned from the scripting class or by examining EA's nectar rack at the very beginning of the Procedure section (see Part 1, step 1). In the example object, [0] GeometryStates gets 0x4CF9B596 (FNV32 hash of "base"), [1] GeometryStates gets 0x8A6C4E39 ("halffull"), and [2] GeometryStates gets 0x95A36FFE ("full").
    8. Since this mesh group ([2] Meshes: 0x3167F41F) contains all the bottles, it should only be visible when the geostate is set to "full", so the StartIndex, VertexCount, and PrimitiveCount of [2] GeometryStates ("full") need to be set. [0] GeometryStates and [1] GeometryStates can be left as is.
      1. Scroll up a bit to note the StartIndex, VertexCount, and PrimitiveCount rows of [2] Meshes itself.
      2. Copy these values into their respective rows under [2] GeometryStates. Collapse [2] Meshes.
    9. Go to ChunkEntries -> [00] ChunkEntries -> RCOLBlock -> Meshes -> [5] Meshes. Verify that the Name row contains the correct hash: 0x3167F413. Repeat steps 3 to 7 for this mesh group.
    10. Since this mesh group ([5] Meshes: 0x3167F413) contains only half of the bottles, it should only be visible when the geostate is set to "halffull", so the StartIndex, VertexCount, and PrimitiveCount of [1] GeometryStates ("halffull") need to be set. [0] GeometryStates and [2] GeometryStates can be left as is.
      1. Scroll up a bit to note the StartIndex, VertexCount, and PrimitiveCount rows of [5] Meshes itself.
      2. Copy these values into their respective rows under [1] GeometryStates.
  2. Click the "Commit" button to apply the changes. Save the .package file.

V. Addendum

Alternately, the two "bottles" mesh groups can be edited to contain half of the bottles each. They can then both be set to visible for the "full" geostate. This would further minimize the overall polycount of the object.

Setting up "empty" geostates in the mesh groups prior to exporting to Blender will cause the subsequent import back into S3PE to fail.

VI. Acknowledgements

Many thanks to @peter9g for explaining the relationship between geostates and mesh groups.
Back to top