From 709fe35852ce6a8745869ae570651237e4c8134f Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Fri, 28 Mar 2025 14:06:12 +0800 Subject: [PATCH] instancing tests/demo --- thermion_dart/test/instancing_tests.dart | 191 ++++++++++++++++++++++- 1 file changed, 185 insertions(+), 6 deletions(-) diff --git a/thermion_dart/test/instancing_tests.dart b/thermion_dart/test/instancing_tests.dart index 68ed39b4..dc069802 100644 --- a/thermion_dart/test/instancing_tests.dart +++ b/thermion_dart/test/instancing_tests.dart @@ -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 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 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 + }); }