Vertex locks in Unity
Written by Jesper Tingvall, Product Expert, Simplygon
Disclaimer: The code in this post is written using version 10.3.2100.0 of Simplygon and Unity 2022.3.13f1. If you encounter this post at a later stage, some of the API calls might have changed. However, the core concepts should still remain valid.
Introduction
In this blog we'll showcase how to use vertex locks in Unity to protect areas during optimization. We do this by editing the Simplygon scene after export from Unity.
For more information about how to protect geometry in the general case consult this blog on Protecting features using vertex locks and weights.
Prerequisites
This example will use the Simplygon 10.3 plugin in Unity. The same concepts can be applied to all other integrations of the Simplygon API. Simplygon's Unity plugin has changed from 10.2 to 10.3 so this blog is not applicable to older versions.
Problem to solve
We have an asset where we want certain areas to not be optimized during reduction. In our example asset's (Television 01 by Gabriel Radić) case we want to preserve all vertexes in the glass screen.
Solution
We will start with the most simple approach then work our way towards vertex locks.
Basic 50% reduction
Let us first try to use ordinary 50% reduction from user interface. We create a Template → Basic → Reduction pipeline and run with triangle ratio target of 50%.
As expected the model is reduced, including in the parts we want to preserve.
Vertex weights
To use vertex weights first we need to paint our model with vertex colors. We start by painting everything grey, then the areas we want to keep bright red. We can specify which color component that it should use to guide optimization. In our case we will go with first color component, red.
We use Template → Advanced → Reduction pipelinen with reduction ratio of 50% as target. Under VertexWeight settings we set UseVertexWeightsInReducer to True
. How much the colors should influence optimization can be changed with WeightsFromColorMultiplier. Let us try first with 4.
The geometry is better preserved, but it is still changed. Even if we increase WeightsFromColorMultiplier to 10 this is still the case. In order to explain why we need to understand how vertex weights work. For each possible optimization the introduced error is scaled by WeightsFromColorMultiplier and the vertex color. If we have no or very small introduced error, as we for example would get if we optimize a flat surface, then it does not have any effect.
Vertex locks
Now let us use vertex locks. For this we need to use scripting. First step is to export the scene from Unity as a Simplygon scene that we can manipulate the scene graph in.
Export from Unity to Simplygon scene
In our Simple reduction using script example we used simplygonProcessing.Run( ... )
. It takes care of export from Unity, processing and importing. Since we want to manipulate the scene after export from Unity we need to do it in a different way.
We start by initializing Simplygon and creating a reduction pipeline using a helper function.
using (ISimplygon simplygon = global::Simplygon.Loader.InitSimplygon (out EErrorCodes simplygonErrorCode, out string simplygonErrorMessage))
{
// if Simplygon handle is valid
if (simplygonErrorCode == EErrorCodes.NoError)
{
using var simplygonReductionPipeline = CreateReductionPipeline(simplygon);
Then we create a SimplygonProcessing
object and initialize is with our reduction pipeline. To export from Unity to a Simplygon scene we run Export
.
var simplygonProcessing = new SimplygonProcessing();
simplygonProcessing.Initialize(simplygon, simplygonReductionPipeline, Selection.gameObjects.ToList());
using (spScene exportedScene = simplygonProcessing.Export(true))
After the scene is exported we can manipulate the scene. Here we run our function to add vertex locks. We also set up some Unity specific settings.
// Add vertex locks
AddVertexLocksToScene(exportedScene);
// Set Unity specific settings
simplygon.SetGlobalDefaultTangentCalculatorTypeSetting(ETangentSpaceMethod.MikkTSpace);
simplygon.EnableLogToFile(ELogLevel.Info, (uint)ELogDecoration.AllDecorations);
We can now process the scene by calling RunScene
on our pipeline.
var error = simplygonReductionPipeline.RunScene(exportedScene, EPipelineRunMode.RunInThisProcess);
Lastly, if everything went well, we import the result into Simplygon with Import
.
if (error == EErrorCodes.NoError)
{
simplygonProcessing.Import(true, false);
}
Add vertex locks to geometry
This functions adds vertex locks to a geometry where depending on how red it's vertex color field is. We start by adding a vertex lock field as well as checking that the geometry contains vertex colors.
private static void AddVertexLocksToGeometry(spGeometryData geometry)
{
Debug.Log("Adding vertex locks to " + geometry.GetName());
// Add vertex lock field if needed.
if (geometry.GetVertexLocks().IsNull())
geometry.AddVertexLocks();
// Unlock all vertices.
var locks = geometry.GetVertexLocks();
// Get the color channel containing the weights
var colors = geometry.GetColors(0);
if (colors.IsNull())
{
Debug.LogWarning("Geometry is missing vertex colors. No vertex locks will be created." + geometry.GetName());
return;
}
var color_data = colors.GetData();
var vertex_ids = geometry.GetVertexIds();
var vertex_ids_data = vertex_ids.GetData();
if (colors.GetTupleCount() != vertex_ids_data.GetItemCount())
{
Debug.LogError("Expecting vertex color and vertex id count to be the same.");
return;
}
We then loop through all vertex ids and check if the red color component is above a certain limit. If so we set the vertex lock of that vertex id to True
.
// we'll just check the red component of the vertex color as we're assuming a gray scale.
for (uint i = 0; i < vertex_ids_data.GetItemCount(); i++)
{
var weight = color_data.GetItem(i * 4);
if (weight > VertexLockWeightTreshold)
{
var vertex_id = vertex_ids_data.GetItem(i);
if (vertex_id < 0)
{
continue;
}
else
{
locks.SetItem(vertex_id, true);
}
}
}
}
Add vertex locks to entire scene
To interate through all geometries in the scene we create a selection set that contains all scene meshes. We can then iterate through it and call our AddVertexLocksToGeometry
function.
static void AddVertexLocksToScene(spScene scene)
{
var scene_meshes_selection_set_id = scene.SelectNodes(SceneMeshTypeName);
var scene_meshes_selection_set = scene.GetSelectionSetTable().GetSelectionSet(scene_meshes_selection_set_id);
for (uint i = 0; i < scene_meshes_selection_set.GetItemCount(); i++)
{
var scene_mesh = spSceneMesh.SafeCast(scene.GetNodeByGUID(scene_meshes_selection_set.GetItem(i)));
var geometry = scene_mesh.GetGeometry();
if (!geometry.IsNull())
{
AddVertexLocksToGeometry(geometry);
}
}
scene_meshes_selection_set = null;
scene.GetSelectionSetTable().RemoveItem(scene_meshes_selection_set_id);
}
Result
After processing the asset with our script the area we marked for keeping, the screen, is kept intact. It is worth pointing out that with both vertex locks and vertex weights the areas aside from the screen gets less geometry allocated to them them. This is because we are still doing a 50% reduction, and if we allocate more geometry in one area we need to spend less in other areas. This would not be the case if we used screen size or max deviation as reduction target.
Complete script
This script should be placed in an Editor
folder.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
using Simplygon.Unity.EditorPlugin;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace Simplygon.Examples.VertexLock
{
public class VertexLocks
{
static string SceneMeshTypeName = "SceneMesh";
static float VertexLockWeightTreshold = 0.8f;
[MenuItem("Simplygon/Reduce with vertex locks")]
static void ReduceWithVertexLocks()
{
if (Selection.gameObjects.Length > 0)
{
using (ISimplygon simplygon = global::Simplygon.Loader.InitSimplygon
(out EErrorCodes simplygonErrorCode, out string simplygonErrorMessage))
{
// if Simplygon handle is valid
if (simplygonErrorCode == EErrorCodes.NoError)
{
using var simplygonReductionPipeline = CreateReductionPipeline(simplygon);
var simplygonProcessing = new SimplygonProcessing();
simplygonProcessing.Initialize(simplygon, simplygonReductionPipeline, Selection.gameObjects.ToList());
using (spScene exportedScene = simplygonProcessing.Export(true))
{
if (exportedScene != null && !exportedScene.IsNull())
{
// Add vertex locks
AddVertexLocksToScene(exportedScene);
// Set Unity specific settings
simplygon.SetGlobalDefaultTangentCalculatorTypeSetting(ETangentSpaceMethod.MikkTSpace);
simplygon.EnableLogToFile(ELogLevel.Info, (uint)ELogDecoration.AllDecorations);
var error = simplygonReductionPipeline.RunScene(exportedScene, EPipelineRunMode.RunInThisProcess);
if (error == EErrorCodes.NoError)
{
simplygonProcessing.Import(true, false);
}
}
}
}
// if invalid handle, output error message to the Unity console
else
{
Debug.LogError("Simplygon initializing failed!");
}
}
}
}
static void AddVertexLocksToScene(spScene scene)
{
var scene_meshes_selection_set_id = scene.SelectNodes(SceneMeshTypeName);
var scene_meshes_selection_set = scene.GetSelectionSetTable().GetSelectionSet(scene_meshes_selection_set_id);
for (uint i = 0; i < scene_meshes_selection_set.GetItemCount(); i++)
{
var scene_mesh = spSceneMesh.SafeCast(scene.GetNodeByGUID(scene_meshes_selection_set.GetItem(i)));
var geometry = scene_mesh.GetGeometry();
if (!geometry.IsNull())
{
AddVertexLocksToGeometry(geometry);
}
}
scene_meshes_selection_set = null;
scene.GetSelectionSetTable().RemoveItem(scene_meshes_selection_set_id);
}
static spReductionPipeline CreateReductionPipeline(ISimplygon simplygon)
{
var simplygonReductionPipeline = simplygon.CreateReductionPipeline();
using var simplygonReductionSettings = simplygonReductionPipeline.GetReductionSettings();
simplygonReductionSettings.SetReductionTargets(EStopCondition.All, true, false, false, false);
simplygonReductionSettings.SetReductionTargetTriangleRatio(0.5f);
return simplygonReductionPipeline;
}
private static void AddVertexLocksToGeometry(spGeometryData geometry)
{
Debug.Log("Adding vertex locks to " + geometry.GetName());
// Add vertex lock field if needed.
if (geometry.GetVertexLocks().IsNull())
geometry.AddVertexLocks();
// Unlock all vertices.
var locks = geometry.GetVertexLocks();
// Get the color channel containing the weights
var colors = geometry.GetColors(0);
if (colors.IsNull())
{
Debug.LogWarning("Geometry is missing vertex colors. No vertex locks will be created." + geometry.GetName());
return;
}
var color_data = colors.GetData();
var vertex_ids = geometry.GetVertexIds();
var vertex_ids_data = vertex_ids.GetData();
if (colors.GetTupleCount() != vertex_ids_data.GetItemCount())
{
Debug.LogError("Expecting vertex color and vertex id count to be the same.");
return;
}
// we'll just check the red component of the vertex color as we're assuming a gray scale.
for (uint i = 0; i < vertex_ids_data.GetItemCount(); i++)
{
var weight = color_data.GetItem(i * 4);
if (weight > VertexLockWeightTreshold)
{
var vertex_id = vertex_ids_data.GetItem(i);
if (vertex_id < 0)
{
continue;
}
else
{
locks.SetItem(vertex_id, true);
}
}
}
}
}
}