Kinematic Support Geometry#
Kinematic support geometry is moved by an external controller while dynamic bodies respond to it through contact. Pallets, fixtures, end-effector-held supports, and conveyors all fit this description, but they use two different motion mechanisms.
Translating Supports and Kinematic Targets#
Use a kinematic rigid body when the support itself moves through space.
Before you begin, confirm the following:
The stage authors a
PhysicsSceneprim, the support body, and a dynamic rider body resting on the support (refer to Physics Scene).DirectGPU (
eENABLE_DIRECT_GPU_API) is disabled. The ovstage transform-update path that drives a kinematic target is not applied to rigid actors under DirectGPU.The producer that publishes transforms can write both the local and the resolved world transform of the support at the same ovstage ordinal.
Author and drive the support as follows:
Apply
PhysicsRigidBodyAPIandPhysicsCollisionAPIto the body.Set
physics:kinematicEnabled = true.Bind a physics material with enough static and dynamic friction for the load.
Write a new target pose before each step.
The support is driven correctly when session reads of RIGID_BODY position
and orientation place the support at the pose you wrote for that step, and the rider’s position along the
motion axis has advanced with it over successive steps. A rider displacement near
zero while the support moves means the support was teleported rather than driven
to a kinematic target, which is the failure the shipped samples check for.
Publish the body’s local omni:xform and resolved
omni:fabric:worldMatrix attributes through ovstage at a new control ordinal,
seal the ordinal, and pass it to update_from_ovstage() before stepping. For a
kinematic body, the runtime converts that transform update to
PxRigidDynamic::setKinematicTarget(). Preserve the body’s authored scale in
both matrices; changing scale is structural and reconstructs the PhysX actor
instead of updating its target.
The local and world matrices are equal only when the body’s parent has an
identity world transform, as in the shipped samples. Under a transformed
parent, compose omni:fabric:worldMatrix from the local transform and the
parent’s resolved world transform. If a producer moves a parent, it must also
republish the affected descendants’ resolved world matrices. A gRPC or other
transport should express the same operation as consistent local and world
ovstage writes, seal the control ordinal, and drain it before stepping.
This transform-update path is not applied to rigid actors after DirectGPU
(eENABLE_DIRECT_GPU_API) is enabled. Leave DirectGPU disabled when controlling
kinematic supports through ovstage. GPU dynamics without DirectGPU can still be
used.
Do not substitute a legacy RIGID_BODY_POSE tensor write. That path calls
setGlobalPose and teleports the body; it does not provide the contact velocity
needed to carry a rider. The tensor-binding write surface is deprecated (superseded by the
session write API, ovphysx_write / PhysX.write) and does not expose a separate
kinematic-target tensor.
C++ applications can also read physxPointer and call
PxRigidDynamic::setKinematicTarget() directly. Refer to
PhysX Interop.
Stationary Conveyors and Surface Velocity#
Use PhysxSurfaceVelocityAPI when the support stays in place but its contact
surface moves:
#usda 1.0
def Cube "Conveyor" (
prepend apiSchemas = [
"PhysicsCollisionAPI",
"PhysicsRigidBodyAPI",
"PhysxSurfaceVelocityAPI"
]
)
{
bool physics:kinematicEnabled = true
bool physxSurfaceVelocity:surfaceVelocityEnabled = true
vector3f physxSurfaceVelocity:surfaceVelocity = (1, 0, 0)
bool physxSurfaceVelocity:surfaceVelocityLocalSpace = false
}
Apply the surface-velocity API to the rigid-body prim, not only to a child
collider. Runtime changes use the same physxSurfaceVelocity:* attributes
through the ovstage control-ordinal path. Nonzero physics:velocity and
physics:angularVelocity on a kinematic body are retained as a legacy
surface-velocity shortcut, but new code should use
PhysxSurfaceVelocityAPI.
Surface velocity uses contact modification. Leave DirectGPU disabled for scenes that need it. GPU dynamics without DirectGPU can still be used.
Combining Target Motion and Surface Velocity#
The two mechanisms are additive. A kinematic target contributes the support’s
physical step velocity; PhysxSurfaceVelocityAPI contributes an additional
contact-target velocity. Use both only when the intended surface motion is
relative to an already-moving support. Adding surface velocity to compensate
for a teleported pallet is not equivalent to driving a kinematic target and can
double-drive cargo after the target path is corrected.
The C and Python samples in Runnable Samples exercise three isolated lanes in one scene:
a translating platform driven by ovstage transform updates;
a stationary platform driven only by surface velocity;
a translating platform with additional surface velocity.
Each lane checks rider displacement, and the combined lane checks that its motion is observably greater than either independent lane.
Sleep, Friction, and Ordering#
A surface-velocity change does not wake a body that has settled to sleep.
Disable sleeping for continuously controlled loads by setting
physxRigidBody:sleepThreshold = 0, or write the session wake attribute with
nonzero values for the selected dynamic bodies immediately before motion begins.
Use a prim-set query to include those bodies while they are asleep; see
Wake and sleep commands. Kinematic support motion and surface velocity
also require sufficient friction to transmit tangential motion; bind an
explicit physics material instead of relying on defaults.
Before you enter the control loop, confirm the following:
The stage is attached, and the first ordinal the loop writes is above the ordinal that
attach_ovstage()consumed.Sleeping is disabled on the controlled loads, or the loop wakes them before motion begins.
DirectGPU is disabled, as required by both mechanisms on this page.
Then, for each frame:
Publish and seal ovstage transform and surface-velocity control edits.
Call
update_from_ovstage()for exactly those control ordinals and wait for its task.Step and wait for completion.
Read poses and velocities.
The loop is correct when each read pose matches the edit published for that ordinal, and the rider’s displacement grows monotonically across frames instead of stalling after the first one. A rider that stops advancing while edits keep arriving indicates a sleeping load or insufficient friction.
Reset and Readback#
Wait successfully for detach_ovstage(), then reload or reattach the baseline scene for deterministic
reruns. Replaying only poses and velocities from an in-contact mid-run state
does not restore PhysX contact caches and is not a deterministic reset.
Read resolved runtime state through the session read (PhysX.read /
ovphysx_read), selecting the rider by its path as the samples below do.
Reading the source USD only shows authored values; it does not prove that the
live PhysX actor received a target or that contact carried the rider.
Runnable Samples#
The Python sample loads kinematic_support.usda, which ships with every package under
ovphysx/samples/data/ in the wheel, <sdk-root>/samples/data/ in the C/C++ SDK,
and tests/data/ in a repository checkout.
Python:
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# @implements REQ-USD-KINEMATIC-SUPPORT-001
# @covers AC-1 AC-3
"""Kinematic support sample: transform motion, surface velocity, and both."""
# @implements REQ-CAPI-OVSTAGE-XFORM-001
# @covers AC-1
# @implements REQ-PYTHON-SAMPLE-002
# @covers AC-1 AC-2
from pathlib import Path
import numpy as np
import ovstage
import ovphysx
from ovphysx.samples.python_samples.schema_support import physx_schema_paths
from ovphysx import PhysX
from ovphysx.types import SimObjectType
_physx_schemas_registered = False
def attach_scene(physx, usd_path):
if not ovstage.population.available():
raise RuntimeError("ovstage population bridge is unavailable")
# Register before population; this scene can omit the Newton provider.
global _physx_schemas_registered
if not _physx_schemas_registered:
schema_roots = [str(p) for p in physx_schema_paths()]
try:
schema_roots.append(str(ovphysx.newton_schema_root()))
except FileNotFoundError:
pass # newton-usd-schemas is optional; this scene has no newton:* attributes.
ovstage.population.register_usd_schemas(schema_roots)
_physx_schemas_registered = True
stage = ovstage.Stage("ovphysx-kinematic-support-sample")
try:
ovstage.population.open_usd(
stage, str(usd_path), ordinal=1, domains=ovstage.PopulationDomain.ALL
)
stage.advance_write_floor(ordinal=1).wait()
physx.wait_task(physx.attach_ovstage(stage, read_ordinal=1))
return stage
except Exception:
if physx.is_stage_attached(stage):
physx.wait_task(physx.detach_ovstage())
stage.destroy()
raise
RIDERS = ("/World/TargetRider", "/World/ConveyorRider", "/World/CombinedRider")
def read_rider_x(physx, stage):
"""World x of each rider, in RIDERS order, through one read selected by the riders' paths."""
with ovstage.PathDictionary(stage) as paths:
handles = [paths.intern_path(path) for path in RIDERS]
x = {}
with physx.read_sync(SimObjectType.RIGID_BODY, ["position"], prim_paths=handles) as result:
for group in result.groups:
listed = paths.get_paths(group.prim_list)
positions = group.tensors[0].numpy().reshape(group.prim_count, -1)
for row in range(group.prim_count):
x[paths.path_to_string(listed[group.prim_offset + row])] = float(positions[row, 0])
missing = [path for path in RIDERS if path not in x]
if missing:
raise RuntimeError(f"Expected a rigid body at each of {missing}")
return np.array([x[path] for path in RIDERS])
class TransformWriter:
"""Publish world transforms for one prim through an ovstage query."""
def __init__(self, stage, path):
self._stage = stage
self._paths = ovstage.PathDictionary(stage)
self._path_list = None
self._query = None
try:
self._path_list = self._paths.create_path_list_from_strings([path])
self._query = stage.query_from_path_list(self._path_list)
except Exception:
if self._path_list is not None:
self._paths.destroy_path_list(self._path_list)
self._paths.destroy()
raise
def write_attribute(self, attribute, tensor, ordinal):
self._stage.write_attribute(
self._query, attribute, ordinal, tensor, is_array=False
).wait()
def write_pose(self, x, z, ordinal):
target = np.eye(4, dtype=np.float64)
target[0, 0] = 6.0
target[2, 2] = 6.0
target[3, :3] = (x, 0.0, z)
tensor = ovstage.make_dltensor(
target,
dtype=ovstage.DLDataType(ovstage.DLDataTypeCode.kDLFloat, 64, 16),
shape=[1],
ndim=1,
)
for attribute in ("omni:xform", "omni:fabric:worldMatrix"):
self.write_attribute(attribute, tensor, ordinal)
def destroy(self):
if self._query is not None:
self._stage.release_query(self._query).wait()
self._query = None
if self._path_list is not None:
self._paths.destroy_path_list(self._path_list)
self._path_list = None
self._paths.destroy()
def main():
PhysX.set_cpu_mode(True)
physx = PhysX()
stage = None
writers = []
try:
# Prefer package data so a copied sample works. Fall back to the checked-in
# sample's adjacent data directory when package data is absent.
usd_path = Path(ovphysx.__file__).resolve().parent / "samples" / "data" / "kinematic_support.usda"
if not usd_path.is_file():
usd_path = Path(__file__).resolve().parent.parent / "data" / "kinematic_support.usda"
if not usd_path.is_file():
raise FileNotFoundError(f"ovphysx sample data is missing: {usd_path}")
stage = attach_scene(physx, usd_path)
physx.wait_all()
target_platform = TransformWriter(stage, "/World/TargetPlatform")
writers.append(target_platform)
combined_platform = TransformWriter(stage, "/World/CombinedPlatform")
writers.append(combined_platform)
dt = 1.0 / 60.0
for _ in range(10):
physx.step_sync(dt)
start = read_rider_x(physx, stage)
for frame in range(1, 121):
x = frame * dt
ordinal = frame + 1
target_platform.write_pose(x, 0.0, ordinal)
combined_platform.write_pose(x, 16.0, ordinal)
stage.advance_write_floor(ordinal=ordinal).wait()
physx.wait_task(physx.update_from_ovstage(ordinal, ordinal))
physx.step_sync(dt)
displacement = read_rider_x(physx, stage) - start
target_dx, conveyor_dx, combined_dx = displacement
print(
"Rider displacement: "
f"transform={target_dx:.3f}, surface={conveyor_dx:.3f}, combined={combined_dx:.3f}"
)
if target_dx < 0.5:
raise RuntimeError("ovstage transform did not carry its rider")
if conveyor_dx < 0.5:
raise RuntimeError("Surface velocity did not carry its rider")
if combined_dx <= max(target_dx, conveyor_dx) + 0.2:
raise RuntimeError("Combined transform and surface velocity were not observably additive")
finally:
for writer in reversed(writers):
writer.destroy()
if stage is not None:
physx.wait_task(physx.detach_ovstage())
stage.destroy()
physx.destroy()
if __name__ == "__main__":
main()
C:
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
/**
* @implements REQ-USD-KINEMATIC-SUPPORT-001
* @covers AC-1 AC-3
*/
// Kinematic support sample: transform motion, surface velocity, and both.
#include <ovphysx/ovphysx.h>
#include <ovphysx/ovphysx_attributes.h>
#include <ovphysx/ovphysx_types.h>
#include <ovx/path_dictionary/path_dictionary.h>
#include "ovstage_sample.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <stdatomic.h>
#endif
#ifdef __cplusplus
#error "This file must be compiled as C, not C++"
#endif
#ifdef _WIN32
typedef volatile LONG object_change_counter_t;
#else
typedef atomic_uint object_change_counter_t;
#endif
static void object_change_counter_init(object_change_counter_t* counter)
{
#ifdef _WIN32
InterlockedExchange(counter, 0);
#else
atomic_init(counter, 0);
#endif
}
static void object_change_counter_increment(object_change_counter_t* counter)
{
#ifdef _WIN32
InterlockedIncrement(counter);
#else
atomic_fetch_add_explicit(counter, 1, memory_order_relaxed);
#endif
}
static unsigned int object_change_counter_load(object_change_counter_t* counter)
{
#ifdef _WIN32
return (unsigned int)InterlockedCompareExchange(counter, 0, 0);
#else
return atomic_load_explicit(counter, memory_order_relaxed);
#endif
}
typedef struct path_query_t
{
ovstage_query_handle_t query;
ovx_primpath_list_t path_list;
} path_query_t;
typedef struct object_change_counts_t
{
object_change_counter_t created;
object_change_counter_t destroyed;
} object_change_counts_t;
static void on_object_created(
ovphysx_string_t prim_path,
ovphysx_physx_type_t type,
void* user_data)
{
(void)prim_path;
(void)type;
object_change_counts_t* counts = (object_change_counts_t*)user_data;
object_change_counter_increment(&counts->created);
}
static void on_object_destroyed(
ovphysx_string_t prim_path,
ovphysx_physx_type_t type,
void* user_data)
{
(void)prim_path;
(void)type;
object_change_counts_t* counts = (object_change_counts_t*)user_data;
object_change_counter_increment(&counts->destroyed);
}
static int check_result(ovphysx_result_t result, const char* operation)
{
if (result.status == OVPHYSX_API_SUCCESS)
return 1;
const ovphysx_string_t error = ovphysx_get_last_error();
fprintf(stderr, "%s failed: %.*s\n", operation, (int)error.length, error.ptr ? error.ptr : "");
return 0;
}
static int wait_step(ovphysx_instance_handle_t handle, float dt)
{
const ovphysx_enqueue_result_t step = ovphysx_step(handle, dt);
if (step.status != OVPHYSX_API_SUCCESS)
return 0;
ovphysx_wait_task_result_t wait_result = { 0 };
const ovphysx_result_t waited =
ovphysx_wait_task(handle, step, OVPHYSX_TIMEOUT_INFINITE, &wait_result);
const int ok = waited.status == OVPHYSX_API_SUCCESS && wait_result.num_errors == 0;
ovphysx_destroy_wait_task_result(&wait_result);
return ok;
}
static int wait_ovstage(ovstage_instance_t* stage, ovstage_enqueue_result_t op)
{
if (op.status != OVSTAGE_OK)
{
fprintf(stderr, "ovstage enqueue failed: %s\n", ovstage_get_error_string(stage, op.status));
return 0;
}
if (op.op_index == OVSTAGE_INVALID_OP_ID)
return 1;
ovstage_op_wait_result_t wait_result;
memset(&wait_result, 0, sizeof(wait_result));
const ovstage_api_status_t status =
ovstage_wait_op(stage, op.op_index, OVSTAGE_TIMEOUT_INFINITE, &wait_result);
const int ok = status == OVSTAGE_OK && wait_result.error_op_id_count == 0;
if (!ok)
{
fprintf(stderr, "ovstage operation failed: %s\n", ovstage_get_error_string(stage, status));
for (size_t i = 0; i < wait_result.error_op_id_count; ++i)
{
const ovx_string_t error =
ovstage_get_last_op_error(stage, wait_result.error_op_ids[i]);
fprintf(stderr, " %.*s\n", (int)error.length, error.ptr ? error.ptr : "");
}
}
return ovstage_release_op(stage, op.op_index) == OVSTAGE_OK && ok;
}
static ovx_string_or_token_t attribute_name(const char* name)
{
ovx_string_or_token_t result;
memset(&result, 0, sizeof(result));
result.string.ptr = name;
result.string.length = strnlen(name, 256);
return result;
}
static int write_matrix(
ovstage_instance_t* stage,
ovstage_query_handle_t query,
const char* attribute,
ovstage_ordinal_t ordinal,
double matrix[16])
{
int64_t shape[1] = { 1 };
DLTensor tensor;
memset(&tensor, 0, sizeof(tensor));
tensor.data = matrix;
tensor.device.device_type = kDLCPU;
tensor.dtype.code = kDLFloat;
tensor.dtype.bits = 64;
tensor.dtype.lanes = 16;
tensor.ndim = 1;
tensor.shape = shape;
ovstage_write_data_t write;
memset(&write, 0, sizeof(write));
write.tensors = &tensor;
write.tensor_count = 1;
return wait_ovstage(
stage,
ovstage_write_attribute(
stage, query, attribute_name(attribute), ordinal, write, OVSTAGE_PRIM_MODE_UPSERT));
}
static int write_transform(
ovstage_instance_t* stage,
ovstage_query_handle_t query,
ovstage_ordinal_t ordinal,
double x,
double z)
{
double matrix[16] = {
6.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 6.0, 0.0,
x, 0.0, z, 1.0
};
return write_matrix(stage, query, "omni:xform", ordinal, matrix) &&
write_matrix(stage, query, "omni:fabric:worldMatrix", ordinal, matrix);
}
static int create_path_query(
ovstage_instance_t* stage,
const char* path,
path_query_t* path_query)
{
path_dictionary_instance_t* dictionary = ovstage_get_path_dictionary(stage);
if (!dictionary)
return 0;
ovx_string_t path_string = { path, strnlen(path, 256) };
ovx_api_result_t created =
dictionary->vtable->create_path_list_from_strings(
dictionary->context, &path_string, 1, &path_query->path_list);
if (created.status != OVX_API_SUCCESS ||
path_query->path_list == OVX_INVALID_PRIMPATH_LIST)
{
if (created.error.ptr)
dictionary->vtable->release_error(dictionary->context, created.error);
return 0;
}
const ovstage_api_status_t queried =
ovstage_query_from_path_list(stage, path_query->path_list, &path_query->query);
return queried == OVSTAGE_OK && path_query->query != OVSTAGE_INVALID_QUERY_HANDLE;
}
static int destroy_path_query(
ovstage_instance_t* stage,
path_query_t* path_query)
{
int ok = 1;
if (path_query->query != OVSTAGE_INVALID_QUERY_HANDLE)
{
ok = wait_ovstage(stage, ovstage_release_query(stage, path_query->query));
path_query->query = OVSTAGE_INVALID_QUERY_HANDLE;
}
if (path_query->path_list != OVX_INVALID_PRIMPATH_LIST)
{
path_dictionary_instance_t* dictionary = ovstage_get_path_dictionary(stage);
if (!dictionary)
return 0;
ovx_api_result_t released =
dictionary->vtable->release_path_list_reference(
dictionary->context, path_query->path_list);
path_query->path_list = OVX_INVALID_PRIMPATH_LIST;
if (released.error.ptr)
dictionary->vtable->release_error(dictionary->context, released.error);
ok = released.status == OVX_API_SUCCESS && ok;
}
return ok;
}
static int publish_control_ordinal(
ovphysx_instance_handle_t handle,
ovstage_instance_t* stage,
ovstage_ordinal_t ordinal)
{
ovstage_write_floor_desc_t floor;
memset(&floor, 0, sizeof(floor));
floor.ordinal = ordinal;
floor.scope = OVSTAGE_SCOPE_ALL;
if (!wait_ovstage(stage, ovstage_advance_write_floor(stage, &floor)))
return 0;
const ovstage_ordinal_range_t range = { ordinal, ordinal, true };
return check_result(
ovphysx_sample_complete_task(handle, ovphysx_update_from_ovstage(handle, range)),
"update from ovstage");
}
// Reads the world x of the rider at `path` through a session read selected by the rider's path.
static int read_x(
ovphysx_instance_handle_t handle,
const char* path,
float* x)
{
ovphysx_query_selection_t selection;
memset(&selection, 0, sizeof(selection));
selection.pattern.ptr = path;
selection.pattern.length = strnlen(path, 256);
ovphysx_query_handle_t query = 0;
if (!check_result(
ovphysx_sample_complete_task(
handle, ovphysx_query_selection(handle, OVPHYSX_QUERY_TARGET_RIGID_BODY, &selection, &query, NULL)),
"open rider query"))
return 0;
ovx_string_or_token_t position;
memset(&position, 0, sizeof(position));
position.string.ptr = OVPHYSX_ATTR_POSITION;
position.string.length = strlen(OVPHYSX_ATTR_POSITION);
ovphysx_read_handle_t read = 0;
const ovstage_read_group_t* group = NULL;
int ok = check_result(
ovphysx_sample_complete_task(handle, ovphysx_read(handle, query, &position, 1, &read, NULL)), "read rider pose");
if (ok)
ok = check_result(
ovphysx_sample_complete_task(handle, ovphysx_fetch_read_next(handle, read, &group)), "fetch rider pose");
if (ok && (!group || group->prims.count != 1 || group->data.tensor_count != 1 || !group->data.tensors[0].data))
{
fprintf(stderr, "Expected one rigid body at %s\n", path);
ok = 0;
}
if (ok)
{
// The sample runs in CPU mode, so the position column is host memory.
const DLTensor* tensor = &group->data.tensors[0];
*x = ((const float*)((const char*)tensor->data + tensor->byte_offset))[0];
}
if (group)
ok = check_result(
ovphysx_sample_complete_task(handle, ovphysx_release_group(handle, read, group->read_group_id)),
"release rider group") && ok;
if (read)
ok = check_result(
ovphysx_sample_complete_task(handle, ovphysx_release_read(handle, read)), "release rider read") && ok;
return check_result(
ovphysx_sample_complete_task(handle, ovphysx_release_query(handle, query)), "release rider query") && ok;
}
int main(void)
{
int exit_code = 1;
ovphysx_instance_handle_t handle = 0;
ovphysx_sample_stage_attachment_t attachment;
path_query_t target_platform = {
OVSTAGE_INVALID_QUERY_HANDLE, OVX_INVALID_PRIMPATH_LIST
};
path_query_t combined_platform = {
OVSTAGE_INVALID_QUERY_HANDLE, OVX_INVALID_PRIMPATH_LIST
};
object_change_counts_t object_changes;
ovphysx_subscription_id_t object_change_subscription =
OVPHYSX_INVALID_SUBSCRIPTION_ID;
memset(&attachment, 0, sizeof(attachment));
object_change_counter_init(&object_changes.created);
object_change_counter_init(&object_changes.destroyed);
ovphysx_set_cpu_mode(true);
if (!check_result(ovphysx_initialize(), "initialize"))
return exit_code;
if (!check_result(ovphysx_create_instance(&handle), "create instance"))
goto cleanup;
if (!ovphysx_sample_attach_usd_with_ovstage(
handle, OVPHYSX_TEST_DATA "/kinematic_support.usda", &attachment))
goto cleanup;
ovphysx_object_change_callbacks_t callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.on_object_created = on_object_created;
callbacks.on_object_destroyed = on_object_destroyed;
callbacks.user_data = &object_changes;
if (!check_result(
ovphysx_subscribe_object_changes(
handle, &callbacks, &object_change_subscription),
"subscribe to object changes"))
goto cleanup;
if (!create_path_query(attachment.stage, "/World/TargetPlatform", &target_platform) ||
!create_path_query(attachment.stage, "/World/CombinedPlatform", &combined_platform))
goto cleanup;
const float dt = 1.0f / 60.0f;
for (int frame = 0; frame < 10; ++frame)
{
if (!wait_step(handle, dt))
goto cleanup;
}
float target_start = 0.0f;
float conveyor_start = 0.0f;
float combined_start = 0.0f;
if (!read_x(handle, "/World/TargetRider", &target_start) ||
!read_x(handle, "/World/ConveyorRider", &conveyor_start) ||
!read_x(handle, "/World/CombinedRider", &combined_start))
goto cleanup;
for (int frame = 1; frame <= 120; ++frame)
{
const float x = frame * dt;
const ovstage_ordinal_t ordinal = (ovstage_ordinal_t)frame + attachment.ordinal;
if (!write_transform(attachment.stage, target_platform.query, ordinal, x, 0.0) ||
!write_transform(attachment.stage, combined_platform.query, ordinal, x, 16.0) ||
!publish_control_ordinal(handle, attachment.stage, ordinal) ||
!wait_step(handle, dt))
goto cleanup;
}
float target_end = 0.0f;
float conveyor_end = 0.0f;
float combined_end = 0.0f;
if (!read_x(handle, "/World/TargetRider", &target_end) ||
!read_x(handle, "/World/ConveyorRider", &conveyor_end) ||
!read_x(handle, "/World/CombinedRider", &combined_end))
goto cleanup;
const float target_dx = target_end - target_start;
const float conveyor_dx = conveyor_end - conveyor_start;
const float combined_dx = combined_end - combined_start;
printf("Rider displacement: transform=%.3f, surface=%.3f, combined=%.3f\n",
target_dx, conveyor_dx, combined_dx);
const float independent_max = target_dx > conveyor_dx ? target_dx : conveyor_dx;
const unsigned int created = object_change_counter_load(&object_changes.created);
const unsigned int destroyed = object_change_counter_load(&object_changes.destroyed);
printf("Physics object changes: created=%u, destroyed=%u\n", created, destroyed);
if (created != 0 || destroyed != 0)
fprintf(stderr,
"Transform updates reconstructed physics objects "
"(created=%u, destroyed=%u)\n",
created, destroyed);
else if (target_dx < 0.5f)
fprintf(stderr, "ovstage transform did not carry its rider\n");
else if (conveyor_dx < 0.5f)
fprintf(stderr, "Surface velocity did not carry its rider\n");
else if (combined_dx <= independent_max + 0.2f)
fprintf(stderr, "Combined transform and surface velocity were not observably additive\n");
else
exit_code = 0;
cleanup:
if (object_change_subscription != OVPHYSX_INVALID_SUBSCRIPTION_ID)
check_result(
ovphysx_unsubscribe_object_changes(handle, object_change_subscription),
"unsubscribe from object changes");
if (attachment.stage)
{
destroy_path_query(attachment.stage, &target_platform);
destroy_path_query(attachment.stage, &combined_platform);
}
if (handle)
{
ovphysx_sample_destroy_stage(handle, &attachment);
ovphysx_destroy_instance(handle);
}
ovphysx_shutdown();
return exit_code;
}