Wavefront OBJ exporter

Here's the place for discussion related to coding in FreeCAD, C++ or Python. Design, interfaces and structures.
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
User avatar
vejmarie
Posts: 713
Joined: Mon Jan 04, 2016 4:52 pm
Location: Somewhere between France, USA and Taiwan
Contact:

Wavefront OBJ exporter

Post by vejmarie »

Hello,

I am playing a little with xeogl stuff which is a javascript object viewer in webgl and that support wavefront obj as input. I struggled at displaying properly our model despite the export of the material file. After some time digging it looks like that this is coming from the lack of normal vector export within the file (vn) for each surface contained into the OBJ file. Is this something that somebody would like to have a look to ? I had a quick look at the python code written by Yorik, but I didn't find an easy way to export or compute the normal into it. I believe it needs to be done for each triangles when the Shape is tesselated. I could get inspired by the Mesh module either into Fem or the Mesh workbench but if somebody has a better idea feel free to suggest !

vejmarie
User avatar
adrianinsaval
Veteran
Posts: 5551
Joined: Thu Apr 05, 2018 5:15 pm

Re: Wavefront OBJ exporter

Post by adrianinsaval »

If somone is going to do it maybe he could take a look at this too? https://forum.freecadweb.org/viewtopic. ... 18#p338318
vocx
Veteran
Posts: 5197
Joined: Thu Oct 18, 2018 9:18 pm

Re: Wavefront OBJ exporter

Post by vocx »

vejmarie wrote: Sat Oct 05, 2019 8:39 pm ... After some time digging it looks like that this is coming from the lack of normal vector export within the file (vn) for each surface contained into the OBJ file.
...
As I see from Arch/importOBJ.py, the exporter only exports lists of vertices, edges, and faces. You are probably right, it doesn't consider face normals.
Is this something that somebody would like to have a look to ? I had a quick look at the python code written by Yorik, but I didn't find an easy way to export or compute the normal into it. I believe it needs to be done for each triangles when the Shape is tesselated.
It seems that the code just tessellates the Shape, with

Code: Select all

tris = shape.Face.tessellate(1)
https://github.com/FreeCAD/FreeCAD/blob ... .cpp#L1907

The tessellate command comes from the Part module. Maybe there is an OpenCASCADE function in Part which provides the normal as well.

Lately Nocturnial wanted to improve the OBJ exporter as well, in order to correctly export multi-materials. I've never seen the Wavefront OBJ standard, so I am not sure what is supposed to support. If the standard mentions face normals, then we should support them of course. By reading Nocturnial's post, it gave me the impression that the OBJ exporter was written by Yorik mostly with the intention of producing OBJs that are are compatible with Blender. Maybe he didn't add more functionality, just the basics.
Nocturnial wrote: Wed Sep 04, 2019 6:36 pm I'm glad you brought this up because it forced me to reread the standard. I'm using version 4.2 and couldn't find anything more recent online and offline. The change from transparency "Tr" to dissolve "d" is correct. Transparency "Tr" isn't in the standard but dissolve is. From what I gathered is that Tr = 1.0 - d. I think "Tr" is an extension introduced by some programs. However I might do a better job in selecting the illumination model "illum".
...
It's nitpicking because I "technically" follow the standard but not the spirit of the standard and probably only affects real-time engines.
...
I think the authors of the blender wavefront importer made the same mistake as I initially did. ... I'll submit a patch to blender and see what they say about this.
Always add the important information to your posts if you need help. Also see Tutorials and Video tutorials.
To support the documentation effort, and code development, your donation is appreciated: liberapay.com/FreeCAD.
Nocturnial

Re: Wavefront OBJ exporter

Post by Nocturnial »

I currently use this code in the render workbench code.

This code uses meshes as input and uses the normals per face. If you want a smoothed object you need to extract the normals per point:

Code: Select all

mesh.Point[i].Normal
instead of per face

Code: Select all

mesh.Facets[i].Normal
Ideally we would use a crease angle, but that's a lot more work. (see https://github.com/FreeCAD/FreeCAD-render/issues/11)
You can remove the parts where I write the texture coordinates if you don't need them. Otherwise coordinate with furti, you need to install a specific branch of archtexture.

To convert a shape to a mesh you can use the following code (instead of using tesselate):

Code: Select all

import MeshPart
MeshPart.meshFromShape(Shape=shape, LinearDeflection=0.1, AngularDeflection=0.523599, Relative=False)
It has a lot more options than tesselate and you get the normals for free. I got the parameters I used from working on yoriks render workbench, but I would recommend setting relative to true. Otherwise a very large curved surface get tesselated into a lot of small tris without adding much detail. An obj file of a couple kb (Relative=True) became 22Mb (Relative=False). In any case, experiment a bit with the parameters until you have something you like. Check the help for meshFromShape for all possible parameters.
vocx
Veteran
Posts: 5197
Joined: Thu Oct 18, 2018 9:18 pm

Re: Wavefront OBJ exporter

Post by vocx »

Nocturnial wrote: Sun Oct 06, 2019 3:51 am I currently use ... in the render workbench code.
...
By the way, did you do anything else with the Wavefront OBJ exporter? Or did you decide to focus on the Render workbench instead?
Always add the important information to your posts if you need help. Also see Tutorials and Video tutorials.
To support the documentation effort, and code development, your donation is appreciated: liberapay.com/FreeCAD.
User avatar
vejmarie
Posts: 713
Joined: Mon Jan 04, 2016 4:52 pm
Location: Somewhere between France, USA and Taiwan
Contact:

Re: Wavefront OBJ exporter

Post by vejmarie »

Thanks for all your feedback. We definitely need the normal into the wavefront to properly render the colors etc ... It is supported by the file format which in the end is a good option to share multi meshes and render in webgl lately. I will pursue some work there (I a not a python guy at all, but will learn).
User avatar
vejmarie
Posts: 713
Joined: Mon Jan 04, 2016 4:52 pm
Location: Somewhere between France, USA and Taiwan
Contact:

Re: Wavefront OBJ exporter

Post by vejmarie »

Got a small fix to Yorik writer based on your very useful feedback (it saved me a lot of time ;)). Will run for a PR later today or early this week
User avatar
vejmarie
Posts: 713
Joined: Mon Jan 04, 2016 4:52 pm
Location: Somewhere between France, USA and Taiwan
Contact:

Re: Wavefront OBJ exporter

Post by vejmarie »

vocx wrote: Sun Oct 06, 2019 4:19 am
Nocturnial wrote: Sun Oct 06, 2019 3:51 am I currently use ... in the render workbench code.
...
By the way, did you do anything else with the Wavefront OBJ exporter? Or did you decide to focus on the Render workbench instead?
Got it working. Still need to find a way to fix the placement issue. I am trying to export an assembly everything is inside with the relevant normal vector now, but the placement is broken (and seriously). I am trying to convert in fact big step files to obj ones
wmayer
Founder
Posts: 20309
Joined: Thu Feb 19, 2009 10:32 am
Contact:

Re: Wavefront OBJ exporter

Post by wmayer »

Got it working. Still need to find a way to fix the placement issue. I am trying to export an assembly everything is inside with the relevant normal vector now, but the placement is broken (and seriously). I am trying to convert in fact big step files to obj ones
I don't know how your code looks like but when applying a placement to the normal vectors you must only apply the rotational part but not the translational part.

Let's consider the placement P with rotation R and translation t and the points p1 and p2 and the vector v = p2-p1. If you apply the placement to p1 and p2 you get:
p1' = R * p1 + t
p2' = R * p2 + t

v' will be then p2' - p1' = (R * p2 + t) - (R * p1 + t) = R * p2 - R * p1 = R * (p2 - p1) = R * v
As you can see t disappears for the vector which makes sense because a vector is position-invariant.
User avatar
vejmarie
Posts: 713
Joined: Mon Jan 04, 2016 4:52 pm
Location: Somewhere between France, USA and Taiwan
Contact:

Re: Wavefront OBJ exporter

Post by vejmarie »

wmayer wrote: Mon Oct 07, 2019 8:26 am
Got it working. Still need to find a way to fix the placement issue. I am trying to export an assembly everything is inside with the relevant normal vector now, but the placement is broken (and seriously). I am trying to convert in fact big step files to obj ones
I don't know how your code looks like but when applying a placement to the normal vectors you must only apply the rotational part but not the translational part.

Let's consider the placement P with rotation R and translation t and the points p1 and p2 and the vector v = p2-p1. If you apply the placement to p1 and p2 you get:
p1' = R * p1 + t
p2' = R * p2 + t

