How to detect broken assets with scene validation

Disclaimer: The code in this post is written using version 9.2.4200.0 of Simplygon in Python. 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

A lot of unexpected Simplygon behavior is caused by broken assets. To make it easier to detect these we introduced our scene validator in Simplygon 9.2. This blog post is about how to use it to validate your assets.

Prerequisites

This example will use the Simplygon Python API, but the same concepts can be applied to all other integrations of the Simplygon API.

Problem to solve

We want to detect if an asset has errors in it which can cause issues when processing through Simplygon.

Solution

We are going to create a simple Python script we can load our asset into. It will tell us if Simplygon's scene validator detects that something is wrong with it.

Load scene

First we add a function for loading a file as a Simplygon scene.

def LoadScene(sg: Simplygon.ISimplygon, path: str):
    # Create scene importer 
    sgSceneImporter = sg.CreateSceneImporter()
    sgSceneImporter.SetImportFilePath(path)
    
    # Run scene importer. 
    importResult = sgSceneImporter.RunImport()
    if not importResult:
        raise Exception('Failed to load scene.')
    sgScene = sgSceneImporter.GetScene()
    return sgScene

Check log for warnings and errors

To detect if any error has been raised we can check ErrorOccured. Any eventual error messages can be accessed through GetErrorMessages which puts them into the specified StringArray. After printing the errors we are going to clear the error messages using ClearErrorMessages.

def CheckLog(sg: Simplygon.ISimplygon):
    # Check if any errors occurred. 
    hasErrors = sg.ErrorOccurred()
    if hasErrors:
        errors = sg.CreateStringArray()
        sg.GetErrorMessages(errors)
        errorCount = errors.GetItemCount()
        if errorCount > 0:
            print("Errors:")
            for errorIndex in range(errorCount):
                errorString = errors.GetItem(errorIndex)
                print(errorString)
            sg.ClearErrorMessages()
    else:
        print("No errors.")

We are going to do exactly the same thing but for warnings. So instead of ErrorOccured and GetErrorMessages we are going to use WarningOccurred and GetWarningMessages.

    # Check if any warnings occurred. 
    hasWarnings = sg.WarningOccurred()
    if hasWarnings:
        warnings = sg.CreateStringArray()
        sg.GetWarningMessages(warnings)
        warningCount = warnings.GetItemCount()
        if warningCount > 0:
            print("Warnings:")
            for warningIndex in range(warningCount):
                warningString = warnings.GetItem(warningIndex)
                print(warningString)
            sg.ClearWarningMessages()
    else:
        print("No warnings.")

Specify file via argument

To be able to specify a file to run on we add an ArgumentParser with file_path as argument.

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("file_path", type=Path)
    p = parser.parse_args()
    if not p.file_path.exists():
        print("Error: Can not file file: " + p.file_path.as_posix())
        exit()

Putting it all together

We initialize Simplygon and load the specified file. After loading it we check the log for errors.

    sg = simplygon_loader.init_simplygon()

    # Load scene to process.     
    print("Loading scene...")
    sgScene = LoadScene(sg, p.file_path.as_posix())

    if sg is None:
        exit(Simplygon.GetLastInitializationError())

    CheckLog(sg)

    sg = None
    gc.collect()

Result

To test our script we are going to process two assets with it.

cube.obj

Cube.obj is a standard cube which contains no errors.

3D cube in Blender

When we process it via out script we get the following output telling us that no error could be detected.

> python error_check.py cube.obj
Loading scene...
No errors.
No warnings.

cube_error.obj

Inside cube_error.obj we have introduced 2 issues:

  • One face where the vertex is out of bounds.
  • One face where all 3 vertex indices are the same.
# Blender v2.93.5 OBJ File: ''
# www.blender.org
mtllib cube_triangulated_error.mtl
o Cube
v 1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 1.000000 -1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 1.000000
vt 0.875000 0.500000
vt 0.625000 0.750000
vt 0.625000 0.500000
vt 0.375000 1.000000
vt 0.375000 0.750000
vt 0.625000 0.000000
vt 0.375000 0.250000
vt 0.375000 0.000000
vt 0.375000 0.500000
vt 0.125000 0.750000
vt 0.125000 0.500000
vt 0.625000 0.250000
vt 0.875000 0.750000
vt 0.625000 1.000000
vn 0.0000 1.0000 0.0000
vn 0.0000 0.0000 1.0000
vn -1.0000 0.0000 0.0000
vn 0.0000 -1.0000 0.0000
vn 1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
usemtl Material
s off
f 5/1/1 3/2/1 1/3/1

