From d2e8910d4994b475df3603901274c570da40fb42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Do=CC=88ring?= <956212+mario-deluna@users.noreply.github.com> Date: Sun, 28 Dec 2025 00:27:08 +0100 Subject: [PATCH 1/9] Added support for attach/detach ecs event listeners --- src/ECS/EntitiesInterface.php | 37 +++++ src/ECS/EntityRegistry.php | 141 +++++++++++++++++- tests/ECS/EntityRegistryTest.php | 244 +++++++++++++++++++++++++++++++ 3 files changed, 416 insertions(+), 6 deletions(-) diff --git a/src/ECS/EntitiesInterface.php b/src/ECS/EntitiesInterface.php index d9b736f..ecf4907 100644 --- a/src/ECS/EntitiesInterface.php +++ b/src/ECS/EntitiesInterface.php @@ -63,6 +63,25 @@ public function destroy(int $entity) : void; */ public function attach(int $entity, object $component) : object; + /** + * Regsiters a callback which is beeing invoked every time a component of given type is + * attached to an entity. + * + * @template T + * @param class-string $componentClassName the component class name to listen for + * @param callable(int, T): void $callback + * + * @return int Returns the listener handle which can be used to deregister the listener later on + */ + public function onAttach(string $componentClassName, callable $callback) : int; + + /** + * Deregisters a previously registered attach listener + * + * @param int $listenerHandle The listener handle returned by `onAttach` + */ + public function releaseOnAttach(int $listenerHandle) : void; + /** * Dettaches a component by class its class name * @@ -78,6 +97,24 @@ public function detach(int $entity, string $componentClassName) : void; */ public function detachAll(int $entity) : void; + /** + * Registers a callback which is beeing invoked every time a component of given type is + * dettached from an entity. + * @template T + * @param class-string $componentClassName the component class name to listen for + * @param callable(int, T): void $callback + * + * @return int Returns the listener handle which can be used to deregister the listener later on + */ + public function onDetach(string $componentClassName, callable $callback) : int; + + /** + * Deregisters a previously registered detach listener + * + * @param int $listenerHandle The listener handle returned by `onDetach` + */ + public function releaseOnDetach(int $listenerHandle) : void; + /** * Returns a component for the given entity * ! Warning: This method does no error checking and assumes you made sure the component needs to actually exist! diff --git a/src/ECS/EntityRegistry.php b/src/ECS/EntityRegistry.php index 0b9a072..98ecf3b 100644 --- a/src/ECS/EntityRegistry.php +++ b/src/ECS/EntityRegistry.php @@ -40,6 +40,40 @@ class EntityRegistry implements EntitiesInterface */ private array $entityComponents = []; + /** + * An array holding the "attach" listeners + * + * @var array + */ + private array $attachListeners = []; + + /** + * @var array> + */ + private array $attachListenersHandles = []; + + /** + * the attach listener index + */ + private int $attachListenerIndex = 0; + + /** + * An array holding the "detached" listeners + * + * @var array + */ + private array $detachListeners = []; + + /** + * @var array> + */ + private array $detachListenersHandles = []; + + /** + * the detach listener index + */ + private int $detachListenerIndex = 0; + /** * Creates an entity and returns its ID */ @@ -76,6 +110,10 @@ public function registerComponent(string $componentClassName) : void } $this->components[$componentClassName] = []; + + // also the slots for the listeners + $this->attachListenersHandles[$componentClassName] = []; + $this->detachListenersHandles[$componentClassName] = []; } /** @@ -131,12 +169,7 @@ public function listComponents(string $componentClassName) : array */ public function destroy(int $entity) : void { - $componentClasses = array_keys($this->entityComponents[$entity]); - unset($this->entityComponents[$entity]); - foreach($componentClasses as $componentClass) { - unset($this->components[$componentClass][$entity]); - } - + $this->detachAll($entity); $this->freelist[] = $entity; } @@ -158,9 +191,54 @@ public function attach(int $entity, object $component) : object $this->components[$className][$entity] = $component; $this->entityComponents[$entity][$className] = $component; + // invoke attach listeners + foreach($this->attachListenersHandles[$className] as $handle) { + $listener = $this->attachListeners[$handle]; + $listener($entity, $component); + } + return $component; } + /** + * Regsiters a callback which is beeing invoked every time a component of given type is + * attached to an entity. + * + * @template T of object + * @param class-string $componentClassName the component class name to listen for + * @param callable(int, T): void $callback + * + * @return int Returns the listener handle which can be used to deregister the listener later on + */ + public function onAttach(string $componentClassName, callable $callback) : int + { + $handle = $this->attachListenerIndex++; + $this->attachListeners[$handle] = $callback; // @phpstan-ignore-line + $this->attachListenersHandles[$componentClassName][$handle] = $handle; + + return $handle; + } + + /** + * Deregisters a previously registered attach listener + * + * @param int $listenerHandle The listener handle returned by `onAttach` + */ + public function releaseOnAttach(int $listenerHandle) : void + { + if (!isset($this->attachListeners[$listenerHandle])) { + return; + } + + unset($this->attachListeners[$listenerHandle]); + + foreach($this->attachListenersHandles as $componentClassName => $handles) { + if (isset($handles[$listenerHandle])) { + unset($this->attachListenersHandles[$componentClassName][$listenerHandle]); + } + } + } + /** * Dettaches a component by class its class name * @@ -169,6 +247,13 @@ public function attach(int $entity, object $component) : object */ public function detach(int $entity, string $componentClassName) : void { + // invoke detach listeners + foreach($this->detachListenersHandles[$componentClassName] as $handle) { + $listener = $this->detachListeners[$handle]; + $listener($entity, $this->entityComponents[$entity][$componentClassName]); + } + + // actually detach the component unset( $this->components[$componentClassName][$entity], $this->entityComponents[$entity][$componentClassName] @@ -183,12 +268,56 @@ public function detach(int $entity, string $componentClassName) : void public function detachAll(int $entity) : void { foreach($this->entityComponents[$entity] as $componentClassName => $component) { + // invoke detach listeners + foreach($this->detachListenersHandles[$componentClassName] as $handle) { + $listener = $this->detachListeners[$handle]; + $listener($entity, $component); + } + unset($this->components[$componentClassName][$entity]); } unset($this->entityComponents[$entity]); } + /** + * Registers a callback which is beeing invoked every time a component of given type is + * dettached from an entity. + * @template T + * @param class-string $componentClassName the component class name to listen for + * @param callable(int, T): void $callback + * + * @return int Returns the listener handle which can be used to deregister the listener later on + */ + public function onDetach(string $componentClassName, callable $callback) : int + { + $handle = $this->detachListenerIndex++; + $this->detachListeners[$handle] = $callback; // @phpstan-ignore-line + $this->detachListenersHandles[$componentClassName][$handle] = $handle; + + return $handle; + } + + /** + * Deregisters a previously registered detach listener + * + * @param int $listenerHandle The listener handle returned by `onDetach` + */ + public function releaseOnDetach(int $listenerHandle) : void + { + if (!isset($this->detachListeners[$listenerHandle])) { + return; + } + + unset($this->detachListeners[$listenerHandle]); + + foreach($this->detachListenersHandles as $componentClassName => $handles) { + if (isset($handles[$listenerHandle])) { + unset($this->detachListenersHandles[$componentClassName][$listenerHandle]); + } + } + } + /** * Returns a component for the given entity * ! Warning: This method does no error checking and assumes you made sure the component needs to actually exist! diff --git a/tests/ECS/EntityRegistryTest.php b/tests/ECS/EntityRegistryTest.php index 903a4f8..80a0d72 100644 --- a/tests/ECS/EntityRegistryTest.php +++ b/tests/ECS/EntityRegistryTest.php @@ -187,4 +187,248 @@ public function testViewWith() : void $this->assertEquals([$e2, $e3], $actualBuffer); } + + public function testOnAttachListener() : void + { + $entities = new EntityRegistry(); + $entities->registerComponent(\Exception::class); + + $attachedEntities = []; + $attachedComponents = []; + + // register an attach listener + $handle = $entities->onAttach(\Exception::class, function(int $entity, \Exception $component) use (&$attachedEntities, &$attachedComponents) { + $attachedEntities[] = $entity; + $attachedComponents[] = $component->getMessage(); + }); + + $this->assertIsInt($handle); + + // create entities and attach components + $e1 = $entities->create(); + $entities->attach($e1, new \Exception('e1')); + + $e2 = $entities->create(); + $entities->attach($e2, new \Exception('e2')); + + // verify the listener was called + $this->assertEquals([$e1, $e2], $attachedEntities); + $this->assertEquals(['e1', 'e2'], $attachedComponents); + } + + public function testOnDetachListener() : void + { + $entities = new EntityRegistry(); + $entities->registerComponent(\Exception::class); + + $detachedEntities = []; + $detachedComponents = []; + + // register a detach listener + $handle = $entities->onDetach(\Exception::class, function(int $entity, \Exception $component) use (&$detachedEntities, &$detachedComponents) { + $detachedEntities[] = $entity; + $detachedComponents[] = $component->getMessage(); + }); + + $this->assertIsInt($handle); + + // create entities and attach components + $e1 = $entities->create(); + $entities->attach($e1, new \Exception('e1')); + + $e2 = $entities->create(); + $entities->attach($e2, new \Exception('e2')); + + // detach components + $entities->detach($e1, \Exception::class); + $entities->detach($e2, \Exception::class); + + // verify the listener was called + $this->assertEquals([$e1, $e2], $detachedEntities); + $this->assertEquals(['e1', 'e2'], $detachedComponents); + } + + public function testOnDetachListenerWithDetachAll() : void + { + $entities = new EntityRegistry(); + $entities->registerComponent(\Exception::class); + $entities->registerComponent(\Error::class); + + $detachedExceptions = []; + $detachedErrors = []; + + // register detach listeners for both component types + $entities->onDetach(\Exception::class, function(int $entity, \Exception $component) use (&$detachedExceptions) { + $detachedExceptions[] = $component->getMessage(); + }); + + $entities->onDetach(\Error::class, function(int $entity, \Error $component) use (&$detachedErrors) { + $detachedErrors[] = $component->getMessage(); + }); + + // create entity and attach both components + $e1 = $entities->create(); + $entities->attach($e1, new \Exception('exception')); + $entities->attach($e1, new \Error('error')); + + // detach all components + $entities->detachAll($e1); + + // verify both listeners were called + $this->assertEquals(['exception'], $detachedExceptions); + $this->assertEquals(['error'], $detachedErrors); + } + + public function testReleaseOnAttach() : void + { + $entities = new EntityRegistry(); + $entities->registerComponent(\Exception::class); + + $attachedComponents = []; + + // register an attach listener + $handle = $entities->onAttach(\Exception::class, function(int $entity, \Exception $component) use (&$attachedComponents) { + $attachedComponents[] = $component->getMessage(); + }); + + // attach a component - listener should be called + $e1 = $entities->create(); + $entities->attach($e1, new \Exception('before release')); + + $this->assertEquals(['before release'], $attachedComponents); + + // release the listener + $entities->releaseOnAttach($handle); + + // attach another component - listener should NOT be called + $e2 = $entities->create(); + $entities->attach($e2, new \Exception('after release')); + + // should still be just the first one + $this->assertEquals(['before release'], $attachedComponents); + } + + public function testReleaseOnDetach() : void + { + $entities = new EntityRegistry(); + $entities->registerComponent(\Exception::class); + + $detachedComponents = []; + + // register a detach listener + $handle = $entities->onDetach(\Exception::class, function(int $entity, \Exception $component) use (&$detachedComponents) { + $detachedComponents[] = $component->getMessage(); + }); + + // create and attach components + $e1 = $entities->create(); + $entities->attach($e1, new \Exception('before release')); + + $e2 = $entities->create(); + $entities->attach($e2, new \Exception('after release')); + + // detach first component - listener should be called + $entities->detach($e1, \Exception::class); + $this->assertEquals(['before release'], $detachedComponents); + + // release the listener + $entities->releaseOnDetach($handle); + + // detach second component - listener should NOT be called + $entities->detach($e2, \Exception::class); + + // should still be just the first one + $this->assertEquals(['before release'], $detachedComponents); + } + + public function testMultipleAttachListeners() : void + { + $entities = new EntityRegistry(); + $entities->registerComponent(\Exception::class); + + $listener1Called = false; + $listener2Called = false; + + // register two different attach listeners + $entities->onAttach(\Exception::class, function(int $entity, \Exception $component) use (&$listener1Called) { + $listener1Called = true; + }); + + $entities->onAttach(\Exception::class, function(int $entity, \Exception $component) use (&$listener2Called) { + $listener2Called = true; + }); + + // attach a component + $e1 = $entities->create(); + $entities->attach($e1, new \Exception('test')); + + // both listeners should have been called + $this->assertTrue($listener1Called); + $this->assertTrue($listener2Called); + } + + public function testMultipleDetachListeners() : void + { + $entities = new EntityRegistry(); + $entities->registerComponent(\Exception::class); + + $listener1Called = false; + $listener2Called = false; + + // register two different detach listeners + $entities->onDetach(\Exception::class, function(int $entity, \Exception $component) use (&$listener1Called) { + $listener1Called = true; + }); + + $entities->onDetach(\Exception::class, function(int $entity, \Exception $component) use (&$listener2Called) { + $listener2Called = true; + }); + + // create entity and attach component + $e1 = $entities->create(); + $entities->attach($e1, new \Exception('test')); + + // detach the component + $entities->detach($e1, \Exception::class); + + // both listeners should have been called + $this->assertTrue($listener1Called); + $this->assertTrue($listener2Called); + } + + public function testListenerWithEntityDestroy() : void + { + $entities = new EntityRegistry(); + $entities->registerComponent(\Exception::class); + + $detachedComponents = []; + + // register a detach listener + $entities->onDetach(\Exception::class, function(int $entity, \Exception $component) use (&$detachedComponents) { + $detachedComponents[] = $component->getMessage(); + }); + + // create entity and attach component + $e1 = $entities->create(); + $entities->attach($e1, new \Exception('to be destroyed')); + + // destroy the entity (which calls detachAll internally) + $entities->destroy($e1); + + // verify the detach listener was called + $this->assertEquals(['to be destroyed'], $detachedComponents); + $this->assertFalse($entities->valid($e1)); + } + + public function testReleaseNonExistentListener() : void + { + $entities = new EntityRegistry(); + + // releasing non-existent listeners should not cause errors + $entities->releaseOnAttach(999); + $entities->releaseOnDetach(999); + + // test should pass without exceptions + $this->assertTrue(true); + } } \ No newline at end of file From 9d62f474f10f4c917345ffff76b7a8ec306de9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Do=CC=88ring?= <956212+mario-deluna@users.noreply.github.com> Date: Fri, 9 Jan 2026 16:10:52 +0100 Subject: [PATCH 2/9] VLU LPRendering, DCA, Audio, EventLoop, ECS --- .gitignore | 1 + examples/bootstrap.php | 32 ++ .../rendering/low_poly_pipeline_basic.php | 121 ++++++ .../rendering/low_poly_pipeline_complex.php | 121 ++++++ resources/model/visu/mat_test.mtl | 72 ++++ resources/model/visu/mat_test.obj | 304 ++++++++++++++ .../shader/include/visu/gbuffer_layout.glsl | 5 +- .../shader/include/visu/gbuffer_uniform.glsl | 5 +- .../lowpoly/deferred_instanced_mesh.frag.glsl | 22 + .../lowpoly/deferred_instanced_mesh.vert.glsl | 36 ++ .../visu/lowpoly/deferred_lightpass.frag.glsl | 25 +- .../lowpoly/deferred_single_mesh.frag.glsl | 3 + src/Command/HDRIToCubemapCommand.php | 134 ++++++ ...RenderableModel.php => LPDynamicModel.php} | 2 +- src/Component/VISULowPoly/LPStaticModel.php | 30 ++ src/ECS/EntitiesInterface.php | 10 +- src/ECS/EntityRegistry.php | 18 +- src/Graphics/CubeVertexArray.php | 124 ++++++ src/Graphics/Cubemap.php | 385 ++++++++++++++++++ src/Graphics/Framebuffer.php | 17 +- src/Graphics/HDRIToCubemap.php | 256 ++++++++++++ src/Graphics/QuadVertexArray.php | 2 +- src/Graphics/Rendering/Pass/CubemapPass.php | 89 ++++ .../Rendering/Pass/DeferredLightPass.php | 6 + .../Rendering/Pass/FullscreenQuadPass.php | 2 - src/Graphics/Rendering/Pass/GBufferPass.php | 21 + .../Rendering/Pass/GBufferPassData.php | 3 + src/Graphics/Rendering/PipelineResources.php | 68 ++++ src/Graphics/Rendering/RenderPipeline.php | 20 + .../Rendering/Renderer/CubemapRenderer.php | 91 +++++ .../Rendering/Resource/CubemapResource.php | 26 ++ src/Graphics/Texture.php | 1 + src/Quickstart/QuickstartApp.php | 18 + .../Render/QuickstartDebugMetricsOverlay.php | 2 +- src/Runtime/EventLoop.php | 173 ++++++++ src/Runtime/Timer.php | 32 ++ src/Runtime/TimerHeap.php | 16 + src/System/VISULowPoly/LPException.php | 9 + src/System/VISULowPoly/LPMaterial.php | 49 ++- src/System/VISULowPoly/LPMesh.php | 10 + src/System/VISULowPoly/LPModel.php | 10 + src/System/VISULowPoly/LPObjLoader.php | 240 ++++++++--- src/System/VISULowPoly/LPRenderingSystem.php | 273 +++++++++++-- src/System/VISULowPoly/LPVertexBuffer.php | 94 ++++- tests/ECS/EntityRegistryTest.php | 23 +- visu.ctn | 3 + 46 files changed, 2863 insertions(+), 141 deletions(-) create mode 100644 examples/rendering/low_poly_pipeline_basic.php create mode 100644 examples/rendering/low_poly_pipeline_complex.php create mode 100644 resources/model/visu/mat_test.mtl create mode 100644 resources/model/visu/mat_test.obj create mode 100644 resources/shader/visu/lowpoly/deferred_instanced_mesh.frag.glsl create mode 100644 resources/shader/visu/lowpoly/deferred_instanced_mesh.vert.glsl create mode 100644 src/Command/HDRIToCubemapCommand.php rename src/Component/VISULowPoly/{DynamicRenderableModel.php => LPDynamicModel.php} (93%) create mode 100644 src/Component/VISULowPoly/LPStaticModel.php create mode 100644 src/Graphics/CubeVertexArray.php create mode 100644 src/Graphics/Cubemap.php create mode 100644 src/Graphics/HDRIToCubemap.php create mode 100644 src/Graphics/Rendering/Pass/CubemapPass.php create mode 100644 src/Graphics/Rendering/Renderer/CubemapRenderer.php create mode 100644 src/Graphics/Rendering/Resource/CubemapResource.php create mode 100644 src/Runtime/EventLoop.php create mode 100644 src/Runtime/Timer.php create mode 100644 src/Runtime/TimerHeap.php create mode 100644 src/System/VISULowPoly/LPException.php diff --git a/.gitignore b/.gitignore index fff46e7..1f5913a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ docs/docs-assets/ .phpdoc/ docs/api bin/phpDocumentor.phar +/examples/resources/assets/ \ No newline at end of file diff --git a/examples/bootstrap.php b/examples/bootstrap.php index 7fbe1f1..d19e0aa 100644 --- a/examples/bootstrap.php +++ b/examples/bootstrap.php @@ -38,5 +38,37 @@ */ $container = require __DIR__ . '/../bootstrap.php'; +/** + * --------------------------------------------------------------- + * Download Assets for the example + * --------------------------------------------------------------- + */ +$exampleAssetsUrl = 'https://github.com/phpgl/visu-example-assets/archive/refs/heads/master.zip'; +$exampleAssetsPath = VISU_PATH_RESOURCES . DS . 'assets'; + +if (!is_dir($exampleAssetsPath)) { + if (!extension_loaded('zip')) { + echo "Error: PHP Zip extension is required to download and extract example assets.\n"; + echo "Please install the PHP Zip extension and try again.\n"; + exit(1); + } + + echo "Downloading example assets...\n"; + $zipFile = VISU_PATH_CACHE . DS . 'visu-example-assets.zip'; + file_put_contents($zipFile, fopen($exampleAssetsUrl, 'r')); + $zip = new ZipArchive(); + if ($zip->open($zipFile) === TRUE) { + $zip->extractTo(VISU_PATH_RESOURCES); + $zip->close(); + rename(VISU_PATH_RESOURCES . DS . 'visu-example-assets-master', $exampleAssetsPath); + unlink($zipFile); + echo "[ok] Example assets downloaded and extracted.\n"; + } else { + echo "[error] Failed to download example assets.\n"; + } +} else { + echo "[ok] Example assets already present.\n"; +} + // forward the container return $container; \ No newline at end of file diff --git a/examples/rendering/low_poly_pipeline_basic.php b/examples/rendering/low_poly_pipeline_basic.php new file mode 100644 index 0000000..aec3693 --- /dev/null +++ b/examples/rendering/low_poly_pipeline_basic.php @@ -0,0 +1,121 @@ +container = $container; + $app->ready = function(QuickstartApp $app) use(&$state) + { + // create a model collection and load + $state->models = new LPModelCollection(); + $state->renderingSystem = new LPRenderingSystem($app->gl, $app->shaders, $state->models); + + // load the VISU models coming with the engine + $loader = new LPObjLoader($app->gl); + $loader->loadAllInDirectory(VISU_PATH_FRAMEWORK_RESOURCES . '/model/visu', $state->models); + + // to render 3D we need a camera + $state->cameraSystem = new VISUCameraSystem($app->input, $app->dispatcher); + + // register the rendering system + $app->bindSystems([ + $state->renderingSystem, + $state->cameraSystem + ]); + }; + + // Initalize the scene + // -------------------------------------------------------------------- + $app->initializeScene = function(QuickstartApp $app) use(&$state) + { + $state->cameraSystem->spawnDefaultFlyingCamera($app->entities, new Vec3(0.0, 0.0, 2.0)); + + // spawn a visu model in the middle + $state->logoEntity = $app->entities->create(); + $app->entities->attach($state->logoEntity, new LPDynamicModel('visu_logo')); + $transform = $app->entities->attach($state->logoEntity, new Transform()); + $transform->orientation->rotate(GLM::radians(90.0), new Vec3(1.0, 0.0, 0.0)); + + // initialize rotation state for interpolation + $state->logoRotationPrevious = $transform->orientation->copy(); + $state->logoRotationCurrent = $transform->orientation->copy(); + }; + + $app->update = function(QuickstartApp $app) use(&$state) + { + $app->updateSystem($state->cameraSystem); + + // store previous state before updating + $state->logoRotationPrevious = $state->logoRotationCurrent->copy(); + + // rotate the logo by a fixed amount per tick + $state->logoRotationCurrent->rotate(GLM::radians(1.0), new Vec3(0.0, 0.0, 1.0)); + }; + + $app->render = function(QuickstartApp $app, RenderContext $context, RenderTargetResource $target) use(&$state) + { + // interpolate between previous and current rotation state using compensation + // this way you get butter smooth rotation + $transform = $app->entities->get($state->logoEntity, Transform::class); + $transform->orientation = Quat::slerp($state->logoRotationPrevious, $state->logoRotationCurrent, $context->compensation); + $transform->markDirty(); + + // make sure to tell the low poly rendering system which render target we are using + $state->renderingSystem->setRenderTarget($target); + + $app->renderSystem($state->cameraSystem, $context); + $app->renderSystem($state->renderingSystem, $context); + + // Example: You can override the ouput texture to any texture generated in the pipeline + // the the code below we "debug" output the SSAO texture instead of the final render + // $ssaoData = $context->data->get(SSAOData::class); + // $quickstartPassData = $context->data->get(QuickstartPassData::class); + // $quickstartPassData->outputTexture = $ssaoData->ssaoTexture; + }; +}); + +$quickstart->run(); diff --git a/examples/rendering/low_poly_pipeline_complex.php b/examples/rendering/low_poly_pipeline_complex.php new file mode 100644 index 0000000..5a547de --- /dev/null +++ b/examples/rendering/low_poly_pipeline_complex.php @@ -0,0 +1,121 @@ +container = $container; + $app->ready = function(QuickstartApp $app) use(&$state) + { + // $app->loadCompatGPUProfiler(); + + // create a model collection and load + $state->models = new LPModelCollection(); + $state->renderingSystem = new LPRenderingSystem($app->gl, $app->shaders, $state->models); + + // load the VISU models coming with the engine + $loader = new LPObjLoader($app->gl); + $loader->loadAllInDirectory(VISU_PATH_RESOURCES . '/assets/models/lp/', $state->models); + + // to render 3D we need a camera + $state->cameraSystem = new VISUCameraSystem($app->input, $app->dispatcher); + + // register the rendering system + $app->bindSystems([ + $state->renderingSystem, + $state->cameraSystem + ]); + }; + + // Initalize the scene + // -------------------------------------------------------------------- + $app->initializeScene = function(QuickstartApp $app) use(&$state) + { + $state->cameraSystem->spawnDefaultFlyingCamera($app->entities, new Vec3(0.0, 0.0, 2.0)); + + // we spawn 5000 random low poly models in the scene + $availableModels = $state->models->models; + + for ($i = 0; $i < 50000; $i++) { + $modelIndex = rand(0, count($availableModels) - 1); + $modelName = array_keys($availableModels)[$modelIndex]; + + $entity = $app->entities->create(); + $transform = $app->entities->attach($entity, new Transform()); + + // random position + $transform->position = new Vec3( + rand(-1000, 1000) / 10.0, + rand(-1000, 1000) / 10.0, + rand(-1000, 1000) / 10.0 + ); + + // random rotation + $transform->orientation->rotate(GLM::radians(rand(0, 360)), new Vec3(0.0, 1.0, 0.0)); + $transform->orientation->rotate(GLM::radians(rand(0, 360)), new Vec3(1.0, 0.0, 0.0)); + + // random scale + $scale = rand(5, 20) / 10.0; + $transform->scale = new Vec3($scale, $scale, $scale); + + // attach the low poly model component + $app->entities->attach($entity, new LPStaticModel($modelName)); + } + }; + + $app->update = function(QuickstartApp $app) use(&$state) + { + $app->updateSystem($state->cameraSystem); + }; + + $app->render = function(QuickstartApp $app, RenderContext $context, RenderTargetResource $target) use(&$state) + { + // make sure to tell the low poly rendering system which render target we are using + $state->renderingSystem->setRenderTarget($target); + + $app->renderSystem($state->cameraSystem, $context); + $app->renderSystem($state->renderingSystem, $context); + + // $ssaoData = $context->data->get(SSAOData::class); + // $quickstartPassData = $context->data->get(QuickstartPassData::class); + + // $quickstartPassData->outputTexture = $ssaoData->ssaoTexture; + }; +}); + +$quickstart->run(); diff --git a/resources/model/visu/mat_test.mtl b/resources/model/visu/mat_test.mtl new file mode 100644 index 0000000..f4e6532 --- /dev/null +++ b/resources/model/visu/mat_test.mtl @@ -0,0 +1,72 @@ +# Blender 3.4.1 MTL File: 'None' +# www.blender.org + +newmtl Basic_Colored +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.181164 0.799103 0.215861 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.450000 +d 1.000000 +illum 2 + +newmtl Emissive +Ns 151.843796 +Ka 1.000000 1.000000 1.000000 +Kd 0.800000 0.047443 0.056804 +Ks 0.500000 0.500000 0.500000 +Ke 1.000000 0.011089 0.011089 +Ni 1.450000 +d 1.000000 +illum 2 + +newmtl Metallic_Rough +Ns 0.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.181164 0.799103 0.215861 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.450000 +d 1.000000 +illum 3 + +newmtl Metallic_SHiny +Ns 1000.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.181164 0.799103 0.215861 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.450000 +d 1.000000 +illum 3 + +newmtl Metallic_SHiny.001 +Ns 1000.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.800000 0.575873 0.081503 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.450000 +d 1.000000 +illum 3 + +newmtl Very_Rough +Ns 0.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.181164 0.799103 0.215861 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.450000 +d 1.000000 +illum 2 + +newmtl Very_Shinyy +Ns 1000.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.181164 0.799103 0.215861 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.450000 +d 1.000000 +illum 2 diff --git a/resources/model/visu/mat_test.obj b/resources/model/visu/mat_test.obj new file mode 100644 index 0000000..976634e --- /dev/null +++ b/resources/model/visu/mat_test.obj @@ -0,0 +1,304 @@ +# Blender 3.4.1 +# www.blender.org +mtllib mat_test.mtl +o Cube +v 1.000000 1.000000 -1.000000 +v 1.000000 -1.000000 -1.000000 +v 1.000000 1.000000 1.000000 +v 1.000000 -1.000000 1.000000 +v -1.000000 1.000000 -1.000000 +v -1.000000 -1.000000 -1.000000 +v -1.000000 1.000000 1.000000 +v -1.000000 -1.000000 1.000000 +vn -0.0000 1.0000 -0.0000 +vn -0.0000 -0.0000 1.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vn 1.0000 -0.0000 -0.0000 +vn -0.0000 -0.0000 -1.0000 +vt 0.625000 0.500000 +vt 0.375000 0.500000 +vt 0.625000 0.750000 +vt 0.375000 0.750000 +vt 0.875000 0.500000 +vt 0.625000 0.250000 +vt 0.125000 0.500000 +vt 0.375000 0.250000 +vt 0.875000 0.750000 +vt 0.625000 1.000000 +vt 0.625000 0.000000 +vt 0.375000 0.000000 +vt 0.375000 1.000000 +vt 0.125000 0.750000 +s 0 +usemtl Basic_Colored +f 5/5/1 3/3/1 1/1/1 +f 3/3/2 8/13/2 4/4/2 +f 7/11/3 6/8/3 8/12/3 +f 2/2/4 8/14/4 6/7/4 +f 1/1/5 4/4/5 2/2/5 +f 5/6/6 2/2/6 6/8/6 +f 5/5/1 7/9/1 3/3/1 +f 3/3/2 7/10/2 8/13/2 +f 7/11/3 5/6/3 6/8/3 +f 2/2/4 4/4/4 8/14/4 +f 1/1/5 3/3/5 4/4/5 +f 5/6/6 1/1/6 2/2/6 +o Cube.001 +v 1.000000 1.000000 -3.485260 +v 1.000000 -1.000000 -3.485260 +v 1.000000 1.000000 -1.485260 +v 1.000000 -1.000000 -1.485260 +v -1.000000 1.000000 -3.485260 +v -1.000000 -1.000000 -3.485260 +v -1.000000 1.000000 -1.485260 +v -1.000000 -1.000000 -1.485260 +vn -0.0000 1.0000 -0.0000 +vn -0.0000 -0.0000 1.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vn 1.0000 -0.0000 -0.0000 +vn -0.0000 -0.0000 -1.0000 +vt 0.625000 0.500000 +vt 0.375000 0.500000 +vt 0.625000 0.750000 +vt 0.375000 0.750000 +vt 0.875000 0.500000 +vt 0.625000 0.250000 +vt 0.125000 0.500000 +vt 0.375000 0.250000 +vt 0.875000 0.750000 +vt 0.625000 1.000000 +vt 0.625000 0.000000 +vt 0.375000 0.000000 +vt 0.375000 1.000000 +vt 0.125000 0.750000 +s 0 +usemtl Very_Shinyy +f 13/19/7 11/17/7 9/15/7 +f 11/17/8 16/27/8 12/18/8 +f 15/25/9 14/22/9 16/26/9 +f 10/16/10 16/28/10 14/21/10 +f 9/15/11 12/18/11 10/16/11 +f 13/20/12 10/16/12 14/22/12 +f 13/19/7 15/23/7 11/17/7 +f 11/17/8 15/24/8 16/27/8 +f 15/25/9 13/20/9 14/22/9 +f 10/16/10 12/18/10 16/28/10 +f 9/15/11 11/17/11 12/18/11 +f 13/20/12 9/15/12 10/16/12 +o Cube.002 +v 1.000000 1.000000 -5.851812 +v 1.000000 -1.000000 -5.851812 +v 1.000000 1.000000 -3.851812 +v 1.000000 -1.000000 -3.851812 +v -1.000000 1.000000 -5.851812 +v -1.000000 -1.000000 -5.851812 +v -1.000000 1.000000 -3.851812 +v -1.000000 -1.000000 -3.851812 +vn -0.0000 1.0000 -0.0000 +vn -0.0000 -0.0000 1.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vn 1.0000 -0.0000 -0.0000 +vn -0.0000 -0.0000 -1.0000 +vt 0.625000 0.500000 +vt 0.375000 0.500000 +vt 0.625000 0.750000 +vt 0.375000 0.750000 +vt 0.875000 0.500000 +vt 0.625000 0.250000 +vt 0.125000 0.500000 +vt 0.375000 0.250000 +vt 0.875000 0.750000 +vt 0.625000 1.000000 +vt 0.625000 0.000000 +vt 0.375000 0.000000 +vt 0.375000 1.000000 +vt 0.125000 0.750000 +s 0 +usemtl Very_Rough +f 21/33/13 19/31/13 17/29/13 +f 19/31/14 24/41/14 20/32/14 +f 23/39/15 22/36/15 24/40/15 +f 18/30/16 24/42/16 22/35/16 +f 17/29/17 20/32/17 18/30/17 +f 21/34/18 18/30/18 22/36/18 +f 21/33/13 23/37/13 19/31/13 +f 19/31/14 23/38/14 24/41/14 +f 23/39/15 21/34/15 22/36/15 +f 18/30/16 20/32/16 24/42/16 +f 17/29/17 19/31/17 20/32/17 +f 21/34/18 17/29/18 18/30/18 +o Cube.003 +v 1.000000 3.306867 -5.851812 +v 1.000000 1.306867 -5.851812 +v 1.000000 3.306867 -3.851812 +v 1.000000 1.306867 -3.851812 +v -1.000000 3.306867 -5.851812 +v -1.000000 1.306867 -5.851812 +v -1.000000 3.306867 -3.851812 +v -1.000000 1.306867 -3.851812 +vn -0.0000 1.0000 -0.0000 +vn -0.0000 -0.0000 1.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vn 1.0000 -0.0000 -0.0000 +vn -0.0000 -0.0000 -1.0000 +vt 0.625000 0.500000 +vt 0.375000 0.500000 +vt 0.625000 0.750000 +vt 0.375000 0.750000 +vt 0.875000 0.500000 +vt 0.625000 0.250000 +vt 0.125000 0.500000 +vt 0.375000 0.250000 +vt 0.875000 0.750000 +vt 0.625000 1.000000 +vt 0.625000 0.000000 +vt 0.375000 0.000000 +vt 0.375000 1.000000 +vt 0.125000 0.750000 +s 0 +usemtl Metallic_Rough +f 29/47/19 27/45/19 25/43/19 +f 27/45/20 32/55/20 28/46/20 +f 31/53/21 30/50/21 32/54/21 +f 26/44/22 32/56/22 30/49/22 +f 25/43/23 28/46/23 26/44/23 +f 29/48/24 26/44/24 30/50/24 +f 29/47/19 31/51/19 27/45/19 +f 27/45/20 31/52/20 32/55/20 +f 31/53/21 29/48/21 30/50/21 +f 26/44/22 28/46/22 32/56/22 +f 25/43/23 27/45/23 28/46/23 +f 29/48/24 25/43/24 26/44/24 +o Cube.004 +v 1.000000 3.306867 -3.409191 +v 1.000000 1.306867 -3.409191 +v 1.000000 3.306867 -1.409191 +v 1.000000 1.306867 -1.409191 +v -1.000000 3.306867 -3.409191 +v -1.000000 1.306867 -3.409191 +v -1.000000 3.306867 -1.409191 +v -1.000000 1.306867 -1.409191 +vn -0.0000 1.0000 -0.0000 +vn -0.0000 -0.0000 1.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vn 1.0000 -0.0000 -0.0000 +vn -0.0000 -0.0000 -1.0000 +vt 0.625000 0.500000 +vt 0.375000 0.500000 +vt 0.625000 0.750000 +vt 0.375000 0.750000 +vt 0.875000 0.500000 +vt 0.625000 0.250000 +vt 0.125000 0.500000 +vt 0.375000 0.250000 +vt 0.875000 0.750000 +vt 0.625000 1.000000 +vt 0.625000 0.000000 +vt 0.375000 0.000000 +vt 0.375000 1.000000 +vt 0.125000 0.750000 +s 0 +usemtl Metallic_SHiny +f 37/61/25 35/59/25 33/57/25 +f 35/59/26 40/69/26 36/60/26 +f 39/67/27 38/64/27 40/68/27 +f 34/58/28 40/70/28 38/63/28 +f 33/57/29 36/60/29 34/58/29 +f 37/62/30 34/58/30 38/64/30 +f 37/61/25 39/65/25 35/59/25 +f 35/59/26 39/66/26 40/69/26 +f 39/67/27 37/62/27 38/64/27 +f 34/58/28 36/60/28 40/70/28 +f 33/57/29 35/59/29 36/60/29 +f 37/62/30 33/57/30 34/58/30 +o Cube.005 +v 1.000000 3.306867 -3.409191 +v 1.000000 1.306867 -3.409191 +v 1.000000 3.306867 -1.409191 +v 1.000000 1.306867 -1.409191 +v -1.000000 3.306867 -3.409191 +v -1.000000 1.306867 -3.409191 +v -1.000000 3.306867 -1.409191 +v -1.000000 1.306867 -1.409191 +vn -0.0000 1.0000 -0.0000 +vn -0.0000 -0.0000 1.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vn 1.0000 -0.0000 -0.0000 +vn -0.0000 -0.0000 -1.0000 +vt 0.625000 0.500000 +vt 0.375000 0.500000 +vt 0.625000 0.750000 +vt 0.375000 0.750000 +vt 0.875000 0.500000 +vt 0.625000 0.250000 +vt 0.125000 0.500000 +vt 0.375000 0.250000 +vt 0.875000 0.750000 +vt 0.625000 1.000000 +vt 0.625000 0.000000 +vt 0.375000 0.000000 +vt 0.375000 1.000000 +vt 0.125000 0.750000 +s 0 +usemtl Metallic_SHiny.001 +f 45/75/31 43/73/31 41/71/31 +f 43/73/32 48/83/32 44/74/32 +f 47/81/33 46/78/33 48/82/33 +f 42/72/34 48/84/34 46/77/34 +f 41/71/35 44/74/35 42/72/35 +f 45/76/36 42/72/36 46/78/36 +f 45/75/31 47/79/31 43/73/31 +f 43/73/32 47/80/32 48/83/32 +f 47/81/33 45/76/33 46/78/33 +f 42/72/34 44/74/34 48/84/34 +f 41/71/35 43/73/35 44/74/35 +f 45/76/36 41/71/36 42/72/36 +o Cube.006 +v 1.000000 3.306867 -0.952895 +v 1.000000 1.306867 -0.952895 +v 1.000000 3.306867 1.047105 +v 1.000000 1.306867 1.047105 +v -1.000000 3.306867 -0.952895 +v -1.000000 1.306867 -0.952895 +v -1.000000 3.306867 1.047105 +v -1.000000 1.306867 1.047105 +vn -0.0000 1.0000 -0.0000 +vn -0.0000 -0.0000 1.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.0000 -1.0000 -0.0000 +vn 1.0000 -0.0000 -0.0000 +vn -0.0000 -0.0000 -1.0000 +vt 0.625000 0.500000 +vt 0.375000 0.500000 +vt 0.625000 0.750000 +vt 0.375000 0.750000 +vt 0.875000 0.500000 +vt 0.625000 0.250000 +vt 0.125000 0.500000 +vt 0.375000 0.250000 +vt 0.875000 0.750000 +vt 0.625000 1.000000 +vt 0.625000 0.000000 +vt 0.375000 0.000000 +vt 0.375000 1.000000 +vt 0.125000 0.750000 +s 0 +usemtl Emissive +f 53/89/37 51/87/37 49/85/37 +f 51/87/38 56/97/38 52/88/38 +f 55/95/39 54/92/39 56/96/39 +f 50/86/40 56/98/40 54/91/40 +f 49/85/41 52/88/41 50/86/41 +f 53/90/42 50/86/42 54/92/42 +f 53/89/37 55/93/37 51/87/37 +f 51/87/38 55/94/38 56/97/38 +f 55/95/39 53/90/39 54/92/39 +f 50/86/40 52/88/40 56/98/40 +f 49/85/41 51/87/41 52/88/41 +f 53/90/42 49/85/42 50/86/42 diff --git a/resources/shader/include/visu/gbuffer_layout.glsl b/resources/shader/include/visu/gbuffer_layout.glsl index 1c36269..66e5fca 100644 --- a/resources/shader/include/visu/gbuffer_layout.glsl +++ b/resources/shader/include/visu/gbuffer_layout.glsl @@ -1,4 +1,7 @@ layout (location = 0) out vec3 gbuffer_position; layout (location = 1) out vec3 gbuffer_vposition; layout (location = 2) out vec3 gbuffer_normal; -layout (location = 3) out vec3 gbuffer_albedo; \ No newline at end of file +layout (location = 3) out vec3 gbuffer_albedo; +layout (location = 4) out float gbuffer_metallic; +layout (location = 5) out float gbuffer_roughness; +layout (location = 6) out vec3 gbuffer_emissive; \ No newline at end of file diff --git a/resources/shader/include/visu/gbuffer_uniform.glsl b/resources/shader/include/visu/gbuffer_uniform.glsl index 441d5ef..2b5680b 100644 --- a/resources/shader/include/visu/gbuffer_uniform.glsl +++ b/resources/shader/include/visu/gbuffer_uniform.glsl @@ -1,4 +1,7 @@ uniform sampler2D gbuffer_position; uniform sampler2D gbuffer_normal; uniform sampler2D gbuffer_depth; -uniform sampler2D gbuffer_albedo; \ No newline at end of file +uniform sampler2D gbuffer_albedo; +uniform sampler2D gbuffer_metallic; +uniform sampler2D gbuffer_roughness; +uniform sampler2D gbuffer_emissive; \ No newline at end of file diff --git a/resources/shader/visu/lowpoly/deferred_instanced_mesh.frag.glsl b/resources/shader/visu/lowpoly/deferred_instanced_mesh.frag.glsl new file mode 100644 index 0000000..12d9120 --- /dev/null +++ b/resources/shader/visu/lowpoly/deferred_instanced_mesh.frag.glsl @@ -0,0 +1,22 @@ +#version 330 core + +#include "visu/gbuffer_layout.glsl" + +in vec3 v_normal; +in vec3 v_position; +in vec4 v_vposition; +in vec3 v_color; +in float v_roughness; +in float v_metallic; +in vec3 v_emissive; + +void main() +{ + gbuffer_albedo = v_color; + gbuffer_normal = v_normal; + gbuffer_position = v_position; + gbuffer_vposition = v_vposition.xyz; + gbuffer_metallic = v_metallic; + gbuffer_roughness = v_roughness; + gbuffer_emissive = v_emissive; +} \ No newline at end of file diff --git a/resources/shader/visu/lowpoly/deferred_instanced_mesh.vert.glsl b/resources/shader/visu/lowpoly/deferred_instanced_mesh.vert.glsl new file mode 100644 index 0000000..2b8b4eb --- /dev/null +++ b/resources/shader/visu/lowpoly/deferred_instanced_mesh.vert.glsl @@ -0,0 +1,36 @@ +#version 330 core +layout (location = 0) in vec3 a_position; +layout (location = 1) in vec3 a_normal; +layout (location = 2) in vec3 a_color; +layout (location = 3) in float a_roughness; +layout (location = 4) in float a_metallic; +layout (location = 5) in vec3 a_emissive; +layout (location = 6) in mat4 a_model; + +out vec3 v_normal; +out vec3 v_position; +out vec4 v_vposition; +out vec3 v_color; +out float v_roughness; +out float v_metallic; +out vec3 v_emissive; + +uniform mat4 projection; +uniform mat4 view; + +void main() +{ + vec4 world_pos = a_model * vec4(a_position, 1.0); + v_position = world_pos.xyz; + v_vposition = view * world_pos; + v_color = a_color; + v_roughness = a_roughness; + v_metallic = a_metallic; + v_emissive = a_emissive; + + vec3 n = normalize(mat3(a_model) * a_normal); + + v_normal = n; + + gl_Position = projection * view * world_pos; +} \ No newline at end of file diff --git a/resources/shader/visu/lowpoly/deferred_lightpass.frag.glsl b/resources/shader/visu/lowpoly/deferred_lightpass.frag.glsl index 614df9b..09c890b 100644 --- a/resources/shader/visu/lowpoly/deferred_lightpass.frag.glsl +++ b/resources/shader/visu/lowpoly/deferred_lightpass.frag.glsl @@ -21,6 +21,9 @@ uniform sampler2D gbuffer_position; uniform sampler2D gbuffer_normal; uniform sampler2D gbuffer_depth; uniform sampler2D gbuffer_albedo; +uniform sampler2D gbuffer_metallic; +uniform sampler2D gbuffer_roughness; +uniform sampler2D gbuffer_emissive; uniform sampler2D gbuffer_ao; // camera uniforms @@ -132,10 +135,12 @@ void main() vec3 buffer_pos = texture(gbuffer_position, v_texture_cords).rgb; vec3 buffer_normal = texture(gbuffer_normal, v_texture_cords).rgb; vec3 buffer_albedo = texture(gbuffer_albedo, v_texture_cords).rgb; - vec3 buffer_ao = texture(gbuffer_ao, v_texture_cords).rgb; - vec3 buffer_emissive = vec3(0.0); - float buffer_metal = 0.0; - float buffer_roughness = 1.0; + float buffer_metal = texture(gbuffer_metallic, v_texture_cords).r; + float buffer_roughness = texture(gbuffer_roughness, v_texture_cords).r; + float ao = texture(gbuffer_ao, v_texture_cords).r; + vec3 buffer_emissive = texture(gbuffer_emissive, v_texture_cords).rgb; + + float roughness = clamp(buffer_roughness, 0.04, 1.0); float inverse_metal = 1.0f - buffer_metal; @@ -152,19 +157,17 @@ void main() vec3 F0 = mix(vec3(0.04), buffer_albedo, buffer_metal); vec3 F = fresnel(F0, max(0.0, dot(H, V))); - vec3 specular = pbr_specular(N, V, H, L, F0, buffer_roughness); + vec3 specular = pbr_specular(N, V, H, L, F0, roughness); float NdotL = max(dot(N, L), 0.0); vec3 kD = (1.0 - F) * inverse_metal; - vec3 Lo = (kD * buffer_albedo / PI + specular) * radiance * NdotL; - - vec3 ambient = vec3(0.05) * buffer_albedo * buffer_ao.r; + vec3 diffuse = kD * buffer_albedo / PI; + vec3 Lo = (diffuse * ao + specular) * radiance * NdotL; - // also apply ao to Lo - Lo *= buffer_ao.r; + vec3 ambient = vec3(0.05) * buffer_albedo * ao; - vec3 fragment = ambient + Lo; + vec3 fragment = ambient + Lo + buffer_emissive; // HDR tonemapping fragment = tone_mapping_ACESFilm(fragment); diff --git a/resources/shader/visu/lowpoly/deferred_single_mesh.frag.glsl b/resources/shader/visu/lowpoly/deferred_single_mesh.frag.glsl index 604f0fc..c66eb92 100644 --- a/resources/shader/visu/lowpoly/deferred_single_mesh.frag.glsl +++ b/resources/shader/visu/lowpoly/deferred_single_mesh.frag.glsl @@ -14,4 +14,7 @@ void main() gbuffer_normal = v_normal; gbuffer_position = v_position; gbuffer_vposition = v_vposition.xyz; + gbuffer_metallic = 0.0; + gbuffer_roughness = 1.0; + gbuffer_emissive = vec3(0.0); } \ No newline at end of file diff --git a/src/Command/HDRIToCubemapCommand.php b/src/Command/HDRIToCubemapCommand.php new file mode 100644 index 0000000..8b5d0f6 --- /dev/null +++ b/src/Command/HDRIToCubemapCommand.php @@ -0,0 +1,134 @@ +> + */ + protected $expectedArguments = [ + 'input' => [ + 'prefix' => 'i', + 'longPrefix' => 'input', + 'description' => 'Path to the input equirectangular HDRI file (.hdr)', + 'required' => true, + ], + 'output' => [ + 'prefix' => 'o', + 'longPrefix' => 'output', + 'description' => 'Output directory for the cubemap face files (defaults to input directory)', + 'defaultValue' => null, + ], + 'size' => [ + 'prefix' => 's', + 'longPrefix' => 'size', + 'description' => 'Resolution of each cubemap face (default: 512)', + 'defaultValue' => '512', + ], + 'prefix' => [ + 'prefix' => 'p', + 'longPrefix' => 'prefix', + 'description' => 'Prefix for output filenames (default: cubemap_)', + 'defaultValue' => 'cubemap_', + ], + ]; + + /** + * Execute this command + */ + public function execute() + { + $inputPath = (string) $this->cli->arguments->get('input'); + $outputDir = $this->cli->arguments->get('output'); + $faceSize = (int) $this->cli->arguments->get('size'); + $prefix = (string) $this->cli->arguments->get('prefix'); + + // validate input file + if (!file_exists($inputPath) || !is_readable($inputPath)) { + $this->cli->error("Input file not found or not readable: {$inputPath}"); + return; + } + + // default output directory to input file directory + if ($outputDir === null) { + $outputDir = dirname($inputPath); + } else { + $outputDir = (string) $outputDir; + } + + $this->info("Converting HDRI to cubemap faces..."); + $this->info("Input: {$inputPath}"); + $this->info("Output: {$outputDir}"); + $this->info("Face size: {$faceSize}x{$faceSize}"); + + // initialize GLFW for headless OpenGL context + if (!glfwInit()) { + $this->cli->error("Failed to initialize GLFW"); + return; + } + + // create a hidden window for OpenGL context + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + + $window = glfwCreateWindow(1, 1, "HDRI to Cubemap", null, null); + glfwMakeContextCurrent($window); + + try { + // create GL state and converter + $gl = new GLState(); + $converter = new HDRIToCubemap($gl); + $cubemap = $converter->convert($inputPath, $faceSize); + $faceNames = ['px', 'nx', 'py', 'ny', 'pz', 'nz']; + + // read each face and save as HDR file + for ($i = 0; $i < 6; $i++) { + $faceName = $faceNames[$i]; + $outputPath = rtrim($outputDir, '/') . '/' . $prefix . $faceName . '.hdr'; + + $this->info("Saving face {$faceName} to {$outputPath}..."); + + // read face data from cubemap + $buffer = $cubemap->readFace($i); + + // use the php-glfw hdr writer... + $texture = Texture2D::fromBufferHDR($faceSize, $faceSize, $buffer, Texture2D::CHANNEL_RGB); + $texture->writeHDR($outputPath); + } + + $this->success("Successfully converted HDRI to 6 cubemap face files"); + $this->info("Face order: +X (px), -X (nx), +Y (py), -Y (ny), +Z (pz), -Z (nz)"); + + } catch (\Exception $e) { + $this->cli->error("Conversion failed: " . $e->getMessage()); + } finally { + glfwDestroyWindow($window); + glfwTerminate(); + } + } +} diff --git a/src/Component/VISULowPoly/DynamicRenderableModel.php b/src/Component/VISULowPoly/LPDynamicModel.php similarity index 93% rename from src/Component/VISULowPoly/DynamicRenderableModel.php rename to src/Component/VISULowPoly/LPDynamicModel.php index db1da09..462df1a 100644 --- a/src/Component/VISULowPoly/DynamicRenderableModel.php +++ b/src/Component/VISULowPoly/LPDynamicModel.php @@ -2,7 +2,7 @@ namespace VISU\Component\VISULowPoly; -class DynamicRenderableModel +class LPDynamicModel { /** * Construct a new DynamicRenderableModel diff --git a/src/Component/VISULowPoly/LPStaticModel.php b/src/Component/VISULowPoly/LPStaticModel.php new file mode 100644 index 0000000..8ed7899 --- /dev/null +++ b/src/Component/VISULowPoly/LPStaticModel.php @@ -0,0 +1,30 @@ + + * @var array */ private array $attachListeners = []; @@ -60,7 +60,7 @@ class EntityRegistry implements EntitiesInterface /** * An array holding the "detached" listeners * - * @var array + * @var array */ private array $detachListeners = []; @@ -194,7 +194,7 @@ public function attach(int $entity, object $component) : object // invoke attach listeners foreach($this->attachListenersHandles[$className] as $handle) { $listener = $this->attachListeners[$handle]; - $listener($entity, $component); + $listener($this, $entity, $component); } return $component; @@ -205,8 +205,8 @@ public function attach(int $entity, object $component) : object * attached to an entity. * * @template T of object - * @param class-string $componentClassName the component class name to listen for - * @param callable(int, T): void $callback + * @param class-string $componentClassName the component class name to listen for + * @param callable(self, int, T): void $callback * * @return int Returns the listener handle which can be used to deregister the listener later on */ @@ -250,7 +250,7 @@ public function detach(int $entity, string $componentClassName) : void // invoke detach listeners foreach($this->detachListenersHandles[$componentClassName] as $handle) { $listener = $this->detachListeners[$handle]; - $listener($entity, $this->entityComponents[$entity][$componentClassName]); + $listener($this, $entity, $this->entityComponents[$entity][$componentClassName]); } // actually detach the component @@ -271,7 +271,7 @@ public function detachAll(int $entity) : void // invoke detach listeners foreach($this->detachListenersHandles[$componentClassName] as $handle) { $listener = $this->detachListeners[$handle]; - $listener($entity, $component); + $listener($this, $entity, $component); } unset($this->components[$componentClassName][$entity]); @@ -284,8 +284,8 @@ public function detachAll(int $entity) : void * Registers a callback which is beeing invoked every time a component of given type is * dettached from an entity. * @template T - * @param class-string $componentClassName the component class name to listen for - * @param callable(int, T): void $callback + * @param class-string $componentClassName the component class name to listen for + * @param callable(self, int, T): void $callback * * @return int Returns the listener handle which can be used to deregister the listener later on */ diff --git a/src/Graphics/CubeVertexArray.php b/src/Graphics/CubeVertexArray.php new file mode 100644 index 0000000..3d0e65d --- /dev/null +++ b/src/Graphics/CubeVertexArray.php @@ -0,0 +1,124 @@ +vertexArray = 0; + $this->vertexBuffer = 0; + + glGenVertexArrays(1, $this->vertexArray); + glGenBuffers(1, $this->vertexBuffer); + $this->state->bindVertexArray($this->vertexArray); + $this->state->bindVertexArrayBuffer($this->vertexBuffer); + + // cube vertices (positions only for skybox) + $buffer = new FloatBuffer([ + // back face + -1.0, -1.0, -1.0, // bottom-left + 1.0, 1.0, -1.0, // top-right + 1.0, -1.0, -1.0, // bottom-right + 1.0, 1.0, -1.0, // top-right + -1.0, -1.0, -1.0, // bottom-left + -1.0, 1.0, -1.0, // top-left + + // front face + -1.0, -1.0, 1.0, // bottom-left + 1.0, -1.0, 1.0, // bottom-right + 1.0, 1.0, 1.0, // top-right + 1.0, 1.0, 1.0, // top-right + -1.0, 1.0, 1.0, // top-left + -1.0, -1.0, 1.0, // bottom-left + + // left face + -1.0, 1.0, 1.0, // top-right + -1.0, 1.0, -1.0, // top-left + -1.0, -1.0, -1.0, // bottom-left + -1.0, -1.0, -1.0, // bottom-left + -1.0, -1.0, 1.0, // bottom-right + -1.0, 1.0, 1.0, // top-right + + // right face + 1.0, 1.0, 1.0, // top-left + 1.0, -1.0, -1.0, // bottom-right + 1.0, 1.0, -1.0, // top-right + 1.0, -1.0, -1.0, // bottom-right + 1.0, 1.0, 1.0, // top-left + 1.0, -1.0, 1.0, // bottom-left + + // bottom face + -1.0, -1.0, -1.0, // top-right + 1.0, -1.0, -1.0, // top-left + 1.0, -1.0, 1.0, // bottom-left + 1.0, -1.0, 1.0, // bottom-left + -1.0, -1.0, 1.0, // bottom-right + -1.0, -1.0, -1.0, // top-right + + // top face + -1.0, 1.0, -1.0, // top-left + 1.0, 1.0, 1.0, // bottom-right + 1.0, 1.0, -1.0, // top-right + 1.0, 1.0, 1.0, // bottom-right + -1.0, 1.0, -1.0, // top-left + -1.0, 1.0, 1.0 // bottom-left + ]); + + glBufferData(GL_ARRAY_BUFFER, $buffer, GL_STATIC_DRAW); + + // declare the vertex attributes (position only) + glVertexAttribPointer(0, 3, GL_FLOAT, false, 3 * GL_SIZEOF_FLOAT, 0); + glEnableVertexAttribArray(0); + } + + /** + * Destructor + */ + public function __destruct() + { + glDeleteVertexArrays(1, $this->vertexArray); + glDeleteBuffers(1, $this->vertexBuffer); + } + + /** + * Binds the vertex array + */ + public function bind() : void + { + $this->state->bindVertexArray($this->vertexArray); + } + + /** + * Draws the cube + */ + public function draw() : void + { + $this->bind(); + glDrawArrays(GL_TRIANGLES, 0, 36); + } +} \ No newline at end of file diff --git a/src/Graphics/Cubemap.php b/src/Graphics/Cubemap.php new file mode 100644 index 0000000..e86f844 --- /dev/null +++ b/src/Graphics/Cubemap.php @@ -0,0 +1,385 @@ +id); + } + + /** + * Destructor + */ + public function __destruct() + { + glDeleteTextures(1, $this->id); + + if ($this->gl->currentTexture === $this->id) { + $this->gl->currentTexture = 0; + } + } + + /** + * Returns the cubemap face size (width and height are equal) + */ + public function size(): int + { + return $this->size; + } + + /** + * Sets the cubemap face size (used when allocating faces externally) + */ + public function setSize(int $size): void + { + $this->size = $size; + } + + /** + * Binds the cubemap to the current context and sets the active texture unit + * + * @param int $unit The texture unit to bind the cubemap to + */ + public function bind(int $unit = GL_TEXTURE0): void + { + if ($this->gl->currentTextureUnit !== $unit) { + glActiveTexture($unit); + $this->gl->currentTextureUnit = $unit; + } + + if ($this->gl->currentTexture !== $this->id) { + glBindTexture(GL_TEXTURE_CUBE_MAP, $this->id); + $this->gl->currentTexture = $this->id; + } + } + + /** + * Applies the textures minification and magnification filter parameters + */ + private function applyFilterParameters(): void + { + // to avoid incomplete textures ensure that mipmaps are generated + // when the min filter is set to mipmapped + if ($this->options->generateMipmaps === false) { + if ($this->options->minFilter === GL_LINEAR_MIPMAP_LINEAR || + $this->options->minFilter === GL_LINEAR_MIPMAP_NEAREST || + $this->options->minFilter === GL_NEAREST_MIPMAP_LINEAR || + $this->options->minFilter === GL_NEAREST_MIPMAP_NEAREST) { + throw new TextureLoadException("Mipmapped minification filter set but mipmaps are not generated, this results in incomplete textures"); + } + } + + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, $this->options->minFilter); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, $this->options->magFilter); + + // cubemaps always use clamp to edge to prevent seam artifacts + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + } + + /** + * Uploads buffer data for a single cubemap face + * + * @param int $faceTarget The cubemap face target (e.g., GL_TEXTURE_CUBE_MAP_POSITIVE_X) + * @param int $size The width/height of the face (must be square) + * @param int $internalFormat The internal format of the texture + * @param int $dataFormat The format of the source data + * @param int $dataType The type of the source data + * @param BufferInterface|null $buffer The buffer containing the pixel data + * @return void + */ + public function uploadFaceBuffer( + int $faceTarget, + int $size, + int $internalFormat, + int $dataFormat, + int $dataType, + ?BufferInterface $buffer = null + ): void + { + glTexImage2D( + $faceTarget, + 0, + $internalFormat, + $size, + $size, + 0, + $dataFormat, + $dataType, + $buffer + ); + } + + /** + * Uploads buffers for all 6 cubemap faces + * + * @param array $buffers Array of 6 buffers in face order (+X, -X, +Y, -Y, +Z, -Z) + * @return void + */ + public function uploadBuffers(TextureOptions $options, array $buffers): void + { + if (count($buffers) !== 6) { + throw new TextureLoadException("Cubemap requires exactly 6 face buffers, " . count($buffers) . " provided."); + } + + // store the options + $this->options = $options; + + // validate size is set + if ($options->width === null) { + throw new TextureLoadException("Width must be set in texture options for cubemap."); + } + + // cubemaps must be square + $this->size = $options->width; + + // bind + $this->bind(); + + // apply the texture parameters + $this->applyFilterParameters(); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + + // validate the formats + if ($options->internalFormat === null) { + throw new TextureLoadException("Internal format not set in texture options, cannot upload buffer to cubemap."); + } + + if ($options->dataFormat === null) { + throw new TextureLoadException("Source format not set in texture options, cannot upload buffer to cubemap."); + } + + if ($options->dataType === null) { + throw new TextureLoadException("Source type not set in texture options, cannot upload buffer to cubemap."); + } + + // upload each face + foreach (self::FACE_TARGETS as $index => $faceTarget) { + $this->uploadFaceBuffer( + $faceTarget, + $this->size, + $options->internalFormat, + $options->dataFormat, + $options->dataType, + $buffers[$index] + ); + } + + // generate mipmaps if requested + if ($options->generateMipmaps) { + glGenerateMipmap(GL_TEXTURE_CUBE_MAP); + } + } + + /** + * Loads a cubemap from 6 image files on disk + * + * @param array $paths Array of 6 file paths in face order (+X, -X, +Y, -Y, +Z, -Z) + * + * @throws TextureLoadException + */ + public function loadFromFiles(array $paths, ?TextureOptions $options = null): void + { + if (count($paths) !== 6) { + throw new TextureLoadException("Cubemap requires exactly 6 face images, " . count($paths) . " provided."); + } + + if ($options === null) { + $options = new TextureOptions(); + } + + // store the options + $this->options = $options; + + // bind + $this->bind(); + + // apply the texture parameters + $this->applyFilterParameters(); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + + $firstSize = null; + + foreach (self::FACE_TARGETS as $index => $faceTarget) { + $path = $paths[$index]; + + if (!file_exists($path) || !is_readable($path)) { + throw new TextureLoadException("Cubemap face image not found or not accessible: {$path}"); + } + + $textureData = Texture2D::fromDisk($path); + + // validate all faces have the same size + $faceSize = $textureData->width(); + if ($textureData->width() !== $textureData->height()) { + throw new TextureLoadException("Cubemap face must be square, got {$textureData->width()}x{$textureData->height()} for: {$path}"); + } + + if ($firstSize === null) { + $firstSize = $faceSize; + $this->size = $faceSize; + } elseif ($faceSize !== $firstSize) { + throw new TextureLoadException("All cubemap faces must have the same size. Expected {$firstSize}, got {$faceSize} for: {$path}"); + } + + switch ($textureData->channels()) { + case 4: + $guessedInternalFormat = $options->isSRGB ? GL_SRGB_ALPHA : GL_RGBA; + $guessedSourceFormat = GL_RGBA; + break; + case 3: + $guessedInternalFormat = $options->isSRGB ? GL_SRGB : GL_RGB; + $guessedSourceFormat = GL_RGB; + break; + case 2: + $guessedInternalFormat = GL_RG; + $guessedSourceFormat = GL_RG; + break; + case 1: + $guessedInternalFormat = GL_RED; + $guessedSourceFormat = GL_RED; + break; + default: + throw new TextureLoadException("Unsupported number of channels: {$textureData->channels()}"); + } + + if ($options->internalFormat === null) { + $options->internalFormat = $guessedInternalFormat; + } + + if ($options->dataFormat === null) { + $options->dataFormat = $guessedSourceFormat; + } + + if ($options->dataType === null) { + $options->dataType = GL_UNSIGNED_BYTE; + } + + glTexImage2D( + $faceTarget, + 0, + $options->internalFormat, + $faceSize, + $faceSize, + 0, + $options->dataFormat, + $options->dataType, + $textureData->buffer() + ); + } + + // generate mipmaps if requested + if ($options->generateMipmaps) { + glGenerateMipmap(GL_TEXTURE_CUBE_MAP); + } + } + + /** + * Allocates an empty cubemap with the given size and options + * + * @param int $size The width/height of each face (cubemaps are always square) + */ + public function allocateEmpty(int $size, ?TextureOptions $options = null): void + { + if ($options === null) { + $options = new TextureOptions(); + } + + if ($options->internalFormat === null) { + $options->internalFormat = GL_RGBA; + } + + if ($options->dataFormat === null) { + $options->dataFormat = GL_RGBA; + } + + if ($options->dataType === null) { + $options->dataType = GL_UNSIGNED_BYTE; + } + + $options->width = $size; + $options->height = $size; + + // create array of null buffers for each face + $buffers = array_fill(0, 6, null); + + $this->uploadBuffers($options, $buffers); + } + + /** + * Reads a face of the cubemap back into a float buffer + * + * @param int $faceIndex The face index (0-5 for px, nx, py, ny, pz, nz) + * @return FloatBuffer + */ + public function readFace(int $faceIndex): FloatBuffer + { + if ($faceIndex < 0 || $faceIndex >= 6) { + throw new \InvalidArgumentException("Face index must be between 0 and 5"); + } + + // create framebuffer for resource management + $framebuffer = new Framebuffer($this->gl); + $framebuffer->bind(); + $framebuffer->attachTextureId(GL_COLOR_ATTACHMENT0, self::FACE_TARGETS[$faceIndex], $this->id); + + // check framebuffer completeness + if (!$framebuffer->isValid($status, $error)) { + throw new VISUException("Framebuffer not complete for reading cubemap face: " . $error); + } + + // read the texture into a float buffer + $buffer = new FloatBuffer(); + glReadPixels(0, 0, $this->size, $this->size, GL_RGB, GL_FLOAT, $buffer); + + return $buffer; + } +} diff --git a/src/Graphics/Framebuffer.php b/src/Graphics/Framebuffer.php index 550c03e..51de457 100644 --- a/src/Graphics/Framebuffer.php +++ b/src/Graphics/Framebuffer.php @@ -27,6 +27,13 @@ final public function __construct(GLState $gl) */ final public function __destruct() { + // if the current framebuffer is bound unbind it + if ($this->gl->currentDrawFramebuffer === $this->id || $this->gl->currentReadFramebuffer === $this->id) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + $this->gl->currentDrawFramebuffer = 0; + $this->gl->currentReadFramebuffer = 0; + } + glDeleteFramebuffers(1, $this->id); } @@ -48,4 +55,12 @@ public function createRenderbufferAttachment(int $format, int $attachment, int $ $this->renderbufferAttachments[$attachment] = $rbo; } -} + + /** + * Attaches a existing texture to the framebuffer + */ + public function attachTextureId(int $attachment, int $textureTarget, int $textureId, int $level = 0): void + { + glFramebufferTexture2D(GL_FRAMEBUFFER, $attachment, $textureTarget, $textureId, $level); + } +} \ No newline at end of file diff --git a/src/Graphics/HDRIToCubemap.php b/src/Graphics/HDRIToCubemap.php new file mode 100644 index 0000000..846531d --- /dev/null +++ b/src/Graphics/HDRIToCubemap.php @@ -0,0 +1,256 @@ +createShaders(); + $this->createGeometry(); + $this->createFramebuffer(); + } + + /** + * Creates the shader program for converting equirectangular to cubemap + */ + private function createShaders(): void + { + $this->equirectangularToCubemapShader = new ShaderProgram($this->gl); + + // vertex shader - renders a full screen cube + $this->equirectangularToCubemapShader->attach(new ShaderStage(ShaderStage::VERTEX, <<<'GLSL' +#version 330 core +layout (location = 0) in vec3 a_pos; + +out vec3 local_pos; + +uniform mat4 projection; +uniform mat4 view; + +void main() +{ + local_pos = a_pos; + + gl_Position = projection * view * vec4(local_pos, 1.0); +} +GLSL + )); + + // fragment shader - converts equirectangular to cubemap + $this->equirectangularToCubemapShader->attach(new ShaderStage(ShaderStage::FRAGMENT, <<<'GLSL' +#version 330 core +out vec3 frag_color; +in vec3 local_pos; + +uniform sampler2D hdritex; + +// https://stackoverflow.com/questions/48494389/how-does-this-code-sample-from-a-spherical-map +const vec2 invAtan = vec2(0.1591, 0.3183); +vec2 sampleSphere(vec3 v) +{ + vec2 uv = vec2(atan(v.z, v.x), asin(v.y)); + uv *= invAtan; + uv += 0.5; + return uv; +} + +void main() +{ + vec2 uv = sampleSphere(normalize(local_pos)); + vec3 color = texture(hdritex, uv).rgb; + + frag_color = color; +} +GLSL + )); + + $this->equirectangularToCubemapShader->link(); + } + + /** + * Creates the cube geometry for rendering + */ + private function createGeometry(): void + { + $this->cubeVAO = new CubeVertexArray($this->gl); + } + + /** + * Creates the framebuffer for rendering to cubemap faces + */ + private function createFramebuffer(): void + { + $this->framebuffer = new Framebuffer($this->gl); + } + + /** + * Converts an equirectangular HDRI image to a cubemap + * + * @param string $hdriPath Path to the HDRI file + * @param int $cubemapSize Size of each cubemap face edge + */ + public function convert(string $hdriPath, int $cubemapSize = 512): Cubemap + { + if (!file_exists($hdriPath)) { + throw new VISUException("HDRI file not found: {$hdriPath}"); + } + + // load the equirectangular HDR texture + $hdriTexture = $this->loadHDRITexture($hdriPath); + + // create the output cubemap + $cubemap = new Cubemap($this->gl, "hdri_converted_cubemap"); + + // allocate cubemap with HDR format + $options = new TextureOptions(); + $options->internalFormat = GL_RGB16F; + $options->dataFormat = GL_RGB; + $options->dataType = GL_FLOAT; + $options->generateMipmaps = false; + $options->minFilter = GL_LINEAR; + $options->magFilter = GL_LINEAR; + + $cubemap->allocateEmpty($cubemapSize, $options); + + // setup framebuffer for rendering to cubemap faces + $this->framebuffer->bind(); + + // setup viewport and projection matrix for cubemap faces + glViewport(0, 0, $cubemapSize, $cubemapSize); + + $captureProjection = new Mat4(); + $captureProjection->perspective(\GL\Math\GLM::radians(90.0), 1.0, 0.1, 10.0); + + // view matrices for each cubemap face + $captureViews = []; + + // +X face + $view = new Mat4(); + $view->lookAt( + new Vec3(0.0, 0.0, 0.0), + new Vec3( 1.0, 0.0, 0.0), + new Vec3(0.0, -1.0, 0.0) + ); + $captureViews[] = $view; + + // -X face + $view = new Mat4(); + $view->lookAt( + new Vec3(0.0, 0.0, 0.0), + new Vec3(-1.0, 0.0, 0.0), + new Vec3(0.0, -1.0, 0.0) + ); + $captureViews[] = $view; + + // +Y face + $view = new Mat4(); + $view->lookAt( + new Vec3(0.0, 0.0, 0.0), + new Vec3( 0.0, 1.0, 0.0), + new Vec3(0.0, 0.0, 1.0) + ); + $captureViews[] = $view; + + // -Y face + $view = new Mat4(); + $view->lookAt( + new Vec3(0.0, 0.0, 0.0), + new Vec3( 0.0, -1.0, 0.0), + new Vec3(0.0, 0.0, -1.0) + ); + $captureViews[] = $view; + + // +Z face + $view = new Mat4(); + $view->lookAt( + new Vec3(0.0, 0.0, 0.0), + new Vec3( 0.0, 0.0, 1.0), + new Vec3(0.0, -1.0, 0.0) + ); + $captureViews[] = $view; + + // -Z face + $view = new Mat4(); + $view->lookAt( + new Vec3(0.0, 0.0, 0.0), + new Vec3( 0.0, 0.0, -1.0), + new Vec3(0.0, -1.0, 0.0) + ); + $captureViews[] = $view; + + $this->equirectangularToCubemapShader->use(); + $this->equirectangularToCubemapShader->setUniformMat4('projection', false, $captureProjection); + + $hdriTexture->bind(GL_TEXTURE0); + $this->equirectangularToCubemapShader->setUniformInt('hdritex', 0); + + // render to each cubemap face + for ($i = 0; $i < 6; $i++) { + $this->equirectangularToCubemapShader->setUniformMat4('view', false, $captureViews[$i]); + + // attach current face to framebuffer + $this->framebuffer->attachTextureId(GL_COLOR_ATTACHMENT0, Cubemap::FACE_TARGETS[$i], $cubemap->id); + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // render cube + $this->cubeVAO->draw(); + } + + // generate mipmaps for the cubemap + $cubemap->bind(); + glGenerateMipmap(GL_TEXTURE_CUBE_MAP); + + return $cubemap; + } + + /** + * loads an HDRI texture from a file + */ + private function loadHDRITexture(string $path): Texture + { + $hdriTexture2D = Texture2D::fromDisk($path); + + $texture = new Texture($this->gl, 'hdri_equirectangular'); + + $options = new TextureOptions(); + $options->width = $hdriTexture2D->width(); + $options->height = $hdriTexture2D->height(); + $options->internalFormat = GL_RGB16F; + $options->dataFormat = GL_RGB; + $options->dataType = GL_FLOAT; + $options->generateMipmaps = false; + $options->minFilter = GL_LINEAR; + $options->magFilter = GL_LINEAR; + $options->wrapS = GL_CLAMP_TO_EDGE; + $options->wrapT = GL_CLAMP_TO_EDGE; + + $texture->uploadBuffer($options, $hdriTexture2D->buffer()); + + return $texture; + } +} \ No newline at end of file diff --git a/src/Graphics/QuadVertexArray.php b/src/Graphics/QuadVertexArray.php index cbcae7d..f89af8d 100644 --- a/src/Graphics/QuadVertexArray.php +++ b/src/Graphics/QuadVertexArray.php @@ -40,7 +40,7 @@ public function __construct( // two triangles to form a quad (CCW) $buffer = new FloatBuffer([ - // positions // texture Coords + // positions // texture coords -1.0, 1.0, 0.0, 0.0, 1.0, // top left -1.0, -1.0, 0.0, 0.0, 0.0, // bottom left 1.0, 1.0, 0.0, 1.0, 1.0, // top right diff --git a/src/Graphics/Rendering/Pass/CubemapPass.php b/src/Graphics/Rendering/Pass/CubemapPass.php new file mode 100644 index 0000000..14991b9 --- /dev/null +++ b/src/Graphics/Rendering/Pass/CubemapPass.php @@ -0,0 +1,89 @@ +reads($this, $this->cubemapRes); + $pipeline->writes($this, $this->renderTargetRes); + } + + /** + * Executes the render pass + */ + public function execute(PipelineContainer $data, PipelineResources $resources): void + { + $resources->activateRenderTarget($this->renderTargetRes); + + // fetch camera data + $cameraData = $data->get(CameraData::class); + + /** @var CubeVertexArray */ + $cubeVA = $resources->cacheStaticResource('cubeva', function(GLState $gl) { + return new CubeVertexArray($gl); + }); + + $cubeVA->bind(); + $this->shader->use(); + + // we need to see the inner faces of the cubemap + glDisable(GL_CULL_FACE); + + $glCubemap = $resources->getCubemap($this->cubemapRes); + $this->shader->setUniform1i($this->cubemapUniformName, 0); + $this->shader->setUniformMat4('u_projection', false, $cameraData->projection); + $this->shader->setUniformMat4('u_view', false, $cameraData->view); + + // render skybox last and disable depth writes + if (!$this->writeDepth) { + glDepthFunc(GL_LEQUAL); + glDepthMask(false); + } + + $glCubemap->bind(); + $cubeVA->draw(); + + // restore depth state + if (!$this->writeDepth) { + glDepthMask(true); + glDepthFunc(GL_LESS); + } + } +} \ No newline at end of file diff --git a/src/Graphics/Rendering/Pass/DeferredLightPass.php b/src/Graphics/Rendering/Pass/DeferredLightPass.php index 4e24695..b5c1fc1 100644 --- a/src/Graphics/Rendering/Pass/DeferredLightPass.php +++ b/src/Graphics/Rendering/Pass/DeferredLightPass.php @@ -36,6 +36,9 @@ public function setup(RenderPipeline $pipeline, PipelineContainer $data): void $pipeline->reads($this, $gbufferData->albedoTexture); $pipeline->reads($this, $gbufferData->normalTexture); $pipeline->reads($this, $gbufferData->worldSpacePositionTexture); + $pipeline->reads($this, $gbufferData->metallicTexture); + $pipeline->reads($this, $gbufferData->roughnessTexture); + $pipeline->reads($this, $gbufferData->emissiveTexture); // create light pass target with the same size as the gbuffer $lightpassData->renderTarget = $pipeline->createRenderTarget('lightpass', $gbufferData->renderTarget->width, $gbufferData->renderTarget->height); @@ -83,6 +86,9 @@ public function execute(PipelineContainer $data, PipelineResources $resources): [$gbufferData->normalTexture, 'normal'], [$gbufferData->depthTexture, 'depth'], [$gbufferData->albedoTexture, 'albedo'], + [$gbufferData->metallicTexture, 'metallic'], + [$gbufferData->roughnessTexture, 'roughness'], + [$gbufferData->emissiveTexture, 'emissive'], [$ssaoData->blurTexture, 'ao'], ] as $i => $tuple) { list($texture, $name) = $tuple; diff --git a/src/Graphics/Rendering/Pass/FullscreenQuadPass.php b/src/Graphics/Rendering/Pass/FullscreenQuadPass.php index 9822400..d884024 100644 --- a/src/Graphics/Rendering/Pass/FullscreenQuadPass.php +++ b/src/Graphics/Rendering/Pass/FullscreenQuadPass.php @@ -35,8 +35,6 @@ class FullscreenQuadPass extends RenderPass /** * Constructor - * - * @return void */ public function __construct( private RenderTargetResource $renderTargetRes, diff --git a/src/Graphics/Rendering/Pass/GBufferPass.php b/src/Graphics/Rendering/Pass/GBufferPass.php index 27fafa4..a0ae683 100644 --- a/src/Graphics/Rendering/Pass/GBufferPass.php +++ b/src/Graphics/Rendering/Pass/GBufferPass.php @@ -39,6 +39,27 @@ public function setup(RenderPipeline $pipeline, PipelineContainer $data): void $albedoTextureOptions = new TextureOptions; $albedoTextureOptions->internalFormat = GL_SRGB; $gbufferData->albedoTexture = $pipeline->createColorAttachment($gbufferData->renderTarget, 'albedo', $albedoTextureOptions); + + $metallicTextureOptions = new TextureOptions; + $metallicTextureOptions->dataFormat = GL_RED; + $metallicTextureOptions->dataType = GL_UNSIGNED_BYTE; + $metallicTextureOptions->internalFormat = GL_R8; + $metallicTextureOptions->generateMipmaps = false; + $gbufferData->metallicTexture = $pipeline->createColorAttachment($gbufferData->renderTarget, 'metallic', $metallicTextureOptions); + + $roughnessTextureOptions = new TextureOptions; + $roughnessTextureOptions->dataFormat = GL_RED; + $roughnessTextureOptions->dataType = GL_UNSIGNED_BYTE; + $roughnessTextureOptions->internalFormat = GL_R8; + $roughnessTextureOptions->generateMipmaps = false; + $gbufferData->roughnessTexture = $pipeline->createColorAttachment($gbufferData->renderTarget, 'roughness', $roughnessTextureOptions); + + $emissiveTextureOptions = new TextureOptions; + $emissiveTextureOptions->internalFormat = GL_RGB16F; + $emissiveTextureOptions->dataFormat = GL_RGB; + $emissiveTextureOptions->dataType = GL_FLOAT; + $emissiveTextureOptions->generateMipmaps = false; + $gbufferData->emissiveTexture = $pipeline->createColorAttachment($gbufferData->renderTarget, 'emissive', $emissiveTextureOptions); } /** diff --git a/src/Graphics/Rendering/Pass/GBufferPassData.php b/src/Graphics/Rendering/Pass/GBufferPassData.php index b020f9f..c1e371f 100644 --- a/src/Graphics/Rendering/Pass/GBufferPassData.php +++ b/src/Graphics/Rendering/Pass/GBufferPassData.php @@ -13,4 +13,7 @@ class GBufferPassData public TextureResource $viewSpacePositionTexture; public TextureResource $normalTexture; public TextureResource $albedoTexture; + public TextureResource $metallicTexture; + public TextureResource $roughnessTexture; + public TextureResource $emissiveTexture; } diff --git a/src/Graphics/Rendering/PipelineResources.php b/src/Graphics/Rendering/PipelineResources.php index 3ed3116..b52630d 100644 --- a/src/Graphics/Rendering/PipelineResources.php +++ b/src/Graphics/Rendering/PipelineResources.php @@ -2,6 +2,7 @@ namespace VISU\Graphics\Rendering; +use VISU\Graphics\Cubemap; use VISU\Graphics\Exception\PipelineResourceException; use VISU\Graphics\Framebuffer; use VISU\Graphics\GLState; @@ -33,6 +34,13 @@ class PipelineResources */ private array $textures = []; + /** + * Internal array of cubemaps + * + * @var array + */ + private array $cubemaps = []; + /** * Holder of mixed generic static resources * @@ -369,6 +377,64 @@ public function cacheStaticResource(string $name, callable $callback): mixed return $this->staticStorage[$name]; } + /** + * Sets a cubemap to the given handle + * + * @param RenderResource $resource + * @param Cubemap $cubemap + * + * @return void + */ + public function setCubemap(RenderResource $resource, Cubemap $cubemap): void + { + $this->cubemaps[$resource->name] = $cubemap; + } + + /** + * Returns a cubemap for the given resource + * + * @param RenderResource $resource + * @return Cubemap + */ + public function getCubemap(RenderResource $resource): Cubemap + { + if (!isset($this->cubemaps[$resource->name])) { + throw new PipelineResourceException("Cubemap not found for resource handle: " . $resource->handle . ' name: ' . $resource->name); + } + + $this->resourceUseTick[$resource->name] = $this->tickIndex; + return $this->cubemaps[$resource->name]; + } + + /** + * Returns a cubemap ID for the given resource + * The cubemap ID is the raw GL handle + * + * @param RenderResource $resource + * @return int + */ + public function getCubemapID(RenderResource $resource): int + { + return $this->getCubemap($resource)->id; + } + + /** + * Returns a cubemap by its name, will return null if not found + * This will also update the resource use tick + * + * @param string $name + * @return Cubemap|null + */ + public function getCubemapByName(string $name): ?Cubemap + { + if (!isset($this->cubemaps[$name])) { + return null; + } + + $this->resourceUseTick[$name] = $this->tickIndex; + return $this->cubemaps[$name]; + } + /** * Collects all garbage and removes all unused resources * @@ -379,6 +445,8 @@ public function collectGarbage(): void foreach ($this->resourceUseTick as $name => $tick) { if ($tick < $this->tickIndex) { unset($this->renderTargets[$name]); + unset($this->textures[$name]); + unset($this->cubemaps[$name]); unset($this->resourceUseTick[$name]); } } diff --git a/src/Graphics/Rendering/RenderPipeline.php b/src/Graphics/Rendering/RenderPipeline.php index 99f5132..4616d04 100644 --- a/src/Graphics/Rendering/RenderPipeline.php +++ b/src/Graphics/Rendering/RenderPipeline.php @@ -2,9 +2,11 @@ namespace VISU\Graphics\Rendering; +use VISU\Graphics\Cubemap; use VISU\Graphics\Rendering\Pass\BackbufferData; use VISU\Graphics\Rendering\Resource\RenderTargetResource; use VISU\Graphics\Rendering\Resource\TextureResource; +use VISU\Graphics\Rendering\Resource\CubemapResource; use VISU\Graphics\RenderTarget; use VISU\Graphics\Texture; use VISU\Graphics\TextureOptions; @@ -210,6 +212,24 @@ public function importTexture(string $resourceName, Texture $texture): TextureRe return $resource; } + /** + * Imports a cubemap resource + * + * @param string $resourceName + * @param Cubemap $cubemap + * + * @return CubemapResource + */ + public function importCubemap(string $resourceName, Cubemap $cubemap): CubemapResource + { + /** @var CubemapResource */ + $resource = $this->createResource(CubemapResource::class, $resourceName, $cubemap->size()); + + $this->resourceAllocator->setCubemap($resource, $cubemap); + + return $resource; + } + /** * Adds a new render pass to the pipeline * diff --git a/src/Graphics/Rendering/Renderer/CubemapRenderer.php b/src/Graphics/Rendering/Renderer/CubemapRenderer.php new file mode 100644 index 0000000..47df1a3 --- /dev/null +++ b/src/Graphics/Rendering/Renderer/CubemapRenderer.php @@ -0,0 +1,91 @@ +skyboxShaderProgram = new ShaderProgram($glstate); + + // attach skybox vertex shader + $this->skyboxShaderProgram->attach(new ShaderStage(ShaderStage::VERTEX, <<< 'GLSL' + #version 330 core + + layout (location = 0) in vec3 a_pos; + + out vec3 tex_coords; + + uniform mat4 u_projection; + uniform mat4 u_view; + + void main() + { + tex_coords = a_pos; + // drop translation, we are in the skybox + mat4 view = mat4(mat3(u_view)); + vec4 pos = u_projection * view * vec4(a_pos, 1.0); + gl_Position = pos.xyww; + } + GLSL)); + + // attach skybox fragment shader + $this->skyboxShaderProgram->attach(new ShaderStage(ShaderStage::FRAGMENT, <<< 'GLSL' + #version 330 core + + out vec4 frag_color; + + in vec3 tex_coords; + + uniform samplerCube u_skybox; + + void main() + { + frag_color = texture(u_skybox, tex_coords); + } + GLSL)); + + $this->skyboxShaderProgram->link(); + } + + /** + * Attaches a Skybox pass with the given cubemap + */ + public function attachSkyboxPass( + RenderPipeline $pipeline, + RenderTargetResource $renderTarget, + CubemapResource $cubemap, + ) : CubemapPass + { + $pass = new CubemapPass( + $renderTarget, + $cubemap, + $this->skyboxShaderProgram, + ); + + $pipeline->addPass($pass); + + return $pass; + } +} \ No newline at end of file diff --git a/src/Graphics/Rendering/Resource/CubemapResource.php b/src/Graphics/Rendering/Resource/CubemapResource.php new file mode 100644 index 0000000..7be6865 --- /dev/null +++ b/src/Graphics/Rendering/Resource/CubemapResource.php @@ -0,0 +1,26 @@ +dispatcher = $getOrCreateService('visu.dispatcher', function() { return new Dispatcher(); @@ -151,6 +161,11 @@ public function __construct( return $shaders; }); + // audio engine + $getOrCreateService('audio.engine', function(): Engine { + return new Engine();; + }); + // create & initialize the window $windowHints = new WindowHints(); if ($options->windowHeadless) { @@ -239,6 +254,9 @@ public function update() : void // poll for new events $this->window->pollEvents(); + // process event loop timers + EventLoop::main()->tick(); + // run the update callback if available $this->options->update?->__invoke($this); diff --git a/src/Quickstart/Render/QuickstartDebugMetricsOverlay.php b/src/Quickstart/Render/QuickstartDebugMetricsOverlay.php index b0a2740..695d253 100644 --- a/src/Quickstart/Render/QuickstartDebugMetricsOverlay.php +++ b/src/Quickstart/Render/QuickstartDebugMetricsOverlay.php @@ -114,7 +114,7 @@ private function gameLoopMetrics(float $deltaTime) : string * * Example: * [RenderPass] CPU(10): 1.23 ms | GPU(10): 2.34 ms | Tri: 12345 - * [ShadowPass] CPU(10): 0.56 ms | GPU(10): 1.78 ms § | Tri: 6789 + * [ShadowPass] CPU(10): 0.56 ms | GPU(10): 1.78 ms | Tri: 6789 * * @return array */ diff --git a/src/Runtime/EventLoop.php b/src/Runtime/EventLoop.php new file mode 100644 index 0000000..12ac14e --- /dev/null +++ b/src/Runtime/EventLoop.php @@ -0,0 +1,173 @@ +schedule($delay, $callback); + } + + /** + * @var TimerHeap Min-heap of scheduled timers ordered by execution time + */ + private TimerHeap $timers; + + /** + * @var array Map of timer IDs to Timer objects for O(1) cancellation lookup + */ + private array $timerMap = []; + + private int $timerIdCounter = 0; + + /** + * Cached next timer execution time for early termination optimization + */ + private ?int $nextTimerTime = null; + + /** + * Constructor + */ + public function __construct() + { + $this->timers = new TimerHeap(); + } + + /** + * Creates the internal timer and returns a cancel function + */ + public function schedule(int $delay, callable $callback) : callable + { + $id = ++$this->timerIdCounter; + $targetTime = $this->now() + max(0, $delay); + + // convert callable + $closureCallback = $callback instanceof Closure ? $callback : Closure::fromCallable($callback); + $timer = new Timer($targetTime, $closureCallback, $id); + + // add to heap + $this->timers->insert($timer); + + // maintain map + $this->timerMap[$id] = $timer; + + // update cached next timer time + if ($this->nextTimerTime === null || $targetTime < $this->nextTimerTime) { + $this->nextTimerTime = $targetTime; + } + + // return cancel function + return function () use ($id) { + if (isset($this->timerMap[$id])) { + $timer = $this->timerMap[$id]; + $timer->cancel(); + + // invalidate cache if we're removing the next timer + if ($this->nextTimerTime === $timer->time) { + $this->nextTimerTime = null; + } + + unset($this->timerMap[$id]); + } + }; + } + + /** + * Ticker, call this in your game loop. + * + * This will be where the scheduled timers are checked and executed. + * Optimized with early termination and heap-based processing. + */ + public function tick(): void + { + // early termination: if no timers or next timer isn't ready yet + if ($this->timers->count() === 0) { + return; + } + + $now = $this->now(); + + // early termination using cached next timer time + if ($this->nextTimerTime !== null && $now < $this->nextTimerTime) { + return; + } + + // process ready timers from heap (already sorted by time) + while ($this->timers->count() > 0) { + $timer = $this->timers->top(); + + // early termination: if next timer isn't ready, we're done + if ($timer->time > $now) { + $this->nextTimerTime = $timer->time; + break; + } + + // remove from heap + $this->timers->extract(); + + // skip cancelled timers + if ($timer->isCancelled()) { + continue; + } + + // remove from map + unset($this->timerMap[$timer->id]); + + // execute the callback + ($timer->callback)(); + } + + // update next timer cache + $this->nextTimerTime = $this->timers->count() > 0 ? $this->timers->top()->time : null; + } + + private function now(): int + { + return (int)(glfwGetTime() * 1000); // convert seconds to milliseconds + } +} \ No newline at end of file diff --git a/src/Runtime/Timer.php b/src/Runtime/Timer.php new file mode 100644 index 0000000..fa7f4b1 --- /dev/null +++ b/src/Runtime/Timer.php @@ -0,0 +1,32 @@ +cancelled = true; + } + + /** + * Check if this timer has been cancelled + */ + public function isCancelled(): bool + { + return $this->cancelled; + } +} \ No newline at end of file diff --git a/src/Runtime/TimerHeap.php b/src/Runtime/TimerHeap.php new file mode 100644 index 0000000..ada501b --- /dev/null +++ b/src/Runtime/TimerHeap.php @@ -0,0 +1,16 @@ + + */ +class TimerHeap extends SplMinHeap +{ + protected function compare(mixed $timer1, mixed $timer2): int + { + return $timer2->time <=> $timer1->time; + } +} \ No newline at end of file diff --git a/src/System/VISULowPoly/LPException.php b/src/System/VISULowPoly/LPException.php new file mode 100644 index 0000000..f29f45b --- /dev/null +++ b/src/System/VISULowPoly/LPException.php @@ -0,0 +1,9 @@ +id = self::$materialCounter++; $this->name = $name; $this->color = $color; - $this->shininess = $shininess; + $this->roughness = $roughness; + $this->metallic = $metallic; } } diff --git a/src/System/VISULowPoly/LPMesh.php b/src/System/VISULowPoly/LPMesh.php index cee2038..06b5c72 100644 --- a/src/System/VISULowPoly/LPMesh.php +++ b/src/System/VISULowPoly/LPMesh.php @@ -6,6 +6,16 @@ class LPMesh { + /** + * The handle of the static geometry draw call assembler entry for this mesh + */ + public ?int $staticDCAHandle = null; + + /** + * The handle of the dynamic geometry draw call assembler entry for this mesh + */ + public ?int $dynamicDCAHandle = null; + /** * Constructor * diff --git a/src/System/VISULowPoly/LPModel.php b/src/System/VISULowPoly/LPModel.php index 21e4723..06563a4 100644 --- a/src/System/VISULowPoly/LPModel.php +++ b/src/System/VISULowPoly/LPModel.php @@ -26,6 +26,16 @@ class LPModel */ public AABB $aabb; + /** + * The handle of the static geometry draw call assembler entry for this mesh + */ + public ?int $staticDCAHandle = null; + + /** + * The handle of the dynamic geometry draw call assembler entry for this mesh + */ + public ?int $dynamicDCAHandle = null; + /** * Constructor * diff --git a/src/System/VISULowPoly/LPObjLoader.php b/src/System/VISULowPoly/LPObjLoader.php index 570f1d3..b77c3ba 100644 --- a/src/System/VISULowPoly/LPObjLoader.php +++ b/src/System/VISULowPoly/LPObjLoader.php @@ -4,6 +4,7 @@ use GL\Buffer\FloatBuffer; use GL\Geometry\ObjFileParser; +use GL\Math\Vec3; use VISU\Exception\VISUException; use VISU\Geo\AABB; use VISU\Graphics\GLState; @@ -15,89 +16,228 @@ public function __construct(private GLState $gl) } /** - * Loads a single object file and returns it + * Imports a mesh from a source buffer into the given LowPoly vertex buffer * - * @param string $path - * @return LPModel + * The source mesh is expected to be in the format of: + * - [3: pos, 3: normal] = stride of 6 floats per vertex + * + * The material will be baked into the vertex buffer as well. */ - private function loadFile(string $path, FloatBuffer $buffer, LPVertexBuffer $vb, int &$vertexOffset, float $scaleModifier = 1.0) : LPModel + public function importMesh( + FloatBuffer $sourceMesh, + LPMaterial $material, + LPVertexBuffer $vb, + float $scaleModifier = 1.0 + ) : LPMesh + { + // this is quite inefficient, as we create a temporary buffer for each mesh + // and then copy the data over. But this only happens during loading, and therefor + // doesnt really impact runtime performance so i keep this simpler approach for now. + $tmpBuffer = new FloatBuffer(); + + $aabbMin = new Vec3(PHP_FLOAT_MAX, PHP_FLOAT_MAX, PHP_FLOAT_MAX); + $aabbMax = new Vec3(-PHP_FLOAT_MAX, -PHP_FLOAT_MAX, -PHP_FLOAT_MAX); + + for ($i = 0; $i < $sourceMesh->size(); $i+=6) { + // position + $tmpBuffer->push($sourceMesh[$i + 0] * $scaleModifier); + $tmpBuffer->push($sourceMesh[$i + 1] * $scaleModifier); + $tmpBuffer->push($sourceMesh[$i + 2] * $scaleModifier); + + // normal + $tmpBuffer->push($sourceMesh[$i + 3]); + $tmpBuffer->push($sourceMesh[$i + 4]); + $tmpBuffer->push($sourceMesh[$i + 5]); + + // color + $tmpBuffer->push($material->color->x); + $tmpBuffer->push($material->color->y); + $tmpBuffer->push($material->color->z); + + // roughness + $tmpBuffer->push($material->roughness); + + // metallic + $tmpBuffer->push($material->metallic); + + // emissive + if ($material->emissive) { + $tmpBuffer->push($material->emissive->x); + $tmpBuffer->push($material->emissive->y); + $tmpBuffer->push($material->emissive->z); + } else { + $tmpBuffer->push(0.0); + $tmpBuffer->push(0.0); + $tmpBuffer->push(0.0); + } + + // update AABB + $aabbMin->x = min($aabbMin->x, $sourceMesh[$i + 0] * $scaleModifier); + $aabbMin->y = min($aabbMin->y, $sourceMesh[$i + 1] * $scaleModifier); + $aabbMin->z = min($aabbMin->z, $sourceMesh[$i + 2] * $scaleModifier); + $aabbMax->x = max($aabbMax->x, $sourceMesh[$i + 0] * $scaleModifier); + $aabbMax->y = max($aabbMax->y, $sourceMesh[$i + 1] * $scaleModifier); + $aabbMax->z = max($aabbMax->z, $sourceMesh[$i + 2] * $scaleModifier); + } + + $mesh = new LPMesh( + $material, + $vb, + $vb->appennd($tmpBuffer), + $sourceMesh->size() / 6, + new AABB( + $aabbMin, + $aabbMax, + ) + ); + + return $mesh; + } + + private function shininessToRoughness(float $ns): float + { + $ns = max(0.0, min(1000.0, $ns)); + $roughness = 1.0 - sqrt($ns / 1000.0); + return max(0.0, min(1.0, $roughness)); + } + + private function vec3IsNonZero(Vec3 $v, float $eps = 1e-6): bool + { + return (abs($v->x) > $eps) || (abs($v->y) > $eps) || (abs($v->z) > $eps); + } + + /** + * Loads a single object file into the given model collection + */ + private function loadFile( + string $path, + string $modelName, + LPVertexBuffer $vb, + LPModelCollection $collection, + float $scaleModifier = 1.0 + ) : void { $source = new ObjFileParser($path); $sourceMeshes = $source->getMeshes('pn'); - - $model = new LPModel(basename($path)); + $combinedModel = new LPModel($modelName); + $combinedModelBaseName = basename($modelName); foreach ($sourceMeshes as $sourceMesh) { + $srcMat = $sourceMesh->material; + $roughness = $this->shininessToRoughness($srcMat->shininess); $material = new LPMaterial( - $sourceMesh->material->name, - $sourceMesh->material->diffuse, - $sourceMesh->material->shininess + $srcMat->name, + $srcMat->diffuse, + $roughness, + metallic: ($srcMat->illuminationModel === 3) ? 1.0 : 0.0 ); - // this is EXTREMELY inefficient, but it's the easiest way to get it working - // for now until we add some sort of buffer merging into PHP-GLFW or something - for ($i = 0; $i < $sourceMesh->vertices->size(); $i+=6) { - $buffer->push($sourceMesh->vertices[$i + 0] * $scaleModifier); - $buffer->push($sourceMesh->vertices[$i + 1] * $scaleModifier); - $buffer->push($sourceMesh->vertices[$i + 2] * $scaleModifier); - - $buffer->push($sourceMesh->vertices[$i + 3]); - $buffer->push($sourceMesh->vertices[$i + 4]); - $buffer->push($sourceMesh->vertices[$i + 5]); + // set emissive if non zero + if ($srcMat->emissive && $this->vec3IsNonZero($srcMat->emissive)) { + $material->emissive = $srcMat->emissive; } - $mesh = new LPMesh( - $material, - $vb, - $vertexOffset, - $sourceMesh->vertices->size() / 6, - new AABB( - $sourceMesh->aabbMin * $scaleModifier, - $sourceMesh->aabbMax * $scaleModifier, - ) - ); - - $vertexOffset += $sourceMesh->vertices->size() / 6; - - $model->meshes[] = $mesh; + $mesh = $this->importMesh($sourceMesh->vertices, $material, $vb, $scaleModifier); + $combinedModel->meshes[] = $mesh; } // dont forget to recalculate the AABB - $model->recalculateAABB(); + $combinedModel->recalculateAABB(); + + // and push to collection + $collection->add($combinedModel); + + // we have to do the same for all sub objects in the file + // so we could use individual objects inside of a model as separate models + // this will cause duplicated vertex data in the buffer, but makes access soo much easier + foreach($source->groups as $objectGroup) + { + $objectModel = new LPModel($modelName . '.' . $objectGroup->name); - return $model; + // skip duplicates in sub objects, or objects of the same name as the combined model + if ($collection->has($objectModel->name) || $combinedModelBaseName === $objectGroup->name) { + continue; + } + + $sourceMeshes = $source->getMeshes('pn', $objectGroup); + + foreach ($sourceMeshes as $sourceMesh) { + $srcMat = $sourceMesh->material; + $roughness = $this->shininessToRoughness($srcMat->shininess); + + $material = new LPMaterial( + $srcMat->name, + $srcMat->diffuse, + $roughness, + metallic: ($srcMat->illuminationModel === 3) ? 1.0 : 0.0 + ); + + // set emissive if non zero + if ($srcMat->emissive && $this->vec3IsNonZero($srcMat->emissive)) { + $material->emissive = $srcMat->emissive; + } + + $mesh = $this->importMesh($sourceMesh->vertices, $material, $vb, $scaleModifier); + $objectModel->meshes[] = $mesh; + } + + // dont forget to recalculate the AABB + $objectModel->recalculateAABB(); + + // and push to collection + $collection->add($objectModel); + } } /** * Loads all object files in a given directory and returns them in assoc array * - * @param string $directory The directory to load the files from - * @param LPModelCollection $collection The collection to store the models in - * @param float $scaleModifier The scale modifier to apply to the models while loading + * @param string $directory The directory to load the files from + * @param LPModelCollection $collection The collection to store the models in + * @param float $scaleModifier The scale modifier to apply to the models while loading + * @param LPVertexBuffer|null $vertexBuffer The vertex buffer to use * @return void */ - public function loadAllInDirectory(string $directory, LPModelCollection $collection, float $scaleModifier = 1.0): void + public function loadAllInDirectory(string $directory, LPModelCollection $collection, float $scaleModifier = 1.0, ?LPVertexBuffer $vertexBuffer = null): void { if (!is_dir($directory)) { throw new VISUException('Cannot load objects, directory does not exist: ' . $directory); } - // create a vertex buffer to store all the objects in - $vb = new LPVertexBuffer($this->gl); - $vertices = new FloatBuffer(); - $indexOffset = 0; - - $files = scandir($directory) ?: []; - - foreach ($files as $file) { - if (substr($file, -4) === '.obj') { - $collection->add($this->loadFile($directory . '/' . $file, $vertices, $vb, $indexOffset, $scaleModifier)); + // create a vertex buffer to store all the objects in, or use the provided one + $vb = $vertexBuffer ?? new LPVertexBuffer($this->gl); + + // use recursive iterator to scan all .obj files + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS) + ); + + foreach ($iterator as $file) { + if ($file->getExtension() === 'obj') { + // calculate relative path for model name + $basePath = realpath($directory); + $filePath = realpath($file->getPathname()); + + if ($basePath === false || $filePath === false) { + continue; // skip if paths cannot be resolved + } + + $relativePath = substr($filePath, strlen($basePath) + 1); + $modelName = substr($relativePath, 0, -4); // remove .obj extension + + $this->loadFile( + $file->getPathname(), + $modelName, + $vb, + $collection, + $scaleModifier + ); } } // upload the data to the GPU - $vb->uploadData($vertices); + $vb->upload(); } } diff --git a/src/System/VISULowPoly/LPRenderingSystem.php b/src/System/VISULowPoly/LPRenderingSystem.php index 330a01c..6420940 100644 --- a/src/System/VISULowPoly/LPRenderingSystem.php +++ b/src/System/VISULowPoly/LPRenderingSystem.php @@ -2,9 +2,12 @@ namespace VISU\System\VISULowPoly; +use GL\Buffer\FloatBuffer; use GL\Math\{GLM, Quat, Vec2, Vec3}; +use GL\Rendering\DrawCallAssembler; use VISU\Component\DirectionalLightComponent; -use VISU\Component\VISULowPoly\DynamicRenderableModel; +use VISU\Component\VISULowPoly\LPDynamicModel; +use VISU\Component\VISULowPoly\LPStaticModel; use VISU\D3D; use VISU\ECS\EntitiesInterface; use VISU\ECS\Picker\DevEntityPickerRenderInterface; @@ -30,6 +33,8 @@ use VISU\Graphics\Rendering\Resource\RenderTargetResource; use VISU\Graphics\ShaderCollection; use VISU\Graphics\ShaderProgram; +use VISU\OS\Logger; +use VISU\Quickstart\Render\QuickstartDebugMetricsOverlay; class LPRenderingSystem implements SystemInterface, DevEntityPickerRenderInterface { @@ -43,6 +48,9 @@ class LPRenderingSystem implements SystemInterface, DevEntityPickerRenderInterfa const DEBUG_MODE_DEPTH = 4; const DEBUG_MODE_ALBEDO = 5; const DEBUG_MODE_SSAO = 6; + const DEBUG_MODE_METALLIC = 7; + const DEBUG_MODE_ROUGHNESS = 8; + const DEBUG_MODE_EMISSIVE = 9; public int $debugMode = self::DEBUG_MODE_NONE; /** @@ -76,9 +84,41 @@ class LPRenderingSystem implements SystemInterface, DevEntityPickerRenderInterfa * Shader programs */ private ShaderProgram $objectShader; + private ShaderProgram $objectInstancedShader; private ShaderProgram $devPickingShader; private ShaderProgram $lightingShader; + /** + * onAttach callback handle "LPStaticModel" + */ + private int $onAttachStaticModelHandle; + + /** + * onDetach callback handle "LPStaticModel" + */ + private int $onDetachStaticModelHandle; + + /** + * onAttach callback handle "LPDynamicModel" + */ + private int $onAttachDynamicModelHandle; + + + /** + * The static geomentry draw call assembler + */ + private DrawCallAssembler $staticGeometryDCA; + + /** + * Indicates whether the static geometry DCA needs to be rebuilt + */ + private bool $staticGeometryIsDirty = true; + + /** + * The dynamic geomentry draw call assembler + */ + private DrawCallAssembler $dynamicGeometryDCA; + /** * Constructor */ @@ -94,6 +134,7 @@ public function __construct( // load the required shaders $this->objectShader = $this->shaders->get('visu/lowpoly/deferred_single_mesh'); + $this->objectInstancedShader = $this->shaders->get('visu/lowpoly/deferred_instanced_mesh'); $this->devPickingShader = $this->shaders->get('visu/lowpoly/devpicking'); $this->lightingShader = $this->shaders->get('visu/lowpoly/deferred_lightpass'); } @@ -116,11 +157,23 @@ public function addGeometryRenderer(GBufferGeometryPassInterface $renderer) : vo */ public function register(EntitiesInterface $entities) : void { - $entities->registerComponent(DynamicRenderableModel::class); + $entities->registerComponent(LPDynamicModel::class); + $entities->registerComponent(LPStaticModel::class); $entities->registerComponent(Transform::class); // create single directional light $entities->setSingleton(new DirectionalLightComponent); + + // register attach/detach handlers for static models + $this->onAttachStaticModelHandle = $entities->onAttach(LPStaticModel::class, [$this, 'handleAttachStaticModel']); + $this->onDetachStaticModelHandle = $entities->onDetach(LPStaticModel::class, [$this, 'handleDetachStaticModel']); + $this->onAttachDynamicModelHandle = $entities->onAttach(LPDynamicModel::class, [$this, 'handleAttachDynamicModel']); + + // construct the draw call assembler for static geometry + $this->staticGeometryDCA = new DrawCallAssembler(); + + // construct the draw call assembler for dynamic geometry + $this->dynamicGeometryDCA = new DrawCallAssembler(); } /** @@ -130,6 +183,102 @@ public function register(EntitiesInterface $entities) : void */ public function unregister(EntitiesInterface $entities) : void { + $entities->releaseOnAttach($this->onAttachStaticModelHandle); + $entities->releaseOnDetach($this->onDetachStaticModelHandle); + } + + private function registerModelWithDCA(LPModel $model, DrawCallAssembler $dca, ?int &$meshDcaHandle) : void + { + // we render the lowpoly models as a single mesh instead of the multiple sub-meshes + // which were used to color them differently. We pack that information into the vertex data. + // so register all the meshes as one mesh if not already done + if (is_null($meshDcaHandle)) { + + // we load the sub-meshes in sequence into the same VBO, so we really + // can just calculate the offsets and counts here + $startOffset = 0; + $vertexCount = 0; + $fistMesh = null; + foreach($model->meshes as $mesh) { + if ($vertexCount === 0) { + $startOffset = $mesh->vertexOffset; + $fistMesh = $mesh; + } + + $vertexCount += $mesh->vertexCount; + } + + if (is_null($fistMesh)) { + throw new LPException('LPRenderingSystem - Cannot register model, no meshes found: ' . $model->name); + } + + $meshDcaHandle = $dca->registerMesh( + vao: $fistMesh->vertexBuffer->getVertexArrayId(), + vertexOffset: $startOffset, + vertexCount: $vertexCount, + indexOffset: 0, + indexCount: 0, + aabbMin: $model->aabb->min, + aabbMax: $model->aabb->max, + materialHint: 0, + primitive: GL_TRIANGLES + ); + + $dca->bindTransformBuffer($fistMesh->vertexBuffer->getVertexArrayId(), 6); + } + } + + /** + * Handles the attachment of a static model + */ + public function handleAttachStaticModel(EntitiesInterface $entities, int $entity, LPStaticModel $component) : void + { + $this->staticGeometryIsDirty = true; + + // validate the model is present + if (!isset($this->modelCollection->models[$component->modelIdentifier])) { + Logger::warn('LPRenderingSystem - Model not found: ' . $component->modelIdentifier); + return; + } + + $model = $this->modelCollection->models[$component->modelIdentifier]; + + $this->registerModelWithDCA($model, $this->staticGeometryDCA, $model->staticDCAHandle); + + if (!$transform = $entities->tryGet($entity, Transform::class)) { + throw new LPException('LPRenderingSystem - Please attach a Transform component first, entity: ' . $entity); + } + + // submit the instance + $this->staticGeometryDCA->submit( + meshHandle: $model->staticDCAHandle, + transform: $transform->getWorldMatrix($entities), + materialId: 1, // material data is packed into the vertex buffer. + ); + } + + /** + * Handles the detachment of a static model + */ + public function handleDetachStaticModel(EntitiesInterface $entities, int $entity, LPStaticModel $component) : void + { + $this->staticGeometryIsDirty = true; + } + + /** + * Handles the attachment of a dynamic model + */ + public function handleAttachDynamicModel(EntitiesInterface $entities, int $entity, LPDynamicModel $component) : void + { + // validate the model is present + if (!isset($this->modelCollection->models[$component->modelIdentifier])) { + Logger::warn('LPRenderingSystem - Model not found: ' . $component->modelIdentifier); + return; + } + + $model = $this->modelCollection->models[$component->modelIdentifier]; + + $this->registerModelWithDCA($model, $this->dynamicGeometryDCA, $model->dynamicDCAHandle); } /** @@ -139,11 +288,6 @@ public function unregister(EntitiesInterface $entities) : void */ public function update(EntitiesInterface $entities) : void { - // all dynamic renderables need an up to date aabb - foreach($entities->view(DynamicRenderableModel::class) as $entity => $renerable) - { - - } } /** @@ -180,44 +324,100 @@ public function render(EntitiesInterface $entities, RenderContext $context) : vo $renderer->renderToGBuffer($entities, $context, $gbuffer); } - // create a simple render pass for our models - // just to test if everything works @todo move this into seperate system + $renderMetrics = [ + 'static_draw_calls' => 0, + 'static_instances' => 0, + 'dynamic_draw_calls' => 0, + 'dynamic_instances' => 0, + ]; + + /** + * Static Geometry Render Pass + */ $context->pipeline->addPass(new CallbackPass( - 'LPModels', + 'LPStaticMesh', // setup function(RenderPass $pass, RenderPipeline $pipeline, PipelineContainer $data) use($gbuffer) { $pipeline->writes($pass, $gbuffer->renderTarget); }, // execute - function(PipelineContainer $data, PipelineResources $resources) use($entities) + function(PipelineContainer $data, PipelineResources $resources) use(&$renderMetrics) { $cameraData = $data->get(CameraData::class); - $this->objectShader->use(); - $this->objectShader->setUniformMatrix4f('projection', false, $cameraData->projection); - $this->objectShader->setUniformMatrix4f('view', false, $cameraData->view); + $this->objectInstancedShader->use(); + $this->objectInstancedShader->setUniformMatrix4f('projection', false, $cameraData->projection); + $this->objectInstancedShader->setUniformMatrix4f('view', false, $cameraData->view); glEnable(GL_DEPTH_TEST); - foreach($entities->view(DynamicRenderableModel::class) as $entity => $renderable) - { - $transform = $entities->get($entity, Transform::class); + $this->staticGeometryDCA->setCameraData( + $cameraData->renderCamera->transform->position, + $cameraData->view, + $cameraData->projection + ); - $this->objectShader->setUniformMatrix4f('model', false, $transform->getWorldMatrix($entities)); + $this->staticGeometryDCA->execute(function(int $meshHandle, int $materialId, int $instanceOffset, int $instanceCount, int $flags) use(&$renderMetrics) { + $renderMetrics['static_draw_calls'] += 1; + $renderMetrics['static_instances'] += $instanceCount; + }); + } + )); + /** + * Dynamic Geometry Render Pass + */ + $context->pipeline->addPass(new CallbackPass( + 'LPDynamicMesh', + // setup + function(RenderPass $pass, RenderPipeline $pipeline, PipelineContainer $data) use($gbuffer) + { + $pipeline->writes($pass, $gbuffer->renderTarget); + }, + // execute + function(PipelineContainer $data, PipelineResources $resources) use($entities, &$renderMetrics) + { + $cameraData = $data->get(CameraData::class); + + $this->dynamicGeometryDCA->clearInstances(); + + // fetch all dynamic models and submit their instances + /** @var iterable $dynamicView */ + $dynamicView = $entities->viewWith(LPDynamicModel::class, Transform::class); + foreach ($dynamicView as $entity => [$renderable, $transform]) { if (!isset($this->modelCollection->models[$renderable->modelIdentifier])) { - throw new \Exception('Model not found: ' . $renderable->modelIdentifier); + continue; } - // render each mesh - foreach($this->modelCollection->models[$renderable->modelIdentifier]->meshes as $mesh) - { - $mesh->vertexBuffer->bind(); - $this->objectShader->setUniformVec3('color', $mesh->material->color); - - glDrawArrays(GL_TRIANGLES, $mesh->vertexOffset, $mesh->vertexCount); + $model = $this->modelCollection->models[$renderable->modelIdentifier]; + + // unregistered model + if (is_null($model->dynamicDCAHandle)) { + continue; } + + $this->dynamicGeometryDCA->submit( + meshHandle: $model->dynamicDCAHandle, + transform: $transform->getWorldMatrix($entities), + materialId: 1, // material data is packed into the vertex buffer. + ); } + + $this->objectInstancedShader->use(); + $this->objectInstancedShader->setUniformMatrix4f('projection', false, $cameraData->projection); + $this->objectInstancedShader->setUniformMatrix4f('view', false, $cameraData->view); + glEnable(GL_DEPTH_TEST); + + $this->dynamicGeometryDCA->setCameraData( + $cameraData->renderCamera->transform->position, + $cameraData->view, + $cameraData->projection + ); + + $this->dynamicGeometryDCA->execute(function(int $meshHandle, int $materialId, int $instanceOffset, int $instanceCount, int $flags) use(&$renderMetrics) { + $renderMetrics['dynamic_draw_calls'] += 1; + $renderMetrics['dynamic_instances'] += $instanceCount; + }); } )); @@ -243,6 +443,18 @@ function(PipelineContainer $data, PipelineResources $resources) use($entities) $this->fullscreenRenderer->attachPass($context->pipeline, $this->currentRenderTargetRes, $gbuffer->albedoTexture); return; } + elseif ($this->debugMode === self::DEBUG_MODE_METALLIC) { + $this->fullscreenRenderer->attachPass($context->pipeline, $this->currentRenderTargetRes, $gbuffer->metallicTexture); + return; + } + elseif ($this->debugMode === self::DEBUG_MODE_ROUGHNESS) { + $this->fullscreenRenderer->attachPass($context->pipeline, $this->currentRenderTargetRes, $gbuffer->roughnessTexture); + return; + } + elseif ($this->debugMode === self::DEBUG_MODE_EMISSIVE) { + $this->fullscreenRenderer->attachPass($context->pipeline, $this->currentRenderTargetRes, $gbuffer->emissiveTexture); + return; + } // make ssao pass $this->ssaoRenderer->attachPass($context->pipeline); @@ -269,6 +481,13 @@ function(PipelineContainer $data, PipelineResources $resources) use($entities) $this->currentRenderTargetRes = null; } + /** + * Renders the static geometry ("LPStaticModel" components) + */ + public function renderStaticGeometry(EntitiesInterface $entities, RenderContext $context, GBufferPassData $gbufferData) : void + { + + } /** * Renders all entites to a picking framebuffer @@ -285,7 +504,7 @@ public function renderEntityIdsForPicking(EntitiesInterface $entities, CameraDat $this->devPickingShader->setUniformMat4('projection', false, $cameraData->projection); $this->devPickingShader->setUniformMat4('view', false, $cameraData->view); - foreach($entities->view(DynamicRenderableModel::class) as $entity => $renderable) + foreach($entities->view(LPDynamicModel::class) as $entity => $renderable) { $transform = $entities->get($entity, Transform::class); $this->devPickingShader->setUniformMatrix4f('model', false, $transform->getWorldMatrix($entities)); diff --git a/src/System/VISULowPoly/LPVertexBuffer.php b/src/System/VISULowPoly/LPVertexBuffer.php index c2318c6..577a15e 100644 --- a/src/System/VISULowPoly/LPVertexBuffer.php +++ b/src/System/VISULowPoly/LPVertexBuffer.php @@ -5,22 +5,38 @@ use GL\Buffer\FloatBuffer; use VISU\Graphics\GLState; +/** + * A vertex buffer designed for the VISU low poly rendering pipeline. + * + * We bake the material information into the vertex buffer for simplicity, + * if you require sepcialized materials, handle them yourself. + * + * This vertex buffer follows a static layout of: + * - [3: pos, 3: norm, 3: color, 1: shinyness] = stride of 10 floats per vertex + */ class LPVertexBuffer { /** * The vertex array object from GL - * - * @var int */ private int $vertexArray; /** * The vertex buffer object from GL - * - * @var int */ private int $vertexBuffer; + /** + * The internal buffer holding the vertex data before upload + */ + private FloatBuffer $buffer; + + /** + * The size of a single vertex in floats and bytes + */ + public const STRIDE = 14; + public const STRIDE_BYTES = 14 * GL_SIZEOF_FLOAT; + /** * Constructor * @@ -33,6 +49,7 @@ public function __construct( { $this->vertexArray = 0; $this->vertexBuffer = 0; + $this->buffer = new FloatBuffer(); glGenVertexArrays(1, $this->vertexArray); glGenBuffers(1, $this->vertexBuffer); @@ -41,20 +58,79 @@ public function __construct( // declare the vertex attributes // position, normal - glVertexAttribPointer(0, 3, GL_FLOAT, false, 6 * GL_SIZEOF_FLOAT, 0); + glVertexAttribPointer(0, 3, GL_FLOAT, false, self::STRIDE_BYTES, 0); glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 3, GL_FLOAT, false, 6 * GL_SIZEOF_FLOAT, 3 * GL_SIZEOF_FLOAT); + glVertexAttribPointer(1, 3, GL_FLOAT, false, self::STRIDE_BYTES, 3 * GL_SIZEOF_FLOAT); glEnableVertexAttribArray(1); + // color + glVertexAttribPointer(2, 3, GL_FLOAT, false, self::STRIDE_BYTES, 6 * GL_SIZEOF_FLOAT); + glEnableVertexAttribArray(2); + // roughness + glVertexAttribPointer(3, 1, GL_FLOAT, false, self::STRIDE_BYTES, 9 * GL_SIZEOF_FLOAT); + glEnableVertexAttribArray(3); + // metallic + glVertexAttribPointer(4, 1, GL_FLOAT, false, self::STRIDE_BYTES, 10 * GL_SIZEOF_FLOAT); + glEnableVertexAttribArray(4); + // emissive + glVertexAttribPointer(5, 3, GL_FLOAT, false, self::STRIDE_BYTES, 11 * GL_SIZEOF_FLOAT); + glEnableVertexAttribArray(5); + } + + /** + * Destructor + */ + public function __destruct() + { + glDeleteVertexArrays(1, $this->vertexArray); + glDeleteBuffers(1, $this->vertexBuffer); + } + + /** + * Returns the vertex buffer object ID + */ + public function getVertexBufferId() : int + { + return $this->vertexBuffer; + } + + /** + * Returns the vertex array object ID + */ + public function getVertexArrayId() : int + { + return $this->vertexArray; + } + + /** + * Appends the given vertex data buffer to this vertex buffer's internal buffer + * + * @return int The offset (in vertices) where the appended data starts + */ + public function appennd(FloatBuffer $buffer) : int + { + // sanity check + if ($buffer->size() % self::STRIDE !== 0) { + throw new LPException(sprintf( + "Attempted to append vertex buffer with size %d which is not a multiple of the vertex stride %d", + $buffer->size(), + self::STRIDE + )); + } + + $this->buffer->append($buffer); + + // return the offset in vertices + return ($this->buffer->size() - $buffer->size()) / self::STRIDE; } /** - * Uploads the given data to the GPU + * Upload the internal buffer to the GPU */ - public function uploadData(FloatBuffer $buffer) : void + public function upload() : void { $this->state->bindVertexArray($this->vertexArray); $this->state->bindVertexArrayBuffer($this->vertexBuffer); - glBufferData(GL_ARRAY_BUFFER, $buffer, GL_STATIC_DRAW); + glBufferData(GL_ARRAY_BUFFER, $this->buffer, GL_STATIC_DRAW); } /** diff --git a/tests/ECS/EntityRegistryTest.php b/tests/ECS/EntityRegistryTest.php index 80a0d72..558c5e0 100644 --- a/tests/ECS/EntityRegistryTest.php +++ b/tests/ECS/EntityRegistryTest.php @@ -3,6 +3,7 @@ namespace App\Tests\ECS; use Exception; +use VISU\ECS\EntitiesInterface; use VISU\ECS\EntityRegistry; use VISU\ECS\Exception\EntityRegistryException; @@ -197,7 +198,7 @@ public function testOnAttachListener() : void $attachedComponents = []; // register an attach listener - $handle = $entities->onAttach(\Exception::class, function(int $entity, \Exception $component) use (&$attachedEntities, &$attachedComponents) { + $handle = $entities->onAttach(\Exception::class, function(EntitiesInterface $entities, int $entity, \Exception $component) use (&$attachedEntities, &$attachedComponents) { $attachedEntities[] = $entity; $attachedComponents[] = $component->getMessage(); }); @@ -225,7 +226,7 @@ public function testOnDetachListener() : void $detachedComponents = []; // register a detach listener - $handle = $entities->onDetach(\Exception::class, function(int $entity, \Exception $component) use (&$detachedEntities, &$detachedComponents) { + $handle = $entities->onDetach(\Exception::class, function(EntitiesInterface $entities, int $entity, \Exception $component) use (&$detachedEntities, &$detachedComponents) { $detachedEntities[] = $entity; $detachedComponents[] = $component->getMessage(); }); @@ -258,11 +259,11 @@ public function testOnDetachListenerWithDetachAll() : void $detachedErrors = []; // register detach listeners for both component types - $entities->onDetach(\Exception::class, function(int $entity, \Exception $component) use (&$detachedExceptions) { + $entities->onDetach(\Exception::class, function(EntitiesInterface $entities, int $entity, \Exception $component) use (&$detachedExceptions) { $detachedExceptions[] = $component->getMessage(); }); - $entities->onDetach(\Error::class, function(int $entity, \Error $component) use (&$detachedErrors) { + $entities->onDetach(\Error::class, function(EntitiesInterface $entities, int $entity, \Error $component) use (&$detachedErrors) { $detachedErrors[] = $component->getMessage(); }); @@ -287,7 +288,7 @@ public function testReleaseOnAttach() : void $attachedComponents = []; // register an attach listener - $handle = $entities->onAttach(\Exception::class, function(int $entity, \Exception $component) use (&$attachedComponents) { + $handle = $entities->onAttach(\Exception::class, function(EntitiesInterface $entities, int $entity, \Exception $component) use (&$attachedComponents) { $attachedComponents[] = $component->getMessage(); }); @@ -316,7 +317,7 @@ public function testReleaseOnDetach() : void $detachedComponents = []; // register a detach listener - $handle = $entities->onDetach(\Exception::class, function(int $entity, \Exception $component) use (&$detachedComponents) { + $handle = $entities->onDetach(\Exception::class, function(EntitiesInterface $entities, int $entity, \Exception $component) use (&$detachedComponents) { $detachedComponents[] = $component->getMessage(); }); @@ -350,11 +351,11 @@ public function testMultipleAttachListeners() : void $listener2Called = false; // register two different attach listeners - $entities->onAttach(\Exception::class, function(int $entity, \Exception $component) use (&$listener1Called) { + $entities->onAttach(\Exception::class, function(EntitiesInterface $entities, int $entity, \Exception $component) use (&$listener1Called) { $listener1Called = true; }); - $entities->onAttach(\Exception::class, function(int $entity, \Exception $component) use (&$listener2Called) { + $entities->onAttach(\Exception::class, function(EntitiesInterface $entities, int $entity, \Exception $component) use (&$listener2Called) { $listener2Called = true; }); @@ -376,11 +377,11 @@ public function testMultipleDetachListeners() : void $listener2Called = false; // register two different detach listeners - $entities->onDetach(\Exception::class, function(int $entity, \Exception $component) use (&$listener1Called) { + $entities->onDetach(\Exception::class, function(EntitiesInterface $entities, int $entity, \Exception $component) use (&$listener1Called) { $listener1Called = true; }); - $entities->onDetach(\Exception::class, function(int $entity, \Exception $component) use (&$listener2Called) { + $entities->onDetach(\Exception::class, function(EntitiesInterface $entities, int $entity, \Exception $component) use (&$listener2Called) { $listener2Called = true; }); @@ -404,7 +405,7 @@ public function testListenerWithEntityDestroy() : void $detachedComponents = []; // register a detach listener - $entities->onDetach(\Exception::class, function(int $entity, \Exception $component) use (&$detachedComponents) { + $entities->onDetach(\Exception::class, function(EntitiesInterface $entities, int $entity, \Exception $component) use (&$detachedComponents) { $detachedComponents[] = $component->getMessage(); }); diff --git a/visu.ctn b/visu.ctn index ab98e95..ff170f5 100644 --- a/visu.ctn +++ b/visu.ctn @@ -45,6 +45,9 @@ import app @visu.command.dump_signal_handlers: VISU\Command\SignalDumpCommand(@visu.dispatcher) = command: 'signals:dump' +@visu.command.hdri_cubemap: VISU\Command\HDRIToCubemapCommand + = command: 'tools:hdri_to_cubemap' + /** * Maker / CodeGenerator * From 34282433453d340e66044fb2bfe9a8cfbadac5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Do=CC=88ring?= <956212+mario-deluna@users.noreply.github.com> Date: Fri, 9 Jan 2026 16:12:39 +0100 Subject: [PATCH 3/9] Music player --- src/Audio/Exception/MusicPlayerException.php | 7 + src/Audio/MusicPlayer.php | 214 +++++++++++++++++++ src/Audio/MusicPlayerTrack.php | 57 +++++ 3 files changed, 278 insertions(+) create mode 100644 src/Audio/Exception/MusicPlayerException.php create mode 100644 src/Audio/MusicPlayer.php create mode 100644 src/Audio/MusicPlayerTrack.php diff --git a/src/Audio/Exception/MusicPlayerException.php b/src/Audio/Exception/MusicPlayerException.php new file mode 100644 index 0000000..b57d22d --- /dev/null +++ b/src/Audio/Exception/MusicPlayerException.php @@ -0,0 +1,7 @@ + + */ + private array $tracks = []; + + /** + * Currently playing track + */ + private ?MusicPlayerTrack $currentTrack = null; + + /** + * Next track to be played (after the current one ends) + */ + private ?MusicPlayerTrack $nextTrack = null; + + /** + * Boolean if in the process of switching tracks + */ + private bool $isSwitchingTracks = false; + + /** + * Constructor + */ + public function __construct( + private Engine $engine + ) + { + // sanity check that we have a main event loop as we rely on it + if (!EventLoop::hasMain()) { + throw new MusicPlayerException('MusicPlayer requires a main event loop to be registered before instantiation'); + } + } + + /** + * Returns the active audio engine + */ + public function getEngine() : Engine + { + return $this->engine; + } + + /** + * Instantly loads and plays the track with the given name + */ + public function play(string $name) : void + { + if (!isset($this->tracks[$name])) { + throw new MusicPlayerException("Track with ID '$name' does not exist in music player"); + } + + $track = $this->tracks[$name]; + $track->load($this->engine); + + // stop current track if playing + if ($this->currentTrack !== null) { + $this->currentTrack->getSound()?->stop(); + } + + // play the new track + $track->getSound()?->play(); + $this->currentTrack = $track; + } + + /** + * Switches to the track with the given name + * + * Will fade the current track out and fade the new track in + */ + public function switchTo(string $name) : void + { + if (!isset($this->tracks[$name])) { + throw new MusicPlayerException("Track with ID '$name' does not exist in music player"); + } + + // if nothing is playing yet just play the track + if (!$this->currentTrack) { + $this->play($name); + return; + } + + if ($this->isSwitchingTracks) { + Logger::warn(sprintf('[MusicPlayer] Already switching tracks, ignoring request to switch to \'%s\'', $name)); + return; + } + + $this->isSwitchingTracks = true; + + $this->nextTrack = $this->tracks[$name]; + $this->nextTrack->load($this->engine); + + if ($currentSound = $this->currentTrack->getSound()) { + $currentSound->fadeOut($this->fadeDuration); + + if ($nextSound = $this->nextTrack->getSound()) { + $nextSound->play(); + $nextSound->setFade(0, 1, $this->fadeDuration); + } + + EventLoop::defer((int)($this->fadeDuration * 1000), function() { + $this->currentTrack?->unload(); + $this->currentTrack = $this->nextTrack; + $this->nextTrack = null; + + $this->isSwitchingTracks = false; + }); + } + } + + /** + * Converts a filepath to a track name + * + * We consider the track name the basic filename without extension + */ + private function filepathToTrackName(string $path, ?string $basePath = null) : string + { + if (is_null($basePath)) { + $basename = basename($path); + } else { + $basename = ltrim(str_replace($basePath, '', $path), '/\\'); + } + $dotPos = strrpos($basename, '.'); + if ($dotPos !== false) { + return substr($basename, 0, $dotPos); + } + + return $basename; + } + + /** + * Binds a given track to music player + */ + public function bindTrack(string $name, MusicPlayerTrack $track) : void + { + // double check the file is loadable and exists + if (!file_exists($track->path) || !is_readable($track->path)) { + throw new MusicPlayerException("Audio file '{$track->path}' does not exist or is not readable"); + } + + if (isset($this->tracks[$name])) { + throw new MusicPlayerException("Track with ID '$name' already exists in music player"); + } + + $this->tracks[$name] = $track; + } + + /** + * Adds a track to the music player using the given path + */ + public function addTrack(string $path, ?string $basePath = null) : MusicPlayerTrack + { + $track = new MusicPlayerTrack($path); + $this->bindTrack($this->filepathToTrackName($path, $basePath), $track); + return $track; + } + + /** + * Recursively scans a directory and adds all audio files as tracks + * + * @param array $extensions Supported audio file extensions + * @return array + */ + public function addTracksFromDirectory(string $directory, array $extensions = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a']) : array + { + if (!is_dir($directory)) { + throw new MusicPlayerException("Directory '$directory' does not exist or is not a directory"); + } + + if (!is_readable($directory)) { + throw new MusicPlayerException("Directory '$directory' is not readable"); + } + + $addedTracks = []; + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + $extensions = array_map('mb_strtolower', $extensions); + + foreach ($iterator as $file) + { + /** @var \SplFileInfo $file */ + if ($file->isFile()) + { + $extension = mb_strtolower($file->getExtension()); + + // check if the file has a supported audio extension + if (in_array($extension, $extensions)) { + $this->addTrack($file->getRealPath(), $directory); + } + } + } + + return $addedTracks; + } +} \ No newline at end of file diff --git a/src/Audio/MusicPlayerTrack.php b/src/Audio/MusicPlayerTrack.php new file mode 100644 index 0000000..7bf12a5 --- /dev/null +++ b/src/Audio/MusicPlayerTrack.php @@ -0,0 +1,57 @@ +sound; + } + + /** + * Loads the sound file from disk + */ + public function load(Engine $engine) : void + { + if ($this->sound === null) { + Logger::info(sprintf('[MusicPlayer] Loading track \'%s\'', basename($this->path))); + $this->sound = $engine->soundFromDisk($this->path); + } + } + + /** + * Unloads the sound from memory + */ + public function unload() : void + { + if ($this->sound !== null) { + Logger::info(sprintf('[MusicPlayer] Unloading track \'%s\'', basename($this->path))); + $this->sound = null; + } + } +} \ No newline at end of file From d28c41056a815c4a13d88711d9319292a5ecb6ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Do=CC=88ring?= <956212+mario-deluna@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:11:24 +0200 Subject: [PATCH 4/9] crap ton of work on IBL & PBR --- examples/rendering/cubemap_skybox_demo.php | 112 + examples/rendering/low_poly_pipeline.php | 101 - .../rendering/low_poly_pipeline_basic.php | 22 +- .../low_poly_pipeline_with_cubemap.php | 351 + resources/model/visu/sphere.obj | 9943 +++++++++++++++++ resources/shader/include/visu/constants.glsl | 10 + .../shader/include/visu/cubemap_vert.glsl | 13 + .../shader/include/visu/functions/brdf.glsl | 134 + .../include/visu/functions/gamma_corr.glsl | 19 + .../visu/functions/importance_sampling.glsl | 49 + .../include/visu/functions/tone_mapping.glsl | 59 + .../shader/include/visu/gbuffer_layout.glsl | 2 +- .../shader/include/visu/gbuffer_uniform.glsl | 43 +- resources/shader/include/visu/pbr/shade.glsl | 26 + .../shader/include/visu/pbr/surface.glsl | 42 + .../visu/lowpoly/deferred_lightpass.frag.glsl | 279 +- .../visu/lowpoly/deferred_lightpass.vert.glsl | 4 +- .../visu/pbr_v1/bake_brdf_lut.frag.glsl | 83 + .../visu/pbr_v1/bake_brdf_lut.vert.glsl | 3 + .../visu/pbr_v1/bake_irradiance.frag.glsl | 39 + .../visu/pbr_v1/bake_irradiance.vert.glsl | 3 + .../pbr_v1/bake_prefiltered_env.frag.glsl | 109 + .../pbr_v1/bake_prefiltered_env.vert.glsl | 3 + resources/shader/visu/ssao.frag.glsl | 20 +- src/Graphics/Cubemap.php | 121 +- src/Graphics/Framebuffer.php | 16 + src/Graphics/GLState.php | 47 +- src/Graphics/HDRIToCubemap.php | 15 +- src/Graphics/RenderTarget.php | 43 + src/Graphics/Rendering/Pass/CubemapPass.php | 9 + .../Rendering/Pass/DeferredLightPass.php | 134 +- .../Pass/DeferredLightPassPermutation.php | 52 + src/Graphics/Rendering/Pass/IBLCache.php | 64 + .../Rendering/Pass/IBLPrecomputeData.php | 30 + .../Rendering/Pass/IBLPrecomputePass.php | 198 + src/Graphics/Rendering/PipelineResources.php | 110 +- src/Graphics/Rendering/RenderPipeline.php | 35 + .../Rendering/Renderer/CubemapRenderer.php | 24 +- .../Rendering/Renderer/Debug3DRenderer.php | 2 +- .../Rendering/Renderer/SSAORenderer.php | 3 - .../Resource/RenderTargetResource.php | 2 +- src/Graphics/ShaderCollection.php | 147 +- src/Graphics/ShaderProgram.php | 32 +- src/Graphics/Texture.php | 19 +- src/System/VISULowPoly/LPRenderingSystem.php | 118 +- tests/Graphics/ShaderPermutationTest.php | 113 + tests/resources/shaders/triangle.frag.glsl | 2 + tests/resources/shaders/triangle.vert.glsl | 4 + 48 files changed, 12423 insertions(+), 386 deletions(-) create mode 100644 examples/rendering/cubemap_skybox_demo.php delete mode 100644 examples/rendering/low_poly_pipeline.php create mode 100644 examples/rendering/low_poly_pipeline_with_cubemap.php create mode 100644 resources/model/visu/sphere.obj create mode 100644 resources/shader/include/visu/constants.glsl create mode 100644 resources/shader/include/visu/cubemap_vert.glsl create mode 100644 resources/shader/include/visu/functions/brdf.glsl create mode 100644 resources/shader/include/visu/functions/gamma_corr.glsl create mode 100644 resources/shader/include/visu/functions/importance_sampling.glsl create mode 100644 resources/shader/include/visu/functions/tone_mapping.glsl create mode 100644 resources/shader/include/visu/pbr/shade.glsl create mode 100644 resources/shader/include/visu/pbr/surface.glsl create mode 100644 resources/shader/visu/pbr_v1/bake_brdf_lut.frag.glsl create mode 100644 resources/shader/visu/pbr_v1/bake_brdf_lut.vert.glsl create mode 100644 resources/shader/visu/pbr_v1/bake_irradiance.frag.glsl create mode 100644 resources/shader/visu/pbr_v1/bake_irradiance.vert.glsl create mode 100644 resources/shader/visu/pbr_v1/bake_prefiltered_env.frag.glsl create mode 100644 resources/shader/visu/pbr_v1/bake_prefiltered_env.vert.glsl create mode 100644 src/Graphics/Rendering/Pass/DeferredLightPassPermutation.php create mode 100644 src/Graphics/Rendering/Pass/IBLCache.php create mode 100644 src/Graphics/Rendering/Pass/IBLPrecomputeData.php create mode 100644 src/Graphics/Rendering/Pass/IBLPrecomputePass.php create mode 100644 tests/Graphics/ShaderPermutationTest.php diff --git a/examples/rendering/cubemap_skybox_demo.php b/examples/rendering/cubemap_skybox_demo.php new file mode 100644 index 0000000..22f77fe --- /dev/null +++ b/examples/rendering/cubemap_skybox_demo.php @@ -0,0 +1,112 @@ +data->get(CameraData::class); + + // import cubemap into the render pipeline + $cubemapRes = $context->pipeline->importCubemap('skybox_cubemap', $this->cubemap); + + // add the skybox pass (pass reads camera data internally) + $this->cubemapRenderer->attachSkyboxPass( + $context->pipeline, + $renderTarget, + $cubemapRes, + ); + } +} + +$quickstart = new Quickstart(function(QuickstartOptions $app) +{ + $app->appClass = CubemapDemoApp::class; + + $app->ready = function(QuickstartApp $app) { + /** @var CubemapDemoApp $app */ + + // define path to HDRI file - change this to point to your HDRI file + $hdriPath = '/Users/mariodoring/Downloads/cedar_bridge_sunset_1_4k.hdr'; + + // create HDRI to cubemap converter + $converter = new HDRIToCubemap($app->gl); + + // convert HDRI to cubemap (512x512 faces) + $app->cubemap = $converter->convert($hdriPath, 1024); + + // create the cubemap renderer + $app->cubemapRenderer = new CubemapRenderer($app->gl); + + // create camera system for 3D navigation + $app->cameraSystem = new VISUCameraSystem($app->input, $app->dispatcher); + + // register the camera system + $app->bindSystems([$app->cameraSystem]); + }; + + $app->initializeScene = function(QuickstartApp $app) { + /** @var CubemapDemoApp $app */ + + // spawn a flying camera at origin + $app->cameraSystem->spawnDefaultFlyingCamera($app->entities, new Vec3(0.0, 0.0, 0.0)); + }; + + $app->update = function(QuickstartApp $app) { + /** @var CubemapDemoApp $app */ + + // update the camera system + $app->updateSystem($app->cameraSystem); + }; + + $app->render = function(QuickstartApp $app, RenderContext $context, RenderTargetResource $target) { + /** @var CubemapDemoApp $app */ + + // render the camera system (this sets up CameraData in the context) + $app->renderSystem($app->cameraSystem, $context); + }; + +}); + +$quickstart->run(); \ No newline at end of file diff --git a/examples/rendering/low_poly_pipeline.php b/examples/rendering/low_poly_pipeline.php deleted file mode 100644 index 59bde1a..0000000 --- a/examples/rendering/low_poly_pipeline.php +++ /dev/null @@ -1,101 +0,0 @@ -container = $container; - $app->ready = function(QuickstartApp $app) use(&$state) - { - // create a model collection and load - $state->models = new LPModelCollection(); - $state->renderingSystem = new LPRenderingSystem($app->gl, $app->shaders, $state->models); - - // load the VISU models coming with the engine - $loader = new LPObjLoader($app->gl); - $loader->loadAllInDirectory(VISU_PATH_FRAMEWORK_RESOURCES . '/model/visu', $state->models); - - // to render 3D we need a camera - $state->cameraSystem = new VISUCameraSystem($app->input, $app->dispatcher); - - // register the rendering system - $app->bindSystems([ - $state->renderingSystem, - $state->cameraSystem - ]); - }; - - // Initalize the scene - // -------------------------------------------------------------------- - $app->initializeScene = function(QuickstartApp $app) use(&$state) - { - $state->cameraSystem->spawnDefaultFlyingCamera($app->entities, new Vec3(0.0, 0.0, 2.0)); - - // spawn a visu model in the middle - $logoEntity = $app->entities->create(); - $app->entities->attach($logoEntity, new DynamicRenderableModel('visu_logo.obj')); - $transform = $app->entities->attach($logoEntity, new Transform()); - $transform->orientation->rotate(GLM::radians(90.0), new Vec3(1.0, 0.0, 0.0)); - }; - - $app->update = function(QuickstartApp $app) use(&$state) - { - $app->updateSystem($state->cameraSystem); - - // rotate the logo - $model = $app->entities->firstWith(DynamicRenderableModel::class); - $transform = $app->entities->get($model, Transform::class); - $transform->orientation->rotate(GLM::radians(1.0), new Vec3(0.0, 0.0, 1.0)); - $transform->markDirty(); - }; - - $app->render = function(QuickstartApp $app, RenderContext $context, RenderTargetResource $target) use(&$state) - { - // make sure to tell the low poly rendering system which render target we are using - $state->renderingSystem->setRenderTarget($target); - - $app->renderSystem($state->cameraSystem, $context); - $app->renderSystem($state->renderingSystem, $context); - - // $ssaoData = $context->data->get(SSAOData::class); - // $quickstartPassData = $context->data->get(QuickstartPassData::class); - - // $quickstartPassData->outputTexture = $ssaoData->ssaoTexture; - }; -}); - -$quickstart->run(); diff --git a/examples/rendering/low_poly_pipeline_basic.php b/examples/rendering/low_poly_pipeline_basic.php index aec3693..8dc46a8 100644 --- a/examples/rendering/low_poly_pipeline_basic.php +++ b/examples/rendering/low_poly_pipeline_basic.php @@ -1,27 +1,19 @@ logoRotationCurrent = $transform->orientation->copy(); }; + // Update the scene + // -------------------------------------------------------------------- $app->update = function(QuickstartApp $app) use(&$state) { $app->updateSystem($state->cameraSystem); @@ -96,6 +90,8 @@ class LowPolyRendererDemoState $state->logoRotationCurrent->rotate(GLM::radians(1.0), new Vec3(0.0, 0.0, 1.0)); }; + // Render the scene + // -------------------------------------------------------------------- $app->render = function(QuickstartApp $app, RenderContext $context, RenderTargetResource $target) use(&$state) { // interpolate between previous and current rotation state using compensation diff --git a/examples/rendering/low_poly_pipeline_with_cubemap.php b/examples/rendering/low_poly_pipeline_with_cubemap.php new file mode 100644 index 0000000..7792132 --- /dev/null +++ b/examples/rendering/low_poly_pipeline_with_cubemap.php @@ -0,0 +1,351 @@ + [$x * $radius, $y * $radius, $z * $radius], + 'norm' => [$x, $y, $z] // normal is normalized position for unit sphere + ]; +} + +function addVertexToBuffer(FloatBuffer $buffer, array $vertex): void +{ + // position + normal format (6 floats per vertex) + // this matches what LPObjLoader::importMesh expects + $buffer->push($vertex['pos'][0]); + $buffer->push($vertex['pos'][1]); + $buffer->push($vertex['pos'][2]); + + $buffer->push($vertex['norm'][0]); + $buffer->push($vertex['norm'][1]); + $buffer->push($vertex['norm'][2]); +} + +// Demo State +// -------------------------------------------------------------------- +class LowPolyWithCubemapDemoState +{ + public VISUCameraSystem $cameraSystem; + public LPRenderingSystem $renderingSystem; + public LPModelCollection $models; + public ?Cubemap $environmentCubemap = null; + + // logo entity + public int $logoEntity; + + // rotation state for interpolation + public Quat $logoRotationPrevious; + public Quat $logoRotationCurrent; + + // grid configuration + public const GRID_ROUGHNESS_STEPS = 10; // x-axis: roughness 0.0 to 1.0 + public const GRID_METALLIC_STEPS = 5; // y-axis: metallic 0.0 or 1.0 + public const SPHERE_SPACING = 1.2; + + // sphere entities + /** @var array */ + public array $sphereEntities = []; +} + +$state = new LowPolyWithCubemapDemoState; + +/** + * Main Entry Point + * + * ---------------------------------------------------------------------------- + */ +$quickstart = new Quickstart(function(QuickstartOptions $app) use(&$state, $container) +{ + // Initialize the application + // -------------------------------------------------------------------- + $app->container = $container; + $app->ready = function(QuickstartApp $app) use(&$state) + { + // create a model collection + $state->models = new LPModelCollection(); + $state->renderingSystem = new LPRenderingSystem($app->gl, $app->shaders, $state->models); + + // load the VISU models coming with the engine (including logo) + $loader = new LPObjLoader($app->gl); + $loader->loadAllInDirectory(VISU_PATH_FRAMEWORK_RESOURCES . '/model/visu', $state->models); + + // create a vertex buffer for our procedural spheres + $vb = new LPVertexBuffer($app->gl); + + // generate sphere mesh data once (position + normal format) + $sphereMeshData = generateSphereMesh(0.5, 32, 16); + + // create sphere models with varying roughness and metallic values + // x-axis: roughness (0.0 to 1.0) + // y-axis: metallic (0.0 = dielectric, 1.0 = metallic) + $baseColor = new Vec3(0.8, 0.2, 0.2); // red-ish base color + $baseColor = new Vec3(1.); + + for ($my = 0; $my < LowPolyWithCubemapDemoState::GRID_METALLIC_STEPS; $my++) { + $metallic = $my / max(1, LowPolyWithCubemapDemoState::GRID_METALLIC_STEPS - 1); + + for ($rx = 0; $rx < LowPolyWithCubemapDemoState::GRID_ROUGHNESS_STEPS; $rx++) { + $roughness = $rx / max(1, LowPolyWithCubemapDemoState::GRID_ROUGHNESS_STEPS - 1); + + $roughness = min(max($roughness, 0.04), 1.0); // avoid 0.0 roughness for better visibility + $metallic = min(max($metallic, 0.0), 1.0); + + // create unique material for this sphere + $materialName = sprintf("pbr_r%.2f_m%.2f", $roughness, $metallic); + $material = new LPMaterial( + $materialName, + $baseColor->copy(), + $roughness, + $metallic + ); + + // import mesh with this material + $mesh = $loader->importMesh($sphereMeshData, $material, $vb); + + // create model and add to collection + $model = new LPModel("sphere_{$rx}_{$my}", [$mesh]); + $model->recalculateAABB(); + $state->models->add($model); + } + } + + // upload all vertex data to GPU + $vb->upload(); + + // create environment cubemap from HDRI (if available) + $hdriPath = '/Users/mariodoring/Downloads/cedar_bridge_sunset_1_4k.hdr'; + // $hdriPath = '/Users/mariodoring/Downloads/industrial_wooden_attic_4k.hdr'; + // $hdriPath = '/Users/mariodoring/Downloads/newport_loft.hdr'; + + $cubemapResolution = 1024; + + if (file_exists($hdriPath)) { + // convert HDRI to cubemap (1024x1024 faces for higher quality) + $converter = new HDRIToCubemap($app->gl); + $state->environmentCubemap = $converter->convert($hdriPath, $cubemapResolution); + echo "Loaded HDRI environment: $hdriPath\n"; + echo "Cubemap resolution: {$cubemapResolution}x{$cubemapResolution} per face\n"; + } else { + echo "No HDRI file found at: $hdriPath\n"; + echo "You can download free HDRI files from https://polyhaven.com/hdris\n"; + echo "Place your .hdr file at: /Users/mariodoring/Downloads/cedar_bridge_sunset_1_4k.hdr\n"; + } + + // set the environment cubemap in the rendering system + if ($state->environmentCubemap) { + $state->renderingSystem->setEnvironmentCubemap($state->environmentCubemap); + $state->renderingSystem->renderSkybox = true; // enable skybox rendering + } + + // to render 3D we need a camera + $state->cameraSystem = new VISUCameraSystem($app->input, $app->dispatcher); + + // register the rendering system + $app->bindSystems([ + $state->renderingSystem, + $state->cameraSystem + ]); + }; + + // Initialize the scene + // -------------------------------------------------------------------- + $app->initializeScene = function(QuickstartApp $app) use(&$state) + { + // position camera to see the entire grid + $gridWidth = LowPolyWithCubemapDemoState::GRID_ROUGHNESS_STEPS * LowPolyWithCubemapDemoState::SPHERE_SPACING; + $gridHeight = LowPolyWithCubemapDemoState::GRID_METALLIC_STEPS * LowPolyWithCubemapDemoState::SPHERE_SPACING; + $cameraDistance = max($gridWidth, $gridHeight) * 1.2; + + $state->cameraSystem->spawnDefaultFlyingCamera($app->entities, new Vec3( + $gridWidth * 0.5 - LowPolyWithCubemapDemoState::SPHERE_SPACING * 0.5, + $gridHeight * 0.5 - LowPolyWithCubemapDemoState::SPHERE_SPACING * 0.5, + $cameraDistance + )); + + // spawn a visu logo in the middle + $state->logoEntity = $app->entities->create(); + $app->entities->attach($state->logoEntity, new LPDynamicModel('visu_logo')); + $logoTransform = $app->entities->attach($state->logoEntity, new Transform()); + $logoTransform->orientation->rotate(GLM::radians(90.0), new Vec3(1.0, 0.0, 0.0)); + $logoTransform->position = new Vec3( + $gridWidth * 0.5 - LowPolyWithCubemapDemoState::SPHERE_SPACING * 0.5, + $gridHeight * 0.5 - LowPolyWithCubemapDemoState::SPHERE_SPACING * 0.5, + -1.0 // place logo slightly in front of the sphere grid + ); + + // initialize rotation state for interpolation + $state->logoRotationPrevious = $logoTransform->orientation->copy(); + $state->logoRotationCurrent = $logoTransform->orientation->copy(); + + // spawn sphere grid + // x-axis: roughness (left = 0.0, right = 1.0) + // y-axis: metallic (bottom = 0.0, top = 1.0) + for ($my = 0; $my < LowPolyWithCubemapDemoState::GRID_METALLIC_STEPS; $my++) { + for ($rx = 0; $rx < LowPolyWithCubemapDemoState::GRID_ROUGHNESS_STEPS; $rx++) { + $entity = $app->entities->create(); + + // attach the corresponding sphere model + $modelName = "sphere_{$rx}_{$my}"; + $app->entities->attach($entity, new LPDynamicModel($modelName)); + + // position in grid + $transform = $app->entities->attach($entity, new Transform()); + $transform->position = new Vec3( + $rx * LowPolyWithCubemapDemoState::SPHERE_SPACING, + $my * LowPolyWithCubemapDemoState::SPHERE_SPACING, + 0.0 + ); + + $state->sphereEntities[] = $entity; + } + } + + echo "Spawned " . count($state->sphereEntities) . " spheres in a grid\n"; + echo "X-axis: Roughness (0.0 left -> 1.0 right)\n"; + echo "Y-axis: Metallic (0.0 bottom -> 1.0 top)\n"; + }; + + // Update the scene + // -------------------------------------------------------------------- + $app->update = function(QuickstartApp $app) use(&$state) + { + $app->updateSystem($state->cameraSystem); + + // store previous state before updating + $state->logoRotationPrevious = $state->logoRotationCurrent->copy(); + + // rotate the logo by a fixed amount per tick + $state->logoRotationCurrent->rotate(GLM::radians(1.0), new Vec3(0.0, 0.0, 1.0)); + }; + + // Render the scene + // -------------------------------------------------------------------- + $app->render = function(QuickstartApp $app, RenderContext $context, RenderTargetResource $target) use(&$state) + { + // interpolate between previous and current rotation state using compensation + // this way you get butter smooth rotation + $logoTransform = $app->entities->get($state->logoEntity, Transform::class); + $logoTransform->orientation = Quat::slerp($state->logoRotationPrevious, $state->logoRotationCurrent, $context->compensation); + $logoTransform->markDirty(); + + // make sure to tell the low poly rendering system which render target we are using + $state->renderingSystem->setRenderTarget($target); + + $app->renderSystem($state->cameraSystem, $context); + $app->renderSystem($state->renderingSystem, $context); + }; +}); + +echo "PBR Material Debug Grid with Enhanced IBL\n"; +echo "==========================================\n"; +echo "Controls:\n"; +echo " WASD/Mouse - Fly around\n"; +echo "\n"; +echo "IBL Quality Improvements:\n"; +echo " - 4096 samples for prefiltered environment maps\n"; +echo " - 1024x1024 cubemap resolution (configurable)\n"; +echo " - 256x256 prefilter maps with better mip chaining\n"; +echo " - Improved filtering and mip level calculations\n"; +echo " - Anisotropic filtering when supported\n"; +echo "\n"; +echo "Noise should be significantly reduced compared to default settings.\n"; +echo "Adjust cubemapResolution variable for performance vs quality trade-off.\n"; +echo "\n"; + +$quickstart->run(); \ No newline at end of file diff --git a/resources/model/visu/sphere.obj b/resources/model/visu/sphere.obj new file mode 100644 index 0000000..e6f14c6 --- /dev/null +++ b/resources/model/visu/sphere.obj @@ -0,0 +1,9943 @@ +# Blender 3.4.1 +# www.blender.org +mtllib sphere.mtl +o Sphere +v 0.000000 0.831470 -0.555570 +v 0.000000 0.555570 -0.831470 +v 0.000000 0.195090 -0.980785 +v 0.000000 0.000000 -1.000000 +v 0.000000 -0.195090 -0.980785 +v 0.000000 -0.555570 -0.831470 +v 0.038060 0.980785 -0.191342 +v 0.074658 0.923880 -0.375330 +v 0.108386 0.831470 -0.544895 +v 0.137950 0.707107 -0.693520 +v 0.162212 0.555570 -0.815493 +v 0.180240 0.382683 -0.906127 +v 0.191342 0.195090 -0.961940 +v 0.195090 0.000000 -0.980785 +v 0.191342 -0.195090 -0.961940 +v 0.180240 -0.382683 -0.906127 +v 0.162212 -0.555570 -0.815493 +v 0.137950 -0.707107 -0.693520 +v 0.108386 -0.831470 -0.544895 +v 0.074658 -0.923880 -0.375330 +v 0.038060 -0.980785 -0.191342 +v 0.074658 0.980785 -0.180240 +v 0.146447 0.923880 -0.353553 +v 0.212608 0.831470 -0.513280 +v 0.270598 0.707107 -0.653281 +v 0.318190 0.555570 -0.768178 +v 0.353553 0.382683 -0.853553 +v 0.375330 0.195090 -0.906127 +v 0.382683 0.000000 -0.923879 +v 0.375330 -0.195090 -0.906127 +v 0.353553 -0.382683 -0.853553 +v 0.318190 -0.555570 -0.768178 +v 0.270598 -0.707107 -0.653281 +v 0.212608 -0.831470 -0.513280 +v 0.146447 -0.923880 -0.353553 +v 0.074658 -0.980785 -0.180240 +v 0.108386 0.980785 -0.162212 +v 0.212608 0.923880 -0.318190 +v 0.308658 0.831470 -0.461940 +v 0.392847 0.707107 -0.587938 +v 0.461940 0.555570 -0.691342 +v 0.513280 0.382683 -0.768178 +v 0.544895 0.195090 -0.815493 +v 0.555570 0.000000 -0.831469 +v 0.544895 -0.195090 -0.815493 +v 0.513280 -0.382683 -0.768178 +v 0.461940 -0.555570 -0.691342 +v 0.392847 -0.707107 -0.587938 +v 0.308658 -0.831470 -0.461940 +v 0.212608 -0.923880 -0.318190 +v 0.108386 -0.980785 -0.162212 +v 0.137950 0.980785 -0.137950 +v 0.270598 0.923880 -0.270598 +v 0.392847 0.831470 -0.392847 +v 0.500000 0.707107 -0.500000 +v 0.587938 0.555570 -0.587938 +v 0.653281 0.382683 -0.653281 +v 0.693520 0.195090 -0.693520 +v 0.707107 0.000000 -0.707107 +v 0.693520 -0.195090 -0.693520 +v 0.653281 -0.382683 -0.653281 +v 0.587938 -0.555570 -0.587938 +v 0.500000 -0.707107 -0.500000 +v 0.392847 -0.831470 -0.392847 +v 0.270598 -0.923880 -0.270598 +v 0.137950 -0.980785 -0.137950 +v 0.162212 0.980785 -0.108386 +v 0.318190 0.923880 -0.212608 +v 0.461940 0.831470 -0.308658 +v 0.587938 0.707107 -0.392847 +v 0.691342 0.555570 -0.461940 +v 0.768178 0.382683 -0.513280 +v 0.815493 0.195090 -0.544895 +v 0.831470 0.000000 -0.555570 +v 0.815493 -0.195090 -0.544895 +v 0.768178 -0.382683 -0.513280 +v 0.691342 -0.555570 -0.461940 +v 0.587938 -0.707107 -0.392847 +v 0.461940 -0.831470 -0.308658 +v 0.318190 -0.923880 -0.212608 +v 0.162212 -0.980785 -0.108386 +v 0.000000 1.000000 0.000000 +v 0.180240 0.980785 -0.074658 +v 0.353553 0.923880 -0.146447 +v 0.513280 0.831470 -0.212607 +v 0.653281 0.707107 -0.270598 +v 0.768178 0.555570 -0.318190 +v 0.853553 0.382683 -0.353553 +v 0.906127 0.195090 -0.375330 +v 0.923879 0.000000 -0.382683 +v 0.906127 -0.195090 -0.375330 +v 0.853553 -0.382683 -0.353553 +v 0.768178 -0.555570 -0.318190 +v 0.653281 -0.707107 -0.270598 +v 0.513280 -0.831470 -0.212607 +v 0.353553 -0.923880 -0.146447 +v 0.180240 -0.980785 -0.074658 +v 0.191342 0.980785 -0.038060 +v 0.375330 0.923880 -0.074658 +v 0.544895 0.831470 -0.108386 +v 0.693520 0.707107 -0.137950 +v 0.815493 0.555570 -0.162212 +v 0.906127 0.382683 -0.180240 +v 0.961940 0.195090 -0.191342 +v 0.980785 0.000000 -0.195090 +v 0.961940 -0.195090 -0.191342 +v 0.906127 -0.382683 -0.180240 +v 0.815493 -0.555570 -0.162212 +v 0.693520 -0.707107 -0.137950 +v 0.544895 -0.831470 -0.108386 +v 0.375330 -0.923880 -0.074658 +v 0.191342 -0.980785 -0.038060 +v 0.195090 0.980785 0.000000 +v 0.382683 0.923880 0.000000 +v 0.555570 0.831470 0.000000 +v 0.707107 0.707107 -0.000000 +v 0.831469 0.555570 0.000000 +v 0.923879 0.382683 -0.000000 +v 0.980785 0.195090 0.000000 +v 1.000000 0.000000 0.000000 +v 0.980785 -0.195090 0.000000 +v 0.923879 -0.382683 -0.000000 +v 0.831469 -0.555570 0.000000 +v 0.707107 -0.707107 -0.000000 +v 0.555570 -0.831470 0.000000 +v 0.382683 -0.923880 0.000000 +v 0.195090 -0.980785 0.000000 +v 0.191342 0.980785 0.038060 +v 0.375330 0.923880 0.074658 +v 0.544895 0.831470 0.108386 +v 0.693520 0.707107 0.137950 +v 0.815493 0.555570 0.162212 +v 0.906127 0.382683 0.180240 +v 0.961940 0.195090 0.191342 +v 0.980785 0.000000 0.195090 +v 0.961940 -0.195090 0.191342 +v 0.906127 -0.382683 0.180240 +v 0.815493 -0.555570 0.162212 +v 0.693520 -0.707107 0.137950 +v 0.544895 -0.831470 0.108386 +v 0.375330 -0.923880 0.074658 +v 0.191342 -0.980785 0.038060 +v 0.180240 0.980785 0.074658 +v 0.353553 0.923880 0.146447 +v 0.513280 0.831470 0.212608 +v 0.653281 0.707107 0.270598 +v 0.768178 0.555570 0.318190 +v 0.853553 0.382683 0.353553 +v 0.906127 0.195090 0.375330 +v 0.923879 0.000000 0.382683 +v 0.906127 -0.195090 0.375330 +v 0.853553 -0.382683 0.353553 +v 0.768178 -0.555570 0.318190 +v 0.653281 -0.707107 0.270598 +v 0.513280 -0.831470 0.212608 +v 0.353553 -0.923880 0.146447 +v 0.180240 -0.980785 0.074658 +v 0.162212 0.980785 0.108386 +v 0.318190 0.923880 0.212608 +v 0.461940 0.831470 0.308658 +v 0.587938 0.707107 0.392847 +v 0.691341 0.555570 0.461940 +v 0.768178 0.382683 0.513280 +v 0.815493 0.195090 0.544895 +v 0.831469 0.000000 0.555570 +v 0.815493 -0.195090 0.544895 +v 0.768178 -0.382683 0.513280 +v 0.691341 -0.555570 0.461940 +v 0.587938 -0.707107 0.392847 +v 0.461940 -0.831470 0.308658 +v 0.318190 -0.923880 0.212608 +v 0.162212 -0.980785 0.108386 +v 0.137950 0.980785 0.137950 +v 0.270598 0.923880 0.270598 +v 0.392847 0.831470 0.392847 +v 0.500000 0.707107 0.500000 +v 0.587938 0.555570 0.587938 +v 0.653281 0.382683 0.653281 +v 0.693520 0.195090 0.693520 +v 0.707106 0.000000 0.707107 +v 0.693520 -0.195090 0.693520 +v 0.653281 -0.382683 0.653281 +v 0.587938 -0.555570 0.587938 +v 0.500000 -0.707107 0.500000 +v 0.392847 -0.831470 0.392847 +v 0.270598 -0.923880 0.270598 +v 0.137950 -0.980785 0.137950 +v 0.108386 0.980785 0.162212 +v 0.212607 0.923880 0.318190 +v 0.308658 0.831470 0.461940 +v 0.392847 0.707107 0.587938 +v 0.461940 0.555570 0.691342 +v 0.513280 0.382683 0.768178 +v 0.544895 0.195090 0.815493 +v 0.555570 0.000000 0.831469 +v 0.544895 -0.195090 0.815493 +v 0.513280 -0.382683 0.768178 +v 0.461940 -0.555570 0.691342 +v 0.392847 -0.707107 0.587938 +v 0.308658 -0.831470 0.461940 +v 0.212607 -0.923880 0.318190 +v 0.108386 -0.980785 0.162212 +v 0.074658 0.980785 0.180240 +v 0.146447 0.923880 0.353553 +v 0.212607 0.831470 0.513280 +v 0.270598 0.707107 0.653281 +v 0.318189 0.555570 0.768178 +v 0.353553 0.382683 0.853553 +v 0.375330 0.195090 0.906127 +v 0.382683 0.000000 0.923879 +v 0.375330 -0.195090 0.906127 +v 0.353553 -0.382683 0.853553 +v 0.318189 -0.555570 0.768178 +v 0.270598 -0.707107 0.653281 +v 0.212607 -0.831470 0.513280 +v 0.146447 -0.923880 0.353553 +v 0.074658 -0.980785 0.180240 +v 0.038060 0.980785 0.191342 +v 0.074658 0.923880 0.375330 +v 0.108386 0.831470 0.544895 +v 0.137950 0.707107 0.693520 +v 0.162212 0.555570 0.815493 +v 0.180240 0.382683 0.906127 +v 0.191342 0.195090 0.961939 +v 0.195090 0.000000 0.980785 +v 0.191342 -0.195090 0.961939 +v 0.180240 -0.382683 0.906127 +v 0.162212 -0.555570 0.815493 +v 0.137950 -0.707107 0.693520 +v 0.108386 -0.831470 0.544895 +v 0.074658 -0.923880 0.375330 +v 0.038060 -0.980785 0.191342 +v -0.000000 0.980785 0.195090 +v -0.000000 0.923880 0.382683 +v -0.000000 0.831470 0.555570 +v -0.000000 0.707107 0.707107 +v -0.000000 0.555570 0.831469 +v 0.000000 0.382683 0.923879 +v -0.000000 0.195090 0.980785 +v -0.000000 0.000000 0.999999 +v -0.000000 -0.195090 0.980785 +v 0.000000 -0.382683 0.923879 +v -0.000000 -0.555570 0.831469 +v -0.000000 -0.707107 0.707107 +v -0.000000 -0.831470 0.555570 +v -0.000000 -0.923880 0.382683 +v -0.000000 -0.980785 0.195090 +v -0.038060 0.980785 0.191342 +v -0.074658 0.923880 0.375330 +v -0.108386 0.831470 0.544895 +v -0.137950 0.707107 0.693520 +v -0.162212 0.555570 0.815493 +v -0.180240 0.382683 0.906127 +v -0.191342 0.195090 0.961939 +v -0.195091 0.000000 0.980785 +v -0.191342 -0.195090 0.961939 +v -0.180240 -0.382683 0.906127 +v -0.162212 -0.555570 0.815493 +v -0.137950 -0.707107 0.693520 +v -0.108386 -0.831470 0.544895 +v -0.074658 -0.923880 0.375330 +v -0.038060 -0.980785 0.191342 +v -0.074658 0.980785 0.180240 +v -0.146447 0.923880 0.353553 +v -0.212608 0.831470 0.513280 +v -0.270598 0.707107 0.653281 +v -0.318190 0.555570 0.768177 +v -0.353553 0.382683 0.853553 +v -0.375330 0.195090 0.906127 +v -0.382683 0.000000 0.923879 +v -0.375330 -0.195090 0.906127 +v -0.353553 -0.382683 0.853553 +v -0.318190 -0.555570 0.768177 +v -0.270598 -0.707107 0.653281 +v -0.212608 -0.831470 0.513280 +v -0.146447 -0.923880 0.353553 +v -0.074658 -0.980785 0.180240 +v -0.108386 0.980785 0.162212 +v -0.212608 0.923880 0.318190 +v -0.308658 0.831470 0.461939 +v -0.392847 0.707107 0.587938 +v -0.461940 0.555570 0.691341 +v -0.513280 0.382683 0.768178 +v -0.544895 0.195090 0.815493 +v -0.555570 0.000000 0.831469 +v -0.544895 -0.195090 0.815493 +v -0.513280 -0.382683 0.768178 +v -0.461940 -0.555570 0.691341 +v -0.392847 -0.707107 0.587938 +v -0.308658 -0.831470 0.461939 +v -0.212608 -0.923880 0.318190 +v -0.108386 -0.980785 0.162212 +v -0.137950 0.980785 0.137950 +v -0.270598 0.923880 0.270598 +v -0.392847 0.831470 0.392847 +v -0.500000 0.707107 0.500000 +v -0.587938 0.555570 0.587937 +v -0.653281 0.382683 0.653281 +v -0.693520 0.195090 0.693520 +v -0.707106 0.000000 0.707106 +v -0.693520 -0.195090 0.693520 +v -0.653281 -0.382683 0.653281 +v -0.587938 -0.555570 0.587937 +v -0.500000 -0.707107 0.500000 +v -0.392847 -0.831470 0.392847 +v -0.270598 -0.923880 0.270598 +v -0.137950 -0.980785 0.137950 +v 0.000000 -1.000000 0.000000 +v -0.162212 0.980785 0.108386 +v -0.318190 0.923880 0.212607 +v -0.461940 0.831470 0.308658 +v -0.587938 0.707107 0.392847 +v -0.691341 0.555570 0.461939 +v -0.768177 0.382683 0.513280 +v -0.815493 0.195090 0.544895 +v -0.831469 0.000000 0.555569 +v -0.815493 -0.195090 0.544895 +v -0.768177 -0.382683 0.513280 +v -0.691341 -0.555570 0.461939 +v -0.587938 -0.707107 0.392847 +v -0.461940 -0.831470 0.308658 +v -0.318190 -0.923880 0.212607 +v -0.162212 -0.980785 0.108386 +v -0.180240 0.980785 0.074658 +v -0.353553 0.923880 0.146447 +v -0.513280 0.831470 0.212607 +v -0.653281 0.707107 0.270598 +v -0.768177 0.555570 0.318189 +v -0.853553 0.382683 0.353553 +v -0.906127 0.195090 0.375330 +v -0.923879 0.000000 0.382683 +v -0.906127 -0.195090 0.375330 +v -0.853553 -0.382683 0.353553 +v -0.768177 -0.555570 0.318189 +v -0.653281 -0.707107 0.270598 +v -0.513280 -0.831470 0.212607 +v -0.353553 -0.923880 0.146447 +v -0.180240 -0.980785 0.074658 +v -0.191342 0.980785 0.038060 +v -0.375330 0.923880 0.074658 +v -0.544895 0.831470 0.108386 +v -0.693520 0.707107 0.137950 +v -0.815493 0.555570 0.162211 +v -0.906127 0.382683 0.180240 +v -0.961939 0.195090 0.191341 +v -0.980784 0.000000 0.195090 +v -0.961939 -0.195090 0.191341 +v -0.906127 -0.382683 0.180240 +v -0.815493 -0.555570 0.162211 +v -0.693520 -0.707107 0.137950 +v -0.544895 -0.831470 0.108386 +v -0.375330 -0.923880 0.074658 +v -0.191342 -0.980785 0.038060 +v -0.195090 0.980785 -0.000000 +v -0.382683 0.923880 -0.000000 +v -0.555570 0.831470 -0.000000 +v -0.707107 0.707107 -0.000000 +v -0.831469 0.555570 -0.000000 +v -0.923879 0.382683 -0.000000 +v -0.980785 0.195090 -0.000000 +v -0.999999 0.000000 -0.000000 +v -0.980785 -0.195090 -0.000000 +v -0.923879 -0.382683 -0.000000 +v -0.831469 -0.555570 -0.000000 +v -0.707107 -0.707107 -0.000000 +v -0.555570 -0.831470 -0.000000 +v -0.382683 -0.923880 -0.000000 +v -0.195090 -0.980785 -0.000000 +v -0.191342 0.980785 -0.038060 +v -0.375330 0.923880 -0.074658 +v -0.544895 0.831470 -0.108386 +v -0.693520 0.707107 -0.137950 +v -0.815493 0.555570 -0.162212 +v -0.906127 0.382683 -0.180240 +v -0.961939 0.195090 -0.191342 +v -0.980784 0.000000 -0.195091 +v -0.961939 -0.195090 -0.191342 +v -0.906127 -0.382683 -0.180240 +v -0.815493 -0.555570 -0.162212 +v -0.693520 -0.707107 -0.137950 +v -0.544895 -0.831470 -0.108386 +v -0.375330 -0.923880 -0.074658 +v -0.191342 -0.980785 -0.038060 +v -0.180240 0.980785 -0.074658 +v -0.353553 0.923880 -0.146447 +v -0.513279 0.831470 -0.212607 +v -0.653281 0.707107 -0.270598 +v -0.768177 0.555570 -0.318190 +v -0.853553 0.382683 -0.353553 +v -0.906127 0.195090 -0.375330 +v -0.923878 0.000000 -0.382683 +v -0.906127 -0.195090 -0.375330 +v -0.853553 -0.382683 -0.353553 +v -0.768177 -0.555570 -0.318190 +v -0.653281 -0.707107 -0.270598 +v -0.513279 -0.831470 -0.212607 +v -0.353553 -0.923880 -0.146447 +v -0.180240 -0.980785 -0.074658 +v -0.162212 0.980785 -0.108386 +v -0.318189 0.923880 -0.212607 +v -0.461939 0.831470 -0.308658 +v -0.587938 0.707107 -0.392847 +v -0.691341 0.555570 -0.461940 +v -0.768177 0.382683 -0.513280 +v -0.815493 0.195090 -0.544895 +v -0.831468 0.000000 -0.555570 +v -0.815493 -0.195090 -0.544895 +v -0.768177 -0.382683 -0.513280 +v -0.691341 -0.555570 -0.461940 +v -0.587938 -0.707107 -0.392847 +v -0.461939 -0.831470 -0.308658 +v -0.318189 -0.923880 -0.212607 +v -0.162212 -0.980785 -0.108386 +v -0.137950 0.980785 -0.137950 +v -0.270598 0.923880 -0.270598 +v -0.392847 0.831470 -0.392847 +v -0.500000 0.707107 -0.500000 +v -0.587937 0.555570 -0.587938 +v -0.653281 0.382683 -0.653281 +v -0.693519 0.195090 -0.693520 +v -0.707106 0.000000 -0.707106 +v -0.693519 -0.195090 -0.693520 +v -0.653281 -0.382683 -0.653281 +v -0.587937 -0.555570 -0.587938 +v -0.500000 -0.707107 -0.500000 +v -0.392847 -0.831470 -0.392847 +v -0.270598 -0.923880 -0.270598 +v -0.137950 -0.980785 -0.137950 +v -0.108386 0.980785 -0.162212 +v -0.212607 0.923880 -0.318190 +v -0.308658 0.831470 -0.461939 +v -0.392847 0.707107 -0.587938 +v -0.461939 0.555570 -0.691341 +v -0.513280 0.382683 -0.768177 +v -0.544895 0.195090 -0.815493 +v -0.555569 0.000000 -0.831469 +v -0.544895 -0.195090 -0.815493 +v -0.513280 -0.382683 -0.768177 +v -0.461939 -0.555570 -0.691341 +v -0.392847 -0.707107 -0.587938 +v -0.308658 -0.831470 -0.461939 +v -0.212607 -0.923880 -0.318190 +v -0.108386 -0.980785 -0.162212 +v -0.074658 0.980785 -0.180240 +v -0.146446 0.923880 -0.353553 +v -0.212607 0.831470 -0.513279 +v -0.270598 0.707107 -0.653281 +v -0.318189 0.555570 -0.768177 +v -0.353553 0.382683 -0.853553 +v -0.375330 0.195090 -0.906127 +v -0.382683 0.000000 -0.923879 +v -0.375330 -0.195090 -0.906127 +v -0.353553 -0.382683 -0.853553 +v -0.318189 -0.555570 -0.768177 +v -0.270598 -0.707107 -0.653281 +v -0.212607 -0.831470 -0.513279 +v -0.146446 -0.923880 -0.353553 +v -0.074658 -0.980785 -0.180240 +v -0.038060 0.980785 -0.191342 +v -0.074658 0.923880 -0.375330 +v -0.108386 0.831470 -0.544895 +v -0.137950 0.707107 -0.693520 +v -0.162211 0.555570 -0.815493 +v -0.180240 0.382683 -0.906127 +v -0.191341 0.195090 -0.961939 +v -0.195090 0.000000 -0.980784 +v -0.191341 -0.195090 -0.961939 +v -0.180240 -0.382683 -0.906127 +v -0.162211 -0.555570 -0.815493 +v -0.137950 -0.707107 -0.693520 +v -0.108386 -0.831470 -0.544895 +v -0.074658 -0.923880 -0.375330 +v -0.038060 -0.980785 -0.191342 +v 0.000000 0.980785 -0.195090 +v 0.000000 0.923880 -0.382683 +v 0.000000 0.707107 -0.707107 +v 0.000000 0.382683 -0.923879 +v 0.000000 -0.382683 -0.923879 +v 0.000000 -0.707107 -0.707107 +v 0.000000 -0.831470 -0.555570 +v 0.000000 -0.923880 -0.382683 +v 0.000000 -0.980785 -0.195090 +vn -0.0000 0.8286 -0.5598 +vn 0.0757 0.9217 -0.3804 +vn 0.1092 0.8286 -0.5490 +vn -0.0000 -0.3805 -0.9248 +vn 0.1626 -0.5528 -0.8173 +vn -0.0000 -0.5528 -0.8333 +vn -0.0000 0.7041 -0.7101 +vn 0.1385 0.7041 -0.6965 +vn 0.1385 -0.7041 -0.6965 +vn -0.0000 -0.7041 -0.7101 +vn -0.0000 0.5528 -0.8333 +vn 0.1626 0.5528 -0.8173 +vn 0.1092 -0.8286 -0.5490 +vn -0.0000 -0.8286 -0.5598 +vn -0.0000 0.3805 -0.9248 +vn 0.1804 0.3805 -0.9070 +vn 0.0757 -0.9217 -0.3804 +vn -0.0000 -0.9217 -0.3879 +vn -0.0000 0.1939 -0.9810 +vn 0.1914 0.1939 -0.9622 +vn 0.0392 -0.9796 -0.1971 +vn -0.0000 -0.9796 -0.2010 +vn 0.1951 -0.0000 -0.9808 +vn -0.0000 -0.0000 -1.0000 +vn -0.0000 0.9796 -0.2010 +vn -0.0000 1.0000 -0.0000 +vn 0.0392 0.9796 -0.1971 +vn -0.0000 -1.0000 -0.0000 +vn 0.1914 -0.1939 -0.9622 +vn -0.0000 -0.1939 -0.9810 +vn -0.0000 0.9217 -0.3879 +vn 0.1804 -0.3805 -0.9070 +vn 0.1484 0.9217 -0.3584 +vn 0.3539 -0.3805 -0.8544 +vn 0.2142 0.8286 -0.5172 +vn 0.3189 -0.5528 -0.7699 +vn 0.2718 0.7041 -0.6561 +vn 0.2718 -0.7041 -0.6561 +vn 0.3189 0.5528 -0.7699 +vn 0.2142 -0.8286 -0.5172 +vn 0.3539 0.3805 -0.8544 +vn 0.1484 -0.9217 -0.3584 +vn 0.3754 0.1939 -0.9063 +vn 0.0769 -0.9796 -0.1857 +vn 0.3827 -0.0000 -0.9239 +vn 0.0769 0.9796 -0.1857 +vn 0.3754 -0.1939 -0.9063 +vn 0.5138 0.3805 -0.7689 +vn 0.2155 -0.9217 -0.3225 +vn 0.5450 0.1939 -0.8157 +vn 0.1117 -0.9796 -0.1671 +vn 0.5556 -0.0000 -0.8315 +vn 0.1117 0.9796 -0.1671 +vn 0.5450 -0.1939 -0.8157 +vn 0.2155 0.9217 -0.3225 +vn 0.5138 -0.3805 -0.7689 +vn 0.3110 0.8286 -0.4654 +vn 0.4630 -0.5528 -0.6929 +vn 0.3945 0.7041 -0.5905 +vn 0.3945 -0.7041 -0.5905 +vn 0.4630 0.5528 -0.6929 +vn 0.3110 -0.8286 -0.4654 +vn 0.6539 -0.3805 -0.6539 +vn 0.3958 0.8286 -0.3958 +vn 0.5893 -0.5528 -0.5893 +vn 0.5021 0.7041 -0.5021 +vn 0.5021 -0.7041 -0.5021 +vn 0.5893 0.5528 -0.5893 +vn 0.3958 -0.8286 -0.3958 +vn 0.6539 0.3805 -0.6539 +vn 0.2743 -0.9217 -0.2743 +vn 0.6937 0.1939 -0.6937 +vn 0.1421 -0.9796 -0.1421 +vn 0.7071 -0.0000 -0.7071 +vn 0.1421 0.9796 -0.1421 +vn 0.6937 -0.1939 -0.6937 +vn 0.2743 0.9217 -0.2743 +vn 0.4654 -0.8286 -0.3110 +vn 0.3225 -0.9217 -0.2155 +vn 0.7689 0.3805 -0.5138 +vn 0.8157 0.1939 -0.5450 +vn 0.1671 -0.9796 -0.1117 +vn 0.8315 -0.0000 -0.5556 +vn 0.1671 0.9796 -0.1117 +vn 0.8157 -0.1939 -0.5450 +vn 0.3225 0.9217 -0.2155 +vn 0.7689 -0.3805 -0.5138 +vn 0.4654 0.8286 -0.3110 +vn 0.6929 -0.5528 -0.4630 +vn 0.5905 0.7041 -0.3945 +vn 0.5905 -0.7041 -0.3945 +vn 0.6929 0.5528 -0.4630 +vn 0.5172 0.8286 -0.2142 +vn 0.8544 -0.3805 -0.3539 +vn 0.7699 -0.5528 -0.3189 +vn 0.6561 0.7041 -0.2718 +vn 0.6561 -0.7041 -0.2718 +vn 0.7699 0.5528 -0.3189 +vn 0.5172 -0.8286 -0.2142 +vn 0.8544 0.3805 -0.3539 +vn 0.3584 -0.9217 -0.1484 +vn 0.9063 0.1939 -0.3754 +vn 0.1857 -0.9796 -0.0769 +vn 0.9239 -0.0000 -0.3827 +vn 0.1857 0.9796 -0.0769 +vn 0.9063 -0.1939 -0.3754 +vn 0.3584 0.9217 -0.1484 +vn 0.9070 0.3805 -0.1804 +vn 0.9622 0.1939 -0.1914 +vn 0.1971 -0.9796 -0.0392 +vn 0.9808 -0.0000 -0.1951 +vn 0.1971 0.9796 -0.0392 +vn 0.9622 -0.1939 -0.1914 +vn 0.3804 0.9217 -0.0757 +vn 0.9070 -0.3805 -0.1804 +vn 0.5490 0.8286 -0.1092 +vn 0.8173 -0.5528 -0.1626 +vn 0.6965 0.7041 -0.1385 +vn 0.6965 -0.7041 -0.1385 +vn 0.8173 0.5528 -0.1626 +vn 0.5490 -0.8286 -0.1092 +vn 0.3804 -0.9217 -0.0757 +vn 0.9248 -0.3805 -0.0000 +vn 0.8333 -0.5528 -0.0000 +vn 0.7101 0.7041 -0.0000 +vn 0.7101 -0.7041 -0.0000 +vn 0.8333 0.5528 -0.0000 +vn 0.5598 -0.8286 -0.0000 +vn 0.9248 0.3805 -0.0000 +vn 0.3879 -0.9217 -0.0000 +vn 0.9810 0.1939 -0.0000 +vn 0.2010 -0.9796 -0.0000 +vn 1.0000 -0.0000 -0.0000 +vn 0.2010 0.9796 -0.0000 +vn 0.9810 -0.1939 -0.0000 +vn 0.3879 0.9217 -0.0000 +vn 0.5598 0.8286 -0.0000 +vn 0.1971 -0.9796 0.0392 +vn 0.9622 0.1939 0.1914 +vn 0.9808 -0.0000 0.1951 +vn 0.1971 0.9796 0.0392 +vn 0.9622 -0.1939 0.1914 +vn 0.3804 0.9217 0.0757 +vn 0.9070 -0.3805 0.1804 +vn 0.5490 0.8286 0.1092 +vn 0.8173 -0.5528 0.1626 +vn 0.6965 0.7041 0.1385 +vn 0.6965 -0.7041 0.1385 +vn 0.8173 0.5528 0.1626 +vn 0.5490 -0.8286 0.1092 +vn 0.9070 0.3805 0.1804 +vn 0.3804 -0.9217 0.0757 +vn 0.6561 -0.7041 0.2718 +vn 0.6561 0.7041 0.2718 +vn 0.7699 0.5528 0.3189 +vn 0.5172 -0.8286 0.2142 +vn 0.8544 0.3805 0.3539 +vn 0.3584 -0.9217 0.1484 +vn 0.9063 0.1939 0.3754 +vn 0.1857 -0.9796 0.0769 +vn 0.9239 -0.0000 0.3827 +vn 0.1857 0.9796 0.0769 +vn 0.9063 -0.1939 0.3754 +vn 0.3584 0.9217 0.1484 +vn 0.8544 -0.3805 0.3539 +vn 0.5172 0.8286 0.2142 +vn 0.7699 -0.5528 0.3189 +vn 0.1671 0.9796 0.1117 +vn 0.1671 -0.9796 0.1117 +vn 0.8157 -0.1939 0.5450 +vn 0.3225 0.9217 0.2155 +vn 0.7689 -0.3805 0.5138 +vn 0.4654 0.8286 0.3110 +vn 0.6929 -0.5528 0.4630 +vn 0.5905 0.7041 0.3945 +vn 0.5905 -0.7041 0.3945 +vn 0.6929 0.5528 0.4630 +vn 0.4654 -0.8286 0.3110 +vn 0.7689 0.3805 0.5138 +vn 0.3225 -0.9217 0.2155 +vn 0.8157 0.1939 0.5450 +vn 0.8315 -0.0000 0.5556 +vn 0.5021 0.7041 0.5021 +vn 0.5893 0.5528 0.5893 +vn 0.3958 -0.8286 0.3958 +vn 0.6539 0.3805 0.6539 +vn 0.2743 -0.9217 0.2743 +vn 0.6937 0.1939 0.6937 +vn 0.1421 -0.9796 0.1421 +vn 0.7071 -0.0000 0.7071 +vn 0.1421 0.9796 0.1421 +vn 0.6937 -0.1939 0.6937 +vn 0.2743 0.9217 0.2743 +vn 0.6539 -0.3805 0.6539 +vn 0.3958 0.8286 0.3958 +vn 0.5893 -0.5528 0.5893 +vn 0.5021 -0.7041 0.5021 +vn 0.5450 -0.1939 0.8157 +vn 0.1117 0.9796 0.1671 +vn 0.2155 0.9217 0.3225 +vn 0.5138 -0.3805 0.7689 +vn 0.3110 0.8286 0.4654 +vn 0.4630 -0.5528 0.6929 +vn 0.3945 0.7041 0.5905 +vn 0.3945 -0.7041 0.5905 +vn 0.4630 0.5528 0.6929 +vn 0.3110 -0.8286 0.4654 +vn 0.5138 0.3805 0.7689 +vn 0.2155 -0.9217 0.3225 +vn 0.5450 0.1939 0.8157 +vn 0.1117 -0.9796 0.1671 +vn 0.5556 -0.0000 0.8315 +vn 0.2718 -0.7041 0.6561 +vn 0.2142 -0.8286 0.5172 +vn 0.3539 0.3805 0.8544 +vn 0.1484 -0.9217 0.3584 +vn 0.3754 0.1939 0.9063 +vn 0.0769 -0.9796 0.1857 +vn 0.3827 -0.0000 0.9239 +vn 0.0769 0.9796 0.1857 +vn 0.3754 -0.1939 0.9063 +vn 0.1484 0.9217 0.3584 +vn 0.3539 -0.3805 0.8544 +vn 0.2142 0.8286 0.5172 +vn 0.3189 -0.5528 0.7699 +vn 0.2718 0.7041 0.6561 +vn 0.3189 0.5528 0.7699 +vn 0.0757 0.9217 0.3804 +vn 0.1804 -0.3805 0.9070 +vn 0.1092 0.8286 0.5490 +vn 0.1626 -0.5528 0.8173 +vn 0.1385 0.7041 0.6965 +vn 0.1385 -0.7041 0.6965 +vn 0.1626 0.5528 0.8173 +vn 0.1092 -0.8286 0.5490 +vn 0.1804 0.3805 0.9070 +vn 0.0757 -0.9217 0.3804 +vn 0.1914 0.1939 0.9622 +vn 0.0392 -0.9796 0.1971 +vn 0.1951 -0.0000 0.9808 +vn 0.0392 0.9796 0.1971 +vn 0.1914 -0.1939 0.9622 +vn -0.0000 0.3805 0.9248 +vn -0.0000 -0.9217 0.3879 +vn -0.0000 0.1939 0.9810 +vn -0.0000 -0.9796 0.2010 +vn -0.0000 -0.0000 1.0000 +vn -0.0000 0.9796 0.2010 +vn -0.0000 -0.1939 0.9810 +vn -0.0000 0.9217 0.3879 +vn -0.0000 -0.3805 0.9248 +vn -0.0000 0.8286 0.5598 +vn -0.0000 -0.5528 0.8333 +vn -0.0000 0.7041 0.7101 +vn -0.0000 -0.7041 0.7101 +vn -0.0000 0.5528 0.8333 +vn -0.0000 -0.8286 0.5598 +vn -0.1804 -0.3805 0.9070 +vn -0.1092 0.8286 0.5490 +vn -0.1626 -0.5528 0.8173 +vn -0.1385 0.7041 0.6965 +vn -0.1385 -0.7041 0.6965 +vn -0.1626 0.5528 0.8173 +vn -0.1092 -0.8286 0.5490 +vn -0.1804 0.3805 0.9070 +vn -0.0757 -0.9217 0.3804 +vn -0.1914 0.1939 0.9622 +vn -0.0392 -0.9796 0.1971 +vn -0.1951 -0.0000 0.9808 +vn -0.0392 0.9796 0.1971 +vn -0.1914 -0.1939 0.9622 +vn -0.0757 0.9217 0.3804 +vn -0.1484 -0.9217 0.3584 +vn -0.3539 0.3805 0.8544 +vn -0.3754 0.1939 0.9063 +vn -0.0769 -0.9796 0.1857 +vn -0.3827 -0.0000 0.9239 +vn -0.0769 0.9796 0.1857 +vn -0.3754 -0.1939 0.9063 +vn -0.1484 0.9217 0.3584 +vn -0.3539 -0.3805 0.8544 +vn -0.2142 0.8286 0.5172 +vn -0.3189 -0.5528 0.7699 +vn -0.2718 0.7041 0.6561 +vn -0.2718 -0.7041 0.6561 +vn -0.3189 0.5528 0.7699 +vn -0.2142 -0.8286 0.5172 +vn -0.5138 -0.3805 0.7689 +vn -0.4630 -0.5528 0.6929 +vn -0.3945 0.7041 0.5905 +vn -0.3945 -0.7041 0.5905 +vn -0.4630 0.5528 0.6929 +vn -0.3110 -0.8286 0.4654 +vn -0.5138 0.3805 0.7689 +vn -0.2155 -0.9217 0.3225 +vn -0.5450 0.1939 0.8157 +vn -0.1117 -0.9796 0.1671 +vn -0.5556 -0.0000 0.8315 +vn -0.1117 0.9796 0.1671 +vn -0.5450 -0.1939 0.8157 +vn -0.2155 0.9217 0.3225 +vn -0.3110 0.8286 0.4654 +vn -0.2743 -0.9217 0.2743 +vn -0.1421 -0.9796 0.1421 +vn -0.6937 0.1939 0.6937 +vn -0.7071 -0.0000 0.7071 +vn -0.1421 0.9796 0.1421 +vn -0.6937 -0.1939 0.6937 +vn -0.2743 0.9217 0.2743 +vn -0.6539 -0.3805 0.6539 +vn -0.3958 0.8286 0.3958 +vn -0.5893 -0.5528 0.5893 +vn -0.5021 0.7041 0.5021 +vn -0.5021 -0.7041 0.5021 +vn -0.5893 0.5528 0.5893 +vn -0.3958 -0.8286 0.3958 +vn -0.6539 0.3805 0.6539 +vn -0.5905 0.7041 0.3945 +vn -0.5905 -0.7041 0.3945 +vn -0.6929 0.5528 0.4630 +vn -0.4654 -0.8286 0.3110 +vn -0.7689 0.3805 0.5138 +vn -0.3225 -0.9217 0.2155 +vn -0.8157 0.1939 0.5450 +vn -0.1671 -0.9796 0.1117 +vn -0.8315 -0.0000 0.5556 +vn -0.1671 0.9796 0.1117 +vn -0.8157 -0.1939 0.5450 +vn -0.3225 0.9217 0.2155 +vn -0.7689 -0.3805 0.5138 +vn -0.4654 0.8286 0.3110 +vn -0.6929 -0.5528 0.4630 +vn -0.9063 0.1939 0.3754 +vn -0.9239 -0.0000 0.3827 +vn -0.1857 0.9796 0.0769 +vn -0.1857 -0.9796 0.0769 +vn -0.9063 -0.1939 0.3754 +vn -0.3584 0.9217 0.1484 +vn -0.8544 -0.3805 0.3539 +vn -0.5172 0.8286 0.2142 +vn -0.7699 -0.5528 0.3189 +vn -0.6561 0.7041 0.2718 +vn -0.6561 -0.7041 0.2718 +vn -0.7699 0.5528 0.3189 +vn -0.5172 -0.8286 0.2142 +vn -0.8544 0.3805 0.3539 +vn -0.3584 -0.9217 0.1484 +vn -0.6965 -0.7041 0.1385 +vn -0.6965 0.7041 0.1385 +vn -0.8173 0.5528 0.1626 +vn -0.5490 -0.8286 0.1092 +vn -0.9070 0.3805 0.1804 +vn -0.3804 -0.9217 0.0757 +vn -0.9622 0.1939 0.1914 +vn -0.1971 -0.9796 0.0392 +vn -0.9808 -0.0000 0.1951 +vn -0.1971 0.9796 0.0392 +vn -0.9622 -0.1939 0.1914 +vn -0.3804 0.9217 0.0757 +vn -0.9070 -0.3805 0.1804 +vn -0.5490 0.8286 0.1092 +vn -0.8173 -0.5528 0.1626 +vn -0.2010 0.9796 -0.0000 +vn -0.2010 -0.9796 -0.0000 +vn -0.9810 -0.1939 -0.0000 +vn -0.3879 0.9217 -0.0000 +vn -0.9248 -0.3805 -0.0000 +vn -0.5598 0.8286 -0.0000 +vn -0.8333 -0.5528 -0.0000 +vn -0.7101 0.7041 -0.0000 +vn -0.7101 -0.7041 -0.0000 +vn -0.8333 0.5528 -0.0000 +vn -0.5598 -0.8286 -0.0000 +vn -0.9248 0.3805 -0.0000 +vn -0.3879 -0.9217 -0.0000 +vn -0.9810 0.1939 -0.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.6965 0.7041 -0.1385 +vn -0.8173 0.5528 -0.1626 +vn -0.6965 -0.7041 -0.1385 +vn -0.5490 -0.8286 -0.1092 +vn -0.9070 0.3805 -0.1804 +vn -0.3804 -0.9217 -0.0757 +vn -0.9622 0.1939 -0.1914 +vn -0.1971 -0.9796 -0.0392 +vn -0.9808 -0.0000 -0.1951 +vn -0.1971 0.9796 -0.0392 +vn -0.9622 -0.1939 -0.1914 +vn -0.3804 0.9217 -0.0757 +vn -0.9070 -0.3805 -0.1804 +vn -0.5490 0.8286 -0.1092 +vn -0.8173 -0.5528 -0.1626 +vn -0.9063 -0.1939 -0.3754 +vn -0.3584 0.9217 -0.1484 +vn -0.8544 -0.3805 -0.3539 +vn -0.5172 0.8286 -0.2142 +vn -0.7699 -0.5528 -0.3189 +vn -0.6561 0.7041 -0.2718 +vn -0.6561 -0.7041 -0.2718 +vn -0.7699 0.5528 -0.3189 +vn -0.5172 -0.8286 -0.2142 +vn -0.8544 0.3805 -0.3539 +vn -0.3584 -0.9217 -0.1484 +vn -0.9063 0.1939 -0.3754 +vn -0.1857 -0.9796 -0.0769 +vn -0.9239 -0.0000 -0.3827 +vn -0.1857 0.9796 -0.0769 +vn -0.5905 -0.7041 -0.3945 +vn -0.4654 -0.8286 -0.3110 +vn -0.7689 0.3805 -0.5138 +vn -0.3225 -0.9217 -0.2155 +vn -0.8157 0.1939 -0.5450 +vn -0.1671 -0.9796 -0.1117 +vn -0.8315 -0.0000 -0.5556 +vn -0.1671 0.9796 -0.1117 +vn -0.8157 -0.1939 -0.5450 +vn -0.3225 0.9217 -0.2155 +vn -0.7689 -0.3805 -0.5138 +vn -0.4654 0.8286 -0.3110 +vn -0.6929 -0.5528 -0.4630 +vn -0.5905 0.7041 -0.3945 +vn -0.6929 0.5528 -0.4630 +vn -0.6539 -0.3805 -0.6539 +vn -0.2743 0.9217 -0.2743 +vn -0.3958 0.8286 -0.3958 +vn -0.5893 -0.5528 -0.5893 +vn -0.5021 0.7041 -0.5021 +vn -0.5021 -0.7041 -0.5021 +vn -0.5893 0.5528 -0.5893 +vn -0.3958 -0.8286 -0.3958 +vn -0.6539 0.3805 -0.6539 +vn -0.2743 -0.9217 -0.2743 +vn -0.6937 0.1939 -0.6937 +vn -0.1421 -0.9796 -0.1421 +vn -0.7071 -0.0000 -0.7071 +vn -0.1421 0.9796 -0.1421 +vn -0.6937 -0.1939 -0.6937 +vn -0.2155 -0.9217 -0.3225 +vn -0.5138 0.3805 -0.7689 +vn -0.5450 0.1939 -0.8157 +vn -0.1117 -0.9796 -0.1671 +vn -0.5556 -0.0000 -0.8315 +vn -0.1117 0.9796 -0.1671 +vn -0.5450 -0.1939 -0.8157 +vn -0.2155 0.9217 -0.3225 +vn -0.5138 -0.3805 -0.7689 +vn -0.3110 0.8286 -0.4654 +vn -0.4630 -0.5528 -0.6929 +vn -0.3945 0.7041 -0.5905 +vn -0.3945 -0.7041 -0.5905 +vn -0.4630 0.5528 -0.6929 +vn -0.3110 -0.8286 -0.4654 +vn -0.2142 0.8286 -0.5172 +vn -0.3539 -0.3805 -0.8544 +vn -0.3189 -0.5528 -0.7699 +vn -0.2718 0.7041 -0.6561 +vn -0.2718 -0.7041 -0.6561 +vn -0.3189 0.5528 -0.7699 +vn -0.2142 -0.8286 -0.5172 +vn -0.3539 0.3805 -0.8544 +vn -0.1484 -0.9217 -0.3584 +vn -0.3754 0.1939 -0.9063 +vn -0.0769 -0.9796 -0.1857 +vn -0.3827 -0.0000 -0.9239 +vn -0.0769 0.9796 -0.1857 +vn -0.3754 -0.1939 -0.9063 +vn -0.1484 0.9217 -0.3584 +vn -0.1804 0.3805 -0.9070 +vn -0.1914 0.1939 -0.9622 +vn -0.0757 -0.9217 -0.3804 +vn -0.0392 -0.9796 -0.1971 +vn -0.1951 -0.0000 -0.9808 +vn -0.0392 0.9796 -0.1971 +vn -0.1914 -0.1939 -0.9622 +vn -0.0757 0.9217 -0.3804 +vn -0.1804 -0.3805 -0.9070 +vn -0.1092 0.8286 -0.5490 +vn -0.1626 -0.5528 -0.8173 +vn -0.1385 0.7041 -0.6965 +vn -0.1385 -0.7041 -0.6965 +vn -0.1626 0.5528 -0.8173 +vn -0.1092 -0.8286 -0.5490 +vt 0.750000 0.812500 +vt 0.750000 0.687500 +vt 0.750000 0.562500 +vt 0.750000 0.500000 +vt 0.750000 0.437500 +vt 0.750000 0.312500 +vt 0.718750 0.937500 +vt 0.718750 0.875000 +vt 0.718750 0.812500 +vt 0.718750 0.750000 +vt 0.718750 0.687500 +vt 0.718750 0.625000 +vt 0.718750 0.562500 +vt 0.718750 0.500000 +vt 0.718750 0.437500 +vt 0.718750 0.375000 +vt 0.718750 0.312500 +vt 0.718750 0.250000 +vt 0.718750 0.187500 +vt 0.718750 0.125000 +vt 0.718750 0.062500 +vt 0.687500 0.937500 +vt 0.687500 0.875000 +vt 0.687500 0.812500 +vt 0.687500 0.750000 +vt 0.687500 0.687500 +vt 0.687500 0.625000 +vt 0.687500 0.562500 +vt 0.687500 0.500000 +vt 0.687500 0.437500 +vt 0.687500 0.375000 +vt 0.687500 0.312500 +vt 0.687500 0.250000 +vt 0.687500 0.187500 +vt 0.687500 0.125000 +vt 0.687500 0.062500 +vt 0.656250 0.937500 +vt 0.656250 0.875000 +vt 0.656250 0.812500 +vt 0.656250 0.750000 +vt 0.656250 0.687500 +vt 0.656250 0.625000 +vt 0.656250 0.562500 +vt 0.656250 0.500000 +vt 0.656250 0.437500 +vt 0.656250 0.375000 +vt 0.656250 0.312500 +vt 0.656250 0.250000 +vt 0.656250 0.187500 +vt 0.656250 0.125000 +vt 0.656250 0.062500 +vt 0.625000 0.937500 +vt 0.625000 0.875000 +vt 0.625000 0.812500 +vt 0.625000 0.750000 +vt 0.625000 0.687500 +vt 0.625000 0.625000 +vt 0.625000 0.562500 +vt 0.625000 0.500000 +vt 0.625000 0.437500 +vt 0.625000 0.375000 +vt 0.625000 0.312500 +vt 0.625000 0.250000 +vt 0.625000 0.187500 +vt 0.625000 0.125000 +vt 0.625000 0.062500 +vt 0.593750 0.937500 +vt 0.593750 0.875000 +vt 0.593750 0.812500 +vt 0.593750 0.750000 +vt 0.593750 0.687500 +vt 0.593750 0.625000 +vt 0.593750 0.562500 +vt 0.593750 0.500000 +vt 0.593750 0.437500 +vt 0.593750 0.375000 +vt 0.593750 0.312500 +vt 0.593750 0.250000 +vt 0.593750 0.187500 +vt 0.593750 0.125000 +vt 0.593750 0.062500 +vt 0.734375 1.000000 +vt 0.703125 1.000000 +vt 0.671875 1.000000 +vt 0.640625 1.000000 +vt 0.609375 1.000000 +vt 0.578125 1.000000 +vt 0.546875 1.000000 +vt 0.515625 1.000000 +vt 0.484375 1.000000 +vt 0.453125 1.000000 +vt 0.421875 1.000000 +vt 0.390625 1.000000 +vt 0.359375 1.000000 +vt 0.328125 1.000000 +vt 0.296875 1.000000 +vt 0.265625 1.000000 +vt 0.234375 1.000000 +vt 0.203125 1.000000 +vt 0.171875 1.000000 +vt 0.140625 1.000000 +vt 0.109375 1.000000 +vt 0.078125 1.000000 +vt 0.046875 1.000000 +vt 0.015625 1.000000 +vt 0.984375 1.000000 +vt 0.953125 1.000000 +vt 0.921875 1.000000 +vt 0.890625 1.000000 +vt 0.859375 1.000000 +vt 0.828125 1.000000 +vt 0.796875 1.000000 +vt 0.765625 1.000000 +vt 0.562500 0.937500 +vt 0.562500 0.875000 +vt 0.562500 0.812500 +vt 0.562500 0.750000 +vt 0.562500 0.687500 +vt 0.562500 0.625000 +vt 0.562500 0.562500 +vt 0.562500 0.500000 +vt 0.562500 0.437500 +vt 0.562500 0.375000 +vt 0.562500 0.312500 +vt 0.562500 0.250000 +vt 0.562500 0.187500 +vt 0.562500 0.125000 +vt 0.562500 0.062500 +vt 0.531250 0.937500 +vt 0.531250 0.875000 +vt 0.531250 0.812500 +vt 0.531250 0.750000 +vt 0.531250 0.687500 +vt 0.531250 0.625000 +vt 0.531250 0.562500 +vt 0.531250 0.500000 +vt 0.531250 0.437500 +vt 0.531250 0.375000 +vt 0.531250 0.312500 +vt 0.531250 0.250000 +vt 0.531250 0.187500 +vt 0.531250 0.125000 +vt 0.531250 0.062500 +vt 0.500000 0.937500 +vt 0.500000 0.875000 +vt 0.500000 0.812500 +vt 0.500000 0.750000 +vt 0.500000 0.687500 +vt 0.500000 0.625000 +vt 0.500000 0.562500 +vt 0.500000 0.500000 +vt 0.500000 0.437500 +vt 0.500000 0.375000 +vt 0.500000 0.312500 +vt 0.500000 0.250000 +vt 0.500000 0.187500 +vt 0.500000 0.125000 +vt 0.500000 0.062500 +vt 0.468750 0.937500 +vt 0.468750 0.875000 +vt 0.468750 0.812500 +vt 0.468750 0.750000 +vt 0.468750 0.687500 +vt 0.468750 0.625000 +vt 0.468750 0.562500 +vt 0.468750 0.500000 +vt 0.468750 0.437500 +vt 0.468750 0.375000 +vt 0.468750 0.312500 +vt 0.468750 0.250000 +vt 0.468750 0.187500 +vt 0.468750 0.125000 +vt 0.468750 0.062500 +vt 0.437500 0.937500 +vt 0.437500 0.875000 +vt 0.437500 0.812500 +vt 0.437500 0.750000 +vt 0.437500 0.687500 +vt 0.437500 0.625000 +vt 0.437500 0.562500 +vt 0.437500 0.500000 +vt 0.437500 0.437500 +vt 0.437500 0.375000 +vt 0.437500 0.312500 +vt 0.437500 0.250000 +vt 0.437500 0.187500 +vt 0.437500 0.125000 +vt 0.437500 0.062500 +vt 0.406250 0.937500 +vt 0.406250 0.875000 +vt 0.406250 0.812500 +vt 0.406250 0.750000 +vt 0.406250 0.687500 +vt 0.406250 0.625000 +vt 0.406250 0.562500 +vt 0.406250 0.500000 +vt 0.406250 0.437500 +vt 0.406250 0.375000 +vt 0.406250 0.312500 +vt 0.406250 0.250000 +vt 0.406250 0.187500 +vt 0.406250 0.125000 +vt 0.406250 0.062500 +vt 0.375000 0.937500 +vt 0.375000 0.875000 +vt 0.375000 0.812500 +vt 0.375000 0.750000 +vt 0.375000 0.687500 +vt 0.375000 0.625000 +vt 0.375000 0.562500 +vt 0.375000 0.500000 +vt 0.375000 0.437500 +vt 0.375000 0.375000 +vt 0.375000 0.312500 +vt 0.375000 0.250000 +vt 0.375000 0.187500 +vt 0.375000 0.125000 +vt 0.375000 0.062500 +vt 0.343750 0.937500 +vt 0.343750 0.875000 +vt 0.343750 0.812500 +vt 0.343750 0.750000 +vt 0.343750 0.687500 +vt 0.343750 0.625000 +vt 0.343750 0.562500 +vt 0.343750 0.500000 +vt 0.343750 0.437500 +vt 0.343750 0.375000 +vt 0.343750 0.312500 +vt 0.343750 0.250000 +vt 0.343750 0.187500 +vt 0.343750 0.125000 +vt 0.343750 0.062500 +vt 0.312500 0.937500 +vt 0.312500 0.875000 +vt 0.312500 0.812500 +vt 0.312500 0.750000 +vt 0.312500 0.687500 +vt 0.312500 0.625000 +vt 0.312500 0.562500 +vt 0.312500 0.500000 +vt 0.312500 0.437500 +vt 0.312500 0.375000 +vt 0.312500 0.312500 +vt 0.312500 0.250000 +vt 0.312500 0.187500 +vt 0.312500 0.125000 +vt 0.312500 0.062500 +vt 0.281250 0.937500 +vt 0.281250 0.875000 +vt 0.281250 0.812500 +vt 0.281250 0.750000 +vt 0.281250 0.687500 +vt 0.281250 0.625000 +vt 0.281250 0.562500 +vt 0.281250 0.500000 +vt 0.281250 0.437500 +vt 0.281250 0.375000 +vt 0.281250 0.312500 +vt 0.281250 0.250000 +vt 0.281250 0.187500 +vt 0.281250 0.125000 +vt 0.281250 0.062500 +vt 0.250000 0.937500 +vt 0.250000 0.875000 +vt 0.250000 0.812500 +vt 0.250000 0.750000 +vt 0.250000 0.687500 +vt 0.250000 0.625000 +vt 0.250000 0.562500 +vt 0.250000 0.500000 +vt 0.250000 0.437500 +vt 0.250000 0.375000 +vt 0.250000 0.312500 +vt 0.250000 0.250000 +vt 0.250000 0.187500 +vt 0.250000 0.125000 +vt 0.250000 0.062500 +vt 0.218750 0.937500 +vt 0.218750 0.875000 +vt 0.218750 0.812500 +vt 0.218750 0.750000 +vt 0.218750 0.687500 +vt 0.218750 0.625000 +vt 0.218750 0.562500 +vt 0.218750 0.500000 +vt 0.218750 0.437500 +vt 0.218750 0.375000 +vt 0.218750 0.312500 +vt 0.218750 0.250000 +vt 0.218750 0.187500 +vt 0.218750 0.125000 +vt 0.218750 0.062500 +vt 0.187500 0.937500 +vt 0.187500 0.875000 +vt 0.187500 0.812500 +vt 0.187500 0.750000 +vt 0.187500 0.687500 +vt 0.187500 0.625000 +vt 0.187500 0.562500 +vt 0.187500 0.500000 +vt 0.187500 0.437500 +vt 0.187500 0.375000 +vt 0.187500 0.312500 +vt 0.187500 0.250000 +vt 0.187500 0.187500 +vt 0.187500 0.125000 +vt 0.187500 0.062500 +vt 0.156250 0.937500 +vt 0.156250 0.875000 +vt 0.156250 0.812500 +vt 0.156250 0.750000 +vt 0.156250 0.687500 +vt 0.156250 0.625000 +vt 0.156250 0.562500 +vt 0.156250 0.500000 +vt 0.156250 0.437500 +vt 0.156250 0.375000 +vt 0.156250 0.312500 +vt 0.156250 0.250000 +vt 0.156250 0.187500 +vt 0.156250 0.125000 +vt 0.156250 0.062500 +vt 0.125000 0.937500 +vt 0.125000 0.875000 +vt 0.125000 0.812500 +vt 0.125000 0.750000 +vt 0.125000 0.687500 +vt 0.125000 0.625000 +vt 0.125000 0.562500 +vt 0.125000 0.500000 +vt 0.125000 0.437500 +vt 0.125000 0.375000 +vt 0.125000 0.312500 +vt 0.125000 0.250000 +vt 0.125000 0.187500 +vt 0.125000 0.125000 +vt 0.125000 0.062500 +vt 0.734375 0.000000 +vt 0.703125 0.000000 +vt 0.671875 0.000000 +vt 0.640625 0.000000 +vt 0.609375 0.000000 +vt 0.578125 0.000000 +vt 0.546875 0.000000 +vt 0.515625 0.000000 +vt 0.484375 0.000000 +vt 0.453125 0.000000 +vt 0.421875 0.000000 +vt 0.390625 0.000000 +vt 0.359375 0.000000 +vt 0.328125 0.000000 +vt 0.296875 0.000000 +vt 0.265625 0.000000 +vt 0.234375 0.000000 +vt 0.203125 0.000000 +vt 0.171875 0.000000 +vt 0.140625 0.000000 +vt 0.109375 0.000000 +vt 0.078125 0.000000 +vt 0.046875 0.000000 +vt 0.015625 0.000000 +vt 0.984375 0.000000 +vt 0.953125 0.000000 +vt 0.921875 0.000000 +vt 0.890625 0.000000 +vt 0.859375 0.000000 +vt 0.828125 0.000000 +vt 0.796875 0.000000 +vt 0.765625 0.000000 +vt 0.093750 0.937500 +vt 0.093750 0.875000 +vt 0.093750 0.812500 +vt 0.093750 0.750000 +vt 0.093750 0.687500 +vt 0.093750 0.625000 +vt 0.093750 0.562500 +vt 0.093750 0.500000 +vt 0.093750 0.437500 +vt 0.093750 0.375000 +vt 0.093750 0.312500 +vt 0.093750 0.250000 +vt 0.093750 0.187500 +vt 0.093750 0.125000 +vt 0.093750 0.062500 +vt 0.062500 0.937500 +vt 0.062500 0.875000 +vt 0.062500 0.812500 +vt 0.062500 0.750000 +vt 0.062500 0.687500 +vt 0.062500 0.625000 +vt 0.062500 0.562500 +vt 0.062500 0.500000 +vt 0.062500 0.437500 +vt 0.062500 0.375000 +vt 0.062500 0.312500 +vt 0.062500 0.250000 +vt 0.062500 0.187500 +vt 0.062500 0.125000 +vt 0.062500 0.062500 +vt 0.031250 0.937500 +vt 0.031250 0.875000 +vt 0.031250 0.812500 +vt 0.031250 0.750000 +vt 0.031250 0.687500 +vt 0.031250 0.625000 +vt 0.031250 0.562500 +vt 0.031250 0.500000 +vt 0.031250 0.437500 +vt 0.031250 0.375000 +vt 0.031250 0.312500 +vt 0.031250 0.250000 +vt 0.031250 0.187500 +vt 0.031250 0.125000 +vt 0.031250 0.062500 +vt 0.000000 0.937500 +vt 1.000000 0.937500 +vt 0.000000 0.875000 +vt 1.000000 0.875000 +vt 0.000000 0.812500 +vt 1.000000 0.812500 +vt 0.000000 0.750000 +vt 1.000000 0.750000 +vt 0.000000 0.687500 +vt 1.000000 0.687500 +vt 0.000000 0.625000 +vt 1.000000 0.625000 +vt 0.000000 0.562500 +vt 1.000000 0.562500 +vt 0.000000 0.500000 +vt 1.000000 0.500000 +vt 0.000000 0.437500 +vt 1.000000 0.437500 +vt 0.000000 0.375000 +vt 1.000000 0.375000 +vt 0.000000 0.312500 +vt 1.000000 0.312500 +vt 0.000000 0.250000 +vt 1.000000 0.250000 +vt 0.000000 0.187500 +vt 1.000000 0.187500 +vt 0.000000 0.125000 +vt 1.000000 0.125000 +vt 1.000000 0.062500 +vt 0.000000 0.062500 +vt 0.968750 0.937500 +vt 0.968750 0.875000 +vt 0.968750 0.812500 +vt 0.968750 0.750000 +vt 0.968750 0.687500 +vt 0.968750 0.625000 +vt 0.968750 0.562500 +vt 0.968750 0.500000 +vt 0.968750 0.437500 +vt 0.968750 0.375000 +vt 0.968750 0.312500 +vt 0.968750 0.250000 +vt 0.968750 0.187500 +vt 0.968750 0.125000 +vt 0.968750 0.062500 +vt 0.937500 0.937500 +vt 0.937500 0.875000 +vt 0.937500 0.812500 +vt 0.937500 0.750000 +vt 0.937500 0.687500 +vt 0.937500 0.625000 +vt 0.937500 0.562500 +vt 0.937500 0.500000 +vt 0.937500 0.437500 +vt 0.937500 0.375000 +vt 0.937500 0.312500 +vt 0.937500 0.250000 +vt 0.937500 0.187500 +vt 0.937500 0.125000 +vt 0.937500 0.062500 +vt 0.906250 0.937500 +vt 0.906250 0.875000 +vt 0.906250 0.812500 +vt 0.906250 0.750000 +vt 0.906250 0.687500 +vt 0.906250 0.625000 +vt 0.906250 0.562500 +vt 0.906250 0.500000 +vt 0.906250 0.437500 +vt 0.906250 0.375000 +vt 0.906250 0.312500 +vt 0.906250 0.250000 +vt 0.906250 0.187500 +vt 0.906250 0.125000 +vt 0.906250 0.062500 +vt 0.875000 0.937500 +vt 0.875000 0.875000 +vt 0.875000 0.812500 +vt 0.875000 0.750000 +vt 0.875000 0.687500 +vt 0.875000 0.625000 +vt 0.875000 0.562500 +vt 0.875000 0.500000 +vt 0.875000 0.437500 +vt 0.875000 0.375000 +vt 0.875000 0.312500 +vt 0.875000 0.250000 +vt 0.875000 0.187500 +vt 0.875000 0.125000 +vt 0.875000 0.062500 +vt 0.843750 0.937500 +vt 0.843750 0.875000 +vt 0.843750 0.812500 +vt 0.843750 0.750000 +vt 0.843750 0.687500 +vt 0.843750 0.625000 +vt 0.843750 0.562500 +vt 0.843750 0.500000 +vt 0.843750 0.437500 +vt 0.843750 0.375000 +vt 0.843750 0.312500 +vt 0.843750 0.250000 +vt 0.843750 0.187500 +vt 0.843750 0.125000 +vt 0.843750 0.062500 +vt 0.812500 0.937500 +vt 0.812500 0.875000 +vt 0.812500 0.812500 +vt 0.812500 0.750000 +vt 0.812500 0.687500 +vt 0.812500 0.625000 +vt 0.812500 0.562500 +vt 0.812500 0.500000 +vt 0.812500 0.437500 +vt 0.812500 0.375000 +vt 0.812500 0.312500 +vt 0.812500 0.250000 +vt 0.812500 0.187500 +vt 0.812500 0.125000 +vt 0.812500 0.062500 +vt 0.781250 0.937500 +vt 0.781250 0.875000 +vt 0.781250 0.812500 +vt 0.781250 0.750000 +vt 0.781250 0.687500 +vt 0.781250 0.625000 +vt 0.781250 0.562500 +vt 0.781250 0.500000 +vt 0.781250 0.437500 +vt 0.781250 0.375000 +vt 0.781250 0.312500 +vt 0.781250 0.250000 +vt 0.781250 0.187500 +vt 0.781250 0.125000 +vt 0.781250 0.062500 +vt 0.750000 0.937500 +vt 0.750000 0.875000 +vt 0.750000 0.750000 +vt 0.750000 0.625000 +vt 0.750000 0.375000 +vt 0.750000 0.250000 +vt 0.750000 0.187500 +vt 0.750000 0.125000 +vt 0.750000 0.062500 +s 1 +f 1/1/1 8/8/2 9/9/3 +f 478/555/4 17/17/5 6/6/6 +f 476/553/7 9/9/3 10/10/8 +f 6/6/6 18/18/9 479/556/10 +f 2/2/11 10/10/8 11/11/12 +f 479/556/10 19/19/13 480/557/14 +f 477/554/15 11/11/12 12/12/16 +f 480/557/14 20/20/17 481/558/18 +f 3/3/19 12/12/16 13/13/20 +f 481/558/18 21/21/21 482/559/22 +f 3/3/19 14/14/23 4/4/24 +f 474/551/25 82/82/26 7/7/27 +f 308/339/28 482/559/22 21/21/21 +f 4/4/24 15/15/29 5/5/30 +f 475/552/31 7/7/27 8/8/2 +f 5/5/30 16/16/32 478/555/4 +f 7/7/27 23/23/33 8/8/2 +f 15/15/29 31/31/34 16/16/32 +f 8/8/2 24/24/35 9/9/3 +f 16/16/32 32/32/36 17/17/5 +f 9/9/3 25/25/37 10/10/8 +f 17/17/5 33/33/38 18/18/9 +f 10/10/8 26/26/39 11/11/12 +f 19/19/13 33/33/38 34/34/40 +f 11/11/12 27/27/41 12/12/16 +f 20/20/17 34/34/40 35/35/42 +f 13/13/20 27/27/41 28/28/43 +f 20/20/17 36/36/44 21/21/21 +f 13/13/20 29/29/45 14/14/23 +f 7/7/27 82/83/26 22/22/46 +f 308/340/28 21/21/21 36/36/44 +f 14/14/23 30/30/47 15/15/29 +f 26/26/39 42/42/48 27/27/41 +f 34/34/40 50/50/49 35/35/42 +f 27/27/41 43/43/50 28/28/43 +f 35/35/42 51/51/51 36/36/44 +f 28/28/43 44/44/52 29/29/45 +f 22/22/46 82/84/26 37/37/53 +f 308/341/28 36/36/44 51/51/51 +f 30/30/47 44/44/52 45/45/54 +f 22/22/46 38/38/55 23/23/33 +f 30/30/47 46/46/56 31/31/34 +f 23/23/33 39/39/57 24/24/35 +f 32/32/36 46/46/56 47/47/58 +f 24/24/35 40/40/59 25/25/37 +f 33/33/38 47/47/58 48/48/60 +f 25/25/37 41/41/61 26/26/39 +f 34/34/40 48/48/60 49/49/62 +f 45/45/54 61/61/63 46/46/56 +f 38/38/55 54/54/64 39/39/57 +f 47/47/58 61/61/63 62/62/65 +f 39/39/57 55/55/66 40/40/59 +f 47/47/58 63/63/67 48/48/60 +f 40/40/59 56/56/68 41/41/61 +f 48/48/60 64/64/69 49/49/62 +f 41/41/61 57/57/70 42/42/48 +f 49/49/62 65/65/71 50/50/49 +f 43/43/50 57/57/70 58/58/72 +f 50/50/49 66/66/73 51/51/51 +f 44/44/52 58/58/72 59/59/74 +f 37/37/53 82/85/26 52/52/75 +f 308/342/28 51/51/51 66/66/73 +f 44/44/52 60/60/76 45/45/54 +f 37/37/53 53/53/77 38/38/55 +f 65/65/71 79/79/78 80/80/79 +f 58/58/72 72/72/80 73/73/81 +f 65/65/71 81/81/82 66/66/73 +f 59/59/74 73/73/81 74/74/83 +f 52/52/75 82/86/26 67/67/84 +f 308/343/28 66/66/73 81/81/82 +f 59/59/74 75/75/85 60/60/76 +f 52/52/75 68/68/86 53/53/77 +f 60/60/76 76/76/87 61/61/63 +f 53/53/77 69/69/88 54/54/64 +f 62/62/65 76/76/87 77/77/89 +f 54/54/64 70/70/90 55/55/66 +f 62/62/65 78/78/91 63/63/67 +f 56/56/68 70/70/90 71/71/92 +f 63/63/67 79/79/78 64/64/69 +f 56/56/68 72/72/80 57/57/70 +f 68/68/86 85/116/93 69/69/88 +f 77/77/89 92/123/94 93/124/95 +f 69/69/88 86/117/96 70/70/90 +f 77/77/89 94/125/97 78/78/91 +f 71/71/92 86/117/96 87/118/98 +f 78/78/91 95/126/99 79/79/78 +f 71/71/92 88/119/100 72/72/80 +f 79/79/78 96/127/101 80/80/79 +f 73/73/81 88/119/100 89/120/102 +f 81/81/82 96/127/101 97/128/103 +f 74/74/83 89/120/102 90/121/104 +f 67/67/84 82/87/26 83/114/105 +f 308/344/28 81/81/82 97/128/103 +f 74/74/83 91/122/106 75/75/85 +f 67/67/84 84/115/107 68/68/86 +f 75/75/85 92/123/94 76/76/87 +f 89/120/102 103/134/108 104/135/109 +f 96/127/101 112/143/110 97/128/103 +f 90/121/104 104/135/109 105/136/111 +f 83/114/105 82/88/26 98/129/112 +f 308/345/28 97/128/103 112/143/110 +f 90/121/104 106/137/113 91/122/106 +f 84/115/107 98/129/112 99/130/114 +f 91/122/106 107/138/115 92/123/94 +f 84/115/107 100/131/116 85/116/93 +f 93/124/95 107/138/115 108/139/117 +f 85/116/93 101/132/118 86/117/96 +f 93/124/95 109/140/119 94/125/97 +f 87/118/98 101/132/118 102/133/120 +f 95/126/99 109/140/119 110/141/121 +f 87/118/98 103/134/108 88/119/100 +f 96/127/101 110/141/121 111/142/122 +f 108/139/117 122/153/123 123/154/124 +f 100/131/116 116/147/125 101/132/118 +f 108/139/117 124/155/126 109/140/119 +f 102/133/120 116/147/125 117/148/127 +f 110/141/121 124/155/126 125/156/128 +f 102/133/120 118/149/129 103/134/108 +f 110/141/121 126/157/130 111/142/122 +f 104/135/109 118/149/129 119/150/131 +f 111/142/122 127/158/132 112/143/110 +f 105/136/111 119/150/131 120/151/133 +f 98/129/112 82/89/26 113/144/134 +f 308/346/28 112/143/110 127/158/132 +f 105/136/111 121/152/135 106/137/113 +f 99/130/114 113/144/134 114/145/136 +f 106/137/113 122/153/123 107/138/115 +f 99/130/114 115/146/137 100/131/116 +f 126/157/130 142/173/138 127/158/132 +f 120/151/133 134/165/139 135/166/140 +f 113/144/134 82/90/26 128/159/141 +f 308/347/28 127/158/132 142/173/138 +f 120/151/133 136/167/142 121/152/135 +f 113/144/134 129/160/143 114/145/136 +f 121/152/135 137/168/144 122/153/123 +f 114/145/136 130/161/145 115/146/137 +f 123/154/124 137/168/144 138/169/146 +f 115/146/137 131/162/147 116/147/125 +f 123/154/124 139/170/148 124/155/126 +f 117/148/127 131/162/147 132/163/149 +f 125/156/128 139/170/148 140/171/150 +f 117/148/127 133/164/151 118/149/129 +f 125/156/128 141/172/152 126/157/130 +f 119/150/131 133/164/151 134/165/139 +f 138/169/146 154/185/153 139/170/148 +f 132/163/149 146/177/154 147/178/155 +f 140/171/150 154/185/153 155/186/156 +f 132/163/149 148/179/157 133/164/151 +f 140/171/150 156/187/158 141/172/152 +f 134/165/139 148/179/157 149/180/159 +f 142/173/138 156/187/158 157/188/160 +f 135/166/140 149/180/159 150/181/161 +f 128/159/141 82/91/26 143/174/162 +f 308/348/28 142/173/138 157/188/160 +f 135/166/140 151/182/163 136/167/142 +f 128/159/141 144/175/164 129/160/143 +f 136/167/142 152/183/165 137/168/144 +f 130/161/145 144/175/164 145/176/166 +f 138/169/146 152/183/165 153/184/167 +f 130/161/145 146/177/154 131/162/147 +f 143/174/162 82/92/26 158/189/168 +f 308/349/28 157/188/160 172/203/169 +f 150/181/161 166/197/170 151/182/163 +f 143/174/162 159/190/171 144/175/164 +f 151/182/163 167/198/172 152/183/165 +f 145/176/166 159/190/171 160/191/173 +f 153/184/167 167/198/172 168/199/174 +f 145/176/166 161/192/175 146/177/154 +f 153/184/167 169/200/176 154/185/153 +f 147/178/155 161/192/175 162/193/177 +f 155/186/156 169/200/176 170/201/178 +f 147/178/155 163/194/179 148/179/157 +f 155/186/156 171/202/180 156/187/158 +f 149/180/159 163/194/179 164/195/181 +f 156/187/158 172/203/169 157/188/160 +f 150/181/161 164/195/181 165/196/182 +f 162/193/177 176/207/183 177/208/184 +f 169/200/176 185/216/185 170/201/178 +f 162/193/177 178/209/186 163/194/179 +f 171/202/180 185/216/185 186/217/187 +f 164/195/181 178/209/186 179/210/188 +f 172/203/169 186/217/187 187/218/189 +f 165/196/182 179/210/188 180/211/190 +f 158/189/168 82/93/26 173/204/191 +f 308/350/28 172/203/169 187/218/189 +f 165/196/182 181/212/192 166/197/170 +f 158/189/168 174/205/193 159/190/171 +f 166/197/170 182/213/194 167/198/172 +f 159/190/171 175/206/195 160/191/173 +f 168/199/174 182/213/194 183/214/196 +f 160/191/173 176/207/183 161/192/175 +f 168/199/174 184/215/197 169/200/176 +f 180/211/190 196/227/198 181/212/192 +f 174/205/193 188/219/199 189/220/200 +f 181/212/192 197/228/201 182/213/194 +f 175/206/195 189/220/200 190/221/202 +f 183/214/196 197/228/201 198/229/203 +f 175/206/195 191/222/204 176/207/183 +f 183/214/196 199/230/205 184/215/197 +f 177/208/184 191/222/204 192/223/206 +f 185/216/185 199/230/205 200/231/207 +f 177/208/184 193/224/208 178/209/186 +f 185/216/185 201/232/209 186/217/187 +f 179/210/188 193/224/208 194/225/210 +f 186/217/187 202/233/211 187/218/189 +f 180/211/190 194/225/210 195/226/212 +f 173/204/191 82/94/26 188/219/199 +f 308/351/28 187/218/189 202/233/211 +f 200/231/207 214/245/213 215/246/214 +f 192/223/206 208/239/215 193/224/208 +f 200/231/207 216/247/216 201/232/209 +f 194/225/210 208/239/215 209/240/217 +f 201/232/209 217/248/218 202/233/211 +f 195/226/212 209/240/217 210/241/219 +f 188/219/199 82/95/26 203/234/220 +f 308/352/28 202/233/211 217/248/218 +f 195/226/212 211/242/221 196/227/198 +f 188/219/199 204/235/222 189/220/200 +f 196/227/198 212/243/223 197/228/201 +f 189/220/200 205/236/224 190/221/202 +f 198/229/203 212/243/223 213/244/225 +f 190/221/202 206/237/226 191/222/204 +f 198/229/203 214/245/213 199/230/205 +f 192/223/206 206/237/226 207/238/227 +f 203/234/220 219/250/228 204/235/222 +f 211/242/221 227/258/229 212/243/223 +f 204/235/222 220/251/230 205/236/224 +f 213/244/225 227/258/229 228/259/231 +f 205/236/224 221/252/232 206/237/226 +f 213/244/225 229/260/233 214/245/213 +f 207/238/227 221/252/232 222/253/234 +f 215/246/214 229/260/233 230/261/235 +f 207/238/227 223/254/236 208/239/215 +f 215/246/214 231/262/237 216/247/216 +f 209/240/217 223/254/236 224/255/238 +f 216/247/216 232/263/239 217/248/218 +f 210/241/219 224/255/238 225/256/240 +f 203/234/220 82/96/26 218/249/241 +f 308/353/28 217/248/218 232/263/239 +f 210/241/219 226/257/242 211/242/221 +f 222/253/234 238/269/243 223/254/236 +f 230/261/235 246/277/244 231/262/237 +f 224/255/238 238/269/243 239/270/245 +f 232/263/239 246/277/244 247/278/246 +f 225/256/240 239/270/245 240/271/247 +f 218/249/241 82/97/26 233/264/248 +f 308/354/28 232/263/239 247/278/246 +f 225/256/240 241/272/249 226/257/242 +f 218/249/241 234/265/250 219/250/228 +f 226/257/242 242/273/251 227/258/229 +f 220/251/230 234/265/250 235/266/252 +f 228/259/231 242/273/251 243/274/253 +f 220/251/230 236/267/254 221/252/232 +f 228/259/231 244/275/255 229/260/233 +f 222/253/234 236/267/254 237/268/256 +f 230/261/235 244/275/255 245/276/257 +f 241/272/249 257/288/258 242/273/251 +f 234/265/250 250/281/259 235/266/252 +f 243/274/253 257/288/258 258/289/260 +f 235/266/252 251/282/261 236/267/254 +f 243/274/253 259/290/262 244/275/255 +f 237/268/256 251/282/261 252/283/263 +f 245/276/257 259/290/262 260/291/264 +f 237/268/256 253/284/265 238/269/243 +f 245/276/257 261/292/266 246/277/244 +f 239/270/245 253/284/265 254/285/267 +f 246/277/244 262/293/268 247/278/246 +f 240/271/247 254/285/267 255/286/269 +f 233/264/248 82/98/26 248/279/270 +f 308/355/28 247/278/246 262/293/268 +f 240/271/247 256/287/271 241/272/249 +f 233/264/248 249/280/272 234/265/250 +f 260/291/264 276/307/273 261/292/266 +f 254/285/267 268/299/274 269/300/275 +f 261/292/266 277/308/276 262/293/268 +f 255/286/269 269/300/275 270/301/277 +f 248/279/270 82/99/26 263/294/278 +f 308/356/28 262/293/268 277/308/276 +f 255/286/269 271/302/279 256/287/271 +f 249/280/272 263/294/278 264/295/280 +f 256/287/271 272/303/281 257/288/258 +f 249/280/272 265/296/282 250/281/259 +f 258/289/260 272/303/281 273/304/283 +f 250/281/259 266/297/284 251/282/261 +f 258/289/260 274/305/285 259/290/262 +f 252/283/263 266/297/284 267/298/286 +f 260/291/264 274/305/285 275/306/287 +f 252/283/263 268/299/274 253/284/265 +f 273/304/283 287/318/288 288/319/289 +f 265/296/282 281/312/290 266/297/284 +f 273/304/283 289/320/291 274/305/285 +f 267/298/286 281/312/290 282/313/292 +f 275/306/287 289/320/291 290/321/293 +f 267/298/286 283/314/294 268/299/274 +f 275/306/287 291/322/295 276/307/273 +f 269/300/275 283/314/294 284/315/296 +f 277/308/276 291/322/295 292/323/297 +f 270/301/277 284/315/296 285/316/298 +f 263/294/278 82/100/26 278/309/299 +f 308/357/28 277/308/276 292/323/297 +f 270/301/277 286/317/300 271/302/279 +f 264/295/280 278/309/299 279/310/301 +f 271/302/279 287/318/288 272/303/281 +f 264/295/280 280/311/302 265/296/282 +f 292/323/297 306/337/303 307/338/304 +f 285/316/298 299/330/305 300/331/306 +f 278/309/299 82/101/26 293/324/307 +f 308/358/28 292/323/297 307/338/304 +f 285/316/298 301/332/308 286/317/300 +f 278/309/299 294/325/309 279/310/301 +f 286/317/300 302/333/310 287/318/288 +f 279/310/301 295/326/311 280/311/302 +f 288/319/289 302/333/310 303/334/312 +f 280/311/302 296/327/313 281/312/290 +f 288/319/289 304/335/314 289/320/291 +f 282/313/292 296/327/313 297/328/315 +f 290/321/293 304/335/314 305/336/316 +f 282/313/292 298/329/317 283/314/294 +f 290/321/293 306/337/303 291/322/295 +f 284/315/296 298/329/317 299/330/305 +f 295/326/311 312/374/318 296/327/313 +f 303/334/312 320/382/319 304/335/314 +f 297/328/315 312/374/318 313/375/320 +f 305/336/316 320/382/319 321/383/321 +f 297/328/315 314/376/322 298/329/317 +f 305/336/316 322/384/323 306/337/303 +f 299/330/305 314/376/322 315/377/324 +f 306/337/303 323/385/325 307/338/304 +f 300/331/306 315/377/324 316/378/326 +f 293/324/307 82/102/26 309/371/327 +f 308/359/28 307/338/304 323/385/325 +f 300/331/306 317/379/328 301/332/308 +f 293/324/307 310/372/329 294/325/309 +f 301/332/308 318/380/330 302/333/310 +f 294/325/309 311/373/331 295/326/311 +f 303/334/312 318/380/330 319/381/332 +f 316/378/326 330/392/333 331/393/334 +f 309/371/327 82/103/26 324/386/335 +f 308/360/28 323/385/325 338/400/336 +f 316/378/326 332/394/337 317/379/328 +f 309/371/327 325/387/338 310/372/329 +f 317/379/328 333/395/339 318/380/330 +f 310/372/329 326/388/340 311/373/331 +f 319/381/332 333/395/339 334/396/341 +f 311/373/331 327/389/342 312/374/318 +f 319/381/332 335/397/343 320/382/319 +f 312/374/318 328/390/344 313/375/320 +f 321/383/321 335/397/343 336/398/345 +f 313/375/320 329/391/346 314/376/322 +f 321/383/321 337/399/347 322/384/323 +f 315/377/324 329/391/346 330/392/333 +f 322/384/323 338/400/336 323/385/325 +f 334/396/341 350/412/348 335/397/343 +f 328/390/344 342/404/349 343/405/350 +f 336/398/345 350/412/348 351/413/351 +f 328/390/344 344/406/352 329/391/346 +f 336/398/345 352/414/353 337/399/347 +f 330/392/333 344/406/352 345/407/354 +f 337/399/347 353/415/355 338/400/336 +f 331/393/334 345/407/354 346/408/356 +f 324/386/335 82/104/26 339/401/357 +f 308/361/28 338/400/336 353/415/355 +f 331/393/334 347/409/358 332/394/337 +f 324/386/335 340/402/359 325/387/338 +f 332/394/337 348/410/360 333/395/339 +f 325/387/338 341/403/361 326/388/340 +f 334/396/341 348/410/360 349/411/362 +f 326/388/340 342/404/349 327/389/342 +f 339/401/357 82/105/26 354/416/363 +f 308/362/28 353/415/355 368/445/364 +f 346/408/356 362/432/365 347/409/358 +f 339/401/357 355/418/366 340/402/359 +f 347/409/358 363/434/367 348/410/360 +f 341/403/361 355/418/366 356/420/368 +f 349/411/362 363/434/367 364/436/369 +f 341/403/361 357/422/370 342/404/349 +f 349/411/362 365/438/371 350/412/348 +f 343/405/350 357/422/370 358/424/372 +f 351/413/351 365/438/371 366/440/373 +f 343/405/350 359/426/374 344/406/352 +f 351/413/351 367/442/375 352/414/353 +f 345/407/354 359/426/374 360/428/376 +f 352/414/353 368/445/364 353/415/355 +f 346/408/356 360/428/376 361/430/377 +f 358/425/372 372/449/378 373/450/379 +f 366/441/373 380/457/380 381/458/381 +f 358/425/372 374/451/382 359/427/374 +f 366/441/373 382/459/383 367/443/375 +f 360/429/376 374/451/382 375/452/384 +f 367/443/375 383/460/385 368/444/364 +f 361/431/377 375/452/384 376/453/386 +f 354/417/363 82/106/26 369/446/387 +f 308/363/28 368/444/364 383/460/385 +f 361/431/377 377/454/388 362/433/365 +f 354/417/363 370/447/389 355/419/366 +f 362/433/365 378/455/390 363/435/367 +f 356/421/368 370/447/389 371/448/391 +f 364/437/369 378/455/390 379/456/392 +f 356/421/368 372/449/378 357/423/370 +f 364/437/369 380/457/380 365/439/371 +f 376/453/386 392/469/393 377/454/388 +f 369/446/387 385/462/394 370/447/389 +f 377/454/388 393/470/395 378/455/390 +f 371/448/391 385/462/394 386/463/396 +f 379/456/392 393/470/395 394/471/397 +f 371/448/391 387/464/398 372/449/378 +f 379/456/392 395/472/399 380/457/380 +f 373/450/379 387/464/398 388/465/400 +f 381/458/381 395/472/399 396/473/401 +f 373/450/379 389/466/402 374/451/382 +f 381/458/381 397/474/403 382/459/383 +f 375/452/384 389/466/402 390/467/404 +f 382/459/383 398/475/405 383/460/385 +f 376/453/386 390/467/404 391/468/406 +f 369/446/387 82/107/26 384/461/407 +f 308/364/28 383/460/385 398/475/405 +f 396/473/401 410/487/408 411/488/409 +f 388/465/400 404/481/410 389/466/402 +f 397/474/403 411/488/409 412/489/411 +f 390/467/404 404/481/410 405/482/412 +f 397/474/403 413/490/413 398/475/405 +f 391/468/406 405/482/412 406/483/414 +f 384/461/407 82/108/26 399/476/415 +f 308/365/28 398/475/405 413/490/413 +f 391/468/406 407/484/416 392/469/393 +f 385/462/394 399/476/415 400/477/417 +f 392/469/393 408/485/418 393/470/395 +f 385/462/394 401/478/419 386/463/396 +f 394/471/397 408/485/418 409/486/420 +f 386/463/396 402/479/421 387/464/398 +f 394/471/397 410/487/408 395/472/399 +f 388/465/400 402/479/421 403/480/422 +f 407/484/416 423/500/423 408/485/418 +f 401/478/419 415/492/424 416/493/425 +f 409/486/420 423/500/423 424/501/426 +f 401/478/419 417/494/427 402/479/421 +f 409/486/420 425/502/428 410/487/408 +f 403/480/422 417/494/427 418/495/429 +f 411/488/409 425/502/428 426/503/430 +f 403/480/422 419/496/431 404/481/410 +f 411/488/409 427/504/432 412/489/411 +f 405/482/412 419/496/431 420/497/433 +f 412/489/411 428/505/434 413/490/413 +f 406/483/414 420/497/433 421/498/435 +f 399/476/415 82/109/26 414/491/436 +f 308/366/28 413/490/413 428/505/434 +f 406/483/414 422/499/437 407/484/416 +f 399/476/415 415/492/424 400/477/417 +f 426/503/430 442/519/438 427/504/432 +f 420/497/433 434/511/439 435/512/440 +f 427/504/432 443/520/441 428/505/434 +f 421/498/435 435/512/440 436/513/442 +f 414/491/436 82/110/26 429/506/443 +f 308/367/28 428/505/434 443/520/441 +f 421/498/435 437/514/444 422/499/437 +f 414/491/436 430/507/445 415/492/424 +f 422/499/437 438/515/446 423/500/423 +f 416/493/425 430/507/445 431/508/447 +f 424/501/426 438/515/446 439/516/448 +f 416/493/425 432/509/449 417/494/427 +f 424/501/426 440/517/450 425/502/428 +f 418/495/429 432/509/449 433/510/451 +f 426/503/430 440/517/450 441/518/452 +f 418/495/429 434/511/439 419/496/431 +f 430/507/445 446/523/453 431/508/447 +f 439/516/448 453/530/454 454/531/455 +f 431/508/447 447/524/456 432/509/449 +f 439/516/448 455/532/457 440/517/450 +f 433/510/451 447/524/456 448/525/458 +f 441/518/452 455/532/457 456/533/459 +f 433/510/451 449/526/460 434/511/439 +f 441/518/452 457/534/461 442/519/438 +f 435/512/440 449/526/460 450/527/462 +f 442/519/438 458/535/463 443/520/441 +f 436/513/442 450/527/462 451/528/464 +f 429/506/443 82/111/26 444/521/465 +f 308/368/28 443/520/441 458/535/463 +f 436/513/442 452/529/466 437/514/444 +f 430/507/445 444/521/465 445/522/467 +f 437/514/444 453/530/454 438/515/446 +f 450/527/462 464/541/468 465/542/469 +f 458/535/463 472/549/470 473/550/471 +f 451/528/464 465/542/469 466/543/472 +f 444/521/465 82/112/26 459/536/473 +f 308/369/28 458/535/463 473/550/471 +f 451/528/464 467/544/474 452/529/466 +f 444/521/465 460/537/475 445/522/467 +f 452/529/466 468/545/476 453/530/454 +f 446/523/453 460/537/475 461/538/477 +f 454/531/455 468/545/476 469/546/478 +f 446/523/453 462/539/479 447/524/456 +f 454/531/455 470/547/480 455/532/457 +f 448/525/458 462/539/479 463/540/481 +f 456/533/459 470/547/480 471/548/482 +f 448/525/458 464/541/468 449/526/460 +f 456/533/459 472/549/470 457/534/461 +f 468/545/476 6/6/6 469/546/478 +f 462/539/479 1/1/1 476/553/7 +f 469/546/478 479/556/10 470/547/480 +f 463/540/481 476/553/7 2/2/11 +f 471/548/482 479/556/10 480/557/14 +f 464/541/468 2/2/11 477/554/15 +f 471/548/482 481/558/18 472/549/470 +f 465/542/469 477/554/15 3/3/19 +f 472/549/470 482/559/22 473/550/471 +f 466/543/472 3/3/19 4/4/24 +f 459/536/473 82/113/26 474/551/25 +f 308/370/28 473/550/471 482/559/22 +f 466/543/472 5/5/30 467/544/474 +f 460/537/475 474/551/25 475/552/31 +f 467/544/474 478/555/4 468/545/476 +f 461/538/477 475/552/31 1/1/1 +f 1/1/1 475/552/31 8/8/2 +f 478/555/4 16/16/32 17/17/5 +f 476/553/7 1/1/1 9/9/3 +f 6/6/6 17/17/5 18/18/9 +f 2/2/11 476/553/7 10/10/8 +f 479/556/10 18/18/9 19/19/13 +f 477/554/15 2/2/11 11/11/12 +f 480/557/14 19/19/13 20/20/17 +f 3/3/19 477/554/15 12/12/16 +f 481/558/18 20/20/17 21/21/21 +f 3/3/19 13/13/20 14/14/23 +f 4/4/24 14/14/23 15/15/29 +f 475/552/31 474/551/25 7/7/27 +f 5/5/30 15/15/29 16/16/32 +f 7/7/27 22/22/46 23/23/33 +f 15/15/29 30/30/47 31/31/34 +f 8/8/2 23/23/33 24/24/35 +f 16/16/32 31/31/34 32/32/36 +f 9/9/3 24/24/35 25/25/37 +f 17/17/5 32/32/36 33/33/38 +f 10/10/8 25/25/37 26/26/39 +f 19/19/13 18/18/9 33/33/38 +f 11/11/12 26/26/39 27/27/41 +f 20/20/17 19/19/13 34/34/40 +f 13/13/20 12/12/16 27/27/41 +f 20/20/17 35/35/42 36/36/44 +f 13/13/20 28/28/43 29/29/45 +f 14/14/23 29/29/45 30/30/47 +f 26/26/39 41/41/61 42/42/48 +f 34/34/40 49/49/62 50/50/49 +f 27/27/41 42/42/48 43/43/50 +f 35/35/42 50/50/49 51/51/51 +f 28/28/43 43/43/50 44/44/52 +f 30/30/47 29/29/45 44/44/52 +f 22/22/46 37/37/53 38/38/55 +f 30/30/47 45/45/54 46/46/56 +f 23/23/33 38/38/55 39/39/57 +f 32/32/36 31/31/34 46/46/56 +f 24/24/35 39/39/57 40/40/59 +f 33/33/38 32/32/36 47/47/58 +f 25/25/37 40/40/59 41/41/61 +f 34/34/40 33/33/38 48/48/60 +f 45/45/54 60/60/76 61/61/63 +f 38/38/55 53/53/77 54/54/64 +f 47/47/58 46/46/56 61/61/63 +f 39/39/57 54/54/64 55/55/66 +f 47/47/58 62/62/65 63/63/67 +f 40/40/59 55/55/66 56/56/68 +f 48/48/60 63/63/67 64/64/69 +f 41/41/61 56/56/68 57/57/70 +f 49/49/62 64/64/69 65/65/71 +f 43/43/50 42/42/48 57/57/70 +f 50/50/49 65/65/71 66/66/73 +f 44/44/52 43/43/50 58/58/72 +f 44/44/52 59/59/74 60/60/76 +f 37/37/53 52/52/75 53/53/77 +f 65/65/71 64/64/69 79/79/78 +f 58/58/72 57/57/70 72/72/80 +f 65/65/71 80/80/79 81/81/82 +f 59/59/74 58/58/72 73/73/81 +f 59/59/74 74/74/83 75/75/85 +f 52/52/75 67/67/84 68/68/86 +f 60/60/76 75/75/85 76/76/87 +f 53/53/77 68/68/86 69/69/88 +f 62/62/65 61/61/63 76/76/87 +f 54/54/64 69/69/88 70/70/90 +f 62/62/65 77/77/89 78/78/91 +f 56/56/68 55/55/66 70/70/90 +f 63/63/67 78/78/91 79/79/78 +f 56/56/68 71/71/92 72/72/80 +f 68/68/86 84/115/107 85/116/93 +f 77/77/89 76/76/87 92/123/94 +f 69/69/88 85/116/93 86/117/96 +f 77/77/89 93/124/95 94/125/97 +f 71/71/92 70/70/90 86/117/96 +f 78/78/91 94/125/97 95/126/99 +f 71/71/92 87/118/98 88/119/100 +f 79/79/78 95/126/99 96/127/101 +f 73/73/81 72/72/80 88/119/100 +f 81/81/82 80/80/79 96/127/101 +f 74/74/83 73/73/81 89/120/102 +f 74/74/83 90/121/104 91/122/106 +f 67/67/84 83/114/105 84/115/107 +f 75/75/85 91/122/106 92/123/94 +f 89/120/102 88/119/100 103/134/108 +f 96/127/101 111/142/122 112/143/110 +f 90/121/104 89/120/102 104/135/109 +f 90/121/104 105/136/111 106/137/113 +f 84/115/107 83/114/105 98/129/112 +f 91/122/106 106/137/113 107/138/115 +f 84/115/107 99/130/114 100/131/116 +f 93/124/95 92/123/94 107/138/115 +f 85/116/93 100/131/116 101/132/118 +f 93/124/95 108/139/117 109/140/119 +f 87/118/98 86/117/96 101/132/118 +f 95/126/99 94/125/97 109/140/119 +f 87/118/98 102/133/120 103/134/108 +f 96/127/101 95/126/99 110/141/121 +f 108/139/117 107/138/115 122/153/123 +f 100/131/116 115/146/137 116/147/125 +f 108/139/117 123/154/124 124/155/126 +f 102/133/120 101/132/118 116/147/125 +f 110/141/121 109/140/119 124/155/126 +f 102/133/120 117/148/127 118/149/129 +f 110/141/121 125/156/128 126/157/130 +f 104/135/109 103/134/108 118/149/129 +f 111/142/122 126/157/130 127/158/132 +f 105/136/111 104/135/109 119/150/131 +f 105/136/111 120/151/133 121/152/135 +f 99/130/114 98/129/112 113/144/134 +f 106/137/113 121/152/135 122/153/123 +f 99/130/114 114/145/136 115/146/137 +f 126/157/130 141/172/152 142/173/138 +f 120/151/133 119/150/131 134/165/139 +f 120/151/133 135/166/140 136/167/142 +f 113/144/134 128/159/141 129/160/143 +f 121/152/135 136/167/142 137/168/144 +f 114/145/136 129/160/143 130/161/145 +f 123/154/124 122/153/123 137/168/144 +f 115/146/137 130/161/145 131/162/147 +f 123/154/124 138/169/146 139/170/148 +f 117/148/127 116/147/125 131/162/147 +f 125/156/128 124/155/126 139/170/148 +f 117/148/127 132/163/149 133/164/151 +f 125/156/128 140/171/150 141/172/152 +f 119/150/131 118/149/129 133/164/151 +f 138/169/146 153/184/167 154/185/153 +f 132/163/149 131/162/147 146/177/154 +f 140/171/150 139/170/148 154/185/153 +f 132/163/149 147/178/155 148/179/157 +f 140/171/150 155/186/156 156/187/158 +f 134/165/139 133/164/151 148/179/157 +f 142/173/138 141/172/152 156/187/158 +f 135/166/140 134/165/139 149/180/159 +f 135/166/140 150/181/161 151/182/163 +f 128/159/141 143/174/162 144/175/164 +f 136/167/142 151/182/163 152/183/165 +f 130/161/145 129/160/143 144/175/164 +f 138/169/146 137/168/144 152/183/165 +f 130/161/145 145/176/166 146/177/154 +f 150/181/161 165/196/182 166/197/170 +f 143/174/162 158/189/168 159/190/171 +f 151/182/163 166/197/170 167/198/172 +f 145/176/166 144/175/164 159/190/171 +f 153/184/167 152/183/165 167/198/172 +f 145/176/166 160/191/173 161/192/175 +f 153/184/167 168/199/174 169/200/176 +f 147/178/155 146/177/154 161/192/175 +f 155/186/156 154/185/153 169/200/176 +f 147/178/155 162/193/177 163/194/179 +f 155/186/156 170/201/178 171/202/180 +f 149/180/159 148/179/157 163/194/179 +f 156/187/158 171/202/180 172/203/169 +f 150/181/161 149/180/159 164/195/181 +f 162/193/177 161/192/175 176/207/183 +f 169/200/176 184/215/197 185/216/185 +f 162/193/177 177/208/184 178/209/186 +f 171/202/180 170/201/178 185/216/185 +f 164/195/181 163/194/179 178/209/186 +f 172/203/169 171/202/180 186/217/187 +f 165/196/182 164/195/181 179/210/188 +f 165/196/182 180/211/190 181/212/192 +f 158/189/168 173/204/191 174/205/193 +f 166/197/170 181/212/192 182/213/194 +f 159/190/171 174/205/193 175/206/195 +f 168/199/174 167/198/172 182/213/194 +f 160/191/173 175/206/195 176/207/183 +f 168/199/174 183/214/196 184/215/197 +f 180/211/190 195/226/212 196/227/198 +f 174/205/193 173/204/191 188/219/199 +f 181/212/192 196/227/198 197/228/201 +f 175/206/195 174/205/193 189/220/200 +f 183/214/196 182/213/194 197/228/201 +f 175/206/195 190/221/202 191/222/204 +f 183/214/196 198/229/203 199/230/205 +f 177/208/184 176/207/183 191/222/204 +f 185/216/185 184/215/197 199/230/205 +f 177/208/184 192/223/206 193/224/208 +f 185/216/185 200/231/207 201/232/209 +f 179/210/188 178/209/186 193/224/208 +f 186/217/187 201/232/209 202/233/211 +f 180/211/190 179/210/188 194/225/210 +f 200/231/207 199/230/205 214/245/213 +f 192/223/206 207/238/227 208/239/215 +f 200/231/207 215/246/214 216/247/216 +f 194/225/210 193/224/208 208/239/215 +f 201/232/209 216/247/216 217/248/218 +f 195/226/212 194/225/210 209/240/217 +f 195/226/212 210/241/219 211/242/221 +f 188/219/199 203/234/220 204/235/222 +f 196/227/198 211/242/221 212/243/223 +f 189/220/200 204/235/222 205/236/224 +f 198/229/203 197/228/201 212/243/223 +f 190/221/202 205/236/224 206/237/226 +f 198/229/203 213/244/225 214/245/213 +f 192/223/206 191/222/204 206/237/226 +f 203/234/220 218/249/241 219/250/228 +f 211/242/221 226/257/242 227/258/229 +f 204/235/222 219/250/228 220/251/230 +f 213/244/225 212/243/223 227/258/229 +f 205/236/224 220/251/230 221/252/232 +f 213/244/225 228/259/231 229/260/233 +f 207/238/227 206/237/226 221/252/232 +f 215/246/214 214/245/213 229/260/233 +f 207/238/227 222/253/234 223/254/236 +f 215/246/214 230/261/235 231/262/237 +f 209/240/217 208/239/215 223/254/236 +f 216/247/216 231/262/237 232/263/239 +f 210/241/219 209/240/217 224/255/238 +f 210/241/219 225/256/240 226/257/242 +f 222/253/234 237/268/256 238/269/243 +f 230/261/235 245/276/257 246/277/244 +f 224/255/238 223/254/236 238/269/243 +f 232/263/239 231/262/237 246/277/244 +f 225/256/240 224/255/238 239/270/245 +f 225/256/240 240/271/247 241/272/249 +f 218/249/241 233/264/248 234/265/250 +f 226/257/242 241/272/249 242/273/251 +f 220/251/230 219/250/228 234/265/250 +f 228/259/231 227/258/229 242/273/251 +f 220/251/230 235/266/252 236/267/254 +f 228/259/231 243/274/253 244/275/255 +f 222/253/234 221/252/232 236/267/254 +f 230/261/235 229/260/233 244/275/255 +f 241/272/249 256/287/271 257/288/258 +f 234/265/250 249/280/272 250/281/259 +f 243/274/253 242/273/251 257/288/258 +f 235/266/252 250/281/259 251/282/261 +f 243/274/253 258/289/260 259/290/262 +f 237/268/256 236/267/254 251/282/261 +f 245/276/257 244/275/255 259/290/262 +f 237/268/256 252/283/263 253/284/265 +f 245/276/257 260/291/264 261/292/266 +f 239/270/245 238/269/243 253/284/265 +f 246/277/244 261/292/266 262/293/268 +f 240/271/247 239/270/245 254/285/267 +f 240/271/247 255/286/269 256/287/271 +f 233/264/248 248/279/270 249/280/272 +f 260/291/264 275/306/287 276/307/273 +f 254/285/267 253/284/265 268/299/274 +f 261/292/266 276/307/273 277/308/276 +f 255/286/269 254/285/267 269/300/275 +f 255/286/269 270/301/277 271/302/279 +f 249/280/272 248/279/270 263/294/278 +f 256/287/271 271/302/279 272/303/281 +f 249/280/272 264/295/280 265/296/282 +f 258/289/260 257/288/258 272/303/281 +f 250/281/259 265/296/282 266/297/284 +f 258/289/260 273/304/283 274/305/285 +f 252/283/263 251/282/261 266/297/284 +f 260/291/264 259/290/262 274/305/285 +f 252/283/263 267/298/286 268/299/274 +f 273/304/283 272/303/281 287/318/288 +f 265/296/282 280/311/302 281/312/290 +f 273/304/283 288/319/289 289/320/291 +f 267/298/286 266/297/284 281/312/290 +f 275/306/287 274/305/285 289/320/291 +f 267/298/286 282/313/292 283/314/294 +f 275/306/287 290/321/293 291/322/295 +f 269/300/275 268/299/274 283/314/294 +f 277/308/276 276/307/273 291/322/295 +f 270/301/277 269/300/275 284/315/296 +f 270/301/277 285/316/298 286/317/300 +f 264/295/280 263/294/278 278/309/299 +f 271/302/279 286/317/300 287/318/288 +f 264/295/280 279/310/301 280/311/302 +f 292/323/297 291/322/295 306/337/303 +f 285/316/298 284/315/296 299/330/305 +f 285/316/298 300/331/306 301/332/308 +f 278/309/299 293/324/307 294/325/309 +f 286/317/300 301/332/308 302/333/310 +f 279/310/301 294/325/309 295/326/311 +f 288/319/289 287/318/288 302/333/310 +f 280/311/302 295/326/311 296/327/313 +f 288/319/289 303/334/312 304/335/314 +f 282/313/292 281/312/290 296/327/313 +f 290/321/293 289/320/291 304/335/314 +f 282/313/292 297/328/315 298/329/317 +f 290/321/293 305/336/316 306/337/303 +f 284/315/296 283/314/294 298/329/317 +f 295/326/311 311/373/331 312/374/318 +f 303/334/312 319/381/332 320/382/319 +f 297/328/315 296/327/313 312/374/318 +f 305/336/316 304/335/314 320/382/319 +f 297/328/315 313/375/320 314/376/322 +f 305/336/316 321/383/321 322/384/323 +f 299/330/305 298/329/317 314/376/322 +f 306/337/303 322/384/323 323/385/325 +f 300/331/306 299/330/305 315/377/324 +f 300/331/306 316/378/326 317/379/328 +f 293/324/307 309/371/327 310/372/329 +f 301/332/308 317/379/328 318/380/330 +f 294/325/309 310/372/329 311/373/331 +f 303/334/312 302/333/310 318/380/330 +f 316/378/326 315/377/324 330/392/333 +f 316/378/326 331/393/334 332/394/337 +f 309/371/327 324/386/335 325/387/338 +f 317/379/328 332/394/337 333/395/339 +f 310/372/329 325/387/338 326/388/340 +f 319/381/332 318/380/330 333/395/339 +f 311/373/331 326/388/340 327/389/342 +f 319/381/332 334/396/341 335/397/343 +f 312/374/318 327/389/342 328/390/344 +f 321/383/321 320/382/319 335/397/343 +f 313/375/320 328/390/344 329/391/346 +f 321/383/321 336/398/345 337/399/347 +f 315/377/324 314/376/322 329/391/346 +f 322/384/323 337/399/347 338/400/336 +f 334/396/341 349/411/362 350/412/348 +f 328/390/344 327/389/342 342/404/349 +f 336/398/345 335/397/343 350/412/348 +f 328/390/344 343/405/350 344/406/352 +f 336/398/345 351/413/351 352/414/353 +f 330/392/333 329/391/346 344/406/352 +f 337/399/347 352/414/353 353/415/355 +f 331/393/334 330/392/333 345/407/354 +f 331/393/334 346/408/356 347/409/358 +f 324/386/335 339/401/357 340/402/359 +f 332/394/337 347/409/358 348/410/360 +f 325/387/338 340/402/359 341/403/361 +f 334/396/341 333/395/339 348/410/360 +f 326/388/340 341/403/361 342/404/349 +f 346/408/356 361/430/377 362/432/365 +f 339/401/357 354/416/363 355/418/366 +f 347/409/358 362/432/365 363/434/367 +f 341/403/361 340/402/359 355/418/366 +f 349/411/362 348/410/360 363/434/367 +f 341/403/361 356/420/368 357/422/370 +f 349/411/362 364/436/369 365/438/371 +f 343/405/350 342/404/349 357/422/370 +f 351/413/351 350/412/348 365/438/371 +f 343/405/350 358/424/372 359/426/374 +f 351/413/351 366/440/373 367/442/375 +f 345/407/354 344/406/352 359/426/374 +f 352/414/353 367/442/375 368/445/364 +f 346/408/356 345/407/354 360/428/376 +f 358/425/372 357/423/370 372/449/378 +f 366/441/373 365/439/371 380/457/380 +f 358/425/372 373/450/379 374/451/382 +f 366/441/373 381/458/381 382/459/383 +f 360/429/376 359/427/374 374/451/382 +f 367/443/375 382/459/383 383/460/385 +f 361/431/377 360/429/376 375/452/384 +f 361/431/377 376/453/386 377/454/388 +f 354/417/363 369/446/387 370/447/389 +f 362/433/365 377/454/388 378/455/390 +f 356/421/368 355/419/366 370/447/389 +f 364/437/369 363/435/367 378/455/390 +f 356/421/368 371/448/391 372/449/378 +f 364/437/369 379/456/392 380/457/380 +f 376/453/386 391/468/406 392/469/393 +f 369/446/387 384/461/407 385/462/394 +f 377/454/388 392/469/393 393/470/395 +f 371/448/391 370/447/389 385/462/394 +f 379/456/392 378/455/390 393/470/395 +f 371/448/391 386/463/396 387/464/398 +f 379/456/392 394/471/397 395/472/399 +f 373/450/379 372/449/378 387/464/398 +f 381/458/381 380/457/380 395/472/399 +f 373/450/379 388/465/400 389/466/402 +f 381/458/381 396/473/401 397/474/403 +f 375/452/384 374/451/382 389/466/402 +f 382/459/383 397/474/403 398/475/405 +f 376/453/386 375/452/384 390/467/404 +f 396/473/401 395/472/399 410/487/408 +f 388/465/400 403/480/422 404/481/410 +f 397/474/403 396/473/401 411/488/409 +f 390/467/404 389/466/402 404/481/410 +f 397/474/403 412/489/411 413/490/413 +f 391/468/406 390/467/404 405/482/412 +f 391/468/406 406/483/414 407/484/416 +f 385/462/394 384/461/407 399/476/415 +f 392/469/393 407/484/416 408/485/418 +f 385/462/394 400/477/417 401/478/419 +f 394/471/397 393/470/395 408/485/418 +f 386/463/396 401/478/419 402/479/421 +f 394/471/397 409/486/420 410/487/408 +f 388/465/400 387/464/398 402/479/421 +f 407/484/416 422/499/437 423/500/423 +f 401/478/419 400/477/417 415/492/424 +f 409/486/420 408/485/418 423/500/423 +f 401/478/419 416/493/425 417/494/427 +f 409/486/420 424/501/426 425/502/428 +f 403/480/422 402/479/421 417/494/427 +f 411/488/409 410/487/408 425/502/428 +f 403/480/422 418/495/429 419/496/431 +f 411/488/409 426/503/430 427/504/432 +f 405/482/412 404/481/410 419/496/431 +f 412/489/411 427/504/432 428/505/434 +f 406/483/414 405/482/412 420/497/433 +f 406/483/414 421/498/435 422/499/437 +f 399/476/415 414/491/436 415/492/424 +f 426/503/430 441/518/452 442/519/438 +f 420/497/433 419/496/431 434/511/439 +f 427/504/432 442/519/438 443/520/441 +f 421/498/435 420/497/433 435/512/440 +f 421/498/435 436/513/442 437/514/444 +f 414/491/436 429/506/443 430/507/445 +f 422/499/437 437/514/444 438/515/446 +f 416/493/425 415/492/424 430/507/445 +f 424/501/426 423/500/423 438/515/446 +f 416/493/425 431/508/447 432/509/449 +f 424/501/426 439/516/448 440/517/450 +f 418/495/429 417/494/427 432/509/449 +f 426/503/430 425/502/428 440/517/450 +f 418/495/429 433/510/451 434/511/439 +f 430/507/445 445/522/467 446/523/453 +f 439/516/448 438/515/446 453/530/454 +f 431/508/447 446/523/453 447/524/456 +f 439/516/448 454/531/455 455/532/457 +f 433/510/451 432/509/449 447/524/456 +f 441/518/452 440/517/450 455/532/457 +f 433/510/451 448/525/458 449/526/460 +f 441/518/452 456/533/459 457/534/461 +f 435/512/440 434/511/439 449/526/460 +f 442/519/438 457/534/461 458/535/463 +f 436/513/442 435/512/440 450/527/462 +f 436/513/442 451/528/464 452/529/466 +f 430/507/445 429/506/443 444/521/465 +f 437/514/444 452/529/466 453/530/454 +f 450/527/462 449/526/460 464/541/468 +f 458/535/463 457/534/461 472/549/470 +f 451/528/464 450/527/462 465/542/469 +f 451/528/464 466/543/472 467/544/474 +f 444/521/465 459/536/473 460/537/475 +f 452/529/466 467/544/474 468/545/476 +f 446/523/453 445/522/467 460/537/475 +f 454/531/455 453/530/454 468/545/476 +f 446/523/453 461/538/477 462/539/479 +f 454/531/455 469/546/478 470/547/480 +f 448/525/458 447/524/456 462/539/479 +f 456/533/459 455/532/457 470/547/480 +f 448/525/458 463/540/481 464/541/468 +f 456/533/459 471/548/482 472/549/470 +f 468/545/476 478/555/4 6/6/6 +f 462/539/479 461/538/477 1/1/1 +f 469/546/478 6/6/6 479/556/10 +f 463/540/481 462/539/479 476/553/7 +f 471/548/482 470/547/480 479/556/10 +f 464/541/468 463/540/481 2/2/11 +f 471/548/482 480/557/14 481/558/18 +f 465/542/469 464/541/468 477/554/15 +f 472/549/470 481/558/18 482/559/22 +f 466/543/472 465/542/469 3/3/19 +f 466/543/472 4/4/24 5/5/30 +f 460/537/475 459/536/473 474/551/25 +f 467/544/474 5/5/30 478/555/4 +f 461/538/477 460/537/475 475/552/31 +o Sphere.001 +v 0.000000 3.751747 -0.555570 +v 0.000000 3.475848 -0.831470 +v 0.000000 3.115368 -0.980785 +v 0.000000 2.920277 -1.000000 +v 0.000000 2.725187 -0.980785 +v 0.000000 2.364707 -0.831470 +v 0.038060 3.901062 -0.191342 +v 0.074658 3.844157 -0.375330 +v 0.108386 3.751747 -0.544895 +v 0.137950 3.627384 -0.693520 +v 0.162212 3.475848 -0.815493 +v 0.180240 3.302961 -0.906127 +v 0.191342 3.115368 -0.961940 +v 0.195090 2.920277 -0.980785 +v 0.191342 2.725187 -0.961940 +v 0.180240 2.537594 -0.906127 +v 0.162212 2.364707 -0.815493 +v 0.137950 2.213171 -0.693520 +v 0.108386 2.088808 -0.544895 +v 0.074658 1.996398 -0.375330 +v 0.038060 1.939492 -0.191342 +v 0.074658 3.901062 -0.180240 +v 0.146447 3.844157 -0.353553 +v 0.212608 3.751747 -0.513280 +v 0.270598 3.627384 -0.653281 +v 0.318190 3.475848 -0.768178 +v 0.353553 3.302961 -0.853553 +v 0.375330 3.115368 -0.906127 +v 0.382683 2.920277 -0.923879 +v 0.375330 2.725187 -0.906127 +v 0.353553 2.537594 -0.853553 +v 0.318190 2.364707 -0.768178 +v 0.270598 2.213171 -0.653281 +v 0.212608 2.088808 -0.513280 +v 0.146447 1.996398 -0.353553 +v 0.074658 1.939492 -0.180240 +v 0.108386 3.901062 -0.162212 +v 0.212608 3.844157 -0.318190 +v 0.308658 3.751747 -0.461940 +v 0.392847 3.627384 -0.587938 +v 0.461940 3.475848 -0.691342 +v 0.513280 3.302961 -0.768178 +v 0.544895 3.115368 -0.815493 +v 0.555570 2.920277 -0.831469 +v 0.544895 2.725187 -0.815493 +v 0.513280 2.537594 -0.768178 +v 0.461940 2.364707 -0.691342 +v 0.392847 2.213171 -0.587938 +v 0.308658 2.088808 -0.461940 +v 0.212608 1.996398 -0.318190 +v 0.108386 1.939492 -0.162212 +v 0.137950 3.901062 -0.137950 +v 0.270598 3.844157 -0.270598 +v 0.392847 3.751747 -0.392847 +v 0.500000 3.627384 -0.500000 +v 0.587938 3.475848 -0.587938 +v 0.653281 3.302961 -0.653281 +v 0.693520 3.115368 -0.693520 +v 0.707107 2.920277 -0.707107 +v 0.693520 2.725187 -0.693520 +v 0.653281 2.537594 -0.653281 +v 0.587938 2.364707 -0.587938 +v 0.500000 2.213171 -0.500000 +v 0.392847 2.088808 -0.392847 +v 0.270598 1.996398 -0.270598 +v 0.137950 1.939492 -0.137950 +v 0.162212 3.901062 -0.108386 +v 0.318190 3.844157 -0.212608 +v 0.461940 3.751747 -0.308658 +v 0.587938 3.627384 -0.392847 +v 0.691342 3.475848 -0.461940 +v 0.768178 3.302961 -0.513280 +v 0.815493 3.115368 -0.544895 +v 0.831470 2.920277 -0.555570 +v 0.815493 2.725187 -0.544895 +v 0.768178 2.537594 -0.513280 +v 0.691342 2.364707 -0.461940 +v 0.587938 2.213171 -0.392847 +v 0.461940 2.088808 -0.308658 +v 0.318190 1.996398 -0.212608 +v 0.162212 1.939492 -0.108386 +v 0.000000 3.920277 0.000000 +v 0.180240 3.901062 -0.074658 +v 0.353553 3.844157 -0.146447 +v 0.513280 3.751747 -0.212607 +v 0.653281 3.627384 -0.270598 +v 0.768178 3.475848 -0.318190 +v 0.853553 3.302961 -0.353553 +v 0.906127 3.115368 -0.375330 +v 0.923879 2.920277 -0.382683 +v 0.906127 2.725187 -0.375330 +v 0.853553 2.537594 -0.353553 +v 0.768178 2.364707 -0.318190 +v 0.653281 2.213171 -0.270598 +v 0.513280 2.088808 -0.212607 +v 0.353553 1.996398 -0.146447 +v 0.180240 1.939492 -0.074658 +v 0.191342 3.901062 -0.038060 +v 0.375330 3.844157 -0.074658 +v 0.544895 3.751747 -0.108386 +v 0.693520 3.627384 -0.137950 +v 0.815493 3.475848 -0.162212 +v 0.906127 3.302961 -0.180240 +v 0.961940 3.115368 -0.191342 +v 0.980785 2.920277 -0.195090 +v 0.961940 2.725187 -0.191342 +v 0.906127 2.537594 -0.180240 +v 0.815493 2.364707 -0.162212 +v 0.693520 2.213171 -0.137950 +v 0.544895 2.088808 -0.108386 +v 0.375330 1.996398 -0.074658 +v 0.191342 1.939492 -0.038060 +v 0.195090 3.901062 0.000000 +v 0.382683 3.844157 0.000000 +v 0.555570 3.751747 0.000000 +v 0.707107 3.627384 -0.000000 +v 0.831469 3.475848 0.000000 +v 0.923879 3.302961 -0.000000 +v 0.980785 3.115368 0.000000 +v 1.000000 2.920277 0.000000 +v 0.980785 2.725187 0.000000 +v 0.923879 2.537594 -0.000000 +v 0.831469 2.364707 0.000000 +v 0.707107 2.213171 -0.000000 +v 0.555570 2.088808 0.000000 +v 0.382683 1.996398 0.000000 +v 0.195090 1.939492 0.000000 +v 0.191342 3.901062 0.038060 +v 0.375330 3.844157 0.074658 +v 0.544895 3.751747 0.108386 +v 0.693520 3.627384 0.137950 +v 0.815493 3.475848 0.162212 +v 0.906127 3.302961 0.180240 +v 0.961940 3.115368 0.191342 +v 0.980785 2.920277 0.195090 +v 0.961940 2.725187 0.191342 +v 0.906127 2.537594 0.180240 +v 0.815493 2.364707 0.162212 +v 0.693520 2.213171 0.137950 +v 0.544895 2.088808 0.108386 +v 0.375330 1.996398 0.074658 +v 0.191342 1.939492 0.038060 +v 0.180240 3.901062 0.074658 +v 0.353553 3.844157 0.146447 +v 0.513280 3.751747 0.212608 +v 0.653281 3.627384 0.270598 +v 0.768178 3.475848 0.318190 +v 0.853553 3.302961 0.353553 +v 0.906127 3.115368 0.375330 +v 0.923879 2.920277 0.382683 +v 0.906127 2.725187 0.375330 +v 0.853553 2.537594 0.353553 +v 0.768178 2.364707 0.318190 +v 0.653281 2.213171 0.270598 +v 0.513280 2.088808 0.212608 +v 0.353553 1.996398 0.146447 +v 0.180240 1.939492 0.074658 +v 0.162212 3.901062 0.108386 +v 0.318190 3.844157 0.212608 +v 0.461940 3.751747 0.308658 +v 0.587938 3.627384 0.392847 +v 0.691341 3.475848 0.461940 +v 0.768178 3.302961 0.513280 +v 0.815493 3.115368 0.544895 +v 0.831469 2.920277 0.555570 +v 0.815493 2.725187 0.544895 +v 0.768178 2.537594 0.513280 +v 0.691341 2.364707 0.461940 +v 0.587938 2.213171 0.392847 +v 0.461940 2.088808 0.308658 +v 0.318190 1.996398 0.212608 +v 0.162212 1.939492 0.108386 +v 0.137950 3.901062 0.137950 +v 0.270598 3.844157 0.270598 +v 0.392847 3.751747 0.392847 +v 0.500000 3.627384 0.500000 +v 0.587938 3.475848 0.587938 +v 0.653281 3.302961 0.653281 +v 0.693520 3.115368 0.693520 +v 0.707106 2.920277 0.707107 +v 0.693520 2.725187 0.693520 +v 0.653281 2.537594 0.653281 +v 0.587938 2.364707 0.587938 +v 0.500000 2.213171 0.500000 +v 0.392847 2.088808 0.392847 +v 0.270598 1.996398 0.270598 +v 0.137950 1.939492 0.137950 +v 0.108386 3.901062 0.162212 +v 0.212607 3.844157 0.318190 +v 0.308658 3.751747 0.461940 +v 0.392847 3.627384 0.587938 +v 0.461940 3.475848 0.691342 +v 0.513280 3.302961 0.768178 +v 0.544895 3.115368 0.815493 +v 0.555570 2.920277 0.831469 +v 0.544895 2.725187 0.815493 +v 0.513280 2.537594 0.768178 +v 0.461940 2.364707 0.691342 +v 0.392847 2.213171 0.587938 +v 0.308658 2.088808 0.461940 +v 0.212607 1.996398 0.318190 +v 0.108386 1.939492 0.162212 +v 0.074658 3.901062 0.180240 +v 0.146447 3.844157 0.353553 +v 0.212607 3.751747 0.513280 +v 0.270598 3.627384 0.653281 +v 0.318189 3.475848 0.768178 +v 0.353553 3.302961 0.853553 +v 0.375330 3.115368 0.906127 +v 0.382683 2.920277 0.923879 +v 0.375330 2.725187 0.906127 +v 0.353553 2.537594 0.853553 +v 0.318189 2.364707 0.768178 +v 0.270598 2.213171 0.653281 +v 0.212607 2.088808 0.513280 +v 0.146447 1.996398 0.353553 +v 0.074658 1.939492 0.180240 +v 0.038060 3.901062 0.191342 +v 0.074658 3.844157 0.375330 +v 0.108386 3.751747 0.544895 +v 0.137950 3.627384 0.693520 +v 0.162212 3.475848 0.815493 +v 0.180240 3.302961 0.906127 +v 0.191342 3.115368 0.961939 +v 0.195090 2.920277 0.980785 +v 0.191342 2.725187 0.961939 +v 0.180240 2.537594 0.906127 +v 0.162212 2.364707 0.815493 +v 0.137950 2.213171 0.693520 +v 0.108386 2.088808 0.544895 +v 0.074658 1.996398 0.375330 +v 0.038060 1.939492 0.191342 +v -0.000000 3.901062 0.195090 +v -0.000000 3.844157 0.382683 +v -0.000000 3.751747 0.555570 +v -0.000000 3.627384 0.707107 +v -0.000000 3.475848 0.831469 +v 0.000000 3.302961 0.923879 +v -0.000000 3.115368 0.980785 +v -0.000000 2.920277 0.999999 +v -0.000000 2.725187 0.980785 +v 0.000000 2.537594 0.923879 +v -0.000000 2.364707 0.831469 +v -0.000000 2.213171 0.707107 +v -0.000000 2.088808 0.555570 +v -0.000000 1.996398 0.382683 +v -0.000000 1.939492 0.195090 +v -0.038060 3.901062 0.191342 +v -0.074658 3.844157 0.375330 +v -0.108386 3.751747 0.544895 +v -0.137950 3.627384 0.693520 +v -0.162212 3.475848 0.815493 +v -0.180240 3.302961 0.906127 +v -0.191342 3.115368 0.961939 +v -0.195091 2.920277 0.980785 +v -0.191342 2.725187 0.961939 +v -0.180240 2.537594 0.906127 +v -0.162212 2.364707 0.815493 +v -0.137950 2.213171 0.693520 +v -0.108386 2.088808 0.544895 +v -0.074658 1.996398 0.375330 +v -0.038060 1.939492 0.191342 +v -0.074658 3.901062 0.180240 +v -0.146447 3.844157 0.353553 +v -0.212608 3.751747 0.513280 +v -0.270598 3.627384 0.653281 +v -0.318190 3.475848 0.768177 +v -0.353553 3.302961 0.853553 +v -0.375330 3.115368 0.906127 +v -0.382683 2.920277 0.923879 +v -0.375330 2.725187 0.906127 +v -0.353553 2.537594 0.853553 +v -0.318190 2.364707 0.768177 +v -0.270598 2.213171 0.653281 +v -0.212608 2.088808 0.513280 +v -0.146447 1.996398 0.353553 +v -0.074658 1.939492 0.180240 +v -0.108386 3.901062 0.162212 +v -0.212608 3.844157 0.318190 +v -0.308658 3.751747 0.461939 +v -0.392847 3.627384 0.587938 +v -0.461940 3.475848 0.691341 +v -0.513280 3.302961 0.768178 +v -0.544895 3.115368 0.815493 +v -0.555570 2.920277 0.831469 +v -0.544895 2.725187 0.815493 +v -0.513280 2.537594 0.768178 +v -0.461940 2.364707 0.691341 +v -0.392847 2.213171 0.587938 +v -0.308658 2.088808 0.461939 +v -0.212608 1.996398 0.318190 +v -0.108386 1.939492 0.162212 +v -0.137950 3.901062 0.137950 +v -0.270598 3.844157 0.270598 +v -0.392847 3.751747 0.392847 +v -0.500000 3.627384 0.500000 +v -0.587938 3.475848 0.587937 +v -0.653281 3.302961 0.653281 +v -0.693520 3.115368 0.693520 +v -0.707106 2.920277 0.707106 +v -0.693520 2.725187 0.693520 +v -0.653281 2.537594 0.653281 +v -0.587938 2.364707 0.587937 +v -0.500000 2.213171 0.500000 +v -0.392847 2.088808 0.392847 +v -0.270598 1.996398 0.270598 +v -0.137950 1.939492 0.137950 +v 0.000000 1.920277 0.000000 +v -0.162212 3.901062 0.108386 +v -0.318190 3.844157 0.212607 +v -0.461940 3.751747 0.308658 +v -0.587938 3.627384 0.392847 +v -0.691341 3.475848 0.461939 +v -0.768177 3.302961 0.513280 +v -0.815493 3.115368 0.544895 +v -0.831469 2.920277 0.555569 +v -0.815493 2.725187 0.544895 +v -0.768177 2.537594 0.513280 +v -0.691341 2.364707 0.461939 +v -0.587938 2.213171 0.392847 +v -0.461940 2.088808 0.308658 +v -0.318190 1.996398 0.212607 +v -0.162212 1.939492 0.108386 +v -0.180240 3.901062 0.074658 +v -0.353553 3.844157 0.146447 +v -0.513280 3.751747 0.212607 +v -0.653281 3.627384 0.270598 +v -0.768177 3.475848 0.318189 +v -0.853553 3.302961 0.353553 +v -0.906127 3.115368 0.375330 +v -0.923879 2.920277 0.382683 +v -0.906127 2.725187 0.375330 +v -0.853553 2.537594 0.353553 +v -0.768177 2.364707 0.318189 +v -0.653281 2.213171 0.270598 +v -0.513280 2.088808 0.212607 +v -0.353553 1.996398 0.146447 +v -0.180240 1.939492 0.074658 +v -0.191342 3.901062 0.038060 +v -0.375330 3.844157 0.074658 +v -0.544895 3.751747 0.108386 +v -0.693520 3.627384 0.137950 +v -0.815493 3.475848 0.162211 +v -0.906127 3.302961 0.180240 +v -0.961939 3.115368 0.191341 +v -0.980784 2.920277 0.195090 +v -0.961939 2.725187 0.191341 +v -0.906127 2.537594 0.180240 +v -0.815493 2.364707 0.162211 +v -0.693520 2.213171 0.137950 +v -0.544895 2.088808 0.108386 +v -0.375330 1.996398 0.074658 +v -0.191342 1.939492 0.038060 +v -0.195090 3.901062 -0.000000 +v -0.382683 3.844157 -0.000000 +v -0.555570 3.751747 -0.000000 +v -0.707107 3.627384 -0.000000 +v -0.831469 3.475848 -0.000000 +v -0.923879 3.302961 -0.000000 +v -0.980785 3.115368 -0.000000 +v -0.999999 2.920277 -0.000000 +v -0.980785 2.725187 -0.000000 +v -0.923879 2.537594 -0.000000 +v -0.831469 2.364707 -0.000000 +v -0.707107 2.213171 -0.000000 +v -0.555570 2.088808 -0.000000 +v -0.382683 1.996398 -0.000000 +v -0.195090 1.939492 -0.000000 +v -0.191342 3.901062 -0.038060 +v -0.375330 3.844157 -0.074658 +v -0.544895 3.751747 -0.108386 +v -0.693520 3.627384 -0.137950 +v -0.815493 3.475848 -0.162212 +v -0.906127 3.302961 -0.180240 +v -0.961939 3.115368 -0.191342 +v -0.980784 2.920277 -0.195091 +v -0.961939 2.725187 -0.191342 +v -0.906127 2.537594 -0.180240 +v -0.815493 2.364707 -0.162212 +v -0.693520 2.213171 -0.137950 +v -0.544895 2.088808 -0.108386 +v -0.375330 1.996398 -0.074658 +v -0.191342 1.939492 -0.038060 +v -0.180240 3.901062 -0.074658 +v -0.353553 3.844157 -0.146447 +v -0.513279 3.751747 -0.212607 +v -0.653281 3.627384 -0.270598 +v -0.768177 3.475848 -0.318190 +v -0.853553 3.302961 -0.353553 +v -0.906127 3.115368 -0.375330 +v -0.923878 2.920277 -0.382683 +v -0.906127 2.725187 -0.375330 +v -0.853553 2.537594 -0.353553 +v -0.768177 2.364707 -0.318190 +v -0.653281 2.213171 -0.270598 +v -0.513279 2.088808 -0.212607 +v -0.353553 1.996398 -0.146447 +v -0.180240 1.939492 -0.074658 +v -0.162212 3.901062 -0.108386 +v -0.318189 3.844157 -0.212607 +v -0.461939 3.751747 -0.308658 +v -0.587938 3.627384 -0.392847 +v -0.691341 3.475848 -0.461940 +v -0.768177 3.302961 -0.513280 +v -0.815493 3.115368 -0.544895 +v -0.831468 2.920277 -0.555570 +v -0.815493 2.725187 -0.544895 +v -0.768177 2.537594 -0.513280 +v -0.691341 2.364707 -0.461940 +v -0.587938 2.213171 -0.392847 +v -0.461939 2.088808 -0.308658 +v -0.318189 1.996398 -0.212607 +v -0.162212 1.939492 -0.108386 +v -0.137950 3.901062 -0.137950 +v -0.270598 3.844157 -0.270598 +v -0.392847 3.751747 -0.392847 +v -0.500000 3.627384 -0.500000 +v -0.587937 3.475848 -0.587938 +v -0.653281 3.302961 -0.653281 +v -0.693519 3.115368 -0.693520 +v -0.707106 2.920277 -0.707106 +v -0.693519 2.725187 -0.693520 +v -0.653281 2.537594 -0.653281 +v -0.587937 2.364707 -0.587938 +v -0.500000 2.213171 -0.500000 +v -0.392847 2.088808 -0.392847 +v -0.270598 1.996398 -0.270598 +v -0.137950 1.939492 -0.137950 +v -0.108386 3.901062 -0.162212 +v -0.212607 3.844157 -0.318190 +v -0.308658 3.751747 -0.461939 +v -0.392847 3.627384 -0.587938 +v -0.461939 3.475848 -0.691341 +v -0.513280 3.302961 -0.768177 +v -0.544895 3.115368 -0.815493 +v -0.555569 2.920277 -0.831469 +v -0.544895 2.725187 -0.815493 +v -0.513280 2.537594 -0.768177 +v -0.461939 2.364707 -0.691341 +v -0.392847 2.213171 -0.587938 +v -0.308658 2.088808 -0.461939 +v -0.212607 1.996398 -0.318190 +v -0.108386 1.939492 -0.162212 +v -0.074658 3.901062 -0.180240 +v -0.146446 3.844157 -0.353553 +v -0.212607 3.751747 -0.513279 +v -0.270598 3.627384 -0.653281 +v -0.318189 3.475848 -0.768177 +v -0.353553 3.302961 -0.853553 +v -0.375330 3.115368 -0.906127 +v -0.382683 2.920277 -0.923879 +v -0.375330 2.725187 -0.906127 +v -0.353553 2.537594 -0.853553 +v -0.318189 2.364707 -0.768177 +v -0.270598 2.213171 -0.653281 +v -0.212607 2.088808 -0.513279 +v -0.146446 1.996398 -0.353553 +v -0.074658 1.939492 -0.180240 +v -0.038060 3.901062 -0.191342 +v -0.074658 3.844157 -0.375330 +v -0.108386 3.751747 -0.544895 +v -0.137950 3.627384 -0.693520 +v -0.162211 3.475848 -0.815493 +v -0.180240 3.302961 -0.906127 +v -0.191341 3.115368 -0.961939 +v -0.195090 2.920277 -0.980784 +v -0.191341 2.725187 -0.961939 +v -0.180240 2.537594 -0.906127 +v -0.162211 2.364707 -0.815493 +v -0.137950 2.213171 -0.693520 +v -0.108386 2.088808 -0.544895 +v -0.074658 1.996398 -0.375330 +v -0.038060 1.939492 -0.191342 +v 0.000000 3.901062 -0.195090 +v 0.000000 3.844157 -0.382683 +v 0.000000 3.627384 -0.707107 +v 0.000000 3.302961 -0.923879 +v 0.000000 2.537594 -0.923879 +v 0.000000 2.213171 -0.707107 +v 0.000000 2.088808 -0.555570 +v 0.000000 1.996398 -0.382683 +v 0.000000 1.939492 -0.195090 +vn -0.0000 0.8286 -0.5598 +vn 0.0757 0.9217 -0.3804 +vn 0.1092 0.8286 -0.5490 +vn -0.0000 -0.3805 -0.9248 +vn 0.1626 -0.5528 -0.8173 +vn -0.0000 -0.5528 -0.8333 +vn -0.0000 0.7041 -0.7101 +vn 0.1385 0.7041 -0.6965 +vn 0.1385 -0.7041 -0.6965 +vn -0.0000 -0.7041 -0.7101 +vn -0.0000 0.5528 -0.8333 +vn 0.1626 0.5528 -0.8173 +vn 0.1092 -0.8286 -0.5490 +vn -0.0000 -0.8286 -0.5598 +vn -0.0000 0.3805 -0.9248 +vn 0.1804 0.3805 -0.9070 +vn 0.0757 -0.9217 -0.3804 +vn -0.0000 -0.9217 -0.3879 +vn -0.0000 0.1939 -0.9810 +vn 0.1914 0.1939 -0.9622 +vn 0.0392 -0.9796 -0.1971 +vn -0.0000 -0.9796 -0.2010 +vn 0.1951 -0.0000 -0.9808 +vn -0.0000 -0.0000 -1.0000 +vn -0.0000 0.9796 -0.2010 +vn -0.0000 1.0000 -0.0000 +vn 0.0392 0.9796 -0.1971 +vn -0.0000 -1.0000 -0.0000 +vn 0.1914 -0.1939 -0.9622 +vn -0.0000 -0.1939 -0.9810 +vn -0.0000 0.9217 -0.3879 +vn 0.1804 -0.3805 -0.9070 +vn 0.1484 0.9217 -0.3584 +vn 0.3539 -0.3805 -0.8544 +vn 0.2142 0.8286 -0.5172 +vn 0.3189 -0.5528 -0.7699 +vn 0.2718 0.7041 -0.6561 +vn 0.2718 -0.7041 -0.6561 +vn 0.3189 0.5528 -0.7699 +vn 0.2142 -0.8286 -0.5172 +vn 0.3539 0.3805 -0.8544 +vn 0.1484 -0.9217 -0.3584 +vn 0.3754 0.1939 -0.9063 +vn 0.0769 -0.9796 -0.1857 +vn 0.3827 -0.0000 -0.9239 +vn 0.0769 0.9796 -0.1857 +vn 0.3754 -0.1939 -0.9063 +vn 0.5138 0.3805 -0.7689 +vn 0.2155 -0.9217 -0.3225 +vn 0.5450 0.1939 -0.8157 +vn 0.1117 -0.9796 -0.1671 +vn 0.5556 -0.0000 -0.8315 +vn 0.1117 0.9796 -0.1671 +vn 0.5450 -0.1939 -0.8157 +vn 0.2155 0.9217 -0.3225 +vn 0.5138 -0.3805 -0.7689 +vn 0.3110 0.8286 -0.4654 +vn 0.4630 -0.5528 -0.6929 +vn 0.3945 0.7041 -0.5905 +vn 0.3945 -0.7041 -0.5905 +vn 0.4630 0.5528 -0.6929 +vn 0.3110 -0.8286 -0.4654 +vn 0.6539 -0.3805 -0.6539 +vn 0.3958 0.8286 -0.3958 +vn 0.5893 -0.5528 -0.5893 +vn 0.5021 0.7041 -0.5021 +vn 0.5021 -0.7041 -0.5021 +vn 0.5893 0.5528 -0.5893 +vn 0.3958 -0.8286 -0.3958 +vn 0.6539 0.3805 -0.6539 +vn 0.2743 -0.9217 -0.2743 +vn 0.6937 0.1939 -0.6937 +vn 0.1421 -0.9796 -0.1421 +vn 0.7071 -0.0000 -0.7071 +vn 0.1421 0.9796 -0.1421 +vn 0.6937 -0.1939 -0.6937 +vn 0.2743 0.9217 -0.2743 +vn 0.4654 -0.8286 -0.3110 +vn 0.3225 -0.9217 -0.2155 +vn 0.7689 0.3805 -0.5138 +vn 0.8157 0.1939 -0.5450 +vn 0.1671 -0.9796 -0.1117 +vn 0.8315 -0.0000 -0.5556 +vn 0.1671 0.9796 -0.1117 +vn 0.8157 -0.1939 -0.5450 +vn 0.3225 0.9217 -0.2155 +vn 0.7689 -0.3805 -0.5138 +vn 0.4654 0.8286 -0.3110 +vn 0.6929 -0.5528 -0.4630 +vn 0.5905 0.7041 -0.3945 +vn 0.5905 -0.7041 -0.3945 +vn 0.6929 0.5528 -0.4630 +vn 0.5172 0.8286 -0.2142 +vn 0.8544 -0.3805 -0.3539 +vn 0.7699 -0.5528 -0.3189 +vn 0.6561 0.7041 -0.2718 +vn 0.6561 -0.7041 -0.2718 +vn 0.7699 0.5528 -0.3189 +vn 0.5172 -0.8286 -0.2142 +vn 0.8544 0.3805 -0.3539 +vn 0.3584 -0.9217 -0.1484 +vn 0.9063 0.1939 -0.3754 +vn 0.1857 -0.9796 -0.0769 +vn 0.9239 -0.0000 -0.3827 +vn 0.1857 0.9796 -0.0769 +vn 0.9063 -0.1939 -0.3754 +vn 0.3584 0.9217 -0.1484 +vn 0.9070 0.3805 -0.1804 +vn 0.9622 0.1939 -0.1914 +vn 0.1971 -0.9796 -0.0392 +vn 0.9808 -0.0000 -0.1951 +vn 0.1971 0.9796 -0.0392 +vn 0.9622 -0.1939 -0.1914 +vn 0.3804 0.9217 -0.0757 +vn 0.9070 -0.3805 -0.1804 +vn 0.5490 0.8286 -0.1092 +vn 0.8173 -0.5528 -0.1626 +vn 0.6965 0.7041 -0.1385 +vn 0.6965 -0.7041 -0.1385 +vn 0.8173 0.5528 -0.1626 +vn 0.5490 -0.8286 -0.1092 +vn 0.3804 -0.9217 -0.0757 +vn 0.9248 -0.3805 -0.0000 +vn 0.8333 -0.5528 -0.0000 +vn 0.7101 0.7041 -0.0000 +vn 0.7101 -0.7041 -0.0000 +vn 0.8333 0.5528 -0.0000 +vn 0.5598 -0.8286 -0.0000 +vn 0.9248 0.3805 -0.0000 +vn 0.3879 -0.9217 -0.0000 +vn 0.9810 0.1939 -0.0000 +vn 0.2010 -0.9796 -0.0000 +vn 1.0000 -0.0000 -0.0000 +vn 0.2010 0.9796 -0.0000 +vn 0.9810 -0.1939 -0.0000 +vn 0.3879 0.9217 -0.0000 +vn 0.5598 0.8286 -0.0000 +vn 0.1971 -0.9796 0.0392 +vn 0.9622 0.1939 0.1914 +vn 0.9808 -0.0000 0.1951 +vn 0.1971 0.9796 0.0392 +vn 0.9622 -0.1939 0.1914 +vn 0.3804 0.9217 0.0757 +vn 0.9070 -0.3805 0.1804 +vn 0.5490 0.8286 0.1092 +vn 0.8173 -0.5528 0.1626 +vn 0.6965 0.7041 0.1385 +vn 0.6965 -0.7041 0.1385 +vn 0.8173 0.5528 0.1626 +vn 0.5490 -0.8286 0.1092 +vn 0.9070 0.3805 0.1804 +vn 0.3804 -0.9217 0.0757 +vn 0.6561 -0.7041 0.2718 +vn 0.6561 0.7041 0.2718 +vn 0.7699 0.5528 0.3189 +vn 0.5172 -0.8286 0.2142 +vn 0.8544 0.3805 0.3539 +vn 0.3584 -0.9217 0.1484 +vn 0.9063 0.1939 0.3754 +vn 0.1857 -0.9796 0.0769 +vn 0.9239 -0.0000 0.3827 +vn 0.1857 0.9796 0.0769 +vn 0.9063 -0.1939 0.3754 +vn 0.3584 0.9217 0.1484 +vn 0.8544 -0.3805 0.3539 +vn 0.5172 0.8286 0.2142 +vn 0.7699 -0.5528 0.3189 +vn 0.1671 0.9796 0.1117 +vn 0.1671 -0.9796 0.1117 +vn 0.8157 -0.1939 0.5450 +vn 0.3225 0.9217 0.2155 +vn 0.7689 -0.3805 0.5138 +vn 0.4654 0.8286 0.3110 +vn 0.6929 -0.5528 0.4630 +vn 0.5905 0.7041 0.3945 +vn 0.5905 -0.7041 0.3945 +vn 0.6929 0.5528 0.4630 +vn 0.4654 -0.8286 0.3110 +vn 0.7689 0.3805 0.5138 +vn 0.3225 -0.9217 0.2155 +vn 0.8157 0.1939 0.5450 +vn 0.8315 -0.0000 0.5556 +vn 0.5021 0.7041 0.5021 +vn 0.5893 0.5528 0.5893 +vn 0.3958 -0.8286 0.3958 +vn 0.6539 0.3805 0.6539 +vn 0.2743 -0.9217 0.2743 +vn 0.6937 0.1939 0.6937 +vn 0.1421 -0.9796 0.1421 +vn 0.7071 -0.0000 0.7071 +vn 0.1421 0.9796 0.1421 +vn 0.6937 -0.1939 0.6937 +vn 0.2743 0.9217 0.2743 +vn 0.6539 -0.3805 0.6539 +vn 0.3958 0.8286 0.3958 +vn 0.5893 -0.5528 0.5893 +vn 0.5021 -0.7041 0.5021 +vn 0.5450 -0.1939 0.8157 +vn 0.1117 0.9796 0.1671 +vn 0.2155 0.9217 0.3225 +vn 0.5138 -0.3805 0.7689 +vn 0.3110 0.8286 0.4654 +vn 0.4630 -0.5528 0.6929 +vn 0.3945 0.7041 0.5905 +vn 0.3945 -0.7041 0.5905 +vn 0.4630 0.5528 0.6929 +vn 0.3110 -0.8286 0.4654 +vn 0.5138 0.3805 0.7689 +vn 0.2155 -0.9217 0.3225 +vn 0.5450 0.1939 0.8157 +vn 0.1117 -0.9796 0.1671 +vn 0.5556 -0.0000 0.8315 +vn 0.2718 -0.7041 0.6561 +vn 0.2142 -0.8286 0.5172 +vn 0.3539 0.3805 0.8544 +vn 0.1484 -0.9217 0.3584 +vn 0.3754 0.1939 0.9063 +vn 0.0769 -0.9796 0.1857 +vn 0.3827 -0.0000 0.9239 +vn 0.0769 0.9796 0.1857 +vn 0.3754 -0.1939 0.9063 +vn 0.1484 0.9217 0.3584 +vn 0.3539 -0.3805 0.8544 +vn 0.2142 0.8286 0.5172 +vn 0.3189 -0.5528 0.7699 +vn 0.2718 0.7041 0.6561 +vn 0.3189 0.5528 0.7699 +vn 0.0757 0.9217 0.3804 +vn 0.1804 -0.3805 0.9070 +vn 0.1092 0.8286 0.5490 +vn 0.1626 -0.5528 0.8173 +vn 0.1385 0.7041 0.6965 +vn 0.1385 -0.7041 0.6965 +vn 0.1626 0.5528 0.8173 +vn 0.1092 -0.8286 0.5490 +vn 0.1804 0.3805 0.9070 +vn 0.0757 -0.9217 0.3804 +vn 0.1914 0.1939 0.9622 +vn 0.0392 -0.9796 0.1971 +vn 0.1951 -0.0000 0.9808 +vn 0.0392 0.9796 0.1971 +vn 0.1914 -0.1939 0.9622 +vn -0.0000 0.3805 0.9248 +vn -0.0000 -0.9217 0.3879 +vn -0.0000 0.1939 0.9810 +vn -0.0000 -0.9796 0.2010 +vn -0.0000 -0.0000 1.0000 +vn -0.0000 0.9796 0.2010 +vn -0.0000 -0.1939 0.9810 +vn -0.0000 0.9217 0.3879 +vn -0.0000 -0.3805 0.9248 +vn -0.0000 0.8286 0.5598 +vn -0.0000 -0.5528 0.8333 +vn -0.0000 0.7041 0.7101 +vn -0.0000 -0.7041 0.7101 +vn -0.0000 0.5528 0.8333 +vn -0.0000 -0.8286 0.5598 +vn -0.1804 -0.3805 0.9070 +vn -0.1092 0.8286 0.5490 +vn -0.1626 -0.5528 0.8173 +vn -0.1385 0.7041 0.6965 +vn -0.1385 -0.7041 0.6965 +vn -0.1626 0.5528 0.8173 +vn -0.1092 -0.8286 0.5490 +vn -0.1804 0.3805 0.9070 +vn -0.0757 -0.9217 0.3804 +vn -0.1914 0.1939 0.9622 +vn -0.0392 -0.9796 0.1971 +vn -0.1951 -0.0000 0.9808 +vn -0.0392 0.9796 0.1971 +vn -0.1914 -0.1939 0.9622 +vn -0.0757 0.9217 0.3804 +vn -0.1484 -0.9217 0.3584 +vn -0.3539 0.3805 0.8544 +vn -0.3754 0.1939 0.9063 +vn -0.0769 -0.9796 0.1857 +vn -0.3827 -0.0000 0.9239 +vn -0.0769 0.9796 0.1857 +vn -0.3754 -0.1939 0.9063 +vn -0.1484 0.9217 0.3584 +vn -0.3539 -0.3805 0.8544 +vn -0.2142 0.8286 0.5172 +vn -0.3189 -0.5528 0.7699 +vn -0.2718 0.7041 0.6561 +vn -0.2718 -0.7041 0.6561 +vn -0.3189 0.5528 0.7699 +vn -0.2142 -0.8286 0.5172 +vn -0.5138 -0.3805 0.7689 +vn -0.4630 -0.5528 0.6929 +vn -0.3945 0.7041 0.5905 +vn -0.3945 -0.7041 0.5905 +vn -0.4630 0.5528 0.6929 +vn -0.3110 -0.8286 0.4654 +vn -0.5138 0.3805 0.7689 +vn -0.2155 -0.9217 0.3225 +vn -0.5450 0.1939 0.8157 +vn -0.1117 -0.9796 0.1671 +vn -0.5556 -0.0000 0.8315 +vn -0.1117 0.9796 0.1671 +vn -0.5450 -0.1939 0.8157 +vn -0.2155 0.9217 0.3225 +vn -0.3110 0.8286 0.4654 +vn -0.2743 -0.9217 0.2743 +vn -0.1421 -0.9796 0.1421 +vn -0.6937 0.1939 0.6937 +vn -0.7071 -0.0000 0.7071 +vn -0.1421 0.9796 0.1421 +vn -0.6937 -0.1939 0.6937 +vn -0.2743 0.9217 0.2743 +vn -0.6539 -0.3805 0.6539 +vn -0.3958 0.8286 0.3958 +vn -0.5893 -0.5528 0.5893 +vn -0.5021 0.7041 0.5021 +vn -0.5021 -0.7041 0.5021 +vn -0.5893 0.5528 0.5893 +vn -0.3958 -0.8286 0.3958 +vn -0.6539 0.3805 0.6539 +vn -0.5905 0.7041 0.3945 +vn -0.5905 -0.7041 0.3945 +vn -0.6929 0.5528 0.4630 +vn -0.4654 -0.8286 0.3110 +vn -0.7689 0.3805 0.5138 +vn -0.3225 -0.9217 0.2155 +vn -0.8157 0.1939 0.5450 +vn -0.1671 -0.9796 0.1117 +vn -0.8315 -0.0000 0.5556 +vn -0.1671 0.9796 0.1117 +vn -0.8157 -0.1939 0.5450 +vn -0.3225 0.9217 0.2155 +vn -0.7689 -0.3805 0.5138 +vn -0.4654 0.8286 0.3110 +vn -0.6929 -0.5528 0.4630 +vn -0.9063 0.1939 0.3754 +vn -0.9239 -0.0000 0.3827 +vn -0.1857 0.9796 0.0769 +vn -0.1857 -0.9796 0.0769 +vn -0.9063 -0.1939 0.3754 +vn -0.3584 0.9217 0.1484 +vn -0.8544 -0.3805 0.3539 +vn -0.5172 0.8286 0.2142 +vn -0.7699 -0.5528 0.3189 +vn -0.6561 0.7041 0.2718 +vn -0.6561 -0.7041 0.2718 +vn -0.7699 0.5528 0.3189 +vn -0.5172 -0.8286 0.2142 +vn -0.8544 0.3805 0.3539 +vn -0.3584 -0.9217 0.1484 +vn -0.6965 -0.7041 0.1385 +vn -0.6965 0.7041 0.1385 +vn -0.8173 0.5528 0.1626 +vn -0.5490 -0.8286 0.1092 +vn -0.9070 0.3805 0.1804 +vn -0.3804 -0.9217 0.0757 +vn -0.9622 0.1939 0.1914 +vn -0.1971 -0.9796 0.0392 +vn -0.9808 -0.0000 0.1951 +vn -0.1971 0.9796 0.0392 +vn -0.9622 -0.1939 0.1914 +vn -0.3804 0.9217 0.0757 +vn -0.9070 -0.3805 0.1804 +vn -0.5490 0.8286 0.1092 +vn -0.8173 -0.5528 0.1626 +vn -0.2010 0.9796 -0.0000 +vn -0.2010 -0.9796 -0.0000 +vn -0.9810 -0.1939 -0.0000 +vn -0.3879 0.9217 -0.0000 +vn -0.9248 -0.3805 -0.0000 +vn -0.5598 0.8286 -0.0000 +vn -0.8333 -0.5528 -0.0000 +vn -0.7101 0.7041 -0.0000 +vn -0.7101 -0.7041 -0.0000 +vn -0.8333 0.5528 -0.0000 +vn -0.5598 -0.8286 -0.0000 +vn -0.9248 0.3805 -0.0000 +vn -0.3879 -0.9217 -0.0000 +vn -0.9810 0.1939 -0.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.6965 0.7041 -0.1385 +vn -0.8173 0.5528 -0.1626 +vn -0.6965 -0.7041 -0.1385 +vn -0.5490 -0.8286 -0.1092 +vn -0.9070 0.3805 -0.1804 +vn -0.3804 -0.9217 -0.0757 +vn -0.9622 0.1939 -0.1914 +vn -0.1971 -0.9796 -0.0392 +vn -0.9808 -0.0000 -0.1951 +vn -0.1971 0.9796 -0.0392 +vn -0.9622 -0.1939 -0.1914 +vn -0.3804 0.9217 -0.0757 +vn -0.9070 -0.3805 -0.1804 +vn -0.5490 0.8286 -0.1092 +vn -0.8173 -0.5528 -0.1626 +vn -0.9063 -0.1939 -0.3754 +vn -0.3584 0.9217 -0.1484 +vn -0.8544 -0.3805 -0.3539 +vn -0.5172 0.8286 -0.2142 +vn -0.7699 -0.5528 -0.3189 +vn -0.6561 0.7041 -0.2718 +vn -0.6561 -0.7041 -0.2718 +vn -0.7699 0.5528 -0.3189 +vn -0.5172 -0.8286 -0.2142 +vn -0.8544 0.3805 -0.3539 +vn -0.3584 -0.9217 -0.1484 +vn -0.9063 0.1939 -0.3754 +vn -0.1857 -0.9796 -0.0769 +vn -0.9239 -0.0000 -0.3827 +vn -0.1857 0.9796 -0.0769 +vn -0.5905 -0.7041 -0.3945 +vn -0.4654 -0.8286 -0.3110 +vn -0.7689 0.3805 -0.5138 +vn -0.3225 -0.9217 -0.2155 +vn -0.8157 0.1939 -0.5450 +vn -0.1671 -0.9796 -0.1117 +vn -0.8315 -0.0000 -0.5556 +vn -0.1671 0.9796 -0.1117 +vn -0.8157 -0.1939 -0.5450 +vn -0.3225 0.9217 -0.2155 +vn -0.7689 -0.3805 -0.5138 +vn -0.4654 0.8286 -0.3110 +vn -0.6929 -0.5528 -0.4630 +vn -0.5905 0.7041 -0.3945 +vn -0.6929 0.5528 -0.4630 +vn -0.6539 -0.3805 -0.6539 +vn -0.2743 0.9217 -0.2743 +vn -0.3958 0.8286 -0.3958 +vn -0.5893 -0.5528 -0.5893 +vn -0.5021 0.7041 -0.5021 +vn -0.5021 -0.7041 -0.5021 +vn -0.5893 0.5528 -0.5893 +vn -0.3958 -0.8286 -0.3958 +vn -0.6539 0.3805 -0.6539 +vn -0.2743 -0.9217 -0.2743 +vn -0.6937 0.1939 -0.6937 +vn -0.1421 -0.9796 -0.1421 +vn -0.7071 -0.0000 -0.7071 +vn -0.1421 0.9796 -0.1421 +vn -0.6937 -0.1939 -0.6937 +vn -0.2155 -0.9217 -0.3225 +vn -0.5138 0.3805 -0.7689 +vn -0.5450 0.1939 -0.8157 +vn -0.1117 -0.9796 -0.1671 +vn -0.5556 -0.0000 -0.8315 +vn -0.1117 0.9796 -0.1671 +vn -0.5450 -0.1939 -0.8157 +vn -0.2155 0.9217 -0.3225 +vn -0.5138 -0.3805 -0.7689 +vn -0.3110 0.8286 -0.4654 +vn -0.4630 -0.5528 -0.6929 +vn -0.3945 0.7041 -0.5905 +vn -0.3945 -0.7041 -0.5905 +vn -0.4630 0.5528 -0.6929 +vn -0.3110 -0.8286 -0.4654 +vn -0.2142 0.8286 -0.5172 +vn -0.3539 -0.3805 -0.8544 +vn -0.3189 -0.5528 -0.7699 +vn -0.2718 0.7041 -0.6561 +vn -0.2718 -0.7041 -0.6561 +vn -0.3189 0.5528 -0.7699 +vn -0.2142 -0.8286 -0.5172 +vn -0.3539 0.3805 -0.8544 +vn -0.1484 -0.9217 -0.3584 +vn -0.3754 0.1939 -0.9063 +vn -0.0769 -0.9796 -0.1857 +vn -0.3827 -0.0000 -0.9239 +vn -0.0769 0.9796 -0.1857 +vn -0.3754 -0.1939 -0.9063 +vn -0.1484 0.9217 -0.3584 +vn -0.1804 0.3805 -0.9070 +vn -0.1914 0.1939 -0.9622 +vn -0.0757 -0.9217 -0.3804 +vn -0.0392 -0.9796 -0.1971 +vn -0.1951 -0.0000 -0.9808 +vn -0.0392 0.9796 -0.1971 +vn -0.1914 -0.1939 -0.9622 +vn -0.0757 0.9217 -0.3804 +vn -0.1804 -0.3805 -0.9070 +vn -0.1092 0.8286 -0.5490 +vn -0.1626 -0.5528 -0.8173 +vn -0.1385 0.7041 -0.6965 +vn -0.1385 -0.7041 -0.6965 +vn -0.1626 0.5528 -0.8173 +vn -0.1092 -0.8286 -0.5490 +vt 0.750000 0.812500 +vt 0.750000 0.687500 +vt 0.750000 0.562500 +vt 0.750000 0.500000 +vt 0.750000 0.437500 +vt 0.750000 0.312500 +vt 0.718750 0.937500 +vt 0.718750 0.875000 +vt 0.718750 0.812500 +vt 0.718750 0.750000 +vt 0.718750 0.687500 +vt 0.718750 0.625000 +vt 0.718750 0.562500 +vt 0.718750 0.500000 +vt 0.718750 0.437500 +vt 0.718750 0.375000 +vt 0.718750 0.312500 +vt 0.718750 0.250000 +vt 0.718750 0.187500 +vt 0.718750 0.125000 +vt 0.718750 0.062500 +vt 0.687500 0.937500 +vt 0.687500 0.875000 +vt 0.687500 0.812500 +vt 0.687500 0.750000 +vt 0.687500 0.687500 +vt 0.687500 0.625000 +vt 0.687500 0.562500 +vt 0.687500 0.500000 +vt 0.687500 0.437500 +vt 0.687500 0.375000 +vt 0.687500 0.312500 +vt 0.687500 0.250000 +vt 0.687500 0.187500 +vt 0.687500 0.125000 +vt 0.687500 0.062500 +vt 0.656250 0.937500 +vt 0.656250 0.875000 +vt 0.656250 0.812500 +vt 0.656250 0.750000 +vt 0.656250 0.687500 +vt 0.656250 0.625000 +vt 0.656250 0.562500 +vt 0.656250 0.500000 +vt 0.656250 0.437500 +vt 0.656250 0.375000 +vt 0.656250 0.312500 +vt 0.656250 0.250000 +vt 0.656250 0.187500 +vt 0.656250 0.125000 +vt 0.656250 0.062500 +vt 0.625000 0.937500 +vt 0.625000 0.875000 +vt 0.625000 0.812500 +vt 0.625000 0.750000 +vt 0.625000 0.687500 +vt 0.625000 0.625000 +vt 0.625000 0.562500 +vt 0.625000 0.500000 +vt 0.625000 0.437500 +vt 0.625000 0.375000 +vt 0.625000 0.312500 +vt 0.625000 0.250000 +vt 0.625000 0.187500 +vt 0.625000 0.125000 +vt 0.625000 0.062500 +vt 0.593750 0.937500 +vt 0.593750 0.875000 +vt 0.593750 0.812500 +vt 0.593750 0.750000 +vt 0.593750 0.687500 +vt 0.593750 0.625000 +vt 0.593750 0.562500 +vt 0.593750 0.500000 +vt 0.593750 0.437500 +vt 0.593750 0.375000 +vt 0.593750 0.312500 +vt 0.593750 0.250000 +vt 0.593750 0.187500 +vt 0.593750 0.125000 +vt 0.593750 0.062500 +vt 0.734375 1.000000 +vt 0.703125 1.000000 +vt 0.671875 1.000000 +vt 0.640625 1.000000 +vt 0.609375 1.000000 +vt 0.578125 1.000000 +vt 0.546875 1.000000 +vt 0.515625 1.000000 +vt 0.484375 1.000000 +vt 0.453125 1.000000 +vt 0.421875 1.000000 +vt 0.390625 1.000000 +vt 0.359375 1.000000 +vt 0.328125 1.000000 +vt 0.296875 1.000000 +vt 0.265625 1.000000 +vt 0.234375 1.000000 +vt 0.203125 1.000000 +vt 0.171875 1.000000 +vt 0.140625 1.000000 +vt 0.109375 1.000000 +vt 0.078125 1.000000 +vt 0.046875 1.000000 +vt 0.015625 1.000000 +vt 0.984375 1.000000 +vt 0.953125 1.000000 +vt 0.921875 1.000000 +vt 0.890625 1.000000 +vt 0.859375 1.000000 +vt 0.828125 1.000000 +vt 0.796875 1.000000 +vt 0.765625 1.000000 +vt 0.562500 0.937500 +vt 0.562500 0.875000 +vt 0.562500 0.812500 +vt 0.562500 0.750000 +vt 0.562500 0.687500 +vt 0.562500 0.625000 +vt 0.562500 0.562500 +vt 0.562500 0.500000 +vt 0.562500 0.437500 +vt 0.562500 0.375000 +vt 0.562500 0.312500 +vt 0.562500 0.250000 +vt 0.562500 0.187500 +vt 0.562500 0.125000 +vt 0.562500 0.062500 +vt 0.531250 0.937500 +vt 0.531250 0.875000 +vt 0.531250 0.812500 +vt 0.531250 0.750000 +vt 0.531250 0.687500 +vt 0.531250 0.625000 +vt 0.531250 0.562500 +vt 0.531250 0.500000 +vt 0.531250 0.437500 +vt 0.531250 0.375000 +vt 0.531250 0.312500 +vt 0.531250 0.250000 +vt 0.531250 0.187500 +vt 0.531250 0.125000 +vt 0.531250 0.062500 +vt 0.500000 0.937500 +vt 0.500000 0.875000 +vt 0.500000 0.812500 +vt 0.500000 0.750000 +vt 0.500000 0.687500 +vt 0.500000 0.625000 +vt 0.500000 0.562500 +vt 0.500000 0.500000 +vt 0.500000 0.437500 +vt 0.500000 0.375000 +vt 0.500000 0.312500 +vt 0.500000 0.250000 +vt 0.500000 0.187500 +vt 0.500000 0.125000 +vt 0.500000 0.062500 +vt 0.468750 0.937500 +vt 0.468750 0.875000 +vt 0.468750 0.812500 +vt 0.468750 0.750000 +vt 0.468750 0.687500 +vt 0.468750 0.625000 +vt 0.468750 0.562500 +vt 0.468750 0.500000 +vt 0.468750 0.437500 +vt 0.468750 0.375000 +vt 0.468750 0.312500 +vt 0.468750 0.250000 +vt 0.468750 0.187500 +vt 0.468750 0.125000 +vt 0.468750 0.062500 +vt 0.437500 0.937500 +vt 0.437500 0.875000 +vt 0.437500 0.812500 +vt 0.437500 0.750000 +vt 0.437500 0.687500 +vt 0.437500 0.625000 +vt 0.437500 0.562500 +vt 0.437500 0.500000 +vt 0.437500 0.437500 +vt 0.437500 0.375000 +vt 0.437500 0.312500 +vt 0.437500 0.250000 +vt 0.437500 0.187500 +vt 0.437500 0.125000 +vt 0.437500 0.062500 +vt 0.406250 0.937500 +vt 0.406250 0.875000 +vt 0.406250 0.812500 +vt 0.406250 0.750000 +vt 0.406250 0.687500 +vt 0.406250 0.625000 +vt 0.406250 0.562500 +vt 0.406250 0.500000 +vt 0.406250 0.437500 +vt 0.406250 0.375000 +vt 0.406250 0.312500 +vt 0.406250 0.250000 +vt 0.406250 0.187500 +vt 0.406250 0.125000 +vt 0.406250 0.062500 +vt 0.375000 0.937500 +vt 0.375000 0.875000 +vt 0.375000 0.812500 +vt 0.375000 0.750000 +vt 0.375000 0.687500 +vt 0.375000 0.625000 +vt 0.375000 0.562500 +vt 0.375000 0.500000 +vt 0.375000 0.437500 +vt 0.375000 0.375000 +vt 0.375000 0.312500 +vt 0.375000 0.250000 +vt 0.375000 0.187500 +vt 0.375000 0.125000 +vt 0.375000 0.062500 +vt 0.343750 0.937500 +vt 0.343750 0.875000 +vt 0.343750 0.812500 +vt 0.343750 0.750000 +vt 0.343750 0.687500 +vt 0.343750 0.625000 +vt 0.343750 0.562500 +vt 0.343750 0.500000 +vt 0.343750 0.437500 +vt 0.343750 0.375000 +vt 0.343750 0.312500 +vt 0.343750 0.250000 +vt 0.343750 0.187500 +vt 0.343750 0.125000 +vt 0.343750 0.062500 +vt 0.312500 0.937500 +vt 0.312500 0.875000 +vt 0.312500 0.812500 +vt 0.312500 0.750000 +vt 0.312500 0.687500 +vt 0.312500 0.625000 +vt 0.312500 0.562500 +vt 0.312500 0.500000 +vt 0.312500 0.437500 +vt 0.312500 0.375000 +vt 0.312500 0.312500 +vt 0.312500 0.250000 +vt 0.312500 0.187500 +vt 0.312500 0.125000 +vt 0.312500 0.062500 +vt 0.281250 0.937500 +vt 0.281250 0.875000 +vt 0.281250 0.812500 +vt 0.281250 0.750000 +vt 0.281250 0.687500 +vt 0.281250 0.625000 +vt 0.281250 0.562500 +vt 0.281250 0.500000 +vt 0.281250 0.437500 +vt 0.281250 0.375000 +vt 0.281250 0.312500 +vt 0.281250 0.250000 +vt 0.281250 0.187500 +vt 0.281250 0.125000 +vt 0.281250 0.062500 +vt 0.250000 0.937500 +vt 0.250000 0.875000 +vt 0.250000 0.812500 +vt 0.250000 0.750000 +vt 0.250000 0.687500 +vt 0.250000 0.625000 +vt 0.250000 0.562500 +vt 0.250000 0.500000 +vt 0.250000 0.437500 +vt 0.250000 0.375000 +vt 0.250000 0.312500 +vt 0.250000 0.250000 +vt 0.250000 0.187500 +vt 0.250000 0.125000 +vt 0.250000 0.062500 +vt 0.218750 0.937500 +vt 0.218750 0.875000 +vt 0.218750 0.812500 +vt 0.218750 0.750000 +vt 0.218750 0.687500 +vt 0.218750 0.625000 +vt 0.218750 0.562500 +vt 0.218750 0.500000 +vt 0.218750 0.437500 +vt 0.218750 0.375000 +vt 0.218750 0.312500 +vt 0.218750 0.250000 +vt 0.218750 0.187500 +vt 0.218750 0.125000 +vt 0.218750 0.062500 +vt 0.187500 0.937500 +vt 0.187500 0.875000 +vt 0.187500 0.812500 +vt 0.187500 0.750000 +vt 0.187500 0.687500 +vt 0.187500 0.625000 +vt 0.187500 0.562500 +vt 0.187500 0.500000 +vt 0.187500 0.437500 +vt 0.187500 0.375000 +vt 0.187500 0.312500 +vt 0.187500 0.250000 +vt 0.187500 0.187500 +vt 0.187500 0.125000 +vt 0.187500 0.062500 +vt 0.156250 0.937500 +vt 0.156250 0.875000 +vt 0.156250 0.812500 +vt 0.156250 0.750000 +vt 0.156250 0.687500 +vt 0.156250 0.625000 +vt 0.156250 0.562500 +vt 0.156250 0.500000 +vt 0.156250 0.437500 +vt 0.156250 0.375000 +vt 0.156250 0.312500 +vt 0.156250 0.250000 +vt 0.156250 0.187500 +vt 0.156250 0.125000 +vt 0.156250 0.062500 +vt 0.125000 0.937500 +vt 0.125000 0.875000 +vt 0.125000 0.812500 +vt 0.125000 0.750000 +vt 0.125000 0.687500 +vt 0.125000 0.625000 +vt 0.125000 0.562500 +vt 0.125000 0.500000 +vt 0.125000 0.437500 +vt 0.125000 0.375000 +vt 0.125000 0.312500 +vt 0.125000 0.250000 +vt 0.125000 0.187500 +vt 0.125000 0.125000 +vt 0.125000 0.062500 +vt 0.734375 0.000000 +vt 0.703125 0.000000 +vt 0.671875 0.000000 +vt 0.640625 0.000000 +vt 0.609375 0.000000 +vt 0.578125 0.000000 +vt 0.546875 0.000000 +vt 0.515625 0.000000 +vt 0.484375 0.000000 +vt 0.453125 0.000000 +vt 0.421875 0.000000 +vt 0.390625 0.000000 +vt 0.359375 0.000000 +vt 0.328125 0.000000 +vt 0.296875 0.000000 +vt 0.265625 0.000000 +vt 0.234375 0.000000 +vt 0.203125 0.000000 +vt 0.171875 0.000000 +vt 0.140625 0.000000 +vt 0.109375 0.000000 +vt 0.078125 0.000000 +vt 0.046875 0.000000 +vt 0.015625 0.000000 +vt 0.984375 0.000000 +vt 0.953125 0.000000 +vt 0.921875 0.000000 +vt 0.890625 0.000000 +vt 0.859375 0.000000 +vt 0.828125 0.000000 +vt 0.796875 0.000000 +vt 0.765625 0.000000 +vt 0.093750 0.937500 +vt 0.093750 0.875000 +vt 0.093750 0.812500 +vt 0.093750 0.750000 +vt 0.093750 0.687500 +vt 0.093750 0.625000 +vt 0.093750 0.562500 +vt 0.093750 0.500000 +vt 0.093750 0.437500 +vt 0.093750 0.375000 +vt 0.093750 0.312500 +vt 0.093750 0.250000 +vt 0.093750 0.187500 +vt 0.093750 0.125000 +vt 0.093750 0.062500 +vt 0.062500 0.937500 +vt 0.062500 0.875000 +vt 0.062500 0.812500 +vt 0.062500 0.750000 +vt 0.062500 0.687500 +vt 0.062500 0.625000 +vt 0.062500 0.562500 +vt 0.062500 0.500000 +vt 0.062500 0.437500 +vt 0.062500 0.375000 +vt 0.062500 0.312500 +vt 0.062500 0.250000 +vt 0.062500 0.187500 +vt 0.062500 0.125000 +vt 0.062500 0.062500 +vt 0.031250 0.937500 +vt 0.031250 0.875000 +vt 0.031250 0.812500 +vt 0.031250 0.750000 +vt 0.031250 0.687500 +vt 0.031250 0.625000 +vt 0.031250 0.562500 +vt 0.031250 0.500000 +vt 0.031250 0.437500 +vt 0.031250 0.375000 +vt 0.031250 0.312500 +vt 0.031250 0.250000 +vt 0.031250 0.187500 +vt 0.031250 0.125000 +vt 0.031250 0.062500 +vt 0.000000 0.937500 +vt 1.000000 0.937500 +vt 0.000000 0.875000 +vt 1.000000 0.875000 +vt 0.000000 0.812500 +vt 1.000000 0.812500 +vt 0.000000 0.750000 +vt 1.000000 0.750000 +vt 0.000000 0.687500 +vt 1.000000 0.687500 +vt 0.000000 0.625000 +vt 1.000000 0.625000 +vt 0.000000 0.562500 +vt 1.000000 0.562500 +vt 0.000000 0.500000 +vt 1.000000 0.500000 +vt 0.000000 0.437500 +vt 1.000000 0.437500 +vt 0.000000 0.375000 +vt 1.000000 0.375000 +vt 0.000000 0.312500 +vt 1.000000 0.312500 +vt 0.000000 0.250000 +vt 1.000000 0.250000 +vt 0.000000 0.187500 +vt 1.000000 0.187500 +vt 0.000000 0.125000 +vt 1.000000 0.125000 +vt 1.000000 0.062500 +vt 0.000000 0.062500 +vt 0.968750 0.937500 +vt 0.968750 0.875000 +vt 0.968750 0.812500 +vt 0.968750 0.750000 +vt 0.968750 0.687500 +vt 0.968750 0.625000 +vt 0.968750 0.562500 +vt 0.968750 0.500000 +vt 0.968750 0.437500 +vt 0.968750 0.375000 +vt 0.968750 0.312500 +vt 0.968750 0.250000 +vt 0.968750 0.187500 +vt 0.968750 0.125000 +vt 0.968750 0.062500 +vt 0.937500 0.937500 +vt 0.937500 0.875000 +vt 0.937500 0.812500 +vt 0.937500 0.750000 +vt 0.937500 0.687500 +vt 0.937500 0.625000 +vt 0.937500 0.562500 +vt 0.937500 0.500000 +vt 0.937500 0.437500 +vt 0.937500 0.375000 +vt 0.937500 0.312500 +vt 0.937500 0.250000 +vt 0.937500 0.187500 +vt 0.937500 0.125000 +vt 0.937500 0.062500 +vt 0.906250 0.937500 +vt 0.906250 0.875000 +vt 0.906250 0.812500 +vt 0.906250 0.750000 +vt 0.906250 0.687500 +vt 0.906250 0.625000 +vt 0.906250 0.562500 +vt 0.906250 0.500000 +vt 0.906250 0.437500 +vt 0.906250 0.375000 +vt 0.906250 0.312500 +vt 0.906250 0.250000 +vt 0.906250 0.187500 +vt 0.906250 0.125000 +vt 0.906250 0.062500 +vt 0.875000 0.937500 +vt 0.875000 0.875000 +vt 0.875000 0.812500 +vt 0.875000 0.750000 +vt 0.875000 0.687500 +vt 0.875000 0.625000 +vt 0.875000 0.562500 +vt 0.875000 0.500000 +vt 0.875000 0.437500 +vt 0.875000 0.375000 +vt 0.875000 0.312500 +vt 0.875000 0.250000 +vt 0.875000 0.187500 +vt 0.875000 0.125000 +vt 0.875000 0.062500 +vt 0.843750 0.937500 +vt 0.843750 0.875000 +vt 0.843750 0.812500 +vt 0.843750 0.750000 +vt 0.843750 0.687500 +vt 0.843750 0.625000 +vt 0.843750 0.562500 +vt 0.843750 0.500000 +vt 0.843750 0.437500 +vt 0.843750 0.375000 +vt 0.843750 0.312500 +vt 0.843750 0.250000 +vt 0.843750 0.187500 +vt 0.843750 0.125000 +vt 0.843750 0.062500 +vt 0.812500 0.937500 +vt 0.812500 0.875000 +vt 0.812500 0.812500 +vt 0.812500 0.750000 +vt 0.812500 0.687500 +vt 0.812500 0.625000 +vt 0.812500 0.562500 +vt 0.812500 0.500000 +vt 0.812500 0.437500 +vt 0.812500 0.375000 +vt 0.812500 0.312500 +vt 0.812500 0.250000 +vt 0.812500 0.187500 +vt 0.812500 0.125000 +vt 0.812500 0.062500 +vt 0.781250 0.937500 +vt 0.781250 0.875000 +vt 0.781250 0.812500 +vt 0.781250 0.750000 +vt 0.781250 0.687500 +vt 0.781250 0.625000 +vt 0.781250 0.562500 +vt 0.781250 0.500000 +vt 0.781250 0.437500 +vt 0.781250 0.375000 +vt 0.781250 0.312500 +vt 0.781250 0.250000 +vt 0.781250 0.187500 +vt 0.781250 0.125000 +vt 0.781250 0.062500 +vt 0.750000 0.937500 +vt 0.750000 0.875000 +vt 0.750000 0.750000 +vt 0.750000 0.625000 +vt 0.750000 0.375000 +vt 0.750000 0.250000 +vt 0.750000 0.187500 +vt 0.750000 0.125000 +vt 0.750000 0.062500 +s 1 +f 483/560/483 490/567/484 491/568/485 +f 960/1114/486 499/576/487 488/565/488 +f 958/1112/489 491/568/485 492/569/490 +f 488/565/488 500/577/491 961/1115/492 +f 484/561/493 492/569/490 493/570/494 +f 961/1115/492 501/578/495 962/1116/496 +f 959/1113/497 493/570/494 494/571/498 +f 962/1116/496 502/579/499 963/1117/500 +f 485/562/501 494/571/498 495/572/502 +f 963/1117/500 503/580/503 964/1118/504 +f 485/562/501 496/573/505 486/563/506 +f 956/1110/507 564/641/508 489/566/509 +f 790/898/510 964/1118/504 503/580/503 +f 486/563/506 497/574/511 487/564/512 +f 957/1111/513 489/566/509 490/567/484 +f 487/564/512 498/575/514 960/1114/486 +f 489/566/509 505/582/515 490/567/484 +f 497/574/511 513/590/516 498/575/514 +f 490/567/484 506/583/517 491/568/485 +f 498/575/514 514/591/518 499/576/487 +f 491/568/485 507/584/519 492/569/490 +f 499/576/487 515/592/520 500/577/491 +f 492/569/490 508/585/521 493/570/494 +f 501/578/495 515/592/520 516/593/522 +f 493/570/494 509/586/523 494/571/498 +f 502/579/499 516/593/522 517/594/524 +f 495/572/502 509/586/523 510/587/525 +f 502/579/499 518/595/526 503/580/503 +f 495/572/502 511/588/527 496/573/505 +f 489/566/509 564/642/508 504/581/528 +f 790/899/510 503/580/503 518/595/526 +f 496/573/505 512/589/529 497/574/511 +f 508/585/521 524/601/530 509/586/523 +f 516/593/522 532/609/531 517/594/524 +f 509/586/523 525/602/532 510/587/525 +f 517/594/524 533/610/533 518/595/526 +f 510/587/525 526/603/534 511/588/527 +f 504/581/528 564/643/508 519/596/535 +f 790/900/510 518/595/526 533/610/533 +f 512/589/529 526/603/534 527/604/536 +f 504/581/528 520/597/537 505/582/515 +f 512/589/529 528/605/538 513/590/516 +f 505/582/515 521/598/539 506/583/517 +f 514/591/518 528/605/538 529/606/540 +f 506/583/517 522/599/541 507/584/519 +f 515/592/520 529/606/540 530/607/542 +f 507/584/519 523/600/543 508/585/521 +f 516/593/522 530/607/542 531/608/544 +f 527/604/536 543/620/545 528/605/538 +f 520/597/537 536/613/546 521/598/539 +f 529/606/540 543/620/545 544/621/547 +f 521/598/539 537/614/548 522/599/541 +f 529/606/540 545/622/549 530/607/542 +f 522/599/541 538/615/550 523/600/543 +f 530/607/542 546/623/551 531/608/544 +f 523/600/543 539/616/552 524/601/530 +f 531/608/544 547/624/553 532/609/531 +f 525/602/532 539/616/552 540/617/554 +f 532/609/531 548/625/555 533/610/533 +f 526/603/534 540/617/554 541/618/556 +f 519/596/535 564/644/508 534/611/557 +f 790/901/510 533/610/533 548/625/555 +f 526/603/534 542/619/558 527/604/536 +f 519/596/535 535/612/559 520/597/537 +f 547/624/553 561/638/560 562/639/561 +f 540/617/554 554/631/562 555/632/563 +f 547/624/553 563/640/564 548/625/555 +f 541/618/556 555/632/563 556/633/565 +f 534/611/557 564/645/508 549/626/566 +f 790/902/510 548/625/555 563/640/564 +f 541/618/556 557/634/567 542/619/558 +f 534/611/557 550/627/568 535/612/559 +f 542/619/558 558/635/569 543/620/545 +f 535/612/559 551/628/570 536/613/546 +f 544/621/547 558/635/569 559/636/571 +f 536/613/546 552/629/572 537/614/548 +f 544/621/547 560/637/573 545/622/549 +f 538/615/550 552/629/572 553/630/574 +f 545/622/549 561/638/560 546/623/551 +f 538/615/550 554/631/562 539/616/552 +f 550/627/568 567/675/575 551/628/570 +f 559/636/571 574/682/576 575/683/577 +f 551/628/570 568/676/578 552/629/572 +f 559/636/571 576/684/579 560/637/573 +f 553/630/574 568/676/578 569/677/580 +f 560/637/573 577/685/581 561/638/560 +f 553/630/574 570/678/582 554/631/562 +f 561/638/560 578/686/583 562/639/561 +f 555/632/563 570/678/582 571/679/584 +f 563/640/564 578/686/583 579/687/585 +f 556/633/565 571/679/584 572/680/586 +f 549/626/566 564/646/508 565/673/587 +f 790/903/510 563/640/564 579/687/585 +f 556/633/565 573/681/588 557/634/567 +f 549/626/566 566/674/589 550/627/568 +f 557/634/567 574/682/576 558/635/569 +f 571/679/584 585/693/590 586/694/591 +f 578/686/583 594/702/592 579/687/585 +f 572/680/586 586/694/591 587/695/593 +f 565/673/587 564/647/508 580/688/594 +f 790/904/510 579/687/585 594/702/592 +f 572/680/586 588/696/595 573/681/588 +f 566/674/589 580/688/594 581/689/596 +f 573/681/588 589/697/597 574/682/576 +f 566/674/589 582/690/598 567/675/575 +f 575/683/577 589/697/597 590/698/599 +f 567/675/575 583/691/600 568/676/578 +f 575/683/577 591/699/601 576/684/579 +f 569/677/580 583/691/600 584/692/602 +f 577/685/581 591/699/601 592/700/603 +f 569/677/580 585/693/590 570/678/582 +f 578/686/583 592/700/603 593/701/604 +f 590/698/599 604/712/605 605/713/606 +f 582/690/598 598/706/607 583/691/600 +f 590/698/599 606/714/608 591/699/601 +f 584/692/602 598/706/607 599/707/609 +f 592/700/603 606/714/608 607/715/610 +f 584/692/602 600/708/611 585/693/590 +f 592/700/603 608/716/612 593/701/604 +f 586/694/591 600/708/611 601/709/613 +f 593/701/604 609/717/614 594/702/592 +f 587/695/593 601/709/613 602/710/615 +f 580/688/594 564/648/508 595/703/616 +f 790/905/510 594/702/592 609/717/614 +f 587/695/593 603/711/617 588/696/595 +f 581/689/596 595/703/616 596/704/618 +f 588/696/595 604/712/605 589/697/597 +f 581/689/596 597/705/619 582/690/598 +f 608/716/612 624/732/620 609/717/614 +f 602/710/615 616/724/621 617/725/622 +f 595/703/616 564/649/508 610/718/623 +f 790/906/510 609/717/614 624/732/620 +f 602/710/615 618/726/624 603/711/617 +f 595/703/616 611/719/625 596/704/618 +f 603/711/617 619/727/626 604/712/605 +f 596/704/618 612/720/627 597/705/619 +f 605/713/606 619/727/626 620/728/628 +f 597/705/619 613/721/629 598/706/607 +f 605/713/606 621/729/630 606/714/608 +f 599/707/609 613/721/629 614/722/631 +f 607/715/610 621/729/630 622/730/632 +f 599/707/609 615/723/633 600/708/611 +f 607/715/610 623/731/634 608/716/612 +f 601/709/613 615/723/633 616/724/621 +f 620/728/628 636/744/635 621/729/630 +f 614/722/631 628/736/636 629/737/637 +f 622/730/632 636/744/635 637/745/638 +f 614/722/631 630/738/639 615/723/633 +f 622/730/632 638/746/640 623/731/634 +f 616/724/621 630/738/639 631/739/641 +f 624/732/620 638/746/640 639/747/642 +f 617/725/622 631/739/641 632/740/643 +f 610/718/623 564/650/508 625/733/644 +f 790/907/510 624/732/620 639/747/642 +f 617/725/622 633/741/645 618/726/624 +f 610/718/623 626/734/646 611/719/625 +f 618/726/624 634/742/647 619/727/626 +f 612/720/627 626/734/646 627/735/648 +f 620/728/628 634/742/647 635/743/649 +f 612/720/627 628/736/636 613/721/629 +f 625/733/644 564/651/508 640/748/650 +f 790/908/510 639/747/642 654/762/651 +f 632/740/643 648/756/652 633/741/645 +f 625/733/644 641/749/653 626/734/646 +f 633/741/645 649/757/654 634/742/647 +f 627/735/648 641/749/653 642/750/655 +f 635/743/649 649/757/654 650/758/656 +f 627/735/648 643/751/657 628/736/636 +f 635/743/649 651/759/658 636/744/635 +f 629/737/637 643/751/657 644/752/659 +f 637/745/638 651/759/658 652/760/660 +f 629/737/637 645/753/661 630/738/639 +f 637/745/638 653/761/662 638/746/640 +f 631/739/641 645/753/661 646/754/663 +f 638/746/640 654/762/651 639/747/642 +f 632/740/643 646/754/663 647/755/664 +f 644/752/659 658/766/665 659/767/666 +f 651/759/658 667/775/667 652/760/660 +f 644/752/659 660/768/668 645/753/661 +f 653/761/662 667/775/667 668/776/669 +f 646/754/663 660/768/668 661/769/670 +f 654/762/651 668/776/669 669/777/671 +f 647/755/664 661/769/670 662/770/672 +f 640/748/650 564/652/508 655/763/673 +f 790/909/510 654/762/651 669/777/671 +f 647/755/664 663/771/674 648/756/652 +f 640/748/650 656/764/675 641/749/653 +f 648/756/652 664/772/676 649/757/654 +f 641/749/653 657/765/677 642/750/655 +f 650/758/656 664/772/676 665/773/678 +f 642/750/655 658/766/665 643/751/657 +f 650/758/656 666/774/679 651/759/658 +f 662/770/672 678/786/680 663/771/674 +f 656/764/675 670/778/681 671/779/682 +f 663/771/674 679/787/683 664/772/676 +f 657/765/677 671/779/682 672/780/684 +f 665/773/678 679/787/683 680/788/685 +f 657/765/677 673/781/686 658/766/665 +f 665/773/678 681/789/687 666/774/679 +f 659/767/666 673/781/686 674/782/688 +f 667/775/667 681/789/687 682/790/689 +f 659/767/666 675/783/690 660/768/668 +f 667/775/667 683/791/691 668/776/669 +f 661/769/670 675/783/690 676/784/692 +f 668/776/669 684/792/693 669/777/671 +f 662/770/672 676/784/692 677/785/694 +f 655/763/673 564/653/508 670/778/681 +f 790/910/510 669/777/671 684/792/693 +f 682/790/689 696/804/695 697/805/696 +f 674/782/688 690/798/697 675/783/690 +f 682/790/689 698/806/698 683/791/691 +f 676/784/692 690/798/697 691/799/699 +f 683/791/691 699/807/700 684/792/693 +f 677/785/694 691/799/699 692/800/701 +f 670/778/681 564/654/508 685/793/702 +f 790/911/510 684/792/693 699/807/700 +f 677/785/694 693/801/703 678/786/680 +f 670/778/681 686/794/704 671/779/682 +f 678/786/680 694/802/705 679/787/683 +f 671/779/682 687/795/706 672/780/684 +f 680/788/685 694/802/705 695/803/707 +f 672/780/684 688/796/708 673/781/686 +f 680/788/685 696/804/695 681/789/687 +f 674/782/688 688/796/708 689/797/709 +f 685/793/702 701/809/710 686/794/704 +f 693/801/703 709/817/711 694/802/705 +f 686/794/704 702/810/712 687/795/706 +f 695/803/707 709/817/711 710/818/713 +f 687/795/706 703/811/714 688/796/708 +f 695/803/707 711/819/715 696/804/695 +f 689/797/709 703/811/714 704/812/716 +f 697/805/696 711/819/715 712/820/717 +f 689/797/709 705/813/718 690/798/697 +f 697/805/696 713/821/719 698/806/698 +f 691/799/699 705/813/718 706/814/720 +f 698/806/698 714/822/721 699/807/700 +f 692/800/701 706/814/720 707/815/722 +f 685/793/702 564/655/508 700/808/723 +f 790/912/510 699/807/700 714/822/721 +f 692/800/701 708/816/724 693/801/703 +f 704/812/716 720/828/725 705/813/718 +f 712/820/717 728/836/726 713/821/719 +f 706/814/720 720/828/725 721/829/727 +f 714/822/721 728/836/726 729/837/728 +f 707/815/722 721/829/727 722/830/729 +f 700/808/723 564/656/508 715/823/730 +f 790/913/510 714/822/721 729/837/728 +f 707/815/722 723/831/731 708/816/724 +f 700/808/723 716/824/732 701/809/710 +f 708/816/724 724/832/733 709/817/711 +f 702/810/712 716/824/732 717/825/734 +f 710/818/713 724/832/733 725/833/735 +f 702/810/712 718/826/736 703/811/714 +f 710/818/713 726/834/737 711/819/715 +f 704/812/716 718/826/736 719/827/738 +f 712/820/717 726/834/737 727/835/739 +f 723/831/731 739/847/740 724/832/733 +f 716/824/732 732/840/741 717/825/734 +f 725/833/735 739/847/740 740/848/742 +f 717/825/734 733/841/743 718/826/736 +f 725/833/735 741/849/744 726/834/737 +f 719/827/738 733/841/743 734/842/745 +f 727/835/739 741/849/744 742/850/746 +f 719/827/738 735/843/747 720/828/725 +f 727/835/739 743/851/748 728/836/726 +f 721/829/727 735/843/747 736/844/749 +f 728/836/726 744/852/750 729/837/728 +f 722/830/729 736/844/749 737/845/751 +f 715/823/730 564/657/508 730/838/752 +f 790/914/510 729/837/728 744/852/750 +f 722/830/729 738/846/753 723/831/731 +f 715/823/730 731/839/754 716/824/732 +f 742/850/746 758/866/755 743/851/748 +f 736/844/749 750/858/756 751/859/757 +f 743/851/748 759/867/758 744/852/750 +f 737/845/751 751/859/757 752/860/759 +f 730/838/752 564/658/508 745/853/760 +f 790/915/510 744/852/750 759/867/758 +f 737/845/751 753/861/761 738/846/753 +f 731/839/754 745/853/760 746/854/762 +f 738/846/753 754/862/763 739/847/740 +f 731/839/754 747/855/764 732/840/741 +f 740/848/742 754/862/763 755/863/765 +f 732/840/741 748/856/766 733/841/743 +f 740/848/742 756/864/767 741/849/744 +f 734/842/745 748/856/766 749/857/768 +f 742/850/746 756/864/767 757/865/769 +f 734/842/745 750/858/756 735/843/747 +f 755/863/765 769/877/770 770/878/771 +f 747/855/764 763/871/772 748/856/766 +f 755/863/765 771/879/773 756/864/767 +f 749/857/768 763/871/772 764/872/774 +f 757/865/769 771/879/773 772/880/775 +f 749/857/768 765/873/776 750/858/756 +f 757/865/769 773/881/777 758/866/755 +f 751/859/757 765/873/776 766/874/778 +f 759/867/758 773/881/777 774/882/779 +f 752/860/759 766/874/778 767/875/780 +f 745/853/760 564/659/508 760/868/781 +f 790/916/510 759/867/758 774/882/779 +f 752/860/759 768/876/782 753/861/761 +f 746/854/762 760/868/781 761/869/783 +f 753/861/761 769/877/770 754/862/763 +f 746/854/762 762/870/784 747/855/764 +f 774/882/779 788/896/785 789/897/786 +f 767/875/780 781/889/787 782/890/788 +f 760/868/781 564/660/508 775/883/789 +f 790/917/510 774/882/779 789/897/786 +f 767/875/780 783/891/790 768/876/782 +f 760/868/781 776/884/791 761/869/783 +f 768/876/782 784/892/792 769/877/770 +f 761/869/783 777/885/793 762/870/784 +f 770/878/771 784/892/792 785/893/794 +f 762/870/784 778/886/795 763/871/772 +f 770/878/771 786/894/796 771/879/773 +f 764/872/774 778/886/795 779/887/797 +f 772/880/775 786/894/796 787/895/798 +f 764/872/774 780/888/799 765/873/776 +f 772/880/775 788/896/785 773/881/777 +f 766/874/778 780/888/799 781/889/787 +f 777/885/793 794/933/800 778/886/795 +f 785/893/794 802/941/801 786/894/796 +f 779/887/797 794/933/800 795/934/802 +f 787/895/798 802/941/801 803/942/803 +f 779/887/797 796/935/804 780/888/799 +f 787/895/798 804/943/805 788/896/785 +f 781/889/787 796/935/804 797/936/806 +f 788/896/785 805/944/807 789/897/786 +f 782/890/788 797/936/806 798/937/808 +f 775/883/789 564/661/508 791/930/809 +f 790/918/510 789/897/786 805/944/807 +f 782/890/788 799/938/810 783/891/790 +f 775/883/789 792/931/811 776/884/791 +f 783/891/790 800/939/812 784/892/792 +f 776/884/791 793/932/813 777/885/793 +f 785/893/794 800/939/812 801/940/814 +f 798/937/808 812/951/815 813/952/816 +f 791/930/809 564/662/508 806/945/817 +f 790/919/510 805/944/807 820/959/818 +f 798/937/808 814/953/819 799/938/810 +f 791/930/809 807/946/820 792/931/811 +f 799/938/810 815/954/821 800/939/812 +f 792/931/811 808/947/822 793/932/813 +f 801/940/814 815/954/821 816/955/823 +f 793/932/813 809/948/824 794/933/800 +f 801/940/814 817/956/825 802/941/801 +f 794/933/800 810/949/826 795/934/802 +f 803/942/803 817/956/825 818/957/827 +f 795/934/802 811/950/828 796/935/804 +f 803/942/803 819/958/829 804/943/805 +f 797/936/806 811/950/828 812/951/815 +f 804/943/805 820/959/818 805/944/807 +f 816/955/823 832/971/830 817/956/825 +f 810/949/826 824/963/831 825/964/832 +f 818/957/827 832/971/830 833/972/833 +f 810/949/826 826/965/834 811/950/828 +f 818/957/827 834/973/835 819/958/829 +f 812/951/815 826/965/834 827/966/836 +f 819/958/829 835/974/837 820/959/818 +f 813/952/816 827/966/836 828/967/838 +f 806/945/817 564/663/508 821/960/839 +f 790/920/510 820/959/818 835/974/837 +f 813/952/816 829/968/840 814/953/819 +f 806/945/817 822/961/841 807/946/820 +f 814/953/819 830/969/842 815/954/821 +f 807/946/820 823/962/843 808/947/822 +f 816/955/823 830/969/842 831/970/844 +f 808/947/822 824/963/831 809/948/824 +f 821/960/839 564/664/508 836/975/845 +f 790/921/510 835/974/837 850/1004/846 +f 828/967/838 844/991/847 829/968/840 +f 821/960/839 837/977/848 822/961/841 +f 829/968/840 845/993/849 830/969/842 +f 823/962/843 837/977/848 838/979/850 +f 831/970/844 845/993/849 846/995/851 +f 823/962/843 839/981/852 824/963/831 +f 831/970/844 847/997/853 832/971/830 +f 825/964/832 839/981/852 840/983/854 +f 833/972/833 847/997/853 848/999/855 +f 825/964/832 841/985/856 826/965/834 +f 833/972/833 849/1001/857 834/973/835 +f 827/966/836 841/985/856 842/987/858 +f 834/973/835 850/1004/846 835/974/837 +f 828/967/838 842/987/858 843/989/859 +f 840/984/854 854/1008/860 855/1009/861 +f 848/1000/855 862/1016/862 863/1017/863 +f 840/984/854 856/1010/864 841/986/856 +f 848/1000/855 864/1018/865 849/1002/857 +f 842/988/858 856/1010/864 857/1011/866 +f 849/1002/857 865/1019/867 850/1003/846 +f 843/990/859 857/1011/866 858/1012/868 +f 836/976/845 564/665/508 851/1005/869 +f 790/922/510 850/1003/846 865/1019/867 +f 843/990/859 859/1013/870 844/992/847 +f 836/976/845 852/1006/871 837/978/848 +f 844/992/847 860/1014/872 845/994/849 +f 838/980/850 852/1006/871 853/1007/873 +f 846/996/851 860/1014/872 861/1015/874 +f 838/980/850 854/1008/860 839/982/852 +f 846/996/851 862/1016/862 847/998/853 +f 858/1012/868 874/1028/875 859/1013/870 +f 851/1005/869 867/1021/876 852/1006/871 +f 859/1013/870 875/1029/877 860/1014/872 +f 853/1007/873 867/1021/876 868/1022/878 +f 861/1015/874 875/1029/877 876/1030/879 +f 853/1007/873 869/1023/880 854/1008/860 +f 861/1015/874 877/1031/881 862/1016/862 +f 855/1009/861 869/1023/880 870/1024/882 +f 863/1017/863 877/1031/881 878/1032/883 +f 855/1009/861 871/1025/884 856/1010/864 +f 863/1017/863 879/1033/885 864/1018/865 +f 857/1011/866 871/1025/884 872/1026/886 +f 864/1018/865 880/1034/887 865/1019/867 +f 858/1012/868 872/1026/886 873/1027/888 +f 851/1005/869 564/666/508 866/1020/889 +f 790/923/510 865/1019/867 880/1034/887 +f 878/1032/883 892/1046/890 893/1047/891 +f 870/1024/882 886/1040/892 871/1025/884 +f 879/1033/885 893/1047/891 894/1048/893 +f 872/1026/886 886/1040/892 887/1041/894 +f 879/1033/885 895/1049/895 880/1034/887 +f 873/1027/888 887/1041/894 888/1042/896 +f 866/1020/889 564/667/508 881/1035/897 +f 790/924/510 880/1034/887 895/1049/895 +f 873/1027/888 889/1043/898 874/1028/875 +f 867/1021/876 881/1035/897 882/1036/899 +f 874/1028/875 890/1044/900 875/1029/877 +f 867/1021/876 883/1037/901 868/1022/878 +f 876/1030/879 890/1044/900 891/1045/902 +f 868/1022/878 884/1038/903 869/1023/880 +f 876/1030/879 892/1046/890 877/1031/881 +f 870/1024/882 884/1038/903 885/1039/904 +f 889/1043/898 905/1059/905 890/1044/900 +f 883/1037/901 897/1051/906 898/1052/907 +f 891/1045/902 905/1059/905 906/1060/908 +f 883/1037/901 899/1053/909 884/1038/903 +f 891/1045/902 907/1061/910 892/1046/890 +f 885/1039/904 899/1053/909 900/1054/911 +f 893/1047/891 907/1061/910 908/1062/912 +f 885/1039/904 901/1055/913 886/1040/892 +f 893/1047/891 909/1063/914 894/1048/893 +f 887/1041/894 901/1055/913 902/1056/915 +f 894/1048/893 910/1064/916 895/1049/895 +f 888/1042/896 902/1056/915 903/1057/917 +f 881/1035/897 564/668/508 896/1050/918 +f 790/925/510 895/1049/895 910/1064/916 +f 888/1042/896 904/1058/919 889/1043/898 +f 881/1035/897 897/1051/906 882/1036/899 +f 908/1062/912 924/1078/920 909/1063/914 +f 902/1056/915 916/1070/921 917/1071/922 +f 909/1063/914 925/1079/923 910/1064/916 +f 903/1057/917 917/1071/922 918/1072/924 +f 896/1050/918 564/669/508 911/1065/925 +f 790/926/510 910/1064/916 925/1079/923 +f 903/1057/917 919/1073/926 904/1058/919 +f 896/1050/918 912/1066/927 897/1051/906 +f 904/1058/919 920/1074/928 905/1059/905 +f 898/1052/907 912/1066/927 913/1067/929 +f 906/1060/908 920/1074/928 921/1075/930 +f 898/1052/907 914/1068/931 899/1053/909 +f 906/1060/908 922/1076/932 907/1061/910 +f 900/1054/911 914/1068/931 915/1069/933 +f 908/1062/912 922/1076/932 923/1077/934 +f 900/1054/911 916/1070/921 901/1055/913 +f 912/1066/927 928/1082/935 913/1067/929 +f 921/1075/930 935/1089/936 936/1090/937 +f 913/1067/929 929/1083/938 914/1068/931 +f 921/1075/930 937/1091/939 922/1076/932 +f 915/1069/933 929/1083/938 930/1084/940 +f 923/1077/934 937/1091/939 938/1092/941 +f 915/1069/933 931/1085/942 916/1070/921 +f 923/1077/934 939/1093/943 924/1078/920 +f 917/1071/922 931/1085/942 932/1086/944 +f 924/1078/920 940/1094/945 925/1079/923 +f 918/1072/924 932/1086/944 933/1087/946 +f 911/1065/925 564/670/508 926/1080/947 +f 790/927/510 925/1079/923 940/1094/945 +f 918/1072/924 934/1088/948 919/1073/926 +f 912/1066/927 926/1080/947 927/1081/949 +f 919/1073/926 935/1089/936 920/1074/928 +f 932/1086/944 946/1100/950 947/1101/951 +f 940/1094/945 954/1108/952 955/1109/953 +f 933/1087/946 947/1101/951 948/1102/954 +f 926/1080/947 564/671/508 941/1095/955 +f 790/928/510 940/1094/945 955/1109/953 +f 933/1087/946 949/1103/956 934/1088/948 +f 926/1080/947 942/1096/957 927/1081/949 +f 934/1088/948 950/1104/958 935/1089/936 +f 928/1082/935 942/1096/957 943/1097/959 +f 936/1090/937 950/1104/958 951/1105/960 +f 928/1082/935 944/1098/961 929/1083/938 +f 936/1090/937 952/1106/962 937/1091/939 +f 930/1084/940 944/1098/961 945/1099/963 +f 938/1092/941 952/1106/962 953/1107/964 +f 930/1084/940 946/1100/950 931/1085/942 +f 938/1092/941 954/1108/952 939/1093/943 +f 950/1104/958 488/565/488 951/1105/960 +f 944/1098/961 483/560/483 958/1112/489 +f 951/1105/960 961/1115/492 952/1106/962 +f 945/1099/963 958/1112/489 484/561/493 +f 953/1107/964 961/1115/492 962/1116/496 +f 946/1100/950 484/561/493 959/1113/497 +f 953/1107/964 963/1117/500 954/1108/952 +f 947/1101/951 959/1113/497 485/562/501 +f 954/1108/952 964/1118/504 955/1109/953 +f 948/1102/954 485/562/501 486/563/506 +f 941/1095/955 564/672/508 956/1110/507 +f 790/929/510 955/1109/953 964/1118/504 +f 948/1102/954 487/564/512 949/1103/956 +f 942/1096/957 956/1110/507 957/1111/513 +f 949/1103/956 960/1114/486 950/1104/958 +f 943/1097/959 957/1111/513 483/560/483 +f 483/560/483 957/1111/513 490/567/484 +f 960/1114/486 498/575/514 499/576/487 +f 958/1112/489 483/560/483 491/568/485 +f 488/565/488 499/576/487 500/577/491 +f 484/561/493 958/1112/489 492/569/490 +f 961/1115/492 500/577/491 501/578/495 +f 959/1113/497 484/561/493 493/570/494 +f 962/1116/496 501/578/495 502/579/499 +f 485/562/501 959/1113/497 494/571/498 +f 963/1117/500 502/579/499 503/580/503 +f 485/562/501 495/572/502 496/573/505 +f 486/563/506 496/573/505 497/574/511 +f 957/1111/513 956/1110/507 489/566/509 +f 487/564/512 497/574/511 498/575/514 +f 489/566/509 504/581/528 505/582/515 +f 497/574/511 512/589/529 513/590/516 +f 490/567/484 505/582/515 506/583/517 +f 498/575/514 513/590/516 514/591/518 +f 491/568/485 506/583/517 507/584/519 +f 499/576/487 514/591/518 515/592/520 +f 492/569/490 507/584/519 508/585/521 +f 501/578/495 500/577/491 515/592/520 +f 493/570/494 508/585/521 509/586/523 +f 502/579/499 501/578/495 516/593/522 +f 495/572/502 494/571/498 509/586/523 +f 502/579/499 517/594/524 518/595/526 +f 495/572/502 510/587/525 511/588/527 +f 496/573/505 511/588/527 512/589/529 +f 508/585/521 523/600/543 524/601/530 +f 516/593/522 531/608/544 532/609/531 +f 509/586/523 524/601/530 525/602/532 +f 517/594/524 532/609/531 533/610/533 +f 510/587/525 525/602/532 526/603/534 +f 512/589/529 511/588/527 526/603/534 +f 504/581/528 519/596/535 520/597/537 +f 512/589/529 527/604/536 528/605/538 +f 505/582/515 520/597/537 521/598/539 +f 514/591/518 513/590/516 528/605/538 +f 506/583/517 521/598/539 522/599/541 +f 515/592/520 514/591/518 529/606/540 +f 507/584/519 522/599/541 523/600/543 +f 516/593/522 515/592/520 530/607/542 +f 527/604/536 542/619/558 543/620/545 +f 520/597/537 535/612/559 536/613/546 +f 529/606/540 528/605/538 543/620/545 +f 521/598/539 536/613/546 537/614/548 +f 529/606/540 544/621/547 545/622/549 +f 522/599/541 537/614/548 538/615/550 +f 530/607/542 545/622/549 546/623/551 +f 523/600/543 538/615/550 539/616/552 +f 531/608/544 546/623/551 547/624/553 +f 525/602/532 524/601/530 539/616/552 +f 532/609/531 547/624/553 548/625/555 +f 526/603/534 525/602/532 540/617/554 +f 526/603/534 541/618/556 542/619/558 +f 519/596/535 534/611/557 535/612/559 +f 547/624/553 546/623/551 561/638/560 +f 540/617/554 539/616/552 554/631/562 +f 547/624/553 562/639/561 563/640/564 +f 541/618/556 540/617/554 555/632/563 +f 541/618/556 556/633/565 557/634/567 +f 534/611/557 549/626/566 550/627/568 +f 542/619/558 557/634/567 558/635/569 +f 535/612/559 550/627/568 551/628/570 +f 544/621/547 543/620/545 558/635/569 +f 536/613/546 551/628/570 552/629/572 +f 544/621/547 559/636/571 560/637/573 +f 538/615/550 537/614/548 552/629/572 +f 545/622/549 560/637/573 561/638/560 +f 538/615/550 553/630/574 554/631/562 +f 550/627/568 566/674/589 567/675/575 +f 559/636/571 558/635/569 574/682/576 +f 551/628/570 567/675/575 568/676/578 +f 559/636/571 575/683/577 576/684/579 +f 553/630/574 552/629/572 568/676/578 +f 560/637/573 576/684/579 577/685/581 +f 553/630/574 569/677/580 570/678/582 +f 561/638/560 577/685/581 578/686/583 +f 555/632/563 554/631/562 570/678/582 +f 563/640/564 562/639/561 578/686/583 +f 556/633/565 555/632/563 571/679/584 +f 556/633/565 572/680/586 573/681/588 +f 549/626/566 565/673/587 566/674/589 +f 557/634/567 573/681/588 574/682/576 +f 571/679/584 570/678/582 585/693/590 +f 578/686/583 593/701/604 594/702/592 +f 572/680/586 571/679/584 586/694/591 +f 572/680/586 587/695/593 588/696/595 +f 566/674/589 565/673/587 580/688/594 +f 573/681/588 588/696/595 589/697/597 +f 566/674/589 581/689/596 582/690/598 +f 575/683/577 574/682/576 589/697/597 +f 567/675/575 582/690/598 583/691/600 +f 575/683/577 590/698/599 591/699/601 +f 569/677/580 568/676/578 583/691/600 +f 577/685/581 576/684/579 591/699/601 +f 569/677/580 584/692/602 585/693/590 +f 578/686/583 577/685/581 592/700/603 +f 590/698/599 589/697/597 604/712/605 +f 582/690/598 597/705/619 598/706/607 +f 590/698/599 605/713/606 606/714/608 +f 584/692/602 583/691/600 598/706/607 +f 592/700/603 591/699/601 606/714/608 +f 584/692/602 599/707/609 600/708/611 +f 592/700/603 607/715/610 608/716/612 +f 586/694/591 585/693/590 600/708/611 +f 593/701/604 608/716/612 609/717/614 +f 587/695/593 586/694/591 601/709/613 +f 587/695/593 602/710/615 603/711/617 +f 581/689/596 580/688/594 595/703/616 +f 588/696/595 603/711/617 604/712/605 +f 581/689/596 596/704/618 597/705/619 +f 608/716/612 623/731/634 624/732/620 +f 602/710/615 601/709/613 616/724/621 +f 602/710/615 617/725/622 618/726/624 +f 595/703/616 610/718/623 611/719/625 +f 603/711/617 618/726/624 619/727/626 +f 596/704/618 611/719/625 612/720/627 +f 605/713/606 604/712/605 619/727/626 +f 597/705/619 612/720/627 613/721/629 +f 605/713/606 620/728/628 621/729/630 +f 599/707/609 598/706/607 613/721/629 +f 607/715/610 606/714/608 621/729/630 +f 599/707/609 614/722/631 615/723/633 +f 607/715/610 622/730/632 623/731/634 +f 601/709/613 600/708/611 615/723/633 +f 620/728/628 635/743/649 636/744/635 +f 614/722/631 613/721/629 628/736/636 +f 622/730/632 621/729/630 636/744/635 +f 614/722/631 629/737/637 630/738/639 +f 622/730/632 637/745/638 638/746/640 +f 616/724/621 615/723/633 630/738/639 +f 624/732/620 623/731/634 638/746/640 +f 617/725/622 616/724/621 631/739/641 +f 617/725/622 632/740/643 633/741/645 +f 610/718/623 625/733/644 626/734/646 +f 618/726/624 633/741/645 634/742/647 +f 612/720/627 611/719/625 626/734/646 +f 620/728/628 619/727/626 634/742/647 +f 612/720/627 627/735/648 628/736/636 +f 632/740/643 647/755/664 648/756/652 +f 625/733/644 640/748/650 641/749/653 +f 633/741/645 648/756/652 649/757/654 +f 627/735/648 626/734/646 641/749/653 +f 635/743/649 634/742/647 649/757/654 +f 627/735/648 642/750/655 643/751/657 +f 635/743/649 650/758/656 651/759/658 +f 629/737/637 628/736/636 643/751/657 +f 637/745/638 636/744/635 651/759/658 +f 629/737/637 644/752/659 645/753/661 +f 637/745/638 652/760/660 653/761/662 +f 631/739/641 630/738/639 645/753/661 +f 638/746/640 653/761/662 654/762/651 +f 632/740/643 631/739/641 646/754/663 +f 644/752/659 643/751/657 658/766/665 +f 651/759/658 666/774/679 667/775/667 +f 644/752/659 659/767/666 660/768/668 +f 653/761/662 652/760/660 667/775/667 +f 646/754/663 645/753/661 660/768/668 +f 654/762/651 653/761/662 668/776/669 +f 647/755/664 646/754/663 661/769/670 +f 647/755/664 662/770/672 663/771/674 +f 640/748/650 655/763/673 656/764/675 +f 648/756/652 663/771/674 664/772/676 +f 641/749/653 656/764/675 657/765/677 +f 650/758/656 649/757/654 664/772/676 +f 642/750/655 657/765/677 658/766/665 +f 650/758/656 665/773/678 666/774/679 +f 662/770/672 677/785/694 678/786/680 +f 656/764/675 655/763/673 670/778/681 +f 663/771/674 678/786/680 679/787/683 +f 657/765/677 656/764/675 671/779/682 +f 665/773/678 664/772/676 679/787/683 +f 657/765/677 672/780/684 673/781/686 +f 665/773/678 680/788/685 681/789/687 +f 659/767/666 658/766/665 673/781/686 +f 667/775/667 666/774/679 681/789/687 +f 659/767/666 674/782/688 675/783/690 +f 667/775/667 682/790/689 683/791/691 +f 661/769/670 660/768/668 675/783/690 +f 668/776/669 683/791/691 684/792/693 +f 662/770/672 661/769/670 676/784/692 +f 682/790/689 681/789/687 696/804/695 +f 674/782/688 689/797/709 690/798/697 +f 682/790/689 697/805/696 698/806/698 +f 676/784/692 675/783/690 690/798/697 +f 683/791/691 698/806/698 699/807/700 +f 677/785/694 676/784/692 691/799/699 +f 677/785/694 692/800/701 693/801/703 +f 670/778/681 685/793/702 686/794/704 +f 678/786/680 693/801/703 694/802/705 +f 671/779/682 686/794/704 687/795/706 +f 680/788/685 679/787/683 694/802/705 +f 672/780/684 687/795/706 688/796/708 +f 680/788/685 695/803/707 696/804/695 +f 674/782/688 673/781/686 688/796/708 +f 685/793/702 700/808/723 701/809/710 +f 693/801/703 708/816/724 709/817/711 +f 686/794/704 701/809/710 702/810/712 +f 695/803/707 694/802/705 709/817/711 +f 687/795/706 702/810/712 703/811/714 +f 695/803/707 710/818/713 711/819/715 +f 689/797/709 688/796/708 703/811/714 +f 697/805/696 696/804/695 711/819/715 +f 689/797/709 704/812/716 705/813/718 +f 697/805/696 712/820/717 713/821/719 +f 691/799/699 690/798/697 705/813/718 +f 698/806/698 713/821/719 714/822/721 +f 692/800/701 691/799/699 706/814/720 +f 692/800/701 707/815/722 708/816/724 +f 704/812/716 719/827/738 720/828/725 +f 712/820/717 727/835/739 728/836/726 +f 706/814/720 705/813/718 720/828/725 +f 714/822/721 713/821/719 728/836/726 +f 707/815/722 706/814/720 721/829/727 +f 707/815/722 722/830/729 723/831/731 +f 700/808/723 715/823/730 716/824/732 +f 708/816/724 723/831/731 724/832/733 +f 702/810/712 701/809/710 716/824/732 +f 710/818/713 709/817/711 724/832/733 +f 702/810/712 717/825/734 718/826/736 +f 710/818/713 725/833/735 726/834/737 +f 704/812/716 703/811/714 718/826/736 +f 712/820/717 711/819/715 726/834/737 +f 723/831/731 738/846/753 739/847/740 +f 716/824/732 731/839/754 732/840/741 +f 725/833/735 724/832/733 739/847/740 +f 717/825/734 732/840/741 733/841/743 +f 725/833/735 740/848/742 741/849/744 +f 719/827/738 718/826/736 733/841/743 +f 727/835/739 726/834/737 741/849/744 +f 719/827/738 734/842/745 735/843/747 +f 727/835/739 742/850/746 743/851/748 +f 721/829/727 720/828/725 735/843/747 +f 728/836/726 743/851/748 744/852/750 +f 722/830/729 721/829/727 736/844/749 +f 722/830/729 737/845/751 738/846/753 +f 715/823/730 730/838/752 731/839/754 +f 742/850/746 757/865/769 758/866/755 +f 736/844/749 735/843/747 750/858/756 +f 743/851/748 758/866/755 759/867/758 +f 737/845/751 736/844/749 751/859/757 +f 737/845/751 752/860/759 753/861/761 +f 731/839/754 730/838/752 745/853/760 +f 738/846/753 753/861/761 754/862/763 +f 731/839/754 746/854/762 747/855/764 +f 740/848/742 739/847/740 754/862/763 +f 732/840/741 747/855/764 748/856/766 +f 740/848/742 755/863/765 756/864/767 +f 734/842/745 733/841/743 748/856/766 +f 742/850/746 741/849/744 756/864/767 +f 734/842/745 749/857/768 750/858/756 +f 755/863/765 754/862/763 769/877/770 +f 747/855/764 762/870/784 763/871/772 +f 755/863/765 770/878/771 771/879/773 +f 749/857/768 748/856/766 763/871/772 +f 757/865/769 756/864/767 771/879/773 +f 749/857/768 764/872/774 765/873/776 +f 757/865/769 772/880/775 773/881/777 +f 751/859/757 750/858/756 765/873/776 +f 759/867/758 758/866/755 773/881/777 +f 752/860/759 751/859/757 766/874/778 +f 752/860/759 767/875/780 768/876/782 +f 746/854/762 745/853/760 760/868/781 +f 753/861/761 768/876/782 769/877/770 +f 746/854/762 761/869/783 762/870/784 +f 774/882/779 773/881/777 788/896/785 +f 767/875/780 766/874/778 781/889/787 +f 767/875/780 782/890/788 783/891/790 +f 760/868/781 775/883/789 776/884/791 +f 768/876/782 783/891/790 784/892/792 +f 761/869/783 776/884/791 777/885/793 +f 770/878/771 769/877/770 784/892/792 +f 762/870/784 777/885/793 778/886/795 +f 770/878/771 785/893/794 786/894/796 +f 764/872/774 763/871/772 778/886/795 +f 772/880/775 771/879/773 786/894/796 +f 764/872/774 779/887/797 780/888/799 +f 772/880/775 787/895/798 788/896/785 +f 766/874/778 765/873/776 780/888/799 +f 777/885/793 793/932/813 794/933/800 +f 785/893/794 801/940/814 802/941/801 +f 779/887/797 778/886/795 794/933/800 +f 787/895/798 786/894/796 802/941/801 +f 779/887/797 795/934/802 796/935/804 +f 787/895/798 803/942/803 804/943/805 +f 781/889/787 780/888/799 796/935/804 +f 788/896/785 804/943/805 805/944/807 +f 782/890/788 781/889/787 797/936/806 +f 782/890/788 798/937/808 799/938/810 +f 775/883/789 791/930/809 792/931/811 +f 783/891/790 799/938/810 800/939/812 +f 776/884/791 792/931/811 793/932/813 +f 785/893/794 784/892/792 800/939/812 +f 798/937/808 797/936/806 812/951/815 +f 798/937/808 813/952/816 814/953/819 +f 791/930/809 806/945/817 807/946/820 +f 799/938/810 814/953/819 815/954/821 +f 792/931/811 807/946/820 808/947/822 +f 801/940/814 800/939/812 815/954/821 +f 793/932/813 808/947/822 809/948/824 +f 801/940/814 816/955/823 817/956/825 +f 794/933/800 809/948/824 810/949/826 +f 803/942/803 802/941/801 817/956/825 +f 795/934/802 810/949/826 811/950/828 +f 803/942/803 818/957/827 819/958/829 +f 797/936/806 796/935/804 811/950/828 +f 804/943/805 819/958/829 820/959/818 +f 816/955/823 831/970/844 832/971/830 +f 810/949/826 809/948/824 824/963/831 +f 818/957/827 817/956/825 832/971/830 +f 810/949/826 825/964/832 826/965/834 +f 818/957/827 833/972/833 834/973/835 +f 812/951/815 811/950/828 826/965/834 +f 819/958/829 834/973/835 835/974/837 +f 813/952/816 812/951/815 827/966/836 +f 813/952/816 828/967/838 829/968/840 +f 806/945/817 821/960/839 822/961/841 +f 814/953/819 829/968/840 830/969/842 +f 807/946/820 822/961/841 823/962/843 +f 816/955/823 815/954/821 830/969/842 +f 808/947/822 823/962/843 824/963/831 +f 828/967/838 843/989/859 844/991/847 +f 821/960/839 836/975/845 837/977/848 +f 829/968/840 844/991/847 845/993/849 +f 823/962/843 822/961/841 837/977/848 +f 831/970/844 830/969/842 845/993/849 +f 823/962/843 838/979/850 839/981/852 +f 831/970/844 846/995/851 847/997/853 +f 825/964/832 824/963/831 839/981/852 +f 833/972/833 832/971/830 847/997/853 +f 825/964/832 840/983/854 841/985/856 +f 833/972/833 848/999/855 849/1001/857 +f 827/966/836 826/965/834 841/985/856 +f 834/973/835 849/1001/857 850/1004/846 +f 828/967/838 827/966/836 842/987/858 +f 840/984/854 839/982/852 854/1008/860 +f 848/1000/855 847/998/853 862/1016/862 +f 840/984/854 855/1009/861 856/1010/864 +f 848/1000/855 863/1017/863 864/1018/865 +f 842/988/858 841/986/856 856/1010/864 +f 849/1002/857 864/1018/865 865/1019/867 +f 843/990/859 842/988/858 857/1011/866 +f 843/990/859 858/1012/868 859/1013/870 +f 836/976/845 851/1005/869 852/1006/871 +f 844/992/847 859/1013/870 860/1014/872 +f 838/980/850 837/978/848 852/1006/871 +f 846/996/851 845/994/849 860/1014/872 +f 838/980/850 853/1007/873 854/1008/860 +f 846/996/851 861/1015/874 862/1016/862 +f 858/1012/868 873/1027/888 874/1028/875 +f 851/1005/869 866/1020/889 867/1021/876 +f 859/1013/870 874/1028/875 875/1029/877 +f 853/1007/873 852/1006/871 867/1021/876 +f 861/1015/874 860/1014/872 875/1029/877 +f 853/1007/873 868/1022/878 869/1023/880 +f 861/1015/874 876/1030/879 877/1031/881 +f 855/1009/861 854/1008/860 869/1023/880 +f 863/1017/863 862/1016/862 877/1031/881 +f 855/1009/861 870/1024/882 871/1025/884 +f 863/1017/863 878/1032/883 879/1033/885 +f 857/1011/866 856/1010/864 871/1025/884 +f 864/1018/865 879/1033/885 880/1034/887 +f 858/1012/868 857/1011/866 872/1026/886 +f 878/1032/883 877/1031/881 892/1046/890 +f 870/1024/882 885/1039/904 886/1040/892 +f 879/1033/885 878/1032/883 893/1047/891 +f 872/1026/886 871/1025/884 886/1040/892 +f 879/1033/885 894/1048/893 895/1049/895 +f 873/1027/888 872/1026/886 887/1041/894 +f 873/1027/888 888/1042/896 889/1043/898 +f 867/1021/876 866/1020/889 881/1035/897 +f 874/1028/875 889/1043/898 890/1044/900 +f 867/1021/876 882/1036/899 883/1037/901 +f 876/1030/879 875/1029/877 890/1044/900 +f 868/1022/878 883/1037/901 884/1038/903 +f 876/1030/879 891/1045/902 892/1046/890 +f 870/1024/882 869/1023/880 884/1038/903 +f 889/1043/898 904/1058/919 905/1059/905 +f 883/1037/901 882/1036/899 897/1051/906 +f 891/1045/902 890/1044/900 905/1059/905 +f 883/1037/901 898/1052/907 899/1053/909 +f 891/1045/902 906/1060/908 907/1061/910 +f 885/1039/904 884/1038/903 899/1053/909 +f 893/1047/891 892/1046/890 907/1061/910 +f 885/1039/904 900/1054/911 901/1055/913 +f 893/1047/891 908/1062/912 909/1063/914 +f 887/1041/894 886/1040/892 901/1055/913 +f 894/1048/893 909/1063/914 910/1064/916 +f 888/1042/896 887/1041/894 902/1056/915 +f 888/1042/896 903/1057/917 904/1058/919 +f 881/1035/897 896/1050/918 897/1051/906 +f 908/1062/912 923/1077/934 924/1078/920 +f 902/1056/915 901/1055/913 916/1070/921 +f 909/1063/914 924/1078/920 925/1079/923 +f 903/1057/917 902/1056/915 917/1071/922 +f 903/1057/917 918/1072/924 919/1073/926 +f 896/1050/918 911/1065/925 912/1066/927 +f 904/1058/919 919/1073/926 920/1074/928 +f 898/1052/907 897/1051/906 912/1066/927 +f 906/1060/908 905/1059/905 920/1074/928 +f 898/1052/907 913/1067/929 914/1068/931 +f 906/1060/908 921/1075/930 922/1076/932 +f 900/1054/911 899/1053/909 914/1068/931 +f 908/1062/912 907/1061/910 922/1076/932 +f 900/1054/911 915/1069/933 916/1070/921 +f 912/1066/927 927/1081/949 928/1082/935 +f 921/1075/930 920/1074/928 935/1089/936 +f 913/1067/929 928/1082/935 929/1083/938 +f 921/1075/930 936/1090/937 937/1091/939 +f 915/1069/933 914/1068/931 929/1083/938 +f 923/1077/934 922/1076/932 937/1091/939 +f 915/1069/933 930/1084/940 931/1085/942 +f 923/1077/934 938/1092/941 939/1093/943 +f 917/1071/922 916/1070/921 931/1085/942 +f 924/1078/920 939/1093/943 940/1094/945 +f 918/1072/924 917/1071/922 932/1086/944 +f 918/1072/924 933/1087/946 934/1088/948 +f 912/1066/927 911/1065/925 926/1080/947 +f 919/1073/926 934/1088/948 935/1089/936 +f 932/1086/944 931/1085/942 946/1100/950 +f 940/1094/945 939/1093/943 954/1108/952 +f 933/1087/946 932/1086/944 947/1101/951 +f 933/1087/946 948/1102/954 949/1103/956 +f 926/1080/947 941/1095/955 942/1096/957 +f 934/1088/948 949/1103/956 950/1104/958 +f 928/1082/935 927/1081/949 942/1096/957 +f 936/1090/937 935/1089/936 950/1104/958 +f 928/1082/935 943/1097/959 944/1098/961 +f 936/1090/937 951/1105/960 952/1106/962 +f 930/1084/940 929/1083/938 944/1098/961 +f 938/1092/941 937/1091/939 952/1106/962 +f 930/1084/940 945/1099/963 946/1100/950 +f 938/1092/941 953/1107/964 954/1108/952 +f 950/1104/958 960/1114/486 488/565/488 +f 944/1098/961 943/1097/959 483/560/483 +f 951/1105/960 488/565/488 961/1115/492 +f 945/1099/963 944/1098/961 958/1112/489 +f 953/1107/964 952/1106/962 961/1115/492 +f 946/1100/950 945/1099/963 484/561/493 +f 953/1107/964 962/1116/496 963/1117/500 +f 947/1101/951 946/1100/950 959/1113/497 +f 954/1108/952 963/1117/500 964/1118/504 +f 948/1102/954 947/1101/951 485/562/501 +f 948/1102/954 486/563/506 487/564/512 +f 942/1096/957 941/1095/955 956/1110/507 +f 949/1103/956 487/564/512 960/1114/486 +f 943/1097/959 942/1096/957 957/1111/513 +o Sphere.002 +v 0.000000 3.751747 -3.984501 +v 0.000000 3.475848 -4.260401 +v 0.000000 3.115368 -4.409716 +v 0.000000 2.920277 -4.428931 +v 0.000000 2.725187 -4.409716 +v 0.000000 2.364707 -4.260401 +v 0.038060 3.901062 -3.620273 +v 0.074658 3.844157 -3.804261 +v 0.108386 3.751747 -3.973826 +v 0.137950 3.627384 -4.122451 +v 0.162212 3.475848 -4.244424 +v 0.180240 3.302961 -4.335058 +v 0.191342 3.115368 -4.390871 +v 0.195090 2.920277 -4.409716 +v 0.191342 2.725187 -4.390871 +v 0.180240 2.537594 -4.335058 +v 0.162212 2.364707 -4.244424 +v 0.137950 2.213171 -4.122451 +v 0.108386 2.088808 -3.973826 +v 0.074658 1.996398 -3.804261 +v 0.038060 1.939492 -3.620273 +v 0.074658 3.901062 -3.609171 +v 0.146447 3.844157 -3.782485 +v 0.212608 3.751747 -3.942211 +v 0.270598 3.627384 -4.082212 +v 0.318190 3.475848 -4.197109 +v 0.353553 3.302961 -4.282485 +v 0.375330 3.115368 -4.335058 +v 0.382683 2.920277 -4.352810 +v 0.375330 2.725187 -4.335058 +v 0.353553 2.537594 -4.282485 +v 0.318190 2.364707 -4.197109 +v 0.270598 2.213171 -4.082212 +v 0.212608 2.088808 -3.942211 +v 0.146447 1.996398 -3.782485 +v 0.074658 1.939492 -3.609171 +v 0.108386 3.901062 -3.591143 +v 0.212608 3.844157 -3.747121 +v 0.308658 3.751747 -3.890871 +v 0.392847 3.627384 -4.016869 +v 0.461940 3.475848 -4.120273 +v 0.513280 3.302961 -4.197109 +v 0.544895 3.115368 -4.244424 +v 0.555570 2.920277 -4.260400 +v 0.544895 2.725187 -4.244424 +v 0.513280 2.537594 -4.197109 +v 0.461940 2.364707 -4.120273 +v 0.392847 2.213171 -4.016869 +v 0.308658 2.088808 -3.890871 +v 0.212608 1.996398 -3.747121 +v 0.108386 1.939492 -3.591143 +v 0.137950 3.901062 -3.566881 +v 0.270598 3.844157 -3.699529 +v 0.392847 3.751747 -3.821778 +v 0.500000 3.627384 -3.928931 +v 0.587938 3.475848 -4.016869 +v 0.653281 3.302961 -4.082212 +v 0.693520 3.115368 -4.122451 +v 0.707107 2.920277 -4.136038 +v 0.693520 2.725187 -4.122451 +v 0.653281 2.537594 -4.082212 +v 0.587938 2.364707 -4.016869 +v 0.500000 2.213171 -3.928931 +v 0.392847 2.088808 -3.821778 +v 0.270598 1.996398 -3.699529 +v 0.137950 1.939492 -3.566881 +v 0.162212 3.901062 -3.537317 +v 0.318190 3.844157 -3.641539 +v 0.461940 3.751747 -3.737589 +v 0.587938 3.627384 -3.821779 +v 0.691342 3.475848 -3.890871 +v 0.768178 3.302961 -3.942211 +v 0.815493 3.115368 -3.973826 +v 0.831470 2.920277 -3.984501 +v 0.815493 2.725187 -3.973826 +v 0.768178 2.537594 -3.942211 +v 0.691342 2.364707 -3.890871 +v 0.587938 2.213171 -3.821779 +v 0.461940 2.088808 -3.737589 +v 0.318190 1.996398 -3.641539 +v 0.162212 1.939492 -3.537317 +v 0.000000 3.920277 -3.428931 +v 0.180240 3.901062 -3.503589 +v 0.353553 3.844157 -3.575377 +v 0.513280 3.751747 -3.641538 +v 0.653281 3.627384 -3.699529 +v 0.768178 3.475848 -3.747120 +v 0.853553 3.302961 -3.782485 +v 0.906127 3.115368 -3.804261 +v 0.923879 2.920277 -3.811614 +v 0.906127 2.725187 -3.804261 +v 0.853553 2.537594 -3.782485 +v 0.768178 2.364707 -3.747120 +v 0.653281 2.213171 -3.699529 +v 0.513280 2.088808 -3.641538 +v 0.353553 1.996398 -3.575377 +v 0.180240 1.939492 -3.503589 +v 0.191342 3.901062 -3.466991 +v 0.375330 3.844157 -3.503589 +v 0.544895 3.751747 -3.537317 +v 0.693520 3.627384 -3.566881 +v 0.815493 3.475848 -3.591143 +v 0.906127 3.302961 -3.609171 +v 0.961940 3.115368 -3.620273 +v 0.980785 2.920277 -3.624021 +v 0.961940 2.725187 -3.620273 +v 0.906127 2.537594 -3.609171 +v 0.815493 2.364707 -3.591143 +v 0.693520 2.213171 -3.566881 +v 0.544895 2.088808 -3.537317 +v 0.375330 1.996398 -3.503589 +v 0.191342 1.939492 -3.466991 +v 0.195090 3.901062 -3.428931 +v 0.382683 3.844157 -3.428931 +v 0.555570 3.751747 -3.428931 +v 0.707107 3.627384 -3.428931 +v 0.831469 3.475848 -3.428931 +v 0.923879 3.302961 -3.428931 +v 0.980785 3.115368 -3.428931 +v 1.000000 2.920277 -3.428931 +v 0.980785 2.725187 -3.428931 +v 0.923879 2.537594 -3.428931 +v 0.831469 2.364707 -3.428931 +v 0.707107 2.213171 -3.428931 +v 0.555570 2.088808 -3.428931 +v 0.382683 1.996398 -3.428931 +v 0.195090 1.939492 -3.428931 +v 0.191342 3.901062 -3.390871 +v 0.375330 3.844157 -3.354273 +v 0.544895 3.751747 -3.320544 +v 0.693520 3.627384 -3.290981 +v 0.815493 3.475848 -3.266719 +v 0.906127 3.302961 -3.248691 +v 0.961940 3.115368 -3.237589 +v 0.980785 2.920277 -3.233840 +v 0.961940 2.725187 -3.237589 +v 0.906127 2.537594 -3.248691 +v 0.815493 2.364707 -3.266719 +v 0.693520 2.213171 -3.290981 +v 0.544895 2.088808 -3.320544 +v 0.375330 1.996398 -3.354273 +v 0.191342 1.939492 -3.390871 +v 0.180240 3.901062 -3.354273 +v 0.353553 3.844157 -3.282484 +v 0.513280 3.751747 -3.216323 +v 0.653281 3.627384 -3.158333 +v 0.768178 3.475848 -3.110741 +v 0.853553 3.302961 -3.075378 +v 0.906127 3.115368 -3.053601 +v 0.923879 2.920277 -3.046247 +v 0.906127 2.725187 -3.053601 +v 0.853553 2.537594 -3.075378 +v 0.768178 2.364707 -3.110741 +v 0.653281 2.213171 -3.158333 +v 0.513280 2.088808 -3.216323 +v 0.353553 1.996398 -3.282484 +v 0.180240 1.939492 -3.354273 +v 0.162212 3.901062 -3.320545 +v 0.318190 3.844157 -3.216323 +v 0.461940 3.751747 -3.120273 +v 0.587938 3.627384 -3.036084 +v 0.691341 3.475848 -2.966991 +v 0.768178 3.302961 -2.915651 +v 0.815493 3.115368 -2.884036 +v 0.831469 2.920277 -2.873361 +v 0.815493 2.725187 -2.884036 +v 0.768178 2.537594 -2.915651 +v 0.691341 2.364707 -2.966991 +v 0.587938 2.213171 -3.036084 +v 0.461940 2.088808 -3.120273 +v 0.318190 1.996398 -3.216323 +v 0.162212 1.939492 -3.320545 +v 0.137950 3.901062 -3.290981 +v 0.270598 3.844157 -3.158333 +v 0.392847 3.751747 -3.036084 +v 0.500000 3.627384 -2.928931 +v 0.587938 3.475848 -2.840993 +v 0.653281 3.302961 -2.775650 +v 0.693520 3.115368 -2.735411 +v 0.707106 2.920277 -2.721824 +v 0.693520 2.725187 -2.735411 +v 0.653281 2.537594 -2.775650 +v 0.587938 2.364707 -2.840993 +v 0.500000 2.213171 -2.928931 +v 0.392847 2.088808 -3.036084 +v 0.270598 1.996398 -3.158333 +v 0.137950 1.939492 -3.290981 +v 0.108386 3.901062 -3.266719 +v 0.212607 3.844157 -3.110741 +v 0.308658 3.751747 -2.966991 +v 0.392847 3.627384 -2.840993 +v 0.461940 3.475848 -2.737589 +v 0.513280 3.302961 -2.660753 +v 0.544895 3.115368 -2.613438 +v 0.555570 2.920277 -2.597462 +v 0.544895 2.725187 -2.613438 +v 0.513280 2.537594 -2.660753 +v 0.461940 2.364707 -2.737589 +v 0.392847 2.213171 -2.840993 +v 0.308658 2.088808 -2.966991 +v 0.212607 1.996398 -3.110741 +v 0.108386 1.939492 -3.266719 +v 0.074658 3.901062 -3.248691 +v 0.146447 3.844157 -3.075378 +v 0.212607 3.751747 -2.915651 +v 0.270598 3.627384 -2.775650 +v 0.318189 3.475848 -2.660753 +v 0.353553 3.302961 -2.575378 +v 0.375330 3.115368 -2.522804 +v 0.382683 2.920277 -2.505052 +v 0.375330 2.725187 -2.522804 +v 0.353553 2.537594 -2.575378 +v 0.318189 2.364707 -2.660753 +v 0.270598 2.213171 -2.775650 +v 0.212607 2.088808 -2.915651 +v 0.146447 1.996398 -3.075378 +v 0.074658 1.939492 -3.248691 +v 0.038060 3.901062 -3.237589 +v 0.074658 3.844157 -3.053601 +v 0.108386 3.751747 -2.884036 +v 0.137950 3.627384 -2.735411 +v 0.162212 3.475848 -2.613438 +v 0.180240 3.302961 -2.522804 +v 0.191342 3.115368 -2.466991 +v 0.195090 2.920277 -2.448146 +v 0.191342 2.725187 -2.466991 +v 0.180240 2.537594 -2.522804 +v 0.162212 2.364707 -2.613438 +v 0.137950 2.213171 -2.735411 +v 0.108386 2.088808 -2.884036 +v 0.074658 1.996398 -3.053601 +v 0.038060 1.939492 -3.237589 +v -0.000000 3.901062 -3.233841 +v -0.000000 3.844157 -3.046248 +v -0.000000 3.751747 -2.873361 +v -0.000000 3.627384 -2.721824 +v -0.000000 3.475848 -2.597462 +v 0.000000 3.302961 -2.505052 +v -0.000000 3.115368 -2.448146 +v -0.000000 2.920277 -2.428932 +v -0.000000 2.725187 -2.448146 +v 0.000000 2.537594 -2.505052 +v -0.000000 2.364707 -2.597462 +v -0.000000 2.213171 -2.721824 +v -0.000000 2.088808 -2.873361 +v -0.000000 1.996398 -3.046248 +v -0.000000 1.939492 -3.233841 +v -0.038060 3.901062 -3.237589 +v -0.074658 3.844157 -3.053601 +v -0.108386 3.751747 -2.884036 +v -0.137950 3.627384 -2.735411 +v -0.162212 3.475848 -2.613438 +v -0.180240 3.302961 -2.522804 +v -0.191342 3.115368 -2.466992 +v -0.195091 2.920277 -2.448146 +v -0.191342 2.725187 -2.466992 +v -0.180240 2.537594 -2.522804 +v -0.162212 2.364707 -2.613438 +v -0.137950 2.213171 -2.735411 +v -0.108386 2.088808 -2.884036 +v -0.074658 1.996398 -3.053601 +v -0.038060 1.939492 -3.237589 +v -0.074658 3.901062 -3.248691 +v -0.146447 3.844157 -3.075378 +v -0.212608 3.751747 -2.915651 +v -0.270598 3.627384 -2.775650 +v -0.318190 3.475848 -2.660754 +v -0.353553 3.302961 -2.575378 +v -0.375330 3.115368 -2.522804 +v -0.382683 2.920277 -2.505052 +v -0.375330 2.725187 -2.522804 +v -0.353553 2.537594 -2.575378 +v -0.318190 2.364707 -2.660754 +v -0.270598 2.213171 -2.775650 +v -0.212608 2.088808 -2.915651 +v -0.146447 1.996398 -3.075378 +v -0.074658 1.939492 -3.248691 +v -0.108386 3.901062 -3.266719 +v -0.212608 3.844157 -3.110741 +v -0.308658 3.751747 -2.966992 +v -0.392847 3.627384 -2.840993 +v -0.461940 3.475848 -2.737590 +v -0.513280 3.302961 -2.660753 +v -0.544895 3.115368 -2.613438 +v -0.555570 2.920277 -2.597462 +v -0.544895 2.725187 -2.613438 +v -0.513280 2.537594 -2.660753 +v -0.461940 2.364707 -2.737590 +v -0.392847 2.213171 -2.840993 +v -0.308658 2.088808 -2.966992 +v -0.212608 1.996398 -3.110741 +v -0.108386 1.939492 -3.266719 +v -0.137950 3.901062 -3.290982 +v -0.270598 3.844157 -3.158333 +v -0.392847 3.751747 -3.036084 +v -0.500000 3.627384 -2.928931 +v -0.587938 3.475848 -2.840994 +v -0.653281 3.302961 -2.775650 +v -0.693520 3.115368 -2.735411 +v -0.707106 2.920277 -2.721825 +v -0.693520 2.725187 -2.735411 +v -0.653281 2.537594 -2.775650 +v -0.587938 2.364707 -2.840994 +v -0.500000 2.213171 -2.928931 +v -0.392847 2.088808 -3.036084 +v -0.270598 1.996398 -3.158333 +v -0.137950 1.939492 -3.290982 +v 0.000000 1.920277 -3.428931 +v -0.162212 3.901062 -3.320545 +v -0.318190 3.844157 -3.216324 +v -0.461940 3.751747 -3.120273 +v -0.587938 3.627384 -3.036084 +v -0.691341 3.475848 -2.966992 +v -0.768177 3.302961 -2.915651 +v -0.815493 3.115368 -2.884036 +v -0.831469 2.920277 -2.873362 +v -0.815493 2.725187 -2.884036 +v -0.768177 2.537594 -2.915651 +v -0.691341 2.364707 -2.966992 +v -0.587938 2.213171 -3.036084 +v -0.461940 2.088808 -3.120273 +v -0.318190 1.996398 -3.216324 +v -0.162212 1.939492 -3.320545 +v -0.180240 3.901062 -3.354273 +v -0.353553 3.844157 -3.282485 +v -0.513280 3.751747 -3.216324 +v -0.653281 3.627384 -3.158333 +v -0.768177 3.475848 -3.110742 +v -0.853553 3.302961 -3.075378 +v -0.906127 3.115368 -3.053601 +v -0.923879 2.920277 -3.046248 +v -0.906127 2.725187 -3.053601 +v -0.853553 2.537594 -3.075378 +v -0.768177 2.364707 -3.110742 +v -0.653281 2.213171 -3.158333 +v -0.513280 2.088808 -3.216324 +v -0.353553 1.996398 -3.282485 +v -0.180240 1.939492 -3.354273 +v -0.191342 3.901062 -3.390871 +v -0.375330 3.844157 -3.354273 +v -0.544895 3.751747 -3.320545 +v -0.693520 3.627384 -3.290981 +v -0.815493 3.475848 -3.266720 +v -0.906127 3.302961 -3.248691 +v -0.961939 3.115368 -3.237590 +v -0.980784 2.920277 -3.233841 +v -0.961939 2.725187 -3.237590 +v -0.906127 2.537594 -3.248691 +v -0.815493 2.364707 -3.266720 +v -0.693520 2.213171 -3.290981 +v -0.544895 2.088808 -3.320545 +v -0.375330 1.996398 -3.354273 +v -0.191342 1.939492 -3.390871 +v -0.195090 3.901062 -3.428931 +v -0.382683 3.844157 -3.428931 +v -0.555570 3.751747 -3.428931 +v -0.707107 3.627384 -3.428931 +v -0.831469 3.475848 -3.428931 +v -0.923879 3.302961 -3.428931 +v -0.980785 3.115368 -3.428931 +v -0.999999 2.920277 -3.428931 +v -0.980785 2.725187 -3.428931 +v -0.923879 2.537594 -3.428931 +v -0.831469 2.364707 -3.428931 +v -0.707107 2.213171 -3.428931 +v -0.555570 2.088808 -3.428931 +v -0.382683 1.996398 -3.428931 +v -0.195090 1.939492 -3.428931 +v -0.191342 3.901062 -3.466991 +v -0.375330 3.844157 -3.503589 +v -0.544895 3.751747 -3.537318 +v -0.693520 3.627384 -3.566881 +v -0.815493 3.475848 -3.591143 +v -0.906127 3.302961 -3.609171 +v -0.961939 3.115368 -3.620273 +v -0.980784 2.920277 -3.624022 +v -0.961939 2.725187 -3.620273 +v -0.906127 2.537594 -3.609171 +v -0.815493 2.364707 -3.591143 +v -0.693520 2.213171 -3.566881 +v -0.544895 2.088808 -3.537318 +v -0.375330 1.996398 -3.503589 +v -0.191342 1.939492 -3.466991 +v -0.180240 3.901062 -3.503589 +v -0.353553 3.844157 -3.575377 +v -0.513279 3.751747 -3.641538 +v -0.653281 3.627384 -3.699529 +v -0.768177 3.475848 -3.747121 +v -0.853553 3.302961 -3.782484 +v -0.906127 3.115368 -3.804261 +v -0.923878 2.920277 -3.811615 +v -0.906127 2.725187 -3.804261 +v -0.853553 2.537594 -3.782484 +v -0.768177 2.364707 -3.747121 +v -0.653281 2.213171 -3.699529 +v -0.513279 2.088808 -3.641538 +v -0.353553 1.996398 -3.575377 +v -0.180240 1.939492 -3.503589 +v -0.162212 3.901062 -3.537317 +v -0.318189 3.844157 -3.641538 +v -0.461939 3.751747 -3.737589 +v -0.587938 3.627384 -3.821778 +v -0.691341 3.475848 -3.890871 +v -0.768177 3.302961 -3.942211 +v -0.815493 3.115368 -3.973826 +v -0.831468 2.920277 -3.984501 +v -0.815493 2.725187 -3.973826 +v -0.768177 2.537594 -3.942211 +v -0.691341 2.364707 -3.890871 +v -0.587938 2.213171 -3.821778 +v -0.461939 2.088808 -3.737589 +v -0.318189 1.996398 -3.641538 +v -0.162212 1.939492 -3.537317 +v -0.137950 3.901062 -3.566880 +v -0.270598 3.844157 -3.699529 +v -0.392847 3.751747 -3.821778 +v -0.500000 3.627384 -3.928931 +v -0.587937 3.475848 -4.016869 +v -0.653281 3.302961 -4.082212 +v -0.693519 3.115368 -4.122451 +v -0.707106 2.920277 -4.136037 +v -0.693519 2.725187 -4.122451 +v -0.653281 2.537594 -4.082212 +v -0.587937 2.364707 -4.016869 +v -0.500000 2.213171 -3.928931 +v -0.392847 2.088808 -3.821778 +v -0.270598 1.996398 -3.699529 +v -0.137950 1.939492 -3.566880 +v -0.108386 3.901062 -3.591142 +v -0.212607 3.844157 -3.747120 +v -0.308658 3.751747 -3.890870 +v -0.392847 3.627384 -4.016869 +v -0.461939 3.475848 -4.120272 +v -0.513280 3.302961 -4.197108 +v -0.544895 3.115368 -4.244424 +v -0.555569 2.920277 -4.260400 +v -0.544895 2.725187 -4.244424 +v -0.513280 2.537594 -4.197108 +v -0.461939 2.364707 -4.120272 +v -0.392847 2.213171 -4.016869 +v -0.308658 2.088808 -3.890870 +v -0.212607 1.996398 -3.747120 +v -0.108386 1.939492 -3.591142 +v -0.074658 3.901062 -3.609171 +v -0.146446 3.844157 -3.782484 +v -0.212607 3.751747 -3.942210 +v -0.270598 3.627384 -4.082212 +v -0.318189 3.475848 -4.197108 +v -0.353553 3.302961 -4.282484 +v -0.375330 3.115368 -4.335058 +v -0.382683 2.920277 -4.352809 +v -0.375330 2.725187 -4.335058 +v -0.353553 2.537594 -4.282484 +v -0.318189 2.364707 -4.197108 +v -0.270598 2.213171 -4.082212 +v -0.212607 2.088808 -3.942210 +v -0.146446 1.996398 -3.782484 +v -0.074658 1.939492 -3.609171 +v -0.038060 3.901062 -3.620272 +v -0.074658 3.844157 -3.804261 +v -0.108386 3.751747 -3.973825 +v -0.137950 3.627384 -4.122451 +v -0.162211 3.475848 -4.244423 +v -0.180240 3.302961 -4.335058 +v -0.191341 3.115368 -4.390870 +v -0.195090 2.920277 -4.409715 +v -0.191341 2.725187 -4.390870 +v -0.180240 2.537594 -4.335058 +v -0.162211 2.364707 -4.244423 +v -0.137950 2.213171 -4.122451 +v -0.108386 2.088808 -3.973825 +v -0.074658 1.996398 -3.804261 +v -0.038060 1.939492 -3.620272 +v 0.000000 3.901062 -3.624021 +v 0.000000 3.844157 -3.811614 +v 0.000000 3.627384 -4.136038 +v 0.000000 3.302961 -4.352810 +v 0.000000 2.537594 -4.352810 +v 0.000000 2.213171 -4.136038 +v 0.000000 2.088808 -3.984500 +v 0.000000 1.996398 -3.811614 +v 0.000000 1.939492 -3.624021 +vn -0.0000 0.8286 -0.5598 +vn 0.0757 0.9217 -0.3804 +vn 0.1092 0.8286 -0.5490 +vn -0.0000 -0.3805 -0.9248 +vn 0.1626 -0.5528 -0.8173 +vn -0.0000 -0.5528 -0.8333 +vn -0.0000 0.7041 -0.7101 +vn 0.1385 0.7041 -0.6965 +vn 0.1385 -0.7041 -0.6965 +vn -0.0000 -0.7041 -0.7101 +vn -0.0000 0.5528 -0.8333 +vn 0.1626 0.5528 -0.8173 +vn 0.1092 -0.8286 -0.5490 +vn -0.0000 -0.8286 -0.5598 +vn -0.0000 0.3805 -0.9248 +vn 0.1804 0.3805 -0.9070 +vn 0.0757 -0.9217 -0.3804 +vn -0.0000 -0.9217 -0.3879 +vn -0.0000 0.1939 -0.9810 +vn 0.1914 0.1939 -0.9622 +vn 0.0392 -0.9796 -0.1971 +vn -0.0000 -0.9796 -0.2010 +vn 0.1951 -0.0000 -0.9808 +vn -0.0000 -0.0000 -1.0000 +vn -0.0000 0.9796 -0.2010 +vn -0.0000 1.0000 -0.0000 +vn 0.0392 0.9796 -0.1971 +vn -0.0000 -1.0000 -0.0000 +vn 0.1914 -0.1939 -0.9622 +vn -0.0000 -0.1939 -0.9810 +vn -0.0000 0.9217 -0.3879 +vn 0.1804 -0.3805 -0.9070 +vn 0.1484 0.9217 -0.3584 +vn 0.3539 -0.3805 -0.8544 +vn 0.2142 0.8286 -0.5172 +vn 0.3189 -0.5528 -0.7699 +vn 0.2718 0.7041 -0.6561 +vn 0.2718 -0.7041 -0.6561 +vn 0.3189 0.5528 -0.7699 +vn 0.2142 -0.8286 -0.5172 +vn 0.3539 0.3805 -0.8544 +vn 0.1484 -0.9217 -0.3584 +vn 0.3754 0.1939 -0.9063 +vn 0.0769 -0.9796 -0.1857 +vn 0.3827 -0.0000 -0.9239 +vn 0.0769 0.9796 -0.1857 +vn 0.3754 -0.1939 -0.9063 +vn 0.5138 0.3805 -0.7689 +vn 0.2155 -0.9217 -0.3225 +vn 0.5450 0.1939 -0.8157 +vn 0.1117 -0.9796 -0.1671 +vn 0.5556 -0.0000 -0.8315 +vn 0.1117 0.9796 -0.1671 +vn 0.5450 -0.1939 -0.8157 +vn 0.2155 0.9217 -0.3225 +vn 0.5138 -0.3805 -0.7689 +vn 0.3110 0.8286 -0.4654 +vn 0.4630 -0.5528 -0.6929 +vn 0.3945 0.7041 -0.5905 +vn 0.3945 -0.7041 -0.5905 +vn 0.4630 0.5528 -0.6929 +vn 0.3110 -0.8286 -0.4654 +vn 0.6539 -0.3805 -0.6539 +vn 0.3958 0.8286 -0.3958 +vn 0.5893 -0.5528 -0.5893 +vn 0.5021 0.7041 -0.5021 +vn 0.5021 -0.7041 -0.5021 +vn 0.5893 0.5528 -0.5893 +vn 0.3958 -0.8286 -0.3958 +vn 0.6539 0.3805 -0.6539 +vn 0.2743 -0.9217 -0.2743 +vn 0.6937 0.1939 -0.6937 +vn 0.1421 -0.9796 -0.1421 +vn 0.7071 -0.0000 -0.7071 +vn 0.1421 0.9796 -0.1421 +vn 0.6937 -0.1939 -0.6937 +vn 0.2743 0.9217 -0.2743 +vn 0.4654 -0.8286 -0.3110 +vn 0.3225 -0.9217 -0.2155 +vn 0.7689 0.3805 -0.5138 +vn 0.8157 0.1939 -0.5450 +vn 0.1671 -0.9796 -0.1117 +vn 0.8315 -0.0000 -0.5556 +vn 0.1671 0.9796 -0.1117 +vn 0.8157 -0.1939 -0.5450 +vn 0.3225 0.9217 -0.2155 +vn 0.7689 -0.3805 -0.5138 +vn 0.4654 0.8286 -0.3110 +vn 0.6929 -0.5528 -0.4630 +vn 0.5905 0.7041 -0.3945 +vn 0.5905 -0.7041 -0.3945 +vn 0.6929 0.5528 -0.4630 +vn 0.5172 0.8286 -0.2142 +vn 0.8544 -0.3805 -0.3539 +vn 0.7699 -0.5528 -0.3189 +vn 0.6561 0.7041 -0.2718 +vn 0.6561 -0.7041 -0.2718 +vn 0.7699 0.5528 -0.3189 +vn 0.5172 -0.8286 -0.2142 +vn 0.8544 0.3805 -0.3539 +vn 0.3584 -0.9217 -0.1484 +vn 0.9063 0.1939 -0.3754 +vn 0.1857 -0.9796 -0.0769 +vn 0.9239 -0.0000 -0.3827 +vn 0.1857 0.9796 -0.0769 +vn 0.9063 -0.1939 -0.3754 +vn 0.3584 0.9217 -0.1484 +vn 0.9070 0.3805 -0.1804 +vn 0.9622 0.1939 -0.1914 +vn 0.1971 -0.9796 -0.0392 +vn 0.9808 -0.0000 -0.1951 +vn 0.1971 0.9796 -0.0392 +vn 0.9622 -0.1939 -0.1914 +vn 0.3804 0.9217 -0.0757 +vn 0.9070 -0.3805 -0.1804 +vn 0.5490 0.8286 -0.1092 +vn 0.8173 -0.5528 -0.1626 +vn 0.6965 0.7041 -0.1385 +vn 0.6965 -0.7041 -0.1385 +vn 0.8173 0.5528 -0.1626 +vn 0.5490 -0.8286 -0.1092 +vn 0.3804 -0.9217 -0.0757 +vn 0.9248 -0.3805 -0.0000 +vn 0.8333 -0.5528 -0.0000 +vn 0.7101 0.7041 -0.0000 +vn 0.7101 -0.7041 -0.0000 +vn 0.8333 0.5528 -0.0000 +vn 0.5598 -0.8286 -0.0000 +vn 0.9248 0.3805 -0.0000 +vn 0.3879 -0.9217 -0.0000 +vn 0.9810 0.1939 -0.0000 +vn 0.2010 -0.9796 -0.0000 +vn 1.0000 -0.0000 -0.0000 +vn 0.2010 0.9796 -0.0000 +vn 0.9810 -0.1939 -0.0000 +vn 0.3879 0.9217 -0.0000 +vn 0.5598 0.8286 -0.0000 +vn 0.1971 -0.9796 0.0392 +vn 0.9622 0.1939 0.1914 +vn 0.9808 -0.0000 0.1951 +vn 0.1971 0.9796 0.0392 +vn 0.9622 -0.1939 0.1914 +vn 0.3804 0.9217 0.0757 +vn 0.9070 -0.3805 0.1804 +vn 0.5490 0.8286 0.1092 +vn 0.8173 -0.5528 0.1626 +vn 0.6965 0.7041 0.1385 +vn 0.6965 -0.7041 0.1385 +vn 0.8173 0.5528 0.1626 +vn 0.5490 -0.8286 0.1092 +vn 0.9070 0.3805 0.1804 +vn 0.3804 -0.9217 0.0757 +vn 0.6561 -0.7041 0.2718 +vn 0.6561 0.7041 0.2718 +vn 0.7699 0.5528 0.3189 +vn 0.5172 -0.8286 0.2142 +vn 0.8544 0.3805 0.3539 +vn 0.3584 -0.9217 0.1484 +vn 0.9063 0.1939 0.3754 +vn 0.1857 -0.9796 0.0769 +vn 0.9239 -0.0000 0.3827 +vn 0.1857 0.9796 0.0769 +vn 0.9063 -0.1939 0.3754 +vn 0.3584 0.9217 0.1484 +vn 0.8544 -0.3805 0.3539 +vn 0.5172 0.8286 0.2142 +vn 0.7699 -0.5528 0.3189 +vn 0.1671 0.9796 0.1117 +vn 0.1671 -0.9796 0.1117 +vn 0.8157 -0.1939 0.5450 +vn 0.3225 0.9217 0.2155 +vn 0.7689 -0.3805 0.5138 +vn 0.4654 0.8286 0.3110 +vn 0.6929 -0.5528 0.4630 +vn 0.5905 0.7041 0.3945 +vn 0.5905 -0.7041 0.3945 +vn 0.6929 0.5528 0.4630 +vn 0.4654 -0.8286 0.3110 +vn 0.7689 0.3805 0.5138 +vn 0.3225 -0.9217 0.2155 +vn 0.8157 0.1939 0.5450 +vn 0.8315 -0.0000 0.5556 +vn 0.5021 0.7041 0.5021 +vn 0.5893 0.5528 0.5893 +vn 0.3958 -0.8286 0.3958 +vn 0.6539 0.3805 0.6539 +vn 0.2743 -0.9217 0.2743 +vn 0.6937 0.1939 0.6937 +vn 0.1421 -0.9796 0.1421 +vn 0.7071 -0.0000 0.7071 +vn 0.1421 0.9796 0.1421 +vn 0.6937 -0.1939 0.6937 +vn 0.2743 0.9217 0.2743 +vn 0.6539 -0.3805 0.6539 +vn 0.3958 0.8286 0.3958 +vn 0.5893 -0.5528 0.5893 +vn 0.5021 -0.7041 0.5021 +vn 0.5450 -0.1939 0.8157 +vn 0.1117 0.9796 0.1671 +vn 0.2155 0.9217 0.3225 +vn 0.5138 -0.3805 0.7689 +vn 0.3110 0.8286 0.4654 +vn 0.4630 -0.5528 0.6929 +vn 0.3945 0.7041 0.5905 +vn 0.3945 -0.7041 0.5905 +vn 0.4630 0.5528 0.6929 +vn 0.3110 -0.8286 0.4654 +vn 0.5138 0.3805 0.7689 +vn 0.2155 -0.9217 0.3225 +vn 0.5450 0.1939 0.8157 +vn 0.1117 -0.9796 0.1671 +vn 0.5556 -0.0000 0.8315 +vn 0.2718 -0.7041 0.6561 +vn 0.2142 -0.8286 0.5172 +vn 0.3539 0.3805 0.8544 +vn 0.1484 -0.9217 0.3584 +vn 0.3754 0.1939 0.9063 +vn 0.0769 -0.9796 0.1857 +vn 0.3827 -0.0000 0.9239 +vn 0.0769 0.9796 0.1857 +vn 0.3754 -0.1939 0.9063 +vn 0.1484 0.9217 0.3584 +vn 0.3539 -0.3805 0.8544 +vn 0.2142 0.8286 0.5172 +vn 0.3189 -0.5528 0.7699 +vn 0.2718 0.7041 0.6561 +vn 0.3189 0.5528 0.7699 +vn 0.0757 0.9217 0.3804 +vn 0.1804 -0.3805 0.9070 +vn 0.1092 0.8286 0.5490 +vn 0.1626 -0.5528 0.8173 +vn 0.1385 0.7041 0.6965 +vn 0.1385 -0.7041 0.6965 +vn 0.1626 0.5528 0.8173 +vn 0.1092 -0.8286 0.5490 +vn 0.1804 0.3805 0.9070 +vn 0.0757 -0.9217 0.3804 +vn 0.1914 0.1939 0.9622 +vn 0.0392 -0.9796 0.1971 +vn 0.1951 -0.0000 0.9808 +vn 0.0392 0.9796 0.1971 +vn 0.1914 -0.1939 0.9622 +vn -0.0000 0.3805 0.9248 +vn -0.0000 -0.9217 0.3879 +vn -0.0000 0.1939 0.9810 +vn -0.0000 -0.9796 0.2010 +vn -0.0000 -0.0000 1.0000 +vn -0.0000 0.9796 0.2010 +vn -0.0000 -0.1939 0.9810 +vn -0.0000 0.9217 0.3879 +vn -0.0000 -0.3805 0.9248 +vn -0.0000 0.8286 0.5598 +vn -0.0000 -0.5528 0.8333 +vn -0.0000 0.7041 0.7101 +vn -0.0000 -0.7041 0.7101 +vn -0.0000 0.5528 0.8333 +vn -0.0000 -0.8286 0.5598 +vn -0.1804 -0.3805 0.9070 +vn -0.1092 0.8286 0.5490 +vn -0.1626 -0.5528 0.8173 +vn -0.1385 0.7041 0.6965 +vn -0.1385 -0.7041 0.6965 +vn -0.1626 0.5528 0.8173 +vn -0.1092 -0.8286 0.5490 +vn -0.1804 0.3805 0.9070 +vn -0.0757 -0.9217 0.3804 +vn -0.1914 0.1939 0.9622 +vn -0.0392 -0.9796 0.1971 +vn -0.1951 -0.0000 0.9808 +vn -0.0392 0.9796 0.1971 +vn -0.1914 -0.1939 0.9622 +vn -0.0757 0.9217 0.3804 +vn -0.1484 -0.9217 0.3584 +vn -0.3539 0.3805 0.8544 +vn -0.3754 0.1939 0.9063 +vn -0.0769 -0.9796 0.1857 +vn -0.3827 -0.0000 0.9239 +vn -0.0769 0.9796 0.1857 +vn -0.3754 -0.1939 0.9063 +vn -0.1484 0.9217 0.3584 +vn -0.3539 -0.3805 0.8544 +vn -0.2142 0.8286 0.5172 +vn -0.3189 -0.5528 0.7699 +vn -0.2718 0.7041 0.6561 +vn -0.2718 -0.7041 0.6561 +vn -0.3189 0.5528 0.7699 +vn -0.2142 -0.8286 0.5172 +vn -0.5138 -0.3805 0.7689 +vn -0.4630 -0.5528 0.6929 +vn -0.3945 0.7041 0.5905 +vn -0.3945 -0.7041 0.5905 +vn -0.4630 0.5528 0.6929 +vn -0.3110 -0.8286 0.4654 +vn -0.5138 0.3805 0.7689 +vn -0.2155 -0.9217 0.3225 +vn -0.5450 0.1939 0.8157 +vn -0.1117 -0.9796 0.1671 +vn -0.5556 -0.0000 0.8315 +vn -0.1117 0.9796 0.1671 +vn -0.5450 -0.1939 0.8157 +vn -0.2155 0.9217 0.3225 +vn -0.3110 0.8286 0.4654 +vn -0.2743 -0.9217 0.2743 +vn -0.1421 -0.9796 0.1421 +vn -0.6937 0.1939 0.6937 +vn -0.7071 -0.0000 0.7071 +vn -0.1421 0.9796 0.1421 +vn -0.6937 -0.1939 0.6937 +vn -0.2743 0.9217 0.2743 +vn -0.6539 -0.3805 0.6539 +vn -0.3958 0.8286 0.3958 +vn -0.5893 -0.5528 0.5893 +vn -0.5021 0.7041 0.5021 +vn -0.5021 -0.7041 0.5021 +vn -0.5893 0.5528 0.5893 +vn -0.3958 -0.8286 0.3958 +vn -0.6539 0.3805 0.6539 +vn -0.5905 0.7041 0.3945 +vn -0.5905 -0.7041 0.3945 +vn -0.6929 0.5528 0.4630 +vn -0.4654 -0.8286 0.3110 +vn -0.7689 0.3805 0.5138 +vn -0.3225 -0.9217 0.2155 +vn -0.8157 0.1939 0.5450 +vn -0.1671 -0.9796 0.1117 +vn -0.8315 -0.0000 0.5556 +vn -0.1671 0.9796 0.1117 +vn -0.8157 -0.1939 0.5450 +vn -0.3225 0.9217 0.2155 +vn -0.7689 -0.3805 0.5138 +vn -0.4654 0.8286 0.3110 +vn -0.6929 -0.5528 0.4630 +vn -0.9063 0.1939 0.3754 +vn -0.9239 -0.0000 0.3827 +vn -0.1857 0.9796 0.0769 +vn -0.1857 -0.9796 0.0769 +vn -0.9063 -0.1939 0.3754 +vn -0.3584 0.9217 0.1484 +vn -0.8544 -0.3805 0.3539 +vn -0.5172 0.8286 0.2142 +vn -0.7699 -0.5528 0.3189 +vn -0.6561 0.7041 0.2718 +vn -0.6561 -0.7041 0.2718 +vn -0.7699 0.5528 0.3189 +vn -0.5172 -0.8286 0.2142 +vn -0.8544 0.3805 0.3539 +vn -0.3584 -0.9217 0.1484 +vn -0.6965 -0.7041 0.1385 +vn -0.6965 0.7041 0.1385 +vn -0.8173 0.5528 0.1626 +vn -0.5490 -0.8286 0.1092 +vn -0.9070 0.3805 0.1804 +vn -0.3804 -0.9217 0.0757 +vn -0.9622 0.1939 0.1914 +vn -0.1971 -0.9796 0.0392 +vn -0.9808 -0.0000 0.1951 +vn -0.1971 0.9796 0.0392 +vn -0.9622 -0.1939 0.1914 +vn -0.3804 0.9217 0.0757 +vn -0.9070 -0.3805 0.1804 +vn -0.5490 0.8286 0.1092 +vn -0.8173 -0.5528 0.1626 +vn -0.2010 0.9796 -0.0000 +vn -0.2010 -0.9796 -0.0000 +vn -0.9810 -0.1939 -0.0000 +vn -0.3879 0.9217 -0.0000 +vn -0.9248 -0.3805 -0.0000 +vn -0.5598 0.8286 -0.0000 +vn -0.8333 -0.5528 -0.0000 +vn -0.7101 0.7041 -0.0000 +vn -0.7101 -0.7041 -0.0000 +vn -0.8333 0.5528 -0.0000 +vn -0.5598 -0.8286 -0.0000 +vn -0.9248 0.3805 -0.0000 +vn -0.3879 -0.9217 -0.0000 +vn -0.9810 0.1939 -0.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.6965 0.7041 -0.1385 +vn -0.8173 0.5528 -0.1626 +vn -0.6965 -0.7041 -0.1385 +vn -0.5490 -0.8286 -0.1092 +vn -0.9070 0.3805 -0.1804 +vn -0.3804 -0.9217 -0.0757 +vn -0.9622 0.1939 -0.1914 +vn -0.1971 -0.9796 -0.0392 +vn -0.9808 -0.0000 -0.1951 +vn -0.1971 0.9796 -0.0392 +vn -0.9622 -0.1939 -0.1914 +vn -0.3804 0.9217 -0.0757 +vn -0.9070 -0.3805 -0.1804 +vn -0.5490 0.8286 -0.1092 +vn -0.8173 -0.5528 -0.1626 +vn -0.9063 -0.1939 -0.3754 +vn -0.3584 0.9217 -0.1484 +vn -0.8544 -0.3805 -0.3539 +vn -0.5172 0.8286 -0.2142 +vn -0.7699 -0.5528 -0.3189 +vn -0.6561 0.7041 -0.2718 +vn -0.6561 -0.7041 -0.2718 +vn -0.7699 0.5528 -0.3189 +vn -0.5172 -0.8286 -0.2142 +vn -0.8544 0.3805 -0.3539 +vn -0.3584 -0.9217 -0.1484 +vn -0.9063 0.1939 -0.3754 +vn -0.1857 -0.9796 -0.0769 +vn -0.9239 -0.0000 -0.3827 +vn -0.1857 0.9796 -0.0769 +vn -0.5905 -0.7041 -0.3945 +vn -0.4654 -0.8286 -0.3110 +vn -0.7689 0.3805 -0.5138 +vn -0.3225 -0.9217 -0.2155 +vn -0.8157 0.1939 -0.5450 +vn -0.1671 -0.9796 -0.1117 +vn -0.8315 -0.0000 -0.5556 +vn -0.1671 0.9796 -0.1117 +vn -0.8157 -0.1939 -0.5450 +vn -0.3225 0.9217 -0.2155 +vn -0.7689 -0.3805 -0.5138 +vn -0.4654 0.8286 -0.3110 +vn -0.6929 -0.5528 -0.4630 +vn -0.5905 0.7041 -0.3945 +vn -0.6929 0.5528 -0.4630 +vn -0.6539 -0.3805 -0.6539 +vn -0.2743 0.9217 -0.2743 +vn -0.3958 0.8286 -0.3958 +vn -0.5893 -0.5528 -0.5893 +vn -0.5021 0.7041 -0.5021 +vn -0.5021 -0.7041 -0.5021 +vn -0.5893 0.5528 -0.5893 +vn -0.3958 -0.8286 -0.3958 +vn -0.6539 0.3805 -0.6539 +vn -0.2743 -0.9217 -0.2743 +vn -0.6937 0.1939 -0.6937 +vn -0.1421 -0.9796 -0.1421 +vn -0.7071 -0.0000 -0.7071 +vn -0.1421 0.9796 -0.1421 +vn -0.6937 -0.1939 -0.6937 +vn -0.2155 -0.9217 -0.3225 +vn -0.5138 0.3805 -0.7689 +vn -0.5450 0.1939 -0.8157 +vn -0.1117 -0.9796 -0.1671 +vn -0.5556 -0.0000 -0.8315 +vn -0.1117 0.9796 -0.1671 +vn -0.5450 -0.1939 -0.8157 +vn -0.2155 0.9217 -0.3225 +vn -0.5138 -0.3805 -0.7689 +vn -0.3110 0.8286 -0.4654 +vn -0.4630 -0.5528 -0.6929 +vn -0.3945 0.7041 -0.5905 +vn -0.3945 -0.7041 -0.5905 +vn -0.4630 0.5528 -0.6929 +vn -0.3110 -0.8286 -0.4654 +vn -0.2142 0.8286 -0.5172 +vn -0.3539 -0.3805 -0.8544 +vn -0.3189 -0.5528 -0.7699 +vn -0.2718 0.7041 -0.6561 +vn -0.2718 -0.7041 -0.6561 +vn -0.3189 0.5528 -0.7699 +vn -0.2142 -0.8286 -0.5172 +vn -0.3539 0.3805 -0.8544 +vn -0.1484 -0.9217 -0.3584 +vn -0.3754 0.1939 -0.9063 +vn -0.0769 -0.9796 -0.1857 +vn -0.3827 -0.0000 -0.9239 +vn -0.0769 0.9796 -0.1857 +vn -0.3754 -0.1939 -0.9063 +vn -0.1484 0.9217 -0.3584 +vn -0.1804 0.3805 -0.9070 +vn -0.1914 0.1939 -0.9622 +vn -0.0757 -0.9217 -0.3804 +vn -0.0392 -0.9796 -0.1971 +vn -0.1951 -0.0000 -0.9808 +vn -0.0392 0.9796 -0.1971 +vn -0.1914 -0.1939 -0.9622 +vn -0.0757 0.9217 -0.3804 +vn -0.1804 -0.3805 -0.9070 +vn -0.1092 0.8286 -0.5490 +vn -0.1626 -0.5528 -0.8173 +vn -0.1385 0.7041 -0.6965 +vn -0.1385 -0.7041 -0.6965 +vn -0.1626 0.5528 -0.8173 +vn -0.1092 -0.8286 -0.5490 +vt 0.750000 0.812500 +vt 0.750000 0.687500 +vt 0.750000 0.562500 +vt 0.750000 0.500000 +vt 0.750000 0.437500 +vt 0.750000 0.312500 +vt 0.718750 0.937500 +vt 0.718750 0.875000 +vt 0.718750 0.812500 +vt 0.718750 0.750000 +vt 0.718750 0.687500 +vt 0.718750 0.625000 +vt 0.718750 0.562500 +vt 0.718750 0.500000 +vt 0.718750 0.437500 +vt 0.718750 0.375000 +vt 0.718750 0.312500 +vt 0.718750 0.250000 +vt 0.718750 0.187500 +vt 0.718750 0.125000 +vt 0.718750 0.062500 +vt 0.687500 0.937500 +vt 0.687500 0.875000 +vt 0.687500 0.812500 +vt 0.687500 0.750000 +vt 0.687500 0.687500 +vt 0.687500 0.625000 +vt 0.687500 0.562500 +vt 0.687500 0.500000 +vt 0.687500 0.437500 +vt 0.687500 0.375000 +vt 0.687500 0.312500 +vt 0.687500 0.250000 +vt 0.687500 0.187500 +vt 0.687500 0.125000 +vt 0.687500 0.062500 +vt 0.656250 0.937500 +vt 0.656250 0.875000 +vt 0.656250 0.812500 +vt 0.656250 0.750000 +vt 0.656250 0.687500 +vt 0.656250 0.625000 +vt 0.656250 0.562500 +vt 0.656250 0.500000 +vt 0.656250 0.437500 +vt 0.656250 0.375000 +vt 0.656250 0.312500 +vt 0.656250 0.250000 +vt 0.656250 0.187500 +vt 0.656250 0.125000 +vt 0.656250 0.062500 +vt 0.625000 0.937500 +vt 0.625000 0.875000 +vt 0.625000 0.812500 +vt 0.625000 0.750000 +vt 0.625000 0.687500 +vt 0.625000 0.625000 +vt 0.625000 0.562500 +vt 0.625000 0.500000 +vt 0.625000 0.437500 +vt 0.625000 0.375000 +vt 0.625000 0.312500 +vt 0.625000 0.250000 +vt 0.625000 0.187500 +vt 0.625000 0.125000 +vt 0.625000 0.062500 +vt 0.593750 0.937500 +vt 0.593750 0.875000 +vt 0.593750 0.812500 +vt 0.593750 0.750000 +vt 0.593750 0.687500 +vt 0.593750 0.625000 +vt 0.593750 0.562500 +vt 0.593750 0.500000 +vt 0.593750 0.437500 +vt 0.593750 0.375000 +vt 0.593750 0.312500 +vt 0.593750 0.250000 +vt 0.593750 0.187500 +vt 0.593750 0.125000 +vt 0.593750 0.062500 +vt 0.734375 1.000000 +vt 0.703125 1.000000 +vt 0.671875 1.000000 +vt 0.640625 1.000000 +vt 0.609375 1.000000 +vt 0.578125 1.000000 +vt 0.546875 1.000000 +vt 0.515625 1.000000 +vt 0.484375 1.000000 +vt 0.453125 1.000000 +vt 0.421875 1.000000 +vt 0.390625 1.000000 +vt 0.359375 1.000000 +vt 0.328125 1.000000 +vt 0.296875 1.000000 +vt 0.265625 1.000000 +vt 0.234375 1.000000 +vt 0.203125 1.000000 +vt 0.171875 1.000000 +vt 0.140625 1.000000 +vt 0.109375 1.000000 +vt 0.078125 1.000000 +vt 0.046875 1.000000 +vt 0.015625 1.000000 +vt 0.984375 1.000000 +vt 0.953125 1.000000 +vt 0.921875 1.000000 +vt 0.890625 1.000000 +vt 0.859375 1.000000 +vt 0.828125 1.000000 +vt 0.796875 1.000000 +vt 0.765625 1.000000 +vt 0.562500 0.937500 +vt 0.562500 0.875000 +vt 0.562500 0.812500 +vt 0.562500 0.750000 +vt 0.562500 0.687500 +vt 0.562500 0.625000 +vt 0.562500 0.562500 +vt 0.562500 0.500000 +vt 0.562500 0.437500 +vt 0.562500 0.375000 +vt 0.562500 0.312500 +vt 0.562500 0.250000 +vt 0.562500 0.187500 +vt 0.562500 0.125000 +vt 0.562500 0.062500 +vt 0.531250 0.937500 +vt 0.531250 0.875000 +vt 0.531250 0.812500 +vt 0.531250 0.750000 +vt 0.531250 0.687500 +vt 0.531250 0.625000 +vt 0.531250 0.562500 +vt 0.531250 0.500000 +vt 0.531250 0.437500 +vt 0.531250 0.375000 +vt 0.531250 0.312500 +vt 0.531250 0.250000 +vt 0.531250 0.187500 +vt 0.531250 0.125000 +vt 0.531250 0.062500 +vt 0.500000 0.937500 +vt 0.500000 0.875000 +vt 0.500000 0.812500 +vt 0.500000 0.750000 +vt 0.500000 0.687500 +vt 0.500000 0.625000 +vt 0.500000 0.562500 +vt 0.500000 0.500000 +vt 0.500000 0.437500 +vt 0.500000 0.375000 +vt 0.500000 0.312500 +vt 0.500000 0.250000 +vt 0.500000 0.187500 +vt 0.500000 0.125000 +vt 0.500000 0.062500 +vt 0.468750 0.937500 +vt 0.468750 0.875000 +vt 0.468750 0.812500 +vt 0.468750 0.750000 +vt 0.468750 0.687500 +vt 0.468750 0.625000 +vt 0.468750 0.562500 +vt 0.468750 0.500000 +vt 0.468750 0.437500 +vt 0.468750 0.375000 +vt 0.468750 0.312500 +vt 0.468750 0.250000 +vt 0.468750 0.187500 +vt 0.468750 0.125000 +vt 0.468750 0.062500 +vt 0.437500 0.937500 +vt 0.437500 0.875000 +vt 0.437500 0.812500 +vt 0.437500 0.750000 +vt 0.437500 0.687500 +vt 0.437500 0.625000 +vt 0.437500 0.562500 +vt 0.437500 0.500000 +vt 0.437500 0.437500 +vt 0.437500 0.375000 +vt 0.437500 0.312500 +vt 0.437500 0.250000 +vt 0.437500 0.187500 +vt 0.437500 0.125000 +vt 0.437500 0.062500 +vt 0.406250 0.937500 +vt 0.406250 0.875000 +vt 0.406250 0.812500 +vt 0.406250 0.750000 +vt 0.406250 0.687500 +vt 0.406250 0.625000 +vt 0.406250 0.562500 +vt 0.406250 0.500000 +vt 0.406250 0.437500 +vt 0.406250 0.375000 +vt 0.406250 0.312500 +vt 0.406250 0.250000 +vt 0.406250 0.187500 +vt 0.406250 0.125000 +vt 0.406250 0.062500 +vt 0.375000 0.937500 +vt 0.375000 0.875000 +vt 0.375000 0.812500 +vt 0.375000 0.750000 +vt 0.375000 0.687500 +vt 0.375000 0.625000 +vt 0.375000 0.562500 +vt 0.375000 0.500000 +vt 0.375000 0.437500 +vt 0.375000 0.375000 +vt 0.375000 0.312500 +vt 0.375000 0.250000 +vt 0.375000 0.187500 +vt 0.375000 0.125000 +vt 0.375000 0.062500 +vt 0.343750 0.937500 +vt 0.343750 0.875000 +vt 0.343750 0.812500 +vt 0.343750 0.750000 +vt 0.343750 0.687500 +vt 0.343750 0.625000 +vt 0.343750 0.562500 +vt 0.343750 0.500000 +vt 0.343750 0.437500 +vt 0.343750 0.375000 +vt 0.343750 0.312500 +vt 0.343750 0.250000 +vt 0.343750 0.187500 +vt 0.343750 0.125000 +vt 0.343750 0.062500 +vt 0.312500 0.937500 +vt 0.312500 0.875000 +vt 0.312500 0.812500 +vt 0.312500 0.750000 +vt 0.312500 0.687500 +vt 0.312500 0.625000 +vt 0.312500 0.562500 +vt 0.312500 0.500000 +vt 0.312500 0.437500 +vt 0.312500 0.375000 +vt 0.312500 0.312500 +vt 0.312500 0.250000 +vt 0.312500 0.187500 +vt 0.312500 0.125000 +vt 0.312500 0.062500 +vt 0.281250 0.937500 +vt 0.281250 0.875000 +vt 0.281250 0.812500 +vt 0.281250 0.750000 +vt 0.281250 0.687500 +vt 0.281250 0.625000 +vt 0.281250 0.562500 +vt 0.281250 0.500000 +vt 0.281250 0.437500 +vt 0.281250 0.375000 +vt 0.281250 0.312500 +vt 0.281250 0.250000 +vt 0.281250 0.187500 +vt 0.281250 0.125000 +vt 0.281250 0.062500 +vt 0.250000 0.937500 +vt 0.250000 0.875000 +vt 0.250000 0.812500 +vt 0.250000 0.750000 +vt 0.250000 0.687500 +vt 0.250000 0.625000 +vt 0.250000 0.562500 +vt 0.250000 0.500000 +vt 0.250000 0.437500 +vt 0.250000 0.375000 +vt 0.250000 0.312500 +vt 0.250000 0.250000 +vt 0.250000 0.187500 +vt 0.250000 0.125000 +vt 0.250000 0.062500 +vt 0.218750 0.937500 +vt 0.218750 0.875000 +vt 0.218750 0.812500 +vt 0.218750 0.750000 +vt 0.218750 0.687500 +vt 0.218750 0.625000 +vt 0.218750 0.562500 +vt 0.218750 0.500000 +vt 0.218750 0.437500 +vt 0.218750 0.375000 +vt 0.218750 0.312500 +vt 0.218750 0.250000 +vt 0.218750 0.187500 +vt 0.218750 0.125000 +vt 0.218750 0.062500 +vt 0.187500 0.937500 +vt 0.187500 0.875000 +vt 0.187500 0.812500 +vt 0.187500 0.750000 +vt 0.187500 0.687500 +vt 0.187500 0.625000 +vt 0.187500 0.562500 +vt 0.187500 0.500000 +vt 0.187500 0.437500 +vt 0.187500 0.375000 +vt 0.187500 0.312500 +vt 0.187500 0.250000 +vt 0.187500 0.187500 +vt 0.187500 0.125000 +vt 0.187500 0.062500 +vt 0.156250 0.937500 +vt 0.156250 0.875000 +vt 0.156250 0.812500 +vt 0.156250 0.750000 +vt 0.156250 0.687500 +vt 0.156250 0.625000 +vt 0.156250 0.562500 +vt 0.156250 0.500000 +vt 0.156250 0.437500 +vt 0.156250 0.375000 +vt 0.156250 0.312500 +vt 0.156250 0.250000 +vt 0.156250 0.187500 +vt 0.156250 0.125000 +vt 0.156250 0.062500 +vt 0.125000 0.937500 +vt 0.125000 0.875000 +vt 0.125000 0.812500 +vt 0.125000 0.750000 +vt 0.125000 0.687500 +vt 0.125000 0.625000 +vt 0.125000 0.562500 +vt 0.125000 0.500000 +vt 0.125000 0.437500 +vt 0.125000 0.375000 +vt 0.125000 0.312500 +vt 0.125000 0.250000 +vt 0.125000 0.187500 +vt 0.125000 0.125000 +vt 0.125000 0.062500 +vt 0.734375 0.000000 +vt 0.703125 0.000000 +vt 0.671875 0.000000 +vt 0.640625 0.000000 +vt 0.609375 0.000000 +vt 0.578125 0.000000 +vt 0.546875 0.000000 +vt 0.515625 0.000000 +vt 0.484375 0.000000 +vt 0.453125 0.000000 +vt 0.421875 0.000000 +vt 0.390625 0.000000 +vt 0.359375 0.000000 +vt 0.328125 0.000000 +vt 0.296875 0.000000 +vt 0.265625 0.000000 +vt 0.234375 0.000000 +vt 0.203125 0.000000 +vt 0.171875 0.000000 +vt 0.140625 0.000000 +vt 0.109375 0.000000 +vt 0.078125 0.000000 +vt 0.046875 0.000000 +vt 0.015625 0.000000 +vt 0.984375 0.000000 +vt 0.953125 0.000000 +vt 0.921875 0.000000 +vt 0.890625 0.000000 +vt 0.859375 0.000000 +vt 0.828125 0.000000 +vt 0.796875 0.000000 +vt 0.765625 0.000000 +vt 0.093750 0.937500 +vt 0.093750 0.875000 +vt 0.093750 0.812500 +vt 0.093750 0.750000 +vt 0.093750 0.687500 +vt 0.093750 0.625000 +vt 0.093750 0.562500 +vt 0.093750 0.500000 +vt 0.093750 0.437500 +vt 0.093750 0.375000 +vt 0.093750 0.312500 +vt 0.093750 0.250000 +vt 0.093750 0.187500 +vt 0.093750 0.125000 +vt 0.093750 0.062500 +vt 0.062500 0.937500 +vt 0.062500 0.875000 +vt 0.062500 0.812500 +vt 0.062500 0.750000 +vt 0.062500 0.687500 +vt 0.062500 0.625000 +vt 0.062500 0.562500 +vt 0.062500 0.500000 +vt 0.062500 0.437500 +vt 0.062500 0.375000 +vt 0.062500 0.312500 +vt 0.062500 0.250000 +vt 0.062500 0.187500 +vt 0.062500 0.125000 +vt 0.062500 0.062500 +vt 0.031250 0.937500 +vt 0.031250 0.875000 +vt 0.031250 0.812500 +vt 0.031250 0.750000 +vt 0.031250 0.687500 +vt 0.031250 0.625000 +vt 0.031250 0.562500 +vt 0.031250 0.500000 +vt 0.031250 0.437500 +vt 0.031250 0.375000 +vt 0.031250 0.312500 +vt 0.031250 0.250000 +vt 0.031250 0.187500 +vt 0.031250 0.125000 +vt 0.031250 0.062500 +vt 0.000000 0.937500 +vt 1.000000 0.937500 +vt 0.000000 0.875000 +vt 1.000000 0.875000 +vt 0.000000 0.812500 +vt 1.000000 0.812500 +vt 0.000000 0.750000 +vt 1.000000 0.750000 +vt 0.000000 0.687500 +vt 1.000000 0.687500 +vt 0.000000 0.625000 +vt 1.000000 0.625000 +vt 0.000000 0.562500 +vt 1.000000 0.562500 +vt 0.000000 0.500000 +vt 1.000000 0.500000 +vt 0.000000 0.437500 +vt 1.000000 0.437500 +vt 0.000000 0.375000 +vt 1.000000 0.375000 +vt 0.000000 0.312500 +vt 1.000000 0.312500 +vt 0.000000 0.250000 +vt 1.000000 0.250000 +vt 0.000000 0.187500 +vt 1.000000 0.187500 +vt 0.000000 0.125000 +vt 1.000000 0.125000 +vt 1.000000 0.062500 +vt 0.000000 0.062500 +vt 0.968750 0.937500 +vt 0.968750 0.875000 +vt 0.968750 0.812500 +vt 0.968750 0.750000 +vt 0.968750 0.687500 +vt 0.968750 0.625000 +vt 0.968750 0.562500 +vt 0.968750 0.500000 +vt 0.968750 0.437500 +vt 0.968750 0.375000 +vt 0.968750 0.312500 +vt 0.968750 0.250000 +vt 0.968750 0.187500 +vt 0.968750 0.125000 +vt 0.968750 0.062500 +vt 0.937500 0.937500 +vt 0.937500 0.875000 +vt 0.937500 0.812500 +vt 0.937500 0.750000 +vt 0.937500 0.687500 +vt 0.937500 0.625000 +vt 0.937500 0.562500 +vt 0.937500 0.500000 +vt 0.937500 0.437500 +vt 0.937500 0.375000 +vt 0.937500 0.312500 +vt 0.937500 0.250000 +vt 0.937500 0.187500 +vt 0.937500 0.125000 +vt 0.937500 0.062500 +vt 0.906250 0.937500 +vt 0.906250 0.875000 +vt 0.906250 0.812500 +vt 0.906250 0.750000 +vt 0.906250 0.687500 +vt 0.906250 0.625000 +vt 0.906250 0.562500 +vt 0.906250 0.500000 +vt 0.906250 0.437500 +vt 0.906250 0.375000 +vt 0.906250 0.312500 +vt 0.906250 0.250000 +vt 0.906250 0.187500 +vt 0.906250 0.125000 +vt 0.906250 0.062500 +vt 0.875000 0.937500 +vt 0.875000 0.875000 +vt 0.875000 0.812500 +vt 0.875000 0.750000 +vt 0.875000 0.687500 +vt 0.875000 0.625000 +vt 0.875000 0.562500 +vt 0.875000 0.500000 +vt 0.875000 0.437500 +vt 0.875000 0.375000 +vt 0.875000 0.312500 +vt 0.875000 0.250000 +vt 0.875000 0.187500 +vt 0.875000 0.125000 +vt 0.875000 0.062500 +vt 0.843750 0.937500 +vt 0.843750 0.875000 +vt 0.843750 0.812500 +vt 0.843750 0.750000 +vt 0.843750 0.687500 +vt 0.843750 0.625000 +vt 0.843750 0.562500 +vt 0.843750 0.500000 +vt 0.843750 0.437500 +vt 0.843750 0.375000 +vt 0.843750 0.312500 +vt 0.843750 0.250000 +vt 0.843750 0.187500 +vt 0.843750 0.125000 +vt 0.843750 0.062500 +vt 0.812500 0.937500 +vt 0.812500 0.875000 +vt 0.812500 0.812500 +vt 0.812500 0.750000 +vt 0.812500 0.687500 +vt 0.812500 0.625000 +vt 0.812500 0.562500 +vt 0.812500 0.500000 +vt 0.812500 0.437500 +vt 0.812500 0.375000 +vt 0.812500 0.312500 +vt 0.812500 0.250000 +vt 0.812500 0.187500 +vt 0.812500 0.125000 +vt 0.812500 0.062500 +vt 0.781250 0.937500 +vt 0.781250 0.875000 +vt 0.781250 0.812500 +vt 0.781250 0.750000 +vt 0.781250 0.687500 +vt 0.781250 0.625000 +vt 0.781250 0.562500 +vt 0.781250 0.500000 +vt 0.781250 0.437500 +vt 0.781250 0.375000 +vt 0.781250 0.312500 +vt 0.781250 0.250000 +vt 0.781250 0.187500 +vt 0.781250 0.125000 +vt 0.781250 0.062500 +vt 0.750000 0.937500 +vt 0.750000 0.875000 +vt 0.750000 0.750000 +vt 0.750000 0.625000 +vt 0.750000 0.375000 +vt 0.750000 0.250000 +vt 0.750000 0.187500 +vt 0.750000 0.125000 +vt 0.750000 0.062500 +s 1 +f 965/1119/965 972/1126/966 973/1127/967 +f 1442/1673/968 981/1135/969 970/1124/970 +f 1440/1671/971 973/1127/967 974/1128/972 +f 970/1124/970 982/1136/973 1443/1674/974 +f 966/1120/975 974/1128/972 975/1129/976 +f 1443/1674/974 983/1137/977 1444/1675/978 +f 1441/1672/979 975/1129/976 976/1130/980 +f 1444/1675/978 984/1138/981 1445/1676/982 +f 967/1121/983 976/1130/980 977/1131/984 +f 1445/1676/982 985/1139/985 1446/1677/986 +f 967/1121/983 978/1132/987 968/1122/988 +f 1438/1669/989 1046/1200/990 971/1125/991 +f 1272/1457/992 1446/1677/986 985/1139/985 +f 968/1122/988 979/1133/993 969/1123/994 +f 1439/1670/995 971/1125/991 972/1126/966 +f 969/1123/994 980/1134/996 1442/1673/968 +f 971/1125/991 987/1141/997 972/1126/966 +f 979/1133/993 995/1149/998 980/1134/996 +f 972/1126/966 988/1142/999 973/1127/967 +f 980/1134/996 996/1150/1000 981/1135/969 +f 973/1127/967 989/1143/1001 974/1128/972 +f 981/1135/969 997/1151/1002 982/1136/973 +f 974/1128/972 990/1144/1003 975/1129/976 +f 983/1137/977 997/1151/1002 998/1152/1004 +f 975/1129/976 991/1145/1005 976/1130/980 +f 984/1138/981 998/1152/1004 999/1153/1006 +f 977/1131/984 991/1145/1005 992/1146/1007 +f 984/1138/981 1000/1154/1008 985/1139/985 +f 977/1131/984 993/1147/1009 978/1132/987 +f 971/1125/991 1046/1201/990 986/1140/1010 +f 1272/1458/992 985/1139/985 1000/1154/1008 +f 978/1132/987 994/1148/1011 979/1133/993 +f 990/1144/1003 1006/1160/1012 991/1145/1005 +f 998/1152/1004 1014/1168/1013 999/1153/1006 +f 991/1145/1005 1007/1161/1014 992/1146/1007 +f 999/1153/1006 1015/1169/1015 1000/1154/1008 +f 992/1146/1007 1008/1162/1016 993/1147/1009 +f 986/1140/1010 1046/1202/990 1001/1155/1017 +f 1272/1459/992 1000/1154/1008 1015/1169/1015 +f 994/1148/1011 1008/1162/1016 1009/1163/1018 +f 986/1140/1010 1002/1156/1019 987/1141/997 +f 994/1148/1011 1010/1164/1020 995/1149/998 +f 987/1141/997 1003/1157/1021 988/1142/999 +f 996/1150/1000 1010/1164/1020 1011/1165/1022 +f 988/1142/999 1004/1158/1023 989/1143/1001 +f 997/1151/1002 1011/1165/1022 1012/1166/1024 +f 989/1143/1001 1005/1159/1025 990/1144/1003 +f 998/1152/1004 1012/1166/1024 1013/1167/1026 +f 1009/1163/1018 1025/1179/1027 1010/1164/1020 +f 1002/1156/1019 1018/1172/1028 1003/1157/1021 +f 1011/1165/1022 1025/1179/1027 1026/1180/1029 +f 1003/1157/1021 1019/1173/1030 1004/1158/1023 +f 1011/1165/1022 1027/1181/1031 1012/1166/1024 +f 1004/1158/1023 1020/1174/1032 1005/1159/1025 +f 1012/1166/1024 1028/1182/1033 1013/1167/1026 +f 1005/1159/1025 1021/1175/1034 1006/1160/1012 +f 1013/1167/1026 1029/1183/1035 1014/1168/1013 +f 1007/1161/1014 1021/1175/1034 1022/1176/1036 +f 1014/1168/1013 1030/1184/1037 1015/1169/1015 +f 1008/1162/1016 1022/1176/1036 1023/1177/1038 +f 1001/1155/1017 1046/1203/990 1016/1170/1039 +f 1272/1460/992 1015/1169/1015 1030/1184/1037 +f 1008/1162/1016 1024/1178/1040 1009/1163/1018 +f 1001/1155/1017 1017/1171/1041 1002/1156/1019 +f 1029/1183/1035 1043/1197/1042 1044/1198/1043 +f 1022/1176/1036 1036/1190/1044 1037/1191/1045 +f 1029/1183/1035 1045/1199/1046 1030/1184/1037 +f 1023/1177/1038 1037/1191/1045 1038/1192/1047 +f 1016/1170/1039 1046/1204/990 1031/1185/1048 +f 1272/1461/992 1030/1184/1037 1045/1199/1046 +f 1023/1177/1038 1039/1193/1049 1024/1178/1040 +f 1016/1170/1039 1032/1186/1050 1017/1171/1041 +f 1024/1178/1040 1040/1194/1051 1025/1179/1027 +f 1017/1171/1041 1033/1187/1052 1018/1172/1028 +f 1026/1180/1029 1040/1194/1051 1041/1195/1053 +f 1018/1172/1028 1034/1188/1054 1019/1173/1030 +f 1026/1180/1029 1042/1196/1055 1027/1181/1031 +f 1020/1174/1032 1034/1188/1054 1035/1189/1056 +f 1027/1181/1031 1043/1197/1042 1028/1182/1033 +f 1020/1174/1032 1036/1190/1044 1021/1175/1034 +f 1032/1186/1050 1049/1234/1057 1033/1187/1052 +f 1041/1195/1053 1056/1241/1058 1057/1242/1059 +f 1033/1187/1052 1050/1235/1060 1034/1188/1054 +f 1041/1195/1053 1058/1243/1061 1042/1196/1055 +f 1035/1189/1056 1050/1235/1060 1051/1236/1062 +f 1042/1196/1055 1059/1244/1063 1043/1197/1042 +f 1035/1189/1056 1052/1237/1064 1036/1190/1044 +f 1043/1197/1042 1060/1245/1065 1044/1198/1043 +f 1037/1191/1045 1052/1237/1064 1053/1238/1066 +f 1045/1199/1046 1060/1245/1065 1061/1246/1067 +f 1038/1192/1047 1053/1238/1066 1054/1239/1068 +f 1031/1185/1048 1046/1205/990 1047/1232/1069 +f 1272/1462/992 1045/1199/1046 1061/1246/1067 +f 1038/1192/1047 1055/1240/1070 1039/1193/1049 +f 1031/1185/1048 1048/1233/1071 1032/1186/1050 +f 1039/1193/1049 1056/1241/1058 1040/1194/1051 +f 1053/1238/1066 1067/1252/1072 1068/1253/1073 +f 1060/1245/1065 1076/1261/1074 1061/1246/1067 +f 1054/1239/1068 1068/1253/1073 1069/1254/1075 +f 1047/1232/1069 1046/1206/990 1062/1247/1076 +f 1272/1463/992 1061/1246/1067 1076/1261/1074 +f 1054/1239/1068 1070/1255/1077 1055/1240/1070 +f 1048/1233/1071 1062/1247/1076 1063/1248/1078 +f 1055/1240/1070 1071/1256/1079 1056/1241/1058 +f 1048/1233/1071 1064/1249/1080 1049/1234/1057 +f 1057/1242/1059 1071/1256/1079 1072/1257/1081 +f 1049/1234/1057 1065/1250/1082 1050/1235/1060 +f 1057/1242/1059 1073/1258/1083 1058/1243/1061 +f 1051/1236/1062 1065/1250/1082 1066/1251/1084 +f 1059/1244/1063 1073/1258/1083 1074/1259/1085 +f 1051/1236/1062 1067/1252/1072 1052/1237/1064 +f 1060/1245/1065 1074/1259/1085 1075/1260/1086 +f 1072/1257/1081 1086/1271/1087 1087/1272/1088 +f 1064/1249/1080 1080/1265/1089 1065/1250/1082 +f 1072/1257/1081 1088/1273/1090 1073/1258/1083 +f 1066/1251/1084 1080/1265/1089 1081/1266/1091 +f 1074/1259/1085 1088/1273/1090 1089/1274/1092 +f 1066/1251/1084 1082/1267/1093 1067/1252/1072 +f 1074/1259/1085 1090/1275/1094 1075/1260/1086 +f 1068/1253/1073 1082/1267/1093 1083/1268/1095 +f 1075/1260/1086 1091/1276/1096 1076/1261/1074 +f 1069/1254/1075 1083/1268/1095 1084/1269/1097 +f 1062/1247/1076 1046/1207/990 1077/1262/1098 +f 1272/1464/992 1076/1261/1074 1091/1276/1096 +f 1069/1254/1075 1085/1270/1099 1070/1255/1077 +f 1063/1248/1078 1077/1262/1098 1078/1263/1100 +f 1070/1255/1077 1086/1271/1087 1071/1256/1079 +f 1063/1248/1078 1079/1264/1101 1064/1249/1080 +f 1090/1275/1094 1106/1291/1102 1091/1276/1096 +f 1084/1269/1097 1098/1283/1103 1099/1284/1104 +f 1077/1262/1098 1046/1208/990 1092/1277/1105 +f 1272/1465/992 1091/1276/1096 1106/1291/1102 +f 1084/1269/1097 1100/1285/1106 1085/1270/1099 +f 1077/1262/1098 1093/1278/1107 1078/1263/1100 +f 1085/1270/1099 1101/1286/1108 1086/1271/1087 +f 1078/1263/1100 1094/1279/1109 1079/1264/1101 +f 1087/1272/1088 1101/1286/1108 1102/1287/1110 +f 1079/1264/1101 1095/1280/1111 1080/1265/1089 +f 1087/1272/1088 1103/1288/1112 1088/1273/1090 +f 1081/1266/1091 1095/1280/1111 1096/1281/1113 +f 1089/1274/1092 1103/1288/1112 1104/1289/1114 +f 1081/1266/1091 1097/1282/1115 1082/1267/1093 +f 1089/1274/1092 1105/1290/1116 1090/1275/1094 +f 1083/1268/1095 1097/1282/1115 1098/1283/1103 +f 1102/1287/1110 1118/1303/1117 1103/1288/1112 +f 1096/1281/1113 1110/1295/1118 1111/1296/1119 +f 1104/1289/1114 1118/1303/1117 1119/1304/1120 +f 1096/1281/1113 1112/1297/1121 1097/1282/1115 +f 1104/1289/1114 1120/1305/1122 1105/1290/1116 +f 1098/1283/1103 1112/1297/1121 1113/1298/1123 +f 1106/1291/1102 1120/1305/1122 1121/1306/1124 +f 1099/1284/1104 1113/1298/1123 1114/1299/1125 +f 1092/1277/1105 1046/1209/990 1107/1292/1126 +f 1272/1466/992 1106/1291/1102 1121/1306/1124 +f 1099/1284/1104 1115/1300/1127 1100/1285/1106 +f 1092/1277/1105 1108/1293/1128 1093/1278/1107 +f 1100/1285/1106 1116/1301/1129 1101/1286/1108 +f 1094/1279/1109 1108/1293/1128 1109/1294/1130 +f 1102/1287/1110 1116/1301/1129 1117/1302/1131 +f 1094/1279/1109 1110/1295/1118 1095/1280/1111 +f 1107/1292/1126 1046/1210/990 1122/1307/1132 +f 1272/1467/992 1121/1306/1124 1136/1321/1133 +f 1114/1299/1125 1130/1315/1134 1115/1300/1127 +f 1107/1292/1126 1123/1308/1135 1108/1293/1128 +f 1115/1300/1127 1131/1316/1136 1116/1301/1129 +f 1109/1294/1130 1123/1308/1135 1124/1309/1137 +f 1117/1302/1131 1131/1316/1136 1132/1317/1138 +f 1109/1294/1130 1125/1310/1139 1110/1295/1118 +f 1117/1302/1131 1133/1318/1140 1118/1303/1117 +f 1111/1296/1119 1125/1310/1139 1126/1311/1141 +f 1119/1304/1120 1133/1318/1140 1134/1319/1142 +f 1111/1296/1119 1127/1312/1143 1112/1297/1121 +f 1119/1304/1120 1135/1320/1144 1120/1305/1122 +f 1113/1298/1123 1127/1312/1143 1128/1313/1145 +f 1120/1305/1122 1136/1321/1133 1121/1306/1124 +f 1114/1299/1125 1128/1313/1145 1129/1314/1146 +f 1126/1311/1141 1140/1325/1147 1141/1326/1148 +f 1133/1318/1140 1149/1334/1149 1134/1319/1142 +f 1126/1311/1141 1142/1327/1150 1127/1312/1143 +f 1135/1320/1144 1149/1334/1149 1150/1335/1151 +f 1128/1313/1145 1142/1327/1150 1143/1328/1152 +f 1136/1321/1133 1150/1335/1151 1151/1336/1153 +f 1129/1314/1146 1143/1328/1152 1144/1329/1154 +f 1122/1307/1132 1046/1211/990 1137/1322/1155 +f 1272/1468/992 1136/1321/1133 1151/1336/1153 +f 1129/1314/1146 1145/1330/1156 1130/1315/1134 +f 1122/1307/1132 1138/1323/1157 1123/1308/1135 +f 1130/1315/1134 1146/1331/1158 1131/1316/1136 +f 1123/1308/1135 1139/1324/1159 1124/1309/1137 +f 1132/1317/1138 1146/1331/1158 1147/1332/1160 +f 1124/1309/1137 1140/1325/1147 1125/1310/1139 +f 1132/1317/1138 1148/1333/1161 1133/1318/1140 +f 1144/1329/1154 1160/1345/1162 1145/1330/1156 +f 1138/1323/1157 1152/1337/1163 1153/1338/1164 +f 1145/1330/1156 1161/1346/1165 1146/1331/1158 +f 1139/1324/1159 1153/1338/1164 1154/1339/1166 +f 1147/1332/1160 1161/1346/1165 1162/1347/1167 +f 1139/1324/1159 1155/1340/1168 1140/1325/1147 +f 1147/1332/1160 1163/1348/1169 1148/1333/1161 +f 1141/1326/1148 1155/1340/1168 1156/1341/1170 +f 1149/1334/1149 1163/1348/1169 1164/1349/1171 +f 1141/1326/1148 1157/1342/1172 1142/1327/1150 +f 1149/1334/1149 1165/1350/1173 1150/1335/1151 +f 1143/1328/1152 1157/1342/1172 1158/1343/1174 +f 1150/1335/1151 1166/1351/1175 1151/1336/1153 +f 1144/1329/1154 1158/1343/1174 1159/1344/1176 +f 1137/1322/1155 1046/1212/990 1152/1337/1163 +f 1272/1469/992 1151/1336/1153 1166/1351/1175 +f 1164/1349/1171 1178/1363/1177 1179/1364/1178 +f 1156/1341/1170 1172/1357/1179 1157/1342/1172 +f 1164/1349/1171 1180/1365/1180 1165/1350/1173 +f 1158/1343/1174 1172/1357/1179 1173/1358/1181 +f 1165/1350/1173 1181/1366/1182 1166/1351/1175 +f 1159/1344/1176 1173/1358/1181 1174/1359/1183 +f 1152/1337/1163 1046/1213/990 1167/1352/1184 +f 1272/1470/992 1166/1351/1175 1181/1366/1182 +f 1159/1344/1176 1175/1360/1185 1160/1345/1162 +f 1152/1337/1163 1168/1353/1186 1153/1338/1164 +f 1160/1345/1162 1176/1361/1187 1161/1346/1165 +f 1153/1338/1164 1169/1354/1188 1154/1339/1166 +f 1162/1347/1167 1176/1361/1187 1177/1362/1189 +f 1154/1339/1166 1170/1355/1190 1155/1340/1168 +f 1162/1347/1167 1178/1363/1177 1163/1348/1169 +f 1156/1341/1170 1170/1355/1190 1171/1356/1191 +f 1167/1352/1184 1183/1368/1192 1168/1353/1186 +f 1175/1360/1185 1191/1376/1193 1176/1361/1187 +f 1168/1353/1186 1184/1369/1194 1169/1354/1188 +f 1177/1362/1189 1191/1376/1193 1192/1377/1195 +f 1169/1354/1188 1185/1370/1196 1170/1355/1190 +f 1177/1362/1189 1193/1378/1197 1178/1363/1177 +f 1171/1356/1191 1185/1370/1196 1186/1371/1198 +f 1179/1364/1178 1193/1378/1197 1194/1379/1199 +f 1171/1356/1191 1187/1372/1200 1172/1357/1179 +f 1179/1364/1178 1195/1380/1201 1180/1365/1180 +f 1173/1358/1181 1187/1372/1200 1188/1373/1202 +f 1180/1365/1180 1196/1381/1203 1181/1366/1182 +f 1174/1359/1183 1188/1373/1202 1189/1374/1204 +f 1167/1352/1184 1046/1214/990 1182/1367/1205 +f 1272/1471/992 1181/1366/1182 1196/1381/1203 +f 1174/1359/1183 1190/1375/1206 1175/1360/1185 +f 1186/1371/1198 1202/1387/1207 1187/1372/1200 +f 1194/1379/1199 1210/1395/1208 1195/1380/1201 +f 1188/1373/1202 1202/1387/1207 1203/1388/1209 +f 1196/1381/1203 1210/1395/1208 1211/1396/1210 +f 1189/1374/1204 1203/1388/1209 1204/1389/1211 +f 1182/1367/1205 1046/1215/990 1197/1382/1212 +f 1272/1472/992 1196/1381/1203 1211/1396/1210 +f 1189/1374/1204 1205/1390/1213 1190/1375/1206 +f 1182/1367/1205 1198/1383/1214 1183/1368/1192 +f 1190/1375/1206 1206/1391/1215 1191/1376/1193 +f 1184/1369/1194 1198/1383/1214 1199/1384/1216 +f 1192/1377/1195 1206/1391/1215 1207/1392/1217 +f 1184/1369/1194 1200/1385/1218 1185/1370/1196 +f 1192/1377/1195 1208/1393/1219 1193/1378/1197 +f 1186/1371/1198 1200/1385/1218 1201/1386/1220 +f 1194/1379/1199 1208/1393/1219 1209/1394/1221 +f 1205/1390/1213 1221/1406/1222 1206/1391/1215 +f 1198/1383/1214 1214/1399/1223 1199/1384/1216 +f 1207/1392/1217 1221/1406/1222 1222/1407/1224 +f 1199/1384/1216 1215/1400/1225 1200/1385/1218 +f 1207/1392/1217 1223/1408/1226 1208/1393/1219 +f 1201/1386/1220 1215/1400/1225 1216/1401/1227 +f 1209/1394/1221 1223/1408/1226 1224/1409/1228 +f 1201/1386/1220 1217/1402/1229 1202/1387/1207 +f 1209/1394/1221 1225/1410/1230 1210/1395/1208 +f 1203/1388/1209 1217/1402/1229 1218/1403/1231 +f 1210/1395/1208 1226/1411/1232 1211/1396/1210 +f 1204/1389/1211 1218/1403/1231 1219/1404/1233 +f 1197/1382/1212 1046/1216/990 1212/1397/1234 +f 1272/1473/992 1211/1396/1210 1226/1411/1232 +f 1204/1389/1211 1220/1405/1235 1205/1390/1213 +f 1197/1382/1212 1213/1398/1236 1198/1383/1214 +f 1224/1409/1228 1240/1425/1237 1225/1410/1230 +f 1218/1403/1231 1232/1417/1238 1233/1418/1239 +f 1225/1410/1230 1241/1426/1240 1226/1411/1232 +f 1219/1404/1233 1233/1418/1239 1234/1419/1241 +f 1212/1397/1234 1046/1217/990 1227/1412/1242 +f 1272/1474/992 1226/1411/1232 1241/1426/1240 +f 1219/1404/1233 1235/1420/1243 1220/1405/1235 +f 1213/1398/1236 1227/1412/1242 1228/1413/1244 +f 1220/1405/1235 1236/1421/1245 1221/1406/1222 +f 1213/1398/1236 1229/1414/1246 1214/1399/1223 +f 1222/1407/1224 1236/1421/1245 1237/1422/1247 +f 1214/1399/1223 1230/1415/1248 1215/1400/1225 +f 1222/1407/1224 1238/1423/1249 1223/1408/1226 +f 1216/1401/1227 1230/1415/1248 1231/1416/1250 +f 1224/1409/1228 1238/1423/1249 1239/1424/1251 +f 1216/1401/1227 1232/1417/1238 1217/1402/1229 +f 1237/1422/1247 1251/1436/1252 1252/1437/1253 +f 1229/1414/1246 1245/1430/1254 1230/1415/1248 +f 1237/1422/1247 1253/1438/1255 1238/1423/1249 +f 1231/1416/1250 1245/1430/1254 1246/1431/1256 +f 1239/1424/1251 1253/1438/1255 1254/1439/1257 +f 1231/1416/1250 1247/1432/1258 1232/1417/1238 +f 1239/1424/1251 1255/1440/1259 1240/1425/1237 +f 1233/1418/1239 1247/1432/1258 1248/1433/1260 +f 1241/1426/1240 1255/1440/1259 1256/1441/1261 +f 1234/1419/1241 1248/1433/1260 1249/1434/1262 +f 1227/1412/1242 1046/1218/990 1242/1427/1263 +f 1272/1475/992 1241/1426/1240 1256/1441/1261 +f 1234/1419/1241 1250/1435/1264 1235/1420/1243 +f 1228/1413/1244 1242/1427/1263 1243/1428/1265 +f 1235/1420/1243 1251/1436/1252 1236/1421/1245 +f 1228/1413/1244 1244/1429/1266 1229/1414/1246 +f 1256/1441/1261 1270/1455/1267 1271/1456/1268 +f 1249/1434/1262 1263/1448/1269 1264/1449/1270 +f 1242/1427/1263 1046/1219/990 1257/1442/1271 +f 1272/1476/992 1256/1441/1261 1271/1456/1268 +f 1249/1434/1262 1265/1450/1272 1250/1435/1264 +f 1242/1427/1263 1258/1443/1273 1243/1428/1265 +f 1250/1435/1264 1266/1451/1274 1251/1436/1252 +f 1243/1428/1265 1259/1444/1275 1244/1429/1266 +f 1252/1437/1253 1266/1451/1274 1267/1452/1276 +f 1244/1429/1266 1260/1445/1277 1245/1430/1254 +f 1252/1437/1253 1268/1453/1278 1253/1438/1255 +f 1246/1431/1256 1260/1445/1277 1261/1446/1279 +f 1254/1439/1257 1268/1453/1278 1269/1454/1280 +f 1246/1431/1256 1262/1447/1281 1247/1432/1258 +f 1254/1439/1257 1270/1455/1267 1255/1440/1259 +f 1248/1433/1260 1262/1447/1281 1263/1448/1269 +f 1259/1444/1275 1276/1492/1282 1260/1445/1277 +f 1267/1452/1276 1284/1500/1283 1268/1453/1278 +f 1261/1446/1279 1276/1492/1282 1277/1493/1284 +f 1269/1454/1280 1284/1500/1283 1285/1501/1285 +f 1261/1446/1279 1278/1494/1286 1262/1447/1281 +f 1269/1454/1280 1286/1502/1287 1270/1455/1267 +f 1263/1448/1269 1278/1494/1286 1279/1495/1288 +f 1270/1455/1267 1287/1503/1289 1271/1456/1268 +f 1264/1449/1270 1279/1495/1288 1280/1496/1290 +f 1257/1442/1271 1046/1220/990 1273/1489/1291 +f 1272/1477/992 1271/1456/1268 1287/1503/1289 +f 1264/1449/1270 1281/1497/1292 1265/1450/1272 +f 1257/1442/1271 1274/1490/1293 1258/1443/1273 +f 1265/1450/1272 1282/1498/1294 1266/1451/1274 +f 1258/1443/1273 1275/1491/1295 1259/1444/1275 +f 1267/1452/1276 1282/1498/1294 1283/1499/1296 +f 1280/1496/1290 1294/1510/1297 1295/1511/1298 +f 1273/1489/1291 1046/1221/990 1288/1504/1299 +f 1272/1478/992 1287/1503/1289 1302/1518/1300 +f 1280/1496/1290 1296/1512/1301 1281/1497/1292 +f 1273/1489/1291 1289/1505/1302 1274/1490/1293 +f 1281/1497/1292 1297/1513/1303 1282/1498/1294 +f 1274/1490/1293 1290/1506/1304 1275/1491/1295 +f 1283/1499/1296 1297/1513/1303 1298/1514/1305 +f 1275/1491/1295 1291/1507/1306 1276/1492/1282 +f 1283/1499/1296 1299/1515/1307 1284/1500/1283 +f 1276/1492/1282 1292/1508/1308 1277/1493/1284 +f 1285/1501/1285 1299/1515/1307 1300/1516/1309 +f 1277/1493/1284 1293/1509/1310 1278/1494/1286 +f 1285/1501/1285 1301/1517/1311 1286/1502/1287 +f 1279/1495/1288 1293/1509/1310 1294/1510/1297 +f 1286/1502/1287 1302/1518/1300 1287/1503/1289 +f 1298/1514/1305 1314/1530/1312 1299/1515/1307 +f 1292/1508/1308 1306/1522/1313 1307/1523/1314 +f 1300/1516/1309 1314/1530/1312 1315/1531/1315 +f 1292/1508/1308 1308/1524/1316 1293/1509/1310 +f 1300/1516/1309 1316/1532/1317 1301/1517/1311 +f 1294/1510/1297 1308/1524/1316 1309/1525/1318 +f 1301/1517/1311 1317/1533/1319 1302/1518/1300 +f 1295/1511/1298 1309/1525/1318 1310/1526/1320 +f 1288/1504/1299 1046/1222/990 1303/1519/1321 +f 1272/1479/992 1302/1518/1300 1317/1533/1319 +f 1295/1511/1298 1311/1527/1322 1296/1512/1301 +f 1288/1504/1299 1304/1520/1323 1289/1505/1302 +f 1296/1512/1301 1312/1528/1324 1297/1513/1303 +f 1289/1505/1302 1305/1521/1325 1290/1506/1304 +f 1298/1514/1305 1312/1528/1324 1313/1529/1326 +f 1290/1506/1304 1306/1522/1313 1291/1507/1306 +f 1303/1519/1321 1046/1223/990 1318/1534/1327 +f 1272/1480/992 1317/1533/1319 1332/1563/1328 +f 1310/1526/1320 1326/1550/1329 1311/1527/1322 +f 1303/1519/1321 1319/1536/1330 1304/1520/1323 +f 1311/1527/1322 1327/1552/1331 1312/1528/1324 +f 1305/1521/1325 1319/1536/1330 1320/1538/1332 +f 1313/1529/1326 1327/1552/1331 1328/1554/1333 +f 1305/1521/1325 1321/1540/1334 1306/1522/1313 +f 1313/1529/1326 1329/1556/1335 1314/1530/1312 +f 1307/1523/1314 1321/1540/1334 1322/1542/1336 +f 1315/1531/1315 1329/1556/1335 1330/1558/1337 +f 1307/1523/1314 1323/1544/1338 1308/1524/1316 +f 1315/1531/1315 1331/1560/1339 1316/1532/1317 +f 1309/1525/1318 1323/1544/1338 1324/1546/1340 +f 1316/1532/1317 1332/1563/1328 1317/1533/1319 +f 1310/1526/1320 1324/1546/1340 1325/1548/1341 +f 1322/1543/1336 1336/1567/1342 1337/1568/1343 +f 1330/1559/1337 1344/1575/1344 1345/1576/1345 +f 1322/1543/1336 1338/1569/1346 1323/1545/1338 +f 1330/1559/1337 1346/1577/1347 1331/1561/1339 +f 1324/1547/1340 1338/1569/1346 1339/1570/1348 +f 1331/1561/1339 1347/1578/1349 1332/1562/1328 +f 1325/1549/1341 1339/1570/1348 1340/1571/1350 +f 1318/1535/1327 1046/1224/990 1333/1564/1351 +f 1272/1481/992 1332/1562/1328 1347/1578/1349 +f 1325/1549/1341 1341/1572/1352 1326/1551/1329 +f 1318/1535/1327 1334/1565/1353 1319/1537/1330 +f 1326/1551/1329 1342/1573/1354 1327/1553/1331 +f 1320/1539/1332 1334/1565/1353 1335/1566/1355 +f 1328/1555/1333 1342/1573/1354 1343/1574/1356 +f 1320/1539/1332 1336/1567/1342 1321/1541/1334 +f 1328/1555/1333 1344/1575/1344 1329/1557/1335 +f 1340/1571/1350 1356/1587/1357 1341/1572/1352 +f 1333/1564/1351 1349/1580/1358 1334/1565/1353 +f 1341/1572/1352 1357/1588/1359 1342/1573/1354 +f 1335/1566/1355 1349/1580/1358 1350/1581/1360 +f 1343/1574/1356 1357/1588/1359 1358/1589/1361 +f 1335/1566/1355 1351/1582/1362 1336/1567/1342 +f 1343/1574/1356 1359/1590/1363 1344/1575/1344 +f 1337/1568/1343 1351/1582/1362 1352/1583/1364 +f 1345/1576/1345 1359/1590/1363 1360/1591/1365 +f 1337/1568/1343 1353/1584/1366 1338/1569/1346 +f 1345/1576/1345 1361/1592/1367 1346/1577/1347 +f 1339/1570/1348 1353/1584/1366 1354/1585/1368 +f 1346/1577/1347 1362/1593/1369 1347/1578/1349 +f 1340/1571/1350 1354/1585/1368 1355/1586/1370 +f 1333/1564/1351 1046/1225/990 1348/1579/1371 +f 1272/1482/992 1347/1578/1349 1362/1593/1369 +f 1360/1591/1365 1374/1605/1372 1375/1606/1373 +f 1352/1583/1364 1368/1599/1374 1353/1584/1366 +f 1361/1592/1367 1375/1606/1373 1376/1607/1375 +f 1354/1585/1368 1368/1599/1374 1369/1600/1376 +f 1361/1592/1367 1377/1608/1377 1362/1593/1369 +f 1355/1586/1370 1369/1600/1376 1370/1601/1378 +f 1348/1579/1371 1046/1226/990 1363/1594/1379 +f 1272/1483/992 1362/1593/1369 1377/1608/1377 +f 1355/1586/1370 1371/1602/1380 1356/1587/1357 +f 1349/1580/1358 1363/1594/1379 1364/1595/1381 +f 1356/1587/1357 1372/1603/1382 1357/1588/1359 +f 1349/1580/1358 1365/1596/1383 1350/1581/1360 +f 1358/1589/1361 1372/1603/1382 1373/1604/1384 +f 1350/1581/1360 1366/1597/1385 1351/1582/1362 +f 1358/1589/1361 1374/1605/1372 1359/1590/1363 +f 1352/1583/1364 1366/1597/1385 1367/1598/1386 +f 1371/1602/1380 1387/1618/1387 1372/1603/1382 +f 1365/1596/1383 1379/1610/1388 1380/1611/1389 +f 1373/1604/1384 1387/1618/1387 1388/1619/1390 +f 1365/1596/1383 1381/1612/1391 1366/1597/1385 +f 1373/1604/1384 1389/1620/1392 1374/1605/1372 +f 1367/1598/1386 1381/1612/1391 1382/1613/1393 +f 1375/1606/1373 1389/1620/1392 1390/1621/1394 +f 1367/1598/1386 1383/1614/1395 1368/1599/1374 +f 1375/1606/1373 1391/1622/1396 1376/1607/1375 +f 1369/1600/1376 1383/1614/1395 1384/1615/1397 +f 1376/1607/1375 1392/1623/1398 1377/1608/1377 +f 1370/1601/1378 1384/1615/1397 1385/1616/1399 +f 1363/1594/1379 1046/1227/990 1378/1609/1400 +f 1272/1484/992 1377/1608/1377 1392/1623/1398 +f 1370/1601/1378 1386/1617/1401 1371/1602/1380 +f 1363/1594/1379 1379/1610/1388 1364/1595/1381 +f 1390/1621/1394 1406/1637/1402 1391/1622/1396 +f 1384/1615/1397 1398/1629/1403 1399/1630/1404 +f 1391/1622/1396 1407/1638/1405 1392/1623/1398 +f 1385/1616/1399 1399/1630/1404 1400/1631/1406 +f 1378/1609/1400 1046/1228/990 1393/1624/1407 +f 1272/1485/992 1392/1623/1398 1407/1638/1405 +f 1385/1616/1399 1401/1632/1408 1386/1617/1401 +f 1378/1609/1400 1394/1625/1409 1379/1610/1388 +f 1386/1617/1401 1402/1633/1410 1387/1618/1387 +f 1380/1611/1389 1394/1625/1409 1395/1626/1411 +f 1388/1619/1390 1402/1633/1410 1403/1634/1412 +f 1380/1611/1389 1396/1627/1413 1381/1612/1391 +f 1388/1619/1390 1404/1635/1414 1389/1620/1392 +f 1382/1613/1393 1396/1627/1413 1397/1628/1415 +f 1390/1621/1394 1404/1635/1414 1405/1636/1416 +f 1382/1613/1393 1398/1629/1403 1383/1614/1395 +f 1394/1625/1409 1410/1641/1417 1395/1626/1411 +f 1403/1634/1412 1417/1648/1418 1418/1649/1419 +f 1395/1626/1411 1411/1642/1420 1396/1627/1413 +f 1403/1634/1412 1419/1650/1421 1404/1635/1414 +f 1397/1628/1415 1411/1642/1420 1412/1643/1422 +f 1405/1636/1416 1419/1650/1421 1420/1651/1423 +f 1397/1628/1415 1413/1644/1424 1398/1629/1403 +f 1405/1636/1416 1421/1652/1425 1406/1637/1402 +f 1399/1630/1404 1413/1644/1424 1414/1645/1426 +f 1406/1637/1402 1422/1653/1427 1407/1638/1405 +f 1400/1631/1406 1414/1645/1426 1415/1646/1428 +f 1393/1624/1407 1046/1229/990 1408/1639/1429 +f 1272/1486/992 1407/1638/1405 1422/1653/1427 +f 1400/1631/1406 1416/1647/1430 1401/1632/1408 +f 1394/1625/1409 1408/1639/1429 1409/1640/1431 +f 1401/1632/1408 1417/1648/1418 1402/1633/1410 +f 1414/1645/1426 1428/1659/1432 1429/1660/1433 +f 1422/1653/1427 1436/1667/1434 1437/1668/1435 +f 1415/1646/1428 1429/1660/1433 1430/1661/1436 +f 1408/1639/1429 1046/1230/990 1423/1654/1437 +f 1272/1487/992 1422/1653/1427 1437/1668/1435 +f 1415/1646/1428 1431/1662/1438 1416/1647/1430 +f 1408/1639/1429 1424/1655/1439 1409/1640/1431 +f 1416/1647/1430 1432/1663/1440 1417/1648/1418 +f 1410/1641/1417 1424/1655/1439 1425/1656/1441 +f 1418/1649/1419 1432/1663/1440 1433/1664/1442 +f 1410/1641/1417 1426/1657/1443 1411/1642/1420 +f 1418/1649/1419 1434/1665/1444 1419/1650/1421 +f 1412/1643/1422 1426/1657/1443 1427/1658/1445 +f 1420/1651/1423 1434/1665/1444 1435/1666/1446 +f 1412/1643/1422 1428/1659/1432 1413/1644/1424 +f 1420/1651/1423 1436/1667/1434 1421/1652/1425 +f 1432/1663/1440 970/1124/970 1433/1664/1442 +f 1426/1657/1443 965/1119/965 1440/1671/971 +f 1433/1664/1442 1443/1674/974 1434/1665/1444 +f 1427/1658/1445 1440/1671/971 966/1120/975 +f 1435/1666/1446 1443/1674/974 1444/1675/978 +f 1428/1659/1432 966/1120/975 1441/1672/979 +f 1435/1666/1446 1445/1676/982 1436/1667/1434 +f 1429/1660/1433 1441/1672/979 967/1121/983 +f 1436/1667/1434 1446/1677/986 1437/1668/1435 +f 1430/1661/1436 967/1121/983 968/1122/988 +f 1423/1654/1437 1046/1231/990 1438/1669/989 +f 1272/1488/992 1437/1668/1435 1446/1677/986 +f 1430/1661/1436 969/1123/994 1431/1662/1438 +f 1424/1655/1439 1438/1669/989 1439/1670/995 +f 1431/1662/1438 1442/1673/968 1432/1663/1440 +f 1425/1656/1441 1439/1670/995 965/1119/965 +f 965/1119/965 1439/1670/995 972/1126/966 +f 1442/1673/968 980/1134/996 981/1135/969 +f 1440/1671/971 965/1119/965 973/1127/967 +f 970/1124/970 981/1135/969 982/1136/973 +f 966/1120/975 1440/1671/971 974/1128/972 +f 1443/1674/974 982/1136/973 983/1137/977 +f 1441/1672/979 966/1120/975 975/1129/976 +f 1444/1675/978 983/1137/977 984/1138/981 +f 967/1121/983 1441/1672/979 976/1130/980 +f 1445/1676/982 984/1138/981 985/1139/985 +f 967/1121/983 977/1131/984 978/1132/987 +f 968/1122/988 978/1132/987 979/1133/993 +f 1439/1670/995 1438/1669/989 971/1125/991 +f 969/1123/994 979/1133/993 980/1134/996 +f 971/1125/991 986/1140/1010 987/1141/997 +f 979/1133/993 994/1148/1011 995/1149/998 +f 972/1126/966 987/1141/997 988/1142/999 +f 980/1134/996 995/1149/998 996/1150/1000 +f 973/1127/967 988/1142/999 989/1143/1001 +f 981/1135/969 996/1150/1000 997/1151/1002 +f 974/1128/972 989/1143/1001 990/1144/1003 +f 983/1137/977 982/1136/973 997/1151/1002 +f 975/1129/976 990/1144/1003 991/1145/1005 +f 984/1138/981 983/1137/977 998/1152/1004 +f 977/1131/984 976/1130/980 991/1145/1005 +f 984/1138/981 999/1153/1006 1000/1154/1008 +f 977/1131/984 992/1146/1007 993/1147/1009 +f 978/1132/987 993/1147/1009 994/1148/1011 +f 990/1144/1003 1005/1159/1025 1006/1160/1012 +f 998/1152/1004 1013/1167/1026 1014/1168/1013 +f 991/1145/1005 1006/1160/1012 1007/1161/1014 +f 999/1153/1006 1014/1168/1013 1015/1169/1015 +f 992/1146/1007 1007/1161/1014 1008/1162/1016 +f 994/1148/1011 993/1147/1009 1008/1162/1016 +f 986/1140/1010 1001/1155/1017 1002/1156/1019 +f 994/1148/1011 1009/1163/1018 1010/1164/1020 +f 987/1141/997 1002/1156/1019 1003/1157/1021 +f 996/1150/1000 995/1149/998 1010/1164/1020 +f 988/1142/999 1003/1157/1021 1004/1158/1023 +f 997/1151/1002 996/1150/1000 1011/1165/1022 +f 989/1143/1001 1004/1158/1023 1005/1159/1025 +f 998/1152/1004 997/1151/1002 1012/1166/1024 +f 1009/1163/1018 1024/1178/1040 1025/1179/1027 +f 1002/1156/1019 1017/1171/1041 1018/1172/1028 +f 1011/1165/1022 1010/1164/1020 1025/1179/1027 +f 1003/1157/1021 1018/1172/1028 1019/1173/1030 +f 1011/1165/1022 1026/1180/1029 1027/1181/1031 +f 1004/1158/1023 1019/1173/1030 1020/1174/1032 +f 1012/1166/1024 1027/1181/1031 1028/1182/1033 +f 1005/1159/1025 1020/1174/1032 1021/1175/1034 +f 1013/1167/1026 1028/1182/1033 1029/1183/1035 +f 1007/1161/1014 1006/1160/1012 1021/1175/1034 +f 1014/1168/1013 1029/1183/1035 1030/1184/1037 +f 1008/1162/1016 1007/1161/1014 1022/1176/1036 +f 1008/1162/1016 1023/1177/1038 1024/1178/1040 +f 1001/1155/1017 1016/1170/1039 1017/1171/1041 +f 1029/1183/1035 1028/1182/1033 1043/1197/1042 +f 1022/1176/1036 1021/1175/1034 1036/1190/1044 +f 1029/1183/1035 1044/1198/1043 1045/1199/1046 +f 1023/1177/1038 1022/1176/1036 1037/1191/1045 +f 1023/1177/1038 1038/1192/1047 1039/1193/1049 +f 1016/1170/1039 1031/1185/1048 1032/1186/1050 +f 1024/1178/1040 1039/1193/1049 1040/1194/1051 +f 1017/1171/1041 1032/1186/1050 1033/1187/1052 +f 1026/1180/1029 1025/1179/1027 1040/1194/1051 +f 1018/1172/1028 1033/1187/1052 1034/1188/1054 +f 1026/1180/1029 1041/1195/1053 1042/1196/1055 +f 1020/1174/1032 1019/1173/1030 1034/1188/1054 +f 1027/1181/1031 1042/1196/1055 1043/1197/1042 +f 1020/1174/1032 1035/1189/1056 1036/1190/1044 +f 1032/1186/1050 1048/1233/1071 1049/1234/1057 +f 1041/1195/1053 1040/1194/1051 1056/1241/1058 +f 1033/1187/1052 1049/1234/1057 1050/1235/1060 +f 1041/1195/1053 1057/1242/1059 1058/1243/1061 +f 1035/1189/1056 1034/1188/1054 1050/1235/1060 +f 1042/1196/1055 1058/1243/1061 1059/1244/1063 +f 1035/1189/1056 1051/1236/1062 1052/1237/1064 +f 1043/1197/1042 1059/1244/1063 1060/1245/1065 +f 1037/1191/1045 1036/1190/1044 1052/1237/1064 +f 1045/1199/1046 1044/1198/1043 1060/1245/1065 +f 1038/1192/1047 1037/1191/1045 1053/1238/1066 +f 1038/1192/1047 1054/1239/1068 1055/1240/1070 +f 1031/1185/1048 1047/1232/1069 1048/1233/1071 +f 1039/1193/1049 1055/1240/1070 1056/1241/1058 +f 1053/1238/1066 1052/1237/1064 1067/1252/1072 +f 1060/1245/1065 1075/1260/1086 1076/1261/1074 +f 1054/1239/1068 1053/1238/1066 1068/1253/1073 +f 1054/1239/1068 1069/1254/1075 1070/1255/1077 +f 1048/1233/1071 1047/1232/1069 1062/1247/1076 +f 1055/1240/1070 1070/1255/1077 1071/1256/1079 +f 1048/1233/1071 1063/1248/1078 1064/1249/1080 +f 1057/1242/1059 1056/1241/1058 1071/1256/1079 +f 1049/1234/1057 1064/1249/1080 1065/1250/1082 +f 1057/1242/1059 1072/1257/1081 1073/1258/1083 +f 1051/1236/1062 1050/1235/1060 1065/1250/1082 +f 1059/1244/1063 1058/1243/1061 1073/1258/1083 +f 1051/1236/1062 1066/1251/1084 1067/1252/1072 +f 1060/1245/1065 1059/1244/1063 1074/1259/1085 +f 1072/1257/1081 1071/1256/1079 1086/1271/1087 +f 1064/1249/1080 1079/1264/1101 1080/1265/1089 +f 1072/1257/1081 1087/1272/1088 1088/1273/1090 +f 1066/1251/1084 1065/1250/1082 1080/1265/1089 +f 1074/1259/1085 1073/1258/1083 1088/1273/1090 +f 1066/1251/1084 1081/1266/1091 1082/1267/1093 +f 1074/1259/1085 1089/1274/1092 1090/1275/1094 +f 1068/1253/1073 1067/1252/1072 1082/1267/1093 +f 1075/1260/1086 1090/1275/1094 1091/1276/1096 +f 1069/1254/1075 1068/1253/1073 1083/1268/1095 +f 1069/1254/1075 1084/1269/1097 1085/1270/1099 +f 1063/1248/1078 1062/1247/1076 1077/1262/1098 +f 1070/1255/1077 1085/1270/1099 1086/1271/1087 +f 1063/1248/1078 1078/1263/1100 1079/1264/1101 +f 1090/1275/1094 1105/1290/1116 1106/1291/1102 +f 1084/1269/1097 1083/1268/1095 1098/1283/1103 +f 1084/1269/1097 1099/1284/1104 1100/1285/1106 +f 1077/1262/1098 1092/1277/1105 1093/1278/1107 +f 1085/1270/1099 1100/1285/1106 1101/1286/1108 +f 1078/1263/1100 1093/1278/1107 1094/1279/1109 +f 1087/1272/1088 1086/1271/1087 1101/1286/1108 +f 1079/1264/1101 1094/1279/1109 1095/1280/1111 +f 1087/1272/1088 1102/1287/1110 1103/1288/1112 +f 1081/1266/1091 1080/1265/1089 1095/1280/1111 +f 1089/1274/1092 1088/1273/1090 1103/1288/1112 +f 1081/1266/1091 1096/1281/1113 1097/1282/1115 +f 1089/1274/1092 1104/1289/1114 1105/1290/1116 +f 1083/1268/1095 1082/1267/1093 1097/1282/1115 +f 1102/1287/1110 1117/1302/1131 1118/1303/1117 +f 1096/1281/1113 1095/1280/1111 1110/1295/1118 +f 1104/1289/1114 1103/1288/1112 1118/1303/1117 +f 1096/1281/1113 1111/1296/1119 1112/1297/1121 +f 1104/1289/1114 1119/1304/1120 1120/1305/1122 +f 1098/1283/1103 1097/1282/1115 1112/1297/1121 +f 1106/1291/1102 1105/1290/1116 1120/1305/1122 +f 1099/1284/1104 1098/1283/1103 1113/1298/1123 +f 1099/1284/1104 1114/1299/1125 1115/1300/1127 +f 1092/1277/1105 1107/1292/1126 1108/1293/1128 +f 1100/1285/1106 1115/1300/1127 1116/1301/1129 +f 1094/1279/1109 1093/1278/1107 1108/1293/1128 +f 1102/1287/1110 1101/1286/1108 1116/1301/1129 +f 1094/1279/1109 1109/1294/1130 1110/1295/1118 +f 1114/1299/1125 1129/1314/1146 1130/1315/1134 +f 1107/1292/1126 1122/1307/1132 1123/1308/1135 +f 1115/1300/1127 1130/1315/1134 1131/1316/1136 +f 1109/1294/1130 1108/1293/1128 1123/1308/1135 +f 1117/1302/1131 1116/1301/1129 1131/1316/1136 +f 1109/1294/1130 1124/1309/1137 1125/1310/1139 +f 1117/1302/1131 1132/1317/1138 1133/1318/1140 +f 1111/1296/1119 1110/1295/1118 1125/1310/1139 +f 1119/1304/1120 1118/1303/1117 1133/1318/1140 +f 1111/1296/1119 1126/1311/1141 1127/1312/1143 +f 1119/1304/1120 1134/1319/1142 1135/1320/1144 +f 1113/1298/1123 1112/1297/1121 1127/1312/1143 +f 1120/1305/1122 1135/1320/1144 1136/1321/1133 +f 1114/1299/1125 1113/1298/1123 1128/1313/1145 +f 1126/1311/1141 1125/1310/1139 1140/1325/1147 +f 1133/1318/1140 1148/1333/1161 1149/1334/1149 +f 1126/1311/1141 1141/1326/1148 1142/1327/1150 +f 1135/1320/1144 1134/1319/1142 1149/1334/1149 +f 1128/1313/1145 1127/1312/1143 1142/1327/1150 +f 1136/1321/1133 1135/1320/1144 1150/1335/1151 +f 1129/1314/1146 1128/1313/1145 1143/1328/1152 +f 1129/1314/1146 1144/1329/1154 1145/1330/1156 +f 1122/1307/1132 1137/1322/1155 1138/1323/1157 +f 1130/1315/1134 1145/1330/1156 1146/1331/1158 +f 1123/1308/1135 1138/1323/1157 1139/1324/1159 +f 1132/1317/1138 1131/1316/1136 1146/1331/1158 +f 1124/1309/1137 1139/1324/1159 1140/1325/1147 +f 1132/1317/1138 1147/1332/1160 1148/1333/1161 +f 1144/1329/1154 1159/1344/1176 1160/1345/1162 +f 1138/1323/1157 1137/1322/1155 1152/1337/1163 +f 1145/1330/1156 1160/1345/1162 1161/1346/1165 +f 1139/1324/1159 1138/1323/1157 1153/1338/1164 +f 1147/1332/1160 1146/1331/1158 1161/1346/1165 +f 1139/1324/1159 1154/1339/1166 1155/1340/1168 +f 1147/1332/1160 1162/1347/1167 1163/1348/1169 +f 1141/1326/1148 1140/1325/1147 1155/1340/1168 +f 1149/1334/1149 1148/1333/1161 1163/1348/1169 +f 1141/1326/1148 1156/1341/1170 1157/1342/1172 +f 1149/1334/1149 1164/1349/1171 1165/1350/1173 +f 1143/1328/1152 1142/1327/1150 1157/1342/1172 +f 1150/1335/1151 1165/1350/1173 1166/1351/1175 +f 1144/1329/1154 1143/1328/1152 1158/1343/1174 +f 1164/1349/1171 1163/1348/1169 1178/1363/1177 +f 1156/1341/1170 1171/1356/1191 1172/1357/1179 +f 1164/1349/1171 1179/1364/1178 1180/1365/1180 +f 1158/1343/1174 1157/1342/1172 1172/1357/1179 +f 1165/1350/1173 1180/1365/1180 1181/1366/1182 +f 1159/1344/1176 1158/1343/1174 1173/1358/1181 +f 1159/1344/1176 1174/1359/1183 1175/1360/1185 +f 1152/1337/1163 1167/1352/1184 1168/1353/1186 +f 1160/1345/1162 1175/1360/1185 1176/1361/1187 +f 1153/1338/1164 1168/1353/1186 1169/1354/1188 +f 1162/1347/1167 1161/1346/1165 1176/1361/1187 +f 1154/1339/1166 1169/1354/1188 1170/1355/1190 +f 1162/1347/1167 1177/1362/1189 1178/1363/1177 +f 1156/1341/1170 1155/1340/1168 1170/1355/1190 +f 1167/1352/1184 1182/1367/1205 1183/1368/1192 +f 1175/1360/1185 1190/1375/1206 1191/1376/1193 +f 1168/1353/1186 1183/1368/1192 1184/1369/1194 +f 1177/1362/1189 1176/1361/1187 1191/1376/1193 +f 1169/1354/1188 1184/1369/1194 1185/1370/1196 +f 1177/1362/1189 1192/1377/1195 1193/1378/1197 +f 1171/1356/1191 1170/1355/1190 1185/1370/1196 +f 1179/1364/1178 1178/1363/1177 1193/1378/1197 +f 1171/1356/1191 1186/1371/1198 1187/1372/1200 +f 1179/1364/1178 1194/1379/1199 1195/1380/1201 +f 1173/1358/1181 1172/1357/1179 1187/1372/1200 +f 1180/1365/1180 1195/1380/1201 1196/1381/1203 +f 1174/1359/1183 1173/1358/1181 1188/1373/1202 +f 1174/1359/1183 1189/1374/1204 1190/1375/1206 +f 1186/1371/1198 1201/1386/1220 1202/1387/1207 +f 1194/1379/1199 1209/1394/1221 1210/1395/1208 +f 1188/1373/1202 1187/1372/1200 1202/1387/1207 +f 1196/1381/1203 1195/1380/1201 1210/1395/1208 +f 1189/1374/1204 1188/1373/1202 1203/1388/1209 +f 1189/1374/1204 1204/1389/1211 1205/1390/1213 +f 1182/1367/1205 1197/1382/1212 1198/1383/1214 +f 1190/1375/1206 1205/1390/1213 1206/1391/1215 +f 1184/1369/1194 1183/1368/1192 1198/1383/1214 +f 1192/1377/1195 1191/1376/1193 1206/1391/1215 +f 1184/1369/1194 1199/1384/1216 1200/1385/1218 +f 1192/1377/1195 1207/1392/1217 1208/1393/1219 +f 1186/1371/1198 1185/1370/1196 1200/1385/1218 +f 1194/1379/1199 1193/1378/1197 1208/1393/1219 +f 1205/1390/1213 1220/1405/1235 1221/1406/1222 +f 1198/1383/1214 1213/1398/1236 1214/1399/1223 +f 1207/1392/1217 1206/1391/1215 1221/1406/1222 +f 1199/1384/1216 1214/1399/1223 1215/1400/1225 +f 1207/1392/1217 1222/1407/1224 1223/1408/1226 +f 1201/1386/1220 1200/1385/1218 1215/1400/1225 +f 1209/1394/1221 1208/1393/1219 1223/1408/1226 +f 1201/1386/1220 1216/1401/1227 1217/1402/1229 +f 1209/1394/1221 1224/1409/1228 1225/1410/1230 +f 1203/1388/1209 1202/1387/1207 1217/1402/1229 +f 1210/1395/1208 1225/1410/1230 1226/1411/1232 +f 1204/1389/1211 1203/1388/1209 1218/1403/1231 +f 1204/1389/1211 1219/1404/1233 1220/1405/1235 +f 1197/1382/1212 1212/1397/1234 1213/1398/1236 +f 1224/1409/1228 1239/1424/1251 1240/1425/1237 +f 1218/1403/1231 1217/1402/1229 1232/1417/1238 +f 1225/1410/1230 1240/1425/1237 1241/1426/1240 +f 1219/1404/1233 1218/1403/1231 1233/1418/1239 +f 1219/1404/1233 1234/1419/1241 1235/1420/1243 +f 1213/1398/1236 1212/1397/1234 1227/1412/1242 +f 1220/1405/1235 1235/1420/1243 1236/1421/1245 +f 1213/1398/1236 1228/1413/1244 1229/1414/1246 +f 1222/1407/1224 1221/1406/1222 1236/1421/1245 +f 1214/1399/1223 1229/1414/1246 1230/1415/1248 +f 1222/1407/1224 1237/1422/1247 1238/1423/1249 +f 1216/1401/1227 1215/1400/1225 1230/1415/1248 +f 1224/1409/1228 1223/1408/1226 1238/1423/1249 +f 1216/1401/1227 1231/1416/1250 1232/1417/1238 +f 1237/1422/1247 1236/1421/1245 1251/1436/1252 +f 1229/1414/1246 1244/1429/1266 1245/1430/1254 +f 1237/1422/1247 1252/1437/1253 1253/1438/1255 +f 1231/1416/1250 1230/1415/1248 1245/1430/1254 +f 1239/1424/1251 1238/1423/1249 1253/1438/1255 +f 1231/1416/1250 1246/1431/1256 1247/1432/1258 +f 1239/1424/1251 1254/1439/1257 1255/1440/1259 +f 1233/1418/1239 1232/1417/1238 1247/1432/1258 +f 1241/1426/1240 1240/1425/1237 1255/1440/1259 +f 1234/1419/1241 1233/1418/1239 1248/1433/1260 +f 1234/1419/1241 1249/1434/1262 1250/1435/1264 +f 1228/1413/1244 1227/1412/1242 1242/1427/1263 +f 1235/1420/1243 1250/1435/1264 1251/1436/1252 +f 1228/1413/1244 1243/1428/1265 1244/1429/1266 +f 1256/1441/1261 1255/1440/1259 1270/1455/1267 +f 1249/1434/1262 1248/1433/1260 1263/1448/1269 +f 1249/1434/1262 1264/1449/1270 1265/1450/1272 +f 1242/1427/1263 1257/1442/1271 1258/1443/1273 +f 1250/1435/1264 1265/1450/1272 1266/1451/1274 +f 1243/1428/1265 1258/1443/1273 1259/1444/1275 +f 1252/1437/1253 1251/1436/1252 1266/1451/1274 +f 1244/1429/1266 1259/1444/1275 1260/1445/1277 +f 1252/1437/1253 1267/1452/1276 1268/1453/1278 +f 1246/1431/1256 1245/1430/1254 1260/1445/1277 +f 1254/1439/1257 1253/1438/1255 1268/1453/1278 +f 1246/1431/1256 1261/1446/1279 1262/1447/1281 +f 1254/1439/1257 1269/1454/1280 1270/1455/1267 +f 1248/1433/1260 1247/1432/1258 1262/1447/1281 +f 1259/1444/1275 1275/1491/1295 1276/1492/1282 +f 1267/1452/1276 1283/1499/1296 1284/1500/1283 +f 1261/1446/1279 1260/1445/1277 1276/1492/1282 +f 1269/1454/1280 1268/1453/1278 1284/1500/1283 +f 1261/1446/1279 1277/1493/1284 1278/1494/1286 +f 1269/1454/1280 1285/1501/1285 1286/1502/1287 +f 1263/1448/1269 1262/1447/1281 1278/1494/1286 +f 1270/1455/1267 1286/1502/1287 1287/1503/1289 +f 1264/1449/1270 1263/1448/1269 1279/1495/1288 +f 1264/1449/1270 1280/1496/1290 1281/1497/1292 +f 1257/1442/1271 1273/1489/1291 1274/1490/1293 +f 1265/1450/1272 1281/1497/1292 1282/1498/1294 +f 1258/1443/1273 1274/1490/1293 1275/1491/1295 +f 1267/1452/1276 1266/1451/1274 1282/1498/1294 +f 1280/1496/1290 1279/1495/1288 1294/1510/1297 +f 1280/1496/1290 1295/1511/1298 1296/1512/1301 +f 1273/1489/1291 1288/1504/1299 1289/1505/1302 +f 1281/1497/1292 1296/1512/1301 1297/1513/1303 +f 1274/1490/1293 1289/1505/1302 1290/1506/1304 +f 1283/1499/1296 1282/1498/1294 1297/1513/1303 +f 1275/1491/1295 1290/1506/1304 1291/1507/1306 +f 1283/1499/1296 1298/1514/1305 1299/1515/1307 +f 1276/1492/1282 1291/1507/1306 1292/1508/1308 +f 1285/1501/1285 1284/1500/1283 1299/1515/1307 +f 1277/1493/1284 1292/1508/1308 1293/1509/1310 +f 1285/1501/1285 1300/1516/1309 1301/1517/1311 +f 1279/1495/1288 1278/1494/1286 1293/1509/1310 +f 1286/1502/1287 1301/1517/1311 1302/1518/1300 +f 1298/1514/1305 1313/1529/1326 1314/1530/1312 +f 1292/1508/1308 1291/1507/1306 1306/1522/1313 +f 1300/1516/1309 1299/1515/1307 1314/1530/1312 +f 1292/1508/1308 1307/1523/1314 1308/1524/1316 +f 1300/1516/1309 1315/1531/1315 1316/1532/1317 +f 1294/1510/1297 1293/1509/1310 1308/1524/1316 +f 1301/1517/1311 1316/1532/1317 1317/1533/1319 +f 1295/1511/1298 1294/1510/1297 1309/1525/1318 +f 1295/1511/1298 1310/1526/1320 1311/1527/1322 +f 1288/1504/1299 1303/1519/1321 1304/1520/1323 +f 1296/1512/1301 1311/1527/1322 1312/1528/1324 +f 1289/1505/1302 1304/1520/1323 1305/1521/1325 +f 1298/1514/1305 1297/1513/1303 1312/1528/1324 +f 1290/1506/1304 1305/1521/1325 1306/1522/1313 +f 1310/1526/1320 1325/1548/1341 1326/1550/1329 +f 1303/1519/1321 1318/1534/1327 1319/1536/1330 +f 1311/1527/1322 1326/1550/1329 1327/1552/1331 +f 1305/1521/1325 1304/1520/1323 1319/1536/1330 +f 1313/1529/1326 1312/1528/1324 1327/1552/1331 +f 1305/1521/1325 1320/1538/1332 1321/1540/1334 +f 1313/1529/1326 1328/1554/1333 1329/1556/1335 +f 1307/1523/1314 1306/1522/1313 1321/1540/1334 +f 1315/1531/1315 1314/1530/1312 1329/1556/1335 +f 1307/1523/1314 1322/1542/1336 1323/1544/1338 +f 1315/1531/1315 1330/1558/1337 1331/1560/1339 +f 1309/1525/1318 1308/1524/1316 1323/1544/1338 +f 1316/1532/1317 1331/1560/1339 1332/1563/1328 +f 1310/1526/1320 1309/1525/1318 1324/1546/1340 +f 1322/1543/1336 1321/1541/1334 1336/1567/1342 +f 1330/1559/1337 1329/1557/1335 1344/1575/1344 +f 1322/1543/1336 1337/1568/1343 1338/1569/1346 +f 1330/1559/1337 1345/1576/1345 1346/1577/1347 +f 1324/1547/1340 1323/1545/1338 1338/1569/1346 +f 1331/1561/1339 1346/1577/1347 1347/1578/1349 +f 1325/1549/1341 1324/1547/1340 1339/1570/1348 +f 1325/1549/1341 1340/1571/1350 1341/1572/1352 +f 1318/1535/1327 1333/1564/1351 1334/1565/1353 +f 1326/1551/1329 1341/1572/1352 1342/1573/1354 +f 1320/1539/1332 1319/1537/1330 1334/1565/1353 +f 1328/1555/1333 1327/1553/1331 1342/1573/1354 +f 1320/1539/1332 1335/1566/1355 1336/1567/1342 +f 1328/1555/1333 1343/1574/1356 1344/1575/1344 +f 1340/1571/1350 1355/1586/1370 1356/1587/1357 +f 1333/1564/1351 1348/1579/1371 1349/1580/1358 +f 1341/1572/1352 1356/1587/1357 1357/1588/1359 +f 1335/1566/1355 1334/1565/1353 1349/1580/1358 +f 1343/1574/1356 1342/1573/1354 1357/1588/1359 +f 1335/1566/1355 1350/1581/1360 1351/1582/1362 +f 1343/1574/1356 1358/1589/1361 1359/1590/1363 +f 1337/1568/1343 1336/1567/1342 1351/1582/1362 +f 1345/1576/1345 1344/1575/1344 1359/1590/1363 +f 1337/1568/1343 1352/1583/1364 1353/1584/1366 +f 1345/1576/1345 1360/1591/1365 1361/1592/1367 +f 1339/1570/1348 1338/1569/1346 1353/1584/1366 +f 1346/1577/1347 1361/1592/1367 1362/1593/1369 +f 1340/1571/1350 1339/1570/1348 1354/1585/1368 +f 1360/1591/1365 1359/1590/1363 1374/1605/1372 +f 1352/1583/1364 1367/1598/1386 1368/1599/1374 +f 1361/1592/1367 1360/1591/1365 1375/1606/1373 +f 1354/1585/1368 1353/1584/1366 1368/1599/1374 +f 1361/1592/1367 1376/1607/1375 1377/1608/1377 +f 1355/1586/1370 1354/1585/1368 1369/1600/1376 +f 1355/1586/1370 1370/1601/1378 1371/1602/1380 +f 1349/1580/1358 1348/1579/1371 1363/1594/1379 +f 1356/1587/1357 1371/1602/1380 1372/1603/1382 +f 1349/1580/1358 1364/1595/1381 1365/1596/1383 +f 1358/1589/1361 1357/1588/1359 1372/1603/1382 +f 1350/1581/1360 1365/1596/1383 1366/1597/1385 +f 1358/1589/1361 1373/1604/1384 1374/1605/1372 +f 1352/1583/1364 1351/1582/1362 1366/1597/1385 +f 1371/1602/1380 1386/1617/1401 1387/1618/1387 +f 1365/1596/1383 1364/1595/1381 1379/1610/1388 +f 1373/1604/1384 1372/1603/1382 1387/1618/1387 +f 1365/1596/1383 1380/1611/1389 1381/1612/1391 +f 1373/1604/1384 1388/1619/1390 1389/1620/1392 +f 1367/1598/1386 1366/1597/1385 1381/1612/1391 +f 1375/1606/1373 1374/1605/1372 1389/1620/1392 +f 1367/1598/1386 1382/1613/1393 1383/1614/1395 +f 1375/1606/1373 1390/1621/1394 1391/1622/1396 +f 1369/1600/1376 1368/1599/1374 1383/1614/1395 +f 1376/1607/1375 1391/1622/1396 1392/1623/1398 +f 1370/1601/1378 1369/1600/1376 1384/1615/1397 +f 1370/1601/1378 1385/1616/1399 1386/1617/1401 +f 1363/1594/1379 1378/1609/1400 1379/1610/1388 +f 1390/1621/1394 1405/1636/1416 1406/1637/1402 +f 1384/1615/1397 1383/1614/1395 1398/1629/1403 +f 1391/1622/1396 1406/1637/1402 1407/1638/1405 +f 1385/1616/1399 1384/1615/1397 1399/1630/1404 +f 1385/1616/1399 1400/1631/1406 1401/1632/1408 +f 1378/1609/1400 1393/1624/1407 1394/1625/1409 +f 1386/1617/1401 1401/1632/1408 1402/1633/1410 +f 1380/1611/1389 1379/1610/1388 1394/1625/1409 +f 1388/1619/1390 1387/1618/1387 1402/1633/1410 +f 1380/1611/1389 1395/1626/1411 1396/1627/1413 +f 1388/1619/1390 1403/1634/1412 1404/1635/1414 +f 1382/1613/1393 1381/1612/1391 1396/1627/1413 +f 1390/1621/1394 1389/1620/1392 1404/1635/1414 +f 1382/1613/1393 1397/1628/1415 1398/1629/1403 +f 1394/1625/1409 1409/1640/1431 1410/1641/1417 +f 1403/1634/1412 1402/1633/1410 1417/1648/1418 +f 1395/1626/1411 1410/1641/1417 1411/1642/1420 +f 1403/1634/1412 1418/1649/1419 1419/1650/1421 +f 1397/1628/1415 1396/1627/1413 1411/1642/1420 +f 1405/1636/1416 1404/1635/1414 1419/1650/1421 +f 1397/1628/1415 1412/1643/1422 1413/1644/1424 +f 1405/1636/1416 1420/1651/1423 1421/1652/1425 +f 1399/1630/1404 1398/1629/1403 1413/1644/1424 +f 1406/1637/1402 1421/1652/1425 1422/1653/1427 +f 1400/1631/1406 1399/1630/1404 1414/1645/1426 +f 1400/1631/1406 1415/1646/1428 1416/1647/1430 +f 1394/1625/1409 1393/1624/1407 1408/1639/1429 +f 1401/1632/1408 1416/1647/1430 1417/1648/1418 +f 1414/1645/1426 1413/1644/1424 1428/1659/1432 +f 1422/1653/1427 1421/1652/1425 1436/1667/1434 +f 1415/1646/1428 1414/1645/1426 1429/1660/1433 +f 1415/1646/1428 1430/1661/1436 1431/1662/1438 +f 1408/1639/1429 1423/1654/1437 1424/1655/1439 +f 1416/1647/1430 1431/1662/1438 1432/1663/1440 +f 1410/1641/1417 1409/1640/1431 1424/1655/1439 +f 1418/1649/1419 1417/1648/1418 1432/1663/1440 +f 1410/1641/1417 1425/1656/1441 1426/1657/1443 +f 1418/1649/1419 1433/1664/1442 1434/1665/1444 +f 1412/1643/1422 1411/1642/1420 1426/1657/1443 +f 1420/1651/1423 1419/1650/1421 1434/1665/1444 +f 1412/1643/1422 1427/1658/1445 1428/1659/1432 +f 1420/1651/1423 1435/1666/1446 1436/1667/1434 +f 1432/1663/1440 1442/1673/968 970/1124/970 +f 1426/1657/1443 1425/1656/1441 965/1119/965 +f 1433/1664/1442 970/1124/970 1443/1674/974 +f 1427/1658/1445 1426/1657/1443 1440/1671/971 +f 1435/1666/1446 1434/1665/1444 1443/1674/974 +f 1428/1659/1432 1427/1658/1445 966/1120/975 +f 1435/1666/1446 1444/1675/978 1445/1676/982 +f 1429/1660/1433 1428/1659/1432 1441/1672/979 +f 1436/1667/1434 1445/1676/982 1446/1677/986 +f 1430/1661/1436 1429/1660/1433 967/1121/983 +f 1430/1661/1436 968/1122/988 969/1123/994 +f 1424/1655/1439 1423/1654/1437 1438/1669/989 +f 1431/1662/1438 969/1123/994 1442/1673/968 +f 1425/1656/1441 1424/1655/1439 1439/1670/995 +o Sphere.003 +v 0.000000 0.918536 -3.984501 +v 0.000000 0.642636 -4.260401 +v 0.000000 0.282156 -4.409716 +v 0.000000 0.087066 -4.428931 +v 0.000000 -0.108024 -4.409716 +v 0.000000 -0.468504 -4.260401 +v 0.038060 1.067851 -3.620273 +v 0.074658 1.010945 -3.804261 +v 0.108386 0.918536 -3.973826 +v 0.137950 0.794173 -4.122451 +v 0.162212 0.642636 -4.244424 +v 0.180240 0.469749 -4.335058 +v 0.191342 0.282156 -4.390871 +v 0.195090 0.087066 -4.409716 +v 0.191342 -0.108024 -4.390871 +v 0.180240 -0.295618 -4.335058 +v 0.162212 -0.468504 -4.244424 +v 0.137950 -0.620041 -4.122451 +v 0.108386 -0.744404 -3.973826 +v 0.074658 -0.836814 -3.804261 +v 0.038060 -0.893719 -3.620273 +v 0.074658 1.067851 -3.609171 +v 0.146447 1.010945 -3.782485 +v 0.212608 0.918536 -3.942211 +v 0.270598 0.794173 -4.082212 +v 0.318190 0.642636 -4.197109 +v 0.353553 0.469749 -4.282485 +v 0.375330 0.282156 -4.335058 +v 0.382683 0.087066 -4.352810 +v 0.375330 -0.108024 -4.335058 +v 0.353553 -0.295618 -4.282485 +v 0.318190 -0.468504 -4.197109 +v 0.270598 -0.620041 -4.082212 +v 0.212608 -0.744404 -3.942211 +v 0.146447 -0.836814 -3.782485 +v 0.074658 -0.893719 -3.609171 +v 0.108386 1.067851 -3.591143 +v 0.212608 1.010945 -3.747121 +v 0.308658 0.918536 -3.890871 +v 0.392847 0.794173 -4.016869 +v 0.461940 0.642636 -4.120273 +v 0.513280 0.469749 -4.197109 +v 0.544895 0.282156 -4.244424 +v 0.555570 0.087066 -4.260400 +v 0.544895 -0.108024 -4.244424 +v 0.513280 -0.295618 -4.197109 +v 0.461940 -0.468504 -4.120273 +v 0.392847 -0.620041 -4.016869 +v 0.308658 -0.744404 -3.890871 +v 0.212608 -0.836814 -3.747121 +v 0.108386 -0.893719 -3.591143 +v 0.137950 1.067851 -3.566881 +v 0.270598 1.010945 -3.699529 +v 0.392847 0.918536 -3.821778 +v 0.500000 0.794173 -3.928931 +v 0.587938 0.642636 -4.016869 +v 0.653281 0.469749 -4.082212 +v 0.693520 0.282156 -4.122451 +v 0.707107 0.087066 -4.136038 +v 0.693520 -0.108024 -4.122451 +v 0.653281 -0.295618 -4.082212 +v 0.587938 -0.468504 -4.016869 +v 0.500000 -0.620041 -3.928931 +v 0.392847 -0.744404 -3.821778 +v 0.270598 -0.836814 -3.699529 +v 0.137950 -0.893719 -3.566881 +v 0.162212 1.067851 -3.537317 +v 0.318190 1.010945 -3.641539 +v 0.461940 0.918536 -3.737589 +v 0.587938 0.794173 -3.821779 +v 0.691342 0.642636 -3.890871 +v 0.768178 0.469749 -3.942211 +v 0.815493 0.282156 -3.973826 +v 0.831470 0.087066 -3.984501 +v 0.815493 -0.108024 -3.973826 +v 0.768178 -0.295618 -3.942211 +v 0.691342 -0.468504 -3.890871 +v 0.587938 -0.620041 -3.821779 +v 0.461940 -0.744404 -3.737589 +v 0.318190 -0.836814 -3.641539 +v 0.162212 -0.893719 -3.537317 +v 0.000000 1.087066 -3.428931 +v 0.180240 1.067851 -3.503589 +v 0.353553 1.010945 -3.575377 +v 0.513280 0.918536 -3.641538 +v 0.653281 0.794173 -3.699529 +v 0.768178 0.642636 -3.747120 +v 0.853553 0.469749 -3.782485 +v 0.906127 0.282156 -3.804261 +v 0.923879 0.087066 -3.811614 +v 0.906127 -0.108024 -3.804261 +v 0.853553 -0.295618 -3.782485 +v 0.768178 -0.468504 -3.747120 +v 0.653281 -0.620041 -3.699529 +v 0.513280 -0.744404 -3.641538 +v 0.353553 -0.836814 -3.575377 +v 0.180240 -0.893719 -3.503589 +v 0.191342 1.067851 -3.466991 +v 0.375330 1.010945 -3.503589 +v 0.544895 0.918536 -3.537317 +v 0.693520 0.794173 -3.566881 +v 0.815493 0.642636 -3.591143 +v 0.906127 0.469749 -3.609171 +v 0.961940 0.282156 -3.620273 +v 0.980785 0.087066 -3.624021 +v 0.961940 -0.108024 -3.620273 +v 0.906127 -0.295618 -3.609171 +v 0.815493 -0.468504 -3.591143 +v 0.693520 -0.620041 -3.566881 +v 0.544895 -0.744404 -3.537317 +v 0.375330 -0.836814 -3.503589 +v 0.191342 -0.893719 -3.466991 +v 0.195090 1.067851 -3.428931 +v 0.382683 1.010945 -3.428931 +v 0.555570 0.918536 -3.428931 +v 0.707107 0.794173 -3.428931 +v 0.831469 0.642636 -3.428931 +v 0.923879 0.469749 -3.428931 +v 0.980785 0.282156 -3.428931 +v 1.000000 0.087066 -3.428931 +v 0.980785 -0.108024 -3.428931 +v 0.923879 -0.295618 -3.428931 +v 0.831469 -0.468504 -3.428931 +v 0.707107 -0.620041 -3.428931 +v 0.555570 -0.744404 -3.428931 +v 0.382683 -0.836814 -3.428931 +v 0.195090 -0.893719 -3.428931 +v 0.191342 1.067851 -3.390871 +v 0.375330 1.010945 -3.354273 +v 0.544895 0.918536 -3.320544 +v 0.693520 0.794173 -3.290981 +v 0.815493 0.642636 -3.266719 +v 0.906127 0.469749 -3.248691 +v 0.961940 0.282156 -3.237589 +v 0.980785 0.087066 -3.233840 +v 0.961940 -0.108024 -3.237589 +v 0.906127 -0.295618 -3.248691 +v 0.815493 -0.468504 -3.266719 +v 0.693520 -0.620041 -3.290981 +v 0.544895 -0.744404 -3.320544 +v 0.375330 -0.836814 -3.354273 +v 0.191342 -0.893719 -3.390871 +v 0.180240 1.067851 -3.354273 +v 0.353553 1.010945 -3.282484 +v 0.513280 0.918536 -3.216323 +v 0.653281 0.794173 -3.158333 +v 0.768178 0.642636 -3.110741 +v 0.853553 0.469749 -3.075378 +v 0.906127 0.282156 -3.053601 +v 0.923879 0.087066 -3.046247 +v 0.906127 -0.108024 -3.053601 +v 0.853553 -0.295618 -3.075378 +v 0.768178 -0.468504 -3.110741 +v 0.653281 -0.620041 -3.158333 +v 0.513280 -0.744404 -3.216323 +v 0.353553 -0.836814 -3.282484 +v 0.180240 -0.893719 -3.354273 +v 0.162212 1.067851 -3.320545 +v 0.318190 1.010945 -3.216323 +v 0.461940 0.918536 -3.120273 +v 0.587938 0.794173 -3.036084 +v 0.691341 0.642636 -2.966991 +v 0.768178 0.469749 -2.915651 +v 0.815493 0.282156 -2.884036 +v 0.831469 0.087066 -2.873361 +v 0.815493 -0.108024 -2.884036 +v 0.768178 -0.295618 -2.915651 +v 0.691341 -0.468504 -2.966991 +v 0.587938 -0.620041 -3.036084 +v 0.461940 -0.744404 -3.120273 +v 0.318190 -0.836814 -3.216323 +v 0.162212 -0.893719 -3.320545 +v 0.137950 1.067851 -3.290981 +v 0.270598 1.010945 -3.158333 +v 0.392847 0.918536 -3.036084 +v 0.500000 0.794173 -2.928931 +v 0.587938 0.642636 -2.840993 +v 0.653281 0.469749 -2.775650 +v 0.693520 0.282156 -2.735411 +v 0.707106 0.087066 -2.721824 +v 0.693520 -0.108024 -2.735411 +v 0.653281 -0.295618 -2.775650 +v 0.587938 -0.468504 -2.840993 +v 0.500000 -0.620041 -2.928931 +v 0.392847 -0.744404 -3.036084 +v 0.270598 -0.836814 -3.158333 +v 0.137950 -0.893719 -3.290981 +v 0.108386 1.067851 -3.266719 +v 0.212607 1.010945 -3.110741 +v 0.308658 0.918536 -2.966991 +v 0.392847 0.794173 -2.840993 +v 0.461940 0.642636 -2.737589 +v 0.513280 0.469749 -2.660753 +v 0.544895 0.282156 -2.613438 +v 0.555570 0.087066 -2.597462 +v 0.544895 -0.108024 -2.613438 +v 0.513280 -0.295618 -2.660753 +v 0.461940 -0.468504 -2.737589 +v 0.392847 -0.620041 -2.840993 +v 0.308658 -0.744404 -2.966991 +v 0.212607 -0.836814 -3.110741 +v 0.108386 -0.893719 -3.266719 +v 0.074658 1.067851 -3.248691 +v 0.146447 1.010945 -3.075378 +v 0.212607 0.918536 -2.915651 +v 0.270598 0.794173 -2.775650 +v 0.318189 0.642636 -2.660753 +v 0.353553 0.469749 -2.575378 +v 0.375330 0.282156 -2.522804 +v 0.382683 0.087066 -2.505052 +v 0.375330 -0.108024 -2.522804 +v 0.353553 -0.295618 -2.575378 +v 0.318189 -0.468504 -2.660753 +v 0.270598 -0.620041 -2.775650 +v 0.212607 -0.744404 -2.915651 +v 0.146447 -0.836814 -3.075378 +v 0.074658 -0.893719 -3.248691 +v 0.038060 1.067851 -3.237589 +v 0.074658 1.010945 -3.053601 +v 0.108386 0.918536 -2.884036 +v 0.137950 0.794173 -2.735411 +v 0.162212 0.642636 -2.613438 +v 0.180240 0.469749 -2.522804 +v 0.191342 0.282156 -2.466991 +v 0.195090 0.087066 -2.448146 +v 0.191342 -0.108024 -2.466991 +v 0.180240 -0.295618 -2.522804 +v 0.162212 -0.468504 -2.613438 +v 0.137950 -0.620041 -2.735411 +v 0.108386 -0.744404 -2.884036 +v 0.074658 -0.836814 -3.053601 +v 0.038060 -0.893719 -3.237589 +v -0.000000 1.067851 -3.233841 +v -0.000000 1.010945 -3.046248 +v -0.000000 0.918536 -2.873361 +v -0.000000 0.794173 -2.721824 +v -0.000000 0.642636 -2.597462 +v 0.000000 0.469749 -2.505052 +v -0.000000 0.282156 -2.448146 +v -0.000000 0.087066 -2.428932 +v -0.000000 -0.108024 -2.448146 +v 0.000000 -0.295618 -2.505052 +v -0.000000 -0.468504 -2.597462 +v -0.000000 -0.620041 -2.721824 +v -0.000000 -0.744404 -2.873361 +v -0.000000 -0.836814 -3.046248 +v -0.000000 -0.893719 -3.233841 +v -0.038060 1.067851 -3.237589 +v -0.074658 1.010945 -3.053601 +v -0.108386 0.918536 -2.884036 +v -0.137950 0.794173 -2.735411 +v -0.162212 0.642636 -2.613438 +v -0.180240 0.469749 -2.522804 +v -0.191342 0.282156 -2.466992 +v -0.195091 0.087066 -2.448146 +v -0.191342 -0.108024 -2.466992 +v -0.180240 -0.295618 -2.522804 +v -0.162212 -0.468504 -2.613438 +v -0.137950 -0.620041 -2.735411 +v -0.108386 -0.744404 -2.884036 +v -0.074658 -0.836814 -3.053601 +v -0.038060 -0.893719 -3.237589 +v -0.074658 1.067851 -3.248691 +v -0.146447 1.010945 -3.075378 +v -0.212608 0.918536 -2.915651 +v -0.270598 0.794173 -2.775650 +v -0.318190 0.642636 -2.660754 +v -0.353553 0.469749 -2.575378 +v -0.375330 0.282156 -2.522804 +v -0.382683 0.087066 -2.505052 +v -0.375330 -0.108024 -2.522804 +v -0.353553 -0.295618 -2.575378 +v -0.318190 -0.468504 -2.660754 +v -0.270598 -0.620041 -2.775650 +v -0.212608 -0.744404 -2.915651 +v -0.146447 -0.836814 -3.075378 +v -0.074658 -0.893719 -3.248691 +v -0.108386 1.067851 -3.266719 +v -0.212608 1.010945 -3.110741 +v -0.308658 0.918536 -2.966992 +v -0.392847 0.794173 -2.840993 +v -0.461940 0.642636 -2.737590 +v -0.513280 0.469749 -2.660753 +v -0.544895 0.282156 -2.613438 +v -0.555570 0.087066 -2.597462 +v -0.544895 -0.108024 -2.613438 +v -0.513280 -0.295618 -2.660753 +v -0.461940 -0.468504 -2.737590 +v -0.392847 -0.620041 -2.840993 +v -0.308658 -0.744404 -2.966992 +v -0.212608 -0.836814 -3.110741 +v -0.108386 -0.893719 -3.266719 +v -0.137950 1.067851 -3.290982 +v -0.270598 1.010945 -3.158333 +v -0.392847 0.918536 -3.036084 +v -0.500000 0.794173 -2.928931 +v -0.587938 0.642636 -2.840994 +v -0.653281 0.469749 -2.775650 +v -0.693520 0.282156 -2.735411 +v -0.707106 0.087066 -2.721825 +v -0.693520 -0.108024 -2.735411 +v -0.653281 -0.295618 -2.775650 +v -0.587938 -0.468504 -2.840994 +v -0.500000 -0.620041 -2.928931 +v -0.392847 -0.744404 -3.036084 +v -0.270598 -0.836814 -3.158333 +v -0.137950 -0.893719 -3.290982 +v 0.000000 -0.912934 -3.428931 +v -0.162212 1.067851 -3.320545 +v -0.318190 1.010945 -3.216324 +v -0.461940 0.918536 -3.120273 +v -0.587938 0.794173 -3.036084 +v -0.691341 0.642636 -2.966992 +v -0.768177 0.469749 -2.915651 +v -0.815493 0.282156 -2.884036 +v -0.831469 0.087066 -2.873362 +v -0.815493 -0.108024 -2.884036 +v -0.768177 -0.295618 -2.915651 +v -0.691341 -0.468504 -2.966992 +v -0.587938 -0.620041 -3.036084 +v -0.461940 -0.744404 -3.120273 +v -0.318190 -0.836814 -3.216324 +v -0.162212 -0.893719 -3.320545 +v -0.180240 1.067851 -3.354273 +v -0.353553 1.010945 -3.282485 +v -0.513280 0.918536 -3.216324 +v -0.653281 0.794173 -3.158333 +v -0.768177 0.642636 -3.110742 +v -0.853553 0.469749 -3.075378 +v -0.906127 0.282156 -3.053601 +v -0.923879 0.087066 -3.046248 +v -0.906127 -0.108024 -3.053601 +v -0.853553 -0.295618 -3.075378 +v -0.768177 -0.468504 -3.110742 +v -0.653281 -0.620041 -3.158333 +v -0.513280 -0.744404 -3.216324 +v -0.353553 -0.836814 -3.282485 +v -0.180240 -0.893719 -3.354273 +v -0.191342 1.067851 -3.390871 +v -0.375330 1.010945 -3.354273 +v -0.544895 0.918536 -3.320545 +v -0.693520 0.794173 -3.290981 +v -0.815493 0.642636 -3.266720 +v -0.906127 0.469749 -3.248691 +v -0.961939 0.282156 -3.237590 +v -0.980784 0.087066 -3.233841 +v -0.961939 -0.108024 -3.237590 +v -0.906127 -0.295618 -3.248691 +v -0.815493 -0.468504 -3.266720 +v -0.693520 -0.620041 -3.290981 +v -0.544895 -0.744404 -3.320545 +v -0.375330 -0.836814 -3.354273 +v -0.191342 -0.893719 -3.390871 +v -0.195090 1.067851 -3.428931 +v -0.382683 1.010945 -3.428931 +v -0.555570 0.918536 -3.428931 +v -0.707107 0.794173 -3.428931 +v -0.831469 0.642636 -3.428931 +v -0.923879 0.469749 -3.428931 +v -0.980785 0.282156 -3.428931 +v -0.999999 0.087066 -3.428931 +v -0.980785 -0.108024 -3.428931 +v -0.923879 -0.295618 -3.428931 +v -0.831469 -0.468504 -3.428931 +v -0.707107 -0.620041 -3.428931 +v -0.555570 -0.744404 -3.428931 +v -0.382683 -0.836814 -3.428931 +v -0.195090 -0.893719 -3.428931 +v -0.191342 1.067851 -3.466991 +v -0.375330 1.010945 -3.503589 +v -0.544895 0.918536 -3.537318 +v -0.693520 0.794173 -3.566881 +v -0.815493 0.642636 -3.591143 +v -0.906127 0.469749 -3.609171 +v -0.961939 0.282156 -3.620273 +v -0.980784 0.087066 -3.624022 +v -0.961939 -0.108024 -3.620273 +v -0.906127 -0.295618 -3.609171 +v -0.815493 -0.468504 -3.591143 +v -0.693520 -0.620041 -3.566881 +v -0.544895 -0.744404 -3.537318 +v -0.375330 -0.836814 -3.503589 +v -0.191342 -0.893719 -3.466991 +v -0.180240 1.067851 -3.503589 +v -0.353553 1.010945 -3.575377 +v -0.513279 0.918536 -3.641538 +v -0.653281 0.794173 -3.699529 +v -0.768177 0.642636 -3.747121 +v -0.853553 0.469749 -3.782484 +v -0.906127 0.282156 -3.804261 +v -0.923878 0.087066 -3.811615 +v -0.906127 -0.108024 -3.804261 +v -0.853553 -0.295618 -3.782484 +v -0.768177 -0.468504 -3.747121 +v -0.653281 -0.620041 -3.699529 +v -0.513279 -0.744404 -3.641538 +v -0.353553 -0.836814 -3.575377 +v -0.180240 -0.893719 -3.503589 +v -0.162212 1.067851 -3.537317 +v -0.318189 1.010945 -3.641538 +v -0.461939 0.918536 -3.737589 +v -0.587938 0.794173 -3.821778 +v -0.691341 0.642636 -3.890871 +v -0.768177 0.469749 -3.942211 +v -0.815493 0.282156 -3.973826 +v -0.831468 0.087066 -3.984501 +v -0.815493 -0.108024 -3.973826 +v -0.768177 -0.295618 -3.942211 +v -0.691341 -0.468504 -3.890871 +v -0.587938 -0.620041 -3.821778 +v -0.461939 -0.744404 -3.737589 +v -0.318189 -0.836814 -3.641538 +v -0.162212 -0.893719 -3.537317 +v -0.137950 1.067851 -3.566880 +v -0.270598 1.010945 -3.699529 +v -0.392847 0.918536 -3.821778 +v -0.500000 0.794173 -3.928931 +v -0.587937 0.642636 -4.016869 +v -0.653281 0.469749 -4.082212 +v -0.693519 0.282156 -4.122451 +v -0.707106 0.087066 -4.136037 +v -0.693519 -0.108024 -4.122451 +v -0.653281 -0.295618 -4.082212 +v -0.587937 -0.468504 -4.016869 +v -0.500000 -0.620041 -3.928931 +v -0.392847 -0.744404 -3.821778 +v -0.270598 -0.836814 -3.699529 +v -0.137950 -0.893719 -3.566880 +v -0.108386 1.067851 -3.591142 +v -0.212607 1.010945 -3.747120 +v -0.308658 0.918536 -3.890870 +v -0.392847 0.794173 -4.016869 +v -0.461939 0.642636 -4.120272 +v -0.513280 0.469749 -4.197108 +v -0.544895 0.282156 -4.244424 +v -0.555569 0.087066 -4.260400 +v -0.544895 -0.108024 -4.244424 +v -0.513280 -0.295618 -4.197108 +v -0.461939 -0.468504 -4.120272 +v -0.392847 -0.620041 -4.016869 +v -0.308658 -0.744404 -3.890870 +v -0.212607 -0.836814 -3.747120 +v -0.108386 -0.893719 -3.591142 +v -0.074658 1.067851 -3.609171 +v -0.146446 1.010945 -3.782484 +v -0.212607 0.918536 -3.942210 +v -0.270598 0.794173 -4.082212 +v -0.318189 0.642636 -4.197108 +v -0.353553 0.469749 -4.282484 +v -0.375330 0.282156 -4.335058 +v -0.382683 0.087066 -4.352809 +v -0.375330 -0.108024 -4.335058 +v -0.353553 -0.295618 -4.282484 +v -0.318189 -0.468504 -4.197108 +v -0.270598 -0.620041 -4.082212 +v -0.212607 -0.744404 -3.942210 +v -0.146446 -0.836814 -3.782484 +v -0.074658 -0.893719 -3.609171 +v -0.038060 1.067851 -3.620272 +v -0.074658 1.010945 -3.804261 +v -0.108386 0.918536 -3.973825 +v -0.137950 0.794173 -4.122451 +v -0.162211 0.642636 -4.244423 +v -0.180240 0.469749 -4.335058 +v -0.191341 0.282156 -4.390870 +v -0.195090 0.087066 -4.409715 +v -0.191341 -0.108024 -4.390870 +v -0.180240 -0.295618 -4.335058 +v -0.162211 -0.468504 -4.244423 +v -0.137950 -0.620041 -4.122451 +v -0.108386 -0.744404 -3.973825 +v -0.074658 -0.836814 -3.804261 +v -0.038060 -0.893719 -3.620272 +v 0.000000 1.067851 -3.624021 +v 0.000000 1.010945 -3.811614 +v 0.000000 0.794173 -4.136038 +v 0.000000 0.469749 -4.352810 +v 0.000000 -0.295618 -4.352810 +v 0.000000 -0.620041 -4.136038 +v 0.000000 -0.744404 -3.984500 +v 0.000000 -0.836814 -3.811614 +v 0.000000 -0.893719 -3.624021 +vn -0.0000 0.8286 -0.5598 +vn 0.0757 0.9217 -0.3804 +vn 0.1092 0.8286 -0.5490 +vn -0.0000 -0.3805 -0.9248 +vn 0.1626 -0.5528 -0.8173 +vn -0.0000 -0.5528 -0.8333 +vn -0.0000 0.7041 -0.7101 +vn 0.1385 0.7041 -0.6965 +vn 0.1385 -0.7041 -0.6965 +vn -0.0000 -0.7041 -0.7101 +vn -0.0000 0.5528 -0.8333 +vn 0.1626 0.5528 -0.8173 +vn 0.1092 -0.8286 -0.5490 +vn -0.0000 -0.8286 -0.5598 +vn -0.0000 0.3805 -0.9248 +vn 0.1804 0.3805 -0.9070 +vn 0.0757 -0.9217 -0.3804 +vn -0.0000 -0.9217 -0.3879 +vn -0.0000 0.1939 -0.9810 +vn 0.1914 0.1939 -0.9622 +vn 0.0392 -0.9796 -0.1971 +vn -0.0000 -0.9796 -0.2010 +vn 0.1951 -0.0000 -0.9808 +vn -0.0000 -0.0000 -1.0000 +vn -0.0000 0.9796 -0.2010 +vn -0.0000 1.0000 -0.0000 +vn 0.0392 0.9796 -0.1971 +vn -0.0000 -1.0000 -0.0000 +vn 0.1914 -0.1939 -0.9622 +vn -0.0000 -0.1939 -0.9810 +vn -0.0000 0.9217 -0.3879 +vn 0.1804 -0.3805 -0.9070 +vn 0.1484 0.9217 -0.3584 +vn 0.3539 -0.3805 -0.8544 +vn 0.2142 0.8286 -0.5172 +vn 0.3189 -0.5528 -0.7699 +vn 0.2718 0.7041 -0.6561 +vn 0.2718 -0.7041 -0.6561 +vn 0.3189 0.5528 -0.7699 +vn 0.2142 -0.8286 -0.5172 +vn 0.3539 0.3805 -0.8544 +vn 0.1484 -0.9217 -0.3584 +vn 0.3754 0.1939 -0.9063 +vn 0.0769 -0.9796 -0.1857 +vn 0.3827 -0.0000 -0.9239 +vn 0.0769 0.9796 -0.1857 +vn 0.3754 -0.1939 -0.9063 +vn 0.5138 0.3805 -0.7689 +vn 0.2155 -0.9217 -0.3225 +vn 0.5450 0.1939 -0.8157 +vn 0.1117 -0.9796 -0.1671 +vn 0.5556 -0.0000 -0.8315 +vn 0.1117 0.9796 -0.1671 +vn 0.5450 -0.1939 -0.8157 +vn 0.2155 0.9217 -0.3225 +vn 0.5138 -0.3805 -0.7689 +vn 0.3110 0.8286 -0.4654 +vn 0.4630 -0.5528 -0.6929 +vn 0.3945 0.7041 -0.5905 +vn 0.3945 -0.7041 -0.5905 +vn 0.4630 0.5528 -0.6929 +vn 0.3110 -0.8286 -0.4654 +vn 0.6539 -0.3805 -0.6539 +vn 0.3958 0.8286 -0.3958 +vn 0.5893 -0.5528 -0.5893 +vn 0.5021 0.7041 -0.5021 +vn 0.5021 -0.7041 -0.5021 +vn 0.5893 0.5528 -0.5893 +vn 0.3958 -0.8286 -0.3958 +vn 0.6539 0.3805 -0.6539 +vn 0.2743 -0.9217 -0.2743 +vn 0.6937 0.1939 -0.6937 +vn 0.1421 -0.9796 -0.1421 +vn 0.7071 -0.0000 -0.7071 +vn 0.1421 0.9796 -0.1421 +vn 0.6937 -0.1939 -0.6937 +vn 0.2743 0.9217 -0.2743 +vn 0.4654 -0.8286 -0.3110 +vn 0.3225 -0.9217 -0.2155 +vn 0.7689 0.3805 -0.5138 +vn 0.8157 0.1939 -0.5450 +vn 0.1671 -0.9796 -0.1117 +vn 0.8315 -0.0000 -0.5556 +vn 0.1671 0.9796 -0.1117 +vn 0.8157 -0.1939 -0.5450 +vn 0.3225 0.9217 -0.2155 +vn 0.7689 -0.3805 -0.5138 +vn 0.4654 0.8286 -0.3110 +vn 0.6929 -0.5528 -0.4630 +vn 0.5905 0.7041 -0.3945 +vn 0.5905 -0.7041 -0.3945 +vn 0.6929 0.5528 -0.4630 +vn 0.5172 0.8286 -0.2142 +vn 0.8544 -0.3805 -0.3539 +vn 0.7699 -0.5528 -0.3189 +vn 0.6561 0.7041 -0.2718 +vn 0.6561 -0.7041 -0.2718 +vn 0.7699 0.5528 -0.3189 +vn 0.5172 -0.8286 -0.2142 +vn 0.8544 0.3805 -0.3539 +vn 0.3584 -0.9217 -0.1484 +vn 0.9063 0.1939 -0.3754 +vn 0.1857 -0.9796 -0.0769 +vn 0.9239 -0.0000 -0.3827 +vn 0.1857 0.9796 -0.0769 +vn 0.9063 -0.1939 -0.3754 +vn 0.3584 0.9217 -0.1484 +vn 0.9070 0.3805 -0.1804 +vn 0.9622 0.1939 -0.1914 +vn 0.1971 -0.9796 -0.0392 +vn 0.9808 -0.0000 -0.1951 +vn 0.1971 0.9796 -0.0392 +vn 0.9622 -0.1939 -0.1914 +vn 0.3804 0.9217 -0.0757 +vn 0.9070 -0.3805 -0.1804 +vn 0.5490 0.8286 -0.1092 +vn 0.8173 -0.5528 -0.1626 +vn 0.6965 0.7041 -0.1385 +vn 0.6965 -0.7041 -0.1385 +vn 0.8173 0.5528 -0.1626 +vn 0.5490 -0.8286 -0.1092 +vn 0.3804 -0.9217 -0.0757 +vn 0.9248 -0.3805 -0.0000 +vn 0.8333 -0.5528 -0.0000 +vn 0.7101 0.7041 -0.0000 +vn 0.7101 -0.7041 -0.0000 +vn 0.8333 0.5528 -0.0000 +vn 0.5598 -0.8286 -0.0000 +vn 0.9248 0.3805 -0.0000 +vn 0.3879 -0.9217 -0.0000 +vn 0.9810 0.1939 -0.0000 +vn 0.2010 -0.9796 -0.0000 +vn 1.0000 -0.0000 -0.0000 +vn 0.2010 0.9796 -0.0000 +vn 0.9810 -0.1939 -0.0000 +vn 0.3879 0.9217 -0.0000 +vn 0.5598 0.8286 -0.0000 +vn 0.1971 -0.9796 0.0392 +vn 0.9622 0.1939 0.1914 +vn 0.9808 -0.0000 0.1951 +vn 0.1971 0.9796 0.0392 +vn 0.9622 -0.1939 0.1914 +vn 0.3804 0.9217 0.0757 +vn 0.9070 -0.3805 0.1804 +vn 0.5490 0.8286 0.1092 +vn 0.8173 -0.5528 0.1626 +vn 0.6965 0.7041 0.1385 +vn 0.6965 -0.7041 0.1385 +vn 0.8173 0.5528 0.1626 +vn 0.5490 -0.8286 0.1092 +vn 0.9070 0.3805 0.1804 +vn 0.3804 -0.9217 0.0757 +vn 0.6561 -0.7041 0.2718 +vn 0.6561 0.7041 0.2718 +vn 0.7699 0.5528 0.3189 +vn 0.5172 -0.8286 0.2142 +vn 0.8544 0.3805 0.3539 +vn 0.3584 -0.9217 0.1484 +vn 0.9063 0.1939 0.3754 +vn 0.1857 -0.9796 0.0769 +vn 0.9239 -0.0000 0.3827 +vn 0.1857 0.9796 0.0769 +vn 0.9063 -0.1939 0.3754 +vn 0.3584 0.9217 0.1484 +vn 0.8544 -0.3805 0.3539 +vn 0.5172 0.8286 0.2142 +vn 0.7699 -0.5528 0.3189 +vn 0.1671 0.9796 0.1117 +vn 0.1671 -0.9796 0.1117 +vn 0.8157 -0.1939 0.5450 +vn 0.3225 0.9217 0.2155 +vn 0.7689 -0.3805 0.5138 +vn 0.4654 0.8286 0.3110 +vn 0.6929 -0.5528 0.4630 +vn 0.5905 0.7041 0.3945 +vn 0.5905 -0.7041 0.3945 +vn 0.6929 0.5528 0.4630 +vn 0.4654 -0.8286 0.3110 +vn 0.7689 0.3805 0.5138 +vn 0.3225 -0.9217 0.2155 +vn 0.8157 0.1939 0.5450 +vn 0.8315 -0.0000 0.5556 +vn 0.5021 0.7041 0.5021 +vn 0.5893 0.5528 0.5893 +vn 0.3958 -0.8286 0.3958 +vn 0.6539 0.3805 0.6539 +vn 0.2743 -0.9217 0.2743 +vn 0.6937 0.1939 0.6937 +vn 0.1421 -0.9796 0.1421 +vn 0.7071 -0.0000 0.7071 +vn 0.1421 0.9796 0.1421 +vn 0.6937 -0.1939 0.6937 +vn 0.2743 0.9217 0.2743 +vn 0.6539 -0.3805 0.6539 +vn 0.3958 0.8286 0.3958 +vn 0.5893 -0.5528 0.5893 +vn 0.5021 -0.7041 0.5021 +vn 0.5450 -0.1939 0.8157 +vn 0.1117 0.9796 0.1671 +vn 0.2155 0.9217 0.3225 +vn 0.5138 -0.3805 0.7689 +vn 0.3110 0.8286 0.4654 +vn 0.4630 -0.5528 0.6929 +vn 0.3945 0.7041 0.5905 +vn 0.3945 -0.7041 0.5905 +vn 0.4630 0.5528 0.6929 +vn 0.3110 -0.8286 0.4654 +vn 0.5138 0.3805 0.7689 +vn 0.2155 -0.9217 0.3225 +vn 0.5450 0.1939 0.8157 +vn 0.1117 -0.9796 0.1671 +vn 0.5556 -0.0000 0.8315 +vn 0.2718 -0.7041 0.6561 +vn 0.2142 -0.8286 0.5172 +vn 0.3539 0.3805 0.8544 +vn 0.1484 -0.9217 0.3584 +vn 0.3754 0.1939 0.9063 +vn 0.0769 -0.9796 0.1857 +vn 0.3827 -0.0000 0.9239 +vn 0.0769 0.9796 0.1857 +vn 0.3754 -0.1939 0.9063 +vn 0.1484 0.9217 0.3584 +vn 0.3539 -0.3805 0.8544 +vn 0.2142 0.8286 0.5172 +vn 0.3189 -0.5528 0.7699 +vn 0.2718 0.7041 0.6561 +vn 0.3189 0.5528 0.7699 +vn 0.0757 0.9217 0.3804 +vn 0.1804 -0.3805 0.9070 +vn 0.1092 0.8286 0.5490 +vn 0.1626 -0.5528 0.8173 +vn 0.1385 0.7041 0.6965 +vn 0.1385 -0.7041 0.6965 +vn 0.1626 0.5528 0.8173 +vn 0.1092 -0.8286 0.5490 +vn 0.1804 0.3805 0.9070 +vn 0.0757 -0.9217 0.3804 +vn 0.1914 0.1939 0.9622 +vn 0.0392 -0.9796 0.1971 +vn 0.1951 -0.0000 0.9808 +vn 0.0392 0.9796 0.1971 +vn 0.1914 -0.1939 0.9622 +vn -0.0000 0.3805 0.9248 +vn -0.0000 -0.9217 0.3879 +vn -0.0000 0.1939 0.9810 +vn -0.0000 -0.9796 0.2010 +vn -0.0000 -0.0000 1.0000 +vn -0.0000 0.9796 0.2010 +vn -0.0000 -0.1939 0.9810 +vn -0.0000 0.9217 0.3879 +vn -0.0000 -0.3805 0.9248 +vn -0.0000 0.8286 0.5598 +vn -0.0000 -0.5528 0.8333 +vn -0.0000 0.7041 0.7101 +vn -0.0000 -0.7041 0.7101 +vn -0.0000 0.5528 0.8333 +vn -0.0000 -0.8286 0.5598 +vn -0.1804 -0.3805 0.9070 +vn -0.1092 0.8286 0.5490 +vn -0.1626 -0.5528 0.8173 +vn -0.1385 0.7041 0.6965 +vn -0.1385 -0.7041 0.6965 +vn -0.1626 0.5528 0.8173 +vn -0.1092 -0.8286 0.5490 +vn -0.1804 0.3805 0.9070 +vn -0.0757 -0.9217 0.3804 +vn -0.1914 0.1939 0.9622 +vn -0.0392 -0.9796 0.1971 +vn -0.1951 -0.0000 0.9808 +vn -0.0392 0.9796 0.1971 +vn -0.1914 -0.1939 0.9622 +vn -0.0757 0.9217 0.3804 +vn -0.1484 -0.9217 0.3584 +vn -0.3539 0.3805 0.8544 +vn -0.3754 0.1939 0.9063 +vn -0.0769 -0.9796 0.1857 +vn -0.3827 -0.0000 0.9239 +vn -0.0769 0.9796 0.1857 +vn -0.3754 -0.1939 0.9063 +vn -0.1484 0.9217 0.3584 +vn -0.3539 -0.3805 0.8544 +vn -0.2142 0.8286 0.5172 +vn -0.3189 -0.5528 0.7699 +vn -0.2718 0.7041 0.6561 +vn -0.2718 -0.7041 0.6561 +vn -0.3189 0.5528 0.7699 +vn -0.2142 -0.8286 0.5172 +vn -0.5138 -0.3805 0.7689 +vn -0.4630 -0.5528 0.6929 +vn -0.3945 0.7041 0.5905 +vn -0.3945 -0.7041 0.5905 +vn -0.4630 0.5528 0.6929 +vn -0.3110 -0.8286 0.4654 +vn -0.5138 0.3805 0.7689 +vn -0.2155 -0.9217 0.3225 +vn -0.5450 0.1939 0.8157 +vn -0.1117 -0.9796 0.1671 +vn -0.5556 -0.0000 0.8315 +vn -0.1117 0.9796 0.1671 +vn -0.5450 -0.1939 0.8157 +vn -0.2155 0.9217 0.3225 +vn -0.3110 0.8286 0.4654 +vn -0.2743 -0.9217 0.2743 +vn -0.1421 -0.9796 0.1421 +vn -0.6937 0.1939 0.6937 +vn -0.7071 -0.0000 0.7071 +vn -0.1421 0.9796 0.1421 +vn -0.6937 -0.1939 0.6937 +vn -0.2743 0.9217 0.2743 +vn -0.6539 -0.3805 0.6539 +vn -0.3958 0.8286 0.3958 +vn -0.5893 -0.5528 0.5893 +vn -0.5021 0.7041 0.5021 +vn -0.5021 -0.7041 0.5021 +vn -0.5893 0.5528 0.5893 +vn -0.3958 -0.8286 0.3958 +vn -0.6539 0.3805 0.6539 +vn -0.5905 0.7041 0.3945 +vn -0.5905 -0.7041 0.3945 +vn -0.6929 0.5528 0.4630 +vn -0.4654 -0.8286 0.3110 +vn -0.7689 0.3805 0.5138 +vn -0.3225 -0.9217 0.2155 +vn -0.8157 0.1939 0.5450 +vn -0.1671 -0.9796 0.1117 +vn -0.8315 -0.0000 0.5556 +vn -0.1671 0.9796 0.1117 +vn -0.8157 -0.1939 0.5450 +vn -0.3225 0.9217 0.2155 +vn -0.7689 -0.3805 0.5138 +vn -0.4654 0.8286 0.3110 +vn -0.6929 -0.5528 0.4630 +vn -0.9063 0.1939 0.3754 +vn -0.9239 -0.0000 0.3827 +vn -0.1857 0.9796 0.0769 +vn -0.1857 -0.9796 0.0769 +vn -0.9063 -0.1939 0.3754 +vn -0.3584 0.9217 0.1484 +vn -0.8544 -0.3805 0.3539 +vn -0.5172 0.8286 0.2142 +vn -0.7699 -0.5528 0.3189 +vn -0.6561 0.7041 0.2718 +vn -0.6561 -0.7041 0.2718 +vn -0.7699 0.5528 0.3189 +vn -0.5172 -0.8286 0.2142 +vn -0.8544 0.3805 0.3539 +vn -0.3584 -0.9217 0.1484 +vn -0.6965 -0.7041 0.1385 +vn -0.6965 0.7041 0.1385 +vn -0.8173 0.5528 0.1626 +vn -0.5490 -0.8286 0.1092 +vn -0.9070 0.3805 0.1804 +vn -0.3804 -0.9217 0.0757 +vn -0.9622 0.1939 0.1914 +vn -0.1971 -0.9796 0.0392 +vn -0.9808 -0.0000 0.1951 +vn -0.1971 0.9796 0.0392 +vn -0.9622 -0.1939 0.1914 +vn -0.3804 0.9217 0.0757 +vn -0.9070 -0.3805 0.1804 +vn -0.5490 0.8286 0.1092 +vn -0.8173 -0.5528 0.1626 +vn -0.2010 0.9796 -0.0000 +vn -0.2010 -0.9796 -0.0000 +vn -0.9810 -0.1939 -0.0000 +vn -0.3879 0.9217 -0.0000 +vn -0.9248 -0.3805 -0.0000 +vn -0.5598 0.8286 -0.0000 +vn -0.8333 -0.5528 -0.0000 +vn -0.7101 0.7041 -0.0000 +vn -0.7101 -0.7041 -0.0000 +vn -0.8333 0.5528 -0.0000 +vn -0.5598 -0.8286 -0.0000 +vn -0.9248 0.3805 -0.0000 +vn -0.3879 -0.9217 -0.0000 +vn -0.9810 0.1939 -0.0000 +vn -1.0000 -0.0000 -0.0000 +vn -0.6965 0.7041 -0.1385 +vn -0.8173 0.5528 -0.1626 +vn -0.6965 -0.7041 -0.1385 +vn -0.5490 -0.8286 -0.1092 +vn -0.9070 0.3805 -0.1804 +vn -0.3804 -0.9217 -0.0757 +vn -0.9622 0.1939 -0.1914 +vn -0.1971 -0.9796 -0.0392 +vn -0.9808 -0.0000 -0.1951 +vn -0.1971 0.9796 -0.0392 +vn -0.9622 -0.1939 -0.1914 +vn -0.3804 0.9217 -0.0757 +vn -0.9070 -0.3805 -0.1804 +vn -0.5490 0.8286 -0.1092 +vn -0.8173 -0.5528 -0.1626 +vn -0.9063 -0.1939 -0.3754 +vn -0.3584 0.9217 -0.1484 +vn -0.8544 -0.3805 -0.3539 +vn -0.5172 0.8286 -0.2142 +vn -0.7699 -0.5528 -0.3189 +vn -0.6561 0.7041 -0.2718 +vn -0.6561 -0.7041 -0.2718 +vn -0.7699 0.5528 -0.3189 +vn -0.5172 -0.8286 -0.2142 +vn -0.8544 0.3805 -0.3539 +vn -0.3584 -0.9217 -0.1484 +vn -0.9063 0.1939 -0.3754 +vn -0.1857 -0.9796 -0.0769 +vn -0.9239 -0.0000 -0.3827 +vn -0.1857 0.9796 -0.0769 +vn -0.5905 -0.7041 -0.3945 +vn -0.4654 -0.8286 -0.3110 +vn -0.7689 0.3805 -0.5138 +vn -0.3225 -0.9217 -0.2155 +vn -0.8157 0.1939 -0.5450 +vn -0.1671 -0.9796 -0.1117 +vn -0.8315 -0.0000 -0.5556 +vn -0.1671 0.9796 -0.1117 +vn -0.8157 -0.1939 -0.5450 +vn -0.3225 0.9217 -0.2155 +vn -0.7689 -0.3805 -0.5138 +vn -0.4654 0.8286 -0.3110 +vn -0.6929 -0.5528 -0.4630 +vn -0.5905 0.7041 -0.3945 +vn -0.6929 0.5528 -0.4630 +vn -0.6539 -0.3805 -0.6539 +vn -0.2743 0.9217 -0.2743 +vn -0.3958 0.8286 -0.3958 +vn -0.5893 -0.5528 -0.5893 +vn -0.5021 0.7041 -0.5021 +vn -0.5021 -0.7041 -0.5021 +vn -0.5893 0.5528 -0.5893 +vn -0.3958 -0.8286 -0.3958 +vn -0.6539 0.3805 -0.6539 +vn -0.2743 -0.9217 -0.2743 +vn -0.6937 0.1939 -0.6937 +vn -0.1421 -0.9796 -0.1421 +vn -0.7071 -0.0000 -0.7071 +vn -0.1421 0.9796 -0.1421 +vn -0.6937 -0.1939 -0.6937 +vn -0.2155 -0.9217 -0.3225 +vn -0.5138 0.3805 -0.7689 +vn -0.5450 0.1939 -0.8157 +vn -0.1117 -0.9796 -0.1671 +vn -0.5556 -0.0000 -0.8315 +vn -0.1117 0.9796 -0.1671 +vn -0.5450 -0.1939 -0.8157 +vn -0.2155 0.9217 -0.3225 +vn -0.5138 -0.3805 -0.7689 +vn -0.3110 0.8286 -0.4654 +vn -0.4630 -0.5528 -0.6929 +vn -0.3945 0.7041 -0.5905 +vn -0.3945 -0.7041 -0.5905 +vn -0.4630 0.5528 -0.6929 +vn -0.3110 -0.8286 -0.4654 +vn -0.2142 0.8286 -0.5172 +vn -0.3539 -0.3805 -0.8544 +vn -0.3189 -0.5528 -0.7699 +vn -0.2718 0.7041 -0.6561 +vn -0.2718 -0.7041 -0.6561 +vn -0.3189 0.5528 -0.7699 +vn -0.2142 -0.8286 -0.5172 +vn -0.3539 0.3805 -0.8544 +vn -0.1484 -0.9217 -0.3584 +vn -0.3754 0.1939 -0.9063 +vn -0.0769 -0.9796 -0.1857 +vn -0.3827 -0.0000 -0.9239 +vn -0.0769 0.9796 -0.1857 +vn -0.3754 -0.1939 -0.9063 +vn -0.1484 0.9217 -0.3584 +vn -0.1804 0.3805 -0.9070 +vn -0.1914 0.1939 -0.9622 +vn -0.0757 -0.9217 -0.3804 +vn -0.0392 -0.9796 -0.1971 +vn -0.1951 -0.0000 -0.9808 +vn -0.0392 0.9796 -0.1971 +vn -0.1914 -0.1939 -0.9622 +vn -0.0757 0.9217 -0.3804 +vn -0.1804 -0.3805 -0.9070 +vn -0.1092 0.8286 -0.5490 +vn -0.1626 -0.5528 -0.8173 +vn -0.1385 0.7041 -0.6965 +vn -0.1385 -0.7041 -0.6965 +vn -0.1626 0.5528 -0.8173 +vn -0.1092 -0.8286 -0.5490 +vt 0.750000 0.812500 +vt 0.750000 0.687500 +vt 0.750000 0.562500 +vt 0.750000 0.500000 +vt 0.750000 0.437500 +vt 0.750000 0.312500 +vt 0.718750 0.937500 +vt 0.718750 0.875000 +vt 0.718750 0.812500 +vt 0.718750 0.750000 +vt 0.718750 0.687500 +vt 0.718750 0.625000 +vt 0.718750 0.562500 +vt 0.718750 0.500000 +vt 0.718750 0.437500 +vt 0.718750 0.375000 +vt 0.718750 0.312500 +vt 0.718750 0.250000 +vt 0.718750 0.187500 +vt 0.718750 0.125000 +vt 0.718750 0.062500 +vt 0.687500 0.937500 +vt 0.687500 0.875000 +vt 0.687500 0.812500 +vt 0.687500 0.750000 +vt 0.687500 0.687500 +vt 0.687500 0.625000 +vt 0.687500 0.562500 +vt 0.687500 0.500000 +vt 0.687500 0.437500 +vt 0.687500 0.375000 +vt 0.687500 0.312500 +vt 0.687500 0.250000 +vt 0.687500 0.187500 +vt 0.687500 0.125000 +vt 0.687500 0.062500 +vt 0.656250 0.937500 +vt 0.656250 0.875000 +vt 0.656250 0.812500 +vt 0.656250 0.750000 +vt 0.656250 0.687500 +vt 0.656250 0.625000 +vt 0.656250 0.562500 +vt 0.656250 0.500000 +vt 0.656250 0.437500 +vt 0.656250 0.375000 +vt 0.656250 0.312500 +vt 0.656250 0.250000 +vt 0.656250 0.187500 +vt 0.656250 0.125000 +vt 0.656250 0.062500 +vt 0.625000 0.937500 +vt 0.625000 0.875000 +vt 0.625000 0.812500 +vt 0.625000 0.750000 +vt 0.625000 0.687500 +vt 0.625000 0.625000 +vt 0.625000 0.562500 +vt 0.625000 0.500000 +vt 0.625000 0.437500 +vt 0.625000 0.375000 +vt 0.625000 0.312500 +vt 0.625000 0.250000 +vt 0.625000 0.187500 +vt 0.625000 0.125000 +vt 0.625000 0.062500 +vt 0.593750 0.937500 +vt 0.593750 0.875000 +vt 0.593750 0.812500 +vt 0.593750 0.750000 +vt 0.593750 0.687500 +vt 0.593750 0.625000 +vt 0.593750 0.562500 +vt 0.593750 0.500000 +vt 0.593750 0.437500 +vt 0.593750 0.375000 +vt 0.593750 0.312500 +vt 0.593750 0.250000 +vt 0.593750 0.187500 +vt 0.593750 0.125000 +vt 0.593750 0.062500 +vt 0.734375 1.000000 +vt 0.703125 1.000000 +vt 0.671875 1.000000 +vt 0.640625 1.000000 +vt 0.609375 1.000000 +vt 0.578125 1.000000 +vt 0.546875 1.000000 +vt 0.515625 1.000000 +vt 0.484375 1.000000 +vt 0.453125 1.000000 +vt 0.421875 1.000000 +vt 0.390625 1.000000 +vt 0.359375 1.000000 +vt 0.328125 1.000000 +vt 0.296875 1.000000 +vt 0.265625 1.000000 +vt 0.234375 1.000000 +vt 0.203125 1.000000 +vt 0.171875 1.000000 +vt 0.140625 1.000000 +vt 0.109375 1.000000 +vt 0.078125 1.000000 +vt 0.046875 1.000000 +vt 0.015625 1.000000 +vt 0.984375 1.000000 +vt 0.953125 1.000000 +vt 0.921875 1.000000 +vt 0.890625 1.000000 +vt 0.859375 1.000000 +vt 0.828125 1.000000 +vt 0.796875 1.000000 +vt 0.765625 1.000000 +vt 0.562500 0.937500 +vt 0.562500 0.875000 +vt 0.562500 0.812500 +vt 0.562500 0.750000 +vt 0.562500 0.687500 +vt 0.562500 0.625000 +vt 0.562500 0.562500 +vt 0.562500 0.500000 +vt 0.562500 0.437500 +vt 0.562500 0.375000 +vt 0.562500 0.312500 +vt 0.562500 0.250000 +vt 0.562500 0.187500 +vt 0.562500 0.125000 +vt 0.562500 0.062500 +vt 0.531250 0.937500 +vt 0.531250 0.875000 +vt 0.531250 0.812500 +vt 0.531250 0.750000 +vt 0.531250 0.687500 +vt 0.531250 0.625000 +vt 0.531250 0.562500 +vt 0.531250 0.500000 +vt 0.531250 0.437500 +vt 0.531250 0.375000 +vt 0.531250 0.312500 +vt 0.531250 0.250000 +vt 0.531250 0.187500 +vt 0.531250 0.125000 +vt 0.531250 0.062500 +vt 0.500000 0.937500 +vt 0.500000 0.875000 +vt 0.500000 0.812500 +vt 0.500000 0.750000 +vt 0.500000 0.687500 +vt 0.500000 0.625000 +vt 0.500000 0.562500 +vt 0.500000 0.500000 +vt 0.500000 0.437500 +vt 0.500000 0.375000 +vt 0.500000 0.312500 +vt 0.500000 0.250000 +vt 0.500000 0.187500 +vt 0.500000 0.125000 +vt 0.500000 0.062500 +vt 0.468750 0.937500 +vt 0.468750 0.875000 +vt 0.468750 0.812500 +vt 0.468750 0.750000 +vt 0.468750 0.687500 +vt 0.468750 0.625000 +vt 0.468750 0.562500 +vt 0.468750 0.500000 +vt 0.468750 0.437500 +vt 0.468750 0.375000 +vt 0.468750 0.312500 +vt 0.468750 0.250000 +vt 0.468750 0.187500 +vt 0.468750 0.125000 +vt 0.468750 0.062500 +vt 0.437500 0.937500 +vt 0.437500 0.875000 +vt 0.437500 0.812500 +vt 0.437500 0.750000 +vt 0.437500 0.687500 +vt 0.437500 0.625000 +vt 0.437500 0.562500 +vt 0.437500 0.500000 +vt 0.437500 0.437500 +vt 0.437500 0.375000 +vt 0.437500 0.312500 +vt 0.437500 0.250000 +vt 0.437500 0.187500 +vt 0.437500 0.125000 +vt 0.437500 0.062500 +vt 0.406250 0.937500 +vt 0.406250 0.875000 +vt 0.406250 0.812500 +vt 0.406250 0.750000 +vt 0.406250 0.687500 +vt 0.406250 0.625000 +vt 0.406250 0.562500 +vt 0.406250 0.500000 +vt 0.406250 0.437500 +vt 0.406250 0.375000 +vt 0.406250 0.312500 +vt 0.406250 0.250000 +vt 0.406250 0.187500 +vt 0.406250 0.125000 +vt 0.406250 0.062500 +vt 0.375000 0.937500 +vt 0.375000 0.875000 +vt 0.375000 0.812500 +vt 0.375000 0.750000 +vt 0.375000 0.687500 +vt 0.375000 0.625000 +vt 0.375000 0.562500 +vt 0.375000 0.500000 +vt 0.375000 0.437500 +vt 0.375000 0.375000 +vt 0.375000 0.312500 +vt 0.375000 0.250000 +vt 0.375000 0.187500 +vt 0.375000 0.125000 +vt 0.375000 0.062500 +vt 0.343750 0.937500 +vt 0.343750 0.875000 +vt 0.343750 0.812500 +vt 0.343750 0.750000 +vt 0.343750 0.687500 +vt 0.343750 0.625000 +vt 0.343750 0.562500 +vt 0.343750 0.500000 +vt 0.343750 0.437500 +vt 0.343750 0.375000 +vt 0.343750 0.312500 +vt 0.343750 0.250000 +vt 0.343750 0.187500 +vt 0.343750 0.125000 +vt 0.343750 0.062500 +vt 0.312500 0.937500 +vt 0.312500 0.875000 +vt 0.312500 0.812500 +vt 0.312500 0.750000 +vt 0.312500 0.687500 +vt 0.312500 0.625000 +vt 0.312500 0.562500 +vt 0.312500 0.500000 +vt 0.312500 0.437500 +vt 0.312500 0.375000 +vt 0.312500 0.312500 +vt 0.312500 0.250000 +vt 0.312500 0.187500 +vt 0.312500 0.125000 +vt 0.312500 0.062500 +vt 0.281250 0.937500 +vt 0.281250 0.875000 +vt 0.281250 0.812500 +vt 0.281250 0.750000 +vt 0.281250 0.687500 +vt 0.281250 0.625000 +vt 0.281250 0.562500 +vt 0.281250 0.500000 +vt 0.281250 0.437500 +vt 0.281250 0.375000 +vt 0.281250 0.312500 +vt 0.281250 0.250000 +vt 0.281250 0.187500 +vt 0.281250 0.125000 +vt 0.281250 0.062500 +vt 0.250000 0.937500 +vt 0.250000 0.875000 +vt 0.250000 0.812500 +vt 0.250000 0.750000 +vt 0.250000 0.687500 +vt 0.250000 0.625000 +vt 0.250000 0.562500 +vt 0.250000 0.500000 +vt 0.250000 0.437500 +vt 0.250000 0.375000 +vt 0.250000 0.312500 +vt 0.250000 0.250000 +vt 0.250000 0.187500 +vt 0.250000 0.125000 +vt 0.250000 0.062500 +vt 0.218750 0.937500 +vt 0.218750 0.875000 +vt 0.218750 0.812500 +vt 0.218750 0.750000 +vt 0.218750 0.687500 +vt 0.218750 0.625000 +vt 0.218750 0.562500 +vt 0.218750 0.500000 +vt 0.218750 0.437500 +vt 0.218750 0.375000 +vt 0.218750 0.312500 +vt 0.218750 0.250000 +vt 0.218750 0.187500 +vt 0.218750 0.125000 +vt 0.218750 0.062500 +vt 0.187500 0.937500 +vt 0.187500 0.875000 +vt 0.187500 0.812500 +vt 0.187500 0.750000 +vt 0.187500 0.687500 +vt 0.187500 0.625000 +vt 0.187500 0.562500 +vt 0.187500 0.500000 +vt 0.187500 0.437500 +vt 0.187500 0.375000 +vt 0.187500 0.312500 +vt 0.187500 0.250000 +vt 0.187500 0.187500 +vt 0.187500 0.125000 +vt 0.187500 0.062500 +vt 0.156250 0.937500 +vt 0.156250 0.875000 +vt 0.156250 0.812500 +vt 0.156250 0.750000 +vt 0.156250 0.687500 +vt 0.156250 0.625000 +vt 0.156250 0.562500 +vt 0.156250 0.500000 +vt 0.156250 0.437500 +vt 0.156250 0.375000 +vt 0.156250 0.312500 +vt 0.156250 0.250000 +vt 0.156250 0.187500 +vt 0.156250 0.125000 +vt 0.156250 0.062500 +vt 0.125000 0.937500 +vt 0.125000 0.875000 +vt 0.125000 0.812500 +vt 0.125000 0.750000 +vt 0.125000 0.687500 +vt 0.125000 0.625000 +vt 0.125000 0.562500 +vt 0.125000 0.500000 +vt 0.125000 0.437500 +vt 0.125000 0.375000 +vt 0.125000 0.312500 +vt 0.125000 0.250000 +vt 0.125000 0.187500 +vt 0.125000 0.125000 +vt 0.125000 0.062500 +vt 0.734375 0.000000 +vt 0.703125 0.000000 +vt 0.671875 0.000000 +vt 0.640625 0.000000 +vt 0.609375 0.000000 +vt 0.578125 0.000000 +vt 0.546875 0.000000 +vt 0.515625 0.000000 +vt 0.484375 0.000000 +vt 0.453125 0.000000 +vt 0.421875 0.000000 +vt 0.390625 0.000000 +vt 0.359375 0.000000 +vt 0.328125 0.000000 +vt 0.296875 0.000000 +vt 0.265625 0.000000 +vt 0.234375 0.000000 +vt 0.203125 0.000000 +vt 0.171875 0.000000 +vt 0.140625 0.000000 +vt 0.109375 0.000000 +vt 0.078125 0.000000 +vt 0.046875 0.000000 +vt 0.015625 0.000000 +vt 0.984375 0.000000 +vt 0.953125 0.000000 +vt 0.921875 0.000000 +vt 0.890625 0.000000 +vt 0.859375 0.000000 +vt 0.828125 0.000000 +vt 0.796875 0.000000 +vt 0.765625 0.000000 +vt 0.093750 0.937500 +vt 0.093750 0.875000 +vt 0.093750 0.812500 +vt 0.093750 0.750000 +vt 0.093750 0.687500 +vt 0.093750 0.625000 +vt 0.093750 0.562500 +vt 0.093750 0.500000 +vt 0.093750 0.437500 +vt 0.093750 0.375000 +vt 0.093750 0.312500 +vt 0.093750 0.250000 +vt 0.093750 0.187500 +vt 0.093750 0.125000 +vt 0.093750 0.062500 +vt 0.062500 0.937500 +vt 0.062500 0.875000 +vt 0.062500 0.812500 +vt 0.062500 0.750000 +vt 0.062500 0.687500 +vt 0.062500 0.625000 +vt 0.062500 0.562500 +vt 0.062500 0.500000 +vt 0.062500 0.437500 +vt 0.062500 0.375000 +vt 0.062500 0.312500 +vt 0.062500 0.250000 +vt 0.062500 0.187500 +vt 0.062500 0.125000 +vt 0.062500 0.062500 +vt 0.031250 0.937500 +vt 0.031250 0.875000 +vt 0.031250 0.812500 +vt 0.031250 0.750000 +vt 0.031250 0.687500 +vt 0.031250 0.625000 +vt 0.031250 0.562500 +vt 0.031250 0.500000 +vt 0.031250 0.437500 +vt 0.031250 0.375000 +vt 0.031250 0.312500 +vt 0.031250 0.250000 +vt 0.031250 0.187500 +vt 0.031250 0.125000 +vt 0.031250 0.062500 +vt 0.000000 0.937500 +vt 1.000000 0.937500 +vt 0.000000 0.875000 +vt 1.000000 0.875000 +vt 0.000000 0.812500 +vt 1.000000 0.812500 +vt 0.000000 0.750000 +vt 1.000000 0.750000 +vt 0.000000 0.687500 +vt 1.000000 0.687500 +vt 0.000000 0.625000 +vt 1.000000 0.625000 +vt 0.000000 0.562500 +vt 1.000000 0.562500 +vt 0.000000 0.500000 +vt 1.000000 0.500000 +vt 0.000000 0.437500 +vt 1.000000 0.437500 +vt 0.000000 0.375000 +vt 1.000000 0.375000 +vt 0.000000 0.312500 +vt 1.000000 0.312500 +vt 0.000000 0.250000 +vt 1.000000 0.250000 +vt 0.000000 0.187500 +vt 1.000000 0.187500 +vt 0.000000 0.125000 +vt 1.000000 0.125000 +vt 1.000000 0.062500 +vt 0.000000 0.062500 +vt 0.968750 0.937500 +vt 0.968750 0.875000 +vt 0.968750 0.812500 +vt 0.968750 0.750000 +vt 0.968750 0.687500 +vt 0.968750 0.625000 +vt 0.968750 0.562500 +vt 0.968750 0.500000 +vt 0.968750 0.437500 +vt 0.968750 0.375000 +vt 0.968750 0.312500 +vt 0.968750 0.250000 +vt 0.968750 0.187500 +vt 0.968750 0.125000 +vt 0.968750 0.062500 +vt 0.937500 0.937500 +vt 0.937500 0.875000 +vt 0.937500 0.812500 +vt 0.937500 0.750000 +vt 0.937500 0.687500 +vt 0.937500 0.625000 +vt 0.937500 0.562500 +vt 0.937500 0.500000 +vt 0.937500 0.437500 +vt 0.937500 0.375000 +vt 0.937500 0.312500 +vt 0.937500 0.250000 +vt 0.937500 0.187500 +vt 0.937500 0.125000 +vt 0.937500 0.062500 +vt 0.906250 0.937500 +vt 0.906250 0.875000 +vt 0.906250 0.812500 +vt 0.906250 0.750000 +vt 0.906250 0.687500 +vt 0.906250 0.625000 +vt 0.906250 0.562500 +vt 0.906250 0.500000 +vt 0.906250 0.437500 +vt 0.906250 0.375000 +vt 0.906250 0.312500 +vt 0.906250 0.250000 +vt 0.906250 0.187500 +vt 0.906250 0.125000 +vt 0.906250 0.062500 +vt 0.875000 0.937500 +vt 0.875000 0.875000 +vt 0.875000 0.812500 +vt 0.875000 0.750000 +vt 0.875000 0.687500 +vt 0.875000 0.625000 +vt 0.875000 0.562500 +vt 0.875000 0.500000 +vt 0.875000 0.437500 +vt 0.875000 0.375000 +vt 0.875000 0.312500 +vt 0.875000 0.250000 +vt 0.875000 0.187500 +vt 0.875000 0.125000 +vt 0.875000 0.062500 +vt 0.843750 0.937500 +vt 0.843750 0.875000 +vt 0.843750 0.812500 +vt 0.843750 0.750000 +vt 0.843750 0.687500 +vt 0.843750 0.625000 +vt 0.843750 0.562500 +vt 0.843750 0.500000 +vt 0.843750 0.437500 +vt 0.843750 0.375000 +vt 0.843750 0.312500 +vt 0.843750 0.250000 +vt 0.843750 0.187500 +vt 0.843750 0.125000 +vt 0.843750 0.062500 +vt 0.812500 0.937500 +vt 0.812500 0.875000 +vt 0.812500 0.812500 +vt 0.812500 0.750000 +vt 0.812500 0.687500 +vt 0.812500 0.625000 +vt 0.812500 0.562500 +vt 0.812500 0.500000 +vt 0.812500 0.437500 +vt 0.812500 0.375000 +vt 0.812500 0.312500 +vt 0.812500 0.250000 +vt 0.812500 0.187500 +vt 0.812500 0.125000 +vt 0.812500 0.062500 +vt 0.781250 0.937500 +vt 0.781250 0.875000 +vt 0.781250 0.812500 +vt 0.781250 0.750000 +vt 0.781250 0.687500 +vt 0.781250 0.625000 +vt 0.781250 0.562500 +vt 0.781250 0.500000 +vt 0.781250 0.437500 +vt 0.781250 0.375000 +vt 0.781250 0.312500 +vt 0.781250 0.250000 +vt 0.781250 0.187500 +vt 0.781250 0.125000 +vt 0.781250 0.062500 +vt 0.750000 0.937500 +vt 0.750000 0.875000 +vt 0.750000 0.750000 +vt 0.750000 0.625000 +vt 0.750000 0.375000 +vt 0.750000 0.250000 +vt 0.750000 0.187500 +vt 0.750000 0.125000 +vt 0.750000 0.062500 +s 1 +f 1447/1678/1447 1454/1685/1448 1455/1686/1449 +f 1924/2232/1450 1463/1694/1451 1452/1683/1452 +f 1922/2230/1453 1455/1686/1449 1456/1687/1454 +f 1452/1683/1452 1464/1695/1455 1925/2233/1456 +f 1448/1679/1457 1456/1687/1454 1457/1688/1458 +f 1925/2233/1456 1465/1696/1459 1926/2234/1460 +f 1923/2231/1461 1457/1688/1458 1458/1689/1462 +f 1926/2234/1460 1466/1697/1463 1927/2235/1464 +f 1449/1680/1465 1458/1689/1462 1459/1690/1466 +f 1927/2235/1464 1467/1698/1467 1928/2236/1468 +f 1449/1680/1465 1460/1691/1469 1450/1681/1470 +f 1920/2228/1471 1528/1759/1472 1453/1684/1473 +f 1754/2016/1474 1928/2236/1468 1467/1698/1467 +f 1450/1681/1470 1461/1692/1475 1451/1682/1476 +f 1921/2229/1477 1453/1684/1473 1454/1685/1448 +f 1451/1682/1476 1462/1693/1478 1924/2232/1450 +f 1453/1684/1473 1469/1700/1479 1454/1685/1448 +f 1461/1692/1475 1477/1708/1480 1462/1693/1478 +f 1454/1685/1448 1470/1701/1481 1455/1686/1449 +f 1462/1693/1478 1478/1709/1482 1463/1694/1451 +f 1455/1686/1449 1471/1702/1483 1456/1687/1454 +f 1463/1694/1451 1479/1710/1484 1464/1695/1455 +f 1456/1687/1454 1472/1703/1485 1457/1688/1458 +f 1465/1696/1459 1479/1710/1484 1480/1711/1486 +f 1457/1688/1458 1473/1704/1487 1458/1689/1462 +f 1466/1697/1463 1480/1711/1486 1481/1712/1488 +f 1459/1690/1466 1473/1704/1487 1474/1705/1489 +f 1466/1697/1463 1482/1713/1490 1467/1698/1467 +f 1459/1690/1466 1475/1706/1491 1460/1691/1469 +f 1453/1684/1473 1528/1760/1472 1468/1699/1492 +f 1754/2017/1474 1467/1698/1467 1482/1713/1490 +f 1460/1691/1469 1476/1707/1493 1461/1692/1475 +f 1472/1703/1485 1488/1719/1494 1473/1704/1487 +f 1480/1711/1486 1496/1727/1495 1481/1712/1488 +f 1473/1704/1487 1489/1720/1496 1474/1705/1489 +f 1481/1712/1488 1497/1728/1497 1482/1713/1490 +f 1474/1705/1489 1490/1721/1498 1475/1706/1491 +f 1468/1699/1492 1528/1761/1472 1483/1714/1499 +f 1754/2018/1474 1482/1713/1490 1497/1728/1497 +f 1476/1707/1493 1490/1721/1498 1491/1722/1500 +f 1468/1699/1492 1484/1715/1501 1469/1700/1479 +f 1476/1707/1493 1492/1723/1502 1477/1708/1480 +f 1469/1700/1479 1485/1716/1503 1470/1701/1481 +f 1478/1709/1482 1492/1723/1502 1493/1724/1504 +f 1470/1701/1481 1486/1717/1505 1471/1702/1483 +f 1479/1710/1484 1493/1724/1504 1494/1725/1506 +f 1471/1702/1483 1487/1718/1507 1472/1703/1485 +f 1480/1711/1486 1494/1725/1506 1495/1726/1508 +f 1491/1722/1500 1507/1738/1509 1492/1723/1502 +f 1484/1715/1501 1500/1731/1510 1485/1716/1503 +f 1493/1724/1504 1507/1738/1509 1508/1739/1511 +f 1485/1716/1503 1501/1732/1512 1486/1717/1505 +f 1493/1724/1504 1509/1740/1513 1494/1725/1506 +f 1486/1717/1505 1502/1733/1514 1487/1718/1507 +f 1494/1725/1506 1510/1741/1515 1495/1726/1508 +f 1487/1718/1507 1503/1734/1516 1488/1719/1494 +f 1495/1726/1508 1511/1742/1517 1496/1727/1495 +f 1489/1720/1496 1503/1734/1516 1504/1735/1518 +f 1496/1727/1495 1512/1743/1519 1497/1728/1497 +f 1490/1721/1498 1504/1735/1518 1505/1736/1520 +f 1483/1714/1499 1528/1762/1472 1498/1729/1521 +f 1754/2019/1474 1497/1728/1497 1512/1743/1519 +f 1490/1721/1498 1506/1737/1522 1491/1722/1500 +f 1483/1714/1499 1499/1730/1523 1484/1715/1501 +f 1511/1742/1517 1525/1756/1524 1526/1757/1525 +f 1504/1735/1518 1518/1749/1526 1519/1750/1527 +f 1511/1742/1517 1527/1758/1528 1512/1743/1519 +f 1505/1736/1520 1519/1750/1527 1520/1751/1529 +f 1498/1729/1521 1528/1763/1472 1513/1744/1530 +f 1754/2020/1474 1512/1743/1519 1527/1758/1528 +f 1505/1736/1520 1521/1752/1531 1506/1737/1522 +f 1498/1729/1521 1514/1745/1532 1499/1730/1523 +f 1506/1737/1522 1522/1753/1533 1507/1738/1509 +f 1499/1730/1523 1515/1746/1534 1500/1731/1510 +f 1508/1739/1511 1522/1753/1533 1523/1754/1535 +f 1500/1731/1510 1516/1747/1536 1501/1732/1512 +f 1508/1739/1511 1524/1755/1537 1509/1740/1513 +f 1502/1733/1514 1516/1747/1536 1517/1748/1538 +f 1509/1740/1513 1525/1756/1524 1510/1741/1515 +f 1502/1733/1514 1518/1749/1526 1503/1734/1516 +f 1514/1745/1532 1531/1793/1539 1515/1746/1534 +f 1523/1754/1535 1538/1800/1540 1539/1801/1541 +f 1515/1746/1534 1532/1794/1542 1516/1747/1536 +f 1523/1754/1535 1540/1802/1543 1524/1755/1537 +f 1517/1748/1538 1532/1794/1542 1533/1795/1544 +f 1524/1755/1537 1541/1803/1545 1525/1756/1524 +f 1517/1748/1538 1534/1796/1546 1518/1749/1526 +f 1525/1756/1524 1542/1804/1547 1526/1757/1525 +f 1519/1750/1527 1534/1796/1546 1535/1797/1548 +f 1527/1758/1528 1542/1804/1547 1543/1805/1549 +f 1520/1751/1529 1535/1797/1548 1536/1798/1550 +f 1513/1744/1530 1528/1764/1472 1529/1791/1551 +f 1754/2021/1474 1527/1758/1528 1543/1805/1549 +f 1520/1751/1529 1537/1799/1552 1521/1752/1531 +f 1513/1744/1530 1530/1792/1553 1514/1745/1532 +f 1521/1752/1531 1538/1800/1540 1522/1753/1533 +f 1535/1797/1548 1549/1811/1554 1550/1812/1555 +f 1542/1804/1547 1558/1820/1556 1543/1805/1549 +f 1536/1798/1550 1550/1812/1555 1551/1813/1557 +f 1529/1791/1551 1528/1765/1472 1544/1806/1558 +f 1754/2022/1474 1543/1805/1549 1558/1820/1556 +f 1536/1798/1550 1552/1814/1559 1537/1799/1552 +f 1530/1792/1553 1544/1806/1558 1545/1807/1560 +f 1537/1799/1552 1553/1815/1561 1538/1800/1540 +f 1530/1792/1553 1546/1808/1562 1531/1793/1539 +f 1539/1801/1541 1553/1815/1561 1554/1816/1563 +f 1531/1793/1539 1547/1809/1564 1532/1794/1542 +f 1539/1801/1541 1555/1817/1565 1540/1802/1543 +f 1533/1795/1544 1547/1809/1564 1548/1810/1566 +f 1541/1803/1545 1555/1817/1565 1556/1818/1567 +f 1533/1795/1544 1549/1811/1554 1534/1796/1546 +f 1542/1804/1547 1556/1818/1567 1557/1819/1568 +f 1554/1816/1563 1568/1830/1569 1569/1831/1570 +f 1546/1808/1562 1562/1824/1571 1547/1809/1564 +f 1554/1816/1563 1570/1832/1572 1555/1817/1565 +f 1548/1810/1566 1562/1824/1571 1563/1825/1573 +f 1556/1818/1567 1570/1832/1572 1571/1833/1574 +f 1548/1810/1566 1564/1826/1575 1549/1811/1554 +f 1556/1818/1567 1572/1834/1576 1557/1819/1568 +f 1550/1812/1555 1564/1826/1575 1565/1827/1577 +f 1557/1819/1568 1573/1835/1578 1558/1820/1556 +f 1551/1813/1557 1565/1827/1577 1566/1828/1579 +f 1544/1806/1558 1528/1766/1472 1559/1821/1580 +f 1754/2023/1474 1558/1820/1556 1573/1835/1578 +f 1551/1813/1557 1567/1829/1581 1552/1814/1559 +f 1545/1807/1560 1559/1821/1580 1560/1822/1582 +f 1552/1814/1559 1568/1830/1569 1553/1815/1561 +f 1545/1807/1560 1561/1823/1583 1546/1808/1562 +f 1572/1834/1576 1588/1850/1584 1573/1835/1578 +f 1566/1828/1579 1580/1842/1585 1581/1843/1586 +f 1559/1821/1580 1528/1767/1472 1574/1836/1587 +f 1754/2024/1474 1573/1835/1578 1588/1850/1584 +f 1566/1828/1579 1582/1844/1588 1567/1829/1581 +f 1559/1821/1580 1575/1837/1589 1560/1822/1582 +f 1567/1829/1581 1583/1845/1590 1568/1830/1569 +f 1560/1822/1582 1576/1838/1591 1561/1823/1583 +f 1569/1831/1570 1583/1845/1590 1584/1846/1592 +f 1561/1823/1583 1577/1839/1593 1562/1824/1571 +f 1569/1831/1570 1585/1847/1594 1570/1832/1572 +f 1563/1825/1573 1577/1839/1593 1578/1840/1595 +f 1571/1833/1574 1585/1847/1594 1586/1848/1596 +f 1563/1825/1573 1579/1841/1597 1564/1826/1575 +f 1571/1833/1574 1587/1849/1598 1572/1834/1576 +f 1565/1827/1577 1579/1841/1597 1580/1842/1585 +f 1584/1846/1592 1600/1862/1599 1585/1847/1594 +f 1578/1840/1595 1592/1854/1600 1593/1855/1601 +f 1586/1848/1596 1600/1862/1599 1601/1863/1602 +f 1578/1840/1595 1594/1856/1603 1579/1841/1597 +f 1586/1848/1596 1602/1864/1604 1587/1849/1598 +f 1580/1842/1585 1594/1856/1603 1595/1857/1605 +f 1588/1850/1584 1602/1864/1604 1603/1865/1606 +f 1581/1843/1586 1595/1857/1605 1596/1858/1607 +f 1574/1836/1587 1528/1768/1472 1589/1851/1608 +f 1754/2025/1474 1588/1850/1584 1603/1865/1606 +f 1581/1843/1586 1597/1859/1609 1582/1844/1588 +f 1574/1836/1587 1590/1852/1610 1575/1837/1589 +f 1582/1844/1588 1598/1860/1611 1583/1845/1590 +f 1576/1838/1591 1590/1852/1610 1591/1853/1612 +f 1584/1846/1592 1598/1860/1611 1599/1861/1613 +f 1576/1838/1591 1592/1854/1600 1577/1839/1593 +f 1589/1851/1608 1528/1769/1472 1604/1866/1614 +f 1754/2026/1474 1603/1865/1606 1618/1880/1615 +f 1596/1858/1607 1612/1874/1616 1597/1859/1609 +f 1589/1851/1608 1605/1867/1617 1590/1852/1610 +f 1597/1859/1609 1613/1875/1618 1598/1860/1611 +f 1591/1853/1612 1605/1867/1617 1606/1868/1619 +f 1599/1861/1613 1613/1875/1618 1614/1876/1620 +f 1591/1853/1612 1607/1869/1621 1592/1854/1600 +f 1599/1861/1613 1615/1877/1622 1600/1862/1599 +f 1593/1855/1601 1607/1869/1621 1608/1870/1623 +f 1601/1863/1602 1615/1877/1622 1616/1878/1624 +f 1593/1855/1601 1609/1871/1625 1594/1856/1603 +f 1601/1863/1602 1617/1879/1626 1602/1864/1604 +f 1595/1857/1605 1609/1871/1625 1610/1872/1627 +f 1602/1864/1604 1618/1880/1615 1603/1865/1606 +f 1596/1858/1607 1610/1872/1627 1611/1873/1628 +f 1608/1870/1623 1622/1884/1629 1623/1885/1630 +f 1615/1877/1622 1631/1893/1631 1616/1878/1624 +f 1608/1870/1623 1624/1886/1632 1609/1871/1625 +f 1617/1879/1626 1631/1893/1631 1632/1894/1633 +f 1610/1872/1627 1624/1886/1632 1625/1887/1634 +f 1618/1880/1615 1632/1894/1633 1633/1895/1635 +f 1611/1873/1628 1625/1887/1634 1626/1888/1636 +f 1604/1866/1614 1528/1770/1472 1619/1881/1637 +f 1754/2027/1474 1618/1880/1615 1633/1895/1635 +f 1611/1873/1628 1627/1889/1638 1612/1874/1616 +f 1604/1866/1614 1620/1882/1639 1605/1867/1617 +f 1612/1874/1616 1628/1890/1640 1613/1875/1618 +f 1605/1867/1617 1621/1883/1641 1606/1868/1619 +f 1614/1876/1620 1628/1890/1640 1629/1891/1642 +f 1606/1868/1619 1622/1884/1629 1607/1869/1621 +f 1614/1876/1620 1630/1892/1643 1615/1877/1622 +f 1626/1888/1636 1642/1904/1644 1627/1889/1638 +f 1620/1882/1639 1634/1896/1645 1635/1897/1646 +f 1627/1889/1638 1643/1905/1647 1628/1890/1640 +f 1621/1883/1641 1635/1897/1646 1636/1898/1648 +f 1629/1891/1642 1643/1905/1647 1644/1906/1649 +f 1621/1883/1641 1637/1899/1650 1622/1884/1629 +f 1629/1891/1642 1645/1907/1651 1630/1892/1643 +f 1623/1885/1630 1637/1899/1650 1638/1900/1652 +f 1631/1893/1631 1645/1907/1651 1646/1908/1653 +f 1623/1885/1630 1639/1901/1654 1624/1886/1632 +f 1631/1893/1631 1647/1909/1655 1632/1894/1633 +f 1625/1887/1634 1639/1901/1654 1640/1902/1656 +f 1632/1894/1633 1648/1910/1657 1633/1895/1635 +f 1626/1888/1636 1640/1902/1656 1641/1903/1658 +f 1619/1881/1637 1528/1771/1472 1634/1896/1645 +f 1754/2028/1474 1633/1895/1635 1648/1910/1657 +f 1646/1908/1653 1660/1922/1659 1661/1923/1660 +f 1638/1900/1652 1654/1916/1661 1639/1901/1654 +f 1646/1908/1653 1662/1924/1662 1647/1909/1655 +f 1640/1902/1656 1654/1916/1661 1655/1917/1663 +f 1647/1909/1655 1663/1925/1664 1648/1910/1657 +f 1641/1903/1658 1655/1917/1663 1656/1918/1665 +f 1634/1896/1645 1528/1772/1472 1649/1911/1666 +f 1754/2029/1474 1648/1910/1657 1663/1925/1664 +f 1641/1903/1658 1657/1919/1667 1642/1904/1644 +f 1634/1896/1645 1650/1912/1668 1635/1897/1646 +f 1642/1904/1644 1658/1920/1669 1643/1905/1647 +f 1635/1897/1646 1651/1913/1670 1636/1898/1648 +f 1644/1906/1649 1658/1920/1669 1659/1921/1671 +f 1636/1898/1648 1652/1914/1672 1637/1899/1650 +f 1644/1906/1649 1660/1922/1659 1645/1907/1651 +f 1638/1900/1652 1652/1914/1672 1653/1915/1673 +f 1649/1911/1666 1665/1927/1674 1650/1912/1668 +f 1657/1919/1667 1673/1935/1675 1658/1920/1669 +f 1650/1912/1668 1666/1928/1676 1651/1913/1670 +f 1659/1921/1671 1673/1935/1675 1674/1936/1677 +f 1651/1913/1670 1667/1929/1678 1652/1914/1672 +f 1659/1921/1671 1675/1937/1679 1660/1922/1659 +f 1653/1915/1673 1667/1929/1678 1668/1930/1680 +f 1661/1923/1660 1675/1937/1679 1676/1938/1681 +f 1653/1915/1673 1669/1931/1682 1654/1916/1661 +f 1661/1923/1660 1677/1939/1683 1662/1924/1662 +f 1655/1917/1663 1669/1931/1682 1670/1932/1684 +f 1662/1924/1662 1678/1940/1685 1663/1925/1664 +f 1656/1918/1665 1670/1932/1684 1671/1933/1686 +f 1649/1911/1666 1528/1773/1472 1664/1926/1687 +f 1754/2030/1474 1663/1925/1664 1678/1940/1685 +f 1656/1918/1665 1672/1934/1688 1657/1919/1667 +f 1668/1930/1680 1684/1946/1689 1669/1931/1682 +f 1676/1938/1681 1692/1954/1690 1677/1939/1683 +f 1670/1932/1684 1684/1946/1689 1685/1947/1691 +f 1678/1940/1685 1692/1954/1690 1693/1955/1692 +f 1671/1933/1686 1685/1947/1691 1686/1948/1693 +f 1664/1926/1687 1528/1774/1472 1679/1941/1694 +f 1754/2031/1474 1678/1940/1685 1693/1955/1692 +f 1671/1933/1686 1687/1949/1695 1672/1934/1688 +f 1664/1926/1687 1680/1942/1696 1665/1927/1674 +f 1672/1934/1688 1688/1950/1697 1673/1935/1675 +f 1666/1928/1676 1680/1942/1696 1681/1943/1698 +f 1674/1936/1677 1688/1950/1697 1689/1951/1699 +f 1666/1928/1676 1682/1944/1700 1667/1929/1678 +f 1674/1936/1677 1690/1952/1701 1675/1937/1679 +f 1668/1930/1680 1682/1944/1700 1683/1945/1702 +f 1676/1938/1681 1690/1952/1701 1691/1953/1703 +f 1687/1949/1695 1703/1965/1704 1688/1950/1697 +f 1680/1942/1696 1696/1958/1705 1681/1943/1698 +f 1689/1951/1699 1703/1965/1704 1704/1966/1706 +f 1681/1943/1698 1697/1959/1707 1682/1944/1700 +f 1689/1951/1699 1705/1967/1708 1690/1952/1701 +f 1683/1945/1702 1697/1959/1707 1698/1960/1709 +f 1691/1953/1703 1705/1967/1708 1706/1968/1710 +f 1683/1945/1702 1699/1961/1711 1684/1946/1689 +f 1691/1953/1703 1707/1969/1712 1692/1954/1690 +f 1685/1947/1691 1699/1961/1711 1700/1962/1713 +f 1692/1954/1690 1708/1970/1714 1693/1955/1692 +f 1686/1948/1693 1700/1962/1713 1701/1963/1715 +f 1679/1941/1694 1528/1775/1472 1694/1956/1716 +f 1754/2032/1474 1693/1955/1692 1708/1970/1714 +f 1686/1948/1693 1702/1964/1717 1687/1949/1695 +f 1679/1941/1694 1695/1957/1718 1680/1942/1696 +f 1706/1968/1710 1722/1984/1719 1707/1969/1712 +f 1700/1962/1713 1714/1976/1720 1715/1977/1721 +f 1707/1969/1712 1723/1985/1722 1708/1970/1714 +f 1701/1963/1715 1715/1977/1721 1716/1978/1723 +f 1694/1956/1716 1528/1776/1472 1709/1971/1724 +f 1754/2033/1474 1708/1970/1714 1723/1985/1722 +f 1701/1963/1715 1717/1979/1725 1702/1964/1717 +f 1695/1957/1718 1709/1971/1724 1710/1972/1726 +f 1702/1964/1717 1718/1980/1727 1703/1965/1704 +f 1695/1957/1718 1711/1973/1728 1696/1958/1705 +f 1704/1966/1706 1718/1980/1727 1719/1981/1729 +f 1696/1958/1705 1712/1974/1730 1697/1959/1707 +f 1704/1966/1706 1720/1982/1731 1705/1967/1708 +f 1698/1960/1709 1712/1974/1730 1713/1975/1732 +f 1706/1968/1710 1720/1982/1731 1721/1983/1733 +f 1698/1960/1709 1714/1976/1720 1699/1961/1711 +f 1719/1981/1729 1733/1995/1734 1734/1996/1735 +f 1711/1973/1728 1727/1989/1736 1712/1974/1730 +f 1719/1981/1729 1735/1997/1737 1720/1982/1731 +f 1713/1975/1732 1727/1989/1736 1728/1990/1738 +f 1721/1983/1733 1735/1997/1737 1736/1998/1739 +f 1713/1975/1732 1729/1991/1740 1714/1976/1720 +f 1721/1983/1733 1737/1999/1741 1722/1984/1719 +f 1715/1977/1721 1729/1991/1740 1730/1992/1742 +f 1723/1985/1722 1737/1999/1741 1738/2000/1743 +f 1716/1978/1723 1730/1992/1742 1731/1993/1744 +f 1709/1971/1724 1528/1777/1472 1724/1986/1745 +f 1754/2034/1474 1723/1985/1722 1738/2000/1743 +f 1716/1978/1723 1732/1994/1746 1717/1979/1725 +f 1710/1972/1726 1724/1986/1745 1725/1987/1747 +f 1717/1979/1725 1733/1995/1734 1718/1980/1727 +f 1710/1972/1726 1726/1988/1748 1711/1973/1728 +f 1738/2000/1743 1752/2014/1749 1753/2015/1750 +f 1731/1993/1744 1745/2007/1751 1746/2008/1752 +f 1724/1986/1745 1528/1778/1472 1739/2001/1753 +f 1754/2035/1474 1738/2000/1743 1753/2015/1750 +f 1731/1993/1744 1747/2009/1754 1732/1994/1746 +f 1724/1986/1745 1740/2002/1755 1725/1987/1747 +f 1732/1994/1746 1748/2010/1756 1733/1995/1734 +f 1725/1987/1747 1741/2003/1757 1726/1988/1748 +f 1734/1996/1735 1748/2010/1756 1749/2011/1758 +f 1726/1988/1748 1742/2004/1759 1727/1989/1736 +f 1734/1996/1735 1750/2012/1760 1735/1997/1737 +f 1728/1990/1738 1742/2004/1759 1743/2005/1761 +f 1736/1998/1739 1750/2012/1760 1751/2013/1762 +f 1728/1990/1738 1744/2006/1763 1729/1991/1740 +f 1736/1998/1739 1752/2014/1749 1737/1999/1741 +f 1730/1992/1742 1744/2006/1763 1745/2007/1751 +f 1741/2003/1757 1758/2051/1764 1742/2004/1759 +f 1749/2011/1758 1766/2059/1765 1750/2012/1760 +f 1743/2005/1761 1758/2051/1764 1759/2052/1766 +f 1751/2013/1762 1766/2059/1765 1767/2060/1767 +f 1743/2005/1761 1760/2053/1768 1744/2006/1763 +f 1751/2013/1762 1768/2061/1769 1752/2014/1749 +f 1745/2007/1751 1760/2053/1768 1761/2054/1770 +f 1752/2014/1749 1769/2062/1771 1753/2015/1750 +f 1746/2008/1752 1761/2054/1770 1762/2055/1772 +f 1739/2001/1753 1528/1779/1472 1755/2048/1773 +f 1754/2036/1474 1753/2015/1750 1769/2062/1771 +f 1746/2008/1752 1763/2056/1774 1747/2009/1754 +f 1739/2001/1753 1756/2049/1775 1740/2002/1755 +f 1747/2009/1754 1764/2057/1776 1748/2010/1756 +f 1740/2002/1755 1757/2050/1777 1741/2003/1757 +f 1749/2011/1758 1764/2057/1776 1765/2058/1778 +f 1762/2055/1772 1776/2069/1779 1777/2070/1780 +f 1755/2048/1773 1528/1780/1472 1770/2063/1781 +f 1754/2037/1474 1769/2062/1771 1784/2077/1782 +f 1762/2055/1772 1778/2071/1783 1763/2056/1774 +f 1755/2048/1773 1771/2064/1784 1756/2049/1775 +f 1763/2056/1774 1779/2072/1785 1764/2057/1776 +f 1756/2049/1775 1772/2065/1786 1757/2050/1777 +f 1765/2058/1778 1779/2072/1785 1780/2073/1787 +f 1757/2050/1777 1773/2066/1788 1758/2051/1764 +f 1765/2058/1778 1781/2074/1789 1766/2059/1765 +f 1758/2051/1764 1774/2067/1790 1759/2052/1766 +f 1767/2060/1767 1781/2074/1789 1782/2075/1791 +f 1759/2052/1766 1775/2068/1792 1760/2053/1768 +f 1767/2060/1767 1783/2076/1793 1768/2061/1769 +f 1761/2054/1770 1775/2068/1792 1776/2069/1779 +f 1768/2061/1769 1784/2077/1782 1769/2062/1771 +f 1780/2073/1787 1796/2089/1794 1781/2074/1789 +f 1774/2067/1790 1788/2081/1795 1789/2082/1796 +f 1782/2075/1791 1796/2089/1794 1797/2090/1797 +f 1774/2067/1790 1790/2083/1798 1775/2068/1792 +f 1782/2075/1791 1798/2091/1799 1783/2076/1793 +f 1776/2069/1779 1790/2083/1798 1791/2084/1800 +f 1783/2076/1793 1799/2092/1801 1784/2077/1782 +f 1777/2070/1780 1791/2084/1800 1792/2085/1802 +f 1770/2063/1781 1528/1781/1472 1785/2078/1803 +f 1754/2038/1474 1784/2077/1782 1799/2092/1801 +f 1777/2070/1780 1793/2086/1804 1778/2071/1783 +f 1770/2063/1781 1786/2079/1805 1771/2064/1784 +f 1778/2071/1783 1794/2087/1806 1779/2072/1785 +f 1771/2064/1784 1787/2080/1807 1772/2065/1786 +f 1780/2073/1787 1794/2087/1806 1795/2088/1808 +f 1772/2065/1786 1788/2081/1795 1773/2066/1788 +f 1785/2078/1803 1528/1782/1472 1800/2093/1809 +f 1754/2039/1474 1799/2092/1801 1814/2122/1810 +f 1792/2085/1802 1808/2109/1811 1793/2086/1804 +f 1785/2078/1803 1801/2095/1812 1786/2079/1805 +f 1793/2086/1804 1809/2111/1813 1794/2087/1806 +f 1787/2080/1807 1801/2095/1812 1802/2097/1814 +f 1795/2088/1808 1809/2111/1813 1810/2113/1815 +f 1787/2080/1807 1803/2099/1816 1788/2081/1795 +f 1795/2088/1808 1811/2115/1817 1796/2089/1794 +f 1789/2082/1796 1803/2099/1816 1804/2101/1818 +f 1797/2090/1797 1811/2115/1817 1812/2117/1819 +f 1789/2082/1796 1805/2103/1820 1790/2083/1798 +f 1797/2090/1797 1813/2119/1821 1798/2091/1799 +f 1791/2084/1800 1805/2103/1820 1806/2105/1822 +f 1798/2091/1799 1814/2122/1810 1799/2092/1801 +f 1792/2085/1802 1806/2105/1822 1807/2107/1823 +f 1804/2102/1818 1818/2126/1824 1819/2127/1825 +f 1812/2118/1819 1826/2134/1826 1827/2135/1827 +f 1804/2102/1818 1820/2128/1828 1805/2104/1820 +f 1812/2118/1819 1828/2136/1829 1813/2120/1821 +f 1806/2106/1822 1820/2128/1828 1821/2129/1830 +f 1813/2120/1821 1829/2137/1831 1814/2121/1810 +f 1807/2108/1823 1821/2129/1830 1822/2130/1832 +f 1800/2094/1809 1528/1783/1472 1815/2123/1833 +f 1754/2040/1474 1814/2121/1810 1829/2137/1831 +f 1807/2108/1823 1823/2131/1834 1808/2110/1811 +f 1800/2094/1809 1816/2124/1835 1801/2096/1812 +f 1808/2110/1811 1824/2132/1836 1809/2112/1813 +f 1802/2098/1814 1816/2124/1835 1817/2125/1837 +f 1810/2114/1815 1824/2132/1836 1825/2133/1838 +f 1802/2098/1814 1818/2126/1824 1803/2100/1816 +f 1810/2114/1815 1826/2134/1826 1811/2116/1817 +f 1822/2130/1832 1838/2146/1839 1823/2131/1834 +f 1815/2123/1833 1831/2139/1840 1816/2124/1835 +f 1823/2131/1834 1839/2147/1841 1824/2132/1836 +f 1817/2125/1837 1831/2139/1840 1832/2140/1842 +f 1825/2133/1838 1839/2147/1841 1840/2148/1843 +f 1817/2125/1837 1833/2141/1844 1818/2126/1824 +f 1825/2133/1838 1841/2149/1845 1826/2134/1826 +f 1819/2127/1825 1833/2141/1844 1834/2142/1846 +f 1827/2135/1827 1841/2149/1845 1842/2150/1847 +f 1819/2127/1825 1835/2143/1848 1820/2128/1828 +f 1827/2135/1827 1843/2151/1849 1828/2136/1829 +f 1821/2129/1830 1835/2143/1848 1836/2144/1850 +f 1828/2136/1829 1844/2152/1851 1829/2137/1831 +f 1822/2130/1832 1836/2144/1850 1837/2145/1852 +f 1815/2123/1833 1528/1784/1472 1830/2138/1853 +f 1754/2041/1474 1829/2137/1831 1844/2152/1851 +f 1842/2150/1847 1856/2164/1854 1857/2165/1855 +f 1834/2142/1846 1850/2158/1856 1835/2143/1848 +f 1843/2151/1849 1857/2165/1855 1858/2166/1857 +f 1836/2144/1850 1850/2158/1856 1851/2159/1858 +f 1843/2151/1849 1859/2167/1859 1844/2152/1851 +f 1837/2145/1852 1851/2159/1858 1852/2160/1860 +f 1830/2138/1853 1528/1785/1472 1845/2153/1861 +f 1754/2042/1474 1844/2152/1851 1859/2167/1859 +f 1837/2145/1852 1853/2161/1862 1838/2146/1839 +f 1831/2139/1840 1845/2153/1861 1846/2154/1863 +f 1838/2146/1839 1854/2162/1864 1839/2147/1841 +f 1831/2139/1840 1847/2155/1865 1832/2140/1842 +f 1840/2148/1843 1854/2162/1864 1855/2163/1866 +f 1832/2140/1842 1848/2156/1867 1833/2141/1844 +f 1840/2148/1843 1856/2164/1854 1841/2149/1845 +f 1834/2142/1846 1848/2156/1867 1849/2157/1868 +f 1853/2161/1862 1869/2177/1869 1854/2162/1864 +f 1847/2155/1865 1861/2169/1870 1862/2170/1871 +f 1855/2163/1866 1869/2177/1869 1870/2178/1872 +f 1847/2155/1865 1863/2171/1873 1848/2156/1867 +f 1855/2163/1866 1871/2179/1874 1856/2164/1854 +f 1849/2157/1868 1863/2171/1873 1864/2172/1875 +f 1857/2165/1855 1871/2179/1874 1872/2180/1876 +f 1849/2157/1868 1865/2173/1877 1850/2158/1856 +f 1857/2165/1855 1873/2181/1878 1858/2166/1857 +f 1851/2159/1858 1865/2173/1877 1866/2174/1879 +f 1858/2166/1857 1874/2182/1880 1859/2167/1859 +f 1852/2160/1860 1866/2174/1879 1867/2175/1881 +f 1845/2153/1861 1528/1786/1472 1860/2168/1882 +f 1754/2043/1474 1859/2167/1859 1874/2182/1880 +f 1852/2160/1860 1868/2176/1883 1853/2161/1862 +f 1845/2153/1861 1861/2169/1870 1846/2154/1863 +f 1872/2180/1876 1888/2196/1884 1873/2181/1878 +f 1866/2174/1879 1880/2188/1885 1881/2189/1886 +f 1873/2181/1878 1889/2197/1887 1874/2182/1880 +f 1867/2175/1881 1881/2189/1886 1882/2190/1888 +f 1860/2168/1882 1528/1787/1472 1875/2183/1889 +f 1754/2044/1474 1874/2182/1880 1889/2197/1887 +f 1867/2175/1881 1883/2191/1890 1868/2176/1883 +f 1860/2168/1882 1876/2184/1891 1861/2169/1870 +f 1868/2176/1883 1884/2192/1892 1869/2177/1869 +f 1862/2170/1871 1876/2184/1891 1877/2185/1893 +f 1870/2178/1872 1884/2192/1892 1885/2193/1894 +f 1862/2170/1871 1878/2186/1895 1863/2171/1873 +f 1870/2178/1872 1886/2194/1896 1871/2179/1874 +f 1864/2172/1875 1878/2186/1895 1879/2187/1897 +f 1872/2180/1876 1886/2194/1896 1887/2195/1898 +f 1864/2172/1875 1880/2188/1885 1865/2173/1877 +f 1876/2184/1891 1892/2200/1899 1877/2185/1893 +f 1885/2193/1894 1899/2207/1900 1900/2208/1901 +f 1877/2185/1893 1893/2201/1902 1878/2186/1895 +f 1885/2193/1894 1901/2209/1903 1886/2194/1896 +f 1879/2187/1897 1893/2201/1902 1894/2202/1904 +f 1887/2195/1898 1901/2209/1903 1902/2210/1905 +f 1879/2187/1897 1895/2203/1906 1880/2188/1885 +f 1887/2195/1898 1903/2211/1907 1888/2196/1884 +f 1881/2189/1886 1895/2203/1906 1896/2204/1908 +f 1888/2196/1884 1904/2212/1909 1889/2197/1887 +f 1882/2190/1888 1896/2204/1908 1897/2205/1910 +f 1875/2183/1889 1528/1788/1472 1890/2198/1911 +f 1754/2045/1474 1889/2197/1887 1904/2212/1909 +f 1882/2190/1888 1898/2206/1912 1883/2191/1890 +f 1876/2184/1891 1890/2198/1911 1891/2199/1913 +f 1883/2191/1890 1899/2207/1900 1884/2192/1892 +f 1896/2204/1908 1910/2218/1914 1911/2219/1915 +f 1904/2212/1909 1918/2226/1916 1919/2227/1917 +f 1897/2205/1910 1911/2219/1915 1912/2220/1918 +f 1890/2198/1911 1528/1789/1472 1905/2213/1919 +f 1754/2046/1474 1904/2212/1909 1919/2227/1917 +f 1897/2205/1910 1913/2221/1920 1898/2206/1912 +f 1890/2198/1911 1906/2214/1921 1891/2199/1913 +f 1898/2206/1912 1914/2222/1922 1899/2207/1900 +f 1892/2200/1899 1906/2214/1921 1907/2215/1923 +f 1900/2208/1901 1914/2222/1922 1915/2223/1924 +f 1892/2200/1899 1908/2216/1925 1893/2201/1902 +f 1900/2208/1901 1916/2224/1926 1901/2209/1903 +f 1894/2202/1904 1908/2216/1925 1909/2217/1927 +f 1902/2210/1905 1916/2224/1926 1917/2225/1928 +f 1894/2202/1904 1910/2218/1914 1895/2203/1906 +f 1902/2210/1905 1918/2226/1916 1903/2211/1907 +f 1914/2222/1922 1452/1683/1452 1915/2223/1924 +f 1908/2216/1925 1447/1678/1447 1922/2230/1453 +f 1915/2223/1924 1925/2233/1456 1916/2224/1926 +f 1909/2217/1927 1922/2230/1453 1448/1679/1457 +f 1917/2225/1928 1925/2233/1456 1926/2234/1460 +f 1910/2218/1914 1448/1679/1457 1923/2231/1461 +f 1917/2225/1928 1927/2235/1464 1918/2226/1916 +f 1911/2219/1915 1923/2231/1461 1449/1680/1465 +f 1918/2226/1916 1928/2236/1468 1919/2227/1917 +f 1912/2220/1918 1449/1680/1465 1450/1681/1470 +f 1905/2213/1919 1528/1790/1472 1920/2228/1471 +f 1754/2047/1474 1919/2227/1917 1928/2236/1468 +f 1912/2220/1918 1451/1682/1476 1913/2221/1920 +f 1906/2214/1921 1920/2228/1471 1921/2229/1477 +f 1913/2221/1920 1924/2232/1450 1914/2222/1922 +f 1907/2215/1923 1921/2229/1477 1447/1678/1447 +f 1447/1678/1447 1921/2229/1477 1454/1685/1448 +f 1924/2232/1450 1462/1693/1478 1463/1694/1451 +f 1922/2230/1453 1447/1678/1447 1455/1686/1449 +f 1452/1683/1452 1463/1694/1451 1464/1695/1455 +f 1448/1679/1457 1922/2230/1453 1456/1687/1454 +f 1925/2233/1456 1464/1695/1455 1465/1696/1459 +f 1923/2231/1461 1448/1679/1457 1457/1688/1458 +f 1926/2234/1460 1465/1696/1459 1466/1697/1463 +f 1449/1680/1465 1923/2231/1461 1458/1689/1462 +f 1927/2235/1464 1466/1697/1463 1467/1698/1467 +f 1449/1680/1465 1459/1690/1466 1460/1691/1469 +f 1450/1681/1470 1460/1691/1469 1461/1692/1475 +f 1921/2229/1477 1920/2228/1471 1453/1684/1473 +f 1451/1682/1476 1461/1692/1475 1462/1693/1478 +f 1453/1684/1473 1468/1699/1492 1469/1700/1479 +f 1461/1692/1475 1476/1707/1493 1477/1708/1480 +f 1454/1685/1448 1469/1700/1479 1470/1701/1481 +f 1462/1693/1478 1477/1708/1480 1478/1709/1482 +f 1455/1686/1449 1470/1701/1481 1471/1702/1483 +f 1463/1694/1451 1478/1709/1482 1479/1710/1484 +f 1456/1687/1454 1471/1702/1483 1472/1703/1485 +f 1465/1696/1459 1464/1695/1455 1479/1710/1484 +f 1457/1688/1458 1472/1703/1485 1473/1704/1487 +f 1466/1697/1463 1465/1696/1459 1480/1711/1486 +f 1459/1690/1466 1458/1689/1462 1473/1704/1487 +f 1466/1697/1463 1481/1712/1488 1482/1713/1490 +f 1459/1690/1466 1474/1705/1489 1475/1706/1491 +f 1460/1691/1469 1475/1706/1491 1476/1707/1493 +f 1472/1703/1485 1487/1718/1507 1488/1719/1494 +f 1480/1711/1486 1495/1726/1508 1496/1727/1495 +f 1473/1704/1487 1488/1719/1494 1489/1720/1496 +f 1481/1712/1488 1496/1727/1495 1497/1728/1497 +f 1474/1705/1489 1489/1720/1496 1490/1721/1498 +f 1476/1707/1493 1475/1706/1491 1490/1721/1498 +f 1468/1699/1492 1483/1714/1499 1484/1715/1501 +f 1476/1707/1493 1491/1722/1500 1492/1723/1502 +f 1469/1700/1479 1484/1715/1501 1485/1716/1503 +f 1478/1709/1482 1477/1708/1480 1492/1723/1502 +f 1470/1701/1481 1485/1716/1503 1486/1717/1505 +f 1479/1710/1484 1478/1709/1482 1493/1724/1504 +f 1471/1702/1483 1486/1717/1505 1487/1718/1507 +f 1480/1711/1486 1479/1710/1484 1494/1725/1506 +f 1491/1722/1500 1506/1737/1522 1507/1738/1509 +f 1484/1715/1501 1499/1730/1523 1500/1731/1510 +f 1493/1724/1504 1492/1723/1502 1507/1738/1509 +f 1485/1716/1503 1500/1731/1510 1501/1732/1512 +f 1493/1724/1504 1508/1739/1511 1509/1740/1513 +f 1486/1717/1505 1501/1732/1512 1502/1733/1514 +f 1494/1725/1506 1509/1740/1513 1510/1741/1515 +f 1487/1718/1507 1502/1733/1514 1503/1734/1516 +f 1495/1726/1508 1510/1741/1515 1511/1742/1517 +f 1489/1720/1496 1488/1719/1494 1503/1734/1516 +f 1496/1727/1495 1511/1742/1517 1512/1743/1519 +f 1490/1721/1498 1489/1720/1496 1504/1735/1518 +f 1490/1721/1498 1505/1736/1520 1506/1737/1522 +f 1483/1714/1499 1498/1729/1521 1499/1730/1523 +f 1511/1742/1517 1510/1741/1515 1525/1756/1524 +f 1504/1735/1518 1503/1734/1516 1518/1749/1526 +f 1511/1742/1517 1526/1757/1525 1527/1758/1528 +f 1505/1736/1520 1504/1735/1518 1519/1750/1527 +f 1505/1736/1520 1520/1751/1529 1521/1752/1531 +f 1498/1729/1521 1513/1744/1530 1514/1745/1532 +f 1506/1737/1522 1521/1752/1531 1522/1753/1533 +f 1499/1730/1523 1514/1745/1532 1515/1746/1534 +f 1508/1739/1511 1507/1738/1509 1522/1753/1533 +f 1500/1731/1510 1515/1746/1534 1516/1747/1536 +f 1508/1739/1511 1523/1754/1535 1524/1755/1537 +f 1502/1733/1514 1501/1732/1512 1516/1747/1536 +f 1509/1740/1513 1524/1755/1537 1525/1756/1524 +f 1502/1733/1514 1517/1748/1538 1518/1749/1526 +f 1514/1745/1532 1530/1792/1553 1531/1793/1539 +f 1523/1754/1535 1522/1753/1533 1538/1800/1540 +f 1515/1746/1534 1531/1793/1539 1532/1794/1542 +f 1523/1754/1535 1539/1801/1541 1540/1802/1543 +f 1517/1748/1538 1516/1747/1536 1532/1794/1542 +f 1524/1755/1537 1540/1802/1543 1541/1803/1545 +f 1517/1748/1538 1533/1795/1544 1534/1796/1546 +f 1525/1756/1524 1541/1803/1545 1542/1804/1547 +f 1519/1750/1527 1518/1749/1526 1534/1796/1546 +f 1527/1758/1528 1526/1757/1525 1542/1804/1547 +f 1520/1751/1529 1519/1750/1527 1535/1797/1548 +f 1520/1751/1529 1536/1798/1550 1537/1799/1552 +f 1513/1744/1530 1529/1791/1551 1530/1792/1553 +f 1521/1752/1531 1537/1799/1552 1538/1800/1540 +f 1535/1797/1548 1534/1796/1546 1549/1811/1554 +f 1542/1804/1547 1557/1819/1568 1558/1820/1556 +f 1536/1798/1550 1535/1797/1548 1550/1812/1555 +f 1536/1798/1550 1551/1813/1557 1552/1814/1559 +f 1530/1792/1553 1529/1791/1551 1544/1806/1558 +f 1537/1799/1552 1552/1814/1559 1553/1815/1561 +f 1530/1792/1553 1545/1807/1560 1546/1808/1562 +f 1539/1801/1541 1538/1800/1540 1553/1815/1561 +f 1531/1793/1539 1546/1808/1562 1547/1809/1564 +f 1539/1801/1541 1554/1816/1563 1555/1817/1565 +f 1533/1795/1544 1532/1794/1542 1547/1809/1564 +f 1541/1803/1545 1540/1802/1543 1555/1817/1565 +f 1533/1795/1544 1548/1810/1566 1549/1811/1554 +f 1542/1804/1547 1541/1803/1545 1556/1818/1567 +f 1554/1816/1563 1553/1815/1561 1568/1830/1569 +f 1546/1808/1562 1561/1823/1583 1562/1824/1571 +f 1554/1816/1563 1569/1831/1570 1570/1832/1572 +f 1548/1810/1566 1547/1809/1564 1562/1824/1571 +f 1556/1818/1567 1555/1817/1565 1570/1832/1572 +f 1548/1810/1566 1563/1825/1573 1564/1826/1575 +f 1556/1818/1567 1571/1833/1574 1572/1834/1576 +f 1550/1812/1555 1549/1811/1554 1564/1826/1575 +f 1557/1819/1568 1572/1834/1576 1573/1835/1578 +f 1551/1813/1557 1550/1812/1555 1565/1827/1577 +f 1551/1813/1557 1566/1828/1579 1567/1829/1581 +f 1545/1807/1560 1544/1806/1558 1559/1821/1580 +f 1552/1814/1559 1567/1829/1581 1568/1830/1569 +f 1545/1807/1560 1560/1822/1582 1561/1823/1583 +f 1572/1834/1576 1587/1849/1598 1588/1850/1584 +f 1566/1828/1579 1565/1827/1577 1580/1842/1585 +f 1566/1828/1579 1581/1843/1586 1582/1844/1588 +f 1559/1821/1580 1574/1836/1587 1575/1837/1589 +f 1567/1829/1581 1582/1844/1588 1583/1845/1590 +f 1560/1822/1582 1575/1837/1589 1576/1838/1591 +f 1569/1831/1570 1568/1830/1569 1583/1845/1590 +f 1561/1823/1583 1576/1838/1591 1577/1839/1593 +f 1569/1831/1570 1584/1846/1592 1585/1847/1594 +f 1563/1825/1573 1562/1824/1571 1577/1839/1593 +f 1571/1833/1574 1570/1832/1572 1585/1847/1594 +f 1563/1825/1573 1578/1840/1595 1579/1841/1597 +f 1571/1833/1574 1586/1848/1596 1587/1849/1598 +f 1565/1827/1577 1564/1826/1575 1579/1841/1597 +f 1584/1846/1592 1599/1861/1613 1600/1862/1599 +f 1578/1840/1595 1577/1839/1593 1592/1854/1600 +f 1586/1848/1596 1585/1847/1594 1600/1862/1599 +f 1578/1840/1595 1593/1855/1601 1594/1856/1603 +f 1586/1848/1596 1601/1863/1602 1602/1864/1604 +f 1580/1842/1585 1579/1841/1597 1594/1856/1603 +f 1588/1850/1584 1587/1849/1598 1602/1864/1604 +f 1581/1843/1586 1580/1842/1585 1595/1857/1605 +f 1581/1843/1586 1596/1858/1607 1597/1859/1609 +f 1574/1836/1587 1589/1851/1608 1590/1852/1610 +f 1582/1844/1588 1597/1859/1609 1598/1860/1611 +f 1576/1838/1591 1575/1837/1589 1590/1852/1610 +f 1584/1846/1592 1583/1845/1590 1598/1860/1611 +f 1576/1838/1591 1591/1853/1612 1592/1854/1600 +f 1596/1858/1607 1611/1873/1628 1612/1874/1616 +f 1589/1851/1608 1604/1866/1614 1605/1867/1617 +f 1597/1859/1609 1612/1874/1616 1613/1875/1618 +f 1591/1853/1612 1590/1852/1610 1605/1867/1617 +f 1599/1861/1613 1598/1860/1611 1613/1875/1618 +f 1591/1853/1612 1606/1868/1619 1607/1869/1621 +f 1599/1861/1613 1614/1876/1620 1615/1877/1622 +f 1593/1855/1601 1592/1854/1600 1607/1869/1621 +f 1601/1863/1602 1600/1862/1599 1615/1877/1622 +f 1593/1855/1601 1608/1870/1623 1609/1871/1625 +f 1601/1863/1602 1616/1878/1624 1617/1879/1626 +f 1595/1857/1605 1594/1856/1603 1609/1871/1625 +f 1602/1864/1604 1617/1879/1626 1618/1880/1615 +f 1596/1858/1607 1595/1857/1605 1610/1872/1627 +f 1608/1870/1623 1607/1869/1621 1622/1884/1629 +f 1615/1877/1622 1630/1892/1643 1631/1893/1631 +f 1608/1870/1623 1623/1885/1630 1624/1886/1632 +f 1617/1879/1626 1616/1878/1624 1631/1893/1631 +f 1610/1872/1627 1609/1871/1625 1624/1886/1632 +f 1618/1880/1615 1617/1879/1626 1632/1894/1633 +f 1611/1873/1628 1610/1872/1627 1625/1887/1634 +f 1611/1873/1628 1626/1888/1636 1627/1889/1638 +f 1604/1866/1614 1619/1881/1637 1620/1882/1639 +f 1612/1874/1616 1627/1889/1638 1628/1890/1640 +f 1605/1867/1617 1620/1882/1639 1621/1883/1641 +f 1614/1876/1620 1613/1875/1618 1628/1890/1640 +f 1606/1868/1619 1621/1883/1641 1622/1884/1629 +f 1614/1876/1620 1629/1891/1642 1630/1892/1643 +f 1626/1888/1636 1641/1903/1658 1642/1904/1644 +f 1620/1882/1639 1619/1881/1637 1634/1896/1645 +f 1627/1889/1638 1642/1904/1644 1643/1905/1647 +f 1621/1883/1641 1620/1882/1639 1635/1897/1646 +f 1629/1891/1642 1628/1890/1640 1643/1905/1647 +f 1621/1883/1641 1636/1898/1648 1637/1899/1650 +f 1629/1891/1642 1644/1906/1649 1645/1907/1651 +f 1623/1885/1630 1622/1884/1629 1637/1899/1650 +f 1631/1893/1631 1630/1892/1643 1645/1907/1651 +f 1623/1885/1630 1638/1900/1652 1639/1901/1654 +f 1631/1893/1631 1646/1908/1653 1647/1909/1655 +f 1625/1887/1634 1624/1886/1632 1639/1901/1654 +f 1632/1894/1633 1647/1909/1655 1648/1910/1657 +f 1626/1888/1636 1625/1887/1634 1640/1902/1656 +f 1646/1908/1653 1645/1907/1651 1660/1922/1659 +f 1638/1900/1652 1653/1915/1673 1654/1916/1661 +f 1646/1908/1653 1661/1923/1660 1662/1924/1662 +f 1640/1902/1656 1639/1901/1654 1654/1916/1661 +f 1647/1909/1655 1662/1924/1662 1663/1925/1664 +f 1641/1903/1658 1640/1902/1656 1655/1917/1663 +f 1641/1903/1658 1656/1918/1665 1657/1919/1667 +f 1634/1896/1645 1649/1911/1666 1650/1912/1668 +f 1642/1904/1644 1657/1919/1667 1658/1920/1669 +f 1635/1897/1646 1650/1912/1668 1651/1913/1670 +f 1644/1906/1649 1643/1905/1647 1658/1920/1669 +f 1636/1898/1648 1651/1913/1670 1652/1914/1672 +f 1644/1906/1649 1659/1921/1671 1660/1922/1659 +f 1638/1900/1652 1637/1899/1650 1652/1914/1672 +f 1649/1911/1666 1664/1926/1687 1665/1927/1674 +f 1657/1919/1667 1672/1934/1688 1673/1935/1675 +f 1650/1912/1668 1665/1927/1674 1666/1928/1676 +f 1659/1921/1671 1658/1920/1669 1673/1935/1675 +f 1651/1913/1670 1666/1928/1676 1667/1929/1678 +f 1659/1921/1671 1674/1936/1677 1675/1937/1679 +f 1653/1915/1673 1652/1914/1672 1667/1929/1678 +f 1661/1923/1660 1660/1922/1659 1675/1937/1679 +f 1653/1915/1673 1668/1930/1680 1669/1931/1682 +f 1661/1923/1660 1676/1938/1681 1677/1939/1683 +f 1655/1917/1663 1654/1916/1661 1669/1931/1682 +f 1662/1924/1662 1677/1939/1683 1678/1940/1685 +f 1656/1918/1665 1655/1917/1663 1670/1932/1684 +f 1656/1918/1665 1671/1933/1686 1672/1934/1688 +f 1668/1930/1680 1683/1945/1702 1684/1946/1689 +f 1676/1938/1681 1691/1953/1703 1692/1954/1690 +f 1670/1932/1684 1669/1931/1682 1684/1946/1689 +f 1678/1940/1685 1677/1939/1683 1692/1954/1690 +f 1671/1933/1686 1670/1932/1684 1685/1947/1691 +f 1671/1933/1686 1686/1948/1693 1687/1949/1695 +f 1664/1926/1687 1679/1941/1694 1680/1942/1696 +f 1672/1934/1688 1687/1949/1695 1688/1950/1697 +f 1666/1928/1676 1665/1927/1674 1680/1942/1696 +f 1674/1936/1677 1673/1935/1675 1688/1950/1697 +f 1666/1928/1676 1681/1943/1698 1682/1944/1700 +f 1674/1936/1677 1689/1951/1699 1690/1952/1701 +f 1668/1930/1680 1667/1929/1678 1682/1944/1700 +f 1676/1938/1681 1675/1937/1679 1690/1952/1701 +f 1687/1949/1695 1702/1964/1717 1703/1965/1704 +f 1680/1942/1696 1695/1957/1718 1696/1958/1705 +f 1689/1951/1699 1688/1950/1697 1703/1965/1704 +f 1681/1943/1698 1696/1958/1705 1697/1959/1707 +f 1689/1951/1699 1704/1966/1706 1705/1967/1708 +f 1683/1945/1702 1682/1944/1700 1697/1959/1707 +f 1691/1953/1703 1690/1952/1701 1705/1967/1708 +f 1683/1945/1702 1698/1960/1709 1699/1961/1711 +f 1691/1953/1703 1706/1968/1710 1707/1969/1712 +f 1685/1947/1691 1684/1946/1689 1699/1961/1711 +f 1692/1954/1690 1707/1969/1712 1708/1970/1714 +f 1686/1948/1693 1685/1947/1691 1700/1962/1713 +f 1686/1948/1693 1701/1963/1715 1702/1964/1717 +f 1679/1941/1694 1694/1956/1716 1695/1957/1718 +f 1706/1968/1710 1721/1983/1733 1722/1984/1719 +f 1700/1962/1713 1699/1961/1711 1714/1976/1720 +f 1707/1969/1712 1722/1984/1719 1723/1985/1722 +f 1701/1963/1715 1700/1962/1713 1715/1977/1721 +f 1701/1963/1715 1716/1978/1723 1717/1979/1725 +f 1695/1957/1718 1694/1956/1716 1709/1971/1724 +f 1702/1964/1717 1717/1979/1725 1718/1980/1727 +f 1695/1957/1718 1710/1972/1726 1711/1973/1728 +f 1704/1966/1706 1703/1965/1704 1718/1980/1727 +f 1696/1958/1705 1711/1973/1728 1712/1974/1730 +f 1704/1966/1706 1719/1981/1729 1720/1982/1731 +f 1698/1960/1709 1697/1959/1707 1712/1974/1730 +f 1706/1968/1710 1705/1967/1708 1720/1982/1731 +f 1698/1960/1709 1713/1975/1732 1714/1976/1720 +f 1719/1981/1729 1718/1980/1727 1733/1995/1734 +f 1711/1973/1728 1726/1988/1748 1727/1989/1736 +f 1719/1981/1729 1734/1996/1735 1735/1997/1737 +f 1713/1975/1732 1712/1974/1730 1727/1989/1736 +f 1721/1983/1733 1720/1982/1731 1735/1997/1737 +f 1713/1975/1732 1728/1990/1738 1729/1991/1740 +f 1721/1983/1733 1736/1998/1739 1737/1999/1741 +f 1715/1977/1721 1714/1976/1720 1729/1991/1740 +f 1723/1985/1722 1722/1984/1719 1737/1999/1741 +f 1716/1978/1723 1715/1977/1721 1730/1992/1742 +f 1716/1978/1723 1731/1993/1744 1732/1994/1746 +f 1710/1972/1726 1709/1971/1724 1724/1986/1745 +f 1717/1979/1725 1732/1994/1746 1733/1995/1734 +f 1710/1972/1726 1725/1987/1747 1726/1988/1748 +f 1738/2000/1743 1737/1999/1741 1752/2014/1749 +f 1731/1993/1744 1730/1992/1742 1745/2007/1751 +f 1731/1993/1744 1746/2008/1752 1747/2009/1754 +f 1724/1986/1745 1739/2001/1753 1740/2002/1755 +f 1732/1994/1746 1747/2009/1754 1748/2010/1756 +f 1725/1987/1747 1740/2002/1755 1741/2003/1757 +f 1734/1996/1735 1733/1995/1734 1748/2010/1756 +f 1726/1988/1748 1741/2003/1757 1742/2004/1759 +f 1734/1996/1735 1749/2011/1758 1750/2012/1760 +f 1728/1990/1738 1727/1989/1736 1742/2004/1759 +f 1736/1998/1739 1735/1997/1737 1750/2012/1760 +f 1728/1990/1738 1743/2005/1761 1744/2006/1763 +f 1736/1998/1739 1751/2013/1762 1752/2014/1749 +f 1730/1992/1742 1729/1991/1740 1744/2006/1763 +f 1741/2003/1757 1757/2050/1777 1758/2051/1764 +f 1749/2011/1758 1765/2058/1778 1766/2059/1765 +f 1743/2005/1761 1742/2004/1759 1758/2051/1764 +f 1751/2013/1762 1750/2012/1760 1766/2059/1765 +f 1743/2005/1761 1759/2052/1766 1760/2053/1768 +f 1751/2013/1762 1767/2060/1767 1768/2061/1769 +f 1745/2007/1751 1744/2006/1763 1760/2053/1768 +f 1752/2014/1749 1768/2061/1769 1769/2062/1771 +f 1746/2008/1752 1745/2007/1751 1761/2054/1770 +f 1746/2008/1752 1762/2055/1772 1763/2056/1774 +f 1739/2001/1753 1755/2048/1773 1756/2049/1775 +f 1747/2009/1754 1763/2056/1774 1764/2057/1776 +f 1740/2002/1755 1756/2049/1775 1757/2050/1777 +f 1749/2011/1758 1748/2010/1756 1764/2057/1776 +f 1762/2055/1772 1761/2054/1770 1776/2069/1779 +f 1762/2055/1772 1777/2070/1780 1778/2071/1783 +f 1755/2048/1773 1770/2063/1781 1771/2064/1784 +f 1763/2056/1774 1778/2071/1783 1779/2072/1785 +f 1756/2049/1775 1771/2064/1784 1772/2065/1786 +f 1765/2058/1778 1764/2057/1776 1779/2072/1785 +f 1757/2050/1777 1772/2065/1786 1773/2066/1788 +f 1765/2058/1778 1780/2073/1787 1781/2074/1789 +f 1758/2051/1764 1773/2066/1788 1774/2067/1790 +f 1767/2060/1767 1766/2059/1765 1781/2074/1789 +f 1759/2052/1766 1774/2067/1790 1775/2068/1792 +f 1767/2060/1767 1782/2075/1791 1783/2076/1793 +f 1761/2054/1770 1760/2053/1768 1775/2068/1792 +f 1768/2061/1769 1783/2076/1793 1784/2077/1782 +f 1780/2073/1787 1795/2088/1808 1796/2089/1794 +f 1774/2067/1790 1773/2066/1788 1788/2081/1795 +f 1782/2075/1791 1781/2074/1789 1796/2089/1794 +f 1774/2067/1790 1789/2082/1796 1790/2083/1798 +f 1782/2075/1791 1797/2090/1797 1798/2091/1799 +f 1776/2069/1779 1775/2068/1792 1790/2083/1798 +f 1783/2076/1793 1798/2091/1799 1799/2092/1801 +f 1777/2070/1780 1776/2069/1779 1791/2084/1800 +f 1777/2070/1780 1792/2085/1802 1793/2086/1804 +f 1770/2063/1781 1785/2078/1803 1786/2079/1805 +f 1778/2071/1783 1793/2086/1804 1794/2087/1806 +f 1771/2064/1784 1786/2079/1805 1787/2080/1807 +f 1780/2073/1787 1779/2072/1785 1794/2087/1806 +f 1772/2065/1786 1787/2080/1807 1788/2081/1795 +f 1792/2085/1802 1807/2107/1823 1808/2109/1811 +f 1785/2078/1803 1800/2093/1809 1801/2095/1812 +f 1793/2086/1804 1808/2109/1811 1809/2111/1813 +f 1787/2080/1807 1786/2079/1805 1801/2095/1812 +f 1795/2088/1808 1794/2087/1806 1809/2111/1813 +f 1787/2080/1807 1802/2097/1814 1803/2099/1816 +f 1795/2088/1808 1810/2113/1815 1811/2115/1817 +f 1789/2082/1796 1788/2081/1795 1803/2099/1816 +f 1797/2090/1797 1796/2089/1794 1811/2115/1817 +f 1789/2082/1796 1804/2101/1818 1805/2103/1820 +f 1797/2090/1797 1812/2117/1819 1813/2119/1821 +f 1791/2084/1800 1790/2083/1798 1805/2103/1820 +f 1798/2091/1799 1813/2119/1821 1814/2122/1810 +f 1792/2085/1802 1791/2084/1800 1806/2105/1822 +f 1804/2102/1818 1803/2100/1816 1818/2126/1824 +f 1812/2118/1819 1811/2116/1817 1826/2134/1826 +f 1804/2102/1818 1819/2127/1825 1820/2128/1828 +f 1812/2118/1819 1827/2135/1827 1828/2136/1829 +f 1806/2106/1822 1805/2104/1820 1820/2128/1828 +f 1813/2120/1821 1828/2136/1829 1829/2137/1831 +f 1807/2108/1823 1806/2106/1822 1821/2129/1830 +f 1807/2108/1823 1822/2130/1832 1823/2131/1834 +f 1800/2094/1809 1815/2123/1833 1816/2124/1835 +f 1808/2110/1811 1823/2131/1834 1824/2132/1836 +f 1802/2098/1814 1801/2096/1812 1816/2124/1835 +f 1810/2114/1815 1809/2112/1813 1824/2132/1836 +f 1802/2098/1814 1817/2125/1837 1818/2126/1824 +f 1810/2114/1815 1825/2133/1838 1826/2134/1826 +f 1822/2130/1832 1837/2145/1852 1838/2146/1839 +f 1815/2123/1833 1830/2138/1853 1831/2139/1840 +f 1823/2131/1834 1838/2146/1839 1839/2147/1841 +f 1817/2125/1837 1816/2124/1835 1831/2139/1840 +f 1825/2133/1838 1824/2132/1836 1839/2147/1841 +f 1817/2125/1837 1832/2140/1842 1833/2141/1844 +f 1825/2133/1838 1840/2148/1843 1841/2149/1845 +f 1819/2127/1825 1818/2126/1824 1833/2141/1844 +f 1827/2135/1827 1826/2134/1826 1841/2149/1845 +f 1819/2127/1825 1834/2142/1846 1835/2143/1848 +f 1827/2135/1827 1842/2150/1847 1843/2151/1849 +f 1821/2129/1830 1820/2128/1828 1835/2143/1848 +f 1828/2136/1829 1843/2151/1849 1844/2152/1851 +f 1822/2130/1832 1821/2129/1830 1836/2144/1850 +f 1842/2150/1847 1841/2149/1845 1856/2164/1854 +f 1834/2142/1846 1849/2157/1868 1850/2158/1856 +f 1843/2151/1849 1842/2150/1847 1857/2165/1855 +f 1836/2144/1850 1835/2143/1848 1850/2158/1856 +f 1843/2151/1849 1858/2166/1857 1859/2167/1859 +f 1837/2145/1852 1836/2144/1850 1851/2159/1858 +f 1837/2145/1852 1852/2160/1860 1853/2161/1862 +f 1831/2139/1840 1830/2138/1853 1845/2153/1861 +f 1838/2146/1839 1853/2161/1862 1854/2162/1864 +f 1831/2139/1840 1846/2154/1863 1847/2155/1865 +f 1840/2148/1843 1839/2147/1841 1854/2162/1864 +f 1832/2140/1842 1847/2155/1865 1848/2156/1867 +f 1840/2148/1843 1855/2163/1866 1856/2164/1854 +f 1834/2142/1846 1833/2141/1844 1848/2156/1867 +f 1853/2161/1862 1868/2176/1883 1869/2177/1869 +f 1847/2155/1865 1846/2154/1863 1861/2169/1870 +f 1855/2163/1866 1854/2162/1864 1869/2177/1869 +f 1847/2155/1865 1862/2170/1871 1863/2171/1873 +f 1855/2163/1866 1870/2178/1872 1871/2179/1874 +f 1849/2157/1868 1848/2156/1867 1863/2171/1873 +f 1857/2165/1855 1856/2164/1854 1871/2179/1874 +f 1849/2157/1868 1864/2172/1875 1865/2173/1877 +f 1857/2165/1855 1872/2180/1876 1873/2181/1878 +f 1851/2159/1858 1850/2158/1856 1865/2173/1877 +f 1858/2166/1857 1873/2181/1878 1874/2182/1880 +f 1852/2160/1860 1851/2159/1858 1866/2174/1879 +f 1852/2160/1860 1867/2175/1881 1868/2176/1883 +f 1845/2153/1861 1860/2168/1882 1861/2169/1870 +f 1872/2180/1876 1887/2195/1898 1888/2196/1884 +f 1866/2174/1879 1865/2173/1877 1880/2188/1885 +f 1873/2181/1878 1888/2196/1884 1889/2197/1887 +f 1867/2175/1881 1866/2174/1879 1881/2189/1886 +f 1867/2175/1881 1882/2190/1888 1883/2191/1890 +f 1860/2168/1882 1875/2183/1889 1876/2184/1891 +f 1868/2176/1883 1883/2191/1890 1884/2192/1892 +f 1862/2170/1871 1861/2169/1870 1876/2184/1891 +f 1870/2178/1872 1869/2177/1869 1884/2192/1892 +f 1862/2170/1871 1877/2185/1893 1878/2186/1895 +f 1870/2178/1872 1885/2193/1894 1886/2194/1896 +f 1864/2172/1875 1863/2171/1873 1878/2186/1895 +f 1872/2180/1876 1871/2179/1874 1886/2194/1896 +f 1864/2172/1875 1879/2187/1897 1880/2188/1885 +f 1876/2184/1891 1891/2199/1913 1892/2200/1899 +f 1885/2193/1894 1884/2192/1892 1899/2207/1900 +f 1877/2185/1893 1892/2200/1899 1893/2201/1902 +f 1885/2193/1894 1900/2208/1901 1901/2209/1903 +f 1879/2187/1897 1878/2186/1895 1893/2201/1902 +f 1887/2195/1898 1886/2194/1896 1901/2209/1903 +f 1879/2187/1897 1894/2202/1904 1895/2203/1906 +f 1887/2195/1898 1902/2210/1905 1903/2211/1907 +f 1881/2189/1886 1880/2188/1885 1895/2203/1906 +f 1888/2196/1884 1903/2211/1907 1904/2212/1909 +f 1882/2190/1888 1881/2189/1886 1896/2204/1908 +f 1882/2190/1888 1897/2205/1910 1898/2206/1912 +f 1876/2184/1891 1875/2183/1889 1890/2198/1911 +f 1883/2191/1890 1898/2206/1912 1899/2207/1900 +f 1896/2204/1908 1895/2203/1906 1910/2218/1914 +f 1904/2212/1909 1903/2211/1907 1918/2226/1916 +f 1897/2205/1910 1896/2204/1908 1911/2219/1915 +f 1897/2205/1910 1912/2220/1918 1913/2221/1920 +f 1890/2198/1911 1905/2213/1919 1906/2214/1921 +f 1898/2206/1912 1913/2221/1920 1914/2222/1922 +f 1892/2200/1899 1891/2199/1913 1906/2214/1921 +f 1900/2208/1901 1899/2207/1900 1914/2222/1922 +f 1892/2200/1899 1907/2215/1923 1908/2216/1925 +f 1900/2208/1901 1915/2223/1924 1916/2224/1926 +f 1894/2202/1904 1893/2201/1902 1908/2216/1925 +f 1902/2210/1905 1901/2209/1903 1916/2224/1926 +f 1894/2202/1904 1909/2217/1927 1910/2218/1914 +f 1902/2210/1905 1917/2225/1928 1918/2226/1916 +f 1914/2222/1922 1924/2232/1450 1452/1683/1452 +f 1908/2216/1925 1907/2215/1923 1447/1678/1447 +f 1915/2223/1924 1452/1683/1452 1925/2233/1456 +f 1909/2217/1927 1908/2216/1925 1922/2230/1453 +f 1917/2225/1928 1916/2224/1926 1925/2233/1456 +f 1910/2218/1914 1909/2217/1927 1448/1679/1457 +f 1917/2225/1928 1926/2234/1460 1927/2235/1464 +f 1911/2219/1915 1910/2218/1914 1923/2231/1461 +f 1918/2226/1916 1927/2235/1464 1928/2236/1468 +f 1912/2220/1918 1911/2219/1915 1449/1680/1465 +f 1912/2220/1918 1450/1681/1470 1451/1682/1476 +f 1906/2214/1921 1905/2213/1919 1920/2228/1471 +f 1913/2221/1920 1451/1682/1476 1924/2232/1450 +f 1907/2215/1923 1906/2214/1921 1921/2229/1477 diff --git a/resources/shader/include/visu/constants.glsl b/resources/shader/include/visu/constants.glsl new file mode 100644 index 0000000..3661974 --- /dev/null +++ b/resources/shader/include/visu/constants.glsl @@ -0,0 +1,10 @@ +/** + * PI constant + * ---------------------------------------------------------------------------- + */ +#ifndef VISU_CONSTANTS_GLSL +#define VISU_CONSTANTS_GLSL + +const float PI = 3.14159265359; + +#endif \ No newline at end of file diff --git a/resources/shader/include/visu/cubemap_vert.glsl b/resources/shader/include/visu/cubemap_vert.glsl new file mode 100644 index 0000000..4dfe7cd --- /dev/null +++ b/resources/shader/include/visu/cubemap_vert.glsl @@ -0,0 +1,13 @@ + +layout (location = 0) in vec3 a_position; + +out vec3 v_position; + +uniform mat4 projection; +uniform mat4 view; + +void main() +{ + v_position = a_position; + gl_Position = projection * view * vec4(v_position, 1.0); +} \ No newline at end of file diff --git a/resources/shader/include/visu/functions/brdf.glsl b/resources/shader/include/visu/functions/brdf.glsl new file mode 100644 index 0000000..6b30d7e --- /dev/null +++ b/resources/shader/include/visu/functions/brdf.glsl @@ -0,0 +1,134 @@ +#ifndef BRDF_GLSL +#define BRDF_GLSL + +/** + * The PBR Distrubition & Geometry functions + * + * PBR_DISTRIBUTION_GGX + * PBR_DISTRIBUTION_BECKMANN + * + * PBR_GEOMETRY_SCHLICK + * PBR_GEOMETRY_COOK_TORRANCE + * PBR_GEOMETRY_KELEMEN + * PBR_GEOMETRY_SMITH_GGX_CORRELATED + */ +#define PBR_DISTRIBUTION_GGX +#define PBR_GEOMETRY_SMITH_GGX_CORRELATED + +#include "visu/constants.glsl" + +const float EPS_DOT = 1e-5; +const float EPS_DENOM = 1e-6; + +/** + * I've used a bunch of function from "optimized-ggx.hlsl" by John Hable + * http://filmicworlds.com/blog/optimizing-ggx-shaders-with-dotlh/ + * Released under the unlicense + */ + +/** + * Fresnel Functions + * ---------------------------------------------------------------------------- + */ + +vec3 fresnel_schlick(vec3 F0, float cosTheta) +{ + return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); +} + +vec3 fresnel_schlick_roughness(vec3 F0, float cosTheta, float roughness) +{ + return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); +} + +float distribution_GGX(float NdotH, float roughness) +{ + float alpha = roughness*roughness; + float alphaSqr = alpha*alpha; + float denom = NdotH * NdotH *(alphaSqr-1.0) + 1.0f; + + float D = alphaSqr/(PI * denom * denom); + return D; +} + +float distribution_beckmann(float NdotH, float roughness) +{ + float a = roughness * roughness; + float a2 = a * a; + float r1 = 1.0 / (PI * a2 * pow(NdotH, 4.0)); + float r2 = (NdotH * NdotH - 1.0) / (a2 * NdotH * NdotH); + return r1 * exp(r2); +} + +float geometry_schlick(float NdotL, float NdotV, float roughness) +{ + float a = roughness + 1.0; + float k = a * a * 0.125; + float G1 = NdotL / (NdotL * (1.0 - k) + k); + float G2 = NdotV / (NdotV * (1.0 - k) + k); + return G1 * G2; +} + +float geometry_cook_torrance(float NdotL, float NdotV, float NdotH, float VdotH) +{ + float G1 = (2.0 * NdotH * NdotV) / VdotH; + float G2 = (2.0 * NdotH * NdotL) / VdotH; + return min(1.0, min(G1, G2)); +} + +float geometry_smith_ggx_correlated(float NdotL, float NdotV, float roughness) +{ + float a = roughness * roughness; + float a2 = a * a; + + float gv = NdotL * sqrt(a2 + (1.0 - a2) * NdotV * NdotV); + float gl = NdotV * sqrt(a2 + (1.0 - a2) * NdotL * NdotL); + + return (2.0 * NdotL * NdotV) / max(gv + gl, EPS_DENOM); +} + +float geometry_kelemen(float NdotL, float NdotV, float VdotH) +{ + return (NdotL * NdotV) / (VdotH * VdotH); +} + +vec3 pbr_specular(vec3 N, vec3 V, vec3 H, vec3 L, vec3 F0, float roughness, out vec3 fresnel) +{ + float NdotH = max(EPS_DOT, dot(N, H)); + float NdotV = max(dot(N, V), 0.0); + float NdotL = max(dot(N, L), 0.0); + float VdotH = max(EPS_DOT, dot(V, H)); + + if (NdotV <= 0.0 || NdotL <= 0.0) { + fresnel = vec3(0.0); + return vec3(0.0); + } + +#ifdef PBR_DISTRIBUTION_GGX + float D = distribution_GGX(NdotH, roughness); +#endif +#ifdef PBR_DISTRIBUTION_BECKMANN + float D = distribution_beckmann(NdotH, roughness); +#endif + +#ifdef PBR_GEOMETRY_SCHLICK + float G = geometry_schlick(NdotL, NdotV, roughness); +#endif +#ifdef PBR_GEOMETRY_COOK_TORRANCE + float G = geometry_cook_torrance(NdotL, NdotV, NdotH, VdotH); +#endif +#ifdef PBR_GEOMETRY_KELEMEN + float G = geometry_kelemen(NdotL, NdotV, VdotH); +#endif +#ifdef PBR_GEOMETRY_SMITH_GGX_CORRELATED + float G = geometry_smith_ggx_correlated(NdotL, NdotV, roughness); +#endif + + fresnel = fresnel_schlick(F0, VdotH); + + float denom = max(4.0 * NdotV * NdotL, EPS_DENOM); + + return (D * fresnel * G) / denom; +} + +#endif diff --git a/resources/shader/include/visu/functions/gamma_corr.glsl b/resources/shader/include/visu/functions/gamma_corr.glsl new file mode 100644 index 0000000..657efef --- /dev/null +++ b/resources/shader/include/visu/functions/gamma_corr.glsl @@ -0,0 +1,19 @@ +#ifndef GAMMA_CORR_GLSL +#define GAMMA_CORR_GLSL +/** + * Gamma Correction + * ---------------------------------------------------------------------------- + */ + +// gamma constant definition +uniform float u_display_gamma = 2.2; + +/** + * Apply gamma correction to a color + */ +vec3 gamma_correct(vec3 color) +{ + return pow(color, vec3(1.0 / u_display_gamma)); +} + +#endif \ No newline at end of file diff --git a/resources/shader/include/visu/functions/importance_sampling.glsl b/resources/shader/include/visu/functions/importance_sampling.glsl new file mode 100644 index 0000000..262329b --- /dev/null +++ b/resources/shader/include/visu/functions/importance_sampling.glsl @@ -0,0 +1,49 @@ +#ifndef IMPORTANCE_SAMPLING_GLSL +#define IMPORTANCE_SAMPLING_GLSL + +#include "visu/constants.glsl" + +// ---------------------------------------------------------------------------- +// http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html +// efficient VanDerCorpus calculation. +float radical_inverse_vdc(uint bits) +{ + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10; // / 0x100000000 +} + +// ---------------------------------------------------------------------------- +vec2 hammersley(uint i, uint N) +{ + return vec2(float(i)/float(N), radical_inverse_vdc(i)); +} + +// ---------------------------------------------------------------------------- +vec3 importance_sample_ggx(vec2 Xi, vec3 N, float roughness) +{ + float a = roughness*roughness; + + float phi = 2.0 * PI * Xi.x; + float cos_theta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y)); + float sin_theta = sqrt(1.0 - cos_theta*cos_theta); + + // from spherical coordinates to cartesian coordinates - halfway vector + vec3 H; + H.x = cos(phi) * sin_theta; + H.y = sin(phi) * sin_theta; + H.z = cos_theta; + + // from tangent-space H vector to world-space sample vector + vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + vec3 tangent = normalize(cross(up, N)); + vec3 bitangent = cross(N, tangent); + + vec3 sample = tangent * H.x + bitangent * H.y + N * H.z; + return normalize(sample); +} + +#endif \ No newline at end of file diff --git a/resources/shader/include/visu/functions/tone_mapping.glsl b/resources/shader/include/visu/functions/tone_mapping.glsl new file mode 100644 index 0000000..4253301 --- /dev/null +++ b/resources/shader/include/visu/functions/tone_mapping.glsl @@ -0,0 +1,59 @@ +/** + * Common tone mapping functions + * ---------------------------------------------------------------------------- + */ + +/** + * ACES Filmic Tone Mapping + * Reference: https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/ + */ +vec3 tonemap_ACESFilm(vec3 x) +{ + const float a = 2.51; + const float b = 0.03; + const float c = 2.43; + const float d = 0.59; + const float e = 0.14; + return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0); +} + +/** + * Reinhard Tone Mapping + */ +vec3 tonemap_reinhard(vec3 x) +{ + return x / (1.0 + x); +} + +/** + * Reinhard2 Tone Mapping + */ +vec3 tonemap_reinhard2(vec3 x) +{ + const float L_white = 4.0; + return (x * (1.0 + x / (L_white * L_white))) / (1.0 + x); +} + +/** + * Khronos PBR Neutral Tone Mapper + * https://github.com/KhronosGroup/ToneMapping/tree/main/PBR_Neutral + */ +vec3 tonemap_neutral(vec3 color) +{ + const float startCompression = 0.8 - 0.04; + const float desaturation = 0.15; + + float x = min(color.r, min(color.g, color.b)); + float offset = x < 0.08 ? x - 6.25 * x * x : 0.04; + color -= offset; + + float peak = max(color.r, max(color.g, color.b)); + if (peak < startCompression) return color; + + const float d = 1.0 - startCompression; + float newPeak = 1.0 - d * d / (peak + d - startCompression); + color *= newPeak / peak; + + float g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0); + return mix(color, vec3(newPeak), g); +} \ No newline at end of file diff --git a/resources/shader/include/visu/gbuffer_layout.glsl b/resources/shader/include/visu/gbuffer_layout.glsl index 66e5fca..6f2c6ed 100644 --- a/resources/shader/include/visu/gbuffer_layout.glsl +++ b/resources/shader/include/visu/gbuffer_layout.glsl @@ -4,4 +4,4 @@ layout (location = 2) out vec3 gbuffer_normal; layout (location = 3) out vec3 gbuffer_albedo; layout (location = 4) out float gbuffer_metallic; layout (location = 5) out float gbuffer_roughness; -layout (location = 6) out vec3 gbuffer_emissive; \ No newline at end of file +layout (location = 6) out vec3 gbuffer_emissive; diff --git a/resources/shader/include/visu/gbuffer_uniform.glsl b/resources/shader/include/visu/gbuffer_uniform.glsl index 2b5680b..f67704f 100644 --- a/resources/shader/include/visu/gbuffer_uniform.glsl +++ b/resources/shader/include/visu/gbuffer_uniform.glsl @@ -1,7 +1,48 @@ +/** + * GBuffer Uniforms + * ---------------------------------------------------------------------------- + */ +#ifndef GBUFFER_UNIFORM_GLSL +#define GBUFFER_UNIFORM_GLSL + uniform sampler2D gbuffer_position; uniform sampler2D gbuffer_normal; uniform sampler2D gbuffer_depth; uniform sampler2D gbuffer_albedo; uniform sampler2D gbuffer_metallic; uniform sampler2D gbuffer_roughness; -uniform sampler2D gbuffer_emissive; \ No newline at end of file +uniform sampler2D gbuffer_emissive; +uniform sampler2D gbuffer_ao; + +struct GBuffer +{ + vec3 P; + vec3 N; + vec3 albedo; + float metallic; + float roughness; + float ao; + vec3 emissive; +}; + +/** + * Fetches data from the GBuffer uniforms at the given UV coordinates. + * + * Note: this really just gives you raw data, no normalisation or clamping is done. + */ +GBuffer gbuffer_make(vec2 uv) +{ + GBuffer gbuffer; + + gbuffer.P = texture(gbuffer_position, uv).rgb; + gbuffer.N = texture(gbuffer_normal, uv).rgb; + gbuffer.albedo = texture(gbuffer_albedo, uv).rgb; + gbuffer.metallic = texture(gbuffer_metallic, uv).r; + gbuffer.roughness = texture(gbuffer_roughness, uv).r; + gbuffer.ao = texture(gbuffer_ao, uv).r; + gbuffer.emissive = texture(gbuffer_emissive, uv).rgb; + + return gbuffer; +} + +#endif diff --git a/resources/shader/include/visu/pbr/shade.glsl b/resources/shader/include/visu/pbr/shade.glsl new file mode 100644 index 0000000..bd42f05 --- /dev/null +++ b/resources/shader/include/visu/pbr/shade.glsl @@ -0,0 +1,26 @@ +#ifndef PBR_SHADE_GLSL +#define PBR_SHADE_GLSL + +#include "visu/functions/brdf.glsl" +#include "visu/pbr/surface.glsl" +#include "visu/constants.glsl" + +vec3 pbr_shade(in PBRSurface s, vec3 L, vec3 radiance) +{ + float NdotL = max(dot(s.N, L), 0.0); + // if (NdotL <= 0.0) return vec3(0.0); + + vec3 H = normalize(s.V + L); + float VdotH = max(dot(s.V, H), 0.0); + + // energy conservation + vec3 F; + vec3 spec = pbr_specular(s.N, s.V, H, L, s.F0, s.roughness, F); + vec3 kS = F; + vec3 kD = (vec3(1.0) - kS) * (1.0 - s.metallic); + vec3 diff = kD * s.albedo / PI; + + return (diff + spec) * radiance * NdotL; +} + +#endif \ No newline at end of file diff --git a/resources/shader/include/visu/pbr/surface.glsl b/resources/shader/include/visu/pbr/surface.glsl new file mode 100644 index 0000000..21a0965 --- /dev/null +++ b/resources/shader/include/visu/pbr/surface.glsl @@ -0,0 +1,42 @@ +#ifndef PBR_SURFACE_GLSL +#define PBR_SURFACE_GLSL + +struct PBRSurface +{ + vec3 P; + vec3 N; + vec3 V; + + vec3 albedo; + float metallic; + float roughness; + float ao; + + vec3 emissive; + vec3 F0; +}; + + +PBRSurface pbr_surface_make( + GBuffer gbuffer, + vec3 camera_position +) +{ + PBRSurface s; + + s.P = gbuffer.P; + s.N = normalize(gbuffer.N); + s.V = normalize(camera_position - s.P); + + s.albedo = max(gbuffer.albedo, vec3(0.0)); + s.metallic = clamp(gbuffer.metallic, 0.0, 1.0); + s.roughness = clamp(gbuffer.roughness, 0.04, 1.0); + s.ao = clamp(gbuffer.ao, 0.0, 1.0); + s.emissive = gbuffer.emissive; + + s.F0 = mix(vec3(0.04), s.albedo, s.metallic); + + return s; +} + +#endif diff --git a/resources/shader/visu/lowpoly/deferred_lightpass.frag.glsl b/resources/shader/visu/lowpoly/deferred_lightpass.frag.glsl index 09c890b..9518204 100644 --- a/resources/shader/visu/lowpoly/deferred_lightpass.frag.glsl +++ b/resources/shader/visu/lowpoly/deferred_lightpass.frag.glsl @@ -1,30 +1,44 @@ #version 330 core -/** - * The PBR Distrubition & Geometry functions - * - * PBR_DISTRIBUTION_GGX - * PBR_DISTRIBUTION_BECKMANN - * - * PBR_GEOMETRY_SCHLICK - * PBR_GEOMETRY_COOK_TORRANCE - * PBR_GEOMETRY_KELEMAN - */ -#define PBR_DISTRIBUTION_GGX -#define PBR_GEOMETRY_COOK_TORRANCE - -in vec2 v_texture_cords; +// Tone mapping +// ---------------------------------------------------------------------------- +// +// define TONEMAP_METHOD in as a shader option to change. +// available methods: +// TONEMAP_NEUTRAL (1) - Khronos PBR neutral tone mapper (default) +// TONEMAP_ACES (2) - ACES filmic curve +// TONEMAP_REINHARD (3) - basic Reinhard +// TONEMAP_REINHARD2 (4) - Reinhard with white point +#define TONEMAP_NONE 0 +#define TONEMAP_NEUTRAL 1 +#define TONEMAP_ACES 2 +#define TONEMAP_REINHARD 3 +#define TONEMAP_REINHARD2 4 + +#ifndef TONEMAP_METHOD +#define TONEMAP_METHOD TONEMAP_NEUTRAL +#endif + + +// Gamma correction +// ---------------------------------------------------------------------------- +// +// The gamme value itself can changed by setting the "u_display_gamma" uniform. +#ifndef SHOULD_CORRECT_GAMMA +#define SHOULD_CORRECT_GAMMA 1 +#endif + + +in vec2 v_uv; out vec4 fragment_color; -// gbuffer textures -uniform sampler2D gbuffer_position; -uniform sampler2D gbuffer_normal; -uniform sampler2D gbuffer_depth; -uniform sampler2D gbuffer_albedo; -uniform sampler2D gbuffer_metallic; -uniform sampler2D gbuffer_roughness; -uniform sampler2D gbuffer_emissive; -uniform sampler2D gbuffer_ao; +#include "visu/functions/gamma_corr.glsl" +#include "visu/functions/tone_mapping.glsl" +#include "visu/functions/brdf.glsl" + +#include "visu/gbuffer_uniform.glsl" +#include "visu/pbr/surface.glsl" +#include "visu/pbr/shade.glsl" // camera uniforms uniform vec3 camera_position; @@ -35,149 +49,94 @@ uniform vec3 sun_direction; uniform vec3 sun_color; uniform float sun_intensity; -const float gamma = 2.2; -const float PI = 3.14159265359; -const float exposure = 1.5; - -vec3 fresnel(vec3 F0, float b) -{ - return F0 + (1.0 - F0) * pow(clamp(1.0 - b, 0.0, 1.0), 5.0); -} - -float GGX(float NdotH, float roughness) -{ - float a = roughness * roughness; - float a2 = a * a; - float d = NdotH * NdotH * (a2 - 1.0) + 1.0; - return a2 / (PI * d * d); -} - -float distribution_beckmann(float NdotH, float roughness) -{ - float a = roughness * roughness; - float a2 = a * a; - float r1 = 1.0 / (4.0 * a2 * pow(NdotH, 4.0)); - float r2 = (NdotH * NdotH - 1.0) / (a2 * NdotH * NdotH); - return r1 * exp(r2); -} - -float geometry_schlick(float NdotL, float NdotV, float roughness) -{ - float a = roughness + 1.0; - float k = a * a * 0.125; - float G1 = NdotL / (NdotL * (1.0 - k) + k); - float G2 = NdotV / (NdotV * (1.0 - k) + k); - return G1 * G2; -} - -float geometry_cook_torrance(float NdotL, float NdotV, float NdotH, float VdotH) -{ - float G1 = (2.0 * NdotH * NdotV) / VdotH; - float G2 = (2.0 * NdotH * NdotL) / VdotH; - return min(1.0, min(G1, G2)); -} +// environment cubemap +#ifdef USE_ENV_CUBEMAP +uniform samplerCube environment_cubemap; +uniform int environment_mip_count; +#endif -float geometry_kelman(float NdotL, float NdotV, float VdotH) -{ - return (NdotL * NdotV) / (VdotH * VdotH); -} +// IBL +#ifdef USE_IBL +uniform samplerCube ibl_irradiance_map; +uniform samplerCube ibl_prefilter_map; +uniform int prefilter_mip_count; +uniform sampler2D ibl_brdf_lut; +#endif -vec3 pbr_specular(vec3 N, vec3 V, vec3 H, vec3 L, vec3 F0, float roughness) +void main() { - float NdotH = max(0.0, dot(N, H)); - float NdotV = max(1e-7, dot(N, V)); - float NdotL = max(1e-7, dot(N, L)); - float VdotH = max(0.0, dot(V, H)); - -#ifdef PBR_DISTRIBUTION_GGX - float D = GGX(NdotH, roughness); -#endif -#ifdef PBR_DISTRIBUTION_BECKMANN - float D = distribution_beckmann(NdotH, roughness); -#endif - -#ifdef PBR_GEOMETRY_SCHLICK - float G = geometry_schlick(NdotL, NdotV, roughness); -#endif -#ifdef PBR_GEOMETRY_COOK_TORRANCE - float G = geometry_cook_torrance(NdotL, NdotV, NdotH, VdotH); + // fetch gbuffer data + GBuffer gbuffer = gbuffer_make(v_uv); + + // create PBR surface from gbuffer data + PBRSurface s = pbr_surface_make( + gbuffer, + camera_position + ); + + vec3 Lo = vec3(0.0); + + // directional light (sun) + { + vec3 L = normalize(-sun_direction); + vec3 radiance = sun_color * sun_intensity; + Lo += pbr_shade(s, L, radiance); + } + + // ambient / IBL + vec3 ambient = vec3(0.0); + { + float NdotV = max(dot(s.N, s.V), 0.0); + vec3 R = normalize(reflect(-s.V, s.N)); + + vec3 F = fresnel_schlick_roughness(s.F0, NdotV, s.roughness); + vec3 kS = F; + vec3 kD = (vec3(1.0) - kS) * (1.0 - s.metallic); + + // diffuse IBL + vec3 diffuseIBL = vec3(0.0); +#ifdef USE_IBL + vec3 irradiance = texture(ibl_irradiance_map, s.N).rgb; + diffuseIBL = irradiance * s.albedo / PI; +#else + // basic ambient fallback + diffuseIBL = vec3(0.03) * s.albedo; #endif -#ifdef PBR_GEOMETRY_KELEMAN - float G = geometry_kelman(NdotL, NdotV, VdotH); -#endif - - vec3 F = fresnel(F0, VdotH); - - return (D * F * G) / (4.0 * NdotL * NdotV); -} -vec3 tone_mapping_ACESFilm(vec3 x) -{ - x *= exposure; + // specular IBL + vec3 specIBL = vec3(0.0); +#ifdef USE_IBL + float maxLod = float(prefilter_mip_count - 1); + vec3 prefiltered = textureLod(ibl_prefilter_map, R, s.roughness * maxLod).rgb; + vec2 brdf = texture(ibl_brdf_lut, vec2(NdotV, s.roughness)).rg; + specIBL = prefiltered * (s.F0 * brdf.x + brdf.y); +#elif defined(USE_ENV_CUBEMAP) + float maxLod = float(max(environment_mip_count - 1, 0)); + vec3 env = textureLod(environment_cubemap, R, s.roughness * maxLod).rgb; + specIBL = env * F; // fallback without proper LUT +#endif - float a = 2.51f; - float b = 0.03f; - float c = 2.43f; - float d = 0.59f; - float e = 0.14f; + float specAO = s.ao; + ambient = kD * diffuseIBL * s.ao + specIBL * specAO; + } + + // compose lighting, ambient and emissive + vec3 color = Lo + ambient + s.emissive; + + // tone mapping +#if TONEMAP_METHOD == TONEMAP_NEUTRAL + color = tonemap_neutral(color); +#elif TONEMAP_METHOD == TONEMAP_ACES + color = tonemap_ACESFilm(color); +#elif TONEMAP_METHOD == TONEMAP_REINHARD + color = tonemap_reinhard(color); +#elif TONEMAP_METHOD == TONEMAP_REINHARD2 + color = tonemap_reinhard2(color); +#endif - return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0.0, 1.0); -} +#if SHOULD_CORRECT_GAMMA + color = gamma_correct(color); +#endif -vec3 gamma_correct(vec3 color) -{ - return pow(color, vec3(1.0 / gamma)); + fragment_color = vec4(color, 1.0); } - -void main() -{ - // retrieve data from gbuffer - vec3 buffer_pos = texture(gbuffer_position, v_texture_cords).rgb; - vec3 buffer_normal = texture(gbuffer_normal, v_texture_cords).rgb; - vec3 buffer_albedo = texture(gbuffer_albedo, v_texture_cords).rgb; - float buffer_metal = texture(gbuffer_metallic, v_texture_cords).r; - float buffer_roughness = texture(gbuffer_roughness, v_texture_cords).r; - float ao = texture(gbuffer_ao, v_texture_cords).r; - vec3 buffer_emissive = texture(gbuffer_emissive, v_texture_cords).rgb; - - float roughness = clamp(buffer_roughness, 0.04, 1.0); - - float inverse_metal = 1.0f - buffer_metal; - - // lighting - vec3 N = normalize(buffer_normal); - vec3 V = normalize(camera_position - buffer_pos); - vec3 L = normalize(sun_direction); - vec3 R = normalize(reflect(-L, N)); - vec3 H = normalize(L + V); - - float visibility = 1.0; - float attenuation = 1.0; - vec3 radiance = sun_color * sun_intensity * attenuation; - - vec3 F0 = mix(vec3(0.04), buffer_albedo, buffer_metal); - vec3 F = fresnel(F0, max(0.0, dot(H, V))); - vec3 specular = pbr_specular(N, V, H, L, F0, roughness); - - float NdotL = max(dot(N, L), 0.0); - vec3 kD = (1.0 - F) * inverse_metal; - - vec3 diffuse = kD * buffer_albedo / PI; - vec3 Lo = (diffuse * ao + specular) * radiance * NdotL; - - vec3 ambient = vec3(0.05) * buffer_albedo * ao; - - vec3 fragment = ambient + Lo + buffer_emissive; - - // HDR tonemapping - fragment = tone_mapping_ACESFilm(fragment); - fragment = gamma_correct(fragment); - - // // tmp blueish sky if albedo is 0 - // // this is a hack till we build a proper skybox renderer - // if (buffer_albedo == vec3(0.0)) { - // fragment = vec3(0.654, 0.68, 0.8); - // } - - fragment_color = vec4(fragment, 1.0); -} \ No newline at end of file diff --git a/resources/shader/visu/lowpoly/deferred_lightpass.vert.glsl b/resources/shader/visu/lowpoly/deferred_lightpass.vert.glsl index 436a99b..3cf9816 100644 --- a/resources/shader/visu/lowpoly/deferred_lightpass.vert.glsl +++ b/resources/shader/visu/lowpoly/deferred_lightpass.vert.glsl @@ -2,10 +2,10 @@ layout (location = 0) in vec3 a_position; layout (location = 1) in vec2 a_texture_cords; -out vec2 v_texture_cords; +out vec2 v_uv; void main() { - v_texture_cords = a_texture_cords; + v_uv = a_texture_cords; gl_Position = vec4(a_position, 1.0); } \ No newline at end of file diff --git a/resources/shader/visu/pbr_v1/bake_brdf_lut.frag.glsl b/resources/shader/visu/pbr_v1/bake_brdf_lut.frag.glsl new file mode 100644 index 0000000..35ef886 --- /dev/null +++ b/resources/shader/visu/pbr_v1/bake_brdf_lut.frag.glsl @@ -0,0 +1,83 @@ +#version 330 core + +out vec2 FragColor; +in vec2 v_uv; + +#include "visu/constants.glsl" +#include "visu/functions/brdf.glsl" +#include "visu/functions/importance_sampling.glsl" + +// ---------------------------------------------------------------------------- +// specialized geometry function for IBL - uses different k for IBL +float geometry_schlick_ggx_ibl(float NdotV, float roughness) +{ + // note that we use a different k for IBL + float a = roughness; + float k = (a * a) / 2.0; + + float nom = NdotV; + float denom = NdotV * (1.0 - k) + k; + + return nom / denom; +} + +float geometry_smith_ibl(vec3 N, vec3 V, vec3 L, float roughness) +{ + float NdotV = max(dot(N, V), 0.0); + float NdotL = max(dot(N, L), 0.0); + float ggx2 = geometry_schlick_ggx_ibl(NdotV, roughness); + float ggx1 = geometry_schlick_ggx_ibl(NdotL, roughness); + + return ggx1 * ggx2; +} + +// ---------------------------------------------------------------------------- +vec2 integrate_brdf(float NdotV, float roughness) +{ + vec3 V; + V.x = sqrt(1.0 - NdotV*NdotV); + V.y = 0.0; + V.z = NdotV; + + float A = 0.0; + float B = 0.0; + + vec3 N = vec3(0.0, 0.0, 1.0); + + const uint SAMPLE_COUNT = 4096u; + for(uint i = 0u; i < SAMPLE_COUNT; ++i) + { + // generates a sample vector that's biased towards the + // preferred alignment direction (importance sampling). + vec2 Xi = hammersley(i, SAMPLE_COUNT); + vec3 H = importance_sample_ggx(Xi, N, roughness); + vec3 L = normalize(2.0 * dot(V, H) * H - V); + + float NdotL = max(L.z, 0.0); + float NdotH = max(H.z, 0.0); + float VdotH = max(dot(V, H), 0.0); + + if(NdotL > 0.0) + { + float G = geometry_smith_ibl(N, V, L, roughness); + float G_Vis = (G * VdotH) / (NdotH * NdotV); + float Fc = pow(1.0 - VdotH, 5.0); + + A += (1.0 - Fc) * G_Vis; + B += Fc * G_Vis; + } + } + A /= float(SAMPLE_COUNT); + B /= float(SAMPLE_COUNT); + return vec2(A, B); +} + +// ---------------------------------------------------------------------------- +void main() +{ + vec2 brdf = integrate_brdf(v_uv.x, v_uv.y); + FragColor = brdf; +} + + + diff --git a/resources/shader/visu/pbr_v1/bake_brdf_lut.vert.glsl b/resources/shader/visu/pbr_v1/bake_brdf_lut.vert.glsl new file mode 100644 index 0000000..ccce8a6 --- /dev/null +++ b/resources/shader/visu/pbr_v1/bake_brdf_lut.vert.glsl @@ -0,0 +1,3 @@ +#version 330 core + +#include "visu/fullscreen_quad.glsl" \ No newline at end of file diff --git a/resources/shader/visu/pbr_v1/bake_irradiance.frag.glsl b/resources/shader/visu/pbr_v1/bake_irradiance.frag.glsl new file mode 100644 index 0000000..a74f223 --- /dev/null +++ b/resources/shader/visu/pbr_v1/bake_irradiance.frag.glsl @@ -0,0 +1,39 @@ +#version 330 core + +out vec4 FragColor; +in vec3 v_position; + +uniform samplerCube u_env_cubemap; + +#include "visu/constants.glsl" + +void main() +{ + vec3 N = normalize(v_position); + + vec3 irradiance = vec3(0.0); + + // tangent space calculation from origin point + vec3 up = vec3(0.0, 1.0, 0.0); + vec3 right = normalize(cross(up, N)); + up = normalize(cross(N, right)); + + float sampleDelta = 0.025; + float samples = 0.0f; + for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) + { + for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) + { + // spherical to cartesian (in tangent space) + vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); + // tangent space to world + vec3 sample = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; + + irradiance += texture(u_env_cubemap, sample).rgb * cos(theta) * sin(theta); + samples++; + } + } + irradiance = PI * irradiance * (1.0 / float(samples)); + + FragColor = vec4(irradiance, 1.0); +} \ No newline at end of file diff --git a/resources/shader/visu/pbr_v1/bake_irradiance.vert.glsl b/resources/shader/visu/pbr_v1/bake_irradiance.vert.glsl new file mode 100644 index 0000000..94c84b6 --- /dev/null +++ b/resources/shader/visu/pbr_v1/bake_irradiance.vert.glsl @@ -0,0 +1,3 @@ +#version 330 core + +#include "visu/cubemap_vert.glsl" \ No newline at end of file diff --git a/resources/shader/visu/pbr_v1/bake_prefiltered_env.frag.glsl b/resources/shader/visu/pbr_v1/bake_prefiltered_env.frag.glsl new file mode 100644 index 0000000..7d397cd --- /dev/null +++ b/resources/shader/visu/pbr_v1/bake_prefiltered_env.frag.glsl @@ -0,0 +1,109 @@ +#version 330 core + +out vec4 FragColor; +in vec3 v_position; + +uniform samplerCube u_env_cubemap; +uniform float u_roughness; +uniform float u_source_resolution; + +const float PI = 3.14159265359; + +// ---------------------------------------------------------------------------- +float distribution_ggx(vec3 N, vec3 H, float roughness) +{ + float a = roughness*roughness; + float a2 = a*a; + float NdotH = max(dot(N, H), 0.0); + float NdotH2 = NdotH*NdotH; + + float nom = a2; + float denom = (NdotH2 * (a2 - 1.0) + 1.0); + denom = PI * denom * denom; + + return nom / denom; +} + +// ---------------------------------------------------------------------------- +// http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html +// efficient VanDerCorpus calculation. +float radical_inverse_vdc(uint bits) +{ + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10; // / 0x100000000 +} + +// ---------------------------------------------------------------------------- +vec2 hammersley(uint i, uint N) +{ + return vec2(float(i)/float(N), radical_inverse_vdc(i)); +} + +// ---------------------------------------------------------------------------- +vec3 importance_sample_ggx(vec2 Xi, vec3 N, float roughness) +{ + float a = roughness*roughness; + + float phi = 2.0 * PI * Xi.x; + float cos_theta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y)); + float sin_theta = sqrt(1.0 - cos_theta*cos_theta); + + // from spherical coordinates to cartesian coordinates - halfway vector + vec3 H; + H.x = cos(phi) * sin_theta; + H.y = sin(phi) * sin_theta; + H.z = cos_theta; + + // from tangent-space H vector to world-space sample vector + vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); + vec3 tangent = normalize(cross(up, N)); + vec3 bitangent = cross(N, tangent); + + vec3 sample = tangent * H.x + bitangent * H.y + N * H.z; + return normalize(sample); +} + +// ---------------------------------------------------------------------------- +void main() +{ + vec3 N = normalize(v_position); + vec3 V = N; + + float rough = max(u_roughness, 1e-4); + + const uint SAMPLE_COUNT = 4096u; + vec3 prefiltered = vec3(0.0); + float weight = 0.0; + + for (uint i = 0u; i < SAMPLE_COUNT; ++i) + { + vec2 Xi = hammersley(i, SAMPLE_COUNT); + vec3 H = importance_sample_ggx(Xi, N, rough); + vec3 L = normalize(2.0 * dot(V, H) * H - V); + + float NdotL = max(dot(N, L), 0.0); + if (NdotL > 0.0) + { + float D = distribution_ggx(N, H, rough); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + + float pdf = D * NdotH / max(4.0 * HdotV, 1e-6) + 1e-4; + + float saTexel = 4.0 * PI / (6.0 * u_source_resolution * u_source_resolution); + float saSample = 1.0 / (float(SAMPLE_COUNT) * pdf + 1e-4); + + float mip = (u_roughness < 1e-4) ? 0.0 : 0.5 * log2(saSample / saTexel); + + prefiltered += textureLod(u_env_cubemap, L, mip).rgb * NdotL; + weight += NdotL; + } + } + + prefiltered /= max(weight, 1e-6); + FragColor = vec4(prefiltered, 1.0); +} diff --git a/resources/shader/visu/pbr_v1/bake_prefiltered_env.vert.glsl b/resources/shader/visu/pbr_v1/bake_prefiltered_env.vert.glsl new file mode 100644 index 0000000..94c84b6 --- /dev/null +++ b/resources/shader/visu/pbr_v1/bake_prefiltered_env.vert.glsl @@ -0,0 +1,3 @@ +#version 330 core + +#include "visu/cubemap_vert.glsl" \ No newline at end of file diff --git a/resources/shader/visu/ssao.frag.glsl b/resources/shader/visu/ssao.frag.glsl index d027615..63b4bc1 100644 --- a/resources/shader/visu/ssao.frag.glsl +++ b/resources/shader/visu/ssao.frag.glsl @@ -31,14 +31,14 @@ uniform mat4 projection; uniform mat4 inverse_projection; uniform mat4 normal_matrix; -vec4 getViewPos(vec2 texCoord) +vec4 get_view_pos(vec2 uv) { // calculate view space position from depth texture - float x = texCoord.s * 2.0 - 1.0; - float y = texCoord.t * 2.0 - 1.0; + float x = uv.s * 2.0 - 1.0; + float y = uv.t * 2.0 - 1.0; // get depth from depth buffer and convert to NDC - float z = texture(gbuffer_depth, texCoord).r * 2.0 - 1.0; + float z = texture(gbuffer_depth, uv).r * 2.0 - 1.0; vec4 posProj = vec4(x, y, z, 1.0); @@ -63,17 +63,17 @@ void main() } // calculate view space position from depth - vec4 view_position = getViewPos(v_uv); + vec4 view_pos = get_view_pos(v_uv); // get world normal and convert to view space - vec3 world_normal = texture(gbuffer_normal, v_uv).xyz; - vec3 view_normal = normalize(mat3(normal_matrix) * world_normal); + vec3 normal = texture(gbuffer_normal, v_uv).xyz; + vec3 view_normal = normalize(mat3(normal_matrix) * normal); // get noise vector for kernel rotation - vec3 noise_vec = normalize(texture(noise_texture, v_uv * noise_size).xyz * 2.0 - 1.0); + vec3 noise = normalize(texture(noise_texture, v_uv * noise_size).xyz * 2.0 - 1.0); // use Gram-Schmidt process to get orthogonal tangent vector - vec3 tangent = normalize(noise_vec - dot(noise_vec, view_normal) * view_normal); + vec3 tangent = normalize(noise - dot(noise, view_normal) * view_normal); vec3 bitangent = cross(view_normal, tangent); mat3 TBN = mat3(tangent, bitangent, view_normal); @@ -85,7 +85,7 @@ void main() vec3 sample_vec = TBN * samples[i]; // calculate sample point in view space - vec4 sample_position = view_position + radius * vec4(sample_vec, 0.0); + vec4 sample_position = view_pos + radius * vec4(sample_vec, 0.0); // project sample position to screen space vec4 sample_ndc = projection * sample_position; diff --git a/src/Graphics/Cubemap.php b/src/Graphics/Cubemap.php index e86f844..053a217 100644 --- a/src/Graphics/Cubemap.php +++ b/src/Graphics/Cubemap.php @@ -4,12 +4,101 @@ use GL\Buffer\BufferInterface; use GL\Buffer\FloatBuffer; +use GL\Math\Mat4; +use GL\Math\Vec3; use GL\Texture\Texture2D; use VISU\Exception\VISUException; use VISU\Graphics\Exception\TextureLoadException; class Cubemap -{ +{ + /** + * @var array|null The view matrices for capturing each cubemap face + */ + private static ?array $captureViews = null; + + /** + * Returns the view matrices for capturing each cubemap face + * + * @return array + */ + public static function captureViews() : array + { + if (self::$captureViews !== null) { + return self::$captureViews; + } + + // view matrices for each cubemap face + $captureViews = []; + + // +X face + $view = new Mat4(); + $view->lookAt( + new Vec3(0.0, 0.0, 0.0), + new Vec3( 1.0, 0.0, 0.0), + new Vec3(0.0, -1.0, 0.0) + ); + $captureViews[] = $view; + + // -X face + $view = new Mat4(); + $view->lookAt( + new Vec3(0.0, 0.0, 0.0), + new Vec3(-1.0, 0.0, 0.0), + new Vec3(0.0, -1.0, 0.0) + ); + $captureViews[] = $view; + + // +Y face + $view = new Mat4(); + $view->lookAt( + new Vec3(0.0, 0.0, 0.0), + new Vec3( 0.0, 1.0, 0.0), + new Vec3(0.0, 0.0, 1.0) + ); + $captureViews[] = $view; + + // -Y face + $view = new Mat4(); + $view->lookAt( + new Vec3(0.0, 0.0, 0.0), + new Vec3( 0.0, -1.0, 0.0), + new Vec3(0.0, 0.0, -1.0) + ); + $captureViews[] = $view; + + // +Z face + $view = new Mat4(); + $view->lookAt( + new Vec3(0.0, 0.0, 0.0), + new Vec3( 0.0, 0.0, 1.0), + new Vec3(0.0, -1.0, 0.0) + ); + $captureViews[] = $view; + + // -Z face + $view = new Mat4(); + $view->lookAt( + new Vec3(0.0, 0.0, 0.0), + new Vec3( 0.0, 0.0, -1.0), + new Vec3(0.0, -1.0, 0.0) + ); + $captureViews[] = $view; + + self::$captureViews = $captureViews; + return self::$captureViews; + } + + /** + * Returns a projection matrix for capturing cubemap faces + */ + public static function captureProjectionMatrix() : Mat4 + { + $captureProjection = new Mat4(); + $captureProjection->perspective(\GL\Math\GLM::radians(90.0), 1.0, 0.1, 10.0); + return $captureProjection; + } + /** * The GL texture id / handle */ @@ -59,9 +148,13 @@ public function __destruct() { glDeleteTextures(1, $this->id); - if ($this->gl->currentTexture === $this->id) { - $this->gl->currentTexture = 0; + // remove this cubemap from all tracked bindings + foreach ($this->gl->currentTextures as $unit => &$targets) { + if (isset($targets[GL_TEXTURE_CUBE_MAP]) && $targets[GL_TEXTURE_CUBE_MAP] === $this->id) { + $targets[GL_TEXTURE_CUBE_MAP] = 0; + } } + unset($targets); } /** @@ -87,15 +180,7 @@ public function setSize(int $size): void */ public function bind(int $unit = GL_TEXTURE0): void { - if ($this->gl->currentTextureUnit !== $unit) { - glActiveTexture($unit); - $this->gl->currentTextureUnit = $unit; - } - - if ($this->gl->currentTexture !== $this->id) { - glBindTexture(GL_TEXTURE_CUBE_MAP, $this->id); - $this->gl->currentTexture = $this->id; - } + $this->gl->bindTexture($unit, GL_TEXTURE_CUBE_MAP, $this->id); } /** @@ -103,17 +188,6 @@ public function bind(int $unit = GL_TEXTURE0): void */ private function applyFilterParameters(): void { - // to avoid incomplete textures ensure that mipmaps are generated - // when the min filter is set to mipmapped - if ($this->options->generateMipmaps === false) { - if ($this->options->minFilter === GL_LINEAR_MIPMAP_LINEAR || - $this->options->minFilter === GL_LINEAR_MIPMAP_NEAREST || - $this->options->minFilter === GL_NEAREST_MIPMAP_LINEAR || - $this->options->minFilter === GL_NEAREST_MIPMAP_NEAREST) { - throw new TextureLoadException("Mipmapped minification filter set but mipmaps are not generated, this results in incomplete textures"); - } - } - glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, $this->options->minFilter); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, $this->options->magFilter); @@ -368,7 +442,6 @@ public function readFace(int $faceIndex): FloatBuffer // create framebuffer for resource management $framebuffer = new Framebuffer($this->gl); - $framebuffer->bind(); $framebuffer->attachTextureId(GL_COLOR_ATTACHMENT0, self::FACE_TARGETS[$faceIndex], $this->id); // check framebuffer completeness diff --git a/src/Graphics/Framebuffer.php b/src/Graphics/Framebuffer.php index 51de457..4e394ee 100644 --- a/src/Graphics/Framebuffer.php +++ b/src/Graphics/Framebuffer.php @@ -61,6 +61,22 @@ public function createRenderbufferAttachment(int $format, int $attachment, int $ */ public function attachTextureId(int $attachment, int $textureTarget, int $textureId, int $level = 0): void { + $this->bind(); glFramebufferTexture2D(GL_FRAMEBUFFER, $attachment, $textureTarget, $textureId, $level); } + + /** + * Resizes the framebuffer attachments + */ + public function resizeAttachments(int $width, int $height): void + { + $this->bind(); + + foreach ($this->renderbufferAttachments as $attachment => $rbo) { + glBindRenderbuffer(GL_RENDERBUFFER, $rbo); + $format = 0; + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_INTERNAL_FORMAT, $format); + glRenderbufferStorage(GL_RENDERBUFFER, $format, $width, $height); + } + } } \ No newline at end of file diff --git a/src/Graphics/GLState.php b/src/Graphics/GLState.php index 7c4eef9..5db35bd 100644 --- a/src/Graphics/GLState.php +++ b/src/Graphics/GLState.php @@ -73,13 +73,13 @@ class GLState public int $currentIndexBuffer = 0; /** - * Currently bound texture object + * Currently bound textures per unit and target * * **Note:** You should never manually manipulate this value. * - * @var int + * @var array> */ - public int $currentTexture = 0; + public array $currentTextures = []; /** * Currently bound texture unit @@ -88,7 +88,7 @@ class GLState * * @var int */ - public int $currentTextureUnit = 0; + public int $currentTextureUnit = GL_TEXTURE0; /** * Resets the state of this object. @@ -101,8 +101,8 @@ public function reset() : void $this->currentVertexArray = 0; $this->currentVertexArrayBuffer = 0; $this->currentIndexBuffer = 0; - $this->currentTexture = 0; - $this->currentTextureUnit = 0; + $this->currentTextures = []; + $this->currentTextureUnit = GL_TEXTURE0; glUseProgram(0); glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); @@ -111,6 +111,7 @@ public function reset() : void glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); glBindTexture(GL_TEXTURE_2D, 0); + glBindTexture(GL_TEXTURE_CUBE_MAP, 0); glActiveTexture(GL_TEXTURE0); } @@ -170,4 +171,38 @@ public function bindIndexBuffer(int $ibo) : void glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, $ibo); } } + + /** + * State aware binding of texture to specific unit and target. + * + * @param int $unit The texture unit (GL_TEXTURE0, GL_TEXTURE1, etc.) + * @param int $target The texture target (GL_TEXTURE_2D, GL_TEXTURE_CUBE_MAP, etc.) + * @param int $textureId The texture ID to bind + */ + public function bindTexture(int $unit, int $target, int $textureId) : void + { + // check if we need to switch texture unit + if ($this->currentTextureUnit !== $unit) { + glActiveTexture($unit); + $this->currentTextureUnit = $unit; + } + + // check if this texture target on this unit is already bound to this texture + if (!isset($this->currentTextures[$unit][$target]) || $this->currentTextures[$unit][$target] !== $textureId) { + glBindTexture($target, $textureId); + $this->currentTextures[$unit][$target] = $textureId; + } + } + + /** + * Gets the currently bound texture for a specific unit and target. + * + * @param int $unit The texture unit + * @param int $target The texture target + * @return int The currently bound texture ID (0 if none) + */ + public function getBoundTexture(int $unit, int $target) : int + { + return $this->currentTextures[$unit][$target] ?? 0; + } } diff --git a/src/Graphics/HDRIToCubemap.php b/src/Graphics/HDRIToCubemap.php index 846531d..120713c 100644 --- a/src/Graphics/HDRIToCubemap.php +++ b/src/Graphics/HDRIToCubemap.php @@ -83,12 +83,15 @@ private function createShaders(): void vec2 uv = sampleSphere(normalize(local_pos)); vec3 color = texture(hdritex, uv).rgb; + // clamp extremely bright values to prevent numerical issues + color = min(color, vec3(65504.0)); // max value for 16-bit float + frag_color = color; } GLSL )); - $this->equirectangularToCubemapShader->link(); + $this->equirectangularToCubemapShader->link('equirectangular_to_cubemap'); } /** @@ -130,8 +133,8 @@ public function convert(string $hdriPath, int $cubemapSize = 512): Cubemap $options->internalFormat = GL_RGB16F; $options->dataFormat = GL_RGB; $options->dataType = GL_FLOAT; - $options->generateMipmaps = false; - $options->minFilter = GL_LINEAR; + $options->generateMipmaps = true; + $options->minFilter = GL_LINEAR_MIPMAP_LINEAR; $options->magFilter = GL_LINEAR; $cubemap->allocateEmpty($cubemapSize, $options); @@ -225,6 +228,12 @@ public function convert(string $hdriPath, int $cubemapSize = 512): Cubemap $cubemap->bind(); glGenerateMipmap(GL_TEXTURE_CUBE_MAP); + // unbind framebuffer to restore default state + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + // reset GL state + $this->gl->reset(); + return $cubemap; } diff --git a/src/Graphics/RenderTarget.php b/src/Graphics/RenderTarget.php index 3e6a4a2..b4e260e 100644 --- a/src/Graphics/RenderTarget.php +++ b/src/Graphics/RenderTarget.php @@ -4,6 +4,7 @@ use Exception; use GL\Math\Vec2; +use VISU\Exception\VISUException; class RenderTarget { @@ -86,6 +87,18 @@ public function framebuffer(): AbstractFramebuffer return $this->framebuffer; } + /** + * Returns the framebuffer only if it is of instance "Framebuffer" and so is offscreen + */ + public function offscreenFramebuffer(): Framebuffer + { + if (!($this->framebuffer instanceof Framebuffer)) { + return throw new VISUException("RenderTarget does not have an offscreen framebuffer"); + } + + return $this->framebuffer;; + } + /** * Returns boolean if the render target is an offscreen render target */ @@ -114,4 +127,34 @@ public function preparePass(): void $this->framebuffer->bind(); $this->updateViewport(); } + + /** + * Bind the framebuffer for reading + */ + public function bindForRead(): void + { + $this->framebuffer->bind(FramebufferTarget::READ); + } + + /** + * Resizes the render target to new dimensions + * Note: This only updates the internal dimensions and viewport, + * it does not resize any attached textures or renderbuffers + * + * @param int $width new width in pixels + * @param int $height new height in pixels + */ + public function resize(int $width, int $height): void + { + $this->width = $width; + $this->height = $height; + + // update the viewport if this render target is currently active + $this->framebuffer->bind(); + $this->updateViewport(); + + if ($this->framebuffer instanceof Framebuffer) { + $this->framebuffer->resizeAttachments($width, $height); + } + } } diff --git a/src/Graphics/Rendering/Pass/CubemapPass.php b/src/Graphics/Rendering/Pass/CubemapPass.php index 14991b9..b241fa2 100644 --- a/src/Graphics/Rendering/Pass/CubemapPass.php +++ b/src/Graphics/Rendering/Pass/CubemapPass.php @@ -11,6 +11,7 @@ use VISU\Graphics\Rendering\RenderPipeline; use VISU\Graphics\Rendering\Resource\CubemapResource; use VISU\Graphics\Rendering\Resource\RenderTargetResource; +use VISU\Graphics\Rendering\Resource\TextureResource; use VISU\Graphics\ShaderProgram; class CubemapPass extends RenderPass @@ -32,6 +33,7 @@ public function __construct( private RenderTargetResource $renderTargetRes, private CubemapResource $cubemapRes, private ShaderProgram $shader, + private ?TextureResource $depthTextureRes = null, ) { } @@ -43,6 +45,9 @@ public function setup(RenderPipeline $pipeline, PipelineContainer $data): void { $pipeline->reads($this, $this->cubemapRes); $pipeline->writes($this, $this->renderTargetRes); + if ($this->depthTextureRes !== null) { + $pipeline->reads($this, $this->depthTextureRes); + } } /** @@ -51,6 +56,9 @@ public function setup(RenderPipeline $pipeline, PipelineContainer $data): void public function execute(PipelineContainer $data, PipelineResources $resources): void { $resources->activateRenderTarget($this->renderTargetRes); + if ($this->depthTextureRes !== null) { + $resources->useDepthTexture($this->depthTextureRes); + } // fetch camera data $cameraData = $data->get(CameraData::class); @@ -65,6 +73,7 @@ public function execute(PipelineContainer $data, PipelineResources $resources): // we need to see the inner faces of the cubemap glDisable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); $glCubemap = $resources->getCubemap($this->cubemapRes); $this->shader->setUniform1i($this->cubemapUniformName, 0); diff --git a/src/Graphics/Rendering/Pass/DeferredLightPass.php b/src/Graphics/Rendering/Pass/DeferredLightPass.php index b5c1fc1..1b03f38 100644 --- a/src/Graphics/Rendering/Pass/DeferredLightPass.php +++ b/src/Graphics/Rendering/Pass/DeferredLightPass.php @@ -11,18 +11,32 @@ use VISU\Graphics\Rendering\PipelineResources; use VISU\Graphics\Rendering\RenderPass; use VISU\Graphics\Rendering\RenderPipeline; +use VISU\Graphics\Rendering\Resource\CubemapResource; +use VISU\Graphics\Rendering\Resource\TextureResource; +use VISU\Graphics\ShaderCollection; use VISU\Graphics\ShaderProgram; class DeferredLightPass extends RenderPass { + /** + * The name of the shader in the collection + */ + public const SHADER_NAME = 'visu/lowpoly/deferred_lightpass'; + /** * Constructor */ public function __construct( - private ShaderProgram $lightingShader, - private DirectionalLightComponent $sun + private ShaderCollection $shaders, + private DirectionalLightComponent $sun, + private ?CubemapResource $environmentCubemap = null, ) { + // register lighting shader permutations for different environment setups + $this->shaders->definePermutations( + self::SHADER_NAME, + DeferredLightPassPermutation::getAllPermutationDefines() + ); } /** @@ -40,11 +54,57 @@ public function setup(RenderPipeline $pipeline, PipelineContainer $data): void $pipeline->reads($this, $gbufferData->roughnessTexture); $pipeline->reads($this, $gbufferData->emissiveTexture); + // read environment cubemap if available + if ($this->environmentCubemap) { + $pipeline->reads($this, $this->environmentCubemap); + } + + // read IBL precompute data if available + if ($data->has(IBLPrecomputeData::class)) { + $iblData = $data->get(IBLPrecomputeData::class); + $pipeline->reads($this, $iblData->irradianceCubemap); + $pipeline->reads($this, $iblData->prefilterCubemap); + $pipeline->reads($this, $iblData->brdfLut); + } + // create light pass target with the same size as the gbuffer $lightpassData->renderTarget = $pipeline->createRenderTarget('lightpass', $gbufferData->renderTarget->width, $gbufferData->renderTarget->height); $lightpassData->output = $pipeline->createColorAttachment($lightpassData->renderTarget, 'lightpass_output'); } + /** + * Determines which shader permutation to use based on available resources + */ + private function determinePermutation(PipelineContainer $data, PipelineResources $resources): DeferredLightPassPermutation + { + // check if IBL data is available and valid + if ($data->has(IBLPrecomputeData::class)) { + $iblData = $data->get(IBLPrecomputeData::class); + + $irradianceCubemap = $resources->getCubemap($iblData->irradianceCubemap); + $prefilterCubemap = $resources->getCubemap($iblData->prefilterCubemap); + $brdfLut = $resources->getTexture($iblData->brdfLut); + + // full IBL when all three IBL textures are available + if ($irradianceCubemap && $irradianceCubemap->id > 0 && + $prefilterCubemap && $prefilterCubemap->id > 0 && + $brdfLut && $brdfLut->id > 0) { + return DeferredLightPassPermutation::IBL; + } + } + + // check if environment cubemap is available + if ($this->environmentCubemap) { + $glCubemap = $resources->getCubemap($this->environmentCubemap); + if ($glCubemap && $glCubemap->id > 0) { + return DeferredLightPassPermutation::envCubemap; + } + } + + // basic lighting only + return DeferredLightPassPermutation::basic; + } + /** * Executes the render pass */ @@ -62,25 +122,25 @@ public function execute(PipelineContainer $data, PipelineResources $resources): return new QuadVertexArray($gl); }); - // prepare the shader - $this->lightingShader->use(); - $this->lightingShader->setUniformVec3('camera_position', $cameraData->renderCamera->transform->position); - $this->lightingShader->setUniform2f('camera_resolution', $cameraData->resolutionX, $cameraData->resolutionY); + // determine the appropriate shader permutation + $permutation = $this->determinePermutation($data, $resources); + $shader = $this->shaders->getPermutation(self::SHADER_NAME, $permutation->value); - // set sun properties - $this->sun->direction->x = sin((glfwGetTime() - 1000) * 0.001) * 3; - $this->sun->direction->y = 1.0; - $this->sun->intensity = 1.0; - $this->sun->direction->normalize(); + // prepare the shader + $shader->use(); + $shader->setUniformVec3('camera_position', $cameraData->renderCamera->transform->position); + $shader->setUniform2f('camera_resolution', $cameraData->resolutionX, $cameraData->resolutionY); - // D3D::ray(new Vec3(0.0), $this->sun->direction, D3D::$colorYellow, 200.0); - // D3D::cross(new Vec3(0.0), D3D::$colorYellow, 50.0); + // set sun properties (use values from sun component directly) + $sunDir = $this->sun->direction->copy(); + $sunDir->normalize(); - $this->lightingShader->setUniformVec3('sun_direction', $this->sun->direction); - $this->lightingShader->setUniformVec3('sun_color', $this->sun->color); - $this->lightingShader->setUniform1f('sun_intensity', $this->sun->intensity); + $shader->setUniformVec3('sun_direction', $sunDir); + $shader->setUniformVec3('sun_color', $this->sun->color); + $shader->setUniform1f('sun_intensity', $this->sun->intensity); // bind the gbuffer textures + $textureUnit = 0; foreach([ [$gbufferData->worldSpacePositionTexture, 'position'], [$gbufferData->normalTexture, 'normal'], @@ -93,10 +153,48 @@ public function execute(PipelineContainer $data, PipelineResources $resources): ] as $i => $tuple) { list($texture, $name) = $tuple; $glTexture = $resources->getTexture($texture); - $glTexture->bind(GL_TEXTURE0 + $i); - $this->lightingShader->setUniform1i('gbuffer_' . $name, $i); + $glTexture->bind(GL_TEXTURE0 + $textureUnit); + $shader->setUniform1i('gbuffer_' . $name, $textureUnit); + $textureUnit++; } + // bind permutation-specific uniforms + if ($permutation === DeferredLightPassPermutation::envCubemap) { + $glCubemap = $resources->getCubemap($this->environmentCubemap); + $glCubemap->bind(GL_TEXTURE0 + $textureUnit); + $shader->setUniform1i('environment_cubemap', $textureUnit); + + // clamp mip count to a valid range so the shader can use lod based sampling + $mipCount = max(1, (int) floor(log(max($glCubemap->size(), 1), 2)) + 1); + $shader->setUniform1i('environment_mip_count', $mipCount); + $textureUnit++; + } + elseif ($permutation === DeferredLightPassPermutation::IBL) { + $iblData = $data->get(IBLPrecomputeData::class); + + // irradiance map + $irradianceCubemap = $resources->getCubemap($iblData->irradianceCubemap); + $irradianceCubemap->bind(GL_TEXTURE0 + $textureUnit); + $shader->setUniform1i('ibl_irradiance_map', $textureUnit); + $textureUnit++; + + // prefiltered environment map + $prefilterCubemap = $resources->getCubemap($iblData->prefilterCubemap); + $prefilterCubemap->bind(GL_TEXTURE0 + $textureUnit); + $shader->setUniform1i('ibl_prefilter_map', $textureUnit); + $shader->setUniform1i('prefilter_mip_count', $iblData->prefilterMipLevels); + $textureUnit++; + + // brdf lookup texture + $brdfLut = $resources->getTexture($iblData->brdfLut); + $brdfLut->bind(GL_TEXTURE0 + $textureUnit); + $shader->setUniform1i('ibl_brdf_lut', $textureUnit); + $textureUnit++; + } + + // enable seamless cubemap sampling for IBL + glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); + glDisable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); diff --git a/src/Graphics/Rendering/Pass/DeferredLightPassPermutation.php b/src/Graphics/Rendering/Pass/DeferredLightPassPermutation.php new file mode 100644 index 0000000..32793b4 --- /dev/null +++ b/src/Graphics/Rendering/Pass/DeferredLightPassPermutation.php @@ -0,0 +1,52 @@ + + */ + public function getDefines(): array + { + return match ($this) { + self::basic => [], + self::envCubemap => ['USE_ENV_CUBEMAP' => 1], + self::IBL => ['USE_IBL' => 1], + }; + } + + /** + * Returns all permutations as an array suitable for ShaderCollection::definePermutations() + * + * @return array> + */ + public static function getAllPermutationDefines(): array + { + $permutations = []; + foreach (self::cases() as $case) { + $permutations[$case->value] = $case->getDefines(); + } + return $permutations; + } +} diff --git a/src/Graphics/Rendering/Pass/IBLCache.php b/src/Graphics/Rendering/Pass/IBLCache.php new file mode 100644 index 0000000..8449c0c --- /dev/null +++ b/src/Graphics/Rendering/Pass/IBLCache.php @@ -0,0 +1,64 @@ +isValid && $this->sourceCubemapId === $sourceCubemap->id; + } + + /** + * Invalidates the cache, forcing a recompute on next use + */ + public function invalidate(): void + { + $this->isValid = false; + $this->sourceCubemapId = 0; + } + + /** + * Marks the cache as valid for the given source cubemap + * + * @param Cubemap $sourceCubemap The environment cubemap this cache was computed from + */ + public function markValid(Cubemap $sourceCubemap): void + { + $this->sourceCubemapId = $sourceCubemap->id; + $this->isValid = true; + } +} diff --git a/src/Graphics/Rendering/Pass/IBLPrecomputeData.php b/src/Graphics/Rendering/Pass/IBLPrecomputeData.php new file mode 100644 index 0000000..81b0d7f --- /dev/null +++ b/src/Graphics/Rendering/Pass/IBLPrecomputeData.php @@ -0,0 +1,30 @@ +reads($this, $this->environmentCubemap); + + $iblData = $data->create(IBLPrecomputeData::class); + + // create the irradiance resources + $iblData->irradianceRenderTarget = $pipeline->createRenderTarget('ibl_precompute_irradiance_rt', $this->irradianceSize, $this->irradianceSize); + $iblData->irradianceRenderTarget->createRenderbufferDepthStencil = true; + $irradianceTextureOptions = new TextureOptions; + $irradianceTextureOptions->internalFormat = GL_RGB16F; + $irradianceTextureOptions->dataFormat = GL_RGB; + $irradianceTextureOptions->dataType = GL_FLOAT; + $irradianceTextureOptions->generateMipmaps = false; + $irradianceTextureOptions->minFilter = GL_LINEAR; + $irradianceTextureOptions->magFilter = GL_LINEAR; + $irradianceTextureOptions->wrapS = GL_CLAMP_TO_EDGE; + $irradianceTextureOptions->wrapT = GL_CLAMP_TO_EDGE; + $iblData->irradianceCubemap = $pipeline->createCubemapColorAttachment($iblData->irradianceRenderTarget, 'ibl_precompute_irradiance', $irradianceTextureOptions); + + // create the prefilter resources + $iblData->prefilterRenderTarget = $pipeline->createRenderTarget('ibl_precompute_prefilter_rt', $this->prefilterSize, $this->prefilterSize); + $iblData->prefilterRenderTarget->createRenderbufferDepthStencil = true; + $prefilterTextureOptions = new TextureOptions; + $prefilterTextureOptions->internalFormat = GL_RGB16F; + $prefilterTextureOptions->dataFormat = GL_RGB; + $prefilterTextureOptions->dataType = GL_FLOAT; + $prefilterTextureOptions->generateMipmaps = true; + $prefilterTextureOptions->minFilter = GL_LINEAR_MIPMAP_LINEAR; + $prefilterTextureOptions->magFilter = GL_LINEAR; + $prefilterTextureOptions->wrapS = GL_CLAMP_TO_EDGE; + $prefilterTextureOptions->wrapT = GL_CLAMP_TO_EDGE; + $iblData->prefilterCubemap = $pipeline->createCubemapColorAttachment($iblData->prefilterRenderTarget, 'ibl_precompute_prefilter', $prefilterTextureOptions); + + // and the BRDF LUT resources + $iblData->brdfLutRenderTarget = $pipeline->createRenderTarget('ibl_precompute_brdf_lut_rt', $this->brdfLutSize, $this->brdfLutSize); + $iblData->brdfLutRenderTarget->createRenderbufferDepthStencil = false; + $brdfLutTextureOptions = new TextureOptions; + $brdfLutTextureOptions->internalFormat = GL_RG16F; + $brdfLutTextureOptions->dataFormat = GL_RG; + $brdfLutTextureOptions->dataType = GL_FLOAT; + $brdfLutTextureOptions->generateMipmaps = false; + $brdfLutTextureOptions->minFilter = GL_LINEAR; + $brdfLutTextureOptions->magFilter = GL_LINEAR; + $brdfLutTextureOptions->wrapS = GL_CLAMP_TO_EDGE; + $brdfLutTextureOptions->wrapT = GL_CLAMP_TO_EDGE; + $iblData->brdfLut = $pipeline->createColorAttachment($iblData->brdfLutRenderTarget, 'ibl_precompute_brdf_lut', $brdfLutTextureOptions); + + // calculate mip levels based on prefilter size (stop at 8x8 minimum) + $minMipSize = 8; + $iblData->prefilterMipLevels = (int) floor(log($this->prefilterSize / $minMipSize, 2)) + 1; + + // store the cache reference in the data for the light pass to use + $iblData->cache = $this->cache; + } + + /** + * Forces the IBL maps to be recomputed on the next frame + */ + public function invalidateCache(): void + { + if ($this->cache !== null) { + $this->cache->invalidate(); + } + } + + public function execute(PipelineContainer $data, PipelineResources $resources): void + { + $iblData = $data->get(IBLPrecomputeData::class); + $envCubemap = $resources->getCubemap($this->environmentCubemap); + + // check if the cache is valid for this environment cubemap + if ($this->cache !== null && $this->cache->isValidFor($envCubemap)) { + // cache is valid, skip the expensive computation + return; + } + + // cube geometry + /** @var CubeVertexArray */ + $cubeVA = $resources->cacheStaticResource('ibl_cube_va', function(GLState $gl) { + return new CubeVertexArray($gl); + }); + + /** @var QuadVertexArray */ + $quadVA = $resources->cacheStaticResource('ibl_quad_va', function(GLState $gl) { + return new QuadVertexArray($gl); + }); + + + // disable depth testing and culling for IBL baking + glEnable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + + // first the irradiance map + $rt = $resources->activateRenderTarget($iblData->irradianceRenderTarget); + $irradianceCubemap = $resources->getCubemap($iblData->irradianceCubemap); + + $captureViews = Cubemap::captureViews(); + $captureProjection = Cubemap::captureProjectionMatrix(); + + $this->irradianceShader->use(); + $this->irradianceShader->setUniformMat4('projection', false, $captureProjection); + $this->irradianceShader->bindTextures([ + 'u_env_cubemap' => $envCubemap, + ]); + + $cubeVA->bind(); + for ($face = 0; $face < 6; $face++) { + $rt->offscreenFramebuffer()->attachTextureId(GL_COLOR_ATTACHMENT0, Cubemap::FACE_TARGETS[$face], $irradianceCubemap->id); + $rt->offscreenFramebuffer()->clear(); + + $this->irradianceShader->setUniformMat4('view', false, $captureViews[$face]); + + $cubeVA->draw(); + } + + // next the prefiltered environment map + $rt = $resources->activateRenderTarget($iblData->prefilterRenderTarget); + $prefilterCubemap = $resources->getCubemap($iblData->prefilterCubemap); + + $this->prefilterShader->use(); + $this->prefilterShader->setUniformMat4('projection', false, $captureProjection); + $this->prefilterShader->setUniform1f('u_source_resolution', (float) $envCubemap->size()); + $this->prefilterShader->bindTextures([ + 'u_env_cubemap' => $envCubemap, + ]); + + // use the mip level count calculated in setup() + $maxMipLevels = $iblData->prefilterMipLevels; + for ($mip = 0; $mip < $maxMipLevels; $mip++) { + // resize framebuffer according to mip-level size + $mipWidth = (int) ($this->prefilterSize * pow(0.5, $mip)); + $mipHeight = (int) ($this->prefilterSize * pow(0.5, $mip)); + $rt->resize($mipWidth, $mipHeight); + + $roughness = $mip / ($maxMipLevels - 1); + $this->prefilterShader->setUniform1f('u_roughness', $roughness); + + for ($face = 0; $face < 6; $face++) { + $rt->offscreenFramebuffer()->attachTextureId(GL_COLOR_ATTACHMENT0, Cubemap::FACE_TARGETS[$face], $prefilterCubemap->id, $mip); + $rt->offscreenFramebuffer()->clear(); + + $this->prefilterShader->setUniformMat4('view', false, $captureViews[$face]); + + $cubeVA->draw(); + } + } + + // now that all mip levels are rendered, enable mipmap filtering + $prefilterCubemap->bind(); + glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + + // finally the BRDF LUT + $rt = $resources->activateRenderTarget($iblData->brdfLutRenderTarget); + $rt->offscreenFramebuffer()->clear(); + $this->brdfShader->use(); + + $quadVA->bind(); + $quadVA->draw(); + + // mark the cache as valid for this environment cubemap + if ($this->cache !== null) { + $this->cache->markValid($envCubemap); + } + } +} diff --git a/src/Graphics/Rendering/PipelineResources.php b/src/Graphics/Rendering/PipelineResources.php index b52630d..f90a844 100644 --- a/src/Graphics/Rendering/PipelineResources.php +++ b/src/Graphics/Rendering/PipelineResources.php @@ -6,7 +6,10 @@ use VISU\Graphics\Exception\PipelineResourceException; use VISU\Graphics\Framebuffer; use VISU\Graphics\GLState; +use VISU\Graphics\FramebufferTarget; +use VISU\Graphics\Rendering\Resource\CubemapResource; use VISU\Graphics\Rendering\Resource\RenderTargetResource; +use VISU\Graphics\Rendering\Resource\TextureResource; use VISU\Graphics\RenderTarget; use VISU\Graphics\Texture; use VISU\Graphics\TextureOptions; @@ -115,30 +118,51 @@ private function createRenderTarget(RenderTargetResource $resource) : void // attach color attachments foreach($resource->colorAttachments as $i => $colorAttachmentTextureResource) { - $texture = new Texture($this->gl, $colorAttachmentTextureResource->name); - $options = $colorAttachmentTextureResource->options ?? new TextureOptions; - // if min filter is using a mipmap, fallback to linear - if ($options->minFilter === GL_NEAREST_MIPMAP_NEAREST || $options->minFilter === GL_NEAREST_MIPMAP_LINEAR) { - $options->minFilter = GL_NEAREST; - } elseif ($options->minFilter === GL_LINEAR_MIPMAP_NEAREST || $options->minFilter === GL_LINEAR_MIPMAP_LINEAR) { - $options->minFilter = GL_LINEAR; + if ($colorAttachmentTextureResource instanceof TextureResource) { + $texture = new Texture($this->gl, $colorAttachmentTextureResource->name); + $options = $colorAttachmentTextureResource->options ?? new TextureOptions; + + // if min filter is using a mipmap, fallback to linear + if ($options->minFilter === GL_NEAREST_MIPMAP_NEAREST || $options->minFilter === GL_NEAREST_MIPMAP_LINEAR) { + $options->minFilter = GL_NEAREST; + } elseif ($options->minFilter === GL_LINEAR_MIPMAP_NEAREST || $options->minFilter === GL_LINEAR_MIPMAP_LINEAR) { + $options->minFilter = GL_LINEAR; + } + $options->generateMipmaps = false; + $texture->allocateEmpty( + $colorAttachmentTextureResource->width, + $colorAttachmentTextureResource->height, + $options + ); + + // store the texture + $this->textures[$colorAttachmentTextureResource->name] = $texture; + + $target->framebuffer()->bind(); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + $i, $texture->target, $texture->id, 0); + + $drawBuffers[] = GL_COLOR_ATTACHMENT0 + $i; } - $options->generateMipmaps = false; - $texture->allocateEmpty( - $colorAttachmentTextureResource->width, - $colorAttachmentTextureResource->height, - $options - ); + else if ($colorAttachmentTextureResource instanceof CubemapResource) { + $cubemap = new Cubemap($this->gl, $colorAttachmentTextureResource->name); + $options = $colorAttachmentTextureResource->options ?? new TextureOptions; + + $cubemap->allocateEmpty( + $colorAttachmentTextureResource->size, + $options + ); - // store the texture - $this->textures[$colorAttachmentTextureResource->name] = $texture; + // store the cubemap + $this->cubemaps[$colorAttachmentTextureResource->name] = $cubemap; - $target->framebuffer()->bind(); + $target->framebuffer()->bind(); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + $i, $texture->target, $texture->id, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + $i, Cubemap::FACE_TARGETS[0], $cubemap->id, 0); - $drawBuffers[] = GL_COLOR_ATTACHMENT0 + $i; + $drawBuffers[] = GL_COLOR_ATTACHMENT0 + $i; + } } if (!empty($drawBuffers)) { @@ -264,6 +288,56 @@ public function activateRenderTarget(RenderTargetResource $resource): RenderTarg return $target; } + /** + * Activates a depth buffer to be read from + * + * @param RenderTargetResource $resource + */ + public function useDepthFrom(RenderTargetResource $resource): void + { + if ($resource->depthAttachment === null) { + throw new PipelineResourceException('The requested depth source has no depth attachment.'); + } + + // delegate to texture-based variant using the render target dimensions + $this->useDepthTexture( + $resource->depthAttachment, + $resource->width, + $resource->height + ); + } + + /** + * Uses a depth texture as depth attachment for the currently active render target. + * The texture must be the same size as the active render target. + */ + public function useDepthTexture(TextureResource $depthResource, ?int $expectedWidth = null, ?int $expectedHeight = null): void + { + if ($this->activeRenderTarget === null) { + throw new PipelineResourceException('Cannot use depth buffer before a render target is active.'); + } + + $depthTexture = $this->getTexture($depthResource); + + $targetWidth = $this->activeRenderTarget->width(); + $targetHeight = $this->activeRenderTarget->height(); + + $requiredWidth = $expectedWidth ?? $depthResource->width; + $requiredHeight = $expectedHeight ?? $depthResource->height; + + if ($requiredWidth !== $targetWidth || $requiredHeight !== $targetHeight) { + throw new PipelineResourceException('Depth source size does not match the active render target size.'); + } + + $destinationFramebuffer = $this->activeRenderTarget->framebuffer(); + if (!$destinationFramebuffer instanceof Framebuffer) { + throw new PipelineResourceException('Cannot attach a depth buffer to the active render target.'); + } + + $destinationFramebuffer->bind(FramebufferTarget::READ_DRAW); + $destinationFramebuffer->attachTextureId(GL_DEPTH_ATTACHMENT, $depthTexture->target, $depthTexture->id); + } + /** * Returns the currently active render target, throws an exception if none is active * diff --git a/src/Graphics/Rendering/RenderPipeline.php b/src/Graphics/Rendering/RenderPipeline.php index 4616d04..f1be66a 100644 --- a/src/Graphics/Rendering/RenderPipeline.php +++ b/src/Graphics/Rendering/RenderPipeline.php @@ -172,7 +172,22 @@ public function createColorAttachment(RenderTargetResource $target, string $name { /** @var TextureResource */ $resource = $this->createResource(TextureResource::class, $target->name . '.attachment.color_' . $name, $target->width, $target->height, $options); + $target->colorAttachments[] = $resource; + + return $resource; + } + /** + * Creates a cubemap color attachment for a render target resource + * + * @param RenderTargetResource $target + * @param string $name + * @param TextureOptions|null $options Optional texture options for the attachment + */ + public function createCubemapColorAttachment(RenderTargetResource $target, string $name, ?TextureOptions $options = null): CubemapResource + { + /** @var CubemapResource */ + $resource = $this->createResource(CubemapResource::class, $target->name . '.attachment.cubemap_' . $name, $target->width, $options); $target->colorAttachments[] = $resource; return $resource; @@ -230,6 +245,26 @@ public function importCubemap(string $resourceName, Cubemap $cubemap): CubemapRe return $resource; } + /** + * Creates an empty cubemap resource without allocating GL storage. + */ + public function createCubemapResource(string $resourceName, int $size, ?TextureOptions $options = null): CubemapResource + { + /** @var CubemapResource */ + return $this->createResource(CubemapResource::class, $resourceName, $size, $options); + } + + /** + * Creates a standalone 2D texture resource. + */ + public function createTextureResource(string $resourceName, int $width, int $height, ?TextureOptions $options = null): TextureResource + { + /** @var TextureResource */ + $resource = $this->createResource(TextureResource::class, $resourceName, $width, $height, $options); + $resource->options = $options; + return $resource; + } + /** * Adds a new render pass to the pipeline * diff --git a/src/Graphics/Rendering/Renderer/CubemapRenderer.php b/src/Graphics/Rendering/Renderer/CubemapRenderer.php index 47df1a3..34b5de3 100644 --- a/src/Graphics/Rendering/Renderer/CubemapRenderer.php +++ b/src/Graphics/Rendering/Renderer/CubemapRenderer.php @@ -7,6 +7,7 @@ use VISU\Graphics\Rendering\RenderPipeline; use VISU\Graphics\Rendering\Resource\CubemapResource; use VISU\Graphics\Rendering\Resource\RenderTargetResource; +use VISU\Graphics\Rendering\Resource\TextureResource; use VISU\Graphics\ShaderProgram; use VISU\Graphics\ShaderStage; @@ -60,13 +61,30 @@ public function __construct( uniform samplerCube u_skybox; + // simple reinhard tonemapping + vec3 reinhard(vec3 color) { + return color / (color + vec3(1.0)); + } + + // gamma correction + vec3 gammaCorrect(vec3 color) { + return pow(color, vec3(1.0/2.2)); + } + void main() { - frag_color = texture(u_skybox, tex_coords); + // vec3 hdrColor = textureLod(u_skybox, tex_coords, 0).rgb; + vec3 hdrColor = texture(u_skybox, tex_coords).rgb; + + // apply tonemapping and gamma correction + vec3 mapped = reinhard(hdrColor); + vec3 gamma = gammaCorrect(mapped); + + frag_color = vec4(gamma, 1.0); } GLSL)); - $this->skyboxShaderProgram->link(); + $this->skyboxShaderProgram->link('skybox'); } /** @@ -76,12 +94,14 @@ public function attachSkyboxPass( RenderPipeline $pipeline, RenderTargetResource $renderTarget, CubemapResource $cubemap, + ?TextureResource $depthTexture = null, ) : CubemapPass { $pass = new CubemapPass( $renderTarget, $cubemap, $this->skyboxShaderProgram, + $depthTexture, ); $pipeline->addPass($pass); diff --git a/src/Graphics/Rendering/Renderer/Debug3DRenderer.php b/src/Graphics/Rendering/Renderer/Debug3DRenderer.php index 8ab42c2..f982574 100644 --- a/src/Graphics/Rendering/Renderer/Debug3DRenderer.php +++ b/src/Graphics/Rendering/Renderer/Debug3DRenderer.php @@ -232,7 +232,7 @@ public function __construct( fragment_color = vec4(v_color, 1.0f); } GLSL)); - $this->shaderProgram->link(); + $this->shaderProgram->link('D3D'); } /** diff --git a/src/Graphics/Rendering/Renderer/SSAORenderer.php b/src/Graphics/Rendering/Renderer/SSAORenderer.php index ddc53b9..b9ad35b 100644 --- a/src/Graphics/Rendering/Renderer/SSAORenderer.php +++ b/src/Graphics/Rendering/Renderer/SSAORenderer.php @@ -239,9 +239,6 @@ function(PipelineContainer $data, PipelineResources $resources) $this->ssaoShaderProgram->setUniform1f('strength', $this->currentQuality->strength); $this->ssaoShaderProgram->setUniform1i('sample_count', $this->currentQuality->sampleCount); - $normalMatrix = $cameradData->view->copy(); - $normalMatrix->transpose(); - $normalMatrix->inverse(); $this->ssaoShaderProgram->setUniformMat4('normal_matrix', false, $cameradData->view); $this->ssaoShaderProgram->setUniformVec3Array('samples', $this->kernel); diff --git a/src/Graphics/Rendering/Resource/RenderTargetResource.php b/src/Graphics/Rendering/Resource/RenderTargetResource.php index 9c5e646..3621613 100644 --- a/src/Graphics/Rendering/Resource/RenderTargetResource.php +++ b/src/Graphics/Rendering/Resource/RenderTargetResource.php @@ -30,7 +30,7 @@ class RenderTargetResource extends RenderResource /** * An array of TextureResource objects that are attached to the render target * - * @var array + * @var array */ public array $colorAttachments = []; diff --git a/src/Graphics/ShaderCollection.php b/src/Graphics/ShaderCollection.php index 70ba5af..436deb6 100644 --- a/src/Graphics/ShaderCollection.php +++ b/src/Graphics/ShaderCollection.php @@ -28,6 +28,20 @@ class ShaderCollection */ private array $globalDefines = []; + /** + * Array of permutation definitions for shaders + * + * @var array>> + */ + private array $permutationDefinitions = []; + + /** + * Array of compiled shader permutations + * + * @var array + */ + private array $shaderPermutations = []; + /** * Shader file loader instance */ @@ -66,6 +80,23 @@ public function setGlobalDefine(string $name, $value): void $this->globalDefines[$name] = $value; } + /** + * Defines shader permutations with different define combinations + * + * @param string $shaderName The base name of the shader (without permutation suffix) + * @param array> $permutations Array of permutation name => defines + * + * Example: + * $shaders->definePermutations('lighting', [ + * 'basic' => ['USE_SHADOWS' => 0, 'MAX_LIGHTS' => 4], + * 'advanced' => ['USE_SHADOWS' => 1, 'MAX_LIGHTS' => 8, 'USE_IBL' => 1] + * ]); + */ + public function definePermutations(string $shaderName, array $permutations): void + { + $this->permutationDefinitions[$shaderName] = $permutations; + } + /** * Adds a shader program to the collection * @@ -125,7 +156,7 @@ public function get(string $name) : ShaderProgram // link the shader program try { - $shaderProgram->link(); + $shaderProgram->link($name); } catch(ShaderException $e) { throw new ShaderProgramLinkingException("ShaderException ('{$name}'): " . $e->getMessage(), $e->getCode(), $e); } @@ -137,6 +168,90 @@ public function get(string $name) : ShaderProgram return $this->shaderPrograms[$name]; } + /** + * Returns a specific permutation of a shader program + * + * @param string $shaderName The base name of the shader + * @param string $permutationName The name of the permutation variant + * @return ShaderProgram The compiled shader program with the permutation defines + * + * Example: $shader = $shaders->getPermutation('lighting', 'advanced'); + */ + public function getPermutation(string $shaderName, string $permutationName): ShaderProgram + { + $permutationKey = $shaderName . '#' . $permutationName; + + if (!isset($this->shaderPermutations[$permutationKey])) { + // check if the permutation is defined + if (!isset($this->permutationDefinitions[$shaderName][$permutationName])) { + throw new ShaderException("Permutation '{$permutationName}' not defined for shader '{$shaderName}'"); + } + + // check if the base shader files exist + if (!isset($this->avilableShaderFiles[$shaderName])) { + throw new ShaderException("Base shader '{$shaderName}' does not exist and is not registered"); + } + + $shaderProgram = new ShaderProgram($this->gl); + $permutationDefines = $this->permutationDefinitions[$shaderName][$permutationName]; + + // merge global defines with permutation-specific defines + $combinedDefines = array_merge($this->globalDefines, $permutationDefines); + + // attach all shader stages with the combined defines + foreach($this->avilableShaderFiles[$shaderName] as $stage => $path) { + $shaderProgram->attach(new ShaderStage($stage, $this->shaderFileLoader->loadShader($path, $combinedDefines))); + } + + // link the shader program + try { + $shaderProgram->link($permutationKey); + } catch(ShaderException $e) { + throw new ShaderProgramLinkingException("ShaderException ('{$permutationKey}'): " . $e->getMessage(), $e->getCode(), $e); + } + + $this->shaderPermutations[$permutationKey] = $shaderProgram; + } + + return $this->shaderPermutations[$permutationKey]; + } + + /** + * Gets the available permutation names for a shader + * + * @param string $shaderName The base name of the shader + * @return array Array of available permutation names + */ + public function getAvailablePermutations(string $shaderName): array + { + return isset($this->permutationDefinitions[$shaderName]) ? array_keys($this->permutationDefinitions[$shaderName]) : []; + } + + /** + * Checks if a shader has permutation variants defined + * + * @param string $shaderName The base name of the shader + * @return bool True if permutations are defined + */ + public function hasPermutations(string $shaderName): bool + { + return isset($this->permutationDefinitions[$shaderName]) && !empty($this->permutationDefinitions[$shaderName]); + } + + /** + * Clears all cached permutations for a shader (useful for hot-reloading) + * + * @param string $shaderName The base name of the shader + */ + public function clearPermutations(string $shaderName): void + { + foreach ($this->shaderPermutations as $key => $shader) { + if (str_starts_with($key, $shaderName . '#')) { + unset($this->shaderPermutations[$key]); + } + } + } + /** * Scans the shader directory for shader files and adds them to the collection * @@ -235,4 +350,34 @@ public function loadAll(?callable $callback = null) : void if ($callback) $callback($name, $shader); } } + + /** + * Preloads all permutations for a specific shader + * + * @param string $shaderName The base name of the shader + * @param callable|null $callback Optional callback called after each permutation is loaded + */ + public function loadAllPermutations(string $shaderName, ?callable $callback = null): void + { + if (!isset($this->permutationDefinitions[$shaderName])) { + return; // no permutations defined + } + + foreach ($this->permutationDefinitions[$shaderName] as $permutationName => $defines) { + $shader = $this->getPermutation($shaderName, $permutationName); + if ($callback) $callback($shaderName, $permutationName, $shader); + } + } + + /** + * Preloads all defined permutations for all shaders + * + * @param callable|null $callback Optional callback called after each permutation is loaded + */ + public function loadAllPermutationsForAllShaders(?callable $callback = null): void + { + foreach ($this->permutationDefinitions as $shaderName => $permutations) { + $this->loadAllPermutations($shaderName, $callback); + } + } } diff --git a/src/Graphics/ShaderProgram.php b/src/Graphics/ShaderProgram.php index cd0e6bf..914818f 100644 --- a/src/Graphics/ShaderProgram.php +++ b/src/Graphics/ShaderProgram.php @@ -8,6 +8,7 @@ use GL\Math\Vec4; use VISU\Graphics\Exception\ShaderProgramException; use VISU\Graphics\Exception\ShaderProgramLinkingException; +use VISU\OS\Logger; /** * This class is a wrapper for OpenGL shader programs. @@ -85,7 +86,9 @@ public function __construct( */ public function __destruct() { - glDeleteProgram($this->id); + if ($this->id > 0) { + glDeleteProgram($this->id); + } } /** @@ -188,9 +191,10 @@ public function getInfoLog() : string /** * Compiles all stages (if requrired) and links the program. * + * @param string|null $name Optional name for logging purposes * @return void */ - public function link() : void + public function link(?string $name = null) : void { $this->compileShaderStage($this->vertexShader); $this->compileShaderStage($this->fragmentShader); @@ -209,6 +213,10 @@ public function link() : void } $this->isLinked = true; + + if (!is_null($name)) { + Logger::info(sprintf("Shader program '%s', linked with ID %d", $name, $this->id)); + } } /** @@ -1339,6 +1347,26 @@ public function setUniformVec4Array(string $name, \GL\Buffer\FloatBuffer|array $ glUniform4fv($this->getUniformLocation($name), $values); } + /** + * Bind the given set of textures and sets the uniforms automatically, sampler ID is + * determined by the order of the given array. + * + * @param array $textures The textures to bind and set as uniforms + * @return void + */ + public function bindTextures(array $textures) : void + { + $this->use(); + + $textureUnit = 0; + foreach($textures as $uniformName => $texture) + { + $texture->bind(GL_TEXTURE0 + $textureUnit); + $this->setUniform1i($uniformName, $textureUnit); + $textureUnit++; + } + } + /** * Sets an array of uniforms using their key as the location and guessed type of the value * diff --git a/src/Graphics/Texture.php b/src/Graphics/Texture.php index 2ebd10a..b2f7d4f 100644 --- a/src/Graphics/Texture.php +++ b/src/Graphics/Texture.php @@ -60,8 +60,13 @@ public function __destruct() { glDeleteTextures(1, $this->id); - if ($this->gl->currentTexture === $this->id) { - $this->gl->currentTexture = 0; + // clear this texture from all tracked bindings + foreach ($this->gl->currentTextures as $unit => $targets) { + foreach ($targets as $target => $boundId) { + if ($boundId === $this->id) { + $this->gl->currentTextures[$unit][$target] = 0; + } + } } } @@ -96,15 +101,7 @@ public function size(): Vec2 */ public function bind(int $unit = GL_TEXTURE0): void { - if ($this->gl->currentTextureUnit !== $unit) { // TODO: changing buffer, will reset texture unit.. - glActiveTexture($unit); - $this->gl->currentTextureUnit = $unit; - } - - if ($this->gl->currentTexture !== $this->id) { - glBindTexture($this->target, $this->id); - $this->gl->currentTexture = $this->id; - } + $this->gl->bindTexture($unit, $this->target, $this->id); } /** diff --git a/src/System/VISULowPoly/LPRenderingSystem.php b/src/System/VISULowPoly/LPRenderingSystem.php index 6420940..1581fd4 100644 --- a/src/System/VISULowPoly/LPRenderingSystem.php +++ b/src/System/VISULowPoly/LPRenderingSystem.php @@ -22,17 +22,25 @@ use VISU\Graphics\Rendering\Pass\GBufferPassData; use VISU\Graphics\Rendering\Pass\DeferredLightPassData; use VISU\Graphics\Rendering\Pass\SSAOData; +use VISU\Graphics\Rendering\Pass\IBLPrecomputePass; +use VISU\Graphics\Rendering\Pass\IBLPrecomputeData; +use VISU\Graphics\Rendering\Pass\IBLCache; +use VISU\Graphics\Rendering\Pass\DeferredLightPassPermutation; use VISU\Graphics\Rendering\PipelineContainer; use VISU\Graphics\Rendering\PipelineResources; use VISU\Graphics\Rendering\RenderContext; use VISU\Graphics\Rendering\Renderer\FullscreenDebugDepthRenderer; use VISU\Graphics\Rendering\Renderer\FullscreenTextureRenderer; use VISU\Graphics\Rendering\Renderer\SSAORenderer; +use VISU\Graphics\Rendering\Renderer\CubemapRenderer; use VISU\Graphics\Rendering\RenderPass; use VISU\Graphics\Rendering\RenderPipeline; use VISU\Graphics\Rendering\Resource\RenderTargetResource; +use VISU\Graphics\Rendering\Resource\CubemapResource; use VISU\Graphics\ShaderCollection; use VISU\Graphics\ShaderProgram; +use VISU\Graphics\Cubemap; +use VISU\Graphics\Texture; use VISU\OS\Logger; use VISU\Quickstart\Render\QuickstartDebugMetricsOverlay; @@ -80,13 +88,38 @@ class LPRenderingSystem implements SystemInterface, DevEntityPickerRenderInterfa */ private SSAORenderer $ssaoRenderer; + /** + * Cubemap Renderer for environment mapping + */ + private CubemapRenderer $cubemapRenderer; + + /** + * The cubemap used for environment lighting and reflections + */ + private ?Cubemap $environmentCubemap = null; + + /** + * Cache for IBL precompute results to avoid expensive recomputation + */ + private IBLCache $iblCache; + + /** + * Should the environment cubemap be rendered as skybox? + */ + public bool $renderSkybox = true; + /** * Shader programs */ - private ShaderProgram $objectShader; private ShaderProgram $objectInstancedShader; private ShaderProgram $devPickingShader; - private ShaderProgram $lightingShader; + + /** + * Image based lighting shaders + */ + private ShaderProgram $iblIrradianceShader; + private ShaderProgram $iblPrefilterShader; + private ShaderProgram $iblBrdfShader; /** * onAttach callback handle "LPStaticModel" @@ -131,12 +164,16 @@ public function __construct( $this->fullscreenRenderer = new FullscreenTextureRenderer($this->gl); $this->fullscreenDebugDepthRenderer = new FullscreenDebugDepthRenderer($this->gl); $this->ssaoRenderer = new SSAORenderer($this->gl, $this->shaders); + $this->cubemapRenderer = new CubemapRenderer($this->gl); + $this->iblCache = new IBLCache(); // load the required shaders - $this->objectShader = $this->shaders->get('visu/lowpoly/deferred_single_mesh'); $this->objectInstancedShader = $this->shaders->get('visu/lowpoly/deferred_instanced_mesh'); $this->devPickingShader = $this->shaders->get('visu/lowpoly/devpicking'); - $this->lightingShader = $this->shaders->get('visu/lowpoly/deferred_lightpass'); + + $this->iblIrradianceShader = $this->shaders->get('visu/pbr_v1/bake_irradiance'); + $this->iblPrefilterShader = $this->shaders->get('visu/pbr_v1/bake_prefiltered_env'); + $this->iblBrdfShader = $this->shaders->get('visu/pbr_v1/bake_brdf_lut'); } /** @@ -302,6 +339,41 @@ public function setRenderTarget(RenderTargetResource $renderTargetRes) : void $this->currentRenderTargetRes = $renderTargetRes; } + /** + * Sets the environment cubemap for rendering + * + * @param Cubemap|null $cubemap The cubemap to use for environment lighting and reflections + * @return void + */ + public function setEnvironmentCubemap(?Cubemap $cubemap) : void + { + // invalidate the cache when the cubemap changes + if ($this->environmentCubemap !== $cubemap) { + $this->iblCache->invalidate(); + } + $this->environmentCubemap = $cubemap; + } + + /** + * Forces the IBL cache to be recomputed on the next frame + * + * @return void + */ + public function invalidateIBLCache() : void + { + $this->iblCache->invalidate(); + } + + /** + * Gets the current environment cubemap + * + * @return Cubemap|null The current environment cubemap + */ + public function getEnvironmentCubemap() : ?Cubemap + { + return $this->environmentCubemap; + } + /** * Handles rendering of the scene, here you can attach additional render passes, * modify the render pipeline or customize rendering related data. @@ -444,11 +516,11 @@ function(PipelineContainer $data, PipelineResources $resources) use($entities, & return; } elseif ($this->debugMode === self::DEBUG_MODE_METALLIC) { - $this->fullscreenRenderer->attachPass($context->pipeline, $this->currentRenderTargetRes, $gbuffer->metallicTexture); + $this->fullscreenRenderer->attachPass($context->pipeline, $this->currentRenderTargetRes, $gbuffer->metallicTexture, true); return; } elseif ($this->debugMode === self::DEBUG_MODE_ROUGHNESS) { - $this->fullscreenRenderer->attachPass($context->pipeline, $this->currentRenderTargetRes, $gbuffer->roughnessTexture); + $this->fullscreenRenderer->attachPass($context->pipeline, $this->currentRenderTargetRes, $gbuffer->roughnessTexture, true); return; } elseif ($this->debugMode === self::DEBUG_MODE_EMISSIVE) { @@ -465,14 +537,44 @@ function(PipelineContainer $data, PipelineResources $resources) use($entities, & return; } + // import cubemap if available + $cubemapRes = null; + if ($this->environmentCubemap) { + $cubemapRes = $context->pipeline->importCubemap('environment_cubemap', $this->environmentCubemap); + } + + if ($this->environmentCubemap !== null) { + $context->pipeline->addPass(new IBLPrecomputePass( + $cubemapRes, + $this->iblIrradianceShader, + $this->iblPrefilterShader, + $this->iblBrdfShader, + $this->iblCache, + 32, + 256, + 128, + )); + } + // add a light pass $context->pipeline->addPass(new DeferredLightPass( - $this->lightingShader, - $entities->getSingleton(DirectionalLightComponent::class) + $this->shaders, + $entities->getSingleton(DirectionalLightComponent::class), + $cubemapRes, )); // read the light pass data $lightpass = $context->data->get(DeferredLightPassData::class); + + // add a skybox cubemap pass + if ($this->renderSkybox && $cubemapRes) { + $this->cubemapRenderer->attachSkyboxPass( + $context->pipeline, + $lightpass->renderTarget, + $cubemapRes, + $gbuffer->depthTexture + ); + } // copy over to the main render target $this->fullscreenRenderer->attachPass($context->pipeline, $this->currentRenderTargetRes, $lightpass->output); diff --git a/tests/Graphics/ShaderPermutationTest.php b/tests/Graphics/ShaderPermutationTest.php new file mode 100644 index 0000000..74ed69b --- /dev/null +++ b/tests/Graphics/ShaderPermutationTest.php @@ -0,0 +1,113 @@ +window = $this->createWindow(); + $this->shaderCollection = new ShaderCollection(new GLState(), PATH_TEST_RES_SHADER); + } + + public function testDefinePermutations() + { + $this->shaderCollection->definePermutations('test', [ + 'basic' => ['DEFINE_A' => 1, 'DEFINE_B' => 0], + 'advanced' => ['DEFINE_A' => 1, 'DEFINE_B' => 1, 'DEFINE_C' => 'test'], + ]); + + $this->assertTrue($this->shaderCollection->hasPermutations('test')); + $this->assertFalse($this->shaderCollection->hasPermutations('nonexistent')); + + $permutations = $this->shaderCollection->getAvailablePermutations('test'); + $this->assertCount(2, $permutations); + $this->assertContains('basic', $permutations); + $this->assertContains('advanced', $permutations); + } + + public function testGetAvailablePermutations() + { + $this->shaderCollection->definePermutations('lighting', [ + 'mobile' => ['MOBILE' => 1], + 'desktop' => ['MOBILE' => 0], + 'vr' => ['VR' => 1], + ]); + + $permutations = $this->shaderCollection->getAvailablePermutations('lighting'); + $expected = ['mobile', 'desktop', 'vr']; + + $this->assertEquals($expected, $permutations); + $this->assertEmpty($this->shaderCollection->getAvailablePermutations('nonexistent')); + } + + public function testHasPermutations() + { + $this->assertFalse($this->shaderCollection->hasPermutations('test')); + + $this->shaderCollection->definePermutations('test', [ + 'variant1' => ['TEST' => 1], + ]); + + $this->assertTrue($this->shaderCollection->hasPermutations('test')); + + // empty permutations should return false + $this->shaderCollection->definePermutations('empty', []); + $this->assertFalse($this->shaderCollection->hasPermutations('empty')); + } + + public function testGetPermutationWithNonexistentShader() + { + $this->shaderCollection->definePermutations('nonexistent', [ + 'basic' => ['TEST' => 1], + ]); + + $this->expectException(ShaderException::class); + $this->expectExceptionMessage("Base shader 'nonexistent' does not exist and is not registered"); + + $this->shaderCollection->getPermutation('nonexistent', 'basic'); + } + + public function testGetPermutationWithNonexistentPermutation() + { + $this->shaderCollection->definePermutations('test', [ + 'basic' => ['TEST' => 1], + ]); + + $this->expectException(ShaderException::class); + $this->expectExceptionMessage("Permutation 'advanced' not defined for shader 'test'"); + + $this->shaderCollection->getPermutation('test', 'advanced'); + } + + public function testGetPermutation() + { + // register a basic shader first + $this->shaderCollection->registerFromFiles('triangle', [ + \VISU\Graphics\ShaderStage::VERTEX => PATH_TEST_RES_SHADER . '/triangle.vert.glsl', + \VISU\Graphics\ShaderStage::FRAGMENT => PATH_TEST_RES_SHADER . '/triangle.frag.glsl', + ]); + + $this->shaderCollection->definePermutations('triangle', [ + 'a' => ['TEST' => 1], + 'b' => ['TEST' => 2], + ]); + + // get a permutation to cache it + $shader1 = $this->shaderCollection->getPermutation('triangle', 'a'); + $shader2 = $this->shaderCollection->getPermutation('triangle', 'b'); + + $this->assertNotSame($shader1, $shader2); + } +} \ No newline at end of file diff --git a/tests/resources/shaders/triangle.frag.glsl b/tests/resources/shaders/triangle.frag.glsl index 10f53c8..8fc465e 100644 --- a/tests/resources/shaders/triangle.frag.glsl +++ b/tests/resources/shaders/triangle.frag.glsl @@ -1,6 +1,8 @@ #version 330 core + out vec4 fragment_color; in vec4 pcolor; + void main() { fragment_color = pcolor; diff --git a/tests/resources/shaders/triangle.vert.glsl b/tests/resources/shaders/triangle.vert.glsl index 6d991b2..05bd525 100644 --- a/tests/resources/shaders/triangle.vert.glsl +++ b/tests/resources/shaders/triangle.vert.glsl @@ -1,6 +1,10 @@ +#version 330 core + layout (location = 0) in vec3 position; layout (location = 1) in vec3 color; + out vec4 pcolor; + void main() { pcolor = vec4(color, 1.0f); From 986717914caa607b5fc4f100dc1738a73883f1c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Do=CC=88ring?= <956212+mario-deluna@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:33:18 +0200 Subject: [PATCH 5/9] PBR Demo --- examples/rendering/cubemap_skybox_demo.php | 94 ++++---- examples/rendering/pbr_materials.php | 228 ++++++++++++++++++ .../include/visu/functions/tone_mapping.glsl | 45 +++- .../visu/lowpoly/deferred_lightpass.frag.glsl | 28 +-- resources/shader/visu/skybox.frag.glsl | 22 ++ resources/shader/visu/skybox.vert.glsl | 18 ++ src/Geo/Primitive/Cone.php | 112 +++++++++ src/Geo/Primitive/Cube.php | 74 ++++++ src/Geo/Primitive/Cylinder.php | 102 ++++++++ src/Geo/Primitive/Plane.php | 78 ++++++ src/Geo/Primitive/PrimitiveBuilder.php | 61 +++++ src/Geo/Primitive/PrimitiveException.php | 9 + src/Geo/Primitive/Sphere.php | 135 +++++++++++ src/Geo/Primitive/VertexAttribute.php | 36 +++ src/Geo/Primitive/VertexLayout.php | 106 ++++++++ .../Rendering/Renderer/CubemapRenderer.php | 72 +----- src/System/VISULowPoly/LPRenderingSystem.php | 2 +- tests/Geo/Primitive/PrimitiveTest.php | 126 ++++++++++ tests/Geo/Primitive/VertexLayoutTest.php | 60 +++++ 19 files changed, 1271 insertions(+), 137 deletions(-) create mode 100644 examples/rendering/pbr_materials.php create mode 100644 resources/shader/visu/skybox.frag.glsl create mode 100644 resources/shader/visu/skybox.vert.glsl create mode 100644 src/Geo/Primitive/Cone.php create mode 100644 src/Geo/Primitive/Cube.php create mode 100644 src/Geo/Primitive/Cylinder.php create mode 100644 src/Geo/Primitive/Plane.php create mode 100644 src/Geo/Primitive/PrimitiveBuilder.php create mode 100644 src/Geo/Primitive/PrimitiveException.php create mode 100644 src/Geo/Primitive/Sphere.php create mode 100644 src/Geo/Primitive/VertexAttribute.php create mode 100644 src/Geo/Primitive/VertexLayout.php create mode 100644 tests/Geo/Primitive/PrimitiveTest.php create mode 100644 tests/Geo/Primitive/VertexLayoutTest.php diff --git a/examples/rendering/cubemap_skybox_demo.php b/examples/rendering/cubemap_skybox_demo.php index 22f77fe..7b984ea 100644 --- a/examples/rendering/cubemap_skybox_demo.php +++ b/examples/rendering/cubemap_skybox_demo.php @@ -2,43 +2,43 @@ /** * Cubemap/Skybox Rendering Example - * - * This example demonstrates how to render a cubemap as a skybox using + * + * This example demonstrates how to render a cubemap as a skybox using * the CubemapRenderer and CubemapPass classes. - * + * * The example loads a cubemap from an HDRI file, or falls back to * a test cubemap with solid colors if the HDRI file is not found. - * + * * To use with a real HDRI file, place your .hdr file at: * examples/resources/assets/environment.hdr - * + * * Free HDRI files can be downloaded from: * - https://polyhaven.com/hdris * - https://hdrihaven.com/ */ +use GL\Math\Vec3; +use VISU\Graphics\Cubemap; +use VISU\Graphics\HDRIToCubemap; +use VISU\Graphics\Rendering\Pass\CameraData; +use VISU\Graphics\Rendering\Renderer\CubemapRenderer; +use VISU\Graphics\Rendering\RenderContext; +use VISU\Graphics\Rendering\Resource\RenderTargetResource; use VISU\Quickstart; use VISU\Quickstart\QuickstartApp; use VISU\Quickstart\QuickstartOptions; -use VISU\Graphics\Rendering\Renderer\CubemapRenderer; -use VISU\Graphics\Rendering\Resource\CubemapResource; -use VISU\Graphics\Rendering\Resource\RenderTargetResource; -use VISU\Graphics\Rendering\RenderContext; -use VISU\Graphics\Rendering\Pass\CameraData; -use VISU\Graphics\RenderTarget; -use VISU\Graphics\Cubemap; -use VISU\Graphics\HDRIToCubemap; -use VISU\Graphics\TextureOptions; use VISU\System\VISUCameraSystem; -use GL\Buffer\UByteBuffer; -use GL\Math\{GLM, Vec3, Mat4}; -if (!defined('DS')) { define('DS', DIRECTORY_SEPARATOR); } +// HDRI conversion can take a while, so lift the execution time limit set_time_limit(0); -require __DIR__ . '/../bootstrap.php'; +$container = require __DIR__ . '/../bootstrap.php'; -class CubemapDemoApp extends QuickstartApp { +/** + * Custom QuickstartApp that renders an HDRI-derived cubemap as a skybox + */ +class CubemapDemoApp extends QuickstartApp +{ public Cubemap $cubemap; public CubemapRenderer $cubemapRenderer; public VISUCameraSystem $cameraSystem; @@ -47,66 +47,66 @@ public function setupDrawAfter(RenderContext $context, RenderTargetResource $ren { // get camera data from the context (set by camera system during render) $cameraData = $context->data->get(CameraData::class); - + // import cubemap into the render pipeline $cubemapRes = $context->pipeline->importCubemap('skybox_cubemap', $this->cubemap); - + // add the skybox pass (pass reads camera data internally) $this->cubemapRenderer->attachSkyboxPass( $context->pipeline, - $renderTarget, + $renderTarget, $cubemapRes, ); } } -$quickstart = new Quickstart(function(QuickstartOptions $app) +/** + * Main Entry Point + * + * ---------------------------------------------------------------------------- + */ +$quickstart = new Quickstart(function(QuickstartOptions $app) use($container) { + // Initalize the application + // -------------------------------------------------------------------- + $app->container = $container; + $app->windowTitle = 'VISU Cubemap / Skybox Demo'; $app->appClass = CubemapDemoApp::class; - - $app->ready = function(QuickstartApp $app) { - /** @var CubemapDemoApp $app */ - + + $app->ready = function(CubemapDemoApp $app) { // define path to HDRI file - change this to point to your HDRI file $hdriPath = '/Users/mariodoring/Downloads/cedar_bridge_sunset_1_4k.hdr'; - + // create HDRI to cubemap converter $converter = new HDRIToCubemap($app->gl); - - // convert HDRI to cubemap (512x512 faces) + + // convert HDRI to cubemap (1024x1024 faces) $app->cubemap = $converter->convert($hdriPath, 1024); - + // create the cubemap renderer $app->cubemapRenderer = new CubemapRenderer($app->gl); - + // create camera system for 3D navigation $app->cameraSystem = new VISUCameraSystem($app->input, $app->dispatcher); - + // register the camera system $app->bindSystems([$app->cameraSystem]); }; - - $app->initializeScene = function(QuickstartApp $app) { - /** @var CubemapDemoApp $app */ - + + $app->initializeScene = function(CubemapDemoApp $app) { // spawn a flying camera at origin $app->cameraSystem->spawnDefaultFlyingCamera($app->entities, new Vec3(0.0, 0.0, 0.0)); }; - - $app->update = function(QuickstartApp $app) { - /** @var CubemapDemoApp $app */ - + + $app->update = function(CubemapDemoApp $app) { // update the camera system $app->updateSystem($app->cameraSystem); }; - - $app->render = function(QuickstartApp $app, RenderContext $context, RenderTargetResource $target) { - /** @var CubemapDemoApp $app */ - + + $app->render = function(CubemapDemoApp $app, RenderContext $context, RenderTargetResource $target) { // render the camera system (this sets up CameraData in the context) $app->renderSystem($app->cameraSystem, $context); }; - }); -$quickstart->run(); \ No newline at end of file +$quickstart->run(); diff --git a/examples/rendering/pbr_materials.php b/examples/rendering/pbr_materials.php new file mode 100644 index 0000000..2f4184d --- /dev/null +++ b/examples/rendering/pbr_materials.php @@ -0,0 +1,228 @@ + 1.0 right) + * - y-axis: metallic (0.0 bottom -> 1.0 top) + * + * The grid is lit by image based lighting (IBL) derived from an HDRI + * environment map, which is also rendered as an optional skybox. If no HDRI + * is found the grid still renders, just without environment lighting. + * + * Free HDRI files can be downloaded from: + * - https://polyhaven.com/hdris + * - https://hdrihaven.com/ + */ + +use GL\Buffer\FloatBuffer; +use GL\Math\Vec3; +use VISU\Component\VISULowPoly\LPDynamicModel; +use VISU\Geo\Primitive\Sphere; +use VISU\Geo\Primitive\VertexLayout; +use VISU\Geo\Transform; +use VISU\Graphics\Cubemap; +use VISU\Graphics\HDRIToCubemap; +use VISU\Graphics\Rendering\RenderContext; +use VISU\Graphics\Rendering\Resource\RenderTargetResource; +use VISU\Quickstart; +use VISU\Quickstart\QuickstartApp; +use VISU\Quickstart\QuickstartOptions; +use VISU\System\VISUCameraSystem; +use VISU\System\VISULowPoly\LPMaterial; +use VISU\System\VISULowPoly\LPModel; +use VISU\System\VISULowPoly\LPModelCollection; +use VISU\System\VISULowPoly\LPObjLoader; +use VISU\System\VISULowPoly\LPRenderingSystem; +use VISU\System\VISULowPoly\LPVertexBuffer; + +$container = require __DIR__ . '/../bootstrap.php'; + +// Demo State +// -------------------------------------------------------------------- +class PBRMaterialsDemoState +{ + public VISUCameraSystem $cameraSystem; + public LPRenderingSystem $renderingSystem; + public LPModelCollection $models; + public ?Cubemap $environmentCubemap = null; + + // grid configuration + public const GRID_ROUGHNESS_STEPS = 10; // x-axis: roughness 0.0 to 1.0 + public const GRID_METALLIC_STEPS = 5; // y-axis: metallic 0.0 to 1.0 + public const SPHERE_SPACING = 1.2; + + // sphere entities + /** @var array */ + public array $sphereEntities = []; +} + +$state = new PBRMaterialsDemoState; + +/** + * Main Entry Point + * + * ---------------------------------------------------------------------------- + */ +$quickstart = new Quickstart(function(QuickstartOptions $app) use(&$state, $container) +{ + // Initialize the application + // -------------------------------------------------------------------- + $app->container = $container; + $app->ready = function(QuickstartApp $app) use(&$state) + { + // create a model collection and the rendering system + $state->models = new LPModelCollection(); + $state->renderingSystem = new LPRenderingSystem($app->gl, $app->shaders, $state->models); + + // create a vertex buffer for our procedural spheres + $vb = new LPVertexBuffer($app->gl); + $loader = new LPObjLoader($app->gl); + + // generate sphere mesh data once (position + normal format, which is what + // LPObjLoader::importMesh consumes) + $sphereMeshData = new FloatBuffer(); + Sphere::make( + VertexLayout::with([VertexLayout::position, VertexLayout::normal]), + $sphereMeshData, + 0.5, 32, 16 + ); + + // create sphere models with varying roughness and metallic values + // x-axis: roughness (0.0 to 1.0) + // y-axis: metallic (0.0 = dielectric, 1.0 = metallic) + $baseColor = new Vec3(1.0); + + for ($my = 0; $my < PBRMaterialsDemoState::GRID_METALLIC_STEPS; $my++) { + $metallic = $my / max(1, PBRMaterialsDemoState::GRID_METALLIC_STEPS - 1); + + for ($rx = 0; $rx < PBRMaterialsDemoState::GRID_ROUGHNESS_STEPS; $rx++) { + $roughness = $rx / max(1, PBRMaterialsDemoState::GRID_ROUGHNESS_STEPS - 1); + + $roughness = min(max($roughness, 0.04), 1.0); // avoid 0.0 roughness for better visibility + $metallic = min(max($metallic, 0.0), 1.0); + + // create a unique material for this sphere + $materialName = sprintf("pbr_r%.2f_m%.2f", $roughness, $metallic); + $material = new LPMaterial( + $materialName, + $baseColor->copy(), + $roughness, + $metallic + ); + + // import mesh with this material + $mesh = $loader->importMesh($sphereMeshData, $material, $vb); + + // create model and add to collection + $model = new LPModel("sphere_{$rx}_{$my}", [$mesh]); + $model->recalculateAABB(); + $state->models->add($model); + } + } + + // upload all vertex data to GPU + $vb->upload(); + + // create environment cubemap from HDRI (if available) + $hdriPath = VISU_PATH_RESOURCES . '/assets/hdri/cowboy_town_saloon_2k.hdr'; + $cubemapResolution = 1024; + + if (file_exists($hdriPath)) { + // convert HDRI to cubemap for image based lighting + $converter = new HDRIToCubemap($app->gl); + $state->environmentCubemap = $converter->convert($hdriPath, $cubemapResolution); + echo "Loaded HDRI environment: $hdriPath\n"; + } else { + echo "No HDRI file found at: $hdriPath\n"; + echo "Download free HDRI files from https://polyhaven.com/hdris and place one there.\n"; + } + + // feed the environment cubemap into the rendering system for IBL + skybox + if ($state->environmentCubemap) { + $state->renderingSystem->setEnvironmentCubemap($state->environmentCubemap); + $state->renderingSystem->renderSkybox = true; + } + + // to render 3D we need a camera + $state->cameraSystem = new VISUCameraSystem($app->input, $app->dispatcher); + + // register the systems + $app->bindSystems([ + $state->renderingSystem, + $state->cameraSystem + ]); + }; + + // Initialize the scene + // -------------------------------------------------------------------- + $app->initializeScene = function(QuickstartApp $app) use(&$state) + { + // position camera to see the entire grid + $gridWidth = PBRMaterialsDemoState::GRID_ROUGHNESS_STEPS * PBRMaterialsDemoState::SPHERE_SPACING; + $gridHeight = PBRMaterialsDemoState::GRID_METALLIC_STEPS * PBRMaterialsDemoState::SPHERE_SPACING; + $cameraDistance = max($gridWidth, $gridHeight) * 1.2; + + $state->cameraSystem->spawnDefaultFlyingCamera($app->entities, new Vec3( + $gridWidth * 0.5 - PBRMaterialsDemoState::SPHERE_SPACING * 0.5, + $gridHeight * 0.5 - PBRMaterialsDemoState::SPHERE_SPACING * 0.5, + $cameraDistance + )); + + // spawn sphere grid + // x-axis: roughness (left = 0.0, right = 1.0) + // y-axis: metallic (bottom = 0.0, top = 1.0) + for ($my = 0; $my < PBRMaterialsDemoState::GRID_METALLIC_STEPS; $my++) { + for ($rx = 0; $rx < PBRMaterialsDemoState::GRID_ROUGHNESS_STEPS; $rx++) { + $entity = $app->entities->create(); + + // attach the corresponding sphere model + $modelName = "sphere_{$rx}_{$my}"; + $app->entities->attach($entity, new LPDynamicModel($modelName)); + + // position in grid + $transform = $app->entities->attach($entity, new Transform()); + $transform->position = new Vec3( + $rx * PBRMaterialsDemoState::SPHERE_SPACING, + $my * PBRMaterialsDemoState::SPHERE_SPACING, + 0.0 + ); + + $state->sphereEntities[] = $entity; + } + } + + echo "Spawned " . count($state->sphereEntities) . " spheres in a grid\n"; + echo "X-axis: Roughness (0.0 left -> 1.0 right)\n"; + echo "Y-axis: Metallic (0.0 bottom -> 1.0 top)\n"; + }; + + // Update the scene + // -------------------------------------------------------------------- + $app->update = function(QuickstartApp $app) use(&$state) + { + $app->updateSystem($state->cameraSystem); + }; + + // Render the scene + // -------------------------------------------------------------------- + $app->render = function(QuickstartApp $app, RenderContext $context, RenderTargetResource $target) use(&$state) + { + // tell the rendering system which render target we are using + $state->renderingSystem->setRenderTarget($target); + + $app->renderSystem($state->cameraSystem, $context); + $app->renderSystem($state->renderingSystem, $context); + }; +}); + +echo "PBR Materials\n"; +echo "=============\n"; +echo "Controls:\n"; +echo " WASD/Mouse - Fly around\n"; +echo "\n"; + +$quickstart->run(); diff --git a/resources/shader/include/visu/functions/tone_mapping.glsl b/resources/shader/include/visu/functions/tone_mapping.glsl index 4253301..b031366 100644 --- a/resources/shader/include/visu/functions/tone_mapping.glsl +++ b/resources/shader/include/visu/functions/tone_mapping.glsl @@ -1,8 +1,28 @@ +#ifndef TONE_MAPPING_GLSL +#define TONE_MAPPING_GLSL /** * Common tone mapping functions * ---------------------------------------------------------------------------- */ +// define TONEMAP_METHOD in as a shader option to change. +// available methods: +// TONEMAP_NEUTRAL (1) - Khronos PBR neutral tone mapper (default) +// TONEMAP_ACES (2) - ACES filmic curve +// TONEMAP_REINHARD (3) - basic Reinhard +// TONEMAP_REINHARD2 (4) - Reinhard with white point +#define TONEMAP_NONE 0 +#define TONEMAP_NEUTRAL 1 +#define TONEMAP_ACES 2 +#define TONEMAP_REINHARD 3 +#define TONEMAP_REINHARD2 4 + +// default to the Khronos PBR neutral mapper when nothing is specified, this is +// the single source of truth for the whole scene (lit geometry and skybox). +#ifndef TONEMAP_METHOD +#define TONEMAP_METHOD TONEMAP_NEUTRAL +#endif + /** * ACES Filmic Tone Mapping * Reference: https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/ @@ -56,4 +76,27 @@ vec3 tonemap_neutral(vec3 color) float g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0); return mix(color, vec3(newPeak), g); -} \ No newline at end of file +} + +/** + * Applies the tone mapping curve selected via TONEMAP_METHOD. + * + * Call this instead of the individual curves so every shader in the scene + * resolves to the exact same mapping and can be switched from one place. + */ +vec3 apply_tonemap(vec3 color) +{ +#if TONEMAP_METHOD == TONEMAP_NEUTRAL + return tonemap_neutral(color); +#elif TONEMAP_METHOD == TONEMAP_ACES + return tonemap_ACESFilm(color); +#elif TONEMAP_METHOD == TONEMAP_REINHARD + return tonemap_reinhard(color); +#elif TONEMAP_METHOD == TONEMAP_REINHARD2 + return tonemap_reinhard2(color); +#else + return color; +#endif +} + +#endif \ No newline at end of file diff --git a/resources/shader/visu/lowpoly/deferred_lightpass.frag.glsl b/resources/shader/visu/lowpoly/deferred_lightpass.frag.glsl index 9518204..e81793b 100644 --- a/resources/shader/visu/lowpoly/deferred_lightpass.frag.glsl +++ b/resources/shader/visu/lowpoly/deferred_lightpass.frag.glsl @@ -3,21 +3,9 @@ // Tone mapping // ---------------------------------------------------------------------------- // -// define TONEMAP_METHOD in as a shader option to change. -// available methods: -// TONEMAP_NEUTRAL (1) - Khronos PBR neutral tone mapper (default) -// TONEMAP_ACES (2) - ACES filmic curve -// TONEMAP_REINHARD (3) - basic Reinhard -// TONEMAP_REINHARD2 (4) - Reinhard with white point -#define TONEMAP_NONE 0 -#define TONEMAP_NEUTRAL 1 -#define TONEMAP_ACES 2 -#define TONEMAP_REINHARD 3 -#define TONEMAP_REINHARD2 4 - -#ifndef TONEMAP_METHOD -#define TONEMAP_METHOD TONEMAP_NEUTRAL -#endif +// the tone mapping method and curves live in visu/functions/tone_mapping.glsl +// (included below). select a method by defining TONEMAP_METHOD as a shader +// option; it defaults to the Khronos PBR neutral mapper. // Gamma correction @@ -124,15 +112,7 @@ void main() vec3 color = Lo + ambient + s.emissive; // tone mapping -#if TONEMAP_METHOD == TONEMAP_NEUTRAL - color = tonemap_neutral(color); -#elif TONEMAP_METHOD == TONEMAP_ACES - color = tonemap_ACESFilm(color); -#elif TONEMAP_METHOD == TONEMAP_REINHARD - color = tonemap_reinhard(color); -#elif TONEMAP_METHOD == TONEMAP_REINHARD2 - color = tonemap_reinhard2(color); -#endif + color = apply_tonemap(color); #if SHOULD_CORRECT_GAMMA color = gamma_correct(color); diff --git a/resources/shader/visu/skybox.frag.glsl b/resources/shader/visu/skybox.frag.glsl new file mode 100644 index 0000000..e8a9ea9 --- /dev/null +++ b/resources/shader/visu/skybox.frag.glsl @@ -0,0 +1,22 @@ +#version 330 core + +out vec4 frag_color; + +in vec3 tex_coords; + +uniform samplerCube u_skybox; + +#include "visu/functions/tone_mapping.glsl" +#include "visu/functions/gamma_corr.glsl" + +void main() +{ + // the environment cubemap holds linear HDR radiance, tone map and gamma + // correct it with the exact same curve the deferred light pass uses so the + // skybox and the lit geometry (and its reflections) read consistently. + vec3 color = texture(u_skybox, tex_coords).rgb; + color = apply_tonemap(color); + color = gamma_correct(color); + + frag_color = vec4(color, 1.0); +} diff --git a/resources/shader/visu/skybox.vert.glsl b/resources/shader/visu/skybox.vert.glsl new file mode 100644 index 0000000..e9e16b8 --- /dev/null +++ b/resources/shader/visu/skybox.vert.glsl @@ -0,0 +1,18 @@ +#version 330 core + +layout (location = 0) in vec3 a_pos; + +out vec3 tex_coords; + +uniform mat4 u_projection; +uniform mat4 u_view; + +void main() +{ + tex_coords = a_pos; + // drop translation, we are in the skybox + mat4 view = mat4(mat3(u_view)); + vec4 pos = u_projection * view * vec4(a_pos, 1.0); + // force the skybox to the far plane so it only fills empty depth + gl_Position = pos.xyww; +} diff --git a/src/Geo/Primitive/Cone.php b/src/Geo/Primitive/Cone.php new file mode 100644 index 0000000..46b5a3c --- /dev/null +++ b/src/Geo/Primitive/Cone.php @@ -0,0 +1,112 @@ += 3) + * @param bool $cap Whether to generate the bottom cap + */ + public static function make( + VertexLayout $layout, + FloatBuffer $target, + float $radius = 1.0, + float $height = 1.0, + int $segments = 32, + bool $cap = true + ) : void + { + $builder = new PrimitiveBuilder($layout, $target); + + $segments = max(3, $segments); + $hy = $height * 0.5; + $apex = new Vec3(0.0, $hy, 0.0); + + // reserve space up front so the target does not reallocate while filling: + // 3 side verts per segment, plus 3 per segment for the cap when requested + $vertexCount = $segments * 3 + ($cap ? $segments * 3 : 0); + $target->reserve($target->size() + $vertexCount * $layout->stride()); + + // trig tables for the segment angle. the boundary table has one extra + // entry to close the seam, the mid table samples each segment center. + $cosTheta = []; + $sinTheta = []; + for ($seg = 0; $seg <= $segments; $seg++) { + $theta = 2.0 * M_PI * $seg / $segments; + $cosTheta[$seg] = cos($theta); + $sinTheta[$seg] = sin($theta); + } + $cosMid = []; + $sinMid = []; + for ($seg = 0; $seg < $segments; $seg++) { + $theta = 2.0 * M_PI * ($seg + 0.5) / $segments; + $cosMid[$seg] = cos($theta); + $sinMid[$seg] = sin($theta); + } + + $bottomNormal = new Vec3(0.0, -1.0, 0.0); + $bottomCenter = new Vec3(0.0, -$hy, 0.0); + + for ($seg = 0; $seg < $segments; $seg++) { + $c0 = $cosTheta[$seg]; + $s0 = $sinTheta[$seg]; + $c1 = $cosTheta[$seg + 1]; + $s1 = $sinTheta[$seg + 1]; + + $u0 = $seg / $segments; + $u1 = ($seg + 1) / $segments; + $uMid = ($u0 + $u1) * 0.5; + + $b0 = new Vec3($radius * $c0, -$hy, $radius * $s0); + $b1 = new Vec3($radius * $c1, -$hy, $radius * $s1); + + // the side normal tilts with the slope of the cone + $n0 = self::sideNormal($c0, $s0, $radius, $height); + $n1 = self::sideNormal($c1, $s1, $radius, $height); + $nApex = self::sideNormal($cosMid[$seg], $sinMid[$seg], $radius, $height); + + // side, wound counter-clockwise seen from outside + $builder->addVertex($b0, $n0, new Vec2($u0, 0.0)); + $builder->addVertex($apex, $nApex, new Vec2($uMid, 1.0)); + $builder->addVertex($b1, $n1, new Vec2($u1, 0.0)); + + if (!$cap) { + continue; + } + + // bottom cap faces down (-Y) + $builder->addVertex($bottomCenter, $bottomNormal, new Vec2(0.5, 0.5)); + $builder->addVertex($b0, $bottomNormal, new Vec2(0.5 + 0.5 * $c0, 0.5 + 0.5 * $s0)); + $builder->addVertex($b1, $bottomNormal, new Vec2(0.5 + 0.5 * $c1, 0.5 + 0.5 * $s1)); + } + } + + /** + * Computes the outward side normal from the segment angle's cosine and sine, + * tilted by the slope defined by the base radius and the cone height. + */ + private static function sideNormal(float $cosTheta, float $sinTheta, float $radius, float $height) : Vec3 + { + $normal = new Vec3($height * $cosTheta, $radius, $height * $sinTheta); + $normal->normalize(); + + return $normal; + } +} diff --git a/src/Geo/Primitive/Cube.php b/src/Geo/Primitive/Cube.php new file mode 100644 index 0000000..d82e3a3 --- /dev/null +++ b/src/Geo/Primitive/Cube.php @@ -0,0 +1,74 @@ +addVertex($bl, $normal, new Vec2(0.0, 0.0)); + $builder->addVertex($br, $normal, new Vec2(1.0, 0.0)); + $builder->addVertex($tr, $normal, new Vec2(1.0, 1.0)); + + $builder->addVertex($bl, $normal, new Vec2(0.0, 0.0)); + $builder->addVertex($tr, $normal, new Vec2(1.0, 1.0)); + $builder->addVertex($tl, $normal, new Vec2(0.0, 1.0)); + } + } + + /** + * Writes a 1x1x1 cube into the target buffer. + */ + public static function standardCube(VertexLayout $layout, FloatBuffer $target) : void + { + self::make($layout, $target, 1.0, 1.0, 1.0); + } +} diff --git a/src/Geo/Primitive/Cylinder.php b/src/Geo/Primitive/Cylinder.php new file mode 100644 index 0000000..b1024a3 --- /dev/null +++ b/src/Geo/Primitive/Cylinder.php @@ -0,0 +1,102 @@ += 3) + * @param bool $caps Whether to generate the top and bottom caps + */ + public static function make( + VertexLayout $layout, + FloatBuffer $target, + float $radius = 1.0, + float $height = 1.0, + int $segments = 32, + bool $caps = true + ) : void + { + $builder = new PrimitiveBuilder($layout, $target); + + $segments = max(3, $segments); + $hy = $height * 0.5; + + // reserve space up front so the target does not reallocate while filling: + // 6 side verts per segment, plus 3 per cap when caps are requested + $vertexCount = $segments * 6 + ($caps ? $segments * 6 : 0); + $target->reserve($target->size() + $vertexCount * $layout->stride()); + + // trig table for the segment angle, one extra entry closes the seam + $cosTheta = []; + $sinTheta = []; + for ($seg = 0; $seg <= $segments; $seg++) { + $theta = 2.0 * M_PI * $seg / $segments; + $cosTheta[$seg] = cos($theta); + $sinTheta[$seg] = sin($theta); + } + + $topNormal = new Vec3(0.0, 1.0, 0.0); + $topCenter = new Vec3(0.0, $hy, 0.0); + $bottomNormal = new Vec3(0.0, -1.0, 0.0); + $bottomCenter = new Vec3(0.0, -$hy, 0.0); + + for ($seg = 0; $seg < $segments; $seg++) { + $c0 = $cosTheta[$seg]; + $s0 = $sinTheta[$seg]; + $c1 = $cosTheta[$seg + 1]; + $s1 = $sinTheta[$seg + 1]; + + $u0 = $seg / $segments; + $u1 = ($seg + 1) / $segments; + + $n0 = new Vec3($c0, 0.0, $s0); + $n1 = new Vec3($c1, 0.0, $s1); + + $b0 = new Vec3($radius * $c0, -$hy, $radius * $s0); + $b1 = new Vec3($radius * $c1, -$hy, $radius * $s1); + $t0 = new Vec3($radius * $c0, $hy, $radius * $s0); + $t1 = new Vec3($radius * $c1, $hy, $radius * $s1); + + // side, wound counter-clockwise seen from outside + $builder->addVertex($b0, $n0, new Vec2($u0, 0.0)); + $builder->addVertex($t0, $n0, new Vec2($u0, 1.0)); + $builder->addVertex($b1, $n1, new Vec2($u1, 0.0)); + + $builder->addVertex($b1, $n1, new Vec2($u1, 0.0)); + $builder->addVertex($t0, $n0, new Vec2($u0, 1.0)); + $builder->addVertex($t1, $n1, new Vec2($u1, 1.0)); + + if (!$caps) { + continue; + } + + // top cap faces up (+Y) + $builder->addVertex($topCenter, $topNormal, new Vec2(0.5, 0.5)); + $builder->addVertex($t1, $topNormal, new Vec2(0.5 + 0.5 * $c1, 0.5 + 0.5 * $s1)); + $builder->addVertex($t0, $topNormal, new Vec2(0.5 + 0.5 * $c0, 0.5 + 0.5 * $s0)); + + // bottom cap faces down (-Y) + $builder->addVertex($bottomCenter, $bottomNormal, new Vec2(0.5, 0.5)); + $builder->addVertex($b0, $bottomNormal, new Vec2(0.5 + 0.5 * $c0, 0.5 + 0.5 * $s0)); + $builder->addVertex($b1, $bottomNormal, new Vec2(0.5 + 0.5 * $c1, 0.5 + 0.5 * $s1)); + } + } +} diff --git a/src/Geo/Primitive/Plane.php b/src/Geo/Primitive/Plane.php new file mode 100644 index 0000000..76ab8a6 --- /dev/null +++ b/src/Geo/Primitive/Plane.php @@ -0,0 +1,78 @@ += 1) + */ + public static function make( + VertexLayout $layout, + FloatBuffer $target, + float $width = 1.0, + float $depth = 1.0, + int $subdivisions = 1 + ) : void + { + $builder = new PrimitiveBuilder($layout, $target); + + $subdivisions = max(1, $subdivisions); + $normal = new Vec3(0.0, 1.0, 0.0); + + $hw = $width * 0.5; + $hd = $depth * 0.5; + + for ($ix = 0; $ix < $subdivisions; $ix++) { + $fx0 = $ix / $subdivisions; + $fx1 = ($ix + 1) / $subdivisions; + + for ($iz = 0; $iz < $subdivisions; $iz++) { + $fz0 = $iz / $subdivisions; + $fz1 = ($iz + 1) / $subdivisions; + + $x0 = -$hw + $fx0 * $width; + $x1 = -$hw + $fx1 * $width; + $z0 = -$hd + $fz0 * $depth; + $z1 = -$hd + $fz1 * $depth; + + $p00 = new Vec3($x0, 0.0, $z0); + $p01 = new Vec3($x0, 0.0, $z1); + $p10 = new Vec3($x1, 0.0, $z0); + $p11 = new Vec3($x1, 0.0, $z1); + + // counter-clockwise winding when viewed from above (+Y) + $builder->addVertex($p00, $normal, new Vec2($fx0, $fz0)); + $builder->addVertex($p01, $normal, new Vec2($fx0, $fz1)); + $builder->addVertex($p11, $normal, new Vec2($fx1, $fz1)); + + $builder->addVertex($p00, $normal, new Vec2($fx0, $fz0)); + $builder->addVertex($p11, $normal, new Vec2($fx1, $fz1)); + $builder->addVertex($p10, $normal, new Vec2($fx1, $fz0)); + } + } + } + + /** + * Writes a 1x1 plane into the target buffer. + */ + public static function unitPlane(VertexLayout $layout, FloatBuffer $target) : void + { + self::make($layout, $target, 1.0, 1.0, 1); + } +} diff --git a/src/Geo/Primitive/PrimitiveBuilder.php b/src/Geo/Primitive/PrimitiveBuilder.php new file mode 100644 index 0000000..5c10e59 --- /dev/null +++ b/src/Geo/Primitive/PrimitiveBuilder.php @@ -0,0 +1,61 @@ +layout->attributes() as $attribute) { + switch ($attribute) { + case VertexAttribute::position: + $this->target->pushVec3($position); + break; + + case VertexAttribute::normal: + if ($normal === null) { + throw new PrimitiveException('The layout requests a normal but the primitive did not provide one.'); + } + $this->target->pushVec3($normal); + break; + + case VertexAttribute::uv: + if ($uv === null) { + throw new PrimitiveException('The layout requests a UV but the primitive did not provide one.'); + } + $this->target->pushVec2($uv); + break; + } + } + } +} diff --git a/src/Geo/Primitive/PrimitiveException.php b/src/Geo/Primitive/PrimitiveException.php new file mode 100644 index 0000000..c9a422b --- /dev/null +++ b/src/Geo/Primitive/PrimitiveException.php @@ -0,0 +1,9 @@ += 3) + * @param int $rings Number of vertical rings (latitude, >= 2) + */ + public static function make( + VertexLayout $layout, + FloatBuffer $target, + float $radius = 1.0, + int $segments = 32, + int $rings = 16 + ) : void + { + $builder = new PrimitiveBuilder($layout, $target); + + $segments = max(3, $segments); + $rings = max(2, $rings); + + // reserve space up front so the target does not reallocate while filling. + // the two pole rings are fans (3 verts per segment), the interior rings + // are quads (6 verts per segment). + $vertexCount = 2 * $segments * 3 + max(0, $rings - 2) * $segments * 6; + $target->reserve($target->size() + $vertexCount * $layout->stride()); + + // trig tables for the segment angle, one extra entry closes the seam + $cols = $segments + 1; + $cosTheta = []; + $sinTheta = []; + for ($seg = 0; $seg <= $segments; $seg++) { + $theta = 2.0 * M_PI * $seg / $segments; + $cosTheta[$seg] = cos($theta); + $sinTheta[$seg] = sin($theta); + } + + // build the grid of distinct nodes: (rings + 1) rows by (segments + 1) + // columns. each node is computed once and shared by the triangles. + /** @var array $positions */ + $positions = []; + /** @var array $normals */ + $normals = []; + /** @var array $uvs */ + $uvs = []; + + for ($ring = 0; $ring <= $rings; $ring++) { + $phi = M_PI * $ring / $rings; + $cosPhi = cos($phi); + $sinPhi = sin($phi); + $v = $ring / $rings; + + $rowOffset = $ring * $cols; + for ($seg = 0; $seg <= $segments; $seg++) { + // the normal of a sphere is simply its normalized position + $normal = new Vec3($sinPhi * $cosTheta[$seg], $cosPhi, $sinPhi * $sinTheta[$seg]); + + $index = $rowOffset + $seg; + $normals[$index] = $normal; + $positions[$index] = $normal * $radius; + $uvs[$index] = new Vec2($seg / $segments, $v); + } + } + + // assemble the triangle list from the grid nodes. the winding is counter + // clockwise so the faces point outward. + for ($ring = 0; $ring < $rings; $ring++) { + $v0 = $ring / $rings; + $v1 = ($ring + 1) / $rings; + + $rowTop = $ring * $cols; + $rowBottom = ($ring + 1) * $cols; + + $isTopCap = $ring === 0; + $isBottomCap = $ring === $rings - 1; + + for ($seg = 0; $seg < $segments; $seg++) { + $uMid = ($seg + 0.5) / $segments; + + if ($isTopCap) { + // north pole fan: apex + the two corners of the lower ring. + // the apex takes the midpoint u to reduce pole distortion. + $builder->addVertex($positions[$rowTop + $seg], $normals[$rowTop + $seg], new Vec2($uMid, $v0)); + $builder->addVertex($positions[$rowBottom + $seg + 1], $normals[$rowBottom + $seg + 1], $uvs[$rowBottom + $seg + 1]); + $builder->addVertex($positions[$rowBottom + $seg], $normals[$rowBottom + $seg], $uvs[$rowBottom + $seg]); + } elseif ($isBottomCap) { + // south pole fan: the two corners of the upper ring + apex + $builder->addVertex($positions[$rowTop + $seg], $normals[$rowTop + $seg], $uvs[$rowTop + $seg]); + $builder->addVertex($positions[$rowTop + $seg + 1], $normals[$rowTop + $seg + 1], $uvs[$rowTop + $seg + 1]); + $builder->addVertex($positions[$rowBottom + $seg], $normals[$rowBottom + $seg], new Vec2($uMid, $v1)); + } else { + // full quad as two triangles + $builder->addVertex($positions[$rowTop + $seg], $normals[$rowTop + $seg], $uvs[$rowTop + $seg]); + $builder->addVertex($positions[$rowTop + $seg + 1], $normals[$rowTop + $seg + 1], $uvs[$rowTop + $seg + 1]); + $builder->addVertex($positions[$rowBottom + $seg], $normals[$rowBottom + $seg], $uvs[$rowBottom + $seg]); + + $builder->addVertex($positions[$rowTop + $seg + 1], $normals[$rowTop + $seg + 1], $uvs[$rowTop + $seg + 1]); + $builder->addVertex($positions[$rowBottom + $seg + 1], $normals[$rowBottom + $seg + 1], $uvs[$rowBottom + $seg + 1]); + $builder->addVertex($positions[$rowBottom + $seg], $normals[$rowBottom + $seg], $uvs[$rowBottom + $seg]); + } + } + } + } + + /** + * Writes a unit sphere (radius 1.0) into the target buffer. + */ + public static function unitSphere(VertexLayout $layout, FloatBuffer $target) : void + { + self::make($layout, $target, 1.0); + } +} diff --git a/src/Geo/Primitive/VertexAttribute.php b/src/Geo/Primitive/VertexAttribute.php new file mode 100644 index 0000000..d1efb6a --- /dev/null +++ b/src/Geo/Primitive/VertexAttribute.php @@ -0,0 +1,36 @@ + 3, + self::normal => 3, + self::uv => 2, + }; + } +} diff --git a/src/Geo/Primitive/VertexLayout.php b/src/Geo/Primitive/VertexLayout.php new file mode 100644 index 0000000..8e056c6 --- /dev/null +++ b/src/Geo/Primitive/VertexLayout.php @@ -0,0 +1,106 @@ + + */ + private array $attributes; + + /** + * Constructs a layout from the given, already normalized, attributes. + * + * @param array $attributes + */ + private function __construct(array $attributes) + { + $this->attributes = $attributes; + } + + /** + * Builds a layout from the given attributes, removing duplicates while + * preserving the first occurrence's position. + * + * @param array $attributes + */ + public static function with(array $attributes) : self + { + $unique = []; + foreach ($attributes as $attribute) { + if (!in_array($attribute, $unique, true)) { + $unique[] = $attribute; + } + } + + return new self($unique); + } + + /** + * Returns the attributes in write order. + * + * @return array + */ + public function attributes() : array + { + return $this->attributes; + } + + /** + * Returns the stride of a single vertex in floats. + */ + public function stride() : int + { + $stride = 0; + foreach ($this->attributes as $attribute) { + $stride += $attribute->size(); + } + + return $stride; + } + + /** + * Returns whether the given attribute is part of this layout. + */ + public function has(VertexAttribute $attribute) : bool + { + return in_array($attribute, $this->attributes, true); + } + + /** + * Returns the float offset of the given attribute inside a vertex, or -1 + * if the attribute is not part of this layout. + */ + public function offsetOf(VertexAttribute $attribute) : int + { + $offset = 0; + foreach ($this->attributes as $current) { + if ($current === $attribute) { + return $offset; + } + $offset += $current->size(); + } + + return -1; + } +} diff --git a/src/Graphics/Rendering/Renderer/CubemapRenderer.php b/src/Graphics/Rendering/Renderer/CubemapRenderer.php index 34b5de3..f6f95c7 100644 --- a/src/Graphics/Rendering/Renderer/CubemapRenderer.php +++ b/src/Graphics/Rendering/Renderer/CubemapRenderer.php @@ -2,14 +2,13 @@ namespace VISU\Graphics\Rendering\Renderer; -use VISU\Graphics\GLState; use VISU\Graphics\Rendering\Pass\CubemapPass; use VISU\Graphics\Rendering\RenderPipeline; use VISU\Graphics\Rendering\Resource\CubemapResource; use VISU\Graphics\Rendering\Resource\RenderTargetResource; use VISU\Graphics\Rendering\Resource\TextureResource; +use VISU\Graphics\ShaderCollection; use VISU\Graphics\ShaderProgram; -use VISU\Graphics\ShaderStage; class CubemapRenderer { @@ -19,72 +18,17 @@ class CubemapRenderer private ShaderProgram $skyboxShaderProgram; /** - * Constructor - * - * @param GLState $glstate The current GL state. + * Constructor + * + * @param ShaderCollection $shaders The shader collection to load the skybox shader from. */ public function __construct( - GLState $glstate, + ShaderCollection $shaders, ) { - // create the skybox shader program - $this->skyboxShaderProgram = new ShaderProgram($glstate); - - // attach skybox vertex shader - $this->skyboxShaderProgram->attach(new ShaderStage(ShaderStage::VERTEX, <<< 'GLSL' - #version 330 core - - layout (location = 0) in vec3 a_pos; - - out vec3 tex_coords; - - uniform mat4 u_projection; - uniform mat4 u_view; - - void main() - { - tex_coords = a_pos; - // drop translation, we are in the skybox - mat4 view = mat4(mat3(u_view)); - vec4 pos = u_projection * view * vec4(a_pos, 1.0); - gl_Position = pos.xyww; - } - GLSL)); - - // attach skybox fragment shader - $this->skyboxShaderProgram->attach(new ShaderStage(ShaderStage::FRAGMENT, <<< 'GLSL' - #version 330 core - - out vec4 frag_color; - - in vec3 tex_coords; - - uniform samplerCube u_skybox; - - // simple reinhard tonemapping - vec3 reinhard(vec3 color) { - return color / (color + vec3(1.0)); - } - - // gamma correction - vec3 gammaCorrect(vec3 color) { - return pow(color, vec3(1.0/2.2)); - } - - void main() - { - // vec3 hdrColor = textureLod(u_skybox, tex_coords, 0).rgb; - vec3 hdrColor = texture(u_skybox, tex_coords).rgb; - - // apply tonemapping and gamma correction - vec3 mapped = reinhard(hdrColor); - vec3 gamma = gammaCorrect(mapped); - - frag_color = vec4(gamma, 1.0); - } - GLSL)); - - $this->skyboxShaderProgram->link('skybox'); + // load the skybox shader from the collection so it shares the same + // tone mapping include as the rest of the scene (visu/skybox.{vert,frag}.glsl) + $this->skyboxShaderProgram = $shaders->get('visu/skybox'); } /** diff --git a/src/System/VISULowPoly/LPRenderingSystem.php b/src/System/VISULowPoly/LPRenderingSystem.php index 1581fd4..558a5ec 100644 --- a/src/System/VISULowPoly/LPRenderingSystem.php +++ b/src/System/VISULowPoly/LPRenderingSystem.php @@ -164,7 +164,7 @@ public function __construct( $this->fullscreenRenderer = new FullscreenTextureRenderer($this->gl); $this->fullscreenDebugDepthRenderer = new FullscreenDebugDepthRenderer($this->gl); $this->ssaoRenderer = new SSAORenderer($this->gl, $this->shaders); - $this->cubemapRenderer = new CubemapRenderer($this->gl); + $this->cubemapRenderer = new CubemapRenderer($this->shaders); $this->iblCache = new IBLCache(); // load the required shaders diff --git a/tests/Geo/Primitive/PrimitiveTest.php b/tests/Geo/Primitive/PrimitiveTest.php new file mode 100644 index 0000000..c0a512a --- /dev/null +++ b/tests/Geo/Primitive/PrimitiveTest.php @@ -0,0 +1,126 @@ + + */ + public static function primitiveProvider() : array + { + return [ + 'sphere' => [fn (VertexLayout $l, FloatBuffer $t) => Sphere::make($l, $t, 0.5, 16, 8)], + 'cube' => [fn (VertexLayout $l, FloatBuffer $t) => Cube::make($l, $t, 2.0, 1.0, 3.0)], + 'plane' => [fn (VertexLayout $l, FloatBuffer $t) => Plane::make($l, $t, 4.0, 4.0, 3)], + 'cylinder' => [fn (VertexLayout $l, FloatBuffer $t) => Cylinder::make($l, $t, 1.0, 2.0, 12)], + 'cone' => [fn (VertexLayout $l, FloatBuffer $t) => Cone::make($l, $t, 1.0, 2.0, 12)], + ]; + } + + /** + * @dataProvider primitiveProvider + */ + public function testProducesWholeTrianglesForPositionNormal(callable $make) + { + $layout = VertexLayout::with([VertexLayout::position, VertexLayout::normal]); + $buffer = new FloatBuffer(); + $make($layout, $buffer); + + $this->assertGreaterThan(0, $buffer->size()); + $this->assertSame(0, $buffer->size() % $layout->stride(), 'buffer holds whole vertices'); + $this->assertSame(0, $buffer->size() % (3 * $layout->stride()), 'buffer holds whole triangles'); + } + + /** + * @dataProvider primitiveProvider + */ + public function testProducesWholeTrianglesForPositionNormalUv(callable $make) + { + $layout = VertexLayout::with([VertexLayout::position, VertexLayout::normal, VertexLayout::uv]); + $buffer = new FloatBuffer(); + $make($layout, $buffer); + + $this->assertSame(8, $layout->stride()); + $this->assertGreaterThan(0, $buffer->size()); + $this->assertSame(0, $buffer->size() % (3 * $layout->stride())); + } + + public function testAppendsIntoExistingBuffer() + { + $layout = VertexLayout::with([VertexLayout::position, VertexLayout::normal]); + $buffer = new FloatBuffer(); + + Cube::standardCube($layout, $buffer); + $afterCube = $buffer->size(); + + Sphere::unitSphere($layout, $buffer); + + $this->assertGreaterThan($afterCube, $buffer->size()); + } + + public function testSphereNormalsAreUnitAndPositionsOnRadius() + { + $radius = 0.5; + $layout = VertexLayout::with([VertexLayout::position, VertexLayout::normal]); + $buffer = new FloatBuffer(); + Sphere::make($layout, $buffer, $radius, 16, 8); + + for ($i = 0; $i < $buffer->size(); $i += 6) { + $pos = new Vec3($buffer[$i], $buffer[$i + 1], $buffer[$i + 2]); + $normal = new Vec3($buffer[$i + 3], $buffer[$i + 4], $buffer[$i + 5]); + + $this->assertEqualsWithDelta(1.0, $normal->length(), 1e-4); + $this->assertEqualsWithDelta($radius, $pos->length(), 1e-4); + } + } + + public function testSphereHasNoDegenerateOrInwardTriangles() + { + $layout = VertexLayout::with([VertexLayout::position, VertexLayout::normal]); + $buffer = new FloatBuffer(); + Sphere::make($layout, $buffer, 0.5, 32, 16); + + $stride = 6; + for ($i = 0; $i < $buffer->size(); $i += 3 * $stride) { + $p0 = new Vec3($buffer[$i], $buffer[$i + 1], $buffer[$i + 2]); + $p1 = new Vec3($buffer[$i + $stride], $buffer[$i + $stride + 1], $buffer[$i + $stride + 2]); + $p2 = new Vec3($buffer[$i + 2 * $stride], $buffer[$i + 2 * $stride + 1], $buffer[$i + 2 * $stride + 2]); + + $faceNormal = Vec3::cross($p1 - $p0, $p2 - $p0); + + // no degenerate (zero area) triangles, especially at the poles + $this->assertGreaterThan(1e-9, $faceNormal->length(), 'triangle is degenerate'); + + // winding matches the stored normals, i.e. the face points outward + $averageNormal = new Vec3( + $buffer[$i + 3] + $buffer[$i + $stride + 3] + $buffer[$i + 2 * $stride + 3], + $buffer[$i + 4] + $buffer[$i + $stride + 4] + $buffer[$i + 2 * $stride + 4], + $buffer[$i + 5] + $buffer[$i + $stride + 5] + $buffer[$i + 2 * $stride + 5] + ); + $this->assertGreaterThan(0.0, Vec3::dot($faceNormal, $averageNormal), 'triangle faces inward'); + } + } + + public function testBuilderThrowsWhenAttributeMissing() + { + $this->expectException(PrimitiveException::class); + + $layout = VertexLayout::with([VertexLayout::position, VertexLayout::uv]); + $builder = new PrimitiveBuilder($layout, new FloatBuffer()); + + // uv is requested but not provided + $builder->addVertex(new Vec3(0.0, 0.0, 0.0)); + } +} diff --git a/tests/Geo/Primitive/VertexLayoutTest.php b/tests/Geo/Primitive/VertexLayoutTest.php new file mode 100644 index 0000000..42b2e14 --- /dev/null +++ b/tests/Geo/Primitive/VertexLayoutTest.php @@ -0,0 +1,60 @@ +assertSame(8, $layout->stride()); + } + + public function testPositionOnlyStride() + { + $layout = VertexLayout::with([VertexLayout::position]); + $this->assertSame(3, $layout->stride()); + } + + public function testOffsets() + { + $layout = VertexLayout::with([ + VertexLayout::position, + VertexLayout::normal, + VertexLayout::uv, + ]); + + $this->assertSame(0, $layout->offsetOf(VertexAttribute::position)); + $this->assertSame(3, $layout->offsetOf(VertexAttribute::normal)); + $this->assertSame(6, $layout->offsetOf(VertexAttribute::uv)); + } + + public function testHasAndMissingOffset() + { + $layout = VertexLayout::with([VertexLayout::position, VertexLayout::normal]); + + $this->assertTrue($layout->has(VertexAttribute::normal)); + $this->assertFalse($layout->has(VertexAttribute::uv)); + $this->assertSame(-1, $layout->offsetOf(VertexAttribute::uv)); + } + + public function testDuplicatesAreRemoved() + { + $layout = VertexLayout::with([ + VertexLayout::position, + VertexLayout::position, + VertexLayout::normal, + ]); + + $this->assertCount(2, $layout->attributes()); + $this->assertSame(6, $layout->stride()); + } +} From d3e95f3a47d07b5f626f4e60aaa3bca5b1cc896e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Do=CC=88ring?= <956212+mario-deluna@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:35:39 +0200 Subject: [PATCH 6/9] Updted supported versions --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74da75c..cc7e144 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: operating-system: ['ubuntu-latest'] - php-versions: ['8.1', '8.2'] + php-versions: ['8.2', '8.3', '8.4', '8.5'] phpunit-versions: ['9.6'] steps: From 16a06ce3221bdbc30967c032efc3f5af8e9e9a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Do=CC=88ring?= <956212+mario-deluna@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:05:50 +0200 Subject: [PATCH 7/9] Updaty --- composer.json | 2 +- examples/rendering/cubemap_skybox_demo.php | 4 +- .../low_poly_pipeline_with_cubemap.php | 351 ------------------ phpstan.neon | 1 - src/Command/CommandRegistry.php | 7 +- src/ECS/Picker/DevEntityPicker.php | 12 - src/FlyUI/FUIPerformanceTrace.php | 14 +- src/FlyUI/FUIPerformanceTracerOverlay.php | 4 +- .../Rendering/Pass/DeferredLightPass.php | 10 +- src/Graphics/Rendering/PipelineResources.php | 2 +- src/Signals/Runtime/ConsoleCommandSignal.php | 2 +- src/System/VISULowPoly/LPRenderingSystem.php | 64 +++- 12 files changed, 72 insertions(+), 401 deletions(-) delete mode 100644 examples/rendering/low_poly_pipeline_with_cubemap.php diff --git a/composer.json b/composer.json index 7b3ed53..b64e88e 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ }, "require-dev": { "phpunit/phpunit": "^9.5", - "phpstan/phpstan": "^1.8", + "phpstan/phpstan": "^2.2", "phpgl/ide-stubs": "dev-main", "phpbench/phpbench": "^1.2" }, diff --git a/examples/rendering/cubemap_skybox_demo.php b/examples/rendering/cubemap_skybox_demo.php index 7b984ea..a0051f9 100644 --- a/examples/rendering/cubemap_skybox_demo.php +++ b/examples/rendering/cubemap_skybox_demo.php @@ -75,7 +75,7 @@ public function setupDrawAfter(RenderContext $context, RenderTargetResource $ren $app->ready = function(CubemapDemoApp $app) { // define path to HDRI file - change this to point to your HDRI file - $hdriPath = '/Users/mariodoring/Downloads/cedar_bridge_sunset_1_4k.hdr'; + $hdriPath = VISU_PATH_RESOURCES . '/assets/hdri/cowboy_town_saloon_2k.hdr'; // create HDRI to cubemap converter $converter = new HDRIToCubemap($app->gl); @@ -84,7 +84,7 @@ public function setupDrawAfter(RenderContext $context, RenderTargetResource $ren $app->cubemap = $converter->convert($hdriPath, 1024); // create the cubemap renderer - $app->cubemapRenderer = new CubemapRenderer($app->gl); + $app->cubemapRenderer = new CubemapRenderer($app->shaders); // create camera system for 3D navigation $app->cameraSystem = new VISUCameraSystem($app->input, $app->dispatcher); diff --git a/examples/rendering/low_poly_pipeline_with_cubemap.php b/examples/rendering/low_poly_pipeline_with_cubemap.php deleted file mode 100644 index 7792132..0000000 --- a/examples/rendering/low_poly_pipeline_with_cubemap.php +++ /dev/null @@ -1,351 +0,0 @@ - [$x * $radius, $y * $radius, $z * $radius], - 'norm' => [$x, $y, $z] // normal is normalized position for unit sphere - ]; -} - -function addVertexToBuffer(FloatBuffer $buffer, array $vertex): void -{ - // position + normal format (6 floats per vertex) - // this matches what LPObjLoader::importMesh expects - $buffer->push($vertex['pos'][0]); - $buffer->push($vertex['pos'][1]); - $buffer->push($vertex['pos'][2]); - - $buffer->push($vertex['norm'][0]); - $buffer->push($vertex['norm'][1]); - $buffer->push($vertex['norm'][2]); -} - -// Demo State -// -------------------------------------------------------------------- -class LowPolyWithCubemapDemoState -{ - public VISUCameraSystem $cameraSystem; - public LPRenderingSystem $renderingSystem; - public LPModelCollection $models; - public ?Cubemap $environmentCubemap = null; - - // logo entity - public int $logoEntity; - - // rotation state for interpolation - public Quat $logoRotationPrevious; - public Quat $logoRotationCurrent; - - // grid configuration - public const GRID_ROUGHNESS_STEPS = 10; // x-axis: roughness 0.0 to 1.0 - public const GRID_METALLIC_STEPS = 5; // y-axis: metallic 0.0 or 1.0 - public const SPHERE_SPACING = 1.2; - - // sphere entities - /** @var array */ - public array $sphereEntities = []; -} - -$state = new LowPolyWithCubemapDemoState; - -/** - * Main Entry Point - * - * ---------------------------------------------------------------------------- - */ -$quickstart = new Quickstart(function(QuickstartOptions $app) use(&$state, $container) -{ - // Initialize the application - // -------------------------------------------------------------------- - $app->container = $container; - $app->ready = function(QuickstartApp $app) use(&$state) - { - // create a model collection - $state->models = new LPModelCollection(); - $state->renderingSystem = new LPRenderingSystem($app->gl, $app->shaders, $state->models); - - // load the VISU models coming with the engine (including logo) - $loader = new LPObjLoader($app->gl); - $loader->loadAllInDirectory(VISU_PATH_FRAMEWORK_RESOURCES . '/model/visu', $state->models); - - // create a vertex buffer for our procedural spheres - $vb = new LPVertexBuffer($app->gl); - - // generate sphere mesh data once (position + normal format) - $sphereMeshData = generateSphereMesh(0.5, 32, 16); - - // create sphere models with varying roughness and metallic values - // x-axis: roughness (0.0 to 1.0) - // y-axis: metallic (0.0 = dielectric, 1.0 = metallic) - $baseColor = new Vec3(0.8, 0.2, 0.2); // red-ish base color - $baseColor = new Vec3(1.); - - for ($my = 0; $my < LowPolyWithCubemapDemoState::GRID_METALLIC_STEPS; $my++) { - $metallic = $my / max(1, LowPolyWithCubemapDemoState::GRID_METALLIC_STEPS - 1); - - for ($rx = 0; $rx < LowPolyWithCubemapDemoState::GRID_ROUGHNESS_STEPS; $rx++) { - $roughness = $rx / max(1, LowPolyWithCubemapDemoState::GRID_ROUGHNESS_STEPS - 1); - - $roughness = min(max($roughness, 0.04), 1.0); // avoid 0.0 roughness for better visibility - $metallic = min(max($metallic, 0.0), 1.0); - - // create unique material for this sphere - $materialName = sprintf("pbr_r%.2f_m%.2f", $roughness, $metallic); - $material = new LPMaterial( - $materialName, - $baseColor->copy(), - $roughness, - $metallic - ); - - // import mesh with this material - $mesh = $loader->importMesh($sphereMeshData, $material, $vb); - - // create model and add to collection - $model = new LPModel("sphere_{$rx}_{$my}", [$mesh]); - $model->recalculateAABB(); - $state->models->add($model); - } - } - - // upload all vertex data to GPU - $vb->upload(); - - // create environment cubemap from HDRI (if available) - $hdriPath = '/Users/mariodoring/Downloads/cedar_bridge_sunset_1_4k.hdr'; - // $hdriPath = '/Users/mariodoring/Downloads/industrial_wooden_attic_4k.hdr'; - // $hdriPath = '/Users/mariodoring/Downloads/newport_loft.hdr'; - - $cubemapResolution = 1024; - - if (file_exists($hdriPath)) { - // convert HDRI to cubemap (1024x1024 faces for higher quality) - $converter = new HDRIToCubemap($app->gl); - $state->environmentCubemap = $converter->convert($hdriPath, $cubemapResolution); - echo "Loaded HDRI environment: $hdriPath\n"; - echo "Cubemap resolution: {$cubemapResolution}x{$cubemapResolution} per face\n"; - } else { - echo "No HDRI file found at: $hdriPath\n"; - echo "You can download free HDRI files from https://polyhaven.com/hdris\n"; - echo "Place your .hdr file at: /Users/mariodoring/Downloads/cedar_bridge_sunset_1_4k.hdr\n"; - } - - // set the environment cubemap in the rendering system - if ($state->environmentCubemap) { - $state->renderingSystem->setEnvironmentCubemap($state->environmentCubemap); - $state->renderingSystem->renderSkybox = true; // enable skybox rendering - } - - // to render 3D we need a camera - $state->cameraSystem = new VISUCameraSystem($app->input, $app->dispatcher); - - // register the rendering system - $app->bindSystems([ - $state->renderingSystem, - $state->cameraSystem - ]); - }; - - // Initialize the scene - // -------------------------------------------------------------------- - $app->initializeScene = function(QuickstartApp $app) use(&$state) - { - // position camera to see the entire grid - $gridWidth = LowPolyWithCubemapDemoState::GRID_ROUGHNESS_STEPS * LowPolyWithCubemapDemoState::SPHERE_SPACING; - $gridHeight = LowPolyWithCubemapDemoState::GRID_METALLIC_STEPS * LowPolyWithCubemapDemoState::SPHERE_SPACING; - $cameraDistance = max($gridWidth, $gridHeight) * 1.2; - - $state->cameraSystem->spawnDefaultFlyingCamera($app->entities, new Vec3( - $gridWidth * 0.5 - LowPolyWithCubemapDemoState::SPHERE_SPACING * 0.5, - $gridHeight * 0.5 - LowPolyWithCubemapDemoState::SPHERE_SPACING * 0.5, - $cameraDistance - )); - - // spawn a visu logo in the middle - $state->logoEntity = $app->entities->create(); - $app->entities->attach($state->logoEntity, new LPDynamicModel('visu_logo')); - $logoTransform = $app->entities->attach($state->logoEntity, new Transform()); - $logoTransform->orientation->rotate(GLM::radians(90.0), new Vec3(1.0, 0.0, 0.0)); - $logoTransform->position = new Vec3( - $gridWidth * 0.5 - LowPolyWithCubemapDemoState::SPHERE_SPACING * 0.5, - $gridHeight * 0.5 - LowPolyWithCubemapDemoState::SPHERE_SPACING * 0.5, - -1.0 // place logo slightly in front of the sphere grid - ); - - // initialize rotation state for interpolation - $state->logoRotationPrevious = $logoTransform->orientation->copy(); - $state->logoRotationCurrent = $logoTransform->orientation->copy(); - - // spawn sphere grid - // x-axis: roughness (left = 0.0, right = 1.0) - // y-axis: metallic (bottom = 0.0, top = 1.0) - for ($my = 0; $my < LowPolyWithCubemapDemoState::GRID_METALLIC_STEPS; $my++) { - for ($rx = 0; $rx < LowPolyWithCubemapDemoState::GRID_ROUGHNESS_STEPS; $rx++) { - $entity = $app->entities->create(); - - // attach the corresponding sphere model - $modelName = "sphere_{$rx}_{$my}"; - $app->entities->attach($entity, new LPDynamicModel($modelName)); - - // position in grid - $transform = $app->entities->attach($entity, new Transform()); - $transform->position = new Vec3( - $rx * LowPolyWithCubemapDemoState::SPHERE_SPACING, - $my * LowPolyWithCubemapDemoState::SPHERE_SPACING, - 0.0 - ); - - $state->sphereEntities[] = $entity; - } - } - - echo "Spawned " . count($state->sphereEntities) . " spheres in a grid\n"; - echo "X-axis: Roughness (0.0 left -> 1.0 right)\n"; - echo "Y-axis: Metallic (0.0 bottom -> 1.0 top)\n"; - }; - - // Update the scene - // -------------------------------------------------------------------- - $app->update = function(QuickstartApp $app) use(&$state) - { - $app->updateSystem($state->cameraSystem); - - // store previous state before updating - $state->logoRotationPrevious = $state->logoRotationCurrent->copy(); - - // rotate the logo by a fixed amount per tick - $state->logoRotationCurrent->rotate(GLM::radians(1.0), new Vec3(0.0, 0.0, 1.0)); - }; - - // Render the scene - // -------------------------------------------------------------------- - $app->render = function(QuickstartApp $app, RenderContext $context, RenderTargetResource $target) use(&$state) - { - // interpolate between previous and current rotation state using compensation - // this way you get butter smooth rotation - $logoTransform = $app->entities->get($state->logoEntity, Transform::class); - $logoTransform->orientation = Quat::slerp($state->logoRotationPrevious, $state->logoRotationCurrent, $context->compensation); - $logoTransform->markDirty(); - - // make sure to tell the low poly rendering system which render target we are using - $state->renderingSystem->setRenderTarget($target); - - $app->renderSystem($state->cameraSystem, $context); - $app->renderSystem($state->renderingSystem, $context); - }; -}); - -echo "PBR Material Debug Grid with Enhanced IBL\n"; -echo "==========================================\n"; -echo "Controls:\n"; -echo " WASD/Mouse - Fly around\n"; -echo "\n"; -echo "IBL Quality Improvements:\n"; -echo " - 4096 samples for prefiltered environment maps\n"; -echo " - 1024x1024 cubemap resolution (configurable)\n"; -echo " - 256x256 prefilter maps with better mip chaining\n"; -echo " - Improved filtering and mip level calculations\n"; -echo " - Anisotropic filtering when supported\n"; -echo "\n"; -echo "Noise should be significantly reduced compared to default settings.\n"; -echo "Adjust cubemapResolution variable for performance vs quality trade-off.\n"; -echo "\n"; - -$quickstart->run(); \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon index 97cae0e..6aee602 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -19,6 +19,5 @@ parameters: - '/expects +VISU\\Signal\\SignalQueue, VISU\\Signal\\SignalQueue<.*>/' - '/.*expects GL\\Buffer\\GL\\.*/' - '/Parameter .* expects GL\\Math\\Vec[0-4], \((array\|)?float\|int\) given\./' - - '/constructor expects GL\\Math\\Vec[0-4], float given\./' - '/.*[copy]\(\) on \(float\|int\).*/' - '/should return GL\\Math\\Vec[0-4] but returns float\./' \ No newline at end of file diff --git a/src/Command/CommandRegistry.php b/src/Command/CommandRegistry.php index da7358b..621677c 100644 --- a/src/Command/CommandRegistry.php +++ b/src/Command/CommandRegistry.php @@ -143,14 +143,9 @@ public function executeWithVector(string $commandName, array $argumentVector = [ $argv = $argumentVector; } - // load the service + // load the service $command = $this->load($commandName); - // make sure it extends the base command - if (!$command instanceof Command) { - throw new CommandException("The command '$commandName' does not extend the base command \\VISU\\Command\\Command"); - } - // create a new command line interface if (is_null($cli)) { $cli = new CliInterface; diff --git a/src/ECS/Picker/DevEntityPicker.php b/src/ECS/Picker/DevEntityPicker.php index 36ded4e..704d100 100644 --- a/src/ECS/Picker/DevEntityPicker.php +++ b/src/ECS/Picker/DevEntityPicker.php @@ -6,7 +6,6 @@ use GL\Math\Vec4; use VISU\ECS\EntitiesInterface; use VISU\ECS\EntityRegistry; -use VISU\ECS\Exception\EntityPickerException; use VISU\Graphics\Rendering\Pass\CameraData; use VISU\Graphics\RenderTarget; use VISU\Signal\Dispatcher; @@ -54,13 +53,6 @@ public function __construct( array $systems, // Array of systems that can produce pickable geometry ) { - // validate all systems are of the correct type - foreach($systems as $system) { - if (!$system instanceof DevEntityPickerRenderInterface) { - throw new EntityPickerException("All system that contribute to entity picking must extend the 'DevEntityPickerRenderInterface'."); - } - } - $this->systems = $systems; // register a click event handler @@ -117,10 +109,6 @@ public static function pickEntity(EntitiesInterface $entities, RenderTarget $ren $renderTarget->framebuffer()->clear(); foreach($systems as $system) { - if (!$system instanceof DevEntityPickerRenderInterface) { - throw new EntityPickerException("All system that contribute to entity picking must extend the 'DevEntityPickerRenderInterface'."); - } - $system->renderEntityIdsForPicking($entities, $cameraData); } diff --git a/src/FlyUI/FUIPerformanceTrace.php b/src/FlyUI/FUIPerformanceTrace.php index d2f0d45..cdef917 100644 --- a/src/FlyUI/FUIPerformanceTrace.php +++ b/src/FlyUI/FUIPerformanceTrace.php @@ -57,8 +57,8 @@ public function getHierarchicalData(): array /** * Get the raw flat tracing data - * - * @return array + * + * @return list */ public function getRawData(): array { @@ -150,7 +150,7 @@ private function countMethodCallsByType(array $node, string $methodType, int &$c * Flattens hierarchical tree to raw tracing data * * @param array $node - * @param array &$rawData + * @param list &$rawData */ private function flattenTreeToRawData(array $node, array &$rawData): void { @@ -270,11 +270,13 @@ private function sortHierarchicalTree(array &$tree, string $sortBy, bool $showMe usort($tree['children'], function($a, $b) { $aMethods = $a['methods'] ?? []; $bMethods = $b['methods'] ?? []; - if (empty($aMethods) || empty($bMethods)) { + $aTimestamps = array_column($aMethods, 'timestamp'); + $bTimestamps = array_column($bMethods, 'timestamp'); + if (empty($aTimestamps) || empty($bTimestamps)) { return $a['object_id'] <=> $b['object_id']; } - $aTimestamp = min(array_column($aMethods, 'timestamp')); - $bTimestamp = min(array_column($bMethods, 'timestamp')); + $aTimestamp = min($aTimestamps); + $bTimestamp = min($bTimestamps); return $aTimestamp <=> $bTimestamp; }); } diff --git a/src/FlyUI/FUIPerformanceTracerOverlay.php b/src/FlyUI/FUIPerformanceTracerOverlay.php index c11c6d9..d596cc6 100644 --- a/src/FlyUI/FUIPerformanceTracerOverlay.php +++ b/src/FlyUI/FUIPerformanceTracerOverlay.php @@ -514,8 +514,8 @@ private function renderTreeContent(FUIRenderContext $ctx, FUIPerformanceTrace $t $treeOutput = $trace->renderPerformanceTree($this->showMethods, 'timestamp'); $lines = explode("\n", $treeOutput); - $visibleLines = floor($height / $this->lineHeight); - $startLine = floor($this->scrollOffset / $this->lineHeight); + $visibleLines = (int) floor($height / $this->lineHeight); + $startLine = (int) floor($this->scrollOffset / $this->lineHeight); $endLine = min(count($lines), $startLine + $visibleLines); for ($i = $startLine; $i < $endLine; $i++) { diff --git a/src/Graphics/Rendering/Pass/DeferredLightPass.php b/src/Graphics/Rendering/Pass/DeferredLightPass.php index 1b03f38..300e827 100644 --- a/src/Graphics/Rendering/Pass/DeferredLightPass.php +++ b/src/Graphics/Rendering/Pass/DeferredLightPass.php @@ -86,9 +86,9 @@ private function determinePermutation(PipelineContainer $data, PipelineResources $brdfLut = $resources->getTexture($iblData->brdfLut); // full IBL when all three IBL textures are available - if ($irradianceCubemap && $irradianceCubemap->id > 0 && - $prefilterCubemap && $prefilterCubemap->id > 0 && - $brdfLut && $brdfLut->id > 0) { + if ($irradianceCubemap->id > 0 && + $prefilterCubemap->id > 0 && + $brdfLut->id > 0) { return DeferredLightPassPermutation::IBL; } } @@ -96,7 +96,7 @@ private function determinePermutation(PipelineContainer $data, PipelineResources // check if environment cubemap is available if ($this->environmentCubemap) { $glCubemap = $resources->getCubemap($this->environmentCubemap); - if ($glCubemap && $glCubemap->id > 0) { + if ($glCubemap->id > 0) { return DeferredLightPassPermutation::envCubemap; } } @@ -159,7 +159,7 @@ public function execute(PipelineContainer $data, PipelineResources $resources): } // bind permutation-specific uniforms - if ($permutation === DeferredLightPassPermutation::envCubemap) { + if ($permutation === DeferredLightPassPermutation::envCubemap && $this->environmentCubemap !== null) { $glCubemap = $resources->getCubemap($this->environmentCubemap); $glCubemap->bind(GL_TEXTURE0 + $textureUnit); $shader->setUniform1i('environment_cubemap', $textureUnit); diff --git a/src/Graphics/Rendering/PipelineResources.php b/src/Graphics/Rendering/PipelineResources.php index f90a844..491be04 100644 --- a/src/Graphics/Rendering/PipelineResources.php +++ b/src/Graphics/Rendering/PipelineResources.php @@ -145,7 +145,7 @@ private function createRenderTarget(RenderTargetResource $resource) : void $drawBuffers[] = GL_COLOR_ATTACHMENT0 + $i; } - else if ($colorAttachmentTextureResource instanceof CubemapResource) { + else { $cubemap = new Cubemap($this->gl, $colorAttachmentTextureResource->name); $options = $colorAttachmentTextureResource->options ?? new TextureOptions; diff --git a/src/Signals/Runtime/ConsoleCommandSignal.php b/src/Signals/Runtime/ConsoleCommandSignal.php index 79a38a6..0dbf0c7 100644 --- a/src/Signals/Runtime/ConsoleCommandSignal.php +++ b/src/Signals/Runtime/ConsoleCommandSignal.php @@ -41,6 +41,6 @@ public function __construct( */ public function isAction(string $command) : bool { - return ($this->commandParts[0] ?? null) === $command; + return $this->commandParts[0] === $command; } } diff --git a/src/System/VISULowPoly/LPRenderingSystem.php b/src/System/VISULowPoly/LPRenderingSystem.php index 558a5ec..a212a0f 100644 --- a/src/System/VISULowPoly/LPRenderingSystem.php +++ b/src/System/VISULowPoly/LPRenderingSystem.php @@ -143,7 +143,9 @@ class LPRenderingSystem implements SystemInterface, DevEntityPickerRenderInterfa private DrawCallAssembler $staticGeometryDCA; /** - * Indicates whether the static geometry DCA needs to be rebuilt + * Indicates whether the static geometry DCA needs to be rebuilt. + * Attaching or detaching a static model flags this, the static render + * pass then re-bakes all static instances on the next frame. */ private bool $staticGeometryIsDirty = true; @@ -207,9 +209,13 @@ public function register(EntitiesInterface $entities) : void $this->onAttachDynamicModelHandle = $entities->onAttach(LPDynamicModel::class, [$this, 'handleAttachDynamicModel']); // construct the draw call assembler for static geometry + // a persistent octree is reused across frames and only rebuilt when the + // instance set changes, ideal for the large mostly-static scene $this->staticGeometryDCA = new DrawCallAssembler(); + $this->staticGeometryDCA->setCullingStrategy(DrawCallAssembler::CULL_OCTREE); // construct the draw call assembler for dynamic geometry + // per-instance frustum culling each frame, best for the fully dynamic set $this->dynamicGeometryDCA = new DrawCallAssembler(); } @@ -222,8 +228,15 @@ public function unregister(EntitiesInterface $entities) : void { $entities->releaseOnAttach($this->onAttachStaticModelHandle); $entities->releaseOnDetach($this->onDetachStaticModelHandle); + $entities->releaseOnAttach($this->onAttachDynamicModelHandle); } + /** + * Registers the given model's geometry with the draw call assembler if it + * has not been registered yet, storing the resulting mesh handle by-reference. + * + * @param-out int $meshDcaHandle + */ private function registerModelWithDCA(LPModel $model, DrawCallAssembler $dca, ?int &$meshDcaHandle) : void { // we render the lowpoly models as a single mesh instead of the multiple sub-meshes @@ -270,6 +283,7 @@ private function registerModelWithDCA(LPModel $model, DrawCallAssembler $dca, ?i */ public function handleAttachStaticModel(EntitiesInterface $entities, int $entity, LPStaticModel $component) : void { + // the static assembler needs to be rebuilt to include the new instance $this->staticGeometryIsDirty = true; // validate the model is present @@ -280,18 +294,9 @@ public function handleAttachStaticModel(EntitiesInterface $entities, int $entity $model = $this->modelCollection->models[$component->modelIdentifier]; + // register the mesh geometry once, the per-instance transforms are + // (re)submitted during the dirty rebuild in the static render pass $this->registerModelWithDCA($model, $this->staticGeometryDCA, $model->staticDCAHandle); - - if (!$transform = $entities->tryGet($entity, Transform::class)) { - throw new LPException('LPRenderingSystem - Please attach a Transform component first, entity: ' . $entity); - } - - // submit the instance - $this->staticGeometryDCA->submit( - meshHandle: $model->staticDCAHandle, - transform: $transform->getWorldMatrix($entities), - materialId: 1, // material data is packed into the vertex buffer. - ); } /** @@ -299,6 +304,7 @@ public function handleAttachStaticModel(EntitiesInterface $entities, int $entity */ public function handleDetachStaticModel(EntitiesInterface $entities, int $entity, LPStaticModel $component) : void { + // the static assembler needs to be rebuilt so the removed instance is dropped $this->staticGeometryIsDirty = true; } @@ -414,10 +420,42 @@ function(RenderPass $pass, RenderPipeline $pipeline, PipelineContainer $data) us $pipeline->writes($pass, $gbuffer->renderTarget); }, // execute - function(PipelineContainer $data, PipelineResources $resources) use(&$renderMetrics) + function(PipelineContainer $data, PipelineResources $resources) use($entities, &$renderMetrics) { $cameraData = $data->get(CameraData::class); + // rebuild the static instance buffer only when the geometry changed + if ($this->staticGeometryIsDirty) { + $this->staticGeometryDCA->clearInstances(); + + // fetch all static models and (re)submit their instances + $instanceCount = 0; + /** @var iterable $staticView */ + $staticView = $entities->viewWith(LPStaticModel::class, Transform::class); + foreach ($staticView as $entity => [$renderable, $transform]) { + if (!isset($this->modelCollection->models[$renderable->modelIdentifier])) { + continue; + } + + $model = $this->modelCollection->models[$renderable->modelIdentifier]; + + // unregistered model + if (is_null($model->staticDCAHandle)) { + continue; + } + + $this->staticGeometryDCA->submit( + meshHandle: $model->staticDCAHandle, + transform: $transform->getWorldMatrix($entities), + materialId: 1, // material data is packed into the vertex buffer. + ); + $instanceCount++; + } + + $this->staticGeometryIsDirty = false; + Logger::info(sprintf('[LPRenderingSystem] Rebuilding static geometry DCA with %d instances', $instanceCount)); + } + $this->objectInstancedShader->use(); $this->objectInstancedShader->setUniformMatrix4f('projection', false, $cameraData->projection); $this->objectInstancedShader->setUniformMatrix4f('view', false, $cameraData->view); From 89d64a7edaa143e90b2d030bcb61ec1b2c9d8d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Do=CC=88ring?= <956212+mario-deluna@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:45:23 +0200 Subject: [PATCH 8/9] Visual regressions --- phpunit.php | 13 +- src/Testing/ImageCapture.php | 32 +++ src/Testing/ImageComparator.php | 162 ++++++++++++++ src/Testing/ImageComparison.php | 89 ++++++++ tests/Visual/FlyUIVisualTest.php | 53 +++++ tests/Visual/VisualTestCase.php | 197 ++++++++++++++++++ .../resources/visual/expected/flyui_card.png | Bin 0 -> 9687 bytes 7 files changed, 542 insertions(+), 4 deletions(-) create mode 100644 src/Testing/ImageCapture.php create mode 100644 src/Testing/ImageComparator.php create mode 100644 src/Testing/ImageComparison.php create mode 100644 tests/Visual/FlyUIVisualTest.php create mode 100644 tests/Visual/VisualTestCase.php create mode 100644 tests/resources/visual/expected/flyui_card.png diff --git a/phpunit.php b/phpunit.php index e97b695..6f5b720 100644 --- a/phpunit.php +++ b/phpunit.php @@ -7,12 +7,17 @@ * * We need to access our dependencies & autloader.. */ -require __DIR__ . - DS . - 'vendor' . - DS . +require __DIR__ . + DS . + 'vendor' . + DS . 'autoload.php'; +// don't capture function arguments in exception backtraces (matches the production php.ini default). +// with args captured, an exception constructed inside the test harness stores closures from the call +// stack in its trace, which makes serializing exception-like components fail. +ini_set('zend.exception_ignore_args', '1'); + // main paths define('VISU_PATH_ROOT', __DIR__ . DS . 'tests_env'); define('VISU_PATH_CACHE', VISU_PATH_ROOT . DS . 'var' . DS . 'cache'); diff --git a/src/Testing/ImageCapture.php b/src/Testing/ImageCapture.php new file mode 100644 index 0000000..042235d --- /dev/null +++ b/src/Testing/ImageCapture.php @@ -0,0 +1,32 @@ +bind(); + + // tightly pack rows; the default GL_PACK_ALIGNMENT (4) pads rows when width*3 isn't a + // multiple of 4 (e.g. width 250 -> 750 bytes/row) which would skew the captured image + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, $buffer); + + return Texture2D::fromBuffer($texture->width(), $texture->height(), $buffer, Texture2D::CHANNEL_RGB); + } +} diff --git a/src/Testing/ImageComparator.php b/src/Testing/ImageComparator.php new file mode 100644 index 0000000..671e7e8 --- /dev/null +++ b/src/Testing/ImageComparator.php @@ -0,0 +1,162 @@ +width() !== $actual->width() || $expected->height() !== $actual->height()) { + return ImageComparison::dimensionMismatch(sprintf( + 'expected %dx%d, got %dx%d', + $expected->width(), $expected->height(), $actual->width(), $actual->height() + )); + } + + if ($expected->channels() !== $actual->channels()) { + return ImageComparison::dimensionMismatch(sprintf( + 'expected %d channels, got %d', + $expected->channels(), $actual->channels() + )); + } + + $width = $expected->width(); + $height = $expected->height(); + $channels = $expected->channels(); + + $e = $expected->buffer()->dump(); + $a = $actual->buffer()->dump(); + + // fast path: byte identical + if ($e === $a) { + return ImageComparison::identical($width, $height); + } + + $length = strlen($e); + + $sum = 0; + $maxChannelDiff = 0; + $badPixelCount = 0; + + // identical bytes become \0 in the xor, allowing us to skip long identical runs at C speed + // via strspn and only inspect the bytes that actually differ in PHP. + $xor = $e ^ $a; + $i = strspn($xor, "\0"); + + $currentPixel = -1; + $currentPixelMax = 0; + + while ($i < $length) { + $diff = abs(ord($e[$i]) - ord($a[$i])); + $sum += $diff; + if ($diff > $maxChannelDiff) { + $maxChannelDiff = $diff; + } + + // group channels into pixels, counting a pixel as bad once its largest channel diff + // exceeds the tolerance + $pixel = intdiv($i, $channels); + if ($pixel !== $currentPixel) { + if ($currentPixel !== -1 && $currentPixelMax > $this->pixelTolerance) { + $badPixelCount++; + } + $currentPixel = $pixel; + $currentPixelMax = $diff; + } elseif ($diff > $currentPixelMax) { + $currentPixelMax = $diff; + } + + $i++; + // skip forward to the next differing byte + $i += strspn($xor, "\0", $i); + } + + // account for the very last touched pixel + if ($currentPixel !== -1 && $currentPixelMax > $this->pixelTolerance) { + $badPixelCount++; + } + + $meanError = $length > 0 ? $sum / ($length * 255) : 0.0; + $badPixelRatio = ($width * $height) > 0 ? $badPixelCount / ($width * $height) : 0.0; + + return new ImageComparison($width, $height, $meanError, $badPixelRatio, $maxChannelDiff); + } + + /** + * Writes a grayscale divergence visualization for two images to disk + * + * Each output pixel is the largest channel difference of the corresponding input pixels, + * amplified 5x and clamped, so that small differences remain visible (brighter = more + * divergence). Does nothing if the images differ in size. + */ + public function writeDiffImage(Texture2D $expected, Texture2D $actual, string $path) : void + { + if ($expected->width() !== $actual->width() || $expected->height() !== $actual->height()) { + return; + } + if ($expected->channels() !== $actual->channels()) { + return; + } + + $width = $expected->width(); + $height = $expected->height(); + $channels = $expected->channels(); + + $e = $expected->buffer()->dump(); + $a = $actual->buffer()->dump(); + $length = strlen($e); + + $diffBuffer = new UByteBuffer(); + + for ($pixel = 0; $pixel * $channels < $length; $pixel++) { + $base = $pixel * $channels; + + $pixelMax = 0; + for ($c = 0; $c < $channels; $c++) { + $diff = abs(ord($e[$base + $c]) - ord($a[$base + $c])); + if ($diff > $pixelMax) { + $pixelMax = $diff; + } + } + + // amplify small differences so they are actually visible + $value = min(255, $pixelMax * 5); + $diffBuffer->push($value); + $diffBuffer->push($value); + $diffBuffer->push($value); + } + + Texture2D::fromBuffer($width, $height, $diffBuffer, Texture2D::CHANNEL_RGB)->writePNG($path); + } +} diff --git a/src/Testing/ImageComparison.php b/src/Testing/ImageComparison.php new file mode 100644 index 0000000..9f42a6f --- /dev/null +++ b/src/Testing/ImageComparison.php @@ -0,0 +1,89 @@ +dimensionMismatch !== null) { + return false; + } + + if ($this->badPixelRatio > $maxBadPixelRatio) { + return false; + } + + if ($maxMeanError !== null && $this->meanError > $maxMeanError) { + return false; + } + + return true; + } + + /** + * Returns a short human readable summary of the metrics, useful for test failure messages + */ + public function summary() : string + { + if ($this->dimensionMismatch !== null) { + return "dimension mismatch: {$this->dimensionMismatch}"; + } + + return sprintf( + 'bad %.3f%% mean %.3f%% maxChannelDiff %d', + $this->badPixelRatio * 100.0, + $this->meanError * 100.0, + $this->maxChannelDiff + ); + } +} diff --git a/tests/Visual/FlyUIVisualTest.php b/tests/Visual/FlyUIVisualTest.php new file mode 100644 index 0000000..9b2de3e --- /dev/null +++ b/tests/Visual/FlyUIVisualTest.php @@ -0,0 +1,53 @@ +renderFrame(function (QuickstartApp $app, RenderContext $context, RenderTarget $target) : void { + // center a fixed size card in the viewport so the layout is deterministic + FlyUI::beginLayout() + ->verticalFill() + ->horizontalFill() + ->flow(FUILayoutFlow::vertical) + ->alignCenter(); + + FlyUI::beginCardView() + ->fixedWidth(180.0) + ->paddingAll(14.0) + ->spacing(8.0); + + FlyUI::text('Visual Test', new VGColor(0.1, 0.1, 0.12, 1.0)); + FlyUI::button('Click me', function () : void { + // no-op, the button is only rendered + }); + + FlyUI::end(); // card + FlyUI::end(); // centering layout + }); + + $this->assertImageMatchesBaseline($image, 'flyui_card'); + } +} diff --git a/tests/Visual/VisualTestCase.php b/tests/Visual/VisualTestCase.php new file mode 100644 index 0000000..f3f4c64 --- /dev/null +++ b/tests/Visual/VisualTestCase.php @@ -0,0 +1,197 @@ +bootApp(); + } + + /** + * Boots the shared headless QuickstartApp once + */ + protected function bootApp() : void + { + if (self::$app !== null) { + return; + } + + // QuickstartApp resolves shaders from VISU_PATH_RESOURCES_SHADER, which the app bootstrap + // normally defines but the test bootstrap (phpunit.php) does not. Point it at the real + // framework shaders so the full render path is exercised. + if (!defined('VISU_PATH_RESOURCES_SHADER')) { + define('VISU_PATH_RESOURCES_SHADER', VISU_PATH_FRAMEWORK_RESOURCES_SHADER); + } + + $options = new QuickstartOptions(); + $options->windowHeadless = true; + $options->windowVsync = false; + $options->windowTitle = 'VISU Visual Test'; + $options->windowWidth = static::VISUAL_WIDTH; + $options->windowHeight = static::VISUAL_HEIGHT; + + $app = new QuickstartApp(new Container(), $options); + $app->ready(); + + // the quickstart debug overlay pass reads a "loop" service for its metrics; register one so + // a single headless render can complete. the overlay draws to the backbuffer, never to the + // offscreen target we capture, so it does not affect the compared image. + $app->container->set('loop', new GameLoop($app, $options->gameLoopTickRate, $options->gameLoopMaxUpdatesPerFrame)); + + self::$app = $app; + } + + /** + * Renders a single frame using the given draw callback and returns the captured image + * + * The callback receives the same arguments as a QuickstartApp draw callback and should issue + * its draw calls (FlyUI / vector graphics / GL). FlyUI and vector graphics frames are already + * managed by the app around this callback. + * + * @param Closure(QuickstartApp, \VISU\Graphics\Rendering\RenderContext, \VISU\Graphics\RenderTarget): void $draw + */ + protected function renderFrame(Closure $draw) : Texture2D + { + $app = self::$app; + if ($app === null) { + throw new \RuntimeException('Visual test app has not been booted'); + } + + $app->options->draw = $draw; + $app->render(0.0); + + $texture = $app->renderResources->findTextureByName(self::CAPTURE_TEXTURE); + if ($texture === null) { + throw new \RuntimeException('Could not find the offscreen capture texture "' . self::CAPTURE_TEXTURE . '"'); + } + + return ImageCapture::fromColorTexture($texture); + } + + /** + * Asserts that the given capture matches the committed baseline for $name + * + * When no baseline exists yet, or when running in bless mode (VISU_VISUAL_UPDATE), the baseline + * is (re)generated and the test is marked skipped. Otherwise the capture is compared against the + * baseline; on mismatch the actual + diff images are written to the artifacts directory and the + * test fails with the divergence metrics. + */ + protected function assertImageMatchesBaseline(Texture2D $actual, string $name, ?ImageComparator $comparator = null) : void + { + $comparator ??= new ImageComparator(); + + $baselinePath = $this->baselineDir() . DIRECTORY_SEPARATOR . $name . '.png'; + $actualPath = $this->artifactDir() . DIRECTORY_SEPARATOR . $name . '.actual.png'; + $diffPath = $this->artifactDir() . DIRECTORY_SEPARATOR . $name . '.diff.png'; + + // (re)generate the baseline when blessing or when it is missing + if ($this->isBlessMode() || !is_file($baselinePath)) { + $actual->writePNG($baselinePath); + $this->markTestSkipped("baseline (re)generated for {$name}"); + } + + // always persist the actual first, then compare disk-to-disk so both images go through the + // identical Texture2D::fromDisk flip handling (avoids glReadPixels vs PNG orientation drift) + $actual->writePNG($actualPath); + + $expectedTexture = Texture2D::fromDisk($baselinePath); + $actualTexture = Texture2D::fromDisk($actualPath); + + $comparison = $comparator->compare($expectedTexture, $actualTexture); + + if (!$comparison->passes($comparator->maxBadPixelRatio, $comparator->maxMeanError)) { + $comparator->writeDiffImage($expectedTexture, $actualTexture, $diffPath); + $this->fail(sprintf( + "Visual mismatch for \"%s\": %s\n expected: %s\n actual: %s\n diff: %s", + $name, $comparison->summary(), $baselinePath, $actualPath, $diffPath + )); + } + + // clean up the actual artifact on success to keep the artifacts dir tidy + @unlink($actualPath); + + $this->assertTrue(true); + } + + /** + * Returns true if baselines should be (re)generated instead of compared + */ + protected function isBlessMode() : bool + { + $value = getenv('VISU_VISUAL_UPDATE'); + return $value !== false && $value !== '' && $value !== '0'; + } + + /** + * Directory holding the committed baseline images + */ + protected function baselineDir() : string + { + $dir = PATH_TEST_RESOURCES . DIRECTORY_SEPARATOR . 'visual' . DIRECTORY_SEPARATOR . 'expected'; + if (!is_dir($dir)) { + mkdir($dir, 0777, true); + } + return $dir; + } + + /** + * Writable directory for failure artifacts (actual + diff images) + */ + protected function artifactDir() : string + { + $dir = VISU_PATH_ROOT . DIRECTORY_SEPARATOR . 'var' . DIRECTORY_SEPARATOR . 'visual'; + if (!is_dir($dir)) { + mkdir($dir, 0777, true); + } + return $dir; + } +} diff --git a/tests/resources/visual/expected/flyui_card.png b/tests/resources/visual/expected/flyui_card.png new file mode 100644 index 0000000000000000000000000000000000000000..46be600ae3672932e09e3b1822e112af6097d5e5 GIT binary patch literal 9687 zcmeHtc|4Tw*Z*wxnX&KdP*Ra?>|q#L>SL+Mk}MIUCVNPh8AOY%6e3KG)E7xeQ<2Fo zi9yPiv3wX=BD*ZVd!+BT=l6P^zn|xi&mZpB>s)i+*L9t9u5-@&eJI|LY+D%4g4f%fKFm>AiHxp;`MkWngPkbfTk*x;XY@J|^0Urz;? zG#)nzso`&gr|H8f0`bTi{|)5}XsN&lW(Mjl4!vc;lF@t$CWbNMixu9Ah`-5w#6 z2XsglBqWAaEoS~aUHYFr$r!-maSv4YaN+3T6SVx_3)Lr3d6dwmr#r~kBbXYxtpWoV zjksIvB(}G=rQ1&I?iO=`T}2I9J(BE$PTpM&un4O+d!kSWd4JH(r)ZdnDHnrg@}JXy zF=kJ!B$=iCGBjXqK*l7AI@r@QJvEiI&p}AZsdI3Ut;)drbIDUaKE8=|*K0R!KnvNjY+PN{ z!)i~u4vh`fM0XWfmc4lK?(f3-`aA00A9+W|^Yd40Yd2`!JA1*j^N1*s9z1yZ@S)RnsgQ{b#a(jASBjUBM# zG;$Nw7^Wb|#|Jx3Dk>_!ys!>LO;{L(`xSMXfjc68uccl2?Uj|1?CfJMF37Ub`ucj@ z%o{ngfTcRPIgvSqO+OP(;FGeu4m8h z5fMP4c$*DkN?Titwo+nfJR}!(ctc=RFq!jc@#Qee{2amQ9rd=Dx~`TlEXj|ZlT%km zXC!5MrazPw7B}$vQg%+x;Kz@R4GkRHfn||DlkePtyR~La$CZW5tT#71G|Nzmi;JtP zB|A2_r46idbgL^XiMf;9IFB=D6A8bub zU{82OsxX>7`+;m(d4DHl;$7G~D(t}c!;sVdm6cYRl@5-*;75gp=Q%3v^z|c&2Z)Fd z--l|j0#JJ+&@pckDbJOsv@|{&DyFN zD5Y2|O;R2LRjJ$R5_6(_rYzs|Y$aTH3XPLP+v{{6=A;o;$rEiG|h0=G7f z=sN>b4Ab+ml`D$`y#ex;n@l}V{25>ny#+4@a!`lxYmix#Ows%y*Z7``Xim=f<3Jja zlLPWDZZVA2mluvi!fyBuP&mHWC746Ald`q^z-6&95Ds}by%dkbvD@_KTbv}2C8$YC z$}UwT-@x^yDPjL4Hvh2Dp5j+NK8~rZdXmexL6P9+#~d9e2VQYCCFfvFA6w-RyIo%T z4G~u}kVdLbCz9f+Do7&jT!@3L)}B57wQ&~8u5WdtHsX?!G;A7L(jL23g>iUd-uuRI z$m;8}P@M69zyAcx!YWG&ck9yA(9ls+yMG&zK$R7t+=Ws&dA>Nr+e=~Uh<7>to0AP% z1k`>6Eq!~@(ZTMl2}x)kKJjm5Wthq;-YS0bgx$F^V*G<69WRnkh)3rUk8zESd0w{= zq?YLWr@6SeEGLSVuY5Q3|Nb>nrm(p9-r>s!=8C!LYUkB%_atyjpFPV*FpCB)1qKB{ zGYnf>TlXJ4C@w1cSDr!8=Mw!zixTQbScj~I1qT#*U(XT7!prs2hgX1nqzfp1sm!eY z+zjn6&*Omc>#rogc){_-fJKe)?Hb;o&Js3TjyqrI?e+vnJBZR=y`b@8@Ah0RjTf&e z+R(N*g;;b{^tP#?>Zmw-gjvYIz^et~BX$2dVVu0bI{F7jvy@P9I-KPcoDOl~)3&#$ zdQ=Zi$9X^(Mq=Yyy&e4>(5zXj^m4KCAnX!0Kv!GFgsRAnNzE>d4DGi2Wp@ z9Gxl{=jm``pMt6S#IMQ^wzL%k%{s-yO+X3Akndl)x}o^xAM^9PWKl|(oTer(O9mJ>8Z!+e@5&_rkYL2Oy4D3h*3}HgZB{{d?ZL7K9J^u1G5q@Id6 zEG9rPf>M&iDG7hi4a?oTr=#UN5HnpITLhamhm~nv>()M2@5~t!2vA=+br-Zw4;Cy_zRM{9R^eE#?mn3C`pXeVt#M_L~tBMKH z;~)-~;(Ix-tzkMlI=cJ%q-t4wl?UF08U|u`r3I+#jg5`qv&pw^Il8;IFrr=qb>%Ej zS4Mm*Sd!Fx2p7UOHzQG<7}lP0hQAMzlM^(n0M0FhPgg=;%0MPqiP$fVji%ySB?tt< zw1>s{(o3JN*O{3L^jbJcyEL_|R3%UIqME-bN7gX)C%6(1mI`s_!dFAsnyl|@a}20J z4#$Nz+}O)gz4%u&bt$Inu{CS%Abdar>YNg`y#?jqm@h3&1;B)!j*iTmxK$D;c^e44 zy{f7%wzdM4C%lx}EO!9_-!&t_jSm=+=4{dC6p1gJY$cB4V`DbQkH2%Rhw8H^HeJ1X zmHwtK;4+W)7T4iaeuq2Ic9GiX+I^BS>joep%dFor@OHs3NXq;K ze8Sj`iCX~__TPY50LlJ|apR5H6OjRl@}oP80grqpDoT-W`zjaGOXc57 zGX>~x0mub^VqAa%nI>qPrp^BfS$23QGeeoBjql1#*#8IECfwl>xoDJ9knwwIrtlxN z0Lb?Tba&^0kg*cvmatvm&g!$nA}oHh2tY1PY=oEaXb1e5c-mj%H?$QwwVe0aZ!4L# zn+^vb=k?OC6SA{ArD1o9D`=U96GIS(1t8>?;U3r9`a`3vv**`F9=VczB5_vB)uC3r5?bNnL=YJDCoR zHyvWQyc^C)GaC)A2$uS=mFqZ?!bCGo|6F*VBKwoutfIc}Eyse>@soQen=5G$>ST%Z2lmR!lY{Vxrlq&f6C6}KhD@UR_esssaz%>tt6I1B$AY*4I(h;CyovxEUOP#V^Z*7|~ zwye`Vo{Q;a>Fm%?)@b&`WTUPF$&^>3di}6l$0no=x3<@>DtIg{eewEB@YLR5&GD9# zhTb2xA~wd_E0ejEU6ZAb={prN89yh}@|LpV+GVsJ#U~Hp`k7%<%ipK8A|mJJxNQbL z#0@rP)wSG$Y7d~~-#cfvXWacMlp;q^V%QbcKxnwPzgVLWGu*oNG!mN{r@oaMCC<)l zvn!HNczWrNcXfsY#)NW|GPf0p6t8=*RH7zmMe9(gf1uiXb1Zpswt8s0)Kb;h^feJH z+Mc@!ShU^n;bYX^y_u+!8JK=){jG(Ceal7y6!ECdt?fA!j(-2{U0!WJ4%_Ip_sU~N zB3@psN|u^PWMlb6)JQ06$6t~cE3Nak!Q8wXdVQ)QO5tW$&}n8|bz2KlM>>0N@t>WY zR5u)cq+VtTd{X(sYvOMfgz8#YfK_Hn8BG-w^49`ARv@2sZEBbU38#aO zIgr-1{I-@YH(-a;*%_Gwcmv^mNb~F51AEGXKCdKY*HlN|JaGmyfcJTa3|22S1!sYZ zD@x96>xW|)IIxBoB$H{|lZJMg_sW6-jP|XKD{aJlib;{)w(EJDk;Za$d&O_ea!b|o z(E=;m16N7ey$#MzAGq;%t#g+-QicS^9G$XYGLIHsE!60~HrU!%D=0CYNG;PaNKfF^ zyuM$y);YB)BXvVwfpCzT!sRPBS4hoNkfOg&8;E(?Y&kjomjUUhX9Acj+(WK1_t3kt z_bb#TCGx9aX2t@VKtNn9@yAw4plzeKx1y(nJ{HWsxKKm9hMywKy&8VBaLdEcDsOA= zp87GjCX_OFnZ62PYJ6tsO-OXpd4tl$X&nC;%d=M2R<^F^5wLZcmCz=1mN-m65c3r87W=a5$7X+MzE0r2 zPR8=r$f02_HsTuz)Fbtd6K<$0-=%{2ljC5)q7?ZLZSAfb`oSX4_ws3j%6z!Gxv2UY z$}%F)6Wn5(nG<-7X%*>+9Dn(`i)SnAsV{&Ol5@5qKnkCM6@KENeQOet$?u7=Ww zJcS`e2)9p=;y~Lwjs*PJrxEhyOTc*w+zHZ@&CpWK{rPd99znF5NQd1u?vu$0h0Oh+=oO zEb*O8-eK$(d^>gB0`f&(KSDs4gY_^k2jv#5E4e<*#e5Qer>|^A?_bH30H`iINP>ru z_Y9^A)4m3FHUbYA%}##g$TN@29-?jXQXA5@aKA9> z6m+nqp+$SQbO7ZwJfGNd(g2&T3c8uwBmXWRCItrDoN2DF2jSNsS}}Di!5}tFiSy=> z!fe9n?=ska>TM*&8df4nIUq{mhf$1sKWDer~1mpY76PH1s&HTQ~7(^$VTM0>z;*9j2jsieLkqNJGw}lL%*X&)EkYPOv zrPI)KZL0ny%;L9Cu5=1p1g})bS$Y&>ypr4VgDgY?lF{{d>}Q$&-ay67gMniVYkKMR zY9+Ra$>zSLA^$amHl(X_%e`-pyPG3Nx7`_3s_Wu1caon2OL%nOhxT4k(xZ-+>s4bS6iK?R7N^MShQJ zat5*`bmsLlE@AA-TWIKe(7~s*hYi@SAkB*|FJ(P5OWBlAGTVW2(@rzam&Y??aNym%5=?n z+WnmqPLP0L1F7E)5wG7_`6}ab)Cdz+pr@epKHHb$gWoXz&32Qo@@n-`hLhCt$P;BUZgD zrgxYp6@XM89pn~|{{i_j+ueV(nK4IybV_Bnv;vJ%mtDwgx;zv1OPA^;}bqu&jr z(`!(%*z<+tgUE!_AyU97Bpjd|@;(^5yO+=!+`r#+!f>adNt#X=b$AT`&mZYInQ+hW zh)9kQ8l}ffZqmjMu#zDtZ@`Kj24418AghD z1K;(~*^OMy(S@tWYE08sJ2=7gS6`^N5T9t33%J?_AqaM!LIZ%cx0Au2Y(SbWc#`ZB zIQv73xe+ Date: Thu, 23 Jul 2026 15:58:55 +0200 Subject: [PATCH 9/9] Fixed res render for regression --- src/Quickstart/QuickstartApp.php | 16 ++++++++++++++-- src/Quickstart/QuickstartOptions.php | 11 +++++++++++ tests/Visual/VisualTestCase.php | 12 +++++++++--- .../resources/visual/expected/flyui_card.png | Bin 9687 -> 3465 bytes 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Quickstart/QuickstartApp.php b/src/Quickstart/QuickstartApp.php index 773564e..b486bd3 100644 --- a/src/Quickstart/QuickstartApp.php +++ b/src/Quickstart/QuickstartApp.php @@ -288,8 +288,20 @@ public function render(float $deltaTime) : void $quickstartPassData = $data->create(QuickstartPassData::class); // create an intermediate render target with a texture attachment - $appContentScale = $windowRenderTarget->contentScaleX; - $quickstartPassData->renderTarget = $context->pipeline->createRenderTargetLike('quickstartTarget', $windowRenderTarget); + // when a fixed render resolution is requested we render at that exact pixel size with a + // content scale of 1.0 so the output is deterministic regardless of window / display DPI, + // otherwise we mirror the window render target (inheriting its high dpi content scale) + if ($this->options->renderResolution !== null) { + $appContentScale = 1.0; + $quickstartPassData->renderTarget = $context->pipeline->createRenderTarget( + 'quickstartTarget', + (int) $this->options->renderResolution->x, + (int) $this->options->renderResolution->y, + ); + } else { + $appContentScale = $windowRenderTarget->contentScaleX; + $quickstartPassData->renderTarget = $context->pipeline->createRenderTargetLike('quickstartTarget', $windowRenderTarget); + } // we want a depth and stencil buffer $quickstartPassData->renderTarget->createRenderbufferDepthStencil = true; diff --git a/src/Quickstart/QuickstartOptions.php b/src/Quickstart/QuickstartOptions.php index c1c7c01..d881280 100644 --- a/src/Quickstart/QuickstartOptions.php +++ b/src/Quickstart/QuickstartOptions.php @@ -4,6 +4,7 @@ use ClanCats\Container\Container; use Closure; +use GL\Math\Vec2; use VISU\Graphics\Rendering\RenderContext; use VISU\Graphics\Rendering\Resource\RenderTargetResource; use VISU\Graphics\RenderTarget; @@ -58,6 +59,16 @@ class QuickstartOptions */ public bool $windowHeadless = false; + /** + * Optional fixed render resolution for the intermediate draw target. + * + * When set, the offscreen render target the draw callback renders into is created at this exact + * device pixel resolution with a content scale of 1.0, independent of the window framebuffer size + * or display DPI. This makes the captured output identical across machines (e.g. so a retina Mac + * and a headless CI runner produce the same pixels) and is primarily used by visual regression tests. + */ + public ?Vec2 $renderResolution = null; + /** * Should the app automatically initalize and render a vector graphics frame in the draw call? */ diff --git a/tests/Visual/VisualTestCase.php b/tests/Visual/VisualTestCase.php index f3f4c64..28c5948 100644 --- a/tests/Visual/VisualTestCase.php +++ b/tests/Visual/VisualTestCase.php @@ -4,6 +4,7 @@ use ClanCats\Container\Container; use Closure; +use GL\Math\Vec2; use GL\Texture\Texture2D; use VISU\Quickstart\QuickstartApp; use VISU\Quickstart\QuickstartOptions; @@ -32,9 +33,10 @@ abstract class VisualTestCase extends GLContextTestCase { /** - * The fixed offscreen resolution all visual tests render at (logical points). - * Kept small and 4-byte aligned for deterministic, cheap captures. Note the actual captured - * pixel resolution is this multiplied by the window content scale (e.g. 2x on retina displays). + * The fixed offscreen resolution all visual tests render at (device pixels). + * Kept small and 4-byte aligned for deterministic, cheap captures. This is forced onto the app + * via QuickstartOptions::$renderResolution (content scale 1.0) so the captured pixels are identical + * across machines regardless of display DPI (e.g. a retina Mac and a headless CI runner match). */ protected const VISUAL_WIDTH = 240; protected const VISUAL_HEIGHT = 160; @@ -79,6 +81,10 @@ protected function bootApp() : void $options->windowWidth = static::VISUAL_WIDTH; $options->windowHeight = static::VISUAL_HEIGHT; + // force a fixed device-pixel render resolution (content scale 1.0) so the capture is identical + // across machines, otherwise a retina display would capture at 2x and diverge from CI baselines + $options->renderResolution = new Vec2(static::VISUAL_WIDTH, static::VISUAL_HEIGHT); + $app = new QuickstartApp(new Container(), $options); $app->ready(); diff --git a/tests/resources/visual/expected/flyui_card.png b/tests/resources/visual/expected/flyui_card.png index 46be600ae3672932e09e3b1822e112af6097d5e5..466c06a92248e8b1ed3335877e95dec7545086c7 100644 GIT binary patch literal 3465 zcmeH~`8yO`8^>oe1~VB(S!?V|Dci{2*uvCUD!a`JU_kobS11XJd)rm*58g00^9w z`FZZV#2tk&5I5G>kTtn^JDjbqUb_!#`_ad;_N5t{S&$T!dl{8pWB5Po3HczR}L6U%R522g=eaPcL4j6?+tGhn#VtA+T~ zPy$4h866ulc^AsKMQ%g{_|uvvi7-MUz#0WbpfJquh7$S3G8AzT###&$5DJjADQ4?Q zfSk=Jsi}^9cl|LREzJnJ2`pm#5$jHPF<%r$h?dBbM#Qm&qcHw=vbu`>2^ff&#o1}L zIrn0#1>@L?EySaefuXdLI5tx3g$zqmOp3g*lv?555E=1S#=y5ND=e%-7c1Us1(A%2WJs>7sL0%pN-myf@+ri^Z*b^HolUH%L%>r!f zsLaOdROk436&f3(uAxy?S*fLUp(Ve)J#cZj-t5kk&I0U6{U7LL^1_f2>F?{Xt$~+* zkD7fhUmi=9v$R}gg&Y8mJ6Z4k{&m*PEp~muDCD@?o1nfPt=*k19lMFiwrnKy9>Uu& zinB4&5F3)&TIJplv$He?zQ~`0UKp-VQk7Luh+P~udU3$5F5+o!u1-nsY+o=X5@0RW zE^e%CV-sYI8h#&bQ0thHgDkvMixyG996R>Bp<$$!Bssb}#PmpSXiyUnC_U{>B(7~N zdlmLRbGTYv&5@IRNT;`RuNHm|p3M_F)#>hRZx0o6uO{-TpQTc%l43cpUy;&;;%y5~ zt#UY|kPvpC4(@Cl$X75~Ca1#+hvWU3cjwN+hYx9N)2RML0M6&Hny{(Nj9zT)E~uAB zK5p)|sG13$BJ!Md{-T3}K>s%=|K-~2;{DQb89l8D3mGasX=&XQe#&VQ9{*wahZ1Fu zE6v%n#{g|4>D&!r3F6z^w|SXDE9;9RO12N8A~pbFJYA5B9U)<>pr4iO@%ydZ13yHo zHjxt(q21l*4ak(WDFDvJ#f1=#92jJEyW;U8{oOo=@@i}Kjg0am90RUhyLSAt;mDUS z9Bw`1U&agbgW*>E<^(7}HaJ+VkW3g~`}ONo$f$G|BrQ+F)wKr;fwFb4TUn_y9|Pg> zbV;2gD&%1TkNPIJEoOhs_L0dp<>e1pP@M-X(3;xp-x4Ge!pjSlecY|Fxp{4I#61lp z+D`?L@ju4XC<;c%vWqoPBJAI*GYXR&VE0`2Kv&5;JPy)NOH0={GZ)^HtH~f{cg3@3 zJg;%5qJKl|i_6QyetvOD9)tK&u|?NtBBU~q;qcY4f^5j%*^@4k=VSvJeD1FO`F>WCRbpe+uHnzM9$h8I7ye{ zFWT>2U>GUUg}m5tFCzos`-v?Z$uc8^SVwae0tz@u-x5C|EBm3cQlqFSwYJt`W@aEi zKM({08T-u4JY}8JYPEyf0nLl)^vr|=@sJE~X~g1i^wFb7y&;_OPpd)0bv{8sV5v#K z9ehi(1rX6VNz2H1lpv4|$TchWk0O{6k^>t-S#Mds)upA%u6L2n$)?}lwiazJ|F9&e zoRF6{$05PI_?(;^N`g_9jB`FvWP5F1Qb%W<$WRmmRS(8%*FFisY$A+dv;`F@YjUxS zu|14-0)5z8x#=jx0># zDn^;%11S}P=pEpnyDc+XZ$Gy=#n4M;JPv`(0!q}?as_i3;q$u>^41hFD}Q|>xd*<` zGrf6^Y!tn@Qt^6shwX(OJ@=`l<>RMYVU+Wf0-SR)-{o#C4GlnzlvH7=0*=o&S@$0G zW$Fchof@+a-G1<<5+w0F9|C`+qa&~4^4|R1CQPNsuVWLAf3bZ-dEh)gBXYp=9O0ai z->{#*%FkK};6b0ST;jObaRi*R8e;bZ^*D#;^sYRIybj#^YA}W}mwFgIvr=KKQn+@^ zDfU%(%-rlDVNt)Bl?hw3)ucbxO_I)hw$kczyora_PJ9_Cus)7{AH9cVRPh?_@1#t+ z6QoMC&D+>SXN6LxGC@7l6aEFvL^(-GFQ<3+lGVdTKH3K6qhDbLLSJs&bC8&R>5#RI z#x93_@V<^R9MYRo+kk&LsQ)bhwM9L2Vpo&&TS;8}P_XTZLO$bkgF&jZi}2X^Fjou&Ep0-!)!!cI+yC|bD`1@}J{KN&+>%XoFrB8TzetDq^cfL@Qr`j5hyktz* zY5X<@4DvYO>bSfU2~(pMmj1kF*!xE zAxeA5u}B@Rt)3ywNxHj7;S+VX0NwsuXOsueqH6nudpvB2rIxvk~R9xN|!*T(l0hkhsR?=@Sz{dDjFr-nbavh(vbbaDlCn|T9{ z7=PH=y@_QhYBHY!@LeoblWyejyg@9*ML@T9WplMb5WRbFa&@F;>)rdACL-g=5>m{w ziOfn(scjZ?D&HlOCt&;&QR@rt{-3r4L@NcAz`4N3nTu%b?gf_<0DQY&=T6?qRjtru zaBdw-2tKOr?x2TN6XY`iYSwHYW@ag?2@!*ZAP4HcFjgZm+JcElF_AXQ{2{ATcp=>m zz#MgF8)vUZOHUWlJre(_4DTTYNO~Tp2pIe-fB$80_D7iQPPN;q&zk0DYkg|MP?7gN z0o0t{M~1E`J^B!fb&)$Il^zE{`!>qtt|IYD5=A+p*_-<4h0vtS@0OWD<)S!AD@z+a zy@!SdrYKB^0jCw$CHMKI9L5B!vkrb^c9)7laDGnXpcwkJ$@TcX=^81yCk%=DG|%g* zp3VuZ=}eLR#UaW3aqMf;Y^vr9Q^J0!X*R@&0ktqAFcVosJ|o6SORizV>^pG<4pBl3 z0o13m_JMhyeo`-4AddUA(-gDVNERH(P=rflbxn2TOVT_3mr$Jc?2T|{rc+_9*D@Q5WEG;iz3kw7P$&NT&DFX=5 zLF|q#L>SL+Mk}MIUCVNPh8AOY%6e3KG)E7xeQ<2Fo zi9yPiv3wX=BD*ZVd!+BT=l6P^zn|xi&mZpB>s)i+*L9t9u5-@&eJI|LY+D%4g4f%fKFm>AiHxp;`MkWngPkbfTk*x;XY@J|^0Urz;? zG#)nzso`&gr|H8f0`bTi{|)5}XsN&lW(Mjl4!vc;lF@t$CWbNMixu9Ah`-5w#6 z2XsglBqWAaEoS~aUHYFr$r!-maSv4YaN+3T6SVx_3)Lr3d6dwmr#r~kBbXYxtpWoV zjksIvB(}G=rQ1&I?iO=`T}2I9J(BE$PTpM&un4O+d!kSWd4JH(r)ZdnDHnrg@}JXy zF=kJ!B$=iCGBjXqK*l7AI@r@QJvEiI&p}AZsdI3Ut;)drbIDUaKE8=|*K0R!KnvNjY+PN{ z!)i~u4vh`fM0XWfmc4lK?(f3-`aA00A9+W|^Yd40Yd2`!JA1*j^N1*s9z1yZ@S)RnsgQ{b#a(jASBjUBM# zG;$Nw7^Wb|#|Jx3Dk>_!ys!>LO;{L(`xSMXfjc68uccl2?Uj|1?CfJMF37Ub`ucj@ z%o{ngfTcRPIgvSqO+OP(;FGeu4m8h z5fMP4c$*DkN?Titwo+nfJR}!(ctc=RFq!jc@#Qee{2amQ9rd=Dx~`TlEXj|ZlT%km zXC!5MrazPw7B}$vQg%+x;Kz@R4GkRHfn||DlkePtyR~La$CZW5tT#71G|Nzmi;JtP zB|A2_r46idbgL^XiMf;9IFB=D6A8bub zU{82OsxX>7`+;m(d4DHl;$7G~D(t}c!;sVdm6cYRl@5-*;75gp=Q%3v^z|c&2Z)Fd z--l|j0#JJ+&@pckDbJOsv@|{&DyFN zD5Y2|O;R2LRjJ$R5_6(_rYzs|Y$aTH3XPLP+v{{6=A;o;$rEiG|h0=G7f z=sN>b4Ab+ml`D$`y#ex;n@l}V{25>ny#+4@a!`lxYmix#Ows%y*Z7``Xim=f<3Jja zlLPWDZZVA2mluvi!fyBuP&mHWC746Ald`q^z-6&95Ds}by%dkbvD@_KTbv}2C8$YC z$}UwT-@x^yDPjL4Hvh2Dp5j+NK8~rZdXmexL6P9+#~d9e2VQYCCFfvFA6w-RyIo%T z4G~u}kVdLbCz9f+Do7&jT!@3L)}B57wQ&~8u5WdtHsX?!G;A7L(jL23g>iUd-uuRI z$m;8}P@M69zyAcx!YWG&ck9yA(9ls+yMG&zK$R7t+=Ws&dA>Nr+e=~Uh<7>to0AP% z1k`>6Eq!~@(ZTMl2}x)kKJjm5Wthq;-YS0bgx$F^V*G<69WRnkh)3rUk8zESd0w{= zq?YLWr@6SeEGLSVuY5Q3|Nb>nrm(p9-r>s!=8C!LYUkB%_atyjpFPV*FpCB)1qKB{ zGYnf>TlXJ4C@w1cSDr!8=Mw!zixTQbScj~I1qT#*U(XT7!prs2hgX1nqzfp1sm!eY z+zjn6&*Omc>#rogc){_-fJKe)?Hb;o&Js3TjyqrI?e+vnJBZR=y`b@8@Ah0RjTf&e z+R(N*g;;b{^tP#?>Zmw-gjvYIz^et~BX$2dVVu0bI{F7jvy@P9I-KPcoDOl~)3&#$ zdQ=Zi$9X^(Mq=Yyy&e4>(5zXj^m4KCAnX!0Kv!GFgsRAnNzE>d4DGi2Wp@ z9Gxl{=jm``pMt6S#IMQ^wzL%k%{s-yO+X3Akndl)x}o^xAM^9PWKl|(oTer(O9mJ>8Z!+e@5&_rkYL2Oy4D3h*3}HgZB{{d?ZL7K9J^u1G5q@Id6 zEG9rPf>M&iDG7hi4a?oTr=#UN5HnpITLhamhm~nv>()M2@5~t!2vA=+br-Zw4;Cy_zRM{9R^eE#?mn3C`pXeVt#M_L~tBMKH z;~)-~;(Ix-tzkMlI=cJ%q-t4wl?UF08U|u`r3I+#jg5`qv&pw^Il8;IFrr=qb>%Ej zS4Mm*Sd!Fx2p7UOHzQG<7}lP0hQAMzlM^(n0M0FhPgg=;%0MPqiP$fVji%ySB?tt< zw1>s{(o3JN*O{3L^jbJcyEL_|R3%UIqME-bN7gX)C%6(1mI`s_!dFAsnyl|@a}20J z4#$Nz+}O)gz4%u&bt$Inu{CS%Abdar>YNg`y#?jqm@h3&1;B)!j*iTmxK$D;c^e44 zy{f7%wzdM4C%lx}EO!9_-!&t_jSm=+=4{dC6p1gJY$cB4V`DbQkH2%Rhw8H^HeJ1X zmHwtK;4+W)7T4iaeuq2Ic9GiX+I^BS>joep%dFor@OHs3NXq;K ze8Sj`iCX~__TPY50LlJ|apR5H6OjRl@}oP80grqpDoT-W`zjaGOXc57 zGX>~x0mub^VqAa%nI>qPrp^BfS$23QGeeoBjql1#*#8IECfwl>xoDJ9knwwIrtlxN z0Lb?Tba&^0kg*cvmatvm&g!$nA}oHh2tY1PY=oEaXb1e5c-mj%H?$QwwVe0aZ!4L# zn+^vb=k?OC6SA{ArD1o9D`=U96GIS(1t8>?;U3r9`a`3vv**`F9=VczB5_vB)uC3r5?bNnL=YJDCoR zHyvWQyc^C)GaC)A2$uS=mFqZ?!bCGo|6F*VBKwoutfIc}Eyse>@soQen=5G$>ST%Z2lmR!lY{Vxrlq&f6C6}KhD@UR_esssaz%>tt6I1B$AY*4I(h;CyovxEUOP#V^Z*7|~ zwye`Vo{Q;a>Fm%?)@b&`WTUPF$&^>3di}6l$0no=x3<@>DtIg{eewEB@YLR5&GD9# zhTb2xA~wd_E0ejEU6ZAb={prN89yh}@|LpV+GVsJ#U~Hp`k7%<%ipK8A|mJJxNQbL z#0@rP)wSG$Y7d~~-#cfvXWacMlp;q^V%QbcKxnwPzgVLWGu*oNG!mN{r@oaMCC<)l zvn!HNczWrNcXfsY#)NW|GPf0p6t8=*RH7zmMe9(gf1uiXb1Zpswt8s0)Kb;h^feJH z+Mc@!ShU^n;bYX^y_u+!8JK=){jG(Ceal7y6!ECdt?fA!j(-2{U0!WJ4%_Ip_sU~N zB3@psN|u^PWMlb6)JQ06$6t~cE3Nak!Q8wXdVQ)QO5tW$&}n8|bz2KlM>>0N@t>WY zR5u)cq+VtTd{X(sYvOMfgz8#YfK_Hn8BG-w^49`ARv@2sZEBbU38#aO zIgr-1{I-@YH(-a;*%_Gwcmv^mNb~F51AEGXKCdKY*HlN|JaGmyfcJTa3|22S1!sYZ zD@x96>xW|)IIxBoB$H{|lZJMg_sW6-jP|XKD{aJlib;{)w(EJDk;Za$d&O_ea!b|o z(E=;m16N7ey$#MzAGq;%t#g+-QicS^9G$XYGLIHsE!60~HrU!%D=0CYNG;PaNKfF^ zyuM$y);YB)BXvVwfpCzT!sRPBS4hoNkfOg&8;E(?Y&kjomjUUhX9Acj+(WK1_t3kt z_bb#TCGx9aX2t@VKtNn9@yAw4plzeKx1y(nJ{HWsxKKm9hMywKy&8VBaLdEcDsOA= zp87GjCX_OFnZ62PYJ6tsO-OXpd4tl$X&nC;%d=M2R<^F^5wLZcmCz=1mN-m65c3r87W=a5$7X+MzE0r2 zPR8=r$f02_HsTuz)Fbtd6K<$0-=%{2ljC5)q7?ZLZSAfb`oSX4_ws3j%6z!Gxv2UY z$}%F)6Wn5(nG<-7X%*>+9Dn(`i)SnAsV{&Ol5@5qKnkCM6@KENeQOet$?u7=Ww zJcS`e2)9p=;y~Lwjs*PJrxEhyOTc*w+zHZ@&CpWK{rPd99znF5NQd1u?vu$0h0Oh+=oO zEb*O8-eK$(d^>gB0`f&(KSDs4gY_^k2jv#5E4e<*#e5Qer>|^A?_bH30H`iINP>ru z_Y9^A)4m3FHUbYA%}##g$TN@29-?jXQXA5@aKA9> z6m+nqp+$SQbO7ZwJfGNd(g2&T3c8uwBmXWRCItrDoN2DF2jSNsS}}Di!5}tFiSy=> z!fe9n?=ska>TM*&8df4nIUq{mhf$1sKWDer~1mpY76PH1s&HTQ~7(^$VTM0>z;*9j2jsieLkqNJGw}lL%*X&)EkYPOv zrPI)KZL0ny%;L9Cu5=1p1g})bS$Y&>ypr4VgDgY?lF{{d>}Q$&-ay67gMniVYkKMR zY9+Ra$>zSLA^$amHl(X_%e`-pyPG3Nx7`_3s_Wu1caon2OL%nOhxT4k(xZ-+>s4bS6iK?R7N^MShQJ zat5*`bmsLlE@AA-TWIKe(7~s*hYi@SAkB*|FJ(P5OWBlAGTVW2(@rzam&Y??aNym%5=?n z+WnmqPLP0L1F7E)5wG7_`6}ab)Cdz+pr@epKHHb$gWoXz&32Qo@@n-`hLhCt$P;BUZgD zrgxYp6@XM89pn~|{{i_j+ueV(nK4IybV_Bnv;vJ%mtDwgx;zv1OPA^;}bqu&jr z(`!(%*z<+tgUE!_AyU97Bpjd|@;(^5yO+=!+`r#+!f>adNt#X=b$AT`&mZYInQ+hW zh)9kQ8l}ffZqmjMu#zDtZ@`Kj24418AghD z1K;(~*^OMy(S@tWYE08sJ2=7gS6`^N5T9t33%J?_AqaM!LIZ%cx0Au2Y(SbWc#`ZB zIQv73xe+