def make_cases_obj(): """Writes obj files demonstrating the main cases of marching cubes""" import marching_cubes_gen as gen mesh = Mesh() highlights = Mesh() offset = V3(0, 0, 0) for bits, faces in sorted(gen.BASE_CASES.items()): verts = set(gen.bits_to_verts(bits)) # Run marching cubes for a just this case def f(x, y, z): vert = gen.VERTICES.index((x, y, z)) return 1 if vert in verts else -1 case_mesh = marching_cubes_3d_single_cell(f, 0, 0, 0) case_mesh = case_mesh.translate(offset) mesh.extend(case_mesh) # Output the solid verts highlight = Mesh([V3(*gen.VERTICES[v]) for v in verts], []) highlight = highlight.translate(offset) highlights.extend(highlight) offset.x += 1.5 if offset.x > 6: offset.x = 0 offset.y += 1.5 with open("cases.obj", "w") as f: make_obj(f, mesh) with open("case_highlights.obj", "w") as f: make_obj(f, highlights)
def edge_to_boundary_vertex(edge): """Returns the vertex in the middle of the specified edge""" # Find the two vertices specified by this edge, and interpolate between # them according to adapt, as in the 2d case v0, v1 = EDGES[edge] f0 = f_eval[v0] f1 = f_eval[v1] t0 = 1 - adapt(f0, f1) t1 = 1 - t0 vert_pos0 = VERTICES[v0] vert_pos1 = VERTICES[v1] return V3(x + vert_pos0[0] * t0 + vert_pos1[0] * t1, y + vert_pos0[1] * t0 + vert_pos1[1] * t1, z + vert_pos0[2] * t0 + vert_pos1[2] * t1)
def norm(x, y, z): num_samples = 1000 sample_points = [] for i in range(0, num_samples): sample_point = V3(0, 0, 0) u = random.randint(0, 1000) / 1000.0 v = random.randint(0, 1000) / 1000.0 pi = 4 * math.atan(1) theta = 2.0 * pi * u phi = math.acos(2.0 * v - 1.0) # generate pseudorandom location on 2-sphere of radius d sample_point.x = d * math.cos(theta) * math.sin(phi) sample_point.y = d * math.sin(theta) * math.sin(phi) sample_point.z = d * math.cos(phi) sample_points.append(sample_point) normal = V3(0, 0, 0) for sample_counter in range(0, len(sample_points)): if f(sample_points[sample_counter].x, sample_points[sample_counter].y, sample_points[sample_counter].z) < 0: normal.x += -sample_points[sample_counter].x normal.y += -sample_points[sample_counter].y normal.z += -sample_points[sample_counter].z if 0 == normal.x * normal.x + normal.y * normal.y + normal.z * normal.z: normal.x = 1 return normal.normalize()
def dual_contour_3d_find_best_vertex(f, f_normal, x, y, z): if not ADAPTIVE: return V3(x + 0.5, y + 0.5, z + 0.5) # Evaluate f at each corner v = np.empty((2, 2, 2)) for dx in (0, 1): for dy in (0, 1): for dz in (0, 1): v[dx, dy, dz] = f(x + dx, y + dy, z + dz) # For each edge, identify where there is a sign change. # There are 4 edges along each of the three axes changes = [] for dx in (0, 1): for dy in (0, 1): if (v[dx, dy, 0] > 0) != (v[dx, dy, 1] > 0): changes.append( (x + dx, y + dy, z + adapt(v[dx, dy, 0], v[dx, dy, 1]))) for dx in (0, 1): for dz in (0, 1): if (v[dx, 0, dz] > 0) != (v[dx, 1, dz] > 0): changes.append( (x + dx, y + adapt(v[dx, 0, dz], v[dx, 1, dz]), z + dz)) for dy in (0, 1): for dz in (0, 1): if (v[0, dy, dz] > 0) != (v[1, dy, dz] > 0): changes.append( (x + adapt(v[0, dy, dz], v[1, dy, dz]), y + dy, z + dz)) if len(changes) <= 1: return None # For each sign change location v[i], we find the normal n[i]. # The error term we are trying to minimize is sum( dot(x-v[i], n[i]) ^ 2) # In other words, minimize || A * x - b || ^2 where A and b are a matrix and vector # derived from v and n normals = [] for v in changes: n = f_normal(v[0], v[1], v[2]) normals.append([n.x, n.y, n.z]) return solve_qef_3d(x, y, z, changes, normals)
def solve_qef_3d(x, y, z, positions, normals): # The error term we are trying to minimize is sum( dot(x-v[i], n[i]) ^ 2) # This should be minimized over the unit square with top left point (x, y) # In other words, minimize || A * x - b || ^2 where A and b are a matrix and vector # derived from v and n # The heavy lifting is done by the QEF class, but this function includes some important # tricks to cope with edge cases # This is demonstration code and isn't optimized, there are many good C++ implementations # out there if you need speed. if settings.BIAS: # Add extra normals that add extra error the further we go # from the cell, this encourages the final result to be # inside the cell # These normals are shorter than the input normals # as that makes the bias weaker, we want them to only # really be important when the input is ambiguous # Take a simple average of positions as the point we will # pull towards. mass_point = numpy.mean(positions, axis=0) normals.append([settings.BIAS_STRENGTH, 0, 0]) positions.append(mass_point) normals.append([0, settings.BIAS_STRENGTH, 0]) positions.append(mass_point) normals.append([0, 0, settings.BIAS_STRENGTH]) positions.append(mass_point) qef = QEF.make_3d(positions, normals) residual, v = qef.solve() if settings.BOUNDARY: def inside(r): return x <= r[1][0] <= x + 1 and y <= r[1][1] <= y + 1 and z <= r[ 1][2] <= z + 1 # It's entirely possible that the best solution to the qef is not actually # inside the cell. if not inside((residual, v)): # If so, we constrain the the qef to the 6 # planes bordering the cell, and find the best point of those r1 = qef.fix_axis(0, x + 0).solve() r2 = qef.fix_axis(0, x + 1).solve() r3 = qef.fix_axis(1, y + 0).solve() r4 = qef.fix_axis(1, y + 1).solve() r5 = qef.fix_axis(2, z + 0).solve() r6 = qef.fix_axis(2, z + 1).solve() rs = list(filter(inside, [r1, r2, r3, r4, r5, r6])) if len(rs) == 0: # It's still possible that those planes (which are infinite) # cause solutions outside the box. # So now try the 12 lines bordering the cell r1 = qef.fix_axis(1, y + 0).fix_axis(0, x + 0).solve() r2 = qef.fix_axis(1, y + 1).fix_axis(0, x + 0).solve() r3 = qef.fix_axis(1, y + 0).fix_axis(0, x + 1).solve() r4 = qef.fix_axis(1, y + 1).fix_axis(0, x + 1).solve() r5 = qef.fix_axis(2, z + 0).fix_axis(0, x + 0).solve() r6 = qef.fix_axis(2, z + 1).fix_axis(0, x + 0).solve() r7 = qef.fix_axis(2, z + 0).fix_axis(0, x + 1).solve() r8 = qef.fix_axis(2, z + 1).fix_axis(0, x + 1).solve() r9 = qef.fix_axis(2, z + 0).fix_axis(1, y + 0).solve() r10 = qef.fix_axis(2, z + 1).fix_axis(1, y + 0).solve() r11 = qef.fix_axis(2, z + 0).fix_axis(1, y + 1).solve() r12 = qef.fix_axis(2, z + 1).fix_axis(1, y + 1).solve() rs = list( filter( inside, [r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12])) if len(rs) == 0: # So finally, we evaluate which corner # of the cell looks best r1 = qef.eval_with_pos((x + 0, y + 0, z + 0)) r2 = qef.eval_with_pos((x + 0, y + 0, z + 1)) r3 = qef.eval_with_pos((x + 0, y + 1, z + 0)) r4 = qef.eval_with_pos((x + 0, y + 1, z + 1)) r5 = qef.eval_with_pos((x + 1, y + 0, z + 0)) r6 = qef.eval_with_pos((x + 1, y + 0, z + 1)) r7 = qef.eval_with_pos((x + 1, y + 1, z + 0)) r8 = qef.eval_with_pos((x + 1, y + 1, z + 1)) rs = list(filter(inside, [r1, r2, r3, r4, r5, r6, r7, r8])) # Pick the best of the available options residual, v = min(rs) if settings.CLIP: # Crudely force v to be inside the cell v[0] = numpy.clip(v[0], x, x + 1) v[1] = numpy.clip(v[1], y, y + 1) v[2] = numpy.clip(v[2], z, z + 1) return V3(v[0], v[1], v[2])
def norm(x, y, z): return V3( (f(x + d, y, z) - f(x - d, y, z)) / 2 / d, (f(x, y + d, z) - f(x, y - d, z)) / 2 / d, (f(x, y, z + d) - f(x, y, z - d)) / 2 / d, ).normalize()
def circle_normal(x, y, z): l = math.sqrt(x * x + y * y + z * z) return V3(-x / l, -y / l, -z / l)
def get_normal_from_mc( src_string, src_z_w, src_C_x, src_C_y, src_C_z, src_C_w, src_max_iterations, src_threshold, src_x_grid_min, src_x_grid_max, src_y_grid_min, src_y_grid_max, src_z_grid_min, src_z_grid_max, src_x_res, src_y_res, src_z_res, src_make_border, src_write_triangles): float3 = ct.c_float * 3 int1 = ct.c_uint float1 = ct.c_float z_w = float1(src_z_w) C_x = float1(src_C_x) C_y = float1(src_C_y) C_z = float1(src_C_z) C_w = float1(src_C_w) max_iterations = int1(src_max_iterations) threshold = float1(src_threshold) x_grid_min = float1(src_x_grid_min) x_grid_max = float1(src_x_grid_max) y_grid_min = float1(src_y_grid_min) y_grid_max = float1(src_y_grid_max) z_grid_min = float1(src_z_grid_min) z_grid_max = float1(src_z_grid_max) x_res = int1(src_x_res) y_res = int1(src_y_res) z_res = int1(src_z_res) output_arr = float3() make_border = int1(src_make_border) write_triangles = int1(src_write_triangles) lib = ct.CDLL("mc_dll.dll") lib.get_normal(ct.c_char_p(src_string.encode('utf-8')), z_w, C_x, C_y, C_z, C_w, max_iterations, threshold, x_grid_min, x_grid_max, y_grid_min, y_grid_max, z_grid_min, z_grid_max, x_res, y_res, z_res, output_arr, make_border, write_triangles) ret = V3(output_arr[0], output_arr[1], output_arr[2]) return ret