instancing tests/demo

This commit is contained in:
Nick Fisher
2025-03-28 14:06:12 +08:00
parent ecb8d8672a
commit 709fe35852

View File

@@ -1,8 +1,24 @@
@Timeout(const Duration(seconds: 600))
import 'dart:math';
import 'package:test/test.dart';
import 'package:thermion_dart/src/filament/src/asset.dart';
import 'package:thermion_dart/src/filament/src/filament_app.dart';
import 'package:vector_math/vector_math_64.dart';
import 'helpers.dart';
// Helper class to store physics state for each instance
class PhysicsState {
final ThermionAsset instance;
Vector3 position = Vector3.zero();
final double scale;
Vector3 velocity = Vector3.zero();
bool launched = false;
bool addedToScene = false; // Track if added to prevent multiple adds
PhysicsState(this.instance, this.scale);
}
void main() async {
final testHelper = TestHelper("instancing");
await testHelper.setup();
@@ -23,18 +39,181 @@ void main() async {
await viewer.setPostProcessing(true);
await viewer.setAntiAliasing(false, true, false);
// Loading a glTF asset always creates a single instance behind the scenes,
// but the entities exposed by the asset are used to manipulate all
// instances as a group, not the singular "default" instance.
// If you are only creating a single instance (the default behaviour),
// then you don't need to worry about the difference.
//
// When creating multiple instances, however,you usually want to work
// with each instance individually, rather than the owning asset.
var asset = await viewer.loadGltf(
"file://${testHelper.testDir}/assets/cube.glb",
addToScene: false,
numInstances: 2);
var defaultInstance = await asset.getInstance(0);
await viewer.addToScene(defaultInstance);
await testHelper.capture(viewer.view, "gltf_without_instance");
await testHelper.capture(viewer.view, "gltf");
var instance = await asset.createInstance();
await viewer.addToScene(instance);
print(instance.entity);
print(await instance.getChildEntities());
await instance.setTransform(Matrix4.translation(Vector3(1, 0, 0)));
await testHelper.capture(viewer.view, "gltf_with_instance");
await testHelper.capture(viewer.view, "gltf_instance_created");
await viewer.addToScene(instance);
await testHelper.capture(viewer.view, "gltf_instance_add_to_scene");
await viewer.removeFromScene(instance);
await testHelper.capture(viewer.view, "gltf_instance_remove_from_scene");
});
});
test('physics simulation with 100 instances', () async {
await testHelper.withViewer((viewer) async {
// --- Scene Setup ---
await viewer
.loadIbl("file://${testHelper.testDir}/assets/default_env_ibl.ktx");
await viewer.loadSkybox(
"file://${testHelper.testDir}/assets/default_env_skybox.ktx");
await viewer.setPostProcessing(true);
await viewer.setAntiAliasing(false, true, false); // Enable FXAA
final camera = await viewer.getActiveCamera();
final orbitDist = 12.0; // Slightly further back to see spread pattern
final lookAtTarget = Vector3(0, 0.5, 0); // Look at middle of trajectory
// --- Load Asset & Create/Prepare Instances ---
print("Loading asset...");
var numInstances = 100;
var asset = await viewer.loadGltf(
"file://${testHelper.testDir}/assets/cube.glb",
numInstances: numInstances,
addToScene: false);
print("Creating 100 instances...");
List<PhysicsState> instanceStates = [];
final rnd = Random();
for (int i = 0; i < numInstances - 1; i++) {
var mi = await FilamentApp.instance!
.createUbershaderMaterialInstance(unlit: true);
var instance = await asset.createInstance(materialInstances: [mi]);
await viewer.removeFromScene(instance);
await mi.setParameterFloat4("baseColorFactor", rnd.nextDouble(),
rnd.nextDouble(), rnd.nextDouble(), 1.0);
var scale = max(0.25, rnd.nextDouble());
instanceStates.add(PhysicsState(instance, scale));
}
print("Instances created and colored.");
// --- Simulation Parameters ---
final gravity = Vector3(0, -9.81, 0);
// Calculate initial velocity to reach 1 meter in 1 second
// Using kinematics: y = v0*t + 0.5*a*t^2
// At t=1s, y=1m, a=-9.81m/s^2
// Solving for v0: v0 = 1 - 0.5*(-9.81)*1^2 = 1 + 4.905 = 5.905 m/s
final initialUpwardSpeed = 5.905; // Calculated for 1m height in 1s
final timeStep = 1 / 60.0; // Simulate at 60 FPS
final frameDuration =
Duration(microseconds: (timeStep * 1000000).round());
final launchInterval = 0.5; // 100 ms between launches
final totalSimulationTime = 20.0; // Simulation duration in seconds
final orbitDuration = 10.0; // Time for one full camera orbit
// --- Simulation Loop ---
double currentTime = 0.0;
double timeSinceLastLaunch =
launchInterval; // Start launching immediately
int launchedCount = 0;
int frameCounter = 0;
int captureCounter = 0;
print("Starting simulation loop (${totalSimulationTime}s)...");
final startTime = DateTime.now();
while (currentTime < totalSimulationTime) {
final loopStart = DateTime.now();
// 1. Launch Instance Logic
if (launchedCount < instanceStates.length &&
timeSinceLastLaunch >= launchInterval) {
final state = instanceStates[launchedCount];
if (!state.launched) {
print("Launching instance ${launchedCount + 1}/100");
// Add a slight angle to launch direction
// Using a spiral pattern with increasing angle
final angle = (launchedCount * 0.2) %
(2 * pi); // Increasing angle creating spiral
final horizontalComponent =
initialUpwardSpeed * 0.15; // 15% horizontal velocity
state.velocity = Vector3(
horizontalComponent * cos(angle),
initialUpwardSpeed,
horizontalComponent * sin(angle)); // Angled velocity
state.position = Vector3(0, 0.1, 0); // Start slightly above origin
state.launched = true;
await viewer
.addToScene(state.instance); // Add to scene ONLY when launched
state.addedToScene = true;
launchedCount++;
timeSinceLastLaunch -= launchInterval;
}
}
// 2. Update Physics and Transforms for launched instances
List<Future> transformUpdates = [];
for (var state in instanceStates) {
if (state.launched) {
// Basic Euler integration
state.velocity.add(gravity * timeStep);
state.position.add(state.velocity * timeStep);
// Queue the asynchronous transform update
transformUpdates.add(state.instance
.setTransform(Matrix4.compose(state.position, Quaternion.identity(), Vector3.all(state.scale))));
}
}
// Wait for all instance transforms in this step to complete
if (transformUpdates.isNotEmpty) {
await Future.wait(transformUpdates);
}
// 3. Update Camera Orbit
final angle = (currentTime / orbitDuration) * 2 * pi;
await camera.lookAt(
Vector3(
sin(angle) * orbitDist,
orbitDist * 0.3, // Lower camera height to see 1-meter trajectories
cos(angle) * orbitDist,
),
focus: lookAtTarget, // Point towards the peak of trajectories
up: Vector3(0, 1, 0), // Keep up vector standard
);
// 4. Capture Frame Periodically (e.g., every 6 physics steps => 10 captures/sec)
if (frameCounter % 6 == 0) {
await testHelper.capture(viewer.view,
"capture_physics_orbit_${captureCounter.toString().padLeft(3, '0')}");
captureCounter++;
}
// 5. Advance Time and Wait
currentTime += timeStep;
timeSinceLastLaunch += timeStep;
frameCounter++;
// Ensure the loop doesn't run faster than the desired frame rate
final elapsed = loopStart.difference(loopStart);
if (elapsed < frameDuration) {
await Future.delayed(frameDuration - elapsed);
}
}
final endTime = DateTime.now();
print(
"Simulation loop finished in ${endTime.difference(startTime).inSeconds} seconds.");
print("Captured $captureCounter frames.");
// Optional: Capture one final frame after simulation ends
await testHelper.capture(viewer.view, "capture_physics_orbit_final");
}, viewportDimensions: (width:1024, height:1024)); // End withViewer
});
}