v' will be then p2' - p1' = (R * p2 + t) - (R * p1 + t) = R * p2 - R * p1 = R * (p2 - p1) = R * v
As you can see t disappears for the vector which makes sense because a vector is position-invariant.
Hi, thanks for the feedback. I was expecting to apply rotation only. I am facing in the end the same issue than https://forum.freecadweb.org/viewtopic.php?t=16545. The proposed solution is not fully ready to be implemented within Yorik code.

Here is the update I made

Code: Select all


def getIndices(shape,offsetv,offsetvn):
    "returns a list with 2 lists: vertices and face indexes, offsetted with the given amount"
    vlist = []
    vnlist = []
    elist = []
    flist = []
    curves = None

    if isinstance(shape,Part.Shape):
        for e in shape.Edges:
            try:
                if not isinstance(e.Curve,Part.LineSegment):
                    if not curves:
                        mesh=MeshPart.meshFromShape(Shape=shape, LinearDeflection=0.1, AngularDeflection=0.523599, Relative=False)
                        FreeCAD.Console.PrintWarning(translate("Arch","Found a shape containing curves, triangulating")+"\n")
                        break
            except: # unimplemented curve type
                mesh=MeshPart.meshFromShape(Shape=shape, LinearDeflection=0.1, AngularDeflection=0.523599, Relative=False)
                FreeCAD.Console.PrintWarning(translate("Arch","Found a shape containing curves, triangulating")+"\n")
                break
    elif isinstance(shape,Mesh.Mesh):
        curves = shape.Topology
    if mesh:
        for v in mesh.Topology[0]:
              vlist.append(" "+str(round(v[0],p))+" "+str(round(v[1],p))+" "+str(round(v[2],p)))

        for vn in mesh.Facets:
              vnlist.append(" "+str(vn.Normal[0]) + " " + str(vn.Normal[1]) + " " + str(vn.Normal[2]))

        for i, vn in enumerate(mesh.Topology[1]):
              flist.append(" "+str(vn[0]+offsetv)+"//"+str(i+offsetvn)+" "+str(vn[1]+offsetv)+"//"+str(i+offsetvn)+" "+str(vn[2]+offsetv)+"//"+str(i+offsetvn)+" ")
    else:
        print("using original code")
        if curves:
            for v in curves[0]:
                vlist.append(" "+str(round(v.x,p))+" "+str(round(v.y,p))+" "+str(round(v.z,p)))
            for f in curves[1]:
                fi = ""
                for vi in f:
                    fi += " " + str(vi + offsetv)
                flist.append(fi)
        else:
            for v in shape.Vertexes:
                vlist.append(" "+str(round(v.X,p))+" "+str(round(v.Y,p))+" "+str(round(v.Z,p)))
            if not shape.Faces:
                for e in shape.Edges:
                   if DraftGeomUtils.geomType(e) == "Line":
                        ei = " " + str(findVert(e.Vertexes[0],shape.Vertexes) + offsetv)
                        ei += " " + str(findVert(e.Vertexes[-1],shape.Vertexes) + offsetv)
                        elist.append(ei)
            for f in shape.Faces:
                if len(f.Wires) > 1:
                    # if we have holes, we triangulate
                    tris = f.tessellate(1)
                    for fdata in tris[1]:
                        fi = ""
                        for vi in fdata:
                            vdata = Part.Vertex(tris[0][vi])
                            fi += " " + str(findVert(vdata,shape.Vertexes) + offsetv)
                        flist.append(fi)
                else:
                    fi = ""
                    for e in f.OuterWire.OrderedEdges:
                        #print(e.Vertexes[0].Point,e.Vertexes[1].Point)
                        v = e.Vertexes[0]
                        ind = findVert(v,shape.Vertexes)
                        if ind == None:
                            return None,None,None
                        fi += " " + str(ind + offsetv)
                    flist.append(fi)
    return vlist,vnlist,elist,flist
    
    

So I manage 2 offset, one for the vertex and one for the normals. Then write the normal into the OBJ. Still need to find the Part parent of the geometry (most of the Step import is based on Parts + Solid and there is no link from Solid to Part and compute the global placement, we do have the info for the relative Placement), a simple loop within the object tree shall be able to do it. If you have any ideas, I am open to them
Last edited by vejmarie on Mon Oct 07, 2019 12:45 pm, edited 1 time in total.
Post Reply