All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.javagl.jgltf.obj.model.ObjGltfModelCreator Maven / Gradle / Ivy

/*
 * www.javagl.de - JglTF
 *
 * Copyright 2015-2016 Marco Hutter - http://www.javagl.de
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
package de.javagl.jgltf.obj.model;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;

import de.javagl.jgltf.model.GltfConstants;
import de.javagl.jgltf.model.GltfModel;
import de.javagl.jgltf.model.MaterialModel;
import de.javagl.jgltf.model.MeshPrimitiveModel;
import de.javagl.jgltf.model.Optionals;
import de.javagl.jgltf.model.creation.GltfModelBuilder;
import de.javagl.jgltf.model.creation.MeshPrimitiveBuilder;
import de.javagl.jgltf.model.impl.DefaultMeshModel;
import de.javagl.jgltf.model.impl.DefaultMeshPrimitiveModel;
import de.javagl.jgltf.model.impl.DefaultNodeModel;
import de.javagl.jgltf.model.impl.DefaultSceneModel;
import de.javagl.jgltf.model.io.IO;
import de.javagl.jgltf.obj.ObjNormals;
import de.javagl.obj.Mtl;
import de.javagl.obj.MtlReader;
import de.javagl.obj.Obj;
import de.javagl.obj.ObjData;
import de.javagl.obj.ObjGroup;
import de.javagl.obj.ObjReader;
import de.javagl.obj.ObjSplitting;
import de.javagl.obj.ObjUtils;
import de.javagl.obj.ReadableObj;

/**
 * A class for creating {@link GltfModel} objects from OBJ files 
 */
public class ObjGltfModelCreator
{
    
    /**
     * The logger used in this class
     */
    private static final Logger logger = 
        Logger.getLogger(ObjGltfModelCreator.class.getName());
    
    /**
     * The log level
     */
    private static final Level level = Level.INFO;
    
    /**
     * The {@link MtlMaterialHandler}
     */
    private MtlMaterialHandler mtlMaterialHandler;
    
    /**
     * The component type for the indices of the resulting glTF. 
     * For glTF 1.0, this may at most be GL_UNSIGNED_SHORT.  
     */
    private int indicesComponentType = GltfConstants.GL_UNSIGNED_SHORT;
    
    /**
     * A function that will receive all OBJ groups that should be processed,
     * and may (or may not) split them into multiple parts. 
     */
    private Function> 
        objSplitter;
    
    /**
     * For testing: Assign a material with a random color to each 
     * mesh primitive that was created during the splitting process
     */
    private boolean assigningRandomColorsToParts = false;
    
    /**
     * Whether technique-based materials should be used
     */
    private boolean techniqueBasedMaterials = false;
    
    /**
     * Whether each primitive should be in its own mesh, attached
     * to its own node
     */
    private boolean oneMeshPerPrimitive = false;
    
    /**
     * Default constructor
     */
    public ObjGltfModelCreator()
    {
        setIndicesComponentType(GltfConstants.GL_UNSIGNED_SHORT);
    }
    
    /**
     * Set whether technique-based (glTF 1.0) materials should be created
     * 
     * @param techniqueBasedMaterials Whether technique-based materials should
     * be created
     */
    public void setTechniqueBasedMaterials(boolean techniqueBasedMaterials)
    {
        this.techniqueBasedMaterials = techniqueBasedMaterials;
    }
    
    /**
     * Set the component type for the indices of the {@link MeshPrimitiveModel}
     * objects
     * 
     * @param indicesComponentType The component type
     * @throws IllegalArgumentException If the given type is not
     * GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT or GL_UNSIGNED_INT
     */
    public void setIndicesComponentType(int indicesComponentType)
    {
        List validTypes = Arrays.asList(
            GltfConstants.GL_UNSIGNED_BYTE,
            GltfConstants.GL_UNSIGNED_SHORT,
            GltfConstants.GL_UNSIGNED_INT);
        if (!validTypes.contains(indicesComponentType))
        {
            throw new IllegalArgumentException(
                "The indices component type must be GL_UNSIGNED_BYTE," + 
                "GL_UNSIGNED_SHORT or GL_UNSIGNED_INT, but is " +
                GltfConstants.stringFor(indicesComponentType));
        }
        this.indicesComponentType = indicesComponentType;

        if (indicesComponentType == GltfConstants.GL_UNSIGNED_INT)
        {
            this.objSplitter = obj -> Collections.singletonList(obj);
        }
        else if (indicesComponentType == GltfConstants.GL_UNSIGNED_SHORT)
        {
            int maxNumVertices = 65536 - 3;
            this.objSplitter = 
                obj -> ObjSplitting.splitByMaxNumVertices(obj, maxNumVertices);
        }
        else if (indicesComponentType == GltfConstants.GL_UNSIGNED_BYTE)
        {
            int maxNumVertices = 256 - 3;
            this.objSplitter = 
                obj -> ObjSplitting.splitByMaxNumVertices(obj, maxNumVertices);
        }
    }
    
