Cycles: Add support for motion vectors in the viewport

This patch introduces the capability for Cycles to track changes to
camera, object and vertex positions, so that it can compute motion
vectors when any such changes occur, during viewport camera movements,
objects being dragged around or meshes being modified.
When Cycles debug mode is enabled, the motion pass is added to the
viewport passes selection for testing purposes.
This patch also adds motion vectors for the background during camera
rotations.

Pull Request: https://projects.blender.org/blender/blender/pulls/156227
This commit is contained in:
Patrick Mours 2026-05-18 12:54:08 +02:00 • committed by Patrick Mours
parent 70626760a2
commit ba033d2ff5
21 changed files with 320 additions and 48 deletions

View file

@ -239,6 +239,8 @@ enum_view3d_shading_render_pass = (
)
enum_view3d_debug_render_pass = (
('MOTION', "Vector", "Show motion vectors"),
('VOLUME_SCATTER', "Volume Scatter", "Show the contribution of scattered ray in volume"),
('VOLUME_TRANSMIT', "Volume Transmit", "Show the contribution of transmitted ray in volume"),
('VOLUME_MAJORANT', "Volume Majorant", "Show the majorant transmittance of the volume")

View file

@ -680,8 +680,11 @@ static void blender_camera_sync(Camera *cam,
/* transform */
cam->set_matrix(blender_camera_matrix(bcam->matrix, bcam->type, bcam->panorama_type));
array<Transform> motion;
array<Transform> motion = cam->get_motion();
motion.resize(bcam->motion_steps, cam->get_matrix());
if (bcam->motion_steps != 0) {
motion[bcam->motion_steps / 2] = cam->get_matrix();
}
cam->set_motion(motion);
cam->set_use_perspective_motion(false);
@ -1166,6 +1169,7 @@ void BlenderSync::sync_view(blender::View3D *b_v3d,
&bcam, *b_engine, b_render_settings, *b_scene, *b_data, b_v3d, b_rv3d, width, height);
blender_camera_border(
&bcam, *b_engine, b_render_settings, *b_scene, *b_data, b_v3d, b_rv3d, width, height);
bcam.motion_steps = scene->need_motion() == Scene::MOTION_PASS_INTERACTIVE ? 2 : 0;
blender::PointerRNA scene_rna_ptr = RNA_id_pointer_create(&b_scene->id);
blender::PointerRNA cscene = RNA_pointer_get(&scene_rna_ptr, "cycles");
blender_camera_sync(scene->camera, scene, &bcam, width, height, "", &cscene);

View file

@ -1094,6 +1094,21 @@ void BlenderSync::sync_hair(BObjectInfo &b_ob_info, Hair *hair)
free_object_to_mesh(b_ob_info, *b_mesh);
}
}
if (scene->need_motion() == Scene::MOTION_PASS_INTERACTIVE &&
hair->num_keys() == new_hair.num_keys())
{
new_hair.set_motion_steps(2);
Attribute *attr_mP = hair->attributes.find(ATTR_STD_MOTION_VERTEX_POSITION);
Attribute *new_attr_mP = new_hair.attributes.add(ATTR_STD_MOTION_VERTEX_POSITION);
if (attr_mP) {
new_attr_mP->set_data_from(std::move(*attr_mP));
}
else {
new_hair.copy_center_to_motion_step(0);
}
}
}
/* update original sockets */

View file

@ -969,6 +969,21 @@ void BlenderSync::sync_mesh(BObjectInfo &b_ob_info, Mesh *mesh)
free_object_to_mesh(b_ob_info, const_cast<blender::Mesh &>(*b_mesh));
}
if (scene->need_motion() == Scene::MOTION_PASS_INTERACTIVE &&
mesh->num_verts() == new_mesh.num_verts())
{
new_mesh.set_motion_steps(2);
Attribute *attr_mP = mesh->attributes.find(ATTR_STD_MOTION_VERTEX_POSITION);
Attribute *new_attr_mP = new_mesh.attributes.add(ATTR_STD_MOTION_VERTEX_POSITION);
if (attr_mP) {
new_attr_mP->set_data_from(std::move(*attr_mP));
}
else {
new_mesh.copy_center_to_motion_step(0);
}
}
}
/* update original sockets */

View file

@ -120,8 +120,7 @@ void BlenderSync::sync_object_motion_init(blender::Object &b_parent,
{
/* Initialize motion blur for object, detecting if it's enabled and creating motion
* steps array if so. */
array<Transform> motion;
object->set_motion(motion);
array<Transform> motion = object->get_motion();
Geometry *geom = object->get_geometry();
if (!geom) {
@ -156,6 +155,9 @@ void BlenderSync::sync_object_motion_init(blender::Object &b_parent,
motion_times.insert(object->motion_time(step));
}
}
else {
object->set_motion(motion);
}
}
Object *BlenderSync::sync_object(blender::ViewLayer &b_view_layer,
@ -584,19 +586,15 @@ void BlenderSync::sync_objects(blender::Depsgraph &b_depsgraph,
}
}
void BlenderSync::sync_motion(blender::RenderData &b_render,
blender::Depsgraph &b_depsgraph,
blender::bScreen *b_screen,
blender::View3D *b_v3d,
blender::RegionView3D *b_rv3d,
const int width,
const int height,
void **python_thread_state)
void BlenderSync::sync_objects_and_motion(blender::RenderData &b_render,
blender::Depsgraph &b_depsgraph,
blender::bScreen *b_screen,
blender::View3D *b_v3d,
blender::RegionView3D *b_rv3d,
const int width,
const int height,
void **python_thread_state)
{
if (scene->need_motion() == Scene::MOTION_NONE) {
return;
}
/* get camera object here to deal with camera switch */
blender::Object *b_cam = get_camera_object(b_v3d, b_rv3d);
@ -604,7 +602,7 @@ void BlenderSync::sync_motion(blender::RenderData &b_render,
const float subframe_center = b_scene->r.subframe;
float frame_center_delta = 0.0f;
if (scene->need_motion() != Scene::MOTION_PASS &&
if (scene->need_motion() == Scene::MOTION_BLUR &&
scene->camera->get_motion_position() != MOTION_POSITION_CENTER)
{
const float shuttertime = scene->camera->get_shuttertime();
@ -625,7 +623,20 @@ void BlenderSync::sync_motion(blender::RenderData &b_render,
if (b_cam) {
sync_camera_motion(b_render, b_cam, width, height, 0.0f);
}
sync_objects(b_depsgraph, b_screen, b_v3d);
}
sync_objects(b_depsgraph, b_screen, b_v3d);
/* In the viewport, only motion between previous frame and current frame is of interest, which is
* kept updated separately. */
if (b_v3d) {
assert(scene->need_motion() == Scene::MOTION_NONE ||
scene->need_motion() == Scene::MOTION_PASS_INTERACTIVE);
return;
}
if (scene->need_motion() == Scene::MOTION_NONE) {
return;
}
/* Insert motion times from camera. Motion times from other objects

View file

@ -225,9 +225,25 @@ void BlenderSync::sync_pointcloud(PointCloud *pointcloud, BObjectInfo &b_ob_info
0.0f;
export_pointcloud(scene, &new_pointcloud, *b_pointcloud, need_motion, motion_scale);
pointcloud->clear_non_sockets();
if (scene->need_motion() == Scene::MOTION_PASS_INTERACTIVE &&
pointcloud->num_points() == new_pointcloud.num_points())
{
new_pointcloud.set_motion_steps(2);
Attribute *attr_mP = pointcloud->attributes.find(ATTR_STD_MOTION_VERTEX_POSITION);
Attribute *new_attr_mP = new_pointcloud.attributes.add(ATTR_STD_MOTION_VERTEX_POSITION);
if (attr_mP) {
new_attr_mP->set_data_from(std::move(*attr_mP));
}
else {
new_pointcloud.copy_center_to_motion_step(0);
}
}
/* Update original sockets. */
pointcloud->clear_non_sockets();
for (const SocketType &socket : new_pointcloud.type->inputs) {
/* Those sockets are updated in sync_object, so do not modify them. */
if (socket.name == "use_motion_blur" || socket.name == "used_shaders") {

View file

@ -326,12 +326,8 @@ void BlenderSync::sync_data(blender::RenderData &b_render,
geometry_synced.clear(); /* use for objects and motion sync */
if (scene->need_motion() == Scene::MOTION_NONE || scene->need_motion() == Scene::MOTION_PASS ||
scene->camera->get_motion_position() == MOTION_POSITION_CENTER)
{
sync_objects(b_depsgraph, b_screen, b_v3d);
}
sync_motion(b_render, b_depsgraph, b_screen, b_v3d, b_rv3d, width, height, python_thread_state);
sync_objects_and_motion(
b_render, b_depsgraph, b_screen, b_v3d, b_rv3d, width, height, python_thread_state);
geometry_synced.clear();

View file

@ -126,14 +126,14 @@ class BlenderSync {
blender::bScreen *b_screen,
blender::View3D *b_v3d,
const float motion_time = 0.0f);
void sync_motion(blender::RenderData &b_render,
blender::Depsgraph &b_depsgraph,
blender::bScreen *b_screen,
blender::View3D *b_v3d,
blender::RegionView3D *b_rv3d,
const int width,
const int height,
void **python_thread_state);
void sync_objects_and_motion(blender::RenderData &b_render,
blender::Depsgraph &b_depsgraph,
blender::bScreen *b_screen,
blender::View3D *b_v3d,
blender::RegionView3D *b_rv3d,
const int width,
const int height,
void **python_thread_state);
void sync_film(blender::ViewLayer &b_view_layer,
blender::bScreen *b_screen,
blender::View3D *b_v3d);

View file

@ -598,4 +598,66 @@ ccl_device_inline float3 camera_world_to_ndc(KernelGlobals kg,
return P;
}
/* Motion vector for motion pass */
ccl_device_forceinline float4 camera_motion_vector_direction(KernelGlobals kg, const float3 D)
{
Transform tfm;
float3 motion_center;
float3 motion_pre;
float3 motion_post;
/* Camera motion, for perspective/orthographic motion.pre/post will be a
* world-to-raster matrix, for panorama it's world-to-camera, for custom
* we fall back to the world position until we have inverse mapping for it */
if (kernel_data.cam.type == CAMERA_CUSTOM) {
/* TODO: Custom cameras don't have inverse mappings yet, so we fall back to
* camera-space vectors here for now. */
tfm = kernel_data.cam.worldtocamera;
motion_center = normalize(transform_direction(&tfm, D));
tfm = kernel_data.cam.motion_pass_pre;
motion_pre = normalize(transform_direction(&tfm, D));
tfm = kernel_data.cam.motion_pass_post;
motion_post = normalize(transform_direction(&tfm, D));
}
else if (kernel_data.cam.type != CAMERA_PANORAMA) {
/* Perspective and orthographics camera use the world-to-raster matrix. */
ProjectionTransform projection = kernel_data.cam.worldtoraster;
motion_center = transform_perspective_direction(&projection, D);
projection = kernel_data.cam.perspective_pre;
motion_pre = transform_perspective_direction(&projection, D);
projection = kernel_data.cam.perspective_post;
motion_post = transform_perspective_direction(&projection, D);
}
else {
/* Panorama cameras have their own inverse mappings. */
tfm = kernel_data.cam.worldtocamera;
motion_center = normalize(transform_direction(&tfm, D));
motion_center = make_float3(direction_to_panorama(&kernel_data.cam, motion_center));
motion_center.x *= kernel_data.cam.width;
motion_center.y *= kernel_data.cam.height;
tfm = kernel_data.cam.motion_pass_pre;
motion_pre = normalize(transform_direction(&tfm, D));
motion_pre = make_float3(direction_to_panorama(&kernel_data.cam, motion_pre));
motion_pre.x *= kernel_data.cam.width;
motion_pre.y *= kernel_data.cam.height;
tfm = kernel_data.cam.motion_pass_post;
motion_post = normalize(transform_direction(&tfm, D));
motion_post = make_float3(direction_to_panorama(&kernel_data.cam, motion_post));
motion_post.x *= kernel_data.cam.width;
motion_post.y *= kernel_data.cam.height;
}
motion_pre = motion_pre - motion_center;
motion_post = motion_center - motion_post;
return make_float4(motion_pre.x, motion_pre.y, motion_post.x, motion_post.y);
}
CCL_NAMESPACE_END

View file

@ -211,6 +211,12 @@ ccl_device_inline void film_write_data_passes_background(
film_overwrite_pass_float3(buffer + kernel_data.film.pass_position, zero_float3());
}
}
if (flag & PASSMASK(MOTION)) {
const float4 speed = camera_motion_vector_direction(kg, INTEGRATOR_STATE(state, ray, D));
film_write_pass_float4(buffer + kernel_data.film.pass_motion, speed);
film_write_pass_float(buffer + kernel_data.film.pass_motion_weight, 1.0f);
}
}
#endif
}

View file

@ -226,13 +226,20 @@ ccl_device_forceinline void primitive_motion_data_without_camera(KernelGlobals k
if (is_attribute_found(desc)) {
/* get motion info */
const int numverts = kernel_data_fetch(objects, sd->object).numverts;
const ccl_global KernelObject *kobject = &kernel_data_fetch(objects, sd->object);
const int numverts = kobject->numverts;
const int num_motion_steps = kobject->num_geom_steps;
#if defined(__HAIR__) || defined(__POINTCLOUD__)
if (is_curve_or_point) {
*motion_pre = make_float3(primitive_surface_attribute<float4>(kg, sd, desc));
desc.offset += numverts;
*motion_post = make_float3(primitive_surface_attribute<float4>(kg, sd, desc));
if (num_motion_steps > 2) {
desc.offset += numverts;
*motion_post = make_float3(primitive_surface_attribute<float4>(kg, sd, desc));
}
else {
object_inverse_position_transform(kg, sd, motion_post);
}
/* Curve */
if ((sd->object_flag & SD_OBJECT_HAS_VERTEX_MOTION) == 0) {
@ -246,8 +253,13 @@ ccl_device_forceinline void primitive_motion_data_without_camera(KernelGlobals k
{
/* Triangle */
*motion_pre = triangle_attribute<float3>(kg, sd, desc);
desc.offset += numverts;
*motion_post = triangle_attribute<float3>(kg, sd, desc);
if (num_motion_steps > 2) {
desc.offset += numverts;
*motion_post = triangle_attribute<float3>(kg, sd, desc);
}
else {
object_inverse_position_transform(kg, sd, motion_post);
}
}
}

View file

@ -361,7 +361,7 @@ void Camera::update(Scene *scene)
have_motion = have_motion || motion[i] != matrix;
}
if (need_motion == Scene::MOTION_PASS) {
if (need_motion == Scene::MOTION_PASS || need_motion == Scene::MOTION_PASS_INTERACTIVE) {
if (have_motion) {
kcam->motion_pass_pre = transform_inverse(motion[0]);
kcam->motion_pass_post = transform_inverse(motion[motion.size() - 1]);
@ -506,6 +506,20 @@ void Camera::update(Scene *scene)
previous_need_motion = need_motion;
}
void Camera::update_interactive_motion()
{
array<Transform> motion = get_motion();
if (!motion.empty()) {
motion[0] = matrix;
/* Trigger another update if there was motion compared to previous frame, so that last viewport
* camera movement does not stick around. */
set_motion(motion);
}
set_fov_pre(fov);
}
void Camera::device_update(Device * /*device*/, DeviceScene *dscene, Scene *scene)
{
update(scene);

View file

@ -208,6 +208,8 @@ class Camera : public Node {
void update(Scene *scene);
void update_interactive_motion();
void device_update(Device *device, DeviceScene *dscene, Scene *scene);
void device_update_volume(Device *device, DeviceScene *dscene, Scene *scene);
void device_free(Device *device, DeviceScene *dscene, Scene *scene);

View file

@ -245,7 +245,8 @@ void Film::device_update(Device *device, DeviceScene *dscene, Scene *scene)
/* Can't do motion pass if no motion vectors are available. */
if (pass->get_type() == PASS_MOTION || pass->get_type() == PASS_MOTION_WEIGHT) {
if (scene->need_motion() != Scene::MOTION_PASS) {
const Scene::MotionType need_motion = scene->need_motion();
if (need_motion != Scene::MOTION_PASS && need_motion != Scene::MOTION_PASS_INTERACTIVE) {
kfilm->pass_stride += pass->get_info().num_components;
continue;
}

View file

@ -173,6 +173,56 @@ GeometryManager::GeometryManager()
GeometryManager::~GeometryManager() = default;
void GeometryManager::update_interactive_motion(Scene *scene)
{
bool update = false;
parallel_for(blocked_range<size_t>(0, scene->geometry.size(), 32),
[&](const blocked_range<size_t> &r) {
for (size_t i = r.begin(); i != r.end(); i++) {
Geometry *geom = scene->geometry[i];
Attribute *attr_mP = geom->attributes.find(ATTR_STD_MOTION_VERTEX_POSITION);
if (attr_mP) {
if (geom->is_mesh()) {
Mesh *mesh = static_cast<Mesh *>(geom);
if (std::memcmp(mesh->get_verts().data(),
attr_mP->data_float3(),
sizeof(float3) * mesh->num_verts()) != 0)
{
mesh->copy_center_to_motion_step(0);
attr_mP->modified = update = true;
}
}
else if (geom->is_hair()) {
Hair *hair = static_cast<Hair *>(geom);
if (std::memcmp(hair->get_curve_keys().data(),
attr_mP->data_float3(),
sizeof(float3) * hair->num_keys()) != 0)
{
hair->copy_center_to_motion_step(0);
attr_mP->modified = update = true;
}
}
else if (geom->is_pointcloud()) {
PointCloud *pointcloud = static_cast<PointCloud *>(geom);
if (std::memcmp(pointcloud->get_points().data(),
attr_mP->data_float3(),
sizeof(float3) * pointcloud->num_points()) != 0)
{
pointcloud->copy_center_to_motion_step(0);
attr_mP->modified = update = true;
}
}
}
}
});
if (update) {
tag_update(scene, TRANSFORM_MODIFIED);
}
}
void GeometryManager::update_osl_globals(Device *device, Scene *scene)
{
#ifdef WITH_OSL

View file

@ -251,6 +251,8 @@ class GeometryManager {
GeometryManager();
~GeometryManager();
void update_interactive_motion(Scene *scene);
/* Device Updates */
void device_update_preprocess(Device *device, Scene *scene, Progress &progress);
void device_update(Device *device, DeviceScene *dscene, Scene *scene, Progress &progress);

View file

@ -74,6 +74,10 @@ class Mesh : public Geometry {
return tri;
}
size_t num_verts() const
{
return verts.size();
}
size_t num_triangles() const
{
return triangles.size() / 3;

View file

@ -143,6 +143,13 @@ void Object::update_motion()
motion.clear();
return;
}
if (have_motion && i == (motion.size() - 1)) {
/* Remove last motion when it is not actually set. */
motion.resize(motion.size() - 1);
return;
}
/* Otherwise just copy center motion. */
motion[i] = tfm;
}
@ -496,6 +503,40 @@ ObjectManager::ObjectManager()
ObjectManager::~ObjectManager() = default;
void ObjectManager::update_interactive_motion(Scene *scene)
{
bool update = false;
parallel_for(blocked_range<size_t>(0, scene->objects.size(), 32),
[&](const blocked_range<size_t> &r) {
for (size_t i = r.begin(); i != r.end(); i++) {
Object *ob = scene->objects[i];
const bool use_motion = ob->use_motion();
array<Transform> motion = ob->get_motion();
if (motion.empty()) {
/* Can always store current matrix in motion array with a single element,
* since that still causes 'use_motion()' to return false. */
motion.resize(1);
}
motion[0] = ob->tfm;
/* Trigger another update if there was motion compared to previous frame, so
* that last movement does not stick around. */
ob->set_motion(motion);
if (use_motion && ob->motion_is_modified()) {
update = true;
}
}
});
if (update) {
tag_update(scene, TRANSFORM_MODIFIED);
}
}
static float object_volume_density(const Transform &tfm, Geometry *geom)
{
if (geom->is_volume()) {
@ -596,7 +637,9 @@ void ObjectManager::device_update_object_transform(UpdateObjectTransformState *s
}
}
if (state->need_motion == Scene::MOTION_PASS) {
if (state->need_motion == Scene::MOTION_PASS ||
state->need_motion == Scene::MOTION_PASS_INTERACTIVE)
{
/* Clear motion array if there is no actual motion. */
ob->update_motion();
@ -752,7 +795,9 @@ void ObjectManager::device_update_transforms(DeviceScene *dscene, Scene *scene,
state.object_motion = nullptr;
state.object_motion_pass = nullptr;
if (state.need_motion == Scene::MOTION_PASS) {
if (state.need_motion == Scene::MOTION_PASS ||
state.need_motion == Scene::MOTION_PASS_INTERACTIVE)
{
state.object_motion_pass = dscene->object_motion_pass.alloc(OBJECT_MOTION_PASS_SIZE *
scene->objects.size());
}
@ -801,7 +846,9 @@ void ObjectManager::device_update_transforms(DeviceScene *dscene, Scene *scene,
}
dscene->objects.copy_to_device_if_modified();
if (state.need_motion == Scene::MOTION_PASS) {
if (state.need_motion == Scene::MOTION_PASS ||
state.need_motion == Scene::MOTION_PASS_INTERACTIVE)
{
dscene->object_motion_pass.copy_to_device();
}
else if (state.need_motion == Scene::MOTION_BLUR) {
@ -1092,7 +1139,8 @@ void ObjectManager::apply_static_transforms(DeviceScene *dscene, Scene *scene, P
map<Geometry *, int> geometry_users;
const Scene::MotionType need_motion = scene->need_motion();
const bool motion_blur = need_motion == Scene::MOTION_BLUR;
const bool apply_to_motion = need_motion != Scene::MOTION_PASS;
const bool apply_to_motion = need_motion != Scene::MOTION_PASS &&
need_motion != Scene::MOTION_PASS_INTERACTIVE;
int i = 0;
for (Object *object : scene->objects) {

View file

@ -167,6 +167,8 @@ class ObjectManager {
ObjectManager();
~ObjectManager();
void update_interactive_motion(Scene *scene);
void device_update(Device *device, DeviceScene *dscene, Scene *scene, Progress &progress);
void device_update_transforms(DeviceScene *dscene, Scene *scene, Progress &progress);
void device_update_prim_offsets(Device *device, DeviceScene *dscene, Scene *scene);

View file

@ -395,6 +395,13 @@ void Scene::device_update(Device *device_, Progress &progress)
device->optimize_for_scene(this);
if (need_motion() == MOTION_PASS_INTERACTIVE) {
/* Swap current camera/object/vertex positions to previous positions for next frame. */
camera->update_interactive_motion();
object_manager->update_interactive_motion(this);
geometry_manager->update_interactive_motion(this);
}
if (print_stats) {
const size_t mem_used = util_guarded_get_mem_used();
const size_t mem_peak = util_guarded_get_mem_peak();
@ -412,17 +419,20 @@ Scene::MotionType Scene::need_motion() const
if (integrator->get_motion_blur()) {
return MOTION_BLUR;
}
if (Pass::contains(passes, PASS_MOTION) ||
Pass::contains(passes, PASS_DENOISING_BACKWARD_MOTION))
const bool denoiser_motion = integrator->get_use_denoise() &&
(integrator->get_denoiser_passes() &
(DENOISER_PASS_MOTION | DENOISER_PASS_BACKWARD_MOTION)) != 0;
if (denoiser_motion || (Pass::contains(passes, PASS_MOTION) ||
Pass::contains(passes, PASS_DENOISING_BACKWARD_MOTION)))
{
return MOTION_PASS;
return params.background ? MOTION_PASS : MOTION_PASS_INTERACTIVE;
}
return MOTION_NONE;
}
float Scene::motion_shutter_time()
{
if (need_motion() == Scene::MOTION_PASS) {
if (need_motion() == Scene::MOTION_PASS || need_motion() == Scene::MOTION_PASS_INTERACTIVE) {
return 2.0f;
}
return camera->get_shuttertime();

View file

@ -199,7 +199,7 @@ class Scene : public NodeOwner {
bool need_global_attribute(AttributeStandard std) const;
void need_global_attributes(AttributeRequestSet &attributes);
enum MotionType { MOTION_NONE = 0, MOTION_PASS, MOTION_BLUR };
enum MotionType { MOTION_NONE = 0, MOTION_PASS, MOTION_BLUR, MOTION_PASS_INTERACTIVE };
MotionType need_motion() const;
float motion_shutter_time();