Last active
September 13, 2022 02:11
-
-
Save ecmjohnson/33b05aa97c623968706fe32c8e2db8e6 to your computer and use it in GitHub Desktop.
Blender Add-on: Export animation renders using bmild/NeRF's Blender data format
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
bl_info = { | |
"name": "NeRF Data Exporter", | |
"description": "Outputs images and camera parameters in bmild/NeRF's Blender data format.", | |
"author": "ecmjohnson", | |
"version": (0, 3), | |
"blender": (2, 80, 0), | |
"support": "TESTING", | |
"category": "Import-Export", | |
} | |
import bpy | |
from bpy.app.handlers import persistent | |
import json | |
import os | |
from os.path import join as pjoin | |
import numpy as np | |
import bmesh | |
### Constants | |
# Expected data split in NeRF code | |
train = 'train' | |
test = 'test' | |
val = 'val' | |
# Expected filenames in NeRF code | |
train_json = 'transforms_{}.json'.format(train) | |
test_json = 'transforms_{}.json'.format(test) | |
val_json = 'transforms_{}.json'.format(val) | |
# Filename for novel view camera | |
novel_json = 'novel_view.json' | |
### User preferences | |
class NerfDataExporterPreferences(bpy.types.AddonPreferences): | |
bl_idname = __name__ | |
# Defaults to directory of Blender .blend file | |
output_path: bpy.props.StringProperty( | |
name = 'Data output folder', | |
subtype = 'FILE_PATH' | |
) | |
# train -> test -> val | |
train_end: bpy.props.IntProperty( | |
name = 'Index of last training frame', | |
default = 50, | |
min = 1 | |
) | |
# TODO some way to enforce test_end > train_end ? | |
test_end: bpy.props.IntProperty( | |
name = 'Index of last testing frame (must be greater than last training frame)', | |
default = 75, | |
min = 1 | |
) | |
novel_cam_name: bpy.props.StringProperty( | |
name = 'Novel view camera' | |
) | |
def draw(self, context): | |
layout = self.layout | |
layout.label(text = 'Preferences for NeRF data exporter') | |
layout.prop(self, 'output_path') | |
layout.prop(self, 'train_end') | |
layout.prop(self, 'test_end') | |
layout.prop(self, 'novel_cam_name') | |
### Helper functions | |
def get_prefs(): | |
return bpy.context.preferences.addons[__name__].preferences | |
def is_train(f): | |
prefs = get_prefs() | |
return f <= prefs.train_end | |
def is_test(f): | |
prefs = get_prefs() | |
return (f > prefs.train_end) and (f <= prefs.test_end) | |
def is_val(f): | |
prefs = get_prefs() | |
return f > prefs.test_end | |
def get_name(f): | |
if is_train(f): | |
return train | |
elif is_test(f): | |
return test | |
elif is_val(f): | |
return val | |
else: | |
print('Failure in get_name, f =', f) | |
return 'fail' | |
def sanitize_path(p): | |
return p.replace('\\', '/') | |
### Callback functions | |
root_folder = '' | |
f = 1 | |
frames = [] | |
out = {} | |
novel = [] | |
# On start of rendering | |
@persistent | |
def render_init_fn(scene): | |
# Clear previous state | |
frames.clear() | |
out.clear() | |
novel.clear() | |
# Setup output folders | |
global root_folder | |
prefs = get_prefs() | |
if prefs.output_path != '': | |
root_folder = prefs.output_path | |
else: | |
root_folder = os.path.dirname(bpy.data.filepath) | |
os.makedirs(pjoin(root_folder, train), exist_ok=True) | |
os.makedirs(pjoin(root_folder, test), exist_ok=True) | |
os.makedirs(pjoin(root_folder, val), exist_ok=True) | |
# Training images get written first | |
bpy.context.scene.render.filepath = pjoin(root_folder, train, '') | |
# Compute the assumed static camera information | |
cam = scene.camera | |
cam_angle_x = cam.data.angle_x # in radians | |
out['camera_angle_x'] = cam_angle_x | |
out['clip_start'] = cam.data.clip_start | |
out['clip_end'] = cam.data.clip_end | |
# After a rendered frame is written | |
@persistent | |
def render_write_fn(scene): | |
prefs = get_prefs() | |
# Get the required file name and camera parameters | |
global f | |
f = scene.frame_current | |
fout = {'id': f} | |
imgfilepath = scene.render.frame_path(frame=f) | |
imgfilename = bpy.path.basename(imgfilepath) | |
# NeRF relative path doesn't include file extension! | |
imgrelpath = pjoin('.', get_name(f), imgfilename.split('.')[0]) | |
# Change any Windows slashes to Unix slashes | |
fout['file_path'] = sanitize_path(imgrelpath) | |
mat = scene.camera.matrix_world | |
# Blender's Matrix datatype is not directly JSON serializable | |
fout['transform_matrix'] = [list(row) for row in mat] | |
frames.append(fout) | |
# train -> test -> val | |
if f == prefs.train_end: | |
out['frames'] = frames | |
with open(pjoin(root_folder, train_json), 'w') as w: | |
json.dump(out, w, indent=4) | |
frames.clear() | |
# Moving on to test images | |
bpy.context.scene.render.filepath = pjoin(root_folder, test, '') | |
elif f == prefs.test_end: | |
out['frames'] = frames # overwrites last set of frames | |
with open(pjoin(root_folder, test_json), 'w') as w: | |
json.dump(out, w, indent=4) | |
frames.clear() | |
# Moving on to val images | |
bpy.context.scene.render.filepath = pjoin(root_folder, val, '') | |
# Add the novel view transform for this frame | |
if prefs.novel_cam_name in scene.objects: | |
novel_cam = scene.objects[prefs.novel_cam_name] | |
mat = novel_cam.matrix_world | |
nout = { | |
'id': f, | |
'transform_matrix': [list(row) for row in mat] | |
} | |
novel.append(nout) | |
# After all frames are rendered | |
@persistent | |
def render_complete_fn(scene): | |
# Save out the camera parameters of the last set | |
if is_train(f): | |
final_json = pjoin(root_folder, train_json) | |
elif is_test(f): | |
final_json = pjoin(root_folder, test_json) | |
elif is_val(f): | |
final_json = pjoin(root_folder, val_json) | |
else: | |
print('Failure in render_complete_fn') | |
final_json = pjoin(root_folder, 'failure.json') | |
out['frames'] = frames # overwrites last set of frames | |
with open(final_json, 'w') as w: | |
json.dump(out, w, indent=4) | |
# Save out novel view transforms | |
if len(novel) > 0: | |
nout = {'frames': novel} | |
with open(pjoin(root_folder, novel_json), 'w') as w: | |
json.dump(nout, w, indent=4) | |
### Register/unregister addon | |
def register(): | |
bpy.utils.register_class(NerfDataExporterPreferences) | |
bpy.app.handlers.render_init.append(render_init_fn) | |
bpy.app.handlers.render_write.append(render_write_fn) | |
bpy.app.handlers.render_complete.append(render_complete_fn) | |
def unregister(): | |
bpy.utils.unregister_class(NerfDataExporterPreferences) | |
bpy.app.handlers.render_init.remove(render_init_fn) | |
bpy.app.handlers.render_write.remove(render_write_fn) | |
bpy.app.handlers.render_complete.remove(render_complete_fn) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment