feat: 🔖 POC ThreeJS <-> Open3D
This commit is contained in:
+6
-1
@@ -5,6 +5,8 @@ import torch
|
||||
import folder_paths
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
from .geo_tools import mesh_to_json
|
||||
import open3d as o3d
|
||||
|
||||
|
||||
class Debug:
|
||||
@@ -23,7 +25,7 @@ class Debug:
|
||||
|
||||
def do_debug(self, **kwargs):
|
||||
output = {
|
||||
"ui": {"b64_images": [], "text": []},
|
||||
"ui": {"b64_images": [], "text": [], "geometry": []},
|
||||
"result": ("A"),
|
||||
}
|
||||
for k, v in kwargs.items():
|
||||
@@ -49,6 +51,9 @@ class Debug:
|
||||
elif isinstance(anything, bool):
|
||||
log.debug(f"Input {k} contains boolean: {anything}")
|
||||
output["ui"]["text"] += ["True" if anything else "False"]
|
||||
elif isinstance(anything, o3d.geometry.Geometry):
|
||||
log.debug(f"Input {k} contains geometry")
|
||||
output["ui"]["geometry"] += [mesh_to_json(anything)]
|
||||
else:
|
||||
text = str(anything)
|
||||
log.debug(f"Input {k} contains text: {text}")
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
import itertools
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import json
|
||||
from ..utils import log
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def euler_to_rotation_matrix(x_deg, y_deg, z_deg):
|
||||
# Convert degrees to radians
|
||||
x = np.radians(x_deg)
|
||||
y = np.radians(y_deg)
|
||||
z = np.radians(z_deg)
|
||||
|
||||
# Rotation matrix around x-axis
|
||||
Rx = np.array([[1, 0, 0], [0, np.cos(x), -np.sin(x)], [0, np.sin(x), np.cos(x)]])
|
||||
|
||||
# Rotation matrix around y-axis
|
||||
Ry = np.array([[np.cos(y), 0, np.sin(y)], [0, 1, 0], [-np.sin(y), 0, np.cos(y)]])
|
||||
|
||||
# Rotation matrix around z-axis
|
||||
Rz = np.array([[np.cos(z), -np.sin(z), 0], [np.sin(z), np.cos(z), 0], [0, 0, 1]])
|
||||
|
||||
return Rz @ Ry @ Rx
|
||||
|
||||
|
||||
def rotate_mesh(mesh, x_deg, y_deg, z_deg, center=None):
|
||||
R = euler_to_rotation_matrix(x_deg, y_deg, z_deg)
|
||||
return mesh.rotate(R, center) if center is not None else mesh.rotate(R)
|
||||
|
||||
|
||||
def get_transformation_matrix(position, rotation, scale):
|
||||
# Construct the translation matrix
|
||||
T = np.eye(4)
|
||||
T[:3, 3] = position
|
||||
|
||||
# Get the rotation matrix from Euler angles
|
||||
R = euler_to_rotation_matrix(*rotation)
|
||||
R_homo = np.eye(4)
|
||||
R_homo[:3, :3] = R
|
||||
|
||||
# Construct the scaling matrix
|
||||
S = np.eye(4)
|
||||
S[0, 0] = scale[0]
|
||||
S[1, 1] = scale[1]
|
||||
S[2, 2] = scale[2]
|
||||
|
||||
# Combined transforms
|
||||
return T @ R_homo @ S
|
||||
|
||||
|
||||
def mesh_to_json(mesh: o3d.geometry.MeshBase):
|
||||
"""Convert an Open3D mesh to JSON."""
|
||||
mesh_dict = {
|
||||
"vertices": np.asarray(mesh.vertices).tolist(),
|
||||
"triangles": np.asarray(mesh.triangles).tolist(),
|
||||
}
|
||||
|
||||
if mesh.has_vertex_normals():
|
||||
mesh_dict["vertex_normals"] = np.asarray(mesh.vertex_normals).tolist()
|
||||
|
||||
if mesh.has_vertex_colors():
|
||||
mesh_dict["vertex_colors"] = np.asarray(mesh.vertex_colors).tolist()
|
||||
|
||||
if mesh.has_triangle_uvs():
|
||||
mesh_dict["triangle_uvs"] = np.asarray(mesh.triangle_uvs).tolist()
|
||||
|
||||
return json.dumps(mesh_dict)
|
||||
|
||||
|
||||
def create_grid(scale=(1, 1, 1), rows=10, columns=10):
|
||||
dx, dy, dz = scale
|
||||
|
||||
# Create vertices
|
||||
vertices = []
|
||||
for i in np.linspace(-dy / 2, dy / 2, rows + 1):
|
||||
vertices.extend([j, 0, i] for j in np.linspace(-dx / 2, dx / 2, columns + 1))
|
||||
# Generate triangles
|
||||
triangles = []
|
||||
for i, j in itertools.product(range(rows), range(columns)):
|
||||
p1 = i * (columns + 1) + j
|
||||
p2 = i * (columns + 1) + j + 1
|
||||
p3 = (i + 1) * (columns + 1) + j
|
||||
p4 = (i + 1) * (columns + 1) + j + 1
|
||||
|
||||
triangles.extend(([p1, p2, p3], [p2, p4, p3]))
|
||||
vertices = o3d.utility.Vector3dVector(np.array(vertices))
|
||||
triangles = o3d.utility.Vector3iVector(np.array(triangles))
|
||||
mesh = o3d.geometry.TriangleMesh(vertices, triangles)
|
||||
|
||||
mesh.compute_vertex_normals()
|
||||
|
||||
return mesh
|
||||
|
||||
|
||||
def create_box(scale=(1, 1, 1), divisions=(1, 1, 1)):
|
||||
dx, dy, dz = scale
|
||||
div_x, div_y, div_z = divisions
|
||||
|
||||
vertices = []
|
||||
for i in np.linspace(-dx / 2, dx / 2, div_x + 1):
|
||||
for j in np.linspace(-dy / 2, dy / 2, div_y + 1):
|
||||
vertices.extend([i, j, k] for k in np.linspace(-dz / 2, dz / 2, div_z + 1))
|
||||
# Generate triangles for the box faces
|
||||
triangles = []
|
||||
for x, y in itertools.product(range(div_x), range(div_y)):
|
||||
for z in range(div_z):
|
||||
# Define base index for this cube
|
||||
base = z * (div_x + 1) * (div_y + 1) + y * (div_x + 1) + x
|
||||
|
||||
# Indices for the 8 vertices of the cube
|
||||
v0 = base
|
||||
v1 = base + 1
|
||||
v2 = base + (div_x + 1) + 1
|
||||
v3 = base + (div_x + 1)
|
||||
v4 = base + (div_x + 1) * (div_y + 1)
|
||||
v5 = v4 + 1
|
||||
v6 = v4 + (div_x + 1) + 1
|
||||
v7 = v4 + (div_x + 1)
|
||||
|
||||
triangles.extend(
|
||||
(
|
||||
[v0, v1, v2],
|
||||
[v2, v3, v0],
|
||||
[v4, v5, v6],
|
||||
[v6, v7, v4],
|
||||
[v0, v3, v7],
|
||||
[v7, v4, v0],
|
||||
[v1, v5, v6],
|
||||
[v6, v2, v1],
|
||||
[v0, v4, v5],
|
||||
[v5, v1, v0],
|
||||
[v3, v2, v6],
|
||||
[v6, v7, v3],
|
||||
)
|
||||
)
|
||||
vertices = o3d.utility.Vector3dVector(np.array(vertices))
|
||||
triangles = o3d.utility.Vector3iVector(np.array(triangles))
|
||||
mesh = o3d.geometry.TriangleMesh(vertices, triangles)
|
||||
|
||||
mesh.compute_vertex_normals()
|
||||
|
||||
return mesh
|
||||
|
||||
|
||||
def create_sphere(radius=1, columns=10, rows=10):
|
||||
# Create vertex positions
|
||||
vertices = []
|
||||
for i in range(rows + 1):
|
||||
lat = i * np.pi / rows
|
||||
sin_lat = np.sin(lat)
|
||||
cos_lat = np.cos(lat)
|
||||
|
||||
for j in range(columns + 1):
|
||||
lon = j * 2 * np.pi / columns
|
||||
sin_lon = np.sin(lon)
|
||||
cos_lon = np.cos(lon)
|
||||
|
||||
x = radius * cos_lon * sin_lat
|
||||
y = radius * cos_lat
|
||||
z = radius * sin_lon * sin_lat
|
||||
vertices.append([x, y, z])
|
||||
|
||||
# Create triangles
|
||||
triangles = []
|
||||
for i in range(rows):
|
||||
for j in range(columns):
|
||||
p1 = i * (columns + 1) + j
|
||||
p2 = i * (columns + 1) + j + 1
|
||||
p3 = (i + 1) * (columns + 1) + j
|
||||
p4 = (i + 1) * (columns + 1) + j + 1
|
||||
|
||||
triangles.extend(([p1, p2, p3], [p2, p4, p3]))
|
||||
vertices = o3d.utility.Vector3dVector(np.array(vertices))
|
||||
triangles = o3d.utility.Vector3iVector(np.array(triangles))
|
||||
mesh = o3d.geometry.TriangleMesh(vertices, triangles)
|
||||
|
||||
# Assigning random colors to vertices
|
||||
N = len(vertices)
|
||||
mesh.vertex_colors = o3d.utility.Vector3dVector(
|
||||
np.random.uniform(0, 1, size=(N, 3))
|
||||
)
|
||||
mesh.compute_vertex_normals()
|
||||
|
||||
return mesh
|
||||
|
||||
|
||||
def create_torus(torus_radius=1, ring_radius=0.5, rows=10, columns=10):
|
||||
vertices = []
|
||||
for i in range(rows + 1):
|
||||
theta = i * 2 * np.pi / rows
|
||||
cos_theta = np.cos(theta)
|
||||
sin_theta = np.sin(theta)
|
||||
|
||||
circle_center = torus_radius + ring_radius * cos_theta
|
||||
|
||||
for j in range(columns + 1):
|
||||
phi = j * 2 * np.pi / columns
|
||||
cos_phi = np.cos(phi)
|
||||
sin_phi = np.sin(phi)
|
||||
|
||||
x = circle_center * cos_phi
|
||||
y = ring_radius * sin_theta
|
||||
z = circle_center * sin_phi
|
||||
vertices.append([x, y, z])
|
||||
|
||||
triangles = []
|
||||
for i in range(rows):
|
||||
for j in range(columns):
|
||||
p1 = i * (columns + 1) + j
|
||||
p2 = i * (columns + 1) + j + 1
|
||||
p3 = (i + 1) * (columns + 1) + j
|
||||
p4 = (i + 1) * (columns + 1) + j + 1
|
||||
|
||||
triangles.extend(([p1, p2, p3], [p2, p4, p3]))
|
||||
vertices = o3d.utility.Vector3dVector(np.array(vertices))
|
||||
triangles = o3d.utility.Vector3iVector(np.array(triangles))
|
||||
mesh = o3d.geometry.TriangleMesh(vertices, triangles)
|
||||
|
||||
mesh.compute_vertex_normals()
|
||||
|
||||
return mesh
|
||||
|
||||
|
||||
# class GeoPrimitive:
|
||||
# """Primitive 3D geometry"""
|
||||
|
||||
# @classmethod
|
||||
# def INPUT_TYPES(cls):
|
||||
# return {
|
||||
# "required": {
|
||||
# "kind": (["Box", "Sphere", "Cylinder", "Torus"], {"default": "Box"})
|
||||
# }
|
||||
# }
|
||||
|
||||
# RETURN_TYPES = ("UV_MAP",)
|
||||
# RETURN_NAMES = ("uv_map",)
|
||||
# FUNCTION = "distort_uvs"
|
||||
# CATEGORY = "mtb/uv"
|
||||
|
||||
|
||||
class TransformGeometry:
|
||||
"""Transforms the input geometry"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"mesh": ("GEOMETRY",),
|
||||
"position_x": ("FLOAT", {"default": 0.0, "step": 0.1}),
|
||||
"position_y": ("FLOAT", {"default": 0.0, "step": 0.1}),
|
||||
"position_z": ("FLOAT", {"default": 0.0, "step": 0.1}),
|
||||
"rotation_x": ("FLOAT", {"default": 0.0, "step": 1}),
|
||||
"rotation_y": ("FLOAT", {"default": 0.0, "step": 1}),
|
||||
"rotation_z": ("FLOAT", {"default": 0.0, "step": 1}),
|
||||
"scale_x": ("FLOAT", {"default": 1.0, "step": 0.1}),
|
||||
"scale_y": ("FLOAT", {"default": 1.0, "step": 0.1}),
|
||||
"scale_z": ("FLOAT", {"default": 1.0, "step": 0.1}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("GEOMETRY",)
|
||||
RETURN_NAMES = ("geometry",)
|
||||
FUNCTION = "transform_geometry"
|
||||
CATEGORY = "mtb/3D"
|
||||
|
||||
def transform_geometry(
|
||||
self,
|
||||
mesh,
|
||||
position_x=0.0,
|
||||
position_y=0.0,
|
||||
position_z=0.0,
|
||||
rotation_x=0,
|
||||
rotation_y=0,
|
||||
rotation_z=0,
|
||||
scale_x=1,
|
||||
scale_y=1,
|
||||
scale_z=1,
|
||||
):
|
||||
# mesh = o3d.geometry.TriangleMesh.create_box(
|
||||
# width,
|
||||
# height,
|
||||
# depth,
|
||||
|
||||
# )
|
||||
# mesh.compute_vertex_normals()
|
||||
|
||||
position = np.array([position_x, position_y, position_z])
|
||||
rotation = (rotation_x, rotation_y, rotation_z)
|
||||
scale = np.array([scale_x, scale_y, scale_z])
|
||||
|
||||
transformation_matrix = get_transformation_matrix(position, rotation, scale)
|
||||
return (mesh.transform(transformation_matrix),)
|
||||
|
||||
|
||||
class GeometrySphere:
|
||||
"""Makes a Sphere 3D geometry"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"create_uv_map": ("BOOLEAN", {"default": True}),
|
||||
"radius": ("FLOAT", {"default": 1.0, "step": 0.1}),
|
||||
"resolution": ("INT", {"default": 20, "min": 1}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("GEOMETRY",)
|
||||
RETURN_NAMES = ("geometry",)
|
||||
FUNCTION = "make_sphere"
|
||||
CATEGORY = "mtb/3D"
|
||||
|
||||
def make_sphere(self, create_uv_map, radius, resolution):
|
||||
mesh = o3d.geometry.TriangleMesh.create_sphere(
|
||||
radius,
|
||||
resolution,
|
||||
create_uv_map,
|
||||
)
|
||||
mesh.compute_vertex_normals()
|
||||
|
||||
return (mesh,)
|
||||
|
||||
|
||||
class GeometryTest:
|
||||
"""Fetches an Open3D data geometry"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"name": (
|
||||
[
|
||||
"ArmadilloMesh",
|
||||
"AvocadoModel",
|
||||
"BunnyMesh",
|
||||
"CrateModel",
|
||||
"DamagedHelmetModel",
|
||||
"FlightHelmetModel",
|
||||
"KnotMesh",
|
||||
"MonkeyModel",
|
||||
"SwordModel",
|
||||
],
|
||||
{
|
||||
"default": "KnotMesh",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("GEOMETRY",)
|
||||
RETURN_NAMES = ("geometry",)
|
||||
FUNCTION = "fetch_data"
|
||||
CATEGORY = "mtb/3D"
|
||||
|
||||
def fetch_data(self, name):
|
||||
model = getattr(o3d.data, name)()
|
||||
mesh = o3d.io.read_triangle_mesh(model.path)
|
||||
mesh.compute_vertex_normals()
|
||||
return (mesh,)
|
||||
|
||||
|
||||
class GeometryBox:
|
||||
"""Makes a Box 3D geometry"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
# "create_uv_map": ("BOOLEAN", {"default": True}),
|
||||
"uniform_scale": ("FLOAT", {"default": 1.0, "step": 0.1}),
|
||||
"width": ("FLOAT", {"default": 1.0, "step": 0.05}),
|
||||
"height": ("FLOAT", {"default": 1.0, "step": 0.05}),
|
||||
"depth": ("FLOAT", {"default": 1.0, "step": 0.05}),
|
||||
"divisions_x": ("INT", {"default": 1}),
|
||||
"divisions_y": ("INT", {"default": 1}),
|
||||
"divisions_z": ("INT", {"default": 1}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("GEOMETRY",)
|
||||
RETURN_NAMES = ("geometry",)
|
||||
FUNCTION = "make_box"
|
||||
CATEGORY = "mtb/3D"
|
||||
|
||||
def make_box(
|
||||
self,
|
||||
uniform_scale,
|
||||
width,
|
||||
height,
|
||||
depth,
|
||||
divisions_x,
|
||||
divisions_y,
|
||||
divisions_z,
|
||||
):
|
||||
width, height, depth = (width, height, depth) * uniform_scale
|
||||
|
||||
# mesh = o3d.geometry.TriangleMesh.create_box(
|
||||
# width,
|
||||
# height,
|
||||
# depth,
|
||||
|
||||
# )
|
||||
# mesh.compute_vertex_normals()
|
||||
|
||||
mesh = create_box(
|
||||
(width, height, depth), (divisions_x, divisions_y, divisions_z)
|
||||
)
|
||||
|
||||
return (mesh,)
|
||||
|
||||
|
||||
class LoadGeometry:
|
||||
"""Load a 3D geometry"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"path": ("STRING", {"default": ""})}}
|
||||
|
||||
RETURN_TYPES = ("GEOMETRY",)
|
||||
RETURN_NAMES = ("geometry",)
|
||||
FUNCTION = "load_geo"
|
||||
CATEGORY = "mtb/3D"
|
||||
|
||||
def load_geo(self, path):
|
||||
if not os.path.exists(path):
|
||||
raise ValueError(f"Path {path} does not exist")
|
||||
|
||||
mesh = o3d.io.read_triangle_mesh(path)
|
||||
mesh.compute_vertex_normals()
|
||||
|
||||
return {
|
||||
"result": (mesh,),
|
||||
}
|
||||
|
||||
|
||||
class GeometryInfo:
|
||||
"""Retrieve information about a 3D geometry"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"geometry": ("GEOMETRY", {})}}
|
||||
|
||||
RETURN_TYPES = ("INT", "INT")
|
||||
RETURN_NAMES = ("num_vertices", "num_triangles")
|
||||
FUNCTION = "get_info"
|
||||
CATEGORY = "mtb/3D"
|
||||
|
||||
def get_info(self, geometry):
|
||||
log.debug(geometry)
|
||||
return (
|
||||
len(geometry.vertices),
|
||||
len(geometry.triangles),
|
||||
)
|
||||
|
||||
|
||||
class GeometryDecimater:
|
||||
"""Optimized the geometry to match the target number of triangles"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"mesh": ("GEOMETRY", {}),
|
||||
"target": ("INT", {"default": 1500, "min": 3, "max": 500000}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("GEOMETRY",)
|
||||
RETURN_NAMES = ("geometry",)
|
||||
FUNCTION = "decimate"
|
||||
CATEGORY = "mtb/3D"
|
||||
|
||||
def decimate(self, mesh, target):
|
||||
mesh = mesh.simplify_quadric_decimation(target_number_of_triangles=target)
|
||||
mesh.compute_vertex_normals()
|
||||
|
||||
return (mesh,)
|
||||
|
||||
|
||||
class GeometrySceneSetup:
|
||||
"""Scene setup for the renderer"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"geometry": ("GEOMETRY",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("SCENE",)
|
||||
RETURN_NAMES = ("scene",)
|
||||
FUNCTION = "setup"
|
||||
CATEGORY = "mtb/3D"
|
||||
|
||||
def setup(self, mesh, target):
|
||||
return ({"geometry": mesh, "camera": cam},)
|
||||
|
||||
|
||||
class GeometryRender:
|
||||
"""Renders a Geometry to an image"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"geometry": ("SCENE", {}),
|
||||
"width": ("INT", {"default": 512, "min": 1}),
|
||||
"height": ("INT", {"default": 512, "min": 1}),
|
||||
"background": ("COLOR", {"default": [0.0, 0.0, 0.0]}),
|
||||
"camera": ("CAMERA",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "render"
|
||||
CATEGORY = "mtb/3D"
|
||||
|
||||
def render(self, geometry, width, height, background, camera):
|
||||
# create a renderer
|
||||
renderer = o3d.visualization.rendering.OffscreenRenderer(width, height)
|
||||
renderer.set_camera(camera)
|
||||
renderer.clear(background)
|
||||
renderer.add_geometry(geometry)
|
||||
renderer.render()
|
||||
image = renderer.get_image()
|
||||
return (image,)
|
||||
|
||||
|
||||
__nodes__ = [
|
||||
LoadGeometry,
|
||||
GeometryInfo,
|
||||
GeometryTest,
|
||||
GeometryDecimater,
|
||||
GeometrySphere,
|
||||
TransformGeometry,
|
||||
GeometryBox,
|
||||
]
|
||||
@@ -11,6 +11,7 @@ import { app } from '/scripts/app.js'
|
||||
import * as shared from '/extensions/mtb/comfy_shared.js'
|
||||
import { log } from '/extensions/mtb/comfy_shared.js'
|
||||
import { MtbWidgets } from '/extensions/mtb/mtb_widgets.js'
|
||||
import { o3d_to_three } from '/extensions/mtb/geometry_nodes.js'
|
||||
|
||||
// TODO: respect inputs order...
|
||||
|
||||
@@ -84,6 +85,18 @@ app.registerExtension({
|
||||
this.setSize(this.computeSize())
|
||||
}
|
||||
|
||||
if (message.geometry) {
|
||||
for (const geom of message.geometry) {
|
||||
console.log('Adding geom', geom, typeof geom)
|
||||
const w = this.addCustomWidget(
|
||||
MtbWidgets.DEBUG_GEOM(`${prefix}_${widgetI}`, geom)
|
||||
)
|
||||
w.parent = this
|
||||
widgetI++
|
||||
}
|
||||
this.setSize(this.computeSize())
|
||||
}
|
||||
|
||||
this.onRemoved = function () {
|
||||
// When removing this node we need to remove the input from the DOM
|
||||
for (let y in this.widgets) {
|
||||
|
||||
Vendored
+51466
File diff suppressed because one or more lines are too long
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* File: geometry_nodes.js
|
||||
* Project: comfy_mtb
|
||||
* Author: Mel Massadian
|
||||
*
|
||||
* Copyright (c) 2023 Mel Massadian
|
||||
*
|
||||
*/
|
||||
|
||||
import { app } from '/scripts/app.js'
|
||||
|
||||
import * as THREE from '/extensions/mtb/extern/three.module.js'
|
||||
|
||||
export const make_wireframe = (mesh) => {
|
||||
const wireframeGeometry = new THREE.WireframeGeometry(mesh.geometry)
|
||||
const wireframeMaterial = new THREE.LineBasicMaterial({
|
||||
color: 0x000000,
|
||||
linewidth: 1,
|
||||
})
|
||||
const wireframe = new THREE.LineSegments(wireframeGeometry, wireframeMaterial)
|
||||
|
||||
// return {
|
||||
// wireframe,
|
||||
// wireframeGeometry,
|
||||
// wireframeMaterial,
|
||||
// }
|
||||
return wireframe
|
||||
}
|
||||
|
||||
export const o3d_to_three = (data) => {
|
||||
// Parse the JSON data
|
||||
const meshData = JSON.parse(data)
|
||||
|
||||
// Create a geometry
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
|
||||
// Set vertices and triangles
|
||||
const vertices = new Float32Array(meshData.vertices.flat())
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
|
||||
|
||||
const indices = new Uint32Array(meshData.triangles.flat())
|
||||
geometry.setIndex(new THREE.BufferAttribute(indices, 1))
|
||||
|
||||
// If vertex_normals are available
|
||||
if (meshData.vertex_normals) {
|
||||
const normals = new Float32Array(meshData.vertex_normals.flat())
|
||||
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3))
|
||||
}
|
||||
|
||||
// If vertex_colors are available
|
||||
if (meshData.vertex_colors) {
|
||||
const colors = new Float32Array(meshData.vertex_colors.flat())
|
||||
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))
|
||||
}
|
||||
|
||||
// If triangle_uvs are available
|
||||
if (meshData.triangle_uvs) {
|
||||
const uvs = new Float32Array(meshData.triangle_uvs.flat())
|
||||
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2))
|
||||
}
|
||||
const material_opts = {
|
||||
// wireframe: true,
|
||||
}
|
||||
// For visualization, you might choose to use the MeshPhongMaterial to get the benefit of lighting with normals
|
||||
const material = meshData.vertex_colors
|
||||
? new THREE.MeshPhongMaterial({ ...material_opts, vertexColors: true })
|
||||
: new THREE.MeshPhongMaterial({ ...material_opts, color: 0x00ff00 })
|
||||
|
||||
const threeMesh = new THREE.Mesh(geometry, material)
|
||||
|
||||
return threeMesh
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'mtb.geometry_nodes',
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
switch (nodeData.name) {
|
||||
case 'Load Geometry (mtb)':
|
||||
const onExecuted = nodeType.prototype.onExecuted
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecuted?.apply(this, arguments)
|
||||
console.log('Executed Load Geometry', arguments)
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -10,6 +10,9 @@
|
||||
import { app } from '/scripts/app.js'
|
||||
import parseCss from '/extensions/mtb/extern/parse-css.js'
|
||||
import * as shared from '/extensions/mtb/comfy_shared.js'
|
||||
import { o3d_to_three, make_wireframe } from '/extensions/mtb/geometry_nodes.js'
|
||||
import * as THREE from '/extensions/mtb/extern/three.module.js'
|
||||
|
||||
import { log } from '/extensions/mtb/comfy_shared.js'
|
||||
import { api } from '/scripts/api.js'
|
||||
|
||||
@@ -306,6 +309,164 @@ export const MtbWidgets = {
|
||||
document.body.appendChild(w.inputEl)
|
||||
return w
|
||||
},
|
||||
DEBUG_GEOM: (name, val) => {
|
||||
const w = {
|
||||
name,
|
||||
type: 'geometry',
|
||||
value: val,
|
||||
animate: false,
|
||||
show_wireframe: false,
|
||||
menu: null,
|
||||
// draw: function (ctx, node, widgetWidth, widgetY, height) {
|
||||
// const [cw, ch] = this.computeSize(widgetWidth)
|
||||
// shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
|
||||
// },
|
||||
draw: function (ctx, node, widgetWidth, widgetY, height) {
|
||||
const [cw, ch] = this.computeSize(widgetWidth)
|
||||
shared.offsetDOMWidget(this, ctx, node, widgetWidth, widgetY, ch)
|
||||
// write text
|
||||
ctx.fillStyle = '#000'
|
||||
ctx.font = '14px Arial'
|
||||
ctx.textAlign = 'center'
|
||||
// ctx.fillText('CLICK ME', widgetWidth * 0.5, widgetY + 14)
|
||||
},
|
||||
initThreeJS: function (canvas) {
|
||||
const size = this.computeSize()
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
canvas: canvas,
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
})
|
||||
// this.renderer.setClearColor(0x0000ff)
|
||||
this.scene = new THREE.Scene()
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
75,
|
||||
size[0] / size[1],
|
||||
0.1,
|
||||
1000
|
||||
)
|
||||
this.camera.position.z = 5
|
||||
|
||||
// Ambient Light
|
||||
const ambientLight = new THREE.AmbientLight(0x404040, 0.5) // Soft white ambient light
|
||||
this.scene.add(ambientLight)
|
||||
|
||||
// Directional Light
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
|
||||
directionalLight.position.set(2, 4, -3)
|
||||
directionalLight.castShadow = true
|
||||
|
||||
// Define the visible area of the projected shadow
|
||||
directionalLight.shadow.camera.left = -10
|
||||
directionalLight.shadow.camera.right = 10
|
||||
directionalLight.shadow.camera.top = 10
|
||||
directionalLight.shadow.camera.bottom = -10
|
||||
directionalLight.shadow.camera.near = 0.1
|
||||
directionalLight.shadow.camera.far = 1000
|
||||
|
||||
directionalLight.shadow.mapSize.width = 512 // Shadow resolution
|
||||
directionalLight.shadow.mapSize.height = 512
|
||||
|
||||
this.scene.add(directionalLight)
|
||||
|
||||
// Update function
|
||||
const animate = () => {
|
||||
requestAnimationFrame(animate)
|
||||
if (this.mesh && this.animate) {
|
||||
this.group.rotation.x += 0.005
|
||||
this.group.rotation.y += 0.005
|
||||
}
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
}
|
||||
animate()
|
||||
log('Scene setup done and loop running.')
|
||||
},
|
||||
createThreeCanvas: function (ctx) {
|
||||
// this.canvas = LiteGraph.createCanvas(this.size[0], this.size[1])
|
||||
// this.addWidget('canvas', '', '', this.canvas)
|
||||
this.canvas = w.inputEl
|
||||
this.ctx = this.canvas.getContext('2d')
|
||||
// Initialize Three.js here
|
||||
this.initThreeJS(this.canvas)
|
||||
},
|
||||
|
||||
onResize: function (size) {
|
||||
console.log(size)
|
||||
},
|
||||
computeSize: function (width) {
|
||||
if (width) {
|
||||
return [width, width]
|
||||
}
|
||||
return [128, 128]
|
||||
},
|
||||
onRemoved: function () {
|
||||
if (this.inputEl) {
|
||||
this.inputEl.remove()
|
||||
}
|
||||
},
|
||||
}
|
||||
log('Creating canvas')
|
||||
w.inputEl = document.createElement('canvas')
|
||||
|
||||
// add context menu with "animate" and "show wireframe"
|
||||
w.inputEl.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault()
|
||||
if (w.menu) {
|
||||
w.menu.remove()
|
||||
}
|
||||
|
||||
w.menu = document.createElement('div')
|
||||
Object.assign(w.menu.style, {
|
||||
position: 'absolute',
|
||||
top: `${e.clientY}px`,
|
||||
left: `${e.clientX}px`,
|
||||
background: '#333',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
padding: '5px',
|
||||
borderRadius: '5px',
|
||||
zIndex: 999,
|
||||
})
|
||||
const anim_btn = document.createElement('button')
|
||||
anim_btn.textContent = `${w.animate ? 'stop' : 'start'} animation`
|
||||
anim_btn.onclick = () => {
|
||||
w.animate = !w.animate
|
||||
if (w.menu) {
|
||||
w.menu.remove()
|
||||
}
|
||||
}
|
||||
|
||||
w.menu.appendChild(anim_btn)
|
||||
const wire = document.createElement('button')
|
||||
wire.textContent = `${w.show_wireframe ? 'hide' : 'show'} wireframe`
|
||||
wire.onclick = () => {
|
||||
w.show_wireframe = !w.show_wireframe
|
||||
if (w.show_wireframe) {
|
||||
w.group.add(w.mesh_wireframe)
|
||||
} else {
|
||||
w.group.remove(w.mesh_wireframe)
|
||||
}
|
||||
if (w.menu) {
|
||||
w.menu.remove()
|
||||
}
|
||||
}
|
||||
|
||||
w.menu.appendChild(wire)
|
||||
document.body.appendChild(w.menu)
|
||||
})
|
||||
|
||||
w.initThreeJS(w.inputEl, val)
|
||||
|
||||
w.mesh = o3d_to_three(val)
|
||||
w.mesh_wireframe = make_wireframe(w.mesh)
|
||||
w.group = new THREE.Group()
|
||||
|
||||
w.group.add(w.mesh)
|
||||
w.scene.add(w.group)
|
||||
|
||||
document.body.appendChild(w.inputEl)
|
||||
return w
|
||||
},
|
||||
DEBUG_STRING: (name, val) => {
|
||||
const fontSize = 16
|
||||
const w = {
|
||||
|
||||
Reference in New Issue
Block a user