    /**
     * For testing and debugging: Assign random colors to the parts that
     * are created when splitting the OBJ data
     * 
     * @param assigningRandomColorsToParts The flag
     */
    public void setAssigningRandomColorsToParts(
        boolean assigningRandomColorsToParts)
    {
        this.assigningRandomColorsToParts = assigningRandomColorsToParts;
    }
    
    /**
     * Set whether each mesh primitive should be in its own mesh,
     * assigned to its own node
     * 
     * @param oneMeshPerPrimitive The flag
     */
    public void setOneMeshPerPrimitive(boolean oneMeshPerPrimitive)
    {
        this.oneMeshPerPrimitive = oneMeshPerPrimitive;
    }
    
    /**
     * Create a {@link GltfModel} from the OBJ file with the given URI
     * 
     * @param objUri The OBJ URI
     * @return The {@link GltfModel}
     * @throws IOException If an IO error occurs
     */
    public GltfModel create(URI objUri) throws IOException
    {
        logger.log(level, "Creating glTF");
        
        // Obtain the relative path information and file names
        logger.log(level, "Resolving paths from " + objUri);
        URI baseUri = IO.getParent(objUri);
        String objFileName = IO.extractFileName(objUri);
        String baseName = stripFileNameExtension(objFileName);

        if (techniqueBasedMaterials)
        {
            mtlMaterialHandler = new MtlMaterialHandlerV1(baseUri.toString());
        }
        else
        {
            mtlMaterialHandler = new MtlMaterialHandlerV2(baseUri.toString());
        }
        
        // Read the input data
        Obj obj = readObj(objUri);

        List mtlFileNames =
            new ArrayList(obj.getMtlFileNames());

        // If no MTL file name was contained in the OBJ file, then
        // try to use an MTL file that has the same name as the OBJ
        // file, but the extension ".mtl".
        if (mtlFileNames.isEmpty())
        {
            String mtlFileName = baseName + ".mtl";
            logger.log(level, "Using default MTL file name " + mtlFileName);
            mtlFileNames.add(mtlFileName);
        }

        // Collect all material definitions from the input MTL files.
        // Note: Technically, there could be multiple MTL files referenced
        // by one OBJ. The OBJ library only supports one MTL file, but the
        // general case is handled here
        Map mtls = new LinkedHashMap();
        for (String mtlFileName : mtlFileNames)
        {
            URI mtlUri = IO.makeAbsolute(baseUri, mtlFileName);
            if (IO.existsUnchecked(mtlUri))
            {
                Map fileMtls = readMtls(mtlUri);
                mtls.putAll(fileMtls);
            }
        }
        return convert(obj, mtls, baseName, baseUri);
    }
    
    /**
     * Read the OBJ from the given URI, and return it as a "renderable" OBJ,
     * which contains only triangles, has unique vertex coordinates and
     * normals, and is single-indexed 
     * 
     * @param objUri The OBJ URI
     * @return The OBJ
     * @throws IOException If an IO error occurs
     */
    private static Obj readObj(URI objUri) throws IOException
    {
        logger.log(level, "Reading OBJ from " + objUri);
        
        try (InputStream objInputStream =  objUri.toURL().openStream())
        {
            Obj obj = ObjReader.read(objInputStream);
            return ObjUtils.convertToRenderable(obj);
        }
    }

