Skip to content

Commit

Permalink
feat(conversion): 3D rendering with Blender (#327)
Browse files Browse the repository at this point in the history
  • Loading branch information
bouassaba authored Sep 13, 2024
1 parent 469fa41 commit 3959635
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 55 deletions.
49 changes: 24 additions & 25 deletions conversion/pipeline/glb_pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,39 +112,38 @@ func (p *glbPipeline) patchSnapshotPreviewField(inputPath string, opts api_clien
}

func (p *glbPipeline) createThumbnail(inputPath string, opts api_client.PipelineRunOptions) error {
tmpPath := filepath.FromSlash(os.TempDir() + "/" + helper.NewID() + ".jpeg")
tmpPath := filepath.FromSlash(os.TempDir() + "/" + helper.NewID() + ".png")
defer func(path string) {
if err := os.Remove(path); errors.Is(err, os.ErrNotExist) {
return
} else if err != nil {
infra.GetLogger().Error(err)
}
}(tmpPath)
// We don't consider failing the creation of the thumbnail as an error
_ = p.glbProc.Thumbnail(inputPath, p.config.Limits.ImagePreviewMaxWidth, p.config.Limits.ImagePreviewMaxHeight, "rgb(255,255,255)", tmpPath)
props, err := p.imageProc.MeasureImage(tmpPath)
if err != nil {
return err
}
// We don't consider failing to create the thumbnail as an error
_ = p.glbProc.Thumbnail(inputPath, p.config.Limits.ImagePreviewMaxWidth, p.config.Limits.ImagePreviewMaxHeight, tmpPath)
stat, err := os.Stat(tmpPath)
if err != nil {
return err
}
s3Object := &api_client.S3Object{
Bucket: opts.Bucket,
Key: opts.SnapshotID + "/thumbnail" + filepath.Ext(tmpPath),
Image: props,
Size: helper.ToPtr(stat.Size()),
}
if err := p.s3.PutFile(s3Object.Key, tmpPath, helper.DetectMimeFromFile(tmpPath), s3Object.Bucket, minio.PutObjectOptions{}); err != nil {
return err
}
if err := p.snapshotClient.Patch(api_client.SnapshotPatchOptions{
Options: opts,
Fields: []string{api_client.SnapshotFieldThumbnail},
Thumbnail: s3Object,
}); err != nil {
return err
if err == nil {
props, err := p.imageProc.MeasureImage(tmpPath)
if err != nil {
return err
}
s3Object := &api_client.S3Object{
Bucket: opts.Bucket,
Key: opts.SnapshotID + "/thumbnail" + filepath.Ext(tmpPath),
Image: props,
Size: helper.ToPtr(stat.Size()),
}
if err := p.s3.PutFile(s3Object.Key, tmpPath, helper.DetectMimeFromFile(tmpPath), s3Object.Bucket, minio.PutObjectOptions{}); err != nil {
return err
}
if err := p.snapshotClient.Patch(api_client.SnapshotPatchOptions{
Options: opts,
Fields: []string{api_client.SnapshotFieldThumbnail},
Thumbnail: s3Object,
}); err != nil {
return err
}
}
return nil
}
146 changes: 144 additions & 2 deletions conversion/processor/glb_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,150 @@ func NewGLBProcessor() *GLBProcessor {
}
}

func (p *GLBProcessor) Thumbnail(inputPath string, width int, height int, color string, outputPath string) error {
if err := infra.NewCommand().Exec("screenshot-glb", "-i", inputPath, "-o", outputPath, "--width", fmt.Sprintf("%d", width), "--height", fmt.Sprintf("%d", height), "--color", color); err != nil {
func (p *GLBProcessor) Thumbnail(inputPath string, width int, height int, outputPath string) error {
err := infra.NewCommand().Exec("blender", "--background", "--python-expr", fmt.Sprintf(`
import bpy
import sys
from mathutils import Vector
def debug_print(message):
print(message)
def get_combined_dimensions(objects):
"""
Calculate the combined world-coordinate dimensions of the given objects.
:param objects: List of Blender objects
:return: Vector with dimensions (width, depth, height)
"""
min_coord = Vector((float("inf"), float("inf"), float("inf")))
max_coord = Vector((float("-inf"), float("-inf"), float("-inf")))
for obj in objects:
debug_print(f"Processing object: {obj.name}, type: {obj.type}")
if obj.type == "MESH":
# Ensure transformations are applied
bpy.context.view_layer.objects.active = obj
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
# Update the dependency graph
depsgraph = bpy.context.evaluated_depsgraph_get()
ob_eval = obj.evaluated_get(depsgraph)
for vert in ob_eval.data.vertices:
world_coord = ob_eval.matrix_world @ vert.co
min_coord.x = min(min_coord.x, world_coord.x)
min_coord.y = min(min_coord.y, world_coord.y)
min_coord.z = min(min_coord.z, world_coord.z)
max_coord.x = max(max_coord.x, world_coord.x)
max_coord.y = max(max_coord.y, world_coord.y)
max_coord.z = max(max_coord.z, world_coord.z)
dimensions = max_coord - min_coord
debug_print(f"Combined dimensions: {dimensions}")
return dimensions
# Read command-line arguments
argv = sys.argv
argv = argv[argv.index("--") + 1 :]
input_file = argv[argv.index("--input") + 1]
output_file = argv[argv.index("--output") + 1]
# Clear existing objects
bpy.ops.object.select_all(action="SELECT")
bpy.ops.object.delete(use_global=False)
# Import GLB file
bpy.ops.import_scene.gltf(filepath=input_file)
debug_print("Imported GLB file")
# Collect all imported objects
imported_objects = bpy.context.selected_objects
if not imported_objects:
raise RuntimeError("No objects imported")
# Move each imported object to center
for obj in imported_objects:
obj.location = (0, 0, 0)
debug_print(f"Imported objects: {[obj.name for obj in imported_objects]}")
# Calculate combined dimensions for all mesh objects
dimensions = get_combined_dimensions(imported_objects)
max_dimension = max(dimensions)
debug_print(f"Max dimension: {max_dimension}")
# Determine a suitable distance for the camera based on the object's size
base_distance = 5.0
scaling_factor = 2.0 # Adjust this factor as needed
distance = base_distance + scaling_factor * max_dimension
debug_print(f"Calculated camera distance: {distance}")
# Add a camera
camera_data = bpy.data.cameras.new(name="Camera")
camera_object = bpy.data.objects.new("Camera", camera_data)
bpy.context.collection.objects.link(camera_object)
bpy.context.scene.camera = camera_object
# Set camera position and rotation
camera_object.location = (
distance,
-distance,
distance * 0.65,
) # Adjusted based on object size
camera_object.rotation_euler = (1.2, 0.0, 0.8) # Maintain original rotation angles
debug_print(f"Camera location: {camera_object.location}")
debug_print(f"Camera rotation: {camera_object.rotation_euler}")
# Add a key light source
light_data = bpy.data.lights.new(name="KeyLight", type="POINT")
light_object = bpy.data.objects.new(name="KeyLight", object_data=light_data)
bpy.context.collection.objects.link(light_object)
light_object.location = (distance, -distance, distance) # Adjusted based on object size
light_data.energy = 1500 # Increased intensity
# Add a fill light source (for better rendering)
fill_light_data = bpy.data.lights.new(name="FillLight", type="POINT")
fill_light_object = bpy.data.objects.new(name="FillLight", object_data=fill_light_data)
bpy.context.collection.objects.link(fill_light_object)
fill_light_object.location = (
-distance,
distance,
distance,
) # Adjusted based on object size
fill_light_data.energy = 1000 # Increased intensity
debug_print(f"Light locations: {light_object.location}, {fill_light_object.location}")
# Set white background
if bpy.context.scene.world.node_tree:
world = bpy.context.scene.world
nodes = world.node_tree.nodes
background = nodes.get("Background", None)
if background:
background.inputs[0].default_value = (1, 1, 1, 1) # White color (R, G, B, A)
# Set render settings
bpy.context.scene.render.engine = "CYCLES"
bpy.context.scene.render.filepath = output_file
bpy.context.scene.render.resolution_x = %d
bpy.context.scene.render.resolution_y = %d
# Center and view all objects
bpy.ops.object.select_all(action="DESELECT")
for obj in imported_objects:
obj.select_set(True)
bpy.context.view_layer.objects.active = imported_objects[0]
bpy.ops.view3d.camera_to_view_selected()
# Disable denoising because Blender on Debian/Ubuntu is built without OpenImageDenoiser
bpy.context.scene.cycles.use_denoising = False
# Render the scene
bpy.ops.render.render(write_still=True)
debug_print("Render complete")`, width, height,
), "--", "--input", inputPath, "--output", outputPath)
if err != nil {
return err
}
return nil
Expand Down
39 changes: 11 additions & 28 deletions conversion/runtime/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ func (d *Installer) Start() {
d.updatePackageList()
d.installCoreTools()
d.installGltfPipeline()
d.installScreenshotGLB()
d.installBrowsersDeps()
d.installBrowsers()
d.installBlender()
d.installLibreOffice()
d.installTesseract()
d.installFonts()
Expand All @@ -41,7 +39,7 @@ func (d *Installer) Start() {
}

func (d *Installer) updatePackageList() {
infra.GetLogger().Named(infra.StrInstaller).Infow(" updating", "debian", "package list")
infra.GetLogger().Named(infra.StrInstaller).Infow("🔄 updating", "debian", "package list")
if err := d.cmd.Exec("apt-get", "update"); err != nil {
infra.GetLogger().Error(err)
infra.GetLogger().Named(infra.StrInstaller).Infow("❌️ failed", "debian", "package list")
Expand Down Expand Up @@ -85,34 +83,19 @@ func (d *Installer) installGltfPipeline() {
infra.GetLogger().Named(infra.StrInstaller).Infow("✅️ completed", "package", "gltf-pipeline")
}

func (d *Installer) installScreenshotGLB() {
infra.GetLogger().Named(infra.StrInstaller).Infow("⬇️ installing", "package", "@koupr/screenshot-glb")
if err := d.cmd.Exec("npm", "i", "-g", "@koupr/[email protected]"); err != nil {
infra.GetLogger().Error(err)
infra.GetLogger().Named(infra.StrInstaller).Infow("❌️ failed", "package", "@koupr/screenshot-glb")
return
}
infra.GetLogger().Named(infra.StrInstaller).Infow("✅️ completed", "package", "@koupr/screenshot-glb")
}

func (d *Installer) installBrowsersDeps() {
infra.GetLogger().Named(infra.StrInstaller).Infow("⬇️ installing", "package", "browsers-deps")
if err := d.cmd.Exec("npx", "playwright", "install-deps"); err != nil {
infra.GetLogger().Error(err)
infra.GetLogger().Named(infra.StrInstaller).Infow("❌️ failed", "package", "browsers-deps")
return
func (d *Installer) installBlender() {
infra.GetLogger().Named(infra.StrInstaller).Infow("⬇️ installing", "package", "blender")
packages := []string{
"blender",
"python3-numpy",
}
infra.GetLogger().Named(infra.StrInstaller).Infow("✅️ completed", "package", "browsers-deps")
}

func (d *Installer) installBrowsers() {
infra.GetLogger().Named(infra.StrInstaller).Infow("⬇️ installing", "package", "browsers")
if err := d.cmd.Exec("npx", "playwright", "install"); err != nil {
args := append([]string{"install", "-y"}, packages...)
if err := d.cmd.Exec("apt-get", args...); err != nil {
infra.GetLogger().Error(err)
infra.GetLogger().Named(infra.StrInstaller).Infow("❌️ failed", "package", "browsers")
infra.GetLogger().Named(infra.StrInstaller).Infow("❌️ failed", "package", "blender")
return
}
infra.GetLogger().Named(infra.StrInstaller).Infow("✅️ completed", "package", "browsers")
infra.GetLogger().Named(infra.StrInstaller).Infow("✅️ completed", "package", "blender")
}

func (d *Installer) installLibreOffice() {
Expand Down

0 comments on commit 3959635

Please sign in to comment.