951 lines
44 KiB
Plaintext
951 lines
44 KiB
Plaintext
|
{
|
||
|
"cells": [
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"\n",
|
||
|
"# Product application in 3D\n",
|
||
|
"\n",
|
||
|
"## Algorithm to generate 3D mesh from extrusion profile and arbitrary polygon\n",
|
||
|
"\n",
|
||
|
"The input polygon that we want to cover with 3D tiles needs to be:\n",
|
||
|
" - simple\n",
|
||
|
" - planar\n",
|
||
|
"\n",
|
||
|
"Base of the idea is in establishing TBN space, defined by (t)angent, (b)itangent and (n)ormal vector of the input polygon. In this space we can align the (repeated) 2D profile to the top (start) and bottom (end) edge of the polygon's bounding box. These two paths are then bridged to create a quad-mesh. This mesh is then clipped to the original polygon using quasi-CSG operations.\n",
|
||
|
"\n",
|
||
|
"Notebook requirements:\n",
|
||
|
" * Jupyter lab\n",
|
||
|
" * numpy, scipy, ipywidgets\n",
|
||
|
" * pythreejs (both the package and the lab extension)\n",
|
||
|
" * cython-csg\n",
|
||
|
" "
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"### Imports and basic tools"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 225,
|
||
|
"metadata": {
|
||
|
"jupyter": {
|
||
|
"source_hidden": true
|
||
|
}
|
||
|
},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"import random\n",
|
||
|
"import numpy as np\n",
|
||
|
"from math import pi, ceil, floor\n",
|
||
|
"from scipy.linalg import expm, norm\n",
|
||
|
"from scipy.spatial import Delaunay\n",
|
||
|
"import ipywidgets as widgets\n",
|
||
|
"from ipywidgets import interact, interact_manual\n",
|
||
|
"\n",
|
||
|
"EPS = 1e-5\n",
|
||
|
"\n",
|
||
|
"def poly_normal(poly):\n",
|
||
|
" a, b, c = poly[:3]\n",
|
||
|
" x = np.linalg.det([[1,a[1],a[2]],\n",
|
||
|
" [1,b[1],b[2]],\n",
|
||
|
" [1,c[1],c[2]]])\n",
|
||
|
" y = np.linalg.det([[a[0],1,a[2]],\n",
|
||
|
" [b[0],1,b[2]],\n",
|
||
|
" [c[0],1,c[2]]])\n",
|
||
|
" z = np.linalg.det([[a[0],a[1],1],\n",
|
||
|
" [b[0],b[1],1],\n",
|
||
|
" [c[0],c[1],1]])\n",
|
||
|
" magnitude = (x**2 + y**2 + z**2)**.5\n",
|
||
|
" return (x/magnitude, y/magnitude, z/magnitude)\n",
|
||
|
"\n",
|
||
|
"def poly_area(poly):\n",
|
||
|
" if len(poly) < 3: # not a plane - no area\n",
|
||
|
" return 0\n",
|
||
|
" total = [0, 0, 0]\n",
|
||
|
" N = len(poly)\n",
|
||
|
" for i in range(N):\n",
|
||
|
" vi1 = poly[i]\n",
|
||
|
" vi2 = poly[(i+1) % N]\n",
|
||
|
" prod = np.cross(vi1, vi2)\n",
|
||
|
" total[0] += prod[0]\n",
|
||
|
" total[1] += prod[1]\n",
|
||
|
" total[2] += prod[2]\n",
|
||
|
" result = np.dot(total, poly_normal(poly[0], poly[1], poly[2]))\n",
|
||
|
" return result/2\n",
|
||
|
"\n",
|
||
|
"def normalize(v):\n",
|
||
|
" return v/np.linalg.norm(v)"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"### Display functions"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 226,
|
||
|
"metadata": {
|
||
|
"jupyter": {
|
||
|
"source_hidden": true
|
||
|
}
|
||
|
},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"from collections import namedtuple\n",
|
||
|
"from pythreejs import *\n",
|
||
|
"from IPython.display import display\n",
|
||
|
"import matplotlib.pyplot as plt\n",
|
||
|
"import matplotlib.patches as pltpat\n",
|
||
|
"from matplotlib.colors import to_hex\n",
|
||
|
"\n",
|
||
|
"Face3D = namedtuple('Face', [\n",
|
||
|
" 'a','b','c',\n",
|
||
|
" 'normal','color','materialIndex'\n",
|
||
|
"])\n",
|
||
|
"\n",
|
||
|
"Bounds = namedtuple('Bounds', [\n",
|
||
|
" 'min', 'max', 'size', 'center', 'mag'\n",
|
||
|
"])\n",
|
||
|
"\n",
|
||
|
"W = 600\n",
|
||
|
"H = 400\n",
|
||
|
"\n",
|
||
|
"def bounds(position):\n",
|
||
|
" bmin = np.min(position, axis=0)\n",
|
||
|
" bmax = np.max(position, axis=0)\n",
|
||
|
" bext = bmax - bmin\n",
|
||
|
" bmag = np.max(bext)\n",
|
||
|
" bctr = bmin + bext*0.5\n",
|
||
|
" return Bounds(\n",
|
||
|
" min=bmin, max=bmax, size=bext, center=bctr, mag=bmag)\n",
|
||
|
"\n",
|
||
|
"def make_scene(*args):\n",
|
||
|
" scene = Scene(children=[\n",
|
||
|
" DirectionalLight(\n",
|
||
|
" color=\"#ffffff\",\n",
|
||
|
" position=[3,5,1],\n",
|
||
|
" intensity=1.0),\n",
|
||
|
" AmbientLight(\n",
|
||
|
" color=\"#ffffff\",\n",
|
||
|
" intensity=0.3),\n",
|
||
|
" GridHelper(15,15),\n",
|
||
|
" AxesHelper(1)\n",
|
||
|
" ])\n",
|
||
|
" for x in args:\n",
|
||
|
" scene.add(x)\n",
|
||
|
" return scene\n",
|
||
|
"\n",
|
||
|
"def display_poly2D(poly):\n",
|
||
|
" p = pltpat.Polygon(poly, closed=False)\n",
|
||
|
" ax = plt.gca()\n",
|
||
|
" ax.add_patch(p)\n",
|
||
|
" bmin = np.min(poly, axis=0)\n",
|
||
|
" bmax = np.max(poly, axis=0)\n",
|
||
|
" ax.set_xlim(bmin[0]-1, bmax[0]+1)\n",
|
||
|
" ax.set_ylim(bmin[1]-1, bmax[1]+1)\n",
|
||
|
" plt.show()\n",
|
||
|
"\n",
|
||
|
"def poly_to_mesh(position, indices, color, opacity=1):\n",
|
||
|
" geom = Geometry()\n",
|
||
|
" geom.vertices = position\n",
|
||
|
" geom.faces = [\n",
|
||
|
" Face3D(a=a, b=b, c=c, \n",
|
||
|
" normal=poly_normal([\n",
|
||
|
" position[a],\n",
|
||
|
" position[b],\n",
|
||
|
" position[c]\n",
|
||
|
" ]), \n",
|
||
|
" color=(1,1,1), \n",
|
||
|
" materialIndex=0)\n",
|
||
|
" for a, b, c in indices]\n",
|
||
|
" mtl = MeshStandardMaterial(\n",
|
||
|
" color=to_hex(color),\n",
|
||
|
" metallicity=0,\n",
|
||
|
" roughness=1,\n",
|
||
|
" opacity=opacity,\n",
|
||
|
" transparent=True,\n",
|
||
|
" side='DoubleSide')\n",
|
||
|
" return Mesh(geom, mtl), bounds(position)\n",
|
||
|
"\n",
|
||
|
"def mesh_renderer(mesh, bbox):\n",
|
||
|
" scene = make_scene(mesh)\n",
|
||
|
" camera = PerspectiveCamera(\n",
|
||
|
" position=[1,1,1], up=[0,1,0], aspect=W/H)\n",
|
||
|
" camera.position = tuple(bbox.center + [0, bbox.mag/2, bbox.mag])\n",
|
||
|
" ctrl = OrbitControls(\n",
|
||
|
" controlling=camera, \n",
|
||
|
" target=tuple(bbox.center))\n",
|
||
|
" ctrl.exec_three_obj_method('update')\n",
|
||
|
" \n",
|
||
|
" return Renderer(\n",
|
||
|
" width=W, height=H,\n",
|
||
|
" camera=camera, scene=scene, controls=[ctrl])\n",
|
||
|
" \n",
|
||
|
"# display(\n",
|
||
|
"# mesh_renderer(\n",
|
||
|
"# *poly_to_mesh(\n",
|
||
|
"# [[0,0,0], [1,0,0], [1,0,-1], [0,0,-1]], \n",
|
||
|
"# [[0,1,2],[0,2,3]],\n",
|
||
|
"# (1,0,0))))"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"### Define input polygon"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 227,
|
||
|
"metadata": {},
|
||
|
"outputs": [
|
||
|
{
|
||
|
"data": {
|
||
|
"image/png": "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
|
||
|
"text/plain": [
|
||
|
"<Figure size 432x288 with 1 Axes>"
|
||
|
]
|
||
|
},
|
||
|
"metadata": {
|
||
|
"needs_background": "light"
|
||
|
},
|
||
|
"output_type": "display_data"
|
||
|
}
|
||
|
],
|
||
|
"source": [
|
||
|
"# Start in XZ plane\n",
|
||
|
"face_xz = np.array([\n",
|
||
|
" [-5, 0, 0],\n",
|
||
|
" [ 5, 0, 0],\n",
|
||
|
" [ 6, 0, -3],\n",
|
||
|
" [ 0, 0, -5]\n",
|
||
|
"])\n",
|
||
|
"face_xz_2D = face_xz[:,[0,2]]\n",
|
||
|
"\n",
|
||
|
"display_poly2D(face_xz_2D)"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"### Bring it to 3D"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 228,
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"import random\n",
|
||
|
"\n",
|
||
|
"def rotate(axis, theta):\n",
|
||
|
" return expm(np.cross(np.eye(3), axis/norm(axis)*theta))\n",
|
||
|
"\n",
|
||
|
"tf = np.dot(\n",
|
||
|
" # rotate on Y axis by 90 deg.\n",
|
||
|
" rotate([0,1,0], random.uniform(-pi/2, pi/2)), \n",
|
||
|
" # rotate on X axis by n deg.\n",
|
||
|
" rotate([1,0,0], random.uniform(0, pi/4))\n",
|
||
|
")\n",
|
||
|
"tf_inv = np.linalg.inv(tf)\n",
|
||
|
"\n",
|
||
|
"face = tf.dot(face_xz.T).T\n",
|
||
|
"face_indices = Delaunay(face_xz_2D).simplices"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 229,
|
||
|
"metadata": {
|
||
|
"jupyter": {
|
||
|
"source_hidden": true
|
||
|
}
|
||
|
},
|
||
|
"outputs": [
|
||
|
{
|
||
|
"data": {
|
||
|
"application/vnd.jupyter.widget-view+json": {
|
||
|
"model_id": "1bbfd79531d74a6aa21caffb2699a9f2",
|
||
|
"version_major": 2,
|
||
|
"version_minor": 0
|
||
|
},
|
||
|
"text/plain": [
|
||
|
"Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…"
|
||
|
]
|
||
|
},
|
||
|
"metadata": {},
|
||
|
"output_type": "display_data"
|
||
|
}
|
||
|
],
|
||
|
"source": [
|
||
|
"display(mesh_renderer(*poly_to_mesh(face.tolist(), face_indices, (1,0.5,1))))"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"### Find basis for surface tangent space\n",
|
||
|
"\n",
|
||
|
"Following doesn't work well for polygons aligned to XZ plane, ie. when the normal is the same as the \"projection\" vector, which in our \"common\" case is the up-vector."
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 230,
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"def poly_basis(p, poly):\n",
|
||
|
" n = poly_normal(poly)\n",
|
||
|
" t = np.cross(p, n)\n",
|
||
|
" if np.linalg.norm(t) < EPS:\n",
|
||
|
" # colinear normal with projection, this could be\n",
|
||
|
" # some \"base edge\" if known\n",
|
||
|
" t = np.array([1, 0, 0])\n",
|
||
|
" else:\n",
|
||
|
" t = normalize(t)\n",
|
||
|
" b = normalize(np.cross(t, n))\n",
|
||
|
" return [t, b, n]\n",
|
||
|
"\n",
|
||
|
"basis = poly_basis([0,1,0], face)"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 231,
|
||
|
"metadata": {
|
||
|
"jupyter": {
|
||
|
"source_hidden": true
|
||
|
}
|
||
|
},
|
||
|
"outputs": [
|
||
|
{
|
||
|
"data": {
|
||
|
"application/vnd.jupyter.widget-view+json": {
|
||
|
"model_id": "f7fce8d8be4a4387bcd6b5a65099adc2",
|
||
|
"version_major": 2,
|
||
|
"version_minor": 0
|
||
|
},
|
||
|
"text/plain": [
|
||
|
"Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…"
|
||
|
]
|
||
|
},
|
||
|
"metadata": {},
|
||
|
"output_type": "display_data"
|
||
|
}
|
||
|
],
|
||
|
"source": [
|
||
|
"RGB = [(1,0,0),(0,1,0),(0,0,1)]\n",
|
||
|
"\n",
|
||
|
"def basis_helpers_rgb(origin, t, b, n):\n",
|
||
|
" return [\n",
|
||
|
" ArrowHelper(\n",
|
||
|
" origin=origin, dir=tuple(v), \n",
|
||
|
" color=to_hex(RGB[i]), headWidth=0.2)\n",
|
||
|
" for i, v in enumerate([t, b, n])]\n",
|
||
|
"\n",
|
||
|
"def basis_helpers(origin, color, t, b, n):\n",
|
||
|
" return [\n",
|
||
|
" ArrowHelper(\n",
|
||
|
" origin=origin, dir=tuple(v), \n",
|
||
|
" color=color, headWidth=0.2)\n",
|
||
|
" for i, v in enumerate([t, b, n])]\n",
|
||
|
"\n",
|
||
|
"def basis_renderer(origin, opacity=1):\n",
|
||
|
" renderer = mesh_renderer(*poly_to_mesh(face.tolist(), face_indices, (1,0.5,1), opacity))\n",
|
||
|
" for h in basis_helpers_rgb(origin, *basis):\n",
|
||
|
" renderer.scene.add(h)\n",
|
||
|
" return renderer\n",
|
||
|
"\n",
|
||
|
"origin = tuple(tf.dot(bounds(face_xz).center+[0,0.01,0]))\n",
|
||
|
"display(basis_renderer(origin))"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"### Find extrusion origin\n",
|
||
|
"\n",
|
||
|
"We need to find an origin point where we can place the 2D profile to be extruded, as well as extrusion axis (the profile will be oriented perpendicular to the extrusion axis).\n",
|
||
|
"\n",
|
||
|
"To do this, we transform the face into tangent space using TBN (tangent, bitangent, normal) matrix, and then take the min of its bounding box. This is then transformed using inverse TBN matrix back to world space."
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 232,
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"TBN = np.array(basis)\n",
|
||
|
"TBN_inv = np.linalg.inv(TBN)\n",
|
||
|
"face_tbn = TBN.dot(face.T).T\n",
|
||
|
"ext_origin = TBN_inv.dot(np.min(face_tbn, axis=0))"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 233,
|
||
|
"metadata": {
|
||
|
"jupyter": {
|
||
|
"source_hidden": true
|
||
|
}
|
||
|
},
|
||
|
"outputs": [
|
||
|
{
|
||
|
"data": {
|
||
|
"application/vnd.jupyter.widget-view+json": {
|
||
|
"model_id": "ea3a6b17dede40f5ad0ff13bc85ec181",
|
||
|
"version_major": 2,
|
||
|
"version_minor": 0
|
||
|
},
|
||
|
"text/plain": [
|
||
|
"Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…"
|
||
|
]
|
||
|
},
|
||
|
"metadata": {},
|
||
|
"output_type": "display_data"
|
||
|
}
|
||
|
],
|
||
|
"source": [
|
||
|
"def tbn_and_origin_renderer():\n",
|
||
|
" mesh, _ = poly_to_mesh(face_tbn.tolist(), face_indices, (0,1,1), 0.5)\n",
|
||
|
" renderer = basis_renderer(ext_origin.tolist(), opacity=0.5)\n",
|
||
|
" renderer.scene.add(mesh)\n",
|
||
|
" return renderer\n",
|
||
|
"\n",
|
||
|
"display(tbn_and_origin_renderer())"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"### Define 2D profile curve\n",
|
||
|
"\n",
|
||
|
"This will be specified on input, such as a path from SVG source."
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 234,
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"profile = np.array([\n",
|
||
|
" [0.0, 0.2],\n",
|
||
|
" [1.0, 0.2],\n",
|
||
|
" [1.1, 0.5],\n",
|
||
|
" [1.9, 0.5],\n",
|
||
|
" [2.0, 0.2]\n",
|
||
|
"], dtype=np.float32)\n",
|
||
|
"profile_bounds = bounds(profile)\n",
|
||
|
"unit_profile = (profile - profile_bounds.min) / profile_bounds.size[0]"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 235,
|
||
|
"metadata": {
|
||
|
"jupyter": {
|
||
|
"source_hidden": true
|
||
|
}
|
||
|
},
|
||
|
"outputs": [
|
||
|
{
|
||
|
"data": {
|
||
|
"text/plain": [
|
||
|
"(-0.29999999701976776, 1.0)"
|
||
|
]
|
||
|
},
|
||
|
"execution_count": 235,
|
||
|
"metadata": {},
|
||
|
"output_type": "execute_result"
|
||
|
},
|
||
|
{
|
||
|
"data": {
|
||
|
"image/png": "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
|
||
|
"text/plain": [
|
||
|
"<Figure size 432x288 with 1 Axes>"
|
||
|
]
|
||
|
},
|
||
|
"metadata": {
|
||
|
"needs_background": "light"
|
||
|
},
|
||
|
"output_type": "display_data"
|
||
|
}
|
||
|
],
|
||
|
"source": [
|
||
|
"plt.plot(profile[:,0], profile[:,1])\n",
|
||
|
"plt.plot(unit_profile[:,0], unit_profile[:,1])\n",
|
||
|
"plt.xlim(profile_bounds.min[0]-0.5, profile_bounds.max[0]+0.5)\n",
|
||
|
"plt.ylim(profile_bounds.min[1]-0.5, profile_bounds.max[1]+0.5)"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"### Transform and extend the profile\n",
|
||
|
"\n",
|
||
|
"- Such that its origin aligns with extrusion origin and basis vectors.\n",
|
||
|
"- Such that the paths is repeated the cover the face in tangent direction."
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 236,
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"unit_profile_3D = TBN_inv.dot(np.insert(unit_profile, 1, values=0, axis=1).T).T + ext_origin\n",
|
||
|
"nrep = ceil(bounds(face_tbn).size[0])\n",
|
||
|
"\n",
|
||
|
"def repeat_path(path, nrep):\n",
|
||
|
" step = bounds(path).size[0]\n",
|
||
|
" vertices = np.empty((0, path.shape[1]))\n",
|
||
|
" offset = 0\n",
|
||
|
" for i in range(nrep):\n",
|
||
|
" offset_vertices = path + [offset, 0]\n",
|
||
|
" if len(vertices) > 0 and np.linalg.norm(offset_vertices[0] - vertices[-1]) < EPS:\n",
|
||
|
" vertices = np.concatenate((vertices, offset_vertices[1:]), axis=0)\n",
|
||
|
" else:\n",
|
||
|
" vertices = np.concatenate((vertices, offset_vertices), axis=0)\n",
|
||
|
" offset += step\n",
|
||
|
" return np.array(vertices)\n",
|
||
|
"\n",
|
||
|
"profile_ext = TBN_inv.dot(\n",
|
||
|
" np.insert(\n",
|
||
|
" repeat_path(unit_profile, nrep), \n",
|
||
|
" 1, values=0, axis=1).T\n",
|
||
|
").T + ext_origin"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 237,
|
||
|
"metadata": {
|
||
|
"jupyter": {
|
||
|
"source_hidden": true
|
||
|
}
|
||
|
},
|
||
|
"outputs": [
|
||
|
{
|
||
|
"data": {
|
||
|
"application/vnd.jupyter.widget-view+json": {
|
||
|
"model_id": "d853bc291ada4785ab9d61e7bff07504",
|
||
|
"version_major": 2,
|
||
|
"version_minor": 0
|
||
|
},
|
||
|
"text/plain": [
|
||
|
"HBox(children=(Label(value='Width :'), FloatSlider(value=1.0, max=3.0, min=0.25, step=0.01)))"
|
||
|
]
|
||
|
},
|
||
|
"metadata": {},
|
||
|
"output_type": "display_data"
|
||
|
},
|
||
|
{
|
||
|
"data": {
|
||
|
"application/vnd.jupyter.widget-view+json": {
|
||
|
"model_id": "48edac81872d404d89279aa8caf6d186",
|
||
|
"version_major": 2,
|
||
|
"version_minor": 0
|
||
|
},
|
||
|
"text/plain": [
|
||
|
"Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…"
|
||
|
]
|
||
|
},
|
||
|
"metadata": {},
|
||
|
"output_type": "display_data"
|
||
|
}
|
||
|
],
|
||
|
"source": [
|
||
|
"def profile_geom(path, width, offset=[0,0,0]):\n",
|
||
|
" nrep = ceil(bounds(face_tbn).size[0]/width)\n",
|
||
|
" single_profile = path*width\n",
|
||
|
" retpath = TBN_inv.dot(\n",
|
||
|
" np.insert(\n",
|
||
|
" repeat_path(single_profile, nrep), \n",
|
||
|
" 1, values=0, axis=1).T\n",
|
||
|
" ).T + ext_origin + offset\n",
|
||
|
" return BufferGeometry(\n",
|
||
|
" attributes={\n",
|
||
|
" 'position': BufferAttribute(retpath.astype(np.float32))\n",
|
||
|
" }, normalized=False)\n",
|
||
|
" \n",
|
||
|
"def profile_mesh(path, width, offset=[0,0,0], color=\"#ff0000\"):\n",
|
||
|
" geom = profile_geom(path, width, offset)\n",
|
||
|
" mtl = LineBasicMaterial(color=color)\n",
|
||
|
" return Line(geom, mtl)\n",
|
||
|
"\n",
|
||
|
"def profile_renderer(path, width):\n",
|
||
|
" mesh = profile_mesh(path, width)\n",
|
||
|
" renderer = mesh_renderer(\n",
|
||
|
" *poly_to_mesh(face.tolist(), face_indices, (1,0.5,1), \n",
|
||
|
" opacity=0.75))\n",
|
||
|
" for h in basis_helpers(ext_origin.tolist(), \"#cccccc\", *basis):\n",
|
||
|
" renderer.scene.add(h)\n",
|
||
|
" renderer.scene.add(mesh)\n",
|
||
|
" return renderer, mesh\n",
|
||
|
"\n",
|
||
|
"def run_profile_renderer():\n",
|
||
|
" renderer, mesh = profile_renderer(unit_profile, 1.0)\n",
|
||
|
" def update_profile_renderer(width):\n",
|
||
|
" mesh.geometry.exec_three_obj_method('dispose')\n",
|
||
|
" mesh.geometry = profile_geom(unit_profile, width['new'])\n",
|
||
|
" width = widgets.FloatSlider(min=0.25, max=3.0, step=0.01, value=1.0)\n",
|
||
|
" width.observe(update_profile_renderer, 'value')\n",
|
||
|
" display(widgets.HBox(children=[\n",
|
||
|
" widgets.Label(\"Width :\"),\n",
|
||
|
" width]))\n",
|
||
|
" display(renderer)\n",
|
||
|
"\n",
|
||
|
"run_profile_renderer()"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"Next, create the other end of the extrusion by offseting the base along bitangent vector, by the Y extent in TBN space:"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 238,
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"extrusion_length = bounds(face_tbn).size[1]\n",
|
||
|
"profile_ext_end = profile_ext + basis[1]*extrusion_length"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 239,
|
||
|
"metadata": {
|
||
|
"jupyter": {
|
||
|
"source_hidden": true
|
||
|
}
|
||
|
},
|
||
|
"outputs": [
|
||
|
{
|
||
|
"data": {
|
||
|
"application/vnd.jupyter.widget-view+json": {
|
||
|
"model_id": "239e8f7593324210860fb5c51704bbf6",
|
||
|
"version_major": 2,
|
||
|
"version_minor": 0
|
||
|
},
|
||
|
"text/plain": [
|
||
|
"HBox(children=(Label(value='Width :'), FloatSlider(value=1.0, max=3.0, min=0.25, step=0.01)))"
|
||
|
]
|
||
|
},
|
||
|
"metadata": {},
|
||
|
"output_type": "display_data"
|
||
|
},
|
||
|
{
|
||
|
"data": {
|
||
|
"application/vnd.jupyter.widget-view+json": {
|
||
|
"model_id": "f76f4a2c5c7e4a668be19be4aca8ee5b",
|
||
|
"version_major": 2,
|
||
|
"version_minor": 0
|
||
|
},
|
||
|
"text/plain": [
|
||
|
"Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…"
|
||
|
]
|
||
|
},
|
||
|
"metadata": {},
|
||
|
"output_type": "display_data"
|
||
|
}
|
||
|
],
|
||
|
"source": [
|
||
|
"def extrusion_end_renderer(path, width):\n",
|
||
|
" renderer, start = profile_renderer(path, width)\n",
|
||
|
" end = profile_mesh(path, width, basis[1]*extrusion_length, \"#0000ff\")\n",
|
||
|
" renderer.scene.add(end)\n",
|
||
|
" return renderer, start, end\n",
|
||
|
"\n",
|
||
|
"def run_extrusion_end_renderer(width):\n",
|
||
|
" renderer, start, end = extrusion_end_renderer(unit_profile, width)\n",
|
||
|
" def update_renderer(width):\n",
|
||
|
" start.geometry.exec_three_obj_method('dispose')\n",
|
||
|
" start.geometry = profile_geom(unit_profile, width['new'])\n",
|
||
|
" end.geometry.exec_three_obj_method('dispose')\n",
|
||
|
" end.geometry = profile_geom(unit_profile, width['new'], basis[1]*extrusion_length)\n",
|
||
|
" wwidth = widgets.FloatSlider(min=0.25, max=3.0, step=0.01, value=width)\n",
|
||
|
" wwidth.observe(update_renderer, 'value')\n",
|
||
|
" display(widgets.HBox(children=[\n",
|
||
|
" widgets.Label(\"Width :\"),\n",
|
||
|
" wwidth]))\n",
|
||
|
" display(renderer)\n",
|
||
|
"\n",
|
||
|
"run_extrusion_end_renderer(1.0)"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"### Bridge start and end paths to create triangle mesh"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 240,
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"def edges(path):\n",
|
||
|
" \"\"\" Generate edges from path vertices \"\"\"\n",
|
||
|
" for i in range(0, len(path)-1):\n",
|
||
|
" yield path[i:i+2]\n",
|
||
|
"\n",
|
||
|
"def bridge_quads(start, end):\n",
|
||
|
" \"\"\" Bridge two paths, generating quads \"\"\"\n",
|
||
|
" for [s0, s1], [e0, e1] in zip(edges(start), edges(end)):\n",
|
||
|
" yield [s0, s1, e1, e0]\n",
|
||
|
"\n",
|
||
|
"def bridge_tris(start, end):\n",
|
||
|
" \"\"\" Bridge two paths, generating triangles \"\"\"\n",
|
||
|
" for a, b, c, d in bridge_quads(start, end):\n",
|
||
|
" yield [a, b, c]\n",
|
||
|
" yield [a, c, d]\n",
|
||
|
" \n",
|
||
|
"def bridge_faces():\n",
|
||
|
" return bridge_quads(profile_ext_end, profile_ext)"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 241,
|
||
|
"metadata": {
|
||
|
"jupyter": {
|
||
|
"source_hidden": true
|
||
|
}
|
||
|
},
|
||
|
"outputs": [
|
||
|
{
|
||
|
"data": {
|
||
|
"application/vnd.jupyter.widget-view+json": {
|
||
|
"model_id": "62d6f81da24649c0aa1a73421144a1fb",
|
||
|
"version_major": 2,
|
||
|
"version_minor": 0
|
||
|
},
|
||
|
"text/plain": [
|
||
|
"Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…"
|
||
|
]
|
||
|
},
|
||
|
"metadata": {},
|
||
|
"output_type": "display_data"
|
||
|
}
|
||
|
],
|
||
|
"source": [
|
||
|
"def faces_to_mesh(faces):\n",
|
||
|
" geom = BufferGeometry(\n",
|
||
|
" attributes={\n",
|
||
|
" 'position': BufferAttribute(faces.astype(np.float32))\n",
|
||
|
" })\n",
|
||
|
" geom.exec_three_obj_method('computeVertexNormals')\n",
|
||
|
" return geom\n",
|
||
|
"\n",
|
||
|
"def face_outline(path):\n",
|
||
|
" line = np.copy(path).tolist()\n",
|
||
|
" line.append(line[0])\n",
|
||
|
" return BufferGeometry(\n",
|
||
|
" attributes={\n",
|
||
|
" 'position': BufferAttribute(np.array(line, dtype=np.float32))\n",
|
||
|
" }, normalized=False)\n",
|
||
|
"\n",
|
||
|
"def bridge_renderer(start, end):\n",
|
||
|
" renderer, _, _ = extrusion_end_renderer(unit_profile, 1.0)\n",
|
||
|
" renderer.scene.remove(renderer.scene.children[4])\n",
|
||
|
" geom = faces_to_mesh(np.array(list(bridge_tris(end, start))))\n",
|
||
|
" mtl = MeshStandardMaterial(\n",
|
||
|
" color=\"#66ffff\", \n",
|
||
|
" opacity=0.8, \n",
|
||
|
" transparent=True,\n",
|
||
|
" side='DoubleSide')\n",
|
||
|
" mesh = Mesh(geom, mtl)\n",
|
||
|
" renderer.scene.add(mesh)\n",
|
||
|
" geom = face_outline(face)\n",
|
||
|
" mtl = LineBasicMaterial(color=\"#000000\")\n",
|
||
|
" mesh = Line(geom, mtl)\n",
|
||
|
" renderer.scene.add(mesh)\n",
|
||
|
" return renderer\n",
|
||
|
" \n",
|
||
|
"display(bridge_renderer(profile_ext, profile_ext_end))"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"### Clip generated faces using input face"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"Polygon clipping is a big problem on its own, we need to use external library to save time. Here we are using [Cython CSG](https://github.com/tomturner/cython-csg) library for Python, it's an optimized port if PyCSG library by Tim Knip, which is a port of [csg.js](https://evanw.github.io/csg.js/) library by Evan W. "
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 242,
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"from _cython_csg import CSG, BSPNode, Polygon, Vertex"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 269,
|
||
|
"metadata": {},
|
||
|
"outputs": [],
|
||
|
"source": [
|
||
|
"def polys_to_csg(polys):\n",
|
||
|
" \"\"\" Convert sequence of polygons into CSG object \"\"\"\n",
|
||
|
" return CSG.fromPolygons([\n",
|
||
|
" Polygon([Vertex([x, y, z]) for x, y, z in poly])\n",
|
||
|
" for poly in polys\n",
|
||
|
" ])\n",
|
||
|
"\n",
|
||
|
"def poly_to_clipping_csg(poly, n, offset=100):\n",
|
||
|
" \"\"\" Create clipping CSG object by extruding sides of the polygon\n",
|
||
|
" along the vector n \"\"\"\n",
|
||
|
" noff = n*offset\n",
|
||
|
" def make_side(a, b):\n",
|
||
|
" return [\n",
|
||
|
" a - noff,\n",
|
||
|
" b - noff,\n",
|
||
|
" b + noff,\n",
|
||
|
" a + noff\n",
|
||
|
" ]\n",
|
||
|
" return polys_to_csg([\n",
|
||
|
" make_side(a, b) for a, b in edges(np.concatenate((poly, [poly[0]])))\n",
|
||
|
" ])\n",
|
||
|
"\n",
|
||
|
"def csg_to_polys(csg):\n",
|
||
|
" \"\"\" Convert CSG object to polygon sequence \"\"\"\n",
|
||
|
" return [\n",
|
||
|
" np.array([[p.pos.x, p.pos.y, p.pos.z] for p in poly.vertices])\n",
|
||
|
" for poly in csg.toPolygons()\n",
|
||
|
" ]\n",
|
||
|
"\n",
|
||
|
"def clip(A, B):\n",
|
||
|
" \"\"\" Custom clipping operation based on BSP tree node operations \"\"\"\n",
|
||
|
" a = BSPNode(A.clone().polygons)\n",
|
||
|
" b = BSPNode(B.clone().polygons)\n",
|
||
|
" b.invert()\n",
|
||
|
" a.clipTo(b)\n",
|
||
|
" return CSG.fromPolygons(a.allPolygons())\n",
|
||
|
"\n",
|
||
|
"bridge_csg = polys_to_csg(bridge_faces())\n",
|
||
|
"clipping_csg = poly_to_clipping_csg(face, TBN[2])\n",
|
||
|
"clipped_polys = csg_to_polys(clip(bridge_csg, clipping_csg))\n",
|
||
|
"\n",
|
||
|
"def triangulate3(poly):\n",
|
||
|
" \"\"\" Triangulate 3D polygon by first transforming it into XY plane,\n",
|
||
|
" and then computing Dealaunay triangulation. \"\"\"\n",
|
||
|
" tbn = np.array(poly_basis([0,0,1], poly))\n",
|
||
|
" poly_tbn = tbn.dot(poly.T).T[:,[0,1]]\n",
|
||
|
" indices = Delaunay(poly_tbn).simplices\n",
|
||
|
" return poly[indices]\n",
|
||
|
" \n",
|
||
|
"clipped_tris = np.concatenate(\n",
|
||
|
" [triangulate3(face) for face in clipped_polys], axis=0)\n"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "code",
|
||
|
"execution_count": 271,
|
||
|
"metadata": {
|
||
|
"jupyter": {
|
||
|
"source_hidden": true
|
||
|
}
|
||
|
},
|
||
|
"outputs": [
|
||
|
{
|
||
|
"data": {
|
||
|
"application/vnd.jupyter.widget-view+json": {
|
||
|
"model_id": "35a716badd8749edac90db265d88f4ff",
|
||
|
"version_major": 2,
|
||
|
"version_minor": 0
|
||
|
},
|
||
|
"text/plain": [
|
||
|
"Renderer(camera=PerspectiveCamera(aspect=1.5, position=(0.28830735601264745, 6.947213875632741, 9.114960387348…"
|
||
|
]
|
||
|
},
|
||
|
"metadata": {},
|
||
|
"output_type": "display_data"
|
||
|
}
|
||
|
],
|
||
|
"source": [
|
||
|
"def csg_result_renderer():\n",
|
||
|
" renderer, _, _ = extrusion_end_renderer(unit_profile, 1.0)\n",
|
||
|
" renderer.scene.remove(renderer.scene.children[4])\n",
|
||
|
" geom = faces_to_mesh(clipped_tris)\n",
|
||
|
" mtl = MeshStandardMaterial(\n",
|
||
|
" color=\"#66ffff\", \n",
|
||
|
" opacity=0.8, \n",
|
||
|
" transparent=True,\n",
|
||
|
" side='DoubleSide')\n",
|
||
|
" mesh = Mesh(geom, mtl)\n",
|
||
|
" renderer.scene.add(mesh)\n",
|
||
|
" geom = face_outline(face)\n",
|
||
|
" mtl = LineBasicMaterial(color=\"#000000\")\n",
|
||
|
" mesh = Line(geom, mtl)\n",
|
||
|
" renderer.scene.add(mesh)\n",
|
||
|
" return renderer\n",
|
||
|
" \n",
|
||
|
"display(csg_result_renderer())"
|
||
|
]
|
||
|
},
|
||
|
{
|
||
|
"cell_type": "markdown",
|
||
|
"metadata": {},
|
||
|
"source": [
|
||
|
"# The End"
|
||
|
]
|
||
|
}
|
||
|
],
|
||
|
"metadata": {
|
||
|
"kernelspec": {
|
||
|
"display_name": "Python 3",
|
||
|
"language": "python",
|
||
|
"name": "python3"
|
||
|
},
|
||
|
"language_info": {
|
||
|
"codemirror_mode": {
|
||
|
"name": "ipython",
|
||
|
"version": 3
|
||
|
},
|
||
|
"file_extension": ".py",
|
||
|
"mimetype": "text/x-python",
|
||
|
"name": "python",
|
||
|
"nbconvert_exporter": "python",
|
||
|
"pygments_lexer": "ipython3",
|
||
|
"version": "3.9.0"
|
||
|
}
|
||
|
},
|
||
|
"nbformat": 4,
|
||
|
"nbformat_minor": 4
|
||
|
}
|