    /**
     * Read a mapping from material names to MTL objects from the given URI
     * 
     * @param mtlUri The MTL URI
     * @return The mapping
     * @throws IOException If an IO error occurs
     */
    private static Map readMtls(URI mtlUri) throws IOException
    {
        logger.log(level, "Reading MTL from " + mtlUri);

        try (InputStream mtlInputStream = mtlUri.toURL().openStream())
        {
            List mtlList = MtlReader.read(mtlInputStream);
            Map mtls = mtlList.stream().collect(
                LinkedHashMap::new, 
                (map, mtl) -> map.put(mtl.getName(), mtl), 
                (map0, map1) -> map0.putAll(map1));
            return mtls;
        }
    }
    
    
    /**
     * Convert the given OBJ into a {@link GltfModel}.
     *   
     * @param obj The OBJ
     * @param mtlsMap The mapping from material names to MTL instances
     * @param baseName The base name for the glTF
     * @param baseUri The base URI to resolve external data against
     * @return The {@link GltfModel}
     */
    GltfModel convert(
        ReadableObj obj, Map mtlsMap, String baseName, URI baseUri)
    {
        // Create the MeshPrimitives from the OBJ and MTL data
        Map mtls = Optionals.of(mtlsMap);
        List meshPrimitives = 
            createMeshPrimitives(obj, mtls);
        
        DefaultSceneModel scene = new DefaultSceneModel();

        if (oneMeshPerPrimitive)
        {
            for (MeshPrimitiveModel meshPrimitive : meshPrimitives)
            {
                DefaultMeshModel mesh = new DefaultMeshModel();
                mesh.addMeshPrimitiveModel(meshPrimitive);
                DefaultNodeModel node = new DefaultNodeModel();
                node.addMeshModel(mesh);

                scene.addNode(node);
            }
        }
        else
        {
            DefaultMeshModel mesh = new DefaultMeshModel();
            for (MeshPrimitiveModel meshPrimitive : meshPrimitives)
            {
                mesh.addMeshPrimitiveModel(meshPrimitive);
            }
            DefaultNodeModel node = new DefaultNodeModel();
            node.addMeshModel(mesh);
            scene.addNode(node);
        }
        
        
        GltfModelBuilder gltfModelBuilder = GltfModelBuilder.create();
        gltfModelBuilder.addSceneModel(scene);
        
        if (techniqueBasedMaterials)
        {
            return gltfModelBuilder.buildV1();
        }
        return gltfModelBuilder.build();
    }
    
    
    /**
     * Create the {@link MeshPrimitiveModel} objects for the given OBJ- and 
     * MTL data
     * 
     * @param obj The OBJ
     * @param mtls The MTLs
     * @return The {@link MeshPrimitiveModel} objects
     */
    private List createMeshPrimitives(
        ReadableObj obj, Map mtls)
    {
        // When there are no materials, create the MeshPrimitives for the OBJ
        int numMaterialGroups = obj.getNumMaterialGroups();
        if (numMaterialGroups == 0 || mtls.isEmpty())
        {
            return createMeshPrimitives(obj);
        }
        
        // Create the MeshPrimitives for the material groups
        List meshPrimitives = 
            new ArrayList();
        for (int i = 0; i < numMaterialGroups; i++)
        {
            ObjGroup materialGroup = obj.getMaterialGroup(i);
            String materialGroupName = materialGroup.getName();
            Obj materialObj = ObjUtils.groupToObj(obj, materialGroup, null);
            Mtl mtl = mtls.get(materialGroupName);
            
            logger.log(level, "Creating MeshPrimitive for material " + 
                materialGroupName);

            // If the material group is too large, it may have to
            // be split into multiple parts
            List subMeshPrimitives = 
                createPartMeshPrimitives(materialObj);
            
            assignMaterial(subMeshPrimitives, obj, mtl);
            meshPrimitives.addAll(subMeshPrimitives);
        }
        return meshPrimitives;
    }

    
    
    /**
     * Create simple {@link MeshPrimitiveModel} objects for the given OBJ
     * 
     * @param obj The OBJ
     * @return The {@link MeshPrimitiveModel} objects
     */
    private List createMeshPrimitives(
        ReadableObj obj)
    {
        logger.log(level, "Creating MeshPrimitives for OBJ");
        
        List meshPrimitives = 
            createPartMeshPrimitives(obj);
        
        boolean withNormals = obj.getNumNormals() > 0;
        if (assigningRandomColorsToParts)
        {
            assignRandomColorMaterials(meshPrimitives, withNormals);
        }
        else
        {
            assignDefaultMaterial(meshPrimitives, withNormals);
        }
        return meshPrimitives;
    }

    
    /**
     * Create a {@link MaterialModel} for the given OBJ and MTL, and assign it
     * to all the given {@link MeshPrimitiveModel} instances
     * 
     * @param meshPrimitives The {@link MeshPrimitiveModel} instances
     * @param obj The OBJ
     * @param mtl The MTL
     */
    private void assignMaterial(
        Iterable meshPrimitives, 
        ReadableObj obj, Mtl mtl)
    {
        MaterialModel material = mtlMaterialHandler.createMaterial(obj, mtl);
        for (DefaultMeshPrimitiveModel meshPrimitive : meshPrimitives)
        {
            meshPrimitive.setMaterialModel(material);
        }
    }
    
