Skip to content

Commit

Permalink
Merge pull request OpenDroneMap#506 from pulquero/video
Browse files Browse the repository at this point in the history
Blender 360 and VR video scripts
  • Loading branch information
pierotofy authored Nov 18, 2017
2 parents 9438e4b + 7e14c54 commit a236d85
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 42 deletions.
18 changes: 18 additions & 0 deletions contrib/blender/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# odm_photo
Renders photos from ODM generated texture models.
Currently can produce 360 panoramic photos and 360 3D panoramic (VR) photos.
NB: the default resolution for 360 photos is 6000x3000 (maximum supported by Facebook).

## Requirements
* Blender
Expand All @@ -21,3 +22,20 @@ To generate a 360 3D panoramic photo:
Output is `<project-path>/odm_photo/odm_photo_vr_L.jpg` and `<project-path>/odm_photo/odm_photo_vr_R.jpg`.

**NB: argument order matters!**

# odm_video
Renders videos from ODM generated texture models.
Currently can produce 360 panoramic videos.
NB: the default resolution is 4096x2048 (maximum supported by Facebook).

## Requirements
* Blender
* Python 2.7 (must be on your PATH)
* Spatial Media Metadata Injector (https://github.com/google/spatial-media/tree/master/spatialmedia) (place in `spatialmedia` subdirectory)

## Usage
To generate a 360 panoramic photo:

blender -b photo_360.blend --python odm_video.py -- <project-path> <camera-waypoints.xyz> <number-of-frames>

Output is `<project-path>/odm_video/odm_video_360.mp4`.
45 changes: 45 additions & 0 deletions contrib/blender/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import bpy
import materials_utils

def loadMesh(file):

bpy.utils.register_module('materials_utils')

bpy.ops.import_scene.obj(filepath=file,
axis_forward='Y',
axis_up='Z')

bpy.ops.xps_tools.convert_to_cycles_all()

model = bpy.data.objects[-1]
minX = float('inf')
maxX = float('-inf')
minY = float('inf')
maxY = float('-inf')
minZ = float('inf')
maxZ = float('-inf')
for coord in model.bound_box:
x = coord[0]
y = coord[1]
z = coord[2]
minX = min(x, minX)
maxX = max(x, maxX)
minY = min(y, minY)
maxY = max(y, maxY)
minZ = min(z, minZ)
maxZ = max(z, maxZ)

model.location[2] += (maxZ - minZ)/2

surfaceShaderType = 'ShaderNodeEmission'
surfaceShaderName = 'Emission'

for m in bpy.data.materials:
nt = m.node_tree
nt.nodes.remove(nt.nodes['Color Mult'])
nt.nodes.remove(nt.nodes['Diffuse BSDF'])
nt.nodes.new(surfaceShaderType)
nt.links.new(nt.nodes['Material Output'].inputs[0],
nt.nodes[surfaceShaderName].outputs[0])
nt.links.new(nt.nodes[surfaceShaderName].inputs[0],
nt.nodes['Diffuse Texture'].outputs[0])
46 changes: 4 additions & 42 deletions contrib/blender/odm_photo.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@

import sys
import bpy
import materials_utils
import subprocess

surfaceShaderType = 'ShaderNodeEmission'
surfaceShaderName = 'Emission'
from common import loadMesh


def main():
Expand All @@ -24,47 +21,12 @@ def main():

projectHome = sys.argv[-1]

bpy.utils.register_module('materials_utils')

bpy.ops.import_scene.obj(filepath=projectHome +
'/odm_texturing/odm_textured_model_geo.obj',
axis_forward='Y', axis_up='Z')

bpy.ops.xps_tools.convert_to_cycles_all()

model = bpy.data.objects[-1]
minX = float('inf')
maxX = float('-inf')
minY = float('inf')
maxY = float('-inf')
minZ = float('inf')
maxZ = float('-inf')
for coord in model.bound_box:
x = coord[0]
y = coord[1]
z = coord[2]
minX = min(x, minX)
maxX = max(x, maxX)
minY = min(y, minY)
maxY = max(y, maxY)
minZ = min(z, minZ)
maxZ = max(z, maxZ)

model.location[2] += (maxZ - minZ)/2

for m in bpy.data.materials:
nt = m.node_tree
nt.nodes.remove(nt.nodes['Color Mult'])
nt.nodes.remove(nt.nodes['Diffuse BSDF'])
nt.nodes.new(surfaceShaderType)
nt.links.new(nt.nodes['Material Output'].inputs[0],
nt.nodes[surfaceShaderName].outputs[0])
nt.links.new(nt.nodes[surfaceShaderName].inputs[0],
nt.nodes['Diffuse Texture'].outputs[0])
loadMesh(projectHome +
'/odm_texturing/odm_textured_model_geo.obj')

blendName = bpy.path.display_name_from_filepath(bpy.data.filepath)
fileName = projectHome + '/odm_photo/odm_' + blendName
render = bpy.data.scenes[0].render
render = bpy.data.scenes['Scene'].render
render.filepath = fileName
bpy.ops.render.render(write_still=True)

Expand Down
113 changes: 113 additions & 0 deletions contrib/blender/odm_video.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env python

# Renders a video.
# To generate a 360 panoramic video:
# blender -b photo_360.blend --python odm_video.py -- <project-path> <camera-waypoints.xyz> <number-of-frames>

import sys
import subprocess
import os
import bpy
from common import loadMesh


def main():

if len(sys.argv) < 7 or sys.argv[-4] != '--':
sys.exit('Please provide the ODM project path, camera waypoints (xyz format), and number of frames.')

projectHome = sys.argv[-3]
waypointFile = sys.argv[-2]
numFrames = int(sys.argv[-1])

loadMesh(projectHome +
'/odm_texturing/odm_textured_model_geo.obj')

waypoints = loadWaypoints(waypointFile)
numWaypoints = len(waypoints)

scene = bpy.data.scenes['Scene']

# create path thru waypoints
curve = bpy.data.curves.new(name='CameraPath', type='CURVE')
curve.dimensions = '3D'
curve.twist_mode = 'Z_UP'
nurbs = curve.splines.new('NURBS')
nurbs.points.add(numWaypoints-1)
weight = 1
for i in range(numWaypoints):
nurbs.points[i].co[0] = waypoints[i][0]
nurbs.points[i].co[1] = waypoints[i][1]
nurbs.points[i].co[2] = waypoints[i][2]
nurbs.points[i].co[3] = weight
nurbs.use_endpoint_u = True
path = bpy.data.objects.new(name='CameraPath', object_data=curve)
scene.objects.link(path)

camera = bpy.data.objects['Camera']
camera.location[0] = 0
camera.location[1] = 0
camera.location[2] = 0
followPath = camera.constraints.new(type='FOLLOW_PATH')
followPath.name = 'CameraFollowPath'
followPath.target = path
followPath.use_curve_follow = True
animateContext = bpy.context.copy()
animateContext['constraint'] = followPath
bpy.ops.constraint.followpath_path_animate(animateContext,
constraint='CameraFollowPath',
frame_start=0,
length=numFrames)

blendName = bpy.path.display_name_from_filepath(bpy.data.filepath)
fileName = projectHome + '/odm_video/odm_' + blendName.replace('photo', 'video')
scene.frame_start = 0
scene.frame_end = numFrames
render = scene.render
render.filepath = fileName + '.mp4'
render.image_settings.file_format = 'FFMPEG'
if(render.use_multiview):
render.image_settings.stereo_3d_format.display_mode = 'TOPBOTTOM'
render.image_settings.views_format = 'STEREO_3D'
render.views[0].file_suffix = ''
format3d = 'top-bottom'
else:
width = render.resolution_x
height = render.resolution_y
format3d = 'none'
render.resolution_x = 4096
render.resolution_y = 2048

render.ffmpeg.audio_codec = 'AAC'
render.ffmpeg.codec = 'H264'
render.ffmpeg.format = 'MPEG4'
render.ffmpeg.video_bitrate = 45000
bpy.ops.render.render(animation=True)

writeMetadata(fileName+'.mp4', format3d)


def loadWaypoints(filename):
waypoints = []
with open(filename) as f:
for line in f:
xyz = line.split()
waypoints.append((float(xyz[0]), float(xyz[1]), float(xyz[2])))
return waypoints


def writeMetadata(filename, format3d):
subprocess.run(['python',
'spatialmedia',
'-i',
'--stereo='+format3d,
filename,
filename+'.injected'])
# check metadata injector was succesful
if os.path.exists(filename+'.injected'):
os.remove(filename)
os.rename(filename+'.injected', filename)


if __name__ == '__main__':
main()
Binary file modified contrib/blender/photo_360.blend
Binary file not shown.

0 comments on commit a236d85

Please sign in to comment.