andreasplesch / OCCToX3D

convert OCC (step, iges, stl ..) to X3D
https://mybinder.org/v2/gh/andreasplesch/OCCtoX3D/conda2
GNU Lesser General Public License v2.1
5 stars 1 forks source link

Export to X3D native primitives #8

Open tpaviot opened 4 years ago

tpaviot commented 4 years ago

As far as I know, current X3D exporters mostly use meshes to describe the shapes geometry. But I may have missed something. It could be possible to remain even closer to the BRep semantics using x3d available primitives such as planes, extrusion, nurbs etc. It would also result in smaller files, tesselation should be performed on the client.

I just uploaded to the pythonocc-demos suite an example related to nurbs, see https://github.com/tpaviot/pythonocc-demos/commit/28fbd486ee28ce742d724aaa529a22507471589e

It first converts all faces of a shape to nurbs, and then get the nurbs properties. Running this, I get the following output (note that the input geometry is a torus):

$ python core_geometry_nurbs_converter.py 
=== Face 1 ===
UDegree: 2
VDegree: 2
Uknots:0.0 2.0943951023931953 4.1887902047863905 6.283185307179586 
Vknots:0.0 2.0943951023931953 4.1887902047863905 6.283185307179586 

Weights:1.0 0.5 1.0 0.5 0.5 0.25 0.5 0.25 1.0 0.5 1.0 0.5 0.5 0.25 0.5 0.25 

it think this could be mapped to a Nurbs node in x3d. It's just a starting point, I did not deeply enough study the nurbs representation in x3d.

tpaviot commented 4 years ago

Another starting point would the https://github.com/tpaviot/pythonocc-demos/blob/master/examples/core_geometry_face_recognition_from_stepfile.py example: each face geometry is identified (whether it's a plane, a cylindrical face etc.). I guess each of these geometries could be mapped to an x3d node.

tpaviot commented 4 years ago

Here is an example for an export to a NurbsPatchSurface node. It's open loop process: I did not try to visualize the x3d output

##Copyright 2020 Thomas Paviot (tpaviot@gmail.com)
##
##This file is part of pythonOCC.
##
##pythonOCC is free software: you can redistribute it and/or modify
##it under the terms of the GNU Lesser General Public License as published by
##the Free Software Foundation, either version 3 of the License, or
##(at your option) any later version.
##
##pythonOCC is distributed in the hope that it will be useful,
##but WITHOUT ANY WARRANTY; without even the implied warranty of
##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##GNU Lesser General Public License for more details.
##
##You should have received a copy of the GNU Lesser General Public License
##along with pythonOCC.  If not, see <http://www.gnu.org/licenses/>.

from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeTorus
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_NurbsConvert
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface

from OCC.Extend.TopologyUtils import TopologyExplorer
from OCC.Core.GeomAbs import GeomAbs_BSplineSurface

# then export to x3d
X3D_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE X3D PUBLIC "ISO//Web3D//DTD X3D 4.0//EN" "https://www.web3d.org/specifications/x3d-4.0.dtd">
<X3D profile='Immersive' version='4.0' xmlns:xsd='http://www.w3.org/2001/XMLSchema-instance' xsd:noNamespaceSchemaLocation='http://www.web3d.org/specifications/x3d-4.0.xsd'>
<head>
    <meta name='generator' content='pythonocc-7.4.1-dev X3D exporter (www.pythonocc.org)'/>
    <meta name='creator' content='pythonocc-7.4.1-dev generator'/>
    <meta name='identifier' content='http://www.pythonocc.org'/>
    <meta name='description' content='pythonocc-7.4.1-dev x3dom based shape rendering'/>
</head>
    <Scene>
    %s
    </Scene>
</X3D>
"""

base_shape = BRepPrimAPI_MakeTorus(30, 10).Shape()

# conversion to a nurbs representation
nurbs_converter = BRepBuilderAPI_NurbsConvert(base_shape, True)
#nurbs_converter.Perform()
converted_shape = nurbs_converter.Shape()

# now, all edges should be BSpline curves and surfaces BSpline surfaces
# see https://www.opencascade.com/doc/occt-7.4.0/refman/html/class_b_rep_builder_a_p_i___nurbs_convert.html#details

expl = TopologyExplorer(converted_shape)

nurbs_node_str = ""

face_idx = 1

for face in expl.faces():
    surf = BRepAdaptor_Surface(face, True)
    surf_type = surf.GetType()
    # check each of the is a BSpline surface
    # it should be, since we used the nurbs converter before
    if not surf_type == GeomAbs_BSplineSurface:
        raise AssertionError("the face was not converted to a GeomAbs_BSplineSurface")
    # get the nurbs
    bsrf = surf.BSpline()

    # fill in the x3d template with nurbs information
    nurbs_node_str = "<Shape>"
    nurbs_node_str += "<NurbsPatchSurface DEF='nurbs_%i' solid='false' " % face_idx
    nurbs_node_str += 'uDimension=%i uOrder=%i ' % (bsrf.UDegree(), bsrf.UDegree())
    nurbs_node_str += 'vDimension=%i vOrder=%i ' % (bsrf.VDegree(), bsrf.VDegree())
    nurbs_node_str += "uKnot='"
    uknots = bsrf.UKnots()
    for i in range(bsrf.NbUKnots()):
        nurbs_node_str += "%g " % uknots.Value(i + 1)
    nurbs_node_str +="' "

    nurbs_node_str += "vKnot='"
    vknots = bsrf.VKnots()
    for i in range(bsrf.NbVKnots()):
        nurbs_node_str += "%g " % vknots.Value(i + 1)
    nurbs_node_str +="' "

    weights = bsrf.Weights()
    # weights can be None
    if weights is not None:
        nurbs_node_str += "weight='"
        for i in range(bsrf.NbUKnots()):
            for j in range(bsrf.NbVKnots()):
                nurbs_node_str +="%g " % weights.Value(i + 1, j + 1)
        nurbs_node_str +="' "

    # weights can be None
    if weights is not None:
        nurbs_node_str += "weight='"
        for i in range(bsrf.NbUKnots()):
            for j in range(bsrf.NbVKnots()):
                nurbs_node_str +="%g " % weights.Value(i + 1, j + 1)
        nurbs_node_str +="' "

    nurbs_node_str += "containerField='geometry'>\n"
    # the control points
    nurbs_node_str += "<Coordinate containerField='controlPoint' point='"
    # control points (aka poles), as a 2d array
    poles = bsrf.Poles()
    # weights can be None
    if poles is not None:
        for i in range(bsrf.NbUPoles()):
            for j in range(bsrf.NbVPoles()):
                p = poles.Value(i + 1, j + 1)
                nurbs_node_str += "%g %g %g " % (p.X(), p.Y(), p.Z())
        nurbs_node_str +="'/>"

    nurbs_node_str += "</NurbsPatchSurface></Shape>\n"

    face_idx += 1

# write x3d file
fp = open("nurbs.x3d", "w")
fp.write(X3D_TEMPLATE % nurbs_node_str)
fp.close()
andreasplesch commented 4 years ago

Yes, exporting to primitives would be another option. It is a different approach which would rely more on X3D to generate the correct visual representation. Files sizes would be smaller, especially if Nurbs are used but the X3D internal tesselation takes resources. X3D does not have CSG or trimming (except for Nurbs) or any advanced geometry modification of primitives.

Here are X3D Nurbs examples: https://www.web3d.org/x3d/content/examples/Basic/NURBS/index.html

Some X3D browser may not implement the Nurbs component. x3dom has some issues with extrusions.

andreasplesch commented 4 years ago

I fixed the code a little bit, to get a result. But it is not a torus ;)

image

I think in x3d knots have to be repeated whereas in occ they may have the number of repeats stored separately.

##Copyright 2020 Thomas Paviot (tpaviot@gmail.com)
##
##This file is part of pythonOCC.
##
##pythonOCC is free software: you can redistribute it and/or modify
##it under the terms of the GNU Lesser General Public License as published by
##the Free Software Foundation, either version 3 of the License, or
##(at your option) any later version.
##
##pythonOCC is distributed in the hope that it will be useful,
##but WITHOUT ANY WARRANTY; without even the implied warranty of
##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##GNU Lesser General Public License for more details.
##
##You should have received a copy of the GNU Lesser General Public License
##along with pythonOCC.  If not, see <http://www.gnu.org/licenses/>.

from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeTorus
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_NurbsConvert
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface

from OCC.Extend.TopologyUtils import TopologyExplorer
from OCC.Core.GeomAbs import GeomAbs_BSplineSurface

# then export to x3d
X3D_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE X3D PUBLIC "ISO//Web3D//DTD X3D 4.0//EN" "https://www.web3d.org/specifications/x3d-4.0.dtd">
<X3D profile='Immersive' version='4.0' xmlns:xsd='http://www.w3.org/2001/XMLSchema-instance' xsd:noNamespaceSchemaLocation='http://www.web3d.org/specifications/x3d-4.0.xsd'>
<head>
    <meta name='generator' content='pythonocc-7.4.1-dev X3D exporter (www.pythonocc.org)'/>
    <meta name='creator' content='pythonocc-7.4.1-dev generator'/>
    <meta name='identifier' content='http://www.pythonocc.org'/>
    <meta name='description' content='pythonocc-7.4.1-dev x3dom based shape rendering'/>
</head>
    <Scene>
    %s
    </Scene>
</X3D>
"""

base_shape = BRepPrimAPI_MakeTorus(30, 10).Shape()

# conversion to a nurbs representation
nurbs_converter = BRepBuilderAPI_NurbsConvert(base_shape, True)
#nurbs_converter.Perform()
converted_shape = nurbs_converter.Shape()

# now, all edges should be BSpline curves and surfaces BSpline surfaces
# see https://www.opencascade.com/doc/occt-7.4.0/refman/html/class_b_rep_builder_a_p_i___nurbs_convert.html#details

expl = TopologyExplorer(converted_shape)

nurbs_node_str = ""

face_idx = 1

for face in expl.faces():
    surf = BRepAdaptor_Surface(face, True)
    surf_type = surf.GetType()
    # check each of the is a BSpline surface
    # it should be, since we used the nurbs converter before
    if not surf_type == GeomAbs_BSplineSurface:
        raise AssertionError("the face was not converted to a GeomAbs_BSplineSurface")
    # get the nurbs
    bsrf = surf.BSpline()

    # fill in the x3d template with nurbs information
    nurbs_node_str = "<Shape>"
    nurbs_node_str += "<Appearance><Material></Material></Appearance>\n"
    nurbs_node_str += "<NurbsPatchSurface DEF='nurbs_%i' solid='false' " % face_idx
    nurbs_node_str += 'uDimension="%i" uOrder="%i" ' % (bsrf.NbUPoles(), bsrf.UDegree())
    nurbs_node_str += 'vDimension="%i" vOrder="%i" ' % (bsrf.NbVPoles(), bsrf.VDegree())
    nurbs_node_str += "uKnot='"
    uknots = bsrf.UKnots()
    for i in range(bsrf.NbUKnots()):
        nurbs_node_str += "%g " % uknots.Value(i + 1)
    nurbs_node_str +="' "

    nurbs_node_str += "vKnot='"
    vknots = bsrf.VKnots()
    for i in range(bsrf.NbVKnots()):
        nurbs_node_str += "%g " % vknots.Value(i + 1)
    nurbs_node_str +="' "

    weights = bsrf.Weights()
    # weights can be None
    if weights is not None:
        nurbs_node_str += "weight='"
        for i in range(bsrf.NbUKnots()):
            for j in range(bsrf.NbVKnots()):
                nurbs_node_str +="%g " % weights.Value(i + 1, j + 1)
        nurbs_node_str +="' "

    # weights can be None
#     if weights is not None:
#         nurbs_node_str += "weight='"
#         for i in range(bsrf.NbUKnots()):
#             for j in range(bsrf.NbVKnots()):
#                 nurbs_node_str +="%g " % weights.Value(i + 1, j + 1)
#         nurbs_node_str +="' "

    nurbs_node_str += "containerField='geometry'>\n"
    # the control points
    nurbs_node_str += "<Coordinate containerField='controlPoint' point='"
    # control points (aka poles), as a 2d array
    poles = bsrf.Poles()
    # weights can be None
    if poles is not None:
        for i in range(bsrf.NbVPoles()):
            for j in range(bsrf.NbUPoles()):
                p = poles.Value(j + 1, i + 1)
                nurbs_node_str += "%g %g %g " % (p.X(), p.Y(), p.Z())
        nurbs_node_str +="'/>"

    nurbs_node_str += "</NurbsPatchSurface></Shape>\n"

    face_idx += 1

# write x3d file
#fp = open("nurbs.x3d", "w")
#fp.write(X3D_TEMPLATE % nurbs_node_str)
#fp.close()

x3ddoc = X3D_TEMPLATE % nurbs_node_str
print(x3ddoc)
tpaviot commented 4 years ago

Yes, I realized there were errors. I can't see a torus either, even if changing my glasses ;)

tpaviot commented 4 years ago

freewrl complains about a wrong uknot vector:

libfreewrl version 4.7.0
-h for commandline use
openGL version 4.6.0 - Build 26.20.100.7325
GLSL shader max version 4.60 - Build 26.20.100.7325 460
maximum texture size system/gpu: 16384 runtime/freewrl: 8192
processor architecture x64
maxiumum image texture units 32
depth bits 24
javascript engine spidermonkey version 185 SM2
bad u knot vector given, replacing with:
[0]=0.000000
[1]=0.000000
[2]=0.000000
[3]=0.500000
[4]=1.000000
[5]=1.000000
[6]=1.000000
bad v knot vector given, replacing with:
[0]=0.000000
[1]=0.000000
[2]=0.000000
[3]=0.500000
[4]=1.000000
[5]=1.000000
[6]=1.000000
tpaviot commented 4 years ago

http://create3000.de/users-guide/components/nurbs/nurbspatchsurface/ is quite a good document

andreasplesch commented 4 years ago

It is often best to go straight to the spec. It is quite readable: https://www.web3d.org/documents/specifications/19775-1/V3.3/Part01/components/nurbs.html

andreasplesch commented 4 years ago

freeWrl complains because there not enough knots. "The number of knots shall be equal to the number of control points plus the order of the curve. " I think the occ knot vectors have to be expanded by using their multiplicities: https://www.opencascade.com/doc/occt-7.4.0/refman/html/class_geom___b_spline_surface.html#a9d6409304c3ff4fb9f52761281f264ca

andreasplesch commented 4 years ago

Here is a working torus: image The main issue among some smaller ones was to remember that order is degree + 1. Also, x3d does not have periodic nurbs, but simply setting u and v non-periodic adds the correct end points (eg. the starting point).

##Copyright 2020 Thomas Paviot (tpaviot@gmail.com)
##
##This file is part of pythonOCC.
##
##pythonOCC is free software: you can redistribute it and/or modify
##it under the terms of the GNU Lesser General Public License as published by
##the Free Software Foundation, either version 3 of the License, or
##(at your option) any later version.
##
##pythonOCC is distributed in the hope that it will be useful,
##but WITHOUT ANY WARRANTY; without even the implied warranty of
##MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##GNU Lesser General Public License for more details.
##
##You should have received a copy of the GNU Lesser General Public License
##along with pythonOCC.  If not, see <http://www.gnu.org/licenses/>.

from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeTorus
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_NurbsConvert
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface

from OCC.Extend.TopologyUtils import TopologyExplorer
from OCC.Core.GeomAbs import GeomAbs_BSplineSurface

# then export to x3d
X3D_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE X3D PUBLIC "ISO//Web3D//DTD X3D 4.0//EN" "https://www.web3d.org/specifications/x3d-4.0.dtd">
<X3D profile='Immersive' version='4.0' xmlns:xsd='http://www.w3.org/2001/XMLSchema-instance' xsd:noNamespaceSchemaLocation='http://www.web3d.org/specifications/x3d-4.0.xsd'>
<head>
    <meta name='generator' content='pythonocc-7.4.1-dev X3D exporter (www.pythonocc.org)'/>
    <meta name='creator' content='pythonocc-7.4.1-dev generator'/>
    <meta name='identifier' content='http://www.pythonocc.org'/>
    <meta name='description' content='pythonocc-7.4.1-dev x3dom based shape rendering'/>
</head>
    <Scene>
    %s
    </Scene>
</X3D>
"""

base_shape = BRepPrimAPI_MakeTorus(30, 10).Shape()

# conversion to a nurbs representation
nurbs_converter = BRepBuilderAPI_NurbsConvert(base_shape, True)
#nurbs_converter.Perform()
converted_shape = nurbs_converter.Shape()

# now, all edges should be BSpline curves and surfaces BSpline surfaces
# see https://www.opencascade.com/doc/occt-7.4.0/refman/html/class_b_rep_builder_a_p_i___nurbs_convert.html#details

expl = TopologyExplorer(converted_shape)

nurbs_node_str = ""

face_idx = 1

for face in expl.faces():
    surf = BRepAdaptor_Surface(face, True)
    surf_type = surf.GetType()
    # check each of the is a BSpline surface
    # it should be, since we used the nurbs converter before
    if not surf_type == GeomAbs_BSplineSurface:
        raise AssertionError("the face was not converted to a GeomAbs_BSplineSurface")
    # get the nurbs
    bsrf = surf.BSpline()
    print(bsrf.IsUPeriodic())
    print(bsrf.IsVPeriodic())
    bsrf.SetUNotPeriodic()
    bsrf.SetVNotPeriodic()

    # fill in the x3d template with nurbs information
    nurbs_node_str = "<Shape>"
    nurbs_node_str += "<Appearance><Material></Material></Appearance>\n"
    nurbs_node_str += "<NurbsPatchSurface DEF='nurbs_%i' solid='false' " % face_idx
    nurbs_node_str += 'uDimension="%i" uOrder="%i" ' % (bsrf.NbUPoles(), bsrf.UDegree()+1)
    nurbs_node_str += 'vDimension="%i" vOrder="%i" ' % (bsrf.NbVPoles(), bsrf.VDegree()+1)
    nurbs_node_str += "uKnot='"
    uknots = bsrf.UKnots()
    for i in range(bsrf.NbUKnots()):
# AP: repeat knots as necessary
        m=bsrf.UMultiplicity(i+1)
        nurbs_node_str += ("%g " % uknots.Value(i + 1)) * m
    nurbs_node_str +="' "

    nurbs_node_str += "vKnot='"
    vknots = bsrf.VKnots()
    for i in range(bsrf.NbVKnots()):
        m=bsrf.VMultiplicity(i+1)
        nurbs_node_str += ("%g " % vknots.Value(i + 1)) * m
    nurbs_node_str +="' "

    weights = bsrf.Weights()
    # weights can be None
    if weights is not None:
        nurbs_node_str += "weight='"
# weight is per pole
# x3d has u as fast dim. in the grid
        for i in range(bsrf.NbVPoles()):
            for j in range(bsrf.NbUPoles()):
                nurbs_node_str +="%g " % bsrf.Weight(j+1, i+1) #weights.Value(j + 1, i + 1)
        nurbs_node_str +="' "

    # weights can be None
#     if weights is None:

    nurbs_node_str += "containerField='geometry'>\n"
    # the control points
    nurbs_node_str += "<Coordinate containerField='controlPoint' point='"
    # control points (aka poles), as a 2d array
    poles = bsrf.Poles()
    # weights can be None
    if poles is not None:
        for i in range(bsrf.NbVPoles()):
            for j in range(bsrf.NbUPoles()):
                p = bsrf.Pole(j + 1, i + 1) #poles.Value(j + 1, i + 1)
                nurbs_node_str += "%g %g %g, " % (p.X(), p.Y(), p.Z())
        nurbs_node_str +="'/>"

    nurbs_node_str += "</NurbsPatchSurface></Shape>\n"

    face_idx += 1

# write x3d file
#fp = open("nurbs.x3d", "w")
#fp.write(X3D_TEMPLATE % nurbs_node_str)
#fp.close()

x3ddoc = X3D_TEMPLATE % nurbs_node_str
print(x3ddoc)
tpaviot commented 4 years ago

@andreasplesch I tried your code but I don't get a torus using freewrl and instant reality. Instead, I get something like that (I don't know how to call that thing): scr_pseudo_torus

tpaviot commented 4 years ago

@andreasplesch I created a branch wip/x3d-nurbs at pythonocc demos so that we can trace changes. I uploaded https://github.com/tpaviot/pythonocc-demos/blob/wip/x3d-nurbs/examples/core_geometry_nurbs_to_x3d.py

tpaviot commented 4 years ago

ping @cmasia who seems to be interested in x3d nurbs

andreasplesch commented 4 years ago

I am getting the same result with freeWrl and instantplayer. But if I premultiply the controlpoints with the weight I get the correct torus: image So it has to do with the issue @cmasia had. I checked the x3d spec., it is not specific about premultiplying. Let's also try view3dscene, I think it also has nurbs. view3dscene also expects premultilpied controlpoints.

tpaviot commented 4 years ago

ok, thank you, I updated the example https://github.com/tpaviot/pythonocc-demos/commit/29789c7962c34a774bcf4c4b0a72f102d7aa37ca This indeed does the job

tpaviot commented 4 years ago

it works for a single box with 6 faces, however each nurbs has to (?) be mapped to one shape, and for bigger shapes (e.g. the cylinder head), it results in a high number of shapes (around 1000 for the cylinder_head) thathas to be tesselated by the js browser engine can be quite looong. I have to run a few tests, but I'm not optimistic about the fact that exporting faces to nurbs rather than triangle meshes would be such a good choice.

Is there a way to define, using x3d, a shape as a collection of geometries ?

andreasplesch commented 4 years ago

Yes, I agree that Nurbs for x3dom may not be feasible for a large number of shapes since js is slow for tessellating. Perhaps a wasm based tessellator would help which would be a big project. But for other x3d browsers Nurbs shapes may still be a good option.

There may be ways to combine/merge Nurbs shapes (faces of the same solid) in occ into a single Nurbs ?

In x3d, you cannot aggregate heterogenuous geometries into a single x3d shape. But one could use a Group or StaticGroup node for a collection of x3d shapes, each with its own geometry, and a shared DEF/USE Appearance. A StaticGroup node would allow a x3d browsers to optimize the content, by translating the complete group to a static, render-ready (webgl) unit. Unfortunately, x3dom does not do that kind of optimization but it is something to think about.