instancing tests/demo
This commit is contained in:
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user