def obj_to_ursinamesh(path=application.compressed_models_folder, outpath=application.compressed_models_folder, name='*', return_mesh=True, save_to_file=False, delete_obj=False): if name.endswith('.obj'): name = name[:-4] for f in path.glob(f'**/{name}.obj'): # filepath = path / (os.path.splitext(f)[0] + '.obj') print('read obj at:', f) with f.open('r') as file: lines = file.readlines() verts = [] tris = [] uv_indices = [] uvs = [] norm_indices = [] norms = [] normals = [] # final normals made getting norms with norm_indices vertex_colors = [] current_color = None mtl_data = None mtl_dict = {} # parse the obj file to a Mesh for i, l in enumerate(lines): if l.startswith('v '): vert = [float(v) for v in l[2:].strip().split(' ')] vert[0] = -vert[0] verts.append(tuple(vert)) elif l.startswith('vn '): n = l[3:].strip().split(' ') norms.append(tuple(float(e) for e in n)) elif l.startswith('vt '): uv = l[3:].strip() uv = uv.split(' ') uvs.append(tuple(float(e) for e in uv)) elif l.startswith('f '): l = l[2:] l = l.split(' ') try: tri = tuple( int(t.split('/')[0]) - 1 for t in l if t != '\n') except: print_warning('error in obj file line:', i, ':', l) return if len(tri) == 3: tris.extend(tri) if current_color: vertex_colors.extend([current_color for i in range(3)]) elif len(tri) == 4: tris.extend( (tri[0], tri[1], tri[2], tri[2], tri[3], tri[0])) if current_color: vertex_colors.extend([current_color for i in range(6)]) else: # ngon for i in range(1, len(tri) - 1): tris.extend((tri[i], tri[i + 1], tri[0])) if current_color: vertex_colors.extend( [current_color for i in range(len(tri))]) try: uv = tuple(int(t.split('/')[1]) - 1 for t in l) if len(uv) == 3: uv_indices.extend(uv) elif len(uv) == 4: uv_indices.extend( (uv[0], uv[1], uv[2], uv[2], uv[3], uv[0])) else: # ngon for i in range(1, len(uv) - 1): uv_indices.extend((uv[i], uv[i + 1], uv[0])) except: # if no uvs pass try: n = tuple(int(t.split('/')[2]) - 1 for t in l) if len(n) == 3: norm_indices.extend(n) elif len(uv) == 4: norm_indices.extend( (n[0], n[1], n[2], n[2], n[3], n[0])) else: # ngon for i in range(1, len(n) - 1): norm_indices.extend((n[i], n[i + 1], n[0])) except: # if no normals pass elif l.startswith('mtllib '): # load mtl file mtl_file_name = Path(str(f).rstrip('.obj') + '.mtl') if mtl_file_name.exists(): with open(mtl_file_name) as mtl_file: mtl_data = mtl_file.readlines() for i in range(len(mtl_data) - 1): if mtl_data[i].startswith('newmtl '): material_name = mtl_data[i].strip()[ 7:] # remove 'newmtl ' for j in range(i + 1, min(i + 8, len(mtl_data))): if mtl_data[j].startswith('newmtl'): break if mtl_data[j].startswith('Kd '): material_color = [ float(e) for e in mtl_data[j].strip()[3:].split(' ') ] mtl_dict[ material_name] = *material_color, 1 elif l.startswith('usemtl ') and mtl_data: # apply material color material_name = l[7:].strip() # remove 'usemtl ' if material_name in mtl_dict: current_color = mtl_dict[material_name] if norms: # make sure we have normals and not just normal indices (weird edge case). normals = [(-norms[nid][0], norms[nid][1], norms[nid][2]) for nid in norm_indices] if return_mesh: return Mesh(vertices=[verts[t] for t in tris], normals=normals, uvs=[uvs[uid] for uid in uv_indices], colors=vertex_colors) meshstring = '' meshstring += 'Mesh(' meshstring += '\nvertices=' meshstring += str(tuple(verts[t] for t in tris)) if vertex_colors: meshstring += '\ncolors=' meshstring += str(tuple(col for col in vertex_colors)) if uv_indices: meshstring += ', \nuvs=' meshstring += str(tuple(uvs[uid] for uid in uv_indices)) if normals: meshstring += ', \nnormals=' meshstring += str(normals) meshstring += ''', \nmode='triangle')''' if not save_to_file: return meshstring outfilepath = outpath / (os.path.splitext(f)[0] + '.ursinamesh') with open(outfilepath, 'w') as file: file.write(meshstring) if delete_obj: os.remove(filepath) print_info('saved ursinamesh to:', outfilepath)
class Raycaster(Entity): line_model = Mesh(vertices=[Vec3(0, 0, 0), Vec3(0, 0, 1)], mode='line') _boxcast_box = Entity(model='cube', origin_z=-.5, collider='box', color=color.white33, enabled=False) def __init__(self): super().__init__(name='raycaster', eternal=True) self._picker = CollisionTraverser() # Make a traverser self._pq = CollisionHandlerQueue() # Make a handler self._pickerNode = CollisionNode('raycaster') self._pickerNode.set_into_collide_mask(0) self._pickerNP = self.attach_new_node(self._pickerNode) self._picker.addCollider(self._pickerNP, self._pq) def distance(self, a, b): return sqrt(sum((a - b)**2 for a, b in zip(a, b))) def raycast(self, origin, direction=(0, 0, 1), distance=inf, traverse_target=scene, ignore=list(), debug=False): self.position = origin self.look_at(self.position + direction) self._pickerNode.clearSolids() ray = CollisionRay() ray.setOrigin(Vec3(0, 0, 0)) ray.setDirection(Vec3(0, 0, 1)) self._pickerNode.addSolid(ray) if debug: temp = Entity(position=origin, model=Raycaster.line_model, scale=Vec3(1, 1, min(distance, 9999)), add_to_scene_entities=False) temp.look_at(self.position + direction) destroy(temp, 1 / 30) self._picker.traverse(traverse_target) if self._pq.get_num_entries() == 0: self.hit = HitInfo(hit=False, distance=distance) return self.hit ignore += tuple([e for e in scene.entities if not e.collision]) self._pq.sort_entries() self.entries = [ # filter out ignored entities e for e in self._pq.getEntries() if e.get_into_node_path().parent not in ignore and self.distance( self.world_position, Vec3( *e.get_surface_point(render))) <= distance ] if len(self.entries) == 0: self.hit = HitInfo(hit=False, distance=distance) return self.hit self.collision = self.entries[0] nP = self.collision.get_into_node_path().parent point = Vec3(*self.collision.get_surface_point(nP)) world_point = Vec3(*self.collision.get_surface_point(render)) hit_dist = self.distance(self.world_position, world_point) self.hit = HitInfo(hit=True, distance=distance) for e in scene.entities: if e == nP: self.hit.entity = e nPs = [e.get_into_node_path().parent for e in self.entries] self.hit.entities = [e for e in scene.entities if e in nPs] self.hit.point = point self.hit.world_point = world_point self.hit.distance = hit_dist self.hit.normal = Vec3(*self.collision.get_surface_normal( self.collision.get_into_node_path().parent).normalized()) self.hit.world_normal = Vec3( *self.collision.get_surface_normal(render).normalized()) return self.hit self.hit = HitInfo(hit=False, distance=distance) return self.hit def boxcast(self, origin, direction=(0, 0, 1), distance=9999, thickness=(1, 1), traverse_target=scene, ignore=list(), debug=False): # similar to raycast, but with width and height if isinstance(thickness, (int, float, complex)): thickness = (thickness, thickness) Raycaster._boxcast_box.enabled = True Raycaster._boxcast_box.collision = True Raycaster._boxcast_box.position = origin Raycaster._boxcast_box.scale = Vec3(abs(thickness[0]), abs(thickness[1]), abs(distance)) Raycaster._boxcast_box.always_on_top = debug Raycaster._boxcast_box.visible = debug Raycaster._boxcast_box.look_at(origin + direction) hit_info = Raycaster._boxcast_box.intersects( traverse_target=traverse_target, ignore=ignore) if hit_info.world_point: hit_info.distance = ursinamath.distance(origin, hit_info.world_point) else: hit_info.distance = distance if debug: Raycaster._boxcast_box.collision = False Raycaster._boxcast_box.scale_z = hit_info.distance invoke(setattr, Raycaster._boxcast_box, 'enabled', False, delay=.2) else: Raycaster._boxcast_box.enabled = False return hit_info
def obj_to_ursinamesh(path=application.compressed_models_folder, outpath=application.compressed_models_folder, name='*', return_mesh=True, save_to_file=False, delete_obj=False): if name.endswith('.obj'): name = name[:-4] for f in path.glob(f'**/{name}.obj'): filepath = path / (os.path.splitext(f)[0] + '.obj') print('read obj at:', filepath) with open(filepath, 'r') as file: lines = file.readlines() verts = list() tris = list() uv_indices = list() uvs = list() norm_indices = list() norms = list() # parse the obj file to a Mesh for i, l in enumerate(lines): if l.startswith('v '): vert = [float(v) for v in l[2:].strip().split(' ')] vert[0] = -vert[0] verts.append(tuple(vert)) elif l.startswith('vn '): n = l[3:].strip().split(' ') norms.append(tuple([float(e) for e in n])) elif l.startswith('vt '): uv = l[3:].strip() uv = uv.split(' ') uvs.append(tuple([float(e) for e in uv])) elif l.startswith('f '): l = l[2:] l = l.split(' ') try: tri = tuple( [int(t.split('/')[0]) - 1 for t in l if t != '\n']) except: print('error in obj file line:', i, ':', l) return if len(tri) == 3: tris.extend(tri) elif len(tri) == 4: tris.extend( (tri[0], tri[1], tri[2], tri[2], tri[3], tri[0])) else: # ngon for i in range(1, len(tri) - 1): tris.extend((tri[i], tri[i + 1], tri[0])) try: uv = tuple([int(t.split('/')[1]) - 1 for t in l]) if len(uv) == 3: uv_indices.extend(uv) elif len(uv) == 4: uv_indices.extend( (uv[0], uv[1], uv[2], uv[2], uv[3], uv[0])) else: # ngon for i in range(1, len(uv) - 1): uv_indices.extend((uv[i], uv[i + 1], uv[0])) except: # if no uvs pass try: n = tuple([int(t.split('/')[2]) - 1 for t in l]) if len(n) == 3: norm_indices.extend(n) elif len(uv) == 4: norm_indices.extend( (n[0], n[1], n[2], n[2], n[3], n[0])) else: # ngon for i in range(1, len(n) - 1): norm_indices.extend((n[i], n[i + 1], n[0])) except: # if no normals pass if return_mesh: return Mesh(vertices=[verts[t] for t in tris], normals=[norms[nid] for nid in norm_indices], uvs=[uvs[uid] for uid in uv_indices]) meshstring = '' meshstring += 'Mesh(' meshstring += '\nvertices=' meshstring += str(tuple([verts[t] for t in tris])) if uv_indices: meshstring += ', \nuvs=' meshstring += str(tuple([uvs[uid] for uid in uv_indices])) if norm_indices: meshstring += ', \nnormals=' meshstring += str(tuple([norms[nid] for nid in norm_indices])) meshstring += ''', \nmode='triangle')''' if not save_to_file: return meshstring outfilepath = outpath / (os.path.splitext(f)[0] + '.ursinamesh') with open(outfilepath, 'w') as file: file.write(meshstring) if delete_obj: os.remove(filepath) print('saved ursinamesh to:', outfilepath)