    /**
     * Create a default {@link MaterialModel}, and assign it to all the given 
     * {@link MeshPrimitiveModel} instances
     * 
     * @param meshPrimitives The {@link MeshPrimitiveModel} instances
     * @param withNormals Whether the {@link MeshPrimitiveModel} instances have
     * normal information
     */
    private void assignDefaultMaterial(
        Iterable meshPrimitives, 
        boolean withNormals)
    {
        MaterialModel material = mtlMaterialHandler.createMaterialWithColor(
            withNormals, 0.75f, 0.75f, 0.75f);
        for (DefaultMeshPrimitiveModel meshPrimitive : meshPrimitives)
        {
            meshPrimitive.setMaterialModel(material);
        }
    }

    /**
     * Create {@link MaterialModel} instances with random colors, and assign 
     * them to the given {@link MeshPrimitiveModel} instances
     * 
     * @param meshPrimitives The {@link MeshPrimitiveModel} instances
     * @param withNormals Whether the {@link MeshPrimitiveModel} instances have
     * normal information
     */
    private void assignRandomColorMaterials(
        Iterable meshPrimitives, 
        boolean withNormals)
    {
        Random random = new Random(0);
        for (DefaultMeshPrimitiveModel meshPrimitive : meshPrimitives)
        {
            float r = random.nextFloat(); 
            float g = random.nextFloat(); 
            float b = random.nextFloat(); 
            MaterialModel material = 
                mtlMaterialHandler.createMaterialWithColor(
                    withNormals, r, g, b);
            meshPrimitive.setMaterialModel(material);
        }
    }
    
    


    /**
     * Create the {@link MeshPrimitiveModel} objects from the given OBJ data.
     * 
     * @param obj The OBJ
     * @return The {@link MeshPrimitiveModel} list
     */
    private List createPartMeshPrimitives(
        ReadableObj obj)
    {
        List parts = objSplitter.apply(obj);
        
        MeshPrimitiveBuilder meshPrimitiveBuilder = 
            MeshPrimitiveBuilder.create();
        
        List meshPrimitives = 
            new ArrayList();
        for (int i = 0; i < parts.size(); i++)
        {
            ReadableObj part = parts.get(i);
            DefaultMeshPrimitiveModel meshPrimitive =
                createMeshPrimitive(meshPrimitiveBuilder, part);
            meshPrimitives.add(meshPrimitive);
        }
        return meshPrimitives;
    }
    
    /**
     * Create the {@link MeshPrimitiveModel} from the given OBJ data.
     * 
     * @param meshPrimitiveBuilder The {@link MeshPrimitiveBuilder}
     * @param part The OBJ
     * @return The {@link MeshPrimitiveModel}
     */
    private DefaultMeshPrimitiveModel createMeshPrimitive(
        MeshPrimitiveBuilder meshPrimitiveBuilder, ReadableObj part)
    {
        meshPrimitiveBuilder.setTriangles();

        // Set the indices 
        int numVerticesPerFace = 3;
        IntBuffer objIndices = 
            ObjData.getFaceVertexIndices(part, numVerticesPerFace);
        meshPrimitiveBuilder.setIndicesAs(objIndices, indicesComponentType);
        
        // Add the vertices (positions) from the OBJ
        FloatBuffer objVertices = ObjData.getVertices(part);
        meshPrimitiveBuilder.addPositions3D(objVertices);

        // Add the texture coordinates from the OBJ
        boolean flipY = true;
        FloatBuffer objTexCoords = ObjData.getTexCoords(part, 2, flipY);
        if (objTexCoords.capacity() > 0)
        {
            meshPrimitiveBuilder.addTexCoords02D(objTexCoords);
        }
        
        // Add the normals from the OBJ
        FloatBuffer objNormals = ObjData.getNormals(part);
        if (objNormals.capacity() > 0)
        {
            ObjNormals.normalize(objNormals);
            meshPrimitiveBuilder.addNormals3D(objNormals);
        }

        return meshPrimitiveBuilder.build();
    }
    
    /**
     * Remove the extension from the given file name. That is, the part 
     * starting with the last '.' dot. If the given file name
     * does not contain a dot, it will be returned unmodified
     * 
     * @param fileName The file name
     * @return The file name without the extension
     */
    private static String stripFileNameExtension(String fileName)
    {
        int lastDotIndex = fileName.lastIndexOf('.');
        if (lastDotIndex < 0)
        {
            return fileName;
        }
        return fileName.substring(0, lastDotIndex);
    }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy