From 5f5c22a0786ba07096800991a88f1b0acf8766c3 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Mon, 4 Feb 2013 21:10:58 -0800 Subject: [PATCH 1/8] Revamped Python bindings that use SWIG --- CMakeLists.txt | 26 ++++ python/LICENSE | 50 ++++++++ python/README.md | 43 +++++++ python/demo/README.md | 12 ++ python/demo/__init__.py | 60 +++++++++ python/demo/canvas.py | 97 ++++++++++++++ python/demo/interactive.py | 136 ++++++++++++++++++++ python/demo/main.py | 147 ++++++++++++++++++++++ python/demo/renderer.py | 154 +++++++++++++++++++++++ python/demo/screenshot.png | Bin 0 -> 61006 bytes python/demo/shaders.py | 149 ++++++++++++++++++++++ python/demo/simple.glsl | 143 +++++++++++++++++++++ python/demo/utility.py | 135 ++++++++++++++++++++ python/demo/window.py | 72 +++++++++++ python/doc/Makefile | 153 ++++++++++++++++++++++ python/doc/conf.py | 244 +++++++++++++++++++++++++++++++++++ python/doc/index.rst | 69 ++++++++++ python/osd/__init__.py | 60 +++++++++ python/osd/adapters.py | 141 +++++++++++++++++++++ python/osd/buffer.h | 98 +++++++++++++++ python/osd/common.py | 94 ++++++++++++++ python/osd/internal.h | 82 ++++++++++++ python/osd/osdshim.i | 251 ++++++++++++++++++++++++++++++++++++ python/osd/subdivider.cpp | 106 ++++++++++++++++ python/osd/subdivider.h | 89 +++++++++++++ python/osd/subdivider.py | 149 ++++++++++++++++++++++ python/osd/topology.cpp | 252 +++++++++++++++++++++++++++++++++++++ python/osd/topology.h | 106 ++++++++++++++++ python/osd/topology.py | 209 ++++++++++++++++++++++++++++++ python/setup.py | 171 +++++++++++++++++++++++++ python/test/__init__.py | 58 +++++++++ python/test/simple.py | 197 +++++++++++++++++++++++++++++ 32 files changed, 3753 insertions(+) create mode 100644 python/LICENSE create mode 100644 python/README.md create mode 100644 python/demo/README.md create mode 100644 python/demo/__init__.py create mode 100644 python/demo/canvas.py create mode 100644 python/demo/interactive.py create mode 100644 python/demo/main.py create mode 100644 python/demo/renderer.py create mode 100644 python/demo/screenshot.png create mode 100644 python/demo/shaders.py create mode 100644 python/demo/simple.glsl create mode 100644 python/demo/utility.py create mode 100644 python/demo/window.py create mode 100644 python/doc/Makefile create mode 100644 python/doc/conf.py create mode 100644 python/doc/index.rst create mode 100644 python/osd/__init__.py create mode 100644 python/osd/adapters.py create mode 100644 python/osd/buffer.h create mode 100644 python/osd/common.py create mode 100644 python/osd/internal.h create mode 100644 python/osd/osdshim.i create mode 100644 python/osd/subdivider.cpp create mode 100644 python/osd/subdivider.h create mode 100644 python/osd/subdivider.py create mode 100644 python/osd/topology.cpp create mode 100644 python/osd/topology.h create mode 100644 python/osd/topology.py create mode 100755 python/setup.py create mode 100644 python/test/__init__.py create mode 100755 python/test/simple.py diff --git a/CMakeLists.txt b/CMakeLists.txt index d46ef1ec..f67c1907 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -201,6 +201,8 @@ find_package(OpenCL 1.1) find_package(CUDA 4.0) find_package(GLFW 2.7.0) find_package(PTex 2.0) +find_package(PythonInterp) +find_package(SWIG) if (NOT APPLE AND OPENGL_FOUND) find_package(GLEW REQUIRED) @@ -323,6 +325,30 @@ else() ) endif() +if(PYTHON_FOUND AND SWIG_FOUND) + execute_process( + COMMAND + ${PYTHON_EXECUTABLE} -c "import numpy; print numpy.get_include()" + OUTPUT_VARIABLE NUMPY_INCLUDE_PATH + RESULT_VARIABLE NUMPY_ERR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(NUMPY_ERR) + message(WARNING "Unable to import numpy.") + else() + add_custom_command( + COMMENT "Building Python bindings with distutils" + TARGET ${target} POST_BUILD + WORKING_DIRECTORY python + DEPENDS opensubdiv + COMMAND ${PYTHON_EXECUTABLE} setup.py build osddir='${LIBRARY_OUTPUT_PATH}' + ) + install(CODE "execute_process(" + "WORKING_DIRECTORY python " + "COMMAND ${PYTHON_EXECUTABLE} setup.py install") + endif() +endif() + # Link examples & regressions dynamically against Osd set( OSD_LINK_TARGET osd_dynamic_cpu osd_dynamic_gpu ) diff --git a/python/LICENSE b/python/LICENSE new file mode 100644 index 00000000..8234fc4a --- /dev/null +++ b/python/LICENSE @@ -0,0 +1,50 @@ +Microsoft Public License (Ms-PL) + +This license governs use of the accompanying software. If you use the +software, you accept this license. If you do not accept the license, +do not use the software. + +Definitions + +The terms "reproduce," "reproduction," "derivative works," and +"distribution" have the same meaning here as under U.S. copyright law. +A "contribution" is the original software, or any additions or changes +to the software. A "contributor" is any person that distributes its +contribution under this license. "Licensed patents" are a +contributor's patent claims that read directly on its contribution. + +Grant of Rights + +(A) Copyright Grant- Subject to the terms of this license, including +the license conditions and limitations in section 3, each contributor +grants you a non-exclusive, worldwide, royalty-free copyright license +to reproduce its contribution, prepare derivative works of its +contribution, and distribute its contribution or any derivative works +that you create. (B) Patent Grant- Subject to the terms of this +license, including the license conditions and limitations in section +3, each contributor grants you a non-exclusive, worldwide, +royalty-free license under its licensed patents to make, have made, +use, sell, offer for sale, import, and/or otherwise dispose of its +contribution in the software or derivative works of the contribution +in the software. + +Conditions and Limitations + +(A) No Trademark License- This license does not grant you rights to +use any contributors' name, logo, or trademarks. (B) If you bring a +patent claim against any contributor over patents that you claim are +infringed by the software, your patent license from such contributor +to the software ends automatically. (C) If you distribute any portion +of the software, you must retain all copyright, patent, trademark, and +attribution notices that are present in the software. (D) If you +distribute any portion of the software in source code form, you may do +so only under this license by including a complete copy of this +license with your distribution. If you distribute any portion of the +software in compiled or object code form, you may only do so under a +license that complies with this license. (E) The software is licensed +"as-is." You bear the risk of using it. The contributors give no +express warranties, guarantees, or conditions. You may have additional +consumer rights under your local laws which this license cannot +change. To the extent permitted under your local laws, the +contributors exclude the implied warranties of merchantability, +fitness for a particular purpose and non-infringement. diff --git a/python/README.md b/python/README.md new file mode 100644 index 00000000..429b8662 --- /dev/null +++ b/python/README.md @@ -0,0 +1,43 @@ +# Instructions + +The OpenSubdiv Python wrapper has been tested with Python 2.6 and Python 2.7. +Make sure you have the numpy module installed before you begin. + +First, try building the extension with: + + ./setup.py build osddir='../build/lib' + +You'll need to replace `../build/lib` with the folder that has `libosdCPU.a` et al. + +This creates a build folder with a platform-specific subfolder, such as: + + ./build/lib.macosx-10.8-intel-2.7 + +Next, try out the unit tests: + + ./setup.py test + +You can clean, build, and test in one go like this: + + ./setup.py clean --all build test + +If you'd like, you can try out an OpenGL demo. For this, you need to have PyQt and PyOpenGL installed. + + ./setup.py install --user demo + +You can also install the module globally with: + + sudo ./setup.py install + +After installing the module, you can generate and view the Sphinx-generated documentation like so: + + ./setup.py doc + open ./doc/_build/html/index.html + +# To Do Items + +- Add support for face varying data by augmenting _FaceAdapter in adapters.py + - Exercise this in the demo by getting it down to GPU (maybe do "discard" for certain faces) +- Instead of using OsdCpuVertexBuffer, create a "NumpyCpuVertexBuffer" that wraps a numpy array +- Add an API that looks very similar to the RIB parameters for RiHierarchicalSubdiv +- Remove all the caveats that are listed in the Sphinx docs :) diff --git a/python/demo/README.md b/python/demo/README.md new file mode 100644 index 00000000..1961fb1e --- /dev/null +++ b/python/demo/README.md @@ -0,0 +1,12 @@ +This folder defines a small demo application that uses PyQt and newish version of PyOpenGL. + +![Screenshot](http://raw.github.com/prideout/OpenSubdiv/master/python/demo/screenshot.png) + +- **main.py** All calls to the OSD wrapper go here. This creates a `QApplication` and periodically pushes new VBO data into the renderer. (see below) +- **renderer.py** Defines the renderer; implements `draw` and `init`. All OpenGL calls are made in this file, and there's no dependency on Qt or OSD. +- **canvas.py** Inherits from `QGLWidget` and calls out to the renderer object (see above) +- **shaders.py** Implements a miniature FX format by extracting named strings from a file and pasting them together +- **simple.glsl** Specifies the GLSL shaders for the demo using the miniature FX format +- **utility.py** Some linear algebra stuff to make it easier to use Modern OpenGL +- **window.py** Inherits from `QMainWindow`, instances a canvas object +- **\_\_init\_\_.py** Exports `main` into the package namespace to make it easy to run the demo from `setup.py` diff --git a/python/demo/__init__.py b/python/demo/__init__.py new file mode 100644 index 00000000..71ae683c --- /dev/null +++ b/python/demo/__init__.py @@ -0,0 +1,60 @@ +# +# Copyright (C) Pixar. All rights reserved. +# +# This license governs use of the accompanying software. If you +# use the software, you accept this license. If you do not accept +# the license, do not use the software. +# +# 1. Definitions +# The terms "reproduce," "reproduction," "derivative works," and +# "distribution" have the same meaning here as under U.S. +# copyright law. A "contribution" is the original software, or +# any additions or changes to the software. +# A "contributor" is any person or entity that distributes its +# contribution under this license. +# "Licensed patents" are a contributor's patent claims that read +# directly on its contribution. +# +# 2. Grant of Rights +# (A) Copyright Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free copyright license to reproduce its contribution, +# prepare derivative works of its contribution, and distribute +# its contribution or any derivative works that you create. +# (B) Patent Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free license under its licensed patents to make, have +# made, use, sell, offer for sale, import, and/or otherwise +# dispose of its contribution in the software or derivative works +# of the contribution in the software. +# +# 3. Conditions and Limitations +# (A) No Trademark License- This license does not grant you +# rights to use any contributor's name, logo, or trademarks. +# (B) If you bring a patent claim against any contributor over +# patents that you claim are infringed by the software, your +# patent license from such contributor to the software ends +# automatically. +# (C) If you distribute any portion of the software, you must +# retain all copyright, patent, trademark, and attribution +# notices that are present in the software. +# (D) If you distribute any portion of the software in source +# code form, you may do so only under this license by including a +# complete copy of this license with your distribution. If you +# distribute any portion of the software in compiled or object +# code form, you may only do so under a license that complies +# with this license. +# (E) The software is licensed "as-is." You bear the risk of +# using it. The contributors give no express warranties, +# guarantees or conditions. You may have additional consumer +# rights under your local laws which this license cannot change. +# To the extent permitted under your local laws, the contributors +# exclude the implied warranties of merchantability, fitness for +# a particular purpose and non-infringement. +# + +from main import main +from interactive import interactive + diff --git a/python/demo/canvas.py b/python/demo/canvas.py new file mode 100644 index 00000000..ac987c2a --- /dev/null +++ b/python/demo/canvas.py @@ -0,0 +1,97 @@ +# +# Copyright (C) Pixar. All rights reserved. +# +# This license governs use of the accompanying software. If you +# use the software, you accept this license. If you do not accept +# the license, do not use the software. +# +# 1. Definitions +# The terms "reproduce," "reproduction," "derivative works," and +# "distribution" have the same meaning here as under U.S. +# copyright law. A "contribution" is the original software, or +# any additions or changes to the software. +# A "contributor" is any person or entity that distributes its +# contribution under this license. +# "Licensed patents" are a contributor's patent claims that read +# directly on its contribution. +# +# 2. Grant of Rights +# (A) Copyright Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free copyright license to reproduce its contribution, +# prepare derivative works of its contribution, and distribute +# its contribution or any derivative works that you create. +# (B) Patent Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free license under its licensed patents to make, have +# made, use, sell, offer for sale, import, and/or otherwise +# dispose of its contribution in the software or derivative works +# of the contribution in the software. +# +# 3. Conditions and Limitations +# (A) No Trademark License- This license does not grant you +# rights to use any contributor's name, logo, or trademarks. +# (B) If you bring a patent claim against any contributor over +# patents that you claim are infringed by the software, your +# patent license from such contributor to the software ends +# automatically. +# (C) If you distribute any portion of the software, you must +# retain all copyright, patent, trademark, and attribution +# notices that are present in the software. +# (D) If you distribute any portion of the software in source +# code form, you may do so only under this license by including a +# complete copy of this license with your distribution. If you +# distribute any portion of the software in compiled or object +# code form, you may only do so under a license that complies +# with this license. +# (E) The software is licensed "as-is." You bear the risk of +# using it. The contributors give no express warranties, +# guarantees or conditions. You may have additional consumer +# rights under your local laws which this license cannot change. +# To the extent permitted under your local laws, the contributors +# exclude the implied warranties of merchantability, fitness for +# a particular purpose and non-infringement. +# + +from PyQt4 import QtCore +from PyQt4.QtOpenGL import * +from OpenGL.GL import * +from time import time + +# Set this to 'None' to refresh as rapidly as possible +# (assuming that vsync is disabled.) +ThrottleFps = 60 + +class Canvas(QGLWidget): + def __init__(self, parent, client): + self.client = client + f = QGLFormat(QGL.SampleBuffers) + if hasattr(QGLFormat, 'setVersion'): + f.setVersion(3, 2) + f.setProfile(QGLFormat.CoreProfile) + else: + pass + if f.sampleBuffers(): + f.setSamples(16) + c = QGLContext(f, None) + QGLWidget.__init__(self, c, parent) + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.updateGL) + interval = 1000.0 / ThrottleFps if ThrottleFps else 0 + self.timer.start(interval) + self.setMinimumSize(500, 500) + + def paintGL(self): + self.client.draw() + + def updateGL(self): + self.client.draw() + self.update() + + def resizeGL(self, w, h): + self.client.resize(w, h) + + def initializeGL(self): + self.client.init() diff --git a/python/demo/interactive.py b/python/demo/interactive.py new file mode 100644 index 00000000..255c505b --- /dev/null +++ b/python/demo/interactive.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# +# Copyright (C) Pixar. All rights reserved. +# +# This license governs use of the accompanying software. If you +# use the software, you accept this license. If you do not accept +# the license, do not use the software. +# +# 1. Definitions +# The terms "reproduce," "reproduction," "derivative works," and +# "distribution" have the same meaning here as under U.S. +# copyright law. A "contribution" is the original software, or +# any additions or changes to the software. +# A "contributor" is any person or entity that distributes its +# contribution under this license. +# "Licensed patents" are a contributor's patent claims that read +# directly on its contribution. +# +# 2. Grant of Rights +# (A) Copyright Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free copyright license to reproduce its contribution, +# prepare derivative works of its contribution, and distribute +# its contribution or any derivative works that you create. +# (B) Patent Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free license under its licensed patents to make, have +# made, use, sell, offer for sale, import, and/or otherwise +# dispose of its contribution in the software or derivative works +# of the contribution in the software. +# +# 3. Conditions and Limitations +# (A) No Trademark License- This license does not grant you +# rights to use any contributor's name, logo, or trademarks. +# (B) If you bring a patent claim against any contributor over +# patents that you claim are infringed by the software, your +# patent license from such contributor to the software ends +# automatically. +# (C) If you distribute any portion of the software, you must +# retain all copyright, patent, trademark, and attribution +# notices that are present in the software. +# (D) If you distribute any portion of the software in source +# code form, you may do so only under this license by including a +# complete copy of this license with your distribution. If you +# distribute any portion of the software in compiled or object +# code form, you may only do so under a license that complies +# with this license. +# (E) The software is licensed "as-is." You bear the risk of +# using it. The contributors give no express warranties, +# guarantees or conditions. You may have additional consumer +# rights under your local laws which this license cannot change. +# To the extent permitted under your local laws, the contributors +# exclude the implied warranties of merchantability, fitness for +# a particular purpose and non-infringement. +# + +from PyQt4 import QtGui, QtCore +from window import Window +from renderer import Renderer +import sys +import numpy as np +import osd +import utility + +from time import time +import math + +def interactive(): + + app = QtGui.QApplication(sys.argv) + renderer = Renderer() + win = Window(renderer) + win.raise_() + + cage = [ 0.0, -1.414214, 1.0, # 0 + 1.414214, 0.0, 1.0, # 1 + -1.414214, 0.0, 1.0, # 2 + 0.0, 1.414214, 1.0, # 3 + -1.414214, 0.0, -1.0, # 4 + 0.0, 1.414214, -1.0, # 5 + 0.0, -1.414214, -1.0, # 6 + 1.414214, 0.0, -1.0 ] # 7 + + cage = np.array(cage, np.float32).reshape((-1, 3)) + + faces = [ (0,1,3,2), # 0 + (2,3,5,4), # 1 + (4,5,7,6), # 2 + (6,7,1,0), # 3 + (1,7,5,3), # 4 + (6,0,2,4) ] # 5 + + topo = osd.Topology(faces) + + dtype = [('x', np.float32), + ('y', np.float32), + ('z', np.float32)] + + def updateTopo(numLevels = 4): + global subdivider + topo.reset() + topo.finalize() + subdivider = osd.Subdivider( + topo, + vertexLayout = dtype, + indexType = np.uint32, + levels = numLevels) + quads = subdivider.getRefinedQuads() + renderer.updateIndicesVbo(quads) + + def updateCoarseVertices(): + global subdivider + subdivider.setCoarseVertices(cage) + subdivider.refine() + pts = subdivider.getRefinedVertices() + renderer.updatePointsVbo(pts) + + updateTopo() + updateCoarseVertices() + + renderer.drawHook = updateCoarseVertices + + # Start an interactive session + import code + from time import time + timer = QtCore.QTimer() + code.interact(local=dict(globals(), **locals())) + + sys.exit(0) + +if __name__ == '__main__': + interactive() diff --git a/python/demo/main.py b/python/demo/main.py new file mode 100644 index 00000000..847b7b0c --- /dev/null +++ b/python/demo/main.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# +# Copyright (C) Pixar. All rights reserved. +# +# This license governs use of the accompanying software. If you +# use the software, you accept this license. If you do not accept +# the license, do not use the software. +# +# 1. Definitions +# The terms "reproduce," "reproduction," "derivative works," and +# "distribution" have the same meaning here as under U.S. +# copyright law. A "contribution" is the original software, or +# any additions or changes to the software. +# A "contributor" is any person or entity that distributes its +# contribution under this license. +# "Licensed patents" are a contributor's patent claims that read +# directly on its contribution. +# +# 2. Grant of Rights +# (A) Copyright Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free copyright license to reproduce its contribution, +# prepare derivative works of its contribution, and distribute +# its contribution or any derivative works that you create. +# (B) Patent Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free license under its licensed patents to make, have +# made, use, sell, offer for sale, import, and/or otherwise +# dispose of its contribution in the software or derivative works +# of the contribution in the software. +# +# 3. Conditions and Limitations +# (A) No Trademark License- This license does not grant you +# rights to use any contributor's name, logo, or trademarks. +# (B) If you bring a patent claim against any contributor over +# patents that you claim are infringed by the software, your +# patent license from such contributor to the software ends +# automatically. +# (C) If you distribute any portion of the software, you must +# retain all copyright, patent, trademark, and attribution +# notices that are present in the software. +# (D) If you distribute any portion of the software in source +# code form, you may do so only under this license by including a +# complete copy of this license with your distribution. If you +# distribute any portion of the software in compiled or object +# code form, you may only do so under a license that complies +# with this license. +# (E) The software is licensed "as-is." You bear the risk of +# using it. The contributors give no express warranties, +# guarantees or conditions. You may have additional consumer +# rights under your local laws which this license cannot change. +# To the extent permitted under your local laws, the contributors +# exclude the implied warranties of merchantability, fitness for +# a particular purpose and non-infringement. +# + +from PyQt4 import QtGui, QtCore +from window import Window +from renderer import Renderer +import sys +import numpy as np +import osd + +def main(): + + app = QtGui.QApplication(sys.argv) + renderer = Renderer() + win = Window(renderer) + win.raise_() + + verts = [ 0.0, -1.414214, 1.0, # 0 + 1.414214, 0.0, 1.0, # 1 + -1.414214, 0.0, 1.0, # 2 + 0.0, 1.414214, 1.0, # 3 + -1.414214, 0.0, -1.0, # 4 + 0.0, 1.414214, -1.0, # 5 + 0.0, -1.414214, -1.0, # 6 + 1.414214, 0.0, -1.0 ] # 7 + + verts = np.array(verts, np.float32).reshape((-1, 3)) + + faces = [ (0,1,3,2), # 0 + (2,3,5,4), # 1 + (4,5,7,6), # 2 + (6,7,1,0), # 3 + (1,7,5,3), # 4 + (6,0,2,4) ] # 5 + + dtype = np.dtype([ + ('Px', np.float32), + ('Py', np.float32), + ('Pz', np.float32)]) + + topo = osd.Topology(faces) + topo.boundaryMode = osd.BoundaryMode.EDGE_ONLY + for v in (2, 3, 4, 5): + topo.vertices[v].sharpness = 2.0 + for e in xrange(4): + topo.faces[3].edges[e].sharpness = 3 + topo.finalize() + + subdivider = osd.Subdivider( + topo, + vertexLayout = dtype, + indexType = np.uint32, + levels = 4) + subdivider.setCoarseVertices(verts) + subdivider.refine() + inds = subdivider.getRefinedQuads() + renderer.updateIndicesVbo(inds) + + def animateVerts(): + from time import time + import math + t = 4 * time() + t = 0 + t = 0.5 + 0.5 * math.sin(t) + t = 0.25 + t * 0.75 + a = np.array([ 0.0, -1.414214, 1.0]) + b = np.array([ 1.414214, 0.0, 1.0]) + c = np.array([ 0.0, -1.414214, -1.0]) + d = np.array([1.414214, 0.0, -1.0 ]) + center = (a + b + c + d) / 4 + center = np.multiply(center, 1-t) + verts[0] = center + np.multiply(a, t) + verts[1] = center + np.multiply(b, t) + verts[6] = center + np.multiply(c, t) + verts[7] = center + np.multiply(d, t) + + def updateAnimation(): + animateVerts() + subdivider.setCoarseVertices(verts) + subdivider.refine() + pts = subdivider.getRefinedVertices() + renderer.updatePointsVbo(pts) + + updateAnimation() + renderer.drawHook = updateAnimation + retcode = app.exec_() + sys.exit(retcode) + +if __name__ == '__main__': + main() diff --git a/python/demo/renderer.py b/python/demo/renderer.py new file mode 100644 index 00000000..9aa8948a --- /dev/null +++ b/python/demo/renderer.py @@ -0,0 +1,154 @@ +# +# Copyright (C) Pixar. All rights reserved. +# +# This license governs use of the accompanying software. If you +# use the software, you accept this license. If you do not accept +# the license, do not use the software. +# +# 1. Definitions +# The terms "reproduce," "reproduction," "derivative works," and +# "distribution" have the same meaning here as under U.S. +# copyright law. A "contribution" is the original software, or +# any additions or changes to the software. +# A "contributor" is any person or entity that distributes its +# contribution under this license. +# "Licensed patents" are a contributor's patent claims that read +# directly on its contribution. +# +# 2. Grant of Rights +# (A) Copyright Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free copyright license to reproduce its contribution, +# prepare derivative works of its contribution, and distribute +# its contribution or any derivative works that you create. +# (B) Patent Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free license under its licensed patents to make, have +# made, use, sell, offer for sale, import, and/or otherwise +# dispose of its contribution in the software or derivative works +# of the contribution in the software. +# +# 3. Conditions and Limitations +# (A) No Trademark License- This license does not grant you +# rights to use any contributor's name, logo, or trademarks. +# (B) If you bring a patent claim against any contributor over +# patents that you claim are infringed by the software, your +# patent license from such contributor to the software ends +# automatically. +# (C) If you distribute any portion of the software, you must +# retain all copyright, patent, trademark, and attribution +# notices that are present in the software. +# (D) If you distribute any portion of the software in source +# code form, you may do so only under this license by including a +# complete copy of this license with your distribution. If you +# distribute any portion of the software in compiled or object +# code form, you may only do so under a license that complies +# with this license. +# (E) The software is licensed "as-is." You bear the risk of +# using it. The contributors give no express warranties, +# guarantees or conditions. You may have additional consumer +# rights under your local laws which this license cannot change. +# To the extent permitted under your local laws, the contributors +# exclude the implied warranties of merchantability, fitness for +# a particular purpose and non-infringement. +# + +import math +import numpy as np + +from OpenGL.GL import * +from shaders import * +from utility import * +from canvas import * + +class Renderer: + def __init__(self): + self.indexCount = 0 + + def draw(self): + + if hasattr(self, "drawHook"): + self.drawHook() + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) + if not self.indexCount: + return + + theta = time() * math.pi / 2 + eye = V3(0, 0, 10) + target = V3(0, 0, 0) + up = V3(0, 1, 0) + view = look_at(eye, target, up) + + model = np.identity(4, 'f') + model = rotation(-1.1 + theta, [0,1,0]) + + objToEye = view * model + eyeToClip = perspective(15, self.aspect, 5, 200) + normalM = np.identity(3, 'f') + normalM[:3,:3] = objToEye[:3,:3] + + glUseProgram(self.programs['BareBones']) + glUniformMatrix4fv(U("Projection"), 1, True, eyeToClip) + glUniformMatrix4fv(U("Modelview"), 1, True, objToEye) + glUniformMatrix3fv(U("NormalMatrix"), 1, True, normalM) + glBindVertexArray(self.vao) + + # Since quads are deprecated we use LINES_ADJACENCY + # simply because they're 4 verts per prim, and we can + # expand them to triangles in the GS. + glDrawElements( + GL_LINES_ADJACENCY, + self.indexCount, + GL_UNSIGNED_INT, + None) + + def resize(self, w, h): + self.aspect = float(w) / float(h) + glViewport(0, 0, w, h) + + def init(self): + print glGetString(GL_VERSION) + glClearColor(0.0, 0.25, 0.5, 1.0) + self.programs = load_shaders() + + try: + self.vao = glGenVertexArrays(1) + except: + import sys + print "glGenVertexArrays isn't available, so you might need a newer version of PyOpenGL." + sys.exit(1) + + self.pointsVbo = glGenBuffers(1) + self.normalsVbo = None + self.indicesVbo = glGenBuffers(1) + glBindVertexArray(self.vao) + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.indicesVbo) + + glBindBuffer(GL_ARRAY_BUFFER, self.pointsVbo) + glVertexAttribPointer(Attribs.POSITION, 3, GL_FLOAT, GL_FALSE, 12, None) + glEnableVertexAttribArray(Attribs.POSITION) + + glEnable(GL_CULL_FACE) + glEnable(GL_DEPTH_TEST) + + def updatePointsVbo(self, points): + glBindBuffer(GL_ARRAY_BUFFER, self.pointsVbo) + glBufferData(GL_ARRAY_BUFFER, points, GL_STATIC_DRAW) + + def updateNormalsVbo(self, normals): + if not self.normalsVbo: + self.normalsVbo = glGenBuffers(1) + glBindVertexArray(self.vao) + glBindBuffer(GL_ARRAY_BUFFER, self.normalsVbo) + glVertexAttribPointer(Attribs.NORMAL, 3, GL_FLOAT, GL_FALSE, 12, None) + glEnableVertexAttribArray(Attribs.NORMAL) + glBindBuffer(GL_ARRAY_BUFFER, self.normalsVbo) + glBufferData(GL_ARRAY_BUFFER, normals, GL_STATIC_DRAW) + + def updateIndicesVbo(self, indices): + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.indicesVbo) + glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW) + self.indexCount = len(indices) diff --git a/python/demo/screenshot.png b/python/demo/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..86c6104fe1b73ef3e8139ea8690eb122ef2af59a GIT binary patch literal 61006 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' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/OpenSubdiv.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/OpenSubdiv.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/OpenSubdiv" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/OpenSubdiv" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/python/doc/conf.py b/python/doc/conf.py new file mode 100644 index 00000000..c301df3c --- /dev/null +++ b/python/doc/conf.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +# +# OpenSubdiv documentation build configuration file, created by +# sphinx-quickstart on Wed Nov 21 15:45:14 2012. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', 'sphinx.ext.mathjax'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'OpenSubdiv' +copyright = u'2012, Pixar Animation Studios' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '1.0' +# The full version, including alpha/beta/rc tags. +release = '1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'OpenSubdivdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'OpenSubdiv.tex', u'OpenSubdiv Documentation', + u'Pixar Animation Studios', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'opensubdiv', u'OpenSubdiv Documentation', + [u'Pixar Animation Studios'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'OpenSubdiv', u'OpenSubdiv Documentation', + u'Pixar Animation Studios', 'OpenSubdiv', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +autodoc_member_order = 'bysource' diff --git a/python/doc/index.rst b/python/doc/index.rst new file mode 100644 index 00000000..c8103458 --- /dev/null +++ b/python/doc/index.rst @@ -0,0 +1,69 @@ +.. OpenSubdiv documentation master file, created by + sphinx-quickstart on Wed Nov 21 15:45:14 2012. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Overview and Design Philosophy +======= + +The Python module for OpenSubdiv does not provide one-to-one wrapping of the native C++ APIs: Osd, Hbr, and Far. Rather, the ``osd`` module is a higher-level utility layer composed of a small handful of classes and free functions. + +We do not yet support rendering or GPU-accelerated subdivision from Python, but a demo is provided that renders a subdivided surface using ``PyOpenGL`` and ``QGLWidget``. The demo uses a "modern" OpenGL context (Core Profile). + +These bindings leverage numpy_ arrays for passing data. The numpy library is a de facto standard for encapsulating large swaths of typed data in Python. However, for special attributes (such as sharpness), the OpenSubdiv wrapper exposes Pythonic interfaces, using properties and list accessors. For example:: + + import osd + import numpy as np + faceList = np.array([[0,1,2,3],[0,1,6,7]]) + topo = osd.Topology(faceList) + topo.vertices[2].sharpness = 1.3 + topo.faces[0].edges[3].sharpness = 0.6 + +After constructing a :class:`osd.Topology` object, clients should finalize it and pass it into a :class:`osd.Subdivider` instance:: + + topo.finalize() + subdivider = osd.Subdivider( + topo, + vertexLayout = [np.float32] * 3, + indexType = np.uint32, + levels = 4) + +The final step is to perform actual refinement. This often occurs inside an animation loop or callback function:: + + subdivider.setCage(positions) + subdivider.refine() + pts = subdivider.getRefinedVertices() + +.. _numpy: http://www.numpy.org + +Topology Class +============== + +.. autoclass:: osd.Topology + :members: + +Subdivider Class +============== + +.. autoclass:: osd.Subdivider + :members: + +Enumerations +============== + +.. autoclass:: osd.InterpolateBoundary + :members: + +.. autoclass:: osd.Sharpness + :members: + +Caveats +======= + +- Hierarchical edits are not yet supported +- Face varying data is not yet supported +- Feature adaptive subdivision is not yet supported +- CUDA and GLSL tessellation shaders are not yet supported +- Only the Catmull-Clark scheme is supported (no loop or linear schemes) +- Vertex data must be 32-bit floats although API is in place to accept heterogenous interpolation data +- Index data must be 32-bit unsigned integers although API is in place to accept other types diff --git a/python/osd/__init__.py b/python/osd/__init__.py new file mode 100644 index 00000000..26d9734a --- /dev/null +++ b/python/osd/__init__.py @@ -0,0 +1,60 @@ +# +# Copyright (C) Pixar. All rights reserved. +# +# This license governs use of the accompanying software. If you +# use the software, you accept this license. If you do not accept +# the license, do not use the software. +# +# 1. Definitions +# The terms "reproduce," "reproduction," "derivative works," and +# "distribution" have the same meaning here as under U.S. +# copyright law. A "contribution" is the original software, or +# any additions or changes to the software. +# A "contributor" is any person or entity that distributes its +# contribution under this license. +# "Licensed patents" are a contributor's patent claims that read +# directly on its contribution. +# +# 2. Grant of Rights +# (A) Copyright Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free copyright license to reproduce its contribution, +# prepare derivative works of its contribution, and distribute +# its contribution or any derivative works that you create. +# (B) Patent Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free license under its licensed patents to make, have +# made, use, sell, offer for sale, import, and/or otherwise +# dispose of its contribution in the software or derivative works +# of the contribution in the software. +# +# 3. Conditions and Limitations +# (A) No Trademark License- This license does not grant you +# rights to use any contributor's name, logo, or trademarks. +# (B) If you bring a patent claim against any contributor over +# patents that you claim are infringed by the software, your +# patent license from such contributor to the software ends +# automatically. +# (C) If you distribute any portion of the software, you must +# retain all copyright, patent, trademark, and attribution +# notices that are present in the software. +# (D) If you distribute any portion of the software in source +# code form, you may do so only under this license by including a +# complete copy of this license with your distribution. If you +# distribute any portion of the software in compiled or object +# code form, you may only do so under a license that complies +# with this license. +# (E) The software is licensed "as-is." You bear the risk of +# using it. The contributors give no express warranties, +# guarantees or conditions. You may have additional consumer +# rights under your local laws which this license cannot change. +# To the extent permitted under your local laws, the contributors +# exclude the implied warranties of merchantability, fitness for +# a particular purpose and non-infringement. +# + +from common import * +from topology import Topology +from subdivider import Subdivider diff --git a/python/osd/adapters.py b/python/osd/adapters.py new file mode 100644 index 00000000..f5ffa1ec --- /dev/null +++ b/python/osd/adapters.py @@ -0,0 +1,141 @@ +# +# Copyright (C) Pixar. All rights reserved. +# +# This license governs use of the accompanying software. If you +# use the software, you accept this license. If you do not accept +# the license, do not use the software. +# +# 1. Definitions +# The terms "reproduce," "reproduction," "derivative works," and +# "distribution" have the same meaning here as under U.S. +# copyright law. A "contribution" is the original software, or +# any additions or changes to the software. +# A "contributor" is any person or entity that distributes its +# contribution under this license. +# "Licensed patents" are a contributor's patent claims that read +# directly on its contribution. +# +# 2. Grant of Rights +# (A) Copyright Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free copyright license to reproduce its contribution, +# prepare derivative works of its contribution, and distribute +# its contribution or any derivative works that you create. +# (B) Patent Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free license under its licensed patents to make, have +# made, use, sell, offer for sale, import, and/or otherwise +# dispose of its contribution in the software or derivative works +# of the contribution in the software. +# +# 3. Conditions and Limitations +# (A) No Trademark License- This license does not grant you +# rights to use any contributor's name, logo, or trademarks. +# (B) If you bring a patent claim against any contributor over +# patents that you claim are infringed by the software, your +# patent license from such contributor to the software ends +# automatically. +# (C) If you distribute any portion of the software, you must +# retain all copyright, patent, trademark, and attribution +# notices that are present in the software. +# (D) If you distribute any portion of the software in source +# code form, you may do so only under this license by including a +# complete copy of this license with your distribution. If you +# distribute any portion of the software in compiled or object +# code form, you may only do so under a license that complies +# with this license. +# (E) The software is licensed "as-is." You bear the risk of +# using it. The contributors give no express warranties, +# guarantees or conditions. You may have additional consumer +# rights under your local laws which this license cannot change. +# To the extent permitted under your local laws, the contributors +# exclude the implied warranties of merchantability, fitness for +# a particular purpose and non-infringement. +# + +# Helper classes that present mesh data as a Pythonic list-like +# objects with properties. In general we prefer clients to use numpy +# arrays for data. However, for OSD-specific data such as sharpness, +# we provide these adapter classes. + +class VertexListAdapter(object): + def __init__(self, shim): + self.shim = shim + self.length = shim.getNumVertices() + def __getitem__(self, index): + if index >= self.length: + raise IndexError("Index out of bounds") + return _VertexAdapter(self.shim, index) + def __len__(self): + return self.length + +class _VertexAdapter(object): + def __init__(self, shim, index): + self.shim = shim + self.index = index + @property + def sharpness(self): + return self.shim.getVertexSharpness( + self.index) + @sharpness.setter + def sharpness(self, value): + self.shim.setVertexSharpness( + self.index, + value) + +class FaceListAdapter(object): + def __init__(self, shim): + self.shim = shim + self.length = shim.getNumFaces() + def __getitem__(self, index): + if index >= self.length: + raise IndexError("Index out of bounds") + return _FaceAdapter(self.shim, index) + def __len__(self): + return self.length + +class _FaceAdapter(object): + def __init__(self, shim, faceIndex): + self.shim = shim + self.faceIndex = faceIndex + self._edgeListAdapter = _EdgeListAdapter(self.shim, faceIndex) + @property + def hole(self): + return self.shim.getFaceHole(self.faceIndex) + @hole.setter + def hole(self, value): + self.shim.setFaceHole(self.faceIndex, value) + @property + def edges(self): + return self._edgeListAdapter + +class _EdgeListAdapter(object): + def __init__(self, shim, faceIndex): + self.shim = shim + self.faceIndex = faceIndex + self.length = shim.getNumEdges(faceIndex) + def __getitem__(self, edgeIndex): + if edgeIndex >= self.length: + raise IndexError("Index out of bounds") + return _EdgeAdapter(self.shim, self.faceIndex, edgeIndex) + def __len__(self): + return self.length + +class _EdgeAdapter(object): + def __init__(self, shim, faceIndex, edgeIndex): + self.shim = shim + self.faceIndex = faceIndex + self.edgeIndex = edgeIndex + @property + def sharpness(self): + return self.shim.getEdgeSharpness( + self.faceIndex, + self.edgeIndex) + @sharpness.setter + def sharpness(self, value): + self.shim.setEdgeSharpness( + self.faceIndex, + self.edgeIndex, + value) diff --git a/python/osd/buffer.h b/python/osd/buffer.h new file mode 100644 index 00000000..e7d3f05a --- /dev/null +++ b/python/osd/buffer.h @@ -0,0 +1,98 @@ +// +// Copyright (C) Pixar. All rights reserved. +// +// This license governs use of the accompanying software. If you +// use the software, you accept this license. If you do not accept +// the license, do not use the software. +// +// 1. Definitions +// The terms "reproduce," "reproduction," "derivative works," and +// "distribution" have the same meaning here as under U.S. +// copyright law. A "contribution" is the original software, or +// any additions or changes to the software. +// A "contributor" is any person or entity that distributes its +// contribution under this license. +// "Licensed patents" are a contributor's patent claims that read +// directly on its contribution. +// +// 2. Grant of Rights +// (A) Copyright Grant- Subject to the terms of this license, +// including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, +// royalty-free copyright license to reproduce its contribution, +// prepare derivative works of its contribution, and distribute +// its contribution or any derivative works that you create. +// (B) Patent Grant- Subject to the terms of this license, +// including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, +// royalty-free license under its licensed patents to make, have +// made, use, sell, offer for sale, import, and/or otherwise +// dispose of its contribution in the software or derivative works +// of the contribution in the software. +// +// 3. Conditions and Limitations +// (A) No Trademark License- This license does not grant you +// rights to use any contributor's name, logo, or trademarks. +// (B) If you bring a patent claim against any contributor over +// patents that you claim are infringed by the software, your +// patent license from such contributor to the software ends +// automatically. +// (C) If you distribute any portion of the software, you must +// retain all copyright, patent, trademark, and attribution +// notices that are present in the software. +// (D) If you distribute any portion of the software in source +// code form, you may do so only under this license by including a +// complete copy of this license with your distribution. If you +// distribute any portion of the software in compiled or object +// code form, you may only do so under a license that complies +// with this license. +// (E) The software is licensed "as-is." You bear the risk of +// using it. The contributors give no express warranties, +// guarantees or conditions. You may have additional consumer +// rights under your local laws which this license cannot change. +// To the extent permitted under your local laws, the contributors +// exclude the implied warranties of merchantability, fitness for +// a particular purpose and non-infringement. +// + +#pragma once +#include + +namespace shim { + + typedef std::vector Buffer; + + enum DataType { + invalid, + int8, + uint8, + int16, + uint16, + int32, + uint32, + int64, + uint64, + int128, + uint128, + int256, + uint256, + float16, + float32, + float64, + float80, + float96, + float128, + }; + + struct HomogeneousBuffer { + shim::Buffer Buffer; + shim::DataType Type; + }; + + typedef std::vector Layout; + + struct HeterogeneousBuffer { + shim::Buffer Buffer; + shim::Layout Layout; + }; +} diff --git a/python/osd/common.py b/python/osd/common.py new file mode 100644 index 00000000..6e9514e2 --- /dev/null +++ b/python/osd/common.py @@ -0,0 +1,94 @@ +# +# Copyright (C) Pixar. All rights reserved. +# +# This license governs use of the accompanying software. If you +# use the software, you accept this license. If you do not accept +# the license, do not use the software. +# +# 1. Definitions +# The terms "reproduce," "reproduction," "derivative works," and +# "distribution" have the same meaning here as under U.S. +# copyright law. A "contribution" is the original software, or +# any additions or changes to the software. +# A "contributor" is any person or entity that distributes its +# contribution under this license. +# "Licensed patents" are a contributor's patent claims that read +# directly on its contribution. +# +# 2. Grant of Rights +# (A) Copyright Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free copyright license to reproduce its contribution, +# prepare derivative works of its contribution, and distribute +# its contribution or any derivative works that you create. +# (B) Patent Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free license under its licensed patents to make, have +# made, use, sell, offer for sale, import, and/or otherwise +# dispose of its contribution in the software or derivative works +# of the contribution in the software. +# +# 3. Conditions and Limitations +# (A) No Trademark License- This license does not grant you +# rights to use any contributor's name, logo, or trademarks. +# (B) If you bring a patent claim against any contributor over +# patents that you claim are infringed by the software, your +# patent license from such contributor to the software ends +# automatically. +# (C) If you distribute any portion of the software, you must +# retain all copyright, patent, trademark, and attribution +# notices that are present in the software. +# (D) If you distribute any portion of the software in source +# code form, you may do so only under this license by including a +# complete copy of this license with your distribution. If you +# distribute any portion of the software in compiled or object +# code form, you may only do so under a license that complies +# with this license. +# (E) The software is licensed "as-is." You bear the risk of +# using it. The contributors give no express warranties, +# guarantees or conditions. You may have additional consumer +# rights under your local laws which this license cannot change. +# To the extent permitted under your local laws, the contributors +# exclude the implied warranties of merchantability, fitness for +# a particular purpose and non-infringement. +# + +class TopoError(Exception): + def __init__(self, message): + Exception.__init__(self, message) + +class OsdTypeError(Exception): + def __init__(self, message): + Exception.__init__(self, message) + +# From hbr/mesh.h +class BoundaryMode: + "Subdivision boundary rules for :class:`osd.Topology` construction." + + NONE = 0 + "Boundary edges are corner verts are not processed specially." + + EDGE_ONLY = 1 + "Boundary edges are marked infinitely sharp." + + EDGE_AND_CORNER = 2 + '''Boundary edges are marked infinitely sharp, and vertices with + valence=2 are marked infinitely sharp.''' + + ALWAYS_SHARP = 3 + "Unused." + +class Sharpness: + '''Provides some floating-point constants that clients can + optionally use to set sharpness.''' + + SMOOTH = 0 + "As smooth as possible." + + SHARP = 1 + "Moderately sharp." + + INFINITELY_SHARP = 2 + "As sharp as possible." diff --git a/python/osd/internal.h b/python/osd/internal.h new file mode 100644 index 00000000..052a6250 --- /dev/null +++ b/python/osd/internal.h @@ -0,0 +1,82 @@ +// +// Copyright (C) Pixar. All rights reserved. +// +// This license governs use of the accompanying software. If you +// use the software, you accept this license. If you do not accept +// the license, do not use the software. +// +// 1. Definitions +// The terms "reproduce," "reproduction," "derivative works," and +// "distribution" have the same meaning here as under U.S. +// copyright law. A "contribution" is the original software, or +// any additions or changes to the software. +// A "contributor" is any person or entity that distributes its +// contribution under this license. +// "Licensed patents" are a contributor's patent claims that read +// directly on its contribution. +// +// 2. Grant of Rights +// (A) Copyright Grant- Subject to the terms of this license, +// including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, +// royalty-free copyright license to reproduce its contribution, +// prepare derivative works of its contribution, and distribute +// its contribution or any derivative works that you create. +// (B) Patent Grant- Subject to the terms of this license, +// including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, +// royalty-free license under its licensed patents to make, have +// made, use, sell, offer for sale, import, and/or otherwise +// dispose of its contribution in the software or derivative works +// of the contribution in the software. +// +// 3. Conditions and Limitations +// (A) No Trademark License- This license does not grant you +// rights to use any contributor's name, logo, or trademarks. +// (B) If you bring a patent claim against any contributor over +// patents that you claim are infringed by the software, your +// patent license from such contributor to the software ends +// automatically. +// (C) If you distribute any portion of the software, you must +// retain all copyright, patent, trademark, and attribution +// notices that are present in the software. +// (D) If you distribute any portion of the software in source +// code form, you may do so only under this license by including a +// complete copy of this license with your distribution. If you +// distribute any portion of the software in compiled or object +// code form, you may only do so under a license that complies +// with this license. +// (E) The software is licensed "as-is." You bear the risk of +// using it. The contributors give no express warranties, +// guarantees or conditions. You may have additional consumer +// rights under your local laws which this license cannot change. +// To the extent permitted under your local laws, the contributors +// exclude the implied warranties of merchantability, fitness for +// a particular purpose and non-infringement. +// + +#include +#include +#include +#include +#include +#include +#include +#include + +typedef OpenSubdiv::HbrMesh OsdHbrMesh; +typedef OpenSubdiv::HbrVertex OsdHbrVertex; +typedef OpenSubdiv::HbrFace OsdHbrFace; +typedef OpenSubdiv::HbrHalfedge OsdHbrHalfedge; + +struct TopologyImpl { + OsdHbrMesh *hmesh; + std::vector faces; + size_t numVertices; +}; + +struct SubdividerImpl { + OpenSubdiv::FarMesh* farMesh; + OpenSubdiv::OsdCpuComputeContext* computeContext; + OpenSubdiv::OsdCpuVertexBuffer* vertexBuffer; +}; diff --git a/python/osd/osdshim.i b/python/osd/osdshim.i new file mode 100644 index 00000000..fc8787d3 --- /dev/null +++ b/python/osd/osdshim.i @@ -0,0 +1,251 @@ +// +// Copyright (C) Pixar. All rights reserved. +// +// This license governs use of the accompanying software. If you +// use the software, you accept this license. If you do not accept +// the license, do not use the software. +// +// 1. Definitions +// The terms "reproduce," "reproduction," "derivative works," and +// "distribution" have the same meaning here as under U.S. +// copyright law. A "contribution" is the original software, or +// any additions or changes to the software. +// A "contributor" is any person or entity that distributes its +// contribution under this license. +// "Licensed patents" are a contributor's patent claims that read +// directly on its contribution. +// +// 2. Grant of Rights +// (A) Copyright Grant- Subject to the terms of this license, +// including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, +// royalty-free copyright license to reproduce its contribution, +// prepare derivative works of its contribution, and distribute +// its contribution or any derivative works that you create. +// (B) Patent Grant- Subject to the terms of this license, +// including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, +// royalty-free license under its licensed patents to make, have +// made, use, sell, offer for sale, import, and/or otherwise +// dispose of its contribution in the software or derivative works +// of the contribution in the software. +// +// 3. Conditions and Limitations +// (A) No Trademark License- This license does not grant you +// rights to use any contributor's name, logo, or trademarks. +// (B) If you bring a patent claim against any contributor over +// patents that you claim are infringed by the software, your +// patent license from such contributor to the software ends +// automatically. +// (C) If you distribute any portion of the software, you must +// retain all copyright, patent, trademark, and attribution +// notices that are present in the software. +// (D) If you distribute any portion of the software in source +// code form, you may do so only under this license by including a +// complete copy of this license with your distribution. If you +// distribute any portion of the software in compiled or object +// code form, you may only do so under a license that complies +// with this license. +// (E) The software is licensed "as-is." You bear the risk of +// using it. The contributors give no express warranties, +// guarantees or conditions. You may have additional consumer +// rights under your local laws which this license cannot change. +// To the extent permitted under your local laws, the contributors +// exclude the implied warranties of merchantability, fitness for +// a particular purpose and non-infringement. +// + +%module shim + +%{ +#include "subdivider.h" +#include "topology.h" +%} + +%header %{ +#include +#include + +static shim::DataType +_ShimTypeFromNumpyType(int numpyType) +{ + using namespace std; + switch (numpyType) { + case PyArray_FLOAT32: return shim::float32; + case PyArray_UINT32: return shim::uint32; + case PyArray_INT32: return shim::int32; + case PyArray_INT8: return shim::int8; + case PyArray_UBYTE: return shim::uint8; + case PyArray_VOID: + cerr << "Expected a type but got VOID." << endl; + abort(); + return shim::invalid; + case PyArray_STRING: + case PyArray_UNICODE: + cerr << "Strings are not expected here." << endl; + return shim::invalid; + case PyArray_OBJECT: + cerr << "Complex type not allowed here." << endl; + return shim::invalid; + } + cerr << "Unsupported numpy type " << numpyType << endl; + abort(); + return shim::invalid; +} + +static shim::DataType +_ShimTypeFromNumpyType(PyTypeObject* item) +{ + std::string numpyType = ((PyTypeObject*) item)->tp_name; + if (numpyType == "numpy.float32") return shim::float32; + if (numpyType == "numpy.uint32") return shim::uint32; + if (numpyType == "numpy.int32") return shim::int32; + if (numpyType == "numpy.uint8") return shim::uint8; + std::cerr << "Unsupported numpy type " << numpyType << std::endl; + return shim::invalid; +} + +static shim::Layout +_ShimLayoutFromObject(PyObject* obj) +{ + using namespace std; + shim::Layout layout; + if (!obj) { + cerr << "Missing type.\n"; + } else if (PyArray_DescrCheck(obj)) { + PyArray_Descr* descr = (PyArray_Descr*) obj; + int numpyType = descr->type_num; + if (numpyType != PyArray_VOID) { + shim::DataType dtype = _ShimTypeFromNumpyType(numpyType); + layout.push_back(dtype); + } else if (PyDataType_HASFIELDS(descr)) { + PyObject* rawTypes = PyDict_Values(descr->fields); + Py_ssize_t count = PySequence_Size(rawTypes); + + // Each field value is a type-offset pair. We don't honor + // the offsets, so just extract the type and move on. + for (Py_ssize_t i = 0; i < count; ++i) { + PyObject* pair = PySequence_ITEM(rawTypes, i); + PyObject* item = PySequence_ITEM(pair, 0); + shim::Layout newTypes = _ShimLayoutFromObject(item); + layout.insert(layout.end(), newTypes.begin(), newTypes.end()); + Py_DECREF(item); + Py_DECREF(pair); + } + + Py_DECREF(rawTypes); + } else { + cerr << "Please provide a record-style numpy type with fields." << endl; + } + } else if (PyType_Check(obj)) { + layout.push_back( _ShimTypeFromNumpyType((PyTypeObject*) obj) ); + } else if (PySequence_Check(obj)) { + Py_ssize_t count = PySequence_Size(obj); + for (Py_ssize_t i = 0; i < count; ++i) { + PyObject* item = PySequence_ITEM(obj, i); + shim::Layout newTypes = _ShimLayoutFromObject(item); + layout.insert(layout.end(), newTypes.begin(), newTypes.end()); + Py_DECREF(item); + } + } else { + int dtype = (int) PyInt_AsLong(obj); + if (dtype == -1) { + cerr << "Got a " << obj->ob_type->tp_name << " but wanted a data type." << endl; + } else if (dtype != shim::invalid) { + layout.push_back(_ShimTypeFromNumpyType(dtype)); + } + } + return layout; +} + +%} + +%init %{ + import_array(); +%} + +%typemap(in) const shim::HomogeneousBuffer& { + if (!PyArray_CheckExact($input)) { + std::cerr << "This requires a numpy array." << std::endl; + } + $1 = new shim::HomogeneousBuffer(); + int byteCount = PyArray_NBYTES($input); + unsigned char* begin = (unsigned char*) PyArray_BYTES($input); + unsigned char* end = begin + byteCount; + $1->Buffer.assign(begin, end); + int dtype = PyArray_TYPE($input); + $1->Type = _ShimTypeFromNumpyType(dtype); +} + +%typemap(in) shim::Layout { + $1 = _ShimLayoutFromObject($input); +} + +%typemap(in) shim::DataType { + using namespace std; + shim::Layout layout = _ShimLayoutFromObject($input); + if (layout.size() < 1) { + cerr << "Can't convert PyObject " << $input << " to a shim data type." << endl; + $1 = shim::invalid; + } else if (layout.size() > 1) { + cerr << "Complex data type not allowed here." << endl; + $1 = shim::invalid; + } else { + $1 = layout[0]; + } +} + +%typemap(in) const shim::HeterogeneousBuffer& { + using namespace std; + if (!PyArray_CheckExact($input)) { + cerr << "This requires a numpy array." << endl; + } + $1 = new shim::HeterogeneousBuffer(); + int byteCount = PyArray_NBYTES($input); + unsigned char* begin = (unsigned char*) PyArray_BYTES($input); + unsigned char* end = begin + byteCount; + + $1->Buffer.assign(begin, end); + $1->Layout = _ShimLayoutFromObject((PyObject*) PyArray_DESCR($input)); +} + +%typemap(in) shim::Buffer* INOUT { + $1 = new shim::Buffer(); +} + +%typemap(argout) shim::Buffer* INOUT { + using namespace std; + PyObject* obj = (PyObject*) PyArray_DESCR($input); + shim::Layout layout = _ShimLayoutFromObject(obj); + if (layout.size() == 0) { + cerr << "Unknown type for Buffer processing." << endl; + } else if (layout[0] == shim::float32) { + npy_intp numFloats = (npy_intp) ($1->size() / sizeof(float)); + float* ptrFloats = (float*) &(*$1)[0]; + + $result = PyArray_SimpleNew(1, &numFloats, PyArray_FLOAT32); + memcpy(PyArray_BYTES($result), ptrFloats, $1->size()); + + // SimpleNewFromData would avoid a copy but we'd need to keep + // the buffer around in our shim class. It's not + // impossible... + //$result = PyArray_SimpleNewFromData( + // 1, &numFloats, PyArray_FLOAT, (void*) ptrFloats); + + } else if (layout[0] == shim::uint32) { + npy_intp numInts = (npy_intp) ($1->size() / sizeof(unsigned int)); + unsigned int* ptrInts = (unsigned int*) &(*$1)[0]; + $result = PyArray_SimpleNew(1, &numInts, PyArray_UINT32); + memcpy(PyArray_BYTES($result), ptrInts, $1->size()); + } else { + cerr << layout[0] << " is not an understood shim type." + << endl; + } +} + +%typemap(freearg) shim::Buffer* INOUT { + delete $1; +} + +%include "subdivider.h" +%include "topology.h" diff --git a/python/osd/subdivider.cpp b/python/osd/subdivider.cpp new file mode 100644 index 00000000..9aa5595e --- /dev/null +++ b/python/osd/subdivider.cpp @@ -0,0 +1,106 @@ +#include "internal.h" +#include "subdivider.h" +#include "topology.h" + +using namespace std; +using namespace shim; + +OpenSubdiv::OsdCpuComputeController* g_osdComputeController = 0; + +shim::Subdivider::Subdivider( + const Topology& topo, + Layout refinedLayout, + DataType refinedIndexType, + int subdivisionLevels) +{ + self = new SubdividerImpl(); + + if (!g_osdComputeController) { + g_osdComputeController = new OpenSubdiv::OsdCpuComputeController(); + } + + size_t numFloatsPerVertex = 0; + Layout::const_iterator it; + for (it = refinedLayout.begin(); it != refinedLayout.end(); ++it) { + if (*it != float32) { + cerr << "Unsupported vertex type." << endl; + break; + } + ++numFloatsPerVertex; + } + OpenSubdiv::FarMeshFactory meshFactory( + topo.self->hmesh, + subdivisionLevels); + self->farMesh = meshFactory.Create(); + self->computeContext = OpenSubdiv::OsdCpuComputeContext::Create( + self->farMesh); + + self->vertexBuffer = OpenSubdiv::OsdCpuVertexBuffer::Create( + numFloatsPerVertex, self->farMesh->GetNumVertices()); +} + +shim::Subdivider::~Subdivider() +{ + delete self->computeContext; + delete self->farMesh; + delete self->vertexBuffer; + delete self; +} + +void +shim::Subdivider::setCoarseVertices(const HeterogeneousBuffer& cage) +{ + float* pFloats = (float*) &cage.Buffer[0]; + int numFloats = cage.Buffer.size() / sizeof(float); + self->vertexBuffer->UpdateData(pFloats, numFloats); +} + +void +shim::Subdivider::refine() +{ + g_osdComputeController->Refine(self->computeContext, self->vertexBuffer); +} + +void +shim::Subdivider::getRefinedVertices(Buffer* refinedVertices) +{ + float* pFloats = self->vertexBuffer->BindCpuBuffer(); + + int numFloats = self->vertexBuffer->GetNumElements() * + self->vertexBuffer->GetNumVertices(); + + unsigned char* srcBegin = (unsigned char*) pFloats; + unsigned char* srcEnd = srcBegin + numFloats * 4; + refinedVertices->assign(srcBegin, srcEnd); +} + +void +shim::Subdivider::getRefinedQuads(Buffer* refinedQuads) +{ + OpenSubdiv::FarPatchTables const * patchTables = + self->farMesh->GetPatchTables(); + + if (patchTables) { + cerr << "Feature adaptive not supported" << endl; + return; + } + + const OpenSubdiv::FarSubdivisionTables *tables = + self->farMesh->GetSubdivisionTables(); + + bool loop = dynamic_cast*>(tables); + + if (loop) { + cerr << "loop subdivision not supported" << endl; + return; + } + + int level = tables->GetMaxLevel(); + const std::vector &indices = self->farMesh->GetFaceVertices(level-1); + int numInts = (int) indices.size(); + + unsigned char* srcBegin = (unsigned char*) &indices[0]; + unsigned char* srcEnd = srcBegin + numInts * 4; + refinedQuads->assign(srcBegin, srcEnd); +} diff --git a/python/osd/subdivider.h b/python/osd/subdivider.h new file mode 100644 index 00000000..d072fa64 --- /dev/null +++ b/python/osd/subdivider.h @@ -0,0 +1,89 @@ +// +// Copyright (C) Pixar. All rights reserved. +// +// This license governs use of the accompanying software. If you +// use the software, you accept this license. If you do not accept +// the license, do not use the software. +// +// 1. Definitions +// The terms "reproduce," "reproduction," "derivative works," and +// "distribution" have the same meaning here as under U.S. +// copyright law. A "contribution" is the original software, or +// any additions or changes to the software. +// A "contributor" is any person or entity that distributes its +// contribution under this license. +// "Licensed patents" are a contributor's patent claims that read +// directly on its contribution. +// +// 2. Grant of Rights +// (A) Copyright Grant- Subject to the terms of this license, +// including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, +// royalty-free copyright license to reproduce its contribution, +// prepare derivative works of its contribution, and distribute +// its contribution or any derivative works that you create. +// (B) Patent Grant- Subject to the terms of this license, +// including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, +// royalty-free license under its licensed patents to make, have +// made, use, sell, offer for sale, import, and/or otherwise +// dispose of its contribution in the software or derivative works +// of the contribution in the software. +// +// 3. Conditions and Limitations +// (A) No Trademark License- This license does not grant you +// rights to use any contributor's name, logo, or trademarks. +// (B) If you bring a patent claim against any contributor over +// patents that you claim are infringed by the software, your +// patent license from such contributor to the software ends +// automatically. +// (C) If you distribute any portion of the software, you must +// retain all copyright, patent, trademark, and attribution +// notices that are present in the software. +// (D) If you distribute any portion of the software in source +// code form, you may do so only under this license by including a +// complete copy of this license with your distribution. If you +// distribute any portion of the software in compiled or object +// code form, you may only do so under a license that complies +// with this license. +// (E) The software is licensed "as-is." You bear the risk of +// using it. The contributors give no express warranties, +// guarantees or conditions. You may have additional consumer +// rights under your local laws which this license cannot change. +// To the extent permitted under your local laws, the contributors +// exclude the implied warranties of merchantability, fitness for +// a particular purpose and non-infringement. +// + +#pragma once +#include "buffer.h" + +struct SubdividerImpl; + +namespace shim { + + class Topology; + + class Subdivider { + public: + + Subdivider( + const Topology& topo, + shim::Layout refinedLayout, + shim::DataType refinedIndexType, + int subdivisionLevels); + ~Subdivider(); + + void setCoarseVertices(const shim::HeterogeneousBuffer& cage); + void refine(); + + // These argument names must be INOUT to inform SWIG that + // they're mutable: + void getRefinedVertices(shim::Buffer* INOUT); + void getRefinedQuads(shim::Buffer* INOUT); + + private: + SubdividerImpl* self; + }; + +} diff --git a/python/osd/subdivider.py b/python/osd/subdivider.py new file mode 100644 index 00000000..3f7b8c6a --- /dev/null +++ b/python/osd/subdivider.py @@ -0,0 +1,149 @@ +# +# Copyright (C) Pixar. All rights reserved. +# +# This license governs use of the accompanying software. If you +# use the software, you accept this license. If you do not accept +# the license, do not use the software. +# +# 1. Definitions +# The terms "reproduce," "reproduction," "derivative works," and +# "distribution" have the same meaning here as under U.S. +# copyright law. A "contribution" is the original software, or +# any additions or changes to the software. +# A "contributor" is any person or entity that distributes its +# contribution under this license. +# "Licensed patents" are a contributor's patent claims that read +# directly on its contribution. +# +# 2. Grant of Rights +# (A) Copyright Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free copyright license to reproduce its contribution, +# prepare derivative works of its contribution, and distribute +# its contribution or any derivative works that you create. +# (B) Patent Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free license under its licensed patents to make, have +# made, use, sell, offer for sale, import, and/or otherwise +# dispose of its contribution in the software or derivative works +# of the contribution in the software. +# +# 3. Conditions and Limitations +# (A) No Trademark License- This license does not grant you +# rights to use any contributor's name, logo, or trademarks. +# (B) If you bring a patent claim against any contributor over +# patents that you claim are infringed by the software, your +# patent license from such contributor to the software ends +# automatically. +# (C) If you distribute any portion of the software, you must +# retain all copyright, patent, trademark, and attribution +# notices that are present in the software. +# (D) If you distribute any portion of the software in source +# code form, you may do so only under this license by including a +# complete copy of this license with your distribution. If you +# distribute any portion of the software in compiled or object +# code form, you may only do so under a license that complies +# with this license. +# (E) The software is licensed "as-is." You bear the risk of +# using it. The contributors give no express warranties, +# guarantees or conditions. You may have additional consumer +# rights under your local laws which this license cannot change. +# To the extent permitted under your local laws, the contributors +# exclude the implied warranties of merchantability, fitness for +# a particular purpose and non-infringement. +# + +from osd import * +import shim, numpy + +class Subdivider(object): + '''Wraps a frozen :class:`osd.Topology` object and efficiently + subdivides it into triangles. + + On creation, the :class:`Subdivider` is locked to an immutable, + finalized :class:`osd.Topology` object. However, actual + subdivision computation can be requested repeatedly using dynamic + per-vertex data. + + :param topo: Finalized mesh topology. + :type topo: :class:`osd.Topology` + :param vertexLayout: Describes the data structure composing each vertex. + :type vertexLayout: record-style numpy data type_ object + :param indexType: Integer type for the indices returned from `getRefinedTopology`. + :type indexType: single numpy type_ + :param levels: Number of subdivisions. + :type levels: integer + + .. note:: In the current implementation, ``vertexLayout`` must be + composed of ``numpy.float32``, and ``indexType`` must be + ``numpy.uint32``. + + .. _types: http://docs.scipy.org/doc/numpy/user/basics.types.html + .. _type: http://docs.scipy.org/doc/numpy/user/basics.types.html + ''' + + def __init__(self, topo, vertexLayout, indexType, levels): + if levels < 2: + raise TopoError("Subdivision levels must be 2 or greater") + if type(vertexLayout) != numpy.dtype: + vertexLayout = numpy.dtype(vertexLayout) + self.vertexLayout = vertexLayout + self.indexType = indexType + self.levels = levels + self.shim = shim.Subdivider(topo.shim, vertexLayout, indexType, levels) + + # Calls UpdateData on the vertexBuffer. + def setCoarseVertices(self, coarseVerts, listType = None): + '''Pushes a new set of coarse verts to the mesh without + changing topology. + + If a numpy array is supplied for ``coarseVerts``, its + ``dtype`` must be castable (via view_) to the ``vertexLayout`` + of the :class:`Subdivider`. + + If a Python list is supplied for ``coarseVerts``, the client + must also supply the ``listType`` argument to specify the + numpy type of the incoming array before it gets cast to + ``vertexLayout``. + + .. _view: http://docs.scipy.org/doc/numpy-1.6.0/reference/generated/numpy.ndarray.view.html + ''' + if type(coarseVerts) is not numpy.ndarray: + if not listType: + raise TopoError("listType must be supplied") + coarseVerts = numpy.array(coarseVerts, listType) + coarseVerts = coarseVerts.view(self.vertexLayout) + self.shim.setCoarseVertices(coarseVerts) + + # Calls Refine on the compute controller, passing it the compute + # context and vertexBuffer. + def refine(self): + '''Performs the actual subdivision work.''' + self.shim.refine() + + # Calls the strangely-named "BindCpuBuffer" on the + # OsdCpuVertexBuffer to get back a float* + def getRefinedVertices(self): + '''Returns a numpy array representing the vertex data in the + subdivided mesh. + + The data is returned in the format specified by the client + when instancing the subdivider (``vertexLayout``). + ''' + empty = numpy.empty(0, self.vertexLayout) + return self.shim.getRefinedVertices(empty) + + def getRefinedQuads(self): + '''Returns a numpy array representing the vertex indices of each quad in + subdivided mesh. + + The data is returned in the format specified by the client + when instancing the subdivider (``indexType``). + ''' + empty = numpy.empty(0, self.indexType) + return self.shim.getRefinedQuads(empty) + + def __del__(self): + pass diff --git a/python/osd/topology.cpp b/python/osd/topology.cpp new file mode 100644 index 00000000..990a299a --- /dev/null +++ b/python/osd/topology.cpp @@ -0,0 +1,252 @@ +// +// Copyright (C) Pixar. All rights reserved. +// +// This license governs use of the accompanying software. If you +// use the software, you accept this license. If you do not accept +// the license, do not use the software. +// +// 1. Definitions +// The terms "reproduce," "reproduction," "derivative works," and +// "distribution" have the same meaning here as under U.S. +// copyright law. A "contribution" is the original software, or +// any additions or changes to the software. +// A "contributor" is any person or entity that distributes its +// contribution under this license. +// "Licensed patents" are a contributor's patent claims that read +// directly on its contribution. +// +// 2. Grant of Rights +// (A) Copyright Grant- Subject to the terms of this license, +// including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, +// royalty-free copyright license to reproduce its contribution, +// prepare derivative works of its contribution, and distribute +// its contribution or any derivative works that you create. +// (B) Patent Grant- Subject to the terms of this license, +// including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, +// royalty-free license under its licensed patents to make, have +// made, use, sell, offer for sale, import, and/or otherwise +// dispose of its contribution in the software or derivative works +// of the contribution in the software. +// +// 3. Conditions and Limitations +// (A) No Trademark License- This license does not grant you +// rights to use any contributor's name, logo, or trademarks. +// (B) If you bring a patent claim against any contributor over +// patents that you claim are infringed by the software, your +// patent license from such contributor to the software ends +// automatically. +// (C) If you distribute any portion of the software, you must +// retain all copyright, patent, trademark, and attribution +// notices that are present in the software. +// (D) If you distribute any portion of the software in source +// code form, you may do so only under this license by including a +// complete copy of this license with your distribution. If you +// distribute any portion of the software in compiled or object +// code form, you may only do so under a license that complies +// with this license. +// (E) The software is licensed "as-is." You bear the risk of +// using it. The contributors give no express warranties, +// guarantees or conditions. You may have additional consumer +// rights under your local laws which this license cannot change. +// To the extent permitted under your local laws, the contributors +// exclude the implied warranties of merchantability, fitness for +// a particular purpose and non-infringement. +// + +#include "internal.h" +#include "topology.h" + +using namespace shim; +using namespace std; + +static OpenSubdiv::HbrCatmarkSubdivision _catmark; + +shim::Topology::Topology( + const HomogeneousBuffer& indices, const HomogeneousBuffer& valences) +{ + self = new TopologyImpl(); + self->hmesh = new OsdHbrMesh(&_catmark); + + size_t maxIndex = 0; + size_t byteCount = indices.Buffer.size(); + + switch (indices.Type) { + case int32: { + const int *d = (const int*) &indices.Buffer[0]; + maxIndex = (size_t) *max_element(d, d + byteCount / 4); + break; + } + default: + cerr << "Unsupported index type " << indices.Type << endl; + }; + + size_t maxValence = 0; + switch (valences.Type) { + case uint8: { + const unsigned char *d = (const unsigned char*) &valences.Buffer[0]; + maxValence = (size_t) *max_element(d, d + byteCount); + break; + } + default: + cerr << "Unsupported valence type " << valences.Type << endl; + }; + + self->numVertices = 1 + (int) maxIndex; + OpenSubdiv::OsdVertex vert; + for (size_t i = 0; i < self->numVertices; ++i) { + OpenSubdiv::HbrVertex* pVert = + self->hmesh->NewVertex((int) i, vert); + if (!pVert) { + cerr << "Error: Unable to create vertex " << i << endl; + } + } + + int* pIndices = (int*) &indices.Buffer[0]; + unsigned char* pValence = (unsigned char*) &valences.Buffer[0]; + size_t valenceCount = valences.Buffer.size(); + while (valenceCount--) { + int vertsPerFace = *pValence; + OpenSubdiv::HbrFace* pFace = + self->hmesh->NewFace(vertsPerFace, pIndices, 0); + if (!pFace) { + cerr << "Error: Unable to create face (valence = " + << vertsPerFace << ")\n"; + } + pIndices += vertsPerFace; + ++pValence; + } + + self->hmesh->GetFaces(back_inserter(self->faces)); +} + +shim::Topology::~Topology() +{ + delete self->hmesh; + delete self; +} + +void +shim::Topology::copyAnnotationsFrom(const Topology& topo) +{ + int vertexCount = getNumVertices(); + for (int i = 0; i < vertexCount; ++i) { + float s = topo.getVertexSharpness(i); + setVertexSharpness(i, s); + } + + int faceCount = getNumFaces(); + for (int faceIndex = 0; faceIndex < faceCount; ++faceIndex) { + int numEdges = topo.getNumEdges(faceIndex); + for (int edgeIndex = 0; edgeIndex < numEdges; ++edgeIndex) { + float s = topo.getEdgeSharpness(faceIndex, edgeIndex); + setEdgeSharpness(faceIndex, edgeIndex, s); + } + } +} + +void +shim::Topology::finalize() +{ + self->hmesh->Finish(); +} + +BoundaryMode::e +shim::Topology::getBoundaryMode() const +{ + OsdHbrMesh::InterpolateBoundaryMethod bm = + self->hmesh->GetInterpolateBoundaryMethod(); + switch (bm) { + case OsdHbrMesh::k_InterpolateBoundaryNone: + return BoundaryMode::NONE; + case OsdHbrMesh::k_InterpolateBoundaryEdgeOnly: + return BoundaryMode::EDGE_ONLY; + case OsdHbrMesh::k_InterpolateBoundaryEdgeAndCorner: + return BoundaryMode::EDGE_AND_CORNER; + case OsdHbrMesh::k_InterpolateBoundaryAlwaysSharp: + return BoundaryMode::ALWAYS_SHARP; + } + throw("Bad interpolation method."); +} + +void +shim::Topology::setBoundaryMode(BoundaryMode::e bm) +{ + switch (bm) { + case BoundaryMode::NONE: + self->hmesh->SetInterpolateBoundaryMethod( + OsdHbrMesh::k_InterpolateBoundaryNone); + break; + case BoundaryMode::EDGE_ONLY: + self->hmesh->SetInterpolateBoundaryMethod( + OsdHbrMesh::k_InterpolateBoundaryEdgeOnly); + break; + case BoundaryMode::EDGE_AND_CORNER: + self->hmesh->SetInterpolateBoundaryMethod( + OsdHbrMesh::k_InterpolateBoundaryEdgeAndCorner); + break; + case BoundaryMode::ALWAYS_SHARP: + self->hmesh->SetInterpolateBoundaryMethod( + OsdHbrMesh::k_InterpolateBoundaryAlwaysSharp); + break; + } +} + +int +shim::Topology::getNumVertices() const +{ + return self->numVertices; +} + +float +shim::Topology::getVertexSharpness(int vertex) const +{ + return self->hmesh->GetVertex(vertex)->GetSharpness(); +} + +void +shim::Topology::setVertexSharpness(int vertex, float sharpness) +{ + self->hmesh->GetVertex(vertex)->SetSharpness(sharpness); +} + +int +shim::Topology::getNumFaces() const +{ + return self->hmesh->GetNumFaces(); +} + +bool +shim::Topology::getFaceHole(int faceIndex) const +{ + return self->faces[faceIndex]->IsHole(); +} + +void +shim::Topology::setFaceHole(int faceIndex, bool isHole) +{ + if (!isHole) { + cerr << "Unsetting holeness is not supported." << endl; + return; + } + self->faces[faceIndex]->SetHole(); +} + +int +shim::Topology::getNumEdges(int faceIndex) const +{ + return self->faces[faceIndex]->GetNumVertices(); +} + +float +shim::Topology::getEdgeSharpness(int faceIndex, int edgeIndex) const +{ + return self->faces[faceIndex]->GetEdge(edgeIndex)->GetSharpness(); +} + +void +shim::Topology::setEdgeSharpness(int faceIndex, int edgeIndex, float sharpness) +{ + self->faces[faceIndex]->GetEdge(edgeIndex)->SetSharpness(sharpness); +} diff --git a/python/osd/topology.h b/python/osd/topology.h new file mode 100644 index 00000000..c546879a --- /dev/null +++ b/python/osd/topology.h @@ -0,0 +1,106 @@ +// +// Copyright (C) Pixar. All rights reserved. +// +// This license governs use of the accompanying software. If you +// use the software, you accept this license. If you do not accept +// the license, do not use the software. +// +// 1. Definitions +// The terms "reproduce," "reproduction," "derivative works," and +// "distribution" have the same meaning here as under U.S. +// copyright law. A "contribution" is the original software, or +// any additions or changes to the software. +// A "contributor" is any person or entity that distributes its +// contribution under this license. +// "Licensed patents" are a contributor's patent claims that read +// directly on its contribution. +// +// 2. Grant of Rights +// (A) Copyright Grant- Subject to the terms of this license, +// including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, +// royalty-free copyright license to reproduce its contribution, +// prepare derivative works of its contribution, and distribute +// its contribution or any derivative works that you create. +// (B) Patent Grant- Subject to the terms of this license, +// including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, +// royalty-free license under its licensed patents to make, have +// made, use, sell, offer for sale, import, and/or otherwise +// dispose of its contribution in the software or derivative works +// of the contribution in the software. +// +// 3. Conditions and Limitations +// (A) No Trademark License- This license does not grant you +// rights to use any contributor's name, logo, or trademarks. +// (B) If you bring a patent claim against any contributor over +// patents that you claim are infringed by the software, your +// patent license from such contributor to the software ends +// automatically. +// (C) If you distribute any portion of the software, you must +// retain all copyright, patent, trademark, and attribution +// notices that are present in the software. +// (D) If you distribute any portion of the software in source +// code form, you may do so only under this license by including a +// complete copy of this license with your distribution. If you +// distribute any portion of the software in compiled or object +// code form, you may only do so under a license that complies +// with this license. +// (E) The software is licensed "as-is." You bear the risk of +// using it. The contributors give no express warranties, +// guarantees or conditions. You may have additional consumer +// rights under your local laws which this license cannot change. +// To the extent permitted under your local laws, the contributors +// exclude the implied warranties of merchantability, fitness for +// a particular purpose and non-infringement. +// + +#pragma once +#include "buffer.h" + +struct TopologyImpl; + +namespace shim { + + struct BoundaryMode { + enum e { + NONE, + EDGE_ONLY, + EDGE_AND_CORNER, + ALWAYS_SHARP, + }; + }; + + class Subdivider; + + class Topology { + public: + Topology(const shim::HomogeneousBuffer& indices, + const shim::HomogeneousBuffer& valences); + ~Topology(); + + void copyAnnotationsFrom(const Topology& topo); + + void finalize(); + + BoundaryMode::e getBoundaryMode() const; + void setBoundaryMode(BoundaryMode::e bm); + + int getNumVertices() const; + float getVertexSharpness(int vertex) const; + void setVertexSharpness(int vertex, float sharpness); + + int getNumFaces() const; + bool getFaceHole(int face) const; + void setFaceHole(int face, bool isHole); + + int getNumEdges(int face) const; + float getEdgeSharpness(int face, int edge) const; + void setEdgeSharpness(int face, int edge, float sharpness); + + private: + TopologyImpl *self; + friend class shim::Subdivider; + }; + +} diff --git a/python/osd/topology.py b/python/osd/topology.py new file mode 100644 index 00000000..9e2adba6 --- /dev/null +++ b/python/osd/topology.py @@ -0,0 +1,209 @@ +# +# Copyright (C) Pixar. All rights reserved. +# +# This license governs use of the accompanying software. If you +# use the software, you accept this license. If you do not accept +# the license, do not use the software. +# +# 1. Definitions +# The terms "reproduce," "reproduction," "derivative works," and +# "distribution" have the same meaning here as under U.S. +# copyright law. A "contribution" is the original software, or +# any additions or changes to the software. +# A "contributor" is any person or entity that distributes its +# contribution under this license. +# "Licensed patents" are a contributor's patent claims that read +# directly on its contribution. +# +# 2. Grant of Rights +# (A) Copyright Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free copyright license to reproduce its contribution, +# prepare derivative works of its contribution, and distribute +# its contribution or any derivative works that you create. +# (B) Patent Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free license under its licensed patents to make, have +# made, use, sell, offer for sale, import, and/or otherwise +# dispose of its contribution in the software or derivative works +# of the contribution in the software. +# +# 3. Conditions and Limitations +# (A) No Trademark License- This license does not grant you +# rights to use any contributor's name, logo, or trademarks. +# (B) If you bring a patent claim against any contributor over +# patents that you claim are infringed by the software, your +# patent license from such contributor to the software ends +# automatically. +# (C) If you distribute any portion of the software, you must +# retain all copyright, patent, trademark, and attribution +# notices that are present in the software. +# (D) If you distribute any portion of the software in source +# code form, you may do so only under this license by including a +# complete copy of this license with your distribution. If you +# distribute any portion of the software in compiled or object +# code form, you may only do so under a license that complies +# with this license. +# (E) The software is licensed "as-is." You bear the risk of +# using it. The contributors give no express warranties, +# guarantees or conditions. You may have additional consumer +# rights under your local laws which this license cannot change. +# To the extent permitted under your local laws, the contributors +# exclude the implied warranties of merchantability, fitness for +# a particular purpose and non-infringement. +# + +from common import * +from adapters import * + +import numpy as np +import shim +import itertools + +class Topology(object): + '''Represents an abstract graph of connected polygons. + + A :class:`Topology` object contains only connectivity information; + it does not contain any coordinate data. Instances are simply + populated, finalized, and submitted to an instance of + :class:`osd.Subdivider`. + + The constructor can take a single two-dimensional numpy array + (``indices``), or two one-dimensional numpy arrays + (``indices`` and ``valences``). If every face has the same + valence, clients can pass in a single integer for + ``valences``. + + If desired, simple Python lists can be used in lieu of numpy + arrays. + + .. note:: Input data is always copied to internal storage, + rather than referenced. + + :param indices: Defines each face as a list of vertex indices. + :type indices: list or numpy array + :param valences: If ``indices`` is 2D, this should be :const:`None`. If every face has the same valence, this can be a single integer. Otherwise this is a list of integers that specify the valence of each face. + :type valences: list, number, or numpy array + ''' + + def __init__(self, indices, valences = None): + indices, valences = _flatten_args(indices, valences) + valences = _process_valences(indices, valences) + _check_topology(indices, valences) + + self.indices = np.array(indices, 'int32') + self.valences = np.array(valences, 'uint8') + self.shim = shim.Topology(self.indices, self.valences) + self.boundaryMode = BoundaryMode.EDGE_ONLY + self._vertexListAdapter = VertexListAdapter(self.shim) + self._faceListAdapter = FaceListAdapter(self.shim) + + def reset(self): + '''Un-finalizes the mesh to allow adjustment of sharpness and + custom data. + + This is a costly operation since it effectively recreates the + HBR mesh. + ''' + topo = shim.Topology(self.indices, self.valences) + topo.copyAnnotationsFrom(self.shim) + self._vertexListAdapter = VertexListAdapter(topo) + self._faceListAdapter = FaceListAdapter(topo) + self.shim = topo + + @property + def boundaryMode(self): + '''Gets or sets the boundary interpolation method for this + mesh to one of the values defined in + :class:`osd.BoundaryMode`. + ''' + return self.shim.getBoundaryMode() + + @boundaryMode.setter + def boundaryMode(self, value): + self.shim.setBoundaryMode(value) + + @property + def vertices(self): + '''Pythonic read/write access to special attributes (e.g., sharpness) in + the HBR mesh. + + This property is not an actual list-of-things; it's a short-lived + adapter that allows clients to use Pythonic properties:: + + topo = osd.Topology(faces) + topo.vertices[2].sharpness = 1.3 + + ''' + return self._vertexListAdapter + + @property + def faces(self): + '''Pythonic read/write access to face data and edge data in the HBR mesh. + + This property is not an actual list-of-things; it's a short-lived + adapter that allows clients to use Pythonic properties:: + + topo = osd.Topology(faces) + topo.faces[1].edges[0].sharpness = 0.6 + + ''' + return self._faceListAdapter + + def finalize(self): + '''Calls finish on the HBR mesh, thus preparing it for subdivision.''' + self.shim.finalize() + + def __del__(self): + pass + +# Checks that no valence is less than 3, and that the sum of all valences +# equal to the # of indices. +def _check_topology(indices, valences): + acc = 0 + for v in valences: + if v < 3: + raise TopoError("all valences must be 3 or greater") + acc = acc + v + if len(indices) != acc: + msg = "sum of valences ({0}) isn't equal to the number of indices ({1})" + raise TopoError(msg.format(acc, len(indices))) + +# Given a list-of-lists, returns a list pair where the first list has +# values and the second list has the original counts. +def _flatten(faces): + flattened = list(itertools.chain(*faces)) + lengths = [len(face) for face in faces] + return flattened, lengths + +# If indices is two-dimensional, splits it into two lists. +# Otherwise returns the lists unchanged. +def _flatten_args(indices, valences): + try: + flattened, lengths = _flatten(indices) + if valences is not None: + raise OsdTypeError( + "valences must be None if indices is two-dimensional") + return (flattened, lengths) + except TypeError: + if valences is None: + raise OsdTypeError( + "valences must be provided if indices is one-dimensional") + return (indices, valences) + +# If valences is a scalar, returns a list of valences. +# Otherwise returns the original valence list. +def _process_valences(indices, valences): + try: + v = int(valences) + faceCount = len(indices) / v + if len(indices) % v is not 0: + msg = "Scalar provided for valences argument ({0}) that " \ + "does evenly divide the number of indices ({1})" + raise OsdTypeError(msg.format(len(indices), v)) + valences = [v] * faceCount + except TypeError: + pass + return valences diff --git a/python/setup.py b/python/setup.py new file mode 100755 index 00000000..e8ae2a01 --- /dev/null +++ b/python/setup.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python + +# +# Copyright (C) Pixar. All rights reserved. +# +# This license governs use of the accompanying software. If you +# use the software, you accept this license. If you do not accept +# the license, do not use the software. +# +# 1. Definitions +# The terms "reproduce," "reproduction," "derivative works," and +# "distribution" have the same meaning here as under U.S. +# copyright law. A "contribution" is the original software, or +# any additions or changes to the software. +# A "contributor" is any person or entity that distributes its +# contribution under this license. +# "Licensed patents" are a contributor's patent claims that read +# directly on its contribution. +# +# 2. Grant of Rights +# (A) Copyright Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free copyright license to reproduce its contribution, +# prepare derivative works of its contribution, and distribute +# its contribution or any derivative works that you create. +# (B) Patent Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free license under its licensed patents to make, have +# made, use, sell, offer for sale, import, and/or otherwise +# dispose of its contribution in the software or derivative works +# of the contribution in the software. +# +# 3. Conditions and Limitations +# (A) No Trademark License- This license does not grant you +# rights to use any contributor's name, logo, or trademarks. +# (B) If you bring a patent claim against any contributor over +# patents that you claim are infringed by the software, your +# patent license from such contributor to the software ends +# automatically. +# (C) If you distribute any portion of the software, you must +# retain all copyright, patent, trademark, and attribution +# notices that are present in the software. +# (D) If you distribute any portion of the software in source +# code form, you may do so only under this license by including a +# complete copy of this license with your distribution. If you +# distribute any portion of the software in compiled or object +# code form, you may only do so under a license that complies +# with this license. +# (E) The software is licensed "as-is." You bear the risk of +# using it. The contributors give no express warranties, +# guarantees or conditions. You may have additional consumer +# rights under your local laws which this license cannot change. +# To the extent permitted under your local laws, the contributors +# exclude the implied warranties of merchantability, fitness for +# a particular purpose and non-infringement. +# + +from distutils.core import setup, Command, Extension +import numpy +import os, os.path + +np_include_dir = numpy.get_include() +np_library_dir = os.path.join(np_include_dir, '../lib') +osd_include_dirs = ['../opensubdiv', '../regression'] + +def import_build_folder(): + import sys, distutils.util, os.path + build_dir = "build/lib.{0}-{1}.{2}".format( + distutils.util.get_platform(), + *sys.version_info) + if not os.path.exists(build_dir): + print "Folder does not exist: " + build_dir + print "Perhaps you need to run:" + print " python setup.py build" + else: + sys.path.insert(0, build_dir) + +osd_shim = Extension( + 'osd._shim', + runtime_library_dirs = [osd_lib_path], + include_dirs = osd_include_dirs, + library_dirs = ['../build/lib', np_library_dir], + libraries = ['osdCPU', 'npymath'], + swig_opts = ['-c++'], + sources = [ + 'osd/osdshim.i', + 'osd/subdivider.cpp', + 'osd/topology.cpp']) + +osd_shim.extra_compile_args = \ + ["-Wno-unused-function"] + +os.environ['ARCHFLAGS'] = '-arch ' + os.uname()[4] + +class TestCommand(Command): + description = "runs unit tests" + user_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + import_build_folder() + import unittest, test + suite = unittest.defaultTestLoader.loadTestsFromModule(test) + unittest.TextTestRunner(verbosity=2).run(suite) + +class DemoCommand(Command): + description = "runs a little PyQt demo of the Python wrapper" + user_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + import_build_folder() + import demo + demo.main() + +class InteractiveCommand(Command): + description = "runs a little PyQt demo of the Python wrapper" + user_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + import_build_folder() + import demo + demo.interactive() + +class DocCommand(Command): + description = "Generate HTML documentation with Sphinx" + user_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + import os + os.chdir('doc') + os.system('make clean html') + +class BuildCommand(build): + description = "Builds the Python bindings" + user_options = [ + ('osddir=', 'o', + 'directory that contains libosdCPU.a etc')] + def initialize_options(self): + self.osddir = None + def finalize_options(self): + if self.osddir is None: + self.osddir = '../build/lib' + def run(self): + build.run(self) + +setup(name = "OpenSubdiv", + version = "0.1", + packages = ['osd'], + author = 'Pixar Animation Studios', + cmdclass = { + 'build': BuildCommand, + 'test': TestCommand, + 'doc': DocCommand, + 'interactive': InteractiveCommand, + 'demo': DemoCommand}, + include_dirs = [np_include_dir], + ext_modules = [osd_shim], + description = 'Python Bindings to the Pixar Subdivision Library') diff --git a/python/test/__init__.py b/python/test/__init__.py new file mode 100644 index 00000000..8c856d92 --- /dev/null +++ b/python/test/__init__.py @@ -0,0 +1,58 @@ +# +# Copyright (C) Pixar. All rights reserved. +# +# This license governs use of the accompanying software. If you +# use the software, you accept this license. If you do not accept +# the license, do not use the software. +# +# 1. Definitions +# The terms "reproduce," "reproduction," "derivative works," and +# "distribution" have the same meaning here as under U.S. +# copyright law. A "contribution" is the original software, or +# any additions or changes to the software. +# A "contributor" is any person or entity that distributes its +# contribution under this license. +# "Licensed patents" are a contributor's patent claims that read +# directly on its contribution. +# +# 2. Grant of Rights +# (A) Copyright Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free copyright license to reproduce its contribution, +# prepare derivative works of its contribution, and distribute +# its contribution or any derivative works that you create. +# (B) Patent Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free license under its licensed patents to make, have +# made, use, sell, offer for sale, import, and/or otherwise +# dispose of its contribution in the software or derivative works +# of the contribution in the software. +# +# 3. Conditions and Limitations +# (A) No Trademark License- This license does not grant you +# rights to use any contributor's name, logo, or trademarks. +# (B) If you bring a patent claim against any contributor over +# patents that you claim are infringed by the software, your +# patent license from such contributor to the software ends +# automatically. +# (C) If you distribute any portion of the software, you must +# retain all copyright, patent, trademark, and attribution +# notices that are present in the software. +# (D) If you distribute any portion of the software in source +# code form, you may do so only under this license by including a +# complete copy of this license with your distribution. If you +# distribute any portion of the software in compiled or object +# code form, you may only do so under a license that complies +# with this license. +# (E) The software is licensed "as-is." You bear the risk of +# using it. The contributors give no express warranties, +# guarantees or conditions. You may have additional consumer +# rights under your local laws which this license cannot change. +# To the extent permitted under your local laws, the contributors +# exclude the implied warranties of merchantability, fitness for +# a particular purpose and non-infringement. +# + +from simple import * diff --git a/python/test/simple.py b/python/test/simple.py new file mode 100755 index 00000000..546da07b --- /dev/null +++ b/python/test/simple.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python + +# +# Copyright (C) Pixar. All rights reserved. +# +# This license governs use of the accompanying software. If you +# use the software, you accept this license. If you do not accept +# the license, do not use the software. +# +# 1. Definitions +# The terms "reproduce," "reproduction," "derivative works," and +# "distribution" have the same meaning here as under U.S. +# copyright law. A "contribution" is the original software, or +# any additions or changes to the software. +# A "contributor" is any person or entity that distributes its +# contribution under this license. +# "Licensed patents" are a contributor's patent claims that read +# directly on its contribution. +# +# 2. Grant of Rights +# (A) Copyright Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free copyright license to reproduce its contribution, +# prepare derivative works of its contribution, and distribute +# its contribution or any derivative works that you create. +# (B) Patent Grant- Subject to the terms of this license, +# including the license conditions and limitations in section 3, +# each contributor grants you a non-exclusive, worldwide, +# royalty-free license under its licensed patents to make, have +# made, use, sell, offer for sale, import, and/or otherwise +# dispose of its contribution in the software or derivative works +# of the contribution in the software. +# +# 3. Conditions and Limitations +# (A) No Trademark License- This license does not grant you +# rights to use any contributor's name, logo, or trademarks. +# (B) If you bring a patent claim against any contributor over +# patents that you claim are infringed by the software, your +# patent license from such contributor to the software ends +# automatically. +# (C) If you distribute any portion of the software, you must +# retain all copyright, patent, trademark, and attribution +# notices that are present in the software. +# (D) If you distribute any portion of the software in source +# code form, you may do so only under this license by including a +# complete copy of this license with your distribution. If you +# distribute any portion of the software in compiled or object +# code form, you may only do so under a license that complies +# with this license. +# (E) The software is licensed "as-is." You bear the risk of +# using it. The contributors give no express warranties, +# guarantees or conditions. You may have additional consumer +# rights under your local laws which this license cannot change. +# To the extent permitted under your local laws, the contributors +# exclude the implied warranties of merchantability, fitness for +# a particular purpose and non-infringement. +# + +import numpy as np +import unittest, sys +import osd + +# Topology of a cube. +faces = [ (0,1,3,2), + (2,3,5,4), + (4,5,7,6), + (6,7,1,0), + (1,7,5,3), + (6,0,2,4) ] + +# Vertex positions and "temperature" as an example of a custom +# attribute. +verts = [ 0.000000, -1.414214, 1.000000, 71, + 1.414214, 0.000000, 1.000000, 82, + -1.414214, 0.000000, 1.000000, 95, + 0.000000, 1.414214, 1.000000, 100, + -1.414214, 0.000000, -1.000000, 63, + 0.000000, 1.414214, -1.000000, 77, + 0.000000, -1.414214, -1.000000, 82, + 1.414214, 0.000000, -1.000000, 32 ] + +dtype = [('Px', np.float32), + ('Py', np.float32), + ('Pz', np.float32), + ('temperature', np.float32)] + +class SimpleTest(unittest.TestCase): + + def test_usage(self): + mesh = osd.Topology(faces) + mesh.boundaryMode = osd.BoundaryMode.EDGE_ONLY + + mesh.vertices[0].sharpness = 2.7 + self.assertAlmostEqual(mesh.vertices[0].sharpness, 2.7) + + self.assertEqual(len(mesh.vertices), len(verts) / len(dtype)) + self.assertEqual(len(mesh.faces), len(faces)) + self.assertEqual(len(mesh.faces[0].edges), len(faces[0])) + + mesh.finalize() + + subdivider = osd.Subdivider( + mesh, + vertexLayout = dtype, + indexType = np.uint32, + levels = 4) + + subdivider.setCoarseVertices(verts, np.float32) + subdivider.refine() + + numQuads = len(subdivider.getRefinedQuads()) / 4 + numVerts = len(subdivider.getRefinedVertices()) / len(dtype) + + self.assertEqual(numQuads, 1536, "Unexpected number of refined quads") + self.assertEqual(numVerts, 2056, "Unexpected number of refined verts") + + # For now, disable the leak test by prepending "do_not_". + def do_not_test_leaks(self): + self.test_usage() + start = _get_heap_usage() + history = [] + for i in xrange(1024): + self.test_usage() + if ((i+1) % 256) == 0: + history.append(_get_heap_usage() - start) + print str(history[-1]) + "...", + sys.stdout.flush() + print + total = 0 + for i in xrange(1, len(history)): + delta = history[i] - history[i - 1] + if delta <= 0: + return + total = total + delta + avg = total / (len(history) - 1) + self.fail("Memory usage is strictly increasing ({0}).".format(avg)) + + def test_Topology_creation(self): + + # Input data + indices, valences = _flatten(faces) + + # Native list-of-lists, constant valence: + mesh = osd.Topology(faces) + self.assert_(mesh, + "Unable to construct Topology object from a list-of-lists") + + # Native list, constant valence: + mesh = osd.Topology(indices, 4) + self.assert_(mesh, + "Unable to construct Topology object from a list") + + # Native list-of-lists, variable valence: + faces2 = faces + [(8,9,10)] + mesh = osd.Topology(faces2) + self.assert_(mesh, + "Unable to construct Topology object from a list of " + "variable-sized lists") + + # Two-dimensional numpy array: + numpyFaces = np.array(indices, 'uint16').reshape(-1, 4) + mesh = osd.Topology(numpyFaces) + self.assert_(mesh, + "Unable to construct Topology object from a " + "two-dimensional numpy array") + + # Native index list and valence list: + mesh = osd.Topology(indices, valences) + self.assert_(mesh) + + # Numpy index list and valence list: + indices = np.array(indices, 'uint16') + valences = np.array(valences, 'uint8') + mesh = osd.Topology(indices, valences) + self.assert_(mesh) + + # Ensure various topology checks + self.assertRaises(osd.OsdTypeError, osd.Topology, indices, None) + self.assertRaises(osd.OsdTypeError, osd.Topology, faces, valences) + faces2 = faces + [(8,9)] + self.assertRaises(osd.TopoError, osd.Topology, faces2) + valences2 = valences + [3] + self.assertRaises(osd.TopoError, osd.Topology, indices, valences2) + +def _get_heap_usage(): + import resource + return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + +def _flatten(faces): + import itertools + flattened = list(itertools.chain(*faces)) + lengths = [len(face) for face in faces] + return flattened, lengths + +if __name__ == "__main__": + unittest.main() From eb63ac07531bc78b3beb1710099c35e8088628c5 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Wed, 6 Feb 2013 08:53:44 -0800 Subject: [PATCH 2/8] iterating on the cmake stuff for a python module --- CMakeLists.txt | 18 +++++++++++------- python/setup.py | 8 ++++++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f67c1907..003760e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -201,7 +201,7 @@ find_package(OpenCL 1.1) find_package(CUDA 4.0) find_package(GLFW 2.7.0) find_package(PTex 2.0) -find_package(PythonInterp) +find_package(PythonInterp 2.6) find_package(SWIG) if (NOT APPLE AND OPENGL_FOUND) @@ -325,7 +325,8 @@ else() ) endif() -if(PYTHON_FOUND AND SWIG_FOUND) +if(PYTHONINTERP_FOUND AND SWIG_FOUND) + message(STATUS "Python and SWIG found. Looking for numpy...") execute_process( COMMAND ${PYTHON_EXECUTABLE} -c "import numpy; print numpy.get_include()" @@ -336,16 +337,19 @@ if(PYTHON_FOUND AND SWIG_FOUND) if(NUMPY_ERR) message(WARNING "Unable to import numpy.") else() + message(STATUS "Numpy package has been located, Python bindings will be built.") add_custom_command( - COMMENT "Building Python bindings with distutils" - TARGET ${target} POST_BUILD + OUTPUT ${PROJECT_BINARY_DIR}/python + COMMAND ${PYTHON_EXECUTABLE} setup.py build --osddir=${LIBRARY_OUTPUT_PATH} --build-base=${PROJECT_BINARY_DIR}/python WORKING_DIRECTORY python - DEPENDS opensubdiv - COMMAND ${PYTHON_EXECUTABLE} setup.py build osddir='${LIBRARY_OUTPUT_PATH}' + DEPENDS osd_static_cpu osd_dynamic_cpu + COMMENT "Building Python bindings with distutils" ) + add_custom_target(python ALL + DEPENDS ${PROJECT_BINARY_DIR}/python) install(CODE "execute_process(" "WORKING_DIRECTORY python " - "COMMAND ${PYTHON_EXECUTABLE} setup.py install") + "COMMAND ${PYTHON_EXECUTABLE} setup.py build --osddir=${LIBRARY_OUTPUT_PATH} --build-base=/home/prideout/git/OpenSubdiv/build/python install") endif() endif() diff --git a/python/setup.py b/python/setup.py index e8ae2a01..8b427373 100755 --- a/python/setup.py +++ b/python/setup.py @@ -58,6 +58,8 @@ # from distutils.core import setup, Command, Extension +from distutils.command.build import build + import numpy import os, os.path @@ -79,7 +81,6 @@ def import_build_folder(): osd_shim = Extension( 'osd._shim', - runtime_library_dirs = [osd_lib_path], include_dirs = osd_include_dirs, library_dirs = ['../build/lib', np_library_dir], libraries = ['osdCPU', 'npymath'], @@ -145,14 +146,17 @@ class DocCommand(Command): class BuildCommand(build): description = "Builds the Python bindings" - user_options = [ + user_options = build.user_options + [ ('osddir=', 'o', 'directory that contains libosdCPU.a etc')] def initialize_options(self): + build.initialize_options(self) self.osddir = None def finalize_options(self): + build.finalize_options(self) if self.osddir is None: self.osddir = '../build/lib' + osd_shim.runtime_library_dirs = [self.osddir] def run(self): build.run(self) From 8c6785594c870ded9cb99d9bcecd6238e5324588 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Wed, 6 Feb 2013 19:24:40 -0800 Subject: [PATCH 3/8] CMake now invokes distutils correctly --- CMakeLists.txt | 22 +++++++++++++-------- python/README.md | 13 +++++-------- python/demo/interactive.py | 0 python/demo/main.py | 0 python/demo/shaders.py | 2 +- python/setup.py | 40 ++++++++++++++++++++++---------------- 6 files changed, 43 insertions(+), 34 deletions(-) mode change 100644 => 100755 python/demo/interactive.py mode change 100644 => 100755 python/demo/main.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 003760e8..40832fa0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -337,19 +337,25 @@ if(PYTHONINTERP_FOUND AND SWIG_FOUND) if(NUMPY_ERR) message(WARNING "Unable to import numpy.") else() - message(STATUS "Numpy package has been located, Python bindings will be built.") + message(STATUS "Numpy package has been located.") + set(PYCMD ${PYTHON_EXECUTABLE} setup.py build ) + list(APPEND PYCMD --osddir=${LIBRARY_OUTPUT_PATH} ) + list(APPEND PYCMD --build-platlib=${PROJECT_BINARY_DIR}/python ) + list(APPEND PYCMD --build-temp=${PROJECT_BINARY_DIR}/temp ) add_custom_command( - OUTPUT ${PROJECT_BINARY_DIR}/python - COMMAND ${PYTHON_EXECUTABLE} setup.py build --osddir=${LIBRARY_OUTPUT_PATH} --build-base=${PROJECT_BINARY_DIR}/python - WORKING_DIRECTORY python + OUTPUT ${PROJECT_BINARY_DIR}/python/osd + COMMAND ${PYCMD} + WORKING_DIRECTORY ../python DEPENDS osd_static_cpu osd_dynamic_cpu COMMENT "Building Python bindings with distutils" ) add_custom_target(python ALL - DEPENDS ${PROJECT_BINARY_DIR}/python) - install(CODE "execute_process(" - "WORKING_DIRECTORY python " - "COMMAND ${PYTHON_EXECUTABLE} setup.py build --osddir=${LIBRARY_OUTPUT_PATH} --build-base=/home/prideout/git/OpenSubdiv/build/python install") + DEPENDS ${PROJECT_BINARY_DIR}/python/osd + ) + install(CODE "execute_process( + WORKING_DIRECTORY ../python + COMMAND ${PYCMD} install --user)" + ) endif() endif() diff --git a/python/README.md b/python/README.md index 429b8662..7d8dd22d 100644 --- a/python/README.md +++ b/python/README.md @@ -1,17 +1,13 @@ # Instructions The OpenSubdiv Python wrapper has been tested with Python 2.6 and Python 2.7. -Make sure you have the numpy module installed before you begin. +Make sure you install SWIG and numpy before you begin. -First, try building the extension with: +CMake builds the extension like this: - ./setup.py build osddir='../build/lib' + ./setup.py build --osddir='../build/lib' --build-platlib='../build/python' -You'll need to replace `../build/lib` with the folder that has `libosdCPU.a` et al. - -This creates a build folder with a platform-specific subfolder, such as: - - ./build/lib.macosx-10.8-intel-2.7 +If you invoke this manually, you'll need to replace `../build/lib` with the folder that has `libosdCPU.a`. Next, try out the unit tests: @@ -41,3 +37,4 @@ After installing the module, you can generate and view the Sphinx-generated docu - Instead of using OsdCpuVertexBuffer, create a "NumpyCpuVertexBuffer" that wraps a numpy array - Add an API that looks very similar to the RIB parameters for RiHierarchicalSubdiv - Remove all the caveats that are listed in the Sphinx docs :) +- Sphinx documentation should be CMake-ified. diff --git a/python/demo/interactive.py b/python/demo/interactive.py old mode 100644 new mode 100755 diff --git a/python/demo/main.py b/python/demo/main.py old mode 100644 new mode 100755 diff --git a/python/demo/shaders.py b/python/demo/shaders.py index 8d0853d9..7ea04554 100644 --- a/python/demo/shaders.py +++ b/python/demo/shaders.py @@ -57,7 +57,7 @@ from OpenGL.GL import * -ProgramFiles = ['demo/simple.glsl'] +ProgramFiles = ['simple.glsl'] Programs = { "BareBones" : { diff --git a/python/setup.py b/python/setup.py index 8b427373..c926ff04 100755 --- a/python/setup.py +++ b/python/setup.py @@ -66,23 +66,12 @@ import os, os.path np_include_dir = numpy.get_include() np_library_dir = os.path.join(np_include_dir, '../lib') osd_include_dirs = ['../opensubdiv', '../regression'] - -def import_build_folder(): - import sys, distutils.util, os.path - build_dir = "build/lib.{0}-{1}.{2}".format( - distutils.util.get_platform(), - *sys.version_info) - if not os.path.exists(build_dir): - print "Folder does not exist: " + build_dir - print "Perhaps you need to run:" - print " python setup.py build" - else: - sys.path.insert(0, build_dir) +osddir = '../build/lib' osd_shim = Extension( 'osd._shim', include_dirs = osd_include_dirs, - library_dirs = ['../build/lib', np_library_dir], + library_dirs = [osddir, np_library_dir], libraries = ['osdCPU', 'npymath'], swig_opts = ['-c++'], sources = [ @@ -95,6 +84,21 @@ osd_shim.extra_compile_args = \ os.environ['ARCHFLAGS'] = '-arch ' + os.uname()[4] +def setBuildFolder(folder): + osddir = folder + osd_shim.runtime_library_dirs = [folder] + osd_shim.library_dirs = [folder, np_library_dir] + +def importBuildFolder(): + import os.path + builddir = os.path.join(osddir, "../python") + if not os.path.exists(builddir): + print "Folder does not exist: " + builddir + print "Perhaps you need to run:" + print " python setup.py build" + else: + sys.path.insert(0, builddir) + class TestCommand(Command): description = "runs unit tests" user_options = [] @@ -103,7 +107,7 @@ class TestCommand(Command): def finalize_options(self): pass def run(self): - import_build_folder() + importBuildFolder() import unittest, test suite = unittest.defaultTestLoader.loadTestsFromModule(test) unittest.TextTestRunner(verbosity=2).run(suite) @@ -116,8 +120,9 @@ class DemoCommand(Command): def finalize_options(self): pass def run(self): - import_build_folder() + importBuildFolder() import demo + os.chdir('demo') demo.main() class InteractiveCommand(Command): @@ -128,8 +133,9 @@ class InteractiveCommand(Command): def finalize_options(self): pass def run(self): - import_build_folder() + importBuildFolder() import demo + os.chdir('demo') demo.interactive() class DocCommand(Command): @@ -156,7 +162,7 @@ class BuildCommand(build): build.finalize_options(self) if self.osddir is None: self.osddir = '../build/lib' - osd_shim.runtime_library_dirs = [self.osddir] + setBuildFolder(self.osddir) def run(self): build.run(self) From bb6fd9489b6d7743f53d232c3ad6a904b6a232d8 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Wed, 6 Feb 2013 21:27:16 -0800 Subject: [PATCH 4/8] update docs --- CMakeLists.txt | 2 +- python/demo/main.py | 9 ++++----- python/doc/index.rst | 28 +++++++++++++++++----------- python/osd/subdivider.py | 8 ++++---- python/osd/topology.py | 5 +++-- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 40832fa0..395a4cd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -201,7 +201,7 @@ find_package(OpenCL 1.1) find_package(CUDA 4.0) find_package(GLFW 2.7.0) find_package(PTex 2.0) -find_package(PythonInterp 2.6) +find_package(PythonInterp 2.7) find_package(SWIG) if (NOT APPLE AND OPENGL_FOUND) diff --git a/python/demo/main.py b/python/demo/main.py index 847b7b0c..2a6054e9 100755 --- a/python/demo/main.py +++ b/python/demo/main.py @@ -90,10 +90,9 @@ def main(): (1,7,5,3), # 4 (6,0,2,4) ] # 5 - dtype = np.dtype([ - ('Px', np.float32), - ('Py', np.float32), - ('Pz', np.float32)]) + dtype = [ ('x', np.float32), + ('y', np.float32), + ('z', np.float32) ] topo = osd.Topology(faces) topo.boundaryMode = osd.BoundaryMode.EDGE_ONLY @@ -105,7 +104,7 @@ def main(): subdivider = osd.Subdivider( topo, - vertexLayout = dtype, + vertexLayout = 'f4, f4, f4', indexType = np.uint32, levels = 4) subdivider.setCoarseVertices(verts) diff --git a/python/doc/index.rst b/python/doc/index.rst index c8103458..6678f29d 100644 --- a/python/doc/index.rst +++ b/python/doc/index.rst @@ -10,29 +10,35 @@ The Python module for OpenSubdiv does not provide one-to-one wrapping of the nat We do not yet support rendering or GPU-accelerated subdivision from Python, but a demo is provided that renders a subdivided surface using ``PyOpenGL`` and ``QGLWidget``. The demo uses a "modern" OpenGL context (Core Profile). -These bindings leverage numpy_ arrays for passing data. The numpy library is a de facto standard for encapsulating large swaths of typed data in Python. However, for special attributes (such as sharpness), the OpenSubdiv wrapper exposes Pythonic interfaces, using properties and list accessors. For example:: +These bindings leverage numpy_ arrays for passing data. The numpy library is the de facto standard for encapsulating large swaths of typed data in Python. However, for special attributes (such as sharpness), the OpenSubdiv wrapper exposes Pythonic interfaces, using properties and list accessors. For example:: - import osd - import numpy as np - faceList = np.array([[0,1,2,3],[0,1,6,7]]) - topo = osd.Topology(faceList) - topo.vertices[2].sharpness = 1.3 - topo.faces[0].edges[3].sharpness = 0.6 + import osd + import numpy as np + faceList = np.array([[0,1,2,3],[0,1,6,7]]) + topo = osd.Topology(faceList) + topo.vertices[2].sharpness = 1.3 + topo.faces[0].edges[3].sharpness = 0.6 -After constructing a :class:`osd.Topology` object, clients should finalize it and pass it into a :class:`osd.Subdivider` instance:: +After constructing a :class:`osd.Topology` object, simply finalize it and pass it into a :class:`osd.Subdivider` instance:: topo.finalize() subdivider = osd.Subdivider( - topo, - vertexLayout = [np.float32] * 3, + topology = topo, + vertexLayout = np.dtype('f4, f4, f4'), indexType = np.uint32, levels = 4) The final step is to perform actual refinement. This often occurs inside an animation loop or callback function:: - subdivider.setCage(positions) + subdivider.setCoarseVertices(positions) subdivider.refine() pts = subdivider.getRefinedVertices() + +Only uniform subdivision is supported from Python, which means the topology of the subdivided mesh will never change:: + + indices = subdivider.getRefinedQuads() + +This returns a flat list of indices (four per quad) using the integer type that was specified as the ``indexType`` argument in the constructor. .. _numpy: http://www.numpy.org diff --git a/python/osd/subdivider.py b/python/osd/subdivider.py index 3f7b8c6a..0b9c069d 100644 --- a/python/osd/subdivider.py +++ b/python/osd/subdivider.py @@ -70,7 +70,7 @@ class Subdivider(object): :param topo: Finalized mesh topology. :type topo: :class:`osd.Topology` :param vertexLayout: Describes the data structure composing each vertex. - :type vertexLayout: record-style numpy data type_ object + :type vertexLayout: numpy dtype_ object or short-hand string :param indexType: Integer type for the indices returned from `getRefinedTopology`. :type indexType: single numpy type_ :param levels: Number of subdivisions. @@ -80,11 +80,11 @@ class Subdivider(object): composed of ``numpy.float32``, and ``indexType`` must be ``numpy.uint32``. - .. _types: http://docs.scipy.org/doc/numpy/user/basics.types.html .. _type: http://docs.scipy.org/doc/numpy/user/basics.types.html + .. _dtype: http://docs.scipy.org/doc/numpy/reference/arrays.dtypes.html ''' - def __init__(self, topo, vertexLayout, indexType, levels): + def __init__(self, topology, vertexLayout, indexType, levels): if levels < 2: raise TopoError("Subdivision levels must be 2 or greater") if type(vertexLayout) != numpy.dtype: @@ -92,7 +92,7 @@ class Subdivider(object): self.vertexLayout = vertexLayout self.indexType = indexType self.levels = levels - self.shim = shim.Subdivider(topo.shim, vertexLayout, indexType, levels) + self.shim = shim.Subdivider(topology.shim, vertexLayout, indexType, levels) # Calls UpdateData on the vertexBuffer. def setCoarseVertices(self, coarseVerts, listType = None): diff --git a/python/osd/topology.py b/python/osd/topology.py index 9e2adba6..b885acc8 100644 --- a/python/osd/topology.py +++ b/python/osd/topology.py @@ -76,8 +76,9 @@ class Topology(object): valence, clients can pass in a single integer for ``valences``. - If desired, simple Python lists can be used in lieu of numpy - arrays. + If desired, simple Python lists can be passed in rather than numpy + arrays. However they will get converted into numpy arrays + internally. .. note:: Input data is always copied to internal storage, rather than referenced. From 90c017ebe05413ee72ed815981586ef9572bbbd4 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Wed, 6 Feb 2013 22:05:56 -0800 Subject: [PATCH 5/8] update doc to match enum names --- python/doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/doc/index.rst b/python/doc/index.rst index 6678f29d..8d8ec1fe 100644 --- a/python/doc/index.rst +++ b/python/doc/index.rst @@ -57,7 +57,7 @@ Subdivider Class Enumerations ============== -.. autoclass:: osd.InterpolateBoundary +.. autoclass:: osd.BoundaryMode :members: .. autoclass:: osd.Sharpness From 604c35ac4f5fc2b70067b9bc4e157f93c7cb3576 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Thu, 7 Feb 2013 11:37:00 -0800 Subject: [PATCH 6/8] incorp feedback from mkraemer: remove the license file, move the demo to examples/python, change the min Python version to 2.6 --- CMakeLists.txt | 4 +- {python/demo => examples/python}/README.md | 7 +-- {python/demo => examples/python}/__init__.py | 0 {python/demo => examples/python}/canvas.py | 0 .../demo => examples/python}/interactive.py | 0 {python/demo => examples/python}/main.py | 0 {python/demo => examples/python}/renderer.py | 0 .../demo => examples/python}/screenshot.png | Bin {python/demo => examples/python}/shaders.py | 0 {python/demo => examples/python}/simple.glsl | 0 {python/demo => examples/python}/utility.py | 0 {python/demo => examples/python}/window.py | 0 python/LICENSE | 50 ------------------ python/README.md | 18 +++---- python/setup.py | 30 +---------- 15 files changed, 14 insertions(+), 95 deletions(-) rename {python/demo => examples/python}/README.md (61%) rename {python/demo => examples/python}/__init__.py (100%) rename {python/demo => examples/python}/canvas.py (100%) rename {python/demo => examples/python}/interactive.py (100%) rename {python/demo => examples/python}/main.py (100%) rename {python/demo => examples/python}/renderer.py (100%) rename {python/demo => examples/python}/screenshot.png (100%) rename {python/demo => examples/python}/shaders.py (100%) rename {python/demo => examples/python}/simple.glsl (100%) rename {python/demo => examples/python}/utility.py (100%) rename {python/demo => examples/python}/window.py (100%) delete mode 100644 python/LICENSE diff --git a/CMakeLists.txt b/CMakeLists.txt index 395a4cd3..149a7ecb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -201,8 +201,8 @@ find_package(OpenCL 1.1) find_package(CUDA 4.0) find_package(GLFW 2.7.0) find_package(PTex 2.0) -find_package(PythonInterp 2.7) -find_package(SWIG) +find_package(PythonInterp 2.6) +find_package(SWIG 1.3.40) if (NOT APPLE AND OPENGL_FOUND) find_package(GLEW REQUIRED) diff --git a/python/demo/README.md b/examples/python/README.md similarity index 61% rename from python/demo/README.md rename to examples/python/README.md index 1961fb1e..047939e6 100644 --- a/python/demo/README.md +++ b/examples/python/README.md @@ -1,12 +1,13 @@ -This folder defines a small demo application that uses PyQt and newish version of PyOpenGL. +This folder defines a small demo application that required PyQt, PyOpenGL, and the Python bindings for OpenSubdiv (which in turn require numpy and SWIG). ![Screenshot](http://raw.github.com/prideout/OpenSubdiv/master/python/demo/screenshot.png) -- **main.py** All calls to the OSD wrapper go here. This creates a `QApplication` and periodically pushes new VBO data into the renderer. (see below) -- **renderer.py** Defines the renderer; implements `draw` and `init`. All OpenGL calls are made in this file, and there's no dependency on Qt or OSD. +- **main.py** This what you invoke from the command line. All calls to the `osd` module go here. Creates a `QApplication` and periodically pushes new VBO data into the renderer. (see below) +- **renderer.py** Defines the renderer; implements `draw` and `init`. All OpenGL calls are made in this file, and there's no dependency on Qt or `osd`. - **canvas.py** Inherits from `QGLWidget` and calls out to the renderer object (see above) - **shaders.py** Implements a miniature FX format by extracting named strings from a file and pasting them together - **simple.glsl** Specifies the GLSL shaders for the demo using the miniature FX format - **utility.py** Some linear algebra stuff to make it easier to use Modern OpenGL - **window.py** Inherits from `QMainWindow`, instances a canvas object +- **interactive.py** Invoke this from the command line to spawn an alternative demo that has an interactive prompt. - **\_\_init\_\_.py** Exports `main` into the package namespace to make it easy to run the demo from `setup.py` diff --git a/python/demo/__init__.py b/examples/python/__init__.py similarity index 100% rename from python/demo/__init__.py rename to examples/python/__init__.py diff --git a/python/demo/canvas.py b/examples/python/canvas.py similarity index 100% rename from python/demo/canvas.py rename to examples/python/canvas.py diff --git a/python/demo/interactive.py b/examples/python/interactive.py similarity index 100% rename from python/demo/interactive.py rename to examples/python/interactive.py diff --git a/python/demo/main.py b/examples/python/main.py similarity index 100% rename from python/demo/main.py rename to examples/python/main.py diff --git a/python/demo/renderer.py b/examples/python/renderer.py similarity index 100% rename from python/demo/renderer.py rename to examples/python/renderer.py diff --git a/python/demo/screenshot.png b/examples/python/screenshot.png similarity index 100% rename from python/demo/screenshot.png rename to examples/python/screenshot.png diff --git a/python/demo/shaders.py b/examples/python/shaders.py similarity index 100% rename from python/demo/shaders.py rename to examples/python/shaders.py diff --git a/python/demo/simple.glsl b/examples/python/simple.glsl similarity index 100% rename from python/demo/simple.glsl rename to examples/python/simple.glsl diff --git a/python/demo/utility.py b/examples/python/utility.py similarity index 100% rename from python/demo/utility.py rename to examples/python/utility.py diff --git a/python/demo/window.py b/examples/python/window.py similarity index 100% rename from python/demo/window.py rename to examples/python/window.py diff --git a/python/LICENSE b/python/LICENSE deleted file mode 100644 index 8234fc4a..00000000 --- a/python/LICENSE +++ /dev/null @@ -1,50 +0,0 @@ -Microsoft Public License (Ms-PL) - -This license governs use of the accompanying software. If you use the -software, you accept this license. If you do not accept the license, -do not use the software. - -Definitions - -The terms "reproduce," "reproduction," "derivative works," and -"distribution" have the same meaning here as under U.S. copyright law. -A "contribution" is the original software, or any additions or changes -to the software. A "contributor" is any person that distributes its -contribution under this license. "Licensed patents" are a -contributor's patent claims that read directly on its contribution. - -Grant of Rights - -(A) Copyright Grant- Subject to the terms of this license, including -the license conditions and limitations in section 3, each contributor -grants you a non-exclusive, worldwide, royalty-free copyright license -to reproduce its contribution, prepare derivative works of its -contribution, and distribute its contribution or any derivative works -that you create. (B) Patent Grant- Subject to the terms of this -license, including the license conditions and limitations in section -3, each contributor grants you a non-exclusive, worldwide, -royalty-free license under its licensed patents to make, have made, -use, sell, offer for sale, import, and/or otherwise dispose of its -contribution in the software or derivative works of the contribution -in the software. - -Conditions and Limitations - -(A) No Trademark License- This license does not grant you rights to -use any contributors' name, logo, or trademarks. (B) If you bring a -patent claim against any contributor over patents that you claim are -infringed by the software, your patent license from such contributor -to the software ends automatically. (C) If you distribute any portion -of the software, you must retain all copyright, patent, trademark, and -attribution notices that are present in the software. (D) If you -distribute any portion of the software in source code form, you may do -so only under this license by including a complete copy of this -license with your distribution. If you distribute any portion of the -software in compiled or object code form, you may only do so under a -license that complies with this license. (E) The software is licensed -"as-is." You bear the risk of using it. The contributors give no -express warranties, guarantees, or conditions. You may have additional -consumer rights under your local laws which this license cannot -change. To the extent permitted under your local laws, the -contributors exclude the implied warranties of merchantability, -fitness for a particular purpose and non-infringement. diff --git a/python/README.md b/python/README.md index 7d8dd22d..db546f26 100644 --- a/python/README.md +++ b/python/README.md @@ -5,11 +5,15 @@ Make sure you install SWIG and numpy before you begin. CMake builds the extension like this: - ./setup.py build --osddir='../build/lib' --build-platlib='../build/python' + ./setup.py build --osddir='../build/lib' \ + --build-platlib='../build/python' \ + --build-temp='../build/temp' If you invoke this manually, you'll need to replace `../build/lib` with the folder that has `libosdCPU.a`. -Next, try out the unit tests: +The demo that uses PyQt and PyOpenGL can be found in `../examples/python`. + +You can run some unit tests like so: ./setup.py test @@ -17,15 +21,7 @@ You can clean, build, and test in one go like this: ./setup.py clean --all build test -If you'd like, you can try out an OpenGL demo. For this, you need to have PyQt and PyOpenGL installed. - - ./setup.py install --user demo - -You can also install the module globally with: - - sudo ./setup.py install - -After installing the module, you can generate and view the Sphinx-generated documentation like so: +You can generate and view the Sphinx-generated documentation like so: ./setup.py doc open ./doc/_build/html/index.html diff --git a/python/setup.py b/python/setup.py index c926ff04..d97ce0cc 100755 --- a/python/setup.py +++ b/python/setup.py @@ -112,32 +112,6 @@ class TestCommand(Command): suite = unittest.defaultTestLoader.loadTestsFromModule(test) unittest.TextTestRunner(verbosity=2).run(suite) -class DemoCommand(Command): - description = "runs a little PyQt demo of the Python wrapper" - user_options = [] - def initialize_options(self): - pass - def finalize_options(self): - pass - def run(self): - importBuildFolder() - import demo - os.chdir('demo') - demo.main() - -class InteractiveCommand(Command): - description = "runs a little PyQt demo of the Python wrapper" - user_options = [] - def initialize_options(self): - pass - def finalize_options(self): - pass - def run(self): - importBuildFolder() - import demo - os.chdir('demo') - demo.interactive() - class DocCommand(Command): description = "Generate HTML documentation with Sphinx" user_options = [] @@ -173,9 +147,7 @@ setup(name = "OpenSubdiv", cmdclass = { 'build': BuildCommand, 'test': TestCommand, - 'doc': DocCommand, - 'interactive': InteractiveCommand, - 'demo': DemoCommand}, + 'doc': DocCommand}, include_dirs = [np_include_dir], ext_modules = [osd_shim], description = 'Python Bindings to the Pixar Subdivision Library') From ad040466a0f875740f7753f485276791ef399e4c Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Thu, 7 Feb 2013 11:41:48 -0800 Subject: [PATCH 7/8] fix typos in README --- examples/python/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/python/README.md b/examples/python/README.md index 047939e6..8dd672bf 100644 --- a/examples/python/README.md +++ b/examples/python/README.md @@ -1,6 +1,6 @@ -This folder defines a small demo application that required PyQt, PyOpenGL, and the Python bindings for OpenSubdiv (which in turn require numpy and SWIG). +This folder defines a small demo application that requires PyQt, PyOpenGL, and the Python bindings for OpenSubdiv (which in turn require numpy and SWIG). -![Screenshot](http://raw.github.com/prideout/OpenSubdiv/master/python/demo/screenshot.png) +![Screenshot](http://raw.github.com/PixarAnimationStudios/OpenSubdiv/master/examples/python/screenshot.png) - **main.py** This what you invoke from the command line. All calls to the `osd` module go here. Creates a `QApplication` and periodically pushes new VBO data into the renderer. (see below) - **renderer.py** Defines the renderer; implements `draw` and `init`. All OpenGL calls are made in this file, and there's no dependency on Qt or `osd`. From 67398c3b3c00fde81c6975278df5f03da58d16c1 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Thu, 7 Feb 2013 16:18:14 -0800 Subject: [PATCH 8/8] augmenting gitignore for pyc and SWIG-generated cruft --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index d6da35cb..d97e2e53 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ # ignore stringified kernels *.inc *.inc.rule + +*.pyc +osdshim_wrap.cpp +shim.py \ No newline at end of file