# Broken face generating error
f 300/2/2 8/4/2 4/5/2

# Broken face generating a warning
f 1 1 1

f 2/9/4 8/10/4 6/11/4
f 1/3/5 4/5/5 2/9/5
f 5/12/6 2/9/6 6/7/6
f 5/1/1 7/13/1 3/2/1
f 3/2/2 7/14/2 8/4/2
f 7/6/3 5/12/3 6/7/3
f 2/9/4 4/5/4 8/10/4
f 1/3/5 3/2/5 4/5/5
f 5/12/6 1/3/6 2/9/6

When we process it via out script we get the following output telling us that it is broken in two ways; invalid indices and triangle with area 0. The error means that the asset can not be processed and warnings means that the asset can be processed, but might cause unexpected output.

> python error_check.py cube_error.obj
Loading scene...
Errors:
Found invalid indices in geometry when calculating extents.
Warnings:
------------------------------------------------------------
In SceneImporter: (ImportFilePath = cube_error.obj):
The validation of the input scene failed.
The non-masked validation error code (all detected errors) is: 0x210
The masked validation error code (errors that cannot be fixed) is: 0x210
Number of non-masked errors: 1
Error(1): The mesh ('default') found in the scene failed geometry validation. Details follow.
        (1): Triangle 2 contains the same vertex index at least twice.
Triangle #2 info:
        Vertex Ids: { 2 , 2 , 2 }
(2): Triangle 2 has an area less than the squared epsilon value of floating point numbers. This is a very small area, very close to 0, and this might cause issues with some processings, such as calculating triangle normals etc. If you experience issues, please consider resizing/normalizing the geometry, and stability may improve.
Triangle #2 info:
        Vertex Ids: { 2 , 2 , 2 }
        Vertex Coords: {
                { 1 , 1 , -1}
                { 1 , 1 , -1}
                { 1 , 1 , -1}
        }
        Triangle Area: { 0 }

------------------------------------------------------------
Input scene validation failed.
The input scene validation Failed, and the global setting InputSceneValidationAction is neither set to Skip or RaiseError, so the invalid scenes error is logged, but the processing continues. 

One thing to notice is that Simplygon's scene validation does not detect all possible asset errors, but it is useful as a smoke test.

Complete script

The complete script can be found below.

# Copyright (c) Microsoft Corporation. 
# Licensed under the MIT License. 

import gc
import argparse

from pathlib import Path
from simplygon import simplygon_loader
from simplygon import Simplygon


def LoadScene(sg: Simplygon.ISimplygon, path: str):
    # Create scene importer 
    sgSceneImporter = sg.CreateSceneImporter()
    sgSceneImporter.SetImportFilePath(path)
    
    # Run scene importer. 
    importResult = sgSceneImporter.RunImport()
    if not importResult:
        raise Exception('Failed to load scene.')
    sgScene = sgSceneImporter.GetScene()
    return sgScene

def CheckLog(sg: Simplygon.ISimplygon):
    # Check if any errors occurred. 
    hasErrors = sg.ErrorOccurred()
    if hasErrors:
        errors = sg.CreateStringArray()
        sg.GetErrorMessages(errors)
        errorCount = errors.GetItemCount()
        if errorCount > 0:
            print("Errors:")
            for errorIndex in range(errorCount):
                errorString = errors.GetItem(errorIndex)
                print(errorString)
            sg.ClearErrorMessages()
    else:
        print("No errors.")
    
    # Check if any warnings occurred. 
    hasWarnings = sg.WarningOccurred()
    if hasWarnings:
        warnings = sg.CreateStringArray()
        sg.GetWarningMessages(warnings)
        warningCount = warnings.GetItemCount()
        if warningCount > 0:
            print("Warnings:")
            for warningIndex in range(warningCount):
                warningString = warnings.GetItem(warningIndex)
                print(warningString)
            sg.ClearWarningMessages()
    else:
        print("No warnings.")

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("file_path", type=Path)
    p = parser.parse_args()
    if not p.file_path.exists():
        print("Error: Can not file file: " + p.file_path.as_posix())
        exit()

    sg = simplygon_loader.init_simplygon()

    # Load scene to process.     
    print("Loading scene...")
    sgScene = LoadScene(sg, p.file_path.as_posix())

    if sg is None:
        exit(Simplygon.GetLastInitializationError())

    CheckLog(sg)

    sg = None
    gc.collect()
⇐ Back to blog post list