mirror of
https://github.com/PixarAnimationStudios/OpenSubdiv
synced 2024-12-22 16:00:07 +00:00
Adding first cut of Python bindings.
This commit is contained in:
parent
95c84b8f56
commit
34671a27f0
50
python/LICENSE
Normal file
50
python/LICENSE
Normal file
@ -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.
|
46
python/README.md
Normal file
46
python/README.md
Normal file
@ -0,0 +1,46 @@
|
||||
# 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, open **setup.py** and make sure that the `osd_lib_path` variable points to a folder that has `libosdCPU.a` et al.
|
||||
|
||||
Next, try building the extension with:
|
||||
|
||||
./setup.py build
|
||||
|
||||
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
|
||||
|
||||
- Fix and enable the `do_not_test_leaks` unit test
|
||||
- The C++ portion of the shim defines a bunch of badly-named free functions and two badly-named custom types: "OpaqueHbrMesh" and "CSubd".
|
||||
- Rename everything and methodize the free functions.
|
||||
- Factor the Python-specific bits out of these types, so they be used on their own as a pure C++ utility layer.
|
||||
- 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
|
||||
- Remove all the caveats that are listed in the Sphinx docs :)
|
12
python/demo/README.md
Normal file
12
python/demo/README.md
Normal file
@ -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)
|
||||
- **demo.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`
|
1
python/demo/__init__.py
Normal file
1
python/demo/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from main import main
|
40
python/demo/canvas.py
Normal file
40
python/demo/canvas.py
Normal file
@ -0,0 +1,40 @@
|
||||
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()
|
97
python/demo/demo.py
Normal file
97
python/demo/demo.py
Normal file
@ -0,0 +1,97 @@
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
from OpenGL.GL import *
|
||||
from shaders import *
|
||||
from utility import *
|
||||
from canvas import *
|
||||
|
||||
class Demo:
|
||||
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
|
||||
eye = V3(.1, -0.1, 10)
|
||||
target = V3(.1, -0.1, 0)
|
||||
up = V3(0, 1, 0)
|
||||
view = look_at(eye, target, up)
|
||||
|
||||
model = np.identity(4, 'f')
|
||||
model = rotation(-1.1, [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)
|
90
python/demo/main.py
Executable file
90
python/demo/main.py
Executable file
@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from PyQt4 import QtGui, QtCore
|
||||
from window import Window
|
||||
from demo import Demo
|
||||
import sys
|
||||
import numpy as np
|
||||
import osd
|
||||
|
||||
def main():
|
||||
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
demo = Demo()
|
||||
win = Window(demo)
|
||||
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 = [('Px', np.float32),
|
||||
('Py', np.float32),
|
||||
('Pz', np.float32)]
|
||||
|
||||
topo = osd.Topology(faces)
|
||||
topo.boundaryInterpolation = osd.InterpolateBoundary.EDGE_ONLY
|
||||
topo.finalize()
|
||||
|
||||
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
|
||||
|
||||
subdivider = osd.Subdivider(
|
||||
topo,
|
||||
vertexLayout = dtype,
|
||||
indexType = np.uint32,
|
||||
levels = 5)
|
||||
|
||||
inds = subdivider.getRefinedTopology()
|
||||
demo.updateIndicesVbo(inds)
|
||||
|
||||
def animateVerts():
|
||||
from time import time
|
||||
import math
|
||||
t = 4 * time()
|
||||
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.setCage(verts)
|
||||
subdivider.refine()
|
||||
pts = subdivider.getRefinedVertices()
|
||||
demo.updatePointsVbo(pts)
|
||||
|
||||
updateAnimation()
|
||||
demo.drawHook = updateAnimation
|
||||
|
||||
retcode = app.exec_()
|
||||
sys.exit(retcode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
BIN
python/demo/screenshot.png
Normal file
BIN
python/demo/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
92
python/demo/shaders.py
Normal file
92
python/demo/shaders.py
Normal file
@ -0,0 +1,92 @@
|
||||
from OpenGL.GL import *
|
||||
|
||||
ProgramFiles = ['demo/simple.glsl']
|
||||
|
||||
Programs = {
|
||||
"BareBones" : {
|
||||
"GL_VERTEX_SHADER" : "simple.xforms simple.vert",
|
||||
"GL_GEOMETRY_SHADER" : "simple.xforms simple.geom",
|
||||
"GL_FRAGMENT_SHADER" : "simple.xforms simple.frag"
|
||||
}
|
||||
}
|
||||
|
||||
class Attribs:
|
||||
POSITION = 0
|
||||
NORMAL = 1
|
||||
|
||||
def load_shaders(
|
||||
glslFiles = ProgramFiles,
|
||||
attribs = Attribs,
|
||||
programMap = Programs):
|
||||
"""Parse a series of simple text files, each of which is composed
|
||||
series of shader snippets. The given 'attribs' class defines an
|
||||
enumeration of attribute slots to bind (aka semantics).
|
||||
|
||||
In each text file, lines starting with two dash characters start a new
|
||||
shader snippet with the given name. For example:
|
||||
-- S1
|
||||
uniform float foo;
|
||||
-- Prefix
|
||||
#version 150
|
||||
-- S2
|
||||
uniform float bar;
|
||||
"""
|
||||
|
||||
import os.path
|
||||
|
||||
# First parse the file to populate 'snippetMap'
|
||||
class ParserState:
|
||||
HEADER = 0
|
||||
SOURCE = 1
|
||||
parserState = ParserState.HEADER
|
||||
snippetMap = {}
|
||||
for glslFile in glslFiles:
|
||||
basename = os.path.basename(glslFile)
|
||||
snippetPrefix = os.path.splitext(basename)[0] + '.'
|
||||
for line in open(glslFile):
|
||||
if line.startswith('--'):
|
||||
if parserState is ParserState.HEADER:
|
||||
parserState = ParserState.SOURCE
|
||||
elif len(source):
|
||||
snippetMap[snippetName] = ''.join(source)
|
||||
snippetName = snippetPrefix + line[2:].strip()
|
||||
source = []
|
||||
continue
|
||||
if parserState is ParserState.HEADER:
|
||||
pass
|
||||
else:
|
||||
source.append(line)
|
||||
if len(source):
|
||||
snippetMap[snippetName] = ''.join(source)
|
||||
|
||||
# Now, glue together the strings and feed them to OpenGL
|
||||
stagePrefix = "#version 150\n"
|
||||
programs = {}
|
||||
for key, val in programMap.items():
|
||||
programHandle = glCreateProgram()
|
||||
for stageName, snippetList in val.items():
|
||||
snippets = map(snippetMap.get, snippetList.split())
|
||||
stageSource = stagePrefix + ''.join(snippets)
|
||||
stage = getattr(OpenGL.GL, stageName)
|
||||
sHandle = glCreateShader(stage)
|
||||
glShaderSource(sHandle, stageSource)
|
||||
glCompileShader(sHandle)
|
||||
success = glGetShaderiv(sHandle, GL_COMPILE_STATUS)
|
||||
if not success:
|
||||
print 'Error in', stageName, snippetList
|
||||
infolog = glGetShaderInfoLog(sHandle)
|
||||
raise SystemExit(infolog)
|
||||
glAttachShader(programHandle, sHandle)
|
||||
for attrib in dir(attribs):
|
||||
if attrib.startswith('__'):
|
||||
continue
|
||||
slot = getattr(attribs, attrib)
|
||||
name = attrib[0] + attrib[1:].lower()
|
||||
glBindAttribLocation(programHandle, slot, name)
|
||||
glLinkProgram(programHandle)
|
||||
success = glGetProgramiv(programHandle, GL_LINK_STATUS);
|
||||
if not success:
|
||||
infolog = glGetProgramInfoLog(programHandle)
|
||||
raise SystemExit(infolog)
|
||||
programs[key] = programHandle
|
||||
return programs
|
86
python/demo/simple.glsl
Normal file
86
python/demo/simple.glsl
Normal file
@ -0,0 +1,86 @@
|
||||
|
||||
-- xforms
|
||||
|
||||
uniform mat4 Projection;
|
||||
uniform mat4 Modelview;
|
||||
uniform mat3 NormalMatrix;
|
||||
|
||||
-- vert
|
||||
|
||||
in vec4 Position;
|
||||
in vec3 Normal;
|
||||
out vec4 vPosition;
|
||||
out vec3 vNormal;
|
||||
void main()
|
||||
{
|
||||
vPosition = Position;
|
||||
vNormal = Normal;
|
||||
gl_Position = Projection * Modelview * Position;
|
||||
}
|
||||
|
||||
-- geom
|
||||
|
||||
in vec4 vPosition[4];
|
||||
in vec3 vNormal[4];
|
||||
out vec3 gNormal;
|
||||
const bool facets = true;
|
||||
|
||||
layout(lines_adjacency) in;
|
||||
layout(triangle_strip, max_vertices = 4) out;
|
||||
|
||||
void emit(int index)
|
||||
{
|
||||
if (!facets) {
|
||||
gNormal = NormalMatrix * vNormal[index];
|
||||
}
|
||||
gl_Position = Projection * Modelview * vPosition[index];
|
||||
EmitVertex();
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
if (facets) {
|
||||
vec3 A = vPosition[0].xyz;
|
||||
vec3 B = vPosition[1].xyz;
|
||||
vec3 C = vPosition[2].xyz;
|
||||
gNormal = NormalMatrix * normalize(cross(B - A, C - A));
|
||||
}
|
||||
|
||||
emit(0);
|
||||
emit(1);
|
||||
emit(3);
|
||||
emit(2);
|
||||
EndPrimitive();
|
||||
}
|
||||
|
||||
-- frag
|
||||
|
||||
in vec3 gNormal;
|
||||
out vec4 FragColor;
|
||||
|
||||
uniform vec4 LightPosition = vec4(0.75, -0.25, 1, 1);
|
||||
uniform vec3 AmbientMaterial = vec3(0.2, 0.1, 0.1);
|
||||
uniform vec4 DiffuseMaterial = vec4(1.0, 209.0/255.0, 54.0/255.0, 1.0);
|
||||
uniform vec3 SpecularMaterial = vec3(0.4, 0.4, 0.3);
|
||||
uniform float Shininess = 200.0;
|
||||
uniform float Fresnel = 0.1;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec3 N = normalize(gNormal);
|
||||
vec3 L = normalize((LightPosition).xyz);
|
||||
|
||||
vec3 Eye = vec3(0, 0, 1);
|
||||
vec3 H = normalize(L + Eye);
|
||||
|
||||
float df = max(0.0, dot(N, L));
|
||||
float sf = pow(max(0.0, dot(N, H)), Shininess);
|
||||
float rfTheta = Fresnel + (1-Fresnel) * pow(1-dot(N,Eye), 5);
|
||||
|
||||
vec3 color = AmbientMaterial +
|
||||
df * DiffuseMaterial.rgb +
|
||||
sf * SpecularMaterial +
|
||||
rfTheta;
|
||||
|
||||
FragColor = vec4(color, DiffuseMaterial.a);
|
||||
}
|
74
python/demo/utility.py
Normal file
74
python/demo/utility.py
Normal file
@ -0,0 +1,74 @@
|
||||
from OpenGL.GL import *
|
||||
from time import time
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
from numpy import linalg as LA
|
||||
|
||||
# Provide a terse way to get a uniform location from its name
|
||||
def U(name):
|
||||
p = glGetIntegerv(GL_CURRENT_PROGRAM)
|
||||
return glGetUniformLocation(p, name)
|
||||
|
||||
# Provide a terse way to create a f32 numpy 3-tuple
|
||||
def V3(x, y, z):
|
||||
return np.array([x, y, z], 'f')
|
||||
|
||||
def translation(direction):
|
||||
M = np.identity(4, 'f')
|
||||
M[:3, 3] = direction[:3]
|
||||
return M
|
||||
|
||||
def unit_vector(data, axis=None, out=None):
|
||||
if out is None:
|
||||
data = np.array(data, dtype=np.float32, copy=True)
|
||||
if data.ndim == 1:
|
||||
data /= math.sqrt(np.dot(data, data))
|
||||
return data
|
||||
else:
|
||||
if out is not data:
|
||||
out[:] = np.array(data, copy=False)
|
||||
data = out
|
||||
length = np.atleast_1d(np.sum(data*data, axis))
|
||||
np.sqrt(length, length)
|
||||
if axis is not None:
|
||||
length = np.expand_dims(length, axis)
|
||||
data /= length
|
||||
if out is None:
|
||||
return data
|
||||
|
||||
def rotation(angle, direction):
|
||||
sina = math.sin(angle)
|
||||
cosa = math.cos(angle)
|
||||
direction = unit_vector(direction[:3])
|
||||
R = np.diag([cosa, cosa, cosa])
|
||||
R += np.outer(direction, direction) * (1.0 - cosa)
|
||||
direction *= sina
|
||||
R += np.array([[ 0.0, -direction[2], direction[1]],
|
||||
[ direction[2], 0.0, -direction[0]],
|
||||
[-direction[1], direction[0], 0.0]])
|
||||
M = np.identity(4, 'f')
|
||||
M[:3, :3] = R
|
||||
return M
|
||||
|
||||
def look_at(eye, target, up):
|
||||
F = target[:3] - eye[:3]
|
||||
f = F / LA.norm(F)
|
||||
U = up / LA.norm(up)
|
||||
s = np.cross(f, U)
|
||||
u = np.cross(s, f)
|
||||
M = np.matrix(np.identity(4))
|
||||
M[:3,:3] = [s,u,-f]
|
||||
T = translation(-eye)
|
||||
return np.matrix(M * T, 'f')
|
||||
|
||||
def perspective(fovy, aspect, f, n):
|
||||
s = 1.0/math.tan(math.radians(fovy)/2.0)
|
||||
sx, sy = s / aspect, s
|
||||
zz = (f+n)/(n-f)
|
||||
zw = 2*f*n/(n-f)
|
||||
m = np.matrix([[sx,0,0,0],
|
||||
[0,sy,0,0],
|
||||
[0,0,zz,zw],
|
||||
[0,0,-1,0]], 'f')
|
||||
return m
|
15
python/demo/window.py
Normal file
15
python/demo/window.py
Normal file
@ -0,0 +1,15 @@
|
||||
from PyQt4 import QtGui, QtCore
|
||||
from canvas import Canvas
|
||||
|
||||
class Window(QtGui.QMainWindow):
|
||||
def __init__(self, client):
|
||||
super(Window, self).__init__()
|
||||
self.canvas = Canvas(self, client)
|
||||
self.setCentralWidget(self.canvas)
|
||||
self.setGeometry(300, 300, 250, 150)
|
||||
self.setWindowTitle('PyQt')
|
||||
self.show()
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
if e.key() == QtCore.Qt.Key_Escape:
|
||||
self.close()
|
153
python/doc/Makefile
Normal file
153
python/doc/Makefile
Normal file
@ -0,0 +1,153 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> 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."
|
244
python/doc/conf.py
Normal file
244
python/doc/conf.py
Normal file
@ -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
|
||||
# "<project> v<release> 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 <link> 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'
|
68
python/doc/index.rst
Normal file
68
python/doc/index.rst
Normal file
@ -0,0 +1,68 @@
|
||||
.. 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
|
||||
=======
|
||||
|
||||
- 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
|
6
python/osd/__init__.py
Normal file
6
python/osd/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from osd import *
|
||||
from topology import Topology
|
||||
from subdivider import Subdivider
|
||||
import utility
|
||||
|
||||
|
92
python/osd/adapters.py
Normal file
92
python/osd/adapters.py
Normal file
@ -0,0 +1,92 @@
|
||||
# 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.
|
||||
|
||||
import shim
|
||||
|
||||
class VertexListAdapter(object):
|
||||
def __init__(self, topo):
|
||||
self.topo = topo
|
||||
def __getitem__(self, index):
|
||||
return _VertexAdapter(self.topo, index)
|
||||
def __len__(self):
|
||||
return self.topo._maxIndex + 1
|
||||
|
||||
class _VertexAdapter(object):
|
||||
def __init__(self, topo, index):
|
||||
self.mesh = topo._hbr_mesh
|
||||
self.index = index
|
||||
@property
|
||||
def sharpness(self):
|
||||
return shim.hbr_get_vertex_sharpness(
|
||||
self.mesh,
|
||||
self.index)
|
||||
@sharpness.setter
|
||||
def sharpness(self, value):
|
||||
shim.hbr_set_vertex_sharpness(
|
||||
self.mesh,
|
||||
self.index,
|
||||
value)
|
||||
|
||||
class FaceListAdapter(object):
|
||||
def __init__(self, topo):
|
||||
self.mesh = topo._hbr_mesh
|
||||
shim.hbr_update_faces(self.mesh)
|
||||
self.length = shim.hbr_get_num_faces(
|
||||
self.mesh)
|
||||
def __getitem__(self, index):
|
||||
return _FaceAdapter(self.mesh, index)
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
class _FaceAdapter(object):
|
||||
def __init__(self, mesh, faceIndex):
|
||||
self.mesh = mesh
|
||||
self.faceIndex = faceIndex
|
||||
self._edgeListAdapter = _EdgeListAdapter(mesh, faceIndex)
|
||||
@property
|
||||
def hole(self):
|
||||
return shim.hbr_get_face_hole(
|
||||
self.mesh,
|
||||
self.faceIndex)
|
||||
@hole.setter
|
||||
def hole(self, value):
|
||||
shim.hbr_set_face_hole(
|
||||
self.mesh,
|
||||
self.faceIndex,
|
||||
value)
|
||||
@property
|
||||
def edges(self):
|
||||
return self._edgeListAdapter
|
||||
|
||||
class _EdgeListAdapter(object):
|
||||
def __init__(self, mesh, faceIndex):
|
||||
self.mesh = mesh
|
||||
self.faceIndex = faceIndex
|
||||
self.length = shim.hbr_get_num_edges(
|
||||
mesh,
|
||||
faceIndex)
|
||||
def __getitem__(self, edgeIndex):
|
||||
return _EdgeAdapter(self.mesh, self.faceIndex, edgeIndex)
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
class _EdgeAdapter(object):
|
||||
def __init__(self, mesh, faceIndex, edgeIndex):
|
||||
self.mesh = mesh
|
||||
self.faceIndex = faceIndex
|
||||
self.edgeIndex = edgeIndex
|
||||
@property
|
||||
def sharpness(self):
|
||||
return shim.hbr_get_edge_sharpness(
|
||||
self.mesh,
|
||||
self.faceIndex,
|
||||
self.edgeIndex)
|
||||
@sharpness.setter
|
||||
def sharpness(self, value):
|
||||
shim.hbr_set_edge_sharpness(
|
||||
self.mesh,
|
||||
self.faceIndex,
|
||||
self.edgeIndex,
|
||||
value)
|
37
python/osd/osd.py
Normal file
37
python/osd/osd.py
Normal file
@ -0,0 +1,37 @@
|
||||
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 InterpolateBoundary:
|
||||
"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."
|
385
python/osd/shim.cpp
Normal file
385
python/osd/shim.cpp
Normal file
@ -0,0 +1,385 @@
|
||||
|
||||
// These functions do not need to be incredibly user friendly (in
|
||||
// terms of error checking) because the client is the Python portion
|
||||
// of the osd wrapper, rather than the end user. The C++ side of the
|
||||
// shim is quite verbose so let's try to keep most of the sanity
|
||||
// checking on the Python side of things.
|
||||
//
|
||||
// Useful references:
|
||||
// http://docs.scipy.org/doc/numpy/user/c-info.how-to-extend.html
|
||||
// http://dsnra.jpl.nasa.gov/software/Python/numpydoc/numpy-13.html
|
||||
|
||||
#include <Python.h>
|
||||
#include <numpy/arrayobject.h>
|
||||
#include <osd/cpuComputeController.h>
|
||||
#include "shim_types.h"
|
||||
#include "shim_adapters.cpp"
|
||||
|
||||
// These flags are useful for disabling the core OpenSubdiv library
|
||||
// to help determine where problems (such as leaks) occur.
|
||||
static const bool HBR_STUBBED = false;
|
||||
static const bool FAR_STUBBED = false;
|
||||
|
||||
OpenSubdiv::OsdCpuComputeController * g_osdComputeController = 0;
|
||||
|
||||
// - args is a list with 1 element, which is a Topology object
|
||||
// - returns an opaque handle to a HbrMesh (shim.OpaqueHbrMesh)
|
||||
static PyObject *
|
||||
HbrNew(PyObject *self, PyObject *args)
|
||||
{
|
||||
Py_ssize_t n = PyTuple_Size(args);
|
||||
if (n != 1) {
|
||||
PyErr_SetString(PyExc_TypeError, "hbr_new requires a single argument.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* topology = PyTuple_GetItem(args, 0);
|
||||
PyObject* boundaryInterp =
|
||||
PyObject_GetAttrString(topology, "boundaryInterpolation");
|
||||
PyObject* indices = PyObject_GetAttrString(topology, "indices");
|
||||
PyObject* valences = PyObject_GetAttrString(topology, "valences");
|
||||
if (!boundaryInterp || !indices || !valences) {
|
||||
PyErr_SetString(PyExc_TypeError, "hbr_new requires a Topology object.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static OpenSubdiv::HbrCatmarkSubdivision<OpenSubdiv::OsdVertex> _catmark;
|
||||
OsdHbrMesh* hmesh = 0;
|
||||
|
||||
if (!HBR_STUBBED) {
|
||||
hmesh = new OsdHbrMesh(&_catmark);
|
||||
|
||||
// TODO - allow input indices to be unsigned and/or 16 bits each
|
||||
|
||||
long vertexCount = 1 + PyInt_AsLong(
|
||||
PyObject_GetAttrString(topology, "_maxIndex"));
|
||||
|
||||
OpenSubdiv::OsdVertex vert;
|
||||
for (long i = 0; i < vertexCount; ++i) {
|
||||
OpenSubdiv::HbrVertex<OpenSubdiv::OsdVertex>* pVert =
|
||||
hmesh->NewVertex((int) i, vert);
|
||||
if (!pVert) {
|
||||
printf("Error: Unable to create vertex %ld\n", i);
|
||||
}
|
||||
}
|
||||
|
||||
int* pIndices = (int*) PyArray_DATA(indices);
|
||||
unsigned char* pValence = (unsigned char*) PyArray_DATA(valences);
|
||||
Py_ssize_t valenceCount = PySequence_Length(valences);
|
||||
while (valenceCount--) {
|
||||
int vertsPerFace = *pValence;
|
||||
OpenSubdiv::HbrFace<OpenSubdiv::OsdVertex>* pFace =
|
||||
hmesh->NewFace(vertsPerFace, pIndices, 0);
|
||||
if (!pFace) {
|
||||
printf("Error: Unable to create face (valence = %d)\n",
|
||||
vertsPerFace);
|
||||
}
|
||||
pIndices += vertsPerFace;
|
||||
++pValence;
|
||||
}
|
||||
|
||||
OsdHbrMesh::InterpolateBoundaryMethod bm =
|
||||
(OsdHbrMesh::InterpolateBoundaryMethod) PyInt_AsLong(boundaryInterp);
|
||||
hmesh->SetInterpolateBoundaryMethod(bm);
|
||||
}
|
||||
|
||||
Py_DECREF(boundaryInterp);
|
||||
Py_DECREF(indices);
|
||||
Py_DECREF(valences);
|
||||
|
||||
OpaqueHbrMesh *retval = PyObject_New(OpaqueHbrMesh,
|
||||
&OpaqueHbrMesh_Type);
|
||||
retval->hmesh = hmesh;
|
||||
retval->faces = new std::vector<OsdHbrFace*>();
|
||||
Py_INCREF(retval);
|
||||
return (PyObject *) retval;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
HbrFinish(PyObject *self, PyObject *args)
|
||||
{
|
||||
OpaqueHbrMesh* hmesh = (OpaqueHbrMesh*) PyTuple_GetItem(args, 0);
|
||||
hmesh->hmesh->Finish();
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
// - args is a list with 1 element, which is an opaque HbrMesh handle
|
||||
// - returns None
|
||||
static PyObject *
|
||||
HbrDelete(PyObject *self, PyObject *args)
|
||||
{
|
||||
OpaqueHbrMesh* hmesh = (OpaqueHbrMesh*) PyTuple_GetItem(args, 0);
|
||||
delete hmesh->hmesh;
|
||||
delete hmesh->faces;
|
||||
OpaqueHbrMesh_dealloc(hmesh);
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
// - args is a list with 2 objects (Subdivider, Topology)
|
||||
// - returns an opaque handle to a FarMesh (shim.CSubd)
|
||||
static PyObject *
|
||||
CSubdNew(PyObject *self, PyObject *args)
|
||||
{
|
||||
Py_ssize_t n = PyTuple_Size(args);
|
||||
if (n != 2) {
|
||||
PyErr_SetString(PyExc_TypeError, "csubd_new requires two arguments.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* subdivider = PyTuple_GetItem(args, 0);
|
||||
PyObject* topology = PyTuple_GetItem(args, 1);
|
||||
|
||||
OsdHbrMesh* hmesh;
|
||||
{
|
||||
OpaqueHbrMesh* handle;
|
||||
handle = (OpaqueHbrMesh*) PyObject_GetAttrString(topology, "_hbr_mesh");
|
||||
if (!handle || (!handle->hmesh && !HBR_STUBBED && !FAR_STUBBED)) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"csubd_new requires a finalized topology object.");
|
||||
return NULL;
|
||||
}
|
||||
hmesh = handle->hmesh;
|
||||
Py_DECREF(handle);
|
||||
}
|
||||
|
||||
int level;
|
||||
{
|
||||
PyObject* levelObject = PyObject_GetAttrString(subdivider, "level");
|
||||
level = (int) PyInt_AsLong(levelObject);
|
||||
Py_DECREF(levelObject);
|
||||
}
|
||||
|
||||
int numFloatsPerVertex = 0;
|
||||
{
|
||||
PyObject* vertexLayout = PyObject_GetAttrString(
|
||||
subdivider, "vertexLayout");
|
||||
|
||||
PyObject* seq = PySequence_Fast(vertexLayout, "expected a sequence");
|
||||
Py_ssize_t len = PySequence_Size(seq);
|
||||
for (Py_ssize_t i = 0; i < len; i++) {
|
||||
PyObject* item = PySequence_Fast_GET_ITEM(seq, i);
|
||||
item = PyTuple_GetItem(item, 1);
|
||||
if (!PyType_Check(item)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Bad vertex layout.");
|
||||
return NULL;
|
||||
}
|
||||
std::string typeName = ((PyTypeObject*) item)->tp_name;
|
||||
if (typeName == "numpy.float32") {
|
||||
++numFloatsPerVertex;
|
||||
} else {
|
||||
PyErr_SetString(
|
||||
PyExc_TypeError,
|
||||
"Types other than numpy.float32 are not yet supported.");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
Py_DECREF(seq);
|
||||
Py_DECREF(vertexLayout);
|
||||
}
|
||||
|
||||
OpenSubdiv::FarMesh<OpenSubdiv::OsdVertex>* farMesh = 0;
|
||||
OpenSubdiv::OsdCpuComputeContext* computeContext = 0;
|
||||
OpenSubdiv::OsdCpuVertexBuffer* vertexBuffer = 0;
|
||||
if (!FAR_STUBBED) {
|
||||
OpenSubdiv::FarMeshFactory<OpenSubdiv::OsdVertex> meshFactory(
|
||||
hmesh,
|
||||
level);
|
||||
farMesh = meshFactory.Create();
|
||||
computeContext = OpenSubdiv::OsdCpuComputeContext::Create(farMesh);
|
||||
vertexBuffer = OpenSubdiv::OsdCpuVertexBuffer::Create(
|
||||
numFloatsPerVertex, farMesh->GetNumVertices());
|
||||
}
|
||||
|
||||
CSubd *retval = PyObject_New(CSubd, &CSubd_Type);
|
||||
retval->farMesh = farMesh;
|
||||
retval->computeContext = computeContext;
|
||||
retval->vertexBuffer = vertexBuffer;
|
||||
Py_INCREF(retval);
|
||||
return (PyObject *) retval;
|
||||
}
|
||||
|
||||
// - args is a list with 1 element
|
||||
// - returns None
|
||||
static PyObject *
|
||||
CSubdDelete(PyObject *self, PyObject *args)
|
||||
{
|
||||
CSubd* csubd = (CSubd*) PyTuple_GetItem(args, 0);
|
||||
delete csubd->computeContext;
|
||||
delete csubd->farMesh;
|
||||
delete csubd->vertexBuffer;
|
||||
CSubd_dealloc(csubd);
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
// - args is a list with 2 objects (CSubd and numpy array)
|
||||
// - calls UpdateData on the vertexBuffer.
|
||||
static PyObject *
|
||||
CSubdUpdate(PyObject *self, PyObject *args)
|
||||
{
|
||||
PyObject* csubdObject = PyTuple_GetItem(args, 0);
|
||||
PyObject* arrayObject = PyTuple_GetItem(args, 1);
|
||||
|
||||
if (!CSubd_Check(csubdObject) || !PyArray_Check(arrayObject)) {
|
||||
PyErr_SetString(PyExc_TypeError, "csubd_update");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CSubd* csubd = (CSubd*) csubdObject;
|
||||
PyArrayObject* coarseVerts = (PyArrayObject*) arrayObject;
|
||||
|
||||
float* pFloats = (float*) PyArray_DATA(coarseVerts);
|
||||
int numFloats = PyArray_NBYTES(coarseVerts) / sizeof(float);
|
||||
|
||||
csubd->vertexBuffer->UpdateData(pFloats, numFloats);
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
// - args is a list with 1 objects (CSubd)
|
||||
// - Calls Refine on the compute controller, passing it the compute
|
||||
// context and vertexBuffer.
|
||||
static PyObject *
|
||||
CSubdRefine(PyObject *self, PyObject *args)
|
||||
{
|
||||
PyObject* csubdObject = PyTuple_GetItem(args, 0);
|
||||
|
||||
if (!CSubd_Check(csubdObject)) {
|
||||
PyErr_SetString(PyExc_TypeError, "csubd_refine");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CSubd* csubd = (CSubd*) csubdObject;
|
||||
g_osdComputeController->Refine(
|
||||
csubd->computeContext,
|
||||
csubd->vertexBuffer);
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
CSubdGetVerts(PyObject *self, PyObject *args)
|
||||
{
|
||||
CSubd* csubd = (CSubd*) PyTuple_GetItem(args, 0);
|
||||
|
||||
float* pFloats = csubd->vertexBuffer->BindCpuBuffer();
|
||||
|
||||
npy_intp numFloats = (npy_intp)
|
||||
(csubd->vertexBuffer->GetNumElements() *
|
||||
csubd->vertexBuffer->GetNumVertices());
|
||||
|
||||
PyObject* retval = PyArray_SimpleNewFromData(
|
||||
1, &numFloats, PyArray_FLOAT, (void*) pFloats);
|
||||
|
||||
Py_INCREF(retval);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
CSubdGetQuads(PyObject *self, PyObject *args)
|
||||
{
|
||||
CSubd* csubd = (CSubd*) PyTuple_GetItem(args, 0);
|
||||
PyObject* dtype = PyTuple_GetItem(args, 1);
|
||||
|
||||
std::string typeName = ((PyTypeObject*) dtype)->tp_name;
|
||||
if (typeName != "numpy.uint32") {
|
||||
PyErr_SetString(
|
||||
PyExc_TypeError,
|
||||
"Index types other than numpy.uint32 are not yet supported.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
OpenSubdiv::FarPatchTables const * patchTables =
|
||||
csubd->farMesh->GetPatchTables();
|
||||
|
||||
if (patchTables) {
|
||||
PyErr_SetString(PyExc_TypeError, "feature adaptive not supported");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const OpenSubdiv::FarSubdivisionTables<OpenSubdiv::OsdVertex> *tables =
|
||||
csubd->farMesh->GetSubdivisionTables();
|
||||
|
||||
bool loop = dynamic_cast<const OpenSubdiv::FarLoopSubdivisionTables<
|
||||
OpenSubdiv::OsdVertex>*>(tables);
|
||||
|
||||
if (loop) {
|
||||
PyErr_SetString(PyExc_TypeError, "loop subdivision not supported");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int level = tables->GetMaxLevel();
|
||||
const std::vector<int> &indices = csubd->farMesh->GetFaceVertices(level-1);
|
||||
npy_intp length = (npy_intp) indices.size();
|
||||
|
||||
// this does NOT create a copy
|
||||
PyObject* retval = PyArray_SimpleNewFromData(
|
||||
1, &length, PyArray_UINT, (void*) &indices[0]);
|
||||
|
||||
Py_INCREF(retval);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static PyMethodDef osdFreeFunctions[] = {
|
||||
{"hbr_new", HbrNew, METH_VARARGS},
|
||||
{"hbr_delete", HbrDelete, METH_VARARGS},
|
||||
{"hbr_finish", HbrFinish, METH_VARARGS},
|
||||
{"csubd_new", CSubdNew, METH_VARARGS},
|
||||
{"csubd_delete", CSubdDelete, METH_VARARGS},
|
||||
{"csubd_update", CSubdUpdate, METH_VARARGS},
|
||||
{"csubd_refine", CSubdRefine, METH_VARARGS},
|
||||
{"csubd_getquads", CSubdGetQuads, METH_VARARGS},
|
||||
{"csubd_getverts", CSubdGetVerts, METH_VARARGS},
|
||||
|
||||
{"hbr_update_faces", HbrUpdateFaces, METH_VARARGS},
|
||||
{"hbr_get_vertex_sharpness", HbrGetVertexSharpness, METH_VARARGS},
|
||||
{"hbr_set_vertex_sharpness", HbrSetVertexSharpness, METH_VARARGS},
|
||||
{"hbr_get_num_faces", HbrGetNumFaces, METH_VARARGS},
|
||||
{"hbr_get_face_hole", HbrGetFaceHole, METH_VARARGS},
|
||||
{"hbr_set_face_hole", HbrSetFaceHole, METH_VARARGS},
|
||||
{"hbr_get_num_edges", HbrGetNumEdges, METH_VARARGS},
|
||||
{"hbr_get_edge_sharpness", HbrGetEdgeSharpness, METH_VARARGS},
|
||||
{"hbr_set_edge_sharpness", HbrSetEdgeSharpness, METH_VARARGS},
|
||||
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyMODINIT_FUNC
|
||||
initshim(void)
|
||||
{
|
||||
PyObject* m = Py_InitModule3(
|
||||
"shim",
|
||||
osdFreeFunctions,
|
||||
"Python bindings for Pixar's OpenSubdiv library");
|
||||
|
||||
OpaqueHbrMesh_Type.ob_type = &PyType_Type;
|
||||
OpaqueHbrMesh_Type.tp_new = OpaqueHbrMesh_new;
|
||||
OpaqueHbrMesh_Type.tp_methods = &OpaqueHbrMesh_methods[0];
|
||||
if (PyType_Ready(&OpaqueHbrMesh_Type) < 0) {
|
||||
printf("Can't register OpaqueHbrMesh type");
|
||||
return;
|
||||
}
|
||||
Py_INCREF(&OpaqueHbrMesh_Type);
|
||||
PyModule_AddObject(m, "OpaqueHbrMesh", (PyObject *)&OpaqueHbrMesh_Type);
|
||||
|
||||
CSubd_Type.ob_type = &PyType_Type;
|
||||
CSubd_Type.tp_new = CSubd_new;
|
||||
CSubd_Type.tp_methods = &CSubd_methods[0];
|
||||
if (PyType_Ready(&CSubd_Type) < 0) {
|
||||
printf("Can't register CSubd type");
|
||||
return;
|
||||
}
|
||||
Py_INCREF(&CSubd_Type);
|
||||
PyModule_AddObject(m, "CSubd", (PyObject *)&CSubd_Type);
|
||||
|
||||
// Numpy one-time initializations:
|
||||
import_array();
|
||||
|
||||
// OSD one-time initializations:
|
||||
g_osdComputeController = new OpenSubdiv::OsdCpuComputeController();
|
||||
}
|
102
python/osd/shim_adapters.cpp
Normal file
102
python/osd/shim_adapters.cpp
Normal file
@ -0,0 +1,102 @@
|
||||
static PyObject *
|
||||
HbrUpdateFaces(PyObject *self, PyObject *args)
|
||||
{
|
||||
OpaqueHbrMesh* hmesh = (OpaqueHbrMesh*) PyTuple_GetItem(args, 0);
|
||||
hmesh->faces->clear();
|
||||
hmesh->hmesh->GetFaces(std::back_inserter(*(hmesh->faces)));
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
HbrGetVertexSharpness(PyObject *self, PyObject *args)
|
||||
{
|
||||
OpaqueHbrMesh* hmesh = (OpaqueHbrMesh*) PyTuple_GetItem(args, 0);
|
||||
int index = (int) PyInt_AsLong(PyTuple_GetItem(args, 1));
|
||||
float sharpness = hmesh->hmesh->GetVertex(index)->GetSharpness();
|
||||
PyObject* retval = PyFloat_FromDouble(sharpness);
|
||||
Py_INCREF(retval);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
HbrSetVertexSharpness(PyObject *self, PyObject *args)
|
||||
{
|
||||
OpaqueHbrMesh* hmesh = (OpaqueHbrMesh*) PyTuple_GetItem(args, 0);
|
||||
int index = (int) PyInt_AsLong(PyTuple_GetItem(args, 1));
|
||||
double value = (double) PyFloat_AsDouble(PyTuple_GetItem(args, 2));
|
||||
hmesh->hmesh->GetVertex(index)->SetSharpness(value);
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
HbrGetNumFaces(PyObject *self, PyObject *args)
|
||||
{
|
||||
OpaqueHbrMesh* hmesh = (OpaqueHbrMesh*) PyTuple_GetItem(args, 0);
|
||||
PyObject* retval = PyInt_FromLong(hmesh->hmesh->GetNumFaces());
|
||||
Py_INCREF(retval);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
HbrGetFaceHole(PyObject *self, PyObject *args)
|
||||
{
|
||||
OpaqueHbrMesh* hmesh = (OpaqueHbrMesh*) PyTuple_GetItem(args, 0);
|
||||
int faceIndex = (int) PyInt_AsLong(PyTuple_GetItem(args, 1));
|
||||
bool isHole = hmesh->faces->at(faceIndex)->IsHole();
|
||||
PyObject* retval = isHole ? Py_True : Py_False;
|
||||
Py_INCREF(retval);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
HbrSetFaceHole(PyObject *self, PyObject *args)
|
||||
{
|
||||
OpaqueHbrMesh* hmesh = (OpaqueHbrMesh*) PyTuple_GetItem(args, 0);
|
||||
int faceIndex = (int) PyInt_AsLong(PyTuple_GetItem(args, 1));
|
||||
bool value = PyTuple_GetItem(args, 2) == Py_True;
|
||||
if (!value) {
|
||||
printf("Um, there's no API to unset a hole.\n");
|
||||
}
|
||||
|
||||
hmesh->faces->at(faceIndex)->SetHole();
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
HbrGetNumEdges(PyObject *self, PyObject *args)
|
||||
{
|
||||
OpaqueHbrMesh* hmesh = (OpaqueHbrMesh*) PyTuple_GetItem(args, 0);
|
||||
int faceIndex = (int) PyInt_AsLong(PyTuple_GetItem(args, 1));
|
||||
int edgeCount = hmesh->faces->at(faceIndex)->GetNumVertices();
|
||||
PyObject* retval = PyInt_FromLong(edgeCount);
|
||||
Py_INCREF(retval);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
HbrGetEdgeSharpness(PyObject *self, PyObject *args)
|
||||
{
|
||||
OpaqueHbrMesh* hmesh = (OpaqueHbrMesh*) PyTuple_GetItem(args, 0);
|
||||
int faceIndex = (int) PyInt_AsLong(PyTuple_GetItem(args, 1));
|
||||
int edgeIndex = (int) PyInt_AsLong(PyTuple_GetItem(args, 2));
|
||||
float sharpness = hmesh->faces->at(faceIndex)->
|
||||
GetEdge(edgeIndex)->GetSharpness();
|
||||
PyObject* retval = PyFloat_FromDouble(sharpness);
|
||||
Py_INCREF(retval);
|
||||
return retval;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
HbrSetEdgeSharpness(PyObject *self, PyObject *args)
|
||||
{
|
||||
OpaqueHbrMesh* hmesh = (OpaqueHbrMesh*) PyTuple_GetItem(args, 0);
|
||||
int faceIndex = (int) PyInt_AsLong(PyTuple_GetItem(args, 1));
|
||||
int edgeIndex = (int) PyInt_AsLong(PyTuple_GetItem(args, 2));
|
||||
double value = (double) PyFloat_AsDouble(PyTuple_GetItem(args, 3));
|
||||
hmesh->faces->at(faceIndex)->GetEdge(edgeIndex)->SetSharpness(value);
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
121
python/osd/shim_types.h
Normal file
121
python/osd/shim_types.h
Normal file
@ -0,0 +1,121 @@
|
||||
#include <Python.h>
|
||||
|
||||
#include <common/mutex.h>
|
||||
#include <far/meshFactory.h>
|
||||
#include <osd/vertex.h>
|
||||
#include <osd/cpuComputeContext.h>
|
||||
#include <osd/cpuVertexBuffer.h>
|
||||
|
||||
typedef OpenSubdiv::HbrMesh<OpenSubdiv::OsdVertex> OsdHbrMesh;
|
||||
typedef OpenSubdiv::HbrVertex<OpenSubdiv::OsdVertex> OsdHbrVertex;
|
||||
typedef OpenSubdiv::HbrFace<OpenSubdiv::OsdVertex> OsdHbrFace;
|
||||
typedef OpenSubdiv::HbrHalfedge<OpenSubdiv::OsdVertex> OsdHbrHalfedge;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
struct OpaqueHbrMesh {
|
||||
PyObject_HEAD
|
||||
OsdHbrMesh *hmesh;
|
||||
std::vector<OsdHbrFace*>* faces;
|
||||
};
|
||||
|
||||
static void
|
||||
OpaqueHbrMesh_dealloc(OpaqueHbrMesh *self)
|
||||
{
|
||||
self->ob_type->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
static PyMethodDef OpaqueHbrMesh_methods[] = {
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
statichere PyTypeObject OpaqueHbrMesh_Type = {
|
||||
PyObject_HEAD_INIT(0)
|
||||
0, // ob_size
|
||||
"OpaqueHbrMesh", // tp_name
|
||||
sizeof(OpaqueHbrMesh), // tp_basicsize
|
||||
0, // tp_itemsize
|
||||
(destructor) OpaqueHbrMesh_dealloc, // tp_dealloc
|
||||
0, // tp_print
|
||||
0, // tp_getattr
|
||||
0, // tp_setattr
|
||||
0, // tp_compare
|
||||
0, // tp_repr
|
||||
0, // tp_as_number
|
||||
0, // tp_as_sequence
|
||||
0, // tp_as_mapping
|
||||
0, // tp_hash
|
||||
0, // tp_call
|
||||
0, // tp_str
|
||||
0, // tp_getattro
|
||||
0, // tp_setattro
|
||||
0, // tp_as_buffer
|
||||
Py_TPFLAGS_DEFAULT, // tp_flags
|
||||
"OpaqueHbrMesh Object", // tp_doc
|
||||
};
|
||||
|
||||
#define OpaqueHbrMesh_Check(v) ((v)->ob_type == &OpaqueHbrMesh_Type)
|
||||
|
||||
PyObject *
|
||||
OpaqueHbrMesh_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
|
||||
{
|
||||
OpaqueHbrMesh *self = (OpaqueHbrMesh*) type->tp_alloc(type, 0);
|
||||
self->hmesh = NULL;
|
||||
return (PyObject*) self;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
struct CSubd {
|
||||
PyObject_HEAD
|
||||
OpenSubdiv::FarMesh<OpenSubdiv::OsdVertex>* farMesh;
|
||||
OpenSubdiv::OsdCpuComputeContext* computeContext;
|
||||
OpenSubdiv::OsdCpuVertexBuffer* vertexBuffer;
|
||||
};
|
||||
|
||||
static void
|
||||
CSubd_dealloc(CSubd *self)
|
||||
{
|
||||
self->ob_type->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
static PyMethodDef CSubd_methods[] = {
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
statichere PyTypeObject CSubd_Type = {
|
||||
PyObject_HEAD_INIT(0)
|
||||
0, // ob_size
|
||||
"CSubd", // tp_name
|
||||
sizeof(CSubd), // tp_basicsize
|
||||
0, // tp_itemsize
|
||||
(destructor) CSubd_dealloc, // tp_dealloc
|
||||
0, // tp_print
|
||||
0, // tp_getattr
|
||||
0, // tp_setattr
|
||||
0, // tp_compare
|
||||
0, // tp_repr
|
||||
0, // tp_as_number
|
||||
0, // tp_as_sequence
|
||||
0, // tp_as_mapping
|
||||
0, // tp_hash
|
||||
0, // tp_call
|
||||
0, // tp_str
|
||||
0, // tp_getattro
|
||||
0, // tp_setattro
|
||||
0, // tp_as_buffer
|
||||
Py_TPFLAGS_DEFAULT, // tp_flags
|
||||
"CSubd Object", // tp_doc
|
||||
};
|
||||
|
||||
#define CSubd_Check(v) ((v)->ob_type == &CSubd_Type)
|
||||
|
||||
PyObject *
|
||||
CSubd_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
|
||||
{
|
||||
CSubd *self = (CSubd*) type->tp_alloc(type, 0);
|
||||
self->farMesh = 0;
|
||||
self->computeContext = 0;
|
||||
self->vertexBuffer = 0;
|
||||
return (PyObject*) self;
|
||||
}
|
90
python/osd/subdivider.py
Normal file
90
python/osd/subdivider.py
Normal file
@ -0,0 +1,90 @@
|
||||
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: list of numpy types_
|
||||
: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")
|
||||
self.vertexLayout = vertexLayout
|
||||
self.indexType = indexType
|
||||
self.level = levels
|
||||
self._csubd = shim.csubd_new(self, topo)
|
||||
|
||||
# Calls UpdateData on the vertexBuffer.
|
||||
def setCage(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:
|
||||
coarseVerts = numpy.array(coarseVerts, listType)
|
||||
coarseVerts = coarseVerts.view(self.vertexLayout)
|
||||
shim.csubd_update(self._csubd, coarseVerts)
|
||||
pass
|
||||
|
||||
# Calls Refine on the compute controller, passing it the compute
|
||||
# context and vertexBuffer.
|
||||
def refine(self):
|
||||
'''Performs the actual subdivision work.'''
|
||||
shim.csubd_refine(self._csubd)
|
||||
|
||||
# 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``).
|
||||
'''
|
||||
return shim.csubd_getverts(self._csubd)
|
||||
|
||||
def getRefinedTopology(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``).
|
||||
'''
|
||||
if not hasattr(self, "_quads"):
|
||||
self._quads = shim.csubd_getquads(self._csubd, self.indexType)
|
||||
return self._quads
|
||||
|
||||
def __del__(self):
|
||||
if hasattr(self, "_far_mesh"):
|
||||
shim.csubd_delete(self._far_mesh)
|
144
python/osd/topology.py
Normal file
144
python/osd/topology.py
Normal file
@ -0,0 +1,144 @@
|
||||
from osd import *
|
||||
from adapters import *
|
||||
|
||||
import itertools
|
||||
import numpy as np
|
||||
import shim
|
||||
|
||||
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.boundaryInterpolation = InterpolateBoundary.EDGE_ONLY
|
||||
self.indices = np.array(indices, 'int32')
|
||||
self.valences = np.array(valences, 'uint8')
|
||||
|
||||
# TODO remove this in favor of HbrMesh::GetNumVertices
|
||||
self._maxIndex = int(self.indices.max())
|
||||
|
||||
self._hbr_mesh = shim.hbr_new(self)
|
||||
self._vertexListAdapter = VertexListAdapter(self)
|
||||
self._faceListAdapter = FaceListAdapter(self)
|
||||
|
||||
@property
|
||||
def boundaryInterpolation(self):
|
||||
'''Gets or sets the boundary interpolation method for this
|
||||
mesh to one of the values defined in
|
||||
:class:`osd.InterpolateBoundary`.
|
||||
'''
|
||||
return self._bi
|
||||
|
||||
@boundaryInterpolation.setter
|
||||
def boundaryInterpolation(self, value):
|
||||
self._bi = 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.'''
|
||||
shim.hbr_finish(self._hbr_mesh)
|
||||
|
||||
def __del__(self):
|
||||
if hasattr(self, "_hbr_mesh"):
|
||||
shim.hbr_delete(self._hbr_mesh)
|
||||
|
||||
# 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
|
69
python/osd/utility.py
Normal file
69
python/osd/utility.py
Normal file
@ -0,0 +1,69 @@
|
||||
import numpy as np
|
||||
import numpy.linalg as linalg
|
||||
from itertools import izip
|
||||
|
||||
def grouped(iterable, n):
|
||||
"s -> (s0,s1,s2,...sn-1), (sn,sn+1,sn+2,...s2n-1), (s2n,s2n+1,s2n+2,...s3n-1), ..."
|
||||
return izip(*[iter(iterable)]*n)
|
||||
|
||||
def repl():
|
||||
"For debugging, breaks to an interactive Python session."
|
||||
import code, inspect
|
||||
frame = inspect.currentframe()
|
||||
myLocals = locals()
|
||||
callersLocals = frame.f_back.f_locals
|
||||
code.interact(local=dict(globals(), **callersLocals))
|
||||
|
||||
def dumpMesh(name, subd):
|
||||
"Dumps refined mesh data to a pair of raw binary files"
|
||||
|
||||
quads = subd.getRefinedTopology()
|
||||
if quads.max() >= (2 ** 16):
|
||||
indexType = 'uint32'
|
||||
else:
|
||||
indexType = 'uint16'
|
||||
quads.astype(indexType).tofile(name + ".quads")
|
||||
|
||||
positions = subd.getRefinedVertices()
|
||||
positions.astype('float32').tofile(name + ".positions")
|
||||
|
||||
def computeSmoothNormals(coords, quads):
|
||||
"Returns a list of normals, whose length is the same as coords."
|
||||
|
||||
if quads.dtype != np.uint32 or coords.dtype != np.float32:
|
||||
raise OsdTypeError("Only uint32 indices and float coords are supported")
|
||||
|
||||
if (len(quads) % 4) or (len(coords) % 3):
|
||||
raise OsdTypeError("Only quads and 3D coords are supported")
|
||||
|
||||
print "Computing normals..."
|
||||
|
||||
quads = quads.reshape((-1,4))
|
||||
coords = coords.reshape((-1,3))
|
||||
vertexToQuads = [[] for c in coords]
|
||||
quadNormals = np.empty((len(quads),3), np.float32)
|
||||
vertexNormals = np.empty((len(coords),3), np.float32)
|
||||
|
||||
for quadIndex, q in enumerate(quads):
|
||||
vertexToQuads[q[0]].append(quadIndex)
|
||||
vertexToQuads[q[1]].append(quadIndex)
|
||||
vertexToQuads[q[2]].append(quadIndex)
|
||||
vertexToQuads[q[3]].append(quadIndex)
|
||||
a, b, c = coords[q[0]], coords[q[1]], coords[q[2]]
|
||||
ab = np.subtract(b, a)
|
||||
ac = np.subtract(c, a)
|
||||
n = np.cross(ab, ac)
|
||||
n = n / linalg.norm(n)
|
||||
quadNormals[quadIndex] = n
|
||||
|
||||
for i, v2q in enumerate(vertexToQuads):
|
||||
n = np.zeros(3, np.float32)
|
||||
if not v2q:
|
||||
vertexNormals[i] = n
|
||||
continue
|
||||
for q in v2q:
|
||||
n = n + quadNormals[q]
|
||||
vertexNormals[i] = n / linalg.norm(n)
|
||||
|
||||
vertexNormals.resize(len(vertexNormals) * 3)
|
||||
return vertexNormals
|
86
python/setup.py
Executable file
86
python/setup.py
Executable file
@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from distutils.core import setup, Command, Extension
|
||||
import numpy
|
||||
import os, os.path
|
||||
|
||||
# You may need to change this location,
|
||||
# depending on where you built the core
|
||||
# OpenSubdiv library:
|
||||
osd_lib_path = '../build/lib'
|
||||
|
||||
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)
|
||||
|
||||
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 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')
|
||||
|
||||
np_include_dir = numpy.get_include()
|
||||
np_library_dir = os.path.join(np_include_dir, '../lib')
|
||||
|
||||
osd_shim = Extension('osd.shim',
|
||||
runtime_library_dirs = [osd_lib_path],
|
||||
include_dirs = ['../opensubdiv', '../regression'],
|
||||
library_dirs = ['../build/lib', np_library_dir],
|
||||
libraries = ['osdCPU', 'npymath'],
|
||||
sources = ['osd/shim.cpp'])
|
||||
|
||||
# Disable warnings produced by numpy headers:
|
||||
osd_shim.extra_compile_args = [
|
||||
"-Wno-unused-function"]
|
||||
|
||||
os.environ['ARCHFLAGS'] = '-arch ' + os.uname()[4]
|
||||
|
||||
setup(name = "OpenSubdiv",
|
||||
version = "0.1",
|
||||
packages = ['osd'],
|
||||
author = 'Pixar Animation Studios',
|
||||
cmdclass = {'test': TestCommand,
|
||||
'doc': DocCommand,
|
||||
'demo': DemoCommand},
|
||||
include_dirs = [np_include_dir],
|
||||
ext_modules = [osd_shim],
|
||||
description = 'Python Bindings to the Pixar Subdivision Library')
|
1
python/test/__init__.py
Normal file
1
python/test/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from simple import *
|
140
python/test/simple.py
Executable file
140
python/test/simple.py
Executable file
@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
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.boundaryInterpolation = osd.InterpolateBoundary.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.setCage(verts, np.float32)
|
||||
subdivider.refine()
|
||||
|
||||
numQuads = len(subdivider.getRefinedTopology()) / 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()
|
Loading…
Reference in New Issue
Block a user