From a885b89fa40d16264475ce41b71b021f9e6526f3 Mon Sep 17 00:00:00 2001 From: Karel Petranek Date: Mon, 5 Sep 2016 20:52:13 -0500 Subject: [PATCH 01/37] Mavenize project, add gitignore, remove old build --- .classpath | 7 - .externalToolBuilders/Ant Build.launch | 20 - .project | 27 - .settings/org.eclipse.jdt.core.prefs | 258 ---- .settings/org.eclipse.jdt.ui.prefs | 6 - .../org.eclipse.ltk.core.refactoring.prefs | 3 - janino.jar | Bin 416460 -> 0 bytes src/SunflowGUI.java | 1252 ---------------- src/org/sunflow/AsciiFileSunflowAPI.java | 93 -- src/org/sunflow/Benchmark.java | 280 ---- src/org/sunflow/BinaryFileSunflowAPI.java | 225 --- src/org/sunflow/FileSunflowAPI.java | 315 ---- src/org/sunflow/PluginRegistry.java | 322 ----- src/org/sunflow/RealtimeBenchmark.java | 123 -- src/org/sunflow/RenderObjectMap.java | 333 ----- src/org/sunflow/SunflowAPI.java | 700 --------- src/org/sunflow/SunflowAPIInterface.java | 275 ---- .../sunflow/core/AccelerationStructure.java | 19 - .../core/AccelerationStructureFactory.java | 33 - src/org/sunflow/core/BucketOrder.java | 20 - src/org/sunflow/core/Camera.java | 120 -- src/org/sunflow/core/CameraLens.java | 28 - .../core/CausticPhotonMapInterface.java | 14 - src/org/sunflow/core/Display.java | 77 - src/org/sunflow/core/Filter.java | 26 - src/org/sunflow/core/GIEngine.java | 41 - src/org/sunflow/core/Geometry.java | 144 -- .../core/GlobalPhotonMapInterface.java | 21 - src/org/sunflow/core/ImageSampler.java | 25 - src/org/sunflow/core/Instance.java | 213 --- src/org/sunflow/core/InstanceList.java | 71 - src/org/sunflow/core/IntersectionState.java | 116 -- src/org/sunflow/core/LightSample.java | 103 -- src/org/sunflow/core/LightServer.java | 360 ----- src/org/sunflow/core/LightSource.java | 66 - src/org/sunflow/core/Modifier.java | 16 - src/org/sunflow/core/Options.java | 18 - src/org/sunflow/core/ParameterList.java | 729 ---------- src/org/sunflow/core/PhotonStore.java | 62 - src/org/sunflow/core/PrimitiveList.java | 69 - src/org/sunflow/core/Ray.java | 217 --- src/org/sunflow/core/RenderObject.java | 24 - src/org/sunflow/core/Scene.java | 371 ----- src/org/sunflow/core/SceneParser.java | 20 - src/org/sunflow/core/Shader.java | 29 - src/org/sunflow/core/ShadingCache.java | 75 - src/org/sunflow/core/ShadingState.java | 929 ------------ src/org/sunflow/core/Statistics.java | 83 -- src/org/sunflow/core/Tesselatable.java | 35 - src/org/sunflow/core/Texture.java | 123 -- src/org/sunflow/core/TextureCache.java | 47 - .../core/accel/BoundingIntervalHierarchy.java | 613 -------- src/org/sunflow/core/accel/KDTree.java | 800 ---------- .../sunflow/core/accel/NullAccelerator.java | 26 - src/org/sunflow/core/accel/UniformGrid.java | 294 ---- .../core/bucket/BucketOrderFactory.java | 25 - .../core/bucket/ColumnBucketOrder.java | 18 - .../core/bucket/DiagonalBucketOrder.java | 26 - .../core/bucket/HilbertBucketOrder.java | 62 - .../core/bucket/InvertedBucketOrder.java | 26 - .../core/bucket/RandomBucketOrder.java | 46 - .../sunflow/core/bucket/RowBucketOrder.java | 18 - .../core/bucket/SpiralBucketOrder.java | 41 - src/org/sunflow/core/camera/FisheyeLens.java | 21 - src/org/sunflow/core/camera/PinholeLens.java | 40 - .../sunflow/core/camera/SphericalLens.java | 19 - src/org/sunflow/core/camera/ThinLens.java | 103 -- src/org/sunflow/core/display/FastDisplay.java | 107 -- src/org/sunflow/core/display/FileDisplay.java | 73 - .../sunflow/core/display/FrameDisplay.java | 85 -- .../sunflow/core/display/ImgPipeDisplay.java | 101 -- .../core/filter/BlackmanHarrisFilter.java | 24 - src/org/sunflow/core/filter/BoxFilter.java | 13 - .../sunflow/core/filter/CatmullRomFilter.java | 24 - src/org/sunflow/core/filter/CubicBSpline.java | 28 - .../sunflow/core/filter/GaussianFilter.java | 21 - .../sunflow/core/filter/LanczosFilter.java | 26 - .../sunflow/core/filter/MitchellFilter.java | 24 - src/org/sunflow/core/filter/SincFilter.java | 21 - .../sunflow/core/filter/TriangleFilter.java | 13 - .../core/gi/AmbientOcclusionGIEngine.java | 53 - src/org/sunflow/core/gi/FakeGIEngine.java | 43 - src/org/sunflow/core/gi/InstantGI.java | 176 --- .../core/gi/IrradianceCacheGIEngine.java | 246 ---- .../sunflow/core/gi/PathTracingGIEngine.java | 59 - .../core/light/DirectionalSpotlight.java | 96 -- .../sunflow/core/light/ImageBasedLight.java | 275 ---- src/org/sunflow/core/light/PointLight.java | 65 - src/org/sunflow/core/light/SphereLight.java | 153 -- src/org/sunflow/core/light/SunSkyLight.java | 337 ----- .../sunflow/core/light/TriangleMeshLight.java | 272 ---- .../core/modifiers/BumpMappingModifier.java | 33 - .../core/modifiers/NormalMapModifier.java | 30 - .../core/modifiers/PerlinModifier.java | 76 - src/org/sunflow/core/parser/RA2Parser.java | 100 -- src/org/sunflow/core/parser/RA3Parser.java | 61 - .../sunflow/core/parser/SCAbstractParser.java | 294 ---- .../sunflow/core/parser/SCAsciiParser.java | 213 --- .../sunflow/core/parser/SCBinaryParser.java | 154 -- src/org/sunflow/core/parser/SCParser.java | 1284 ----------------- .../sunflow/core/parser/ShaveRibParser.java | 165 --- src/org/sunflow/core/parser/TriParser.java | 73 - .../core/photonmap/CausticPhotonMap.java | 401 ----- .../core/photonmap/GlobalPhotonMap.java | 498 ------- .../sunflow/core/photonmap/GridPhotonMap.java | 282 ---- .../sunflow/core/primitive/Background.java | 45 - .../core/primitive/BanchoffSurface.java | 90 -- src/org/sunflow/core/primitive/Box.java | 197 --- .../sunflow/core/primitive/CornellBox.java | 446 ------ src/org/sunflow/core/primitive/CubeGrid.java | 288 ---- src/org/sunflow/core/primitive/Cylinder.java | 93 -- src/org/sunflow/core/primitive/Hair.java | 261 ---- .../sunflow/core/primitive/JuliaFractal.java | 253 ---- .../core/primitive/ParticleSurface.java | 102 -- src/org/sunflow/core/primitive/Plane.java | 153 -- src/org/sunflow/core/primitive/QuadMesh.java | 392 ----- src/org/sunflow/core/primitive/Sphere.java | 89 -- .../sunflow/core/primitive/SphereFlake.java | 219 --- src/org/sunflow/core/primitive/Torus.java | 134 -- .../sunflow/core/primitive/TriangleMesh.java | 784 ---------- .../sunflow/core/renderer/BucketRenderer.java | 469 ------ .../core/renderer/MultipassRenderer.java | 226 --- .../core/renderer/ProgressiveRenderer.java | 158 -- .../sunflow/core/renderer/SimpleRenderer.java | 101 -- .../core/shader/AmbientOcclusionShader.java | 48 - .../core/shader/AnisotropicWardShader.java | 211 --- .../sunflow/core/shader/ConstantShader.java | 27 - .../sunflow/core/shader/DiffuseShader.java | 61 - src/org/sunflow/core/shader/GlassShader.java | 139 -- src/org/sunflow/core/shader/IDShader.java | 23 - src/org/sunflow/core/shader/MirrorShader.java | 62 - src/org/sunflow/core/shader/NormalShader.java | 27 - src/org/sunflow/core/shader/PhongShader.java | 85 -- src/org/sunflow/core/shader/PrimIDShader.java | 26 - .../sunflow/core/shader/QuickGrayShader.java | 59 - .../core/shader/ShinyDiffuseShader.java | 93 -- src/org/sunflow/core/shader/SimpleShader.java | 20 - .../TexturedAmbientOcclusionShader.java | 29 - .../core/shader/TexturedDiffuseShader.java | 29 - .../core/shader/TexturedPhongShader.java | 29 - .../shader/TexturedShinyDiffuseShader.java | 29 - .../core/shader/TexturedWardShader.java | 29 - src/org/sunflow/core/shader/UVShader.java | 22 - src/org/sunflow/core/shader/UberShader.java | 141 -- .../core/shader/ViewCausticsShader.java | 28 - .../core/shader/ViewGlobalPhotonsShader.java | 21 - .../core/shader/ViewIrradianceShader.java | 21 - .../sunflow/core/shader/WireframeShader.java | 76 - .../sunflow/core/tesselatable/BezierMesh.java | 248 ---- .../sunflow/core/tesselatable/FileMesh.java | 237 --- src/org/sunflow/core/tesselatable/Gumbo.java | 1144 --------------- src/org/sunflow/core/tesselatable/Teapot.java | 236 --- src/org/sunflow/image/Bitmap.java | 14 - src/org/sunflow/image/BitmapReader.java | 35 - src/org/sunflow/image/BitmapWriter.java | 78 - src/org/sunflow/image/BlackbodySpectrum.java | 15 - .../sunflow/image/ChromaticitySpectrum.java | 59 - src/org/sunflow/image/Color.java | 370 ----- src/org/sunflow/image/ColorEncoder.java | 90 -- src/org/sunflow/image/ColorFactory.java | 112 -- .../sunflow/image/ConstantSpectralCurve.java | 20 - .../sunflow/image/IrregularSpectralCurve.java | 50 - src/org/sunflow/image/RGBSpace.java | 199 --- .../sunflow/image/RegularSpectralCurve.java | 28 - src/org/sunflow/image/SpectralCurve.java | 119 -- src/org/sunflow/image/XYZColor.java | 48 - .../sunflow/image/formats/BitmapBlack.java | 26 - src/org/sunflow/image/formats/BitmapG8.java | 35 - src/org/sunflow/image/formats/BitmapGA8.java | 29 - src/org/sunflow/image/formats/BitmapRGB8.java | 39 - .../sunflow/image/formats/BitmapRGBA8.java | 39 - src/org/sunflow/image/formats/BitmapRGBE.java | 56 - src/org/sunflow/image/formats/BitmapXYZ.java | 37 - .../sunflow/image/formats/GenericBitmap.java | 71 - .../image/readers/BMPBitmapReader.java | 38 - .../image/readers/HDRBitmapReader.java | 148 -- .../image/readers/IGIBitmapReader.java | 94 -- .../image/readers/JPGBitmapReader.java | 38 - .../image/readers/PNGBitmapReader.java | 39 - .../image/readers/TGABitmapReader.java | 131 -- .../image/writers/EXRBitmapWriter.java | 377 ----- .../image/writers/HDRBitmapWriter.java | 51 - .../image/writers/IGIBitmapWriter.java | 73 - .../image/writers/PNGBitmapWriter.java | 36 - .../image/writers/TGABitmapWriter.java | 62 - src/org/sunflow/math/BoundingBox.java | 315 ---- src/org/sunflow/math/MathUtils.java | 131 -- src/org/sunflow/math/Matrix4.java | 561 ------- src/org/sunflow/math/MovingMatrix4.java | 115 -- src/org/sunflow/math/OrthoNormalBasis.java | 109 -- src/org/sunflow/math/PerlinScalar.java | 330 ----- src/org/sunflow/math/PerlinVector.java | 131 -- src/org/sunflow/math/Point2.java | 35 - src/org/sunflow/math/Point3.java | 132 -- src/org/sunflow/math/QMC.java | 194 --- src/org/sunflow/math/Solvers.java | 137 -- src/org/sunflow/math/Vector3.java | 195 --- .../sunflow/system/BenchmarkFramework.java | 67 - src/org/sunflow/system/BenchmarkTest.java | 17 - src/org/sunflow/system/ByteUtil.java | 118 -- src/org/sunflow/system/FileUtils.java | 24 - src/org/sunflow/system/ImagePanel.java | 262 ---- src/org/sunflow/system/Memory.java | 15 - src/org/sunflow/system/Parser.java | 155 -- src/org/sunflow/system/Plugins.java | 153 -- .../sunflow/system/RenderGlobalsPanel.java | 207 --- src/org/sunflow/system/SearchPath.java | 61 - src/org/sunflow/system/Timer.java | 51 - src/org/sunflow/system/UI.java | 103 -- src/org/sunflow/system/UserInterface.java | 45 - .../sunflow/system/ui/ConsoleInterface.java | 42 - .../sunflow/system/ui/SilentInterface.java | 23 - src/org/sunflow/util/FastHashMap.java | 206 --- src/org/sunflow/util/FloatArray.java | 76 - src/org/sunflow/util/IntArray.java | 76 - 215 files changed, 31202 deletions(-) delete mode 100644 .classpath delete mode 100644 .externalToolBuilders/Ant Build.launch delete mode 100644 .project delete mode 100644 .settings/org.eclipse.jdt.core.prefs delete mode 100644 .settings/org.eclipse.jdt.ui.prefs delete mode 100644 .settings/org.eclipse.ltk.core.refactoring.prefs delete mode 100644 janino.jar delete mode 100644 src/SunflowGUI.java delete mode 100644 src/org/sunflow/AsciiFileSunflowAPI.java delete mode 100644 src/org/sunflow/Benchmark.java delete mode 100644 src/org/sunflow/BinaryFileSunflowAPI.java delete mode 100644 src/org/sunflow/FileSunflowAPI.java delete mode 100644 src/org/sunflow/PluginRegistry.java delete mode 100644 src/org/sunflow/RealtimeBenchmark.java delete mode 100644 src/org/sunflow/RenderObjectMap.java delete mode 100644 src/org/sunflow/SunflowAPI.java delete mode 100644 src/org/sunflow/SunflowAPIInterface.java delete mode 100644 src/org/sunflow/core/AccelerationStructure.java delete mode 100644 src/org/sunflow/core/AccelerationStructureFactory.java delete mode 100644 src/org/sunflow/core/BucketOrder.java delete mode 100644 src/org/sunflow/core/Camera.java delete mode 100644 src/org/sunflow/core/CameraLens.java delete mode 100644 src/org/sunflow/core/CausticPhotonMapInterface.java delete mode 100644 src/org/sunflow/core/Display.java delete mode 100644 src/org/sunflow/core/Filter.java delete mode 100644 src/org/sunflow/core/GIEngine.java delete mode 100644 src/org/sunflow/core/Geometry.java delete mode 100644 src/org/sunflow/core/GlobalPhotonMapInterface.java delete mode 100644 src/org/sunflow/core/ImageSampler.java delete mode 100644 src/org/sunflow/core/Instance.java delete mode 100644 src/org/sunflow/core/InstanceList.java delete mode 100644 src/org/sunflow/core/IntersectionState.java delete mode 100644 src/org/sunflow/core/LightSample.java delete mode 100644 src/org/sunflow/core/LightServer.java delete mode 100644 src/org/sunflow/core/LightSource.java delete mode 100644 src/org/sunflow/core/Modifier.java delete mode 100644 src/org/sunflow/core/Options.java delete mode 100644 src/org/sunflow/core/ParameterList.java delete mode 100644 src/org/sunflow/core/PhotonStore.java delete mode 100644 src/org/sunflow/core/PrimitiveList.java delete mode 100644 src/org/sunflow/core/Ray.java delete mode 100644 src/org/sunflow/core/RenderObject.java delete mode 100644 src/org/sunflow/core/Scene.java delete mode 100644 src/org/sunflow/core/SceneParser.java delete mode 100644 src/org/sunflow/core/Shader.java delete mode 100644 src/org/sunflow/core/ShadingCache.java delete mode 100644 src/org/sunflow/core/ShadingState.java delete mode 100644 src/org/sunflow/core/Statistics.java delete mode 100644 src/org/sunflow/core/Tesselatable.java delete mode 100644 src/org/sunflow/core/Texture.java delete mode 100644 src/org/sunflow/core/TextureCache.java delete mode 100644 src/org/sunflow/core/accel/BoundingIntervalHierarchy.java delete mode 100644 src/org/sunflow/core/accel/KDTree.java delete mode 100644 src/org/sunflow/core/accel/NullAccelerator.java delete mode 100644 src/org/sunflow/core/accel/UniformGrid.java delete mode 100644 src/org/sunflow/core/bucket/BucketOrderFactory.java delete mode 100644 src/org/sunflow/core/bucket/ColumnBucketOrder.java delete mode 100644 src/org/sunflow/core/bucket/DiagonalBucketOrder.java delete mode 100644 src/org/sunflow/core/bucket/HilbertBucketOrder.java delete mode 100644 src/org/sunflow/core/bucket/InvertedBucketOrder.java delete mode 100644 src/org/sunflow/core/bucket/RandomBucketOrder.java delete mode 100644 src/org/sunflow/core/bucket/RowBucketOrder.java delete mode 100644 src/org/sunflow/core/bucket/SpiralBucketOrder.java delete mode 100644 src/org/sunflow/core/camera/FisheyeLens.java delete mode 100644 src/org/sunflow/core/camera/PinholeLens.java delete mode 100644 src/org/sunflow/core/camera/SphericalLens.java delete mode 100644 src/org/sunflow/core/camera/ThinLens.java delete mode 100644 src/org/sunflow/core/display/FastDisplay.java delete mode 100644 src/org/sunflow/core/display/FileDisplay.java delete mode 100644 src/org/sunflow/core/display/FrameDisplay.java delete mode 100644 src/org/sunflow/core/display/ImgPipeDisplay.java delete mode 100644 src/org/sunflow/core/filter/BlackmanHarrisFilter.java delete mode 100644 src/org/sunflow/core/filter/BoxFilter.java delete mode 100644 src/org/sunflow/core/filter/CatmullRomFilter.java delete mode 100644 src/org/sunflow/core/filter/CubicBSpline.java delete mode 100644 src/org/sunflow/core/filter/GaussianFilter.java delete mode 100644 src/org/sunflow/core/filter/LanczosFilter.java delete mode 100644 src/org/sunflow/core/filter/MitchellFilter.java delete mode 100644 src/org/sunflow/core/filter/SincFilter.java delete mode 100644 src/org/sunflow/core/filter/TriangleFilter.java delete mode 100644 src/org/sunflow/core/gi/AmbientOcclusionGIEngine.java delete mode 100644 src/org/sunflow/core/gi/FakeGIEngine.java delete mode 100644 src/org/sunflow/core/gi/InstantGI.java delete mode 100644 src/org/sunflow/core/gi/IrradianceCacheGIEngine.java delete mode 100644 src/org/sunflow/core/gi/PathTracingGIEngine.java delete mode 100644 src/org/sunflow/core/light/DirectionalSpotlight.java delete mode 100644 src/org/sunflow/core/light/ImageBasedLight.java delete mode 100644 src/org/sunflow/core/light/PointLight.java delete mode 100644 src/org/sunflow/core/light/SphereLight.java delete mode 100644 src/org/sunflow/core/light/SunSkyLight.java delete mode 100644 src/org/sunflow/core/light/TriangleMeshLight.java delete mode 100644 src/org/sunflow/core/modifiers/BumpMappingModifier.java delete mode 100644 src/org/sunflow/core/modifiers/NormalMapModifier.java delete mode 100644 src/org/sunflow/core/modifiers/PerlinModifier.java delete mode 100644 src/org/sunflow/core/parser/RA2Parser.java delete mode 100644 src/org/sunflow/core/parser/RA3Parser.java delete mode 100644 src/org/sunflow/core/parser/SCAbstractParser.java delete mode 100644 src/org/sunflow/core/parser/SCAsciiParser.java delete mode 100644 src/org/sunflow/core/parser/SCBinaryParser.java delete mode 100644 src/org/sunflow/core/parser/SCParser.java delete mode 100644 src/org/sunflow/core/parser/ShaveRibParser.java delete mode 100644 src/org/sunflow/core/parser/TriParser.java delete mode 100644 src/org/sunflow/core/photonmap/CausticPhotonMap.java delete mode 100644 src/org/sunflow/core/photonmap/GlobalPhotonMap.java delete mode 100644 src/org/sunflow/core/photonmap/GridPhotonMap.java delete mode 100644 src/org/sunflow/core/primitive/Background.java delete mode 100644 src/org/sunflow/core/primitive/BanchoffSurface.java delete mode 100644 src/org/sunflow/core/primitive/Box.java delete mode 100644 src/org/sunflow/core/primitive/CornellBox.java delete mode 100644 src/org/sunflow/core/primitive/CubeGrid.java delete mode 100644 src/org/sunflow/core/primitive/Cylinder.java delete mode 100644 src/org/sunflow/core/primitive/Hair.java delete mode 100644 src/org/sunflow/core/primitive/JuliaFractal.java delete mode 100644 src/org/sunflow/core/primitive/ParticleSurface.java delete mode 100644 src/org/sunflow/core/primitive/Plane.java delete mode 100644 src/org/sunflow/core/primitive/QuadMesh.java delete mode 100644 src/org/sunflow/core/primitive/Sphere.java delete mode 100644 src/org/sunflow/core/primitive/SphereFlake.java delete mode 100644 src/org/sunflow/core/primitive/Torus.java delete mode 100644 src/org/sunflow/core/primitive/TriangleMesh.java delete mode 100644 src/org/sunflow/core/renderer/BucketRenderer.java delete mode 100644 src/org/sunflow/core/renderer/MultipassRenderer.java delete mode 100644 src/org/sunflow/core/renderer/ProgressiveRenderer.java delete mode 100644 src/org/sunflow/core/renderer/SimpleRenderer.java delete mode 100644 src/org/sunflow/core/shader/AmbientOcclusionShader.java delete mode 100644 src/org/sunflow/core/shader/AnisotropicWardShader.java delete mode 100644 src/org/sunflow/core/shader/ConstantShader.java delete mode 100644 src/org/sunflow/core/shader/DiffuseShader.java delete mode 100644 src/org/sunflow/core/shader/GlassShader.java delete mode 100644 src/org/sunflow/core/shader/IDShader.java delete mode 100644 src/org/sunflow/core/shader/MirrorShader.java delete mode 100644 src/org/sunflow/core/shader/NormalShader.java delete mode 100644 src/org/sunflow/core/shader/PhongShader.java delete mode 100644 src/org/sunflow/core/shader/PrimIDShader.java delete mode 100644 src/org/sunflow/core/shader/QuickGrayShader.java delete mode 100644 src/org/sunflow/core/shader/ShinyDiffuseShader.java delete mode 100644 src/org/sunflow/core/shader/SimpleShader.java delete mode 100644 src/org/sunflow/core/shader/TexturedAmbientOcclusionShader.java delete mode 100644 src/org/sunflow/core/shader/TexturedDiffuseShader.java delete mode 100644 src/org/sunflow/core/shader/TexturedPhongShader.java delete mode 100644 src/org/sunflow/core/shader/TexturedShinyDiffuseShader.java delete mode 100644 src/org/sunflow/core/shader/TexturedWardShader.java delete mode 100644 src/org/sunflow/core/shader/UVShader.java delete mode 100644 src/org/sunflow/core/shader/UberShader.java delete mode 100644 src/org/sunflow/core/shader/ViewCausticsShader.java delete mode 100644 src/org/sunflow/core/shader/ViewGlobalPhotonsShader.java delete mode 100644 src/org/sunflow/core/shader/ViewIrradianceShader.java delete mode 100644 src/org/sunflow/core/shader/WireframeShader.java delete mode 100644 src/org/sunflow/core/tesselatable/BezierMesh.java delete mode 100644 src/org/sunflow/core/tesselatable/FileMesh.java delete mode 100644 src/org/sunflow/core/tesselatable/Gumbo.java delete mode 100644 src/org/sunflow/core/tesselatable/Teapot.java delete mode 100644 src/org/sunflow/image/Bitmap.java delete mode 100644 src/org/sunflow/image/BitmapReader.java delete mode 100644 src/org/sunflow/image/BitmapWriter.java delete mode 100644 src/org/sunflow/image/BlackbodySpectrum.java delete mode 100644 src/org/sunflow/image/ChromaticitySpectrum.java delete mode 100644 src/org/sunflow/image/Color.java delete mode 100644 src/org/sunflow/image/ColorEncoder.java delete mode 100644 src/org/sunflow/image/ColorFactory.java delete mode 100644 src/org/sunflow/image/ConstantSpectralCurve.java delete mode 100644 src/org/sunflow/image/IrregularSpectralCurve.java delete mode 100644 src/org/sunflow/image/RGBSpace.java delete mode 100644 src/org/sunflow/image/RegularSpectralCurve.java delete mode 100644 src/org/sunflow/image/SpectralCurve.java delete mode 100644 src/org/sunflow/image/XYZColor.java delete mode 100644 src/org/sunflow/image/formats/BitmapBlack.java delete mode 100644 src/org/sunflow/image/formats/BitmapG8.java delete mode 100644 src/org/sunflow/image/formats/BitmapGA8.java delete mode 100644 src/org/sunflow/image/formats/BitmapRGB8.java delete mode 100644 src/org/sunflow/image/formats/BitmapRGBA8.java delete mode 100644 src/org/sunflow/image/formats/BitmapRGBE.java delete mode 100644 src/org/sunflow/image/formats/BitmapXYZ.java delete mode 100644 src/org/sunflow/image/formats/GenericBitmap.java delete mode 100644 src/org/sunflow/image/readers/BMPBitmapReader.java delete mode 100644 src/org/sunflow/image/readers/HDRBitmapReader.java delete mode 100644 src/org/sunflow/image/readers/IGIBitmapReader.java delete mode 100644 src/org/sunflow/image/readers/JPGBitmapReader.java delete mode 100644 src/org/sunflow/image/readers/PNGBitmapReader.java delete mode 100644 src/org/sunflow/image/readers/TGABitmapReader.java delete mode 100644 src/org/sunflow/image/writers/EXRBitmapWriter.java delete mode 100644 src/org/sunflow/image/writers/HDRBitmapWriter.java delete mode 100644 src/org/sunflow/image/writers/IGIBitmapWriter.java delete mode 100644 src/org/sunflow/image/writers/PNGBitmapWriter.java delete mode 100644 src/org/sunflow/image/writers/TGABitmapWriter.java delete mode 100644 src/org/sunflow/math/BoundingBox.java delete mode 100644 src/org/sunflow/math/MathUtils.java delete mode 100644 src/org/sunflow/math/Matrix4.java delete mode 100644 src/org/sunflow/math/MovingMatrix4.java delete mode 100644 src/org/sunflow/math/OrthoNormalBasis.java delete mode 100644 src/org/sunflow/math/PerlinScalar.java delete mode 100644 src/org/sunflow/math/PerlinVector.java delete mode 100644 src/org/sunflow/math/Point2.java delete mode 100644 src/org/sunflow/math/Point3.java delete mode 100644 src/org/sunflow/math/QMC.java delete mode 100644 src/org/sunflow/math/Solvers.java delete mode 100644 src/org/sunflow/math/Vector3.java delete mode 100644 src/org/sunflow/system/BenchmarkFramework.java delete mode 100644 src/org/sunflow/system/BenchmarkTest.java delete mode 100644 src/org/sunflow/system/ByteUtil.java delete mode 100644 src/org/sunflow/system/FileUtils.java delete mode 100644 src/org/sunflow/system/ImagePanel.java delete mode 100644 src/org/sunflow/system/Memory.java delete mode 100644 src/org/sunflow/system/Parser.java delete mode 100644 src/org/sunflow/system/Plugins.java delete mode 100644 src/org/sunflow/system/RenderGlobalsPanel.java delete mode 100644 src/org/sunflow/system/SearchPath.java delete mode 100644 src/org/sunflow/system/Timer.java delete mode 100644 src/org/sunflow/system/UI.java delete mode 100644 src/org/sunflow/system/UserInterface.java delete mode 100644 src/org/sunflow/system/ui/ConsoleInterface.java delete mode 100644 src/org/sunflow/system/ui/SilentInterface.java delete mode 100644 src/org/sunflow/util/FastHashMap.java delete mode 100644 src/org/sunflow/util/FloatArray.java delete mode 100644 src/org/sunflow/util/IntArray.java diff --git a/.classpath b/.classpath deleted file mode 100644 index a3a7486..0000000 --- a/.classpath +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/.externalToolBuilders/Ant Build.launch b/.externalToolBuilders/Ant Build.launch deleted file mode 100644 index 41ca506..0000000 --- a/.externalToolBuilders/Ant Build.launch +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/.project b/.project deleted file mode 100644 index 2615127..0000000 --- a/.project +++ /dev/null @@ -1,27 +0,0 @@ - - - sunflow - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.ui.externaltools.ExternalToolBuilder - full,incremental, - - - LaunchConfigHandle - <project>/.externalToolBuilders/Ant Build.launch - - - - - - org.eclipse.jdt.core.javanature - - diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 9cdf284..0000000 --- a/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,258 +0,0 @@ -#Sun Nov 19 15:00:49 CST 2006 -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.5 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.5 -org.eclipse.jdt.core.formatter.align_type_members_on_columns=false -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=0 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=0 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=0 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=0 -org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=0 -org.eclipse.jdt.core.formatter.alignment_for_assignment=0 -org.eclipse.jdt.core.formatter.alignment_for_binary_expression=0 -org.eclipse.jdt.core.formatter.alignment_for_compact_if=0 -org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=0 -org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 -org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 -org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=0 -org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=0 -org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=0 -org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=0 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=0 -org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=0 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=0 -org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=0 -org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_after_package=1 -org.eclipse.jdt.core.formatter.blank_lines_before_field=0 -org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 -org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 -org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 -org.eclipse.jdt.core.formatter.blank_lines_before_method=1 -org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=0 -org.eclipse.jdt.core.formatter.blank_lines_before_package=0 -org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 -org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line -org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line -org.eclipse.jdt.core.formatter.comment.clear_blank_lines=true -org.eclipse.jdt.core.formatter.comment.format_comments=true -org.eclipse.jdt.core.formatter.comment.format_header=true -org.eclipse.jdt.core.formatter.comment.format_html=true -org.eclipse.jdt.core.formatter.comment.format_source_code=true -org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true -org.eclipse.jdt.core.formatter.comment.indent_root_tags=true -org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert -org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert -org.eclipse.jdt.core.formatter.comment.line_length=80 -org.eclipse.jdt.core.formatter.compact_else_if=true -org.eclipse.jdt.core.formatter.continuation_indentation=2 -org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 -org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true -org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true -org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_empty_lines=false -org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true -org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true -org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true -org.eclipse.jdt.core.formatter.indentation.size=4 -org.eclipse.jdt.core.formatter.insert_new_line_after_annotation=insert -org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert -org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert -org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert -org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert -org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert -org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert -org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert -org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert -org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert -org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert -org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert -org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false -org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false -org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false -org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false -org.eclipse.jdt.core.formatter.lineSplit=80 -org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 -org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 -org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true -org.eclipse.jdt.core.formatter.tabulation.char=space -org.eclipse.jdt.core.formatter.tabulation.size=4 -org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index c4f6cf3..0000000 --- a/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null @@ -1,6 +0,0 @@ -#Sat Sep 23 11:47:05 CDT 2006 -eclipse.preferences.version=1 -formatter_profile=_Sunflow Conventions -formatter_settings_version=10 -internal.default.compliance=default -org.eclipse.jdt.ui.text.custom_code_templates= diff --git a/.settings/org.eclipse.ltk.core.refactoring.prefs b/.settings/org.eclipse.ltk.core.refactoring.prefs deleted file mode 100644 index 436b429..0000000 --- a/.settings/org.eclipse.ltk.core.refactoring.prefs +++ /dev/null @@ -1,3 +0,0 @@ -#Sat Sep 23 11:47:05 CDT 2006 -eclipse.preferences.version=1 -org.eclipse.ltk.core.refactoring.enable.project.refactoring.history=false diff --git a/janino.jar b/janino.jar deleted file mode 100644 index 50b34ea9e6fcdc00951c3816b7bab8eb39a0c0b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 416460 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�b=ckgxy^4^^C~`;>!M- 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
Ksh4@gXoP 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 0) { - boolean showFrame = true; - String sampler = null; - boolean noRender = false; - String filename = null; - String input = null; - int i = 0; - int threads = 0; - boolean lowPriority = true; - boolean showAA = false; - boolean noGI = false; - boolean noCaustics = false; - int pathGI = 0; - float maxDist = 0; - String shaderOverride = null; - int resolutionW = 0, resolutionH = 0; - int aaMin = -5, aaMax = -5; - int samples = -1; - int bucketSize = 0; - String bucketOrder = null; - String bakingName = null; - boolean bakeViewdep = false; - String filterType = null; - boolean runBenchmark = false; - boolean runRTBenchmark = false; - String translateFilename = null; - int frameStart = 1, frameStop = 1; - while (i < args.length) { - if (args[i].equals("-o")) { - if (i > args.length - 2) - usage(false); - filename = args[i + 1]; - i += 2; - } else if (args[i].equals("-nogui")) { - showFrame = false; - i++; - } else if (args[i].equals("-ipr")) { - sampler = "ipr"; - i++; - } else if (args[i].equals("-threads")) { - if (i > args.length - 2) - usage(false); - threads = Integer.parseInt(args[i + 1]); - i += 2; - } else if (args[i].equals("-lopri")) { - lowPriority = true; - i++; - } else if (args[i].equals("-hipri")) { - lowPriority = false; - i++; - } else if (args[i].equals("-sampler")) { - if (i > args.length - 2) - usage(false); - sampler = args[i + 1]; - i += 2; - } else if (args[i].equals("-smallmesh")) { - TriangleMesh.setSmallTriangles(true); - i++; - } else if (args[i].equals("-dumpkd")) { - KDTree.setDumpMode(true, "kdtree"); - i++; - } else if (args[i].equals("-buildonly")) { - noRender = true; - i++; - } else if (args[i].equals("-showaa")) { - showAA = true; - i++; - } else if (args[i].equals("-nogi")) { - noGI = true; - i++; - } else if (args[i].equals("-nocaustics")) { - noCaustics = true; - i++; - } else if (args[i].equals("-pathgi")) { - if (i > args.length - 2) - usage(false); - pathGI = Integer.parseInt(args[i + 1]); - i += 2; - } else if (args[i].equals("-quick_ambocc")) { - if (i > args.length - 2) - usage(false); - maxDist = Float.parseFloat(args[i + 1]); - shaderOverride = "ambient_occlusion"; // new - // AmbientOcclusionShader(Color.WHITE, - // d); - i += 2; - } else if (args[i].equals("-quick_uvs")) { - if (i > args.length - 1) - usage(false); - shaderOverride = "show_uvs"; - i++; - } else if (args[i].equals("-quick_normals")) { - if (i > args.length - 1) - usage(false); - shaderOverride = "show_normals"; - i++; - } else if (args[i].equals("-quick_id")) { - if (i > args.length - 1) - usage(false); - shaderOverride = "show_instance_id"; - i++; - } else if (args[i].equals("-quick_prims")) { - if (i > args.length - 1) - usage(false); - shaderOverride = "show_primitive_id"; - i++; - } else if (args[i].equals("-quick_gray")) { - if (i > args.length - 1) - usage(false); - shaderOverride = "quick_gray"; - i++; - } else if (args[i].equals("-quick_wire")) { - if (i > args.length - 1) - usage(false); - shaderOverride = "wireframe"; - i++; - } else if (args[i].equals("-resolution")) { - if (i > args.length - 3) - usage(false); - resolutionW = Integer.parseInt(args[i + 1]); - resolutionH = Integer.parseInt(args[i + 2]); - i += 3; - } else if (args[i].equals("-aa")) { - if (i > args.length - 3) - usage(false); - aaMin = Integer.parseInt(args[i + 1]); - aaMax = Integer.parseInt(args[i + 2]); - i += 3; - } else if (args[i].equals("-samples")) { - if (i > args.length - 2) - usage(false); - samples = Integer.parseInt(args[i+1]); - i += 2; - } else if (args[i].equals("-bucket")) { - if (i > args.length - 3) - usage(false); - bucketSize = Integer.parseInt(args[i + 1]); - bucketOrder = args[i + 2]; - i += 3; - } else if (args[i].equals("-bake")) { - if (i > args.length - 2) - usage(false); - bakingName = args[i + 1]; - i += 2; - } else if (args[i].equals("-bakedir")) { - if (i > args.length - 2) - usage(false); - String baketype = args[i + 1]; - if (baketype.equals("view")) - bakeViewdep = true; - else if (baketype.equals("ortho")) - bakeViewdep = false; - else - usage(false); - i += 2; - } else if (args[i].equals("-filter")) { - if (i > args.length - 2) - usage(false); - filterType = args[i + 1]; - i += 2; - } else if (args[i].equals("-bench")) { - runBenchmark = true; - i++; - } else if (args[i].equals("-rtbench")) { - runRTBenchmark = true; - i++; - } else if (args[i].equals("-frame")) { - if (i > args.length - 2) - usage(false); - frameStart = frameStop = Integer.parseInt(args[i + 1]); - i += 2; - } else if (args[i].equals("-anim")) { - if (i > args.length - 3) - usage(false); - frameStart = Integer.parseInt(args[i + 1]); - frameStop = Integer.parseInt(args[i + 2]); - i += 3; - } else if (args[i].equals("-v")) { - if (i > args.length - 2) - usage(false); - UI.verbosity(Integer.parseInt(args[i + 1])); - i += 2; - } else if (args[i].equals("-translate")) { - if (i > args.length - 2) - usage(false); - translateFilename = args[i + 1]; - i += 2; - } else if (args[i].equals("-h") || args[i].equals("-help")) { - usage(true); - } else { - if (input != null) - usage(false); - input = args[i]; - i++; - } - } - if (runBenchmark) { - SunflowAPI.runSystemCheck(); - new Benchmark().execute(); - return; - } - if (runRTBenchmark) { - SunflowAPI.runSystemCheck(); - new RealtimeBenchmark(showFrame, threads); - return; - } - if (input == null) - usage(false); - SunflowAPI.runSystemCheck(); - if (translateFilename != null) { - SunflowAPI.translate(input, translateFilename); - return; - } - if (frameStart < frameStop && showFrame) { - UI.printWarning(Module.GUI, "Animations should not be rendered without -nogui - forcing GUI off anyway"); - showFrame = false; - } - if (frameStart < frameStop && filename == null) { - filename = "output.#.png"; - UI.printWarning(Module.GUI, "Animation output was not specified - defaulting to: \"%s\"", filename); - } - for (int frameNumber = frameStart; frameNumber <= frameStop; frameNumber++) { - SunflowAPI api = SunflowAPI.create(input, frameNumber); - if (api == null) - continue; - if (noRender) - continue; - if (resolutionW > 0 && resolutionH > 0) { - api.parameter("resolutionX", resolutionW); - api.parameter("resolutionY", resolutionH); - } - if (aaMin != -5 || aaMax != -5) { - api.parameter("aa.min", aaMin); - api.parameter("aa.max", aaMax); - } - if (samples >= 0) - api.parameter("aa.samples", samples); - if (bucketSize > 0) - api.parameter("bucket.size", bucketSize); - if (bucketOrder != null) - api.parameter("bucket.order", bucketOrder); - api.parameter("aa.display", showAA); - api.parameter("threads", threads); - api.parameter("threads.lowPriority", lowPriority); - if (bakingName != null) { - api.parameter("baking.instance", bakingName); - api.parameter("baking.viewdep", bakeViewdep); - } - if (filterType != null) - api.parameter("filter", filterType); - if (noGI) - api.parameter("gi.engine", "none"); - else if (pathGI > 0) { - api.parameter("gi.engine", "path"); - api.parameter("gi.path.samples", pathGI); - } - if (noCaustics) - api.parameter("caustics", "none"); - if (sampler != null) - api.parameter("sampler", sampler); - api.options(SunflowAPI.DEFAULT_OPTIONS); - if (shaderOverride != null) { - if (shaderOverride.equals("ambient_occlusion")) - api.parameter("maxdist", maxDist); - api.shader("cmdline_override", shaderOverride); - api.parameter("override.shader", "cmdline_override"); - api.parameter("override.photons", true); - api.options(SunflowAPI.DEFAULT_OPTIONS); - } - // create display - Display display; - String currentFilename = (filename != null) ? filename.replace("#", String.format("%04d", frameNumber)) : null; - if (showFrame) { - display = new FrameDisplay(currentFilename); - } else { - if (currentFilename != null && currentFilename.equals("imgpipe")) { - display = new ImgPipeDisplay(); - } else - display = new FileDisplay(currentFilename); - } - api.render(SunflowAPI.DEFAULT_OPTIONS, display); - } - } else { - MetalLookAndFeel.setCurrentTheme(new DefaultMetalTheme()); - SunflowGUI gui = new SunflowGUI(); - gui.setVisible(true); - Dimension screenRes = Toolkit.getDefaultToolkit().getScreenSize(); - if (screenRes.getWidth() <= DEFAULT_WIDTH || screenRes.getHeight() <= DEFAULT_HEIGHT) - gui.setExtendedState(MAXIMIZED_BOTH); - gui.tileWindowMenuItem.doClick(); - SunflowAPI.runSystemCheck(); - } - } - - public SunflowGUI() { - super(); - currentFile = null; - lastSaveDirectory = null; - api = null; - initGUI(); - pack(); - setLocationRelativeTo(null); - newFileMenuItemActionPerformed(null); - UI.set(this); - } - - private void initGUI() { - setTitle("Sunflow v" + SunflowAPI.VERSION); - setDefaultCloseOperation(EXIT_ON_CLOSE); - { - desktop = new JDesktopPane(); - getContentPane().add(desktop, BorderLayout.CENTER); - Dimension screenRes = Toolkit.getDefaultToolkit().getScreenSize(); - if (screenRes.getWidth() <= DEFAULT_WIDTH || screenRes.getHeight() <= DEFAULT_HEIGHT) - desktop.setPreferredSize(new java.awt.Dimension(640, 480)); - else - desktop.setPreferredSize(new java.awt.Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT)); - { - imagePanelFrame = new JInternalFrame(); - desktop.add(imagePanelFrame); - { - jPanel1 = new JPanel(); - FlowLayout jPanel1Layout = new FlowLayout(); - jPanel1Layout.setAlignment(FlowLayout.LEFT); - jPanel1.setLayout(jPanel1Layout); - imagePanelFrame.getContentPane().add(jPanel1, BorderLayout.NORTH); - { - renderButton = new JButton(); - jPanel1.add(renderButton); - renderButton.setText("Render"); - renderButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - renderMenuItemActionPerformed(evt); - } - }); - } - { - iprButton = new JButton(); - jPanel1.add(iprButton); - iprButton.setText("IPR"); - iprButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - iprMenuItemActionPerformed(evt); - } - }); - } - } - { - imagePanel = new ImagePanel(); - imagePanelFrame.getContentPane().add(imagePanel, BorderLayout.CENTER); - } - imagePanelFrame.pack(); - imagePanelFrame.setResizable(true); - imagePanelFrame.setMaximizable(true); - imagePanelFrame.setVisible(true); - imagePanelFrame.setTitle("Image"); - imagePanelFrame.setIconifiable(true); - } - { - editorFrame = new JInternalFrame(); - desktop.add(editorFrame); - editorFrame.setTitle("Script Editor"); - editorFrame.setMaximizable(true); - editorFrame.setResizable(true); - editorFrame.setIconifiable(true); - { - jScrollPane1 = new JScrollPane(); - editorFrame.getContentPane().add(jScrollPane1, BorderLayout.CENTER); - jScrollPane1.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); - jScrollPane1.setPreferredSize(new java.awt.Dimension(360, 280)); - { - editorTextArea = new JTextArea(); - jScrollPane1.setViewportView(editorTextArea); - editorTextArea.setFont(new java.awt.Font("Monospaced", 0, 12)); - // drag and drop - editorTextArea.setTransferHandler(new SceneTransferHandler()); - } - } - { - jPanel3 = new JPanel(); - editorFrame.getContentPane().add(jPanel3, BorderLayout.SOUTH); - FlowLayout jPanel3Layout = new FlowLayout(); - jPanel3Layout.setAlignment(FlowLayout.RIGHT); - jPanel3.setLayout(jPanel3Layout); - { - buildButton = new JButton(); - jPanel3.add(buildButton); - buildButton.setText("Build Scene"); - buildButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - buildMenuItemActionPerformed(evt); - } - }); - } - } - editorFrame.pack(); - editorFrame.setVisible(true); - } - { - consoleFrame = new JInternalFrame(); - desktop.add(consoleFrame); - consoleFrame.setIconifiable(true); - consoleFrame.setMaximizable(true); - consoleFrame.setResizable(true); - consoleFrame.setTitle("Console"); - { - jScrollPane2 = new JScrollPane(); - consoleFrame.getContentPane().add(jScrollPane2, BorderLayout.CENTER); - jScrollPane2.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); - jScrollPane2.setPreferredSize(new java.awt.Dimension(360, 100)); - { - consoleTextArea = new JTextArea(); - jScrollPane2.setViewportView(consoleTextArea); - consoleTextArea.setFont(new java.awt.Font("Monospaced", 0, 12)); - consoleTextArea.setEditable(false); - } - } - { - jPanel4 = new JPanel(); - consoleFrame.getContentPane().add(jPanel4, BorderLayout.SOUTH); - BorderLayout jPanel4Layout = new BorderLayout(); - jPanel4.setLayout(jPanel4Layout); - { - jPanel6 = new JPanel(); - BorderLayout jPanel6Layout = new BorderLayout(); - jPanel6.setLayout(jPanel6Layout); - jPanel4.add(jPanel6, BorderLayout.CENTER); - jPanel6.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 0)); - { - taskProgressBar = new JProgressBar(); - jPanel6.add(taskProgressBar); - taskProgressBar.setEnabled(false); - taskProgressBar.setString(""); - taskProgressBar.setStringPainted(true); - taskProgressBar.setOpaque(false); - } - } - { - jPanel5 = new JPanel(); - FlowLayout jPanel5Layout = new FlowLayout(); - jPanel5Layout.setAlignment(FlowLayout.RIGHT); - jPanel5.setLayout(jPanel5Layout); - jPanel4.add(jPanel5, BorderLayout.EAST); - { - taskCancelButton = new JButton(); - jPanel5.add(taskCancelButton); - taskCancelButton.setText("Cancel"); - taskCancelButton.setEnabled(false); - taskCancelButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - UI.taskCancel(); - } - }); - } - { - clearConsoleButton = new JButton(); - jPanel5.add(clearConsoleButton); - clearConsoleButton.setText("Clear"); - clearConsoleButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - clearConsole(); - } - }); - } - } - } - consoleFrame.pack(); - consoleFrame.setVisible(true); - } - } - { - jMenuBar1 = new JMenuBar(); - setJMenuBar(jMenuBar1); - { - fileMenu = new JMenu(); - jMenuBar1.add(fileMenu); - fileMenu.setText("File"); - { - newFileMenuItem = new JMenuItem(); - fileMenu.add(newFileMenuItem); - newFileMenuItem.setText("New"); - newFileMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl N")); - newFileMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - newFileMenuItemActionPerformed(evt); - } - }); - } - { - openFileMenuItem = new JMenuItem(); - fileMenu.add(openFileMenuItem); - openFileMenuItem.setText("Open ..."); - openFileMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl O")); - openFileMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - openFileMenuItemActionPerformed(evt); - } - }); - } - { - saveMenuItem = new JMenuItem(); - fileMenu.add(saveMenuItem); - saveMenuItem.setText("Save"); - saveMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl S")); - saveMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - saveCurrentFile(currentFile); - } - }); - } - { - saveAsMenuItem = new JMenuItem(); - fileMenu.add(saveAsMenuItem); - saveAsMenuItem.setText("Save As ..."); - saveAsMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - saveAsMenuItemActionPerformed(evt); - } - }); - } - { - jSeparator2 = new JSeparator(); - fileMenu.add(jSeparator2); - } - { - exitMenuItem = new JMenuItem(); - fileMenu.add(exitMenuItem); - exitMenuItem.setText("Exit"); - exitMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - System.exit(0); - } - }); - } - } - { - sceneMenu = new JMenu(); - jMenuBar1.add(sceneMenu); - sceneMenu.setText("Scene"); - { - buildMenuItem = new JMenuItem(); - sceneMenu.add(buildMenuItem); - buildMenuItem.setText("Build"); - buildMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl B")); - buildMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - if (sceneMenu.isEnabled()) - buildMenuItemActionPerformed(evt); - } - }); - } - { - autoBuildMenuItem = new JCheckBoxMenuItem(); - sceneMenu.add(autoBuildMenuItem); - autoBuildMenuItem.setText("Build on open"); - autoBuildMenuItem.setSelected(true); - } - { - jSeparator3 = new JSeparator(); - sceneMenu.add(jSeparator3); - } - { - renderMenuItem = new JMenuItem(); - sceneMenu.add(renderMenuItem); - renderMenuItem.setText("Render"); - renderMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - renderMenuItemActionPerformed(evt); - } - }); - } - { - iprMenuItem = new JMenuItem(); - sceneMenu.add(iprMenuItem); - iprMenuItem.setText("IPR"); - iprMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - iprMenuItemActionPerformed(evt); - } - }); - } - { - clearLogMenuItem = new JCheckBoxMenuItem(); - sceneMenu.add(clearLogMenuItem); - clearLogMenuItem.setText("Auto Clear Log"); - clearLogMenuItem.setToolTipText("Clears the console before building or rendering"); - clearLogMenuItem.setSelected(true); - } - { - jSeparator4 = new JSeparator(); - sceneMenu.add(jSeparator4); - } - { - textureCacheClearMenuItem = new JMenuItem(); - sceneMenu.add(textureCacheClearMenuItem); - textureCacheClearMenuItem.setText("Clear Texture Cache"); - textureCacheClearMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - textureCacheClearMenuItemActionPerformed(evt); - } - }); - } - { - smallTrianglesMenuItem = new JCheckBoxMenuItem(); - sceneMenu.add(smallTrianglesMenuItem); - smallTrianglesMenuItem.setText("Low Mem Triangles"); - smallTrianglesMenuItem.setToolTipText("Load future meshes using a low memory footprint triangle representation"); - smallTrianglesMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - smallTrianglesMenuItemActionPerformed(evt); - } - }); - } - } - { - imageMenu = new JMenu(); - jMenuBar1.add(imageMenu); - imageMenu.setText("Image"); - { - resetZoomMenuItem = new JMenuItem(); - imageMenu.add(resetZoomMenuItem); - resetZoomMenuItem.setText("Reset Zoom"); - resetZoomMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - imagePanel.reset(); - } - }); - } - { - fitWindowMenuItem = new JMenuItem(); - imageMenu.add(fitWindowMenuItem); - fitWindowMenuItem.setText("Fit to Window"); - fitWindowMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - imagePanel.fit(); - } - }); - } - { - jSeparator1 = new JSeparator(); - imageMenu.add(jSeparator1); - } - { - jMenuItem4 = new JMenuItem(); - imageMenu.add(jMenuItem4); - jMenuItem4.setText("Save Image ..."); - jMenuItem4.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - // imagePanel.image; - JFileChooser fc = new JFileChooser("."); - fc.setFileFilter(new FileFilter() { - @Override - public String getDescription() { - return "Image File"; - } - - @Override - public boolean accept(File f) { - return (f.isDirectory() || f.getName().endsWith(".png") || f.getName().endsWith(".tga")); - } - }); - if (fc.showSaveDialog(SunflowGUI.this) == JFileChooser.APPROVE_OPTION) { - String filename = fc.getSelectedFile().getAbsolutePath(); - imagePanel.save(filename); - } - } - }); - } - } - { - windowMenu = new JMenu(); - jMenuBar1.add(windowMenu); - windowMenu.setText("Window"); - } - { - imageWindowMenuItem = new JMenuItem(); - windowMenu.add(imageWindowMenuItem); - imageWindowMenuItem.setText("Image"); - imageWindowMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl 1")); - imageWindowMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - selectFrame(imagePanelFrame); - } - }); - } - { - editorWindowMenuItem = new JMenuItem(); - windowMenu.add(editorWindowMenuItem); - editorWindowMenuItem.setText("Script Editor"); - editorWindowMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl 2")); - editorWindowMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - selectFrame(editorFrame); - } - }); - } - { - consoleWindowMenuItem = new JMenuItem(); - windowMenu.add(consoleWindowMenuItem); - consoleWindowMenuItem.setText("Console"); - consoleWindowMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl 3")); - consoleWindowMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - selectFrame(consoleFrame); - } - }); - } - { - jSeparator5 = new JSeparator(); - windowMenu.add(jSeparator5); - } - { - tileWindowMenuItem = new JMenuItem(); - windowMenu.add(tileWindowMenuItem); - tileWindowMenuItem.setText("Tile"); - tileWindowMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl T")); - tileWindowMenuItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent evt) { - tileWindowMenuItemActionPerformed(evt); - } - }); - } - } - } - - private void newFileMenuItemActionPerformed(ActionEvent evt) { - if (evt != null) { - // check save? - } - // put some template code into the editor - String template = "import org.sunflow.core.*;\nimport org.sunflow.core.accel.*;\nimport org.sunflow.core.camera.*;\nimport org.sunflow.core.primitive.*;\nimport org.sunflow.core.shader.*;\nimport org.sunflow.image.Color;\nimport org.sunflow.math.*;\n\npublic void build() {\n // your code goes here\n\n}\n"; - editorTextArea.setText(template); - } - - private void openFileMenuItemActionPerformed(ActionEvent evt) { - JFileChooser fc = new JFileChooser("."); - if (lastSaveDirectory != null) - fc.setCurrentDirectory(lastSaveDirectory); - fc.setFileFilter(new FileFilter() { - @Override - public String getDescription() { - return "Scene File"; - } - - @Override - public boolean accept(File f) { - return (f.isDirectory() || f.getName().endsWith(".sc") || f.getName().endsWith(".java")); - } - }); - - if (fc.showOpenDialog(SunflowGUI.this) == JFileChooser.APPROVE_OPTION) { - final String f = fc.getSelectedFile().getAbsolutePath(); - openFile(f); - lastSaveDirectory = fc.getSelectedFile().getParentFile(); - } - } - - private void buildMenuItemActionPerformed(ActionEvent evt) { - new Thread() { - @Override - public void run() { - setEnableInterface(false); - if (clearLogMenuItem.isSelected()) - clearConsole(); - Timer t = new Timer(); - t.start(); - try { - api = SunflowAPI.compile(editorTextArea.getText()); - } catch (NoClassDefFoundError e) { - UI.printError(Module.GUI, "Janino library not found. Please check command line."); - api = null; - } - if (api != null) { - try { - if (currentFile != null) { - String dir = new File(currentFile).getAbsoluteFile().getParent(); - api.searchpath("texture", dir); - api.searchpath("include", dir); - } - api.build(); - } catch (Exception e) { - UI.printError(Module.GUI, "Build terminated abnormally: %s", e.getMessage()); - for (StackTraceElement elt : e.getStackTrace()) { - UI.printInfo(Module.GUI, " at %s", elt.toString()); - } - e.printStackTrace(); - } - t.end(); - UI.printInfo(Module.GUI, "Build time: %s", t.toString()); - } - setEnableInterface(true); - } - }.start(); - } - - private void clearConsole() { - consoleTextArea.setText(null); - } - - private void println(final String s) { - SwingUtilities.invokeLater(new Runnable() { - public void run() { - consoleTextArea.append(s + "\n"); - } - }); - } - - private void setEnableInterface(boolean enabled) { - // lock or unlock options which are unsafe during builds or renders - newFileMenuItem.setEnabled(enabled); - openFileMenuItem.setEnabled(enabled); - saveMenuItem.setEnabled(enabled); - saveAsMenuItem.setEnabled(enabled); - sceneMenu.setEnabled(enabled); - buildButton.setEnabled(enabled); - renderButton.setEnabled(enabled); - iprButton.setEnabled(enabled); - } - - public void print(Module m, PrintLevel level, String s) { - if (level == PrintLevel.ERROR) - JOptionPane.showMessageDialog(SunflowGUI.this, s, String.format("Error - %s", m.name()), JOptionPane.ERROR_MESSAGE); - println(UI.formatOutput(m, level, s)); - } - - public void taskStart(String s, int min, int max) { - currentTask = s; - currentTaskLastP = -1; - final int taskMin = min; - final int taskMax = max; - SwingUtilities.invokeLater(new Runnable() { - public void run() { - taskProgressBar.setEnabled(true); - taskCancelButton.setEnabled(true); - taskProgressBar.setMinimum(taskMin); - taskProgressBar.setMaximum(taskMax); - taskProgressBar.setValue(taskMin); - taskProgressBar.setString(currentTask); - } - }); - } - - public void taskUpdate(int current) { - final int taskCurrent = current; - final String taskString = currentTask; - SwingUtilities.invokeLater(new Runnable() { - public void run() { - taskProgressBar.setValue(taskCurrent); - int p = (int) (100.0 * taskProgressBar.getPercentComplete()); - if (p > currentTaskLastP) { - taskProgressBar.setString(taskString + " [" + p + "%]"); - currentTaskLastP = p; - } - } - }); - } - - public void taskStop() { - SwingUtilities.invokeLater(new Runnable() { - public void run() { - taskProgressBar.setValue(taskProgressBar.getMinimum()); - taskProgressBar.setString(""); - taskProgressBar.setEnabled(false); - taskCancelButton.setEnabled(false); - } - }); - } - - private void renderMenuItemActionPerformed(ActionEvent evt) { - new Thread() { - @Override - public void run() { - setEnableInterface(false); - if (clearLogMenuItem.isSelected()) - clearConsole(); - if (api != null) { - api.parameter("sampler", "bucket"); - api.options(SunflowAPI.DEFAULT_OPTIONS); - api.render(SunflowAPI.DEFAULT_OPTIONS, imagePanel); - } else - UI.printError(Module.GUI, "Nothing to render!"); - setEnableInterface(true); - } - }.start(); - } - - private void iprMenuItemActionPerformed(ActionEvent evt) { - new Thread() { - @Override - public void run() { - setEnableInterface(false); - if (clearLogMenuItem.isSelected()) - clearConsole(); - if (api != null) { - api.parameter("sampler", "ipr"); - api.options(SunflowAPI.DEFAULT_OPTIONS); - api.render(SunflowAPI.DEFAULT_OPTIONS, imagePanel); - } else - UI.printError(Module.GUI, "Nothing to IPR!"); - setEnableInterface(true); - } - }.start(); - } - - private void textureCacheClearMenuItemActionPerformed(ActionEvent evt) { - TextureCache.flush(); - } - - private void smallTrianglesMenuItemActionPerformed(ActionEvent evt) { - TriangleMesh.setSmallTriangles(smallTrianglesMenuItem.isSelected()); - } - - private void saveAsMenuItemActionPerformed(ActionEvent evt) { - JFileChooser fc = new JFileChooser("."); - if (lastSaveDirectory != null) - fc.setCurrentDirectory(lastSaveDirectory); - fc.setFileFilter(new FileFilter() { - @Override - public String getDescription() { - return "Scene File"; - } - - @Override - public boolean accept(File f) { - return (f.isDirectory() || f.getName().endsWith(".java")); - } - }); - - if (fc.showSaveDialog(SunflowGUI.this) == JFileChooser.APPROVE_OPTION) { - String f = fc.getSelectedFile().getAbsolutePath(); - if (!f.endsWith(".java")) - f += ".java"; - File file = new File(f); - if (!file.exists() || JOptionPane.showConfirmDialog(SunflowGUI.this, "This file already exists.\nOverwrite?", "Warning", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { - // save file - saveCurrentFile(f); - lastSaveDirectory = file.getParentFile(); - } - } - } - - private void saveCurrentFile(String filename) { - if (filename == null) { - // no filename was picked, go to save as dialog - saveAsMenuItemActionPerformed(null); - return; - } - FileWriter file; - try { - file = new FileWriter(filename); - // get text from editor pane - file.write(editorTextArea.getText()); - file.close(); - // update current filename - currentFile = filename; - UI.printInfo(Module.GUI, "Saved current script to \"%s\"", filename); - } catch (IOException e) { - UI.printError(Module.GUI, "Unable to save: \"%s\"", filename); - e.printStackTrace(); - } - } - - private void selectFrame(JInternalFrame frame) { - try { - frame.setSelected(true); - frame.setIcon(false); - } catch (PropertyVetoException e) { - // this should never happen - e.printStackTrace(); - } - } - - private void tileWindowMenuItemActionPerformed(ActionEvent evt) { - try { - if (imagePanelFrame.isIcon()) - imagePanelFrame.setIcon(false); - if (editorFrame.isIcon()) - editorFrame.setIcon(false); - if (consoleFrame.isIcon()) - consoleFrame.setIcon(false); - - int width = desktop.getWidth(); - int height = desktop.getHeight(); - int widthLeft = width * 7 / 12; - int widthRight = width - widthLeft; - int pad = 2; - int pad2 = pad + pad; - - imagePanelFrame.reshape(pad, pad, widthLeft - pad2, height - pad2); - editorFrame.reshape(pad + widthLeft, pad, widthRight - pad2, height / 2 - pad2); - consoleFrame.reshape(pad + widthLeft, pad + height / 2, widthRight - pad2, height / 2 - pad2); - } catch (PropertyVetoException e) { - e.printStackTrace(); - } - } - - private void openFile(String filename) { - if (filename.endsWith(".java")) { - // read the file line by line - String code = ""; - FileReader file; - try { - file = new FileReader(filename); - BufferedReader bf = new BufferedReader(file); - while (true) { - String line; - line = bf.readLine(); - if (line == null) - break; - code += line; - code += "\n"; - } - file.close(); - editorTextArea.setText(code); - } catch (FileNotFoundException e) { - UI.printError(Module.GUI, "Unable to load: \"%s\"", filename); - return; - } catch (IOException e) { - UI.printError(Module.GUI, "Unable to load: \"%s\"", filename); - return; - } - // loade went ok, use filename as current - currentFile = filename; - UI.printInfo(Module.GUI, "Loaded script: \"%s\"", filename); - } else if (filename.endsWith(".sc")) { - String template = "import org.sunflow.core.*;\nimport org.sunflow.core.accel.*;\nimport org.sunflow.core.camera.*;\nimport org.sunflow.core.primitive.*;\nimport org.sunflow.core.shader.*;\nimport org.sunflow.image.Color;\nimport org.sunflow.math.*;\n\npublic void build() {\n include(\"" + filename.replace("\\", "\\\\") + "\");\n}\n"; - editorTextArea.setText(template); - // no java file associated - currentFile = null; - UI.printInfo(Module.GUI, "Created template for \"%s\"", filename); - } else { - UI.printError(Module.GUI, "Unknown file format selected"); - return; - } - editorTextArea.setCaretPosition(0); - if (autoBuildMenuItem.isSelected()) { - // try to compile the code we just loaded - buildMenuItemActionPerformed(null); - } - - } - - private class SceneTransferHandler extends TransferHandler { - @Override - public boolean importData(JComponent c, Transferable t) { - if (!sceneMenu.isEnabled()) - return false; - // can I import it? - if (!canImport(c, t.getTransferDataFlavors())) { - return false; - } - try { - // get a List of Files - List files = (java.util.List) t.getTransferData(DataFlavor.javaFileListFlavor); - for (int i = 0; i < files.size(); i++) { - final File file = (File) files.get(i); - String filename = file.getAbsolutePath(); - // check extension - if (filename.endsWith(".sc") || filename.endsWith(".java")) { - openFile(filename); - // load only one file at a time, stop here - break; - } - } - } catch (Exception exp) { - // debug - exp.printStackTrace(); - } - - return false; - } - - @Override - public boolean canImport(JComponent c, DataFlavor[] flavors) { - // Just a quick check to see if a file can be accepted at this time - // Are there any files around? - for (int i = 0; i < flavors.length; i++) { - if (flavors[i].isFlavorJavaFileListType()) - return true; - } - - // guess not - return false; - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/AsciiFileSunflowAPI.java b/src/org/sunflow/AsciiFileSunflowAPI.java deleted file mode 100644 index 5da75b7..0000000 --- a/src/org/sunflow/AsciiFileSunflowAPI.java +++ /dev/null @@ -1,93 +0,0 @@ -package org.sunflow; - -import java.io.BufferedOutputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Locale; - -import org.sunflow.core.ParameterList.InterpolationType; -import org.sunflow.core.parser.SCAbstractParser.Keyword; -import org.sunflow.math.Matrix4; - -class AsciiFileSunflowAPI extends FileSunflowAPI { - private OutputStream stream; - - AsciiFileSunflowAPI(String filename) throws IOException { - stream = new BufferedOutputStream(new FileOutputStream(filename)); - } - - @Override - protected void writeBoolean(boolean value) { - if (value) - writeString("true"); - else - writeString("false"); - } - - @Override - protected void writeFloat(float value) { - writeString(String.format("%s", value)); - } - - @Override - protected void writeInt(int value) { - writeString(String.format("%d", value)); - } - - @Override - protected void writeInterpolationType(InterpolationType interp) { - writeString(String.format("%s", interp.toString().toLowerCase(Locale.ENGLISH))); - } - - @Override - protected void writeKeyword(Keyword keyword) { - writeString(String.format("%s", keyword.toString().toLowerCase(Locale.ENGLISH).replace("_array", "[]"))); - } - - @Override - protected void writeMatrix(Matrix4 value) { - writeString("row"); - for (float f : value.asRowMajor()) - writeFloat(f); - } - - @Override - protected void writeNewline(int indentNext) { - try { - stream.write('\n'); - for (int i = 0; i < indentNext; i++) - stream.write('\t'); - } catch (IOException e) { - throw new RuntimeException(e.getMessage()); - } - } - - @Override - protected void writeString(String string) { - try { - // check if we need to write string with quotes - if (string.contains(" ") && !string.contains("")) - stream.write(String.format("\"%s\"", string).getBytes()); - else - stream.write(string.getBytes()); - stream.write(' '); - } catch (IOException e) { - throw new RuntimeException(e.getMessage()); - } - } - - @Override - protected void writeVerbatimString(String string) { - writeString(String.format("%s\n ", string)); - } - - @Override - public void close() { - try { - stream.close(); - } catch (IOException e) { - throw new RuntimeException(e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/Benchmark.java b/src/org/sunflow/Benchmark.java deleted file mode 100644 index 956c5b5..0000000 --- a/src/org/sunflow/Benchmark.java +++ /dev/null @@ -1,280 +0,0 @@ -package org.sunflow; - -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.net.URL; - -import javax.imageio.ImageIO; - -import org.sunflow.core.Display; -import org.sunflow.core.display.FileDisplay; -import org.sunflow.core.display.FrameDisplay; -import org.sunflow.image.Color; -import org.sunflow.math.Matrix4; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.BenchmarkFramework; -import org.sunflow.system.BenchmarkTest; -import org.sunflow.system.UI; -import org.sunflow.system.UserInterface; -import org.sunflow.system.UI.Module; -import org.sunflow.system.UI.PrintLevel; - -public class Benchmark implements BenchmarkTest, UserInterface, Display { - private int resolution; - private boolean showOutput; - private boolean showBenchmarkOutput; - private boolean saveOutput; - private boolean showWindow; - private int threads; - private int[] referenceImage; - private int[] validationImage; - private int errorThreshold; - - public static void main(String[] args) { - if (args.length == 0) { - System.out.println("Benchmark options:"); - System.out.println(" -regen Regenerate reference images for a variety of sizes"); - System.out.println(" -bench [threads] [resolution] Run a single iteration of the benchmark using the specified thread count and image resolution"); - System.out.println(" Default: threads=0 (auto-detect cpus), resolution=256"); - System.out.println(" -show Render the benchmark scene into a window without performing validation"); - } else if (args[0].equals("-regen")) { - int[] sizes = { 32, 64, 96, 128, 256, 384, 512 }; - for (int s : sizes) { - // run a single iteration to generate the reference image - Benchmark b = new Benchmark(s, true, false, true); - b.kernelMain(); - } - } else if (args[0].equals("-bench")) { - int threads = 0, resolution = 256; - if (args.length > 1) - threads = Integer.parseInt(args[1]); - if (args.length > 2) - resolution = Integer.parseInt(args[2]); - Benchmark benchmark = new Benchmark(resolution, false, true, false, threads, false); - benchmark.kernelBegin(); - benchmark.kernelMain(); - benchmark.kernelEnd(); - } else if (args[0].equals("-show")) { - Benchmark benchmark = new Benchmark(512, true, true, false, 0, true); - benchmark.kernelMain(); - } - } - - public Benchmark() { - this(384, false, true, false); - } - - public Benchmark(int resolution, boolean showOutput, boolean showBenchmarkOutput, boolean saveOutput) { - this(resolution, showOutput, showBenchmarkOutput, saveOutput, 0, false); - } - - public Benchmark(int resolution, boolean showOutput, boolean showBenchmarkOutput, boolean saveOutput, int threads, boolean showWindow) { - UI.set(this); - this.resolution = resolution; - this.showOutput = showOutput; - this.showBenchmarkOutput = showBenchmarkOutput; - this.saveOutput = saveOutput; - this.showWindow = showWindow; - this.threads = threads; - errorThreshold = 6; - // fetch reference image from resources (jar file or classpath) - if (saveOutput) - return; - URL imageURL = Benchmark.class.getResource(String.format("/resources/golden_%04X.png", resolution)); - if (imageURL == null) - UI.printError(Module.BENCH, "Unable to find reference frame!"); - UI.printInfo(Module.BENCH, "Loading reference image from: %s", imageURL); - try { - BufferedImage bi = ImageIO.read(imageURL); - if (bi.getWidth() != resolution || bi.getHeight() != resolution) - UI.printError(Module.BENCH, "Reference image has invalid resolution! Expected %dx%d found %dx%d", resolution, resolution, bi.getWidth(), bi.getHeight()); - referenceImage = new int[resolution * resolution]; - for (int y = 0, i = 0; y < resolution; y++) - for (int x = 0; x < resolution; x++, i++) - referenceImage[i] = bi.getRGB(x, resolution - 1 - y); // flip - } catch (IOException e) { - UI.printError(Module.BENCH, "Unable to load reference frame!"); - } - } - - public void execute() { - // 10 iterations maximum - 10 minute time limit - BenchmarkFramework framework = new BenchmarkFramework(10, 600); - framework.execute(this); - } - - private class BenchmarkScene extends SunflowAPI { - public BenchmarkScene() { - build(); - render(SunflowAPI.DEFAULT_OPTIONS, showWindow ? new FrameDisplay() : saveOutput ? new FileDisplay(String.format("resources/golden_%04X.png", resolution)) : Benchmark.this); - } - - @Override - public void build() { - // settings - parameter("threads", threads); - // spawn regular priority threads - parameter("threads.lowPriority", false); - parameter("resolutionX", resolution); - parameter("resolutionY", resolution); - parameter("aa.min", -1); - parameter("aa.max", 1); - parameter("filter", "triangle"); - parameter("depths.diffuse", 2); - parameter("depths.reflection", 2); - parameter("depths.refraction", 2); - parameter("bucket.order", "hilbert"); - parameter("bucket.size", 32); - // gi options - parameter("gi.engine", "igi"); - parameter("gi.igi.samples", 90); - parameter("gi.igi.c", 0.000008f); - options(SunflowAPI.DEFAULT_OPTIONS); - buildCornellBox(); - } - - private void buildCornellBox() { - // camera - parameter("transform", Matrix4.lookAt(new Point3(0, 0, -600), new Point3(0, 0, 0), new Vector3(0, 1, 0))); - parameter("fov", 45.0f); - camera("main_camera", "pinhole"); - parameter("camera", "main_camera"); - options(SunflowAPI.DEFAULT_OPTIONS); - // cornell box - float minX = -200; - float maxX = 200; - float minY = -160; - float maxY = minY + 400; - float minZ = -250; - float maxZ = 200; - - float[] verts = new float[] { minX, minY, minZ, maxX, minY, minZ, - maxX, minY, maxZ, minX, minY, maxZ, minX, maxY, minZ, maxX, - maxY, minZ, maxX, maxY, maxZ, minX, maxY, maxZ, }; - int[] indices = new int[] { 0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4, 1, - 2, 5, 5, 6, 2, 2, 3, 6, 6, 7, 3, 0, 3, 4, 4, 7, 3 }; - - parameter("diffuse", null, 0.70f, 0.70f, 0.70f); - shader("gray_shader", "diffuse"); - parameter("diffuse", null, 0.80f, 0.25f, 0.25f); - shader("red_shader", "diffuse"); - parameter("diffuse", null, 0.25f, 0.25f, 0.80f); - shader("blue_shader", "diffuse"); - - // build walls - parameter("triangles", indices); - parameter("points", "point", "vertex", verts); - parameter("faceshaders", new int[] { 0, 0, 0, 0, 1, 1, 0, 0, 2, 2 }); - geometry("walls", "triangle_mesh"); - - // instance walls - parameter("shaders", new String[] { "gray_shader", "red_shader", - "blue_shader" }); - instance("walls.instance", "walls"); - - // create mesh light - parameter("points", "point", "vertex", new float[] { -50, maxY - 1, - -50, 50, maxY - 1, -50, 50, maxY - 1, 50, -50, maxY - 1, 50 }); - parameter("triangles", new int[] { 0, 1, 2, 2, 3, 0 }); - parameter("radiance", null, 15, 15, 15); - parameter("samples", 8); - light("light", "triangle_mesh"); - - // spheres - parameter("eta", 1.6f); - shader("Glass", "glass"); - sphere("glass_sphere", "Glass", -120, minY + 55, -150, 50); - parameter("color", null, 0.70f, 0.70f, 0.70f); - shader("Mirror", "mirror"); - sphere("mirror_sphere", "Mirror", 100, minY + 60, -50, 50); - - // scanned model - geometry("teapot", "teapot"); - parameter("transform", Matrix4.translation(80, -50, 100).multiply(Matrix4.rotateX((float) -Math.PI / 6)).multiply(Matrix4.rotateY((float) Math.PI / 4)).multiply(Matrix4.rotateX((float) -Math.PI / 2).multiply(Matrix4.scale(1.2f)))); - parameter("shaders", "gray_shader"); - instance("teapot.instance1", "teapot"); - parameter("transform", Matrix4.translation(-80, -160, 50).multiply(Matrix4.rotateY((float) Math.PI / 4)).multiply(Matrix4.rotateX((float) -Math.PI / 2).multiply(Matrix4.scale(1.2f)))); - parameter("shaders", "gray_shader"); - instance("teapot.instance2", "teapot"); - } - - private void sphere(String name, String shaderName, float x, float y, float z, float radius) { - geometry(name, "sphere"); - parameter("transform", Matrix4.translation(x, y, z).multiply(Matrix4.scale(radius))); - parameter("shaders", shaderName); - instance(name + ".instance", name); - } - } - - public void kernelBegin() { - // allocate a fresh validation target - validationImage = new int[resolution * resolution]; - } - - public void kernelMain() { - // this builds and renders the scene - new BenchmarkScene(); - } - - public void kernelEnd() { - // make sure the rendered image was correct - int diff = 0; - if (referenceImage != null && validationImage.length == referenceImage.length) { - for (int i = 0; i < validationImage.length; i++) { - // count absolute RGB differences - diff += Math.abs((validationImage[i] & 0xFF) - (referenceImage[i] & 0xFF)); - diff += Math.abs(((validationImage[i] >> 8) & 0xFF) - ((referenceImage[i] >> 8) & 0xFF)); - diff += Math.abs(((validationImage[i] >> 16) & 0xFF) - ((referenceImage[i] >> 16) & 0xFF)); - } - if (diff > errorThreshold) - UI.printError(Module.BENCH, "Image check failed! - #errors: %d", diff); - else - UI.printInfo(Module.BENCH, "Image check passed!"); - } else - UI.printError(Module.BENCH, "Image check failed! - reference is not comparable"); - - } - - public void print(Module m, PrintLevel level, String s) { - if (showOutput || (showBenchmarkOutput && m == Module.BENCH)) - System.out.println(UI.formatOutput(m, level, s)); - if (level == PrintLevel.ERROR) - throw new RuntimeException(s); - } - - public void taskStart(String s, int min, int max) { - // render progress display not needed - } - - public void taskStop() { - // render progress display not needed - } - - public void taskUpdate(int current) { - // render progress display not needed - } - - public void imageBegin(int w, int h, int bucketSize) { - // we can assume w == h == resolution - } - - public void imageEnd() { - // nothing needs to be done - image verification is done externally - } - - public void imageFill(int x, int y, int w, int h, Color c, float alpha) { - // this is not used - } - - public void imagePrepare(int x, int y, int w, int h, int id) { - // this is not needed - } - - public void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha) { - // copy bucket data to validation image - for (int j = 0, index = 0; j < h; j++, y++) - for (int i = 0, offset = x + resolution * (resolution - 1 - y); i < w; i++, index++, offset++) - validationImage[offset] = data[index].copy().toNonLinear().toRGB(); - } -} \ No newline at end of file diff --git a/src/org/sunflow/BinaryFileSunflowAPI.java b/src/org/sunflow/BinaryFileSunflowAPI.java deleted file mode 100644 index 60ac622..0000000 --- a/src/org/sunflow/BinaryFileSunflowAPI.java +++ /dev/null @@ -1,225 +0,0 @@ -package org.sunflow; - -import java.io.BufferedOutputStream; -import java.io.DataOutputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; - -import org.sunflow.core.ParameterList.InterpolationType; -import org.sunflow.core.parser.SCAbstractParser.Keyword; -import org.sunflow.math.Matrix4; - -class BinaryFileSunflowAPI extends FileSunflowAPI { - private DataOutputStream stream; - - BinaryFileSunflowAPI(String filename) throws FileNotFoundException { - stream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(filename))); - } - - @Override - protected void writeBoolean(boolean value) { - try { - if (value) - stream.write(1); - else - stream.write(0); - } catch (IOException e) { - // throw as a silent exception to avoid having to propage throw - // declarations upwards - throw new RuntimeException(e.getMessage()); - } - } - - @Override - protected void writeFloat(float value) { - writeInt(Float.floatToRawIntBits(value)); - } - - @Override - protected void writeInt(int value) { - try { - // little endian, LSB first - stream.write(value & 0xFF); - stream.write((value >>> 8) & 0xFF); - stream.write((value >>> 16) & 0xFF); - stream.write((value >>> 24) & 0xFF); - } catch (IOException e) { - throw new RuntimeException(e.getMessage()); - } - } - - @Override - protected void writeInterpolationType(InterpolationType interp) { - try { - switch (interp) { - case NONE: - stream.write('n'); - break; - case VERTEX: - stream.write('v'); - break; - case FACE: - stream.write('p'); - break; - case FACEVARYING: - stream.write('f'); - break; - default: - throw new RuntimeException(String.format("Unknown interpolation type \"%s\"", interp.toString())); - } - } catch (IOException e) { - throw new RuntimeException(e.getMessage()); - } - } - - @Override - protected void writeKeyword(Keyword keyword) { - try { - switch (keyword) { - case RESET: - writeExtendedKeyword('R'); - break; - case PARAMETER: - stream.write('p'); - break; - case GEOMETRY: - stream.write('g'); - break; - case INSTANCE: - stream.write('i'); - break; - case SHADER: - stream.write('s'); - break; - case MODIFIER: - stream.write('m'); - break; - case LIGHT: - stream.write('l'); - break; - case CAMERA: - stream.write('c'); - break; - case OPTIONS: - stream.write('o'); - break; - case INCLUDE: - writeExtendedKeyword('i'); - break; - case REMOVE: - writeExtendedKeyword('r'); - break; - case FRAME: - writeExtendedKeyword('f'); - break; - case PLUGIN: - writeExtendedKeyword('p'); - break; - case SEARCHPATH: - writeExtendedKeyword('s'); - break; - case STRING: - writeDatatypeKeyword('s', false); - break; - case BOOL: - writeDatatypeKeyword('b', false); - break; - case INT: - writeDatatypeKeyword('i', false); - break; - case FLOAT: - writeDatatypeKeyword('f', false); - break; - case COLOR: - writeDatatypeKeyword('c', false); - break; - case POINT: - writeDatatypeKeyword('p', false); - break; - case VECTOR: - writeDatatypeKeyword('v', false); - break; - case TEXCOORD: - writeDatatypeKeyword('t', false); - break; - case MATRIX: - writeDatatypeKeyword('m', false); - break; - case STRING_ARRAY: - writeDatatypeKeyword('s', true); - break; - case INT_ARRAY: - writeDatatypeKeyword('i', true); - break; - case FLOAT_ARRAY: - writeDatatypeKeyword('f', true); - break; - case POINT_ARRAY: - writeDatatypeKeyword('p', true); - break; - case VECTOR_ARRAY: - writeDatatypeKeyword('v', true); - break; - case TEXCOORD_ARRAY: - writeDatatypeKeyword('t', true); - break; - case MATRIX_ARRAY: - writeDatatypeKeyword('m', true); - break; - default: - throw new RuntimeException(String.format("Unknown keyword \"%s\" requested", keyword.toString())); - } - } catch (IOException e) { - throw new RuntimeException(e.getMessage()); - } - } - - private void writeExtendedKeyword(int code) throws IOException { - stream.write('x'); - stream.write(code); - } - - // helper routine for datatype keywords - private void writeDatatypeKeyword(int type, boolean isArray) throws IOException { - stream.write('t'); - stream.write(type); - writeBoolean(isArray); - } - - @Override - protected void writeMatrix(Matrix4 value) { - for (float f : value.asRowMajor()) - writeFloat(f); - } - - @Override - protected void writeString(String string) { - try { - byte[] data = string.getBytes("UTF-8"); - writeInt(data.length); - stream.write(data); - } catch (Exception e) { - throw new RuntimeException(e.getMessage()); - } - } - - @Override - protected void writeVerbatimString(String string) { - writeString(string); - } - - @Override - protected void writeNewline(int indentNext) { - // does nothing - } - - @Override - public void close() { - try { - stream.close(); - } catch (IOException e) { - throw new RuntimeException(e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/FileSunflowAPI.java b/src/org/sunflow/FileSunflowAPI.java deleted file mode 100644 index f6a2f50..0000000 --- a/src/org/sunflow/FileSunflowAPI.java +++ /dev/null @@ -1,315 +0,0 @@ -package org.sunflow; - -import java.util.Locale; - -import org.sunflow.core.Display; -import org.sunflow.core.ParameterList.InterpolationType; -import org.sunflow.core.parser.SCAbstractParser.Keyword; -import org.sunflow.image.ColorFactory; -import org.sunflow.math.Matrix4; -import org.sunflow.math.Point2; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -abstract class FileSunflowAPI implements SunflowAPIInterface { - private int frame; - - protected FileSunflowAPI() { - frame = 1; - reset(); - } - - public void camera(String name, String lensType) { - writeKeyword(Keyword.CAMERA); - writeString(name); - writeString(lensType); - writeNewline(0); - writeNewline(0); - } - - public void geometry(String name, String typeName) { - writeKeyword(Keyword.GEOMETRY); - writeString(name); - writeString(typeName); - writeNewline(0); - writeNewline(0); - } - - public int getCurrentFrame() { - return frame; - } - - public void instance(String name, String geoname) { - writeKeyword(Keyword.INSTANCE); - writeString(name); - writeString(geoname); - writeNewline(0); - writeNewline(0); - } - - public void light(String name, String lightType) { - writeKeyword(Keyword.LIGHT); - writeString(name); - writeString(lightType); - writeNewline(0); - writeNewline(0); - } - - public void modifier(String name, String modifierType) { - writeKeyword(Keyword.MODIFIER); - writeString(name); - writeString(modifierType); - writeNewline(0); - writeNewline(0); - } - - public void options(String name) { - writeKeyword(Keyword.OPTIONS); - writeString(name); - writeNewline(0); - writeNewline(0); - } - - public void parameter(String name, String value) { - writeKeyword(Keyword.PARAMETER); - writeString(name); - writeKeyword(Keyword.STRING); - writeString(value); - writeNewline(0); - } - - public void parameter(String name, boolean value) { - writeKeyword(Keyword.PARAMETER); - writeString(name); - writeKeyword(Keyword.BOOL); - writeBoolean(value); - writeNewline(0); - } - - public void parameter(String name, int value) { - writeKeyword(Keyword.PARAMETER); - writeString(name); - writeKeyword(Keyword.INT); - writeInt(value); - writeNewline(0); - } - - public void parameter(String name, float value) { - writeKeyword(Keyword.PARAMETER); - writeString(name); - writeKeyword(Keyword.FLOAT); - writeFloat(value); - writeNewline(0); - } - - public void parameter(String name, String colorspace, float... data) { - writeKeyword(Keyword.PARAMETER); - writeString(name); - writeKeyword(Keyword.COLOR); - if (colorspace == null) - writeString(colorspace = ColorFactory.getInternalColorspace()); - else - writeString(colorspace); - if (ColorFactory.getRequiredDataValues(colorspace) == -1) - writeInt(data.length); - int idx = 0; - int step = 9; - for (float f : data) { - if (data.length > step && idx % step == 0) - writeNewline(1); - writeFloat(f); - idx++; - } - writeNewline(0); - } - - public void parameter(String name, Point3 value) { - writeKeyword(Keyword.PARAMETER); - writeString(name); - writeKeyword(Keyword.POINT); - writeFloat(value.x); - writeFloat(value.y); - writeFloat(value.z); - writeNewline(0); - } - - public void parameter(String name, Vector3 value) { - writeKeyword(Keyword.PARAMETER); - writeString(name); - writeKeyword(Keyword.VECTOR); - writeFloat(value.x); - writeFloat(value.y); - writeFloat(value.z); - writeNewline(0); - } - - public void parameter(String name, Point2 value) { - writeKeyword(Keyword.PARAMETER); - writeString(name); - writeKeyword(Keyword.TEXCOORD); - writeFloat(value.x); - writeFloat(value.y); - writeNewline(0); - } - - public void parameter(String name, Matrix4 value) { - writeKeyword(Keyword.PARAMETER); - writeString(name); - writeKeyword(Keyword.MATRIX); - writeMatrix(value); - writeNewline(0); - } - - public void parameter(String name, int[] value) { - writeKeyword(Keyword.PARAMETER); - writeString(name); - writeKeyword(Keyword.INT_ARRAY); - writeInt(value.length); - int idx = 0; - int step = 9; - for (int v : value) { - if (idx % step == 0) - writeNewline(1); - writeInt(v); - idx++; - } - writeNewline(0); - } - - public void parameter(String name, String[] value) { - writeKeyword(Keyword.PARAMETER); - writeString(name); - writeKeyword(Keyword.STRING_ARRAY); - writeInt(value.length); - for (String v : value) { - writeNewline(1); - writeString(v); - } - writeNewline(0); - } - - public void parameter(String name, String type, String interpolation, float[] data) { - InterpolationType interp; - try { - interp = InterpolationType.valueOf(interpolation.toUpperCase(Locale.ENGLISH)); - } catch (IllegalArgumentException e) { - UI.printError(Module.API, "Unknown interpolation type: %s -- ignoring parameter \"%s\"", interpolation, name); - return; - } - Keyword typeKeyword; - int lengthFactor; - if (type.equals("float")) { - typeKeyword = Keyword.FLOAT_ARRAY; - lengthFactor = 1; - } else if (type.equals("point")) { - typeKeyword = Keyword.POINT_ARRAY; - lengthFactor = 3; - } else if (type.equals("vector")) { - typeKeyword = Keyword.VECTOR_ARRAY; - lengthFactor = 3; - } else if (type.equals("texcoord")) { - typeKeyword = Keyword.TEXCOORD_ARRAY; - lengthFactor = 2; - } else if (type.equals("matrix")) { - typeKeyword = Keyword.MATRIX_ARRAY; - lengthFactor = 16; - } else { - UI.printError(Module.API, "Unknown parameter type: %s -- ignoring parameter \"%s\"", type, name); - return; - } - writeKeyword(Keyword.PARAMETER); - - writeString(name); - writeKeyword(typeKeyword); - writeInterpolationType(interp); - writeInt(data.length / lengthFactor); - int idx = 0; - if (data.length > 16) - lengthFactor *= 8; - for (float v : data) { - if (lengthFactor > 1 && idx % lengthFactor == 0) - writeNewline(1); - writeFloat(v); - idx++; - } - writeNewline(0); - } - - public boolean include(String filename) { - writeKeyword(Keyword.INCLUDE); - writeString(filename); - writeNewline(0); - writeNewline(0); - return true; - } - - public void plugin(String type, String name, String code) { - writeKeyword(Keyword.PLUGIN); - writeString(type); - writeString(name); - writeVerbatimString(code); - writeNewline(0); - writeNewline(0); - } - - public void remove(String name) { - writeKeyword(Keyword.REMOVE); - writeString(name); - writeNewline(0); - writeNewline(0); - } - - public void render(String optionsName, Display display) { - UI.printWarning(Module.API, "Unable to render file stream"); - } - - public void reset() { - frame = 1; - } - - public void searchpath(String type, String path) { - writeKeyword(Keyword.SEARCHPATH); - writeString(type); - writeString(path); - writeNewline(0); - writeNewline(0); - - } - - public void currentFrame(int currentFrame) { - writeKeyword(Keyword.FRAME); - writeInt(frame = currentFrame); - writeNewline(0); - writeNewline(0); - } - - public void shader(String name, String shaderType) { - writeKeyword(Keyword.SHADER); - writeString(name); - writeString(shaderType); - writeNewline(0); - writeNewline(0); - } - - protected abstract void writeKeyword(Keyword keyword); - - protected abstract void writeInterpolationType(InterpolationType interp); - - protected abstract void writeBoolean(boolean value); - - protected abstract void writeInt(int value); - - protected abstract void writeFloat(float value); - - protected abstract void writeString(String string); - - protected abstract void writeVerbatimString(String string); - - protected abstract void writeMatrix(Matrix4 value); - - protected abstract void writeNewline(int indentNext); - - public abstract void close(); -} \ No newline at end of file diff --git a/src/org/sunflow/PluginRegistry.java b/src/org/sunflow/PluginRegistry.java deleted file mode 100644 index def4853..0000000 --- a/src/org/sunflow/PluginRegistry.java +++ /dev/null @@ -1,322 +0,0 @@ -package org.sunflow; - -import org.sunflow.core.AccelerationStructure; -import org.sunflow.core.BucketOrder; -import org.sunflow.core.CameraLens; -import org.sunflow.core.CausticPhotonMapInterface; -import org.sunflow.core.Filter; -import org.sunflow.core.GIEngine; -import org.sunflow.core.GlobalPhotonMapInterface; -import org.sunflow.core.ImageSampler; -import org.sunflow.core.LightSource; -import org.sunflow.core.Modifier; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.SceneParser; -import org.sunflow.core.Shader; -import org.sunflow.core.Tesselatable; -import org.sunflow.core.accel.BoundingIntervalHierarchy; -import org.sunflow.core.accel.KDTree; -import org.sunflow.core.accel.NullAccelerator; -import org.sunflow.core.accel.UniformGrid; -import org.sunflow.core.bucket.ColumnBucketOrder; -import org.sunflow.core.bucket.DiagonalBucketOrder; -import org.sunflow.core.bucket.HilbertBucketOrder; -import org.sunflow.core.bucket.RandomBucketOrder; -import org.sunflow.core.bucket.RowBucketOrder; -import org.sunflow.core.bucket.SpiralBucketOrder; -import org.sunflow.core.camera.FisheyeLens; -import org.sunflow.core.camera.PinholeLens; -import org.sunflow.core.camera.SphericalLens; -import org.sunflow.core.camera.ThinLens; -import org.sunflow.core.filter.BlackmanHarrisFilter; -import org.sunflow.core.filter.BoxFilter; -import org.sunflow.core.filter.CatmullRomFilter; -import org.sunflow.core.filter.CubicBSpline; -import org.sunflow.core.filter.GaussianFilter; -import org.sunflow.core.filter.LanczosFilter; -import org.sunflow.core.filter.MitchellFilter; -import org.sunflow.core.filter.SincFilter; -import org.sunflow.core.filter.TriangleFilter; -import org.sunflow.core.gi.AmbientOcclusionGIEngine; -import org.sunflow.core.gi.FakeGIEngine; -import org.sunflow.core.gi.InstantGI; -import org.sunflow.core.gi.IrradianceCacheGIEngine; -import org.sunflow.core.gi.PathTracingGIEngine; -import org.sunflow.core.light.DirectionalSpotlight; -import org.sunflow.core.light.ImageBasedLight; -import org.sunflow.core.light.PointLight; -import org.sunflow.core.light.SphereLight; -import org.sunflow.core.light.SunSkyLight; -import org.sunflow.core.light.TriangleMeshLight; -import org.sunflow.core.modifiers.BumpMappingModifier; -import org.sunflow.core.modifiers.NormalMapModifier; -import org.sunflow.core.modifiers.PerlinModifier; -import org.sunflow.core.parser.RA2Parser; -import org.sunflow.core.parser.RA3Parser; -import org.sunflow.core.parser.SCAsciiParser; -import org.sunflow.core.parser.SCBinaryParser; -import org.sunflow.core.parser.SCParser; -import org.sunflow.core.parser.ShaveRibParser; -import org.sunflow.core.photonmap.CausticPhotonMap; -import org.sunflow.core.photonmap.GlobalPhotonMap; -import org.sunflow.core.photonmap.GridPhotonMap; -import org.sunflow.core.primitive.Background; -import org.sunflow.core.primitive.BanchoffSurface; -import org.sunflow.core.primitive.Box; -import org.sunflow.core.primitive.CornellBox; -import org.sunflow.core.primitive.Cylinder; -import org.sunflow.core.primitive.Hair; -import org.sunflow.core.primitive.JuliaFractal; -import org.sunflow.core.primitive.ParticleSurface; -import org.sunflow.core.primitive.Plane; -import org.sunflow.core.primitive.QuadMesh; -import org.sunflow.core.primitive.Sphere; -import org.sunflow.core.primitive.SphereFlake; -import org.sunflow.core.primitive.Torus; -import org.sunflow.core.primitive.TriangleMesh; -import org.sunflow.core.renderer.BucketRenderer; -import org.sunflow.core.renderer.MultipassRenderer; -import org.sunflow.core.renderer.ProgressiveRenderer; -import org.sunflow.core.renderer.SimpleRenderer; -import org.sunflow.core.shader.AmbientOcclusionShader; -import org.sunflow.core.shader.AnisotropicWardShader; -import org.sunflow.core.shader.ConstantShader; -import org.sunflow.core.shader.DiffuseShader; -import org.sunflow.core.shader.GlassShader; -import org.sunflow.core.shader.IDShader; -import org.sunflow.core.shader.MirrorShader; -import org.sunflow.core.shader.NormalShader; -import org.sunflow.core.shader.PhongShader; -import org.sunflow.core.shader.PrimIDShader; -import org.sunflow.core.shader.QuickGrayShader; -import org.sunflow.core.shader.ShinyDiffuseShader; -import org.sunflow.core.shader.SimpleShader; -import org.sunflow.core.shader.TexturedAmbientOcclusionShader; -import org.sunflow.core.shader.TexturedDiffuseShader; -import org.sunflow.core.shader.TexturedPhongShader; -import org.sunflow.core.shader.TexturedShinyDiffuseShader; -import org.sunflow.core.shader.TexturedWardShader; -import org.sunflow.core.shader.UVShader; -import org.sunflow.core.shader.UberShader; -import org.sunflow.core.shader.ViewCausticsShader; -import org.sunflow.core.shader.ViewGlobalPhotonsShader; -import org.sunflow.core.shader.ViewIrradianceShader; -import org.sunflow.core.shader.WireframeShader; -import org.sunflow.core.tesselatable.BezierMesh; -import org.sunflow.core.tesselatable.FileMesh; -import org.sunflow.core.tesselatable.Gumbo; -import org.sunflow.core.tesselatable.Teapot; -import org.sunflow.image.BitmapReader; -import org.sunflow.image.BitmapWriter; -import org.sunflow.image.readers.BMPBitmapReader; -import org.sunflow.image.readers.HDRBitmapReader; -import org.sunflow.image.readers.IGIBitmapReader; -import org.sunflow.image.readers.JPGBitmapReader; -import org.sunflow.image.readers.PNGBitmapReader; -import org.sunflow.image.readers.TGABitmapReader; -import org.sunflow.image.writers.EXRBitmapWriter; -import org.sunflow.image.writers.HDRBitmapWriter; -import org.sunflow.image.writers.IGIBitmapWriter; -import org.sunflow.image.writers.PNGBitmapWriter; -import org.sunflow.image.writers.TGABitmapWriter; -import org.sunflow.system.Plugins; - -/** - * This class acts as the central repository for all user extensible types in - * Sunflow, even built-in types are registered here. This class is static so - * that new plugins may be reused by an application across several render - * scenes. - */ -public final class PluginRegistry { - // base types - needed by SunflowAPI - public static final Plugins primitivePlugins = new Plugins(PrimitiveList.class); - public static final Plugins tesselatablePlugins = new Plugins(Tesselatable.class); - public static final Plugins shaderPlugins = new Plugins(Shader.class); - public static final Plugins modifierPlugins = new Plugins(Modifier.class); - public static final Plugins lightSourcePlugins = new Plugins(LightSource.class); - public static final Plugins cameraLensPlugins = new Plugins(CameraLens.class); - - // advanced types - used inside the Sunflow core - public static final Plugins accelPlugins = new Plugins(AccelerationStructure.class); - public static final Plugins bucketOrderPlugins = new Plugins(BucketOrder.class); - public static final Plugins filterPlugins = new Plugins(Filter.class); - public static final Plugins giEnginePlugins = new Plugins(GIEngine.class); - public static final Plugins causticPhotonMapPlugins = new Plugins(CausticPhotonMapInterface.class); - public static final Plugins globalPhotonMapPlugins = new Plugins(GlobalPhotonMapInterface.class); - public static final Plugins imageSamplerPlugins = new Plugins(ImageSampler.class); - public static final Plugins parserPlugins = new Plugins(SceneParser.class); - public static final Plugins bitmapReaderPlugins = new Plugins(BitmapReader.class); - public static final Plugins bitmapWriterPlugins = new Plugins(BitmapWriter.class); - - // Register all plugins on startup: - static { - // primitives - primitivePlugins.registerPlugin("triangle_mesh", TriangleMesh.class); - primitivePlugins.registerPlugin("sphere", Sphere.class); - primitivePlugins.registerPlugin("cylinder", Cylinder.class); - primitivePlugins.registerPlugin("box", Box.class); - primitivePlugins.registerPlugin("banchoff", BanchoffSurface.class); - primitivePlugins.registerPlugin("hair", Hair.class); - primitivePlugins.registerPlugin("julia", JuliaFractal.class); - primitivePlugins.registerPlugin("particles", ParticleSurface.class); - primitivePlugins.registerPlugin("plane", Plane.class); - primitivePlugins.registerPlugin("quad_mesh", QuadMesh.class); - primitivePlugins.registerPlugin("torus", Torus.class); - primitivePlugins.registerPlugin("background", Background.class); - primitivePlugins.registerPlugin("sphereflake", SphereFlake.class); - } - - static { - // tesslatable - tesselatablePlugins.registerPlugin("bezier_mesh", BezierMesh.class); - tesselatablePlugins.registerPlugin("file_mesh", FileMesh.class); - tesselatablePlugins.registerPlugin("gumbo", Gumbo.class); - tesselatablePlugins.registerPlugin("teapot", Teapot.class); - } - - static { - // shaders - shaderPlugins.registerPlugin("ambient_occlusion", AmbientOcclusionShader.class); - shaderPlugins.registerPlugin("constant", ConstantShader.class); - shaderPlugins.registerPlugin("diffuse", DiffuseShader.class); - shaderPlugins.registerPlugin("glass", GlassShader.class); - shaderPlugins.registerPlugin("mirror", MirrorShader.class); - shaderPlugins.registerPlugin("phong", PhongShader.class); - shaderPlugins.registerPlugin("shiny_diffuse", ShinyDiffuseShader.class); - shaderPlugins.registerPlugin("uber", UberShader.class); - shaderPlugins.registerPlugin("ward", AnisotropicWardShader.class); - shaderPlugins.registerPlugin("wireframe", WireframeShader.class); - - // textured shaders - shaderPlugins.registerPlugin("textured_ambient_occlusion", TexturedAmbientOcclusionShader.class); - shaderPlugins.registerPlugin("textured_diffuse", TexturedDiffuseShader.class); - shaderPlugins.registerPlugin("textured_phong", TexturedPhongShader.class); - shaderPlugins.registerPlugin("textured_shiny_diffuse", TexturedShinyDiffuseShader.class); - shaderPlugins.registerPlugin("textured_ward", TexturedWardShader.class); - - // preview shaders - shaderPlugins.registerPlugin("quick_gray", QuickGrayShader.class); - shaderPlugins.registerPlugin("simple", SimpleShader.class); - shaderPlugins.registerPlugin("show_normals", NormalShader.class); - shaderPlugins.registerPlugin("show_uvs", UVShader.class); - shaderPlugins.registerPlugin("show_instance_id", IDShader.class); - shaderPlugins.registerPlugin("show_primitive_id", PrimIDShader.class); - shaderPlugins.registerPlugin("view_caustics", ViewCausticsShader.class); - shaderPlugins.registerPlugin("view_global", ViewGlobalPhotonsShader.class); - shaderPlugins.registerPlugin("view_irradiance", ViewIrradianceShader.class); - } - - static { - // modifiers - modifierPlugins.registerPlugin("bump_map", BumpMappingModifier.class); - modifierPlugins.registerPlugin("normal_map", NormalMapModifier.class); - modifierPlugins.registerPlugin("perlin", PerlinModifier.class); - } - - static { - // light sources - lightSourcePlugins.registerPlugin("directional", DirectionalSpotlight.class); - lightSourcePlugins.registerPlugin("ibl", ImageBasedLight.class); - lightSourcePlugins.registerPlugin("point", PointLight.class); - lightSourcePlugins.registerPlugin("sphere", SphereLight.class); - lightSourcePlugins.registerPlugin("sunsky", SunSkyLight.class); - lightSourcePlugins.registerPlugin("triangle_mesh", TriangleMeshLight.class); - lightSourcePlugins.registerPlugin("cornell_box", CornellBox.class); - } - - static { - // camera lenses - cameraLensPlugins.registerPlugin("pinhole", PinholeLens.class); - cameraLensPlugins.registerPlugin("thinlens", ThinLens.class); - cameraLensPlugins.registerPlugin("fisheye", FisheyeLens.class); - cameraLensPlugins.registerPlugin("spherical", SphericalLens.class); - } - - static { - // accels - accelPlugins.registerPlugin("bih", BoundingIntervalHierarchy.class); - accelPlugins.registerPlugin("kdtree", KDTree.class); - accelPlugins.registerPlugin("null", NullAccelerator.class); - accelPlugins.registerPlugin("uniformgrid", UniformGrid.class); - } - - static { - // bucket orders - bucketOrderPlugins.registerPlugin("column", ColumnBucketOrder.class); - bucketOrderPlugins.registerPlugin("diagonal", DiagonalBucketOrder.class); - bucketOrderPlugins.registerPlugin("hilbert", HilbertBucketOrder.class); - bucketOrderPlugins.registerPlugin("random", RandomBucketOrder.class); - bucketOrderPlugins.registerPlugin("row", RowBucketOrder.class); - bucketOrderPlugins.registerPlugin("spiral", SpiralBucketOrder.class); - } - - static { - // filters - filterPlugins.registerPlugin("blackman-harris", BlackmanHarrisFilter.class); - filterPlugins.registerPlugin("box", BoxFilter.class); - filterPlugins.registerPlugin("catmull-rom", CatmullRomFilter.class); - filterPlugins.registerPlugin("gaussian", GaussianFilter.class); - filterPlugins.registerPlugin("lanczos", LanczosFilter.class); - filterPlugins.registerPlugin("mitchell", MitchellFilter.class); - filterPlugins.registerPlugin("sinc", SincFilter.class); - filterPlugins.registerPlugin("triangle", TriangleFilter.class); - filterPlugins.registerPlugin("bspline", CubicBSpline.class); - } - - static { - // gi engines - giEnginePlugins.registerPlugin("ambocc", AmbientOcclusionGIEngine.class); - giEnginePlugins.registerPlugin("fake", FakeGIEngine.class); - giEnginePlugins.registerPlugin("igi", InstantGI.class); - giEnginePlugins.registerPlugin("irr-cache", IrradianceCacheGIEngine.class); - giEnginePlugins.registerPlugin("path", PathTracingGIEngine.class); - } - - static { - // caustic photon maps - causticPhotonMapPlugins.registerPlugin("kd", CausticPhotonMap.class); - } - - static { - // global photon maps - globalPhotonMapPlugins.registerPlugin("grid", GridPhotonMap.class); - globalPhotonMapPlugins.registerPlugin("kd", GlobalPhotonMap.class); - } - - static { - // image samplers - imageSamplerPlugins.registerPlugin("bucket", BucketRenderer.class); - imageSamplerPlugins.registerPlugin("ipr", ProgressiveRenderer.class); - imageSamplerPlugins.registerPlugin("fast", SimpleRenderer.class); - imageSamplerPlugins.registerPlugin("multipass", MultipassRenderer.class); - } - - static { - // parsers - parserPlugins.registerPlugin("sc", SCParser.class); - parserPlugins.registerPlugin("sca", SCAsciiParser.class); - parserPlugins.registerPlugin("scb", SCBinaryParser.class); - parserPlugins.registerPlugin("rib", ShaveRibParser.class); - parserPlugins.registerPlugin("ra2", RA2Parser.class); - parserPlugins.registerPlugin("ra3", RA3Parser.class); - } - - static { - // bitmap readers - bitmapReaderPlugins.registerPlugin("hdr", HDRBitmapReader.class); - bitmapReaderPlugins.registerPlugin("tga", TGABitmapReader.class); - bitmapReaderPlugins.registerPlugin("png", PNGBitmapReader.class); - bitmapReaderPlugins.registerPlugin("jpg", JPGBitmapReader.class); - bitmapReaderPlugins.registerPlugin("bmp", BMPBitmapReader.class); - bitmapReaderPlugins.registerPlugin("igi", IGIBitmapReader.class); - } - - static { - // bitmap writers - bitmapWriterPlugins.registerPlugin("png", PNGBitmapWriter.class); - bitmapWriterPlugins.registerPlugin("hdr", HDRBitmapWriter.class); - bitmapWriterPlugins.registerPlugin("tga", TGABitmapWriter.class); - bitmapWriterPlugins.registerPlugin("exr", EXRBitmapWriter.class); - bitmapWriterPlugins.registerPlugin("igi", IGIBitmapWriter.class); - } -} \ No newline at end of file diff --git a/src/org/sunflow/RealtimeBenchmark.java b/src/org/sunflow/RealtimeBenchmark.java deleted file mode 100644 index af20648..0000000 --- a/src/org/sunflow/RealtimeBenchmark.java +++ /dev/null @@ -1,123 +0,0 @@ -package org.sunflow; - -import org.sunflow.core.Display; -import org.sunflow.core.display.FastDisplay; -import org.sunflow.core.display.FileDisplay; -import org.sunflow.math.Matrix4; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; -import org.sunflow.system.ui.ConsoleInterface; - -public class RealtimeBenchmark extends SunflowAPI { - public RealtimeBenchmark(boolean showGUI, int threads) { - Display display = showGUI ? new FastDisplay() : new FileDisplay(false); - UI.printInfo(Module.BENCH, "Preparing benchmarking scene ..."); - // settings - parameter("threads", threads); - // spawn regular priority threads - parameter("threads.lowPriority", false); - parameter("resolutionX", 512); - parameter("resolutionY", 512); - parameter("aa.min", -3); - parameter("aa.max", 0); - parameter("depths.diffuse", 1); - parameter("depths.reflection", 1); - parameter("depths.refraction", 0); - parameter("bucket.order", "hilbert"); - parameter("bucket.size", 32); - options(SunflowAPI.DEFAULT_OPTIONS); - // camera - Point3 eye = new Point3(30, 0, 10.967f); - Point3 target = new Point3(0, 0, 5.4f); - Vector3 up = new Vector3(0, 0, 1); - parameter("transform", Matrix4.lookAt(eye, target, up)); - parameter("fov", 45.0f); - camera("camera", "pinhole"); - parameter("camera", "camera"); - options(SunflowAPI.DEFAULT_OPTIONS); - // geometry - createGeometry(); - // this first render is not timed, it caches the acceleration data - // structures and tesselations so they won't be - // included in the main timing - UI.printInfo(Module.BENCH, "Rendering warmup frame ..."); - render(SunflowAPI.DEFAULT_OPTIONS, display); - // now disable all output - and run the benchmark - UI.set(null); - Timer t = new Timer(); - t.start(); - float phi = 0; - int frames = 0; - while (phi < 4 * Math.PI) { - eye.x = 30 * (float) Math.cos(phi); - eye.y = 30 * (float) Math.sin(phi); - phi += Math.PI / 30; - frames++; - // update camera - parameter("transform", Matrix4.lookAt(eye, target, up)); - camera("camera", null); - render(SunflowAPI.DEFAULT_OPTIONS, display); - } - t.end(); - UI.set(new ConsoleInterface()); - UI.printInfo(Module.BENCH, "Benchmark results:"); - UI.printInfo(Module.BENCH, " * Average FPS: %.2f", frames / t.seconds()); - UI.printInfo(Module.BENCH, " * Total time: %s", t); - } - - private void createGeometry() { - // light source - parameter("source", new Point3(-15.5945f, -30.0581f, 45.967f)); - parameter("dir", new Vector3(15.5945f, 30.0581f, -45.967f)); - parameter("radius", 60.0f); - parameter("radiance", null, 3, 3, 3); - light("light", "directional"); - - // gi-engine - parameter("gi.engine", "fake"); - parameter("gi.fake.sky", null, 0.25f, 0.25f, 0.25f); - parameter("gi.fake.ground", null, 0.01f, 0.01f, 0.5f); - parameter("gi.fake.up", new Vector3(0, 0, 1)); - options(DEFAULT_OPTIONS); - - // shaders - parameter("diffuse", null, 0.5f, 0.5f, 0.5f); - shader("default", "diffuse"); - parameter("diffuse", null, 0.5f, 0.5f, 0.5f); - parameter("shiny", 0.2f); - shader("refl", "shiny_diffuse"); - // objects - - // teapot - parameter("subdivs", 10); - geometry("teapot", "teapot"); - parameter("shaders", "default"); - Matrix4 m = Matrix4.IDENTITY; - m = Matrix4.scale(0.075f).multiply(m); - m = Matrix4.rotateZ((float) Math.toRadians(-45f)).multiply(m); - m = Matrix4.translation(-7, 0, 0).multiply(m); - parameter("transform", m); - instance("teapot.instance", "teapot"); - - // gumbo - parameter("subdivs", 10); - geometry("gumbo", "gumbo"); - m = Matrix4.IDENTITY; - m = Matrix4.scale(0.5f).multiply(m); - m = Matrix4.rotateZ((float) Math.toRadians(25f)).multiply(m); - m = Matrix4.translation(3, -7, 0).multiply(m); - parameter("shaders", "default"); - parameter("transform", m); - instance("gumbo.instance", "gumbo"); - - // ground plane - parameter("center", new Point3(0, 0, 0)); - parameter("normal", new Vector3(0, 0, 1)); - geometry("ground", "plane"); - parameter("shaders", "refl"); - instance("ground.instance", "ground"); - } -} \ No newline at end of file diff --git a/src/org/sunflow/RenderObjectMap.java b/src/org/sunflow/RenderObjectMap.java deleted file mode 100644 index fa49cd2..0000000 --- a/src/org/sunflow/RenderObjectMap.java +++ /dev/null @@ -1,333 +0,0 @@ -package org.sunflow; - -import java.util.ArrayList; -import java.util.Locale; - -import org.sunflow.core.Camera; -import org.sunflow.core.Geometry; -import org.sunflow.core.Instance; -import org.sunflow.core.LightSource; -import org.sunflow.core.Modifier; -import org.sunflow.core.Options; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.RenderObject; -import org.sunflow.core.Scene; -import org.sunflow.core.Shader; -import org.sunflow.core.Tesselatable; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; -import org.sunflow.util.FastHashMap; - -final class RenderObjectMap { - private FastHashMap renderObjects; - private boolean rebuildInstanceList; - private boolean rebuildLightList; - - private enum RenderObjectType { - UNKNOWN, SHADER, MODIFIER, GEOMETRY, INSTANCE, LIGHT, CAMERA, OPTIONS - } - - RenderObjectMap() { - renderObjects = new FastHashMap(); - rebuildInstanceList = rebuildLightList = false; - } - - final boolean has(String name) { - return renderObjects.containsKey(name); - } - - final void remove(String name) { - RenderObjectHandle obj = renderObjects.get(name); - if (obj == null) { - UI.printWarning(Module.API, "Unable to remove \"%s\" - object was not defined yet"); - return; - } - UI.printDetailed(Module.API, "Removing object \"%s\"", name); - renderObjects.remove(name); - // scan through all objects to make sure we don't have any - // references to the old object still around - switch (obj.type) { - case SHADER: - Shader s = obj.getShader(); - for (FastHashMap.Entry e : renderObjects) { - Instance i = e.getValue().getInstance(); - if (i != null) { - UI.printWarning(Module.API, "Removing shader \"%s\" from instance \"%s\"", name, e.getKey()); - i.removeShader(s); - } - } - break; - case MODIFIER: - Modifier m = obj.getModifier(); - for (FastHashMap.Entry e : renderObjects) { - Instance i = e.getValue().getInstance(); - if (i != null) { - UI.printWarning(Module.API, "Removing modifier \"%s\" from instance \"%s\"", name, e.getKey()); - i.removeModifier(m); - } - } - break; - case GEOMETRY: { - Geometry g = obj.getGeometry(); - for (FastHashMap.Entry e : renderObjects) { - Instance i = e.getValue().getInstance(); - if (i != null && i.hasGeometry(g)) { - UI.printWarning(Module.API, "Removing instance \"%s\" because it referenced geometry \"%s\"", e.getKey(), name); - remove(e.getKey()); - } - } - break; - } - case INSTANCE: - rebuildInstanceList = true; - break; - case LIGHT: - rebuildLightList = true; - break; - default: - // no dependencies - break; - } - } - - final boolean update(String name, ParameterList pl, SunflowAPI api) { - RenderObjectHandle obj = renderObjects.get(name); - boolean success; - if (obj == null) { - UI.printError(Module.API, "Unable to update \"%s\" - object was not defined yet", name); - success = false; - } else { - UI.printDetailed(Module.API, "Updating %s object \"%s\"", obj.typeName(), name); - success = obj.update(pl, api); - if (!success) { - UI.printError(Module.API, "Unable to update \"%s\" - removing", name); - remove(name); - } else { - switch (obj.type) { - case GEOMETRY: - case INSTANCE: - rebuildInstanceList = true; - break; - case LIGHT: - rebuildLightList = true; - break; - default: - break; - } - } - } - return success; - } - - final void updateScene(Scene scene) { - if (rebuildInstanceList) { - UI.printInfo(Module.API, "Building scene instance list for rendering ..."); - int numInfinite = 0, numInstance = 0; - for (FastHashMap.Entry e : renderObjects) { - Instance i = e.getValue().getInstance(); - if (i != null) { - i.updateBounds(); - if (i.getBounds() == null) - numInfinite++; - else if (!i.getBounds().isEmpty()) - numInstance++; - else - UI.printWarning(Module.API, "Ignoring empty instance: \"%s\"", e.getKey()); - } - } - Instance[] infinite = new Instance[numInfinite]; - Instance[] instance = new Instance[numInstance]; - numInfinite = numInstance = 0; - for (FastHashMap.Entry e : renderObjects) { - Instance i = e.getValue().getInstance(); - if (i != null) { - if (i.getBounds() == null) { - infinite[numInfinite] = i; - numInfinite++; - } else if (!i.getBounds().isEmpty()) { - instance[numInstance] = i; - numInstance++; - } - } - } - scene.setInstanceLists(instance, infinite); - rebuildInstanceList = false; - } - if (rebuildLightList) { - UI.printInfo(Module.API, "Building scene light list for rendering ..."); - ArrayList lightList = new ArrayList(); - for (FastHashMap.Entry e : renderObjects) { - LightSource light = e.getValue().getLight(); - if (light != null) - lightList.add(light); - - } - scene.setLightList(lightList.toArray(new LightSource[lightList.size()])); - rebuildLightList = false; - } - } - - final void put(String name, Shader shader) { - renderObjects.put(name, new RenderObjectHandle(shader)); - } - - final void put(String name, Modifier modifier) { - renderObjects.put(name, new RenderObjectHandle(modifier)); - } - - final void put(String name, PrimitiveList primitives) { - renderObjects.put(name, new RenderObjectHandle(primitives)); - } - - final void put(String name, Tesselatable tesselatable) { - renderObjects.put(name, new RenderObjectHandle(tesselatable)); - } - - final void put(String name, Instance instance) { - renderObjects.put(name, new RenderObjectHandle(instance)); - } - - final void put(String name, LightSource light) { - renderObjects.put(name, new RenderObjectHandle(light)); - } - - final void put(String name, Camera camera) { - renderObjects.put(name, new RenderObjectHandle(camera)); - } - - final void put(String name, Options options) { - renderObjects.put(name, new RenderObjectHandle(options)); - } - - final Geometry lookupGeometry(String name) { - if (name == null) - return null; - RenderObjectHandle handle = renderObjects.get(name); - return (handle == null) ? null : handle.getGeometry(); - } - - final Instance lookupInstance(String name) { - if (name == null) - return null; - RenderObjectHandle handle = renderObjects.get(name); - return (handle == null) ? null : handle.getInstance(); - } - - final Camera lookupCamera(String name) { - if (name == null) - return null; - RenderObjectHandle handle = renderObjects.get(name); - return (handle == null) ? null : handle.getCamera(); - } - - final Options lookupOptions(String name) { - if (name == null) - return null; - RenderObjectHandle handle = renderObjects.get(name); - return (handle == null) ? null : handle.getOptions(); - } - - final Shader lookupShader(String name) { - if (name == null) - return null; - RenderObjectHandle handle = renderObjects.get(name); - return (handle == null) ? null : handle.getShader(); - } - - final Modifier lookupModifier(String name) { - if (name == null) - return null; - RenderObjectHandle handle = renderObjects.get(name); - return (handle == null) ? null : handle.getModifier(); - } - - final LightSource lookupLight(String name) { - if (name == null) - return null; - RenderObjectHandle handle = renderObjects.get(name); - return (handle == null) ? null : handle.getLight(); - } - - private static final class RenderObjectHandle { - private final RenderObject obj; - private final RenderObjectType type; - - private RenderObjectHandle(Shader shader) { - obj = shader; - type = RenderObjectType.SHADER; - } - - private RenderObjectHandle(Modifier modifier) { - obj = modifier; - type = RenderObjectType.MODIFIER; - } - - private RenderObjectHandle(Tesselatable tesselatable) { - obj = new Geometry(tesselatable); - type = RenderObjectType.GEOMETRY; - } - - private RenderObjectHandle(PrimitiveList prims) { - obj = new Geometry(prims); - type = RenderObjectType.GEOMETRY; - } - - private RenderObjectHandle(Instance instance) { - obj = instance; - type = RenderObjectType.INSTANCE; - } - - private RenderObjectHandle(LightSource light) { - obj = light; - type = RenderObjectType.LIGHT; - } - - private RenderObjectHandle(Camera camera) { - obj = camera; - type = RenderObjectType.CAMERA; - } - - private RenderObjectHandle(Options options) { - obj = options; - type = RenderObjectType.OPTIONS; - } - - private boolean update(ParameterList pl, SunflowAPI api) { - return obj.update(pl, api); - } - - private String typeName() { - return type.name().toLowerCase(Locale.ENGLISH); - } - - private Shader getShader() { - return (type == RenderObjectType.SHADER) ? (Shader) obj : null; - } - - private Modifier getModifier() { - return (type == RenderObjectType.MODIFIER) ? (Modifier) obj : null; - } - - private Geometry getGeometry() { - return (type == RenderObjectType.GEOMETRY) ? (Geometry) obj : null; - } - - private Instance getInstance() { - return (type == RenderObjectType.INSTANCE) ? (Instance) obj : null; - } - - private LightSource getLight() { - return (type == RenderObjectType.LIGHT) ? (LightSource) obj : null; - } - - private Camera getCamera() { - return (type == RenderObjectType.CAMERA) ? (Camera) obj : null; - } - - private Options getOptions() { - return (type == RenderObjectType.OPTIONS) ? (Options) obj : null; - } - } - -} \ No newline at end of file diff --git a/src/org/sunflow/SunflowAPI.java b/src/org/sunflow/SunflowAPI.java deleted file mode 100644 index 49ea83a..0000000 --- a/src/org/sunflow/SunflowAPI.java +++ /dev/null @@ -1,700 +0,0 @@ -package org.sunflow; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.StringReader; -import java.util.Locale; - -import org.codehaus.janino.ClassBodyEvaluator; -import org.codehaus.janino.CompileException; -import org.codehaus.janino.Scanner; -import org.codehaus.janino.Parser.ParseException; -import org.codehaus.janino.Scanner.ScanException; -import org.sunflow.core.Camera; -import org.sunflow.core.CameraLens; -import org.sunflow.core.Display; -import org.sunflow.core.Geometry; -import org.sunflow.core.ImageSampler; -import org.sunflow.core.Instance; -import org.sunflow.core.LightSource; -import org.sunflow.core.Modifier; -import org.sunflow.core.Options; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Scene; -import org.sunflow.core.SceneParser; -import org.sunflow.core.Shader; -import org.sunflow.core.Tesselatable; -import org.sunflow.core.ParameterList.InterpolationType; -import org.sunflow.image.ColorFactory; -import org.sunflow.image.ColorFactory.ColorSpecificationException; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.Point2; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.FileUtils; -import org.sunflow.system.SearchPath; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -/** - * This API gives a simple interface for creating scenes procedurally. This is - * the main entry point to Sunflow. To use this class, extend from it and - * implement the build method which may execute arbitrary code to create a - * scene. - */ -public class SunflowAPI implements SunflowAPIInterface { - public static final String VERSION = "0.07.3"; - public static final String DEFAULT_OPTIONS = "::options"; - - private Scene scene; - private SearchPath includeSearchPath; - private SearchPath textureSearchPath; - private ParameterList parameterList; - private RenderObjectMap renderObjects; - private int currentFrame; - - /** - * This is a quick system test which verifies that the user has launched - * Java properly. - */ - public static void runSystemCheck() { - final long RECOMMENDED_MAX_SIZE = 800; - long maxMb = Runtime.getRuntime().maxMemory() / 1048576; - if (maxMb < RECOMMENDED_MAX_SIZE) - UI.printError(Module.API, "JVM available memory is below %d MB (found %d MB only).\nPlease make sure you launched the program with the -Xmx command line options.", RECOMMENDED_MAX_SIZE, maxMb); - String compiler = System.getProperty("java.vm.name"); - if (compiler == null || !(compiler.contains("HotSpot") && compiler.contains("Server"))) - UI.printError(Module.API, "You do not appear to be running Sun's server JVM\nPerformance may suffer"); - UI.printDetailed(Module.API, "Java environment settings:"); - UI.printDetailed(Module.API, " * Max memory available : %d MB", maxMb); - UI.printDetailed(Module.API, " * Virtual machine name : %s", compiler == null ? "true if the update was succesfull, or - * false if the update failed - */ - private boolean update(String name) { - boolean success = renderObjects.update(name, parameterList, this); - parameterList.clear(success); - return success; - } - - public final void searchpath(String type, String path) { - if (type.equals("include")) - includeSearchPath.addSearchPath(path); - else if (type.equals("texture")) - textureSearchPath.addSearchPath(path); - else - UI.printWarning(Module.API, "Invalid searchpath type: \"%s\"", type); - } - - /** - * Attempts to resolve the specified filename by checking it against the - * texture search path. - * - * @param filename filename - * @return a path which matches the filename, or filename if no matches are - * found - */ - public final String resolveTextureFilename(String filename) { - return textureSearchPath.resolvePath(filename); - } - - /** - * Attempts to resolve the specified filename by checking it against the - * include search path. - * - * @param filename filename - * @return a path which matches the filename, or filename if no matches are - * found - */ - public final String resolveIncludeFilename(String filename) { - return includeSearchPath.resolvePath(filename); - } - - public final void shader(String name, String shaderType) { - if (!isIncremental(shaderType)) { - // we are declaring a shader for the first time - if (renderObjects.has(name)) { - UI.printError(Module.API, "Unable to declare shader \"%s\", name is already in use", name); - parameterList.clear(true); - return; - } - Shader shader = PluginRegistry.shaderPlugins.createObject(shaderType); - if (shader == null) { - UI.printError(Module.API, "Unable to create shader of type \"%s\"", shaderType); - return; - } - renderObjects.put(name, shader); - } - // update existing shader (only if it is valid) - if (lookupShader(name) != null) - update(name); - else { - UI.printError(Module.API, "Unable to update shader \"%s\" - shader object was not found", name); - parameterList.clear(true); - } - } - - public final void modifier(String name, String modifierType) { - if (!isIncremental(modifierType)) { - // we are declaring a shader for the first time - if (renderObjects.has(name)) { - UI.printError(Module.API, "Unable to declare modifier \"%s\", name is already in use", name); - parameterList.clear(true); - return; - } - Modifier modifier = PluginRegistry.modifierPlugins.createObject(modifierType); - if (modifier == null) { - UI.printError(Module.API, "Unable to create modifier of type \"%s\"", modifierType); - return; - } - renderObjects.put(name, modifier); - } - // update existing shader (only if it is valid) - if (lookupModifier(name) != null) - update(name); - else { - UI.printError(Module.API, "Unable to update modifier \"%s\" - modifier object was not found", name); - parameterList.clear(true); - } - } - - public final void geometry(String name, String typeName) { - if (!isIncremental(typeName)) { - // we are declaring a geometry for the first time - if (renderObjects.has(name)) { - UI.printError(Module.API, "Unable to declare geometry \"%s\", name is already in use", name); - parameterList.clear(true); - return; - } - // check tesselatable first - if (PluginRegistry.tesselatablePlugins.hasType(typeName)) { - Tesselatable tesselatable = PluginRegistry.tesselatablePlugins.createObject(typeName); - if (tesselatable == null) { - UI.printError(Module.API, "Unable to create tesselatable object of type \"%s\"", typeName); - return; - } - renderObjects.put(name, tesselatable); - } else { - PrimitiveList primitives = PluginRegistry.primitivePlugins.createObject(typeName); - if (primitives == null) { - UI.printError(Module.API, "Unable to create primitive of type \"%s\"", typeName); - return; - } - renderObjects.put(name, primitives); - } - } - if (lookupGeometry(name) != null) - update(name); - else { - UI.printError(Module.API, "Unable to update geometry \"%s\" - geometry object was not found", name); - parameterList.clear(true); - } - } - - public final void instance(String name, String geoname) { - if (!isIncremental(geoname)) { - // we are declaring this instance for the first time - if (renderObjects.has(name)) { - UI.printError(Module.API, "Unable to declare instance \"%s\", name is already in use", name); - parameterList.clear(true); - return; - } - parameter("geometry", geoname); - renderObjects.put(name, new Instance()); - } - if (lookupInstance(name) != null) - update(name); - else { - UI.printError(Module.API, "Unable to update instance \"%s\" - instance object was not found", name); - parameterList.clear(true); - } - } - - public final void light(String name, String lightType) { - if (!isIncremental(lightType)) { - // we are declaring this light for the first time - if (renderObjects.has(name)) { - UI.printError(Module.API, "Unable to declare light \"%s\", name is already in use", name); - parameterList.clear(true); - return; - } - LightSource light = PluginRegistry.lightSourcePlugins.createObject(lightType); - if (light == null) { - UI.printError(Module.API, "Unable to create light source of type \"%s\"", lightType); - return; - } - renderObjects.put(name, light); - } - if (lookupLight(name) != null) - update(name); - else { - UI.printError(Module.API, "Unable to update instance \"%s\" - instance object was not found", name); - parameterList.clear(true); - } - } - - public final void camera(String name, String lensType) { - if (!isIncremental(lensType)) { - // we are declaring this camera for the first time - if (renderObjects.has(name)) { - UI.printError(Module.API, "Unable to declare camera \"%s\", name is already in use", name); - parameterList.clear(true); - return; - } - CameraLens lens = PluginRegistry.cameraLensPlugins.createObject(lensType); - if (lens == null) { - UI.printError(Module.API, "Unable to create a camera lens of type \"%s\"", lensType); - return; - } - renderObjects.put(name, new Camera(lens)); - } - // update existing shader (only if it is valid) - if (lookupCamera(name) != null) - update(name); - else { - UI.printError(Module.API, "Unable to update camera \"%s\" - camera object was not found", name); - parameterList.clear(true); - } - } - - public final void options(String name) { - if (lookupOptions(name) == null) { - if (renderObjects.has(name)) { - UI.printError(Module.API, "Unable to declare options \"%s\", name is already in use", name); - parameterList.clear(true); - return; - } - renderObjects.put(name, new Options()); - } - assert lookupOptions(name) != null; - update(name); - } - - private final boolean isIncremental(String typeName) { - return typeName == null || typeName.equals("incremental"); - } - - /** - * Retrieve a geometry object by its name, or null if no - * geometry was found, or if the specified object is not a geometry. - * - * @param name geometry name - * @return the geometry object associated with that name - */ - public final Geometry lookupGeometry(String name) { - return renderObjects.lookupGeometry(name); - } - - /** - * Retrieve an instance object by its name, or null if no - * instance was found, or if the specified object is not an instance. - * - * @param name instance name - * @return the instance object associated with that name - */ - private final Instance lookupInstance(String name) { - return renderObjects.lookupInstance(name); - } - - /** - * Retrieve a shader object by its name, or null if no shader - * was found, or if the specified object is not a shader. - * - * @param name camera name - * @return the camera object associate with that name - */ - private final Camera lookupCamera(String name) { - return renderObjects.lookupCamera(name); - } - - private final Options lookupOptions(String name) { - return renderObjects.lookupOptions(name); - } - - /** - * Retrieve a shader object by its name, or null if no shader - * was found, or if the specified object is not a shader. - * - * @param name shader name - * @return the shader object associated with that name - */ - public final Shader lookupShader(String name) { - return renderObjects.lookupShader(name); - } - - /** - * Retrieve a modifier object by its name, or null if no - * modifier was found, or if the specified object is not a modifier. - * - * @param name modifier name - * @return the modifier object associated with that name - */ - public final Modifier lookupModifier(String name) { - return renderObjects.lookupModifier(name); - } - - /** - * Retrieve a light object by its name, or null if no shader - * was found, or if the specified object is not a light. - * - * @param name light name - * @return the light object associated with that name - */ - private final LightSource lookupLight(String name) { - return renderObjects.lookupLight(name); - } - - public final void render(String optionsName, Display display) { - renderObjects.updateScene(scene); - Options opt = lookupOptions(optionsName); - if (opt == null) - opt = new Options(); - scene.setCamera(lookupCamera(opt.getString("camera", null))); - - // shader override - String shaderOverrideName = opt.getString("override.shader", "none"); - boolean overridePhotons = opt.getBoolean("override.photons", false); - - if (shaderOverrideName.equals("none")) - scene.setShaderOverride(null, false); - else { - Shader shader = lookupShader(shaderOverrideName); - if (shader == null) - UI.printWarning(Module.API, "Unable to find shader \"%s\" for override, disabling", shaderOverrideName); - scene.setShaderOverride(shader, overridePhotons); - } - - // baking - String bakingInstanceName = opt.getString("baking.instance", null); - if (bakingInstanceName != null) { - Instance bakingInstance = lookupInstance(bakingInstanceName); - if (bakingInstance == null) { - UI.printError(Module.API, "Unable to bake instance \"%s\" - not found", bakingInstanceName); - return; - } - scene.setBakingInstance(bakingInstance); - } else - scene.setBakingInstance(null); - - ImageSampler sampler = PluginRegistry.imageSamplerPlugins.createObject(opt.getString("sampler", "bucket")); - scene.render(opt, sampler, display); - } - - public final boolean include(String filename) { - if (filename == null) - return false; - filename = includeSearchPath.resolvePath(filename); - String extension = FileUtils.getExtension(filename); - SceneParser parser = PluginRegistry.parserPlugins.createObject(extension); - if (parser == null) { - UI.printError(Module.API, "Unable to find a suitable parser for: \"%s\" (extension: %s)", filename, extension); - return false; - } - String currentFolder = new File(filename).getAbsoluteFile().getParentFile().getAbsolutePath(); - includeSearchPath.addSearchPath(currentFolder); - textureSearchPath.addSearchPath(currentFolder); - return parser.parse(filename, this); - } - - /** - * Retrieve the bounding box of the scene. This method will be valid only - * after a first call to {@link #render(String, Display)} has been made. - */ - public final BoundingBox getBounds() { - return scene.getBounds(); - } - - /** - * This method does nothing, but may be overriden to create scenes - * procedurally. - */ - public void build() { - } - - /** - * Create an API object from the specified file. Java files are read by - * Janino and are expected to implement a build method (they implement a - * derived class of SunflowAPI. The build method is called if the code - * compiles succesfully. Other files types are handled by the parse method. - * - * @param filename filename to load - * @return a valid SunflowAPI object or null on failure - */ - public static SunflowAPI create(String filename, int frameNumber) { - if (filename == null) - return new SunflowAPI(); - SunflowAPI api = null; - if (filename.endsWith(".java")) { - Timer t = new Timer(); - UI.printInfo(Module.API, "Compiling \"" + filename + "\" ..."); - t.start(); - try { - FileInputStream stream = new FileInputStream(filename); - api = (SunflowAPI) ClassBodyEvaluator.createFastClassBodyEvaluator(new Scanner(filename, stream), SunflowAPI.class, ClassLoader.getSystemClassLoader()); - stream.close(); - } catch (CompileException e) { - UI.printError(Module.API, "Could not compile: \"%s\"", filename); - UI.printError(Module.API, "%s", e.getMessage()); - return null; - } catch (ParseException e) { - UI.printError(Module.API, "Could not compile: \"%s\"", filename); - UI.printError(Module.API, "%s", e.getMessage()); - return null; - } catch (ScanException e) { - UI.printError(Module.API, "Could not compile: \"%s\"", filename); - UI.printError(Module.API, "%s", e.getMessage()); - return null; - } catch (IOException e) { - UI.printError(Module.API, "Could not compile: \"%s\"", filename); - UI.printError(Module.API, "%s", e.getMessage()); - return null; - } - t.end(); - UI.printInfo(Module.API, "Compile time: " + t.toString()); - // allow relative paths - String currentFolder = new File(filename).getAbsoluteFile().getParentFile().getAbsolutePath(); - api.includeSearchPath.addSearchPath(currentFolder); - api.textureSearchPath.addSearchPath(currentFolder); - UI.printInfo(Module.API, "Build script running ..."); - t.start(); - api.currentFrame(frameNumber); - api.build(); - t.end(); - UI.printInfo(Module.API, "Build script time: %s", t.toString()); - } else { - api = new SunflowAPI(); - api = api.include(filename) ? api : null; - } - return api; - } - - /** - * Translate specfied file into the native sunflow scene file format. - * - * @param filename input filename - * @param outputFilename output filename - * @return true upon success, false otherwise - */ - public static boolean translate(String filename, String outputFilename) { - FileSunflowAPI api = null; - try { - if (outputFilename.endsWith(".sca")) - api = new AsciiFileSunflowAPI(outputFilename); - else if (outputFilename.endsWith(".scb")) - api = new BinaryFileSunflowAPI(outputFilename); - else { - UI.printError(Module.API, "Unable to determine output filetype: \"%s\"", outputFilename); - return false; - } - } catch (IOException e) { - UI.printError(Module.API, "Unable to create output file - %s", e.getMessage()); - return false; - } - String extension = filename.substring(filename.lastIndexOf('.') + 1); - SceneParser parser = PluginRegistry.parserPlugins.createObject(extension); - if (parser == null) { - UI.printError(Module.API, "Unable to find a suitable parser for: \"%s\"", filename); - return false; - } - try { - return parser.parse(filename, api); - } catch (RuntimeException e) { - e.printStackTrace(); - UI.printError(Module.API, "Error occured during translation: %s", e.getMessage()); - return false; - } finally { - api.close(); - } - } - - /** - * Compile the specified code string via Janino. The code must implement a - * build method as described above. The build method is not called on the - * output, it is up the caller to do so. - * - * @param code java code string - * @return a valid SunflowAPI object upon succes, null - * otherwise. - */ - public static SunflowAPI compile(String code) { - try { - Timer t = new Timer(); - t.start(); - SunflowAPI api = (SunflowAPI) ClassBodyEvaluator.createFastClassBodyEvaluator(new Scanner(null, new StringReader(code)), SunflowAPI.class, (ClassLoader) null); - t.end(); - UI.printInfo(Module.API, "Compile time: %s", t.toString()); - return api; - } catch (CompileException e) { - UI.printError(Module.API, "%s", e.getMessage()); - return null; - } catch (ParseException e) { - UI.printError(Module.API, "%s", e.getMessage()); - return null; - } catch (ScanException e) { - UI.printError(Module.API, "%s", e.getMessage()); - return null; - } catch (IOException e) { - UI.printError(Module.API, "%s", e.getMessage()); - return null; - } - } - - /** - * Read the value of the current frame. This value is intended only for - * procedural animation creation. It is not used by the Sunflow core in - * anyway. The default value is 1. - * - * @return current frame number - */ - public int currentFrame() { - return currentFrame; - } - - public void currentFrame(int currentFrame) { - this.currentFrame = currentFrame; - } -} \ No newline at end of file diff --git a/src/org/sunflow/SunflowAPIInterface.java b/src/org/sunflow/SunflowAPIInterface.java deleted file mode 100644 index fd32f8f..0000000 --- a/src/org/sunflow/SunflowAPIInterface.java +++ /dev/null @@ -1,275 +0,0 @@ -package org.sunflow; - -import org.sunflow.core.Display; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.RenderObject; -import org.sunflow.core.Tesselatable; -import org.sunflow.core.ParameterList.InterpolationType; -import org.sunflow.math.Matrix4; -import org.sunflow.math.Point2; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; - -/** - * This interface represents the entry point for rendering scenes using Sunflow. - * Classes which implement this interface are able to receive input from any of - * the Sunflow parsers. - */ -public interface SunflowAPIInterface { - /** - * Reset the state of the API completely. The object table is cleared, and - * all search paths are set back to their default values. - */ - public void reset(); - - /** - * Declare a plugin of the specified type with the given name from a java - * code string. The code will be compiled with Janino and registered as a - * new plugin type upon success. - * - * @param type - * @param name - * @param code - */ - public void plugin(String type, String name, String code); - - /** - * Declare a parameter with the specified name and value. This parameter - * will be added to the currently active parameter list. - * - * @param name parameter name - * @param value parameter value - */ - public void parameter(String name, String value); - - /** - * Declare a parameter with the specified name and value. This parameter - * will be added to the currently active parameter list. - * - * @param name parameter name - * @param value parameter value - */ - public void parameter(String name, boolean value); - - /** - * Declare a parameter with the specified name and value. This parameter - * will be added to the currently active parameter list. - * - * @param name parameter name - * @param value parameter value - */ - public void parameter(String name, int value); - - /** - * Declare a parameter with the specified name and value. This parameter - * will be added to the currently active parameter list. - * - * @param name parameter name - * @param value parameter value - */ - public void parameter(String name, float value); - - /** - * Declare a color parameter in the given colorspace using the specified - * name and value. This parameter will be added to the currently active - * parameter list. - * - * @param name parameter name - * @param colorspace color space or null to assume internal - * color space - * @param data floating point color data - */ - public void parameter(String name, String colorspace, float... data); - - /** - * Declare a parameter with the specified name and value. This parameter - * will be added to the currently active parameter list. - * - * @param name parameter name - * @param value parameter value - */ - public void parameter(String name, Point3 value); - - /** - * Declare a parameter with the specified name and value. This parameter - * will be added to the currently active parameter list. - * - * @param name parameter name - * @param value parameter value - */ - public void parameter(String name, Vector3 value); - - /** - * Declare a parameter with the specified name and value. This parameter - * will be added to the currently active parameter list. - * - * @param name parameter name - * @param value parameter value - */ - public void parameter(String name, Point2 value); - - /** - * Declare a parameter with the specified name and value. This parameter - * will be added to the currently active parameter list. - * - * @param name parameter name - * @param value parameter value - */ - public void parameter(String name, Matrix4 value); - - /** - * Declare a parameter with the specified name and value. This parameter - * will be added to the currently active parameter list. - * - * @param name parameter name - * @param value parameter value - */ - public void parameter(String name, int[] value); - - /** - * Declare a parameter with the specified name and value. This parameter - * will be added to the currently active parameter list. - * - * @param name parameter name - * @param value parameter value - */ - public void parameter(String name, String[] value); - - /** - * Declare a parameter with the specified name. The type may be one of the - * follow: "float", "point", "vector", "texcoord", "matrix". The - * interpolation determines how the parameter is to be interpreted over - * surface (see {@link InterpolationType}). The data is specified in a - * flattened float array. - * - * @param name parameter name - * @param type parameter data type - * @param interpolation parameter interpolation mode - * @param data raw floating point data - */ - public void parameter(String name, String type, String interpolation, float[] data); - - /** - * Remove the specified render object. Note that this may cause the removal - * of other objects which depended on it. - * - * @param name name of the object to remove - */ - public void remove(String name); - - /** - * Add the specified path to the list of directories which are searched - * automatically to resolve scene filenames or textures. Currently the - * supported searchpath types are: "include" and "texture". All other types - * will be ignored. - * - * @param path - */ - public void searchpath(String type, String path); - - /** - * Defines a shader with a given name. If the shader type name is left - * null, the shader with the given name will be updated (if - * it exists). - * - * @param name a unique name given to the shader - * @param shaderType a shader plugin type - */ - public void shader(String name, String shaderType); - - /** - * Defines a modifier with a given name. If the modifier type name is left - * null, the modifier with the given name will be updated - * (if it exists). - * - * @param name a unique name given to the modifier - * @param modifierType a modifier plugin type name - */ - public void modifier(String name, String modifierType); - - /** - * Defines a geometry with a given name. The geometry is built from the - * specified type. Note that geometries may be created from - * {@link Tesselatable} objects or {@link PrimitiveList} objects. This means - * that two seperate plugin lists will be searched for the geometry type. - * {@link Tesselatable} objects are search first. If the type name is left - * null, the geometry with the given name will be updated - * (if it exists). - * - * @param name a unique name given to the geometry - * @param typeName a tesselatable or primitive plugin type name - */ - public void geometry(String name, String typeName); - - /** - * Instance the specified geometry into the scene. If geoname is - * null, the specified instance object will be updated (if - * it exists). In order to change the instancing relationship of an existing - * instance, you should use the "geometry" string attribute. - * - * @param name instance name - * @param geoname name of the geometry to instance - */ - public void instance(String name, String geoname); - - /** - * Defines a light source with a given name. If the light type name is left - * null, the light source with the given name will be - * updated (if it exists). - * - * @param name a unique name given to the light source - * @param lightType a light source plugin type name - */ - public void light(String name, String lightType); - - /** - * Defines a camera with a given name. The camera is built from the - * specified camera lens type plugin. If the lens type name is left - * null, the camera with the given name will be updated (if - * it exists). It is not currently possible to change the lens of a camera - * after it has been created. - * - * @param name camera name - * @param lensType a camera lens plugin type name - */ - public void camera(String name, String lensType); - - /** - * Defines an option object to hold the current parameters. If the object - * already exists, the values will simply override previous ones. - * - * @param name - */ - public void options(String name); - - /** - * Render using the specified options and the specified display. If the - * specified options do not exist - defaults will be used. - * - * @param optionsName name of the {@link RenderObject} which contains the - * options - * @param display display object - */ - public void render(String optionsName, Display display); - - /** - * Parse the specified filename. The include paths are searched first. The - * contents of the file are simply added to the active scene. This allows to - * break up a scene into parts, even across file formats. The appropriate - * parser is chosen based on file extension. - * - * @param filename filename to load - * @return true upon sucess, false if an error - * occured. - */ - public boolean include(String filename); - - /** - * Set the value of the current frame. This value is intended only for - * procedural animation creation. It is not used by the Sunflow core in - * anyway. The default value is 1. - * - * @param currentFrame current frame number - */ - public void currentFrame(int currentFrame); -} \ No newline at end of file diff --git a/src/org/sunflow/core/AccelerationStructure.java b/src/org/sunflow/core/AccelerationStructure.java deleted file mode 100644 index cae979f..0000000 --- a/src/org/sunflow/core/AccelerationStructure.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.sunflow.core; - -public interface AccelerationStructure { - /** - * Construct an acceleration structure for the specified primitive list. - * - * @param primitives - */ - public void build(PrimitiveList primitives); - - /** - * Intersect the specified ray with the geometry in local space. The ray - * will be provided in local space. - * - * @param r ray in local space - * @param istate state to store the intersection into - */ - public void intersect(Ray r, IntersectionState istate); -} \ No newline at end of file diff --git a/src/org/sunflow/core/AccelerationStructureFactory.java b/src/org/sunflow/core/AccelerationStructureFactory.java deleted file mode 100644 index 90b6be9..0000000 --- a/src/org/sunflow/core/AccelerationStructureFactory.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.PluginRegistry; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -class AccelerationStructureFactory { - static final AccelerationStructure create(String name, int n, boolean primitives) { - if (name == null || name.equals("auto")) { - if (primitives) { - if (n > 20000000) - name = "uniformgrid"; - else if (n > 2000000) - name = "bih"; - else if (n > 2) - name = "kdtree"; - else - name = "null"; - } else { - if (n > 2) - name = "bih"; - else - name = "null"; - } - } - AccelerationStructure accel = PluginRegistry.accelPlugins.createObject(name); - if (accel == null) { - UI.printWarning(Module.ACCEL, "Unrecognized intersection accelerator \"%s\" - using auto", name); - return create(null, n, primitives); - } - return accel; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/BucketOrder.java b/src/org/sunflow/core/BucketOrder.java deleted file mode 100644 index d09a362..0000000 --- a/src/org/sunflow/core/BucketOrder.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.sunflow.core; - -/** - * Creates an array of coordinates that iterate over the tiled screen. Classes - * which implement this interface are responsible for guarenteeing the entire - * screen is tiled. No attempt is made to check for duplicates or incomplete - * coverage. - */ -public interface BucketOrder { - /** - * Computes the order in which each coordinate on the screen should be - * visited. - * - * @param nbw number of buckets in the X direction - * @param nbh number of buckets in the Y direction - * @return array of coordinates with interleaved X, Y of the positions of - * buckets to be rendered. - */ - int[] getBucketSequence(int nbw, int nbh); -} \ No newline at end of file diff --git a/src/org/sunflow/core/Camera.java b/src/org/sunflow/core/Camera.java deleted file mode 100644 index 512ac74..0000000 --- a/src/org/sunflow/core/Camera.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.SunflowAPI; -import org.sunflow.math.Matrix4; -import org.sunflow.math.MovingMatrix4; -import org.sunflow.math.Point3; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -/** - * This class represents a camera to the renderer. It handles the mapping of - * camera space to world space, as well as the mounting of {@link CameraLens} - * objects which compute the actual projection. - */ -public class Camera implements RenderObject { - private final CameraLens lens; - private float shutterOpen; - private float shutterClose; - private MovingMatrix4 c2w; - private MovingMatrix4 w2c; - - public Camera(CameraLens lens) { - this.lens = lens; - c2w = new MovingMatrix4(null); - w2c = new MovingMatrix4(null); - shutterOpen = shutterClose = 0; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - shutterOpen = pl.getFloat("shutter.open", shutterOpen); - shutterClose = pl.getFloat("shutter.close", shutterClose); - c2w = pl.getMovingMatrix("transform", c2w); - w2c = c2w.inverse(); - if (w2c == null) { - UI.printWarning(Module.CAM, "Unable to compute camera's inverse transform"); - return false; - } - return lens.update(pl, api); - } - - /** - * Computes actual time from a time sample in the interval [0,1). This - * random number is mapped somewhere between the shutterOpen and - * shutterClose times. - * - * @param time - * @return - */ - public float getTime(float time) { - if (shutterOpen >= shutterClose) - return shutterOpen; - // warp the time sample by a tent filter - this helps simulates the - // behaviour of a standard shutter as explained here: - // "Shutter Efficiency and Temporal Sampling" by "Ian Stephenson" - // http://www.dctsystems.co.uk/Text/shutter.pdf - if (time < 0.5) - time = -1 + (float) Math.sqrt(2 * time); - else - time = 1 - (float) Math.sqrt(2 - 2 * time); - time = 0.5f * (time + 1); - return (1 - time) * shutterOpen + time * shutterClose; - } - - /** - * Generate a ray passing though the specified point on the image plane. - * Additional random variables are provided for the lens to optionally - * compute depth-of-field or motion blur effects. Note that the camera may - * return null for invalid arguments or for pixels which - * don't project to anything. - * - * @param x x pixel coordinate - * @param y y pixel coordinate - * @param imageWidth width of the image in pixels - * @param imageHeight height of the image in pixels - * @param lensX a random variable in [0,1) to be used for DOF sampling - * @param lensY a random variable in [0,1) to be used for DOF sampling - * @param time a random variable in [0,1) to be used for motion blur - * sampling - * @return a ray passing through the specified pixel, or null - */ - public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lensX, double lensY, float time) { - Ray r = lens.getRay(x, y, imageWidth, imageHeight, lensX, lensY, time); - if (r != null) { - // transform from camera space to world space - r = r.transform(c2w.sample(time)); - // renormalize to account for scale factors embeded in the transform - r.normalize(); - } - return r; - } - - /** - * Generate a ray from the origin of camera space toward the specified - * point. - * - * @param p point in world space - * @return ray from the origin of camera space to the specified point - */ - Ray getRay(Point3 p, float time) { - return new Ray(c2w == null ? new Point3(0, 0, 0) : c2w.sample(time).transformP(new Point3(0, 0, 0)), p); - } - - /** - * Returns a transformation matrix mapping camera space to world space. - * - * @return a transformation matrix - */ - Matrix4 getCameraToWorld(float time) { - return c2w == null ? Matrix4.IDENTITY : c2w.sample(time); - } - - /** - * Returns a transformation matrix mapping world space to camera space. - * - * @return a transformation matrix - */ - Matrix4 getWorldToCamera(float time) { - return w2c == null ? Matrix4.IDENTITY : w2c.sample(time); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/CameraLens.java b/src/org/sunflow/core/CameraLens.java deleted file mode 100644 index bf44c17..0000000 --- a/src/org/sunflow/core/CameraLens.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.sunflow.core; - -/** - * Represents a mapping from the 3D scene onto the final image. A camera lens is - * responsible for determining what ray to cast through each pixel. - */ -public interface CameraLens extends RenderObject { - /** - * Create a new {@link Ray ray}to be cast through pixel (x,y) on the image - * plane. Two sampling parameters are provided for lens sampling. They are - * guarenteed to be in the interval [0,1). They can be used to perturb the - * position of the source of the ray on the lens of the camera for DOF - * effects. A third sampling parameter is provided for motion blur effects. - * Note that the {@link Camera} class already handles camera movement motion - * blur. Rays should be generated in camera space - that is, with the eye at - * the origin, looking down the -Z axis, with +Y pointing up. - * - * @param x x coordinate of the (sub)pixel - * @param y y coordinate of the (sub)pixel - * @param imageWidth image width in pixels - * @param imageHeight image height in pixels - * @param lensX x lens sampling parameter - * @param lensY y lens sampling parameter - * @param time time sampling parameter - * @return a new ray passing through the given pixel - */ - public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lensX, double lensY, double time); -} \ No newline at end of file diff --git a/src/org/sunflow/core/CausticPhotonMapInterface.java b/src/org/sunflow/core/CausticPhotonMapInterface.java deleted file mode 100644 index 682da08..0000000 --- a/src/org/sunflow/core/CausticPhotonMapInterface.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.sunflow.core; - -/** - * This class is a generic interface to caustic photon mapping capabilities. - */ -public interface CausticPhotonMapInterface extends PhotonStore { - /** - * Retrieve caustic photons at the specified shading location and add them - * as diffuse light samples. - * - * @param state - */ - void getSamples(ShadingState state); -} \ No newline at end of file diff --git a/src/org/sunflow/core/Display.java b/src/org/sunflow/core/Display.java deleted file mode 100644 index 5fdf90a..0000000 --- a/src/org/sunflow/core/Display.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.image.Color; - -/** - * Represents an image output device. - */ -public interface Display { - /** - * This is called before an image is rendered to indicate how large the - * rendered image will be. This allows the display driver to write out image - * headers or allocate surfaces. Bucket size will be 0 when called from a - * non-bucket based source. - * - * @param w width of the rendered image in pixels - * @param h height of the rendered image in pixels - * @param bucketSize size of the buckets in pixels - */ - void imageBegin(int w, int h, int bucketSize); - - /** - * Prepare the specified area to be rendered. This may be used to highlight - * the work in progress area or simply to setup the display driver to - * receive the specified portion of the image - * - * @param x x coordinate of the bucket within the image - * @param y y coordinate of the bucket within the image - * @param w width of the bucket in pixels - * @param h height of the bucket in pixels - * @param id unique identifier corresponding to the thread which invoked - * this call - */ - void imagePrepare(int x, int y, int w, int h, int id); - - /** - * Update the current image with a bucket of data. The region is guarenteed - * to be within the bounds created by the call to imageBegin. No clipping is - * necessary. Colors are passed in unprocessed. It is up the display driver - * to do any type of quantization, gamma compensation or tone-mapping - * needed. The array of colors will be exactly w * h long and - * in row major order. - * - * @param x x coordinate of the bucket within the image - * @param y y coordinate of the bucket within the image - * @param w width of the bucket in pixels - * @param h height of the bucket in pixels - * @param data bucket data, this array will be exactly w * h - * long - * @param alpha pixel coverage data, this array will be exactly - * w * h long - */ - void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha); - - /** - * Update the current image with a region of flat color. This is used by - * progressive rendering to render progressively smaller regions of the - * screen which will overlap. The region is guarenteed to be within the - * bounds created by the call to imageBegin. No clipping is necessary. - * Colors are passed in unprocessed. It is up the display driver to do any - * type of quantization , gamma compensation or tone-mapping needed. - * - * @param x x coordinate of the region within the image - * @param y y coordinate of the region within the image - * @param w with of the region in pixels - * @param h height of the region in pixels - * @param c color to fill the region with - * @param alpha pixel coverage - */ - void imageFill(int x, int y, int w, int h, Color c, float alpha); - - /** - * This call is made after the image has been rendered. This allows the - * display driver to close any open files, write the image to disk or flush - * any other type of buffers. - */ - void imageEnd(); -} \ No newline at end of file diff --git a/src/org/sunflow/core/Filter.java b/src/org/sunflow/core/Filter.java deleted file mode 100644 index 9271f60..0000000 --- a/src/org/sunflow/core/Filter.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.sunflow.core; - -/** - * Represents a multi-pixel image filter kernel. - */ -public interface Filter { - /** - * Width in pixels of the filter extents. The filter will be applied to the - * range of pixels within a box of +/- getSize() / 2 around - * the center of the pixel. - * - * @return width in pixels - */ - public float getSize(); - - /** - * Get value of the filter at offset (x, y). The filter should never be - * called with values beyond its extents but should return 0 in those cases - * anyway. - * - * @param x x offset in pixels - * @param y y offset in pixels - * @return value of the filter at the specified location - */ - public float get(float x, float y); -} \ No newline at end of file diff --git a/src/org/sunflow/core/GIEngine.java b/src/org/sunflow/core/GIEngine.java deleted file mode 100644 index 389db1a..0000000 --- a/src/org/sunflow/core/GIEngine.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.image.Color; - -/** - * This represents a global illumination algorithm. It provides an interface to - * compute indirect diffuse bounces of light and make those results available to - * shaders. - */ -public interface GIEngine { - /** - * This is an optional method for engines that contain a secondary - * illumination engine which can return an approximation of the global - * radiance in the scene (like a photon map). Engines can safely return - * Color.BLACK if they can't or don't wish to support this. - * - * @param state shading state - * @return color approximating global radiance - */ - public Color getGlobalRadiance(ShadingState state); - - /** - * Initialize the engine. This is called before rendering begins. - * - * @return true if the init phase succeeded, - * false otherwise - */ - public boolean init(Options options, Scene scene); - - /** - * Return the incomming irradiance due to indirect diffuse illumination at - * the specified surface point. - * - * @param state current render state describing the point to be computed - * @param diffuseReflectance diffuse albedo of the point being shaded, this - * can be used for importance tracking - * @return irradiance from indirect diffuse illumination at the specified - * point - */ - public Color getIrradiance(ShadingState state, Color diffuseReflectance); -} \ No newline at end of file diff --git a/src/org/sunflow/core/Geometry.java b/src/org/sunflow/core/Geometry.java deleted file mode 100644 index abde995..0000000 --- a/src/org/sunflow/core/Geometry.java +++ /dev/null @@ -1,144 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.accel.NullAccelerator; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -/** - * This class represent a geometric object in its native object space. These - * object are not rendered directly, they must be instanced via {@link Instance}. - * This class performs all the bookkeeping needed for on-demand tesselation and - * acceleration structure building. - */ -public class Geometry implements RenderObject { - private Tesselatable tesselatable; - private PrimitiveList primitives; - private AccelerationStructure accel; - private int builtAccel; - private int builtTess; - private String acceltype; - - /** - * Create a geometry from the specified tesselatable object. The actual - * renderable primitives will be generated on demand. - * - * @param tesselatable tesselation object - */ - public Geometry(Tesselatable tesselatable) { - this.tesselatable = tesselatable; - primitives = null; - accel = null; - builtAccel = 0; - builtTess = 0; - acceltype = null; - } - - /** - * Create a geometry from the specified primitive aggregate. The - * acceleration structure for this object will be built on demand. - * - * @param primitives primitive list object - */ - public Geometry(PrimitiveList primitives) { - tesselatable = null; - this.primitives = primitives; - accel = null; - builtAccel = 0; - builtTess = 1; // already tesselated - } - - public boolean update(ParameterList pl, SunflowAPI api) { - acceltype = pl.getString("accel", acceltype); - // clear up old tesselation if it exists - if (tesselatable != null) { - primitives = null; - builtTess = 0; - } - // clear acceleration structure so it will be rebuilt - accel = null; - builtAccel = 0; - if (tesselatable != null) - return tesselatable.update(pl, api); - // update primitives - return primitives.update(pl, api); - } - - int getNumPrimitives() { - return primitives == null ? 0 : primitives.getNumPrimitives(); - } - - BoundingBox getWorldBounds(Matrix4 o2w) { - if (primitives == null) { - - BoundingBox b = tesselatable.getWorldBounds(o2w); - if (b != null) - return b; - if (builtTess == 0) - tesselate(); - if (primitives == null) - return null; // failed tesselation, return infinite bounding - // box - } - return primitives.getWorldBounds(o2w); - } - - void intersect(Ray r, IntersectionState state) { - if (builtTess == 0) - tesselate(); - if (builtAccel == 0) - build(); - accel.intersect(r, state); - } - - private synchronized void tesselate() { - // double check flag - if (builtTess != 0) - return; - if (tesselatable != null && primitives == null) { - UI.printInfo(Module.GEOM, "Tesselating geometry ..."); - primitives = tesselatable.tesselate(); - if (primitives == null) - UI.printError(Module.GEOM, "Tesselation failed - geometry will be discarded"); - else - UI.printDetailed(Module.GEOM, "Tesselation produced %d primitives", primitives.getNumPrimitives()); - } - builtTess = 1; - } - - private synchronized void build() { - // double check flag - if (builtAccel != 0) - return; - if (primitives != null) { - int n = primitives.getNumPrimitives(); - if (n >= 1000) - UI.printInfo(Module.GEOM, "Building acceleration structure for %d primitives ...", n); - accel = AccelerationStructureFactory.create(acceltype, n, true); - accel.build(primitives); - } else { - // create an empty accelerator to avoid having to check for null - // pointers in the intersect method - accel = new NullAccelerator(); - } - builtAccel = 1; - } - - void prepareShadingState(ShadingState state) { - primitives.prepareShadingState(state); - } - - PrimitiveList getBakingPrimitives() { - if (builtTess == 0) - tesselate(); - if (primitives == null) - return null; - return primitives.getBakingPrimitives(); - } - - PrimitiveList getPrimitiveList() { - return primitives; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/GlobalPhotonMapInterface.java b/src/org/sunflow/core/GlobalPhotonMapInterface.java deleted file mode 100644 index 70a4266..0000000 --- a/src/org/sunflow/core/GlobalPhotonMapInterface.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.image.Color; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; - -/** - * Represents a global photon map. This is a structure which can return a rough - * approximation of the diffuse radiance at a given surface point. - */ -public interface GlobalPhotonMapInterface extends PhotonStore { - - /** - * Lookup the global diffuse radiance at the specified surface point. - * - * @param p surface position - * @param n surface normal - * @return an approximation of global diffuse radiance at this point - */ - public Color getRadiance(Point3 p, Vector3 n); -} \ No newline at end of file diff --git a/src/org/sunflow/core/ImageSampler.java b/src/org/sunflow/core/ImageSampler.java deleted file mode 100644 index 87f65f4..0000000 --- a/src/org/sunflow/core/ImageSampler.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.sunflow.core; - -/** - * This interface represents an image sampling algorithm capable of rendering - * the entire image. Implementations are responsible for anti-aliasing and - * filtering. - */ -public interface ImageSampler { - /** - * Prepare the sampler for rendering an image of w x h pixels - * - * @param w width of the image - * @param h height of the image - */ - public boolean prepare(Options options, Scene scene, int w, int h); - - /** - * Render the image to the specified display. The sampler can assume the - * display has been opened and that it will be closed after the method - * returns. - * - * @param display Display driver to send image data to - */ - public void render(Display display); -} \ No newline at end of file diff --git a/src/org/sunflow/core/Instance.java b/src/org/sunflow/core/Instance.java deleted file mode 100644 index 51dec8f..0000000 --- a/src/org/sunflow/core/Instance.java +++ /dev/null @@ -1,213 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.SunflowAPI; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.MovingMatrix4; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -/** - * This represents an instance of a {@link Geometry} into the scene. This class - * maps object space to world space and maintains a list of shaders and - * modifiers attached to the surface. - */ -public class Instance implements RenderObject { - private MovingMatrix4 o2w; - private MovingMatrix4 w2o; - private BoundingBox bounds; - private Geometry geometry; - private Shader[] shaders; - private Modifier[] modifiers; - - public Instance() { - o2w = new MovingMatrix4(null); - w2o = new MovingMatrix4(null); - bounds = null; - geometry = null; - shaders = null; - modifiers = null; - } - - public static Instance createTemporary(PrimitiveList primitives, Matrix4 transform, Shader shader) { - Instance i = new Instance(); - i.o2w = new MovingMatrix4(transform); - i.w2o = i.o2w.inverse(); - if (i.w2o == null) { - UI.printError(Module.GEOM, "Unable to compute transform inverse"); - return null; - } - i.geometry = new Geometry(primitives); - i.shaders = new Shader[] { shader }; - i.updateBounds(); - return i; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - String geometryName = pl.getString("geometry", null); - if (geometry == null || geometryName != null) { - if (geometryName == null) { - UI.printError(Module.GEOM, "geometry parameter missing - unable to create instance"); - return false; - } - geometry = api.lookupGeometry(geometryName); - if (geometry == null) { - UI.printError(Module.GEOM, "Geometry \"%s\" was not declared yet - instance is invalid", geometryName); - return false; - } - } - String[] shaderNames = pl.getStringArray("shaders", null); - if (shaderNames != null) { - // new shader names have been provided - shaders = new Shader[shaderNames.length]; - for (int i = 0; i < shaders.length; i++) { - shaders[i] = api.lookupShader(shaderNames[i]); - if (shaders[i] == null) - UI.printWarning(Module.GEOM, "Shader \"%s\" was not declared yet - ignoring", shaderNames[i]); - } - } else { - // re-use existing shader array - } - String[] modifierNames = pl.getStringArray("modifiers", null); - if (modifierNames != null) { - // new modifier names have been provided - modifiers = new Modifier[modifierNames.length]; - for (int i = 0; i < modifiers.length; i++) { - modifiers[i] = api.lookupModifier(modifierNames[i]); - if (modifiers[i] == null) - UI.printWarning(Module.GEOM, "Modifier \"%s\" was not declared yet - ignoring", modifierNames[i]); - } - } - o2w = pl.getMovingMatrix("transform", o2w); - w2o = o2w.inverse(); - if (w2o == null) { - UI.printError(Module.GEOM, "Unable to compute transform inverse"); - return false; - } - return true; - } - - /** - * Recompute world space bounding box of this instance. - */ - public void updateBounds() { - bounds = geometry.getWorldBounds(o2w.getData(0)); - for (int i = 1; i < o2w.numSegments(); i++) - bounds.include(geometry.getWorldBounds(o2w.getData(i))); - } - - /** - * Checks to see if this instance is relative to the specified geometry. - * - * @param g geometry to check against - * @return true if the instanced geometry is equals to g, - * false otherwise - */ - public boolean hasGeometry(Geometry g) { - return geometry == g; - } - - /** - * Remove the specified shader from the instance's list if it is being used. - * - * @param s shader to remove - */ - public void removeShader(Shader s) { - if (shaders != null) { - for (int i = 0; i < shaders.length; i++) - if (shaders[i] == s) - shaders[i] = null; - } - } - - /** - * Remove the specified modifier from the instance's list if it is being - * used. - * - * @param m modifier to remove - */ - public void removeModifier(Modifier m) { - if (modifiers != null) { - for (int i = 0; i < modifiers.length; i++) - if (modifiers[i] == m) - modifiers[i] = null; - } - } - - /** - * Get the world space bounding box for this instance. - * - * @return bounding box in world space - */ - public BoundingBox getBounds() { - return bounds; - } - - int getNumPrimitives() { - return geometry.getNumPrimitives(); - } - - void intersect(Ray r, IntersectionState state) { - Ray localRay = r.transform(w2o.sample(state.time)); - state.current = this; - geometry.intersect(localRay, state); - // FIXME: transfer max distance to current ray - r.setMax(localRay.getMax()); - } - - /** - * Prepare the shading state for shader invocation. This also runs the - * currently attached surface modifier. - * - * @param state shading state to be prepared - */ - public void prepareShadingState(ShadingState state) { - geometry.prepareShadingState(state); - if (state.getNormal() != null && state.getGeoNormal() != null) - state.correctShadingNormal(); - // run modifier if it was provided - if (state.getModifier() != null) - state.getModifier().modify(state); - } - - /** - * Get a shader for the instance's list. - * - * @param i index into the shader list - * @return requested shader, or null if the input is invalid - */ - public Shader getShader(int i) { - if (shaders == null || i < 0 || i >= shaders.length) - return null; - return shaders[i]; - } - - /** - * Get a modifier for the instance's list. - * - * @param i index into the modifier list - * @return requested modifier, or null if the input is - * invalid - */ - public Modifier getModifier(int i) { - if (modifiers == null || i < 0 || i >= modifiers.length) - return null; - return modifiers[i]; - } - - Matrix4 getObjectToWorld(float time) { - return o2w.sample(time); - } - - Matrix4 getWorldToObject(float time) { - return w2o.sample(time); - } - - PrimitiveList getBakingPrimitives() { - return geometry.getBakingPrimitives(); - } - - Geometry getGeometry() { - return geometry; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/InstanceList.java b/src/org/sunflow/core/InstanceList.java deleted file mode 100644 index 631ba9e..0000000 --- a/src/org/sunflow/core/InstanceList.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.SunflowAPI; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; - -final class InstanceList implements PrimitiveList { - private Instance[] instances; - private Instance[] lights; - - InstanceList() { - instances = new Instance[0]; - clearLightSources(); - } - - InstanceList(Instance[] instances) { - this.instances = instances; - clearLightSources(); - } - - void addLightSourceInstances(Instance[] lights) { - this.lights = lights; - } - - void clearLightSources() { - lights = new Instance[0]; - } - - public final float getPrimitiveBound(int primID, int i) { - if (primID < instances.length) - return instances[primID].getBounds().getBound(i); - else - return lights[primID - instances.length].getBounds().getBound(i); - } - - public final BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(); - for (Instance i : instances) - bounds.include(i.getBounds()); - for (Instance i : lights) - bounds.include(i.getBounds()); - return bounds; - } - - public final void intersectPrimitive(Ray r, int primID, IntersectionState state) { - if (primID < instances.length) - instances[primID].intersect(r, state); - else - lights[primID - instances.length].intersect(r, state); - } - - public final int getNumPrimitives() { - return instances.length + lights.length; - } - - public final int getNumPrimitives(int primID) { - return primID < instances.length ? instances[primID].getNumPrimitives() : lights[primID - instances.length].getNumPrimitives(); - } - - public final void prepareShadingState(ShadingState state) { - state.getInstance().prepareShadingState(state); - } - - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public PrimitiveList getBakingPrimitives() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/IntersectionState.java b/src/org/sunflow/core/IntersectionState.java deleted file mode 100644 index e47cbbf..0000000 --- a/src/org/sunflow/core/IntersectionState.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.sunflow.core; - -/** - * This class is used to store ray/object intersections. It also provides - * additional data to assist {@link AccelerationStructure} objects with - * traversal. - */ -public final class IntersectionState { - private static final int MAX_STACK_SIZE = 64; - float time; - float u, v, w; - Instance instance; - int id; - private final StackNode[][] stacks = new StackNode[2][MAX_STACK_SIZE]; - Instance current; - long numEyeRays; - long numShadowRays; - long numReflectionRays; - long numGlossyRays; - long numRefractionRays; - long numRays; - - /** - * Traversal stack node, helps with tree-based {@link AccelerationStructure} - * traversal. - */ - public static final class StackNode { - public int node; - public float near; - public float far; - } - - /** - * Initializes all traversal stacks. - */ - public IntersectionState() { - for (int i = 0; i < stacks.length; i++) - for (int j = 0; j < stacks[i].length; j++) - stacks[i][j] = new StackNode(); - } - - /** - * Returns the time at which the intersection should be calculated. This - * will be constant for a given ray-tree. This value is guarenteed to be - * between the camera's shutter open and shutter close time. - * - * @return time value - */ - public float getTime() { - return time; - } - - /** - * Get stack object for tree based {@link AccelerationStructure}s. - * - * @return array of stack nodes - */ - public final StackNode[] getStack() { - return current == null ? stacks[0] : stacks[1]; - } - - /** - * Checks to see if a hit has been recorded. - * - * @return true if a hit has been recorded, - * false otherwise - */ - public final boolean hit() { - return instance != null; - } - - /** - * Record an intersection with the specified primitive id. The parent object - * is assumed to be the current instance. The u and v parameters are used to - * pinpoint the location on the surface if needed. - * - * @param id primitive id of the intersected object - */ - public final void setIntersection(int id) { - instance = current; - this.id = id; - } - - /** - * Record an intersection with the specified primitive id. The parent object - * is assumed to be the current instance. The u and v parameters are used to - * pinpoint the location on the surface if needed. - * - * @param id primitive id of the intersected object - * @param u u surface paramater of the intersection point - * @param v v surface parameter of the intersection point - */ - public final void setIntersection(int id, float u, float v) { - instance = current; - this.id = id; - this.u = u; - this.v = v; - } - - /** - * Record an intersection with the specified primitive id. The parent object - * is assumed to be the current instance. The u and v parameters are used to - * pinpoint the location on the surface if needed. - * - * @param id primitive id of the intersected object - * @param u u surface paramater of the intersection point - * @param v v surface parameter of the intersection point - */ - public final void setIntersection(int id, float u, float v, float w) { - instance = current; - this.id = id; - this.u = u; - this.v = v; - this.w = w; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/LightSample.java b/src/org/sunflow/core/LightSample.java deleted file mode 100644 index 7fac673..0000000 --- a/src/org/sunflow/core/LightSample.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.image.Color; -import org.sunflow.math.Vector3; - -/** - * Represents a sample taken from a light source that faces a point being - * shaded. - */ -public class LightSample { - private Ray shadowRay; // ray to be used to evaluate if the point is in - // shadow - private Color ldiff; - private Color lspec; - LightSample next; // pointer to next item in a linked list of samples - - /** - * Creates a new light sample object (invalid by default). - */ - public LightSample() { - ldiff = lspec = null; - shadowRay = null; - next = null; - } - - boolean isValid() { - return ldiff != null && lspec != null && shadowRay != null; - } - - /** - * Set the current shadow ray. The ray's direction is used as the sample's - * orientation. - * - * @param shadowRay shadow ray from the point being shaded towards the light - */ - public void setShadowRay(Ray shadowRay) { - this.shadowRay = shadowRay; - } - - /** - * Trace the shadow ray, attenuating the sample's color by the opacity of - * intersected objects. - * - * @param state shading state representing the point to be shaded - */ - public final void traceShadow(ShadingState state) { - Color opacity = state.traceShadow(shadowRay); - Color.blend(ldiff, Color.BLACK, opacity, ldiff); - Color.blend(lspec, Color.BLACK, opacity, lspec); - } - - /** - * Get the sample's shadow ray. - * - * @return shadow ray - */ - public Ray getShadowRay() { - return shadowRay; - } - - /** - * Get diffuse radiance. - * - * @return diffuse radiance - */ - public Color getDiffuseRadiance() { - return ldiff; - } - - /** - * Get specular radiance. - * - * @return specular radiance - */ - public Color getSpecularRadiance() { - return lspec; - } - - /** - * Set the diffuse and specular radiance emitted by the current light - * source. These should usually be the same, but are distinguished to allow - * for non-physical light setups or light source types which compute diffuse - * and specular responses seperately. - * - * @param d diffuse radiance - * @param s specular radiance - */ - public void setRadiance(Color d, Color s) { - ldiff = d.copy(); - lspec = s.copy(); - } - - /** - * Compute a dot product between the current shadow ray direction and the - * specified vector. - * - * @param v direction vector - * @return dot product of the vector with the shadow ray direction - */ - public float dot(Vector3 v) { - return shadowRay.dot(v); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/LightServer.java b/src/org/sunflow/core/LightServer.java deleted file mode 100644 index d28e820..0000000 --- a/src/org/sunflow/core/LightServer.java +++ /dev/null @@ -1,360 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.PluginRegistry; -import org.sunflow.image.Color; -import org.sunflow.math.Point3; -import org.sunflow.math.QMC; -import org.sunflow.math.Vector3; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -class LightServer { - // parent - private Scene scene; - - // lighting - LightSource[] lights; - - // shading override - private Shader shaderOverride; - private boolean shaderOverridePhotons; - - // direct illumination - private int maxDiffuseDepth; - private int maxReflectionDepth; - private int maxRefractionDepth; - - // indirect illumination - private CausticPhotonMapInterface causticPhotonMap; - private GIEngine giEngine; - private int photonCounter; - - LightServer(Scene scene) { - this.scene = scene; - lights = new LightSource[0]; - causticPhotonMap = null; - - shaderOverride = null; - shaderOverridePhotons = false; - - maxDiffuseDepth = 1; - maxReflectionDepth = 4; - maxRefractionDepth = 4; - - causticPhotonMap = null; - giEngine = null; - } - - void setLights(LightSource[] lights) { - this.lights = lights; - } - - Scene getScene() { - return scene; - } - - void setShaderOverride(Shader shader, boolean photonOverride) { - shaderOverride = shader; - shaderOverridePhotons = photonOverride; - } - - boolean build(Options options) { - // read options - maxDiffuseDepth = options.getInt("depths.diffuse", maxDiffuseDepth); - maxReflectionDepth = options.getInt("depths.reflection", maxReflectionDepth); - maxRefractionDepth = options.getInt("depths.refraction", maxRefractionDepth); - String giEngineType = options.getString("gi.engine", null); - giEngine = PluginRegistry.giEnginePlugins.createObject(giEngineType); - String caustics = options.getString("caustics", null); - causticPhotonMap = PluginRegistry.causticPhotonMapPlugins.createObject(caustics); - - // validate options - maxDiffuseDepth = Math.max(0, maxDiffuseDepth); - maxReflectionDepth = Math.max(0, maxReflectionDepth); - maxRefractionDepth = Math.max(0, maxRefractionDepth); - - Timer t = new Timer(); - t.start(); - // count total number of light samples - int numLightSamples = 0; - for (int i = 0; i < lights.length; i++) - numLightSamples += lights[i].getNumSamples(); - // initialize gi engine - if (giEngine != null) { - if (!giEngine.init(options, scene)) - return false; - } - - if (!calculatePhotons(causticPhotonMap, "caustic", 0, options)) - return false; - t.end(); - UI.printInfo(Module.LIGHT, "Light Server stats:"); - UI.printInfo(Module.LIGHT, " * Light sources found: %d", lights.length); - UI.printInfo(Module.LIGHT, " * Light samples: %d", numLightSamples); - UI.printInfo(Module.LIGHT, " * Max raytrace depth:"); - UI.printInfo(Module.LIGHT, " - Diffuse %d", maxDiffuseDepth); - UI.printInfo(Module.LIGHT, " - Reflection %d", maxReflectionDepth); - UI.printInfo(Module.LIGHT, " - Refraction %d", maxRefractionDepth); - UI.printInfo(Module.LIGHT, " * GI engine %s", giEngineType == null ? "none" : giEngineType); - UI.printInfo(Module.LIGHT, " * Caustics: %s", caustics == null ? "none" : caustics); - UI.printInfo(Module.LIGHT, " * Shader override: %b", shaderOverride); - UI.printInfo(Module.LIGHT, " * Photon override: %b", shaderOverridePhotons); - UI.printInfo(Module.LIGHT, " * Build time: %s", t.toString()); - return true; - } - - void showStats() { - } - - boolean calculatePhotons(final PhotonStore map, String type, final int seed, Options options) { - if (map == null) - return true; - if (lights.length == 0) { - UI.printError(Module.LIGHT, "Unable to trace %s photons, no lights in scene", type); - return false; - } - final float[] histogram = new float[lights.length]; - histogram[0] = lights[0].getPower(); - for (int i = 1; i < lights.length; i++) - histogram[i] = histogram[i - 1] + lights[i].getPower(); - UI.printInfo(Module.LIGHT, "Tracing %s photons ...", type); - map.prepare(options, scene.getBounds()); - int numEmittedPhotons = map.numEmit(); - if (numEmittedPhotons <= 0 || histogram[histogram.length - 1] <= 0) { - UI.printError(Module.LIGHT, "Photon mapping enabled, but no %s photons to emit", type); - return false; - } - UI.taskStart("Tracing " + type + " photons", 0, numEmittedPhotons); - Thread[] photonThreads = new Thread[scene.getThreads()]; - final float scale = 1.0f / numEmittedPhotons; - int delta = numEmittedPhotons / photonThreads.length; - photonCounter = 0; - Timer photonTimer = new Timer(); - photonTimer.start(); - for (int i = 0; i < photonThreads.length; i++) { - final int threadID = i; - final int start = threadID * delta; - final int end = (threadID == (photonThreads.length - 1)) ? numEmittedPhotons : (threadID + 1) * delta; - photonThreads[i] = new Thread(new Runnable() { - public void run() { - IntersectionState istate = new IntersectionState(); - for (int i = start; i < end; i++) { - synchronized (LightServer.this) { - UI.taskUpdate(photonCounter); - photonCounter++; - if (UI.taskCanceled()) - return; - } - - int qmcI = i + seed; - - double rand = QMC.halton(0, qmcI) * histogram[histogram.length - 1]; - int j = 0; - while (rand >= histogram[j] && j < histogram.length) - j++; - // make sure we didn't pick a zero-probability light - if (j == histogram.length) - continue; - - double randX1 = (j == 0) ? rand / histogram[0] : (rand - histogram[j]) / (histogram[j] - histogram[j - 1]); - double randY1 = QMC.halton(1, qmcI); - double randX2 = QMC.halton(2, qmcI); - double randY2 = QMC.halton(3, qmcI); - Point3 pt = new Point3(); - Vector3 dir = new Vector3(); - Color power = new Color(); - lights[j].getPhoton(randX1, randY1, randX2, randY2, pt, dir, power); - power.mul(scale); - Ray r = new Ray(pt, dir); - scene.trace(r, istate); - if (istate.hit()) - shadePhoton(ShadingState.createPhotonState(r, istate, qmcI, map, LightServer.this), power); - } - } - }); - photonThreads[i].setPriority(scene.getThreadPriority()); - photonThreads[i].start(); - } - for (int i = 0; i < photonThreads.length; i++) { - try { - photonThreads[i].join(); - } catch (InterruptedException e) { - UI.printError(Module.LIGHT, "Photon thread %d of %d was interrupted", i + 1, photonThreads.length); - return false; - } - } - if (UI.taskCanceled()) { - UI.taskStop(); // shut down task cleanly - return false; - } - photonTimer.end(); - UI.taskStop(); - UI.printInfo(Module.LIGHT, "Tracing time for %s photons: %s", type, photonTimer.toString()); - map.init(); - return true; - } - - void shadePhoton(ShadingState state, Color power) { - state.getInstance().prepareShadingState(state); - Shader shader = getPhotonShader(state); - // scatter photon - if (shader != null) - shader.scatterPhoton(state, power); - } - - void traceDiffusePhoton(ShadingState previous, Ray r, Color power) { - if (previous.getDiffuseDepth() >= maxDiffuseDepth) - return; - IntersectionState istate = previous.getIntersectionState(); - scene.trace(r, istate); - if (previous.getIntersectionState().hit()) { - // create a new shading context - ShadingState state = ShadingState.createDiffuseBounceState(previous, r, 0); - shadePhoton(state, power); - } - } - - void traceReflectionPhoton(ShadingState previous, Ray r, Color power) { - if (previous.getReflectionDepth() >= maxReflectionDepth) - return; - IntersectionState istate = previous.getIntersectionState(); - scene.trace(r, istate); - if (previous.getIntersectionState().hit()) { - // create a new shading context - ShadingState state = ShadingState.createReflectionBounceState(previous, r, 0); - shadePhoton(state, power); - } - } - - void traceRefractionPhoton(ShadingState previous, Ray r, Color power) { - if (previous.getRefractionDepth() >= maxRefractionDepth) - return; - IntersectionState istate = previous.getIntersectionState(); - scene.trace(r, istate); - if (previous.getIntersectionState().hit()) { - // create a new shading context - ShadingState state = ShadingState.createRefractionBounceState(previous, r, 0); - shadePhoton(state, power); - } - } - - private Shader getShader(ShadingState state) { - return shaderOverride != null ? shaderOverride : state.getShader(); - } - - private Shader getPhotonShader(ShadingState state) { - return (shaderOverride != null && shaderOverridePhotons) ? shaderOverride : state.getShader(); - - } - - ShadingState getRadiance(float rx, float ry, float time, int i, int d, Ray r, IntersectionState istate, ShadingCache cache) { - // set this value once - will stay constant for the entire ray-tree - istate.time = time; - scene.trace(r, istate); - if (istate.hit()) { - ShadingState state = ShadingState.createState(istate, rx, ry, time, r, i, d, this); - state.getInstance().prepareShadingState(state); - Shader shader = getShader(state); - if (shader == null) { - state.setResult(Color.BLACK); - return state; - } - if (cache != null) { - Color c = cache.lookup(state, shader); - if (c != null) { - state.setResult(c); - return state; - } - } - state.setResult(shader.getRadiance(state)); - if (cache != null) - cache.add(state, shader, state.getResult()); - checkNanInf(state.getResult()); - return state; - } else - return null; - } - - private static final void checkNanInf(Color c) { - if (c.isNan()) - UI.printWarning(Module.LIGHT, "NaN shading sample!"); - else if (c.isInf()) - UI.printWarning(Module.LIGHT, "Inf shading sample!"); - } - - void shadeBakeResult(ShadingState state) { - Shader shader = getShader(state); - if (shader != null) - state.setResult(shader.getRadiance(state)); - else - state.setResult(Color.BLACK); - } - - Color shadeHit(ShadingState state) { - state.getInstance().prepareShadingState(state); - Shader shader = getShader(state); - return (shader != null) ? shader.getRadiance(state) : Color.BLACK; - } - - Color traceGlossy(ShadingState previous, Ray r, int i) { - // limit path depth and disable caustic paths - if (previous.getReflectionDepth() >= maxReflectionDepth || previous.getDiffuseDepth() > 0) - return Color.BLACK; - IntersectionState istate = previous.getIntersectionState(); - istate.numGlossyRays++; - scene.trace(r, istate); - return istate.hit() ? shadeHit(ShadingState.createGlossyBounceState(previous, r, i)) : Color.BLACK; - } - - Color traceReflection(ShadingState previous, Ray r, int i) { - // limit path depth and disable caustic paths - if (previous.getReflectionDepth() >= maxReflectionDepth || previous.getDiffuseDepth() > 0) - return Color.BLACK; - IntersectionState istate = previous.getIntersectionState(); - istate.numReflectionRays++; - scene.trace(r, istate); - return istate.hit() ? shadeHit(ShadingState.createReflectionBounceState(previous, r, i)) : Color.BLACK; - } - - Color traceRefraction(ShadingState previous, Ray r, int i) { - // limit path depth and disable caustic paths - if (previous.getRefractionDepth() >= maxRefractionDepth || previous.getDiffuseDepth() > 0) - return Color.BLACK; - IntersectionState istate = previous.getIntersectionState(); - istate.numRefractionRays++; - scene.trace(r, istate); - return istate.hit() ? shadeHit(ShadingState.createRefractionBounceState(previous, r, i)) : Color.BLACK; - } - - ShadingState traceFinalGather(ShadingState previous, Ray r, int i) { - if (previous.getDiffuseDepth() >= maxDiffuseDepth) - return null; - IntersectionState istate = previous.getIntersectionState(); - scene.trace(r, istate); - return istate.hit() ? ShadingState.createFinalGatherState(previous, r, i) : null; - } - - Color getGlobalRadiance(ShadingState state) { - if (giEngine == null) - return Color.BLACK; - return giEngine.getGlobalRadiance(state); - } - - Color getIrradiance(ShadingState state, Color diffuseReflectance) { - // no gi engine, or we have already exceeded number of available bounces - if (giEngine == null || state.getDiffuseDepth() >= maxDiffuseDepth) - return Color.BLACK; - return giEngine.getIrradiance(state, diffuseReflectance); - } - - void initLightSamples(ShadingState state) { - for (LightSource l : lights) - l.getSamples(state); - } - - void initCausticSamples(ShadingState state) { - if (causticPhotonMap != null) - causticPhotonMap.getSamples(state); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/LightSource.java b/src/org/sunflow/core/LightSource.java deleted file mode 100644 index ae797cf..0000000 --- a/src/org/sunflow/core/LightSource.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.image.Color; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; - -/** - * This interface is used to represent any light emitting primitive. It permits - * efficient sampling of direct illumination and photon shooting. - */ -public interface LightSource extends RenderObject { - /** - * Get the maximum number of samples that can be taken from this light - * source. This is currently only used for statistics reporting. - * - * @return maximum number of samples to be taken from this light source - */ - public int getNumSamples(); - - /** - * Samples the light source to compute direct illumination. Light samples - * can be created using the {@link LightSample} class and added to the - * current {@link ShadingState}. This method is responsible for the - * shooting of shadow rays which allows for non-physical lights that don't - * cast shadows. It is recommended that only a single shadow ray be shot if - * {@link ShadingState#getDiffuseDepth()} is greater than 0. This avoids an - * exponential number of shadow rays from being traced. - * - * @param state current state, including point to be shaded - * @see LightSample - */ - public void getSamples(ShadingState state); - - /** - * Gets a photon to emit from this light source by setting each of the - * arguments. The two sampling parameters are points on the unit square that - * can be used to sample a position and/or direction for the emitted photon. - * - * @param randX1 sampling parameter - * @param randY1 sampling parameter - * @param randX2 sampling parameter - * @param randY2 sampling parameter - * @param p position to shoot the photon from - * @param dir direction to shoot the photon in - * @param power power of the photon - */ - public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power); - - /** - * Get the total power emitted by this light source. Lights that have 0 - * power will not emit any photons. - * - * @return light source power - */ - public float getPower(); - - /** - * Create an instance which represents the geometry of this light source. - * This instance will be created just before and removed immediately after - * rendering. Non-area light sources can return null to - * indicate that no geometry needs to be created. - * - * @return an instance describing the light source - */ - public Instance createInstance(); -} \ No newline at end of file diff --git a/src/org/sunflow/core/Modifier.java b/src/org/sunflow/core/Modifier.java deleted file mode 100644 index 33e2238..0000000 --- a/src/org/sunflow/core/Modifier.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.sunflow.core; - -/** - * This represents a surface modifier. This is run on each instance prior to - * shading and can modify the shading state in arbitrary ways to provide effects - * such as bump mapping. - */ -public interface Modifier extends RenderObject { - - /** - * Modify the shading state for the point to be shaded. - * - * @param state shading state to modify - */ - public void modify(ShadingState state); -} \ No newline at end of file diff --git a/src/org/sunflow/core/Options.java b/src/org/sunflow/core/Options.java deleted file mode 100644 index 9a59102..0000000 --- a/src/org/sunflow/core/Options.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.SunflowAPI; -import org.sunflow.util.FastHashMap; - -/** - * This holds rendering objects as key, value pairs. - */ -public final class Options extends ParameterList implements RenderObject { - public boolean update(ParameterList pl, SunflowAPI api) { - // take all attributes, and update them into the current set - for (FastHashMap.Entry e : pl.list) { - list.put(e.getKey(), e.getValue()); - e.getValue().check(); - } - return true; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/ParameterList.java b/src/org/sunflow/core/ParameterList.java deleted file mode 100644 index e824f6f..0000000 --- a/src/org/sunflow/core/ParameterList.java +++ /dev/null @@ -1,729 +0,0 @@ -package org.sunflow.core; - -import java.util.Locale; - -import org.sunflow.image.Color; -import org.sunflow.math.Matrix4; -import org.sunflow.math.MovingMatrix4; -import org.sunflow.math.Point2; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; -import org.sunflow.util.FastHashMap; - -/** - * This class holds a list of "parameters". These are defined and then passed - * onto rendering objects through the API. They can hold arbitrary typed and - * named variables as a unified way of getting data into user objects. - */ -public class ParameterList { - protected final FastHashMap list; - private int numVerts, numFaces, numFaceVerts; - - private enum ParameterType { - STRING, INT, BOOL, FLOAT, POINT, VECTOR, TEXCOORD, MATRIX, COLOR - } - - public enum InterpolationType { - NONE, FACE, VERTEX, FACEVARYING - } - - /** - * Creates an empty ParameterList. - */ - public ParameterList() { - list = new FastHashMap(); - numVerts = numFaces = numFaceVerts = 0; - } - - /** - * Clears the list of all its members. If some members were never used, a - * warning will be printed to remind the user something may be wrong. - */ - public void clear(boolean showUnused) { - if (showUnused) { - for (FastHashMap.Entry e : list) { - if (!e.getValue().checked) - UI.printWarning(Module.API, "Unused parameter: %s - %s", e.getKey(), e.getValue()); - } - } - list.clear(); - numVerts = numFaces = numFaceVerts = 0; - } - - /** - * Setup how many faces should be used to check member count on "face" - * interpolated parameters. - * - * @param numFaces number of faces - */ - public void setFaceCount(int numFaces) { - this.numFaces = numFaces; - } - - /** - * Setup how many vertices should be used to check member count of "vertex" - * interpolated parameters. - * - * @param numVerts number of vertices - */ - public void setVertexCount(int numVerts) { - this.numVerts = numVerts; - } - - /** - * Setup how many "face-vertices" should be used to check member count of - * "facevarying" interpolated parameters. This should be equal to the sum of - * the number of vertices on each face. - * - * @param numFaceVerts number of "face-vertices" - */ - public void setFaceVertexCount(int numFaceVerts) { - this.numFaceVerts = numFaceVerts; - } - - /** - * Add the specified string as a parameter. null values are - * not permitted. - * - * @param name parameter name - * @param value parameter value - */ - public void addString(String name, String value) { - add(name, new Parameter(value)); - } - - /** - * Add the specified integer as a parameter. null values are - * not permitted. - * - * @param name parameter name - * @param value parameter value - */ - public void addInteger(String name, int value) { - add(name, new Parameter(value)); - } - - /** - * Add the specified boolean as a parameter. null values are - * not permitted. - * - * @param name parameter name - * @param value parameter value - */ - public void addBoolean(String name, boolean value) { - add(name, new Parameter(value)); - } - - /** - * Add the specified float as a parameter. null values are - * not permitted. - * - * @param name parameter name - * @param value parameter value - */ - public void addFloat(String name, float value) { - add(name, new Parameter(value)); - } - - /** - * Add the specified color as a parameter. null values are - * not permitted. - * - * @param name parameter name - * @param value parameter value - */ - public void addColor(String name, Color value) { - if (value == null) - throw new NullPointerException(); - add(name, new Parameter(value)); - } - - /** - * Add the specified array of integers as a parameter. null - * values are not permitted. - * - * @param name parameter name - * @param array parameter value - */ - public void addIntegerArray(String name, int[] array) { - if (array == null) - throw new NullPointerException(); - add(name, new Parameter(array)); - } - - /** - * Add the specified array of integers as a parameter. null - * values are not permitted. - * - * @param name parameter name - * @param array parameter value - */ - public void addStringArray(String name, String[] array) { - if (array == null) - throw new NullPointerException(); - add(name, new Parameter(array)); - } - - /** - * Add the specified floats as a parameter. null values are - * not permitted. - * - * @param name parameter name - * @param interp interpolation type - * @param data parameter value - */ - public void addFloats(String name, InterpolationType interp, float[] data) { - if (data == null) { - UI.printError(Module.API, "Cannot create float parameter %s -- invalid data length", name); - return; - } - add(name, new Parameter(ParameterType.FLOAT, interp, data)); - } - - /** - * Add the specified points as a parameter. null values are - * not permitted. - * - * @param name parameter name - * @param interp interpolation type - * @param data parameter value - */ - public void addPoints(String name, InterpolationType interp, float[] data) { - if (data == null || data.length % 3 != 0) { - UI.printError(Module.API, "Cannot create point parameter %s -- invalid data length", name); - return; - } - add(name, new Parameter(ParameterType.POINT, interp, data)); - } - - /** - * Add the specified vectors as a parameter. null values are - * not permitted. - * - * @param name parameter name - * @param interp interpolation type - * @param data parameter value - */ - - public void addVectors(String name, InterpolationType interp, float[] data) { - if (data == null || data.length % 3 != 0) { - UI.printError(Module.API, "Cannot create vector parameter %s -- invalid data length", name); - return; - } - add(name, new Parameter(ParameterType.VECTOR, interp, data)); - } - - /** - * Add the specified texture coordinates as a parameter. null - * values are not permitted. - * - * @param name parameter name - * @param interp interpolation type - * @param data parameter value - */ - public void addTexCoords(String name, InterpolationType interp, float[] data) { - if (data == null || data.length % 2 != 0) { - UI.printError(Module.API, "Cannot create texcoord parameter %s -- invalid data length", name); - return; - } - add(name, new Parameter(ParameterType.TEXCOORD, interp, data)); - } - - /** - * Add the specified matrices as a parameter. null values are - * not permitted. - * - * @param name parameter name - * @param interp interpolation type - * @param data parameter value - */ - public void addMatrices(String name, InterpolationType interp, float[] data) { - if (data == null || data.length % 16 != 0) { - UI.printError(Module.API, "Cannot create matrix parameter %s -- invalid data length", name); - return; - } - add(name, new Parameter(ParameterType.MATRIX, interp, data)); - } - - private void add(String name, Parameter param) { - if (name == null) - UI.printError(Module.API, "Cannot declare parameter with null name"); - else if (list.put(name, param) != null) - UI.printWarning(Module.API, "Parameter %s was already defined -- overwriting", name); - } - - /** - * Get the specified string parameter from this list. - * - * @param name name of the parameter - * @param defaultValue value to return if not found - * @return the value of the parameter specified or default value if not - * found - */ - public String getString(String name, String defaultValue) { - Parameter p = list.get(name); - if (isValidParameter(name, ParameterType.STRING, InterpolationType.NONE, 1, p)) - return p.getStringValue(); - return defaultValue; - } - - /** - * Get the specified string array parameter from this list. - * - * @param name name of the parameter - * @param defaultValue value to return if not found - * @return the value of the parameter specified or default value if not - * found - */ - public String[] getStringArray(String name, String[] defaultValue) { - Parameter p = list.get(name); - if (isValidParameter(name, ParameterType.STRING, InterpolationType.NONE, -1, p)) - return p.getStrings(); - return defaultValue; - } - - /** - * Get the specified integer parameter from this list. - * - * @param name name of the parameter - * @param defaultValue value to return if not found - * @return the value of the parameter specified or default value if not - * found - */ - public int getInt(String name, int defaultValue) { - Parameter p = list.get(name); - if (isValidParameter(name, ParameterType.INT, InterpolationType.NONE, 1, p)) - return p.getIntValue(); - return defaultValue; - } - - /** - * Get the specified integer array parameter from this list. - * - * @param name name of the parameter - * @return the value of the parameter specified or null if - * not found - */ - public int[] getIntArray(String name) { - Parameter p = list.get(name); - if (isValidParameter(name, ParameterType.INT, InterpolationType.NONE, -1, p)) - return p.getInts(); - return null; - } - - /** - * Get the specified boolean parameter from this list. - * - * @param name name of the parameter - * @param defaultValue value to return if not found - * @return the value of the parameter specified or default value if not - * found - */ - public boolean getBoolean(String name, boolean defaultValue) { - Parameter p = list.get(name); - if (isValidParameter(name, ParameterType.BOOL, InterpolationType.NONE, 1, p)) - return p.getBoolValue(); - return defaultValue; - } - - /** - * Get the specified float parameter from this list. - * - * @param name name of the parameter - * @param defaultValue value to return if not found - * @return the value of the parameter specified or default value if not - * found - */ - public float getFloat(String name, float defaultValue) { - Parameter p = list.get(name); - if (isValidParameter(name, ParameterType.FLOAT, InterpolationType.NONE, 1, p)) - return p.getFloatValue(); - return defaultValue; - } - - /** - * Get the specified color parameter from this list. - * - * @param name name of the parameter - * @param defaultValue value to return if not found - * @return the value of the parameter specified or default value if not - * found - */ - public Color getColor(String name, Color defaultValue) { - Parameter p = list.get(name); - if (isValidParameter(name, ParameterType.COLOR, InterpolationType.NONE, 1, p)) - return p.getColor(); - return defaultValue; - } - - /** - * Get the specified point parameter from this list. - * - * @param name name of the parameter - * @param defaultValue value to return if not found - * @return the value of the parameter specified or default value if not - * found - */ - public Point3 getPoint(String name, Point3 defaultValue) { - Parameter p = list.get(name); - if (isValidParameter(name, ParameterType.POINT, InterpolationType.NONE, 1, p)) - return p.getPoint(); - return defaultValue; - } - - /** - * Get the specified vector parameter from this list. - * - * @param name name of the parameter - * @param defaultValue value to return if not found - * @return the value of the parameter specified or default value if not - * found - */ - public Vector3 getVector(String name, Vector3 defaultValue) { - Parameter p = list.get(name); - if (isValidParameter(name, ParameterType.VECTOR, InterpolationType.NONE, 1, p)) - return p.getVector(); - return defaultValue; - } - - /** - * Get the specified texture coordinate parameter from this list. - * - * @param name name of the parameter - * @param defaultValue value to return if not found - * @return the value of the parameter specified or default value if not - * found - */ - public Point2 getTexCoord(String name, Point2 defaultValue) { - Parameter p = list.get(name); - if (isValidParameter(name, ParameterType.TEXCOORD, InterpolationType.NONE, 1, p)) - return p.getTexCoord(); - return defaultValue; - } - - /** - * Get the specified matrix parameter from this list. - * - * @param name name of the parameter - * @param defaultValue value to return if not found - * @return the value of the parameter specified or default value if not - * found - */ - public Matrix4 getMatrix(String name, Matrix4 defaultValue) { - Parameter p = list.get(name); - if (isValidParameter(name, ParameterType.MATRIX, InterpolationType.NONE, 1, p)) - return p.getMatrix(); - return defaultValue; - } - - /** - * Get the specified float array parameter from this list. - * - * @param name name of the parameter - * @return the value of the parameter specified or null if - * not found - */ - public FloatParameter getFloatArray(String name) { - return getFloatParameter(name, ParameterType.FLOAT, list.get(name)); - } - - /** - * Get the specified point array parameter from this list. - * - * @param name name of the parameter - * @return the value of the parameter specified or null if - * not found - */ - public FloatParameter getPointArray(String name) { - return getFloatParameter(name, ParameterType.POINT, list.get(name)); - } - - /** - * Get the specified vector array parameter from this list. - * - * @param name name of the parameter - * @return the value of the parameter specified or null if - * not found - */ - public FloatParameter getVectorArray(String name) { - return getFloatParameter(name, ParameterType.VECTOR, list.get(name)); - } - - /** - * Get the specified texture coordinate array parameter from this list. - * - * @param name name of the parameter - * @return the value of the parameter specified or null if - * not found - */ - public FloatParameter getTexCoordArray(String name) { - return getFloatParameter(name, ParameterType.TEXCOORD, list.get(name)); - } - - /** - * Get the specified matrix array parameter from this list. - * - * @param name name of the parameter - * @return the value of the parameter specified or null if - * not found - */ - public FloatParameter getMatrixArray(String name) { - return getFloatParameter(name, ParameterType.MATRIX, list.get(name)); - } - - private boolean isValidParameter(String name, ParameterType type, InterpolationType interp, int requestedSize, Parameter p) { - if (p == null) - return false; - if (p.type != type) { - UI.printWarning(Module.API, "Parameter %s requested as a %s - declared as %s", name, type.name().toLowerCase(Locale.ENGLISH), p.type.name().toLowerCase(Locale.ENGLISH)); - return false; - } - if (p.interp != interp) { - UI.printWarning(Module.API, "Parameter %s requested as a %s - declared as %s", name, interp.name().toLowerCase(Locale.ENGLISH), p.interp.name().toLowerCase(Locale.ENGLISH)); - return false; - } - if (requestedSize > 0 && p.size() != requestedSize) { - UI.printWarning(Module.API, "Parameter %s requires %d %s - declared with %d", name, requestedSize, requestedSize == 1 ? "value" : "values", p.size()); - return false; - } - p.checked = true; - return true; - } - - private FloatParameter getFloatParameter(String name, ParameterType type, Parameter p) { - if (p == null) - return null; - switch (p.interp) { - case NONE: - if (!isValidParameter(name, type, p.interp, -1, p)) - return null; - break; - case VERTEX: - if (!isValidParameter(name, type, p.interp, numVerts, p)) - return null; - break; - case FACE: - if (!isValidParameter(name, type, p.interp, numFaces, p)) - return null; - break; - case FACEVARYING: - if (!isValidParameter(name, type, p.interp, numFaceVerts, p)) - return null; - break; - default: - return null; - } - return p.getFloats(); - } - - /** - * Represents an array of floating point values. This can store single - * float, points, vectors, texture coordinates or matrices. The parameter - * should be interpolated over the surface according to the interp parameter - * when applicable. - */ - public static final class FloatParameter { - public final InterpolationType interp; - public final float[] data; - - public FloatParameter() { - this(InterpolationType.NONE, null); - } - - public FloatParameter(float f) { - this(InterpolationType.NONE, new float[] { f }); - } - - private FloatParameter(InterpolationType interp, float[] data) { - this.interp = interp; - this.data = data; - } - } - - public final MovingMatrix4 getMovingMatrix(String name, MovingMatrix4 defaultValue) { - // step 1: check for a non-moving specification: - Matrix4 m = getMatrix(name, null); - if (m != null) - return new MovingMatrix4(m); - // step 2: check to see if the time range has been updated - FloatParameter times = getFloatArray(name + ".times"); - if (times != null) { - if (times.data.length <= 1) - defaultValue.updateTimes(0, 0); - else { - if (times.data.length != 2) - UI.printWarning(Module.API, "Time value specification using only endpoints of %d values specified", times.data.length); - // get endpoint times - we might allow multiple time values - // later - float t0 = times.data[0]; - float t1 = times.data[times.data.length - 1]; - defaultValue.updateTimes(t0, t1); - } - } else { - // time range stays at default - } - // step 3: check to see if a number of steps has been specified - int steps = getInt(name + ".steps", 0); - if (steps <= 0) { - // not specified - return default value - } else { - // update each element - defaultValue.setSteps(steps); - for (int i = 0; i < steps; i++) - defaultValue.updateData(i, getMatrix(String.format("%s[%d]", name, i), defaultValue.getData(i))); - } - return defaultValue; - } - - protected static final class Parameter { - private ParameterType type; - private InterpolationType interp; - private Object obj; - private boolean checked; - - private Parameter(String value) { - type = ParameterType.STRING; - interp = InterpolationType.NONE; - obj = new String[] { value }; - checked = false; - } - - private Parameter(int value) { - type = ParameterType.INT; - interp = InterpolationType.NONE; - obj = new int[] { value }; - checked = false; - } - - private Parameter(boolean value) { - type = ParameterType.BOOL; - interp = InterpolationType.NONE; - obj = value; - checked = false; - } - - private Parameter(float value) { - type = ParameterType.FLOAT; - interp = InterpolationType.NONE; - obj = new float[] { value }; - checked = false; - } - - private Parameter(int[] array) { - type = ParameterType.INT; - interp = InterpolationType.NONE; - obj = array; - checked = false; - } - - private Parameter(String[] array) { - type = ParameterType.STRING; - interp = InterpolationType.NONE; - obj = array; - checked = false; - } - - private Parameter(Color c) { - type = ParameterType.COLOR; - interp = InterpolationType.NONE; - obj = c; - checked = false; - } - - private Parameter(ParameterType type, InterpolationType interp, float[] data) { - this.type = type; - this.interp = interp; - obj = data; - checked = false; - } - - private int size() { - // number of elements - switch (type) { - case STRING: - return ((String[]) obj).length; - case INT: - return ((int[]) obj).length; - case BOOL: - return 1; - case FLOAT: - return ((float[]) obj).length; - case POINT: - return ((float[]) obj).length / 3; - case VECTOR: - return ((float[]) obj).length / 3; - case TEXCOORD: - return ((float[]) obj).length / 2; - case MATRIX: - return ((float[]) obj).length / 16; - case COLOR: - return 1; - default: - return -1; - } - } - - protected void check() { - checked = true; - } - - @Override - public String toString() { - return String.format("%s%s[%d]", interp == InterpolationType.NONE ? "" : interp.name().toLowerCase() + " ", type.name().toLowerCase(), size()); - } - - private String getStringValue() { - return ((String[]) obj)[0]; - } - - private boolean getBoolValue() { - return (Boolean) obj; - } - - private int getIntValue() { - return ((int[]) obj)[0]; - } - - private int[] getInts() { - return (int[]) obj; - } - - private String[] getStrings() { - return (String[]) obj; - } - - private float getFloatValue() { - return ((float[]) obj)[0]; - } - - private FloatParameter getFloats() { - return new FloatParameter(interp, (float[]) obj); - } - - private Point3 getPoint() { - float[] floats = (float[]) obj; - return new Point3(floats[0], floats[1], floats[2]); - } - - private Vector3 getVector() { - float[] floats = (float[]) obj; - return new Vector3(floats[0], floats[1], floats[2]); - } - - private Point2 getTexCoord() { - float[] floats = (float[]) obj; - return new Point2(floats[0], floats[1]); - } - - private Matrix4 getMatrix() { - float[] floats = (float[]) obj; - return new Matrix4(floats, true); - } - - private Color getColor() { - return (Color) obj; - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/PhotonStore.java b/src/org/sunflow/core/PhotonStore.java deleted file mode 100644 index c10da55..0000000 --- a/src/org/sunflow/core/PhotonStore.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.image.Color; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Vector3; - -/** - * Describes an object which can store photons. - */ -public interface PhotonStore { - /** - * Number of photons to emit from this surface. - * - * @return number of photons - */ - int numEmit(); - - /** - * Initialize this object for the specified scene size. - * - * @param sceneBounds scene bounding box - */ - void prepare(Options options, BoundingBox sceneBounds); - - /** - * Store the specified photon. - * - * @param state shading state - * @param dir photon direction - * @param power photon power - * @param diffuse diffuse color at the hit point - */ - void store(ShadingState state, Vector3 dir, Color power, Color diffuse); - - /** - * Initialize the map after all photons have been stored. This can be used - * to balance a kd-tree based photon map for example. - */ - void init(); - - /** - * Allow photons reflected diffusely? - * - * @return true if diffuse bounces should be traced - */ - boolean allowDiffuseBounced(); - - /** - * Allow specularly reflected photons? - * - * @return true if specular reflection bounces should be - * traced - */ - boolean allowReflectionBounced(); - - /** - * Allow refracted photons? - * - * @return true if refracted bounces should be traced - */ - boolean allowRefractionBounced(); -} \ No newline at end of file diff --git a/src/org/sunflow/core/PrimitiveList.java b/src/org/sunflow/core/PrimitiveList.java deleted file mode 100644 index 36012f0..0000000 --- a/src/org/sunflow/core/PrimitiveList.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; - -/** - * This class represents an object made up of many primitives. - */ -public interface PrimitiveList extends RenderObject { - /** - * Compute a bounding box of this object in world space, using the specified - * object-to-world transformation matrix. The bounds should be as exact as - * possible, if they are difficult or expensive to compute exactly, you may - * use {@link Matrix4#transform(BoundingBox)}. If the matrix is - * null no transformation is needed, and object space is - * equivalent to world space. - * - * @param o2w object to world transformation matrix - * @return object bounding box in world space - */ - public BoundingBox getWorldBounds(Matrix4 o2w); - - /** - * Returns the number of individual primtives in this aggregate object. - * - * @return number of primitives - */ - public int getNumPrimitives(); - - /** - * Retrieve the bounding box component of a particular primitive in object - * space. Even indexes get minimum values, while odd indexes get the maximum - * values for each axis. - * - * @param primID primitive index - * @param i bounding box side index - * @return value of the request bound - */ - public float getPrimitiveBound(int primID, int i); - - /** - * Intersect the specified primitive in local space. - * - * @param r ray in the object's local space - * @param primID primitive index to intersect - * @param state intersection state - * @see Ray#setMax(float) - * @see IntersectionState#setIntersection(int, float, float) - */ - public void intersectPrimitive(Ray r, int primID, IntersectionState state); - - /** - * Prepare the specified {@link ShadingState} by setting all of its internal - * parameters. - * - * @param state shading state to fill in - */ - public void prepareShadingState(ShadingState state); - - /** - * Create a new {@link PrimitiveList} object suitable for baking lightmaps. - * This means a set of primitives laid out in the unit square UV space. This - * method is optional, objects which do not support it should simply return - * null. - * - * @return a list of baking primitives - */ - public PrimitiveList getBakingPrimitives(); -} \ No newline at end of file diff --git a/src/org/sunflow/core/Ray.java b/src/org/sunflow/core/Ray.java deleted file mode 100644 index 46bc655..0000000 --- a/src/org/sunflow/core/Ray.java +++ /dev/null @@ -1,217 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.math.Matrix4; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; - -/** - * This class represents a ray as a oriented half line segment. The ray - * direction is always normalized. The valid region is delimted by two distances - * along the ray, tMin and tMax. - */ -public final class Ray { - public float ox, oy, oz; - public float dx, dy, dz; - private float tMin; - private float tMax; - private static final float EPSILON = 0;// 0.01f; - - private Ray() { - } - - /** - * Creates a new ray that points from the given origin to the given - * direction. The ray has infinite length. The direction vector is - * normalized. - * - * @param ox ray origin x - * @param oy ray origin y - * @param oz ray origin z - * @param dx ray direction x - * @param dy ray direction y - * @param dz ray direction z - */ - - public Ray(float ox, float oy, float oz, float dx, float dy, float dz) { - this.ox = ox; - this.oy = oy; - this.oz = oz; - this.dx = dx; - this.dy = dy; - this.dz = dz; - float in = 1.0f / (float) Math.sqrt(dx * dx + dy * dy + dz * dz); - this.dx *= in; - this.dy *= in; - this.dz *= in; - tMin = EPSILON; - tMax = Float.POSITIVE_INFINITY; - } - - /** - * Creates a new ray that points from the given origin to the given - * direction. The ray has infinite length. The direction vector is - * normalized. - * - * @param o ray origin - * @param d ray direction (need not be normalized) - */ - public Ray(Point3 o, Vector3 d) { - ox = o.x; - oy = o.y; - oz = o.z; - dx = d.x; - dy = d.y; - dz = d.z; - float in = 1.0f / (float) Math.sqrt(dx * dx + dy * dy + dz * dz); - dx *= in; - dy *= in; - dz *= in; - tMin = EPSILON; - tMax = Float.POSITIVE_INFINITY; - } - - /** - * Creates a new ray that points from point a to point b. The created ray - * will set tMin and tMax to limit the ray to the segment (a,b) - * (non-inclusive of a and b). This is often used to create shadow rays. - * - * @param a start point - * @param b end point - */ - public Ray(Point3 a, Point3 b) { - ox = a.x; - oy = a.y; - oz = a.z; - dx = b.x - ox; - dy = b.y - oy; - dz = b.z - oz; - tMin = EPSILON; - float n = (float) Math.sqrt(dx * dx + dy * dy + dz * dz); - float in = 1.0f / n; - dx *= in; - dy *= in; - dz *= in; - tMax = n - EPSILON; - } - - /** - * Create a new ray by transforming the supplied one by the given matrix. If - * the matrix is null, the original ray is returned. - * - * @param m matrix to transform the ray by - */ - public Ray transform(Matrix4 m) { - if (m == null) - return this; - Ray r = new Ray(); - r.ox = m.transformPX(ox, oy, oz); - r.oy = m.transformPY(ox, oy, oz); - r.oz = m.transformPZ(ox, oy, oz); - r.dx = m.transformVX(dx, dy, dz); - r.dy = m.transformVY(dx, dy, dz); - r.dz = m.transformVZ(dx, dy, dz); - r.tMin = tMin; - r.tMax = tMax; - return r; - } - - /** - * Normalize the direction component of the ray. - */ - public void normalize() { - float in = 1.0f / (float) Math.sqrt(dx * dx + dy * dy + dz * dz); - dx *= in; - dy *= in; - dz *= in; - } - - /** - * Gets the minimum distance along the ray - usually 0. - * - * @return value of the smallest distance along the ray - */ - public final float getMin() { - return tMin; - } - - /** - * Gets the maximum distance along the ray. May be infinite. - * - * @return value of the largest distance along the ray - */ - public final float getMax() { - return tMax; - } - - /** - * Creates a vector to represent the direction of the ray. - * - * @return a vector equal to the direction of this ray - */ - public final Vector3 getDirection() { - return new Vector3(dx, dy, dz); - } - - /** - * Checks to see if the specified distance falls within the valid range on - * this ray. This should always be used before an intersection with the ray - * is detected. - * - * @param t distance to be tested - * @return true if t falls between the minimum and maximum - * distance of this ray, false otherwise - */ - public final boolean isInside(float t) { - return (tMin < t) && (t < tMax); - } - - /** - * Gets the end point of the ray. A reference to dest is - * returned to support chaining. - * - * @param dest reference to the point to store - * @return reference to dest - */ - public final Point3 getPoint(Point3 dest) { - dest.x = ox + (tMax * dx); - dest.y = oy + (tMax * dy); - dest.z = oz + (tMax * dz); - return dest; - } - - /** - * Computes the dot product of an arbitrary vector with the direction of the - * ray. This method avoids having to call getDirection() which would - * instantiate a new Vector object. - * - * @param v vector - * @return dot product of the ray direction and the specified vector - */ - public final float dot(Vector3 v) { - return dx * v.x + dy * v.y + dz * v.z; - } - - /** - * Computes the dot product of an arbitrary vector with the direction of the - * ray. This method avoids having to call getDirection() which would - * instantiate a new Vector object. - * - * @param vx vector x coordinate - * @param vy vector y coordinate - * @param vz vector z coordinate - * @return dot product of the ray direction and the specified vector - */ - public final float dot(float vx, float vy, float vz) { - return dx * vx + dy * vy + dz * vz; - } - - /** - * Updates the maximum to the specified distance if and only if the new - * distance is smaller than the current one. - * - * @param t new maximum distance - */ - public final void setMax(float t) { - tMax = t; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/RenderObject.java b/src/org/sunflow/core/RenderObject.java deleted file mode 100644 index d451c62..0000000 --- a/src/org/sunflow/core/RenderObject.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.SunflowAPI; - -/** - * This is the base interface for all public rendering object interfaces. It - * handles incremental updates via {@link ParameterList} objects. - */ -public interface RenderObject { - /** - * Update this object given a list of parameters. This method is guarenteed - * to be called at least once on every object, but it should correctly - * handle empty parameter lists. This means that the object should be in a - * valid state from the time it is constructed. This method should also - * return true or false depending on whether the update was succesfull or - * not. - * - * @param pl list of parameters to read from - * @param api reference to the current scene - * @return true if the update is succesfull, - * false otherwise - */ - public boolean update(ParameterList pl, SunflowAPI api); -} \ No newline at end of file diff --git a/src/org/sunflow/core/Scene.java b/src/org/sunflow/core/Scene.java deleted file mode 100644 index 97b2cae..0000000 --- a/src/org/sunflow/core/Scene.java +++ /dev/null @@ -1,371 +0,0 @@ -package org.sunflow.core; - -import java.util.ArrayList; - -import org.sunflow.core.display.FrameDisplay; -import org.sunflow.image.Color; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.MathUtils; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -/** - * Represents a entire scene, defined as a collection of instances viewed by a - * camera. - */ -public class Scene { - // scene storage - private LightServer lightServer; - private InstanceList instanceList; - private InstanceList infiniteInstanceList; - private Camera camera; - private AccelerationStructure intAccel; - private String acceltype; - private Statistics stats; - - // baking - private boolean bakingViewDependent; - private Instance bakingInstance; - private PrimitiveList bakingPrimitives; - private AccelerationStructure bakingAccel; - - private boolean rebuildAccel; - - // image size - private int imageWidth; - private int imageHeight; - - // global options - private int threads; - private boolean lowPriority; - - /** - * Creates an empty scene. - */ - public Scene() { - lightServer = new LightServer(this); - instanceList = new InstanceList(); - infiniteInstanceList = new InstanceList(); - acceltype = "auto"; - stats = new Statistics(); - - bakingViewDependent = false; - bakingInstance = null; - bakingPrimitives = null; - bakingAccel = null; - - camera = null; - imageWidth = 640; - imageHeight = 480; - threads = 0; - lowPriority = true; - - rebuildAccel = true; - } - - /** - * Get number of allowed threads for multi-threaded operations. - * - * @return number of threads that can be started - */ - public int getThreads() { - return threads <= 0 ? Runtime.getRuntime().availableProcessors() : threads; - } - - /** - * Get the priority level to assign to multi-threaded operations. - * - * @return thread priority - */ - public int getThreadPriority() { - return lowPriority ? Thread.MIN_PRIORITY : Thread.NORM_PRIORITY; - } - - /** - * Sets the current camera (no support for multiple cameras yet). - * - * @param camera camera to be used as the viewpoint for the scene - */ - public void setCamera(Camera camera) { - this.camera = camera; - } - - Camera getCamera() { - return camera; - } - - /** - * Update the instance lists for this scene. - * - * @param instances regular instances - * @param infinite infinite instances (no bounds) - */ - public void setInstanceLists(Instance[] instances, Instance[] infinite) { - infiniteInstanceList = new InstanceList(infinite); - instanceList = new InstanceList(instances); - rebuildAccel = true; - } - - /** - * Update the light list for this scene. - * - * @param lights array of light source objects - */ - public void setLightList(LightSource[] lights) { - lightServer.setLights(lights); - } - - /** - * Enables shader overiding (set null to disable). The specified shader will - * be used to shade all surfaces - * - * @param shader shader to run over all surfaces, or null to - * disable overriding - * @param photonOverride true to override photon scattering - * with this shader or false to run the regular - * shaders - */ - public void setShaderOverride(Shader shader, boolean photonOverride) { - lightServer.setShaderOverride(shader, photonOverride); - } - - /** - * The provided instance will be considered for lightmap baking. If the - * specified instance is null, lightmap baking will be - * disabled and normal rendering will occur. - * - * @param instance instance to bake - */ - public void setBakingInstance(Instance instance) { - bakingInstance = instance; - } - - /** - * Get the radiance seen through a particular pixel - * - * @param istate intersection state for ray tracing - * @param rx pixel x coordinate - * @param ry pixel y coordinate - * @param lensU DOF sampling variable - * @param lensV DOF sampling variable - * @param time motion blur sampling variable - * @param instance QMC instance seed - * @return a shading state for the intersected primitive, or - * null if nothing is seen through the specifieFd - * point - */ - public ShadingState getRadiance(IntersectionState istate, float rx, float ry, double lensU, double lensV, double time, int instance, int dim, ShadingCache cache) { - istate.numEyeRays++; - float sceneTime = camera.getTime((float) time); - if (bakingPrimitives == null) { - Ray r = camera.getRay(rx, ry, imageWidth, imageHeight, lensU, lensV, sceneTime); - return r != null ? lightServer.getRadiance(rx, ry, sceneTime, instance, dim, r, istate, cache) : null; - } else { - Ray r = new Ray(rx / imageWidth, ry / imageHeight, -1, 0, 0, 1); - traceBake(r, istate); - if (!istate.hit()) - return null; - ShadingState state = ShadingState.createState(istate, rx, ry, sceneTime, r, instance, dim, lightServer); - bakingPrimitives.prepareShadingState(state); - if (bakingViewDependent) - state.setRay(camera.getRay(state.getPoint(), sceneTime)); - else { - Point3 p = state.getPoint(); - Vector3 n = state.getNormal(); - // create a ray coming from directly above the point being - // shaded - Ray incoming = new Ray(p.x + n.x, p.y + n.y, p.z + n.z, -n.x, -n.y, -n.z); - incoming.setMax(1); - state.setRay(incoming); - } - lightServer.shadeBakeResult(state); - return state; - } - } - - /** - * Get scene world space bounding box. - * - * @return scene bounding box - */ - public BoundingBox getBounds() { - return instanceList.getWorldBounds(null); - } - - public void accumulateStats(IntersectionState state) { - stats.accumulate(state); - } - - public void accumulateStats(ShadingCache cache) { - stats.accumulate(cache); - } - - void trace(Ray r, IntersectionState state) { - // stats - state.numRays++; - // reset object - state.instance = null; - state.current = null; - for (int i = 0; i < infiniteInstanceList.getNumPrimitives(); i++) - infiniteInstanceList.intersectPrimitive(r, i, state); - // reset for next accel structure - state.current = null; - intAccel.intersect(r, state); - } - - Color traceShadow(Ray r, IntersectionState state) { - state.numShadowRays++; - trace(r, state); - return state.hit() ? Color.WHITE : Color.BLACK; - } - - void traceBake(Ray r, IntersectionState state) { - // set the instance as if tracing a regular instanced object - state.current = bakingInstance; - // reset object - state.instance = null; - bakingAccel.intersect(r, state); - } - - private void createAreaLightInstances() { - ArrayList infiniteAreaLights = null; - ArrayList areaLights = null; - // create an area light instance from each light source if possible - for (LightSource l : lightServer.lights) { - Instance lightInstance = l.createInstance(); - if (lightInstance != null) { - if (lightInstance.getBounds() == null) { - if (infiniteAreaLights == null) - infiniteAreaLights = new ArrayList(); - infiniteAreaLights.add(lightInstance); - } else { - if (areaLights == null) - areaLights = new ArrayList(); - areaLights.add(lightInstance); - } - } - } - // add area light sources to the list of instances if they exist - if (infiniteAreaLights != null && infiniteAreaLights.size() > 0) - infiniteInstanceList.addLightSourceInstances(infiniteAreaLights.toArray(new Instance[infiniteAreaLights.size()])); - else - infiniteInstanceList.clearLightSources(); - if (areaLights != null && areaLights.size() > 0) - instanceList.addLightSourceInstances(areaLights.toArray(new Instance[areaLights.size()])); - else - instanceList.clearLightSources(); - // FIXME: this _could_ be done incrementally to avoid top-level rebuilds - // each frame - rebuildAccel = true; - } - - private void removeAreaLightInstances() { - infiniteInstanceList.clearLightSources(); - instanceList.clearLightSources(); - } - - /** - * Render the scene using the specified options, image sampler and display. - * - * @param options rendering options object - * @param sampler image sampler - * @param display display to send the final image to, a default display will - * be created if null - */ - public void render(Options options, ImageSampler sampler, Display display) { - stats.reset(); - if (display == null) - display = new FrameDisplay(); - - if (bakingInstance != null) { - UI.printDetailed(Module.SCENE, "Creating primitives for lightmapping ..."); - bakingPrimitives = bakingInstance.getBakingPrimitives(); - if (bakingPrimitives == null) { - UI.printError(Module.SCENE, "Lightmap baking is not supported for the given instance."); - return; - } - int n = bakingPrimitives.getNumPrimitives(); - UI.printInfo(Module.SCENE, "Building acceleration structure for lightmapping (%d num primitives) ...", n); - bakingAccel = AccelerationStructureFactory.create("auto", n, true); - bakingAccel.build(bakingPrimitives); - } else { - bakingPrimitives = null; - bakingAccel = null; - } - bakingViewDependent = options.getBoolean("baking.viewdep", bakingViewDependent); - - if ((bakingInstance != null && bakingViewDependent && camera == null) || (bakingInstance == null && camera == null)) { - UI.printError(Module.SCENE, "No camera found"); - return; - } - - // read from options - threads = options.getInt("threads", 0); - lowPriority = options.getBoolean("threads.lowPriority", true); - imageWidth = options.getInt("resolutionX", 640); - imageHeight = options.getInt("resolutionY", 480); - // limit resolution to 16k - imageWidth = MathUtils.clamp(imageWidth, 1, 1 << 14); - imageHeight = MathUtils.clamp(imageHeight, 1, 1 << 14); - - // prepare lights - createAreaLightInstances(); - - // get acceleration structure info - // count scene primitives - long numPrimitives = 0; - for (int i = 0; i < instanceList.getNumPrimitives(); i++) - numPrimitives += instanceList.getNumPrimitives(i); - UI.printInfo(Module.SCENE, "Scene stats:"); - UI.printInfo(Module.SCENE, " * Infinite instances: %d", infiniteInstanceList.getNumPrimitives()); - UI.printInfo(Module.SCENE, " * Instances: %d", instanceList.getNumPrimitives()); - UI.printInfo(Module.SCENE, " * Primitives: %d", numPrimitives); - String accelName = options.getString("accel", null); - if (accelName != null) { - rebuildAccel = rebuildAccel || !acceltype.equals(accelName); - acceltype = accelName; - } - UI.printInfo(Module.SCENE, " * Instance accel: %s", acceltype); - if (rebuildAccel) { - intAccel = AccelerationStructureFactory.create(acceltype, instanceList.getNumPrimitives(), false); - intAccel.build(instanceList); - rebuildAccel = false; - } - UI.printInfo(Module.SCENE, " * Scene bounds: %s", getBounds()); - UI.printInfo(Module.SCENE, " * Scene center: %s", getBounds().getCenter()); - UI.printInfo(Module.SCENE, " * Scene diameter: %.2f", getBounds().getExtents().length()); - UI.printInfo(Module.SCENE, " * Lightmap bake: %s", bakingInstance != null ? (bakingViewDependent ? "view" : "ortho") : "off"); - if (sampler == null) - return; - if (!lightServer.build(options)) - return; - // render - UI.printInfo(Module.SCENE, "Rendering ..."); - stats.setResolution(imageWidth, imageHeight); - sampler.prepare(options, this, imageWidth, imageHeight); - sampler.render(display); - // show statistics - stats.displayStats(); - lightServer.showStats(); - // discard area lights - removeAreaLightInstances(); - // discard baking tesselation/accel structure - bakingPrimitives = null; - bakingAccel = null; - UI.printInfo(Module.SCENE, "Done."); - } - - /** - * Create a photon map as prescribed by the given {@link PhotonStore}. - * - * @param map object that will recieve shot photons - * @param type type of photons being shot - * @param seed QMC seed parameter - * @return true upon success - */ - public boolean calculatePhotons(PhotonStore map, String type, int seed, Options options) { - return lightServer.calculatePhotons(map, type, seed, options); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/SceneParser.java b/src/org/sunflow/core/SceneParser.java deleted file mode 100644 index 369a7c7..0000000 --- a/src/org/sunflow/core/SceneParser.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.SunflowAPI; -import org.sunflow.SunflowAPIInterface; - -/** - * Simple interface to allow for scene creation from arbitrary file formats. - */ -public interface SceneParser { - /** - * Parse the specified file to create a scene description into the provided - * {@link SunflowAPI} object. - * - * @param filename filename to parse - * @param api scene to parse the file into - * @return true upon sucess, or false if - * errors have occured. - */ - public boolean parse(String filename, SunflowAPIInterface api); -} \ No newline at end of file diff --git a/src/org/sunflow/core/Shader.java b/src/org/sunflow/core/Shader.java deleted file mode 100644 index dc9d14c..0000000 --- a/src/org/sunflow/core/Shader.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.image.Color; - -/** - * A shader represents a particular light-surface interaction. - */ -public interface Shader extends RenderObject { - /** - * Gets the radiance for a specified rendering state. When this method is - * called, you can assume that a hit has been registered in the state and - * that the hit surface information has been computed. - * - * @param state current render state - * @return color emitted or reflected by the shader - */ - public Color getRadiance(ShadingState state); - - /** - * Scatter a photon with the specied power. Incoming photon direction is - * specified by the ray attached to the current render state. This method - * can safely do nothing if photon scattering is not supported or relevant - * for the shader type. - * - * @param state current state - * @param power power of the incoming photon. - */ - public void scatterPhoton(ShadingState state, Color power); -} \ No newline at end of file diff --git a/src/org/sunflow/core/ShadingCache.java b/src/org/sunflow/core/ShadingCache.java deleted file mode 100644 index c7e9faa..0000000 --- a/src/org/sunflow/core/ShadingCache.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.image.Color; - -public class ShadingCache { - private Sample first; - private int depth; - // stats - long hits; - long misses; - long sumDepth; - long numCaches; - - private static class Sample { - Instance i; - Shader s; - float nx, ny, nz; - float dx, dy, dz; - Color c; - Sample next; // linked list - } - - public ShadingCache() { - reset(); - hits = 0; - misses = 0; - } - - public void reset() { - sumDepth += depth; - if (depth > 0) - numCaches++; - first = null; - depth = 0; - } - - public Color lookup(ShadingState state, Shader shader) { - if (state.getNormal() == null) - return null; - // search further - for (Sample s = first; s != null; s = s.next) { - if (s.i != state.getInstance()) - continue; - if (s.s != shader) - continue; - if (state.getRay().dot(s.dx, s.dy, s.dz) < 0.999f) - continue; - if (state.getNormal().dot(s.nx, s.ny, s.nz) < 0.99f) - continue; - // we have a match - hits++; - return s.c; - } - misses++; - return null; - } - - public void add(ShadingState state, Shader shader, Color c) { - if (state.getNormal() == null) - return; - depth++; - Sample s = new Sample(); - s.i = state.getInstance(); - s.s = shader; - s.c = c; - s.dx = state.getRay().dx; - s.dy = state.getRay().dy; - s.dz = state.getRay().dz; - s.nx = state.getNormal().x; - s.ny = state.getNormal().y; - s.nz = state.getNormal().z; - s.next = first; - first = s; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/ShadingState.java b/src/org/sunflow/core/ShadingState.java deleted file mode 100644 index d4d3613..0000000 --- a/src/org/sunflow/core/ShadingState.java +++ /dev/null @@ -1,929 +0,0 @@ -package org.sunflow.core; - -import java.util.Iterator; - -import org.sunflow.core.primitive.TriangleMesh; -import org.sunflow.image.Color; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point2; -import org.sunflow.math.Point3; -import org.sunflow.math.QMC; -import org.sunflow.math.Vector3; - -/** - * Represents a point to be shaded and provides various options for the shading - * of this point, including spawning of new rays. - */ -public final class ShadingState implements Iterable { - private IntersectionState istate; - private LightServer server; - private float rx, ry, time; - private Color result; - private Point3 p; - private Vector3 n; - private Point2 tex; - private Vector3 ng; - private OrthoNormalBasis basis; - private float cosND; - private float bias; - private boolean behind; - private float hitU, hitV, hitW; - private Instance instance; - private int primitiveID; - private Matrix4 o2w, w2o; - private Ray r; - private int d; // quasi monte carlo instance variables - private int i; // quasi monte carlo instance variables - private double qmcD0I; - private double qmcD1I; - private Shader shader; - private Modifier modifier; - private int diffuseDepth; - private int reflectionDepth; - private int refractionDepth; - private boolean includeLights; - private boolean includeSpecular; - private LightSample lightSample; - private PhotonStore map; - - static ShadingState createPhotonState(Ray r, IntersectionState istate, int i, PhotonStore map, LightServer server) { - ShadingState s = new ShadingState(null, istate, r, i, 4); - s.server = server; - s.map = map; - return s; - - } - - static ShadingState createState(IntersectionState istate, float rx, float ry, float time, Ray r, int i, int d, LightServer server) { - ShadingState s = new ShadingState(null, istate, r, i, d); - s.server = server; - s.rx = rx; - s.ry = ry; - s.time = time; - return s; - } - - static ShadingState createDiffuseBounceState(ShadingState previous, Ray r, int i) { - ShadingState s = new ShadingState(previous, previous.istate, r, i, 2); - s.diffuseDepth++; - return s; - } - - static ShadingState createGlossyBounceState(ShadingState previous, Ray r, int i) { - ShadingState s = new ShadingState(previous, previous.istate, r, i, 2); - s.includeLights = false; - s.includeSpecular = false; - s.reflectionDepth++; - return s; - } - - static ShadingState createReflectionBounceState(ShadingState previous, Ray r, int i) { - ShadingState s = new ShadingState(previous, previous.istate, r, i, 2); - s.reflectionDepth++; - return s; - } - - static ShadingState createRefractionBounceState(ShadingState previous, Ray r, int i) { - ShadingState s = new ShadingState(previous, previous.istate, r, i, 2); - s.refractionDepth++; - return s; - } - - static ShadingState createFinalGatherState(ShadingState state, Ray r, int i) { - ShadingState finalGatherState = new ShadingState(state, state.istate, r, i, 2); - finalGatherState.diffuseDepth++; - finalGatherState.includeLights = false; - finalGatherState.includeSpecular = false; - return finalGatherState; - } - - private ShadingState(ShadingState previous, IntersectionState istate, Ray r, int i, int d) { - this.r = r; - this.istate = istate; - this.i = i; - this.d = d; - time = istate.time; - instance = istate.instance; // local copy - primitiveID = istate.id; - hitU = istate.u; - hitV = istate.v; - hitW = istate.w; - // get matrices for current time - o2w = instance.getObjectToWorld(time); - w2o = instance.getWorldToObject(time); - if (previous == null) { - diffuseDepth = 0; - reflectionDepth = 0; - refractionDepth = 0; - - } else { - diffuseDepth = previous.diffuseDepth; - reflectionDepth = previous.reflectionDepth; - refractionDepth = previous.refractionDepth; - server = previous.server; - map = previous.map; - rx = previous.rx; - ry = previous.ry; - this.i += previous.i; - this.d += previous.d; - } - behind = false; - cosND = Float.NaN; - includeLights = includeSpecular = true; - qmcD0I = QMC.halton(this.d, this.i); - qmcD1I = QMC.halton(this.d + 1, this.i); - result = null; - bias = 0.001f; - } - - final void setRay(Ray r) { - this.r = r; - } - - /** - * Create objects needed for surface shading: point, normal, texture - * coordinates and basis. - */ - public final void init() { - p = new Point3(); - n = new Vector3(); - tex = new Point2(); - ng = new Vector3(); - basis = null; - } - - /** - * Run the shader at this surface point. - * - * @return shaded result - */ - public final Color shade() { - return server.shadeHit(this); - } - - final void correctShadingNormal() { - // correct shading normals pointing the wrong way - if (Vector3.dot(n, ng) < 0) { - n.negate(); - basis.flipW(); - } - } - - /** - * Flip the surface normals to ensure they are facing the current ray. This - * method also offsets the shading point away from the surface so that new - * rays will not intersect the same surface again by mistake. - */ - public final void faceforward() { - // make sure we are on the right side of the material - if (r.dot(ng) < 0) { - } else { - // this ensure the ray and the geomtric normal are pointing in the - // same direction - ng.negate(); - n.negate(); - basis.flipW(); - behind = true; - } - cosND = Math.max(-r.dot(n), 0); // can't be negative - // offset the shaded point away from the surface to prevent - // self-intersection errors - if (Math.abs(ng.x) > Math.abs(ng.y) && Math.abs(ng.x) > Math.abs(ng.z)) - bias = Math.max(bias, 25 * Math.ulp(Math.abs(p.x))); - else if (Math.abs(ng.y) > Math.abs(ng.z)) - bias = Math.max(bias, 25 * Math.ulp(Math.abs(p.y))); - else - bias = Math.max(bias, 25 * Math.ulp(Math.abs(p.z))); - p.x += bias * ng.x; - p.y += bias * ng.y; - p.z += bias * ng.z; - } - - /** - * Get x coordinate of the pixel being shaded. - * - * @return pixel x coordinate - */ - public final float getRasterX() { - return rx; - } - - /** - * Get y coordinate of the pixel being shaded. - * - * @return pixel y coordinate - */ - public final float getRasterY() { - return ry; - } - - /** - * Cosine between the shading normal and the ray. This is set by - * {@link #faceforward()}. - * - * @return cosine between shading normal and the ray - */ - public final float getCosND() { - return cosND; - } - - /** - * Returns true if the ray hit the surface from behind. This is set by - * {@link #faceforward()}. - * - * @return true if the surface was hit from behind. - */ - public final boolean isBehind() { - return behind; - } - - final IntersectionState getIntersectionState() { - return istate; - } - - /** - * Get u barycentric coordinate of the intersection point. - * - * @return u barycentric coordinate - */ - public final float getU() { - return hitU; - } - - /** - * Get v barycentric coordinate of the intersection point. - * - * @return v barycentric coordinate - */ - public final float getV() { - return hitV; - } - - /** - * Get w barycentric coordinate of the intersection point. - * - * @return w barycentric coordinate - */ - public final float getW() { - return hitW; - } - - /** - * Get the instance which was intersected - * - * @return intersected instance object - */ - public final Instance getInstance() { - return instance; - } - - /** - * Get the primitive ID which was intersected - * - * @return intersected primitive ID - */ - public final int getPrimitiveID() { - return primitiveID; - } - - /** - * Transform the given point from object space to world space. A new - * {@link Point3} object is returned. - * - * @param p object space position to transform - * @return transformed position - */ - public Point3 transformObjectToWorld(Point3 p) { - return o2w == null ? new Point3(p) : o2w.transformP(p); - } - - /** - * Transform the given point from world space to object space. A new - * {@link Point3} object is returned. - * - * @param p world space position to transform - * @return transformed position - */ - public Point3 transformWorldToObject(Point3 p) { - return w2o == null ? new Point3(p) : w2o.transformP(p); - } - - /** - * Transform the given normal from object space to world space. A new - * {@link Vector3} object is returned. - * - * @param n object space normal to transform - * @return transformed normal - */ - public Vector3 transformNormalObjectToWorld(Vector3 n) { - return o2w == null ? new Vector3(n) : w2o.transformTransposeV(n); - } - - /** - * Transform the given normal from world space to object space. A new - * {@link Vector3} object is returned. - * - * @param n world space normal to transform - * @return transformed normal - */ - public Vector3 transformNormalWorldToObject(Vector3 n) { - return o2w == null ? new Vector3(n) : o2w.transformTransposeV(n); - } - - /** - * Transform the given vector from object space to world space. A new - * {@link Vector3} object is returned. - * - * @param v object space vector to transform - * @return transformed vector - */ - public Vector3 transformVectorObjectToWorld(Vector3 v) { - return o2w == null ? new Vector3(v) : o2w.transformV(v); - } - - /** - * Transform the given vector from world space to object space. A new - * {@link Vector3} object is returned. - * - * @param v world space vector to transform - * @return transformed vector - */ - public Vector3 transformVectorWorldToObject(Vector3 v) { - return o2w == null ? new Vector3(v) : w2o.transformV(v); - } - - final void setResult(Color c) { - result = c; - } - - /** - * Get the result of shading this point - * - * @return shaded result - */ - public final Color getResult() { - return result; - } - - final LightServer getLightServer() { - return server; - } - - /** - * Add the specified light sample to the list of lights to be used - * - * @param sample a valid light sample - */ - public final void addSample(LightSample sample) { - // add to list - sample.next = lightSample; - lightSample = sample; - } - - /** - * Get a QMC sample from an infinite sequence. - * - * @param j sample number (starts from 0) - * @param dim dimension to sample - * @return pseudo-random value in [0,1) - */ - public final double getRandom(int j, int dim) { - switch (dim) { - case 0: - return QMC.mod1(qmcD0I + QMC.halton(0, j)); - case 1: - return QMC.mod1(qmcD1I + QMC.halton(1, j)); - default: - return QMC.mod1(QMC.halton(d + dim, i) + QMC.halton(dim, j)); - } - } - - /** - * Get a QMC sample from a finite sequence of n elements. This provides - * better stratification than the infinite version, but does not allow for - * adaptive sampling. - * - * @param j sample number (starts from 0) - * @param dim dimension to sample - * @param n number of samples - * @return pseudo-random value in [0,1) - */ - public final double getRandom(int j, int dim, int n) { - switch (dim) { - case 0: - return QMC.mod1(qmcD0I + (double) j / (double) n); - case 1: - return QMC.mod1(qmcD1I + QMC.halton(0, j)); - default: - return QMC.mod1(QMC.halton(d + dim, i) + QMC.halton(dim - 1, j)); - } - } - - /** - * Checks to see if the shader should include emitted light. - * - * @return true if emitted light should be included, - * false otherwise - */ - public final boolean includeLights() { - return includeLights; - } - - /** - * Checks to see if the shader should include specular terms. - * - * @return true if specular terms should be included, - * false otherwise - */ - public final boolean includeSpecular() { - return includeSpecular; - } - - /** - * Get the shader to be used to shade this surface. - * - * @return shader to be used - */ - public final Shader getShader() { - return shader; - } - - /** - * Record which shader should be executed for the intersected surface. - * - * @param shader surface shader to use to shade the current intersection - * point - */ - public final void setShader(Shader shader) { - this.shader = shader; - } - - final Modifier getModifier() { - return modifier; - } - - /** - * Record which modifier should be applied to the intersected surface - * - * @param modifier modifier to use the change this shading state - */ - public final void setModifier(Modifier modifier) { - this.modifier = modifier; - } - - /** - * Get the current total tracing depth. First generation rays have a depth - * of 0. - * - * @return current tracing depth - */ - public final int getDepth() { - return diffuseDepth + reflectionDepth + refractionDepth; - } - - /** - * Get the current diffuse tracing depth. This is the number of diffuse - * surfaces reflected from. - * - * @return current diffuse tracing depth - */ - public final int getDiffuseDepth() { - return diffuseDepth; - } - - /** - * Get the current reflection tracing depth. This is the number of specular - * surfaces reflected from. - * - * @return current reflection tracing depth - */ - public final int getReflectionDepth() { - return reflectionDepth; - } - - /** - * Get the current refraction tracing depth. This is the number of specular - * surfaces refracted from. - * - * @return current refraction tracing depth - */ - public final int getRefractionDepth() { - return refractionDepth; - } - - /** - * Get hit point. - * - * @return hit point - */ - public final Point3 getPoint() { - return p; - } - - /** - * Get shading normal at the hit point. This may differ from the geometric - * normal - * - * @return shading normal - */ - public final Vector3 getNormal() { - return n; - } - - /** - * Get texture coordinates at the hit point. - * - * @return texture coordinate - */ - public final Point2 getUV() { - return tex; - } - - /** - * Gets the geometric normal of the current hit point. - * - * @return geometric normal of the current hit point - */ - public final Vector3 getGeoNormal() { - return ng; - } - - /** - * Gets the local orthonormal basis for the current hit point. - * - * @return local basis or null if undefined - */ - public final OrthoNormalBasis getBasis() { - return basis; - } - - /** - * Define the orthonormal basis for the current hit point. - * - * @param basis - */ - public final void setBasis(OrthoNormalBasis basis) { - this.basis = basis; - } - - /** - * Gets the ray that is associated with this state. - * - * @return ray associated with this state. - */ - public final Ray getRay() { - return r; - } - - /** - * Get a transformation matrix that will transform camera space points into - * world space. - * - * @return camera to world transform - */ - public final Matrix4 getCameraToWorld() { - Camera c = server.getScene().getCamera(); - return c != null ? c.getCameraToWorld(time) : Matrix4.IDENTITY; - } - - /** - * Get a transformation matrix that will transform world space points into - * camera space. - * - * @return world to camera transform - */ - public final Matrix4 getWorldToCamera() { - Camera c = server.getScene().getCamera(); - return c != null ? c.getWorldToCamera(time) : Matrix4.IDENTITY; - } - - /** - * Get the three triangle corners in object space if the hit object is a - * mesh, returns false otherwise. - * - * @param p array of 3 points - * @return true if the points were read succesfully, - * falseotherwise - */ - public final boolean getTrianglePoints(Point3[] p) { - PrimitiveList prims = instance.getGeometry().getPrimitiveList(); - if (prims instanceof TriangleMesh) { - TriangleMesh m = (TriangleMesh) prims; - m.getPoint(primitiveID, 0, p[0] = new Point3()); - m.getPoint(primitiveID, 1, p[1] = new Point3()); - m.getPoint(primitiveID, 2, p[2] = new Point3()); - return true; - } - return false; - } - - /** - * Initialize the use of light samples. Prepares a list of visible lights - * from the current point. - */ - public final void initLightSamples() { - server.initLightSamples(this); - } - - /** - * Add caustic samples to the current light sample set. This method does - * nothing if caustics are not enabled. - */ - public final void initCausticSamples() { - server.initCausticSamples(this); - } - - /** - * Returns the color obtained by recursively tracing the specified ray. The - * reflection is assumed to be glossy. - * - * @param r ray to trace - * @param i instance number of this sample - * @return color observed along specified ray. - */ - public final Color traceGlossy(Ray r, int i) { - return server.traceGlossy(this, r, i); - } - - /** - * Returns the color obtained by recursively tracing the specified ray. The - * reflection is assumed to be specular. - * - * @param r ray to trace - * @param i instance number of this sample - * @return color observed along specified ray. - */ - public final Color traceReflection(Ray r, int i) { - return server.traceReflection(this, r, i); - } - - /** - * Returns the color obtained by recursively tracing the specified ray. - * - * @param r ray to trace - * @param i instance number of this sample - * @return color observed along specified ray. - */ - public final Color traceRefraction(Ray r, int i) { - // this assumes the refraction ray is pointing away from the normal - r.ox -= 2 * bias * ng.x; - r.oy -= 2 * bias * ng.y; - r.oz -= 2 * bias * ng.z; - return server.traceRefraction(this, r, i); - } - - /** - * Trace transparency, this is equivalent to tracing a refraction ray in the - * incoming ray direction. - * - * @return color observed behind the current shading point - */ - public final Color traceTransparency() { - return traceRefraction(new Ray(p.x, p.y, p.z, r.dx, r.dy, r.dz), 0); - } - - /** - * Trace a shadow ray against the scene, and computes the accumulated - * opacity along the ray. - * - * @param r ray to trace - * @return opacity along the shadow ray - */ - public final Color traceShadow(Ray r) { - return server.getScene().traceShadow(r, istate); - } - - /** - * Records a photon at the specified location. - * - * @param dir incoming direction of the photon - * @param power photon power - * @param diffuse diffuse reflectance at the given point - */ - public final void storePhoton(Vector3 dir, Color power, Color diffuse) { - map.store(this, dir, power, diffuse); - } - - /** - * Trace a new photon from the current location. This assumes that the - * photon was reflected by a specular surface. - * - * @param r ray to trace photon along - * @param power power of the new photon - */ - public final void traceReflectionPhoton(Ray r, Color power) { - if (map.allowReflectionBounced()) - server.traceReflectionPhoton(this, r, power); - } - - /** - * Trace a new photon from the current location. This assumes that the - * photon was refracted by a specular surface. - * - * @param r ray to trace photon along - * @param power power of the new photon - */ - public final void traceRefractionPhoton(Ray r, Color power) { - if (map.allowRefractionBounced()) { - // this assumes the refraction ray is pointing away from the normal - r.ox -= 0.002f * ng.x; - r.oy -= 0.002f * ng.y; - r.oz -= 0.002f * ng.z; - server.traceRefractionPhoton(this, r, power); - } - } - - /** - * Trace a new photon from the current location. This assumes that the - * photon was reflected by a diffuse surface. - * - * @param r ray to trace photon along - * @param power power of the new photon - */ - public final void traceDiffusePhoton(Ray r, Color power) { - if (map.allowDiffuseBounced()) - server.traceDiffusePhoton(this, r, power); - } - - /** - * Returns the glboal diffuse radiance estimate given by the current - * {@link GIEngine} if present. - * - * @return global diffuse radiance estimate - */ - public final Color getGlobalRadiance() { - return server.getGlobalRadiance(this); - } - - /** - * Gets the total irradiance reaching the current point from diffuse - * surfaces. - * - * @param diffuseReflectance diffuse reflectance at the current point, can - * be used for importance tracking - * @return indirect diffuse irradiance reaching the point - */ - public final Color getIrradiance(Color diffuseReflectance) { - return server.getIrradiance(this, diffuseReflectance); - } - - /** - * Trace a final gather ray and return the intersection result as a new - * render state - * - * @param r ray to shoot - * @param i instance of the ray - * @return new render state object corresponding to the intersection result - */ - public final ShadingState traceFinalGather(Ray r, int i) { - return server.traceFinalGather(this, r, i); - } - - /** - * Simple black and white ambient occlusion. - * - * @param samples number of sample rays - * @param maxDist maximum length of the rays - * @return occlusion color - */ - public final Color occlusion(int samples, float maxDist) { - return occlusion(samples, maxDist, Color.WHITE, Color.BLACK); - } - - /** - * Ambient occlusion routine, returns a value between bright and dark - * depending on the amount of geometric occlusion in the scene. - * - * @param samples number of sample rays - * @param maxDist maximum length of the rays - * @param bright color when nothing is occluded - * @param dark color when fully occluded - * @return occlusion color - */ - public final Color occlusion(int samples, float maxDist, Color bright, Color dark) { - if (n == null) { - // in case we got called on a geometry without orientation - return bright; - } - // make sure we are on the right side of the material - faceforward(); - OrthoNormalBasis onb = getBasis(); - Vector3 w = new Vector3(); - Color result = Color.black(); - for (int i = 0; i < samples; i++) { - float xi = (float) getRandom(i, 0, samples); - float xj = (float) getRandom(i, 1, samples); - float phi = (float) (2 * Math.PI * xi); - float cosPhi = (float) Math.cos(phi); - float sinPhi = (float) Math.sin(phi); - float sinTheta = (float) Math.sqrt(xj); - float cosTheta = (float) Math.sqrt(1.0f - xj); - w.x = cosPhi * sinTheta; - w.y = sinPhi * sinTheta; - w.z = cosTheta; - onb.transform(w); - Ray r = new Ray(p, w); - r.setMax(maxDist); - result.add(Color.blend(bright, dark, traceShadow(r))); - } - return result.mul(1.0f / samples); - } - - /** - * Computes a plain diffuse response to the current light samples and global - * illumination. - * - * @param diff diffuse color - * @return shaded result - */ - public final Color diffuse(Color diff) { - // integrate a diffuse function - Color lr = Color.black(); - if (diff.isBlack()) - return lr; - for (LightSample sample : this) - lr.madd(sample.dot(n), sample.getDiffuseRadiance()); - lr.add(getIrradiance(diff)); - return lr.mul(diff).mul(1.0f / (float) Math.PI); - } - - /** - * Computes a phong specular response to the current light samples and - * global illumination. - * - * @param spec specular color - * @param power phong exponent - * @param numRays number of glossy rays to trace - * @return shaded color - */ - public final Color specularPhong(Color spec, float power, int numRays) { - // integrate a phong specular function - Color lr = Color.black(); - if (!includeSpecular || spec.isBlack()) - return lr; - // reflected direction - float dn = 2 * cosND; - Vector3 refDir = new Vector3(); - refDir.x = (dn * n.x) + r.dx; - refDir.y = (dn * n.y) + r.dy; - refDir.z = (dn * n.z) + r.dz; - // direct lighting - for (LightSample sample : this) { - float cosNL = sample.dot(n); - float cosLR = sample.dot(refDir); - if (cosLR > 0) - lr.madd(cosNL * (float) Math.pow(cosLR, power), sample.getSpecularRadiance()); - } - // indirect lighting - if (numRays > 0) { - int numSamples = getDepth() == 0 ? numRays : 1; - OrthoNormalBasis onb = OrthoNormalBasis.makeFromW(refDir); - float mul = (2.0f * (float) Math.PI / (power + 1)) / numSamples; - for (int i = 0; i < numSamples; i++) { - // specular indirect lighting - double r1 = getRandom(i, 0, numSamples); - double r2 = getRandom(i, 1, numSamples); - double u = 2 * Math.PI * r1; - double s = (float) Math.pow(r2, 1 / (power + 1)); - double s1 = (float) Math.sqrt(1 - s * s); - Vector3 w = new Vector3((float) (Math.cos(u) * s1), (float) (Math.sin(u) * s1), (float) s); - w = onb.transform(w, new Vector3()); - float wn = Vector3.dot(w, n); - if (wn > 0) - lr.madd(wn * mul, traceGlossy(new Ray(p, w), i)); - } - } - lr.mul(spec).mul((power + 2) / (2.0f * (float) Math.PI)); - return lr; - } - - /** - * Allows iteration over current light samples. - */ - public Iterator iterator() { - return new LightSampleIterator(lightSample); - } - - private static class LightSampleIterator implements Iterator { - private LightSample current; - - LightSampleIterator(LightSample head) { - current = head; - } - - public boolean hasNext() { - return current != null; - } - - public LightSample next() { - LightSample c = current; - current = current.next; - return c; - } - - public void remove() { - throw new UnsupportedOperationException(); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/Statistics.java b/src/org/sunflow/core/Statistics.java deleted file mode 100644 index bb7b06c..0000000 --- a/src/org/sunflow/core/Statistics.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class Statistics { - // raytracing - private long numEyeRays; - private long numShadowRays; - private long numReflectionRays; - private long numGlossyRays; - private long numRefractionRays; - private long numRays; - private long numPixels; - // shading cache - private long cacheHits; - private long cacheMisses; - private long cacheSumDepth; - private long cacheNumCaches; - - Statistics() { - reset(); - } - - void reset() { - numEyeRays = 0; - numShadowRays = 0; - numReflectionRays = 0; - numGlossyRays = 0; - numRefractionRays = 0; - numRays = 0; - numPixels = 0; - cacheHits = 0; - cacheMisses = 0; - cacheSumDepth = 0; - cacheNumCaches = 0; - } - - void accumulate(IntersectionState state) { - numEyeRays += state.numEyeRays; - numShadowRays += state.numShadowRays; - numReflectionRays += state.numReflectionRays; - numGlossyRays += state.numGlossyRays; - numRefractionRays += state.numRefractionRays; - numRays += state.numRays; - } - - void accumulate(ShadingCache cache) { - cacheHits += cache.hits; - cacheMisses += cache.misses; - cacheSumDepth += cache.sumDepth; - cacheNumCaches += cache.numCaches; - } - - void setResolution(int w, int h) { - numPixels = w * h; - } - - void displayStats() { - // display raytracing stats - UI.printInfo(Module.SCENE, "Raytracing stats:"); - UI.printInfo(Module.SCENE, " * Rays traced: (per pixel) (per eye ray) (percentage)", numRays); - printRayTypeStats("eye", numEyeRays); - printRayTypeStats("shadow", numShadowRays); - printRayTypeStats("reflection", numReflectionRays); - printRayTypeStats("glossy", numGlossyRays); - printRayTypeStats("refraction", numRefractionRays); - printRayTypeStats("other", numRays - numEyeRays - numShadowRays - numReflectionRays - numGlossyRays - numRefractionRays); - printRayTypeStats("total", numRays); - if (cacheHits + cacheMisses > 0) { - UI.printInfo(Module.LIGHT, "Shading cache stats:"); - UI.printInfo(Module.LIGHT, " * Lookups: %d", cacheHits + cacheMisses); - UI.printInfo(Module.LIGHT, " * Hits: %d", cacheHits); - UI.printInfo(Module.LIGHT, " * Hit rate: %d%%", (100 * cacheHits) / (cacheHits + cacheMisses)); - UI.printInfo(Module.LIGHT, " * Average cache depth: %.2f", (double) cacheSumDepth / (double) cacheNumCaches); - } - } - - private void printRayTypeStats(String name, long n) { - if (n > 0) - UI.printInfo(Module.SCENE, " %-10s %11d %7.2f %7.2f %6.2f%%", name, n, (double) n / (double) numPixels, (double) n / (double) numEyeRays, (double) (n * 100) / (double) numRays); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/Tesselatable.java b/src/org/sunflow/core/Tesselatable.java deleted file mode 100644 index 672bb15..0000000 --- a/src/org/sunflow/core/Tesselatable.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.sunflow.core; - -import org.sunflow.core.primitive.TriangleMesh; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; - -/** - * Represents an object which can be tesselated into a list of primitives such - * as a {@link TriangleMesh}. - */ -public interface Tesselatable extends RenderObject { - /** - * Tesselate this object into a {@link PrimitiveList}. This may return - * null if tesselation fails. - * - * @return a list of primitives generated by the tesselation - */ - public PrimitiveList tesselate(); - - /** - * Compute a bounding box of this object in world space, using the specified - * object-to-world transformation matrix. The bounds should be as exact as - * possible, if they are difficult or expensive to compute exactly, you may - * use {@link Matrix4#transform(BoundingBox)}. If the matrix is - * null no transformation is needed, and object space is - * equivalent to world space. This method may return null if - * these bounds are difficult or impossible to compute, in which case the - * tesselation will be executed right away and the bounds of the resulting - * primitives will be used. - * - * @param o2w object to world transformation matrix - * @return object bounding box in world space - */ - public BoundingBox getWorldBounds(Matrix4 o2w); -} \ No newline at end of file diff --git a/src/org/sunflow/core/Texture.java b/src/org/sunflow/core/Texture.java deleted file mode 100644 index 7719ce9..0000000 --- a/src/org/sunflow/core/Texture.java +++ /dev/null @@ -1,123 +0,0 @@ -package org.sunflow.core; - -import java.io.IOException; - -import org.sunflow.PluginRegistry; -import org.sunflow.image.Bitmap; -import org.sunflow.image.BitmapReader; -import org.sunflow.image.Color; -import org.sunflow.image.BitmapReader.BitmapFormatException; -import org.sunflow.image.formats.BitmapBlack; -import org.sunflow.math.MathUtils; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Vector3; -import org.sunflow.system.FileUtils; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -/** - * Represents a 2D texture, typically used by {@link Shader shaders}. - */ -public class Texture { - private String filename; - private boolean isLinear; - private Bitmap bitmap; - private int loaded; - - /** - * Creates a new texture from the specfied file. - * - * @param filename image file to load - * @param isLinear is the texture gamma corrected already? - */ - Texture(String filename, boolean isLinear) { - this.filename = filename; - this.isLinear = isLinear; - loaded = 0; - } - - private synchronized void load() { - if (loaded != 0) - return; - String extension = FileUtils.getExtension(filename); - try { - UI.printInfo(Module.TEX, "Reading texture bitmap from: \"%s\" ...", filename); - BitmapReader reader = PluginRegistry.bitmapReaderPlugins.createObject(extension); - if (reader != null) { - bitmap = reader.load(filename, isLinear); - if (bitmap.getWidth() == 0 || bitmap.getHeight() == 0) - bitmap = null; - } - if (bitmap == null) { - UI.printError(Module.TEX, "Bitmap reading failed"); - bitmap = new BitmapBlack(); - } else - UI.printDetailed(Module.TEX, "Texture bitmap reading complete: %dx%d pixels found", bitmap.getWidth(), bitmap.getHeight()); - } catch (IOException e) { - UI.printError(Module.TEX, "%s", e.getMessage()); - } catch (BitmapFormatException e) { - UI.printError(Module.TEX, "%s format error: %s", extension, e.getMessage()); - } - loaded = 1; - } - - public Bitmap getBitmap() { - if (loaded == 0) - load(); - return bitmap; - } - - /** - * Gets the color at location (x,y) in the texture. The lookup is performed - * using the fractional component of the coordinates, treating the texture - * as a unit square tiled in both directions. Bicubic filtering is performed - * on the four nearest pixels to the lookup point. - * - * @param x x coordinate into the texture - * @param y y coordinate into the texture - * @return filtered color at location (x,y) - */ - public Color getPixel(float x, float y) { - Bitmap bitmap = getBitmap(); - x = MathUtils.frac(x); - y = MathUtils.frac(y); - float dx = x * (bitmap.getWidth() - 1); - float dy = y * (bitmap.getHeight() - 1); - int ix0 = (int) dx; - int iy0 = (int) dy; - int ix1 = (ix0 + 1) % bitmap.getWidth(); - int iy1 = (iy0 + 1) % bitmap.getHeight(); - float u = dx - ix0; - float v = dy - iy0; - u = u * u * (3.0f - (2.0f * u)); - v = v * v * (3.0f - (2.0f * v)); - float k00 = (1.0f - u) * (1.0f - v); - Color c00 = bitmap.readColor(ix0, iy0); - float k01 = (1.0f - u) * v; - Color c01 = bitmap.readColor(ix0, iy1); - float k10 = u * (1.0f - v); - Color c10 = bitmap.readColor(ix1, iy0); - float k11 = u * v; - Color c11 = bitmap.readColor(ix1, iy1); - Color c = Color.mul(k00, c00); - c.madd(k01, c01); - c.madd(k10, c10); - c.madd(k11, c11); - return c; - } - - public Vector3 getNormal(float x, float y, OrthoNormalBasis basis) { - float[] rgb = getPixel(x, y).getRGB(); - return basis.transform(new Vector3(2 * rgb[0] - 1, 2 * rgb[1] - 1, 2 * rgb[2] - 1)).normalize(); - } - - public Vector3 getBump(float x, float y, OrthoNormalBasis basis, float scale) { - Bitmap bitmap = getBitmap(); - float dx = 1.0f / bitmap.getWidth(); - float dy = 1.0f / bitmap.getHeight(); - float b0 = getPixel(x, y).getLuminance(); - float bx = getPixel(x + dx, y).getLuminance(); - float by = getPixel(x, y + dy).getLuminance(); - return basis.transform(new Vector3(scale * (b0 - bx), scale * (b0 - by), 1)).normalize(); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/TextureCache.java b/src/org/sunflow/core/TextureCache.java deleted file mode 100644 index 52ba7b3..0000000 --- a/src/org/sunflow/core/TextureCache.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.sunflow.core; - -import java.util.HashMap; - -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -/** - * Maintains a cache of all loaded texture maps. This is usefull if the same - * texture might be used more than once in your scene. - */ -public final class TextureCache { - private static HashMap textures = new HashMap(); - - private TextureCache() { - } - - /** - * Gets a reference to the texture specified by the given filename. If the - * texture has already been loaded the previous reference is returned, - * otherwise, a new texture is created. - * - * @param filename image file to load - * @param isLinear is the texture gamma corrected? - * @return texture object - * @see Texture - */ - public synchronized static Texture getTexture(String filename, boolean isLinear) { - if (textures.containsKey(filename)) { - UI.printInfo(Module.TEX, "Using cached copy for file \"%s\" ...", filename); - return textures.get(filename); - } - UI.printInfo(Module.TEX, "Using file \"%s\" ...", filename); - Texture t = new Texture(filename, isLinear); - textures.put(filename, t); - return t; - } - - /** - * Flush all textures from the cache, this will cause them to be reloaded - * anew the next time they are accessed. - */ - public synchronized static void flush() { - UI.printInfo(Module.TEX, "Flushing texture cache"); - textures.clear(); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/accel/BoundingIntervalHierarchy.java b/src/org/sunflow/core/accel/BoundingIntervalHierarchy.java deleted file mode 100644 index 6335006..0000000 --- a/src/org/sunflow/core/accel/BoundingIntervalHierarchy.java +++ /dev/null @@ -1,613 +0,0 @@ -package org.sunflow.core.accel; - -import org.sunflow.core.AccelerationStructure; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.math.BoundingBox; -import org.sunflow.system.Memory; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; -import org.sunflow.util.IntArray; - -public class BoundingIntervalHierarchy implements AccelerationStructure { - private int[] tree; - private int[] objects; - private PrimitiveList primitives; - private BoundingBox bounds; - private int maxPrims; - - public BoundingIntervalHierarchy() { - maxPrims = 2; - } - - public void build(PrimitiveList primitives) { - this.primitives = primitives; - int n = primitives.getNumPrimitives(); - UI.printDetailed(Module.ACCEL, "Getting bounding box ..."); - bounds = primitives.getWorldBounds(null); - objects = new int[n]; - for (int i = 0; i < n; i++) - objects[i] = i; - UI.printDetailed(Module.ACCEL, "Creating tree ..."); - int initialSize = 3 * (2 * 6 * n + 1); - IntArray tempTree = new IntArray((initialSize + 3) / 4); - BuildStats stats = new BuildStats(); - Timer t = new Timer(); - t.start(); - buildHierarchy(tempTree, objects, stats); - t.end(); - UI.printDetailed(Module.ACCEL, "Trimming tree ..."); - tree = tempTree.trim(); - // display stats - stats.printStats(); - UI.printDetailed(Module.ACCEL, " * Creation time: %s", t); - UI.printDetailed(Module.ACCEL, " * Usage of init: %6.2f%%", (double) (100.0 * tree.length) / initialSize); - UI.printDetailed(Module.ACCEL, " * Tree memory: %s", Memory.sizeof(tree)); - UI.printDetailed(Module.ACCEL, " * Indices memory: %s", Memory.sizeof(objects)); - } - - private static class BuildStats { - private int numNodes; - private int numLeaves; - private int sumObjects; - private int minObjects; - private int maxObjects; - private int sumDepth; - private int minDepth; - private int maxDepth; - private int numLeaves0; - private int numLeaves1; - private int numLeaves2; - private int numLeaves3; - private int numLeaves4; - private int numLeaves4p; - private int numBVH2; - - BuildStats() { - numNodes = numLeaves = 0; - sumObjects = 0; - minObjects = Integer.MAX_VALUE; - maxObjects = Integer.MIN_VALUE; - sumDepth = 0; - minDepth = Integer.MAX_VALUE; - maxDepth = Integer.MIN_VALUE; - numLeaves0 = 0; - numLeaves1 = 0; - numLeaves2 = 0; - numLeaves3 = 0; - numLeaves4 = 0; - numLeaves4p = 0; - numBVH2 = 0; - } - - void updateInner() { - numNodes++; - } - - void updateBVH2() { - numBVH2++; - } - - void updateLeaf(int depth, int n) { - numLeaves++; - minDepth = Math.min(depth, minDepth); - maxDepth = Math.max(depth, maxDepth); - sumDepth += depth; - minObjects = Math.min(n, minObjects); - maxObjects = Math.max(n, maxObjects); - sumObjects += n; - switch (n) { - case 0: - numLeaves0++; - break; - case 1: - numLeaves1++; - break; - case 2: - numLeaves2++; - break; - case 3: - numLeaves3++; - break; - case 4: - numLeaves4++; - break; - default: - numLeaves4p++; - break; - } - } - - void printStats() { - UI.printDetailed(Module.ACCEL, "Tree stats:"); - UI.printDetailed(Module.ACCEL, " * Nodes: %d", numNodes); - UI.printDetailed(Module.ACCEL, " * Leaves: %d", numLeaves); - UI.printDetailed(Module.ACCEL, " * Objects: min %d", minObjects); - UI.printDetailed(Module.ACCEL, " avg %.2f", (float) sumObjects / numLeaves); - UI.printDetailed(Module.ACCEL, " avg(n>0) %.2f", (float) sumObjects / (numLeaves - numLeaves0)); - UI.printDetailed(Module.ACCEL, " max %d", maxObjects); - UI.printDetailed(Module.ACCEL, " * Depth: min %d", minDepth); - UI.printDetailed(Module.ACCEL, " avg %.2f", (float) sumDepth / numLeaves); - UI.printDetailed(Module.ACCEL, " max %d", maxDepth); - UI.printDetailed(Module.ACCEL, " * Leaves w/: N=0 %3d%%", 100 * numLeaves0 / numLeaves); - UI.printDetailed(Module.ACCEL, " N=1 %3d%%", 100 * numLeaves1 / numLeaves); - UI.printDetailed(Module.ACCEL, " N=2 %3d%%", 100 * numLeaves2 / numLeaves); - UI.printDetailed(Module.ACCEL, " N=3 %3d%%", 100 * numLeaves3 / numLeaves); - UI.printDetailed(Module.ACCEL, " N=4 %3d%%", 100 * numLeaves4 / numLeaves); - UI.printDetailed(Module.ACCEL, " N>4 %3d%%", 100 * numLeaves4p / numLeaves); - UI.printDetailed(Module.ACCEL, " * BVH2 nodes: %d (%3d%%)", numBVH2, 100 * numBVH2 / (numNodes + numLeaves - 2 * numBVH2)); - } - } - - private void buildHierarchy(IntArray tempTree, int[] indices, BuildStats stats) { - // create space for the first node - tempTree.add(3 << 30); // dummy leaf - tempTree.add(0); - tempTree.add(0); - if (objects.length == 0) - return; - // seed bbox - float[] gridBox = { bounds.getMinimum().x, bounds.getMaximum().x, - bounds.getMinimum().y, bounds.getMaximum().y, - bounds.getMinimum().z, bounds.getMaximum().z }; - float[] nodeBox = { bounds.getMinimum().x, bounds.getMaximum().x, - bounds.getMinimum().y, bounds.getMaximum().y, - bounds.getMinimum().z, bounds.getMaximum().z }; - // seed subdivide function - subdivide(0, objects.length - 1, tempTree, indices, gridBox, nodeBox, 0, 1, stats); - } - - private void createNode(IntArray tempTree, int nodeIndex, int left, int right) { - // write leaf node - tempTree.set(nodeIndex + 0, (3 << 30) | left); - tempTree.set(nodeIndex + 1, right - left + 1); - } - - private void subdivide(int left, int right, IntArray tempTree, int[] indices, float[] gridBox, float[] nodeBox, int nodeIndex, int depth, BuildStats stats) { - if ((right - left + 1) <= maxPrims || depth >= 64) { - // write leaf node - stats.updateLeaf(depth, right - left + 1); - createNode(tempTree, nodeIndex, left, right); - return; - } - // calculate extents - int axis = -1, prevAxis, rightOrig; - float clipL = Float.NaN, clipR = Float.NaN, prevClip = Float.NaN; - float split = Float.NaN, prevSplit; - boolean wasLeft = true; - while (true) { - prevAxis = axis; - prevSplit = split; - // perform quick consistency checks - float d[] = { gridBox[1] - gridBox[0], gridBox[3] - gridBox[2], - gridBox[5] - gridBox[4] }; - if (d[0] < 0 || d[1] < 0 || d[2] < 0) - throw new IllegalStateException("negative node extents"); - for (int i = 0; i < 3; i++) { - if (nodeBox[2 * i + 1] < gridBox[2 * i] || nodeBox[2 * i] > gridBox[2 * i + 1]) { - UI.printError(Module.ACCEL, "Reached tree area in error - discarding node with: %d objects", right - left + 1); - throw new IllegalStateException("invalid node overlap"); - } - } - // find longest axis - if (d[0] > d[1] && d[0] > d[2]) - axis = 0; - else if (d[1] > d[2]) - axis = 1; - else - axis = 2; - split = 0.5f * (gridBox[2 * axis] + gridBox[2 * axis + 1]); - // partition L/R subsets - clipL = Float.NEGATIVE_INFINITY; - clipR = Float.POSITIVE_INFINITY; - rightOrig = right; // save this for later - float nodeL = Float.POSITIVE_INFINITY; - float nodeR = Float.NEGATIVE_INFINITY; - for (int i = left; i <= right;) { - int obj = indices[i]; - float minb = primitives.getPrimitiveBound(obj, 2 * axis + 0); - float maxb = primitives.getPrimitiveBound(obj, 2 * axis + 1); - float center = (minb + maxb) * 0.5f; - if (center <= split) { - // stay left - i++; - if (clipL < maxb) - clipL = maxb; - } else { - // move to the right most - int t = indices[i]; - indices[i] = indices[right]; - indices[right] = t; - right--; - if (clipR > minb) - clipR = minb; - } - if (nodeL > minb) - nodeL = minb; - if (nodeR < maxb) - nodeR = maxb; - } - // check for empty space - if (nodeL > nodeBox[2 * axis + 0] && nodeR < nodeBox[2 * axis + 1]) { - float nodeBoxW = nodeBox[2 * axis + 1] - nodeBox[2 * axis + 0]; - float nodeNewW = nodeR - nodeL; - // node box is too big compare to space occupied by primitives? - if (1.3f * nodeNewW < nodeBoxW) { - stats.updateBVH2(); - int nextIndex = tempTree.getSize(); - // allocate child - tempTree.add(0); - tempTree.add(0); - tempTree.add(0); - // write bvh2 clip node - stats.updateInner(); - tempTree.set(nodeIndex + 0, (axis << 30) | (1 << 29) | nextIndex); - tempTree.set(nodeIndex + 1, Float.floatToRawIntBits(nodeL)); - tempTree.set(nodeIndex + 2, Float.floatToRawIntBits(nodeR)); - // update nodebox and recurse - nodeBox[2 * axis + 0] = nodeL; - nodeBox[2 * axis + 1] = nodeR; - subdivide(left, rightOrig, tempTree, indices, gridBox, nodeBox, nextIndex, depth + 1, stats); - return; - } - } - // ensure we are making progress in the subdivision - if (right == rightOrig) { - // all left - if (clipL <= split) { - // keep looping on left half - gridBox[2 * axis + 1] = split; - prevClip = clipL; - wasLeft = true; - continue; - } - if (prevAxis == axis && prevSplit == split) { - // we are stuck here - create a leaf - stats.updateLeaf(depth, right - left + 1); - createNode(tempTree, nodeIndex, left, right); - return; - } - gridBox[2 * axis + 1] = split; - prevClip = Float.NaN; - } else if (left > right) { - // all right - right = rightOrig; - if (clipR >= split) { - // keep looping on right half - gridBox[2 * axis + 0] = split; - prevClip = clipR; - wasLeft = false; - continue; - } - if (prevAxis == axis && prevSplit == split) { - // we are stuck here - create a leaf - stats.updateLeaf(depth, right - left + 1); - createNode(tempTree, nodeIndex, left, right); - return; - } - gridBox[2 * axis + 0] = split; - prevClip = Float.NaN; - } else { - // we are actually splitting stuff - if (prevAxis != -1 && !Float.isNaN(prevClip)) { - // second time through - lets create the previous split - // since it produced empty space - int nextIndex = tempTree.getSize(); - // allocate child node - tempTree.add(0); - tempTree.add(0); - tempTree.add(0); - if (wasLeft) { - // create a node with a left child - // write leaf node - stats.updateInner(); - tempTree.set(nodeIndex + 0, (prevAxis << 30) | nextIndex); - tempTree.set(nodeIndex + 1, Float.floatToRawIntBits(prevClip)); - tempTree.set(nodeIndex + 2, Float.floatToRawIntBits(Float.POSITIVE_INFINITY)); - } else { - // create a node with a right child - // write leaf node - stats.updateInner(); - tempTree.set(nodeIndex + 0, (prevAxis << 30) | (nextIndex - 3)); - tempTree.set(nodeIndex + 1, Float.floatToRawIntBits(Float.NEGATIVE_INFINITY)); - tempTree.set(nodeIndex + 2, Float.floatToRawIntBits(prevClip)); - } - // count stats for the unused leaf - depth++; - stats.updateLeaf(depth, 0); - // now we keep going as we are, with a new nodeIndex: - nodeIndex = nextIndex; - } - break; - } - } - // compute index of child nodes - int nextIndex = tempTree.getSize(); - // allocate left node - int nl = right - left + 1; - int nr = rightOrig - (right + 1) + 1; - if (nl > 0) { - tempTree.add(0); - tempTree.add(0); - tempTree.add(0); - } else - nextIndex -= 3; - // allocate right node - if (nr > 0) { - tempTree.add(0); - tempTree.add(0); - tempTree.add(0); - } - // write leaf node - stats.updateInner(); - tempTree.set(nodeIndex + 0, (axis << 30) | nextIndex); - tempTree.set(nodeIndex + 1, Float.floatToRawIntBits(clipL)); - tempTree.set(nodeIndex + 2, Float.floatToRawIntBits(clipR)); - // prepare L/R child boxes - float[] gridBoxL = new float[6]; - float[] gridBoxR = new float[6]; - float[] nodeBoxL = new float[6]; - float[] nodeBoxR = new float[6]; - for (int i = 0; i < 6; i++) { - gridBoxL[i] = gridBoxR[i] = gridBox[i]; - nodeBoxL[i] = nodeBoxR[i] = nodeBox[i]; - } - gridBoxL[2 * axis + 1] = gridBoxR[2 * axis] = split; - nodeBoxL[2 * axis + 1] = clipL; - nodeBoxR[2 * axis + 0] = clipR; - // free memory - gridBox = nodeBox = null; - // recurse - if (nl > 0) - subdivide(left, right, tempTree, indices, gridBoxL, nodeBoxL, nextIndex, depth + 1, stats); - else - stats.updateLeaf(depth + 1, 0); - if (nr > 0) - subdivide(right + 1, rightOrig, tempTree, indices, gridBoxR, nodeBoxR, nextIndex + 3, depth + 1, stats); - else - stats.updateLeaf(depth + 1, 0); - } - - public void intersect(Ray r, IntersectionState state) { - float intervalMin = r.getMin(); - float intervalMax = r.getMax(); - float orgX = r.ox; - float dirX = r.dx, invDirX = 1 / dirX; - float t1, t2; - t1 = (bounds.getMinimum().x - orgX) * invDirX; - t2 = (bounds.getMaximum().x - orgX) * invDirX; - if (invDirX > 0) { - if (t1 > intervalMin) - intervalMin = t1; - if (t2 < intervalMax) - intervalMax = t2; - } else { - if (t2 > intervalMin) - intervalMin = t2; - if (t1 < intervalMax) - intervalMax = t1; - } - if (intervalMin > intervalMax) - return; - float orgY = r.oy; - float dirY = r.dy, invDirY = 1 / dirY; - t1 = (bounds.getMinimum().y - orgY) * invDirY; - t2 = (bounds.getMaximum().y - orgY) * invDirY; - if (invDirY > 0) { - if (t1 > intervalMin) - intervalMin = t1; - if (t2 < intervalMax) - intervalMax = t2; - } else { - if (t2 > intervalMin) - intervalMin = t2; - if (t1 < intervalMax) - intervalMax = t1; - } - if (intervalMin > intervalMax) - return; - float orgZ = r.oz; - float dirZ = r.dz, invDirZ = 1 / dirZ; - t1 = (bounds.getMinimum().z - orgZ) * invDirZ; - t2 = (bounds.getMaximum().z - orgZ) * invDirZ; - if (invDirZ > 0) { - if (t1 > intervalMin) - intervalMin = t1; - if (t2 < intervalMax) - intervalMax = t2; - } else { - if (t2 > intervalMin) - intervalMin = t2; - if (t1 < intervalMax) - intervalMax = t1; - } - if (intervalMin > intervalMax) - return; - - // compute custom offsets from direction sign bit - - int offsetXFront = Float.floatToRawIntBits(dirX) >>> 31; - int offsetYFront = Float.floatToRawIntBits(dirY) >>> 31; - int offsetZFront = Float.floatToRawIntBits(dirZ) >>> 31; - - int offsetXBack = offsetXFront ^ 1; - int offsetYBack = offsetYFront ^ 1; - int offsetZBack = offsetZFront ^ 1; - - int offsetXFront3 = offsetXFront * 3; - int offsetYFront3 = offsetYFront * 3; - int offsetZFront3 = offsetZFront * 3; - - int offsetXBack3 = offsetXBack * 3; - int offsetYBack3 = offsetYBack * 3; - int offsetZBack3 = offsetZBack * 3; - - // avoid always adding 1 during the inner loop - offsetXFront++; - offsetYFront++; - offsetZFront++; - offsetXBack++; - offsetYBack++; - offsetZBack++; - - IntersectionState.StackNode[] stack = state.getStack(); - int stackPos = 0; - int node = 0; - - while (true) { - pushloop: while (true) { - int tn = tree[node]; - int axis = tn & (7 << 29); - int offset = tn & ~(7 << 29); - switch (axis) { - case 0: { - // x axis - float tf = (Float.intBitsToFloat(tree[node + offsetXFront]) - orgX) * invDirX; - float tb = (Float.intBitsToFloat(tree[node + offsetXBack]) - orgX) * invDirX; - // ray passes between clip zones - if (tf < intervalMin && tb > intervalMax) - break pushloop; - int back = offset + offsetXBack3; - node = back; - // ray passes through far node only - if (tf < intervalMin) { - intervalMin = (tb >= intervalMin) ? tb : intervalMin; - continue; - } - node = offset + offsetXFront3; // front - // ray passes through near node only - if (tb > intervalMax) { - intervalMax = (tf <= intervalMax) ? tf : intervalMax; - continue; - } - // ray passes through both nodes - // push back node - stack[stackPos].node = back; - stack[stackPos].near = (tb >= intervalMin) ? tb : intervalMin; - stack[stackPos].far = intervalMax; - stackPos++; - // update ray interval for front node - intervalMax = (tf <= intervalMax) ? tf : intervalMax; - continue; - } - case 1 << 30: { - float tf = (Float.intBitsToFloat(tree[node + offsetYFront]) - orgY) * invDirY; - float tb = (Float.intBitsToFloat(tree[node + offsetYBack]) - orgY) * invDirY; - // ray passes between clip zones - if (tf < intervalMin && tb > intervalMax) - break pushloop; - int back = offset + offsetYBack3; - node = back; - // ray passes through far node only - if (tf < intervalMin) { - intervalMin = (tb >= intervalMin) ? tb : intervalMin; - continue; - } - node = offset + offsetYFront3; // front - // ray passes through near node only - if (tb > intervalMax) { - intervalMax = (tf <= intervalMax) ? tf : intervalMax; - continue; - } - // ray passes through both nodes - // push back node - stack[stackPos].node = back; - stack[stackPos].near = (tb >= intervalMin) ? tb : intervalMin; - stack[stackPos].far = intervalMax; - stackPos++; - // update ray interval for front node - intervalMax = (tf <= intervalMax) ? tf : intervalMax; - continue; - } - case 2 << 30: { - // z axis - float tf = (Float.intBitsToFloat(tree[node + offsetZFront]) - orgZ) * invDirZ; - float tb = (Float.intBitsToFloat(tree[node + offsetZBack]) - orgZ) * invDirZ; - // ray passes between clip zones - if (tf < intervalMin && tb > intervalMax) - break pushloop; - int back = offset + offsetZBack3; - node = back; - // ray passes through far node only - if (tf < intervalMin) { - intervalMin = (tb >= intervalMin) ? tb : intervalMin; - continue; - } - node = offset + offsetZFront3; // front - // ray passes through near node only - if (tb > intervalMax) { - intervalMax = (tf <= intervalMax) ? tf : intervalMax; - continue; - } - // ray passes through both nodes - // push back node - stack[stackPos].node = back; - stack[stackPos].near = (tb >= intervalMin) ? tb : intervalMin; - stack[stackPos].far = intervalMax; - stackPos++; - // update ray interval for front node - intervalMax = (tf <= intervalMax) ? tf : intervalMax; - continue; - } - case 3 << 30: { - // leaf - test some objects - int n = tree[node + 1]; - while (n > 0) { - primitives.intersectPrimitive(r, objects[offset], state); - n--; - offset++; - } - break pushloop; - } - case 1 << 29: { - float tf = (Float.intBitsToFloat(tree[node + offsetXFront]) - orgX) * invDirX; - float tb = (Float.intBitsToFloat(tree[node + offsetXBack]) - orgX) * invDirX; - node = offset; - intervalMin = (tf >= intervalMin) ? tf : intervalMin; - intervalMax = (tb <= intervalMax) ? tb : intervalMax; - if (intervalMin > intervalMax) - break pushloop; - continue; - } - case 3 << 29: { - float tf = (Float.intBitsToFloat(tree[node + offsetYFront]) - orgY) * invDirY; - float tb = (Float.intBitsToFloat(tree[node + offsetYBack]) - orgY) * invDirY; - node = offset; - intervalMin = (tf >= intervalMin) ? tf : intervalMin; - intervalMax = (tb <= intervalMax) ? tb : intervalMax; - if (intervalMin > intervalMax) - break pushloop; - continue; - } - case 5 << 29: { - float tf = (Float.intBitsToFloat(tree[node + offsetZFront]) - orgZ) * invDirZ; - float tb = (Float.intBitsToFloat(tree[node + offsetZBack]) - orgZ) * invDirZ; - node = offset; - intervalMin = (tf >= intervalMin) ? tf : intervalMin; - intervalMax = (tb <= intervalMax) ? tb : intervalMax; - if (intervalMin > intervalMax) - break pushloop; - continue; - } - default: - return; // should not happen - } // switch - } // traversal loop - do { - // stack is empty? - if (stackPos == 0) - return; - // move back up the stack - stackPos--; - intervalMin = stack[stackPos].near; - if (r.getMax() < intervalMin) - continue; - node = stack[stackPos].node; - intervalMax = stack[stackPos].far; - break; - } while (true); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/accel/KDTree.java b/src/org/sunflow/core/accel/KDTree.java deleted file mode 100644 index 9ba7be4..0000000 --- a/src/org/sunflow/core/accel/KDTree.java +++ /dev/null @@ -1,800 +0,0 @@ -package org.sunflow.core.accel; - -import java.io.FileWriter; -import java.io.IOException; - -import org.sunflow.core.AccelerationStructure; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.image.Color; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Point3; -import org.sunflow.system.Memory; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; -import org.sunflow.util.IntArray; - -public class KDTree implements AccelerationStructure { - private int[] tree; - private int[] primitives; - private PrimitiveList primitiveList; - private BoundingBox bounds; - - private int maxPrims; - - private static final float INTERSECT_COST = 0.5f; - private static final float TRAVERSAL_COST = 1; - private static final float EMPTY_BONUS = 0.2f; - private static final int MAX_DEPTH = 64; - - private static boolean dump = false; - private static String dumpPrefix = "kdtree"; - - public KDTree() { - this(0); - } - - public KDTree(int maxPrims) { - this.maxPrims = maxPrims; - } - - private static class BuildStats { - private int numNodes; - private int numLeaves; - private int sumObjects; - private int minObjects; - private int maxObjects; - private int sumDepth; - private int minDepth; - private int maxDepth; - private int numLeaves0; - private int numLeaves1; - private int numLeaves2; - private int numLeaves3; - private int numLeaves4; - private int numLeaves4p; - - BuildStats() { - numNodes = numLeaves = 0; - sumObjects = 0; - minObjects = Integer.MAX_VALUE; - maxObjects = Integer.MIN_VALUE; - sumDepth = 0; - minDepth = Integer.MAX_VALUE; - maxDepth = Integer.MIN_VALUE; - numLeaves0 = 0; - numLeaves1 = 0; - numLeaves2 = 0; - numLeaves3 = 0; - numLeaves4 = 0; - numLeaves4p = 0; - } - - void updateInner() { - numNodes++; - } - - void updateLeaf(int depth, int n) { - numLeaves++; - minDepth = Math.min(depth, minDepth); - maxDepth = Math.max(depth, maxDepth); - sumDepth += depth; - minObjects = Math.min(n, minObjects); - maxObjects = Math.max(n, maxObjects); - sumObjects += n; - switch (n) { - case 0: - numLeaves0++; - break; - case 1: - numLeaves1++; - break; - case 2: - numLeaves2++; - break; - case 3: - numLeaves3++; - break; - case 4: - numLeaves4++; - break; - default: - numLeaves4p++; - break; - } - } - - void printStats() { - UI.printDetailed(Module.ACCEL, "KDTree stats:"); - UI.printDetailed(Module.ACCEL, " * Nodes: %d", numNodes); - UI.printDetailed(Module.ACCEL, " * Leaves: %d", numLeaves); - UI.printDetailed(Module.ACCEL, " * Objects: min %d", minObjects); - UI.printDetailed(Module.ACCEL, " avg %.2f", (float) sumObjects / numLeaves); - UI.printDetailed(Module.ACCEL, " avg(n>0) %.2f", (float) sumObjects / (numLeaves - numLeaves0)); - UI.printDetailed(Module.ACCEL, " max %d", maxObjects); - UI.printDetailed(Module.ACCEL, " * Depth: min %d", minDepth); - UI.printDetailed(Module.ACCEL, " avg %.2f", (float) sumDepth / numLeaves); - UI.printDetailed(Module.ACCEL, " max %d", maxDepth); - UI.printDetailed(Module.ACCEL, " * Leaves w/: N=0 %3d%%", 100 * numLeaves0 / numLeaves); - UI.printDetailed(Module.ACCEL, " N=1 %3d%%", 100 * numLeaves1 / numLeaves); - UI.printDetailed(Module.ACCEL, " N=2 %3d%%", 100 * numLeaves2 / numLeaves); - UI.printDetailed(Module.ACCEL, " N=3 %3d%%", 100 * numLeaves3 / numLeaves); - UI.printDetailed(Module.ACCEL, " N=4 %3d%%", 100 * numLeaves4 / numLeaves); - UI.printDetailed(Module.ACCEL, " N>4 %3d%%", 100 * numLeaves4p / numLeaves); - } - } - - public static void setDumpMode(boolean dump, String prefix) { - KDTree.dump = dump; - KDTree.dumpPrefix = prefix; - } - - public void build(PrimitiveList primitives) { - UI.printDetailed(Module.ACCEL, "KDTree settings"); - UI.printDetailed(Module.ACCEL, " * Max Leaf Size: %d", maxPrims); - UI.printDetailed(Module.ACCEL, " * Max Depth: %d", MAX_DEPTH); - UI.printDetailed(Module.ACCEL, " * Traversal cost: %.2f", TRAVERSAL_COST); - UI.printDetailed(Module.ACCEL, " * Intersect cost: %.2f", INTERSECT_COST); - UI.printDetailed(Module.ACCEL, " * Empty bonus: %.2f", EMPTY_BONUS); - UI.printDetailed(Module.ACCEL, " * Dump leaves: %s", dump ? "enabled" : "disabled"); - Timer total = new Timer(); - total.start(); - primitiveList = primitives; - // get the object space bounds - bounds = primitives.getWorldBounds(null); - int nPrim = primitiveList.getNumPrimitives(), nSplits = 0; - BuildTask task = new BuildTask(nPrim); - Timer prepare = new Timer(); - prepare.start(); - for (int i = 0; i < nPrim; i++) { - for (int axis = 0; axis < 3; axis++) { - float ls = primitiveList.getPrimitiveBound(i, 2 * axis + 0); - float rs = primitiveList.getPrimitiveBound(i, 2 * axis + 1); - if (ls == rs) { - // flat in this dimension - task.splits[nSplits] = pack(ls, PLANAR, axis, i); - nSplits++; - } else { - task.splits[nSplits + 0] = pack(ls, OPENED, axis, i); - task.splits[nSplits + 1] = pack(rs, CLOSED, axis, i); - nSplits += 2; - } - } - } - task.n = nSplits; - prepare.end(); - Timer t = new Timer(); - IntArray tempTree = new IntArray(); - IntArray tempList = new IntArray(); - tempTree.add(0); - tempTree.add(1); - t.start(); - // sort it - Timer sorting = new Timer(); - sorting.start(); - radix12(task.splits, task.n); - sorting.end(); - // build the actual tree - BuildStats stats = new BuildStats(); - buildTree(bounds.getMinimum().x, bounds.getMaximum().x, bounds.getMinimum().y, bounds.getMaximum().y, bounds.getMinimum().z, bounds.getMaximum().z, task, 1, tempTree, 0, tempList, stats); - t.end(); - // write out final arrays - // free some memory - task = null; - tree = tempTree.trim(); - tempTree = null; - this.primitives = tempList.trim(); - tempList = null; - total.end(); - // display some extra info - stats.printStats(); - UI.printDetailed(Module.ACCEL, " * Node memory: %s", Memory.sizeof(tree)); - UI.printDetailed(Module.ACCEL, " * Object memory: %s", Memory.sizeof(this.primitives)); - UI.printDetailed(Module.ACCEL, " * Prepare time: %s", prepare); - UI.printDetailed(Module.ACCEL, " * Sorting time: %s", sorting); - UI.printDetailed(Module.ACCEL, " * Tree creation: %s", t); - UI.printDetailed(Module.ACCEL, " * Build time: %s", total); - if (dump) { - try { - UI.printInfo(Module.ACCEL, "Dumping mtls to %s.mtl ...", dumpPrefix); - FileWriter mtlFile = new FileWriter(dumpPrefix + ".mtl"); - int maxN = stats.maxObjects; - for (int n = 0; n <= maxN; n++) { - float blend = (float) n / (float) maxN; - Color nc; - if (blend < 0.25) - nc = Color.blend(Color.BLUE, Color.GREEN, blend / 0.25f); - else if (blend < 0.5) - nc = Color.blend(Color.GREEN, Color.YELLOW, (blend - 0.25f) / 0.25f); - else if (blend < 0.75) - nc = Color.blend(Color.YELLOW, Color.RED, (blend - 0.50f) / 0.25f); - else - nc = Color.MAGENTA; - mtlFile.write(String.format("newmtl mtl%d\n", n)); - float[] rgb = nc.getRGB(); - mtlFile.write("Ka 0.1 0.1 0.1\n"); - mtlFile.write(String.format("Kd %.12g %.12g %.12g\n", rgb[0], rgb[1], rgb[2])); - mtlFile.write("illum 1\n\n"); - } - FileWriter objFile = new FileWriter(dumpPrefix + ".obj"); - UI.printInfo(Module.ACCEL, "Dumping tree to %s.obj ...", dumpPrefix); - dumpObj(0, 0, maxN, new BoundingBox(bounds), objFile, mtlFile); - objFile.close(); - mtlFile.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - private int dumpObj(int offset, int vertOffset, int maxN, BoundingBox bounds, FileWriter file, FileWriter mtlFile) throws IOException { - if (offset == 0) - file.write(String.format("mtllib %s.mtl\n", dumpPrefix)); - int nextOffset = tree[offset]; - if ((nextOffset & (3 << 30)) == (3 << 30)) { - // leaf - int n = tree[offset + 1]; - if (n > 0) { - // output the current voxel to the file - Point3 min = bounds.getMinimum(); - Point3 max = bounds.getMaximum(); - file.write(String.format("o node%d\n", offset)); - file.write(String.format("v %g %g %g\n", max.x, max.y, min.z)); - file.write(String.format("v %g %g %g\n", max.x, min.y, min.z)); - file.write(String.format("v %g %g %g\n", min.x, min.y, min.z)); - file.write(String.format("v %g %g %g\n", min.x, max.y, min.z)); - file.write(String.format("v %g %g %g\n", max.x, max.y, max.z)); - file.write(String.format("v %g %g %g\n", max.x, min.y, max.z)); - file.write(String.format("v %g %g %g\n", min.x, min.y, max.z)); - file.write(String.format("v %g %g %g\n", min.x, max.y, max.z)); - int v0 = vertOffset; - file.write(String.format("usemtl mtl%d\n", n)); - file.write("s off\n"); - file.write(String.format("f %d %d %d %d\n", v0 + 1, v0 + 2, v0 + 3, v0 + 4)); - file.write(String.format("f %d %d %d %d\n", v0 + 5, v0 + 8, v0 + 7, v0 + 6)); - file.write(String.format("f %d %d %d %d\n", v0 + 1, v0 + 5, v0 + 6, v0 + 2)); - file.write(String.format("f %d %d %d %d\n", v0 + 2, v0 + 6, v0 + 7, v0 + 3)); - file.write(String.format("f %d %d %d %d\n", v0 + 3, v0 + 7, v0 + 8, v0 + 4)); - file.write(String.format("f %d %d %d %d\n", v0 + 5, v0 + 1, v0 + 4, v0 + 8)); - vertOffset += 8; - } - return vertOffset; - } else { - // node, recurse - int axis = nextOffset & (3 << 30), v0; - float split = Float.intBitsToFloat(tree[offset + 1]), min, max; - nextOffset &= ~(3 << 30); - switch (axis) { - case 0: - max = bounds.getMaximum().x; - bounds.getMaximum().x = split; - v0 = dumpObj(nextOffset, vertOffset, maxN, bounds, file, mtlFile); - // restore and go to other side - bounds.getMaximum().x = max; - min = bounds.getMinimum().x; - bounds.getMinimum().x = split; - v0 = dumpObj(nextOffset + 2, v0, maxN, bounds, file, mtlFile); - bounds.getMinimum().x = min; - break; - case 1 << 30: - max = bounds.getMaximum().y; - bounds.getMaximum().y = split; - v0 = dumpObj(nextOffset, vertOffset, maxN, bounds, file, mtlFile); - // restore and go to other side - bounds.getMaximum().y = max; - min = bounds.getMinimum().y; - bounds.getMinimum().y = split; - v0 = dumpObj(nextOffset + 2, v0, maxN, bounds, file, mtlFile); - bounds.getMinimum().y = min; - break; - case 2 << 30: - max = bounds.getMaximum().z; - bounds.getMaximum().z = split; - v0 = dumpObj(nextOffset, vertOffset, maxN, bounds, file, mtlFile); - // restore and go to other side - bounds.getMaximum().z = max; - min = bounds.getMinimum().z; - bounds.getMinimum().z = split; - v0 = dumpObj(nextOffset + 2, v0, maxN, bounds, file, mtlFile); - // restore and go to other side - bounds.getMinimum().z = min; - break; - default: - v0 = vertOffset; - break; - } - return v0; - } - } - - // type is encoded as 2 shifted bits - private static final long CLOSED = 0L << 30; - private static final long PLANAR = 1L << 30; - private static final long OPENED = 2L << 30; - private static final long TYPE_MASK = 3L << 30; - - // pack split values into a 64bit integer - private static long pack(float split, long type, int axis, int object) { - // pack float in sortable form - int f = Float.floatToRawIntBits(split); - int top = f ^ ((f >> 31) | 0x80000000); - long p = (top & 0xFFFFFFFFL) << 32; - p |= type; // encode type as 2 bits - p |= ((long) axis) << 28; // encode axis as 2 bits - p |= (object & 0xFFFFFFFL); // pack object number - return p; - } - - private static int unpackObject(long p) { - return (int) (p & 0xFFFFFFFL); - } - - private static int unpackAxis(long p) { - return (int) (p >>> 28) & 3; - } - - private static long unpackSplitType(long p) { - return p & TYPE_MASK; - } - - private static float unpackSplit(long p) { - int f = (int) ((p >>> 32) & 0xFFFFFFFFL); - int m = ((f >>> 31) - 1) | 0x80000000; - return Float.intBitsToFloat(f ^ m); - } - - // radix sort on top 36 bits - returns sorted result - private static void radix12(long[] splits, int n) { - // allocate working memory - final int[] hist = new int[2048]; - final long[] sorted = new long[n]; - // parallel histogramming pass - for (int i = 0; i < n; i++) { - long pi = splits[i]; - hist[0x000 + ((int) (pi >>> 28) & 0x1FF)]++; - hist[0x200 + ((int) (pi >>> 37) & 0x1FF)]++; - hist[0x400 + ((int) (pi >>> 46) & 0x1FF)]++; - hist[0x600 + ((int) (pi >>> 55))]++; - } - - // sum the histograms - each histogram entry records the number of - // values preceding itself. - { - int sum0 = 0, sum1 = 0, sum2 = 0, sum3 = 0; - int tsum; - for (int i = 0; i < 512; i++) { - tsum = hist[0x000 + i] + sum0; - hist[0x000 + i] = sum0 - 1; - sum0 = tsum; - tsum = hist[0x200 + i] + sum1; - hist[0x200 + i] = sum1 - 1; - sum1 = tsum; - tsum = hist[0x400 + i] + sum2; - hist[0x400 + i] = sum2 - 1; - sum2 = tsum; - tsum = hist[0x600 + i] + sum3; - hist[0x600 + i] = sum3 - 1; - sum3 = tsum; - } - } - - // read/write histogram passes - for (int i = 0; i < n; i++) { - long pi = splits[i]; - int pos = (int) (pi >>> 28) & 0x1FF; - sorted[++hist[0x000 + pos]] = pi; - } - for (int i = 0; i < n; i++) { - long pi = sorted[i]; - int pos = (int) (pi >>> 37) & 0x1FF; - splits[++hist[0x200 + pos]] = pi; - } - for (int i = 0; i < n; i++) { - long pi = splits[i]; - int pos = (int) (pi >>> 46) & 0x1FF; - sorted[++hist[0x400 + pos]] = pi; - } - for (int i = 0; i < n; i++) { - long pi = sorted[i]; - int pos = (int) (pi >>> 55); - splits[++hist[0x600 + pos]] = pi; - } - } - - private static class BuildTask { - long[] splits; - int numObjects; - int n; - byte[] leftRightTable; - - BuildTask(int numObjects) { - splits = new long[6 * numObjects]; - this.numObjects = numObjects; - n = 0; - // 2 bits per object - leftRightTable = new byte[(numObjects + 3) / 4]; - } - - BuildTask(int numObjects, BuildTask parent) { - splits = new long[6 * numObjects]; - this.numObjects = numObjects; - n = 0; - leftRightTable = parent.leftRightTable; - } - } - - private void buildTree(float minx, float maxx, float miny, float maxy, float minz, float maxz, BuildTask task, int depth, IntArray tempTree, int offset, IntArray tempList, BuildStats stats) { - // get node bounding box extents - if (task.numObjects > maxPrims && depth < MAX_DEPTH) { - float dx = maxx - minx; - float dy = maxy - miny; - float dz = maxz - minz; - // search for best possible split - float bestCost = INTERSECT_COST * task.numObjects; - int bestAxis = -1; - int bestOffsetStart = -1; - int bestOffsetEnd = -1; - float bestSplit = 0; - boolean bestPlanarLeft = false; - int bnl = 0, bnr = 0; - // inverse area of the bounding box (factor of 2 ommitted) - float area = (dx * dy + dy * dz + dz * dx); - float ISECT_COST = INTERSECT_COST / area; - // setup counts for each axis - int[] nl = { 0, 0, 0 }; - int[] nr = { task.numObjects, task.numObjects, task.numObjects }; - // setup bounds for each axis - float[] dp = { dy * dz, dz * dx, dx * dy }; - float[] ds = { dy + dz, dz + dx, dx + dy }; - float[] nodeMin = { minx, miny, minz }; - float[] nodeMax = { maxx, maxy, maxz }; - // search for best cost - int nSplits = task.n; - long[] splits = task.splits; - byte[] lrtable = task.leftRightTable; - for (int i = 0; i < nSplits;) { - // extract current split - long ptr = splits[i]; - float split = unpackSplit(ptr); - int axis = unpackAxis(ptr); - // mark current position - int currentOffset = i; - // count number of primitives start/stopping/lying on the - // current plane - int pClosed = 0, pPlanar = 0, pOpened = 0; - long ptrMasked = ptr & (~TYPE_MASK & 0xFFFFFFFFF0000000L); - long ptrClosed = ptrMasked | CLOSED; - long ptrPlanar = ptrMasked | PLANAR; - long ptrOpened = ptrMasked | OPENED; - while (i < nSplits && (splits[i] & 0xFFFFFFFFF0000000L) == ptrClosed) { - int obj = unpackObject(splits[i]); - lrtable[obj >>> 2] = 0; - pClosed++; - i++; - } - while (i < nSplits && (splits[i] & 0xFFFFFFFFF0000000L) == ptrPlanar) { - int obj = unpackObject(splits[i]); - lrtable[obj >>> 2] = 0; - pPlanar++; - i++; - } - while (i < nSplits && (splits[i] & 0xFFFFFFFFF0000000L) == ptrOpened) { - int obj = unpackObject(splits[i]); - lrtable[obj >>> 2] = 0; - pOpened++; - i++; - } - // now we have summed all contributions from this plane - nr[axis] -= pPlanar + pClosed; - // compute cost - if (split >= nodeMin[axis] && split <= nodeMax[axis]) { - // left and right surface area (factor of 2 ommitted) - float dl = split - nodeMin[axis]; - float dr = nodeMax[axis] - split; - float lp = dp[axis] + dl * ds[axis]; - float rp = dp[axis] + dr * ds[axis]; - // planar prims go to smallest cell always - boolean planarLeft = dl < dr; - int numLeft = nl[axis] + (planarLeft ? pPlanar : 0); - int numRight = nr[axis] + (planarLeft ? 0 : pPlanar); - float eb = ((numLeft == 0 && dl > 0) || (numRight == 0 && dr > 0)) ? EMPTY_BONUS : 0; - float cost = TRAVERSAL_COST + ISECT_COST * (1 - eb) * (lp * numLeft + rp * numRight); - if (cost < bestCost) { - bestCost = cost; - bestAxis = axis; - bestSplit = split; - bestOffsetStart = currentOffset; - bestOffsetEnd = i; - bnl = numLeft; - bnr = numRight; - bestPlanarLeft = planarLeft; - } - } - // move objects left - nl[axis] += pOpened + pPlanar; - } - // debug check for correctness of the scan - for (int axis = 0; axis < 3; axis++) { - int numLeft = nl[axis]; - int numRight = nr[axis]; - if (numLeft != task.numObjects || numRight != 0) - UI.printError(Module.ACCEL, "Didn't scan full range of objects @depth=%d. Left overs for axis %d: [L: %d] [R: %d]", depth, axis, numLeft, numRight); - } - // found best split? - if (bestAxis != -1) { - // allocate space for child nodes - BuildTask taskL = new BuildTask(bnl, task); - BuildTask taskR = new BuildTask(bnr, task); - int lk = 0, rk = 0; - for (int i = 0; i < bestOffsetStart; i++) { - long ptr = splits[i]; - if (unpackAxis(ptr) == bestAxis) { - if (unpackSplitType(ptr) != CLOSED) { - int obj = unpackObject(ptr); - lrtable[obj >>> 2] |= 1 << ((obj & 3) << 1); - lk++; - } - } - } - for (int i = bestOffsetStart; i < bestOffsetEnd; i++) { - long ptr = splits[i]; - assert unpackAxis(ptr) == bestAxis; - if (unpackSplitType(ptr) == PLANAR) { - if (bestPlanarLeft) { - int obj = unpackObject(ptr); - lrtable[obj >>> 2] |= 1 << ((obj & 3) << 1); - lk++; - } else { - int obj = unpackObject(ptr); - lrtable[obj >>> 2] |= 2 << ((obj & 3) << 1); - rk++; - } - } - } - for (int i = bestOffsetEnd; i < nSplits; i++) { - long ptr = splits[i]; - if (unpackAxis(ptr) == bestAxis) { - if (unpackSplitType(ptr) != OPENED) { - int obj = unpackObject(ptr); - lrtable[obj >>> 2] |= 2 << ((obj & 3) << 1); - rk++; - } - } - } - // output new splits while maintaining order - long[] splitsL = taskL.splits; - long[] splitsR = taskR.splits; - int nsl = 0, nsr = 0; - for (int i = 0; i < nSplits; i++) { - long ptr = splits[i]; - int obj = unpackObject(ptr); - int idx = obj >>> 2; - int mask = 1 << ((obj & 3) << 1); - if ((lrtable[idx] & mask) != 0) { - splitsL[nsl] = ptr; - nsl++; - } - if ((lrtable[idx] & (mask << 1)) != 0) { - splitsR[nsr] = ptr; - nsr++; - } - } - taskL.n = nsl; - taskR.n = nsr; - // free more memory - task.splits = splits = splitsL = splitsR = null; - task = null; - // allocate child nodes - int nextOffset = tempTree.getSize(); - tempTree.add(0); - tempTree.add(0); - tempTree.add(0); - tempTree.add(0); - // create current node - tempTree.set(offset + 0, (bestAxis << 30) | nextOffset); - tempTree.set(offset + 1, Float.floatToRawIntBits(bestSplit)); - // recurse for child nodes - free object arrays after each step - stats.updateInner(); - switch (bestAxis) { - case 0: - buildTree(minx, bestSplit, miny, maxy, minz, maxz, taskL, depth + 1, tempTree, nextOffset, tempList, stats); - taskL = null; - buildTree(bestSplit, maxx, miny, maxy, minz, maxz, taskR, depth + 1, tempTree, nextOffset + 2, tempList, stats); - taskR = null; - return; - case 1: - buildTree(minx, maxx, miny, bestSplit, minz, maxz, taskL, depth + 1, tempTree, nextOffset, tempList, stats); - taskL = null; - buildTree(minx, maxx, bestSplit, maxy, minz, maxz, taskR, depth + 1, tempTree, nextOffset + 2, tempList, stats); - taskR = null; - return; - case 2: - buildTree(minx, maxx, miny, maxy, minz, bestSplit, taskL, depth + 1, tempTree, nextOffset, tempList, stats); - taskL = null; - buildTree(minx, maxx, miny, maxy, bestSplit, maxz, taskR, depth + 1, tempTree, nextOffset + 2, tempList, stats); - taskR = null; - return; - default: - assert false; - } - } - } - // create leaf node - int listOffset = tempList.getSize(); - int n = 0; - for (int i = 0; i < task.n; i++) { - long ptr = task.splits[i]; - if (unpackAxis(ptr) == 0 && unpackSplitType(ptr) != CLOSED) { - tempList.add(unpackObject(ptr)); - n++; - } - } - stats.updateLeaf(depth, n); - if (n != task.numObjects) - UI.printError(Module.ACCEL, "Error creating leaf node - expecting %d found %d", task.numObjects, n); - tempTree.set(offset + 0, (3 << 30) | listOffset); - tempTree.set(offset + 1, task.numObjects); - // free some memory - task.splits = null; - } - - public void intersect(Ray r, IntersectionState state) { - float intervalMin = r.getMin(); - float intervalMax = r.getMax(); - float orgX = r.ox; - float dirX = r.dx, invDirX = 1 / dirX; - float t1, t2; - t1 = (bounds.getMinimum().x - orgX) * invDirX; - t2 = (bounds.getMaximum().x - orgX) * invDirX; - if (invDirX > 0) { - if (t1 > intervalMin) - intervalMin = t1; - if (t2 < intervalMax) - intervalMax = t2; - } else { - if (t2 > intervalMin) - intervalMin = t2; - if (t1 < intervalMax) - intervalMax = t1; - } - if (intervalMin > intervalMax) - return; - float orgY = r.oy; - float dirY = r.dy, invDirY = 1 / dirY; - t1 = (bounds.getMinimum().y - orgY) * invDirY; - t2 = (bounds.getMaximum().y - orgY) * invDirY; - if (invDirY > 0) { - if (t1 > intervalMin) - intervalMin = t1; - if (t2 < intervalMax) - intervalMax = t2; - } else { - if (t2 > intervalMin) - intervalMin = t2; - if (t1 < intervalMax) - intervalMax = t1; - } - if (intervalMin > intervalMax) - return; - float orgZ = r.oz; - float dirZ = r.dz, invDirZ = 1 / dirZ; - t1 = (bounds.getMinimum().z - orgZ) * invDirZ; - t2 = (bounds.getMaximum().z - orgZ) * invDirZ; - if (invDirZ > 0) { - if (t1 > intervalMin) - intervalMin = t1; - if (t2 < intervalMax) - intervalMax = t2; - } else { - if (t2 > intervalMin) - intervalMin = t2; - if (t1 < intervalMax) - intervalMax = t1; - } - if (intervalMin > intervalMax) - return; - - // compute custom offsets from direction sign bit - int offsetXFront = (Float.floatToRawIntBits(dirX) & (1 << 31)) >>> 30; - int offsetYFront = (Float.floatToRawIntBits(dirY) & (1 << 31)) >>> 30; - int offsetZFront = (Float.floatToRawIntBits(dirZ) & (1 << 31)) >>> 30; - - int offsetXBack = offsetXFront ^ 2; - int offsetYBack = offsetYFront ^ 2; - int offsetZBack = offsetZFront ^ 2; - - IntersectionState.StackNode[] stack = state.getStack(); - int stackPos = 0; - int node = 0; - - while (true) { - int tn = tree[node]; - int axis = tn & (3 << 30); - int offset = tn & ~(3 << 30); - switch (axis) { - case 0: { - float d = (Float.intBitsToFloat(tree[node + 1]) - orgX) * invDirX; - int back = offset + offsetXBack; - node = back; - if (d < intervalMin) - continue; - node = offset + offsetXFront; // front - if (d > intervalMax) - continue; - // push back node - stack[stackPos].node = back; - stack[stackPos].near = (d >= intervalMin) ? d : intervalMin; - stack[stackPos].far = intervalMax; - stackPos++; - // update ray interval for front node - intervalMax = (d <= intervalMax) ? d : intervalMax; - continue; - } - case 1 << 30: { - // y axis - float d = (Float.intBitsToFloat(tree[node + 1]) - orgY) * invDirY; - int back = offset + offsetYBack; - node = back; - if (d < intervalMin) - continue; - node = offset + offsetYFront; // front - if (d > intervalMax) - continue; - // push back node - stack[stackPos].node = back; - stack[stackPos].near = (d >= intervalMin) ? d : intervalMin; - stack[stackPos].far = intervalMax; - stackPos++; - // update ray interval for front node - intervalMax = (d <= intervalMax) ? d : intervalMax; - continue; - } - case 2 << 30: { - // z axis - float d = (Float.intBitsToFloat(tree[node + 1]) - orgZ) * invDirZ; - int back = offset + offsetZBack; - node = back; - if (d < intervalMin) - continue; - node = offset + offsetZFront; // front - if (d > intervalMax) - continue; - // push back node - stack[stackPos].node = back; - stack[stackPos].near = (d >= intervalMin) ? d : intervalMin; - stack[stackPos].far = intervalMax; - stackPos++; - // update ray interval for front node - intervalMax = (d <= intervalMax) ? d : intervalMax; - continue; - } - default: { - // leaf - test some objects - int n = tree[node + 1]; - while (n > 0) { - primitiveList.intersectPrimitive(r, primitives[offset], state); - n--; - offset++; - } - if (r.getMax() < intervalMax) - return; - do { - // stack is empty? - if (stackPos == 0) - return; - // move back up the stack - stackPos--; - intervalMin = stack[stackPos].near; - if (r.getMax() < intervalMin) - continue; - node = stack[stackPos].node; - intervalMax = stack[stackPos].far; - break; - } while (true); - } - } // switch - } // traversal loop - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/accel/NullAccelerator.java b/src/org/sunflow/core/accel/NullAccelerator.java deleted file mode 100644 index 2bb37ba..0000000 --- a/src/org/sunflow/core/accel/NullAccelerator.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.sunflow.core.accel; - -import org.sunflow.core.AccelerationStructure; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; - -public class NullAccelerator implements AccelerationStructure { - private PrimitiveList primitives; - private int n; - - public NullAccelerator() { - primitives = null; - n = 0; - } - - public void build(PrimitiveList primitives) { - this.primitives = primitives; - n = primitives.getNumPrimitives(); - } - - public void intersect(Ray r, IntersectionState state) { - for (int i = 0; i < n; i++) - primitives.intersectPrimitive(r, i, state); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/accel/UniformGrid.java b/src/org/sunflow/core/accel/UniformGrid.java deleted file mode 100644 index 65eae26..0000000 --- a/src/org/sunflow/core/accel/UniformGrid.java +++ /dev/null @@ -1,294 +0,0 @@ -package org.sunflow.core.accel; - -import org.sunflow.core.AccelerationStructure; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.MathUtils; -import org.sunflow.math.Vector3; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; -import org.sunflow.util.IntArray; - -public final class UniformGrid implements AccelerationStructure { - private int nx, ny, nz; - private PrimitiveList primitives; - private BoundingBox bounds; - private int[][] cells; - private float voxelwx, voxelwy, voxelwz; - private float invVoxelwx, invVoxelwy, invVoxelwz; - - public UniformGrid() { - nx = ny = nz = 0; - bounds = null; - cells = null; - voxelwx = voxelwy = voxelwz = 0; - invVoxelwx = invVoxelwy = invVoxelwz = 0; - } - - public void build(PrimitiveList primitives) { - Timer t = new Timer(); - t.start(); - this.primitives = primitives; - int n = primitives.getNumPrimitives(); - // compute bounds - bounds = primitives.getWorldBounds(null); - // create grid from number of objects - bounds.enlargeUlps(); - Vector3 w = bounds.getExtents(); - double s = Math.pow((w.x * w.y * w.z) / n, 1 / 3.0); - nx = MathUtils.clamp((int) ((w.x / s) + 0.5), 1, 128); - ny = MathUtils.clamp((int) ((w.y / s) + 0.5), 1, 128); - nz = MathUtils.clamp((int) ((w.z / s) + 0.5), 1, 128); - voxelwx = w.x / nx; - voxelwy = w.y / ny; - voxelwz = w.z / nz; - invVoxelwx = 1 / voxelwx; - invVoxelwy = 1 / voxelwy; - invVoxelwz = 1 / voxelwz; - UI.printDetailed(Module.ACCEL, "Creating grid: %dx%dx%d ...", nx, ny, nz); - IntArray[] buildCells = new IntArray[nx * ny * nz]; - // add all objects into the grid cells they overlap - int[] imin = new int[3]; - int[] imax = new int[3]; - int numCellsPerObject = 0; - for (int i = 0; i < n; i++) { - getGridIndex(primitives.getPrimitiveBound(i, 0), primitives.getPrimitiveBound(i, 2), primitives.getPrimitiveBound(i, 4), imin); - getGridIndex(primitives.getPrimitiveBound(i, 1), primitives.getPrimitiveBound(i, 3), primitives.getPrimitiveBound(i, 5), imax); - for (int ix = imin[0]; ix <= imax[0]; ix++) { - for (int iy = imin[1]; iy <= imax[1]; iy++) { - for (int iz = imin[2]; iz <= imax[2]; iz++) { - int idx = ix + (nx * iy) + (nx * ny * iz); - if (buildCells[idx] == null) - buildCells[idx] = new IntArray(); - buildCells[idx].add(i); - numCellsPerObject++; - } - } - } - } - UI.printDetailed(Module.ACCEL, "Building cells ..."); - int numEmpty = 0; - int numInFull = 0; - cells = new int[nx * ny * nz][]; - int i = 0; - for (IntArray cell : buildCells) { - if (cell != null) { - if (cell.getSize() == 0) { - numEmpty++; - cell = null; - } else { - cells[i] = cell.trim(); - numInFull += cell.getSize(); - } - } else - numEmpty++; - i++; - } - t.end(); - UI.printDetailed(Module.ACCEL, "Uniform grid statistics:"); - UI.printDetailed(Module.ACCEL, " * Grid cells: %d", cells.length); - UI.printDetailed(Module.ACCEL, " * Used cells: %d", cells.length - numEmpty); - UI.printDetailed(Module.ACCEL, " * Empty cells: %d", numEmpty); - UI.printDetailed(Module.ACCEL, " * Occupancy: %.2f%%", 100.0 * (cells.length - numEmpty) / cells.length); - UI.printDetailed(Module.ACCEL, " * Objects/Cell: %.2f", (double) numInFull / (double) cells.length); - UI.printDetailed(Module.ACCEL, " * Objects/Used Cell: %.2f", (double) numInFull / (double) (cells.length - numEmpty)); - UI.printDetailed(Module.ACCEL, " * Cells/Object: %.2f", (double) numCellsPerObject / (double) n); - UI.printDetailed(Module.ACCEL, " * Build time: %s", t.toString()); - } - - public void intersect(Ray r, IntersectionState state) { - float intervalMin = r.getMin(); - float intervalMax = r.getMax(); - float orgX = r.ox; - float dirX = r.dx, invDirX = 1 / dirX; - float t1, t2; - t1 = (bounds.getMinimum().x - orgX) * invDirX; - t2 = (bounds.getMaximum().x - orgX) * invDirX; - if (invDirX > 0) { - if (t1 > intervalMin) - intervalMin = t1; - if (t2 < intervalMax) - intervalMax = t2; - } else { - if (t2 > intervalMin) - intervalMin = t2; - if (t1 < intervalMax) - intervalMax = t1; - } - if (intervalMin > intervalMax) - return; - float orgY = r.oy; - float dirY = r.dy, invDirY = 1 / dirY; - t1 = (bounds.getMinimum().y - orgY) * invDirY; - t2 = (bounds.getMaximum().y - orgY) * invDirY; - if (invDirY > 0) { - if (t1 > intervalMin) - intervalMin = t1; - if (t2 < intervalMax) - intervalMax = t2; - } else { - if (t2 > intervalMin) - intervalMin = t2; - if (t1 < intervalMax) - intervalMax = t1; - } - if (intervalMin > intervalMax) - return; - float orgZ = r.oz; - float dirZ = r.dz, invDirZ = 1 / dirZ; - t1 = (bounds.getMinimum().z - orgZ) * invDirZ; - t2 = (bounds.getMaximum().z - orgZ) * invDirZ; - if (invDirZ > 0) { - if (t1 > intervalMin) - intervalMin = t1; - if (t2 < intervalMax) - intervalMax = t2; - } else { - if (t2 > intervalMin) - intervalMin = t2; - if (t1 < intervalMax) - intervalMax = t1; - } - if (intervalMin > intervalMax) - return; - // box is hit at [intervalMin, intervalMax] - orgX += intervalMin * dirX; - orgY += intervalMin * dirY; - orgZ += intervalMin * dirZ; - // locate starting point inside the grid - // and set up 3D-DDA vars - int indxX, indxY, indxZ; - int stepX, stepY, stepZ; - int stopX, stopY, stopZ; - float deltaX, deltaY, deltaZ; - float tnextX, tnextY, tnextZ; - // stepping factors along X - indxX = (int) ((orgX - bounds.getMinimum().x) * invVoxelwx); - if (indxX < 0) - indxX = 0; - else if (indxX >= nx) - indxX = nx - 1; - if (Math.abs(dirX) < 1e-6f) { - stepX = 0; - stopX = indxX; - deltaX = 0; - tnextX = Float.POSITIVE_INFINITY; - } else if (dirX > 0) { - stepX = 1; - stopX = nx; - deltaX = voxelwx * invDirX; - tnextX = intervalMin + ((indxX + 1) * voxelwx + bounds.getMinimum().x - orgX) * invDirX; - } else { - stepX = -1; - stopX = -1; - deltaX = -voxelwx * invDirX; - tnextX = intervalMin + (indxX * voxelwx + bounds.getMinimum().x - orgX) * invDirX; - } - // stepping factors along Y - indxY = (int) ((orgY - bounds.getMinimum().y) * invVoxelwy); - if (indxY < 0) - indxY = 0; - else if (indxY >= ny) - indxY = ny - 1; - if (Math.abs(dirY) < 1e-6f) { - stepY = 0; - stopY = indxY; - deltaY = 0; - tnextY = Float.POSITIVE_INFINITY; - } else if (dirY > 0) { - stepY = 1; - stopY = ny; - deltaY = voxelwy * invDirY; - tnextY = intervalMin + ((indxY + 1) * voxelwy + bounds.getMinimum().y - orgY) * invDirY; - } else { - stepY = -1; - stopY = -1; - deltaY = -voxelwy * invDirY; - tnextY = intervalMin + (indxY * voxelwy + bounds.getMinimum().y - orgY) * invDirY; - } - // stepping factors along Z - indxZ = (int) ((orgZ - bounds.getMinimum().z) * invVoxelwz); - if (indxZ < 0) - indxZ = 0; - else if (indxZ >= nz) - indxZ = nz - 1; - if (Math.abs(dirZ) < 1e-6f) { - stepZ = 0; - stopZ = indxZ; - deltaZ = 0; - tnextZ = Float.POSITIVE_INFINITY; - } else if (dirZ > 0) { - stepZ = 1; - stopZ = nz; - deltaZ = voxelwz * invDirZ; - tnextZ = intervalMin + ((indxZ + 1) * voxelwz + bounds.getMinimum().z - orgZ) * invDirZ; - } else { - stepZ = -1; - stopZ = -1; - deltaZ = -voxelwz * invDirZ; - tnextZ = intervalMin + (indxZ * voxelwz + bounds.getMinimum().z - orgZ) * invDirZ; - } - int cellstepX = stepX; - int cellstepY = stepY * nx; - int cellstepZ = stepZ * ny * nx; - int cell = indxX + indxY * nx + indxZ * ny * nx; - // trace through the grid - for (;;) { - if (tnextX < tnextY && tnextX < tnextZ) { - if (cells[cell] != null) { - for (int i : cells[cell]) - primitives.intersectPrimitive(r, i, state); - if (state.hit() && (r.getMax() < tnextX && r.getMax() < intervalMax)) - return; - } - intervalMin = tnextX; - if (intervalMin > intervalMax) - return; - indxX += stepX; - if (indxX == stopX) - return; - tnextX += deltaX; - cell += cellstepX; - } else if (tnextY < tnextZ) { - if (cells[cell] != null) { - for (int i : cells[cell]) - primitives.intersectPrimitive(r, i, state); - if (state.hit() && (r.getMax() < tnextY && r.getMax() < intervalMax)) - return; - } - intervalMin = tnextY; - if (intervalMin > intervalMax) - return; - indxY += stepY; - if (indxY == stopY) - return; - tnextY += deltaY; - cell += cellstepY; - } else { - if (cells[cell] != null) { - for (int i : cells[cell]) - primitives.intersectPrimitive(r, i, state); - if (state.hit() && (r.getMax() < tnextZ && r.getMax() < intervalMax)) - return; - } - intervalMin = tnextZ; - if (intervalMin > intervalMax) - return; - indxZ += stepZ; - if (indxZ == stopZ) - return; - tnextZ += deltaZ; - cell += cellstepZ; - } - } - } - - private void getGridIndex(float x, float y, float z, int[] i) { - i[0] = MathUtils.clamp((int) ((x - bounds.getMinimum().x) * invVoxelwx), 0, nx - 1); - i[1] = MathUtils.clamp((int) ((y - bounds.getMinimum().y) * invVoxelwy), 0, ny - 1); - i[2] = MathUtils.clamp((int) ((z - bounds.getMinimum().z) * invVoxelwz), 0, nz - 1); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/bucket/BucketOrderFactory.java b/src/org/sunflow/core/bucket/BucketOrderFactory.java deleted file mode 100644 index 9027054..0000000 --- a/src/org/sunflow/core/bucket/BucketOrderFactory.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.sunflow.core.bucket; - -import org.sunflow.PluginRegistry; -import org.sunflow.core.BucketOrder; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class BucketOrderFactory { - public static BucketOrder create(String order) { - boolean flip = false; - if (order.startsWith("inverse") || order.startsWith("invert") || order.startsWith("reverse")) { - String[] tokens = order.split("\\s+"); - if (tokens.length == 2) { - order = tokens[1]; - flip = true; - } - } - BucketOrder o = PluginRegistry.bucketOrderPlugins.createObject(order); - if (o == null) { - UI.printWarning(Module.BCKT, "Unrecognized bucket ordering: \"%s\" - using hilbert", order); - return create("hilbert"); - } - return flip ? new InvertedBucketOrder(o) : o; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/bucket/ColumnBucketOrder.java b/src/org/sunflow/core/bucket/ColumnBucketOrder.java deleted file mode 100644 index c0eac16..0000000 --- a/src/org/sunflow/core/bucket/ColumnBucketOrder.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.sunflow.core.bucket; - -import org.sunflow.core.BucketOrder; - -public class ColumnBucketOrder implements BucketOrder { - public int[] getBucketSequence(int nbw, int nbh) { - int[] coords = new int[2 * nbw * nbh]; - for (int i = 0; i < nbw * nbh; i++) { - int bx = i / nbh; - int by = i % nbh; - if ((bx & 1) == 1) - by = nbh - 1 - by; - coords[2 * i + 0] = bx; - coords[2 * i + 1] = by; - } - return coords; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/bucket/DiagonalBucketOrder.java b/src/org/sunflow/core/bucket/DiagonalBucketOrder.java deleted file mode 100644 index bddefea..0000000 --- a/src/org/sunflow/core/bucket/DiagonalBucketOrder.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.sunflow.core.bucket; - -import org.sunflow.core.BucketOrder; - -public class DiagonalBucketOrder implements BucketOrder { - public int[] getBucketSequence(int nbw, int nbh) { - int[] coords = new int[2 * nbw * nbh]; - int x = 0, y = 0, nx = 1, ny = 0; - for (int i = 0; i < nbw * nbh; i++) { - coords[2 * i + 0] = x; - coords[2 * i + 1] = y; - do { - if (y == ny) { - y = 0; - x = nx; - ny++; - nx++; - } else { - x--; - y++; - } - } while ((y >= nbh || x >= nbw) && i != (nbw * nbh - 1)); - } - return coords; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/bucket/HilbertBucketOrder.java b/src/org/sunflow/core/bucket/HilbertBucketOrder.java deleted file mode 100644 index 8f546e8..0000000 --- a/src/org/sunflow/core/bucket/HilbertBucketOrder.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.sunflow.core.bucket; - -import org.sunflow.core.BucketOrder; - -public class HilbertBucketOrder implements BucketOrder { - public int[] getBucketSequence(int nbw, int nbh) { - int hi = 0; // hilbert curve index - int hn = 0; // hilbert curve order - while (((1 << hn) < nbw || (1 << hn) < nbh) && hn < 16) - hn++; // fit to number of buckets - int hN = 1 << (2 * hn); // number of hilbert buckets - 2**2n - int n = nbw * nbh; // total number of buckets - int[] coords = new int[2 * n]; // storage for bucket coordinates - for (int i = 0; i < n; i++) { - int hx, hy; - do { - // s is the hilbert index, shifted to start in the middle - int s = hi; // (hi + (hN >> 1)) & (hN - 1); - // int n = hn; - // adapted from Hacker's Delight - int comp, swap, cs, t, sr; - s = s | (0x55555555 << (2 * hn)); // Pad s on left with 01 - sr = (s >>> 1) & 0x55555555; // (no change) groups. - cs = ((s & 0x55555555) + sr) ^ 0x55555555;// Compute - // complement - // & swap info in - // two-bit groups. - // Parallel prefix xor op to propagate both complement - // and swap info together from left to right (there is - // no step "cs ^= cs >> 1", so in effect it computes - // two independent parallel prefix operations on two - // interleaved sets of sixteen bits). - cs = cs ^ (cs >>> 2); - cs = cs ^ (cs >>> 4); - cs = cs ^ (cs >>> 8); - cs = cs ^ (cs >>> 16); - swap = cs & 0x55555555; // Separate the swap and - comp = (cs >>> 1) & 0x55555555; // complement bits. - t = (s & swap) ^ comp; // Calculate x and y in - s = s ^ sr ^ t ^ (t << 1); // the odd & even bit - // positions, resp. - s = s & ((1 << 2 * hn) - 1); // Clear out any junk - // on the left (unpad). - // Now "unshuffle" to separate the x and y bits. - t = (s ^ (s >>> 1)) & 0x22222222; - s = s ^ t ^ (t << 1); - t = (s ^ (s >>> 2)) & 0x0C0C0C0C; - s = s ^ t ^ (t << 2); - t = (s ^ (s >>> 4)) & 0x00F000F0; - s = s ^ t ^ (t << 4); - t = (s ^ (s >>> 8)) & 0x0000FF00; - s = s ^ t ^ (t << 8); - hx = s >>> 16; // Assign the two halves - hy = s & 0xFFFF; // of t to x and y. - hi++; - } while ((hx >= nbw || hy >= nbh || hx < 0 || hy < 0) && hi < hN); - coords[2 * i + 0] = hx; - coords[2 * i + 1] = hy; - } - return coords; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/bucket/InvertedBucketOrder.java b/src/org/sunflow/core/bucket/InvertedBucketOrder.java deleted file mode 100644 index bef33b4..0000000 --- a/src/org/sunflow/core/bucket/InvertedBucketOrder.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.sunflow.core.bucket; - -import org.sunflow.core.BucketOrder; - -public class InvertedBucketOrder implements BucketOrder { - private BucketOrder order; - - public InvertedBucketOrder(BucketOrder order) { - this.order = order; - } - - public int[] getBucketSequence(int nbw, int nbh) { - int[] coords = order.getBucketSequence(nbw, nbh); - for (int i = 0; i < coords.length / 2; i += 2) { - int src = i; - int dst = coords.length - 2 - i; - int tmp = coords[src + 0]; - coords[src + 0] = coords[dst + 0]; - coords[dst + 0] = tmp; - tmp = coords[src + 1]; - coords[src + 1] = coords[dst + 1]; - coords[dst + 1] = tmp; - } - return coords; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/bucket/RandomBucketOrder.java b/src/org/sunflow/core/bucket/RandomBucketOrder.java deleted file mode 100644 index bd608aa..0000000 --- a/src/org/sunflow/core/bucket/RandomBucketOrder.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.sunflow.core.bucket; - -import org.sunflow.core.BucketOrder; - -public class RandomBucketOrder implements BucketOrder { - public int[] getBucketSequence(int nbw, int nbh) { - int[] coords = new int[2 * nbw * nbh]; - for (int i = 0; i < nbw * nbh; i++) { - int by = i / nbw; - int bx = i % nbw; - if ((by & 1) == 1) - bx = nbw - 1 - bx; - coords[2 * i + 0] = bx; - coords[2 * i + 1] = by; - } - - long seed = 2463534242L; - for (int i = 0; i < coords.length; i++) { - // pick 2 random indices - seed = xorshift(seed); - int src = mod((int) seed, nbw * nbh); - seed = xorshift(seed); - int dst = mod((int) seed, nbw * nbh); - int tmp = coords[2 * src + 0]; - coords[2 * src + 0] = coords[2 * dst + 0]; - coords[2 * dst + 0] = tmp; - tmp = coords[2 * src + 1]; - coords[2 * src + 1] = coords[2 * dst + 1]; - coords[2 * dst + 1] = tmp; - } - - return coords; - } - - private int mod(int a, int b) { - int m = a % b; - return (m < 0) ? m + b : m; - } - - private long xorshift(long y) { - y = y ^ (y << 13); - y = y ^ (y >>> 17); // unsigned - y = y ^ (y << 5); - return y; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/bucket/RowBucketOrder.java b/src/org/sunflow/core/bucket/RowBucketOrder.java deleted file mode 100644 index 8b02688..0000000 --- a/src/org/sunflow/core/bucket/RowBucketOrder.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.sunflow.core.bucket; - -import org.sunflow.core.BucketOrder; - -public class RowBucketOrder implements BucketOrder { - public int[] getBucketSequence(int nbw, int nbh) { - int[] coords = new int[2 * nbw * nbh]; - for (int i = 0; i < nbw * nbh; i++) { - int by = i / nbw; - int bx = i % nbw; - if ((by & 1) == 1) - bx = nbw - 1 - bx; - coords[2 * i + 0] = bx; - coords[2 * i + 1] = by; - } - return coords; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/bucket/SpiralBucketOrder.java b/src/org/sunflow/core/bucket/SpiralBucketOrder.java deleted file mode 100644 index 4d33688..0000000 --- a/src/org/sunflow/core/bucket/SpiralBucketOrder.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.sunflow.core.bucket; - -import org.sunflow.core.BucketOrder; - -public class SpiralBucketOrder implements BucketOrder { - public int[] getBucketSequence(int nbw, int nbh) { - int[] coords = new int[2 * nbw * nbh]; - for (int i = 0; i < nbw * nbh; i++) { - int bx, by; - int center = (Math.min(nbw, nbh) - 1) / 2; - int nx = nbw; - int ny = nbh; - while (i < (nx * ny)) { - nx--; - ny--; - } - int nxny = nx * ny; - int minnxny = Math.min(nx, ny); - if ((minnxny & 1) == 1) { - if (i <= (nxny + ny)) { - bx = nx - minnxny / 2; - by = -minnxny / 2 + i - nxny; - } else { - bx = nx - minnxny / 2 - (i - (nxny + ny)); - by = ny - minnxny / 2; - } - } else { - if (i <= (nxny + ny)) { - bx = -minnxny / 2; - by = ny - minnxny / 2 - (i - nxny); - } else { - bx = -minnxny / 2 + (i - (nxny + ny)); - by = -minnxny / 2; - } - } - coords[2 * i + 0] = bx + center; - coords[2 * i + 1] = by + center; - } - return coords; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/camera/FisheyeLens.java b/src/org/sunflow/core/camera/FisheyeLens.java deleted file mode 100644 index fecd778..0000000 --- a/src/org/sunflow/core/camera/FisheyeLens.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.sunflow.core.camera; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.CameraLens; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; - -public class FisheyeLens implements CameraLens { - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lensX, double lensY, double time) { - float cx = 2.0f * x / imageWidth - 1.0f; - float cy = 2.0f * y / imageHeight - 1.0f; - float r2 = cx * cx + cy * cy; - if (r2 > 1) - return null; // outside the fisheye - return new Ray(0, 0, 0, cx, cy, (float) -Math.sqrt(1 - r2)); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/camera/PinholeLens.java b/src/org/sunflow/core/camera/PinholeLens.java deleted file mode 100644 index 0a9b86e..0000000 --- a/src/org/sunflow/core/camera/PinholeLens.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.sunflow.core.camera; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.CameraLens; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; - -public class PinholeLens implements CameraLens { - private float au, av; - private float aspect, fov; - private float shiftX, shiftY; - - public PinholeLens() { - fov = 90; - aspect = 1; - shiftX = shiftY = 0; - update(); - } - - public boolean update(ParameterList pl, SunflowAPI api) { - // get parameters - fov = pl.getFloat("fov", fov); - aspect = pl.getFloat("aspect", aspect); - shiftX = pl.getFloat("shift.x", shiftX); - shiftY = pl.getFloat("shift.y", shiftY); - update(); - return true; - } - - private void update() { - au = (float) Math.tan(Math.toRadians(fov * 0.5f)); - av = au / aspect; - } - - public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lensX, double lensY, double time) { - float du = shiftX - au + ((2.0f * au * x) / (imageWidth - 1.0f)); - float dv = shiftY - av + ((2.0f * av * y) / (imageHeight - 1.0f)); - return new Ray(0, 0, 0, du, dv, -1); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/camera/SphericalLens.java b/src/org/sunflow/core/camera/SphericalLens.java deleted file mode 100644 index 5746aac..0000000 --- a/src/org/sunflow/core/camera/SphericalLens.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.sunflow.core.camera; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.CameraLens; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; - -public class SphericalLens implements CameraLens { - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lensX, double lensY, double time) { - // Generate environment camera ray direction - double theta = 2 * Math.PI * x / imageWidth + Math.PI / 2; - double phi = Math.PI * (imageHeight - 1 - y) / imageHeight; - return new Ray(0, 0, 0, (float) (Math.cos(theta) * Math.sin(phi)), (float) (Math.cos(phi)), (float) (Math.sin(theta) * Math.sin(phi))); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/camera/ThinLens.java b/src/org/sunflow/core/camera/ThinLens.java deleted file mode 100644 index 092a6e4..0000000 --- a/src/org/sunflow/core/camera/ThinLens.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.sunflow.core.camera; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.CameraLens; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; - -public class ThinLens implements CameraLens { - private float au, av; - private float aspect, fov; - private float shiftX, shiftY; - private float focusDistance; - private float lensRadius; - private int lensSides; - private float lensRotation; - private float lensRotationRadians; - - public ThinLens() { - focusDistance = 1; - lensRadius = 0; - fov = 90; - aspect = 1; - lensSides = 0; // < 3 means use circular lens - lensRotation = lensRotationRadians = 0; // this rotates polygonal lenses - } - - public boolean update(ParameterList pl, SunflowAPI api) { - // get parameters - fov = pl.getFloat("fov", fov); - aspect = pl.getFloat("aspect", aspect); - shiftX = pl.getFloat("shift.x", shiftX); - shiftY = pl.getFloat("shift.y", shiftY); - focusDistance = pl.getFloat("focus.distance", focusDistance); - lensRadius = pl.getFloat("lens.radius", lensRadius); - lensSides = pl.getInt("lens.sides", lensSides); - lensRotation = pl.getFloat("lens.rotation", lensRotation); - update(); - return true; - } - - private void update() { - au = (float) Math.tan(Math.toRadians(fov * 0.5f)) * focusDistance; - av = au / aspect; - lensRotationRadians = (float) Math.toRadians(lensRotation); - } - - public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lensX, double lensY, double time) { - float du = shiftX * focusDistance - au + ((2.0f * au * x) / (imageWidth - 1.0f)); - float dv = shiftY * focusDistance - av + ((2.0f * av * y) / (imageHeight - 1.0f)); - - float eyeX, eyeY; - if (lensSides < 3) { - double angle, r; - // concentric map sampling - double r1 = 2 * lensX - 1; - double r2 = 2 * lensY - 1; - if (r1 > -r2) { - if (r1 > r2) { - r = r1; - angle = 0.25 * Math.PI * r2 / r1; - } else { - r = r2; - angle = 0.25 * Math.PI * (2 - r1 / r2); - } - } else { - if (r1 < r2) { - r = -r1; - angle = 0.25 * Math.PI * (4 + r2 / r1); - } else { - r = -r2; - if (r2 != 0) - angle = 0.25 * Math.PI * (6 - r1 / r2); - else - angle = 0; - } - } - r *= lensRadius; - // point on the lens - eyeX = (float) (Math.cos(angle) * r); - eyeY = (float) (Math.sin(angle) * r); - } else { - // sample N-gon - // FIXME: this could use concentric sampling - lensY *= lensSides; - float side = (int) lensY; - float offs = (float) lensY - side; - float dist = (float) Math.sqrt(lensX); - float a0 = (float) (side * Math.PI * 2.0f / lensSides + lensRotationRadians); - float a1 = (float) ((side + 1.0f) * Math.PI * 2.0f / lensSides + lensRotationRadians); - eyeX = (float) ((Math.cos(a0) * (1.0f - offs) + Math.cos(a1) * offs) * dist); - eyeY = (float) ((Math.sin(a0) * (1.0f - offs) + Math.sin(a1) * offs) * dist); - eyeX *= lensRadius; - eyeY *= lensRadius; - } - float eyeZ = 0; - // point on the image plane - float dirX = du; - float dirY = dv; - float dirZ = -focusDistance; - // ray - return new Ray(eyeX, eyeY, eyeZ, dirX - eyeX, dirY - eyeY, dirZ - eyeZ); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/display/FastDisplay.java b/src/org/sunflow/core/display/FastDisplay.java deleted file mode 100644 index 5c62f4e..0000000 --- a/src/org/sunflow/core/display/FastDisplay.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.sunflow.core.display; - -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.event.KeyAdapter; -import java.awt.event.KeyEvent; -import java.awt.image.BufferedImage; - -import javax.swing.JFrame; -import javax.swing.JPanel; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Display; -import org.sunflow.image.Color; -import org.sunflow.system.Timer; - -@SuppressWarnings("serial") -public class FastDisplay extends JPanel implements Display { - private JFrame frame; - private BufferedImage image; - private int[] pixels; - private Timer t; - private float seconds; - private int frames; - - public FastDisplay() { - image = null; - frame = null; - t = new Timer(); - frames = 0; - seconds = 0; - } - - public synchronized void imageBegin(int w, int h, int bucketSize) { - if (frame != null && image != null && w == image.getWidth() && h == image.getHeight()) { - // nothing to do - } else { - // allocate new framebuffer - pixels = new int[w * h]; - image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - // prepare frame - if (frame == null) { - setPreferredSize(new Dimension(w, h)); - frame = new JFrame("Sunflow v" + SunflowAPI.VERSION); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.addKeyListener(new KeyAdapter() { - @Override - public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ESCAPE) - System.exit(0); - } - }); - frame.setContentPane(this); - frame.pack(); - frame.setLocationRelativeTo(null); - frame.setVisible(true); - } - } - // start counter - t.start(); - } - - public void imagePrepare(int x, int y, int w, int h, int id) { - } - - public void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha) { - int iw = image.getWidth(); - int off = x + iw * y; - iw -= w; - for (int j = 0, index = 0; j < h; j++, off += iw) - for (int i = 0; i < w; i++, index++, off++) - pixels[off] = 0xFF000000 | data[index].toRGB(); - } - - public void imageFill(int x, int y, int w, int h, Color c, float alpha) { - int iw = image.getWidth(); - int off = x + iw * y; - iw -= w; - int rgb = 0xFF000000 | c.toRGB(); - for (int j = 0, index = 0; j < h; j++, off += iw) - for (int i = 0; i < w; i++, index++, off++) - pixels[off] = rgb; - } - - public synchronized void imageEnd() { - // copy buffer - image.setRGB(0, 0, image.getWidth(), image.getHeight(), pixels, 0, image.getWidth()); - repaint(); - // update stats - t.end(); - seconds += t.seconds(); - frames++; - if (seconds > 1) { - // display average fps every second - frame.setTitle(String.format("Sunflow v%s - %.2f fps", SunflowAPI.VERSION, frames / seconds)); - frames = 0; - seconds = 0; - } - } - - @Override - public synchronized void paint(Graphics g) { - if (image == null) - return; - g.drawImage(image, 0, 0, null); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/display/FileDisplay.java b/src/org/sunflow/core/display/FileDisplay.java deleted file mode 100644 index 151b684..0000000 --- a/src/org/sunflow/core/display/FileDisplay.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.sunflow.core.display; - -import java.io.IOException; - -import org.sunflow.PluginRegistry; -import org.sunflow.core.Display; -import org.sunflow.image.BitmapWriter; -import org.sunflow.image.Color; -import org.sunflow.system.FileUtils; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class FileDisplay implements Display { - private BitmapWriter writer; - private String filename; - - public FileDisplay(boolean saveImage) { - this(saveImage ? "output.png" : ".none"); - } - - public FileDisplay(String filename) { - this.filename = filename == null ? "output.png" : filename; - String extension = FileUtils.getExtension(filename); - writer = PluginRegistry.bitmapWriterPlugins.createObject(extension); - } - - public void imageBegin(int w, int h, int bucketSize) { - if (writer == null) - return; - try { - writer.openFile(filename); - writer.writeHeader(w, h, bucketSize); - } catch (IOException e) { - UI.printError(Module.IMG, "I/O error occured while preparing image for display: %s", e.getMessage()); - } - } - - public void imagePrepare(int x, int y, int w, int h, int id) { - // does nothing for files - } - - public void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha) { - if (writer == null) - return; - try { - writer.writeTile(x, y, w, h, data, alpha); - } catch (IOException e) { - UI.printError(Module.IMG, "I/O error occured while writing image tile [(%d,%d) %dx%d] image for display: %s", x, y, w, h, e.getMessage()); - } - } - - public void imageFill(int x, int y, int w, int h, Color c, float alpha) { - if (writer == null) - return; - Color[] colorTile = new Color[w * h]; - float[] alphaTile = new float[w * h]; - for (int i = 0; i < colorTile.length; i++) { - colorTile[i] = c; - alphaTile[i] = alpha; - } - imageUpdate(x, y, w, h, colorTile, alphaTile); - } - - public void imageEnd() { - if (writer == null) - return; - try { - writer.closeFile(); - } catch (IOException e) { - UI.printError(Module.IMG, "I/O error occured while closing the display: %s", e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/display/FrameDisplay.java b/src/org/sunflow/core/display/FrameDisplay.java deleted file mode 100644 index 7999f7a..0000000 --- a/src/org/sunflow/core/display/FrameDisplay.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.sunflow.core.display; - -import java.awt.Dimension; -import java.awt.Toolkit; -import java.awt.event.KeyAdapter; -import java.awt.event.KeyEvent; - -import javax.swing.JFrame; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Display; -import org.sunflow.image.Color; -import org.sunflow.system.ImagePanel; - -public class FrameDisplay implements Display { - private String filename; - private RenderFrame frame; - - public FrameDisplay() { - this(null); - } - - public FrameDisplay(String filename) { - this.filename = filename; - frame = null; - } - - public void imageBegin(int w, int h, int bucketSize) { - if (frame == null) { - frame = new RenderFrame(); - frame.imagePanel.imageBegin(w, h, bucketSize); - Dimension screenRes = Toolkit.getDefaultToolkit().getScreenSize(); - boolean needFit = false; - if (w >= (screenRes.getWidth() - 200) || h >= (screenRes.getHeight() - 200)) { - frame.imagePanel.setPreferredSize(new Dimension((int) screenRes.getWidth() - 200, (int) screenRes.getHeight() - 200)); - needFit = true; - } else - frame.imagePanel.setPreferredSize(new Dimension(w, h)); - frame.pack(); - frame.setLocationRelativeTo(null); - frame.setVisible(true); - if (needFit) - frame.imagePanel.fit(); - } else - frame.imagePanel.imageBegin(w, h, bucketSize); - } - - public void imagePrepare(int x, int y, int w, int h, int id) { - frame.imagePanel.imagePrepare(x, y, w, h, id); - } - - public void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha) { - frame.imagePanel.imageUpdate(x, y, w, h, data, alpha); - } - - public void imageFill(int x, int y, int w, int h, Color c, float alpha) { - frame.imagePanel.imageFill(x, y, w, h, c, alpha); - } - - public void imageEnd() { - frame.imagePanel.imageEnd(); - if (filename != null) - frame.imagePanel.save(filename); - } - - @SuppressWarnings("serial") - private static class RenderFrame extends JFrame { - ImagePanel imagePanel; - - RenderFrame() { - super("Sunflow v" + SunflowAPI.VERSION); - setDefaultCloseOperation(EXIT_ON_CLOSE); - addKeyListener(new KeyAdapter() { - @Override - public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ESCAPE) - System.exit(0); - } - }); - imagePanel = new ImagePanel(); - setContentPane(imagePanel); - pack(); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/display/ImgPipeDisplay.java b/src/org/sunflow/core/display/ImgPipeDisplay.java deleted file mode 100644 index b11017a..0000000 --- a/src/org/sunflow/core/display/ImgPipeDisplay.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.sunflow.core.display; - -import java.io.IOException; - -import javax.swing.JPanel; - -import org.sunflow.core.Display; -import org.sunflow.image.Color; - -@SuppressWarnings("serial") -public class ImgPipeDisplay extends JPanel implements Display { - private int ih; - - /** - * Render to stdout using the imgpipe protocol used in mental image's - * imf_disp viewer. http://www.lamrug.org/resources/stubtips.html - */ - public ImgPipeDisplay() { - } - - public synchronized void imageBegin(int w, int h, int bucketSize) { - ih = h; - outputPacket(5, w, h, Float.floatToRawIntBits(1.0f), 0); - System.out.flush(); - } - - public synchronized void imagePrepare(int x, int y, int w, int h, int id) { - } - - public synchronized void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha) { - int xl = x; - int xh = x + w - 1; - int yl = ih - 1 - (y + h - 1); - int yh = ih - 1 - y; - outputPacket(2, xl, xh, yl, yh); - byte[] rgba = new byte[4 * (yh - yl + 1) * (xh - xl + 1)]; - for (int j = 0, idx = 0; j < h; j++) { - for (int i = 0; i < w; i++, idx += 4) { - int rgb = data[(h - j - 1) * w + i].toNonLinear().toRGB(); - int cr = (rgb >> 16) & 0xFF; - int cg = (rgb >> 8) & 0xFF; - int cb = rgb & 0xFF; - rgba[idx + 0] = (byte) (cr & 0xFF); - rgba[idx + 1] = (byte) (cg & 0xFF); - rgba[idx + 2] = (byte) (cb & 0xFF); - rgba[idx + 3] = (byte) (0xFF); - } - } - try { - System.out.write(rgba); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public synchronized void imageFill(int x, int y, int w, int h, Color c, float alpha) { - int xl = x; - int xh = x + w - 1; - int yl = ih - 1 - (y + h - 1); - int yh = ih - 1 - y; - outputPacket(2, xl, xh, yl, yh); - int rgb = c.toNonLinear().toRGB(); - int cr = (rgb >> 16) & 0xFF; - int cg = (rgb >> 8) & 0xFF; - int cb = rgb & 0xFF; - byte[] rgba = new byte[4 * (yh - yl + 1) * (xh - xl + 1)]; - for (int j = 0, idx = 0; j < h; j++) { - for (int i = 0; i < w; i++, idx += 4) { - rgba[idx + 0] = (byte) (cr & 0xFF); - rgba[idx + 1] = (byte) (cg & 0xFF); - rgba[idx + 2] = (byte) (cb & 0xFF); - rgba[idx + 3] = (byte) (0xFF); - } - } - try { - System.out.write(rgba); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public synchronized void imageEnd() { - outputPacket(4, 0, 0, 0, 0); - System.out.flush(); - } - - private void outputPacket(int type, int d0, int d1, int d2, int d3) { - outputInt32(type); - outputInt32(d0); - outputInt32(d1); - outputInt32(d2); - outputInt32(d3); - } - - private void outputInt32(int i) { - System.out.write((i >> 24) & 0xFF); - System.out.write((i >> 16) & 0xFF); - System.out.write((i >> 8) & 0xFF); - System.out.write(i & 0xFF); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/filter/BlackmanHarrisFilter.java b/src/org/sunflow/core/filter/BlackmanHarrisFilter.java deleted file mode 100644 index e5f26f6..0000000 --- a/src/org/sunflow/core/filter/BlackmanHarrisFilter.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.sunflow.core.filter; - -import org.sunflow.core.Filter; - -public class BlackmanHarrisFilter implements Filter { - public float getSize() { - return 4; - } - - public float get(float x, float y) { - return bh1d(x * 0.5f) * bh1d(y * 0.5f); - } - - private float bh1d(float x) { - if (x < -1.0f || x > 1.0f) - return 0.0f; - x = (x + 1) * 0.5f; - final double A0 = 0.35875; - final double A1 = -0.48829; - final double A2 = 0.14128; - final double A3 = -0.01168; - return (float) (A0 + A1 * Math.cos(2 * Math.PI * x) + A2 * Math.cos(4 * Math.PI * x) + A3 * Math.cos(6 * Math.PI * x)); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/filter/BoxFilter.java b/src/org/sunflow/core/filter/BoxFilter.java deleted file mode 100644 index 78f9cf2..0000000 --- a/src/org/sunflow/core/filter/BoxFilter.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.sunflow.core.filter; - -import org.sunflow.core.Filter; - -public class BoxFilter implements Filter { - public float getSize() { - return 1.0f; - } - - public float get(float x, float y) { - return 1.0f; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/filter/CatmullRomFilter.java b/src/org/sunflow/core/filter/CatmullRomFilter.java deleted file mode 100644 index 1f97c17..0000000 --- a/src/org/sunflow/core/filter/CatmullRomFilter.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.sunflow.core.filter; - -import org.sunflow.core.Filter; - -public class CatmullRomFilter implements Filter { - public float getSize() { - return 4.0f; - } - - public float get(float x, float y) { - return catrom1d(x) * catrom1d(y); - } - - private float catrom1d(float x) { - x = Math.abs(x); - float x2 = x * x; - float x3 = x * x2; - if (x >= 2) - return 0; - if (x < 1) - return 3 * x3 - 5 * x2 + 2; - return -x3 + 5 * x2 - 8 * x + 4; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/filter/CubicBSpline.java b/src/org/sunflow/core/filter/CubicBSpline.java deleted file mode 100644 index 805d6ea..0000000 --- a/src/org/sunflow/core/filter/CubicBSpline.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.sunflow.core.filter; - -import org.sunflow.core.Filter; - -public class CubicBSpline implements Filter { - public float get(float x, float y) { - return B3(x) * B3(y); - } - - public float getSize() { - return 4; - } - - private float B3(float t) { - t = Math.abs(t); - if (t <= 1) - return b1(1 - t); - return b0(2 - t); - } - - private float b0(float t) { - return t * t * t * (1.0f / 6); - } - - private float b1(float t) { - return (1.0f / 6) * (-3 * t * t * t + 3 * t * t + 3 * t + 1); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/filter/GaussianFilter.java b/src/org/sunflow/core/filter/GaussianFilter.java deleted file mode 100644 index 4f4a082..0000000 --- a/src/org/sunflow/core/filter/GaussianFilter.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.sunflow.core.filter; - -import org.sunflow.core.Filter; - -public class GaussianFilter implements Filter { - private float es2; - - public GaussianFilter() { - es2 = (float) -Math.exp(-getSize() * getSize()); - } - - public float getSize() { - return 3.0f; - } - - public float get(float x, float y) { - float gx = (float) Math.exp(-x * x) + es2; - float gy = (float) Math.exp(-y * y) + es2; - return gx * gy; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/filter/LanczosFilter.java b/src/org/sunflow/core/filter/LanczosFilter.java deleted file mode 100644 index 4fa56ae..0000000 --- a/src/org/sunflow/core/filter/LanczosFilter.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.sunflow.core.filter; - -import org.sunflow.core.Filter; - -public class LanczosFilter implements Filter { - public float getSize() { - return 4.0f; - } - - public float get(float x, float y) { - return sinc1d(x * 0.5f) * sinc1d(y * 0.5f); - } - - private float sinc1d(float x) { - x = Math.abs(x); - if (x < 1e-5f) - return 1; - if (x > 1.0f) - return 0; - x *= Math.PI; - float sinc = (float) Math.sin(3 * x) / (3 * x); - float lanczos = (float) Math.sin(x) / x; - return sinc * lanczos; - } - -} \ No newline at end of file diff --git a/src/org/sunflow/core/filter/MitchellFilter.java b/src/org/sunflow/core/filter/MitchellFilter.java deleted file mode 100644 index 6a0117f..0000000 --- a/src/org/sunflow/core/filter/MitchellFilter.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.sunflow.core.filter; - -import org.sunflow.core.Filter; - -public class MitchellFilter implements Filter { - public float getSize() { - return 4.0f; - } - - public float get(float x, float y) { - return mitchell(x) * mitchell(y); - } - - private float mitchell(float x) { - final float B = 1 / 3.0f; - final float C = 1 / 3.0f; - final float SIXTH = 1 / 6.0f; - x = Math.abs(x); - float x2 = x * x; - if (x > 1.0f) - return ((-B - 6 * C) * x * x2 + (6 * B + 30 * C) * x2 + (-12 * B - 48 * C) * x + (8 * B + 24 * C)) * SIXTH; - return ((12 - 9 * B - 6 * C) * x * x2 + (-18 + 12 * B + 6 * C) * x2 + (6 - 2 * B)) * SIXTH; - } -} diff --git a/src/org/sunflow/core/filter/SincFilter.java b/src/org/sunflow/core/filter/SincFilter.java deleted file mode 100644 index dea9e37..0000000 --- a/src/org/sunflow/core/filter/SincFilter.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.sunflow.core.filter; - -import org.sunflow.core.Filter; - -public class SincFilter implements Filter { - public float getSize() { - return 4; - } - - public float get(float x, float y) { - return sinc1d(x) * sinc1d(y); - } - - private float sinc1d(float x) { - x = Math.abs(x); - if (x < 0.0001f) - return 1.0f; - x *= Math.PI; - return (float) Math.sin(x) / x; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/filter/TriangleFilter.java b/src/org/sunflow/core/filter/TriangleFilter.java deleted file mode 100644 index 3cbca11..0000000 --- a/src/org/sunflow/core/filter/TriangleFilter.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.sunflow.core.filter; - -import org.sunflow.core.Filter; - -public class TriangleFilter implements Filter { - public float getSize() { - return 2; - } - - public float get(float x, float y) { - return (1.0f - Math.abs(x)) * (1.0f - Math.abs(y)); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/gi/AmbientOcclusionGIEngine.java b/src/org/sunflow/core/gi/AmbientOcclusionGIEngine.java deleted file mode 100644 index 80a9dea..0000000 --- a/src/org/sunflow/core/gi/AmbientOcclusionGIEngine.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.sunflow.core.gi; - -import org.sunflow.core.GIEngine; -import org.sunflow.core.Options; -import org.sunflow.core.Ray; -import org.sunflow.core.Scene; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Vector3; - -public class AmbientOcclusionGIEngine implements GIEngine { - private Color bright; - private Color dark; - private int samples; - private float maxDist; - - public Color getGlobalRadiance(ShadingState state) { - return Color.BLACK; - } - - public boolean init(Options options, Scene scene) { - bright = options.getColor("gi.ambocc.bright", Color.WHITE); - dark = options.getColor("gi.ambocc.dark", Color.BLACK); - samples = options.getInt("gi.ambocc.samples", 32); - maxDist = options.getFloat("gi.ambocc.maxdist", 0); - maxDist = (maxDist <= 0) ? Float.POSITIVE_INFINITY : maxDist; - return true; - } - - public Color getIrradiance(ShadingState state, Color diffuseReflectance) { - OrthoNormalBasis onb = state.getBasis(); - Vector3 w = new Vector3(); - Color result = Color.black(); - for (int i = 0; i < samples; i++) { - float xi = (float) state.getRandom(i, 0, samples); - float xj = (float) state.getRandom(i, 1, samples); - float phi = (float) (2 * Math.PI * xi); - float cosPhi = (float) Math.cos(phi); - float sinPhi = (float) Math.sin(phi); - float sinTheta = (float) Math.sqrt(xj); - float cosTheta = (float) Math.sqrt(1.0f - xj); - w.x = cosPhi * sinTheta; - w.y = sinPhi * sinTheta; - w.z = cosTheta; - onb.transform(w); - Ray r = new Ray(state.getPoint(), w); - r.setMax(maxDist); - result.add(Color.blend(bright, dark, state.traceShadow(r))); - } - return result.mul((float) Math.PI / samples); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/gi/FakeGIEngine.java b/src/org/sunflow/core/gi/FakeGIEngine.java deleted file mode 100644 index c1cc46c..0000000 --- a/src/org/sunflow/core/gi/FakeGIEngine.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.sunflow.core.gi; - -import org.sunflow.core.GIEngine; -import org.sunflow.core.Options; -import org.sunflow.core.Scene; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.Vector3; - -/** - * This is a quick way to get a bit of ambient lighting into your scene with - * hardly any overhead. It's based on the formula found here: - * - * @link http://www.cs.utah.edu/~shirley/papers/rtrt/node7.html#SECTION00031100000000000000 - */ -public class FakeGIEngine implements GIEngine { - private Vector3 up; - private Color sky; - private Color ground; - - public Color getIrradiance(ShadingState state, Color diffuseReflectance) { - float cosTheta = Vector3.dot(up, state.getNormal()); - float sin2 = (1 - cosTheta * cosTheta); - float sine = sin2 > 0 ? (float) Math.sqrt(sin2) * 0.5f : 0; - if (cosTheta > 0) - return Color.blend(sky, ground, sine); - else - return Color.blend(ground, sky, sine); - } - - public Color getGlobalRadiance(ShadingState state) { - return Color.BLACK; - } - - public boolean init(Options options, Scene scene) { - up = options.getVector("gi.fake.up", new Vector3(0, 1, 0)).normalize(); - sky = options.getColor("gi.fake.sky", Color.WHITE).copy(); - ground = options.getColor("gi.fake.ground", Color.BLACK).copy(); - sky.mul((float) Math.PI); - ground.mul((float) Math.PI); - return true; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/gi/InstantGI.java b/src/org/sunflow/core/gi/InstantGI.java deleted file mode 100644 index 6f41a60..0000000 --- a/src/org/sunflow/core/gi/InstantGI.java +++ /dev/null @@ -1,176 +0,0 @@ -package org.sunflow.core.gi; - -import java.util.ArrayList; - -import org.sunflow.core.GIEngine; -import org.sunflow.core.Options; -import org.sunflow.core.PhotonStore; -import org.sunflow.core.Ray; -import org.sunflow.core.Scene; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class InstantGI implements GIEngine { - private int numPhotons; - private int numSets; - private float c; - private int numBias; - private PointLight[][] virtualLights; - - public Color getGlobalRadiance(ShadingState state) { - Point3 p = state.getPoint(); - Vector3 n = state.getNormal(); - int set = (int) (state.getRandom(0, 1, 1) * numSets); - float maxAvgPow = 0; - float minDist = 1; - Color pow = null; - for (PointLight vpl : virtualLights[set]) { - maxAvgPow = Math.max(maxAvgPow, vpl.power.getAverage()); - if (Vector3.dot(n, vpl.n) > 0.9f) { - float d = vpl.p.distanceToSquared(p); - if (d < minDist) { - pow = vpl.power; - minDist = d; - } - } - } - return pow == null ? Color.BLACK : pow.copy().mul(1.0f / maxAvgPow); - } - - public boolean init(Options options, Scene scene) { - numPhotons = options.getInt("gi.igi.samples", 64); - numSets = options.getInt("gi.igi.sets", 1); - c = options.getFloat("gi.igi.c", 0.00003f); - numBias = options.getInt("gi.igi.bias_samples", 0); - virtualLights = null; - if (numSets < 1) - numSets = 1; - UI.printInfo(Module.LIGHT, "Instant Global Illumination settings:"); - UI.printInfo(Module.LIGHT, " * Samples: %d", numPhotons); - UI.printInfo(Module.LIGHT, " * Sets: %d", numSets); - UI.printInfo(Module.LIGHT, " * Bias bound: %f", c); - UI.printInfo(Module.LIGHT, " * Bias rays: %d", numBias); - virtualLights = new PointLight[numSets][]; - if (numPhotons > 0) { - for (int i = 0, seed = 0; i < virtualLights.length; i++, seed += numPhotons) { - PointLightStore map = new PointLightStore(); - if (!scene.calculatePhotons(map, "virtual", seed, options)) - return false; - virtualLights[i] = map.virtualLights.toArray(new PointLight[map.virtualLights.size()]); - UI.printInfo(Module.LIGHT, "Stored %d virtual point lights for set %d of %d", virtualLights[i].length, i + 1, numSets); - } - } else { - // create an empty array - for (int i = 0; i < virtualLights.length; i++) - virtualLights[i] = new PointLight[0]; - } - return true; - } - - public Color getIrradiance(ShadingState state, Color diffuseReflectance) { - float b = (float) Math.PI * c / diffuseReflectance.getMax(); - Color irr = Color.black(); - Point3 p = state.getPoint(); - Vector3 n = state.getNormal(); - int set = (int) (state.getRandom(0, 1, 1) * numSets); - for (PointLight vpl : virtualLights[set]) { - Ray r = new Ray(p, vpl.p); - float dotNlD = -(r.dx * vpl.n.x + r.dy * vpl.n.y + r.dz * vpl.n.z); - float dotND = r.dx * n.x + r.dy * n.y + r.dz * n.z; - if (dotNlD > 0 && dotND > 0) { - float r2 = r.getMax() * r.getMax(); - Color opacity = state.traceShadow(r); - Color power = Color.blend(vpl.power, Color.BLACK, opacity); - float g = (dotND * dotNlD) / r2; - irr.madd(0.25f * Math.min(g, b), power); - } - } - // bias compensation - int nb = (state.getDiffuseDepth() == 0 || numBias <= 0) ? numBias : 1; - if (nb <= 0) - return irr; - OrthoNormalBasis onb = state.getBasis(); - Vector3 w = new Vector3(); - float scale = (float) Math.PI / nb; - for (int i = 0; i < nb; i++) { - float xi = (float) state.getRandom(i, 0, nb); - float xj = (float) state.getRandom(i, 1, nb); - float phi = (float) (xi * 2 * Math.PI); - float cosPhi = (float) Math.cos(phi); - float sinPhi = (float) Math.sin(phi); - float sinTheta = (float) Math.sqrt(xj); - float cosTheta = (float) Math.sqrt(1.0f - xj); - w.x = cosPhi * sinTheta; - w.y = sinPhi * sinTheta; - w.z = cosTheta; - onb.transform(w); - Ray r = new Ray(state.getPoint(), w); - r.setMax((float) Math.sqrt(cosTheta / b)); - ShadingState temp = state.traceFinalGather(r, i); - if (temp != null) { - temp.getInstance().prepareShadingState(temp); - if (temp.getShader() != null) { - float dist = temp.getRay().getMax(); - float r2 = dist * dist; - float cosThetaY = -Vector3.dot(w, temp.getNormal()); - if (cosThetaY > 0) { - float g = (cosTheta * cosThetaY) / r2; - // was this path accounted for yet? - if (g > b) - irr.madd(scale * (g - b) / g, temp.getShader().getRadiance(temp)); - } - } - } - } - return irr; - } - - private static class PointLight { - Point3 p; - Vector3 n; - Color power; - } - - private class PointLightStore implements PhotonStore { - ArrayList virtualLights = new ArrayList(); - - public int numEmit() { - return numPhotons; - } - - public void prepare(Options options, BoundingBox sceneBounds) { - } - - public void store(ShadingState state, Vector3 dir, Color power, Color diffuse) { - state.faceforward(); - PointLight vpl = new PointLight(); - vpl.p = state.getPoint(); - vpl.n = state.getNormal(); - vpl.power = power; - synchronized (this) { - virtualLights.add(vpl); - } - } - - public void init() { - } - - public boolean allowDiffuseBounced() { - return true; - } - - public boolean allowReflectionBounced() { - return true; - } - - public boolean allowRefractionBounced() { - return true; - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/gi/IrradianceCacheGIEngine.java b/src/org/sunflow/core/gi/IrradianceCacheGIEngine.java deleted file mode 100644 index 230198b..0000000 --- a/src/org/sunflow/core/gi/IrradianceCacheGIEngine.java +++ /dev/null @@ -1,246 +0,0 @@ -package org.sunflow.core.gi; - -import java.util.concurrent.locks.ReentrantReadWriteLock; - -import org.sunflow.PluginRegistry; -import org.sunflow.core.GIEngine; -import org.sunflow.core.GlobalPhotonMapInterface; -import org.sunflow.core.Options; -import org.sunflow.core.Ray; -import org.sunflow.core.Scene; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.MathUtils; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class IrradianceCacheGIEngine implements GIEngine { - private int samples; - private float tolerance; - private float invTolerance; - private float minSpacing; - private float maxSpacing; - private Node root; - private ReentrantReadWriteLock rwl; - private GlobalPhotonMapInterface globalPhotonMap; - - public boolean init(Options options, Scene scene) { - // get settings - samples = options.getInt("gi.irr-cache.samples", 256); - tolerance = options.getFloat("gi.irr-cache.tolerance", 0.05f); - invTolerance = 1.0f / tolerance; - minSpacing = options.getFloat("gi.irr-cache.min_spacing", 0.05f); - maxSpacing = options.getFloat("gi.irr-cache.max_spacing", 5.00f); - root = null; - rwl = new ReentrantReadWriteLock(); - globalPhotonMap = PluginRegistry.globalPhotonMapPlugins.createObject(options.getString("gi.irr-cache.gmap", null)); - // check settings - samples = Math.max(0, samples); - minSpacing = Math.max(0.001f, minSpacing); - maxSpacing = Math.max(0.001f, maxSpacing); - // display settings - UI.printInfo(Module.LIGHT, "Irradiance cache settings:"); - UI.printInfo(Module.LIGHT, " * Samples: %d", samples); - if (tolerance <= 0) - UI.printInfo(Module.LIGHT, " * Tolerance: off"); - else - UI.printInfo(Module.LIGHT, " * Tolerance: %.3f", tolerance); - UI.printInfo(Module.LIGHT, " * Spacing: %.3f to %.3f", minSpacing, maxSpacing); - // prepare root node - Vector3 ext = scene.getBounds().getExtents(); - root = new Node(scene.getBounds().getCenter(), 1.0001f * MathUtils.max(ext.x, ext.y, ext.z)); - // init global photon map - return (globalPhotonMap != null) ? scene.calculatePhotons(globalPhotonMap, "global", 0, options) : true; - } - - public Color getGlobalRadiance(ShadingState state) { - if (globalPhotonMap == null) { - if (state.getShader() != null) - return state.getShader().getRadiance(state); - else - return Color.BLACK; - } else - return globalPhotonMap.getRadiance(state.getPoint(), state.getNormal()); - } - - public Color getIrradiance(ShadingState state, Color diffuseReflectance) { - if (samples <= 0) - return Color.BLACK; - if (state.getDiffuseDepth() > 0) { - // do simple path tracing for additional bounces (single ray) - float xi = (float) state.getRandom(0, 0, 1); - float xj = (float) state.getRandom(0, 1, 1); - float phi = (float) (xi * 2 * Math.PI); - float cosPhi = (float) Math.cos(phi); - float sinPhi = (float) Math.sin(phi); - float sinTheta = (float) Math.sqrt(xj); - float cosTheta = (float) Math.sqrt(1.0f - xj); - Vector3 w = new Vector3(); - w.x = cosPhi * sinTheta; - w.y = sinPhi * sinTheta; - w.z = cosTheta; - OrthoNormalBasis onb = state.getBasis(); - onb.transform(w); - Ray r = new Ray(state.getPoint(), w); - ShadingState temp = state.traceFinalGather(r, 0); - return temp != null ? getGlobalRadiance(temp).copy().mul((float) Math.PI) : Color.BLACK; - } - rwl.readLock().lock(); - Color irr = getIrradiance(state.getPoint(), state.getNormal()); - rwl.readLock().unlock(); - if (irr == null) { - // compute new sample - irr = Color.black(); - OrthoNormalBasis onb = state.getBasis(); - float invR = 0; - float minR = Float.POSITIVE_INFINITY; - Vector3 w = new Vector3(); - for (int i = 0; i < samples; i++) { - float xi = (float) state.getRandom(i, 0, samples); - float xj = (float) state.getRandom(i, 1, samples); - float phi = (float) (xi * 2 * Math.PI); - float cosPhi = (float) Math.cos(phi); - float sinPhi = (float) Math.sin(phi); - float sinTheta = (float) Math.sqrt(xj); - float cosTheta = (float) Math.sqrt(1.0f - xj); - w.x = cosPhi * sinTheta; - w.y = sinPhi * sinTheta; - w.z = cosTheta; - onb.transform(w); - Ray r = new Ray(state.getPoint(), w); - ShadingState temp = state.traceFinalGather(r, i); - if (temp != null) { - minR = Math.min(r.getMax(), minR); - invR += 1.0f / r.getMax(); - temp.getInstance().prepareShadingState(temp); - irr.add(getGlobalRadiance(temp)); - } - } - irr.mul((float) Math.PI / samples); - invR = samples / invR; - rwl.writeLock().lock(); - insert(state.getPoint(), state.getNormal(), invR, irr); - rwl.writeLock().unlock(); - // view irr-cache points - // irr = Color.YELLOW.copy().mul(1e6f); - } - return irr; - } - - private void insert(Point3 p, Vector3 n, float r0, Color irr) { - if (tolerance <= 0) - return; - Node node = root; - r0 = MathUtils.clamp(r0 * tolerance, minSpacing, maxSpacing) * invTolerance; - if (root.isInside(p)) { - while (node.sideLength >= (4.0 * r0 * tolerance)) { - int k = 0; - k |= (p.x > node.center.x) ? 1 : 0; - k |= (p.y > node.center.y) ? 2 : 0; - k |= (p.z > node.center.z) ? 4 : 0; - if (node.children[k] == null) { - Point3 c = new Point3(node.center); - c.x += ((k & 1) == 0) ? -node.quadSideLength : node.quadSideLength; - c.y += ((k & 2) == 0) ? -node.quadSideLength : node.quadSideLength; - c.z += ((k & 4) == 0) ? -node.quadSideLength : node.quadSideLength; - node.children[k] = new Node(c, node.halfSideLength); - } - node = node.children[k]; - } - } - Sample s = new Sample(p, n, r0, irr); - s.next = node.first; - node.first = s; - } - - private Color getIrradiance(Point3 p, Vector3 n) { - if (tolerance <= 0) - return null; - Sample x = new Sample(p, n); - float w = root.find(x); - return (x.irr == null) ? null : x.irr.mul(1.0f / w); - } - - private final class Node { - Node[] children; - Sample first; - Point3 center; - float sideLength; - float halfSideLength; - float quadSideLength; - - Node(Point3 center, float sideLength) { - children = new Node[8]; - for (int i = 0; i < 8; i++) - children[i] = null; - this.center = new Point3(center); - this.sideLength = sideLength; - halfSideLength = 0.5f * sideLength; - quadSideLength = 0.5f * halfSideLength; - first = null; - } - - final boolean isInside(Point3 p) { - return (Math.abs(p.x - center.x) < halfSideLength) && (Math.abs(p.y - center.y) < halfSideLength) && (Math.abs(p.z - center.z) < halfSideLength); - } - - final float find(Sample x) { - float weight = 0; - for (Sample s = first; s != null; s = s.next) { - float c2 = 1.0f - (x.nix * s.nix + x.niy * s.niy + x.niz * s.niz); - float d2 = (x.pix - s.pix) * (x.pix - s.pix) + (x.piy - s.piy) * (x.piy - s.piy) + (x.piz - s.piz) * (x.piz - s.piz); - if (c2 > tolerance * tolerance || d2 > maxSpacing * maxSpacing) - continue; - float invWi = (float) (Math.sqrt(d2) * s.invR0 + Math.sqrt(Math.max(c2, 0))); - if (invWi < tolerance || d2 < minSpacing * minSpacing) { - float wi = Math.min(1e10f, 1.0f / invWi); - if (x.irr != null) - x.irr.madd(wi, s.irr); - else - x.irr = s.irr.copy().mul(wi); - weight += wi; - } - } - for (int i = 0; i < 8; i++) - if ((children[i] != null) && (Math.abs(children[i].center.x - x.pix) <= halfSideLength) && (Math.abs(children[i].center.y - x.piy) <= halfSideLength) && (Math.abs(children[i].center.z - x.piz) <= halfSideLength)) - weight += children[i].find(x); - return weight; - } - } - - private static final class Sample { - float pix, piy, piz; - float nix, niy, niz; - float invR0; - Color irr; - Sample next; - - Sample(Point3 p, Vector3 n) { - pix = p.x; - piy = p.y; - piz = p.z; - Vector3 ni = new Vector3(n).normalize(); - nix = ni.x; - niy = ni.y; - niz = ni.z; - irr = null; - next = null; - } - - Sample(Point3 p, Vector3 n, float r0, Color irr) { - pix = p.x; - piy = p.y; - piz = p.z; - Vector3 ni = new Vector3(n).normalize(); - nix = ni.x; - niy = ni.y; - niz = ni.z; - invR0 = 1.0f / r0; - this.irr = irr; - next = null; - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/gi/PathTracingGIEngine.java b/src/org/sunflow/core/gi/PathTracingGIEngine.java deleted file mode 100644 index 09b499f..0000000 --- a/src/org/sunflow/core/gi/PathTracingGIEngine.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.sunflow.core.gi; - -import org.sunflow.core.GIEngine; -import org.sunflow.core.Options; -import org.sunflow.core.Ray; -import org.sunflow.core.Scene; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Vector3; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class PathTracingGIEngine implements GIEngine { - private int samples; - - public boolean init(Options options, Scene scene) { - samples = options.getInt("gi.path.samples", 16); - samples = Math.max(0, samples); - UI.printInfo(Module.LIGHT, "Path tracer settings:"); - UI.printInfo(Module.LIGHT, " * Samples: %d", samples); - return true; - } - - public Color getIrradiance(ShadingState state, Color diffuseReflectance) { - if (samples <= 0) - return Color.BLACK; - // compute new sample - Color irr = Color.black(); - OrthoNormalBasis onb = state.getBasis(); - Vector3 w = new Vector3(); - int n = state.getDiffuseDepth() == 0 ? samples : 1; - for (int i = 0; i < n; i++) { - float xi = (float) state.getRandom(i, 0, n); - float xj = (float) state.getRandom(i, 1, n); - float phi = (float) (xi * 2 * Math.PI); - float cosPhi = (float) Math.cos(phi); - float sinPhi = (float) Math.sin(phi); - float sinTheta = (float) Math.sqrt(xj); - float cosTheta = (float) Math.sqrt(1.0f - xj); - w.x = cosPhi * sinTheta; - w.y = sinPhi * sinTheta; - w.z = cosTheta; - onb.transform(w); - ShadingState temp = state.traceFinalGather(new Ray(state.getPoint(), w), i); - if (temp != null) { - temp.getInstance().prepareShadingState(temp); - if (temp.getShader() != null) - irr.add(temp.getShader().getRadiance(temp)); - } - } - irr.mul((float) Math.PI / n); - return irr; - } - - public Color getGlobalRadiance(ShadingState state) { - return Color.BLACK; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/light/DirectionalSpotlight.java b/src/org/sunflow/core/light/DirectionalSpotlight.java deleted file mode 100644 index d95a38f..0000000 --- a/src/org/sunflow/core/light/DirectionalSpotlight.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.sunflow.core.light; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.LightSample; -import org.sunflow.core.LightSource; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; - -public class DirectionalSpotlight implements LightSource { - private Point3 src; - private Vector3 dir; - private OrthoNormalBasis basis; - private float r, r2; - private Color radiance; - - public DirectionalSpotlight() { - src = new Point3(0, 0, 0); - dir = new Vector3(0, 0, -1); - dir.normalize(); - basis = OrthoNormalBasis.makeFromW(dir); - r = 1; - r2 = r * r; - radiance = Color.WHITE; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - src = pl.getPoint("source", src); - dir = pl.getVector("dir", dir); - dir.normalize(); - r = pl.getFloat("radius", r); - basis = OrthoNormalBasis.makeFromW(dir); - r2 = r * r; - radiance = pl.getColor("radiance", radiance); - return true; - } - - public int getNumSamples() { - return 1; - } - - public int getLowSamples() { - return 1; - } - - public void getSamples(ShadingState state) { - if (Vector3.dot(dir, state.getGeoNormal()) < 0 && Vector3.dot(dir, state.getNormal()) < 0) { - // project point onto source plane - float x = state.getPoint().x - src.x; - float y = state.getPoint().y - src.y; - float z = state.getPoint().z - src.z; - float t = ((x * dir.x) + (y * dir.y) + (z * dir.z)); - if (t >= 0.0) { - x -= (t * dir.x); - y -= (t * dir.y); - z -= (t * dir.z); - if (((x * x) + (y * y) + (z * z)) <= r2) { - Point3 p = new Point3(); - p.x = src.x + x; - p.y = src.y + y; - p.z = src.z + z; - LightSample dest = new LightSample(); - dest.setShadowRay(new Ray(state.getPoint(), p)); - dest.setRadiance(radiance, radiance); - dest.traceShadow(state); - state.addSample(dest); - } - } - } - } - - public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power) { - float phi = (float) (2 * Math.PI * randX1); - float s = (float) Math.sqrt(1.0f - randY1); - dir.x = r * (float) Math.cos(phi) * s; - dir.y = r * (float) Math.sin(phi) * s; - dir.z = 0; - basis.transform(dir); - Point3.add(src, dir, p); - dir.set(this.dir); - power.set(radiance).mul((float) Math.PI * r2); - } - - public float getPower() { - return radiance.copy().mul((float) Math.PI * r2).getLuminance(); - } - - public Instance createInstance() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/light/ImageBasedLight.java b/src/org/sunflow/core/light/ImageBasedLight.java deleted file mode 100644 index 8d3b465..0000000 --- a/src/org/sunflow/core/light/ImageBasedLight.java +++ /dev/null @@ -1,275 +0,0 @@ -package org.sunflow.core.light; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.LightSample; -import org.sunflow.core.LightSource; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.core.Texture; -import org.sunflow.core.TextureCache; -import org.sunflow.image.Bitmap; -import org.sunflow.image.Color; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.QMC; -import org.sunflow.math.Vector3; - -public class ImageBasedLight implements PrimitiveList, LightSource, Shader { - private Texture texture; - private OrthoNormalBasis basis; - private int numSamples; - private int numLowSamples; - private float jacobian; - private float[] colHistogram; - private float[][] imageHistogram; - private Vector3[] samples; - private Vector3[] lowSamples; - private Color[] colors; - private Color[] lowColors; - - public ImageBasedLight() { - texture = null; - updateBasis(new Vector3(0, 0, -1), new Vector3(0, 1, 0)); - numSamples = 64; - numLowSamples = 8; - } - - private void updateBasis(Vector3 center, Vector3 up) { - if (center != null && up != null) { - basis = OrthoNormalBasis.makeFromWV(center, up); - basis.swapWU(); - basis.flipV(); - } - } - - public boolean update(ParameterList pl, SunflowAPI api) { - updateBasis(pl.getVector("center", null), pl.getVector("up", null)); - numSamples = pl.getInt("samples", numSamples); - numLowSamples = pl.getInt("lowsamples", numLowSamples); - String filename = pl.getString("texture", null); - if (filename != null) - texture = TextureCache.getTexture(api.resolveTextureFilename(filename), false); - - // no texture provided - if (texture == null) - return false; - Bitmap b = texture.getBitmap(); - if (b == null) - return false; - - // rebuild histograms if this is a new texture - if (filename != null) { - imageHistogram = new float[b.getWidth()][b.getHeight()]; - colHistogram = new float[b.getWidth()]; - float du = 1.0f / b.getWidth(); - float dv = 1.0f / b.getHeight(); - for (int x = 0; x < b.getWidth(); x++) { - for (int y = 0; y < b.getHeight(); y++) { - float u = (x + 0.5f) * du; - float v = (y + 0.5f) * dv; - Color c = texture.getPixel(u, v); - imageHistogram[x][y] = c.getLuminance() * (float) Math.sin(Math.PI * v); - if (y > 0) - imageHistogram[x][y] += imageHistogram[x][y - 1]; - } - colHistogram[x] = imageHistogram[x][b.getHeight() - 1]; - if (x > 0) - colHistogram[x] += colHistogram[x - 1]; - for (int y = 0; y < b.getHeight(); y++) - imageHistogram[x][y] /= imageHistogram[x][b.getHeight() - 1]; - } - for (int x = 0; x < b.getWidth(); x++) - colHistogram[x] /= colHistogram[b.getWidth() - 1]; - jacobian = (float) (2 * Math.PI * Math.PI) / (b.getWidth() * b.getHeight()); - } - // take fixed samples - if (pl.getBoolean("fixed", samples != null)) { - // high density samples - samples = new Vector3[numSamples]; - colors = new Color[numSamples]; - generateFixedSamples(samples, colors); - // low density samples - lowSamples = new Vector3[numLowSamples]; - lowColors = new Color[numLowSamples]; - generateFixedSamples(lowSamples, lowColors); - } else { - // turn off - samples = lowSamples = null; - colors = lowColors = null; - } - return true; - } - - private void generateFixedSamples(Vector3[] samples, Color[] colors) { - for (int i = 0; i < samples.length; i++) { - double randX = (double) i / (double) samples.length; - double randY = QMC.halton(0, i); - int x = 0; - while (randX >= colHistogram[x] && x < colHistogram.length - 1) - x++; - float[] rowHistogram = imageHistogram[x]; - int y = 0; - while (randY >= rowHistogram[y] && y < rowHistogram.length - 1) - y++; - // sample from (x, y) - float u = (float) ((x == 0) ? (randX / colHistogram[0]) : ((randX - colHistogram[x - 1]) / (colHistogram[x] - colHistogram[x - 1]))); - float v = (float) ((y == 0) ? (randY / rowHistogram[0]) : ((randY - rowHistogram[y - 1]) / (rowHistogram[y] - rowHistogram[y - 1]))); - - float px = ((x == 0) ? colHistogram[0] : (colHistogram[x] - colHistogram[x - 1])); - float py = ((y == 0) ? rowHistogram[0] : (rowHistogram[y] - rowHistogram[y - 1])); - - float su = (x + u) / colHistogram.length; - float sv = (y + v) / rowHistogram.length; - - float invP = (float) Math.sin(sv * Math.PI) * jacobian / (numSamples * px * py); - samples[i] = getDirection(su, sv); - basis.transform(samples[i]); - colors[i] = texture.getPixel(su, sv).mul(invP); - } - } - - public void prepareShadingState(ShadingState state) { - if (state.includeLights()) - state.setShader(this); - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - if (r.getMax() == Float.POSITIVE_INFINITY) - state.setIntersection(0); - } - - public int getNumPrimitives() { - return 1; - } - - public float getPrimitiveBound(int primID, int i) { - return 0; - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - return null; - } - - public PrimitiveList getBakingPrimitives() { - return null; - } - - public int getNumSamples() { - return numSamples; - } - - public void getSamples(ShadingState state) { - if (samples == null) { - int n = state.getDiffuseDepth() > 0 ? 1 : numSamples; - for (int i = 0; i < n; i++) { - // random offset on unit square, we use the infinite version of - // getRandom because the light sampling is adaptive - double randX = state.getRandom(i, 0, n); - double randY = state.getRandom(i, 1, n); - int x = 0; - while (randX >= colHistogram[x] && x < colHistogram.length - 1) - x++; - float[] rowHistogram = imageHistogram[x]; - int y = 0; - while (randY >= rowHistogram[y] && y < rowHistogram.length - 1) - y++; - // sample from (x, y) - float u = (float) ((x == 0) ? (randX / colHistogram[0]) : ((randX - colHistogram[x - 1]) / (colHistogram[x] - colHistogram[x - 1]))); - float v = (float) ((y == 0) ? (randY / rowHistogram[0]) : ((randY - rowHistogram[y - 1]) / (rowHistogram[y] - rowHistogram[y - 1]))); - - float px = ((x == 0) ? colHistogram[0] : (colHistogram[x] - colHistogram[x - 1])); - float py = ((y == 0) ? rowHistogram[0] : (rowHistogram[y] - rowHistogram[y - 1])); - - float su = (x + u) / colHistogram.length; - float sv = (y + v) / rowHistogram.length; - float invP = (float) Math.sin(sv * Math.PI) * jacobian / (n * px * py); - Vector3 dir = getDirection(su, sv); - basis.transform(dir); - if (Vector3.dot(dir, state.getGeoNormal()) > 0) { - LightSample dest = new LightSample(); - dest.setShadowRay(new Ray(state.getPoint(), dir)); - dest.getShadowRay().setMax(Float.MAX_VALUE); - Color radiance = texture.getPixel(su, sv); - dest.setRadiance(radiance, radiance); - dest.getDiffuseRadiance().mul(invP); - dest.getSpecularRadiance().mul(invP); - dest.traceShadow(state); - state.addSample(dest); - } - } - } else { - if (state.getDiffuseDepth() > 0) { - for (int i = 0; i < numLowSamples; i++) { - if (Vector3.dot(lowSamples[i], state.getGeoNormal()) > 0 && Vector3.dot(lowSamples[i], state.getNormal()) > 0) { - LightSample dest = new LightSample(); - dest.setShadowRay(new Ray(state.getPoint(), lowSamples[i])); - dest.getShadowRay().setMax(Float.MAX_VALUE); - dest.setRadiance(lowColors[i], lowColors[i]); - dest.traceShadow(state); - state.addSample(dest); - } - } - } else { - for (int i = 0; i < numSamples; i++) { - if (Vector3.dot(samples[i], state.getGeoNormal()) > 0 && Vector3.dot(samples[i], state.getNormal()) > 0) { - LightSample dest = new LightSample(); - dest.setShadowRay(new Ray(state.getPoint(), samples[i])); - dest.getShadowRay().setMax(Float.MAX_VALUE); - dest.setRadiance(colors[i], colors[i]); - dest.traceShadow(state); - state.addSample(dest); - } - } - } - } - } - - public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power) { - } - - public Color getRadiance(ShadingState state) { - // lookup texture based on ray direction - return state.includeLights() ? getColor(basis.untransform(state.getRay().getDirection(), new Vector3())) : Color.BLACK; - } - - private Color getColor(Vector3 dir) { - float u, v; - // assume lon/lat format - double phi = 0, theta = 0; - phi = Math.acos(dir.y); - theta = Math.atan2(dir.z, dir.x); - u = (float) (0.5 - 0.5 * theta / Math.PI); - v = (float) (phi / Math.PI); - return texture.getPixel(u, v); - } - - private Vector3 getDirection(float u, float v) { - Vector3 dest = new Vector3(); - double phi = 0, theta = 0; - theta = u * 2 * Math.PI; - phi = v * Math.PI; - double sin_phi = Math.sin(phi); - dest.x = (float) (-sin_phi * Math.cos(theta)); - dest.y = (float) Math.cos(phi); - dest.z = (float) (sin_phi * Math.sin(theta)); - return dest; - } - - public void scatterPhoton(ShadingState state, Color power) { - } - - public float getPower() { - return 0; - } - - public Instance createInstance() { - return Instance.createTemporary(this, null, this); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/light/PointLight.java b/src/org/sunflow/core/light/PointLight.java deleted file mode 100644 index 86e5955..0000000 --- a/src/org/sunflow/core/light/PointLight.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.sunflow.core.light; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.LightSample; -import org.sunflow.core.LightSource; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; - -public class PointLight implements LightSource { - private Point3 lightPoint; - private Color power; - - public PointLight() { - lightPoint = new Point3(0, 0, 0); - power = Color.WHITE; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - lightPoint = pl.getPoint("center", lightPoint); - power = pl.getColor("power", power); - return true; - } - - public int getNumSamples() { - return 1; - } - - public void getSamples(ShadingState state) { - Vector3 d = Point3.sub(lightPoint, state.getPoint(), new Vector3()); - if (Vector3.dot(d, state.getNormal()) > 0 && Vector3.dot(d, state.getGeoNormal()) > 0) { - LightSample dest = new LightSample(); - // prepare shadow ray - dest.setShadowRay(new Ray(state.getPoint(), lightPoint)); - float scale = 1.0f / (float) (4 * Math.PI * lightPoint.distanceToSquared(state.getPoint())); - dest.setRadiance(power, power); - dest.getDiffuseRadiance().mul(scale); - dest.getSpecularRadiance().mul(scale); - dest.traceShadow(state); - state.addSample(dest); - } - } - - public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power) { - p.set(lightPoint); - float phi = (float) (2 * Math.PI * randX1); - float s = (float) Math.sqrt(randY1 * (1.0f - randY1)); - dir.x = (float) Math.cos(phi) * s; - dir.y = (float) Math.sin(phi) * s; - dir.z = (float) (1 - 2 * randY1); - power.set(this.power); - } - - public float getPower() { - return power.getLuminance(); - } - - public Instance createInstance() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/light/SphereLight.java b/src/org/sunflow/core/light/SphereLight.java deleted file mode 100644 index d589a02..0000000 --- a/src/org/sunflow/core/light/SphereLight.java +++ /dev/null @@ -1,153 +0,0 @@ -package org.sunflow.core.light; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.LightSample; -import org.sunflow.core.LightSource; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.core.primitive.Sphere; -import org.sunflow.image.Color; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Solvers; -import org.sunflow.math.Vector3; - -public class SphereLight implements LightSource, Shader { - private Color radiance; - private int numSamples; - private Point3 center; - private float radius; - private float r2; - - public SphereLight() { - radiance = Color.WHITE; - numSamples = 4; - center = new Point3(); - radius = r2 = 1; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - radiance = pl.getColor("radiance", radiance); - numSamples = pl.getInt("samples", numSamples); - radius = pl.getFloat("radius", radius); - r2 = radius * radius; - center = pl.getPoint("center", center); - return true; - } - - public int getNumSamples() { - return numSamples; - } - - public int getLowSamples() { - return 1; - } - - public boolean isVisible(ShadingState state) { - return state.getPoint().distanceToSquared(center) > r2; - } - - public void getSamples(ShadingState state) { - if (getNumSamples() <= 0) - return; - Vector3 wc = Point3.sub(center, state.getPoint(), new Vector3()); - float l2 = wc.lengthSquared(); - if (l2 <= r2) - return; // inside the sphere? - // top of the sphere as viewed from the current shading point - float topX = wc.x + state.getNormal().x * radius; - float topY = wc.y + state.getNormal().y * radius; - float topZ = wc.z + state.getNormal().z * radius; - if (state.getNormal().dot(topX, topY, topZ) <= 0) - return; // top of the sphere is below the horizon - float cosThetaMax = (float) Math.sqrt(Math.max(0, 1 - r2 / Vector3.dot(wc, wc))); - OrthoNormalBasis basis = OrthoNormalBasis.makeFromW(wc); - int samples = state.getDiffuseDepth() > 0 ? 1 : getNumSamples(); - float scale = (float) (2 * Math.PI * (1 - cosThetaMax)); - Color c = Color.mul(scale / samples, radiance); - for (int i = 0; i < samples; i++) { - // random offset on unit square - double randX = state.getRandom(i, 0, samples); - double randY = state.getRandom(i, 1, samples); - - // cone sampling - double cosTheta = (1 - randX) * cosThetaMax + randX; - double sinTheta = Math.sqrt(1 - cosTheta * cosTheta); - double phi = randY * 2 * Math.PI; - Vector3 dir = new Vector3((float) (Math.cos(phi) * sinTheta), (float) (Math.sin(phi) * sinTheta), (float) cosTheta); - basis.transform(dir); - - // check that the direction of the sample is the same as the - // normal - float cosNx = Vector3.dot(dir, state.getNormal()); - if (cosNx <= 0) - continue; - - float ocx = state.getPoint().x - center.x; - float ocy = state.getPoint().y - center.y; - float ocz = state.getPoint().z - center.z; - float qa = Vector3.dot(dir, dir); - float qb = 2 * ((dir.x * ocx) + (dir.y * ocy) + (dir.z * ocz)); - float qc = ((ocx * ocx) + (ocy * ocy) + (ocz * ocz)) - r2; - double[] t = Solvers.solveQuadric(qa, qb, qc); - if (t == null) - continue; - LightSample dest = new LightSample(); - // compute shadow ray to the sampled point - dest.setShadowRay(new Ray(state.getPoint(), dir)); - // FIXME: arbitrary bias, should handle as in other places - dest.getShadowRay().setMax((float) t[0] - 1e-3f); - // prepare sample - dest.setRadiance(c, c); - dest.traceShadow(state); - state.addSample(dest); - } - } - - public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power) { - float z = (float) (1 - 2 * randX2); - float r = (float) Math.sqrt(Math.max(0, 1 - z * z)); - float phi = (float) (2 * Math.PI * randY2); - float x = r * (float) Math.cos(phi); - float y = r * (float) Math.sin(phi); - p.x = center.x + x * radius; - p.y = center.y + y * radius; - p.z = center.z + z * radius; - OrthoNormalBasis basis = OrthoNormalBasis.makeFromW(new Vector3(x, y, z)); - phi = (float) (2 * Math.PI * randX1); - float cosPhi = (float) Math.cos(phi); - float sinPhi = (float) Math.sin(phi); - float sinTheta = (float) Math.sqrt(randY1); - float cosTheta = (float) Math.sqrt(1 - randY1); - dir.x = cosPhi * sinTheta; - dir.y = sinPhi * sinTheta; - dir.z = cosTheta; - basis.transform(dir); - power.set(radiance); - power.mul((float) (Math.PI * Math.PI * 4 * r2)); - } - - public float getPower() { - return radiance.copy().mul((float) (Math.PI * Math.PI * 4 * r2)).getLuminance(); - } - - public Color getRadiance(ShadingState state) { - if (!state.includeLights()) - return Color.BLACK; - state.faceforward(); - // emit constant radiance - return state.isBehind() ? Color.BLACK : radiance; - } - - public void scatterPhoton(ShadingState state, Color power) { - // do not scatter photons - } - - public Instance createInstance() { - return Instance.createTemporary(new Sphere(), Matrix4.translation(center.x, center.y, center.z).multiply(Matrix4.scale(radius)), this); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/light/SunSkyLight.java b/src/org/sunflow/core/light/SunSkyLight.java deleted file mode 100644 index 8b9c794..0000000 --- a/src/org/sunflow/core/light/SunSkyLight.java +++ /dev/null @@ -1,337 +0,0 @@ -package org.sunflow.core.light; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.LightSample; -import org.sunflow.core.LightSource; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.ChromaticitySpectrum; -import org.sunflow.image.Color; -import org.sunflow.image.ConstantSpectralCurve; -import org.sunflow.image.IrregularSpectralCurve; -import org.sunflow.image.RGBSpace; -import org.sunflow.image.RegularSpectralCurve; -import org.sunflow.image.SpectralCurve; -import org.sunflow.image.XYZColor; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.MathUtils; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; - -public class SunSkyLight implements LightSource, PrimitiveList, Shader { - // sunflow parameters - private int numSkySamples; - private OrthoNormalBasis basis; - private boolean groundExtendSky; - private Color groundColor; - // parameters to the model - private Vector3 sunDirWorld; - private float turbidity; - // derived quantities - private Vector3 sunDir; - private SpectralCurve sunSpectralRadiance; - private Color sunColor; - private float sunTheta; - private double zenithY, zenithx, zenithy; - private final double[] perezY = new double[5]; - private final double[] perezx = new double[5]; - private final double[] perezy = new double[5]; - private float jacobian; - private float[] colHistogram; - private float[][] imageHistogram; - // constant data - private static final float[] solAmplitudes = { 165.5f, 162.3f, 211.2f, - 258.8f, 258.2f, 242.3f, 267.6f, 296.6f, 305.4f, 300.6f, 306.6f, - 288.3f, 287.1f, 278.2f, 271.0f, 272.3f, 263.6f, 255.0f, 250.6f, - 253.1f, 253.5f, 251.3f, 246.3f, 241.7f, 236.8f, 232.1f, 228.2f, - 223.4f, 219.7f, 215.3f, 211.0f, 207.3f, 202.4f, 198.7f, 194.3f, - 190.7f, 186.3f, 182.6f }; - private static final RegularSpectralCurve solCurve = new RegularSpectralCurve(solAmplitudes, 380, 750); - private static final float[] k_oWavelengths = { 300, 305, 310, 315, 320, - 325, 330, 335, 340, 345, 350, 355, 445, 450, 455, 460, 465, 470, - 475, 480, 485, 490, 495, 500, 505, 510, 515, 520, 525, 530, 535, - 540, 545, 550, 555, 560, 565, 570, 575, 580, 585, 590, 595, 600, - 605, 610, 620, 630, 640, 650, 660, 670, 680, 690, 700, 710, 720, - 730, 740, 750, 760, 770, 780, 790, }; - private static final float[] k_oAmplitudes = { 10.0f, 4.8f, 2.7f, 1.35f, - .8f, .380f, .160f, .075f, .04f, .019f, .007f, .0f, .003f, .003f, - .004f, .006f, .008f, .009f, .012f, .014f, .017f, .021f, .025f, - .03f, .035f, .04f, .045f, .048f, .057f, .063f, .07f, .075f, .08f, - .085f, .095f, .103f, .110f, .12f, .122f, .12f, .118f, .115f, .12f, - .125f, .130f, .12f, .105f, .09f, .079f, .067f, .057f, .048f, .036f, - .028f, .023f, .018f, .014f, .011f, .010f, .009f, .007f, .004f, .0f, - .0f }; - private static final float[] k_gWavelengths = { 759, 760, 770, 771 }; - private static final float[] k_gAmplitudes = { 0, 3.0f, 0.210f, 0 }; - private static final float[] k_waWavelengths = { 689, 690, 700, 710, 720, - 730, 740, 750, 760, 770, 780, 790, 800 }; - private static final float[] k_waAmplitudes = { 0f, 0.160e-1f, 0.240e-1f, - 0.125e-1f, 0.100e+1f, 0.870f, 0.610e-1f, 0.100e-2f, 0.100e-4f, - 0.100e-4f, 0.600e-3f, 0.175e-1f, 0.360e-1f }; - - private static final IrregularSpectralCurve k_oCurve = new IrregularSpectralCurve(k_oWavelengths, k_oAmplitudes); - private static final IrregularSpectralCurve k_gCurve = new IrregularSpectralCurve(k_gWavelengths, k_gAmplitudes); - private static final IrregularSpectralCurve k_waCurve = new IrregularSpectralCurve(k_waWavelengths, k_waAmplitudes); - - public SunSkyLight() { - numSkySamples = 64; - sunDirWorld = new Vector3(1, 1, 1); - turbidity = 6; - basis = OrthoNormalBasis.makeFromWV(new Vector3(0, 0, 1), new Vector3(0, 1, 0)); - groundExtendSky = false; - groundColor = Color.BLACK; - initSunSky(); - } - - private SpectralCurve computeAttenuatedSunlight(float theta, float turbidity) { - float[] data = new float[91]; // holds the sunsky curve data - final double alpha = 1.3; - final double lozone = 0.35; - final double w = 2.0; - double beta = 0.04608365822050 * turbidity - 0.04586025928522; - // Relative optical mass - double m = 1.0 / (Math.cos(theta) + 0.000940 * Math.pow(1.6386 - theta, -1.253)); - for (int i = 0, lambda = 350; lambda <= 800; i++, lambda += 5) { - // Rayleigh scattering - double tauR = Math.exp(-m * 0.008735 * Math.pow(lambda / 1000.0, -4.08)); - // Aerosol (water + dust) attenuation - double tauA = Math.exp(-m * beta * Math.pow(lambda / 1000.0, -alpha)); - // Attenuation due to ozone absorption - double tauO = Math.exp(-m * k_oCurve.sample(lambda) * lozone); - // Attenuation due to mixed gases absorption - double tauG = Math.exp(-1.41 * k_gCurve.sample(lambda) * m / Math.pow(1.0 + 118.93 * k_gCurve.sample(lambda) * m, 0.45)); - // Attenuation due to water vapor absorption - double tauWA = Math.exp(-0.2385 * k_waCurve.sample(lambda) * w * m / Math.pow(1.0 + 20.07 * k_waCurve.sample(lambda) * w * m, 0.45)); - // 100.0 comes from solAmplitudes begin in wrong units. - double amp = /* 100.0 * */solCurve.sample(lambda) * tauR * tauA * tauO * tauG * tauWA; - data[i] = (float) amp; - } - return new RegularSpectralCurve(data, 350, 800); - } - - private double perezFunction(final double[] lam, double theta, double gamma, double lvz) { - double den = ((1.0 + lam[0] * Math.exp(lam[1])) * (1.0 + lam[2] * Math.exp(lam[3] * sunTheta) + lam[4] * Math.cos(sunTheta) * Math.cos(sunTheta))); - double num = ((1.0 + lam[0] * Math.exp(lam[1] / Math.cos(theta))) * (1.0 + lam[2] * Math.exp(lam[3] * gamma) + lam[4] * Math.cos(gamma) * Math.cos(gamma))); - return lvz * num / den; - } - - private void initSunSky() { - // perform all the required initialization of constants - sunDirWorld.normalize(); - sunDir = basis.untransform(sunDirWorld, new Vector3()); - sunDir.normalize(); - sunTheta = (float) Math.acos(MathUtils.clamp(sunDir.z, -1, 1)); - if (sunDir.z > 0) { - sunSpectralRadiance = computeAttenuatedSunlight(sunTheta, turbidity); - // produce color suitable for rendering - sunColor = RGBSpace.SRGB.convertXYZtoRGB(sunSpectralRadiance.toXYZ().mul(1e-4f)).constrainRGB(); - } else { - sunSpectralRadiance = new ConstantSpectralCurve(0); - } - // sunSolidAngle = (float) (0.25 * Math.PI * 1.39 * 1.39 / (150 * 150)); - float theta2 = sunTheta * sunTheta; - float theta3 = sunTheta * theta2; - float T = turbidity; - float T2 = turbidity * turbidity; - double chi = (4.0 / 9.0 - T / 120.0) * (Math.PI - 2.0 * sunTheta); - zenithY = (4.0453 * T - 4.9710) * Math.tan(chi) - 0.2155 * T + 2.4192; - zenithY *= 1000; /* conversion from kcd/m^2 to cd/m^2 */ - zenithx = (0.00165 * theta3 - 0.00374 * theta2 + 0.00208 * sunTheta + 0) * T2 + (-0.02902 * theta3 + 0.06377 * theta2 - 0.03202 * sunTheta + 0.00394) * T + (0.11693 * theta3 - 0.21196 * theta2 + 0.06052 * sunTheta + 0.25885); - zenithy = (0.00275 * theta3 - 0.00610 * theta2 + 0.00316 * sunTheta + 0) * T2 + (-0.04212 * theta3 + 0.08970 * theta2 - 0.04153 * sunTheta + 0.00515) * T + (0.15346 * theta3 - 0.26756 * theta2 + 0.06669 * sunTheta + 0.26688); - - perezY[0] = 0.17872 * T - 1.46303; - perezY[1] = -0.35540 * T + 0.42749; - perezY[2] = -0.02266 * T + 5.32505; - perezY[3] = 0.12064 * T - 2.57705; - perezY[4] = -0.06696 * T + 0.37027; - - perezx[0] = -0.01925 * T - 0.25922; - perezx[1] = -0.06651 * T + 0.00081; - perezx[2] = -0.00041 * T + 0.21247; - perezx[3] = -0.06409 * T - 0.89887; - perezx[4] = -0.00325 * T + 0.04517; - - perezy[0] = -0.01669 * T - 0.26078; - perezy[1] = -0.09495 * T + 0.00921; - perezy[2] = -0.00792 * T + 0.21023; - perezy[3] = -0.04405 * T - 1.65369; - perezy[4] = -0.01092 * T + 0.05291; - - final int w = 32, h = 32; - imageHistogram = new float[w][h]; - colHistogram = new float[w]; - float du = 1.0f / w; - float dv = 1.0f / h; - for (int x = 0; x < w; x++) { - for (int y = 0; y < h; y++) { - float u = (x + 0.5f) * du; - float v = (y + 0.5f) * dv; - Color c = getSkyRGB(getDirection(u, v)); - imageHistogram[x][y] = c.getLuminance() * (float) Math.sin(Math.PI * v); - if (y > 0) - imageHistogram[x][y] += imageHistogram[x][y - 1]; - } - colHistogram[x] = imageHistogram[x][h - 1]; - if (x > 0) - colHistogram[x] += colHistogram[x - 1]; - for (int y = 0; y < h; y++) - imageHistogram[x][y] /= imageHistogram[x][h - 1]; - } - for (int x = 0; x < w; x++) - colHistogram[x] /= colHistogram[w - 1]; - jacobian = (float) (2 * Math.PI * Math.PI) / (w * h); - } - - public boolean update(ParameterList pl, SunflowAPI api) { - Vector3 up = pl.getVector("up", null); - Vector3 east = pl.getVector("east", null); - if (up != null && east != null) - basis = OrthoNormalBasis.makeFromWV(up, east); - else if (up != null) - basis = OrthoNormalBasis.makeFromW(up); - numSkySamples = pl.getInt("samples", numSkySamples); - sunDirWorld = pl.getVector("sundir", sunDirWorld); - turbidity = pl.getFloat("turbidity", turbidity); - groundExtendSky = pl.getBoolean("ground.extendsky", groundExtendSky); - groundColor = pl.getColor("ground.color", groundColor); - // recompute model - initSunSky(); - return true; - } - - private Color getSkyRGB(Vector3 dir) { - if (dir.z < 0 && !groundExtendSky) - return groundColor; - if (dir.z < 0.001f) - dir.z = 0.001f; - dir.normalize(); - double theta = Math.acos(MathUtils.clamp(dir.z, -1, 1)); - double gamma = Math.acos(MathUtils.clamp(Vector3.dot(dir, sunDir), -1, 1)); - double x = perezFunction(perezx, theta, gamma, zenithx); - double y = perezFunction(perezy, theta, gamma, zenithy); - double Y = perezFunction(perezY, theta, gamma, zenithY) * 1e-4; - XYZColor c = ChromaticitySpectrum.get((float) x, (float) y); - // XYZColor c = new ChromaticitySpectrum((float) x, (float) y).toXYZ(); - float X = (float) (c.getX() * Y / c.getY()); - float Z = (float) (c.getZ() * Y / c.getY()); - return RGBSpace.SRGB.convertXYZtoRGB(X, (float) Y, Z); - } - - public int getNumSamples() { - return 1 + numSkySamples; - } - - public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power) { - // FIXME: not implemented - } - - public float getPower() { - return 0; - } - - public void getSamples(ShadingState state) { - if (Vector3.dot(sunDirWorld, state.getGeoNormal()) > 0 && Vector3.dot(sunDirWorld, state.getNormal()) > 0) { - LightSample dest = new LightSample(); - dest.setShadowRay(new Ray(state.getPoint(), sunDirWorld)); - dest.getShadowRay().setMax(Float.MAX_VALUE); - dest.setRadiance(sunColor, sunColor); - dest.traceShadow(state); - state.addSample(dest); - } - int n = state.getDiffuseDepth() > 0 ? 1 : numSkySamples; - for (int i = 0; i < n; i++) { - // random offset on unit square, we use the infinite version of - // getRandom because the light sampling is adaptive - double randX = state.getRandom(i, 0, n); - double randY = state.getRandom(i, 1, n); - - int x = 0; - while (randX >= colHistogram[x] && x < colHistogram.length - 1) - x++; - float[] rowHistogram = imageHistogram[x]; - int y = 0; - while (randY >= rowHistogram[y] && y < rowHistogram.length - 1) - y++; - // sample from (x, y) - float u = (float) ((x == 0) ? (randX / colHistogram[0]) : ((randX - colHistogram[x - 1]) / (colHistogram[x] - colHistogram[x - 1]))); - float v = (float) ((y == 0) ? (randY / rowHistogram[0]) : ((randY - rowHistogram[y - 1]) / (rowHistogram[y] - rowHistogram[y - 1]))); - - float px = ((x == 0) ? colHistogram[0] : (colHistogram[x] - colHistogram[x - 1])); - float py = ((y == 0) ? rowHistogram[0] : (rowHistogram[y] - rowHistogram[y - 1])); - - float su = (x + u) / colHistogram.length; - float sv = (y + v) / rowHistogram.length; - float invP = (float) Math.sin(sv * Math.PI) * jacobian / (n * px * py); - Vector3 localDir = getDirection(su, sv); - Vector3 dir = basis.transform(localDir, new Vector3()); - if (Vector3.dot(dir, state.getGeoNormal()) > 0 && Vector3.dot(dir, state.getNormal()) > 0) { - LightSample dest = new LightSample(); - dest.setShadowRay(new Ray(state.getPoint(), dir)); - dest.getShadowRay().setMax(Float.MAX_VALUE); - Color radiance = getSkyRGB(localDir); - dest.setRadiance(radiance, radiance); - dest.getDiffuseRadiance().mul(invP); - dest.getSpecularRadiance().mul(invP); - dest.traceShadow(state); - state.addSample(dest); - } - } - } - - public PrimitiveList getBakingPrimitives() { - return null; - } - - public int getNumPrimitives() { - return 1; - } - - public float getPrimitiveBound(int primID, int i) { - return 0; - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - return null; - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - if (r.getMax() == Float.POSITIVE_INFINITY) - state.setIntersection(0); - } - - public void prepareShadingState(ShadingState state) { - if (state.includeLights()) - state.setShader(this); - } - - public Color getRadiance(ShadingState state) { - return getSkyRGB(basis.untransform(state.getRay().getDirection())).constrainRGB(); - } - - public void scatterPhoton(ShadingState state, Color power) { - // let photon escape - } - - private Vector3 getDirection(float u, float v) { - Vector3 dest = new Vector3(); - double phi = 0, theta = 0; - theta = u * 2 * Math.PI; - phi = v * Math.PI; - double sin_phi = Math.sin(phi); - dest.x = (float) (-sin_phi * Math.cos(theta)); - dest.y = (float) Math.cos(phi); - dest.z = (float) (sin_phi * Math.sin(theta)); - return dest; - } - - public Instance createInstance() { - return Instance.createTemporary(this, null, this); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/light/TriangleMeshLight.java b/src/org/sunflow/core/light/TriangleMeshLight.java deleted file mode 100644 index 3af8071..0000000 --- a/src/org/sunflow/core/light/TriangleMeshLight.java +++ /dev/null @@ -1,272 +0,0 @@ -package org.sunflow.core.light; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.LightSample; -import org.sunflow.core.LightSource; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.core.primitive.TriangleMesh; -import org.sunflow.image.Color; -import org.sunflow.math.MathUtils; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; - -public class TriangleMeshLight extends TriangleMesh implements Shader, LightSource { - private Color radiance; - private int numSamples; - private float[] areas; - private float totalArea; - private Vector3[] ngs; - - public TriangleMeshLight() { - radiance = Color.WHITE; - numSamples = 4; - } - - @Override - public boolean update(ParameterList pl, SunflowAPI api) { - radiance = pl.getColor("radiance", radiance); - numSamples = pl.getInt("samples", numSamples); - if (super.update(pl, api)) { - // precompute triangle areas and normals - areas = new float[getNumPrimitives()]; - ngs = new Vector3[getNumPrimitives()]; - totalArea = 0; - for (int tri3 = 0, i = 0; tri3 < triangles.length; tri3 += 3, i++) { - int a = triangles[tri3 + 0]; - int b = triangles[tri3 + 1]; - int c = triangles[tri3 + 2]; - Point3 v0p = getPoint(a); - Point3 v1p = getPoint(b); - Point3 v2p = getPoint(c); - ngs[i] = Point3.normal(v0p, v1p, v2p); - areas[i] = 0.5f * ngs[i].length(); - ngs[i].normalize(); - totalArea += areas[i]; - } - } else - return false; - return true; - } - - private final boolean intersectTriangleKensler(int tri3, Ray r) { - int a = 3 * triangles[tri3 + 0]; - int b = 3 * triangles[tri3 + 1]; - int c = 3 * triangles[tri3 + 2]; - float edge0x = points[b + 0] - points[a + 0]; - float edge0y = points[b + 1] - points[a + 1]; - float edge0z = points[b + 2] - points[a + 2]; - float edge1x = points[a + 0] - points[c + 0]; - float edge1y = points[a + 1] - points[c + 1]; - float edge1z = points[a + 2] - points[c + 2]; - float nx = edge0y * edge1z - edge0z * edge1y; - float ny = edge0z * edge1x - edge0x * edge1z; - float nz = edge0x * edge1y - edge0y * edge1x; - float v = r.dot(nx, ny, nz); - float iv = 1 / v; - float edge2x = points[a + 0] - r.ox; - float edge2y = points[a + 1] - r.oy; - float edge2z = points[a + 2] - r.oz; - float va = nx * edge2x + ny * edge2y + nz * edge2z; - float t = iv * va; - if (t <= 0) - return false; - float ix = edge2y * r.dz - edge2z * r.dy; - float iy = edge2z * r.dx - edge2x * r.dz; - float iz = edge2x * r.dy - edge2y * r.dx; - float v1 = ix * edge1x + iy * edge1y + iz * edge1z; - float beta = iv * v1; - if (beta < 0) - return false; - float v2 = ix * edge0x + iy * edge0y + iz * edge0z; - if ((v1 + v2) * v > v * v) - return false; - float gamma = iv * v2; - if (gamma < 0) - return false; - // FIXME: arbitrary bias, should handle as in other places - r.setMax(t - 1e-3f); - return true; - } - - public Color getRadiance(ShadingState state) { - if (!state.includeLights()) - return Color.BLACK; - state.faceforward(); - // emit constant radiance - return state.isBehind() ? Color.BLACK : radiance; - } - - public void scatterPhoton(ShadingState state, Color power) { - // do not scatter photons - } - - public Instance createInstance() { - return Instance.createTemporary(this, null, this); - } - - public int getNumSamples() { - return numSamples * getNumPrimitives(); - } - - public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power) { - double rnd = randX1 * totalArea; - int j = areas.length - 1; - for (int i = 0; i < areas.length; i++) { - if (rnd < areas[i]) { - j = i; - break; - } - rnd -= areas[i]; // try next triangle - } - rnd /= areas[j]; - randX1 = rnd; - double s = Math.sqrt(1 - randX2); - float u = (float) (randY2 * s); - float v = (float) (1 - s); - float w = 1 - u - v; - int tri3 = j * 3; - int index0 = 3 * triangles[tri3 + 0]; - int index1 = 3 * triangles[tri3 + 1]; - int index2 = 3 * triangles[tri3 + 2]; - p.x = w * points[index0 + 0] + u * points[index1 + 0] + v * points[index2 + 0]; - p.y = w * points[index0 + 1] + u * points[index1 + 1] + v * points[index2 + 1]; - p.z = w * points[index0 + 2] + u * points[index1 + 2] + v * points[index2 + 2]; - p.x += 0.001f * ngs[j].x; - p.y += 0.001f * ngs[j].y; - p.z += 0.001f * ngs[j].z; - OrthoNormalBasis onb = OrthoNormalBasis.makeFromW(ngs[j]); - u = (float) (2 * Math.PI * randX1); - s = Math.sqrt(randY1); - onb.transform(new Vector3((float) (Math.cos(u) * s), (float) (Math.sin(u) * s), (float) (Math.sqrt(1 - randY1))), dir); - Color.mul((float) Math.PI * areas[j], radiance, power); - } - - public float getPower() { - return radiance.copy().mul((float) Math.PI * totalArea).getLuminance(); - } - - public void getSamples(ShadingState state) { - if (numSamples == 0) - return; - Vector3 n = state.getNormal(); - Point3 p = state.getPoint(); - for (int tri3 = 0, i = 0; tri3 < triangles.length; tri3 += 3, i++) { - // vector towards each vertex of the light source - Vector3 p0 = Point3.sub(getPoint(triangles[tri3 + 0]), p, new Vector3()); - // cull triangle if it is facing the wrong way - if (Vector3.dot(p0, ngs[i]) >= 0) - continue; - Vector3 p1 = Point3.sub(getPoint(triangles[tri3 + 1]), p, new Vector3()); - Vector3 p2 = Point3.sub(getPoint(triangles[tri3 + 2]), p, new Vector3()); - // if all three vertices are below the hemisphere, stop - if (Vector3.dot(p0, n) <= 0 && Vector3.dot(p1, n) <= 0 && Vector3.dot(p2, n) <= 0) - continue; - p0.normalize(); - p1.normalize(); - p2.normalize(); - float dot = Vector3.dot(p2, p0); - Vector3 h = new Vector3(); - h.x = p2.x - dot * p0.x; - h.y = p2.y - dot * p0.y; - h.z = p2.z - dot * p0.z; - float hlen = h.length(); - if (hlen > 1e-6f) - h.div(hlen); - else - continue; - Vector3 n0 = Vector3.cross(p0, p1, new Vector3()); - float len0 = n0.length(); - if (len0 > 1e-6f) - n0.div(len0); - else - continue; - Vector3 n1 = Vector3.cross(p1, p2, new Vector3()); - float len1 = n1.length(); - if (len1 > 1e-6f) - n1.div(len1); - else - continue; - Vector3 n2 = Vector3.cross(p2, p0, new Vector3()); - float len2 = n2.length(); - if (len2 > 1e-6f) - n2.div(len2); - else - continue; - - float cosAlpha = MathUtils.clamp(-Vector3.dot(n2, n0), -1.0f, 1.0f); - float cosBeta = MathUtils.clamp(-Vector3.dot(n0, n1), -1.0f, 1.0f); - float cosGamma = MathUtils.clamp(-Vector3.dot(n1, n2), -1.0f, 1.0f); - - float alpha = (float) Math.acos(cosAlpha); - float beta = (float) Math.acos(cosBeta); - float gamma = (float) Math.acos(cosGamma); - - float area = alpha + beta + gamma - (float) Math.PI; - - float cosC = MathUtils.clamp(Vector3.dot(p0, p1), -1.0f, 1.0f); - float salpha = (float) Math.sin(alpha); - float product = salpha * cosC; - - // use lower sampling depth for diffuse bounces - int samples = state.getDiffuseDepth() > 0 ? 1 : numSamples; - Color c = Color.mul(area / samples, radiance); - for (int j = 0; j < samples; j++) { - // random offset on unit square - double randX = state.getRandom(j, 0, samples); - double randY = state.getRandom(j, 1, samples); - - float phi = (float) randX * area - alpha + (float) Math.PI; - float sinPhi = (float) Math.sin(phi); - float cosPhi = (float) Math.cos(phi); - - float u = cosPhi + cosAlpha; - float v = sinPhi - product; - - float q = (-v + cosAlpha * (cosPhi * -v + sinPhi * u)) / (salpha * (sinPhi * -v - cosPhi * u)); - float q1 = 1.0f - q * q; - if (q1 < 0.0f) - q1 = 0.0f; - - float sqrtq1 = (float) Math.sqrt(q1); - float ncx = q * p0.x + sqrtq1 * h.x; - float ncy = q * p0.y + sqrtq1 * h.y; - float ncz = q * p0.z + sqrtq1 * h.z; - dot = p1.dot(ncx, ncy, ncz); - float z = 1.0f - (float) randY * (1.0f - dot); - float z1 = 1.0f - z * z; - if (z1 < 0.0f) - z1 = 0.0f; - Vector3 nd = new Vector3(); - nd.x = ncx - dot * p1.x; - nd.y = ncy - dot * p1.y; - nd.z = ncz - dot * p1.z; - nd.normalize(); - float sqrtz1 = (float) Math.sqrt(z1); - Vector3 result = new Vector3(); - result.x = z * p1.x + sqrtz1 * nd.x; - result.y = z * p1.y + sqrtz1 * nd.y; - result.z = z * p1.z + sqrtz1 * nd.z; - - // make sure the sample is in the right hemisphere - facing in - // the right direction - if (Vector3.dot(result, n) > 0 && Vector3.dot(result, state.getGeoNormal()) > 0 && Vector3.dot(result, ngs[i]) < 0) { - // compute intersection with triangle (if any) - Ray shadowRay = new Ray(state.getPoint(), result); - if (!intersectTriangleKensler(tri3, shadowRay)) - continue; - LightSample dest = new LightSample(); - dest.setShadowRay(shadowRay); - // prepare sample - dest.setRadiance(c, c); - dest.traceShadow(state); - state.addSample(dest); - } - } - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/modifiers/BumpMappingModifier.java b/src/org/sunflow/core/modifiers/BumpMappingModifier.java deleted file mode 100644 index ef525db..0000000 --- a/src/org/sunflow/core/modifiers/BumpMappingModifier.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.sunflow.core.modifiers; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Modifier; -import org.sunflow.core.ParameterList; -import org.sunflow.core.ShadingState; -import org.sunflow.core.Texture; -import org.sunflow.core.TextureCache; -import org.sunflow.math.OrthoNormalBasis; - -public class BumpMappingModifier implements Modifier { - private Texture bumpTexture; - private float scale; - - public BumpMappingModifier() { - bumpTexture = null; - scale = 1; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - String filename = pl.getString("texture", null); - if (filename != null) - bumpTexture = TextureCache.getTexture(api.resolveTextureFilename(filename), true); - scale = pl.getFloat("scale", scale); - return bumpTexture != null; - } - - public void modify(ShadingState state) { - // apply bump - state.getNormal().set(bumpTexture.getBump(state.getUV().x, state.getUV().y, state.getBasis(), scale)); - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/modifiers/NormalMapModifier.java b/src/org/sunflow/core/modifiers/NormalMapModifier.java deleted file mode 100644 index a297cc3..0000000 --- a/src/org/sunflow/core/modifiers/NormalMapModifier.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.sunflow.core.modifiers; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Modifier; -import org.sunflow.core.ParameterList; -import org.sunflow.core.ShadingState; -import org.sunflow.core.Texture; -import org.sunflow.core.TextureCache; -import org.sunflow.math.OrthoNormalBasis; - -public class NormalMapModifier implements Modifier { - private Texture normalMap; - - public NormalMapModifier() { - normalMap = null; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - String filename = pl.getString("texture", null); - if (filename != null) - normalMap = TextureCache.getTexture(api.resolveTextureFilename(filename), true); - return normalMap != null; - } - - public void modify(ShadingState state) { - // apply normal map - state.getNormal().set(normalMap.getNormal(state.getUV().x, state.getUV().y, state.getBasis())); - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/modifiers/PerlinModifier.java b/src/org/sunflow/core/modifiers/PerlinModifier.java deleted file mode 100644 index 08bfc77..0000000 --- a/src/org/sunflow/core/modifiers/PerlinModifier.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.sunflow.core.modifiers; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Modifier; -import org.sunflow.core.ParameterList; -import org.sunflow.core.ShadingState; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.PerlinScalar; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; - -public class PerlinModifier implements Modifier { - private int function = 0; - private float scale = 50; - private float size = 1; - - public boolean update(ParameterList pl, SunflowAPI api) { - function = pl.getInt("function", function); - size = pl.getFloat("size", size); - scale = pl.getFloat("scale", scale); - return true; - } - - public void modify(ShadingState state) { - Point3 p = state.transformWorldToObject(state.getPoint()); - p.x *= size; - p.y *= size; - p.z *= size; - Vector3 normal = state.transformNormalWorldToObject(state.getNormal()); - double f0 = f(p.x, p.y, p.z); - double fx = f(p.x + .0001, p.y, p.z); - double fy = f(p.x, p.y + .0001, p.z); - double fz = f(p.x, p.y, p.z + .0001); - - normal.x -= scale * (fx - f0) / .0001; - normal.y -= scale * (fy - f0) / .0001; - normal.z -= scale * (fz - f0) / .0001; - normal.normalize(); - - state.getNormal().set(state.transformNormalObjectToWorld(normal)); - state.getNormal().normalize(); - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - } - - double f(double x, double y, double z) { - switch (function) { - case 0: - return .03 * noise(x, y, z, 8); - case 1: - return .01 * stripes(x + 2 * turbulence(x, y, z, 1), 1.6); - default: - return -.10 * turbulence(x, y, z, 1); - } - } - - private static final double stripes(double x, double f) { - double t = .5 + .5 * Math.sin(f * 2 * Math.PI * x); - return t * t - .5; - } - - private static final double turbulence(double x, double y, double z, double freq) { - double t = -.5; - for (; freq <= 300 / 12; freq *= 2) - t += Math.abs(noise(x, y, z, freq) / freq); - return t; - } - - private static final double noise(double x, double y, double z, double freq) { - double x1, y1, z1; - x1 = .707 * x - .707 * z; - z1 = .707 * x + .707 * z; - y1 = .707 * x1 + .707 * y; - x1 = .707 * x1 - .707 * y; - return PerlinScalar.snoise((float) (freq * x1 + 100), (float) (freq * y1), (float) (freq * z1)); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/parser/RA2Parser.java b/src/org/sunflow/core/parser/RA2Parser.java deleted file mode 100644 index f6e5761..0000000 --- a/src/org/sunflow/core/parser/RA2Parser.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.sunflow.core.parser; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.ByteOrder; -import java.nio.FloatBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; - -import org.sunflow.SunflowAPI; -import org.sunflow.SunflowAPIInterface; -import org.sunflow.core.SceneParser; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.Parser; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class RA2Parser implements SceneParser { - public boolean parse(String filename, SunflowAPIInterface api) { - try { - UI.printInfo(Module.USER, "RA2 - Reading geometry: \"%s\" ...", filename); - File file = new File(filename); - FileInputStream stream = new FileInputStream(filename); - MappedByteBuffer map = stream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()); - map.order(ByteOrder.LITTLE_ENDIAN); - FloatBuffer buffer = map.asFloatBuffer(); - float[] data = new float[buffer.capacity()]; - for (int i = 0; i < data.length; i++) - data[i] = buffer.get(i); - stream.close(); - api.parameter("points", "point", "vertex", data); - int[] triangles = new int[3 * (data.length / 9)]; - for (int i = 0; i < triangles.length; i++) - triangles[i] = i; - // create geo - api.parameter("triangles", triangles); - api.geometry(filename, "triangle_mesh"); - // create shader - api.shader(filename + ".shader", "simple"); - // create instance - api.parameter("shaders", filename + ".shader"); - api.instance(filename + ".instance", filename); - } catch (FileNotFoundException e) { - e.printStackTrace(); - return false; - } catch (IOException e) { - e.printStackTrace(); - return false; - } - try { - filename = filename.replace(".ra2", ".txt"); - UI.printInfo(Module.USER, "RA2 - Reading camera : \"%s\" ...", filename); - Parser p = new Parser(filename); - Point3 eye = new Point3(); - eye.x = p.getNextFloat(); - eye.y = p.getNextFloat(); - eye.z = p.getNextFloat(); - Point3 to = new Point3(); - to.x = p.getNextFloat(); - to.y = p.getNextFloat(); - to.z = p.getNextFloat(); - Vector3 up = new Vector3(); - switch (p.getNextInt()) { - case 0: - up.set(1, 0, 0); - break; - case 1: - up.set(0, 1, 0); - break; - case 2: - up.set(0, 0, 1); - break; - default: - UI.printWarning(Module.USER, "RA2 - Invalid up vector specification - using Z axis"); - up.set(0, 0, 1); - break; - } - api.parameter("eye", eye); - api.parameter("target", to); - api.parameter("up", up); - String cameraName = filename + ".camera"; - api.parameter("fov", 80f); - api.camera(cameraName, "pinhole"); - api.parameter("camera", cameraName); - api.parameter("resolutionX", 1024); - api.parameter("resolutionY", 1024); - api.options(SunflowAPI.DEFAULT_OPTIONS); - p.close(); - } catch (FileNotFoundException e) { - UI.printWarning(Module.USER, "RA2 - Camera file not found"); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - return true; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/parser/RA3Parser.java b/src/org/sunflow/core/parser/RA3Parser.java deleted file mode 100644 index 3d76c1a..0000000 --- a/src/org/sunflow/core/parser/RA3Parser.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.sunflow.core.parser; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.ByteOrder; -import java.nio.FloatBuffer; -import java.nio.IntBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; - -import org.sunflow.SunflowAPIInterface; -import org.sunflow.core.SceneParser; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class RA3Parser implements SceneParser { - public boolean parse(String filename, SunflowAPIInterface api) { - try { - UI.printInfo(Module.USER, "RA3 - Reading geometry: \"%s\" ...", filename); - File file = new File(filename); - FileInputStream stream = new FileInputStream(filename); - MappedByteBuffer map = stream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()); - map.order(ByteOrder.LITTLE_ENDIAN); - IntBuffer ints = map.asIntBuffer(); - FloatBuffer buffer = map.asFloatBuffer(); - int numVerts = ints.get(0); - int numTris = ints.get(1); - UI.printInfo(Module.USER, "RA3 - * Reading %d vertices ...", numVerts); - float[] verts = new float[3 * numVerts]; - for (int i = 0; i < verts.length; i++) - verts[i] = buffer.get(2 + i); - UI.printInfo(Module.USER, "RA3 - * Reading %d triangles ...", numTris); - int[] tris = new int[3 * numTris]; - for (int i = 0; i < tris.length; i++) - tris[i] = ints.get(2 + verts.length + i); - stream.close(); - UI.printInfo(Module.USER, "RA3 - * Creating mesh ..."); - - // create geometry - api.parameter("triangles", tris); - api.parameter("points", "point", "vertex", verts); - api.geometry(filename, "triangle_mesh"); - - // create default shader (this will simply error out if the shader - // already exists) - api.shader("ra3shader", "simple"); - // create instance - api.parameter("shaders", "ra3shader"); - api.instance(filename + ".instance", filename); - } catch (FileNotFoundException e) { - e.printStackTrace(); - return false; - } catch (IOException e) { - e.printStackTrace(); - return false; - } - return true; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/parser/SCAbstractParser.java b/src/org/sunflow/core/parser/SCAbstractParser.java deleted file mode 100644 index 94bb0b2..0000000 --- a/src/org/sunflow/core/parser/SCAbstractParser.java +++ /dev/null @@ -1,294 +0,0 @@ -package org.sunflow.core.parser; - -import java.io.EOFException; -import java.io.IOException; - -import org.sunflow.SunflowAPIInterface; -import org.sunflow.core.SceneParser; -import org.sunflow.core.ParameterList.InterpolationType; -import org.sunflow.image.ColorFactory; -import org.sunflow.math.Matrix4; -import org.sunflow.math.Point2; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public abstract class SCAbstractParser implements SceneParser { - public enum Keyword { - RESET, PARAMETER, GEOMETRY, INSTANCE, SHADER, MODIFIER, LIGHT, CAMERA, OPTIONS, INCLUDE, REMOVE, FRAME, PLUGIN, SEARCHPATH, STRING, BOOL, INT, FLOAT, COLOR, POINT, VECTOR, TEXCOORD, MATRIX, STRING_ARRAY, INT_ARRAY, FLOAT_ARRAY, POINT_ARRAY, VECTOR_ARRAY, TEXCOORD_ARRAY, MATRIX_ARRAY, END_OF_FILE, - } - - public boolean parse(String filename, SunflowAPIInterface api) { - Timer timer = new Timer(); - timer.start(); - UI.printInfo(Module.API, "Parsing \"%s\" ...", filename); - try { - openParser(filename); - parseloop: while (true) { - Keyword k = parseKeyword(); - switch (k) { - case RESET: - api.reset(); - break; - case PARAMETER: - parseParameter(api); - break; - case GEOMETRY: { - String name = parseString(); - String type = parseString(); - api.geometry(name, type); - break; - } - case INSTANCE: { - String name = parseString(); - String geoname = parseString(); - api.instance(name, geoname); - break; - } - case SHADER: { - String name = parseString(); - String type = parseString(); - api.shader(name, type); - break; - } - case MODIFIER: { - String name = parseString(); - String type = parseString(); - api.modifier(name, type); - break; - } - case LIGHT: { - String name = parseString(); - String type = parseString(); - api.light(name, type); - break; - } - case CAMERA: { - String name = parseString(); - String type = parseString(); - api.camera(name, type); - break; - } - case OPTIONS: { - api.options(parseString()); - break; - } - case INCLUDE: { - String file = parseString(); - UI.printInfo(Module.API, "Including: \"%s\" ...", file); - api.include(file); - break; - } - case REMOVE: { - api.remove(parseString()); - break; - } - case FRAME: { - api.currentFrame(parseInt()); - break; - } - case PLUGIN: { - String type = parseString(); - String name = parseString(); - String code = parseVerbatimString(); - api.plugin(type, name, code); - break; - } - case SEARCHPATH: { - String type = parseString(); - api.searchpath(type, parseString()); - break; - } - case END_OF_FILE: { - // clean exit - break parseloop; - } - default: { - UI.printWarning(Module.API, "Unexpected token %s", k); - break; - } - } - } - closeParser(); - } catch (Exception e) { - // catch all exceptions - e.printStackTrace(); - UI.printError(Module.API, "%s", e.getMessage()); - return false; - } - timer.end(); - UI.printInfo(Module.API, "Done parsing (took %s)", timer.toString()); - return true; - } - - private void parseParameter(SunflowAPIInterface api) throws IOException { - String name = parseString(); - Keyword k = parseKeyword(); - switch (k) { - case STRING: { - api.parameter(name, parseString()); - break; - } - case BOOL: { - api.parameter(name, parseBoolean()); - break; - } - case INT: { - api.parameter(name, parseInt()); - break; - } - case FLOAT: { - api.parameter(name, parseFloat()); - break; - } - case COLOR: { - String colorspace = parseString(); - int req = ColorFactory.getRequiredDataValues(colorspace); - if (req == -2) - api.parameter(name, colorspace); // call just to generate - // an error - else - api.parameter(name, colorspace, parseFloatArray(req == -1 ? parseInt() : req)); - break; - } - case POINT: { - api.parameter(name, parsePoint()); - break; - } - case VECTOR: { - api.parameter(name, parseVector()); - break; - } - case TEXCOORD: { - api.parameter(name, parseTexcoord()); - break; - } - case MATRIX: { - api.parameter(name, parseMatrix()); - break; - } - case STRING_ARRAY: { - int n = parseInt(); - api.parameter(name, parseStringArray(n)); - break; - } - case INT_ARRAY: { - int n = parseInt(); - api.parameter(name, parseIntArray(n)); - break; - } - case FLOAT_ARRAY: { - String interp = parseInterpolationType().toString(); - int n = parseInt(); - api.parameter(name, "float", interp, parseFloatArray(n)); - break; - } - case POINT_ARRAY: { - String interp = parseInterpolationType().toString(); - int n = parseInt(); - api.parameter(name, "point", interp, parseFloatArray(3 * n)); - break; - } - case VECTOR_ARRAY: { - String interp = parseInterpolationType().toString(); - int n = parseInt(); - api.parameter(name, "vector", interp, parseFloatArray(3 * n)); - break; - } - case TEXCOORD_ARRAY: { - String interp = parseInterpolationType().toString(); - int n = parseInt(); - api.parameter(name, "texcoord", interp, parseFloatArray(2 * n)); - break; - } - case MATRIX_ARRAY: { - String interp = parseInterpolationType().toString(); - int n = parseInt(); - api.parameter(name, "matrix", interp, parseMatrixArray(n)); - break; - } - case END_OF_FILE: - throw new EOFException(); - default: { - UI.printWarning(Module.API, "Unexpected keyword: %s", k); - break; - } - } - } - - private String[] parseStringArray(int size) throws IOException { - String[] data = new String[size]; - for (int i = 0; i < size; i++) - data[i] = parseString(); - return data; - } - - private int[] parseIntArray(int size) throws IOException { - int[] data = new int[size]; - for (int i = 0; i < size; i++) - data[i] = parseInt(); - return data; - } - - protected float[] parseFloatArray(int size) throws IOException { - float[] data = new float[size]; - for (int i = 0; i < size; i++) - data[i] = parseFloat(); - return data; - } - - private float[] parseMatrixArray(int size) throws IOException { - float[] data = new float[16 * size]; - for (int i = 0, offset = 0; i < size; i++, offset += 16) { - // copy the next matrix into a linear array - in row major order - float[] rowdata = parseMatrix().asRowMajor(); - for (int j = 0; j < 16; j++) - data[offset + j] = rowdata[j]; - } - return data; - } - - private Point3 parsePoint() throws IOException { - float x = parseFloat(); - float y = parseFloat(); - float z = parseFloat(); - return new Point3(x, y, z); - } - - private Vector3 parseVector() throws IOException { - float x = parseFloat(); - float y = parseFloat(); - float z = parseFloat(); - return new Vector3(x, y, z); - } - - private Point2 parseTexcoord() throws IOException { - float x = parseFloat(); - float y = parseFloat(); - return new Point2(x, y); - } - - protected abstract InterpolationType parseInterpolationType() throws IOException; - - // abstract methods - to be implemented by subclasses - - protected abstract void openParser(String filename) throws IOException; - - protected abstract void closeParser() throws IOException; - - protected abstract Keyword parseKeyword() throws IOException; - - protected abstract boolean parseBoolean() throws IOException; - - protected abstract int parseInt() throws IOException; - - protected abstract float parseFloat() throws IOException; - - protected abstract String parseString() throws IOException; - - protected abstract String parseVerbatimString() throws IOException; - - protected abstract Matrix4 parseMatrix() throws IOException; -} \ No newline at end of file diff --git a/src/org/sunflow/core/parser/SCAsciiParser.java b/src/org/sunflow/core/parser/SCAsciiParser.java deleted file mode 100644 index 763f074..0000000 --- a/src/org/sunflow/core/parser/SCAsciiParser.java +++ /dev/null @@ -1,213 +0,0 @@ -package org.sunflow.core.parser; - -import java.io.IOException; - -import org.sunflow.core.ParameterList.InterpolationType; -import org.sunflow.image.Color; -import org.sunflow.math.Matrix4; -import org.sunflow.system.Parser; -import org.sunflow.system.UI; -import org.sunflow.system.Parser.ParserException; -import org.sunflow.system.UI.Module; - -public class SCAsciiParser extends SCAbstractParser { - private Parser p; - - protected Color parseColor() throws IOException { - String space = p.getNextToken(); - Color c = null; - if (space.equals("sRGB nonlinear")) { - float r = p.getNextFloat(); - float g = p.getNextFloat(); - float b = p.getNextFloat(); - c = new Color(r, g, b); - c.toLinear(); - } else if (space.equals("sRGB linear")) { - float r = p.getNextFloat(); - float g = p.getNextFloat(); - float b = p.getNextFloat(); - c = new Color(r, g, b); - } else - UI.printWarning(Module.API, "Unrecognized color space: %s", space); - return c; - } - - @Override - protected Matrix4 parseMatrix() throws IOException { - if (p.peekNextToken("row")) { - return new Matrix4(parseFloatArray(16), true); - } else if (p.peekNextToken("col")) { - return new Matrix4(parseFloatArray(16), false); - } else { - Matrix4 m = Matrix4.IDENTITY; - try { - p.checkNextToken("{"); - } catch (ParserException e) { - throw new IOException(e.getMessage()); - } - while (!p.peekNextToken("}")) { - Matrix4 t = null; - if (p.peekNextToken("translate")) { - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - t = Matrix4.translation(x, y, z); - } else if (p.peekNextToken("scaleu")) { - float s = p.getNextFloat(); - t = Matrix4.scale(s); - } else if (p.peekNextToken("scale")) { - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - t = Matrix4.scale(x, y, z); - } else if (p.peekNextToken("rotatex")) { - float angle = p.getNextFloat(); - t = Matrix4.rotateX((float) Math.toRadians(angle)); - } else if (p.peekNextToken("rotatey")) { - float angle = p.getNextFloat(); - t = Matrix4.rotateY((float) Math.toRadians(angle)); - } else if (p.peekNextToken("rotatez")) { - float angle = p.getNextFloat(); - t = Matrix4.rotateZ((float) Math.toRadians(angle)); - } else if (p.peekNextToken("rotate")) { - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - float angle = p.getNextFloat(); - t = Matrix4.rotate(x, y, z, (float) Math.toRadians(angle)); - } else - UI.printWarning(Module.API, "Unrecognized transformation type: %s", p.getNextToken()); - if (t != null) - m = t.multiply(m); - } - return m; - } - } - - @Override - protected void closeParser() throws IOException { - p.close(); - } - - @Override - protected void openParser(String filename) throws IOException { - p = new Parser(filename); - } - - @Override - protected boolean parseBoolean() throws IOException { - return Boolean.parseBoolean(parseString()); - } - - @Override - protected float parseFloat() throws IOException { - return p.getNextFloat(); - } - - @Override - protected int parseInt() throws IOException { - return p.getNextInt(); - } - - @Override - protected String parseString() throws IOException { - return p.getNextToken(); - } - - @Override - protected String parseVerbatimString() throws IOException { - try { - return p.getNextCodeBlock(); - } catch (ParserException e) { - throw new IOException(e.getMessage()); - } - } - - @Override - protected InterpolationType parseInterpolationType() throws IOException { - if (p.peekNextToken("none")) - return InterpolationType.NONE; - else if (p.peekNextToken("vertex")) - return InterpolationType.VERTEX; - else if (p.peekNextToken("face")) - return InterpolationType.FACE; - else if (p.peekNextToken("facevarying")) - return InterpolationType.FACEVARYING; - return InterpolationType.NONE; - } - - @Override - protected Keyword parseKeyword() throws IOException { - String keyword = p.getNextToken(); - if (keyword == null) - return Keyword.END_OF_FILE; - if (anyEqual(keyword, "reset")) - return Keyword.RESET; - if (anyEqual(keyword, "parameter", "param", "p")) - return Keyword.PARAMETER; - if (anyEqual(keyword, "geometry", "geom", "g")) - return Keyword.GEOMETRY; - if (anyEqual(keyword, "instance", "inst", "i")) - return Keyword.INSTANCE; - if (anyEqual(keyword, "shader", "shd", "s")) - return Keyword.SHADER; - if (anyEqual(keyword, "modifier", "mod", "m")) - return Keyword.MODIFIER; - if (anyEqual(keyword, "light", "l")) - return Keyword.LIGHT; - if (anyEqual(keyword, "camera", "cam", "c")) - return Keyword.CAMERA; - if (anyEqual(keyword, "options", "opt", "o")) - return Keyword.OPTIONS; - if (anyEqual(keyword, "include", "inc")) - return Keyword.INCLUDE; - if (anyEqual(keyword, "remove")) - return Keyword.REMOVE; - if (anyEqual(keyword, "frame")) - return Keyword.FRAME; - if (anyEqual(keyword, "plugin", "plug")) - return Keyword.PLUGIN; - if (anyEqual(keyword, "searchpath")) - return Keyword.SEARCHPATH; - if (anyEqual(keyword, "string", "str")) - return Keyword.STRING; - if (anyEqual(keyword, "string[]", "str[]")) - return Keyword.STRING_ARRAY; - if (anyEqual(keyword, "boolean", "bool")) - return Keyword.BOOL; - if (anyEqual(keyword, "integer", "int")) - return Keyword.INT; - if (anyEqual(keyword, "integer[]", "int[]")) - return Keyword.INT_ARRAY; - if (anyEqual(keyword, "float", "flt")) - return Keyword.FLOAT; - if (anyEqual(keyword, "float[]", "flt[]")) - return Keyword.FLOAT_ARRAY; - if (anyEqual(keyword, "color", "col")) - return Keyword.COLOR; - if (anyEqual(keyword, "point", "pnt")) - return Keyword.POINT; - if (anyEqual(keyword, "point[]", "pnt[]")) - return Keyword.POINT_ARRAY; - if (anyEqual(keyword, "vector", "vec")) - return Keyword.VECTOR; - if (anyEqual(keyword, "vector[]", "vec[]")) - return Keyword.VECTOR_ARRAY; - if (anyEqual(keyword, "texcoord", "tex")) - return Keyword.TEXCOORD; - if (anyEqual(keyword, "texcoord[]", "tex[]")) - return Keyword.TEXCOORD_ARRAY; - if (anyEqual(keyword, "matrix", "mat")) - return Keyword.MATRIX; - if (anyEqual(keyword, "matrix[]", "mat[]")) - return Keyword.MATRIX_ARRAY; - return null; - } - - private boolean anyEqual(String source, String... values) { - for (String v : values) - if (source.equals(v)) - return true; - return false; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/parser/SCBinaryParser.java b/src/org/sunflow/core/parser/SCBinaryParser.java deleted file mode 100644 index 09eab28..0000000 --- a/src/org/sunflow/core/parser/SCBinaryParser.java +++ /dev/null @@ -1,154 +0,0 @@ -package org.sunflow.core.parser; - -import java.io.BufferedInputStream; -import java.io.DataInputStream; -import java.io.FileInputStream; -import java.io.IOException; - -import org.sunflow.core.ParameterList.InterpolationType; -import org.sunflow.math.Matrix4; - -public class SCBinaryParser extends SCAbstractParser { - private DataInputStream stream; - - @Override - protected void closeParser() throws IOException { - stream.close(); - } - - @Override - protected void openParser(String filename) throws IOException { - stream = new DataInputStream(new BufferedInputStream(new FileInputStream(filename))); - } - - @Override - protected boolean parseBoolean() throws IOException { - return stream.readUnsignedByte() != 0; - } - - @Override - protected float parseFloat() throws IOException { - return Float.intBitsToFloat(parseInt()); - } - - @Override - protected int parseInt() throws IOException { - // note that we use readUnsignedByte(), not read() to get EOF exceptions - return stream.readUnsignedByte() | (stream.readUnsignedByte() << 8) | (stream.readUnsignedByte() << 16) | (stream.readUnsignedByte() << 24); - } - - @Override - protected Matrix4 parseMatrix() throws IOException { - return new Matrix4(parseFloatArray(16), true); - } - - @Override - protected String parseString() throws IOException { - byte[] b = new byte[parseInt()]; - stream.read(b); - return new String(b, "UTF-8"); - } - - @Override - protected String parseVerbatimString() throws IOException { - return parseString(); - } - - @Override - protected InterpolationType parseInterpolationType() throws IOException { - int c; - switch (c = stream.readUnsignedByte()) { - case 'n': - return InterpolationType.NONE; - case 'v': - return InterpolationType.VERTEX; - case 'f': - return InterpolationType.FACEVARYING; - case 'p': - return InterpolationType.FACE; - default: - throw new IOException(String.format("Unknown byte found for interpolation type %c", (char) c)); - } - } - - @Override - protected Keyword parseKeyword() throws IOException { - int code = stream.read(); // read a single byte - allow for EOF (<0) - switch (code) { - case 'p': - return Keyword.PARAMETER; - case 'g': - return Keyword.GEOMETRY; - case 'i': - return Keyword.INSTANCE; - case 's': - return Keyword.SHADER; - case 'm': - return Keyword.MODIFIER; - case 'l': - return Keyword.LIGHT; - case 'c': - return Keyword.CAMERA; - case 'o': - return Keyword.OPTIONS; - case 'x': { - // extended keywords (less frequent) - // note we don't use stream.read() here because we should throw - // an exception if the end of the file is reached - switch (code = stream.readUnsignedByte()) { - case 'R': - return Keyword.RESET; - case 'i': - return Keyword.INCLUDE; - case 'r': - return Keyword.REMOVE; - case 'f': - return Keyword.FRAME; - case 'p': - return Keyword.PLUGIN; - case 's': - return Keyword.SEARCHPATH; - default: - throw new IOException(String.format("Unknown extended keyword code: %c", (char) code)); - } - } - case 't': { - // data types - // note we don't use stream.read() here because we should throw - // an exception if the end of the file is reached - int type = stream.readUnsignedByte(); - // note that while not all types can be arrays at the moment, we - // always parse this boolean flag to keep the syntax consistent - // and allow for future improvements - boolean isArray = parseBoolean(); - switch (type) { - case 's': - return isArray ? Keyword.STRING_ARRAY : Keyword.STRING; - case 'b': - return Keyword.BOOL; - case 'i': - return isArray ? Keyword.INT_ARRAY : Keyword.INT; - case 'f': - return isArray ? Keyword.FLOAT_ARRAY : Keyword.FLOAT; - case 'c': - return Keyword.COLOR; - case 'p': - return isArray ? Keyword.POINT_ARRAY : Keyword.POINT; - case 'v': - return isArray ? Keyword.VECTOR_ARRAY : Keyword.VECTOR; - case 't': - return isArray ? Keyword.TEXCOORD_ARRAY : Keyword.TEXCOORD; - case 'm': - return isArray ? Keyword.MATRIX_ARRAY : Keyword.MATRIX; - default: - throw new IOException(String.format("Unknown datatype keyword code: %c", (char) type)); - } - } - default: - if (code < 0) - return Keyword.END_OF_FILE; // normal end of file reached - else - throw new IOException(String.format("Unknown keyword code: %c", (char) code)); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/parser/SCParser.java b/src/org/sunflow/core/parser/SCParser.java deleted file mode 100644 index eee7286..0000000 --- a/src/org/sunflow/core/parser/SCParser.java +++ /dev/null @@ -1,1284 +0,0 @@ -package org.sunflow.core.parser; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.ByteOrder; -import java.nio.FloatBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.util.HashMap; - -import org.sunflow.PluginRegistry; -import org.sunflow.SunflowAPI; -import org.sunflow.SunflowAPIInterface; -import org.sunflow.core.SceneParser; -import org.sunflow.image.Color; -import org.sunflow.image.ColorFactory; -import org.sunflow.image.ColorFactory.ColorSpecificationException; -import org.sunflow.math.Matrix4; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.Parser; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.Parser.ParserException; -import org.sunflow.system.UI.Module; - -/** - * This class provides a static method for loading files in the Sunflow scene - * file format. - */ -public class SCParser implements SceneParser { - private static int instanceCounter = 0; - private int instanceNumber; - private Parser p; - private int numLightSamples; - // used to generate unique names inside this parser - private HashMap objectNames; - - public SCParser() { - objectNames = new HashMap(); - instanceCounter++; - instanceNumber = instanceCounter; - } - - private String generateUniqueName(String prefix) { - // generate a unique name for this class: - int index = 1; - Integer value = objectNames.get(prefix); - if (value != null) { - index = value; - objectNames.put(prefix, index + 1); - } else { - objectNames.put(prefix, index + 1); - } - return String.format("@sc_%d::%s_%d", instanceNumber, prefix, index); - } - - public boolean parse(String filename, SunflowAPIInterface api) { - String localDir = new File(filename).getAbsoluteFile().getParentFile().getAbsolutePath(); - numLightSamples = 1; - Timer timer = new Timer(); - timer.start(); - UI.printInfo(Module.API, "Parsing \"%s\" ...", filename); - try { - p = new Parser(filename); - while (true) { - String token = p.getNextToken(); - if (token == null) - break; - if (token.equals("image")) { - UI.printInfo(Module.API, "Reading image settings ..."); - parseImageBlock(api); - } else if (token.equals("background")) { - UI.printInfo(Module.API, "Reading background ..."); - parseBackgroundBlock(api); - } else if (token.equals("accel")) { - UI.printInfo(Module.API, "Reading accelerator type ..."); - p.getNextToken(); - UI.printWarning(Module.API, "Setting accelerator type is not recommended - ignoring"); - } else if (token.equals("filter")) { - UI.printInfo(Module.API, "Reading image filter type ..."); - parseFilter(api); - } else if (token.equals("bucket")) { - UI.printInfo(Module.API, "Reading bucket settings ..."); - api.parameter("bucket.size", p.getNextInt()); - api.parameter("bucket.order", p.getNextToken()); - api.options(SunflowAPI.DEFAULT_OPTIONS); - } else if (token.equals("photons")) { - UI.printInfo(Module.API, "Reading photon settings ..."); - parsePhotonBlock(api); - } else if (token.equals("gi")) { - UI.printInfo(Module.API, "Reading global illumination settings ..."); - parseGIBlock(api); - } else if (token.equals("lightserver")) { - UI.printInfo(Module.API, "Reading light server settings ..."); - parseLightserverBlock(api); - } else if (token.equals("trace-depths")) { - UI.printInfo(Module.API, "Reading trace depths ..."); - parseTraceBlock(api); - } else if (token.equals("camera")) { - parseCamera(api); - } else if (token.equals("shader")) { - if (!parseShader(api)) - return false; - } else if (token.equals("modifier")) { - if (!parseModifier(api)) - return false; - } else if (token.equals("override")) { - api.parameter("override.shader", p.getNextToken()); - api.parameter("override.photons", p.getNextBoolean()); - api.options(SunflowAPI.DEFAULT_OPTIONS); - } else if (token.equals("object")) { - parseObjectBlock(api); - } else if (token.equals("instance")) { - parseInstanceBlock(api); - } else if (token.equals("light")) { - parseLightBlock(api); - } else if (token.equals("texturepath")) { - String path = p.getNextToken(); - if (!new File(path).isAbsolute()) - path = localDir + File.separator + path; - api.searchpath("texture", path); - } else if (token.equals("includepath")) { - String path = p.getNextToken(); - if (!new File(path).isAbsolute()) - path = localDir + File.separator + path; - api.searchpath("include", path); - } else if (token.equals("include")) { - String file = p.getNextToken(); - UI.printInfo(Module.API, "Including: \"%s\" ...", file); - api.include(file); - } else - UI.printWarning(Module.API, "Unrecognized token %s", token); - } - p.close(); - } catch (ParserException e) { - UI.printError(Module.API, "%s", e.getMessage()); - e.printStackTrace(); - return false; - } catch (FileNotFoundException e) { - UI.printError(Module.API, "%s", e.getMessage()); - return false; - } catch (IOException e) { - UI.printError(Module.API, "%s", e.getMessage()); - return false; - } catch (ColorSpecificationException e) { - UI.printError(Module.API, "%s", e.getMessage()); - return false; - } - timer.end(); - UI.printInfo(Module.API, "Done parsing."); - UI.printInfo(Module.API, "Parsing time: %s", timer.toString()); - return true; - } - - private void parseImageBlock(SunflowAPIInterface api) throws IOException, ParserException { - p.checkNextToken("{"); - if (p.peekNextToken("resolution")) { - api.parameter("resolutionX", p.getNextInt()); - api.parameter("resolutionY", p.getNextInt()); - } - if (p.peekNextToken("sampler")) - api.parameter("sampler", p.getNextToken()); - if (p.peekNextToken("aa")) { - api.parameter("aa.min", p.getNextInt()); - api.parameter("aa.max", p.getNextInt()); - } - if (p.peekNextToken("samples")) - api.parameter("aa.samples", p.getNextInt()); - if (p.peekNextToken("contrast")) - api.parameter("aa.contrast", p.getNextFloat()); - if (p.peekNextToken("filter")) - api.parameter("filter", p.getNextToken()); - if (p.peekNextToken("jitter")) - api.parameter("aa.jitter", p.getNextBoolean()); - if (p.peekNextToken("show-aa")) { - UI.printWarning(Module.API, "Deprecated: show-aa ignored"); - p.getNextBoolean(); - } - if (p.peekNextToken("cache")) - api.parameter("aa.cache", p.getNextBoolean()); - if (p.peekNextToken("output")) { - UI.printWarning(Module.API, "Deprecated: output statement ignored"); - p.getNextToken(); - } - api.options(SunflowAPI.DEFAULT_OPTIONS); - p.checkNextToken("}"); - } - - private void parseBackgroundBlock(SunflowAPIInterface api) throws IOException, ParserException, ColorSpecificationException { - p.checkNextToken("{"); - p.checkNextToken("color"); - api.parameter("color", null, parseColor().getRGB()); - api.shader("background.shader", "constant"); - api.geometry("background", "background"); - api.parameter("shaders", "background.shader"); - api.instance("background.instance", "background"); - p.checkNextToken("}"); - } - - private void parseFilter(SunflowAPIInterface api) throws IOException, ParserException { - UI.printWarning(Module.API, "Deprecated keyword \"filter\" - set this option in the image block"); - String name = p.getNextToken(); - api.parameter("filter", name); - api.options(SunflowAPI.DEFAULT_OPTIONS); - boolean hasSizeParams = name.equals("box") || name.equals("gaussian") || name.equals("blackman-harris") || name.equals("sinc") || name.equals("triangle"); - if (hasSizeParams) { - p.getNextFloat(); - p.getNextFloat(); - } - } - - private void parsePhotonBlock(SunflowAPIInterface api) throws ParserException, IOException { - int numEmit = 0; - boolean globalEmit = false; - p.checkNextToken("{"); - if (p.peekNextToken("emit")) { - UI.printWarning(Module.API, "Shared photon emit values are deprectated - specify number of photons to emit per map"); - numEmit = p.getNextInt(); - globalEmit = true; - } - if (p.peekNextToken("global")) { - UI.printWarning(Module.API, "Global photon map setting belonds inside the gi block - ignoring"); - if (!globalEmit) - p.getNextInt(); - p.getNextToken(); - p.getNextInt(); - p.getNextFloat(); - } - p.checkNextToken("caustics"); - if (!globalEmit) - numEmit = p.getNextInt(); - api.parameter("caustics.emit", numEmit); - api.parameter("caustics", p.getNextToken()); - api.parameter("caustics.gather", p.getNextInt()); - api.parameter("caustics.radius", p.getNextFloat()); - api.options(SunflowAPI.DEFAULT_OPTIONS); - p.checkNextToken("}"); - } - - private void parseGIBlock(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { - p.checkNextToken("{"); - p.checkNextToken("type"); - if (p.peekNextToken("irr-cache")) { - api.parameter("gi.engine", "irr-cache"); - p.checkNextToken("samples"); - api.parameter("gi.irr-cache.samples", p.getNextInt()); - p.checkNextToken("tolerance"); - api.parameter("gi.irr-cache.tolerance", p.getNextFloat()); - p.checkNextToken("spacing"); - api.parameter("gi.irr-cache.min_spacing", p.getNextFloat()); - api.parameter("gi.irr-cache.max_spacing", p.getNextFloat()); - // parse global photon map info - if (p.peekNextToken("global")) { - api.parameter("gi.irr-cache.gmap.emit", p.getNextInt()); - api.parameter("gi.irr-cache.gmap", p.getNextToken()); - api.parameter("gi.irr-cache.gmap.gather", p.getNextInt()); - api.parameter("gi.irr-cache.gmap.radius", p.getNextFloat()); - } - } else if (p.peekNextToken("path")) { - api.parameter("gi.engine", "path"); - p.checkNextToken("samples"); - api.parameter("gi.path.samples", p.getNextInt()); - if (p.peekNextToken("bounces")) { - UI.printWarning(Module.API, "Deprecated setting: bounces - use diffuse trace depth instead"); - p.getNextInt(); - } - } else if (p.peekNextToken("fake")) { - api.parameter("gi.engine", "fake"); - p.checkNextToken("up"); - api.parameter("gi.fake.up", parseVector()); - p.checkNextToken("sky"); - api.parameter("gi.fake.sky", null, parseColor().getRGB()); - p.checkNextToken("ground"); - api.parameter("gi.fake.ground", null, parseColor().getRGB()); - } else if (p.peekNextToken("igi")) { - api.parameter("gi.engine", "igi"); - p.checkNextToken("samples"); - api.parameter("gi.igi.samples", p.getNextInt()); - p.checkNextToken("sets"); - api.parameter("gi.igi.sets", p.getNextInt()); - if (!p.peekNextToken("b")) - p.checkNextToken("c"); - api.parameter("gi.igi.c", p.getNextFloat()); - p.checkNextToken("bias-samples"); - api.parameter("gi.igi.bias_samples", p.getNextInt()); - } else if (p.peekNextToken("ambocc")) { - api.parameter("gi.engine", "ambocc"); - p.checkNextToken("bright"); - api.parameter("gi.ambocc.bright", null, parseColor().getRGB()); - p.checkNextToken("dark"); - api.parameter("gi.ambocc.dark", null, parseColor().getRGB()); - p.checkNextToken("samples"); - api.parameter("gi.ambocc.samples", p.getNextInt()); - if (p.peekNextToken("maxdist")) - api.parameter("gi.ambocc.maxdist", p.getNextFloat()); - } else if (p.peekNextToken("none") || p.peekNextToken("null")) { - // disable GI - api.parameter("gi.engine", "none"); - } else - UI.printWarning(Module.API, "Unrecognized gi engine type \"%s\" - ignoring", p.getNextToken()); - api.options(SunflowAPI.DEFAULT_OPTIONS); - p.checkNextToken("}"); - } - - private void parseLightserverBlock(SunflowAPIInterface api) throws ParserException, IOException { - p.checkNextToken("{"); - if (p.peekNextToken("shadows")) { - UI.printWarning(Module.API, "Deprecated: shadows setting ignored"); - p.getNextBoolean(); - } - if (p.peekNextToken("direct-samples")) { - UI.printWarning(Module.API, "Deprecated: use samples keyword in area light definitions"); - numLightSamples = p.getNextInt(); - } - if (p.peekNextToken("glossy-samples")) { - UI.printWarning(Module.API, "Deprecated: use samples keyword in glossy shader definitions"); - p.getNextInt(); - } - if (p.peekNextToken("max-depth")) { - UI.printWarning(Module.API, "Deprecated: max-depth setting - use trace-depths block instead"); - int d = p.getNextInt(); - api.parameter("depths.diffuse", 1); - api.parameter("depths.reflection", d - 1); - api.parameter("depths.refraction", 0); - api.options(SunflowAPI.DEFAULT_OPTIONS); - } - if (p.peekNextToken("global")) { - UI.printWarning(Module.API, "Deprecated: global settings ignored - use photons block instead"); - p.getNextBoolean(); - p.getNextInt(); - p.getNextInt(); - p.getNextInt(); - p.getNextFloat(); - } - if (p.peekNextToken("caustics")) { - UI.printWarning(Module.API, "Deprecated: caustics settings ignored - use photons block instead"); - p.getNextBoolean(); - p.getNextInt(); - p.getNextFloat(); - p.getNextInt(); - p.getNextFloat(); - } - if (p.peekNextToken("irr-cache")) { - UI.printWarning(Module.API, "Deprecated: irradiance cache settings ignored - use gi block instead"); - p.getNextInt(); - p.getNextFloat(); - p.getNextFloat(); - p.getNextFloat(); - } - p.checkNextToken("}"); - } - - private void parseTraceBlock(SunflowAPIInterface api) throws ParserException, IOException { - p.checkNextToken("{"); - if (p.peekNextToken("diff")) - api.parameter("depths.diffuse", p.getNextInt()); - if (p.peekNextToken("refl")) - api.parameter("depths.reflection", p.getNextInt()); - if (p.peekNextToken("refr")) - api.parameter("depths.refraction", p.getNextInt()); - p.checkNextToken("}"); - api.options(SunflowAPI.DEFAULT_OPTIONS); - } - - private void parseCamera(SunflowAPIInterface api) throws ParserException, IOException { - p.checkNextToken("{"); - p.checkNextToken("type"); - String type = p.getNextToken(); - UI.printInfo(Module.API, "Reading %s camera ...", type); - if (p.peekNextToken("shutter")) { - api.parameter("shutter.open", p.getNextFloat()); - api.parameter("shutter.close", p.getNextFloat()); - } - parseCameraTransform(api); - String name = generateUniqueName("camera"); - if (type.equals("pinhole")) { - p.checkNextToken("fov"); - api.parameter("fov", p.getNextFloat()); - p.checkNextToken("aspect"); - api.parameter("aspect", p.getNextFloat()); - if (p.peekNextToken("shift")) { - api.parameter("shift.x", p.getNextFloat()); - api.parameter("shift.y", p.getNextFloat()); - } - api.camera(name, "pinhole"); - } else if (type.equals("thinlens")) { - p.checkNextToken("fov"); - api.parameter("fov", p.getNextFloat()); - p.checkNextToken("aspect"); - api.parameter("aspect", p.getNextFloat()); - if (p.peekNextToken("shift")) { - api.parameter("shift.x", p.getNextFloat()); - api.parameter("shift.y", p.getNextFloat()); - } - p.checkNextToken("fdist"); - api.parameter("focus.distance", p.getNextFloat()); - p.checkNextToken("lensr"); - api.parameter("lens.radius", p.getNextFloat()); - if (p.peekNextToken("sides")) - api.parameter("lens.sides", p.getNextInt()); - if (p.peekNextToken("rotation")) - api.parameter("lens.rotation", p.getNextFloat()); - api.camera(name, "thinlens"); - } else if (type.equals("spherical")) { - // no extra arguments - api.camera(name, "spherical"); - } else if (type.equals("fisheye")) { - // no extra arguments - api.camera(name, "fisheye"); - } else { - UI.printWarning(Module.API, "Unrecognized camera type: %s", p.getNextToken()); - p.checkNextToken("}"); - return; - } - p.checkNextToken("}"); - if (name != null) { - api.parameter("camera", name); - api.options(SunflowAPI.DEFAULT_OPTIONS); - } - } - - private void parseCameraTransform(SunflowAPIInterface api) throws ParserException, IOException { - if (p.peekNextToken("steps")) { - // motion blur camera - int n = p.getNextInt(); - api.parameter("transform.steps", n); - // parse time extents - p.checkNextToken("times"); - float t0 = p.getNextFloat(); - float t1 = p.getNextFloat(); - api.parameter("transform.times", "float", "none", new float[] { t0, - t1 }); - for (int i = 0; i < n; i++) - parseCameraMatrix(i, api); - } else - parseCameraMatrix(-1, api); - } - - private void parseCameraMatrix(int index, SunflowAPIInterface api) throws IOException, ParserException { - String offset = index < 0 ? "" : String.format("[%d]", index); - if (p.peekNextToken("transform")) { - // advanced camera - api.parameter(String.format("transform%s", offset), parseMatrix()); - } else { - if (index >= 0) - p.checkNextToken("{"); - // regular camera specification - p.checkNextToken("eye"); - Point3 eye = parsePoint(); - p.checkNextToken("target"); - Point3 target = parsePoint(); - p.checkNextToken("up"); - Vector3 up = parseVector(); - api.parameter(String.format("transform%s", offset), Matrix4.lookAt(eye, target, up)); - if (index >= 0) - p.checkNextToken("}"); - } - } - - private boolean parseShader(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { - p.checkNextToken("{"); - p.checkNextToken("name"); - String name = p.getNextToken(); - UI.printInfo(Module.API, "Reading shader: %s ...", name); - p.checkNextToken("type"); - if (p.peekNextToken("diffuse")) { - if (p.peekNextToken("diff")) { - api.parameter("diffuse", null, parseColor().getRGB()); - api.shader(name, "diffuse"); - } else if (p.peekNextToken("texture")) { - api.parameter("texture", p.getNextToken()); - api.shader(name, "textured_diffuse"); - } else - UI.printWarning(Module.API, "Unrecognized option in diffuse shader block: %s", p.getNextToken()); - } else if (p.peekNextToken("phong")) { - String tex = null; - if (p.peekNextToken("texture")) - api.parameter("texture", tex = p.getNextToken()); - else { - p.checkNextToken("diff"); - api.parameter("diffuse", null, parseColor().getRGB()); - } - p.checkNextToken("spec"); - api.parameter("specular", null, parseColor().getRGB()); - api.parameter("power", p.getNextFloat()); - if (p.peekNextToken("samples")) - api.parameter("samples", p.getNextInt()); - if (tex != null) - api.shader(name, "textured_phong"); - else - api.shader(name, "phong"); - } else if (p.peekNextToken("amb-occ") || p.peekNextToken("amb-occ2")) { - String tex = null; - if (p.peekNextToken("diff") || p.peekNextToken("bright")) - api.parameter("bright", null, parseColor().getRGB()); - else if (p.peekNextToken("texture")) - api.parameter("texture", tex = p.getNextToken()); - if (p.peekNextToken("dark")) { - api.parameter("dark", null, parseColor().getRGB()); - p.checkNextToken("samples"); - api.parameter("samples", p.getNextInt()); - p.checkNextToken("dist"); - api.parameter("maxdist", p.getNextFloat()); - } - if (tex == null) - api.shader(name, "ambient_occlusion"); - else - api.shader(name, "textured_ambient_occlusion"); - } else if (p.peekNextToken("mirror")) { - p.checkNextToken("refl"); - api.parameter("color", null, parseColor().getRGB()); - api.shader(name, "mirror"); - } else if (p.peekNextToken("glass")) { - p.checkNextToken("eta"); - api.parameter("eta", p.getNextFloat()); - p.checkNextToken("color"); - api.parameter("color", null, parseColor().getRGB()); - if (p.peekNextToken("absorption.distance") || p.peekNextToken("absorbtion.distance")) - api.parameter("absorption.distance", p.getNextFloat()); - if (p.peekNextToken("absorption.color") || p.peekNextToken("absorbtion.color")) - api.parameter("absorption.color", null, parseColor().getRGB()); - api.shader(name, "glass"); - } else if (p.peekNextToken("shiny")) { - String tex = null; - if (p.peekNextToken("texture")) - api.parameter("texture", tex = p.getNextToken()); - else { - p.checkNextToken("diff"); - api.parameter("diffuse", null, parseColor().getRGB()); - } - p.checkNextToken("refl"); - api.parameter("shiny", p.getNextFloat()); - if (tex == null) - api.shader(name, "shiny_diffuse"); - else - api.shader(name, "textured_shiny_diffuse"); - } else if (p.peekNextToken("ward")) { - String tex = null; - if (p.peekNextToken("texture")) - api.parameter("texture", tex = p.getNextToken()); - else { - p.checkNextToken("diff"); - api.parameter("diffuse", null, parseColor().getRGB()); - } - p.checkNextToken("spec"); - api.parameter("specular", null, parseColor().getRGB()); - p.checkNextToken("rough"); - api.parameter("roughnessX", p.getNextFloat()); - api.parameter("roughnessY", p.getNextFloat()); - if (p.peekNextToken("samples")) - api.parameter("samples", p.getNextInt()); - if (tex != null) - api.shader(name, "textured_ward"); - else - api.shader(name, "ward"); - } else if (p.peekNextToken("view-caustics")) { - api.shader(name, "view_caustics"); - } else if (p.peekNextToken("view-irradiance")) { - api.shader(name, "view_irradiance"); - } else if (p.peekNextToken("view-global")) { - api.shader(name, "view_global"); - } else if (p.peekNextToken("constant")) { - // backwards compatibility -- peek only - p.peekNextToken("color"); - api.parameter("color", null, parseColor().getRGB()); - api.shader(name, "constant"); - } else if (p.peekNextToken("janino")) { - String typename = p.peekNextToken("typename") ? p.getNextToken() : PluginRegistry.shaderPlugins.generateUniqueName("janino_shader"); - if (!PluginRegistry.shaderPlugins.registerPlugin(typename, p.getNextCodeBlock())) - return false; - api.shader(name, typename); - } else if (p.peekNextToken("id")) { - api.shader(name, "show_instance_id"); - } else if (p.peekNextToken("uber")) { - if (p.peekNextToken("diff")) - api.parameter("diffuse", null, parseColor().getRGB()); - if (p.peekNextToken("diff.texture")) - api.parameter("diffuse.texture", p.getNextToken()); - if (p.peekNextToken("diff.blend")) - api.parameter("diffuse.blend", p.getNextFloat()); - if (p.peekNextToken("refl") || p.peekNextToken("spec")) - api.parameter("specular", null, parseColor().getRGB()); - if (p.peekNextToken("texture")) { - // deprecated - UI.printWarning(Module.API, "Deprecated uber shader parameter \"texture\" - please use \"diffuse.texture\" and \"diffuse.blend\" instead"); - api.parameter("diffuse.texture", p.getNextToken()); - api.parameter("diffuse.blend", p.getNextFloat()); - } - if (p.peekNextToken("spec.texture")) - api.parameter("specular.texture", p.getNextToken()); - if (p.peekNextToken("spec.blend")) - api.parameter("specular.blend", p.getNextFloat()); - if (p.peekNextToken("glossy")) - api.parameter("glossyness", p.getNextFloat()); - if (p.peekNextToken("samples")) - api.parameter("samples", p.getNextInt()); - api.shader(name, "uber"); - } else - UI.printWarning(Module.API, "Unrecognized shader type: %s", p.getNextToken()); - p.checkNextToken("}"); - return true; - } - - private boolean parseModifier(SunflowAPIInterface api) throws ParserException, IOException { - p.checkNextToken("{"); - p.checkNextToken("name"); - String name = p.getNextToken(); - UI.printInfo(Module.API, "Reading modifier: %s ...", name); - p.checkNextToken("type"); - if (p.peekNextToken("bump")) { - p.checkNextToken("texture"); - api.parameter("texture", p.getNextToken()); - p.checkNextToken("scale"); - api.parameter("scale", p.getNextFloat()); - api.modifier(name, "bump_map"); - } else if (p.peekNextToken("normalmap")) { - p.checkNextToken("texture"); - api.parameter("texture", p.getNextToken()); - api.modifier(name, "normal_map"); - } else if (p.peekNextToken("perlin")) { - p.checkNextToken("function"); - api.parameter("function", p.getNextInt()); - p.checkNextToken("size"); - api.parameter("size", p.getNextFloat()); - p.checkNextToken("scale"); - api.parameter("scale", p.getNextFloat()); - api.modifier(name, "perlin"); - } else { - UI.printWarning(Module.API, "Unrecognized modifier type: %s", p.getNextToken()); - } - p.checkNextToken("}"); - return true; - } - - private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, IOException { - p.checkNextToken("{"); - boolean noInstance = false; - Matrix4[] transform = null; - float transformTime0 = 0, transformTime1 = 0; - String name = null; - String[] shaders = null; - String[] modifiers = null; - if (p.peekNextToken("noinstance")) { - // this indicates that the geometry is to be created, but not - // instanced into the scene - noInstance = true; - } else { - // these are the parameters to be passed to the instance - if (p.peekNextToken("shaders")) { - int n = p.getNextInt(); - shaders = new String[n]; - for (int i = 0; i < n; i++) - shaders[i] = p.getNextToken(); - } else { - p.checkNextToken("shader"); - shaders = new String[] { p.getNextToken() }; - } - if (p.peekNextToken("modifiers")) { - int n = p.getNextInt(); - modifiers = new String[n]; - for (int i = 0; i < n; i++) - modifiers[i] = p.getNextToken(); - } else if (p.peekNextToken("modifier")) - modifiers = new String[] { p.getNextToken() }; - if (p.peekNextToken("transform")) { - if (p.peekNextToken("steps")) { - transform = new Matrix4[p.getNextInt()]; - p.checkNextToken("times"); - transformTime0 = p.getNextFloat(); - transformTime1 = p.getNextFloat(); - for (int i = 0; i < transform.length; i++) - transform[i] = parseMatrix(); - } else - transform = new Matrix4[] { parseMatrix() }; - } - } - if (p.peekNextToken("accel")) - api.parameter("accel", p.getNextToken()); - p.checkNextToken("type"); - String type = p.getNextToken(); - if (p.peekNextToken("name")) - name = p.getNextToken(); - else - name = generateUniqueName(type); - if (type.equals("mesh")) { - UI.printWarning(Module.API, "Deprecated object type: mesh"); - UI.printInfo(Module.API, "Reading mesh: %s ...", name); - int numVertices = p.getNextInt(); - int numTriangles = p.getNextInt(); - float[] points = new float[numVertices * 3]; - float[] normals = new float[numVertices * 3]; - float[] uvs = new float[numVertices * 2]; - for (int i = 0; i < numVertices; i++) { - p.checkNextToken("v"); - points[3 * i + 0] = p.getNextFloat(); - points[3 * i + 1] = p.getNextFloat(); - points[3 * i + 2] = p.getNextFloat(); - normals[3 * i + 0] = p.getNextFloat(); - normals[3 * i + 1] = p.getNextFloat(); - normals[3 * i + 2] = p.getNextFloat(); - uvs[2 * i + 0] = p.getNextFloat(); - uvs[2 * i + 1] = p.getNextFloat(); - } - int[] triangles = new int[numTriangles * 3]; - for (int i = 0; i < numTriangles; i++) { - p.checkNextToken("t"); - triangles[i * 3 + 0] = p.getNextInt(); - triangles[i * 3 + 1] = p.getNextInt(); - triangles[i * 3 + 2] = p.getNextInt(); - } - // create geometry - api.parameter("triangles", triangles); - api.parameter("points", "point", "vertex", points); - api.parameter("normals", "vector", "vertex", normals); - api.parameter("uvs", "texcoord", "vertex", uvs); - api.geometry(name, "triangle_mesh"); - } else if (type.equals("flat-mesh")) { - UI.printWarning(Module.API, "Deprecated object type: flat-mesh"); - UI.printInfo(Module.API, "Reading flat mesh: %s ...", name); - int numVertices = p.getNextInt(); - int numTriangles = p.getNextInt(); - float[] points = new float[numVertices * 3]; - float[] uvs = new float[numVertices * 2]; - for (int i = 0; i < numVertices; i++) { - p.checkNextToken("v"); - points[3 * i + 0] = p.getNextFloat(); - points[3 * i + 1] = p.getNextFloat(); - points[3 * i + 2] = p.getNextFloat(); - p.getNextFloat(); - p.getNextFloat(); - p.getNextFloat(); - uvs[2 * i + 0] = p.getNextFloat(); - uvs[2 * i + 1] = p.getNextFloat(); - } - int[] triangles = new int[numTriangles * 3]; - for (int i = 0; i < numTriangles; i++) { - p.checkNextToken("t"); - triangles[i * 3 + 0] = p.getNextInt(); - triangles[i * 3 + 1] = p.getNextInt(); - triangles[i * 3 + 2] = p.getNextInt(); - } - // create geometry - api.parameter("triangles", triangles); - api.parameter("points", "point", "vertex", points); - api.parameter("uvs", "texcoord", "vertex", uvs); - api.geometry(name, "triangle_mesh"); - } else if (type.equals("sphere")) { - UI.printInfo(Module.API, "Reading sphere ..."); - api.geometry(name, "sphere"); - if (transform == null && !noInstance) { - // legacy method of specifying transformation for spheres - p.checkNextToken("c"); - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - p.checkNextToken("r"); - float radius = p.getNextFloat(); - api.parameter("transform", Matrix4.translation(x, y, z).multiply(Matrix4.scale(radius))); - api.parameter("shaders", shaders); - if (modifiers != null) - api.parameter("modifiers", modifiers); - api.instance(name + ".instance", name); - // disable future instancing - instance has already been created - noInstance = true; - } - } else if (type.equals("cylinder")) { - UI.printInfo(Module.API, "Reading cylinder ..."); - api.geometry(name, "cylinder"); - } else if (type.equals("banchoff")) { - UI.printInfo(Module.API, "Reading banchoff ..."); - api.geometry(name, "banchoff"); - } else if (type.equals("torus")) { - UI.printInfo(Module.API, "Reading torus ..."); - p.checkNextToken("r"); - api.parameter("radiusInner", p.getNextFloat()); - api.parameter("radiusOuter", p.getNextFloat()); - api.geometry(name, "torus"); - } else if (type.equals("sphereflake")) { - UI.printInfo(Module.API, "Reading sphereflake ..."); - if (p.peekNextToken("level")) - api.parameter("level", p.getNextInt()); - if (p.peekNextToken("axis")) - api.parameter("axis", parseVector()); - if (p.peekNextToken("radius")) - api.parameter("radius", p.getNextFloat()); - api.geometry(name, "sphereflake"); - } else if (type.equals("plane")) { - UI.printInfo(Module.API, "Reading plane ..."); - p.checkNextToken("p"); - api.parameter("center", parsePoint()); - if (p.peekNextToken("n")) { - api.parameter("normal", parseVector()); - } else { - p.checkNextToken("p"); - api.parameter("point1", parsePoint()); - p.checkNextToken("p"); - api.parameter("point2", parsePoint()); - } - api.geometry(name, "plane"); - } else if (type.equals("generic-mesh")) { - UI.printInfo(Module.API, "Reading generic mesh: %s ... ", name); - // parse vertices - p.checkNextToken("points"); - int np = p.getNextInt(); - api.parameter("points", "point", "vertex", parseFloatArray(np * 3)); - // parse triangle indices - p.checkNextToken("triangles"); - int nt = p.getNextInt(); - api.parameter("triangles", parseIntArray(nt * 3)); - // parse normals - p.checkNextToken("normals"); - if (p.peekNextToken("vertex")) - api.parameter("normals", "vector", "vertex", parseFloatArray(np * 3)); - else if (p.peekNextToken("facevarying")) - api.parameter("normals", "vector", "facevarying", parseFloatArray(nt * 9)); - else - p.checkNextToken("none"); - // parse texture coordinates - p.checkNextToken("uvs"); - if (p.peekNextToken("vertex")) - api.parameter("uvs", "texcoord", "vertex", parseFloatArray(np * 2)); - else if (p.peekNextToken("facevarying")) - api.parameter("uvs", "texcoord", "facevarying", parseFloatArray(nt * 6)); - else - p.checkNextToken("none"); - if (p.peekNextToken("face_shaders")) - api.parameter("faceshaders", parseIntArray(nt)); - api.geometry(name, "triangle_mesh"); - } else if (type.equals("hair")) { - UI.printInfo(Module.API, "Reading hair curves: %s ... ", name); - p.checkNextToken("segments"); - api.parameter("segments", p.getNextInt()); - p.checkNextToken("width"); - api.parameter("widths", p.getNextFloat()); - p.checkNextToken("points"); - api.parameter("points", "point", "vertex", parseFloatArray(p.getNextInt())); - api.geometry(name, "hair"); - } else if (type.equals("janino-tesselatable")) { - UI.printInfo(Module.API, "Reading procedural primitive: %s ... ", name); - String typename = p.peekNextToken("typename") ? p.getNextToken() : PluginRegistry.shaderPlugins.generateUniqueName("janino_shader"); - if (!PluginRegistry.tesselatablePlugins.registerPlugin(typename, p.getNextCodeBlock())) - noInstance = true; - else - api.geometry(name, typename); - } else if (type.equals("teapot")) { - UI.printInfo(Module.API, "Reading teapot: %s ... ", name); - if (p.peekNextToken("subdivs")) - api.parameter("subdivs", p.getNextInt()); - if (p.peekNextToken("smooth")) - api.parameter("smooth", p.getNextBoolean()); - api.geometry(name, "teapot"); - } else if (type.equals("gumbo")) { - UI.printInfo(Module.API, "Reading gumbo: %s ... ", name); - if (p.peekNextToken("subdivs")) - api.parameter("subdivs", p.getNextInt()); - if (p.peekNextToken("smooth")) - api.parameter("smooth", p.getNextBoolean()); - api.geometry(name, "gumbo"); - } else if (type.equals("julia")) { - UI.printInfo(Module.API, "Reading julia fractal: %s ... ", name); - if (p.peekNextToken("q")) { - api.parameter("cw", p.getNextFloat()); - api.parameter("cx", p.getNextFloat()); - api.parameter("cy", p.getNextFloat()); - api.parameter("cz", p.getNextFloat()); - } - if (p.peekNextToken("iterations")) - api.parameter("iterations", p.getNextInt()); - if (p.peekNextToken("epsilon")) - api.parameter("epsilon", p.getNextFloat()); - api.geometry(name, "julia"); - } else if (type.equals("particles") || type.equals("dlasurface")) { - if (type.equals("dlasurface")) - UI.printWarning(Module.API, "Deprecated object type: \"dlasurface\" - please use \"particles\" instead"); - float[] data; - if (p.peekNextToken("filename")) { - // FIXME: this code should be moved into an on demand loading - // primitive - String filename = p.getNextToken(); - boolean littleEndian = false; - if (p.peekNextToken("little_endian")) - littleEndian = true; - UI.printInfo(Module.USER, "Loading particle file: %s", filename); - File file = new File(filename); - FileInputStream stream = new FileInputStream(filename); - MappedByteBuffer map = stream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()); - if (littleEndian) - map.order(ByteOrder.LITTLE_ENDIAN); - FloatBuffer buffer = map.asFloatBuffer(); - data = new float[buffer.capacity()]; - for (int i = 0; i < data.length; i++) - data[i] = buffer.get(i); - stream.close(); - } else { - p.checkNextToken("points"); - int n = p.getNextInt(); - data = parseFloatArray(n * 3); // read 3n points - } - api.parameter("particles", "point", "vertex", data); - if (p.peekNextToken("num")) - api.parameter("num", p.getNextInt()); - else - api.parameter("num", data.length / 3); - p.checkNextToken("radius"); - api.parameter("radius", p.getNextFloat()); - api.geometry(name, "particles"); - } else if (type.equals("file-mesh")) { - UI.printInfo(Module.API, "Reading file mesh: %s ... ", name); - p.checkNextToken("filename"); - api.parameter("filename", p.getNextToken()); - if (p.peekNextToken("smooth_normals")) - api.parameter("smooth_normals", p.getNextBoolean()); - api.geometry(name, "file_mesh"); - } else if (type.equals("bezier-mesh")) { - UI.printInfo(Module.API, "Reading bezier mesh: %s ... ", name); - p.checkNextToken("n"); - int nu, nv; - api.parameter("nu", nu = p.getNextInt()); - api.parameter("nv", nv = p.getNextInt()); - if (p.peekNextToken("wrap")) { - api.parameter("uwrap", p.getNextBoolean()); - api.parameter("vwrap", p.getNextBoolean()); - } - p.checkNextToken("points"); - float[] points = new float[3 * nu * nv]; - for (int i = 0; i < points.length; i++) - points[i] = p.getNextFloat(); - api.parameter("points", "point", "vertex", points); - if (p.peekNextToken("subdivs")) - api.parameter("subdivs", p.getNextInt()); - if (p.peekNextToken("smooth")) - api.parameter("smooth", p.getNextBoolean()); - api.geometry(name, "bezier_mesh"); - } else { - UI.printWarning(Module.API, "Unrecognized object type: %s", p.getNextToken()); - noInstance = true; - } - if (!noInstance) { - // create instance - api.parameter("shaders", shaders); - if (modifiers != null) - api.parameter("modifiers", modifiers); - if (transform != null && transform.length > 0) { - if (transform.length == 1) - api.parameter("transform", transform[0]); - else { - api.parameter("transform.steps", transform.length); - api.parameter("transform.times", "float", "none", new float[] { - transformTime0, transformTime1 }); - for (int i = 0; i < transform.length; i++) - api.parameter(String.format("transform[%d]", i), transform[i]); - } - } - api.instance(name + ".instance", name); - } - p.checkNextToken("}"); - } - - private void parseInstanceBlock(SunflowAPIInterface api) throws ParserException, IOException { - p.checkNextToken("{"); - p.checkNextToken("name"); - String name = p.getNextToken(); - UI.printInfo(Module.API, "Reading instance: %s ...", name); - p.checkNextToken("geometry"); - String geoname = p.getNextToken(); - p.checkNextToken("transform"); - if (p.peekNextToken("steps")) { - int n = p.getNextInt(); - api.parameter("transform.steps", n); - p.checkNextToken("times"); - float[] times = new float[2]; - times[0] = p.getNextFloat(); - times[1] = p.getNextFloat(); - api.parameter("transform.times", "float", "none", times); - for (int i = 0; i < n; i++) - api.parameter(String.format("transform[%d]", i), parseMatrix()); - } else - api.parameter("transform", parseMatrix()); - String[] shaders; - if (p.peekNextToken("shaders")) { - int n = p.getNextInt(); - shaders = new String[n]; - for (int i = 0; i < n; i++) - shaders[i] = p.getNextToken(); - } else { - p.checkNextToken("shader"); - shaders = new String[] { p.getNextToken() }; - } - api.parameter("shaders", shaders); - String[] modifiers = null; - if (p.peekNextToken("modifiers")) { - int n = p.getNextInt(); - modifiers = new String[n]; - for (int i = 0; i < n; i++) - modifiers[i] = p.getNextToken(); - } else if (p.peekNextToken("modifier")) - modifiers = new String[] { p.getNextToken() }; - if (modifiers != null) - api.parameter("modifiers", modifiers); - api.instance(name, geoname); - p.checkNextToken("}"); - } - - private void parseLightBlock(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { - p.checkNextToken("{"); - p.checkNextToken("type"); - if (p.peekNextToken("mesh")) { - UI.printWarning(Module.API, "Deprecated light type: mesh"); - p.checkNextToken("name"); - String name = p.getNextToken(); - UI.printInfo(Module.API, "Reading light mesh: %s ...", name); - p.checkNextToken("emit"); - api.parameter("radiance", null, parseColor().getRGB()); - int samples = numLightSamples; - if (p.peekNextToken("samples")) - samples = p.getNextInt(); - else - UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", samples); - api.parameter("samples", samples); - int numVertices = p.getNextInt(); - int numTriangles = p.getNextInt(); - float[] points = new float[3 * numVertices]; - int[] triangles = new int[3 * numTriangles]; - for (int i = 0; i < numVertices; i++) { - p.checkNextToken("v"); - points[3 * i + 0] = p.getNextFloat(); - points[3 * i + 1] = p.getNextFloat(); - points[3 * i + 2] = p.getNextFloat(); - // ignored - p.getNextFloat(); - p.getNextFloat(); - p.getNextFloat(); - p.getNextFloat(); - p.getNextFloat(); - } - for (int i = 0; i < numTriangles; i++) { - p.checkNextToken("t"); - triangles[3 * i + 0] = p.getNextInt(); - triangles[3 * i + 1] = p.getNextInt(); - triangles[3 * i + 2] = p.getNextInt(); - } - api.parameter("points", "point", "vertex", points); - api.parameter("triangles", triangles); - api.light(name, "triangle_mesh"); - } else if (p.peekNextToken("point")) { - UI.printInfo(Module.API, "Reading point light ..."); - Color pow; - if (p.peekNextToken("color")) { - pow = parseColor(); - p.checkNextToken("power"); - float po = p.getNextFloat(); - pow.mul(po); - } else { - UI.printWarning(Module.API, "Deprecated color specification - please use color and power instead"); - p.checkNextToken("power"); - pow = parseColor(); - } - p.checkNextToken("p"); - api.parameter("center", parsePoint()); - api.parameter("power", null, pow.getRGB()); - api.light(generateUniqueName("pointlight"), "point"); - } else if (p.peekNextToken("spherical")) { - UI.printInfo(Module.API, "Reading spherical light ..."); - p.checkNextToken("color"); - Color pow = parseColor(); - p.checkNextToken("radiance"); - pow.mul(p.getNextFloat()); - api.parameter("radiance", null, pow.getRGB()); - p.checkNextToken("center"); - api.parameter("center", parsePoint()); - p.checkNextToken("radius"); - api.parameter("radius", p.getNextFloat()); - p.checkNextToken("samples"); - api.parameter("samples", p.getNextInt()); - api.light(generateUniqueName("spherelight"), "sphere"); - } else if (p.peekNextToken("directional")) { - UI.printInfo(Module.API, "Reading directional light ..."); - p.checkNextToken("source"); - Point3 s = parsePoint(); - api.parameter("source", s); - p.checkNextToken("target"); - Point3 t = parsePoint(); - api.parameter("dir", Point3.sub(t, s, new Vector3())); - p.checkNextToken("radius"); - api.parameter("radius", p.getNextFloat()); - p.checkNextToken("emit"); - Color e = parseColor(); - if (p.peekNextToken("intensity")) { - float i = p.getNextFloat(); - e.mul(i); - } else - UI.printWarning(Module.API, "Deprecated color specification - please use emit and intensity instead"); - api.parameter("radiance", null, e.getRGB()); - api.light(generateUniqueName("dirlight"), "directional"); - } else if (p.peekNextToken("ibl")) { - UI.printInfo(Module.API, "Reading image based light ..."); - p.checkNextToken("image"); - api.parameter("texture", p.getNextToken()); - p.checkNextToken("center"); - api.parameter("center", parseVector()); - p.checkNextToken("up"); - api.parameter("up", parseVector()); - p.checkNextToken("lock"); - api.parameter("fixed", p.getNextBoolean()); - int samples = numLightSamples; - if (p.peekNextToken("samples")) - samples = p.getNextInt(); - else - UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", samples); - api.parameter("samples", samples); - if (p.peekNextToken("lowsamples")) - api.parameter("lowsamples", p.getNextInt()); - else - api.parameter("lowsamples", samples); - api.light(generateUniqueName("ibl"), "ibl"); - } else if (p.peekNextToken("meshlight")) { - p.checkNextToken("name"); - String name = p.getNextToken(); - UI.printInfo(Module.API, "Reading meshlight: %s ...", name); - p.checkNextToken("emit"); - Color e = parseColor(); - if (p.peekNextToken("radiance")) { - float r = p.getNextFloat(); - e.mul(r); - } else - UI.printWarning(Module.API, "Deprecated color specification - please use emit and radiance instead"); - api.parameter("radiance", null, e.getRGB()); - int samples = numLightSamples; - if (p.peekNextToken("samples")) - samples = p.getNextInt(); - else - UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", samples); - api.parameter("samples", samples); - // parse vertices - p.checkNextToken("points"); - int np = p.getNextInt(); - api.parameter("points", "point", "vertex", parseFloatArray(np * 3)); - // parse triangle indices - p.checkNextToken("triangles"); - int nt = p.getNextInt(); - api.parameter("triangles", parseIntArray(nt * 3)); - api.light(name, "triangle_mesh"); - } else if (p.peekNextToken("sunsky")) { - p.checkNextToken("up"); - api.parameter("up", parseVector()); - p.checkNextToken("east"); - api.parameter("east", parseVector()); - p.checkNextToken("sundir"); - api.parameter("sundir", parseVector()); - p.checkNextToken("turbidity"); - api.parameter("turbidity", p.getNextFloat()); - if (p.peekNextToken("samples")) - api.parameter("samples", p.getNextInt()); - if (p.peekNextToken("ground.extendsky")) - api.parameter("ground.extendsky", p.getNextBoolean()); - else if (p.peekNextToken("ground.color")) - api.parameter("ground.color", null, parseColor().getRGB()); - api.light(generateUniqueName("sunsky"), "sunsky"); - } else if (p.peekNextToken("cornellbox")) { - UI.printInfo(Module.API, "Reading cornell box ..."); - p.checkNextToken("corner0"); - api.parameter("corner0", parsePoint()); - p.checkNextToken("corner1"); - api.parameter("corner1", parsePoint()); - p.checkNextToken("left"); - api.parameter("leftColor", null, parseColor().getRGB()); - p.checkNextToken("right"); - api.parameter("rightColor", null, parseColor().getRGB()); - p.checkNextToken("top"); - api.parameter("topColor", null, parseColor().getRGB()); - p.checkNextToken("bottom"); - api.parameter("bottomColor", null, parseColor().getRGB()); - p.checkNextToken("back"); - api.parameter("backColor", null, parseColor().getRGB()); - p.checkNextToken("emit"); - api.parameter("radiance", null, parseColor().getRGB()); - if (p.peekNextToken("samples")) - api.parameter("samples", p.getNextInt()); - api.light(generateUniqueName("cornellbox"), "cornell_box"); - } else - UI.printWarning(Module.API, "Unrecognized object type: %s", p.getNextToken()); - p.checkNextToken("}"); - } - - private Color parseColor() throws IOException, ParserException, ColorSpecificationException { - if (p.peekNextToken("{")) { - String space = p.getNextToken(); - int req = ColorFactory.getRequiredDataValues(space); - if (req == -2) { - UI.printWarning(Module.API, "Unrecognized color space: %s", space); - return null; - } else if (req == -1) { - // array required, parse how many values are required - req = p.getNextInt(); - } - Color c = ColorFactory.createColor(space, parseFloatArray(req)); - p.checkNextToken("}"); - return c; - } else { - float r = p.getNextFloat(); - float g = p.getNextFloat(); - float b = p.getNextFloat(); - return ColorFactory.createColor(null, r, g, b); - } - } - - private Point3 parsePoint() throws IOException { - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - return new Point3(x, y, z); - } - - private Vector3 parseVector() throws IOException { - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - return new Vector3(x, y, z); - } - - private int[] parseIntArray(int size) throws IOException { - int[] data = new int[size]; - for (int i = 0; i < size; i++) - data[i] = p.getNextInt(); - return data; - } - - private float[] parseFloatArray(int size) throws IOException { - float[] data = new float[size]; - for (int i = 0; i < size; i++) - data[i] = p.getNextFloat(); - return data; - } - - private Matrix4 parseMatrix() throws IOException, ParserException { - if (p.peekNextToken("row")) { - return new Matrix4(parseFloatArray(16), true); - } else if (p.peekNextToken("col")) { - return new Matrix4(parseFloatArray(16), false); - } else { - Matrix4 m = Matrix4.IDENTITY; - p.checkNextToken("{"); - while (!p.peekNextToken("}")) { - Matrix4 t = null; - if (p.peekNextToken("translate")) { - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - t = Matrix4.translation(x, y, z); - } else if (p.peekNextToken("scaleu")) { - float s = p.getNextFloat(); - t = Matrix4.scale(s); - } else if (p.peekNextToken("scale")) { - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - t = Matrix4.scale(x, y, z); - } else if (p.peekNextToken("rotatex")) { - float angle = p.getNextFloat(); - t = Matrix4.rotateX((float) Math.toRadians(angle)); - } else if (p.peekNextToken("rotatey")) { - float angle = p.getNextFloat(); - t = Matrix4.rotateY((float) Math.toRadians(angle)); - } else if (p.peekNextToken("rotatez")) { - float angle = p.getNextFloat(); - t = Matrix4.rotateZ((float) Math.toRadians(angle)); - } else if (p.peekNextToken("rotate")) { - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - float angle = p.getNextFloat(); - t = Matrix4.rotate(x, y, z, (float) Math.toRadians(angle)); - } else - UI.printWarning(Module.API, "Unrecognized transformation type: %s", p.getNextToken()); - if (t != null) - m = t.multiply(m); - } - return m; - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/parser/ShaveRibParser.java b/src/org/sunflow/core/parser/ShaveRibParser.java deleted file mode 100644 index 5c2a3cb..0000000 --- a/src/org/sunflow/core/parser/ShaveRibParser.java +++ /dev/null @@ -1,165 +0,0 @@ -package org.sunflow.core.parser; - -import java.io.FileNotFoundException; -import java.io.IOException; - -import org.sunflow.SunflowAPIInterface; -import org.sunflow.core.SceneParser; -import org.sunflow.system.Parser; -import org.sunflow.system.UI; -import org.sunflow.system.Parser.ParserException; -import org.sunflow.system.UI.Module; -import org.sunflow.util.FloatArray; -import org.sunflow.util.IntArray; - -public class ShaveRibParser implements SceneParser { - public boolean parse(String filename, SunflowAPIInterface api) { - try { - Parser p = new Parser(filename); - p.checkNextToken("version"); - p.checkNextToken("3.04"); - p.checkNextToken("TransformBegin"); - - if (p.peekNextToken("Procedural")) { - // read procedural shave rib - boolean done = false; - while (!done) { - p.checkNextToken("DelayedReadArchive"); - p.checkNextToken("["); - String f = p.getNextToken(); - UI.printInfo(Module.USER, "RIB - Reading voxel: \"%s\" ...", f); - api.include(f); - p.checkNextToken("]"); - while (true) { - String t = p.getNextToken(); - if (t == null || t.equals("TransformEnd")) { - done = true; - break; - } else if (t.equals("Procedural")) - break; - } - } - return true; - } - - boolean cubic = false; - if (p.peekNextToken("Basis")) { - cubic = true; - // u basis - p.checkNextToken("catmull-rom"); - p.checkNextToken("1"); - // v basis - p.checkNextToken("catmull-rom"); - p.checkNextToken("1"); - } - while (p.peekNextToken("Declare")) { - p.getNextToken(); // name - p.getNextToken(); // interpolation & type - } - int index = 0; - boolean done = false; - p.checkNextToken("Curves"); - do { - if (cubic) - p.checkNextToken("cubic"); - else - p.checkNextToken("linear"); - int[] nverts = parseIntArray(p); - for (int i = 1; i < nverts.length; i++) { - if (nverts[0] != nverts[i]) { - UI.printError(Module.USER, "RIB - Found variable number of hair segments"); - return false; - } - } - int nhairs = nverts.length; - - UI.printInfo(Module.USER, "RIB - Parsed %d hair curves", nhairs); - - api.parameter("segments", nverts[0] - 1); - - p.checkNextToken("nonperiodic"); - p.checkNextToken("P"); - float[] points = parseFloatArray(p); - if (points.length != 3 * nhairs * nverts[0]) { - UI.printError(Module.USER, "RIB - Invalid number of points - expecting %d - found %d", nhairs * nverts[0], points.length / 3); - return false; - } - api.parameter("points", "point", "vertex", points); - - UI.printInfo(Module.USER, "RIB - Parsed %d hair vertices", points.length / 3); - - p.checkNextToken("width"); - float[] w = parseFloatArray(p); - if (w.length != nhairs * nverts[0]) { - UI.printError(Module.USER, "RIB - Invalid number of hair widths - expecting %d - found %d", nhairs * nverts[0], w.length); - return false; - } - api.parameter("widths", "float", "vertex", w); - - UI.printInfo(Module.USER, "RIB - Parsed %d hair widths", w.length); - - String name = String.format("%s[%d]", filename, index); - UI.printInfo(Module.USER, "RIB - Creating hair object \"%s\"", name); - api.geometry(name, "hair"); - api.instance(name + ".instance", name); - - UI.printInfo(Module.USER, "RIB - Searching for next curve group ..."); - while (true) { - String t = p.getNextToken(); - if (t == null || t.equals("TransformEnd")) { - done = true; - break; - } else if (t.equals("Curves")) - break; - } - index++; - } while (!done); - UI.printInfo(Module.USER, "RIB - Finished reading rib file"); - } catch (FileNotFoundException e) { - UI.printError(Module.USER, "RIB - File not found: %s", filename); - e.printStackTrace(); - return false; - } catch (ParserException e) { - UI.printError(Module.USER, "RIB - Parser exception: %s", e); - e.printStackTrace(); - return false; - } catch (IOException e) { - UI.printError(Module.USER, "RIB - I/O exception: %s", e); - e.printStackTrace(); - return false; - } - return true; - } - - private int[] parseIntArray(Parser p) throws IOException { - IntArray array = new IntArray(); - boolean done = false; - do { - String s = p.getNextToken(); - if (s.startsWith("[")) - s = s.substring(1); - if (s.endsWith("]")) { - s = s.substring(0, s.length() - 1); - done = true; - } - array.add(Integer.parseInt(s)); - } while (!done); - return array.trim(); - } - - private float[] parseFloatArray(Parser p) throws IOException { - FloatArray array = new FloatArray(); - boolean done = false; - do { - String s = p.getNextToken(); - if (s.startsWith("[")) - s = s.substring(1); - if (s.endsWith("]")) { - s = s.substring(0, s.length() - 1); - done = true; - } - array.add(Float.parseFloat(s)); - } while (!done); - return array.trim(); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/parser/TriParser.java b/src/org/sunflow/core/parser/TriParser.java deleted file mode 100644 index a404c2f..0000000 --- a/src/org/sunflow/core/parser/TriParser.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.sunflow.core.parser; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.ByteOrder; -import java.nio.FloatBuffer; -import java.nio.IntBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel.MapMode; - -import org.sunflow.SunflowAPIInterface; -import org.sunflow.core.SceneParser; -import org.sunflow.system.Parser; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class TriParser implements SceneParser { - public boolean parse(String filename, SunflowAPIInterface api) { - try { - UI.printInfo(Module.USER, "TRI - Reading geometry: \"%s\" ...", filename); - Parser p = new Parser(filename); - float[] verts = new float[3 * p.getNextInt()]; - for (int v = 0; v < verts.length; v += 3) { - verts[v + 0] = p.getNextFloat(); - verts[v + 1] = p.getNextFloat(); - verts[v + 2] = p.getNextFloat(); - p.getNextToken(); - p.getNextToken(); - } - int[] triangles = new int[p.getNextInt() * 3]; - for (int t = 0; t < triangles.length; t += 3) { - triangles[t + 0] = p.getNextInt(); - triangles[t + 1] = p.getNextInt(); - triangles[t + 2] = p.getNextInt(); - } - - // create geometry - api.parameter("triangles", triangles); - api.parameter("points", "point", "vertex", verts); - api.geometry(filename, "triangle_mesh"); - - // create shader - api.shader(filename + ".shader", "simple"); - api.parameter("shaders", filename + ".shader"); - - // create instance - api.instance(filename + ".instance", filename); - - p.close(); - // output to ra3 format - RandomAccessFile stream = new RandomAccessFile(filename.replace(".tri", ".ra3"), "rw"); - MappedByteBuffer map = stream.getChannel().map(MapMode.READ_WRITE, 0, 8 + 4 * (verts.length + triangles.length)); - map.order(ByteOrder.LITTLE_ENDIAN); - IntBuffer ints = map.asIntBuffer(); - FloatBuffer floats = map.asFloatBuffer(); - ints.put(0, verts.length / 3); - ints.put(1, triangles.length / 3); - for (int i = 0; i < verts.length; i++) - floats.put(2 + i, verts[i]); - for (int i = 0; i < triangles.length; i++) - ints.put(2 + verts.length + i, triangles[i]); - stream.close(); - } catch (FileNotFoundException e) { - e.printStackTrace(); - return false; - } catch (IOException e) { - e.printStackTrace(); - return false; - } - return true; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/photonmap/CausticPhotonMap.java b/src/org/sunflow/core/photonmap/CausticPhotonMap.java deleted file mode 100644 index 27caa09..0000000 --- a/src/org/sunflow/core/photonmap/CausticPhotonMap.java +++ /dev/null @@ -1,401 +0,0 @@ -package org.sunflow.core.photonmap; - -import java.util.ArrayList; - -import org.sunflow.core.CausticPhotonMapInterface; -import org.sunflow.core.LightSample; -import org.sunflow.core.Options; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public final class CausticPhotonMap implements CausticPhotonMapInterface { - private ArrayList photonList; - private Photon[] photons; - private int storedPhotons; - private int halfStoredPhotons; - private int log2n; - private int gatherNum; - private float gatherRadius; - private BoundingBox bounds; - private float filterValue; - private float maxPower; - private float maxRadius; - private int numEmit; - - public void prepare(Options options, BoundingBox sceneBounds) { - // get options - numEmit = options.getInt("caustics.emit", 10000); - gatherNum = options.getInt("caustics.gather", 50); - gatherRadius = options.getFloat("caustics.radius", 0.5f); - filterValue = options.getFloat("caustics.filter", 1.1f); - // init - bounds = new BoundingBox(); - maxPower = 0; - maxRadius = 0; - photonList = new ArrayList(); - photonList.add(null); - photons = null; - storedPhotons = halfStoredPhotons = 0; - } - - private void locatePhotons(NearestPhotons np) { - float[] dist1d2 = new float[log2n]; - int[] chosen = new int[log2n]; - int i = 1; - int level = 0; - int cameFrom; - while (true) { - while (i < halfStoredPhotons) { - float dist1d = photons[i].getDist1(np.px, np.py, np.pz); - dist1d2[level] = dist1d * dist1d; - i += i; - if (dist1d > 0.0f) - i++; - chosen[level++] = i; - } - np.checkAddNearest(photons[i]); - do { - cameFrom = i; - i >>= 1; - level--; - if (i == 0) - return; - } while ((dist1d2[level] >= np.dist2[0]) || (cameFrom != chosen[level])); - np.checkAddNearest(photons[i]); - i = chosen[level++] ^ 1; - } - } - - private void balance() { - if (storedPhotons == 0) - return; - photons = photonList.toArray(new Photon[photonList.size()]); - photonList = null; - Photon[] temp = new Photon[storedPhotons + 1]; - balanceSegment(temp, 1, 1, storedPhotons); - photons = temp; - halfStoredPhotons = storedPhotons / 2; - log2n = (int) Math.ceil(Math.log(storedPhotons) / Math.log(2.0)); - } - - private void balanceSegment(Photon[] temp, int index, int start, int end) { - int median = 1; - while ((4 * median) <= (end - start + 1)) - median += median; - if ((3 * median) <= (end - start + 1)) { - median += median; - median += (start - 1); - } else - median = end - median + 1; - int axis = Photon.SPLIT_Z; - Vector3 extents = bounds.getExtents(); - if ((extents.x > extents.y) && (extents.x > extents.z)) - axis = Photon.SPLIT_X; - else if (extents.y > extents.z) - axis = Photon.SPLIT_Y; - int left = start; - int right = end; - while (right > left) { - double v = photons[right].getCoord(axis); - int i = left - 1; - int j = right; - while (true) { - while (photons[++i].getCoord(axis) < v) { - } - while ((photons[--j].getCoord(axis) > v) && (j > left)) { - } - if (i >= j) - break; - swap(i, j); - } - swap(i, right); - if (i >= median) - right = i - 1; - if (i <= median) - left = i + 1; - } - temp[index] = photons[median]; - temp[index].setSplitAxis(axis); - if (median > start) { - if (start < (median - 1)) { - float tmp; - switch (axis) { - case Photon.SPLIT_X: - tmp = bounds.getMaximum().x; - bounds.getMaximum().x = temp[index].x; - balanceSegment(temp, 2 * index, start, median - 1); - bounds.getMaximum().x = tmp; - break; - case Photon.SPLIT_Y: - tmp = bounds.getMaximum().y; - bounds.getMaximum().y = temp[index].y; - balanceSegment(temp, 2 * index, start, median - 1); - bounds.getMaximum().y = tmp; - break; - default: - tmp = bounds.getMaximum().z; - bounds.getMaximum().z = temp[index].z; - balanceSegment(temp, 2 * index, start, median - 1); - bounds.getMaximum().z = tmp; - } - } else - temp[2 * index] = photons[start]; - } - if (median < end) { - if ((median + 1) < end) { - float tmp; - switch (axis) { - case Photon.SPLIT_X: - tmp = bounds.getMinimum().x; - bounds.getMinimum().x = temp[index].x; - balanceSegment(temp, (2 * index) + 1, median + 1, end); - bounds.getMinimum().x = tmp; - break; - case Photon.SPLIT_Y: - tmp = bounds.getMinimum().y; - bounds.getMinimum().y = temp[index].y; - balanceSegment(temp, (2 * index) + 1, median + 1, end); - bounds.getMinimum().y = tmp; - break; - default: - tmp = bounds.getMinimum().z; - bounds.getMinimum().z = temp[index].z; - balanceSegment(temp, (2 * index) + 1, median + 1, end); - bounds.getMinimum().z = tmp; - } - } else - temp[(2 * index) + 1] = photons[end]; - } - } - - private void swap(int i, int j) { - Photon tmp = photons[i]; - photons[i] = photons[j]; - photons[j] = tmp; - } - - public void store(ShadingState state, Vector3 dir, Color power, Color diffuse) { - if (((state.getDiffuseDepth() == 0) && (state.getReflectionDepth() > 0 || state.getRefractionDepth() > 0))) { - // this is a caustic photon - Photon p = new Photon(state.getPoint(), dir, power); - synchronized (this) { - storedPhotons++; - photonList.add(p); - bounds.include(new Point3(p.x, p.y, p.z)); - maxPower = Math.max(maxPower, power.getMax()); - } - } - } - - public void init() { - UI.printInfo(Module.LIGHT, "Balancing caustics photon map ..."); - Timer t = new Timer(); - t.start(); - balance(); - t.end(); - UI.printInfo(Module.LIGHT, "Caustic photon map:"); - UI.printInfo(Module.LIGHT, " * Photons stored: %d", storedPhotons); - UI.printInfo(Module.LIGHT, " * Photons/estimate: %d", gatherNum); - maxRadius = 1.4f * (float) Math.sqrt(maxPower * gatherNum); - UI.printInfo(Module.LIGHT, " * Estimate radius: %.3f", gatherRadius); - UI.printInfo(Module.LIGHT, " * Maximum radius: %.3f", maxRadius); - UI.printInfo(Module.LIGHT, " * Balancing time: %s", t.toString()); - if (gatherRadius > maxRadius) - gatherRadius = maxRadius; - } - - public void getSamples(ShadingState state) { - if (storedPhotons == 0) - return; - NearestPhotons np = new NearestPhotons(state.getPoint(), gatherNum, gatherRadius * gatherRadius); - locatePhotons(np); - if (np.found < 8) - return; - Point3 ppos = new Point3(); - Vector3 pdir = new Vector3(); - Vector3 pvec = new Vector3(); - float invArea = 1.0f / ((float) Math.PI * np.dist2[0]); - float maxNDist = np.dist2[0] * 0.05f; - float f2r2 = 1.0f / (filterValue * filterValue * np.dist2[0]); - float fInv = 1.0f / (1.0f - 2.0f / (3.0f * filterValue)); - for (int i = 1; i <= np.found; i++) { - Photon phot = np.index[i]; - Vector3.decode(phot.dir, pdir); - float cos = -Vector3.dot(pdir, state.getNormal()); - if (cos > 0.001) { - ppos.set(phot.x, phot.y, phot.z); - Point3.sub(ppos, state.getPoint(), pvec); - float pcos = Vector3.dot(pvec, state.getNormal()); - if ((pcos < maxNDist) && (pcos > -maxNDist)) { - LightSample sample = new LightSample(); - sample.setShadowRay(new Ray(state.getPoint(), pdir.negate())); - sample.setRadiance(new Color().setRGBE(np.index[i].power).mul(invArea / cos), Color.BLACK); - sample.getDiffuseRadiance().mul((1.0f - (float) Math.sqrt(np.dist2[i] * f2r2)) * fInv); - state.addSample(sample); - } - } - } - } - - private static class NearestPhotons { - int found; - float px, py, pz; - private int max; - private boolean gotHeap; - protected float[] dist2; - protected Photon[] index; - - NearestPhotons(Point3 p, int n, float maxDist2) { - max = n; - found = 0; - gotHeap = false; - px = p.x; - py = p.y; - pz = p.z; - dist2 = new float[n + 1]; - index = new Photon[n + 1]; - dist2[0] = maxDist2; - } - - void reset(Point3 p, float maxDist2) { - found = 0; - gotHeap = false; - px = p.x; - py = p.y; - pz = p.z; - dist2[0] = maxDist2; - } - - void checkAddNearest(Photon p) { - float fdist2 = p.getDist2(px, py, pz); - if (fdist2 < dist2[0]) { - if (found < max) { - found++; - dist2[found] = fdist2; - index[found] = p; - } else { - int j; - int parent; - if (!gotHeap) { - float dst2; - Photon phot; - int halfFound = found >> 1; - for (int k = halfFound; k >= 1; k--) { - parent = k; - phot = index[k]; - dst2 = dist2[k]; - while (parent <= halfFound) { - j = parent + parent; - if ((j < found) && (dist2[j] < dist2[j + 1])) - j++; - if (dst2 >= dist2[j]) - break; - dist2[parent] = dist2[j]; - index[parent] = index[j]; - parent = j; - } - dist2[parent] = dst2; - index[parent] = phot; - } - gotHeap = true; - } - parent = 1; - j = 2; - while (j <= found) { - if ((j < found) && (dist2[j] < dist2[j + 1])) - j++; - if (fdist2 > dist2[j]) - break; - dist2[parent] = dist2[j]; - index[parent] = index[j]; - parent = j; - j += j; - } - dist2[parent] = fdist2; - index[parent] = p; - dist2[0] = dist2[1]; - } - } - } - } - - private static class Photon { - float x; - float y; - float z; - short dir; - int power; - int flags; - - static final int SPLIT_X = 0; - static final int SPLIT_Y = 1; - static final int SPLIT_Z = 2; - static final int SPLIT_MASK = 3; - - Photon(Point3 p, Vector3 dir, Color power) { - x = p.x; - y = p.y; - z = p.z; - this.dir = dir.encode(); - this.power = power.toRGBE(); - flags = SPLIT_X; - } - - void setSplitAxis(int axis) { - flags &= ~SPLIT_MASK; - flags |= axis; - } - - float getCoord(int axis) { - switch (axis) { - case SPLIT_X: - return x; - case SPLIT_Y: - return y; - default: - return z; - } - } - - float getDist1(float px, float py, float pz) { - switch (flags & SPLIT_MASK) { - case SPLIT_X: - return px - x; - case SPLIT_Y: - return py - y; - default: - return pz - z; - } - } - - float getDist2(float px, float py, float pz) { - float dx = x - px; - float dy = y - py; - float dz = z - pz; - return (dx * dx) + (dy * dy) + (dz * dz); - } - } - - public boolean allowDiffuseBounced() { - return false; - } - - public boolean allowReflectionBounced() { - return true; - } - - public boolean allowRefractionBounced() { - return true; - } - - public int numEmit() { - return numEmit; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/photonmap/GlobalPhotonMap.java b/src/org/sunflow/core/photonmap/GlobalPhotonMap.java deleted file mode 100644 index d9ef2cf..0000000 --- a/src/org/sunflow/core/photonmap/GlobalPhotonMap.java +++ /dev/null @@ -1,498 +0,0 @@ -package org.sunflow.core.photonmap; - -import java.util.ArrayList; - -import org.sunflow.core.GlobalPhotonMapInterface; -import org.sunflow.core.Options; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public final class GlobalPhotonMap implements GlobalPhotonMapInterface { - private ArrayList photonList; - private Photon[] photons; - private int storedPhotons; - private int halfStoredPhotons; - private int log2n; - private int numGather; - private float gatherRadius; - private BoundingBox bounds; - private boolean hasRadiance; - private float maxPower; - private float maxRadius; - private int numEmit; - - public GlobalPhotonMap() { - bounds = new BoundingBox(); - hasRadiance = false; - maxPower = 0; - maxRadius = 0; - } - - public void prepare(Options options, BoundingBox sceneBounds) { - // get settings - numEmit = options.getInt("gi.irr-cache.gmap.emit", 100000); - numGather = options.getInt("gi.irr-cache.gmap.gather", 50); - gatherRadius = options.getFloat("gi.irr-cache.gmap.radius", 0.5f); - // init - photonList = new ArrayList(); - photonList.add(null); - photons = null; - storedPhotons = halfStoredPhotons = 0; - } - - public void store(ShadingState state, Vector3 dir, Color power, Color diffuse) { - Photon p = new Photon(state.getPoint(), state.getNormal(), dir, power, diffuse); - synchronized (this) { - storedPhotons++; - photonList.add(p); - bounds.include(new Point3(p.x, p.y, p.z)); - maxPower = Math.max(maxPower, power.getMax()); - } - } - - private void locatePhotons(NearestPhotons np) { - float[] dist1d2 = new float[log2n]; - int[] chosen = new int[log2n]; - int i = 1; - int level = 0; - int cameFrom; - while (true) { - while (i < halfStoredPhotons) { - float dist1d = photons[i].getDist1(np.px, np.py, np.pz); - dist1d2[level] = dist1d * dist1d; - i += i; - if (dist1d > 0.0f) - i++; - chosen[level++] = i; - } - np.checkAddNearest(photons[i]); - do { - cameFrom = i; - i >>= 1; - level--; - if (i == 0) - return; - } while ((dist1d2[level] >= np.dist2[0]) || (cameFrom != chosen[level])); - np.checkAddNearest(photons[i]); - i = chosen[level++] ^ 1; - } - } - - private void balance() { - if (storedPhotons == 0) - return; - photons = photonList.toArray(new Photon[photonList.size()]); - photonList = null; - Photon[] temp = new Photon[storedPhotons + 1]; - balanceSegment(temp, 1, 1, storedPhotons); - photons = temp; - halfStoredPhotons = storedPhotons / 2; - log2n = (int) Math.ceil(Math.log(storedPhotons) / Math.log(2.0)); - } - - private void balanceSegment(Photon[] temp, int index, int start, int end) { - int median = 1; - while ((4 * median) <= (end - start + 1)) - median += median; - if ((3 * median) <= (end - start + 1)) { - median += median; - median += (start - 1); - } else - median = end - median + 1; - int axis = Photon.SPLIT_Z; - Vector3 extents = bounds.getExtents(); - if ((extents.x > extents.y) && (extents.x > extents.z)) - axis = Photon.SPLIT_X; - else if (extents.y > extents.z) - axis = Photon.SPLIT_Y; - int left = start; - int right = end; - while (right > left) { - double v = photons[right].getCoord(axis); - int i = left - 1; - int j = right; - while (true) { - while (photons[++i].getCoord(axis) < v) { - } - while ((photons[--j].getCoord(axis) > v) && (j > left)) { - } - if (i >= j) - break; - swap(i, j); - } - swap(i, right); - if (i >= median) - right = i - 1; - if (i <= median) - left = i + 1; - } - temp[index] = photons[median]; - temp[index].setSplitAxis(axis); - if (median > start) { - if (start < (median - 1)) { - float tmp; - switch (axis) { - case Photon.SPLIT_X: - tmp = bounds.getMaximum().x; - bounds.getMaximum().x = temp[index].x; - balanceSegment(temp, 2 * index, start, median - 1); - bounds.getMaximum().x = tmp; - break; - case Photon.SPLIT_Y: - tmp = bounds.getMaximum().y; - bounds.getMaximum().y = temp[index].y; - balanceSegment(temp, 2 * index, start, median - 1); - bounds.getMaximum().y = tmp; - break; - default: - tmp = bounds.getMaximum().z; - bounds.getMaximum().z = temp[index].z; - balanceSegment(temp, 2 * index, start, median - 1); - bounds.getMaximum().z = tmp; - } - } else - temp[2 * index] = photons[start]; - } - if (median < end) { - if ((median + 1) < end) { - float tmp; - switch (axis) { - case Photon.SPLIT_X: - tmp = bounds.getMinimum().x; - bounds.getMinimum().x = temp[index].x; - balanceSegment(temp, (2 * index) + 1, median + 1, end); - bounds.getMinimum().x = tmp; - break; - case Photon.SPLIT_Y: - tmp = bounds.getMinimum().y; - bounds.getMinimum().y = temp[index].y; - balanceSegment(temp, (2 * index) + 1, median + 1, end); - bounds.getMinimum().y = tmp; - break; - default: - tmp = bounds.getMinimum().z; - bounds.getMinimum().z = temp[index].z; - balanceSegment(temp, (2 * index) + 1, median + 1, end); - bounds.getMinimum().z = tmp; - } - } else - temp[(2 * index) + 1] = photons[end]; - } - } - - private void swap(int i, int j) { - Photon tmp = photons[i]; - photons[i] = photons[j]; - photons[j] = tmp; - } - - static class Photon { - float x; - float y; - float z; - short dir; - short normal; - int data; - int power; - int flags; - - static final int SPLIT_X = 0; - static final int SPLIT_Y = 1; - static final int SPLIT_Z = 2; - static final int SPLIT_MASK = 3; - - Photon(Point3 p, Vector3 n, Vector3 dir, Color power, Color diffuse) { - x = p.x; - y = p.y; - z = p.z; - this.dir = dir.encode(); - this.power = power.toRGBE(); - flags = 0; - normal = n.encode(); - data = diffuse.toRGB(); - } - - void setSplitAxis(int axis) { - flags &= ~SPLIT_MASK; - flags |= axis; - } - - float getCoord(int axis) { - switch (axis) { - case SPLIT_X: - return x; - case SPLIT_Y: - return y; - default: - return z; - } - } - - float getDist1(float px, float py, float pz) { - switch (flags & SPLIT_MASK) { - case SPLIT_X: - return px - x; - case SPLIT_Y: - return py - y; - default: - return pz - z; - } - } - - float getDist2(float px, float py, float pz) { - float dx = x - px; - float dy = y - py; - float dz = z - pz; - return (dx * dx) + (dy * dy) + (dz * dz); - } - } - - public void init() { - UI.printInfo(Module.LIGHT, "Balancing global photon map ..."); - UI.taskStart("Balancing global photon map", 0, 1); - Timer t = new Timer(); - t.start(); - balance(); - t.end(); - UI.taskStop(); - UI.printInfo(Module.LIGHT, "Global photon map:"); - UI.printInfo(Module.LIGHT, " * Photons stored: %d", storedPhotons); - UI.printInfo(Module.LIGHT, " * Photons/estimate: %d", numGather); - UI.printInfo(Module.LIGHT, " * Estimate radius: %.3f", gatherRadius); - maxRadius = 1.4f * (float) Math.sqrt(maxPower * numGather); - UI.printInfo(Module.LIGHT, " * Maximum radius: %.3f", maxRadius); - UI.printInfo(Module.LIGHT, " * Balancing time: %s", t.toString()); - if (gatherRadius > maxRadius) - gatherRadius = maxRadius; - t.start(); - precomputeRadiance(); - t.end(); - UI.printInfo(Module.LIGHT, " * Precompute time: %s", t.toString()); - UI.printInfo(Module.LIGHT, " * Radiance photons: %d", storedPhotons); - UI.printInfo(Module.LIGHT, " * Search radius: %.3f", gatherRadius); - } - - public void precomputeRadiance() { - if (storedPhotons == 0) - return; - // precompute the radiance for all photons that are neither - // leaves nor parents of leaves in the tree. - int quadStoredPhotons = halfStoredPhotons / 2; - Point3 p = new Point3(); - Vector3 n = new Vector3(); - Point3 ppos = new Point3(); - Vector3 pdir = new Vector3(); - Vector3 pvec = new Vector3(); - Color irr = new Color(); - Color pow = new Color(); - float maxDist2 = gatherRadius * gatherRadius; - NearestPhotons np = new NearestPhotons(p, numGather, maxDist2); - Photon[] temp = new Photon[quadStoredPhotons + 1]; - UI.taskStart("Precomputing radiance", 1, quadStoredPhotons); - for (int i = 1; i <= quadStoredPhotons; i++) { - UI.taskUpdate(i); - Photon curr = photons[i]; - p.set(curr.x, curr.y, curr.z); - Vector3.decode(curr.normal, n); - irr.set(Color.BLACK); - np.reset(p, maxDist2); - locatePhotons(np); - if (np.found < 8) { - curr.data = 0; - temp[i] = curr; - continue; - } - float invArea = 1.0f / ((float) Math.PI * np.dist2[0]); - float maxNDist = np.dist2[0] * 0.05f; - for (int j = 1; j <= np.found; j++) { - Photon phot = np.index[j]; - Vector3.decode(phot.dir, pdir); - float cos = -Vector3.dot(pdir, n); - if (cos > 0.01f) { - ppos.set(phot.x, phot.y, phot.z); - Point3.sub(ppos, p, pvec); - float pcos = Vector3.dot(pvec, n); - if ((pcos < maxNDist) && (pcos > -maxNDist)) - irr.add(pow.setRGBE(phot.power)); - } - } - irr.mul(invArea); - // compute radiance - irr.mul(new Color(curr.data)).mul(1.0f / (float) Math.PI); - curr.data = irr.toRGBE(); - temp[i] = curr; - } - UI.taskStop(); - - // resize photon map to only include irradiance photons - numGather /= 4; - maxRadius = 1.4f * (float) Math.sqrt(maxPower * numGather); - if (gatherRadius > maxRadius) - gatherRadius = maxRadius; - storedPhotons = quadStoredPhotons; - halfStoredPhotons = storedPhotons / 2; - log2n = (int) Math.ceil(Math.log(storedPhotons) / Math.log(2.0)); - photons = temp; - hasRadiance = true; - } - - public Color getRadiance(Point3 p, Vector3 n) { - if (!hasRadiance || (storedPhotons == 0)) - return Color.BLACK; - float px = p.x; - float py = p.y; - float pz = p.z; - int i = 1; - int level = 0; - int cameFrom; - float dist2; - float maxDist2 = gatherRadius * gatherRadius; - Photon nearest = null; - Photon curr; - Vector3 photN = new Vector3(); - float[] dist1d2 = new float[log2n]; - int[] chosen = new int[log2n]; - while (true) { - while (i < halfStoredPhotons) { - float dist1d = photons[i].getDist1(px, py, pz); - dist1d2[level] = dist1d * dist1d; - i += i; - if (dist1d > 0) - i++; - chosen[level++] = i; - } - curr = photons[i]; - dist2 = curr.getDist2(px, py, pz); - if (dist2 < maxDist2) { - Vector3.decode(curr.normal, photN); - float currentDotN = Vector3.dot(photN, n); - if (currentDotN > 0.9f) { - nearest = curr; - maxDist2 = dist2; - } - } - do { - cameFrom = i; - i >>= 1; - level--; - if (i == 0) - return (nearest == null) ? Color.BLACK : new Color().setRGBE(nearest.data); - } while ((dist1d2[level] >= maxDist2) || (cameFrom != chosen[level])); - curr = photons[i]; - dist2 = curr.getDist2(px, py, pz); - if (dist2 < maxDist2) { - Vector3.decode(curr.normal, photN); - float currentDotN = Vector3.dot(photN, n); - if (currentDotN > 0.9f) { - nearest = curr; - maxDist2 = dist2; - } - } - i = chosen[level++] ^ 1; - } - } - - private static class NearestPhotons { - int found; - float px, py, pz; - private int max; - private boolean gotHeap; - protected float[] dist2; - protected Photon[] index; - - NearestPhotons(Point3 p, int n, float maxDist2) { - max = n; - found = 0; - gotHeap = false; - px = p.x; - py = p.y; - pz = p.z; - dist2 = new float[n + 1]; - index = new Photon[n + 1]; - dist2[0] = maxDist2; - } - - void reset(Point3 p, float maxDist2) { - found = 0; - gotHeap = false; - px = p.x; - py = p.y; - pz = p.z; - dist2[0] = maxDist2; - } - - void checkAddNearest(Photon p) { - float fdist2 = p.getDist2(px, py, pz); - if (fdist2 < dist2[0]) { - if (found < max) { - found++; - dist2[found] = fdist2; - index[found] = p; - } else { - int j; - int parent; - if (!gotHeap) { - float dst2; - Photon phot; - int halfFound = found >> 1; - for (int k = halfFound; k >= 1; k--) { - parent = k; - phot = index[k]; - dst2 = dist2[k]; - while (parent <= halfFound) { - j = parent + parent; - if ((j < found) && (dist2[j] < dist2[j + 1])) - j++; - if (dst2 >= dist2[j]) - break; - dist2[parent] = dist2[j]; - index[parent] = index[j]; - parent = j; - } - dist2[parent] = dst2; - index[parent] = phot; - } - gotHeap = true; - } - parent = 1; - j = 2; - while (j <= found) { - if ((j < found) && (dist2[j] < dist2[j + 1])) - j++; - if (fdist2 > dist2[j]) - break; - dist2[parent] = dist2[j]; - index[parent] = index[j]; - parent = j; - j += j; - } - dist2[parent] = fdist2; - index[parent] = p; - dist2[0] = dist2[1]; - } - } - } - } - - public boolean allowDiffuseBounced() { - return true; - } - - public boolean allowReflectionBounced() { - return true; - } - - public boolean allowRefractionBounced() { - return true; - } - - public int numEmit() { - return numEmit; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/photonmap/GridPhotonMap.java b/src/org/sunflow/core/photonmap/GridPhotonMap.java deleted file mode 100644 index e7590ab..0000000 --- a/src/org/sunflow/core/photonmap/GridPhotonMap.java +++ /dev/null @@ -1,282 +0,0 @@ -package org.sunflow.core.photonmap; - -import java.util.concurrent.locks.ReentrantReadWriteLock; - -import org.sunflow.core.GlobalPhotonMapInterface; -import org.sunflow.core.Options; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.MathUtils; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class GridPhotonMap implements GlobalPhotonMapInterface { - private int numGather; - private float gatherRadius; - private int numStoredPhotons; - private int nx, ny, nz; - private BoundingBox bounds; - private PhotonGroup[] cellHash; - private int hashSize; - private int hashPrime; - private ReentrantReadWriteLock rwl; - private int numEmit; - - private static final float NORMAL_THRESHOLD = (float) Math.cos(10.0 * Math.PI / 180.0); - private static final int[] PRIMES = { 11, 19, 37, 109, 163, 251, 367, 557, - 823, 1237, 1861, 2777, 4177, 6247, 9371, 21089, 31627, 47431, - 71143, 106721, 160073, 240101, 360163, 540217, 810343, 1215497, - 1823231, 2734867, 4102283, 6153409, 9230113, 13845163 }; - - public GridPhotonMap() { - numStoredPhotons = 0; - hashSize = 0; // number of unique IDs in the hash - rwl = new ReentrantReadWriteLock(); - numEmit = 100000; - } - - public void prepare(Options options, BoundingBox sceneBounds) { - // get settings - numEmit = options.getInt("gi.irr-cache.gmap.emit", 100000); - numGather = options.getInt("gi.irr-cache.gmap.gather", 50); - gatherRadius = options.getFloat("gi.irr-cache.gmap.radius", 0.5f); - - bounds = new BoundingBox(sceneBounds); - bounds.enlargeUlps(); - Vector3 w = bounds.getExtents(); - nx = (int) Math.max(((w.x / gatherRadius) + 0.5f), 1); - ny = (int) Math.max(((w.y / gatherRadius) + 0.5f), 1); - nz = (int) Math.max(((w.z / gatherRadius) + 0.5f), 1); - int numCells = nx * ny * nz; - UI.printInfo(Module.LIGHT, "Initializing grid photon map:"); - UI.printInfo(Module.LIGHT, " * Resolution: %dx%dx%d", nx, ny, nz); - UI.printInfo(Module.LIGHT, " * Total cells: %d", numCells); - for (hashPrime = 0; hashPrime < PRIMES.length; hashPrime++) - if (PRIMES[hashPrime] > (numCells / 5)) - break; - cellHash = new PhotonGroup[PRIMES[hashPrime]]; - UI.printInfo(Module.LIGHT, " * Initial hash size: %d", cellHash.length); - } - - public int size() { - return numStoredPhotons; - } - - public void store(ShadingState state, Vector3 dir, Color power, Color diffuse) { - // don't store on the wrong side of a surface - if (Vector3.dot(state.getNormal(), dir) > 0) - return; - Point3 pt = state.getPoint(); - // outside grid bounds ? - if (!bounds.contains(pt)) - return; - Vector3 ext = bounds.getExtents(); - int ix = (int) (((pt.x - bounds.getMinimum().x) * nx) / ext.x); - int iy = (int) (((pt.y - bounds.getMinimum().y) * ny) / ext.y); - int iz = (int) (((pt.z - bounds.getMinimum().z) * nz) / ext.z); - ix = MathUtils.clamp(ix, 0, nx - 1); - iy = MathUtils.clamp(iy, 0, ny - 1); - iz = MathUtils.clamp(iz, 0, nz - 1); - int id = ix + iy * nx + iz * nx * ny; - synchronized (this) { - int hid = id % cellHash.length; - PhotonGroup g = cellHash[hid]; - PhotonGroup last = null; - boolean hasID = false; - while (g != null) { - if (g.id == id) { - hasID = true; - if (Vector3.dot(state.getNormal(), g.normal) > NORMAL_THRESHOLD) - break; - } - last = g; - g = g.next; - } - if (g == null) { - g = new PhotonGroup(id, state.getNormal()); - if (last == null) - cellHash[hid] = g; - else - last.next = g; - if (!hasID) { - hashSize++; // we have not seen this ID before - // resize hash if we have grown too large - if (hashSize > cellHash.length) - growPhotonHash(); - } - } - g.count++; - g.flux.add(power); - g.diffuse.add(diffuse); - numStoredPhotons++; - } - } - - public void init() { - UI.printInfo(Module.LIGHT, "Initializing photon grid ..."); - UI.printInfo(Module.LIGHT, " * Photon hits: %d", numStoredPhotons); - UI.printInfo(Module.LIGHT, " * Final hash size: %d", cellHash.length); - int cells = 0; - for (int i = 0; i < cellHash.length; i++) { - for (PhotonGroup g = cellHash[i]; g != null; g = g.next) { - g.diffuse.mul(1.0f / g.count); - cells++; - } - } - UI.printInfo(Module.LIGHT, " * Num photon cells: %d", cells); - } - - public void precomputeRadiance(boolean includeDirect, boolean includeCaustics) { - } - - private void growPhotonHash() { - // enlarge the hash size: - if (hashPrime >= PRIMES.length - 1) - return; - PhotonGroup[] temp = new PhotonGroup[PRIMES[++hashPrime]]; - for (int i = 0; i < cellHash.length; i++) { - PhotonGroup g = cellHash[i]; - while (g != null) { - // re-hash into the new table - int hid = g.id % temp.length; - PhotonGroup last = null; - for (PhotonGroup gn = temp[hid]; gn != null; gn = gn.next) - last = gn; - if (last == null) - temp[hid] = g; - else - last.next = g; - PhotonGroup next = g.next; - g.next = null; - g = next; - } - } - cellHash = temp; - } - - public synchronized Color getRadiance(Point3 p, Vector3 n) { - if (!bounds.contains(p)) - return Color.BLACK; - Vector3 ext = bounds.getExtents(); - int ix = (int) (((p.x - bounds.getMinimum().x) * nx) / ext.x); - int iy = (int) (((p.y - bounds.getMinimum().y) * ny) / ext.y); - int iz = (int) (((p.z - bounds.getMinimum().z) * nz) / ext.z); - ix = MathUtils.clamp(ix, 0, nx - 1); - iy = MathUtils.clamp(iy, 0, ny - 1); - iz = MathUtils.clamp(iz, 0, nz - 1); - int id = ix + iy * nx + iz * nx * ny; - rwl.readLock().lock(); - PhotonGroup center = null; - for (PhotonGroup g = get(ix, iy, iz); g != null; g = g.next) { - if (g.id == id && Vector3.dot(n, g.normal) > NORMAL_THRESHOLD) { - if (g.radiance == null) { - center = g; - break; - } - Color r = g.radiance.copy(); - rwl.readLock().unlock(); - return r; - } - } - int vol = 1; - while (true) { - int numPhotons = 0; - int ndiff = 0; - Color irr = Color.black(); - Color diff = (center == null) ? Color.black() : null; - for (int z = iz - (vol - 1); z <= iz + (vol - 1); z++) { - for (int y = iy - (vol - 1); y <= iy + (vol - 1); y++) { - for (int x = ix - (vol - 1); x <= ix + (vol - 1); x++) { - int vid = x + y * nx + z * nx * ny; - for (PhotonGroup g = get(x, y, z); g != null; g = g.next) { - if (g.id == vid && Vector3.dot(n, g.normal) > NORMAL_THRESHOLD) { - numPhotons += g.count; - irr.add(g.flux); - if (diff != null) { - diff.add(g.diffuse); - ndiff++; - } - break; // only one valid group can be found, - // skip the others - } - } - } - } - } - if (numPhotons >= numGather || vol >= 3) { - // we have found enough photons - // cache irradiance and return - float area = (2 * vol - 1) / 3.0f * ((ext.x / nx) + (ext.y / ny) + (ext.z / nz)); - area *= area; - area *= Math.PI; - irr.mul(1.0f / area); - // upgrade lock manually - rwl.readLock().unlock(); - rwl.writeLock().lock(); - if (center == null) { - if (ndiff > 0) - diff.mul(1.0f / ndiff); - center = new PhotonGroup(id, n); - center.diffuse.set(diff); - center.next = cellHash[id % cellHash.length]; - cellHash[id % cellHash.length] = center; - } - irr.mul(center.diffuse); - center.radiance = irr.copy(); - rwl.writeLock().unlock(); // unlock write - done - return irr; - } - vol++; - } - } - - private PhotonGroup get(int x, int y, int z) { - // returns the list associated with the specified location - if (x < 0 || x >= nx) - return null; - if (y < 0 || y >= ny) - return null; - if (z < 0 || z >= nz) - return null; - return cellHash[(x + y * nx + z * nx * ny) % cellHash.length]; - } - - private class PhotonGroup { - int id; - int count; - Vector3 normal; - Color flux; - Color radiance; - Color diffuse; - PhotonGroup next; - - PhotonGroup(int id, Vector3 n) { - normal = new Vector3(n); - flux = Color.black(); - diffuse = Color.black(); - radiance = null; - count = 0; - this.id = id; - next = null; - } - } - - public boolean allowDiffuseBounced() { - return true; - } - - public boolean allowReflectionBounced() { - return true; - } - - public boolean allowRefractionBounced() { - return true; - } - - public int numEmit() { - return numEmit; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/Background.java b/src/org/sunflow/core/primitive/Background.java deleted file mode 100644 index a2930ac..0000000 --- a/src/org/sunflow/core/primitive/Background.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.sunflow.core.primitive; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; - -public class Background implements PrimitiveList { - public Background() { - } - - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public void prepareShadingState(ShadingState state) { - if (state.getDepth() == 0) - state.setShader(state.getInstance().getShader(0)); - } - - public int getNumPrimitives() { - return 1; - } - - public float getPrimitiveBound(int primID, int i) { - return 0; - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - return null; - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - if (r.getMax() == Float.POSITIVE_INFINITY) - state.setIntersection(0); - } - - public PrimitiveList getBakingPrimitives() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/BanchoffSurface.java b/src/org/sunflow/core/primitive/BanchoffSurface.java deleted file mode 100644 index d1bb19e..0000000 --- a/src/org/sunflow/core/primitive/BanchoffSurface.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.sunflow.core.primitive; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Solvers; -import org.sunflow.math.Vector3; - -public class BanchoffSurface implements PrimitiveList { - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(1.5f); - if (o2w != null) - bounds = o2w.transform(bounds); - return bounds; - } - - public float getPrimitiveBound(int primID, int i) { - return (i & 1) == 0 ? -1.5f : 1.5f; - } - - public int getNumPrimitives() { - return 1; - } - - public void prepareShadingState(ShadingState state) { - state.init(); - state.getRay().getPoint(state.getPoint()); - Instance parent = state.getInstance(); - Point3 n = state.transformWorldToObject(state.getPoint()); - state.getNormal().set(n.x * (2 * n.x * n.x - 1), n.y * (2 * n.y * n.y - 1), n.z * (2 * n.z * n.z - 1)); - state.getNormal().normalize(); - state.setShader(parent.getShader(0)); - state.setModifier(parent.getModifier(0)); - // into world space - Vector3 worldNormal = state.transformNormalObjectToWorld(state.getNormal()); - state.getNormal().set(worldNormal); - state.getNormal().normalize(); - state.getGeoNormal().set(state.getNormal()); - // create basis in world space - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - // intersect in local space - float rd2x = r.dx * r.dx; - float rd2y = r.dy * r.dy; - float rd2z = r.dz * r.dz; - float ro2x = r.ox * r.ox; - float ro2y = r.oy * r.oy; - float ro2z = r.oz * r.oz; - // setup the quartic coefficients - // some common terms could probably be shared across these - double A = (rd2y * rd2y + rd2z * rd2z + rd2x * rd2x); - double B = 4 * (r.oy * rd2y * r.dy + r.oz * r.dz * rd2z + r.ox * r.dx * rd2x); - double C = (-rd2x - rd2y - rd2z + 6 * (ro2y * rd2y + ro2z * rd2z + ro2x * rd2x)); - double D = 2 * (2 * ro2z * r.oz * r.dz - r.oz * r.dz + 2 * ro2x * r.ox * r.dx + 2 * ro2y * r.oy * r.dy - r.ox * r.dx - r.oy * r.dy); - double E = 3.0f / 8.0f + (-ro2z + ro2z * ro2z - ro2y + ro2y * ro2y - ro2x + ro2x * ro2x); - // solve equation - double[] t = Solvers.solveQuartic(A, B, C, D, E); - if (t != null) { - // early rejection - if (t[0] >= r.getMax() || t[t.length - 1] <= r.getMin()) - return; - // find first intersection in front of the ray - for (int i = 0; i < t.length; i++) { - if (t[i] > r.getMin()) { - r.setMax((float) t[i]); - state.setIntersection(0); - return; - } - } - } - } - - public PrimitiveList getBakingPrimitives() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/Box.java b/src/org/sunflow/core/primitive/Box.java deleted file mode 100644 index 3964631..0000000 --- a/src/org/sunflow/core/primitive/Box.java +++ /dev/null @@ -1,197 +0,0 @@ -package org.sunflow.core.primitive; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.core.ParameterList.FloatParameter; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Vector3; - -public class Box implements PrimitiveList { - private float minX, minY, minZ; - private float maxX, maxY, maxZ; - - public Box() { - minX = minY = minZ = -1; - maxX = maxY = maxZ = +1; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - FloatParameter pts = pl.getPointArray("points"); - if (pts != null) { - BoundingBox bounds = new BoundingBox(); - for (int i = 0; i < pts.data.length; i += 3) - bounds.include(pts.data[i], pts.data[i + 1], pts.data[i + 2]); - // cube extents - minX = bounds.getMinimum().x; - minY = bounds.getMinimum().y; - minZ = bounds.getMinimum().z; - maxX = bounds.getMaximum().x; - maxY = bounds.getMaximum().y; - maxZ = bounds.getMaximum().z; - } - return true; - } - - public void prepareShadingState(ShadingState state) { - state.init(); - state.getRay().getPoint(state.getPoint()); - int n = state.getPrimitiveID(); - switch (n) { - case 0: - state.getNormal().set(new Vector3(1, 0, 0)); - break; - case 1: - state.getNormal().set(new Vector3(-1, 0, 0)); - break; - case 2: - state.getNormal().set(new Vector3(0, 1, 0)); - break; - case 3: - state.getNormal().set(new Vector3(0, -1, 0)); - break; - case 4: - state.getNormal().set(new Vector3(0, 0, 1)); - break; - case 5: - state.getNormal().set(new Vector3(0, 0, -1)); - break; - default: - state.getNormal().set(new Vector3(0, 0, 0)); - break; - } - state.getGeoNormal().set(state.getNormal()); - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - state.setShader(state.getInstance().getShader(0)); - state.setModifier(state.getInstance().getModifier(0)); - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - float intervalMin = Float.NEGATIVE_INFINITY; - float intervalMax = Float.POSITIVE_INFINITY; - float orgX = r.ox; - float invDirX = 1 / r.dx; - float t1, t2; - t1 = (minX - orgX) * invDirX; - t2 = (maxX - orgX) * invDirX; - int sideIn = -1, sideOut = -1; - if (invDirX > 0) { - if (t1 > intervalMin) { - intervalMin = t1; - sideIn = 0; - } - if (t2 < intervalMax) { - intervalMax = t2; - sideOut = 1; - } - } else { - if (t2 > intervalMin) { - intervalMin = t2; - sideIn = 1; - } - if (t1 < intervalMax) { - intervalMax = t1; - sideOut = 0; - } - } - if (intervalMin > intervalMax) - return; - float orgY = r.oy; - float invDirY = 1 / r.dy; - t1 = (minY - orgY) * invDirY; - t2 = (maxY - orgY) * invDirY; - if (invDirY > 0) { - if (t1 > intervalMin) { - intervalMin = t1; - sideIn = 2; - } - if (t2 < intervalMax) { - intervalMax = t2; - sideOut = 3; - } - } else { - if (t2 > intervalMin) { - intervalMin = t2; - sideIn = 3; - } - if (t1 < intervalMax) { - intervalMax = t1; - sideOut = 2; - } - } - if (intervalMin > intervalMax) - return; - float orgZ = r.oz; - float invDirZ = 1 / r.dz; - t1 = (minZ - orgZ) * invDirZ; // no front wall - t2 = (maxZ - orgZ) * invDirZ; - if (invDirZ > 0) { - if (t1 > intervalMin) { - intervalMin = t1; - sideIn = 4; - } - if (t2 < intervalMax) { - intervalMax = t2; - sideOut = 5; - } - } else { - if (t2 > intervalMin) { - intervalMin = t2; - sideIn = 5; - } - if (t1 < intervalMax) { - intervalMax = t1; - sideOut = 4; - } - } - if (intervalMin > intervalMax) - return; - if (r.isInside(intervalMin)) { - r.setMax(intervalMin); - state.setIntersection(sideIn); - } else if (r.isInside(intervalMax)) { - r.setMax(intervalMax); - state.setIntersection(sideOut); - } - } - - public int getNumPrimitives() { - return 1; - } - - public float getPrimitiveBound(int primID, int i) { - switch (i) { - case 0: - return minX; - case 1: - return maxX; - case 2: - return minY; - case 3: - return maxY; - case 4: - return minZ; - case 5: - return maxZ; - default: - return 0; - } - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(minX, minY, minZ); - bounds.include(maxX, maxY, maxZ); - if (o2w == null) - return bounds; - return o2w.transform(bounds); - } - - public PrimitiveList getBakingPrimitives() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/CornellBox.java b/src/org/sunflow/core/primitive/CornellBox.java deleted file mode 100644 index 6b2c88c..0000000 --- a/src/org/sunflow/core/primitive/CornellBox.java +++ /dev/null @@ -1,446 +0,0 @@ -package org.sunflow.core.primitive; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.LightSample; -import org.sunflow.core.LightSource; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; - -public class CornellBox implements PrimitiveList, Shader, LightSource { - private float minX, minY, minZ; - private float maxX, maxY, maxZ; - private Color left, right, top, bottom, back; - private Color radiance; - private int samples; - private float lxmin, lymin, lxmax, lymax; - private float area; - private BoundingBox lightBounds; - - public CornellBox() { - updateGeometry(new Point3(-1, -1, -1), new Point3(1, 1, 1)); - - // cube colors - left = new Color(0.80f, 0.25f, 0.25f); - right = new Color(0.25f, 0.25f, 0.80f); - Color gray = new Color(0.70f, 0.70f, 0.70f); - top = bottom = back = gray; - - // light source - radiance = Color.WHITE; - samples = 16; - } - - private void updateGeometry(Point3 c0, Point3 c1) { - // figure out cube extents - lightBounds = new BoundingBox(c0); - lightBounds.include(c1); - - // cube extents - minX = lightBounds.getMinimum().x; - minY = lightBounds.getMinimum().y; - minZ = lightBounds.getMinimum().z; - maxX = lightBounds.getMaximum().x; - maxY = lightBounds.getMaximum().y; - maxZ = lightBounds.getMaximum().z; - - // work around epsilon problems for light test - lightBounds.enlargeUlps(); - - // light source geometry - lxmin = maxX / 3 + 2 * minX / 3; - lxmax = minX / 3 + 2 * maxX / 3; - lymin = maxY / 3 + 2 * minY / 3; - lymax = minY / 3 + 2 * maxY / 3; - area = (lxmax - lxmin) * (lymax - lymin); - } - - public boolean update(ParameterList pl, SunflowAPI api) { - Point3 corner0 = pl.getPoint("corner0", null); - Point3 corner1 = pl.getPoint("corner1", null); - if (corner0 != null && corner1 != null) { - updateGeometry(corner0, corner1); - } - - // shader colors - left = pl.getColor("leftColor", left); - right = pl.getColor("rightColor", right); - top = pl.getColor("topColor", top); - bottom = pl.getColor("bottomColor", bottom); - back = pl.getColor("backColor", back); - - // light - radiance = pl.getColor("radiance", radiance); - samples = pl.getInt("samples", samples); - return true; - } - - public BoundingBox getBounds() { - return lightBounds; - } - - public float getBound(int i) { - switch (i) { - case 0: - return minX; - case 1: - return maxX; - case 2: - return minY; - case 3: - return maxY; - case 4: - return minZ; - case 5: - return maxZ; - default: - return 0; - } - } - - public boolean intersects(BoundingBox box) { - // this could be optimized - BoundingBox b = new BoundingBox(); - b.include(new Point3(minX, minY, minZ)); - b.include(new Point3(maxX, maxY, maxZ)); - if (b.intersects(box)) { - // the box is overlapping or enclosed - if (!b.contains(new Point3(box.getMinimum().x, box.getMinimum().y, box.getMinimum().z))) - return true; - if (!b.contains(new Point3(box.getMinimum().x, box.getMinimum().y, box.getMaximum().z))) - return true; - if (!b.contains(new Point3(box.getMinimum().x, box.getMaximum().y, box.getMinimum().z))) - return true; - if (!b.contains(new Point3(box.getMinimum().x, box.getMaximum().y, box.getMaximum().z))) - return true; - if (!b.contains(new Point3(box.getMaximum().x, box.getMinimum().y, box.getMinimum().z))) - return true; - if (!b.contains(new Point3(box.getMaximum().x, box.getMinimum().y, box.getMaximum().z))) - return true; - if (!b.contains(new Point3(box.getMaximum().x, box.getMaximum().y, box.getMinimum().z))) - return true; - if (!b.contains(new Point3(box.getMaximum().x, box.getMaximum().y, box.getMaximum().z))) - return true; - // all vertices of the box are inside - the surface of the box is - // not intersected - } - return false; - } - - public void prepareShadingState(ShadingState state) { - state.init(); - state.getRay().getPoint(state.getPoint()); - int n = state.getPrimitiveID(); - switch (n) { - case 0: - state.getNormal().set(new Vector3(1, 0, 0)); - break; - case 1: - state.getNormal().set(new Vector3(-1, 0, 0)); - break; - case 2: - state.getNormal().set(new Vector3(0, 1, 0)); - break; - case 3: - state.getNormal().set(new Vector3(0, -1, 0)); - break; - case 4: - state.getNormal().set(new Vector3(0, 0, 1)); - break; - case 5: - state.getNormal().set(new Vector3(0, 0, -1)); - break; - default: - state.getNormal().set(new Vector3(0, 0, 0)); - break; - } - state.getGeoNormal().set(state.getNormal()); - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - state.setShader(this); - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - float intervalMin = Float.NEGATIVE_INFINITY; - float intervalMax = Float.POSITIVE_INFINITY; - float orgX = r.ox; - float invDirX = 1 / r.dx; - float t1, t2; - t1 = (minX - orgX) * invDirX; - t2 = (maxX - orgX) * invDirX; - int sideIn = -1, sideOut = -1; - if (invDirX > 0) { - if (t1 > intervalMin) { - intervalMin = t1; - sideIn = 0; - } - if (t2 < intervalMax) { - intervalMax = t2; - sideOut = 1; - } - } else { - if (t2 > intervalMin) { - intervalMin = t2; - sideIn = 1; - } - if (t1 < intervalMax) { - intervalMax = t1; - sideOut = 0; - } - } - if (intervalMin > intervalMax) - return; - float orgY = r.oy; - float invDirY = 1 / r.dy; - t1 = (minY - orgY) * invDirY; - t2 = (maxY - orgY) * invDirY; - if (invDirY > 0) { - if (t1 > intervalMin) { - intervalMin = t1; - sideIn = 2; - } - if (t2 < intervalMax) { - intervalMax = t2; - sideOut = 3; - } - } else { - if (t2 > intervalMin) { - intervalMin = t2; - sideIn = 3; - } - if (t1 < intervalMax) { - intervalMax = t1; - sideOut = 2; - } - } - if (intervalMin > intervalMax) - return; - float orgZ = r.oz; - float invDirZ = 1 / r.dz; - t1 = (minZ - orgZ) * invDirZ; // no front wall - t2 = (maxZ - orgZ) * invDirZ; - if (invDirZ > 0) { - if (t1 > intervalMin) { - intervalMin = t1; - sideIn = 4; - } - if (t2 < intervalMax) { - intervalMax = t2; - sideOut = 5; - } - } else { - if (t2 > intervalMin) { - intervalMin = t2; - sideIn = 5; - } - if (t1 < intervalMax) { - intervalMax = t1; - sideOut = 4; - } - } - if (intervalMin > intervalMax) - return; - assert sideIn != -1; - assert sideOut != -1; - // can't hit minY wall, there is none - if (sideIn != 2 && r.isInside(intervalMin)) { - r.setMax(intervalMin); - state.setIntersection(sideIn); - } else if (sideOut != 2 && r.isInside(intervalMax)) { - r.setMax(intervalMax); - state.setIntersection(sideOut); - } - } - - public Color getRadiance(ShadingState state) { - int side = state.getPrimitiveID(); - Color kd = null; - switch (side) { - case 0: - kd = left; - break; - case 1: - kd = right; - break; - case 3: - kd = back; - break; - case 4: - kd = bottom; - break; - case 5: - float lx = state.getPoint().x; - float ly = state.getPoint().y; - if (lx >= lxmin && lx < lxmax && ly >= lymin && ly < lymax && state.getRay().dz > 0) - return state.includeLights() ? radiance : Color.BLACK; - kd = top; - break; - default: - assert false; - } - // make sure we are on the right side of the material - state.faceforward(); - // setup lighting - state.initLightSamples(); - state.initCausticSamples(); - return state.diffuse(kd); - } - - public void scatterPhoton(ShadingState state, Color power) { - int side = state.getPrimitiveID(); - Color kd = null; - switch (side) { - case 0: - kd = left; - break; - case 1: - kd = right; - break; - case 3: - kd = back; - break; - case 4: - kd = bottom; - break; - case 5: - float lx = state.getPoint().x; - float ly = state.getPoint().y; - if (lx >= lxmin && lx < lxmax && ly >= lymin && ly < lymax && state.getRay().dz > 0) - return; - kd = top; - break; - default: - assert false; - } - // make sure we are on the right side of the material - if (Vector3.dot(state.getNormal(), state.getRay().getDirection()) > 0) { - state.getNormal().negate(); - state.getGeoNormal().negate(); - } - state.storePhoton(state.getRay().getDirection(), power, kd); - double avg = kd.getAverage(); - double rnd = state.getRandom(0, 0, 1); - if (rnd < avg) { - // photon is scattered - power.mul(kd).mul(1 / (float) avg); - OrthoNormalBasis onb = OrthoNormalBasis.makeFromW(state.getNormal()); - double u = 2 * Math.PI * rnd / avg; - double v = state.getRandom(0, 1, 1); - float s = (float) Math.sqrt(v); - float s1 = (float) Math.sqrt(1.0 - v); - Vector3 w = new Vector3((float) Math.cos(u) * s, (float) Math.sin(u) * s, s1); - w = onb.transform(w, new Vector3()); - state.traceDiffusePhoton(new Ray(state.getPoint(), w), power); - } - } - - public int getNumSamples() { - return samples; - } - - public void getSamples(ShadingState state) { - if (lightBounds.contains(state.getPoint()) && state.getPoint().z < maxZ) { - int n = state.getDiffuseDepth() > 0 ? 1 : samples; - float a = area / n; - for (int i = 0; i < n; i++) { - // random offset on unit square - double randX = state.getRandom(i, 0, n); - double randY = state.getRandom(i, 1, n); - Point3 p = new Point3(); - p.x = (float) (lxmin * (1 - randX) + lxmax * randX); - p.y = (float) (lymin * (1 - randY) + lymax * randY); - p.z = maxZ - 0.001f; - - LightSample dest = new LightSample(); - // prepare shadow ray to sampled point - dest.setShadowRay(new Ray(state.getPoint(), p)); - - // check that the direction of the sample is the same as the - // normal - float cosNx = dest.dot(state.getNormal()); - if (cosNx <= 0) - return; - - // light source facing point ? - // (need to check with light source's normal) - float cosNy = dest.getShadowRay().dz; - if (cosNy > 0) { - // compute geometric attenuation and probability scale - // factor - float r = dest.getShadowRay().getMax(); - float g = cosNy / (r * r); - float scale = g * a; - // set final sample radiance - dest.setRadiance(radiance, radiance); - dest.getDiffuseRadiance().mul(scale); - dest.getSpecularRadiance().mul(scale); - dest.traceShadow(state); - state.addSample(dest); - } - } - } - } - - public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power) { - p.x = (float) (lxmin * (1 - randX2) + lxmax * randX2); - p.y = (float) (lymin * (1 - randY2) + lymax * randY2); - p.z = maxZ - 0.001f; - - double u = 2 * Math.PI * randX1; - double s = Math.sqrt(randY1); - dir.set((float) (Math.cos(u) * s), (float) (Math.sin(u) * s), (float) -Math.sqrt(1.0f - randY1)); - Color.mul((float) Math.PI * area, radiance, power); - } - - public float getPower() { - return radiance.copy().mul((float) Math.PI * area).getLuminance(); - } - - public int getNumPrimitives() { - return 1; - } - - public float getPrimitiveBound(int primID, int i) { - switch (i) { - case 0: - return minX; - case 1: - return maxX; - case 2: - return minY; - case 3: - return maxY; - case 4: - return minZ; - case 5: - return maxZ; - default: - return 0; - } - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(minX, minY, minZ); - bounds.include(maxX, maxY, maxZ); - if (o2w == null) - return bounds; - return o2w.transform(bounds); - } - - public PrimitiveList getBakingPrimitives() { - return null; - } - - public Instance createInstance() { - return Instance.createTemporary(this, null, this); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/CubeGrid.java b/src/org/sunflow/core/primitive/CubeGrid.java deleted file mode 100644 index 75b1224..0000000 --- a/src/org/sunflow/core/primitive/CubeGrid.java +++ /dev/null @@ -1,288 +0,0 @@ -package org.sunflow.core.primitive; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Vector3; - -public abstract class CubeGrid implements PrimitiveList { - private int nx, ny, nz; - private float voxelwx, voxelwy, voxelwz; - private float invVoxelwx, invVoxelwy, invVoxelwz; - private BoundingBox bounds; - - public CubeGrid() { - nx = ny = nz = 1; - bounds = new BoundingBox(1); - } - - public boolean update(ParameterList pl, SunflowAPI api) { - nx = pl.getInt("resolutionX", nx); - ny = pl.getInt("resolutionY", ny); - nz = pl.getInt("resolutionZ", nz); - voxelwx = 2.0f / nx; - voxelwy = 2.0f / ny; - voxelwz = 2.0f / nz; - invVoxelwx = 1 / voxelwx; - invVoxelwy = 1 / voxelwy; - invVoxelwz = 1 / voxelwz; - return true; - } - - protected abstract boolean inside(int x, int y, int z); - - public BoundingBox getBounds() { - return bounds; - } - - public void prepareShadingState(ShadingState state) { - state.init(); - state.getRay().getPoint(state.getPoint()); - Instance parent = state.getInstance(); - Vector3 normal; - switch (state.getPrimitiveID()) { - case 0: - normal = new Vector3(-1, 0, 0); - break; - case 1: - normal = new Vector3(1, 0, 0); - break; - case 2: - normal = new Vector3(0, -1, 0); - break; - case 3: - normal = new Vector3(0, 1, 0); - break; - case 4: - normal = new Vector3(0, 0, -1); - break; - case 5: - normal = new Vector3(0, 0, 1); - break; - default: - normal = new Vector3(0, 0, 0); - break; - } - state.getNormal().set(state.transformNormalObjectToWorld(normal)); - state.getGeoNormal().set(state.getNormal()); - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - state.setShader(parent.getShader(0)); - state.setModifier(parent.getModifier(0)); - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - float intervalMin = r.getMin(); - float intervalMax = r.getMax(); - float orgX = r.ox; - float orgY = r.oy; - float orgZ = r.oz; - float dirX = r.dx, invDirX = 1 / dirX; - float dirY = r.dy, invDirY = 1 / dirY; - float dirZ = r.dz, invDirZ = 1 / dirZ; - float t1, t2; - t1 = (-1 - orgX) * invDirX; - t2 = (+1 - orgX) * invDirX; - int curr = -1; - if (invDirX > 0) { - if (t1 > intervalMin) { - intervalMin = t1; - curr = 0; - } - if (t2 < intervalMax) - intervalMax = t2; - if (intervalMin > intervalMax) - return; - } else { - if (t2 > intervalMin) { - intervalMin = t2; - curr = 1; - } - if (t1 < intervalMax) - intervalMax = t1; - if (intervalMin > intervalMax) - return; - } - t1 = (-1 - orgY) * invDirY; - t2 = (+1 - orgY) * invDirY; - if (invDirY > 0) { - if (t1 > intervalMin) { - intervalMin = t1; - curr = 2; - } - if (t2 < intervalMax) - intervalMax = t2; - if (intervalMin > intervalMax) - return; - } else { - if (t2 > intervalMin) { - intervalMin = t2; - curr = 3; - } - if (t1 < intervalMax) - intervalMax = t1; - if (intervalMin > intervalMax) - return; - } - t1 = (-1 - orgZ) * invDirZ; - t2 = (+1 - orgZ) * invDirZ; - if (invDirZ > 0) { - if (t1 > intervalMin) { - intervalMin = t1; - curr = 4; - } - if (t2 < intervalMax) - intervalMax = t2; - if (intervalMin > intervalMax) - return; - } else { - if (t2 > intervalMin) { - intervalMin = t2; - curr = 5; - } - if (t1 < intervalMax) - intervalMax = t1; - if (intervalMin > intervalMax) - return; - } - // box is hit at [intervalMin, intervalMax] - orgX += intervalMin * dirX; - orgY += intervalMin * dirY; - orgZ += intervalMin * dirZ; - // locate starting point inside the grid - // and set up 3D-DDA vars - int indxX, indxY, indxZ; - int stepX, stepY, stepZ; - int stopX, stopY, stopZ; - float deltaX, deltaY, deltaZ; - float tnextX, tnextY, tnextZ; - // stepping factors along X - indxX = (int) ((orgX + 1) * invVoxelwx); - if (indxX < 0) - indxX = 0; - else if (indxX >= nx) - indxX = nx - 1; - if (Math.abs(dirX) < 1e-6f) { - stepX = 0; - stopX = indxX; - deltaX = 0; - tnextX = Float.POSITIVE_INFINITY; - } else if (dirX > 0) { - stepX = 1; - stopX = nx; - deltaX = voxelwx * invDirX; - tnextX = intervalMin + ((indxX + 1) * voxelwx - 1 - orgX) * invDirX; - } else { - stepX = -1; - stopX = -1; - deltaX = -voxelwx * invDirX; - tnextX = intervalMin + (indxX * voxelwx - 1 - orgX) * invDirX; - } - // stepping factors along Y - indxY = (int) ((orgY + 1) * invVoxelwy); - if (indxY < 0) - indxY = 0; - else if (indxY >= ny) - indxY = ny - 1; - if (Math.abs(dirY) < 1e-6f) { - stepY = 0; - stopY = indxY; - deltaY = 0; - tnextY = Float.POSITIVE_INFINITY; - } else if (dirY > 0) { - stepY = 1; - stopY = ny; - deltaY = voxelwy * invDirY; - tnextY = intervalMin + ((indxY + 1) * voxelwy - 1 - orgY) * invDirY; - } else { - stepY = -1; - stopY = -1; - deltaY = -voxelwy * invDirY; - tnextY = intervalMin + (indxY * voxelwy - 1 - orgY) * invDirY; - } - // stepping factors along Z - indxZ = (int) ((orgZ + 1) * invVoxelwz); - if (indxZ < 0) - indxZ = 0; - else if (indxZ >= nz) - indxZ = nz - 1; - if (Math.abs(dirZ) < 1e-6f) { - stepZ = 0; - stopZ = indxZ; - deltaZ = 0; - tnextZ = Float.POSITIVE_INFINITY; - } else if (dirZ > 0) { - stepZ = 1; - stopZ = nz; - deltaZ = voxelwz * invDirZ; - tnextZ = intervalMin + ((indxZ + 1) * voxelwz - 1 - orgZ) * invDirZ; - } else { - stepZ = -1; - stopZ = -1; - deltaZ = -voxelwz * invDirZ; - tnextZ = intervalMin + (indxZ * voxelwz - 1 - orgZ) * invDirZ; - } - // are we starting inside the cube - boolean isInside = inside(indxX, indxY, indxZ) && bounds.contains(r.ox, r.oy, r.oz); - // trace through the grid - for (;;) { - if (inside(indxX, indxY, indxZ) != isInside) { - // we hit a boundary - r.setMax(intervalMin); - // if we are inside, the last bit needs to be flipped - if (isInside) - curr ^= 1; - state.setIntersection(curr); - return; - } - if (tnextX < tnextY && tnextX < tnextZ) { - curr = dirX > 0 ? 0 : 1; - intervalMin = tnextX; - if (intervalMin > intervalMax) - return; - indxX += stepX; - if (indxX == stopX) - return; - tnextX += deltaX; - } else if (tnextY < tnextZ) { - curr = dirY > 0 ? 2 : 3; - intervalMin = tnextY; - if (intervalMin > intervalMax) - return; - indxY += stepY; - if (indxY == stopY) - return; - tnextY += deltaY; - } else { - curr = dirZ > 0 ? 4 : 5; - intervalMin = tnextZ; - if (intervalMin > intervalMax) - return; - indxZ += stepZ; - if (indxZ == stopZ) - return; - tnextZ += deltaZ; - } - } - } - - public int getNumPrimitives() { - return 1; - } - - public float getPrimitiveBound(int primID, int i) { - return ((i & 1) == 0) ? -1 : 1; - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - if (o2w == null) - return bounds; - return o2w.transform(bounds); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/Cylinder.java b/src/org/sunflow/core/primitive/Cylinder.java deleted file mode 100644 index e6b8067..0000000 --- a/src/org/sunflow/core/primitive/Cylinder.java +++ /dev/null @@ -1,93 +0,0 @@ -package org.sunflow.core.primitive; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Solvers; -import org.sunflow.math.Vector3; - -public class Cylinder implements PrimitiveList { - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(1); - if (o2w != null) - bounds = o2w.transform(bounds); - return bounds; - } - - public float getPrimitiveBound(int primID, int i) { - return (i & 1) == 0 ? -1 : 1; - } - - public int getNumPrimitives() { - return 1; - } - - public void prepareShadingState(ShadingState state) { - state.init(); - state.getRay().getPoint(state.getPoint()); - Instance parent = state.getInstance(); - Point3 localPoint = state.transformWorldToObject(state.getPoint()); - state.getNormal().set(localPoint.x, localPoint.y, 0); - state.getNormal().normalize(); - - float phi = (float) Math.atan2(state.getNormal().y, state.getNormal().x); - if (phi < 0) - phi += 2 * Math.PI; - state.getUV().x = phi / (float) (2 * Math.PI); - state.getUV().y = (localPoint.z + 1) * 0.5f; - state.setShader(parent.getShader(0)); - state.setModifier(parent.getModifier(0)); - // into world space - Vector3 worldNormal = state.transformNormalObjectToWorld(state.getNormal()); - Vector3 v = state.transformVectorObjectToWorld(new Vector3(0, 0, 1)); - state.getNormal().set(worldNormal); - state.getNormal().normalize(); - state.getGeoNormal().set(state.getNormal()); - // compute basis in world space - state.setBasis(OrthoNormalBasis.makeFromWV(state.getNormal(), v)); - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - // intersect in local space - float qa = r.dx * r.dx + r.dy * r.dy; - float qb = 2 * ((r.dx * r.ox) + (r.dy * r.oy)); - float qc = ((r.ox * r.ox) + (r.oy * r.oy)) - 1; - double[] t = Solvers.solveQuadric(qa, qb, qc); - if (t != null) { - // early rejection - if (t[0] >= r.getMax() || t[1] <= r.getMin()) - return; - if (t[0] > r.getMin()) { - float z = r.oz + (float) t[0] * r.dz; - if (z >= -1 && z <= 1) { - r.setMax((float) t[0]); - state.setIntersection(0); - return; - } - } - if (t[1] < r.getMax()) { - float z = r.oz + (float) t[1] * r.dz; - if (z >= -1 && z <= 1) { - r.setMax((float) t[1]); - state.setIntersection(0); - } - } - } - } - - public PrimitiveList getBakingPrimitives() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/Hair.java b/src/org/sunflow/core/primitive/Hair.java deleted file mode 100644 index a74b777..0000000 --- a/src/org/sunflow/core/primitive/Hair.java +++ /dev/null @@ -1,261 +0,0 @@ -package org.sunflow.core.primitive; - -import java.util.Locale; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.LightSample; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.core.ParameterList.FloatParameter; -import org.sunflow.core.ParameterList.InterpolationType; -import org.sunflow.image.Color; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Vector3; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class Hair implements PrimitiveList, Shader { - private int numSegments; - private float[] points; - private FloatParameter widths; - - public Hair() { - numSegments = 1; - points = null; - widths = new FloatParameter(1.0f); - } - - public int getNumPrimitives() { - return numSegments * (points.length / (3 * (numSegments + 1))); - } - - public float getPrimitiveBound(int primID, int i) { - int hair = primID / numSegments; - int line = primID % numSegments; - int vn = hair * (numSegments + 1) + line; - int vRoot = hair * 3 * (numSegments + 1); - int v0 = vRoot + line * 3; - int v1 = v0 + 3; - int axis = i >>> 1; - if ((i & 1) == 0) { - return Math.min(points[v0 + axis] - 0.5f * getWidth(vn), points[v1 + axis] - 0.5f * getWidth(vn + 1)); - } else { - return Math.max(points[v0 + axis] + 0.5f * getWidth(vn), points[v1 + axis] + 0.5f * getWidth(vn + 1)); - } - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(); - for (int i = 0, j = 0; i < points.length; i += 3, j++) { - float w = 0.5f * getWidth(j); - bounds.include(points[i] - w, points[i + 1] - w, points[i + 2] - w); - bounds.include(points[i] + w, points[i + 1] + w, points[i + 2] + w); - } - if (o2w != null) - bounds = o2w.transform(bounds); - return bounds; - } - - private float getWidth(int i) { - switch (widths.interp) { - case NONE: - return widths.data[0]; - case VERTEX: - return widths.data[i]; - default: - return 0; - } - } - - private Vector3 getTangent(int line, int v0, float v) { - Vector3 vcurr = new Vector3(points[v0 + 3] - points[v0 + 0], points[v0 + 4] - points[v0 + 1], points[v0 + 5] - points[v0 + 2]); - vcurr.normalize(); - if (line == 0 || line == numSegments - 1) - return vcurr; - if (v <= 0.5f) { - // get previous segment - Vector3 vprev = new Vector3(points[v0 + 0] - points[v0 - 3], points[v0 + 1] - points[v0 - 2], points[v0 + 2] - points[v0 - 1]); - vprev.normalize(); - float t = v + 0.5f; - float s = 1 - t; - float vx = vprev.x * s + vcurr.x * t; - float vy = vprev.y * s + vcurr.y * t; - float vz = vprev.z * s + vcurr.z * t; - return new Vector3(vx, vy, vz); - } else { - // get next segment - v0 += 3; - Vector3 vnext = new Vector3(points[v0 + 3] - points[v0 + 0], points[v0 + 4] - points[v0 + 1], points[v0 + 5] - points[v0 + 2]); - vnext.normalize(); - float t = 1.5f - v; - float s = 1 - t; - float vx = vnext.x * s + vcurr.x * t; - float vy = vnext.y * s + vcurr.y * t; - float vz = vnext.z * s + vcurr.z * t; - return new Vector3(vx, vy, vz); - } - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - int hair = primID / numSegments; - int line = primID % numSegments; - int vRoot = hair * 3 * (numSegments + 1); - int v0 = vRoot + line * 3; - int v1 = v0 + 3; - float vx = points[v1 + 0] - points[v0 + 0]; - float vy = points[v1 + 1] - points[v0 + 1]; - float vz = points[v1 + 2] - points[v0 + 2]; - float ux = r.dy * vz - r.dz * vy; - float uy = r.dz * vx - r.dx * vz; - float uz = r.dx * vy - r.dy * vx; - float nx = uy * vz - uz * vy; - float ny = uz * vx - ux * vz; - float nz = ux * vy - uy * vx; - float tden = 1 / (nx * r.dx + ny * r.dy + nz * r.dz); - float tnum = nx * (points[v0 + 0] - r.ox) + ny * (points[v0 + 1] - r.oy) + nz * (points[v0 + 2] - r.oz); - float t = tnum * tden; - if (r.isInside(t)) { - int vn = hair * (numSegments + 1) + line; - float px = r.ox + t * r.dx; - float py = r.oy + t * r.dy; - float pz = r.oz + t * r.dz; - float qx = px - points[v0 + 0]; - float qy = py - points[v0 + 1]; - float qz = pz - points[v0 + 2]; - float q = (vx * qx + vy * qy + vz * qz) / (vx * vx + vy * vy + vz * vz); - if (q <= 0) { - // don't included rounded tip at root - if (line == 0) - return; - float dx = points[v0 + 0] - px; - float dy = points[v0 + 1] - py; - float dz = points[v0 + 2] - pz; - float d2 = dx * dx + dy * dy + dz * dz; - float width = getWidth(vn); - if (d2 < (width * width * 0.25f)) { - r.setMax(t); - state.setIntersection(primID, 0, 0); - } - } else if (q >= 1) { - float dx = points[v1 + 0] - px; - float dy = points[v1 + 1] - py; - float dz = points[v1 + 2] - pz; - float d2 = dx * dx + dy * dy + dz * dz; - float width = getWidth(vn + 1); - if (d2 < (width * width * 0.25f)) { - r.setMax(t); - state.setIntersection(primID, 0, 1); - } - } else { - float dx = points[v0 + 0] + q * vx - px; - float dy = points[v0 + 1] + q * vy - py; - float dz = points[v0 + 2] + q * vz - pz; - float d2 = dx * dx + dy * dy + dz * dz; - float width = (1 - q) * getWidth(vn) + q * getWidth(vn + 1); - if (d2 < (width * width * 0.25f)) { - r.setMax(t); - state.setIntersection(primID, 0, q); - } - } - } - } - - public void prepareShadingState(ShadingState state) { - state.init(); - Instance i = state.getInstance(); - state.getRay().getPoint(state.getPoint()); - Ray r = state.getRay(); - Shader s = i.getShader(0); - state.setShader(s != null ? s : this); - int primID = state.getPrimitiveID(); - int hair = primID / numSegments; - int line = primID % numSegments; - int vRoot = hair * 3 * (numSegments + 1); - int v0 = vRoot + line * 3; - - // tangent vector - Vector3 v = getTangent(line, v0, state.getV()); - v = state.transformVectorObjectToWorld(v); - state.setBasis(OrthoNormalBasis.makeFromWV(v, new Vector3(-r.dx, -r.dy, -r.dz))); - state.getBasis().swapVW(); - // normal - state.getNormal().set(0, 0, 1); - state.getBasis().transform(state.getNormal()); - state.getGeoNormal().set(state.getNormal()); - - state.getUV().set(0, (line + state.getV()) / numSegments); - } - - public boolean update(ParameterList pl, SunflowAPI api) { - numSegments = pl.getInt("segments", numSegments); - if (numSegments < 1) { - UI.printError(Module.HAIR, "Invalid number of segments: %d", numSegments); - return false; - } - FloatParameter pointsP = pl.getPointArray("points"); - if (pointsP != null) { - if (pointsP.interp != InterpolationType.VERTEX) - UI.printError(Module.HAIR, "Point interpolation type must be set to \"vertex\" - was \"%s\"", pointsP.interp.name().toLowerCase(Locale.ENGLISH)); - else { - points = pointsP.data; - } - } - if (points == null) { - UI.printError(Module.HAIR, "Unabled to update hair - vertices are missing"); - return false; - } - - pl.setVertexCount(points.length / 3); - FloatParameter widthsP = pl.getFloatArray("widths"); - if (widthsP != null) { - if (widthsP.interp == InterpolationType.NONE || widthsP.interp == InterpolationType.VERTEX) - widths = widthsP; - else - UI.printWarning(Module.HAIR, "Width interpolation type %s is not supported -- ignoring", widthsP.interp.name().toLowerCase(Locale.ENGLISH)); - } - return true; - } - - public Color getRadiance(ShadingState state) { - // don't use these - gather lights for sphere of directions - // gather lights - state.initLightSamples(); - state.initCausticSamples(); - Vector3 v = state.getRay().getDirection(); - v.negate(); - Vector3 h = new Vector3(); - Vector3 t = state.getBasis().transform(new Vector3(0, 1, 0)); - Color diff = Color.black(); - Color spec = Color.black(); - for (LightSample ls : state) { - Vector3 l = ls.getShadowRay().getDirection(); - float dotTL = Vector3.dot(t, l); - float sinTL = (float) Math.sqrt(1 - dotTL * dotTL); - // float dotVL = Vector3.dot(v, l); - diff.madd(sinTL, ls.getDiffuseRadiance()); - Vector3.add(v, l, h); - h.normalize(); - float dotTH = Vector3.dot(t, h); - float sinTH = (float) Math.sqrt(1 - dotTH * dotTH); - float s = (float) Math.pow(sinTH, 10.0f); - spec.madd(s, ls.getSpecularRadiance()); - } - Color c = Color.add(diff, spec, new Color()); - // transparency - return Color.blend(c, state.traceTransparency(), state.getV(), new Color()); - } - - public void scatterPhoton(ShadingState state, Color power) { - } - - public PrimitiveList getBakingPrimitives() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/JuliaFractal.java b/src/org/sunflow/core/primitive/JuliaFractal.java deleted file mode 100644 index a6b6c2a..0000000 --- a/src/org/sunflow/core/primitive/JuliaFractal.java +++ /dev/null @@ -1,253 +0,0 @@ -package org.sunflow.core.primitive; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Solvers; -import org.sunflow.math.Vector3; - -public class JuliaFractal implements PrimitiveList { - private static float BOUNDING_RADIUS = (float) Math.sqrt(3); - private static float BOUNDING_RADIUS2 = 3; - private static float ESCAPE_THRESHOLD = 1e1f; - private static float DELTA = 1e-4f; - - // quaternion constant - private float cx; - private float cy; - private float cz; - private float cw; - private int maxIterations; - private float epsilon; - - public JuliaFractal() { - // good defaults? - cw = -.4f; - cx = .2f; - cy = .3f; - cz = -.2f; - - maxIterations = 15; - epsilon = 0.00001f; - } - - public int getNumPrimitives() { - return 1; - } - - public float getPrimitiveBound(int primID, int i) { - return ((i & 1) == 0) ? -BOUNDING_RADIUS : BOUNDING_RADIUS; - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(BOUNDING_RADIUS); - if (o2w != null) - bounds = o2w.transform(bounds); - return bounds; - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - // intersect with bounding sphere - float qc = ((r.ox * r.ox) + (r.oy * r.oy) + (r.oz * r.oz)) - BOUNDING_RADIUS2; - float qt = r.getMin(); - if (qc > 0) { - // we are starting outside the sphere, find intersection on the - // sphere - float qa = r.dx * r.dx + r.dy * r.dy + r.dz * r.dz; - float qb = 2 * ((r.dx * r.ox) + (r.dy * r.oy) + (r.dz * r.oz)); - double[] t = Solvers.solveQuadric(qa, qb, qc); - // early rejection - if (t == null || t[0] >= r.getMax() || t[1] <= r.getMin()) - return; - qt = (float) t[0]; - } - float dist = Float.POSITIVE_INFINITY; - float rox = r.ox + qt * r.dx; - float roy = r.oy + qt * r.dy; - float roz = r.oz + qt * r.dz; - float invRayLength = (float) (1 / Math.sqrt(r.dx * r.dx + r.dy * r.dy + r.dz * r.dz)); - // now we can start intersection - while (true) { - float zw = rox; - float zx = roy; - float zy = roz; - float zz = 0; - - float zpw = 1; - float zpx = 0; - float zpy = 0; - float zpz = 0; - - // run several iterations - float dotz = 0; - for (int i = 0; i < maxIterations; i++) { - { - // zp = 2 * (z * zp) - float nw = zw * zpw - zx * zpx - zy * zpy - zz * zpz; - float nx = zw * zpx + zx * zpw + zy * zpz - zz * zpy; - float ny = zw * zpy + zy * zpw + zz * zpx - zx * zpz; - zpz = 2 * (zw * zpz + zz * zpw + zx * zpy - zy * zpx); - zpw = 2 * nw; - zpx = 2 * nx; - zpy = 2 * ny; - } - { - // z = z*z + c - float nw = zw * zw - zx * zx - zy * zy - zz * zz + cw; - zx = 2 * zw * zx + cx; - zy = 2 * zw * zy + cy; - zz = 2 * zw * zz + cz; - zw = nw; - } - dotz = zw * zw + zx * zx + zy * zy + zz * zz; - if (dotz > ESCAPE_THRESHOLD) - break; - - } - float normZ = (float) Math.sqrt(dotz); - dist = 0.5f * normZ * (float) Math.log(normZ) / length(zpw, zpx, zpy, zpz); - rox += dist * r.dx; - roy += dist * r.dy; - roz += dist * r.dz; - qt += dist; - if (dist * invRayLength < epsilon) - break; - if (rox * rox + roy * roy + roz * roz > BOUNDING_RADIUS2) - return; - } - // now test t value again - if (!r.isInside(qt)) - return; - if (dist * invRayLength < epsilon) { - // valid hit - r.setMax(qt); - state.setIntersection(0); - } - } - - public void prepareShadingState(ShadingState state) { - state.init(); - state.getRay().getPoint(state.getPoint()); - Instance parent = state.getInstance(); - // compute local normal - Point3 p = state.transformWorldToObject(state.getPoint()); - float gx1w = p.x - DELTA; - float gx1x = p.y; - float gx1y = p.z; - float gx1z = 0; - float gx2w = p.x + DELTA; - float gx2x = p.y; - float gx2y = p.z; - float gx2z = 0; - - float gy1w = p.x; - float gy1x = p.y - DELTA; - float gy1y = p.z; - float gy1z = 0; - float gy2w = p.x; - float gy2x = p.y + DELTA; - float gy2y = p.z; - float gy2z = 0; - - float gz1w = p.x; - float gz1x = p.y; - float gz1y = p.z - DELTA; - float gz1z = 0; - float gz2w = p.x; - float gz2x = p.y; - float gz2y = p.z + DELTA; - float gz2z = 0; - - for (int i = 0; i < maxIterations; i++) { - { - // z = z*z + c - float nw = gx1w * gx1w - gx1x * gx1x - gx1y * gx1y - gx1z * gx1z + cw; - gx1x = 2 * gx1w * gx1x + cx; - gx1y = 2 * gx1w * gx1y + cy; - gx1z = 2 * gx1w * gx1z + cz; - gx1w = nw; - } - { - // z = z*z + c - float nw = gx2w * gx2w - gx2x * gx2x - gx2y * gx2y - gx2z * gx2z + cw; - gx2x = 2 * gx2w * gx2x + cx; - gx2y = 2 * gx2w * gx2y + cy; - gx2z = 2 * gx2w * gx2z + cz; - gx2w = nw; - } - { - // z = z*z + c - float nw = gy1w * gy1w - gy1x * gy1x - gy1y * gy1y - gy1z * gy1z + cw; - gy1x = 2 * gy1w * gy1x + cx; - gy1y = 2 * gy1w * gy1y + cy; - gy1z = 2 * gy1w * gy1z + cz; - gy1w = nw; - } - { - // z = z*z + c - float nw = gy2w * gy2w - gy2x * gy2x - gy2y * gy2y - gy2z * gy2z + cw; - gy2x = 2 * gy2w * gy2x + cx; - gy2y = 2 * gy2w * gy2y + cy; - gy2z = 2 * gy2w * gy2z + cz; - gy2w = nw; - } - { - // z = z*z + c - float nw = gz1w * gz1w - gz1x * gz1x - gz1y * gz1y - gz1z * gz1z + cw; - gz1x = 2 * gz1w * gz1x + cx; - gz1y = 2 * gz1w * gz1y + cy; - gz1z = 2 * gz1w * gz1z + cz; - gz1w = nw; - } - { - // z = z*z + c - float nw = gz2w * gz2w - gz2x * gz2x - gz2y * gz2y - gz2z * gz2z + cw; - gz2x = 2 * gz2w * gz2x + cx; - gz2y = 2 * gz2w * gz2y + cy; - gz2z = 2 * gz2w * gz2z + cz; - gz2w = nw; - } - } - float gradX = length(gx2w, gx2x, gx2y, gx2z) - length(gx1w, gx1x, gx1y, gx1z); - float gradY = length(gy2w, gy2x, gy2y, gy2z) - length(gy1w, gy1x, gy1y, gy1z); - float gradZ = length(gz2w, gz2x, gz2y, gz2z) - length(gz1w, gz1x, gz1y, gz1z); - Vector3 n = new Vector3(gradX, gradY, gradZ); - state.getNormal().set(state.transformNormalObjectToWorld(n)); - state.getNormal().normalize(); - state.getGeoNormal().set(state.getNormal()); - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - - state.getPoint().x += state.getNormal().x * epsilon * 20; - state.getPoint().y += state.getNormal().y * epsilon * 20; - state.getPoint().z += state.getNormal().z * epsilon * 20; - - state.setShader(parent.getShader(0)); - state.setModifier(parent.getModifier(0)); - } - - private static float length(float w, float x, float y, float z) { - return (float) Math.sqrt(w * w + x * x + y * y + z * z); - } - - public boolean update(ParameterList pl, SunflowAPI api) { - maxIterations = pl.getInt("iterations", maxIterations); - epsilon = pl.getFloat("epsilon", epsilon); - cw = pl.getFloat("cw", cw); - cx = pl.getFloat("cx", cx); - cy = pl.getFloat("cy", cy); - cz = pl.getFloat("cz", cz); - return true; - } - - public PrimitiveList getBakingPrimitives() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/ParticleSurface.java b/src/org/sunflow/core/primitive/ParticleSurface.java deleted file mode 100644 index 39c9d04..0000000 --- a/src/org/sunflow/core/primitive/ParticleSurface.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.sunflow.core.primitive; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.core.ParameterList.FloatParameter; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Solvers; -import org.sunflow.math.Vector3; - -public class ParticleSurface implements PrimitiveList { - private float[] particles; - private float r, r2; - private int n; - - public ParticleSurface() { - particles = null; - r = r2 = 1; - n = 0; - } - - public int getNumPrimitives() { - return n; - } - - public float getPrimitiveBound(int primID, int i) { - float c = particles[primID * 3 + (i >>> 1)]; - return (i & 1) == 0 ? c - r : c + r; - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(); - for (int i = 0, i3 = 0; i < n; i++, i3 += 3) - bounds.include(particles[i3], particles[i3 + 1], particles[i3 + 2]); - bounds.include(bounds.getMinimum().x - r, bounds.getMinimum().y - r, bounds.getMinimum().z - r); - bounds.include(bounds.getMaximum().x + r, bounds.getMaximum().y + r, bounds.getMaximum().z + r); - return o2w == null ? bounds : o2w.transform(bounds); - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - int i3 = primID * 3; - float ocx = r.ox - particles[i3 + 0]; - float ocy = r.oy - particles[i3 + 1]; - float ocz = r.oz - particles[i3 + 2]; - float qa = r.dx * r.dx + r.dy * r.dy + r.dz * r.dz; - float qb = 2 * ((r.dx * ocx) + (r.dy * ocy) + (r.dz * ocz)); - float qc = ((ocx * ocx) + (ocy * ocy) + (ocz * ocz)) - r2; - double[] t = Solvers.solveQuadric(qa, qb, qc); - if (t != null) { - // early rejection - if (t[0] >= r.getMax() || t[1] <= r.getMin()) - return; - if (t[0] > r.getMin()) - r.setMax((float) t[0]); - else - r.setMax((float) t[1]); - state.setIntersection(primID); - } - } - - public void prepareShadingState(ShadingState state) { - state.init(); - state.getRay().getPoint(state.getPoint()); - Point3 localPoint = state.transformWorldToObject(state.getPoint()); - - localPoint.x -= particles[3 * state.getPrimitiveID() + 0]; - localPoint.y -= particles[3 * state.getPrimitiveID() + 1]; - localPoint.z -= particles[3 * state.getPrimitiveID() + 2]; - - state.getNormal().set(localPoint.x, localPoint.y, localPoint.z); - state.getNormal().normalize(); - - state.setShader(state.getInstance().getShader(0)); - state.setModifier(state.getInstance().getModifier(0)); - // into object space - Vector3 worldNormal = state.transformNormalObjectToWorld(state.getNormal()); - state.getNormal().set(worldNormal); - state.getNormal().normalize(); - state.getGeoNormal().set(state.getNormal()); - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - } - - public boolean update(ParameterList pl, SunflowAPI api) { - FloatParameter p = pl.getPointArray("particles"); - if (p != null) - particles = p.data; - r = pl.getFloat("radius", r); - r2 = r * r; - n = pl.getInt("num", n); - return particles != null && n <= (particles.length / 3); - } - - public PrimitiveList getBakingPrimitives() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/Plane.java b/src/org/sunflow/core/primitive/Plane.java deleted file mode 100644 index b9e8eee..0000000 --- a/src/org/sunflow/core/primitive/Plane.java +++ /dev/null @@ -1,153 +0,0 @@ -package org.sunflow.core.primitive; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; - -public class Plane implements PrimitiveList { - private Point3 center; - private Vector3 normal; - int k; - private float bnu, bnv, bnd; - private float cnu, cnv, cnd; - - public Plane() { - center = new Point3(0, 0, 0); - normal = new Vector3(0, 1, 0); - k = 3; - bnu = bnv = bnd = 0; - cnu = cnv = cnd = 0; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - center = pl.getPoint("center", center); - Point3 b = pl.getPoint("point1", null); - Point3 c = pl.getPoint("point2", null); - if (b != null && c != null) { - Point3 v0 = center; - Point3 v1 = b; - Point3 v2 = c; - Vector3 ng = normal = Vector3.cross(Point3.sub(v1, v0, new Vector3()), Point3.sub(v2, v0, new Vector3()), new Vector3()).normalize(); - if (Math.abs(ng.x) > Math.abs(ng.y) && Math.abs(ng.x) > Math.abs(ng.z)) - k = 0; - else if (Math.abs(ng.y) > Math.abs(ng.z)) - k = 1; - else - k = 2; - float ax, ay, bx, by, cx, cy; - switch (k) { - case 0: { - ax = v0.y; - ay = v0.z; - bx = v2.y - ax; - by = v2.z - ay; - cx = v1.y - ax; - cy = v1.z - ay; - break; - } - case 1: { - ax = v0.z; - ay = v0.x; - bx = v2.z - ax; - by = v2.x - ay; - cx = v1.z - ax; - cy = v1.x - ay; - break; - } - case 2: - default: { - ax = v0.x; - ay = v0.y; - bx = v2.x - ax; - by = v2.y - ay; - cx = v1.x - ax; - cy = v1.y - ay; - } - } - float det = bx * cy - by * cx; - bnu = -by / det; - bnv = bx / det; - bnd = (by * ax - bx * ay) / det; - cnu = cy / det; - cnv = -cx / det; - cnd = (cx * ay - cy * ax) / det; - } else { - normal = pl.getVector("normal", normal); - k = 3; - bnu = bnv = bnd = 0; - cnu = cnv = cnd = 0; - } - return true; - } - - public void prepareShadingState(ShadingState state) { - state.init(); - state.getRay().getPoint(state.getPoint()); - Instance parent = state.getInstance(); - Vector3 worldNormal = state.transformNormalObjectToWorld(normal); - state.getNormal().set(worldNormal); - state.getGeoNormal().set(worldNormal); - state.setShader(parent.getShader(0)); - state.setModifier(parent.getModifier(0)); - Point3 p = state.transformWorldToObject(state.getPoint()); - float hu, hv; - switch (k) { - case 0: { - hu = p.y; - hv = p.z; - break; - } - case 1: { - hu = p.z; - hv = p.x; - break; - } - case 2: { - hu = p.x; - hv = p.y; - break; - } - default: - hu = hv = 0; - } - state.getUV().x = hu * bnu + hv * bnv + bnd; - state.getUV().y = hu * cnu + hv * cnv + cnd; - state.setBasis(OrthoNormalBasis.makeFromW(normal)); - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - float dn = normal.x * r.dx + normal.y * r.dy + normal.z * r.dz; - if (dn == 0.0) - return; - float t = (((center.x - r.ox) * normal.x) + ((center.y - r.oy) * normal.y) + ((center.z - r.oz) * normal.z)) / dn; - if (r.isInside(t)) { - r.setMax(t); - state.setIntersection(0); - } - } - - public int getNumPrimitives() { - return 1; - } - - public float getPrimitiveBound(int primID, int i) { - return 0; - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - return null; - } - - public PrimitiveList getBakingPrimitives() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/QuadMesh.java b/src/org/sunflow/core/primitive/QuadMesh.java deleted file mode 100644 index c02e5a6..0000000 --- a/src/org/sunflow/core/primitive/QuadMesh.java +++ /dev/null @@ -1,392 +0,0 @@ -package org.sunflow.core.primitive; - -import java.io.FileWriter; -import java.io.IOException; -import java.util.Locale; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.core.ParameterList.FloatParameter; -import org.sunflow.core.ParameterList.InterpolationType; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.MathUtils; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class QuadMesh implements PrimitiveList { - protected float[] points; - protected int[] quads; - private FloatParameter normals; - private FloatParameter uvs; - private byte[] faceShaders; - - public QuadMesh() { - quads = null; - points = null; - normals = uvs = new FloatParameter(); - faceShaders = null; - } - - public void writeObj(String filename) { - try { - FileWriter file = new FileWriter(filename); - file.write(String.format("o object\n")); - for (int i = 0; i < points.length; i += 3) - file.write(String.format("v %g %g %g\n", points[i], points[i + 1], points[i + 2])); - file.write("s off\n"); - for (int i = 0; i < quads.length; i += 4) - file.write(String.format("f %d %d %d %d\n", quads[i] + 1, quads[i + 1] + 1, quads[i + 2] + 1, quads[i + 3] + 1)); - file.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public boolean update(ParameterList pl, SunflowAPI api) { - { - int[] quads = pl.getIntArray("quads"); - if (quads != null) { - this.quads = quads; - } - } - if (quads == null) { - UI.printError(Module.GEOM, "Unable to update mesh - quad indices are missing"); - return false; - } - if (quads.length % 4 != 0) - UI.printWarning(Module.GEOM, "Quad index data is not a multiple of 4 - some quads may be missing"); - pl.setFaceCount(quads.length / 4); - { - FloatParameter pointsP = pl.getPointArray("points"); - if (pointsP != null) - if (pointsP.interp != InterpolationType.VERTEX) - UI.printError(Module.GEOM, "Point interpolation type must be set to \"vertex\" - was \"%s\"", pointsP.interp.name().toLowerCase(Locale.ENGLISH)); - else { - points = pointsP.data; - } - } - if (points == null) { - UI.printError(Module.GEOM, "Unabled to update mesh - vertices are missing"); - return false; - } - pl.setVertexCount(points.length / 3); - pl.setFaceVertexCount(4 * (quads.length / 4)); - FloatParameter normals = pl.getVectorArray("normals"); - if (normals != null) - this.normals = normals; - FloatParameter uvs = pl.getTexCoordArray("uvs"); - if (uvs != null) - this.uvs = uvs; - int[] faceShaders = pl.getIntArray("faceshaders"); - if (faceShaders != null && faceShaders.length == quads.length / 4) { - this.faceShaders = new byte[faceShaders.length]; - for (int i = 0; i < faceShaders.length; i++) { - int v = faceShaders[i]; - if (v > 255) - UI.printWarning(Module.GEOM, "Shader index too large on quad %d", i); - this.faceShaders[i] = (byte) (v & 0xFF); - } - } - return true; - } - - public float getPrimitiveBound(int primID, int i) { - int quad = 4 * primID; - int a = 3 * quads[quad + 0]; - int b = 3 * quads[quad + 1]; - int c = 3 * quads[quad + 2]; - int d = 3 * quads[quad + 3]; - int axis = i >>> 1; - if ((i & 1) == 0) - return MathUtils.min(points[a + axis], points[b + axis], points[c + axis], points[d + axis]); - else - return MathUtils.max(points[a + axis], points[b + axis], points[c + axis], points[d + axis]); - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(); - if (o2w == null) { - for (int i = 0; i < points.length; i += 3) - bounds.include(points[i], points[i + 1], points[i + 2]); - } else { - // transform vertices first - for (int i = 0; i < points.length; i += 3) { - float x = points[i]; - float y = points[i + 1]; - float z = points[i + 2]; - float wx = o2w.transformPX(x, y, z); - float wy = o2w.transformPY(x, y, z); - float wz = o2w.transformPZ(x, y, z); - bounds.include(wx, wy, wz); - } - } - return bounds; - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - // ray/bilinear patch intersection adapted from "Production Rendering: - // Design and Implementation" by Ian Stephenson (Ed.) - int quad = 4 * primID; - int p0 = 3 * quads[quad + 0]; - int p1 = 3 * quads[quad + 1]; - int p2 = 3 * quads[quad + 2]; - int p3 = 3 * quads[quad + 3]; - // transform patch into Hilbert space - final float A[] = { - points[p2 + 0] - points[p3 + 0] - points[p1 + 0] + points[p0 + 0], - points[p2 + 1] - points[p3 + 1] - points[p1 + 1] + points[p0 + 1], - points[p2 + 2] - points[p3 + 2] - points[p1 + 2] + points[p0 + 2] }; - final float B[] = { points[p1 + 0] - points[p0 + 0], - points[p1 + 1] - points[p0 + 1], - points[p1 + 2] - points[p0 + 2] }; - final float C[] = { points[p3 + 0] - points[p0 + 0], - points[p3 + 1] - points[p0 + 1], - points[p3 + 2] - points[p0 + 2] }; - final float R[] = { r.ox - points[p0 + 0], r.oy - points[p0 + 1], - r.oz - points[p0 + 2] }; - final float Q[] = { r.dx, r.dy, r.dz }; - - // pick major direction - float absqx = Math.abs(r.dx); - float absqy = Math.abs(r.dy); - float absqz = Math.abs(r.dz); - - int X = 0, Y = 1, Z = 2; - if (absqx > absqy && absqx > absqz) { - // X = 0, Y = 1, Z = 2 - } else if (absqy > absqz) { - // X = 1, Y = 0, Z = 2 - X = 1; - Y = 0; - } else { - // X = 2, Y = 1, Z = 0 - X = 2; - Z = 0; - } - - float Cxz = C[X] * Q[Z] - C[Z] * Q[X]; - float Cyx = C[Y] * Q[X] - C[X] * Q[Y]; - float Czy = C[Z] * Q[Y] - C[Y] * Q[Z]; - float Rxz = R[X] * Q[Z] - R[Z] * Q[X]; - float Ryx = R[Y] * Q[X] - R[X] * Q[Y]; - float Rzy = R[Z] * Q[Y] - R[Y] * Q[Z]; - float Bxy = B[X] * Q[Y] - B[Y] * Q[X]; - float Byz = B[Y] * Q[Z] - B[Z] * Q[Y]; - float Bzx = B[Z] * Q[X] - B[X] * Q[Z]; - float a = A[X] * Byz + A[Y] * Bzx + A[Z] * Bxy; - if (a == 0) { - // setup for linear equation - float b = B[X] * Czy + B[Y] * Cxz + B[Z] * Cyx; - float c = C[X] * Rzy + C[Y] * Rxz + C[Z] * Ryx; - float u = -c / b; - if (u >= 0 && u <= 1) { - float v = (u * Bxy + Ryx) / Cyx; - if (v >= 0 && v <= 1) { - float t = (B[X] * u + C[X] * v - R[X]) / Q[X]; - if (r.isInside(t)) { - r.setMax(t); - state.setIntersection(primID, u, v); - } - } - } - } else { - // setup for quadratic equation - float b = A[X] * Rzy + A[Y] * Rxz + A[Z] * Ryx + B[X] * Czy + B[Y] * Cxz + B[Z] * Cyx; - float c = C[X] * Rzy + C[Y] * Rxz + C[Z] * Ryx; - float discrim = b * b - 4 * a * c; - // reject trivial cases - if (c * (a + b + c) > 0 && (discrim < 0 || a * c < 0 || b / a > 0 || b / a < -2)) - return; - // solve quadratic - float q = b > 0 ? -0.5f * (b + (float) Math.sqrt(discrim)) : -0.5f * (b - (float) Math.sqrt(discrim)); - // check first solution - float Axy = A[X] * Q[Y] - A[Y] * Q[X]; - float u = q / a; - if (u >= 0 && u <= 1) { - float d = u * Axy - Cyx; - float v = -(u * Bxy + Ryx) / d; - if (v >= 0 && v <= 1) { - float t = (A[X] * u * v + B[X] * u + C[X] * v - R[X]) / Q[X]; - if (r.isInside(t)) { - r.setMax(t); - state.setIntersection(primID, u, v); - } - } - } - u = c / q; - if (u >= 0 && u <= 1) { - float d = u * Axy - Cyx; - float v = -(u * Bxy + Ryx) / d; - if (v >= 0 && v <= 1) { - float t = (A[X] * u * v + B[X] * u + C[X] * v - R[X]) / Q[X]; - if (r.isInside(t)) { - r.setMax(t); - state.setIntersection(primID, u, v); - } - } - } - } - } - - public int getNumPrimitives() { - return quads.length / 4; - } - - public void prepareShadingState(ShadingState state) { - state.init(); - Instance parent = state.getInstance(); - int primID = state.getPrimitiveID(); - float u = state.getU(); - float v = state.getV(); - state.getRay().getPoint(state.getPoint()); - int quad = 4 * primID; - int index0 = quads[quad + 0]; - int index1 = quads[quad + 1]; - int index2 = quads[quad + 2]; - int index3 = quads[quad + 3]; - Point3 v0p = getPoint(index0); - Point3 v1p = getPoint(index1); - Point3 v2p = getPoint(index2); - Point3 v3p = getPoint(index2); - float tanux = (1 - v) * (v1p.x - v0p.x) + v * (v2p.x - v3p.x); - float tanuy = (1 - v) * (v1p.y - v0p.y) + v * (v2p.y - v3p.y); - float tanuz = (1 - v) * (v1p.z - v0p.z) + v * (v2p.z - v3p.z); - - float tanvx = (1 - u) * (v3p.x - v0p.x) + u * (v2p.x - v1p.x); - float tanvy = (1 - u) * (v3p.y - v0p.y) + u * (v2p.y - v1p.y); - float tanvz = (1 - u) * (v3p.z - v0p.z) + u * (v2p.z - v1p.z); - - float nx = tanuy * tanvz - tanuz * tanvy; - float ny = tanuz * tanvx - tanux * tanvz; - float nz = tanux * tanvy - tanuy * tanvx; - - Vector3 ng = new Vector3(nx, ny, nz); - ng = state.transformNormalObjectToWorld(ng); - ng.normalize(); - state.getGeoNormal().set(ng); - - float k00 = (1 - u) * (1 - v); - float k10 = u * (1 - v); - float k01 = (1 - u) * v; - float k11 = u * v; - - switch (normals.interp) { - case NONE: - case FACE: { - state.getNormal().set(ng); - break; - } - case VERTEX: { - int i30 = 3 * index0; - int i31 = 3 * index1; - int i32 = 3 * index2; - int i33 = 3 * index3; - float[] normals = this.normals.data; - state.getNormal().x = k00 * normals[i30 + 0] + k10 * normals[i31 + 0] + k11 * normals[i32 + 0] + k01 * normals[i33 + 0]; - state.getNormal().y = k00 * normals[i30 + 1] + k10 * normals[i31 + 1] + k11 * normals[i32 + 1] + k01 * normals[i33 + 1]; - state.getNormal().z = k00 * normals[i30 + 2] + k10 * normals[i31 + 2] + k11 * normals[i32 + 2] + k01 * normals[i33 + 2]; - state.getNormal().set(state.transformNormalObjectToWorld(state.getNormal())); - state.getNormal().normalize(); - break; - } - case FACEVARYING: { - int idx = 3 * quad; - float[] normals = this.normals.data; - state.getNormal().x = k00 * normals[idx + 0] + k10 * normals[idx + 3] + k11 * normals[idx + 6] + k01 * normals[idx + 9]; - state.getNormal().y = k00 * normals[idx + 1] + k10 * normals[idx + 4] + k11 * normals[idx + 7] + k01 * normals[idx + 10]; - state.getNormal().z = k00 * normals[idx + 2] + k10 * normals[idx + 5] + k11 * normals[idx + 8] + k01 * normals[idx + 11]; - state.getNormal().set(state.transformNormalObjectToWorld(state.getNormal())); - state.getNormal().normalize(); - break; - } - } - float uv00 = 0, uv01 = 0, uv10 = 0, uv11 = 0, uv20 = 0, uv21 = 0, uv30 = 0, uv31 = 0; - switch (uvs.interp) { - case NONE: - case FACE: { - state.getUV().x = 0; - state.getUV().y = 0; - break; - } - case VERTEX: { - int i20 = 2 * index0; - int i21 = 2 * index1; - int i22 = 2 * index2; - int i23 = 2 * index3; - float[] uvs = this.uvs.data; - uv00 = uvs[i20 + 0]; - uv01 = uvs[i20 + 1]; - uv10 = uvs[i21 + 0]; - uv11 = uvs[i21 + 1]; - uv20 = uvs[i22 + 0]; - uv21 = uvs[i22 + 1]; - uv20 = uvs[i23 + 0]; - uv21 = uvs[i23 + 1]; - break; - } - case FACEVARYING: { - int idx = quad << 1; - float[] uvs = this.uvs.data; - uv00 = uvs[idx + 0]; - uv01 = uvs[idx + 1]; - uv10 = uvs[idx + 2]; - uv11 = uvs[idx + 3]; - uv20 = uvs[idx + 4]; - uv21 = uvs[idx + 5]; - uv30 = uvs[idx + 6]; - uv31 = uvs[idx + 7]; - break; - } - } - if (uvs.interp != InterpolationType.NONE) { - // get exact uv coords and compute tangent vectors - state.getUV().x = k00 * uv00 + k10 * uv10 + k11 * uv20 + k01 * uv30; - state.getUV().y = k00 * uv01 + k10 * uv11 + k11 * uv21 + k01 * uv31; - float du1 = uv00 - uv20; - float du2 = uv10 - uv20; - float dv1 = uv01 - uv21; - float dv2 = uv11 - uv21; - Vector3 dp1 = Point3.sub(v0p, v2p, new Vector3()), dp2 = Point3.sub(v1p, v2p, new Vector3()); - float determinant = du1 * dv2 - dv1 * du2; - if (determinant == 0.0f) { - // create basis in world space - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - } else { - float invdet = 1.f / determinant; - // Vector3 dpdu = new Vector3(); - // dpdu.x = (dv2 * dp1.x - dv1 * dp2.x) * invdet; - // dpdu.y = (dv2 * dp1.y - dv1 * dp2.y) * invdet; - // dpdu.z = (dv2 * dp1.z - dv1 * dp2.z) * invdet; - Vector3 dpdv = new Vector3(); - dpdv.x = (-du2 * dp1.x + du1 * dp2.x) * invdet; - dpdv.y = (-du2 * dp1.y + du1 * dp2.y) * invdet; - dpdv.z = (-du2 * dp1.z + du1 * dp2.z) * invdet; - dpdv = state.transformVectorObjectToWorld(dpdv); - // create basis in world space - state.setBasis(OrthoNormalBasis.makeFromWV(state.getNormal(), dpdv)); - } - } else - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - int shaderIndex = faceShaders == null ? 0 : (faceShaders[primID] & 0xFF); - state.setShader(parent.getShader(shaderIndex)); - state.setModifier(parent.getModifier(shaderIndex)); - } - - protected Point3 getPoint(int i) { - i *= 3; - return new Point3(points[i], points[i + 1], points[i + 2]); - } - - public PrimitiveList getBakingPrimitives() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/Sphere.java b/src/org/sunflow/core/primitive/Sphere.java deleted file mode 100644 index 5b55e74..0000000 --- a/src/org/sunflow/core/primitive/Sphere.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.sunflow.core.primitive; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Solvers; -import org.sunflow.math.Vector3; - -public class Sphere implements PrimitiveList { - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(1); - if (o2w != null) - bounds = o2w.transform(bounds); - return bounds; - } - - public float getPrimitiveBound(int primID, int i) { - return (i & 1) == 0 ? -1 : 1; - } - - public int getNumPrimitives() { - return 1; - } - - public void prepareShadingState(ShadingState state) { - state.init(); - state.getRay().getPoint(state.getPoint()); - Instance parent = state.getInstance(); - Point3 localPoint = state.transformWorldToObject(state.getPoint()); - state.getNormal().set(localPoint.x, localPoint.y, localPoint.z); - state.getNormal().normalize(); - - float phi = (float) Math.atan2(state.getNormal().y, state.getNormal().x); - if (phi < 0) - phi += 2 * Math.PI; - float theta = (float) Math.acos(state.getNormal().z); - state.getUV().y = theta / (float) Math.PI; - state.getUV().x = phi / (float) (2 * Math.PI); - Vector3 v = new Vector3(); - v.x = -2 * (float) Math.PI * state.getNormal().y; - v.y = 2 * (float) Math.PI * state.getNormal().x; - v.z = 0; - state.setShader(parent.getShader(0)); - state.setModifier(parent.getModifier(0)); - // into world space - Vector3 worldNormal = state.transformNormalObjectToWorld(state.getNormal()); - v = state.transformVectorObjectToWorld(v); - state.getNormal().set(worldNormal); - state.getNormal().normalize(); - state.getGeoNormal().set(state.getNormal()); - // compute basis in world space - state.setBasis(OrthoNormalBasis.makeFromWV(state.getNormal(), v)); - - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - // intersect in local space - float qa = r.dx * r.dx + r.dy * r.dy + r.dz * r.dz; - float qb = 2 * ((r.dx * r.ox) + (r.dy * r.oy) + (r.dz * r.oz)); - float qc = ((r.ox * r.ox) + (r.oy * r.oy) + (r.oz * r.oz)) - 1; - double[] t = Solvers.solveQuadric(qa, qb, qc); - if (t != null) { - // early rejection - if (t[0] >= r.getMax() || t[1] <= r.getMin()) - return; - if (t[0] > r.getMin()) - r.setMax((float) t[0]); - else - r.setMax((float) t[1]); - state.setIntersection(0); - } - } - - public PrimitiveList getBakingPrimitives() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/SphereFlake.java b/src/org/sunflow/core/primitive/SphereFlake.java deleted file mode 100644 index bc9942d..0000000 --- a/src/org/sunflow/core/primitive/SphereFlake.java +++ /dev/null @@ -1,219 +0,0 @@ -package org.sunflow.core.primitive; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.MathUtils; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; - -public class SphereFlake implements PrimitiveList { - private static final int MAX_LEVEL = 20; - private static final float[] boundingRadiusOffset = new float[MAX_LEVEL + 1]; - private static final float[] recursivePattern = new float[9 * 3]; - private int level = 2; - private Vector3 axis = new Vector3(0, 0, 1); - private float baseRadius = 1; - - static { - // geometric series table, to compute bounding radius quickly - for (int i = 0, r = 3; i < boundingRadiusOffset.length; i++, r *= 3) - boundingRadiusOffset[i] = (r - 3.0f) / r; - // lower ring - double a = 0, daL = 2 * Math.PI / 6, daU = 2 * Math.PI / 3; - for (int i = 0; i < 6; i++) { - recursivePattern[3 * i + 0] = -0.3f; - recursivePattern[3 * i + 1] = (float) Math.sin(a); - recursivePattern[3 * i + 2] = (float) Math.cos(a); - a += daL; - } - a -= daL / 2; // tweak - for (int i = 6; i < 9; i++) { - recursivePattern[3 * i + 0] = +0.7f; - recursivePattern[3 * i + 1] = (float) Math.sin(a); - recursivePattern[3 * i + 2] = (float) Math.cos(a); - a += daU; - } - for (int i = 0; i < recursivePattern.length; i += 3) { - float x = recursivePattern[i + 0]; - float y = recursivePattern[i + 1]; - float z = recursivePattern[i + 2]; - float n = 1 / (float) Math.sqrt(x * x + y * y + z * z); - recursivePattern[i + 0] = x * n; - recursivePattern[i + 1] = y * n; - recursivePattern[i + 2] = z * n; - } - } - - public boolean update(ParameterList pl, SunflowAPI api) { - level = MathUtils.clamp(pl.getInt("level", level), 0, 20); - axis = pl.getVector("axis", axis); - axis.normalize(); - baseRadius = Math.abs(pl.getFloat("radius", baseRadius)); - return true; - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(getPrimitiveBound(0, 1)); - if (o2w != null) - bounds = o2w.transform(bounds); - return bounds; - } - - public float getPrimitiveBound(int primID, int i) { - float br = 1 + boundingRadiusOffset[level]; - return (i & 1) == 0 ? -br : br; - } - - public int getNumPrimitives() { - return 1; - } - - public void prepareShadingState(ShadingState state) { - state.init(); - state.getRay().getPoint(state.getPoint()); - Instance parent = state.getInstance(); - Point3 localPoint = state.transformWorldToObject(state.getPoint()); - - float cx = state.getU(); - float cy = state.getV(); - float cz = state.getW(); - - state.getNormal().set(localPoint.x - cx, localPoint.y - cy, localPoint.z - cz); - state.getNormal().normalize(); - - float phi = (float) Math.atan2(state.getNormal().y, state.getNormal().x); - if (phi < 0) - phi += 2 * Math.PI; - float theta = (float) Math.acos(state.getNormal().z); - state.getUV().y = theta / (float) Math.PI; - state.getUV().x = phi / (float) (2 * Math.PI); - Vector3 v = new Vector3(); - v.x = -2 * (float) Math.PI * state.getNormal().y; - v.y = 2 * (float) Math.PI * state.getNormal().x; - v.z = 0; - state.setShader(parent.getShader(0)); - state.setModifier(parent.getModifier(0)); - // into world space - Vector3 worldNormal = state.transformNormalObjectToWorld(state.getNormal()); - v = state.transformVectorObjectToWorld(v); - state.getNormal().set(worldNormal); - state.getNormal().normalize(); - state.getGeoNormal().set(state.getNormal()); - // compute basis in world space - state.setBasis(OrthoNormalBasis.makeFromWV(state.getNormal(), v)); - - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - // intersect in local space - float qa = r.dx * r.dx + r.dy * r.dy + r.dz * r.dz; - intersectFlake(r, state, level, qa, 1 / qa, 0, 0, 0, axis.x, axis.y, axis.z, baseRadius); - } - - private void intersectFlake(Ray r, IntersectionState state, int level, float qa, float qaInv, float cx, float cy, float cz, float dx, float dy, float dz, float radius) { - if (level <= 0) { - // we reached the bottom - intersect sphere and bail out - float vcx = cx - r.ox; - float vcy = cy - r.oy; - float vcz = cz - r.oz; - float b = r.dx * vcx + r.dy * vcy + r.dz * vcz; - float disc = b * b - qa * ((vcx * vcx + vcy * vcy + vcz * vcz) - radius * radius); - if (disc > 0) { - // intersects - check t values - float d = (float) Math.sqrt(disc); - float t1 = (b - d) * qaInv; - float t2 = (b + d) * qaInv; - if (t1 >= r.getMax() || t2 <= r.getMin()) - return; - if (t1 > r.getMin()) - r.setMax(t1); - else - r.setMax(t2); - state.setIntersection(0, cx, cy, cz); - } - } else { - float boundRadius = radius * (1 + boundingRadiusOffset[level]); - float vcx = cx - r.ox; - float vcy = cy - r.oy; - float vcz = cz - r.oz; - float b = r.dx * vcx + r.dy * vcy + r.dz * vcz; - float vcd = (vcx * vcx + vcy * vcy + vcz * vcz); - float disc = b * b - qa * (vcd - boundRadius * boundRadius); - if (disc > 0) { - // intersects - check t values - float d = (float) Math.sqrt(disc); - float t1 = (b - d) * qaInv; - float t2 = (b + d) * qaInv; - if (t1 >= r.getMax() || t2 <= r.getMin()) - return; - - // we hit the bounds, now compute intersection with the actual - // leaf sphere - disc = b * b - qa * (vcd - radius * radius); - if (disc > 0) { - d = (float) Math.sqrt(disc); - t1 = (b - d) * qaInv; - t2 = (b + d) * qaInv; - if (t1 >= r.getMax() || t2 <= r.getMin()) { - // no hit - } else { - if (t1 > r.getMin()) - r.setMax(t1); - else - r.setMax(t2); - state.setIntersection(0, cx, cy, cz); - } - } - - // recursively intersect 9 other spheres - // step1: compute basis around displacement vector - float b1x, b1y, b1z; - if (dx * dx < dy * dy && dx * dx < dz * dz) { - b1x = 0; - b1y = dz; - b1z = -dy; - } else if (dy * dy < dz * dz) { - b1x = dz; - b1y = 0; - b1z = -dx; - } else { - b1x = dy; - b1y = -dx; - b1z = 0; - } - float n = 1 / (float) Math.sqrt(b1x * b1x + b1y * b1y + b1z * b1z); - b1x *= n; - b1y *= n; - b1z *= n; - float b2x = dy * b1z - dz * b1y; - float b2y = dz * b1x - dx * b1z; - float b2z = dx * b1y - dy * b1x; - b1x = dy * b2z - dz * b2y; - b1y = dz * b2x - dx * b2z; - b1z = dx * b2y - dy * b2x; - // step2: generate 9 children recursively - float nr = radius * (1 / 3.0f), scale = radius + nr; - for (int i = 0; i < 9 * 3; i += 3) { - // transform by basis - float ndx = recursivePattern[i] * dx + recursivePattern[i + 1] * b1x + recursivePattern[i + 2] * b2x; - float ndy = recursivePattern[i] * dy + recursivePattern[i + 1] * b1y + recursivePattern[i + 2] * b2y; - float ndz = recursivePattern[i] * dz + recursivePattern[i + 1] * b1z + recursivePattern[i + 2] * b2z; - // recurse! - intersectFlake(r, state, level - 1, qa, qaInv, cx + scale * ndx, cy + scale * ndy, cz + scale * ndz, ndx, ndy, ndz, nr); - } - } - } - } - - public PrimitiveList getBakingPrimitives() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/Torus.java b/src/org/sunflow/core/primitive/Torus.java deleted file mode 100644 index b42f52b..0000000 --- a/src/org/sunflow/core/primitive/Torus.java +++ /dev/null @@ -1,134 +0,0 @@ -package org.sunflow.core.primitive; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.MathUtils; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Solvers; -import org.sunflow.math.Vector3; - -public class Torus implements PrimitiveList { - private float ri2, ro2; - private float ri, ro; - - public Torus() { - ri = 0.25f; - ro = 1; - ri2 = ri * ri; - ro2 = ro * ro; - - } - - public boolean update(ParameterList pl, SunflowAPI api) { - ri = pl.getFloat("radiusInner", ri); - ro = pl.getFloat("radiusOuter", ro); - ri2 = ri * ri; - ro2 = ro * ro; - return true; - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(-ro - ri, -ro - ri, -ri); - bounds.include(ro + ri, ro + ri, ri); - if (o2w != null) - bounds = o2w.transform(bounds); - return bounds; - } - - public float getPrimitiveBound(int primID, int i) { - switch (i) { - case 0: - case 2: - return -ro - ri; - case 1: - case 3: - return ro + ri; - case 4: - return -ri; - case 5: - return ri; - default: - return 0; - } - } - - public int getNumPrimitives() { - return 1; - } - - public void prepareShadingState(ShadingState state) { - state.init(); - state.getRay().getPoint(state.getPoint()); - Instance parent = state.getInstance(); - // get local point - Point3 p = state.transformWorldToObject(state.getPoint()); - // compute local normal - float deriv = p.x * p.x + p.y * p.y + p.z * p.z - ri2 - ro2; - state.getNormal().set(p.x * deriv, p.y * deriv, p.z * deriv + 2 * ro2 * p.z); - state.getNormal().normalize(); - - double phi = Math.asin(MathUtils.clamp(p.z / ri, -1, 1)); - double theta = Math.atan2(p.y, p.x); - if (theta < 0) - theta += 2 * Math.PI; - state.getUV().x = (float) (theta / (2 * Math.PI)); - state.getUV().y = (float) ((phi + Math.PI / 2) / Math.PI); - state.setShader(parent.getShader(0)); - state.setModifier(parent.getModifier(0)); - // into world space - Vector3 worldNormal = state.transformNormalObjectToWorld(state.getNormal()); - state.getNormal().set(worldNormal); - state.getNormal().normalize(); - state.getGeoNormal().set(state.getNormal()); - // make basis in world space - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - // intersect in local space - float rd2x = r.dx * r.dx; - float rd2y = r.dy * r.dy; - float rd2z = r.dz * r.dz; - float ro2x = r.ox * r.ox; - float ro2y = r.oy * r.oy; - float ro2z = r.oz * r.oz; - // compute some common factors - double alpha = rd2x + rd2y + rd2z; - double beta = 2 * (r.ox * r.dx + r.oy * r.dy + r.oz * r.dz); - double gamma = (ro2x + ro2y + ro2z) - ri2 - ro2; - // setup quartic coefficients - double A = alpha * alpha; - double B = 2 * alpha * beta; - double C = beta * beta + 2 * alpha * gamma + 4 * ro2 * rd2z; - double D = 2 * beta * gamma + 8 * ro2 * r.oz * r.dz; - double E = gamma * gamma + 4 * ro2 * ro2z - 4 * ro2 * ri2; - // solve equation - double[] t = Solvers.solveQuartic(A, B, C, D, E); - if (t != null) { - // early rejection - if (t[0] >= r.getMax() || t[t.length - 1] <= r.getMin()) - return; - // find first intersection in front of the ray - for (int i = 0; i < t.length; i++) { - if (t[i] > r.getMin()) { - r.setMax((float) t[i]); - state.setIntersection(0); - return; - } - } - } - } - - public PrimitiveList getBakingPrimitives() { - return null; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/primitive/TriangleMesh.java b/src/org/sunflow/core/primitive/TriangleMesh.java deleted file mode 100644 index 399a043..0000000 --- a/src/org/sunflow/core/primitive/TriangleMesh.java +++ /dev/null @@ -1,784 +0,0 @@ -package org.sunflow.core.primitive; - -import java.io.FileWriter; -import java.io.IOException; -import java.util.Locale; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Ray; -import org.sunflow.core.ShadingState; -import org.sunflow.core.ParameterList.FloatParameter; -import org.sunflow.core.ParameterList.InterpolationType; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.MathUtils; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class TriangleMesh implements PrimitiveList { - private static boolean smallTriangles = false; - protected float[] points; - protected int[] triangles; - private WaldTriangle[] triaccel; - private FloatParameter normals; - private FloatParameter uvs; - private byte[] faceShaders; - - public static void setSmallTriangles(boolean smallTriangles) { - if (smallTriangles) - UI.printInfo(Module.GEOM, "Small trimesh mode: enabled"); - else - UI.printInfo(Module.GEOM, "Small trimesh mode: disabled"); - TriangleMesh.smallTriangles = smallTriangles; - } - - public TriangleMesh() { - triangles = null; - points = null; - normals = uvs = new FloatParameter(); - faceShaders = null; - } - - public void writeObj(String filename) { - try { - FileWriter file = new FileWriter(filename); - file.write(String.format("o object\n")); - for (int i = 0; i < points.length; i += 3) - file.write(String.format("v %g %g %g\n", points[i], points[i + 1], points[i + 2])); - file.write("s off\n"); - for (int i = 0; i < triangles.length; i += 3) - file.write(String.format("f %d %d %d\n", triangles[i] + 1, triangles[i + 1] + 1, triangles[i + 2] + 1)); - file.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public boolean update(ParameterList pl, SunflowAPI api) { - boolean updatedTopology = false; - { - int[] triangles = pl.getIntArray("triangles"); - if (triangles != null) { - this.triangles = triangles; - updatedTopology = true; - } - } - if (triangles == null) { - UI.printError(Module.GEOM, "Unable to update mesh - triangle indices are missing"); - return false; - } - if (triangles.length % 3 != 0) - UI.printWarning(Module.GEOM, "Triangle index data is not a multiple of 3 - triangles may be missing"); - pl.setFaceCount(triangles.length / 3); - { - FloatParameter pointsP = pl.getPointArray("points"); - if (pointsP != null) - if (pointsP.interp != InterpolationType.VERTEX) - UI.printError(Module.GEOM, "Point interpolation type must be set to \"vertex\" - was \"%s\"", pointsP.interp.name().toLowerCase(Locale.ENGLISH)); - else { - points = pointsP.data; - updatedTopology = true; - } - } - if (points == null) { - UI.printError(Module.GEOM, "Unable to update mesh - vertices are missing"); - return false; - } - pl.setVertexCount(points.length / 3); - pl.setFaceVertexCount(3 * (triangles.length / 3)); - FloatParameter normals = pl.getVectorArray("normals"); - if (normals != null) - this.normals = normals; - FloatParameter uvs = pl.getTexCoordArray("uvs"); - if (uvs != null) - this.uvs = uvs; - int[] faceShaders = pl.getIntArray("faceshaders"); - if (faceShaders != null && faceShaders.length == triangles.length / 3) { - this.faceShaders = new byte[faceShaders.length]; - for (int i = 0; i < faceShaders.length; i++) { - int v = faceShaders[i]; - if (v > 255) - UI.printWarning(Module.GEOM, "Shader index too large on triangle %d", i); - this.faceShaders[i] = (byte) (v & 0xFF); - } - } - if (updatedTopology) { - // create triangle acceleration structure - init(); - } - return true; - } - - public float getPrimitiveBound(int primID, int i) { - int tri = 3 * primID; - int a = 3 * triangles[tri + 0]; - int b = 3 * triangles[tri + 1]; - int c = 3 * triangles[tri + 2]; - int axis = i >>> 1; - if ((i & 1) == 0) - return MathUtils.min(points[a + axis], points[b + axis], points[c + axis]); - else - return MathUtils.max(points[a + axis], points[b + axis], points[c + axis]); - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(); - if (o2w == null) { - for (int i = 0; i < points.length; i += 3) - bounds.include(points[i], points[i + 1], points[i + 2]); - } else { - // transform vertices first - for (int i = 0; i < points.length; i += 3) { - float x = points[i]; - float y = points[i + 1]; - float z = points[i + 2]; - float wx = o2w.transformPX(x, y, z); - float wy = o2w.transformPY(x, y, z); - float wz = o2w.transformPZ(x, y, z); - bounds.include(wx, wy, wz); - } - } - return bounds; - } - - private final void intersectTriangleKensler(Ray r, int primID, IntersectionState state) { - int tri = 3 * primID; - int a = 3 * triangles[tri + 0]; - int b = 3 * triangles[tri + 1]; - int c = 3 * triangles[tri + 2]; - float edge0x = points[b + 0] - points[a + 0]; - float edge0y = points[b + 1] - points[a + 1]; - float edge0z = points[b + 2] - points[a + 2]; - float edge1x = points[a + 0] - points[c + 0]; - float edge1y = points[a + 1] - points[c + 1]; - float edge1z = points[a + 2] - points[c + 2]; - float nx = edge0y * edge1z - edge0z * edge1y; - float ny = edge0z * edge1x - edge0x * edge1z; - float nz = edge0x * edge1y - edge0y * edge1x; - float v = r.dot(nx, ny, nz); - float iv = 1 / v; - float edge2x = points[a + 0] - r.ox; - float edge2y = points[a + 1] - r.oy; - float edge2z = points[a + 2] - r.oz; - float va = nx * edge2x + ny * edge2y + nz * edge2z; - float t = iv * va; - if (!r.isInside(t)) - return; - float ix = edge2y * r.dz - edge2z * r.dy; - float iy = edge2z * r.dx - edge2x * r.dz; - float iz = edge2x * r.dy - edge2y * r.dx; - float v1 = ix * edge1x + iy * edge1y + iz * edge1z; - float beta = iv * v1; - if (beta < 0) - return; - float v2 = ix * edge0x + iy * edge0y + iz * edge0z; - if ((v1 + v2) * v > v * v) - return; - float gamma = iv * v2; - if (gamma < 0) - return; - r.setMax(t); - state.setIntersection(primID, beta, gamma); - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - // alternative test -- disabled for now - // intersectPrimitiveRobust(r, primID, state); - - if (triaccel != null) { - // optional fast intersection method - triaccel[primID].intersect(r, primID, state); - return; - } - intersectTriangleKensler(r, primID, state); - } - - public int getNumPrimitives() { - return triangles.length / 3; - } - - public void prepareShadingState(ShadingState state) { - state.init(); - Instance parent = state.getInstance(); - int primID = state.getPrimitiveID(); - float u = state.getU(); - float v = state.getV(); - float w = 1 - u - v; - state.getRay().getPoint(state.getPoint()); - int tri = 3 * primID; - int index0 = triangles[tri + 0]; - int index1 = triangles[tri + 1]; - int index2 = triangles[tri + 2]; - Point3 v0p = getPoint(index0); - Point3 v1p = getPoint(index1); - Point3 v2p = getPoint(index2); - Vector3 ng = Point3.normal(v0p, v1p, v2p); - ng = state.transformNormalObjectToWorld(ng); - ng.normalize(); - state.getGeoNormal().set(ng); - switch (normals.interp) { - case NONE: - case FACE: { - state.getNormal().set(ng); - break; - } - case VERTEX: { - int i30 = 3 * index0; - int i31 = 3 * index1; - int i32 = 3 * index2; - float[] normals = this.normals.data; - state.getNormal().x = w * normals[i30 + 0] + u * normals[i31 + 0] + v * normals[i32 + 0]; - state.getNormal().y = w * normals[i30 + 1] + u * normals[i31 + 1] + v * normals[i32 + 1]; - state.getNormal().z = w * normals[i30 + 2] + u * normals[i31 + 2] + v * normals[i32 + 2]; - state.getNormal().set(state.transformNormalObjectToWorld(state.getNormal())); - state.getNormal().normalize(); - break; - } - case FACEVARYING: { - int idx = 3 * tri; - float[] normals = this.normals.data; - state.getNormal().x = w * normals[idx + 0] + u * normals[idx + 3] + v * normals[idx + 6]; - state.getNormal().y = w * normals[idx + 1] + u * normals[idx + 4] + v * normals[idx + 7]; - state.getNormal().z = w * normals[idx + 2] + u * normals[idx + 5] + v * normals[idx + 8]; - state.getNormal().set(state.transformNormalObjectToWorld(state.getNormal())); - state.getNormal().normalize(); - break; - } - } - float uv00 = 0, uv01 = 0, uv10 = 0, uv11 = 0, uv20 = 0, uv21 = 0; - switch (uvs.interp) { - case NONE: - case FACE: { - state.getUV().x = 0; - state.getUV().y = 0; - break; - } - case VERTEX: { - int i20 = 2 * index0; - int i21 = 2 * index1; - int i22 = 2 * index2; - float[] uvs = this.uvs.data; - uv00 = uvs[i20 + 0]; - uv01 = uvs[i20 + 1]; - uv10 = uvs[i21 + 0]; - uv11 = uvs[i21 + 1]; - uv20 = uvs[i22 + 0]; - uv21 = uvs[i22 + 1]; - break; - } - case FACEVARYING: { - int idx = tri << 1; - float[] uvs = this.uvs.data; - uv00 = uvs[idx + 0]; - uv01 = uvs[idx + 1]; - uv10 = uvs[idx + 2]; - uv11 = uvs[idx + 3]; - uv20 = uvs[idx + 4]; - uv21 = uvs[idx + 5]; - break; - } - } - if (uvs.interp != InterpolationType.NONE) { - // get exact uv coords and compute tangent vectors - state.getUV().x = w * uv00 + u * uv10 + v * uv20; - state.getUV().y = w * uv01 + u * uv11 + v * uv21; - float du1 = uv00 - uv20; - float du2 = uv10 - uv20; - float dv1 = uv01 - uv21; - float dv2 = uv11 - uv21; - Vector3 dp1 = Point3.sub(v0p, v2p, new Vector3()), dp2 = Point3.sub(v1p, v2p, new Vector3()); - float determinant = du1 * dv2 - dv1 * du2; - if (determinant == 0.0f) { - // create basis in world space - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - } else { - float invdet = 1.f / determinant; - // Vector3 dpdu = new Vector3(); - // dpdu.x = (dv2 * dp1.x - dv1 * dp2.x) * invdet; - // dpdu.y = (dv2 * dp1.y - dv1 * dp2.y) * invdet; - // dpdu.z = (dv2 * dp1.z - dv1 * dp2.z) * invdet; - Vector3 dpdv = new Vector3(); - dpdv.x = (-du2 * dp1.x + du1 * dp2.x) * invdet; - dpdv.y = (-du2 * dp1.y + du1 * dp2.y) * invdet; - dpdv.z = (-du2 * dp1.z + du1 * dp2.z) * invdet; - dpdv = state.transformVectorObjectToWorld(dpdv); - // create basis in world space - state.setBasis(OrthoNormalBasis.makeFromWV(state.getNormal(), dpdv)); - } - } else - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - int shaderIndex = faceShaders == null ? 0 : (faceShaders[primID] & 0xFF); - state.setShader(parent.getShader(shaderIndex)); - state.setModifier(parent.getModifier(shaderIndex)); - } - - public void init() { - triaccel = null; - int nt = getNumPrimitives(); - if (!smallTriangles) { - // too many triangles? -- don't generate triaccel to save memory - if (nt > 2000000) { - UI.printWarning(Module.GEOM, "TRI - Too many triangles -- triaccel generation skipped"); - return; - } - triaccel = new WaldTriangle[nt]; - for (int i = 0; i < nt; i++) - triaccel[i] = new WaldTriangle(this, i); - } - } - - protected Point3 getPoint(int i) { - i *= 3; - return new Point3(points[i], points[i + 1], points[i + 2]); - } - - public void getPoint(int tri, int i, Point3 p) { - int index = 3 * triangles[3 * tri + i]; - p.set(points[index], points[index + 1], points[index + 2]); - } - - private static final class WaldTriangle { - // private data for fast triangle intersection testing - private int k; - private float nu, nv, nd; - private float bnu, bnv, bnd; - private float cnu, cnv, cnd; - - private WaldTriangle(TriangleMesh mesh, int tri) { - k = 0; - tri *= 3; - int index0 = mesh.triangles[tri + 0]; - int index1 = mesh.triangles[tri + 1]; - int index2 = mesh.triangles[tri + 2]; - Point3 v0p = mesh.getPoint(index0); - Point3 v1p = mesh.getPoint(index1); - Point3 v2p = mesh.getPoint(index2); - Vector3 ng = Point3.normal(v0p, v1p, v2p); - if (Math.abs(ng.x) > Math.abs(ng.y) && Math.abs(ng.x) > Math.abs(ng.z)) - k = 0; - else if (Math.abs(ng.y) > Math.abs(ng.z)) - k = 1; - else - k = 2; - float ax, ay, bx, by, cx, cy; - switch (k) { - case 0: { - nu = ng.y / ng.x; - nv = ng.z / ng.x; - nd = v0p.x + (nu * v0p.y) + (nv * v0p.z); - ax = v0p.y; - ay = v0p.z; - bx = v2p.y - ax; - by = v2p.z - ay; - cx = v1p.y - ax; - cy = v1p.z - ay; - break; - } - case 1: { - nu = ng.z / ng.y; - nv = ng.x / ng.y; - nd = (nv * v0p.x) + v0p.y + (nu * v0p.z); - ax = v0p.z; - ay = v0p.x; - bx = v2p.z - ax; - by = v2p.x - ay; - cx = v1p.z - ax; - cy = v1p.x - ay; - break; - } - case 2: - default: { - nu = ng.x / ng.z; - nv = ng.y / ng.z; - nd = (nu * v0p.x) + (nv * v0p.y) + v0p.z; - ax = v0p.x; - ay = v0p.y; - bx = v2p.x - ax; - by = v2p.y - ay; - cx = v1p.x - ax; - cy = v1p.y - ay; - } - } - float det = bx * cy - by * cx; - bnu = -by / det; - bnv = bx / det; - bnd = (by * ax - bx * ay) / det; - cnu = cy / det; - cnv = -cx / det; - cnd = (cx * ay - cy * ax) / det; - } - - void intersect(Ray r, int primID, IntersectionState state) { - switch (k) { - case 0: { - float det = 1.0f / (r.dx + nu * r.dy + nv * r.dz); - float t = (nd - r.ox - nu * r.oy - nv * r.oz) * det; - if (!r.isInside(t)) - return; - float hu = r.oy + t * r.dy; - float hv = r.oz + t * r.dz; - float u = hu * bnu + hv * bnv + bnd; - if (u < 0.0f) - return; - float v = hu * cnu + hv * cnv + cnd; - if (v < 0.0f) - return; - if (u + v > 1.0f) - return; - r.setMax(t); - state.setIntersection(primID, u, v); - return; - } - case 1: { - float det = 1.0f / (r.dy + nu * r.dz + nv * r.dx); - float t = (nd - r.oy - nu * r.oz - nv * r.ox) * det; - if (!r.isInside(t)) - return; - float hu = r.oz + t * r.dz; - float hv = r.ox + t * r.dx; - float u = hu * bnu + hv * bnv + bnd; - if (u < 0.0f) - return; - float v = hu * cnu + hv * cnv + cnd; - if (v < 0.0f) - return; - if (u + v > 1.0f) - return; - r.setMax(t); - state.setIntersection(primID, u, v); - return; - } - case 2: { - float det = 1.0f / (r.dz + nu * r.dx + nv * r.dy); - float t = (nd - r.oz - nu * r.ox - nv * r.oy) * det; - if (!r.isInside(t)) - return; - float hu = r.ox + t * r.dx; - float hv = r.oy + t * r.dy; - float u = hu * bnu + hv * bnv + bnd; - if (u < 0.0f) - return; - float v = hu * cnu + hv * cnv + cnd; - if (v < 0.0f) - return; - if (u + v > 1.0f) - return; - r.setMax(t); - state.setIntersection(primID, u, v); - return; - } - } - } - } - - public PrimitiveList getBakingPrimitives() { - switch (uvs.interp) { - case NONE: - case FACE: - UI.printError(Module.GEOM, "Cannot generate baking surface without texture coordinate data"); - return null; - default: - return new BakingSurface(); - } - } - - private class BakingSurface implements PrimitiveList { - public PrimitiveList getBakingPrimitives() { - return null; - } - - public int getNumPrimitives() { - return TriangleMesh.this.getNumPrimitives(); - } - - public float getPrimitiveBound(int primID, int i) { - if (i > 3) - return 0; - switch (uvs.interp) { - case NONE: - case FACE: - default: { - return 0; - } - case VERTEX: { - int tri = 3 * primID; - int index0 = triangles[tri + 0]; - int index1 = triangles[tri + 1]; - int index2 = triangles[tri + 2]; - int i20 = 2 * index0; - int i21 = 2 * index1; - int i22 = 2 * index2; - float[] uvs = TriangleMesh.this.uvs.data; - switch (i) { - case 0: - return MathUtils.min(uvs[i20 + 0], uvs[i21 + 0], uvs[i22 + 0]); - case 1: - return MathUtils.max(uvs[i20 + 0], uvs[i21 + 0], uvs[i22 + 0]); - case 2: - return MathUtils.min(uvs[i20 + 1], uvs[i21 + 1], uvs[i22 + 1]); - case 3: - return MathUtils.max(uvs[i20 + 1], uvs[i21 + 1], uvs[i22 + 1]); - default: - return 0; - } - } - case FACEVARYING: { - int idx = 6 * primID; - float[] uvs = TriangleMesh.this.uvs.data; - switch (i) { - case 0: - return MathUtils.min(uvs[idx + 0], uvs[idx + 2], uvs[idx + 4]); - case 1: - return MathUtils.max(uvs[idx + 0], uvs[idx + 2], uvs[idx + 4]); - case 2: - return MathUtils.min(uvs[idx + 1], uvs[idx + 3], uvs[idx + 5]); - case 3: - return MathUtils.max(uvs[idx + 1], uvs[idx + 3], uvs[idx + 5]); - default: - return 0; - } - } - } - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(); - if (o2w == null) { - for (int i = 0; i < uvs.data.length; i += 2) - bounds.include(uvs.data[i], uvs.data[i + 1], 0); - } else { - // transform vertices first - for (int i = 0; i < uvs.data.length; i += 2) { - float x = uvs.data[i]; - float y = uvs.data[i + 1]; - float wx = o2w.transformPX(x, y, 0); - float wy = o2w.transformPY(x, y, 0); - float wz = o2w.transformPZ(x, y, 0); - bounds.include(wx, wy, wz); - } - } - return bounds; - } - - public void intersectPrimitive(Ray r, int primID, IntersectionState state) { - float uv00 = 0, uv01 = 0, uv10 = 0, uv11 = 0, uv20 = 0, uv21 = 0; - switch (uvs.interp) { - case NONE: - case FACE: - default: - return; - case VERTEX: { - int tri = 3 * primID; - int index0 = triangles[tri + 0]; - int index1 = triangles[tri + 1]; - int index2 = triangles[tri + 2]; - int i20 = 2 * index0; - int i21 = 2 * index1; - int i22 = 2 * index2; - float[] uvs = TriangleMesh.this.uvs.data; - uv00 = uvs[i20 + 0]; - uv01 = uvs[i20 + 1]; - uv10 = uvs[i21 + 0]; - uv11 = uvs[i21 + 1]; - uv20 = uvs[i22 + 0]; - uv21 = uvs[i22 + 1]; - break; - - } - case FACEVARYING: { - int idx = (3 * primID) << 1; - float[] uvs = TriangleMesh.this.uvs.data; - uv00 = uvs[idx + 0]; - uv01 = uvs[idx + 1]; - uv10 = uvs[idx + 2]; - uv11 = uvs[idx + 3]; - uv20 = uvs[idx + 4]; - uv21 = uvs[idx + 5]; - break; - } - } - - double edge1x = uv10 - uv00; - double edge1y = uv11 - uv01; - double edge2x = uv20 - uv00; - double edge2y = uv21 - uv01; - double pvecx = r.dy * 0 - r.dz * edge2y; - double pvecy = r.dz * edge2x - r.dx * 0; - double pvecz = r.dx * edge2y - r.dy * edge2x; - double qvecx, qvecy, qvecz; - double u, v; - double det = edge1x * pvecx + edge1y * pvecy + 0 * pvecz; - if (det > 0) { - double tvecx = r.ox - uv00; - double tvecy = r.oy - uv01; - double tvecz = r.oz; - u = (tvecx * pvecx + tvecy * pvecy + tvecz * pvecz); - if (u < 0.0 || u > det) - return; - qvecx = tvecy * 0 - tvecz * edge1y; - qvecy = tvecz * edge1x - tvecx * 0; - qvecz = tvecx * edge1y - tvecy * edge1x; - v = (r.dx * qvecx + r.dy * qvecy + r.dz * qvecz); - if (v < 0.0 || u + v > det) - return; - } else if (det < 0) { - double tvecx = r.ox - uv00; - double tvecy = r.oy - uv01; - double tvecz = r.oz; - u = (tvecx * pvecx + tvecy * pvecy + tvecz * pvecz); - if (u > 0.0 || u < det) - return; - qvecx = tvecy * 0 - tvecz * edge1y; - qvecy = tvecz * edge1x - tvecx * 0; - qvecz = tvecx * edge1y - tvecy * edge1x; - v = (r.dx * qvecx + r.dy * qvecy + r.dz * qvecz); - if (v > 0.0 || u + v < det) - return; - } else - return; - double inv_det = 1.0 / det; - float t = (float) ((edge2x * qvecx + edge2y * qvecy + 0 * qvecz) * inv_det); - if (r.isInside(t)) { - r.setMax(t); - state.setIntersection(primID, (float) (u * inv_det), (float) (v * inv_det)); - } - } - - public void prepareShadingState(ShadingState state) { - state.init(); - Instance parent = state.getInstance(); - int primID = state.getPrimitiveID(); - float u = state.getU(); - float v = state.getV(); - float w = 1 - u - v; - // state.getRay().getPoint(state.getPoint()); - int tri = 3 * primID; - int index0 = triangles[tri + 0]; - int index1 = triangles[tri + 1]; - int index2 = triangles[tri + 2]; - Point3 v0p = getPoint(index0); - Point3 v1p = getPoint(index1); - Point3 v2p = getPoint(index2); - - // get object space point from barycentric coordinates - state.getPoint().x = w * v0p.x + u * v1p.x + v * v2p.x; - state.getPoint().y = w * v0p.y + u * v1p.y + v * v2p.y; - state.getPoint().z = w * v0p.z + u * v1p.z + v * v2p.z; - // move into world space - state.getPoint().set(state.transformObjectToWorld(state.getPoint())); - - Vector3 ng = Point3.normal(v0p, v1p, v2p); - if (parent != null) - ng = state.transformNormalObjectToWorld(ng); - ng.normalize(); - state.getGeoNormal().set(ng); - switch (normals.interp) { - case NONE: - case FACE: { - state.getNormal().set(ng); - break; - } - case VERTEX: { - int i30 = 3 * index0; - int i31 = 3 * index1; - int i32 = 3 * index2; - float[] normals = TriangleMesh.this.normals.data; - state.getNormal().x = w * normals[i30 + 0] + u * normals[i31 + 0] + v * normals[i32 + 0]; - state.getNormal().y = w * normals[i30 + 1] + u * normals[i31 + 1] + v * normals[i32 + 1]; - state.getNormal().z = w * normals[i30 + 2] + u * normals[i31 + 2] + v * normals[i32 + 2]; - if (parent != null) - state.getNormal().set(state.transformNormalObjectToWorld(state.getNormal())); - state.getNormal().normalize(); - break; - } - case FACEVARYING: { - int idx = 3 * tri; - float[] normals = TriangleMesh.this.normals.data; - state.getNormal().x = w * normals[idx + 0] + u * normals[idx + 3] + v * normals[idx + 6]; - state.getNormal().y = w * normals[idx + 1] + u * normals[idx + 4] + v * normals[idx + 7]; - state.getNormal().z = w * normals[idx + 2] + u * normals[idx + 5] + v * normals[idx + 8]; - if (parent != null) - state.getNormal().set(state.transformNormalObjectToWorld(state.getNormal())); - state.getNormal().normalize(); - break; - } - } - float uv00 = 0, uv01 = 0, uv10 = 0, uv11 = 0, uv20 = 0, uv21 = 0; - switch (uvs.interp) { - case NONE: - case FACE: { - state.getUV().x = 0; - state.getUV().y = 0; - break; - } - case VERTEX: { - int i20 = 2 * index0; - int i21 = 2 * index1; - int i22 = 2 * index2; - float[] uvs = TriangleMesh.this.uvs.data; - uv00 = uvs[i20 + 0]; - uv01 = uvs[i20 + 1]; - uv10 = uvs[i21 + 0]; - uv11 = uvs[i21 + 1]; - uv20 = uvs[i22 + 0]; - uv21 = uvs[i22 + 1]; - break; - } - case FACEVARYING: { - int idx = tri << 1; - float[] uvs = TriangleMesh.this.uvs.data; - uv00 = uvs[idx + 0]; - uv01 = uvs[idx + 1]; - uv10 = uvs[idx + 2]; - uv11 = uvs[idx + 3]; - uv20 = uvs[idx + 4]; - uv21 = uvs[idx + 5]; - break; - } - } - if (uvs.interp != InterpolationType.NONE) { - // get exact uv coords and compute tangent vectors - state.getUV().x = w * uv00 + u * uv10 + v * uv20; - state.getUV().y = w * uv01 + u * uv11 + v * uv21; - float du1 = uv00 - uv20; - float du2 = uv10 - uv20; - float dv1 = uv01 - uv21; - float dv2 = uv11 - uv21; - Vector3 dp1 = Point3.sub(v0p, v2p, new Vector3()), dp2 = Point3.sub(v1p, v2p, new Vector3()); - float determinant = du1 * dv2 - dv1 * du2; - if (determinant == 0.0f) { - // create basis in world space - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - } else { - float invdet = 1.f / determinant; - // Vector3 dpdu = new Vector3(); - // dpdu.x = (dv2 * dp1.x - dv1 * dp2.x) * invdet; - // dpdu.y = (dv2 * dp1.y - dv1 * dp2.y) * invdet; - // dpdu.z = (dv2 * dp1.z - dv1 * dp2.z) * invdet; - Vector3 dpdv = new Vector3(); - dpdv.x = (-du2 * dp1.x + du1 * dp2.x) * invdet; - dpdv.y = (-du2 * dp1.y + du1 * dp2.y) * invdet; - dpdv.z = (-du2 * dp1.z + du1 * dp2.z) * invdet; - if (parent != null) - dpdv = state.transformVectorObjectToWorld(dpdv); - // create basis in world space - state.setBasis(OrthoNormalBasis.makeFromWV(state.getNormal(), dpdv)); - } - } else - state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); - int shaderIndex = faceShaders == null ? 0 : (faceShaders[primID] & 0xFF); - state.setShader(parent.getShader(shaderIndex)); - } - - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/renderer/BucketRenderer.java b/src/org/sunflow/core/renderer/BucketRenderer.java deleted file mode 100644 index 3cea7f9..0000000 --- a/src/org/sunflow/core/renderer/BucketRenderer.java +++ /dev/null @@ -1,469 +0,0 @@ -package org.sunflow.core.renderer; - -import org.sunflow.PluginRegistry; -import org.sunflow.core.BucketOrder; -import org.sunflow.core.Display; -import org.sunflow.core.Filter; -import org.sunflow.core.ImageSampler; -import org.sunflow.core.Instance; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.Options; -import org.sunflow.core.Scene; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.core.bucket.BucketOrderFactory; -import org.sunflow.core.filter.BoxFilter; -import org.sunflow.image.Color; -import org.sunflow.image.formats.GenericBitmap; -import org.sunflow.math.MathUtils; -import org.sunflow.math.QMC; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class BucketRenderer implements ImageSampler { - private Scene scene; - private Display display; - // resolution - private int imageWidth; - private int imageHeight; - // bucketing - private String bucketOrderName; - private BucketOrder bucketOrder; - private int bucketSize; - private int bucketCounter; - private int[] bucketCoords; - private boolean dumpBuckets; - - // anti-aliasing - private int minAADepth; - private int maxAADepth; - private int superSampling; - private float contrastThreshold; - private boolean jitter; - private boolean displayAA; - - // derived quantities - private double invSuperSampling; - private int subPixelSize; - private int minStepSize; - private int maxStepSize; - private int sigmaOrder; - private int sigmaLength; - private float thresh; - private boolean useJitter; - - // filtering - private String filterName; - private Filter filter; - private int fs; - private float fhs; - - public BucketRenderer() { - bucketSize = 32; - bucketOrderName = "hilbert"; - displayAA = false; - contrastThreshold = 0.1f; - filterName = "box"; - jitter = false; // off by default - dumpBuckets = false; // for debugging only - not user settable - } - - public boolean prepare(Options options, Scene scene, int w, int h) { - this.scene = scene; - imageWidth = w; - imageHeight = h; - - // fetch options - bucketSize = options.getInt("bucket.size", bucketSize); - bucketOrderName = options.getString("bucket.order", bucketOrderName); - minAADepth = options.getInt("aa.min", minAADepth); - maxAADepth = options.getInt("aa.max", maxAADepth); - superSampling = options.getInt("aa.samples", superSampling); - displayAA = options.getBoolean("aa.display", displayAA); - jitter = options.getBoolean("aa.jitter", jitter); - contrastThreshold = options.getFloat("aa.contrast", contrastThreshold); - - // limit bucket size and compute number of buckets in each direction - bucketSize = MathUtils.clamp(bucketSize, 16, 512); - int numBucketsX = (imageWidth + bucketSize - 1) / bucketSize; - int numBucketsY = (imageHeight + bucketSize - 1) / bucketSize; - bucketOrder = BucketOrderFactory.create(bucketOrderName); - bucketCoords = bucketOrder.getBucketSequence(numBucketsX, numBucketsY); - // validate AA options - minAADepth = MathUtils.clamp(minAADepth, -4, 5); - maxAADepth = MathUtils.clamp(maxAADepth, minAADepth, 5); - superSampling = MathUtils.clamp(superSampling, 1, 256); - invSuperSampling = 1.0 / superSampling; - // compute AA stepping sizes - subPixelSize = (maxAADepth > 0) ? (1 << maxAADepth) : 1; - minStepSize = maxAADepth >= 0 ? 1 : 1 << (-maxAADepth); - if (minAADepth == maxAADepth) - maxStepSize = minStepSize; - else - maxStepSize = minAADepth > 0 ? 1 << minAADepth : subPixelSize << (-minAADepth); - useJitter = jitter && maxAADepth > 0; - // compute anti-aliasing contrast thresholds - contrastThreshold = MathUtils.clamp(contrastThreshold, 0, 1); - thresh = contrastThreshold * (float) Math.pow(2.0f, minAADepth); - // read filter settings from scene - filterName = options.getString("filter", filterName); - filter = PluginRegistry.filterPlugins.createObject(filterName); - // adjust filter - if (filter == null) { - UI.printWarning(Module.BCKT, "Unrecognized filter type: \"%s\" - defaulting to box", filterName); - filter = new BoxFilter(); - filterName = "box"; - } - fhs = filter.getSize() * 0.5f; - fs = (int) Math.ceil(subPixelSize * (fhs - 0.5f)); - - // prepare QMC sampling - sigmaOrder = Math.min(QMC.MAX_SIGMA_ORDER, Math.max(0, maxAADepth) + 13); // FIXME: how big should the table be? - sigmaLength = 1 << sigmaOrder; - UI.printInfo(Module.BCKT, "Bucket renderer settings:"); - UI.printInfo(Module.BCKT, " * Resolution: %dx%d", imageWidth, imageHeight); - UI.printInfo(Module.BCKT, " * Bucket size: %d", bucketSize); - UI.printInfo(Module.BCKT, " * Number of buckets: %dx%d", numBucketsX, numBucketsY); - if (minAADepth != maxAADepth) - UI.printInfo(Module.BCKT, " * Anti-aliasing: %s -> %s (adaptive)", aaDepthToString(minAADepth), aaDepthToString(maxAADepth)); - else - UI.printInfo(Module.BCKT, " * Anti-aliasing: %s (fixed)", aaDepthToString(minAADepth)); - UI.printInfo(Module.BCKT, " * Rays per sample: %d", superSampling); - UI.printInfo(Module.BCKT, " * Subpixel jitter: %s", useJitter ? "on" : (jitter ? "auto-off" : "off")); - UI.printInfo(Module.BCKT, " * Contrast threshold: %.2f", contrastThreshold); - UI.printInfo(Module.BCKT, " * Filter type: %s", filterName); - UI.printInfo(Module.BCKT, " * Filter size: %.2f pixels", filter.getSize()); - return true; - } - - private String aaDepthToString(int depth) { - int pixelAA = (depth) < 0 ? -(1 << (-depth)) : (1 << depth); - return String.format("%s%d sample%s", depth < 0 ? "1/" : "", pixelAA * pixelAA, depth == 0 ? "" : "s"); - } - - public void render(Display display) { - this.display = display; - display.imageBegin(imageWidth, imageHeight, bucketSize); - // set members variables - bucketCounter = 0; - // start task - UI.taskStart("Rendering", 0, bucketCoords.length); - Timer timer = new Timer(); - timer.start(); - BucketThread[] renderThreads = new BucketThread[scene.getThreads()]; - for (int i = 0; i < renderThreads.length; i++) { - renderThreads[i] = new BucketThread(i); - renderThreads[i].setPriority(scene.getThreadPriority()); - renderThreads[i].start(); - } - for (int i = 0; i < renderThreads.length; i++) { - try { - renderThreads[i].join(); - } catch (InterruptedException e) { - UI.printError(Module.BCKT, "Bucket processing thread %d of %d was interrupted", i + 1, renderThreads.length); - } finally { - renderThreads[i].updateStats(); - } - } - UI.taskStop(); - timer.end(); - UI.printInfo(Module.BCKT, "Render time: %s", timer.toString()); - display.imageEnd(); - } - - private class BucketThread extends Thread { - private final int threadID; - private final IntersectionState istate; - - BucketThread(int threadID) { - this.threadID = threadID; - istate = new IntersectionState(); - } - - @Override - public void run() { - while (true) { - int bx, by; - synchronized (BucketRenderer.this) { - if (bucketCounter >= bucketCoords.length) - return; - UI.taskUpdate(bucketCounter); - bx = bucketCoords[bucketCounter + 0]; - by = bucketCoords[bucketCounter + 1]; - bucketCounter += 2; - } - renderBucket(display, bx, by, threadID, istate); - if (UI.taskCanceled()) - return; - } - } - - void updateStats() { - scene.accumulateStats(istate); - } - } - - private void renderBucket(Display display, int bx, int by, int threadID, IntersectionState istate) { - // pixel sized extents - int x0 = bx * bucketSize; - int y0 = by * bucketSize; - int bw = Math.min(bucketSize, imageWidth - x0); - int bh = Math.min(bucketSize, imageHeight - y0); - - // prepare bucket - display.imagePrepare(x0, y0, bw, bh, threadID); - - Color[] bucketRGB = new Color[bw * bh]; - float[] bucketAlpha = new float[bw * bh]; - - // subpixel extents - int sx0 = x0 * subPixelSize - fs; - int sy0 = y0 * subPixelSize - fs; - int sbw = bw * subPixelSize + fs * 2; - int sbh = bh * subPixelSize + fs * 2; - - // round up to align with maximum step size - sbw = (sbw + (maxStepSize - 1)) & (~(maxStepSize - 1)); - sbh = (sbh + (maxStepSize - 1)) & (~(maxStepSize - 1)); - // extra padding as needed - if (maxStepSize > 1) { - sbw++; - sbh++; - } - // allocate bucket memory - ImageSample[] samples = new ImageSample[sbw * sbh]; - // allocate samples and compute jitter offsets - float invSubPixelSize = 1.0f / subPixelSize; - for (int y = 0, index = 0; y < sbh; y++) { - for (int x = 0; x < sbw; x++, index++) { - int sx = sx0 + x; - int sy = sy0 + y; - int j = sx & (sigmaLength - 1); - int k = sy & (sigmaLength - 1); - int i = (j << sigmaOrder) + QMC.sigma(k, sigmaOrder); - float dx = useJitter ? (float) QMC.halton(0, k) : 0.5f; - float dy = useJitter ? (float) QMC.halton(0, j) : 0.5f; - float rx = (sx + dx) * invSubPixelSize; - float ry = (sy + dy) * invSubPixelSize; - ry = imageHeight - ry; - samples[index] = new ImageSample(rx, ry, i); - } - } - for (int x = 0; x < sbw - 1; x += maxStepSize) - for (int y = 0; y < sbh - 1; y += maxStepSize) - refineSamples(samples, sbw, x, y, maxStepSize, thresh, istate); - if (dumpBuckets) { - UI.printInfo(Module.BCKT, "Dumping bucket [%d, %d] to file ...", bx, by); - GenericBitmap bitmap = new GenericBitmap(sbw, sbh); - for (int y = sbh - 1, index = 0; y >= 0; y--) - for (int x = 0; x < sbw; x++, index++) - bitmap.writePixel(x, y, samples[index].c, samples[index].alpha); - bitmap.save(String.format("bucket_%04d_%04d.png", bx, by)); - } - if (displayAA) { - // color coded image of what is visible - float invArea = invSubPixelSize * invSubPixelSize; - for (int y = 0, index = 0; y < bh; y++) { - for (int x = 0; x < bw; x++, index++) { - int sampled = 0; - for (int i = 0; i < subPixelSize; i++) { - for (int j = 0; j < subPixelSize; j++) { - int sx = x * subPixelSize + fs + i; - int sy = y * subPixelSize + fs + j; - int s = sx + sy * sbw; - sampled += samples[s].sampled() ? 1 : 0; - } - } - bucketRGB[index] = new Color(sampled * invArea); - bucketAlpha[index] = 1.0f; - } - } - } else { - // filter samples into pixels - float cy = imageHeight - (y0 + 0.5f); - for (int y = 0, index = 0; y < bh; y++, cy--) { - float cx = x0 + 0.5f; - for (int x = 0; x < bw; x++, index++, cx++) { - Color c = Color.black(); - float a = 0; - float weight = 0.0f; - for (int j = -fs, sy = y * subPixelSize; j <= fs; j++, sy++) { - for (int i = -fs, sx = x * subPixelSize, s = sx + sy * sbw; i <= fs; i++, sx++, s++) { - float dx = samples[s].rx - cx; - if (Math.abs(dx) > fhs) - continue; - float dy = samples[s].ry - cy; - if (Math.abs(dy) > fhs) - continue; - float f = filter.get(dx, dy); - c.madd(f, samples[s].c); - a += f * samples[s].alpha; - weight += f; - - } - } - float invWeight = 1.0f / weight; - c.mul(invWeight); - a *= invWeight; - bucketRGB[index] = c; - bucketAlpha[index] = a; - } - } - } - // update pixels - display.imageUpdate(x0, y0, bw, bh, bucketRGB, bucketAlpha); - } - - private void computeSubPixel(ImageSample sample, IntersectionState istate) { - float x = sample.rx; - float y = sample.ry; - double q0 = QMC.halton(1, sample.i); - double q1 = QMC.halton(2, sample.i); - double q2 = QMC.halton(3, sample.i); - if (superSampling > 1) { - // multiple sampling - sample.add(scene.getRadiance(istate, x, y, q1, q2, q0, sample.i, 4, null)); - for (int i = 1; i < superSampling; i++) { - double time = QMC.mod1(q0 + i * invSuperSampling); - double lensU = QMC.mod1(q1 + QMC.halton(0, i)); - double lensV = QMC.mod1(q2 + QMC.halton(1, i)); - sample.add(scene.getRadiance(istate, x, y, lensU, lensV, time, sample.i + i, 4, null)); - } - sample.scale((float) invSuperSampling); - } else { - // single sample - sample.set(scene.getRadiance(istate, x, y, q1, q2, q0, sample.i, 4, null)); - } - } - - private void refineSamples(ImageSample[] samples, int sbw, int x, int y, int stepSize, float thresh, IntersectionState istate) { - int dx = stepSize; - int dy = stepSize * sbw; - int i00 = x + y * sbw; - ImageSample s00 = samples[i00]; - ImageSample s01 = samples[i00 + dy]; - ImageSample s10 = samples[i00 + dx]; - ImageSample s11 = samples[i00 + dx + dy]; - if (!s00.sampled()) - computeSubPixel(s00, istate); - if (!s01.sampled()) - computeSubPixel(s01, istate); - if (!s10.sampled()) - computeSubPixel(s10, istate); - if (!s11.sampled()) - computeSubPixel(s11, istate); - if (stepSize > minStepSize) { - if (s00.isDifferent(s01, thresh) || s00.isDifferent(s10, thresh) || s00.isDifferent(s11, thresh) || s01.isDifferent(s11, thresh) || s10.isDifferent(s11, thresh) || s01.isDifferent(s10, thresh)) { - stepSize >>= 1; - thresh *= 2; - refineSamples(samples, sbw, x, y, stepSize, thresh, istate); - refineSamples(samples, sbw, x + stepSize, y, stepSize, thresh, istate); - refineSamples(samples, sbw, x, y + stepSize, stepSize, thresh, istate); - refineSamples(samples, sbw, x + stepSize, y + stepSize, stepSize, thresh, istate); - return; - } - } - - // interpolate remaining samples - float ds = 1.0f / stepSize; - for (int i = 0; i <= stepSize; i++) - for (int j = 0; j <= stepSize; j++) - if (!samples[x + i + (y + j) * sbw].processed()) - ImageSample.bilerp(samples[x + i + (y + j) * sbw], s00, s01, s10, s11, i * ds, j * ds); - } - - private static final class ImageSample { - float rx, ry; - int i, n; - Color c; - float alpha; - Instance instance; - Shader shader; - float nx, ny, nz; - - ImageSample(float rx, float ry, int i) { - this.rx = rx; - this.ry = ry; - this.i = i; - n = 0; - c = null; - alpha = 0; - instance = null; - shader = null; - nx = ny = nz = 1; - } - - final void set(ShadingState state) { - if (state == null) - c = Color.BLACK; - else { - c = state.getResult(); - shader = state.getShader(); - instance = state.getInstance(); - if (state.getNormal() != null) { - nx = state.getNormal().x; - ny = state.getNormal().y; - nz = state.getNormal().z; - } - alpha = state.getInstance() == null ? 0 : 1; - } - n = 1; - } - - final void add(ShadingState state) { - if (n == 0) - c = Color.black(); - if (state != null) { - c.add(state.getResult()); - alpha += state.getInstance() == null ? 0 : 1; - } - n++; - } - - final void scale(float s) { - c.mul(s); - alpha *= s; - } - - final boolean processed() { - return c != null; - } - - final boolean sampled() { - return n > 0; - } - - final boolean isDifferent(ImageSample sample, float thresh) { - if (instance != sample.instance) - return true; - if (shader != sample.shader) - return true; - if (Color.hasContrast(c, sample.c, thresh)) - return true; - if (Math.abs(alpha - sample.alpha) / (alpha + sample.alpha) > thresh) - return true; - // only compare normals if this pixel has not been averaged - float dot = (nx * sample.nx + ny * sample.ny + nz * sample.nz); - return dot < 0.9f; - } - - static final ImageSample bilerp(ImageSample result, ImageSample i00, ImageSample i01, ImageSample i10, ImageSample i11, float dx, float dy) { - float k00 = (1.0f - dx) * (1.0f - dy); - float k01 = (1.0f - dx) * dy; - float k10 = dx * (1.0f - dy); - float k11 = dx * dy; - Color c00 = i00.c; - Color c01 = i01.c; - Color c10 = i10.c; - Color c11 = i11.c; - Color c = Color.mul(k00, c00); - c.madd(k01, c01); - c.madd(k10, c10); - c.madd(k11, c11); - result.c = c; - result.alpha = k00 * i00.alpha + k01 * i01.alpha + k10 * i10.alpha + k11 * i11.alpha; - return result; - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/renderer/MultipassRenderer.java b/src/org/sunflow/core/renderer/MultipassRenderer.java deleted file mode 100644 index 6331042..0000000 --- a/src/org/sunflow/core/renderer/MultipassRenderer.java +++ /dev/null @@ -1,226 +0,0 @@ -package org.sunflow.core.renderer; - -import org.sunflow.core.BucketOrder; -import org.sunflow.core.Display; -import org.sunflow.core.ImageSampler; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.Options; -import org.sunflow.core.Scene; -import org.sunflow.core.ShadingCache; -import org.sunflow.core.ShadingState; -import org.sunflow.core.bucket.BucketOrderFactory; -import org.sunflow.image.Color; -import org.sunflow.math.MathUtils; -import org.sunflow.math.QMC; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class MultipassRenderer implements ImageSampler { - private Scene scene; - private Display display; - // resolution - private int imageWidth; - private int imageHeight; - // bucketing - private String bucketOrderName; - private BucketOrder bucketOrder; - private int bucketSize; - private int bucketCounter; - private int[] bucketCoords; - - // anti-aliasing - private int numSamples; - private float invNumSamples; - private boolean shadingCache; - - public MultipassRenderer() { - bucketSize = 32; - bucketOrderName = "hilbert"; - numSamples = 16; - shadingCache = false; - } - - public boolean prepare(Options options, Scene scene, int w, int h) { - this.scene = scene; - imageWidth = w; - imageHeight = h; - - // fetch options - bucketSize = options.getInt("bucket.size", bucketSize); - bucketOrderName = options.getString("bucket.order", bucketOrderName); - numSamples = options.getInt("aa.samples", numSamples); - shadingCache = options.getBoolean("aa.cache", shadingCache); - - // limit bucket size and compute number of buckets in each direction - bucketSize = MathUtils.clamp(bucketSize, 16, 512); - int numBucketsX = (imageWidth + bucketSize - 1) / bucketSize; - int numBucketsY = (imageHeight + bucketSize - 1) / bucketSize; - bucketOrder = BucketOrderFactory.create(bucketOrderName); - bucketCoords = bucketOrder.getBucketSequence(numBucketsX, numBucketsY); - // validate AA options - numSamples = Math.max(1, numSamples); - invNumSamples = 1.0f / numSamples; - // prepare QMC sampling - UI.printInfo(Module.BCKT, "Multipass renderer settings:"); - UI.printInfo(Module.BCKT, " * Resolution: %dx%d", imageWidth, imageHeight); - UI.printInfo(Module.BCKT, " * Bucket size: %d", bucketSize); - UI.printInfo(Module.BCKT, " * Number of buckets: %dx%d", numBucketsX, numBucketsY); - UI.printInfo(Module.BCKT, " * Samples / pixel: %d", numSamples); - UI.printInfo(Module.BCKT, " * Shading cache: %s", shadingCache ? "enabled" : "disabled"); - return true; - } - - public void render(Display display) { - this.display = display; - display.imageBegin(imageWidth, imageHeight, bucketSize); - // set members variables - bucketCounter = 0; - // start task - Timer timer = new Timer(); - timer.start(); - UI.taskStart("Rendering", 0, bucketCoords.length); - BucketThread[] renderThreads = new BucketThread[scene.getThreads()]; - for (int i = 0; i < renderThreads.length; i++) { - renderThreads[i] = new BucketThread(i); - renderThreads[i].setPriority(scene.getThreadPriority()); - renderThreads[i].start(); - } - for (int i = 0; i < renderThreads.length; i++) { - try { - renderThreads[i].join(); - } catch (InterruptedException e) { - UI.printError(Module.BCKT, "Bucket processing thread %d of %d was interrupted", i + 1, renderThreads.length); - } finally { - renderThreads[i].updateStats(); - } - } - UI.taskStop(); - timer.end(); - UI.printInfo(Module.BCKT, "Render time: %s", timer.toString()); - display.imageEnd(); - } - - private class BucketThread extends Thread { - private final int threadID; - private final IntersectionState istate; - private final ShadingCache cache; - - BucketThread(int threadID) { - this.threadID = threadID; - istate = new IntersectionState(); - cache = shadingCache ? new ShadingCache() : null; - } - - @Override - public void run() { - while (true) { - int bx, by; - synchronized (MultipassRenderer.this) { - if (bucketCounter >= bucketCoords.length) - return; - UI.taskUpdate(bucketCounter); - bx = bucketCoords[bucketCounter + 0]; - by = bucketCoords[bucketCounter + 1]; - bucketCounter += 2; - } - renderBucket(display, bx, by, threadID, istate, cache); - } - } - - void updateStats() { - scene.accumulateStats(istate); - if (shadingCache) - scene.accumulateStats(cache); - } - } - - private void renderBucket(Display display, int bx, int by, int threadID, IntersectionState istate, ShadingCache cache) { - // pixel sized extents - int x0 = bx * bucketSize; - int y0 = by * bucketSize; - int bw = Math.min(bucketSize, imageWidth - x0); - int bh = Math.min(bucketSize, imageHeight - y0); - - // prepare bucket - display.imagePrepare(x0, y0, bw, bh, threadID); - - Color[] bucketRGB = new Color[bw * bh]; - float[] bucketAlpha = new float[bw * bh]; - - for (int y = 0, i = 0, cy = imageHeight - 1 - y0; y < bh; y++, cy--) { - for (int x = 0, cx = x0; x < bw; x++, i++, cx++) { - // sample pixel - Color c = Color.black(); - float a = 0; - int instance = ((cx & ((1 << QMC.MAX_SIGMA_ORDER) - 1)) << QMC.MAX_SIGMA_ORDER) + QMC.sigma(cy & ((1 << QMC.MAX_SIGMA_ORDER) - 1), QMC.MAX_SIGMA_ORDER); - double jitterX = QMC.halton(0, instance); - double jitterY = QMC.halton(1, instance); - double jitterT = QMC.halton(2, instance); - double jitterU = QMC.halton(3, instance); - double jitterV = QMC.halton(4, instance); - for (int s = 0; s < numSamples; s++) { - float rx = cx + 0.5f + (float) warpCubic(QMC.mod1(jitterX + s * invNumSamples)); - float ry = cy + 0.5f + (float) warpCubic(QMC.mod1(jitterY + QMC.halton(0, s))); - double time = QMC.mod1(jitterT + QMC.halton(1, s)); - double lensU = QMC.mod1(jitterU + QMC.halton(2, s)); - double lensV = QMC.mod1(jitterV + QMC.halton(3, s)); - ShadingState state = scene.getRadiance(istate, rx, ry, lensU, lensV, time, instance + s, 5, cache); - if (state != null) { - c.add(state.getResult()); - a++; - } - } - bucketRGB[i] = c.mul(invNumSamples); - bucketAlpha[i] = a * invNumSamples; - if (cache != null) - cache.reset(); - } - } - // update pixels - display.imageUpdate(x0, y0, bw, bh, bucketRGB, bucketAlpha); - } - - /** - * Tent filter warping function. - * - * @param x sample in the [0,1) range - * @return warped sample in the [-1,+1) range - */ - @SuppressWarnings("unused") - private static final float warpTent(float x) { - if (x < 0.5f) - return -1 + (float) Math.sqrt(2 * x); - else - return +1 - (float) Math.sqrt(2 - 2 * x); - } - - /** - * Cubic BSpline warping functions. Formulas from: "Generation of Stratified - * Samples for B-Spline Pixel Filtering" - * http://www.cs.utah.edu/~mstark/papers/ - * - * @param x samples in the [0,1) range - * @return warped sample in the [-2,+2) range - */ - private static final double warpCubic(double x) { - if (x < (1.0 / 24)) - return qpow(24 * x) - 2; - if (x < 0.5f) - return distb1((24.0 / 11.0) * (x - (1.0 / 24.0))) - 1; - if (x < (23.0f / 24)) - return 1 - distb1((24.0 / 11.0) * ((23.0 / 24.0) - x)); - return 2 - qpow(24 * (1 - x)); - } - - private static final double qpow(double x) { - return Math.sqrt(Math.sqrt(x)); - } - - private static final double distb1(double x) { - double u = x; - for (int i = 0; i < 5; i++) - u = (11 * x + u * u * (6 + u * (8 - 9 * u))) / (4 + 12 * u * (1 + u * (1 - u))); - return u; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/renderer/ProgressiveRenderer.java b/src/org/sunflow/core/renderer/ProgressiveRenderer.java deleted file mode 100644 index cd7b525..0000000 --- a/src/org/sunflow/core/renderer/ProgressiveRenderer.java +++ /dev/null @@ -1,158 +0,0 @@ -package org.sunflow.core.renderer; - -import java.util.concurrent.PriorityBlockingQueue; - -import org.sunflow.core.Display; -import org.sunflow.core.ImageSampler; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.Options; -import org.sunflow.core.Scene; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.QMC; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class ProgressiveRenderer implements ImageSampler { - private Scene scene; - private int imageWidth, imageHeight; - private PriorityBlockingQueue smallBucketQueue; - private Display display; - private int counter, counterMax; - - public ProgressiveRenderer() { - imageWidth = 640; - imageHeight = 480; - smallBucketQueue = null; - } - - public boolean prepare(Options options, Scene scene, int w, int h) { - this.scene = scene; - imageWidth = w; - imageHeight = h; - // prepare table used by deterministic anti-aliasing - return true; - } - - public void render(Display display) { - this.display = display; - display.imageBegin(imageWidth, imageHeight, 0); - // create first bucket - SmallBucket b = new SmallBucket(); - b.x = b.y = 0; - int s = Math.max(imageWidth, imageHeight); - b.size = 1; - while (b.size < s) - b.size <<= 1; - smallBucketQueue = new PriorityBlockingQueue(); - smallBucketQueue.add(b); - UI.taskStart("Progressive Render", 0, imageWidth * imageHeight); - Timer t = new Timer(); - t.start(); - counter = 0; - counterMax = imageWidth * imageHeight; - - SmallBucketThread[] renderThreads = new SmallBucketThread[scene.getThreads()]; - for (int i = 0; i < renderThreads.length; i++) { - renderThreads[i] = new SmallBucketThread(); - renderThreads[i].start(); - } - for (int i = 0; i < renderThreads.length; i++) { - try { - renderThreads[i].join(); - } catch (InterruptedException e) { - UI.printError(Module.IPR, "Thread %d of %d was interrupted", i + 1, renderThreads.length); - } finally { - renderThreads[i].updateStats(); - } - } - UI.taskStop(); - t.end(); - UI.printInfo(Module.IPR, "Rendering time: %s", t.toString()); - display.imageEnd(); - } - - private class SmallBucketThread extends Thread { - private final IntersectionState istate = new IntersectionState(); - - @Override - public void run() { - while (true) { - int n = progressiveRenderNext(istate); - synchronized (ProgressiveRenderer.this) { - if (counter >= counterMax) - return; - counter += n; - UI.taskUpdate(counter); - } - if (UI.taskCanceled()) - return; - } - } - - void updateStats() { - scene.accumulateStats(istate); - } - } - - private int progressiveRenderNext(IntersectionState istate) { - final int TASK_SIZE = 16; - SmallBucket first = smallBucketQueue.poll(); - if (first == null) - return 0; - int ds = first.size / TASK_SIZE; - boolean useMask = !smallBucketQueue.isEmpty(); - int mask = 2 * first.size / TASK_SIZE - 1; - int pixels = 0; - for (int i = 0, y = first.y; i < TASK_SIZE && y < imageHeight; i++, y += ds) { - for (int j = 0, x = first.x; j < TASK_SIZE && x < imageWidth; j++, x += ds) { - // check to see if this is a pixel from a higher level tile - if (useMask && (x & mask) == 0 && (y & mask) == 0) - continue; - int instance = ((x & ((1 << QMC.MAX_SIGMA_ORDER) - 1)) << QMC.MAX_SIGMA_ORDER) + QMC.sigma(y & ((1 << QMC.MAX_SIGMA_ORDER) - 1), QMC.MAX_SIGMA_ORDER); - double time = QMC.halton(1, instance); - double lensU = QMC.halton(2, instance); - double lensV = QMC.halton(3, instance); - ShadingState state = scene.getRadiance(istate, x, imageHeight - 1 - y, lensU, lensV, time, instance, 4, null); - Color c = state != null ? state.getResult() : Color.BLACK; - pixels++; - // fill region - display.imageFill(x, y, Math.min(ds, imageWidth - x), Math.min(ds, imageHeight - y), c, state == null ? 0 : 1); - } - } - if (first.size >= 2 * TASK_SIZE) { - // generate child buckets - int size = first.size >>> 1; - for (int i = 0; i < 2; i++) { - if (first.y + i * size < imageHeight) { - for (int j = 0; j < 2; j++) { - if (first.x + j * size < imageWidth) { - SmallBucket b = new SmallBucket(); - b.x = first.x + j * size; - b.y = first.y + i * size; - b.size = size; - b.constrast = 1.0f / size; - smallBucketQueue.put(b); - } - } - } - } - } - return pixels; - } - - // progressive rendering - private static class SmallBucket implements Comparable { - int x, y, size; - float constrast; - - public int compareTo(SmallBucket o) { - if (constrast < o.constrast) - return -1; - if (constrast == o.constrast) - return 0; - return 1; - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/renderer/SimpleRenderer.java b/src/org/sunflow/core/renderer/SimpleRenderer.java deleted file mode 100644 index fd7bf5e..0000000 --- a/src/org/sunflow/core/renderer/SimpleRenderer.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.sunflow.core.renderer; - -import org.sunflow.core.Display; -import org.sunflow.core.ImageSampler; -import org.sunflow.core.IntersectionState; -import org.sunflow.core.Options; -import org.sunflow.core.Scene; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class SimpleRenderer implements ImageSampler { - private Scene scene; - private Display display; - private int imageWidth, imageHeight; - private int numBucketsX, numBucketsY; - private int bucketCounter, numBuckets; - - public boolean prepare(Options options, Scene scene, int w, int h) { - this.scene = scene; - imageWidth = w; - imageHeight = h; - numBucketsX = (imageWidth + 31) >>> 5; - numBucketsY = (imageHeight + 31) >>> 5; - numBuckets = numBucketsX * numBucketsY; - return true; - } - - public void render(Display display) { - this.display = display; - display.imageBegin(imageWidth, imageHeight, 32); - // set members variables - bucketCounter = 0; - // start task - Timer timer = new Timer(); - timer.start(); - BucketThread[] renderThreads = new BucketThread[scene.getThreads()]; - for (int i = 0; i < renderThreads.length; i++) { - renderThreads[i] = new BucketThread(); - renderThreads[i].start(); - } - for (int i = 0; i < renderThreads.length; i++) { - try { - renderThreads[i].join(); - } catch (InterruptedException e) { - UI.printError(Module.BCKT, "Bucket processing thread %d of %d was interrupted", i + 1, renderThreads.length); - } finally { - renderThreads[i].updateStats(); - } - } - timer.end(); - UI.printInfo(Module.BCKT, "Render time: %s", timer.toString()); - display.imageEnd(); - } - - private class BucketThread extends Thread { - private final IntersectionState istate = new IntersectionState(); - - @Override - public void run() { - while (true) { - int bx, by; - synchronized (SimpleRenderer.this) { - if (bucketCounter >= numBuckets) - return; - by = bucketCounter / numBucketsX; - bx = bucketCounter % numBucketsX; - bucketCounter++; - } - renderBucket(bx, by, istate); - } - } - - void updateStats() { - scene.accumulateStats(istate); - } - } - - public void renderBucket(int bx, int by, IntersectionState istate) { - // pixel sized extents - int x0 = bx * 32; - int y0 = by * 32; - int bw = Math.min(32, imageWidth - x0); - int bh = Math.min(32, imageHeight - y0); - - Color[] bucketRGB = new Color[bw * bh]; - float[] bucketAlpha = new float[bw * bh]; - - for (int y = 0, i = 0; y < bh; y++) { - for (int x = 0; x < bw; x++, i++) { - ShadingState state = scene.getRadiance(istate, x0 + x, imageHeight - 1 - (y0 + y), 0.0, 0.0, 0.0, 0, 0, null); - bucketRGB[i] = (state != null) ? state.getResult() : Color.BLACK; - bucketAlpha[i] = (state != null) ? 1 : 0; - } - } - // update pixels - display.imageUpdate(x0, y0, bw, bh, bucketRGB, bucketAlpha); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/AmbientOcclusionShader.java b/src/org/sunflow/core/shader/AmbientOcclusionShader.java deleted file mode 100644 index 5f3b087..0000000 --- a/src/org/sunflow/core/shader/AmbientOcclusionShader.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; - -public class AmbientOcclusionShader implements Shader { - private Color bright; - private Color dark; - private int samples; - private float maxDist; - - public AmbientOcclusionShader() { - bright = Color.WHITE; - dark = Color.BLACK; - samples = 32; - maxDist = Float.POSITIVE_INFINITY; - } - - public AmbientOcclusionShader(Color c, float d) { - this(); - bright = c; - maxDist = d; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - bright = pl.getColor("bright", bright); - dark = pl.getColor("dark", dark); - samples = pl.getInt("samples", samples); - maxDist = pl.getFloat("maxdist", maxDist); - if (maxDist <= 0) - maxDist = Float.POSITIVE_INFINITY; - return true; - } - - public Color getBrightColor(ShadingState state) { - return bright; - } - - public Color getRadiance(ShadingState state) { - return state.occlusion(samples, maxDist, getBrightColor(state), dark); - } - - public void scatterPhoton(ShadingState state, Color power) { - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/AnisotropicWardShader.java b/src/org/sunflow/core/shader/AnisotropicWardShader.java deleted file mode 100644 index 72aed8e..0000000 --- a/src/org/sunflow/core/shader/AnisotropicWardShader.java +++ /dev/null @@ -1,211 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.LightSample; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Vector3; - -public class AnisotropicWardShader implements Shader { - private Color rhoD; // diffuse reflectance - private Color rhoS; // specular reflectance - private float alphaX; - private float alphaY; - private int numRays; - - public AnisotropicWardShader() { - rhoD = Color.GRAY; - rhoS = Color.GRAY; - alphaX = 1; - alphaY = 1; - numRays = 4; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - rhoD = pl.getColor("diffuse", rhoD); - rhoS = pl.getColor("specular", rhoS); - alphaX = pl.getFloat("roughnessX", alphaX); - alphaY = pl.getFloat("roughnessY", alphaY); - numRays = pl.getInt("samples", numRays); - return true; - } - - protected Color getDiffuse(ShadingState state) { - return rhoD; - } - - private float brdf(Vector3 i, Vector3 o, OrthoNormalBasis basis) { - float fr = 4 * (float) Math.PI * alphaX * alphaY; - float p = basis.untransformZ(i) * basis.untransformZ(o); - if (p > 0) - fr *= (float) Math.sqrt(p); - else - fr = 0; - Vector3 h = Vector3.add(i, o, new Vector3()); - basis.untransform(h); - float hx = h.x / alphaX; - hx *= hx; - float hy = h.y / alphaY; - hy *= hy; - float hn = h.z * h.z; - if (fr > 0) - fr = (float) Math.exp(-(hx + hy) / hn) / fr; - return fr; - } - - public Color getRadiance(ShadingState state) { - // make sure we are on the right side of the material - state.faceforward(); - OrthoNormalBasis onb = state.getBasis(); - // direct lighting and caustics - state.initLightSamples(); - state.initCausticSamples(); - Color lr = Color.black(); - // compute specular contribution - if (state.includeSpecular()) { - Vector3 in = state.getRay().getDirection().negate(new Vector3()); - for (LightSample sample : state) { - float cosNL = sample.dot(state.getNormal()); - float fr = brdf(in, sample.getShadowRay().getDirection(), onb); - lr.madd(cosNL * fr, sample.getSpecularRadiance()); - } - - // indirect lighting - specular - if (numRays > 0) { - int n = state.getDepth() == 0 ? numRays : 1; - for (int i = 0; i < n; i++) { - // specular indirect lighting - double r1 = state.getRandom(i, 0, n); - double r2 = state.getRandom(i, 1, n); - - float alphaRatio = alphaY / alphaX; - float phi = 0; - if (r1 < 0.25) { - double val = 4 * r1; - phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); - } else if (r1 < 0.5) { - double val = 1 - 4 * (0.5 - r1); - phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); - phi = (float) Math.PI - phi; - } else if (r1 < 0.75) { - double val = 4 * (r1 - 0.5); - phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); - phi += Math.PI; - } else { - double val = 1 - 4 * (1 - r1); - phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); - phi = 2 * (float) Math.PI - phi; - } - - float cosPhi = (float) Math.cos(phi); - float sinPhi = (float) Math.sin(phi); - - float denom = (cosPhi * cosPhi) / (alphaX * alphaX) + (sinPhi * sinPhi) / (alphaY * alphaY); - float theta = (float) Math.atan(Math.sqrt(-Math.log(1 - r2) / denom)); - - float sinTheta = (float) Math.sin(theta); - float cosTheta = (float) Math.cos(theta); - - Vector3 h = new Vector3(); - h.x = sinTheta * cosPhi; - h.y = sinTheta * sinPhi; - h.z = cosTheta; - onb.transform(h); - - Vector3 o = new Vector3(); - float ih = Vector3.dot(h, in); - o.x = 2 * ih * h.x - in.x; - o.y = 2 * ih * h.y - in.y; - o.z = 2 * ih * h.z - in.z; - - float no = onb.untransformZ(o); - float ni = onb.untransformZ(in); - float w = ih * cosTheta * cosTheta * cosTheta * (float) Math.sqrt(Math.abs(no / ni)); - - Ray r = new Ray(state.getPoint(), o); - lr.madd(w / n, state.traceGlossy(r, i)); - } - } - lr.mul(rhoS); - } - // add diffuse contribution - lr.add(state.diffuse(getDiffuse(state))); - return lr; - } - - public void scatterPhoton(ShadingState state, Color power) { - // make sure we are on the right side of the material - state.faceforward(); - Color d = getDiffuse(state); - state.storePhoton(state.getRay().getDirection(), power, d); - float avgD = d.getAverage(); - float avgS = rhoS.getAverage(); - double rnd = state.getRandom(0, 0, 1); - if (rnd < avgD) { - // photon is scattered diffusely - power.mul(d).mul(1.0f / avgD); - OrthoNormalBasis onb = state.getBasis(); - double u = 2 * Math.PI * rnd / avgD; - double v = state.getRandom(0, 1, 1); - float s = (float) Math.sqrt(v); - float s1 = (float) Math.sqrt(1.0f - v); - Vector3 w = new Vector3((float) Math.cos(u) * s, (float) Math.sin(u) * s, s1); - w = onb.transform(w, new Vector3()); - state.traceDiffusePhoton(new Ray(state.getPoint(), w), power); - } else if (rnd < avgD + avgS) { - // photon is scattered specularly - power.mul(rhoS).mul(1 / avgS); - OrthoNormalBasis basis = state.getBasis(); - Vector3 in = state.getRay().getDirection().negate(new Vector3()); - double r1 = rnd / avgS; - double r2 = state.getRandom(0, 1, 1); - - float alphaRatio = alphaY / alphaX; - float phi = 0; - if (r1 < 0.25) { - double val = 4 * r1; - phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); - } else if (r1 < 0.5) { - double val = 1 - 4 * (0.5 - r1); - phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); - phi = (float) Math.PI - phi; - } else if (r1 < 0.75) { - double val = 4 * (r1 - 0.5); - phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); - phi += Math.PI; - } else { - double val = 1 - 4 * (1 - r1); - phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); - phi = 2 * (float) Math.PI - phi; - } - - float cosPhi = (float) Math.cos(phi); - float sinPhi = (float) Math.sin(phi); - - float denom = (cosPhi * cosPhi) / (alphaX * alphaX) + (sinPhi * sinPhi) / (alphaY * alphaY); - float theta = (float) Math.atan(Math.sqrt(-Math.log(1 - r2) / denom)); - - float sinTheta = (float) Math.sin(theta); - float cosTheta = (float) Math.cos(theta); - - Vector3 h = new Vector3(); - h.x = sinTheta * cosPhi; - h.y = sinTheta * sinPhi; - h.z = cosTheta; - basis.transform(h); - - Vector3 o = new Vector3(); - float ih = Vector3.dot(h, in); - o.x = 2 * ih * h.x - in.x; - o.y = 2 * ih * h.y - in.y; - o.z = 2 * ih * h.z - in.z; - - Ray r = new Ray(state.getPoint(), o); - state.traceReflectionPhoton(r, power); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/ConstantShader.java b/src/org/sunflow/core/shader/ConstantShader.java deleted file mode 100644 index e012e0d..0000000 --- a/src/org/sunflow/core/shader/ConstantShader.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; - -public class ConstantShader implements Shader { - private Color c; - - public ConstantShader() { - c = Color.WHITE; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - c = pl.getColor("color", c); - return true; - } - - public Color getRadiance(ShadingState state) { - return c; - } - - public void scatterPhoton(ShadingState state, Color power) { - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/DiffuseShader.java b/src/org/sunflow/core/shader/DiffuseShader.java deleted file mode 100644 index afb4765..0000000 --- a/src/org/sunflow/core/shader/DiffuseShader.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Vector3; - -public class DiffuseShader implements Shader { - private Color diff; - - public DiffuseShader() { - diff = Color.WHITE; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - diff = pl.getColor("diffuse", diff); - return true; - } - - public Color getDiffuse(ShadingState state) { - return diff; - } - - public Color getRadiance(ShadingState state) { - // make sure we are on the right side of the material - state.faceforward(); - // setup lighting - state.initLightSamples(); - state.initCausticSamples(); - return state.diffuse(getDiffuse(state)); - } - - public void scatterPhoton(ShadingState state, Color power) { - Color diffuse; - // make sure we are on the right side of the material - if (Vector3.dot(state.getNormal(), state.getRay().getDirection()) > 0.0) { - state.getNormal().negate(); - state.getGeoNormal().negate(); - } - diffuse = getDiffuse(state); - state.storePhoton(state.getRay().getDirection(), power, diffuse); - float avg = diffuse.getAverage(); - double rnd = state.getRandom(0, 0, 1); - if (rnd < avg) { - // photon is scattered - power.mul(diffuse).mul(1.0f / avg); - OrthoNormalBasis onb = state.getBasis(); - double u = 2 * Math.PI * rnd / avg; - double v = state.getRandom(0, 1, 1); - float s = (float) Math.sqrt(v); - float s1 = (float) Math.sqrt(1.0 - v); - Vector3 w = new Vector3((float) Math.cos(u) * s, (float) Math.sin(u) * s, s1); - w = onb.transform(w, new Vector3()); - state.traceDiffusePhoton(new Ray(state.getPoint(), w), power); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/GlassShader.java b/src/org/sunflow/core/shader/GlassShader.java deleted file mode 100644 index fce5583..0000000 --- a/src/org/sunflow/core/shader/GlassShader.java +++ /dev/null @@ -1,139 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.Vector3; - -public class GlassShader implements Shader { - private float eta; // refraction index ratio - private float f0; // fresnel normal incidence - private Color color; - private float absorptionDistance; - private Color absorptionColor; - - public GlassShader() { - eta = 1.3f; - color = Color.WHITE; - absorptionDistance = 0; // disabled by default - absorptionColor = Color.GRAY; // 50% absorbtion - } - - public boolean update(ParameterList pl, SunflowAPI api) { - color = pl.getColor("color", color); - eta = pl.getFloat("eta", eta); - f0 = (1 - eta) / (1 + eta); - f0 = f0 * f0; - absorptionDistance = pl.getFloat("absorption.distance", absorptionDistance); - absorptionColor = pl.getColor("absorption.color", absorptionColor); - return true; - } - - public Color getRadiance(ShadingState state) { - if (!state.includeSpecular()) - return Color.BLACK; - Vector3 reflDir = new Vector3(); - Vector3 refrDir = new Vector3(); - state.faceforward(); - float cos = state.getCosND(); - boolean inside = state.isBehind(); - float neta = inside ? eta : 1.0f / eta; - - float dn = 2 * cos; - reflDir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; - reflDir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; - reflDir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; - - // refracted ray - float arg = 1 - (neta * neta * (1 - (cos * cos))); - boolean tir = arg < 0; - if (tir) - refrDir.x = refrDir.y = refrDir.z = 0; - else { - float nK = (neta * cos) - (float) Math.sqrt(arg); - refrDir.x = (neta * state.getRay().dx) + (nK * state.getNormal().x); - refrDir.y = (neta * state.getRay().dy) + (nK * state.getNormal().y); - refrDir.z = (neta * state.getRay().dz) + (nK * state.getNormal().z); - } - - // compute Fresnel terms - float cosTheta1 = Vector3.dot(state.getNormal(), reflDir); - float cosTheta2 = -Vector3.dot(state.getNormal(), refrDir); - - float pPara = (cosTheta1 - eta * cosTheta2) / (cosTheta1 + eta * cosTheta2); - float pPerp = (eta * cosTheta1 - cosTheta2) / (eta * cosTheta1 + cosTheta2); - float kr = 0.5f * (pPara * pPara + pPerp * pPerp); - float kt = 1 - kr; - - Color absorbtion = null; - if (inside && absorptionDistance > 0) { - // this ray is inside the object and leaving it - // compute attenuation that occured along the ray - absorbtion = Color.mul(-state.getRay().getMax() / absorptionDistance, absorptionColor.copy().opposite()).exp(); - if (absorbtion.isBlack()) - return Color.BLACK; // nothing goes through - } - // refracted ray - Color ret = Color.black(); - if (!tir) { - ret.madd(kt, state.traceRefraction(new Ray(state.getPoint(), refrDir), 0)).mul(color); - } - if (!inside || tir) - ret.add(Color.mul(kr, state.traceReflection(new Ray(state.getPoint(), reflDir), 0)).mul(color)); - return absorbtion != null ? ret.mul(absorbtion) : ret; - } - - public void scatterPhoton(ShadingState state, Color power) { - Color refr = Color.mul(1 - f0, color); - Color refl = Color.mul(f0, color); - float avgR = refl.getAverage(); - float avgT = refr.getAverage(); - double rnd = state.getRandom(0, 0, 1); - if (rnd < avgR) { - state.faceforward(); - // don't reflect internally - if (state.isBehind()) - return; - // photon is reflected - float cos = state.getCosND(); - power.mul(refl).mul(1.0f / avgR); - float dn = 2 * cos; - Vector3 dir = new Vector3(); - dir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; - dir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; - dir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; - state.traceReflectionPhoton(new Ray(state.getPoint(), dir), power); - } else if (rnd < avgR + avgT) { - state.faceforward(); - // photon is refracted - float cos = state.getCosND(); - float neta = state.isBehind() ? eta : 1.0f / eta; - power.mul(refr).mul(1.0f / avgT); - float wK = -neta; - float arg = 1 - (neta * neta * (1 - (cos * cos))); - Vector3 dir = new Vector3(); - if (state.isBehind() && absorptionDistance > 0) { - // this ray is inside the object and leaving it - // compute attenuation that occured along the ray - power.mul(Color.mul(-state.getRay().getMax() / absorptionDistance, absorptionColor.copy().opposite()).exp()); - } - if (arg < 0) { - // TIR - float dn = 2 * cos; - dir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; - dir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; - dir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; - state.traceReflectionPhoton(new Ray(state.getPoint(), dir), power); - } else { - float nK = (neta * cos) - (float) Math.sqrt(arg); - dir.x = (-wK * state.getRay().dx) + (nK * state.getNormal().x); - dir.y = (-wK * state.getRay().dy) + (nK * state.getNormal().y); - dir.z = (-wK * state.getRay().dz) + (nK * state.getNormal().z); - state.traceRefractionPhoton(new Ray(state.getPoint(), dir), power); - } - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/IDShader.java b/src/org/sunflow/core/shader/IDShader.java deleted file mode 100644 index cb65ac6..0000000 --- a/src/org/sunflow/core/shader/IDShader.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.Vector3; - -public class IDShader implements Shader { - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public Color getRadiance(ShadingState state) { - Vector3 n = state.getNormal(); - float f = n == null ? 1.0f : Math.abs(state.getRay().dot(n)); - return new Color(state.getInstance().hashCode()).mul(f); - } - - public void scatterPhoton(ShadingState state, Color power) { - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/MirrorShader.java b/src/org/sunflow/core/shader/MirrorShader.java deleted file mode 100644 index 158e383..0000000 --- a/src/org/sunflow/core/shader/MirrorShader.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.Vector3; - -public class MirrorShader implements Shader { - private Color color; - - public MirrorShader() { - color = Color.WHITE; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - color = pl.getColor("color", color); - return true; - } - - public Color getRadiance(ShadingState state) { - if (!state.includeSpecular()) - return Color.BLACK; - state.faceforward(); - float cos = state.getCosND(); - float dn = 2 * cos; - Vector3 refDir = new Vector3(); - refDir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; - refDir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; - refDir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; - Ray refRay = new Ray(state.getPoint(), refDir); - - // compute Fresnel term - cos = 1 - cos; - float cos2 = cos * cos; - float cos5 = cos2 * cos2 * cos; - Color ret = Color.white(); - ret.sub(color); - ret.mul(cos5); - ret.add(color); - return ret.mul(state.traceReflection(refRay, 0)); - } - - public void scatterPhoton(ShadingState state, Color power) { - float avg = color.getAverage(); - double rnd = state.getRandom(0, 0, 1); - if (rnd >= avg) - return; - state.faceforward(); - float cos = state.getCosND(); - power.mul(color).mul(1.0f / avg); - // photon is reflected - float dn = 2 * cos; - Vector3 dir = new Vector3(); - dir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; - dir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; - dir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; - state.traceReflectionPhoton(new Ray(state.getPoint(), dir), power); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/NormalShader.java b/src/org/sunflow/core/shader/NormalShader.java deleted file mode 100644 index f07b5be..0000000 --- a/src/org/sunflow/core/shader/NormalShader.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.Vector3; - -public class NormalShader implements Shader { - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public Color getRadiance(ShadingState state) { - Vector3 n = state.getNormal(); - if (n == null) - return Color.BLACK; - float r = (n.x + 1) * 0.5f; - float g = (n.y + 1) * 0.5f; - float b = (n.z + 1) * 0.5f; - return new Color(r, g, b); - } - - public void scatterPhoton(ShadingState state, Color power) { - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/PhongShader.java b/src/org/sunflow/core/shader/PhongShader.java deleted file mode 100644 index accae35..0000000 --- a/src/org/sunflow/core/shader/PhongShader.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Vector3; - -public class PhongShader implements Shader { - private Color diff; - private Color spec; - private float power; - private int numRays; - - public PhongShader() { - diff = Color.GRAY; - spec = Color.GRAY; - power = 20; - numRays = 4; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - diff = pl.getColor("diffuse", diff); - spec = pl.getColor("specular", spec); - power = pl.getFloat("power", power); - numRays = pl.getInt("samples", numRays); - return true; - } - - protected Color getDiffuse(ShadingState state) { - return diff; - } - - public Color getRadiance(ShadingState state) { - // make sure we are on the right side of the material - state.faceforward(); - // setup lighting - state.initLightSamples(); - state.initCausticSamples(); - // execute shader - return state.diffuse(getDiffuse(state)).add(state.specularPhong(spec, power, numRays)); - } - - public void scatterPhoton(ShadingState state, Color power) { - // make sure we are on the right side of the material - state.faceforward(); - Color d = getDiffuse(state); - state.storePhoton(state.getRay().getDirection(), power, d); - float avgD = d.getAverage(); - float avgS = spec.getAverage(); - double rnd = state.getRandom(0, 0, 1); - if (rnd < avgD) { - // photon is scattered diffusely - power.mul(d).mul(1.0f / avgD); - OrthoNormalBasis onb = state.getBasis(); - double u = 2 * Math.PI * rnd / avgD; - double v = state.getRandom(0, 1, 1); - float s = (float) Math.sqrt(v); - float s1 = (float) Math.sqrt(1.0f - v); - Vector3 w = new Vector3((float) Math.cos(u) * s, (float) Math.sin(u) * s, s1); - w = onb.transform(w, new Vector3()); - state.traceDiffusePhoton(new Ray(state.getPoint(), w), power); - } else if (rnd < avgD + avgS) { - // photon is scattered specularly - float dn = 2.0f * state.getCosND(); - // reflected direction - Vector3 refDir = new Vector3(); - refDir.x = (dn * state.getNormal().x) + state.getRay().dx; - refDir.y = (dn * state.getNormal().y) + state.getRay().dy; - refDir.z = (dn * state.getNormal().z) + state.getRay().dz; - power.mul(spec).mul(1.0f / avgS); - OrthoNormalBasis onb = state.getBasis(); - double u = 2 * Math.PI * (rnd - avgD) / avgS; - double v = state.getRandom(0, 1, 1); - float s = (float) Math.pow(v, 1 / (this.power + 1)); - float s1 = (float) Math.sqrt(1 - s * s); - Vector3 w = new Vector3((float) Math.cos(u) * s1, (float) Math.sin(u) * s1, s); - w = onb.transform(w, new Vector3()); - state.traceReflectionPhoton(new Ray(state.getPoint(), w), power); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/PrimIDShader.java b/src/org/sunflow/core/shader/PrimIDShader.java deleted file mode 100644 index 12191ea..0000000 --- a/src/org/sunflow/core/shader/PrimIDShader.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.Vector3; - -public class PrimIDShader implements Shader { - private static final Color[] BORDERS = { Color.RED, Color.GREEN, - Color.BLUE, Color.YELLOW, Color.CYAN, Color.MAGENTA }; - - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public Color getRadiance(ShadingState state) { - Vector3 n = state.getNormal(); - float f = n == null ? 1.0f : Math.abs(state.getRay().dot(n)); - return BORDERS[state.getPrimitiveID() % BORDERS.length].copy().mul(f); - } - - public void scatterPhoton(ShadingState state, Color power) { - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/QuickGrayShader.java b/src/org/sunflow/core/shader/QuickGrayShader.java deleted file mode 100644 index 26911f1..0000000 --- a/src/org/sunflow/core/shader/QuickGrayShader.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Vector3; - -public class QuickGrayShader implements Shader { - public QuickGrayShader() { - } - - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public Color getRadiance(ShadingState state) { - if (state.getNormal() == null) { - // if this shader has been applied to an infinite instance because - // of shader overrides - // run the default shader, otherwise, just shade black - return state.getShader() != this ? state.getShader().getRadiance(state) : Color.BLACK; - } - // make sure we are on the right side of the material - state.faceforward(); - // setup lighting - state.initLightSamples(); - state.initCausticSamples(); - return state.diffuse(Color.GRAY); - } - - public void scatterPhoton(ShadingState state, Color power) { - Color diffuse; - // make sure we are on the right side of the material - if (Vector3.dot(state.getNormal(), state.getRay().getDirection()) > 0.0) { - state.getNormal().negate(); - state.getGeoNormal().negate(); - } - diffuse = Color.GRAY; - state.storePhoton(state.getRay().getDirection(), power, diffuse); - float avg = diffuse.getAverage(); - double rnd = state.getRandom(0, 0, 1); - if (rnd < avg) { - // photon is scattered - power.mul(diffuse).mul(1.0f / avg); - OrthoNormalBasis onb = state.getBasis(); - double u = 2 * Math.PI * rnd / avg; - double v = state.getRandom(0, 1, 1); - float s = (float) Math.sqrt(v); - float s1 = (float) Math.sqrt(1.0 - v); - Vector3 w = new Vector3((float) Math.cos(u) * s, (float) Math.sin(u) * s, s1); - w = onb.transform(w, new Vector3()); - state.traceDiffusePhoton(new Ray(state.getPoint(), w), power); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/ShinyDiffuseShader.java b/src/org/sunflow/core/shader/ShinyDiffuseShader.java deleted file mode 100644 index 904b4dc..0000000 --- a/src/org/sunflow/core/shader/ShinyDiffuseShader.java +++ /dev/null @@ -1,93 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Vector3; - -public class ShinyDiffuseShader implements Shader { - private Color diff; - private float refl; - - public ShinyDiffuseShader() { - diff = Color.GRAY; - refl = 0.5f; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - diff = pl.getColor("diffuse", diff); - refl = pl.getFloat("shiny", refl); - return true; - } - - public Color getDiffuse(ShadingState state) { - return diff; - } - - public Color getRadiance(ShadingState state) { - // make sure we are on the right side of the material - state.faceforward(); - // direct lighting - state.initLightSamples(); - state.initCausticSamples(); - Color d = getDiffuse(state); - Color lr = state.diffuse(d); - if (!state.includeSpecular()) - return lr; - float cos = state.getCosND(); - float dn = 2 * cos; - Vector3 refDir = new Vector3(); - refDir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; - refDir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; - refDir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; - Ray refRay = new Ray(state.getPoint(), refDir); - // compute Fresnel term - cos = 1 - cos; - float cos2 = cos * cos; - float cos5 = cos2 * cos2 * cos; - - Color ret = Color.white(); - Color r = d.copy().mul(refl); - ret.sub(r); - ret.mul(cos5); - ret.add(r); - return lr.add(ret.mul(state.traceReflection(refRay, 0))); - } - - public void scatterPhoton(ShadingState state, Color power) { - Color diffuse; - // make sure we are on the right side of the material - state.faceforward(); - diffuse = getDiffuse(state); - state.storePhoton(state.getRay().getDirection(), power, diffuse); - float d = diffuse.getAverage(); - float r = d * refl; - double rnd = state.getRandom(0, 0, 1); - if (rnd < d) { - // photon is scattered - power.mul(diffuse).mul(1.0f / d); - OrthoNormalBasis onb = state.getBasis(); - double u = 2 * Math.PI * rnd / d; - double v = state.getRandom(0, 1, 1); - float s = (float) Math.sqrt(v); - float s1 = (float) Math.sqrt(1.0 - v); - Vector3 w = new Vector3((float) Math.cos(u) * s, (float) Math.sin(u) * s, s1); - w = onb.transform(w, new Vector3()); - state.traceDiffusePhoton(new Ray(state.getPoint(), w), power); - } else if (rnd < d + r) { - float cos = -Vector3.dot(state.getNormal(), state.getRay().getDirection()); - power.mul(diffuse).mul(1.0f / d); - // photon is reflected - float dn = 2 * cos; - Vector3 dir = new Vector3(); - dir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; - dir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; - dir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; - state.traceReflectionPhoton(new Ray(state.getPoint(), dir), power); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/SimpleShader.java b/src/org/sunflow/core/shader/SimpleShader.java deleted file mode 100644 index 9296b02..0000000 --- a/src/org/sunflow/core/shader/SimpleShader.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; - -public class SimpleShader implements Shader { - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public Color getRadiance(ShadingState state) { - return new Color(Math.abs(state.getRay().dot(state.getNormal()))); - } - - public void scatterPhoton(ShadingState state, Color power) { - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/TexturedAmbientOcclusionShader.java b/src/org/sunflow/core/shader/TexturedAmbientOcclusionShader.java deleted file mode 100644 index adb8059..0000000 --- a/src/org/sunflow/core/shader/TexturedAmbientOcclusionShader.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.ShadingState; -import org.sunflow.core.Texture; -import org.sunflow.core.TextureCache; -import org.sunflow.image.Color; - -public class TexturedAmbientOcclusionShader extends AmbientOcclusionShader { - private Texture tex; - - public TexturedAmbientOcclusionShader() { - tex = null; - } - - @Override - public boolean update(ParameterList pl, SunflowAPI api) { - String filename = pl.getString("texture", null); - if (filename != null) - tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false); - return tex != null && super.update(pl, api); - } - - @Override - public Color getBrightColor(ShadingState state) { - return tex.getPixel(state.getUV().x, state.getUV().y); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/TexturedDiffuseShader.java b/src/org/sunflow/core/shader/TexturedDiffuseShader.java deleted file mode 100644 index d29f0c5..0000000 --- a/src/org/sunflow/core/shader/TexturedDiffuseShader.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.ShadingState; -import org.sunflow.core.Texture; -import org.sunflow.core.TextureCache; -import org.sunflow.image.Color; - -public class TexturedDiffuseShader extends DiffuseShader { - private Texture tex; - - public TexturedDiffuseShader() { - tex = null; - } - - @Override - public boolean update(ParameterList pl, SunflowAPI api) { - String filename = pl.getString("texture", null); - if (filename != null) - tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false); - return tex != null && super.update(pl, api); - } - - @Override - public Color getDiffuse(ShadingState state) { - return tex.getPixel(state.getUV().x, state.getUV().y); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/TexturedPhongShader.java b/src/org/sunflow/core/shader/TexturedPhongShader.java deleted file mode 100644 index 20aeae1..0000000 --- a/src/org/sunflow/core/shader/TexturedPhongShader.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.ShadingState; -import org.sunflow.core.Texture; -import org.sunflow.core.TextureCache; -import org.sunflow.image.Color; - -public class TexturedPhongShader extends PhongShader { - private Texture tex; - - public TexturedPhongShader() { - tex = null; - } - - @Override - public boolean update(ParameterList pl, SunflowAPI api) { - String filename = pl.getString("texture", null); - if (filename != null) - tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false); - return tex != null && super.update(pl, api); - } - - @Override - public Color getDiffuse(ShadingState state) { - return tex.getPixel(state.getUV().x, state.getUV().y); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/TexturedShinyDiffuseShader.java b/src/org/sunflow/core/shader/TexturedShinyDiffuseShader.java deleted file mode 100644 index 19328a7..0000000 --- a/src/org/sunflow/core/shader/TexturedShinyDiffuseShader.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.ShadingState; -import org.sunflow.core.Texture; -import org.sunflow.core.TextureCache; -import org.sunflow.image.Color; - -public class TexturedShinyDiffuseShader extends ShinyDiffuseShader { - private Texture tex; - - public TexturedShinyDiffuseShader() { - tex = null; - } - - @Override - public boolean update(ParameterList pl, SunflowAPI api) { - String filename = pl.getString("texture", null); - if (filename != null) - tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false); - return tex != null && super.update(pl, api); - } - - @Override - public Color getDiffuse(ShadingState state) { - return tex.getPixel(state.getUV().x, state.getUV().y); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/TexturedWardShader.java b/src/org/sunflow/core/shader/TexturedWardShader.java deleted file mode 100644 index 188390a..0000000 --- a/src/org/sunflow/core/shader/TexturedWardShader.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.ShadingState; -import org.sunflow.core.Texture; -import org.sunflow.core.TextureCache; -import org.sunflow.image.Color; - -public class TexturedWardShader extends AnisotropicWardShader { - private Texture tex; - - public TexturedWardShader() { - tex = null; - } - - @Override - public boolean update(ParameterList pl, SunflowAPI api) { - String filename = pl.getString("texture", null); - if (filename != null) - tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false); - return tex != null && super.update(pl, api); - } - - @Override - public Color getDiffuse(ShadingState state) { - return tex.getPixel(state.getUV().x, state.getUV().y); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/UVShader.java b/src/org/sunflow/core/shader/UVShader.java deleted file mode 100644 index 8646fb8..0000000 --- a/src/org/sunflow/core/shader/UVShader.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; - -public class UVShader implements Shader { - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public Color getRadiance(ShadingState state) { - if (state.getUV() == null) - return Color.BLACK; - return new Color(state.getUV().x, state.getUV().y, 0); - } - - public void scatterPhoton(ShadingState state, Color power) { - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/UberShader.java b/src/org/sunflow/core/shader/UberShader.java deleted file mode 100644 index 236436e..0000000 --- a/src/org/sunflow/core/shader/UberShader.java +++ /dev/null @@ -1,141 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Ray; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.core.Texture; -import org.sunflow.core.TextureCache; -import org.sunflow.image.Color; -import org.sunflow.math.MathUtils; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Vector3; - -public class UberShader implements Shader { - private Color diff; - private Color spec; - private Texture diffmap; - private Texture specmap; - private float diffBlend; - private float specBlend; - private float glossyness; - private int numSamples; - - public UberShader() { - diff = spec = Color.GRAY; - diffmap = specmap = null; - diffBlend = specBlend = 1; - glossyness = 0; - numSamples = 4; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - diff = pl.getColor("diffuse", diff); - spec = pl.getColor("specular", spec); - String filename; - filename = pl.getString("diffuse.texture", null); - if (filename != null) - diffmap = TextureCache.getTexture(api.resolveTextureFilename(filename), false); - filename = pl.getString("specular.texture", null); - if (filename != null) - specmap = TextureCache.getTexture(api.resolveTextureFilename(filename), false); - diffBlend = MathUtils.clamp(pl.getFloat("diffuse.blend", diffBlend), 0, 1); - specBlend = MathUtils.clamp(pl.getFloat("specular.blend", diffBlend), 0, 1); - glossyness = MathUtils.clamp(pl.getFloat("glossyness", glossyness), 0, 1); - numSamples = pl.getInt("samples", numSamples); - return true; - } - - public Color getDiffuse(ShadingState state) { - return diffmap == null ? diff : Color.blend(diff, diffmap.getPixel(state.getUV().x, state.getUV().y), diffBlend); - } - - public Color getSpecular(ShadingState state) { - return specmap == null ? spec : Color.blend(spec, specmap.getPixel(state.getUV().x, state.getUV().y), specBlend); - } - - public Color getRadiance(ShadingState state) { - // make sure we are on the right side of the material - state.faceforward(); - // direct lighting - state.initLightSamples(); - state.initCausticSamples(); - Color d = getDiffuse(state); - Color lr = state.diffuse(d); - if (!state.includeSpecular()) - return lr; - if (glossyness == 0) { - float cos = state.getCosND(); - float dn = 2 * cos; - Vector3 refDir = new Vector3(); - refDir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; - refDir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; - refDir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; - Ray refRay = new Ray(state.getPoint(), refDir); - // compute Fresnel term - cos = 1 - cos; - float cos2 = cos * cos; - float cos5 = cos2 * cos2 * cos; - Color spec = getSpecular(state); - Color ret = Color.white(); - ret.sub(spec); - ret.mul(cos5); - ret.add(spec); - return lr.add(ret.mul(state.traceReflection(refRay, 0))); - } else - return lr.add(state.specularPhong(getSpecular(state), 2 / glossyness, numSamples)); - } - - public void scatterPhoton(ShadingState state, Color power) { - Color diffuse, specular; - // make sure we are on the right side of the material - state.faceforward(); - diffuse = getDiffuse(state); - specular = getSpecular(state); - state.storePhoton(state.getRay().getDirection(), power, diffuse); - float d = diffuse.getAverage(); - float r = specular.getAverage(); - double rnd = state.getRandom(0, 0, 1); - if (rnd < d) { - // photon is scattered - power.mul(diffuse).mul(1.0f / d); - OrthoNormalBasis onb = state.getBasis(); - double u = 2 * Math.PI * rnd / d; - double v = state.getRandom(0, 1, 1); - float s = (float) Math.sqrt(v); - float s1 = (float) Math.sqrt(1.0 - v); - Vector3 w = new Vector3((float) Math.cos(u) * s, (float) Math.sin(u) * s, s1); - w = onb.transform(w, new Vector3()); - state.traceDiffusePhoton(new Ray(state.getPoint(), w), power); - } else if (rnd < d + r) { - if (glossyness == 0) { - float cos = -Vector3.dot(state.getNormal(), state.getRay().getDirection()); - power.mul(diffuse).mul(1.0f / d); - // photon is reflected - float dn = 2 * cos; - Vector3 dir = new Vector3(); - dir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; - dir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; - dir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; - state.traceReflectionPhoton(new Ray(state.getPoint(), dir), power); - } else { - float dn = 2.0f * state.getCosND(); - // reflected direction - Vector3 refDir = new Vector3(); - refDir.x = (dn * state.getNormal().x) + state.getRay().dx; - refDir.y = (dn * state.getNormal().y) + state.getRay().dy; - refDir.z = (dn * state.getNormal().z) + state.getRay().dz; - power.mul(spec).mul(1.0f / r); - OrthoNormalBasis onb = state.getBasis(); - double u = 2 * Math.PI * (rnd - r) / r; - double v = state.getRandom(0, 1, 1); - float s = (float) Math.pow(v, 1 / ((1.0f / glossyness) + 1)); - float s1 = (float) Math.sqrt(1 - s * s); - Vector3 w = new Vector3((float) Math.cos(u) * s1, (float) Math.sin(u) * s1, s); - w = onb.transform(w, new Vector3()); - state.traceReflectionPhoton(new Ray(state.getPoint(), w), power); - } - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/ViewCausticsShader.java b/src/org/sunflow/core/shader/ViewCausticsShader.java deleted file mode 100644 index a583623..0000000 --- a/src/org/sunflow/core/shader/ViewCausticsShader.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.LightSample; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; - -public class ViewCausticsShader implements Shader { - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public Color getRadiance(ShadingState state) { - state.faceforward(); - state.initCausticSamples(); - // integrate a diffuse function - Color lr = Color.black(); - for (LightSample sample : state) - lr.madd(sample.dot(state.getNormal()), sample.getDiffuseRadiance()); - return lr.mul(1.0f / (float) Math.PI); - - } - - public void scatterPhoton(ShadingState state, Color power) { - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/ViewGlobalPhotonsShader.java b/src/org/sunflow/core/shader/ViewGlobalPhotonsShader.java deleted file mode 100644 index badf0a2..0000000 --- a/src/org/sunflow/core/shader/ViewGlobalPhotonsShader.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; - -public class ViewGlobalPhotonsShader implements Shader { - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public Color getRadiance(ShadingState state) { - state.faceforward(); - return state.getGlobalRadiance(); - } - - public void scatterPhoton(ShadingState state, Color power) { - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/ViewIrradianceShader.java b/src/org/sunflow/core/shader/ViewIrradianceShader.java deleted file mode 100644 index 7fc70d8..0000000 --- a/src/org/sunflow/core/shader/ViewIrradianceShader.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; - -public class ViewIrradianceShader implements Shader { - public boolean update(ParameterList pl, SunflowAPI api) { - return true; - } - - public Color getRadiance(ShadingState state) { - state.faceforward(); - return new Color().set(state.getIrradiance(Color.WHITE)).mul(1.0f / (float) Math.PI); - } - - public void scatterPhoton(ShadingState state, Color power) { - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/shader/WireframeShader.java b/src/org/sunflow/core/shader/WireframeShader.java deleted file mode 100644 index 6dbf1f1..0000000 --- a/src/org/sunflow/core/shader/WireframeShader.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.sunflow.core.shader; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.Shader; -import org.sunflow.core.ShadingState; -import org.sunflow.image.Color; -import org.sunflow.math.Matrix4; -import org.sunflow.math.Point3; - -public class WireframeShader implements Shader { - private Color lineColor; - private Color fillColor; - private float width; - private float cosWidth; - - public WireframeShader() { - lineColor = Color.BLACK; - fillColor = Color.WHITE; - // pick a very small angle - should be roughly the half the angular - // width of a - // pixel - width = (float) (Math.PI * 0.5 / 4096); - cosWidth = (float) Math.cos(width); - } - - public boolean update(ParameterList pl, SunflowAPI api) { - lineColor = pl.getColor("line", lineColor); - fillColor = pl.getColor("fill", fillColor); - width = pl.getFloat("width", width); - cosWidth = (float) Math.cos(width); - return true; - } - - public Color getFillColor(ShadingState state) { - return fillColor; - } - - public Color getLineColor(ShadingState state) { - return lineColor; - } - - public Color getRadiance(ShadingState state) { - Point3[] p = new Point3[3]; - if (!state.getTrianglePoints(p)) - return getFillColor(state); - // transform points into camera space - Point3 center = state.getPoint(); - Matrix4 w2c = state.getWorldToCamera(); - center = w2c.transformP(center); - for (int i = 0; i < 3; i++) - p[i] = w2c.transformP(state.transformObjectToWorld(p[i])); - float cn = 1.0f / (float) Math.sqrt(center.x * center.x + center.y * center.y + center.z * center.z); - for (int i = 0, i2 = 2; i < 3; i2 = i, i++) { - // compute orthogonal projection of the shading point onto each - // triangle edge as in: - // http://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html - float t = (center.x - p[i].x) * (p[i2].x - p[i].x); - t += (center.y - p[i].y) * (p[i2].y - p[i].y); - t += (center.z - p[i].z) * (p[i2].z - p[i].z); - t /= p[i].distanceToSquared(p[i2]); - float projx = (1 - t) * p[i].x + t * p[i2].x; - float projy = (1 - t) * p[i].y + t * p[i2].y; - float projz = (1 - t) * p[i].z + t * p[i2].z; - float n = 1.0f / (float) Math.sqrt(projx * projx + projy * projy + projz * projz); - // check angular width - float dot = projx * center.x + projy * center.y + projz * center.z; - if (dot * n * cn >= cosWidth) - return getLineColor(state); - } - return getFillColor(state); - } - - public void scatterPhoton(ShadingState state, Color power) { - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/tesselatable/BezierMesh.java b/src/org/sunflow/core/tesselatable/BezierMesh.java deleted file mode 100644 index 06fd26b..0000000 --- a/src/org/sunflow/core/tesselatable/BezierMesh.java +++ /dev/null @@ -1,248 +0,0 @@ -package org.sunflow.core.tesselatable; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Tesselatable; -import org.sunflow.core.ParameterList.FloatParameter; -import org.sunflow.core.ParameterList.InterpolationType; -import org.sunflow.core.primitive.QuadMesh; -import org.sunflow.core.primitive.TriangleMesh; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class BezierMesh implements Tesselatable { - private int subdivs; - private boolean smooth; - private boolean quads; - private float[][] patches; - - public BezierMesh() { - this(null); - } - - public BezierMesh(float[][] patches) { - subdivs = 8; - smooth = true; - quads = false; - // convert to single precision - this.patches = patches; - } - - public BoundingBox getWorldBounds(Matrix4 o2w) { - BoundingBox bounds = new BoundingBox(); - if (o2w == null) { - for (int i = 0; i < patches.length; i++) { - float[] patch = patches[i]; - for (int j = 0; j < patch.length; j += 3) - bounds.include(patch[j], patch[j + 1], patch[j + 2]); - } - } else { - // transform vertices first - for (int i = 0; i < patches.length; i++) { - float[] patch = patches[i]; - for (int j = 0; j < patch.length; j += 3) { - float x = patch[j]; - float y = patch[j + 1]; - float z = patch[j + 2]; - float wx = o2w.transformPX(x, y, z); - float wy = o2w.transformPY(x, y, z); - float wz = o2w.transformPZ(x, y, z); - bounds.include(wx, wy, wz); - } - } - } - return bounds; - } - - private float[] bernstein(float u) { - float[] b = new float[4]; - float i = 1 - u; - b[0] = i * i * i; - b[1] = 3 * u * i * i; - b[2] = 3 * u * u * i; - b[3] = u * u * u; - return b; - } - - private float[] bernsteinDeriv(float u) { - if (!smooth) - return null; - float[] b = new float[4]; - float i = 1 - u; - b[0] = 3 * (0 - i * i); - b[1] = 3 * (i * i - 2 * u * i); - b[2] = 3 * (2 * u * i - u * u); - b[3] = 3 * (u * u - 0); - return b; - } - - private void getPatchPoint(float u, float v, float[] ctrl, float[] bu, float[] bv, float[] bdu, float[] bdv, Point3 p, Vector3 n) { - float px = 0; - float py = 0; - float pz = 0; - for (int i = 0, index = 0; i < 4; i++) { - for (int j = 0; j < 4; j++, index += 3) { - float scale = bu[j] * bv[i]; - px += ctrl[index + 0] * scale; - py += ctrl[index + 1] * scale; - pz += ctrl[index + 2] * scale; - } - } - p.x = px; - p.y = py; - p.z = pz; - if (n != null) { - float dpdux = 0; - float dpduy = 0; - float dpduz = 0; - float dpdvx = 0; - float dpdvy = 0; - float dpdvz = 0; - for (int i = 0, index = 0; i < 4; i++) { - for (int j = 0; j < 4; j++, index += 3) { - float scaleu = bdu[j] * bv[i]; - dpdux += ctrl[index + 0] * scaleu; - dpduy += ctrl[index + 1] * scaleu; - dpduz += ctrl[index + 2] * scaleu; - float scalev = bu[j] * bdv[i]; - dpdvx += ctrl[index + 0] * scalev; - dpdvy += ctrl[index + 1] * scalev; - dpdvz += ctrl[index + 2] * scalev; - } - } - // surface normal - n.x = (dpduy * dpdvz - dpduz * dpdvy); - n.y = (dpduz * dpdvx - dpdux * dpdvz); - n.z = (dpdux * dpdvy - dpduy * dpdvx); - } - } - - public PrimitiveList tesselate() { - float[] vertices = new float[patches.length * (subdivs + 1) * (subdivs + 1) * 3]; - float[] normals = smooth ? new float[patches.length * (subdivs + 1) * (subdivs + 1) * 3] : null; - float[] uvs = new float[patches.length * (subdivs + 1) * (subdivs + 1) * 2]; - int[] indices = new int[patches.length * subdivs * subdivs * (quads ? 4 : (2 * 3))]; - - int vidx = 0, pidx = 0; - float step = 1.0f / subdivs; - int vstride = subdivs + 1; - Point3 p = new Point3(); - Vector3 n = smooth ? new Vector3() : null; - for (float[] patch : patches) { - // create patch vertices - for (int i = 0, voff = 0; i <= subdivs; i++) { - float u = i * step; - float[] bu = bernstein(u); - float[] bdu = bernsteinDeriv(u); - for (int j = 0; j <= subdivs; j++, voff += 3) { - float v = j * step; - float[] bv = bernstein(v); - float[] bdv = bernsteinDeriv(v); - getPatchPoint(u, v, patch, bu, bv, bdu, bdv, p, n); - vertices[vidx + voff + 0] = p.x; - vertices[vidx + voff + 1] = p.y; - vertices[vidx + voff + 2] = p.z; - if (smooth) { - normals[vidx + voff + 0] = n.x; - normals[vidx + voff + 1] = n.y; - normals[vidx + voff + 2] = n.z; - } - uvs[(vidx + voff) / 3 * 2 + 0] = u; - uvs[(vidx + voff) / 3 * 2 + 1] = v; - } - } - // generate patch triangles - for (int i = 0, vbase = vidx / 3; i < subdivs; i++) { - for (int j = 0; j < subdivs; j++) { - int v00 = (i + 0) * vstride + (j + 0); - int v10 = (i + 1) * vstride + (j + 0); - int v01 = (i + 0) * vstride + (j + 1); - int v11 = (i + 1) * vstride + (j + 1); - if (quads) { - indices[pidx + 0] = vbase + v01; - indices[pidx + 1] = vbase + v00; - indices[pidx + 2] = vbase + v10; - indices[pidx + 3] = vbase + v11; - pidx += 4; - } else { - // add 2 triangles - indices[pidx + 0] = vbase + v00; - indices[pidx + 1] = vbase + v10; - indices[pidx + 2] = vbase + v01; - indices[pidx + 3] = vbase + v10; - indices[pidx + 4] = vbase + v11; - indices[pidx + 5] = vbase + v01; - pidx += 6; - } - } - } - vidx += vstride * vstride * 3; - } - ParameterList pl = new ParameterList(); - pl.addPoints("points", InterpolationType.VERTEX, vertices); - if (quads) - pl.addIntegerArray("quads", indices); - else - pl.addIntegerArray("triangles", indices); - pl.addTexCoords("uvs", InterpolationType.VERTEX, uvs); - if (smooth) - pl.addVectors("normals", InterpolationType.VERTEX, normals); - PrimitiveList m = quads ? new QuadMesh() : new TriangleMesh(); - m.update(pl, null); - pl.clear(true); - return m; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - subdivs = pl.getInt("subdivs", subdivs); - smooth = pl.getBoolean("smooth", smooth); - quads = pl.getBoolean("quads", quads); - int nu = pl.getInt("nu", 0); - int nv = pl.getInt("nv", 0); - pl.setVertexCount(nu * nv); - boolean uwrap = pl.getBoolean("uwrap", false); - boolean vwrap = pl.getBoolean("vwrap", false); - FloatParameter points = pl.getPointArray("points"); - if (points != null && points.interp == InterpolationType.VERTEX) { - int numUPatches = uwrap ? nu / 3 : (nu - 4) / 3 + 1; - int numVPatches = vwrap ? nv / 3 : (nv - 4) / 3 + 1; - if (numUPatches < 1 || numVPatches < 1) { - UI.printError(Module.GEOM, "Invalid number of patches for bezier mesh - ignoring"); - return false; - } - // generate patches - patches = new float[numUPatches * numVPatches][]; - for (int v = 0, p = 0; v < numVPatches; v++) { - for (int u = 0; u < numUPatches; u++, p++) { - float[] patch = patches[p] = new float[16 * 3]; - int up = u * 3; - int vp = v * 3; - for (int pv = 0; pv < 4; pv++) { - for (int pu = 0; pu < 4; pu++) { - int meshU = (up + pu) % nu; - int meshV = (vp + pv) % nv; - // copy point - patch[3 * (pv * 4 + pu) + 0] = points.data[3 * (meshU + nu * meshV) + 0]; - patch[3 * (pv * 4 + pu) + 1] = points.data[3 * (meshU + nu * meshV) + 1]; - patch[3 * (pv * 4 + pu) + 2] = points.data[3 * (meshU + nu * meshV) + 2]; - } - } - } - } - } - if (subdivs < 1) { - UI.printError(Module.GEOM, "Invalid subdivisions for bezier mesh - ignoring"); - return false; - } - if (patches == null) { - UI.printError(Module.GEOM, "No patch data present in bezier mesh - ignoring"); - return false; - } - return true; - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/tesselatable/FileMesh.java b/src/org/sunflow/core/tesselatable/FileMesh.java deleted file mode 100644 index a33883d..0000000 --- a/src/org/sunflow/core/tesselatable/FileMesh.java +++ /dev/null @@ -1,237 +0,0 @@ -package org.sunflow.core.tesselatable; - -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.DataInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.nio.ByteOrder; -import java.nio.FloatBuffer; -import java.nio.IntBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; - -import org.sunflow.SunflowAPI; -import org.sunflow.core.ParameterList; -import org.sunflow.core.PrimitiveList; -import org.sunflow.core.Tesselatable; -import org.sunflow.core.ParameterList.InterpolationType; -import org.sunflow.core.primitive.TriangleMesh; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.Memory; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; -import org.sunflow.util.FloatArray; -import org.sunflow.util.IntArray; - -public class FileMesh implements Tesselatable { - private String filename = null; - private boolean smoothNormals = false; - - public BoundingBox getWorldBounds(Matrix4 o2w) { - // world bounds can't be computed without reading file - // return null so the mesh will be loaded right away - return null; - } - - public PrimitiveList tesselate() { - if (filename.endsWith(".ra3")) { - try { - UI.printInfo(Module.GEOM, "RA3 - Reading geometry: \"%s\" ...", filename); - File file = new File(filename); - FileInputStream stream = new FileInputStream(filename); - MappedByteBuffer map = stream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()); - map.order(ByteOrder.LITTLE_ENDIAN); - IntBuffer ints = map.asIntBuffer(); - FloatBuffer buffer = map.asFloatBuffer(); - int numVerts = ints.get(0); - int numTris = ints.get(1); - UI.printInfo(Module.GEOM, "RA3 - * Reading %d vertices ...", numVerts); - float[] verts = new float[3 * numVerts]; - for (int i = 0; i < verts.length; i++) - verts[i] = buffer.get(2 + i); - UI.printInfo(Module.GEOM, "RA3 - * Reading %d triangles ...", numTris); - int[] tris = new int[3 * numTris]; - for (int i = 0; i < tris.length; i++) - tris[i] = ints.get(2 + verts.length + i); - stream.close(); - UI.printInfo(Module.GEOM, "RA3 - * Creating mesh ..."); - return generate(tris, verts, smoothNormals); - } catch (FileNotFoundException e) { - e.printStackTrace(); - UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - file not found", filename); - } catch (IOException e) { - e.printStackTrace(); - UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - I/O error occured", filename); - } - } else if (filename.endsWith(".obj")) { - int lineNumber = 1; - try { - UI.printInfo(Module.GEOM, "OBJ - Reading geometry: \"%s\" ...", filename); - FloatArray verts = new FloatArray(); - IntArray tris = new IntArray(); - FileReader file = new FileReader(filename); - BufferedReader bf = new BufferedReader(file); - String line; - while ((line = bf.readLine()) != null) { - if (line.startsWith("v")) { - String[] v = line.split("\\s+"); - verts.add(Float.parseFloat(v[1])); - verts.add(Float.parseFloat(v[2])); - verts.add(Float.parseFloat(v[3])); - } else if (line.startsWith("f")) { - String[] f = line.split("\\s+"); - if (f.length == 5) { - tris.add(Integer.parseInt(f[1]) - 1); - tris.add(Integer.parseInt(f[2]) - 1); - tris.add(Integer.parseInt(f[3]) - 1); - tris.add(Integer.parseInt(f[1]) - 1); - tris.add(Integer.parseInt(f[3]) - 1); - tris.add(Integer.parseInt(f[4]) - 1); - } else if (f.length == 4) { - tris.add(Integer.parseInt(f[1]) - 1); - tris.add(Integer.parseInt(f[2]) - 1); - tris.add(Integer.parseInt(f[3]) - 1); - } - } - if (lineNumber % 100000 == 0) - UI.printInfo(Module.GEOM, "OBJ - * Parsed %7d lines ...", lineNumber); - lineNumber++; - } - file.close(); - UI.printInfo(Module.GEOM, "OBJ - * Creating mesh ..."); - return generate(tris.trim(), verts.trim(), smoothNormals); - } catch (FileNotFoundException e) { - e.printStackTrace(); - UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - file not found", filename); - } catch (NumberFormatException e) { - e.printStackTrace(); - UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - syntax error at line %d", lineNumber); - } catch (IOException e) { - e.printStackTrace(); - UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - I/O error occured", filename); - } - } else if (filename.endsWith(".stl")) { - try { - UI.printInfo(Module.GEOM, "STL - Reading geometry: \"%s\" ...", filename); - FileInputStream file = new FileInputStream(filename); - DataInputStream stream = new DataInputStream(new BufferedInputStream(file)); - file.skip(80); - int numTris = getLittleEndianInt(stream.readInt()); - UI.printInfo(Module.GEOM, "STL - * Reading %d triangles ...", numTris); - long filesize = new File(filename).length(); - if (filesize != (84 + 50 * numTris)) { - UI.printWarning(Module.GEOM, "STL - Size of file mismatch (expecting %s, found %s)", Memory.bytesToString(84 + 14 * numTris), Memory.bytesToString(filesize)); - return null; - } - int[] tris = new int[3 * numTris]; - float[] verts = new float[9 * numTris]; - for (int i = 0, i3 = 0, index = 0; i < numTris; i++, i3 += 3) { - // skip normal - stream.readInt(); - stream.readInt(); - stream.readInt(); - for (int j = 0; j < 3; j++, index += 3) { - tris[i3 + j] = i3 + j; - // get xyz - verts[index + 0] = getLittleEndianFloat(stream.readInt()); - verts[index + 1] = getLittleEndianFloat(stream.readInt()); - verts[index + 2] = getLittleEndianFloat(stream.readInt()); - } - stream.readShort(); - if ((i + 1) % 100000 == 0) - UI.printInfo(Module.GEOM, "STL - * Parsed %7d triangles ...", i + 1); - } - file.close(); - // create geometry - UI.printInfo(Module.GEOM, "STL - * Creating mesh ..."); - if (smoothNormals) - UI.printWarning(Module.GEOM, "STL - format does not support shared vertices - normal smoothing disabled"); - return generate(tris, verts, false); - } catch (FileNotFoundException e) { - e.printStackTrace(); - UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - file not found", filename); - } catch (IOException e) { - e.printStackTrace(); - UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - I/O error occured", filename); - } - } else - UI.printWarning(Module.GEOM, "Unable to read mesh file \"%s\" - unrecognized format", filename); - return null; - } - - private TriangleMesh generate(int[] tris, float[] verts, boolean smoothNormals) { - ParameterList pl = new ParameterList(); - pl.addIntegerArray("triangles", tris); - pl.addPoints("points", InterpolationType.VERTEX, verts); - if (smoothNormals) { - float[] normals = new float[verts.length]; // filled with 0's - Point3 p0 = new Point3(); - Point3 p1 = new Point3(); - Point3 p2 = new Point3(); - Vector3 n = new Vector3(); - for (int i3 = 0; i3 < tris.length; i3 += 3) { - int v0 = tris[i3 + 0]; - int v1 = tris[i3 + 1]; - int v2 = tris[i3 + 2]; - p0.set(verts[3 * v0 + 0], verts[3 * v0 + 1], verts[3 * v0 + 2]); - p1.set(verts[3 * v1 + 0], verts[3 * v1 + 1], verts[3 * v1 + 2]); - p2.set(verts[3 * v2 + 0], verts[3 * v2 + 1], verts[3 * v2 + 2]); - Point3.normal(p0, p1, p2, n); // compute normal - // add face normal to each vertex - // note that these are not normalized so this in fact weights - // each normal by the area of the triangle - normals[3 * v0 + 0] += n.x; - normals[3 * v0 + 1] += n.y; - normals[3 * v0 + 2] += n.z; - normals[3 * v1 + 0] += n.x; - normals[3 * v1 + 1] += n.y; - normals[3 * v1 + 2] += n.z; - normals[3 * v2 + 0] += n.x; - normals[3 * v2 + 1] += n.y; - normals[3 * v2 + 2] += n.z; - } - // normalize all the vectors - for (int i3 = 0; i3 < normals.length; i3 += 3) { - n.set(normals[i3 + 0], normals[i3 + 1], normals[i3 + 2]); - n.normalize(); - normals[i3 + 0] = n.x; - normals[i3 + 1] = n.y; - normals[i3 + 2] = n.z; - } - pl.addVectors("normals", InterpolationType.VERTEX, normals); - } - TriangleMesh m = new TriangleMesh(); - if (m.update(pl, null)) - return m; - // something failed in creating the mesh, the error message will be - // printed by the mesh itself - no need to repeat it here - return null; - } - - public boolean update(ParameterList pl, SunflowAPI api) { - String file = pl.getString("filename", null); - if (file != null) - filename = api.resolveIncludeFilename(file); - smoothNormals = pl.getBoolean("smooth_normals", smoothNormals); - return filename != null; - } - - private int getLittleEndianInt(int i) { - // input integer has its bytes in big endian byte order - // swap them around - return (i >>> 24) | ((i >>> 8) & 0xFF00) | ((i << 8) & 0xFF0000) | (i << 24); - } - - private float getLittleEndianFloat(int i) { - // input integer has its bytes in big endian byte order - // swap them around and interpret data as floating point - return Float.intBitsToFloat(getLittleEndianInt(i)); - } -} \ No newline at end of file diff --git a/src/org/sunflow/core/tesselatable/Gumbo.java b/src/org/sunflow/core/tesselatable/Gumbo.java deleted file mode 100644 index 59f7c28..0000000 --- a/src/org/sunflow/core/tesselatable/Gumbo.java +++ /dev/null @@ -1,1144 +0,0 @@ -package org.sunflow.core.tesselatable; - -import java.io.FileNotFoundException; -import java.io.IOException; - -import org.sunflow.math.Matrix4; -import org.sunflow.system.Parser; -import org.sunflow.system.Parser.ParserException; -import org.sunflow.util.FloatArray; - -public class Gumbo extends BezierMesh { - // generate raw patch data from source rib file - public static void main(String[] args) { - try { - Parser p; - p = new Parser("gumbo.rib"); - int begins = 1; - System.out.println("{"); - Matrix4 m = Matrix4.IDENTITY; - p.checkNextToken("AttributeBegin"); - while (begins != 0) { - if (p.peekNextToken("Patch")) { - p.checkNextToken("bicubic"); - p.checkNextToken("P"); - float[] patch = parseFloatArray(p); - if (patch.length == 48) { - // transform patch - for (int i = 0; i < 16; i++) { - float x = patch[3 * i + 0]; - float y = patch[3 * i + 1]; - float z = patch[3 * i + 2]; - patch[3 * i + 0] = m.transformPX(x, y, z); - patch[3 * i + 1] = m.transformPY(x, y, z); - patch[3 * i + 2] = m.transformPZ(x, y, z); - } - System.out.println("{"); - for (float v : patch) - System.out.printf(" %g,\n", v); - System.out.println("},"); - } - } else if (p.peekNextToken("Translate")) { - Matrix4 t = Matrix4.translation(p.getNextFloat(), p.getNextFloat(), p.getNextFloat()); - m = m.multiply(t); - } else if (p.peekNextToken("Rotate")) { - float angle = (float) Math.toRadians(p.getNextFloat()); - Matrix4 t = Matrix4.rotate(p.getNextFloat(), p.getNextFloat(), p.getNextFloat(), angle); - m = m.multiply(t); - } else if (p.peekNextToken("Scale")) { - Matrix4 t = Matrix4.scale(p.getNextFloat(), p.getNextFloat(), p.getNextFloat()); - m = m.multiply(t); - } else if (p.peekNextToken("TransformEnd")) { - m = Matrix4.IDENTITY; - } else if (p.peekNextToken("AttributeBegin")) { - begins++; - } else if (p.peekNextToken("AttributeEnd")) { - begins--; - } else - p.getNextToken(); - } - System.out.println("};"); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } catch (ParserException e) { - e.printStackTrace(); - } - } - - private static float[] parseFloatArray(Parser p) throws IOException { - FloatArray array = new FloatArray(); - boolean done = false; - do { - String s = p.getNextToken(); - if (s.startsWith("[")) - s = s.substring(1); - if (s.endsWith("]")) { - s = s.substring(0, s.length() - 1); - done = true; - } - array.add(Float.parseFloat(s)); - } while (!done); - return array.trim(); - } - - // copy and paste data here - private static final float[][] PATCHES = { - { 10.0000f, 2.00000f, 0.00000f, 10.0000f, 2.00000f, 0.00000f, - 14.0000f, 2.00000f, 0.00000f, 14.0000f, 2.00000f, 0.00000f, - 10.0000f, 2.00000f, 0.00000f, 10.2277f, 2.22776f, - -0.911042f, 13.7722f, 2.22776f, -0.911042f, 14.0000f, - 2.00000f, 0.00000f, 10.0000f, 6.00000f, 0.00000f, 10.2277f, - 5.77223f, -0.911041f, 13.7722f, 5.77224f, -0.911041f, - 14.0000f, 6.00000f, 0.00000f, 10.0000f, 6.00000f, 0.00000f, - 10.0000f, 6.00000f, 0.00000f, 14.0000f, 6.00000f, 0.00000f, - 14.0000f, 6.00000f, 0.00000f, }, - { 10.0000f, 2.00000f, 0.00000f, 10.0000f, 2.00000f, 0.00000f, - 10.2483f, 2.08468f, 5.33563f, 10.0000f, 2.00000f, 8.00000f, - 10.0000f, 2.00000f, 0.00000f, 10.5077f, 0.457924f, - 2.06861f, 11.2875f, 1.04546f, 5.33563f, 11.0392f, - 0.960774f, 8.00000f, 14.0000f, 2.00000f, 0.00000f, - 13.4933f, 0.456761f, 2.07326f, 12.2875f, 2.17941f, - 5.23824f, 12.5766f, 1.37605f, 7.76601f, 14.0000f, 2.00000f, - 0.00000f, 14.0000f, 2.00000f, 0.00000f, 13.5399f, 2.92014f, - 4.93284f, 14.0000f, 2.00000f, 8.00000f, }, - { 14.0000f, 2.00000f, 0.00000f, 14.0000f, 2.00000f, 0.00000f, - 13.5399f, 2.92014f, 4.93284f, 14.0000f, 2.00000f, 8.00000f, - 14.0000f, 2.00000f, 0.00000f, 15.5432f, 2.50660f, 2.07326f, - 14.6921f, 3.60159f, 4.65188f, 15.8610f, 3.45395f, 6.60420f, - 14.0000f, 6.00000f, 0.00000f, 15.5425f, 5.49273f, 2.07061f, - 13.5521f, 3.61407f, 5.16626f, 15.0332f, 3.93355f, 6.96677f, - 14.0000f, 6.00000f, 0.00000f, 14.0000f, 6.00000f, 0.00000f, - 12.5859f, 4.58590f, 5.17181f, 14.0000f, 6.00000f, 8.00000f, }, - { 14.0000f, 6.00000f, 0.00000f, 14.0000f, 6.00000f, 0.00000f, - 12.5859f, 4.58590f, 5.17181f, 14.0000f, 6.00000f, 8.00000f, - 14.0000f, 6.00000f, 0.00000f, 13.4927f, 7.54257f, 2.07061f, - 11.6360f, 5.54118f, 5.17727f, 12.2594f, 6.87025f, 7.12974f, - 10.0000f, 6.00000f, 0.00000f, 10.5090f, 7.54076f, 2.06338f, - 11.5055f, 6.72455f, 4.92427f, 11.3818f, 7.59478f, 7.30228f, - 10.0000f, 6.00000f, 0.00000f, 10.0000f, 6.00000f, 0.00000f, - 10.6165f, 5.55161f, 5.03744f, 10.0000f, 6.00000f, 8.00000f, }, - { 10.0000f, 6.00000f, 0.00000f, 10.0000f, 6.00000f, 0.00000f, - 10.6165f, 5.55161f, 5.03744f, 10.0000f, 6.00000f, 8.00000f, - 10.0000f, 6.00000f, 0.00000f, 8.45923f, 5.49092f, 2.06338f, - 9.71866f, 4.36704f, 5.15174f, 8.96952f, 4.43964f, 7.67212f, - 10.0000f, 2.00000f, 0.00000f, 8.45792f, 2.50777f, 2.06861f, - 9.18040f, 3.15264f, 5.33563f, 8.93204f, 3.06795f, 8.00000f, - 10.0000f, 2.00000f, 0.00000f, 10.0000f, 2.00000f, 0.00000f, - 10.2483f, 2.08468f, 5.33563f, 10.0000f, 2.00000f, 8.00000f, }, - { 18.0000f, 2.00000f, 0.00000f, 18.0000f, 2.00000f, 0.00000f, - 22.0000f, 2.00000f, 0.00000f, 22.0000f, 2.00000f, 0.00000f, - 18.0000f, 2.00000f, 0.00000f, 18.2277f, 2.22776f, - -0.911042f, 21.7722f, 2.22776f, -0.911042f, 22.0000f, - 2.00000f, 0.00000f, 18.0000f, 6.00000f, 0.00000f, 18.2277f, - 5.77224f, -0.911042f, 21.7722f, 5.77223f, -0.911044f, - 22.0000f, 6.00000f, 0.00000f, 18.0000f, 6.00000f, 0.00000f, - 18.0000f, 6.00000f, 0.00000f, 22.0000f, 6.00000f, 0.00000f, - 22.0000f, 6.00000f, 0.00000f, }, - { 18.0000f, 2.00000f, 0.00000f, 18.0000f, 2.00000f, 0.00000f, - 18.6124f, 3.22488f, 4.93779f, 18.0000f, 2.00000f, 8.00000f, - 18.0000f, 2.00000f, 0.00000f, 18.5064f, 0.456647f, - 2.07371f, 19.8347f, 2.44159f, 5.28208f, 19.5248f, 1.39004f, - 7.69502f, 22.0000f, 2.00000f, 0.00000f, 21.4928f, - 0.457306f, 2.07108f, 21.0113f, 1.23344f, 5.33483f, - 20.9582f, 0.958220f, 8.00000f, 22.0000f, 2.00000f, - 0.00000f, 22.0000f, 2.00000f, 0.00000f, 22.0531f, 2.27522f, - 5.33483f, 22.0000f, 2.00000f, 8.00000f, }, - { 22.0000f, 2.00000f, 0.00000f, 22.0000f, 2.00000f, 0.00000f, - 22.0531f, 2.27522f, 5.33483f, 22.0000f, 2.00000f, 8.00000f, - 22.0000f, 2.00000f, 0.00000f, 23.5426f, 2.50715f, 2.07108f, - 23.1026f, 3.32477f, 5.33483f, 23.0495f, 3.04954f, 8.00000f, - 22.0000f, 6.00000f, 0.00000f, 23.5427f, 5.49294f, 2.07146f, - 22.2045f, 4.41977f, 5.20618f, 22.8434f, 4.43176f, 7.78914f, - 22.0000f, 6.00000f, 0.00000f, 22.0000f, 6.00000f, 0.00000f, - 21.4391f, 5.67950f, 5.02396f, 22.0000f, 6.00000f, 8.00000f, }, - { 22.0000f, 6.00000f, 0.00000f, 22.0000f, 6.00000f, 0.00000f, - 21.4391f, 5.67950f, 5.02396f, 22.0000f, 6.00000f, 8.00000f, - 22.0000f, 6.00000f, 0.00000f, 21.4929f, 7.54278f, 2.07146f, - 20.6640f, 6.95514f, 4.83944f, 20.8048f, 7.75546f, 7.23198f, - 18.0000f, 6.00000f, 0.00000f, 18.5072f, 7.54257f, 2.07061f, - 20.3933f, 5.55591f, 5.16253f, 19.7699f, 6.88498f, 7.11501f, - 18.0000f, 6.00000f, 0.00000f, 18.0000f, 6.00000f, 0.00000f, - 19.4157f, 4.58430f, 5.16861f, 18.0000f, 6.00000f, 8.00000f, }, - { 18.0000f, 6.00000f, 0.00000f, 18.0000f, 6.00000f, 0.00000f, - 19.4157f, 4.58430f, 5.16861f, 18.0000f, 6.00000f, 8.00000f, - 18.0000f, 6.00000f, 0.00000f, 16.4574f, 5.49273f, 2.07061f, - 18.4561f, 3.63070f, 5.17458f, 16.9751f, 3.95019f, 6.97509f, - 18.0000f, 2.00000f, 0.00000f, 16.4566f, 2.50649f, 2.07371f, - 17.5879f, 3.88144f, 4.64919f, 16.4270f, 3.57293f, 6.42706f, - 18.0000f, 2.00000f, 0.00000f, 18.0000f, 2.00000f, 0.00000f, - 18.6124f, 3.22488f, 4.93779f, 18.0000f, 2.00000f, 8.00000f, }, - { 18.0000f, 8.00000f, 0.00000f, 18.0000f, 8.00000f, 0.00000f, - 22.0000f, 8.00000f, 0.00000f, 22.0000f, 8.00000f, 0.00000f, - 18.0000f, 8.00000f, 0.00000f, 18.2277f, 8.22776f, - -0.911043f, 21.7722f, 8.22775f, -0.911043f, 22.0000f, - 8.00000f, 0.00000f, 18.0000f, 12.0000f, 0.00000f, 18.2277f, - 11.7722f, -0.911043f, 21.7722f, 11.7722f, -0.911042f, - 22.0000f, 12.0000f, 0.00000f, 18.0000f, 12.0000f, 0.00000f, - 18.0000f, 12.0000f, 0.00000f, 22.0000f, 12.0000f, 0.00000f, - 22.0000f, 12.0000f, 0.00000f, }, - { 18.0000f, 8.00000f, 0.00000f, 18.0000f, 8.00000f, 0.00000f, - 19.4156f, 9.41569f, 5.16861f, 18.0000f, 8.00000f, 8.00000f, - 18.0000f, 8.00000f, 0.00000f, 18.5072f, 6.45742f, 2.07061f, - 20.3933f, 8.44408f, 5.16253f, 19.7699f, 7.11501f, 7.11501f, - 22.0000f, 8.00000f, 0.00000f, 21.4929f, 6.45721f, 2.07146f, - 20.6640f, 7.04485f, 4.83943f, 20.8048f, 6.24453f, 7.23198f, - 22.0000f, 8.00000f, 0.00000f, 22.0000f, 8.00000f, 0.00000f, - 21.4391f, 8.32049f, 5.02396f, 22.0000f, 8.00000f, 8.00000f, }, - { 22.0000f, 8.00000f, 0.00000f, 22.0000f, 8.00000f, 0.00000f, - 21.4391f, 8.32049f, 5.02396f, 22.0000f, 8.00000f, 8.00000f, - 22.0000f, 8.00000f, 0.00000f, 23.5427f, 8.50705f, 2.07146f, - 22.2045f, 9.58022f, 5.20618f, 22.8434f, 9.56823f, 7.78914f, - 22.0000f, 12.0000f, 0.00000f, 23.5426f, 11.4928f, 2.07108f, - 23.1026f, 10.6752f, 5.33483f, 23.0495f, 10.9504f, 8.00000f, - 22.0000f, 12.0000f, 0.00000f, 22.0000f, 12.0000f, 0.00000f, - 22.0531f, 11.7247f, 5.33483f, 22.0000f, 12.0000f, 8.00000f, }, - { 22.0000f, 12.0000f, 0.00000f, 22.0000f, 12.0000f, 0.00000f, - 22.0531f, 11.7247f, 5.33483f, 22.0000f, 12.0000f, 8.00000f, - 22.0000f, 12.0000f, 0.00000f, 21.4928f, 13.5426f, 2.07108f, - 21.0113f, 12.7665f, 5.33483f, 20.9582f, 13.0417f, 8.00000f, - 18.0000f, 12.0000f, 0.00000f, 18.5064f, 13.5433f, 2.07371f, - 19.8347f, 11.5584f, 5.28208f, 19.5257f, 12.6103f, 7.69484f, - 18.0000f, 12.0000f, 0.00000f, 18.0000f, 12.0000f, 0.00000f, - 18.6124f, 10.7751f, 4.93779f, 18.0000f, 12.0000f, 8.00000f, }, - { 18.0000f, 12.0000f, 0.00000f, 18.0000f, 12.0000f, 0.00000f, - 18.6124f, 10.7751f, 4.93779f, 18.0000f, 12.0000f, 8.00000f, - 18.0000f, 12.0000f, 0.00000f, 16.4566f, 11.4935f, 2.07371f, - 17.5879f, 10.1185f, 4.64919f, 16.4281f, 10.4281f, 6.42818f, - 18.0000f, 8.00000f, 0.00000f, 16.4574f, 8.50726f, 2.07061f, - 18.4561f, 10.3693f, 5.17458f, 16.9750f, 10.0498f, 6.97509f, - 18.0000f, 8.00000f, 0.00000f, 18.0000f, 8.00000f, 0.00000f, - 19.4156f, 9.41569f, 5.16861f, 18.0000f, 8.00000f, 8.00000f, }, - { 10.0000f, 8.00000f, 0.00000f, 10.0000f, 8.00000f, 0.00000f, - 14.0000f, 8.00000f, 0.00000f, 14.0000f, 8.00000f, 0.00000f, - 10.0000f, 8.00000f, 0.00000f, 10.2277f, 8.22776f, - -0.911042f, 13.7722f, 8.22775f, -0.911042f, 14.0000f, - 8.00000f, 0.00000f, 10.0000f, 12.0000f, 0.00000f, 10.2277f, - 11.7722f, -0.911041f, 13.7722f, 11.7722f, -0.911042f, - 14.0000f, 12.0000f, 0.00000f, 10.0000f, 12.0000f, 0.00000f, - 10.0000f, 12.0000f, 0.00000f, 14.0000f, 12.0000f, 0.00000f, - 14.0000f, 12.0000f, 0.00000f, }, - { 10.0000f, 8.00000f, 0.00000f, 10.0000f, 8.00000f, 0.00000f, - 10.6165f, 8.44838f, 5.03744f, 10.0000f, 8.00000f, 8.00000f, - 10.0000f, 8.00000f, 0.00000f, 10.5090f, 6.45923f, 2.06338f, - 11.5055f, 7.27544f, 4.92427f, 11.3818f, 6.40521f, 7.30228f, - 14.0000f, 8.00000f, 0.00000f, 13.4927f, 6.45742f, 2.07061f, - 11.6360f, 8.45881f, 5.17727f, 12.2594f, 7.12974f, 7.12974f, - 14.0000f, 8.00000f, 0.00000f, 14.0000f, 8.00000f, 0.00000f, - 12.5879f, 9.41203f, 5.17592f, 14.0000f, 8.00000f, 8.00000f, }, - { 14.0000f, 8.00000f, 0.00000f, 14.0000f, 8.00000f, 0.00000f, - 12.5879f, 9.41203f, 5.17592f, 14.0000f, 8.00000f, 8.00000f, - 14.0000f, 8.00000f, 0.00000f, 15.5425f, 8.50726f, 2.07061f, - 13.5438f, 10.3693f, 5.17458f, 15.0249f, 10.0498f, 6.97509f, - 14.0000f, 12.0000f, 0.00000f, 15.5433f, 11.4935f, 2.07371f, - 14.4120f, 10.1185f, 4.64919f, 15.5718f, 10.4281f, 6.42818f, - 14.0000f, 12.0000f, 0.00000f, 14.0000f, 12.0000f, 0.00000f, - 13.3876f, 10.7753f, 4.93833f, 14.0000f, 12.0000f, 8.00000f, }, - { 14.0000f, 12.0000f, 0.00000f, 14.0000f, 12.0000f, 0.00000f, - 13.3876f, 10.7753f, 4.93833f, 14.0000f, 12.0000f, 8.00000f, - 14.0000f, 12.0000f, 0.00000f, 13.4935f, 13.5433f, 2.07371f, - 12.1678f, 11.5573f, 5.28260f, 12.4764f, 12.6094f, 7.69529f, - 10.0000f, 12.0000f, 0.00000f, 10.5077f, 13.5420f, 2.06861f, - 11.2881f, 12.9550f, 5.33563f, 11.0397f, 13.0397f, 7.99999f, - 10.0000f, 12.0000f, 0.00000f, 10.0000f, 12.0000f, 0.00000f, - 10.2483f, 11.9153f, 5.33563f, 10.0000f, 12.0000f, 8.00000f, }, - { 10.0000f, 12.0000f, 0.00000f, 10.0000f, 12.0000f, 0.00000f, - 10.2483f, 11.9153f, 5.33563f, 10.0000f, 12.0000f, 8.00000f, - 10.0000f, 12.0000f, 0.00000f, 8.45792f, 11.4922f, 2.06861f, - 9.18040f, 10.8473f, 5.33563f, 8.93204f, 10.9320f, 8.00000f, - 10.0000f, 8.00000f, 0.00000f, 8.45923f, 8.50907f, 2.06338f, - 9.71866f, 9.63295f, 5.15174f, 8.96952f, 9.56035f, 7.67212f, - 10.0000f, 8.00000f, 0.00000f, 10.0000f, 8.00000f, 0.00000f, - 10.6165f, 8.44838f, 5.03744f, 10.0000f, 8.00000f, 8.00000f, }, - { 14.0000f, 2.00000f, 8.00000f, 15.9088f, 2.83677f, 8.31378f, - 16.2210f, 2.71156f, 8.35578f, 18.0000f, 2.00000f, 8.00000f, - 15.8610f, 3.45395f, 6.60420f, 16.3392f, 3.39355f, 7.40290f, - 15.8223f, 3.41220f, 7.35328f, 16.4270f, 3.57293f, 6.42706f, - 15.0332f, 3.93355f, 6.96677f, 15.8446f, 4.10859f, 7.95327f, - 16.1636f, 4.12523f, 7.96159f, 16.9751f, 3.95019f, 6.97509f, - 14.0000f, 6.00000f, 8.00000f, 15.1986f, 5.40068f, 8.59932f, - 16.8013f, 5.40068f, 8.59932f, 18.0000f, 6.00000f, 8.00000f, }, - { 14.0000f, 6.00000f, 8.00000f, 15.1986f, 5.40068f, 8.59932f, - 16.8013f, 5.40068f, 8.59932f, 18.0000f, 6.00000f, 8.00000f, - 13.7003f, 6.59931f, 8.29966f, 14.8989f, 5.99999f, 8.89898f, - 17.1010f, 6.00000f, 8.89898f, 18.2996f, 6.59932f, 8.29966f, - 13.7003f, 7.40068f, 8.29966f, 14.8989f, 8.00000f, 8.89898f, - 17.1010f, 8.00000f, 8.89897f, 18.2996f, 7.40068f, 8.29966f, - 14.0000f, 8.00000f, 8.00000f, 15.1986f, 8.59932f, 8.59932f, - 16.8013f, 8.59931f, 8.59931f, 18.0000f, 8.00000f, 8.00000f, }, - { 14.0000f, 8.00000f, 8.00000f, 15.1986f, 8.59932f, 8.59932f, - 16.8013f, 8.59931f, 8.59931f, 18.0000f, 8.00000f, 8.00000f, - 15.0249f, 10.0498f, 6.97509f, 15.8363f, 9.87476f, 7.96159f, - 16.1636f, 9.87476f, 7.96159f, 16.9750f, 10.0498f, 6.97509f, - 15.5718f, 10.4281f, 6.42818f, 16.1744f, 10.5890f, 7.35264f, - 15.8255f, 10.5890f, 7.35264f, 16.4281f, 10.4281f, 6.42818f, - 14.0000f, 12.0000f, 8.00000f, 15.7780f, 11.2887f, 8.35561f, - 16.2225f, 11.2890f, 8.35548f, 18.0000f, 12.0000f, 8.00000f, }, - { 10.0000f, 6.00000f, 8.00000f, 11.3818f, 7.59478f, 7.30228f, - 12.2594f, 6.87025f, 7.12974f, 14.0000f, 6.00000f, 8.00000f, - 10.6753f, 7.02258f, 8.21487f, 11.3643f, 7.71766f, 7.63807f, - 12.4302f, 7.23435f, 7.66462f, 13.7003f, 6.59931f, 8.29966f, - 10.6753f, 6.97741f, 8.21487f, 11.3643f, 6.28233f, 7.63807f, - 12.4302f, 6.76564f, 7.66462f, 13.7003f, 7.40068f, 8.29966f, - 10.0000f, 8.00000f, 8.00000f, 11.3818f, 6.40521f, 7.30228f, - 12.2594f, 7.12974f, 7.12974f, 14.0000f, 8.00000f, 8.00000f, }, - { 18.0000f, 6.00000f, 8.00000f, 19.7699f, 6.88498f, 7.11501f, - 20.8048f, 7.75546f, 7.23198f, 22.0000f, 6.00000f, 8.00000f, - 18.2996f, 6.59932f, 8.29966f, 19.5991f, 7.24908f, 7.64989f, - 20.8231f, 7.85928f, 7.54235f, 21.2738f, 7.35025f, 8.18154f, - 18.2996f, 7.40068f, 8.29966f, 19.5991f, 6.75091f, 7.64989f, - 20.8231f, 6.14071f, 7.54235f, 21.2738f, 6.64974f, 8.18154f, - 18.0000f, 8.00000f, 8.00000f, 19.7699f, 7.11501f, 7.11501f, - 20.8048f, 6.24453f, 7.23198f, 22.0000f, 8.00000f, 8.00000f, }, - { 10.0000f, 2.00000f, 8.00000f, 9.87124f, 1.95609f, 9.38125f, - 9.34408f, 1.50592f, 9.94049f, 9.00000f, 1.50000f, 12.0000f, - 11.0392f, 0.960774f, 8.00000f, 10.9104f, 0.916871f, - 9.38125f, 10.6451f, 0.577962f, 10.5477f, 10.5822f, - 0.465660f, 11.9069f, 12.5766f, 1.37605f, 7.76601f, - 12.7310f, 0.946813f, 9.11659f, 12.3287f, 1.27854f, - 10.5830f, 12.3287f, 1.27854f, 12.0000f, 14.0000f, 2.00000f, - 8.00000f, 14.0621f, 1.53028f, 9.33539f, 14.0000f, 1.00000f, - 10.5830f, 14.0000f, 1.00000f, 12.0000f, }, - { 14.0000f, 2.00000f, 8.00000f, 14.0621f, 1.53028f, 9.33539f, - 14.0000f, 1.00000f, 10.5830f, 14.0000f, 1.00000f, 12.0000f, - 15.9088f, 2.83677f, 8.31378f, 15.3278f, 2.08512f, 9.54346f, - 15.3579f, 0.773669f, 10.5830f, 15.3579f, 0.773669f, - 12.0000f, 16.2210f, 2.71156f, 8.35578f, 16.7241f, 1.82762f, - 9.62057f, 16.6232f, 0.00000f, 10.4734f, 16.6232f, 0.00000f, - 12.0000f, 18.0000f, 2.00000f, 8.00000f, 18.0000f, 1.31729f, - 9.36540f, 18.0000f, 0.00000f, 10.4734f, 18.0000f, 0.00000f, - 12.0000f, }, - { 18.0000f, 2.00000f, 8.00000f, 18.0000f, 1.31729f, 9.36540f, - 18.0000f, 0.00000f, 10.4734f, 18.0000f, 0.00000f, 12.0000f, - 19.5248f, 1.39004f, 7.69502f, 19.3449f, 0.779322f, - 9.09642f, 19.4294f, 0.00000f, 10.4734f, 19.4294f, 0.00000f, - 12.0000f, 20.9582f, 0.958220f, 8.00000f, 20.9309f, - 0.816847f, 9.36900f, 21.1736f, -0.0212363f, 10.6358f, - 20.9892f, -0.0107873f, 12.0000f, 22.0000f, 2.00000f, - 8.00000f, 21.9727f, 1.85862f, 9.36900f, 22.1844f, - 0.989551f, 10.6358f, 22.0000f, 1.00000f, 12.0000f, }, - { 22.0000f, 2.00000f, 8.00000f, 21.9727f, 1.85862f, 9.36900f, - 22.1844f, 0.989551f, 10.6358f, 22.0000f, 1.00000f, - 12.0000f, 23.0495f, 3.04954f, 8.00000f, 23.0222f, 2.90817f, - 9.36900f, 23.1847f, 1.98990f, 10.6359f, 23.0003f, 2.00035f, - 12.0000f, 22.8434f, 4.43176f, 7.78914f, 23.1846f, 4.43817f, - 9.16885f, 22.7037f, 3.28188f, 10.5868f, 22.8245f, 3.59620f, - 12.0001f, 22.0000f, 6.00000f, 8.00000f, 22.4865f, 5.73632f, - 9.34339f, 22.8791f, 4.68567f, 10.5866f, 23.0000f, 5.00000f, - 12.0000f, }, - { 22.0000f, 6.00000f, 8.00000f, 22.4865f, 5.73632f, 9.34339f, - 22.8791f, 4.68567f, 10.5866f, 23.0000f, 5.00000f, 12.0000f, - 21.2738f, 7.35025f, 8.18154f, 22.1519f, 6.35839f, 9.42703f, - 23.0451f, 6.01382f, 10.5865f, 23.1660f, 6.32815f, 11.9998f, - 21.2738f, 6.64974f, 8.18154f, 22.1519f, 7.64160f, 9.42703f, - 23.0451f, 7.98617f, 10.5865f, 23.1660f, 7.67185f, 11.9998f, - 22.0000f, 8.00000f, 8.00000f, 22.4865f, 8.26367f, 9.34339f, - 22.8791f, 9.31432f, 10.5866f, 23.0000f, 9.00000f, 12.0000f, }, - { 22.0000f, 8.00000f, 8.00000f, 22.4865f, 8.26367f, 9.34339f, - 22.8791f, 9.31432f, 10.5866f, 23.0000f, 9.00000f, 12.0000f, - 22.8434f, 9.56823f, 7.78914f, 23.1846f, 9.56182f, 9.16885f, - 22.7037f, 10.7181f, 10.5868f, 22.8245f, 10.4038f, 12.0001f, - 23.0495f, 10.9504f, 8.00000f, 23.0222f, 11.0918f, 9.36900f, - 23.1847f, 12.0100f, 10.6358f, 23.0003f, 11.9996f, 12.0000f, - 22.0000f, 12.0000f, 8.00000f, 21.9727f, 12.1413f, 9.36900f, - 22.1844f, 13.0104f, 10.6358f, 22.0000f, 13.0000f, 12.0000f, }, - { 22.0000f, 12.0000f, 8.00000f, 21.9727f, 12.1413f, 9.36900f, - 22.1844f, 13.0104f, 10.6358f, 22.0000f, 13.0000f, 12.0000f, - 20.9582f, 13.0417f, 8.00000f, 20.9309f, 13.1831f, 9.36900f, - 21.1850f, 14.0098f, 10.6358f, 21.0006f, 13.9994f, 12.0000f, - 19.5257f, 12.6103f, 7.69484f, 19.3449f, 13.2259f, 9.10702f, - 19.4024f, 13.8246f, 10.4615f, 19.4024f, 13.8246f, 12.0000f, - 18.0000f, 12.0000f, 8.00000f, 18.0000f, 12.6880f, 9.37601f, - 18.0000f, 14.0000f, 10.4615f, 18.0000f, 14.0000f, 12.0000f, }, - { 18.0000f, 12.0000f, 8.00000f, 18.0000f, 12.6880f, 9.37601f, - 18.0000f, 14.0000f, 10.4615f, 18.0000f, 14.0000f, 12.0000f, - 16.2225f, 11.2890f, 8.35548f, 16.7274f, 12.1789f, 9.63053f, - 16.6700f, 14.1662f, 10.4615f, 16.6700f, 14.1662f, 12.0000f, - 15.7780f, 11.2887f, 8.35561f, 15.2726f, 12.1809f, 9.63458f, - 15.3221f, 14.2203f, 10.4570f, 15.3221f, 14.2203f, 12.0000f, - 14.0000f, 12.0000f, 8.00000f, 14.0000f, 12.6900f, 9.38006f, - 14.0000f, 14.0000f, 10.4570f, 14.0000f, 14.0000f, 12.0000f, }, - { 14.0000f, 12.0000f, 8.00000f, 14.0000f, 12.6900f, 9.38006f, - 14.0000f, 14.0000f, 10.4570f, 14.0000f, 14.0000f, 12.0000f, - 12.4764f, 12.6094f, 7.69529f, 12.6576f, 13.2269f, 9.11159f, - 12.2533f, 13.7089f, 10.4570f, 12.2533f, 13.7089f, 12.0000f, - 11.0397f, 13.0397f, 7.99999f, 10.9112f, 13.0836f, 9.37941f, - 10.4694f, 13.7456f, 10.6337f, 10.3994f, 13.6615f, 12.0112f, - 10.0000f, 12.0000f, 8.00000f, 9.87141f, 12.0438f, 9.37941f, - 9.15043f, 12.6614f, 10.0622f, 9.00000f, 12.5000f, 12.0000f, }, - { 9.00000f, 1.50000f, 12.0000f, 9.07887f, 1.31725f, 13.1541f, - 9.52420f, 0.976845f, 14.7029f, 10.0000f, 2.00000f, - 16.0000f, 10.5822f, 0.465660f, 11.9069f, 10.5150f, - 0.345727f, 13.3585f, 10.9295f, 1.13219f, 14.8938f, - 11.1033f, 2.00243f, 15.9980f, 12.3287f, 1.27854f, 12.0000f, - 12.3287f, 1.27854f, 13.4306f, 12.6655f, 0.988352f, - 14.9883f, 12.6655f, 2.00000f, 16.0000f, 14.0000f, 1.00000f, - 12.0000f, 14.0000f, 1.00000f, 13.4306f, 14.0000f, - 0.988352f, 14.9883f, 14.0000f, 2.00000f, 16.0000f, }, - { 14.0000f, 1.00000f, 12.0000f, 14.0000f, 1.00000f, 13.4306f, - 14.0000f, 0.988352f, 14.9883f, 14.0000f, 2.00000f, - 16.0000f, 15.3579f, 0.773669f, 12.0000f, 15.3579f, - 0.773669f, 13.4306f, 15.3354f, 0.988352f, 14.9883f, - 15.3354f, 2.00000f, 16.0000f, 16.6232f, 0.00000f, 12.0000f, - 16.6232f, 0.00000f, 13.5696f, 16.8433f, 0.801365f, - 15.2136f, 16.6794f, 2.01251f, 16.1985f, 18.0000f, 0.00000f, - 12.0000f, 18.0000f, 0.00000f, 13.5696f, 18.1639f, - 0.788853f, 15.0150f, 18.0000f, 2.00000f, 16.0000f, }, - { 18.0000f, 0.00000f, 12.0000f, 18.0000f, 0.00000f, 13.5696f, - 18.1639f, 0.788853f, 15.0150f, 18.0000f, 2.00000f, - 16.0000f, 19.4294f, 0.00000f, 12.0000f, 19.4294f, 0.00000f, - 13.5696f, 19.2285f, 0.778765f, 14.8549f, 19.0646f, - 1.98991f, 15.8399f, 20.9892f, -0.0107873f, 12.0000f, - 20.8354f, -0.00207507f, 13.1374f, 20.6978f, 0.580195f, - 14.6361f, 20.2213f, 1.34332f, 15.3489f, 22.0000f, 1.00000f, - 12.0000f, 21.8462f, 1.00871f, 13.1374f, 21.4764f, 1.23687f, - 14.2872f, 21.0000f, 2.00000f, 15.0000f, }, - { 22.0000f, 1.00000f, 12.0000f, 21.8462f, 1.00871f, 13.1374f, - 21.4764f, 1.23687f, 14.2872f, 21.0000f, 2.00000f, 15.0000f, - 23.0003f, 2.00035f, 12.0000f, 22.8465f, 2.00907f, 13.1374f, - 22.7558f, 2.31585f, 13.7139f, 22.2794f, 3.07898f, 14.4267f, - 22.8245f, 3.59620f, 12.0001f, 22.9168f, 3.83627f, 13.0796f, - 22.3069f, 4.55472f, 14.7895f, 21.9821f, 5.49730f, 15.2768f, - 23.0000f, 5.00000f, 12.0000f, 23.0922f, 5.24007f, 13.0794f, - 22.8248f, 6.05742f, 14.0127f, 22.5000f, 7.00000f, 14.5000f, }, - { 22.5000f, 7.00000f, 14.5000f, 22.8248f, 7.94257f, 14.0127f, - 23.0922f, 8.75992f, 13.0794f, 23.0000f, 9.00000f, 12.0000f, - 22.5000f, 7.00000f, 14.5000f, 23.1496f, 7.00000f, 13.5254f, - 23.2583f, 7.43177f, 13.0793f, 23.1660f, 7.67185f, 11.9998f, - 22.5000f, 7.00000f, 14.5000f, 23.1496f, 7.00000f, 13.5254f, - 23.2583f, 6.56822f, 13.0793f, 23.1660f, 6.32815f, 11.9998f, - 22.5000f, 7.00000f, 14.5000f, 22.8248f, 6.05742f, 14.0127f, - 23.0922f, 5.24007f, 13.0794f, 23.0000f, 5.00000f, 12.0000f, }, - { 23.0000f, 9.00000f, 12.0000f, 23.0922f, 8.75992f, 13.0794f, - 22.8248f, 7.94257f, 14.0127f, 22.5000f, 7.00000f, 14.5000f, - 22.8245f, 10.4038f, 12.0001f, 22.9168f, 10.1637f, 13.0796f, - 22.3069f, 9.44527f, 14.7895f, 21.9821f, 8.50269f, 15.2768f, - 23.0003f, 11.9996f, 12.0000f, 22.8465f, 11.9909f, 13.1374f, - 22.7558f, 11.6841f, 13.7139f, 22.2794f, 10.9210f, 14.4267f, - 22.0000f, 13.0000f, 12.0000f, 21.8462f, 12.9912f, 13.1374f, - 21.4764f, 12.7631f, 14.2872f, 21.0000f, 12.0000f, 15.0000f, }, - { 22.0000f, 13.0000f, 12.0000f, 21.8462f, 12.9912f, 13.1374f, - 21.4764f, 12.7631f, 14.2872f, 21.0000f, 12.0000f, 15.0000f, - 21.0006f, 13.9994f, 12.0000f, 20.8468f, 13.9906f, 13.1374f, - 20.6978f, 13.4198f, 14.6361f, 20.2213f, 12.6566f, 15.3489f, - 19.4024f, 13.8246f, 12.0000f, 19.4024f, 13.8246f, 13.5675f, - 19.2283f, 13.2196f, 14.8562f, 19.0646f, 12.0100f, 15.8399f, - 18.0000f, 14.0000f, 12.0000f, 18.0000f, 14.0000f, 13.5675f, - 18.1636f, 13.2095f, 15.0163f, 18.0000f, 12.0000f, 16.0000f, }, - { 18.0000f, 14.0000f, 12.0000f, 18.0000f, 14.0000f, 13.5675f, - 18.1636f, 13.2095f, 15.0163f, 18.0000f, 12.0000f, 16.0000f, - 16.6700f, 14.1662f, 12.0000f, 16.6700f, 14.1662f, 13.5675f, - 16.8420f, 13.1970f, 15.2151f, 16.6783f, 11.9874f, 16.1987f, - 15.3221f, 14.2203f, 12.0000f, 15.3221f, 14.2203f, 13.5616f, - 15.3213f, 13.3497f, 14.9846f, 15.3264f, 12.1643f, 16.0012f, - 14.0000f, 14.0000f, 12.0000f, 14.0000f, 14.0000f, 13.5616f, - 13.9949f, 13.1854f, 14.9833f, 14.0000f, 12.0000f, 16.0000f, }, - { 14.0000f, 14.0000f, 12.0000f, 14.0000f, 14.0000f, 13.5616f, - 13.9949f, 13.1854f, 14.9833f, 14.0000f, 12.0000f, 16.0000f, - 12.2533f, 13.7089f, 12.0000f, 12.2533f, 13.7089f, 13.5616f, - 12.6271f, 13.0159f, 14.9821f, 12.6322f, 11.8305f, 15.9987f, - 10.3994f, 13.6615f, 12.0112f, 10.3212f, 13.5676f, 13.5504f, - 10.9955f, 12.0544f, 14.6564f, 11.1786f, 11.1102f, 15.8546f, - 9.00000f, 12.5000f, 12.0000f, 8.96405f, 12.4448f, 13.3410f, - 9.51945f, 12.2157f, 14.4584f, 10.0000f, 11.0000f, 16.0000f, }, - { 10.0000f, 2.00000f, 16.0000f, 10.0036f, 3.39890f, 17.7763f, - 9.97797f, 5.10529f, 18.0055f, 10.0000f, 7.00000f, 18.0000f, - 11.1033f, 2.00243f, 15.9980f, 11.3375f, 3.17526f, 17.4863f, - 11.2748f, 5.10529f, 17.6813f, 11.2968f, 7.00000f, 17.6757f, - 12.6655f, 2.00000f, 16.0000f, 12.6655f, 3.31963f, 17.3196f, - 12.6644f, 5.13375f, 18.0572f, 12.6644f, 7.00000f, 18.0572f, - 14.0000f, 2.00000f, 16.0000f, 14.0000f, 3.31963f, 17.3196f, - 14.0000f, 5.13375f, 18.0000f, 14.0000f, 7.00000f, 18.0000f, }, - { 14.0000f, 2.00000f, 16.0000f, 14.0000f, 3.31963f, 17.3196f, - 14.0000f, 5.13375f, 18.0000f, 14.0000f, 7.00000f, 18.0000f, - 15.3354f, 2.00000f, 16.0000f, 15.3354f, 3.31963f, 17.3196f, - 15.0055f, 5.13375f, 17.9569f, 15.0055f, 7.00000f, 17.9569f, - 16.6794f, 2.01251f, 16.1985f, 16.4870f, 3.43424f, 17.3547f, - 16.0130f, 5.15743f, 17.8973f, 16.0130f, 7.00000f, 17.8973f, - 18.0000f, 2.00000f, 16.0000f, 17.8076f, 3.42172f, 17.1561f, - 17.0000f, 5.15743f, 17.7000f, 17.0000f, 7.00000f, 17.7000f, }, - { 18.0000f, 2.00000f, 16.0000f, 17.8076f, 3.42172f, 17.1561f, - 17.0000f, 5.15743f, 17.7000f, 17.0000f, 7.00000f, 17.7000f, - 19.0646f, 1.98991f, 15.8399f, 18.8722f, 3.41164f, 16.9960f, - 17.6994f, 5.15743f, 17.5601f, 17.6994f, 7.00000f, 17.5601f, - 20.2213f, 1.34332f, 15.3489f, 19.4209f, 2.62530f, 16.5462f, - 19.1846f, 5.31432f, 16.8753f, 19.0000f, 7.00000f, 17.0000f, - 21.0000f, 2.00000f, 15.0000f, 20.1996f, 3.28198f, 16.1973f, - 19.0000f, 7.00000f, 17.0000f, 19.0000f, 7.00000f, 17.0000f, }, - { 21.0000f, 2.00000f, 15.0000f, 20.1996f, 3.28198f, 16.1973f, - 19.0000f, 7.00000f, 17.0000f, 19.0000f, 7.00000f, 17.0000f, - 22.2794f, 3.07898f, 14.4267f, 21.4790f, 4.36096f, 15.6240f, - 20.5517f, 6.99999f, 15.9525f, 19.0000f, 7.00000f, 17.0000f, - 21.9821f, 5.49730f, 15.2768f, 21.4642f, 7.00000f, 16.0536f, - 21.4790f, 9.63903f, 15.6240f, 20.1996f, 10.7180f, 16.1973f, - 22.5000f, 7.00000f, 14.5000f, 21.9821f, 8.50269f, 15.2768f, - 22.2794f, 10.9210f, 14.4267f, 21.0000f, 12.0000f, 15.0000f, }, - { 21.0000f, 12.0000f, 15.0000f, 20.1996f, 10.7180f, 16.1973f, - 19.0000f, 7.00000f, 17.0000f, 19.0000f, 7.00000f, 17.0000f, - 20.2213f, 12.6566f, 15.3489f, 19.4209f, 11.3746f, 16.5462f, - 19.1846f, 8.68567f, 16.8753f, 19.0000f, 7.00000f, 17.0000f, - 19.0646f, 12.0100f, 15.8399f, 18.8722f, 10.5883f, 16.9960f, - 17.6994f, 8.84256f, 17.5601f, 17.6994f, 7.00000f, 17.5601f, - 18.0000f, 12.0000f, 16.0000f, 17.8076f, 10.5782f, 17.1561f, - 17.0000f, 8.84256f, 17.7000f, 17.0000f, 7.00000f, 17.7000f, }, - { 18.0000f, 12.0000f, 16.0000f, 17.8076f, 10.5782f, 17.1561f, - 17.0000f, 8.84256f, 17.7000f, 17.0000f, 7.00000f, 17.7000f, - 16.6783f, 11.9874f, 16.1987f, 16.4859f, 10.5657f, 17.3549f, - 16.0130f, 8.84256f, 17.8973f, 16.0130f, 7.00000f, 17.8973f, - 15.3264f, 12.1643f, 16.0012f, 15.3324f, 10.7567f, 17.2083f, - 15.0055f, 8.85429f, 17.9569f, 15.0055f, 7.00000f, 17.9569f, - 14.0000f, 12.0000f, 16.0000f, 14.0060f, 10.5924f, 17.2071f, - 14.0000f, 8.85429f, 18.0000f, 14.0000f, 7.00000f, 18.0000f, }, - { 14.0000f, 12.0000f, 16.0000f, 14.0060f, 10.5924f, 17.2071f, - 14.0000f, 8.85429f, 18.0000f, 14.0000f, 7.00000f, 18.0000f, - 12.6322f, 11.8305f, 15.9987f, 12.6382f, 10.4229f, 17.2058f, - 12.6644f, 8.85429f, 18.0572f, 12.6644f, 7.00000f, 18.0572f, - 11.1786f, 11.1102f, 15.8546f, 11.3641f, 10.1538f, 17.0682f, - 11.3151f, 8.57346f, 17.6712f, 11.2968f, 7.00000f, 17.6757f, - 10.0000f, 11.0000f, 16.0000f, 10.0023f, 9.92533f, 17.3646f, - 10.0182f, 8.57346f, 17.9954f, 10.0000f, 7.00000f, 18.0000f, }, - { 10.0000f, 7.00000f, 18.0000f, 9.97797f, 5.10529f, 18.0055f, - 10.0036f, 3.39890f, 17.7763f, 10.0000f, 2.00000f, 16.0000f, - 8.54835f, 7.00000f, 18.3629f, 8.52633f, 5.10529f, 18.3684f, - 8.65977f, 3.62422f, 18.0685f, 7.86846f, 2.71708f, 16.9202f, - 7.49631f, 7.00000f, 20.0000f, 7.49631f, 5.45253f, 20.0000f, - 7.26780f, 3.46826f, 18.6874f, 6.00000f, 3.00000f, 18.0000f, - 6.00000f, 7.00000f, 20.0000f, 6.00000f, 5.45253f, 20.0000f, - 6.00000f, 3.00000f, 18.0000f, 6.00000f, 3.00000f, 18.0000f, }, - { 6.00000f, 7.00000f, 20.0000f, 6.00000f, 5.45253f, 20.0000f, - 6.00000f, 3.00000f, 18.0000f, 6.00000f, 3.00000f, 18.0000f, - 4.70898f, 7.00000f, 20.0000f, 4.70898f, 5.45253f, 20.0000f, - 5.43536f, 3.74871f, 18.6055f, 6.00000f, 3.00000f, 18.0000f, - 3.62697f, 7.00000f, 19.1285f, 3.62697f, 5.28417f, 19.1285f, - 4.35824f, 3.90451f, 17.7575f, 5.28514f, 2.90705f, 16.7135f, - 3.00000f, 7.00000f, 18.0000f, 2.99999f, 5.28417f, 18.0000f, - 4.07310f, 3.99745f, 17.0440f, 5.00000f, 3.00000f, 16.0000f, }, - { 3.00000f, 7.00000f, 18.0000f, 2.99999f, 5.28417f, 18.0000f, - 4.07310f, 3.99745f, 17.0440f, 5.00000f, 3.00000f, 16.0000f, - 1.79545f, 7.00000f, 15.8318f, 1.79545f, 5.28417f, 15.8318f, - 3.32781f, 4.24038f, 15.1790f, 4.25471f, 3.24292f, 14.1350f, - 0.794020f, 7.00000f, 13.4717f, 0.794020f, 5.60875f, - 13.4717f, 1.51717f, 3.42451f, 12.7792f, 2.88595f, 3.40717f, - 12.5308f, 1.00000f, 7.00000f, 11.0000f, 1.00000f, 5.60875f, - 11.0000f, 1.63122f, 4.01734f, 10.8484f, 3.00000f, 4.00000f, - 10.6000f, }, - { 1.00000f, 7.00000f, 11.0000f, 1.00000f, 5.60875f, 11.0000f, - 1.63122f, 4.01734f, 10.8484f, 3.00000f, 4.00000f, 10.6000f, - 1.16542f, 7.00000f, 9.01491f, 1.16542f, 5.60875f, 9.01491f, - 1.71610f, 4.45857f, 9.41132f, 3.08488f, 4.44123f, 9.16289f, - 2.70364f, 7.00000f, 7.51241f, 2.70364f, 6.27240f, 7.51241f, - 3.60899f, 5.37796f, 7.63476f, 4.12348f, 5.37796f, 8.14925f, - 4.00000f, 7.00000f, 6.00000f, 4.00000f, 6.27240f, 6.00000f, - 4.48551f, 5.80000f, 6.48551f, 5.00000f, 5.80000f, 7.00000f, }, - { 4.00000f, 7.00000f, 6.00000f, 4.00000f, 6.27240f, 6.00000f, - 4.48551f, 5.80000f, 6.48551f, 5.00000f, 5.80000f, 7.00000f, - 4.79829f, 7.00000f, 5.06865f, 4.79829f, 6.27240f, 5.06865f, - 5.03793f, 6.06598f, 5.76119f, 5.55242f, 6.06598f, 6.27568f, - 5.82508f, 7.00000f, 4.35247f, 5.82508f, 6.44606f, 4.35247f, - 6.08632f, 5.95431f, 4.69838f, 6.08632f, 5.95431f, 5.25232f, - 7.00000f, 7.00000f, 4.00000f, 7.00000f, 6.44606f, 4.00000f, - 7.00000f, 6.00000f, 4.44606f, 7.00000f, 6.00000f, 5.00000f, }, - { 7.00000f, 7.00000f, 4.00000f, 7.00000f, 6.44606f, 4.00000f, - 7.00000f, 6.00000f, 4.44606f, 7.00000f, 6.00000f, 5.00000f, - 7.69551f, 7.00000f, 3.79134f, 7.69551f, 6.44606f, 3.79134f, - 7.66874f, 6.03343f, 4.26137f, 7.66874f, 6.03343f, 4.81531f, - 9.00000f, 7.00000f, 4.50000f, 8.63511f, 6.54994f, 4.22633f, - 8.59332f, 5.79666f, 4.83390f, 9.00000f, 6.00000f, 5.50000f, - 9.00000f, 7.00000f, 4.50000f, 9.00000f, 7.00000f, 4.50000f, - 9.00000f, 6.00000f, 5.50000f, 9.00000f, 6.00000f, 5.50000f, }, - { 9.00000f, 7.00000f, 4.50000f, 9.00000f, 7.00000f, 4.50000f, - 9.00000f, 6.00000f, 5.50000f, 9.00000f, 6.00000f, 5.50000f, - 9.00000f, 7.00000f, 4.50000f, 9.43205f, 7.00000f, 4.82404f, - 9.44642f, 6.22321f, 5.33934f, 9.00000f, 6.00000f, 5.50000f, - 9.00000f, 8.00000f, 5.50000f, 9.44642f, 7.77679f, 5.33934f, - 9.09980f, 7.00000f, 5.97504f, 9.00000f, 7.00000f, 6.00000f, - 9.00000f, 8.00000f, 5.50000f, 9.00000f, 8.00000f, 5.50000f, - 9.00000f, 7.00000f, 6.00000f, 9.00000f, 7.00000f, 6.00000f, }, - { 10.0000f, 2.00000f, 8.00000f, 8.93204f, 3.06795f, 8.00000f, - 8.96952f, 4.43964f, 7.67212f, 10.0000f, 6.00000f, 8.00000f, - 9.87124f, 1.95609f, 9.38125f, 8.80329f, 3.02405f, 9.38125f, - 8.74608f, 4.46130f, 8.42383f, 9.56588f, 5.70264f, 8.68468f, - 9.34408f, 1.50592f, 9.94049f, 8.51649f, 2.09617f, 9.55423f, - 9.59858f, 3.81408f, 9.77086f, 9.59783f, 5.18668f, 9.40552f, - 9.00000f, 1.50000f, 12.0000f, 7.73438f, 2.55240f, 10.0865f, - 9.00075f, 3.62739f, 10.3653f, 9.00000f, 5.00000f, 10.0000f, }, - { 9.00000f, 1.50000f, 12.0000f, 7.73438f, 2.55240f, 10.0865f, - 9.00075f, 3.62739f, 10.3653f, 9.00000f, 5.00000f, 10.0000f, - 9.00000f, 1.50000f, 12.0000f, 7.15956f, 2.88771f, 10.4777f, - 8.48141f, 3.46521f, 10.8817f, 8.48065f, 4.83782f, 10.5164f, - 9.00000f, 1.50000f, 12.0000f, 7.15956f, 2.88771f, 10.4777f, - 8.32175f, 3.72942f, 10.9234f, 7.85973f, 4.98761f, 10.5389f, - 9.00000f, 1.50000f, 12.0000f, 7.45296f, 2.59077f, 11.3891f, - 7.91171f, 3.25991f, 11.3835f, 7.00000f, 5.00000f, 11.0000f, }, - { 9.00000f, 1.50000f, 12.0000f, 7.45296f, 2.59077f, 11.3891f, - 7.91171f, 3.25991f, 11.3835f, 7.00000f, 5.00000f, 11.0000f, - 9.07887f, 1.31725f, 13.1541f, 7.97031f, 2.06717f, 12.9963f, - 7.41411f, 2.69014f, 11.9419f, 6.75115f, 3.94166f, 11.6699f, - 9.52420f, 0.976845f, 14.7029f, 8.32713f, 0.844520f, - 14.5403f, 8.27262f, 2.45611f, 13.7536f, 7.52117f, 3.19943f, - 12.9181f, 10.0000f, 2.00000f, 16.0000f, 7.84991f, 1.51717f, - 15.3966f, 7.75144f, 2.05668f, 14.8355f, 7.00000f, 2.80000f, - 14.0000f, }, - { 10.0000f, 2.00000f, 16.0000f, 7.84991f, 1.51717f, 15.3966f, - 7.75144f, 2.05668f, 14.8355f, 7.00000f, 2.80000f, 14.0000f, - 7.86846f, 2.71708f, 16.9202f, 7.38876f, 2.16717f, 16.2241f, - 7.34956f, 1.74867f, 15.6698f, 6.59812f, 2.49199f, 14.8342f, - 6.00000f, 3.00000f, 18.0000f, 6.83143f, 1.96557f, 17.1732f, - 5.81231f, 2.33975f, 16.1197f, 5.52717f, 2.43269f, 15.4062f, - 6.00000f, 3.00000f, 18.0000f, 6.00000f, 3.00000f, 18.0000f, - 5.28514f, 2.90705f, 16.7135f, 5.00000f, 3.00000f, 16.0000f, }, - { 5.00000f, 3.00000f, 16.0000f, 5.52717f, 2.43269f, 15.4062f, - 6.59812f, 2.49199f, 14.8342f, 7.00000f, 2.80000f, 14.0000f, - 4.25471f, 3.24292f, 14.1350f, 4.78188f, 2.67562f, 13.5412f, - 5.34262f, 3.73390f, 13.4382f, 5.74450f, 4.04191f, 12.6039f, - 2.88595f, 3.40717f, 12.5308f, 4.34454f, 3.38869f, 12.2661f, - 6.08167f, 3.98531f, 10.4018f, 7.19502f, 4.53322f, 11.1711f, - 3.00000f, 4.00000f, 10.6000f, 4.45858f, 3.98151f, 10.3352f, - 4.69919f, 5.29811f, 9.44672f, 5.50000f, 7.00000f, 10.0000f, }, - { 3.00000f, 4.00000f, 10.6000f, 4.45858f, 3.98151f, 10.3352f, - 4.69919f, 5.29811f, 9.44672f, 5.50000f, 7.00000f, 10.0000f, - 3.08488f, 4.44123f, 9.16289f, 4.54347f, 4.42275f, 8.89816f, - 4.26221f, 5.71307f, 9.14480f, 4.26221f, 7.00000f, 9.14480f, - 4.12348f, 5.37796f, 8.14925f, 4.62387f, 5.37796f, 8.64963f, - 5.73017f, 6.29235f, 8.71953f, 5.73017f, 7.00000f, 8.71953f, - 5.00000f, 5.80000f, 7.00000f, 5.50038f, 5.80000f, 7.50038f, - 6.00000f, 6.29235f, 8.00000f, 6.00000f, 7.00000f, 8.00000f, }, - { 5.00000f, 5.80000f, 7.00000f, 5.50038f, 5.80000f, 7.50038f, - 6.00000f, 6.29235f, 8.00000f, 6.00000f, 7.00000f, 8.00000f, - 5.55242f, 6.06598f, 6.27568f, 6.05280f, 6.06598f, 6.77606f, - 6.26818f, 6.29235f, 7.28483f, 6.26818f, 7.00000f, 7.28483f, - 6.08632f, 5.95431f, 5.25232f, 6.08632f, 5.95431f, 5.80147f, - 6.36448f, 6.45084f, 6.42367f, 6.36448f, 7.00000f, 6.42367f, - 7.00000f, 6.00000f, 5.00000f, 7.00000f, 6.00000f, 5.54915f, - 7.00000f, 6.45084f, 6.00000f, 7.00000f, 7.00000f, 6.00000f, }, - { 7.00000f, 6.00000f, 5.00000f, 7.00000f, 6.00000f, 5.54915f, - 7.00000f, 6.45084f, 6.00000f, 7.00000f, 7.00000f, 6.00000f, - 7.66874f, 6.03343f, 4.81531f, 7.66874f, 6.03343f, 5.36447f, - 7.55880f, 6.45084f, 5.62746f, 7.55880f, 7.00000f, 5.62746f, - 9.00000f, 6.00000f, 5.50000f, 8.65645f, 5.82822f, 5.63149f, - 8.39835f, 6.57583f, 6.15041f, 9.00000f, 7.00000f, 6.00000f, - 9.00000f, 6.00000f, 5.50000f, 9.00000f, 6.00000f, 5.50000f, - 9.00000f, 7.00000f, 6.00000f, 9.00000f, 7.00000f, 6.00000f, }, - { 10.0000f, 11.0000f, 16.0000f, 10.0023f, 9.92533f, 17.3646f, - 10.0182f, 8.57346f, 17.9954f, 10.0000f, 7.00000f, 18.0000f, - 7.93998f, 10.5043f, 16.6387f, 8.61321f, 9.69225f, 17.6669f, - 8.56665f, 8.57346f, 18.3583f, 8.54835f, 7.00000f, 18.3629f, - 6.00000f, 11.0000f, 18.0000f, 7.40668f, 10.2558f, 18.7441f, - 7.49631f, 8.54969f, 20.0000f, 7.49631f, 7.00000f, 20.0000f, - 6.00000f, 11.0000f, 18.0000f, 6.00000f, 11.0000f, 18.0000f, - 6.00000f, 8.54969f, 20.0000f, 6.00000f, 7.00000f, 20.0000f, }, - { 6.00000f, 11.0000f, 18.0000f, 6.00000f, 11.0000f, 18.0000f, - 6.00000f, 8.54969f, 20.0000f, 6.00000f, 7.00000f, 20.0000f, - 6.00000f, 11.0000f, 18.0000f, 5.54861f, 10.3555f, 18.6444f, - 4.70898f, 8.54969f, 20.0000f, 4.70898f, 7.00000f, 20.0000f, - 5.28802f, 11.0938f, 16.7207f, 4.36112f, 10.0964f, 17.7647f, - 3.62697f, 8.71582f, 19.1285f, 3.62697f, 7.00000f, 19.1285f, - 5.00000f, 11.0000f, 16.0000f, 4.07310f, 10.0025f, 17.0440f, - 3.00000f, 8.71582f, 18.0000f, 3.00000f, 7.00000f, 18.0000f, }, - { 5.00000f, 11.0000f, 16.0000f, 4.07310f, 10.0025f, 17.0440f, - 3.00000f, 8.71582f, 18.0000f, 3.00000f, 7.00000f, 18.0000f, - 4.25471f, 10.7570f, 14.1350f, 3.32781f, 9.75961f, 15.1790f, - 1.79545f, 8.71582f, 15.8318f, 1.79545f, 7.00000f, 15.8318f, - 2.88595f, 10.5928f, 12.5308f, 1.51717f, 10.5754f, 12.7792f, - 0.794020f, 8.39124f, 13.4717f, 0.794020f, 7.00000f, - 13.4717f, 3.00000f, 10.0000f, 10.6000f, 1.63122f, 9.98265f, - 10.8484f, 1.00000f, 8.39124f, 11.0000f, 1.00000f, 7.00000f, - 11.0000f, }, - { 3.00000f, 10.0000f, 10.6000f, 1.63122f, 9.98265f, 10.8484f, - 1.00000f, 8.39124f, 11.0000f, 1.00000f, 7.00000f, 11.0000f, - 3.08488f, 9.55876f, 9.16289f, 1.71610f, 9.54142f, 9.41132f, - 1.16542f, 8.39124f, 9.01491f, 1.16542f, 7.00000f, 9.01491f, - 4.12348f, 8.62203f, 8.14925f, 3.60899f, 8.62203f, 7.63476f, - 2.70364f, 7.72759f, 7.51241f, 2.70364f, 7.00000f, 7.51241f, - 5.00000f, 8.20000f, 7.00000f, 4.48551f, 8.20000f, 6.48550f, - 4.00000f, 7.72759f, 6.00000f, 4.00000f, 7.00000f, 6.00000f, }, - { 5.00000f, 8.20000f, 7.00000f, 4.48551f, 8.20000f, 6.48550f, - 4.00000f, 7.72759f, 6.00000f, 4.00000f, 7.00000f, 6.00000f, - 5.55241f, 7.93401f, 6.27568f, 5.03792f, 7.93401f, 5.76119f, - 4.79829f, 7.72759f, 5.06865f, 4.79829f, 7.00000f, 5.06865f, - 6.08632f, 8.04568f, 5.25232f, 6.08632f, 8.04568f, 4.69838f, - 5.82508f, 7.55393f, 4.35247f, 5.82508f, 7.00000f, 4.35247f, - 7.00000f, 8.00000f, 5.00000f, 7.00000f, 8.00000f, 4.44606f, - 7.00000f, 7.55393f, 4.00000f, 7.00000f, 7.00000f, 4.00000f, }, - { 7.00000f, 8.00000f, 5.00000f, 7.00000f, 8.00000f, 4.44606f, - 7.00000f, 7.55393f, 4.00000f, 7.00000f, 7.00000f, 4.00000f, - 7.66874f, 7.96656f, 4.81531f, 7.66874f, 7.96656f, 4.26138f, - 7.69551f, 7.55393f, 3.79134f, 7.69551f, 7.00000f, 3.79134f, - 9.00000f, 8.00000f, 5.50000f, 8.59332f, 8.20333f, 4.83390f, - 8.63511f, 7.45005f, 4.22633f, 9.00000f, 7.00000f, 4.50000f, - 9.00000f, 8.00000f, 5.50000f, 9.00000f, 8.00000f, 5.50000f, - 9.00000f, 7.00000f, 4.50000f, 9.00000f, 7.00000f, 4.50000f, }, - { 10.0000f, 8.00000f, 8.00000f, 8.96952f, 9.56035f, 7.67212f, - 8.93204f, 10.9320f, 8.00000f, 10.0000f, 12.0000f, 8.00000f, - 9.56588f, 8.29735f, 8.68468f, 8.74608f, 9.53870f, 8.42383f, - 8.80346f, 10.9758f, 9.37941f, 9.87141f, 12.0438f, 9.37941f, - 9.59783f, 8.81331f, 9.40552f, 9.59859f, 10.1923f, 9.77256f, - 8.27272f, 11.9399f, 9.68199f, 9.15043f, 12.6614f, 10.0622f, - 9.00000f, 9.00000f, 10.0000f, 9.00075f, 10.3789f, 10.3670f, - 7.51132f, 11.2958f, 10.3210f, 9.00000f, 12.5000f, 12.0000f, }, - { 9.00000f, 9.00000f, 10.0000f, 9.00075f, 10.3789f, 10.3670f, - 7.51132f, 11.2958f, 10.3210f, 9.00000f, 12.5000f, 12.0000f, - 9.00000f, 9.00000f, 10.0000f, 8.48141f, 10.5411f, 10.8834f, - 7.07052f, 10.9228f, 10.6910f, 7.30706f, 11.1052f, 11.4309f, - 9.00000f, 9.00000f, 10.0000f, 8.48141f, 10.5411f, 10.8834f, - 8.32883f, 10.2840f, 10.9264f, 7.91955f, 10.7550f, 11.3868f, - 9.00000f, 9.00000f, 10.0000f, 8.48065f, 9.16217f, 10.5164f, - 7.86080f, 9.01240f, 10.5383f, 7.00000f, 9.00000f, 11.0000f, }, - { 7.00000f, 9.00000f, 11.0000f, 7.91955f, 10.7550f, 11.3868f, - 7.30706f, 11.1052f, 11.4309f, 9.00000f, 12.5000f, 12.0000f, - 6.74950f, 10.0653f, 11.6744f, 7.41955f, 11.3303f, 11.9493f, - 7.86345f, 11.5343f, 13.1713f, 8.96405f, 12.4448f, 13.3410f, - 7.52546f, 10.7873f, 12.9152f, 8.32087f, 11.3878f, 13.7595f, - 8.49662f, 12.3274f, 14.3212f, 9.51945f, 12.2157f, 14.4584f, - 7.00000f, 11.2000f, 14.0000f, 7.79541f, 11.8004f, 14.8442f, - 7.99336f, 11.7065f, 15.1118f, 10.0000f, 11.0000f, 16.0000f, }, - { 7.00000f, 11.2000f, 14.0000f, 7.79541f, 11.8004f, 14.8442f, - 7.99336f, 11.7065f, 15.1118f, 10.0000f, 11.0000f, 16.0000f, - 6.59576f, 11.5174f, 14.8344f, 7.39118f, 12.1179f, 15.6787f, - 7.47421f, 11.0661f, 15.9274f, 7.93998f, 10.5043f, 16.6387f, - 5.52943f, 11.5697f, 15.4036f, 5.81745f, 11.6636f, 16.1244f, - 6.95530f, 11.8030f, 17.1969f, 6.00000f, 11.0000f, 18.0000f, - 5.00000f, 11.0000f, 16.0000f, 5.28802f, 11.0938f, 16.7207f, - 6.00000f, 11.0000f, 18.0000f, 6.00000f, 11.0000f, 18.0000f, }, - { 7.00000f, 11.2000f, 14.0000f, 6.59576f, 11.5174f, 14.8344f, - 5.52943f, 11.5697f, 15.4036f, 5.00000f, 11.0000f, 16.0000f, - 5.63935f, 10.1728f, 12.5557f, 5.23511f, 10.4902f, 13.3902f, - 4.78415f, 11.3268f, 13.5386f, 4.25471f, 10.7570f, 14.1350f, - 7.18618f, 9.45391f, 11.1650f, 6.07284f, 10.0018f, 10.3957f, - 4.34454f, 10.6113f, 12.2661f, 2.88595f, 10.5928f, 12.5308f, - 5.50000f, 7.00000f, 10.0000f, 4.69955f, 8.70113f, 9.44696f, - 4.45858f, 10.0184f, 10.3352f, 3.00000f, 10.0000f, 10.6000f, }, - { 5.50000f, 7.00000f, 10.0000f, 4.69955f, 8.70113f, 9.44696f, - 4.45858f, 10.0184f, 10.3352f, 3.00000f, 10.0000f, 10.6000f, - 4.26221f, 7.00000f, 9.14480f, 4.26221f, 8.28692f, 9.14480f, - 4.54346f, 9.57725f, 8.89816f, 3.08488f, 9.55876f, 9.16289f, - 5.73017f, 7.00000f, 8.71953f, 5.73017f, 7.70764f, 8.71953f, - 4.62387f, 8.62203f, 8.64963f, 4.12348f, 8.62203f, 8.14925f, - 6.00000f, 7.00000f, 8.00000f, 6.00000f, 7.70764f, 8.00000f, - 5.50038f, 8.20000f, 7.50038f, 5.00000f, 8.20000f, 7.00000f, }, - { 6.00000f, 7.00000f, 8.00000f, 6.00000f, 7.70764f, 8.00000f, - 5.50038f, 8.20000f, 7.50038f, 5.00000f, 8.20000f, 7.00000f, - 6.26818f, 7.00000f, 7.28483f, 6.26818f, 7.70764f, 7.28483f, - 6.05280f, 7.93401f, 6.77606f, 5.55241f, 7.93401f, 6.27568f, - 6.36448f, 7.00000f, 6.42367f, 6.36448f, 7.54915f, 6.42367f, - 6.08632f, 8.04568f, 5.80147f, 6.08632f, 8.04568f, 5.25232f, - 7.00000f, 7.00000f, 6.00000f, 7.00000f, 7.54915f, 6.00000f, - 7.00000f, 8.00000f, 5.54915f, 7.00000f, 8.00000f, 5.00000f, }, - { 7.00000f, 7.00000f, 6.00000f, 7.00000f, 7.54915f, 6.00000f, - 7.00000f, 8.00000f, 5.54915f, 7.00000f, 8.00000f, 5.00000f, - 7.55880f, 7.00000f, 5.62746f, 7.55880f, 7.54915f, 5.62746f, - 7.66874f, 7.96656f, 5.36447f, 7.66874f, 7.96656f, 4.81531f, - 9.00000f, 7.00000f, 6.00000f, 8.39835f, 7.42416f, 6.15041f, - 8.65644f, 8.17177f, 5.63149f, 9.00000f, 8.00000f, 5.50000f, - 9.00000f, 7.00000f, 6.00000f, 9.00000f, 7.00000f, 6.00000f, - 9.00000f, 8.00000f, 5.50000f, 9.00000f, 8.00000f, 5.50000f, }, - { 10.0000f, 6.00000f, 8.00000f, 10.6753f, 7.02258f, 8.21487f, - 10.6753f, 6.97741f, 8.21487f, 10.0000f, 8.00000f, 8.00000f, - 9.56588f, 5.70264f, 8.68468f, 9.96312f, 6.30414f, 8.81107f, - 9.96312f, 7.69585f, 8.81107f, 9.56588f, 8.29735f, 8.68468f, - 9.59783f, 5.18668f, 9.40552f, 9.59712f, 6.48744f, 9.05930f, - 9.59712f, 7.51255f, 9.05930f, 9.59783f, 8.81331f, 9.40552f, - 9.00000f, 5.00000f, 10.0000f, 8.99928f, 6.30075f, 9.65378f, - 8.99928f, 7.69924f, 9.65378f, 9.00000f, 9.00000f, 10.0000f, }, - { 9.00000f, 5.00000f, 10.0000f, 8.99928f, 6.30075f, 9.65378f, - 8.99928f, 7.69924f, 9.65378f, 9.00000f, 9.00000f, 10.0000f, - 8.48065f, 4.83782f, 10.5164f, 8.47994f, 6.13857f, 10.1702f, - 8.47994f, 7.86142f, 10.1702f, 8.48065f, 9.16217f, 10.5164f, - 7.85973f, 4.98761f, 10.5389f, 7.39107f, 6.26389f, 10.1488f, - 7.39108f, 7.73610f, 10.1488f, 7.86080f, 9.01240f, 10.5383f, - 7.00000f, 5.00000f, 11.0000f, 6.60445f, 6.86590f, 10.2723f, - 6.60445f, 7.13409f, 10.2723f, 7.00000f, 9.00000f, 11.0000f, }, - { 5.50000f, 7.00000f, 10.0000f, 7.17220f, 4.74482f, 11.1553f, - 6.28350f, 6.33090f, 10.7170f, 7.00000f, 5.00000f, 11.0000f, - 5.50000f, 7.00000f, 10.0000f, 6.57446f, 7.00000f, 10.7423f, - 6.27560f, 7.11757f, 10.3239f, 6.60445f, 6.86590f, 10.2723f, - 5.50000f, 7.00000f, 10.0000f, 6.57446f, 7.00000f, 10.7423f, - 6.27560f, 6.88242f, 10.3238f, 6.60445f, 7.13409f, 10.2723f, - 5.50000f, 7.00000f, 10.0000f, 7.16443f, 9.24469f, 11.1499f, - 6.28292f, 7.66800f, 10.7168f, 7.00000f, 9.00000f, 11.0000f, }, - { 7.00000f, 2.80000f, 14.0000f, 7.52117f, 3.19943f, 12.9181f, - 6.75115f, 3.94166f, 11.6699f, 7.00000f, 5.00000f, 11.0000f, - 7.00000f, 2.80000f, 14.0000f, 6.26568f, 4.44134f, 11.5221f, - 6.29894f, 4.79533f, 11.4844f, 6.28350f, 6.33090f, 10.7170f, - 7.00000f, 2.80000f, 14.0000f, 6.26568f, 4.44134f, 11.5221f, - 7.22445f, 4.54771f, 11.1914f, 7.17220f, 4.74482f, 11.1553f, - 7.00000f, 2.80000f, 14.0000f, 5.74450f, 4.04191f, 12.6039f, - 7.19502f, 4.53322f, 11.1711f, 5.50000f, 7.00000f, 10.0000f, }, - { 7.00000f, 11.2000f, 14.0000f, 5.63935f, 10.1728f, 12.5557f, - 7.18618f, 9.45391f, 11.1650f, 5.50000f, 7.00000f, 10.0000f, - 7.00000f, 11.2000f, 14.0000f, 6.16481f, 9.76016f, 11.4710f, - 7.21561f, 9.43943f, 11.1853f, 7.16443f, 9.24469f, 11.1499f, - 7.00000f, 11.2000f, 14.0000f, 6.16481f, 9.76016f, 11.4710f, - 6.29730f, 9.21164f, 11.4888f, 6.28292f, 7.66800f, 10.7168f, - 7.00000f, 11.2000f, 14.0000f, 7.52546f, 10.7873f, 12.9152f, - 6.74950f, 10.0653f, 11.6744f, 7.00000f, 9.00000f, 11.0000f, }, - { 6.00000f, 4.00000f, 18.0000f, 6.17178f, 3.61615f, 18.9501f, - 7.00000f, 2.00000f, 20.0000f, 7.00000f, 2.00000f, 20.0000f, - 5.82238f, 3.78567f, 15.7951f, 5.99416f, 3.40183f, 16.7453f, - 6.36326f, 1.62520f, 19.4440f, 7.00000f, 2.00000f, 20.0000f, - 6.99191f, -0.0338653f, 16.2383f, 7.06775f, -0.0553456f, - 17.3163f, 6.99797f, -0.708357f, 18.6050f, 7.50000f, - -1.00000f, 19.0000f, 8.00000f, -2.00000f, 16.0000f, - 8.07584f, -2.02148f, 17.0779f, 7.50000f, -1.00000f, - 19.0000f, 7.50000f, -1.00000f, 19.0000f, }, - { 6.00000f, 4.00000f, 18.0000f, 5.82238f, 3.78567f, 15.7951f, - 6.99191f, -0.0338653f, 16.2383f, 8.00000f, -2.00000f, - 16.0000f, 5.77637f, 4.49967f, 16.7631f, 5.59875f, 4.28535f, - 14.5582f, 6.86100f, 0.00320839f, 14.3779f, 7.86909f, - -1.96292f, 14.1395f, 5.91173f, 4.38739f, 15.2929f, - 5.55206f, 2.71906f, 15.1499f, 6.48478f, 0.0405683f, - 12.5429f, 7.05435f, -1.47828f, 11.9935f, 6.00000f, - 4.00000f, 14.0000f, 5.64033f, 2.33167f, 13.8569f, 5.93043f, - 1.51884f, 11.5493f, 6.50000f, 0.00000f, 11.0000f, }, - { 6.00000f, 4.00000f, 14.0000f, 5.64033f, 2.33167f, 13.8569f, - 5.93043f, 1.51884f, 11.5493f, 6.50000f, 0.00000f, 11.0000f, - 6.00000f, 4.00000f, 14.0000f, 5.72105f, 1.97742f, 12.6746f, - 5.71956f, 2.08115f, 11.1714f, 6.28913f, 0.562308f, - 10.6220f, 6.00000f, 4.00000f, 14.0000f, 5.72105f, 1.97742f, - 12.6746f, 5.95300f, 2.11991f, 11.7283f, 6.50000f, 2.00000f, - 11.0000f, 6.00000f, 4.00000f, 14.0000f, 6.08071f, 3.64575f, - 12.8176f, 6.50000f, 2.00000f, 11.0000f, 6.50000f, 2.00000f, - 11.0000f, }, - { 7.00000f, 4.00000f, 18.0000f, 8.01538f, 2.03531f, 17.7856f, - 9.25405f, -1.57894f, 18.1668f, 9.00000f, -2.00000f, - 16.0000f, 7.27289f, 3.61974f, 18.9768f, 8.28828f, 1.65505f, - 18.7624f, 9.43541f, -1.59908f, 19.2725f, 9.18135f, - -2.02014f, 17.1057f, 8.50000f, 2.00000f, 20.0000f, - 8.88419f, 1.96998f, 19.2616f, 9.44404f, -0.614553f, - 18.6694f, 9.00000f, -1.00000f, 19.0000f, 8.50000f, - 2.00000f, 20.0000f, 8.50000f, 2.00000f, 20.0000f, 9.00000f, - -1.00000f, 19.0000f, 9.00000f, -1.00000f, 19.0000f, }, - { 9.00000f, -2.00000f, 16.0000f, 9.25405f, -1.57894f, 18.1668f, - 8.01538f, 2.03531f, 17.7856f, 7.00000f, 4.00000f, 18.0000f, - 8.69788f, -1.96644f, 14.1579f, 8.95194f, -1.54538f, - 16.3247f, 7.67925f, 2.50367f, 16.5825f, 6.66387f, 4.46835f, - 16.7968f, 7.84657f, -1.53663f, 12.0021f, 8.19383f, - -0.932816f, 13.5421f, 7.49810f, 2.87203f, 14.8067f, - 6.90135f, 4.38271f, 15.2742f, 7.50000f, 0.00000f, 11.0000f, - 7.84726f, 0.603817f, 12.5400f, 7.59675f, 2.48932f, - 13.5324f, 7.00000f, 4.00000f, 14.0000f, }, - { 7.50000f, 0.00000f, 11.0000f, 7.84726f, 0.603817f, 12.5400f, - 7.59675f, 2.48932f, 13.5324f, 7.00000f, 4.00000f, 14.0000f, - 7.50000f, 0.00000f, 11.0000f, 7.72215f, 1.15853f, 12.1782f, - 7.68730f, 2.13801f, 12.3627f, 7.09055f, 3.64868f, 12.8302f, - 7.50000f, 0.00000f, 11.0000f, 7.72215f, 1.15853f, 12.1782f, - 7.80364f, 2.19032f, 11.7976f, 7.50000f, 2.00000f, 11.0000f, - 7.50000f, 0.00000f, 11.0000f, 7.37488f, 0.554718f, - 10.6382f, 7.50000f, 2.00000f, 11.0000f, 7.50000f, 2.00000f, - 11.0000f, }, - { 6.00000f, 4.00000f, 18.0000f, 6.04662f, 4.05625f, 18.5787f, - 6.73343f, 4.51577f, 18.0562f, 7.00000f, 4.00000f, 18.0000f, - 6.17178f, 3.61615f, 18.9501f, 6.21840f, 3.67240f, 19.5288f, - 7.00633f, 4.13552f, 19.0331f, 7.27289f, 3.61974f, 18.9768f, - 7.00000f, 2.00000f, 20.0000f, 6.87917f, 2.69654f, 19.9349f, - 7.96063f, 3.27172f, 19.8070f, 8.50000f, 2.00000f, 20.0000f, - 7.00000f, 2.00000f, 20.0000f, 7.00000f, 2.00000f, 20.0000f, - 8.50000f, 2.00000f, 20.0000f, 8.50000f, 2.00000f, 20.0000f, }, - { 7.00000f, 2.00000f, 20.0000f, 7.00000f, 2.00000f, 20.0000f, - 8.50000f, 2.00000f, 20.0000f, 8.50000f, 2.00000f, 20.0000f, - 7.00000f, 2.00000f, 20.0000f, 7.42866f, 0.886911f, - 20.3024f, 8.71101f, 1.41080f, 20.1671f, 8.50000f, 2.00000f, - 20.0000f, 7.50000f, -1.00000f, 19.0000f, 7.50022f, - -0.462675f, 19.7526f, 8.73395f, -0.336419f, 19.9299f, - 9.00000f, -1.00000f, 19.0000f, 7.50000f, -1.00000f, - 19.0000f, 7.50000f, -1.00000f, 19.0000f, 9.00000f, - -1.00000f, 19.0000f, 9.00000f, -1.00000f, 19.0000f, }, - { 7.50000f, -1.00000f, 19.0000f, 7.50000f, -1.00000f, 19.0000f, - 9.00000f, -1.00000f, 19.0000f, 9.00000f, -1.00000f, - 19.0000f, 7.50000f, -1.00000f, 19.0000f, 7.86286f, - -1.90036f, 18.3200f, 8.92549f, -1.83744f, 18.4231f, - 9.00000f, -1.00000f, 19.0000f, 8.07584f, -2.02148f, - 17.0779f, 8.34116f, -2.53894f, 17.0151f, 9.11447f, - -2.13097f, 16.5353f, 9.18135f, -2.02014f, 17.1057f, - 8.00000f, -2.00000f, 16.0000f, 8.26531f, -2.51746f, - 15.9372f, 8.93312f, -2.11083f, 15.4296f, 9.00000f, - -2.00000f, 16.0000f, }, - { 8.00000f, -2.00000f, 16.0000f, 8.26531f, -2.51746f, 15.9372f, - 8.93312f, -2.11083f, 15.4296f, 9.00000f, -2.00000f, - 16.0000f, 7.86909f, -1.96292f, 14.1395f, 8.13441f, - -2.48038f, 14.0768f, 8.63100f, -2.07728f, 13.5875f, - 8.69788f, -1.96644f, 14.1579f, 7.05435f, -1.47828f, - 11.9935f, 7.24070f, -1.97520f, 11.8138f, 7.73145f, - -1.73680f, 11.4916f, 7.84657f, -1.53663f, 12.0021f, - 6.50000f, 0.00000f, 11.0000f, 6.68634f, -0.496919f, - 10.8202f, 7.38487f, -0.200171f, 10.4894f, 7.50000f, - 0.00000f, 11.0000f, }, - { 6.50000f, 0.00000f, 11.0000f, 6.68634f, -0.496919f, 10.8202f, - 7.38487f, -0.200171f, 10.4894f, 7.50000f, 0.00000f, - 11.0000f, 6.28913f, 0.562308f, 10.6220f, 6.47547f, - 0.0653883f, 10.4423f, 7.25976f, 0.354546f, 10.1277f, - 7.37488f, 0.554718f, 10.6382f, 6.50000f, 2.00000f, - 11.0000f, 6.58882f, 1.48812f, 10.5862f, 7.51059f, 1.62025f, - 10.6414f, 7.50000f, 2.00000f, 11.0000f, 6.50000f, 2.00000f, - 11.0000f, 6.50000f, 2.00000f, 11.0000f, 7.50000f, 2.00000f, - 11.0000f, 7.50000f, 2.00000f, 11.0000f, }, - { 6.50000f, 2.00000f, 11.0000f, 6.50000f, 2.00000f, 11.0000f, - 7.50000f, 2.00000f, 11.0000f, 7.50000f, 2.00000f, 11.0000f, - 6.50000f, 2.00000f, 11.0000f, 6.43274f, 2.93280f, 11.6403f, - 7.32203f, 3.04107f, 11.6851f, 7.50000f, 2.00000f, 11.0000f, - 6.08071f, 3.64575f, 12.8176f, 6.18741f, 4.14071f, 12.8601f, - 6.91115f, 4.10281f, 12.9708f, 7.09055f, 3.64868f, 12.8302f, - 6.00000f, 4.00000f, 14.0000f, 6.10670f, 4.49495f, 14.0424f, - 6.82060f, 4.45412f, 14.1405f, 7.00000f, 4.00000f, 14.0000f, }, - { 6.00000f, 4.00000f, 14.0000f, 6.10670f, 4.49495f, 14.0424f, - 6.82060f, 4.45412f, 14.1405f, 7.00000f, 4.00000f, 14.0000f, - 5.91173f, 4.38739f, 15.2929f, 6.01843f, 4.88234f, 15.3353f, - 6.72196f, 4.83683f, 15.4148f, 6.90135f, 4.38271f, 15.2742f, - 5.77637f, 4.49967f, 16.7631f, 5.82299f, 4.55593f, 17.3418f, - 6.39731f, 4.98413f, 16.8531f, 6.66387f, 4.46835f, 16.7968f, - 6.00000f, 4.00000f, 18.0000f, 6.04662f, 4.05625f, 18.5787f, - 6.73343f, 4.51577f, 18.0562f, 7.00000f, 4.00000f, 18.0000f, }, - { 6.00000f, 10.0000f, 18.0000f, 6.17178f, 10.3839f, 18.9501f, - 7.00000f, 12.0000f, 20.0000f, 7.00000f, 12.0000f, 20.0000f, - 5.82238f, 10.2143f, 15.7951f, 5.99416f, 10.5982f, 16.7453f, - 6.36326f, 12.3748f, 19.4440f, 7.00000f, 12.0000f, 20.0000f, - 6.99191f, 14.0339f, 16.2383f, 7.06775f, 14.0553f, 17.3163f, - 6.99797f, 14.7084f, 18.6050f, 7.50000f, 15.0000f, 19.0000f, - 8.00000f, 16.0000f, 16.0000f, 8.07584f, 16.0215f, 17.0779f, - 7.50000f, 15.0000f, 19.0000f, 7.50000f, 15.0000f, 19.0000f, }, - { 6.00000f, 10.0000f, 18.0000f, 5.82238f, 10.2143f, 15.7951f, - 6.99191f, 14.0339f, 16.2383f, 8.00000f, 16.0000f, 16.0000f, - 5.77637f, 9.50033f, 16.7631f, 5.59875f, 9.71465f, 14.5582f, - 6.86100f, 13.9968f, 14.3779f, 7.86909f, 15.9629f, 14.1395f, - 5.91173f, 9.61261f, 15.2929f, 5.55206f, 11.2809f, 15.1499f, - 6.48478f, 13.9594f, 12.5429f, 7.05435f, 15.4783f, 11.9935f, - 6.00000f, 10.0000f, 14.0000f, 5.64033f, 11.6683f, 13.8569f, - 5.93043f, 12.4812f, 11.5493f, 6.50000f, 14.0000f, 11.0000f, }, - { 6.00000f, 10.0000f, 14.0000f, 5.64033f, 11.6683f, 13.8569f, - 5.93043f, 12.4812f, 11.5493f, 6.50000f, 14.0000f, 11.0000f, - 6.00000f, 10.0000f, 14.0000f, 5.72105f, 12.0226f, 12.6746f, - 5.71956f, 11.9188f, 11.1714f, 6.28913f, 13.4377f, 10.6220f, - 6.00000f, 10.0000f, 14.0000f, 5.72105f, 12.0226f, 12.6746f, - 5.95300f, 11.8801f, 11.7283f, 6.50000f, 12.0000f, 11.0000f, - 6.00000f, 10.0000f, 14.0000f, 6.08071f, 10.3542f, 12.8176f, - 6.50000f, 12.0000f, 11.0000f, 6.50000f, 12.0000f, 11.0000f, }, - { 7.00000f, 10.0000f, 18.0000f, 8.01538f, 11.9647f, 17.7856f, - 9.25405f, 15.5789f, 18.1668f, 9.00000f, 16.0000f, 16.0000f, - 7.27289f, 10.3803f, 18.9768f, 8.28828f, 12.3449f, 18.7624f, - 9.43541f, 15.5991f, 19.2725f, 9.18135f, 16.0201f, 17.1057f, - 8.50000f, 12.0000f, 20.0000f, 8.88419f, 12.0300f, 19.2616f, - 9.44404f, 14.6146f, 18.6694f, 9.00000f, 15.0000f, 19.0000f, - 8.50000f, 12.0000f, 20.0000f, 8.50000f, 12.0000f, 20.0000f, - 9.00000f, 15.0000f, 19.0000f, 9.00000f, 15.0000f, 19.0000f, }, - { 9.00000f, 16.0000f, 16.0000f, 9.25405f, 15.5789f, 18.1668f, - 8.01538f, 11.9647f, 17.7856f, 7.00000f, 10.0000f, 18.0000f, - 8.69788f, 15.9664f, 14.1579f, 8.95194f, 15.5454f, 16.3247f, - 7.67925f, 11.4963f, 16.5825f, 6.66387f, 9.53165f, 16.7968f, - 7.84657f, 15.5366f, 12.0021f, 8.19383f, 14.9328f, 13.5421f, - 7.49810f, 11.1280f, 14.8067f, 6.90135f, 9.61729f, 15.2742f, - 7.50000f, 14.0000f, 11.0000f, 7.84726f, 13.3962f, 12.5400f, - 7.59675f, 11.5107f, 13.5324f, 7.00000f, 10.0000f, 14.0000f, }, - { 7.50000f, 14.0000f, 11.0000f, 7.84726f, 13.3962f, 12.5400f, - 7.59675f, 11.5107f, 13.5324f, 7.00000f, 10.0000f, 14.0000f, - 7.50000f, 14.0000f, 11.0000f, 7.72215f, 12.8415f, 12.1782f, - 7.68730f, 11.8620f, 12.3627f, 7.09055f, 10.3513f, 12.8302f, - 7.50000f, 14.0000f, 11.0000f, 7.72215f, 12.8415f, 12.1782f, - 7.80364f, 11.8097f, 11.7976f, 7.50000f, 12.0000f, 11.0000f, - 7.50000f, 14.0000f, 11.0000f, 7.37488f, 13.4453f, 10.6382f, - 7.50000f, 12.0000f, 11.0000f, 7.50000f, 12.0000f, 11.0000f, }, - { 6.00000f, 10.0000f, 18.0000f, 6.04662f, 9.94375f, 18.5787f, - 6.73343f, 9.48423f, 18.0562f, 7.00000f, 10.0000f, 18.0000f, - 6.17178f, 10.3839f, 18.9501f, 6.21840f, 10.3276f, 19.5288f, - 7.00633f, 9.86448f, 19.0331f, 7.27289f, 10.3803f, 18.9768f, - 7.00000f, 12.0000f, 20.0000f, 6.87917f, 11.3035f, 19.9349f, - 7.96063f, 10.7283f, 19.8070f, 8.50000f, 12.0000f, 20.0000f, - 7.00000f, 12.0000f, 20.0000f, 7.00000f, 12.0000f, 20.0000f, - 8.50000f, 12.0000f, 20.0000f, 8.50000f, 12.0000f, 20.0000f, }, - { 7.00000f, 12.0000f, 20.0000f, 7.00000f, 12.0000f, 20.0000f, - 8.50000f, 12.0000f, 20.0000f, 8.50000f, 12.0000f, 20.0000f, - 7.00000f, 12.0000f, 20.0000f, 7.42866f, 13.1131f, 20.3024f, - 8.71101f, 12.5892f, 20.1671f, 8.50000f, 12.0000f, 20.0000f, - 7.50000f, 15.0000f, 19.0000f, 7.50022f, 14.4627f, 19.7526f, - 8.73395f, 14.3364f, 19.9299f, 9.00000f, 15.0000f, 19.0000f, - 7.50000f, 15.0000f, 19.0000f, 7.50000f, 15.0000f, 19.0000f, - 9.00000f, 15.0000f, 19.0000f, 9.00000f, 15.0000f, 19.0000f, }, - { 7.50000f, 15.0000f, 19.0000f, 7.50000f, 15.0000f, 19.0000f, - 9.00000f, 15.0000f, 19.0000f, 9.00000f, 15.0000f, 19.0000f, - 7.50000f, 15.0000f, 19.0000f, 7.86286f, 15.9004f, 18.3200f, - 8.92549f, 15.8374f, 18.4231f, 9.00000f, 15.0000f, 19.0000f, - 8.07584f, 16.0215f, 17.0779f, 8.34116f, 16.5389f, 17.0151f, - 9.11447f, 16.1310f, 16.5353f, 9.18135f, 16.0201f, 17.1057f, - 8.00000f, 16.0000f, 16.0000f, 8.26531f, 16.5175f, 15.9372f, - 8.93312f, 16.1108f, 15.4296f, 9.00000f, 16.0000f, 16.0000f, }, - { 8.00000f, 16.0000f, 16.0000f, 8.26531f, 16.5175f, 15.9372f, - 8.93312f, 16.1108f, 15.4296f, 9.00000f, 16.0000f, 16.0000f, - 7.86909f, 15.9629f, 14.1395f, 8.13441f, 16.4804f, 14.0768f, - 8.63100f, 16.0773f, 13.5875f, 8.69788f, 15.9664f, 14.1579f, - 7.05435f, 15.4783f, 11.9935f, 7.24070f, 15.9752f, 11.8138f, - 7.73145f, 15.7368f, 11.4916f, 7.84657f, 15.5366f, 12.0021f, - 6.50000f, 14.0000f, 11.0000f, 6.68634f, 14.4969f, 10.8202f, - 7.38487f, 14.2002f, 10.4894f, 7.50000f, 14.0000f, 11.0000f, }, - { 6.50000f, 14.0000f, 11.0000f, 6.68634f, 14.4969f, 10.8202f, - 7.38487f, 14.2002f, 10.4894f, 7.50000f, 14.0000f, 11.0000f, - 6.28913f, 13.4377f, 10.6220f, 6.47547f, 13.9346f, 10.4423f, - 7.25976f, 13.6455f, 10.1277f, 7.37488f, 13.4453f, 10.6382f, - 6.50000f, 12.0000f, 11.0000f, 6.58882f, 12.5119f, 10.5862f, - 7.51059f, 12.3798f, 10.6414f, 7.50000f, 12.0000f, 11.0000f, - 6.50000f, 12.0000f, 11.0000f, 6.50000f, 12.0000f, 11.0000f, - 7.50000f, 12.0000f, 11.0000f, 7.50000f, 12.0000f, 11.0000f, }, - { 6.50000f, 12.0000f, 11.0000f, 6.50000f, 12.0000f, 11.0000f, - 7.50000f, 12.0000f, 11.0000f, 7.50000f, 12.0000f, 11.0000f, - 6.50000f, 12.0000f, 11.0000f, 6.43274f, 11.0672f, 11.6403f, - 7.32203f, 10.9589f, 11.6851f, 7.50000f, 12.0000f, 11.0000f, - 6.08071f, 10.3542f, 12.8176f, 6.18741f, 9.85929f, 12.8601f, - 6.91115f, 9.89719f, 12.9708f, 7.09055f, 10.3513f, 12.8302f, - 6.00000f, 10.0000f, 14.0000f, 6.10670f, 9.50505f, 14.0424f, - 6.82060f, 9.54588f, 14.1405f, 7.00000f, 10.0000f, 14.0000f, }, - { 6.00000f, 10.0000f, 14.0000f, 6.10670f, 9.50505f, 14.0424f, - 6.82060f, 9.54588f, 14.1405f, 7.00000f, 10.0000f, 14.0000f, - 5.91173f, 9.61261f, 15.2929f, 6.01843f, 9.11766f, 15.3353f, - 6.72196f, 9.16317f, 15.4148f, 6.90135f, 9.61729f, 15.2742f, - 5.77637f, 9.50033f, 16.7631f, 5.82299f, 9.44407f, 17.3418f, - 6.39731f, 9.01587f, 16.8531f, 6.66387f, 9.53165f, 16.7968f, - 6.00000f, 10.0000f, 18.0000f, 6.04662f, 9.94375f, 18.5787f, - 6.73343f, 9.48423f, 18.0562f, 7.00000f, 10.0000f, 18.0000f, }, - { 6.82446f, 3.34549f, 10.8660f, 6.82446f, 3.34549f, 10.8660f, - 5.66811f, 2.92579f, 9.10044f, 4.82907f, 2.69715f, 8.50000f, - 6.83869f, 3.07901f, 11.1571f, 6.17186f, 2.86235f, 10.3664f, - 5.62209f, 2.62022f, 9.33980f, 4.78305f, 2.39159f, 8.73936f, - 6.49405f, 2.96703f, 11.4410f, 5.82862f, 2.75082f, 10.6519f, - 5.35977f, 2.53062f, 9.79425f, 4.57038f, 2.32249f, 9.12667f, - 6.34893f, 3.19098f, 11.7321f, 6.34893f, 3.19098f, 11.7321f, - 5.14293f, 2.75078f, 10.0336f, 4.35354f, 2.54264f, 9.36603f, }, - { 6.34893f, 3.19098f, 11.7321f, 6.34893f, 3.19098f, 11.7321f, - 5.14293f, 2.75078f, 10.0336f, 4.35354f, 2.54264f, 9.36603f, - 6.07309f, 3.39608f, 11.9085f, 5.40766f, 3.17987f, 11.1194f, - 4.92723f, 2.96978f, 10.2717f, 4.13785f, 2.76165f, 9.60412f, - 5.93731f, 3.81398f, 11.9085f, 5.27188f, 3.59776f, 11.1194f, - 4.75971f, 3.48534f, 10.2717f, 3.99875f, 3.18974f, 9.60412f, - 6.03992f, 4.14204f, 11.7321f, 6.03992f, 4.14204f, 11.7321f, - 4.80549f, 3.78930f, 10.0336f, 4.04453f, 3.49370f, 9.36603f, }, - { 6.03992f, 4.14204f, 11.7321f, 6.03992f, 4.14204f, 11.7321f, - 4.80549f, 3.78930f, 10.0336f, 4.04453f, 3.49370f, 9.36603f, - 6.02569f, 4.40851f, 11.4410f, 5.36025f, 4.19230f, 10.6519f, - 4.85151f, 4.09486f, 9.79425f, 4.09055f, 3.79926f, 9.12667f, - 6.37033f, 4.52049f, 11.1571f, 5.70350f, 4.30383f, 10.3664f, - 5.11641f, 4.17656f, 9.33980f, 4.30322f, 3.86836f, 8.73936f, - 6.51544f, 4.29655f, 10.8660f, 6.51544f, 4.29655f, 10.8660f, - 5.33324f, 3.95640f, 9.10044f, 4.52006f, 3.64821f, 8.50000f, }, - { 6.51544f, 4.29655f, 10.8660f, 6.51544f, 4.29655f, 10.8660f, - 5.33324f, 3.95640f, 9.10044f, 4.52006f, 3.64821f, 8.50000f, - 6.79128f, 4.09145f, 10.6896f, 6.12445f, 3.87478f, 9.89891f, - 5.54985f, 3.73649f, 8.86133f, 4.73666f, 3.42829f, 8.26090f, - 6.92707f, 3.67355f, 10.6896f, 6.26023f, 3.45689f, 9.89891f, - 5.71408f, 3.23103f, 8.86133f, 4.87505f, 3.00239f, 8.26090f, - 6.82446f, 3.34549f, 10.8660f, 6.82446f, 3.34549f, 10.8660f, - 5.66811f, 2.92579f, 9.10044f, 4.82907f, 2.69715f, 8.50000f, }, - { 4.82907f, 2.69715f, 8.50000f, 4.01638f, 2.47569f, 7.91842f, - 2.04314f, 2.05481f, 7.43301f, 2.04314f, 2.05481f, 7.43301f, - 4.78305f, 2.39159f, 8.73936f, 3.97036f, 2.17013f, 8.15778f, - 2.94601f, 2.05055f, 7.80752f, 2.04314f, 2.05481f, 7.43301f, - 4.57038f, 2.32249f, 9.12667f, 3.81921f, 2.12442f, 8.49139f, - 2.70969f, 1.96031f, 8.29323f, 1.80537f, 1.97756f, 7.86603f, - 4.35354f, 2.54264f, 9.36603f, 3.60237f, 2.34458f, 8.73075f, - 1.80537f, 1.97756f, 7.86603f, 1.80537f, 1.97756f, 7.86603f, }, - { 4.35354f, 2.54264f, 9.36603f, 3.60237f, 2.34458f, 8.73075f, - 1.80537f, 1.97756f, 7.86603f, 1.80537f, 1.97756f, 7.86603f, - 4.13785f, 2.76165f, 9.60412f, 3.38667f, 2.56358f, 8.96885f, - 2.49788f, 2.17525f, 8.52685f, 1.80537f, 1.97756f, 7.86603f, - 3.99875f, 3.18974f, 9.60412f, 3.27462f, 2.90844f, 8.96886f, - 2.32732f, 2.70019f, 8.52685f, 1.65086f, 2.45308f, 7.86603f, - 4.04453f, 3.49370f, 9.36603f, 3.32040f, 3.21241f, 8.73076f, - 1.65086f, 2.45308f, 7.86603f, 1.65086f, 2.45308f, 7.86603f, }, - { 4.04453f, 3.49370f, 9.36603f, 3.32040f, 3.21241f, 8.73076f, - 1.65086f, 2.45308f, 7.86603f, 1.65086f, 2.45308f, 7.86603f, - 4.09055f, 3.79926f, 9.12667f, 3.36642f, 3.51797f, 8.49140f, - 2.37234f, 2.99857f, 8.29323f, 1.65086f, 2.45308f, 7.86603f, - 4.30322f, 3.86836f, 8.73936f, 3.51557f, 3.56984f, 8.15778f, - 2.61656f, 3.06448f, 7.80752f, 1.88863f, 2.53034f, 7.43301f, - 4.52006f, 3.64821f, 8.50000f, 3.73241f, 3.34968f, 7.91842f, - 1.88863f, 2.53034f, 7.43301f, 1.88863f, 2.53034f, 7.43301f, }, - { 4.52006f, 3.64821f, 8.50000f, 3.73241f, 3.34968f, 7.91842f, - 1.88863f, 2.53034f, 7.43301f, 1.88863f, 2.53034f, 7.43301f, - 4.73666f, 3.42829f, 8.26090f, 3.94901f, 3.12976f, 7.67932f, - 2.82260f, 2.85571f, 7.58047f, 1.88863f, 2.53034f, 7.43301f, - 4.87505f, 3.00239f, 8.26090f, 4.06236f, 2.78093f, 7.67932f, - 2.98998f, 2.34056f, 7.58047f, 2.04314f, 2.05481f, 7.43301f, - 4.82907f, 2.69715f, 8.50000f, 4.01638f, 2.47569f, 7.91842f, - 2.04314f, 2.05481f, 7.43301f, 2.04314f, 2.05481f, 7.43301f, }, - { 1.80537f, 1.97756f, 7.86603f, 1.80537f, 1.97756f, 7.86603f, - 2.04314f, 2.05481f, 7.43301f, 2.04314f, 2.05481f, 7.43301f, - 1.80537f, 1.97756f, 7.86603f, 1.75245f, 1.96950f, 7.82668f, - 1.94375f, 2.04371f, 7.40157f, 2.04314f, 2.05481f, 7.43301f, - 1.65086f, 2.45308f, 7.86603f, 1.60331f, 2.42850f, 7.82668f, - 1.80169f, 2.48090f, 7.40157f, 1.88863f, 2.53034f, 7.43301f, - 1.65086f, 2.45308f, 7.86603f, 1.65086f, 2.45308f, 7.86603f, - 1.88863f, 2.53034f, 7.43301f, 1.88863f, 2.53034f, 7.43301f, }, - { 6.82446f, 9.15451f, 10.8660f, 6.82446f, 9.15451f, 10.8660f, - 5.64226f, 9.49465f, 9.10044f, 4.82907f, 9.80285f, 8.50000f, - 6.67934f, 8.93056f, 11.1571f, 6.01251f, 9.14723f, 10.3664f, - 5.42542f, 9.27449f, 9.33980f, 4.61223f, 9.58269f, 8.73936f, - 6.33470f, 9.04254f, 11.4410f, 5.66927f, 9.25875f, 10.6519f, - 5.16053f, 9.35619f, 9.79425f, 4.39957f, 9.65179f, 9.12667f, - 6.34893f, 9.30902f, 11.7321f, 6.34893f, 9.30902f, 11.7321f, - 5.11451f, 9.66176f, 10.0336f, 4.35354f, 9.95736f, 9.36603f, }, - { 6.34893f, 9.30902f, 11.7321f, 6.34893f, 9.30902f, 11.7321f, - 5.11451f, 9.66176f, 10.0336f, 4.35354f, 9.95736f, 9.36603f, - 6.24633f, 9.63708f, 11.9085f, 5.58090f, 9.85329f, 11.1194f, - 5.06873f, 9.96572f, 10.2717f, 4.30777f, 10.2613f, 9.60412f, - 6.38211f, 10.0550f, 11.9085f, 5.71668f, 10.2712f, 11.1194f, - 5.23624f, 10.4813f, 10.2717f, 4.44686f, 10.6894f, 9.60412f, - 6.65795f, 10.2601f, 11.7321f, 6.65795f, 10.2601f, 11.7321f, - 5.45194f, 10.7003f, 10.0336f, 4.66256f, 10.9084f, 9.36603f, }, - { 6.65795f, 10.2601f, 11.7321f, 6.65795f, 10.2601f, 11.7321f, - 5.45194f, 10.7003f, 10.0336f, 4.66256f, 10.9084f, 9.36603f, - 6.80307f, 10.4840f, 11.4410f, 6.13764f, 10.7002f, 10.6519f, - 5.66878f, 10.9204f, 9.79425f, 4.87940f, 11.1286f, 9.12667f, - 7.14771f, 10.3720f, 11.1571f, 6.48088f, 10.5887f, 10.3664f, - 5.93110f, 10.8308f, 9.33980f, 5.09207f, 11.0595f, 8.73936f, - 7.13348f, 10.1056f, 10.8660f, 7.13348f, 10.1056f, 10.8660f, - 5.97713f, 10.5253f, 9.10044f, 5.13809f, 10.7539f, 8.50000f, }, - { 7.13348f, 10.1056f, 10.8660f, 7.13348f, 10.1056f, 10.8660f, - 5.97713f, 10.5253f, 9.10044f, 5.13809f, 10.7539f, 8.50000f, - 7.23608f, 9.77750f, 10.6896f, 6.56925f, 9.99417f, 9.89891f, - 6.02310f, 10.2200f, 8.86133f, 5.18406f, 10.4487f, 8.26090f, - 7.10030f, 9.35961f, 10.6896f, 6.43347f, 9.57627f, 9.89891f, - 5.85887f, 9.71457f, 8.86133f, 5.04568f, 10.0228f, 8.26090f, - 6.82446f, 9.15451f, 10.8660f, 6.82446f, 9.15451f, 10.8660f, - 5.64226f, 9.49465f, 9.10044f, 4.82907f, 9.80285f, 8.50000f, }, - { 4.82907f, 9.80285f, 8.50000f, 4.04142f, 10.1014f, 7.91842f, - 2.19765f, 10.9207f, 7.43301f, 2.19765f, 10.9207f, 7.43301f, - 4.61223f, 9.58269f, 8.73936f, 3.82458f, 9.88122f, 8.15778f, - 2.92558f, 10.3866f, 7.80752f, 2.19765f, 10.9207f, 7.43301f, - 4.39957f, 9.65179f, 9.12667f, 3.67543f, 9.93309f, 8.49139f, - 2.68136f, 10.4525f, 8.29323f, 1.95988f, 10.9980f, 7.86603f, - 4.35354f, 9.95736f, 9.36603f, 3.62941f, 10.2387f, 8.73075f, - 1.95988f, 10.9980f, 7.86603f, 1.95988f, 10.9980f, 7.86603f, }, - { 4.35354f, 9.95736f, 9.36603f, 3.62941f, 10.2387f, 8.73075f, - 1.95988f, 10.9980f, 7.86603f, 1.95988f, 10.9980f, 7.86603f, - 4.30777f, 10.2613f, 9.60412f, 3.58363f, 10.5426f, 8.96885f, - 2.63633f, 10.7509f, 8.52685f, 1.95988f, 10.9980f, 7.86603f, - 4.44686f, 10.6894f, 9.60412f, 3.69569f, 10.8875f, 8.96886f, - 2.80690f, 11.2758f, 8.52685f, 2.11439f, 11.4735f, 7.86603f, - 4.66256f, 10.9084f, 9.36603f, 3.91139f, 11.1065f, 8.73076f, - 2.11439f, 11.4735f, 7.86603f, 2.11439f, 11.4735f, 7.86603f, }, - { 4.66256f, 10.9084f, 9.36603f, 3.91139f, 11.1065f, 8.73076f, - 2.11439f, 11.4735f, 7.86603f, 2.11439f, 11.4735f, 7.86603f, - 4.87940f, 11.1286f, 9.12667f, 4.12823f, 11.3266f, 8.49140f, - 3.01871f, 11.4907f, 8.29323f, 2.11439f, 11.4735f, 7.86603f, - 5.09207f, 11.0595f, 8.73936f, 4.27938f, 11.2809f, 8.15778f, - 3.25502f, 11.4005f, 7.80752f, 2.35215f, 11.3962f, 7.43301f, - 5.13809f, 10.7539f, 8.50000f, 4.32540f, 10.9754f, 7.91842f, - 2.35215f, 11.3962f, 7.43301f, 2.35215f, 11.3962f, 7.43301f, }, - { 5.13809f, 10.7539f, 8.50000f, 4.32540f, 10.9754f, 7.91842f, - 2.35215f, 11.3962f, 7.43301f, 2.35215f, 11.3962f, 7.43301f, - 5.18406f, 10.4487f, 8.26090f, 4.37137f, 10.6701f, 7.67932f, - 3.29900f, 11.1105f, 7.58047f, 2.35215f, 11.3962f, 7.43301f, - 5.04568f, 10.0228f, 8.26090f, 4.25803f, 10.3213f, 7.67932f, - 3.13162f, 10.5954f, 7.58047f, 2.19765f, 10.9207f, 7.43301f, - 4.82907f, 9.80285f, 8.50000f, 4.04142f, 10.1014f, 7.91842f, - 2.19765f, 10.9207f, 7.43301f, 2.19765f, 10.9207f, 7.43301f, }, - { 1.95988f, 10.9980f, 7.86603f, 1.95988f, 10.9980f, 7.86603f, - 2.19765f, 10.9207f, 7.43301f, 2.19765f, 10.9207f, 7.43301f, - 1.95988f, 10.9980f, 7.86603f, 1.91233f, 11.0226f, 7.82668f, - 2.11071f, 10.9702f, 7.40157f, 2.19765f, 10.9207f, 7.43301f, - 2.11439f, 11.4735f, 7.86603f, 2.06147f, 11.4816f, 7.82668f, - 2.25276f, 11.4074f, 7.40157f, 2.35215f, 11.3962f, 7.43301f, - 2.11439f, 11.4735f, 7.86603f, 2.11439f, 11.4735f, 7.86603f, - 2.35215f, 11.3962f, 7.43301f, 2.35215f, 11.3962f, 7.43301f, }, }; - - public Gumbo() { - super(PATCHES); - } -} diff --git a/src/org/sunflow/core/tesselatable/Teapot.java b/src/org/sunflow/core/tesselatable/Teapot.java deleted file mode 100644 index 9dd5739..0000000 --- a/src/org/sunflow/core/tesselatable/Teapot.java +++ /dev/null @@ -1,236 +0,0 @@ -package org.sunflow.core.tesselatable; - -public class Teapot extends BezierMesh { - // teapot data, from: http://www.cs.ucsb.edu/~cs280/winter2004/hw2/ - private static final float[][] PATCHES = { - { -80.00f, 0.00f, 30.00f, -80.00f, -44.80f, 30.00f, -44.80f, - -80.00f, 30.00f, 0.00f, -80.00f, 30.00f, -80.00f, 0.00f, - 12.00f, -80.00f, -44.80f, 12.00f, -44.80f, -80.00f, 12.00f, - 0.00f, -80.00f, 12.00f, -60.00f, 0.00f, 3.00f, -60.00f, - -33.60f, 3.00f, -33.60f, -60.00f, 3.00f, 0.00f, -60.00f, - 3.00f, -60.00f, 0.00f, 0.00f, -60.00f, -33.60f, 0.00f, - -33.60f, -60.00f, 0.00f, 0.00f, -60.00f, 0.00f, }, - { 0.00f, -80.00f, 30.00f, 44.80f, -80.00f, 30.00f, 80.00f, -44.80f, - 30.00f, 80.00f, 0.00f, 30.00f, 0.00f, -80.00f, 12.00f, - 44.80f, -80.00f, 12.00f, 80.00f, -44.80f, 12.00f, 80.00f, - 0.00f, 12.00f, 0.00f, -60.00f, 3.00f, 33.60f, -60.00f, - 3.00f, 60.00f, -33.60f, 3.00f, 60.00f, 0.00f, 3.00f, 0.00f, - -60.00f, 0.00f, 33.60f, -60.00f, 0.00f, 60.00f, -33.60f, - 0.00f, 60.00f, 0.00f, 0.00f, }, - { -60.00f, 0.00f, 90.00f, -60.00f, -33.60f, 90.00f, -33.60f, - -60.00f, 90.00f, 0.00f, -60.00f, 90.00f, -70.00f, 0.00f, - 69.00f, -70.00f, -39.20f, 69.00f, -39.20f, -70.00f, 69.00f, - 0.00f, -70.00f, 69.00f, -80.00f, 0.00f, 48.00f, -80.00f, - -44.80f, 48.00f, -44.80f, -80.00f, 48.00f, 0.00f, -80.00f, - 48.00f, -80.00f, 0.00f, 30.00f, -80.00f, -44.80f, 30.00f, - -44.80f, -80.00f, 30.00f, 0.00f, -80.00f, 30.00f, }, - { 0.00f, -60.00f, 90.00f, 33.60f, -60.00f, 90.00f, 60.00f, -33.60f, - 90.00f, 60.00f, 0.00f, 90.00f, 0.00f, -70.00f, 69.00f, - 39.20f, -70.00f, 69.00f, 70.00f, -39.20f, 69.00f, 70.00f, - 0.00f, 69.00f, 0.00f, -80.00f, 48.00f, 44.80f, -80.00f, - 48.00f, 80.00f, -44.80f, 48.00f, 80.00f, 0.00f, 48.00f, - 0.00f, -80.00f, 30.00f, 44.80f, -80.00f, 30.00f, 80.00f, - -44.80f, 30.00f, 80.00f, 0.00f, 30.00f, }, - { -56.00f, 0.00f, 90.00f, -56.00f, -31.36f, 90.00f, -31.36f, - -56.00f, 90.00f, 0.00f, -56.00f, 90.00f, -53.50f, 0.00f, - 95.25f, -53.50f, -29.96f, 95.25f, -29.96f, -53.50f, 95.25f, - 0.00f, -53.50f, 95.25f, -57.50f, 0.00f, 95.25f, -57.50f, - -32.20f, 95.25f, -32.20f, -57.50f, 95.25f, 0.00f, -57.50f, - 95.25f, -60.00f, 0.00f, 90.00f, -60.00f, -33.60f, 90.00f, - -33.60f, -60.00f, 90.00f, 0.00f, -60.00f, 90.00f, }, - { 0.00f, -56.00f, 90.00f, 31.36f, -56.00f, 90.00f, 56.00f, -31.36f, - 90.00f, 56.00f, 0.00f, 90.00f, 0.00f, -53.50f, 95.25f, - 29.96f, -53.50f, 95.25f, 53.50f, -29.96f, 95.25f, 53.50f, - 0.00f, 95.25f, 0.00f, -57.50f, 95.25f, 32.20f, -57.50f, - 95.25f, 57.50f, -32.20f, 95.25f, 57.50f, 0.00f, 95.25f, - 0.00f, -60.00f, 90.00f, 33.60f, -60.00f, 90.00f, 60.00f, - -33.60f, 90.00f, 60.00f, 0.00f, 90.00f, }, - { 80.00f, 0.00f, 30.00f, 80.00f, 44.80f, 30.00f, 44.80f, 80.00f, - 30.00f, 0.00f, 80.00f, 30.00f, 80.00f, 0.00f, 12.00f, - 80.00f, 44.80f, 12.00f, 44.80f, 80.00f, 12.00f, 0.00f, - 80.00f, 12.00f, 60.00f, 0.00f, 3.00f, 60.00f, 33.60f, - 3.00f, 33.60f, 60.00f, 3.00f, 0.00f, 60.00f, 3.00f, 60.00f, - 0.00f, 0.00f, 60.00f, 33.60f, 0.00f, 33.60f, 60.00f, 0.00f, - 0.00f, 60.00f, 0.00f, }, - { 0.00f, 80.00f, 30.00f, -44.80f, 80.00f, 30.00f, -80.00f, 44.80f, - 30.00f, -80.00f, 0.00f, 30.00f, 0.00f, 80.00f, 12.00f, - -44.80f, 80.00f, 12.00f, -80.00f, 44.80f, 12.00f, -80.00f, - 0.00f, 12.00f, 0.00f, 60.00f, 3.00f, -33.60f, 60.00f, - 3.00f, -60.00f, 33.60f, 3.00f, -60.00f, 0.00f, 3.00f, - 0.00f, 60.00f, 0.00f, -33.60f, 60.00f, 0.00f, -60.00f, - 33.60f, 0.00f, -60.00f, 0.00f, 0.00f, }, - { 60.00f, 0.00f, 90.00f, 60.00f, 33.60f, 90.00f, 33.60f, 60.00f, - 90.00f, 0.00f, 60.00f, 90.00f, 70.00f, 0.00f, 69.00f, - 70.00f, 39.20f, 69.00f, 39.20f, 70.00f, 69.00f, 0.00f, - 70.00f, 69.00f, 80.00f, 0.00f, 48.00f, 80.00f, 44.80f, - 48.00f, 44.80f, 80.00f, 48.00f, 0.00f, 80.00f, 48.00f, - 80.00f, 0.00f, 30.00f, 80.00f, 44.80f, 30.00f, 44.80f, - 80.00f, 30.00f, 0.00f, 80.00f, 30.00f, }, - { 0.00f, 60.00f, 90.00f, -33.60f, 60.00f, 90.00f, -60.00f, 33.60f, - 90.00f, -60.00f, 0.00f, 90.00f, 0.00f, 70.00f, 69.00f, - -39.20f, 70.00f, 69.00f, -70.00f, 39.20f, 69.00f, -70.00f, - 0.00f, 69.00f, 0.00f, 80.00f, 48.00f, -44.80f, 80.00f, - 48.00f, -80.00f, 44.80f, 48.00f, -80.00f, 0.00f, 48.00f, - 0.00f, 80.00f, 30.00f, -44.80f, 80.00f, 30.00f, -80.00f, - 44.80f, 30.00f, -80.00f, 0.00f, 30.00f, }, - { 56.00f, 0.00f, 90.00f, 56.00f, 31.36f, 90.00f, 31.36f, 56.00f, - 90.00f, 0.00f, 56.00f, 90.00f, 53.50f, 0.00f, 95.25f, - 53.50f, 29.96f, 95.25f, 29.96f, 53.50f, 95.25f, 0.00f, - 53.50f, 95.25f, 57.50f, 0.00f, 95.25f, 57.50f, 32.20f, - 95.25f, 32.20f, 57.50f, 95.25f, 0.00f, 57.50f, 95.25f, - 60.00f, 0.00f, 90.00f, 60.00f, 33.60f, 90.00f, 33.60f, - 60.00f, 90.00f, 0.00f, 60.00f, 90.00f, }, - { 0.00f, 56.00f, 90.00f, -31.36f, 56.00f, 90.00f, -56.00f, 31.36f, - 90.00f, -56.00f, 0.00f, 90.00f, 0.00f, 53.50f, 95.25f, - -29.96f, 53.50f, 95.25f, -53.50f, 29.96f, 95.25f, -53.50f, - 0.00f, 95.25f, 0.00f, 57.50f, 95.25f, -32.20f, 57.50f, - 95.25f, -57.50f, 32.20f, 95.25f, -57.50f, 0.00f, 95.25f, - 0.00f, 60.00f, 90.00f, -33.60f, 60.00f, 90.00f, -60.00f, - 33.60f, 90.00f, -60.00f, 0.00f, 90.00f, }, - { -64.00f, 0.00f, 75.00f, -64.00f, 12.00f, 75.00f, -60.00f, 12.00f, - 84.00f, -60.00f, 0.00f, 84.00f, -92.00f, 0.00f, 75.00f, - -92.00f, 12.00f, 75.00f, -100.00f, 12.00f, 84.00f, - -100.00f, 0.00f, 84.00f, -108.00f, 0.00f, 75.00f, -108.00f, - 12.00f, 75.00f, -120.00f, 12.00f, 84.00f, -120.00f, 0.00f, - 84.00f, -108.00f, 0.00f, 66.00f, -108.00f, 12.00f, 66.00f, - -120.00f, 12.00f, 66.00f, -120.00f, 0.00f, 66.00f, }, - { -60.00f, 0.00f, 84.00f, -60.00f, -12.00f, 84.00f, -64.00f, - -12.00f, 75.00f, -64.00f, 0.00f, 75.00f, -100.00f, 0.00f, - 84.00f, -100.00f, -12.00f, 84.00f, -92.00f, -12.00f, - 75.00f, -92.00f, 0.00f, 75.00f, -120.00f, 0.00f, 84.00f, - -120.00f, -12.00f, 84.00f, -108.00f, -12.00f, 75.00f, - -108.00f, 0.00f, 75.00f, -120.00f, 0.00f, 66.00f, -120.00f, - -12.00f, 66.00f, -108.00f, -12.00f, 66.00f, -108.00f, - 0.00f, 66.00f, }, - { -108.00f, 0.00f, 66.00f, -108.00f, 12.00f, 66.00f, -120.00f, - 12.00f, 66.00f, -120.00f, 0.00f, 66.00f, -108.00f, 0.00f, - 57.00f, -108.00f, 12.00f, 57.00f, -120.00f, 12.00f, 48.00f, - -120.00f, 0.00f, 48.00f, -100.00f, 0.00f, 39.00f, -100.00f, - 12.00f, 39.00f, -106.00f, 12.00f, 31.50f, -106.00f, 0.00f, - 31.50f, -80.00f, 0.00f, 30.00f, -80.00f, 12.00f, 30.00f, - -76.00f, 12.00f, 18.00f, -76.00f, 0.00f, 18.00f, }, - { -120.00f, 0.00f, 66.00f, -120.00f, -12.00f, 66.00f, -108.00f, - -12.00f, 66.00f, -108.00f, 0.00f, 66.00f, -120.00f, 0.00f, - 48.00f, -120.00f, -12.00f, 48.00f, -108.00f, -12.00f, - 57.00f, -108.00f, 0.00f, 57.00f, -106.00f, 0.00f, 31.50f, - -106.00f, -12.00f, 31.50f, -100.00f, -12.00f, 39.00f, - -100.00f, 0.00f, 39.00f, -76.00f, 0.00f, 18.00f, -76.00f, - -12.00f, 18.00f, -80.00f, -12.00f, 30.00f, -80.00f, 0.00f, - 30.00f, }, - { 68.00f, 0.00f, 51.00f, 68.00f, 26.40f, 51.00f, 68.00f, 26.40f, - 18.00f, 68.00f, 0.00f, 18.00f, 104.00f, 0.00f, 51.00f, - 104.00f, 26.40f, 51.00f, 124.00f, 26.40f, 27.00f, 124.00f, - 0.00f, 27.00f, 92.00f, 0.00f, 78.00f, 92.00f, 10.00f, - 78.00f, 96.00f, 10.00f, 75.00f, 96.00f, 0.00f, 75.00f, - 108.00f, 0.00f, 90.00f, 108.00f, 10.00f, 90.00f, 132.00f, - 10.00f, 90.00f, 132.00f, 0.00f, 90.00f, }, - { 68.00f, 0.00f, 18.00f, 68.00f, -26.40f, 18.00f, 68.00f, -26.40f, - 51.00f, 68.00f, 0.00f, 51.00f, 124.00f, 0.00f, 27.00f, - 124.00f, -26.40f, 27.00f, 104.00f, -26.40f, 51.00f, - 104.00f, 0.00f, 51.00f, 96.00f, 0.00f, 75.00f, 96.00f, - -10.00f, 75.00f, 92.00f, -10.00f, 78.00f, 92.00f, 0.00f, - 78.00f, 132.00f, 0.00f, 90.00f, 132.00f, -10.00f, 90.00f, - 108.00f, -10.00f, 90.00f, 108.00f, 0.00f, 90.00f, }, - { 108.00f, 0.00f, 90.00f, 108.00f, 10.00f, 90.00f, 132.00f, 10.00f, - 90.00f, 132.00f, 0.00f, 90.00f, 112.00f, 0.00f, 93.00f, - 112.00f, 10.00f, 93.00f, 141.00f, 10.00f, 93.75f, 141.00f, - 0.00f, 93.75f, 116.00f, 0.00f, 93.00f, 116.00f, 6.00f, - 93.00f, 138.00f, 6.00f, 94.50f, 138.00f, 0.00f, 94.50f, - 112.00f, 0.00f, 90.00f, 112.00f, 6.00f, 90.00f, 128.00f, - 6.00f, 90.00f, 128.00f, 0.00f, 90.00f, }, - { 132.00f, 0.00f, 90.00f, 132.00f, -10.00f, 90.00f, 108.00f, - -10.00f, 90.00f, 108.00f, 0.00f, 90.00f, 141.00f, 0.00f, - 93.75f, 141.00f, -10.00f, 93.75f, 112.00f, -10.00f, 93.00f, - 112.00f, 0.00f, 93.00f, 138.00f, 0.00f, 94.50f, 138.00f, - -6.00f, 94.50f, 116.00f, -6.00f, 93.00f, 116.00f, 0.00f, - 93.00f, 128.00f, 0.00f, 90.00f, 128.00f, -6.00f, 90.00f, - 112.00f, -6.00f, 90.00f, 112.00f, 0.00f, 90.00f, }, - { 50.00f, 0.00f, 90.00f, 50.00f, 28.00f, 90.00f, 28.00f, 50.00f, - 90.00f, 0.00f, 50.00f, 90.00f, 52.00f, 0.00f, 90.00f, - 52.00f, 29.12f, 90.00f, 29.12f, 52.00f, 90.00f, 0.00f, - 52.00f, 90.00f, 54.00f, 0.00f, 90.00f, 54.00f, 30.24f, - 90.00f, 30.24f, 54.00f, 90.00f, 0.00f, 54.00f, 90.00f, - 56.00f, 0.00f, 90.00f, 56.00f, 31.36f, 90.00f, 31.36f, - 56.00f, 90.00f, 0.00f, 56.00f, 90.00f, }, - { 0.00f, 50.00f, 90.00f, -28.00f, 50.00f, 90.00f, -50.00f, 28.00f, - 90.00f, -50.00f, 0.00f, 90.00f, 0.00f, 52.00f, 90.00f, - -29.12f, 52.00f, 90.00f, -52.00f, 29.12f, 90.00f, -52.00f, - 0.00f, 90.00f, 0.00f, 54.00f, 90.00f, -30.24f, 54.00f, - 90.00f, -54.00f, 30.24f, 90.00f, -54.00f, 0.00f, 90.00f, - 0.00f, 56.00f, 90.00f, -31.36f, 56.00f, 90.00f, -56.00f, - 31.36f, 90.00f, -56.00f, 0.00f, 90.00f, }, - { -50.00f, 0.00f, 90.00f, -50.00f, -28.00f, 90.00f, -28.00f, - -50.00f, 90.00f, 0.00f, -50.00f, 90.00f, -52.00f, 0.00f, - 90.00f, -52.00f, -29.12f, 90.00f, -29.12f, -52.00f, 90.00f, - 0.00f, -52.00f, 90.00f, -54.00f, 0.00f, 90.00f, -54.00f, - -30.24f, 90.00f, -30.24f, -54.00f, 90.00f, 0.00f, -54.00f, - 90.00f, -56.00f, 0.00f, 90.00f, -56.00f, -31.36f, 90.00f, - -31.36f, -56.00f, 90.00f, 0.00f, -56.00f, 90.00f, }, - { 0.00f, -50.00f, 90.00f, 28.00f, -50.00f, 90.00f, 50.00f, -28.00f, - 90.00f, 50.00f, 0.00f, 90.00f, 0.00f, -52.00f, 90.00f, - 29.12f, -52.00f, 90.00f, 52.00f, -29.12f, 90.00f, 52.00f, - 0.00f, 90.00f, 0.00f, -54.00f, 90.00f, 30.24f, -54.00f, - 90.00f, 54.00f, -30.24f, 90.00f, 54.00f, 0.00f, 90.00f, - 0.00f, -56.00f, 90.00f, 31.36f, -56.00f, 90.00f, 56.00f, - -31.36f, 90.00f, 56.00f, 0.00f, 90.00f, }, - { 8.00f, 0.00f, 102.00f, 8.00f, 4.48f, 102.00f, 4.48f, 8.00f, - 102.00f, 0.00f, 8.00f, 102.00f, 16.00f, 0.00f, 96.00f, - 16.00f, 8.96f, 96.00f, 8.96f, 16.00f, 96.00f, 0.00f, - 16.00f, 96.00f, 52.00f, 0.00f, 96.00f, 52.00f, 29.12f, - 96.00f, 29.12f, 52.00f, 96.00f, 0.00f, 52.00f, 96.00f, - 52.00f, 0.00f, 90.00f, 52.00f, 29.12f, 90.00f, 29.12f, - 52.00f, 90.00f, 0.00f, 52.00f, 90.00f, }, - { 0.00f, 8.00f, 102.00f, -4.48f, 8.00f, 102.00f, -8.00f, 4.48f, - 102.00f, -8.00f, 0.00f, 102.00f, 0.00f, 16.00f, 96.00f, - -8.96f, 16.00f, 96.00f, -16.00f, 8.96f, 96.00f, -16.00f, - 0.00f, 96.00f, 0.00f, 52.00f, 96.00f, -29.12f, 52.00f, - 96.00f, -52.00f, 29.12f, 96.00f, -52.00f, 0.00f, 96.00f, - 0.00f, 52.00f, 90.00f, -29.12f, 52.00f, 90.00f, -52.00f, - 29.12f, 90.00f, -52.00f, 0.00f, 90.00f, }, - { -8.00f, 0.00f, 102.00f, -8.00f, -4.48f, 102.00f, -4.48f, -8.00f, - 102.00f, 0.00f, -8.00f, 102.00f, -16.00f, 0.00f, 96.00f, - -16.00f, -8.96f, 96.00f, -8.96f, -16.00f, 96.00f, 0.00f, - -16.00f, 96.00f, -52.00f, 0.00f, 96.00f, -52.00f, -29.12f, - 96.00f, -29.12f, -52.00f, 96.00f, 0.00f, -52.00f, 96.00f, - -52.00f, 0.00f, 90.00f, -52.00f, -29.12f, 90.00f, -29.12f, - -52.00f, 90.00f, 0.00f, -52.00f, 90.00f, }, - { 0.00f, -8.00f, 102.00f, 4.48f, -8.00f, 102.00f, 8.00f, -4.48f, - 102.00f, 8.00f, 0.00f, 102.00f, 0.00f, -16.00f, 96.00f, - 8.96f, -16.00f, 96.00f, 16.00f, -8.96f, 96.00f, 16.00f, - 0.00f, 96.00f, 0.00f, -52.00f, 96.00f, 29.12f, -52.00f, - 96.00f, 52.00f, -29.12f, 96.00f, 52.00f, 0.00f, 96.00f, - 0.00f, -52.00f, 90.00f, 29.12f, -52.00f, 90.00f, 52.00f, - -29.12f, 90.00f, 52.00f, 0.00f, 90.00f, }, - { 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, - 120.00f, 0.00f, 0.00f, 120.00f, 32.00f, 0.00f, 120.00f, - 32.00f, 18.00f, 120.00f, 18.00f, 32.00f, 120.00f, 0.00f, - 32.00f, 120.00f, 0.00f, 0.00f, 108.00f, 0.00f, 0.00f, - 108.00f, 0.00f, 0.00f, 108.00f, 0.00f, 0.00f, 108.00f, - 8.00f, 0.00f, 102.00f, 8.00f, 4.48f, 102.00f, 4.48f, 8.00f, - 102.00f, 0.00f, 8.00f, 102.00f, }, - { 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, - 120.00f, 0.00f, 0.00f, 120.00f, 0.00f, 32.00f, 120.00f, - -18.00f, 32.00f, 120.00f, -32.00f, 18.00f, 120.00f, - -32.00f, 0.00f, 120.00f, 0.00f, 0.00f, 108.00f, 0.00f, - 0.00f, 108.00f, 0.00f, 0.00f, 108.00f, 0.00f, 0.00f, - 108.00f, 0.00f, 8.00f, 102.00f, -4.48f, 8.00f, 102.00f, - -8.00f, 4.48f, 102.00f, -8.00f, 0.00f, 102.00f, }, - { 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, - 120.00f, 0.00f, 0.00f, 120.00f, -32.00f, 0.00f, 120.00f, - -32.00f, -18.00f, 120.00f, -18.00f, -32.00f, 120.00f, - 0.00f, -32.00f, 120.00f, 0.00f, 0.00f, 108.00f, 0.00f, - 0.00f, 108.00f, 0.00f, 0.00f, 108.00f, 0.00f, 0.00f, - 108.00f, -8.00f, 0.00f, 102.00f, -8.00f, -4.48f, 102.00f, - -4.48f, -8.00f, 102.00f, 0.00f, -8.00f, 102.00f, }, - { 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, - 120.00f, 0.00f, 0.00f, 120.00f, 0.00f, -32.00f, 120.00f, - 18.00f, -32.00f, 120.00f, 32.00f, -18.00f, 120.00f, 32.00f, - 0.00f, 120.00f, 0.00f, 0.00f, 108.00f, 0.00f, 0.00f, - 108.00f, 0.00f, 0.00f, 108.00f, 0.00f, 0.00f, 108.00f, - 0.00f, -8.00f, 102.00f, 4.48f, -8.00f, 102.00f, 8.00f, - -4.48f, 102.00f, 8.00f, 0.00f, 102.00f, } }; - - public Teapot() { - super(PATCHES); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/Bitmap.java b/src/org/sunflow/image/Bitmap.java deleted file mode 100644 index cdb269f..0000000 --- a/src/org/sunflow/image/Bitmap.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.sunflow.image; - -public abstract class Bitmap { - protected static final float INV255 = 1.0f / 255; - protected static final float INV65535 = 1.0f / 65535; - - public abstract int getWidth(); - - public abstract int getHeight(); - - public abstract Color readColor(int x, int y); - - public abstract float readAlpha(int x, int y); -} \ No newline at end of file diff --git a/src/org/sunflow/image/BitmapReader.java b/src/org/sunflow/image/BitmapReader.java deleted file mode 100644 index 1bfe344..0000000 --- a/src/org/sunflow/image/BitmapReader.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.sunflow.image; - -import java.io.IOException; - -/** - * This is a very simple interface, designed to handle loading of bitmap data. - */ -public interface BitmapReader { - /** - * Load the specified filename. This method should throw exception if it - * encounters any errors. If the file is valid but its contents are not - * (invalid header for example), a {@link BitmapFormatException} may be - * thrown. It is an error for this method to return null. - * - * @param filename image filename to load - * @param isLinear if this is true, the bitmap is assumed to - * be already in linear space. This can be usefull when reading - * greyscale images for bump mapping for example. HDR formats can - * ignore this flag since they usually always store data in - * linear form. - * @return a new {@link Bitmap} object - */ - public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException; - - /** - * This exception can be used internally by bitmap readers to signal they - * have encountered a valid file but which contains invalid content. - */ - @SuppressWarnings("serial") - public static final class BitmapFormatException extends Exception { - public BitmapFormatException(String message) { - super(message); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/BitmapWriter.java b/src/org/sunflow/image/BitmapWriter.java deleted file mode 100644 index 96e42ef..0000000 --- a/src/org/sunflow/image/BitmapWriter.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.sunflow.image; - -import java.io.IOException; - -/** - * This interface is used to represents an image output format. The methods are - * tile oriented so that tiled image formats may be optimally supported. Note - * that if the header is declared with a 0 tile size, the image will not be - * written with identical sized tiles. The image should either be buffered so it - * can be written all at once on close, or an eror should be thrown. The bitmap - * writer should be designed so that it is thread safe. Specifically, this means - * that the tile writing method can be called by several threads. - */ -public interface BitmapWriter { - /** - * This method will be called before writing begins. It is used to set - * common attributes to file writers. Currently supported keywords include: - *

    - *
  • "compression"
  • - *
  • "channeltype": "byte", "short", "half", "float"
  • - *
- * Note that this method should not fail if its input is not supported or - * invalid. It should gracefully ignore the error and keep its default - * state. - * - * @param option - * @param value - */ - public abstract void configure(String option, String value); - - /** - * Open a handle to the specified file for writing. If the writer buffers - * the image and writes it on close, then the filename should be stored. - * - * @param filename filename to write the bitmap to - * @throws IOException thrown if an I/O error occurs - */ - public abstract void openFile(String filename) throws IOException; - - /** - * Write the bitmap header. This may be defered if the image is buffered for - * writing all at once on close. Note that if tile size is positive, data - * sent to this class is guarenteed to arrive in tiles of that size (except - * at borders). Otherwise, it should be assumed that the data is random, and - * that it may overlap. The writer should then either throw an error or - * start buffering data manually. - * - * @param width image width - * @param height image height - * @param tileSize tile size or 0 if the image will not be sent in tiled - * form - * @throws IOException thrown if an I/O error occurs - * @throws UnsupportedOperationException thrown if this writer does not - * support writing the image with the supplied tile size - */ - public abstract void writeHeader(int width, int height, int tileSize) throws IOException, UnsupportedOperationException; - - /** - * Write a tile of data. Note that this method may be called by more than - * one thread, so it should be made thread-safe if possible. - * - * @param x tile x coordinate - * @param y tile y coordinate - * @param w tile width - * @param h tile height - * @param color color data - * @param alpha alpha data - * @throws IOException thrown if an I/O error occurs - */ - public abstract void writeTile(int x, int y, int w, int h, Color[] color, float[] alpha) throws IOException; - - /** - * Close the file, this completes the bitmap writing process. - * - * @throws IOException thrown if an I/O error occurs - */ - public abstract void closeFile() throws IOException; -} \ No newline at end of file diff --git a/src/org/sunflow/image/BlackbodySpectrum.java b/src/org/sunflow/image/BlackbodySpectrum.java deleted file mode 100644 index 3ceae65..0000000 --- a/src/org/sunflow/image/BlackbodySpectrum.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.sunflow.image; - -public class BlackbodySpectrum extends SpectralCurve { - private float temp; - - public BlackbodySpectrum(float temp) { - this.temp = temp; - } - - @Override - public float sample(float lambda) { - double wavelength = lambda * 1e-9; - return (float) ((3.74183e-16 * Math.pow(wavelength, -5.0)) / (Math.exp(1.4388e-2 / (wavelength * temp)) - 1.0)); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/ChromaticitySpectrum.java b/src/org/sunflow/image/ChromaticitySpectrum.java deleted file mode 100644 index 7c9c7ee..0000000 --- a/src/org/sunflow/image/ChromaticitySpectrum.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.sunflow.image; - -/** - * This spectral curve represents a given (x,y) chromaticity pair as explained - * in the sun/sky paper (section A.5) - */ -public final class ChromaticitySpectrum extends SpectralCurve { - private static final float[] S0Amplitudes = { 0.04f, 6.0f, 29.6f, 55.3f, - 57.3f, 61.8f, 61.5f, 68.8f, 63.4f, 65.8f, 94.8f, 104.8f, 105.9f, - 96.8f, 113.9f, 125.6f, 125.5f, 121.3f, 121.3f, 113.5f, 113.1f, - 110.8f, 106.5f, 108.8f, 105.3f, 104.4f, 100.0f, 96.0f, 95.1f, - 89.1f, 90.5f, 90.3f, 88.4f, 84.0f, 85.1f, 81.9f, 82.6f, 84.9f, - 81.3f, 71.9f, 74.3f, 76.4f, 63.3f, 71.7f, 77.0f, 65.2f, 47.7f, - 68.6f, 65.0f, 66.0f, 61.0f, 53.3f, 58.9f, 61.9f }; - - private static final float[] S1Amplitudes = { 0.02f, 4.5f, 22.4f, 42.0f, - 40.6f, 41.6f, 38.0f, 42.4f, 38.5f, 35.0f, 43.4f, 46.3f, 43.9f, - 37.1f, 36.7f, 35.9f, 32.6f, 27.9f, 24.3f, 20.1f, 16.2f, 13.2f, - 8.6f, 6.1f, 4.2f, 1.9f, 0.0f, -1.6f, -3.5f, -3.5f, -5.8f, -7.2f, - -8.6f, -9.5f, -10.9f, -10.7f, -12.0f, -14.0f, -13.6f, -12.0f, - -13.3f, -12.9f, -10.6f, -11.6f, -12.2f, -10.2f, -7.8f, -11.2f, - -10.4f, -10.6f, -9.7f, -8.3f, -9.3f, -9.8f }; - - private static final float[] S2Amplitudes = { 0.0f, 2.0f, 4.0f, 8.5f, 7.8f, - 6.7f, 5.3f, 6.1f, 3.0f, 1.2f, -1.1f, -0.5f, -0.7f, -1.2f, -2.6f, - -2.9f, -2.8f, -2.6f, -2.6f, -1.8f, -1.5f, -1.3f, -1.2f, -1.0f, - -0.5f, -0.3f, 0.0f, 0.2f, 0.5f, 2.1f, 3.2f, 4.1f, 4.7f, 5.1f, 6.7f, - 7.3f, 8.6f, 9.8f, 10.2f, 8.3f, 9.6f, 8.5f, 7.0f, 7.6f, 8.0f, 6.7f, - 5.2f, 7.4f, 6.8f, 7.0f, 6.4f, 5.5f, 6.1f, 6.5f }; - - private static final RegularSpectralCurve kS0Spectrum = new RegularSpectralCurve(S0Amplitudes, 300, 830); - private static final RegularSpectralCurve kS1Spectrum = new RegularSpectralCurve(S1Amplitudes, 300, 830); - private static final RegularSpectralCurve kS2Spectrum = new RegularSpectralCurve(S2Amplitudes, 300, 830); - - private static final XYZColor S0xyz = kS0Spectrum.toXYZ(); - private static final XYZColor S1xyz = kS1Spectrum.toXYZ(); - private static final XYZColor S2xyz = kS2Spectrum.toXYZ(); - - private final float M1, M2; - - public ChromaticitySpectrum(float x, float y) { - M1 = (-1.3515f - 1.7703f * x + 5.9114f * y) / (0.0241f + 0.2562f * x - 0.7341f * y); - M2 = (0.03f - 31.4424f * x + 30.0717f * y) / (0.0241f + 0.2562f * x - 0.7341f * y); - } - - @Override - public float sample(float lambda) { - return kS0Spectrum.sample(lambda) + M1 * kS1Spectrum.sample(lambda) + M2 * kS2Spectrum.sample(lambda); - } - - public static final XYZColor get(float x, float y) { - float M1 = (-1.3515f - 1.7703f * x + 5.9114f * y) / (0.0241f + 0.2562f * x - 0.7341f * y); - float M2 = (0.03f - 31.4424f * x + 30.0717f * y) / (0.0241f + 0.2562f * x - 0.7341f * y); - float X = S0xyz.getX() + M1 * S1xyz.getX() + M2 * S2xyz.getX(); - float Y = S0xyz.getY() + M1 * S1xyz.getY() + M2 * S2xyz.getY(); - float Z = S0xyz.getZ() + M1 * S1xyz.getZ() + M2 * S2xyz.getZ(); - return new XYZColor(X, Y, Z); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/Color.java b/src/org/sunflow/image/Color.java deleted file mode 100644 index 9281ef0..0000000 --- a/src/org/sunflow/image/Color.java +++ /dev/null @@ -1,370 +0,0 @@ -package org.sunflow.image; - -import org.sunflow.math.MathUtils; - -public final class Color { - private float r, g, b; - public static final RGBSpace NATIVE_SPACE = RGBSpace.SRGB; - public static final Color BLACK = new Color(0, 0, 0); - public static final Color WHITE = new Color(1, 1, 1); - public static final Color RED = new Color(1, 0, 0); - public static final Color GREEN = new Color(0, 1, 0); - public static final Color BLUE = new Color(0, 0, 1); - public static final Color YELLOW = new Color(1, 1, 0); - public static final Color CYAN = new Color(0, 1, 1); - public static final Color MAGENTA = new Color(1, 0, 1); - public static final Color GRAY = new Color(0.5f, 0.5f, 0.5f); - - public static Color black() { - return new Color(); - } - - public static Color white() { - return new Color(1, 1, 1); - } - - private static final float[] EXPONENT = new float[256]; - - static { - EXPONENT[0] = 0; - for (int i = 1; i < 256; i++) { - float f = 1.0f; - int e = i - (128 + 8); - if (e > 0) - for (int j = 0; j < e; j++) - f *= 2.0f; - else - for (int j = 0; j < -e; j++) - f *= 0.5f; - EXPONENT[i] = f; - } - } - - public Color() { - } - - public Color(float gray) { - r = g = b = gray; - } - - public Color(float r, float g, float b) { - this.r = r; - this.g = g; - this.b = b; - } - - public Color toNonLinear() { - r = NATIVE_SPACE.gammaCorrect(r); - g = NATIVE_SPACE.gammaCorrect(g); - b = NATIVE_SPACE.gammaCorrect(b); - return this; - } - - public Color toLinear() { - r = NATIVE_SPACE.ungammaCorrect(r); - g = NATIVE_SPACE.ungammaCorrect(g); - b = NATIVE_SPACE.ungammaCorrect(b); - return this; - } - - public Color(Color c) { - r = c.r; - g = c.g; - b = c.b; - } - - public Color(int rgb) { - r = ((rgb >> 16) & 0xFF) / 255.0f; - g = ((rgb >> 8) & 0xFF) / 255.0f; - b = (rgb & 0xFF) / 255.0f; - } - - public Color copy() { - return new Color(this); - } - - public final Color set(float r, float g, float b) { - this.r = r; - this.g = g; - this.b = b; - return this; - } - - public final Color set(Color c) { - r = c.r; - g = c.g; - b = c.b; - return this; - } - - public final Color setRGB(int rgb) { - r = ((rgb >> 16) & 0xFF) / 255.0f; - g = ((rgb >> 8) & 0xFF) / 255.0f; - b = (rgb & 0xFF) / 255.0f; - return this; - } - - public final Color setRGBE(int rgbe) { - float f = EXPONENT[rgbe & 0xFF]; - r = f * ((rgbe >>> 24) + 0.5f); - g = f * (((rgbe >> 16) & 0xFF) + 0.5f); - b = f * (((rgbe >> 8) & 0xFF) + 0.5f); - return this; - } - - public final boolean isBlack() { - return r <= 0 && g <= 0 && b <= 0; - } - - public final float getLuminance() { - return (0.2989f * r) + (0.5866f * g) + (0.1145f * b); - } - - public final float getMin() { - return MathUtils.min(r, g, b); - } - - public final float getMax() { - return MathUtils.max(r, g, b); - } - - public final float getAverage() { - return (r + g + b) / 3.0f; - } - - public final float[] getRGB() { - return new float[] { r, g, b }; - } - - public final int toRGB() { - int ir = (int) (r * 255 + 0.5); - int ig = (int) (g * 255 + 0.5); - int ib = (int) (b * 255 + 0.5); - ir = MathUtils.clamp(ir, 0, 255); - ig = MathUtils.clamp(ig, 0, 255); - ib = MathUtils.clamp(ib, 0, 255); - return (ir << 16) | (ig << 8) | ib; - } - - public final int toRGBA(float a) { - int ir = (int) (r * 255 + 0.5); - int ig = (int) (g * 255 + 0.5); - int ib = (int) (b * 255 + 0.5); - int ia = (int) (a * 255 + 0.5); - ir = MathUtils.clamp(ir, 0, 255); - ig = MathUtils.clamp(ig, 0, 255); - ib = MathUtils.clamp(ib, 0, 255); - ia = MathUtils.clamp(ia, 0, 255); - return (ia << 24) | (ir << 16) | (ig << 8) | ib; - } - - public final int toRGBE() { - // encode the color into 32bits while preserving HDR using Ward's RGBE - // technique - float v = MathUtils.max(r, g, b); - if (v < 1e-32f) - return 0; - - // get mantissa and exponent - float m = v; - int e = 0; - if (v > 1.0f) { - while (m > 1.0f) { - m *= 0.5f; - e++; - } - } else if (v <= 0.5f) { - while (m <= 0.5f) { - m *= 2.0f; - e--; - } - } - v = (m * 255.0f) / v; - int c = (e + 128); - c |= ((int) (r * v) << 24); - c |= ((int) (g * v) << 16); - c |= ((int) (b * v) << 8); - return c; - } - - public final Color constrainRGB() { - // clamp the RGB value to a representable value - float w = -MathUtils.min(0, r, g, b); - if (w > 0) { - r += w; - g += w; - b += w; - } - return this; - } - - public final boolean isNan() { - return Float.isNaN(r) || Float.isNaN(g) || Float.isNaN(b); - } - - public final boolean isInf() { - return Float.isInfinite(r) || Float.isInfinite(g) || Float.isInfinite(b); - } - - public final Color add(Color c) { - r += c.r; - g += c.g; - b += c.b; - return this; - } - - public static final Color add(Color c1, Color c2) { - return Color.add(c1, c2, new Color()); - } - - public static final Color add(Color c1, Color c2, Color dest) { - dest.r = c1.r + c2.r; - dest.g = c1.g + c2.g; - dest.b = c1.b + c2.b; - return dest; - } - - public final Color madd(float s, Color c) { - r += (s * c.r); - g += (s * c.g); - b += (s * c.b); - return this; - } - - public final Color madd(Color s, Color c) { - r += s.r * c.r; - g += s.g * c.g; - b += s.b * c.b; - return this; - } - - public final Color sub(Color c) { - r -= c.r; - g -= c.g; - b -= c.b; - return this; - } - - public static final Color sub(Color c1, Color c2) { - return Color.sub(c1, c2, new Color()); - } - - public static final Color sub(Color c1, Color c2, Color dest) { - dest.r = c1.r - c2.r; - dest.g = c1.g - c2.g; - dest.b = c1.b - c2.b; - return dest; - } - - public final Color mul(Color c) { - r *= c.r; - g *= c.g; - b *= c.b; - return this; - } - - public static final Color mul(Color c1, Color c2) { - return Color.mul(c1, c2, new Color()); - } - - public static final Color mul(Color c1, Color c2, Color dest) { - dest.r = c1.r * c2.r; - dest.g = c1.g * c2.g; - dest.b = c1.b * c2.b; - return dest; - } - - public final Color mul(float s) { - r *= s; - g *= s; - b *= s; - return this; - } - - public static final Color mul(float s, Color c) { - return Color.mul(s, c, new Color()); - } - - public static final Color mul(float s, Color c, Color dest) { - dest.r = s * c.r; - dest.g = s * c.g; - dest.b = s * c.b; - return dest; - } - - public final Color div(Color c) { - r /= c.r; - g /= c.g; - b /= c.b; - return this; - } - - public static final Color div(Color c1, Color c2) { - return Color.div(c1, c2, new Color()); - } - - public static final Color div(Color c1, Color c2, Color dest) { - dest.r = c1.r / c2.r; - dest.g = c1.g / c2.g; - dest.b = c1.b / c2.b; - return dest; - } - - public final Color exp() { - r = (float) Math.exp(r); - g = (float) Math.exp(g); - b = (float) Math.exp(b); - return this; - } - - public final Color opposite() { - r = 1 - r; - g = 1 - g; - b = 1 - b; - return this; - } - - public final Color clamp(float min, float max) { - r = MathUtils.clamp(r, min, max); - g = MathUtils.clamp(g, min, max); - b = MathUtils.clamp(b, min, max); - return this; - } - - public static final Color blend(Color c1, Color c2, float b) { - return blend(c1, c2, b, new Color()); - } - - public static final Color blend(Color c1, Color c2, float b, Color dest) { - dest.r = (1.0f - b) * c1.r + b * c2.r; - dest.g = (1.0f - b) * c1.g + b * c2.g; - dest.b = (1.0f - b) * c1.b + b * c2.b; - return dest; - } - - public static final Color blend(Color c1, Color c2, Color b) { - return blend(c1, c2, b, new Color()); - } - - public static final Color blend(Color c1, Color c2, Color b, Color dest) { - dest.r = (1.0f - b.r) * c1.r + b.r * c2.r; - dest.g = (1.0f - b.g) * c1.g + b.g * c2.g; - dest.b = (1.0f - b.b) * c1.b + b.b * c2.b; - return dest; - } - - public static final boolean hasContrast(Color c1, Color c2, float thresh) { - if (Math.abs(c1.r - c2.r) / (c1.r + c2.r) > thresh) - return true; - if (Math.abs(c1.g - c2.g) / (c1.g + c2.g) > thresh) - return true; - if (Math.abs(c1.b - c2.b) / (c1.b + c2.b) > thresh) - return true; - return false; - } - - @Override - public String toString() { - return String.format("(%.3f, %.3f, %.3f)", r, g, b); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/ColorEncoder.java b/src/org/sunflow/image/ColorEncoder.java deleted file mode 100644 index 37a379c..0000000 --- a/src/org/sunflow/image/ColorEncoder.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.sunflow.image; - -import org.sunflow.math.MathUtils; - -/** - * This class contains many static helper methods that may be helpful for - * encoding colors into files. - */ -public final class ColorEncoder { - /** - * Undoes the premultiplication of the specified color array. The original - * colors are not modified. - * - * @param color an array of premultiplied colors - * @param alpha alpha values corresponding to the colors - * @return an array of unpremultiplied colors - */ - public static final Color[] unpremult(Color[] color, float[] alpha) { - Color[] output = new Color[color.length]; - for (int i = 0; i < color.length; i++) - output[i] = color[i].copy().mul(1 / alpha[i]); - return output; - } - - /** - * Moves the colors in the specified array to non-linear space. The original - * colors are not modified. - * - * @param color an array of colors in linear space - * @return a new array of the same colors in non-linear space - */ - public static final Color[] unlinearize(Color[] color) { - Color[] output = new Color[color.length]; - for (int i = 0; i < color.length; i++) - output[i] = color[i].copy().toNonLinear(); - return output; - } - - /** - * Quantize the specified colors to 8-bit RGB format. The returned array - * contains 3 bytes for each color in the original array. - * - * @param color array of colors to quantize - * @return array of quantized RGB values - */ - public static final byte[] quantizeRGB8(Color[] color) { - byte[] output = new byte[color.length * 3]; - for (int i = 0, index = 0; i < color.length; i++, index += 3) { - float[] rgb = color[i].getRGB(); - output[index + 0] = (byte) MathUtils.clamp((int) (rgb[0] * 255 + 0.5f), 0, 255); - output[index + 1] = (byte) MathUtils.clamp((int) (rgb[1] * 255 + 0.5f), 0, 255); - output[index + 2] = (byte) MathUtils.clamp((int) (rgb[2] * 255 + 0.5f), 0, 255); - } - return output; - } - - /** - * Quantize the specified colors to 8-bit RGBA format. The returned array - * contains 4 bytes for each color in the original array. - * - * @param color array of colors to quantize - * @param alpha array of alpha values (same length as color) - * @return array of quantized RGBA values - */ - public static final byte[] quantizeRGBA8(Color[] color, float[] alpha) { - byte[] output = new byte[color.length * 4]; - for (int i = 0, index = 0; i < color.length; i++, index += 4) { - float[] rgb = color[i].getRGB(); - output[index + 0] = (byte) MathUtils.clamp((int) (rgb[0] * 255 + 0.5f), 0, 255); - output[index + 1] = (byte) MathUtils.clamp((int) (rgb[1] * 255 + 0.5f), 0, 255); - output[index + 2] = (byte) MathUtils.clamp((int) (rgb[2] * 255 + 0.5f), 0, 255); - output[index + 3] = (byte) MathUtils.clamp((int) (alpha[i] * 255 + 0.5f), 0, 255); - } - return output; - } - - /** - * Encode the specified colors using Ward's RGBE technique. The returned - * array contains one int for each color in the original array. - * - * @param color array of colors to encode - * @return array of encoded colors - */ - public static final int[] encodeRGBE(Color[] color) { - int[] output = new int[color.length]; - for (int i = 0; i < color.length; i++) - output[i] = color[i].toRGBE(); - return output; - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/ColorFactory.java b/src/org/sunflow/image/ColorFactory.java deleted file mode 100644 index c7807cb..0000000 --- a/src/org/sunflow/image/ColorFactory.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.sunflow.image; - -public final class ColorFactory { - /** - * Return the name of the internal color space. This string can be used - * interchangeably with null in the following methods. - * - * @return internal colorspace name - */ - public static String getInternalColorspace() { - return "sRGB linear"; - } - - /** - * Checks to see how many values are required to specify a color using the - * given colorspace. This number can be variable for spectrum colors, in - * which case the returned value is -1. If the colorspace name is invalid, - * this method returns -2. No exception is thrown. This method is intended - * for parsers that want to know how many floating values to retrieve from a - * file. - * - * @param colorspace - * @return number of floating point numbers expected, -1 for any, -2 on - * error - */ - public static int getRequiredDataValues(String colorspace) { - if (colorspace == null) - return 3; - if (colorspace.equals("sRGB nonlinear")) - return 3; - else if (colorspace.equals("sRGB linear")) - return 3; - else if (colorspace.equals("XYZ")) - return 3; - else if (colorspace.equals("blackbody")) - return 1; - else if (colorspace.startsWith("spectrum")) - return -1; - else - return -2; - } - - /** - * Creates a color value in the renderer's internal color space from a - * string (representing the color space name) and an array of floating point - * values. If the colorspace string is null, we assume the data was supplied - * in internal space. This method does much error checking and may throw a - * {@link RuntimeException} if its parameters are not consistent. Here are - * the currently supported color spaces: - *
    - *
  • "sRGB nonlinear" - requires 3 values
  • - *
  • "sRGB linear" - requires 3 values
  • - *
  • "XYZ" - requires 3 values
  • - *
  • blackbody - requires 1 value (temperature in Kelvins)
  • - *
  • spectrum [min] [max] - any number of values (must be - * >0), [start] and [stop] is the range over which the spectrum is defined - * in nanometers.
  • - *
- * - * @param colorspace color space name - * @param data data describing this color - * @return a valid color in the renderer's color space - * @throws ColorSpecificationException - */ - public static Color createColor(String colorspace, float... data) throws ColorSpecificationException { - int required = getRequiredDataValues(colorspace); - if (required == -2) - throw new ColorSpecificationException("unknown colorspace %s"); - if (required != -1 && required != data.length) - throw new ColorSpecificationException(required, data.length); - if (colorspace == null) - return new Color(data[0], data[1], data[2]); - else if (colorspace.equals("sRGB nonlinear")) - return new Color(data[0], data[1], data[2]).toLinear(); - else if (colorspace.equals("sRGB linear")) - return new Color(data[0], data[1], data[2]); - else if (colorspace.equals("XYZ")) - return RGBSpace.SRGB.convertXYZtoRGB(new XYZColor(data[0], data[1], data[2])); - else if (colorspace.equals("blackbody")) - return RGBSpace.SRGB.convertXYZtoRGB(new BlackbodySpectrum(data[0]).toXYZ()); - else if (colorspace.startsWith("spectrum")) { - String[] tokens = colorspace.split("\\s+"); - if (tokens.length != 3) - throw new ColorSpecificationException("invalid spectrum specification"); - if (data.length == 0) - throw new ColorSpecificationException("missing spectrum data"); - try { - float lambdaMin = Float.parseFloat(tokens[1]); - float lambdaMax = Float.parseFloat(tokens[2]); - return RGBSpace.SRGB.convertXYZtoRGB(new RegularSpectralCurve(data, lambdaMin, lambdaMax).toXYZ()); - } catch (NumberFormatException e) { - throw new ColorSpecificationException("unable to parse spectrum wavelength range"); - } - } - throw new ColorSpecificationException(String.format("Inconsistent code! Please report this error. (Input %s - %d)", colorspace, data.length)); - } - - @SuppressWarnings("serial") - public static final class ColorSpecificationException extends Exception { - private ColorSpecificationException() { - super("Invalid color specification"); - } - - private ColorSpecificationException(String message) { - super(String.format("Invalid color specification: %s", message)); - } - - private ColorSpecificationException(int expected, int found) { - this(String.format("invalid data length, expecting %d values, found %d", expected, found)); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/ConstantSpectralCurve.java b/src/org/sunflow/image/ConstantSpectralCurve.java deleted file mode 100644 index ad71782..0000000 --- a/src/org/sunflow/image/ConstantSpectralCurve.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.sunflow.image; - -/** - * Very simple class equivalent to a constant spectral curve. Note that this is - * most likely physically impossible for amplitudes > 0, however this class can - * be handy since in practice spectral curves end up being integrated against - * the finite width color matching functions. - */ -public class ConstantSpectralCurve extends SpectralCurve { - private final float amp; - - public ConstantSpectralCurve(float amp) { - this.amp = amp; - } - - @Override - public float sample(float lambda) { - return amp; - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/IrregularSpectralCurve.java b/src/org/sunflow/image/IrregularSpectralCurve.java deleted file mode 100644 index f13d35c..0000000 --- a/src/org/sunflow/image/IrregularSpectralCurve.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.sunflow.image; - -/** - * This class allows spectral curves to be defined from irregularly sampled - * data. Note that the wavelength array is assumed to be sorted low to high. Any - * values beyond the defined range will simply be extended to infinity from the - * end points. Points inside the valid range will be linearly interpolated - * between the two nearest samples. No explicit error checking is performed, but - * this class will run into {@link ArrayIndexOutOfBoundsException}s if the - * array lengths don't match. - */ -public class IrregularSpectralCurve extends SpectralCurve { - private final float[] wavelengths; - private final float[] amplitudes; - - /** - * Define an irregular spectral curve from the provided (sorted) wavelengths - * and amplitude data. The wavelength array is assumed to contain values in - * nanometers. Array lengths must match. - * - * @param wavelengths sampled wavelengths in nm - * @param amplitudes amplitude of the curve at the sampled points - */ - public IrregularSpectralCurve(float[] wavelengths, float[] amplitudes) { - this.wavelengths = wavelengths; - this.amplitudes = amplitudes; - if (wavelengths.length != amplitudes.length) - throw new RuntimeException(String.format("Error creating irregular spectral curve: %d wavelengths and %d amplitudes", wavelengths.length, amplitudes.length)); - for (int i = 1; i < wavelengths.length; i++) - if (wavelengths[i - 1] >= wavelengths[i]) - throw new RuntimeException(String.format("Error creating irregular spectral curve: values are not sorted - error at index %d", i)); - } - - @Override - public float sample(float lambda) { - if (wavelengths.length == 0) - return 0; // no data - if (wavelengths.length == 1 || lambda <= wavelengths[0]) - return amplitudes[0]; - if (lambda >= wavelengths[wavelengths.length - 1]) - return amplitudes[wavelengths.length - 1]; - for (int i = 1; i < wavelengths.length; i++) { - if (lambda < wavelengths[i]) { - float dx = (lambda - wavelengths[i - 1]) / (wavelengths[i] - wavelengths[i - 1]); - return (1 - dx) * amplitudes[i - 1] + dx * amplitudes[i]; - } - } - return amplitudes[wavelengths.length - 1]; - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/RGBSpace.java b/src/org/sunflow/image/RGBSpace.java deleted file mode 100644 index 1c0c331..0000000 --- a/src/org/sunflow/image/RGBSpace.java +++ /dev/null @@ -1,199 +0,0 @@ -package org.sunflow.image; - -import org.sunflow.math.MathUtils; - -public final class RGBSpace { - public static final RGBSpace ADOBE = new RGBSpace(0.6400f, 0.3300f, 0.2100f, 0.7100f, 0.1500f, 0.0600f, 0.31271f, 0.32902f, 2.2f, 0); - public static final RGBSpace APPLE = new RGBSpace(0.6250f, 0.3400f, 0.2800f, 0.5950f, 0.1550f, 0.0700f, 0.31271f, 0.32902f, 1.8f, 0); - public static final RGBSpace NTSC = new RGBSpace(0.6700f, 0.3300f, 0.2100f, 0.7100f, 0.1400f, 0.0800f, 0.31010f, 0.31620f, 20.0f / 9.0f, 0.018f); - public static final RGBSpace HDTV = new RGBSpace(0.6400f, 0.3300f, 0.3000f, 0.6000f, 0.1500f, 0.0600f, 0.31271f, 0.32902f, 20.0f / 9.0f, 0.018f); - public static final RGBSpace SRGB = new RGBSpace(0.6400f, 0.3300f, 0.3000f, 0.6000f, 0.1500f, 0.0600f, 0.31271f, 0.32902f, 2.4f, 0.00304f); - public static final RGBSpace CIE = new RGBSpace(0.7350f, 0.2650f, 0.2740f, 0.7170f, 0.1670f, 0.0090f, 1 / 3.0f, 1 / 3.0f, 2.2f, 0); - public static final RGBSpace EBU = new RGBSpace(0.6400f, 0.3300f, 0.2900f, 0.6000f, 0.1500f, 0.0600f, 0.31271f, 0.32902f, 20.0f / 9.0f, 0.018f); - public static final RGBSpace SMPTE_C = new RGBSpace(0.6300f, 0.3400f, 0.3100f, 0.5950f, 0.1550f, 0.0700f, 0.31271f, 0.32902f, 20.0f / 9.0f, 0.018f); - public static final RGBSpace SMPTE_240M = new RGBSpace(0.6300f, 0.3400f, 0.3100f, 0.5950f, 0.1550f, 0.0700f, 0.31271f, 0.32902f, 20.0f / 9.0f, 0.018f); - public static final RGBSpace WIDE_GAMUT = new RGBSpace(0.7347f, 0.2653f, 0.1152f, 0.8264f, 0.1566f, 0.0177f, 0.3457f, 0.3585f, 2.2f, 0); - - private final float gamma, breakPoint; - private final float slope, slopeMatch, segmentOffset; - private final float xr, yr, zr, xg, yg, zg, xb, yb, zb; - private final float xw, yw, zw; - private final float rx, ry, rz, gx, gy, gz, bx, by, bz; - private final float rw, gw, bw; - private final int[] GAMMA_CURVE; - private final int[] INV_GAMMA_CURVE; - - public RGBSpace(float xRed, float yRed, float xGreen, float yGreen, float xBlue, float yBlue, float xWhite, float yWhite, float gamma, float breakPoint) { - this.gamma = gamma; - this.breakPoint = breakPoint; - - if (breakPoint > 0) { - slope = 1 / (gamma / (float) Math.pow(breakPoint, 1 / gamma - 1) - gamma * breakPoint + breakPoint); - slopeMatch = gamma * slope / (float) Math.pow(breakPoint, 1 / gamma - 1); - segmentOffset = slopeMatch * (float) Math.pow(breakPoint, 1 / gamma) - slope * breakPoint; - } else { - slope = 1; - slopeMatch = 1; - segmentOffset = 0; - } - - // prepare gamma curves - GAMMA_CURVE = new int[256]; - INV_GAMMA_CURVE = new int[256]; - for (int i = 0; i < 256; i++) { - float c = i / 255.0f; - GAMMA_CURVE[i] = MathUtils.clamp((int) (gammaCorrect(c) * 255 + 0.5f), 0, 255); - INV_GAMMA_CURVE[i] = MathUtils.clamp((int) (ungammaCorrect(c) * 255 + 0.5f), 0, 255); - } - - float xr = xRed; - float yr = yRed; - float zr = 1 - (xr + yr); - float xg = xGreen; - float yg = yGreen; - float zg = 1 - (xg + yg); - float xb = xBlue; - float yb = yBlue; - float zb = 1 - (xb + yb); - - xw = xWhite; - yw = yWhite; - zw = 1 - (xw + yw); - - // xyz -> rgb matrix, before scaling to white. - float rx = (yg * zb) - (yb * zg); - float ry = (xb * zg) - (xg * zb); - float rz = (xg * yb) - (xb * yg); - float gx = (yb * zr) - (yr * zb); - float gy = (xr * zb) - (xb * zr); - float gz = (xb * yr) - (xr * yb); - float bx = (yr * zg) - (yg * zr); - float by = (xg * zr) - (xr * zg); - float bz = (xr * yg) - (xg * yr); - // White scaling factors - // Dividing by yw scales the white luminance to unity, as conventional - rw = ((rx * xw) + (ry * yw) + (rz * zw)) / yw; - gw = ((gx * xw) + (gy * yw) + (gz * zw)) / yw; - bw = ((bx * xw) + (by * yw) + (bz * zw)) / yw; - - // xyz -> rgb matrix, correctly scaled to white - this.rx = rx / rw; - this.ry = ry / rw; - this.rz = rz / rw; - this.gx = gx / gw; - this.gy = gy / gw; - this.gz = gz / gw; - this.bx = bx / bw; - this.by = by / bw; - this.bz = bz / bw; - - // invert matrix again to get proper rgb -> xyz matrix - float s = 1 / (this.rx * (this.gy * this.bz - this.by * this.gz) - this.ry * (this.gx * this.bz - this.bx * this.gz) + this.rz * (this.gx * this.by - this.bx * this.gy)); - this.xr = s * (this.gy * this.bz - this.gz * this.by); - this.xg = s * (this.rz * this.by - this.ry * this.bz); - this.xb = s * (this.ry * this.gz - this.rz * this.gy); - - this.yr = s * (this.gz * this.bx - this.gx * this.bz); - this.yg = s * (this.rx * this.bz - this.rz * this.bx); - this.yb = s * (this.rz * this.gx - this.rx * this.gz); - - this.zr = s * (this.gx * this.by - this.gy * this.bx); - this.zg = s * (this.ry * this.bx - this.rx * this.by); - this.zb = s * (this.rx * this.gy - this.ry * this.gx); - } - - public final Color convertXYZtoRGB(XYZColor c) { - return convertXYZtoRGB(c.getX(), c.getY(), c.getZ()); - } - - public final Color convertXYZtoRGB(float X, float Y, float Z) { - float r = (rx * X) + (ry * Y) + (rz * Z); - float g = (gx * X) + (gy * Y) + (gz * Z); - float b = (bx * X) + (by * Y) + (bz * Z); - return new Color(r, g, b); - } - - public final XYZColor convertRGBtoXYZ(Color c) { - float[] rgb = c.getRGB(); - float X = (xr * rgb[0]) + (xg * rgb[1]) + (xb * rgb[2]); - float Y = (yr * rgb[0]) + (yg * rgb[1]) + (yb * rgb[2]); - float Z = (zr * rgb[0]) + (zg * rgb[1]) + (zb * rgb[2]); - return new XYZColor(X, Y, Z); - } - - public final boolean insideGamut(float r, float g, float b) { - return r >= 0 && g >= 0 && b >= 0; - } - - public final float gammaCorrect(float v) { - if (v <= 0) - return 0; - else if (v >= 1) - return 1; - else if (v <= breakPoint) - return slope * v; - else - return slopeMatch * (float) Math.pow(v, 1 / gamma) - segmentOffset; - } - - public final float ungammaCorrect(float vp) { - if (vp <= 0) - return 0; - else if (vp >= 1) - return 1; - else if (vp <= breakPoint * slope) - return vp / slope; - else - return (float) Math.pow((vp + segmentOffset) / slopeMatch, gamma); - } - - public final int rgbToNonLinear(int rgb) { - // gamma correct 24bit rgb value via tables - int rp = GAMMA_CURVE[(rgb >> 16) & 0xFF]; - int gp = GAMMA_CURVE[(rgb >> 8) & 0xFF]; - int bp = GAMMA_CURVE[rgb & 0xFF]; - return (rp << 16) | (gp << 8) | bp; - } - - public final int rgbToLinear(int rgb) { - // convert a packed RGB triplet to a linearized - // one by applying the proper LUT - int rp = INV_GAMMA_CURVE[(rgb >> 16) & 0xFF]; - int gp = INV_GAMMA_CURVE[(rgb >> 8) & 0xFF]; - int bp = INV_GAMMA_CURVE[rgb & 0xFF]; - return (rp << 16) | (gp << 8) | bp; - } - - public final byte rgbToNonLinear(byte r) { - return (byte) GAMMA_CURVE[r & 0xFF]; - } - - public final byte rgbToLinear(byte r) { - return (byte) INV_GAMMA_CURVE[r & 0xFF]; - } - - @Override - public final String toString() { - String info = "Gamma function parameters:\n"; - info += String.format(" * Gamma: %7.4f\n", gamma); - info += String.format(" * Breakpoint: %7.4f\n", breakPoint); - info += String.format(" * Slope: %7.4f\n", slope); - info += String.format(" * Slope Match: %7.4f\n", slopeMatch); - info += String.format(" * Segment Offset: %7.4f\n", segmentOffset); - info += "XYZ -> RGB Matrix:\n"; - info += String.format("| %7.4f %7.4f %7.4f|\n", rx, ry, rz); - info += String.format("| %7.4f %7.4f %7.4f|\n", gx, gy, gz); - info += String.format("| %7.4f %7.4f %7.4f|\n", bx, by, bz); - info += "RGB -> XYZ Matrix:\n"; - info += String.format("| %7.4f %7.4f %7.4f|\n", xr, xg, xb); - info += String.format("| %7.4f %7.4f %7.4f|\n", yr, yg, yb); - info += String.format("| %7.4f %7.4f %7.4f|\n", zr, zg, zb); - return info; - } - - public static void main(String[] args) { - System.out.println(SRGB.toString()); - System.out.println(HDTV.toString()); - System.out.println(WIDE_GAMUT.toString()); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/RegularSpectralCurve.java b/src/org/sunflow/image/RegularSpectralCurve.java deleted file mode 100644 index 22b98b7..0000000 --- a/src/org/sunflow/image/RegularSpectralCurve.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.sunflow.image; - -public class RegularSpectralCurve extends SpectralCurve { - private final float[] spectrum; - private final float lambdaMin, lambdaMax; - private final float delta, invDelta; - - public RegularSpectralCurve(float[] spectrum, float lambdaMin, float lambdaMax) { - this.lambdaMin = lambdaMin; - this.lambdaMax = lambdaMax; - this.spectrum = spectrum; - delta = (lambdaMax - lambdaMin) / (spectrum.length - 1); - invDelta = 1 / delta; - } - - @Override - public float sample(float lambda) { - // reject wavelengths outside the valid range - if (lambda < lambdaMin || lambda > lambdaMax) - return 0; - // interpolate the two closest samples linearly - float x = (lambda - lambdaMin) * invDelta; - int b0 = (int) x; - int b1 = Math.min(b0 + 1, spectrum.length - 1); - float dx = x - b0; - return (1 - dx) * spectrum[b0] + dx * spectrum[b1]; - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/SpectralCurve.java b/src/org/sunflow/image/SpectralCurve.java deleted file mode 100644 index a4fe37b..0000000 --- a/src/org/sunflow/image/SpectralCurve.java +++ /dev/null @@ -1,119 +0,0 @@ -package org.sunflow.image; - -/** - * This class is an abstract interface to sampled or analytic spectral data. - */ -public abstract class SpectralCurve { - /** - * This function determines the actual spectral curve data. Note that the - * lambda parameter is assumed to be in nanometers. - * - * @param lambda wavelength to sample in nanometers - * @return the value of the spectral curve at this point - */ - public abstract float sample(float lambda); - - private static final int WAVELENGTH_MIN = 360; - private static final int WAVELENGTH_MAX = 830; - private static final double[] CIE_xbar = { 0.000129900000, 0.000232100000, - 0.000414900000, 0.000741600000, 0.001368000000, 0.002236000000, - 0.004243000000, 0.007650000000, 0.014310000000, 0.023190000000, - 0.043510000000, 0.077630000000, 0.134380000000, 0.214770000000, - 0.283900000000, 0.328500000000, 0.348280000000, 0.348060000000, - 0.336200000000, 0.318700000000, 0.290800000000, 0.251100000000, - 0.195360000000, 0.142100000000, 0.095640000000, 0.057950010000, - 0.032010000000, 0.014700000000, 0.004900000000, 0.002400000000, - 0.009300000000, 0.029100000000, 0.063270000000, 0.109600000000, - 0.165500000000, 0.225749900000, 0.290400000000, 0.359700000000, - 0.433449900000, 0.512050100000, 0.594500000000, 0.678400000000, - 0.762100000000, 0.842500000000, 0.916300000000, 0.978600000000, - 1.026300000000, 1.056700000000, 1.062200000000, 1.045600000000, - 1.002600000000, 0.938400000000, 0.854449900000, 0.751400000000, - 0.642400000000, 0.541900000000, 0.447900000000, 0.360800000000, - 0.283500000000, 0.218700000000, 0.164900000000, 0.121200000000, - 0.087400000000, 0.063600000000, 0.046770000000, 0.032900000000, - 0.022700000000, 0.015840000000, 0.011359160000, 0.008110916000, - 0.005790346000, 0.004106457000, 0.002899327000, 0.002049190000, - 0.001439971000, 0.000999949300, 0.000690078600, 0.000476021300, - 0.000332301100, 0.000234826100, 0.000166150500, 0.000117413000, - 0.000083075270, 0.000058706520, 0.000041509940, 0.000029353260, - 0.000020673830, 0.000014559770, 0.000010253980, 0.000007221456, - 0.000005085868, 0.000003581652, 0.000002522525, 0.000001776509, - 0.000001251141, }; - private static final double[] CIE_ybar = { 0.000003917000, 0.000006965000, - 0.000012390000, 0.000022020000, 0.000039000000, 0.000064000000, - 0.000120000000, 0.000217000000, 0.000396000000, 0.000640000000, - 0.001210000000, 0.002180000000, 0.004000000000, 0.007300000000, - 0.011600000000, 0.016840000000, 0.023000000000, 0.029800000000, - 0.038000000000, 0.048000000000, 0.060000000000, 0.073900000000, - 0.090980000000, 0.112600000000, 0.139020000000, 0.169300000000, - 0.208020000000, 0.258600000000, 0.323000000000, 0.407300000000, - 0.503000000000, 0.608200000000, 0.710000000000, 0.793200000000, - 0.862000000000, 0.914850100000, 0.954000000000, 0.980300000000, - 0.994950100000, 1.000000000000, 0.995000000000, 0.978600000000, - 0.952000000000, 0.915400000000, 0.870000000000, 0.816300000000, - 0.757000000000, 0.694900000000, 0.631000000000, 0.566800000000, - 0.503000000000, 0.441200000000, 0.381000000000, 0.321000000000, - 0.265000000000, 0.217000000000, 0.175000000000, 0.138200000000, - 0.107000000000, 0.081600000000, 0.061000000000, 0.044580000000, - 0.032000000000, 0.023200000000, 0.017000000000, 0.011920000000, - 0.008210000000, 0.005723000000, 0.004102000000, 0.002929000000, - 0.002091000000, 0.001484000000, 0.001047000000, 0.000740000000, - 0.000520000000, 0.000361100000, 0.000249200000, 0.000171900000, - 0.000120000000, 0.000084800000, 0.000060000000, 0.000042400000, - 0.000030000000, 0.000021200000, 0.000014990000, 0.000010600000, - 0.000007465700, 0.000005257800, 0.000003702900, 0.000002607800, - 0.000001836600, 0.000001293400, 0.000000910930, 0.000000641530, - 0.000000451810, }; - private static final double[] CIE_zbar = { 0.000606100000, 0.001086000000, - 0.001946000000, 0.003486000000, 0.006450001000, 0.010549990000, - 0.020050010000, 0.036210000000, 0.067850010000, 0.110200000000, - 0.207400000000, 0.371300000000, 0.645600000000, 1.039050100000, - 1.385600000000, 1.622960000000, 1.747060000000, 1.782600000000, - 1.772110000000, 1.744100000000, 1.669200000000, 1.528100000000, - 1.287640000000, 1.041900000000, 0.812950100000, 0.616200000000, - 0.465180000000, 0.353300000000, 0.272000000000, 0.212300000000, - 0.158200000000, 0.111700000000, 0.078249990000, 0.057250010000, - 0.042160000000, 0.029840000000, 0.020300000000, 0.013400000000, - 0.008749999000, 0.005749999000, 0.003900000000, 0.002749999000, - 0.002100000000, 0.001800000000, 0.001650001000, 0.001400000000, - 0.001100000000, 0.001000000000, 0.000800000000, 0.000600000000, - 0.000340000000, 0.000240000000, 0.000190000000, 0.000100000000, - 0.000049999990, 0.000030000000, 0.000020000000, 0.000010000000, - 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, - 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, - 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, - 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, - 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, - 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, - 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, - 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, - 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, - 0.000000000000, }; - - private static final int WAVELENGTH_STEP = (WAVELENGTH_MAX - WAVELENGTH_MIN) / (CIE_xbar.length - 1); - - static { - if (WAVELENGTH_STEP * (CIE_xbar.length - 1) != WAVELENGTH_MAX - WAVELENGTH_MIN) { - String err = String.format("Internal error - spectrum static data is inconsistent!\n * min = %d\n * max = %d\n * step = %d\n * num = %d", WAVELENGTH_MIN, WAVELENGTH_MAX, WAVELENGTH_STEP, CIE_xbar.length); - throw new RuntimeException(err); - } - } - - /** - * Convert this curve to a tristimulus CIE XYZ color by integrating against - * the CIE color matching functions. - * - * @return XYZColor that represents this spectra - */ - public final XYZColor toXYZ() { - float X = 0, Y = 0, Z = 0; - for (int i = 0, w = WAVELENGTH_MIN; i < CIE_xbar.length; i++, w += WAVELENGTH_STEP) { - float s = sample(w); - X += s * CIE_xbar[i]; - Y += s * CIE_ybar[i]; - Z += s * CIE_zbar[i]; - } - return new XYZColor(X, Y, Z).mul(WAVELENGTH_STEP); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/XYZColor.java b/src/org/sunflow/image/XYZColor.java deleted file mode 100644 index 010efaa..0000000 --- a/src/org/sunflow/image/XYZColor.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.sunflow.image; - -public final class XYZColor { - private float X, Y, Z; - - public XYZColor() { - } - - public XYZColor(float X, float Y, float Z) { - this.X = X; - this.Y = Y; - this.Z = Z; - } - - public final float getX() { - return X; - } - - public final float getY() { - return Y; - } - - public final float getZ() { - return Z; - } - - public final XYZColor mul(float s) { - X *= s; - Y *= s; - Z *= s; - return this; - } - - public final void normalize() { - float XYZ = X + Y + Z; - if (XYZ < 1e-6f) - return; - float s = 1 / XYZ; - X *= s; - Y *= s; - Z *= s; - } - - @Override - public final String toString() { - return String.format("(%.3f, %.3f, %.3f)", X, Y, Z); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/formats/BitmapBlack.java b/src/org/sunflow/image/formats/BitmapBlack.java deleted file mode 100644 index cb14ea2..0000000 --- a/src/org/sunflow/image/formats/BitmapBlack.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.sunflow.image.formats; - -import org.sunflow.image.Bitmap; -import org.sunflow.image.Color; - -public class BitmapBlack extends Bitmap { - @Override - public int getWidth() { - return 1; - } - - @Override - public int getHeight() { - return 1; - } - - @Override - public Color readColor(int x, int y) { - return Color.BLACK; - } - - @Override - public float readAlpha(int x, int y) { - return 0; - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/formats/BitmapG8.java b/src/org/sunflow/image/formats/BitmapG8.java deleted file mode 100644 index 87915cb..0000000 --- a/src/org/sunflow/image/formats/BitmapG8.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.sunflow.image.formats; - -import org.sunflow.image.Bitmap; -import org.sunflow.image.Color; - -public class BitmapG8 extends Bitmap { - private int w, h; - private byte[] data; - - public BitmapG8(int w, int h, byte[] data) { - this.w = w; - this.h = h; - this.data = data; - } - - @Override - public int getWidth() { - return w; - } - - @Override - public int getHeight() { - return h; - } - - @Override - public Color readColor(int x, int y) { - return new Color((data[x + y * w] & 0xFF) * INV255); - } - - @Override - public float readAlpha(int x, int y) { - return 1; - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/formats/BitmapGA8.java b/src/org/sunflow/image/formats/BitmapGA8.java deleted file mode 100644 index bd25fcb..0000000 --- a/src/org/sunflow/image/formats/BitmapGA8.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.sunflow.image.formats; - -import org.sunflow.image.Bitmap; -import org.sunflow.image.Color; - -public class BitmapGA8 extends Bitmap { - private int w, h; - private byte[] data; - - @Override - public int getWidth() { - return w; - } - - @Override - public int getHeight() { - return h; - } - - @Override - public Color readColor(int x, int y) { - return new Color((data[2 * (x + y * w) + 0] & 0xFF) * INV255); - } - - @Override - public float readAlpha(int x, int y) { - return (data[2 * (x + y * w) + 1] & 0xFF) * INV255; - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/formats/BitmapRGB8.java b/src/org/sunflow/image/formats/BitmapRGB8.java deleted file mode 100644 index 3978153..0000000 --- a/src/org/sunflow/image/formats/BitmapRGB8.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.sunflow.image.formats; - -import org.sunflow.image.Bitmap; -import org.sunflow.image.Color; - -public class BitmapRGB8 extends Bitmap { - private int w, h; - private byte[] data; - - public BitmapRGB8(int w, int h, byte[] data) { - this.w = w; - this.h = h; - this.data = data; - } - - @Override - public int getWidth() { - return w; - } - - @Override - public int getHeight() { - return h; - } - - @Override - public Color readColor(int x, int y) { - int index = 3 * (x + y * w); - float r = (data[index + 0] & 0xFF) * INV255; - float g = (data[index + 1] & 0xFF) * INV255; - float b = (data[index + 2] & 0xFF) * INV255; - return new Color(r, g, b); - } - - @Override - public float readAlpha(int x, int y) { - return 1; - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/formats/BitmapRGBA8.java b/src/org/sunflow/image/formats/BitmapRGBA8.java deleted file mode 100644 index 652aecb..0000000 --- a/src/org/sunflow/image/formats/BitmapRGBA8.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.sunflow.image.formats; - -import org.sunflow.image.Bitmap; -import org.sunflow.image.Color; - -public class BitmapRGBA8 extends Bitmap { - private int w, h; - private byte[] data; - - public BitmapRGBA8(int w, int h, byte[] data) { - this.w = w; - this.h = h; - this.data = data; - } - - @Override - public int getWidth() { - return w; - } - - @Override - public int getHeight() { - return h; - } - - @Override - public Color readColor(int x, int y) { - int index = 4 * (x + y * w); - float r = (data[index + 0] & 0xFF) * INV255; - float g = (data[index + 1] & 0xFF) * INV255; - float b = (data[index + 2] & 0xFF) * INV255; - return new Color(r, g, b); - } - - @Override - public float readAlpha(int x, int y) { - return (data[4 * (x + y * w) + 3] & 0xFF) * INV255; - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/formats/BitmapRGBE.java b/src/org/sunflow/image/formats/BitmapRGBE.java deleted file mode 100644 index efdbaf2..0000000 --- a/src/org/sunflow/image/formats/BitmapRGBE.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.sunflow.image.formats; - -import org.sunflow.image.Bitmap; -import org.sunflow.image.Color; - -public class BitmapRGBE extends Bitmap { - private int w, h; - private int[] data; - private static final float[] EXPONENT = new float[256]; - - static { - EXPONENT[0] = 0; - for (int i = 1; i < 256; i++) { - float f = 1.0f; - int e = i - (128 + 8); - if (e > 0) - for (int j = 0; j < e; j++) - f *= 2.0f; - else - for (int j = 0; j < -e; j++) - f *= 0.5f; - EXPONENT[i] = f; - } - } - - public BitmapRGBE(int w, int h, int[] data) { - this.w = w; - this.h = h; - this.data = data; - } - - @Override - public int getWidth() { - return w; - } - - @Override - public int getHeight() { - return h; - } - - @Override - public Color readColor(int x, int y) { - int rgbe = data[x + y * w]; - float f = EXPONENT[rgbe & 0xFF]; - float r = f * ((rgbe >>> 24) + 0.5f); - float g = f * (((rgbe >> 16) & 0xFF) + 0.5f); - float b = f * (((rgbe >> 8) & 0xFF) + 0.5f); - return new Color(r, g, b); - } - - @Override - public float readAlpha(int x, int y) { - return 1; - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/formats/BitmapXYZ.java b/src/org/sunflow/image/formats/BitmapXYZ.java deleted file mode 100644 index c237038..0000000 --- a/src/org/sunflow/image/formats/BitmapXYZ.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.sunflow.image.formats; - -import org.sunflow.image.Bitmap; -import org.sunflow.image.Color; -import org.sunflow.image.XYZColor; - -public class BitmapXYZ extends Bitmap { - private int w, h; - private float[] data; - - public BitmapXYZ(int w, int h, float[] data) { - this.w = w; - this.h = h; - this.data = data; - } - - @Override - public int getWidth() { - return w; - } - - @Override - public int getHeight() { - return h; - } - - @Override - public Color readColor(int x, int y) { - int index = 3 * (x + y * w); - return Color.NATIVE_SPACE.convertXYZtoRGB(new XYZColor(data[index], data[index + 1], data[index + 2])).mul(0.1f); - } - - @Override - public float readAlpha(int x, int y) { - return 1; - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/formats/GenericBitmap.java b/src/org/sunflow/image/formats/GenericBitmap.java deleted file mode 100644 index d00a96c..0000000 --- a/src/org/sunflow/image/formats/GenericBitmap.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.sunflow.image.formats; - -import java.io.IOException; - -import org.sunflow.PluginRegistry; -import org.sunflow.image.Bitmap; -import org.sunflow.image.BitmapWriter; -import org.sunflow.image.Color; -import org.sunflow.system.FileUtils; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -/** - * This is a generic and inefficient bitmap format which may be used for - * debugging purposes (dumping small images), when memory usage is not a - * concern. - */ -public class GenericBitmap extends Bitmap { - private int w, h; - private Color[] color; - private float[] alpha; - - public GenericBitmap(int w, int h) { - this.w = w; - this.h = h; - color = new Color[w * h]; - alpha = new float[w * h]; - } - - @Override - public int getWidth() { - return w; - } - - @Override - public int getHeight() { - return h; - } - - @Override - public Color readColor(int x, int y) { - return color[x + y * w]; - } - - @Override - public float readAlpha(int x, int y) { - return alpha[x + y * w]; - } - - public void writePixel(int x, int y, Color c, float a) { - color[x + y * w] = c; - alpha[x + y * w] = a; - } - - public void save(String filename) { - String extension = FileUtils.getExtension(filename); - BitmapWriter writer = PluginRegistry.bitmapWriterPlugins.createObject(extension); - if (writer == null) { - UI.printError(Module.IMG, "Unable to save file \"%s\" - unknown file format: %s", filename, extension); - return; - } - try { - writer.openFile(filename); - writer.writeHeader(w, h, Math.max(w, h)); - writer.writeTile(0, 0, w, h, color, alpha); - writer.closeFile(); - } catch (IOException e) { - UI.printError(Module.IMG, "Unable to save file \"%s\" - %s", filename, e.getLocalizedMessage()); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/readers/BMPBitmapReader.java b/src/org/sunflow/image/readers/BMPBitmapReader.java deleted file mode 100644 index bed1104..0000000 --- a/src/org/sunflow/image/readers/BMPBitmapReader.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.sunflow.image.readers; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; - -import javax.imageio.ImageIO; - -import org.sunflow.image.Bitmap; -import org.sunflow.image.BitmapReader; -import org.sunflow.image.Color; -import org.sunflow.image.formats.BitmapRGB8; - -public class BMPBitmapReader implements BitmapReader { - public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException { - // regular image, load using Java api - ignore alpha channel - BufferedImage bi = ImageIO.read(new File(filename)); - int width = bi.getWidth(); - int height = bi.getHeight(); - byte[] pixels = new byte[3 * width * height]; - for (int y = 0, index = 0; y < height; y++) { - for (int x = 0; x < width; x++, index += 3) { - int argb = bi.getRGB(x, height - 1 - y); - pixels[index + 0] = (byte) (argb >> 16); - pixels[index + 1] = (byte) (argb >> 8); - pixels[index + 2] = (byte) argb; - } - } - if (!isLinear) { - for (int index = 0; index < pixels.length; index += 3) { - pixels[index + 0] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 0]); - pixels[index + 1] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 1]); - pixels[index + 2] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 2]); - } - } - return new BitmapRGB8(width, height, pixels); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/readers/HDRBitmapReader.java b/src/org/sunflow/image/readers/HDRBitmapReader.java deleted file mode 100644 index 4ed9368..0000000 --- a/src/org/sunflow/image/readers/HDRBitmapReader.java +++ /dev/null @@ -1,148 +0,0 @@ -package org.sunflow.image.readers; - -import java.io.BufferedInputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.sunflow.image.Bitmap; -import org.sunflow.image.BitmapReader; -import org.sunflow.image.formats.BitmapRGBE; - -public class HDRBitmapReader implements BitmapReader { - public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException { - // load radiance rgbe file - InputStream f = new BufferedInputStream(new FileInputStream(filename)); - // parse header - boolean parseWidth = false, parseHeight = false; - int width = 0, height = 0; - int last = 0; - while (width == 0 || height == 0 || last != '\n') { - int n = f.read(); - switch (n) { - case 'Y': - parseHeight = last == '-'; - parseWidth = false; - break; - case 'X': - parseHeight = false; - parseWidth = last == '+'; - break; - case ' ': - parseWidth &= width == 0; - parseHeight &= height == 0; - break; - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - if (parseHeight) - height = 10 * height + (n - '0'); - else if (parseWidth) - width = 10 * width + (n - '0'); - break; - default: - parseWidth = parseHeight = false; - break; - } - last = n; - } - // allocate image - int[] pixels = new int[width * height]; - if ((width < 8) || (width > 0x7fff)) { - // run length encoding is not allowed so read flat - readFlatRGBE(f, 0, width * height, pixels); - } else { - int rasterPos = 0; - int numScanlines = height; - int[] scanlineBuffer = new int[4 * width]; - while (numScanlines > 0) { - int r = f.read(); - int g = f.read(); - int b = f.read(); - int e = f.read(); - if ((r != 2) || (g != 2) || ((b & 0x80) != 0)) { - // this file is not run length encoded - pixels[rasterPos] = (r << 24) | (g << 16) | (b << 8) | e; - readFlatRGBE(f, rasterPos + 1, width * numScanlines - 1, pixels); - break; - } - - if (((b << 8) | e) != width) - throw new BitmapFormatException("Invalid scanline width"); - int p = 0; - // read each of the four channels for the scanline into - // the buffer - for (int i = 0; i < 4; i++) { - if (p % width != 0) - throw new BitmapFormatException("Unaligned access to scanline data"); - int end = (i + 1) * width; - while (p < end) { - int b0 = f.read(); - int b1 = f.read(); - if (b0 > 128) { - // a run of the same value - int count = b0 - 128; - if ((count == 0) || (count > (end - p))) - throw new BitmapFormatException("Bad scanline data - invalid RLE run"); - - while (count-- > 0) { - scanlineBuffer[p] = b1; - p++; - } - } else { - // a non-run - int count = b0; - if ((count == 0) || (count > (end - p))) - throw new BitmapFormatException("Bad scanline data - invalid count"); - scanlineBuffer[p] = b1; - p++; - if (--count > 0) { - for (int x = 0; x < count; x++) - scanlineBuffer[p + x] = f.read(); - p += count; - } - } - } - } - // now convert data from buffer into floats - for (int i = 0; i < width; i++) { - r = scanlineBuffer[i]; - g = scanlineBuffer[i + width]; - b = scanlineBuffer[i + 2 * width]; - e = scanlineBuffer[i + 3 * width]; - pixels[rasterPos] = (r << 24) | (g << 16) | (b << 8) | e; - rasterPos++; - } - numScanlines--; - } - } - f.close(); - // flip image - for (int y = 0, i = 0, ir = (height - 1) * width; y < height / 2; y++, ir -= width) { - for (int x = 0, i2 = ir; x < width; x++, i++, i2++) { - int t = pixels[i]; - pixels[i] = pixels[i2]; - pixels[i2] = t; - } - } - return new BitmapRGBE(width, height, pixels); - } - - private void readFlatRGBE(InputStream f, int rasterPos, int numPixels, int[] pixels) throws IOException { - while (numPixels-- > 0) { - int r = f.read(); - int g = f.read(); - int b = f.read(); - int e = f.read(); - pixels[rasterPos] = (r << 24) | (g << 16) | (b << 8) | e; - rasterPos++; - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/readers/IGIBitmapReader.java b/src/org/sunflow/image/readers/IGIBitmapReader.java deleted file mode 100644 index 051ef03..0000000 --- a/src/org/sunflow/image/readers/IGIBitmapReader.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.sunflow.image.readers; - -import java.io.BufferedInputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.sunflow.image.Bitmap; -import org.sunflow.image.BitmapReader; -import org.sunflow.image.formats.BitmapXYZ; - -/** - * Reads images in Indigo's native XYZ format. - * http://www2.indigorenderer.com/joomla/forum/viewtopic.php?p=11430 - */ -public class IGIBitmapReader implements BitmapReader { - public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException { - InputStream stream = new BufferedInputStream(new FileInputStream(filename)); - // read header - int magic = read32i(stream); - int version = read32i(stream); - stream.skip(8); // skip number of samples (double value) - int width = read32i(stream); - int height = read32i(stream); - int superSample = read32i(stream); // super sample factor - int compression = read32i(stream); - int dataSize = read32i(stream); - int colorSpace = read32i(stream); - stream.skip(5000); // skip the rest of the header (unused for now) - // error checking - if (magic != 66613373) - throw new BitmapFormatException("wrong magic: " + magic); - if (version != 1) - throw new BitmapFormatException("unsupported version: " + version); - if (compression != 0) - throw new BitmapFormatException("unsupported compression: " + compression); - if (colorSpace != 0) - throw new BitmapFormatException("unsupported color space: " + colorSpace); - if (dataSize != (width * height * 12)) - throw new BitmapFormatException("invalid data block size: " + dataSize); - if (width <= 0 || height <= 0) - throw new BitmapFormatException("invalid image size: " + width + "x" + height); - if (superSample <= 0) - throw new BitmapFormatException("invalid super sample factor: " + superSample); - if ((width % superSample) != 0 || (height % superSample) != 0) - throw new BitmapFormatException("invalid image size: " + width + "x" + height); - float[] xyz = new float[width * height * 3]; - for (int y = 0, i = 3 * (height - 1) * width; y < height; y++, i -= 6 * width) { - for (int x = 0; x < width; x++, i += 3) { - xyz[i + 0] = read32f(stream); - xyz[i + 1] = read32f(stream); - xyz[i + 2] = read32f(stream); - } - } - stream.close(); - if (superSample > 1) { - // rescale image (basic box filtering) - float[] rescaled = new float[xyz.length / (superSample * superSample)]; - float inv = 1.0f / (superSample * superSample); - for (int y = 0, i = 0; y < height; y += superSample) { - for (int x = 0; x < width; x += superSample, i += 3) { - float X = 0; - float Y = 0; - float Z = 0; - for (int sy = 0; sy < superSample; sy++) { - for (int sx = 0; sx < superSample; sx++) { - int offset = 3 * ((x + sx + (y + sy) * width)); - X += xyz[offset + 0]; - Y += xyz[offset + 1]; - Z += xyz[offset + 2]; - } - } - rescaled[i + 0] = X * inv; - rescaled[i + 1] = Y * inv; - rescaled[i + 2] = Z * inv; - } - } - return new BitmapXYZ(width / superSample, height / superSample, rescaled); - } else - return new BitmapXYZ(width, height, xyz); - } - - private static final int read32i(InputStream stream) throws IOException { - int i = stream.read(); - i |= stream.read() << 8; - i |= stream.read() << 16; - i |= stream.read() << 24; - return i; - } - - private static final float read32f(InputStream stream) throws IOException { - return Float.intBitsToFloat(read32i(stream)); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/readers/JPGBitmapReader.java b/src/org/sunflow/image/readers/JPGBitmapReader.java deleted file mode 100644 index 79635bf..0000000 --- a/src/org/sunflow/image/readers/JPGBitmapReader.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.sunflow.image.readers; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; - -import javax.imageio.ImageIO; - -import org.sunflow.image.Bitmap; -import org.sunflow.image.BitmapReader; -import org.sunflow.image.Color; -import org.sunflow.image.formats.BitmapRGB8; - -public class JPGBitmapReader implements BitmapReader { - public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException { - // regular image, load using Java api - ignore alpha channel - BufferedImage bi = ImageIO.read(new File(filename)); - int width = bi.getWidth(); - int height = bi.getHeight(); - byte[] pixels = new byte[3 * width * height]; - for (int y = 0, index = 0; y < height; y++) { - for (int x = 0; x < width; x++, index += 3) { - int argb = bi.getRGB(x, height - 1 - y); - pixels[index + 0] = (byte) (argb >> 16); - pixels[index + 1] = (byte) (argb >> 8); - pixels[index + 2] = (byte) argb; - } - } - if (!isLinear) { - for (int index = 0; index < pixels.length; index += 3) { - pixels[index + 0] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 0]); - pixels[index + 1] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 1]); - pixels[index + 2] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 2]); - } - } - return new BitmapRGB8(width, height, pixels); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/readers/PNGBitmapReader.java b/src/org/sunflow/image/readers/PNGBitmapReader.java deleted file mode 100644 index 5ba566c..0000000 --- a/src/org/sunflow/image/readers/PNGBitmapReader.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.sunflow.image.readers; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; - -import javax.imageio.ImageIO; - -import org.sunflow.image.Bitmap; -import org.sunflow.image.BitmapReader; -import org.sunflow.image.Color; -import org.sunflow.image.formats.BitmapRGBA8; - -public class PNGBitmapReader implements BitmapReader { - public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException { - // regular image, load using Java api - BufferedImage bi = ImageIO.read(new File(filename)); - int width = bi.getWidth(); - int height = bi.getHeight(); - byte[] pixels = new byte[4 * width * height]; - for (int y = 0, index = 0; y < height; y++) { - for (int x = 0; x < width; x++, index += 4) { - int argb = bi.getRGB(x, height - 1 - y); - pixels[index + 0] = (byte) (argb >> 16); - pixels[index + 1] = (byte) (argb >> 8); - pixels[index + 2] = (byte) argb; - pixels[index + 3] = (byte) (argb >> 24); - } - } - if (!isLinear) { - for (int index = 0; index < pixels.length; index += 4) { - pixels[index + 0] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 0]); - pixels[index + 1] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 1]); - pixels[index + 2] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 2]); - } - } - return new BitmapRGBA8(width, height, pixels); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/readers/TGABitmapReader.java b/src/org/sunflow/image/readers/TGABitmapReader.java deleted file mode 100644 index 2c4145f..0000000 --- a/src/org/sunflow/image/readers/TGABitmapReader.java +++ /dev/null @@ -1,131 +0,0 @@ -package org.sunflow.image.readers; - -import java.io.BufferedInputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -import org.sunflow.image.Bitmap; -import org.sunflow.image.BitmapReader; -import org.sunflow.image.Color; -import org.sunflow.image.formats.BitmapG8; -import org.sunflow.image.formats.BitmapRGB8; -import org.sunflow.image.formats.BitmapRGBA8; - -public class TGABitmapReader implements BitmapReader { - private static final int[] CHANNEL_INDEX = { 2, 1, 0, 3 }; - - public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException { - InputStream f = new BufferedInputStream(new FileInputStream(filename)); - byte[] read = new byte[4]; - - // read header - int idsize = f.read(); - int cmaptype = f.read(); // cmap byte (unsupported) - if (cmaptype != 0) - throw new BitmapFormatException(String.format("Colormapping (type: %d) is unsupported", cmaptype)); - int datatype = f.read(); - - // colormap info (5 bytes ignored) - f.read(); - f.read(); - f.read(); - f.read(); - f.read(); - - f.read(); // xstart, 16 bits (ignored) - f.read(); - f.read(); // ystart, 16 bits (ignored) - f.read(); - - // read resolution - int width = f.read(); - width |= f.read() << 8; - int height = f.read(); - height |= f.read() << 8; - - int bits = f.read(); - int bpp = bits / 8; - - int imgdscr = f.read(); - - // skip image ID if present - if (idsize != 0) - f.skip(idsize); - - // allocate byte buffer to hold the image - byte[] pixels = new byte[width * height * bpp]; - if (datatype == 2 || datatype == 3) { - if (bpp != 1 && bpp != 3 && bpp != 4) - throw new BitmapFormatException(String.format("Invalid bit depth in uncompressed TGA: %d", bits)); - // uncompressed image - for (int ptr = 0; ptr < pixels.length; ptr += bpp) { - // read bytes - f.read(read, 0, bpp); - for (int i = 0; i < bpp; i++) - pixels[ptr + CHANNEL_INDEX[i]] = read[i]; - } - } else if (datatype == 10) { - if (bpp != 3 && bpp != 4) - throw new BitmapFormatException(String.format("Invalid bit depth in run-length encoded TGA: %d", bits)); - // RLE encoded image - for (int ptr = 0; ptr < pixels.length;) { - int rle = f.read(); - int num = 1 + (rle & 0x7F); - if ((rle & 0x80) != 0) { - // rle packet - decode length and copy pixel - f.read(read, 0, bpp); - for (int j = 0; j < num; j++) { - for (int i = 0; i < bpp; i++) - pixels[ptr + CHANNEL_INDEX[i]] = read[i]; - ptr += bpp; - } - } else { - // raw packet - decode length and read pixels - for (int j = 0; j < num; j++) { - f.read(read, 0, bpp); - for (int i = 0; i < bpp; i++) - pixels[ptr + CHANNEL_INDEX[i]] = read[i]; - ptr += bpp; - } - } - } - } else - throw new BitmapFormatException(String.format("Unsupported TGA image type: %d", datatype)); - - if (!isLinear) { - // apply reverse correction - for (int ptr = 0; ptr < pixels.length; ptr += bpp) { - for (int i = 0; i < 3 && i < bpp; i++) - pixels[ptr + i] = Color.NATIVE_SPACE.rgbToLinear(pixels[ptr + i]); - } - } - - // should image be flipped in Y? - if ((imgdscr & 32) == 32) { - for (int y = 0, pix_ptr = 0; y < (height / 2); y++) { - int bot_ptr = bpp * (height - y - 1) * width; - for (int x = 0; x < width; x++) { - for (int i = 0; i < bpp; i++) { - byte t = pixels[pix_ptr + i]; - pixels[pix_ptr + i] = pixels[bot_ptr + i]; - pixels[bot_ptr + i] = t; - } - pix_ptr += bpp; - bot_ptr += bpp; - } - } - - } - f.close(); - switch (bpp) { - case 1: - return new BitmapG8(width, height, pixels); - case 3: - return new BitmapRGB8(width, height, pixels); - case 4: - return new BitmapRGBA8(width, height, pixels); - } - throw new BitmapFormatException("Inconsistent code in TGA reader"); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/writers/EXRBitmapWriter.java b/src/org/sunflow/image/writers/EXRBitmapWriter.java deleted file mode 100644 index d3b4f9f..0000000 --- a/src/org/sunflow/image/writers/EXRBitmapWriter.java +++ /dev/null @@ -1,377 +0,0 @@ -package org.sunflow.image.writers; - -import java.io.IOException; -import java.io.RandomAccessFile; -import java.util.Arrays; -import java.util.zip.Deflater; - -import org.sunflow.image.BitmapWriter; -import org.sunflow.image.Color; -import org.sunflow.system.ByteUtil; -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public class EXRBitmapWriter implements BitmapWriter { - private static final byte HALF = 1; - private static final byte FLOAT = 2; - private static final int HALF_SIZE = 2; - private static final int FLOAT_SIZE = 4; - private final static int OE_MAGIC = 20000630; - private final static int OE_EXR_VERSION = 2; - private final static int OE_TILED_FLAG = 0x00000200; - private static final int NO_COMPRESSION = 0; - private static final int RLE_COMPRESSION = 1; - private static final int ZIP_COMPRESSION = 3; - private static final int RLE_MIN_RUN = 3; - private static final int RLE_MAX_RUN = 127; - - private String filename; - private RandomAccessFile file; - private long[][] tileOffsets; - private long tileOffsetsPosition; - private int tilesX; - private int tilesY; - private int tileSize; - private int compression; - private byte channelType; - private int channelSize; - private byte[] tmpbuf; - private byte[] comprbuf; - - public EXRBitmapWriter() { - // default settings - configure("compression", "zip"); - configure("channeltype", "half"); - } - - public void configure(String option, String value) { - if (option.equals("compression")) { - if (value.equals("none")) - compression = NO_COMPRESSION; - else if (value.equals("rle")) - compression = RLE_COMPRESSION; - else if (value.equals("zip")) - compression = ZIP_COMPRESSION; - else { - UI.printWarning(Module.IMG, "EXR - Compression type was not recognized - defaulting to zip"); - compression = ZIP_COMPRESSION; - } - } else if (option.equals("channeltype")) { - if (value.equals("float")) { - channelType = FLOAT; - channelSize = FLOAT_SIZE; - } else if (value.equals("half")) { - channelType = HALF; - channelSize = HALF_SIZE; - } else { - UI.printWarning(Module.DISP, "EXR - Channel type was not recognized - defaulting to float"); - channelType = FLOAT; - channelSize = FLOAT_SIZE; - } - } - } - - public void openFile(String filename) throws IOException { - this.filename = filename == null ? "output.exr" : filename; - } - - public void writeHeader(int width, int height, int tileSize) throws IOException, UnsupportedOperationException { - file = new RandomAccessFile(filename, "rw"); - file.setLength(0); - if (tileSize <= 0) - throw new UnsupportedOperationException("Can't use OpenEXR bitmap writer without buckets."); - writeRGBAHeader(width, height, tileSize); - } - - public void writeTile(int x, int y, int w, int h, Color[] color, float[] alpha) throws IOException { - int tx = x / tileSize; - int ty = y / tileSize; - writeEXRTile(tx, ty, w, h, color, alpha); - } - - public void closeFile() throws IOException { - writeTileOffsets(); - file.close(); - } - - private void writeRGBAHeader(int w, int h, int tileSize) throws IOException { - byte[] chanOut = { 0, channelType, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, - 0, 0, 0 }; - - file.write(ByteUtil.get4Bytes(OE_MAGIC)); - - file.write(ByteUtil.get4Bytes(OE_EXR_VERSION | OE_TILED_FLAG)); - - file.write("channels".getBytes()); - file.write(0); - file.write("chlist".getBytes()); - file.write(0); - file.write(ByteUtil.get4Bytes(73)); - file.write("R".getBytes()); - file.write(chanOut); - file.write("G".getBytes()); - file.write(chanOut); - file.write("B".getBytes()); - file.write(chanOut); - file.write("A".getBytes()); - file.write(chanOut); - file.write(0); - - // compression - file.write("compression".getBytes()); - file.write(0); - file.write("compression".getBytes()); - file.write(0); - file.write(1); - file.write(ByteUtil.get4BytesInv(compression)); - - // datawindow =~ image size - file.write("dataWindow".getBytes()); - file.write(0); - file.write("box2i".getBytes()); - file.write(0); - file.write(ByteUtil.get4Bytes(0x10)); - file.write(ByteUtil.get4Bytes(0)); - file.write(ByteUtil.get4Bytes(0)); - file.write(ByteUtil.get4Bytes(w - 1)); - file.write(ByteUtil.get4Bytes(h - 1)); - - // dispwindow -> look at openexr.com for more info - file.write("displayWindow".getBytes()); - file.write(0); - file.write("box2i".getBytes()); - file.write(0); - file.write(ByteUtil.get4Bytes(0x10)); - file.write(ByteUtil.get4Bytes(0)); - file.write(ByteUtil.get4Bytes(0)); - file.write(ByteUtil.get4Bytes(w - 1)); - file.write(ByteUtil.get4Bytes(h - 1)); - - /* - * lines in increasing y order = 0 decreasing would be 1 - */ - file.write("lineOrder".getBytes()); - file.write(0); - file.write("lineOrder".getBytes()); - file.write(0); - file.write(1); - file.write(ByteUtil.get4BytesInv(2)); - - file.write("pixelAspectRatio".getBytes()); - file.write(0); - file.write("float".getBytes()); - file.write(0); - file.write(ByteUtil.get4Bytes(4)); - file.write(ByteUtil.get4Bytes(Float.floatToIntBits(1))); - - // meaningless to a flat (2D) image - file.write("screenWindowCenter".getBytes()); - file.write(0); - file.write("v2f".getBytes()); - file.write(0); - file.write(ByteUtil.get4Bytes(8)); - file.write(ByteUtil.get4Bytes(Float.floatToIntBits(0))); - file.write(ByteUtil.get4Bytes(Float.floatToIntBits(0))); - - // meaningless to a flat (2D) image - file.write("screenWindowWidth".getBytes()); - file.write(0); - file.write("float".getBytes()); - file.write(0); - file.write(ByteUtil.get4Bytes(4)); - file.write(ByteUtil.get4Bytes(Float.floatToIntBits(1))); - - this.tileSize = tileSize; - - tilesX = ((w + tileSize - 1) / tileSize); - tilesY = ((h + tileSize - 1) / tileSize); - - /* - * twice the space for the compressing buffer, as for ex. the compressor - * can actually increase the size of the data :) If that happens though, - * it is not saved into the file, but discarded - */ - tmpbuf = new byte[tileSize * tileSize * channelSize * 4]; - comprbuf = new byte[tileSize * tileSize * channelSize * 4 * 2]; - - tileOffsets = new long[tilesX][tilesY]; - - file.write("tiles".getBytes()); - file.write(0); - file.write("tiledesc".getBytes()); - file.write(0); - file.write(ByteUtil.get4Bytes(9)); - - file.write(ByteUtil.get4Bytes(tileSize)); - file.write(ByteUtil.get4Bytes(tileSize)); - - // ONE_LEVEL tiles, ROUNDING_MODE = not important - file.write(0); - - // an attribute with a name of 0 to end the list - file.write(0); - - // save a pointer to where the tileOffsets are stored and write dummy - // fillers for now - tileOffsetsPosition = file.getFilePointer(); - writeTileOffsets(); - } - - private void writeTileOffsets() throws IOException { - file.seek(tileOffsetsPosition); - for (int ty = 0; ty < tilesY; ty++) - for (int tx = 0; tx < tilesX; tx++) - file.write(ByteUtil.get8Bytes(tileOffsets[tx][ty])); - } - - private synchronized void writeEXRTile(int tileX, int tileY, int w, int h, Color[] tile, float[] alpha) throws IOException { - byte[] rgb; - - // setting comprSize to max integer so without compression things - // don't go awry - int pixptr = 0, writeSize = 0, comprSize = Integer.MAX_VALUE; - int tileRangeX = (tileSize < w) ? tileSize : w; - int tileRangeY = (tileSize < h) ? tileSize : h; - int channelBase = tileRangeX * channelSize; - - // lets see if the alignment matches, you can comment this out if - // need be - if ((tileSize != tileRangeX) && (tileX == 0)) - System.out.print(" bad X alignment "); - if ((tileSize != tileRangeY) && (tileY == 0)) - System.out.print(" bad Y alignment "); - - tileOffsets[tileX][tileY] = file.getFilePointer(); - - // the tile header: tile's x&y coordinate, levels x&y coordinate and - // tilesize - file.write(ByteUtil.get4Bytes(tileX)); - file.write(ByteUtil.get4Bytes(tileY)); - file.write(ByteUtil.get4Bytes(0)); - file.write(ByteUtil.get4Bytes(0)); - - // just in case - Arrays.fill(tmpbuf, (byte) 0); - - for (int ty = 0; ty < tileRangeY; ty++) { - for (int tx = 0; tx < tileRangeX; tx++) { - float[] rgbf = tile[tx + ty * tileRangeX].getRGB(); - if (channelType == FLOAT) { - rgb = ByteUtil.get4Bytes(Float.floatToRawIntBits(alpha[tx + ty * tileRangeX])); - tmpbuf[pixptr + 0] = rgb[0]; - tmpbuf[pixptr + 1] = rgb[1]; - tmpbuf[pixptr + 2] = rgb[2]; - tmpbuf[pixptr + 3] = rgb[3]; - } else if (channelType == HALF) { - rgb = ByteUtil.get2Bytes(ByteUtil.floatToHalf(alpha[tx + ty * tileRangeX])); - tmpbuf[pixptr + 0] = rgb[0]; - tmpbuf[pixptr + 1] = rgb[1]; - } - for (int component = 1; component <= 3; component++) { - if (channelType == FLOAT) { - rgb = ByteUtil.get4Bytes(Float.floatToRawIntBits(rgbf[3 - component])); - tmpbuf[(channelBase * component) + pixptr + 0] = rgb[0]; - tmpbuf[(channelBase * component) + pixptr + 1] = rgb[1]; - tmpbuf[(channelBase * component) + pixptr + 2] = rgb[2]; - tmpbuf[(channelBase * component) + pixptr + 3] = rgb[3]; - } else if (channelType == HALF) { - rgb = ByteUtil.get2Bytes(ByteUtil.floatToHalf(rgbf[3 - component])); - tmpbuf[(channelBase * component) + pixptr + 0] = rgb[0]; - tmpbuf[(channelBase * component) + pixptr + 1] = rgb[1]; - } - } - pixptr += channelSize; - } - pixptr += (tileRangeX * channelSize * 3); - } - - writeSize = tileRangeX * tileRangeY * channelSize * 4; - - if (compression != NO_COMPRESSION) - comprSize = compress(compression, tmpbuf, writeSize, comprbuf); - - // lastly, write the size of the tile and the tile itself - // (compressed or not) - if (comprSize < writeSize) { - file.write(ByteUtil.get4Bytes(comprSize)); - file.write(comprbuf, 0, comprSize); - } else { - file.write(ByteUtil.get4Bytes(writeSize)); - file.write(tmpbuf, 0, writeSize); - } - } - - private static final int compress(int tp, byte[] in, int inSize, byte[] out) { - if (inSize == 0) - return 0; - - int t1 = 0, t2 = (inSize + 1) / 2; - int inPtr = 0, ret; - byte[] tmp = new byte[inSize]; - - // zip and rle treat the data first, in the same way so I'm not - // repeating the code - if ((tp == ZIP_COMPRESSION) || (tp == RLE_COMPRESSION)) { - // reorder the pixel data ~ straight from ImfZipCompressor.cpp :) - while (true) { - if (inPtr < inSize) - tmp[t1++] = in[inPtr++]; - else - break; - - if (inPtr < inSize) - tmp[t2++] = in[inPtr++]; - else - break; - } - - // Predictor ~ straight from ImfZipCompressor.cpp :) - t1 = 1; - int p = tmp[t1 - 1]; - while (t1 < inSize) { - int d = tmp[t1] - p + (128 + 256); - p = tmp[t1]; - tmp[t1] = (byte) d; - t1++; - } - } - - // We'll just jump from here to the wanted compress/decompress stuff if - // need be - switch (tp) { - case ZIP_COMPRESSION: - Deflater def = new Deflater(Deflater.DEFAULT_COMPRESSION, false); - def.setInput(tmp, 0, inSize); - def.finish(); - ret = def.deflate(out); - return ret; - case RLE_COMPRESSION: - return rleCompress(tmp, inSize, out); - default: - return -1; - } - } - - private static final int rleCompress(byte[] in, int inLen, byte[] out) { - int runStart = 0, runEnd = 1, outWrite = 0; - while (runStart < inLen) { - while (runEnd < inLen && in[runStart] == in[runEnd] && (runEnd - runStart - 1) < RLE_MAX_RUN) - runEnd++; - if (runEnd - runStart >= RLE_MIN_RUN) { - // Compressable run - out[outWrite++] = (byte) ((runEnd - runStart) - 1); - out[outWrite++] = in[runStart]; - runStart = runEnd; - } else { - // Uncompressable run - while (runEnd < inLen && (((runEnd + 1) >= inLen || in[runEnd] != in[runEnd + 1]) || ((runEnd + 2) >= inLen || in[runEnd + 1] != in[runEnd + 2])) && (runEnd - runStart) < RLE_MAX_RUN) - runEnd++; - out[outWrite++] = (byte) (runStart - runEnd); - while (runStart < runEnd) - out[outWrite++] = in[runStart++]; - } - runEnd++; - } - return outWrite; - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/writers/HDRBitmapWriter.java b/src/org/sunflow/image/writers/HDRBitmapWriter.java deleted file mode 100644 index 32c22f5..0000000 --- a/src/org/sunflow/image/writers/HDRBitmapWriter.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.sunflow.image.writers; - -import java.io.BufferedOutputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -import org.sunflow.image.BitmapWriter; -import org.sunflow.image.Color; -import org.sunflow.image.ColorEncoder; - -public class HDRBitmapWriter implements BitmapWriter { - private String filename; - private int width, height; - private int[] data; - - public void configure(String option, String value) { - } - - public void openFile(String filename) throws IOException { - this.filename = filename; - } - - public void writeHeader(int width, int height, int tileSize) throws IOException, UnsupportedOperationException { - this.width = width; - this.height = height; - data = new int[width * height]; - } - - public void writeTile(int x, int y, int w, int h, Color[] color, float[] alpha) throws IOException { - int[] tileData = ColorEncoder.encodeRGBE(color); - for (int j = 0, index = 0, pixel = x + y * width; j < h; j++, pixel += width - w) - for (int i = 0; i < w; i++, index++, pixel++) - data[pixel] = tileData[index]; - } - - public void closeFile() throws IOException { - OutputStream f = new BufferedOutputStream(new FileOutputStream(filename)); - f.write("#?RGBE\n".getBytes()); - f.write("FORMAT=32-bit_rle_rgbe\n\n".getBytes()); - f.write(("-Y " + height + " +X " + width + "\n").getBytes()); - for (int i = 0; i < data.length; i++) { - int rgbe = data[i]; - f.write(rgbe >> 24); - f.write(rgbe >> 16); - f.write(rgbe >> 8); - f.write(rgbe); - } - f.close(); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/writers/IGIBitmapWriter.java b/src/org/sunflow/image/writers/IGIBitmapWriter.java deleted file mode 100644 index bd3211e..0000000 --- a/src/org/sunflow/image/writers/IGIBitmapWriter.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.sunflow.image.writers; - -import java.io.BufferedOutputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -import org.sunflow.image.BitmapWriter; -import org.sunflow.image.Color; -import org.sunflow.image.XYZColor; - -/** - * Writes images in Indigo's native XYZ format. - * http://www2.indigorenderer.com/joomla/forum/viewtopic.php?p=11430 - */ -public class IGIBitmapWriter implements BitmapWriter { - private String filename; - private int width, height; - private float[] xyz; - - public void configure(String option, String value) { - } - - public void openFile(String filename) throws IOException { - this.filename = filename; - } - - public void writeHeader(int width, int height, int tileSize) throws IOException, UnsupportedOperationException { - this.width = width; - this.height = height; - xyz = new float[width * height * 3]; - } - - public void writeTile(int x, int y, int w, int h, Color[] color, float[] alpha) throws IOException { - for (int j = 0, index = 0, pixel = 3 * (x + y * width); j < h; j++, pixel += 3 * (width - w)) { - for (int i = 0; i < w; i++, index++, pixel += 3) { - XYZColor c = Color.NATIVE_SPACE.convertRGBtoXYZ(color[index]); - xyz[pixel + 0] = c.getX(); - xyz[pixel + 1] = c.getY(); - xyz[pixel + 2] = c.getZ(); - } - } - } - - public void closeFile() throws IOException { - OutputStream stream = new BufferedOutputStream(new FileOutputStream(filename)); - write32(stream, 66613373); // magic number - write32(stream, 1); // version - write32(stream, 0); // this should be a double - assume it won't be used - write32(stream, 0); - write32(stream, width); - write32(stream, height); - write32(stream, 1); // super sampling factor - write32(stream, 0); // compression - write32(stream, width * height * 12); // data size - write32(stream, 0); // colorspace - stream.write(new byte[5000]); - for (float f : xyz) - write32(stream, f); - stream.close(); - } - - private static final void write32(OutputStream stream, int i) throws IOException { - stream.write(i & 0xFF); - stream.write((i >> 8) & 0xFF); - stream.write((i >> 16) & 0xFF); - stream.write((i >> 24) & 0xFF); - } - - private static final void write32(OutputStream stream, float f) throws IOException { - write32(stream, Float.floatToIntBits(f)); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/writers/PNGBitmapWriter.java b/src/org/sunflow/image/writers/PNGBitmapWriter.java deleted file mode 100644 index a638443..0000000 --- a/src/org/sunflow/image/writers/PNGBitmapWriter.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.sunflow.image.writers; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; - -import javax.imageio.ImageIO; - -import org.sunflow.image.BitmapWriter; -import org.sunflow.image.Color; - -public class PNGBitmapWriter implements BitmapWriter { - private String filename; - private BufferedImage image; - - public void configure(String option, String value) { - } - - public void openFile(String filename) throws IOException { - this.filename = filename; - } - - public void writeHeader(int width, int height, int tileSize) throws IOException, UnsupportedOperationException { - image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - } - - public void writeTile(int x, int y, int w, int h, Color[] color, float[] alpha) throws IOException { - for (int j = 0, index = 0; j < h; j++) - for (int i = 0; i < w; i++, index++) - image.setRGB(x + i, y + j, color[index].copy().mul(1.0f / alpha[index]).toNonLinear().toRGBA(alpha[index])); - } - - public void closeFile() throws IOException { - ImageIO.write(image, "png", new File(filename)); - } -} \ No newline at end of file diff --git a/src/org/sunflow/image/writers/TGABitmapWriter.java b/src/org/sunflow/image/writers/TGABitmapWriter.java deleted file mode 100644 index e2c5a5c..0000000 --- a/src/org/sunflow/image/writers/TGABitmapWriter.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.sunflow.image.writers; - -import java.io.BufferedOutputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -import org.sunflow.image.BitmapWriter; -import org.sunflow.image.Color; -import org.sunflow.image.ColorEncoder; - -public class TGABitmapWriter implements BitmapWriter { - private String filename; - private int width, height; - private byte[] data; - - public void configure(String option, String value) { - } - - public void openFile(String filename) throws IOException { - this.filename = filename; - } - - public void writeHeader(int width, int height, int tileSize) throws IOException, UnsupportedOperationException { - this.width = width; - this.height = height; - data = new byte[width * height * 4]; // RGBA8 - } - - public void writeTile(int x, int y, int w, int h, Color[] color, float[] alpha) throws IOException { - color = ColorEncoder.unlinearize(color); // gamma correction - byte[] tileData = ColorEncoder.quantizeRGBA8(color, alpha); - for (int j = 0, index = 0; j < h; j++) { - int imageIndex = 4 * (x + (height - 1 - (y + j)) * width); - for (int i = 0; i < w; i++, index += 4, imageIndex += 4) { - // swap bytes around so buffer is in native BGRA order - data[imageIndex + 0] = tileData[index + 2]; - data[imageIndex + 1] = tileData[index + 1]; - data[imageIndex + 2] = tileData[index + 0]; - data[imageIndex + 3] = tileData[index + 3]; - } - } - } - - public void closeFile() throws IOException { - // actually write the file from here - OutputStream f = new BufferedOutputStream(new FileOutputStream(filename)); - // no id, no colormap, uncompressed 32bpp RGBA - byte[] tgaHeader = { 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - f.write(tgaHeader); - // then the size info - f.write(width & 0xFF); - f.write((width >> 8) & 0xFF); - f.write(height & 0xFF); - f.write((height >> 8) & 0xFF); - // bitsperpixel and filler - f.write(32); - f.write(0); - f.write(data); // write image data bytes (already in BGRA order) - f.close(); - } -} \ No newline at end of file diff --git a/src/org/sunflow/math/BoundingBox.java b/src/org/sunflow/math/BoundingBox.java deleted file mode 100644 index 992d17b..0000000 --- a/src/org/sunflow/math/BoundingBox.java +++ /dev/null @@ -1,315 +0,0 @@ -package org.sunflow.math; - -/** - * 3D axis-aligned bounding box. Stores only the minimum and maximum corner - * points. - */ -public class BoundingBox { - private Point3 minimum; - private Point3 maximum; - - /** - * Creates an empty box. The minimum point will have all components set to - * positive infinity, and the maximum will have all components set to - * negative infinity. - */ - public BoundingBox() { - minimum = new Point3(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY); - maximum = new Point3(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); - } - - /** - * Creates a copy of the given box. - * - * @param b bounding box to copy - */ - public BoundingBox(BoundingBox b) { - minimum = new Point3(b.minimum); - maximum = new Point3(b.maximum); - } - - /** - * Creates a bounding box containing only the specified point. - * - * @param p point to include - */ - public BoundingBox(Point3 p) { - this(p.x, p.y, p.z); - } - - /** - * Creates a bounding box containing only the specified point. - * - * @param x x coordinate of the point to include - * @param y y coordinate of the point to include - * @param z z coordinate of the point to include - */ - public BoundingBox(float x, float y, float z) { - minimum = new Point3(x, y, z); - maximum = new Point3(x, y, z); - } - - /** - * Creates a bounding box centered around the origin. - * - * @param size half edge length of the bounding box - */ - public BoundingBox(float size) { - minimum = new Point3(-size, -size, -size); - maximum = new Point3(size, size, size); - } - - /** - * Gets the minimum corner of the box. That is the corner of smallest - * coordinates on each axis. Note that the returned reference is not cloned - * for efficiency purposes so care must be taken not to change the - * coordinates of the point. - * - * @return a reference to the minimum corner - */ - public final Point3 getMinimum() { - return minimum; - } - - /** - * Gets the maximum corner of the box. That is the corner of largest - * coordinates on each axis. Note that the returned reference is not cloned - * for efficiency purposes so care must be taken not to change the - * coordinates of the point. - * - * @return a reference to the maximum corner - */ - public final Point3 getMaximum() { - return maximum; - } - - /** - * Gets the center of the box, computed as (min + max) / 2. - * - * @return a reference to the center of the box - */ - public final Point3 getCenter() { - return Point3.mid(minimum, maximum, new Point3()); - } - - /** - * Gets a corner of the bounding box. The index scheme uses the binary - * representation of the index to decide which corner to return. Corner 0 is - * equivalent to the minimum and corner 7 is equivalent to the maximum. - * - * @param i a corner index, from 0 to 7 - * @return the corresponding corner - */ - public final Point3 getCorner(int i) { - float x = (i & 1) == 0 ? minimum.x : maximum.x; - float y = (i & 2) == 0 ? minimum.y : maximum.y; - float z = (i & 4) == 0 ? minimum.z : maximum.z; - return new Point3(x, y, z); - } - - /** - * Gets a specific coordinate of the surface's bounding box. - * - * @param i index of a side from 0 to 5 - * @return value of the request bounding box side - */ - public final float getBound(int i) { - switch (i) { - case 0: - return minimum.x; - case 1: - return maximum.x; - case 2: - return minimum.y; - case 3: - return maximum.y; - case 4: - return minimum.z; - case 5: - return maximum.z; - default: - return 0; - } - } - - /** - * Gets the extents vector for the box. This vector is computed as (max - - * min). Its coordinates are always positive and represent the dimensions of - * the box along the three axes. - * - * @return a refreence to the extent vector - * @see org.sunflow.math.Vector3#length() - */ - public final Vector3 getExtents() { - return Point3.sub(maximum, minimum, new Vector3()); - } - - /** - * Gets the surface area of the box. - * - * @return surface area - */ - public final float getArea() { - Vector3 w = getExtents(); - float ax = Math.max(w.x, 0); - float ay = Math.max(w.y, 0); - float az = Math.max(w.z, 0); - return 2 * (ax * ay + ay * az + az * ax); - } - - /** - * Gets the box's volume - * - * @return volume - */ - public final float getVolume() { - Vector3 w = getExtents(); - float ax = Math.max(w.x, 0); - float ay = Math.max(w.y, 0); - float az = Math.max(w.z, 0); - return ax * ay * az; - } - - /** - * Enlarge the bounding box by the minimum possible amount to avoid numeric - * precision related problems. - */ - public final void enlargeUlps() { - final float eps = 0.0001f; - minimum.x -= Math.max(eps, Math.ulp(minimum.x)); - minimum.y -= Math.max(eps, Math.ulp(minimum.y)); - minimum.z -= Math.max(eps, Math.ulp(minimum.z)); - maximum.x += Math.max(eps, Math.ulp(maximum.x)); - maximum.y += Math.max(eps, Math.ulp(maximum.y)); - maximum.z += Math.max(eps, Math.ulp(maximum.z)); - } - - /** - * Returns true when the box has just been initialized, and - * is still empty. This method might also return true if the state of the - * box becomes inconsistent and some component of the minimum corner is - * larger than the corresponding coordinate of the maximum corner. - * - * @return true if the box is empty, false - * otherwise - */ - public final boolean isEmpty() { - return (maximum.x < minimum.x) || (maximum.y < minimum.y) || (maximum.z < minimum.z); - } - - /** - * Returns true if the specified bounding box intersects this - * one. The boxes are treated as volumes, so a box inside another will - * return true. Returns false if the parameter is - * null. - * - * @param b box to be tested for intersection - * @return true if the boxes overlap, false - * otherwise - */ - public final boolean intersects(BoundingBox b) { - return ((b != null) && (minimum.x <= b.maximum.x) && (maximum.x >= b.minimum.x) && (minimum.y <= b.maximum.y) && (maximum.y >= b.minimum.y) && (minimum.z <= b.maximum.z) && (maximum.z >= b.minimum.z)); - } - - /** - * Checks to see if the specified {@link org.sunflow.math.Point3 point}is - * inside the volume defined by this box. Returns false if - * the parameter is null. - * - * @param p point to be tested for containment - * @return true if the point is inside the box, - * false otherwise - */ - public final boolean contains(Point3 p) { - return ((p != null) && (p.x >= minimum.x) && (p.x <= maximum.x) && (p.y >= minimum.y) && (p.y <= maximum.y) && (p.z >= minimum.z) && (p.z <= maximum.z)); - } - - /** - * Check to see if the specified point is inside the volume defined by this - * box. - * - * @param x x coordinate of the point to be tested - * @param y y coordinate of the point to be tested - * @param z z coordinate of the point to be tested - * @return true if the point is inside the box, - * false otherwise - */ - public final boolean contains(float x, float y, float z) { - return ((x >= minimum.x) && (x <= maximum.x) && (y >= minimum.y) && (y <= maximum.y) && (z >= minimum.z) && (z <= maximum.z)); - } - - /** - * Changes the extents of the box as needed to include the given - * {@link org.sunflow.math.Point3 point}into this box. Does nothing if the - * parameter is null. - * - * @param p point to be included - */ - public final void include(Point3 p) { - if (p != null) { - if (p.x < minimum.x) - minimum.x = p.x; - if (p.x > maximum.x) - maximum.x = p.x; - if (p.y < minimum.y) - minimum.y = p.y; - if (p.y > maximum.y) - maximum.y = p.y; - if (p.z < minimum.z) - minimum.z = p.z; - if (p.z > maximum.z) - maximum.z = p.z; - } - } - - /** - * Changes the extents of the box as needed to include the given point into - * this box. - * - * @param x x coordinate of the point - * @param y y coordinate of the point - * @param z z coordinate of the point - */ - public final void include(float x, float y, float z) { - if (x < minimum.x) - minimum.x = x; - if (x > maximum.x) - maximum.x = x; - if (y < minimum.y) - minimum.y = y; - if (y > maximum.y) - maximum.y = y; - if (z < minimum.z) - minimum.z = z; - if (z > maximum.z) - maximum.z = z; - } - - /** - * Changes the extents of the box as needed to include the given box into - * this box. Does nothing if the parameter is null. - * - * @param b box to be included - */ - public final void include(BoundingBox b) { - if (b != null) { - if (b.minimum.x < minimum.x) - minimum.x = b.minimum.x; - if (b.maximum.x > maximum.x) - maximum.x = b.maximum.x; - if (b.minimum.y < minimum.y) - minimum.y = b.minimum.y; - if (b.maximum.y > maximum.y) - maximum.y = b.maximum.y; - if (b.minimum.z < minimum.z) - minimum.z = b.minimum.z; - if (b.maximum.z > maximum.z) - maximum.z = b.maximum.z; - } - } - - @Override - public final String toString() { - return String.format("(%.2f, %.2f, %.2f) to (%.2f, %.2f, %.2f)", minimum.x, minimum.y, minimum.z, maximum.x, maximum.y, maximum.z); - } -} \ No newline at end of file diff --git a/src/org/sunflow/math/MathUtils.java b/src/org/sunflow/math/MathUtils.java deleted file mode 100644 index f763d98..0000000 --- a/src/org/sunflow/math/MathUtils.java +++ /dev/null @@ -1,131 +0,0 @@ -package org.sunflow.math; - -public final class MathUtils { - private MathUtils() { - } - - public static final int clamp(int x, int min, int max) { - if (x > max) - return max; - if (x > min) - return x; - return min; - } - - public static final float clamp(float x, float min, float max) { - if (x > max) - return max; - if (x > min) - return x; - return min; - } - - public static final double clamp(double x, double min, double max) { - if (x > max) - return max; - if (x > min) - return x; - return min; - } - - public static final int min(int a, int b, int c) { - if (a > b) - a = b; - if (a > c) - a = c; - return a; - } - - public static final float min(float a, float b, float c) { - if (a > b) - a = b; - if (a > c) - a = c; - return a; - } - - public static final double min(double a, double b, double c) { - if (a > b) - a = b; - if (a > c) - a = c; - return a; - } - - public static final float min(float a, float b, float c, float d) { - if (a > b) - a = b; - if (a > c) - a = c; - if (a > d) - a = d; - return a; - } - - public static final int max(int a, int b, int c) { - if (a < b) - a = b; - if (a < c) - a = c; - return a; - } - - public static final float max(float a, float b, float c) { - if (a < b) - a = b; - if (a < c) - a = c; - return a; - } - - public static final double max(double a, double b, double c) { - if (a < b) - a = b; - if (a < c) - a = c; - return a; - } - - public static final float max(float a, float b, float c, float d) { - if (a < b) - a = b; - if (a < c) - a = c; - if (a < d) - a = d; - return a; - } - - public static final float smoothStep(float a, float b, float x) { - if (x <= a) - return 0; - if (x >= b) - return 1; - float t = clamp((x - a) / (b - a), 0.0f, 1.0f); - return t * t * (3 - 2 * t); - } - - public static final float frac(float x) { - return x < 0 ? x - (int) x + 1 : x - (int) x; - } - - /** - * Computes a fast approximation to Math.pow(a, b). Adapted - * from http://www.dctsystems.co.uk/Software/power.html. - * - * @param a a positive number - * @param b a number - * @return a^b - */ - public static final float fastPow(float a, float b) { - // adapted from: http://www.dctsystems.co.uk/Software/power.html - float x = Float.floatToRawIntBits(a); - x *= 1.0f / (1 << 23); - x = x - 127; - float y = x - (int) Math.floor(x); - b *= x + (y - y * y) * 0.346607f; - y = b - (int) Math.floor(b); - y = (y - y * y) * 0.33971f; - return Float.intBitsToFloat((int) ((b + 127 - y) * (1 << 23))); - } -} \ No newline at end of file diff --git a/src/org/sunflow/math/Matrix4.java b/src/org/sunflow/math/Matrix4.java deleted file mode 100644 index 8d74b7b..0000000 --- a/src/org/sunflow/math/Matrix4.java +++ /dev/null @@ -1,561 +0,0 @@ -package org.sunflow.math; - -/** - * This class is used to represent general affine transformations in 3D. The - * bottom row of the matrix is assumed to be [0,0,0,1]. Note that the rotation - * matrices assume a right-handed convention. - */ -public final class Matrix4 { - // matrix elements, m(row,col) - private float m00; - private float m01; - private float m02; - private float m03; - private float m10; - private float m11; - private float m12; - private float m13; - private float m20; - private float m21; - private float m22; - private float m23; - - // usefull constant matrices - public static final Matrix4 ZERO = new Matrix4(); - public static final Matrix4 IDENTITY = Matrix4.scale(1); - - /** - * Creates an empty matrix. All elements are 0. - */ - private Matrix4() { - } - - /** - * Creates a matrix with the specified elements - * - * @param m00 value at row 0, col 0 - * @param m01 value at row 0, col 1 - * @param m02 value at row 0, col 2 - * @param m03 value at row 0, col 3 - * @param m10 value at row 1, col 0 - * @param m11 value at row 1, col 1 - * @param m12 value at row 1, col 2 - * @param m13 value at row 1, col 3 - * @param m20 value at row 2, col 0 - * @param m21 value at row 2, col 1 - * @param m22 value at row 2, col 2 - * @param m23 value at row 2, col 3 - */ - public Matrix4(float m00, float m01, float m02, float m03, float m10, float m11, float m12, float m13, float m20, float m21, float m22, float m23) { - this.m00 = m00; - this.m01 = m01; - this.m02 = m02; - this.m03 = m03; - this.m10 = m10; - this.m11 = m11; - this.m12 = m12; - this.m13 = m13; - this.m20 = m20; - this.m21 = m21; - this.m22 = m22; - this.m23 = m23; - } - - /** - * Initialize a matrix from the specified 16 element array. The matrix may - * be given in row or column major form. - * - * @param m a 16 element array in row or column major form - * @param rowMajor true if the array is in row major form, - * falseif it is in column major form - */ - public Matrix4(float[] m, boolean rowMajor) { - if (rowMajor) { - m00 = m[0]; - m01 = m[1]; - m02 = m[2]; - m03 = m[3]; - m10 = m[4]; - m11 = m[5]; - m12 = m[6]; - m13 = m[7]; - m20 = m[8]; - m21 = m[9]; - m22 = m[10]; - m23 = m[11]; - if (m[12] != 0 || m[13] != 0 || m[14] != 0 || m[15] != 1) - throw new RuntimeException(String.format("Matrix is not affine! Bottom row is: [%.3f, %.3f, %.3f, %.3f]", m[12], m[13], m[14], m[15])); - } else { - m00 = m[0]; - m01 = m[4]; - m02 = m[8]; - m03 = m[12]; - m10 = m[1]; - m11 = m[5]; - m12 = m[9]; - m13 = m[13]; - m20 = m[2]; - m21 = m[6]; - m22 = m[10]; - m23 = m[14]; - if (m[3] != 0 || m[7] != 0 || m[11] != 0 || m[15] != 1) - throw new RuntimeException(String.format("Matrix is not affine! Bottom row is: [%.3f, %.3f, %.3f, %.3f]", m[12], m[13], m[14], m[15])); - } - } - - public final boolean isIndentity() { - return equals(IDENTITY); - } - - public final boolean equals(Matrix4 m) { - if (m == null) - return false; - if (this == m) - return true; - return m00 == m.m00 && m01 == m.m01 && m02 == m.m02 && m03 == m.m03 && m10 == m.m10 && m11 == m.m11 && m12 == m.m12 && m13 == m.m13 && m20 == m.m20 && m21 == m.m21 && m22 == m.m22 && m23 == m.m23; - } - - public final float[] asRowMajor() { - return new float[] { m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, - m22, m23, 0, 0, 0, 1 }; - } - - public final float[] asColMajor() { - return new float[] { m00, m10, m20, 0, m01, m11, m21, 0, m02, m12, m22, - 0, m03, m13, m23, 1 }; - } - - /** - * Compute the matrix determinant. - * - * @return determinant of this matrix - */ - public final float determinant() { - float A0 = m00 * m11 - m01 * m10; - float A1 = m00 * m12 - m02 * m10; - float A3 = m01 * m12 - m02 * m11; - - return A0 * m22 - A1 * m21 + A3 * m20; - } - - /** - * Compute the inverse of this matrix and return it as a new object. If the - * matrix is not invertible, null is returned. - * - * @return the inverse of this matrix, or null if not - * invertible - */ - public final Matrix4 inverse() { - float A0 = m00 * m11 - m01 * m10; - float A1 = m00 * m12 - m02 * m10; - float A3 = m01 * m12 - m02 * m11; - float det = A0 * m22 - A1 * m21 + A3 * m20; - if (Math.abs(det) < 1e-12f) - return null; // matrix is not invertible - float invDet = 1 / det; - float A2 = m00 * m13 - m03 * m10; - float A4 = m01 * m13 - m03 * m11; - float A5 = m02 * m13 - m03 * m12; - Matrix4 inv = new Matrix4(); - inv.m00 = (+m11 * m22 - m12 * m21) * invDet; - inv.m10 = (-m10 * m22 + m12 * m20) * invDet; - inv.m20 = (+m10 * m21 - m11 * m20) * invDet; - inv.m01 = (-m01 * m22 + m02 * m21) * invDet; - inv.m11 = (+m00 * m22 - m02 * m20) * invDet; - inv.m21 = (-m00 * m21 + m01 * m20) * invDet; - inv.m02 = +A3 * invDet; - inv.m12 = -A1 * invDet; - inv.m22 = +A0 * invDet; - inv.m03 = (-m21 * A5 + m22 * A4 - m23 * A3) * invDet; - inv.m13 = (+m20 * A5 - m22 * A2 + m23 * A1) * invDet; - inv.m23 = (-m20 * A4 + m21 * A2 - m23 * A0) * invDet; - return inv; - } - - /** - * Computes this*m and return the result as a new Matrix4 - * - * @param m right hand side of the multiplication - * @return a new Matrix4 object equal to this*m - */ - public final Matrix4 multiply(Matrix4 m) { - // matrix multiplication is m[r][c] = (row[r]).(col[c]) - float rm00 = m00 * m.m00 + m01 * m.m10 + m02 * m.m20; - float rm01 = m00 * m.m01 + m01 * m.m11 + m02 * m.m21; - float rm02 = m00 * m.m02 + m01 * m.m12 + m02 * m.m22; - float rm03 = m00 * m.m03 + m01 * m.m13 + m02 * m.m23 + m03; - - float rm10 = m10 * m.m00 + m11 * m.m10 + m12 * m.m20; - float rm11 = m10 * m.m01 + m11 * m.m11 + m12 * m.m21; - float rm12 = m10 * m.m02 + m11 * m.m12 + m12 * m.m22; - float rm13 = m10 * m.m03 + m11 * m.m13 + m12 * m.m23 + m13; - - float rm20 = m20 * m.m00 + m21 * m.m10 + m22 * m.m20; - float rm21 = m20 * m.m01 + m21 * m.m11 + m22 * m.m21; - float rm22 = m20 * m.m02 + m21 * m.m12 + m22 * m.m22; - float rm23 = m20 * m.m03 + m21 * m.m13 + m22 * m.m23 + m23; - - return new Matrix4(rm00, rm01, rm02, rm03, rm10, rm11, rm12, rm13, rm20, rm21, rm22, rm23); - } - - /** - * Transforms each corner of the specified axis-aligned bounding box and - * returns a new bounding box which incloses the transformed corners. - * - * @param b original bounding box - * @return a new BoundingBox object which encloses the transform version of - * b - */ - public final BoundingBox transform(BoundingBox b) { - if (b.isEmpty()) - return new BoundingBox(); - // special case extreme corners - BoundingBox rb = new BoundingBox(transformP(b.getMinimum())); - rb.include(transformP(b.getMaximum())); - // do internal corners - for (int i = 1; i < 7; i++) - rb.include(transformP(b.getCorner(i))); - return rb; - } - - /** - * Computes this*v and returns the result as a new Vector3 object. This - * method assumes the bottom row of the matrix is [0,0,0,1]. - * - * @param v vector to multiply - * @return a new Vector3 object equal to this*v - */ - public final Vector3 transformV(Vector3 v) { - Vector3 rv = new Vector3(); - rv.x = m00 * v.x + m01 * v.y + m02 * v.z; - rv.y = m10 * v.x + m11 * v.y + m12 * v.z; - rv.z = m20 * v.x + m21 * v.y + m22 * v.z; - return rv; - } - - /** - * Computes (this^T)*v and returns the result as a new Vector3 object. This - * method assumes the bottom row of the matrix is [0,0,0,1]. - * - * @param v vector to multiply - * @return a new Vector3 object equal to (this^T)*v - */ - public final Vector3 transformTransposeV(Vector3 v) { - Vector3 rv = new Vector3(); - rv.x = m00 * v.x + m10 * v.y + m20 * v.z; - rv.y = m01 * v.x + m11 * v.y + m21 * v.z; - rv.z = m02 * v.x + m12 * v.y + m22 * v.z; - return rv; - } - - /** - * Computes this*p and returns the result as a new Point3 object. This - * method assumes the bottom row of the matrix is [0,0,0,1]. - * - * @param p point to multiply - * @return a new Point3 object equal to this*v - */ - public final Point3 transformP(Point3 p) { - Point3 rp = new Point3(); - rp.x = m00 * p.x + m01 * p.y + m02 * p.z + m03; - rp.y = m10 * p.x + m11 * p.y + m12 * p.z + m13; - rp.z = m20 * p.x + m21 * p.y + m22 * p.z + m23; - return rp; - } - - /** - * Computes the x component of this*(x,y,z,0). - * - * @param x x coordinate of the vector to multiply - * @param y y coordinate of the vector to multiply - * @param z z coordinate of the vector to multiply - * @return x coordinate transformation result - */ - public final float transformVX(float x, float y, float z) { - return m00 * x + m01 * y + m02 * z; - } - - /** - * Computes the y component of this*(x,y,z,0). - * - * @param x x coordinate of the vector to multiply - * @param y y coordinate of the vector to multiply - * @param z z coordinate of the vector to multiply - * @return y coordinate transformation result - */ - public final float transformVY(float x, float y, float z) { - return m10 * x + m11 * y + m12 * z; - } - - /** - * Computes the z component of this*(x,y,z,0). - * - * @param x x coordinate of the vector to multiply - * @param y y coordinate of the vector to multiply - * @param z z coordinate of the vector to multiply - * @return z coordinate transformation result - */ - public final float transformVZ(float x, float y, float z) { - return m20 * x + m21 * y + m22 * z; - } - - /** - * Computes the x component of (this^T)*(x,y,z,0). - * - * @param x x coordinate of the vector to multiply - * @param y y coordinate of the vector to multiply - * @param z z coordinate of the vector to multiply - * @return x coordinate transformation result - */ - public final float transformTransposeVX(float x, float y, float z) { - return m00 * x + m10 * y + m20 * z; - } - - /** - * Computes the y component of (this^T)*(x,y,z,0). - * - * @param x x coordinate of the vector to multiply - * @param y y coordinate of the vector to multiply - * @param z z coordinate of the vector to multiply - * @return y coordinate transformation result - */ - public final float transformTransposeVY(float x, float y, float z) { - return m01 * x + m11 * y + m21 * z; - } - - /** - * Computes the z component of (this^T)*(x,y,z,0). - * - * @param x x coordinate of the vector to multiply - * @param y y coordinate of the vector to multiply - * @param z z coordinate of the vector to multiply - * @return zcoordinate transformation result - */ - public final float transformTransposeVZ(float x, float y, float z) { - return m02 * x + m12 * y + m22 * z; - } - - /** - * Computes the x component of this*(x,y,z,1). - * - * @param x x coordinate of the vector to multiply - * @param y y coordinate of the vector to multiply - * @param z z coordinate of the vector to multiply - * @return x coordinate transformation result - */ - public final float transformPX(float x, float y, float z) { - return m00 * x + m01 * y + m02 * z + m03; - } - - /** - * Computes the y component of this*(x,y,z,1). - * - * @param x x coordinate of the vector to multiply - * @param y y coordinate of the vector to multiply - * @param z z coordinate of the vector to multiply - * @return y coordinate transformation result - */ - public final float transformPY(float x, float y, float z) { - return m10 * x + m11 * y + m12 * z + m13; - } - - /** - * Computes the z component of this*(x,y,z,1). - * - * @param x x coordinate of the vector to multiply - * @param y y coordinate of the vector to multiply - * @param z z coordinate of the vector to multiply - * @return z coordinate transformation result - */ - public final float transformPZ(float x, float y, float z) { - return m20 * x + m21 * y + m22 * z + m23; - } - - /** - * Create a translation matrix for the specified vector. - * - * @param x x component of translation - * @param y y component of translation - * @param z z component of translation - * @return a new Matrix4 object representing the translation - */ - public final static Matrix4 translation(float x, float y, float z) { - Matrix4 m = new Matrix4(); - m.m00 = m.m11 = m.m22 = 1; - m.m03 = x; - m.m13 = y; - m.m23 = z; - return m; - } - - /** - * Creates a rotation matrix about the X axis. - * - * @param theta angle to rotate about the X axis in radians - * @return a new Matrix4 object representing the rotation - */ - public final static Matrix4 rotateX(float theta) { - Matrix4 m = new Matrix4(); - float s = (float) Math.sin(theta); - float c = (float) Math.cos(theta); - m.m00 = 1; - m.m11 = m.m22 = c; - m.m12 = -s; - m.m21 = +s; - return m; - } - - /** - * Creates a rotation matrix about the Y axis. - * - * @param theta angle to rotate about the Y axis in radians - * @return a new Matrix4 object representing the rotation - */ - public final static Matrix4 rotateY(float theta) { - Matrix4 m = new Matrix4(); - float s = (float) Math.sin(theta); - float c = (float) Math.cos(theta); - m.m11 = 1; - m.m00 = m.m22 = c; - m.m02 = +s; - m.m20 = -s; - return m; - } - - /** - * Creates a rotation matrix about the Z axis. - * - * @param theta angle to rotate about the Z axis in radians - * @return a new Matrix4 object representing the rotation - */ - public final static Matrix4 rotateZ(float theta) { - Matrix4 m = new Matrix4(); - float s = (float) Math.sin(theta); - float c = (float) Math.cos(theta); - m.m22 = 1; - m.m00 = m.m11 = c; - m.m01 = -s; - m.m10 = +s; - return m; - } - - /** - * Creates a rotation matrix about the specified axis. The axis vector need - * not be normalized. - * - * @param x x component of the axis vector - * @param y y component of the axis vector - * @param z z component of the axis vector - * @param theta angle to rotate about the axis in radians - * @return a new Matrix4 object representing the rotation - */ - public final static Matrix4 rotate(float x, float y, float z, float theta) { - Matrix4 m = new Matrix4(); - float invLen = 1 / (float) Math.sqrt(x * x + y * y + z * z); - x *= invLen; - y *= invLen; - z *= invLen; - float s = (float) Math.sin(theta); - float c = (float) Math.cos(theta); - float t = 1 - c; - m.m00 = t * x * x + c; - m.m11 = t * y * y + c; - m.m22 = t * z * z + c; - float txy = t * x * y; - float sz = s * z; - m.m01 = txy - sz; - m.m10 = txy + sz; - float txz = t * x * z; - float sy = s * y; - m.m02 = txz + sy; - m.m20 = txz - sy; - float tyz = t * y * z; - float sx = s * x; - m.m12 = tyz - sx; - m.m21 = tyz + sx; - return m; - } - - /** - * Create a uniform scaling matrix. - * - * @param s scale factor for all three axes - * @return a new Matrix4 object representing the uniform scale - */ - public final static Matrix4 scale(float s) { - Matrix4 m = new Matrix4(); - m.m00 = m.m11 = m.m22 = s; - return m; - } - - /** - * Creates a non-uniform scaling matrix. - * - * @param sx scale factor in the x dimension - * @param sy scale factor in the y dimension - * @param sz scale factor in the z dimension - * @return a new Matrix4 object representing the non-uniform scale - */ - public final static Matrix4 scale(float sx, float sy, float sz) { - Matrix4 m = new Matrix4(); - m.m00 = sx; - m.m11 = sy; - m.m22 = sz; - return m; - } - - /** - * Creates a rotation matrix from an OrthonormalBasis. - * - * @param basis - */ - public final static Matrix4 fromBasis(OrthoNormalBasis basis) { - Matrix4 m = new Matrix4(); - Vector3 u = basis.transform(new Vector3(1, 0, 0)); - Vector3 v = basis.transform(new Vector3(0, 1, 0)); - Vector3 w = basis.transform(new Vector3(0, 0, 1)); - m.m00 = u.x; - m.m01 = v.x; - m.m02 = w.x; - m.m10 = u.y; - m.m11 = v.y; - m.m12 = w.y; - m.m20 = u.z; - m.m21 = v.z; - m.m22 = w.z; - return m; - } - - /** - * Creates a camera positioning matrix from the given eye and target points - * and up vector. - * - * @param eye location of the eye - * @param target location of the target - * @param up vector pointing upwards - * @return - */ - public final static Matrix4 lookAt(Point3 eye, Point3 target, Vector3 up) { - Matrix4 m = Matrix4.fromBasis(OrthoNormalBasis.makeFromWV(Point3.sub(eye, target, new Vector3()), up)); - return Matrix4.translation(eye.x, eye.y, eye.z).multiply(m); - } - - public final static Matrix4 blend(Matrix4 m0, Matrix4 m1, float t) { - Matrix4 m = new Matrix4(); - m.m00 = (1 - t) * m0.m00 + t * m1.m00; - m.m01 = (1 - t) * m0.m01 + t * m1.m01; - m.m02 = (1 - t) * m0.m02 + t * m1.m02; - m.m03 = (1 - t) * m0.m03 + t * m1.m03; - - m.m10 = (1 - t) * m0.m10 + t * m1.m10; - m.m11 = (1 - t) * m0.m11 + t * m1.m11; - m.m12 = (1 - t) * m0.m12 + t * m1.m12; - m.m13 = (1 - t) * m0.m13 + t * m1.m13; - - m.m20 = (1 - t) * m0.m20 + t * m1.m20; - m.m21 = (1 - t) * m0.m21 + t * m1.m21; - m.m22 = (1 - t) * m0.m22 + t * m1.m22; - m.m23 = (1 - t) * m0.m23 + t * m1.m23; - return m; - } -} \ No newline at end of file diff --git a/src/org/sunflow/math/MovingMatrix4.java b/src/org/sunflow/math/MovingMatrix4.java deleted file mode 100644 index 4906cf7..0000000 --- a/src/org/sunflow/math/MovingMatrix4.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.sunflow.math; - -/** - * This class describes a transformation matrix that changes over time. Note - * that while unlimited motion segments are supported, it is assumed that these - * segments represent equidistant samples within a given time range. - */ -public final class MovingMatrix4 { - private Matrix4[] transforms; - private float t0, t1, inv; - - /** - * Constructs a simple static matrix. - * - * @param m matrix value at all times - */ - public MovingMatrix4(Matrix4 m) { - transforms = new Matrix4[] { m }; - t0 = t1 = 0; - inv = 1; - } - - private MovingMatrix4(int n, float t0, float t1, float inv) { - transforms = new Matrix4[n]; - this.t0 = t0; - this.t1 = t1; - this.inv = inv; - } - - /** - * Redefines the number of steps in the matrix. The contents are only - * re-allocated if the number of steps changes. This is to allow the matrix - * to be incrementally specified. - * - * @param n - */ - public void setSteps(int n) { - if (transforms.length != n) { - transforms = new Matrix4[n]; - if (t0 < t1) - inv = (transforms.length - 1) / (t1 - t0); - else - inv = 1; - } - } - - /** - * Updates the matrix for the given time step. - * - * @param i time step to update - * @param m new value for the matrix at this time step - */ - public void updateData(int i, Matrix4 m) { - transforms[i] = m; - } - - /** - * Get the matrix for the given time step. - * - * @param i time step to get - * @return matrix for the specfied time step - */ - public Matrix4 getData(int i) { - return transforms[i]; - } - - /** - * Get the number of matrix segments - * - * @return number of segments - */ - public int numSegments() { - return transforms.length; - } - - /** - * Update the time extents over which the matrix data is changing. If the - * interval is empty, no motion will be produced, even if multiple values - * have been specified. - * - * @param t0 - * @param t1 - */ - public void updateTimes(float t0, float t1) { - this.t0 = t0; - this.t1 = t1; - if (t0 < t1) - inv = (transforms.length - 1) / (t1 - t0); - else - inv = 1; - } - - public MovingMatrix4 inverse() { - MovingMatrix4 mi = new MovingMatrix4(transforms.length, t0, t1, inv); - for (int i = 0; i < transforms.length; i++) { - if (transforms[i] != null) { - mi.transforms[i] = transforms[i].inverse(); - if (mi.transforms[i] == null) - return null; // unable to invert - } - } - return mi; - } - - public Matrix4 sample(float time) { - if (transforms.length == 1 || t0 >= t1) - return transforms[0]; - else { - float nt = (MathUtils.clamp(time, t0, t1) - t0) * inv; - int idx0 = (int) nt; - int idx1 = Math.min(idx0 + 1, transforms.length - 1); - return Matrix4.blend(transforms[idx0], transforms[idx1], (float) (nt - idx0)); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/math/OrthoNormalBasis.java b/src/org/sunflow/math/OrthoNormalBasis.java deleted file mode 100644 index 4c16c71..0000000 --- a/src/org/sunflow/math/OrthoNormalBasis.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.sunflow.math; - -public final class OrthoNormalBasis { - private Vector3 u, v, w; - - private OrthoNormalBasis() { - u = new Vector3(); - v = new Vector3(); - w = new Vector3(); - } - - public void flipU() { - u.negate(); - } - - public void flipV() { - v.negate(); - } - - public void flipW() { - w.negate(); - } - - public void swapUV() { - Vector3 t = u; - u = v; - v = t; - } - - public void swapVW() { - Vector3 t = v; - v = w; - w = t; - } - - public void swapWU() { - Vector3 t = w; - w = u; - u = t; - } - - public Vector3 transform(Vector3 a, Vector3 dest) { - dest.x = (a.x * u.x) + (a.y * v.x) + (a.z * w.x); - dest.y = (a.x * u.y) + (a.y * v.y) + (a.z * w.y); - dest.z = (a.x * u.z) + (a.y * v.z) + (a.z * w.z); - return dest; - } - - public Vector3 transform(Vector3 a) { - float x = (a.x * u.x) + (a.y * v.x) + (a.z * w.x); - float y = (a.x * u.y) + (a.y * v.y) + (a.z * w.y); - float z = (a.x * u.z) + (a.y * v.z) + (a.z * w.z); - return a.set(x, y, z); - } - - public Vector3 untransform(Vector3 a, Vector3 dest) { - dest.x = Vector3.dot(a, u); - dest.y = Vector3.dot(a, v); - dest.z = Vector3.dot(a, w); - return dest; - } - - public Vector3 untransform(Vector3 a) { - float x = Vector3.dot(a, u); - float y = Vector3.dot(a, v); - float z = Vector3.dot(a, w); - return a.set(x, y, z); - } - - public float untransformX(Vector3 a) { - return Vector3.dot(a, u); - } - - public float untransformY(Vector3 a) { - return Vector3.dot(a, v); - } - - public float untransformZ(Vector3 a) { - return Vector3.dot(a, w); - } - - public static final OrthoNormalBasis makeFromW(Vector3 w) { - OrthoNormalBasis onb = new OrthoNormalBasis(); - w.normalize(onb.w); - if ((Math.abs(onb.w.x) < Math.abs(onb.w.y)) && (Math.abs(onb.w.x) < Math.abs(onb.w.z))) { - onb.v.x = 0; - onb.v.y = onb.w.z; - onb.v.z = -onb.w.y; - } else if (Math.abs(onb.w.y) < Math.abs(onb.w.z)) { - onb.v.x = onb.w.z; - onb.v.y = 0; - onb.v.z = -onb.w.x; - } else { - onb.v.x = onb.w.y; - onb.v.y = -onb.w.x; - onb.v.z = 0; - } - Vector3.cross(onb.v.normalize(), onb.w, onb.u); - return onb; - } - - public static final OrthoNormalBasis makeFromWV(Vector3 w, Vector3 v) { - OrthoNormalBasis onb = new OrthoNormalBasis(); - w.normalize(onb.w); - Vector3.cross(v, onb.w, onb.u).normalize(); - Vector3.cross(onb.w, onb.u, onb.v); - return onb; - } -} \ No newline at end of file diff --git a/src/org/sunflow/math/PerlinScalar.java b/src/org/sunflow/math/PerlinScalar.java deleted file mode 100644 index 87f999a..0000000 --- a/src/org/sunflow/math/PerlinScalar.java +++ /dev/null @@ -1,330 +0,0 @@ -package org.sunflow.math; - -/** - * Noise function from Ken Perlin. Additional routines are provided to emulate - * standard Renderman calls. This code was adapted mainly from the mrclasses - * package by Gonzalo Garramuno (http://sourceforge.net/projects/mrclasses/). - * - * @link http://mrl.nyu.edu/~perlin/noise/ - */ -public final class PerlinScalar { - private static final float[] G1 = { -1, 1 }; - private static final float[][] G2 = { { 1, 0 }, { -1, 0 }, { 0, 1 }, - { 0, -1 } }; - private static final float[][] G3 = { { 1, 1, 0 }, { -1, 1, 0 }, - { 1, -1, 0 }, { -1, -1, 0 }, { 1, 0, 1 }, { -1, 0, 1 }, - { 1, 0, -1 }, { -1, 0, -1 }, { 0, 1, 1 }, { 0, -1, 1 }, - { 0, 1, -1 }, { 0, -1, -1 }, { 1, 1, 0 }, { -1, 1, 0 }, - { 0, -1, 1 }, { 0, -1, -1 } }; - private static final float[][] G4 = { { -1, -1, -1, 0 }, { -1, -1, 1, 0 }, - { -1, 1, -1, 0 }, { -1, 1, 1, 0 }, { 1, -1, -1, 0 }, - { 1, -1, 1, 0 }, { 1, 1, -1, 0 }, { 1, 1, 1, 0 }, - { -1, -1, 0, -1 }, { -1, 1, 0, -1 }, { 1, -1, 0, -1 }, - { 1, 1, 0, -1 }, { -1, -1, 0, 1 }, { -1, 1, 0, 1 }, - { 1, -1, 0, 1 }, { 1, 1, 0, 1 }, { -1, 0, -1, -1 }, - { 1, 0, -1, -1 }, { -1, 0, -1, 1 }, { 1, 0, -1, 1 }, - { -1, 0, 1, -1 }, { 1, 0, 1, -1 }, { -1, 0, 1, 1 }, { 1, 0, 1, 1 }, - { 0, -1, -1, -1 }, { 0, -1, -1, 1 }, { 0, -1, 1, -1 }, - { 0, -1, 1, 1 }, { 0, 1, -1, -1 }, { 0, 1, -1, 1 }, - { 0, 1, 1, -1 }, { 0, 1, 1, 1 } }; - private static final int[] p = { 151, 160, 137, 91, 90, 15, 131, 13, 201, - 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, - 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, - 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, - 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, - 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, - 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, - 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, - 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, - 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, - 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, - 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, - 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, - 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, - 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, - 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, - 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, - 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180, - 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, - 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, - 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, - 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, - 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, - 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, - 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, - 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, - 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, - 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, - 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, - 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, - 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, - 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, - 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, - 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, - 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, - 141, 128, 195, 78, 66, 215, 61, 156, 180 }; - - public static final float snoise(float x) { - int xf = (int) Math.floor(x); - int X = xf & 255; - x -= xf; - float u = fade(x); - int A = p[X], B = p[X + 1]; - return lerp(u, grad(p[A], x), grad(p[B], x - 1)); - } - - public static final float snoise(float x, float y) { - int xf = (int) Math.floor(x); - int yf = (int) Math.floor(y); - int X = xf & 255; - int Y = yf & 255; - x -= xf; - y -= yf; - float u = fade(x); - float v = fade(y); - int A = p[X] + Y, B = p[X + 1] + Y; - return lerp(v, lerp(u, grad(p[A], x, y), grad(p[B], x - 1, y)), lerp(u, grad(p[A + 1], x, y - 1), grad(p[B + 1], x - 1, y - 1))); - } - - public static final float snoise(float x, float y, float z) { - int xf = (int) Math.floor(x); - int yf = (int) Math.floor(y); - int zf = (int) Math.floor(z); - int X = xf & 255; - int Y = yf & 255; - int Z = zf & 255; - x -= xf; - y -= yf; - z -= zf; - float u = fade(x); - float v = fade(y); - float w = fade(z); - int A = p[X] + Y, AA = p[A] + Z, AB = p[A + 1] + Z, B = p[X + 1] + Y, BA = p[B] + Z, BB = p[B + 1] + Z; - return lerp(w, lerp(v, lerp(u, grad(p[AA], x, y, z), grad(p[BA], x - 1, y, z)), lerp(u, grad(p[AB], x, y - 1, z), grad(p[BB], x - 1, y - 1, z))), lerp(v, lerp(u, grad(p[AA + 1], x, y, z - 1), grad(p[BA + 1], x - 1, y, z - 1)), lerp(u, grad(p[AB + 1], x, y - 1, z - 1), grad(p[BB + 1], x - 1, y - 1, z - 1)))); - } - - public static final float snoise(float x, float y, float z, float w) { - int xf = (int) Math.floor(x); - int yf = (int) Math.floor(y); - int zf = (int) Math.floor(z); - int wf = (int) Math.floor(w); - int X = xf & 255; - int Y = yf & 255; - int Z = zf & 255; - int W = wf & 255; - x -= xf; - y -= yf; - z -= zf; - w -= wf; - float u = fade(x); - float v = fade(y); - float t = fade(z); - float s = fade(w); - int A = p[X] + Y, AA = p[A] + Z, AB = p[A + 1] + Z, B = p[X + 1] + Y, BA = p[B] + Z, BB = p[B + 1] + Z, AAA = p[AA] + W, AAB = p[AA + 1] + W, ABA = p[AB] + W, ABB = p[AB + 1] + W, BAA = p[BA] + W, BAB = p[BA + 1] + W, BBA = p[BB] + W, BBB = p[BB + 1] + W; - return lerp(s, lerp(t, lerp(v, lerp(u, grad(p[AAA], x, y, z, w), grad(p[BAA], x - 1, y, z, w)), lerp(u, grad(p[ABA], x, y - 1, z, w), grad(p[BBA], x - 1, y - 1, z, w))), lerp(v, lerp(u, grad(p[AAB], x, y, z - 1, w), grad(p[BAB], x - 1, y, z - 1, w)), lerp(u, grad(p[ABB], x, y - 1, z - 1, w), grad(p[BBB], x - 1, y - 1, z - 1, w)))), lerp(t, lerp(v, lerp(u, grad(p[AAA + 1], x, y, z, w - 1), grad(p[BAA + 1], x - 1, y, z, w - 1)), lerp(u, grad(p[ABA + 1], x, y - 1, z, w - 1), grad(p[BBA + 1], x - 1, y - 1, z, w - 1))), lerp(v, lerp(u, grad(p[AAB + 1], x, y, z - 1, w - 1), grad(p[BAB + 1], x - 1, y, z - 1, w - 1)), lerp(u, grad(p[ABB + 1], x, y - 1, z - 1, w - 1), grad(p[BBB + 1], x - 1, y - 1, z - 1, w - 1))))); - } - - public static final float snoise(Point2 p) { - return snoise(p.x, p.y); - } - - public static final float snoise(Point3 p) { - return snoise(p.x, p.y, p.z); - } - - public static final float snoise(Point3 p, float t) { - return snoise(p.x, p.y, p.z, t); - } - - public static final float noise(float x) { - return 0.5f + 0.5f * snoise(x); - } - - public static final float noise(float x, float y) { - return 0.5f + 0.5f * snoise(x, y); - } - - public static final float noise(float x, float y, float z) { - return 0.5f + 0.5f * snoise(x, y, z); - } - - public static final float noise(float x, float y, float z, float t) { - return 0.5f + 0.5f * snoise(x, y, z, t); - } - - public static final float noise(Point2 p) { - return 0.5f + 0.5f * snoise(p.x, p.y); - } - - public static final float noise(Point3 p) { - return 0.5f + 0.5f * snoise(p.x, p.y, p.z); - } - - public static final float noise(Point3 p, float t) { - return 0.5f + 0.5f * snoise(p.x, p.y, p.z, t); - } - - public static final float pnoise(float xi, float period) { - float x = (xi % period) + ((xi < 0) ? period : 0); - return ((period - x) * noise(x) + x * noise(x - period)) / period; - } - - public static final float pnoise(float xi, float yi, float w, float h) { - float x = (xi % w) + ((xi < 0) ? w : 0); - float y = (yi % h) + ((yi < 0) ? h : 0); - float w_x = w - x; - float h_y = h - y; - float x_w = x - w; - float y_h = y - h; - return (noise(x, y) * (w_x) * (h_y) + noise(x_w, y) * (x) * (h_y) + noise(x_w, y_h) * (x) * (y) + noise(x, y_h) * (w_x) * (y)) / (w * h); - } - - public static final float pnoise(float xi, float yi, float zi, float w, float h, float d) { - float x = (xi % w) + ((xi < 0) ? w : 0); - float y = (yi % h) + ((yi < 0) ? h : 0); - float z = (zi % d) + ((zi < 0) ? d : 0); - float w_x = w - x; - float h_y = h - y; - float d_z = d - z; - float x_w = x - w; - float y_h = y - h; - float z_d = z - d; - float xy = x * y; - float h_yXd_z = h_y * d_z; - float h_yXz = h_y * z; - float w_xXy = w_x * y; - return (noise(x, y, z) * (w_x) * h_yXd_z + noise(x, y_h, z) * w_xXy * (d_z) + noise(x_w, y, z) * (x) * h_yXd_z + noise(x_w, y_h, z) * (xy) * (d_z) + noise(x_w, y_h, z_d) * (xy) * (z) + noise(x, y, z_d) * (w_x) * h_yXz + noise(x, y_h, z_d) * w_xXy * (z) + noise(x_w, y, z_d) * (x) * h_yXz) / (w * h * d); - } - - public static final float pnoise(float xi, float yi, float zi, float ti, float w, float h, float d, float p) { - float x = (xi % w) + ((xi < 0) ? w : 0); - float y = (yi % h) + ((yi < 0) ? h : 0); - float z = (zi % d) + ((zi < 0) ? d : 0); - float t = (ti % p) + ((ti < 0) ? p : 0); - float w_x = w - x; - float h_y = h - y; - float d_z = d - z; - float p_t = p - t; - float x_w = x - w; - float y_h = y - h; - float z_d = z - d; - float t_p = t - p; - float xy = x * y; - float d_zXp_t = (d_z) * (p_t); - float zXp_t = z * (p_t); - float zXt = z * t; - float d_zXt = d_z * t; - float w_xXy = w_x * y; - float w_xXh_y = w_x * h_y; - float xXh_y = x * h_y; - return (noise(x, y, z, t) * (w_xXh_y) * d_zXp_t + noise(x_w, y, z, t) * (xXh_y) * d_zXp_t + noise(x_w, y_h, z, t) * (xy) * d_zXp_t + noise(x, y_h, z, t) * (w_xXy) * d_zXp_t + noise(x_w, y_h, z_d, t) * (xy) * (zXp_t) + noise(x, y, z_d, t) * (w_xXh_y) * (zXp_t) + noise(x, y_h, z_d, t) * (w_xXy) * (zXp_t) + noise(x_w, y, z_d, t) * (xXh_y) * (zXp_t) + noise(x, y, z, t_p) * (w_xXh_y) * (d_zXt) + noise(x_w, y, z, t_p) * (xXh_y) * (d_zXt) + noise(x_w, y_h, z, t_p) * (xy) * (d_zXt) + noise(x, y_h, z, t_p) * (w_xXy) * (d_zXt) + noise(x_w, y_h, z_d, t_p) * (xy) * (zXt) + noise(x, y, z_d, t_p) * (w_xXh_y) * (zXt) + noise(x, y_h, z_d, t_p) * (w_xXy) * (zXt) + noise(x_w, y, z_d, t_p) * (xXh_y) * (zXt)) / (w * h * d * t); - } - - public static final float pnoise(Point2 p, float periodx, float periody) { - return pnoise(p.x, p.y, periodx, periody); - } - - public static final float pnoise(Point3 p, Vector3 period) { - return pnoise(p.x, p.y, p.z, period.x, period.y, period.z); - } - - public static final float pnoise(Point3 p, float t, Vector3 pperiod, float tperiod) { - return pnoise(p.x, p.y, p.z, t, pperiod.x, pperiod.y, pperiod.z, tperiod); - } - - public static final float spnoise(float xi, float period) { - float x = (xi % period) + ((xi < 0) ? period : 0); - return (((period - x) * snoise(x) + x * snoise(x - period)) / period); - } - - public static final float spnoise(float xi, float yi, float w, float h) { - float x = (xi % w) + ((xi < 0) ? w : 0); - float y = (yi % h) + ((yi < 0) ? h : 0); - float w_x = w - x; - float h_y = h - y; - float x_w = x - w; - float y_h = y - h; - return ((snoise(x, y) * (w_x) * (h_y) + snoise(x_w, y) * (x) * (h_y) + snoise(x_w, y_h) * (x) * (y) + snoise(x, y_h) * (w_x) * (y)) / (w * h)); - } - - public static final float spnoise(float xi, float yi, float zi, float w, float h, float d) { - float x = (xi % w) + ((xi < 0) ? w : 0); - float y = (yi % h) + ((yi < 0) ? h : 0); - float z = (zi % d) + ((zi < 0) ? d : 0); - float w_x = w - x; - float h_y = h - y; - float d_z = d - z; - float x_w = x - w; - float y_h = y - h; - float z_d = z - d; - float xy = x * y; - float h_yXd_z = h_y * d_z; - float h_yXz = h_y * z; - float w_xXy = w_x * y; - return ((snoise(x, y, z) * (w_x) * h_yXd_z + snoise(x, y_h, z) * w_xXy * (d_z) + snoise(x_w, y, z) * (x) * h_yXd_z + snoise(x_w, y_h, z) * (xy) * (d_z) + snoise(x_w, y_h, z_d) * (xy) * (z) + snoise(x, y, z_d) * (w_x) * h_yXz + snoise(x, y_h, z_d) * w_xXy * (z) + snoise(x_w, y, z_d) * (x) * h_yXz) / (w * h * d)); - } - - public static final float spnoise(float xi, float yi, float zi, float ti, float w, float h, float d, float p) { - float x = (xi % w) + ((xi < 0) ? w : 0); - float y = (yi % h) + ((yi < 0) ? h : 0); - float z = (zi % d) + ((zi < 0) ? d : 0); - float t = (ti % p) + ((ti < 0) ? p : 0); - float w_x = w - x; - float h_y = h - y; - float d_z = d - z; - float p_t = p - t; - float x_w = x - w; - float y_h = y - h; - float z_d = z - d; - float t_p = t - p; - float xy = x * y; - float d_zXp_t = (d_z) * (p_t); - float zXp_t = z * (p_t); - float zXt = z * t; - float d_zXt = d_z * t; - float w_xXy = w_x * y; - float w_xXh_y = w_x * h_y; - float xXh_y = x * h_y; - return ((snoise(x, y, z, t) * (w_xXh_y) * d_zXp_t + snoise(x_w, y, z, t) * (xXh_y) * d_zXp_t + snoise(x_w, y_h, z, t) * (xy) * d_zXp_t + snoise(x, y_h, z, t) * (w_xXy) * d_zXp_t + snoise(x_w, y_h, z_d, t) * (xy) * (zXp_t) + snoise(x, y, z_d, t) * (w_xXh_y) * (zXp_t) + snoise(x, y_h, z_d, t) * (w_xXy) * (zXp_t) + snoise(x_w, y, z_d, t) * (xXh_y) * (zXp_t) + snoise(x, y, z, t_p) * (w_xXh_y) * (d_zXt) + snoise(x_w, y, z, t_p) * (xXh_y) * (d_zXt) + snoise(x_w, y_h, z, t_p) * (xy) * (d_zXt) + snoise(x, y_h, z, t_p) * (w_xXy) * (d_zXt) + snoise(x_w, y_h, z_d, t_p) * (xy) * (zXt) + snoise(x, y, z_d, t_p) * (w_xXh_y) * (zXt) + snoise(x, y_h, z_d, t_p) * (w_xXy) * (zXt) + snoise(x_w, y, z_d, t_p) * (xXh_y) * (zXt)) / (w * h * d * t)); - } - - public static final float spnoise(Point2 p, float periodx, float periody) { - return spnoise(p.x, p.y, periodx, periody); - } - - public static final float spnoise(Point3 p, Vector3 period) { - return spnoise(p.x, p.y, p.z, period.x, period.y, period.z); - } - - public static final float spnoise(Point3 p, float t, Vector3 pperiod, float tperiod) { - return spnoise(p.x, p.y, p.z, t, pperiod.x, pperiod.y, pperiod.z, tperiod); - } - - private static final float fade(float t) { - return t * t * t * (t * (t * 6 - 15) + 10); - } - - private static final float lerp(float t, float a, float b) { - return a + t * (b - a); - } - - private static final float grad(int hash, float x) { - int h = hash & 0x1; - return x * G1[h]; - } - - private static final float grad(int hash, float x, float y) { - int h = hash & 0x3; - return x * G2[h][0] + y * G2[h][1]; - } - - private static final float grad(int hash, float x, float y, float z) { - int h = hash & 15; - return x * G3[h][0] + y * G3[h][1] + z * G3[h][2]; - } - - private static final float grad(int hash, float x, float y, float z, float w) { - int h = hash & 31; - return x * G4[h][0] + y * G4[h][1] + z * G4[h][2] + w * G4[h][3]; - } -} \ No newline at end of file diff --git a/src/org/sunflow/math/PerlinVector.java b/src/org/sunflow/math/PerlinVector.java deleted file mode 100644 index fa62004..0000000 --- a/src/org/sunflow/math/PerlinVector.java +++ /dev/null @@ -1,131 +0,0 @@ -package org.sunflow.math; - -/** - * Vector versions of the standard noise functions. These are provided to - * emulate standard renderman calls.This code was adapted mainly from the - * mrclasses package by Gonzalo Garramuno - * (http://sourceforge.net/projects/mrclasses/). - */ -public class PerlinVector { - private static final float P1x = 0.34f; - private static final float P1y = 0.66f; - private static final float P1z = 0.237f; - private static final float P2x = 0.011f; - private static final float P2y = 0.845f; - private static final float P2z = 0.037f; - private static final float P3x = 0.34f; - private static final float P3y = 0.12f; - private static final float P3z = 0.9f; - - public static final Vector3 snoise(float x) { - return new Vector3(PerlinScalar.snoise(x + P1x), PerlinScalar.snoise(x + P2x), PerlinScalar.snoise(x + P3x)); - } - - public static final Vector3 snoise(float x, float y) { - return new Vector3(PerlinScalar.snoise(x + P1x, y + P1y), PerlinScalar.snoise(x + P2x, y + P2y), PerlinScalar.snoise(x + P3x, y + P3y)); - } - - public static final Vector3 snoise(float x, float y, float z) { - return new Vector3(PerlinScalar.snoise(x + P1x, y + P1y, z + P1z), PerlinScalar.snoise(x + P2x, y + P2y, z + P2z), PerlinScalar.snoise(x + P3x, y + P3y, z + P3z)); - } - - public static final Vector3 snoise(float x, float y, float z, float t) { - return new Vector3(PerlinScalar.snoise(x + P1x, y + P1y, z + P1z, t), PerlinScalar.snoise(x + P2x, y + P2y, z + P2z, t), PerlinScalar.snoise(x + P3x, y + P3y, z + P3z, t)); - } - - public static final Vector3 snoise(Point2 p) { - return snoise(p.x, p.y); - } - - public static final Vector3 snoise(Point3 p) { - return snoise(p.x, p.y, p.z); - } - - public static final Vector3 snoise(Point3 p, float t) { - return snoise(p.x, p.y, p.z, t); - } - - public static final Vector3 noise(float x) { - return new Vector3(PerlinScalar.noise(x + P1x), PerlinScalar.noise(x + P2x), PerlinScalar.noise(x + P3x)); - } - - public static final Vector3 noise(float x, float y) { - return new Vector3(PerlinScalar.noise(x + P1x, y + P1y), PerlinScalar.noise(x + P2x, y + P2y), PerlinScalar.noise(x + P3x, y + P3y)); - } - - public static final Vector3 noise(float x, float y, float z) { - return new Vector3(PerlinScalar.noise(x + P1x, y + P1y, z + P1z), PerlinScalar.noise(x + P2x, y + P2y, z + P2z), PerlinScalar.noise(x + P3x, y + P3y, z + P3z)); - } - - public static final Vector3 noise(float x, float y, float z, float t) { - return new Vector3(PerlinScalar.noise(x + P1x, y + P1y, z + P1z, t), PerlinScalar.noise(x + P2x, y + P2y, z + P2z, t), PerlinScalar.noise(x + P3x, y + P3y, z + P3z, t)); - } - - public static final Vector3 noise(Point2 p) { - return noise(p.x, p.y); - } - - public static final Vector3 noise(Point3 p) { - return noise(p.x, p.y, p.z); - } - - public static final Vector3 noise(Point3 p, float t) { - return noise(p.x, p.y, p.z, t); - } - - public static final Vector3 pnoise(float x, float period) { - return new Vector3(PerlinScalar.pnoise(x + P1x, period), PerlinScalar.pnoise(x + P2x, period), PerlinScalar.pnoise(x + P3x, period)); - } - - public static final Vector3 pnoise(float x, float y, float w, float h) { - return new Vector3(PerlinScalar.pnoise(x + P1x, y + P1y, w, h), PerlinScalar.pnoise(x + P2x, y + P2y, w, h), PerlinScalar.pnoise(x + P3x, y + P3y, w, h)); - } - - public static final Vector3 pnoise(float x, float y, float z, float w, float h, float d) { - return new Vector3(PerlinScalar.pnoise(x + P1x, y + P1y, z + P1z, w, h, d), PerlinScalar.pnoise(x + P2x, y + P2y, z + P2z, w, h, d), PerlinScalar.pnoise(x + P3x, y + P3y, z + P3z, w, h, d)); - } - - public static final Vector3 pnoise(float x, float y, float z, float t, float w, float h, float d, float p) { - return new Vector3(PerlinScalar.pnoise(x + P1x, y + P1y, z + P1z, t, w, h, d, p), PerlinScalar.pnoise(x + P2x, y + P2y, z + P2z, t, w, h, d, p), PerlinScalar.pnoise(x + P3x, y + P3y, z + P3z, t, w, h, d, p)); - } - - public static final Vector3 pnoise(Point2 p, float periodx, float periody) { - return pnoise(p.x, p.y, periodx, periody); - } - - public static final Vector3 pnoise(Point3 p, Vector3 period) { - return pnoise(p.x, p.y, p.z, period.x, period.y, period.z); - } - - public static final Vector3 pnoise(Point3 p, float t, Vector3 pperiod, float tperiod) { - return pnoise(p.x, p.y, p.z, t, pperiod.x, pperiod.y, pperiod.z, tperiod); - } - - public static final Vector3 spnoise(float x, float period) { - return new Vector3(PerlinScalar.spnoise(x + P1x, period), PerlinScalar.spnoise(x + P2x, period), PerlinScalar.spnoise(x + P3x, period)); - } - - public static final Vector3 spnoise(float x, float y, float w, float h) { - return new Vector3(PerlinScalar.spnoise(x + P1x, y + P1y, w, h), PerlinScalar.spnoise(x + P2x, y + P2y, w, h), PerlinScalar.spnoise(x + P3x, y + P3y, w, h)); - } - - public static final Vector3 spnoise(float x, float y, float z, float w, float h, float d) { - return new Vector3(PerlinScalar.spnoise(x + P1x, y + P1y, z + P1z, w, h, d), PerlinScalar.spnoise(x + P2x, y + P2y, z + P2z, w, h, d), PerlinScalar.spnoise(x + P3x, y + P3y, z + P3z, w, h, d)); - } - - public static final Vector3 spnoise(float x, float y, float z, float t, float w, float h, float d, float p) { - return new Vector3(PerlinScalar.spnoise(x + P1x, y + P1y, z + P1z, t, w, h, d, p), PerlinScalar.spnoise(x + P2x, y + P2y, z + P2z, t, w, h, d, p), PerlinScalar.spnoise(x + P3x, y + P3y, z + P3z, t, w, h, d, p)); - } - - public static final Vector3 spnoise(Point2 p, float periodx, float periody) { - return spnoise(p.x, p.y, periodx, periody); - } - - public static final Vector3 spnoise(Point3 p, Vector3 period) { - return spnoise(p.x, p.y, p.z, period.x, period.y, period.z); - } - - public static final Vector3 spnoise(Point3 p, float t, Vector3 pperiod, float tperiod) { - return spnoise(p.x, p.y, p.z, t, pperiod.x, pperiod.y, pperiod.z, tperiod); - } -} \ No newline at end of file diff --git a/src/org/sunflow/math/Point2.java b/src/org/sunflow/math/Point2.java deleted file mode 100644 index 6c1d78f..0000000 --- a/src/org/sunflow/math/Point2.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.sunflow.math; - -public final class Point2 { - public float x, y; - - public Point2() { - } - - public Point2(float x, float y) { - this.x = x; - this.y = y; - } - - public Point2(Point2 p) { - x = p.x; - y = p.y; - } - - public final Point2 set(float x, float y) { - this.x = x; - this.y = y; - return this; - } - - public final Point2 set(Point2 p) { - x = p.x; - y = p.y; - return this; - } - - @Override - public final String toString() { - return String.format("(%.2f, %.2f)", x, y); - } -} \ No newline at end of file diff --git a/src/org/sunflow/math/Point3.java b/src/org/sunflow/math/Point3.java deleted file mode 100644 index cc1bf13..0000000 --- a/src/org/sunflow/math/Point3.java +++ /dev/null @@ -1,132 +0,0 @@ -package org.sunflow.math; - -public final class Point3 { - public float x, y, z; - - public Point3() { - } - - public Point3(float x, float y, float z) { - this.x = x; - this.y = y; - this.z = z; - } - - public Point3(Point3 p) { - x = p.x; - y = p.y; - z = p.z; - } - - public float get(int i) { - switch (i) { - case 0: - return x; - case 1: - return y; - default: - return z; - } - } - - public final float distanceTo(Point3 p) { - float dx = x - p.x; - float dy = y - p.y; - float dz = z - p.z; - return (float) Math.sqrt((dx * dx) + (dy * dy) + (dz * dz)); - } - - public final float distanceTo(float px, float py, float pz) { - float dx = x - px; - float dy = y - py; - float dz = z - pz; - return (float) Math.sqrt((dx * dx) + (dy * dy) + (dz * dz)); - } - - public final float distanceToSquared(Point3 p) { - float dx = x - p.x; - float dy = y - p.y; - float dz = z - p.z; - return (dx * dx) + (dy * dy) + (dz * dz); - } - - public final float distanceToSquared(float px, float py, float pz) { - float dx = x - px; - float dy = y - py; - float dz = z - pz; - return (dx * dx) + (dy * dy) + (dz * dz); - } - - public final Point3 set(float x, float y, float z) { - this.x = x; - this.y = y; - this.z = z; - return this; - } - - public final Point3 set(Point3 p) { - x = p.x; - y = p.y; - z = p.z; - return this; - } - - public static final Point3 add(Point3 p, Vector3 v, Point3 dest) { - dest.x = p.x + v.x; - dest.y = p.y + v.y; - dest.z = p.z + v.z; - return dest; - } - - public static final Vector3 sub(Point3 p1, Point3 p2, Vector3 dest) { - dest.x = p1.x - p2.x; - dest.y = p1.y - p2.y; - dest.z = p1.z - p2.z; - return dest; - } - - public static final Point3 mid(Point3 p1, Point3 p2, Point3 dest) { - dest.x = 0.5f * (p1.x + p2.x); - dest.y = 0.5f * (p1.y + p2.y); - dest.z = 0.5f * (p1.z + p2.z); - return dest; - } - - public static final Point3 blend(Point3 p0, Point3 p1, float blend, Point3 dest) { - dest.x = (1 - blend) * p0.x + blend * p1.x; - dest.y = (1 - blend) * p0.y + blend * p1.y; - dest.z = (1 - blend) * p0.z + blend * p1.z; - return dest; - } - - public static final Vector3 normal(Point3 p0, Point3 p1, Point3 p2) { - float edge1x = p1.x - p0.x; - float edge1y = p1.y - p0.y; - float edge1z = p1.z - p0.z; - float edge2x = p2.x - p0.x; - float edge2y = p2.y - p0.y; - float edge2z = p2.z - p0.z; - float nx = edge1y * edge2z - edge1z * edge2y; - float ny = edge1z * edge2x - edge1x * edge2z; - float nz = edge1x * edge2y - edge1y * edge2x; - return new Vector3(nx, ny, nz); - } - - public static final Vector3 normal(Point3 p0, Point3 p1, Point3 p2, Vector3 dest) { - float edge1x = p1.x - p0.x; - float edge1y = p1.y - p0.y; - float edge1z = p1.z - p0.z; - float edge2x = p2.x - p0.x; - float edge2y = p2.y - p0.y; - float edge2z = p2.z - p0.z; - dest.x = edge1y * edge2z - edge1z * edge2y; - dest.y = edge1z * edge2x - edge1x * edge2z; - dest.z = edge1x * edge2y - edge1y * edge2x; - return dest; - } - - @Override - public final String toString() { - return String.format("(%.2f, %.2f, %.2f)", x, y, z); - } -} \ No newline at end of file diff --git a/src/org/sunflow/math/QMC.java b/src/org/sunflow/math/QMC.java deleted file mode 100644 index e581b94..0000000 --- a/src/org/sunflow/math/QMC.java +++ /dev/null @@ -1,194 +0,0 @@ -package org.sunflow.math; - -import org.sunflow.system.UI; -import org.sunflow.system.UI.Module; - -public final class QMC { - public static final int MAX_SIGMA_ORDER = 15; - private static final int NUM = 128; - private static final int[][] SIGMA = new int[NUM][]; - private static final int[] PRIMES = new int[NUM]; - private static final int[] FIBONACCI = new int[47]; - private static final double[] FIBONACCI_INV = new double[FIBONACCI.length]; - private static final double[] KOROBOV = new double[NUM]; - - static { - UI.printInfo(Module.QMC, "Initializing Faure scrambling tables ..."); - // build table of first primes - PRIMES[0] = 2; - for (int i = 1; i < PRIMES.length; i++) - PRIMES[i] = nextPrime(PRIMES[i - 1]); - int[][] table = new int[PRIMES[PRIMES.length - 1] + 1][]; - table[2] = new int[2]; - table[2][0] = 0; - table[2][1] = 1; - for (int i = 3; i <= PRIMES[PRIMES.length - 1]; i++) { - table[i] = new int[i]; - if ((i & 1) == 0) { - int[] prev = table[i >> 1]; - for (int j = 0; j < prev.length; j++) - table[i][j] = 2 * prev[j]; - for (int j = 0; j < prev.length; j++) - table[i][prev.length + j] = 2 * prev[j] + 1; - } else { - int[] prev = table[i - 1]; - int med = (i - 1) >> 1; - for (int j = 0; j < med; j++) - table[i][j] = prev[j] + ((prev[j] >= med) ? 1 : 0); - table[i][med] = med; - for (int j = 0; j < med; j++) - table[i][med + j + 1] = prev[j + med] + ((prev[j + med] >= med) ? 1 : 0); - } - } - for (int i = 0; i < PRIMES.length; i++) { - int p = PRIMES[i]; - SIGMA[i] = new int[p]; - System.arraycopy(table[p], 0, SIGMA[i], 0, p); - } - UI.printInfo(Module.QMC, "Initializing lattice tables ..."); - FIBONACCI[0] = 0; - FIBONACCI[1] = 1; - for (int i = 2; i < FIBONACCI.length; i++) { - FIBONACCI[i] = FIBONACCI[i - 1] + FIBONACCI[i - 2]; - FIBONACCI_INV[i] = 1.0 / FIBONACCI[i]; - } - KOROBOV[0] = 1; - for (int i = 1; i < KOROBOV.length; i++) - KOROBOV[i] = 203 * KOROBOV[i - 1]; - } - - private static final int nextPrime(int p) { - p = p + (p & 1) + 1; - while (true) { - int div = 3; - boolean isPrime = true; - while (isPrime && ((div * div) <= p)) { - isPrime = ((p % div) != 0); - div += 2; - } - if (isPrime) - return p; - p += 2; - } - } - - private QMC() { - } - - public static double riVDC(int bits, int r) { - bits = (bits << 16) | (bits >>> 16); - bits = ((bits & 0x00ff00ff) << 8) | ((bits & 0xff00ff00) >>> 8); - bits = ((bits & 0x0f0f0f0f) << 4) | ((bits & 0xf0f0f0f0) >>> 4); - bits = ((bits & 0x33333333) << 2) | ((bits & 0xcccccccc) >>> 2); - bits = ((bits & 0x55555555) << 1) | ((bits & 0xaaaaaaaa) >>> 1); - bits ^= r; - return (double) (bits & 0xFFFFFFFFL) / (double) 0x100000000L; - } - - public static double riS(int i, int r) { - for (int v = 1 << 31; i != 0; i >>>= 1, v ^= v >>> 1) - if ((i & 1) != 0) - r ^= v; - return (double) (r & 0xFFFFFFFFL) / (double) 0x100000000L; - } - - public static double riLP(int i, int r) { - for (int v = 1 << 31; i != 0; i >>>= 1, v |= v >>> 1) - if ((i & 1) != 0) - r ^= v; - return (double) (r & 0xFFFFFFFFL) / (double) 0x100000000L; - } - - public static final double halton(int d, int i) { - // generalized Halton sequence - switch (d) { - case 0: { - i = (i << 16) | (i >>> 16); - i = ((i & 0x00ff00ff) << 8) | ((i & 0xff00ff00) >>> 8); - i = ((i & 0x0f0f0f0f) << 4) | ((i & 0xf0f0f0f0) >>> 4); - i = ((i & 0x33333333) << 2) | ((i & 0xcccccccc) >>> 2); - i = ((i & 0x55555555) << 1) | ((i & 0xaaaaaaaa) >>> 1); - return (double) (i & 0xFFFFFFFFL) / (double) 0x100000000L; - } - case 1: { - double v = 0; - double inv = 1.0 / 3; - double p; - int n; - for (p = inv, n = i; n != 0; p *= inv, n /= 3) - v += (n % 3) * p; - return v; - } - default: - } - int base = PRIMES[d]; - int[] perm = SIGMA[d]; - double v = 0; - double inv = 1.0 / base; - double p; - int n; - for (p = inv, n = i; n != 0; p *= inv, n /= base) - v += perm[n % base] * p; - return v; - } - - /** - * Compute mod(x,1), assuming that x is positive or 0. - * - * @param x any number >= 0 - * @return mod(x,1) - */ - public static final double mod1(double x) { - // assumes x >= 0 - return x - (int) x; - } - - /** - * Compute sigma function used to seed QMC sequence trees. The sigma table - * is exactly 2^order elements long, and therefore i should be in the: [0, - * 2^order) interval. This function is equal to 2^order*halton(0,i) - * - * @param i index - * @param order - * @return sigma function - */ - public static final int sigma(int i, int order) { - assert order > 0 && order < 32; - assert i >= 0 && i < (1 << order); - i = (i << 16) | (i >>> 16); - i = ((i & 0x00ff00ff) << 8) | ((i & 0xff00ff00) >>> 8); - i = ((i & 0x0f0f0f0f) << 4) | ((i & 0xf0f0f0f0) >>> 4); - i = ((i & 0x33333333) << 2) | ((i & 0xcccccccc) >>> 2); - i = ((i & 0x55555555) << 1) | ((i & 0xaaaaaaaa) >>> 1); - return i >>> (32 - order); - } - - public static final int getFibonacciRank(int n) { - int k = 3; - while (FIBONACCI[k] <= n) - k++; - return k - 1; - } - - public static final int fibonacci(int k) { - return FIBONACCI[k]; - } - - public static final double fibonacciLattice(int k, int i, int d) { - return d == 0 ? i * FIBONACCI_INV[k] : mod1((i * FIBONACCI[k - 1]) * FIBONACCI_INV[k]); - } - - public static final double reducedCPRotation(int k, int d, double x0, double x1) { - int j1 = FIBONACCI[2 * ((k - 1) >> 2) + 1]; - int j2 = FIBONACCI[2 * ((k + 1) >> 2)]; - if (d == 1) { - j1 = ((j1 * FIBONACCI[k - 1]) % FIBONACCI[k]); - j2 = ((j2 * FIBONACCI[k - 1]) % FIBONACCI[k]) - FIBONACCI[k]; - } - return (x0 * j1 + x1 * j2) * FIBONACCI_INV[k]; - } - - public static final double korobovLattice(int m, int i, int d) { - return mod1(i * KOROBOV[d] / (1 << m)); - } -} \ No newline at end of file diff --git a/src/org/sunflow/math/Solvers.java b/src/org/sunflow/math/Solvers.java deleted file mode 100644 index 5377d43..0000000 --- a/src/org/sunflow/math/Solvers.java +++ /dev/null @@ -1,137 +0,0 @@ -package org.sunflow.math; - -public final class Solvers { - /** - * Solves the equation ax^2+bx+c=0. Solutions are returned in a sorted array - * if they exist. - * - * @param a coefficient of x^2 - * @param b coefficient of x^1 - * @param c coefficient of x^0 - * @return an array containing the two real roots, or null if - * no real solutions exist - */ - public static final double[] solveQuadric(double a, double b, double c) { - double disc = b * b - 4 * a * c; - if (disc < 0) - return null; - disc = Math.sqrt(disc); - double q = ((b < 0) ? -0.5 * (b - disc) : -0.5 * (b + disc)); - double t0 = q / a; - double t1 = c / q; - // return sorted array - return (t0 > t1) ? new double[] { t1, t0 } : new double[] { t0, t1 }; - } - - /** - * Solve a quartic equation of the form ax^4+bx^3+cx^2+cx^1+d=0. The roots - * are returned in a sorted array of doubles in increasing order. - * - * @param a coefficient of x^4 - * @param b coefficient of x^3 - * @param c coefficient of x^2 - * @param d coefficient of x^1 - * @param e coefficient of x^0 - * @return a sorted array of roots, or null if no solutions - * exist - */ - public static double[] solveQuartic(double a, double b, double c, double d, double e) { - double inva = 1 / a; - double c1 = b * inva; - double c2 = c * inva; - double c3 = d * inva; - double c4 = e * inva; - // cubic resolvant - double c12 = c1 * c1; - double p = -0.375 * c12 + c2; - double q = 0.125 * c12 * c1 - 0.5 * c1 * c2 + c3; - double r = -0.01171875 * c12 * c12 + 0.0625 * c12 * c2 - 0.25 * c1 * c3 + c4; - double z = solveCubicForQuartic(-0.5 * p, -r, 0.5 * r * p - 0.125 * q * q); - double d1 = 2.0 * z - p; - if (d1 < 0) { - if (d1 > 1.0e-10) - d1 = 0; - else - return null; - } - double d2; - if (d1 < 1.0e-10) { - d2 = z * z - r; - if (d2 < 0) - return null; - d2 = Math.sqrt(d2); - } else { - d1 = Math.sqrt(d1); - d2 = 0.5 * q / d1; - } - // setup usefull values for the quadratic factors - double q1 = d1 * d1; - double q2 = -0.25 * c1; - double pm = q1 - 4 * (z - d2); - double pp = q1 - 4 * (z + d2); - if (pm >= 0 && pp >= 0) { - // 4 roots (!) - pm = Math.sqrt(pm); - pp = Math.sqrt(pp); - double[] results = new double[4]; - results[0] = -0.5 * (d1 + pm) + q2; - results[1] = -0.5 * (d1 - pm) + q2; - results[2] = 0.5 * (d1 + pp) + q2; - results[3] = 0.5 * (d1 - pp) + q2; - // tiny insertion sort - for (int i = 1; i < 4; i++) { - for (int j = i; j > 0 && results[j - 1] > results[j]; j--) { - double t = results[j]; - results[j] = results[j - 1]; - results[j - 1] = t; - } - } - return results; - } else if (pm >= 0) { - pm = Math.sqrt(pm); - double[] results = new double[2]; - results[0] = -0.5 * (d1 + pm) + q2; - results[1] = -0.5 * (d1 - pm) + q2; - return results; - } else if (pp >= 0) { - pp = Math.sqrt(pp); - double[] results = new double[2]; - results[0] = 0.5 * (d1 - pp) + q2; - results[1] = 0.5 * (d1 + pp) + q2; - return results; - } - return null; - } - - /** - * Return only one root for the specified cubic equation. This routine is - * only meant to be called by the quartic solver. It assumes the cubic is of - * the form: x^3+px^2+qx+r. - * - * @param p - * @param q - * @param r - * @return - */ - private static final double solveCubicForQuartic(double p, double q, double r) { - double A2 = p * p; - double Q = (A2 - 3.0 * q) / 9.0; - double R = (p * (A2 - 4.5 * q) + 13.5 * r) / 27.0; - double Q3 = Q * Q * Q; - double R2 = R * R; - double d = Q3 - R2; - double an = p / 3.0; - if (d >= 0) { - d = R / Math.sqrt(Q3); - double theta = Math.acos(d) / 3.0; - double sQ = -2.0 * Math.sqrt(Q); - return sQ * Math.cos(theta) - an; - } else { - double sQ = Math.pow(Math.sqrt(R2 - Q3) + Math.abs(R), 1.0 / 3.0); - if (R < 0) - return (sQ + Q / sQ) - an; - else - return -(sQ + Q / sQ) - an; - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/math/Vector3.java b/src/org/sunflow/math/Vector3.java deleted file mode 100644 index 30ed598..0000000 --- a/src/org/sunflow/math/Vector3.java +++ /dev/null @@ -1,195 +0,0 @@ -package org.sunflow.math; - -public final class Vector3 { - private static final float[] COS_THETA = new float[256]; - private static final float[] SIN_THETA = new float[256]; - private static final float[] COS_PHI = new float[256]; - private static final float[] SIN_PHI = new float[256]; - - public float x, y, z; - - static { - // precompute tables to compress unit vectors - for (int i = 0; i < 256; i++) { - double angle = (i * Math.PI) / 256.0; - COS_THETA[i] = (float) Math.cos(angle); - SIN_THETA[i] = (float) Math.sin(angle); - COS_PHI[i] = (float) Math.cos(2 * angle); - SIN_PHI[i] = (float) Math.sin(2 * angle); - } - } - - public Vector3() { - } - - public Vector3(float x, float y, float z) { - this.x = x; - this.y = y; - this.z = z; - } - - public Vector3(Vector3 v) { - x = v.x; - y = v.y; - z = v.z; - } - - public static final Vector3 decode(short n, Vector3 dest) { - int t = (n & 0xFF00) >>> 8; - int p = n & 0xFF; - dest.x = SIN_THETA[t] * COS_PHI[p]; - dest.y = SIN_THETA[t] * SIN_PHI[p]; - dest.z = COS_THETA[t]; - return dest; - } - - public static final Vector3 decode(short n) { - return decode(n, new Vector3()); - } - - public final short encode() { - int theta = (int) (Math.acos(z) * (256.0 / Math.PI)); - if (theta > 255) - theta = 255; - int phi = (int) (Math.atan2(y, x) * (128.0 / Math.PI)); - if (phi < 0) - phi += 256; - else if (phi > 255) - phi = 255; - return (short) (((theta & 0xFF) << 8) | (phi & 0xFF)); - } - - public float get(int i) { - switch (i) { - case 0: - return x; - case 1: - return y; - default: - return z; - } - } - - public final float length() { - return (float) Math.sqrt((x * x) + (y * y) + (z * z)); - } - - public final float lengthSquared() { - return (x * x) + (y * y) + (z * z); - } - - public final Vector3 negate() { - x = -x; - y = -y; - z = -z; - return this; - } - - public final Vector3 negate(Vector3 dest) { - dest.x = -x; - dest.y = -y; - dest.z = -z; - return dest; - } - - public final Vector3 mul(float s) { - x *= s; - y *= s; - z *= s; - return this; - } - - public final Vector3 mul(float s, Vector3 dest) { - dest.x = x * s; - dest.y = y * s; - dest.z = z * s; - return dest; - } - - public final Vector3 div(float d) { - x /= d; - y /= d; - z /= d; - return this; - } - - public final Vector3 div(float d, Vector3 dest) { - dest.x = x / d; - dest.y = y / d; - dest.z = z / d; - return dest; - } - - public final float normalizeLength() { - float n = (float) Math.sqrt(x * x + y * y + z * z); - float in = 1.0f / n; - x *= in; - y *= in; - z *= in; - return n; - } - - public final Vector3 normalize() { - float in = 1.0f / (float) Math.sqrt((x * x) + (y * y) + (z * z)); - x *= in; - y *= in; - z *= in; - return this; - } - - public final Vector3 normalize(Vector3 dest) { - float in = 1.0f / (float) Math.sqrt((x * x) + (y * y) + (z * z)); - dest.x = x * in; - dest.y = y * in; - dest.z = z * in; - return dest; - } - - public final Vector3 set(float x, float y, float z) { - this.x = x; - this.y = y; - this.z = z; - return this; - } - - public final Vector3 set(Vector3 v) { - x = v.x; - y = v.y; - z = v.z; - return this; - } - - public final float dot(float vx, float vy, float vz) { - return vx * x + vy * y + vz * z; - } - - public static final float dot(Vector3 v1, Vector3 v2) { - return (v1.x * v2.x) + (v1.y * v2.y) + (v1.z * v2.z); - } - - public static final Vector3 cross(Vector3 v1, Vector3 v2, Vector3 dest) { - dest.x = (v1.y * v2.z) - (v1.z * v2.y); - dest.y = (v1.z * v2.x) - (v1.x * v2.z); - dest.z = (v1.x * v2.y) - (v1.y * v2.x); - return dest; - } - - public static final Vector3 add(Vector3 v1, Vector3 v2, Vector3 dest) { - dest.x = v1.x + v2.x; - dest.y = v1.y + v2.y; - dest.z = v1.z + v2.z; - return dest; - } - - public static final Vector3 sub(Vector3 v1, Vector3 v2, Vector3 dest) { - dest.x = v1.x - v2.x; - dest.y = v1.y - v2.y; - dest.z = v1.z - v2.z; - return dest; - } - - @Override - public final String toString() { - return String.format("(%.2f, %.2f, %.2f)", x, y, z); - } -} \ No newline at end of file diff --git a/src/org/sunflow/system/BenchmarkFramework.java b/src/org/sunflow/system/BenchmarkFramework.java deleted file mode 100644 index b675261..0000000 --- a/src/org/sunflow/system/BenchmarkFramework.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.sunflow.system; - -import org.sunflow.system.UI.Module; - -/** - * This class provides a very simple framework for running a BenchmarkTest - * kernel several times and time the results. - */ -public class BenchmarkFramework { - private Timer[] timers; - private int timeLimit; // time limit in seconds - - public BenchmarkFramework(int iterations, int timeLimit) { - this.timeLimit = timeLimit; - timers = new Timer[iterations]; - } - - public void execute(BenchmarkTest test) { - // clear previous results - for (int i = 0; i < timers.length; i++) - timers[i] = null; - // loop for the specified number of iterations or until the time limit - long startTime = System.nanoTime(); - for (int i = 0; i < timers.length && ((System.nanoTime() - startTime) / 1000000000) < timeLimit; i++) { - UI.printInfo(Module.BENCH, "Running iteration %d", (i + 1)); - timers[i] = new Timer(); - test.kernelBegin(); - timers[i].start(); - test.kernelMain(); - timers[i].end(); - test.kernelEnd(); - } - // report stats - double avg = 0; - double min = Double.POSITIVE_INFINITY; - double max = Double.NEGATIVE_INFINITY; - int n = 0; - for (Timer t : timers) { - if (t == null) - break; - double s = t.seconds(); - min = Math.min(min, s); - max = Math.max(max, s); - avg += s; - n++; - } - if (n == 0) - return; - avg /= n; - double stdDev = 0; - for (Timer t : timers) { - if (t == null) - break; - double s = t.seconds(); - stdDev += (s - avg) * (s - avg); - } - stdDev = Math.sqrt(stdDev / n); - UI.printInfo(Module.BENCH, "Benchmark results:"); - UI.printInfo(Module.BENCH, " * Iterations: %d", n); - UI.printInfo(Module.BENCH, " * Average: %s", Timer.toString(avg)); - UI.printInfo(Module.BENCH, " * Fastest: %s", Timer.toString(min)); - UI.printInfo(Module.BENCH, " * Longest: %s", Timer.toString(max)); - UI.printInfo(Module.BENCH, " * Deviation: %s", Timer.toString(stdDev)); - for (int i = 0; i < timers.length && timers[i] != null; i++) - UI.printDetailed(Module.BENCH, " * Iteration %d: %s", i + 1, timers[i]); - } -} \ No newline at end of file diff --git a/src/org/sunflow/system/BenchmarkTest.java b/src/org/sunflow/system/BenchmarkTest.java deleted file mode 100644 index aa30a91..0000000 --- a/src/org/sunflow/system/BenchmarkTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.sunflow.system; - -/** - * This interface is used to represent a piece of code which is to be - * benchmarked by repeatedly running and timing the kernel code. The begin/end - * routines are called per-iteration to do any local initialization which is not - * meant to be taken into acount in the timing (like preparing or destroying - * data structures). - */ -public interface BenchmarkTest { - - public void kernelBegin(); - - public void kernelMain(); - - public void kernelEnd(); -} \ No newline at end of file diff --git a/src/org/sunflow/system/ByteUtil.java b/src/org/sunflow/system/ByteUtil.java deleted file mode 100644 index 3869cf5..0000000 --- a/src/org/sunflow/system/ByteUtil.java +++ /dev/null @@ -1,118 +0,0 @@ -package org.sunflow.system; - -public class ByteUtil { - - public static final byte[] get2Bytes(int i) { - byte[] b = new byte[2]; - - b[0] = (byte) (i & 0xFF); - b[1] = (byte) ((i >> 8) & 0xFF); - - return b; - } - - public static final byte[] get4Bytes(int i) { - byte[] b = new byte[4]; - - b[0] = (byte) (i & 0xFF); - b[1] = (byte) ((i >> 8) & 0xFF); - b[2] = (byte) ((i >> 16) & 0xFF); - b[3] = (byte) ((i >> 24) & 0xFF); - - return b; - } - - public static final byte[] get4BytesInv(int i) { - byte[] b = new byte[4]; - - b[3] = (byte) (i & 0xFF); - b[2] = (byte) ((i >> 8) & 0xFF); - b[1] = (byte) ((i >> 16) & 0xFF); - b[0] = (byte) ((i >> 24) & 0xFF); - - return b; - } - - public static final byte[] get8Bytes(long i) { - byte[] b = new byte[8]; - - b[0] = (byte) (i & 0xFF); - b[1] = (byte) ((i >> 8) & 0xFF); - b[2] = (byte) ((i >> 16) & 0xFF); - b[3] = (byte) ((i >> 24) & 0xFF); - - b[4] = (byte) ((i >> 32) & 0xFF); - b[5] = (byte) ((i >> 40) & 0xFF); - b[6] = (byte) ((i >> 48) & 0xFF); - b[7] = (byte) ((i >> 56) & 0xFF); - - return b; - } - - public static final long toLong(byte[] in) { - return (((toInt(in[0], in[1], in[2], in[3]))) | ((long) (toInt(in[4], in[5], in[6], in[7])) << (long) 32)); - } - - public static final int toInt(byte in0, byte in1, byte in2, byte in3) { - return (in0 & 0xFF) | ((in1 & 0xFF) << 8) | ((in2 & 0xFF) << 16) | ((in3 & 0xFF) << 24); - } - - public static final int toInt(byte[] in) { - return toInt(in[0], in[1], in[2], in[3]); - } - - public static final int toInt(byte[] in, int ofs) { - return toInt(in[ofs + 0], in[ofs + 1], in[ofs + 2], in[ofs + 3]); - } - - public static final int floatToHalf(float f) { - int i = Float.floatToRawIntBits(f); - // unpack the s, e and m of the float - int s = (i >> 16) & 0x00008000; - int e = ((i >> 23) & 0x000000ff) - (127 - 15); - int m = i & 0x007fffff; - // pack them back up, forming a half - if (e <= 0) { - if (e < -10) { - // E is less than -10. The absolute value of f is less than - // HALF_MIN - // convert f to 0 - return 0; - } - // E is between -10 and 0. - m = (m | 0x00800000) >> (1 - e); - // Round to nearest, round "0.5" up. - if ((m & 0x00001000) == 0x00001000) - m += 0x00002000; - // Assemble the half from s, e (zero) and m. - return s | (m >> 13); - } else if (e == 0xff - (127 - 15)) { - if (m == 0) { - // F is an infinity; convert f to a half infinity - return s | 0x7c00; - } else { - // F is a NAN; we produce a half NAN that preserves the sign bit - // and the 10 leftmost bits of the significand of f - m >>= 13; - return s | 0x7c00 | m | ((m == 0) ? 0 : 1); - } - } else { - // E is greater than zero. F is a normalized float. Round to - // nearest, round "0.5" up - if ((m & 0x00001000) == 0x00001000) { - m += 0x00002000; - if ((m & 0x00800000) == 0x00800000) { - m = 0; - e += 1; - } - } - // Handle exponent overflow - if (e > 30) { - // overflow (); // Cause a hardware floating point overflow; - return s | 0x7c00; // if this returns, the half becomes an - } // infinity with the same sign as f. - // Assemble the half from s, e and m. - return s | (e << 10) | (m >> 13); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/system/FileUtils.java b/src/org/sunflow/system/FileUtils.java deleted file mode 100644 index 3db7e7c..0000000 --- a/src/org/sunflow/system/FileUtils.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.sunflow.system; - -import java.io.File; -import java.util.Locale; - -public final class FileUtils { - /** - * Extract the file extension from the specified filename. - * - * @param filename filename to get the extension of - * @return a string representing the file extension, or null - * if the filename doesn't have any extension, or is not a file - */ - public static final String getExtension(String filename) { - if (filename == null) - return null; - File f = new File(filename); - if (f.isDirectory()) - return null; - String name = new File(filename).getName(); - int idx = name.lastIndexOf('.'); - return idx == -1 ? null : name.substring(idx + 1).toLowerCase(Locale.ENGLISH); - } -} \ No newline at end of file diff --git a/src/org/sunflow/system/ImagePanel.java b/src/org/sunflow/system/ImagePanel.java deleted file mode 100644 index 77c7e18..0000000 --- a/src/org/sunflow/system/ImagePanel.java +++ /dev/null @@ -1,262 +0,0 @@ -package org.sunflow.system; - -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.event.InputEvent; -import java.awt.event.MouseEvent; -import java.awt.event.MouseWheelEvent; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; - -import javax.imageio.ImageIO; -import javax.swing.JPanel; -import javax.swing.event.MouseInputAdapter; - -import org.sunflow.core.Display; -import org.sunflow.image.Color; - -@SuppressWarnings("serial") -public class ImagePanel extends JPanel implements Display { - private static final int[] BORDERS = { Color.RED.toRGB(), - Color.GREEN.toRGB(), Color.BLUE.toRGB(), Color.YELLOW.toRGB(), - Color.CYAN.toRGB(), Color.MAGENTA.toRGB() }; - private BufferedImage image; - private float xo, yo; - private float w, h; - private long repaintCounter; - - private class ScrollZoomListener extends MouseInputAdapter { - int mx; - int my; - boolean dragging; - boolean zooming; - - @Override - public void mousePressed(MouseEvent e) { - mx = e.getX(); - my = e.getY(); - switch (e.getButton()) { - case MouseEvent.BUTTON1: - dragging = true; - zooming = false; - break; - case MouseEvent.BUTTON2: { - dragging = zooming = false; - // if CTRL is pressed - if ((e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) == InputEvent.CTRL_DOWN_MASK) - fit(); - else - reset(); - break; - } - case MouseEvent.BUTTON3: - zooming = true; - dragging = false; - break; - default: - return; - } - repaint(); - } - - @Override - public void mouseDragged(MouseEvent e) { - int mx2 = e.getX(); - int my2 = e.getY(); - if (dragging) - drag(mx2 - mx, my2 - my); - if (zooming) - zoom(mx2 - mx, my2 - my); - mx = mx2; - my = my2; - } - - @Override - public void mouseReleased(MouseEvent e) { - // same behaviour - mouseDragged(e); - } - - @Override - public void mouseWheelMoved(MouseWheelEvent e) { - zoom(-20 * e.getWheelRotation(), 0); - } - } - - public ImagePanel() { - setPreferredSize(new Dimension(640, 480)); - image = null; - xo = yo = 0; - w = h = 0; - ScrollZoomListener listener = new ScrollZoomListener(); - addMouseListener(listener); - addMouseMotionListener(listener); - addMouseWheelListener(listener); - } - - public void save(String filename) { - // Bitmap.save(image, filename); - try { - ImageIO.write(image, "png", new File(filename)); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private synchronized void drag(int dx, int dy) { - xo += dx; - yo += dy; - repaint(); - } - - private synchronized void zoom(int dx, int dy) { - int a = Math.max(dx, dy); - int b = Math.min(dx, dy); - if (Math.abs(b) > Math.abs(a)) - a = b; - if (a == 0) - return; - // window center - float cx = getWidth() * 0.5f; - float cy = getHeight() * 0.5f; - - // origin of the image in window space - float x = xo + (getWidth() - w) * 0.5f; - float y = yo + (getHeight() - h) * 0.5f; - - // coordinates of the pixel we are over - float sx = cx - x; - float sy = cy - y; - - // scale - if (w + a > 100) { - h = (w + a) * h / w; - sx = (w + a) * sx / w; - sy = (w + a) * sy / w; - w = (w + a); - } - - // restore center pixel - - float x2 = cx - sx; - float y2 = cy - sy; - - xo = (x2 - (getWidth() - w) * 0.5f); - yo = (y2 - (getHeight() - h) * 0.5f); - - repaint(); - } - - public synchronized void reset() { - xo = yo = 0; - if (image != null) { - w = image.getWidth(); - h = image.getHeight(); - } - repaint(); - } - - public synchronized void fit() { - xo = yo = 0; - if (image != null) { - float wx = Math.max(getWidth() - 10, 100); - float hx = wx * image.getHeight() / image.getWidth(); - float hy = Math.max(getHeight() - 10, 100); - float wy = hy * image.getWidth() / image.getHeight(); - if (hx > hy) { - w = wy; - h = hy; - } else { - w = wx; - h = hx; - } - repaint(); - } - } - - public synchronized void imageBegin(int w, int h, int bucketSize) { - if (image != null && w == image.getWidth() && h == image.getHeight()) { - // dull image if it has same resolution (75%) - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - int rgba = image.getRGB(x, y); - image.setRGB(x, y, ((rgba & 0xFEFEFEFE) >>> 1) + ((rgba & 0xFCFCFCFC) >>> 2)); - } - } - } else { - // allocate new framebuffer - image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - // center - this.w = w; - this.h = h; - xo = yo = 0; - } - repaintCounter = System.nanoTime(); - repaint(); - } - - public synchronized void imagePrepare(int x, int y, int w, int h, int id) { - int border = BORDERS[id % BORDERS.length] | 0xFF000000; - for (int by = 0; by < h; by++) { - for (int bx = 0; bx < w; bx++) { - if (bx == 0 || bx == w - 1) { - if (5 * by < h || 5 * (h - by - 1) < h) - image.setRGB(x + bx, y + by, border); - } else if (by == 0 || by == h - 1) { - if (5 * bx < w || 5 * (w - bx - 1) < w) - image.setRGB(x + bx, y + by, border); - } - } - } - repaint(); - } - - public synchronized void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha) { - for (int j = 0, index = 0; j < h; j++) - for (int i = 0; i < w; i++, index++) - image.setRGB(x + i, y + j, data[index].copy().mul(1.0f / alpha[index]).toNonLinear().toRGBA(alpha[index])); - repaint(); - } - - public synchronized void imageFill(int x, int y, int w, int h, Color c, float alpha) { - int rgba = c.copy().mul(1.0f / alpha).toNonLinear().toRGBA(alpha); - for (int j = 0, index = 0; j < h; j++) - for (int i = 0; i < w; i++, index++) - image.setRGB(x + i, y + j, rgba); - fastRepaint(); - } - - public void imageEnd() { - repaint(); - } - - private void fastRepaint() { - long t = System.nanoTime(); - if (repaintCounter + 125000000 < t) { - repaintCounter = t; - repaint(); - } - } - - @Override - public synchronized void paintComponent(Graphics g) { - super.paintComponent(g); - if (image == null) - return; - int x = Math.round(xo + (getWidth() - w) * 0.5f); - int y = Math.round(yo + (getHeight() - h) * 0.5f); - int iw = Math.round(w); - int ih = Math.round(h); - int x0 = x - 1; - int y0 = y - 1; - int x1 = x + iw + 1; - int y1 = y + ih + 1; - g.setColor(java.awt.Color.WHITE); - g.drawLine(x0, y0, x1, y0); - g.drawLine(x1, y0, x1, y1); - g.drawLine(x1, y1, x0, y1); - g.drawLine(x0, y1, x0, y0); - g.drawImage(image, x, y, iw, ih, java.awt.Color.BLACK, this); - } -} \ No newline at end of file diff --git a/src/org/sunflow/system/Memory.java b/src/org/sunflow/system/Memory.java deleted file mode 100644 index a77666c..0000000 --- a/src/org/sunflow/system/Memory.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.sunflow.system; - -public final class Memory { - public static final String sizeof(int[] array) { - return bytesToString(array == null ? 0 : 4 * array.length); - } - - public static final String bytesToString(long bytes) { - if (bytes < 1024) - return String.format("%db", bytes); - if (bytes < 1024 * 1024) - return String.format("%dKb", (bytes + 512) >>> 10); - return String.format("%dMb", (bytes + 512 * 1024) >>> 20); - } -} \ No newline at end of file diff --git a/src/org/sunflow/system/Parser.java b/src/org/sunflow/system/Parser.java deleted file mode 100644 index ae351b4..0000000 --- a/src/org/sunflow/system/Parser.java +++ /dev/null @@ -1,155 +0,0 @@ -package org.sunflow.system; - -import java.io.BufferedReader; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.util.ArrayList; - -public class Parser { - private FileReader file; - private BufferedReader bf; - private String[] lineTokens; - private int index; - - public Parser(String filename) throws FileNotFoundException { - file = new FileReader(filename); - bf = new BufferedReader(file); - lineTokens = new String[0]; - index = 0; - } - - public void close() throws IOException { - if (file != null) - file.close(); - bf = null; - } - - public String getNextToken() throws IOException { - while (true) { - String tok = fetchNextToken(); - if (tok == null) - return null; - if (tok.equals("/*")) { - do { - tok = fetchNextToken(); - if (tok == null) - return null; - } while (!tok.equals("*/")); - } else - return tok; - } - } - - public boolean peekNextToken(String tok) throws IOException { - while (true) { - String t = fetchNextToken(); - if (t == null) - return false; // nothing left - if (t.equals("/*")) { - do { - t = fetchNextToken(); - if (t == null) - return false; // nothing left - } while (!t.equals("*/")); - } else if (t.equals(tok)) { - // we found the right token, keep parsing - return true; - } else { - // rewind the token so we can try again - index--; - return false; - } - } - } - - private String fetchNextToken() throws IOException { - if (bf == null) - return null; - while (true) { - if (index < lineTokens.length) - return lineTokens[index++]; - else if (!getNextLine()) - return null; - } - } - - private boolean getNextLine() throws IOException { - String line = bf.readLine(); - - if (line == null) - return false; - - ArrayList tokenList = new ArrayList(); - String current = new String(); - boolean inQuotes = false; - - for (int i = 0; i < line.length(); i++) { - char c = line.charAt(i); - if (current.length() == 0 && (c == '%' || c == '#')) - break; - - boolean quote = c == '\"'; - inQuotes = inQuotes ^ quote; - - if (!quote && (inQuotes || !Character.isWhitespace(c))) - current += c; - else if (current.length() > 0) { - tokenList.add(current); - current = new String(); - } - } - - if (current.length() > 0) - tokenList.add(current); - lineTokens = tokenList.toArray(new String[0]); - index = 0; - return true; - } - - public String getNextCodeBlock() throws ParserException, IOException { - // read a java code block - String code = new String(); - checkNextToken(""); - while (true) { - String line; - try { - line = bf.readLine(); - } catch (IOException e) { - e.printStackTrace(); - return null; - } - if (line.trim().equals("")) - return code; - code += line; - code += "\n"; - } - } - - public boolean getNextBoolean() throws IOException { - return Boolean.valueOf(getNextToken()).booleanValue(); - } - - public int getNextInt() throws IOException { - return Integer.parseInt(getNextToken()); - } - - public float getNextFloat() throws IOException { - return Float.parseFloat(getNextToken()); - } - - public void checkNextToken(String token) throws ParserException, IOException { - String found = getNextToken(); - if (!token.equals(found)) { - close(); - throw new ParserException(token, found); - } - } - - @SuppressWarnings("serial") - public static class ParserException extends Exception { - private ParserException(String token, String found) { - super(String.format("Expecting %s found %s", token, found)); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/system/Plugins.java b/src/org/sunflow/system/Plugins.java deleted file mode 100644 index 991e9ee..0000000 --- a/src/org/sunflow/system/Plugins.java +++ /dev/null @@ -1,153 +0,0 @@ -package org.sunflow.system; - -import org.codehaus.janino.ClassBodyEvaluator; -import org.codehaus.janino.CompileException; -import org.codehaus.janino.Parser.ParseException; -import org.codehaus.janino.Scanner.ScanException; -import org.sunflow.system.UI.Module; -import org.sunflow.util.FastHashMap; - -/** - * This class represents a list of plugins which implement a certain interface - * or extend a certain class. Many plugins may be registered and created at a - * later time by recalling their unique name only. - * - * @param Default constructible type or interface all plugins will derive - * from or implement - */ -public final class Plugins { - private final FastHashMap> pluginClasses; - private final Class baseClass; - - /** - * Create an empty plugin list. You must specify T.class as - * an argument. - * - * @param baseClass - */ - public Plugins(Class baseClass) { - pluginClasses = new FastHashMap>(); - this.baseClass = baseClass; - } - - /** - * Create an object from the specified type name. If this type name is - * unknown or invalid, null is returned. - * - * @param name plugin type name - * @return an instance of the specified plugin type, or null - * if not found or invalid - */ - public T createObject(String name) { - if (name == null || name.equals("none")) - return null; - Class c = pluginClasses.get(name); - if (c == null) { - // don't print an error, this will be handled by the caller - return null; - } - try { - return c.newInstance(); - } catch (InstantiationException e) { - UI.printError(Module.API, "Cannot create object of type \"%s\" - %s", name, e.getLocalizedMessage()); - return null; - } catch (IllegalAccessException e) { - UI.printError(Module.API, "Cannot create object of type \"%s\" - %s", name, e.getLocalizedMessage()); - return null; - } - } - - /** - * Check this plugin list for the presence of the specified type name - * - * @param name plugin type name - * @return true if this name has been registered, - * false otherwise - */ - public boolean hasType(String name) { - return pluginClasses.get(name) != null; - } - - /** - * Generate a unique plugin type name which has not yet been registered. - * This is meant to be used when the actual type name is not crucial, but - * succesfully registration is. - * - * @param prefix a prefix to be used in generating the unique name - * @return a unique plugin type name not yet in use - */ - public String generateUniqueName(String prefix) { - String type; - for (int i = 1; hasType(type = String.format("%s_%d", prefix, i)); i++) { - } - return type; - } - - /** - * Define a new plugin type from java source code. The code string contains - * import declarations and a class body only. The implemented type is - * implicitly the one of the plugin list being registered against.If the - * plugin type name was previously associated with a different class, it - * will be overriden. This allows the behavior core classes to be modified - * at runtime. - * - * @param name plugin type name - * @param sourceCode Java source code definition for the plugin - * @return true if the code compiled and registered - * successfully, false otherwise - */ - @SuppressWarnings("unchecked") - public boolean registerPlugin(String name, String sourceCode) { - try { - ClassBodyEvaluator cbe = new ClassBodyEvaluator(); - cbe.setClassName(name); - if (baseClass.isInterface()) - cbe.setImplementedTypes(new Class[] { baseClass }); - else - cbe.setExtendedType(baseClass); - cbe.cook(sourceCode); - return registerPlugin(name, cbe.getClazz()); - } catch (CompileException e) { - UI.printError(Module.API, "Plugin \"%s\" could not be declared - %s", name, e.getLocalizedMessage()); - return false; - } catch (ParseException e) { - UI.printError(Module.API, "Plugin \"%s\" could not be declared - %s", name, e.getLocalizedMessage()); - return false; - } catch (ScanException e) { - UI.printError(Module.API, "Plugin \"%s\" could not be declared - %s", name, e.getLocalizedMessage()); - return false; - } - } - - /** - * Define a new plugin type from an existing class. This checks to make sure - * the provided class is default constructible (ie: has a constructor with - * no parameters). If the plugin type name was previously associated with a - * different class, it will be overriden. This allows the behavior core - * classes to be modified at runtime. - * - * @param name plugin type name - * @param pluginClass class object for the plugin class - * @return true if the plugin registered successfully, - * false otherwise - */ - public boolean registerPlugin(String name, Class pluginClass) { - // check that the given class is compatible with the base class - try { - if (pluginClass.getConstructor() == null) { - UI.printError(Module.API, "Plugin \"%s\" could not be declared - default constructor was not found", name); - return false; - } - } catch (SecurityException e) { - UI.printError(Module.API, "Plugin \"%s\" could not be declared - default constructor is not visible (%s)", name, e.getLocalizedMessage()); - return false; - } catch (NoSuchMethodException e) { - UI.printError(Module.API, "Plugin \"%s\" could not be declared - default constructor was not found (%s)", name, e.getLocalizedMessage()); - return false; - } - if (pluginClasses.get(name) != null) - UI.printWarning(Module.API, "Plugin \"%s\" was already defined - overwriting previous definition", name); - pluginClasses.put(name, pluginClass); - return true; - } -} \ No newline at end of file diff --git a/src/org/sunflow/system/RenderGlobalsPanel.java b/src/org/sunflow/system/RenderGlobalsPanel.java deleted file mode 100644 index 993358b..0000000 --- a/src/org/sunflow/system/RenderGlobalsPanel.java +++ /dev/null @@ -1,207 +0,0 @@ -package org.sunflow.system; - -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.GridLayout; - -import javax.swing.BorderFactory; -import javax.swing.BoxLayout; -import javax.swing.ComboBoxModel; -import javax.swing.DefaultComboBoxModel; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JRadioButton; -import javax.swing.JTabbedPane; -import javax.swing.JTextField; -import javax.swing.WindowConstants; -import javax.swing.border.BevelBorder; -import javax.swing.border.TitledBorder; - -/** - * This code was edited or generated using CloudGarden's Jigloo SWT/Swing GUI - * Builder, which is free for non-commercial use. If Jigloo is being used - * commercially (ie, by a corporation, company or business for any purpose - * whatever) then you should purchase a license for each developer using Jigloo. - * Please visit www.cloudgarden.com for details. Use of Jigloo implies - * acceptance of these licensing terms. A COMMERCIAL LICENSE HAS NOT BEEN - * PURCHASED FOR THIS MACHINE, SO JIGLOO OR THIS CODE CANNOT BE USED LEGALLY FOR - * ANY CORPORATE OR COMMERCIAL PURPOSE. - */ -@SuppressWarnings("serial") -public class RenderGlobalsPanel extends JTabbedPane { - private JPanel generalPanel; - private JComboBox maxSamplingComboxBox; - private JPanel samplingPanel; - private JComboBox minSamplingComboBox; - private JLabel jLabel6; - private JLabel jLabel5; - private JRadioButton defaultRendererRadioButton; - private JRadioButton bucketRendererRadioButton; - private JPanel bucketRendererPanel; - private JLabel jLabel2; - private JPanel rendererPanel; - private JTextField threadTextField; - private JCheckBox threadCheckBox; - private JLabel jLabel3; - private JPanel threadsPanel; - private JLabel jLabel1; - private JPanel resolutionPanel; - private JTextField resolutionYTextField; - private JTextField resolutionXTextField; - private JCheckBox resolutionCheckBox; - - /** - * This method initializes this - */ - private void initialize() { - - } - - /** - * Auto-generated main method to display this JPanel inside a new JFrame. - */ - public static void main(String[] args) { - JFrame frame = new JFrame(); - frame.getContentPane().add(new RenderGlobalsPanel()); - frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); - frame.pack(); - frame.setVisible(true); - } - - public RenderGlobalsPanel() { - super(); - initialize(); - initGUI(); - } - - private void initGUI() { - try { - setPreferredSize(new Dimension(400, 300)); - { - generalPanel = new JPanel(); - FlowLayout generalPanelLayout = new FlowLayout(); - generalPanelLayout.setAlignment(FlowLayout.LEFT); - generalPanel.setLayout(generalPanelLayout); - this.addTab("General", null, generalPanel, null); - { - resolutionPanel = new JPanel(); - generalPanel.add(resolutionPanel); - FlowLayout resolutionPanelLayout = new FlowLayout(); - resolutionPanel.setLayout(resolutionPanelLayout); - resolutionPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(BevelBorder.LOWERED), "Resolution", TitledBorder.LEADING, TitledBorder.TOP)); - { - resolutionCheckBox = new JCheckBox(); - resolutionPanel.add(resolutionCheckBox); - resolutionCheckBox.setText("Override"); - } - { - jLabel1 = new JLabel(); - resolutionPanel.add(jLabel1); - jLabel1.setText("Image Width:"); - } - { - resolutionXTextField = new JTextField(); - resolutionPanel.add(resolutionXTextField); - resolutionXTextField.setText("640"); - resolutionXTextField.setPreferredSize(new java.awt.Dimension(50, 20)); - } - { - jLabel2 = new JLabel(); - resolutionPanel.add(jLabel2); - jLabel2.setText("Image Height:"); - } - { - resolutionYTextField = new JTextField(); - resolutionPanel.add(resolutionYTextField); - resolutionYTextField.setText("480"); - resolutionYTextField.setPreferredSize(new java.awt.Dimension(50, 20)); - } - } - { - threadsPanel = new JPanel(); - generalPanel.add(threadsPanel); - threadsPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(BevelBorder.LOWERED), "Threads", TitledBorder.LEADING, TitledBorder.TOP)); - { - threadCheckBox = new JCheckBox(); - threadsPanel.add(threadCheckBox); - threadCheckBox.setText("Use All Processors"); - } - { - jLabel3 = new JLabel(); - threadsPanel.add(jLabel3); - jLabel3.setText("Threads:"); - } - { - threadTextField = new JTextField(); - threadsPanel.add(threadTextField); - threadTextField.setText("1"); - threadTextField.setPreferredSize(new java.awt.Dimension(50, 20)); - } - } - } - { - rendererPanel = new JPanel(); - FlowLayout rendererPanelLayout = new FlowLayout(); - rendererPanelLayout.setAlignment(FlowLayout.LEFT); - rendererPanel.setLayout(rendererPanelLayout); - this.addTab("Renderer", null, rendererPanel, null); - { - defaultRendererRadioButton = new JRadioButton(); - rendererPanel.add(defaultRendererRadioButton); - defaultRendererRadioButton.setText("Default Renderer"); - } - { - bucketRendererPanel = new JPanel(); - BoxLayout bucketRendererPanelLayout = new BoxLayout(bucketRendererPanel, javax.swing.BoxLayout.Y_AXIS); - bucketRendererPanel.setLayout(bucketRendererPanelLayout); - rendererPanel.add(bucketRendererPanel); - bucketRendererPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(BevelBorder.LOWERED), "Bucket Renderer", TitledBorder.LEADING, TitledBorder.TOP)); - { - bucketRendererRadioButton = new JRadioButton(); - bucketRendererPanel.add(bucketRendererRadioButton); - bucketRendererRadioButton.setText("Enable"); - } - { - samplingPanel = new JPanel(); - GridLayout samplingPanelLayout = new GridLayout(2, 2); - samplingPanelLayout.setColumns(2); - samplingPanelLayout.setHgap(5); - samplingPanelLayout.setVgap(5); - samplingPanelLayout.setRows(2); - samplingPanel.setLayout(samplingPanelLayout); - bucketRendererPanel.add(samplingPanel); - { - jLabel5 = new JLabel(); - samplingPanel.add(jLabel5); - jLabel5.setText("Min:"); - } - { - ComboBoxModel minSamplingComboBoxModel = new DefaultComboBoxModel(new String[] { - "Item One", "Item Two" }); - minSamplingComboBox = new JComboBox(); - samplingPanel.add(minSamplingComboBox); - minSamplingComboBox.setModel(minSamplingComboBoxModel); - } - { - jLabel6 = new JLabel(); - samplingPanel.add(jLabel6); - jLabel6.setText("Max:"); - } - { - ComboBoxModel maxSamplingComboxBoxModel = new DefaultComboBoxModel(new String[] { - "Item One", "Item Two" }); - maxSamplingComboxBox = new JComboBox(); - samplingPanel.add(maxSamplingComboxBox); - maxSamplingComboxBox.setModel(maxSamplingComboxBoxModel); - } - } - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } -} \ No newline at end of file diff --git a/src/org/sunflow/system/SearchPath.java b/src/org/sunflow/system/SearchPath.java deleted file mode 100644 index 2a6882c..0000000 --- a/src/org/sunflow/system/SearchPath.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.sunflow.system; - -import java.io.File; -import java.io.IOException; -import java.util.LinkedList; - -import org.sunflow.system.UI.Module; - -public class SearchPath { - private LinkedList searchPath; - private String type; - - public SearchPath(String type) { - this.type = type; - searchPath = new LinkedList(); - } - - public void resetSearchPath() { - searchPath.clear(); - } - - public void addSearchPath(String path) { - File f = new File(path); - if (f.exists() && f.isDirectory()) { - try { - path = f.getCanonicalPath(); - for (String prefix : searchPath) - if (prefix.equals(path)) - return; - UI.printInfo(Module.SYS, "Adding %s search path: \"%s\"", type, path); - searchPath.add(path); - } catch (IOException e) { - UI.printError(Module.SYS, "Invalid %s search path specification: \"%s\" - %s", type, path, e.getMessage()); - } - } else - UI.printError(Module.SYS, "Invalid %s search path specification: \"%s\" - invalid directory", type, path); - } - - public String resolvePath(String filename) { - // account for relative naming schemes from 3rd party softwares - if (filename.startsWith("//")) - filename = filename.substring(2); - UI.printDetailed(Module.SYS, "Resolving %s path \"%s\" ...", type, filename); - File f = new File(filename); - if (!f.isAbsolute()) { - for (String prefix : searchPath) { - UI.printDetailed(Module.SYS, " * searching: \"%s\" ...", prefix); - if (prefix.endsWith(File.separator) || filename.startsWith(File.separator)) - f = new File(prefix + filename); - else - f = new File(prefix + File.separator + filename); - if (f.exists()) { - // suggested path exists - try it - return f.getAbsolutePath(); - } - } - } - // file was not found in the search paths - return the filename itself - return filename; - } -} \ No newline at end of file diff --git a/src/org/sunflow/system/Timer.java b/src/org/sunflow/system/Timer.java deleted file mode 100644 index ebf55b7..0000000 --- a/src/org/sunflow/system/Timer.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.sunflow.system; - -public class Timer { - private long startTime, endTime; - - public Timer() { - startTime = endTime = 0; - } - - public void start() { - startTime = endTime = System.nanoTime(); - } - - public void end() { - endTime = System.nanoTime(); - } - - public long nanos() { - return endTime - startTime; - } - - public double seconds() { - return (endTime - startTime) * 1e-9; - } - - public static String toString(long nanos) { - Timer t = new Timer(); - t.endTime = nanos; - return t.toString(); - } - - public static String toString(double seconds) { - Timer t = new Timer(); - t.endTime = (long) (seconds * 1e9); - return t.toString(); - } - - @Override - public String toString() { - long millis = nanos() / (1000 * 1000); - if (millis < 10000) - return String.format("%dms", millis); - long hours = millis / (60 * 60 * 1000); - millis -= hours * 60 * 60 * 1000; - long minutes = millis / (60 * 1000); - millis -= minutes * 60 * 1000; - long seconds = millis / 1000; - millis -= seconds * 1000; - return String.format("%d:%02d:%02d.%1d", hours, minutes, seconds, millis / 100); - } -} \ No newline at end of file diff --git a/src/org/sunflow/system/UI.java b/src/org/sunflow/system/UI.java deleted file mode 100644 index 9f5cfd1..0000000 --- a/src/org/sunflow/system/UI.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.sunflow.system; - -import java.util.Locale; - -import org.sunflow.system.ui.ConsoleInterface; -import org.sunflow.system.ui.SilentInterface; - -/** - * Static singleton interface to a UserInterface object. This is set to a text - * console by default. - */ -public final class UI { - private static UserInterface ui = new ConsoleInterface(); - private static boolean canceled = false; - private static int verbosity = 3; - - public enum Module { - API, GEOM, HAIR, ACCEL, BCKT, IPR, LIGHT, GUI, SCENE, BENCH, TEX, IMG, DISP, QMC, SYS, USER, CAM, - } - - public enum PrintLevel { - ERROR, WARN, INFO, DETAIL - } - - private UI() { - } - - /** - * Sets the active user interface implementation. Passing null - * silences printing completely. - * - * @param ui object to recieve all user interface calls - */ - public final static void set(UserInterface ui) { - if (ui == null) - ui = new SilentInterface(); - UI.ui = ui; - } - - public final static void verbosity(int verbosity) { - UI.verbosity = verbosity; - } - - public final static String formatOutput(Module m, PrintLevel level, String s) { - return String.format("%-5s %-6s: %s", m.name(), level.name().toLowerCase(Locale.ENGLISH), s); - } - - public final static synchronized void printDetailed(Module m, String s, Object... args) { - if (verbosity > 3) - ui.print(m, PrintLevel.DETAIL, String.format(s, args)); - } - - public final static synchronized void printInfo(Module m, String s, Object... args) { - if (verbosity > 2) - ui.print(m, PrintLevel.INFO, String.format(s, args)); - } - - public final static synchronized void printWarning(Module m, String s, Object... args) { - if (verbosity > 1) - ui.print(m, PrintLevel.WARN, String.format(s, args)); - } - - public final static synchronized void printError(Module m, String s, Object... args) { - if (verbosity > 0) - ui.print(m, PrintLevel.ERROR, String.format(s, args)); - } - - public final static synchronized void taskStart(String s, int min, int max) { - ui.taskStart(s, min, max); - } - - public final static synchronized void taskUpdate(int current) { - ui.taskUpdate(current); - } - - public final static synchronized void taskStop() { - ui.taskStop(); - // reset canceled status - // this assume the parent application will deal with it immediately - canceled = false; - } - - /** - * Cancel the currently active task. This forces the application to abort as - * soon as possible. - */ - public final static synchronized void taskCancel() { - printInfo(Module.GUI, "Abort requested by the user ..."); - canceled = true; - } - - /** - * Check to see if the current task should be aborted. - * - * @return true if the current task should be stopped, - * false otherwise - */ - public final static synchronized boolean taskCanceled() { - if (canceled) - printInfo(Module.GUI, "Abort request noticed by the current task"); - return canceled; - } -} \ No newline at end of file diff --git a/src/org/sunflow/system/UserInterface.java b/src/org/sunflow/system/UserInterface.java deleted file mode 100644 index 9863b31..0000000 --- a/src/org/sunflow/system/UserInterface.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.sunflow.system; - -import org.sunflow.system.UI.Module; -import org.sunflow.system.UI.PrintLevel; - -public interface UserInterface { - /** - * Displays some information to the user from the specified module with the - * specified print level. A user interface is free to show or ignore any - * message. Level filtering is done in the core and shouldn't be - * re-implemented by the user interface. All messages will be short enough - * to fit on one line. - * - * @param m module the message came from - * @param level seriousness of the message - * @param s string to display - */ - void print(Module m, PrintLevel level, String s); - - /** - * Prepare a progress bar representing a lengthy task. The actual progress - * is first shown by the call to update and closed when update is closed - * with the max value. It is currently not possible to nest calls to - * setTask, so only one task needs to be tracked at a time. - * - * @param s desriptive string - * @param min minimum value of the task - * @param max maximum value of the task - */ - void taskStart(String s, int min, int max); - - /** - * Updates the current progress bar to a value between the current min and - * max. When min or max are passed the progressed bar is shown or hidden - * respectively. - * - * @param current current value of the task in progress. - */ - void taskUpdate(int current); - - /** - * Closes the current progress bar to indicate the task is over - */ - void taskStop(); -} \ No newline at end of file diff --git a/src/org/sunflow/system/ui/ConsoleInterface.java b/src/org/sunflow/system/ui/ConsoleInterface.java deleted file mode 100644 index 5d85595..0000000 --- a/src/org/sunflow/system/ui/ConsoleInterface.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.sunflow.system.ui; - -import org.sunflow.system.UI; -import org.sunflow.system.UserInterface; -import org.sunflow.system.UI.Module; -import org.sunflow.system.UI.PrintLevel; - -/** - * Basic console implementation of a user interface. - */ -public class ConsoleInterface implements UserInterface { - private int min; - private int max; - private float invP; - private String task; - private int lastP; - - public ConsoleInterface() { - } - - public void print(Module m, PrintLevel level, String s) { - System.err.println(UI.formatOutput(m, level, s)); - } - - public void taskStart(String s, int min, int max) { - task = s; - this.min = min; - this.max = max; - lastP = -1; - invP = 100.0f / (max - min); - } - - public void taskUpdate(int current) { - int p = (min == max) ? 0 : (int) ((current - min) * invP); - if (p != lastP) - System.err.print(task + " [" + (lastP = p) + "%]\r"); - } - - public void taskStop() { - System.err.print(" \r"); - } -} \ No newline at end of file diff --git a/src/org/sunflow/system/ui/SilentInterface.java b/src/org/sunflow/system/ui/SilentInterface.java deleted file mode 100644 index 556d509..0000000 --- a/src/org/sunflow/system/ui/SilentInterface.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.sunflow.system.ui; - -import org.sunflow.system.UserInterface; -import org.sunflow.system.UI.Module; -import org.sunflow.system.UI.PrintLevel; - -/** - * Null implementation of a user interface. This is usefull to silence the - * output. - */ -public class SilentInterface implements UserInterface { - public void print(Module m, PrintLevel level, String s) { - } - - public void taskStart(String s, int min, int max) { - } - - public void taskUpdate(int current) { - } - - public void taskStop() { - } -} \ No newline at end of file diff --git a/src/org/sunflow/util/FastHashMap.java b/src/org/sunflow/util/FastHashMap.java deleted file mode 100644 index 61e89a9..0000000 --- a/src/org/sunflow/util/FastHashMap.java +++ /dev/null @@ -1,206 +0,0 @@ -package org.sunflow.util; - -import java.util.Iterator; - -/** - * Fast hash map implementation which uses array storage along with quadratic - * probing to resolve collisions. The capacity is doubled when the load goes - * beyond 50% and is halved when the load drops below 20%. - * - * @param - * @param - */ -public class FastHashMap implements Iterable> { - private static final int MIN_SIZE = 4; - - public static class Entry { - private final K k; - private V v; - - private Entry(K k, V v) { - this.k = k; - this.v = v; - } - - private boolean isRemoved() { - return v == null; - } - - private void remove() { - v = null; - } - - public K getKey() { - return k; - } - - public V getValue() { - return v; - } - } - - private Entry[] entries; - private int size; - - public FastHashMap() { - clear(); - } - - public void clear() { - size = 0; - entries = alloc(MIN_SIZE); - } - - public V put(K k, V v) { - int hash = k.hashCode(), t = 0; - int pos = entries.length; // mark invalid position - for (;;) { - hash &= entries.length - 1; - if (entries[hash] == null) - break; // done probing - else if (entries[hash].isRemoved() && pos == entries.length) - pos = hash; // store, but keep searching - else if (entries[hash].k.equals(k)) { - // update entry - V old = entries[hash].v; - entries[hash].v = v; - return old; - } - t++; - hash += t; - } - // did we find a spot for insertion among the deleted values ? - if (pos < entries.length) - hash = pos; - entries[hash] = new Entry(k, v); - size++; - if (size * 2 > entries.length) - resize(entries.length * 2); - return null; - } - - public V get(K k) { - int hash = k.hashCode(), t = 0; - for (;;) { - hash &= entries.length - 1; - if (entries[hash] == null) - return null; - else if (!entries[hash].isRemoved() && entries[hash].k.equals(k)) - return entries[hash].v; - t++; - hash += t; - } - } - - public boolean containsKey(K k) { - int hash = k.hashCode(), t = 0; - for (;;) { - hash &= entries.length - 1; - if (entries[hash] == null) - return false; - else if (!entries[hash].isRemoved() && entries[hash].k.equals(k)) - return true; - t++; - hash += t; - } - } - - public void remove(K k) { - int hash = k.hashCode(), t = 0; - for (;;) { - hash &= entries.length - 1; - if (entries[hash] == null) - return; // not found, return - else if (!entries[hash].isRemoved() && entries[hash].k.equals(k)) { - entries[hash].remove(); // flag as removed - size--; - break; - } - t++; - hash += t; - } - // do we need to shrink? - if (entries.length > MIN_SIZE && size * 10 < 2 * entries.length) - resize(entries.length / 2); - } - - /** - * Resize internal storage to the specified capacity. The capacity must be a - * power of two. - * - * @param capacity new capacity for the internal array - */ - private void resize(int capacity) { - assert (capacity & (capacity - 1)) == 0; - assert capacity >= MIN_SIZE; - Entry[] newentries = alloc(capacity); - for (Entry e : entries) { - if (e == null || e.isRemoved()) - continue; - int hash = e.k.hashCode(), t = 0; - for (;;) { - hash &= newentries.length - 1; - if (newentries[hash] == null) - break; - assert !newentries[hash].k.equals(e.k); - t++; - hash += t; - } - newentries[hash] = new Entry(e.k, e.v); - } - // copy new entries over old ones - entries = newentries; - } - - /** - * Wrap the entry array allocation because it requires silencing some - * generics warnings. - * - * @param size number of elements to allocate - * @return - */ - @SuppressWarnings("unchecked") - private Entry[] alloc(int size) { - return new Entry[size]; - } - - private class EntryIterator implements Iterator> { - private int index; - - private EntryIterator() { - index = 0; - if (!readable()) - inc(); - } - - private boolean readable() { - return !(entries[index] == null || entries[index].isRemoved()); - } - - private void inc() { - do { - index++; - } while (hasNext() && !readable()); - } - - public boolean hasNext() { - return index < entries.length; - } - - public Entry next() { - try { - return entries[index]; - } finally { - inc(); - } - } - - public void remove() { - throw new UnsupportedOperationException(); - } - } - - public Iterator> iterator() { - return new EntryIterator(); - } -} \ No newline at end of file diff --git a/src/org/sunflow/util/FloatArray.java b/src/org/sunflow/util/FloatArray.java deleted file mode 100644 index d028486..0000000 --- a/src/org/sunflow/util/FloatArray.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.sunflow.util; - -public final class FloatArray { - private float[] array; - private int size; - - public FloatArray() { - array = new float[10]; - size = 0; - } - - public FloatArray(int capacity) { - array = new float[capacity]; - size = 0; - } - - /** - * Append a float to the end of the array. - * - * @param f - */ - public final void add(float f) { - if (size == array.length) { - float[] oldArray = array; - array = new float[(size * 3) / 2 + 1]; - System.arraycopy(oldArray, 0, array, 0, size); - } - array[size] = f; - size++; - } - - /** - * Write a value to the specified index. Assumes the array is already big - * enough. - * - * @param index - * @param value - */ - public final void set(int index, float value) { - array[index] = value; - } - - /** - * Read value from the array. - * - * @param index index into the array - * @return value at the specified index - */ - public final float get(int index) { - return array[index]; - } - - /** - * Returns the number of elements added to the array. - * - * @return current size of the array - */ - public final int getSize() { - return size; - } - - /** - * Return a copy of the array, trimmed to fit the size of its contents - * exactly. - * - * @return a new array of exactly the right length - */ - public final float[] trim() { - if (size < array.length) { - float[] oldArray = array; - array = new float[size]; - System.arraycopy(oldArray, 0, array, 0, size); - } - return array; - } -} \ No newline at end of file diff --git a/src/org/sunflow/util/IntArray.java b/src/org/sunflow/util/IntArray.java deleted file mode 100644 index b66406a..0000000 --- a/src/org/sunflow/util/IntArray.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.sunflow.util; - -public final class IntArray { - private int[] array; - private int size; - - public IntArray() { - array = new int[10]; - size = 0; - } - - public IntArray(int capacity) { - array = new int[capacity]; - size = 0; - } - - /** - * Append an integer to the end of the array. - * - * @param i - */ - public final void add(int i) { - if (size == array.length) { - int[] oldArray = array; - array = new int[(size * 3) / 2 + 1]; - System.arraycopy(oldArray, 0, array, 0, size); - } - array[size] = i; - size++; - } - - /** - * Write a value to the specified index. Assumes the array is already big - * enough. - * - * @param index - * @param value - */ - public final void set(int index, int value) { - array[index] = value; - } - - /** - * Read value from the array. - * - * @param index index into the array - * @return value at the specified index - */ - public final int get(int index) { - return array[index]; - } - - /** - * Returns the number of elements added to the array. - * - * @return current size of the array - */ - public final int getSize() { - return size; - } - - /** - * Return a copy of the array, trimmed to fit the size of its contents - * exactly. - * - * @return a new array of exactly the right length - */ - public final int[] trim() { - if (size < array.length) { - int[] oldArray = array; - array = new int[size]; - System.arraycopy(oldArray, 0, array, 0, size); - } - return array; - } -} \ No newline at end of file From f0bbd3854502a723852f238ba14bdb3e65244149 Mon Sep 17 00:00:00 2001 From: Karel Petranek Date: Mon, 5 Sep 2016 20:56:06 -0500 Subject: [PATCH 02/37] Add missing files --- .gitignore | 3 + pom.xml | 52 + src/main/java/SunflowGUI.java | 1252 ++++++++++++++++ .../java/org/sunflow/AsciiFileSunflowAPI.java | 93 ++ src/main/java/org/sunflow/Benchmark.java | 280 ++++ .../org/sunflow/BinaryFileSunflowAPI.java | 225 +++ src/main/java/org/sunflow/FileSunflowAPI.java | 315 ++++ src/main/java/org/sunflow/PluginRegistry.java | 322 +++++ .../java/org/sunflow/RealtimeBenchmark.java | 123 ++ .../java/org/sunflow/RenderObjectMap.java | 333 +++++ src/main/java/org/sunflow/SunflowAPI.java | 700 +++++++++ .../java/org/sunflow/SunflowAPIInterface.java | 275 ++++ .../sunflow/core/AccelerationStructure.java | 19 + .../core/AccelerationStructureFactory.java | 33 + .../java/org/sunflow/core/BucketOrder.java | 20 + src/main/java/org/sunflow/core/Camera.java | 120 ++ .../java/org/sunflow/core/CameraLens.java | 28 + .../core/CausticPhotonMapInterface.java | 14 + src/main/java/org/sunflow/core/Display.java | 77 + src/main/java/org/sunflow/core/Filter.java | 26 + src/main/java/org/sunflow/core/GIEngine.java | 41 + src/main/java/org/sunflow/core/Geometry.java | 144 ++ .../core/GlobalPhotonMapInterface.java | 21 + .../java/org/sunflow/core/ImageSampler.java | 25 + src/main/java/org/sunflow/core/Instance.java | 213 +++ .../java/org/sunflow/core/InstanceList.java | 71 + .../org/sunflow/core/IntersectionState.java | 116 ++ .../java/org/sunflow/core/LightSample.java | 103 ++ .../java/org/sunflow/core/LightServer.java | 360 +++++ .../java/org/sunflow/core/LightSource.java | 66 + src/main/java/org/sunflow/core/Modifier.java | 16 + src/main/java/org/sunflow/core/Options.java | 18 + .../java/org/sunflow/core/ParameterList.java | 729 ++++++++++ .../java/org/sunflow/core/PhotonStore.java | 62 + .../java/org/sunflow/core/PrimitiveList.java | 69 + src/main/java/org/sunflow/core/Ray.java | 217 +++ .../java/org/sunflow/core/RenderObject.java | 24 + src/main/java/org/sunflow/core/Scene.java | 371 +++++ .../java/org/sunflow/core/SceneParser.java | 20 + src/main/java/org/sunflow/core/Shader.java | 29 + .../java/org/sunflow/core/ShadingCache.java | 75 + .../java/org/sunflow/core/ShadingState.java | 929 ++++++++++++ .../java/org/sunflow/core/Statistics.java | 83 ++ .../java/org/sunflow/core/Tesselatable.java | 35 + src/main/java/org/sunflow/core/Texture.java | 123 ++ .../java/org/sunflow/core/TextureCache.java | 47 + .../core/accel/BoundingIntervalHierarchy.java | 613 ++++++++ .../java/org/sunflow/core/accel/KDTree.java | 800 ++++++++++ .../sunflow/core/accel/NullAccelerator.java | 26 + .../org/sunflow/core/accel/UniformGrid.java | 294 ++++ .../core/bucket/BucketOrderFactory.java | 25 + .../core/bucket/ColumnBucketOrder.java | 18 + .../core/bucket/DiagonalBucketOrder.java | 26 + .../core/bucket/HilbertBucketOrder.java | 62 + .../core/bucket/InvertedBucketOrder.java | 26 + .../core/bucket/RandomBucketOrder.java | 46 + .../sunflow/core/bucket/RowBucketOrder.java | 18 + .../core/bucket/SpiralBucketOrder.java | 41 + .../org/sunflow/core/camera/FisheyeLens.java | 21 + .../org/sunflow/core/camera/PinholeLens.java | 40 + .../sunflow/core/camera/SphericalLens.java | 19 + .../org/sunflow/core/camera/ThinLens.java | 103 ++ .../org/sunflow/core/display/FastDisplay.java | 107 ++ .../org/sunflow/core/display/FileDisplay.java | 73 + .../sunflow/core/display/FrameDisplay.java | 85 ++ .../sunflow/core/display/ImgPipeDisplay.java | 101 ++ .../core/filter/BlackmanHarrisFilter.java | 24 + .../org/sunflow/core/filter/BoxFilter.java | 13 + .../sunflow/core/filter/CatmullRomFilter.java | 24 + .../org/sunflow/core/filter/CubicBSpline.java | 28 + .../sunflow/core/filter/GaussianFilter.java | 21 + .../sunflow/core/filter/LanczosFilter.java | 26 + .../sunflow/core/filter/MitchellFilter.java | 24 + .../org/sunflow/core/filter/SincFilter.java | 21 + .../sunflow/core/filter/TriangleFilter.java | 13 + .../core/gi/AmbientOcclusionGIEngine.java | 53 + .../org/sunflow/core/gi/FakeGIEngine.java | 43 + .../java/org/sunflow/core/gi/InstantGI.java | 176 +++ .../core/gi/IrradianceCacheGIEngine.java | 246 ++++ .../sunflow/core/gi/PathTracingGIEngine.java | 59 + .../core/light/DirectionalSpotlight.java | 96 ++ .../sunflow/core/light/ImageBasedLight.java | 275 ++++ .../org/sunflow/core/light/PointLight.java | 65 + .../org/sunflow/core/light/SphereLight.java | 153 ++ .../org/sunflow/core/light/SunSkyLight.java | 337 +++++ .../sunflow/core/light/TriangleMeshLight.java | 272 ++++ .../core/modifiers/BumpMappingModifier.java | 33 + .../core/modifiers/NormalMapModifier.java | 30 + .../core/modifiers/PerlinModifier.java | 76 + .../org/sunflow/core/parser/RA2Parser.java | 100 ++ .../org/sunflow/core/parser/RA3Parser.java | 61 + .../sunflow/core/parser/SCAbstractParser.java | 294 ++++ .../sunflow/core/parser/SCAsciiParser.java | 213 +++ .../sunflow/core/parser/SCBinaryParser.java | 154 ++ .../org/sunflow/core/parser/SCParser.java | 1284 +++++++++++++++++ .../sunflow/core/parser/ShaveRibParser.java | 165 +++ .../org/sunflow/core/parser/TriParser.java | 73 + .../core/photonmap/CausticPhotonMap.java | 401 +++++ .../core/photonmap/GlobalPhotonMap.java | 498 +++++++ .../sunflow/core/photonmap/GridPhotonMap.java | 282 ++++ .../sunflow/core/primitive/Background.java | 45 + .../core/primitive/BanchoffSurface.java | 90 ++ .../java/org/sunflow/core/primitive/Box.java | 197 +++ .../sunflow/core/primitive/CornellBox.java | 446 ++++++ .../org/sunflow/core/primitive/CubeGrid.java | 288 ++++ .../org/sunflow/core/primitive/Cylinder.java | 93 ++ .../java/org/sunflow/core/primitive/Hair.java | 261 ++++ .../sunflow/core/primitive/JuliaFractal.java | 253 ++++ .../core/primitive/ParticleSurface.java | 102 ++ .../org/sunflow/core/primitive/Plane.java | 153 ++ .../org/sunflow/core/primitive/QuadMesh.java | 392 +++++ .../org/sunflow/core/primitive/Sphere.java | 89 ++ .../sunflow/core/primitive/SphereFlake.java | 219 +++ .../org/sunflow/core/primitive/Torus.java | 134 ++ .../sunflow/core/primitive/TriangleMesh.java | 784 ++++++++++ .../sunflow/core/renderer/BucketRenderer.java | 469 ++++++ .../core/renderer/MultipassRenderer.java | 226 +++ .../core/renderer/ProgressiveRenderer.java | 158 ++ .../sunflow/core/renderer/SimpleRenderer.java | 101 ++ .../core/shader/AmbientOcclusionShader.java | 48 + .../core/shader/AnisotropicWardShader.java | 211 +++ .../sunflow/core/shader/ConstantShader.java | 27 + .../sunflow/core/shader/DiffuseShader.java | 61 + .../org/sunflow/core/shader/GlassShader.java | 139 ++ .../org/sunflow/core/shader/IDShader.java | 23 + .../org/sunflow/core/shader/MirrorShader.java | 62 + .../org/sunflow/core/shader/NormalShader.java | 27 + .../org/sunflow/core/shader/PhongShader.java | 85 ++ .../org/sunflow/core/shader/PrimIDShader.java | 26 + .../sunflow/core/shader/QuickGrayShader.java | 59 + .../core/shader/ShinyDiffuseShader.java | 93 ++ .../org/sunflow/core/shader/SimpleShader.java | 20 + .../TexturedAmbientOcclusionShader.java | 29 + .../core/shader/TexturedDiffuseShader.java | 29 + .../core/shader/TexturedPhongShader.java | 29 + .../shader/TexturedShinyDiffuseShader.java | 29 + .../core/shader/TexturedWardShader.java | 29 + .../org/sunflow/core/shader/UVShader.java | 22 + .../org/sunflow/core/shader/UberShader.java | 141 ++ .../core/shader/ViewCausticsShader.java | 28 + .../core/shader/ViewGlobalPhotonsShader.java | 21 + .../core/shader/ViewIrradianceShader.java | 21 + .../sunflow/core/shader/WireframeShader.java | 76 + .../sunflow/core/tesselatable/BezierMesh.java | 248 ++++ .../sunflow/core/tesselatable/FileMesh.java | 237 +++ .../org/sunflow/core/tesselatable/Gumbo.java | 1144 +++++++++++++++ .../org/sunflow/core/tesselatable/Teapot.java | 236 +++ src/main/java/org/sunflow/image/Bitmap.java | 14 + .../java/org/sunflow/image/BitmapReader.java | 35 + .../java/org/sunflow/image/BitmapWriter.java | 78 + .../org/sunflow/image/BlackbodySpectrum.java | 15 + .../sunflow/image/ChromaticitySpectrum.java | 59 + src/main/java/org/sunflow/image/Color.java | 370 +++++ .../java/org/sunflow/image/ColorEncoder.java | 90 ++ .../java/org/sunflow/image/ColorFactory.java | 112 ++ .../sunflow/image/ConstantSpectralCurve.java | 20 + .../sunflow/image/IrregularSpectralCurve.java | 50 + src/main/java/org/sunflow/image/RGBSpace.java | 199 +++ .../sunflow/image/RegularSpectralCurve.java | 28 + .../java/org/sunflow/image/SpectralCurve.java | 119 ++ src/main/java/org/sunflow/image/XYZColor.java | 48 + .../sunflow/image/formats/BitmapBlack.java | 26 + .../org/sunflow/image/formats/BitmapG8.java | 35 + .../org/sunflow/image/formats/BitmapGA8.java | 29 + .../org/sunflow/image/formats/BitmapRGB8.java | 39 + .../sunflow/image/formats/BitmapRGBA8.java | 39 + .../org/sunflow/image/formats/BitmapRGBE.java | 56 + .../org/sunflow/image/formats/BitmapXYZ.java | 37 + .../sunflow/image/formats/GenericBitmap.java | 71 + .../image/readers/BMPBitmapReader.java | 38 + .../image/readers/HDRBitmapReader.java | 148 ++ .../image/readers/IGIBitmapReader.java | 94 ++ .../image/readers/JPGBitmapReader.java | 38 + .../image/readers/PNGBitmapReader.java | 39 + .../image/readers/TGABitmapReader.java | 131 ++ .../image/writers/EXRBitmapWriter.java | 377 +++++ .../image/writers/HDRBitmapWriter.java | 51 + .../image/writers/IGIBitmapWriter.java | 73 + .../image/writers/PNGBitmapWriter.java | 36 + .../image/writers/TGABitmapWriter.java | 62 + .../java/org/sunflow/math/BoundingBox.java | 315 ++++ src/main/java/org/sunflow/math/MathUtils.java | 131 ++ src/main/java/org/sunflow/math/Matrix4.java | 561 +++++++ .../java/org/sunflow/math/MovingMatrix4.java | 115 ++ .../org/sunflow/math/OrthoNormalBasis.java | 109 ++ .../java/org/sunflow/math/PerlinScalar.java | 330 +++++ .../java/org/sunflow/math/PerlinVector.java | 131 ++ src/main/java/org/sunflow/math/Point2.java | 35 + src/main/java/org/sunflow/math/Point3.java | 132 ++ src/main/java/org/sunflow/math/QMC.java | 194 +++ src/main/java/org/sunflow/math/Solvers.java | 137 ++ src/main/java/org/sunflow/math/Vector3.java | 195 +++ .../sunflow/system/BenchmarkFramework.java | 67 + .../org/sunflow/system/BenchmarkTest.java | 17 + .../java/org/sunflow/system/ByteUtil.java | 118 ++ .../java/org/sunflow/system/FileUtils.java | 24 + .../java/org/sunflow/system/ImagePanel.java | 262 ++++ src/main/java/org/sunflow/system/Memory.java | 15 + src/main/java/org/sunflow/system/Parser.java | 155 ++ src/main/java/org/sunflow/system/Plugins.java | 153 ++ .../sunflow/system/RenderGlobalsPanel.java | 207 +++ .../java/org/sunflow/system/SearchPath.java | 61 + src/main/java/org/sunflow/system/Timer.java | 51 + src/main/java/org/sunflow/system/UI.java | 103 ++ .../org/sunflow/system/UserInterface.java | 45 + .../sunflow/system/ui/ConsoleInterface.java | 42 + .../sunflow/system/ui/SilentInterface.java | 23 + .../java/org/sunflow/util/FastHashMap.java | 206 +++ .../java/org/sunflow/util/FloatArray.java | 76 + src/main/java/org/sunflow/util/IntArray.java | 76 + 210 files changed, 30936 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/SunflowGUI.java create mode 100644 src/main/java/org/sunflow/AsciiFileSunflowAPI.java create mode 100644 src/main/java/org/sunflow/Benchmark.java create mode 100644 src/main/java/org/sunflow/BinaryFileSunflowAPI.java create mode 100644 src/main/java/org/sunflow/FileSunflowAPI.java create mode 100644 src/main/java/org/sunflow/PluginRegistry.java create mode 100644 src/main/java/org/sunflow/RealtimeBenchmark.java create mode 100644 src/main/java/org/sunflow/RenderObjectMap.java create mode 100644 src/main/java/org/sunflow/SunflowAPI.java create mode 100644 src/main/java/org/sunflow/SunflowAPIInterface.java create mode 100644 src/main/java/org/sunflow/core/AccelerationStructure.java create mode 100644 src/main/java/org/sunflow/core/AccelerationStructureFactory.java create mode 100644 src/main/java/org/sunflow/core/BucketOrder.java create mode 100644 src/main/java/org/sunflow/core/Camera.java create mode 100644 src/main/java/org/sunflow/core/CameraLens.java create mode 100644 src/main/java/org/sunflow/core/CausticPhotonMapInterface.java create mode 100644 src/main/java/org/sunflow/core/Display.java create mode 100644 src/main/java/org/sunflow/core/Filter.java create mode 100644 src/main/java/org/sunflow/core/GIEngine.java create mode 100644 src/main/java/org/sunflow/core/Geometry.java create mode 100644 src/main/java/org/sunflow/core/GlobalPhotonMapInterface.java create mode 100644 src/main/java/org/sunflow/core/ImageSampler.java create mode 100644 src/main/java/org/sunflow/core/Instance.java create mode 100644 src/main/java/org/sunflow/core/InstanceList.java create mode 100644 src/main/java/org/sunflow/core/IntersectionState.java create mode 100644 src/main/java/org/sunflow/core/LightSample.java create mode 100644 src/main/java/org/sunflow/core/LightServer.java create mode 100644 src/main/java/org/sunflow/core/LightSource.java create mode 100644 src/main/java/org/sunflow/core/Modifier.java create mode 100644 src/main/java/org/sunflow/core/Options.java create mode 100644 src/main/java/org/sunflow/core/ParameterList.java create mode 100644 src/main/java/org/sunflow/core/PhotonStore.java create mode 100644 src/main/java/org/sunflow/core/PrimitiveList.java create mode 100644 src/main/java/org/sunflow/core/Ray.java create mode 100644 src/main/java/org/sunflow/core/RenderObject.java create mode 100644 src/main/java/org/sunflow/core/Scene.java create mode 100644 src/main/java/org/sunflow/core/SceneParser.java create mode 100644 src/main/java/org/sunflow/core/Shader.java create mode 100644 src/main/java/org/sunflow/core/ShadingCache.java create mode 100644 src/main/java/org/sunflow/core/ShadingState.java create mode 100644 src/main/java/org/sunflow/core/Statistics.java create mode 100644 src/main/java/org/sunflow/core/Tesselatable.java create mode 100644 src/main/java/org/sunflow/core/Texture.java create mode 100644 src/main/java/org/sunflow/core/TextureCache.java create mode 100644 src/main/java/org/sunflow/core/accel/BoundingIntervalHierarchy.java create mode 100644 src/main/java/org/sunflow/core/accel/KDTree.java create mode 100644 src/main/java/org/sunflow/core/accel/NullAccelerator.java create mode 100644 src/main/java/org/sunflow/core/accel/UniformGrid.java create mode 100644 src/main/java/org/sunflow/core/bucket/BucketOrderFactory.java create mode 100644 src/main/java/org/sunflow/core/bucket/ColumnBucketOrder.java create mode 100644 src/main/java/org/sunflow/core/bucket/DiagonalBucketOrder.java create mode 100644 src/main/java/org/sunflow/core/bucket/HilbertBucketOrder.java create mode 100644 src/main/java/org/sunflow/core/bucket/InvertedBucketOrder.java create mode 100644 src/main/java/org/sunflow/core/bucket/RandomBucketOrder.java create mode 100644 src/main/java/org/sunflow/core/bucket/RowBucketOrder.java create mode 100644 src/main/java/org/sunflow/core/bucket/SpiralBucketOrder.java create mode 100644 src/main/java/org/sunflow/core/camera/FisheyeLens.java create mode 100644 src/main/java/org/sunflow/core/camera/PinholeLens.java create mode 100644 src/main/java/org/sunflow/core/camera/SphericalLens.java create mode 100644 src/main/java/org/sunflow/core/camera/ThinLens.java create mode 100644 src/main/java/org/sunflow/core/display/FastDisplay.java create mode 100644 src/main/java/org/sunflow/core/display/FileDisplay.java create mode 100644 src/main/java/org/sunflow/core/display/FrameDisplay.java create mode 100644 src/main/java/org/sunflow/core/display/ImgPipeDisplay.java create mode 100644 src/main/java/org/sunflow/core/filter/BlackmanHarrisFilter.java create mode 100644 src/main/java/org/sunflow/core/filter/BoxFilter.java create mode 100644 src/main/java/org/sunflow/core/filter/CatmullRomFilter.java create mode 100644 src/main/java/org/sunflow/core/filter/CubicBSpline.java create mode 100644 src/main/java/org/sunflow/core/filter/GaussianFilter.java create mode 100644 src/main/java/org/sunflow/core/filter/LanczosFilter.java create mode 100644 src/main/java/org/sunflow/core/filter/MitchellFilter.java create mode 100644 src/main/java/org/sunflow/core/filter/SincFilter.java create mode 100644 src/main/java/org/sunflow/core/filter/TriangleFilter.java create mode 100644 src/main/java/org/sunflow/core/gi/AmbientOcclusionGIEngine.java create mode 100644 src/main/java/org/sunflow/core/gi/FakeGIEngine.java create mode 100644 src/main/java/org/sunflow/core/gi/InstantGI.java create mode 100644 src/main/java/org/sunflow/core/gi/IrradianceCacheGIEngine.java create mode 100644 src/main/java/org/sunflow/core/gi/PathTracingGIEngine.java create mode 100644 src/main/java/org/sunflow/core/light/DirectionalSpotlight.java create mode 100644 src/main/java/org/sunflow/core/light/ImageBasedLight.java create mode 100644 src/main/java/org/sunflow/core/light/PointLight.java create mode 100644 src/main/java/org/sunflow/core/light/SphereLight.java create mode 100644 src/main/java/org/sunflow/core/light/SunSkyLight.java create mode 100644 src/main/java/org/sunflow/core/light/TriangleMeshLight.java create mode 100644 src/main/java/org/sunflow/core/modifiers/BumpMappingModifier.java create mode 100644 src/main/java/org/sunflow/core/modifiers/NormalMapModifier.java create mode 100644 src/main/java/org/sunflow/core/modifiers/PerlinModifier.java create mode 100644 src/main/java/org/sunflow/core/parser/RA2Parser.java create mode 100644 src/main/java/org/sunflow/core/parser/RA3Parser.java create mode 100644 src/main/java/org/sunflow/core/parser/SCAbstractParser.java create mode 100644 src/main/java/org/sunflow/core/parser/SCAsciiParser.java create mode 100644 src/main/java/org/sunflow/core/parser/SCBinaryParser.java create mode 100644 src/main/java/org/sunflow/core/parser/SCParser.java create mode 100644 src/main/java/org/sunflow/core/parser/ShaveRibParser.java create mode 100644 src/main/java/org/sunflow/core/parser/TriParser.java create mode 100644 src/main/java/org/sunflow/core/photonmap/CausticPhotonMap.java create mode 100644 src/main/java/org/sunflow/core/photonmap/GlobalPhotonMap.java create mode 100644 src/main/java/org/sunflow/core/photonmap/GridPhotonMap.java create mode 100644 src/main/java/org/sunflow/core/primitive/Background.java create mode 100644 src/main/java/org/sunflow/core/primitive/BanchoffSurface.java create mode 100644 src/main/java/org/sunflow/core/primitive/Box.java create mode 100644 src/main/java/org/sunflow/core/primitive/CornellBox.java create mode 100644 src/main/java/org/sunflow/core/primitive/CubeGrid.java create mode 100644 src/main/java/org/sunflow/core/primitive/Cylinder.java create mode 100644 src/main/java/org/sunflow/core/primitive/Hair.java create mode 100644 src/main/java/org/sunflow/core/primitive/JuliaFractal.java create mode 100644 src/main/java/org/sunflow/core/primitive/ParticleSurface.java create mode 100644 src/main/java/org/sunflow/core/primitive/Plane.java create mode 100644 src/main/java/org/sunflow/core/primitive/QuadMesh.java create mode 100644 src/main/java/org/sunflow/core/primitive/Sphere.java create mode 100644 src/main/java/org/sunflow/core/primitive/SphereFlake.java create mode 100644 src/main/java/org/sunflow/core/primitive/Torus.java create mode 100644 src/main/java/org/sunflow/core/primitive/TriangleMesh.java create mode 100644 src/main/java/org/sunflow/core/renderer/BucketRenderer.java create mode 100644 src/main/java/org/sunflow/core/renderer/MultipassRenderer.java create mode 100644 src/main/java/org/sunflow/core/renderer/ProgressiveRenderer.java create mode 100644 src/main/java/org/sunflow/core/renderer/SimpleRenderer.java create mode 100644 src/main/java/org/sunflow/core/shader/AmbientOcclusionShader.java create mode 100644 src/main/java/org/sunflow/core/shader/AnisotropicWardShader.java create mode 100644 src/main/java/org/sunflow/core/shader/ConstantShader.java create mode 100644 src/main/java/org/sunflow/core/shader/DiffuseShader.java create mode 100644 src/main/java/org/sunflow/core/shader/GlassShader.java create mode 100644 src/main/java/org/sunflow/core/shader/IDShader.java create mode 100644 src/main/java/org/sunflow/core/shader/MirrorShader.java create mode 100644 src/main/java/org/sunflow/core/shader/NormalShader.java create mode 100644 src/main/java/org/sunflow/core/shader/PhongShader.java create mode 100644 src/main/java/org/sunflow/core/shader/PrimIDShader.java create mode 100644 src/main/java/org/sunflow/core/shader/QuickGrayShader.java create mode 100644 src/main/java/org/sunflow/core/shader/ShinyDiffuseShader.java create mode 100644 src/main/java/org/sunflow/core/shader/SimpleShader.java create mode 100644 src/main/java/org/sunflow/core/shader/TexturedAmbientOcclusionShader.java create mode 100644 src/main/java/org/sunflow/core/shader/TexturedDiffuseShader.java create mode 100644 src/main/java/org/sunflow/core/shader/TexturedPhongShader.java create mode 100644 src/main/java/org/sunflow/core/shader/TexturedShinyDiffuseShader.java create mode 100644 src/main/java/org/sunflow/core/shader/TexturedWardShader.java create mode 100644 src/main/java/org/sunflow/core/shader/UVShader.java create mode 100644 src/main/java/org/sunflow/core/shader/UberShader.java create mode 100644 src/main/java/org/sunflow/core/shader/ViewCausticsShader.java create mode 100644 src/main/java/org/sunflow/core/shader/ViewGlobalPhotonsShader.java create mode 100644 src/main/java/org/sunflow/core/shader/ViewIrradianceShader.java create mode 100644 src/main/java/org/sunflow/core/shader/WireframeShader.java create mode 100644 src/main/java/org/sunflow/core/tesselatable/BezierMesh.java create mode 100644 src/main/java/org/sunflow/core/tesselatable/FileMesh.java create mode 100644 src/main/java/org/sunflow/core/tesselatable/Gumbo.java create mode 100644 src/main/java/org/sunflow/core/tesselatable/Teapot.java create mode 100644 src/main/java/org/sunflow/image/Bitmap.java create mode 100644 src/main/java/org/sunflow/image/BitmapReader.java create mode 100644 src/main/java/org/sunflow/image/BitmapWriter.java create mode 100644 src/main/java/org/sunflow/image/BlackbodySpectrum.java create mode 100644 src/main/java/org/sunflow/image/ChromaticitySpectrum.java create mode 100644 src/main/java/org/sunflow/image/Color.java create mode 100644 src/main/java/org/sunflow/image/ColorEncoder.java create mode 100644 src/main/java/org/sunflow/image/ColorFactory.java create mode 100644 src/main/java/org/sunflow/image/ConstantSpectralCurve.java create mode 100644 src/main/java/org/sunflow/image/IrregularSpectralCurve.java create mode 100644 src/main/java/org/sunflow/image/RGBSpace.java create mode 100644 src/main/java/org/sunflow/image/RegularSpectralCurve.java create mode 100644 src/main/java/org/sunflow/image/SpectralCurve.java create mode 100644 src/main/java/org/sunflow/image/XYZColor.java create mode 100644 src/main/java/org/sunflow/image/formats/BitmapBlack.java create mode 100644 src/main/java/org/sunflow/image/formats/BitmapG8.java create mode 100644 src/main/java/org/sunflow/image/formats/BitmapGA8.java create mode 100644 src/main/java/org/sunflow/image/formats/BitmapRGB8.java create mode 100644 src/main/java/org/sunflow/image/formats/BitmapRGBA8.java create mode 100644 src/main/java/org/sunflow/image/formats/BitmapRGBE.java create mode 100644 src/main/java/org/sunflow/image/formats/BitmapXYZ.java create mode 100644 src/main/java/org/sunflow/image/formats/GenericBitmap.java create mode 100644 src/main/java/org/sunflow/image/readers/BMPBitmapReader.java create mode 100644 src/main/java/org/sunflow/image/readers/HDRBitmapReader.java create mode 100644 src/main/java/org/sunflow/image/readers/IGIBitmapReader.java create mode 100644 src/main/java/org/sunflow/image/readers/JPGBitmapReader.java create mode 100644 src/main/java/org/sunflow/image/readers/PNGBitmapReader.java create mode 100644 src/main/java/org/sunflow/image/readers/TGABitmapReader.java create mode 100644 src/main/java/org/sunflow/image/writers/EXRBitmapWriter.java create mode 100644 src/main/java/org/sunflow/image/writers/HDRBitmapWriter.java create mode 100644 src/main/java/org/sunflow/image/writers/IGIBitmapWriter.java create mode 100644 src/main/java/org/sunflow/image/writers/PNGBitmapWriter.java create mode 100644 src/main/java/org/sunflow/image/writers/TGABitmapWriter.java create mode 100644 src/main/java/org/sunflow/math/BoundingBox.java create mode 100644 src/main/java/org/sunflow/math/MathUtils.java create mode 100644 src/main/java/org/sunflow/math/Matrix4.java create mode 100644 src/main/java/org/sunflow/math/MovingMatrix4.java create mode 100644 src/main/java/org/sunflow/math/OrthoNormalBasis.java create mode 100644 src/main/java/org/sunflow/math/PerlinScalar.java create mode 100644 src/main/java/org/sunflow/math/PerlinVector.java create mode 100644 src/main/java/org/sunflow/math/Point2.java create mode 100644 src/main/java/org/sunflow/math/Point3.java create mode 100644 src/main/java/org/sunflow/math/QMC.java create mode 100644 src/main/java/org/sunflow/math/Solvers.java create mode 100644 src/main/java/org/sunflow/math/Vector3.java create mode 100644 src/main/java/org/sunflow/system/BenchmarkFramework.java create mode 100644 src/main/java/org/sunflow/system/BenchmarkTest.java create mode 100644 src/main/java/org/sunflow/system/ByteUtil.java create mode 100644 src/main/java/org/sunflow/system/FileUtils.java create mode 100644 src/main/java/org/sunflow/system/ImagePanel.java create mode 100644 src/main/java/org/sunflow/system/Memory.java create mode 100644 src/main/java/org/sunflow/system/Parser.java create mode 100644 src/main/java/org/sunflow/system/Plugins.java create mode 100644 src/main/java/org/sunflow/system/RenderGlobalsPanel.java create mode 100644 src/main/java/org/sunflow/system/SearchPath.java create mode 100644 src/main/java/org/sunflow/system/Timer.java create mode 100644 src/main/java/org/sunflow/system/UI.java create mode 100644 src/main/java/org/sunflow/system/UserInterface.java create mode 100644 src/main/java/org/sunflow/system/ui/ConsoleInterface.java create mode 100644 src/main/java/org/sunflow/system/ui/SilentInterface.java create mode 100644 src/main/java/org/sunflow/util/FastHashMap.java create mode 100644 src/main/java/org/sunflow/util/FloatArray.java create mode 100644 src/main/java/org/sunflow/util/IntArray.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a543a51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.iml +.idea +target/ \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0a50986 --- /dev/null +++ b/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + sunflow + net.sourceforge + 0.07.3-SNAPSHOT + http://sunflow.sourceforge.net + Sunflow Global Illumination Rendering System + + + + UTF-8 + + + + + + org.codehaus.janino + janino + 2.5.16 + + + + + + + maven-compiler-plugin + 3.3 + + 1.6 + 1.6 + UTF-8 + + + + org.apache.maven.plugins + maven-source-plugin + 2.4 + + + attach-sources + + jar + + + + + + + diff --git a/src/main/java/SunflowGUI.java b/src/main/java/SunflowGUI.java new file mode 100644 index 0000000..831fd3f --- /dev/null +++ b/src/main/java/SunflowGUI.java @@ -0,0 +1,1252 @@ +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Toolkit; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyVetoException; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JComponent; +import javax.swing.JDesktopPane; +import javax.swing.JFileChooser; +import javax.swing.JInternalFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JTextArea; +import javax.swing.KeyStroke; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingUtilities; +import javax.swing.TransferHandler; +import javax.swing.filechooser.FileFilter; +import javax.swing.plaf.metal.DefaultMetalTheme; +import javax.swing.plaf.metal.MetalLookAndFeel; + +import org.sunflow.Benchmark; +import org.sunflow.RealtimeBenchmark; +import org.sunflow.SunflowAPI; +import org.sunflow.core.Display; +import org.sunflow.core.TextureCache; +import org.sunflow.core.accel.KDTree; +import org.sunflow.core.display.FileDisplay; +import org.sunflow.core.display.FrameDisplay; +import org.sunflow.core.display.ImgPipeDisplay; +import org.sunflow.core.primitive.TriangleMesh; +import org.sunflow.system.ImagePanel; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UserInterface; +import org.sunflow.system.UI.Module; +import org.sunflow.system.UI.PrintLevel; + +@SuppressWarnings("serial") +public class SunflowGUI extends javax.swing.JFrame implements UserInterface { + private static final int DEFAULT_WIDTH = 1024; + private static final int DEFAULT_HEIGHT = 768; + private JPanel jPanel3; + private JScrollPane jScrollPane1; + private JMenuItem exitMenuItem; + private JSeparator jSeparator2; + private JPanel jPanel1; + private JButton renderButton; + private JMenuItem jMenuItem4; + private JSeparator jSeparator1; + private JMenuItem fitWindowMenuItem; + private JMenuItem tileWindowMenuItem; + private JSeparator jSeparator5; + private JMenuItem consoleWindowMenuItem; + private JMenuItem editorWindowMenuItem; + private JMenuItem imageWindowMenuItem; + private JMenu windowMenu; + private JInternalFrame consoleFrame; + private JInternalFrame editorFrame; + private JInternalFrame imagePanelFrame; + private JDesktopPane desktop; + private JCheckBoxMenuItem smallTrianglesMenuItem; + private JMenuItem textureCacheClearMenuItem; + private JSeparator jSeparator4; + private JMenuItem resetZoomMenuItem; + private JMenu imageMenu; + private ImagePanel imagePanel; + private JPanel jPanel6; + private JCheckBoxMenuItem clearLogMenuItem; + private JPanel jPanel5; + private JButton taskCancelButton; + private JProgressBar taskProgressBar; + private JSeparator jSeparator3; + private JCheckBoxMenuItem autoBuildMenuItem; + private JMenuItem iprMenuItem; + private JMenuItem renderMenuItem; + private JMenuItem buildMenuItem; + private JMenu sceneMenu; + private JTextArea editorTextArea; + private JTextArea consoleTextArea; + private JButton clearConsoleButton; + private JPanel jPanel4; + private JScrollPane jScrollPane2; + private JButton iprButton; + private JButton buildButton; + private JMenuItem saveAsMenuItem; + private JMenuItem saveMenuItem; + private JMenuItem openFileMenuItem; + private JMenuItem newFileMenuItem; + private JMenu fileMenu; + private JMenuBar jMenuBar1; + + // non-swing items + private String currentFile; + private String currentTask; + private int currentTaskLastP; + private SunflowAPI api; + private File lastSaveDirectory; + + public static void usage(boolean verbose) { + System.out.println("Usage: SunflowGUI [options] scenefile"); + if (verbose) { + System.out.println("Sunflow v" + SunflowAPI.VERSION + " textmode"); + System.out.println("Renders the specified scene file"); + System.out.println("Options:"); + System.out.println(" -o filename Saves the output as the specified filename (png, hdr, tga)"); + System.out.println(" #'s get expanded to the current frame number"); + System.out.println(" -nogui Don't open the frame showing rendering progress"); + System.out.println(" -ipr Render using progressive algorithm"); + System.out.println(" -sampler type Render using the specified algorithm"); + System.out.println(" -threads n Render using n threads"); + System.out.println(" -lopri Set thread priority to low (default)"); + System.out.println(" -hipri Set thread priority to high"); + System.out.println(" -smallmesh Load triangle meshes using triangles optimized for memory use"); + System.out.println(" -dumpkd Dump KDTree to an obj file for visualization"); + System.out.println(" -buildonly Do not call render method after loading the scene"); + System.out.println(" -showaa Display sampling levels per pixel for bucket renderer"); + System.out.println(" -nogi Disable any global illumination engines in the scene"); + System.out.println(" -nocaustics Disable any caustic engine in the scene"); + System.out.println(" -pathgi n Use path tracing with n samples to render global illumination"); + System.out.println(" -quick_ambocc d Applies ambient occlusion to the scene with specified maximum distance"); + System.out.println(" -quick_uvs Applies a surface uv visualization shader to the scene"); + System.out.println(" -quick_normals Applies a surface normal visualization shader to the scene"); + System.out.println(" -quick_id Renders using a unique color for each instance"); + System.out.println(" -quick_prims Renders using a unique color for each primitive"); + System.out.println(" -quick_gray Renders using a plain gray diffuse shader"); + System.out.println(" -quick_wire Renders using a wireframe shader"); + System.out.println(" -resolution w h Changes the render resolution to the specified width and height (in pixels)"); + System.out.println(" -aa min max Overrides the image anti-aliasing depths"); + System.out.println(" -samples n Overrides the image sample count (affects bucket and multipass samplers)"); + System.out.println(" -bucket n order Changes the default bucket size to n pixels and the default order"); + System.out.println(" -bake name Bakes a lightmap for the specified instance"); + System.out.println(" -bakedir dir Selects the type of lightmap baking: dir=view or ortho"); + System.out.println(" -filter type Selects the image filter to use"); + System.out.println(" -bench Run several built-in scenes for benchmark purposes"); + System.out.println(" -rtbench Run realtime ray-tracing benchmark"); + System.out.println(" -frame n Set frame number to the specified value"); + System.out.println(" -anim n1 n2 Render all frames between the two specified values (inclusive)"); + System.out.println(" -translate file Translate input scene to the specified filename"); + System.out.println(" -v verbosity Set the verbosity level: 0=none,1=errors,2=warnings,3=info,4=detailed"); + System.out.println(" -h Prints this message"); + } + System.exit(1); + } + + public static void main(String[] args) { + if (args.length > 0) { + boolean showFrame = true; + String sampler = null; + boolean noRender = false; + String filename = null; + String input = null; + int i = 0; + int threads = 0; + boolean lowPriority = true; + boolean showAA = false; + boolean noGI = false; + boolean noCaustics = false; + int pathGI = 0; + float maxDist = 0; + String shaderOverride = null; + int resolutionW = 0, resolutionH = 0; + int aaMin = -5, aaMax = -5; + int samples = -1; + int bucketSize = 0; + String bucketOrder = null; + String bakingName = null; + boolean bakeViewdep = false; + String filterType = null; + boolean runBenchmark = false; + boolean runRTBenchmark = false; + String translateFilename = null; + int frameStart = 1, frameStop = 1; + while (i < args.length) { + if (args[i].equals("-o")) { + if (i > args.length - 2) + usage(false); + filename = args[i + 1]; + i += 2; + } else if (args[i].equals("-nogui")) { + showFrame = false; + i++; + } else if (args[i].equals("-ipr")) { + sampler = "ipr"; + i++; + } else if (args[i].equals("-threads")) { + if (i > args.length - 2) + usage(false); + threads = Integer.parseInt(args[i + 1]); + i += 2; + } else if (args[i].equals("-lopri")) { + lowPriority = true; + i++; + } else if (args[i].equals("-hipri")) { + lowPriority = false; + i++; + } else if (args[i].equals("-sampler")) { + if (i > args.length - 2) + usage(false); + sampler = args[i + 1]; + i += 2; + } else if (args[i].equals("-smallmesh")) { + TriangleMesh.setSmallTriangles(true); + i++; + } else if (args[i].equals("-dumpkd")) { + KDTree.setDumpMode(true, "kdtree"); + i++; + } else if (args[i].equals("-buildonly")) { + noRender = true; + i++; + } else if (args[i].equals("-showaa")) { + showAA = true; + i++; + } else if (args[i].equals("-nogi")) { + noGI = true; + i++; + } else if (args[i].equals("-nocaustics")) { + noCaustics = true; + i++; + } else if (args[i].equals("-pathgi")) { + if (i > args.length - 2) + usage(false); + pathGI = Integer.parseInt(args[i + 1]); + i += 2; + } else if (args[i].equals("-quick_ambocc")) { + if (i > args.length - 2) + usage(false); + maxDist = Float.parseFloat(args[i + 1]); + shaderOverride = "ambient_occlusion"; // new + // AmbientOcclusionShader(Color.WHITE, + // d); + i += 2; + } else if (args[i].equals("-quick_uvs")) { + if (i > args.length - 1) + usage(false); + shaderOverride = "show_uvs"; + i++; + } else if (args[i].equals("-quick_normals")) { + if (i > args.length - 1) + usage(false); + shaderOverride = "show_normals"; + i++; + } else if (args[i].equals("-quick_id")) { + if (i > args.length - 1) + usage(false); + shaderOverride = "show_instance_id"; + i++; + } else if (args[i].equals("-quick_prims")) { + if (i > args.length - 1) + usage(false); + shaderOverride = "show_primitive_id"; + i++; + } else if (args[i].equals("-quick_gray")) { + if (i > args.length - 1) + usage(false); + shaderOverride = "quick_gray"; + i++; + } else if (args[i].equals("-quick_wire")) { + if (i > args.length - 1) + usage(false); + shaderOverride = "wireframe"; + i++; + } else if (args[i].equals("-resolution")) { + if (i > args.length - 3) + usage(false); + resolutionW = Integer.parseInt(args[i + 1]); + resolutionH = Integer.parseInt(args[i + 2]); + i += 3; + } else if (args[i].equals("-aa")) { + if (i > args.length - 3) + usage(false); + aaMin = Integer.parseInt(args[i + 1]); + aaMax = Integer.parseInt(args[i + 2]); + i += 3; + } else if (args[i].equals("-samples")) { + if (i > args.length - 2) + usage(false); + samples = Integer.parseInt(args[i+1]); + i += 2; + } else if (args[i].equals("-bucket")) { + if (i > args.length - 3) + usage(false); + bucketSize = Integer.parseInt(args[i + 1]); + bucketOrder = args[i + 2]; + i += 3; + } else if (args[i].equals("-bake")) { + if (i > args.length - 2) + usage(false); + bakingName = args[i + 1]; + i += 2; + } else if (args[i].equals("-bakedir")) { + if (i > args.length - 2) + usage(false); + String baketype = args[i + 1]; + if (baketype.equals("view")) + bakeViewdep = true; + else if (baketype.equals("ortho")) + bakeViewdep = false; + else + usage(false); + i += 2; + } else if (args[i].equals("-filter")) { + if (i > args.length - 2) + usage(false); + filterType = args[i + 1]; + i += 2; + } else if (args[i].equals("-bench")) { + runBenchmark = true; + i++; + } else if (args[i].equals("-rtbench")) { + runRTBenchmark = true; + i++; + } else if (args[i].equals("-frame")) { + if (i > args.length - 2) + usage(false); + frameStart = frameStop = Integer.parseInt(args[i + 1]); + i += 2; + } else if (args[i].equals("-anim")) { + if (i > args.length - 3) + usage(false); + frameStart = Integer.parseInt(args[i + 1]); + frameStop = Integer.parseInt(args[i + 2]); + i += 3; + } else if (args[i].equals("-v")) { + if (i > args.length - 2) + usage(false); + UI.verbosity(Integer.parseInt(args[i + 1])); + i += 2; + } else if (args[i].equals("-translate")) { + if (i > args.length - 2) + usage(false); + translateFilename = args[i + 1]; + i += 2; + } else if (args[i].equals("-h") || args[i].equals("-help")) { + usage(true); + } else { + if (input != null) + usage(false); + input = args[i]; + i++; + } + } + if (runBenchmark) { + SunflowAPI.runSystemCheck(); + new Benchmark().execute(); + return; + } + if (runRTBenchmark) { + SunflowAPI.runSystemCheck(); + new RealtimeBenchmark(showFrame, threads); + return; + } + if (input == null) + usage(false); + SunflowAPI.runSystemCheck(); + if (translateFilename != null) { + SunflowAPI.translate(input, translateFilename); + return; + } + if (frameStart < frameStop && showFrame) { + UI.printWarning(Module.GUI, "Animations should not be rendered without -nogui - forcing GUI off anyway"); + showFrame = false; + } + if (frameStart < frameStop && filename == null) { + filename = "output.#.png"; + UI.printWarning(Module.GUI, "Animation output was not specified - defaulting to: \"%s\"", filename); + } + for (int frameNumber = frameStart; frameNumber <= frameStop; frameNumber++) { + SunflowAPI api = SunflowAPI.create(input, frameNumber); + if (api == null) + continue; + if (noRender) + continue; + if (resolutionW > 0 && resolutionH > 0) { + api.parameter("resolutionX", resolutionW); + api.parameter("resolutionY", resolutionH); + } + if (aaMin != -5 || aaMax != -5) { + api.parameter("aa.min", aaMin); + api.parameter("aa.max", aaMax); + } + if (samples >= 0) + api.parameter("aa.samples", samples); + if (bucketSize > 0) + api.parameter("bucket.size", bucketSize); + if (bucketOrder != null) + api.parameter("bucket.order", bucketOrder); + api.parameter("aa.display", showAA); + api.parameter("threads", threads); + api.parameter("threads.lowPriority", lowPriority); + if (bakingName != null) { + api.parameter("baking.instance", bakingName); + api.parameter("baking.viewdep", bakeViewdep); + } + if (filterType != null) + api.parameter("filter", filterType); + if (noGI) + api.parameter("gi.engine", "none"); + else if (pathGI > 0) { + api.parameter("gi.engine", "path"); + api.parameter("gi.path.samples", pathGI); + } + if (noCaustics) + api.parameter("caustics", "none"); + if (sampler != null) + api.parameter("sampler", sampler); + api.options(SunflowAPI.DEFAULT_OPTIONS); + if (shaderOverride != null) { + if (shaderOverride.equals("ambient_occlusion")) + api.parameter("maxdist", maxDist); + api.shader("cmdline_override", shaderOverride); + api.parameter("override.shader", "cmdline_override"); + api.parameter("override.photons", true); + api.options(SunflowAPI.DEFAULT_OPTIONS); + } + // create display + Display display; + String currentFilename = (filename != null) ? filename.replace("#", String.format("%04d", frameNumber)) : null; + if (showFrame) { + display = new FrameDisplay(currentFilename); + } else { + if (currentFilename != null && currentFilename.equals("imgpipe")) { + display = new ImgPipeDisplay(); + } else + display = new FileDisplay(currentFilename); + } + api.render(SunflowAPI.DEFAULT_OPTIONS, display); + } + } else { + MetalLookAndFeel.setCurrentTheme(new DefaultMetalTheme()); + SunflowGUI gui = new SunflowGUI(); + gui.setVisible(true); + Dimension screenRes = Toolkit.getDefaultToolkit().getScreenSize(); + if (screenRes.getWidth() <= DEFAULT_WIDTH || screenRes.getHeight() <= DEFAULT_HEIGHT) + gui.setExtendedState(MAXIMIZED_BOTH); + gui.tileWindowMenuItem.doClick(); + SunflowAPI.runSystemCheck(); + } + } + + public SunflowGUI() { + super(); + currentFile = null; + lastSaveDirectory = null; + api = null; + initGUI(); + pack(); + setLocationRelativeTo(null); + newFileMenuItemActionPerformed(null); + UI.set(this); + } + + private void initGUI() { + setTitle("Sunflow v" + SunflowAPI.VERSION); + setDefaultCloseOperation(EXIT_ON_CLOSE); + { + desktop = new JDesktopPane(); + getContentPane().add(desktop, BorderLayout.CENTER); + Dimension screenRes = Toolkit.getDefaultToolkit().getScreenSize(); + if (screenRes.getWidth() <= DEFAULT_WIDTH || screenRes.getHeight() <= DEFAULT_HEIGHT) + desktop.setPreferredSize(new java.awt.Dimension(640, 480)); + else + desktop.setPreferredSize(new java.awt.Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT)); + { + imagePanelFrame = new JInternalFrame(); + desktop.add(imagePanelFrame); + { + jPanel1 = new JPanel(); + FlowLayout jPanel1Layout = new FlowLayout(); + jPanel1Layout.setAlignment(FlowLayout.LEFT); + jPanel1.setLayout(jPanel1Layout); + imagePanelFrame.getContentPane().add(jPanel1, BorderLayout.NORTH); + { + renderButton = new JButton(); + jPanel1.add(renderButton); + renderButton.setText("Render"); + renderButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + renderMenuItemActionPerformed(evt); + } + }); + } + { + iprButton = new JButton(); + jPanel1.add(iprButton); + iprButton.setText("IPR"); + iprButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + iprMenuItemActionPerformed(evt); + } + }); + } + } + { + imagePanel = new ImagePanel(); + imagePanelFrame.getContentPane().add(imagePanel, BorderLayout.CENTER); + } + imagePanelFrame.pack(); + imagePanelFrame.setResizable(true); + imagePanelFrame.setMaximizable(true); + imagePanelFrame.setVisible(true); + imagePanelFrame.setTitle("Image"); + imagePanelFrame.setIconifiable(true); + } + { + editorFrame = new JInternalFrame(); + desktop.add(editorFrame); + editorFrame.setTitle("Script Editor"); + editorFrame.setMaximizable(true); + editorFrame.setResizable(true); + editorFrame.setIconifiable(true); + { + jScrollPane1 = new JScrollPane(); + editorFrame.getContentPane().add(jScrollPane1, BorderLayout.CENTER); + jScrollPane1.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + jScrollPane1.setPreferredSize(new java.awt.Dimension(360, 280)); + { + editorTextArea = new JTextArea(); + jScrollPane1.setViewportView(editorTextArea); + editorTextArea.setFont(new java.awt.Font("Monospaced", 0, 12)); + // drag and drop + editorTextArea.setTransferHandler(new SceneTransferHandler()); + } + } + { + jPanel3 = new JPanel(); + editorFrame.getContentPane().add(jPanel3, BorderLayout.SOUTH); + FlowLayout jPanel3Layout = new FlowLayout(); + jPanel3Layout.setAlignment(FlowLayout.RIGHT); + jPanel3.setLayout(jPanel3Layout); + { + buildButton = new JButton(); + jPanel3.add(buildButton); + buildButton.setText("Build Scene"); + buildButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + buildMenuItemActionPerformed(evt); + } + }); + } + } + editorFrame.pack(); + editorFrame.setVisible(true); + } + { + consoleFrame = new JInternalFrame(); + desktop.add(consoleFrame); + consoleFrame.setIconifiable(true); + consoleFrame.setMaximizable(true); + consoleFrame.setResizable(true); + consoleFrame.setTitle("Console"); + { + jScrollPane2 = new JScrollPane(); + consoleFrame.getContentPane().add(jScrollPane2, BorderLayout.CENTER); + jScrollPane2.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + jScrollPane2.setPreferredSize(new java.awt.Dimension(360, 100)); + { + consoleTextArea = new JTextArea(); + jScrollPane2.setViewportView(consoleTextArea); + consoleTextArea.setFont(new java.awt.Font("Monospaced", 0, 12)); + consoleTextArea.setEditable(false); + } + } + { + jPanel4 = new JPanel(); + consoleFrame.getContentPane().add(jPanel4, BorderLayout.SOUTH); + BorderLayout jPanel4Layout = new BorderLayout(); + jPanel4.setLayout(jPanel4Layout); + { + jPanel6 = new JPanel(); + BorderLayout jPanel6Layout = new BorderLayout(); + jPanel6.setLayout(jPanel6Layout); + jPanel4.add(jPanel6, BorderLayout.CENTER); + jPanel6.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 0)); + { + taskProgressBar = new JProgressBar(); + jPanel6.add(taskProgressBar); + taskProgressBar.setEnabled(false); + taskProgressBar.setString(""); + taskProgressBar.setStringPainted(true); + taskProgressBar.setOpaque(false); + } + } + { + jPanel5 = new JPanel(); + FlowLayout jPanel5Layout = new FlowLayout(); + jPanel5Layout.setAlignment(FlowLayout.RIGHT); + jPanel5.setLayout(jPanel5Layout); + jPanel4.add(jPanel5, BorderLayout.EAST); + { + taskCancelButton = new JButton(); + jPanel5.add(taskCancelButton); + taskCancelButton.setText("Cancel"); + taskCancelButton.setEnabled(false); + taskCancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + UI.taskCancel(); + } + }); + } + { + clearConsoleButton = new JButton(); + jPanel5.add(clearConsoleButton); + clearConsoleButton.setText("Clear"); + clearConsoleButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + clearConsole(); + } + }); + } + } + } + consoleFrame.pack(); + consoleFrame.setVisible(true); + } + } + { + jMenuBar1 = new JMenuBar(); + setJMenuBar(jMenuBar1); + { + fileMenu = new JMenu(); + jMenuBar1.add(fileMenu); + fileMenu.setText("File"); + { + newFileMenuItem = new JMenuItem(); + fileMenu.add(newFileMenuItem); + newFileMenuItem.setText("New"); + newFileMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl N")); + newFileMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + newFileMenuItemActionPerformed(evt); + } + }); + } + { + openFileMenuItem = new JMenuItem(); + fileMenu.add(openFileMenuItem); + openFileMenuItem.setText("Open ..."); + openFileMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl O")); + openFileMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + openFileMenuItemActionPerformed(evt); + } + }); + } + { + saveMenuItem = new JMenuItem(); + fileMenu.add(saveMenuItem); + saveMenuItem.setText("Save"); + saveMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl S")); + saveMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + saveCurrentFile(currentFile); + } + }); + } + { + saveAsMenuItem = new JMenuItem(); + fileMenu.add(saveAsMenuItem); + saveAsMenuItem.setText("Save As ..."); + saveAsMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + saveAsMenuItemActionPerformed(evt); + } + }); + } + { + jSeparator2 = new JSeparator(); + fileMenu.add(jSeparator2); + } + { + exitMenuItem = new JMenuItem(); + fileMenu.add(exitMenuItem); + exitMenuItem.setText("Exit"); + exitMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + System.exit(0); + } + }); + } + } + { + sceneMenu = new JMenu(); + jMenuBar1.add(sceneMenu); + sceneMenu.setText("Scene"); + { + buildMenuItem = new JMenuItem(); + sceneMenu.add(buildMenuItem); + buildMenuItem.setText("Build"); + buildMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl B")); + buildMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + if (sceneMenu.isEnabled()) + buildMenuItemActionPerformed(evt); + } + }); + } + { + autoBuildMenuItem = new JCheckBoxMenuItem(); + sceneMenu.add(autoBuildMenuItem); + autoBuildMenuItem.setText("Build on open"); + autoBuildMenuItem.setSelected(true); + } + { + jSeparator3 = new JSeparator(); + sceneMenu.add(jSeparator3); + } + { + renderMenuItem = new JMenuItem(); + sceneMenu.add(renderMenuItem); + renderMenuItem.setText("Render"); + renderMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + renderMenuItemActionPerformed(evt); + } + }); + } + { + iprMenuItem = new JMenuItem(); + sceneMenu.add(iprMenuItem); + iprMenuItem.setText("IPR"); + iprMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + iprMenuItemActionPerformed(evt); + } + }); + } + { + clearLogMenuItem = new JCheckBoxMenuItem(); + sceneMenu.add(clearLogMenuItem); + clearLogMenuItem.setText("Auto Clear Log"); + clearLogMenuItem.setToolTipText("Clears the console before building or rendering"); + clearLogMenuItem.setSelected(true); + } + { + jSeparator4 = new JSeparator(); + sceneMenu.add(jSeparator4); + } + { + textureCacheClearMenuItem = new JMenuItem(); + sceneMenu.add(textureCacheClearMenuItem); + textureCacheClearMenuItem.setText("Clear Texture Cache"); + textureCacheClearMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + textureCacheClearMenuItemActionPerformed(evt); + } + }); + } + { + smallTrianglesMenuItem = new JCheckBoxMenuItem(); + sceneMenu.add(smallTrianglesMenuItem); + smallTrianglesMenuItem.setText("Low Mem Triangles"); + smallTrianglesMenuItem.setToolTipText("Load future meshes using a low memory footprint triangle representation"); + smallTrianglesMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + smallTrianglesMenuItemActionPerformed(evt); + } + }); + } + } + { + imageMenu = new JMenu(); + jMenuBar1.add(imageMenu); + imageMenu.setText("Image"); + { + resetZoomMenuItem = new JMenuItem(); + imageMenu.add(resetZoomMenuItem); + resetZoomMenuItem.setText("Reset Zoom"); + resetZoomMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + imagePanel.reset(); + } + }); + } + { + fitWindowMenuItem = new JMenuItem(); + imageMenu.add(fitWindowMenuItem); + fitWindowMenuItem.setText("Fit to Window"); + fitWindowMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + imagePanel.fit(); + } + }); + } + { + jSeparator1 = new JSeparator(); + imageMenu.add(jSeparator1); + } + { + jMenuItem4 = new JMenuItem(); + imageMenu.add(jMenuItem4); + jMenuItem4.setText("Save Image ..."); + jMenuItem4.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + // imagePanel.image; + JFileChooser fc = new JFileChooser("."); + fc.setFileFilter(new FileFilter() { + @Override + public String getDescription() { + return "Image File"; + } + + @Override + public boolean accept(File f) { + return (f.isDirectory() || f.getName().endsWith(".png") || f.getName().endsWith(".tga")); + } + }); + if (fc.showSaveDialog(SunflowGUI.this) == JFileChooser.APPROVE_OPTION) { + String filename = fc.getSelectedFile().getAbsolutePath(); + imagePanel.save(filename); + } + } + }); + } + } + { + windowMenu = new JMenu(); + jMenuBar1.add(windowMenu); + windowMenu.setText("Window"); + } + { + imageWindowMenuItem = new JMenuItem(); + windowMenu.add(imageWindowMenuItem); + imageWindowMenuItem.setText("Image"); + imageWindowMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl 1")); + imageWindowMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + selectFrame(imagePanelFrame); + } + }); + } + { + editorWindowMenuItem = new JMenuItem(); + windowMenu.add(editorWindowMenuItem); + editorWindowMenuItem.setText("Script Editor"); + editorWindowMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl 2")); + editorWindowMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + selectFrame(editorFrame); + } + }); + } + { + consoleWindowMenuItem = new JMenuItem(); + windowMenu.add(consoleWindowMenuItem); + consoleWindowMenuItem.setText("Console"); + consoleWindowMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl 3")); + consoleWindowMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + selectFrame(consoleFrame); + } + }); + } + { + jSeparator5 = new JSeparator(); + windowMenu.add(jSeparator5); + } + { + tileWindowMenuItem = new JMenuItem(); + windowMenu.add(tileWindowMenuItem); + tileWindowMenuItem.setText("Tile"); + tileWindowMenuItem.setAccelerator(KeyStroke.getKeyStroke("ctrl T")); + tileWindowMenuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + tileWindowMenuItemActionPerformed(evt); + } + }); + } + } + } + + private void newFileMenuItemActionPerformed(ActionEvent evt) { + if (evt != null) { + // check save? + } + // put some template code into the editor + String template = "import org.sunflow.core.*;\nimport org.sunflow.core.accel.*;\nimport org.sunflow.core.camera.*;\nimport org.sunflow.core.primitive.*;\nimport org.sunflow.core.shader.*;\nimport org.sunflow.image.Color;\nimport org.sunflow.math.*;\n\npublic void build() {\n // your code goes here\n\n}\n"; + editorTextArea.setText(template); + } + + private void openFileMenuItemActionPerformed(ActionEvent evt) { + JFileChooser fc = new JFileChooser("."); + if (lastSaveDirectory != null) + fc.setCurrentDirectory(lastSaveDirectory); + fc.setFileFilter(new FileFilter() { + @Override + public String getDescription() { + return "Scene File"; + } + + @Override + public boolean accept(File f) { + return (f.isDirectory() || f.getName().endsWith(".sc") || f.getName().endsWith(".java")); + } + }); + + if (fc.showOpenDialog(SunflowGUI.this) == JFileChooser.APPROVE_OPTION) { + final String f = fc.getSelectedFile().getAbsolutePath(); + openFile(f); + lastSaveDirectory = fc.getSelectedFile().getParentFile(); + } + } + + private void buildMenuItemActionPerformed(ActionEvent evt) { + new Thread() { + @Override + public void run() { + setEnableInterface(false); + if (clearLogMenuItem.isSelected()) + clearConsole(); + Timer t = new Timer(); + t.start(); + try { + api = SunflowAPI.compile(editorTextArea.getText()); + } catch (NoClassDefFoundError e) { + UI.printError(Module.GUI, "Janino library not found. Please check command line."); + api = null; + } + if (api != null) { + try { + if (currentFile != null) { + String dir = new File(currentFile).getAbsoluteFile().getParent(); + api.searchpath("texture", dir); + api.searchpath("include", dir); + } + api.build(); + } catch (Exception e) { + UI.printError(Module.GUI, "Build terminated abnormally: %s", e.getMessage()); + for (StackTraceElement elt : e.getStackTrace()) { + UI.printInfo(Module.GUI, " at %s", elt.toString()); + } + e.printStackTrace(); + } + t.end(); + UI.printInfo(Module.GUI, "Build time: %s", t.toString()); + } + setEnableInterface(true); + } + }.start(); + } + + private void clearConsole() { + consoleTextArea.setText(null); + } + + private void println(final String s) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + consoleTextArea.append(s + "\n"); + } + }); + } + + private void setEnableInterface(boolean enabled) { + // lock or unlock options which are unsafe during builds or renders + newFileMenuItem.setEnabled(enabled); + openFileMenuItem.setEnabled(enabled); + saveMenuItem.setEnabled(enabled); + saveAsMenuItem.setEnabled(enabled); + sceneMenu.setEnabled(enabled); + buildButton.setEnabled(enabled); + renderButton.setEnabled(enabled); + iprButton.setEnabled(enabled); + } + + public void print(Module m, PrintLevel level, String s) { + if (level == PrintLevel.ERROR) + JOptionPane.showMessageDialog(SunflowGUI.this, s, String.format("Error - %s", m.name()), JOptionPane.ERROR_MESSAGE); + println(UI.formatOutput(m, level, s)); + } + + public void taskStart(String s, int min, int max) { + currentTask = s; + currentTaskLastP = -1; + final int taskMin = min; + final int taskMax = max; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + taskProgressBar.setEnabled(true); + taskCancelButton.setEnabled(true); + taskProgressBar.setMinimum(taskMin); + taskProgressBar.setMaximum(taskMax); + taskProgressBar.setValue(taskMin); + taskProgressBar.setString(currentTask); + } + }); + } + + public void taskUpdate(int current) { + final int taskCurrent = current; + final String taskString = currentTask; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + taskProgressBar.setValue(taskCurrent); + int p = (int) (100.0 * taskProgressBar.getPercentComplete()); + if (p > currentTaskLastP) { + taskProgressBar.setString(taskString + " [" + p + "%]"); + currentTaskLastP = p; + } + } + }); + } + + public void taskStop() { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + taskProgressBar.setValue(taskProgressBar.getMinimum()); + taskProgressBar.setString(""); + taskProgressBar.setEnabled(false); + taskCancelButton.setEnabled(false); + } + }); + } + + private void renderMenuItemActionPerformed(ActionEvent evt) { + new Thread() { + @Override + public void run() { + setEnableInterface(false); + if (clearLogMenuItem.isSelected()) + clearConsole(); + if (api != null) { + api.parameter("sampler", "bucket"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, imagePanel); + } else + UI.printError(Module.GUI, "Nothing to render!"); + setEnableInterface(true); + } + }.start(); + } + + private void iprMenuItemActionPerformed(ActionEvent evt) { + new Thread() { + @Override + public void run() { + setEnableInterface(false); + if (clearLogMenuItem.isSelected()) + clearConsole(); + if (api != null) { + api.parameter("sampler", "ipr"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, imagePanel); + } else + UI.printError(Module.GUI, "Nothing to IPR!"); + setEnableInterface(true); + } + }.start(); + } + + private void textureCacheClearMenuItemActionPerformed(ActionEvent evt) { + TextureCache.flush(); + } + + private void smallTrianglesMenuItemActionPerformed(ActionEvent evt) { + TriangleMesh.setSmallTriangles(smallTrianglesMenuItem.isSelected()); + } + + private void saveAsMenuItemActionPerformed(ActionEvent evt) { + JFileChooser fc = new JFileChooser("."); + if (lastSaveDirectory != null) + fc.setCurrentDirectory(lastSaveDirectory); + fc.setFileFilter(new FileFilter() { + @Override + public String getDescription() { + return "Scene File"; + } + + @Override + public boolean accept(File f) { + return (f.isDirectory() || f.getName().endsWith(".java")); + } + }); + + if (fc.showSaveDialog(SunflowGUI.this) == JFileChooser.APPROVE_OPTION) { + String f = fc.getSelectedFile().getAbsolutePath(); + if (!f.endsWith(".java")) + f += ".java"; + File file = new File(f); + if (!file.exists() || JOptionPane.showConfirmDialog(SunflowGUI.this, "This file already exists.\nOverwrite?", "Warning", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { + // save file + saveCurrentFile(f); + lastSaveDirectory = file.getParentFile(); + } + } + } + + private void saveCurrentFile(String filename) { + if (filename == null) { + // no filename was picked, go to save as dialog + saveAsMenuItemActionPerformed(null); + return; + } + FileWriter file; + try { + file = new FileWriter(filename); + // get text from editor pane + file.write(editorTextArea.getText()); + file.close(); + // update current filename + currentFile = filename; + UI.printInfo(Module.GUI, "Saved current script to \"%s\"", filename); + } catch (IOException e) { + UI.printError(Module.GUI, "Unable to save: \"%s\"", filename); + e.printStackTrace(); + } + } + + private void selectFrame(JInternalFrame frame) { + try { + frame.setSelected(true); + frame.setIcon(false); + } catch (PropertyVetoException e) { + // this should never happen + e.printStackTrace(); + } + } + + private void tileWindowMenuItemActionPerformed(ActionEvent evt) { + try { + if (imagePanelFrame.isIcon()) + imagePanelFrame.setIcon(false); + if (editorFrame.isIcon()) + editorFrame.setIcon(false); + if (consoleFrame.isIcon()) + consoleFrame.setIcon(false); + + int width = desktop.getWidth(); + int height = desktop.getHeight(); + int widthLeft = width * 7 / 12; + int widthRight = width - widthLeft; + int pad = 2; + int pad2 = pad + pad; + + imagePanelFrame.reshape(pad, pad, widthLeft - pad2, height - pad2); + editorFrame.reshape(pad + widthLeft, pad, widthRight - pad2, height / 2 - pad2); + consoleFrame.reshape(pad + widthLeft, pad + height / 2, widthRight - pad2, height / 2 - pad2); + } catch (PropertyVetoException e) { + e.printStackTrace(); + } + } + + private void openFile(String filename) { + if (filename.endsWith(".java")) { + // read the file line by line + String code = ""; + FileReader file; + try { + file = new FileReader(filename); + BufferedReader bf = new BufferedReader(file); + while (true) { + String line; + line = bf.readLine(); + if (line == null) + break; + code += line; + code += "\n"; + } + file.close(); + editorTextArea.setText(code); + } catch (FileNotFoundException e) { + UI.printError(Module.GUI, "Unable to load: \"%s\"", filename); + return; + } catch (IOException e) { + UI.printError(Module.GUI, "Unable to load: \"%s\"", filename); + return; + } + // loade went ok, use filename as current + currentFile = filename; + UI.printInfo(Module.GUI, "Loaded script: \"%s\"", filename); + } else if (filename.endsWith(".sc")) { + String template = "import org.sunflow.core.*;\nimport org.sunflow.core.accel.*;\nimport org.sunflow.core.camera.*;\nimport org.sunflow.core.primitive.*;\nimport org.sunflow.core.shader.*;\nimport org.sunflow.image.Color;\nimport org.sunflow.math.*;\n\npublic void build() {\n include(\"" + filename.replace("\\", "\\\\") + "\");\n}\n"; + editorTextArea.setText(template); + // no java file associated + currentFile = null; + UI.printInfo(Module.GUI, "Created template for \"%s\"", filename); + } else { + UI.printError(Module.GUI, "Unknown file format selected"); + return; + } + editorTextArea.setCaretPosition(0); + if (autoBuildMenuItem.isSelected()) { + // try to compile the code we just loaded + buildMenuItemActionPerformed(null); + } + + } + + private class SceneTransferHandler extends TransferHandler { + @Override + public boolean importData(JComponent c, Transferable t) { + if (!sceneMenu.isEnabled()) + return false; + // can I import it? + if (!canImport(c, t.getTransferDataFlavors())) { + return false; + } + try { + // get a List of Files + List files = (java.util.List) t.getTransferData(DataFlavor.javaFileListFlavor); + for (int i = 0; i < files.size(); i++) { + final File file = (File) files.get(i); + String filename = file.getAbsolutePath(); + // check extension + if (filename.endsWith(".sc") || filename.endsWith(".java")) { + openFile(filename); + // load only one file at a time, stop here + break; + } + } + } catch (Exception exp) { + // debug + exp.printStackTrace(); + } + + return false; + } + + @Override + public boolean canImport(JComponent c, DataFlavor[] flavors) { + // Just a quick check to see if a file can be accepted at this time + // Are there any files around? + for (int i = 0; i < flavors.length; i++) { + if (flavors[i].isFlavorJavaFileListType()) + return true; + } + + // guess not + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/AsciiFileSunflowAPI.java b/src/main/java/org/sunflow/AsciiFileSunflowAPI.java new file mode 100644 index 0000000..5da75b7 --- /dev/null +++ b/src/main/java/org/sunflow/AsciiFileSunflowAPI.java @@ -0,0 +1,93 @@ +package org.sunflow; + +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Locale; + +import org.sunflow.core.ParameterList.InterpolationType; +import org.sunflow.core.parser.SCAbstractParser.Keyword; +import org.sunflow.math.Matrix4; + +class AsciiFileSunflowAPI extends FileSunflowAPI { + private OutputStream stream; + + AsciiFileSunflowAPI(String filename) throws IOException { + stream = new BufferedOutputStream(new FileOutputStream(filename)); + } + + @Override + protected void writeBoolean(boolean value) { + if (value) + writeString("true"); + else + writeString("false"); + } + + @Override + protected void writeFloat(float value) { + writeString(String.format("%s", value)); + } + + @Override + protected void writeInt(int value) { + writeString(String.format("%d", value)); + } + + @Override + protected void writeInterpolationType(InterpolationType interp) { + writeString(String.format("%s", interp.toString().toLowerCase(Locale.ENGLISH))); + } + + @Override + protected void writeKeyword(Keyword keyword) { + writeString(String.format("%s", keyword.toString().toLowerCase(Locale.ENGLISH).replace("_array", "[]"))); + } + + @Override + protected void writeMatrix(Matrix4 value) { + writeString("row"); + for (float f : value.asRowMajor()) + writeFloat(f); + } + + @Override + protected void writeNewline(int indentNext) { + try { + stream.write('\n'); + for (int i = 0; i < indentNext; i++) + stream.write('\t'); + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } + } + + @Override + protected void writeString(String string) { + try { + // check if we need to write string with quotes + if (string.contains(" ") && !string.contains("")) + stream.write(String.format("\"%s\"", string).getBytes()); + else + stream.write(string.getBytes()); + stream.write(' '); + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } + } + + @Override + protected void writeVerbatimString(String string) { + writeString(String.format("%s\n ", string)); + } + + @Override + public void close() { + try { + stream.close(); + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/Benchmark.java b/src/main/java/org/sunflow/Benchmark.java new file mode 100644 index 0000000..956c5b5 --- /dev/null +++ b/src/main/java/org/sunflow/Benchmark.java @@ -0,0 +1,280 @@ +package org.sunflow; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.URL; + +import javax.imageio.ImageIO; + +import org.sunflow.core.Display; +import org.sunflow.core.display.FileDisplay; +import org.sunflow.core.display.FrameDisplay; +import org.sunflow.image.Color; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.BenchmarkFramework; +import org.sunflow.system.BenchmarkTest; +import org.sunflow.system.UI; +import org.sunflow.system.UserInterface; +import org.sunflow.system.UI.Module; +import org.sunflow.system.UI.PrintLevel; + +public class Benchmark implements BenchmarkTest, UserInterface, Display { + private int resolution; + private boolean showOutput; + private boolean showBenchmarkOutput; + private boolean saveOutput; + private boolean showWindow; + private int threads; + private int[] referenceImage; + private int[] validationImage; + private int errorThreshold; + + public static void main(String[] args) { + if (args.length == 0) { + System.out.println("Benchmark options:"); + System.out.println(" -regen Regenerate reference images for a variety of sizes"); + System.out.println(" -bench [threads] [resolution] Run a single iteration of the benchmark using the specified thread count and image resolution"); + System.out.println(" Default: threads=0 (auto-detect cpus), resolution=256"); + System.out.println(" -show Render the benchmark scene into a window without performing validation"); + } else if (args[0].equals("-regen")) { + int[] sizes = { 32, 64, 96, 128, 256, 384, 512 }; + for (int s : sizes) { + // run a single iteration to generate the reference image + Benchmark b = new Benchmark(s, true, false, true); + b.kernelMain(); + } + } else if (args[0].equals("-bench")) { + int threads = 0, resolution = 256; + if (args.length > 1) + threads = Integer.parseInt(args[1]); + if (args.length > 2) + resolution = Integer.parseInt(args[2]); + Benchmark benchmark = new Benchmark(resolution, false, true, false, threads, false); + benchmark.kernelBegin(); + benchmark.kernelMain(); + benchmark.kernelEnd(); + } else if (args[0].equals("-show")) { + Benchmark benchmark = new Benchmark(512, true, true, false, 0, true); + benchmark.kernelMain(); + } + } + + public Benchmark() { + this(384, false, true, false); + } + + public Benchmark(int resolution, boolean showOutput, boolean showBenchmarkOutput, boolean saveOutput) { + this(resolution, showOutput, showBenchmarkOutput, saveOutput, 0, false); + } + + public Benchmark(int resolution, boolean showOutput, boolean showBenchmarkOutput, boolean saveOutput, int threads, boolean showWindow) { + UI.set(this); + this.resolution = resolution; + this.showOutput = showOutput; + this.showBenchmarkOutput = showBenchmarkOutput; + this.saveOutput = saveOutput; + this.showWindow = showWindow; + this.threads = threads; + errorThreshold = 6; + // fetch reference image from resources (jar file or classpath) + if (saveOutput) + return; + URL imageURL = Benchmark.class.getResource(String.format("/resources/golden_%04X.png", resolution)); + if (imageURL == null) + UI.printError(Module.BENCH, "Unable to find reference frame!"); + UI.printInfo(Module.BENCH, "Loading reference image from: %s", imageURL); + try { + BufferedImage bi = ImageIO.read(imageURL); + if (bi.getWidth() != resolution || bi.getHeight() != resolution) + UI.printError(Module.BENCH, "Reference image has invalid resolution! Expected %dx%d found %dx%d", resolution, resolution, bi.getWidth(), bi.getHeight()); + referenceImage = new int[resolution * resolution]; + for (int y = 0, i = 0; y < resolution; y++) + for (int x = 0; x < resolution; x++, i++) + referenceImage[i] = bi.getRGB(x, resolution - 1 - y); // flip + } catch (IOException e) { + UI.printError(Module.BENCH, "Unable to load reference frame!"); + } + } + + public void execute() { + // 10 iterations maximum - 10 minute time limit + BenchmarkFramework framework = new BenchmarkFramework(10, 600); + framework.execute(this); + } + + private class BenchmarkScene extends SunflowAPI { + public BenchmarkScene() { + build(); + render(SunflowAPI.DEFAULT_OPTIONS, showWindow ? new FrameDisplay() : saveOutput ? new FileDisplay(String.format("resources/golden_%04X.png", resolution)) : Benchmark.this); + } + + @Override + public void build() { + // settings + parameter("threads", threads); + // spawn regular priority threads + parameter("threads.lowPriority", false); + parameter("resolutionX", resolution); + parameter("resolutionY", resolution); + parameter("aa.min", -1); + parameter("aa.max", 1); + parameter("filter", "triangle"); + parameter("depths.diffuse", 2); + parameter("depths.reflection", 2); + parameter("depths.refraction", 2); + parameter("bucket.order", "hilbert"); + parameter("bucket.size", 32); + // gi options + parameter("gi.engine", "igi"); + parameter("gi.igi.samples", 90); + parameter("gi.igi.c", 0.000008f); + options(SunflowAPI.DEFAULT_OPTIONS); + buildCornellBox(); + } + + private void buildCornellBox() { + // camera + parameter("transform", Matrix4.lookAt(new Point3(0, 0, -600), new Point3(0, 0, 0), new Vector3(0, 1, 0))); + parameter("fov", 45.0f); + camera("main_camera", "pinhole"); + parameter("camera", "main_camera"); + options(SunflowAPI.DEFAULT_OPTIONS); + // cornell box + float minX = -200; + float maxX = 200; + float minY = -160; + float maxY = minY + 400; + float minZ = -250; + float maxZ = 200; + + float[] verts = new float[] { minX, minY, minZ, maxX, minY, minZ, + maxX, minY, maxZ, minX, minY, maxZ, minX, maxY, minZ, maxX, + maxY, minZ, maxX, maxY, maxZ, minX, maxY, maxZ, }; + int[] indices = new int[] { 0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4, 1, + 2, 5, 5, 6, 2, 2, 3, 6, 6, 7, 3, 0, 3, 4, 4, 7, 3 }; + + parameter("diffuse", null, 0.70f, 0.70f, 0.70f); + shader("gray_shader", "diffuse"); + parameter("diffuse", null, 0.80f, 0.25f, 0.25f); + shader("red_shader", "diffuse"); + parameter("diffuse", null, 0.25f, 0.25f, 0.80f); + shader("blue_shader", "diffuse"); + + // build walls + parameter("triangles", indices); + parameter("points", "point", "vertex", verts); + parameter("faceshaders", new int[] { 0, 0, 0, 0, 1, 1, 0, 0, 2, 2 }); + geometry("walls", "triangle_mesh"); + + // instance walls + parameter("shaders", new String[] { "gray_shader", "red_shader", + "blue_shader" }); + instance("walls.instance", "walls"); + + // create mesh light + parameter("points", "point", "vertex", new float[] { -50, maxY - 1, + -50, 50, maxY - 1, -50, 50, maxY - 1, 50, -50, maxY - 1, 50 }); + parameter("triangles", new int[] { 0, 1, 2, 2, 3, 0 }); + parameter("radiance", null, 15, 15, 15); + parameter("samples", 8); + light("light", "triangle_mesh"); + + // spheres + parameter("eta", 1.6f); + shader("Glass", "glass"); + sphere("glass_sphere", "Glass", -120, minY + 55, -150, 50); + parameter("color", null, 0.70f, 0.70f, 0.70f); + shader("Mirror", "mirror"); + sphere("mirror_sphere", "Mirror", 100, minY + 60, -50, 50); + + // scanned model + geometry("teapot", "teapot"); + parameter("transform", Matrix4.translation(80, -50, 100).multiply(Matrix4.rotateX((float) -Math.PI / 6)).multiply(Matrix4.rotateY((float) Math.PI / 4)).multiply(Matrix4.rotateX((float) -Math.PI / 2).multiply(Matrix4.scale(1.2f)))); + parameter("shaders", "gray_shader"); + instance("teapot.instance1", "teapot"); + parameter("transform", Matrix4.translation(-80, -160, 50).multiply(Matrix4.rotateY((float) Math.PI / 4)).multiply(Matrix4.rotateX((float) -Math.PI / 2).multiply(Matrix4.scale(1.2f)))); + parameter("shaders", "gray_shader"); + instance("teapot.instance2", "teapot"); + } + + private void sphere(String name, String shaderName, float x, float y, float z, float radius) { + geometry(name, "sphere"); + parameter("transform", Matrix4.translation(x, y, z).multiply(Matrix4.scale(radius))); + parameter("shaders", shaderName); + instance(name + ".instance", name); + } + } + + public void kernelBegin() { + // allocate a fresh validation target + validationImage = new int[resolution * resolution]; + } + + public void kernelMain() { + // this builds and renders the scene + new BenchmarkScene(); + } + + public void kernelEnd() { + // make sure the rendered image was correct + int diff = 0; + if (referenceImage != null && validationImage.length == referenceImage.length) { + for (int i = 0; i < validationImage.length; i++) { + // count absolute RGB differences + diff += Math.abs((validationImage[i] & 0xFF) - (referenceImage[i] & 0xFF)); + diff += Math.abs(((validationImage[i] >> 8) & 0xFF) - ((referenceImage[i] >> 8) & 0xFF)); + diff += Math.abs(((validationImage[i] >> 16) & 0xFF) - ((referenceImage[i] >> 16) & 0xFF)); + } + if (diff > errorThreshold) + UI.printError(Module.BENCH, "Image check failed! - #errors: %d", diff); + else + UI.printInfo(Module.BENCH, "Image check passed!"); + } else + UI.printError(Module.BENCH, "Image check failed! - reference is not comparable"); + + } + + public void print(Module m, PrintLevel level, String s) { + if (showOutput || (showBenchmarkOutput && m == Module.BENCH)) + System.out.println(UI.formatOutput(m, level, s)); + if (level == PrintLevel.ERROR) + throw new RuntimeException(s); + } + + public void taskStart(String s, int min, int max) { + // render progress display not needed + } + + public void taskStop() { + // render progress display not needed + } + + public void taskUpdate(int current) { + // render progress display not needed + } + + public void imageBegin(int w, int h, int bucketSize) { + // we can assume w == h == resolution + } + + public void imageEnd() { + // nothing needs to be done - image verification is done externally + } + + public void imageFill(int x, int y, int w, int h, Color c, float alpha) { + // this is not used + } + + public void imagePrepare(int x, int y, int w, int h, int id) { + // this is not needed + } + + public void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha) { + // copy bucket data to validation image + for (int j = 0, index = 0; j < h; j++, y++) + for (int i = 0, offset = x + resolution * (resolution - 1 - y); i < w; i++, index++, offset++) + validationImage[offset] = data[index].copy().toNonLinear().toRGB(); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/BinaryFileSunflowAPI.java b/src/main/java/org/sunflow/BinaryFileSunflowAPI.java new file mode 100644 index 0000000..60ac622 --- /dev/null +++ b/src/main/java/org/sunflow/BinaryFileSunflowAPI.java @@ -0,0 +1,225 @@ +package org.sunflow; + +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +import org.sunflow.core.ParameterList.InterpolationType; +import org.sunflow.core.parser.SCAbstractParser.Keyword; +import org.sunflow.math.Matrix4; + +class BinaryFileSunflowAPI extends FileSunflowAPI { + private DataOutputStream stream; + + BinaryFileSunflowAPI(String filename) throws FileNotFoundException { + stream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(filename))); + } + + @Override + protected void writeBoolean(boolean value) { + try { + if (value) + stream.write(1); + else + stream.write(0); + } catch (IOException e) { + // throw as a silent exception to avoid having to propage throw + // declarations upwards + throw new RuntimeException(e.getMessage()); + } + } + + @Override + protected void writeFloat(float value) { + writeInt(Float.floatToRawIntBits(value)); + } + + @Override + protected void writeInt(int value) { + try { + // little endian, LSB first + stream.write(value & 0xFF); + stream.write((value >>> 8) & 0xFF); + stream.write((value >>> 16) & 0xFF); + stream.write((value >>> 24) & 0xFF); + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } + } + + @Override + protected void writeInterpolationType(InterpolationType interp) { + try { + switch (interp) { + case NONE: + stream.write('n'); + break; + case VERTEX: + stream.write('v'); + break; + case FACE: + stream.write('p'); + break; + case FACEVARYING: + stream.write('f'); + break; + default: + throw new RuntimeException(String.format("Unknown interpolation type \"%s\"", interp.toString())); + } + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } + } + + @Override + protected void writeKeyword(Keyword keyword) { + try { + switch (keyword) { + case RESET: + writeExtendedKeyword('R'); + break; + case PARAMETER: + stream.write('p'); + break; + case GEOMETRY: + stream.write('g'); + break; + case INSTANCE: + stream.write('i'); + break; + case SHADER: + stream.write('s'); + break; + case MODIFIER: + stream.write('m'); + break; + case LIGHT: + stream.write('l'); + break; + case CAMERA: + stream.write('c'); + break; + case OPTIONS: + stream.write('o'); + break; + case INCLUDE: + writeExtendedKeyword('i'); + break; + case REMOVE: + writeExtendedKeyword('r'); + break; + case FRAME: + writeExtendedKeyword('f'); + break; + case PLUGIN: + writeExtendedKeyword('p'); + break; + case SEARCHPATH: + writeExtendedKeyword('s'); + break; + case STRING: + writeDatatypeKeyword('s', false); + break; + case BOOL: + writeDatatypeKeyword('b', false); + break; + case INT: + writeDatatypeKeyword('i', false); + break; + case FLOAT: + writeDatatypeKeyword('f', false); + break; + case COLOR: + writeDatatypeKeyword('c', false); + break; + case POINT: + writeDatatypeKeyword('p', false); + break; + case VECTOR: + writeDatatypeKeyword('v', false); + break; + case TEXCOORD: + writeDatatypeKeyword('t', false); + break; + case MATRIX: + writeDatatypeKeyword('m', false); + break; + case STRING_ARRAY: + writeDatatypeKeyword('s', true); + break; + case INT_ARRAY: + writeDatatypeKeyword('i', true); + break; + case FLOAT_ARRAY: + writeDatatypeKeyword('f', true); + break; + case POINT_ARRAY: + writeDatatypeKeyword('p', true); + break; + case VECTOR_ARRAY: + writeDatatypeKeyword('v', true); + break; + case TEXCOORD_ARRAY: + writeDatatypeKeyword('t', true); + break; + case MATRIX_ARRAY: + writeDatatypeKeyword('m', true); + break; + default: + throw new RuntimeException(String.format("Unknown keyword \"%s\" requested", keyword.toString())); + } + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } + } + + private void writeExtendedKeyword(int code) throws IOException { + stream.write('x'); + stream.write(code); + } + + // helper routine for datatype keywords + private void writeDatatypeKeyword(int type, boolean isArray) throws IOException { + stream.write('t'); + stream.write(type); + writeBoolean(isArray); + } + + @Override + protected void writeMatrix(Matrix4 value) { + for (float f : value.asRowMajor()) + writeFloat(f); + } + + @Override + protected void writeString(String string) { + try { + byte[] data = string.getBytes("UTF-8"); + writeInt(data.length); + stream.write(data); + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + + @Override + protected void writeVerbatimString(String string) { + writeString(string); + } + + @Override + protected void writeNewline(int indentNext) { + // does nothing + } + + @Override + public void close() { + try { + stream.close(); + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/FileSunflowAPI.java b/src/main/java/org/sunflow/FileSunflowAPI.java new file mode 100644 index 0000000..f6a2f50 --- /dev/null +++ b/src/main/java/org/sunflow/FileSunflowAPI.java @@ -0,0 +1,315 @@ +package org.sunflow; + +import java.util.Locale; + +import org.sunflow.core.Display; +import org.sunflow.core.ParameterList.InterpolationType; +import org.sunflow.core.parser.SCAbstractParser.Keyword; +import org.sunflow.image.ColorFactory; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point2; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +abstract class FileSunflowAPI implements SunflowAPIInterface { + private int frame; + + protected FileSunflowAPI() { + frame = 1; + reset(); + } + + public void camera(String name, String lensType) { + writeKeyword(Keyword.CAMERA); + writeString(name); + writeString(lensType); + writeNewline(0); + writeNewline(0); + } + + public void geometry(String name, String typeName) { + writeKeyword(Keyword.GEOMETRY); + writeString(name); + writeString(typeName); + writeNewline(0); + writeNewline(0); + } + + public int getCurrentFrame() { + return frame; + } + + public void instance(String name, String geoname) { + writeKeyword(Keyword.INSTANCE); + writeString(name); + writeString(geoname); + writeNewline(0); + writeNewline(0); + } + + public void light(String name, String lightType) { + writeKeyword(Keyword.LIGHT); + writeString(name); + writeString(lightType); + writeNewline(0); + writeNewline(0); + } + + public void modifier(String name, String modifierType) { + writeKeyword(Keyword.MODIFIER); + writeString(name); + writeString(modifierType); + writeNewline(0); + writeNewline(0); + } + + public void options(String name) { + writeKeyword(Keyword.OPTIONS); + writeString(name); + writeNewline(0); + writeNewline(0); + } + + public void parameter(String name, String value) { + writeKeyword(Keyword.PARAMETER); + writeString(name); + writeKeyword(Keyword.STRING); + writeString(value); + writeNewline(0); + } + + public void parameter(String name, boolean value) { + writeKeyword(Keyword.PARAMETER); + writeString(name); + writeKeyword(Keyword.BOOL); + writeBoolean(value); + writeNewline(0); + } + + public void parameter(String name, int value) { + writeKeyword(Keyword.PARAMETER); + writeString(name); + writeKeyword(Keyword.INT); + writeInt(value); + writeNewline(0); + } + + public void parameter(String name, float value) { + writeKeyword(Keyword.PARAMETER); + writeString(name); + writeKeyword(Keyword.FLOAT); + writeFloat(value); + writeNewline(0); + } + + public void parameter(String name, String colorspace, float... data) { + writeKeyword(Keyword.PARAMETER); + writeString(name); + writeKeyword(Keyword.COLOR); + if (colorspace == null) + writeString(colorspace = ColorFactory.getInternalColorspace()); + else + writeString(colorspace); + if (ColorFactory.getRequiredDataValues(colorspace) == -1) + writeInt(data.length); + int idx = 0; + int step = 9; + for (float f : data) { + if (data.length > step && idx % step == 0) + writeNewline(1); + writeFloat(f); + idx++; + } + writeNewline(0); + } + + public void parameter(String name, Point3 value) { + writeKeyword(Keyword.PARAMETER); + writeString(name); + writeKeyword(Keyword.POINT); + writeFloat(value.x); + writeFloat(value.y); + writeFloat(value.z); + writeNewline(0); + } + + public void parameter(String name, Vector3 value) { + writeKeyword(Keyword.PARAMETER); + writeString(name); + writeKeyword(Keyword.VECTOR); + writeFloat(value.x); + writeFloat(value.y); + writeFloat(value.z); + writeNewline(0); + } + + public void parameter(String name, Point2 value) { + writeKeyword(Keyword.PARAMETER); + writeString(name); + writeKeyword(Keyword.TEXCOORD); + writeFloat(value.x); + writeFloat(value.y); + writeNewline(0); + } + + public void parameter(String name, Matrix4 value) { + writeKeyword(Keyword.PARAMETER); + writeString(name); + writeKeyword(Keyword.MATRIX); + writeMatrix(value); + writeNewline(0); + } + + public void parameter(String name, int[] value) { + writeKeyword(Keyword.PARAMETER); + writeString(name); + writeKeyword(Keyword.INT_ARRAY); + writeInt(value.length); + int idx = 0; + int step = 9; + for (int v : value) { + if (idx % step == 0) + writeNewline(1); + writeInt(v); + idx++; + } + writeNewline(0); + } + + public void parameter(String name, String[] value) { + writeKeyword(Keyword.PARAMETER); + writeString(name); + writeKeyword(Keyword.STRING_ARRAY); + writeInt(value.length); + for (String v : value) { + writeNewline(1); + writeString(v); + } + writeNewline(0); + } + + public void parameter(String name, String type, String interpolation, float[] data) { + InterpolationType interp; + try { + interp = InterpolationType.valueOf(interpolation.toUpperCase(Locale.ENGLISH)); + } catch (IllegalArgumentException e) { + UI.printError(Module.API, "Unknown interpolation type: %s -- ignoring parameter \"%s\"", interpolation, name); + return; + } + Keyword typeKeyword; + int lengthFactor; + if (type.equals("float")) { + typeKeyword = Keyword.FLOAT_ARRAY; + lengthFactor = 1; + } else if (type.equals("point")) { + typeKeyword = Keyword.POINT_ARRAY; + lengthFactor = 3; + } else if (type.equals("vector")) { + typeKeyword = Keyword.VECTOR_ARRAY; + lengthFactor = 3; + } else if (type.equals("texcoord")) { + typeKeyword = Keyword.TEXCOORD_ARRAY; + lengthFactor = 2; + } else if (type.equals("matrix")) { + typeKeyword = Keyword.MATRIX_ARRAY; + lengthFactor = 16; + } else { + UI.printError(Module.API, "Unknown parameter type: %s -- ignoring parameter \"%s\"", type, name); + return; + } + writeKeyword(Keyword.PARAMETER); + + writeString(name); + writeKeyword(typeKeyword); + writeInterpolationType(interp); + writeInt(data.length / lengthFactor); + int idx = 0; + if (data.length > 16) + lengthFactor *= 8; + for (float v : data) { + if (lengthFactor > 1 && idx % lengthFactor == 0) + writeNewline(1); + writeFloat(v); + idx++; + } + writeNewline(0); + } + + public boolean include(String filename) { + writeKeyword(Keyword.INCLUDE); + writeString(filename); + writeNewline(0); + writeNewline(0); + return true; + } + + public void plugin(String type, String name, String code) { + writeKeyword(Keyword.PLUGIN); + writeString(type); + writeString(name); + writeVerbatimString(code); + writeNewline(0); + writeNewline(0); + } + + public void remove(String name) { + writeKeyword(Keyword.REMOVE); + writeString(name); + writeNewline(0); + writeNewline(0); + } + + public void render(String optionsName, Display display) { + UI.printWarning(Module.API, "Unable to render file stream"); + } + + public void reset() { + frame = 1; + } + + public void searchpath(String type, String path) { + writeKeyword(Keyword.SEARCHPATH); + writeString(type); + writeString(path); + writeNewline(0); + writeNewline(0); + + } + + public void currentFrame(int currentFrame) { + writeKeyword(Keyword.FRAME); + writeInt(frame = currentFrame); + writeNewline(0); + writeNewline(0); + } + + public void shader(String name, String shaderType) { + writeKeyword(Keyword.SHADER); + writeString(name); + writeString(shaderType); + writeNewline(0); + writeNewline(0); + } + + protected abstract void writeKeyword(Keyword keyword); + + protected abstract void writeInterpolationType(InterpolationType interp); + + protected abstract void writeBoolean(boolean value); + + protected abstract void writeInt(int value); + + protected abstract void writeFloat(float value); + + protected abstract void writeString(String string); + + protected abstract void writeVerbatimString(String string); + + protected abstract void writeMatrix(Matrix4 value); + + protected abstract void writeNewline(int indentNext); + + public abstract void close(); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/PluginRegistry.java b/src/main/java/org/sunflow/PluginRegistry.java new file mode 100644 index 0000000..def4853 --- /dev/null +++ b/src/main/java/org/sunflow/PluginRegistry.java @@ -0,0 +1,322 @@ +package org.sunflow; + +import org.sunflow.core.AccelerationStructure; +import org.sunflow.core.BucketOrder; +import org.sunflow.core.CameraLens; +import org.sunflow.core.CausticPhotonMapInterface; +import org.sunflow.core.Filter; +import org.sunflow.core.GIEngine; +import org.sunflow.core.GlobalPhotonMapInterface; +import org.sunflow.core.ImageSampler; +import org.sunflow.core.LightSource; +import org.sunflow.core.Modifier; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.SceneParser; +import org.sunflow.core.Shader; +import org.sunflow.core.Tesselatable; +import org.sunflow.core.accel.BoundingIntervalHierarchy; +import org.sunflow.core.accel.KDTree; +import org.sunflow.core.accel.NullAccelerator; +import org.sunflow.core.accel.UniformGrid; +import org.sunflow.core.bucket.ColumnBucketOrder; +import org.sunflow.core.bucket.DiagonalBucketOrder; +import org.sunflow.core.bucket.HilbertBucketOrder; +import org.sunflow.core.bucket.RandomBucketOrder; +import org.sunflow.core.bucket.RowBucketOrder; +import org.sunflow.core.bucket.SpiralBucketOrder; +import org.sunflow.core.camera.FisheyeLens; +import org.sunflow.core.camera.PinholeLens; +import org.sunflow.core.camera.SphericalLens; +import org.sunflow.core.camera.ThinLens; +import org.sunflow.core.filter.BlackmanHarrisFilter; +import org.sunflow.core.filter.BoxFilter; +import org.sunflow.core.filter.CatmullRomFilter; +import org.sunflow.core.filter.CubicBSpline; +import org.sunflow.core.filter.GaussianFilter; +import org.sunflow.core.filter.LanczosFilter; +import org.sunflow.core.filter.MitchellFilter; +import org.sunflow.core.filter.SincFilter; +import org.sunflow.core.filter.TriangleFilter; +import org.sunflow.core.gi.AmbientOcclusionGIEngine; +import org.sunflow.core.gi.FakeGIEngine; +import org.sunflow.core.gi.InstantGI; +import org.sunflow.core.gi.IrradianceCacheGIEngine; +import org.sunflow.core.gi.PathTracingGIEngine; +import org.sunflow.core.light.DirectionalSpotlight; +import org.sunflow.core.light.ImageBasedLight; +import org.sunflow.core.light.PointLight; +import org.sunflow.core.light.SphereLight; +import org.sunflow.core.light.SunSkyLight; +import org.sunflow.core.light.TriangleMeshLight; +import org.sunflow.core.modifiers.BumpMappingModifier; +import org.sunflow.core.modifiers.NormalMapModifier; +import org.sunflow.core.modifiers.PerlinModifier; +import org.sunflow.core.parser.RA2Parser; +import org.sunflow.core.parser.RA3Parser; +import org.sunflow.core.parser.SCAsciiParser; +import org.sunflow.core.parser.SCBinaryParser; +import org.sunflow.core.parser.SCParser; +import org.sunflow.core.parser.ShaveRibParser; +import org.sunflow.core.photonmap.CausticPhotonMap; +import org.sunflow.core.photonmap.GlobalPhotonMap; +import org.sunflow.core.photonmap.GridPhotonMap; +import org.sunflow.core.primitive.Background; +import org.sunflow.core.primitive.BanchoffSurface; +import org.sunflow.core.primitive.Box; +import org.sunflow.core.primitive.CornellBox; +import org.sunflow.core.primitive.Cylinder; +import org.sunflow.core.primitive.Hair; +import org.sunflow.core.primitive.JuliaFractal; +import org.sunflow.core.primitive.ParticleSurface; +import org.sunflow.core.primitive.Plane; +import org.sunflow.core.primitive.QuadMesh; +import org.sunflow.core.primitive.Sphere; +import org.sunflow.core.primitive.SphereFlake; +import org.sunflow.core.primitive.Torus; +import org.sunflow.core.primitive.TriangleMesh; +import org.sunflow.core.renderer.BucketRenderer; +import org.sunflow.core.renderer.MultipassRenderer; +import org.sunflow.core.renderer.ProgressiveRenderer; +import org.sunflow.core.renderer.SimpleRenderer; +import org.sunflow.core.shader.AmbientOcclusionShader; +import org.sunflow.core.shader.AnisotropicWardShader; +import org.sunflow.core.shader.ConstantShader; +import org.sunflow.core.shader.DiffuseShader; +import org.sunflow.core.shader.GlassShader; +import org.sunflow.core.shader.IDShader; +import org.sunflow.core.shader.MirrorShader; +import org.sunflow.core.shader.NormalShader; +import org.sunflow.core.shader.PhongShader; +import org.sunflow.core.shader.PrimIDShader; +import org.sunflow.core.shader.QuickGrayShader; +import org.sunflow.core.shader.ShinyDiffuseShader; +import org.sunflow.core.shader.SimpleShader; +import org.sunflow.core.shader.TexturedAmbientOcclusionShader; +import org.sunflow.core.shader.TexturedDiffuseShader; +import org.sunflow.core.shader.TexturedPhongShader; +import org.sunflow.core.shader.TexturedShinyDiffuseShader; +import org.sunflow.core.shader.TexturedWardShader; +import org.sunflow.core.shader.UVShader; +import org.sunflow.core.shader.UberShader; +import org.sunflow.core.shader.ViewCausticsShader; +import org.sunflow.core.shader.ViewGlobalPhotonsShader; +import org.sunflow.core.shader.ViewIrradianceShader; +import org.sunflow.core.shader.WireframeShader; +import org.sunflow.core.tesselatable.BezierMesh; +import org.sunflow.core.tesselatable.FileMesh; +import org.sunflow.core.tesselatable.Gumbo; +import org.sunflow.core.tesselatable.Teapot; +import org.sunflow.image.BitmapReader; +import org.sunflow.image.BitmapWriter; +import org.sunflow.image.readers.BMPBitmapReader; +import org.sunflow.image.readers.HDRBitmapReader; +import org.sunflow.image.readers.IGIBitmapReader; +import org.sunflow.image.readers.JPGBitmapReader; +import org.sunflow.image.readers.PNGBitmapReader; +import org.sunflow.image.readers.TGABitmapReader; +import org.sunflow.image.writers.EXRBitmapWriter; +import org.sunflow.image.writers.HDRBitmapWriter; +import org.sunflow.image.writers.IGIBitmapWriter; +import org.sunflow.image.writers.PNGBitmapWriter; +import org.sunflow.image.writers.TGABitmapWriter; +import org.sunflow.system.Plugins; + +/** + * This class acts as the central repository for all user extensible types in + * Sunflow, even built-in types are registered here. This class is static so + * that new plugins may be reused by an application across several render + * scenes. + */ +public final class PluginRegistry { + // base types - needed by SunflowAPI + public static final Plugins primitivePlugins = new Plugins(PrimitiveList.class); + public static final Plugins tesselatablePlugins = new Plugins(Tesselatable.class); + public static final Plugins shaderPlugins = new Plugins(Shader.class); + public static final Plugins modifierPlugins = new Plugins(Modifier.class); + public static final Plugins lightSourcePlugins = new Plugins(LightSource.class); + public static final Plugins cameraLensPlugins = new Plugins(CameraLens.class); + + // advanced types - used inside the Sunflow core + public static final Plugins accelPlugins = new Plugins(AccelerationStructure.class); + public static final Plugins bucketOrderPlugins = new Plugins(BucketOrder.class); + public static final Plugins filterPlugins = new Plugins(Filter.class); + public static final Plugins giEnginePlugins = new Plugins(GIEngine.class); + public static final Plugins causticPhotonMapPlugins = new Plugins(CausticPhotonMapInterface.class); + public static final Plugins globalPhotonMapPlugins = new Plugins(GlobalPhotonMapInterface.class); + public static final Plugins imageSamplerPlugins = new Plugins(ImageSampler.class); + public static final Plugins parserPlugins = new Plugins(SceneParser.class); + public static final Plugins bitmapReaderPlugins = new Plugins(BitmapReader.class); + public static final Plugins bitmapWriterPlugins = new Plugins(BitmapWriter.class); + + // Register all plugins on startup: + static { + // primitives + primitivePlugins.registerPlugin("triangle_mesh", TriangleMesh.class); + primitivePlugins.registerPlugin("sphere", Sphere.class); + primitivePlugins.registerPlugin("cylinder", Cylinder.class); + primitivePlugins.registerPlugin("box", Box.class); + primitivePlugins.registerPlugin("banchoff", BanchoffSurface.class); + primitivePlugins.registerPlugin("hair", Hair.class); + primitivePlugins.registerPlugin("julia", JuliaFractal.class); + primitivePlugins.registerPlugin("particles", ParticleSurface.class); + primitivePlugins.registerPlugin("plane", Plane.class); + primitivePlugins.registerPlugin("quad_mesh", QuadMesh.class); + primitivePlugins.registerPlugin("torus", Torus.class); + primitivePlugins.registerPlugin("background", Background.class); + primitivePlugins.registerPlugin("sphereflake", SphereFlake.class); + } + + static { + // tesslatable + tesselatablePlugins.registerPlugin("bezier_mesh", BezierMesh.class); + tesselatablePlugins.registerPlugin("file_mesh", FileMesh.class); + tesselatablePlugins.registerPlugin("gumbo", Gumbo.class); + tesselatablePlugins.registerPlugin("teapot", Teapot.class); + } + + static { + // shaders + shaderPlugins.registerPlugin("ambient_occlusion", AmbientOcclusionShader.class); + shaderPlugins.registerPlugin("constant", ConstantShader.class); + shaderPlugins.registerPlugin("diffuse", DiffuseShader.class); + shaderPlugins.registerPlugin("glass", GlassShader.class); + shaderPlugins.registerPlugin("mirror", MirrorShader.class); + shaderPlugins.registerPlugin("phong", PhongShader.class); + shaderPlugins.registerPlugin("shiny_diffuse", ShinyDiffuseShader.class); + shaderPlugins.registerPlugin("uber", UberShader.class); + shaderPlugins.registerPlugin("ward", AnisotropicWardShader.class); + shaderPlugins.registerPlugin("wireframe", WireframeShader.class); + + // textured shaders + shaderPlugins.registerPlugin("textured_ambient_occlusion", TexturedAmbientOcclusionShader.class); + shaderPlugins.registerPlugin("textured_diffuse", TexturedDiffuseShader.class); + shaderPlugins.registerPlugin("textured_phong", TexturedPhongShader.class); + shaderPlugins.registerPlugin("textured_shiny_diffuse", TexturedShinyDiffuseShader.class); + shaderPlugins.registerPlugin("textured_ward", TexturedWardShader.class); + + // preview shaders + shaderPlugins.registerPlugin("quick_gray", QuickGrayShader.class); + shaderPlugins.registerPlugin("simple", SimpleShader.class); + shaderPlugins.registerPlugin("show_normals", NormalShader.class); + shaderPlugins.registerPlugin("show_uvs", UVShader.class); + shaderPlugins.registerPlugin("show_instance_id", IDShader.class); + shaderPlugins.registerPlugin("show_primitive_id", PrimIDShader.class); + shaderPlugins.registerPlugin("view_caustics", ViewCausticsShader.class); + shaderPlugins.registerPlugin("view_global", ViewGlobalPhotonsShader.class); + shaderPlugins.registerPlugin("view_irradiance", ViewIrradianceShader.class); + } + + static { + // modifiers + modifierPlugins.registerPlugin("bump_map", BumpMappingModifier.class); + modifierPlugins.registerPlugin("normal_map", NormalMapModifier.class); + modifierPlugins.registerPlugin("perlin", PerlinModifier.class); + } + + static { + // light sources + lightSourcePlugins.registerPlugin("directional", DirectionalSpotlight.class); + lightSourcePlugins.registerPlugin("ibl", ImageBasedLight.class); + lightSourcePlugins.registerPlugin("point", PointLight.class); + lightSourcePlugins.registerPlugin("sphere", SphereLight.class); + lightSourcePlugins.registerPlugin("sunsky", SunSkyLight.class); + lightSourcePlugins.registerPlugin("triangle_mesh", TriangleMeshLight.class); + lightSourcePlugins.registerPlugin("cornell_box", CornellBox.class); + } + + static { + // camera lenses + cameraLensPlugins.registerPlugin("pinhole", PinholeLens.class); + cameraLensPlugins.registerPlugin("thinlens", ThinLens.class); + cameraLensPlugins.registerPlugin("fisheye", FisheyeLens.class); + cameraLensPlugins.registerPlugin("spherical", SphericalLens.class); + } + + static { + // accels + accelPlugins.registerPlugin("bih", BoundingIntervalHierarchy.class); + accelPlugins.registerPlugin("kdtree", KDTree.class); + accelPlugins.registerPlugin("null", NullAccelerator.class); + accelPlugins.registerPlugin("uniformgrid", UniformGrid.class); + } + + static { + // bucket orders + bucketOrderPlugins.registerPlugin("column", ColumnBucketOrder.class); + bucketOrderPlugins.registerPlugin("diagonal", DiagonalBucketOrder.class); + bucketOrderPlugins.registerPlugin("hilbert", HilbertBucketOrder.class); + bucketOrderPlugins.registerPlugin("random", RandomBucketOrder.class); + bucketOrderPlugins.registerPlugin("row", RowBucketOrder.class); + bucketOrderPlugins.registerPlugin("spiral", SpiralBucketOrder.class); + } + + static { + // filters + filterPlugins.registerPlugin("blackman-harris", BlackmanHarrisFilter.class); + filterPlugins.registerPlugin("box", BoxFilter.class); + filterPlugins.registerPlugin("catmull-rom", CatmullRomFilter.class); + filterPlugins.registerPlugin("gaussian", GaussianFilter.class); + filterPlugins.registerPlugin("lanczos", LanczosFilter.class); + filterPlugins.registerPlugin("mitchell", MitchellFilter.class); + filterPlugins.registerPlugin("sinc", SincFilter.class); + filterPlugins.registerPlugin("triangle", TriangleFilter.class); + filterPlugins.registerPlugin("bspline", CubicBSpline.class); + } + + static { + // gi engines + giEnginePlugins.registerPlugin("ambocc", AmbientOcclusionGIEngine.class); + giEnginePlugins.registerPlugin("fake", FakeGIEngine.class); + giEnginePlugins.registerPlugin("igi", InstantGI.class); + giEnginePlugins.registerPlugin("irr-cache", IrradianceCacheGIEngine.class); + giEnginePlugins.registerPlugin("path", PathTracingGIEngine.class); + } + + static { + // caustic photon maps + causticPhotonMapPlugins.registerPlugin("kd", CausticPhotonMap.class); + } + + static { + // global photon maps + globalPhotonMapPlugins.registerPlugin("grid", GridPhotonMap.class); + globalPhotonMapPlugins.registerPlugin("kd", GlobalPhotonMap.class); + } + + static { + // image samplers + imageSamplerPlugins.registerPlugin("bucket", BucketRenderer.class); + imageSamplerPlugins.registerPlugin("ipr", ProgressiveRenderer.class); + imageSamplerPlugins.registerPlugin("fast", SimpleRenderer.class); + imageSamplerPlugins.registerPlugin("multipass", MultipassRenderer.class); + } + + static { + // parsers + parserPlugins.registerPlugin("sc", SCParser.class); + parserPlugins.registerPlugin("sca", SCAsciiParser.class); + parserPlugins.registerPlugin("scb", SCBinaryParser.class); + parserPlugins.registerPlugin("rib", ShaveRibParser.class); + parserPlugins.registerPlugin("ra2", RA2Parser.class); + parserPlugins.registerPlugin("ra3", RA3Parser.class); + } + + static { + // bitmap readers + bitmapReaderPlugins.registerPlugin("hdr", HDRBitmapReader.class); + bitmapReaderPlugins.registerPlugin("tga", TGABitmapReader.class); + bitmapReaderPlugins.registerPlugin("png", PNGBitmapReader.class); + bitmapReaderPlugins.registerPlugin("jpg", JPGBitmapReader.class); + bitmapReaderPlugins.registerPlugin("bmp", BMPBitmapReader.class); + bitmapReaderPlugins.registerPlugin("igi", IGIBitmapReader.class); + } + + static { + // bitmap writers + bitmapWriterPlugins.registerPlugin("png", PNGBitmapWriter.class); + bitmapWriterPlugins.registerPlugin("hdr", HDRBitmapWriter.class); + bitmapWriterPlugins.registerPlugin("tga", TGABitmapWriter.class); + bitmapWriterPlugins.registerPlugin("exr", EXRBitmapWriter.class); + bitmapWriterPlugins.registerPlugin("igi", IGIBitmapWriter.class); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/RealtimeBenchmark.java b/src/main/java/org/sunflow/RealtimeBenchmark.java new file mode 100644 index 0000000..af20648 --- /dev/null +++ b/src/main/java/org/sunflow/RealtimeBenchmark.java @@ -0,0 +1,123 @@ +package org.sunflow; + +import org.sunflow.core.Display; +import org.sunflow.core.display.FastDisplay; +import org.sunflow.core.display.FileDisplay; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; +import org.sunflow.system.ui.ConsoleInterface; + +public class RealtimeBenchmark extends SunflowAPI { + public RealtimeBenchmark(boolean showGUI, int threads) { + Display display = showGUI ? new FastDisplay() : new FileDisplay(false); + UI.printInfo(Module.BENCH, "Preparing benchmarking scene ..."); + // settings + parameter("threads", threads); + // spawn regular priority threads + parameter("threads.lowPriority", false); + parameter("resolutionX", 512); + parameter("resolutionY", 512); + parameter("aa.min", -3); + parameter("aa.max", 0); + parameter("depths.diffuse", 1); + parameter("depths.reflection", 1); + parameter("depths.refraction", 0); + parameter("bucket.order", "hilbert"); + parameter("bucket.size", 32); + options(SunflowAPI.DEFAULT_OPTIONS); + // camera + Point3 eye = new Point3(30, 0, 10.967f); + Point3 target = new Point3(0, 0, 5.4f); + Vector3 up = new Vector3(0, 0, 1); + parameter("transform", Matrix4.lookAt(eye, target, up)); + parameter("fov", 45.0f); + camera("camera", "pinhole"); + parameter("camera", "camera"); + options(SunflowAPI.DEFAULT_OPTIONS); + // geometry + createGeometry(); + // this first render is not timed, it caches the acceleration data + // structures and tesselations so they won't be + // included in the main timing + UI.printInfo(Module.BENCH, "Rendering warmup frame ..."); + render(SunflowAPI.DEFAULT_OPTIONS, display); + // now disable all output - and run the benchmark + UI.set(null); + Timer t = new Timer(); + t.start(); + float phi = 0; + int frames = 0; + while (phi < 4 * Math.PI) { + eye.x = 30 * (float) Math.cos(phi); + eye.y = 30 * (float) Math.sin(phi); + phi += Math.PI / 30; + frames++; + // update camera + parameter("transform", Matrix4.lookAt(eye, target, up)); + camera("camera", null); + render(SunflowAPI.DEFAULT_OPTIONS, display); + } + t.end(); + UI.set(new ConsoleInterface()); + UI.printInfo(Module.BENCH, "Benchmark results:"); + UI.printInfo(Module.BENCH, " * Average FPS: %.2f", frames / t.seconds()); + UI.printInfo(Module.BENCH, " * Total time: %s", t); + } + + private void createGeometry() { + // light source + parameter("source", new Point3(-15.5945f, -30.0581f, 45.967f)); + parameter("dir", new Vector3(15.5945f, 30.0581f, -45.967f)); + parameter("radius", 60.0f); + parameter("radiance", null, 3, 3, 3); + light("light", "directional"); + + // gi-engine + parameter("gi.engine", "fake"); + parameter("gi.fake.sky", null, 0.25f, 0.25f, 0.25f); + parameter("gi.fake.ground", null, 0.01f, 0.01f, 0.5f); + parameter("gi.fake.up", new Vector3(0, 0, 1)); + options(DEFAULT_OPTIONS); + + // shaders + parameter("diffuse", null, 0.5f, 0.5f, 0.5f); + shader("default", "diffuse"); + parameter("diffuse", null, 0.5f, 0.5f, 0.5f); + parameter("shiny", 0.2f); + shader("refl", "shiny_diffuse"); + // objects + + // teapot + parameter("subdivs", 10); + geometry("teapot", "teapot"); + parameter("shaders", "default"); + Matrix4 m = Matrix4.IDENTITY; + m = Matrix4.scale(0.075f).multiply(m); + m = Matrix4.rotateZ((float) Math.toRadians(-45f)).multiply(m); + m = Matrix4.translation(-7, 0, 0).multiply(m); + parameter("transform", m); + instance("teapot.instance", "teapot"); + + // gumbo + parameter("subdivs", 10); + geometry("gumbo", "gumbo"); + m = Matrix4.IDENTITY; + m = Matrix4.scale(0.5f).multiply(m); + m = Matrix4.rotateZ((float) Math.toRadians(25f)).multiply(m); + m = Matrix4.translation(3, -7, 0).multiply(m); + parameter("shaders", "default"); + parameter("transform", m); + instance("gumbo.instance", "gumbo"); + + // ground plane + parameter("center", new Point3(0, 0, 0)); + parameter("normal", new Vector3(0, 0, 1)); + geometry("ground", "plane"); + parameter("shaders", "refl"); + instance("ground.instance", "ground"); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/RenderObjectMap.java b/src/main/java/org/sunflow/RenderObjectMap.java new file mode 100644 index 0000000..fa49cd2 --- /dev/null +++ b/src/main/java/org/sunflow/RenderObjectMap.java @@ -0,0 +1,333 @@ +package org.sunflow; + +import java.util.ArrayList; +import java.util.Locale; + +import org.sunflow.core.Camera; +import org.sunflow.core.Geometry; +import org.sunflow.core.Instance; +import org.sunflow.core.LightSource; +import org.sunflow.core.Modifier; +import org.sunflow.core.Options; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.RenderObject; +import org.sunflow.core.Scene; +import org.sunflow.core.Shader; +import org.sunflow.core.Tesselatable; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; +import org.sunflow.util.FastHashMap; + +final class RenderObjectMap { + private FastHashMap renderObjects; + private boolean rebuildInstanceList; + private boolean rebuildLightList; + + private enum RenderObjectType { + UNKNOWN, SHADER, MODIFIER, GEOMETRY, INSTANCE, LIGHT, CAMERA, OPTIONS + } + + RenderObjectMap() { + renderObjects = new FastHashMap(); + rebuildInstanceList = rebuildLightList = false; + } + + final boolean has(String name) { + return renderObjects.containsKey(name); + } + + final void remove(String name) { + RenderObjectHandle obj = renderObjects.get(name); + if (obj == null) { + UI.printWarning(Module.API, "Unable to remove \"%s\" - object was not defined yet"); + return; + } + UI.printDetailed(Module.API, "Removing object \"%s\"", name); + renderObjects.remove(name); + // scan through all objects to make sure we don't have any + // references to the old object still around + switch (obj.type) { + case SHADER: + Shader s = obj.getShader(); + for (FastHashMap.Entry e : renderObjects) { + Instance i = e.getValue().getInstance(); + if (i != null) { + UI.printWarning(Module.API, "Removing shader \"%s\" from instance \"%s\"", name, e.getKey()); + i.removeShader(s); + } + } + break; + case MODIFIER: + Modifier m = obj.getModifier(); + for (FastHashMap.Entry e : renderObjects) { + Instance i = e.getValue().getInstance(); + if (i != null) { + UI.printWarning(Module.API, "Removing modifier \"%s\" from instance \"%s\"", name, e.getKey()); + i.removeModifier(m); + } + } + break; + case GEOMETRY: { + Geometry g = obj.getGeometry(); + for (FastHashMap.Entry e : renderObjects) { + Instance i = e.getValue().getInstance(); + if (i != null && i.hasGeometry(g)) { + UI.printWarning(Module.API, "Removing instance \"%s\" because it referenced geometry \"%s\"", e.getKey(), name); + remove(e.getKey()); + } + } + break; + } + case INSTANCE: + rebuildInstanceList = true; + break; + case LIGHT: + rebuildLightList = true; + break; + default: + // no dependencies + break; + } + } + + final boolean update(String name, ParameterList pl, SunflowAPI api) { + RenderObjectHandle obj = renderObjects.get(name); + boolean success; + if (obj == null) { + UI.printError(Module.API, "Unable to update \"%s\" - object was not defined yet", name); + success = false; + } else { + UI.printDetailed(Module.API, "Updating %s object \"%s\"", obj.typeName(), name); + success = obj.update(pl, api); + if (!success) { + UI.printError(Module.API, "Unable to update \"%s\" - removing", name); + remove(name); + } else { + switch (obj.type) { + case GEOMETRY: + case INSTANCE: + rebuildInstanceList = true; + break; + case LIGHT: + rebuildLightList = true; + break; + default: + break; + } + } + } + return success; + } + + final void updateScene(Scene scene) { + if (rebuildInstanceList) { + UI.printInfo(Module.API, "Building scene instance list for rendering ..."); + int numInfinite = 0, numInstance = 0; + for (FastHashMap.Entry e : renderObjects) { + Instance i = e.getValue().getInstance(); + if (i != null) { + i.updateBounds(); + if (i.getBounds() == null) + numInfinite++; + else if (!i.getBounds().isEmpty()) + numInstance++; + else + UI.printWarning(Module.API, "Ignoring empty instance: \"%s\"", e.getKey()); + } + } + Instance[] infinite = new Instance[numInfinite]; + Instance[] instance = new Instance[numInstance]; + numInfinite = numInstance = 0; + for (FastHashMap.Entry e : renderObjects) { + Instance i = e.getValue().getInstance(); + if (i != null) { + if (i.getBounds() == null) { + infinite[numInfinite] = i; + numInfinite++; + } else if (!i.getBounds().isEmpty()) { + instance[numInstance] = i; + numInstance++; + } + } + } + scene.setInstanceLists(instance, infinite); + rebuildInstanceList = false; + } + if (rebuildLightList) { + UI.printInfo(Module.API, "Building scene light list for rendering ..."); + ArrayList lightList = new ArrayList(); + for (FastHashMap.Entry e : renderObjects) { + LightSource light = e.getValue().getLight(); + if (light != null) + lightList.add(light); + + } + scene.setLightList(lightList.toArray(new LightSource[lightList.size()])); + rebuildLightList = false; + } + } + + final void put(String name, Shader shader) { + renderObjects.put(name, new RenderObjectHandle(shader)); + } + + final void put(String name, Modifier modifier) { + renderObjects.put(name, new RenderObjectHandle(modifier)); + } + + final void put(String name, PrimitiveList primitives) { + renderObjects.put(name, new RenderObjectHandle(primitives)); + } + + final void put(String name, Tesselatable tesselatable) { + renderObjects.put(name, new RenderObjectHandle(tesselatable)); + } + + final void put(String name, Instance instance) { + renderObjects.put(name, new RenderObjectHandle(instance)); + } + + final void put(String name, LightSource light) { + renderObjects.put(name, new RenderObjectHandle(light)); + } + + final void put(String name, Camera camera) { + renderObjects.put(name, new RenderObjectHandle(camera)); + } + + final void put(String name, Options options) { + renderObjects.put(name, new RenderObjectHandle(options)); + } + + final Geometry lookupGeometry(String name) { + if (name == null) + return null; + RenderObjectHandle handle = renderObjects.get(name); + return (handle == null) ? null : handle.getGeometry(); + } + + final Instance lookupInstance(String name) { + if (name == null) + return null; + RenderObjectHandle handle = renderObjects.get(name); + return (handle == null) ? null : handle.getInstance(); + } + + final Camera lookupCamera(String name) { + if (name == null) + return null; + RenderObjectHandle handle = renderObjects.get(name); + return (handle == null) ? null : handle.getCamera(); + } + + final Options lookupOptions(String name) { + if (name == null) + return null; + RenderObjectHandle handle = renderObjects.get(name); + return (handle == null) ? null : handle.getOptions(); + } + + final Shader lookupShader(String name) { + if (name == null) + return null; + RenderObjectHandle handle = renderObjects.get(name); + return (handle == null) ? null : handle.getShader(); + } + + final Modifier lookupModifier(String name) { + if (name == null) + return null; + RenderObjectHandle handle = renderObjects.get(name); + return (handle == null) ? null : handle.getModifier(); + } + + final LightSource lookupLight(String name) { + if (name == null) + return null; + RenderObjectHandle handle = renderObjects.get(name); + return (handle == null) ? null : handle.getLight(); + } + + private static final class RenderObjectHandle { + private final RenderObject obj; + private final RenderObjectType type; + + private RenderObjectHandle(Shader shader) { + obj = shader; + type = RenderObjectType.SHADER; + } + + private RenderObjectHandle(Modifier modifier) { + obj = modifier; + type = RenderObjectType.MODIFIER; + } + + private RenderObjectHandle(Tesselatable tesselatable) { + obj = new Geometry(tesselatable); + type = RenderObjectType.GEOMETRY; + } + + private RenderObjectHandle(PrimitiveList prims) { + obj = new Geometry(prims); + type = RenderObjectType.GEOMETRY; + } + + private RenderObjectHandle(Instance instance) { + obj = instance; + type = RenderObjectType.INSTANCE; + } + + private RenderObjectHandle(LightSource light) { + obj = light; + type = RenderObjectType.LIGHT; + } + + private RenderObjectHandle(Camera camera) { + obj = camera; + type = RenderObjectType.CAMERA; + } + + private RenderObjectHandle(Options options) { + obj = options; + type = RenderObjectType.OPTIONS; + } + + private boolean update(ParameterList pl, SunflowAPI api) { + return obj.update(pl, api); + } + + private String typeName() { + return type.name().toLowerCase(Locale.ENGLISH); + } + + private Shader getShader() { + return (type == RenderObjectType.SHADER) ? (Shader) obj : null; + } + + private Modifier getModifier() { + return (type == RenderObjectType.MODIFIER) ? (Modifier) obj : null; + } + + private Geometry getGeometry() { + return (type == RenderObjectType.GEOMETRY) ? (Geometry) obj : null; + } + + private Instance getInstance() { + return (type == RenderObjectType.INSTANCE) ? (Instance) obj : null; + } + + private LightSource getLight() { + return (type == RenderObjectType.LIGHT) ? (LightSource) obj : null; + } + + private Camera getCamera() { + return (type == RenderObjectType.CAMERA) ? (Camera) obj : null; + } + + private Options getOptions() { + return (type == RenderObjectType.OPTIONS) ? (Options) obj : null; + } + } + +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/SunflowAPI.java b/src/main/java/org/sunflow/SunflowAPI.java new file mode 100644 index 0000000..49ea83a --- /dev/null +++ b/src/main/java/org/sunflow/SunflowAPI.java @@ -0,0 +1,700 @@ +package org.sunflow; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.util.Locale; + +import org.codehaus.janino.ClassBodyEvaluator; +import org.codehaus.janino.CompileException; +import org.codehaus.janino.Scanner; +import org.codehaus.janino.Parser.ParseException; +import org.codehaus.janino.Scanner.ScanException; +import org.sunflow.core.Camera; +import org.sunflow.core.CameraLens; +import org.sunflow.core.Display; +import org.sunflow.core.Geometry; +import org.sunflow.core.ImageSampler; +import org.sunflow.core.Instance; +import org.sunflow.core.LightSource; +import org.sunflow.core.Modifier; +import org.sunflow.core.Options; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Scene; +import org.sunflow.core.SceneParser; +import org.sunflow.core.Shader; +import org.sunflow.core.Tesselatable; +import org.sunflow.core.ParameterList.InterpolationType; +import org.sunflow.image.ColorFactory; +import org.sunflow.image.ColorFactory.ColorSpecificationException; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point2; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.FileUtils; +import org.sunflow.system.SearchPath; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +/** + * This API gives a simple interface for creating scenes procedurally. This is + * the main entry point to Sunflow. To use this class, extend from it and + * implement the build method which may execute arbitrary code to create a + * scene. + */ +public class SunflowAPI implements SunflowAPIInterface { + public static final String VERSION = "0.07.3"; + public static final String DEFAULT_OPTIONS = "::options"; + + private Scene scene; + private SearchPath includeSearchPath; + private SearchPath textureSearchPath; + private ParameterList parameterList; + private RenderObjectMap renderObjects; + private int currentFrame; + + /** + * This is a quick system test which verifies that the user has launched + * Java properly. + */ + public static void runSystemCheck() { + final long RECOMMENDED_MAX_SIZE = 800; + long maxMb = Runtime.getRuntime().maxMemory() / 1048576; + if (maxMb < RECOMMENDED_MAX_SIZE) + UI.printError(Module.API, "JVM available memory is below %d MB (found %d MB only).\nPlease make sure you launched the program with the -Xmx command line options.", RECOMMENDED_MAX_SIZE, maxMb); + String compiler = System.getProperty("java.vm.name"); + if (compiler == null || !(compiler.contains("HotSpot") && compiler.contains("Server"))) + UI.printError(Module.API, "You do not appear to be running Sun's server JVM\nPerformance may suffer"); + UI.printDetailed(Module.API, "Java environment settings:"); + UI.printDetailed(Module.API, " * Max memory available : %d MB", maxMb); + UI.printDetailed(Module.API, " * Virtual machine name : %s", compiler == null ? "true if the update was succesfull, or + * false if the update failed + */ + private boolean update(String name) { + boolean success = renderObjects.update(name, parameterList, this); + parameterList.clear(success); + return success; + } + + public final void searchpath(String type, String path) { + if (type.equals("include")) + includeSearchPath.addSearchPath(path); + else if (type.equals("texture")) + textureSearchPath.addSearchPath(path); + else + UI.printWarning(Module.API, "Invalid searchpath type: \"%s\"", type); + } + + /** + * Attempts to resolve the specified filename by checking it against the + * texture search path. + * + * @param filename filename + * @return a path which matches the filename, or filename if no matches are + * found + */ + public final String resolveTextureFilename(String filename) { + return textureSearchPath.resolvePath(filename); + } + + /** + * Attempts to resolve the specified filename by checking it against the + * include search path. + * + * @param filename filename + * @return a path which matches the filename, or filename if no matches are + * found + */ + public final String resolveIncludeFilename(String filename) { + return includeSearchPath.resolvePath(filename); + } + + public final void shader(String name, String shaderType) { + if (!isIncremental(shaderType)) { + // we are declaring a shader for the first time + if (renderObjects.has(name)) { + UI.printError(Module.API, "Unable to declare shader \"%s\", name is already in use", name); + parameterList.clear(true); + return; + } + Shader shader = PluginRegistry.shaderPlugins.createObject(shaderType); + if (shader == null) { + UI.printError(Module.API, "Unable to create shader of type \"%s\"", shaderType); + return; + } + renderObjects.put(name, shader); + } + // update existing shader (only if it is valid) + if (lookupShader(name) != null) + update(name); + else { + UI.printError(Module.API, "Unable to update shader \"%s\" - shader object was not found", name); + parameterList.clear(true); + } + } + + public final void modifier(String name, String modifierType) { + if (!isIncremental(modifierType)) { + // we are declaring a shader for the first time + if (renderObjects.has(name)) { + UI.printError(Module.API, "Unable to declare modifier \"%s\", name is already in use", name); + parameterList.clear(true); + return; + } + Modifier modifier = PluginRegistry.modifierPlugins.createObject(modifierType); + if (modifier == null) { + UI.printError(Module.API, "Unable to create modifier of type \"%s\"", modifierType); + return; + } + renderObjects.put(name, modifier); + } + // update existing shader (only if it is valid) + if (lookupModifier(name) != null) + update(name); + else { + UI.printError(Module.API, "Unable to update modifier \"%s\" - modifier object was not found", name); + parameterList.clear(true); + } + } + + public final void geometry(String name, String typeName) { + if (!isIncremental(typeName)) { + // we are declaring a geometry for the first time + if (renderObjects.has(name)) { + UI.printError(Module.API, "Unable to declare geometry \"%s\", name is already in use", name); + parameterList.clear(true); + return; + } + // check tesselatable first + if (PluginRegistry.tesselatablePlugins.hasType(typeName)) { + Tesselatable tesselatable = PluginRegistry.tesselatablePlugins.createObject(typeName); + if (tesselatable == null) { + UI.printError(Module.API, "Unable to create tesselatable object of type \"%s\"", typeName); + return; + } + renderObjects.put(name, tesselatable); + } else { + PrimitiveList primitives = PluginRegistry.primitivePlugins.createObject(typeName); + if (primitives == null) { + UI.printError(Module.API, "Unable to create primitive of type \"%s\"", typeName); + return; + } + renderObjects.put(name, primitives); + } + } + if (lookupGeometry(name) != null) + update(name); + else { + UI.printError(Module.API, "Unable to update geometry \"%s\" - geometry object was not found", name); + parameterList.clear(true); + } + } + + public final void instance(String name, String geoname) { + if (!isIncremental(geoname)) { + // we are declaring this instance for the first time + if (renderObjects.has(name)) { + UI.printError(Module.API, "Unable to declare instance \"%s\", name is already in use", name); + parameterList.clear(true); + return; + } + parameter("geometry", geoname); + renderObjects.put(name, new Instance()); + } + if (lookupInstance(name) != null) + update(name); + else { + UI.printError(Module.API, "Unable to update instance \"%s\" - instance object was not found", name); + parameterList.clear(true); + } + } + + public final void light(String name, String lightType) { + if (!isIncremental(lightType)) { + // we are declaring this light for the first time + if (renderObjects.has(name)) { + UI.printError(Module.API, "Unable to declare light \"%s\", name is already in use", name); + parameterList.clear(true); + return; + } + LightSource light = PluginRegistry.lightSourcePlugins.createObject(lightType); + if (light == null) { + UI.printError(Module.API, "Unable to create light source of type \"%s\"", lightType); + return; + } + renderObjects.put(name, light); + } + if (lookupLight(name) != null) + update(name); + else { + UI.printError(Module.API, "Unable to update instance \"%s\" - instance object was not found", name); + parameterList.clear(true); + } + } + + public final void camera(String name, String lensType) { + if (!isIncremental(lensType)) { + // we are declaring this camera for the first time + if (renderObjects.has(name)) { + UI.printError(Module.API, "Unable to declare camera \"%s\", name is already in use", name); + parameterList.clear(true); + return; + } + CameraLens lens = PluginRegistry.cameraLensPlugins.createObject(lensType); + if (lens == null) { + UI.printError(Module.API, "Unable to create a camera lens of type \"%s\"", lensType); + return; + } + renderObjects.put(name, new Camera(lens)); + } + // update existing shader (only if it is valid) + if (lookupCamera(name) != null) + update(name); + else { + UI.printError(Module.API, "Unable to update camera \"%s\" - camera object was not found", name); + parameterList.clear(true); + } + } + + public final void options(String name) { + if (lookupOptions(name) == null) { + if (renderObjects.has(name)) { + UI.printError(Module.API, "Unable to declare options \"%s\", name is already in use", name); + parameterList.clear(true); + return; + } + renderObjects.put(name, new Options()); + } + assert lookupOptions(name) != null; + update(name); + } + + private final boolean isIncremental(String typeName) { + return typeName == null || typeName.equals("incremental"); + } + + /** + * Retrieve a geometry object by its name, or null if no + * geometry was found, or if the specified object is not a geometry. + * + * @param name geometry name + * @return the geometry object associated with that name + */ + public final Geometry lookupGeometry(String name) { + return renderObjects.lookupGeometry(name); + } + + /** + * Retrieve an instance object by its name, or null if no + * instance was found, or if the specified object is not an instance. + * + * @param name instance name + * @return the instance object associated with that name + */ + private final Instance lookupInstance(String name) { + return renderObjects.lookupInstance(name); + } + + /** + * Retrieve a shader object by its name, or null if no shader + * was found, or if the specified object is not a shader. + * + * @param name camera name + * @return the camera object associate with that name + */ + private final Camera lookupCamera(String name) { + return renderObjects.lookupCamera(name); + } + + private final Options lookupOptions(String name) { + return renderObjects.lookupOptions(name); + } + + /** + * Retrieve a shader object by its name, or null if no shader + * was found, or if the specified object is not a shader. + * + * @param name shader name + * @return the shader object associated with that name + */ + public final Shader lookupShader(String name) { + return renderObjects.lookupShader(name); + } + + /** + * Retrieve a modifier object by its name, or null if no + * modifier was found, or if the specified object is not a modifier. + * + * @param name modifier name + * @return the modifier object associated with that name + */ + public final Modifier lookupModifier(String name) { + return renderObjects.lookupModifier(name); + } + + /** + * Retrieve a light object by its name, or null if no shader + * was found, or if the specified object is not a light. + * + * @param name light name + * @return the light object associated with that name + */ + private final LightSource lookupLight(String name) { + return renderObjects.lookupLight(name); + } + + public final void render(String optionsName, Display display) { + renderObjects.updateScene(scene); + Options opt = lookupOptions(optionsName); + if (opt == null) + opt = new Options(); + scene.setCamera(lookupCamera(opt.getString("camera", null))); + + // shader override + String shaderOverrideName = opt.getString("override.shader", "none"); + boolean overridePhotons = opt.getBoolean("override.photons", false); + + if (shaderOverrideName.equals("none")) + scene.setShaderOverride(null, false); + else { + Shader shader = lookupShader(shaderOverrideName); + if (shader == null) + UI.printWarning(Module.API, "Unable to find shader \"%s\" for override, disabling", shaderOverrideName); + scene.setShaderOverride(shader, overridePhotons); + } + + // baking + String bakingInstanceName = opt.getString("baking.instance", null); + if (bakingInstanceName != null) { + Instance bakingInstance = lookupInstance(bakingInstanceName); + if (bakingInstance == null) { + UI.printError(Module.API, "Unable to bake instance \"%s\" - not found", bakingInstanceName); + return; + } + scene.setBakingInstance(bakingInstance); + } else + scene.setBakingInstance(null); + + ImageSampler sampler = PluginRegistry.imageSamplerPlugins.createObject(opt.getString("sampler", "bucket")); + scene.render(opt, sampler, display); + } + + public final boolean include(String filename) { + if (filename == null) + return false; + filename = includeSearchPath.resolvePath(filename); + String extension = FileUtils.getExtension(filename); + SceneParser parser = PluginRegistry.parserPlugins.createObject(extension); + if (parser == null) { + UI.printError(Module.API, "Unable to find a suitable parser for: \"%s\" (extension: %s)", filename, extension); + return false; + } + String currentFolder = new File(filename).getAbsoluteFile().getParentFile().getAbsolutePath(); + includeSearchPath.addSearchPath(currentFolder); + textureSearchPath.addSearchPath(currentFolder); + return parser.parse(filename, this); + } + + /** + * Retrieve the bounding box of the scene. This method will be valid only + * after a first call to {@link #render(String, Display)} has been made. + */ + public final BoundingBox getBounds() { + return scene.getBounds(); + } + + /** + * This method does nothing, but may be overriden to create scenes + * procedurally. + */ + public void build() { + } + + /** + * Create an API object from the specified file. Java files are read by + * Janino and are expected to implement a build method (they implement a + * derived class of SunflowAPI. The build method is called if the code + * compiles succesfully. Other files types are handled by the parse method. + * + * @param filename filename to load + * @return a valid SunflowAPI object or null on failure + */ + public static SunflowAPI create(String filename, int frameNumber) { + if (filename == null) + return new SunflowAPI(); + SunflowAPI api = null; + if (filename.endsWith(".java")) { + Timer t = new Timer(); + UI.printInfo(Module.API, "Compiling \"" + filename + "\" ..."); + t.start(); + try { + FileInputStream stream = new FileInputStream(filename); + api = (SunflowAPI) ClassBodyEvaluator.createFastClassBodyEvaluator(new Scanner(filename, stream), SunflowAPI.class, ClassLoader.getSystemClassLoader()); + stream.close(); + } catch (CompileException e) { + UI.printError(Module.API, "Could not compile: \"%s\"", filename); + UI.printError(Module.API, "%s", e.getMessage()); + return null; + } catch (ParseException e) { + UI.printError(Module.API, "Could not compile: \"%s\"", filename); + UI.printError(Module.API, "%s", e.getMessage()); + return null; + } catch (ScanException e) { + UI.printError(Module.API, "Could not compile: \"%s\"", filename); + UI.printError(Module.API, "%s", e.getMessage()); + return null; + } catch (IOException e) { + UI.printError(Module.API, "Could not compile: \"%s\"", filename); + UI.printError(Module.API, "%s", e.getMessage()); + return null; + } + t.end(); + UI.printInfo(Module.API, "Compile time: " + t.toString()); + // allow relative paths + String currentFolder = new File(filename).getAbsoluteFile().getParentFile().getAbsolutePath(); + api.includeSearchPath.addSearchPath(currentFolder); + api.textureSearchPath.addSearchPath(currentFolder); + UI.printInfo(Module.API, "Build script running ..."); + t.start(); + api.currentFrame(frameNumber); + api.build(); + t.end(); + UI.printInfo(Module.API, "Build script time: %s", t.toString()); + } else { + api = new SunflowAPI(); + api = api.include(filename) ? api : null; + } + return api; + } + + /** + * Translate specfied file into the native sunflow scene file format. + * + * @param filename input filename + * @param outputFilename output filename + * @return true upon success, false otherwise + */ + public static boolean translate(String filename, String outputFilename) { + FileSunflowAPI api = null; + try { + if (outputFilename.endsWith(".sca")) + api = new AsciiFileSunflowAPI(outputFilename); + else if (outputFilename.endsWith(".scb")) + api = new BinaryFileSunflowAPI(outputFilename); + else { + UI.printError(Module.API, "Unable to determine output filetype: \"%s\"", outputFilename); + return false; + } + } catch (IOException e) { + UI.printError(Module.API, "Unable to create output file - %s", e.getMessage()); + return false; + } + String extension = filename.substring(filename.lastIndexOf('.') + 1); + SceneParser parser = PluginRegistry.parserPlugins.createObject(extension); + if (parser == null) { + UI.printError(Module.API, "Unable to find a suitable parser for: \"%s\"", filename); + return false; + } + try { + return parser.parse(filename, api); + } catch (RuntimeException e) { + e.printStackTrace(); + UI.printError(Module.API, "Error occured during translation: %s", e.getMessage()); + return false; + } finally { + api.close(); + } + } + + /** + * Compile the specified code string via Janino. The code must implement a + * build method as described above. The build method is not called on the + * output, it is up the caller to do so. + * + * @param code java code string + * @return a valid SunflowAPI object upon succes, null + * otherwise. + */ + public static SunflowAPI compile(String code) { + try { + Timer t = new Timer(); + t.start(); + SunflowAPI api = (SunflowAPI) ClassBodyEvaluator.createFastClassBodyEvaluator(new Scanner(null, new StringReader(code)), SunflowAPI.class, (ClassLoader) null); + t.end(); + UI.printInfo(Module.API, "Compile time: %s", t.toString()); + return api; + } catch (CompileException e) { + UI.printError(Module.API, "%s", e.getMessage()); + return null; + } catch (ParseException e) { + UI.printError(Module.API, "%s", e.getMessage()); + return null; + } catch (ScanException e) { + UI.printError(Module.API, "%s", e.getMessage()); + return null; + } catch (IOException e) { + UI.printError(Module.API, "%s", e.getMessage()); + return null; + } + } + + /** + * Read the value of the current frame. This value is intended only for + * procedural animation creation. It is not used by the Sunflow core in + * anyway. The default value is 1. + * + * @return current frame number + */ + public int currentFrame() { + return currentFrame; + } + + public void currentFrame(int currentFrame) { + this.currentFrame = currentFrame; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/SunflowAPIInterface.java b/src/main/java/org/sunflow/SunflowAPIInterface.java new file mode 100644 index 0000000..fd32f8f --- /dev/null +++ b/src/main/java/org/sunflow/SunflowAPIInterface.java @@ -0,0 +1,275 @@ +package org.sunflow; + +import org.sunflow.core.Display; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.RenderObject; +import org.sunflow.core.Tesselatable; +import org.sunflow.core.ParameterList.InterpolationType; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point2; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +/** + * This interface represents the entry point for rendering scenes using Sunflow. + * Classes which implement this interface are able to receive input from any of + * the Sunflow parsers. + */ +public interface SunflowAPIInterface { + /** + * Reset the state of the API completely. The object table is cleared, and + * all search paths are set back to their default values. + */ + public void reset(); + + /** + * Declare a plugin of the specified type with the given name from a java + * code string. The code will be compiled with Janino and registered as a + * new plugin type upon success. + * + * @param type + * @param name + * @param code + */ + public void plugin(String type, String name, String code); + + /** + * Declare a parameter with the specified name and value. This parameter + * will be added to the currently active parameter list. + * + * @param name parameter name + * @param value parameter value + */ + public void parameter(String name, String value); + + /** + * Declare a parameter with the specified name and value. This parameter + * will be added to the currently active parameter list. + * + * @param name parameter name + * @param value parameter value + */ + public void parameter(String name, boolean value); + + /** + * Declare a parameter with the specified name and value. This parameter + * will be added to the currently active parameter list. + * + * @param name parameter name + * @param value parameter value + */ + public void parameter(String name, int value); + + /** + * Declare a parameter with the specified name and value. This parameter + * will be added to the currently active parameter list. + * + * @param name parameter name + * @param value parameter value + */ + public void parameter(String name, float value); + + /** + * Declare a color parameter in the given colorspace using the specified + * name and value. This parameter will be added to the currently active + * parameter list. + * + * @param name parameter name + * @param colorspace color space or null to assume internal + * color space + * @param data floating point color data + */ + public void parameter(String name, String colorspace, float... data); + + /** + * Declare a parameter with the specified name and value. This parameter + * will be added to the currently active parameter list. + * + * @param name parameter name + * @param value parameter value + */ + public void parameter(String name, Point3 value); + + /** + * Declare a parameter with the specified name and value. This parameter + * will be added to the currently active parameter list. + * + * @param name parameter name + * @param value parameter value + */ + public void parameter(String name, Vector3 value); + + /** + * Declare a parameter with the specified name and value. This parameter + * will be added to the currently active parameter list. + * + * @param name parameter name + * @param value parameter value + */ + public void parameter(String name, Point2 value); + + /** + * Declare a parameter with the specified name and value. This parameter + * will be added to the currently active parameter list. + * + * @param name parameter name + * @param value parameter value + */ + public void parameter(String name, Matrix4 value); + + /** + * Declare a parameter with the specified name and value. This parameter + * will be added to the currently active parameter list. + * + * @param name parameter name + * @param value parameter value + */ + public void parameter(String name, int[] value); + + /** + * Declare a parameter with the specified name and value. This parameter + * will be added to the currently active parameter list. + * + * @param name parameter name + * @param value parameter value + */ + public void parameter(String name, String[] value); + + /** + * Declare a parameter with the specified name. The type may be one of the + * follow: "float", "point", "vector", "texcoord", "matrix". The + * interpolation determines how the parameter is to be interpreted over + * surface (see {@link InterpolationType}). The data is specified in a + * flattened float array. + * + * @param name parameter name + * @param type parameter data type + * @param interpolation parameter interpolation mode + * @param data raw floating point data + */ + public void parameter(String name, String type, String interpolation, float[] data); + + /** + * Remove the specified render object. Note that this may cause the removal + * of other objects which depended on it. + * + * @param name name of the object to remove + */ + public void remove(String name); + + /** + * Add the specified path to the list of directories which are searched + * automatically to resolve scene filenames or textures. Currently the + * supported searchpath types are: "include" and "texture". All other types + * will be ignored. + * + * @param path + */ + public void searchpath(String type, String path); + + /** + * Defines a shader with a given name. If the shader type name is left + * null, the shader with the given name will be updated (if + * it exists). + * + * @param name a unique name given to the shader + * @param shaderType a shader plugin type + */ + public void shader(String name, String shaderType); + + /** + * Defines a modifier with a given name. If the modifier type name is left + * null, the modifier with the given name will be updated + * (if it exists). + * + * @param name a unique name given to the modifier + * @param modifierType a modifier plugin type name + */ + public void modifier(String name, String modifierType); + + /** + * Defines a geometry with a given name. The geometry is built from the + * specified type. Note that geometries may be created from + * {@link Tesselatable} objects or {@link PrimitiveList} objects. This means + * that two seperate plugin lists will be searched for the geometry type. + * {@link Tesselatable} objects are search first. If the type name is left + * null, the geometry with the given name will be updated + * (if it exists). + * + * @param name a unique name given to the geometry + * @param typeName a tesselatable or primitive plugin type name + */ + public void geometry(String name, String typeName); + + /** + * Instance the specified geometry into the scene. If geoname is + * null, the specified instance object will be updated (if + * it exists). In order to change the instancing relationship of an existing + * instance, you should use the "geometry" string attribute. + * + * @param name instance name + * @param geoname name of the geometry to instance + */ + public void instance(String name, String geoname); + + /** + * Defines a light source with a given name. If the light type name is left + * null, the light source with the given name will be + * updated (if it exists). + * + * @param name a unique name given to the light source + * @param lightType a light source plugin type name + */ + public void light(String name, String lightType); + + /** + * Defines a camera with a given name. The camera is built from the + * specified camera lens type plugin. If the lens type name is left + * null, the camera with the given name will be updated (if + * it exists). It is not currently possible to change the lens of a camera + * after it has been created. + * + * @param name camera name + * @param lensType a camera lens plugin type name + */ + public void camera(String name, String lensType); + + /** + * Defines an option object to hold the current parameters. If the object + * already exists, the values will simply override previous ones. + * + * @param name + */ + public void options(String name); + + /** + * Render using the specified options and the specified display. If the + * specified options do not exist - defaults will be used. + * + * @param optionsName name of the {@link RenderObject} which contains the + * options + * @param display display object + */ + public void render(String optionsName, Display display); + + /** + * Parse the specified filename. The include paths are searched first. The + * contents of the file are simply added to the active scene. This allows to + * break up a scene into parts, even across file formats. The appropriate + * parser is chosen based on file extension. + * + * @param filename filename to load + * @return true upon sucess, false if an error + * occured. + */ + public boolean include(String filename); + + /** + * Set the value of the current frame. This value is intended only for + * procedural animation creation. It is not used by the Sunflow core in + * anyway. The default value is 1. + * + * @param currentFrame current frame number + */ + public void currentFrame(int currentFrame); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/AccelerationStructure.java b/src/main/java/org/sunflow/core/AccelerationStructure.java new file mode 100644 index 0000000..cae979f --- /dev/null +++ b/src/main/java/org/sunflow/core/AccelerationStructure.java @@ -0,0 +1,19 @@ +package org.sunflow.core; + +public interface AccelerationStructure { + /** + * Construct an acceleration structure for the specified primitive list. + * + * @param primitives + */ + public void build(PrimitiveList primitives); + + /** + * Intersect the specified ray with the geometry in local space. The ray + * will be provided in local space. + * + * @param r ray in local space + * @param istate state to store the intersection into + */ + public void intersect(Ray r, IntersectionState istate); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/AccelerationStructureFactory.java b/src/main/java/org/sunflow/core/AccelerationStructureFactory.java new file mode 100644 index 0000000..90b6be9 --- /dev/null +++ b/src/main/java/org/sunflow/core/AccelerationStructureFactory.java @@ -0,0 +1,33 @@ +package org.sunflow.core; + +import org.sunflow.PluginRegistry; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +class AccelerationStructureFactory { + static final AccelerationStructure create(String name, int n, boolean primitives) { + if (name == null || name.equals("auto")) { + if (primitives) { + if (n > 20000000) + name = "uniformgrid"; + else if (n > 2000000) + name = "bih"; + else if (n > 2) + name = "kdtree"; + else + name = "null"; + } else { + if (n > 2) + name = "bih"; + else + name = "null"; + } + } + AccelerationStructure accel = PluginRegistry.accelPlugins.createObject(name); + if (accel == null) { + UI.printWarning(Module.ACCEL, "Unrecognized intersection accelerator \"%s\" - using auto", name); + return create(null, n, primitives); + } + return accel; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/BucketOrder.java b/src/main/java/org/sunflow/core/BucketOrder.java new file mode 100644 index 0000000..d09a362 --- /dev/null +++ b/src/main/java/org/sunflow/core/BucketOrder.java @@ -0,0 +1,20 @@ +package org.sunflow.core; + +/** + * Creates an array of coordinates that iterate over the tiled screen. Classes + * which implement this interface are responsible for guarenteeing the entire + * screen is tiled. No attempt is made to check for duplicates or incomplete + * coverage. + */ +public interface BucketOrder { + /** + * Computes the order in which each coordinate on the screen should be + * visited. + * + * @param nbw number of buckets in the X direction + * @param nbh number of buckets in the Y direction + * @return array of coordinates with interleaved X, Y of the positions of + * buckets to be rendered. + */ + int[] getBucketSequence(int nbw, int nbh); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/Camera.java b/src/main/java/org/sunflow/core/Camera.java new file mode 100644 index 0000000..512ac74 --- /dev/null +++ b/src/main/java/org/sunflow/core/Camera.java @@ -0,0 +1,120 @@ +package org.sunflow.core; + +import org.sunflow.SunflowAPI; +import org.sunflow.math.Matrix4; +import org.sunflow.math.MovingMatrix4; +import org.sunflow.math.Point3; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +/** + * This class represents a camera to the renderer. It handles the mapping of + * camera space to world space, as well as the mounting of {@link CameraLens} + * objects which compute the actual projection. + */ +public class Camera implements RenderObject { + private final CameraLens lens; + private float shutterOpen; + private float shutterClose; + private MovingMatrix4 c2w; + private MovingMatrix4 w2c; + + public Camera(CameraLens lens) { + this.lens = lens; + c2w = new MovingMatrix4(null); + w2c = new MovingMatrix4(null); + shutterOpen = shutterClose = 0; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + shutterOpen = pl.getFloat("shutter.open", shutterOpen); + shutterClose = pl.getFloat("shutter.close", shutterClose); + c2w = pl.getMovingMatrix("transform", c2w); + w2c = c2w.inverse(); + if (w2c == null) { + UI.printWarning(Module.CAM, "Unable to compute camera's inverse transform"); + return false; + } + return lens.update(pl, api); + } + + /** + * Computes actual time from a time sample in the interval [0,1). This + * random number is mapped somewhere between the shutterOpen and + * shutterClose times. + * + * @param time + * @return + */ + public float getTime(float time) { + if (shutterOpen >= shutterClose) + return shutterOpen; + // warp the time sample by a tent filter - this helps simulates the + // behaviour of a standard shutter as explained here: + // "Shutter Efficiency and Temporal Sampling" by "Ian Stephenson" + // http://www.dctsystems.co.uk/Text/shutter.pdf + if (time < 0.5) + time = -1 + (float) Math.sqrt(2 * time); + else + time = 1 - (float) Math.sqrt(2 - 2 * time); + time = 0.5f * (time + 1); + return (1 - time) * shutterOpen + time * shutterClose; + } + + /** + * Generate a ray passing though the specified point on the image plane. + * Additional random variables are provided for the lens to optionally + * compute depth-of-field or motion blur effects. Note that the camera may + * return null for invalid arguments or for pixels which + * don't project to anything. + * + * @param x x pixel coordinate + * @param y y pixel coordinate + * @param imageWidth width of the image in pixels + * @param imageHeight height of the image in pixels + * @param lensX a random variable in [0,1) to be used for DOF sampling + * @param lensY a random variable in [0,1) to be used for DOF sampling + * @param time a random variable in [0,1) to be used for motion blur + * sampling + * @return a ray passing through the specified pixel, or null + */ + public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lensX, double lensY, float time) { + Ray r = lens.getRay(x, y, imageWidth, imageHeight, lensX, lensY, time); + if (r != null) { + // transform from camera space to world space + r = r.transform(c2w.sample(time)); + // renormalize to account for scale factors embeded in the transform + r.normalize(); + } + return r; + } + + /** + * Generate a ray from the origin of camera space toward the specified + * point. + * + * @param p point in world space + * @return ray from the origin of camera space to the specified point + */ + Ray getRay(Point3 p, float time) { + return new Ray(c2w == null ? new Point3(0, 0, 0) : c2w.sample(time).transformP(new Point3(0, 0, 0)), p); + } + + /** + * Returns a transformation matrix mapping camera space to world space. + * + * @return a transformation matrix + */ + Matrix4 getCameraToWorld(float time) { + return c2w == null ? Matrix4.IDENTITY : c2w.sample(time); + } + + /** + * Returns a transformation matrix mapping world space to camera space. + * + * @return a transformation matrix + */ + Matrix4 getWorldToCamera(float time) { + return w2c == null ? Matrix4.IDENTITY : w2c.sample(time); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/CameraLens.java b/src/main/java/org/sunflow/core/CameraLens.java new file mode 100644 index 0000000..bf44c17 --- /dev/null +++ b/src/main/java/org/sunflow/core/CameraLens.java @@ -0,0 +1,28 @@ +package org.sunflow.core; + +/** + * Represents a mapping from the 3D scene onto the final image. A camera lens is + * responsible for determining what ray to cast through each pixel. + */ +public interface CameraLens extends RenderObject { + /** + * Create a new {@link Ray ray}to be cast through pixel (x,y) on the image + * plane. Two sampling parameters are provided for lens sampling. They are + * guarenteed to be in the interval [0,1). They can be used to perturb the + * position of the source of the ray on the lens of the camera for DOF + * effects. A third sampling parameter is provided for motion blur effects. + * Note that the {@link Camera} class already handles camera movement motion + * blur. Rays should be generated in camera space - that is, with the eye at + * the origin, looking down the -Z axis, with +Y pointing up. + * + * @param x x coordinate of the (sub)pixel + * @param y y coordinate of the (sub)pixel + * @param imageWidth image width in pixels + * @param imageHeight image height in pixels + * @param lensX x lens sampling parameter + * @param lensY y lens sampling parameter + * @param time time sampling parameter + * @return a new ray passing through the given pixel + */ + public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lensX, double lensY, double time); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/CausticPhotonMapInterface.java b/src/main/java/org/sunflow/core/CausticPhotonMapInterface.java new file mode 100644 index 0000000..682da08 --- /dev/null +++ b/src/main/java/org/sunflow/core/CausticPhotonMapInterface.java @@ -0,0 +1,14 @@ +package org.sunflow.core; + +/** + * This class is a generic interface to caustic photon mapping capabilities. + */ +public interface CausticPhotonMapInterface extends PhotonStore { + /** + * Retrieve caustic photons at the specified shading location and add them + * as diffuse light samples. + * + * @param state + */ + void getSamples(ShadingState state); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/Display.java b/src/main/java/org/sunflow/core/Display.java new file mode 100644 index 0000000..5fdf90a --- /dev/null +++ b/src/main/java/org/sunflow/core/Display.java @@ -0,0 +1,77 @@ +package org.sunflow.core; + +import org.sunflow.image.Color; + +/** + * Represents an image output device. + */ +public interface Display { + /** + * This is called before an image is rendered to indicate how large the + * rendered image will be. This allows the display driver to write out image + * headers or allocate surfaces. Bucket size will be 0 when called from a + * non-bucket based source. + * + * @param w width of the rendered image in pixels + * @param h height of the rendered image in pixels + * @param bucketSize size of the buckets in pixels + */ + void imageBegin(int w, int h, int bucketSize); + + /** + * Prepare the specified area to be rendered. This may be used to highlight + * the work in progress area or simply to setup the display driver to + * receive the specified portion of the image + * + * @param x x coordinate of the bucket within the image + * @param y y coordinate of the bucket within the image + * @param w width of the bucket in pixels + * @param h height of the bucket in pixels + * @param id unique identifier corresponding to the thread which invoked + * this call + */ + void imagePrepare(int x, int y, int w, int h, int id); + + /** + * Update the current image with a bucket of data. The region is guarenteed + * to be within the bounds created by the call to imageBegin. No clipping is + * necessary. Colors are passed in unprocessed. It is up the display driver + * to do any type of quantization, gamma compensation or tone-mapping + * needed. The array of colors will be exactly w * h long and + * in row major order. + * + * @param x x coordinate of the bucket within the image + * @param y y coordinate of the bucket within the image + * @param w width of the bucket in pixels + * @param h height of the bucket in pixels + * @param data bucket data, this array will be exactly w * h + * long + * @param alpha pixel coverage data, this array will be exactly + * w * h long + */ + void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha); + + /** + * Update the current image with a region of flat color. This is used by + * progressive rendering to render progressively smaller regions of the + * screen which will overlap. The region is guarenteed to be within the + * bounds created by the call to imageBegin. No clipping is necessary. + * Colors are passed in unprocessed. It is up the display driver to do any + * type of quantization , gamma compensation or tone-mapping needed. + * + * @param x x coordinate of the region within the image + * @param y y coordinate of the region within the image + * @param w with of the region in pixels + * @param h height of the region in pixels + * @param c color to fill the region with + * @param alpha pixel coverage + */ + void imageFill(int x, int y, int w, int h, Color c, float alpha); + + /** + * This call is made after the image has been rendered. This allows the + * display driver to close any open files, write the image to disk or flush + * any other type of buffers. + */ + void imageEnd(); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/Filter.java b/src/main/java/org/sunflow/core/Filter.java new file mode 100644 index 0000000..9271f60 --- /dev/null +++ b/src/main/java/org/sunflow/core/Filter.java @@ -0,0 +1,26 @@ +package org.sunflow.core; + +/** + * Represents a multi-pixel image filter kernel. + */ +public interface Filter { + /** + * Width in pixels of the filter extents. The filter will be applied to the + * range of pixels within a box of +/- getSize() / 2 around + * the center of the pixel. + * + * @return width in pixels + */ + public float getSize(); + + /** + * Get value of the filter at offset (x, y). The filter should never be + * called with values beyond its extents but should return 0 in those cases + * anyway. + * + * @param x x offset in pixels + * @param y y offset in pixels + * @return value of the filter at the specified location + */ + public float get(float x, float y); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/GIEngine.java b/src/main/java/org/sunflow/core/GIEngine.java new file mode 100644 index 0000000..389db1a --- /dev/null +++ b/src/main/java/org/sunflow/core/GIEngine.java @@ -0,0 +1,41 @@ +package org.sunflow.core; + +import org.sunflow.image.Color; + +/** + * This represents a global illumination algorithm. It provides an interface to + * compute indirect diffuse bounces of light and make those results available to + * shaders. + */ +public interface GIEngine { + /** + * This is an optional method for engines that contain a secondary + * illumination engine which can return an approximation of the global + * radiance in the scene (like a photon map). Engines can safely return + * Color.BLACK if they can't or don't wish to support this. + * + * @param state shading state + * @return color approximating global radiance + */ + public Color getGlobalRadiance(ShadingState state); + + /** + * Initialize the engine. This is called before rendering begins. + * + * @return true if the init phase succeeded, + * false otherwise + */ + public boolean init(Options options, Scene scene); + + /** + * Return the incomming irradiance due to indirect diffuse illumination at + * the specified surface point. + * + * @param state current render state describing the point to be computed + * @param diffuseReflectance diffuse albedo of the point being shaded, this + * can be used for importance tracking + * @return irradiance from indirect diffuse illumination at the specified + * point + */ + public Color getIrradiance(ShadingState state, Color diffuseReflectance); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/Geometry.java b/src/main/java/org/sunflow/core/Geometry.java new file mode 100644 index 0000000..abde995 --- /dev/null +++ b/src/main/java/org/sunflow/core/Geometry.java @@ -0,0 +1,144 @@ +package org.sunflow.core; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.accel.NullAccelerator; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +/** + * This class represent a geometric object in its native object space. These + * object are not rendered directly, they must be instanced via {@link Instance}. + * This class performs all the bookkeeping needed for on-demand tesselation and + * acceleration structure building. + */ +public class Geometry implements RenderObject { + private Tesselatable tesselatable; + private PrimitiveList primitives; + private AccelerationStructure accel; + private int builtAccel; + private int builtTess; + private String acceltype; + + /** + * Create a geometry from the specified tesselatable object. The actual + * renderable primitives will be generated on demand. + * + * @param tesselatable tesselation object + */ + public Geometry(Tesselatable tesselatable) { + this.tesselatable = tesselatable; + primitives = null; + accel = null; + builtAccel = 0; + builtTess = 0; + acceltype = null; + } + + /** + * Create a geometry from the specified primitive aggregate. The + * acceleration structure for this object will be built on demand. + * + * @param primitives primitive list object + */ + public Geometry(PrimitiveList primitives) { + tesselatable = null; + this.primitives = primitives; + accel = null; + builtAccel = 0; + builtTess = 1; // already tesselated + } + + public boolean update(ParameterList pl, SunflowAPI api) { + acceltype = pl.getString("accel", acceltype); + // clear up old tesselation if it exists + if (tesselatable != null) { + primitives = null; + builtTess = 0; + } + // clear acceleration structure so it will be rebuilt + accel = null; + builtAccel = 0; + if (tesselatable != null) + return tesselatable.update(pl, api); + // update primitives + return primitives.update(pl, api); + } + + int getNumPrimitives() { + return primitives == null ? 0 : primitives.getNumPrimitives(); + } + + BoundingBox getWorldBounds(Matrix4 o2w) { + if (primitives == null) { + + BoundingBox b = tesselatable.getWorldBounds(o2w); + if (b != null) + return b; + if (builtTess == 0) + tesselate(); + if (primitives == null) + return null; // failed tesselation, return infinite bounding + // box + } + return primitives.getWorldBounds(o2w); + } + + void intersect(Ray r, IntersectionState state) { + if (builtTess == 0) + tesselate(); + if (builtAccel == 0) + build(); + accel.intersect(r, state); + } + + private synchronized void tesselate() { + // double check flag + if (builtTess != 0) + return; + if (tesselatable != null && primitives == null) { + UI.printInfo(Module.GEOM, "Tesselating geometry ..."); + primitives = tesselatable.tesselate(); + if (primitives == null) + UI.printError(Module.GEOM, "Tesselation failed - geometry will be discarded"); + else + UI.printDetailed(Module.GEOM, "Tesselation produced %d primitives", primitives.getNumPrimitives()); + } + builtTess = 1; + } + + private synchronized void build() { + // double check flag + if (builtAccel != 0) + return; + if (primitives != null) { + int n = primitives.getNumPrimitives(); + if (n >= 1000) + UI.printInfo(Module.GEOM, "Building acceleration structure for %d primitives ...", n); + accel = AccelerationStructureFactory.create(acceltype, n, true); + accel.build(primitives); + } else { + // create an empty accelerator to avoid having to check for null + // pointers in the intersect method + accel = new NullAccelerator(); + } + builtAccel = 1; + } + + void prepareShadingState(ShadingState state) { + primitives.prepareShadingState(state); + } + + PrimitiveList getBakingPrimitives() { + if (builtTess == 0) + tesselate(); + if (primitives == null) + return null; + return primitives.getBakingPrimitives(); + } + + PrimitiveList getPrimitiveList() { + return primitives; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/GlobalPhotonMapInterface.java b/src/main/java/org/sunflow/core/GlobalPhotonMapInterface.java new file mode 100644 index 0000000..70a4266 --- /dev/null +++ b/src/main/java/org/sunflow/core/GlobalPhotonMapInterface.java @@ -0,0 +1,21 @@ +package org.sunflow.core; + +import org.sunflow.image.Color; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +/** + * Represents a global photon map. This is a structure which can return a rough + * approximation of the diffuse radiance at a given surface point. + */ +public interface GlobalPhotonMapInterface extends PhotonStore { + + /** + * Lookup the global diffuse radiance at the specified surface point. + * + * @param p surface position + * @param n surface normal + * @return an approximation of global diffuse radiance at this point + */ + public Color getRadiance(Point3 p, Vector3 n); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/ImageSampler.java b/src/main/java/org/sunflow/core/ImageSampler.java new file mode 100644 index 0000000..87f65f4 --- /dev/null +++ b/src/main/java/org/sunflow/core/ImageSampler.java @@ -0,0 +1,25 @@ +package org.sunflow.core; + +/** + * This interface represents an image sampling algorithm capable of rendering + * the entire image. Implementations are responsible for anti-aliasing and + * filtering. + */ +public interface ImageSampler { + /** + * Prepare the sampler for rendering an image of w x h pixels + * + * @param w width of the image + * @param h height of the image + */ + public boolean prepare(Options options, Scene scene, int w, int h); + + /** + * Render the image to the specified display. The sampler can assume the + * display has been opened and that it will be closed after the method + * returns. + * + * @param display Display driver to send image data to + */ + public void render(Display display); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/Instance.java b/src/main/java/org/sunflow/core/Instance.java new file mode 100644 index 0000000..51dec8f --- /dev/null +++ b/src/main/java/org/sunflow/core/Instance.java @@ -0,0 +1,213 @@ +package org.sunflow.core; + +import org.sunflow.SunflowAPI; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.MovingMatrix4; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +/** + * This represents an instance of a {@link Geometry} into the scene. This class + * maps object space to world space and maintains a list of shaders and + * modifiers attached to the surface. + */ +public class Instance implements RenderObject { + private MovingMatrix4 o2w; + private MovingMatrix4 w2o; + private BoundingBox bounds; + private Geometry geometry; + private Shader[] shaders; + private Modifier[] modifiers; + + public Instance() { + o2w = new MovingMatrix4(null); + w2o = new MovingMatrix4(null); + bounds = null; + geometry = null; + shaders = null; + modifiers = null; + } + + public static Instance createTemporary(PrimitiveList primitives, Matrix4 transform, Shader shader) { + Instance i = new Instance(); + i.o2w = new MovingMatrix4(transform); + i.w2o = i.o2w.inverse(); + if (i.w2o == null) { + UI.printError(Module.GEOM, "Unable to compute transform inverse"); + return null; + } + i.geometry = new Geometry(primitives); + i.shaders = new Shader[] { shader }; + i.updateBounds(); + return i; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + String geometryName = pl.getString("geometry", null); + if (geometry == null || geometryName != null) { + if (geometryName == null) { + UI.printError(Module.GEOM, "geometry parameter missing - unable to create instance"); + return false; + } + geometry = api.lookupGeometry(geometryName); + if (geometry == null) { + UI.printError(Module.GEOM, "Geometry \"%s\" was not declared yet - instance is invalid", geometryName); + return false; + } + } + String[] shaderNames = pl.getStringArray("shaders", null); + if (shaderNames != null) { + // new shader names have been provided + shaders = new Shader[shaderNames.length]; + for (int i = 0; i < shaders.length; i++) { + shaders[i] = api.lookupShader(shaderNames[i]); + if (shaders[i] == null) + UI.printWarning(Module.GEOM, "Shader \"%s\" was not declared yet - ignoring", shaderNames[i]); + } + } else { + // re-use existing shader array + } + String[] modifierNames = pl.getStringArray("modifiers", null); + if (modifierNames != null) { + // new modifier names have been provided + modifiers = new Modifier[modifierNames.length]; + for (int i = 0; i < modifiers.length; i++) { + modifiers[i] = api.lookupModifier(modifierNames[i]); + if (modifiers[i] == null) + UI.printWarning(Module.GEOM, "Modifier \"%s\" was not declared yet - ignoring", modifierNames[i]); + } + } + o2w = pl.getMovingMatrix("transform", o2w); + w2o = o2w.inverse(); + if (w2o == null) { + UI.printError(Module.GEOM, "Unable to compute transform inverse"); + return false; + } + return true; + } + + /** + * Recompute world space bounding box of this instance. + */ + public void updateBounds() { + bounds = geometry.getWorldBounds(o2w.getData(0)); + for (int i = 1; i < o2w.numSegments(); i++) + bounds.include(geometry.getWorldBounds(o2w.getData(i))); + } + + /** + * Checks to see if this instance is relative to the specified geometry. + * + * @param g geometry to check against + * @return true if the instanced geometry is equals to g, + * false otherwise + */ + public boolean hasGeometry(Geometry g) { + return geometry == g; + } + + /** + * Remove the specified shader from the instance's list if it is being used. + * + * @param s shader to remove + */ + public void removeShader(Shader s) { + if (shaders != null) { + for (int i = 0; i < shaders.length; i++) + if (shaders[i] == s) + shaders[i] = null; + } + } + + /** + * Remove the specified modifier from the instance's list if it is being + * used. + * + * @param m modifier to remove + */ + public void removeModifier(Modifier m) { + if (modifiers != null) { + for (int i = 0; i < modifiers.length; i++) + if (modifiers[i] == m) + modifiers[i] = null; + } + } + + /** + * Get the world space bounding box for this instance. + * + * @return bounding box in world space + */ + public BoundingBox getBounds() { + return bounds; + } + + int getNumPrimitives() { + return geometry.getNumPrimitives(); + } + + void intersect(Ray r, IntersectionState state) { + Ray localRay = r.transform(w2o.sample(state.time)); + state.current = this; + geometry.intersect(localRay, state); + // FIXME: transfer max distance to current ray + r.setMax(localRay.getMax()); + } + + /** + * Prepare the shading state for shader invocation. This also runs the + * currently attached surface modifier. + * + * @param state shading state to be prepared + */ + public void prepareShadingState(ShadingState state) { + geometry.prepareShadingState(state); + if (state.getNormal() != null && state.getGeoNormal() != null) + state.correctShadingNormal(); + // run modifier if it was provided + if (state.getModifier() != null) + state.getModifier().modify(state); + } + + /** + * Get a shader for the instance's list. + * + * @param i index into the shader list + * @return requested shader, or null if the input is invalid + */ + public Shader getShader(int i) { + if (shaders == null || i < 0 || i >= shaders.length) + return null; + return shaders[i]; + } + + /** + * Get a modifier for the instance's list. + * + * @param i index into the modifier list + * @return requested modifier, or null if the input is + * invalid + */ + public Modifier getModifier(int i) { + if (modifiers == null || i < 0 || i >= modifiers.length) + return null; + return modifiers[i]; + } + + Matrix4 getObjectToWorld(float time) { + return o2w.sample(time); + } + + Matrix4 getWorldToObject(float time) { + return w2o.sample(time); + } + + PrimitiveList getBakingPrimitives() { + return geometry.getBakingPrimitives(); + } + + Geometry getGeometry() { + return geometry; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/InstanceList.java b/src/main/java/org/sunflow/core/InstanceList.java new file mode 100644 index 0000000..631ba9e --- /dev/null +++ b/src/main/java/org/sunflow/core/InstanceList.java @@ -0,0 +1,71 @@ +package org.sunflow.core; + +import org.sunflow.SunflowAPI; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; + +final class InstanceList implements PrimitiveList { + private Instance[] instances; + private Instance[] lights; + + InstanceList() { + instances = new Instance[0]; + clearLightSources(); + } + + InstanceList(Instance[] instances) { + this.instances = instances; + clearLightSources(); + } + + void addLightSourceInstances(Instance[] lights) { + this.lights = lights; + } + + void clearLightSources() { + lights = new Instance[0]; + } + + public final float getPrimitiveBound(int primID, int i) { + if (primID < instances.length) + return instances[primID].getBounds().getBound(i); + else + return lights[primID - instances.length].getBounds().getBound(i); + } + + public final BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(); + for (Instance i : instances) + bounds.include(i.getBounds()); + for (Instance i : lights) + bounds.include(i.getBounds()); + return bounds; + } + + public final void intersectPrimitive(Ray r, int primID, IntersectionState state) { + if (primID < instances.length) + instances[primID].intersect(r, state); + else + lights[primID - instances.length].intersect(r, state); + } + + public final int getNumPrimitives() { + return instances.length + lights.length; + } + + public final int getNumPrimitives(int primID) { + return primID < instances.length ? instances[primID].getNumPrimitives() : lights[primID - instances.length].getNumPrimitives(); + } + + public final void prepareShadingState(ShadingState state) { + state.getInstance().prepareShadingState(state); + } + + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public PrimitiveList getBakingPrimitives() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/IntersectionState.java b/src/main/java/org/sunflow/core/IntersectionState.java new file mode 100644 index 0000000..e47cbbf --- /dev/null +++ b/src/main/java/org/sunflow/core/IntersectionState.java @@ -0,0 +1,116 @@ +package org.sunflow.core; + +/** + * This class is used to store ray/object intersections. It also provides + * additional data to assist {@link AccelerationStructure} objects with + * traversal. + */ +public final class IntersectionState { + private static final int MAX_STACK_SIZE = 64; + float time; + float u, v, w; + Instance instance; + int id; + private final StackNode[][] stacks = new StackNode[2][MAX_STACK_SIZE]; + Instance current; + long numEyeRays; + long numShadowRays; + long numReflectionRays; + long numGlossyRays; + long numRefractionRays; + long numRays; + + /** + * Traversal stack node, helps with tree-based {@link AccelerationStructure} + * traversal. + */ + public static final class StackNode { + public int node; + public float near; + public float far; + } + + /** + * Initializes all traversal stacks. + */ + public IntersectionState() { + for (int i = 0; i < stacks.length; i++) + for (int j = 0; j < stacks[i].length; j++) + stacks[i][j] = new StackNode(); + } + + /** + * Returns the time at which the intersection should be calculated. This + * will be constant for a given ray-tree. This value is guarenteed to be + * between the camera's shutter open and shutter close time. + * + * @return time value + */ + public float getTime() { + return time; + } + + /** + * Get stack object for tree based {@link AccelerationStructure}s. + * + * @return array of stack nodes + */ + public final StackNode[] getStack() { + return current == null ? stacks[0] : stacks[1]; + } + + /** + * Checks to see if a hit has been recorded. + * + * @return true if a hit has been recorded, + * false otherwise + */ + public final boolean hit() { + return instance != null; + } + + /** + * Record an intersection with the specified primitive id. The parent object + * is assumed to be the current instance. The u and v parameters are used to + * pinpoint the location on the surface if needed. + * + * @param id primitive id of the intersected object + */ + public final void setIntersection(int id) { + instance = current; + this.id = id; + } + + /** + * Record an intersection with the specified primitive id. The parent object + * is assumed to be the current instance. The u and v parameters are used to + * pinpoint the location on the surface if needed. + * + * @param id primitive id of the intersected object + * @param u u surface paramater of the intersection point + * @param v v surface parameter of the intersection point + */ + public final void setIntersection(int id, float u, float v) { + instance = current; + this.id = id; + this.u = u; + this.v = v; + } + + /** + * Record an intersection with the specified primitive id. The parent object + * is assumed to be the current instance. The u and v parameters are used to + * pinpoint the location on the surface if needed. + * + * @param id primitive id of the intersected object + * @param u u surface paramater of the intersection point + * @param v v surface parameter of the intersection point + */ + public final void setIntersection(int id, float u, float v, float w) { + instance = current; + this.id = id; + this.u = u; + this.v = v; + this.w = w; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/LightSample.java b/src/main/java/org/sunflow/core/LightSample.java new file mode 100644 index 0000000..7fac673 --- /dev/null +++ b/src/main/java/org/sunflow/core/LightSample.java @@ -0,0 +1,103 @@ +package org.sunflow.core; + +import org.sunflow.image.Color; +import org.sunflow.math.Vector3; + +/** + * Represents a sample taken from a light source that faces a point being + * shaded. + */ +public class LightSample { + private Ray shadowRay; // ray to be used to evaluate if the point is in + // shadow + private Color ldiff; + private Color lspec; + LightSample next; // pointer to next item in a linked list of samples + + /** + * Creates a new light sample object (invalid by default). + */ + public LightSample() { + ldiff = lspec = null; + shadowRay = null; + next = null; + } + + boolean isValid() { + return ldiff != null && lspec != null && shadowRay != null; + } + + /** + * Set the current shadow ray. The ray's direction is used as the sample's + * orientation. + * + * @param shadowRay shadow ray from the point being shaded towards the light + */ + public void setShadowRay(Ray shadowRay) { + this.shadowRay = shadowRay; + } + + /** + * Trace the shadow ray, attenuating the sample's color by the opacity of + * intersected objects. + * + * @param state shading state representing the point to be shaded + */ + public final void traceShadow(ShadingState state) { + Color opacity = state.traceShadow(shadowRay); + Color.blend(ldiff, Color.BLACK, opacity, ldiff); + Color.blend(lspec, Color.BLACK, opacity, lspec); + } + + /** + * Get the sample's shadow ray. + * + * @return shadow ray + */ + public Ray getShadowRay() { + return shadowRay; + } + + /** + * Get diffuse radiance. + * + * @return diffuse radiance + */ + public Color getDiffuseRadiance() { + return ldiff; + } + + /** + * Get specular radiance. + * + * @return specular radiance + */ + public Color getSpecularRadiance() { + return lspec; + } + + /** + * Set the diffuse and specular radiance emitted by the current light + * source. These should usually be the same, but are distinguished to allow + * for non-physical light setups or light source types which compute diffuse + * and specular responses seperately. + * + * @param d diffuse radiance + * @param s specular radiance + */ + public void setRadiance(Color d, Color s) { + ldiff = d.copy(); + lspec = s.copy(); + } + + /** + * Compute a dot product between the current shadow ray direction and the + * specified vector. + * + * @param v direction vector + * @return dot product of the vector with the shadow ray direction + */ + public float dot(Vector3 v) { + return shadowRay.dot(v); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/LightServer.java b/src/main/java/org/sunflow/core/LightServer.java new file mode 100644 index 0000000..d28e820 --- /dev/null +++ b/src/main/java/org/sunflow/core/LightServer.java @@ -0,0 +1,360 @@ +package org.sunflow.core; + +import org.sunflow.PluginRegistry; +import org.sunflow.image.Color; +import org.sunflow.math.Point3; +import org.sunflow.math.QMC; +import org.sunflow.math.Vector3; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +class LightServer { + // parent + private Scene scene; + + // lighting + LightSource[] lights; + + // shading override + private Shader shaderOverride; + private boolean shaderOverridePhotons; + + // direct illumination + private int maxDiffuseDepth; + private int maxReflectionDepth; + private int maxRefractionDepth; + + // indirect illumination + private CausticPhotonMapInterface causticPhotonMap; + private GIEngine giEngine; + private int photonCounter; + + LightServer(Scene scene) { + this.scene = scene; + lights = new LightSource[0]; + causticPhotonMap = null; + + shaderOverride = null; + shaderOverridePhotons = false; + + maxDiffuseDepth = 1; + maxReflectionDepth = 4; + maxRefractionDepth = 4; + + causticPhotonMap = null; + giEngine = null; + } + + void setLights(LightSource[] lights) { + this.lights = lights; + } + + Scene getScene() { + return scene; + } + + void setShaderOverride(Shader shader, boolean photonOverride) { + shaderOverride = shader; + shaderOverridePhotons = photonOverride; + } + + boolean build(Options options) { + // read options + maxDiffuseDepth = options.getInt("depths.diffuse", maxDiffuseDepth); + maxReflectionDepth = options.getInt("depths.reflection", maxReflectionDepth); + maxRefractionDepth = options.getInt("depths.refraction", maxRefractionDepth); + String giEngineType = options.getString("gi.engine", null); + giEngine = PluginRegistry.giEnginePlugins.createObject(giEngineType); + String caustics = options.getString("caustics", null); + causticPhotonMap = PluginRegistry.causticPhotonMapPlugins.createObject(caustics); + + // validate options + maxDiffuseDepth = Math.max(0, maxDiffuseDepth); + maxReflectionDepth = Math.max(0, maxReflectionDepth); + maxRefractionDepth = Math.max(0, maxRefractionDepth); + + Timer t = new Timer(); + t.start(); + // count total number of light samples + int numLightSamples = 0; + for (int i = 0; i < lights.length; i++) + numLightSamples += lights[i].getNumSamples(); + // initialize gi engine + if (giEngine != null) { + if (!giEngine.init(options, scene)) + return false; + } + + if (!calculatePhotons(causticPhotonMap, "caustic", 0, options)) + return false; + t.end(); + UI.printInfo(Module.LIGHT, "Light Server stats:"); + UI.printInfo(Module.LIGHT, " * Light sources found: %d", lights.length); + UI.printInfo(Module.LIGHT, " * Light samples: %d", numLightSamples); + UI.printInfo(Module.LIGHT, " * Max raytrace depth:"); + UI.printInfo(Module.LIGHT, " - Diffuse %d", maxDiffuseDepth); + UI.printInfo(Module.LIGHT, " - Reflection %d", maxReflectionDepth); + UI.printInfo(Module.LIGHT, " - Refraction %d", maxRefractionDepth); + UI.printInfo(Module.LIGHT, " * GI engine %s", giEngineType == null ? "none" : giEngineType); + UI.printInfo(Module.LIGHT, " * Caustics: %s", caustics == null ? "none" : caustics); + UI.printInfo(Module.LIGHT, " * Shader override: %b", shaderOverride); + UI.printInfo(Module.LIGHT, " * Photon override: %b", shaderOverridePhotons); + UI.printInfo(Module.LIGHT, " * Build time: %s", t.toString()); + return true; + } + + void showStats() { + } + + boolean calculatePhotons(final PhotonStore map, String type, final int seed, Options options) { + if (map == null) + return true; + if (lights.length == 0) { + UI.printError(Module.LIGHT, "Unable to trace %s photons, no lights in scene", type); + return false; + } + final float[] histogram = new float[lights.length]; + histogram[0] = lights[0].getPower(); + for (int i = 1; i < lights.length; i++) + histogram[i] = histogram[i - 1] + lights[i].getPower(); + UI.printInfo(Module.LIGHT, "Tracing %s photons ...", type); + map.prepare(options, scene.getBounds()); + int numEmittedPhotons = map.numEmit(); + if (numEmittedPhotons <= 0 || histogram[histogram.length - 1] <= 0) { + UI.printError(Module.LIGHT, "Photon mapping enabled, but no %s photons to emit", type); + return false; + } + UI.taskStart("Tracing " + type + " photons", 0, numEmittedPhotons); + Thread[] photonThreads = new Thread[scene.getThreads()]; + final float scale = 1.0f / numEmittedPhotons; + int delta = numEmittedPhotons / photonThreads.length; + photonCounter = 0; + Timer photonTimer = new Timer(); + photonTimer.start(); + for (int i = 0; i < photonThreads.length; i++) { + final int threadID = i; + final int start = threadID * delta; + final int end = (threadID == (photonThreads.length - 1)) ? numEmittedPhotons : (threadID + 1) * delta; + photonThreads[i] = new Thread(new Runnable() { + public void run() { + IntersectionState istate = new IntersectionState(); + for (int i = start; i < end; i++) { + synchronized (LightServer.this) { + UI.taskUpdate(photonCounter); + photonCounter++; + if (UI.taskCanceled()) + return; + } + + int qmcI = i + seed; + + double rand = QMC.halton(0, qmcI) * histogram[histogram.length - 1]; + int j = 0; + while (rand >= histogram[j] && j < histogram.length) + j++; + // make sure we didn't pick a zero-probability light + if (j == histogram.length) + continue; + + double randX1 = (j == 0) ? rand / histogram[0] : (rand - histogram[j]) / (histogram[j] - histogram[j - 1]); + double randY1 = QMC.halton(1, qmcI); + double randX2 = QMC.halton(2, qmcI); + double randY2 = QMC.halton(3, qmcI); + Point3 pt = new Point3(); + Vector3 dir = new Vector3(); + Color power = new Color(); + lights[j].getPhoton(randX1, randY1, randX2, randY2, pt, dir, power); + power.mul(scale); + Ray r = new Ray(pt, dir); + scene.trace(r, istate); + if (istate.hit()) + shadePhoton(ShadingState.createPhotonState(r, istate, qmcI, map, LightServer.this), power); + } + } + }); + photonThreads[i].setPriority(scene.getThreadPriority()); + photonThreads[i].start(); + } + for (int i = 0; i < photonThreads.length; i++) { + try { + photonThreads[i].join(); + } catch (InterruptedException e) { + UI.printError(Module.LIGHT, "Photon thread %d of %d was interrupted", i + 1, photonThreads.length); + return false; + } + } + if (UI.taskCanceled()) { + UI.taskStop(); // shut down task cleanly + return false; + } + photonTimer.end(); + UI.taskStop(); + UI.printInfo(Module.LIGHT, "Tracing time for %s photons: %s", type, photonTimer.toString()); + map.init(); + return true; + } + + void shadePhoton(ShadingState state, Color power) { + state.getInstance().prepareShadingState(state); + Shader shader = getPhotonShader(state); + // scatter photon + if (shader != null) + shader.scatterPhoton(state, power); + } + + void traceDiffusePhoton(ShadingState previous, Ray r, Color power) { + if (previous.getDiffuseDepth() >= maxDiffuseDepth) + return; + IntersectionState istate = previous.getIntersectionState(); + scene.trace(r, istate); + if (previous.getIntersectionState().hit()) { + // create a new shading context + ShadingState state = ShadingState.createDiffuseBounceState(previous, r, 0); + shadePhoton(state, power); + } + } + + void traceReflectionPhoton(ShadingState previous, Ray r, Color power) { + if (previous.getReflectionDepth() >= maxReflectionDepth) + return; + IntersectionState istate = previous.getIntersectionState(); + scene.trace(r, istate); + if (previous.getIntersectionState().hit()) { + // create a new shading context + ShadingState state = ShadingState.createReflectionBounceState(previous, r, 0); + shadePhoton(state, power); + } + } + + void traceRefractionPhoton(ShadingState previous, Ray r, Color power) { + if (previous.getRefractionDepth() >= maxRefractionDepth) + return; + IntersectionState istate = previous.getIntersectionState(); + scene.trace(r, istate); + if (previous.getIntersectionState().hit()) { + // create a new shading context + ShadingState state = ShadingState.createRefractionBounceState(previous, r, 0); + shadePhoton(state, power); + } + } + + private Shader getShader(ShadingState state) { + return shaderOverride != null ? shaderOverride : state.getShader(); + } + + private Shader getPhotonShader(ShadingState state) { + return (shaderOverride != null && shaderOverridePhotons) ? shaderOverride : state.getShader(); + + } + + ShadingState getRadiance(float rx, float ry, float time, int i, int d, Ray r, IntersectionState istate, ShadingCache cache) { + // set this value once - will stay constant for the entire ray-tree + istate.time = time; + scene.trace(r, istate); + if (istate.hit()) { + ShadingState state = ShadingState.createState(istate, rx, ry, time, r, i, d, this); + state.getInstance().prepareShadingState(state); + Shader shader = getShader(state); + if (shader == null) { + state.setResult(Color.BLACK); + return state; + } + if (cache != null) { + Color c = cache.lookup(state, shader); + if (c != null) { + state.setResult(c); + return state; + } + } + state.setResult(shader.getRadiance(state)); + if (cache != null) + cache.add(state, shader, state.getResult()); + checkNanInf(state.getResult()); + return state; + } else + return null; + } + + private static final void checkNanInf(Color c) { + if (c.isNan()) + UI.printWarning(Module.LIGHT, "NaN shading sample!"); + else if (c.isInf()) + UI.printWarning(Module.LIGHT, "Inf shading sample!"); + } + + void shadeBakeResult(ShadingState state) { + Shader shader = getShader(state); + if (shader != null) + state.setResult(shader.getRadiance(state)); + else + state.setResult(Color.BLACK); + } + + Color shadeHit(ShadingState state) { + state.getInstance().prepareShadingState(state); + Shader shader = getShader(state); + return (shader != null) ? shader.getRadiance(state) : Color.BLACK; + } + + Color traceGlossy(ShadingState previous, Ray r, int i) { + // limit path depth and disable caustic paths + if (previous.getReflectionDepth() >= maxReflectionDepth || previous.getDiffuseDepth() > 0) + return Color.BLACK; + IntersectionState istate = previous.getIntersectionState(); + istate.numGlossyRays++; + scene.trace(r, istate); + return istate.hit() ? shadeHit(ShadingState.createGlossyBounceState(previous, r, i)) : Color.BLACK; + } + + Color traceReflection(ShadingState previous, Ray r, int i) { + // limit path depth and disable caustic paths + if (previous.getReflectionDepth() >= maxReflectionDepth || previous.getDiffuseDepth() > 0) + return Color.BLACK; + IntersectionState istate = previous.getIntersectionState(); + istate.numReflectionRays++; + scene.trace(r, istate); + return istate.hit() ? shadeHit(ShadingState.createReflectionBounceState(previous, r, i)) : Color.BLACK; + } + + Color traceRefraction(ShadingState previous, Ray r, int i) { + // limit path depth and disable caustic paths + if (previous.getRefractionDepth() >= maxRefractionDepth || previous.getDiffuseDepth() > 0) + return Color.BLACK; + IntersectionState istate = previous.getIntersectionState(); + istate.numRefractionRays++; + scene.trace(r, istate); + return istate.hit() ? shadeHit(ShadingState.createRefractionBounceState(previous, r, i)) : Color.BLACK; + } + + ShadingState traceFinalGather(ShadingState previous, Ray r, int i) { + if (previous.getDiffuseDepth() >= maxDiffuseDepth) + return null; + IntersectionState istate = previous.getIntersectionState(); + scene.trace(r, istate); + return istate.hit() ? ShadingState.createFinalGatherState(previous, r, i) : null; + } + + Color getGlobalRadiance(ShadingState state) { + if (giEngine == null) + return Color.BLACK; + return giEngine.getGlobalRadiance(state); + } + + Color getIrradiance(ShadingState state, Color diffuseReflectance) { + // no gi engine, or we have already exceeded number of available bounces + if (giEngine == null || state.getDiffuseDepth() >= maxDiffuseDepth) + return Color.BLACK; + return giEngine.getIrradiance(state, diffuseReflectance); + } + + void initLightSamples(ShadingState state) { + for (LightSource l : lights) + l.getSamples(state); + } + + void initCausticSamples(ShadingState state) { + if (causticPhotonMap != null) + causticPhotonMap.getSamples(state); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/LightSource.java b/src/main/java/org/sunflow/core/LightSource.java new file mode 100644 index 0000000..ae797cf --- /dev/null +++ b/src/main/java/org/sunflow/core/LightSource.java @@ -0,0 +1,66 @@ +package org.sunflow.core; + +import org.sunflow.image.Color; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +/** + * This interface is used to represent any light emitting primitive. It permits + * efficient sampling of direct illumination and photon shooting. + */ +public interface LightSource extends RenderObject { + /** + * Get the maximum number of samples that can be taken from this light + * source. This is currently only used for statistics reporting. + * + * @return maximum number of samples to be taken from this light source + */ + public int getNumSamples(); + + /** + * Samples the light source to compute direct illumination. Light samples + * can be created using the {@link LightSample} class and added to the + * current {@link ShadingState}. This method is responsible for the + * shooting of shadow rays which allows for non-physical lights that don't + * cast shadows. It is recommended that only a single shadow ray be shot if + * {@link ShadingState#getDiffuseDepth()} is greater than 0. This avoids an + * exponential number of shadow rays from being traced. + * + * @param state current state, including point to be shaded + * @see LightSample + */ + public void getSamples(ShadingState state); + + /** + * Gets a photon to emit from this light source by setting each of the + * arguments. The two sampling parameters are points on the unit square that + * can be used to sample a position and/or direction for the emitted photon. + * + * @param randX1 sampling parameter + * @param randY1 sampling parameter + * @param randX2 sampling parameter + * @param randY2 sampling parameter + * @param p position to shoot the photon from + * @param dir direction to shoot the photon in + * @param power power of the photon + */ + public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power); + + /** + * Get the total power emitted by this light source. Lights that have 0 + * power will not emit any photons. + * + * @return light source power + */ + public float getPower(); + + /** + * Create an instance which represents the geometry of this light source. + * This instance will be created just before and removed immediately after + * rendering. Non-area light sources can return null to + * indicate that no geometry needs to be created. + * + * @return an instance describing the light source + */ + public Instance createInstance(); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/Modifier.java b/src/main/java/org/sunflow/core/Modifier.java new file mode 100644 index 0000000..33e2238 --- /dev/null +++ b/src/main/java/org/sunflow/core/Modifier.java @@ -0,0 +1,16 @@ +package org.sunflow.core; + +/** + * This represents a surface modifier. This is run on each instance prior to + * shading and can modify the shading state in arbitrary ways to provide effects + * such as bump mapping. + */ +public interface Modifier extends RenderObject { + + /** + * Modify the shading state for the point to be shaded. + * + * @param state shading state to modify + */ + public void modify(ShadingState state); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/Options.java b/src/main/java/org/sunflow/core/Options.java new file mode 100644 index 0000000..9a59102 --- /dev/null +++ b/src/main/java/org/sunflow/core/Options.java @@ -0,0 +1,18 @@ +package org.sunflow.core; + +import org.sunflow.SunflowAPI; +import org.sunflow.util.FastHashMap; + +/** + * This holds rendering objects as key, value pairs. + */ +public final class Options extends ParameterList implements RenderObject { + public boolean update(ParameterList pl, SunflowAPI api) { + // take all attributes, and update them into the current set + for (FastHashMap.Entry e : pl.list) { + list.put(e.getKey(), e.getValue()); + e.getValue().check(); + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/ParameterList.java b/src/main/java/org/sunflow/core/ParameterList.java new file mode 100644 index 0000000..e824f6f --- /dev/null +++ b/src/main/java/org/sunflow/core/ParameterList.java @@ -0,0 +1,729 @@ +package org.sunflow.core; + +import java.util.Locale; + +import org.sunflow.image.Color; +import org.sunflow.math.Matrix4; +import org.sunflow.math.MovingMatrix4; +import org.sunflow.math.Point2; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; +import org.sunflow.util.FastHashMap; + +/** + * This class holds a list of "parameters". These are defined and then passed + * onto rendering objects through the API. They can hold arbitrary typed and + * named variables as a unified way of getting data into user objects. + */ +public class ParameterList { + protected final FastHashMap list; + private int numVerts, numFaces, numFaceVerts; + + private enum ParameterType { + STRING, INT, BOOL, FLOAT, POINT, VECTOR, TEXCOORD, MATRIX, COLOR + } + + public enum InterpolationType { + NONE, FACE, VERTEX, FACEVARYING + } + + /** + * Creates an empty ParameterList. + */ + public ParameterList() { + list = new FastHashMap(); + numVerts = numFaces = numFaceVerts = 0; + } + + /** + * Clears the list of all its members. If some members were never used, a + * warning will be printed to remind the user something may be wrong. + */ + public void clear(boolean showUnused) { + if (showUnused) { + for (FastHashMap.Entry e : list) { + if (!e.getValue().checked) + UI.printWarning(Module.API, "Unused parameter: %s - %s", e.getKey(), e.getValue()); + } + } + list.clear(); + numVerts = numFaces = numFaceVerts = 0; + } + + /** + * Setup how many faces should be used to check member count on "face" + * interpolated parameters. + * + * @param numFaces number of faces + */ + public void setFaceCount(int numFaces) { + this.numFaces = numFaces; + } + + /** + * Setup how many vertices should be used to check member count of "vertex" + * interpolated parameters. + * + * @param numVerts number of vertices + */ + public void setVertexCount(int numVerts) { + this.numVerts = numVerts; + } + + /** + * Setup how many "face-vertices" should be used to check member count of + * "facevarying" interpolated parameters. This should be equal to the sum of + * the number of vertices on each face. + * + * @param numFaceVerts number of "face-vertices" + */ + public void setFaceVertexCount(int numFaceVerts) { + this.numFaceVerts = numFaceVerts; + } + + /** + * Add the specified string as a parameter. null values are + * not permitted. + * + * @param name parameter name + * @param value parameter value + */ + public void addString(String name, String value) { + add(name, new Parameter(value)); + } + + /** + * Add the specified integer as a parameter. null values are + * not permitted. + * + * @param name parameter name + * @param value parameter value + */ + public void addInteger(String name, int value) { + add(name, new Parameter(value)); + } + + /** + * Add the specified boolean as a parameter. null values are + * not permitted. + * + * @param name parameter name + * @param value parameter value + */ + public void addBoolean(String name, boolean value) { + add(name, new Parameter(value)); + } + + /** + * Add the specified float as a parameter. null values are + * not permitted. + * + * @param name parameter name + * @param value parameter value + */ + public void addFloat(String name, float value) { + add(name, new Parameter(value)); + } + + /** + * Add the specified color as a parameter. null values are + * not permitted. + * + * @param name parameter name + * @param value parameter value + */ + public void addColor(String name, Color value) { + if (value == null) + throw new NullPointerException(); + add(name, new Parameter(value)); + } + + /** + * Add the specified array of integers as a parameter. null + * values are not permitted. + * + * @param name parameter name + * @param array parameter value + */ + public void addIntegerArray(String name, int[] array) { + if (array == null) + throw new NullPointerException(); + add(name, new Parameter(array)); + } + + /** + * Add the specified array of integers as a parameter. null + * values are not permitted. + * + * @param name parameter name + * @param array parameter value + */ + public void addStringArray(String name, String[] array) { + if (array == null) + throw new NullPointerException(); + add(name, new Parameter(array)); + } + + /** + * Add the specified floats as a parameter. null values are + * not permitted. + * + * @param name parameter name + * @param interp interpolation type + * @param data parameter value + */ + public void addFloats(String name, InterpolationType interp, float[] data) { + if (data == null) { + UI.printError(Module.API, "Cannot create float parameter %s -- invalid data length", name); + return; + } + add(name, new Parameter(ParameterType.FLOAT, interp, data)); + } + + /** + * Add the specified points as a parameter. null values are + * not permitted. + * + * @param name parameter name + * @param interp interpolation type + * @param data parameter value + */ + public void addPoints(String name, InterpolationType interp, float[] data) { + if (data == null || data.length % 3 != 0) { + UI.printError(Module.API, "Cannot create point parameter %s -- invalid data length", name); + return; + } + add(name, new Parameter(ParameterType.POINT, interp, data)); + } + + /** + * Add the specified vectors as a parameter. null values are + * not permitted. + * + * @param name parameter name + * @param interp interpolation type + * @param data parameter value + */ + + public void addVectors(String name, InterpolationType interp, float[] data) { + if (data == null || data.length % 3 != 0) { + UI.printError(Module.API, "Cannot create vector parameter %s -- invalid data length", name); + return; + } + add(name, new Parameter(ParameterType.VECTOR, interp, data)); + } + + /** + * Add the specified texture coordinates as a parameter. null + * values are not permitted. + * + * @param name parameter name + * @param interp interpolation type + * @param data parameter value + */ + public void addTexCoords(String name, InterpolationType interp, float[] data) { + if (data == null || data.length % 2 != 0) { + UI.printError(Module.API, "Cannot create texcoord parameter %s -- invalid data length", name); + return; + } + add(name, new Parameter(ParameterType.TEXCOORD, interp, data)); + } + + /** + * Add the specified matrices as a parameter. null values are + * not permitted. + * + * @param name parameter name + * @param interp interpolation type + * @param data parameter value + */ + public void addMatrices(String name, InterpolationType interp, float[] data) { + if (data == null || data.length % 16 != 0) { + UI.printError(Module.API, "Cannot create matrix parameter %s -- invalid data length", name); + return; + } + add(name, new Parameter(ParameterType.MATRIX, interp, data)); + } + + private void add(String name, Parameter param) { + if (name == null) + UI.printError(Module.API, "Cannot declare parameter with null name"); + else if (list.put(name, param) != null) + UI.printWarning(Module.API, "Parameter %s was already defined -- overwriting", name); + } + + /** + * Get the specified string parameter from this list. + * + * @param name name of the parameter + * @param defaultValue value to return if not found + * @return the value of the parameter specified or default value if not + * found + */ + public String getString(String name, String defaultValue) { + Parameter p = list.get(name); + if (isValidParameter(name, ParameterType.STRING, InterpolationType.NONE, 1, p)) + return p.getStringValue(); + return defaultValue; + } + + /** + * Get the specified string array parameter from this list. + * + * @param name name of the parameter + * @param defaultValue value to return if not found + * @return the value of the parameter specified or default value if not + * found + */ + public String[] getStringArray(String name, String[] defaultValue) { + Parameter p = list.get(name); + if (isValidParameter(name, ParameterType.STRING, InterpolationType.NONE, -1, p)) + return p.getStrings(); + return defaultValue; + } + + /** + * Get the specified integer parameter from this list. + * + * @param name name of the parameter + * @param defaultValue value to return if not found + * @return the value of the parameter specified or default value if not + * found + */ + public int getInt(String name, int defaultValue) { + Parameter p = list.get(name); + if (isValidParameter(name, ParameterType.INT, InterpolationType.NONE, 1, p)) + return p.getIntValue(); + return defaultValue; + } + + /** + * Get the specified integer array parameter from this list. + * + * @param name name of the parameter + * @return the value of the parameter specified or null if + * not found + */ + public int[] getIntArray(String name) { + Parameter p = list.get(name); + if (isValidParameter(name, ParameterType.INT, InterpolationType.NONE, -1, p)) + return p.getInts(); + return null; + } + + /** + * Get the specified boolean parameter from this list. + * + * @param name name of the parameter + * @param defaultValue value to return if not found + * @return the value of the parameter specified or default value if not + * found + */ + public boolean getBoolean(String name, boolean defaultValue) { + Parameter p = list.get(name); + if (isValidParameter(name, ParameterType.BOOL, InterpolationType.NONE, 1, p)) + return p.getBoolValue(); + return defaultValue; + } + + /** + * Get the specified float parameter from this list. + * + * @param name name of the parameter + * @param defaultValue value to return if not found + * @return the value of the parameter specified or default value if not + * found + */ + public float getFloat(String name, float defaultValue) { + Parameter p = list.get(name); + if (isValidParameter(name, ParameterType.FLOAT, InterpolationType.NONE, 1, p)) + return p.getFloatValue(); + return defaultValue; + } + + /** + * Get the specified color parameter from this list. + * + * @param name name of the parameter + * @param defaultValue value to return if not found + * @return the value of the parameter specified or default value if not + * found + */ + public Color getColor(String name, Color defaultValue) { + Parameter p = list.get(name); + if (isValidParameter(name, ParameterType.COLOR, InterpolationType.NONE, 1, p)) + return p.getColor(); + return defaultValue; + } + + /** + * Get the specified point parameter from this list. + * + * @param name name of the parameter + * @param defaultValue value to return if not found + * @return the value of the parameter specified or default value if not + * found + */ + public Point3 getPoint(String name, Point3 defaultValue) { + Parameter p = list.get(name); + if (isValidParameter(name, ParameterType.POINT, InterpolationType.NONE, 1, p)) + return p.getPoint(); + return defaultValue; + } + + /** + * Get the specified vector parameter from this list. + * + * @param name name of the parameter + * @param defaultValue value to return if not found + * @return the value of the parameter specified or default value if not + * found + */ + public Vector3 getVector(String name, Vector3 defaultValue) { + Parameter p = list.get(name); + if (isValidParameter(name, ParameterType.VECTOR, InterpolationType.NONE, 1, p)) + return p.getVector(); + return defaultValue; + } + + /** + * Get the specified texture coordinate parameter from this list. + * + * @param name name of the parameter + * @param defaultValue value to return if not found + * @return the value of the parameter specified or default value if not + * found + */ + public Point2 getTexCoord(String name, Point2 defaultValue) { + Parameter p = list.get(name); + if (isValidParameter(name, ParameterType.TEXCOORD, InterpolationType.NONE, 1, p)) + return p.getTexCoord(); + return defaultValue; + } + + /** + * Get the specified matrix parameter from this list. + * + * @param name name of the parameter + * @param defaultValue value to return if not found + * @return the value of the parameter specified or default value if not + * found + */ + public Matrix4 getMatrix(String name, Matrix4 defaultValue) { + Parameter p = list.get(name); + if (isValidParameter(name, ParameterType.MATRIX, InterpolationType.NONE, 1, p)) + return p.getMatrix(); + return defaultValue; + } + + /** + * Get the specified float array parameter from this list. + * + * @param name name of the parameter + * @return the value of the parameter specified or null if + * not found + */ + public FloatParameter getFloatArray(String name) { + return getFloatParameter(name, ParameterType.FLOAT, list.get(name)); + } + + /** + * Get the specified point array parameter from this list. + * + * @param name name of the parameter + * @return the value of the parameter specified or null if + * not found + */ + public FloatParameter getPointArray(String name) { + return getFloatParameter(name, ParameterType.POINT, list.get(name)); + } + + /** + * Get the specified vector array parameter from this list. + * + * @param name name of the parameter + * @return the value of the parameter specified or null if + * not found + */ + public FloatParameter getVectorArray(String name) { + return getFloatParameter(name, ParameterType.VECTOR, list.get(name)); + } + + /** + * Get the specified texture coordinate array parameter from this list. + * + * @param name name of the parameter + * @return the value of the parameter specified or null if + * not found + */ + public FloatParameter getTexCoordArray(String name) { + return getFloatParameter(name, ParameterType.TEXCOORD, list.get(name)); + } + + /** + * Get the specified matrix array parameter from this list. + * + * @param name name of the parameter + * @return the value of the parameter specified or null if + * not found + */ + public FloatParameter getMatrixArray(String name) { + return getFloatParameter(name, ParameterType.MATRIX, list.get(name)); + } + + private boolean isValidParameter(String name, ParameterType type, InterpolationType interp, int requestedSize, Parameter p) { + if (p == null) + return false; + if (p.type != type) { + UI.printWarning(Module.API, "Parameter %s requested as a %s - declared as %s", name, type.name().toLowerCase(Locale.ENGLISH), p.type.name().toLowerCase(Locale.ENGLISH)); + return false; + } + if (p.interp != interp) { + UI.printWarning(Module.API, "Parameter %s requested as a %s - declared as %s", name, interp.name().toLowerCase(Locale.ENGLISH), p.interp.name().toLowerCase(Locale.ENGLISH)); + return false; + } + if (requestedSize > 0 && p.size() != requestedSize) { + UI.printWarning(Module.API, "Parameter %s requires %d %s - declared with %d", name, requestedSize, requestedSize == 1 ? "value" : "values", p.size()); + return false; + } + p.checked = true; + return true; + } + + private FloatParameter getFloatParameter(String name, ParameterType type, Parameter p) { + if (p == null) + return null; + switch (p.interp) { + case NONE: + if (!isValidParameter(name, type, p.interp, -1, p)) + return null; + break; + case VERTEX: + if (!isValidParameter(name, type, p.interp, numVerts, p)) + return null; + break; + case FACE: + if (!isValidParameter(name, type, p.interp, numFaces, p)) + return null; + break; + case FACEVARYING: + if (!isValidParameter(name, type, p.interp, numFaceVerts, p)) + return null; + break; + default: + return null; + } + return p.getFloats(); + } + + /** + * Represents an array of floating point values. This can store single + * float, points, vectors, texture coordinates or matrices. The parameter + * should be interpolated over the surface according to the interp parameter + * when applicable. + */ + public static final class FloatParameter { + public final InterpolationType interp; + public final float[] data; + + public FloatParameter() { + this(InterpolationType.NONE, null); + } + + public FloatParameter(float f) { + this(InterpolationType.NONE, new float[] { f }); + } + + private FloatParameter(InterpolationType interp, float[] data) { + this.interp = interp; + this.data = data; + } + } + + public final MovingMatrix4 getMovingMatrix(String name, MovingMatrix4 defaultValue) { + // step 1: check for a non-moving specification: + Matrix4 m = getMatrix(name, null); + if (m != null) + return new MovingMatrix4(m); + // step 2: check to see if the time range has been updated + FloatParameter times = getFloatArray(name + ".times"); + if (times != null) { + if (times.data.length <= 1) + defaultValue.updateTimes(0, 0); + else { + if (times.data.length != 2) + UI.printWarning(Module.API, "Time value specification using only endpoints of %d values specified", times.data.length); + // get endpoint times - we might allow multiple time values + // later + float t0 = times.data[0]; + float t1 = times.data[times.data.length - 1]; + defaultValue.updateTimes(t0, t1); + } + } else { + // time range stays at default + } + // step 3: check to see if a number of steps has been specified + int steps = getInt(name + ".steps", 0); + if (steps <= 0) { + // not specified - return default value + } else { + // update each element + defaultValue.setSteps(steps); + for (int i = 0; i < steps; i++) + defaultValue.updateData(i, getMatrix(String.format("%s[%d]", name, i), defaultValue.getData(i))); + } + return defaultValue; + } + + protected static final class Parameter { + private ParameterType type; + private InterpolationType interp; + private Object obj; + private boolean checked; + + private Parameter(String value) { + type = ParameterType.STRING; + interp = InterpolationType.NONE; + obj = new String[] { value }; + checked = false; + } + + private Parameter(int value) { + type = ParameterType.INT; + interp = InterpolationType.NONE; + obj = new int[] { value }; + checked = false; + } + + private Parameter(boolean value) { + type = ParameterType.BOOL; + interp = InterpolationType.NONE; + obj = value; + checked = false; + } + + private Parameter(float value) { + type = ParameterType.FLOAT; + interp = InterpolationType.NONE; + obj = new float[] { value }; + checked = false; + } + + private Parameter(int[] array) { + type = ParameterType.INT; + interp = InterpolationType.NONE; + obj = array; + checked = false; + } + + private Parameter(String[] array) { + type = ParameterType.STRING; + interp = InterpolationType.NONE; + obj = array; + checked = false; + } + + private Parameter(Color c) { + type = ParameterType.COLOR; + interp = InterpolationType.NONE; + obj = c; + checked = false; + } + + private Parameter(ParameterType type, InterpolationType interp, float[] data) { + this.type = type; + this.interp = interp; + obj = data; + checked = false; + } + + private int size() { + // number of elements + switch (type) { + case STRING: + return ((String[]) obj).length; + case INT: + return ((int[]) obj).length; + case BOOL: + return 1; + case FLOAT: + return ((float[]) obj).length; + case POINT: + return ((float[]) obj).length / 3; + case VECTOR: + return ((float[]) obj).length / 3; + case TEXCOORD: + return ((float[]) obj).length / 2; + case MATRIX: + return ((float[]) obj).length / 16; + case COLOR: + return 1; + default: + return -1; + } + } + + protected void check() { + checked = true; + } + + @Override + public String toString() { + return String.format("%s%s[%d]", interp == InterpolationType.NONE ? "" : interp.name().toLowerCase() + " ", type.name().toLowerCase(), size()); + } + + private String getStringValue() { + return ((String[]) obj)[0]; + } + + private boolean getBoolValue() { + return (Boolean) obj; + } + + private int getIntValue() { + return ((int[]) obj)[0]; + } + + private int[] getInts() { + return (int[]) obj; + } + + private String[] getStrings() { + return (String[]) obj; + } + + private float getFloatValue() { + return ((float[]) obj)[0]; + } + + private FloatParameter getFloats() { + return new FloatParameter(interp, (float[]) obj); + } + + private Point3 getPoint() { + float[] floats = (float[]) obj; + return new Point3(floats[0], floats[1], floats[2]); + } + + private Vector3 getVector() { + float[] floats = (float[]) obj; + return new Vector3(floats[0], floats[1], floats[2]); + } + + private Point2 getTexCoord() { + float[] floats = (float[]) obj; + return new Point2(floats[0], floats[1]); + } + + private Matrix4 getMatrix() { + float[] floats = (float[]) obj; + return new Matrix4(floats, true); + } + + private Color getColor() { + return (Color) obj; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/PhotonStore.java b/src/main/java/org/sunflow/core/PhotonStore.java new file mode 100644 index 0000000..c10da55 --- /dev/null +++ b/src/main/java/org/sunflow/core/PhotonStore.java @@ -0,0 +1,62 @@ +package org.sunflow.core; + +import org.sunflow.image.Color; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Vector3; + +/** + * Describes an object which can store photons. + */ +public interface PhotonStore { + /** + * Number of photons to emit from this surface. + * + * @return number of photons + */ + int numEmit(); + + /** + * Initialize this object for the specified scene size. + * + * @param sceneBounds scene bounding box + */ + void prepare(Options options, BoundingBox sceneBounds); + + /** + * Store the specified photon. + * + * @param state shading state + * @param dir photon direction + * @param power photon power + * @param diffuse diffuse color at the hit point + */ + void store(ShadingState state, Vector3 dir, Color power, Color diffuse); + + /** + * Initialize the map after all photons have been stored. This can be used + * to balance a kd-tree based photon map for example. + */ + void init(); + + /** + * Allow photons reflected diffusely? + * + * @return true if diffuse bounces should be traced + */ + boolean allowDiffuseBounced(); + + /** + * Allow specularly reflected photons? + * + * @return true if specular reflection bounces should be + * traced + */ + boolean allowReflectionBounced(); + + /** + * Allow refracted photons? + * + * @return true if refracted bounces should be traced + */ + boolean allowRefractionBounced(); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/PrimitiveList.java b/src/main/java/org/sunflow/core/PrimitiveList.java new file mode 100644 index 0000000..36012f0 --- /dev/null +++ b/src/main/java/org/sunflow/core/PrimitiveList.java @@ -0,0 +1,69 @@ +package org.sunflow.core; + +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; + +/** + * This class represents an object made up of many primitives. + */ +public interface PrimitiveList extends RenderObject { + /** + * Compute a bounding box of this object in world space, using the specified + * object-to-world transformation matrix. The bounds should be as exact as + * possible, if they are difficult or expensive to compute exactly, you may + * use {@link Matrix4#transform(BoundingBox)}. If the matrix is + * null no transformation is needed, and object space is + * equivalent to world space. + * + * @param o2w object to world transformation matrix + * @return object bounding box in world space + */ + public BoundingBox getWorldBounds(Matrix4 o2w); + + /** + * Returns the number of individual primtives in this aggregate object. + * + * @return number of primitives + */ + public int getNumPrimitives(); + + /** + * Retrieve the bounding box component of a particular primitive in object + * space. Even indexes get minimum values, while odd indexes get the maximum + * values for each axis. + * + * @param primID primitive index + * @param i bounding box side index + * @return value of the request bound + */ + public float getPrimitiveBound(int primID, int i); + + /** + * Intersect the specified primitive in local space. + * + * @param r ray in the object's local space + * @param primID primitive index to intersect + * @param state intersection state + * @see Ray#setMax(float) + * @see IntersectionState#setIntersection(int, float, float) + */ + public void intersectPrimitive(Ray r, int primID, IntersectionState state); + + /** + * Prepare the specified {@link ShadingState} by setting all of its internal + * parameters. + * + * @param state shading state to fill in + */ + public void prepareShadingState(ShadingState state); + + /** + * Create a new {@link PrimitiveList} object suitable for baking lightmaps. + * This means a set of primitives laid out in the unit square UV space. This + * method is optional, objects which do not support it should simply return + * null. + * + * @return a list of baking primitives + */ + public PrimitiveList getBakingPrimitives(); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/Ray.java b/src/main/java/org/sunflow/core/Ray.java new file mode 100644 index 0000000..46bc655 --- /dev/null +++ b/src/main/java/org/sunflow/core/Ray.java @@ -0,0 +1,217 @@ +package org.sunflow.core; + +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +/** + * This class represents a ray as a oriented half line segment. The ray + * direction is always normalized. The valid region is delimted by two distances + * along the ray, tMin and tMax. + */ +public final class Ray { + public float ox, oy, oz; + public float dx, dy, dz; + private float tMin; + private float tMax; + private static final float EPSILON = 0;// 0.01f; + + private Ray() { + } + + /** + * Creates a new ray that points from the given origin to the given + * direction. The ray has infinite length. The direction vector is + * normalized. + * + * @param ox ray origin x + * @param oy ray origin y + * @param oz ray origin z + * @param dx ray direction x + * @param dy ray direction y + * @param dz ray direction z + */ + + public Ray(float ox, float oy, float oz, float dx, float dy, float dz) { + this.ox = ox; + this.oy = oy; + this.oz = oz; + this.dx = dx; + this.dy = dy; + this.dz = dz; + float in = 1.0f / (float) Math.sqrt(dx * dx + dy * dy + dz * dz); + this.dx *= in; + this.dy *= in; + this.dz *= in; + tMin = EPSILON; + tMax = Float.POSITIVE_INFINITY; + } + + /** + * Creates a new ray that points from the given origin to the given + * direction. The ray has infinite length. The direction vector is + * normalized. + * + * @param o ray origin + * @param d ray direction (need not be normalized) + */ + public Ray(Point3 o, Vector3 d) { + ox = o.x; + oy = o.y; + oz = o.z; + dx = d.x; + dy = d.y; + dz = d.z; + float in = 1.0f / (float) Math.sqrt(dx * dx + dy * dy + dz * dz); + dx *= in; + dy *= in; + dz *= in; + tMin = EPSILON; + tMax = Float.POSITIVE_INFINITY; + } + + /** + * Creates a new ray that points from point a to point b. The created ray + * will set tMin and tMax to limit the ray to the segment (a,b) + * (non-inclusive of a and b). This is often used to create shadow rays. + * + * @param a start point + * @param b end point + */ + public Ray(Point3 a, Point3 b) { + ox = a.x; + oy = a.y; + oz = a.z; + dx = b.x - ox; + dy = b.y - oy; + dz = b.z - oz; + tMin = EPSILON; + float n = (float) Math.sqrt(dx * dx + dy * dy + dz * dz); + float in = 1.0f / n; + dx *= in; + dy *= in; + dz *= in; + tMax = n - EPSILON; + } + + /** + * Create a new ray by transforming the supplied one by the given matrix. If + * the matrix is null, the original ray is returned. + * + * @param m matrix to transform the ray by + */ + public Ray transform(Matrix4 m) { + if (m == null) + return this; + Ray r = new Ray(); + r.ox = m.transformPX(ox, oy, oz); + r.oy = m.transformPY(ox, oy, oz); + r.oz = m.transformPZ(ox, oy, oz); + r.dx = m.transformVX(dx, dy, dz); + r.dy = m.transformVY(dx, dy, dz); + r.dz = m.transformVZ(dx, dy, dz); + r.tMin = tMin; + r.tMax = tMax; + return r; + } + + /** + * Normalize the direction component of the ray. + */ + public void normalize() { + float in = 1.0f / (float) Math.sqrt(dx * dx + dy * dy + dz * dz); + dx *= in; + dy *= in; + dz *= in; + } + + /** + * Gets the minimum distance along the ray - usually 0. + * + * @return value of the smallest distance along the ray + */ + public final float getMin() { + return tMin; + } + + /** + * Gets the maximum distance along the ray. May be infinite. + * + * @return value of the largest distance along the ray + */ + public final float getMax() { + return tMax; + } + + /** + * Creates a vector to represent the direction of the ray. + * + * @return a vector equal to the direction of this ray + */ + public final Vector3 getDirection() { + return new Vector3(dx, dy, dz); + } + + /** + * Checks to see if the specified distance falls within the valid range on + * this ray. This should always be used before an intersection with the ray + * is detected. + * + * @param t distance to be tested + * @return true if t falls between the minimum and maximum + * distance of this ray, false otherwise + */ + public final boolean isInside(float t) { + return (tMin < t) && (t < tMax); + } + + /** + * Gets the end point of the ray. A reference to dest is + * returned to support chaining. + * + * @param dest reference to the point to store + * @return reference to dest + */ + public final Point3 getPoint(Point3 dest) { + dest.x = ox + (tMax * dx); + dest.y = oy + (tMax * dy); + dest.z = oz + (tMax * dz); + return dest; + } + + /** + * Computes the dot product of an arbitrary vector with the direction of the + * ray. This method avoids having to call getDirection() which would + * instantiate a new Vector object. + * + * @param v vector + * @return dot product of the ray direction and the specified vector + */ + public final float dot(Vector3 v) { + return dx * v.x + dy * v.y + dz * v.z; + } + + /** + * Computes the dot product of an arbitrary vector with the direction of the + * ray. This method avoids having to call getDirection() which would + * instantiate a new Vector object. + * + * @param vx vector x coordinate + * @param vy vector y coordinate + * @param vz vector z coordinate + * @return dot product of the ray direction and the specified vector + */ + public final float dot(float vx, float vy, float vz) { + return dx * vx + dy * vy + dz * vz; + } + + /** + * Updates the maximum to the specified distance if and only if the new + * distance is smaller than the current one. + * + * @param t new maximum distance + */ + public final void setMax(float t) { + tMax = t; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/RenderObject.java b/src/main/java/org/sunflow/core/RenderObject.java new file mode 100644 index 0000000..d451c62 --- /dev/null +++ b/src/main/java/org/sunflow/core/RenderObject.java @@ -0,0 +1,24 @@ +package org.sunflow.core; + +import org.sunflow.SunflowAPI; + +/** + * This is the base interface for all public rendering object interfaces. It + * handles incremental updates via {@link ParameterList} objects. + */ +public interface RenderObject { + /** + * Update this object given a list of parameters. This method is guarenteed + * to be called at least once on every object, but it should correctly + * handle empty parameter lists. This means that the object should be in a + * valid state from the time it is constructed. This method should also + * return true or false depending on whether the update was succesfull or + * not. + * + * @param pl list of parameters to read from + * @param api reference to the current scene + * @return true if the update is succesfull, + * false otherwise + */ + public boolean update(ParameterList pl, SunflowAPI api); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/Scene.java b/src/main/java/org/sunflow/core/Scene.java new file mode 100644 index 0000000..97b2cae --- /dev/null +++ b/src/main/java/org/sunflow/core/Scene.java @@ -0,0 +1,371 @@ +package org.sunflow.core; + +import java.util.ArrayList; + +import org.sunflow.core.display.FrameDisplay; +import org.sunflow.image.Color; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.MathUtils; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +/** + * Represents a entire scene, defined as a collection of instances viewed by a + * camera. + */ +public class Scene { + // scene storage + private LightServer lightServer; + private InstanceList instanceList; + private InstanceList infiniteInstanceList; + private Camera camera; + private AccelerationStructure intAccel; + private String acceltype; + private Statistics stats; + + // baking + private boolean bakingViewDependent; + private Instance bakingInstance; + private PrimitiveList bakingPrimitives; + private AccelerationStructure bakingAccel; + + private boolean rebuildAccel; + + // image size + private int imageWidth; + private int imageHeight; + + // global options + private int threads; + private boolean lowPriority; + + /** + * Creates an empty scene. + */ + public Scene() { + lightServer = new LightServer(this); + instanceList = new InstanceList(); + infiniteInstanceList = new InstanceList(); + acceltype = "auto"; + stats = new Statistics(); + + bakingViewDependent = false; + bakingInstance = null; + bakingPrimitives = null; + bakingAccel = null; + + camera = null; + imageWidth = 640; + imageHeight = 480; + threads = 0; + lowPriority = true; + + rebuildAccel = true; + } + + /** + * Get number of allowed threads for multi-threaded operations. + * + * @return number of threads that can be started + */ + public int getThreads() { + return threads <= 0 ? Runtime.getRuntime().availableProcessors() : threads; + } + + /** + * Get the priority level to assign to multi-threaded operations. + * + * @return thread priority + */ + public int getThreadPriority() { + return lowPriority ? Thread.MIN_PRIORITY : Thread.NORM_PRIORITY; + } + + /** + * Sets the current camera (no support for multiple cameras yet). + * + * @param camera camera to be used as the viewpoint for the scene + */ + public void setCamera(Camera camera) { + this.camera = camera; + } + + Camera getCamera() { + return camera; + } + + /** + * Update the instance lists for this scene. + * + * @param instances regular instances + * @param infinite infinite instances (no bounds) + */ + public void setInstanceLists(Instance[] instances, Instance[] infinite) { + infiniteInstanceList = new InstanceList(infinite); + instanceList = new InstanceList(instances); + rebuildAccel = true; + } + + /** + * Update the light list for this scene. + * + * @param lights array of light source objects + */ + public void setLightList(LightSource[] lights) { + lightServer.setLights(lights); + } + + /** + * Enables shader overiding (set null to disable). The specified shader will + * be used to shade all surfaces + * + * @param shader shader to run over all surfaces, or null to + * disable overriding + * @param photonOverride true to override photon scattering + * with this shader or false to run the regular + * shaders + */ + public void setShaderOverride(Shader shader, boolean photonOverride) { + lightServer.setShaderOverride(shader, photonOverride); + } + + /** + * The provided instance will be considered for lightmap baking. If the + * specified instance is null, lightmap baking will be + * disabled and normal rendering will occur. + * + * @param instance instance to bake + */ + public void setBakingInstance(Instance instance) { + bakingInstance = instance; + } + + /** + * Get the radiance seen through a particular pixel + * + * @param istate intersection state for ray tracing + * @param rx pixel x coordinate + * @param ry pixel y coordinate + * @param lensU DOF sampling variable + * @param lensV DOF sampling variable + * @param time motion blur sampling variable + * @param instance QMC instance seed + * @return a shading state for the intersected primitive, or + * null if nothing is seen through the specifieFd + * point + */ + public ShadingState getRadiance(IntersectionState istate, float rx, float ry, double lensU, double lensV, double time, int instance, int dim, ShadingCache cache) { + istate.numEyeRays++; + float sceneTime = camera.getTime((float) time); + if (bakingPrimitives == null) { + Ray r = camera.getRay(rx, ry, imageWidth, imageHeight, lensU, lensV, sceneTime); + return r != null ? lightServer.getRadiance(rx, ry, sceneTime, instance, dim, r, istate, cache) : null; + } else { + Ray r = new Ray(rx / imageWidth, ry / imageHeight, -1, 0, 0, 1); + traceBake(r, istate); + if (!istate.hit()) + return null; + ShadingState state = ShadingState.createState(istate, rx, ry, sceneTime, r, instance, dim, lightServer); + bakingPrimitives.prepareShadingState(state); + if (bakingViewDependent) + state.setRay(camera.getRay(state.getPoint(), sceneTime)); + else { + Point3 p = state.getPoint(); + Vector3 n = state.getNormal(); + // create a ray coming from directly above the point being + // shaded + Ray incoming = new Ray(p.x + n.x, p.y + n.y, p.z + n.z, -n.x, -n.y, -n.z); + incoming.setMax(1); + state.setRay(incoming); + } + lightServer.shadeBakeResult(state); + return state; + } + } + + /** + * Get scene world space bounding box. + * + * @return scene bounding box + */ + public BoundingBox getBounds() { + return instanceList.getWorldBounds(null); + } + + public void accumulateStats(IntersectionState state) { + stats.accumulate(state); + } + + public void accumulateStats(ShadingCache cache) { + stats.accumulate(cache); + } + + void trace(Ray r, IntersectionState state) { + // stats + state.numRays++; + // reset object + state.instance = null; + state.current = null; + for (int i = 0; i < infiniteInstanceList.getNumPrimitives(); i++) + infiniteInstanceList.intersectPrimitive(r, i, state); + // reset for next accel structure + state.current = null; + intAccel.intersect(r, state); + } + + Color traceShadow(Ray r, IntersectionState state) { + state.numShadowRays++; + trace(r, state); + return state.hit() ? Color.WHITE : Color.BLACK; + } + + void traceBake(Ray r, IntersectionState state) { + // set the instance as if tracing a regular instanced object + state.current = bakingInstance; + // reset object + state.instance = null; + bakingAccel.intersect(r, state); + } + + private void createAreaLightInstances() { + ArrayList infiniteAreaLights = null; + ArrayList areaLights = null; + // create an area light instance from each light source if possible + for (LightSource l : lightServer.lights) { + Instance lightInstance = l.createInstance(); + if (lightInstance != null) { + if (lightInstance.getBounds() == null) { + if (infiniteAreaLights == null) + infiniteAreaLights = new ArrayList(); + infiniteAreaLights.add(lightInstance); + } else { + if (areaLights == null) + areaLights = new ArrayList(); + areaLights.add(lightInstance); + } + } + } + // add area light sources to the list of instances if they exist + if (infiniteAreaLights != null && infiniteAreaLights.size() > 0) + infiniteInstanceList.addLightSourceInstances(infiniteAreaLights.toArray(new Instance[infiniteAreaLights.size()])); + else + infiniteInstanceList.clearLightSources(); + if (areaLights != null && areaLights.size() > 0) + instanceList.addLightSourceInstances(areaLights.toArray(new Instance[areaLights.size()])); + else + instanceList.clearLightSources(); + // FIXME: this _could_ be done incrementally to avoid top-level rebuilds + // each frame + rebuildAccel = true; + } + + private void removeAreaLightInstances() { + infiniteInstanceList.clearLightSources(); + instanceList.clearLightSources(); + } + + /** + * Render the scene using the specified options, image sampler and display. + * + * @param options rendering options object + * @param sampler image sampler + * @param display display to send the final image to, a default display will + * be created if null + */ + public void render(Options options, ImageSampler sampler, Display display) { + stats.reset(); + if (display == null) + display = new FrameDisplay(); + + if (bakingInstance != null) { + UI.printDetailed(Module.SCENE, "Creating primitives for lightmapping ..."); + bakingPrimitives = bakingInstance.getBakingPrimitives(); + if (bakingPrimitives == null) { + UI.printError(Module.SCENE, "Lightmap baking is not supported for the given instance."); + return; + } + int n = bakingPrimitives.getNumPrimitives(); + UI.printInfo(Module.SCENE, "Building acceleration structure for lightmapping (%d num primitives) ...", n); + bakingAccel = AccelerationStructureFactory.create("auto", n, true); + bakingAccel.build(bakingPrimitives); + } else { + bakingPrimitives = null; + bakingAccel = null; + } + bakingViewDependent = options.getBoolean("baking.viewdep", bakingViewDependent); + + if ((bakingInstance != null && bakingViewDependent && camera == null) || (bakingInstance == null && camera == null)) { + UI.printError(Module.SCENE, "No camera found"); + return; + } + + // read from options + threads = options.getInt("threads", 0); + lowPriority = options.getBoolean("threads.lowPriority", true); + imageWidth = options.getInt("resolutionX", 640); + imageHeight = options.getInt("resolutionY", 480); + // limit resolution to 16k + imageWidth = MathUtils.clamp(imageWidth, 1, 1 << 14); + imageHeight = MathUtils.clamp(imageHeight, 1, 1 << 14); + + // prepare lights + createAreaLightInstances(); + + // get acceleration structure info + // count scene primitives + long numPrimitives = 0; + for (int i = 0; i < instanceList.getNumPrimitives(); i++) + numPrimitives += instanceList.getNumPrimitives(i); + UI.printInfo(Module.SCENE, "Scene stats:"); + UI.printInfo(Module.SCENE, " * Infinite instances: %d", infiniteInstanceList.getNumPrimitives()); + UI.printInfo(Module.SCENE, " * Instances: %d", instanceList.getNumPrimitives()); + UI.printInfo(Module.SCENE, " * Primitives: %d", numPrimitives); + String accelName = options.getString("accel", null); + if (accelName != null) { + rebuildAccel = rebuildAccel || !acceltype.equals(accelName); + acceltype = accelName; + } + UI.printInfo(Module.SCENE, " * Instance accel: %s", acceltype); + if (rebuildAccel) { + intAccel = AccelerationStructureFactory.create(acceltype, instanceList.getNumPrimitives(), false); + intAccel.build(instanceList); + rebuildAccel = false; + } + UI.printInfo(Module.SCENE, " * Scene bounds: %s", getBounds()); + UI.printInfo(Module.SCENE, " * Scene center: %s", getBounds().getCenter()); + UI.printInfo(Module.SCENE, " * Scene diameter: %.2f", getBounds().getExtents().length()); + UI.printInfo(Module.SCENE, " * Lightmap bake: %s", bakingInstance != null ? (bakingViewDependent ? "view" : "ortho") : "off"); + if (sampler == null) + return; + if (!lightServer.build(options)) + return; + // render + UI.printInfo(Module.SCENE, "Rendering ..."); + stats.setResolution(imageWidth, imageHeight); + sampler.prepare(options, this, imageWidth, imageHeight); + sampler.render(display); + // show statistics + stats.displayStats(); + lightServer.showStats(); + // discard area lights + removeAreaLightInstances(); + // discard baking tesselation/accel structure + bakingPrimitives = null; + bakingAccel = null; + UI.printInfo(Module.SCENE, "Done."); + } + + /** + * Create a photon map as prescribed by the given {@link PhotonStore}. + * + * @param map object that will recieve shot photons + * @param type type of photons being shot + * @param seed QMC seed parameter + * @return true upon success + */ + public boolean calculatePhotons(PhotonStore map, String type, int seed, Options options) { + return lightServer.calculatePhotons(map, type, seed, options); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/SceneParser.java b/src/main/java/org/sunflow/core/SceneParser.java new file mode 100644 index 0000000..369a7c7 --- /dev/null +++ b/src/main/java/org/sunflow/core/SceneParser.java @@ -0,0 +1,20 @@ +package org.sunflow.core; + +import org.sunflow.SunflowAPI; +import org.sunflow.SunflowAPIInterface; + +/** + * Simple interface to allow for scene creation from arbitrary file formats. + */ +public interface SceneParser { + /** + * Parse the specified file to create a scene description into the provided + * {@link SunflowAPI} object. + * + * @param filename filename to parse + * @param api scene to parse the file into + * @return true upon sucess, or false if + * errors have occured. + */ + public boolean parse(String filename, SunflowAPIInterface api); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/Shader.java b/src/main/java/org/sunflow/core/Shader.java new file mode 100644 index 0000000..dc9d14c --- /dev/null +++ b/src/main/java/org/sunflow/core/Shader.java @@ -0,0 +1,29 @@ +package org.sunflow.core; + +import org.sunflow.image.Color; + +/** + * A shader represents a particular light-surface interaction. + */ +public interface Shader extends RenderObject { + /** + * Gets the radiance for a specified rendering state. When this method is + * called, you can assume that a hit has been registered in the state and + * that the hit surface information has been computed. + * + * @param state current render state + * @return color emitted or reflected by the shader + */ + public Color getRadiance(ShadingState state); + + /** + * Scatter a photon with the specied power. Incoming photon direction is + * specified by the ray attached to the current render state. This method + * can safely do nothing if photon scattering is not supported or relevant + * for the shader type. + * + * @param state current state + * @param power power of the incoming photon. + */ + public void scatterPhoton(ShadingState state, Color power); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/ShadingCache.java b/src/main/java/org/sunflow/core/ShadingCache.java new file mode 100644 index 0000000..c7e9faa --- /dev/null +++ b/src/main/java/org/sunflow/core/ShadingCache.java @@ -0,0 +1,75 @@ +package org.sunflow.core; + +import org.sunflow.image.Color; + +public class ShadingCache { + private Sample first; + private int depth; + // stats + long hits; + long misses; + long sumDepth; + long numCaches; + + private static class Sample { + Instance i; + Shader s; + float nx, ny, nz; + float dx, dy, dz; + Color c; + Sample next; // linked list + } + + public ShadingCache() { + reset(); + hits = 0; + misses = 0; + } + + public void reset() { + sumDepth += depth; + if (depth > 0) + numCaches++; + first = null; + depth = 0; + } + + public Color lookup(ShadingState state, Shader shader) { + if (state.getNormal() == null) + return null; + // search further + for (Sample s = first; s != null; s = s.next) { + if (s.i != state.getInstance()) + continue; + if (s.s != shader) + continue; + if (state.getRay().dot(s.dx, s.dy, s.dz) < 0.999f) + continue; + if (state.getNormal().dot(s.nx, s.ny, s.nz) < 0.99f) + continue; + // we have a match + hits++; + return s.c; + } + misses++; + return null; + } + + public void add(ShadingState state, Shader shader, Color c) { + if (state.getNormal() == null) + return; + depth++; + Sample s = new Sample(); + s.i = state.getInstance(); + s.s = shader; + s.c = c; + s.dx = state.getRay().dx; + s.dy = state.getRay().dy; + s.dz = state.getRay().dz; + s.nx = state.getNormal().x; + s.ny = state.getNormal().y; + s.nz = state.getNormal().z; + s.next = first; + first = s; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/ShadingState.java b/src/main/java/org/sunflow/core/ShadingState.java new file mode 100644 index 0000000..d4d3613 --- /dev/null +++ b/src/main/java/org/sunflow/core/ShadingState.java @@ -0,0 +1,929 @@ +package org.sunflow.core; + +import java.util.Iterator; + +import org.sunflow.core.primitive.TriangleMesh; +import org.sunflow.image.Color; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point2; +import org.sunflow.math.Point3; +import org.sunflow.math.QMC; +import org.sunflow.math.Vector3; + +/** + * Represents a point to be shaded and provides various options for the shading + * of this point, including spawning of new rays. + */ +public final class ShadingState implements Iterable { + private IntersectionState istate; + private LightServer server; + private float rx, ry, time; + private Color result; + private Point3 p; + private Vector3 n; + private Point2 tex; + private Vector3 ng; + private OrthoNormalBasis basis; + private float cosND; + private float bias; + private boolean behind; + private float hitU, hitV, hitW; + private Instance instance; + private int primitiveID; + private Matrix4 o2w, w2o; + private Ray r; + private int d; // quasi monte carlo instance variables + private int i; // quasi monte carlo instance variables + private double qmcD0I; + private double qmcD1I; + private Shader shader; + private Modifier modifier; + private int diffuseDepth; + private int reflectionDepth; + private int refractionDepth; + private boolean includeLights; + private boolean includeSpecular; + private LightSample lightSample; + private PhotonStore map; + + static ShadingState createPhotonState(Ray r, IntersectionState istate, int i, PhotonStore map, LightServer server) { + ShadingState s = new ShadingState(null, istate, r, i, 4); + s.server = server; + s.map = map; + return s; + + } + + static ShadingState createState(IntersectionState istate, float rx, float ry, float time, Ray r, int i, int d, LightServer server) { + ShadingState s = new ShadingState(null, istate, r, i, d); + s.server = server; + s.rx = rx; + s.ry = ry; + s.time = time; + return s; + } + + static ShadingState createDiffuseBounceState(ShadingState previous, Ray r, int i) { + ShadingState s = new ShadingState(previous, previous.istate, r, i, 2); + s.diffuseDepth++; + return s; + } + + static ShadingState createGlossyBounceState(ShadingState previous, Ray r, int i) { + ShadingState s = new ShadingState(previous, previous.istate, r, i, 2); + s.includeLights = false; + s.includeSpecular = false; + s.reflectionDepth++; + return s; + } + + static ShadingState createReflectionBounceState(ShadingState previous, Ray r, int i) { + ShadingState s = new ShadingState(previous, previous.istate, r, i, 2); + s.reflectionDepth++; + return s; + } + + static ShadingState createRefractionBounceState(ShadingState previous, Ray r, int i) { + ShadingState s = new ShadingState(previous, previous.istate, r, i, 2); + s.refractionDepth++; + return s; + } + + static ShadingState createFinalGatherState(ShadingState state, Ray r, int i) { + ShadingState finalGatherState = new ShadingState(state, state.istate, r, i, 2); + finalGatherState.diffuseDepth++; + finalGatherState.includeLights = false; + finalGatherState.includeSpecular = false; + return finalGatherState; + } + + private ShadingState(ShadingState previous, IntersectionState istate, Ray r, int i, int d) { + this.r = r; + this.istate = istate; + this.i = i; + this.d = d; + time = istate.time; + instance = istate.instance; // local copy + primitiveID = istate.id; + hitU = istate.u; + hitV = istate.v; + hitW = istate.w; + // get matrices for current time + o2w = instance.getObjectToWorld(time); + w2o = instance.getWorldToObject(time); + if (previous == null) { + diffuseDepth = 0; + reflectionDepth = 0; + refractionDepth = 0; + + } else { + diffuseDepth = previous.diffuseDepth; + reflectionDepth = previous.reflectionDepth; + refractionDepth = previous.refractionDepth; + server = previous.server; + map = previous.map; + rx = previous.rx; + ry = previous.ry; + this.i += previous.i; + this.d += previous.d; + } + behind = false; + cosND = Float.NaN; + includeLights = includeSpecular = true; + qmcD0I = QMC.halton(this.d, this.i); + qmcD1I = QMC.halton(this.d + 1, this.i); + result = null; + bias = 0.001f; + } + + final void setRay(Ray r) { + this.r = r; + } + + /** + * Create objects needed for surface shading: point, normal, texture + * coordinates and basis. + */ + public final void init() { + p = new Point3(); + n = new Vector3(); + tex = new Point2(); + ng = new Vector3(); + basis = null; + } + + /** + * Run the shader at this surface point. + * + * @return shaded result + */ + public final Color shade() { + return server.shadeHit(this); + } + + final void correctShadingNormal() { + // correct shading normals pointing the wrong way + if (Vector3.dot(n, ng) < 0) { + n.negate(); + basis.flipW(); + } + } + + /** + * Flip the surface normals to ensure they are facing the current ray. This + * method also offsets the shading point away from the surface so that new + * rays will not intersect the same surface again by mistake. + */ + public final void faceforward() { + // make sure we are on the right side of the material + if (r.dot(ng) < 0) { + } else { + // this ensure the ray and the geomtric normal are pointing in the + // same direction + ng.negate(); + n.negate(); + basis.flipW(); + behind = true; + } + cosND = Math.max(-r.dot(n), 0); // can't be negative + // offset the shaded point away from the surface to prevent + // self-intersection errors + if (Math.abs(ng.x) > Math.abs(ng.y) && Math.abs(ng.x) > Math.abs(ng.z)) + bias = Math.max(bias, 25 * Math.ulp(Math.abs(p.x))); + else if (Math.abs(ng.y) > Math.abs(ng.z)) + bias = Math.max(bias, 25 * Math.ulp(Math.abs(p.y))); + else + bias = Math.max(bias, 25 * Math.ulp(Math.abs(p.z))); + p.x += bias * ng.x; + p.y += bias * ng.y; + p.z += bias * ng.z; + } + + /** + * Get x coordinate of the pixel being shaded. + * + * @return pixel x coordinate + */ + public final float getRasterX() { + return rx; + } + + /** + * Get y coordinate of the pixel being shaded. + * + * @return pixel y coordinate + */ + public final float getRasterY() { + return ry; + } + + /** + * Cosine between the shading normal and the ray. This is set by + * {@link #faceforward()}. + * + * @return cosine between shading normal and the ray + */ + public final float getCosND() { + return cosND; + } + + /** + * Returns true if the ray hit the surface from behind. This is set by + * {@link #faceforward()}. + * + * @return true if the surface was hit from behind. + */ + public final boolean isBehind() { + return behind; + } + + final IntersectionState getIntersectionState() { + return istate; + } + + /** + * Get u barycentric coordinate of the intersection point. + * + * @return u barycentric coordinate + */ + public final float getU() { + return hitU; + } + + /** + * Get v barycentric coordinate of the intersection point. + * + * @return v barycentric coordinate + */ + public final float getV() { + return hitV; + } + + /** + * Get w barycentric coordinate of the intersection point. + * + * @return w barycentric coordinate + */ + public final float getW() { + return hitW; + } + + /** + * Get the instance which was intersected + * + * @return intersected instance object + */ + public final Instance getInstance() { + return instance; + } + + /** + * Get the primitive ID which was intersected + * + * @return intersected primitive ID + */ + public final int getPrimitiveID() { + return primitiveID; + } + + /** + * Transform the given point from object space to world space. A new + * {@link Point3} object is returned. + * + * @param p object space position to transform + * @return transformed position + */ + public Point3 transformObjectToWorld(Point3 p) { + return o2w == null ? new Point3(p) : o2w.transformP(p); + } + + /** + * Transform the given point from world space to object space. A new + * {@link Point3} object is returned. + * + * @param p world space position to transform + * @return transformed position + */ + public Point3 transformWorldToObject(Point3 p) { + return w2o == null ? new Point3(p) : w2o.transformP(p); + } + + /** + * Transform the given normal from object space to world space. A new + * {@link Vector3} object is returned. + * + * @param n object space normal to transform + * @return transformed normal + */ + public Vector3 transformNormalObjectToWorld(Vector3 n) { + return o2w == null ? new Vector3(n) : w2o.transformTransposeV(n); + } + + /** + * Transform the given normal from world space to object space. A new + * {@link Vector3} object is returned. + * + * @param n world space normal to transform + * @return transformed normal + */ + public Vector3 transformNormalWorldToObject(Vector3 n) { + return o2w == null ? new Vector3(n) : o2w.transformTransposeV(n); + } + + /** + * Transform the given vector from object space to world space. A new + * {@link Vector3} object is returned. + * + * @param v object space vector to transform + * @return transformed vector + */ + public Vector3 transformVectorObjectToWorld(Vector3 v) { + return o2w == null ? new Vector3(v) : o2w.transformV(v); + } + + /** + * Transform the given vector from world space to object space. A new + * {@link Vector3} object is returned. + * + * @param v world space vector to transform + * @return transformed vector + */ + public Vector3 transformVectorWorldToObject(Vector3 v) { + return o2w == null ? new Vector3(v) : w2o.transformV(v); + } + + final void setResult(Color c) { + result = c; + } + + /** + * Get the result of shading this point + * + * @return shaded result + */ + public final Color getResult() { + return result; + } + + final LightServer getLightServer() { + return server; + } + + /** + * Add the specified light sample to the list of lights to be used + * + * @param sample a valid light sample + */ + public final void addSample(LightSample sample) { + // add to list + sample.next = lightSample; + lightSample = sample; + } + + /** + * Get a QMC sample from an infinite sequence. + * + * @param j sample number (starts from 0) + * @param dim dimension to sample + * @return pseudo-random value in [0,1) + */ + public final double getRandom(int j, int dim) { + switch (dim) { + case 0: + return QMC.mod1(qmcD0I + QMC.halton(0, j)); + case 1: + return QMC.mod1(qmcD1I + QMC.halton(1, j)); + default: + return QMC.mod1(QMC.halton(d + dim, i) + QMC.halton(dim, j)); + } + } + + /** + * Get a QMC sample from a finite sequence of n elements. This provides + * better stratification than the infinite version, but does not allow for + * adaptive sampling. + * + * @param j sample number (starts from 0) + * @param dim dimension to sample + * @param n number of samples + * @return pseudo-random value in [0,1) + */ + public final double getRandom(int j, int dim, int n) { + switch (dim) { + case 0: + return QMC.mod1(qmcD0I + (double) j / (double) n); + case 1: + return QMC.mod1(qmcD1I + QMC.halton(0, j)); + default: + return QMC.mod1(QMC.halton(d + dim, i) + QMC.halton(dim - 1, j)); + } + } + + /** + * Checks to see if the shader should include emitted light. + * + * @return true if emitted light should be included, + * false otherwise + */ + public final boolean includeLights() { + return includeLights; + } + + /** + * Checks to see if the shader should include specular terms. + * + * @return true if specular terms should be included, + * false otherwise + */ + public final boolean includeSpecular() { + return includeSpecular; + } + + /** + * Get the shader to be used to shade this surface. + * + * @return shader to be used + */ + public final Shader getShader() { + return shader; + } + + /** + * Record which shader should be executed for the intersected surface. + * + * @param shader surface shader to use to shade the current intersection + * point + */ + public final void setShader(Shader shader) { + this.shader = shader; + } + + final Modifier getModifier() { + return modifier; + } + + /** + * Record which modifier should be applied to the intersected surface + * + * @param modifier modifier to use the change this shading state + */ + public final void setModifier(Modifier modifier) { + this.modifier = modifier; + } + + /** + * Get the current total tracing depth. First generation rays have a depth + * of 0. + * + * @return current tracing depth + */ + public final int getDepth() { + return diffuseDepth + reflectionDepth + refractionDepth; + } + + /** + * Get the current diffuse tracing depth. This is the number of diffuse + * surfaces reflected from. + * + * @return current diffuse tracing depth + */ + public final int getDiffuseDepth() { + return diffuseDepth; + } + + /** + * Get the current reflection tracing depth. This is the number of specular + * surfaces reflected from. + * + * @return current reflection tracing depth + */ + public final int getReflectionDepth() { + return reflectionDepth; + } + + /** + * Get the current refraction tracing depth. This is the number of specular + * surfaces refracted from. + * + * @return current refraction tracing depth + */ + public final int getRefractionDepth() { + return refractionDepth; + } + + /** + * Get hit point. + * + * @return hit point + */ + public final Point3 getPoint() { + return p; + } + + /** + * Get shading normal at the hit point. This may differ from the geometric + * normal + * + * @return shading normal + */ + public final Vector3 getNormal() { + return n; + } + + /** + * Get texture coordinates at the hit point. + * + * @return texture coordinate + */ + public final Point2 getUV() { + return tex; + } + + /** + * Gets the geometric normal of the current hit point. + * + * @return geometric normal of the current hit point + */ + public final Vector3 getGeoNormal() { + return ng; + } + + /** + * Gets the local orthonormal basis for the current hit point. + * + * @return local basis or null if undefined + */ + public final OrthoNormalBasis getBasis() { + return basis; + } + + /** + * Define the orthonormal basis for the current hit point. + * + * @param basis + */ + public final void setBasis(OrthoNormalBasis basis) { + this.basis = basis; + } + + /** + * Gets the ray that is associated with this state. + * + * @return ray associated with this state. + */ + public final Ray getRay() { + return r; + } + + /** + * Get a transformation matrix that will transform camera space points into + * world space. + * + * @return camera to world transform + */ + public final Matrix4 getCameraToWorld() { + Camera c = server.getScene().getCamera(); + return c != null ? c.getCameraToWorld(time) : Matrix4.IDENTITY; + } + + /** + * Get a transformation matrix that will transform world space points into + * camera space. + * + * @return world to camera transform + */ + public final Matrix4 getWorldToCamera() { + Camera c = server.getScene().getCamera(); + return c != null ? c.getWorldToCamera(time) : Matrix4.IDENTITY; + } + + /** + * Get the three triangle corners in object space if the hit object is a + * mesh, returns false otherwise. + * + * @param p array of 3 points + * @return true if the points were read succesfully, + * falseotherwise + */ + public final boolean getTrianglePoints(Point3[] p) { + PrimitiveList prims = instance.getGeometry().getPrimitiveList(); + if (prims instanceof TriangleMesh) { + TriangleMesh m = (TriangleMesh) prims; + m.getPoint(primitiveID, 0, p[0] = new Point3()); + m.getPoint(primitiveID, 1, p[1] = new Point3()); + m.getPoint(primitiveID, 2, p[2] = new Point3()); + return true; + } + return false; + } + + /** + * Initialize the use of light samples. Prepares a list of visible lights + * from the current point. + */ + public final void initLightSamples() { + server.initLightSamples(this); + } + + /** + * Add caustic samples to the current light sample set. This method does + * nothing if caustics are not enabled. + */ + public final void initCausticSamples() { + server.initCausticSamples(this); + } + + /** + * Returns the color obtained by recursively tracing the specified ray. The + * reflection is assumed to be glossy. + * + * @param r ray to trace + * @param i instance number of this sample + * @return color observed along specified ray. + */ + public final Color traceGlossy(Ray r, int i) { + return server.traceGlossy(this, r, i); + } + + /** + * Returns the color obtained by recursively tracing the specified ray. The + * reflection is assumed to be specular. + * + * @param r ray to trace + * @param i instance number of this sample + * @return color observed along specified ray. + */ + public final Color traceReflection(Ray r, int i) { + return server.traceReflection(this, r, i); + } + + /** + * Returns the color obtained by recursively tracing the specified ray. + * + * @param r ray to trace + * @param i instance number of this sample + * @return color observed along specified ray. + */ + public final Color traceRefraction(Ray r, int i) { + // this assumes the refraction ray is pointing away from the normal + r.ox -= 2 * bias * ng.x; + r.oy -= 2 * bias * ng.y; + r.oz -= 2 * bias * ng.z; + return server.traceRefraction(this, r, i); + } + + /** + * Trace transparency, this is equivalent to tracing a refraction ray in the + * incoming ray direction. + * + * @return color observed behind the current shading point + */ + public final Color traceTransparency() { + return traceRefraction(new Ray(p.x, p.y, p.z, r.dx, r.dy, r.dz), 0); + } + + /** + * Trace a shadow ray against the scene, and computes the accumulated + * opacity along the ray. + * + * @param r ray to trace + * @return opacity along the shadow ray + */ + public final Color traceShadow(Ray r) { + return server.getScene().traceShadow(r, istate); + } + + /** + * Records a photon at the specified location. + * + * @param dir incoming direction of the photon + * @param power photon power + * @param diffuse diffuse reflectance at the given point + */ + public final void storePhoton(Vector3 dir, Color power, Color diffuse) { + map.store(this, dir, power, diffuse); + } + + /** + * Trace a new photon from the current location. This assumes that the + * photon was reflected by a specular surface. + * + * @param r ray to trace photon along + * @param power power of the new photon + */ + public final void traceReflectionPhoton(Ray r, Color power) { + if (map.allowReflectionBounced()) + server.traceReflectionPhoton(this, r, power); + } + + /** + * Trace a new photon from the current location. This assumes that the + * photon was refracted by a specular surface. + * + * @param r ray to trace photon along + * @param power power of the new photon + */ + public final void traceRefractionPhoton(Ray r, Color power) { + if (map.allowRefractionBounced()) { + // this assumes the refraction ray is pointing away from the normal + r.ox -= 0.002f * ng.x; + r.oy -= 0.002f * ng.y; + r.oz -= 0.002f * ng.z; + server.traceRefractionPhoton(this, r, power); + } + } + + /** + * Trace a new photon from the current location. This assumes that the + * photon was reflected by a diffuse surface. + * + * @param r ray to trace photon along + * @param power power of the new photon + */ + public final void traceDiffusePhoton(Ray r, Color power) { + if (map.allowDiffuseBounced()) + server.traceDiffusePhoton(this, r, power); + } + + /** + * Returns the glboal diffuse radiance estimate given by the current + * {@link GIEngine} if present. + * + * @return global diffuse radiance estimate + */ + public final Color getGlobalRadiance() { + return server.getGlobalRadiance(this); + } + + /** + * Gets the total irradiance reaching the current point from diffuse + * surfaces. + * + * @param diffuseReflectance diffuse reflectance at the current point, can + * be used for importance tracking + * @return indirect diffuse irradiance reaching the point + */ + public final Color getIrradiance(Color diffuseReflectance) { + return server.getIrradiance(this, diffuseReflectance); + } + + /** + * Trace a final gather ray and return the intersection result as a new + * render state + * + * @param r ray to shoot + * @param i instance of the ray + * @return new render state object corresponding to the intersection result + */ + public final ShadingState traceFinalGather(Ray r, int i) { + return server.traceFinalGather(this, r, i); + } + + /** + * Simple black and white ambient occlusion. + * + * @param samples number of sample rays + * @param maxDist maximum length of the rays + * @return occlusion color + */ + public final Color occlusion(int samples, float maxDist) { + return occlusion(samples, maxDist, Color.WHITE, Color.BLACK); + } + + /** + * Ambient occlusion routine, returns a value between bright and dark + * depending on the amount of geometric occlusion in the scene. + * + * @param samples number of sample rays + * @param maxDist maximum length of the rays + * @param bright color when nothing is occluded + * @param dark color when fully occluded + * @return occlusion color + */ + public final Color occlusion(int samples, float maxDist, Color bright, Color dark) { + if (n == null) { + // in case we got called on a geometry without orientation + return bright; + } + // make sure we are on the right side of the material + faceforward(); + OrthoNormalBasis onb = getBasis(); + Vector3 w = new Vector3(); + Color result = Color.black(); + for (int i = 0; i < samples; i++) { + float xi = (float) getRandom(i, 0, samples); + float xj = (float) getRandom(i, 1, samples); + float phi = (float) (2 * Math.PI * xi); + float cosPhi = (float) Math.cos(phi); + float sinPhi = (float) Math.sin(phi); + float sinTheta = (float) Math.sqrt(xj); + float cosTheta = (float) Math.sqrt(1.0f - xj); + w.x = cosPhi * sinTheta; + w.y = sinPhi * sinTheta; + w.z = cosTheta; + onb.transform(w); + Ray r = new Ray(p, w); + r.setMax(maxDist); + result.add(Color.blend(bright, dark, traceShadow(r))); + } + return result.mul(1.0f / samples); + } + + /** + * Computes a plain diffuse response to the current light samples and global + * illumination. + * + * @param diff diffuse color + * @return shaded result + */ + public final Color diffuse(Color diff) { + // integrate a diffuse function + Color lr = Color.black(); + if (diff.isBlack()) + return lr; + for (LightSample sample : this) + lr.madd(sample.dot(n), sample.getDiffuseRadiance()); + lr.add(getIrradiance(diff)); + return lr.mul(diff).mul(1.0f / (float) Math.PI); + } + + /** + * Computes a phong specular response to the current light samples and + * global illumination. + * + * @param spec specular color + * @param power phong exponent + * @param numRays number of glossy rays to trace + * @return shaded color + */ + public final Color specularPhong(Color spec, float power, int numRays) { + // integrate a phong specular function + Color lr = Color.black(); + if (!includeSpecular || spec.isBlack()) + return lr; + // reflected direction + float dn = 2 * cosND; + Vector3 refDir = new Vector3(); + refDir.x = (dn * n.x) + r.dx; + refDir.y = (dn * n.y) + r.dy; + refDir.z = (dn * n.z) + r.dz; + // direct lighting + for (LightSample sample : this) { + float cosNL = sample.dot(n); + float cosLR = sample.dot(refDir); + if (cosLR > 0) + lr.madd(cosNL * (float) Math.pow(cosLR, power), sample.getSpecularRadiance()); + } + // indirect lighting + if (numRays > 0) { + int numSamples = getDepth() == 0 ? numRays : 1; + OrthoNormalBasis onb = OrthoNormalBasis.makeFromW(refDir); + float mul = (2.0f * (float) Math.PI / (power + 1)) / numSamples; + for (int i = 0; i < numSamples; i++) { + // specular indirect lighting + double r1 = getRandom(i, 0, numSamples); + double r2 = getRandom(i, 1, numSamples); + double u = 2 * Math.PI * r1; + double s = (float) Math.pow(r2, 1 / (power + 1)); + double s1 = (float) Math.sqrt(1 - s * s); + Vector3 w = new Vector3((float) (Math.cos(u) * s1), (float) (Math.sin(u) * s1), (float) s); + w = onb.transform(w, new Vector3()); + float wn = Vector3.dot(w, n); + if (wn > 0) + lr.madd(wn * mul, traceGlossy(new Ray(p, w), i)); + } + } + lr.mul(spec).mul((power + 2) / (2.0f * (float) Math.PI)); + return lr; + } + + /** + * Allows iteration over current light samples. + */ + public Iterator iterator() { + return new LightSampleIterator(lightSample); + } + + private static class LightSampleIterator implements Iterator { + private LightSample current; + + LightSampleIterator(LightSample head) { + current = head; + } + + public boolean hasNext() { + return current != null; + } + + public LightSample next() { + LightSample c = current; + current = current.next; + return c; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/Statistics.java b/src/main/java/org/sunflow/core/Statistics.java new file mode 100644 index 0000000..bb7b06c --- /dev/null +++ b/src/main/java/org/sunflow/core/Statistics.java @@ -0,0 +1,83 @@ +package org.sunflow.core; + +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class Statistics { + // raytracing + private long numEyeRays; + private long numShadowRays; + private long numReflectionRays; + private long numGlossyRays; + private long numRefractionRays; + private long numRays; + private long numPixels; + // shading cache + private long cacheHits; + private long cacheMisses; + private long cacheSumDepth; + private long cacheNumCaches; + + Statistics() { + reset(); + } + + void reset() { + numEyeRays = 0; + numShadowRays = 0; + numReflectionRays = 0; + numGlossyRays = 0; + numRefractionRays = 0; + numRays = 0; + numPixels = 0; + cacheHits = 0; + cacheMisses = 0; + cacheSumDepth = 0; + cacheNumCaches = 0; + } + + void accumulate(IntersectionState state) { + numEyeRays += state.numEyeRays; + numShadowRays += state.numShadowRays; + numReflectionRays += state.numReflectionRays; + numGlossyRays += state.numGlossyRays; + numRefractionRays += state.numRefractionRays; + numRays += state.numRays; + } + + void accumulate(ShadingCache cache) { + cacheHits += cache.hits; + cacheMisses += cache.misses; + cacheSumDepth += cache.sumDepth; + cacheNumCaches += cache.numCaches; + } + + void setResolution(int w, int h) { + numPixels = w * h; + } + + void displayStats() { + // display raytracing stats + UI.printInfo(Module.SCENE, "Raytracing stats:"); + UI.printInfo(Module.SCENE, " * Rays traced: (per pixel) (per eye ray) (percentage)", numRays); + printRayTypeStats("eye", numEyeRays); + printRayTypeStats("shadow", numShadowRays); + printRayTypeStats("reflection", numReflectionRays); + printRayTypeStats("glossy", numGlossyRays); + printRayTypeStats("refraction", numRefractionRays); + printRayTypeStats("other", numRays - numEyeRays - numShadowRays - numReflectionRays - numGlossyRays - numRefractionRays); + printRayTypeStats("total", numRays); + if (cacheHits + cacheMisses > 0) { + UI.printInfo(Module.LIGHT, "Shading cache stats:"); + UI.printInfo(Module.LIGHT, " * Lookups: %d", cacheHits + cacheMisses); + UI.printInfo(Module.LIGHT, " * Hits: %d", cacheHits); + UI.printInfo(Module.LIGHT, " * Hit rate: %d%%", (100 * cacheHits) / (cacheHits + cacheMisses)); + UI.printInfo(Module.LIGHT, " * Average cache depth: %.2f", (double) cacheSumDepth / (double) cacheNumCaches); + } + } + + private void printRayTypeStats(String name, long n) { + if (n > 0) + UI.printInfo(Module.SCENE, " %-10s %11d %7.2f %7.2f %6.2f%%", name, n, (double) n / (double) numPixels, (double) n / (double) numEyeRays, (double) (n * 100) / (double) numRays); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/Tesselatable.java b/src/main/java/org/sunflow/core/Tesselatable.java new file mode 100644 index 0000000..672bb15 --- /dev/null +++ b/src/main/java/org/sunflow/core/Tesselatable.java @@ -0,0 +1,35 @@ +package org.sunflow.core; + +import org.sunflow.core.primitive.TriangleMesh; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; + +/** + * Represents an object which can be tesselated into a list of primitives such + * as a {@link TriangleMesh}. + */ +public interface Tesselatable extends RenderObject { + /** + * Tesselate this object into a {@link PrimitiveList}. This may return + * null if tesselation fails. + * + * @return a list of primitives generated by the tesselation + */ + public PrimitiveList tesselate(); + + /** + * Compute a bounding box of this object in world space, using the specified + * object-to-world transformation matrix. The bounds should be as exact as + * possible, if they are difficult or expensive to compute exactly, you may + * use {@link Matrix4#transform(BoundingBox)}. If the matrix is + * null no transformation is needed, and object space is + * equivalent to world space. This method may return null if + * these bounds are difficult or impossible to compute, in which case the + * tesselation will be executed right away and the bounds of the resulting + * primitives will be used. + * + * @param o2w object to world transformation matrix + * @return object bounding box in world space + */ + public BoundingBox getWorldBounds(Matrix4 o2w); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/Texture.java b/src/main/java/org/sunflow/core/Texture.java new file mode 100644 index 0000000..7719ce9 --- /dev/null +++ b/src/main/java/org/sunflow/core/Texture.java @@ -0,0 +1,123 @@ +package org.sunflow.core; + +import java.io.IOException; + +import org.sunflow.PluginRegistry; +import org.sunflow.image.Bitmap; +import org.sunflow.image.BitmapReader; +import org.sunflow.image.Color; +import org.sunflow.image.BitmapReader.BitmapFormatException; +import org.sunflow.image.formats.BitmapBlack; +import org.sunflow.math.MathUtils; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Vector3; +import org.sunflow.system.FileUtils; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +/** + * Represents a 2D texture, typically used by {@link Shader shaders}. + */ +public class Texture { + private String filename; + private boolean isLinear; + private Bitmap bitmap; + private int loaded; + + /** + * Creates a new texture from the specfied file. + * + * @param filename image file to load + * @param isLinear is the texture gamma corrected already? + */ + Texture(String filename, boolean isLinear) { + this.filename = filename; + this.isLinear = isLinear; + loaded = 0; + } + + private synchronized void load() { + if (loaded != 0) + return; + String extension = FileUtils.getExtension(filename); + try { + UI.printInfo(Module.TEX, "Reading texture bitmap from: \"%s\" ...", filename); + BitmapReader reader = PluginRegistry.bitmapReaderPlugins.createObject(extension); + if (reader != null) { + bitmap = reader.load(filename, isLinear); + if (bitmap.getWidth() == 0 || bitmap.getHeight() == 0) + bitmap = null; + } + if (bitmap == null) { + UI.printError(Module.TEX, "Bitmap reading failed"); + bitmap = new BitmapBlack(); + } else + UI.printDetailed(Module.TEX, "Texture bitmap reading complete: %dx%d pixels found", bitmap.getWidth(), bitmap.getHeight()); + } catch (IOException e) { + UI.printError(Module.TEX, "%s", e.getMessage()); + } catch (BitmapFormatException e) { + UI.printError(Module.TEX, "%s format error: %s", extension, e.getMessage()); + } + loaded = 1; + } + + public Bitmap getBitmap() { + if (loaded == 0) + load(); + return bitmap; + } + + /** + * Gets the color at location (x,y) in the texture. The lookup is performed + * using the fractional component of the coordinates, treating the texture + * as a unit square tiled in both directions. Bicubic filtering is performed + * on the four nearest pixels to the lookup point. + * + * @param x x coordinate into the texture + * @param y y coordinate into the texture + * @return filtered color at location (x,y) + */ + public Color getPixel(float x, float y) { + Bitmap bitmap = getBitmap(); + x = MathUtils.frac(x); + y = MathUtils.frac(y); + float dx = x * (bitmap.getWidth() - 1); + float dy = y * (bitmap.getHeight() - 1); + int ix0 = (int) dx; + int iy0 = (int) dy; + int ix1 = (ix0 + 1) % bitmap.getWidth(); + int iy1 = (iy0 + 1) % bitmap.getHeight(); + float u = dx - ix0; + float v = dy - iy0; + u = u * u * (3.0f - (2.0f * u)); + v = v * v * (3.0f - (2.0f * v)); + float k00 = (1.0f - u) * (1.0f - v); + Color c00 = bitmap.readColor(ix0, iy0); + float k01 = (1.0f - u) * v; + Color c01 = bitmap.readColor(ix0, iy1); + float k10 = u * (1.0f - v); + Color c10 = bitmap.readColor(ix1, iy0); + float k11 = u * v; + Color c11 = bitmap.readColor(ix1, iy1); + Color c = Color.mul(k00, c00); + c.madd(k01, c01); + c.madd(k10, c10); + c.madd(k11, c11); + return c; + } + + public Vector3 getNormal(float x, float y, OrthoNormalBasis basis) { + float[] rgb = getPixel(x, y).getRGB(); + return basis.transform(new Vector3(2 * rgb[0] - 1, 2 * rgb[1] - 1, 2 * rgb[2] - 1)).normalize(); + } + + public Vector3 getBump(float x, float y, OrthoNormalBasis basis, float scale) { + Bitmap bitmap = getBitmap(); + float dx = 1.0f / bitmap.getWidth(); + float dy = 1.0f / bitmap.getHeight(); + float b0 = getPixel(x, y).getLuminance(); + float bx = getPixel(x + dx, y).getLuminance(); + float by = getPixel(x, y + dy).getLuminance(); + return basis.transform(new Vector3(scale * (b0 - bx), scale * (b0 - by), 1)).normalize(); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/TextureCache.java b/src/main/java/org/sunflow/core/TextureCache.java new file mode 100644 index 0000000..52ba7b3 --- /dev/null +++ b/src/main/java/org/sunflow/core/TextureCache.java @@ -0,0 +1,47 @@ +package org.sunflow.core; + +import java.util.HashMap; + +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +/** + * Maintains a cache of all loaded texture maps. This is usefull if the same + * texture might be used more than once in your scene. + */ +public final class TextureCache { + private static HashMap textures = new HashMap(); + + private TextureCache() { + } + + /** + * Gets a reference to the texture specified by the given filename. If the + * texture has already been loaded the previous reference is returned, + * otherwise, a new texture is created. + * + * @param filename image file to load + * @param isLinear is the texture gamma corrected? + * @return texture object + * @see Texture + */ + public synchronized static Texture getTexture(String filename, boolean isLinear) { + if (textures.containsKey(filename)) { + UI.printInfo(Module.TEX, "Using cached copy for file \"%s\" ...", filename); + return textures.get(filename); + } + UI.printInfo(Module.TEX, "Using file \"%s\" ...", filename); + Texture t = new Texture(filename, isLinear); + textures.put(filename, t); + return t; + } + + /** + * Flush all textures from the cache, this will cause them to be reloaded + * anew the next time they are accessed. + */ + public synchronized static void flush() { + UI.printInfo(Module.TEX, "Flushing texture cache"); + textures.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/accel/BoundingIntervalHierarchy.java b/src/main/java/org/sunflow/core/accel/BoundingIntervalHierarchy.java new file mode 100644 index 0000000..6335006 --- /dev/null +++ b/src/main/java/org/sunflow/core/accel/BoundingIntervalHierarchy.java @@ -0,0 +1,613 @@ +package org.sunflow.core.accel; + +import org.sunflow.core.AccelerationStructure; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.math.BoundingBox; +import org.sunflow.system.Memory; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; +import org.sunflow.util.IntArray; + +public class BoundingIntervalHierarchy implements AccelerationStructure { + private int[] tree; + private int[] objects; + private PrimitiveList primitives; + private BoundingBox bounds; + private int maxPrims; + + public BoundingIntervalHierarchy() { + maxPrims = 2; + } + + public void build(PrimitiveList primitives) { + this.primitives = primitives; + int n = primitives.getNumPrimitives(); + UI.printDetailed(Module.ACCEL, "Getting bounding box ..."); + bounds = primitives.getWorldBounds(null); + objects = new int[n]; + for (int i = 0; i < n; i++) + objects[i] = i; + UI.printDetailed(Module.ACCEL, "Creating tree ..."); + int initialSize = 3 * (2 * 6 * n + 1); + IntArray tempTree = new IntArray((initialSize + 3) / 4); + BuildStats stats = new BuildStats(); + Timer t = new Timer(); + t.start(); + buildHierarchy(tempTree, objects, stats); + t.end(); + UI.printDetailed(Module.ACCEL, "Trimming tree ..."); + tree = tempTree.trim(); + // display stats + stats.printStats(); + UI.printDetailed(Module.ACCEL, " * Creation time: %s", t); + UI.printDetailed(Module.ACCEL, " * Usage of init: %6.2f%%", (double) (100.0 * tree.length) / initialSize); + UI.printDetailed(Module.ACCEL, " * Tree memory: %s", Memory.sizeof(tree)); + UI.printDetailed(Module.ACCEL, " * Indices memory: %s", Memory.sizeof(objects)); + } + + private static class BuildStats { + private int numNodes; + private int numLeaves; + private int sumObjects; + private int minObjects; + private int maxObjects; + private int sumDepth; + private int minDepth; + private int maxDepth; + private int numLeaves0; + private int numLeaves1; + private int numLeaves2; + private int numLeaves3; + private int numLeaves4; + private int numLeaves4p; + private int numBVH2; + + BuildStats() { + numNodes = numLeaves = 0; + sumObjects = 0; + minObjects = Integer.MAX_VALUE; + maxObjects = Integer.MIN_VALUE; + sumDepth = 0; + minDepth = Integer.MAX_VALUE; + maxDepth = Integer.MIN_VALUE; + numLeaves0 = 0; + numLeaves1 = 0; + numLeaves2 = 0; + numLeaves3 = 0; + numLeaves4 = 0; + numLeaves4p = 0; + numBVH2 = 0; + } + + void updateInner() { + numNodes++; + } + + void updateBVH2() { + numBVH2++; + } + + void updateLeaf(int depth, int n) { + numLeaves++; + minDepth = Math.min(depth, minDepth); + maxDepth = Math.max(depth, maxDepth); + sumDepth += depth; + minObjects = Math.min(n, minObjects); + maxObjects = Math.max(n, maxObjects); + sumObjects += n; + switch (n) { + case 0: + numLeaves0++; + break; + case 1: + numLeaves1++; + break; + case 2: + numLeaves2++; + break; + case 3: + numLeaves3++; + break; + case 4: + numLeaves4++; + break; + default: + numLeaves4p++; + break; + } + } + + void printStats() { + UI.printDetailed(Module.ACCEL, "Tree stats:"); + UI.printDetailed(Module.ACCEL, " * Nodes: %d", numNodes); + UI.printDetailed(Module.ACCEL, " * Leaves: %d", numLeaves); + UI.printDetailed(Module.ACCEL, " * Objects: min %d", minObjects); + UI.printDetailed(Module.ACCEL, " avg %.2f", (float) sumObjects / numLeaves); + UI.printDetailed(Module.ACCEL, " avg(n>0) %.2f", (float) sumObjects / (numLeaves - numLeaves0)); + UI.printDetailed(Module.ACCEL, " max %d", maxObjects); + UI.printDetailed(Module.ACCEL, " * Depth: min %d", minDepth); + UI.printDetailed(Module.ACCEL, " avg %.2f", (float) sumDepth / numLeaves); + UI.printDetailed(Module.ACCEL, " max %d", maxDepth); + UI.printDetailed(Module.ACCEL, " * Leaves w/: N=0 %3d%%", 100 * numLeaves0 / numLeaves); + UI.printDetailed(Module.ACCEL, " N=1 %3d%%", 100 * numLeaves1 / numLeaves); + UI.printDetailed(Module.ACCEL, " N=2 %3d%%", 100 * numLeaves2 / numLeaves); + UI.printDetailed(Module.ACCEL, " N=3 %3d%%", 100 * numLeaves3 / numLeaves); + UI.printDetailed(Module.ACCEL, " N=4 %3d%%", 100 * numLeaves4 / numLeaves); + UI.printDetailed(Module.ACCEL, " N>4 %3d%%", 100 * numLeaves4p / numLeaves); + UI.printDetailed(Module.ACCEL, " * BVH2 nodes: %d (%3d%%)", numBVH2, 100 * numBVH2 / (numNodes + numLeaves - 2 * numBVH2)); + } + } + + private void buildHierarchy(IntArray tempTree, int[] indices, BuildStats stats) { + // create space for the first node + tempTree.add(3 << 30); // dummy leaf + tempTree.add(0); + tempTree.add(0); + if (objects.length == 0) + return; + // seed bbox + float[] gridBox = { bounds.getMinimum().x, bounds.getMaximum().x, + bounds.getMinimum().y, bounds.getMaximum().y, + bounds.getMinimum().z, bounds.getMaximum().z }; + float[] nodeBox = { bounds.getMinimum().x, bounds.getMaximum().x, + bounds.getMinimum().y, bounds.getMaximum().y, + bounds.getMinimum().z, bounds.getMaximum().z }; + // seed subdivide function + subdivide(0, objects.length - 1, tempTree, indices, gridBox, nodeBox, 0, 1, stats); + } + + private void createNode(IntArray tempTree, int nodeIndex, int left, int right) { + // write leaf node + tempTree.set(nodeIndex + 0, (3 << 30) | left); + tempTree.set(nodeIndex + 1, right - left + 1); + } + + private void subdivide(int left, int right, IntArray tempTree, int[] indices, float[] gridBox, float[] nodeBox, int nodeIndex, int depth, BuildStats stats) { + if ((right - left + 1) <= maxPrims || depth >= 64) { + // write leaf node + stats.updateLeaf(depth, right - left + 1); + createNode(tempTree, nodeIndex, left, right); + return; + } + // calculate extents + int axis = -1, prevAxis, rightOrig; + float clipL = Float.NaN, clipR = Float.NaN, prevClip = Float.NaN; + float split = Float.NaN, prevSplit; + boolean wasLeft = true; + while (true) { + prevAxis = axis; + prevSplit = split; + // perform quick consistency checks + float d[] = { gridBox[1] - gridBox[0], gridBox[3] - gridBox[2], + gridBox[5] - gridBox[4] }; + if (d[0] < 0 || d[1] < 0 || d[2] < 0) + throw new IllegalStateException("negative node extents"); + for (int i = 0; i < 3; i++) { + if (nodeBox[2 * i + 1] < gridBox[2 * i] || nodeBox[2 * i] > gridBox[2 * i + 1]) { + UI.printError(Module.ACCEL, "Reached tree area in error - discarding node with: %d objects", right - left + 1); + throw new IllegalStateException("invalid node overlap"); + } + } + // find longest axis + if (d[0] > d[1] && d[0] > d[2]) + axis = 0; + else if (d[1] > d[2]) + axis = 1; + else + axis = 2; + split = 0.5f * (gridBox[2 * axis] + gridBox[2 * axis + 1]); + // partition L/R subsets + clipL = Float.NEGATIVE_INFINITY; + clipR = Float.POSITIVE_INFINITY; + rightOrig = right; // save this for later + float nodeL = Float.POSITIVE_INFINITY; + float nodeR = Float.NEGATIVE_INFINITY; + for (int i = left; i <= right;) { + int obj = indices[i]; + float minb = primitives.getPrimitiveBound(obj, 2 * axis + 0); + float maxb = primitives.getPrimitiveBound(obj, 2 * axis + 1); + float center = (minb + maxb) * 0.5f; + if (center <= split) { + // stay left + i++; + if (clipL < maxb) + clipL = maxb; + } else { + // move to the right most + int t = indices[i]; + indices[i] = indices[right]; + indices[right] = t; + right--; + if (clipR > minb) + clipR = minb; + } + if (nodeL > minb) + nodeL = minb; + if (nodeR < maxb) + nodeR = maxb; + } + // check for empty space + if (nodeL > nodeBox[2 * axis + 0] && nodeR < nodeBox[2 * axis + 1]) { + float nodeBoxW = nodeBox[2 * axis + 1] - nodeBox[2 * axis + 0]; + float nodeNewW = nodeR - nodeL; + // node box is too big compare to space occupied by primitives? + if (1.3f * nodeNewW < nodeBoxW) { + stats.updateBVH2(); + int nextIndex = tempTree.getSize(); + // allocate child + tempTree.add(0); + tempTree.add(0); + tempTree.add(0); + // write bvh2 clip node + stats.updateInner(); + tempTree.set(nodeIndex + 0, (axis << 30) | (1 << 29) | nextIndex); + tempTree.set(nodeIndex + 1, Float.floatToRawIntBits(nodeL)); + tempTree.set(nodeIndex + 2, Float.floatToRawIntBits(nodeR)); + // update nodebox and recurse + nodeBox[2 * axis + 0] = nodeL; + nodeBox[2 * axis + 1] = nodeR; + subdivide(left, rightOrig, tempTree, indices, gridBox, nodeBox, nextIndex, depth + 1, stats); + return; + } + } + // ensure we are making progress in the subdivision + if (right == rightOrig) { + // all left + if (clipL <= split) { + // keep looping on left half + gridBox[2 * axis + 1] = split; + prevClip = clipL; + wasLeft = true; + continue; + } + if (prevAxis == axis && prevSplit == split) { + // we are stuck here - create a leaf + stats.updateLeaf(depth, right - left + 1); + createNode(tempTree, nodeIndex, left, right); + return; + } + gridBox[2 * axis + 1] = split; + prevClip = Float.NaN; + } else if (left > right) { + // all right + right = rightOrig; + if (clipR >= split) { + // keep looping on right half + gridBox[2 * axis + 0] = split; + prevClip = clipR; + wasLeft = false; + continue; + } + if (prevAxis == axis && prevSplit == split) { + // we are stuck here - create a leaf + stats.updateLeaf(depth, right - left + 1); + createNode(tempTree, nodeIndex, left, right); + return; + } + gridBox[2 * axis + 0] = split; + prevClip = Float.NaN; + } else { + // we are actually splitting stuff + if (prevAxis != -1 && !Float.isNaN(prevClip)) { + // second time through - lets create the previous split + // since it produced empty space + int nextIndex = tempTree.getSize(); + // allocate child node + tempTree.add(0); + tempTree.add(0); + tempTree.add(0); + if (wasLeft) { + // create a node with a left child + // write leaf node + stats.updateInner(); + tempTree.set(nodeIndex + 0, (prevAxis << 30) | nextIndex); + tempTree.set(nodeIndex + 1, Float.floatToRawIntBits(prevClip)); + tempTree.set(nodeIndex + 2, Float.floatToRawIntBits(Float.POSITIVE_INFINITY)); + } else { + // create a node with a right child + // write leaf node + stats.updateInner(); + tempTree.set(nodeIndex + 0, (prevAxis << 30) | (nextIndex - 3)); + tempTree.set(nodeIndex + 1, Float.floatToRawIntBits(Float.NEGATIVE_INFINITY)); + tempTree.set(nodeIndex + 2, Float.floatToRawIntBits(prevClip)); + } + // count stats for the unused leaf + depth++; + stats.updateLeaf(depth, 0); + // now we keep going as we are, with a new nodeIndex: + nodeIndex = nextIndex; + } + break; + } + } + // compute index of child nodes + int nextIndex = tempTree.getSize(); + // allocate left node + int nl = right - left + 1; + int nr = rightOrig - (right + 1) + 1; + if (nl > 0) { + tempTree.add(0); + tempTree.add(0); + tempTree.add(0); + } else + nextIndex -= 3; + // allocate right node + if (nr > 0) { + tempTree.add(0); + tempTree.add(0); + tempTree.add(0); + } + // write leaf node + stats.updateInner(); + tempTree.set(nodeIndex + 0, (axis << 30) | nextIndex); + tempTree.set(nodeIndex + 1, Float.floatToRawIntBits(clipL)); + tempTree.set(nodeIndex + 2, Float.floatToRawIntBits(clipR)); + // prepare L/R child boxes + float[] gridBoxL = new float[6]; + float[] gridBoxR = new float[6]; + float[] nodeBoxL = new float[6]; + float[] nodeBoxR = new float[6]; + for (int i = 0; i < 6; i++) { + gridBoxL[i] = gridBoxR[i] = gridBox[i]; + nodeBoxL[i] = nodeBoxR[i] = nodeBox[i]; + } + gridBoxL[2 * axis + 1] = gridBoxR[2 * axis] = split; + nodeBoxL[2 * axis + 1] = clipL; + nodeBoxR[2 * axis + 0] = clipR; + // free memory + gridBox = nodeBox = null; + // recurse + if (nl > 0) + subdivide(left, right, tempTree, indices, gridBoxL, nodeBoxL, nextIndex, depth + 1, stats); + else + stats.updateLeaf(depth + 1, 0); + if (nr > 0) + subdivide(right + 1, rightOrig, tempTree, indices, gridBoxR, nodeBoxR, nextIndex + 3, depth + 1, stats); + else + stats.updateLeaf(depth + 1, 0); + } + + public void intersect(Ray r, IntersectionState state) { + float intervalMin = r.getMin(); + float intervalMax = r.getMax(); + float orgX = r.ox; + float dirX = r.dx, invDirX = 1 / dirX; + float t1, t2; + t1 = (bounds.getMinimum().x - orgX) * invDirX; + t2 = (bounds.getMaximum().x - orgX) * invDirX; + if (invDirX > 0) { + if (t1 > intervalMin) + intervalMin = t1; + if (t2 < intervalMax) + intervalMax = t2; + } else { + if (t2 > intervalMin) + intervalMin = t2; + if (t1 < intervalMax) + intervalMax = t1; + } + if (intervalMin > intervalMax) + return; + float orgY = r.oy; + float dirY = r.dy, invDirY = 1 / dirY; + t1 = (bounds.getMinimum().y - orgY) * invDirY; + t2 = (bounds.getMaximum().y - orgY) * invDirY; + if (invDirY > 0) { + if (t1 > intervalMin) + intervalMin = t1; + if (t2 < intervalMax) + intervalMax = t2; + } else { + if (t2 > intervalMin) + intervalMin = t2; + if (t1 < intervalMax) + intervalMax = t1; + } + if (intervalMin > intervalMax) + return; + float orgZ = r.oz; + float dirZ = r.dz, invDirZ = 1 / dirZ; + t1 = (bounds.getMinimum().z - orgZ) * invDirZ; + t2 = (bounds.getMaximum().z - orgZ) * invDirZ; + if (invDirZ > 0) { + if (t1 > intervalMin) + intervalMin = t1; + if (t2 < intervalMax) + intervalMax = t2; + } else { + if (t2 > intervalMin) + intervalMin = t2; + if (t1 < intervalMax) + intervalMax = t1; + } + if (intervalMin > intervalMax) + return; + + // compute custom offsets from direction sign bit + + int offsetXFront = Float.floatToRawIntBits(dirX) >>> 31; + int offsetYFront = Float.floatToRawIntBits(dirY) >>> 31; + int offsetZFront = Float.floatToRawIntBits(dirZ) >>> 31; + + int offsetXBack = offsetXFront ^ 1; + int offsetYBack = offsetYFront ^ 1; + int offsetZBack = offsetZFront ^ 1; + + int offsetXFront3 = offsetXFront * 3; + int offsetYFront3 = offsetYFront * 3; + int offsetZFront3 = offsetZFront * 3; + + int offsetXBack3 = offsetXBack * 3; + int offsetYBack3 = offsetYBack * 3; + int offsetZBack3 = offsetZBack * 3; + + // avoid always adding 1 during the inner loop + offsetXFront++; + offsetYFront++; + offsetZFront++; + offsetXBack++; + offsetYBack++; + offsetZBack++; + + IntersectionState.StackNode[] stack = state.getStack(); + int stackPos = 0; + int node = 0; + + while (true) { + pushloop: while (true) { + int tn = tree[node]; + int axis = tn & (7 << 29); + int offset = tn & ~(7 << 29); + switch (axis) { + case 0: { + // x axis + float tf = (Float.intBitsToFloat(tree[node + offsetXFront]) - orgX) * invDirX; + float tb = (Float.intBitsToFloat(tree[node + offsetXBack]) - orgX) * invDirX; + // ray passes between clip zones + if (tf < intervalMin && tb > intervalMax) + break pushloop; + int back = offset + offsetXBack3; + node = back; + // ray passes through far node only + if (tf < intervalMin) { + intervalMin = (tb >= intervalMin) ? tb : intervalMin; + continue; + } + node = offset + offsetXFront3; // front + // ray passes through near node only + if (tb > intervalMax) { + intervalMax = (tf <= intervalMax) ? tf : intervalMax; + continue; + } + // ray passes through both nodes + // push back node + stack[stackPos].node = back; + stack[stackPos].near = (tb >= intervalMin) ? tb : intervalMin; + stack[stackPos].far = intervalMax; + stackPos++; + // update ray interval for front node + intervalMax = (tf <= intervalMax) ? tf : intervalMax; + continue; + } + case 1 << 30: { + float tf = (Float.intBitsToFloat(tree[node + offsetYFront]) - orgY) * invDirY; + float tb = (Float.intBitsToFloat(tree[node + offsetYBack]) - orgY) * invDirY; + // ray passes between clip zones + if (tf < intervalMin && tb > intervalMax) + break pushloop; + int back = offset + offsetYBack3; + node = back; + // ray passes through far node only + if (tf < intervalMin) { + intervalMin = (tb >= intervalMin) ? tb : intervalMin; + continue; + } + node = offset + offsetYFront3; // front + // ray passes through near node only + if (tb > intervalMax) { + intervalMax = (tf <= intervalMax) ? tf : intervalMax; + continue; + } + // ray passes through both nodes + // push back node + stack[stackPos].node = back; + stack[stackPos].near = (tb >= intervalMin) ? tb : intervalMin; + stack[stackPos].far = intervalMax; + stackPos++; + // update ray interval for front node + intervalMax = (tf <= intervalMax) ? tf : intervalMax; + continue; + } + case 2 << 30: { + // z axis + float tf = (Float.intBitsToFloat(tree[node + offsetZFront]) - orgZ) * invDirZ; + float tb = (Float.intBitsToFloat(tree[node + offsetZBack]) - orgZ) * invDirZ; + // ray passes between clip zones + if (tf < intervalMin && tb > intervalMax) + break pushloop; + int back = offset + offsetZBack3; + node = back; + // ray passes through far node only + if (tf < intervalMin) { + intervalMin = (tb >= intervalMin) ? tb : intervalMin; + continue; + } + node = offset + offsetZFront3; // front + // ray passes through near node only + if (tb > intervalMax) { + intervalMax = (tf <= intervalMax) ? tf : intervalMax; + continue; + } + // ray passes through both nodes + // push back node + stack[stackPos].node = back; + stack[stackPos].near = (tb >= intervalMin) ? tb : intervalMin; + stack[stackPos].far = intervalMax; + stackPos++; + // update ray interval for front node + intervalMax = (tf <= intervalMax) ? tf : intervalMax; + continue; + } + case 3 << 30: { + // leaf - test some objects + int n = tree[node + 1]; + while (n > 0) { + primitives.intersectPrimitive(r, objects[offset], state); + n--; + offset++; + } + break pushloop; + } + case 1 << 29: { + float tf = (Float.intBitsToFloat(tree[node + offsetXFront]) - orgX) * invDirX; + float tb = (Float.intBitsToFloat(tree[node + offsetXBack]) - orgX) * invDirX; + node = offset; + intervalMin = (tf >= intervalMin) ? tf : intervalMin; + intervalMax = (tb <= intervalMax) ? tb : intervalMax; + if (intervalMin > intervalMax) + break pushloop; + continue; + } + case 3 << 29: { + float tf = (Float.intBitsToFloat(tree[node + offsetYFront]) - orgY) * invDirY; + float tb = (Float.intBitsToFloat(tree[node + offsetYBack]) - orgY) * invDirY; + node = offset; + intervalMin = (tf >= intervalMin) ? tf : intervalMin; + intervalMax = (tb <= intervalMax) ? tb : intervalMax; + if (intervalMin > intervalMax) + break pushloop; + continue; + } + case 5 << 29: { + float tf = (Float.intBitsToFloat(tree[node + offsetZFront]) - orgZ) * invDirZ; + float tb = (Float.intBitsToFloat(tree[node + offsetZBack]) - orgZ) * invDirZ; + node = offset; + intervalMin = (tf >= intervalMin) ? tf : intervalMin; + intervalMax = (tb <= intervalMax) ? tb : intervalMax; + if (intervalMin > intervalMax) + break pushloop; + continue; + } + default: + return; // should not happen + } // switch + } // traversal loop + do { + // stack is empty? + if (stackPos == 0) + return; + // move back up the stack + stackPos--; + intervalMin = stack[stackPos].near; + if (r.getMax() < intervalMin) + continue; + node = stack[stackPos].node; + intervalMax = stack[stackPos].far; + break; + } while (true); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/accel/KDTree.java b/src/main/java/org/sunflow/core/accel/KDTree.java new file mode 100644 index 0000000..9ba7be4 --- /dev/null +++ b/src/main/java/org/sunflow/core/accel/KDTree.java @@ -0,0 +1,800 @@ +package org.sunflow.core.accel; + +import java.io.FileWriter; +import java.io.IOException; + +import org.sunflow.core.AccelerationStructure; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.image.Color; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Point3; +import org.sunflow.system.Memory; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; +import org.sunflow.util.IntArray; + +public class KDTree implements AccelerationStructure { + private int[] tree; + private int[] primitives; + private PrimitiveList primitiveList; + private BoundingBox bounds; + + private int maxPrims; + + private static final float INTERSECT_COST = 0.5f; + private static final float TRAVERSAL_COST = 1; + private static final float EMPTY_BONUS = 0.2f; + private static final int MAX_DEPTH = 64; + + private static boolean dump = false; + private static String dumpPrefix = "kdtree"; + + public KDTree() { + this(0); + } + + public KDTree(int maxPrims) { + this.maxPrims = maxPrims; + } + + private static class BuildStats { + private int numNodes; + private int numLeaves; + private int sumObjects; + private int minObjects; + private int maxObjects; + private int sumDepth; + private int minDepth; + private int maxDepth; + private int numLeaves0; + private int numLeaves1; + private int numLeaves2; + private int numLeaves3; + private int numLeaves4; + private int numLeaves4p; + + BuildStats() { + numNodes = numLeaves = 0; + sumObjects = 0; + minObjects = Integer.MAX_VALUE; + maxObjects = Integer.MIN_VALUE; + sumDepth = 0; + minDepth = Integer.MAX_VALUE; + maxDepth = Integer.MIN_VALUE; + numLeaves0 = 0; + numLeaves1 = 0; + numLeaves2 = 0; + numLeaves3 = 0; + numLeaves4 = 0; + numLeaves4p = 0; + } + + void updateInner() { + numNodes++; + } + + void updateLeaf(int depth, int n) { + numLeaves++; + minDepth = Math.min(depth, minDepth); + maxDepth = Math.max(depth, maxDepth); + sumDepth += depth; + minObjects = Math.min(n, minObjects); + maxObjects = Math.max(n, maxObjects); + sumObjects += n; + switch (n) { + case 0: + numLeaves0++; + break; + case 1: + numLeaves1++; + break; + case 2: + numLeaves2++; + break; + case 3: + numLeaves3++; + break; + case 4: + numLeaves4++; + break; + default: + numLeaves4p++; + break; + } + } + + void printStats() { + UI.printDetailed(Module.ACCEL, "KDTree stats:"); + UI.printDetailed(Module.ACCEL, " * Nodes: %d", numNodes); + UI.printDetailed(Module.ACCEL, " * Leaves: %d", numLeaves); + UI.printDetailed(Module.ACCEL, " * Objects: min %d", minObjects); + UI.printDetailed(Module.ACCEL, " avg %.2f", (float) sumObjects / numLeaves); + UI.printDetailed(Module.ACCEL, " avg(n>0) %.2f", (float) sumObjects / (numLeaves - numLeaves0)); + UI.printDetailed(Module.ACCEL, " max %d", maxObjects); + UI.printDetailed(Module.ACCEL, " * Depth: min %d", minDepth); + UI.printDetailed(Module.ACCEL, " avg %.2f", (float) sumDepth / numLeaves); + UI.printDetailed(Module.ACCEL, " max %d", maxDepth); + UI.printDetailed(Module.ACCEL, " * Leaves w/: N=0 %3d%%", 100 * numLeaves0 / numLeaves); + UI.printDetailed(Module.ACCEL, " N=1 %3d%%", 100 * numLeaves1 / numLeaves); + UI.printDetailed(Module.ACCEL, " N=2 %3d%%", 100 * numLeaves2 / numLeaves); + UI.printDetailed(Module.ACCEL, " N=3 %3d%%", 100 * numLeaves3 / numLeaves); + UI.printDetailed(Module.ACCEL, " N=4 %3d%%", 100 * numLeaves4 / numLeaves); + UI.printDetailed(Module.ACCEL, " N>4 %3d%%", 100 * numLeaves4p / numLeaves); + } + } + + public static void setDumpMode(boolean dump, String prefix) { + KDTree.dump = dump; + KDTree.dumpPrefix = prefix; + } + + public void build(PrimitiveList primitives) { + UI.printDetailed(Module.ACCEL, "KDTree settings"); + UI.printDetailed(Module.ACCEL, " * Max Leaf Size: %d", maxPrims); + UI.printDetailed(Module.ACCEL, " * Max Depth: %d", MAX_DEPTH); + UI.printDetailed(Module.ACCEL, " * Traversal cost: %.2f", TRAVERSAL_COST); + UI.printDetailed(Module.ACCEL, " * Intersect cost: %.2f", INTERSECT_COST); + UI.printDetailed(Module.ACCEL, " * Empty bonus: %.2f", EMPTY_BONUS); + UI.printDetailed(Module.ACCEL, " * Dump leaves: %s", dump ? "enabled" : "disabled"); + Timer total = new Timer(); + total.start(); + primitiveList = primitives; + // get the object space bounds + bounds = primitives.getWorldBounds(null); + int nPrim = primitiveList.getNumPrimitives(), nSplits = 0; + BuildTask task = new BuildTask(nPrim); + Timer prepare = new Timer(); + prepare.start(); + for (int i = 0; i < nPrim; i++) { + for (int axis = 0; axis < 3; axis++) { + float ls = primitiveList.getPrimitiveBound(i, 2 * axis + 0); + float rs = primitiveList.getPrimitiveBound(i, 2 * axis + 1); + if (ls == rs) { + // flat in this dimension + task.splits[nSplits] = pack(ls, PLANAR, axis, i); + nSplits++; + } else { + task.splits[nSplits + 0] = pack(ls, OPENED, axis, i); + task.splits[nSplits + 1] = pack(rs, CLOSED, axis, i); + nSplits += 2; + } + } + } + task.n = nSplits; + prepare.end(); + Timer t = new Timer(); + IntArray tempTree = new IntArray(); + IntArray tempList = new IntArray(); + tempTree.add(0); + tempTree.add(1); + t.start(); + // sort it + Timer sorting = new Timer(); + sorting.start(); + radix12(task.splits, task.n); + sorting.end(); + // build the actual tree + BuildStats stats = new BuildStats(); + buildTree(bounds.getMinimum().x, bounds.getMaximum().x, bounds.getMinimum().y, bounds.getMaximum().y, bounds.getMinimum().z, bounds.getMaximum().z, task, 1, tempTree, 0, tempList, stats); + t.end(); + // write out final arrays + // free some memory + task = null; + tree = tempTree.trim(); + tempTree = null; + this.primitives = tempList.trim(); + tempList = null; + total.end(); + // display some extra info + stats.printStats(); + UI.printDetailed(Module.ACCEL, " * Node memory: %s", Memory.sizeof(tree)); + UI.printDetailed(Module.ACCEL, " * Object memory: %s", Memory.sizeof(this.primitives)); + UI.printDetailed(Module.ACCEL, " * Prepare time: %s", prepare); + UI.printDetailed(Module.ACCEL, " * Sorting time: %s", sorting); + UI.printDetailed(Module.ACCEL, " * Tree creation: %s", t); + UI.printDetailed(Module.ACCEL, " * Build time: %s", total); + if (dump) { + try { + UI.printInfo(Module.ACCEL, "Dumping mtls to %s.mtl ...", dumpPrefix); + FileWriter mtlFile = new FileWriter(dumpPrefix + ".mtl"); + int maxN = stats.maxObjects; + for (int n = 0; n <= maxN; n++) { + float blend = (float) n / (float) maxN; + Color nc; + if (blend < 0.25) + nc = Color.blend(Color.BLUE, Color.GREEN, blend / 0.25f); + else if (blend < 0.5) + nc = Color.blend(Color.GREEN, Color.YELLOW, (blend - 0.25f) / 0.25f); + else if (blend < 0.75) + nc = Color.blend(Color.YELLOW, Color.RED, (blend - 0.50f) / 0.25f); + else + nc = Color.MAGENTA; + mtlFile.write(String.format("newmtl mtl%d\n", n)); + float[] rgb = nc.getRGB(); + mtlFile.write("Ka 0.1 0.1 0.1\n"); + mtlFile.write(String.format("Kd %.12g %.12g %.12g\n", rgb[0], rgb[1], rgb[2])); + mtlFile.write("illum 1\n\n"); + } + FileWriter objFile = new FileWriter(dumpPrefix + ".obj"); + UI.printInfo(Module.ACCEL, "Dumping tree to %s.obj ...", dumpPrefix); + dumpObj(0, 0, maxN, new BoundingBox(bounds), objFile, mtlFile); + objFile.close(); + mtlFile.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private int dumpObj(int offset, int vertOffset, int maxN, BoundingBox bounds, FileWriter file, FileWriter mtlFile) throws IOException { + if (offset == 0) + file.write(String.format("mtllib %s.mtl\n", dumpPrefix)); + int nextOffset = tree[offset]; + if ((nextOffset & (3 << 30)) == (3 << 30)) { + // leaf + int n = tree[offset + 1]; + if (n > 0) { + // output the current voxel to the file + Point3 min = bounds.getMinimum(); + Point3 max = bounds.getMaximum(); + file.write(String.format("o node%d\n", offset)); + file.write(String.format("v %g %g %g\n", max.x, max.y, min.z)); + file.write(String.format("v %g %g %g\n", max.x, min.y, min.z)); + file.write(String.format("v %g %g %g\n", min.x, min.y, min.z)); + file.write(String.format("v %g %g %g\n", min.x, max.y, min.z)); + file.write(String.format("v %g %g %g\n", max.x, max.y, max.z)); + file.write(String.format("v %g %g %g\n", max.x, min.y, max.z)); + file.write(String.format("v %g %g %g\n", min.x, min.y, max.z)); + file.write(String.format("v %g %g %g\n", min.x, max.y, max.z)); + int v0 = vertOffset; + file.write(String.format("usemtl mtl%d\n", n)); + file.write("s off\n"); + file.write(String.format("f %d %d %d %d\n", v0 + 1, v0 + 2, v0 + 3, v0 + 4)); + file.write(String.format("f %d %d %d %d\n", v0 + 5, v0 + 8, v0 + 7, v0 + 6)); + file.write(String.format("f %d %d %d %d\n", v0 + 1, v0 + 5, v0 + 6, v0 + 2)); + file.write(String.format("f %d %d %d %d\n", v0 + 2, v0 + 6, v0 + 7, v0 + 3)); + file.write(String.format("f %d %d %d %d\n", v0 + 3, v0 + 7, v0 + 8, v0 + 4)); + file.write(String.format("f %d %d %d %d\n", v0 + 5, v0 + 1, v0 + 4, v0 + 8)); + vertOffset += 8; + } + return vertOffset; + } else { + // node, recurse + int axis = nextOffset & (3 << 30), v0; + float split = Float.intBitsToFloat(tree[offset + 1]), min, max; + nextOffset &= ~(3 << 30); + switch (axis) { + case 0: + max = bounds.getMaximum().x; + bounds.getMaximum().x = split; + v0 = dumpObj(nextOffset, vertOffset, maxN, bounds, file, mtlFile); + // restore and go to other side + bounds.getMaximum().x = max; + min = bounds.getMinimum().x; + bounds.getMinimum().x = split; + v0 = dumpObj(nextOffset + 2, v0, maxN, bounds, file, mtlFile); + bounds.getMinimum().x = min; + break; + case 1 << 30: + max = bounds.getMaximum().y; + bounds.getMaximum().y = split; + v0 = dumpObj(nextOffset, vertOffset, maxN, bounds, file, mtlFile); + // restore and go to other side + bounds.getMaximum().y = max; + min = bounds.getMinimum().y; + bounds.getMinimum().y = split; + v0 = dumpObj(nextOffset + 2, v0, maxN, bounds, file, mtlFile); + bounds.getMinimum().y = min; + break; + case 2 << 30: + max = bounds.getMaximum().z; + bounds.getMaximum().z = split; + v0 = dumpObj(nextOffset, vertOffset, maxN, bounds, file, mtlFile); + // restore and go to other side + bounds.getMaximum().z = max; + min = bounds.getMinimum().z; + bounds.getMinimum().z = split; + v0 = dumpObj(nextOffset + 2, v0, maxN, bounds, file, mtlFile); + // restore and go to other side + bounds.getMinimum().z = min; + break; + default: + v0 = vertOffset; + break; + } + return v0; + } + } + + // type is encoded as 2 shifted bits + private static final long CLOSED = 0L << 30; + private static final long PLANAR = 1L << 30; + private static final long OPENED = 2L << 30; + private static final long TYPE_MASK = 3L << 30; + + // pack split values into a 64bit integer + private static long pack(float split, long type, int axis, int object) { + // pack float in sortable form + int f = Float.floatToRawIntBits(split); + int top = f ^ ((f >> 31) | 0x80000000); + long p = (top & 0xFFFFFFFFL) << 32; + p |= type; // encode type as 2 bits + p |= ((long) axis) << 28; // encode axis as 2 bits + p |= (object & 0xFFFFFFFL); // pack object number + return p; + } + + private static int unpackObject(long p) { + return (int) (p & 0xFFFFFFFL); + } + + private static int unpackAxis(long p) { + return (int) (p >>> 28) & 3; + } + + private static long unpackSplitType(long p) { + return p & TYPE_MASK; + } + + private static float unpackSplit(long p) { + int f = (int) ((p >>> 32) & 0xFFFFFFFFL); + int m = ((f >>> 31) - 1) | 0x80000000; + return Float.intBitsToFloat(f ^ m); + } + + // radix sort on top 36 bits - returns sorted result + private static void radix12(long[] splits, int n) { + // allocate working memory + final int[] hist = new int[2048]; + final long[] sorted = new long[n]; + // parallel histogramming pass + for (int i = 0; i < n; i++) { + long pi = splits[i]; + hist[0x000 + ((int) (pi >>> 28) & 0x1FF)]++; + hist[0x200 + ((int) (pi >>> 37) & 0x1FF)]++; + hist[0x400 + ((int) (pi >>> 46) & 0x1FF)]++; + hist[0x600 + ((int) (pi >>> 55))]++; + } + + // sum the histograms - each histogram entry records the number of + // values preceding itself. + { + int sum0 = 0, sum1 = 0, sum2 = 0, sum3 = 0; + int tsum; + for (int i = 0; i < 512; i++) { + tsum = hist[0x000 + i] + sum0; + hist[0x000 + i] = sum0 - 1; + sum0 = tsum; + tsum = hist[0x200 + i] + sum1; + hist[0x200 + i] = sum1 - 1; + sum1 = tsum; + tsum = hist[0x400 + i] + sum2; + hist[0x400 + i] = sum2 - 1; + sum2 = tsum; + tsum = hist[0x600 + i] + sum3; + hist[0x600 + i] = sum3 - 1; + sum3 = tsum; + } + } + + // read/write histogram passes + for (int i = 0; i < n; i++) { + long pi = splits[i]; + int pos = (int) (pi >>> 28) & 0x1FF; + sorted[++hist[0x000 + pos]] = pi; + } + for (int i = 0; i < n; i++) { + long pi = sorted[i]; + int pos = (int) (pi >>> 37) & 0x1FF; + splits[++hist[0x200 + pos]] = pi; + } + for (int i = 0; i < n; i++) { + long pi = splits[i]; + int pos = (int) (pi >>> 46) & 0x1FF; + sorted[++hist[0x400 + pos]] = pi; + } + for (int i = 0; i < n; i++) { + long pi = sorted[i]; + int pos = (int) (pi >>> 55); + splits[++hist[0x600 + pos]] = pi; + } + } + + private static class BuildTask { + long[] splits; + int numObjects; + int n; + byte[] leftRightTable; + + BuildTask(int numObjects) { + splits = new long[6 * numObjects]; + this.numObjects = numObjects; + n = 0; + // 2 bits per object + leftRightTable = new byte[(numObjects + 3) / 4]; + } + + BuildTask(int numObjects, BuildTask parent) { + splits = new long[6 * numObjects]; + this.numObjects = numObjects; + n = 0; + leftRightTable = parent.leftRightTable; + } + } + + private void buildTree(float minx, float maxx, float miny, float maxy, float minz, float maxz, BuildTask task, int depth, IntArray tempTree, int offset, IntArray tempList, BuildStats stats) { + // get node bounding box extents + if (task.numObjects > maxPrims && depth < MAX_DEPTH) { + float dx = maxx - minx; + float dy = maxy - miny; + float dz = maxz - minz; + // search for best possible split + float bestCost = INTERSECT_COST * task.numObjects; + int bestAxis = -1; + int bestOffsetStart = -1; + int bestOffsetEnd = -1; + float bestSplit = 0; + boolean bestPlanarLeft = false; + int bnl = 0, bnr = 0; + // inverse area of the bounding box (factor of 2 ommitted) + float area = (dx * dy + dy * dz + dz * dx); + float ISECT_COST = INTERSECT_COST / area; + // setup counts for each axis + int[] nl = { 0, 0, 0 }; + int[] nr = { task.numObjects, task.numObjects, task.numObjects }; + // setup bounds for each axis + float[] dp = { dy * dz, dz * dx, dx * dy }; + float[] ds = { dy + dz, dz + dx, dx + dy }; + float[] nodeMin = { minx, miny, minz }; + float[] nodeMax = { maxx, maxy, maxz }; + // search for best cost + int nSplits = task.n; + long[] splits = task.splits; + byte[] lrtable = task.leftRightTable; + for (int i = 0; i < nSplits;) { + // extract current split + long ptr = splits[i]; + float split = unpackSplit(ptr); + int axis = unpackAxis(ptr); + // mark current position + int currentOffset = i; + // count number of primitives start/stopping/lying on the + // current plane + int pClosed = 0, pPlanar = 0, pOpened = 0; + long ptrMasked = ptr & (~TYPE_MASK & 0xFFFFFFFFF0000000L); + long ptrClosed = ptrMasked | CLOSED; + long ptrPlanar = ptrMasked | PLANAR; + long ptrOpened = ptrMasked | OPENED; + while (i < nSplits && (splits[i] & 0xFFFFFFFFF0000000L) == ptrClosed) { + int obj = unpackObject(splits[i]); + lrtable[obj >>> 2] = 0; + pClosed++; + i++; + } + while (i < nSplits && (splits[i] & 0xFFFFFFFFF0000000L) == ptrPlanar) { + int obj = unpackObject(splits[i]); + lrtable[obj >>> 2] = 0; + pPlanar++; + i++; + } + while (i < nSplits && (splits[i] & 0xFFFFFFFFF0000000L) == ptrOpened) { + int obj = unpackObject(splits[i]); + lrtable[obj >>> 2] = 0; + pOpened++; + i++; + } + // now we have summed all contributions from this plane + nr[axis] -= pPlanar + pClosed; + // compute cost + if (split >= nodeMin[axis] && split <= nodeMax[axis]) { + // left and right surface area (factor of 2 ommitted) + float dl = split - nodeMin[axis]; + float dr = nodeMax[axis] - split; + float lp = dp[axis] + dl * ds[axis]; + float rp = dp[axis] + dr * ds[axis]; + // planar prims go to smallest cell always + boolean planarLeft = dl < dr; + int numLeft = nl[axis] + (planarLeft ? pPlanar : 0); + int numRight = nr[axis] + (planarLeft ? 0 : pPlanar); + float eb = ((numLeft == 0 && dl > 0) || (numRight == 0 && dr > 0)) ? EMPTY_BONUS : 0; + float cost = TRAVERSAL_COST + ISECT_COST * (1 - eb) * (lp * numLeft + rp * numRight); + if (cost < bestCost) { + bestCost = cost; + bestAxis = axis; + bestSplit = split; + bestOffsetStart = currentOffset; + bestOffsetEnd = i; + bnl = numLeft; + bnr = numRight; + bestPlanarLeft = planarLeft; + } + } + // move objects left + nl[axis] += pOpened + pPlanar; + } + // debug check for correctness of the scan + for (int axis = 0; axis < 3; axis++) { + int numLeft = nl[axis]; + int numRight = nr[axis]; + if (numLeft != task.numObjects || numRight != 0) + UI.printError(Module.ACCEL, "Didn't scan full range of objects @depth=%d. Left overs for axis %d: [L: %d] [R: %d]", depth, axis, numLeft, numRight); + } + // found best split? + if (bestAxis != -1) { + // allocate space for child nodes + BuildTask taskL = new BuildTask(bnl, task); + BuildTask taskR = new BuildTask(bnr, task); + int lk = 0, rk = 0; + for (int i = 0; i < bestOffsetStart; i++) { + long ptr = splits[i]; + if (unpackAxis(ptr) == bestAxis) { + if (unpackSplitType(ptr) != CLOSED) { + int obj = unpackObject(ptr); + lrtable[obj >>> 2] |= 1 << ((obj & 3) << 1); + lk++; + } + } + } + for (int i = bestOffsetStart; i < bestOffsetEnd; i++) { + long ptr = splits[i]; + assert unpackAxis(ptr) == bestAxis; + if (unpackSplitType(ptr) == PLANAR) { + if (bestPlanarLeft) { + int obj = unpackObject(ptr); + lrtable[obj >>> 2] |= 1 << ((obj & 3) << 1); + lk++; + } else { + int obj = unpackObject(ptr); + lrtable[obj >>> 2] |= 2 << ((obj & 3) << 1); + rk++; + } + } + } + for (int i = bestOffsetEnd; i < nSplits; i++) { + long ptr = splits[i]; + if (unpackAxis(ptr) == bestAxis) { + if (unpackSplitType(ptr) != OPENED) { + int obj = unpackObject(ptr); + lrtable[obj >>> 2] |= 2 << ((obj & 3) << 1); + rk++; + } + } + } + // output new splits while maintaining order + long[] splitsL = taskL.splits; + long[] splitsR = taskR.splits; + int nsl = 0, nsr = 0; + for (int i = 0; i < nSplits; i++) { + long ptr = splits[i]; + int obj = unpackObject(ptr); + int idx = obj >>> 2; + int mask = 1 << ((obj & 3) << 1); + if ((lrtable[idx] & mask) != 0) { + splitsL[nsl] = ptr; + nsl++; + } + if ((lrtable[idx] & (mask << 1)) != 0) { + splitsR[nsr] = ptr; + nsr++; + } + } + taskL.n = nsl; + taskR.n = nsr; + // free more memory + task.splits = splits = splitsL = splitsR = null; + task = null; + // allocate child nodes + int nextOffset = tempTree.getSize(); + tempTree.add(0); + tempTree.add(0); + tempTree.add(0); + tempTree.add(0); + // create current node + tempTree.set(offset + 0, (bestAxis << 30) | nextOffset); + tempTree.set(offset + 1, Float.floatToRawIntBits(bestSplit)); + // recurse for child nodes - free object arrays after each step + stats.updateInner(); + switch (bestAxis) { + case 0: + buildTree(minx, bestSplit, miny, maxy, minz, maxz, taskL, depth + 1, tempTree, nextOffset, tempList, stats); + taskL = null; + buildTree(bestSplit, maxx, miny, maxy, minz, maxz, taskR, depth + 1, tempTree, nextOffset + 2, tempList, stats); + taskR = null; + return; + case 1: + buildTree(minx, maxx, miny, bestSplit, minz, maxz, taskL, depth + 1, tempTree, nextOffset, tempList, stats); + taskL = null; + buildTree(minx, maxx, bestSplit, maxy, minz, maxz, taskR, depth + 1, tempTree, nextOffset + 2, tempList, stats); + taskR = null; + return; + case 2: + buildTree(minx, maxx, miny, maxy, minz, bestSplit, taskL, depth + 1, tempTree, nextOffset, tempList, stats); + taskL = null; + buildTree(minx, maxx, miny, maxy, bestSplit, maxz, taskR, depth + 1, tempTree, nextOffset + 2, tempList, stats); + taskR = null; + return; + default: + assert false; + } + } + } + // create leaf node + int listOffset = tempList.getSize(); + int n = 0; + for (int i = 0; i < task.n; i++) { + long ptr = task.splits[i]; + if (unpackAxis(ptr) == 0 && unpackSplitType(ptr) != CLOSED) { + tempList.add(unpackObject(ptr)); + n++; + } + } + stats.updateLeaf(depth, n); + if (n != task.numObjects) + UI.printError(Module.ACCEL, "Error creating leaf node - expecting %d found %d", task.numObjects, n); + tempTree.set(offset + 0, (3 << 30) | listOffset); + tempTree.set(offset + 1, task.numObjects); + // free some memory + task.splits = null; + } + + public void intersect(Ray r, IntersectionState state) { + float intervalMin = r.getMin(); + float intervalMax = r.getMax(); + float orgX = r.ox; + float dirX = r.dx, invDirX = 1 / dirX; + float t1, t2; + t1 = (bounds.getMinimum().x - orgX) * invDirX; + t2 = (bounds.getMaximum().x - orgX) * invDirX; + if (invDirX > 0) { + if (t1 > intervalMin) + intervalMin = t1; + if (t2 < intervalMax) + intervalMax = t2; + } else { + if (t2 > intervalMin) + intervalMin = t2; + if (t1 < intervalMax) + intervalMax = t1; + } + if (intervalMin > intervalMax) + return; + float orgY = r.oy; + float dirY = r.dy, invDirY = 1 / dirY; + t1 = (bounds.getMinimum().y - orgY) * invDirY; + t2 = (bounds.getMaximum().y - orgY) * invDirY; + if (invDirY > 0) { + if (t1 > intervalMin) + intervalMin = t1; + if (t2 < intervalMax) + intervalMax = t2; + } else { + if (t2 > intervalMin) + intervalMin = t2; + if (t1 < intervalMax) + intervalMax = t1; + } + if (intervalMin > intervalMax) + return; + float orgZ = r.oz; + float dirZ = r.dz, invDirZ = 1 / dirZ; + t1 = (bounds.getMinimum().z - orgZ) * invDirZ; + t2 = (bounds.getMaximum().z - orgZ) * invDirZ; + if (invDirZ > 0) { + if (t1 > intervalMin) + intervalMin = t1; + if (t2 < intervalMax) + intervalMax = t2; + } else { + if (t2 > intervalMin) + intervalMin = t2; + if (t1 < intervalMax) + intervalMax = t1; + } + if (intervalMin > intervalMax) + return; + + // compute custom offsets from direction sign bit + int offsetXFront = (Float.floatToRawIntBits(dirX) & (1 << 31)) >>> 30; + int offsetYFront = (Float.floatToRawIntBits(dirY) & (1 << 31)) >>> 30; + int offsetZFront = (Float.floatToRawIntBits(dirZ) & (1 << 31)) >>> 30; + + int offsetXBack = offsetXFront ^ 2; + int offsetYBack = offsetYFront ^ 2; + int offsetZBack = offsetZFront ^ 2; + + IntersectionState.StackNode[] stack = state.getStack(); + int stackPos = 0; + int node = 0; + + while (true) { + int tn = tree[node]; + int axis = tn & (3 << 30); + int offset = tn & ~(3 << 30); + switch (axis) { + case 0: { + float d = (Float.intBitsToFloat(tree[node + 1]) - orgX) * invDirX; + int back = offset + offsetXBack; + node = back; + if (d < intervalMin) + continue; + node = offset + offsetXFront; // front + if (d > intervalMax) + continue; + // push back node + stack[stackPos].node = back; + stack[stackPos].near = (d >= intervalMin) ? d : intervalMin; + stack[stackPos].far = intervalMax; + stackPos++; + // update ray interval for front node + intervalMax = (d <= intervalMax) ? d : intervalMax; + continue; + } + case 1 << 30: { + // y axis + float d = (Float.intBitsToFloat(tree[node + 1]) - orgY) * invDirY; + int back = offset + offsetYBack; + node = back; + if (d < intervalMin) + continue; + node = offset + offsetYFront; // front + if (d > intervalMax) + continue; + // push back node + stack[stackPos].node = back; + stack[stackPos].near = (d >= intervalMin) ? d : intervalMin; + stack[stackPos].far = intervalMax; + stackPos++; + // update ray interval for front node + intervalMax = (d <= intervalMax) ? d : intervalMax; + continue; + } + case 2 << 30: { + // z axis + float d = (Float.intBitsToFloat(tree[node + 1]) - orgZ) * invDirZ; + int back = offset + offsetZBack; + node = back; + if (d < intervalMin) + continue; + node = offset + offsetZFront; // front + if (d > intervalMax) + continue; + // push back node + stack[stackPos].node = back; + stack[stackPos].near = (d >= intervalMin) ? d : intervalMin; + stack[stackPos].far = intervalMax; + stackPos++; + // update ray interval for front node + intervalMax = (d <= intervalMax) ? d : intervalMax; + continue; + } + default: { + // leaf - test some objects + int n = tree[node + 1]; + while (n > 0) { + primitiveList.intersectPrimitive(r, primitives[offset], state); + n--; + offset++; + } + if (r.getMax() < intervalMax) + return; + do { + // stack is empty? + if (stackPos == 0) + return; + // move back up the stack + stackPos--; + intervalMin = stack[stackPos].near; + if (r.getMax() < intervalMin) + continue; + node = stack[stackPos].node; + intervalMax = stack[stackPos].far; + break; + } while (true); + } + } // switch + } // traversal loop + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/accel/NullAccelerator.java b/src/main/java/org/sunflow/core/accel/NullAccelerator.java new file mode 100644 index 0000000..2bb37ba --- /dev/null +++ b/src/main/java/org/sunflow/core/accel/NullAccelerator.java @@ -0,0 +1,26 @@ +package org.sunflow.core.accel; + +import org.sunflow.core.AccelerationStructure; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; + +public class NullAccelerator implements AccelerationStructure { + private PrimitiveList primitives; + private int n; + + public NullAccelerator() { + primitives = null; + n = 0; + } + + public void build(PrimitiveList primitives) { + this.primitives = primitives; + n = primitives.getNumPrimitives(); + } + + public void intersect(Ray r, IntersectionState state) { + for (int i = 0; i < n; i++) + primitives.intersectPrimitive(r, i, state); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/accel/UniformGrid.java b/src/main/java/org/sunflow/core/accel/UniformGrid.java new file mode 100644 index 0000000..65eae26 --- /dev/null +++ b/src/main/java/org/sunflow/core/accel/UniformGrid.java @@ -0,0 +1,294 @@ +package org.sunflow.core.accel; + +import org.sunflow.core.AccelerationStructure; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.MathUtils; +import org.sunflow.math.Vector3; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; +import org.sunflow.util.IntArray; + +public final class UniformGrid implements AccelerationStructure { + private int nx, ny, nz; + private PrimitiveList primitives; + private BoundingBox bounds; + private int[][] cells; + private float voxelwx, voxelwy, voxelwz; + private float invVoxelwx, invVoxelwy, invVoxelwz; + + public UniformGrid() { + nx = ny = nz = 0; + bounds = null; + cells = null; + voxelwx = voxelwy = voxelwz = 0; + invVoxelwx = invVoxelwy = invVoxelwz = 0; + } + + public void build(PrimitiveList primitives) { + Timer t = new Timer(); + t.start(); + this.primitives = primitives; + int n = primitives.getNumPrimitives(); + // compute bounds + bounds = primitives.getWorldBounds(null); + // create grid from number of objects + bounds.enlargeUlps(); + Vector3 w = bounds.getExtents(); + double s = Math.pow((w.x * w.y * w.z) / n, 1 / 3.0); + nx = MathUtils.clamp((int) ((w.x / s) + 0.5), 1, 128); + ny = MathUtils.clamp((int) ((w.y / s) + 0.5), 1, 128); + nz = MathUtils.clamp((int) ((w.z / s) + 0.5), 1, 128); + voxelwx = w.x / nx; + voxelwy = w.y / ny; + voxelwz = w.z / nz; + invVoxelwx = 1 / voxelwx; + invVoxelwy = 1 / voxelwy; + invVoxelwz = 1 / voxelwz; + UI.printDetailed(Module.ACCEL, "Creating grid: %dx%dx%d ...", nx, ny, nz); + IntArray[] buildCells = new IntArray[nx * ny * nz]; + // add all objects into the grid cells they overlap + int[] imin = new int[3]; + int[] imax = new int[3]; + int numCellsPerObject = 0; + for (int i = 0; i < n; i++) { + getGridIndex(primitives.getPrimitiveBound(i, 0), primitives.getPrimitiveBound(i, 2), primitives.getPrimitiveBound(i, 4), imin); + getGridIndex(primitives.getPrimitiveBound(i, 1), primitives.getPrimitiveBound(i, 3), primitives.getPrimitiveBound(i, 5), imax); + for (int ix = imin[0]; ix <= imax[0]; ix++) { + for (int iy = imin[1]; iy <= imax[1]; iy++) { + for (int iz = imin[2]; iz <= imax[2]; iz++) { + int idx = ix + (nx * iy) + (nx * ny * iz); + if (buildCells[idx] == null) + buildCells[idx] = new IntArray(); + buildCells[idx].add(i); + numCellsPerObject++; + } + } + } + } + UI.printDetailed(Module.ACCEL, "Building cells ..."); + int numEmpty = 0; + int numInFull = 0; + cells = new int[nx * ny * nz][]; + int i = 0; + for (IntArray cell : buildCells) { + if (cell != null) { + if (cell.getSize() == 0) { + numEmpty++; + cell = null; + } else { + cells[i] = cell.trim(); + numInFull += cell.getSize(); + } + } else + numEmpty++; + i++; + } + t.end(); + UI.printDetailed(Module.ACCEL, "Uniform grid statistics:"); + UI.printDetailed(Module.ACCEL, " * Grid cells: %d", cells.length); + UI.printDetailed(Module.ACCEL, " * Used cells: %d", cells.length - numEmpty); + UI.printDetailed(Module.ACCEL, " * Empty cells: %d", numEmpty); + UI.printDetailed(Module.ACCEL, " * Occupancy: %.2f%%", 100.0 * (cells.length - numEmpty) / cells.length); + UI.printDetailed(Module.ACCEL, " * Objects/Cell: %.2f", (double) numInFull / (double) cells.length); + UI.printDetailed(Module.ACCEL, " * Objects/Used Cell: %.2f", (double) numInFull / (double) (cells.length - numEmpty)); + UI.printDetailed(Module.ACCEL, " * Cells/Object: %.2f", (double) numCellsPerObject / (double) n); + UI.printDetailed(Module.ACCEL, " * Build time: %s", t.toString()); + } + + public void intersect(Ray r, IntersectionState state) { + float intervalMin = r.getMin(); + float intervalMax = r.getMax(); + float orgX = r.ox; + float dirX = r.dx, invDirX = 1 / dirX; + float t1, t2; + t1 = (bounds.getMinimum().x - orgX) * invDirX; + t2 = (bounds.getMaximum().x - orgX) * invDirX; + if (invDirX > 0) { + if (t1 > intervalMin) + intervalMin = t1; + if (t2 < intervalMax) + intervalMax = t2; + } else { + if (t2 > intervalMin) + intervalMin = t2; + if (t1 < intervalMax) + intervalMax = t1; + } + if (intervalMin > intervalMax) + return; + float orgY = r.oy; + float dirY = r.dy, invDirY = 1 / dirY; + t1 = (bounds.getMinimum().y - orgY) * invDirY; + t2 = (bounds.getMaximum().y - orgY) * invDirY; + if (invDirY > 0) { + if (t1 > intervalMin) + intervalMin = t1; + if (t2 < intervalMax) + intervalMax = t2; + } else { + if (t2 > intervalMin) + intervalMin = t2; + if (t1 < intervalMax) + intervalMax = t1; + } + if (intervalMin > intervalMax) + return; + float orgZ = r.oz; + float dirZ = r.dz, invDirZ = 1 / dirZ; + t1 = (bounds.getMinimum().z - orgZ) * invDirZ; + t2 = (bounds.getMaximum().z - orgZ) * invDirZ; + if (invDirZ > 0) { + if (t1 > intervalMin) + intervalMin = t1; + if (t2 < intervalMax) + intervalMax = t2; + } else { + if (t2 > intervalMin) + intervalMin = t2; + if (t1 < intervalMax) + intervalMax = t1; + } + if (intervalMin > intervalMax) + return; + // box is hit at [intervalMin, intervalMax] + orgX += intervalMin * dirX; + orgY += intervalMin * dirY; + orgZ += intervalMin * dirZ; + // locate starting point inside the grid + // and set up 3D-DDA vars + int indxX, indxY, indxZ; + int stepX, stepY, stepZ; + int stopX, stopY, stopZ; + float deltaX, deltaY, deltaZ; + float tnextX, tnextY, tnextZ; + // stepping factors along X + indxX = (int) ((orgX - bounds.getMinimum().x) * invVoxelwx); + if (indxX < 0) + indxX = 0; + else if (indxX >= nx) + indxX = nx - 1; + if (Math.abs(dirX) < 1e-6f) { + stepX = 0; + stopX = indxX; + deltaX = 0; + tnextX = Float.POSITIVE_INFINITY; + } else if (dirX > 0) { + stepX = 1; + stopX = nx; + deltaX = voxelwx * invDirX; + tnextX = intervalMin + ((indxX + 1) * voxelwx + bounds.getMinimum().x - orgX) * invDirX; + } else { + stepX = -1; + stopX = -1; + deltaX = -voxelwx * invDirX; + tnextX = intervalMin + (indxX * voxelwx + bounds.getMinimum().x - orgX) * invDirX; + } + // stepping factors along Y + indxY = (int) ((orgY - bounds.getMinimum().y) * invVoxelwy); + if (indxY < 0) + indxY = 0; + else if (indxY >= ny) + indxY = ny - 1; + if (Math.abs(dirY) < 1e-6f) { + stepY = 0; + stopY = indxY; + deltaY = 0; + tnextY = Float.POSITIVE_INFINITY; + } else if (dirY > 0) { + stepY = 1; + stopY = ny; + deltaY = voxelwy * invDirY; + tnextY = intervalMin + ((indxY + 1) * voxelwy + bounds.getMinimum().y - orgY) * invDirY; + } else { + stepY = -1; + stopY = -1; + deltaY = -voxelwy * invDirY; + tnextY = intervalMin + (indxY * voxelwy + bounds.getMinimum().y - orgY) * invDirY; + } + // stepping factors along Z + indxZ = (int) ((orgZ - bounds.getMinimum().z) * invVoxelwz); + if (indxZ < 0) + indxZ = 0; + else if (indxZ >= nz) + indxZ = nz - 1; + if (Math.abs(dirZ) < 1e-6f) { + stepZ = 0; + stopZ = indxZ; + deltaZ = 0; + tnextZ = Float.POSITIVE_INFINITY; + } else if (dirZ > 0) { + stepZ = 1; + stopZ = nz; + deltaZ = voxelwz * invDirZ; + tnextZ = intervalMin + ((indxZ + 1) * voxelwz + bounds.getMinimum().z - orgZ) * invDirZ; + } else { + stepZ = -1; + stopZ = -1; + deltaZ = -voxelwz * invDirZ; + tnextZ = intervalMin + (indxZ * voxelwz + bounds.getMinimum().z - orgZ) * invDirZ; + } + int cellstepX = stepX; + int cellstepY = stepY * nx; + int cellstepZ = stepZ * ny * nx; + int cell = indxX + indxY * nx + indxZ * ny * nx; + // trace through the grid + for (;;) { + if (tnextX < tnextY && tnextX < tnextZ) { + if (cells[cell] != null) { + for (int i : cells[cell]) + primitives.intersectPrimitive(r, i, state); + if (state.hit() && (r.getMax() < tnextX && r.getMax() < intervalMax)) + return; + } + intervalMin = tnextX; + if (intervalMin > intervalMax) + return; + indxX += stepX; + if (indxX == stopX) + return; + tnextX += deltaX; + cell += cellstepX; + } else if (tnextY < tnextZ) { + if (cells[cell] != null) { + for (int i : cells[cell]) + primitives.intersectPrimitive(r, i, state); + if (state.hit() && (r.getMax() < tnextY && r.getMax() < intervalMax)) + return; + } + intervalMin = tnextY; + if (intervalMin > intervalMax) + return; + indxY += stepY; + if (indxY == stopY) + return; + tnextY += deltaY; + cell += cellstepY; + } else { + if (cells[cell] != null) { + for (int i : cells[cell]) + primitives.intersectPrimitive(r, i, state); + if (state.hit() && (r.getMax() < tnextZ && r.getMax() < intervalMax)) + return; + } + intervalMin = tnextZ; + if (intervalMin > intervalMax) + return; + indxZ += stepZ; + if (indxZ == stopZ) + return; + tnextZ += deltaZ; + cell += cellstepZ; + } + } + } + + private void getGridIndex(float x, float y, float z, int[] i) { + i[0] = MathUtils.clamp((int) ((x - bounds.getMinimum().x) * invVoxelwx), 0, nx - 1); + i[1] = MathUtils.clamp((int) ((y - bounds.getMinimum().y) * invVoxelwy), 0, ny - 1); + i[2] = MathUtils.clamp((int) ((z - bounds.getMinimum().z) * invVoxelwz), 0, nz - 1); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/bucket/BucketOrderFactory.java b/src/main/java/org/sunflow/core/bucket/BucketOrderFactory.java new file mode 100644 index 0000000..9027054 --- /dev/null +++ b/src/main/java/org/sunflow/core/bucket/BucketOrderFactory.java @@ -0,0 +1,25 @@ +package org.sunflow.core.bucket; + +import org.sunflow.PluginRegistry; +import org.sunflow.core.BucketOrder; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class BucketOrderFactory { + public static BucketOrder create(String order) { + boolean flip = false; + if (order.startsWith("inverse") || order.startsWith("invert") || order.startsWith("reverse")) { + String[] tokens = order.split("\\s+"); + if (tokens.length == 2) { + order = tokens[1]; + flip = true; + } + } + BucketOrder o = PluginRegistry.bucketOrderPlugins.createObject(order); + if (o == null) { + UI.printWarning(Module.BCKT, "Unrecognized bucket ordering: \"%s\" - using hilbert", order); + return create("hilbert"); + } + return flip ? new InvertedBucketOrder(o) : o; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/bucket/ColumnBucketOrder.java b/src/main/java/org/sunflow/core/bucket/ColumnBucketOrder.java new file mode 100644 index 0000000..c0eac16 --- /dev/null +++ b/src/main/java/org/sunflow/core/bucket/ColumnBucketOrder.java @@ -0,0 +1,18 @@ +package org.sunflow.core.bucket; + +import org.sunflow.core.BucketOrder; + +public class ColumnBucketOrder implements BucketOrder { + public int[] getBucketSequence(int nbw, int nbh) { + int[] coords = new int[2 * nbw * nbh]; + for (int i = 0; i < nbw * nbh; i++) { + int bx = i / nbh; + int by = i % nbh; + if ((bx & 1) == 1) + by = nbh - 1 - by; + coords[2 * i + 0] = bx; + coords[2 * i + 1] = by; + } + return coords; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/bucket/DiagonalBucketOrder.java b/src/main/java/org/sunflow/core/bucket/DiagonalBucketOrder.java new file mode 100644 index 0000000..bddefea --- /dev/null +++ b/src/main/java/org/sunflow/core/bucket/DiagonalBucketOrder.java @@ -0,0 +1,26 @@ +package org.sunflow.core.bucket; + +import org.sunflow.core.BucketOrder; + +public class DiagonalBucketOrder implements BucketOrder { + public int[] getBucketSequence(int nbw, int nbh) { + int[] coords = new int[2 * nbw * nbh]; + int x = 0, y = 0, nx = 1, ny = 0; + for (int i = 0; i < nbw * nbh; i++) { + coords[2 * i + 0] = x; + coords[2 * i + 1] = y; + do { + if (y == ny) { + y = 0; + x = nx; + ny++; + nx++; + } else { + x--; + y++; + } + } while ((y >= nbh || x >= nbw) && i != (nbw * nbh - 1)); + } + return coords; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/bucket/HilbertBucketOrder.java b/src/main/java/org/sunflow/core/bucket/HilbertBucketOrder.java new file mode 100644 index 0000000..8f546e8 --- /dev/null +++ b/src/main/java/org/sunflow/core/bucket/HilbertBucketOrder.java @@ -0,0 +1,62 @@ +package org.sunflow.core.bucket; + +import org.sunflow.core.BucketOrder; + +public class HilbertBucketOrder implements BucketOrder { + public int[] getBucketSequence(int nbw, int nbh) { + int hi = 0; // hilbert curve index + int hn = 0; // hilbert curve order + while (((1 << hn) < nbw || (1 << hn) < nbh) && hn < 16) + hn++; // fit to number of buckets + int hN = 1 << (2 * hn); // number of hilbert buckets - 2**2n + int n = nbw * nbh; // total number of buckets + int[] coords = new int[2 * n]; // storage for bucket coordinates + for (int i = 0; i < n; i++) { + int hx, hy; + do { + // s is the hilbert index, shifted to start in the middle + int s = hi; // (hi + (hN >> 1)) & (hN - 1); + // int n = hn; + // adapted from Hacker's Delight + int comp, swap, cs, t, sr; + s = s | (0x55555555 << (2 * hn)); // Pad s on left with 01 + sr = (s >>> 1) & 0x55555555; // (no change) groups. + cs = ((s & 0x55555555) + sr) ^ 0x55555555;// Compute + // complement + // & swap info in + // two-bit groups. + // Parallel prefix xor op to propagate both complement + // and swap info together from left to right (there is + // no step "cs ^= cs >> 1", so in effect it computes + // two independent parallel prefix operations on two + // interleaved sets of sixteen bits). + cs = cs ^ (cs >>> 2); + cs = cs ^ (cs >>> 4); + cs = cs ^ (cs >>> 8); + cs = cs ^ (cs >>> 16); + swap = cs & 0x55555555; // Separate the swap and + comp = (cs >>> 1) & 0x55555555; // complement bits. + t = (s & swap) ^ comp; // Calculate x and y in + s = s ^ sr ^ t ^ (t << 1); // the odd & even bit + // positions, resp. + s = s & ((1 << 2 * hn) - 1); // Clear out any junk + // on the left (unpad). + // Now "unshuffle" to separate the x and y bits. + t = (s ^ (s >>> 1)) & 0x22222222; + s = s ^ t ^ (t << 1); + t = (s ^ (s >>> 2)) & 0x0C0C0C0C; + s = s ^ t ^ (t << 2); + t = (s ^ (s >>> 4)) & 0x00F000F0; + s = s ^ t ^ (t << 4); + t = (s ^ (s >>> 8)) & 0x0000FF00; + s = s ^ t ^ (t << 8); + hx = s >>> 16; // Assign the two halves + hy = s & 0xFFFF; // of t to x and y. + hi++; + } while ((hx >= nbw || hy >= nbh || hx < 0 || hy < 0) && hi < hN); + coords[2 * i + 0] = hx; + coords[2 * i + 1] = hy; + } + return coords; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/bucket/InvertedBucketOrder.java b/src/main/java/org/sunflow/core/bucket/InvertedBucketOrder.java new file mode 100644 index 0000000..bef33b4 --- /dev/null +++ b/src/main/java/org/sunflow/core/bucket/InvertedBucketOrder.java @@ -0,0 +1,26 @@ +package org.sunflow.core.bucket; + +import org.sunflow.core.BucketOrder; + +public class InvertedBucketOrder implements BucketOrder { + private BucketOrder order; + + public InvertedBucketOrder(BucketOrder order) { + this.order = order; + } + + public int[] getBucketSequence(int nbw, int nbh) { + int[] coords = order.getBucketSequence(nbw, nbh); + for (int i = 0; i < coords.length / 2; i += 2) { + int src = i; + int dst = coords.length - 2 - i; + int tmp = coords[src + 0]; + coords[src + 0] = coords[dst + 0]; + coords[dst + 0] = tmp; + tmp = coords[src + 1]; + coords[src + 1] = coords[dst + 1]; + coords[dst + 1] = tmp; + } + return coords; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/bucket/RandomBucketOrder.java b/src/main/java/org/sunflow/core/bucket/RandomBucketOrder.java new file mode 100644 index 0000000..bd608aa --- /dev/null +++ b/src/main/java/org/sunflow/core/bucket/RandomBucketOrder.java @@ -0,0 +1,46 @@ +package org.sunflow.core.bucket; + +import org.sunflow.core.BucketOrder; + +public class RandomBucketOrder implements BucketOrder { + public int[] getBucketSequence(int nbw, int nbh) { + int[] coords = new int[2 * nbw * nbh]; + for (int i = 0; i < nbw * nbh; i++) { + int by = i / nbw; + int bx = i % nbw; + if ((by & 1) == 1) + bx = nbw - 1 - bx; + coords[2 * i + 0] = bx; + coords[2 * i + 1] = by; + } + + long seed = 2463534242L; + for (int i = 0; i < coords.length; i++) { + // pick 2 random indices + seed = xorshift(seed); + int src = mod((int) seed, nbw * nbh); + seed = xorshift(seed); + int dst = mod((int) seed, nbw * nbh); + int tmp = coords[2 * src + 0]; + coords[2 * src + 0] = coords[2 * dst + 0]; + coords[2 * dst + 0] = tmp; + tmp = coords[2 * src + 1]; + coords[2 * src + 1] = coords[2 * dst + 1]; + coords[2 * dst + 1] = tmp; + } + + return coords; + } + + private int mod(int a, int b) { + int m = a % b; + return (m < 0) ? m + b : m; + } + + private long xorshift(long y) { + y = y ^ (y << 13); + y = y ^ (y >>> 17); // unsigned + y = y ^ (y << 5); + return y; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/bucket/RowBucketOrder.java b/src/main/java/org/sunflow/core/bucket/RowBucketOrder.java new file mode 100644 index 0000000..8b02688 --- /dev/null +++ b/src/main/java/org/sunflow/core/bucket/RowBucketOrder.java @@ -0,0 +1,18 @@ +package org.sunflow.core.bucket; + +import org.sunflow.core.BucketOrder; + +public class RowBucketOrder implements BucketOrder { + public int[] getBucketSequence(int nbw, int nbh) { + int[] coords = new int[2 * nbw * nbh]; + for (int i = 0; i < nbw * nbh; i++) { + int by = i / nbw; + int bx = i % nbw; + if ((by & 1) == 1) + bx = nbw - 1 - bx; + coords[2 * i + 0] = bx; + coords[2 * i + 1] = by; + } + return coords; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/bucket/SpiralBucketOrder.java b/src/main/java/org/sunflow/core/bucket/SpiralBucketOrder.java new file mode 100644 index 0000000..4d33688 --- /dev/null +++ b/src/main/java/org/sunflow/core/bucket/SpiralBucketOrder.java @@ -0,0 +1,41 @@ +package org.sunflow.core.bucket; + +import org.sunflow.core.BucketOrder; + +public class SpiralBucketOrder implements BucketOrder { + public int[] getBucketSequence(int nbw, int nbh) { + int[] coords = new int[2 * nbw * nbh]; + for (int i = 0; i < nbw * nbh; i++) { + int bx, by; + int center = (Math.min(nbw, nbh) - 1) / 2; + int nx = nbw; + int ny = nbh; + while (i < (nx * ny)) { + nx--; + ny--; + } + int nxny = nx * ny; + int minnxny = Math.min(nx, ny); + if ((minnxny & 1) == 1) { + if (i <= (nxny + ny)) { + bx = nx - minnxny / 2; + by = -minnxny / 2 + i - nxny; + } else { + bx = nx - minnxny / 2 - (i - (nxny + ny)); + by = ny - minnxny / 2; + } + } else { + if (i <= (nxny + ny)) { + bx = -minnxny / 2; + by = ny - minnxny / 2 - (i - nxny); + } else { + bx = -minnxny / 2 + (i - (nxny + ny)); + by = -minnxny / 2; + } + } + coords[2 * i + 0] = bx + center; + coords[2 * i + 1] = by + center; + } + return coords; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/camera/FisheyeLens.java b/src/main/java/org/sunflow/core/camera/FisheyeLens.java new file mode 100644 index 0000000..fecd778 --- /dev/null +++ b/src/main/java/org/sunflow/core/camera/FisheyeLens.java @@ -0,0 +1,21 @@ +package org.sunflow.core.camera; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.CameraLens; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; + +public class FisheyeLens implements CameraLens { + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lensX, double lensY, double time) { + float cx = 2.0f * x / imageWidth - 1.0f; + float cy = 2.0f * y / imageHeight - 1.0f; + float r2 = cx * cx + cy * cy; + if (r2 > 1) + return null; // outside the fisheye + return new Ray(0, 0, 0, cx, cy, (float) -Math.sqrt(1 - r2)); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/camera/PinholeLens.java b/src/main/java/org/sunflow/core/camera/PinholeLens.java new file mode 100644 index 0000000..0a9b86e --- /dev/null +++ b/src/main/java/org/sunflow/core/camera/PinholeLens.java @@ -0,0 +1,40 @@ +package org.sunflow.core.camera; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.CameraLens; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; + +public class PinholeLens implements CameraLens { + private float au, av; + private float aspect, fov; + private float shiftX, shiftY; + + public PinholeLens() { + fov = 90; + aspect = 1; + shiftX = shiftY = 0; + update(); + } + + public boolean update(ParameterList pl, SunflowAPI api) { + // get parameters + fov = pl.getFloat("fov", fov); + aspect = pl.getFloat("aspect", aspect); + shiftX = pl.getFloat("shift.x", shiftX); + shiftY = pl.getFloat("shift.y", shiftY); + update(); + return true; + } + + private void update() { + au = (float) Math.tan(Math.toRadians(fov * 0.5f)); + av = au / aspect; + } + + public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lensX, double lensY, double time) { + float du = shiftX - au + ((2.0f * au * x) / (imageWidth - 1.0f)); + float dv = shiftY - av + ((2.0f * av * y) / (imageHeight - 1.0f)); + return new Ray(0, 0, 0, du, dv, -1); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/camera/SphericalLens.java b/src/main/java/org/sunflow/core/camera/SphericalLens.java new file mode 100644 index 0000000..5746aac --- /dev/null +++ b/src/main/java/org/sunflow/core/camera/SphericalLens.java @@ -0,0 +1,19 @@ +package org.sunflow.core.camera; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.CameraLens; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; + +public class SphericalLens implements CameraLens { + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lensX, double lensY, double time) { + // Generate environment camera ray direction + double theta = 2 * Math.PI * x / imageWidth + Math.PI / 2; + double phi = Math.PI * (imageHeight - 1 - y) / imageHeight; + return new Ray(0, 0, 0, (float) (Math.cos(theta) * Math.sin(phi)), (float) (Math.cos(phi)), (float) (Math.sin(theta) * Math.sin(phi))); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/camera/ThinLens.java b/src/main/java/org/sunflow/core/camera/ThinLens.java new file mode 100644 index 0000000..092a6e4 --- /dev/null +++ b/src/main/java/org/sunflow/core/camera/ThinLens.java @@ -0,0 +1,103 @@ +package org.sunflow.core.camera; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.CameraLens; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; + +public class ThinLens implements CameraLens { + private float au, av; + private float aspect, fov; + private float shiftX, shiftY; + private float focusDistance; + private float lensRadius; + private int lensSides; + private float lensRotation; + private float lensRotationRadians; + + public ThinLens() { + focusDistance = 1; + lensRadius = 0; + fov = 90; + aspect = 1; + lensSides = 0; // < 3 means use circular lens + lensRotation = lensRotationRadians = 0; // this rotates polygonal lenses + } + + public boolean update(ParameterList pl, SunflowAPI api) { + // get parameters + fov = pl.getFloat("fov", fov); + aspect = pl.getFloat("aspect", aspect); + shiftX = pl.getFloat("shift.x", shiftX); + shiftY = pl.getFloat("shift.y", shiftY); + focusDistance = pl.getFloat("focus.distance", focusDistance); + lensRadius = pl.getFloat("lens.radius", lensRadius); + lensSides = pl.getInt("lens.sides", lensSides); + lensRotation = pl.getFloat("lens.rotation", lensRotation); + update(); + return true; + } + + private void update() { + au = (float) Math.tan(Math.toRadians(fov * 0.5f)) * focusDistance; + av = au / aspect; + lensRotationRadians = (float) Math.toRadians(lensRotation); + } + + public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lensX, double lensY, double time) { + float du = shiftX * focusDistance - au + ((2.0f * au * x) / (imageWidth - 1.0f)); + float dv = shiftY * focusDistance - av + ((2.0f * av * y) / (imageHeight - 1.0f)); + + float eyeX, eyeY; + if (lensSides < 3) { + double angle, r; + // concentric map sampling + double r1 = 2 * lensX - 1; + double r2 = 2 * lensY - 1; + if (r1 > -r2) { + if (r1 > r2) { + r = r1; + angle = 0.25 * Math.PI * r2 / r1; + } else { + r = r2; + angle = 0.25 * Math.PI * (2 - r1 / r2); + } + } else { + if (r1 < r2) { + r = -r1; + angle = 0.25 * Math.PI * (4 + r2 / r1); + } else { + r = -r2; + if (r2 != 0) + angle = 0.25 * Math.PI * (6 - r1 / r2); + else + angle = 0; + } + } + r *= lensRadius; + // point on the lens + eyeX = (float) (Math.cos(angle) * r); + eyeY = (float) (Math.sin(angle) * r); + } else { + // sample N-gon + // FIXME: this could use concentric sampling + lensY *= lensSides; + float side = (int) lensY; + float offs = (float) lensY - side; + float dist = (float) Math.sqrt(lensX); + float a0 = (float) (side * Math.PI * 2.0f / lensSides + lensRotationRadians); + float a1 = (float) ((side + 1.0f) * Math.PI * 2.0f / lensSides + lensRotationRadians); + eyeX = (float) ((Math.cos(a0) * (1.0f - offs) + Math.cos(a1) * offs) * dist); + eyeY = (float) ((Math.sin(a0) * (1.0f - offs) + Math.sin(a1) * offs) * dist); + eyeX *= lensRadius; + eyeY *= lensRadius; + } + float eyeZ = 0; + // point on the image plane + float dirX = du; + float dirY = dv; + float dirZ = -focusDistance; + // ray + return new Ray(eyeX, eyeY, eyeZ, dirX - eyeX, dirY - eyeY, dirZ - eyeZ); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/display/FastDisplay.java b/src/main/java/org/sunflow/core/display/FastDisplay.java new file mode 100644 index 0000000..5c62f4e --- /dev/null +++ b/src/main/java/org/sunflow/core/display/FastDisplay.java @@ -0,0 +1,107 @@ +package org.sunflow.core.display; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.image.BufferedImage; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Display; +import org.sunflow.image.Color; +import org.sunflow.system.Timer; + +@SuppressWarnings("serial") +public class FastDisplay extends JPanel implements Display { + private JFrame frame; + private BufferedImage image; + private int[] pixels; + private Timer t; + private float seconds; + private int frames; + + public FastDisplay() { + image = null; + frame = null; + t = new Timer(); + frames = 0; + seconds = 0; + } + + public synchronized void imageBegin(int w, int h, int bucketSize) { + if (frame != null && image != null && w == image.getWidth() && h == image.getHeight()) { + // nothing to do + } else { + // allocate new framebuffer + pixels = new int[w * h]; + image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + // prepare frame + if (frame == null) { + setPreferredSize(new Dimension(w, h)); + frame = new JFrame("Sunflow v" + SunflowAPI.VERSION); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) + System.exit(0); + } + }); + frame.setContentPane(this); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + } + // start counter + t.start(); + } + + public void imagePrepare(int x, int y, int w, int h, int id) { + } + + public void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha) { + int iw = image.getWidth(); + int off = x + iw * y; + iw -= w; + for (int j = 0, index = 0; j < h; j++, off += iw) + for (int i = 0; i < w; i++, index++, off++) + pixels[off] = 0xFF000000 | data[index].toRGB(); + } + + public void imageFill(int x, int y, int w, int h, Color c, float alpha) { + int iw = image.getWidth(); + int off = x + iw * y; + iw -= w; + int rgb = 0xFF000000 | c.toRGB(); + for (int j = 0, index = 0; j < h; j++, off += iw) + for (int i = 0; i < w; i++, index++, off++) + pixels[off] = rgb; + } + + public synchronized void imageEnd() { + // copy buffer + image.setRGB(0, 0, image.getWidth(), image.getHeight(), pixels, 0, image.getWidth()); + repaint(); + // update stats + t.end(); + seconds += t.seconds(); + frames++; + if (seconds > 1) { + // display average fps every second + frame.setTitle(String.format("Sunflow v%s - %.2f fps", SunflowAPI.VERSION, frames / seconds)); + frames = 0; + seconds = 0; + } + } + + @Override + public synchronized void paint(Graphics g) { + if (image == null) + return; + g.drawImage(image, 0, 0, null); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/display/FileDisplay.java b/src/main/java/org/sunflow/core/display/FileDisplay.java new file mode 100644 index 0000000..151b684 --- /dev/null +++ b/src/main/java/org/sunflow/core/display/FileDisplay.java @@ -0,0 +1,73 @@ +package org.sunflow.core.display; + +import java.io.IOException; + +import org.sunflow.PluginRegistry; +import org.sunflow.core.Display; +import org.sunflow.image.BitmapWriter; +import org.sunflow.image.Color; +import org.sunflow.system.FileUtils; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class FileDisplay implements Display { + private BitmapWriter writer; + private String filename; + + public FileDisplay(boolean saveImage) { + this(saveImage ? "output.png" : ".none"); + } + + public FileDisplay(String filename) { + this.filename = filename == null ? "output.png" : filename; + String extension = FileUtils.getExtension(filename); + writer = PluginRegistry.bitmapWriterPlugins.createObject(extension); + } + + public void imageBegin(int w, int h, int bucketSize) { + if (writer == null) + return; + try { + writer.openFile(filename); + writer.writeHeader(w, h, bucketSize); + } catch (IOException e) { + UI.printError(Module.IMG, "I/O error occured while preparing image for display: %s", e.getMessage()); + } + } + + public void imagePrepare(int x, int y, int w, int h, int id) { + // does nothing for files + } + + public void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha) { + if (writer == null) + return; + try { + writer.writeTile(x, y, w, h, data, alpha); + } catch (IOException e) { + UI.printError(Module.IMG, "I/O error occured while writing image tile [(%d,%d) %dx%d] image for display: %s", x, y, w, h, e.getMessage()); + } + } + + public void imageFill(int x, int y, int w, int h, Color c, float alpha) { + if (writer == null) + return; + Color[] colorTile = new Color[w * h]; + float[] alphaTile = new float[w * h]; + for (int i = 0; i < colorTile.length; i++) { + colorTile[i] = c; + alphaTile[i] = alpha; + } + imageUpdate(x, y, w, h, colorTile, alphaTile); + } + + public void imageEnd() { + if (writer == null) + return; + try { + writer.closeFile(); + } catch (IOException e) { + UI.printError(Module.IMG, "I/O error occured while closing the display: %s", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/display/FrameDisplay.java b/src/main/java/org/sunflow/core/display/FrameDisplay.java new file mode 100644 index 0000000..7999f7a --- /dev/null +++ b/src/main/java/org/sunflow/core/display/FrameDisplay.java @@ -0,0 +1,85 @@ +package org.sunflow.core.display; + +import java.awt.Dimension; +import java.awt.Toolkit; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; + +import javax.swing.JFrame; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Display; +import org.sunflow.image.Color; +import org.sunflow.system.ImagePanel; + +public class FrameDisplay implements Display { + private String filename; + private RenderFrame frame; + + public FrameDisplay() { + this(null); + } + + public FrameDisplay(String filename) { + this.filename = filename; + frame = null; + } + + public void imageBegin(int w, int h, int bucketSize) { + if (frame == null) { + frame = new RenderFrame(); + frame.imagePanel.imageBegin(w, h, bucketSize); + Dimension screenRes = Toolkit.getDefaultToolkit().getScreenSize(); + boolean needFit = false; + if (w >= (screenRes.getWidth() - 200) || h >= (screenRes.getHeight() - 200)) { + frame.imagePanel.setPreferredSize(new Dimension((int) screenRes.getWidth() - 200, (int) screenRes.getHeight() - 200)); + needFit = true; + } else + frame.imagePanel.setPreferredSize(new Dimension(w, h)); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + if (needFit) + frame.imagePanel.fit(); + } else + frame.imagePanel.imageBegin(w, h, bucketSize); + } + + public void imagePrepare(int x, int y, int w, int h, int id) { + frame.imagePanel.imagePrepare(x, y, w, h, id); + } + + public void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha) { + frame.imagePanel.imageUpdate(x, y, w, h, data, alpha); + } + + public void imageFill(int x, int y, int w, int h, Color c, float alpha) { + frame.imagePanel.imageFill(x, y, w, h, c, alpha); + } + + public void imageEnd() { + frame.imagePanel.imageEnd(); + if (filename != null) + frame.imagePanel.save(filename); + } + + @SuppressWarnings("serial") + private static class RenderFrame extends JFrame { + ImagePanel imagePanel; + + RenderFrame() { + super("Sunflow v" + SunflowAPI.VERSION); + setDefaultCloseOperation(EXIT_ON_CLOSE); + addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) + System.exit(0); + } + }); + imagePanel = new ImagePanel(); + setContentPane(imagePanel); + pack(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/display/ImgPipeDisplay.java b/src/main/java/org/sunflow/core/display/ImgPipeDisplay.java new file mode 100644 index 0000000..b11017a --- /dev/null +++ b/src/main/java/org/sunflow/core/display/ImgPipeDisplay.java @@ -0,0 +1,101 @@ +package org.sunflow.core.display; + +import java.io.IOException; + +import javax.swing.JPanel; + +import org.sunflow.core.Display; +import org.sunflow.image.Color; + +@SuppressWarnings("serial") +public class ImgPipeDisplay extends JPanel implements Display { + private int ih; + + /** + * Render to stdout using the imgpipe protocol used in mental image's + * imf_disp viewer. http://www.lamrug.org/resources/stubtips.html + */ + public ImgPipeDisplay() { + } + + public synchronized void imageBegin(int w, int h, int bucketSize) { + ih = h; + outputPacket(5, w, h, Float.floatToRawIntBits(1.0f), 0); + System.out.flush(); + } + + public synchronized void imagePrepare(int x, int y, int w, int h, int id) { + } + + public synchronized void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha) { + int xl = x; + int xh = x + w - 1; + int yl = ih - 1 - (y + h - 1); + int yh = ih - 1 - y; + outputPacket(2, xl, xh, yl, yh); + byte[] rgba = new byte[4 * (yh - yl + 1) * (xh - xl + 1)]; + for (int j = 0, idx = 0; j < h; j++) { + for (int i = 0; i < w; i++, idx += 4) { + int rgb = data[(h - j - 1) * w + i].toNonLinear().toRGB(); + int cr = (rgb >> 16) & 0xFF; + int cg = (rgb >> 8) & 0xFF; + int cb = rgb & 0xFF; + rgba[idx + 0] = (byte) (cr & 0xFF); + rgba[idx + 1] = (byte) (cg & 0xFF); + rgba[idx + 2] = (byte) (cb & 0xFF); + rgba[idx + 3] = (byte) (0xFF); + } + } + try { + System.out.write(rgba); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public synchronized void imageFill(int x, int y, int w, int h, Color c, float alpha) { + int xl = x; + int xh = x + w - 1; + int yl = ih - 1 - (y + h - 1); + int yh = ih - 1 - y; + outputPacket(2, xl, xh, yl, yh); + int rgb = c.toNonLinear().toRGB(); + int cr = (rgb >> 16) & 0xFF; + int cg = (rgb >> 8) & 0xFF; + int cb = rgb & 0xFF; + byte[] rgba = new byte[4 * (yh - yl + 1) * (xh - xl + 1)]; + for (int j = 0, idx = 0; j < h; j++) { + for (int i = 0; i < w; i++, idx += 4) { + rgba[idx + 0] = (byte) (cr & 0xFF); + rgba[idx + 1] = (byte) (cg & 0xFF); + rgba[idx + 2] = (byte) (cb & 0xFF); + rgba[idx + 3] = (byte) (0xFF); + } + } + try { + System.out.write(rgba); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public synchronized void imageEnd() { + outputPacket(4, 0, 0, 0, 0); + System.out.flush(); + } + + private void outputPacket(int type, int d0, int d1, int d2, int d3) { + outputInt32(type); + outputInt32(d0); + outputInt32(d1); + outputInt32(d2); + outputInt32(d3); + } + + private void outputInt32(int i) { + System.out.write((i >> 24) & 0xFF); + System.out.write((i >> 16) & 0xFF); + System.out.write((i >> 8) & 0xFF); + System.out.write(i & 0xFF); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/filter/BlackmanHarrisFilter.java b/src/main/java/org/sunflow/core/filter/BlackmanHarrisFilter.java new file mode 100644 index 0000000..e5f26f6 --- /dev/null +++ b/src/main/java/org/sunflow/core/filter/BlackmanHarrisFilter.java @@ -0,0 +1,24 @@ +package org.sunflow.core.filter; + +import org.sunflow.core.Filter; + +public class BlackmanHarrisFilter implements Filter { + public float getSize() { + return 4; + } + + public float get(float x, float y) { + return bh1d(x * 0.5f) * bh1d(y * 0.5f); + } + + private float bh1d(float x) { + if (x < -1.0f || x > 1.0f) + return 0.0f; + x = (x + 1) * 0.5f; + final double A0 = 0.35875; + final double A1 = -0.48829; + final double A2 = 0.14128; + final double A3 = -0.01168; + return (float) (A0 + A1 * Math.cos(2 * Math.PI * x) + A2 * Math.cos(4 * Math.PI * x) + A3 * Math.cos(6 * Math.PI * x)); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/filter/BoxFilter.java b/src/main/java/org/sunflow/core/filter/BoxFilter.java new file mode 100644 index 0000000..78f9cf2 --- /dev/null +++ b/src/main/java/org/sunflow/core/filter/BoxFilter.java @@ -0,0 +1,13 @@ +package org.sunflow.core.filter; + +import org.sunflow.core.Filter; + +public class BoxFilter implements Filter { + public float getSize() { + return 1.0f; + } + + public float get(float x, float y) { + return 1.0f; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/filter/CatmullRomFilter.java b/src/main/java/org/sunflow/core/filter/CatmullRomFilter.java new file mode 100644 index 0000000..1f97c17 --- /dev/null +++ b/src/main/java/org/sunflow/core/filter/CatmullRomFilter.java @@ -0,0 +1,24 @@ +package org.sunflow.core.filter; + +import org.sunflow.core.Filter; + +public class CatmullRomFilter implements Filter { + public float getSize() { + return 4.0f; + } + + public float get(float x, float y) { + return catrom1d(x) * catrom1d(y); + } + + private float catrom1d(float x) { + x = Math.abs(x); + float x2 = x * x; + float x3 = x * x2; + if (x >= 2) + return 0; + if (x < 1) + return 3 * x3 - 5 * x2 + 2; + return -x3 + 5 * x2 - 8 * x + 4; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/filter/CubicBSpline.java b/src/main/java/org/sunflow/core/filter/CubicBSpline.java new file mode 100644 index 0000000..805d6ea --- /dev/null +++ b/src/main/java/org/sunflow/core/filter/CubicBSpline.java @@ -0,0 +1,28 @@ +package org.sunflow.core.filter; + +import org.sunflow.core.Filter; + +public class CubicBSpline implements Filter { + public float get(float x, float y) { + return B3(x) * B3(y); + } + + public float getSize() { + return 4; + } + + private float B3(float t) { + t = Math.abs(t); + if (t <= 1) + return b1(1 - t); + return b0(2 - t); + } + + private float b0(float t) { + return t * t * t * (1.0f / 6); + } + + private float b1(float t) { + return (1.0f / 6) * (-3 * t * t * t + 3 * t * t + 3 * t + 1); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/filter/GaussianFilter.java b/src/main/java/org/sunflow/core/filter/GaussianFilter.java new file mode 100644 index 0000000..4f4a082 --- /dev/null +++ b/src/main/java/org/sunflow/core/filter/GaussianFilter.java @@ -0,0 +1,21 @@ +package org.sunflow.core.filter; + +import org.sunflow.core.Filter; + +public class GaussianFilter implements Filter { + private float es2; + + public GaussianFilter() { + es2 = (float) -Math.exp(-getSize() * getSize()); + } + + public float getSize() { + return 3.0f; + } + + public float get(float x, float y) { + float gx = (float) Math.exp(-x * x) + es2; + float gy = (float) Math.exp(-y * y) + es2; + return gx * gy; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/filter/LanczosFilter.java b/src/main/java/org/sunflow/core/filter/LanczosFilter.java new file mode 100644 index 0000000..4fa56ae --- /dev/null +++ b/src/main/java/org/sunflow/core/filter/LanczosFilter.java @@ -0,0 +1,26 @@ +package org.sunflow.core.filter; + +import org.sunflow.core.Filter; + +public class LanczosFilter implements Filter { + public float getSize() { + return 4.0f; + } + + public float get(float x, float y) { + return sinc1d(x * 0.5f) * sinc1d(y * 0.5f); + } + + private float sinc1d(float x) { + x = Math.abs(x); + if (x < 1e-5f) + return 1; + if (x > 1.0f) + return 0; + x *= Math.PI; + float sinc = (float) Math.sin(3 * x) / (3 * x); + float lanczos = (float) Math.sin(x) / x; + return sinc * lanczos; + } + +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/filter/MitchellFilter.java b/src/main/java/org/sunflow/core/filter/MitchellFilter.java new file mode 100644 index 0000000..6a0117f --- /dev/null +++ b/src/main/java/org/sunflow/core/filter/MitchellFilter.java @@ -0,0 +1,24 @@ +package org.sunflow.core.filter; + +import org.sunflow.core.Filter; + +public class MitchellFilter implements Filter { + public float getSize() { + return 4.0f; + } + + public float get(float x, float y) { + return mitchell(x) * mitchell(y); + } + + private float mitchell(float x) { + final float B = 1 / 3.0f; + final float C = 1 / 3.0f; + final float SIXTH = 1 / 6.0f; + x = Math.abs(x); + float x2 = x * x; + if (x > 1.0f) + return ((-B - 6 * C) * x * x2 + (6 * B + 30 * C) * x2 + (-12 * B - 48 * C) * x + (8 * B + 24 * C)) * SIXTH; + return ((12 - 9 * B - 6 * C) * x * x2 + (-18 + 12 * B + 6 * C) * x2 + (6 - 2 * B)) * SIXTH; + } +} diff --git a/src/main/java/org/sunflow/core/filter/SincFilter.java b/src/main/java/org/sunflow/core/filter/SincFilter.java new file mode 100644 index 0000000..dea9e37 --- /dev/null +++ b/src/main/java/org/sunflow/core/filter/SincFilter.java @@ -0,0 +1,21 @@ +package org.sunflow.core.filter; + +import org.sunflow.core.Filter; + +public class SincFilter implements Filter { + public float getSize() { + return 4; + } + + public float get(float x, float y) { + return sinc1d(x) * sinc1d(y); + } + + private float sinc1d(float x) { + x = Math.abs(x); + if (x < 0.0001f) + return 1.0f; + x *= Math.PI; + return (float) Math.sin(x) / x; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/filter/TriangleFilter.java b/src/main/java/org/sunflow/core/filter/TriangleFilter.java new file mode 100644 index 0000000..3cbca11 --- /dev/null +++ b/src/main/java/org/sunflow/core/filter/TriangleFilter.java @@ -0,0 +1,13 @@ +package org.sunflow.core.filter; + +import org.sunflow.core.Filter; + +public class TriangleFilter implements Filter { + public float getSize() { + return 2; + } + + public float get(float x, float y) { + return (1.0f - Math.abs(x)) * (1.0f - Math.abs(y)); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/gi/AmbientOcclusionGIEngine.java b/src/main/java/org/sunflow/core/gi/AmbientOcclusionGIEngine.java new file mode 100644 index 0000000..80a9dea --- /dev/null +++ b/src/main/java/org/sunflow/core/gi/AmbientOcclusionGIEngine.java @@ -0,0 +1,53 @@ +package org.sunflow.core.gi; + +import org.sunflow.core.GIEngine; +import org.sunflow.core.Options; +import org.sunflow.core.Ray; +import org.sunflow.core.Scene; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Vector3; + +public class AmbientOcclusionGIEngine implements GIEngine { + private Color bright; + private Color dark; + private int samples; + private float maxDist; + + public Color getGlobalRadiance(ShadingState state) { + return Color.BLACK; + } + + public boolean init(Options options, Scene scene) { + bright = options.getColor("gi.ambocc.bright", Color.WHITE); + dark = options.getColor("gi.ambocc.dark", Color.BLACK); + samples = options.getInt("gi.ambocc.samples", 32); + maxDist = options.getFloat("gi.ambocc.maxdist", 0); + maxDist = (maxDist <= 0) ? Float.POSITIVE_INFINITY : maxDist; + return true; + } + + public Color getIrradiance(ShadingState state, Color diffuseReflectance) { + OrthoNormalBasis onb = state.getBasis(); + Vector3 w = new Vector3(); + Color result = Color.black(); + for (int i = 0; i < samples; i++) { + float xi = (float) state.getRandom(i, 0, samples); + float xj = (float) state.getRandom(i, 1, samples); + float phi = (float) (2 * Math.PI * xi); + float cosPhi = (float) Math.cos(phi); + float sinPhi = (float) Math.sin(phi); + float sinTheta = (float) Math.sqrt(xj); + float cosTheta = (float) Math.sqrt(1.0f - xj); + w.x = cosPhi * sinTheta; + w.y = sinPhi * sinTheta; + w.z = cosTheta; + onb.transform(w); + Ray r = new Ray(state.getPoint(), w); + r.setMax(maxDist); + result.add(Color.blend(bright, dark, state.traceShadow(r))); + } + return result.mul((float) Math.PI / samples); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/gi/FakeGIEngine.java b/src/main/java/org/sunflow/core/gi/FakeGIEngine.java new file mode 100644 index 0000000..c1cc46c --- /dev/null +++ b/src/main/java/org/sunflow/core/gi/FakeGIEngine.java @@ -0,0 +1,43 @@ +package org.sunflow.core.gi; + +import org.sunflow.core.GIEngine; +import org.sunflow.core.Options; +import org.sunflow.core.Scene; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.Vector3; + +/** + * This is a quick way to get a bit of ambient lighting into your scene with + * hardly any overhead. It's based on the formula found here: + * + * @link http://www.cs.utah.edu/~shirley/papers/rtrt/node7.html#SECTION00031100000000000000 + */ +public class FakeGIEngine implements GIEngine { + private Vector3 up; + private Color sky; + private Color ground; + + public Color getIrradiance(ShadingState state, Color diffuseReflectance) { + float cosTheta = Vector3.dot(up, state.getNormal()); + float sin2 = (1 - cosTheta * cosTheta); + float sine = sin2 > 0 ? (float) Math.sqrt(sin2) * 0.5f : 0; + if (cosTheta > 0) + return Color.blend(sky, ground, sine); + else + return Color.blend(ground, sky, sine); + } + + public Color getGlobalRadiance(ShadingState state) { + return Color.BLACK; + } + + public boolean init(Options options, Scene scene) { + up = options.getVector("gi.fake.up", new Vector3(0, 1, 0)).normalize(); + sky = options.getColor("gi.fake.sky", Color.WHITE).copy(); + ground = options.getColor("gi.fake.ground", Color.BLACK).copy(); + sky.mul((float) Math.PI); + ground.mul((float) Math.PI); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/gi/InstantGI.java b/src/main/java/org/sunflow/core/gi/InstantGI.java new file mode 100644 index 0000000..6f41a60 --- /dev/null +++ b/src/main/java/org/sunflow/core/gi/InstantGI.java @@ -0,0 +1,176 @@ +package org.sunflow.core.gi; + +import java.util.ArrayList; + +import org.sunflow.core.GIEngine; +import org.sunflow.core.Options; +import org.sunflow.core.PhotonStore; +import org.sunflow.core.Ray; +import org.sunflow.core.Scene; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class InstantGI implements GIEngine { + private int numPhotons; + private int numSets; + private float c; + private int numBias; + private PointLight[][] virtualLights; + + public Color getGlobalRadiance(ShadingState state) { + Point3 p = state.getPoint(); + Vector3 n = state.getNormal(); + int set = (int) (state.getRandom(0, 1, 1) * numSets); + float maxAvgPow = 0; + float minDist = 1; + Color pow = null; + for (PointLight vpl : virtualLights[set]) { + maxAvgPow = Math.max(maxAvgPow, vpl.power.getAverage()); + if (Vector3.dot(n, vpl.n) > 0.9f) { + float d = vpl.p.distanceToSquared(p); + if (d < minDist) { + pow = vpl.power; + minDist = d; + } + } + } + return pow == null ? Color.BLACK : pow.copy().mul(1.0f / maxAvgPow); + } + + public boolean init(Options options, Scene scene) { + numPhotons = options.getInt("gi.igi.samples", 64); + numSets = options.getInt("gi.igi.sets", 1); + c = options.getFloat("gi.igi.c", 0.00003f); + numBias = options.getInt("gi.igi.bias_samples", 0); + virtualLights = null; + if (numSets < 1) + numSets = 1; + UI.printInfo(Module.LIGHT, "Instant Global Illumination settings:"); + UI.printInfo(Module.LIGHT, " * Samples: %d", numPhotons); + UI.printInfo(Module.LIGHT, " * Sets: %d", numSets); + UI.printInfo(Module.LIGHT, " * Bias bound: %f", c); + UI.printInfo(Module.LIGHT, " * Bias rays: %d", numBias); + virtualLights = new PointLight[numSets][]; + if (numPhotons > 0) { + for (int i = 0, seed = 0; i < virtualLights.length; i++, seed += numPhotons) { + PointLightStore map = new PointLightStore(); + if (!scene.calculatePhotons(map, "virtual", seed, options)) + return false; + virtualLights[i] = map.virtualLights.toArray(new PointLight[map.virtualLights.size()]); + UI.printInfo(Module.LIGHT, "Stored %d virtual point lights for set %d of %d", virtualLights[i].length, i + 1, numSets); + } + } else { + // create an empty array + for (int i = 0; i < virtualLights.length; i++) + virtualLights[i] = new PointLight[0]; + } + return true; + } + + public Color getIrradiance(ShadingState state, Color diffuseReflectance) { + float b = (float) Math.PI * c / diffuseReflectance.getMax(); + Color irr = Color.black(); + Point3 p = state.getPoint(); + Vector3 n = state.getNormal(); + int set = (int) (state.getRandom(0, 1, 1) * numSets); + for (PointLight vpl : virtualLights[set]) { + Ray r = new Ray(p, vpl.p); + float dotNlD = -(r.dx * vpl.n.x + r.dy * vpl.n.y + r.dz * vpl.n.z); + float dotND = r.dx * n.x + r.dy * n.y + r.dz * n.z; + if (dotNlD > 0 && dotND > 0) { + float r2 = r.getMax() * r.getMax(); + Color opacity = state.traceShadow(r); + Color power = Color.blend(vpl.power, Color.BLACK, opacity); + float g = (dotND * dotNlD) / r2; + irr.madd(0.25f * Math.min(g, b), power); + } + } + // bias compensation + int nb = (state.getDiffuseDepth() == 0 || numBias <= 0) ? numBias : 1; + if (nb <= 0) + return irr; + OrthoNormalBasis onb = state.getBasis(); + Vector3 w = new Vector3(); + float scale = (float) Math.PI / nb; + for (int i = 0; i < nb; i++) { + float xi = (float) state.getRandom(i, 0, nb); + float xj = (float) state.getRandom(i, 1, nb); + float phi = (float) (xi * 2 * Math.PI); + float cosPhi = (float) Math.cos(phi); + float sinPhi = (float) Math.sin(phi); + float sinTheta = (float) Math.sqrt(xj); + float cosTheta = (float) Math.sqrt(1.0f - xj); + w.x = cosPhi * sinTheta; + w.y = sinPhi * sinTheta; + w.z = cosTheta; + onb.transform(w); + Ray r = new Ray(state.getPoint(), w); + r.setMax((float) Math.sqrt(cosTheta / b)); + ShadingState temp = state.traceFinalGather(r, i); + if (temp != null) { + temp.getInstance().prepareShadingState(temp); + if (temp.getShader() != null) { + float dist = temp.getRay().getMax(); + float r2 = dist * dist; + float cosThetaY = -Vector3.dot(w, temp.getNormal()); + if (cosThetaY > 0) { + float g = (cosTheta * cosThetaY) / r2; + // was this path accounted for yet? + if (g > b) + irr.madd(scale * (g - b) / g, temp.getShader().getRadiance(temp)); + } + } + } + } + return irr; + } + + private static class PointLight { + Point3 p; + Vector3 n; + Color power; + } + + private class PointLightStore implements PhotonStore { + ArrayList virtualLights = new ArrayList(); + + public int numEmit() { + return numPhotons; + } + + public void prepare(Options options, BoundingBox sceneBounds) { + } + + public void store(ShadingState state, Vector3 dir, Color power, Color diffuse) { + state.faceforward(); + PointLight vpl = new PointLight(); + vpl.p = state.getPoint(); + vpl.n = state.getNormal(); + vpl.power = power; + synchronized (this) { + virtualLights.add(vpl); + } + } + + public void init() { + } + + public boolean allowDiffuseBounced() { + return true; + } + + public boolean allowReflectionBounced() { + return true; + } + + public boolean allowRefractionBounced() { + return true; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/gi/IrradianceCacheGIEngine.java b/src/main/java/org/sunflow/core/gi/IrradianceCacheGIEngine.java new file mode 100644 index 0000000..230198b --- /dev/null +++ b/src/main/java/org/sunflow/core/gi/IrradianceCacheGIEngine.java @@ -0,0 +1,246 @@ +package org.sunflow.core.gi; + +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.sunflow.PluginRegistry; +import org.sunflow.core.GIEngine; +import org.sunflow.core.GlobalPhotonMapInterface; +import org.sunflow.core.Options; +import org.sunflow.core.Ray; +import org.sunflow.core.Scene; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.MathUtils; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class IrradianceCacheGIEngine implements GIEngine { + private int samples; + private float tolerance; + private float invTolerance; + private float minSpacing; + private float maxSpacing; + private Node root; + private ReentrantReadWriteLock rwl; + private GlobalPhotonMapInterface globalPhotonMap; + + public boolean init(Options options, Scene scene) { + // get settings + samples = options.getInt("gi.irr-cache.samples", 256); + tolerance = options.getFloat("gi.irr-cache.tolerance", 0.05f); + invTolerance = 1.0f / tolerance; + minSpacing = options.getFloat("gi.irr-cache.min_spacing", 0.05f); + maxSpacing = options.getFloat("gi.irr-cache.max_spacing", 5.00f); + root = null; + rwl = new ReentrantReadWriteLock(); + globalPhotonMap = PluginRegistry.globalPhotonMapPlugins.createObject(options.getString("gi.irr-cache.gmap", null)); + // check settings + samples = Math.max(0, samples); + minSpacing = Math.max(0.001f, minSpacing); + maxSpacing = Math.max(0.001f, maxSpacing); + // display settings + UI.printInfo(Module.LIGHT, "Irradiance cache settings:"); + UI.printInfo(Module.LIGHT, " * Samples: %d", samples); + if (tolerance <= 0) + UI.printInfo(Module.LIGHT, " * Tolerance: off"); + else + UI.printInfo(Module.LIGHT, " * Tolerance: %.3f", tolerance); + UI.printInfo(Module.LIGHT, " * Spacing: %.3f to %.3f", minSpacing, maxSpacing); + // prepare root node + Vector3 ext = scene.getBounds().getExtents(); + root = new Node(scene.getBounds().getCenter(), 1.0001f * MathUtils.max(ext.x, ext.y, ext.z)); + // init global photon map + return (globalPhotonMap != null) ? scene.calculatePhotons(globalPhotonMap, "global", 0, options) : true; + } + + public Color getGlobalRadiance(ShadingState state) { + if (globalPhotonMap == null) { + if (state.getShader() != null) + return state.getShader().getRadiance(state); + else + return Color.BLACK; + } else + return globalPhotonMap.getRadiance(state.getPoint(), state.getNormal()); + } + + public Color getIrradiance(ShadingState state, Color diffuseReflectance) { + if (samples <= 0) + return Color.BLACK; + if (state.getDiffuseDepth() > 0) { + // do simple path tracing for additional bounces (single ray) + float xi = (float) state.getRandom(0, 0, 1); + float xj = (float) state.getRandom(0, 1, 1); + float phi = (float) (xi * 2 * Math.PI); + float cosPhi = (float) Math.cos(phi); + float sinPhi = (float) Math.sin(phi); + float sinTheta = (float) Math.sqrt(xj); + float cosTheta = (float) Math.sqrt(1.0f - xj); + Vector3 w = new Vector3(); + w.x = cosPhi * sinTheta; + w.y = sinPhi * sinTheta; + w.z = cosTheta; + OrthoNormalBasis onb = state.getBasis(); + onb.transform(w); + Ray r = new Ray(state.getPoint(), w); + ShadingState temp = state.traceFinalGather(r, 0); + return temp != null ? getGlobalRadiance(temp).copy().mul((float) Math.PI) : Color.BLACK; + } + rwl.readLock().lock(); + Color irr = getIrradiance(state.getPoint(), state.getNormal()); + rwl.readLock().unlock(); + if (irr == null) { + // compute new sample + irr = Color.black(); + OrthoNormalBasis onb = state.getBasis(); + float invR = 0; + float minR = Float.POSITIVE_INFINITY; + Vector3 w = new Vector3(); + for (int i = 0; i < samples; i++) { + float xi = (float) state.getRandom(i, 0, samples); + float xj = (float) state.getRandom(i, 1, samples); + float phi = (float) (xi * 2 * Math.PI); + float cosPhi = (float) Math.cos(phi); + float sinPhi = (float) Math.sin(phi); + float sinTheta = (float) Math.sqrt(xj); + float cosTheta = (float) Math.sqrt(1.0f - xj); + w.x = cosPhi * sinTheta; + w.y = sinPhi * sinTheta; + w.z = cosTheta; + onb.transform(w); + Ray r = new Ray(state.getPoint(), w); + ShadingState temp = state.traceFinalGather(r, i); + if (temp != null) { + minR = Math.min(r.getMax(), minR); + invR += 1.0f / r.getMax(); + temp.getInstance().prepareShadingState(temp); + irr.add(getGlobalRadiance(temp)); + } + } + irr.mul((float) Math.PI / samples); + invR = samples / invR; + rwl.writeLock().lock(); + insert(state.getPoint(), state.getNormal(), invR, irr); + rwl.writeLock().unlock(); + // view irr-cache points + // irr = Color.YELLOW.copy().mul(1e6f); + } + return irr; + } + + private void insert(Point3 p, Vector3 n, float r0, Color irr) { + if (tolerance <= 0) + return; + Node node = root; + r0 = MathUtils.clamp(r0 * tolerance, minSpacing, maxSpacing) * invTolerance; + if (root.isInside(p)) { + while (node.sideLength >= (4.0 * r0 * tolerance)) { + int k = 0; + k |= (p.x > node.center.x) ? 1 : 0; + k |= (p.y > node.center.y) ? 2 : 0; + k |= (p.z > node.center.z) ? 4 : 0; + if (node.children[k] == null) { + Point3 c = new Point3(node.center); + c.x += ((k & 1) == 0) ? -node.quadSideLength : node.quadSideLength; + c.y += ((k & 2) == 0) ? -node.quadSideLength : node.quadSideLength; + c.z += ((k & 4) == 0) ? -node.quadSideLength : node.quadSideLength; + node.children[k] = new Node(c, node.halfSideLength); + } + node = node.children[k]; + } + } + Sample s = new Sample(p, n, r0, irr); + s.next = node.first; + node.first = s; + } + + private Color getIrradiance(Point3 p, Vector3 n) { + if (tolerance <= 0) + return null; + Sample x = new Sample(p, n); + float w = root.find(x); + return (x.irr == null) ? null : x.irr.mul(1.0f / w); + } + + private final class Node { + Node[] children; + Sample first; + Point3 center; + float sideLength; + float halfSideLength; + float quadSideLength; + + Node(Point3 center, float sideLength) { + children = new Node[8]; + for (int i = 0; i < 8; i++) + children[i] = null; + this.center = new Point3(center); + this.sideLength = sideLength; + halfSideLength = 0.5f * sideLength; + quadSideLength = 0.5f * halfSideLength; + first = null; + } + + final boolean isInside(Point3 p) { + return (Math.abs(p.x - center.x) < halfSideLength) && (Math.abs(p.y - center.y) < halfSideLength) && (Math.abs(p.z - center.z) < halfSideLength); + } + + final float find(Sample x) { + float weight = 0; + for (Sample s = first; s != null; s = s.next) { + float c2 = 1.0f - (x.nix * s.nix + x.niy * s.niy + x.niz * s.niz); + float d2 = (x.pix - s.pix) * (x.pix - s.pix) + (x.piy - s.piy) * (x.piy - s.piy) + (x.piz - s.piz) * (x.piz - s.piz); + if (c2 > tolerance * tolerance || d2 > maxSpacing * maxSpacing) + continue; + float invWi = (float) (Math.sqrt(d2) * s.invR0 + Math.sqrt(Math.max(c2, 0))); + if (invWi < tolerance || d2 < minSpacing * minSpacing) { + float wi = Math.min(1e10f, 1.0f / invWi); + if (x.irr != null) + x.irr.madd(wi, s.irr); + else + x.irr = s.irr.copy().mul(wi); + weight += wi; + } + } + for (int i = 0; i < 8; i++) + if ((children[i] != null) && (Math.abs(children[i].center.x - x.pix) <= halfSideLength) && (Math.abs(children[i].center.y - x.piy) <= halfSideLength) && (Math.abs(children[i].center.z - x.piz) <= halfSideLength)) + weight += children[i].find(x); + return weight; + } + } + + private static final class Sample { + float pix, piy, piz; + float nix, niy, niz; + float invR0; + Color irr; + Sample next; + + Sample(Point3 p, Vector3 n) { + pix = p.x; + piy = p.y; + piz = p.z; + Vector3 ni = new Vector3(n).normalize(); + nix = ni.x; + niy = ni.y; + niz = ni.z; + irr = null; + next = null; + } + + Sample(Point3 p, Vector3 n, float r0, Color irr) { + pix = p.x; + piy = p.y; + piz = p.z; + Vector3 ni = new Vector3(n).normalize(); + nix = ni.x; + niy = ni.y; + niz = ni.z; + invR0 = 1.0f / r0; + this.irr = irr; + next = null; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/gi/PathTracingGIEngine.java b/src/main/java/org/sunflow/core/gi/PathTracingGIEngine.java new file mode 100644 index 0000000..09b499f --- /dev/null +++ b/src/main/java/org/sunflow/core/gi/PathTracingGIEngine.java @@ -0,0 +1,59 @@ +package org.sunflow.core.gi; + +import org.sunflow.core.GIEngine; +import org.sunflow.core.Options; +import org.sunflow.core.Ray; +import org.sunflow.core.Scene; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Vector3; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class PathTracingGIEngine implements GIEngine { + private int samples; + + public boolean init(Options options, Scene scene) { + samples = options.getInt("gi.path.samples", 16); + samples = Math.max(0, samples); + UI.printInfo(Module.LIGHT, "Path tracer settings:"); + UI.printInfo(Module.LIGHT, " * Samples: %d", samples); + return true; + } + + public Color getIrradiance(ShadingState state, Color diffuseReflectance) { + if (samples <= 0) + return Color.BLACK; + // compute new sample + Color irr = Color.black(); + OrthoNormalBasis onb = state.getBasis(); + Vector3 w = new Vector3(); + int n = state.getDiffuseDepth() == 0 ? samples : 1; + for (int i = 0; i < n; i++) { + float xi = (float) state.getRandom(i, 0, n); + float xj = (float) state.getRandom(i, 1, n); + float phi = (float) (xi * 2 * Math.PI); + float cosPhi = (float) Math.cos(phi); + float sinPhi = (float) Math.sin(phi); + float sinTheta = (float) Math.sqrt(xj); + float cosTheta = (float) Math.sqrt(1.0f - xj); + w.x = cosPhi * sinTheta; + w.y = sinPhi * sinTheta; + w.z = cosTheta; + onb.transform(w); + ShadingState temp = state.traceFinalGather(new Ray(state.getPoint(), w), i); + if (temp != null) { + temp.getInstance().prepareShadingState(temp); + if (temp.getShader() != null) + irr.add(temp.getShader().getRadiance(temp)); + } + } + irr.mul((float) Math.PI / n); + return irr; + } + + public Color getGlobalRadiance(ShadingState state) { + return Color.BLACK; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/light/DirectionalSpotlight.java b/src/main/java/org/sunflow/core/light/DirectionalSpotlight.java new file mode 100644 index 0000000..d95a38f --- /dev/null +++ b/src/main/java/org/sunflow/core/light/DirectionalSpotlight.java @@ -0,0 +1,96 @@ +package org.sunflow.core.light; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.LightSample; +import org.sunflow.core.LightSource; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class DirectionalSpotlight implements LightSource { + private Point3 src; + private Vector3 dir; + private OrthoNormalBasis basis; + private float r, r2; + private Color radiance; + + public DirectionalSpotlight() { + src = new Point3(0, 0, 0); + dir = new Vector3(0, 0, -1); + dir.normalize(); + basis = OrthoNormalBasis.makeFromW(dir); + r = 1; + r2 = r * r; + radiance = Color.WHITE; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + src = pl.getPoint("source", src); + dir = pl.getVector("dir", dir); + dir.normalize(); + r = pl.getFloat("radius", r); + basis = OrthoNormalBasis.makeFromW(dir); + r2 = r * r; + radiance = pl.getColor("radiance", radiance); + return true; + } + + public int getNumSamples() { + return 1; + } + + public int getLowSamples() { + return 1; + } + + public void getSamples(ShadingState state) { + if (Vector3.dot(dir, state.getGeoNormal()) < 0 && Vector3.dot(dir, state.getNormal()) < 0) { + // project point onto source plane + float x = state.getPoint().x - src.x; + float y = state.getPoint().y - src.y; + float z = state.getPoint().z - src.z; + float t = ((x * dir.x) + (y * dir.y) + (z * dir.z)); + if (t >= 0.0) { + x -= (t * dir.x); + y -= (t * dir.y); + z -= (t * dir.z); + if (((x * x) + (y * y) + (z * z)) <= r2) { + Point3 p = new Point3(); + p.x = src.x + x; + p.y = src.y + y; + p.z = src.z + z; + LightSample dest = new LightSample(); + dest.setShadowRay(new Ray(state.getPoint(), p)); + dest.setRadiance(radiance, radiance); + dest.traceShadow(state); + state.addSample(dest); + } + } + } + } + + public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power) { + float phi = (float) (2 * Math.PI * randX1); + float s = (float) Math.sqrt(1.0f - randY1); + dir.x = r * (float) Math.cos(phi) * s; + dir.y = r * (float) Math.sin(phi) * s; + dir.z = 0; + basis.transform(dir); + Point3.add(src, dir, p); + dir.set(this.dir); + power.set(radiance).mul((float) Math.PI * r2); + } + + public float getPower() { + return radiance.copy().mul((float) Math.PI * r2).getLuminance(); + } + + public Instance createInstance() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/light/ImageBasedLight.java b/src/main/java/org/sunflow/core/light/ImageBasedLight.java new file mode 100644 index 0000000..8d3b465 --- /dev/null +++ b/src/main/java/org/sunflow/core/light/ImageBasedLight.java @@ -0,0 +1,275 @@ +package org.sunflow.core.light; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.LightSample; +import org.sunflow.core.LightSource; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.core.Texture; +import org.sunflow.core.TextureCache; +import org.sunflow.image.Bitmap; +import org.sunflow.image.Color; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.QMC; +import org.sunflow.math.Vector3; + +public class ImageBasedLight implements PrimitiveList, LightSource, Shader { + private Texture texture; + private OrthoNormalBasis basis; + private int numSamples; + private int numLowSamples; + private float jacobian; + private float[] colHistogram; + private float[][] imageHistogram; + private Vector3[] samples; + private Vector3[] lowSamples; + private Color[] colors; + private Color[] lowColors; + + public ImageBasedLight() { + texture = null; + updateBasis(new Vector3(0, 0, -1), new Vector3(0, 1, 0)); + numSamples = 64; + numLowSamples = 8; + } + + private void updateBasis(Vector3 center, Vector3 up) { + if (center != null && up != null) { + basis = OrthoNormalBasis.makeFromWV(center, up); + basis.swapWU(); + basis.flipV(); + } + } + + public boolean update(ParameterList pl, SunflowAPI api) { + updateBasis(pl.getVector("center", null), pl.getVector("up", null)); + numSamples = pl.getInt("samples", numSamples); + numLowSamples = pl.getInt("lowsamples", numLowSamples); + String filename = pl.getString("texture", null); + if (filename != null) + texture = TextureCache.getTexture(api.resolveTextureFilename(filename), false); + + // no texture provided + if (texture == null) + return false; + Bitmap b = texture.getBitmap(); + if (b == null) + return false; + + // rebuild histograms if this is a new texture + if (filename != null) { + imageHistogram = new float[b.getWidth()][b.getHeight()]; + colHistogram = new float[b.getWidth()]; + float du = 1.0f / b.getWidth(); + float dv = 1.0f / b.getHeight(); + for (int x = 0; x < b.getWidth(); x++) { + for (int y = 0; y < b.getHeight(); y++) { + float u = (x + 0.5f) * du; + float v = (y + 0.5f) * dv; + Color c = texture.getPixel(u, v); + imageHistogram[x][y] = c.getLuminance() * (float) Math.sin(Math.PI * v); + if (y > 0) + imageHistogram[x][y] += imageHistogram[x][y - 1]; + } + colHistogram[x] = imageHistogram[x][b.getHeight() - 1]; + if (x > 0) + colHistogram[x] += colHistogram[x - 1]; + for (int y = 0; y < b.getHeight(); y++) + imageHistogram[x][y] /= imageHistogram[x][b.getHeight() - 1]; + } + for (int x = 0; x < b.getWidth(); x++) + colHistogram[x] /= colHistogram[b.getWidth() - 1]; + jacobian = (float) (2 * Math.PI * Math.PI) / (b.getWidth() * b.getHeight()); + } + // take fixed samples + if (pl.getBoolean("fixed", samples != null)) { + // high density samples + samples = new Vector3[numSamples]; + colors = new Color[numSamples]; + generateFixedSamples(samples, colors); + // low density samples + lowSamples = new Vector3[numLowSamples]; + lowColors = new Color[numLowSamples]; + generateFixedSamples(lowSamples, lowColors); + } else { + // turn off + samples = lowSamples = null; + colors = lowColors = null; + } + return true; + } + + private void generateFixedSamples(Vector3[] samples, Color[] colors) { + for (int i = 0; i < samples.length; i++) { + double randX = (double) i / (double) samples.length; + double randY = QMC.halton(0, i); + int x = 0; + while (randX >= colHistogram[x] && x < colHistogram.length - 1) + x++; + float[] rowHistogram = imageHistogram[x]; + int y = 0; + while (randY >= rowHistogram[y] && y < rowHistogram.length - 1) + y++; + // sample from (x, y) + float u = (float) ((x == 0) ? (randX / colHistogram[0]) : ((randX - colHistogram[x - 1]) / (colHistogram[x] - colHistogram[x - 1]))); + float v = (float) ((y == 0) ? (randY / rowHistogram[0]) : ((randY - rowHistogram[y - 1]) / (rowHistogram[y] - rowHistogram[y - 1]))); + + float px = ((x == 0) ? colHistogram[0] : (colHistogram[x] - colHistogram[x - 1])); + float py = ((y == 0) ? rowHistogram[0] : (rowHistogram[y] - rowHistogram[y - 1])); + + float su = (x + u) / colHistogram.length; + float sv = (y + v) / rowHistogram.length; + + float invP = (float) Math.sin(sv * Math.PI) * jacobian / (numSamples * px * py); + samples[i] = getDirection(su, sv); + basis.transform(samples[i]); + colors[i] = texture.getPixel(su, sv).mul(invP); + } + } + + public void prepareShadingState(ShadingState state) { + if (state.includeLights()) + state.setShader(this); + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + if (r.getMax() == Float.POSITIVE_INFINITY) + state.setIntersection(0); + } + + public int getNumPrimitives() { + return 1; + } + + public float getPrimitiveBound(int primID, int i) { + return 0; + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + return null; + } + + public PrimitiveList getBakingPrimitives() { + return null; + } + + public int getNumSamples() { + return numSamples; + } + + public void getSamples(ShadingState state) { + if (samples == null) { + int n = state.getDiffuseDepth() > 0 ? 1 : numSamples; + for (int i = 0; i < n; i++) { + // random offset on unit square, we use the infinite version of + // getRandom because the light sampling is adaptive + double randX = state.getRandom(i, 0, n); + double randY = state.getRandom(i, 1, n); + int x = 0; + while (randX >= colHistogram[x] && x < colHistogram.length - 1) + x++; + float[] rowHistogram = imageHistogram[x]; + int y = 0; + while (randY >= rowHistogram[y] && y < rowHistogram.length - 1) + y++; + // sample from (x, y) + float u = (float) ((x == 0) ? (randX / colHistogram[0]) : ((randX - colHistogram[x - 1]) / (colHistogram[x] - colHistogram[x - 1]))); + float v = (float) ((y == 0) ? (randY / rowHistogram[0]) : ((randY - rowHistogram[y - 1]) / (rowHistogram[y] - rowHistogram[y - 1]))); + + float px = ((x == 0) ? colHistogram[0] : (colHistogram[x] - colHistogram[x - 1])); + float py = ((y == 0) ? rowHistogram[0] : (rowHistogram[y] - rowHistogram[y - 1])); + + float su = (x + u) / colHistogram.length; + float sv = (y + v) / rowHistogram.length; + float invP = (float) Math.sin(sv * Math.PI) * jacobian / (n * px * py); + Vector3 dir = getDirection(su, sv); + basis.transform(dir); + if (Vector3.dot(dir, state.getGeoNormal()) > 0) { + LightSample dest = new LightSample(); + dest.setShadowRay(new Ray(state.getPoint(), dir)); + dest.getShadowRay().setMax(Float.MAX_VALUE); + Color radiance = texture.getPixel(su, sv); + dest.setRadiance(radiance, radiance); + dest.getDiffuseRadiance().mul(invP); + dest.getSpecularRadiance().mul(invP); + dest.traceShadow(state); + state.addSample(dest); + } + } + } else { + if (state.getDiffuseDepth() > 0) { + for (int i = 0; i < numLowSamples; i++) { + if (Vector3.dot(lowSamples[i], state.getGeoNormal()) > 0 && Vector3.dot(lowSamples[i], state.getNormal()) > 0) { + LightSample dest = new LightSample(); + dest.setShadowRay(new Ray(state.getPoint(), lowSamples[i])); + dest.getShadowRay().setMax(Float.MAX_VALUE); + dest.setRadiance(lowColors[i], lowColors[i]); + dest.traceShadow(state); + state.addSample(dest); + } + } + } else { + for (int i = 0; i < numSamples; i++) { + if (Vector3.dot(samples[i], state.getGeoNormal()) > 0 && Vector3.dot(samples[i], state.getNormal()) > 0) { + LightSample dest = new LightSample(); + dest.setShadowRay(new Ray(state.getPoint(), samples[i])); + dest.getShadowRay().setMax(Float.MAX_VALUE); + dest.setRadiance(colors[i], colors[i]); + dest.traceShadow(state); + state.addSample(dest); + } + } + } + } + } + + public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power) { + } + + public Color getRadiance(ShadingState state) { + // lookup texture based on ray direction + return state.includeLights() ? getColor(basis.untransform(state.getRay().getDirection(), new Vector3())) : Color.BLACK; + } + + private Color getColor(Vector3 dir) { + float u, v; + // assume lon/lat format + double phi = 0, theta = 0; + phi = Math.acos(dir.y); + theta = Math.atan2(dir.z, dir.x); + u = (float) (0.5 - 0.5 * theta / Math.PI); + v = (float) (phi / Math.PI); + return texture.getPixel(u, v); + } + + private Vector3 getDirection(float u, float v) { + Vector3 dest = new Vector3(); + double phi = 0, theta = 0; + theta = u * 2 * Math.PI; + phi = v * Math.PI; + double sin_phi = Math.sin(phi); + dest.x = (float) (-sin_phi * Math.cos(theta)); + dest.y = (float) Math.cos(phi); + dest.z = (float) (sin_phi * Math.sin(theta)); + return dest; + } + + public void scatterPhoton(ShadingState state, Color power) { + } + + public float getPower() { + return 0; + } + + public Instance createInstance() { + return Instance.createTemporary(this, null, this); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/light/PointLight.java b/src/main/java/org/sunflow/core/light/PointLight.java new file mode 100644 index 0000000..86e5955 --- /dev/null +++ b/src/main/java/org/sunflow/core/light/PointLight.java @@ -0,0 +1,65 @@ +package org.sunflow.core.light; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.LightSample; +import org.sunflow.core.LightSource; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class PointLight implements LightSource { + private Point3 lightPoint; + private Color power; + + public PointLight() { + lightPoint = new Point3(0, 0, 0); + power = Color.WHITE; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + lightPoint = pl.getPoint("center", lightPoint); + power = pl.getColor("power", power); + return true; + } + + public int getNumSamples() { + return 1; + } + + public void getSamples(ShadingState state) { + Vector3 d = Point3.sub(lightPoint, state.getPoint(), new Vector3()); + if (Vector3.dot(d, state.getNormal()) > 0 && Vector3.dot(d, state.getGeoNormal()) > 0) { + LightSample dest = new LightSample(); + // prepare shadow ray + dest.setShadowRay(new Ray(state.getPoint(), lightPoint)); + float scale = 1.0f / (float) (4 * Math.PI * lightPoint.distanceToSquared(state.getPoint())); + dest.setRadiance(power, power); + dest.getDiffuseRadiance().mul(scale); + dest.getSpecularRadiance().mul(scale); + dest.traceShadow(state); + state.addSample(dest); + } + } + + public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power) { + p.set(lightPoint); + float phi = (float) (2 * Math.PI * randX1); + float s = (float) Math.sqrt(randY1 * (1.0f - randY1)); + dir.x = (float) Math.cos(phi) * s; + dir.y = (float) Math.sin(phi) * s; + dir.z = (float) (1 - 2 * randY1); + power.set(this.power); + } + + public float getPower() { + return power.getLuminance(); + } + + public Instance createInstance() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/light/SphereLight.java b/src/main/java/org/sunflow/core/light/SphereLight.java new file mode 100644 index 0000000..d589a02 --- /dev/null +++ b/src/main/java/org/sunflow/core/light/SphereLight.java @@ -0,0 +1,153 @@ +package org.sunflow.core.light; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.LightSample; +import org.sunflow.core.LightSource; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.core.primitive.Sphere; +import org.sunflow.image.Color; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Solvers; +import org.sunflow.math.Vector3; + +public class SphereLight implements LightSource, Shader { + private Color radiance; + private int numSamples; + private Point3 center; + private float radius; + private float r2; + + public SphereLight() { + radiance = Color.WHITE; + numSamples = 4; + center = new Point3(); + radius = r2 = 1; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + radiance = pl.getColor("radiance", radiance); + numSamples = pl.getInt("samples", numSamples); + radius = pl.getFloat("radius", radius); + r2 = radius * radius; + center = pl.getPoint("center", center); + return true; + } + + public int getNumSamples() { + return numSamples; + } + + public int getLowSamples() { + return 1; + } + + public boolean isVisible(ShadingState state) { + return state.getPoint().distanceToSquared(center) > r2; + } + + public void getSamples(ShadingState state) { + if (getNumSamples() <= 0) + return; + Vector3 wc = Point3.sub(center, state.getPoint(), new Vector3()); + float l2 = wc.lengthSquared(); + if (l2 <= r2) + return; // inside the sphere? + // top of the sphere as viewed from the current shading point + float topX = wc.x + state.getNormal().x * radius; + float topY = wc.y + state.getNormal().y * radius; + float topZ = wc.z + state.getNormal().z * radius; + if (state.getNormal().dot(topX, topY, topZ) <= 0) + return; // top of the sphere is below the horizon + float cosThetaMax = (float) Math.sqrt(Math.max(0, 1 - r2 / Vector3.dot(wc, wc))); + OrthoNormalBasis basis = OrthoNormalBasis.makeFromW(wc); + int samples = state.getDiffuseDepth() > 0 ? 1 : getNumSamples(); + float scale = (float) (2 * Math.PI * (1 - cosThetaMax)); + Color c = Color.mul(scale / samples, radiance); + for (int i = 0; i < samples; i++) { + // random offset on unit square + double randX = state.getRandom(i, 0, samples); + double randY = state.getRandom(i, 1, samples); + + // cone sampling + double cosTheta = (1 - randX) * cosThetaMax + randX; + double sinTheta = Math.sqrt(1 - cosTheta * cosTheta); + double phi = randY * 2 * Math.PI; + Vector3 dir = new Vector3((float) (Math.cos(phi) * sinTheta), (float) (Math.sin(phi) * sinTheta), (float) cosTheta); + basis.transform(dir); + + // check that the direction of the sample is the same as the + // normal + float cosNx = Vector3.dot(dir, state.getNormal()); + if (cosNx <= 0) + continue; + + float ocx = state.getPoint().x - center.x; + float ocy = state.getPoint().y - center.y; + float ocz = state.getPoint().z - center.z; + float qa = Vector3.dot(dir, dir); + float qb = 2 * ((dir.x * ocx) + (dir.y * ocy) + (dir.z * ocz)); + float qc = ((ocx * ocx) + (ocy * ocy) + (ocz * ocz)) - r2; + double[] t = Solvers.solveQuadric(qa, qb, qc); + if (t == null) + continue; + LightSample dest = new LightSample(); + // compute shadow ray to the sampled point + dest.setShadowRay(new Ray(state.getPoint(), dir)); + // FIXME: arbitrary bias, should handle as in other places + dest.getShadowRay().setMax((float) t[0] - 1e-3f); + // prepare sample + dest.setRadiance(c, c); + dest.traceShadow(state); + state.addSample(dest); + } + } + + public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power) { + float z = (float) (1 - 2 * randX2); + float r = (float) Math.sqrt(Math.max(0, 1 - z * z)); + float phi = (float) (2 * Math.PI * randY2); + float x = r * (float) Math.cos(phi); + float y = r * (float) Math.sin(phi); + p.x = center.x + x * radius; + p.y = center.y + y * radius; + p.z = center.z + z * radius; + OrthoNormalBasis basis = OrthoNormalBasis.makeFromW(new Vector3(x, y, z)); + phi = (float) (2 * Math.PI * randX1); + float cosPhi = (float) Math.cos(phi); + float sinPhi = (float) Math.sin(phi); + float sinTheta = (float) Math.sqrt(randY1); + float cosTheta = (float) Math.sqrt(1 - randY1); + dir.x = cosPhi * sinTheta; + dir.y = sinPhi * sinTheta; + dir.z = cosTheta; + basis.transform(dir); + power.set(radiance); + power.mul((float) (Math.PI * Math.PI * 4 * r2)); + } + + public float getPower() { + return radiance.copy().mul((float) (Math.PI * Math.PI * 4 * r2)).getLuminance(); + } + + public Color getRadiance(ShadingState state) { + if (!state.includeLights()) + return Color.BLACK; + state.faceforward(); + // emit constant radiance + return state.isBehind() ? Color.BLACK : radiance; + } + + public void scatterPhoton(ShadingState state, Color power) { + // do not scatter photons + } + + public Instance createInstance() { + return Instance.createTemporary(new Sphere(), Matrix4.translation(center.x, center.y, center.z).multiply(Matrix4.scale(radius)), this); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/light/SunSkyLight.java b/src/main/java/org/sunflow/core/light/SunSkyLight.java new file mode 100644 index 0000000..8b9c794 --- /dev/null +++ b/src/main/java/org/sunflow/core/light/SunSkyLight.java @@ -0,0 +1,337 @@ +package org.sunflow.core.light; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.LightSample; +import org.sunflow.core.LightSource; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.ChromaticitySpectrum; +import org.sunflow.image.Color; +import org.sunflow.image.ConstantSpectralCurve; +import org.sunflow.image.IrregularSpectralCurve; +import org.sunflow.image.RGBSpace; +import org.sunflow.image.RegularSpectralCurve; +import org.sunflow.image.SpectralCurve; +import org.sunflow.image.XYZColor; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.MathUtils; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class SunSkyLight implements LightSource, PrimitiveList, Shader { + // sunflow parameters + private int numSkySamples; + private OrthoNormalBasis basis; + private boolean groundExtendSky; + private Color groundColor; + // parameters to the model + private Vector3 sunDirWorld; + private float turbidity; + // derived quantities + private Vector3 sunDir; + private SpectralCurve sunSpectralRadiance; + private Color sunColor; + private float sunTheta; + private double zenithY, zenithx, zenithy; + private final double[] perezY = new double[5]; + private final double[] perezx = new double[5]; + private final double[] perezy = new double[5]; + private float jacobian; + private float[] colHistogram; + private float[][] imageHistogram; + // constant data + private static final float[] solAmplitudes = { 165.5f, 162.3f, 211.2f, + 258.8f, 258.2f, 242.3f, 267.6f, 296.6f, 305.4f, 300.6f, 306.6f, + 288.3f, 287.1f, 278.2f, 271.0f, 272.3f, 263.6f, 255.0f, 250.6f, + 253.1f, 253.5f, 251.3f, 246.3f, 241.7f, 236.8f, 232.1f, 228.2f, + 223.4f, 219.7f, 215.3f, 211.0f, 207.3f, 202.4f, 198.7f, 194.3f, + 190.7f, 186.3f, 182.6f }; + private static final RegularSpectralCurve solCurve = new RegularSpectralCurve(solAmplitudes, 380, 750); + private static final float[] k_oWavelengths = { 300, 305, 310, 315, 320, + 325, 330, 335, 340, 345, 350, 355, 445, 450, 455, 460, 465, 470, + 475, 480, 485, 490, 495, 500, 505, 510, 515, 520, 525, 530, 535, + 540, 545, 550, 555, 560, 565, 570, 575, 580, 585, 590, 595, 600, + 605, 610, 620, 630, 640, 650, 660, 670, 680, 690, 700, 710, 720, + 730, 740, 750, 760, 770, 780, 790, }; + private static final float[] k_oAmplitudes = { 10.0f, 4.8f, 2.7f, 1.35f, + .8f, .380f, .160f, .075f, .04f, .019f, .007f, .0f, .003f, .003f, + .004f, .006f, .008f, .009f, .012f, .014f, .017f, .021f, .025f, + .03f, .035f, .04f, .045f, .048f, .057f, .063f, .07f, .075f, .08f, + .085f, .095f, .103f, .110f, .12f, .122f, .12f, .118f, .115f, .12f, + .125f, .130f, .12f, .105f, .09f, .079f, .067f, .057f, .048f, .036f, + .028f, .023f, .018f, .014f, .011f, .010f, .009f, .007f, .004f, .0f, + .0f }; + private static final float[] k_gWavelengths = { 759, 760, 770, 771 }; + private static final float[] k_gAmplitudes = { 0, 3.0f, 0.210f, 0 }; + private static final float[] k_waWavelengths = { 689, 690, 700, 710, 720, + 730, 740, 750, 760, 770, 780, 790, 800 }; + private static final float[] k_waAmplitudes = { 0f, 0.160e-1f, 0.240e-1f, + 0.125e-1f, 0.100e+1f, 0.870f, 0.610e-1f, 0.100e-2f, 0.100e-4f, + 0.100e-4f, 0.600e-3f, 0.175e-1f, 0.360e-1f }; + + private static final IrregularSpectralCurve k_oCurve = new IrregularSpectralCurve(k_oWavelengths, k_oAmplitudes); + private static final IrregularSpectralCurve k_gCurve = new IrregularSpectralCurve(k_gWavelengths, k_gAmplitudes); + private static final IrregularSpectralCurve k_waCurve = new IrregularSpectralCurve(k_waWavelengths, k_waAmplitudes); + + public SunSkyLight() { + numSkySamples = 64; + sunDirWorld = new Vector3(1, 1, 1); + turbidity = 6; + basis = OrthoNormalBasis.makeFromWV(new Vector3(0, 0, 1), new Vector3(0, 1, 0)); + groundExtendSky = false; + groundColor = Color.BLACK; + initSunSky(); + } + + private SpectralCurve computeAttenuatedSunlight(float theta, float turbidity) { + float[] data = new float[91]; // holds the sunsky curve data + final double alpha = 1.3; + final double lozone = 0.35; + final double w = 2.0; + double beta = 0.04608365822050 * turbidity - 0.04586025928522; + // Relative optical mass + double m = 1.0 / (Math.cos(theta) + 0.000940 * Math.pow(1.6386 - theta, -1.253)); + for (int i = 0, lambda = 350; lambda <= 800; i++, lambda += 5) { + // Rayleigh scattering + double tauR = Math.exp(-m * 0.008735 * Math.pow(lambda / 1000.0, -4.08)); + // Aerosol (water + dust) attenuation + double tauA = Math.exp(-m * beta * Math.pow(lambda / 1000.0, -alpha)); + // Attenuation due to ozone absorption + double tauO = Math.exp(-m * k_oCurve.sample(lambda) * lozone); + // Attenuation due to mixed gases absorption + double tauG = Math.exp(-1.41 * k_gCurve.sample(lambda) * m / Math.pow(1.0 + 118.93 * k_gCurve.sample(lambda) * m, 0.45)); + // Attenuation due to water vapor absorption + double tauWA = Math.exp(-0.2385 * k_waCurve.sample(lambda) * w * m / Math.pow(1.0 + 20.07 * k_waCurve.sample(lambda) * w * m, 0.45)); + // 100.0 comes from solAmplitudes begin in wrong units. + double amp = /* 100.0 * */solCurve.sample(lambda) * tauR * tauA * tauO * tauG * tauWA; + data[i] = (float) amp; + } + return new RegularSpectralCurve(data, 350, 800); + } + + private double perezFunction(final double[] lam, double theta, double gamma, double lvz) { + double den = ((1.0 + lam[0] * Math.exp(lam[1])) * (1.0 + lam[2] * Math.exp(lam[3] * sunTheta) + lam[4] * Math.cos(sunTheta) * Math.cos(sunTheta))); + double num = ((1.0 + lam[0] * Math.exp(lam[1] / Math.cos(theta))) * (1.0 + lam[2] * Math.exp(lam[3] * gamma) + lam[4] * Math.cos(gamma) * Math.cos(gamma))); + return lvz * num / den; + } + + private void initSunSky() { + // perform all the required initialization of constants + sunDirWorld.normalize(); + sunDir = basis.untransform(sunDirWorld, new Vector3()); + sunDir.normalize(); + sunTheta = (float) Math.acos(MathUtils.clamp(sunDir.z, -1, 1)); + if (sunDir.z > 0) { + sunSpectralRadiance = computeAttenuatedSunlight(sunTheta, turbidity); + // produce color suitable for rendering + sunColor = RGBSpace.SRGB.convertXYZtoRGB(sunSpectralRadiance.toXYZ().mul(1e-4f)).constrainRGB(); + } else { + sunSpectralRadiance = new ConstantSpectralCurve(0); + } + // sunSolidAngle = (float) (0.25 * Math.PI * 1.39 * 1.39 / (150 * 150)); + float theta2 = sunTheta * sunTheta; + float theta3 = sunTheta * theta2; + float T = turbidity; + float T2 = turbidity * turbidity; + double chi = (4.0 / 9.0 - T / 120.0) * (Math.PI - 2.0 * sunTheta); + zenithY = (4.0453 * T - 4.9710) * Math.tan(chi) - 0.2155 * T + 2.4192; + zenithY *= 1000; /* conversion from kcd/m^2 to cd/m^2 */ + zenithx = (0.00165 * theta3 - 0.00374 * theta2 + 0.00208 * sunTheta + 0) * T2 + (-0.02902 * theta3 + 0.06377 * theta2 - 0.03202 * sunTheta + 0.00394) * T + (0.11693 * theta3 - 0.21196 * theta2 + 0.06052 * sunTheta + 0.25885); + zenithy = (0.00275 * theta3 - 0.00610 * theta2 + 0.00316 * sunTheta + 0) * T2 + (-0.04212 * theta3 + 0.08970 * theta2 - 0.04153 * sunTheta + 0.00515) * T + (0.15346 * theta3 - 0.26756 * theta2 + 0.06669 * sunTheta + 0.26688); + + perezY[0] = 0.17872 * T - 1.46303; + perezY[1] = -0.35540 * T + 0.42749; + perezY[2] = -0.02266 * T + 5.32505; + perezY[3] = 0.12064 * T - 2.57705; + perezY[4] = -0.06696 * T + 0.37027; + + perezx[0] = -0.01925 * T - 0.25922; + perezx[1] = -0.06651 * T + 0.00081; + perezx[2] = -0.00041 * T + 0.21247; + perezx[3] = -0.06409 * T - 0.89887; + perezx[4] = -0.00325 * T + 0.04517; + + perezy[0] = -0.01669 * T - 0.26078; + perezy[1] = -0.09495 * T + 0.00921; + perezy[2] = -0.00792 * T + 0.21023; + perezy[3] = -0.04405 * T - 1.65369; + perezy[4] = -0.01092 * T + 0.05291; + + final int w = 32, h = 32; + imageHistogram = new float[w][h]; + colHistogram = new float[w]; + float du = 1.0f / w; + float dv = 1.0f / h; + for (int x = 0; x < w; x++) { + for (int y = 0; y < h; y++) { + float u = (x + 0.5f) * du; + float v = (y + 0.5f) * dv; + Color c = getSkyRGB(getDirection(u, v)); + imageHistogram[x][y] = c.getLuminance() * (float) Math.sin(Math.PI * v); + if (y > 0) + imageHistogram[x][y] += imageHistogram[x][y - 1]; + } + colHistogram[x] = imageHistogram[x][h - 1]; + if (x > 0) + colHistogram[x] += colHistogram[x - 1]; + for (int y = 0; y < h; y++) + imageHistogram[x][y] /= imageHistogram[x][h - 1]; + } + for (int x = 0; x < w; x++) + colHistogram[x] /= colHistogram[w - 1]; + jacobian = (float) (2 * Math.PI * Math.PI) / (w * h); + } + + public boolean update(ParameterList pl, SunflowAPI api) { + Vector3 up = pl.getVector("up", null); + Vector3 east = pl.getVector("east", null); + if (up != null && east != null) + basis = OrthoNormalBasis.makeFromWV(up, east); + else if (up != null) + basis = OrthoNormalBasis.makeFromW(up); + numSkySamples = pl.getInt("samples", numSkySamples); + sunDirWorld = pl.getVector("sundir", sunDirWorld); + turbidity = pl.getFloat("turbidity", turbidity); + groundExtendSky = pl.getBoolean("ground.extendsky", groundExtendSky); + groundColor = pl.getColor("ground.color", groundColor); + // recompute model + initSunSky(); + return true; + } + + private Color getSkyRGB(Vector3 dir) { + if (dir.z < 0 && !groundExtendSky) + return groundColor; + if (dir.z < 0.001f) + dir.z = 0.001f; + dir.normalize(); + double theta = Math.acos(MathUtils.clamp(dir.z, -1, 1)); + double gamma = Math.acos(MathUtils.clamp(Vector3.dot(dir, sunDir), -1, 1)); + double x = perezFunction(perezx, theta, gamma, zenithx); + double y = perezFunction(perezy, theta, gamma, zenithy); + double Y = perezFunction(perezY, theta, gamma, zenithY) * 1e-4; + XYZColor c = ChromaticitySpectrum.get((float) x, (float) y); + // XYZColor c = new ChromaticitySpectrum((float) x, (float) y).toXYZ(); + float X = (float) (c.getX() * Y / c.getY()); + float Z = (float) (c.getZ() * Y / c.getY()); + return RGBSpace.SRGB.convertXYZtoRGB(X, (float) Y, Z); + } + + public int getNumSamples() { + return 1 + numSkySamples; + } + + public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power) { + // FIXME: not implemented + } + + public float getPower() { + return 0; + } + + public void getSamples(ShadingState state) { + if (Vector3.dot(sunDirWorld, state.getGeoNormal()) > 0 && Vector3.dot(sunDirWorld, state.getNormal()) > 0) { + LightSample dest = new LightSample(); + dest.setShadowRay(new Ray(state.getPoint(), sunDirWorld)); + dest.getShadowRay().setMax(Float.MAX_VALUE); + dest.setRadiance(sunColor, sunColor); + dest.traceShadow(state); + state.addSample(dest); + } + int n = state.getDiffuseDepth() > 0 ? 1 : numSkySamples; + for (int i = 0; i < n; i++) { + // random offset on unit square, we use the infinite version of + // getRandom because the light sampling is adaptive + double randX = state.getRandom(i, 0, n); + double randY = state.getRandom(i, 1, n); + + int x = 0; + while (randX >= colHistogram[x] && x < colHistogram.length - 1) + x++; + float[] rowHistogram = imageHistogram[x]; + int y = 0; + while (randY >= rowHistogram[y] && y < rowHistogram.length - 1) + y++; + // sample from (x, y) + float u = (float) ((x == 0) ? (randX / colHistogram[0]) : ((randX - colHistogram[x - 1]) / (colHistogram[x] - colHistogram[x - 1]))); + float v = (float) ((y == 0) ? (randY / rowHistogram[0]) : ((randY - rowHistogram[y - 1]) / (rowHistogram[y] - rowHistogram[y - 1]))); + + float px = ((x == 0) ? colHistogram[0] : (colHistogram[x] - colHistogram[x - 1])); + float py = ((y == 0) ? rowHistogram[0] : (rowHistogram[y] - rowHistogram[y - 1])); + + float su = (x + u) / colHistogram.length; + float sv = (y + v) / rowHistogram.length; + float invP = (float) Math.sin(sv * Math.PI) * jacobian / (n * px * py); + Vector3 localDir = getDirection(su, sv); + Vector3 dir = basis.transform(localDir, new Vector3()); + if (Vector3.dot(dir, state.getGeoNormal()) > 0 && Vector3.dot(dir, state.getNormal()) > 0) { + LightSample dest = new LightSample(); + dest.setShadowRay(new Ray(state.getPoint(), dir)); + dest.getShadowRay().setMax(Float.MAX_VALUE); + Color radiance = getSkyRGB(localDir); + dest.setRadiance(radiance, radiance); + dest.getDiffuseRadiance().mul(invP); + dest.getSpecularRadiance().mul(invP); + dest.traceShadow(state); + state.addSample(dest); + } + } + } + + public PrimitiveList getBakingPrimitives() { + return null; + } + + public int getNumPrimitives() { + return 1; + } + + public float getPrimitiveBound(int primID, int i) { + return 0; + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + return null; + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + if (r.getMax() == Float.POSITIVE_INFINITY) + state.setIntersection(0); + } + + public void prepareShadingState(ShadingState state) { + if (state.includeLights()) + state.setShader(this); + } + + public Color getRadiance(ShadingState state) { + return getSkyRGB(basis.untransform(state.getRay().getDirection())).constrainRGB(); + } + + public void scatterPhoton(ShadingState state, Color power) { + // let photon escape + } + + private Vector3 getDirection(float u, float v) { + Vector3 dest = new Vector3(); + double phi = 0, theta = 0; + theta = u * 2 * Math.PI; + phi = v * Math.PI; + double sin_phi = Math.sin(phi); + dest.x = (float) (-sin_phi * Math.cos(theta)); + dest.y = (float) Math.cos(phi); + dest.z = (float) (sin_phi * Math.sin(theta)); + return dest; + } + + public Instance createInstance() { + return Instance.createTemporary(this, null, this); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/light/TriangleMeshLight.java b/src/main/java/org/sunflow/core/light/TriangleMeshLight.java new file mode 100644 index 0000000..3af8071 --- /dev/null +++ b/src/main/java/org/sunflow/core/light/TriangleMeshLight.java @@ -0,0 +1,272 @@ +package org.sunflow.core.light; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.LightSample; +import org.sunflow.core.LightSource; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.core.primitive.TriangleMesh; +import org.sunflow.image.Color; +import org.sunflow.math.MathUtils; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class TriangleMeshLight extends TriangleMesh implements Shader, LightSource { + private Color radiance; + private int numSamples; + private float[] areas; + private float totalArea; + private Vector3[] ngs; + + public TriangleMeshLight() { + radiance = Color.WHITE; + numSamples = 4; + } + + @Override + public boolean update(ParameterList pl, SunflowAPI api) { + radiance = pl.getColor("radiance", radiance); + numSamples = pl.getInt("samples", numSamples); + if (super.update(pl, api)) { + // precompute triangle areas and normals + areas = new float[getNumPrimitives()]; + ngs = new Vector3[getNumPrimitives()]; + totalArea = 0; + for (int tri3 = 0, i = 0; tri3 < triangles.length; tri3 += 3, i++) { + int a = triangles[tri3 + 0]; + int b = triangles[tri3 + 1]; + int c = triangles[tri3 + 2]; + Point3 v0p = getPoint(a); + Point3 v1p = getPoint(b); + Point3 v2p = getPoint(c); + ngs[i] = Point3.normal(v0p, v1p, v2p); + areas[i] = 0.5f * ngs[i].length(); + ngs[i].normalize(); + totalArea += areas[i]; + } + } else + return false; + return true; + } + + private final boolean intersectTriangleKensler(int tri3, Ray r) { + int a = 3 * triangles[tri3 + 0]; + int b = 3 * triangles[tri3 + 1]; + int c = 3 * triangles[tri3 + 2]; + float edge0x = points[b + 0] - points[a + 0]; + float edge0y = points[b + 1] - points[a + 1]; + float edge0z = points[b + 2] - points[a + 2]; + float edge1x = points[a + 0] - points[c + 0]; + float edge1y = points[a + 1] - points[c + 1]; + float edge1z = points[a + 2] - points[c + 2]; + float nx = edge0y * edge1z - edge0z * edge1y; + float ny = edge0z * edge1x - edge0x * edge1z; + float nz = edge0x * edge1y - edge0y * edge1x; + float v = r.dot(nx, ny, nz); + float iv = 1 / v; + float edge2x = points[a + 0] - r.ox; + float edge2y = points[a + 1] - r.oy; + float edge2z = points[a + 2] - r.oz; + float va = nx * edge2x + ny * edge2y + nz * edge2z; + float t = iv * va; + if (t <= 0) + return false; + float ix = edge2y * r.dz - edge2z * r.dy; + float iy = edge2z * r.dx - edge2x * r.dz; + float iz = edge2x * r.dy - edge2y * r.dx; + float v1 = ix * edge1x + iy * edge1y + iz * edge1z; + float beta = iv * v1; + if (beta < 0) + return false; + float v2 = ix * edge0x + iy * edge0y + iz * edge0z; + if ((v1 + v2) * v > v * v) + return false; + float gamma = iv * v2; + if (gamma < 0) + return false; + // FIXME: arbitrary bias, should handle as in other places + r.setMax(t - 1e-3f); + return true; + } + + public Color getRadiance(ShadingState state) { + if (!state.includeLights()) + return Color.BLACK; + state.faceforward(); + // emit constant radiance + return state.isBehind() ? Color.BLACK : radiance; + } + + public void scatterPhoton(ShadingState state, Color power) { + // do not scatter photons + } + + public Instance createInstance() { + return Instance.createTemporary(this, null, this); + } + + public int getNumSamples() { + return numSamples * getNumPrimitives(); + } + + public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power) { + double rnd = randX1 * totalArea; + int j = areas.length - 1; + for (int i = 0; i < areas.length; i++) { + if (rnd < areas[i]) { + j = i; + break; + } + rnd -= areas[i]; // try next triangle + } + rnd /= areas[j]; + randX1 = rnd; + double s = Math.sqrt(1 - randX2); + float u = (float) (randY2 * s); + float v = (float) (1 - s); + float w = 1 - u - v; + int tri3 = j * 3; + int index0 = 3 * triangles[tri3 + 0]; + int index1 = 3 * triangles[tri3 + 1]; + int index2 = 3 * triangles[tri3 + 2]; + p.x = w * points[index0 + 0] + u * points[index1 + 0] + v * points[index2 + 0]; + p.y = w * points[index0 + 1] + u * points[index1 + 1] + v * points[index2 + 1]; + p.z = w * points[index0 + 2] + u * points[index1 + 2] + v * points[index2 + 2]; + p.x += 0.001f * ngs[j].x; + p.y += 0.001f * ngs[j].y; + p.z += 0.001f * ngs[j].z; + OrthoNormalBasis onb = OrthoNormalBasis.makeFromW(ngs[j]); + u = (float) (2 * Math.PI * randX1); + s = Math.sqrt(randY1); + onb.transform(new Vector3((float) (Math.cos(u) * s), (float) (Math.sin(u) * s), (float) (Math.sqrt(1 - randY1))), dir); + Color.mul((float) Math.PI * areas[j], radiance, power); + } + + public float getPower() { + return radiance.copy().mul((float) Math.PI * totalArea).getLuminance(); + } + + public void getSamples(ShadingState state) { + if (numSamples == 0) + return; + Vector3 n = state.getNormal(); + Point3 p = state.getPoint(); + for (int tri3 = 0, i = 0; tri3 < triangles.length; tri3 += 3, i++) { + // vector towards each vertex of the light source + Vector3 p0 = Point3.sub(getPoint(triangles[tri3 + 0]), p, new Vector3()); + // cull triangle if it is facing the wrong way + if (Vector3.dot(p0, ngs[i]) >= 0) + continue; + Vector3 p1 = Point3.sub(getPoint(triangles[tri3 + 1]), p, new Vector3()); + Vector3 p2 = Point3.sub(getPoint(triangles[tri3 + 2]), p, new Vector3()); + // if all three vertices are below the hemisphere, stop + if (Vector3.dot(p0, n) <= 0 && Vector3.dot(p1, n) <= 0 && Vector3.dot(p2, n) <= 0) + continue; + p0.normalize(); + p1.normalize(); + p2.normalize(); + float dot = Vector3.dot(p2, p0); + Vector3 h = new Vector3(); + h.x = p2.x - dot * p0.x; + h.y = p2.y - dot * p0.y; + h.z = p2.z - dot * p0.z; + float hlen = h.length(); + if (hlen > 1e-6f) + h.div(hlen); + else + continue; + Vector3 n0 = Vector3.cross(p0, p1, new Vector3()); + float len0 = n0.length(); + if (len0 > 1e-6f) + n0.div(len0); + else + continue; + Vector3 n1 = Vector3.cross(p1, p2, new Vector3()); + float len1 = n1.length(); + if (len1 > 1e-6f) + n1.div(len1); + else + continue; + Vector3 n2 = Vector3.cross(p2, p0, new Vector3()); + float len2 = n2.length(); + if (len2 > 1e-6f) + n2.div(len2); + else + continue; + + float cosAlpha = MathUtils.clamp(-Vector3.dot(n2, n0), -1.0f, 1.0f); + float cosBeta = MathUtils.clamp(-Vector3.dot(n0, n1), -1.0f, 1.0f); + float cosGamma = MathUtils.clamp(-Vector3.dot(n1, n2), -1.0f, 1.0f); + + float alpha = (float) Math.acos(cosAlpha); + float beta = (float) Math.acos(cosBeta); + float gamma = (float) Math.acos(cosGamma); + + float area = alpha + beta + gamma - (float) Math.PI; + + float cosC = MathUtils.clamp(Vector3.dot(p0, p1), -1.0f, 1.0f); + float salpha = (float) Math.sin(alpha); + float product = salpha * cosC; + + // use lower sampling depth for diffuse bounces + int samples = state.getDiffuseDepth() > 0 ? 1 : numSamples; + Color c = Color.mul(area / samples, radiance); + for (int j = 0; j < samples; j++) { + // random offset on unit square + double randX = state.getRandom(j, 0, samples); + double randY = state.getRandom(j, 1, samples); + + float phi = (float) randX * area - alpha + (float) Math.PI; + float sinPhi = (float) Math.sin(phi); + float cosPhi = (float) Math.cos(phi); + + float u = cosPhi + cosAlpha; + float v = sinPhi - product; + + float q = (-v + cosAlpha * (cosPhi * -v + sinPhi * u)) / (salpha * (sinPhi * -v - cosPhi * u)); + float q1 = 1.0f - q * q; + if (q1 < 0.0f) + q1 = 0.0f; + + float sqrtq1 = (float) Math.sqrt(q1); + float ncx = q * p0.x + sqrtq1 * h.x; + float ncy = q * p0.y + sqrtq1 * h.y; + float ncz = q * p0.z + sqrtq1 * h.z; + dot = p1.dot(ncx, ncy, ncz); + float z = 1.0f - (float) randY * (1.0f - dot); + float z1 = 1.0f - z * z; + if (z1 < 0.0f) + z1 = 0.0f; + Vector3 nd = new Vector3(); + nd.x = ncx - dot * p1.x; + nd.y = ncy - dot * p1.y; + nd.z = ncz - dot * p1.z; + nd.normalize(); + float sqrtz1 = (float) Math.sqrt(z1); + Vector3 result = new Vector3(); + result.x = z * p1.x + sqrtz1 * nd.x; + result.y = z * p1.y + sqrtz1 * nd.y; + result.z = z * p1.z + sqrtz1 * nd.z; + + // make sure the sample is in the right hemisphere - facing in + // the right direction + if (Vector3.dot(result, n) > 0 && Vector3.dot(result, state.getGeoNormal()) > 0 && Vector3.dot(result, ngs[i]) < 0) { + // compute intersection with triangle (if any) + Ray shadowRay = new Ray(state.getPoint(), result); + if (!intersectTriangleKensler(tri3, shadowRay)) + continue; + LightSample dest = new LightSample(); + dest.setShadowRay(shadowRay); + // prepare sample + dest.setRadiance(c, c); + dest.traceShadow(state); + state.addSample(dest); + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/modifiers/BumpMappingModifier.java b/src/main/java/org/sunflow/core/modifiers/BumpMappingModifier.java new file mode 100644 index 0000000..ef525db --- /dev/null +++ b/src/main/java/org/sunflow/core/modifiers/BumpMappingModifier.java @@ -0,0 +1,33 @@ +package org.sunflow.core.modifiers; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Modifier; +import org.sunflow.core.ParameterList; +import org.sunflow.core.ShadingState; +import org.sunflow.core.Texture; +import org.sunflow.core.TextureCache; +import org.sunflow.math.OrthoNormalBasis; + +public class BumpMappingModifier implements Modifier { + private Texture bumpTexture; + private float scale; + + public BumpMappingModifier() { + bumpTexture = null; + scale = 1; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + String filename = pl.getString("texture", null); + if (filename != null) + bumpTexture = TextureCache.getTexture(api.resolveTextureFilename(filename), true); + scale = pl.getFloat("scale", scale); + return bumpTexture != null; + } + + public void modify(ShadingState state) { + // apply bump + state.getNormal().set(bumpTexture.getBump(state.getUV().x, state.getUV().y, state.getBasis(), scale)); + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/modifiers/NormalMapModifier.java b/src/main/java/org/sunflow/core/modifiers/NormalMapModifier.java new file mode 100644 index 0000000..a297cc3 --- /dev/null +++ b/src/main/java/org/sunflow/core/modifiers/NormalMapModifier.java @@ -0,0 +1,30 @@ +package org.sunflow.core.modifiers; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Modifier; +import org.sunflow.core.ParameterList; +import org.sunflow.core.ShadingState; +import org.sunflow.core.Texture; +import org.sunflow.core.TextureCache; +import org.sunflow.math.OrthoNormalBasis; + +public class NormalMapModifier implements Modifier { + private Texture normalMap; + + public NormalMapModifier() { + normalMap = null; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + String filename = pl.getString("texture", null); + if (filename != null) + normalMap = TextureCache.getTexture(api.resolveTextureFilename(filename), true); + return normalMap != null; + } + + public void modify(ShadingState state) { + // apply normal map + state.getNormal().set(normalMap.getNormal(state.getUV().x, state.getUV().y, state.getBasis())); + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/modifiers/PerlinModifier.java b/src/main/java/org/sunflow/core/modifiers/PerlinModifier.java new file mode 100644 index 0000000..08bfc77 --- /dev/null +++ b/src/main/java/org/sunflow/core/modifiers/PerlinModifier.java @@ -0,0 +1,76 @@ +package org.sunflow.core.modifiers; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Modifier; +import org.sunflow.core.ParameterList; +import org.sunflow.core.ShadingState; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.PerlinScalar; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class PerlinModifier implements Modifier { + private int function = 0; + private float scale = 50; + private float size = 1; + + public boolean update(ParameterList pl, SunflowAPI api) { + function = pl.getInt("function", function); + size = pl.getFloat("size", size); + scale = pl.getFloat("scale", scale); + return true; + } + + public void modify(ShadingState state) { + Point3 p = state.transformWorldToObject(state.getPoint()); + p.x *= size; + p.y *= size; + p.z *= size; + Vector3 normal = state.transformNormalWorldToObject(state.getNormal()); + double f0 = f(p.x, p.y, p.z); + double fx = f(p.x + .0001, p.y, p.z); + double fy = f(p.x, p.y + .0001, p.z); + double fz = f(p.x, p.y, p.z + .0001); + + normal.x -= scale * (fx - f0) / .0001; + normal.y -= scale * (fy - f0) / .0001; + normal.z -= scale * (fz - f0) / .0001; + normal.normalize(); + + state.getNormal().set(state.transformNormalObjectToWorld(normal)); + state.getNormal().normalize(); + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + } + + double f(double x, double y, double z) { + switch (function) { + case 0: + return .03 * noise(x, y, z, 8); + case 1: + return .01 * stripes(x + 2 * turbulence(x, y, z, 1), 1.6); + default: + return -.10 * turbulence(x, y, z, 1); + } + } + + private static final double stripes(double x, double f) { + double t = .5 + .5 * Math.sin(f * 2 * Math.PI * x); + return t * t - .5; + } + + private static final double turbulence(double x, double y, double z, double freq) { + double t = -.5; + for (; freq <= 300 / 12; freq *= 2) + t += Math.abs(noise(x, y, z, freq) / freq); + return t; + } + + private static final double noise(double x, double y, double z, double freq) { + double x1, y1, z1; + x1 = .707 * x - .707 * z; + z1 = .707 * x + .707 * z; + y1 = .707 * x1 + .707 * y; + x1 = .707 * x1 - .707 * y; + return PerlinScalar.snoise((float) (freq * x1 + 100), (float) (freq * y1), (float) (freq * z1)); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/parser/RA2Parser.java b/src/main/java/org/sunflow/core/parser/RA2Parser.java new file mode 100644 index 0000000..f6e5761 --- /dev/null +++ b/src/main/java/org/sunflow/core/parser/RA2Parser.java @@ -0,0 +1,100 @@ +package org.sunflow.core.parser; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; + +import org.sunflow.SunflowAPI; +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.SceneParser; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.Parser; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class RA2Parser implements SceneParser { + public boolean parse(String filename, SunflowAPIInterface api) { + try { + UI.printInfo(Module.USER, "RA2 - Reading geometry: \"%s\" ...", filename); + File file = new File(filename); + FileInputStream stream = new FileInputStream(filename); + MappedByteBuffer map = stream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()); + map.order(ByteOrder.LITTLE_ENDIAN); + FloatBuffer buffer = map.asFloatBuffer(); + float[] data = new float[buffer.capacity()]; + for (int i = 0; i < data.length; i++) + data[i] = buffer.get(i); + stream.close(); + api.parameter("points", "point", "vertex", data); + int[] triangles = new int[3 * (data.length / 9)]; + for (int i = 0; i < triangles.length; i++) + triangles[i] = i; + // create geo + api.parameter("triangles", triangles); + api.geometry(filename, "triangle_mesh"); + // create shader + api.shader(filename + ".shader", "simple"); + // create instance + api.parameter("shaders", filename + ".shader"); + api.instance(filename + ".instance", filename); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return false; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + try { + filename = filename.replace(".ra2", ".txt"); + UI.printInfo(Module.USER, "RA2 - Reading camera : \"%s\" ...", filename); + Parser p = new Parser(filename); + Point3 eye = new Point3(); + eye.x = p.getNextFloat(); + eye.y = p.getNextFloat(); + eye.z = p.getNextFloat(); + Point3 to = new Point3(); + to.x = p.getNextFloat(); + to.y = p.getNextFloat(); + to.z = p.getNextFloat(); + Vector3 up = new Vector3(); + switch (p.getNextInt()) { + case 0: + up.set(1, 0, 0); + break; + case 1: + up.set(0, 1, 0); + break; + case 2: + up.set(0, 0, 1); + break; + default: + UI.printWarning(Module.USER, "RA2 - Invalid up vector specification - using Z axis"); + up.set(0, 0, 1); + break; + } + api.parameter("eye", eye); + api.parameter("target", to); + api.parameter("up", up); + String cameraName = filename + ".camera"; + api.parameter("fov", 80f); + api.camera(cameraName, "pinhole"); + api.parameter("camera", cameraName); + api.parameter("resolutionX", 1024); + api.parameter("resolutionY", 1024); + api.options(SunflowAPI.DEFAULT_OPTIONS); + p.close(); + } catch (FileNotFoundException e) { + UI.printWarning(Module.USER, "RA2 - Camera file not found"); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/parser/RA3Parser.java b/src/main/java/org/sunflow/core/parser/RA3Parser.java new file mode 100644 index 0000000..3d76c1a --- /dev/null +++ b/src/main/java/org/sunflow/core/parser/RA3Parser.java @@ -0,0 +1,61 @@ +package org.sunflow.core.parser; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.SceneParser; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class RA3Parser implements SceneParser { + public boolean parse(String filename, SunflowAPIInterface api) { + try { + UI.printInfo(Module.USER, "RA3 - Reading geometry: \"%s\" ...", filename); + File file = new File(filename); + FileInputStream stream = new FileInputStream(filename); + MappedByteBuffer map = stream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()); + map.order(ByteOrder.LITTLE_ENDIAN); + IntBuffer ints = map.asIntBuffer(); + FloatBuffer buffer = map.asFloatBuffer(); + int numVerts = ints.get(0); + int numTris = ints.get(1); + UI.printInfo(Module.USER, "RA3 - * Reading %d vertices ...", numVerts); + float[] verts = new float[3 * numVerts]; + for (int i = 0; i < verts.length; i++) + verts[i] = buffer.get(2 + i); + UI.printInfo(Module.USER, "RA3 - * Reading %d triangles ...", numTris); + int[] tris = new int[3 * numTris]; + for (int i = 0; i < tris.length; i++) + tris[i] = ints.get(2 + verts.length + i); + stream.close(); + UI.printInfo(Module.USER, "RA3 - * Creating mesh ..."); + + // create geometry + api.parameter("triangles", tris); + api.parameter("points", "point", "vertex", verts); + api.geometry(filename, "triangle_mesh"); + + // create default shader (this will simply error out if the shader + // already exists) + api.shader("ra3shader", "simple"); + // create instance + api.parameter("shaders", "ra3shader"); + api.instance(filename + ".instance", filename); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return false; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/parser/SCAbstractParser.java b/src/main/java/org/sunflow/core/parser/SCAbstractParser.java new file mode 100644 index 0000000..94bb0b2 --- /dev/null +++ b/src/main/java/org/sunflow/core/parser/SCAbstractParser.java @@ -0,0 +1,294 @@ +package org.sunflow.core.parser; + +import java.io.EOFException; +import java.io.IOException; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.SceneParser; +import org.sunflow.core.ParameterList.InterpolationType; +import org.sunflow.image.ColorFactory; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point2; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public abstract class SCAbstractParser implements SceneParser { + public enum Keyword { + RESET, PARAMETER, GEOMETRY, INSTANCE, SHADER, MODIFIER, LIGHT, CAMERA, OPTIONS, INCLUDE, REMOVE, FRAME, PLUGIN, SEARCHPATH, STRING, BOOL, INT, FLOAT, COLOR, POINT, VECTOR, TEXCOORD, MATRIX, STRING_ARRAY, INT_ARRAY, FLOAT_ARRAY, POINT_ARRAY, VECTOR_ARRAY, TEXCOORD_ARRAY, MATRIX_ARRAY, END_OF_FILE, + } + + public boolean parse(String filename, SunflowAPIInterface api) { + Timer timer = new Timer(); + timer.start(); + UI.printInfo(Module.API, "Parsing \"%s\" ...", filename); + try { + openParser(filename); + parseloop: while (true) { + Keyword k = parseKeyword(); + switch (k) { + case RESET: + api.reset(); + break; + case PARAMETER: + parseParameter(api); + break; + case GEOMETRY: { + String name = parseString(); + String type = parseString(); + api.geometry(name, type); + break; + } + case INSTANCE: { + String name = parseString(); + String geoname = parseString(); + api.instance(name, geoname); + break; + } + case SHADER: { + String name = parseString(); + String type = parseString(); + api.shader(name, type); + break; + } + case MODIFIER: { + String name = parseString(); + String type = parseString(); + api.modifier(name, type); + break; + } + case LIGHT: { + String name = parseString(); + String type = parseString(); + api.light(name, type); + break; + } + case CAMERA: { + String name = parseString(); + String type = parseString(); + api.camera(name, type); + break; + } + case OPTIONS: { + api.options(parseString()); + break; + } + case INCLUDE: { + String file = parseString(); + UI.printInfo(Module.API, "Including: \"%s\" ...", file); + api.include(file); + break; + } + case REMOVE: { + api.remove(parseString()); + break; + } + case FRAME: { + api.currentFrame(parseInt()); + break; + } + case PLUGIN: { + String type = parseString(); + String name = parseString(); + String code = parseVerbatimString(); + api.plugin(type, name, code); + break; + } + case SEARCHPATH: { + String type = parseString(); + api.searchpath(type, parseString()); + break; + } + case END_OF_FILE: { + // clean exit + break parseloop; + } + default: { + UI.printWarning(Module.API, "Unexpected token %s", k); + break; + } + } + } + closeParser(); + } catch (Exception e) { + // catch all exceptions + e.printStackTrace(); + UI.printError(Module.API, "%s", e.getMessage()); + return false; + } + timer.end(); + UI.printInfo(Module.API, "Done parsing (took %s)", timer.toString()); + return true; + } + + private void parseParameter(SunflowAPIInterface api) throws IOException { + String name = parseString(); + Keyword k = parseKeyword(); + switch (k) { + case STRING: { + api.parameter(name, parseString()); + break; + } + case BOOL: { + api.parameter(name, parseBoolean()); + break; + } + case INT: { + api.parameter(name, parseInt()); + break; + } + case FLOAT: { + api.parameter(name, parseFloat()); + break; + } + case COLOR: { + String colorspace = parseString(); + int req = ColorFactory.getRequiredDataValues(colorspace); + if (req == -2) + api.parameter(name, colorspace); // call just to generate + // an error + else + api.parameter(name, colorspace, parseFloatArray(req == -1 ? parseInt() : req)); + break; + } + case POINT: { + api.parameter(name, parsePoint()); + break; + } + case VECTOR: { + api.parameter(name, parseVector()); + break; + } + case TEXCOORD: { + api.parameter(name, parseTexcoord()); + break; + } + case MATRIX: { + api.parameter(name, parseMatrix()); + break; + } + case STRING_ARRAY: { + int n = parseInt(); + api.parameter(name, parseStringArray(n)); + break; + } + case INT_ARRAY: { + int n = parseInt(); + api.parameter(name, parseIntArray(n)); + break; + } + case FLOAT_ARRAY: { + String interp = parseInterpolationType().toString(); + int n = parseInt(); + api.parameter(name, "float", interp, parseFloatArray(n)); + break; + } + case POINT_ARRAY: { + String interp = parseInterpolationType().toString(); + int n = parseInt(); + api.parameter(name, "point", interp, parseFloatArray(3 * n)); + break; + } + case VECTOR_ARRAY: { + String interp = parseInterpolationType().toString(); + int n = parseInt(); + api.parameter(name, "vector", interp, parseFloatArray(3 * n)); + break; + } + case TEXCOORD_ARRAY: { + String interp = parseInterpolationType().toString(); + int n = parseInt(); + api.parameter(name, "texcoord", interp, parseFloatArray(2 * n)); + break; + } + case MATRIX_ARRAY: { + String interp = parseInterpolationType().toString(); + int n = parseInt(); + api.parameter(name, "matrix", interp, parseMatrixArray(n)); + break; + } + case END_OF_FILE: + throw new EOFException(); + default: { + UI.printWarning(Module.API, "Unexpected keyword: %s", k); + break; + } + } + } + + private String[] parseStringArray(int size) throws IOException { + String[] data = new String[size]; + for (int i = 0; i < size; i++) + data[i] = parseString(); + return data; + } + + private int[] parseIntArray(int size) throws IOException { + int[] data = new int[size]; + for (int i = 0; i < size; i++) + data[i] = parseInt(); + return data; + } + + protected float[] parseFloatArray(int size) throws IOException { + float[] data = new float[size]; + for (int i = 0; i < size; i++) + data[i] = parseFloat(); + return data; + } + + private float[] parseMatrixArray(int size) throws IOException { + float[] data = new float[16 * size]; + for (int i = 0, offset = 0; i < size; i++, offset += 16) { + // copy the next matrix into a linear array - in row major order + float[] rowdata = parseMatrix().asRowMajor(); + for (int j = 0; j < 16; j++) + data[offset + j] = rowdata[j]; + } + return data; + } + + private Point3 parsePoint() throws IOException { + float x = parseFloat(); + float y = parseFloat(); + float z = parseFloat(); + return new Point3(x, y, z); + } + + private Vector3 parseVector() throws IOException { + float x = parseFloat(); + float y = parseFloat(); + float z = parseFloat(); + return new Vector3(x, y, z); + } + + private Point2 parseTexcoord() throws IOException { + float x = parseFloat(); + float y = parseFloat(); + return new Point2(x, y); + } + + protected abstract InterpolationType parseInterpolationType() throws IOException; + + // abstract methods - to be implemented by subclasses + + protected abstract void openParser(String filename) throws IOException; + + protected abstract void closeParser() throws IOException; + + protected abstract Keyword parseKeyword() throws IOException; + + protected abstract boolean parseBoolean() throws IOException; + + protected abstract int parseInt() throws IOException; + + protected abstract float parseFloat() throws IOException; + + protected abstract String parseString() throws IOException; + + protected abstract String parseVerbatimString() throws IOException; + + protected abstract Matrix4 parseMatrix() throws IOException; +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/parser/SCAsciiParser.java b/src/main/java/org/sunflow/core/parser/SCAsciiParser.java new file mode 100644 index 0000000..763f074 --- /dev/null +++ b/src/main/java/org/sunflow/core/parser/SCAsciiParser.java @@ -0,0 +1,213 @@ +package org.sunflow.core.parser; + +import java.io.IOException; + +import org.sunflow.core.ParameterList.InterpolationType; +import org.sunflow.image.Color; +import org.sunflow.math.Matrix4; +import org.sunflow.system.Parser; +import org.sunflow.system.UI; +import org.sunflow.system.Parser.ParserException; +import org.sunflow.system.UI.Module; + +public class SCAsciiParser extends SCAbstractParser { + private Parser p; + + protected Color parseColor() throws IOException { + String space = p.getNextToken(); + Color c = null; + if (space.equals("sRGB nonlinear")) { + float r = p.getNextFloat(); + float g = p.getNextFloat(); + float b = p.getNextFloat(); + c = new Color(r, g, b); + c.toLinear(); + } else if (space.equals("sRGB linear")) { + float r = p.getNextFloat(); + float g = p.getNextFloat(); + float b = p.getNextFloat(); + c = new Color(r, g, b); + } else + UI.printWarning(Module.API, "Unrecognized color space: %s", space); + return c; + } + + @Override + protected Matrix4 parseMatrix() throws IOException { + if (p.peekNextToken("row")) { + return new Matrix4(parseFloatArray(16), true); + } else if (p.peekNextToken("col")) { + return new Matrix4(parseFloatArray(16), false); + } else { + Matrix4 m = Matrix4.IDENTITY; + try { + p.checkNextToken("{"); + } catch (ParserException e) { + throw new IOException(e.getMessage()); + } + while (!p.peekNextToken("}")) { + Matrix4 t = null; + if (p.peekNextToken("translate")) { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + t = Matrix4.translation(x, y, z); + } else if (p.peekNextToken("scaleu")) { + float s = p.getNextFloat(); + t = Matrix4.scale(s); + } else if (p.peekNextToken("scale")) { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + t = Matrix4.scale(x, y, z); + } else if (p.peekNextToken("rotatex")) { + float angle = p.getNextFloat(); + t = Matrix4.rotateX((float) Math.toRadians(angle)); + } else if (p.peekNextToken("rotatey")) { + float angle = p.getNextFloat(); + t = Matrix4.rotateY((float) Math.toRadians(angle)); + } else if (p.peekNextToken("rotatez")) { + float angle = p.getNextFloat(); + t = Matrix4.rotateZ((float) Math.toRadians(angle)); + } else if (p.peekNextToken("rotate")) { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + float angle = p.getNextFloat(); + t = Matrix4.rotate(x, y, z, (float) Math.toRadians(angle)); + } else + UI.printWarning(Module.API, "Unrecognized transformation type: %s", p.getNextToken()); + if (t != null) + m = t.multiply(m); + } + return m; + } + } + + @Override + protected void closeParser() throws IOException { + p.close(); + } + + @Override + protected void openParser(String filename) throws IOException { + p = new Parser(filename); + } + + @Override + protected boolean parseBoolean() throws IOException { + return Boolean.parseBoolean(parseString()); + } + + @Override + protected float parseFloat() throws IOException { + return p.getNextFloat(); + } + + @Override + protected int parseInt() throws IOException { + return p.getNextInt(); + } + + @Override + protected String parseString() throws IOException { + return p.getNextToken(); + } + + @Override + protected String parseVerbatimString() throws IOException { + try { + return p.getNextCodeBlock(); + } catch (ParserException e) { + throw new IOException(e.getMessage()); + } + } + + @Override + protected InterpolationType parseInterpolationType() throws IOException { + if (p.peekNextToken("none")) + return InterpolationType.NONE; + else if (p.peekNextToken("vertex")) + return InterpolationType.VERTEX; + else if (p.peekNextToken("face")) + return InterpolationType.FACE; + else if (p.peekNextToken("facevarying")) + return InterpolationType.FACEVARYING; + return InterpolationType.NONE; + } + + @Override + protected Keyword parseKeyword() throws IOException { + String keyword = p.getNextToken(); + if (keyword == null) + return Keyword.END_OF_FILE; + if (anyEqual(keyword, "reset")) + return Keyword.RESET; + if (anyEqual(keyword, "parameter", "param", "p")) + return Keyword.PARAMETER; + if (anyEqual(keyword, "geometry", "geom", "g")) + return Keyword.GEOMETRY; + if (anyEqual(keyword, "instance", "inst", "i")) + return Keyword.INSTANCE; + if (anyEqual(keyword, "shader", "shd", "s")) + return Keyword.SHADER; + if (anyEqual(keyword, "modifier", "mod", "m")) + return Keyword.MODIFIER; + if (anyEqual(keyword, "light", "l")) + return Keyword.LIGHT; + if (anyEqual(keyword, "camera", "cam", "c")) + return Keyword.CAMERA; + if (anyEqual(keyword, "options", "opt", "o")) + return Keyword.OPTIONS; + if (anyEqual(keyword, "include", "inc")) + return Keyword.INCLUDE; + if (anyEqual(keyword, "remove")) + return Keyword.REMOVE; + if (anyEqual(keyword, "frame")) + return Keyword.FRAME; + if (anyEqual(keyword, "plugin", "plug")) + return Keyword.PLUGIN; + if (anyEqual(keyword, "searchpath")) + return Keyword.SEARCHPATH; + if (anyEqual(keyword, "string", "str")) + return Keyword.STRING; + if (anyEqual(keyword, "string[]", "str[]")) + return Keyword.STRING_ARRAY; + if (anyEqual(keyword, "boolean", "bool")) + return Keyword.BOOL; + if (anyEqual(keyword, "integer", "int")) + return Keyword.INT; + if (anyEqual(keyword, "integer[]", "int[]")) + return Keyword.INT_ARRAY; + if (anyEqual(keyword, "float", "flt")) + return Keyword.FLOAT; + if (anyEqual(keyword, "float[]", "flt[]")) + return Keyword.FLOAT_ARRAY; + if (anyEqual(keyword, "color", "col")) + return Keyword.COLOR; + if (anyEqual(keyword, "point", "pnt")) + return Keyword.POINT; + if (anyEqual(keyword, "point[]", "pnt[]")) + return Keyword.POINT_ARRAY; + if (anyEqual(keyword, "vector", "vec")) + return Keyword.VECTOR; + if (anyEqual(keyword, "vector[]", "vec[]")) + return Keyword.VECTOR_ARRAY; + if (anyEqual(keyword, "texcoord", "tex")) + return Keyword.TEXCOORD; + if (anyEqual(keyword, "texcoord[]", "tex[]")) + return Keyword.TEXCOORD_ARRAY; + if (anyEqual(keyword, "matrix", "mat")) + return Keyword.MATRIX; + if (anyEqual(keyword, "matrix[]", "mat[]")) + return Keyword.MATRIX_ARRAY; + return null; + } + + private boolean anyEqual(String source, String... values) { + for (String v : values) + if (source.equals(v)) + return true; + return false; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/parser/SCBinaryParser.java b/src/main/java/org/sunflow/core/parser/SCBinaryParser.java new file mode 100644 index 0000000..09eab28 --- /dev/null +++ b/src/main/java/org/sunflow/core/parser/SCBinaryParser.java @@ -0,0 +1,154 @@ +package org.sunflow.core.parser; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.FileInputStream; +import java.io.IOException; + +import org.sunflow.core.ParameterList.InterpolationType; +import org.sunflow.math.Matrix4; + +public class SCBinaryParser extends SCAbstractParser { + private DataInputStream stream; + + @Override + protected void closeParser() throws IOException { + stream.close(); + } + + @Override + protected void openParser(String filename) throws IOException { + stream = new DataInputStream(new BufferedInputStream(new FileInputStream(filename))); + } + + @Override + protected boolean parseBoolean() throws IOException { + return stream.readUnsignedByte() != 0; + } + + @Override + protected float parseFloat() throws IOException { + return Float.intBitsToFloat(parseInt()); + } + + @Override + protected int parseInt() throws IOException { + // note that we use readUnsignedByte(), not read() to get EOF exceptions + return stream.readUnsignedByte() | (stream.readUnsignedByte() << 8) | (stream.readUnsignedByte() << 16) | (stream.readUnsignedByte() << 24); + } + + @Override + protected Matrix4 parseMatrix() throws IOException { + return new Matrix4(parseFloatArray(16), true); + } + + @Override + protected String parseString() throws IOException { + byte[] b = new byte[parseInt()]; + stream.read(b); + return new String(b, "UTF-8"); + } + + @Override + protected String parseVerbatimString() throws IOException { + return parseString(); + } + + @Override + protected InterpolationType parseInterpolationType() throws IOException { + int c; + switch (c = stream.readUnsignedByte()) { + case 'n': + return InterpolationType.NONE; + case 'v': + return InterpolationType.VERTEX; + case 'f': + return InterpolationType.FACEVARYING; + case 'p': + return InterpolationType.FACE; + default: + throw new IOException(String.format("Unknown byte found for interpolation type %c", (char) c)); + } + } + + @Override + protected Keyword parseKeyword() throws IOException { + int code = stream.read(); // read a single byte - allow for EOF (<0) + switch (code) { + case 'p': + return Keyword.PARAMETER; + case 'g': + return Keyword.GEOMETRY; + case 'i': + return Keyword.INSTANCE; + case 's': + return Keyword.SHADER; + case 'm': + return Keyword.MODIFIER; + case 'l': + return Keyword.LIGHT; + case 'c': + return Keyword.CAMERA; + case 'o': + return Keyword.OPTIONS; + case 'x': { + // extended keywords (less frequent) + // note we don't use stream.read() here because we should throw + // an exception if the end of the file is reached + switch (code = stream.readUnsignedByte()) { + case 'R': + return Keyword.RESET; + case 'i': + return Keyword.INCLUDE; + case 'r': + return Keyword.REMOVE; + case 'f': + return Keyword.FRAME; + case 'p': + return Keyword.PLUGIN; + case 's': + return Keyword.SEARCHPATH; + default: + throw new IOException(String.format("Unknown extended keyword code: %c", (char) code)); + } + } + case 't': { + // data types + // note we don't use stream.read() here because we should throw + // an exception if the end of the file is reached + int type = stream.readUnsignedByte(); + // note that while not all types can be arrays at the moment, we + // always parse this boolean flag to keep the syntax consistent + // and allow for future improvements + boolean isArray = parseBoolean(); + switch (type) { + case 's': + return isArray ? Keyword.STRING_ARRAY : Keyword.STRING; + case 'b': + return Keyword.BOOL; + case 'i': + return isArray ? Keyword.INT_ARRAY : Keyword.INT; + case 'f': + return isArray ? Keyword.FLOAT_ARRAY : Keyword.FLOAT; + case 'c': + return Keyword.COLOR; + case 'p': + return isArray ? Keyword.POINT_ARRAY : Keyword.POINT; + case 'v': + return isArray ? Keyword.VECTOR_ARRAY : Keyword.VECTOR; + case 't': + return isArray ? Keyword.TEXCOORD_ARRAY : Keyword.TEXCOORD; + case 'm': + return isArray ? Keyword.MATRIX_ARRAY : Keyword.MATRIX; + default: + throw new IOException(String.format("Unknown datatype keyword code: %c", (char) type)); + } + } + default: + if (code < 0) + return Keyword.END_OF_FILE; // normal end of file reached + else + throw new IOException(String.format("Unknown keyword code: %c", (char) code)); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/parser/SCParser.java b/src/main/java/org/sunflow/core/parser/SCParser.java new file mode 100644 index 0000000..eee7286 --- /dev/null +++ b/src/main/java/org/sunflow/core/parser/SCParser.java @@ -0,0 +1,1284 @@ +package org.sunflow.core.parser; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.HashMap; + +import org.sunflow.PluginRegistry; +import org.sunflow.SunflowAPI; +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.SceneParser; +import org.sunflow.image.Color; +import org.sunflow.image.ColorFactory; +import org.sunflow.image.ColorFactory.ColorSpecificationException; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.Parser; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.Parser.ParserException; +import org.sunflow.system.UI.Module; + +/** + * This class provides a static method for loading files in the Sunflow scene + * file format. + */ +public class SCParser implements SceneParser { + private static int instanceCounter = 0; + private int instanceNumber; + private Parser p; + private int numLightSamples; + // used to generate unique names inside this parser + private HashMap objectNames; + + public SCParser() { + objectNames = new HashMap(); + instanceCounter++; + instanceNumber = instanceCounter; + } + + private String generateUniqueName(String prefix) { + // generate a unique name for this class: + int index = 1; + Integer value = objectNames.get(prefix); + if (value != null) { + index = value; + objectNames.put(prefix, index + 1); + } else { + objectNames.put(prefix, index + 1); + } + return String.format("@sc_%d::%s_%d", instanceNumber, prefix, index); + } + + public boolean parse(String filename, SunflowAPIInterface api) { + String localDir = new File(filename).getAbsoluteFile().getParentFile().getAbsolutePath(); + numLightSamples = 1; + Timer timer = new Timer(); + timer.start(); + UI.printInfo(Module.API, "Parsing \"%s\" ...", filename); + try { + p = new Parser(filename); + while (true) { + String token = p.getNextToken(); + if (token == null) + break; + if (token.equals("image")) { + UI.printInfo(Module.API, "Reading image settings ..."); + parseImageBlock(api); + } else if (token.equals("background")) { + UI.printInfo(Module.API, "Reading background ..."); + parseBackgroundBlock(api); + } else if (token.equals("accel")) { + UI.printInfo(Module.API, "Reading accelerator type ..."); + p.getNextToken(); + UI.printWarning(Module.API, "Setting accelerator type is not recommended - ignoring"); + } else if (token.equals("filter")) { + UI.printInfo(Module.API, "Reading image filter type ..."); + parseFilter(api); + } else if (token.equals("bucket")) { + UI.printInfo(Module.API, "Reading bucket settings ..."); + api.parameter("bucket.size", p.getNextInt()); + api.parameter("bucket.order", p.getNextToken()); + api.options(SunflowAPI.DEFAULT_OPTIONS); + } else if (token.equals("photons")) { + UI.printInfo(Module.API, "Reading photon settings ..."); + parsePhotonBlock(api); + } else if (token.equals("gi")) { + UI.printInfo(Module.API, "Reading global illumination settings ..."); + parseGIBlock(api); + } else if (token.equals("lightserver")) { + UI.printInfo(Module.API, "Reading light server settings ..."); + parseLightserverBlock(api); + } else if (token.equals("trace-depths")) { + UI.printInfo(Module.API, "Reading trace depths ..."); + parseTraceBlock(api); + } else if (token.equals("camera")) { + parseCamera(api); + } else if (token.equals("shader")) { + if (!parseShader(api)) + return false; + } else if (token.equals("modifier")) { + if (!parseModifier(api)) + return false; + } else if (token.equals("override")) { + api.parameter("override.shader", p.getNextToken()); + api.parameter("override.photons", p.getNextBoolean()); + api.options(SunflowAPI.DEFAULT_OPTIONS); + } else if (token.equals("object")) { + parseObjectBlock(api); + } else if (token.equals("instance")) { + parseInstanceBlock(api); + } else if (token.equals("light")) { + parseLightBlock(api); + } else if (token.equals("texturepath")) { + String path = p.getNextToken(); + if (!new File(path).isAbsolute()) + path = localDir + File.separator + path; + api.searchpath("texture", path); + } else if (token.equals("includepath")) { + String path = p.getNextToken(); + if (!new File(path).isAbsolute()) + path = localDir + File.separator + path; + api.searchpath("include", path); + } else if (token.equals("include")) { + String file = p.getNextToken(); + UI.printInfo(Module.API, "Including: \"%s\" ...", file); + api.include(file); + } else + UI.printWarning(Module.API, "Unrecognized token %s", token); + } + p.close(); + } catch (ParserException e) { + UI.printError(Module.API, "%s", e.getMessage()); + e.printStackTrace(); + return false; + } catch (FileNotFoundException e) { + UI.printError(Module.API, "%s", e.getMessage()); + return false; + } catch (IOException e) { + UI.printError(Module.API, "%s", e.getMessage()); + return false; + } catch (ColorSpecificationException e) { + UI.printError(Module.API, "%s", e.getMessage()); + return false; + } + timer.end(); + UI.printInfo(Module.API, "Done parsing."); + UI.printInfo(Module.API, "Parsing time: %s", timer.toString()); + return true; + } + + private void parseImageBlock(SunflowAPIInterface api) throws IOException, ParserException { + p.checkNextToken("{"); + if (p.peekNextToken("resolution")) { + api.parameter("resolutionX", p.getNextInt()); + api.parameter("resolutionY", p.getNextInt()); + } + if (p.peekNextToken("sampler")) + api.parameter("sampler", p.getNextToken()); + if (p.peekNextToken("aa")) { + api.parameter("aa.min", p.getNextInt()); + api.parameter("aa.max", p.getNextInt()); + } + if (p.peekNextToken("samples")) + api.parameter("aa.samples", p.getNextInt()); + if (p.peekNextToken("contrast")) + api.parameter("aa.contrast", p.getNextFloat()); + if (p.peekNextToken("filter")) + api.parameter("filter", p.getNextToken()); + if (p.peekNextToken("jitter")) + api.parameter("aa.jitter", p.getNextBoolean()); + if (p.peekNextToken("show-aa")) { + UI.printWarning(Module.API, "Deprecated: show-aa ignored"); + p.getNextBoolean(); + } + if (p.peekNextToken("cache")) + api.parameter("aa.cache", p.getNextBoolean()); + if (p.peekNextToken("output")) { + UI.printWarning(Module.API, "Deprecated: output statement ignored"); + p.getNextToken(); + } + api.options(SunflowAPI.DEFAULT_OPTIONS); + p.checkNextToken("}"); + } + + private void parseBackgroundBlock(SunflowAPIInterface api) throws IOException, ParserException, ColorSpecificationException { + p.checkNextToken("{"); + p.checkNextToken("color"); + api.parameter("color", null, parseColor().getRGB()); + api.shader("background.shader", "constant"); + api.geometry("background", "background"); + api.parameter("shaders", "background.shader"); + api.instance("background.instance", "background"); + p.checkNextToken("}"); + } + + private void parseFilter(SunflowAPIInterface api) throws IOException, ParserException { + UI.printWarning(Module.API, "Deprecated keyword \"filter\" - set this option in the image block"); + String name = p.getNextToken(); + api.parameter("filter", name); + api.options(SunflowAPI.DEFAULT_OPTIONS); + boolean hasSizeParams = name.equals("box") || name.equals("gaussian") || name.equals("blackman-harris") || name.equals("sinc") || name.equals("triangle"); + if (hasSizeParams) { + p.getNextFloat(); + p.getNextFloat(); + } + } + + private void parsePhotonBlock(SunflowAPIInterface api) throws ParserException, IOException { + int numEmit = 0; + boolean globalEmit = false; + p.checkNextToken("{"); + if (p.peekNextToken("emit")) { + UI.printWarning(Module.API, "Shared photon emit values are deprectated - specify number of photons to emit per map"); + numEmit = p.getNextInt(); + globalEmit = true; + } + if (p.peekNextToken("global")) { + UI.printWarning(Module.API, "Global photon map setting belonds inside the gi block - ignoring"); + if (!globalEmit) + p.getNextInt(); + p.getNextToken(); + p.getNextInt(); + p.getNextFloat(); + } + p.checkNextToken("caustics"); + if (!globalEmit) + numEmit = p.getNextInt(); + api.parameter("caustics.emit", numEmit); + api.parameter("caustics", p.getNextToken()); + api.parameter("caustics.gather", p.getNextInt()); + api.parameter("caustics.radius", p.getNextFloat()); + api.options(SunflowAPI.DEFAULT_OPTIONS); + p.checkNextToken("}"); + } + + private void parseGIBlock(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { + p.checkNextToken("{"); + p.checkNextToken("type"); + if (p.peekNextToken("irr-cache")) { + api.parameter("gi.engine", "irr-cache"); + p.checkNextToken("samples"); + api.parameter("gi.irr-cache.samples", p.getNextInt()); + p.checkNextToken("tolerance"); + api.parameter("gi.irr-cache.tolerance", p.getNextFloat()); + p.checkNextToken("spacing"); + api.parameter("gi.irr-cache.min_spacing", p.getNextFloat()); + api.parameter("gi.irr-cache.max_spacing", p.getNextFloat()); + // parse global photon map info + if (p.peekNextToken("global")) { + api.parameter("gi.irr-cache.gmap.emit", p.getNextInt()); + api.parameter("gi.irr-cache.gmap", p.getNextToken()); + api.parameter("gi.irr-cache.gmap.gather", p.getNextInt()); + api.parameter("gi.irr-cache.gmap.radius", p.getNextFloat()); + } + } else if (p.peekNextToken("path")) { + api.parameter("gi.engine", "path"); + p.checkNextToken("samples"); + api.parameter("gi.path.samples", p.getNextInt()); + if (p.peekNextToken("bounces")) { + UI.printWarning(Module.API, "Deprecated setting: bounces - use diffuse trace depth instead"); + p.getNextInt(); + } + } else if (p.peekNextToken("fake")) { + api.parameter("gi.engine", "fake"); + p.checkNextToken("up"); + api.parameter("gi.fake.up", parseVector()); + p.checkNextToken("sky"); + api.parameter("gi.fake.sky", null, parseColor().getRGB()); + p.checkNextToken("ground"); + api.parameter("gi.fake.ground", null, parseColor().getRGB()); + } else if (p.peekNextToken("igi")) { + api.parameter("gi.engine", "igi"); + p.checkNextToken("samples"); + api.parameter("gi.igi.samples", p.getNextInt()); + p.checkNextToken("sets"); + api.parameter("gi.igi.sets", p.getNextInt()); + if (!p.peekNextToken("b")) + p.checkNextToken("c"); + api.parameter("gi.igi.c", p.getNextFloat()); + p.checkNextToken("bias-samples"); + api.parameter("gi.igi.bias_samples", p.getNextInt()); + } else if (p.peekNextToken("ambocc")) { + api.parameter("gi.engine", "ambocc"); + p.checkNextToken("bright"); + api.parameter("gi.ambocc.bright", null, parseColor().getRGB()); + p.checkNextToken("dark"); + api.parameter("gi.ambocc.dark", null, parseColor().getRGB()); + p.checkNextToken("samples"); + api.parameter("gi.ambocc.samples", p.getNextInt()); + if (p.peekNextToken("maxdist")) + api.parameter("gi.ambocc.maxdist", p.getNextFloat()); + } else if (p.peekNextToken("none") || p.peekNextToken("null")) { + // disable GI + api.parameter("gi.engine", "none"); + } else + UI.printWarning(Module.API, "Unrecognized gi engine type \"%s\" - ignoring", p.getNextToken()); + api.options(SunflowAPI.DEFAULT_OPTIONS); + p.checkNextToken("}"); + } + + private void parseLightserverBlock(SunflowAPIInterface api) throws ParserException, IOException { + p.checkNextToken("{"); + if (p.peekNextToken("shadows")) { + UI.printWarning(Module.API, "Deprecated: shadows setting ignored"); + p.getNextBoolean(); + } + if (p.peekNextToken("direct-samples")) { + UI.printWarning(Module.API, "Deprecated: use samples keyword in area light definitions"); + numLightSamples = p.getNextInt(); + } + if (p.peekNextToken("glossy-samples")) { + UI.printWarning(Module.API, "Deprecated: use samples keyword in glossy shader definitions"); + p.getNextInt(); + } + if (p.peekNextToken("max-depth")) { + UI.printWarning(Module.API, "Deprecated: max-depth setting - use trace-depths block instead"); + int d = p.getNextInt(); + api.parameter("depths.diffuse", 1); + api.parameter("depths.reflection", d - 1); + api.parameter("depths.refraction", 0); + api.options(SunflowAPI.DEFAULT_OPTIONS); + } + if (p.peekNextToken("global")) { + UI.printWarning(Module.API, "Deprecated: global settings ignored - use photons block instead"); + p.getNextBoolean(); + p.getNextInt(); + p.getNextInt(); + p.getNextInt(); + p.getNextFloat(); + } + if (p.peekNextToken("caustics")) { + UI.printWarning(Module.API, "Deprecated: caustics settings ignored - use photons block instead"); + p.getNextBoolean(); + p.getNextInt(); + p.getNextFloat(); + p.getNextInt(); + p.getNextFloat(); + } + if (p.peekNextToken("irr-cache")) { + UI.printWarning(Module.API, "Deprecated: irradiance cache settings ignored - use gi block instead"); + p.getNextInt(); + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + } + p.checkNextToken("}"); + } + + private void parseTraceBlock(SunflowAPIInterface api) throws ParserException, IOException { + p.checkNextToken("{"); + if (p.peekNextToken("diff")) + api.parameter("depths.diffuse", p.getNextInt()); + if (p.peekNextToken("refl")) + api.parameter("depths.reflection", p.getNextInt()); + if (p.peekNextToken("refr")) + api.parameter("depths.refraction", p.getNextInt()); + p.checkNextToken("}"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + } + + private void parseCamera(SunflowAPIInterface api) throws ParserException, IOException { + p.checkNextToken("{"); + p.checkNextToken("type"); + String type = p.getNextToken(); + UI.printInfo(Module.API, "Reading %s camera ...", type); + if (p.peekNextToken("shutter")) { + api.parameter("shutter.open", p.getNextFloat()); + api.parameter("shutter.close", p.getNextFloat()); + } + parseCameraTransform(api); + String name = generateUniqueName("camera"); + if (type.equals("pinhole")) { + p.checkNextToken("fov"); + api.parameter("fov", p.getNextFloat()); + p.checkNextToken("aspect"); + api.parameter("aspect", p.getNextFloat()); + if (p.peekNextToken("shift")) { + api.parameter("shift.x", p.getNextFloat()); + api.parameter("shift.y", p.getNextFloat()); + } + api.camera(name, "pinhole"); + } else if (type.equals("thinlens")) { + p.checkNextToken("fov"); + api.parameter("fov", p.getNextFloat()); + p.checkNextToken("aspect"); + api.parameter("aspect", p.getNextFloat()); + if (p.peekNextToken("shift")) { + api.parameter("shift.x", p.getNextFloat()); + api.parameter("shift.y", p.getNextFloat()); + } + p.checkNextToken("fdist"); + api.parameter("focus.distance", p.getNextFloat()); + p.checkNextToken("lensr"); + api.parameter("lens.radius", p.getNextFloat()); + if (p.peekNextToken("sides")) + api.parameter("lens.sides", p.getNextInt()); + if (p.peekNextToken("rotation")) + api.parameter("lens.rotation", p.getNextFloat()); + api.camera(name, "thinlens"); + } else if (type.equals("spherical")) { + // no extra arguments + api.camera(name, "spherical"); + } else if (type.equals("fisheye")) { + // no extra arguments + api.camera(name, "fisheye"); + } else { + UI.printWarning(Module.API, "Unrecognized camera type: %s", p.getNextToken()); + p.checkNextToken("}"); + return; + } + p.checkNextToken("}"); + if (name != null) { + api.parameter("camera", name); + api.options(SunflowAPI.DEFAULT_OPTIONS); + } + } + + private void parseCameraTransform(SunflowAPIInterface api) throws ParserException, IOException { + if (p.peekNextToken("steps")) { + // motion blur camera + int n = p.getNextInt(); + api.parameter("transform.steps", n); + // parse time extents + p.checkNextToken("times"); + float t0 = p.getNextFloat(); + float t1 = p.getNextFloat(); + api.parameter("transform.times", "float", "none", new float[] { t0, + t1 }); + for (int i = 0; i < n; i++) + parseCameraMatrix(i, api); + } else + parseCameraMatrix(-1, api); + } + + private void parseCameraMatrix(int index, SunflowAPIInterface api) throws IOException, ParserException { + String offset = index < 0 ? "" : String.format("[%d]", index); + if (p.peekNextToken("transform")) { + // advanced camera + api.parameter(String.format("transform%s", offset), parseMatrix()); + } else { + if (index >= 0) + p.checkNextToken("{"); + // regular camera specification + p.checkNextToken("eye"); + Point3 eye = parsePoint(); + p.checkNextToken("target"); + Point3 target = parsePoint(); + p.checkNextToken("up"); + Vector3 up = parseVector(); + api.parameter(String.format("transform%s", offset), Matrix4.lookAt(eye, target, up)); + if (index >= 0) + p.checkNextToken("}"); + } + } + + private boolean parseShader(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { + p.checkNextToken("{"); + p.checkNextToken("name"); + String name = p.getNextToken(); + UI.printInfo(Module.API, "Reading shader: %s ...", name); + p.checkNextToken("type"); + if (p.peekNextToken("diffuse")) { + if (p.peekNextToken("diff")) { + api.parameter("diffuse", null, parseColor().getRGB()); + api.shader(name, "diffuse"); + } else if (p.peekNextToken("texture")) { + api.parameter("texture", p.getNextToken()); + api.shader(name, "textured_diffuse"); + } else + UI.printWarning(Module.API, "Unrecognized option in diffuse shader block: %s", p.getNextToken()); + } else if (p.peekNextToken("phong")) { + String tex = null; + if (p.peekNextToken("texture")) + api.parameter("texture", tex = p.getNextToken()); + else { + p.checkNextToken("diff"); + api.parameter("diffuse", null, parseColor().getRGB()); + } + p.checkNextToken("spec"); + api.parameter("specular", null, parseColor().getRGB()); + api.parameter("power", p.getNextFloat()); + if (p.peekNextToken("samples")) + api.parameter("samples", p.getNextInt()); + if (tex != null) + api.shader(name, "textured_phong"); + else + api.shader(name, "phong"); + } else if (p.peekNextToken("amb-occ") || p.peekNextToken("amb-occ2")) { + String tex = null; + if (p.peekNextToken("diff") || p.peekNextToken("bright")) + api.parameter("bright", null, parseColor().getRGB()); + else if (p.peekNextToken("texture")) + api.parameter("texture", tex = p.getNextToken()); + if (p.peekNextToken("dark")) { + api.parameter("dark", null, parseColor().getRGB()); + p.checkNextToken("samples"); + api.parameter("samples", p.getNextInt()); + p.checkNextToken("dist"); + api.parameter("maxdist", p.getNextFloat()); + } + if (tex == null) + api.shader(name, "ambient_occlusion"); + else + api.shader(name, "textured_ambient_occlusion"); + } else if (p.peekNextToken("mirror")) { + p.checkNextToken("refl"); + api.parameter("color", null, parseColor().getRGB()); + api.shader(name, "mirror"); + } else if (p.peekNextToken("glass")) { + p.checkNextToken("eta"); + api.parameter("eta", p.getNextFloat()); + p.checkNextToken("color"); + api.parameter("color", null, parseColor().getRGB()); + if (p.peekNextToken("absorption.distance") || p.peekNextToken("absorbtion.distance")) + api.parameter("absorption.distance", p.getNextFloat()); + if (p.peekNextToken("absorption.color") || p.peekNextToken("absorbtion.color")) + api.parameter("absorption.color", null, parseColor().getRGB()); + api.shader(name, "glass"); + } else if (p.peekNextToken("shiny")) { + String tex = null; + if (p.peekNextToken("texture")) + api.parameter("texture", tex = p.getNextToken()); + else { + p.checkNextToken("diff"); + api.parameter("diffuse", null, parseColor().getRGB()); + } + p.checkNextToken("refl"); + api.parameter("shiny", p.getNextFloat()); + if (tex == null) + api.shader(name, "shiny_diffuse"); + else + api.shader(name, "textured_shiny_diffuse"); + } else if (p.peekNextToken("ward")) { + String tex = null; + if (p.peekNextToken("texture")) + api.parameter("texture", tex = p.getNextToken()); + else { + p.checkNextToken("diff"); + api.parameter("diffuse", null, parseColor().getRGB()); + } + p.checkNextToken("spec"); + api.parameter("specular", null, parseColor().getRGB()); + p.checkNextToken("rough"); + api.parameter("roughnessX", p.getNextFloat()); + api.parameter("roughnessY", p.getNextFloat()); + if (p.peekNextToken("samples")) + api.parameter("samples", p.getNextInt()); + if (tex != null) + api.shader(name, "textured_ward"); + else + api.shader(name, "ward"); + } else if (p.peekNextToken("view-caustics")) { + api.shader(name, "view_caustics"); + } else if (p.peekNextToken("view-irradiance")) { + api.shader(name, "view_irradiance"); + } else if (p.peekNextToken("view-global")) { + api.shader(name, "view_global"); + } else if (p.peekNextToken("constant")) { + // backwards compatibility -- peek only + p.peekNextToken("color"); + api.parameter("color", null, parseColor().getRGB()); + api.shader(name, "constant"); + } else if (p.peekNextToken("janino")) { + String typename = p.peekNextToken("typename") ? p.getNextToken() : PluginRegistry.shaderPlugins.generateUniqueName("janino_shader"); + if (!PluginRegistry.shaderPlugins.registerPlugin(typename, p.getNextCodeBlock())) + return false; + api.shader(name, typename); + } else if (p.peekNextToken("id")) { + api.shader(name, "show_instance_id"); + } else if (p.peekNextToken("uber")) { + if (p.peekNextToken("diff")) + api.parameter("diffuse", null, parseColor().getRGB()); + if (p.peekNextToken("diff.texture")) + api.parameter("diffuse.texture", p.getNextToken()); + if (p.peekNextToken("diff.blend")) + api.parameter("diffuse.blend", p.getNextFloat()); + if (p.peekNextToken("refl") || p.peekNextToken("spec")) + api.parameter("specular", null, parseColor().getRGB()); + if (p.peekNextToken("texture")) { + // deprecated + UI.printWarning(Module.API, "Deprecated uber shader parameter \"texture\" - please use \"diffuse.texture\" and \"diffuse.blend\" instead"); + api.parameter("diffuse.texture", p.getNextToken()); + api.parameter("diffuse.blend", p.getNextFloat()); + } + if (p.peekNextToken("spec.texture")) + api.parameter("specular.texture", p.getNextToken()); + if (p.peekNextToken("spec.blend")) + api.parameter("specular.blend", p.getNextFloat()); + if (p.peekNextToken("glossy")) + api.parameter("glossyness", p.getNextFloat()); + if (p.peekNextToken("samples")) + api.parameter("samples", p.getNextInt()); + api.shader(name, "uber"); + } else + UI.printWarning(Module.API, "Unrecognized shader type: %s", p.getNextToken()); + p.checkNextToken("}"); + return true; + } + + private boolean parseModifier(SunflowAPIInterface api) throws ParserException, IOException { + p.checkNextToken("{"); + p.checkNextToken("name"); + String name = p.getNextToken(); + UI.printInfo(Module.API, "Reading modifier: %s ...", name); + p.checkNextToken("type"); + if (p.peekNextToken("bump")) { + p.checkNextToken("texture"); + api.parameter("texture", p.getNextToken()); + p.checkNextToken("scale"); + api.parameter("scale", p.getNextFloat()); + api.modifier(name, "bump_map"); + } else if (p.peekNextToken("normalmap")) { + p.checkNextToken("texture"); + api.parameter("texture", p.getNextToken()); + api.modifier(name, "normal_map"); + } else if (p.peekNextToken("perlin")) { + p.checkNextToken("function"); + api.parameter("function", p.getNextInt()); + p.checkNextToken("size"); + api.parameter("size", p.getNextFloat()); + p.checkNextToken("scale"); + api.parameter("scale", p.getNextFloat()); + api.modifier(name, "perlin"); + } else { + UI.printWarning(Module.API, "Unrecognized modifier type: %s", p.getNextToken()); + } + p.checkNextToken("}"); + return true; + } + + private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, IOException { + p.checkNextToken("{"); + boolean noInstance = false; + Matrix4[] transform = null; + float transformTime0 = 0, transformTime1 = 0; + String name = null; + String[] shaders = null; + String[] modifiers = null; + if (p.peekNextToken("noinstance")) { + // this indicates that the geometry is to be created, but not + // instanced into the scene + noInstance = true; + } else { + // these are the parameters to be passed to the instance + if (p.peekNextToken("shaders")) { + int n = p.getNextInt(); + shaders = new String[n]; + for (int i = 0; i < n; i++) + shaders[i] = p.getNextToken(); + } else { + p.checkNextToken("shader"); + shaders = new String[] { p.getNextToken() }; + } + if (p.peekNextToken("modifiers")) { + int n = p.getNextInt(); + modifiers = new String[n]; + for (int i = 0; i < n; i++) + modifiers[i] = p.getNextToken(); + } else if (p.peekNextToken("modifier")) + modifiers = new String[] { p.getNextToken() }; + if (p.peekNextToken("transform")) { + if (p.peekNextToken("steps")) { + transform = new Matrix4[p.getNextInt()]; + p.checkNextToken("times"); + transformTime0 = p.getNextFloat(); + transformTime1 = p.getNextFloat(); + for (int i = 0; i < transform.length; i++) + transform[i] = parseMatrix(); + } else + transform = new Matrix4[] { parseMatrix() }; + } + } + if (p.peekNextToken("accel")) + api.parameter("accel", p.getNextToken()); + p.checkNextToken("type"); + String type = p.getNextToken(); + if (p.peekNextToken("name")) + name = p.getNextToken(); + else + name = generateUniqueName(type); + if (type.equals("mesh")) { + UI.printWarning(Module.API, "Deprecated object type: mesh"); + UI.printInfo(Module.API, "Reading mesh: %s ...", name); + int numVertices = p.getNextInt(); + int numTriangles = p.getNextInt(); + float[] points = new float[numVertices * 3]; + float[] normals = new float[numVertices * 3]; + float[] uvs = new float[numVertices * 2]; + for (int i = 0; i < numVertices; i++) { + p.checkNextToken("v"); + points[3 * i + 0] = p.getNextFloat(); + points[3 * i + 1] = p.getNextFloat(); + points[3 * i + 2] = p.getNextFloat(); + normals[3 * i + 0] = p.getNextFloat(); + normals[3 * i + 1] = p.getNextFloat(); + normals[3 * i + 2] = p.getNextFloat(); + uvs[2 * i + 0] = p.getNextFloat(); + uvs[2 * i + 1] = p.getNextFloat(); + } + int[] triangles = new int[numTriangles * 3]; + for (int i = 0; i < numTriangles; i++) { + p.checkNextToken("t"); + triangles[i * 3 + 0] = p.getNextInt(); + triangles[i * 3 + 1] = p.getNextInt(); + triangles[i * 3 + 2] = p.getNextInt(); + } + // create geometry + api.parameter("triangles", triangles); + api.parameter("points", "point", "vertex", points); + api.parameter("normals", "vector", "vertex", normals); + api.parameter("uvs", "texcoord", "vertex", uvs); + api.geometry(name, "triangle_mesh"); + } else if (type.equals("flat-mesh")) { + UI.printWarning(Module.API, "Deprecated object type: flat-mesh"); + UI.printInfo(Module.API, "Reading flat mesh: %s ...", name); + int numVertices = p.getNextInt(); + int numTriangles = p.getNextInt(); + float[] points = new float[numVertices * 3]; + float[] uvs = new float[numVertices * 2]; + for (int i = 0; i < numVertices; i++) { + p.checkNextToken("v"); + points[3 * i + 0] = p.getNextFloat(); + points[3 * i + 1] = p.getNextFloat(); + points[3 * i + 2] = p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + uvs[2 * i + 0] = p.getNextFloat(); + uvs[2 * i + 1] = p.getNextFloat(); + } + int[] triangles = new int[numTriangles * 3]; + for (int i = 0; i < numTriangles; i++) { + p.checkNextToken("t"); + triangles[i * 3 + 0] = p.getNextInt(); + triangles[i * 3 + 1] = p.getNextInt(); + triangles[i * 3 + 2] = p.getNextInt(); + } + // create geometry + api.parameter("triangles", triangles); + api.parameter("points", "point", "vertex", points); + api.parameter("uvs", "texcoord", "vertex", uvs); + api.geometry(name, "triangle_mesh"); + } else if (type.equals("sphere")) { + UI.printInfo(Module.API, "Reading sphere ..."); + api.geometry(name, "sphere"); + if (transform == null && !noInstance) { + // legacy method of specifying transformation for spheres + p.checkNextToken("c"); + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + p.checkNextToken("r"); + float radius = p.getNextFloat(); + api.parameter("transform", Matrix4.translation(x, y, z).multiply(Matrix4.scale(radius))); + api.parameter("shaders", shaders); + if (modifiers != null) + api.parameter("modifiers", modifiers); + api.instance(name + ".instance", name); + // disable future instancing - instance has already been created + noInstance = true; + } + } else if (type.equals("cylinder")) { + UI.printInfo(Module.API, "Reading cylinder ..."); + api.geometry(name, "cylinder"); + } else if (type.equals("banchoff")) { + UI.printInfo(Module.API, "Reading banchoff ..."); + api.geometry(name, "banchoff"); + } else if (type.equals("torus")) { + UI.printInfo(Module.API, "Reading torus ..."); + p.checkNextToken("r"); + api.parameter("radiusInner", p.getNextFloat()); + api.parameter("radiusOuter", p.getNextFloat()); + api.geometry(name, "torus"); + } else if (type.equals("sphereflake")) { + UI.printInfo(Module.API, "Reading sphereflake ..."); + if (p.peekNextToken("level")) + api.parameter("level", p.getNextInt()); + if (p.peekNextToken("axis")) + api.parameter("axis", parseVector()); + if (p.peekNextToken("radius")) + api.parameter("radius", p.getNextFloat()); + api.geometry(name, "sphereflake"); + } else if (type.equals("plane")) { + UI.printInfo(Module.API, "Reading plane ..."); + p.checkNextToken("p"); + api.parameter("center", parsePoint()); + if (p.peekNextToken("n")) { + api.parameter("normal", parseVector()); + } else { + p.checkNextToken("p"); + api.parameter("point1", parsePoint()); + p.checkNextToken("p"); + api.parameter("point2", parsePoint()); + } + api.geometry(name, "plane"); + } else if (type.equals("generic-mesh")) { + UI.printInfo(Module.API, "Reading generic mesh: %s ... ", name); + // parse vertices + p.checkNextToken("points"); + int np = p.getNextInt(); + api.parameter("points", "point", "vertex", parseFloatArray(np * 3)); + // parse triangle indices + p.checkNextToken("triangles"); + int nt = p.getNextInt(); + api.parameter("triangles", parseIntArray(nt * 3)); + // parse normals + p.checkNextToken("normals"); + if (p.peekNextToken("vertex")) + api.parameter("normals", "vector", "vertex", parseFloatArray(np * 3)); + else if (p.peekNextToken("facevarying")) + api.parameter("normals", "vector", "facevarying", parseFloatArray(nt * 9)); + else + p.checkNextToken("none"); + // parse texture coordinates + p.checkNextToken("uvs"); + if (p.peekNextToken("vertex")) + api.parameter("uvs", "texcoord", "vertex", parseFloatArray(np * 2)); + else if (p.peekNextToken("facevarying")) + api.parameter("uvs", "texcoord", "facevarying", parseFloatArray(nt * 6)); + else + p.checkNextToken("none"); + if (p.peekNextToken("face_shaders")) + api.parameter("faceshaders", parseIntArray(nt)); + api.geometry(name, "triangle_mesh"); + } else if (type.equals("hair")) { + UI.printInfo(Module.API, "Reading hair curves: %s ... ", name); + p.checkNextToken("segments"); + api.parameter("segments", p.getNextInt()); + p.checkNextToken("width"); + api.parameter("widths", p.getNextFloat()); + p.checkNextToken("points"); + api.parameter("points", "point", "vertex", parseFloatArray(p.getNextInt())); + api.geometry(name, "hair"); + } else if (type.equals("janino-tesselatable")) { + UI.printInfo(Module.API, "Reading procedural primitive: %s ... ", name); + String typename = p.peekNextToken("typename") ? p.getNextToken() : PluginRegistry.shaderPlugins.generateUniqueName("janino_shader"); + if (!PluginRegistry.tesselatablePlugins.registerPlugin(typename, p.getNextCodeBlock())) + noInstance = true; + else + api.geometry(name, typename); + } else if (type.equals("teapot")) { + UI.printInfo(Module.API, "Reading teapot: %s ... ", name); + if (p.peekNextToken("subdivs")) + api.parameter("subdivs", p.getNextInt()); + if (p.peekNextToken("smooth")) + api.parameter("smooth", p.getNextBoolean()); + api.geometry(name, "teapot"); + } else if (type.equals("gumbo")) { + UI.printInfo(Module.API, "Reading gumbo: %s ... ", name); + if (p.peekNextToken("subdivs")) + api.parameter("subdivs", p.getNextInt()); + if (p.peekNextToken("smooth")) + api.parameter("smooth", p.getNextBoolean()); + api.geometry(name, "gumbo"); + } else if (type.equals("julia")) { + UI.printInfo(Module.API, "Reading julia fractal: %s ... ", name); + if (p.peekNextToken("q")) { + api.parameter("cw", p.getNextFloat()); + api.parameter("cx", p.getNextFloat()); + api.parameter("cy", p.getNextFloat()); + api.parameter("cz", p.getNextFloat()); + } + if (p.peekNextToken("iterations")) + api.parameter("iterations", p.getNextInt()); + if (p.peekNextToken("epsilon")) + api.parameter("epsilon", p.getNextFloat()); + api.geometry(name, "julia"); + } else if (type.equals("particles") || type.equals("dlasurface")) { + if (type.equals("dlasurface")) + UI.printWarning(Module.API, "Deprecated object type: \"dlasurface\" - please use \"particles\" instead"); + float[] data; + if (p.peekNextToken("filename")) { + // FIXME: this code should be moved into an on demand loading + // primitive + String filename = p.getNextToken(); + boolean littleEndian = false; + if (p.peekNextToken("little_endian")) + littleEndian = true; + UI.printInfo(Module.USER, "Loading particle file: %s", filename); + File file = new File(filename); + FileInputStream stream = new FileInputStream(filename); + MappedByteBuffer map = stream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()); + if (littleEndian) + map.order(ByteOrder.LITTLE_ENDIAN); + FloatBuffer buffer = map.asFloatBuffer(); + data = new float[buffer.capacity()]; + for (int i = 0; i < data.length; i++) + data[i] = buffer.get(i); + stream.close(); + } else { + p.checkNextToken("points"); + int n = p.getNextInt(); + data = parseFloatArray(n * 3); // read 3n points + } + api.parameter("particles", "point", "vertex", data); + if (p.peekNextToken("num")) + api.parameter("num", p.getNextInt()); + else + api.parameter("num", data.length / 3); + p.checkNextToken("radius"); + api.parameter("radius", p.getNextFloat()); + api.geometry(name, "particles"); + } else if (type.equals("file-mesh")) { + UI.printInfo(Module.API, "Reading file mesh: %s ... ", name); + p.checkNextToken("filename"); + api.parameter("filename", p.getNextToken()); + if (p.peekNextToken("smooth_normals")) + api.parameter("smooth_normals", p.getNextBoolean()); + api.geometry(name, "file_mesh"); + } else if (type.equals("bezier-mesh")) { + UI.printInfo(Module.API, "Reading bezier mesh: %s ... ", name); + p.checkNextToken("n"); + int nu, nv; + api.parameter("nu", nu = p.getNextInt()); + api.parameter("nv", nv = p.getNextInt()); + if (p.peekNextToken("wrap")) { + api.parameter("uwrap", p.getNextBoolean()); + api.parameter("vwrap", p.getNextBoolean()); + } + p.checkNextToken("points"); + float[] points = new float[3 * nu * nv]; + for (int i = 0; i < points.length; i++) + points[i] = p.getNextFloat(); + api.parameter("points", "point", "vertex", points); + if (p.peekNextToken("subdivs")) + api.parameter("subdivs", p.getNextInt()); + if (p.peekNextToken("smooth")) + api.parameter("smooth", p.getNextBoolean()); + api.geometry(name, "bezier_mesh"); + } else { + UI.printWarning(Module.API, "Unrecognized object type: %s", p.getNextToken()); + noInstance = true; + } + if (!noInstance) { + // create instance + api.parameter("shaders", shaders); + if (modifiers != null) + api.parameter("modifiers", modifiers); + if (transform != null && transform.length > 0) { + if (transform.length == 1) + api.parameter("transform", transform[0]); + else { + api.parameter("transform.steps", transform.length); + api.parameter("transform.times", "float", "none", new float[] { + transformTime0, transformTime1 }); + for (int i = 0; i < transform.length; i++) + api.parameter(String.format("transform[%d]", i), transform[i]); + } + } + api.instance(name + ".instance", name); + } + p.checkNextToken("}"); + } + + private void parseInstanceBlock(SunflowAPIInterface api) throws ParserException, IOException { + p.checkNextToken("{"); + p.checkNextToken("name"); + String name = p.getNextToken(); + UI.printInfo(Module.API, "Reading instance: %s ...", name); + p.checkNextToken("geometry"); + String geoname = p.getNextToken(); + p.checkNextToken("transform"); + if (p.peekNextToken("steps")) { + int n = p.getNextInt(); + api.parameter("transform.steps", n); + p.checkNextToken("times"); + float[] times = new float[2]; + times[0] = p.getNextFloat(); + times[1] = p.getNextFloat(); + api.parameter("transform.times", "float", "none", times); + for (int i = 0; i < n; i++) + api.parameter(String.format("transform[%d]", i), parseMatrix()); + } else + api.parameter("transform", parseMatrix()); + String[] shaders; + if (p.peekNextToken("shaders")) { + int n = p.getNextInt(); + shaders = new String[n]; + for (int i = 0; i < n; i++) + shaders[i] = p.getNextToken(); + } else { + p.checkNextToken("shader"); + shaders = new String[] { p.getNextToken() }; + } + api.parameter("shaders", shaders); + String[] modifiers = null; + if (p.peekNextToken("modifiers")) { + int n = p.getNextInt(); + modifiers = new String[n]; + for (int i = 0; i < n; i++) + modifiers[i] = p.getNextToken(); + } else if (p.peekNextToken("modifier")) + modifiers = new String[] { p.getNextToken() }; + if (modifiers != null) + api.parameter("modifiers", modifiers); + api.instance(name, geoname); + p.checkNextToken("}"); + } + + private void parseLightBlock(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { + p.checkNextToken("{"); + p.checkNextToken("type"); + if (p.peekNextToken("mesh")) { + UI.printWarning(Module.API, "Deprecated light type: mesh"); + p.checkNextToken("name"); + String name = p.getNextToken(); + UI.printInfo(Module.API, "Reading light mesh: %s ...", name); + p.checkNextToken("emit"); + api.parameter("radiance", null, parseColor().getRGB()); + int samples = numLightSamples; + if (p.peekNextToken("samples")) + samples = p.getNextInt(); + else + UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", samples); + api.parameter("samples", samples); + int numVertices = p.getNextInt(); + int numTriangles = p.getNextInt(); + float[] points = new float[3 * numVertices]; + int[] triangles = new int[3 * numTriangles]; + for (int i = 0; i < numVertices; i++) { + p.checkNextToken("v"); + points[3 * i + 0] = p.getNextFloat(); + points[3 * i + 1] = p.getNextFloat(); + points[3 * i + 2] = p.getNextFloat(); + // ignored + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + } + for (int i = 0; i < numTriangles; i++) { + p.checkNextToken("t"); + triangles[3 * i + 0] = p.getNextInt(); + triangles[3 * i + 1] = p.getNextInt(); + triangles[3 * i + 2] = p.getNextInt(); + } + api.parameter("points", "point", "vertex", points); + api.parameter("triangles", triangles); + api.light(name, "triangle_mesh"); + } else if (p.peekNextToken("point")) { + UI.printInfo(Module.API, "Reading point light ..."); + Color pow; + if (p.peekNextToken("color")) { + pow = parseColor(); + p.checkNextToken("power"); + float po = p.getNextFloat(); + pow.mul(po); + } else { + UI.printWarning(Module.API, "Deprecated color specification - please use color and power instead"); + p.checkNextToken("power"); + pow = parseColor(); + } + p.checkNextToken("p"); + api.parameter("center", parsePoint()); + api.parameter("power", null, pow.getRGB()); + api.light(generateUniqueName("pointlight"), "point"); + } else if (p.peekNextToken("spherical")) { + UI.printInfo(Module.API, "Reading spherical light ..."); + p.checkNextToken("color"); + Color pow = parseColor(); + p.checkNextToken("radiance"); + pow.mul(p.getNextFloat()); + api.parameter("radiance", null, pow.getRGB()); + p.checkNextToken("center"); + api.parameter("center", parsePoint()); + p.checkNextToken("radius"); + api.parameter("radius", p.getNextFloat()); + p.checkNextToken("samples"); + api.parameter("samples", p.getNextInt()); + api.light(generateUniqueName("spherelight"), "sphere"); + } else if (p.peekNextToken("directional")) { + UI.printInfo(Module.API, "Reading directional light ..."); + p.checkNextToken("source"); + Point3 s = parsePoint(); + api.parameter("source", s); + p.checkNextToken("target"); + Point3 t = parsePoint(); + api.parameter("dir", Point3.sub(t, s, new Vector3())); + p.checkNextToken("radius"); + api.parameter("radius", p.getNextFloat()); + p.checkNextToken("emit"); + Color e = parseColor(); + if (p.peekNextToken("intensity")) { + float i = p.getNextFloat(); + e.mul(i); + } else + UI.printWarning(Module.API, "Deprecated color specification - please use emit and intensity instead"); + api.parameter("radiance", null, e.getRGB()); + api.light(generateUniqueName("dirlight"), "directional"); + } else if (p.peekNextToken("ibl")) { + UI.printInfo(Module.API, "Reading image based light ..."); + p.checkNextToken("image"); + api.parameter("texture", p.getNextToken()); + p.checkNextToken("center"); + api.parameter("center", parseVector()); + p.checkNextToken("up"); + api.parameter("up", parseVector()); + p.checkNextToken("lock"); + api.parameter("fixed", p.getNextBoolean()); + int samples = numLightSamples; + if (p.peekNextToken("samples")) + samples = p.getNextInt(); + else + UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", samples); + api.parameter("samples", samples); + if (p.peekNextToken("lowsamples")) + api.parameter("lowsamples", p.getNextInt()); + else + api.parameter("lowsamples", samples); + api.light(generateUniqueName("ibl"), "ibl"); + } else if (p.peekNextToken("meshlight")) { + p.checkNextToken("name"); + String name = p.getNextToken(); + UI.printInfo(Module.API, "Reading meshlight: %s ...", name); + p.checkNextToken("emit"); + Color e = parseColor(); + if (p.peekNextToken("radiance")) { + float r = p.getNextFloat(); + e.mul(r); + } else + UI.printWarning(Module.API, "Deprecated color specification - please use emit and radiance instead"); + api.parameter("radiance", null, e.getRGB()); + int samples = numLightSamples; + if (p.peekNextToken("samples")) + samples = p.getNextInt(); + else + UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", samples); + api.parameter("samples", samples); + // parse vertices + p.checkNextToken("points"); + int np = p.getNextInt(); + api.parameter("points", "point", "vertex", parseFloatArray(np * 3)); + // parse triangle indices + p.checkNextToken("triangles"); + int nt = p.getNextInt(); + api.parameter("triangles", parseIntArray(nt * 3)); + api.light(name, "triangle_mesh"); + } else if (p.peekNextToken("sunsky")) { + p.checkNextToken("up"); + api.parameter("up", parseVector()); + p.checkNextToken("east"); + api.parameter("east", parseVector()); + p.checkNextToken("sundir"); + api.parameter("sundir", parseVector()); + p.checkNextToken("turbidity"); + api.parameter("turbidity", p.getNextFloat()); + if (p.peekNextToken("samples")) + api.parameter("samples", p.getNextInt()); + if (p.peekNextToken("ground.extendsky")) + api.parameter("ground.extendsky", p.getNextBoolean()); + else if (p.peekNextToken("ground.color")) + api.parameter("ground.color", null, parseColor().getRGB()); + api.light(generateUniqueName("sunsky"), "sunsky"); + } else if (p.peekNextToken("cornellbox")) { + UI.printInfo(Module.API, "Reading cornell box ..."); + p.checkNextToken("corner0"); + api.parameter("corner0", parsePoint()); + p.checkNextToken("corner1"); + api.parameter("corner1", parsePoint()); + p.checkNextToken("left"); + api.parameter("leftColor", null, parseColor().getRGB()); + p.checkNextToken("right"); + api.parameter("rightColor", null, parseColor().getRGB()); + p.checkNextToken("top"); + api.parameter("topColor", null, parseColor().getRGB()); + p.checkNextToken("bottom"); + api.parameter("bottomColor", null, parseColor().getRGB()); + p.checkNextToken("back"); + api.parameter("backColor", null, parseColor().getRGB()); + p.checkNextToken("emit"); + api.parameter("radiance", null, parseColor().getRGB()); + if (p.peekNextToken("samples")) + api.parameter("samples", p.getNextInt()); + api.light(generateUniqueName("cornellbox"), "cornell_box"); + } else + UI.printWarning(Module.API, "Unrecognized object type: %s", p.getNextToken()); + p.checkNextToken("}"); + } + + private Color parseColor() throws IOException, ParserException, ColorSpecificationException { + if (p.peekNextToken("{")) { + String space = p.getNextToken(); + int req = ColorFactory.getRequiredDataValues(space); + if (req == -2) { + UI.printWarning(Module.API, "Unrecognized color space: %s", space); + return null; + } else if (req == -1) { + // array required, parse how many values are required + req = p.getNextInt(); + } + Color c = ColorFactory.createColor(space, parseFloatArray(req)); + p.checkNextToken("}"); + return c; + } else { + float r = p.getNextFloat(); + float g = p.getNextFloat(); + float b = p.getNextFloat(); + return ColorFactory.createColor(null, r, g, b); + } + } + + private Point3 parsePoint() throws IOException { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + return new Point3(x, y, z); + } + + private Vector3 parseVector() throws IOException { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + return new Vector3(x, y, z); + } + + private int[] parseIntArray(int size) throws IOException { + int[] data = new int[size]; + for (int i = 0; i < size; i++) + data[i] = p.getNextInt(); + return data; + } + + private float[] parseFloatArray(int size) throws IOException { + float[] data = new float[size]; + for (int i = 0; i < size; i++) + data[i] = p.getNextFloat(); + return data; + } + + private Matrix4 parseMatrix() throws IOException, ParserException { + if (p.peekNextToken("row")) { + return new Matrix4(parseFloatArray(16), true); + } else if (p.peekNextToken("col")) { + return new Matrix4(parseFloatArray(16), false); + } else { + Matrix4 m = Matrix4.IDENTITY; + p.checkNextToken("{"); + while (!p.peekNextToken("}")) { + Matrix4 t = null; + if (p.peekNextToken("translate")) { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + t = Matrix4.translation(x, y, z); + } else if (p.peekNextToken("scaleu")) { + float s = p.getNextFloat(); + t = Matrix4.scale(s); + } else if (p.peekNextToken("scale")) { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + t = Matrix4.scale(x, y, z); + } else if (p.peekNextToken("rotatex")) { + float angle = p.getNextFloat(); + t = Matrix4.rotateX((float) Math.toRadians(angle)); + } else if (p.peekNextToken("rotatey")) { + float angle = p.getNextFloat(); + t = Matrix4.rotateY((float) Math.toRadians(angle)); + } else if (p.peekNextToken("rotatez")) { + float angle = p.getNextFloat(); + t = Matrix4.rotateZ((float) Math.toRadians(angle)); + } else if (p.peekNextToken("rotate")) { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + float angle = p.getNextFloat(); + t = Matrix4.rotate(x, y, z, (float) Math.toRadians(angle)); + } else + UI.printWarning(Module.API, "Unrecognized transformation type: %s", p.getNextToken()); + if (t != null) + m = t.multiply(m); + } + return m; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/parser/ShaveRibParser.java b/src/main/java/org/sunflow/core/parser/ShaveRibParser.java new file mode 100644 index 0000000..5c2a3cb --- /dev/null +++ b/src/main/java/org/sunflow/core/parser/ShaveRibParser.java @@ -0,0 +1,165 @@ +package org.sunflow.core.parser; + +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.SceneParser; +import org.sunflow.system.Parser; +import org.sunflow.system.UI; +import org.sunflow.system.Parser.ParserException; +import org.sunflow.system.UI.Module; +import org.sunflow.util.FloatArray; +import org.sunflow.util.IntArray; + +public class ShaveRibParser implements SceneParser { + public boolean parse(String filename, SunflowAPIInterface api) { + try { + Parser p = new Parser(filename); + p.checkNextToken("version"); + p.checkNextToken("3.04"); + p.checkNextToken("TransformBegin"); + + if (p.peekNextToken("Procedural")) { + // read procedural shave rib + boolean done = false; + while (!done) { + p.checkNextToken("DelayedReadArchive"); + p.checkNextToken("["); + String f = p.getNextToken(); + UI.printInfo(Module.USER, "RIB - Reading voxel: \"%s\" ...", f); + api.include(f); + p.checkNextToken("]"); + while (true) { + String t = p.getNextToken(); + if (t == null || t.equals("TransformEnd")) { + done = true; + break; + } else if (t.equals("Procedural")) + break; + } + } + return true; + } + + boolean cubic = false; + if (p.peekNextToken("Basis")) { + cubic = true; + // u basis + p.checkNextToken("catmull-rom"); + p.checkNextToken("1"); + // v basis + p.checkNextToken("catmull-rom"); + p.checkNextToken("1"); + } + while (p.peekNextToken("Declare")) { + p.getNextToken(); // name + p.getNextToken(); // interpolation & type + } + int index = 0; + boolean done = false; + p.checkNextToken("Curves"); + do { + if (cubic) + p.checkNextToken("cubic"); + else + p.checkNextToken("linear"); + int[] nverts = parseIntArray(p); + for (int i = 1; i < nverts.length; i++) { + if (nverts[0] != nverts[i]) { + UI.printError(Module.USER, "RIB - Found variable number of hair segments"); + return false; + } + } + int nhairs = nverts.length; + + UI.printInfo(Module.USER, "RIB - Parsed %d hair curves", nhairs); + + api.parameter("segments", nverts[0] - 1); + + p.checkNextToken("nonperiodic"); + p.checkNextToken("P"); + float[] points = parseFloatArray(p); + if (points.length != 3 * nhairs * nverts[0]) { + UI.printError(Module.USER, "RIB - Invalid number of points - expecting %d - found %d", nhairs * nverts[0], points.length / 3); + return false; + } + api.parameter("points", "point", "vertex", points); + + UI.printInfo(Module.USER, "RIB - Parsed %d hair vertices", points.length / 3); + + p.checkNextToken("width"); + float[] w = parseFloatArray(p); + if (w.length != nhairs * nverts[0]) { + UI.printError(Module.USER, "RIB - Invalid number of hair widths - expecting %d - found %d", nhairs * nverts[0], w.length); + return false; + } + api.parameter("widths", "float", "vertex", w); + + UI.printInfo(Module.USER, "RIB - Parsed %d hair widths", w.length); + + String name = String.format("%s[%d]", filename, index); + UI.printInfo(Module.USER, "RIB - Creating hair object \"%s\"", name); + api.geometry(name, "hair"); + api.instance(name + ".instance", name); + + UI.printInfo(Module.USER, "RIB - Searching for next curve group ..."); + while (true) { + String t = p.getNextToken(); + if (t == null || t.equals("TransformEnd")) { + done = true; + break; + } else if (t.equals("Curves")) + break; + } + index++; + } while (!done); + UI.printInfo(Module.USER, "RIB - Finished reading rib file"); + } catch (FileNotFoundException e) { + UI.printError(Module.USER, "RIB - File not found: %s", filename); + e.printStackTrace(); + return false; + } catch (ParserException e) { + UI.printError(Module.USER, "RIB - Parser exception: %s", e); + e.printStackTrace(); + return false; + } catch (IOException e) { + UI.printError(Module.USER, "RIB - I/O exception: %s", e); + e.printStackTrace(); + return false; + } + return true; + } + + private int[] parseIntArray(Parser p) throws IOException { + IntArray array = new IntArray(); + boolean done = false; + do { + String s = p.getNextToken(); + if (s.startsWith("[")) + s = s.substring(1); + if (s.endsWith("]")) { + s = s.substring(0, s.length() - 1); + done = true; + } + array.add(Integer.parseInt(s)); + } while (!done); + return array.trim(); + } + + private float[] parseFloatArray(Parser p) throws IOException { + FloatArray array = new FloatArray(); + boolean done = false; + do { + String s = p.getNextToken(); + if (s.startsWith("[")) + s = s.substring(1); + if (s.endsWith("]")) { + s = s.substring(0, s.length() - 1); + done = true; + } + array.add(Float.parseFloat(s)); + } while (!done); + return array.trim(); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/parser/TriParser.java b/src/main/java/org/sunflow/core/parser/TriParser.java new file mode 100644 index 0000000..a404c2f --- /dev/null +++ b/src/main/java/org/sunflow/core/parser/TriParser.java @@ -0,0 +1,73 @@ +package org.sunflow.core.parser; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel.MapMode; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.SceneParser; +import org.sunflow.system.Parser; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class TriParser implements SceneParser { + public boolean parse(String filename, SunflowAPIInterface api) { + try { + UI.printInfo(Module.USER, "TRI - Reading geometry: \"%s\" ...", filename); + Parser p = new Parser(filename); + float[] verts = new float[3 * p.getNextInt()]; + for (int v = 0; v < verts.length; v += 3) { + verts[v + 0] = p.getNextFloat(); + verts[v + 1] = p.getNextFloat(); + verts[v + 2] = p.getNextFloat(); + p.getNextToken(); + p.getNextToken(); + } + int[] triangles = new int[p.getNextInt() * 3]; + for (int t = 0; t < triangles.length; t += 3) { + triangles[t + 0] = p.getNextInt(); + triangles[t + 1] = p.getNextInt(); + triangles[t + 2] = p.getNextInt(); + } + + // create geometry + api.parameter("triangles", triangles); + api.parameter("points", "point", "vertex", verts); + api.geometry(filename, "triangle_mesh"); + + // create shader + api.shader(filename + ".shader", "simple"); + api.parameter("shaders", filename + ".shader"); + + // create instance + api.instance(filename + ".instance", filename); + + p.close(); + // output to ra3 format + RandomAccessFile stream = new RandomAccessFile(filename.replace(".tri", ".ra3"), "rw"); + MappedByteBuffer map = stream.getChannel().map(MapMode.READ_WRITE, 0, 8 + 4 * (verts.length + triangles.length)); + map.order(ByteOrder.LITTLE_ENDIAN); + IntBuffer ints = map.asIntBuffer(); + FloatBuffer floats = map.asFloatBuffer(); + ints.put(0, verts.length / 3); + ints.put(1, triangles.length / 3); + for (int i = 0; i < verts.length; i++) + floats.put(2 + i, verts[i]); + for (int i = 0; i < triangles.length; i++) + ints.put(2 + verts.length + i, triangles[i]); + stream.close(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return false; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/photonmap/CausticPhotonMap.java b/src/main/java/org/sunflow/core/photonmap/CausticPhotonMap.java new file mode 100644 index 0000000..27caa09 --- /dev/null +++ b/src/main/java/org/sunflow/core/photonmap/CausticPhotonMap.java @@ -0,0 +1,401 @@ +package org.sunflow.core.photonmap; + +import java.util.ArrayList; + +import org.sunflow.core.CausticPhotonMapInterface; +import org.sunflow.core.LightSample; +import org.sunflow.core.Options; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public final class CausticPhotonMap implements CausticPhotonMapInterface { + private ArrayList photonList; + private Photon[] photons; + private int storedPhotons; + private int halfStoredPhotons; + private int log2n; + private int gatherNum; + private float gatherRadius; + private BoundingBox bounds; + private float filterValue; + private float maxPower; + private float maxRadius; + private int numEmit; + + public void prepare(Options options, BoundingBox sceneBounds) { + // get options + numEmit = options.getInt("caustics.emit", 10000); + gatherNum = options.getInt("caustics.gather", 50); + gatherRadius = options.getFloat("caustics.radius", 0.5f); + filterValue = options.getFloat("caustics.filter", 1.1f); + // init + bounds = new BoundingBox(); + maxPower = 0; + maxRadius = 0; + photonList = new ArrayList(); + photonList.add(null); + photons = null; + storedPhotons = halfStoredPhotons = 0; + } + + private void locatePhotons(NearestPhotons np) { + float[] dist1d2 = new float[log2n]; + int[] chosen = new int[log2n]; + int i = 1; + int level = 0; + int cameFrom; + while (true) { + while (i < halfStoredPhotons) { + float dist1d = photons[i].getDist1(np.px, np.py, np.pz); + dist1d2[level] = dist1d * dist1d; + i += i; + if (dist1d > 0.0f) + i++; + chosen[level++] = i; + } + np.checkAddNearest(photons[i]); + do { + cameFrom = i; + i >>= 1; + level--; + if (i == 0) + return; + } while ((dist1d2[level] >= np.dist2[0]) || (cameFrom != chosen[level])); + np.checkAddNearest(photons[i]); + i = chosen[level++] ^ 1; + } + } + + private void balance() { + if (storedPhotons == 0) + return; + photons = photonList.toArray(new Photon[photonList.size()]); + photonList = null; + Photon[] temp = new Photon[storedPhotons + 1]; + balanceSegment(temp, 1, 1, storedPhotons); + photons = temp; + halfStoredPhotons = storedPhotons / 2; + log2n = (int) Math.ceil(Math.log(storedPhotons) / Math.log(2.0)); + } + + private void balanceSegment(Photon[] temp, int index, int start, int end) { + int median = 1; + while ((4 * median) <= (end - start + 1)) + median += median; + if ((3 * median) <= (end - start + 1)) { + median += median; + median += (start - 1); + } else + median = end - median + 1; + int axis = Photon.SPLIT_Z; + Vector3 extents = bounds.getExtents(); + if ((extents.x > extents.y) && (extents.x > extents.z)) + axis = Photon.SPLIT_X; + else if (extents.y > extents.z) + axis = Photon.SPLIT_Y; + int left = start; + int right = end; + while (right > left) { + double v = photons[right].getCoord(axis); + int i = left - 1; + int j = right; + while (true) { + while (photons[++i].getCoord(axis) < v) { + } + while ((photons[--j].getCoord(axis) > v) && (j > left)) { + } + if (i >= j) + break; + swap(i, j); + } + swap(i, right); + if (i >= median) + right = i - 1; + if (i <= median) + left = i + 1; + } + temp[index] = photons[median]; + temp[index].setSplitAxis(axis); + if (median > start) { + if (start < (median - 1)) { + float tmp; + switch (axis) { + case Photon.SPLIT_X: + tmp = bounds.getMaximum().x; + bounds.getMaximum().x = temp[index].x; + balanceSegment(temp, 2 * index, start, median - 1); + bounds.getMaximum().x = tmp; + break; + case Photon.SPLIT_Y: + tmp = bounds.getMaximum().y; + bounds.getMaximum().y = temp[index].y; + balanceSegment(temp, 2 * index, start, median - 1); + bounds.getMaximum().y = tmp; + break; + default: + tmp = bounds.getMaximum().z; + bounds.getMaximum().z = temp[index].z; + balanceSegment(temp, 2 * index, start, median - 1); + bounds.getMaximum().z = tmp; + } + } else + temp[2 * index] = photons[start]; + } + if (median < end) { + if ((median + 1) < end) { + float tmp; + switch (axis) { + case Photon.SPLIT_X: + tmp = bounds.getMinimum().x; + bounds.getMinimum().x = temp[index].x; + balanceSegment(temp, (2 * index) + 1, median + 1, end); + bounds.getMinimum().x = tmp; + break; + case Photon.SPLIT_Y: + tmp = bounds.getMinimum().y; + bounds.getMinimum().y = temp[index].y; + balanceSegment(temp, (2 * index) + 1, median + 1, end); + bounds.getMinimum().y = tmp; + break; + default: + tmp = bounds.getMinimum().z; + bounds.getMinimum().z = temp[index].z; + balanceSegment(temp, (2 * index) + 1, median + 1, end); + bounds.getMinimum().z = tmp; + } + } else + temp[(2 * index) + 1] = photons[end]; + } + } + + private void swap(int i, int j) { + Photon tmp = photons[i]; + photons[i] = photons[j]; + photons[j] = tmp; + } + + public void store(ShadingState state, Vector3 dir, Color power, Color diffuse) { + if (((state.getDiffuseDepth() == 0) && (state.getReflectionDepth() > 0 || state.getRefractionDepth() > 0))) { + // this is a caustic photon + Photon p = new Photon(state.getPoint(), dir, power); + synchronized (this) { + storedPhotons++; + photonList.add(p); + bounds.include(new Point3(p.x, p.y, p.z)); + maxPower = Math.max(maxPower, power.getMax()); + } + } + } + + public void init() { + UI.printInfo(Module.LIGHT, "Balancing caustics photon map ..."); + Timer t = new Timer(); + t.start(); + balance(); + t.end(); + UI.printInfo(Module.LIGHT, "Caustic photon map:"); + UI.printInfo(Module.LIGHT, " * Photons stored: %d", storedPhotons); + UI.printInfo(Module.LIGHT, " * Photons/estimate: %d", gatherNum); + maxRadius = 1.4f * (float) Math.sqrt(maxPower * gatherNum); + UI.printInfo(Module.LIGHT, " * Estimate radius: %.3f", gatherRadius); + UI.printInfo(Module.LIGHT, " * Maximum radius: %.3f", maxRadius); + UI.printInfo(Module.LIGHT, " * Balancing time: %s", t.toString()); + if (gatherRadius > maxRadius) + gatherRadius = maxRadius; + } + + public void getSamples(ShadingState state) { + if (storedPhotons == 0) + return; + NearestPhotons np = new NearestPhotons(state.getPoint(), gatherNum, gatherRadius * gatherRadius); + locatePhotons(np); + if (np.found < 8) + return; + Point3 ppos = new Point3(); + Vector3 pdir = new Vector3(); + Vector3 pvec = new Vector3(); + float invArea = 1.0f / ((float) Math.PI * np.dist2[0]); + float maxNDist = np.dist2[0] * 0.05f; + float f2r2 = 1.0f / (filterValue * filterValue * np.dist2[0]); + float fInv = 1.0f / (1.0f - 2.0f / (3.0f * filterValue)); + for (int i = 1; i <= np.found; i++) { + Photon phot = np.index[i]; + Vector3.decode(phot.dir, pdir); + float cos = -Vector3.dot(pdir, state.getNormal()); + if (cos > 0.001) { + ppos.set(phot.x, phot.y, phot.z); + Point3.sub(ppos, state.getPoint(), pvec); + float pcos = Vector3.dot(pvec, state.getNormal()); + if ((pcos < maxNDist) && (pcos > -maxNDist)) { + LightSample sample = new LightSample(); + sample.setShadowRay(new Ray(state.getPoint(), pdir.negate())); + sample.setRadiance(new Color().setRGBE(np.index[i].power).mul(invArea / cos), Color.BLACK); + sample.getDiffuseRadiance().mul((1.0f - (float) Math.sqrt(np.dist2[i] * f2r2)) * fInv); + state.addSample(sample); + } + } + } + } + + private static class NearestPhotons { + int found; + float px, py, pz; + private int max; + private boolean gotHeap; + protected float[] dist2; + protected Photon[] index; + + NearestPhotons(Point3 p, int n, float maxDist2) { + max = n; + found = 0; + gotHeap = false; + px = p.x; + py = p.y; + pz = p.z; + dist2 = new float[n + 1]; + index = new Photon[n + 1]; + dist2[0] = maxDist2; + } + + void reset(Point3 p, float maxDist2) { + found = 0; + gotHeap = false; + px = p.x; + py = p.y; + pz = p.z; + dist2[0] = maxDist2; + } + + void checkAddNearest(Photon p) { + float fdist2 = p.getDist2(px, py, pz); + if (fdist2 < dist2[0]) { + if (found < max) { + found++; + dist2[found] = fdist2; + index[found] = p; + } else { + int j; + int parent; + if (!gotHeap) { + float dst2; + Photon phot; + int halfFound = found >> 1; + for (int k = halfFound; k >= 1; k--) { + parent = k; + phot = index[k]; + dst2 = dist2[k]; + while (parent <= halfFound) { + j = parent + parent; + if ((j < found) && (dist2[j] < dist2[j + 1])) + j++; + if (dst2 >= dist2[j]) + break; + dist2[parent] = dist2[j]; + index[parent] = index[j]; + parent = j; + } + dist2[parent] = dst2; + index[parent] = phot; + } + gotHeap = true; + } + parent = 1; + j = 2; + while (j <= found) { + if ((j < found) && (dist2[j] < dist2[j + 1])) + j++; + if (fdist2 > dist2[j]) + break; + dist2[parent] = dist2[j]; + index[parent] = index[j]; + parent = j; + j += j; + } + dist2[parent] = fdist2; + index[parent] = p; + dist2[0] = dist2[1]; + } + } + } + } + + private static class Photon { + float x; + float y; + float z; + short dir; + int power; + int flags; + + static final int SPLIT_X = 0; + static final int SPLIT_Y = 1; + static final int SPLIT_Z = 2; + static final int SPLIT_MASK = 3; + + Photon(Point3 p, Vector3 dir, Color power) { + x = p.x; + y = p.y; + z = p.z; + this.dir = dir.encode(); + this.power = power.toRGBE(); + flags = SPLIT_X; + } + + void setSplitAxis(int axis) { + flags &= ~SPLIT_MASK; + flags |= axis; + } + + float getCoord(int axis) { + switch (axis) { + case SPLIT_X: + return x; + case SPLIT_Y: + return y; + default: + return z; + } + } + + float getDist1(float px, float py, float pz) { + switch (flags & SPLIT_MASK) { + case SPLIT_X: + return px - x; + case SPLIT_Y: + return py - y; + default: + return pz - z; + } + } + + float getDist2(float px, float py, float pz) { + float dx = x - px; + float dy = y - py; + float dz = z - pz; + return (dx * dx) + (dy * dy) + (dz * dz); + } + } + + public boolean allowDiffuseBounced() { + return false; + } + + public boolean allowReflectionBounced() { + return true; + } + + public boolean allowRefractionBounced() { + return true; + } + + public int numEmit() { + return numEmit; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/photonmap/GlobalPhotonMap.java b/src/main/java/org/sunflow/core/photonmap/GlobalPhotonMap.java new file mode 100644 index 0000000..d9ef2cf --- /dev/null +++ b/src/main/java/org/sunflow/core/photonmap/GlobalPhotonMap.java @@ -0,0 +1,498 @@ +package org.sunflow.core.photonmap; + +import java.util.ArrayList; + +import org.sunflow.core.GlobalPhotonMapInterface; +import org.sunflow.core.Options; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public final class GlobalPhotonMap implements GlobalPhotonMapInterface { + private ArrayList photonList; + private Photon[] photons; + private int storedPhotons; + private int halfStoredPhotons; + private int log2n; + private int numGather; + private float gatherRadius; + private BoundingBox bounds; + private boolean hasRadiance; + private float maxPower; + private float maxRadius; + private int numEmit; + + public GlobalPhotonMap() { + bounds = new BoundingBox(); + hasRadiance = false; + maxPower = 0; + maxRadius = 0; + } + + public void prepare(Options options, BoundingBox sceneBounds) { + // get settings + numEmit = options.getInt("gi.irr-cache.gmap.emit", 100000); + numGather = options.getInt("gi.irr-cache.gmap.gather", 50); + gatherRadius = options.getFloat("gi.irr-cache.gmap.radius", 0.5f); + // init + photonList = new ArrayList(); + photonList.add(null); + photons = null; + storedPhotons = halfStoredPhotons = 0; + } + + public void store(ShadingState state, Vector3 dir, Color power, Color diffuse) { + Photon p = new Photon(state.getPoint(), state.getNormal(), dir, power, diffuse); + synchronized (this) { + storedPhotons++; + photonList.add(p); + bounds.include(new Point3(p.x, p.y, p.z)); + maxPower = Math.max(maxPower, power.getMax()); + } + } + + private void locatePhotons(NearestPhotons np) { + float[] dist1d2 = new float[log2n]; + int[] chosen = new int[log2n]; + int i = 1; + int level = 0; + int cameFrom; + while (true) { + while (i < halfStoredPhotons) { + float dist1d = photons[i].getDist1(np.px, np.py, np.pz); + dist1d2[level] = dist1d * dist1d; + i += i; + if (dist1d > 0.0f) + i++; + chosen[level++] = i; + } + np.checkAddNearest(photons[i]); + do { + cameFrom = i; + i >>= 1; + level--; + if (i == 0) + return; + } while ((dist1d2[level] >= np.dist2[0]) || (cameFrom != chosen[level])); + np.checkAddNearest(photons[i]); + i = chosen[level++] ^ 1; + } + } + + private void balance() { + if (storedPhotons == 0) + return; + photons = photonList.toArray(new Photon[photonList.size()]); + photonList = null; + Photon[] temp = new Photon[storedPhotons + 1]; + balanceSegment(temp, 1, 1, storedPhotons); + photons = temp; + halfStoredPhotons = storedPhotons / 2; + log2n = (int) Math.ceil(Math.log(storedPhotons) / Math.log(2.0)); + } + + private void balanceSegment(Photon[] temp, int index, int start, int end) { + int median = 1; + while ((4 * median) <= (end - start + 1)) + median += median; + if ((3 * median) <= (end - start + 1)) { + median += median; + median += (start - 1); + } else + median = end - median + 1; + int axis = Photon.SPLIT_Z; + Vector3 extents = bounds.getExtents(); + if ((extents.x > extents.y) && (extents.x > extents.z)) + axis = Photon.SPLIT_X; + else if (extents.y > extents.z) + axis = Photon.SPLIT_Y; + int left = start; + int right = end; + while (right > left) { + double v = photons[right].getCoord(axis); + int i = left - 1; + int j = right; + while (true) { + while (photons[++i].getCoord(axis) < v) { + } + while ((photons[--j].getCoord(axis) > v) && (j > left)) { + } + if (i >= j) + break; + swap(i, j); + } + swap(i, right); + if (i >= median) + right = i - 1; + if (i <= median) + left = i + 1; + } + temp[index] = photons[median]; + temp[index].setSplitAxis(axis); + if (median > start) { + if (start < (median - 1)) { + float tmp; + switch (axis) { + case Photon.SPLIT_X: + tmp = bounds.getMaximum().x; + bounds.getMaximum().x = temp[index].x; + balanceSegment(temp, 2 * index, start, median - 1); + bounds.getMaximum().x = tmp; + break; + case Photon.SPLIT_Y: + tmp = bounds.getMaximum().y; + bounds.getMaximum().y = temp[index].y; + balanceSegment(temp, 2 * index, start, median - 1); + bounds.getMaximum().y = tmp; + break; + default: + tmp = bounds.getMaximum().z; + bounds.getMaximum().z = temp[index].z; + balanceSegment(temp, 2 * index, start, median - 1); + bounds.getMaximum().z = tmp; + } + } else + temp[2 * index] = photons[start]; + } + if (median < end) { + if ((median + 1) < end) { + float tmp; + switch (axis) { + case Photon.SPLIT_X: + tmp = bounds.getMinimum().x; + bounds.getMinimum().x = temp[index].x; + balanceSegment(temp, (2 * index) + 1, median + 1, end); + bounds.getMinimum().x = tmp; + break; + case Photon.SPLIT_Y: + tmp = bounds.getMinimum().y; + bounds.getMinimum().y = temp[index].y; + balanceSegment(temp, (2 * index) + 1, median + 1, end); + bounds.getMinimum().y = tmp; + break; + default: + tmp = bounds.getMinimum().z; + bounds.getMinimum().z = temp[index].z; + balanceSegment(temp, (2 * index) + 1, median + 1, end); + bounds.getMinimum().z = tmp; + } + } else + temp[(2 * index) + 1] = photons[end]; + } + } + + private void swap(int i, int j) { + Photon tmp = photons[i]; + photons[i] = photons[j]; + photons[j] = tmp; + } + + static class Photon { + float x; + float y; + float z; + short dir; + short normal; + int data; + int power; + int flags; + + static final int SPLIT_X = 0; + static final int SPLIT_Y = 1; + static final int SPLIT_Z = 2; + static final int SPLIT_MASK = 3; + + Photon(Point3 p, Vector3 n, Vector3 dir, Color power, Color diffuse) { + x = p.x; + y = p.y; + z = p.z; + this.dir = dir.encode(); + this.power = power.toRGBE(); + flags = 0; + normal = n.encode(); + data = diffuse.toRGB(); + } + + void setSplitAxis(int axis) { + flags &= ~SPLIT_MASK; + flags |= axis; + } + + float getCoord(int axis) { + switch (axis) { + case SPLIT_X: + return x; + case SPLIT_Y: + return y; + default: + return z; + } + } + + float getDist1(float px, float py, float pz) { + switch (flags & SPLIT_MASK) { + case SPLIT_X: + return px - x; + case SPLIT_Y: + return py - y; + default: + return pz - z; + } + } + + float getDist2(float px, float py, float pz) { + float dx = x - px; + float dy = y - py; + float dz = z - pz; + return (dx * dx) + (dy * dy) + (dz * dz); + } + } + + public void init() { + UI.printInfo(Module.LIGHT, "Balancing global photon map ..."); + UI.taskStart("Balancing global photon map", 0, 1); + Timer t = new Timer(); + t.start(); + balance(); + t.end(); + UI.taskStop(); + UI.printInfo(Module.LIGHT, "Global photon map:"); + UI.printInfo(Module.LIGHT, " * Photons stored: %d", storedPhotons); + UI.printInfo(Module.LIGHT, " * Photons/estimate: %d", numGather); + UI.printInfo(Module.LIGHT, " * Estimate radius: %.3f", gatherRadius); + maxRadius = 1.4f * (float) Math.sqrt(maxPower * numGather); + UI.printInfo(Module.LIGHT, " * Maximum radius: %.3f", maxRadius); + UI.printInfo(Module.LIGHT, " * Balancing time: %s", t.toString()); + if (gatherRadius > maxRadius) + gatherRadius = maxRadius; + t.start(); + precomputeRadiance(); + t.end(); + UI.printInfo(Module.LIGHT, " * Precompute time: %s", t.toString()); + UI.printInfo(Module.LIGHT, " * Radiance photons: %d", storedPhotons); + UI.printInfo(Module.LIGHT, " * Search radius: %.3f", gatherRadius); + } + + public void precomputeRadiance() { + if (storedPhotons == 0) + return; + // precompute the radiance for all photons that are neither + // leaves nor parents of leaves in the tree. + int quadStoredPhotons = halfStoredPhotons / 2; + Point3 p = new Point3(); + Vector3 n = new Vector3(); + Point3 ppos = new Point3(); + Vector3 pdir = new Vector3(); + Vector3 pvec = new Vector3(); + Color irr = new Color(); + Color pow = new Color(); + float maxDist2 = gatherRadius * gatherRadius; + NearestPhotons np = new NearestPhotons(p, numGather, maxDist2); + Photon[] temp = new Photon[quadStoredPhotons + 1]; + UI.taskStart("Precomputing radiance", 1, quadStoredPhotons); + for (int i = 1; i <= quadStoredPhotons; i++) { + UI.taskUpdate(i); + Photon curr = photons[i]; + p.set(curr.x, curr.y, curr.z); + Vector3.decode(curr.normal, n); + irr.set(Color.BLACK); + np.reset(p, maxDist2); + locatePhotons(np); + if (np.found < 8) { + curr.data = 0; + temp[i] = curr; + continue; + } + float invArea = 1.0f / ((float) Math.PI * np.dist2[0]); + float maxNDist = np.dist2[0] * 0.05f; + for (int j = 1; j <= np.found; j++) { + Photon phot = np.index[j]; + Vector3.decode(phot.dir, pdir); + float cos = -Vector3.dot(pdir, n); + if (cos > 0.01f) { + ppos.set(phot.x, phot.y, phot.z); + Point3.sub(ppos, p, pvec); + float pcos = Vector3.dot(pvec, n); + if ((pcos < maxNDist) && (pcos > -maxNDist)) + irr.add(pow.setRGBE(phot.power)); + } + } + irr.mul(invArea); + // compute radiance + irr.mul(new Color(curr.data)).mul(1.0f / (float) Math.PI); + curr.data = irr.toRGBE(); + temp[i] = curr; + } + UI.taskStop(); + + // resize photon map to only include irradiance photons + numGather /= 4; + maxRadius = 1.4f * (float) Math.sqrt(maxPower * numGather); + if (gatherRadius > maxRadius) + gatherRadius = maxRadius; + storedPhotons = quadStoredPhotons; + halfStoredPhotons = storedPhotons / 2; + log2n = (int) Math.ceil(Math.log(storedPhotons) / Math.log(2.0)); + photons = temp; + hasRadiance = true; + } + + public Color getRadiance(Point3 p, Vector3 n) { + if (!hasRadiance || (storedPhotons == 0)) + return Color.BLACK; + float px = p.x; + float py = p.y; + float pz = p.z; + int i = 1; + int level = 0; + int cameFrom; + float dist2; + float maxDist2 = gatherRadius * gatherRadius; + Photon nearest = null; + Photon curr; + Vector3 photN = new Vector3(); + float[] dist1d2 = new float[log2n]; + int[] chosen = new int[log2n]; + while (true) { + while (i < halfStoredPhotons) { + float dist1d = photons[i].getDist1(px, py, pz); + dist1d2[level] = dist1d * dist1d; + i += i; + if (dist1d > 0) + i++; + chosen[level++] = i; + } + curr = photons[i]; + dist2 = curr.getDist2(px, py, pz); + if (dist2 < maxDist2) { + Vector3.decode(curr.normal, photN); + float currentDotN = Vector3.dot(photN, n); + if (currentDotN > 0.9f) { + nearest = curr; + maxDist2 = dist2; + } + } + do { + cameFrom = i; + i >>= 1; + level--; + if (i == 0) + return (nearest == null) ? Color.BLACK : new Color().setRGBE(nearest.data); + } while ((dist1d2[level] >= maxDist2) || (cameFrom != chosen[level])); + curr = photons[i]; + dist2 = curr.getDist2(px, py, pz); + if (dist2 < maxDist2) { + Vector3.decode(curr.normal, photN); + float currentDotN = Vector3.dot(photN, n); + if (currentDotN > 0.9f) { + nearest = curr; + maxDist2 = dist2; + } + } + i = chosen[level++] ^ 1; + } + } + + private static class NearestPhotons { + int found; + float px, py, pz; + private int max; + private boolean gotHeap; + protected float[] dist2; + protected Photon[] index; + + NearestPhotons(Point3 p, int n, float maxDist2) { + max = n; + found = 0; + gotHeap = false; + px = p.x; + py = p.y; + pz = p.z; + dist2 = new float[n + 1]; + index = new Photon[n + 1]; + dist2[0] = maxDist2; + } + + void reset(Point3 p, float maxDist2) { + found = 0; + gotHeap = false; + px = p.x; + py = p.y; + pz = p.z; + dist2[0] = maxDist2; + } + + void checkAddNearest(Photon p) { + float fdist2 = p.getDist2(px, py, pz); + if (fdist2 < dist2[0]) { + if (found < max) { + found++; + dist2[found] = fdist2; + index[found] = p; + } else { + int j; + int parent; + if (!gotHeap) { + float dst2; + Photon phot; + int halfFound = found >> 1; + for (int k = halfFound; k >= 1; k--) { + parent = k; + phot = index[k]; + dst2 = dist2[k]; + while (parent <= halfFound) { + j = parent + parent; + if ((j < found) && (dist2[j] < dist2[j + 1])) + j++; + if (dst2 >= dist2[j]) + break; + dist2[parent] = dist2[j]; + index[parent] = index[j]; + parent = j; + } + dist2[parent] = dst2; + index[parent] = phot; + } + gotHeap = true; + } + parent = 1; + j = 2; + while (j <= found) { + if ((j < found) && (dist2[j] < dist2[j + 1])) + j++; + if (fdist2 > dist2[j]) + break; + dist2[parent] = dist2[j]; + index[parent] = index[j]; + parent = j; + j += j; + } + dist2[parent] = fdist2; + index[parent] = p; + dist2[0] = dist2[1]; + } + } + } + } + + public boolean allowDiffuseBounced() { + return true; + } + + public boolean allowReflectionBounced() { + return true; + } + + public boolean allowRefractionBounced() { + return true; + } + + public int numEmit() { + return numEmit; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/photonmap/GridPhotonMap.java b/src/main/java/org/sunflow/core/photonmap/GridPhotonMap.java new file mode 100644 index 0000000..e7590ab --- /dev/null +++ b/src/main/java/org/sunflow/core/photonmap/GridPhotonMap.java @@ -0,0 +1,282 @@ +package org.sunflow.core.photonmap; + +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.sunflow.core.GlobalPhotonMapInterface; +import org.sunflow.core.Options; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.MathUtils; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class GridPhotonMap implements GlobalPhotonMapInterface { + private int numGather; + private float gatherRadius; + private int numStoredPhotons; + private int nx, ny, nz; + private BoundingBox bounds; + private PhotonGroup[] cellHash; + private int hashSize; + private int hashPrime; + private ReentrantReadWriteLock rwl; + private int numEmit; + + private static final float NORMAL_THRESHOLD = (float) Math.cos(10.0 * Math.PI / 180.0); + private static final int[] PRIMES = { 11, 19, 37, 109, 163, 251, 367, 557, + 823, 1237, 1861, 2777, 4177, 6247, 9371, 21089, 31627, 47431, + 71143, 106721, 160073, 240101, 360163, 540217, 810343, 1215497, + 1823231, 2734867, 4102283, 6153409, 9230113, 13845163 }; + + public GridPhotonMap() { + numStoredPhotons = 0; + hashSize = 0; // number of unique IDs in the hash + rwl = new ReentrantReadWriteLock(); + numEmit = 100000; + } + + public void prepare(Options options, BoundingBox sceneBounds) { + // get settings + numEmit = options.getInt("gi.irr-cache.gmap.emit", 100000); + numGather = options.getInt("gi.irr-cache.gmap.gather", 50); + gatherRadius = options.getFloat("gi.irr-cache.gmap.radius", 0.5f); + + bounds = new BoundingBox(sceneBounds); + bounds.enlargeUlps(); + Vector3 w = bounds.getExtents(); + nx = (int) Math.max(((w.x / gatherRadius) + 0.5f), 1); + ny = (int) Math.max(((w.y / gatherRadius) + 0.5f), 1); + nz = (int) Math.max(((w.z / gatherRadius) + 0.5f), 1); + int numCells = nx * ny * nz; + UI.printInfo(Module.LIGHT, "Initializing grid photon map:"); + UI.printInfo(Module.LIGHT, " * Resolution: %dx%dx%d", nx, ny, nz); + UI.printInfo(Module.LIGHT, " * Total cells: %d", numCells); + for (hashPrime = 0; hashPrime < PRIMES.length; hashPrime++) + if (PRIMES[hashPrime] > (numCells / 5)) + break; + cellHash = new PhotonGroup[PRIMES[hashPrime]]; + UI.printInfo(Module.LIGHT, " * Initial hash size: %d", cellHash.length); + } + + public int size() { + return numStoredPhotons; + } + + public void store(ShadingState state, Vector3 dir, Color power, Color diffuse) { + // don't store on the wrong side of a surface + if (Vector3.dot(state.getNormal(), dir) > 0) + return; + Point3 pt = state.getPoint(); + // outside grid bounds ? + if (!bounds.contains(pt)) + return; + Vector3 ext = bounds.getExtents(); + int ix = (int) (((pt.x - bounds.getMinimum().x) * nx) / ext.x); + int iy = (int) (((pt.y - bounds.getMinimum().y) * ny) / ext.y); + int iz = (int) (((pt.z - bounds.getMinimum().z) * nz) / ext.z); + ix = MathUtils.clamp(ix, 0, nx - 1); + iy = MathUtils.clamp(iy, 0, ny - 1); + iz = MathUtils.clamp(iz, 0, nz - 1); + int id = ix + iy * nx + iz * nx * ny; + synchronized (this) { + int hid = id % cellHash.length; + PhotonGroup g = cellHash[hid]; + PhotonGroup last = null; + boolean hasID = false; + while (g != null) { + if (g.id == id) { + hasID = true; + if (Vector3.dot(state.getNormal(), g.normal) > NORMAL_THRESHOLD) + break; + } + last = g; + g = g.next; + } + if (g == null) { + g = new PhotonGroup(id, state.getNormal()); + if (last == null) + cellHash[hid] = g; + else + last.next = g; + if (!hasID) { + hashSize++; // we have not seen this ID before + // resize hash if we have grown too large + if (hashSize > cellHash.length) + growPhotonHash(); + } + } + g.count++; + g.flux.add(power); + g.diffuse.add(diffuse); + numStoredPhotons++; + } + } + + public void init() { + UI.printInfo(Module.LIGHT, "Initializing photon grid ..."); + UI.printInfo(Module.LIGHT, " * Photon hits: %d", numStoredPhotons); + UI.printInfo(Module.LIGHT, " * Final hash size: %d", cellHash.length); + int cells = 0; + for (int i = 0; i < cellHash.length; i++) { + for (PhotonGroup g = cellHash[i]; g != null; g = g.next) { + g.diffuse.mul(1.0f / g.count); + cells++; + } + } + UI.printInfo(Module.LIGHT, " * Num photon cells: %d", cells); + } + + public void precomputeRadiance(boolean includeDirect, boolean includeCaustics) { + } + + private void growPhotonHash() { + // enlarge the hash size: + if (hashPrime >= PRIMES.length - 1) + return; + PhotonGroup[] temp = new PhotonGroup[PRIMES[++hashPrime]]; + for (int i = 0; i < cellHash.length; i++) { + PhotonGroup g = cellHash[i]; + while (g != null) { + // re-hash into the new table + int hid = g.id % temp.length; + PhotonGroup last = null; + for (PhotonGroup gn = temp[hid]; gn != null; gn = gn.next) + last = gn; + if (last == null) + temp[hid] = g; + else + last.next = g; + PhotonGroup next = g.next; + g.next = null; + g = next; + } + } + cellHash = temp; + } + + public synchronized Color getRadiance(Point3 p, Vector3 n) { + if (!bounds.contains(p)) + return Color.BLACK; + Vector3 ext = bounds.getExtents(); + int ix = (int) (((p.x - bounds.getMinimum().x) * nx) / ext.x); + int iy = (int) (((p.y - bounds.getMinimum().y) * ny) / ext.y); + int iz = (int) (((p.z - bounds.getMinimum().z) * nz) / ext.z); + ix = MathUtils.clamp(ix, 0, nx - 1); + iy = MathUtils.clamp(iy, 0, ny - 1); + iz = MathUtils.clamp(iz, 0, nz - 1); + int id = ix + iy * nx + iz * nx * ny; + rwl.readLock().lock(); + PhotonGroup center = null; + for (PhotonGroup g = get(ix, iy, iz); g != null; g = g.next) { + if (g.id == id && Vector3.dot(n, g.normal) > NORMAL_THRESHOLD) { + if (g.radiance == null) { + center = g; + break; + } + Color r = g.radiance.copy(); + rwl.readLock().unlock(); + return r; + } + } + int vol = 1; + while (true) { + int numPhotons = 0; + int ndiff = 0; + Color irr = Color.black(); + Color diff = (center == null) ? Color.black() : null; + for (int z = iz - (vol - 1); z <= iz + (vol - 1); z++) { + for (int y = iy - (vol - 1); y <= iy + (vol - 1); y++) { + for (int x = ix - (vol - 1); x <= ix + (vol - 1); x++) { + int vid = x + y * nx + z * nx * ny; + for (PhotonGroup g = get(x, y, z); g != null; g = g.next) { + if (g.id == vid && Vector3.dot(n, g.normal) > NORMAL_THRESHOLD) { + numPhotons += g.count; + irr.add(g.flux); + if (diff != null) { + diff.add(g.diffuse); + ndiff++; + } + break; // only one valid group can be found, + // skip the others + } + } + } + } + } + if (numPhotons >= numGather || vol >= 3) { + // we have found enough photons + // cache irradiance and return + float area = (2 * vol - 1) / 3.0f * ((ext.x / nx) + (ext.y / ny) + (ext.z / nz)); + area *= area; + area *= Math.PI; + irr.mul(1.0f / area); + // upgrade lock manually + rwl.readLock().unlock(); + rwl.writeLock().lock(); + if (center == null) { + if (ndiff > 0) + diff.mul(1.0f / ndiff); + center = new PhotonGroup(id, n); + center.diffuse.set(diff); + center.next = cellHash[id % cellHash.length]; + cellHash[id % cellHash.length] = center; + } + irr.mul(center.diffuse); + center.radiance = irr.copy(); + rwl.writeLock().unlock(); // unlock write - done + return irr; + } + vol++; + } + } + + private PhotonGroup get(int x, int y, int z) { + // returns the list associated with the specified location + if (x < 0 || x >= nx) + return null; + if (y < 0 || y >= ny) + return null; + if (z < 0 || z >= nz) + return null; + return cellHash[(x + y * nx + z * nx * ny) % cellHash.length]; + } + + private class PhotonGroup { + int id; + int count; + Vector3 normal; + Color flux; + Color radiance; + Color diffuse; + PhotonGroup next; + + PhotonGroup(int id, Vector3 n) { + normal = new Vector3(n); + flux = Color.black(); + diffuse = Color.black(); + radiance = null; + count = 0; + this.id = id; + next = null; + } + } + + public boolean allowDiffuseBounced() { + return true; + } + + public boolean allowReflectionBounced() { + return true; + } + + public boolean allowRefractionBounced() { + return true; + } + + public int numEmit() { + return numEmit; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/Background.java b/src/main/java/org/sunflow/core/primitive/Background.java new file mode 100644 index 0000000..a2930ac --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/Background.java @@ -0,0 +1,45 @@ +package org.sunflow.core.primitive; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; + +public class Background implements PrimitiveList { + public Background() { + } + + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public void prepareShadingState(ShadingState state) { + if (state.getDepth() == 0) + state.setShader(state.getInstance().getShader(0)); + } + + public int getNumPrimitives() { + return 1; + } + + public float getPrimitiveBound(int primID, int i) { + return 0; + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + return null; + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + if (r.getMax() == Float.POSITIVE_INFINITY) + state.setIntersection(0); + } + + public PrimitiveList getBakingPrimitives() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/BanchoffSurface.java b/src/main/java/org/sunflow/core/primitive/BanchoffSurface.java new file mode 100644 index 0000000..d1bb19e --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/BanchoffSurface.java @@ -0,0 +1,90 @@ +package org.sunflow.core.primitive; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Solvers; +import org.sunflow.math.Vector3; + +public class BanchoffSurface implements PrimitiveList { + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(1.5f); + if (o2w != null) + bounds = o2w.transform(bounds); + return bounds; + } + + public float getPrimitiveBound(int primID, int i) { + return (i & 1) == 0 ? -1.5f : 1.5f; + } + + public int getNumPrimitives() { + return 1; + } + + public void prepareShadingState(ShadingState state) { + state.init(); + state.getRay().getPoint(state.getPoint()); + Instance parent = state.getInstance(); + Point3 n = state.transformWorldToObject(state.getPoint()); + state.getNormal().set(n.x * (2 * n.x * n.x - 1), n.y * (2 * n.y * n.y - 1), n.z * (2 * n.z * n.z - 1)); + state.getNormal().normalize(); + state.setShader(parent.getShader(0)); + state.setModifier(parent.getModifier(0)); + // into world space + Vector3 worldNormal = state.transformNormalObjectToWorld(state.getNormal()); + state.getNormal().set(worldNormal); + state.getNormal().normalize(); + state.getGeoNormal().set(state.getNormal()); + // create basis in world space + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + // intersect in local space + float rd2x = r.dx * r.dx; + float rd2y = r.dy * r.dy; + float rd2z = r.dz * r.dz; + float ro2x = r.ox * r.ox; + float ro2y = r.oy * r.oy; + float ro2z = r.oz * r.oz; + // setup the quartic coefficients + // some common terms could probably be shared across these + double A = (rd2y * rd2y + rd2z * rd2z + rd2x * rd2x); + double B = 4 * (r.oy * rd2y * r.dy + r.oz * r.dz * rd2z + r.ox * r.dx * rd2x); + double C = (-rd2x - rd2y - rd2z + 6 * (ro2y * rd2y + ro2z * rd2z + ro2x * rd2x)); + double D = 2 * (2 * ro2z * r.oz * r.dz - r.oz * r.dz + 2 * ro2x * r.ox * r.dx + 2 * ro2y * r.oy * r.dy - r.ox * r.dx - r.oy * r.dy); + double E = 3.0f / 8.0f + (-ro2z + ro2z * ro2z - ro2y + ro2y * ro2y - ro2x + ro2x * ro2x); + // solve equation + double[] t = Solvers.solveQuartic(A, B, C, D, E); + if (t != null) { + // early rejection + if (t[0] >= r.getMax() || t[t.length - 1] <= r.getMin()) + return; + // find first intersection in front of the ray + for (int i = 0; i < t.length; i++) { + if (t[i] > r.getMin()) { + r.setMax((float) t[i]); + state.setIntersection(0); + return; + } + } + } + } + + public PrimitiveList getBakingPrimitives() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/Box.java b/src/main/java/org/sunflow/core/primitive/Box.java new file mode 100644 index 0000000..3964631 --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/Box.java @@ -0,0 +1,197 @@ +package org.sunflow.core.primitive; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.core.ParameterList.FloatParameter; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Vector3; + +public class Box implements PrimitiveList { + private float minX, minY, minZ; + private float maxX, maxY, maxZ; + + public Box() { + minX = minY = minZ = -1; + maxX = maxY = maxZ = +1; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + FloatParameter pts = pl.getPointArray("points"); + if (pts != null) { + BoundingBox bounds = new BoundingBox(); + for (int i = 0; i < pts.data.length; i += 3) + bounds.include(pts.data[i], pts.data[i + 1], pts.data[i + 2]); + // cube extents + minX = bounds.getMinimum().x; + minY = bounds.getMinimum().y; + minZ = bounds.getMinimum().z; + maxX = bounds.getMaximum().x; + maxY = bounds.getMaximum().y; + maxZ = bounds.getMaximum().z; + } + return true; + } + + public void prepareShadingState(ShadingState state) { + state.init(); + state.getRay().getPoint(state.getPoint()); + int n = state.getPrimitiveID(); + switch (n) { + case 0: + state.getNormal().set(new Vector3(1, 0, 0)); + break; + case 1: + state.getNormal().set(new Vector3(-1, 0, 0)); + break; + case 2: + state.getNormal().set(new Vector3(0, 1, 0)); + break; + case 3: + state.getNormal().set(new Vector3(0, -1, 0)); + break; + case 4: + state.getNormal().set(new Vector3(0, 0, 1)); + break; + case 5: + state.getNormal().set(new Vector3(0, 0, -1)); + break; + default: + state.getNormal().set(new Vector3(0, 0, 0)); + break; + } + state.getGeoNormal().set(state.getNormal()); + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + state.setShader(state.getInstance().getShader(0)); + state.setModifier(state.getInstance().getModifier(0)); + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + float intervalMin = Float.NEGATIVE_INFINITY; + float intervalMax = Float.POSITIVE_INFINITY; + float orgX = r.ox; + float invDirX = 1 / r.dx; + float t1, t2; + t1 = (minX - orgX) * invDirX; + t2 = (maxX - orgX) * invDirX; + int sideIn = -1, sideOut = -1; + if (invDirX > 0) { + if (t1 > intervalMin) { + intervalMin = t1; + sideIn = 0; + } + if (t2 < intervalMax) { + intervalMax = t2; + sideOut = 1; + } + } else { + if (t2 > intervalMin) { + intervalMin = t2; + sideIn = 1; + } + if (t1 < intervalMax) { + intervalMax = t1; + sideOut = 0; + } + } + if (intervalMin > intervalMax) + return; + float orgY = r.oy; + float invDirY = 1 / r.dy; + t1 = (minY - orgY) * invDirY; + t2 = (maxY - orgY) * invDirY; + if (invDirY > 0) { + if (t1 > intervalMin) { + intervalMin = t1; + sideIn = 2; + } + if (t2 < intervalMax) { + intervalMax = t2; + sideOut = 3; + } + } else { + if (t2 > intervalMin) { + intervalMin = t2; + sideIn = 3; + } + if (t1 < intervalMax) { + intervalMax = t1; + sideOut = 2; + } + } + if (intervalMin > intervalMax) + return; + float orgZ = r.oz; + float invDirZ = 1 / r.dz; + t1 = (minZ - orgZ) * invDirZ; // no front wall + t2 = (maxZ - orgZ) * invDirZ; + if (invDirZ > 0) { + if (t1 > intervalMin) { + intervalMin = t1; + sideIn = 4; + } + if (t2 < intervalMax) { + intervalMax = t2; + sideOut = 5; + } + } else { + if (t2 > intervalMin) { + intervalMin = t2; + sideIn = 5; + } + if (t1 < intervalMax) { + intervalMax = t1; + sideOut = 4; + } + } + if (intervalMin > intervalMax) + return; + if (r.isInside(intervalMin)) { + r.setMax(intervalMin); + state.setIntersection(sideIn); + } else if (r.isInside(intervalMax)) { + r.setMax(intervalMax); + state.setIntersection(sideOut); + } + } + + public int getNumPrimitives() { + return 1; + } + + public float getPrimitiveBound(int primID, int i) { + switch (i) { + case 0: + return minX; + case 1: + return maxX; + case 2: + return minY; + case 3: + return maxY; + case 4: + return minZ; + case 5: + return maxZ; + default: + return 0; + } + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(minX, minY, minZ); + bounds.include(maxX, maxY, maxZ); + if (o2w == null) + return bounds; + return o2w.transform(bounds); + } + + public PrimitiveList getBakingPrimitives() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/CornellBox.java b/src/main/java/org/sunflow/core/primitive/CornellBox.java new file mode 100644 index 0000000..6b2c88c --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/CornellBox.java @@ -0,0 +1,446 @@ +package org.sunflow.core.primitive; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.LightSample; +import org.sunflow.core.LightSource; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class CornellBox implements PrimitiveList, Shader, LightSource { + private float minX, minY, minZ; + private float maxX, maxY, maxZ; + private Color left, right, top, bottom, back; + private Color radiance; + private int samples; + private float lxmin, lymin, lxmax, lymax; + private float area; + private BoundingBox lightBounds; + + public CornellBox() { + updateGeometry(new Point3(-1, -1, -1), new Point3(1, 1, 1)); + + // cube colors + left = new Color(0.80f, 0.25f, 0.25f); + right = new Color(0.25f, 0.25f, 0.80f); + Color gray = new Color(0.70f, 0.70f, 0.70f); + top = bottom = back = gray; + + // light source + radiance = Color.WHITE; + samples = 16; + } + + private void updateGeometry(Point3 c0, Point3 c1) { + // figure out cube extents + lightBounds = new BoundingBox(c0); + lightBounds.include(c1); + + // cube extents + minX = lightBounds.getMinimum().x; + minY = lightBounds.getMinimum().y; + minZ = lightBounds.getMinimum().z; + maxX = lightBounds.getMaximum().x; + maxY = lightBounds.getMaximum().y; + maxZ = lightBounds.getMaximum().z; + + // work around epsilon problems for light test + lightBounds.enlargeUlps(); + + // light source geometry + lxmin = maxX / 3 + 2 * minX / 3; + lxmax = minX / 3 + 2 * maxX / 3; + lymin = maxY / 3 + 2 * minY / 3; + lymax = minY / 3 + 2 * maxY / 3; + area = (lxmax - lxmin) * (lymax - lymin); + } + + public boolean update(ParameterList pl, SunflowAPI api) { + Point3 corner0 = pl.getPoint("corner0", null); + Point3 corner1 = pl.getPoint("corner1", null); + if (corner0 != null && corner1 != null) { + updateGeometry(corner0, corner1); + } + + // shader colors + left = pl.getColor("leftColor", left); + right = pl.getColor("rightColor", right); + top = pl.getColor("topColor", top); + bottom = pl.getColor("bottomColor", bottom); + back = pl.getColor("backColor", back); + + // light + radiance = pl.getColor("radiance", radiance); + samples = pl.getInt("samples", samples); + return true; + } + + public BoundingBox getBounds() { + return lightBounds; + } + + public float getBound(int i) { + switch (i) { + case 0: + return minX; + case 1: + return maxX; + case 2: + return minY; + case 3: + return maxY; + case 4: + return minZ; + case 5: + return maxZ; + default: + return 0; + } + } + + public boolean intersects(BoundingBox box) { + // this could be optimized + BoundingBox b = new BoundingBox(); + b.include(new Point3(minX, minY, minZ)); + b.include(new Point3(maxX, maxY, maxZ)); + if (b.intersects(box)) { + // the box is overlapping or enclosed + if (!b.contains(new Point3(box.getMinimum().x, box.getMinimum().y, box.getMinimum().z))) + return true; + if (!b.contains(new Point3(box.getMinimum().x, box.getMinimum().y, box.getMaximum().z))) + return true; + if (!b.contains(new Point3(box.getMinimum().x, box.getMaximum().y, box.getMinimum().z))) + return true; + if (!b.contains(new Point3(box.getMinimum().x, box.getMaximum().y, box.getMaximum().z))) + return true; + if (!b.contains(new Point3(box.getMaximum().x, box.getMinimum().y, box.getMinimum().z))) + return true; + if (!b.contains(new Point3(box.getMaximum().x, box.getMinimum().y, box.getMaximum().z))) + return true; + if (!b.contains(new Point3(box.getMaximum().x, box.getMaximum().y, box.getMinimum().z))) + return true; + if (!b.contains(new Point3(box.getMaximum().x, box.getMaximum().y, box.getMaximum().z))) + return true; + // all vertices of the box are inside - the surface of the box is + // not intersected + } + return false; + } + + public void prepareShadingState(ShadingState state) { + state.init(); + state.getRay().getPoint(state.getPoint()); + int n = state.getPrimitiveID(); + switch (n) { + case 0: + state.getNormal().set(new Vector3(1, 0, 0)); + break; + case 1: + state.getNormal().set(new Vector3(-1, 0, 0)); + break; + case 2: + state.getNormal().set(new Vector3(0, 1, 0)); + break; + case 3: + state.getNormal().set(new Vector3(0, -1, 0)); + break; + case 4: + state.getNormal().set(new Vector3(0, 0, 1)); + break; + case 5: + state.getNormal().set(new Vector3(0, 0, -1)); + break; + default: + state.getNormal().set(new Vector3(0, 0, 0)); + break; + } + state.getGeoNormal().set(state.getNormal()); + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + state.setShader(this); + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + float intervalMin = Float.NEGATIVE_INFINITY; + float intervalMax = Float.POSITIVE_INFINITY; + float orgX = r.ox; + float invDirX = 1 / r.dx; + float t1, t2; + t1 = (minX - orgX) * invDirX; + t2 = (maxX - orgX) * invDirX; + int sideIn = -1, sideOut = -1; + if (invDirX > 0) { + if (t1 > intervalMin) { + intervalMin = t1; + sideIn = 0; + } + if (t2 < intervalMax) { + intervalMax = t2; + sideOut = 1; + } + } else { + if (t2 > intervalMin) { + intervalMin = t2; + sideIn = 1; + } + if (t1 < intervalMax) { + intervalMax = t1; + sideOut = 0; + } + } + if (intervalMin > intervalMax) + return; + float orgY = r.oy; + float invDirY = 1 / r.dy; + t1 = (minY - orgY) * invDirY; + t2 = (maxY - orgY) * invDirY; + if (invDirY > 0) { + if (t1 > intervalMin) { + intervalMin = t1; + sideIn = 2; + } + if (t2 < intervalMax) { + intervalMax = t2; + sideOut = 3; + } + } else { + if (t2 > intervalMin) { + intervalMin = t2; + sideIn = 3; + } + if (t1 < intervalMax) { + intervalMax = t1; + sideOut = 2; + } + } + if (intervalMin > intervalMax) + return; + float orgZ = r.oz; + float invDirZ = 1 / r.dz; + t1 = (minZ - orgZ) * invDirZ; // no front wall + t2 = (maxZ - orgZ) * invDirZ; + if (invDirZ > 0) { + if (t1 > intervalMin) { + intervalMin = t1; + sideIn = 4; + } + if (t2 < intervalMax) { + intervalMax = t2; + sideOut = 5; + } + } else { + if (t2 > intervalMin) { + intervalMin = t2; + sideIn = 5; + } + if (t1 < intervalMax) { + intervalMax = t1; + sideOut = 4; + } + } + if (intervalMin > intervalMax) + return; + assert sideIn != -1; + assert sideOut != -1; + // can't hit minY wall, there is none + if (sideIn != 2 && r.isInside(intervalMin)) { + r.setMax(intervalMin); + state.setIntersection(sideIn); + } else if (sideOut != 2 && r.isInside(intervalMax)) { + r.setMax(intervalMax); + state.setIntersection(sideOut); + } + } + + public Color getRadiance(ShadingState state) { + int side = state.getPrimitiveID(); + Color kd = null; + switch (side) { + case 0: + kd = left; + break; + case 1: + kd = right; + break; + case 3: + kd = back; + break; + case 4: + kd = bottom; + break; + case 5: + float lx = state.getPoint().x; + float ly = state.getPoint().y; + if (lx >= lxmin && lx < lxmax && ly >= lymin && ly < lymax && state.getRay().dz > 0) + return state.includeLights() ? radiance : Color.BLACK; + kd = top; + break; + default: + assert false; + } + // make sure we are on the right side of the material + state.faceforward(); + // setup lighting + state.initLightSamples(); + state.initCausticSamples(); + return state.diffuse(kd); + } + + public void scatterPhoton(ShadingState state, Color power) { + int side = state.getPrimitiveID(); + Color kd = null; + switch (side) { + case 0: + kd = left; + break; + case 1: + kd = right; + break; + case 3: + kd = back; + break; + case 4: + kd = bottom; + break; + case 5: + float lx = state.getPoint().x; + float ly = state.getPoint().y; + if (lx >= lxmin && lx < lxmax && ly >= lymin && ly < lymax && state.getRay().dz > 0) + return; + kd = top; + break; + default: + assert false; + } + // make sure we are on the right side of the material + if (Vector3.dot(state.getNormal(), state.getRay().getDirection()) > 0) { + state.getNormal().negate(); + state.getGeoNormal().negate(); + } + state.storePhoton(state.getRay().getDirection(), power, kd); + double avg = kd.getAverage(); + double rnd = state.getRandom(0, 0, 1); + if (rnd < avg) { + // photon is scattered + power.mul(kd).mul(1 / (float) avg); + OrthoNormalBasis onb = OrthoNormalBasis.makeFromW(state.getNormal()); + double u = 2 * Math.PI * rnd / avg; + double v = state.getRandom(0, 1, 1); + float s = (float) Math.sqrt(v); + float s1 = (float) Math.sqrt(1.0 - v); + Vector3 w = new Vector3((float) Math.cos(u) * s, (float) Math.sin(u) * s, s1); + w = onb.transform(w, new Vector3()); + state.traceDiffusePhoton(new Ray(state.getPoint(), w), power); + } + } + + public int getNumSamples() { + return samples; + } + + public void getSamples(ShadingState state) { + if (lightBounds.contains(state.getPoint()) && state.getPoint().z < maxZ) { + int n = state.getDiffuseDepth() > 0 ? 1 : samples; + float a = area / n; + for (int i = 0; i < n; i++) { + // random offset on unit square + double randX = state.getRandom(i, 0, n); + double randY = state.getRandom(i, 1, n); + Point3 p = new Point3(); + p.x = (float) (lxmin * (1 - randX) + lxmax * randX); + p.y = (float) (lymin * (1 - randY) + lymax * randY); + p.z = maxZ - 0.001f; + + LightSample dest = new LightSample(); + // prepare shadow ray to sampled point + dest.setShadowRay(new Ray(state.getPoint(), p)); + + // check that the direction of the sample is the same as the + // normal + float cosNx = dest.dot(state.getNormal()); + if (cosNx <= 0) + return; + + // light source facing point ? + // (need to check with light source's normal) + float cosNy = dest.getShadowRay().dz; + if (cosNy > 0) { + // compute geometric attenuation and probability scale + // factor + float r = dest.getShadowRay().getMax(); + float g = cosNy / (r * r); + float scale = g * a; + // set final sample radiance + dest.setRadiance(radiance, radiance); + dest.getDiffuseRadiance().mul(scale); + dest.getSpecularRadiance().mul(scale); + dest.traceShadow(state); + state.addSample(dest); + } + } + } + } + + public void getPhoton(double randX1, double randY1, double randX2, double randY2, Point3 p, Vector3 dir, Color power) { + p.x = (float) (lxmin * (1 - randX2) + lxmax * randX2); + p.y = (float) (lymin * (1 - randY2) + lymax * randY2); + p.z = maxZ - 0.001f; + + double u = 2 * Math.PI * randX1; + double s = Math.sqrt(randY1); + dir.set((float) (Math.cos(u) * s), (float) (Math.sin(u) * s), (float) -Math.sqrt(1.0f - randY1)); + Color.mul((float) Math.PI * area, radiance, power); + } + + public float getPower() { + return radiance.copy().mul((float) Math.PI * area).getLuminance(); + } + + public int getNumPrimitives() { + return 1; + } + + public float getPrimitiveBound(int primID, int i) { + switch (i) { + case 0: + return minX; + case 1: + return maxX; + case 2: + return minY; + case 3: + return maxY; + case 4: + return minZ; + case 5: + return maxZ; + default: + return 0; + } + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(minX, minY, minZ); + bounds.include(maxX, maxY, maxZ); + if (o2w == null) + return bounds; + return o2w.transform(bounds); + } + + public PrimitiveList getBakingPrimitives() { + return null; + } + + public Instance createInstance() { + return Instance.createTemporary(this, null, this); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/CubeGrid.java b/src/main/java/org/sunflow/core/primitive/CubeGrid.java new file mode 100644 index 0000000..75b1224 --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/CubeGrid.java @@ -0,0 +1,288 @@ +package org.sunflow.core.primitive; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Vector3; + +public abstract class CubeGrid implements PrimitiveList { + private int nx, ny, nz; + private float voxelwx, voxelwy, voxelwz; + private float invVoxelwx, invVoxelwy, invVoxelwz; + private BoundingBox bounds; + + public CubeGrid() { + nx = ny = nz = 1; + bounds = new BoundingBox(1); + } + + public boolean update(ParameterList pl, SunflowAPI api) { + nx = pl.getInt("resolutionX", nx); + ny = pl.getInt("resolutionY", ny); + nz = pl.getInt("resolutionZ", nz); + voxelwx = 2.0f / nx; + voxelwy = 2.0f / ny; + voxelwz = 2.0f / nz; + invVoxelwx = 1 / voxelwx; + invVoxelwy = 1 / voxelwy; + invVoxelwz = 1 / voxelwz; + return true; + } + + protected abstract boolean inside(int x, int y, int z); + + public BoundingBox getBounds() { + return bounds; + } + + public void prepareShadingState(ShadingState state) { + state.init(); + state.getRay().getPoint(state.getPoint()); + Instance parent = state.getInstance(); + Vector3 normal; + switch (state.getPrimitiveID()) { + case 0: + normal = new Vector3(-1, 0, 0); + break; + case 1: + normal = new Vector3(1, 0, 0); + break; + case 2: + normal = new Vector3(0, -1, 0); + break; + case 3: + normal = new Vector3(0, 1, 0); + break; + case 4: + normal = new Vector3(0, 0, -1); + break; + case 5: + normal = new Vector3(0, 0, 1); + break; + default: + normal = new Vector3(0, 0, 0); + break; + } + state.getNormal().set(state.transformNormalObjectToWorld(normal)); + state.getGeoNormal().set(state.getNormal()); + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + state.setShader(parent.getShader(0)); + state.setModifier(parent.getModifier(0)); + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + float intervalMin = r.getMin(); + float intervalMax = r.getMax(); + float orgX = r.ox; + float orgY = r.oy; + float orgZ = r.oz; + float dirX = r.dx, invDirX = 1 / dirX; + float dirY = r.dy, invDirY = 1 / dirY; + float dirZ = r.dz, invDirZ = 1 / dirZ; + float t1, t2; + t1 = (-1 - orgX) * invDirX; + t2 = (+1 - orgX) * invDirX; + int curr = -1; + if (invDirX > 0) { + if (t1 > intervalMin) { + intervalMin = t1; + curr = 0; + } + if (t2 < intervalMax) + intervalMax = t2; + if (intervalMin > intervalMax) + return; + } else { + if (t2 > intervalMin) { + intervalMin = t2; + curr = 1; + } + if (t1 < intervalMax) + intervalMax = t1; + if (intervalMin > intervalMax) + return; + } + t1 = (-1 - orgY) * invDirY; + t2 = (+1 - orgY) * invDirY; + if (invDirY > 0) { + if (t1 > intervalMin) { + intervalMin = t1; + curr = 2; + } + if (t2 < intervalMax) + intervalMax = t2; + if (intervalMin > intervalMax) + return; + } else { + if (t2 > intervalMin) { + intervalMin = t2; + curr = 3; + } + if (t1 < intervalMax) + intervalMax = t1; + if (intervalMin > intervalMax) + return; + } + t1 = (-1 - orgZ) * invDirZ; + t2 = (+1 - orgZ) * invDirZ; + if (invDirZ > 0) { + if (t1 > intervalMin) { + intervalMin = t1; + curr = 4; + } + if (t2 < intervalMax) + intervalMax = t2; + if (intervalMin > intervalMax) + return; + } else { + if (t2 > intervalMin) { + intervalMin = t2; + curr = 5; + } + if (t1 < intervalMax) + intervalMax = t1; + if (intervalMin > intervalMax) + return; + } + // box is hit at [intervalMin, intervalMax] + orgX += intervalMin * dirX; + orgY += intervalMin * dirY; + orgZ += intervalMin * dirZ; + // locate starting point inside the grid + // and set up 3D-DDA vars + int indxX, indxY, indxZ; + int stepX, stepY, stepZ; + int stopX, stopY, stopZ; + float deltaX, deltaY, deltaZ; + float tnextX, tnextY, tnextZ; + // stepping factors along X + indxX = (int) ((orgX + 1) * invVoxelwx); + if (indxX < 0) + indxX = 0; + else if (indxX >= nx) + indxX = nx - 1; + if (Math.abs(dirX) < 1e-6f) { + stepX = 0; + stopX = indxX; + deltaX = 0; + tnextX = Float.POSITIVE_INFINITY; + } else if (dirX > 0) { + stepX = 1; + stopX = nx; + deltaX = voxelwx * invDirX; + tnextX = intervalMin + ((indxX + 1) * voxelwx - 1 - orgX) * invDirX; + } else { + stepX = -1; + stopX = -1; + deltaX = -voxelwx * invDirX; + tnextX = intervalMin + (indxX * voxelwx - 1 - orgX) * invDirX; + } + // stepping factors along Y + indxY = (int) ((orgY + 1) * invVoxelwy); + if (indxY < 0) + indxY = 0; + else if (indxY >= ny) + indxY = ny - 1; + if (Math.abs(dirY) < 1e-6f) { + stepY = 0; + stopY = indxY; + deltaY = 0; + tnextY = Float.POSITIVE_INFINITY; + } else if (dirY > 0) { + stepY = 1; + stopY = ny; + deltaY = voxelwy * invDirY; + tnextY = intervalMin + ((indxY + 1) * voxelwy - 1 - orgY) * invDirY; + } else { + stepY = -1; + stopY = -1; + deltaY = -voxelwy * invDirY; + tnextY = intervalMin + (indxY * voxelwy - 1 - orgY) * invDirY; + } + // stepping factors along Z + indxZ = (int) ((orgZ + 1) * invVoxelwz); + if (indxZ < 0) + indxZ = 0; + else if (indxZ >= nz) + indxZ = nz - 1; + if (Math.abs(dirZ) < 1e-6f) { + stepZ = 0; + stopZ = indxZ; + deltaZ = 0; + tnextZ = Float.POSITIVE_INFINITY; + } else if (dirZ > 0) { + stepZ = 1; + stopZ = nz; + deltaZ = voxelwz * invDirZ; + tnextZ = intervalMin + ((indxZ + 1) * voxelwz - 1 - orgZ) * invDirZ; + } else { + stepZ = -1; + stopZ = -1; + deltaZ = -voxelwz * invDirZ; + tnextZ = intervalMin + (indxZ * voxelwz - 1 - orgZ) * invDirZ; + } + // are we starting inside the cube + boolean isInside = inside(indxX, indxY, indxZ) && bounds.contains(r.ox, r.oy, r.oz); + // trace through the grid + for (;;) { + if (inside(indxX, indxY, indxZ) != isInside) { + // we hit a boundary + r.setMax(intervalMin); + // if we are inside, the last bit needs to be flipped + if (isInside) + curr ^= 1; + state.setIntersection(curr); + return; + } + if (tnextX < tnextY && tnextX < tnextZ) { + curr = dirX > 0 ? 0 : 1; + intervalMin = tnextX; + if (intervalMin > intervalMax) + return; + indxX += stepX; + if (indxX == stopX) + return; + tnextX += deltaX; + } else if (tnextY < tnextZ) { + curr = dirY > 0 ? 2 : 3; + intervalMin = tnextY; + if (intervalMin > intervalMax) + return; + indxY += stepY; + if (indxY == stopY) + return; + tnextY += deltaY; + } else { + curr = dirZ > 0 ? 4 : 5; + intervalMin = tnextZ; + if (intervalMin > intervalMax) + return; + indxZ += stepZ; + if (indxZ == stopZ) + return; + tnextZ += deltaZ; + } + } + } + + public int getNumPrimitives() { + return 1; + } + + public float getPrimitiveBound(int primID, int i) { + return ((i & 1) == 0) ? -1 : 1; + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + if (o2w == null) + return bounds; + return o2w.transform(bounds); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/Cylinder.java b/src/main/java/org/sunflow/core/primitive/Cylinder.java new file mode 100644 index 0000000..e6b8067 --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/Cylinder.java @@ -0,0 +1,93 @@ +package org.sunflow.core.primitive; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Solvers; +import org.sunflow.math.Vector3; + +public class Cylinder implements PrimitiveList { + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(1); + if (o2w != null) + bounds = o2w.transform(bounds); + return bounds; + } + + public float getPrimitiveBound(int primID, int i) { + return (i & 1) == 0 ? -1 : 1; + } + + public int getNumPrimitives() { + return 1; + } + + public void prepareShadingState(ShadingState state) { + state.init(); + state.getRay().getPoint(state.getPoint()); + Instance parent = state.getInstance(); + Point3 localPoint = state.transformWorldToObject(state.getPoint()); + state.getNormal().set(localPoint.x, localPoint.y, 0); + state.getNormal().normalize(); + + float phi = (float) Math.atan2(state.getNormal().y, state.getNormal().x); + if (phi < 0) + phi += 2 * Math.PI; + state.getUV().x = phi / (float) (2 * Math.PI); + state.getUV().y = (localPoint.z + 1) * 0.5f; + state.setShader(parent.getShader(0)); + state.setModifier(parent.getModifier(0)); + // into world space + Vector3 worldNormal = state.transformNormalObjectToWorld(state.getNormal()); + Vector3 v = state.transformVectorObjectToWorld(new Vector3(0, 0, 1)); + state.getNormal().set(worldNormal); + state.getNormal().normalize(); + state.getGeoNormal().set(state.getNormal()); + // compute basis in world space + state.setBasis(OrthoNormalBasis.makeFromWV(state.getNormal(), v)); + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + // intersect in local space + float qa = r.dx * r.dx + r.dy * r.dy; + float qb = 2 * ((r.dx * r.ox) + (r.dy * r.oy)); + float qc = ((r.ox * r.ox) + (r.oy * r.oy)) - 1; + double[] t = Solvers.solveQuadric(qa, qb, qc); + if (t != null) { + // early rejection + if (t[0] >= r.getMax() || t[1] <= r.getMin()) + return; + if (t[0] > r.getMin()) { + float z = r.oz + (float) t[0] * r.dz; + if (z >= -1 && z <= 1) { + r.setMax((float) t[0]); + state.setIntersection(0); + return; + } + } + if (t[1] < r.getMax()) { + float z = r.oz + (float) t[1] * r.dz; + if (z >= -1 && z <= 1) { + r.setMax((float) t[1]); + state.setIntersection(0); + } + } + } + } + + public PrimitiveList getBakingPrimitives() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/Hair.java b/src/main/java/org/sunflow/core/primitive/Hair.java new file mode 100644 index 0000000..a74b777 --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/Hair.java @@ -0,0 +1,261 @@ +package org.sunflow.core.primitive; + +import java.util.Locale; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.LightSample; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.core.ParameterList.FloatParameter; +import org.sunflow.core.ParameterList.InterpolationType; +import org.sunflow.image.Color; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Vector3; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class Hair implements PrimitiveList, Shader { + private int numSegments; + private float[] points; + private FloatParameter widths; + + public Hair() { + numSegments = 1; + points = null; + widths = new FloatParameter(1.0f); + } + + public int getNumPrimitives() { + return numSegments * (points.length / (3 * (numSegments + 1))); + } + + public float getPrimitiveBound(int primID, int i) { + int hair = primID / numSegments; + int line = primID % numSegments; + int vn = hair * (numSegments + 1) + line; + int vRoot = hair * 3 * (numSegments + 1); + int v0 = vRoot + line * 3; + int v1 = v0 + 3; + int axis = i >>> 1; + if ((i & 1) == 0) { + return Math.min(points[v0 + axis] - 0.5f * getWidth(vn), points[v1 + axis] - 0.5f * getWidth(vn + 1)); + } else { + return Math.max(points[v0 + axis] + 0.5f * getWidth(vn), points[v1 + axis] + 0.5f * getWidth(vn + 1)); + } + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(); + for (int i = 0, j = 0; i < points.length; i += 3, j++) { + float w = 0.5f * getWidth(j); + bounds.include(points[i] - w, points[i + 1] - w, points[i + 2] - w); + bounds.include(points[i] + w, points[i + 1] + w, points[i + 2] + w); + } + if (o2w != null) + bounds = o2w.transform(bounds); + return bounds; + } + + private float getWidth(int i) { + switch (widths.interp) { + case NONE: + return widths.data[0]; + case VERTEX: + return widths.data[i]; + default: + return 0; + } + } + + private Vector3 getTangent(int line, int v0, float v) { + Vector3 vcurr = new Vector3(points[v0 + 3] - points[v0 + 0], points[v0 + 4] - points[v0 + 1], points[v0 + 5] - points[v0 + 2]); + vcurr.normalize(); + if (line == 0 || line == numSegments - 1) + return vcurr; + if (v <= 0.5f) { + // get previous segment + Vector3 vprev = new Vector3(points[v0 + 0] - points[v0 - 3], points[v0 + 1] - points[v0 - 2], points[v0 + 2] - points[v0 - 1]); + vprev.normalize(); + float t = v + 0.5f; + float s = 1 - t; + float vx = vprev.x * s + vcurr.x * t; + float vy = vprev.y * s + vcurr.y * t; + float vz = vprev.z * s + vcurr.z * t; + return new Vector3(vx, vy, vz); + } else { + // get next segment + v0 += 3; + Vector3 vnext = new Vector3(points[v0 + 3] - points[v0 + 0], points[v0 + 4] - points[v0 + 1], points[v0 + 5] - points[v0 + 2]); + vnext.normalize(); + float t = 1.5f - v; + float s = 1 - t; + float vx = vnext.x * s + vcurr.x * t; + float vy = vnext.y * s + vcurr.y * t; + float vz = vnext.z * s + vcurr.z * t; + return new Vector3(vx, vy, vz); + } + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + int hair = primID / numSegments; + int line = primID % numSegments; + int vRoot = hair * 3 * (numSegments + 1); + int v0 = vRoot + line * 3; + int v1 = v0 + 3; + float vx = points[v1 + 0] - points[v0 + 0]; + float vy = points[v1 + 1] - points[v0 + 1]; + float vz = points[v1 + 2] - points[v0 + 2]; + float ux = r.dy * vz - r.dz * vy; + float uy = r.dz * vx - r.dx * vz; + float uz = r.dx * vy - r.dy * vx; + float nx = uy * vz - uz * vy; + float ny = uz * vx - ux * vz; + float nz = ux * vy - uy * vx; + float tden = 1 / (nx * r.dx + ny * r.dy + nz * r.dz); + float tnum = nx * (points[v0 + 0] - r.ox) + ny * (points[v0 + 1] - r.oy) + nz * (points[v0 + 2] - r.oz); + float t = tnum * tden; + if (r.isInside(t)) { + int vn = hair * (numSegments + 1) + line; + float px = r.ox + t * r.dx; + float py = r.oy + t * r.dy; + float pz = r.oz + t * r.dz; + float qx = px - points[v0 + 0]; + float qy = py - points[v0 + 1]; + float qz = pz - points[v0 + 2]; + float q = (vx * qx + vy * qy + vz * qz) / (vx * vx + vy * vy + vz * vz); + if (q <= 0) { + // don't included rounded tip at root + if (line == 0) + return; + float dx = points[v0 + 0] - px; + float dy = points[v0 + 1] - py; + float dz = points[v0 + 2] - pz; + float d2 = dx * dx + dy * dy + dz * dz; + float width = getWidth(vn); + if (d2 < (width * width * 0.25f)) { + r.setMax(t); + state.setIntersection(primID, 0, 0); + } + } else if (q >= 1) { + float dx = points[v1 + 0] - px; + float dy = points[v1 + 1] - py; + float dz = points[v1 + 2] - pz; + float d2 = dx * dx + dy * dy + dz * dz; + float width = getWidth(vn + 1); + if (d2 < (width * width * 0.25f)) { + r.setMax(t); + state.setIntersection(primID, 0, 1); + } + } else { + float dx = points[v0 + 0] + q * vx - px; + float dy = points[v0 + 1] + q * vy - py; + float dz = points[v0 + 2] + q * vz - pz; + float d2 = dx * dx + dy * dy + dz * dz; + float width = (1 - q) * getWidth(vn) + q * getWidth(vn + 1); + if (d2 < (width * width * 0.25f)) { + r.setMax(t); + state.setIntersection(primID, 0, q); + } + } + } + } + + public void prepareShadingState(ShadingState state) { + state.init(); + Instance i = state.getInstance(); + state.getRay().getPoint(state.getPoint()); + Ray r = state.getRay(); + Shader s = i.getShader(0); + state.setShader(s != null ? s : this); + int primID = state.getPrimitiveID(); + int hair = primID / numSegments; + int line = primID % numSegments; + int vRoot = hair * 3 * (numSegments + 1); + int v0 = vRoot + line * 3; + + // tangent vector + Vector3 v = getTangent(line, v0, state.getV()); + v = state.transformVectorObjectToWorld(v); + state.setBasis(OrthoNormalBasis.makeFromWV(v, new Vector3(-r.dx, -r.dy, -r.dz))); + state.getBasis().swapVW(); + // normal + state.getNormal().set(0, 0, 1); + state.getBasis().transform(state.getNormal()); + state.getGeoNormal().set(state.getNormal()); + + state.getUV().set(0, (line + state.getV()) / numSegments); + } + + public boolean update(ParameterList pl, SunflowAPI api) { + numSegments = pl.getInt("segments", numSegments); + if (numSegments < 1) { + UI.printError(Module.HAIR, "Invalid number of segments: %d", numSegments); + return false; + } + FloatParameter pointsP = pl.getPointArray("points"); + if (pointsP != null) { + if (pointsP.interp != InterpolationType.VERTEX) + UI.printError(Module.HAIR, "Point interpolation type must be set to \"vertex\" - was \"%s\"", pointsP.interp.name().toLowerCase(Locale.ENGLISH)); + else { + points = pointsP.data; + } + } + if (points == null) { + UI.printError(Module.HAIR, "Unabled to update hair - vertices are missing"); + return false; + } + + pl.setVertexCount(points.length / 3); + FloatParameter widthsP = pl.getFloatArray("widths"); + if (widthsP != null) { + if (widthsP.interp == InterpolationType.NONE || widthsP.interp == InterpolationType.VERTEX) + widths = widthsP; + else + UI.printWarning(Module.HAIR, "Width interpolation type %s is not supported -- ignoring", widthsP.interp.name().toLowerCase(Locale.ENGLISH)); + } + return true; + } + + public Color getRadiance(ShadingState state) { + // don't use these - gather lights for sphere of directions + // gather lights + state.initLightSamples(); + state.initCausticSamples(); + Vector3 v = state.getRay().getDirection(); + v.negate(); + Vector3 h = new Vector3(); + Vector3 t = state.getBasis().transform(new Vector3(0, 1, 0)); + Color diff = Color.black(); + Color spec = Color.black(); + for (LightSample ls : state) { + Vector3 l = ls.getShadowRay().getDirection(); + float dotTL = Vector3.dot(t, l); + float sinTL = (float) Math.sqrt(1 - dotTL * dotTL); + // float dotVL = Vector3.dot(v, l); + diff.madd(sinTL, ls.getDiffuseRadiance()); + Vector3.add(v, l, h); + h.normalize(); + float dotTH = Vector3.dot(t, h); + float sinTH = (float) Math.sqrt(1 - dotTH * dotTH); + float s = (float) Math.pow(sinTH, 10.0f); + spec.madd(s, ls.getSpecularRadiance()); + } + Color c = Color.add(diff, spec, new Color()); + // transparency + return Color.blend(c, state.traceTransparency(), state.getV(), new Color()); + } + + public void scatterPhoton(ShadingState state, Color power) { + } + + public PrimitiveList getBakingPrimitives() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/JuliaFractal.java b/src/main/java/org/sunflow/core/primitive/JuliaFractal.java new file mode 100644 index 0000000..a6b6c2a --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/JuliaFractal.java @@ -0,0 +1,253 @@ +package org.sunflow.core.primitive; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Solvers; +import org.sunflow.math.Vector3; + +public class JuliaFractal implements PrimitiveList { + private static float BOUNDING_RADIUS = (float) Math.sqrt(3); + private static float BOUNDING_RADIUS2 = 3; + private static float ESCAPE_THRESHOLD = 1e1f; + private static float DELTA = 1e-4f; + + // quaternion constant + private float cx; + private float cy; + private float cz; + private float cw; + private int maxIterations; + private float epsilon; + + public JuliaFractal() { + // good defaults? + cw = -.4f; + cx = .2f; + cy = .3f; + cz = -.2f; + + maxIterations = 15; + epsilon = 0.00001f; + } + + public int getNumPrimitives() { + return 1; + } + + public float getPrimitiveBound(int primID, int i) { + return ((i & 1) == 0) ? -BOUNDING_RADIUS : BOUNDING_RADIUS; + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(BOUNDING_RADIUS); + if (o2w != null) + bounds = o2w.transform(bounds); + return bounds; + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + // intersect with bounding sphere + float qc = ((r.ox * r.ox) + (r.oy * r.oy) + (r.oz * r.oz)) - BOUNDING_RADIUS2; + float qt = r.getMin(); + if (qc > 0) { + // we are starting outside the sphere, find intersection on the + // sphere + float qa = r.dx * r.dx + r.dy * r.dy + r.dz * r.dz; + float qb = 2 * ((r.dx * r.ox) + (r.dy * r.oy) + (r.dz * r.oz)); + double[] t = Solvers.solveQuadric(qa, qb, qc); + // early rejection + if (t == null || t[0] >= r.getMax() || t[1] <= r.getMin()) + return; + qt = (float) t[0]; + } + float dist = Float.POSITIVE_INFINITY; + float rox = r.ox + qt * r.dx; + float roy = r.oy + qt * r.dy; + float roz = r.oz + qt * r.dz; + float invRayLength = (float) (1 / Math.sqrt(r.dx * r.dx + r.dy * r.dy + r.dz * r.dz)); + // now we can start intersection + while (true) { + float zw = rox; + float zx = roy; + float zy = roz; + float zz = 0; + + float zpw = 1; + float zpx = 0; + float zpy = 0; + float zpz = 0; + + // run several iterations + float dotz = 0; + for (int i = 0; i < maxIterations; i++) { + { + // zp = 2 * (z * zp) + float nw = zw * zpw - zx * zpx - zy * zpy - zz * zpz; + float nx = zw * zpx + zx * zpw + zy * zpz - zz * zpy; + float ny = zw * zpy + zy * zpw + zz * zpx - zx * zpz; + zpz = 2 * (zw * zpz + zz * zpw + zx * zpy - zy * zpx); + zpw = 2 * nw; + zpx = 2 * nx; + zpy = 2 * ny; + } + { + // z = z*z + c + float nw = zw * zw - zx * zx - zy * zy - zz * zz + cw; + zx = 2 * zw * zx + cx; + zy = 2 * zw * zy + cy; + zz = 2 * zw * zz + cz; + zw = nw; + } + dotz = zw * zw + zx * zx + zy * zy + zz * zz; + if (dotz > ESCAPE_THRESHOLD) + break; + + } + float normZ = (float) Math.sqrt(dotz); + dist = 0.5f * normZ * (float) Math.log(normZ) / length(zpw, zpx, zpy, zpz); + rox += dist * r.dx; + roy += dist * r.dy; + roz += dist * r.dz; + qt += dist; + if (dist * invRayLength < epsilon) + break; + if (rox * rox + roy * roy + roz * roz > BOUNDING_RADIUS2) + return; + } + // now test t value again + if (!r.isInside(qt)) + return; + if (dist * invRayLength < epsilon) { + // valid hit + r.setMax(qt); + state.setIntersection(0); + } + } + + public void prepareShadingState(ShadingState state) { + state.init(); + state.getRay().getPoint(state.getPoint()); + Instance parent = state.getInstance(); + // compute local normal + Point3 p = state.transformWorldToObject(state.getPoint()); + float gx1w = p.x - DELTA; + float gx1x = p.y; + float gx1y = p.z; + float gx1z = 0; + float gx2w = p.x + DELTA; + float gx2x = p.y; + float gx2y = p.z; + float gx2z = 0; + + float gy1w = p.x; + float gy1x = p.y - DELTA; + float gy1y = p.z; + float gy1z = 0; + float gy2w = p.x; + float gy2x = p.y + DELTA; + float gy2y = p.z; + float gy2z = 0; + + float gz1w = p.x; + float gz1x = p.y; + float gz1y = p.z - DELTA; + float gz1z = 0; + float gz2w = p.x; + float gz2x = p.y; + float gz2y = p.z + DELTA; + float gz2z = 0; + + for (int i = 0; i < maxIterations; i++) { + { + // z = z*z + c + float nw = gx1w * gx1w - gx1x * gx1x - gx1y * gx1y - gx1z * gx1z + cw; + gx1x = 2 * gx1w * gx1x + cx; + gx1y = 2 * gx1w * gx1y + cy; + gx1z = 2 * gx1w * gx1z + cz; + gx1w = nw; + } + { + // z = z*z + c + float nw = gx2w * gx2w - gx2x * gx2x - gx2y * gx2y - gx2z * gx2z + cw; + gx2x = 2 * gx2w * gx2x + cx; + gx2y = 2 * gx2w * gx2y + cy; + gx2z = 2 * gx2w * gx2z + cz; + gx2w = nw; + } + { + // z = z*z + c + float nw = gy1w * gy1w - gy1x * gy1x - gy1y * gy1y - gy1z * gy1z + cw; + gy1x = 2 * gy1w * gy1x + cx; + gy1y = 2 * gy1w * gy1y + cy; + gy1z = 2 * gy1w * gy1z + cz; + gy1w = nw; + } + { + // z = z*z + c + float nw = gy2w * gy2w - gy2x * gy2x - gy2y * gy2y - gy2z * gy2z + cw; + gy2x = 2 * gy2w * gy2x + cx; + gy2y = 2 * gy2w * gy2y + cy; + gy2z = 2 * gy2w * gy2z + cz; + gy2w = nw; + } + { + // z = z*z + c + float nw = gz1w * gz1w - gz1x * gz1x - gz1y * gz1y - gz1z * gz1z + cw; + gz1x = 2 * gz1w * gz1x + cx; + gz1y = 2 * gz1w * gz1y + cy; + gz1z = 2 * gz1w * gz1z + cz; + gz1w = nw; + } + { + // z = z*z + c + float nw = gz2w * gz2w - gz2x * gz2x - gz2y * gz2y - gz2z * gz2z + cw; + gz2x = 2 * gz2w * gz2x + cx; + gz2y = 2 * gz2w * gz2y + cy; + gz2z = 2 * gz2w * gz2z + cz; + gz2w = nw; + } + } + float gradX = length(gx2w, gx2x, gx2y, gx2z) - length(gx1w, gx1x, gx1y, gx1z); + float gradY = length(gy2w, gy2x, gy2y, gy2z) - length(gy1w, gy1x, gy1y, gy1z); + float gradZ = length(gz2w, gz2x, gz2y, gz2z) - length(gz1w, gz1x, gz1y, gz1z); + Vector3 n = new Vector3(gradX, gradY, gradZ); + state.getNormal().set(state.transformNormalObjectToWorld(n)); + state.getNormal().normalize(); + state.getGeoNormal().set(state.getNormal()); + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + + state.getPoint().x += state.getNormal().x * epsilon * 20; + state.getPoint().y += state.getNormal().y * epsilon * 20; + state.getPoint().z += state.getNormal().z * epsilon * 20; + + state.setShader(parent.getShader(0)); + state.setModifier(parent.getModifier(0)); + } + + private static float length(float w, float x, float y, float z) { + return (float) Math.sqrt(w * w + x * x + y * y + z * z); + } + + public boolean update(ParameterList pl, SunflowAPI api) { + maxIterations = pl.getInt("iterations", maxIterations); + epsilon = pl.getFloat("epsilon", epsilon); + cw = pl.getFloat("cw", cw); + cx = pl.getFloat("cx", cx); + cy = pl.getFloat("cy", cy); + cz = pl.getFloat("cz", cz); + return true; + } + + public PrimitiveList getBakingPrimitives() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/ParticleSurface.java b/src/main/java/org/sunflow/core/primitive/ParticleSurface.java new file mode 100644 index 0000000..39c9d04 --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/ParticleSurface.java @@ -0,0 +1,102 @@ +package org.sunflow.core.primitive; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.core.ParameterList.FloatParameter; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Solvers; +import org.sunflow.math.Vector3; + +public class ParticleSurface implements PrimitiveList { + private float[] particles; + private float r, r2; + private int n; + + public ParticleSurface() { + particles = null; + r = r2 = 1; + n = 0; + } + + public int getNumPrimitives() { + return n; + } + + public float getPrimitiveBound(int primID, int i) { + float c = particles[primID * 3 + (i >>> 1)]; + return (i & 1) == 0 ? c - r : c + r; + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(); + for (int i = 0, i3 = 0; i < n; i++, i3 += 3) + bounds.include(particles[i3], particles[i3 + 1], particles[i3 + 2]); + bounds.include(bounds.getMinimum().x - r, bounds.getMinimum().y - r, bounds.getMinimum().z - r); + bounds.include(bounds.getMaximum().x + r, bounds.getMaximum().y + r, bounds.getMaximum().z + r); + return o2w == null ? bounds : o2w.transform(bounds); + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + int i3 = primID * 3; + float ocx = r.ox - particles[i3 + 0]; + float ocy = r.oy - particles[i3 + 1]; + float ocz = r.oz - particles[i3 + 2]; + float qa = r.dx * r.dx + r.dy * r.dy + r.dz * r.dz; + float qb = 2 * ((r.dx * ocx) + (r.dy * ocy) + (r.dz * ocz)); + float qc = ((ocx * ocx) + (ocy * ocy) + (ocz * ocz)) - r2; + double[] t = Solvers.solveQuadric(qa, qb, qc); + if (t != null) { + // early rejection + if (t[0] >= r.getMax() || t[1] <= r.getMin()) + return; + if (t[0] > r.getMin()) + r.setMax((float) t[0]); + else + r.setMax((float) t[1]); + state.setIntersection(primID); + } + } + + public void prepareShadingState(ShadingState state) { + state.init(); + state.getRay().getPoint(state.getPoint()); + Point3 localPoint = state.transformWorldToObject(state.getPoint()); + + localPoint.x -= particles[3 * state.getPrimitiveID() + 0]; + localPoint.y -= particles[3 * state.getPrimitiveID() + 1]; + localPoint.z -= particles[3 * state.getPrimitiveID() + 2]; + + state.getNormal().set(localPoint.x, localPoint.y, localPoint.z); + state.getNormal().normalize(); + + state.setShader(state.getInstance().getShader(0)); + state.setModifier(state.getInstance().getModifier(0)); + // into object space + Vector3 worldNormal = state.transformNormalObjectToWorld(state.getNormal()); + state.getNormal().set(worldNormal); + state.getNormal().normalize(); + state.getGeoNormal().set(state.getNormal()); + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + } + + public boolean update(ParameterList pl, SunflowAPI api) { + FloatParameter p = pl.getPointArray("particles"); + if (p != null) + particles = p.data; + r = pl.getFloat("radius", r); + r2 = r * r; + n = pl.getInt("num", n); + return particles != null && n <= (particles.length / 3); + } + + public PrimitiveList getBakingPrimitives() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/Plane.java b/src/main/java/org/sunflow/core/primitive/Plane.java new file mode 100644 index 0000000..b9e8eee --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/Plane.java @@ -0,0 +1,153 @@ +package org.sunflow.core.primitive; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class Plane implements PrimitiveList { + private Point3 center; + private Vector3 normal; + int k; + private float bnu, bnv, bnd; + private float cnu, cnv, cnd; + + public Plane() { + center = new Point3(0, 0, 0); + normal = new Vector3(0, 1, 0); + k = 3; + bnu = bnv = bnd = 0; + cnu = cnv = cnd = 0; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + center = pl.getPoint("center", center); + Point3 b = pl.getPoint("point1", null); + Point3 c = pl.getPoint("point2", null); + if (b != null && c != null) { + Point3 v0 = center; + Point3 v1 = b; + Point3 v2 = c; + Vector3 ng = normal = Vector3.cross(Point3.sub(v1, v0, new Vector3()), Point3.sub(v2, v0, new Vector3()), new Vector3()).normalize(); + if (Math.abs(ng.x) > Math.abs(ng.y) && Math.abs(ng.x) > Math.abs(ng.z)) + k = 0; + else if (Math.abs(ng.y) > Math.abs(ng.z)) + k = 1; + else + k = 2; + float ax, ay, bx, by, cx, cy; + switch (k) { + case 0: { + ax = v0.y; + ay = v0.z; + bx = v2.y - ax; + by = v2.z - ay; + cx = v1.y - ax; + cy = v1.z - ay; + break; + } + case 1: { + ax = v0.z; + ay = v0.x; + bx = v2.z - ax; + by = v2.x - ay; + cx = v1.z - ax; + cy = v1.x - ay; + break; + } + case 2: + default: { + ax = v0.x; + ay = v0.y; + bx = v2.x - ax; + by = v2.y - ay; + cx = v1.x - ax; + cy = v1.y - ay; + } + } + float det = bx * cy - by * cx; + bnu = -by / det; + bnv = bx / det; + bnd = (by * ax - bx * ay) / det; + cnu = cy / det; + cnv = -cx / det; + cnd = (cx * ay - cy * ax) / det; + } else { + normal = pl.getVector("normal", normal); + k = 3; + bnu = bnv = bnd = 0; + cnu = cnv = cnd = 0; + } + return true; + } + + public void prepareShadingState(ShadingState state) { + state.init(); + state.getRay().getPoint(state.getPoint()); + Instance parent = state.getInstance(); + Vector3 worldNormal = state.transformNormalObjectToWorld(normal); + state.getNormal().set(worldNormal); + state.getGeoNormal().set(worldNormal); + state.setShader(parent.getShader(0)); + state.setModifier(parent.getModifier(0)); + Point3 p = state.transformWorldToObject(state.getPoint()); + float hu, hv; + switch (k) { + case 0: { + hu = p.y; + hv = p.z; + break; + } + case 1: { + hu = p.z; + hv = p.x; + break; + } + case 2: { + hu = p.x; + hv = p.y; + break; + } + default: + hu = hv = 0; + } + state.getUV().x = hu * bnu + hv * bnv + bnd; + state.getUV().y = hu * cnu + hv * cnv + cnd; + state.setBasis(OrthoNormalBasis.makeFromW(normal)); + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + float dn = normal.x * r.dx + normal.y * r.dy + normal.z * r.dz; + if (dn == 0.0) + return; + float t = (((center.x - r.ox) * normal.x) + ((center.y - r.oy) * normal.y) + ((center.z - r.oz) * normal.z)) / dn; + if (r.isInside(t)) { + r.setMax(t); + state.setIntersection(0); + } + } + + public int getNumPrimitives() { + return 1; + } + + public float getPrimitiveBound(int primID, int i) { + return 0; + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + return null; + } + + public PrimitiveList getBakingPrimitives() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/QuadMesh.java b/src/main/java/org/sunflow/core/primitive/QuadMesh.java new file mode 100644 index 0000000..c02e5a6 --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/QuadMesh.java @@ -0,0 +1,392 @@ +package org.sunflow.core.primitive; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.Locale; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.core.ParameterList.FloatParameter; +import org.sunflow.core.ParameterList.InterpolationType; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.MathUtils; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class QuadMesh implements PrimitiveList { + protected float[] points; + protected int[] quads; + private FloatParameter normals; + private FloatParameter uvs; + private byte[] faceShaders; + + public QuadMesh() { + quads = null; + points = null; + normals = uvs = new FloatParameter(); + faceShaders = null; + } + + public void writeObj(String filename) { + try { + FileWriter file = new FileWriter(filename); + file.write(String.format("o object\n")); + for (int i = 0; i < points.length; i += 3) + file.write(String.format("v %g %g %g\n", points[i], points[i + 1], points[i + 2])); + file.write("s off\n"); + for (int i = 0; i < quads.length; i += 4) + file.write(String.format("f %d %d %d %d\n", quads[i] + 1, quads[i + 1] + 1, quads[i + 2] + 1, quads[i + 3] + 1)); + file.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public boolean update(ParameterList pl, SunflowAPI api) { + { + int[] quads = pl.getIntArray("quads"); + if (quads != null) { + this.quads = quads; + } + } + if (quads == null) { + UI.printError(Module.GEOM, "Unable to update mesh - quad indices are missing"); + return false; + } + if (quads.length % 4 != 0) + UI.printWarning(Module.GEOM, "Quad index data is not a multiple of 4 - some quads may be missing"); + pl.setFaceCount(quads.length / 4); + { + FloatParameter pointsP = pl.getPointArray("points"); + if (pointsP != null) + if (pointsP.interp != InterpolationType.VERTEX) + UI.printError(Module.GEOM, "Point interpolation type must be set to \"vertex\" - was \"%s\"", pointsP.interp.name().toLowerCase(Locale.ENGLISH)); + else { + points = pointsP.data; + } + } + if (points == null) { + UI.printError(Module.GEOM, "Unabled to update mesh - vertices are missing"); + return false; + } + pl.setVertexCount(points.length / 3); + pl.setFaceVertexCount(4 * (quads.length / 4)); + FloatParameter normals = pl.getVectorArray("normals"); + if (normals != null) + this.normals = normals; + FloatParameter uvs = pl.getTexCoordArray("uvs"); + if (uvs != null) + this.uvs = uvs; + int[] faceShaders = pl.getIntArray("faceshaders"); + if (faceShaders != null && faceShaders.length == quads.length / 4) { + this.faceShaders = new byte[faceShaders.length]; + for (int i = 0; i < faceShaders.length; i++) { + int v = faceShaders[i]; + if (v > 255) + UI.printWarning(Module.GEOM, "Shader index too large on quad %d", i); + this.faceShaders[i] = (byte) (v & 0xFF); + } + } + return true; + } + + public float getPrimitiveBound(int primID, int i) { + int quad = 4 * primID; + int a = 3 * quads[quad + 0]; + int b = 3 * quads[quad + 1]; + int c = 3 * quads[quad + 2]; + int d = 3 * quads[quad + 3]; + int axis = i >>> 1; + if ((i & 1) == 0) + return MathUtils.min(points[a + axis], points[b + axis], points[c + axis], points[d + axis]); + else + return MathUtils.max(points[a + axis], points[b + axis], points[c + axis], points[d + axis]); + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(); + if (o2w == null) { + for (int i = 0; i < points.length; i += 3) + bounds.include(points[i], points[i + 1], points[i + 2]); + } else { + // transform vertices first + for (int i = 0; i < points.length; i += 3) { + float x = points[i]; + float y = points[i + 1]; + float z = points[i + 2]; + float wx = o2w.transformPX(x, y, z); + float wy = o2w.transformPY(x, y, z); + float wz = o2w.transformPZ(x, y, z); + bounds.include(wx, wy, wz); + } + } + return bounds; + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + // ray/bilinear patch intersection adapted from "Production Rendering: + // Design and Implementation" by Ian Stephenson (Ed.) + int quad = 4 * primID; + int p0 = 3 * quads[quad + 0]; + int p1 = 3 * quads[quad + 1]; + int p2 = 3 * quads[quad + 2]; + int p3 = 3 * quads[quad + 3]; + // transform patch into Hilbert space + final float A[] = { + points[p2 + 0] - points[p3 + 0] - points[p1 + 0] + points[p0 + 0], + points[p2 + 1] - points[p3 + 1] - points[p1 + 1] + points[p0 + 1], + points[p2 + 2] - points[p3 + 2] - points[p1 + 2] + points[p0 + 2] }; + final float B[] = { points[p1 + 0] - points[p0 + 0], + points[p1 + 1] - points[p0 + 1], + points[p1 + 2] - points[p0 + 2] }; + final float C[] = { points[p3 + 0] - points[p0 + 0], + points[p3 + 1] - points[p0 + 1], + points[p3 + 2] - points[p0 + 2] }; + final float R[] = { r.ox - points[p0 + 0], r.oy - points[p0 + 1], + r.oz - points[p0 + 2] }; + final float Q[] = { r.dx, r.dy, r.dz }; + + // pick major direction + float absqx = Math.abs(r.dx); + float absqy = Math.abs(r.dy); + float absqz = Math.abs(r.dz); + + int X = 0, Y = 1, Z = 2; + if (absqx > absqy && absqx > absqz) { + // X = 0, Y = 1, Z = 2 + } else if (absqy > absqz) { + // X = 1, Y = 0, Z = 2 + X = 1; + Y = 0; + } else { + // X = 2, Y = 1, Z = 0 + X = 2; + Z = 0; + } + + float Cxz = C[X] * Q[Z] - C[Z] * Q[X]; + float Cyx = C[Y] * Q[X] - C[X] * Q[Y]; + float Czy = C[Z] * Q[Y] - C[Y] * Q[Z]; + float Rxz = R[X] * Q[Z] - R[Z] * Q[X]; + float Ryx = R[Y] * Q[X] - R[X] * Q[Y]; + float Rzy = R[Z] * Q[Y] - R[Y] * Q[Z]; + float Bxy = B[X] * Q[Y] - B[Y] * Q[X]; + float Byz = B[Y] * Q[Z] - B[Z] * Q[Y]; + float Bzx = B[Z] * Q[X] - B[X] * Q[Z]; + float a = A[X] * Byz + A[Y] * Bzx + A[Z] * Bxy; + if (a == 0) { + // setup for linear equation + float b = B[X] * Czy + B[Y] * Cxz + B[Z] * Cyx; + float c = C[X] * Rzy + C[Y] * Rxz + C[Z] * Ryx; + float u = -c / b; + if (u >= 0 && u <= 1) { + float v = (u * Bxy + Ryx) / Cyx; + if (v >= 0 && v <= 1) { + float t = (B[X] * u + C[X] * v - R[X]) / Q[X]; + if (r.isInside(t)) { + r.setMax(t); + state.setIntersection(primID, u, v); + } + } + } + } else { + // setup for quadratic equation + float b = A[X] * Rzy + A[Y] * Rxz + A[Z] * Ryx + B[X] * Czy + B[Y] * Cxz + B[Z] * Cyx; + float c = C[X] * Rzy + C[Y] * Rxz + C[Z] * Ryx; + float discrim = b * b - 4 * a * c; + // reject trivial cases + if (c * (a + b + c) > 0 && (discrim < 0 || a * c < 0 || b / a > 0 || b / a < -2)) + return; + // solve quadratic + float q = b > 0 ? -0.5f * (b + (float) Math.sqrt(discrim)) : -0.5f * (b - (float) Math.sqrt(discrim)); + // check first solution + float Axy = A[X] * Q[Y] - A[Y] * Q[X]; + float u = q / a; + if (u >= 0 && u <= 1) { + float d = u * Axy - Cyx; + float v = -(u * Bxy + Ryx) / d; + if (v >= 0 && v <= 1) { + float t = (A[X] * u * v + B[X] * u + C[X] * v - R[X]) / Q[X]; + if (r.isInside(t)) { + r.setMax(t); + state.setIntersection(primID, u, v); + } + } + } + u = c / q; + if (u >= 0 && u <= 1) { + float d = u * Axy - Cyx; + float v = -(u * Bxy + Ryx) / d; + if (v >= 0 && v <= 1) { + float t = (A[X] * u * v + B[X] * u + C[X] * v - R[X]) / Q[X]; + if (r.isInside(t)) { + r.setMax(t); + state.setIntersection(primID, u, v); + } + } + } + } + } + + public int getNumPrimitives() { + return quads.length / 4; + } + + public void prepareShadingState(ShadingState state) { + state.init(); + Instance parent = state.getInstance(); + int primID = state.getPrimitiveID(); + float u = state.getU(); + float v = state.getV(); + state.getRay().getPoint(state.getPoint()); + int quad = 4 * primID; + int index0 = quads[quad + 0]; + int index1 = quads[quad + 1]; + int index2 = quads[quad + 2]; + int index3 = quads[quad + 3]; + Point3 v0p = getPoint(index0); + Point3 v1p = getPoint(index1); + Point3 v2p = getPoint(index2); + Point3 v3p = getPoint(index2); + float tanux = (1 - v) * (v1p.x - v0p.x) + v * (v2p.x - v3p.x); + float tanuy = (1 - v) * (v1p.y - v0p.y) + v * (v2p.y - v3p.y); + float tanuz = (1 - v) * (v1p.z - v0p.z) + v * (v2p.z - v3p.z); + + float tanvx = (1 - u) * (v3p.x - v0p.x) + u * (v2p.x - v1p.x); + float tanvy = (1 - u) * (v3p.y - v0p.y) + u * (v2p.y - v1p.y); + float tanvz = (1 - u) * (v3p.z - v0p.z) + u * (v2p.z - v1p.z); + + float nx = tanuy * tanvz - tanuz * tanvy; + float ny = tanuz * tanvx - tanux * tanvz; + float nz = tanux * tanvy - tanuy * tanvx; + + Vector3 ng = new Vector3(nx, ny, nz); + ng = state.transformNormalObjectToWorld(ng); + ng.normalize(); + state.getGeoNormal().set(ng); + + float k00 = (1 - u) * (1 - v); + float k10 = u * (1 - v); + float k01 = (1 - u) * v; + float k11 = u * v; + + switch (normals.interp) { + case NONE: + case FACE: { + state.getNormal().set(ng); + break; + } + case VERTEX: { + int i30 = 3 * index0; + int i31 = 3 * index1; + int i32 = 3 * index2; + int i33 = 3 * index3; + float[] normals = this.normals.data; + state.getNormal().x = k00 * normals[i30 + 0] + k10 * normals[i31 + 0] + k11 * normals[i32 + 0] + k01 * normals[i33 + 0]; + state.getNormal().y = k00 * normals[i30 + 1] + k10 * normals[i31 + 1] + k11 * normals[i32 + 1] + k01 * normals[i33 + 1]; + state.getNormal().z = k00 * normals[i30 + 2] + k10 * normals[i31 + 2] + k11 * normals[i32 + 2] + k01 * normals[i33 + 2]; + state.getNormal().set(state.transformNormalObjectToWorld(state.getNormal())); + state.getNormal().normalize(); + break; + } + case FACEVARYING: { + int idx = 3 * quad; + float[] normals = this.normals.data; + state.getNormal().x = k00 * normals[idx + 0] + k10 * normals[idx + 3] + k11 * normals[idx + 6] + k01 * normals[idx + 9]; + state.getNormal().y = k00 * normals[idx + 1] + k10 * normals[idx + 4] + k11 * normals[idx + 7] + k01 * normals[idx + 10]; + state.getNormal().z = k00 * normals[idx + 2] + k10 * normals[idx + 5] + k11 * normals[idx + 8] + k01 * normals[idx + 11]; + state.getNormal().set(state.transformNormalObjectToWorld(state.getNormal())); + state.getNormal().normalize(); + break; + } + } + float uv00 = 0, uv01 = 0, uv10 = 0, uv11 = 0, uv20 = 0, uv21 = 0, uv30 = 0, uv31 = 0; + switch (uvs.interp) { + case NONE: + case FACE: { + state.getUV().x = 0; + state.getUV().y = 0; + break; + } + case VERTEX: { + int i20 = 2 * index0; + int i21 = 2 * index1; + int i22 = 2 * index2; + int i23 = 2 * index3; + float[] uvs = this.uvs.data; + uv00 = uvs[i20 + 0]; + uv01 = uvs[i20 + 1]; + uv10 = uvs[i21 + 0]; + uv11 = uvs[i21 + 1]; + uv20 = uvs[i22 + 0]; + uv21 = uvs[i22 + 1]; + uv20 = uvs[i23 + 0]; + uv21 = uvs[i23 + 1]; + break; + } + case FACEVARYING: { + int idx = quad << 1; + float[] uvs = this.uvs.data; + uv00 = uvs[idx + 0]; + uv01 = uvs[idx + 1]; + uv10 = uvs[idx + 2]; + uv11 = uvs[idx + 3]; + uv20 = uvs[idx + 4]; + uv21 = uvs[idx + 5]; + uv30 = uvs[idx + 6]; + uv31 = uvs[idx + 7]; + break; + } + } + if (uvs.interp != InterpolationType.NONE) { + // get exact uv coords and compute tangent vectors + state.getUV().x = k00 * uv00 + k10 * uv10 + k11 * uv20 + k01 * uv30; + state.getUV().y = k00 * uv01 + k10 * uv11 + k11 * uv21 + k01 * uv31; + float du1 = uv00 - uv20; + float du2 = uv10 - uv20; + float dv1 = uv01 - uv21; + float dv2 = uv11 - uv21; + Vector3 dp1 = Point3.sub(v0p, v2p, new Vector3()), dp2 = Point3.sub(v1p, v2p, new Vector3()); + float determinant = du1 * dv2 - dv1 * du2; + if (determinant == 0.0f) { + // create basis in world space + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + } else { + float invdet = 1.f / determinant; + // Vector3 dpdu = new Vector3(); + // dpdu.x = (dv2 * dp1.x - dv1 * dp2.x) * invdet; + // dpdu.y = (dv2 * dp1.y - dv1 * dp2.y) * invdet; + // dpdu.z = (dv2 * dp1.z - dv1 * dp2.z) * invdet; + Vector3 dpdv = new Vector3(); + dpdv.x = (-du2 * dp1.x + du1 * dp2.x) * invdet; + dpdv.y = (-du2 * dp1.y + du1 * dp2.y) * invdet; + dpdv.z = (-du2 * dp1.z + du1 * dp2.z) * invdet; + dpdv = state.transformVectorObjectToWorld(dpdv); + // create basis in world space + state.setBasis(OrthoNormalBasis.makeFromWV(state.getNormal(), dpdv)); + } + } else + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + int shaderIndex = faceShaders == null ? 0 : (faceShaders[primID] & 0xFF); + state.setShader(parent.getShader(shaderIndex)); + state.setModifier(parent.getModifier(shaderIndex)); + } + + protected Point3 getPoint(int i) { + i *= 3; + return new Point3(points[i], points[i + 1], points[i + 2]); + } + + public PrimitiveList getBakingPrimitives() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/Sphere.java b/src/main/java/org/sunflow/core/primitive/Sphere.java new file mode 100644 index 0000000..5b55e74 --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/Sphere.java @@ -0,0 +1,89 @@ +package org.sunflow.core.primitive; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Solvers; +import org.sunflow.math.Vector3; + +public class Sphere implements PrimitiveList { + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(1); + if (o2w != null) + bounds = o2w.transform(bounds); + return bounds; + } + + public float getPrimitiveBound(int primID, int i) { + return (i & 1) == 0 ? -1 : 1; + } + + public int getNumPrimitives() { + return 1; + } + + public void prepareShadingState(ShadingState state) { + state.init(); + state.getRay().getPoint(state.getPoint()); + Instance parent = state.getInstance(); + Point3 localPoint = state.transformWorldToObject(state.getPoint()); + state.getNormal().set(localPoint.x, localPoint.y, localPoint.z); + state.getNormal().normalize(); + + float phi = (float) Math.atan2(state.getNormal().y, state.getNormal().x); + if (phi < 0) + phi += 2 * Math.PI; + float theta = (float) Math.acos(state.getNormal().z); + state.getUV().y = theta / (float) Math.PI; + state.getUV().x = phi / (float) (2 * Math.PI); + Vector3 v = new Vector3(); + v.x = -2 * (float) Math.PI * state.getNormal().y; + v.y = 2 * (float) Math.PI * state.getNormal().x; + v.z = 0; + state.setShader(parent.getShader(0)); + state.setModifier(parent.getModifier(0)); + // into world space + Vector3 worldNormal = state.transformNormalObjectToWorld(state.getNormal()); + v = state.transformVectorObjectToWorld(v); + state.getNormal().set(worldNormal); + state.getNormal().normalize(); + state.getGeoNormal().set(state.getNormal()); + // compute basis in world space + state.setBasis(OrthoNormalBasis.makeFromWV(state.getNormal(), v)); + + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + // intersect in local space + float qa = r.dx * r.dx + r.dy * r.dy + r.dz * r.dz; + float qb = 2 * ((r.dx * r.ox) + (r.dy * r.oy) + (r.dz * r.oz)); + float qc = ((r.ox * r.ox) + (r.oy * r.oy) + (r.oz * r.oz)) - 1; + double[] t = Solvers.solveQuadric(qa, qb, qc); + if (t != null) { + // early rejection + if (t[0] >= r.getMax() || t[1] <= r.getMin()) + return; + if (t[0] > r.getMin()) + r.setMax((float) t[0]); + else + r.setMax((float) t[1]); + state.setIntersection(0); + } + } + + public PrimitiveList getBakingPrimitives() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/SphereFlake.java b/src/main/java/org/sunflow/core/primitive/SphereFlake.java new file mode 100644 index 0000000..bc9942d --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/SphereFlake.java @@ -0,0 +1,219 @@ +package org.sunflow.core.primitive; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.MathUtils; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class SphereFlake implements PrimitiveList { + private static final int MAX_LEVEL = 20; + private static final float[] boundingRadiusOffset = new float[MAX_LEVEL + 1]; + private static final float[] recursivePattern = new float[9 * 3]; + private int level = 2; + private Vector3 axis = new Vector3(0, 0, 1); + private float baseRadius = 1; + + static { + // geometric series table, to compute bounding radius quickly + for (int i = 0, r = 3; i < boundingRadiusOffset.length; i++, r *= 3) + boundingRadiusOffset[i] = (r - 3.0f) / r; + // lower ring + double a = 0, daL = 2 * Math.PI / 6, daU = 2 * Math.PI / 3; + for (int i = 0; i < 6; i++) { + recursivePattern[3 * i + 0] = -0.3f; + recursivePattern[3 * i + 1] = (float) Math.sin(a); + recursivePattern[3 * i + 2] = (float) Math.cos(a); + a += daL; + } + a -= daL / 2; // tweak + for (int i = 6; i < 9; i++) { + recursivePattern[3 * i + 0] = +0.7f; + recursivePattern[3 * i + 1] = (float) Math.sin(a); + recursivePattern[3 * i + 2] = (float) Math.cos(a); + a += daU; + } + for (int i = 0; i < recursivePattern.length; i += 3) { + float x = recursivePattern[i + 0]; + float y = recursivePattern[i + 1]; + float z = recursivePattern[i + 2]; + float n = 1 / (float) Math.sqrt(x * x + y * y + z * z); + recursivePattern[i + 0] = x * n; + recursivePattern[i + 1] = y * n; + recursivePattern[i + 2] = z * n; + } + } + + public boolean update(ParameterList pl, SunflowAPI api) { + level = MathUtils.clamp(pl.getInt("level", level), 0, 20); + axis = pl.getVector("axis", axis); + axis.normalize(); + baseRadius = Math.abs(pl.getFloat("radius", baseRadius)); + return true; + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(getPrimitiveBound(0, 1)); + if (o2w != null) + bounds = o2w.transform(bounds); + return bounds; + } + + public float getPrimitiveBound(int primID, int i) { + float br = 1 + boundingRadiusOffset[level]; + return (i & 1) == 0 ? -br : br; + } + + public int getNumPrimitives() { + return 1; + } + + public void prepareShadingState(ShadingState state) { + state.init(); + state.getRay().getPoint(state.getPoint()); + Instance parent = state.getInstance(); + Point3 localPoint = state.transformWorldToObject(state.getPoint()); + + float cx = state.getU(); + float cy = state.getV(); + float cz = state.getW(); + + state.getNormal().set(localPoint.x - cx, localPoint.y - cy, localPoint.z - cz); + state.getNormal().normalize(); + + float phi = (float) Math.atan2(state.getNormal().y, state.getNormal().x); + if (phi < 0) + phi += 2 * Math.PI; + float theta = (float) Math.acos(state.getNormal().z); + state.getUV().y = theta / (float) Math.PI; + state.getUV().x = phi / (float) (2 * Math.PI); + Vector3 v = new Vector3(); + v.x = -2 * (float) Math.PI * state.getNormal().y; + v.y = 2 * (float) Math.PI * state.getNormal().x; + v.z = 0; + state.setShader(parent.getShader(0)); + state.setModifier(parent.getModifier(0)); + // into world space + Vector3 worldNormal = state.transformNormalObjectToWorld(state.getNormal()); + v = state.transformVectorObjectToWorld(v); + state.getNormal().set(worldNormal); + state.getNormal().normalize(); + state.getGeoNormal().set(state.getNormal()); + // compute basis in world space + state.setBasis(OrthoNormalBasis.makeFromWV(state.getNormal(), v)); + + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + // intersect in local space + float qa = r.dx * r.dx + r.dy * r.dy + r.dz * r.dz; + intersectFlake(r, state, level, qa, 1 / qa, 0, 0, 0, axis.x, axis.y, axis.z, baseRadius); + } + + private void intersectFlake(Ray r, IntersectionState state, int level, float qa, float qaInv, float cx, float cy, float cz, float dx, float dy, float dz, float radius) { + if (level <= 0) { + // we reached the bottom - intersect sphere and bail out + float vcx = cx - r.ox; + float vcy = cy - r.oy; + float vcz = cz - r.oz; + float b = r.dx * vcx + r.dy * vcy + r.dz * vcz; + float disc = b * b - qa * ((vcx * vcx + vcy * vcy + vcz * vcz) - radius * radius); + if (disc > 0) { + // intersects - check t values + float d = (float) Math.sqrt(disc); + float t1 = (b - d) * qaInv; + float t2 = (b + d) * qaInv; + if (t1 >= r.getMax() || t2 <= r.getMin()) + return; + if (t1 > r.getMin()) + r.setMax(t1); + else + r.setMax(t2); + state.setIntersection(0, cx, cy, cz); + } + } else { + float boundRadius = radius * (1 + boundingRadiusOffset[level]); + float vcx = cx - r.ox; + float vcy = cy - r.oy; + float vcz = cz - r.oz; + float b = r.dx * vcx + r.dy * vcy + r.dz * vcz; + float vcd = (vcx * vcx + vcy * vcy + vcz * vcz); + float disc = b * b - qa * (vcd - boundRadius * boundRadius); + if (disc > 0) { + // intersects - check t values + float d = (float) Math.sqrt(disc); + float t1 = (b - d) * qaInv; + float t2 = (b + d) * qaInv; + if (t1 >= r.getMax() || t2 <= r.getMin()) + return; + + // we hit the bounds, now compute intersection with the actual + // leaf sphere + disc = b * b - qa * (vcd - radius * radius); + if (disc > 0) { + d = (float) Math.sqrt(disc); + t1 = (b - d) * qaInv; + t2 = (b + d) * qaInv; + if (t1 >= r.getMax() || t2 <= r.getMin()) { + // no hit + } else { + if (t1 > r.getMin()) + r.setMax(t1); + else + r.setMax(t2); + state.setIntersection(0, cx, cy, cz); + } + } + + // recursively intersect 9 other spheres + // step1: compute basis around displacement vector + float b1x, b1y, b1z; + if (dx * dx < dy * dy && dx * dx < dz * dz) { + b1x = 0; + b1y = dz; + b1z = -dy; + } else if (dy * dy < dz * dz) { + b1x = dz; + b1y = 0; + b1z = -dx; + } else { + b1x = dy; + b1y = -dx; + b1z = 0; + } + float n = 1 / (float) Math.sqrt(b1x * b1x + b1y * b1y + b1z * b1z); + b1x *= n; + b1y *= n; + b1z *= n; + float b2x = dy * b1z - dz * b1y; + float b2y = dz * b1x - dx * b1z; + float b2z = dx * b1y - dy * b1x; + b1x = dy * b2z - dz * b2y; + b1y = dz * b2x - dx * b2z; + b1z = dx * b2y - dy * b2x; + // step2: generate 9 children recursively + float nr = radius * (1 / 3.0f), scale = radius + nr; + for (int i = 0; i < 9 * 3; i += 3) { + // transform by basis + float ndx = recursivePattern[i] * dx + recursivePattern[i + 1] * b1x + recursivePattern[i + 2] * b2x; + float ndy = recursivePattern[i] * dy + recursivePattern[i + 1] * b1y + recursivePattern[i + 2] * b2y; + float ndz = recursivePattern[i] * dz + recursivePattern[i + 1] * b1z + recursivePattern[i + 2] * b2z; + // recurse! + intersectFlake(r, state, level - 1, qa, qaInv, cx + scale * ndx, cy + scale * ndy, cz + scale * ndz, ndx, ndy, ndz, nr); + } + } + } + } + + public PrimitiveList getBakingPrimitives() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/Torus.java b/src/main/java/org/sunflow/core/primitive/Torus.java new file mode 100644 index 0000000..b42f52b --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/Torus.java @@ -0,0 +1,134 @@ +package org.sunflow.core.primitive; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.MathUtils; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Solvers; +import org.sunflow.math.Vector3; + +public class Torus implements PrimitiveList { + private float ri2, ro2; + private float ri, ro; + + public Torus() { + ri = 0.25f; + ro = 1; + ri2 = ri * ri; + ro2 = ro * ro; + + } + + public boolean update(ParameterList pl, SunflowAPI api) { + ri = pl.getFloat("radiusInner", ri); + ro = pl.getFloat("radiusOuter", ro); + ri2 = ri * ri; + ro2 = ro * ro; + return true; + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(-ro - ri, -ro - ri, -ri); + bounds.include(ro + ri, ro + ri, ri); + if (o2w != null) + bounds = o2w.transform(bounds); + return bounds; + } + + public float getPrimitiveBound(int primID, int i) { + switch (i) { + case 0: + case 2: + return -ro - ri; + case 1: + case 3: + return ro + ri; + case 4: + return -ri; + case 5: + return ri; + default: + return 0; + } + } + + public int getNumPrimitives() { + return 1; + } + + public void prepareShadingState(ShadingState state) { + state.init(); + state.getRay().getPoint(state.getPoint()); + Instance parent = state.getInstance(); + // get local point + Point3 p = state.transformWorldToObject(state.getPoint()); + // compute local normal + float deriv = p.x * p.x + p.y * p.y + p.z * p.z - ri2 - ro2; + state.getNormal().set(p.x * deriv, p.y * deriv, p.z * deriv + 2 * ro2 * p.z); + state.getNormal().normalize(); + + double phi = Math.asin(MathUtils.clamp(p.z / ri, -1, 1)); + double theta = Math.atan2(p.y, p.x); + if (theta < 0) + theta += 2 * Math.PI; + state.getUV().x = (float) (theta / (2 * Math.PI)); + state.getUV().y = (float) ((phi + Math.PI / 2) / Math.PI); + state.setShader(parent.getShader(0)); + state.setModifier(parent.getModifier(0)); + // into world space + Vector3 worldNormal = state.transformNormalObjectToWorld(state.getNormal()); + state.getNormal().set(worldNormal); + state.getNormal().normalize(); + state.getGeoNormal().set(state.getNormal()); + // make basis in world space + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + // intersect in local space + float rd2x = r.dx * r.dx; + float rd2y = r.dy * r.dy; + float rd2z = r.dz * r.dz; + float ro2x = r.ox * r.ox; + float ro2y = r.oy * r.oy; + float ro2z = r.oz * r.oz; + // compute some common factors + double alpha = rd2x + rd2y + rd2z; + double beta = 2 * (r.ox * r.dx + r.oy * r.dy + r.oz * r.dz); + double gamma = (ro2x + ro2y + ro2z) - ri2 - ro2; + // setup quartic coefficients + double A = alpha * alpha; + double B = 2 * alpha * beta; + double C = beta * beta + 2 * alpha * gamma + 4 * ro2 * rd2z; + double D = 2 * beta * gamma + 8 * ro2 * r.oz * r.dz; + double E = gamma * gamma + 4 * ro2 * ro2z - 4 * ro2 * ri2; + // solve equation + double[] t = Solvers.solveQuartic(A, B, C, D, E); + if (t != null) { + // early rejection + if (t[0] >= r.getMax() || t[t.length - 1] <= r.getMin()) + return; + // find first intersection in front of the ray + for (int i = 0; i < t.length; i++) { + if (t[i] > r.getMin()) { + r.setMax((float) t[i]); + state.setIntersection(0); + return; + } + } + } + } + + public PrimitiveList getBakingPrimitives() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/TriangleMesh.java b/src/main/java/org/sunflow/core/primitive/TriangleMesh.java new file mode 100644 index 0000000..399a043 --- /dev/null +++ b/src/main/java/org/sunflow/core/primitive/TriangleMesh.java @@ -0,0 +1,784 @@ +package org.sunflow.core.primitive; + +import java.io.FileWriter; +import java.io.IOException; +import java.util.Locale; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Ray; +import org.sunflow.core.ShadingState; +import org.sunflow.core.ParameterList.FloatParameter; +import org.sunflow.core.ParameterList.InterpolationType; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.MathUtils; +import org.sunflow.math.Matrix4; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class TriangleMesh implements PrimitiveList { + private static boolean smallTriangles = false; + protected float[] points; + protected int[] triangles; + private WaldTriangle[] triaccel; + private FloatParameter normals; + private FloatParameter uvs; + private byte[] faceShaders; + + public static void setSmallTriangles(boolean smallTriangles) { + if (smallTriangles) + UI.printInfo(Module.GEOM, "Small trimesh mode: enabled"); + else + UI.printInfo(Module.GEOM, "Small trimesh mode: disabled"); + TriangleMesh.smallTriangles = smallTriangles; + } + + public TriangleMesh() { + triangles = null; + points = null; + normals = uvs = new FloatParameter(); + faceShaders = null; + } + + public void writeObj(String filename) { + try { + FileWriter file = new FileWriter(filename); + file.write(String.format("o object\n")); + for (int i = 0; i < points.length; i += 3) + file.write(String.format("v %g %g %g\n", points[i], points[i + 1], points[i + 2])); + file.write("s off\n"); + for (int i = 0; i < triangles.length; i += 3) + file.write(String.format("f %d %d %d\n", triangles[i] + 1, triangles[i + 1] + 1, triangles[i + 2] + 1)); + file.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public boolean update(ParameterList pl, SunflowAPI api) { + boolean updatedTopology = false; + { + int[] triangles = pl.getIntArray("triangles"); + if (triangles != null) { + this.triangles = triangles; + updatedTopology = true; + } + } + if (triangles == null) { + UI.printError(Module.GEOM, "Unable to update mesh - triangle indices are missing"); + return false; + } + if (triangles.length % 3 != 0) + UI.printWarning(Module.GEOM, "Triangle index data is not a multiple of 3 - triangles may be missing"); + pl.setFaceCount(triangles.length / 3); + { + FloatParameter pointsP = pl.getPointArray("points"); + if (pointsP != null) + if (pointsP.interp != InterpolationType.VERTEX) + UI.printError(Module.GEOM, "Point interpolation type must be set to \"vertex\" - was \"%s\"", pointsP.interp.name().toLowerCase(Locale.ENGLISH)); + else { + points = pointsP.data; + updatedTopology = true; + } + } + if (points == null) { + UI.printError(Module.GEOM, "Unable to update mesh - vertices are missing"); + return false; + } + pl.setVertexCount(points.length / 3); + pl.setFaceVertexCount(3 * (triangles.length / 3)); + FloatParameter normals = pl.getVectorArray("normals"); + if (normals != null) + this.normals = normals; + FloatParameter uvs = pl.getTexCoordArray("uvs"); + if (uvs != null) + this.uvs = uvs; + int[] faceShaders = pl.getIntArray("faceshaders"); + if (faceShaders != null && faceShaders.length == triangles.length / 3) { + this.faceShaders = new byte[faceShaders.length]; + for (int i = 0; i < faceShaders.length; i++) { + int v = faceShaders[i]; + if (v > 255) + UI.printWarning(Module.GEOM, "Shader index too large on triangle %d", i); + this.faceShaders[i] = (byte) (v & 0xFF); + } + } + if (updatedTopology) { + // create triangle acceleration structure + init(); + } + return true; + } + + public float getPrimitiveBound(int primID, int i) { + int tri = 3 * primID; + int a = 3 * triangles[tri + 0]; + int b = 3 * triangles[tri + 1]; + int c = 3 * triangles[tri + 2]; + int axis = i >>> 1; + if ((i & 1) == 0) + return MathUtils.min(points[a + axis], points[b + axis], points[c + axis]); + else + return MathUtils.max(points[a + axis], points[b + axis], points[c + axis]); + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(); + if (o2w == null) { + for (int i = 0; i < points.length; i += 3) + bounds.include(points[i], points[i + 1], points[i + 2]); + } else { + // transform vertices first + for (int i = 0; i < points.length; i += 3) { + float x = points[i]; + float y = points[i + 1]; + float z = points[i + 2]; + float wx = o2w.transformPX(x, y, z); + float wy = o2w.transformPY(x, y, z); + float wz = o2w.transformPZ(x, y, z); + bounds.include(wx, wy, wz); + } + } + return bounds; + } + + private final void intersectTriangleKensler(Ray r, int primID, IntersectionState state) { + int tri = 3 * primID; + int a = 3 * triangles[tri + 0]; + int b = 3 * triangles[tri + 1]; + int c = 3 * triangles[tri + 2]; + float edge0x = points[b + 0] - points[a + 0]; + float edge0y = points[b + 1] - points[a + 1]; + float edge0z = points[b + 2] - points[a + 2]; + float edge1x = points[a + 0] - points[c + 0]; + float edge1y = points[a + 1] - points[c + 1]; + float edge1z = points[a + 2] - points[c + 2]; + float nx = edge0y * edge1z - edge0z * edge1y; + float ny = edge0z * edge1x - edge0x * edge1z; + float nz = edge0x * edge1y - edge0y * edge1x; + float v = r.dot(nx, ny, nz); + float iv = 1 / v; + float edge2x = points[a + 0] - r.ox; + float edge2y = points[a + 1] - r.oy; + float edge2z = points[a + 2] - r.oz; + float va = nx * edge2x + ny * edge2y + nz * edge2z; + float t = iv * va; + if (!r.isInside(t)) + return; + float ix = edge2y * r.dz - edge2z * r.dy; + float iy = edge2z * r.dx - edge2x * r.dz; + float iz = edge2x * r.dy - edge2y * r.dx; + float v1 = ix * edge1x + iy * edge1y + iz * edge1z; + float beta = iv * v1; + if (beta < 0) + return; + float v2 = ix * edge0x + iy * edge0y + iz * edge0z; + if ((v1 + v2) * v > v * v) + return; + float gamma = iv * v2; + if (gamma < 0) + return; + r.setMax(t); + state.setIntersection(primID, beta, gamma); + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + // alternative test -- disabled for now + // intersectPrimitiveRobust(r, primID, state); + + if (triaccel != null) { + // optional fast intersection method + triaccel[primID].intersect(r, primID, state); + return; + } + intersectTriangleKensler(r, primID, state); + } + + public int getNumPrimitives() { + return triangles.length / 3; + } + + public void prepareShadingState(ShadingState state) { + state.init(); + Instance parent = state.getInstance(); + int primID = state.getPrimitiveID(); + float u = state.getU(); + float v = state.getV(); + float w = 1 - u - v; + state.getRay().getPoint(state.getPoint()); + int tri = 3 * primID; + int index0 = triangles[tri + 0]; + int index1 = triangles[tri + 1]; + int index2 = triangles[tri + 2]; + Point3 v0p = getPoint(index0); + Point3 v1p = getPoint(index1); + Point3 v2p = getPoint(index2); + Vector3 ng = Point3.normal(v0p, v1p, v2p); + ng = state.transformNormalObjectToWorld(ng); + ng.normalize(); + state.getGeoNormal().set(ng); + switch (normals.interp) { + case NONE: + case FACE: { + state.getNormal().set(ng); + break; + } + case VERTEX: { + int i30 = 3 * index0; + int i31 = 3 * index1; + int i32 = 3 * index2; + float[] normals = this.normals.data; + state.getNormal().x = w * normals[i30 + 0] + u * normals[i31 + 0] + v * normals[i32 + 0]; + state.getNormal().y = w * normals[i30 + 1] + u * normals[i31 + 1] + v * normals[i32 + 1]; + state.getNormal().z = w * normals[i30 + 2] + u * normals[i31 + 2] + v * normals[i32 + 2]; + state.getNormal().set(state.transformNormalObjectToWorld(state.getNormal())); + state.getNormal().normalize(); + break; + } + case FACEVARYING: { + int idx = 3 * tri; + float[] normals = this.normals.data; + state.getNormal().x = w * normals[idx + 0] + u * normals[idx + 3] + v * normals[idx + 6]; + state.getNormal().y = w * normals[idx + 1] + u * normals[idx + 4] + v * normals[idx + 7]; + state.getNormal().z = w * normals[idx + 2] + u * normals[idx + 5] + v * normals[idx + 8]; + state.getNormal().set(state.transformNormalObjectToWorld(state.getNormal())); + state.getNormal().normalize(); + break; + } + } + float uv00 = 0, uv01 = 0, uv10 = 0, uv11 = 0, uv20 = 0, uv21 = 0; + switch (uvs.interp) { + case NONE: + case FACE: { + state.getUV().x = 0; + state.getUV().y = 0; + break; + } + case VERTEX: { + int i20 = 2 * index0; + int i21 = 2 * index1; + int i22 = 2 * index2; + float[] uvs = this.uvs.data; + uv00 = uvs[i20 + 0]; + uv01 = uvs[i20 + 1]; + uv10 = uvs[i21 + 0]; + uv11 = uvs[i21 + 1]; + uv20 = uvs[i22 + 0]; + uv21 = uvs[i22 + 1]; + break; + } + case FACEVARYING: { + int idx = tri << 1; + float[] uvs = this.uvs.data; + uv00 = uvs[idx + 0]; + uv01 = uvs[idx + 1]; + uv10 = uvs[idx + 2]; + uv11 = uvs[idx + 3]; + uv20 = uvs[idx + 4]; + uv21 = uvs[idx + 5]; + break; + } + } + if (uvs.interp != InterpolationType.NONE) { + // get exact uv coords and compute tangent vectors + state.getUV().x = w * uv00 + u * uv10 + v * uv20; + state.getUV().y = w * uv01 + u * uv11 + v * uv21; + float du1 = uv00 - uv20; + float du2 = uv10 - uv20; + float dv1 = uv01 - uv21; + float dv2 = uv11 - uv21; + Vector3 dp1 = Point3.sub(v0p, v2p, new Vector3()), dp2 = Point3.sub(v1p, v2p, new Vector3()); + float determinant = du1 * dv2 - dv1 * du2; + if (determinant == 0.0f) { + // create basis in world space + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + } else { + float invdet = 1.f / determinant; + // Vector3 dpdu = new Vector3(); + // dpdu.x = (dv2 * dp1.x - dv1 * dp2.x) * invdet; + // dpdu.y = (dv2 * dp1.y - dv1 * dp2.y) * invdet; + // dpdu.z = (dv2 * dp1.z - dv1 * dp2.z) * invdet; + Vector3 dpdv = new Vector3(); + dpdv.x = (-du2 * dp1.x + du1 * dp2.x) * invdet; + dpdv.y = (-du2 * dp1.y + du1 * dp2.y) * invdet; + dpdv.z = (-du2 * dp1.z + du1 * dp2.z) * invdet; + dpdv = state.transformVectorObjectToWorld(dpdv); + // create basis in world space + state.setBasis(OrthoNormalBasis.makeFromWV(state.getNormal(), dpdv)); + } + } else + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + int shaderIndex = faceShaders == null ? 0 : (faceShaders[primID] & 0xFF); + state.setShader(parent.getShader(shaderIndex)); + state.setModifier(parent.getModifier(shaderIndex)); + } + + public void init() { + triaccel = null; + int nt = getNumPrimitives(); + if (!smallTriangles) { + // too many triangles? -- don't generate triaccel to save memory + if (nt > 2000000) { + UI.printWarning(Module.GEOM, "TRI - Too many triangles -- triaccel generation skipped"); + return; + } + triaccel = new WaldTriangle[nt]; + for (int i = 0; i < nt; i++) + triaccel[i] = new WaldTriangle(this, i); + } + } + + protected Point3 getPoint(int i) { + i *= 3; + return new Point3(points[i], points[i + 1], points[i + 2]); + } + + public void getPoint(int tri, int i, Point3 p) { + int index = 3 * triangles[3 * tri + i]; + p.set(points[index], points[index + 1], points[index + 2]); + } + + private static final class WaldTriangle { + // private data for fast triangle intersection testing + private int k; + private float nu, nv, nd; + private float bnu, bnv, bnd; + private float cnu, cnv, cnd; + + private WaldTriangle(TriangleMesh mesh, int tri) { + k = 0; + tri *= 3; + int index0 = mesh.triangles[tri + 0]; + int index1 = mesh.triangles[tri + 1]; + int index2 = mesh.triangles[tri + 2]; + Point3 v0p = mesh.getPoint(index0); + Point3 v1p = mesh.getPoint(index1); + Point3 v2p = mesh.getPoint(index2); + Vector3 ng = Point3.normal(v0p, v1p, v2p); + if (Math.abs(ng.x) > Math.abs(ng.y) && Math.abs(ng.x) > Math.abs(ng.z)) + k = 0; + else if (Math.abs(ng.y) > Math.abs(ng.z)) + k = 1; + else + k = 2; + float ax, ay, bx, by, cx, cy; + switch (k) { + case 0: { + nu = ng.y / ng.x; + nv = ng.z / ng.x; + nd = v0p.x + (nu * v0p.y) + (nv * v0p.z); + ax = v0p.y; + ay = v0p.z; + bx = v2p.y - ax; + by = v2p.z - ay; + cx = v1p.y - ax; + cy = v1p.z - ay; + break; + } + case 1: { + nu = ng.z / ng.y; + nv = ng.x / ng.y; + nd = (nv * v0p.x) + v0p.y + (nu * v0p.z); + ax = v0p.z; + ay = v0p.x; + bx = v2p.z - ax; + by = v2p.x - ay; + cx = v1p.z - ax; + cy = v1p.x - ay; + break; + } + case 2: + default: { + nu = ng.x / ng.z; + nv = ng.y / ng.z; + nd = (nu * v0p.x) + (nv * v0p.y) + v0p.z; + ax = v0p.x; + ay = v0p.y; + bx = v2p.x - ax; + by = v2p.y - ay; + cx = v1p.x - ax; + cy = v1p.y - ay; + } + } + float det = bx * cy - by * cx; + bnu = -by / det; + bnv = bx / det; + bnd = (by * ax - bx * ay) / det; + cnu = cy / det; + cnv = -cx / det; + cnd = (cx * ay - cy * ax) / det; + } + + void intersect(Ray r, int primID, IntersectionState state) { + switch (k) { + case 0: { + float det = 1.0f / (r.dx + nu * r.dy + nv * r.dz); + float t = (nd - r.ox - nu * r.oy - nv * r.oz) * det; + if (!r.isInside(t)) + return; + float hu = r.oy + t * r.dy; + float hv = r.oz + t * r.dz; + float u = hu * bnu + hv * bnv + bnd; + if (u < 0.0f) + return; + float v = hu * cnu + hv * cnv + cnd; + if (v < 0.0f) + return; + if (u + v > 1.0f) + return; + r.setMax(t); + state.setIntersection(primID, u, v); + return; + } + case 1: { + float det = 1.0f / (r.dy + nu * r.dz + nv * r.dx); + float t = (nd - r.oy - nu * r.oz - nv * r.ox) * det; + if (!r.isInside(t)) + return; + float hu = r.oz + t * r.dz; + float hv = r.ox + t * r.dx; + float u = hu * bnu + hv * bnv + bnd; + if (u < 0.0f) + return; + float v = hu * cnu + hv * cnv + cnd; + if (v < 0.0f) + return; + if (u + v > 1.0f) + return; + r.setMax(t); + state.setIntersection(primID, u, v); + return; + } + case 2: { + float det = 1.0f / (r.dz + nu * r.dx + nv * r.dy); + float t = (nd - r.oz - nu * r.ox - nv * r.oy) * det; + if (!r.isInside(t)) + return; + float hu = r.ox + t * r.dx; + float hv = r.oy + t * r.dy; + float u = hu * bnu + hv * bnv + bnd; + if (u < 0.0f) + return; + float v = hu * cnu + hv * cnv + cnd; + if (v < 0.0f) + return; + if (u + v > 1.0f) + return; + r.setMax(t); + state.setIntersection(primID, u, v); + return; + } + } + } + } + + public PrimitiveList getBakingPrimitives() { + switch (uvs.interp) { + case NONE: + case FACE: + UI.printError(Module.GEOM, "Cannot generate baking surface without texture coordinate data"); + return null; + default: + return new BakingSurface(); + } + } + + private class BakingSurface implements PrimitiveList { + public PrimitiveList getBakingPrimitives() { + return null; + } + + public int getNumPrimitives() { + return TriangleMesh.this.getNumPrimitives(); + } + + public float getPrimitiveBound(int primID, int i) { + if (i > 3) + return 0; + switch (uvs.interp) { + case NONE: + case FACE: + default: { + return 0; + } + case VERTEX: { + int tri = 3 * primID; + int index0 = triangles[tri + 0]; + int index1 = triangles[tri + 1]; + int index2 = triangles[tri + 2]; + int i20 = 2 * index0; + int i21 = 2 * index1; + int i22 = 2 * index2; + float[] uvs = TriangleMesh.this.uvs.data; + switch (i) { + case 0: + return MathUtils.min(uvs[i20 + 0], uvs[i21 + 0], uvs[i22 + 0]); + case 1: + return MathUtils.max(uvs[i20 + 0], uvs[i21 + 0], uvs[i22 + 0]); + case 2: + return MathUtils.min(uvs[i20 + 1], uvs[i21 + 1], uvs[i22 + 1]); + case 3: + return MathUtils.max(uvs[i20 + 1], uvs[i21 + 1], uvs[i22 + 1]); + default: + return 0; + } + } + case FACEVARYING: { + int idx = 6 * primID; + float[] uvs = TriangleMesh.this.uvs.data; + switch (i) { + case 0: + return MathUtils.min(uvs[idx + 0], uvs[idx + 2], uvs[idx + 4]); + case 1: + return MathUtils.max(uvs[idx + 0], uvs[idx + 2], uvs[idx + 4]); + case 2: + return MathUtils.min(uvs[idx + 1], uvs[idx + 3], uvs[idx + 5]); + case 3: + return MathUtils.max(uvs[idx + 1], uvs[idx + 3], uvs[idx + 5]); + default: + return 0; + } + } + } + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(); + if (o2w == null) { + for (int i = 0; i < uvs.data.length; i += 2) + bounds.include(uvs.data[i], uvs.data[i + 1], 0); + } else { + // transform vertices first + for (int i = 0; i < uvs.data.length; i += 2) { + float x = uvs.data[i]; + float y = uvs.data[i + 1]; + float wx = o2w.transformPX(x, y, 0); + float wy = o2w.transformPY(x, y, 0); + float wz = o2w.transformPZ(x, y, 0); + bounds.include(wx, wy, wz); + } + } + return bounds; + } + + public void intersectPrimitive(Ray r, int primID, IntersectionState state) { + float uv00 = 0, uv01 = 0, uv10 = 0, uv11 = 0, uv20 = 0, uv21 = 0; + switch (uvs.interp) { + case NONE: + case FACE: + default: + return; + case VERTEX: { + int tri = 3 * primID; + int index0 = triangles[tri + 0]; + int index1 = triangles[tri + 1]; + int index2 = triangles[tri + 2]; + int i20 = 2 * index0; + int i21 = 2 * index1; + int i22 = 2 * index2; + float[] uvs = TriangleMesh.this.uvs.data; + uv00 = uvs[i20 + 0]; + uv01 = uvs[i20 + 1]; + uv10 = uvs[i21 + 0]; + uv11 = uvs[i21 + 1]; + uv20 = uvs[i22 + 0]; + uv21 = uvs[i22 + 1]; + break; + + } + case FACEVARYING: { + int idx = (3 * primID) << 1; + float[] uvs = TriangleMesh.this.uvs.data; + uv00 = uvs[idx + 0]; + uv01 = uvs[idx + 1]; + uv10 = uvs[idx + 2]; + uv11 = uvs[idx + 3]; + uv20 = uvs[idx + 4]; + uv21 = uvs[idx + 5]; + break; + } + } + + double edge1x = uv10 - uv00; + double edge1y = uv11 - uv01; + double edge2x = uv20 - uv00; + double edge2y = uv21 - uv01; + double pvecx = r.dy * 0 - r.dz * edge2y; + double pvecy = r.dz * edge2x - r.dx * 0; + double pvecz = r.dx * edge2y - r.dy * edge2x; + double qvecx, qvecy, qvecz; + double u, v; + double det = edge1x * pvecx + edge1y * pvecy + 0 * pvecz; + if (det > 0) { + double tvecx = r.ox - uv00; + double tvecy = r.oy - uv01; + double tvecz = r.oz; + u = (tvecx * pvecx + tvecy * pvecy + tvecz * pvecz); + if (u < 0.0 || u > det) + return; + qvecx = tvecy * 0 - tvecz * edge1y; + qvecy = tvecz * edge1x - tvecx * 0; + qvecz = tvecx * edge1y - tvecy * edge1x; + v = (r.dx * qvecx + r.dy * qvecy + r.dz * qvecz); + if (v < 0.0 || u + v > det) + return; + } else if (det < 0) { + double tvecx = r.ox - uv00; + double tvecy = r.oy - uv01; + double tvecz = r.oz; + u = (tvecx * pvecx + tvecy * pvecy + tvecz * pvecz); + if (u > 0.0 || u < det) + return; + qvecx = tvecy * 0 - tvecz * edge1y; + qvecy = tvecz * edge1x - tvecx * 0; + qvecz = tvecx * edge1y - tvecy * edge1x; + v = (r.dx * qvecx + r.dy * qvecy + r.dz * qvecz); + if (v > 0.0 || u + v < det) + return; + } else + return; + double inv_det = 1.0 / det; + float t = (float) ((edge2x * qvecx + edge2y * qvecy + 0 * qvecz) * inv_det); + if (r.isInside(t)) { + r.setMax(t); + state.setIntersection(primID, (float) (u * inv_det), (float) (v * inv_det)); + } + } + + public void prepareShadingState(ShadingState state) { + state.init(); + Instance parent = state.getInstance(); + int primID = state.getPrimitiveID(); + float u = state.getU(); + float v = state.getV(); + float w = 1 - u - v; + // state.getRay().getPoint(state.getPoint()); + int tri = 3 * primID; + int index0 = triangles[tri + 0]; + int index1 = triangles[tri + 1]; + int index2 = triangles[tri + 2]; + Point3 v0p = getPoint(index0); + Point3 v1p = getPoint(index1); + Point3 v2p = getPoint(index2); + + // get object space point from barycentric coordinates + state.getPoint().x = w * v0p.x + u * v1p.x + v * v2p.x; + state.getPoint().y = w * v0p.y + u * v1p.y + v * v2p.y; + state.getPoint().z = w * v0p.z + u * v1p.z + v * v2p.z; + // move into world space + state.getPoint().set(state.transformObjectToWorld(state.getPoint())); + + Vector3 ng = Point3.normal(v0p, v1p, v2p); + if (parent != null) + ng = state.transformNormalObjectToWorld(ng); + ng.normalize(); + state.getGeoNormal().set(ng); + switch (normals.interp) { + case NONE: + case FACE: { + state.getNormal().set(ng); + break; + } + case VERTEX: { + int i30 = 3 * index0; + int i31 = 3 * index1; + int i32 = 3 * index2; + float[] normals = TriangleMesh.this.normals.data; + state.getNormal().x = w * normals[i30 + 0] + u * normals[i31 + 0] + v * normals[i32 + 0]; + state.getNormal().y = w * normals[i30 + 1] + u * normals[i31 + 1] + v * normals[i32 + 1]; + state.getNormal().z = w * normals[i30 + 2] + u * normals[i31 + 2] + v * normals[i32 + 2]; + if (parent != null) + state.getNormal().set(state.transformNormalObjectToWorld(state.getNormal())); + state.getNormal().normalize(); + break; + } + case FACEVARYING: { + int idx = 3 * tri; + float[] normals = TriangleMesh.this.normals.data; + state.getNormal().x = w * normals[idx + 0] + u * normals[idx + 3] + v * normals[idx + 6]; + state.getNormal().y = w * normals[idx + 1] + u * normals[idx + 4] + v * normals[idx + 7]; + state.getNormal().z = w * normals[idx + 2] + u * normals[idx + 5] + v * normals[idx + 8]; + if (parent != null) + state.getNormal().set(state.transformNormalObjectToWorld(state.getNormal())); + state.getNormal().normalize(); + break; + } + } + float uv00 = 0, uv01 = 0, uv10 = 0, uv11 = 0, uv20 = 0, uv21 = 0; + switch (uvs.interp) { + case NONE: + case FACE: { + state.getUV().x = 0; + state.getUV().y = 0; + break; + } + case VERTEX: { + int i20 = 2 * index0; + int i21 = 2 * index1; + int i22 = 2 * index2; + float[] uvs = TriangleMesh.this.uvs.data; + uv00 = uvs[i20 + 0]; + uv01 = uvs[i20 + 1]; + uv10 = uvs[i21 + 0]; + uv11 = uvs[i21 + 1]; + uv20 = uvs[i22 + 0]; + uv21 = uvs[i22 + 1]; + break; + } + case FACEVARYING: { + int idx = tri << 1; + float[] uvs = TriangleMesh.this.uvs.data; + uv00 = uvs[idx + 0]; + uv01 = uvs[idx + 1]; + uv10 = uvs[idx + 2]; + uv11 = uvs[idx + 3]; + uv20 = uvs[idx + 4]; + uv21 = uvs[idx + 5]; + break; + } + } + if (uvs.interp != InterpolationType.NONE) { + // get exact uv coords and compute tangent vectors + state.getUV().x = w * uv00 + u * uv10 + v * uv20; + state.getUV().y = w * uv01 + u * uv11 + v * uv21; + float du1 = uv00 - uv20; + float du2 = uv10 - uv20; + float dv1 = uv01 - uv21; + float dv2 = uv11 - uv21; + Vector3 dp1 = Point3.sub(v0p, v2p, new Vector3()), dp2 = Point3.sub(v1p, v2p, new Vector3()); + float determinant = du1 * dv2 - dv1 * du2; + if (determinant == 0.0f) { + // create basis in world space + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + } else { + float invdet = 1.f / determinant; + // Vector3 dpdu = new Vector3(); + // dpdu.x = (dv2 * dp1.x - dv1 * dp2.x) * invdet; + // dpdu.y = (dv2 * dp1.y - dv1 * dp2.y) * invdet; + // dpdu.z = (dv2 * dp1.z - dv1 * dp2.z) * invdet; + Vector3 dpdv = new Vector3(); + dpdv.x = (-du2 * dp1.x + du1 * dp2.x) * invdet; + dpdv.y = (-du2 * dp1.y + du1 * dp2.y) * invdet; + dpdv.z = (-du2 * dp1.z + du1 * dp2.z) * invdet; + if (parent != null) + dpdv = state.transformVectorObjectToWorld(dpdv); + // create basis in world space + state.setBasis(OrthoNormalBasis.makeFromWV(state.getNormal(), dpdv)); + } + } else + state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); + int shaderIndex = faceShaders == null ? 0 : (faceShaders[primID] & 0xFF); + state.setShader(parent.getShader(shaderIndex)); + } + + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/renderer/BucketRenderer.java b/src/main/java/org/sunflow/core/renderer/BucketRenderer.java new file mode 100644 index 0000000..3cea7f9 --- /dev/null +++ b/src/main/java/org/sunflow/core/renderer/BucketRenderer.java @@ -0,0 +1,469 @@ +package org.sunflow.core.renderer; + +import org.sunflow.PluginRegistry; +import org.sunflow.core.BucketOrder; +import org.sunflow.core.Display; +import org.sunflow.core.Filter; +import org.sunflow.core.ImageSampler; +import org.sunflow.core.Instance; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.Options; +import org.sunflow.core.Scene; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.core.bucket.BucketOrderFactory; +import org.sunflow.core.filter.BoxFilter; +import org.sunflow.image.Color; +import org.sunflow.image.formats.GenericBitmap; +import org.sunflow.math.MathUtils; +import org.sunflow.math.QMC; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class BucketRenderer implements ImageSampler { + private Scene scene; + private Display display; + // resolution + private int imageWidth; + private int imageHeight; + // bucketing + private String bucketOrderName; + private BucketOrder bucketOrder; + private int bucketSize; + private int bucketCounter; + private int[] bucketCoords; + private boolean dumpBuckets; + + // anti-aliasing + private int minAADepth; + private int maxAADepth; + private int superSampling; + private float contrastThreshold; + private boolean jitter; + private boolean displayAA; + + // derived quantities + private double invSuperSampling; + private int subPixelSize; + private int minStepSize; + private int maxStepSize; + private int sigmaOrder; + private int sigmaLength; + private float thresh; + private boolean useJitter; + + // filtering + private String filterName; + private Filter filter; + private int fs; + private float fhs; + + public BucketRenderer() { + bucketSize = 32; + bucketOrderName = "hilbert"; + displayAA = false; + contrastThreshold = 0.1f; + filterName = "box"; + jitter = false; // off by default + dumpBuckets = false; // for debugging only - not user settable + } + + public boolean prepare(Options options, Scene scene, int w, int h) { + this.scene = scene; + imageWidth = w; + imageHeight = h; + + // fetch options + bucketSize = options.getInt("bucket.size", bucketSize); + bucketOrderName = options.getString("bucket.order", bucketOrderName); + minAADepth = options.getInt("aa.min", minAADepth); + maxAADepth = options.getInt("aa.max", maxAADepth); + superSampling = options.getInt("aa.samples", superSampling); + displayAA = options.getBoolean("aa.display", displayAA); + jitter = options.getBoolean("aa.jitter", jitter); + contrastThreshold = options.getFloat("aa.contrast", contrastThreshold); + + // limit bucket size and compute number of buckets in each direction + bucketSize = MathUtils.clamp(bucketSize, 16, 512); + int numBucketsX = (imageWidth + bucketSize - 1) / bucketSize; + int numBucketsY = (imageHeight + bucketSize - 1) / bucketSize; + bucketOrder = BucketOrderFactory.create(bucketOrderName); + bucketCoords = bucketOrder.getBucketSequence(numBucketsX, numBucketsY); + // validate AA options + minAADepth = MathUtils.clamp(minAADepth, -4, 5); + maxAADepth = MathUtils.clamp(maxAADepth, minAADepth, 5); + superSampling = MathUtils.clamp(superSampling, 1, 256); + invSuperSampling = 1.0 / superSampling; + // compute AA stepping sizes + subPixelSize = (maxAADepth > 0) ? (1 << maxAADepth) : 1; + minStepSize = maxAADepth >= 0 ? 1 : 1 << (-maxAADepth); + if (minAADepth == maxAADepth) + maxStepSize = minStepSize; + else + maxStepSize = minAADepth > 0 ? 1 << minAADepth : subPixelSize << (-minAADepth); + useJitter = jitter && maxAADepth > 0; + // compute anti-aliasing contrast thresholds + contrastThreshold = MathUtils.clamp(contrastThreshold, 0, 1); + thresh = contrastThreshold * (float) Math.pow(2.0f, minAADepth); + // read filter settings from scene + filterName = options.getString("filter", filterName); + filter = PluginRegistry.filterPlugins.createObject(filterName); + // adjust filter + if (filter == null) { + UI.printWarning(Module.BCKT, "Unrecognized filter type: \"%s\" - defaulting to box", filterName); + filter = new BoxFilter(); + filterName = "box"; + } + fhs = filter.getSize() * 0.5f; + fs = (int) Math.ceil(subPixelSize * (fhs - 0.5f)); + + // prepare QMC sampling + sigmaOrder = Math.min(QMC.MAX_SIGMA_ORDER, Math.max(0, maxAADepth) + 13); // FIXME: how big should the table be? + sigmaLength = 1 << sigmaOrder; + UI.printInfo(Module.BCKT, "Bucket renderer settings:"); + UI.printInfo(Module.BCKT, " * Resolution: %dx%d", imageWidth, imageHeight); + UI.printInfo(Module.BCKT, " * Bucket size: %d", bucketSize); + UI.printInfo(Module.BCKT, " * Number of buckets: %dx%d", numBucketsX, numBucketsY); + if (minAADepth != maxAADepth) + UI.printInfo(Module.BCKT, " * Anti-aliasing: %s -> %s (adaptive)", aaDepthToString(minAADepth), aaDepthToString(maxAADepth)); + else + UI.printInfo(Module.BCKT, " * Anti-aliasing: %s (fixed)", aaDepthToString(minAADepth)); + UI.printInfo(Module.BCKT, " * Rays per sample: %d", superSampling); + UI.printInfo(Module.BCKT, " * Subpixel jitter: %s", useJitter ? "on" : (jitter ? "auto-off" : "off")); + UI.printInfo(Module.BCKT, " * Contrast threshold: %.2f", contrastThreshold); + UI.printInfo(Module.BCKT, " * Filter type: %s", filterName); + UI.printInfo(Module.BCKT, " * Filter size: %.2f pixels", filter.getSize()); + return true; + } + + private String aaDepthToString(int depth) { + int pixelAA = (depth) < 0 ? -(1 << (-depth)) : (1 << depth); + return String.format("%s%d sample%s", depth < 0 ? "1/" : "", pixelAA * pixelAA, depth == 0 ? "" : "s"); + } + + public void render(Display display) { + this.display = display; + display.imageBegin(imageWidth, imageHeight, bucketSize); + // set members variables + bucketCounter = 0; + // start task + UI.taskStart("Rendering", 0, bucketCoords.length); + Timer timer = new Timer(); + timer.start(); + BucketThread[] renderThreads = new BucketThread[scene.getThreads()]; + for (int i = 0; i < renderThreads.length; i++) { + renderThreads[i] = new BucketThread(i); + renderThreads[i].setPriority(scene.getThreadPriority()); + renderThreads[i].start(); + } + for (int i = 0; i < renderThreads.length; i++) { + try { + renderThreads[i].join(); + } catch (InterruptedException e) { + UI.printError(Module.BCKT, "Bucket processing thread %d of %d was interrupted", i + 1, renderThreads.length); + } finally { + renderThreads[i].updateStats(); + } + } + UI.taskStop(); + timer.end(); + UI.printInfo(Module.BCKT, "Render time: %s", timer.toString()); + display.imageEnd(); + } + + private class BucketThread extends Thread { + private final int threadID; + private final IntersectionState istate; + + BucketThread(int threadID) { + this.threadID = threadID; + istate = new IntersectionState(); + } + + @Override + public void run() { + while (true) { + int bx, by; + synchronized (BucketRenderer.this) { + if (bucketCounter >= bucketCoords.length) + return; + UI.taskUpdate(bucketCounter); + bx = bucketCoords[bucketCounter + 0]; + by = bucketCoords[bucketCounter + 1]; + bucketCounter += 2; + } + renderBucket(display, bx, by, threadID, istate); + if (UI.taskCanceled()) + return; + } + } + + void updateStats() { + scene.accumulateStats(istate); + } + } + + private void renderBucket(Display display, int bx, int by, int threadID, IntersectionState istate) { + // pixel sized extents + int x0 = bx * bucketSize; + int y0 = by * bucketSize; + int bw = Math.min(bucketSize, imageWidth - x0); + int bh = Math.min(bucketSize, imageHeight - y0); + + // prepare bucket + display.imagePrepare(x0, y0, bw, bh, threadID); + + Color[] bucketRGB = new Color[bw * bh]; + float[] bucketAlpha = new float[bw * bh]; + + // subpixel extents + int sx0 = x0 * subPixelSize - fs; + int sy0 = y0 * subPixelSize - fs; + int sbw = bw * subPixelSize + fs * 2; + int sbh = bh * subPixelSize + fs * 2; + + // round up to align with maximum step size + sbw = (sbw + (maxStepSize - 1)) & (~(maxStepSize - 1)); + sbh = (sbh + (maxStepSize - 1)) & (~(maxStepSize - 1)); + // extra padding as needed + if (maxStepSize > 1) { + sbw++; + sbh++; + } + // allocate bucket memory + ImageSample[] samples = new ImageSample[sbw * sbh]; + // allocate samples and compute jitter offsets + float invSubPixelSize = 1.0f / subPixelSize; + for (int y = 0, index = 0; y < sbh; y++) { + for (int x = 0; x < sbw; x++, index++) { + int sx = sx0 + x; + int sy = sy0 + y; + int j = sx & (sigmaLength - 1); + int k = sy & (sigmaLength - 1); + int i = (j << sigmaOrder) + QMC.sigma(k, sigmaOrder); + float dx = useJitter ? (float) QMC.halton(0, k) : 0.5f; + float dy = useJitter ? (float) QMC.halton(0, j) : 0.5f; + float rx = (sx + dx) * invSubPixelSize; + float ry = (sy + dy) * invSubPixelSize; + ry = imageHeight - ry; + samples[index] = new ImageSample(rx, ry, i); + } + } + for (int x = 0; x < sbw - 1; x += maxStepSize) + for (int y = 0; y < sbh - 1; y += maxStepSize) + refineSamples(samples, sbw, x, y, maxStepSize, thresh, istate); + if (dumpBuckets) { + UI.printInfo(Module.BCKT, "Dumping bucket [%d, %d] to file ...", bx, by); + GenericBitmap bitmap = new GenericBitmap(sbw, sbh); + for (int y = sbh - 1, index = 0; y >= 0; y--) + for (int x = 0; x < sbw; x++, index++) + bitmap.writePixel(x, y, samples[index].c, samples[index].alpha); + bitmap.save(String.format("bucket_%04d_%04d.png", bx, by)); + } + if (displayAA) { + // color coded image of what is visible + float invArea = invSubPixelSize * invSubPixelSize; + for (int y = 0, index = 0; y < bh; y++) { + for (int x = 0; x < bw; x++, index++) { + int sampled = 0; + for (int i = 0; i < subPixelSize; i++) { + for (int j = 0; j < subPixelSize; j++) { + int sx = x * subPixelSize + fs + i; + int sy = y * subPixelSize + fs + j; + int s = sx + sy * sbw; + sampled += samples[s].sampled() ? 1 : 0; + } + } + bucketRGB[index] = new Color(sampled * invArea); + bucketAlpha[index] = 1.0f; + } + } + } else { + // filter samples into pixels + float cy = imageHeight - (y0 + 0.5f); + for (int y = 0, index = 0; y < bh; y++, cy--) { + float cx = x0 + 0.5f; + for (int x = 0; x < bw; x++, index++, cx++) { + Color c = Color.black(); + float a = 0; + float weight = 0.0f; + for (int j = -fs, sy = y * subPixelSize; j <= fs; j++, sy++) { + for (int i = -fs, sx = x * subPixelSize, s = sx + sy * sbw; i <= fs; i++, sx++, s++) { + float dx = samples[s].rx - cx; + if (Math.abs(dx) > fhs) + continue; + float dy = samples[s].ry - cy; + if (Math.abs(dy) > fhs) + continue; + float f = filter.get(dx, dy); + c.madd(f, samples[s].c); + a += f * samples[s].alpha; + weight += f; + + } + } + float invWeight = 1.0f / weight; + c.mul(invWeight); + a *= invWeight; + bucketRGB[index] = c; + bucketAlpha[index] = a; + } + } + } + // update pixels + display.imageUpdate(x0, y0, bw, bh, bucketRGB, bucketAlpha); + } + + private void computeSubPixel(ImageSample sample, IntersectionState istate) { + float x = sample.rx; + float y = sample.ry; + double q0 = QMC.halton(1, sample.i); + double q1 = QMC.halton(2, sample.i); + double q2 = QMC.halton(3, sample.i); + if (superSampling > 1) { + // multiple sampling + sample.add(scene.getRadiance(istate, x, y, q1, q2, q0, sample.i, 4, null)); + for (int i = 1; i < superSampling; i++) { + double time = QMC.mod1(q0 + i * invSuperSampling); + double lensU = QMC.mod1(q1 + QMC.halton(0, i)); + double lensV = QMC.mod1(q2 + QMC.halton(1, i)); + sample.add(scene.getRadiance(istate, x, y, lensU, lensV, time, sample.i + i, 4, null)); + } + sample.scale((float) invSuperSampling); + } else { + // single sample + sample.set(scene.getRadiance(istate, x, y, q1, q2, q0, sample.i, 4, null)); + } + } + + private void refineSamples(ImageSample[] samples, int sbw, int x, int y, int stepSize, float thresh, IntersectionState istate) { + int dx = stepSize; + int dy = stepSize * sbw; + int i00 = x + y * sbw; + ImageSample s00 = samples[i00]; + ImageSample s01 = samples[i00 + dy]; + ImageSample s10 = samples[i00 + dx]; + ImageSample s11 = samples[i00 + dx + dy]; + if (!s00.sampled()) + computeSubPixel(s00, istate); + if (!s01.sampled()) + computeSubPixel(s01, istate); + if (!s10.sampled()) + computeSubPixel(s10, istate); + if (!s11.sampled()) + computeSubPixel(s11, istate); + if (stepSize > minStepSize) { + if (s00.isDifferent(s01, thresh) || s00.isDifferent(s10, thresh) || s00.isDifferent(s11, thresh) || s01.isDifferent(s11, thresh) || s10.isDifferent(s11, thresh) || s01.isDifferent(s10, thresh)) { + stepSize >>= 1; + thresh *= 2; + refineSamples(samples, sbw, x, y, stepSize, thresh, istate); + refineSamples(samples, sbw, x + stepSize, y, stepSize, thresh, istate); + refineSamples(samples, sbw, x, y + stepSize, stepSize, thresh, istate); + refineSamples(samples, sbw, x + stepSize, y + stepSize, stepSize, thresh, istate); + return; + } + } + + // interpolate remaining samples + float ds = 1.0f / stepSize; + for (int i = 0; i <= stepSize; i++) + for (int j = 0; j <= stepSize; j++) + if (!samples[x + i + (y + j) * sbw].processed()) + ImageSample.bilerp(samples[x + i + (y + j) * sbw], s00, s01, s10, s11, i * ds, j * ds); + } + + private static final class ImageSample { + float rx, ry; + int i, n; + Color c; + float alpha; + Instance instance; + Shader shader; + float nx, ny, nz; + + ImageSample(float rx, float ry, int i) { + this.rx = rx; + this.ry = ry; + this.i = i; + n = 0; + c = null; + alpha = 0; + instance = null; + shader = null; + nx = ny = nz = 1; + } + + final void set(ShadingState state) { + if (state == null) + c = Color.BLACK; + else { + c = state.getResult(); + shader = state.getShader(); + instance = state.getInstance(); + if (state.getNormal() != null) { + nx = state.getNormal().x; + ny = state.getNormal().y; + nz = state.getNormal().z; + } + alpha = state.getInstance() == null ? 0 : 1; + } + n = 1; + } + + final void add(ShadingState state) { + if (n == 0) + c = Color.black(); + if (state != null) { + c.add(state.getResult()); + alpha += state.getInstance() == null ? 0 : 1; + } + n++; + } + + final void scale(float s) { + c.mul(s); + alpha *= s; + } + + final boolean processed() { + return c != null; + } + + final boolean sampled() { + return n > 0; + } + + final boolean isDifferent(ImageSample sample, float thresh) { + if (instance != sample.instance) + return true; + if (shader != sample.shader) + return true; + if (Color.hasContrast(c, sample.c, thresh)) + return true; + if (Math.abs(alpha - sample.alpha) / (alpha + sample.alpha) > thresh) + return true; + // only compare normals if this pixel has not been averaged + float dot = (nx * sample.nx + ny * sample.ny + nz * sample.nz); + return dot < 0.9f; + } + + static final ImageSample bilerp(ImageSample result, ImageSample i00, ImageSample i01, ImageSample i10, ImageSample i11, float dx, float dy) { + float k00 = (1.0f - dx) * (1.0f - dy); + float k01 = (1.0f - dx) * dy; + float k10 = dx * (1.0f - dy); + float k11 = dx * dy; + Color c00 = i00.c; + Color c01 = i01.c; + Color c10 = i10.c; + Color c11 = i11.c; + Color c = Color.mul(k00, c00); + c.madd(k01, c01); + c.madd(k10, c10); + c.madd(k11, c11); + result.c = c; + result.alpha = k00 * i00.alpha + k01 * i01.alpha + k10 * i10.alpha + k11 * i11.alpha; + return result; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/renderer/MultipassRenderer.java b/src/main/java/org/sunflow/core/renderer/MultipassRenderer.java new file mode 100644 index 0000000..6331042 --- /dev/null +++ b/src/main/java/org/sunflow/core/renderer/MultipassRenderer.java @@ -0,0 +1,226 @@ +package org.sunflow.core.renderer; + +import org.sunflow.core.BucketOrder; +import org.sunflow.core.Display; +import org.sunflow.core.ImageSampler; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.Options; +import org.sunflow.core.Scene; +import org.sunflow.core.ShadingCache; +import org.sunflow.core.ShadingState; +import org.sunflow.core.bucket.BucketOrderFactory; +import org.sunflow.image.Color; +import org.sunflow.math.MathUtils; +import org.sunflow.math.QMC; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class MultipassRenderer implements ImageSampler { + private Scene scene; + private Display display; + // resolution + private int imageWidth; + private int imageHeight; + // bucketing + private String bucketOrderName; + private BucketOrder bucketOrder; + private int bucketSize; + private int bucketCounter; + private int[] bucketCoords; + + // anti-aliasing + private int numSamples; + private float invNumSamples; + private boolean shadingCache; + + public MultipassRenderer() { + bucketSize = 32; + bucketOrderName = "hilbert"; + numSamples = 16; + shadingCache = false; + } + + public boolean prepare(Options options, Scene scene, int w, int h) { + this.scene = scene; + imageWidth = w; + imageHeight = h; + + // fetch options + bucketSize = options.getInt("bucket.size", bucketSize); + bucketOrderName = options.getString("bucket.order", bucketOrderName); + numSamples = options.getInt("aa.samples", numSamples); + shadingCache = options.getBoolean("aa.cache", shadingCache); + + // limit bucket size and compute number of buckets in each direction + bucketSize = MathUtils.clamp(bucketSize, 16, 512); + int numBucketsX = (imageWidth + bucketSize - 1) / bucketSize; + int numBucketsY = (imageHeight + bucketSize - 1) / bucketSize; + bucketOrder = BucketOrderFactory.create(bucketOrderName); + bucketCoords = bucketOrder.getBucketSequence(numBucketsX, numBucketsY); + // validate AA options + numSamples = Math.max(1, numSamples); + invNumSamples = 1.0f / numSamples; + // prepare QMC sampling + UI.printInfo(Module.BCKT, "Multipass renderer settings:"); + UI.printInfo(Module.BCKT, " * Resolution: %dx%d", imageWidth, imageHeight); + UI.printInfo(Module.BCKT, " * Bucket size: %d", bucketSize); + UI.printInfo(Module.BCKT, " * Number of buckets: %dx%d", numBucketsX, numBucketsY); + UI.printInfo(Module.BCKT, " * Samples / pixel: %d", numSamples); + UI.printInfo(Module.BCKT, " * Shading cache: %s", shadingCache ? "enabled" : "disabled"); + return true; + } + + public void render(Display display) { + this.display = display; + display.imageBegin(imageWidth, imageHeight, bucketSize); + // set members variables + bucketCounter = 0; + // start task + Timer timer = new Timer(); + timer.start(); + UI.taskStart("Rendering", 0, bucketCoords.length); + BucketThread[] renderThreads = new BucketThread[scene.getThreads()]; + for (int i = 0; i < renderThreads.length; i++) { + renderThreads[i] = new BucketThread(i); + renderThreads[i].setPriority(scene.getThreadPriority()); + renderThreads[i].start(); + } + for (int i = 0; i < renderThreads.length; i++) { + try { + renderThreads[i].join(); + } catch (InterruptedException e) { + UI.printError(Module.BCKT, "Bucket processing thread %d of %d was interrupted", i + 1, renderThreads.length); + } finally { + renderThreads[i].updateStats(); + } + } + UI.taskStop(); + timer.end(); + UI.printInfo(Module.BCKT, "Render time: %s", timer.toString()); + display.imageEnd(); + } + + private class BucketThread extends Thread { + private final int threadID; + private final IntersectionState istate; + private final ShadingCache cache; + + BucketThread(int threadID) { + this.threadID = threadID; + istate = new IntersectionState(); + cache = shadingCache ? new ShadingCache() : null; + } + + @Override + public void run() { + while (true) { + int bx, by; + synchronized (MultipassRenderer.this) { + if (bucketCounter >= bucketCoords.length) + return; + UI.taskUpdate(bucketCounter); + bx = bucketCoords[bucketCounter + 0]; + by = bucketCoords[bucketCounter + 1]; + bucketCounter += 2; + } + renderBucket(display, bx, by, threadID, istate, cache); + } + } + + void updateStats() { + scene.accumulateStats(istate); + if (shadingCache) + scene.accumulateStats(cache); + } + } + + private void renderBucket(Display display, int bx, int by, int threadID, IntersectionState istate, ShadingCache cache) { + // pixel sized extents + int x0 = bx * bucketSize; + int y0 = by * bucketSize; + int bw = Math.min(bucketSize, imageWidth - x0); + int bh = Math.min(bucketSize, imageHeight - y0); + + // prepare bucket + display.imagePrepare(x0, y0, bw, bh, threadID); + + Color[] bucketRGB = new Color[bw * bh]; + float[] bucketAlpha = new float[bw * bh]; + + for (int y = 0, i = 0, cy = imageHeight - 1 - y0; y < bh; y++, cy--) { + for (int x = 0, cx = x0; x < bw; x++, i++, cx++) { + // sample pixel + Color c = Color.black(); + float a = 0; + int instance = ((cx & ((1 << QMC.MAX_SIGMA_ORDER) - 1)) << QMC.MAX_SIGMA_ORDER) + QMC.sigma(cy & ((1 << QMC.MAX_SIGMA_ORDER) - 1), QMC.MAX_SIGMA_ORDER); + double jitterX = QMC.halton(0, instance); + double jitterY = QMC.halton(1, instance); + double jitterT = QMC.halton(2, instance); + double jitterU = QMC.halton(3, instance); + double jitterV = QMC.halton(4, instance); + for (int s = 0; s < numSamples; s++) { + float rx = cx + 0.5f + (float) warpCubic(QMC.mod1(jitterX + s * invNumSamples)); + float ry = cy + 0.5f + (float) warpCubic(QMC.mod1(jitterY + QMC.halton(0, s))); + double time = QMC.mod1(jitterT + QMC.halton(1, s)); + double lensU = QMC.mod1(jitterU + QMC.halton(2, s)); + double lensV = QMC.mod1(jitterV + QMC.halton(3, s)); + ShadingState state = scene.getRadiance(istate, rx, ry, lensU, lensV, time, instance + s, 5, cache); + if (state != null) { + c.add(state.getResult()); + a++; + } + } + bucketRGB[i] = c.mul(invNumSamples); + bucketAlpha[i] = a * invNumSamples; + if (cache != null) + cache.reset(); + } + } + // update pixels + display.imageUpdate(x0, y0, bw, bh, bucketRGB, bucketAlpha); + } + + /** + * Tent filter warping function. + * + * @param x sample in the [0,1) range + * @return warped sample in the [-1,+1) range + */ + @SuppressWarnings("unused") + private static final float warpTent(float x) { + if (x < 0.5f) + return -1 + (float) Math.sqrt(2 * x); + else + return +1 - (float) Math.sqrt(2 - 2 * x); + } + + /** + * Cubic BSpline warping functions. Formulas from: "Generation of Stratified + * Samples for B-Spline Pixel Filtering" + * http://www.cs.utah.edu/~mstark/papers/ + * + * @param x samples in the [0,1) range + * @return warped sample in the [-2,+2) range + */ + private static final double warpCubic(double x) { + if (x < (1.0 / 24)) + return qpow(24 * x) - 2; + if (x < 0.5f) + return distb1((24.0 / 11.0) * (x - (1.0 / 24.0))) - 1; + if (x < (23.0f / 24)) + return 1 - distb1((24.0 / 11.0) * ((23.0 / 24.0) - x)); + return 2 - qpow(24 * (1 - x)); + } + + private static final double qpow(double x) { + return Math.sqrt(Math.sqrt(x)); + } + + private static final double distb1(double x) { + double u = x; + for (int i = 0; i < 5; i++) + u = (11 * x + u * u * (6 + u * (8 - 9 * u))) / (4 + 12 * u * (1 + u * (1 - u))); + return u; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/renderer/ProgressiveRenderer.java b/src/main/java/org/sunflow/core/renderer/ProgressiveRenderer.java new file mode 100644 index 0000000..cd7b525 --- /dev/null +++ b/src/main/java/org/sunflow/core/renderer/ProgressiveRenderer.java @@ -0,0 +1,158 @@ +package org.sunflow.core.renderer; + +import java.util.concurrent.PriorityBlockingQueue; + +import org.sunflow.core.Display; +import org.sunflow.core.ImageSampler; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.Options; +import org.sunflow.core.Scene; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.QMC; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class ProgressiveRenderer implements ImageSampler { + private Scene scene; + private int imageWidth, imageHeight; + private PriorityBlockingQueue smallBucketQueue; + private Display display; + private int counter, counterMax; + + public ProgressiveRenderer() { + imageWidth = 640; + imageHeight = 480; + smallBucketQueue = null; + } + + public boolean prepare(Options options, Scene scene, int w, int h) { + this.scene = scene; + imageWidth = w; + imageHeight = h; + // prepare table used by deterministic anti-aliasing + return true; + } + + public void render(Display display) { + this.display = display; + display.imageBegin(imageWidth, imageHeight, 0); + // create first bucket + SmallBucket b = new SmallBucket(); + b.x = b.y = 0; + int s = Math.max(imageWidth, imageHeight); + b.size = 1; + while (b.size < s) + b.size <<= 1; + smallBucketQueue = new PriorityBlockingQueue(); + smallBucketQueue.add(b); + UI.taskStart("Progressive Render", 0, imageWidth * imageHeight); + Timer t = new Timer(); + t.start(); + counter = 0; + counterMax = imageWidth * imageHeight; + + SmallBucketThread[] renderThreads = new SmallBucketThread[scene.getThreads()]; + for (int i = 0; i < renderThreads.length; i++) { + renderThreads[i] = new SmallBucketThread(); + renderThreads[i].start(); + } + for (int i = 0; i < renderThreads.length; i++) { + try { + renderThreads[i].join(); + } catch (InterruptedException e) { + UI.printError(Module.IPR, "Thread %d of %d was interrupted", i + 1, renderThreads.length); + } finally { + renderThreads[i].updateStats(); + } + } + UI.taskStop(); + t.end(); + UI.printInfo(Module.IPR, "Rendering time: %s", t.toString()); + display.imageEnd(); + } + + private class SmallBucketThread extends Thread { + private final IntersectionState istate = new IntersectionState(); + + @Override + public void run() { + while (true) { + int n = progressiveRenderNext(istate); + synchronized (ProgressiveRenderer.this) { + if (counter >= counterMax) + return; + counter += n; + UI.taskUpdate(counter); + } + if (UI.taskCanceled()) + return; + } + } + + void updateStats() { + scene.accumulateStats(istate); + } + } + + private int progressiveRenderNext(IntersectionState istate) { + final int TASK_SIZE = 16; + SmallBucket first = smallBucketQueue.poll(); + if (first == null) + return 0; + int ds = first.size / TASK_SIZE; + boolean useMask = !smallBucketQueue.isEmpty(); + int mask = 2 * first.size / TASK_SIZE - 1; + int pixels = 0; + for (int i = 0, y = first.y; i < TASK_SIZE && y < imageHeight; i++, y += ds) { + for (int j = 0, x = first.x; j < TASK_SIZE && x < imageWidth; j++, x += ds) { + // check to see if this is a pixel from a higher level tile + if (useMask && (x & mask) == 0 && (y & mask) == 0) + continue; + int instance = ((x & ((1 << QMC.MAX_SIGMA_ORDER) - 1)) << QMC.MAX_SIGMA_ORDER) + QMC.sigma(y & ((1 << QMC.MAX_SIGMA_ORDER) - 1), QMC.MAX_SIGMA_ORDER); + double time = QMC.halton(1, instance); + double lensU = QMC.halton(2, instance); + double lensV = QMC.halton(3, instance); + ShadingState state = scene.getRadiance(istate, x, imageHeight - 1 - y, lensU, lensV, time, instance, 4, null); + Color c = state != null ? state.getResult() : Color.BLACK; + pixels++; + // fill region + display.imageFill(x, y, Math.min(ds, imageWidth - x), Math.min(ds, imageHeight - y), c, state == null ? 0 : 1); + } + } + if (first.size >= 2 * TASK_SIZE) { + // generate child buckets + int size = first.size >>> 1; + for (int i = 0; i < 2; i++) { + if (first.y + i * size < imageHeight) { + for (int j = 0; j < 2; j++) { + if (first.x + j * size < imageWidth) { + SmallBucket b = new SmallBucket(); + b.x = first.x + j * size; + b.y = first.y + i * size; + b.size = size; + b.constrast = 1.0f / size; + smallBucketQueue.put(b); + } + } + } + } + } + return pixels; + } + + // progressive rendering + private static class SmallBucket implements Comparable { + int x, y, size; + float constrast; + + public int compareTo(SmallBucket o) { + if (constrast < o.constrast) + return -1; + if (constrast == o.constrast) + return 0; + return 1; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/renderer/SimpleRenderer.java b/src/main/java/org/sunflow/core/renderer/SimpleRenderer.java new file mode 100644 index 0000000..fd7bf5e --- /dev/null +++ b/src/main/java/org/sunflow/core/renderer/SimpleRenderer.java @@ -0,0 +1,101 @@ +package org.sunflow.core.renderer; + +import org.sunflow.core.Display; +import org.sunflow.core.ImageSampler; +import org.sunflow.core.IntersectionState; +import org.sunflow.core.Options; +import org.sunflow.core.Scene; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class SimpleRenderer implements ImageSampler { + private Scene scene; + private Display display; + private int imageWidth, imageHeight; + private int numBucketsX, numBucketsY; + private int bucketCounter, numBuckets; + + public boolean prepare(Options options, Scene scene, int w, int h) { + this.scene = scene; + imageWidth = w; + imageHeight = h; + numBucketsX = (imageWidth + 31) >>> 5; + numBucketsY = (imageHeight + 31) >>> 5; + numBuckets = numBucketsX * numBucketsY; + return true; + } + + public void render(Display display) { + this.display = display; + display.imageBegin(imageWidth, imageHeight, 32); + // set members variables + bucketCounter = 0; + // start task + Timer timer = new Timer(); + timer.start(); + BucketThread[] renderThreads = new BucketThread[scene.getThreads()]; + for (int i = 0; i < renderThreads.length; i++) { + renderThreads[i] = new BucketThread(); + renderThreads[i].start(); + } + for (int i = 0; i < renderThreads.length; i++) { + try { + renderThreads[i].join(); + } catch (InterruptedException e) { + UI.printError(Module.BCKT, "Bucket processing thread %d of %d was interrupted", i + 1, renderThreads.length); + } finally { + renderThreads[i].updateStats(); + } + } + timer.end(); + UI.printInfo(Module.BCKT, "Render time: %s", timer.toString()); + display.imageEnd(); + } + + private class BucketThread extends Thread { + private final IntersectionState istate = new IntersectionState(); + + @Override + public void run() { + while (true) { + int bx, by; + synchronized (SimpleRenderer.this) { + if (bucketCounter >= numBuckets) + return; + by = bucketCounter / numBucketsX; + bx = bucketCounter % numBucketsX; + bucketCounter++; + } + renderBucket(bx, by, istate); + } + } + + void updateStats() { + scene.accumulateStats(istate); + } + } + + public void renderBucket(int bx, int by, IntersectionState istate) { + // pixel sized extents + int x0 = bx * 32; + int y0 = by * 32; + int bw = Math.min(32, imageWidth - x0); + int bh = Math.min(32, imageHeight - y0); + + Color[] bucketRGB = new Color[bw * bh]; + float[] bucketAlpha = new float[bw * bh]; + + for (int y = 0, i = 0; y < bh; y++) { + for (int x = 0; x < bw; x++, i++) { + ShadingState state = scene.getRadiance(istate, x0 + x, imageHeight - 1 - (y0 + y), 0.0, 0.0, 0.0, 0, 0, null); + bucketRGB[i] = (state != null) ? state.getResult() : Color.BLACK; + bucketAlpha[i] = (state != null) ? 1 : 0; + } + } + // update pixels + display.imageUpdate(x0, y0, bw, bh, bucketRGB, bucketAlpha); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/AmbientOcclusionShader.java b/src/main/java/org/sunflow/core/shader/AmbientOcclusionShader.java new file mode 100644 index 0000000..5f3b087 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/AmbientOcclusionShader.java @@ -0,0 +1,48 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; + +public class AmbientOcclusionShader implements Shader { + private Color bright; + private Color dark; + private int samples; + private float maxDist; + + public AmbientOcclusionShader() { + bright = Color.WHITE; + dark = Color.BLACK; + samples = 32; + maxDist = Float.POSITIVE_INFINITY; + } + + public AmbientOcclusionShader(Color c, float d) { + this(); + bright = c; + maxDist = d; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + bright = pl.getColor("bright", bright); + dark = pl.getColor("dark", dark); + samples = pl.getInt("samples", samples); + maxDist = pl.getFloat("maxdist", maxDist); + if (maxDist <= 0) + maxDist = Float.POSITIVE_INFINITY; + return true; + } + + public Color getBrightColor(ShadingState state) { + return bright; + } + + public Color getRadiance(ShadingState state) { + return state.occlusion(samples, maxDist, getBrightColor(state), dark); + } + + public void scatterPhoton(ShadingState state, Color power) { + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/AnisotropicWardShader.java b/src/main/java/org/sunflow/core/shader/AnisotropicWardShader.java new file mode 100644 index 0000000..72aed8e --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/AnisotropicWardShader.java @@ -0,0 +1,211 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.LightSample; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Vector3; + +public class AnisotropicWardShader implements Shader { + private Color rhoD; // diffuse reflectance + private Color rhoS; // specular reflectance + private float alphaX; + private float alphaY; + private int numRays; + + public AnisotropicWardShader() { + rhoD = Color.GRAY; + rhoS = Color.GRAY; + alphaX = 1; + alphaY = 1; + numRays = 4; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + rhoD = pl.getColor("diffuse", rhoD); + rhoS = pl.getColor("specular", rhoS); + alphaX = pl.getFloat("roughnessX", alphaX); + alphaY = pl.getFloat("roughnessY", alphaY); + numRays = pl.getInt("samples", numRays); + return true; + } + + protected Color getDiffuse(ShadingState state) { + return rhoD; + } + + private float brdf(Vector3 i, Vector3 o, OrthoNormalBasis basis) { + float fr = 4 * (float) Math.PI * alphaX * alphaY; + float p = basis.untransformZ(i) * basis.untransformZ(o); + if (p > 0) + fr *= (float) Math.sqrt(p); + else + fr = 0; + Vector3 h = Vector3.add(i, o, new Vector3()); + basis.untransform(h); + float hx = h.x / alphaX; + hx *= hx; + float hy = h.y / alphaY; + hy *= hy; + float hn = h.z * h.z; + if (fr > 0) + fr = (float) Math.exp(-(hx + hy) / hn) / fr; + return fr; + } + + public Color getRadiance(ShadingState state) { + // make sure we are on the right side of the material + state.faceforward(); + OrthoNormalBasis onb = state.getBasis(); + // direct lighting and caustics + state.initLightSamples(); + state.initCausticSamples(); + Color lr = Color.black(); + // compute specular contribution + if (state.includeSpecular()) { + Vector3 in = state.getRay().getDirection().negate(new Vector3()); + for (LightSample sample : state) { + float cosNL = sample.dot(state.getNormal()); + float fr = brdf(in, sample.getShadowRay().getDirection(), onb); + lr.madd(cosNL * fr, sample.getSpecularRadiance()); + } + + // indirect lighting - specular + if (numRays > 0) { + int n = state.getDepth() == 0 ? numRays : 1; + for (int i = 0; i < n; i++) { + // specular indirect lighting + double r1 = state.getRandom(i, 0, n); + double r2 = state.getRandom(i, 1, n); + + float alphaRatio = alphaY / alphaX; + float phi = 0; + if (r1 < 0.25) { + double val = 4 * r1; + phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); + } else if (r1 < 0.5) { + double val = 1 - 4 * (0.5 - r1); + phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); + phi = (float) Math.PI - phi; + } else if (r1 < 0.75) { + double val = 4 * (r1 - 0.5); + phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); + phi += Math.PI; + } else { + double val = 1 - 4 * (1 - r1); + phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); + phi = 2 * (float) Math.PI - phi; + } + + float cosPhi = (float) Math.cos(phi); + float sinPhi = (float) Math.sin(phi); + + float denom = (cosPhi * cosPhi) / (alphaX * alphaX) + (sinPhi * sinPhi) / (alphaY * alphaY); + float theta = (float) Math.atan(Math.sqrt(-Math.log(1 - r2) / denom)); + + float sinTheta = (float) Math.sin(theta); + float cosTheta = (float) Math.cos(theta); + + Vector3 h = new Vector3(); + h.x = sinTheta * cosPhi; + h.y = sinTheta * sinPhi; + h.z = cosTheta; + onb.transform(h); + + Vector3 o = new Vector3(); + float ih = Vector3.dot(h, in); + o.x = 2 * ih * h.x - in.x; + o.y = 2 * ih * h.y - in.y; + o.z = 2 * ih * h.z - in.z; + + float no = onb.untransformZ(o); + float ni = onb.untransformZ(in); + float w = ih * cosTheta * cosTheta * cosTheta * (float) Math.sqrt(Math.abs(no / ni)); + + Ray r = new Ray(state.getPoint(), o); + lr.madd(w / n, state.traceGlossy(r, i)); + } + } + lr.mul(rhoS); + } + // add diffuse contribution + lr.add(state.diffuse(getDiffuse(state))); + return lr; + } + + public void scatterPhoton(ShadingState state, Color power) { + // make sure we are on the right side of the material + state.faceforward(); + Color d = getDiffuse(state); + state.storePhoton(state.getRay().getDirection(), power, d); + float avgD = d.getAverage(); + float avgS = rhoS.getAverage(); + double rnd = state.getRandom(0, 0, 1); + if (rnd < avgD) { + // photon is scattered diffusely + power.mul(d).mul(1.0f / avgD); + OrthoNormalBasis onb = state.getBasis(); + double u = 2 * Math.PI * rnd / avgD; + double v = state.getRandom(0, 1, 1); + float s = (float) Math.sqrt(v); + float s1 = (float) Math.sqrt(1.0f - v); + Vector3 w = new Vector3((float) Math.cos(u) * s, (float) Math.sin(u) * s, s1); + w = onb.transform(w, new Vector3()); + state.traceDiffusePhoton(new Ray(state.getPoint(), w), power); + } else if (rnd < avgD + avgS) { + // photon is scattered specularly + power.mul(rhoS).mul(1 / avgS); + OrthoNormalBasis basis = state.getBasis(); + Vector3 in = state.getRay().getDirection().negate(new Vector3()); + double r1 = rnd / avgS; + double r2 = state.getRandom(0, 1, 1); + + float alphaRatio = alphaY / alphaX; + float phi = 0; + if (r1 < 0.25) { + double val = 4 * r1; + phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); + } else if (r1 < 0.5) { + double val = 1 - 4 * (0.5 - r1); + phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); + phi = (float) Math.PI - phi; + } else if (r1 < 0.75) { + double val = 4 * (r1 - 0.5); + phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); + phi += Math.PI; + } else { + double val = 1 - 4 * (1 - r1); + phi = (float) Math.atan(alphaRatio * Math.tan(Math.PI / 2 * val)); + phi = 2 * (float) Math.PI - phi; + } + + float cosPhi = (float) Math.cos(phi); + float sinPhi = (float) Math.sin(phi); + + float denom = (cosPhi * cosPhi) / (alphaX * alphaX) + (sinPhi * sinPhi) / (alphaY * alphaY); + float theta = (float) Math.atan(Math.sqrt(-Math.log(1 - r2) / denom)); + + float sinTheta = (float) Math.sin(theta); + float cosTheta = (float) Math.cos(theta); + + Vector3 h = new Vector3(); + h.x = sinTheta * cosPhi; + h.y = sinTheta * sinPhi; + h.z = cosTheta; + basis.transform(h); + + Vector3 o = new Vector3(); + float ih = Vector3.dot(h, in); + o.x = 2 * ih * h.x - in.x; + o.y = 2 * ih * h.y - in.y; + o.z = 2 * ih * h.z - in.z; + + Ray r = new Ray(state.getPoint(), o); + state.traceReflectionPhoton(r, power); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/ConstantShader.java b/src/main/java/org/sunflow/core/shader/ConstantShader.java new file mode 100644 index 0000000..e012e0d --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/ConstantShader.java @@ -0,0 +1,27 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; + +public class ConstantShader implements Shader { + private Color c; + + public ConstantShader() { + c = Color.WHITE; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + c = pl.getColor("color", c); + return true; + } + + public Color getRadiance(ShadingState state) { + return c; + } + + public void scatterPhoton(ShadingState state, Color power) { + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/DiffuseShader.java b/src/main/java/org/sunflow/core/shader/DiffuseShader.java new file mode 100644 index 0000000..afb4765 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/DiffuseShader.java @@ -0,0 +1,61 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Vector3; + +public class DiffuseShader implements Shader { + private Color diff; + + public DiffuseShader() { + diff = Color.WHITE; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + diff = pl.getColor("diffuse", diff); + return true; + } + + public Color getDiffuse(ShadingState state) { + return diff; + } + + public Color getRadiance(ShadingState state) { + // make sure we are on the right side of the material + state.faceforward(); + // setup lighting + state.initLightSamples(); + state.initCausticSamples(); + return state.diffuse(getDiffuse(state)); + } + + public void scatterPhoton(ShadingState state, Color power) { + Color diffuse; + // make sure we are on the right side of the material + if (Vector3.dot(state.getNormal(), state.getRay().getDirection()) > 0.0) { + state.getNormal().negate(); + state.getGeoNormal().negate(); + } + diffuse = getDiffuse(state); + state.storePhoton(state.getRay().getDirection(), power, diffuse); + float avg = diffuse.getAverage(); + double rnd = state.getRandom(0, 0, 1); + if (rnd < avg) { + // photon is scattered + power.mul(diffuse).mul(1.0f / avg); + OrthoNormalBasis onb = state.getBasis(); + double u = 2 * Math.PI * rnd / avg; + double v = state.getRandom(0, 1, 1); + float s = (float) Math.sqrt(v); + float s1 = (float) Math.sqrt(1.0 - v); + Vector3 w = new Vector3((float) Math.cos(u) * s, (float) Math.sin(u) * s, s1); + w = onb.transform(w, new Vector3()); + state.traceDiffusePhoton(new Ray(state.getPoint(), w), power); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/GlassShader.java b/src/main/java/org/sunflow/core/shader/GlassShader.java new file mode 100644 index 0000000..fce5583 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/GlassShader.java @@ -0,0 +1,139 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.Vector3; + +public class GlassShader implements Shader { + private float eta; // refraction index ratio + private float f0; // fresnel normal incidence + private Color color; + private float absorptionDistance; + private Color absorptionColor; + + public GlassShader() { + eta = 1.3f; + color = Color.WHITE; + absorptionDistance = 0; // disabled by default + absorptionColor = Color.GRAY; // 50% absorbtion + } + + public boolean update(ParameterList pl, SunflowAPI api) { + color = pl.getColor("color", color); + eta = pl.getFloat("eta", eta); + f0 = (1 - eta) / (1 + eta); + f0 = f0 * f0; + absorptionDistance = pl.getFloat("absorption.distance", absorptionDistance); + absorptionColor = pl.getColor("absorption.color", absorptionColor); + return true; + } + + public Color getRadiance(ShadingState state) { + if (!state.includeSpecular()) + return Color.BLACK; + Vector3 reflDir = new Vector3(); + Vector3 refrDir = new Vector3(); + state.faceforward(); + float cos = state.getCosND(); + boolean inside = state.isBehind(); + float neta = inside ? eta : 1.0f / eta; + + float dn = 2 * cos; + reflDir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; + reflDir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; + reflDir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; + + // refracted ray + float arg = 1 - (neta * neta * (1 - (cos * cos))); + boolean tir = arg < 0; + if (tir) + refrDir.x = refrDir.y = refrDir.z = 0; + else { + float nK = (neta * cos) - (float) Math.sqrt(arg); + refrDir.x = (neta * state.getRay().dx) + (nK * state.getNormal().x); + refrDir.y = (neta * state.getRay().dy) + (nK * state.getNormal().y); + refrDir.z = (neta * state.getRay().dz) + (nK * state.getNormal().z); + } + + // compute Fresnel terms + float cosTheta1 = Vector3.dot(state.getNormal(), reflDir); + float cosTheta2 = -Vector3.dot(state.getNormal(), refrDir); + + float pPara = (cosTheta1 - eta * cosTheta2) / (cosTheta1 + eta * cosTheta2); + float pPerp = (eta * cosTheta1 - cosTheta2) / (eta * cosTheta1 + cosTheta2); + float kr = 0.5f * (pPara * pPara + pPerp * pPerp); + float kt = 1 - kr; + + Color absorbtion = null; + if (inside && absorptionDistance > 0) { + // this ray is inside the object and leaving it + // compute attenuation that occured along the ray + absorbtion = Color.mul(-state.getRay().getMax() / absorptionDistance, absorptionColor.copy().opposite()).exp(); + if (absorbtion.isBlack()) + return Color.BLACK; // nothing goes through + } + // refracted ray + Color ret = Color.black(); + if (!tir) { + ret.madd(kt, state.traceRefraction(new Ray(state.getPoint(), refrDir), 0)).mul(color); + } + if (!inside || tir) + ret.add(Color.mul(kr, state.traceReflection(new Ray(state.getPoint(), reflDir), 0)).mul(color)); + return absorbtion != null ? ret.mul(absorbtion) : ret; + } + + public void scatterPhoton(ShadingState state, Color power) { + Color refr = Color.mul(1 - f0, color); + Color refl = Color.mul(f0, color); + float avgR = refl.getAverage(); + float avgT = refr.getAverage(); + double rnd = state.getRandom(0, 0, 1); + if (rnd < avgR) { + state.faceforward(); + // don't reflect internally + if (state.isBehind()) + return; + // photon is reflected + float cos = state.getCosND(); + power.mul(refl).mul(1.0f / avgR); + float dn = 2 * cos; + Vector3 dir = new Vector3(); + dir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; + dir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; + dir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; + state.traceReflectionPhoton(new Ray(state.getPoint(), dir), power); + } else if (rnd < avgR + avgT) { + state.faceforward(); + // photon is refracted + float cos = state.getCosND(); + float neta = state.isBehind() ? eta : 1.0f / eta; + power.mul(refr).mul(1.0f / avgT); + float wK = -neta; + float arg = 1 - (neta * neta * (1 - (cos * cos))); + Vector3 dir = new Vector3(); + if (state.isBehind() && absorptionDistance > 0) { + // this ray is inside the object and leaving it + // compute attenuation that occured along the ray + power.mul(Color.mul(-state.getRay().getMax() / absorptionDistance, absorptionColor.copy().opposite()).exp()); + } + if (arg < 0) { + // TIR + float dn = 2 * cos; + dir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; + dir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; + dir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; + state.traceReflectionPhoton(new Ray(state.getPoint(), dir), power); + } else { + float nK = (neta * cos) - (float) Math.sqrt(arg); + dir.x = (-wK * state.getRay().dx) + (nK * state.getNormal().x); + dir.y = (-wK * state.getRay().dy) + (nK * state.getNormal().y); + dir.z = (-wK * state.getRay().dz) + (nK * state.getNormal().z); + state.traceRefractionPhoton(new Ray(state.getPoint(), dir), power); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/IDShader.java b/src/main/java/org/sunflow/core/shader/IDShader.java new file mode 100644 index 0000000..cb65ac6 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/IDShader.java @@ -0,0 +1,23 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.Vector3; + +public class IDShader implements Shader { + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public Color getRadiance(ShadingState state) { + Vector3 n = state.getNormal(); + float f = n == null ? 1.0f : Math.abs(state.getRay().dot(n)); + return new Color(state.getInstance().hashCode()).mul(f); + } + + public void scatterPhoton(ShadingState state, Color power) { + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/MirrorShader.java b/src/main/java/org/sunflow/core/shader/MirrorShader.java new file mode 100644 index 0000000..158e383 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/MirrorShader.java @@ -0,0 +1,62 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.Vector3; + +public class MirrorShader implements Shader { + private Color color; + + public MirrorShader() { + color = Color.WHITE; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + color = pl.getColor("color", color); + return true; + } + + public Color getRadiance(ShadingState state) { + if (!state.includeSpecular()) + return Color.BLACK; + state.faceforward(); + float cos = state.getCosND(); + float dn = 2 * cos; + Vector3 refDir = new Vector3(); + refDir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; + refDir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; + refDir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; + Ray refRay = new Ray(state.getPoint(), refDir); + + // compute Fresnel term + cos = 1 - cos; + float cos2 = cos * cos; + float cos5 = cos2 * cos2 * cos; + Color ret = Color.white(); + ret.sub(color); + ret.mul(cos5); + ret.add(color); + return ret.mul(state.traceReflection(refRay, 0)); + } + + public void scatterPhoton(ShadingState state, Color power) { + float avg = color.getAverage(); + double rnd = state.getRandom(0, 0, 1); + if (rnd >= avg) + return; + state.faceforward(); + float cos = state.getCosND(); + power.mul(color).mul(1.0f / avg); + // photon is reflected + float dn = 2 * cos; + Vector3 dir = new Vector3(); + dir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; + dir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; + dir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; + state.traceReflectionPhoton(new Ray(state.getPoint(), dir), power); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/NormalShader.java b/src/main/java/org/sunflow/core/shader/NormalShader.java new file mode 100644 index 0000000..f07b5be --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/NormalShader.java @@ -0,0 +1,27 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.Vector3; + +public class NormalShader implements Shader { + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public Color getRadiance(ShadingState state) { + Vector3 n = state.getNormal(); + if (n == null) + return Color.BLACK; + float r = (n.x + 1) * 0.5f; + float g = (n.y + 1) * 0.5f; + float b = (n.z + 1) * 0.5f; + return new Color(r, g, b); + } + + public void scatterPhoton(ShadingState state, Color power) { + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/PhongShader.java b/src/main/java/org/sunflow/core/shader/PhongShader.java new file mode 100644 index 0000000..accae35 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/PhongShader.java @@ -0,0 +1,85 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Vector3; + +public class PhongShader implements Shader { + private Color diff; + private Color spec; + private float power; + private int numRays; + + public PhongShader() { + diff = Color.GRAY; + spec = Color.GRAY; + power = 20; + numRays = 4; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + diff = pl.getColor("diffuse", diff); + spec = pl.getColor("specular", spec); + power = pl.getFloat("power", power); + numRays = pl.getInt("samples", numRays); + return true; + } + + protected Color getDiffuse(ShadingState state) { + return diff; + } + + public Color getRadiance(ShadingState state) { + // make sure we are on the right side of the material + state.faceforward(); + // setup lighting + state.initLightSamples(); + state.initCausticSamples(); + // execute shader + return state.diffuse(getDiffuse(state)).add(state.specularPhong(spec, power, numRays)); + } + + public void scatterPhoton(ShadingState state, Color power) { + // make sure we are on the right side of the material + state.faceforward(); + Color d = getDiffuse(state); + state.storePhoton(state.getRay().getDirection(), power, d); + float avgD = d.getAverage(); + float avgS = spec.getAverage(); + double rnd = state.getRandom(0, 0, 1); + if (rnd < avgD) { + // photon is scattered diffusely + power.mul(d).mul(1.0f / avgD); + OrthoNormalBasis onb = state.getBasis(); + double u = 2 * Math.PI * rnd / avgD; + double v = state.getRandom(0, 1, 1); + float s = (float) Math.sqrt(v); + float s1 = (float) Math.sqrt(1.0f - v); + Vector3 w = new Vector3((float) Math.cos(u) * s, (float) Math.sin(u) * s, s1); + w = onb.transform(w, new Vector3()); + state.traceDiffusePhoton(new Ray(state.getPoint(), w), power); + } else if (rnd < avgD + avgS) { + // photon is scattered specularly + float dn = 2.0f * state.getCosND(); + // reflected direction + Vector3 refDir = new Vector3(); + refDir.x = (dn * state.getNormal().x) + state.getRay().dx; + refDir.y = (dn * state.getNormal().y) + state.getRay().dy; + refDir.z = (dn * state.getNormal().z) + state.getRay().dz; + power.mul(spec).mul(1.0f / avgS); + OrthoNormalBasis onb = state.getBasis(); + double u = 2 * Math.PI * (rnd - avgD) / avgS; + double v = state.getRandom(0, 1, 1); + float s = (float) Math.pow(v, 1 / (this.power + 1)); + float s1 = (float) Math.sqrt(1 - s * s); + Vector3 w = new Vector3((float) Math.cos(u) * s1, (float) Math.sin(u) * s1, s); + w = onb.transform(w, new Vector3()); + state.traceReflectionPhoton(new Ray(state.getPoint(), w), power); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/PrimIDShader.java b/src/main/java/org/sunflow/core/shader/PrimIDShader.java new file mode 100644 index 0000000..12191ea --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/PrimIDShader.java @@ -0,0 +1,26 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.Vector3; + +public class PrimIDShader implements Shader { + private static final Color[] BORDERS = { Color.RED, Color.GREEN, + Color.BLUE, Color.YELLOW, Color.CYAN, Color.MAGENTA }; + + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public Color getRadiance(ShadingState state) { + Vector3 n = state.getNormal(); + float f = n == null ? 1.0f : Math.abs(state.getRay().dot(n)); + return BORDERS[state.getPrimitiveID() % BORDERS.length].copy().mul(f); + } + + public void scatterPhoton(ShadingState state, Color power) { + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/QuickGrayShader.java b/src/main/java/org/sunflow/core/shader/QuickGrayShader.java new file mode 100644 index 0000000..26911f1 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/QuickGrayShader.java @@ -0,0 +1,59 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Vector3; + +public class QuickGrayShader implements Shader { + public QuickGrayShader() { + } + + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public Color getRadiance(ShadingState state) { + if (state.getNormal() == null) { + // if this shader has been applied to an infinite instance because + // of shader overrides + // run the default shader, otherwise, just shade black + return state.getShader() != this ? state.getShader().getRadiance(state) : Color.BLACK; + } + // make sure we are on the right side of the material + state.faceforward(); + // setup lighting + state.initLightSamples(); + state.initCausticSamples(); + return state.diffuse(Color.GRAY); + } + + public void scatterPhoton(ShadingState state, Color power) { + Color diffuse; + // make sure we are on the right side of the material + if (Vector3.dot(state.getNormal(), state.getRay().getDirection()) > 0.0) { + state.getNormal().negate(); + state.getGeoNormal().negate(); + } + diffuse = Color.GRAY; + state.storePhoton(state.getRay().getDirection(), power, diffuse); + float avg = diffuse.getAverage(); + double rnd = state.getRandom(0, 0, 1); + if (rnd < avg) { + // photon is scattered + power.mul(diffuse).mul(1.0f / avg); + OrthoNormalBasis onb = state.getBasis(); + double u = 2 * Math.PI * rnd / avg; + double v = state.getRandom(0, 1, 1); + float s = (float) Math.sqrt(v); + float s1 = (float) Math.sqrt(1.0 - v); + Vector3 w = new Vector3((float) Math.cos(u) * s, (float) Math.sin(u) * s, s1); + w = onb.transform(w, new Vector3()); + state.traceDiffusePhoton(new Ray(state.getPoint(), w), power); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/ShinyDiffuseShader.java b/src/main/java/org/sunflow/core/shader/ShinyDiffuseShader.java new file mode 100644 index 0000000..904b4dc --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/ShinyDiffuseShader.java @@ -0,0 +1,93 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Vector3; + +public class ShinyDiffuseShader implements Shader { + private Color diff; + private float refl; + + public ShinyDiffuseShader() { + diff = Color.GRAY; + refl = 0.5f; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + diff = pl.getColor("diffuse", diff); + refl = pl.getFloat("shiny", refl); + return true; + } + + public Color getDiffuse(ShadingState state) { + return diff; + } + + public Color getRadiance(ShadingState state) { + // make sure we are on the right side of the material + state.faceforward(); + // direct lighting + state.initLightSamples(); + state.initCausticSamples(); + Color d = getDiffuse(state); + Color lr = state.diffuse(d); + if (!state.includeSpecular()) + return lr; + float cos = state.getCosND(); + float dn = 2 * cos; + Vector3 refDir = new Vector3(); + refDir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; + refDir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; + refDir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; + Ray refRay = new Ray(state.getPoint(), refDir); + // compute Fresnel term + cos = 1 - cos; + float cos2 = cos * cos; + float cos5 = cos2 * cos2 * cos; + + Color ret = Color.white(); + Color r = d.copy().mul(refl); + ret.sub(r); + ret.mul(cos5); + ret.add(r); + return lr.add(ret.mul(state.traceReflection(refRay, 0))); + } + + public void scatterPhoton(ShadingState state, Color power) { + Color diffuse; + // make sure we are on the right side of the material + state.faceforward(); + diffuse = getDiffuse(state); + state.storePhoton(state.getRay().getDirection(), power, diffuse); + float d = diffuse.getAverage(); + float r = d * refl; + double rnd = state.getRandom(0, 0, 1); + if (rnd < d) { + // photon is scattered + power.mul(diffuse).mul(1.0f / d); + OrthoNormalBasis onb = state.getBasis(); + double u = 2 * Math.PI * rnd / d; + double v = state.getRandom(0, 1, 1); + float s = (float) Math.sqrt(v); + float s1 = (float) Math.sqrt(1.0 - v); + Vector3 w = new Vector3((float) Math.cos(u) * s, (float) Math.sin(u) * s, s1); + w = onb.transform(w, new Vector3()); + state.traceDiffusePhoton(new Ray(state.getPoint(), w), power); + } else if (rnd < d + r) { + float cos = -Vector3.dot(state.getNormal(), state.getRay().getDirection()); + power.mul(diffuse).mul(1.0f / d); + // photon is reflected + float dn = 2 * cos; + Vector3 dir = new Vector3(); + dir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; + dir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; + dir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; + state.traceReflectionPhoton(new Ray(state.getPoint(), dir), power); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/SimpleShader.java b/src/main/java/org/sunflow/core/shader/SimpleShader.java new file mode 100644 index 0000000..9296b02 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/SimpleShader.java @@ -0,0 +1,20 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; + +public class SimpleShader implements Shader { + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public Color getRadiance(ShadingState state) { + return new Color(Math.abs(state.getRay().dot(state.getNormal()))); + } + + public void scatterPhoton(ShadingState state, Color power) { + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/TexturedAmbientOcclusionShader.java b/src/main/java/org/sunflow/core/shader/TexturedAmbientOcclusionShader.java new file mode 100644 index 0000000..adb8059 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/TexturedAmbientOcclusionShader.java @@ -0,0 +1,29 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.ShadingState; +import org.sunflow.core.Texture; +import org.sunflow.core.TextureCache; +import org.sunflow.image.Color; + +public class TexturedAmbientOcclusionShader extends AmbientOcclusionShader { + private Texture tex; + + public TexturedAmbientOcclusionShader() { + tex = null; + } + + @Override + public boolean update(ParameterList pl, SunflowAPI api) { + String filename = pl.getString("texture", null); + if (filename != null) + tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false); + return tex != null && super.update(pl, api); + } + + @Override + public Color getBrightColor(ShadingState state) { + return tex.getPixel(state.getUV().x, state.getUV().y); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/TexturedDiffuseShader.java b/src/main/java/org/sunflow/core/shader/TexturedDiffuseShader.java new file mode 100644 index 0000000..d29f0c5 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/TexturedDiffuseShader.java @@ -0,0 +1,29 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.ShadingState; +import org.sunflow.core.Texture; +import org.sunflow.core.TextureCache; +import org.sunflow.image.Color; + +public class TexturedDiffuseShader extends DiffuseShader { + private Texture tex; + + public TexturedDiffuseShader() { + tex = null; + } + + @Override + public boolean update(ParameterList pl, SunflowAPI api) { + String filename = pl.getString("texture", null); + if (filename != null) + tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false); + return tex != null && super.update(pl, api); + } + + @Override + public Color getDiffuse(ShadingState state) { + return tex.getPixel(state.getUV().x, state.getUV().y); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/TexturedPhongShader.java b/src/main/java/org/sunflow/core/shader/TexturedPhongShader.java new file mode 100644 index 0000000..20aeae1 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/TexturedPhongShader.java @@ -0,0 +1,29 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.ShadingState; +import org.sunflow.core.Texture; +import org.sunflow.core.TextureCache; +import org.sunflow.image.Color; + +public class TexturedPhongShader extends PhongShader { + private Texture tex; + + public TexturedPhongShader() { + tex = null; + } + + @Override + public boolean update(ParameterList pl, SunflowAPI api) { + String filename = pl.getString("texture", null); + if (filename != null) + tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false); + return tex != null && super.update(pl, api); + } + + @Override + public Color getDiffuse(ShadingState state) { + return tex.getPixel(state.getUV().x, state.getUV().y); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/TexturedShinyDiffuseShader.java b/src/main/java/org/sunflow/core/shader/TexturedShinyDiffuseShader.java new file mode 100644 index 0000000..19328a7 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/TexturedShinyDiffuseShader.java @@ -0,0 +1,29 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.ShadingState; +import org.sunflow.core.Texture; +import org.sunflow.core.TextureCache; +import org.sunflow.image.Color; + +public class TexturedShinyDiffuseShader extends ShinyDiffuseShader { + private Texture tex; + + public TexturedShinyDiffuseShader() { + tex = null; + } + + @Override + public boolean update(ParameterList pl, SunflowAPI api) { + String filename = pl.getString("texture", null); + if (filename != null) + tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false); + return tex != null && super.update(pl, api); + } + + @Override + public Color getDiffuse(ShadingState state) { + return tex.getPixel(state.getUV().x, state.getUV().y); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/TexturedWardShader.java b/src/main/java/org/sunflow/core/shader/TexturedWardShader.java new file mode 100644 index 0000000..188390a --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/TexturedWardShader.java @@ -0,0 +1,29 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.ShadingState; +import org.sunflow.core.Texture; +import org.sunflow.core.TextureCache; +import org.sunflow.image.Color; + +public class TexturedWardShader extends AnisotropicWardShader { + private Texture tex; + + public TexturedWardShader() { + tex = null; + } + + @Override + public boolean update(ParameterList pl, SunflowAPI api) { + String filename = pl.getString("texture", null); + if (filename != null) + tex = TextureCache.getTexture(api.resolveTextureFilename(filename), false); + return tex != null && super.update(pl, api); + } + + @Override + public Color getDiffuse(ShadingState state) { + return tex.getPixel(state.getUV().x, state.getUV().y); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/UVShader.java b/src/main/java/org/sunflow/core/shader/UVShader.java new file mode 100644 index 0000000..8646fb8 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/UVShader.java @@ -0,0 +1,22 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; + +public class UVShader implements Shader { + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public Color getRadiance(ShadingState state) { + if (state.getUV() == null) + return Color.BLACK; + return new Color(state.getUV().x, state.getUV().y, 0); + } + + public void scatterPhoton(ShadingState state, Color power) { + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/UberShader.java b/src/main/java/org/sunflow/core/shader/UberShader.java new file mode 100644 index 0000000..236436e --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/UberShader.java @@ -0,0 +1,141 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Ray; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.core.Texture; +import org.sunflow.core.TextureCache; +import org.sunflow.image.Color; +import org.sunflow.math.MathUtils; +import org.sunflow.math.OrthoNormalBasis; +import org.sunflow.math.Vector3; + +public class UberShader implements Shader { + private Color diff; + private Color spec; + private Texture diffmap; + private Texture specmap; + private float diffBlend; + private float specBlend; + private float glossyness; + private int numSamples; + + public UberShader() { + diff = spec = Color.GRAY; + diffmap = specmap = null; + diffBlend = specBlend = 1; + glossyness = 0; + numSamples = 4; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + diff = pl.getColor("diffuse", diff); + spec = pl.getColor("specular", spec); + String filename; + filename = pl.getString("diffuse.texture", null); + if (filename != null) + diffmap = TextureCache.getTexture(api.resolveTextureFilename(filename), false); + filename = pl.getString("specular.texture", null); + if (filename != null) + specmap = TextureCache.getTexture(api.resolveTextureFilename(filename), false); + diffBlend = MathUtils.clamp(pl.getFloat("diffuse.blend", diffBlend), 0, 1); + specBlend = MathUtils.clamp(pl.getFloat("specular.blend", diffBlend), 0, 1); + glossyness = MathUtils.clamp(pl.getFloat("glossyness", glossyness), 0, 1); + numSamples = pl.getInt("samples", numSamples); + return true; + } + + public Color getDiffuse(ShadingState state) { + return diffmap == null ? diff : Color.blend(diff, diffmap.getPixel(state.getUV().x, state.getUV().y), diffBlend); + } + + public Color getSpecular(ShadingState state) { + return specmap == null ? spec : Color.blend(spec, specmap.getPixel(state.getUV().x, state.getUV().y), specBlend); + } + + public Color getRadiance(ShadingState state) { + // make sure we are on the right side of the material + state.faceforward(); + // direct lighting + state.initLightSamples(); + state.initCausticSamples(); + Color d = getDiffuse(state); + Color lr = state.diffuse(d); + if (!state.includeSpecular()) + return lr; + if (glossyness == 0) { + float cos = state.getCosND(); + float dn = 2 * cos; + Vector3 refDir = new Vector3(); + refDir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; + refDir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; + refDir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; + Ray refRay = new Ray(state.getPoint(), refDir); + // compute Fresnel term + cos = 1 - cos; + float cos2 = cos * cos; + float cos5 = cos2 * cos2 * cos; + Color spec = getSpecular(state); + Color ret = Color.white(); + ret.sub(spec); + ret.mul(cos5); + ret.add(spec); + return lr.add(ret.mul(state.traceReflection(refRay, 0))); + } else + return lr.add(state.specularPhong(getSpecular(state), 2 / glossyness, numSamples)); + } + + public void scatterPhoton(ShadingState state, Color power) { + Color diffuse, specular; + // make sure we are on the right side of the material + state.faceforward(); + diffuse = getDiffuse(state); + specular = getSpecular(state); + state.storePhoton(state.getRay().getDirection(), power, diffuse); + float d = diffuse.getAverage(); + float r = specular.getAverage(); + double rnd = state.getRandom(0, 0, 1); + if (rnd < d) { + // photon is scattered + power.mul(diffuse).mul(1.0f / d); + OrthoNormalBasis onb = state.getBasis(); + double u = 2 * Math.PI * rnd / d; + double v = state.getRandom(0, 1, 1); + float s = (float) Math.sqrt(v); + float s1 = (float) Math.sqrt(1.0 - v); + Vector3 w = new Vector3((float) Math.cos(u) * s, (float) Math.sin(u) * s, s1); + w = onb.transform(w, new Vector3()); + state.traceDiffusePhoton(new Ray(state.getPoint(), w), power); + } else if (rnd < d + r) { + if (glossyness == 0) { + float cos = -Vector3.dot(state.getNormal(), state.getRay().getDirection()); + power.mul(diffuse).mul(1.0f / d); + // photon is reflected + float dn = 2 * cos; + Vector3 dir = new Vector3(); + dir.x = (dn * state.getNormal().x) + state.getRay().getDirection().x; + dir.y = (dn * state.getNormal().y) + state.getRay().getDirection().y; + dir.z = (dn * state.getNormal().z) + state.getRay().getDirection().z; + state.traceReflectionPhoton(new Ray(state.getPoint(), dir), power); + } else { + float dn = 2.0f * state.getCosND(); + // reflected direction + Vector3 refDir = new Vector3(); + refDir.x = (dn * state.getNormal().x) + state.getRay().dx; + refDir.y = (dn * state.getNormal().y) + state.getRay().dy; + refDir.z = (dn * state.getNormal().z) + state.getRay().dz; + power.mul(spec).mul(1.0f / r); + OrthoNormalBasis onb = state.getBasis(); + double u = 2 * Math.PI * (rnd - r) / r; + double v = state.getRandom(0, 1, 1); + float s = (float) Math.pow(v, 1 / ((1.0f / glossyness) + 1)); + float s1 = (float) Math.sqrt(1 - s * s); + Vector3 w = new Vector3((float) Math.cos(u) * s1, (float) Math.sin(u) * s1, s); + w = onb.transform(w, new Vector3()); + state.traceReflectionPhoton(new Ray(state.getPoint(), w), power); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/ViewCausticsShader.java b/src/main/java/org/sunflow/core/shader/ViewCausticsShader.java new file mode 100644 index 0000000..a583623 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/ViewCausticsShader.java @@ -0,0 +1,28 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.LightSample; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; + +public class ViewCausticsShader implements Shader { + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public Color getRadiance(ShadingState state) { + state.faceforward(); + state.initCausticSamples(); + // integrate a diffuse function + Color lr = Color.black(); + for (LightSample sample : state) + lr.madd(sample.dot(state.getNormal()), sample.getDiffuseRadiance()); + return lr.mul(1.0f / (float) Math.PI); + + } + + public void scatterPhoton(ShadingState state, Color power) { + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/ViewGlobalPhotonsShader.java b/src/main/java/org/sunflow/core/shader/ViewGlobalPhotonsShader.java new file mode 100644 index 0000000..badf0a2 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/ViewGlobalPhotonsShader.java @@ -0,0 +1,21 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; + +public class ViewGlobalPhotonsShader implements Shader { + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public Color getRadiance(ShadingState state) { + state.faceforward(); + return state.getGlobalRadiance(); + } + + public void scatterPhoton(ShadingState state, Color power) { + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/ViewIrradianceShader.java b/src/main/java/org/sunflow/core/shader/ViewIrradianceShader.java new file mode 100644 index 0000000..7fc70d8 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/ViewIrradianceShader.java @@ -0,0 +1,21 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; + +public class ViewIrradianceShader implements Shader { + public boolean update(ParameterList pl, SunflowAPI api) { + return true; + } + + public Color getRadiance(ShadingState state) { + state.faceforward(); + return new Color().set(state.getIrradiance(Color.WHITE)).mul(1.0f / (float) Math.PI); + } + + public void scatterPhoton(ShadingState state, Color power) { + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/shader/WireframeShader.java b/src/main/java/org/sunflow/core/shader/WireframeShader.java new file mode 100644 index 0000000..6dbf1f1 --- /dev/null +++ b/src/main/java/org/sunflow/core/shader/WireframeShader.java @@ -0,0 +1,76 @@ +package org.sunflow.core.shader; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.Shader; +import org.sunflow.core.ShadingState; +import org.sunflow.image.Color; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point3; + +public class WireframeShader implements Shader { + private Color lineColor; + private Color fillColor; + private float width; + private float cosWidth; + + public WireframeShader() { + lineColor = Color.BLACK; + fillColor = Color.WHITE; + // pick a very small angle - should be roughly the half the angular + // width of a + // pixel + width = (float) (Math.PI * 0.5 / 4096); + cosWidth = (float) Math.cos(width); + } + + public boolean update(ParameterList pl, SunflowAPI api) { + lineColor = pl.getColor("line", lineColor); + fillColor = pl.getColor("fill", fillColor); + width = pl.getFloat("width", width); + cosWidth = (float) Math.cos(width); + return true; + } + + public Color getFillColor(ShadingState state) { + return fillColor; + } + + public Color getLineColor(ShadingState state) { + return lineColor; + } + + public Color getRadiance(ShadingState state) { + Point3[] p = new Point3[3]; + if (!state.getTrianglePoints(p)) + return getFillColor(state); + // transform points into camera space + Point3 center = state.getPoint(); + Matrix4 w2c = state.getWorldToCamera(); + center = w2c.transformP(center); + for (int i = 0; i < 3; i++) + p[i] = w2c.transformP(state.transformObjectToWorld(p[i])); + float cn = 1.0f / (float) Math.sqrt(center.x * center.x + center.y * center.y + center.z * center.z); + for (int i = 0, i2 = 2; i < 3; i2 = i, i++) { + // compute orthogonal projection of the shading point onto each + // triangle edge as in: + // http://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html + float t = (center.x - p[i].x) * (p[i2].x - p[i].x); + t += (center.y - p[i].y) * (p[i2].y - p[i].y); + t += (center.z - p[i].z) * (p[i2].z - p[i].z); + t /= p[i].distanceToSquared(p[i2]); + float projx = (1 - t) * p[i].x + t * p[i2].x; + float projy = (1 - t) * p[i].y + t * p[i2].y; + float projz = (1 - t) * p[i].z + t * p[i2].z; + float n = 1.0f / (float) Math.sqrt(projx * projx + projy * projy + projz * projz); + // check angular width + float dot = projx * center.x + projy * center.y + projz * center.z; + if (dot * n * cn >= cosWidth) + return getLineColor(state); + } + return getFillColor(state); + } + + public void scatterPhoton(ShadingState state, Color power) { + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/tesselatable/BezierMesh.java b/src/main/java/org/sunflow/core/tesselatable/BezierMesh.java new file mode 100644 index 0000000..06fd26b --- /dev/null +++ b/src/main/java/org/sunflow/core/tesselatable/BezierMesh.java @@ -0,0 +1,248 @@ +package org.sunflow.core.tesselatable; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Tesselatable; +import org.sunflow.core.ParameterList.FloatParameter; +import org.sunflow.core.ParameterList.InterpolationType; +import org.sunflow.core.primitive.QuadMesh; +import org.sunflow.core.primitive.TriangleMesh; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class BezierMesh implements Tesselatable { + private int subdivs; + private boolean smooth; + private boolean quads; + private float[][] patches; + + public BezierMesh() { + this(null); + } + + public BezierMesh(float[][] patches) { + subdivs = 8; + smooth = true; + quads = false; + // convert to single precision + this.patches = patches; + } + + public BoundingBox getWorldBounds(Matrix4 o2w) { + BoundingBox bounds = new BoundingBox(); + if (o2w == null) { + for (int i = 0; i < patches.length; i++) { + float[] patch = patches[i]; + for (int j = 0; j < patch.length; j += 3) + bounds.include(patch[j], patch[j + 1], patch[j + 2]); + } + } else { + // transform vertices first + for (int i = 0; i < patches.length; i++) { + float[] patch = patches[i]; + for (int j = 0; j < patch.length; j += 3) { + float x = patch[j]; + float y = patch[j + 1]; + float z = patch[j + 2]; + float wx = o2w.transformPX(x, y, z); + float wy = o2w.transformPY(x, y, z); + float wz = o2w.transformPZ(x, y, z); + bounds.include(wx, wy, wz); + } + } + } + return bounds; + } + + private float[] bernstein(float u) { + float[] b = new float[4]; + float i = 1 - u; + b[0] = i * i * i; + b[1] = 3 * u * i * i; + b[2] = 3 * u * u * i; + b[3] = u * u * u; + return b; + } + + private float[] bernsteinDeriv(float u) { + if (!smooth) + return null; + float[] b = new float[4]; + float i = 1 - u; + b[0] = 3 * (0 - i * i); + b[1] = 3 * (i * i - 2 * u * i); + b[2] = 3 * (2 * u * i - u * u); + b[3] = 3 * (u * u - 0); + return b; + } + + private void getPatchPoint(float u, float v, float[] ctrl, float[] bu, float[] bv, float[] bdu, float[] bdv, Point3 p, Vector3 n) { + float px = 0; + float py = 0; + float pz = 0; + for (int i = 0, index = 0; i < 4; i++) { + for (int j = 0; j < 4; j++, index += 3) { + float scale = bu[j] * bv[i]; + px += ctrl[index + 0] * scale; + py += ctrl[index + 1] * scale; + pz += ctrl[index + 2] * scale; + } + } + p.x = px; + p.y = py; + p.z = pz; + if (n != null) { + float dpdux = 0; + float dpduy = 0; + float dpduz = 0; + float dpdvx = 0; + float dpdvy = 0; + float dpdvz = 0; + for (int i = 0, index = 0; i < 4; i++) { + for (int j = 0; j < 4; j++, index += 3) { + float scaleu = bdu[j] * bv[i]; + dpdux += ctrl[index + 0] * scaleu; + dpduy += ctrl[index + 1] * scaleu; + dpduz += ctrl[index + 2] * scaleu; + float scalev = bu[j] * bdv[i]; + dpdvx += ctrl[index + 0] * scalev; + dpdvy += ctrl[index + 1] * scalev; + dpdvz += ctrl[index + 2] * scalev; + } + } + // surface normal + n.x = (dpduy * dpdvz - dpduz * dpdvy); + n.y = (dpduz * dpdvx - dpdux * dpdvz); + n.z = (dpdux * dpdvy - dpduy * dpdvx); + } + } + + public PrimitiveList tesselate() { + float[] vertices = new float[patches.length * (subdivs + 1) * (subdivs + 1) * 3]; + float[] normals = smooth ? new float[patches.length * (subdivs + 1) * (subdivs + 1) * 3] : null; + float[] uvs = new float[patches.length * (subdivs + 1) * (subdivs + 1) * 2]; + int[] indices = new int[patches.length * subdivs * subdivs * (quads ? 4 : (2 * 3))]; + + int vidx = 0, pidx = 0; + float step = 1.0f / subdivs; + int vstride = subdivs + 1; + Point3 p = new Point3(); + Vector3 n = smooth ? new Vector3() : null; + for (float[] patch : patches) { + // create patch vertices + for (int i = 0, voff = 0; i <= subdivs; i++) { + float u = i * step; + float[] bu = bernstein(u); + float[] bdu = bernsteinDeriv(u); + for (int j = 0; j <= subdivs; j++, voff += 3) { + float v = j * step; + float[] bv = bernstein(v); + float[] bdv = bernsteinDeriv(v); + getPatchPoint(u, v, patch, bu, bv, bdu, bdv, p, n); + vertices[vidx + voff + 0] = p.x; + vertices[vidx + voff + 1] = p.y; + vertices[vidx + voff + 2] = p.z; + if (smooth) { + normals[vidx + voff + 0] = n.x; + normals[vidx + voff + 1] = n.y; + normals[vidx + voff + 2] = n.z; + } + uvs[(vidx + voff) / 3 * 2 + 0] = u; + uvs[(vidx + voff) / 3 * 2 + 1] = v; + } + } + // generate patch triangles + for (int i = 0, vbase = vidx / 3; i < subdivs; i++) { + for (int j = 0; j < subdivs; j++) { + int v00 = (i + 0) * vstride + (j + 0); + int v10 = (i + 1) * vstride + (j + 0); + int v01 = (i + 0) * vstride + (j + 1); + int v11 = (i + 1) * vstride + (j + 1); + if (quads) { + indices[pidx + 0] = vbase + v01; + indices[pidx + 1] = vbase + v00; + indices[pidx + 2] = vbase + v10; + indices[pidx + 3] = vbase + v11; + pidx += 4; + } else { + // add 2 triangles + indices[pidx + 0] = vbase + v00; + indices[pidx + 1] = vbase + v10; + indices[pidx + 2] = vbase + v01; + indices[pidx + 3] = vbase + v10; + indices[pidx + 4] = vbase + v11; + indices[pidx + 5] = vbase + v01; + pidx += 6; + } + } + } + vidx += vstride * vstride * 3; + } + ParameterList pl = new ParameterList(); + pl.addPoints("points", InterpolationType.VERTEX, vertices); + if (quads) + pl.addIntegerArray("quads", indices); + else + pl.addIntegerArray("triangles", indices); + pl.addTexCoords("uvs", InterpolationType.VERTEX, uvs); + if (smooth) + pl.addVectors("normals", InterpolationType.VERTEX, normals); + PrimitiveList m = quads ? new QuadMesh() : new TriangleMesh(); + m.update(pl, null); + pl.clear(true); + return m; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + subdivs = pl.getInt("subdivs", subdivs); + smooth = pl.getBoolean("smooth", smooth); + quads = pl.getBoolean("quads", quads); + int nu = pl.getInt("nu", 0); + int nv = pl.getInt("nv", 0); + pl.setVertexCount(nu * nv); + boolean uwrap = pl.getBoolean("uwrap", false); + boolean vwrap = pl.getBoolean("vwrap", false); + FloatParameter points = pl.getPointArray("points"); + if (points != null && points.interp == InterpolationType.VERTEX) { + int numUPatches = uwrap ? nu / 3 : (nu - 4) / 3 + 1; + int numVPatches = vwrap ? nv / 3 : (nv - 4) / 3 + 1; + if (numUPatches < 1 || numVPatches < 1) { + UI.printError(Module.GEOM, "Invalid number of patches for bezier mesh - ignoring"); + return false; + } + // generate patches + patches = new float[numUPatches * numVPatches][]; + for (int v = 0, p = 0; v < numVPatches; v++) { + for (int u = 0; u < numUPatches; u++, p++) { + float[] patch = patches[p] = new float[16 * 3]; + int up = u * 3; + int vp = v * 3; + for (int pv = 0; pv < 4; pv++) { + for (int pu = 0; pu < 4; pu++) { + int meshU = (up + pu) % nu; + int meshV = (vp + pv) % nv; + // copy point + patch[3 * (pv * 4 + pu) + 0] = points.data[3 * (meshU + nu * meshV) + 0]; + patch[3 * (pv * 4 + pu) + 1] = points.data[3 * (meshU + nu * meshV) + 1]; + patch[3 * (pv * 4 + pu) + 2] = points.data[3 * (meshU + nu * meshV) + 2]; + } + } + } + } + } + if (subdivs < 1) { + UI.printError(Module.GEOM, "Invalid subdivisions for bezier mesh - ignoring"); + return false; + } + if (patches == null) { + UI.printError(Module.GEOM, "No patch data present in bezier mesh - ignoring"); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/tesselatable/FileMesh.java b/src/main/java/org/sunflow/core/tesselatable/FileMesh.java new file mode 100644 index 0000000..a33883d --- /dev/null +++ b/src/main/java/org/sunflow/core/tesselatable/FileMesh.java @@ -0,0 +1,237 @@ +package org.sunflow.core.tesselatable; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.ParameterList; +import org.sunflow.core.PrimitiveList; +import org.sunflow.core.Tesselatable; +import org.sunflow.core.ParameterList.InterpolationType; +import org.sunflow.core.primitive.TriangleMesh; +import org.sunflow.math.BoundingBox; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.Memory; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; +import org.sunflow.util.FloatArray; +import org.sunflow.util.IntArray; + +public class FileMesh implements Tesselatable { + private String filename = null; + private boolean smoothNormals = false; + + public BoundingBox getWorldBounds(Matrix4 o2w) { + // world bounds can't be computed without reading file + // return null so the mesh will be loaded right away + return null; + } + + public PrimitiveList tesselate() { + if (filename.endsWith(".ra3")) { + try { + UI.printInfo(Module.GEOM, "RA3 - Reading geometry: \"%s\" ...", filename); + File file = new File(filename); + FileInputStream stream = new FileInputStream(filename); + MappedByteBuffer map = stream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()); + map.order(ByteOrder.LITTLE_ENDIAN); + IntBuffer ints = map.asIntBuffer(); + FloatBuffer buffer = map.asFloatBuffer(); + int numVerts = ints.get(0); + int numTris = ints.get(1); + UI.printInfo(Module.GEOM, "RA3 - * Reading %d vertices ...", numVerts); + float[] verts = new float[3 * numVerts]; + for (int i = 0; i < verts.length; i++) + verts[i] = buffer.get(2 + i); + UI.printInfo(Module.GEOM, "RA3 - * Reading %d triangles ...", numTris); + int[] tris = new int[3 * numTris]; + for (int i = 0; i < tris.length; i++) + tris[i] = ints.get(2 + verts.length + i); + stream.close(); + UI.printInfo(Module.GEOM, "RA3 - * Creating mesh ..."); + return generate(tris, verts, smoothNormals); + } catch (FileNotFoundException e) { + e.printStackTrace(); + UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - file not found", filename); + } catch (IOException e) { + e.printStackTrace(); + UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - I/O error occured", filename); + } + } else if (filename.endsWith(".obj")) { + int lineNumber = 1; + try { + UI.printInfo(Module.GEOM, "OBJ - Reading geometry: \"%s\" ...", filename); + FloatArray verts = new FloatArray(); + IntArray tris = new IntArray(); + FileReader file = new FileReader(filename); + BufferedReader bf = new BufferedReader(file); + String line; + while ((line = bf.readLine()) != null) { + if (line.startsWith("v")) { + String[] v = line.split("\\s+"); + verts.add(Float.parseFloat(v[1])); + verts.add(Float.parseFloat(v[2])); + verts.add(Float.parseFloat(v[3])); + } else if (line.startsWith("f")) { + String[] f = line.split("\\s+"); + if (f.length == 5) { + tris.add(Integer.parseInt(f[1]) - 1); + tris.add(Integer.parseInt(f[2]) - 1); + tris.add(Integer.parseInt(f[3]) - 1); + tris.add(Integer.parseInt(f[1]) - 1); + tris.add(Integer.parseInt(f[3]) - 1); + tris.add(Integer.parseInt(f[4]) - 1); + } else if (f.length == 4) { + tris.add(Integer.parseInt(f[1]) - 1); + tris.add(Integer.parseInt(f[2]) - 1); + tris.add(Integer.parseInt(f[3]) - 1); + } + } + if (lineNumber % 100000 == 0) + UI.printInfo(Module.GEOM, "OBJ - * Parsed %7d lines ...", lineNumber); + lineNumber++; + } + file.close(); + UI.printInfo(Module.GEOM, "OBJ - * Creating mesh ..."); + return generate(tris.trim(), verts.trim(), smoothNormals); + } catch (FileNotFoundException e) { + e.printStackTrace(); + UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - file not found", filename); + } catch (NumberFormatException e) { + e.printStackTrace(); + UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - syntax error at line %d", lineNumber); + } catch (IOException e) { + e.printStackTrace(); + UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - I/O error occured", filename); + } + } else if (filename.endsWith(".stl")) { + try { + UI.printInfo(Module.GEOM, "STL - Reading geometry: \"%s\" ...", filename); + FileInputStream file = new FileInputStream(filename); + DataInputStream stream = new DataInputStream(new BufferedInputStream(file)); + file.skip(80); + int numTris = getLittleEndianInt(stream.readInt()); + UI.printInfo(Module.GEOM, "STL - * Reading %d triangles ...", numTris); + long filesize = new File(filename).length(); + if (filesize != (84 + 50 * numTris)) { + UI.printWarning(Module.GEOM, "STL - Size of file mismatch (expecting %s, found %s)", Memory.bytesToString(84 + 14 * numTris), Memory.bytesToString(filesize)); + return null; + } + int[] tris = new int[3 * numTris]; + float[] verts = new float[9 * numTris]; + for (int i = 0, i3 = 0, index = 0; i < numTris; i++, i3 += 3) { + // skip normal + stream.readInt(); + stream.readInt(); + stream.readInt(); + for (int j = 0; j < 3; j++, index += 3) { + tris[i3 + j] = i3 + j; + // get xyz + verts[index + 0] = getLittleEndianFloat(stream.readInt()); + verts[index + 1] = getLittleEndianFloat(stream.readInt()); + verts[index + 2] = getLittleEndianFloat(stream.readInt()); + } + stream.readShort(); + if ((i + 1) % 100000 == 0) + UI.printInfo(Module.GEOM, "STL - * Parsed %7d triangles ...", i + 1); + } + file.close(); + // create geometry + UI.printInfo(Module.GEOM, "STL - * Creating mesh ..."); + if (smoothNormals) + UI.printWarning(Module.GEOM, "STL - format does not support shared vertices - normal smoothing disabled"); + return generate(tris, verts, false); + } catch (FileNotFoundException e) { + e.printStackTrace(); + UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - file not found", filename); + } catch (IOException e) { + e.printStackTrace(); + UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - I/O error occured", filename); + } + } else + UI.printWarning(Module.GEOM, "Unable to read mesh file \"%s\" - unrecognized format", filename); + return null; + } + + private TriangleMesh generate(int[] tris, float[] verts, boolean smoothNormals) { + ParameterList pl = new ParameterList(); + pl.addIntegerArray("triangles", tris); + pl.addPoints("points", InterpolationType.VERTEX, verts); + if (smoothNormals) { + float[] normals = new float[verts.length]; // filled with 0's + Point3 p0 = new Point3(); + Point3 p1 = new Point3(); + Point3 p2 = new Point3(); + Vector3 n = new Vector3(); + for (int i3 = 0; i3 < tris.length; i3 += 3) { + int v0 = tris[i3 + 0]; + int v1 = tris[i3 + 1]; + int v2 = tris[i3 + 2]; + p0.set(verts[3 * v0 + 0], verts[3 * v0 + 1], verts[3 * v0 + 2]); + p1.set(verts[3 * v1 + 0], verts[3 * v1 + 1], verts[3 * v1 + 2]); + p2.set(verts[3 * v2 + 0], verts[3 * v2 + 1], verts[3 * v2 + 2]); + Point3.normal(p0, p1, p2, n); // compute normal + // add face normal to each vertex + // note that these are not normalized so this in fact weights + // each normal by the area of the triangle + normals[3 * v0 + 0] += n.x; + normals[3 * v0 + 1] += n.y; + normals[3 * v0 + 2] += n.z; + normals[3 * v1 + 0] += n.x; + normals[3 * v1 + 1] += n.y; + normals[3 * v1 + 2] += n.z; + normals[3 * v2 + 0] += n.x; + normals[3 * v2 + 1] += n.y; + normals[3 * v2 + 2] += n.z; + } + // normalize all the vectors + for (int i3 = 0; i3 < normals.length; i3 += 3) { + n.set(normals[i3 + 0], normals[i3 + 1], normals[i3 + 2]); + n.normalize(); + normals[i3 + 0] = n.x; + normals[i3 + 1] = n.y; + normals[i3 + 2] = n.z; + } + pl.addVectors("normals", InterpolationType.VERTEX, normals); + } + TriangleMesh m = new TriangleMesh(); + if (m.update(pl, null)) + return m; + // something failed in creating the mesh, the error message will be + // printed by the mesh itself - no need to repeat it here + return null; + } + + public boolean update(ParameterList pl, SunflowAPI api) { + String file = pl.getString("filename", null); + if (file != null) + filename = api.resolveIncludeFilename(file); + smoothNormals = pl.getBoolean("smooth_normals", smoothNormals); + return filename != null; + } + + private int getLittleEndianInt(int i) { + // input integer has its bytes in big endian byte order + // swap them around + return (i >>> 24) | ((i >>> 8) & 0xFF00) | ((i << 8) & 0xFF0000) | (i << 24); + } + + private float getLittleEndianFloat(int i) { + // input integer has its bytes in big endian byte order + // swap them around and interpret data as floating point + return Float.intBitsToFloat(getLittleEndianInt(i)); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/tesselatable/Gumbo.java b/src/main/java/org/sunflow/core/tesselatable/Gumbo.java new file mode 100644 index 0000000..59f7c28 --- /dev/null +++ b/src/main/java/org/sunflow/core/tesselatable/Gumbo.java @@ -0,0 +1,1144 @@ +package org.sunflow.core.tesselatable; + +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.sunflow.math.Matrix4; +import org.sunflow.system.Parser; +import org.sunflow.system.Parser.ParserException; +import org.sunflow.util.FloatArray; + +public class Gumbo extends BezierMesh { + // generate raw patch data from source rib file + public static void main(String[] args) { + try { + Parser p; + p = new Parser("gumbo.rib"); + int begins = 1; + System.out.println("{"); + Matrix4 m = Matrix4.IDENTITY; + p.checkNextToken("AttributeBegin"); + while (begins != 0) { + if (p.peekNextToken("Patch")) { + p.checkNextToken("bicubic"); + p.checkNextToken("P"); + float[] patch = parseFloatArray(p); + if (patch.length == 48) { + // transform patch + for (int i = 0; i < 16; i++) { + float x = patch[3 * i + 0]; + float y = patch[3 * i + 1]; + float z = patch[3 * i + 2]; + patch[3 * i + 0] = m.transformPX(x, y, z); + patch[3 * i + 1] = m.transformPY(x, y, z); + patch[3 * i + 2] = m.transformPZ(x, y, z); + } + System.out.println("{"); + for (float v : patch) + System.out.printf(" %g,\n", v); + System.out.println("},"); + } + } else if (p.peekNextToken("Translate")) { + Matrix4 t = Matrix4.translation(p.getNextFloat(), p.getNextFloat(), p.getNextFloat()); + m = m.multiply(t); + } else if (p.peekNextToken("Rotate")) { + float angle = (float) Math.toRadians(p.getNextFloat()); + Matrix4 t = Matrix4.rotate(p.getNextFloat(), p.getNextFloat(), p.getNextFloat(), angle); + m = m.multiply(t); + } else if (p.peekNextToken("Scale")) { + Matrix4 t = Matrix4.scale(p.getNextFloat(), p.getNextFloat(), p.getNextFloat()); + m = m.multiply(t); + } else if (p.peekNextToken("TransformEnd")) { + m = Matrix4.IDENTITY; + } else if (p.peekNextToken("AttributeBegin")) { + begins++; + } else if (p.peekNextToken("AttributeEnd")) { + begins--; + } else + p.getNextToken(); + } + System.out.println("};"); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (ParserException e) { + e.printStackTrace(); + } + } + + private static float[] parseFloatArray(Parser p) throws IOException { + FloatArray array = new FloatArray(); + boolean done = false; + do { + String s = p.getNextToken(); + if (s.startsWith("[")) + s = s.substring(1); + if (s.endsWith("]")) { + s = s.substring(0, s.length() - 1); + done = true; + } + array.add(Float.parseFloat(s)); + } while (!done); + return array.trim(); + } + + // copy and paste data here + private static final float[][] PATCHES = { + { 10.0000f, 2.00000f, 0.00000f, 10.0000f, 2.00000f, 0.00000f, + 14.0000f, 2.00000f, 0.00000f, 14.0000f, 2.00000f, 0.00000f, + 10.0000f, 2.00000f, 0.00000f, 10.2277f, 2.22776f, + -0.911042f, 13.7722f, 2.22776f, -0.911042f, 14.0000f, + 2.00000f, 0.00000f, 10.0000f, 6.00000f, 0.00000f, 10.2277f, + 5.77223f, -0.911041f, 13.7722f, 5.77224f, -0.911041f, + 14.0000f, 6.00000f, 0.00000f, 10.0000f, 6.00000f, 0.00000f, + 10.0000f, 6.00000f, 0.00000f, 14.0000f, 6.00000f, 0.00000f, + 14.0000f, 6.00000f, 0.00000f, }, + { 10.0000f, 2.00000f, 0.00000f, 10.0000f, 2.00000f, 0.00000f, + 10.2483f, 2.08468f, 5.33563f, 10.0000f, 2.00000f, 8.00000f, + 10.0000f, 2.00000f, 0.00000f, 10.5077f, 0.457924f, + 2.06861f, 11.2875f, 1.04546f, 5.33563f, 11.0392f, + 0.960774f, 8.00000f, 14.0000f, 2.00000f, 0.00000f, + 13.4933f, 0.456761f, 2.07326f, 12.2875f, 2.17941f, + 5.23824f, 12.5766f, 1.37605f, 7.76601f, 14.0000f, 2.00000f, + 0.00000f, 14.0000f, 2.00000f, 0.00000f, 13.5399f, 2.92014f, + 4.93284f, 14.0000f, 2.00000f, 8.00000f, }, + { 14.0000f, 2.00000f, 0.00000f, 14.0000f, 2.00000f, 0.00000f, + 13.5399f, 2.92014f, 4.93284f, 14.0000f, 2.00000f, 8.00000f, + 14.0000f, 2.00000f, 0.00000f, 15.5432f, 2.50660f, 2.07326f, + 14.6921f, 3.60159f, 4.65188f, 15.8610f, 3.45395f, 6.60420f, + 14.0000f, 6.00000f, 0.00000f, 15.5425f, 5.49273f, 2.07061f, + 13.5521f, 3.61407f, 5.16626f, 15.0332f, 3.93355f, 6.96677f, + 14.0000f, 6.00000f, 0.00000f, 14.0000f, 6.00000f, 0.00000f, + 12.5859f, 4.58590f, 5.17181f, 14.0000f, 6.00000f, 8.00000f, }, + { 14.0000f, 6.00000f, 0.00000f, 14.0000f, 6.00000f, 0.00000f, + 12.5859f, 4.58590f, 5.17181f, 14.0000f, 6.00000f, 8.00000f, + 14.0000f, 6.00000f, 0.00000f, 13.4927f, 7.54257f, 2.07061f, + 11.6360f, 5.54118f, 5.17727f, 12.2594f, 6.87025f, 7.12974f, + 10.0000f, 6.00000f, 0.00000f, 10.5090f, 7.54076f, 2.06338f, + 11.5055f, 6.72455f, 4.92427f, 11.3818f, 7.59478f, 7.30228f, + 10.0000f, 6.00000f, 0.00000f, 10.0000f, 6.00000f, 0.00000f, + 10.6165f, 5.55161f, 5.03744f, 10.0000f, 6.00000f, 8.00000f, }, + { 10.0000f, 6.00000f, 0.00000f, 10.0000f, 6.00000f, 0.00000f, + 10.6165f, 5.55161f, 5.03744f, 10.0000f, 6.00000f, 8.00000f, + 10.0000f, 6.00000f, 0.00000f, 8.45923f, 5.49092f, 2.06338f, + 9.71866f, 4.36704f, 5.15174f, 8.96952f, 4.43964f, 7.67212f, + 10.0000f, 2.00000f, 0.00000f, 8.45792f, 2.50777f, 2.06861f, + 9.18040f, 3.15264f, 5.33563f, 8.93204f, 3.06795f, 8.00000f, + 10.0000f, 2.00000f, 0.00000f, 10.0000f, 2.00000f, 0.00000f, + 10.2483f, 2.08468f, 5.33563f, 10.0000f, 2.00000f, 8.00000f, }, + { 18.0000f, 2.00000f, 0.00000f, 18.0000f, 2.00000f, 0.00000f, + 22.0000f, 2.00000f, 0.00000f, 22.0000f, 2.00000f, 0.00000f, + 18.0000f, 2.00000f, 0.00000f, 18.2277f, 2.22776f, + -0.911042f, 21.7722f, 2.22776f, -0.911042f, 22.0000f, + 2.00000f, 0.00000f, 18.0000f, 6.00000f, 0.00000f, 18.2277f, + 5.77224f, -0.911042f, 21.7722f, 5.77223f, -0.911044f, + 22.0000f, 6.00000f, 0.00000f, 18.0000f, 6.00000f, 0.00000f, + 18.0000f, 6.00000f, 0.00000f, 22.0000f, 6.00000f, 0.00000f, + 22.0000f, 6.00000f, 0.00000f, }, + { 18.0000f, 2.00000f, 0.00000f, 18.0000f, 2.00000f, 0.00000f, + 18.6124f, 3.22488f, 4.93779f, 18.0000f, 2.00000f, 8.00000f, + 18.0000f, 2.00000f, 0.00000f, 18.5064f, 0.456647f, + 2.07371f, 19.8347f, 2.44159f, 5.28208f, 19.5248f, 1.39004f, + 7.69502f, 22.0000f, 2.00000f, 0.00000f, 21.4928f, + 0.457306f, 2.07108f, 21.0113f, 1.23344f, 5.33483f, + 20.9582f, 0.958220f, 8.00000f, 22.0000f, 2.00000f, + 0.00000f, 22.0000f, 2.00000f, 0.00000f, 22.0531f, 2.27522f, + 5.33483f, 22.0000f, 2.00000f, 8.00000f, }, + { 22.0000f, 2.00000f, 0.00000f, 22.0000f, 2.00000f, 0.00000f, + 22.0531f, 2.27522f, 5.33483f, 22.0000f, 2.00000f, 8.00000f, + 22.0000f, 2.00000f, 0.00000f, 23.5426f, 2.50715f, 2.07108f, + 23.1026f, 3.32477f, 5.33483f, 23.0495f, 3.04954f, 8.00000f, + 22.0000f, 6.00000f, 0.00000f, 23.5427f, 5.49294f, 2.07146f, + 22.2045f, 4.41977f, 5.20618f, 22.8434f, 4.43176f, 7.78914f, + 22.0000f, 6.00000f, 0.00000f, 22.0000f, 6.00000f, 0.00000f, + 21.4391f, 5.67950f, 5.02396f, 22.0000f, 6.00000f, 8.00000f, }, + { 22.0000f, 6.00000f, 0.00000f, 22.0000f, 6.00000f, 0.00000f, + 21.4391f, 5.67950f, 5.02396f, 22.0000f, 6.00000f, 8.00000f, + 22.0000f, 6.00000f, 0.00000f, 21.4929f, 7.54278f, 2.07146f, + 20.6640f, 6.95514f, 4.83944f, 20.8048f, 7.75546f, 7.23198f, + 18.0000f, 6.00000f, 0.00000f, 18.5072f, 7.54257f, 2.07061f, + 20.3933f, 5.55591f, 5.16253f, 19.7699f, 6.88498f, 7.11501f, + 18.0000f, 6.00000f, 0.00000f, 18.0000f, 6.00000f, 0.00000f, + 19.4157f, 4.58430f, 5.16861f, 18.0000f, 6.00000f, 8.00000f, }, + { 18.0000f, 6.00000f, 0.00000f, 18.0000f, 6.00000f, 0.00000f, + 19.4157f, 4.58430f, 5.16861f, 18.0000f, 6.00000f, 8.00000f, + 18.0000f, 6.00000f, 0.00000f, 16.4574f, 5.49273f, 2.07061f, + 18.4561f, 3.63070f, 5.17458f, 16.9751f, 3.95019f, 6.97509f, + 18.0000f, 2.00000f, 0.00000f, 16.4566f, 2.50649f, 2.07371f, + 17.5879f, 3.88144f, 4.64919f, 16.4270f, 3.57293f, 6.42706f, + 18.0000f, 2.00000f, 0.00000f, 18.0000f, 2.00000f, 0.00000f, + 18.6124f, 3.22488f, 4.93779f, 18.0000f, 2.00000f, 8.00000f, }, + { 18.0000f, 8.00000f, 0.00000f, 18.0000f, 8.00000f, 0.00000f, + 22.0000f, 8.00000f, 0.00000f, 22.0000f, 8.00000f, 0.00000f, + 18.0000f, 8.00000f, 0.00000f, 18.2277f, 8.22776f, + -0.911043f, 21.7722f, 8.22775f, -0.911043f, 22.0000f, + 8.00000f, 0.00000f, 18.0000f, 12.0000f, 0.00000f, 18.2277f, + 11.7722f, -0.911043f, 21.7722f, 11.7722f, -0.911042f, + 22.0000f, 12.0000f, 0.00000f, 18.0000f, 12.0000f, 0.00000f, + 18.0000f, 12.0000f, 0.00000f, 22.0000f, 12.0000f, 0.00000f, + 22.0000f, 12.0000f, 0.00000f, }, + { 18.0000f, 8.00000f, 0.00000f, 18.0000f, 8.00000f, 0.00000f, + 19.4156f, 9.41569f, 5.16861f, 18.0000f, 8.00000f, 8.00000f, + 18.0000f, 8.00000f, 0.00000f, 18.5072f, 6.45742f, 2.07061f, + 20.3933f, 8.44408f, 5.16253f, 19.7699f, 7.11501f, 7.11501f, + 22.0000f, 8.00000f, 0.00000f, 21.4929f, 6.45721f, 2.07146f, + 20.6640f, 7.04485f, 4.83943f, 20.8048f, 6.24453f, 7.23198f, + 22.0000f, 8.00000f, 0.00000f, 22.0000f, 8.00000f, 0.00000f, + 21.4391f, 8.32049f, 5.02396f, 22.0000f, 8.00000f, 8.00000f, }, + { 22.0000f, 8.00000f, 0.00000f, 22.0000f, 8.00000f, 0.00000f, + 21.4391f, 8.32049f, 5.02396f, 22.0000f, 8.00000f, 8.00000f, + 22.0000f, 8.00000f, 0.00000f, 23.5427f, 8.50705f, 2.07146f, + 22.2045f, 9.58022f, 5.20618f, 22.8434f, 9.56823f, 7.78914f, + 22.0000f, 12.0000f, 0.00000f, 23.5426f, 11.4928f, 2.07108f, + 23.1026f, 10.6752f, 5.33483f, 23.0495f, 10.9504f, 8.00000f, + 22.0000f, 12.0000f, 0.00000f, 22.0000f, 12.0000f, 0.00000f, + 22.0531f, 11.7247f, 5.33483f, 22.0000f, 12.0000f, 8.00000f, }, + { 22.0000f, 12.0000f, 0.00000f, 22.0000f, 12.0000f, 0.00000f, + 22.0531f, 11.7247f, 5.33483f, 22.0000f, 12.0000f, 8.00000f, + 22.0000f, 12.0000f, 0.00000f, 21.4928f, 13.5426f, 2.07108f, + 21.0113f, 12.7665f, 5.33483f, 20.9582f, 13.0417f, 8.00000f, + 18.0000f, 12.0000f, 0.00000f, 18.5064f, 13.5433f, 2.07371f, + 19.8347f, 11.5584f, 5.28208f, 19.5257f, 12.6103f, 7.69484f, + 18.0000f, 12.0000f, 0.00000f, 18.0000f, 12.0000f, 0.00000f, + 18.6124f, 10.7751f, 4.93779f, 18.0000f, 12.0000f, 8.00000f, }, + { 18.0000f, 12.0000f, 0.00000f, 18.0000f, 12.0000f, 0.00000f, + 18.6124f, 10.7751f, 4.93779f, 18.0000f, 12.0000f, 8.00000f, + 18.0000f, 12.0000f, 0.00000f, 16.4566f, 11.4935f, 2.07371f, + 17.5879f, 10.1185f, 4.64919f, 16.4281f, 10.4281f, 6.42818f, + 18.0000f, 8.00000f, 0.00000f, 16.4574f, 8.50726f, 2.07061f, + 18.4561f, 10.3693f, 5.17458f, 16.9750f, 10.0498f, 6.97509f, + 18.0000f, 8.00000f, 0.00000f, 18.0000f, 8.00000f, 0.00000f, + 19.4156f, 9.41569f, 5.16861f, 18.0000f, 8.00000f, 8.00000f, }, + { 10.0000f, 8.00000f, 0.00000f, 10.0000f, 8.00000f, 0.00000f, + 14.0000f, 8.00000f, 0.00000f, 14.0000f, 8.00000f, 0.00000f, + 10.0000f, 8.00000f, 0.00000f, 10.2277f, 8.22776f, + -0.911042f, 13.7722f, 8.22775f, -0.911042f, 14.0000f, + 8.00000f, 0.00000f, 10.0000f, 12.0000f, 0.00000f, 10.2277f, + 11.7722f, -0.911041f, 13.7722f, 11.7722f, -0.911042f, + 14.0000f, 12.0000f, 0.00000f, 10.0000f, 12.0000f, 0.00000f, + 10.0000f, 12.0000f, 0.00000f, 14.0000f, 12.0000f, 0.00000f, + 14.0000f, 12.0000f, 0.00000f, }, + { 10.0000f, 8.00000f, 0.00000f, 10.0000f, 8.00000f, 0.00000f, + 10.6165f, 8.44838f, 5.03744f, 10.0000f, 8.00000f, 8.00000f, + 10.0000f, 8.00000f, 0.00000f, 10.5090f, 6.45923f, 2.06338f, + 11.5055f, 7.27544f, 4.92427f, 11.3818f, 6.40521f, 7.30228f, + 14.0000f, 8.00000f, 0.00000f, 13.4927f, 6.45742f, 2.07061f, + 11.6360f, 8.45881f, 5.17727f, 12.2594f, 7.12974f, 7.12974f, + 14.0000f, 8.00000f, 0.00000f, 14.0000f, 8.00000f, 0.00000f, + 12.5879f, 9.41203f, 5.17592f, 14.0000f, 8.00000f, 8.00000f, }, + { 14.0000f, 8.00000f, 0.00000f, 14.0000f, 8.00000f, 0.00000f, + 12.5879f, 9.41203f, 5.17592f, 14.0000f, 8.00000f, 8.00000f, + 14.0000f, 8.00000f, 0.00000f, 15.5425f, 8.50726f, 2.07061f, + 13.5438f, 10.3693f, 5.17458f, 15.0249f, 10.0498f, 6.97509f, + 14.0000f, 12.0000f, 0.00000f, 15.5433f, 11.4935f, 2.07371f, + 14.4120f, 10.1185f, 4.64919f, 15.5718f, 10.4281f, 6.42818f, + 14.0000f, 12.0000f, 0.00000f, 14.0000f, 12.0000f, 0.00000f, + 13.3876f, 10.7753f, 4.93833f, 14.0000f, 12.0000f, 8.00000f, }, + { 14.0000f, 12.0000f, 0.00000f, 14.0000f, 12.0000f, 0.00000f, + 13.3876f, 10.7753f, 4.93833f, 14.0000f, 12.0000f, 8.00000f, + 14.0000f, 12.0000f, 0.00000f, 13.4935f, 13.5433f, 2.07371f, + 12.1678f, 11.5573f, 5.28260f, 12.4764f, 12.6094f, 7.69529f, + 10.0000f, 12.0000f, 0.00000f, 10.5077f, 13.5420f, 2.06861f, + 11.2881f, 12.9550f, 5.33563f, 11.0397f, 13.0397f, 7.99999f, + 10.0000f, 12.0000f, 0.00000f, 10.0000f, 12.0000f, 0.00000f, + 10.2483f, 11.9153f, 5.33563f, 10.0000f, 12.0000f, 8.00000f, }, + { 10.0000f, 12.0000f, 0.00000f, 10.0000f, 12.0000f, 0.00000f, + 10.2483f, 11.9153f, 5.33563f, 10.0000f, 12.0000f, 8.00000f, + 10.0000f, 12.0000f, 0.00000f, 8.45792f, 11.4922f, 2.06861f, + 9.18040f, 10.8473f, 5.33563f, 8.93204f, 10.9320f, 8.00000f, + 10.0000f, 8.00000f, 0.00000f, 8.45923f, 8.50907f, 2.06338f, + 9.71866f, 9.63295f, 5.15174f, 8.96952f, 9.56035f, 7.67212f, + 10.0000f, 8.00000f, 0.00000f, 10.0000f, 8.00000f, 0.00000f, + 10.6165f, 8.44838f, 5.03744f, 10.0000f, 8.00000f, 8.00000f, }, + { 14.0000f, 2.00000f, 8.00000f, 15.9088f, 2.83677f, 8.31378f, + 16.2210f, 2.71156f, 8.35578f, 18.0000f, 2.00000f, 8.00000f, + 15.8610f, 3.45395f, 6.60420f, 16.3392f, 3.39355f, 7.40290f, + 15.8223f, 3.41220f, 7.35328f, 16.4270f, 3.57293f, 6.42706f, + 15.0332f, 3.93355f, 6.96677f, 15.8446f, 4.10859f, 7.95327f, + 16.1636f, 4.12523f, 7.96159f, 16.9751f, 3.95019f, 6.97509f, + 14.0000f, 6.00000f, 8.00000f, 15.1986f, 5.40068f, 8.59932f, + 16.8013f, 5.40068f, 8.59932f, 18.0000f, 6.00000f, 8.00000f, }, + { 14.0000f, 6.00000f, 8.00000f, 15.1986f, 5.40068f, 8.59932f, + 16.8013f, 5.40068f, 8.59932f, 18.0000f, 6.00000f, 8.00000f, + 13.7003f, 6.59931f, 8.29966f, 14.8989f, 5.99999f, 8.89898f, + 17.1010f, 6.00000f, 8.89898f, 18.2996f, 6.59932f, 8.29966f, + 13.7003f, 7.40068f, 8.29966f, 14.8989f, 8.00000f, 8.89898f, + 17.1010f, 8.00000f, 8.89897f, 18.2996f, 7.40068f, 8.29966f, + 14.0000f, 8.00000f, 8.00000f, 15.1986f, 8.59932f, 8.59932f, + 16.8013f, 8.59931f, 8.59931f, 18.0000f, 8.00000f, 8.00000f, }, + { 14.0000f, 8.00000f, 8.00000f, 15.1986f, 8.59932f, 8.59932f, + 16.8013f, 8.59931f, 8.59931f, 18.0000f, 8.00000f, 8.00000f, + 15.0249f, 10.0498f, 6.97509f, 15.8363f, 9.87476f, 7.96159f, + 16.1636f, 9.87476f, 7.96159f, 16.9750f, 10.0498f, 6.97509f, + 15.5718f, 10.4281f, 6.42818f, 16.1744f, 10.5890f, 7.35264f, + 15.8255f, 10.5890f, 7.35264f, 16.4281f, 10.4281f, 6.42818f, + 14.0000f, 12.0000f, 8.00000f, 15.7780f, 11.2887f, 8.35561f, + 16.2225f, 11.2890f, 8.35548f, 18.0000f, 12.0000f, 8.00000f, }, + { 10.0000f, 6.00000f, 8.00000f, 11.3818f, 7.59478f, 7.30228f, + 12.2594f, 6.87025f, 7.12974f, 14.0000f, 6.00000f, 8.00000f, + 10.6753f, 7.02258f, 8.21487f, 11.3643f, 7.71766f, 7.63807f, + 12.4302f, 7.23435f, 7.66462f, 13.7003f, 6.59931f, 8.29966f, + 10.6753f, 6.97741f, 8.21487f, 11.3643f, 6.28233f, 7.63807f, + 12.4302f, 6.76564f, 7.66462f, 13.7003f, 7.40068f, 8.29966f, + 10.0000f, 8.00000f, 8.00000f, 11.3818f, 6.40521f, 7.30228f, + 12.2594f, 7.12974f, 7.12974f, 14.0000f, 8.00000f, 8.00000f, }, + { 18.0000f, 6.00000f, 8.00000f, 19.7699f, 6.88498f, 7.11501f, + 20.8048f, 7.75546f, 7.23198f, 22.0000f, 6.00000f, 8.00000f, + 18.2996f, 6.59932f, 8.29966f, 19.5991f, 7.24908f, 7.64989f, + 20.8231f, 7.85928f, 7.54235f, 21.2738f, 7.35025f, 8.18154f, + 18.2996f, 7.40068f, 8.29966f, 19.5991f, 6.75091f, 7.64989f, + 20.8231f, 6.14071f, 7.54235f, 21.2738f, 6.64974f, 8.18154f, + 18.0000f, 8.00000f, 8.00000f, 19.7699f, 7.11501f, 7.11501f, + 20.8048f, 6.24453f, 7.23198f, 22.0000f, 8.00000f, 8.00000f, }, + { 10.0000f, 2.00000f, 8.00000f, 9.87124f, 1.95609f, 9.38125f, + 9.34408f, 1.50592f, 9.94049f, 9.00000f, 1.50000f, 12.0000f, + 11.0392f, 0.960774f, 8.00000f, 10.9104f, 0.916871f, + 9.38125f, 10.6451f, 0.577962f, 10.5477f, 10.5822f, + 0.465660f, 11.9069f, 12.5766f, 1.37605f, 7.76601f, + 12.7310f, 0.946813f, 9.11659f, 12.3287f, 1.27854f, + 10.5830f, 12.3287f, 1.27854f, 12.0000f, 14.0000f, 2.00000f, + 8.00000f, 14.0621f, 1.53028f, 9.33539f, 14.0000f, 1.00000f, + 10.5830f, 14.0000f, 1.00000f, 12.0000f, }, + { 14.0000f, 2.00000f, 8.00000f, 14.0621f, 1.53028f, 9.33539f, + 14.0000f, 1.00000f, 10.5830f, 14.0000f, 1.00000f, 12.0000f, + 15.9088f, 2.83677f, 8.31378f, 15.3278f, 2.08512f, 9.54346f, + 15.3579f, 0.773669f, 10.5830f, 15.3579f, 0.773669f, + 12.0000f, 16.2210f, 2.71156f, 8.35578f, 16.7241f, 1.82762f, + 9.62057f, 16.6232f, 0.00000f, 10.4734f, 16.6232f, 0.00000f, + 12.0000f, 18.0000f, 2.00000f, 8.00000f, 18.0000f, 1.31729f, + 9.36540f, 18.0000f, 0.00000f, 10.4734f, 18.0000f, 0.00000f, + 12.0000f, }, + { 18.0000f, 2.00000f, 8.00000f, 18.0000f, 1.31729f, 9.36540f, + 18.0000f, 0.00000f, 10.4734f, 18.0000f, 0.00000f, 12.0000f, + 19.5248f, 1.39004f, 7.69502f, 19.3449f, 0.779322f, + 9.09642f, 19.4294f, 0.00000f, 10.4734f, 19.4294f, 0.00000f, + 12.0000f, 20.9582f, 0.958220f, 8.00000f, 20.9309f, + 0.816847f, 9.36900f, 21.1736f, -0.0212363f, 10.6358f, + 20.9892f, -0.0107873f, 12.0000f, 22.0000f, 2.00000f, + 8.00000f, 21.9727f, 1.85862f, 9.36900f, 22.1844f, + 0.989551f, 10.6358f, 22.0000f, 1.00000f, 12.0000f, }, + { 22.0000f, 2.00000f, 8.00000f, 21.9727f, 1.85862f, 9.36900f, + 22.1844f, 0.989551f, 10.6358f, 22.0000f, 1.00000f, + 12.0000f, 23.0495f, 3.04954f, 8.00000f, 23.0222f, 2.90817f, + 9.36900f, 23.1847f, 1.98990f, 10.6359f, 23.0003f, 2.00035f, + 12.0000f, 22.8434f, 4.43176f, 7.78914f, 23.1846f, 4.43817f, + 9.16885f, 22.7037f, 3.28188f, 10.5868f, 22.8245f, 3.59620f, + 12.0001f, 22.0000f, 6.00000f, 8.00000f, 22.4865f, 5.73632f, + 9.34339f, 22.8791f, 4.68567f, 10.5866f, 23.0000f, 5.00000f, + 12.0000f, }, + { 22.0000f, 6.00000f, 8.00000f, 22.4865f, 5.73632f, 9.34339f, + 22.8791f, 4.68567f, 10.5866f, 23.0000f, 5.00000f, 12.0000f, + 21.2738f, 7.35025f, 8.18154f, 22.1519f, 6.35839f, 9.42703f, + 23.0451f, 6.01382f, 10.5865f, 23.1660f, 6.32815f, 11.9998f, + 21.2738f, 6.64974f, 8.18154f, 22.1519f, 7.64160f, 9.42703f, + 23.0451f, 7.98617f, 10.5865f, 23.1660f, 7.67185f, 11.9998f, + 22.0000f, 8.00000f, 8.00000f, 22.4865f, 8.26367f, 9.34339f, + 22.8791f, 9.31432f, 10.5866f, 23.0000f, 9.00000f, 12.0000f, }, + { 22.0000f, 8.00000f, 8.00000f, 22.4865f, 8.26367f, 9.34339f, + 22.8791f, 9.31432f, 10.5866f, 23.0000f, 9.00000f, 12.0000f, + 22.8434f, 9.56823f, 7.78914f, 23.1846f, 9.56182f, 9.16885f, + 22.7037f, 10.7181f, 10.5868f, 22.8245f, 10.4038f, 12.0001f, + 23.0495f, 10.9504f, 8.00000f, 23.0222f, 11.0918f, 9.36900f, + 23.1847f, 12.0100f, 10.6358f, 23.0003f, 11.9996f, 12.0000f, + 22.0000f, 12.0000f, 8.00000f, 21.9727f, 12.1413f, 9.36900f, + 22.1844f, 13.0104f, 10.6358f, 22.0000f, 13.0000f, 12.0000f, }, + { 22.0000f, 12.0000f, 8.00000f, 21.9727f, 12.1413f, 9.36900f, + 22.1844f, 13.0104f, 10.6358f, 22.0000f, 13.0000f, 12.0000f, + 20.9582f, 13.0417f, 8.00000f, 20.9309f, 13.1831f, 9.36900f, + 21.1850f, 14.0098f, 10.6358f, 21.0006f, 13.9994f, 12.0000f, + 19.5257f, 12.6103f, 7.69484f, 19.3449f, 13.2259f, 9.10702f, + 19.4024f, 13.8246f, 10.4615f, 19.4024f, 13.8246f, 12.0000f, + 18.0000f, 12.0000f, 8.00000f, 18.0000f, 12.6880f, 9.37601f, + 18.0000f, 14.0000f, 10.4615f, 18.0000f, 14.0000f, 12.0000f, }, + { 18.0000f, 12.0000f, 8.00000f, 18.0000f, 12.6880f, 9.37601f, + 18.0000f, 14.0000f, 10.4615f, 18.0000f, 14.0000f, 12.0000f, + 16.2225f, 11.2890f, 8.35548f, 16.7274f, 12.1789f, 9.63053f, + 16.6700f, 14.1662f, 10.4615f, 16.6700f, 14.1662f, 12.0000f, + 15.7780f, 11.2887f, 8.35561f, 15.2726f, 12.1809f, 9.63458f, + 15.3221f, 14.2203f, 10.4570f, 15.3221f, 14.2203f, 12.0000f, + 14.0000f, 12.0000f, 8.00000f, 14.0000f, 12.6900f, 9.38006f, + 14.0000f, 14.0000f, 10.4570f, 14.0000f, 14.0000f, 12.0000f, }, + { 14.0000f, 12.0000f, 8.00000f, 14.0000f, 12.6900f, 9.38006f, + 14.0000f, 14.0000f, 10.4570f, 14.0000f, 14.0000f, 12.0000f, + 12.4764f, 12.6094f, 7.69529f, 12.6576f, 13.2269f, 9.11159f, + 12.2533f, 13.7089f, 10.4570f, 12.2533f, 13.7089f, 12.0000f, + 11.0397f, 13.0397f, 7.99999f, 10.9112f, 13.0836f, 9.37941f, + 10.4694f, 13.7456f, 10.6337f, 10.3994f, 13.6615f, 12.0112f, + 10.0000f, 12.0000f, 8.00000f, 9.87141f, 12.0438f, 9.37941f, + 9.15043f, 12.6614f, 10.0622f, 9.00000f, 12.5000f, 12.0000f, }, + { 9.00000f, 1.50000f, 12.0000f, 9.07887f, 1.31725f, 13.1541f, + 9.52420f, 0.976845f, 14.7029f, 10.0000f, 2.00000f, + 16.0000f, 10.5822f, 0.465660f, 11.9069f, 10.5150f, + 0.345727f, 13.3585f, 10.9295f, 1.13219f, 14.8938f, + 11.1033f, 2.00243f, 15.9980f, 12.3287f, 1.27854f, 12.0000f, + 12.3287f, 1.27854f, 13.4306f, 12.6655f, 0.988352f, + 14.9883f, 12.6655f, 2.00000f, 16.0000f, 14.0000f, 1.00000f, + 12.0000f, 14.0000f, 1.00000f, 13.4306f, 14.0000f, + 0.988352f, 14.9883f, 14.0000f, 2.00000f, 16.0000f, }, + { 14.0000f, 1.00000f, 12.0000f, 14.0000f, 1.00000f, 13.4306f, + 14.0000f, 0.988352f, 14.9883f, 14.0000f, 2.00000f, + 16.0000f, 15.3579f, 0.773669f, 12.0000f, 15.3579f, + 0.773669f, 13.4306f, 15.3354f, 0.988352f, 14.9883f, + 15.3354f, 2.00000f, 16.0000f, 16.6232f, 0.00000f, 12.0000f, + 16.6232f, 0.00000f, 13.5696f, 16.8433f, 0.801365f, + 15.2136f, 16.6794f, 2.01251f, 16.1985f, 18.0000f, 0.00000f, + 12.0000f, 18.0000f, 0.00000f, 13.5696f, 18.1639f, + 0.788853f, 15.0150f, 18.0000f, 2.00000f, 16.0000f, }, + { 18.0000f, 0.00000f, 12.0000f, 18.0000f, 0.00000f, 13.5696f, + 18.1639f, 0.788853f, 15.0150f, 18.0000f, 2.00000f, + 16.0000f, 19.4294f, 0.00000f, 12.0000f, 19.4294f, 0.00000f, + 13.5696f, 19.2285f, 0.778765f, 14.8549f, 19.0646f, + 1.98991f, 15.8399f, 20.9892f, -0.0107873f, 12.0000f, + 20.8354f, -0.00207507f, 13.1374f, 20.6978f, 0.580195f, + 14.6361f, 20.2213f, 1.34332f, 15.3489f, 22.0000f, 1.00000f, + 12.0000f, 21.8462f, 1.00871f, 13.1374f, 21.4764f, 1.23687f, + 14.2872f, 21.0000f, 2.00000f, 15.0000f, }, + { 22.0000f, 1.00000f, 12.0000f, 21.8462f, 1.00871f, 13.1374f, + 21.4764f, 1.23687f, 14.2872f, 21.0000f, 2.00000f, 15.0000f, + 23.0003f, 2.00035f, 12.0000f, 22.8465f, 2.00907f, 13.1374f, + 22.7558f, 2.31585f, 13.7139f, 22.2794f, 3.07898f, 14.4267f, + 22.8245f, 3.59620f, 12.0001f, 22.9168f, 3.83627f, 13.0796f, + 22.3069f, 4.55472f, 14.7895f, 21.9821f, 5.49730f, 15.2768f, + 23.0000f, 5.00000f, 12.0000f, 23.0922f, 5.24007f, 13.0794f, + 22.8248f, 6.05742f, 14.0127f, 22.5000f, 7.00000f, 14.5000f, }, + { 22.5000f, 7.00000f, 14.5000f, 22.8248f, 7.94257f, 14.0127f, + 23.0922f, 8.75992f, 13.0794f, 23.0000f, 9.00000f, 12.0000f, + 22.5000f, 7.00000f, 14.5000f, 23.1496f, 7.00000f, 13.5254f, + 23.2583f, 7.43177f, 13.0793f, 23.1660f, 7.67185f, 11.9998f, + 22.5000f, 7.00000f, 14.5000f, 23.1496f, 7.00000f, 13.5254f, + 23.2583f, 6.56822f, 13.0793f, 23.1660f, 6.32815f, 11.9998f, + 22.5000f, 7.00000f, 14.5000f, 22.8248f, 6.05742f, 14.0127f, + 23.0922f, 5.24007f, 13.0794f, 23.0000f, 5.00000f, 12.0000f, }, + { 23.0000f, 9.00000f, 12.0000f, 23.0922f, 8.75992f, 13.0794f, + 22.8248f, 7.94257f, 14.0127f, 22.5000f, 7.00000f, 14.5000f, + 22.8245f, 10.4038f, 12.0001f, 22.9168f, 10.1637f, 13.0796f, + 22.3069f, 9.44527f, 14.7895f, 21.9821f, 8.50269f, 15.2768f, + 23.0003f, 11.9996f, 12.0000f, 22.8465f, 11.9909f, 13.1374f, + 22.7558f, 11.6841f, 13.7139f, 22.2794f, 10.9210f, 14.4267f, + 22.0000f, 13.0000f, 12.0000f, 21.8462f, 12.9912f, 13.1374f, + 21.4764f, 12.7631f, 14.2872f, 21.0000f, 12.0000f, 15.0000f, }, + { 22.0000f, 13.0000f, 12.0000f, 21.8462f, 12.9912f, 13.1374f, + 21.4764f, 12.7631f, 14.2872f, 21.0000f, 12.0000f, 15.0000f, + 21.0006f, 13.9994f, 12.0000f, 20.8468f, 13.9906f, 13.1374f, + 20.6978f, 13.4198f, 14.6361f, 20.2213f, 12.6566f, 15.3489f, + 19.4024f, 13.8246f, 12.0000f, 19.4024f, 13.8246f, 13.5675f, + 19.2283f, 13.2196f, 14.8562f, 19.0646f, 12.0100f, 15.8399f, + 18.0000f, 14.0000f, 12.0000f, 18.0000f, 14.0000f, 13.5675f, + 18.1636f, 13.2095f, 15.0163f, 18.0000f, 12.0000f, 16.0000f, }, + { 18.0000f, 14.0000f, 12.0000f, 18.0000f, 14.0000f, 13.5675f, + 18.1636f, 13.2095f, 15.0163f, 18.0000f, 12.0000f, 16.0000f, + 16.6700f, 14.1662f, 12.0000f, 16.6700f, 14.1662f, 13.5675f, + 16.8420f, 13.1970f, 15.2151f, 16.6783f, 11.9874f, 16.1987f, + 15.3221f, 14.2203f, 12.0000f, 15.3221f, 14.2203f, 13.5616f, + 15.3213f, 13.3497f, 14.9846f, 15.3264f, 12.1643f, 16.0012f, + 14.0000f, 14.0000f, 12.0000f, 14.0000f, 14.0000f, 13.5616f, + 13.9949f, 13.1854f, 14.9833f, 14.0000f, 12.0000f, 16.0000f, }, + { 14.0000f, 14.0000f, 12.0000f, 14.0000f, 14.0000f, 13.5616f, + 13.9949f, 13.1854f, 14.9833f, 14.0000f, 12.0000f, 16.0000f, + 12.2533f, 13.7089f, 12.0000f, 12.2533f, 13.7089f, 13.5616f, + 12.6271f, 13.0159f, 14.9821f, 12.6322f, 11.8305f, 15.9987f, + 10.3994f, 13.6615f, 12.0112f, 10.3212f, 13.5676f, 13.5504f, + 10.9955f, 12.0544f, 14.6564f, 11.1786f, 11.1102f, 15.8546f, + 9.00000f, 12.5000f, 12.0000f, 8.96405f, 12.4448f, 13.3410f, + 9.51945f, 12.2157f, 14.4584f, 10.0000f, 11.0000f, 16.0000f, }, + { 10.0000f, 2.00000f, 16.0000f, 10.0036f, 3.39890f, 17.7763f, + 9.97797f, 5.10529f, 18.0055f, 10.0000f, 7.00000f, 18.0000f, + 11.1033f, 2.00243f, 15.9980f, 11.3375f, 3.17526f, 17.4863f, + 11.2748f, 5.10529f, 17.6813f, 11.2968f, 7.00000f, 17.6757f, + 12.6655f, 2.00000f, 16.0000f, 12.6655f, 3.31963f, 17.3196f, + 12.6644f, 5.13375f, 18.0572f, 12.6644f, 7.00000f, 18.0572f, + 14.0000f, 2.00000f, 16.0000f, 14.0000f, 3.31963f, 17.3196f, + 14.0000f, 5.13375f, 18.0000f, 14.0000f, 7.00000f, 18.0000f, }, + { 14.0000f, 2.00000f, 16.0000f, 14.0000f, 3.31963f, 17.3196f, + 14.0000f, 5.13375f, 18.0000f, 14.0000f, 7.00000f, 18.0000f, + 15.3354f, 2.00000f, 16.0000f, 15.3354f, 3.31963f, 17.3196f, + 15.0055f, 5.13375f, 17.9569f, 15.0055f, 7.00000f, 17.9569f, + 16.6794f, 2.01251f, 16.1985f, 16.4870f, 3.43424f, 17.3547f, + 16.0130f, 5.15743f, 17.8973f, 16.0130f, 7.00000f, 17.8973f, + 18.0000f, 2.00000f, 16.0000f, 17.8076f, 3.42172f, 17.1561f, + 17.0000f, 5.15743f, 17.7000f, 17.0000f, 7.00000f, 17.7000f, }, + { 18.0000f, 2.00000f, 16.0000f, 17.8076f, 3.42172f, 17.1561f, + 17.0000f, 5.15743f, 17.7000f, 17.0000f, 7.00000f, 17.7000f, + 19.0646f, 1.98991f, 15.8399f, 18.8722f, 3.41164f, 16.9960f, + 17.6994f, 5.15743f, 17.5601f, 17.6994f, 7.00000f, 17.5601f, + 20.2213f, 1.34332f, 15.3489f, 19.4209f, 2.62530f, 16.5462f, + 19.1846f, 5.31432f, 16.8753f, 19.0000f, 7.00000f, 17.0000f, + 21.0000f, 2.00000f, 15.0000f, 20.1996f, 3.28198f, 16.1973f, + 19.0000f, 7.00000f, 17.0000f, 19.0000f, 7.00000f, 17.0000f, }, + { 21.0000f, 2.00000f, 15.0000f, 20.1996f, 3.28198f, 16.1973f, + 19.0000f, 7.00000f, 17.0000f, 19.0000f, 7.00000f, 17.0000f, + 22.2794f, 3.07898f, 14.4267f, 21.4790f, 4.36096f, 15.6240f, + 20.5517f, 6.99999f, 15.9525f, 19.0000f, 7.00000f, 17.0000f, + 21.9821f, 5.49730f, 15.2768f, 21.4642f, 7.00000f, 16.0536f, + 21.4790f, 9.63903f, 15.6240f, 20.1996f, 10.7180f, 16.1973f, + 22.5000f, 7.00000f, 14.5000f, 21.9821f, 8.50269f, 15.2768f, + 22.2794f, 10.9210f, 14.4267f, 21.0000f, 12.0000f, 15.0000f, }, + { 21.0000f, 12.0000f, 15.0000f, 20.1996f, 10.7180f, 16.1973f, + 19.0000f, 7.00000f, 17.0000f, 19.0000f, 7.00000f, 17.0000f, + 20.2213f, 12.6566f, 15.3489f, 19.4209f, 11.3746f, 16.5462f, + 19.1846f, 8.68567f, 16.8753f, 19.0000f, 7.00000f, 17.0000f, + 19.0646f, 12.0100f, 15.8399f, 18.8722f, 10.5883f, 16.9960f, + 17.6994f, 8.84256f, 17.5601f, 17.6994f, 7.00000f, 17.5601f, + 18.0000f, 12.0000f, 16.0000f, 17.8076f, 10.5782f, 17.1561f, + 17.0000f, 8.84256f, 17.7000f, 17.0000f, 7.00000f, 17.7000f, }, + { 18.0000f, 12.0000f, 16.0000f, 17.8076f, 10.5782f, 17.1561f, + 17.0000f, 8.84256f, 17.7000f, 17.0000f, 7.00000f, 17.7000f, + 16.6783f, 11.9874f, 16.1987f, 16.4859f, 10.5657f, 17.3549f, + 16.0130f, 8.84256f, 17.8973f, 16.0130f, 7.00000f, 17.8973f, + 15.3264f, 12.1643f, 16.0012f, 15.3324f, 10.7567f, 17.2083f, + 15.0055f, 8.85429f, 17.9569f, 15.0055f, 7.00000f, 17.9569f, + 14.0000f, 12.0000f, 16.0000f, 14.0060f, 10.5924f, 17.2071f, + 14.0000f, 8.85429f, 18.0000f, 14.0000f, 7.00000f, 18.0000f, }, + { 14.0000f, 12.0000f, 16.0000f, 14.0060f, 10.5924f, 17.2071f, + 14.0000f, 8.85429f, 18.0000f, 14.0000f, 7.00000f, 18.0000f, + 12.6322f, 11.8305f, 15.9987f, 12.6382f, 10.4229f, 17.2058f, + 12.6644f, 8.85429f, 18.0572f, 12.6644f, 7.00000f, 18.0572f, + 11.1786f, 11.1102f, 15.8546f, 11.3641f, 10.1538f, 17.0682f, + 11.3151f, 8.57346f, 17.6712f, 11.2968f, 7.00000f, 17.6757f, + 10.0000f, 11.0000f, 16.0000f, 10.0023f, 9.92533f, 17.3646f, + 10.0182f, 8.57346f, 17.9954f, 10.0000f, 7.00000f, 18.0000f, }, + { 10.0000f, 7.00000f, 18.0000f, 9.97797f, 5.10529f, 18.0055f, + 10.0036f, 3.39890f, 17.7763f, 10.0000f, 2.00000f, 16.0000f, + 8.54835f, 7.00000f, 18.3629f, 8.52633f, 5.10529f, 18.3684f, + 8.65977f, 3.62422f, 18.0685f, 7.86846f, 2.71708f, 16.9202f, + 7.49631f, 7.00000f, 20.0000f, 7.49631f, 5.45253f, 20.0000f, + 7.26780f, 3.46826f, 18.6874f, 6.00000f, 3.00000f, 18.0000f, + 6.00000f, 7.00000f, 20.0000f, 6.00000f, 5.45253f, 20.0000f, + 6.00000f, 3.00000f, 18.0000f, 6.00000f, 3.00000f, 18.0000f, }, + { 6.00000f, 7.00000f, 20.0000f, 6.00000f, 5.45253f, 20.0000f, + 6.00000f, 3.00000f, 18.0000f, 6.00000f, 3.00000f, 18.0000f, + 4.70898f, 7.00000f, 20.0000f, 4.70898f, 5.45253f, 20.0000f, + 5.43536f, 3.74871f, 18.6055f, 6.00000f, 3.00000f, 18.0000f, + 3.62697f, 7.00000f, 19.1285f, 3.62697f, 5.28417f, 19.1285f, + 4.35824f, 3.90451f, 17.7575f, 5.28514f, 2.90705f, 16.7135f, + 3.00000f, 7.00000f, 18.0000f, 2.99999f, 5.28417f, 18.0000f, + 4.07310f, 3.99745f, 17.0440f, 5.00000f, 3.00000f, 16.0000f, }, + { 3.00000f, 7.00000f, 18.0000f, 2.99999f, 5.28417f, 18.0000f, + 4.07310f, 3.99745f, 17.0440f, 5.00000f, 3.00000f, 16.0000f, + 1.79545f, 7.00000f, 15.8318f, 1.79545f, 5.28417f, 15.8318f, + 3.32781f, 4.24038f, 15.1790f, 4.25471f, 3.24292f, 14.1350f, + 0.794020f, 7.00000f, 13.4717f, 0.794020f, 5.60875f, + 13.4717f, 1.51717f, 3.42451f, 12.7792f, 2.88595f, 3.40717f, + 12.5308f, 1.00000f, 7.00000f, 11.0000f, 1.00000f, 5.60875f, + 11.0000f, 1.63122f, 4.01734f, 10.8484f, 3.00000f, 4.00000f, + 10.6000f, }, + { 1.00000f, 7.00000f, 11.0000f, 1.00000f, 5.60875f, 11.0000f, + 1.63122f, 4.01734f, 10.8484f, 3.00000f, 4.00000f, 10.6000f, + 1.16542f, 7.00000f, 9.01491f, 1.16542f, 5.60875f, 9.01491f, + 1.71610f, 4.45857f, 9.41132f, 3.08488f, 4.44123f, 9.16289f, + 2.70364f, 7.00000f, 7.51241f, 2.70364f, 6.27240f, 7.51241f, + 3.60899f, 5.37796f, 7.63476f, 4.12348f, 5.37796f, 8.14925f, + 4.00000f, 7.00000f, 6.00000f, 4.00000f, 6.27240f, 6.00000f, + 4.48551f, 5.80000f, 6.48551f, 5.00000f, 5.80000f, 7.00000f, }, + { 4.00000f, 7.00000f, 6.00000f, 4.00000f, 6.27240f, 6.00000f, + 4.48551f, 5.80000f, 6.48551f, 5.00000f, 5.80000f, 7.00000f, + 4.79829f, 7.00000f, 5.06865f, 4.79829f, 6.27240f, 5.06865f, + 5.03793f, 6.06598f, 5.76119f, 5.55242f, 6.06598f, 6.27568f, + 5.82508f, 7.00000f, 4.35247f, 5.82508f, 6.44606f, 4.35247f, + 6.08632f, 5.95431f, 4.69838f, 6.08632f, 5.95431f, 5.25232f, + 7.00000f, 7.00000f, 4.00000f, 7.00000f, 6.44606f, 4.00000f, + 7.00000f, 6.00000f, 4.44606f, 7.00000f, 6.00000f, 5.00000f, }, + { 7.00000f, 7.00000f, 4.00000f, 7.00000f, 6.44606f, 4.00000f, + 7.00000f, 6.00000f, 4.44606f, 7.00000f, 6.00000f, 5.00000f, + 7.69551f, 7.00000f, 3.79134f, 7.69551f, 6.44606f, 3.79134f, + 7.66874f, 6.03343f, 4.26137f, 7.66874f, 6.03343f, 4.81531f, + 9.00000f, 7.00000f, 4.50000f, 8.63511f, 6.54994f, 4.22633f, + 8.59332f, 5.79666f, 4.83390f, 9.00000f, 6.00000f, 5.50000f, + 9.00000f, 7.00000f, 4.50000f, 9.00000f, 7.00000f, 4.50000f, + 9.00000f, 6.00000f, 5.50000f, 9.00000f, 6.00000f, 5.50000f, }, + { 9.00000f, 7.00000f, 4.50000f, 9.00000f, 7.00000f, 4.50000f, + 9.00000f, 6.00000f, 5.50000f, 9.00000f, 6.00000f, 5.50000f, + 9.00000f, 7.00000f, 4.50000f, 9.43205f, 7.00000f, 4.82404f, + 9.44642f, 6.22321f, 5.33934f, 9.00000f, 6.00000f, 5.50000f, + 9.00000f, 8.00000f, 5.50000f, 9.44642f, 7.77679f, 5.33934f, + 9.09980f, 7.00000f, 5.97504f, 9.00000f, 7.00000f, 6.00000f, + 9.00000f, 8.00000f, 5.50000f, 9.00000f, 8.00000f, 5.50000f, + 9.00000f, 7.00000f, 6.00000f, 9.00000f, 7.00000f, 6.00000f, }, + { 10.0000f, 2.00000f, 8.00000f, 8.93204f, 3.06795f, 8.00000f, + 8.96952f, 4.43964f, 7.67212f, 10.0000f, 6.00000f, 8.00000f, + 9.87124f, 1.95609f, 9.38125f, 8.80329f, 3.02405f, 9.38125f, + 8.74608f, 4.46130f, 8.42383f, 9.56588f, 5.70264f, 8.68468f, + 9.34408f, 1.50592f, 9.94049f, 8.51649f, 2.09617f, 9.55423f, + 9.59858f, 3.81408f, 9.77086f, 9.59783f, 5.18668f, 9.40552f, + 9.00000f, 1.50000f, 12.0000f, 7.73438f, 2.55240f, 10.0865f, + 9.00075f, 3.62739f, 10.3653f, 9.00000f, 5.00000f, 10.0000f, }, + { 9.00000f, 1.50000f, 12.0000f, 7.73438f, 2.55240f, 10.0865f, + 9.00075f, 3.62739f, 10.3653f, 9.00000f, 5.00000f, 10.0000f, + 9.00000f, 1.50000f, 12.0000f, 7.15956f, 2.88771f, 10.4777f, + 8.48141f, 3.46521f, 10.8817f, 8.48065f, 4.83782f, 10.5164f, + 9.00000f, 1.50000f, 12.0000f, 7.15956f, 2.88771f, 10.4777f, + 8.32175f, 3.72942f, 10.9234f, 7.85973f, 4.98761f, 10.5389f, + 9.00000f, 1.50000f, 12.0000f, 7.45296f, 2.59077f, 11.3891f, + 7.91171f, 3.25991f, 11.3835f, 7.00000f, 5.00000f, 11.0000f, }, + { 9.00000f, 1.50000f, 12.0000f, 7.45296f, 2.59077f, 11.3891f, + 7.91171f, 3.25991f, 11.3835f, 7.00000f, 5.00000f, 11.0000f, + 9.07887f, 1.31725f, 13.1541f, 7.97031f, 2.06717f, 12.9963f, + 7.41411f, 2.69014f, 11.9419f, 6.75115f, 3.94166f, 11.6699f, + 9.52420f, 0.976845f, 14.7029f, 8.32713f, 0.844520f, + 14.5403f, 8.27262f, 2.45611f, 13.7536f, 7.52117f, 3.19943f, + 12.9181f, 10.0000f, 2.00000f, 16.0000f, 7.84991f, 1.51717f, + 15.3966f, 7.75144f, 2.05668f, 14.8355f, 7.00000f, 2.80000f, + 14.0000f, }, + { 10.0000f, 2.00000f, 16.0000f, 7.84991f, 1.51717f, 15.3966f, + 7.75144f, 2.05668f, 14.8355f, 7.00000f, 2.80000f, 14.0000f, + 7.86846f, 2.71708f, 16.9202f, 7.38876f, 2.16717f, 16.2241f, + 7.34956f, 1.74867f, 15.6698f, 6.59812f, 2.49199f, 14.8342f, + 6.00000f, 3.00000f, 18.0000f, 6.83143f, 1.96557f, 17.1732f, + 5.81231f, 2.33975f, 16.1197f, 5.52717f, 2.43269f, 15.4062f, + 6.00000f, 3.00000f, 18.0000f, 6.00000f, 3.00000f, 18.0000f, + 5.28514f, 2.90705f, 16.7135f, 5.00000f, 3.00000f, 16.0000f, }, + { 5.00000f, 3.00000f, 16.0000f, 5.52717f, 2.43269f, 15.4062f, + 6.59812f, 2.49199f, 14.8342f, 7.00000f, 2.80000f, 14.0000f, + 4.25471f, 3.24292f, 14.1350f, 4.78188f, 2.67562f, 13.5412f, + 5.34262f, 3.73390f, 13.4382f, 5.74450f, 4.04191f, 12.6039f, + 2.88595f, 3.40717f, 12.5308f, 4.34454f, 3.38869f, 12.2661f, + 6.08167f, 3.98531f, 10.4018f, 7.19502f, 4.53322f, 11.1711f, + 3.00000f, 4.00000f, 10.6000f, 4.45858f, 3.98151f, 10.3352f, + 4.69919f, 5.29811f, 9.44672f, 5.50000f, 7.00000f, 10.0000f, }, + { 3.00000f, 4.00000f, 10.6000f, 4.45858f, 3.98151f, 10.3352f, + 4.69919f, 5.29811f, 9.44672f, 5.50000f, 7.00000f, 10.0000f, + 3.08488f, 4.44123f, 9.16289f, 4.54347f, 4.42275f, 8.89816f, + 4.26221f, 5.71307f, 9.14480f, 4.26221f, 7.00000f, 9.14480f, + 4.12348f, 5.37796f, 8.14925f, 4.62387f, 5.37796f, 8.64963f, + 5.73017f, 6.29235f, 8.71953f, 5.73017f, 7.00000f, 8.71953f, + 5.00000f, 5.80000f, 7.00000f, 5.50038f, 5.80000f, 7.50038f, + 6.00000f, 6.29235f, 8.00000f, 6.00000f, 7.00000f, 8.00000f, }, + { 5.00000f, 5.80000f, 7.00000f, 5.50038f, 5.80000f, 7.50038f, + 6.00000f, 6.29235f, 8.00000f, 6.00000f, 7.00000f, 8.00000f, + 5.55242f, 6.06598f, 6.27568f, 6.05280f, 6.06598f, 6.77606f, + 6.26818f, 6.29235f, 7.28483f, 6.26818f, 7.00000f, 7.28483f, + 6.08632f, 5.95431f, 5.25232f, 6.08632f, 5.95431f, 5.80147f, + 6.36448f, 6.45084f, 6.42367f, 6.36448f, 7.00000f, 6.42367f, + 7.00000f, 6.00000f, 5.00000f, 7.00000f, 6.00000f, 5.54915f, + 7.00000f, 6.45084f, 6.00000f, 7.00000f, 7.00000f, 6.00000f, }, + { 7.00000f, 6.00000f, 5.00000f, 7.00000f, 6.00000f, 5.54915f, + 7.00000f, 6.45084f, 6.00000f, 7.00000f, 7.00000f, 6.00000f, + 7.66874f, 6.03343f, 4.81531f, 7.66874f, 6.03343f, 5.36447f, + 7.55880f, 6.45084f, 5.62746f, 7.55880f, 7.00000f, 5.62746f, + 9.00000f, 6.00000f, 5.50000f, 8.65645f, 5.82822f, 5.63149f, + 8.39835f, 6.57583f, 6.15041f, 9.00000f, 7.00000f, 6.00000f, + 9.00000f, 6.00000f, 5.50000f, 9.00000f, 6.00000f, 5.50000f, + 9.00000f, 7.00000f, 6.00000f, 9.00000f, 7.00000f, 6.00000f, }, + { 10.0000f, 11.0000f, 16.0000f, 10.0023f, 9.92533f, 17.3646f, + 10.0182f, 8.57346f, 17.9954f, 10.0000f, 7.00000f, 18.0000f, + 7.93998f, 10.5043f, 16.6387f, 8.61321f, 9.69225f, 17.6669f, + 8.56665f, 8.57346f, 18.3583f, 8.54835f, 7.00000f, 18.3629f, + 6.00000f, 11.0000f, 18.0000f, 7.40668f, 10.2558f, 18.7441f, + 7.49631f, 8.54969f, 20.0000f, 7.49631f, 7.00000f, 20.0000f, + 6.00000f, 11.0000f, 18.0000f, 6.00000f, 11.0000f, 18.0000f, + 6.00000f, 8.54969f, 20.0000f, 6.00000f, 7.00000f, 20.0000f, }, + { 6.00000f, 11.0000f, 18.0000f, 6.00000f, 11.0000f, 18.0000f, + 6.00000f, 8.54969f, 20.0000f, 6.00000f, 7.00000f, 20.0000f, + 6.00000f, 11.0000f, 18.0000f, 5.54861f, 10.3555f, 18.6444f, + 4.70898f, 8.54969f, 20.0000f, 4.70898f, 7.00000f, 20.0000f, + 5.28802f, 11.0938f, 16.7207f, 4.36112f, 10.0964f, 17.7647f, + 3.62697f, 8.71582f, 19.1285f, 3.62697f, 7.00000f, 19.1285f, + 5.00000f, 11.0000f, 16.0000f, 4.07310f, 10.0025f, 17.0440f, + 3.00000f, 8.71582f, 18.0000f, 3.00000f, 7.00000f, 18.0000f, }, + { 5.00000f, 11.0000f, 16.0000f, 4.07310f, 10.0025f, 17.0440f, + 3.00000f, 8.71582f, 18.0000f, 3.00000f, 7.00000f, 18.0000f, + 4.25471f, 10.7570f, 14.1350f, 3.32781f, 9.75961f, 15.1790f, + 1.79545f, 8.71582f, 15.8318f, 1.79545f, 7.00000f, 15.8318f, + 2.88595f, 10.5928f, 12.5308f, 1.51717f, 10.5754f, 12.7792f, + 0.794020f, 8.39124f, 13.4717f, 0.794020f, 7.00000f, + 13.4717f, 3.00000f, 10.0000f, 10.6000f, 1.63122f, 9.98265f, + 10.8484f, 1.00000f, 8.39124f, 11.0000f, 1.00000f, 7.00000f, + 11.0000f, }, + { 3.00000f, 10.0000f, 10.6000f, 1.63122f, 9.98265f, 10.8484f, + 1.00000f, 8.39124f, 11.0000f, 1.00000f, 7.00000f, 11.0000f, + 3.08488f, 9.55876f, 9.16289f, 1.71610f, 9.54142f, 9.41132f, + 1.16542f, 8.39124f, 9.01491f, 1.16542f, 7.00000f, 9.01491f, + 4.12348f, 8.62203f, 8.14925f, 3.60899f, 8.62203f, 7.63476f, + 2.70364f, 7.72759f, 7.51241f, 2.70364f, 7.00000f, 7.51241f, + 5.00000f, 8.20000f, 7.00000f, 4.48551f, 8.20000f, 6.48550f, + 4.00000f, 7.72759f, 6.00000f, 4.00000f, 7.00000f, 6.00000f, }, + { 5.00000f, 8.20000f, 7.00000f, 4.48551f, 8.20000f, 6.48550f, + 4.00000f, 7.72759f, 6.00000f, 4.00000f, 7.00000f, 6.00000f, + 5.55241f, 7.93401f, 6.27568f, 5.03792f, 7.93401f, 5.76119f, + 4.79829f, 7.72759f, 5.06865f, 4.79829f, 7.00000f, 5.06865f, + 6.08632f, 8.04568f, 5.25232f, 6.08632f, 8.04568f, 4.69838f, + 5.82508f, 7.55393f, 4.35247f, 5.82508f, 7.00000f, 4.35247f, + 7.00000f, 8.00000f, 5.00000f, 7.00000f, 8.00000f, 4.44606f, + 7.00000f, 7.55393f, 4.00000f, 7.00000f, 7.00000f, 4.00000f, }, + { 7.00000f, 8.00000f, 5.00000f, 7.00000f, 8.00000f, 4.44606f, + 7.00000f, 7.55393f, 4.00000f, 7.00000f, 7.00000f, 4.00000f, + 7.66874f, 7.96656f, 4.81531f, 7.66874f, 7.96656f, 4.26138f, + 7.69551f, 7.55393f, 3.79134f, 7.69551f, 7.00000f, 3.79134f, + 9.00000f, 8.00000f, 5.50000f, 8.59332f, 8.20333f, 4.83390f, + 8.63511f, 7.45005f, 4.22633f, 9.00000f, 7.00000f, 4.50000f, + 9.00000f, 8.00000f, 5.50000f, 9.00000f, 8.00000f, 5.50000f, + 9.00000f, 7.00000f, 4.50000f, 9.00000f, 7.00000f, 4.50000f, }, + { 10.0000f, 8.00000f, 8.00000f, 8.96952f, 9.56035f, 7.67212f, + 8.93204f, 10.9320f, 8.00000f, 10.0000f, 12.0000f, 8.00000f, + 9.56588f, 8.29735f, 8.68468f, 8.74608f, 9.53870f, 8.42383f, + 8.80346f, 10.9758f, 9.37941f, 9.87141f, 12.0438f, 9.37941f, + 9.59783f, 8.81331f, 9.40552f, 9.59859f, 10.1923f, 9.77256f, + 8.27272f, 11.9399f, 9.68199f, 9.15043f, 12.6614f, 10.0622f, + 9.00000f, 9.00000f, 10.0000f, 9.00075f, 10.3789f, 10.3670f, + 7.51132f, 11.2958f, 10.3210f, 9.00000f, 12.5000f, 12.0000f, }, + { 9.00000f, 9.00000f, 10.0000f, 9.00075f, 10.3789f, 10.3670f, + 7.51132f, 11.2958f, 10.3210f, 9.00000f, 12.5000f, 12.0000f, + 9.00000f, 9.00000f, 10.0000f, 8.48141f, 10.5411f, 10.8834f, + 7.07052f, 10.9228f, 10.6910f, 7.30706f, 11.1052f, 11.4309f, + 9.00000f, 9.00000f, 10.0000f, 8.48141f, 10.5411f, 10.8834f, + 8.32883f, 10.2840f, 10.9264f, 7.91955f, 10.7550f, 11.3868f, + 9.00000f, 9.00000f, 10.0000f, 8.48065f, 9.16217f, 10.5164f, + 7.86080f, 9.01240f, 10.5383f, 7.00000f, 9.00000f, 11.0000f, }, + { 7.00000f, 9.00000f, 11.0000f, 7.91955f, 10.7550f, 11.3868f, + 7.30706f, 11.1052f, 11.4309f, 9.00000f, 12.5000f, 12.0000f, + 6.74950f, 10.0653f, 11.6744f, 7.41955f, 11.3303f, 11.9493f, + 7.86345f, 11.5343f, 13.1713f, 8.96405f, 12.4448f, 13.3410f, + 7.52546f, 10.7873f, 12.9152f, 8.32087f, 11.3878f, 13.7595f, + 8.49662f, 12.3274f, 14.3212f, 9.51945f, 12.2157f, 14.4584f, + 7.00000f, 11.2000f, 14.0000f, 7.79541f, 11.8004f, 14.8442f, + 7.99336f, 11.7065f, 15.1118f, 10.0000f, 11.0000f, 16.0000f, }, + { 7.00000f, 11.2000f, 14.0000f, 7.79541f, 11.8004f, 14.8442f, + 7.99336f, 11.7065f, 15.1118f, 10.0000f, 11.0000f, 16.0000f, + 6.59576f, 11.5174f, 14.8344f, 7.39118f, 12.1179f, 15.6787f, + 7.47421f, 11.0661f, 15.9274f, 7.93998f, 10.5043f, 16.6387f, + 5.52943f, 11.5697f, 15.4036f, 5.81745f, 11.6636f, 16.1244f, + 6.95530f, 11.8030f, 17.1969f, 6.00000f, 11.0000f, 18.0000f, + 5.00000f, 11.0000f, 16.0000f, 5.28802f, 11.0938f, 16.7207f, + 6.00000f, 11.0000f, 18.0000f, 6.00000f, 11.0000f, 18.0000f, }, + { 7.00000f, 11.2000f, 14.0000f, 6.59576f, 11.5174f, 14.8344f, + 5.52943f, 11.5697f, 15.4036f, 5.00000f, 11.0000f, 16.0000f, + 5.63935f, 10.1728f, 12.5557f, 5.23511f, 10.4902f, 13.3902f, + 4.78415f, 11.3268f, 13.5386f, 4.25471f, 10.7570f, 14.1350f, + 7.18618f, 9.45391f, 11.1650f, 6.07284f, 10.0018f, 10.3957f, + 4.34454f, 10.6113f, 12.2661f, 2.88595f, 10.5928f, 12.5308f, + 5.50000f, 7.00000f, 10.0000f, 4.69955f, 8.70113f, 9.44696f, + 4.45858f, 10.0184f, 10.3352f, 3.00000f, 10.0000f, 10.6000f, }, + { 5.50000f, 7.00000f, 10.0000f, 4.69955f, 8.70113f, 9.44696f, + 4.45858f, 10.0184f, 10.3352f, 3.00000f, 10.0000f, 10.6000f, + 4.26221f, 7.00000f, 9.14480f, 4.26221f, 8.28692f, 9.14480f, + 4.54346f, 9.57725f, 8.89816f, 3.08488f, 9.55876f, 9.16289f, + 5.73017f, 7.00000f, 8.71953f, 5.73017f, 7.70764f, 8.71953f, + 4.62387f, 8.62203f, 8.64963f, 4.12348f, 8.62203f, 8.14925f, + 6.00000f, 7.00000f, 8.00000f, 6.00000f, 7.70764f, 8.00000f, + 5.50038f, 8.20000f, 7.50038f, 5.00000f, 8.20000f, 7.00000f, }, + { 6.00000f, 7.00000f, 8.00000f, 6.00000f, 7.70764f, 8.00000f, + 5.50038f, 8.20000f, 7.50038f, 5.00000f, 8.20000f, 7.00000f, + 6.26818f, 7.00000f, 7.28483f, 6.26818f, 7.70764f, 7.28483f, + 6.05280f, 7.93401f, 6.77606f, 5.55241f, 7.93401f, 6.27568f, + 6.36448f, 7.00000f, 6.42367f, 6.36448f, 7.54915f, 6.42367f, + 6.08632f, 8.04568f, 5.80147f, 6.08632f, 8.04568f, 5.25232f, + 7.00000f, 7.00000f, 6.00000f, 7.00000f, 7.54915f, 6.00000f, + 7.00000f, 8.00000f, 5.54915f, 7.00000f, 8.00000f, 5.00000f, }, + { 7.00000f, 7.00000f, 6.00000f, 7.00000f, 7.54915f, 6.00000f, + 7.00000f, 8.00000f, 5.54915f, 7.00000f, 8.00000f, 5.00000f, + 7.55880f, 7.00000f, 5.62746f, 7.55880f, 7.54915f, 5.62746f, + 7.66874f, 7.96656f, 5.36447f, 7.66874f, 7.96656f, 4.81531f, + 9.00000f, 7.00000f, 6.00000f, 8.39835f, 7.42416f, 6.15041f, + 8.65644f, 8.17177f, 5.63149f, 9.00000f, 8.00000f, 5.50000f, + 9.00000f, 7.00000f, 6.00000f, 9.00000f, 7.00000f, 6.00000f, + 9.00000f, 8.00000f, 5.50000f, 9.00000f, 8.00000f, 5.50000f, }, + { 10.0000f, 6.00000f, 8.00000f, 10.6753f, 7.02258f, 8.21487f, + 10.6753f, 6.97741f, 8.21487f, 10.0000f, 8.00000f, 8.00000f, + 9.56588f, 5.70264f, 8.68468f, 9.96312f, 6.30414f, 8.81107f, + 9.96312f, 7.69585f, 8.81107f, 9.56588f, 8.29735f, 8.68468f, + 9.59783f, 5.18668f, 9.40552f, 9.59712f, 6.48744f, 9.05930f, + 9.59712f, 7.51255f, 9.05930f, 9.59783f, 8.81331f, 9.40552f, + 9.00000f, 5.00000f, 10.0000f, 8.99928f, 6.30075f, 9.65378f, + 8.99928f, 7.69924f, 9.65378f, 9.00000f, 9.00000f, 10.0000f, }, + { 9.00000f, 5.00000f, 10.0000f, 8.99928f, 6.30075f, 9.65378f, + 8.99928f, 7.69924f, 9.65378f, 9.00000f, 9.00000f, 10.0000f, + 8.48065f, 4.83782f, 10.5164f, 8.47994f, 6.13857f, 10.1702f, + 8.47994f, 7.86142f, 10.1702f, 8.48065f, 9.16217f, 10.5164f, + 7.85973f, 4.98761f, 10.5389f, 7.39107f, 6.26389f, 10.1488f, + 7.39108f, 7.73610f, 10.1488f, 7.86080f, 9.01240f, 10.5383f, + 7.00000f, 5.00000f, 11.0000f, 6.60445f, 6.86590f, 10.2723f, + 6.60445f, 7.13409f, 10.2723f, 7.00000f, 9.00000f, 11.0000f, }, + { 5.50000f, 7.00000f, 10.0000f, 7.17220f, 4.74482f, 11.1553f, + 6.28350f, 6.33090f, 10.7170f, 7.00000f, 5.00000f, 11.0000f, + 5.50000f, 7.00000f, 10.0000f, 6.57446f, 7.00000f, 10.7423f, + 6.27560f, 7.11757f, 10.3239f, 6.60445f, 6.86590f, 10.2723f, + 5.50000f, 7.00000f, 10.0000f, 6.57446f, 7.00000f, 10.7423f, + 6.27560f, 6.88242f, 10.3238f, 6.60445f, 7.13409f, 10.2723f, + 5.50000f, 7.00000f, 10.0000f, 7.16443f, 9.24469f, 11.1499f, + 6.28292f, 7.66800f, 10.7168f, 7.00000f, 9.00000f, 11.0000f, }, + { 7.00000f, 2.80000f, 14.0000f, 7.52117f, 3.19943f, 12.9181f, + 6.75115f, 3.94166f, 11.6699f, 7.00000f, 5.00000f, 11.0000f, + 7.00000f, 2.80000f, 14.0000f, 6.26568f, 4.44134f, 11.5221f, + 6.29894f, 4.79533f, 11.4844f, 6.28350f, 6.33090f, 10.7170f, + 7.00000f, 2.80000f, 14.0000f, 6.26568f, 4.44134f, 11.5221f, + 7.22445f, 4.54771f, 11.1914f, 7.17220f, 4.74482f, 11.1553f, + 7.00000f, 2.80000f, 14.0000f, 5.74450f, 4.04191f, 12.6039f, + 7.19502f, 4.53322f, 11.1711f, 5.50000f, 7.00000f, 10.0000f, }, + { 7.00000f, 11.2000f, 14.0000f, 5.63935f, 10.1728f, 12.5557f, + 7.18618f, 9.45391f, 11.1650f, 5.50000f, 7.00000f, 10.0000f, + 7.00000f, 11.2000f, 14.0000f, 6.16481f, 9.76016f, 11.4710f, + 7.21561f, 9.43943f, 11.1853f, 7.16443f, 9.24469f, 11.1499f, + 7.00000f, 11.2000f, 14.0000f, 6.16481f, 9.76016f, 11.4710f, + 6.29730f, 9.21164f, 11.4888f, 6.28292f, 7.66800f, 10.7168f, + 7.00000f, 11.2000f, 14.0000f, 7.52546f, 10.7873f, 12.9152f, + 6.74950f, 10.0653f, 11.6744f, 7.00000f, 9.00000f, 11.0000f, }, + { 6.00000f, 4.00000f, 18.0000f, 6.17178f, 3.61615f, 18.9501f, + 7.00000f, 2.00000f, 20.0000f, 7.00000f, 2.00000f, 20.0000f, + 5.82238f, 3.78567f, 15.7951f, 5.99416f, 3.40183f, 16.7453f, + 6.36326f, 1.62520f, 19.4440f, 7.00000f, 2.00000f, 20.0000f, + 6.99191f, -0.0338653f, 16.2383f, 7.06775f, -0.0553456f, + 17.3163f, 6.99797f, -0.708357f, 18.6050f, 7.50000f, + -1.00000f, 19.0000f, 8.00000f, -2.00000f, 16.0000f, + 8.07584f, -2.02148f, 17.0779f, 7.50000f, -1.00000f, + 19.0000f, 7.50000f, -1.00000f, 19.0000f, }, + { 6.00000f, 4.00000f, 18.0000f, 5.82238f, 3.78567f, 15.7951f, + 6.99191f, -0.0338653f, 16.2383f, 8.00000f, -2.00000f, + 16.0000f, 5.77637f, 4.49967f, 16.7631f, 5.59875f, 4.28535f, + 14.5582f, 6.86100f, 0.00320839f, 14.3779f, 7.86909f, + -1.96292f, 14.1395f, 5.91173f, 4.38739f, 15.2929f, + 5.55206f, 2.71906f, 15.1499f, 6.48478f, 0.0405683f, + 12.5429f, 7.05435f, -1.47828f, 11.9935f, 6.00000f, + 4.00000f, 14.0000f, 5.64033f, 2.33167f, 13.8569f, 5.93043f, + 1.51884f, 11.5493f, 6.50000f, 0.00000f, 11.0000f, }, + { 6.00000f, 4.00000f, 14.0000f, 5.64033f, 2.33167f, 13.8569f, + 5.93043f, 1.51884f, 11.5493f, 6.50000f, 0.00000f, 11.0000f, + 6.00000f, 4.00000f, 14.0000f, 5.72105f, 1.97742f, 12.6746f, + 5.71956f, 2.08115f, 11.1714f, 6.28913f, 0.562308f, + 10.6220f, 6.00000f, 4.00000f, 14.0000f, 5.72105f, 1.97742f, + 12.6746f, 5.95300f, 2.11991f, 11.7283f, 6.50000f, 2.00000f, + 11.0000f, 6.00000f, 4.00000f, 14.0000f, 6.08071f, 3.64575f, + 12.8176f, 6.50000f, 2.00000f, 11.0000f, 6.50000f, 2.00000f, + 11.0000f, }, + { 7.00000f, 4.00000f, 18.0000f, 8.01538f, 2.03531f, 17.7856f, + 9.25405f, -1.57894f, 18.1668f, 9.00000f, -2.00000f, + 16.0000f, 7.27289f, 3.61974f, 18.9768f, 8.28828f, 1.65505f, + 18.7624f, 9.43541f, -1.59908f, 19.2725f, 9.18135f, + -2.02014f, 17.1057f, 8.50000f, 2.00000f, 20.0000f, + 8.88419f, 1.96998f, 19.2616f, 9.44404f, -0.614553f, + 18.6694f, 9.00000f, -1.00000f, 19.0000f, 8.50000f, + 2.00000f, 20.0000f, 8.50000f, 2.00000f, 20.0000f, 9.00000f, + -1.00000f, 19.0000f, 9.00000f, -1.00000f, 19.0000f, }, + { 9.00000f, -2.00000f, 16.0000f, 9.25405f, -1.57894f, 18.1668f, + 8.01538f, 2.03531f, 17.7856f, 7.00000f, 4.00000f, 18.0000f, + 8.69788f, -1.96644f, 14.1579f, 8.95194f, -1.54538f, + 16.3247f, 7.67925f, 2.50367f, 16.5825f, 6.66387f, 4.46835f, + 16.7968f, 7.84657f, -1.53663f, 12.0021f, 8.19383f, + -0.932816f, 13.5421f, 7.49810f, 2.87203f, 14.8067f, + 6.90135f, 4.38271f, 15.2742f, 7.50000f, 0.00000f, 11.0000f, + 7.84726f, 0.603817f, 12.5400f, 7.59675f, 2.48932f, + 13.5324f, 7.00000f, 4.00000f, 14.0000f, }, + { 7.50000f, 0.00000f, 11.0000f, 7.84726f, 0.603817f, 12.5400f, + 7.59675f, 2.48932f, 13.5324f, 7.00000f, 4.00000f, 14.0000f, + 7.50000f, 0.00000f, 11.0000f, 7.72215f, 1.15853f, 12.1782f, + 7.68730f, 2.13801f, 12.3627f, 7.09055f, 3.64868f, 12.8302f, + 7.50000f, 0.00000f, 11.0000f, 7.72215f, 1.15853f, 12.1782f, + 7.80364f, 2.19032f, 11.7976f, 7.50000f, 2.00000f, 11.0000f, + 7.50000f, 0.00000f, 11.0000f, 7.37488f, 0.554718f, + 10.6382f, 7.50000f, 2.00000f, 11.0000f, 7.50000f, 2.00000f, + 11.0000f, }, + { 6.00000f, 4.00000f, 18.0000f, 6.04662f, 4.05625f, 18.5787f, + 6.73343f, 4.51577f, 18.0562f, 7.00000f, 4.00000f, 18.0000f, + 6.17178f, 3.61615f, 18.9501f, 6.21840f, 3.67240f, 19.5288f, + 7.00633f, 4.13552f, 19.0331f, 7.27289f, 3.61974f, 18.9768f, + 7.00000f, 2.00000f, 20.0000f, 6.87917f, 2.69654f, 19.9349f, + 7.96063f, 3.27172f, 19.8070f, 8.50000f, 2.00000f, 20.0000f, + 7.00000f, 2.00000f, 20.0000f, 7.00000f, 2.00000f, 20.0000f, + 8.50000f, 2.00000f, 20.0000f, 8.50000f, 2.00000f, 20.0000f, }, + { 7.00000f, 2.00000f, 20.0000f, 7.00000f, 2.00000f, 20.0000f, + 8.50000f, 2.00000f, 20.0000f, 8.50000f, 2.00000f, 20.0000f, + 7.00000f, 2.00000f, 20.0000f, 7.42866f, 0.886911f, + 20.3024f, 8.71101f, 1.41080f, 20.1671f, 8.50000f, 2.00000f, + 20.0000f, 7.50000f, -1.00000f, 19.0000f, 7.50022f, + -0.462675f, 19.7526f, 8.73395f, -0.336419f, 19.9299f, + 9.00000f, -1.00000f, 19.0000f, 7.50000f, -1.00000f, + 19.0000f, 7.50000f, -1.00000f, 19.0000f, 9.00000f, + -1.00000f, 19.0000f, 9.00000f, -1.00000f, 19.0000f, }, + { 7.50000f, -1.00000f, 19.0000f, 7.50000f, -1.00000f, 19.0000f, + 9.00000f, -1.00000f, 19.0000f, 9.00000f, -1.00000f, + 19.0000f, 7.50000f, -1.00000f, 19.0000f, 7.86286f, + -1.90036f, 18.3200f, 8.92549f, -1.83744f, 18.4231f, + 9.00000f, -1.00000f, 19.0000f, 8.07584f, -2.02148f, + 17.0779f, 8.34116f, -2.53894f, 17.0151f, 9.11447f, + -2.13097f, 16.5353f, 9.18135f, -2.02014f, 17.1057f, + 8.00000f, -2.00000f, 16.0000f, 8.26531f, -2.51746f, + 15.9372f, 8.93312f, -2.11083f, 15.4296f, 9.00000f, + -2.00000f, 16.0000f, }, + { 8.00000f, -2.00000f, 16.0000f, 8.26531f, -2.51746f, 15.9372f, + 8.93312f, -2.11083f, 15.4296f, 9.00000f, -2.00000f, + 16.0000f, 7.86909f, -1.96292f, 14.1395f, 8.13441f, + -2.48038f, 14.0768f, 8.63100f, -2.07728f, 13.5875f, + 8.69788f, -1.96644f, 14.1579f, 7.05435f, -1.47828f, + 11.9935f, 7.24070f, -1.97520f, 11.8138f, 7.73145f, + -1.73680f, 11.4916f, 7.84657f, -1.53663f, 12.0021f, + 6.50000f, 0.00000f, 11.0000f, 6.68634f, -0.496919f, + 10.8202f, 7.38487f, -0.200171f, 10.4894f, 7.50000f, + 0.00000f, 11.0000f, }, + { 6.50000f, 0.00000f, 11.0000f, 6.68634f, -0.496919f, 10.8202f, + 7.38487f, -0.200171f, 10.4894f, 7.50000f, 0.00000f, + 11.0000f, 6.28913f, 0.562308f, 10.6220f, 6.47547f, + 0.0653883f, 10.4423f, 7.25976f, 0.354546f, 10.1277f, + 7.37488f, 0.554718f, 10.6382f, 6.50000f, 2.00000f, + 11.0000f, 6.58882f, 1.48812f, 10.5862f, 7.51059f, 1.62025f, + 10.6414f, 7.50000f, 2.00000f, 11.0000f, 6.50000f, 2.00000f, + 11.0000f, 6.50000f, 2.00000f, 11.0000f, 7.50000f, 2.00000f, + 11.0000f, 7.50000f, 2.00000f, 11.0000f, }, + { 6.50000f, 2.00000f, 11.0000f, 6.50000f, 2.00000f, 11.0000f, + 7.50000f, 2.00000f, 11.0000f, 7.50000f, 2.00000f, 11.0000f, + 6.50000f, 2.00000f, 11.0000f, 6.43274f, 2.93280f, 11.6403f, + 7.32203f, 3.04107f, 11.6851f, 7.50000f, 2.00000f, 11.0000f, + 6.08071f, 3.64575f, 12.8176f, 6.18741f, 4.14071f, 12.8601f, + 6.91115f, 4.10281f, 12.9708f, 7.09055f, 3.64868f, 12.8302f, + 6.00000f, 4.00000f, 14.0000f, 6.10670f, 4.49495f, 14.0424f, + 6.82060f, 4.45412f, 14.1405f, 7.00000f, 4.00000f, 14.0000f, }, + { 6.00000f, 4.00000f, 14.0000f, 6.10670f, 4.49495f, 14.0424f, + 6.82060f, 4.45412f, 14.1405f, 7.00000f, 4.00000f, 14.0000f, + 5.91173f, 4.38739f, 15.2929f, 6.01843f, 4.88234f, 15.3353f, + 6.72196f, 4.83683f, 15.4148f, 6.90135f, 4.38271f, 15.2742f, + 5.77637f, 4.49967f, 16.7631f, 5.82299f, 4.55593f, 17.3418f, + 6.39731f, 4.98413f, 16.8531f, 6.66387f, 4.46835f, 16.7968f, + 6.00000f, 4.00000f, 18.0000f, 6.04662f, 4.05625f, 18.5787f, + 6.73343f, 4.51577f, 18.0562f, 7.00000f, 4.00000f, 18.0000f, }, + { 6.00000f, 10.0000f, 18.0000f, 6.17178f, 10.3839f, 18.9501f, + 7.00000f, 12.0000f, 20.0000f, 7.00000f, 12.0000f, 20.0000f, + 5.82238f, 10.2143f, 15.7951f, 5.99416f, 10.5982f, 16.7453f, + 6.36326f, 12.3748f, 19.4440f, 7.00000f, 12.0000f, 20.0000f, + 6.99191f, 14.0339f, 16.2383f, 7.06775f, 14.0553f, 17.3163f, + 6.99797f, 14.7084f, 18.6050f, 7.50000f, 15.0000f, 19.0000f, + 8.00000f, 16.0000f, 16.0000f, 8.07584f, 16.0215f, 17.0779f, + 7.50000f, 15.0000f, 19.0000f, 7.50000f, 15.0000f, 19.0000f, }, + { 6.00000f, 10.0000f, 18.0000f, 5.82238f, 10.2143f, 15.7951f, + 6.99191f, 14.0339f, 16.2383f, 8.00000f, 16.0000f, 16.0000f, + 5.77637f, 9.50033f, 16.7631f, 5.59875f, 9.71465f, 14.5582f, + 6.86100f, 13.9968f, 14.3779f, 7.86909f, 15.9629f, 14.1395f, + 5.91173f, 9.61261f, 15.2929f, 5.55206f, 11.2809f, 15.1499f, + 6.48478f, 13.9594f, 12.5429f, 7.05435f, 15.4783f, 11.9935f, + 6.00000f, 10.0000f, 14.0000f, 5.64033f, 11.6683f, 13.8569f, + 5.93043f, 12.4812f, 11.5493f, 6.50000f, 14.0000f, 11.0000f, }, + { 6.00000f, 10.0000f, 14.0000f, 5.64033f, 11.6683f, 13.8569f, + 5.93043f, 12.4812f, 11.5493f, 6.50000f, 14.0000f, 11.0000f, + 6.00000f, 10.0000f, 14.0000f, 5.72105f, 12.0226f, 12.6746f, + 5.71956f, 11.9188f, 11.1714f, 6.28913f, 13.4377f, 10.6220f, + 6.00000f, 10.0000f, 14.0000f, 5.72105f, 12.0226f, 12.6746f, + 5.95300f, 11.8801f, 11.7283f, 6.50000f, 12.0000f, 11.0000f, + 6.00000f, 10.0000f, 14.0000f, 6.08071f, 10.3542f, 12.8176f, + 6.50000f, 12.0000f, 11.0000f, 6.50000f, 12.0000f, 11.0000f, }, + { 7.00000f, 10.0000f, 18.0000f, 8.01538f, 11.9647f, 17.7856f, + 9.25405f, 15.5789f, 18.1668f, 9.00000f, 16.0000f, 16.0000f, + 7.27289f, 10.3803f, 18.9768f, 8.28828f, 12.3449f, 18.7624f, + 9.43541f, 15.5991f, 19.2725f, 9.18135f, 16.0201f, 17.1057f, + 8.50000f, 12.0000f, 20.0000f, 8.88419f, 12.0300f, 19.2616f, + 9.44404f, 14.6146f, 18.6694f, 9.00000f, 15.0000f, 19.0000f, + 8.50000f, 12.0000f, 20.0000f, 8.50000f, 12.0000f, 20.0000f, + 9.00000f, 15.0000f, 19.0000f, 9.00000f, 15.0000f, 19.0000f, }, + { 9.00000f, 16.0000f, 16.0000f, 9.25405f, 15.5789f, 18.1668f, + 8.01538f, 11.9647f, 17.7856f, 7.00000f, 10.0000f, 18.0000f, + 8.69788f, 15.9664f, 14.1579f, 8.95194f, 15.5454f, 16.3247f, + 7.67925f, 11.4963f, 16.5825f, 6.66387f, 9.53165f, 16.7968f, + 7.84657f, 15.5366f, 12.0021f, 8.19383f, 14.9328f, 13.5421f, + 7.49810f, 11.1280f, 14.8067f, 6.90135f, 9.61729f, 15.2742f, + 7.50000f, 14.0000f, 11.0000f, 7.84726f, 13.3962f, 12.5400f, + 7.59675f, 11.5107f, 13.5324f, 7.00000f, 10.0000f, 14.0000f, }, + { 7.50000f, 14.0000f, 11.0000f, 7.84726f, 13.3962f, 12.5400f, + 7.59675f, 11.5107f, 13.5324f, 7.00000f, 10.0000f, 14.0000f, + 7.50000f, 14.0000f, 11.0000f, 7.72215f, 12.8415f, 12.1782f, + 7.68730f, 11.8620f, 12.3627f, 7.09055f, 10.3513f, 12.8302f, + 7.50000f, 14.0000f, 11.0000f, 7.72215f, 12.8415f, 12.1782f, + 7.80364f, 11.8097f, 11.7976f, 7.50000f, 12.0000f, 11.0000f, + 7.50000f, 14.0000f, 11.0000f, 7.37488f, 13.4453f, 10.6382f, + 7.50000f, 12.0000f, 11.0000f, 7.50000f, 12.0000f, 11.0000f, }, + { 6.00000f, 10.0000f, 18.0000f, 6.04662f, 9.94375f, 18.5787f, + 6.73343f, 9.48423f, 18.0562f, 7.00000f, 10.0000f, 18.0000f, + 6.17178f, 10.3839f, 18.9501f, 6.21840f, 10.3276f, 19.5288f, + 7.00633f, 9.86448f, 19.0331f, 7.27289f, 10.3803f, 18.9768f, + 7.00000f, 12.0000f, 20.0000f, 6.87917f, 11.3035f, 19.9349f, + 7.96063f, 10.7283f, 19.8070f, 8.50000f, 12.0000f, 20.0000f, + 7.00000f, 12.0000f, 20.0000f, 7.00000f, 12.0000f, 20.0000f, + 8.50000f, 12.0000f, 20.0000f, 8.50000f, 12.0000f, 20.0000f, }, + { 7.00000f, 12.0000f, 20.0000f, 7.00000f, 12.0000f, 20.0000f, + 8.50000f, 12.0000f, 20.0000f, 8.50000f, 12.0000f, 20.0000f, + 7.00000f, 12.0000f, 20.0000f, 7.42866f, 13.1131f, 20.3024f, + 8.71101f, 12.5892f, 20.1671f, 8.50000f, 12.0000f, 20.0000f, + 7.50000f, 15.0000f, 19.0000f, 7.50022f, 14.4627f, 19.7526f, + 8.73395f, 14.3364f, 19.9299f, 9.00000f, 15.0000f, 19.0000f, + 7.50000f, 15.0000f, 19.0000f, 7.50000f, 15.0000f, 19.0000f, + 9.00000f, 15.0000f, 19.0000f, 9.00000f, 15.0000f, 19.0000f, }, + { 7.50000f, 15.0000f, 19.0000f, 7.50000f, 15.0000f, 19.0000f, + 9.00000f, 15.0000f, 19.0000f, 9.00000f, 15.0000f, 19.0000f, + 7.50000f, 15.0000f, 19.0000f, 7.86286f, 15.9004f, 18.3200f, + 8.92549f, 15.8374f, 18.4231f, 9.00000f, 15.0000f, 19.0000f, + 8.07584f, 16.0215f, 17.0779f, 8.34116f, 16.5389f, 17.0151f, + 9.11447f, 16.1310f, 16.5353f, 9.18135f, 16.0201f, 17.1057f, + 8.00000f, 16.0000f, 16.0000f, 8.26531f, 16.5175f, 15.9372f, + 8.93312f, 16.1108f, 15.4296f, 9.00000f, 16.0000f, 16.0000f, }, + { 8.00000f, 16.0000f, 16.0000f, 8.26531f, 16.5175f, 15.9372f, + 8.93312f, 16.1108f, 15.4296f, 9.00000f, 16.0000f, 16.0000f, + 7.86909f, 15.9629f, 14.1395f, 8.13441f, 16.4804f, 14.0768f, + 8.63100f, 16.0773f, 13.5875f, 8.69788f, 15.9664f, 14.1579f, + 7.05435f, 15.4783f, 11.9935f, 7.24070f, 15.9752f, 11.8138f, + 7.73145f, 15.7368f, 11.4916f, 7.84657f, 15.5366f, 12.0021f, + 6.50000f, 14.0000f, 11.0000f, 6.68634f, 14.4969f, 10.8202f, + 7.38487f, 14.2002f, 10.4894f, 7.50000f, 14.0000f, 11.0000f, }, + { 6.50000f, 14.0000f, 11.0000f, 6.68634f, 14.4969f, 10.8202f, + 7.38487f, 14.2002f, 10.4894f, 7.50000f, 14.0000f, 11.0000f, + 6.28913f, 13.4377f, 10.6220f, 6.47547f, 13.9346f, 10.4423f, + 7.25976f, 13.6455f, 10.1277f, 7.37488f, 13.4453f, 10.6382f, + 6.50000f, 12.0000f, 11.0000f, 6.58882f, 12.5119f, 10.5862f, + 7.51059f, 12.3798f, 10.6414f, 7.50000f, 12.0000f, 11.0000f, + 6.50000f, 12.0000f, 11.0000f, 6.50000f, 12.0000f, 11.0000f, + 7.50000f, 12.0000f, 11.0000f, 7.50000f, 12.0000f, 11.0000f, }, + { 6.50000f, 12.0000f, 11.0000f, 6.50000f, 12.0000f, 11.0000f, + 7.50000f, 12.0000f, 11.0000f, 7.50000f, 12.0000f, 11.0000f, + 6.50000f, 12.0000f, 11.0000f, 6.43274f, 11.0672f, 11.6403f, + 7.32203f, 10.9589f, 11.6851f, 7.50000f, 12.0000f, 11.0000f, + 6.08071f, 10.3542f, 12.8176f, 6.18741f, 9.85929f, 12.8601f, + 6.91115f, 9.89719f, 12.9708f, 7.09055f, 10.3513f, 12.8302f, + 6.00000f, 10.0000f, 14.0000f, 6.10670f, 9.50505f, 14.0424f, + 6.82060f, 9.54588f, 14.1405f, 7.00000f, 10.0000f, 14.0000f, }, + { 6.00000f, 10.0000f, 14.0000f, 6.10670f, 9.50505f, 14.0424f, + 6.82060f, 9.54588f, 14.1405f, 7.00000f, 10.0000f, 14.0000f, + 5.91173f, 9.61261f, 15.2929f, 6.01843f, 9.11766f, 15.3353f, + 6.72196f, 9.16317f, 15.4148f, 6.90135f, 9.61729f, 15.2742f, + 5.77637f, 9.50033f, 16.7631f, 5.82299f, 9.44407f, 17.3418f, + 6.39731f, 9.01587f, 16.8531f, 6.66387f, 9.53165f, 16.7968f, + 6.00000f, 10.0000f, 18.0000f, 6.04662f, 9.94375f, 18.5787f, + 6.73343f, 9.48423f, 18.0562f, 7.00000f, 10.0000f, 18.0000f, }, + { 6.82446f, 3.34549f, 10.8660f, 6.82446f, 3.34549f, 10.8660f, + 5.66811f, 2.92579f, 9.10044f, 4.82907f, 2.69715f, 8.50000f, + 6.83869f, 3.07901f, 11.1571f, 6.17186f, 2.86235f, 10.3664f, + 5.62209f, 2.62022f, 9.33980f, 4.78305f, 2.39159f, 8.73936f, + 6.49405f, 2.96703f, 11.4410f, 5.82862f, 2.75082f, 10.6519f, + 5.35977f, 2.53062f, 9.79425f, 4.57038f, 2.32249f, 9.12667f, + 6.34893f, 3.19098f, 11.7321f, 6.34893f, 3.19098f, 11.7321f, + 5.14293f, 2.75078f, 10.0336f, 4.35354f, 2.54264f, 9.36603f, }, + { 6.34893f, 3.19098f, 11.7321f, 6.34893f, 3.19098f, 11.7321f, + 5.14293f, 2.75078f, 10.0336f, 4.35354f, 2.54264f, 9.36603f, + 6.07309f, 3.39608f, 11.9085f, 5.40766f, 3.17987f, 11.1194f, + 4.92723f, 2.96978f, 10.2717f, 4.13785f, 2.76165f, 9.60412f, + 5.93731f, 3.81398f, 11.9085f, 5.27188f, 3.59776f, 11.1194f, + 4.75971f, 3.48534f, 10.2717f, 3.99875f, 3.18974f, 9.60412f, + 6.03992f, 4.14204f, 11.7321f, 6.03992f, 4.14204f, 11.7321f, + 4.80549f, 3.78930f, 10.0336f, 4.04453f, 3.49370f, 9.36603f, }, + { 6.03992f, 4.14204f, 11.7321f, 6.03992f, 4.14204f, 11.7321f, + 4.80549f, 3.78930f, 10.0336f, 4.04453f, 3.49370f, 9.36603f, + 6.02569f, 4.40851f, 11.4410f, 5.36025f, 4.19230f, 10.6519f, + 4.85151f, 4.09486f, 9.79425f, 4.09055f, 3.79926f, 9.12667f, + 6.37033f, 4.52049f, 11.1571f, 5.70350f, 4.30383f, 10.3664f, + 5.11641f, 4.17656f, 9.33980f, 4.30322f, 3.86836f, 8.73936f, + 6.51544f, 4.29655f, 10.8660f, 6.51544f, 4.29655f, 10.8660f, + 5.33324f, 3.95640f, 9.10044f, 4.52006f, 3.64821f, 8.50000f, }, + { 6.51544f, 4.29655f, 10.8660f, 6.51544f, 4.29655f, 10.8660f, + 5.33324f, 3.95640f, 9.10044f, 4.52006f, 3.64821f, 8.50000f, + 6.79128f, 4.09145f, 10.6896f, 6.12445f, 3.87478f, 9.89891f, + 5.54985f, 3.73649f, 8.86133f, 4.73666f, 3.42829f, 8.26090f, + 6.92707f, 3.67355f, 10.6896f, 6.26023f, 3.45689f, 9.89891f, + 5.71408f, 3.23103f, 8.86133f, 4.87505f, 3.00239f, 8.26090f, + 6.82446f, 3.34549f, 10.8660f, 6.82446f, 3.34549f, 10.8660f, + 5.66811f, 2.92579f, 9.10044f, 4.82907f, 2.69715f, 8.50000f, }, + { 4.82907f, 2.69715f, 8.50000f, 4.01638f, 2.47569f, 7.91842f, + 2.04314f, 2.05481f, 7.43301f, 2.04314f, 2.05481f, 7.43301f, + 4.78305f, 2.39159f, 8.73936f, 3.97036f, 2.17013f, 8.15778f, + 2.94601f, 2.05055f, 7.80752f, 2.04314f, 2.05481f, 7.43301f, + 4.57038f, 2.32249f, 9.12667f, 3.81921f, 2.12442f, 8.49139f, + 2.70969f, 1.96031f, 8.29323f, 1.80537f, 1.97756f, 7.86603f, + 4.35354f, 2.54264f, 9.36603f, 3.60237f, 2.34458f, 8.73075f, + 1.80537f, 1.97756f, 7.86603f, 1.80537f, 1.97756f, 7.86603f, }, + { 4.35354f, 2.54264f, 9.36603f, 3.60237f, 2.34458f, 8.73075f, + 1.80537f, 1.97756f, 7.86603f, 1.80537f, 1.97756f, 7.86603f, + 4.13785f, 2.76165f, 9.60412f, 3.38667f, 2.56358f, 8.96885f, + 2.49788f, 2.17525f, 8.52685f, 1.80537f, 1.97756f, 7.86603f, + 3.99875f, 3.18974f, 9.60412f, 3.27462f, 2.90844f, 8.96886f, + 2.32732f, 2.70019f, 8.52685f, 1.65086f, 2.45308f, 7.86603f, + 4.04453f, 3.49370f, 9.36603f, 3.32040f, 3.21241f, 8.73076f, + 1.65086f, 2.45308f, 7.86603f, 1.65086f, 2.45308f, 7.86603f, }, + { 4.04453f, 3.49370f, 9.36603f, 3.32040f, 3.21241f, 8.73076f, + 1.65086f, 2.45308f, 7.86603f, 1.65086f, 2.45308f, 7.86603f, + 4.09055f, 3.79926f, 9.12667f, 3.36642f, 3.51797f, 8.49140f, + 2.37234f, 2.99857f, 8.29323f, 1.65086f, 2.45308f, 7.86603f, + 4.30322f, 3.86836f, 8.73936f, 3.51557f, 3.56984f, 8.15778f, + 2.61656f, 3.06448f, 7.80752f, 1.88863f, 2.53034f, 7.43301f, + 4.52006f, 3.64821f, 8.50000f, 3.73241f, 3.34968f, 7.91842f, + 1.88863f, 2.53034f, 7.43301f, 1.88863f, 2.53034f, 7.43301f, }, + { 4.52006f, 3.64821f, 8.50000f, 3.73241f, 3.34968f, 7.91842f, + 1.88863f, 2.53034f, 7.43301f, 1.88863f, 2.53034f, 7.43301f, + 4.73666f, 3.42829f, 8.26090f, 3.94901f, 3.12976f, 7.67932f, + 2.82260f, 2.85571f, 7.58047f, 1.88863f, 2.53034f, 7.43301f, + 4.87505f, 3.00239f, 8.26090f, 4.06236f, 2.78093f, 7.67932f, + 2.98998f, 2.34056f, 7.58047f, 2.04314f, 2.05481f, 7.43301f, + 4.82907f, 2.69715f, 8.50000f, 4.01638f, 2.47569f, 7.91842f, + 2.04314f, 2.05481f, 7.43301f, 2.04314f, 2.05481f, 7.43301f, }, + { 1.80537f, 1.97756f, 7.86603f, 1.80537f, 1.97756f, 7.86603f, + 2.04314f, 2.05481f, 7.43301f, 2.04314f, 2.05481f, 7.43301f, + 1.80537f, 1.97756f, 7.86603f, 1.75245f, 1.96950f, 7.82668f, + 1.94375f, 2.04371f, 7.40157f, 2.04314f, 2.05481f, 7.43301f, + 1.65086f, 2.45308f, 7.86603f, 1.60331f, 2.42850f, 7.82668f, + 1.80169f, 2.48090f, 7.40157f, 1.88863f, 2.53034f, 7.43301f, + 1.65086f, 2.45308f, 7.86603f, 1.65086f, 2.45308f, 7.86603f, + 1.88863f, 2.53034f, 7.43301f, 1.88863f, 2.53034f, 7.43301f, }, + { 6.82446f, 9.15451f, 10.8660f, 6.82446f, 9.15451f, 10.8660f, + 5.64226f, 9.49465f, 9.10044f, 4.82907f, 9.80285f, 8.50000f, + 6.67934f, 8.93056f, 11.1571f, 6.01251f, 9.14723f, 10.3664f, + 5.42542f, 9.27449f, 9.33980f, 4.61223f, 9.58269f, 8.73936f, + 6.33470f, 9.04254f, 11.4410f, 5.66927f, 9.25875f, 10.6519f, + 5.16053f, 9.35619f, 9.79425f, 4.39957f, 9.65179f, 9.12667f, + 6.34893f, 9.30902f, 11.7321f, 6.34893f, 9.30902f, 11.7321f, + 5.11451f, 9.66176f, 10.0336f, 4.35354f, 9.95736f, 9.36603f, }, + { 6.34893f, 9.30902f, 11.7321f, 6.34893f, 9.30902f, 11.7321f, + 5.11451f, 9.66176f, 10.0336f, 4.35354f, 9.95736f, 9.36603f, + 6.24633f, 9.63708f, 11.9085f, 5.58090f, 9.85329f, 11.1194f, + 5.06873f, 9.96572f, 10.2717f, 4.30777f, 10.2613f, 9.60412f, + 6.38211f, 10.0550f, 11.9085f, 5.71668f, 10.2712f, 11.1194f, + 5.23624f, 10.4813f, 10.2717f, 4.44686f, 10.6894f, 9.60412f, + 6.65795f, 10.2601f, 11.7321f, 6.65795f, 10.2601f, 11.7321f, + 5.45194f, 10.7003f, 10.0336f, 4.66256f, 10.9084f, 9.36603f, }, + { 6.65795f, 10.2601f, 11.7321f, 6.65795f, 10.2601f, 11.7321f, + 5.45194f, 10.7003f, 10.0336f, 4.66256f, 10.9084f, 9.36603f, + 6.80307f, 10.4840f, 11.4410f, 6.13764f, 10.7002f, 10.6519f, + 5.66878f, 10.9204f, 9.79425f, 4.87940f, 11.1286f, 9.12667f, + 7.14771f, 10.3720f, 11.1571f, 6.48088f, 10.5887f, 10.3664f, + 5.93110f, 10.8308f, 9.33980f, 5.09207f, 11.0595f, 8.73936f, + 7.13348f, 10.1056f, 10.8660f, 7.13348f, 10.1056f, 10.8660f, + 5.97713f, 10.5253f, 9.10044f, 5.13809f, 10.7539f, 8.50000f, }, + { 7.13348f, 10.1056f, 10.8660f, 7.13348f, 10.1056f, 10.8660f, + 5.97713f, 10.5253f, 9.10044f, 5.13809f, 10.7539f, 8.50000f, + 7.23608f, 9.77750f, 10.6896f, 6.56925f, 9.99417f, 9.89891f, + 6.02310f, 10.2200f, 8.86133f, 5.18406f, 10.4487f, 8.26090f, + 7.10030f, 9.35961f, 10.6896f, 6.43347f, 9.57627f, 9.89891f, + 5.85887f, 9.71457f, 8.86133f, 5.04568f, 10.0228f, 8.26090f, + 6.82446f, 9.15451f, 10.8660f, 6.82446f, 9.15451f, 10.8660f, + 5.64226f, 9.49465f, 9.10044f, 4.82907f, 9.80285f, 8.50000f, }, + { 4.82907f, 9.80285f, 8.50000f, 4.04142f, 10.1014f, 7.91842f, + 2.19765f, 10.9207f, 7.43301f, 2.19765f, 10.9207f, 7.43301f, + 4.61223f, 9.58269f, 8.73936f, 3.82458f, 9.88122f, 8.15778f, + 2.92558f, 10.3866f, 7.80752f, 2.19765f, 10.9207f, 7.43301f, + 4.39957f, 9.65179f, 9.12667f, 3.67543f, 9.93309f, 8.49139f, + 2.68136f, 10.4525f, 8.29323f, 1.95988f, 10.9980f, 7.86603f, + 4.35354f, 9.95736f, 9.36603f, 3.62941f, 10.2387f, 8.73075f, + 1.95988f, 10.9980f, 7.86603f, 1.95988f, 10.9980f, 7.86603f, }, + { 4.35354f, 9.95736f, 9.36603f, 3.62941f, 10.2387f, 8.73075f, + 1.95988f, 10.9980f, 7.86603f, 1.95988f, 10.9980f, 7.86603f, + 4.30777f, 10.2613f, 9.60412f, 3.58363f, 10.5426f, 8.96885f, + 2.63633f, 10.7509f, 8.52685f, 1.95988f, 10.9980f, 7.86603f, + 4.44686f, 10.6894f, 9.60412f, 3.69569f, 10.8875f, 8.96886f, + 2.80690f, 11.2758f, 8.52685f, 2.11439f, 11.4735f, 7.86603f, + 4.66256f, 10.9084f, 9.36603f, 3.91139f, 11.1065f, 8.73076f, + 2.11439f, 11.4735f, 7.86603f, 2.11439f, 11.4735f, 7.86603f, }, + { 4.66256f, 10.9084f, 9.36603f, 3.91139f, 11.1065f, 8.73076f, + 2.11439f, 11.4735f, 7.86603f, 2.11439f, 11.4735f, 7.86603f, + 4.87940f, 11.1286f, 9.12667f, 4.12823f, 11.3266f, 8.49140f, + 3.01871f, 11.4907f, 8.29323f, 2.11439f, 11.4735f, 7.86603f, + 5.09207f, 11.0595f, 8.73936f, 4.27938f, 11.2809f, 8.15778f, + 3.25502f, 11.4005f, 7.80752f, 2.35215f, 11.3962f, 7.43301f, + 5.13809f, 10.7539f, 8.50000f, 4.32540f, 10.9754f, 7.91842f, + 2.35215f, 11.3962f, 7.43301f, 2.35215f, 11.3962f, 7.43301f, }, + { 5.13809f, 10.7539f, 8.50000f, 4.32540f, 10.9754f, 7.91842f, + 2.35215f, 11.3962f, 7.43301f, 2.35215f, 11.3962f, 7.43301f, + 5.18406f, 10.4487f, 8.26090f, 4.37137f, 10.6701f, 7.67932f, + 3.29900f, 11.1105f, 7.58047f, 2.35215f, 11.3962f, 7.43301f, + 5.04568f, 10.0228f, 8.26090f, 4.25803f, 10.3213f, 7.67932f, + 3.13162f, 10.5954f, 7.58047f, 2.19765f, 10.9207f, 7.43301f, + 4.82907f, 9.80285f, 8.50000f, 4.04142f, 10.1014f, 7.91842f, + 2.19765f, 10.9207f, 7.43301f, 2.19765f, 10.9207f, 7.43301f, }, + { 1.95988f, 10.9980f, 7.86603f, 1.95988f, 10.9980f, 7.86603f, + 2.19765f, 10.9207f, 7.43301f, 2.19765f, 10.9207f, 7.43301f, + 1.95988f, 10.9980f, 7.86603f, 1.91233f, 11.0226f, 7.82668f, + 2.11071f, 10.9702f, 7.40157f, 2.19765f, 10.9207f, 7.43301f, + 2.11439f, 11.4735f, 7.86603f, 2.06147f, 11.4816f, 7.82668f, + 2.25276f, 11.4074f, 7.40157f, 2.35215f, 11.3962f, 7.43301f, + 2.11439f, 11.4735f, 7.86603f, 2.11439f, 11.4735f, 7.86603f, + 2.35215f, 11.3962f, 7.43301f, 2.35215f, 11.3962f, 7.43301f, }, }; + + public Gumbo() { + super(PATCHES); + } +} diff --git a/src/main/java/org/sunflow/core/tesselatable/Teapot.java b/src/main/java/org/sunflow/core/tesselatable/Teapot.java new file mode 100644 index 0000000..9dd5739 --- /dev/null +++ b/src/main/java/org/sunflow/core/tesselatable/Teapot.java @@ -0,0 +1,236 @@ +package org.sunflow.core.tesselatable; + +public class Teapot extends BezierMesh { + // teapot data, from: http://www.cs.ucsb.edu/~cs280/winter2004/hw2/ + private static final float[][] PATCHES = { + { -80.00f, 0.00f, 30.00f, -80.00f, -44.80f, 30.00f, -44.80f, + -80.00f, 30.00f, 0.00f, -80.00f, 30.00f, -80.00f, 0.00f, + 12.00f, -80.00f, -44.80f, 12.00f, -44.80f, -80.00f, 12.00f, + 0.00f, -80.00f, 12.00f, -60.00f, 0.00f, 3.00f, -60.00f, + -33.60f, 3.00f, -33.60f, -60.00f, 3.00f, 0.00f, -60.00f, + 3.00f, -60.00f, 0.00f, 0.00f, -60.00f, -33.60f, 0.00f, + -33.60f, -60.00f, 0.00f, 0.00f, -60.00f, 0.00f, }, + { 0.00f, -80.00f, 30.00f, 44.80f, -80.00f, 30.00f, 80.00f, -44.80f, + 30.00f, 80.00f, 0.00f, 30.00f, 0.00f, -80.00f, 12.00f, + 44.80f, -80.00f, 12.00f, 80.00f, -44.80f, 12.00f, 80.00f, + 0.00f, 12.00f, 0.00f, -60.00f, 3.00f, 33.60f, -60.00f, + 3.00f, 60.00f, -33.60f, 3.00f, 60.00f, 0.00f, 3.00f, 0.00f, + -60.00f, 0.00f, 33.60f, -60.00f, 0.00f, 60.00f, -33.60f, + 0.00f, 60.00f, 0.00f, 0.00f, }, + { -60.00f, 0.00f, 90.00f, -60.00f, -33.60f, 90.00f, -33.60f, + -60.00f, 90.00f, 0.00f, -60.00f, 90.00f, -70.00f, 0.00f, + 69.00f, -70.00f, -39.20f, 69.00f, -39.20f, -70.00f, 69.00f, + 0.00f, -70.00f, 69.00f, -80.00f, 0.00f, 48.00f, -80.00f, + -44.80f, 48.00f, -44.80f, -80.00f, 48.00f, 0.00f, -80.00f, + 48.00f, -80.00f, 0.00f, 30.00f, -80.00f, -44.80f, 30.00f, + -44.80f, -80.00f, 30.00f, 0.00f, -80.00f, 30.00f, }, + { 0.00f, -60.00f, 90.00f, 33.60f, -60.00f, 90.00f, 60.00f, -33.60f, + 90.00f, 60.00f, 0.00f, 90.00f, 0.00f, -70.00f, 69.00f, + 39.20f, -70.00f, 69.00f, 70.00f, -39.20f, 69.00f, 70.00f, + 0.00f, 69.00f, 0.00f, -80.00f, 48.00f, 44.80f, -80.00f, + 48.00f, 80.00f, -44.80f, 48.00f, 80.00f, 0.00f, 48.00f, + 0.00f, -80.00f, 30.00f, 44.80f, -80.00f, 30.00f, 80.00f, + -44.80f, 30.00f, 80.00f, 0.00f, 30.00f, }, + { -56.00f, 0.00f, 90.00f, -56.00f, -31.36f, 90.00f, -31.36f, + -56.00f, 90.00f, 0.00f, -56.00f, 90.00f, -53.50f, 0.00f, + 95.25f, -53.50f, -29.96f, 95.25f, -29.96f, -53.50f, 95.25f, + 0.00f, -53.50f, 95.25f, -57.50f, 0.00f, 95.25f, -57.50f, + -32.20f, 95.25f, -32.20f, -57.50f, 95.25f, 0.00f, -57.50f, + 95.25f, -60.00f, 0.00f, 90.00f, -60.00f, -33.60f, 90.00f, + -33.60f, -60.00f, 90.00f, 0.00f, -60.00f, 90.00f, }, + { 0.00f, -56.00f, 90.00f, 31.36f, -56.00f, 90.00f, 56.00f, -31.36f, + 90.00f, 56.00f, 0.00f, 90.00f, 0.00f, -53.50f, 95.25f, + 29.96f, -53.50f, 95.25f, 53.50f, -29.96f, 95.25f, 53.50f, + 0.00f, 95.25f, 0.00f, -57.50f, 95.25f, 32.20f, -57.50f, + 95.25f, 57.50f, -32.20f, 95.25f, 57.50f, 0.00f, 95.25f, + 0.00f, -60.00f, 90.00f, 33.60f, -60.00f, 90.00f, 60.00f, + -33.60f, 90.00f, 60.00f, 0.00f, 90.00f, }, + { 80.00f, 0.00f, 30.00f, 80.00f, 44.80f, 30.00f, 44.80f, 80.00f, + 30.00f, 0.00f, 80.00f, 30.00f, 80.00f, 0.00f, 12.00f, + 80.00f, 44.80f, 12.00f, 44.80f, 80.00f, 12.00f, 0.00f, + 80.00f, 12.00f, 60.00f, 0.00f, 3.00f, 60.00f, 33.60f, + 3.00f, 33.60f, 60.00f, 3.00f, 0.00f, 60.00f, 3.00f, 60.00f, + 0.00f, 0.00f, 60.00f, 33.60f, 0.00f, 33.60f, 60.00f, 0.00f, + 0.00f, 60.00f, 0.00f, }, + { 0.00f, 80.00f, 30.00f, -44.80f, 80.00f, 30.00f, -80.00f, 44.80f, + 30.00f, -80.00f, 0.00f, 30.00f, 0.00f, 80.00f, 12.00f, + -44.80f, 80.00f, 12.00f, -80.00f, 44.80f, 12.00f, -80.00f, + 0.00f, 12.00f, 0.00f, 60.00f, 3.00f, -33.60f, 60.00f, + 3.00f, -60.00f, 33.60f, 3.00f, -60.00f, 0.00f, 3.00f, + 0.00f, 60.00f, 0.00f, -33.60f, 60.00f, 0.00f, -60.00f, + 33.60f, 0.00f, -60.00f, 0.00f, 0.00f, }, + { 60.00f, 0.00f, 90.00f, 60.00f, 33.60f, 90.00f, 33.60f, 60.00f, + 90.00f, 0.00f, 60.00f, 90.00f, 70.00f, 0.00f, 69.00f, + 70.00f, 39.20f, 69.00f, 39.20f, 70.00f, 69.00f, 0.00f, + 70.00f, 69.00f, 80.00f, 0.00f, 48.00f, 80.00f, 44.80f, + 48.00f, 44.80f, 80.00f, 48.00f, 0.00f, 80.00f, 48.00f, + 80.00f, 0.00f, 30.00f, 80.00f, 44.80f, 30.00f, 44.80f, + 80.00f, 30.00f, 0.00f, 80.00f, 30.00f, }, + { 0.00f, 60.00f, 90.00f, -33.60f, 60.00f, 90.00f, -60.00f, 33.60f, + 90.00f, -60.00f, 0.00f, 90.00f, 0.00f, 70.00f, 69.00f, + -39.20f, 70.00f, 69.00f, -70.00f, 39.20f, 69.00f, -70.00f, + 0.00f, 69.00f, 0.00f, 80.00f, 48.00f, -44.80f, 80.00f, + 48.00f, -80.00f, 44.80f, 48.00f, -80.00f, 0.00f, 48.00f, + 0.00f, 80.00f, 30.00f, -44.80f, 80.00f, 30.00f, -80.00f, + 44.80f, 30.00f, -80.00f, 0.00f, 30.00f, }, + { 56.00f, 0.00f, 90.00f, 56.00f, 31.36f, 90.00f, 31.36f, 56.00f, + 90.00f, 0.00f, 56.00f, 90.00f, 53.50f, 0.00f, 95.25f, + 53.50f, 29.96f, 95.25f, 29.96f, 53.50f, 95.25f, 0.00f, + 53.50f, 95.25f, 57.50f, 0.00f, 95.25f, 57.50f, 32.20f, + 95.25f, 32.20f, 57.50f, 95.25f, 0.00f, 57.50f, 95.25f, + 60.00f, 0.00f, 90.00f, 60.00f, 33.60f, 90.00f, 33.60f, + 60.00f, 90.00f, 0.00f, 60.00f, 90.00f, }, + { 0.00f, 56.00f, 90.00f, -31.36f, 56.00f, 90.00f, -56.00f, 31.36f, + 90.00f, -56.00f, 0.00f, 90.00f, 0.00f, 53.50f, 95.25f, + -29.96f, 53.50f, 95.25f, -53.50f, 29.96f, 95.25f, -53.50f, + 0.00f, 95.25f, 0.00f, 57.50f, 95.25f, -32.20f, 57.50f, + 95.25f, -57.50f, 32.20f, 95.25f, -57.50f, 0.00f, 95.25f, + 0.00f, 60.00f, 90.00f, -33.60f, 60.00f, 90.00f, -60.00f, + 33.60f, 90.00f, -60.00f, 0.00f, 90.00f, }, + { -64.00f, 0.00f, 75.00f, -64.00f, 12.00f, 75.00f, -60.00f, 12.00f, + 84.00f, -60.00f, 0.00f, 84.00f, -92.00f, 0.00f, 75.00f, + -92.00f, 12.00f, 75.00f, -100.00f, 12.00f, 84.00f, + -100.00f, 0.00f, 84.00f, -108.00f, 0.00f, 75.00f, -108.00f, + 12.00f, 75.00f, -120.00f, 12.00f, 84.00f, -120.00f, 0.00f, + 84.00f, -108.00f, 0.00f, 66.00f, -108.00f, 12.00f, 66.00f, + -120.00f, 12.00f, 66.00f, -120.00f, 0.00f, 66.00f, }, + { -60.00f, 0.00f, 84.00f, -60.00f, -12.00f, 84.00f, -64.00f, + -12.00f, 75.00f, -64.00f, 0.00f, 75.00f, -100.00f, 0.00f, + 84.00f, -100.00f, -12.00f, 84.00f, -92.00f, -12.00f, + 75.00f, -92.00f, 0.00f, 75.00f, -120.00f, 0.00f, 84.00f, + -120.00f, -12.00f, 84.00f, -108.00f, -12.00f, 75.00f, + -108.00f, 0.00f, 75.00f, -120.00f, 0.00f, 66.00f, -120.00f, + -12.00f, 66.00f, -108.00f, -12.00f, 66.00f, -108.00f, + 0.00f, 66.00f, }, + { -108.00f, 0.00f, 66.00f, -108.00f, 12.00f, 66.00f, -120.00f, + 12.00f, 66.00f, -120.00f, 0.00f, 66.00f, -108.00f, 0.00f, + 57.00f, -108.00f, 12.00f, 57.00f, -120.00f, 12.00f, 48.00f, + -120.00f, 0.00f, 48.00f, -100.00f, 0.00f, 39.00f, -100.00f, + 12.00f, 39.00f, -106.00f, 12.00f, 31.50f, -106.00f, 0.00f, + 31.50f, -80.00f, 0.00f, 30.00f, -80.00f, 12.00f, 30.00f, + -76.00f, 12.00f, 18.00f, -76.00f, 0.00f, 18.00f, }, + { -120.00f, 0.00f, 66.00f, -120.00f, -12.00f, 66.00f, -108.00f, + -12.00f, 66.00f, -108.00f, 0.00f, 66.00f, -120.00f, 0.00f, + 48.00f, -120.00f, -12.00f, 48.00f, -108.00f, -12.00f, + 57.00f, -108.00f, 0.00f, 57.00f, -106.00f, 0.00f, 31.50f, + -106.00f, -12.00f, 31.50f, -100.00f, -12.00f, 39.00f, + -100.00f, 0.00f, 39.00f, -76.00f, 0.00f, 18.00f, -76.00f, + -12.00f, 18.00f, -80.00f, -12.00f, 30.00f, -80.00f, 0.00f, + 30.00f, }, + { 68.00f, 0.00f, 51.00f, 68.00f, 26.40f, 51.00f, 68.00f, 26.40f, + 18.00f, 68.00f, 0.00f, 18.00f, 104.00f, 0.00f, 51.00f, + 104.00f, 26.40f, 51.00f, 124.00f, 26.40f, 27.00f, 124.00f, + 0.00f, 27.00f, 92.00f, 0.00f, 78.00f, 92.00f, 10.00f, + 78.00f, 96.00f, 10.00f, 75.00f, 96.00f, 0.00f, 75.00f, + 108.00f, 0.00f, 90.00f, 108.00f, 10.00f, 90.00f, 132.00f, + 10.00f, 90.00f, 132.00f, 0.00f, 90.00f, }, + { 68.00f, 0.00f, 18.00f, 68.00f, -26.40f, 18.00f, 68.00f, -26.40f, + 51.00f, 68.00f, 0.00f, 51.00f, 124.00f, 0.00f, 27.00f, + 124.00f, -26.40f, 27.00f, 104.00f, -26.40f, 51.00f, + 104.00f, 0.00f, 51.00f, 96.00f, 0.00f, 75.00f, 96.00f, + -10.00f, 75.00f, 92.00f, -10.00f, 78.00f, 92.00f, 0.00f, + 78.00f, 132.00f, 0.00f, 90.00f, 132.00f, -10.00f, 90.00f, + 108.00f, -10.00f, 90.00f, 108.00f, 0.00f, 90.00f, }, + { 108.00f, 0.00f, 90.00f, 108.00f, 10.00f, 90.00f, 132.00f, 10.00f, + 90.00f, 132.00f, 0.00f, 90.00f, 112.00f, 0.00f, 93.00f, + 112.00f, 10.00f, 93.00f, 141.00f, 10.00f, 93.75f, 141.00f, + 0.00f, 93.75f, 116.00f, 0.00f, 93.00f, 116.00f, 6.00f, + 93.00f, 138.00f, 6.00f, 94.50f, 138.00f, 0.00f, 94.50f, + 112.00f, 0.00f, 90.00f, 112.00f, 6.00f, 90.00f, 128.00f, + 6.00f, 90.00f, 128.00f, 0.00f, 90.00f, }, + { 132.00f, 0.00f, 90.00f, 132.00f, -10.00f, 90.00f, 108.00f, + -10.00f, 90.00f, 108.00f, 0.00f, 90.00f, 141.00f, 0.00f, + 93.75f, 141.00f, -10.00f, 93.75f, 112.00f, -10.00f, 93.00f, + 112.00f, 0.00f, 93.00f, 138.00f, 0.00f, 94.50f, 138.00f, + -6.00f, 94.50f, 116.00f, -6.00f, 93.00f, 116.00f, 0.00f, + 93.00f, 128.00f, 0.00f, 90.00f, 128.00f, -6.00f, 90.00f, + 112.00f, -6.00f, 90.00f, 112.00f, 0.00f, 90.00f, }, + { 50.00f, 0.00f, 90.00f, 50.00f, 28.00f, 90.00f, 28.00f, 50.00f, + 90.00f, 0.00f, 50.00f, 90.00f, 52.00f, 0.00f, 90.00f, + 52.00f, 29.12f, 90.00f, 29.12f, 52.00f, 90.00f, 0.00f, + 52.00f, 90.00f, 54.00f, 0.00f, 90.00f, 54.00f, 30.24f, + 90.00f, 30.24f, 54.00f, 90.00f, 0.00f, 54.00f, 90.00f, + 56.00f, 0.00f, 90.00f, 56.00f, 31.36f, 90.00f, 31.36f, + 56.00f, 90.00f, 0.00f, 56.00f, 90.00f, }, + { 0.00f, 50.00f, 90.00f, -28.00f, 50.00f, 90.00f, -50.00f, 28.00f, + 90.00f, -50.00f, 0.00f, 90.00f, 0.00f, 52.00f, 90.00f, + -29.12f, 52.00f, 90.00f, -52.00f, 29.12f, 90.00f, -52.00f, + 0.00f, 90.00f, 0.00f, 54.00f, 90.00f, -30.24f, 54.00f, + 90.00f, -54.00f, 30.24f, 90.00f, -54.00f, 0.00f, 90.00f, + 0.00f, 56.00f, 90.00f, -31.36f, 56.00f, 90.00f, -56.00f, + 31.36f, 90.00f, -56.00f, 0.00f, 90.00f, }, + { -50.00f, 0.00f, 90.00f, -50.00f, -28.00f, 90.00f, -28.00f, + -50.00f, 90.00f, 0.00f, -50.00f, 90.00f, -52.00f, 0.00f, + 90.00f, -52.00f, -29.12f, 90.00f, -29.12f, -52.00f, 90.00f, + 0.00f, -52.00f, 90.00f, -54.00f, 0.00f, 90.00f, -54.00f, + -30.24f, 90.00f, -30.24f, -54.00f, 90.00f, 0.00f, -54.00f, + 90.00f, -56.00f, 0.00f, 90.00f, -56.00f, -31.36f, 90.00f, + -31.36f, -56.00f, 90.00f, 0.00f, -56.00f, 90.00f, }, + { 0.00f, -50.00f, 90.00f, 28.00f, -50.00f, 90.00f, 50.00f, -28.00f, + 90.00f, 50.00f, 0.00f, 90.00f, 0.00f, -52.00f, 90.00f, + 29.12f, -52.00f, 90.00f, 52.00f, -29.12f, 90.00f, 52.00f, + 0.00f, 90.00f, 0.00f, -54.00f, 90.00f, 30.24f, -54.00f, + 90.00f, 54.00f, -30.24f, 90.00f, 54.00f, 0.00f, 90.00f, + 0.00f, -56.00f, 90.00f, 31.36f, -56.00f, 90.00f, 56.00f, + -31.36f, 90.00f, 56.00f, 0.00f, 90.00f, }, + { 8.00f, 0.00f, 102.00f, 8.00f, 4.48f, 102.00f, 4.48f, 8.00f, + 102.00f, 0.00f, 8.00f, 102.00f, 16.00f, 0.00f, 96.00f, + 16.00f, 8.96f, 96.00f, 8.96f, 16.00f, 96.00f, 0.00f, + 16.00f, 96.00f, 52.00f, 0.00f, 96.00f, 52.00f, 29.12f, + 96.00f, 29.12f, 52.00f, 96.00f, 0.00f, 52.00f, 96.00f, + 52.00f, 0.00f, 90.00f, 52.00f, 29.12f, 90.00f, 29.12f, + 52.00f, 90.00f, 0.00f, 52.00f, 90.00f, }, + { 0.00f, 8.00f, 102.00f, -4.48f, 8.00f, 102.00f, -8.00f, 4.48f, + 102.00f, -8.00f, 0.00f, 102.00f, 0.00f, 16.00f, 96.00f, + -8.96f, 16.00f, 96.00f, -16.00f, 8.96f, 96.00f, -16.00f, + 0.00f, 96.00f, 0.00f, 52.00f, 96.00f, -29.12f, 52.00f, + 96.00f, -52.00f, 29.12f, 96.00f, -52.00f, 0.00f, 96.00f, + 0.00f, 52.00f, 90.00f, -29.12f, 52.00f, 90.00f, -52.00f, + 29.12f, 90.00f, -52.00f, 0.00f, 90.00f, }, + { -8.00f, 0.00f, 102.00f, -8.00f, -4.48f, 102.00f, -4.48f, -8.00f, + 102.00f, 0.00f, -8.00f, 102.00f, -16.00f, 0.00f, 96.00f, + -16.00f, -8.96f, 96.00f, -8.96f, -16.00f, 96.00f, 0.00f, + -16.00f, 96.00f, -52.00f, 0.00f, 96.00f, -52.00f, -29.12f, + 96.00f, -29.12f, -52.00f, 96.00f, 0.00f, -52.00f, 96.00f, + -52.00f, 0.00f, 90.00f, -52.00f, -29.12f, 90.00f, -29.12f, + -52.00f, 90.00f, 0.00f, -52.00f, 90.00f, }, + { 0.00f, -8.00f, 102.00f, 4.48f, -8.00f, 102.00f, 8.00f, -4.48f, + 102.00f, 8.00f, 0.00f, 102.00f, 0.00f, -16.00f, 96.00f, + 8.96f, -16.00f, 96.00f, 16.00f, -8.96f, 96.00f, 16.00f, + 0.00f, 96.00f, 0.00f, -52.00f, 96.00f, 29.12f, -52.00f, + 96.00f, 52.00f, -29.12f, 96.00f, 52.00f, 0.00f, 96.00f, + 0.00f, -52.00f, 90.00f, 29.12f, -52.00f, 90.00f, 52.00f, + -29.12f, 90.00f, 52.00f, 0.00f, 90.00f, }, + { 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, + 120.00f, 0.00f, 0.00f, 120.00f, 32.00f, 0.00f, 120.00f, + 32.00f, 18.00f, 120.00f, 18.00f, 32.00f, 120.00f, 0.00f, + 32.00f, 120.00f, 0.00f, 0.00f, 108.00f, 0.00f, 0.00f, + 108.00f, 0.00f, 0.00f, 108.00f, 0.00f, 0.00f, 108.00f, + 8.00f, 0.00f, 102.00f, 8.00f, 4.48f, 102.00f, 4.48f, 8.00f, + 102.00f, 0.00f, 8.00f, 102.00f, }, + { 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, + 120.00f, 0.00f, 0.00f, 120.00f, 0.00f, 32.00f, 120.00f, + -18.00f, 32.00f, 120.00f, -32.00f, 18.00f, 120.00f, + -32.00f, 0.00f, 120.00f, 0.00f, 0.00f, 108.00f, 0.00f, + 0.00f, 108.00f, 0.00f, 0.00f, 108.00f, 0.00f, 0.00f, + 108.00f, 0.00f, 8.00f, 102.00f, -4.48f, 8.00f, 102.00f, + -8.00f, 4.48f, 102.00f, -8.00f, 0.00f, 102.00f, }, + { 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, + 120.00f, 0.00f, 0.00f, 120.00f, -32.00f, 0.00f, 120.00f, + -32.00f, -18.00f, 120.00f, -18.00f, -32.00f, 120.00f, + 0.00f, -32.00f, 120.00f, 0.00f, 0.00f, 108.00f, 0.00f, + 0.00f, 108.00f, 0.00f, 0.00f, 108.00f, 0.00f, 0.00f, + 108.00f, -8.00f, 0.00f, 102.00f, -8.00f, -4.48f, 102.00f, + -4.48f, -8.00f, 102.00f, 0.00f, -8.00f, 102.00f, }, + { 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, 120.00f, 0.00f, 0.00f, + 120.00f, 0.00f, 0.00f, 120.00f, 0.00f, -32.00f, 120.00f, + 18.00f, -32.00f, 120.00f, 32.00f, -18.00f, 120.00f, 32.00f, + 0.00f, 120.00f, 0.00f, 0.00f, 108.00f, 0.00f, 0.00f, + 108.00f, 0.00f, 0.00f, 108.00f, 0.00f, 0.00f, 108.00f, + 0.00f, -8.00f, 102.00f, 4.48f, -8.00f, 102.00f, 8.00f, + -4.48f, 102.00f, 8.00f, 0.00f, 102.00f, } }; + + public Teapot() { + super(PATCHES); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/Bitmap.java b/src/main/java/org/sunflow/image/Bitmap.java new file mode 100644 index 0000000..cdb269f --- /dev/null +++ b/src/main/java/org/sunflow/image/Bitmap.java @@ -0,0 +1,14 @@ +package org.sunflow.image; + +public abstract class Bitmap { + protected static final float INV255 = 1.0f / 255; + protected static final float INV65535 = 1.0f / 65535; + + public abstract int getWidth(); + + public abstract int getHeight(); + + public abstract Color readColor(int x, int y); + + public abstract float readAlpha(int x, int y); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/BitmapReader.java b/src/main/java/org/sunflow/image/BitmapReader.java new file mode 100644 index 0000000..1bfe344 --- /dev/null +++ b/src/main/java/org/sunflow/image/BitmapReader.java @@ -0,0 +1,35 @@ +package org.sunflow.image; + +import java.io.IOException; + +/** + * This is a very simple interface, designed to handle loading of bitmap data. + */ +public interface BitmapReader { + /** + * Load the specified filename. This method should throw exception if it + * encounters any errors. If the file is valid but its contents are not + * (invalid header for example), a {@link BitmapFormatException} may be + * thrown. It is an error for this method to return null. + * + * @param filename image filename to load + * @param isLinear if this is true, the bitmap is assumed to + * be already in linear space. This can be usefull when reading + * greyscale images for bump mapping for example. HDR formats can + * ignore this flag since they usually always store data in + * linear form. + * @return a new {@link Bitmap} object + */ + public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException; + + /** + * This exception can be used internally by bitmap readers to signal they + * have encountered a valid file but which contains invalid content. + */ + @SuppressWarnings("serial") + public static final class BitmapFormatException extends Exception { + public BitmapFormatException(String message) { + super(message); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/BitmapWriter.java b/src/main/java/org/sunflow/image/BitmapWriter.java new file mode 100644 index 0000000..96e42ef --- /dev/null +++ b/src/main/java/org/sunflow/image/BitmapWriter.java @@ -0,0 +1,78 @@ +package org.sunflow.image; + +import java.io.IOException; + +/** + * This interface is used to represents an image output format. The methods are + * tile oriented so that tiled image formats may be optimally supported. Note + * that if the header is declared with a 0 tile size, the image will not be + * written with identical sized tiles. The image should either be buffered so it + * can be written all at once on close, or an eror should be thrown. The bitmap + * writer should be designed so that it is thread safe. Specifically, this means + * that the tile writing method can be called by several threads. + */ +public interface BitmapWriter { + /** + * This method will be called before writing begins. It is used to set + * common attributes to file writers. Currently supported keywords include: + *
    + *
  • "compression"
  • + *
  • "channeltype": "byte", "short", "half", "float"
  • + *
+ * Note that this method should not fail if its input is not supported or + * invalid. It should gracefully ignore the error and keep its default + * state. + * + * @param option + * @param value + */ + public abstract void configure(String option, String value); + + /** + * Open a handle to the specified file for writing. If the writer buffers + * the image and writes it on close, then the filename should be stored. + * + * @param filename filename to write the bitmap to + * @throws IOException thrown if an I/O error occurs + */ + public abstract void openFile(String filename) throws IOException; + + /** + * Write the bitmap header. This may be defered if the image is buffered for + * writing all at once on close. Note that if tile size is positive, data + * sent to this class is guarenteed to arrive in tiles of that size (except + * at borders). Otherwise, it should be assumed that the data is random, and + * that it may overlap. The writer should then either throw an error or + * start buffering data manually. + * + * @param width image width + * @param height image height + * @param tileSize tile size or 0 if the image will not be sent in tiled + * form + * @throws IOException thrown if an I/O error occurs + * @throws UnsupportedOperationException thrown if this writer does not + * support writing the image with the supplied tile size + */ + public abstract void writeHeader(int width, int height, int tileSize) throws IOException, UnsupportedOperationException; + + /** + * Write a tile of data. Note that this method may be called by more than + * one thread, so it should be made thread-safe if possible. + * + * @param x tile x coordinate + * @param y tile y coordinate + * @param w tile width + * @param h tile height + * @param color color data + * @param alpha alpha data + * @throws IOException thrown if an I/O error occurs + */ + public abstract void writeTile(int x, int y, int w, int h, Color[] color, float[] alpha) throws IOException; + + /** + * Close the file, this completes the bitmap writing process. + * + * @throws IOException thrown if an I/O error occurs + */ + public abstract void closeFile() throws IOException; +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/BlackbodySpectrum.java b/src/main/java/org/sunflow/image/BlackbodySpectrum.java new file mode 100644 index 0000000..3ceae65 --- /dev/null +++ b/src/main/java/org/sunflow/image/BlackbodySpectrum.java @@ -0,0 +1,15 @@ +package org.sunflow.image; + +public class BlackbodySpectrum extends SpectralCurve { + private float temp; + + public BlackbodySpectrum(float temp) { + this.temp = temp; + } + + @Override + public float sample(float lambda) { + double wavelength = lambda * 1e-9; + return (float) ((3.74183e-16 * Math.pow(wavelength, -5.0)) / (Math.exp(1.4388e-2 / (wavelength * temp)) - 1.0)); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/ChromaticitySpectrum.java b/src/main/java/org/sunflow/image/ChromaticitySpectrum.java new file mode 100644 index 0000000..7c9c7ee --- /dev/null +++ b/src/main/java/org/sunflow/image/ChromaticitySpectrum.java @@ -0,0 +1,59 @@ +package org.sunflow.image; + +/** + * This spectral curve represents a given (x,y) chromaticity pair as explained + * in the sun/sky paper (section A.5) + */ +public final class ChromaticitySpectrum extends SpectralCurve { + private static final float[] S0Amplitudes = { 0.04f, 6.0f, 29.6f, 55.3f, + 57.3f, 61.8f, 61.5f, 68.8f, 63.4f, 65.8f, 94.8f, 104.8f, 105.9f, + 96.8f, 113.9f, 125.6f, 125.5f, 121.3f, 121.3f, 113.5f, 113.1f, + 110.8f, 106.5f, 108.8f, 105.3f, 104.4f, 100.0f, 96.0f, 95.1f, + 89.1f, 90.5f, 90.3f, 88.4f, 84.0f, 85.1f, 81.9f, 82.6f, 84.9f, + 81.3f, 71.9f, 74.3f, 76.4f, 63.3f, 71.7f, 77.0f, 65.2f, 47.7f, + 68.6f, 65.0f, 66.0f, 61.0f, 53.3f, 58.9f, 61.9f }; + + private static final float[] S1Amplitudes = { 0.02f, 4.5f, 22.4f, 42.0f, + 40.6f, 41.6f, 38.0f, 42.4f, 38.5f, 35.0f, 43.4f, 46.3f, 43.9f, + 37.1f, 36.7f, 35.9f, 32.6f, 27.9f, 24.3f, 20.1f, 16.2f, 13.2f, + 8.6f, 6.1f, 4.2f, 1.9f, 0.0f, -1.6f, -3.5f, -3.5f, -5.8f, -7.2f, + -8.6f, -9.5f, -10.9f, -10.7f, -12.0f, -14.0f, -13.6f, -12.0f, + -13.3f, -12.9f, -10.6f, -11.6f, -12.2f, -10.2f, -7.8f, -11.2f, + -10.4f, -10.6f, -9.7f, -8.3f, -9.3f, -9.8f }; + + private static final float[] S2Amplitudes = { 0.0f, 2.0f, 4.0f, 8.5f, 7.8f, + 6.7f, 5.3f, 6.1f, 3.0f, 1.2f, -1.1f, -0.5f, -0.7f, -1.2f, -2.6f, + -2.9f, -2.8f, -2.6f, -2.6f, -1.8f, -1.5f, -1.3f, -1.2f, -1.0f, + -0.5f, -0.3f, 0.0f, 0.2f, 0.5f, 2.1f, 3.2f, 4.1f, 4.7f, 5.1f, 6.7f, + 7.3f, 8.6f, 9.8f, 10.2f, 8.3f, 9.6f, 8.5f, 7.0f, 7.6f, 8.0f, 6.7f, + 5.2f, 7.4f, 6.8f, 7.0f, 6.4f, 5.5f, 6.1f, 6.5f }; + + private static final RegularSpectralCurve kS0Spectrum = new RegularSpectralCurve(S0Amplitudes, 300, 830); + private static final RegularSpectralCurve kS1Spectrum = new RegularSpectralCurve(S1Amplitudes, 300, 830); + private static final RegularSpectralCurve kS2Spectrum = new RegularSpectralCurve(S2Amplitudes, 300, 830); + + private static final XYZColor S0xyz = kS0Spectrum.toXYZ(); + private static final XYZColor S1xyz = kS1Spectrum.toXYZ(); + private static final XYZColor S2xyz = kS2Spectrum.toXYZ(); + + private final float M1, M2; + + public ChromaticitySpectrum(float x, float y) { + M1 = (-1.3515f - 1.7703f * x + 5.9114f * y) / (0.0241f + 0.2562f * x - 0.7341f * y); + M2 = (0.03f - 31.4424f * x + 30.0717f * y) / (0.0241f + 0.2562f * x - 0.7341f * y); + } + + @Override + public float sample(float lambda) { + return kS0Spectrum.sample(lambda) + M1 * kS1Spectrum.sample(lambda) + M2 * kS2Spectrum.sample(lambda); + } + + public static final XYZColor get(float x, float y) { + float M1 = (-1.3515f - 1.7703f * x + 5.9114f * y) / (0.0241f + 0.2562f * x - 0.7341f * y); + float M2 = (0.03f - 31.4424f * x + 30.0717f * y) / (0.0241f + 0.2562f * x - 0.7341f * y); + float X = S0xyz.getX() + M1 * S1xyz.getX() + M2 * S2xyz.getX(); + float Y = S0xyz.getY() + M1 * S1xyz.getY() + M2 * S2xyz.getY(); + float Z = S0xyz.getZ() + M1 * S1xyz.getZ() + M2 * S2xyz.getZ(); + return new XYZColor(X, Y, Z); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/Color.java b/src/main/java/org/sunflow/image/Color.java new file mode 100644 index 0000000..9281ef0 --- /dev/null +++ b/src/main/java/org/sunflow/image/Color.java @@ -0,0 +1,370 @@ +package org.sunflow.image; + +import org.sunflow.math.MathUtils; + +public final class Color { + private float r, g, b; + public static final RGBSpace NATIVE_SPACE = RGBSpace.SRGB; + public static final Color BLACK = new Color(0, 0, 0); + public static final Color WHITE = new Color(1, 1, 1); + public static final Color RED = new Color(1, 0, 0); + public static final Color GREEN = new Color(0, 1, 0); + public static final Color BLUE = new Color(0, 0, 1); + public static final Color YELLOW = new Color(1, 1, 0); + public static final Color CYAN = new Color(0, 1, 1); + public static final Color MAGENTA = new Color(1, 0, 1); + public static final Color GRAY = new Color(0.5f, 0.5f, 0.5f); + + public static Color black() { + return new Color(); + } + + public static Color white() { + return new Color(1, 1, 1); + } + + private static final float[] EXPONENT = new float[256]; + + static { + EXPONENT[0] = 0; + for (int i = 1; i < 256; i++) { + float f = 1.0f; + int e = i - (128 + 8); + if (e > 0) + for (int j = 0; j < e; j++) + f *= 2.0f; + else + for (int j = 0; j < -e; j++) + f *= 0.5f; + EXPONENT[i] = f; + } + } + + public Color() { + } + + public Color(float gray) { + r = g = b = gray; + } + + public Color(float r, float g, float b) { + this.r = r; + this.g = g; + this.b = b; + } + + public Color toNonLinear() { + r = NATIVE_SPACE.gammaCorrect(r); + g = NATIVE_SPACE.gammaCorrect(g); + b = NATIVE_SPACE.gammaCorrect(b); + return this; + } + + public Color toLinear() { + r = NATIVE_SPACE.ungammaCorrect(r); + g = NATIVE_SPACE.ungammaCorrect(g); + b = NATIVE_SPACE.ungammaCorrect(b); + return this; + } + + public Color(Color c) { + r = c.r; + g = c.g; + b = c.b; + } + + public Color(int rgb) { + r = ((rgb >> 16) & 0xFF) / 255.0f; + g = ((rgb >> 8) & 0xFF) / 255.0f; + b = (rgb & 0xFF) / 255.0f; + } + + public Color copy() { + return new Color(this); + } + + public final Color set(float r, float g, float b) { + this.r = r; + this.g = g; + this.b = b; + return this; + } + + public final Color set(Color c) { + r = c.r; + g = c.g; + b = c.b; + return this; + } + + public final Color setRGB(int rgb) { + r = ((rgb >> 16) & 0xFF) / 255.0f; + g = ((rgb >> 8) & 0xFF) / 255.0f; + b = (rgb & 0xFF) / 255.0f; + return this; + } + + public final Color setRGBE(int rgbe) { + float f = EXPONENT[rgbe & 0xFF]; + r = f * ((rgbe >>> 24) + 0.5f); + g = f * (((rgbe >> 16) & 0xFF) + 0.5f); + b = f * (((rgbe >> 8) & 0xFF) + 0.5f); + return this; + } + + public final boolean isBlack() { + return r <= 0 && g <= 0 && b <= 0; + } + + public final float getLuminance() { + return (0.2989f * r) + (0.5866f * g) + (0.1145f * b); + } + + public final float getMin() { + return MathUtils.min(r, g, b); + } + + public final float getMax() { + return MathUtils.max(r, g, b); + } + + public final float getAverage() { + return (r + g + b) / 3.0f; + } + + public final float[] getRGB() { + return new float[] { r, g, b }; + } + + public final int toRGB() { + int ir = (int) (r * 255 + 0.5); + int ig = (int) (g * 255 + 0.5); + int ib = (int) (b * 255 + 0.5); + ir = MathUtils.clamp(ir, 0, 255); + ig = MathUtils.clamp(ig, 0, 255); + ib = MathUtils.clamp(ib, 0, 255); + return (ir << 16) | (ig << 8) | ib; + } + + public final int toRGBA(float a) { + int ir = (int) (r * 255 + 0.5); + int ig = (int) (g * 255 + 0.5); + int ib = (int) (b * 255 + 0.5); + int ia = (int) (a * 255 + 0.5); + ir = MathUtils.clamp(ir, 0, 255); + ig = MathUtils.clamp(ig, 0, 255); + ib = MathUtils.clamp(ib, 0, 255); + ia = MathUtils.clamp(ia, 0, 255); + return (ia << 24) | (ir << 16) | (ig << 8) | ib; + } + + public final int toRGBE() { + // encode the color into 32bits while preserving HDR using Ward's RGBE + // technique + float v = MathUtils.max(r, g, b); + if (v < 1e-32f) + return 0; + + // get mantissa and exponent + float m = v; + int e = 0; + if (v > 1.0f) { + while (m > 1.0f) { + m *= 0.5f; + e++; + } + } else if (v <= 0.5f) { + while (m <= 0.5f) { + m *= 2.0f; + e--; + } + } + v = (m * 255.0f) / v; + int c = (e + 128); + c |= ((int) (r * v) << 24); + c |= ((int) (g * v) << 16); + c |= ((int) (b * v) << 8); + return c; + } + + public final Color constrainRGB() { + // clamp the RGB value to a representable value + float w = -MathUtils.min(0, r, g, b); + if (w > 0) { + r += w; + g += w; + b += w; + } + return this; + } + + public final boolean isNan() { + return Float.isNaN(r) || Float.isNaN(g) || Float.isNaN(b); + } + + public final boolean isInf() { + return Float.isInfinite(r) || Float.isInfinite(g) || Float.isInfinite(b); + } + + public final Color add(Color c) { + r += c.r; + g += c.g; + b += c.b; + return this; + } + + public static final Color add(Color c1, Color c2) { + return Color.add(c1, c2, new Color()); + } + + public static final Color add(Color c1, Color c2, Color dest) { + dest.r = c1.r + c2.r; + dest.g = c1.g + c2.g; + dest.b = c1.b + c2.b; + return dest; + } + + public final Color madd(float s, Color c) { + r += (s * c.r); + g += (s * c.g); + b += (s * c.b); + return this; + } + + public final Color madd(Color s, Color c) { + r += s.r * c.r; + g += s.g * c.g; + b += s.b * c.b; + return this; + } + + public final Color sub(Color c) { + r -= c.r; + g -= c.g; + b -= c.b; + return this; + } + + public static final Color sub(Color c1, Color c2) { + return Color.sub(c1, c2, new Color()); + } + + public static final Color sub(Color c1, Color c2, Color dest) { + dest.r = c1.r - c2.r; + dest.g = c1.g - c2.g; + dest.b = c1.b - c2.b; + return dest; + } + + public final Color mul(Color c) { + r *= c.r; + g *= c.g; + b *= c.b; + return this; + } + + public static final Color mul(Color c1, Color c2) { + return Color.mul(c1, c2, new Color()); + } + + public static final Color mul(Color c1, Color c2, Color dest) { + dest.r = c1.r * c2.r; + dest.g = c1.g * c2.g; + dest.b = c1.b * c2.b; + return dest; + } + + public final Color mul(float s) { + r *= s; + g *= s; + b *= s; + return this; + } + + public static final Color mul(float s, Color c) { + return Color.mul(s, c, new Color()); + } + + public static final Color mul(float s, Color c, Color dest) { + dest.r = s * c.r; + dest.g = s * c.g; + dest.b = s * c.b; + return dest; + } + + public final Color div(Color c) { + r /= c.r; + g /= c.g; + b /= c.b; + return this; + } + + public static final Color div(Color c1, Color c2) { + return Color.div(c1, c2, new Color()); + } + + public static final Color div(Color c1, Color c2, Color dest) { + dest.r = c1.r / c2.r; + dest.g = c1.g / c2.g; + dest.b = c1.b / c2.b; + return dest; + } + + public final Color exp() { + r = (float) Math.exp(r); + g = (float) Math.exp(g); + b = (float) Math.exp(b); + return this; + } + + public final Color opposite() { + r = 1 - r; + g = 1 - g; + b = 1 - b; + return this; + } + + public final Color clamp(float min, float max) { + r = MathUtils.clamp(r, min, max); + g = MathUtils.clamp(g, min, max); + b = MathUtils.clamp(b, min, max); + return this; + } + + public static final Color blend(Color c1, Color c2, float b) { + return blend(c1, c2, b, new Color()); + } + + public static final Color blend(Color c1, Color c2, float b, Color dest) { + dest.r = (1.0f - b) * c1.r + b * c2.r; + dest.g = (1.0f - b) * c1.g + b * c2.g; + dest.b = (1.0f - b) * c1.b + b * c2.b; + return dest; + } + + public static final Color blend(Color c1, Color c2, Color b) { + return blend(c1, c2, b, new Color()); + } + + public static final Color blend(Color c1, Color c2, Color b, Color dest) { + dest.r = (1.0f - b.r) * c1.r + b.r * c2.r; + dest.g = (1.0f - b.g) * c1.g + b.g * c2.g; + dest.b = (1.0f - b.b) * c1.b + b.b * c2.b; + return dest; + } + + public static final boolean hasContrast(Color c1, Color c2, float thresh) { + if (Math.abs(c1.r - c2.r) / (c1.r + c2.r) > thresh) + return true; + if (Math.abs(c1.g - c2.g) / (c1.g + c2.g) > thresh) + return true; + if (Math.abs(c1.b - c2.b) / (c1.b + c2.b) > thresh) + return true; + return false; + } + + @Override + public String toString() { + return String.format("(%.3f, %.3f, %.3f)", r, g, b); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/ColorEncoder.java b/src/main/java/org/sunflow/image/ColorEncoder.java new file mode 100644 index 0000000..37a379c --- /dev/null +++ b/src/main/java/org/sunflow/image/ColorEncoder.java @@ -0,0 +1,90 @@ +package org.sunflow.image; + +import org.sunflow.math.MathUtils; + +/** + * This class contains many static helper methods that may be helpful for + * encoding colors into files. + */ +public final class ColorEncoder { + /** + * Undoes the premultiplication of the specified color array. The original + * colors are not modified. + * + * @param color an array of premultiplied colors + * @param alpha alpha values corresponding to the colors + * @return an array of unpremultiplied colors + */ + public static final Color[] unpremult(Color[] color, float[] alpha) { + Color[] output = new Color[color.length]; + for (int i = 0; i < color.length; i++) + output[i] = color[i].copy().mul(1 / alpha[i]); + return output; + } + + /** + * Moves the colors in the specified array to non-linear space. The original + * colors are not modified. + * + * @param color an array of colors in linear space + * @return a new array of the same colors in non-linear space + */ + public static final Color[] unlinearize(Color[] color) { + Color[] output = new Color[color.length]; + for (int i = 0; i < color.length; i++) + output[i] = color[i].copy().toNonLinear(); + return output; + } + + /** + * Quantize the specified colors to 8-bit RGB format. The returned array + * contains 3 bytes for each color in the original array. + * + * @param color array of colors to quantize + * @return array of quantized RGB values + */ + public static final byte[] quantizeRGB8(Color[] color) { + byte[] output = new byte[color.length * 3]; + for (int i = 0, index = 0; i < color.length; i++, index += 3) { + float[] rgb = color[i].getRGB(); + output[index + 0] = (byte) MathUtils.clamp((int) (rgb[0] * 255 + 0.5f), 0, 255); + output[index + 1] = (byte) MathUtils.clamp((int) (rgb[1] * 255 + 0.5f), 0, 255); + output[index + 2] = (byte) MathUtils.clamp((int) (rgb[2] * 255 + 0.5f), 0, 255); + } + return output; + } + + /** + * Quantize the specified colors to 8-bit RGBA format. The returned array + * contains 4 bytes for each color in the original array. + * + * @param color array of colors to quantize + * @param alpha array of alpha values (same length as color) + * @return array of quantized RGBA values + */ + public static final byte[] quantizeRGBA8(Color[] color, float[] alpha) { + byte[] output = new byte[color.length * 4]; + for (int i = 0, index = 0; i < color.length; i++, index += 4) { + float[] rgb = color[i].getRGB(); + output[index + 0] = (byte) MathUtils.clamp((int) (rgb[0] * 255 + 0.5f), 0, 255); + output[index + 1] = (byte) MathUtils.clamp((int) (rgb[1] * 255 + 0.5f), 0, 255); + output[index + 2] = (byte) MathUtils.clamp((int) (rgb[2] * 255 + 0.5f), 0, 255); + output[index + 3] = (byte) MathUtils.clamp((int) (alpha[i] * 255 + 0.5f), 0, 255); + } + return output; + } + + /** + * Encode the specified colors using Ward's RGBE technique. The returned + * array contains one int for each color in the original array. + * + * @param color array of colors to encode + * @return array of encoded colors + */ + public static final int[] encodeRGBE(Color[] color) { + int[] output = new int[color.length]; + for (int i = 0; i < color.length; i++) + output[i] = color[i].toRGBE(); + return output; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/ColorFactory.java b/src/main/java/org/sunflow/image/ColorFactory.java new file mode 100644 index 0000000..c7807cb --- /dev/null +++ b/src/main/java/org/sunflow/image/ColorFactory.java @@ -0,0 +1,112 @@ +package org.sunflow.image; + +public final class ColorFactory { + /** + * Return the name of the internal color space. This string can be used + * interchangeably with null in the following methods. + * + * @return internal colorspace name + */ + public static String getInternalColorspace() { + return "sRGB linear"; + } + + /** + * Checks to see how many values are required to specify a color using the + * given colorspace. This number can be variable for spectrum colors, in + * which case the returned value is -1. If the colorspace name is invalid, + * this method returns -2. No exception is thrown. This method is intended + * for parsers that want to know how many floating values to retrieve from a + * file. + * + * @param colorspace + * @return number of floating point numbers expected, -1 for any, -2 on + * error + */ + public static int getRequiredDataValues(String colorspace) { + if (colorspace == null) + return 3; + if (colorspace.equals("sRGB nonlinear")) + return 3; + else if (colorspace.equals("sRGB linear")) + return 3; + else if (colorspace.equals("XYZ")) + return 3; + else if (colorspace.equals("blackbody")) + return 1; + else if (colorspace.startsWith("spectrum")) + return -1; + else + return -2; + } + + /** + * Creates a color value in the renderer's internal color space from a + * string (representing the color space name) and an array of floating point + * values. If the colorspace string is null, we assume the data was supplied + * in internal space. This method does much error checking and may throw a + * {@link RuntimeException} if its parameters are not consistent. Here are + * the currently supported color spaces: + *
    + *
  • "sRGB nonlinear" - requires 3 values
  • + *
  • "sRGB linear" - requires 3 values
  • + *
  • "XYZ" - requires 3 values
  • + *
  • blackbody - requires 1 value (temperature in Kelvins)
  • + *
  • spectrum [min] [max] - any number of values (must be + * >0), [start] and [stop] is the range over which the spectrum is defined + * in nanometers.
  • + *
+ * + * @param colorspace color space name + * @param data data describing this color + * @return a valid color in the renderer's color space + * @throws ColorSpecificationException + */ + public static Color createColor(String colorspace, float... data) throws ColorSpecificationException { + int required = getRequiredDataValues(colorspace); + if (required == -2) + throw new ColorSpecificationException("unknown colorspace %s"); + if (required != -1 && required != data.length) + throw new ColorSpecificationException(required, data.length); + if (colorspace == null) + return new Color(data[0], data[1], data[2]); + else if (colorspace.equals("sRGB nonlinear")) + return new Color(data[0], data[1], data[2]).toLinear(); + else if (colorspace.equals("sRGB linear")) + return new Color(data[0], data[1], data[2]); + else if (colorspace.equals("XYZ")) + return RGBSpace.SRGB.convertXYZtoRGB(new XYZColor(data[0], data[1], data[2])); + else if (colorspace.equals("blackbody")) + return RGBSpace.SRGB.convertXYZtoRGB(new BlackbodySpectrum(data[0]).toXYZ()); + else if (colorspace.startsWith("spectrum")) { + String[] tokens = colorspace.split("\\s+"); + if (tokens.length != 3) + throw new ColorSpecificationException("invalid spectrum specification"); + if (data.length == 0) + throw new ColorSpecificationException("missing spectrum data"); + try { + float lambdaMin = Float.parseFloat(tokens[1]); + float lambdaMax = Float.parseFloat(tokens[2]); + return RGBSpace.SRGB.convertXYZtoRGB(new RegularSpectralCurve(data, lambdaMin, lambdaMax).toXYZ()); + } catch (NumberFormatException e) { + throw new ColorSpecificationException("unable to parse spectrum wavelength range"); + } + } + throw new ColorSpecificationException(String.format("Inconsistent code! Please report this error. (Input %s - %d)", colorspace, data.length)); + } + + @SuppressWarnings("serial") + public static final class ColorSpecificationException extends Exception { + private ColorSpecificationException() { + super("Invalid color specification"); + } + + private ColorSpecificationException(String message) { + super(String.format("Invalid color specification: %s", message)); + } + + private ColorSpecificationException(int expected, int found) { + this(String.format("invalid data length, expecting %d values, found %d", expected, found)); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/ConstantSpectralCurve.java b/src/main/java/org/sunflow/image/ConstantSpectralCurve.java new file mode 100644 index 0000000..ad71782 --- /dev/null +++ b/src/main/java/org/sunflow/image/ConstantSpectralCurve.java @@ -0,0 +1,20 @@ +package org.sunflow.image; + +/** + * Very simple class equivalent to a constant spectral curve. Note that this is + * most likely physically impossible for amplitudes > 0, however this class can + * be handy since in practice spectral curves end up being integrated against + * the finite width color matching functions. + */ +public class ConstantSpectralCurve extends SpectralCurve { + private final float amp; + + public ConstantSpectralCurve(float amp) { + this.amp = amp; + } + + @Override + public float sample(float lambda) { + return amp; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/IrregularSpectralCurve.java b/src/main/java/org/sunflow/image/IrregularSpectralCurve.java new file mode 100644 index 0000000..f13d35c --- /dev/null +++ b/src/main/java/org/sunflow/image/IrregularSpectralCurve.java @@ -0,0 +1,50 @@ +package org.sunflow.image; + +/** + * This class allows spectral curves to be defined from irregularly sampled + * data. Note that the wavelength array is assumed to be sorted low to high. Any + * values beyond the defined range will simply be extended to infinity from the + * end points. Points inside the valid range will be linearly interpolated + * between the two nearest samples. No explicit error checking is performed, but + * this class will run into {@link ArrayIndexOutOfBoundsException}s if the + * array lengths don't match. + */ +public class IrregularSpectralCurve extends SpectralCurve { + private final float[] wavelengths; + private final float[] amplitudes; + + /** + * Define an irregular spectral curve from the provided (sorted) wavelengths + * and amplitude data. The wavelength array is assumed to contain values in + * nanometers. Array lengths must match. + * + * @param wavelengths sampled wavelengths in nm + * @param amplitudes amplitude of the curve at the sampled points + */ + public IrregularSpectralCurve(float[] wavelengths, float[] amplitudes) { + this.wavelengths = wavelengths; + this.amplitudes = amplitudes; + if (wavelengths.length != amplitudes.length) + throw new RuntimeException(String.format("Error creating irregular spectral curve: %d wavelengths and %d amplitudes", wavelengths.length, amplitudes.length)); + for (int i = 1; i < wavelengths.length; i++) + if (wavelengths[i - 1] >= wavelengths[i]) + throw new RuntimeException(String.format("Error creating irregular spectral curve: values are not sorted - error at index %d", i)); + } + + @Override + public float sample(float lambda) { + if (wavelengths.length == 0) + return 0; // no data + if (wavelengths.length == 1 || lambda <= wavelengths[0]) + return amplitudes[0]; + if (lambda >= wavelengths[wavelengths.length - 1]) + return amplitudes[wavelengths.length - 1]; + for (int i = 1; i < wavelengths.length; i++) { + if (lambda < wavelengths[i]) { + float dx = (lambda - wavelengths[i - 1]) / (wavelengths[i] - wavelengths[i - 1]); + return (1 - dx) * amplitudes[i - 1] + dx * amplitudes[i]; + } + } + return amplitudes[wavelengths.length - 1]; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/RGBSpace.java b/src/main/java/org/sunflow/image/RGBSpace.java new file mode 100644 index 0000000..1c0c331 --- /dev/null +++ b/src/main/java/org/sunflow/image/RGBSpace.java @@ -0,0 +1,199 @@ +package org.sunflow.image; + +import org.sunflow.math.MathUtils; + +public final class RGBSpace { + public static final RGBSpace ADOBE = new RGBSpace(0.6400f, 0.3300f, 0.2100f, 0.7100f, 0.1500f, 0.0600f, 0.31271f, 0.32902f, 2.2f, 0); + public static final RGBSpace APPLE = new RGBSpace(0.6250f, 0.3400f, 0.2800f, 0.5950f, 0.1550f, 0.0700f, 0.31271f, 0.32902f, 1.8f, 0); + public static final RGBSpace NTSC = new RGBSpace(0.6700f, 0.3300f, 0.2100f, 0.7100f, 0.1400f, 0.0800f, 0.31010f, 0.31620f, 20.0f / 9.0f, 0.018f); + public static final RGBSpace HDTV = new RGBSpace(0.6400f, 0.3300f, 0.3000f, 0.6000f, 0.1500f, 0.0600f, 0.31271f, 0.32902f, 20.0f / 9.0f, 0.018f); + public static final RGBSpace SRGB = new RGBSpace(0.6400f, 0.3300f, 0.3000f, 0.6000f, 0.1500f, 0.0600f, 0.31271f, 0.32902f, 2.4f, 0.00304f); + public static final RGBSpace CIE = new RGBSpace(0.7350f, 0.2650f, 0.2740f, 0.7170f, 0.1670f, 0.0090f, 1 / 3.0f, 1 / 3.0f, 2.2f, 0); + public static final RGBSpace EBU = new RGBSpace(0.6400f, 0.3300f, 0.2900f, 0.6000f, 0.1500f, 0.0600f, 0.31271f, 0.32902f, 20.0f / 9.0f, 0.018f); + public static final RGBSpace SMPTE_C = new RGBSpace(0.6300f, 0.3400f, 0.3100f, 0.5950f, 0.1550f, 0.0700f, 0.31271f, 0.32902f, 20.0f / 9.0f, 0.018f); + public static final RGBSpace SMPTE_240M = new RGBSpace(0.6300f, 0.3400f, 0.3100f, 0.5950f, 0.1550f, 0.0700f, 0.31271f, 0.32902f, 20.0f / 9.0f, 0.018f); + public static final RGBSpace WIDE_GAMUT = new RGBSpace(0.7347f, 0.2653f, 0.1152f, 0.8264f, 0.1566f, 0.0177f, 0.3457f, 0.3585f, 2.2f, 0); + + private final float gamma, breakPoint; + private final float slope, slopeMatch, segmentOffset; + private final float xr, yr, zr, xg, yg, zg, xb, yb, zb; + private final float xw, yw, zw; + private final float rx, ry, rz, gx, gy, gz, bx, by, bz; + private final float rw, gw, bw; + private final int[] GAMMA_CURVE; + private final int[] INV_GAMMA_CURVE; + + public RGBSpace(float xRed, float yRed, float xGreen, float yGreen, float xBlue, float yBlue, float xWhite, float yWhite, float gamma, float breakPoint) { + this.gamma = gamma; + this.breakPoint = breakPoint; + + if (breakPoint > 0) { + slope = 1 / (gamma / (float) Math.pow(breakPoint, 1 / gamma - 1) - gamma * breakPoint + breakPoint); + slopeMatch = gamma * slope / (float) Math.pow(breakPoint, 1 / gamma - 1); + segmentOffset = slopeMatch * (float) Math.pow(breakPoint, 1 / gamma) - slope * breakPoint; + } else { + slope = 1; + slopeMatch = 1; + segmentOffset = 0; + } + + // prepare gamma curves + GAMMA_CURVE = new int[256]; + INV_GAMMA_CURVE = new int[256]; + for (int i = 0; i < 256; i++) { + float c = i / 255.0f; + GAMMA_CURVE[i] = MathUtils.clamp((int) (gammaCorrect(c) * 255 + 0.5f), 0, 255); + INV_GAMMA_CURVE[i] = MathUtils.clamp((int) (ungammaCorrect(c) * 255 + 0.5f), 0, 255); + } + + float xr = xRed; + float yr = yRed; + float zr = 1 - (xr + yr); + float xg = xGreen; + float yg = yGreen; + float zg = 1 - (xg + yg); + float xb = xBlue; + float yb = yBlue; + float zb = 1 - (xb + yb); + + xw = xWhite; + yw = yWhite; + zw = 1 - (xw + yw); + + // xyz -> rgb matrix, before scaling to white. + float rx = (yg * zb) - (yb * zg); + float ry = (xb * zg) - (xg * zb); + float rz = (xg * yb) - (xb * yg); + float gx = (yb * zr) - (yr * zb); + float gy = (xr * zb) - (xb * zr); + float gz = (xb * yr) - (xr * yb); + float bx = (yr * zg) - (yg * zr); + float by = (xg * zr) - (xr * zg); + float bz = (xr * yg) - (xg * yr); + // White scaling factors + // Dividing by yw scales the white luminance to unity, as conventional + rw = ((rx * xw) + (ry * yw) + (rz * zw)) / yw; + gw = ((gx * xw) + (gy * yw) + (gz * zw)) / yw; + bw = ((bx * xw) + (by * yw) + (bz * zw)) / yw; + + // xyz -> rgb matrix, correctly scaled to white + this.rx = rx / rw; + this.ry = ry / rw; + this.rz = rz / rw; + this.gx = gx / gw; + this.gy = gy / gw; + this.gz = gz / gw; + this.bx = bx / bw; + this.by = by / bw; + this.bz = bz / bw; + + // invert matrix again to get proper rgb -> xyz matrix + float s = 1 / (this.rx * (this.gy * this.bz - this.by * this.gz) - this.ry * (this.gx * this.bz - this.bx * this.gz) + this.rz * (this.gx * this.by - this.bx * this.gy)); + this.xr = s * (this.gy * this.bz - this.gz * this.by); + this.xg = s * (this.rz * this.by - this.ry * this.bz); + this.xb = s * (this.ry * this.gz - this.rz * this.gy); + + this.yr = s * (this.gz * this.bx - this.gx * this.bz); + this.yg = s * (this.rx * this.bz - this.rz * this.bx); + this.yb = s * (this.rz * this.gx - this.rx * this.gz); + + this.zr = s * (this.gx * this.by - this.gy * this.bx); + this.zg = s * (this.ry * this.bx - this.rx * this.by); + this.zb = s * (this.rx * this.gy - this.ry * this.gx); + } + + public final Color convertXYZtoRGB(XYZColor c) { + return convertXYZtoRGB(c.getX(), c.getY(), c.getZ()); + } + + public final Color convertXYZtoRGB(float X, float Y, float Z) { + float r = (rx * X) + (ry * Y) + (rz * Z); + float g = (gx * X) + (gy * Y) + (gz * Z); + float b = (bx * X) + (by * Y) + (bz * Z); + return new Color(r, g, b); + } + + public final XYZColor convertRGBtoXYZ(Color c) { + float[] rgb = c.getRGB(); + float X = (xr * rgb[0]) + (xg * rgb[1]) + (xb * rgb[2]); + float Y = (yr * rgb[0]) + (yg * rgb[1]) + (yb * rgb[2]); + float Z = (zr * rgb[0]) + (zg * rgb[1]) + (zb * rgb[2]); + return new XYZColor(X, Y, Z); + } + + public final boolean insideGamut(float r, float g, float b) { + return r >= 0 && g >= 0 && b >= 0; + } + + public final float gammaCorrect(float v) { + if (v <= 0) + return 0; + else if (v >= 1) + return 1; + else if (v <= breakPoint) + return slope * v; + else + return slopeMatch * (float) Math.pow(v, 1 / gamma) - segmentOffset; + } + + public final float ungammaCorrect(float vp) { + if (vp <= 0) + return 0; + else if (vp >= 1) + return 1; + else if (vp <= breakPoint * slope) + return vp / slope; + else + return (float) Math.pow((vp + segmentOffset) / slopeMatch, gamma); + } + + public final int rgbToNonLinear(int rgb) { + // gamma correct 24bit rgb value via tables + int rp = GAMMA_CURVE[(rgb >> 16) & 0xFF]; + int gp = GAMMA_CURVE[(rgb >> 8) & 0xFF]; + int bp = GAMMA_CURVE[rgb & 0xFF]; + return (rp << 16) | (gp << 8) | bp; + } + + public final int rgbToLinear(int rgb) { + // convert a packed RGB triplet to a linearized + // one by applying the proper LUT + int rp = INV_GAMMA_CURVE[(rgb >> 16) & 0xFF]; + int gp = INV_GAMMA_CURVE[(rgb >> 8) & 0xFF]; + int bp = INV_GAMMA_CURVE[rgb & 0xFF]; + return (rp << 16) | (gp << 8) | bp; + } + + public final byte rgbToNonLinear(byte r) { + return (byte) GAMMA_CURVE[r & 0xFF]; + } + + public final byte rgbToLinear(byte r) { + return (byte) INV_GAMMA_CURVE[r & 0xFF]; + } + + @Override + public final String toString() { + String info = "Gamma function parameters:\n"; + info += String.format(" * Gamma: %7.4f\n", gamma); + info += String.format(" * Breakpoint: %7.4f\n", breakPoint); + info += String.format(" * Slope: %7.4f\n", slope); + info += String.format(" * Slope Match: %7.4f\n", slopeMatch); + info += String.format(" * Segment Offset: %7.4f\n", segmentOffset); + info += "XYZ -> RGB Matrix:\n"; + info += String.format("| %7.4f %7.4f %7.4f|\n", rx, ry, rz); + info += String.format("| %7.4f %7.4f %7.4f|\n", gx, gy, gz); + info += String.format("| %7.4f %7.4f %7.4f|\n", bx, by, bz); + info += "RGB -> XYZ Matrix:\n"; + info += String.format("| %7.4f %7.4f %7.4f|\n", xr, xg, xb); + info += String.format("| %7.4f %7.4f %7.4f|\n", yr, yg, yb); + info += String.format("| %7.4f %7.4f %7.4f|\n", zr, zg, zb); + return info; + } + + public static void main(String[] args) { + System.out.println(SRGB.toString()); + System.out.println(HDTV.toString()); + System.out.println(WIDE_GAMUT.toString()); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/RegularSpectralCurve.java b/src/main/java/org/sunflow/image/RegularSpectralCurve.java new file mode 100644 index 0000000..22b98b7 --- /dev/null +++ b/src/main/java/org/sunflow/image/RegularSpectralCurve.java @@ -0,0 +1,28 @@ +package org.sunflow.image; + +public class RegularSpectralCurve extends SpectralCurve { + private final float[] spectrum; + private final float lambdaMin, lambdaMax; + private final float delta, invDelta; + + public RegularSpectralCurve(float[] spectrum, float lambdaMin, float lambdaMax) { + this.lambdaMin = lambdaMin; + this.lambdaMax = lambdaMax; + this.spectrum = spectrum; + delta = (lambdaMax - lambdaMin) / (spectrum.length - 1); + invDelta = 1 / delta; + } + + @Override + public float sample(float lambda) { + // reject wavelengths outside the valid range + if (lambda < lambdaMin || lambda > lambdaMax) + return 0; + // interpolate the two closest samples linearly + float x = (lambda - lambdaMin) * invDelta; + int b0 = (int) x; + int b1 = Math.min(b0 + 1, spectrum.length - 1); + float dx = x - b0; + return (1 - dx) * spectrum[b0] + dx * spectrum[b1]; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/SpectralCurve.java b/src/main/java/org/sunflow/image/SpectralCurve.java new file mode 100644 index 0000000..a4fe37b --- /dev/null +++ b/src/main/java/org/sunflow/image/SpectralCurve.java @@ -0,0 +1,119 @@ +package org.sunflow.image; + +/** + * This class is an abstract interface to sampled or analytic spectral data. + */ +public abstract class SpectralCurve { + /** + * This function determines the actual spectral curve data. Note that the + * lambda parameter is assumed to be in nanometers. + * + * @param lambda wavelength to sample in nanometers + * @return the value of the spectral curve at this point + */ + public abstract float sample(float lambda); + + private static final int WAVELENGTH_MIN = 360; + private static final int WAVELENGTH_MAX = 830; + private static final double[] CIE_xbar = { 0.000129900000, 0.000232100000, + 0.000414900000, 0.000741600000, 0.001368000000, 0.002236000000, + 0.004243000000, 0.007650000000, 0.014310000000, 0.023190000000, + 0.043510000000, 0.077630000000, 0.134380000000, 0.214770000000, + 0.283900000000, 0.328500000000, 0.348280000000, 0.348060000000, + 0.336200000000, 0.318700000000, 0.290800000000, 0.251100000000, + 0.195360000000, 0.142100000000, 0.095640000000, 0.057950010000, + 0.032010000000, 0.014700000000, 0.004900000000, 0.002400000000, + 0.009300000000, 0.029100000000, 0.063270000000, 0.109600000000, + 0.165500000000, 0.225749900000, 0.290400000000, 0.359700000000, + 0.433449900000, 0.512050100000, 0.594500000000, 0.678400000000, + 0.762100000000, 0.842500000000, 0.916300000000, 0.978600000000, + 1.026300000000, 1.056700000000, 1.062200000000, 1.045600000000, + 1.002600000000, 0.938400000000, 0.854449900000, 0.751400000000, + 0.642400000000, 0.541900000000, 0.447900000000, 0.360800000000, + 0.283500000000, 0.218700000000, 0.164900000000, 0.121200000000, + 0.087400000000, 0.063600000000, 0.046770000000, 0.032900000000, + 0.022700000000, 0.015840000000, 0.011359160000, 0.008110916000, + 0.005790346000, 0.004106457000, 0.002899327000, 0.002049190000, + 0.001439971000, 0.000999949300, 0.000690078600, 0.000476021300, + 0.000332301100, 0.000234826100, 0.000166150500, 0.000117413000, + 0.000083075270, 0.000058706520, 0.000041509940, 0.000029353260, + 0.000020673830, 0.000014559770, 0.000010253980, 0.000007221456, + 0.000005085868, 0.000003581652, 0.000002522525, 0.000001776509, + 0.000001251141, }; + private static final double[] CIE_ybar = { 0.000003917000, 0.000006965000, + 0.000012390000, 0.000022020000, 0.000039000000, 0.000064000000, + 0.000120000000, 0.000217000000, 0.000396000000, 0.000640000000, + 0.001210000000, 0.002180000000, 0.004000000000, 0.007300000000, + 0.011600000000, 0.016840000000, 0.023000000000, 0.029800000000, + 0.038000000000, 0.048000000000, 0.060000000000, 0.073900000000, + 0.090980000000, 0.112600000000, 0.139020000000, 0.169300000000, + 0.208020000000, 0.258600000000, 0.323000000000, 0.407300000000, + 0.503000000000, 0.608200000000, 0.710000000000, 0.793200000000, + 0.862000000000, 0.914850100000, 0.954000000000, 0.980300000000, + 0.994950100000, 1.000000000000, 0.995000000000, 0.978600000000, + 0.952000000000, 0.915400000000, 0.870000000000, 0.816300000000, + 0.757000000000, 0.694900000000, 0.631000000000, 0.566800000000, + 0.503000000000, 0.441200000000, 0.381000000000, 0.321000000000, + 0.265000000000, 0.217000000000, 0.175000000000, 0.138200000000, + 0.107000000000, 0.081600000000, 0.061000000000, 0.044580000000, + 0.032000000000, 0.023200000000, 0.017000000000, 0.011920000000, + 0.008210000000, 0.005723000000, 0.004102000000, 0.002929000000, + 0.002091000000, 0.001484000000, 0.001047000000, 0.000740000000, + 0.000520000000, 0.000361100000, 0.000249200000, 0.000171900000, + 0.000120000000, 0.000084800000, 0.000060000000, 0.000042400000, + 0.000030000000, 0.000021200000, 0.000014990000, 0.000010600000, + 0.000007465700, 0.000005257800, 0.000003702900, 0.000002607800, + 0.000001836600, 0.000001293400, 0.000000910930, 0.000000641530, + 0.000000451810, }; + private static final double[] CIE_zbar = { 0.000606100000, 0.001086000000, + 0.001946000000, 0.003486000000, 0.006450001000, 0.010549990000, + 0.020050010000, 0.036210000000, 0.067850010000, 0.110200000000, + 0.207400000000, 0.371300000000, 0.645600000000, 1.039050100000, + 1.385600000000, 1.622960000000, 1.747060000000, 1.782600000000, + 1.772110000000, 1.744100000000, 1.669200000000, 1.528100000000, + 1.287640000000, 1.041900000000, 0.812950100000, 0.616200000000, + 0.465180000000, 0.353300000000, 0.272000000000, 0.212300000000, + 0.158200000000, 0.111700000000, 0.078249990000, 0.057250010000, + 0.042160000000, 0.029840000000, 0.020300000000, 0.013400000000, + 0.008749999000, 0.005749999000, 0.003900000000, 0.002749999000, + 0.002100000000, 0.001800000000, 0.001650001000, 0.001400000000, + 0.001100000000, 0.001000000000, 0.000800000000, 0.000600000000, + 0.000340000000, 0.000240000000, 0.000190000000, 0.000100000000, + 0.000049999990, 0.000030000000, 0.000020000000, 0.000010000000, + 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, + 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, + 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, + 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, + 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, + 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, + 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, + 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, + 0.000000000000, 0.000000000000, 0.000000000000, 0.000000000000, + 0.000000000000, }; + + private static final int WAVELENGTH_STEP = (WAVELENGTH_MAX - WAVELENGTH_MIN) / (CIE_xbar.length - 1); + + static { + if (WAVELENGTH_STEP * (CIE_xbar.length - 1) != WAVELENGTH_MAX - WAVELENGTH_MIN) { + String err = String.format("Internal error - spectrum static data is inconsistent!\n * min = %d\n * max = %d\n * step = %d\n * num = %d", WAVELENGTH_MIN, WAVELENGTH_MAX, WAVELENGTH_STEP, CIE_xbar.length); + throw new RuntimeException(err); + } + } + + /** + * Convert this curve to a tristimulus CIE XYZ color by integrating against + * the CIE color matching functions. + * + * @return XYZColor that represents this spectra + */ + public final XYZColor toXYZ() { + float X = 0, Y = 0, Z = 0; + for (int i = 0, w = WAVELENGTH_MIN; i < CIE_xbar.length; i++, w += WAVELENGTH_STEP) { + float s = sample(w); + X += s * CIE_xbar[i]; + Y += s * CIE_ybar[i]; + Z += s * CIE_zbar[i]; + } + return new XYZColor(X, Y, Z).mul(WAVELENGTH_STEP); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/XYZColor.java b/src/main/java/org/sunflow/image/XYZColor.java new file mode 100644 index 0000000..010efaa --- /dev/null +++ b/src/main/java/org/sunflow/image/XYZColor.java @@ -0,0 +1,48 @@ +package org.sunflow.image; + +public final class XYZColor { + private float X, Y, Z; + + public XYZColor() { + } + + public XYZColor(float X, float Y, float Z) { + this.X = X; + this.Y = Y; + this.Z = Z; + } + + public final float getX() { + return X; + } + + public final float getY() { + return Y; + } + + public final float getZ() { + return Z; + } + + public final XYZColor mul(float s) { + X *= s; + Y *= s; + Z *= s; + return this; + } + + public final void normalize() { + float XYZ = X + Y + Z; + if (XYZ < 1e-6f) + return; + float s = 1 / XYZ; + X *= s; + Y *= s; + Z *= s; + } + + @Override + public final String toString() { + return String.format("(%.3f, %.3f, %.3f)", X, Y, Z); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/formats/BitmapBlack.java b/src/main/java/org/sunflow/image/formats/BitmapBlack.java new file mode 100644 index 0000000..cb14ea2 --- /dev/null +++ b/src/main/java/org/sunflow/image/formats/BitmapBlack.java @@ -0,0 +1,26 @@ +package org.sunflow.image.formats; + +import org.sunflow.image.Bitmap; +import org.sunflow.image.Color; + +public class BitmapBlack extends Bitmap { + @Override + public int getWidth() { + return 1; + } + + @Override + public int getHeight() { + return 1; + } + + @Override + public Color readColor(int x, int y) { + return Color.BLACK; + } + + @Override + public float readAlpha(int x, int y) { + return 0; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/formats/BitmapG8.java b/src/main/java/org/sunflow/image/formats/BitmapG8.java new file mode 100644 index 0000000..87915cb --- /dev/null +++ b/src/main/java/org/sunflow/image/formats/BitmapG8.java @@ -0,0 +1,35 @@ +package org.sunflow.image.formats; + +import org.sunflow.image.Bitmap; +import org.sunflow.image.Color; + +public class BitmapG8 extends Bitmap { + private int w, h; + private byte[] data; + + public BitmapG8(int w, int h, byte[] data) { + this.w = w; + this.h = h; + this.data = data; + } + + @Override + public int getWidth() { + return w; + } + + @Override + public int getHeight() { + return h; + } + + @Override + public Color readColor(int x, int y) { + return new Color((data[x + y * w] & 0xFF) * INV255); + } + + @Override + public float readAlpha(int x, int y) { + return 1; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/formats/BitmapGA8.java b/src/main/java/org/sunflow/image/formats/BitmapGA8.java new file mode 100644 index 0000000..bd25fcb --- /dev/null +++ b/src/main/java/org/sunflow/image/formats/BitmapGA8.java @@ -0,0 +1,29 @@ +package org.sunflow.image.formats; + +import org.sunflow.image.Bitmap; +import org.sunflow.image.Color; + +public class BitmapGA8 extends Bitmap { + private int w, h; + private byte[] data; + + @Override + public int getWidth() { + return w; + } + + @Override + public int getHeight() { + return h; + } + + @Override + public Color readColor(int x, int y) { + return new Color((data[2 * (x + y * w) + 0] & 0xFF) * INV255); + } + + @Override + public float readAlpha(int x, int y) { + return (data[2 * (x + y * w) + 1] & 0xFF) * INV255; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/formats/BitmapRGB8.java b/src/main/java/org/sunflow/image/formats/BitmapRGB8.java new file mode 100644 index 0000000..3978153 --- /dev/null +++ b/src/main/java/org/sunflow/image/formats/BitmapRGB8.java @@ -0,0 +1,39 @@ +package org.sunflow.image.formats; + +import org.sunflow.image.Bitmap; +import org.sunflow.image.Color; + +public class BitmapRGB8 extends Bitmap { + private int w, h; + private byte[] data; + + public BitmapRGB8(int w, int h, byte[] data) { + this.w = w; + this.h = h; + this.data = data; + } + + @Override + public int getWidth() { + return w; + } + + @Override + public int getHeight() { + return h; + } + + @Override + public Color readColor(int x, int y) { + int index = 3 * (x + y * w); + float r = (data[index + 0] & 0xFF) * INV255; + float g = (data[index + 1] & 0xFF) * INV255; + float b = (data[index + 2] & 0xFF) * INV255; + return new Color(r, g, b); + } + + @Override + public float readAlpha(int x, int y) { + return 1; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/formats/BitmapRGBA8.java b/src/main/java/org/sunflow/image/formats/BitmapRGBA8.java new file mode 100644 index 0000000..652aecb --- /dev/null +++ b/src/main/java/org/sunflow/image/formats/BitmapRGBA8.java @@ -0,0 +1,39 @@ +package org.sunflow.image.formats; + +import org.sunflow.image.Bitmap; +import org.sunflow.image.Color; + +public class BitmapRGBA8 extends Bitmap { + private int w, h; + private byte[] data; + + public BitmapRGBA8(int w, int h, byte[] data) { + this.w = w; + this.h = h; + this.data = data; + } + + @Override + public int getWidth() { + return w; + } + + @Override + public int getHeight() { + return h; + } + + @Override + public Color readColor(int x, int y) { + int index = 4 * (x + y * w); + float r = (data[index + 0] & 0xFF) * INV255; + float g = (data[index + 1] & 0xFF) * INV255; + float b = (data[index + 2] & 0xFF) * INV255; + return new Color(r, g, b); + } + + @Override + public float readAlpha(int x, int y) { + return (data[4 * (x + y * w) + 3] & 0xFF) * INV255; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/formats/BitmapRGBE.java b/src/main/java/org/sunflow/image/formats/BitmapRGBE.java new file mode 100644 index 0000000..efdbaf2 --- /dev/null +++ b/src/main/java/org/sunflow/image/formats/BitmapRGBE.java @@ -0,0 +1,56 @@ +package org.sunflow.image.formats; + +import org.sunflow.image.Bitmap; +import org.sunflow.image.Color; + +public class BitmapRGBE extends Bitmap { + private int w, h; + private int[] data; + private static final float[] EXPONENT = new float[256]; + + static { + EXPONENT[0] = 0; + for (int i = 1; i < 256; i++) { + float f = 1.0f; + int e = i - (128 + 8); + if (e > 0) + for (int j = 0; j < e; j++) + f *= 2.0f; + else + for (int j = 0; j < -e; j++) + f *= 0.5f; + EXPONENT[i] = f; + } + } + + public BitmapRGBE(int w, int h, int[] data) { + this.w = w; + this.h = h; + this.data = data; + } + + @Override + public int getWidth() { + return w; + } + + @Override + public int getHeight() { + return h; + } + + @Override + public Color readColor(int x, int y) { + int rgbe = data[x + y * w]; + float f = EXPONENT[rgbe & 0xFF]; + float r = f * ((rgbe >>> 24) + 0.5f); + float g = f * (((rgbe >> 16) & 0xFF) + 0.5f); + float b = f * (((rgbe >> 8) & 0xFF) + 0.5f); + return new Color(r, g, b); + } + + @Override + public float readAlpha(int x, int y) { + return 1; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/formats/BitmapXYZ.java b/src/main/java/org/sunflow/image/formats/BitmapXYZ.java new file mode 100644 index 0000000..c237038 --- /dev/null +++ b/src/main/java/org/sunflow/image/formats/BitmapXYZ.java @@ -0,0 +1,37 @@ +package org.sunflow.image.formats; + +import org.sunflow.image.Bitmap; +import org.sunflow.image.Color; +import org.sunflow.image.XYZColor; + +public class BitmapXYZ extends Bitmap { + private int w, h; + private float[] data; + + public BitmapXYZ(int w, int h, float[] data) { + this.w = w; + this.h = h; + this.data = data; + } + + @Override + public int getWidth() { + return w; + } + + @Override + public int getHeight() { + return h; + } + + @Override + public Color readColor(int x, int y) { + int index = 3 * (x + y * w); + return Color.NATIVE_SPACE.convertXYZtoRGB(new XYZColor(data[index], data[index + 1], data[index + 2])).mul(0.1f); + } + + @Override + public float readAlpha(int x, int y) { + return 1; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/formats/GenericBitmap.java b/src/main/java/org/sunflow/image/formats/GenericBitmap.java new file mode 100644 index 0000000..d00a96c --- /dev/null +++ b/src/main/java/org/sunflow/image/formats/GenericBitmap.java @@ -0,0 +1,71 @@ +package org.sunflow.image.formats; + +import java.io.IOException; + +import org.sunflow.PluginRegistry; +import org.sunflow.image.Bitmap; +import org.sunflow.image.BitmapWriter; +import org.sunflow.image.Color; +import org.sunflow.system.FileUtils; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +/** + * This is a generic and inefficient bitmap format which may be used for + * debugging purposes (dumping small images), when memory usage is not a + * concern. + */ +public class GenericBitmap extends Bitmap { + private int w, h; + private Color[] color; + private float[] alpha; + + public GenericBitmap(int w, int h) { + this.w = w; + this.h = h; + color = new Color[w * h]; + alpha = new float[w * h]; + } + + @Override + public int getWidth() { + return w; + } + + @Override + public int getHeight() { + return h; + } + + @Override + public Color readColor(int x, int y) { + return color[x + y * w]; + } + + @Override + public float readAlpha(int x, int y) { + return alpha[x + y * w]; + } + + public void writePixel(int x, int y, Color c, float a) { + color[x + y * w] = c; + alpha[x + y * w] = a; + } + + public void save(String filename) { + String extension = FileUtils.getExtension(filename); + BitmapWriter writer = PluginRegistry.bitmapWriterPlugins.createObject(extension); + if (writer == null) { + UI.printError(Module.IMG, "Unable to save file \"%s\" - unknown file format: %s", filename, extension); + return; + } + try { + writer.openFile(filename); + writer.writeHeader(w, h, Math.max(w, h)); + writer.writeTile(0, 0, w, h, color, alpha); + writer.closeFile(); + } catch (IOException e) { + UI.printError(Module.IMG, "Unable to save file \"%s\" - %s", filename, e.getLocalizedMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/readers/BMPBitmapReader.java b/src/main/java/org/sunflow/image/readers/BMPBitmapReader.java new file mode 100644 index 0000000..bed1104 --- /dev/null +++ b/src/main/java/org/sunflow/image/readers/BMPBitmapReader.java @@ -0,0 +1,38 @@ +package org.sunflow.image.readers; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import org.sunflow.image.Bitmap; +import org.sunflow.image.BitmapReader; +import org.sunflow.image.Color; +import org.sunflow.image.formats.BitmapRGB8; + +public class BMPBitmapReader implements BitmapReader { + public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException { + // regular image, load using Java api - ignore alpha channel + BufferedImage bi = ImageIO.read(new File(filename)); + int width = bi.getWidth(); + int height = bi.getHeight(); + byte[] pixels = new byte[3 * width * height]; + for (int y = 0, index = 0; y < height; y++) { + for (int x = 0; x < width; x++, index += 3) { + int argb = bi.getRGB(x, height - 1 - y); + pixels[index + 0] = (byte) (argb >> 16); + pixels[index + 1] = (byte) (argb >> 8); + pixels[index + 2] = (byte) argb; + } + } + if (!isLinear) { + for (int index = 0; index < pixels.length; index += 3) { + pixels[index + 0] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 0]); + pixels[index + 1] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 1]); + pixels[index + 2] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 2]); + } + } + return new BitmapRGB8(width, height, pixels); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/readers/HDRBitmapReader.java b/src/main/java/org/sunflow/image/readers/HDRBitmapReader.java new file mode 100644 index 0000000..4ed9368 --- /dev/null +++ b/src/main/java/org/sunflow/image/readers/HDRBitmapReader.java @@ -0,0 +1,148 @@ +package org.sunflow.image.readers; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.sunflow.image.Bitmap; +import org.sunflow.image.BitmapReader; +import org.sunflow.image.formats.BitmapRGBE; + +public class HDRBitmapReader implements BitmapReader { + public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException { + // load radiance rgbe file + InputStream f = new BufferedInputStream(new FileInputStream(filename)); + // parse header + boolean parseWidth = false, parseHeight = false; + int width = 0, height = 0; + int last = 0; + while (width == 0 || height == 0 || last != '\n') { + int n = f.read(); + switch (n) { + case 'Y': + parseHeight = last == '-'; + parseWidth = false; + break; + case 'X': + parseHeight = false; + parseWidth = last == '+'; + break; + case ' ': + parseWidth &= width == 0; + parseHeight &= height == 0; + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + if (parseHeight) + height = 10 * height + (n - '0'); + else if (parseWidth) + width = 10 * width + (n - '0'); + break; + default: + parseWidth = parseHeight = false; + break; + } + last = n; + } + // allocate image + int[] pixels = new int[width * height]; + if ((width < 8) || (width > 0x7fff)) { + // run length encoding is not allowed so read flat + readFlatRGBE(f, 0, width * height, pixels); + } else { + int rasterPos = 0; + int numScanlines = height; + int[] scanlineBuffer = new int[4 * width]; + while (numScanlines > 0) { + int r = f.read(); + int g = f.read(); + int b = f.read(); + int e = f.read(); + if ((r != 2) || (g != 2) || ((b & 0x80) != 0)) { + // this file is not run length encoded + pixels[rasterPos] = (r << 24) | (g << 16) | (b << 8) | e; + readFlatRGBE(f, rasterPos + 1, width * numScanlines - 1, pixels); + break; + } + + if (((b << 8) | e) != width) + throw new BitmapFormatException("Invalid scanline width"); + int p = 0; + // read each of the four channels for the scanline into + // the buffer + for (int i = 0; i < 4; i++) { + if (p % width != 0) + throw new BitmapFormatException("Unaligned access to scanline data"); + int end = (i + 1) * width; + while (p < end) { + int b0 = f.read(); + int b1 = f.read(); + if (b0 > 128) { + // a run of the same value + int count = b0 - 128; + if ((count == 0) || (count > (end - p))) + throw new BitmapFormatException("Bad scanline data - invalid RLE run"); + + while (count-- > 0) { + scanlineBuffer[p] = b1; + p++; + } + } else { + // a non-run + int count = b0; + if ((count == 0) || (count > (end - p))) + throw new BitmapFormatException("Bad scanline data - invalid count"); + scanlineBuffer[p] = b1; + p++; + if (--count > 0) { + for (int x = 0; x < count; x++) + scanlineBuffer[p + x] = f.read(); + p += count; + } + } + } + } + // now convert data from buffer into floats + for (int i = 0; i < width; i++) { + r = scanlineBuffer[i]; + g = scanlineBuffer[i + width]; + b = scanlineBuffer[i + 2 * width]; + e = scanlineBuffer[i + 3 * width]; + pixels[rasterPos] = (r << 24) | (g << 16) | (b << 8) | e; + rasterPos++; + } + numScanlines--; + } + } + f.close(); + // flip image + for (int y = 0, i = 0, ir = (height - 1) * width; y < height / 2; y++, ir -= width) { + for (int x = 0, i2 = ir; x < width; x++, i++, i2++) { + int t = pixels[i]; + pixels[i] = pixels[i2]; + pixels[i2] = t; + } + } + return new BitmapRGBE(width, height, pixels); + } + + private void readFlatRGBE(InputStream f, int rasterPos, int numPixels, int[] pixels) throws IOException { + while (numPixels-- > 0) { + int r = f.read(); + int g = f.read(); + int b = f.read(); + int e = f.read(); + pixels[rasterPos] = (r << 24) | (g << 16) | (b << 8) | e; + rasterPos++; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/readers/IGIBitmapReader.java b/src/main/java/org/sunflow/image/readers/IGIBitmapReader.java new file mode 100644 index 0000000..051ef03 --- /dev/null +++ b/src/main/java/org/sunflow/image/readers/IGIBitmapReader.java @@ -0,0 +1,94 @@ +package org.sunflow.image.readers; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.sunflow.image.Bitmap; +import org.sunflow.image.BitmapReader; +import org.sunflow.image.formats.BitmapXYZ; + +/** + * Reads images in Indigo's native XYZ format. + * http://www2.indigorenderer.com/joomla/forum/viewtopic.php?p=11430 + */ +public class IGIBitmapReader implements BitmapReader { + public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException { + InputStream stream = new BufferedInputStream(new FileInputStream(filename)); + // read header + int magic = read32i(stream); + int version = read32i(stream); + stream.skip(8); // skip number of samples (double value) + int width = read32i(stream); + int height = read32i(stream); + int superSample = read32i(stream); // super sample factor + int compression = read32i(stream); + int dataSize = read32i(stream); + int colorSpace = read32i(stream); + stream.skip(5000); // skip the rest of the header (unused for now) + // error checking + if (magic != 66613373) + throw new BitmapFormatException("wrong magic: " + magic); + if (version != 1) + throw new BitmapFormatException("unsupported version: " + version); + if (compression != 0) + throw new BitmapFormatException("unsupported compression: " + compression); + if (colorSpace != 0) + throw new BitmapFormatException("unsupported color space: " + colorSpace); + if (dataSize != (width * height * 12)) + throw new BitmapFormatException("invalid data block size: " + dataSize); + if (width <= 0 || height <= 0) + throw new BitmapFormatException("invalid image size: " + width + "x" + height); + if (superSample <= 0) + throw new BitmapFormatException("invalid super sample factor: " + superSample); + if ((width % superSample) != 0 || (height % superSample) != 0) + throw new BitmapFormatException("invalid image size: " + width + "x" + height); + float[] xyz = new float[width * height * 3]; + for (int y = 0, i = 3 * (height - 1) * width; y < height; y++, i -= 6 * width) { + for (int x = 0; x < width; x++, i += 3) { + xyz[i + 0] = read32f(stream); + xyz[i + 1] = read32f(stream); + xyz[i + 2] = read32f(stream); + } + } + stream.close(); + if (superSample > 1) { + // rescale image (basic box filtering) + float[] rescaled = new float[xyz.length / (superSample * superSample)]; + float inv = 1.0f / (superSample * superSample); + for (int y = 0, i = 0; y < height; y += superSample) { + for (int x = 0; x < width; x += superSample, i += 3) { + float X = 0; + float Y = 0; + float Z = 0; + for (int sy = 0; sy < superSample; sy++) { + for (int sx = 0; sx < superSample; sx++) { + int offset = 3 * ((x + sx + (y + sy) * width)); + X += xyz[offset + 0]; + Y += xyz[offset + 1]; + Z += xyz[offset + 2]; + } + } + rescaled[i + 0] = X * inv; + rescaled[i + 1] = Y * inv; + rescaled[i + 2] = Z * inv; + } + } + return new BitmapXYZ(width / superSample, height / superSample, rescaled); + } else + return new BitmapXYZ(width, height, xyz); + } + + private static final int read32i(InputStream stream) throws IOException { + int i = stream.read(); + i |= stream.read() << 8; + i |= stream.read() << 16; + i |= stream.read() << 24; + return i; + } + + private static final float read32f(InputStream stream) throws IOException { + return Float.intBitsToFloat(read32i(stream)); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/readers/JPGBitmapReader.java b/src/main/java/org/sunflow/image/readers/JPGBitmapReader.java new file mode 100644 index 0000000..79635bf --- /dev/null +++ b/src/main/java/org/sunflow/image/readers/JPGBitmapReader.java @@ -0,0 +1,38 @@ +package org.sunflow.image.readers; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import org.sunflow.image.Bitmap; +import org.sunflow.image.BitmapReader; +import org.sunflow.image.Color; +import org.sunflow.image.formats.BitmapRGB8; + +public class JPGBitmapReader implements BitmapReader { + public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException { + // regular image, load using Java api - ignore alpha channel + BufferedImage bi = ImageIO.read(new File(filename)); + int width = bi.getWidth(); + int height = bi.getHeight(); + byte[] pixels = new byte[3 * width * height]; + for (int y = 0, index = 0; y < height; y++) { + for (int x = 0; x < width; x++, index += 3) { + int argb = bi.getRGB(x, height - 1 - y); + pixels[index + 0] = (byte) (argb >> 16); + pixels[index + 1] = (byte) (argb >> 8); + pixels[index + 2] = (byte) argb; + } + } + if (!isLinear) { + for (int index = 0; index < pixels.length; index += 3) { + pixels[index + 0] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 0]); + pixels[index + 1] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 1]); + pixels[index + 2] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 2]); + } + } + return new BitmapRGB8(width, height, pixels); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/readers/PNGBitmapReader.java b/src/main/java/org/sunflow/image/readers/PNGBitmapReader.java new file mode 100644 index 0000000..5ba566c --- /dev/null +++ b/src/main/java/org/sunflow/image/readers/PNGBitmapReader.java @@ -0,0 +1,39 @@ +package org.sunflow.image.readers; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import org.sunflow.image.Bitmap; +import org.sunflow.image.BitmapReader; +import org.sunflow.image.Color; +import org.sunflow.image.formats.BitmapRGBA8; + +public class PNGBitmapReader implements BitmapReader { + public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException { + // regular image, load using Java api + BufferedImage bi = ImageIO.read(new File(filename)); + int width = bi.getWidth(); + int height = bi.getHeight(); + byte[] pixels = new byte[4 * width * height]; + for (int y = 0, index = 0; y < height; y++) { + for (int x = 0; x < width; x++, index += 4) { + int argb = bi.getRGB(x, height - 1 - y); + pixels[index + 0] = (byte) (argb >> 16); + pixels[index + 1] = (byte) (argb >> 8); + pixels[index + 2] = (byte) argb; + pixels[index + 3] = (byte) (argb >> 24); + } + } + if (!isLinear) { + for (int index = 0; index < pixels.length; index += 4) { + pixels[index + 0] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 0]); + pixels[index + 1] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 1]); + pixels[index + 2] = Color.NATIVE_SPACE.rgbToLinear(pixels[index + 2]); + } + } + return new BitmapRGBA8(width, height, pixels); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/readers/TGABitmapReader.java b/src/main/java/org/sunflow/image/readers/TGABitmapReader.java new file mode 100644 index 0000000..2c4145f --- /dev/null +++ b/src/main/java/org/sunflow/image/readers/TGABitmapReader.java @@ -0,0 +1,131 @@ +package org.sunflow.image.readers; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.sunflow.image.Bitmap; +import org.sunflow.image.BitmapReader; +import org.sunflow.image.Color; +import org.sunflow.image.formats.BitmapG8; +import org.sunflow.image.formats.BitmapRGB8; +import org.sunflow.image.formats.BitmapRGBA8; + +public class TGABitmapReader implements BitmapReader { + private static final int[] CHANNEL_INDEX = { 2, 1, 0, 3 }; + + public Bitmap load(String filename, boolean isLinear) throws IOException, BitmapFormatException { + InputStream f = new BufferedInputStream(new FileInputStream(filename)); + byte[] read = new byte[4]; + + // read header + int idsize = f.read(); + int cmaptype = f.read(); // cmap byte (unsupported) + if (cmaptype != 0) + throw new BitmapFormatException(String.format("Colormapping (type: %d) is unsupported", cmaptype)); + int datatype = f.read(); + + // colormap info (5 bytes ignored) + f.read(); + f.read(); + f.read(); + f.read(); + f.read(); + + f.read(); // xstart, 16 bits (ignored) + f.read(); + f.read(); // ystart, 16 bits (ignored) + f.read(); + + // read resolution + int width = f.read(); + width |= f.read() << 8; + int height = f.read(); + height |= f.read() << 8; + + int bits = f.read(); + int bpp = bits / 8; + + int imgdscr = f.read(); + + // skip image ID if present + if (idsize != 0) + f.skip(idsize); + + // allocate byte buffer to hold the image + byte[] pixels = new byte[width * height * bpp]; + if (datatype == 2 || datatype == 3) { + if (bpp != 1 && bpp != 3 && bpp != 4) + throw new BitmapFormatException(String.format("Invalid bit depth in uncompressed TGA: %d", bits)); + // uncompressed image + for (int ptr = 0; ptr < pixels.length; ptr += bpp) { + // read bytes + f.read(read, 0, bpp); + for (int i = 0; i < bpp; i++) + pixels[ptr + CHANNEL_INDEX[i]] = read[i]; + } + } else if (datatype == 10) { + if (bpp != 3 && bpp != 4) + throw new BitmapFormatException(String.format("Invalid bit depth in run-length encoded TGA: %d", bits)); + // RLE encoded image + for (int ptr = 0; ptr < pixels.length;) { + int rle = f.read(); + int num = 1 + (rle & 0x7F); + if ((rle & 0x80) != 0) { + // rle packet - decode length and copy pixel + f.read(read, 0, bpp); + for (int j = 0; j < num; j++) { + for (int i = 0; i < bpp; i++) + pixels[ptr + CHANNEL_INDEX[i]] = read[i]; + ptr += bpp; + } + } else { + // raw packet - decode length and read pixels + for (int j = 0; j < num; j++) { + f.read(read, 0, bpp); + for (int i = 0; i < bpp; i++) + pixels[ptr + CHANNEL_INDEX[i]] = read[i]; + ptr += bpp; + } + } + } + } else + throw new BitmapFormatException(String.format("Unsupported TGA image type: %d", datatype)); + + if (!isLinear) { + // apply reverse correction + for (int ptr = 0; ptr < pixels.length; ptr += bpp) { + for (int i = 0; i < 3 && i < bpp; i++) + pixels[ptr + i] = Color.NATIVE_SPACE.rgbToLinear(pixels[ptr + i]); + } + } + + // should image be flipped in Y? + if ((imgdscr & 32) == 32) { + for (int y = 0, pix_ptr = 0; y < (height / 2); y++) { + int bot_ptr = bpp * (height - y - 1) * width; + for (int x = 0; x < width; x++) { + for (int i = 0; i < bpp; i++) { + byte t = pixels[pix_ptr + i]; + pixels[pix_ptr + i] = pixels[bot_ptr + i]; + pixels[bot_ptr + i] = t; + } + pix_ptr += bpp; + bot_ptr += bpp; + } + } + + } + f.close(); + switch (bpp) { + case 1: + return new BitmapG8(width, height, pixels); + case 3: + return new BitmapRGB8(width, height, pixels); + case 4: + return new BitmapRGBA8(width, height, pixels); + } + throw new BitmapFormatException("Inconsistent code in TGA reader"); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/writers/EXRBitmapWriter.java b/src/main/java/org/sunflow/image/writers/EXRBitmapWriter.java new file mode 100644 index 0000000..d3b4f9f --- /dev/null +++ b/src/main/java/org/sunflow/image/writers/EXRBitmapWriter.java @@ -0,0 +1,377 @@ +package org.sunflow.image.writers; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Arrays; +import java.util.zip.Deflater; + +import org.sunflow.image.BitmapWriter; +import org.sunflow.image.Color; +import org.sunflow.system.ByteUtil; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public class EXRBitmapWriter implements BitmapWriter { + private static final byte HALF = 1; + private static final byte FLOAT = 2; + private static final int HALF_SIZE = 2; + private static final int FLOAT_SIZE = 4; + private final static int OE_MAGIC = 20000630; + private final static int OE_EXR_VERSION = 2; + private final static int OE_TILED_FLAG = 0x00000200; + private static final int NO_COMPRESSION = 0; + private static final int RLE_COMPRESSION = 1; + private static final int ZIP_COMPRESSION = 3; + private static final int RLE_MIN_RUN = 3; + private static final int RLE_MAX_RUN = 127; + + private String filename; + private RandomAccessFile file; + private long[][] tileOffsets; + private long tileOffsetsPosition; + private int tilesX; + private int tilesY; + private int tileSize; + private int compression; + private byte channelType; + private int channelSize; + private byte[] tmpbuf; + private byte[] comprbuf; + + public EXRBitmapWriter() { + // default settings + configure("compression", "zip"); + configure("channeltype", "half"); + } + + public void configure(String option, String value) { + if (option.equals("compression")) { + if (value.equals("none")) + compression = NO_COMPRESSION; + else if (value.equals("rle")) + compression = RLE_COMPRESSION; + else if (value.equals("zip")) + compression = ZIP_COMPRESSION; + else { + UI.printWarning(Module.IMG, "EXR - Compression type was not recognized - defaulting to zip"); + compression = ZIP_COMPRESSION; + } + } else if (option.equals("channeltype")) { + if (value.equals("float")) { + channelType = FLOAT; + channelSize = FLOAT_SIZE; + } else if (value.equals("half")) { + channelType = HALF; + channelSize = HALF_SIZE; + } else { + UI.printWarning(Module.DISP, "EXR - Channel type was not recognized - defaulting to float"); + channelType = FLOAT; + channelSize = FLOAT_SIZE; + } + } + } + + public void openFile(String filename) throws IOException { + this.filename = filename == null ? "output.exr" : filename; + } + + public void writeHeader(int width, int height, int tileSize) throws IOException, UnsupportedOperationException { + file = new RandomAccessFile(filename, "rw"); + file.setLength(0); + if (tileSize <= 0) + throw new UnsupportedOperationException("Can't use OpenEXR bitmap writer without buckets."); + writeRGBAHeader(width, height, tileSize); + } + + public void writeTile(int x, int y, int w, int h, Color[] color, float[] alpha) throws IOException { + int tx = x / tileSize; + int ty = y / tileSize; + writeEXRTile(tx, ty, w, h, color, alpha); + } + + public void closeFile() throws IOException { + writeTileOffsets(); + file.close(); + } + + private void writeRGBAHeader(int w, int h, int tileSize) throws IOException { + byte[] chanOut = { 0, channelType, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0 }; + + file.write(ByteUtil.get4Bytes(OE_MAGIC)); + + file.write(ByteUtil.get4Bytes(OE_EXR_VERSION | OE_TILED_FLAG)); + + file.write("channels".getBytes()); + file.write(0); + file.write("chlist".getBytes()); + file.write(0); + file.write(ByteUtil.get4Bytes(73)); + file.write("R".getBytes()); + file.write(chanOut); + file.write("G".getBytes()); + file.write(chanOut); + file.write("B".getBytes()); + file.write(chanOut); + file.write("A".getBytes()); + file.write(chanOut); + file.write(0); + + // compression + file.write("compression".getBytes()); + file.write(0); + file.write("compression".getBytes()); + file.write(0); + file.write(1); + file.write(ByteUtil.get4BytesInv(compression)); + + // datawindow =~ image size + file.write("dataWindow".getBytes()); + file.write(0); + file.write("box2i".getBytes()); + file.write(0); + file.write(ByteUtil.get4Bytes(0x10)); + file.write(ByteUtil.get4Bytes(0)); + file.write(ByteUtil.get4Bytes(0)); + file.write(ByteUtil.get4Bytes(w - 1)); + file.write(ByteUtil.get4Bytes(h - 1)); + + // dispwindow -> look at openexr.com for more info + file.write("displayWindow".getBytes()); + file.write(0); + file.write("box2i".getBytes()); + file.write(0); + file.write(ByteUtil.get4Bytes(0x10)); + file.write(ByteUtil.get4Bytes(0)); + file.write(ByteUtil.get4Bytes(0)); + file.write(ByteUtil.get4Bytes(w - 1)); + file.write(ByteUtil.get4Bytes(h - 1)); + + /* + * lines in increasing y order = 0 decreasing would be 1 + */ + file.write("lineOrder".getBytes()); + file.write(0); + file.write("lineOrder".getBytes()); + file.write(0); + file.write(1); + file.write(ByteUtil.get4BytesInv(2)); + + file.write("pixelAspectRatio".getBytes()); + file.write(0); + file.write("float".getBytes()); + file.write(0); + file.write(ByteUtil.get4Bytes(4)); + file.write(ByteUtil.get4Bytes(Float.floatToIntBits(1))); + + // meaningless to a flat (2D) image + file.write("screenWindowCenter".getBytes()); + file.write(0); + file.write("v2f".getBytes()); + file.write(0); + file.write(ByteUtil.get4Bytes(8)); + file.write(ByteUtil.get4Bytes(Float.floatToIntBits(0))); + file.write(ByteUtil.get4Bytes(Float.floatToIntBits(0))); + + // meaningless to a flat (2D) image + file.write("screenWindowWidth".getBytes()); + file.write(0); + file.write("float".getBytes()); + file.write(0); + file.write(ByteUtil.get4Bytes(4)); + file.write(ByteUtil.get4Bytes(Float.floatToIntBits(1))); + + this.tileSize = tileSize; + + tilesX = ((w + tileSize - 1) / tileSize); + tilesY = ((h + tileSize - 1) / tileSize); + + /* + * twice the space for the compressing buffer, as for ex. the compressor + * can actually increase the size of the data :) If that happens though, + * it is not saved into the file, but discarded + */ + tmpbuf = new byte[tileSize * tileSize * channelSize * 4]; + comprbuf = new byte[tileSize * tileSize * channelSize * 4 * 2]; + + tileOffsets = new long[tilesX][tilesY]; + + file.write("tiles".getBytes()); + file.write(0); + file.write("tiledesc".getBytes()); + file.write(0); + file.write(ByteUtil.get4Bytes(9)); + + file.write(ByteUtil.get4Bytes(tileSize)); + file.write(ByteUtil.get4Bytes(tileSize)); + + // ONE_LEVEL tiles, ROUNDING_MODE = not important + file.write(0); + + // an attribute with a name of 0 to end the list + file.write(0); + + // save a pointer to where the tileOffsets are stored and write dummy + // fillers for now + tileOffsetsPosition = file.getFilePointer(); + writeTileOffsets(); + } + + private void writeTileOffsets() throws IOException { + file.seek(tileOffsetsPosition); + for (int ty = 0; ty < tilesY; ty++) + for (int tx = 0; tx < tilesX; tx++) + file.write(ByteUtil.get8Bytes(tileOffsets[tx][ty])); + } + + private synchronized void writeEXRTile(int tileX, int tileY, int w, int h, Color[] tile, float[] alpha) throws IOException { + byte[] rgb; + + // setting comprSize to max integer so without compression things + // don't go awry + int pixptr = 0, writeSize = 0, comprSize = Integer.MAX_VALUE; + int tileRangeX = (tileSize < w) ? tileSize : w; + int tileRangeY = (tileSize < h) ? tileSize : h; + int channelBase = tileRangeX * channelSize; + + // lets see if the alignment matches, you can comment this out if + // need be + if ((tileSize != tileRangeX) && (tileX == 0)) + System.out.print(" bad X alignment "); + if ((tileSize != tileRangeY) && (tileY == 0)) + System.out.print(" bad Y alignment "); + + tileOffsets[tileX][tileY] = file.getFilePointer(); + + // the tile header: tile's x&y coordinate, levels x&y coordinate and + // tilesize + file.write(ByteUtil.get4Bytes(tileX)); + file.write(ByteUtil.get4Bytes(tileY)); + file.write(ByteUtil.get4Bytes(0)); + file.write(ByteUtil.get4Bytes(0)); + + // just in case + Arrays.fill(tmpbuf, (byte) 0); + + for (int ty = 0; ty < tileRangeY; ty++) { + for (int tx = 0; tx < tileRangeX; tx++) { + float[] rgbf = tile[tx + ty * tileRangeX].getRGB(); + if (channelType == FLOAT) { + rgb = ByteUtil.get4Bytes(Float.floatToRawIntBits(alpha[tx + ty * tileRangeX])); + tmpbuf[pixptr + 0] = rgb[0]; + tmpbuf[pixptr + 1] = rgb[1]; + tmpbuf[pixptr + 2] = rgb[2]; + tmpbuf[pixptr + 3] = rgb[3]; + } else if (channelType == HALF) { + rgb = ByteUtil.get2Bytes(ByteUtil.floatToHalf(alpha[tx + ty * tileRangeX])); + tmpbuf[pixptr + 0] = rgb[0]; + tmpbuf[pixptr + 1] = rgb[1]; + } + for (int component = 1; component <= 3; component++) { + if (channelType == FLOAT) { + rgb = ByteUtil.get4Bytes(Float.floatToRawIntBits(rgbf[3 - component])); + tmpbuf[(channelBase * component) + pixptr + 0] = rgb[0]; + tmpbuf[(channelBase * component) + pixptr + 1] = rgb[1]; + tmpbuf[(channelBase * component) + pixptr + 2] = rgb[2]; + tmpbuf[(channelBase * component) + pixptr + 3] = rgb[3]; + } else if (channelType == HALF) { + rgb = ByteUtil.get2Bytes(ByteUtil.floatToHalf(rgbf[3 - component])); + tmpbuf[(channelBase * component) + pixptr + 0] = rgb[0]; + tmpbuf[(channelBase * component) + pixptr + 1] = rgb[1]; + } + } + pixptr += channelSize; + } + pixptr += (tileRangeX * channelSize * 3); + } + + writeSize = tileRangeX * tileRangeY * channelSize * 4; + + if (compression != NO_COMPRESSION) + comprSize = compress(compression, tmpbuf, writeSize, comprbuf); + + // lastly, write the size of the tile and the tile itself + // (compressed or not) + if (comprSize < writeSize) { + file.write(ByteUtil.get4Bytes(comprSize)); + file.write(comprbuf, 0, comprSize); + } else { + file.write(ByteUtil.get4Bytes(writeSize)); + file.write(tmpbuf, 0, writeSize); + } + } + + private static final int compress(int tp, byte[] in, int inSize, byte[] out) { + if (inSize == 0) + return 0; + + int t1 = 0, t2 = (inSize + 1) / 2; + int inPtr = 0, ret; + byte[] tmp = new byte[inSize]; + + // zip and rle treat the data first, in the same way so I'm not + // repeating the code + if ((tp == ZIP_COMPRESSION) || (tp == RLE_COMPRESSION)) { + // reorder the pixel data ~ straight from ImfZipCompressor.cpp :) + while (true) { + if (inPtr < inSize) + tmp[t1++] = in[inPtr++]; + else + break; + + if (inPtr < inSize) + tmp[t2++] = in[inPtr++]; + else + break; + } + + // Predictor ~ straight from ImfZipCompressor.cpp :) + t1 = 1; + int p = tmp[t1 - 1]; + while (t1 < inSize) { + int d = tmp[t1] - p + (128 + 256); + p = tmp[t1]; + tmp[t1] = (byte) d; + t1++; + } + } + + // We'll just jump from here to the wanted compress/decompress stuff if + // need be + switch (tp) { + case ZIP_COMPRESSION: + Deflater def = new Deflater(Deflater.DEFAULT_COMPRESSION, false); + def.setInput(tmp, 0, inSize); + def.finish(); + ret = def.deflate(out); + return ret; + case RLE_COMPRESSION: + return rleCompress(tmp, inSize, out); + default: + return -1; + } + } + + private static final int rleCompress(byte[] in, int inLen, byte[] out) { + int runStart = 0, runEnd = 1, outWrite = 0; + while (runStart < inLen) { + while (runEnd < inLen && in[runStart] == in[runEnd] && (runEnd - runStart - 1) < RLE_MAX_RUN) + runEnd++; + if (runEnd - runStart >= RLE_MIN_RUN) { + // Compressable run + out[outWrite++] = (byte) ((runEnd - runStart) - 1); + out[outWrite++] = in[runStart]; + runStart = runEnd; + } else { + // Uncompressable run + while (runEnd < inLen && (((runEnd + 1) >= inLen || in[runEnd] != in[runEnd + 1]) || ((runEnd + 2) >= inLen || in[runEnd + 1] != in[runEnd + 2])) && (runEnd - runStart) < RLE_MAX_RUN) + runEnd++; + out[outWrite++] = (byte) (runStart - runEnd); + while (runStart < runEnd) + out[outWrite++] = in[runStart++]; + } + runEnd++; + } + return outWrite; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/writers/HDRBitmapWriter.java b/src/main/java/org/sunflow/image/writers/HDRBitmapWriter.java new file mode 100644 index 0000000..32c22f5 --- /dev/null +++ b/src/main/java/org/sunflow/image/writers/HDRBitmapWriter.java @@ -0,0 +1,51 @@ +package org.sunflow.image.writers; + +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.sunflow.image.BitmapWriter; +import org.sunflow.image.Color; +import org.sunflow.image.ColorEncoder; + +public class HDRBitmapWriter implements BitmapWriter { + private String filename; + private int width, height; + private int[] data; + + public void configure(String option, String value) { + } + + public void openFile(String filename) throws IOException { + this.filename = filename; + } + + public void writeHeader(int width, int height, int tileSize) throws IOException, UnsupportedOperationException { + this.width = width; + this.height = height; + data = new int[width * height]; + } + + public void writeTile(int x, int y, int w, int h, Color[] color, float[] alpha) throws IOException { + int[] tileData = ColorEncoder.encodeRGBE(color); + for (int j = 0, index = 0, pixel = x + y * width; j < h; j++, pixel += width - w) + for (int i = 0; i < w; i++, index++, pixel++) + data[pixel] = tileData[index]; + } + + public void closeFile() throws IOException { + OutputStream f = new BufferedOutputStream(new FileOutputStream(filename)); + f.write("#?RGBE\n".getBytes()); + f.write("FORMAT=32-bit_rle_rgbe\n\n".getBytes()); + f.write(("-Y " + height + " +X " + width + "\n").getBytes()); + for (int i = 0; i < data.length; i++) { + int rgbe = data[i]; + f.write(rgbe >> 24); + f.write(rgbe >> 16); + f.write(rgbe >> 8); + f.write(rgbe); + } + f.close(); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/writers/IGIBitmapWriter.java b/src/main/java/org/sunflow/image/writers/IGIBitmapWriter.java new file mode 100644 index 0000000..bd3211e --- /dev/null +++ b/src/main/java/org/sunflow/image/writers/IGIBitmapWriter.java @@ -0,0 +1,73 @@ +package org.sunflow.image.writers; + +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.sunflow.image.BitmapWriter; +import org.sunflow.image.Color; +import org.sunflow.image.XYZColor; + +/** + * Writes images in Indigo's native XYZ format. + * http://www2.indigorenderer.com/joomla/forum/viewtopic.php?p=11430 + */ +public class IGIBitmapWriter implements BitmapWriter { + private String filename; + private int width, height; + private float[] xyz; + + public void configure(String option, String value) { + } + + public void openFile(String filename) throws IOException { + this.filename = filename; + } + + public void writeHeader(int width, int height, int tileSize) throws IOException, UnsupportedOperationException { + this.width = width; + this.height = height; + xyz = new float[width * height * 3]; + } + + public void writeTile(int x, int y, int w, int h, Color[] color, float[] alpha) throws IOException { + for (int j = 0, index = 0, pixel = 3 * (x + y * width); j < h; j++, pixel += 3 * (width - w)) { + for (int i = 0; i < w; i++, index++, pixel += 3) { + XYZColor c = Color.NATIVE_SPACE.convertRGBtoXYZ(color[index]); + xyz[pixel + 0] = c.getX(); + xyz[pixel + 1] = c.getY(); + xyz[pixel + 2] = c.getZ(); + } + } + } + + public void closeFile() throws IOException { + OutputStream stream = new BufferedOutputStream(new FileOutputStream(filename)); + write32(stream, 66613373); // magic number + write32(stream, 1); // version + write32(stream, 0); // this should be a double - assume it won't be used + write32(stream, 0); + write32(stream, width); + write32(stream, height); + write32(stream, 1); // super sampling factor + write32(stream, 0); // compression + write32(stream, width * height * 12); // data size + write32(stream, 0); // colorspace + stream.write(new byte[5000]); + for (float f : xyz) + write32(stream, f); + stream.close(); + } + + private static final void write32(OutputStream stream, int i) throws IOException { + stream.write(i & 0xFF); + stream.write((i >> 8) & 0xFF); + stream.write((i >> 16) & 0xFF); + stream.write((i >> 24) & 0xFF); + } + + private static final void write32(OutputStream stream, float f) throws IOException { + write32(stream, Float.floatToIntBits(f)); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/writers/PNGBitmapWriter.java b/src/main/java/org/sunflow/image/writers/PNGBitmapWriter.java new file mode 100644 index 0000000..a638443 --- /dev/null +++ b/src/main/java/org/sunflow/image/writers/PNGBitmapWriter.java @@ -0,0 +1,36 @@ +package org.sunflow.image.writers; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import org.sunflow.image.BitmapWriter; +import org.sunflow.image.Color; + +public class PNGBitmapWriter implements BitmapWriter { + private String filename; + private BufferedImage image; + + public void configure(String option, String value) { + } + + public void openFile(String filename) throws IOException { + this.filename = filename; + } + + public void writeHeader(int width, int height, int tileSize) throws IOException, UnsupportedOperationException { + image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + } + + public void writeTile(int x, int y, int w, int h, Color[] color, float[] alpha) throws IOException { + for (int j = 0, index = 0; j < h; j++) + for (int i = 0; i < w; i++, index++) + image.setRGB(x + i, y + j, color[index].copy().mul(1.0f / alpha[index]).toNonLinear().toRGBA(alpha[index])); + } + + public void closeFile() throws IOException { + ImageIO.write(image, "png", new File(filename)); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/image/writers/TGABitmapWriter.java b/src/main/java/org/sunflow/image/writers/TGABitmapWriter.java new file mode 100644 index 0000000..e2c5a5c --- /dev/null +++ b/src/main/java/org/sunflow/image/writers/TGABitmapWriter.java @@ -0,0 +1,62 @@ +package org.sunflow.image.writers; + +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.sunflow.image.BitmapWriter; +import org.sunflow.image.Color; +import org.sunflow.image.ColorEncoder; + +public class TGABitmapWriter implements BitmapWriter { + private String filename; + private int width, height; + private byte[] data; + + public void configure(String option, String value) { + } + + public void openFile(String filename) throws IOException { + this.filename = filename; + } + + public void writeHeader(int width, int height, int tileSize) throws IOException, UnsupportedOperationException { + this.width = width; + this.height = height; + data = new byte[width * height * 4]; // RGBA8 + } + + public void writeTile(int x, int y, int w, int h, Color[] color, float[] alpha) throws IOException { + color = ColorEncoder.unlinearize(color); // gamma correction + byte[] tileData = ColorEncoder.quantizeRGBA8(color, alpha); + for (int j = 0, index = 0; j < h; j++) { + int imageIndex = 4 * (x + (height - 1 - (y + j)) * width); + for (int i = 0; i < w; i++, index += 4, imageIndex += 4) { + // swap bytes around so buffer is in native BGRA order + data[imageIndex + 0] = tileData[index + 2]; + data[imageIndex + 1] = tileData[index + 1]; + data[imageIndex + 2] = tileData[index + 0]; + data[imageIndex + 3] = tileData[index + 3]; + } + } + } + + public void closeFile() throws IOException { + // actually write the file from here + OutputStream f = new BufferedOutputStream(new FileOutputStream(filename)); + // no id, no colormap, uncompressed 32bpp RGBA + byte[] tgaHeader = { 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + f.write(tgaHeader); + // then the size info + f.write(width & 0xFF); + f.write((width >> 8) & 0xFF); + f.write(height & 0xFF); + f.write((height >> 8) & 0xFF); + // bitsperpixel and filler + f.write(32); + f.write(0); + f.write(data); // write image data bytes (already in BGRA order) + f.close(); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/math/BoundingBox.java b/src/main/java/org/sunflow/math/BoundingBox.java new file mode 100644 index 0000000..992d17b --- /dev/null +++ b/src/main/java/org/sunflow/math/BoundingBox.java @@ -0,0 +1,315 @@ +package org.sunflow.math; + +/** + * 3D axis-aligned bounding box. Stores only the minimum and maximum corner + * points. + */ +public class BoundingBox { + private Point3 minimum; + private Point3 maximum; + + /** + * Creates an empty box. The minimum point will have all components set to + * positive infinity, and the maximum will have all components set to + * negative infinity. + */ + public BoundingBox() { + minimum = new Point3(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY); + maximum = new Point3(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); + } + + /** + * Creates a copy of the given box. + * + * @param b bounding box to copy + */ + public BoundingBox(BoundingBox b) { + minimum = new Point3(b.minimum); + maximum = new Point3(b.maximum); + } + + /** + * Creates a bounding box containing only the specified point. + * + * @param p point to include + */ + public BoundingBox(Point3 p) { + this(p.x, p.y, p.z); + } + + /** + * Creates a bounding box containing only the specified point. + * + * @param x x coordinate of the point to include + * @param y y coordinate of the point to include + * @param z z coordinate of the point to include + */ + public BoundingBox(float x, float y, float z) { + minimum = new Point3(x, y, z); + maximum = new Point3(x, y, z); + } + + /** + * Creates a bounding box centered around the origin. + * + * @param size half edge length of the bounding box + */ + public BoundingBox(float size) { + minimum = new Point3(-size, -size, -size); + maximum = new Point3(size, size, size); + } + + /** + * Gets the minimum corner of the box. That is the corner of smallest + * coordinates on each axis. Note that the returned reference is not cloned + * for efficiency purposes so care must be taken not to change the + * coordinates of the point. + * + * @return a reference to the minimum corner + */ + public final Point3 getMinimum() { + return minimum; + } + + /** + * Gets the maximum corner of the box. That is the corner of largest + * coordinates on each axis. Note that the returned reference is not cloned + * for efficiency purposes so care must be taken not to change the + * coordinates of the point. + * + * @return a reference to the maximum corner + */ + public final Point3 getMaximum() { + return maximum; + } + + /** + * Gets the center of the box, computed as (min + max) / 2. + * + * @return a reference to the center of the box + */ + public final Point3 getCenter() { + return Point3.mid(minimum, maximum, new Point3()); + } + + /** + * Gets a corner of the bounding box. The index scheme uses the binary + * representation of the index to decide which corner to return. Corner 0 is + * equivalent to the minimum and corner 7 is equivalent to the maximum. + * + * @param i a corner index, from 0 to 7 + * @return the corresponding corner + */ + public final Point3 getCorner(int i) { + float x = (i & 1) == 0 ? minimum.x : maximum.x; + float y = (i & 2) == 0 ? minimum.y : maximum.y; + float z = (i & 4) == 0 ? minimum.z : maximum.z; + return new Point3(x, y, z); + } + + /** + * Gets a specific coordinate of the surface's bounding box. + * + * @param i index of a side from 0 to 5 + * @return value of the request bounding box side + */ + public final float getBound(int i) { + switch (i) { + case 0: + return minimum.x; + case 1: + return maximum.x; + case 2: + return minimum.y; + case 3: + return maximum.y; + case 4: + return minimum.z; + case 5: + return maximum.z; + default: + return 0; + } + } + + /** + * Gets the extents vector for the box. This vector is computed as (max - + * min). Its coordinates are always positive and represent the dimensions of + * the box along the three axes. + * + * @return a refreence to the extent vector + * @see org.sunflow.math.Vector3#length() + */ + public final Vector3 getExtents() { + return Point3.sub(maximum, minimum, new Vector3()); + } + + /** + * Gets the surface area of the box. + * + * @return surface area + */ + public final float getArea() { + Vector3 w = getExtents(); + float ax = Math.max(w.x, 0); + float ay = Math.max(w.y, 0); + float az = Math.max(w.z, 0); + return 2 * (ax * ay + ay * az + az * ax); + } + + /** + * Gets the box's volume + * + * @return volume + */ + public final float getVolume() { + Vector3 w = getExtents(); + float ax = Math.max(w.x, 0); + float ay = Math.max(w.y, 0); + float az = Math.max(w.z, 0); + return ax * ay * az; + } + + /** + * Enlarge the bounding box by the minimum possible amount to avoid numeric + * precision related problems. + */ + public final void enlargeUlps() { + final float eps = 0.0001f; + minimum.x -= Math.max(eps, Math.ulp(minimum.x)); + minimum.y -= Math.max(eps, Math.ulp(minimum.y)); + minimum.z -= Math.max(eps, Math.ulp(minimum.z)); + maximum.x += Math.max(eps, Math.ulp(maximum.x)); + maximum.y += Math.max(eps, Math.ulp(maximum.y)); + maximum.z += Math.max(eps, Math.ulp(maximum.z)); + } + + /** + * Returns true when the box has just been initialized, and + * is still empty. This method might also return true if the state of the + * box becomes inconsistent and some component of the minimum corner is + * larger than the corresponding coordinate of the maximum corner. + * + * @return true if the box is empty, false + * otherwise + */ + public final boolean isEmpty() { + return (maximum.x < minimum.x) || (maximum.y < minimum.y) || (maximum.z < minimum.z); + } + + /** + * Returns true if the specified bounding box intersects this + * one. The boxes are treated as volumes, so a box inside another will + * return true. Returns false if the parameter is + * null. + * + * @param b box to be tested for intersection + * @return true if the boxes overlap, false + * otherwise + */ + public final boolean intersects(BoundingBox b) { + return ((b != null) && (minimum.x <= b.maximum.x) && (maximum.x >= b.minimum.x) && (minimum.y <= b.maximum.y) && (maximum.y >= b.minimum.y) && (minimum.z <= b.maximum.z) && (maximum.z >= b.minimum.z)); + } + + /** + * Checks to see if the specified {@link org.sunflow.math.Point3 point}is + * inside the volume defined by this box. Returns false if + * the parameter is null. + * + * @param p point to be tested for containment + * @return true if the point is inside the box, + * false otherwise + */ + public final boolean contains(Point3 p) { + return ((p != null) && (p.x >= minimum.x) && (p.x <= maximum.x) && (p.y >= minimum.y) && (p.y <= maximum.y) && (p.z >= minimum.z) && (p.z <= maximum.z)); + } + + /** + * Check to see if the specified point is inside the volume defined by this + * box. + * + * @param x x coordinate of the point to be tested + * @param y y coordinate of the point to be tested + * @param z z coordinate of the point to be tested + * @return true if the point is inside the box, + * false otherwise + */ + public final boolean contains(float x, float y, float z) { + return ((x >= minimum.x) && (x <= maximum.x) && (y >= minimum.y) && (y <= maximum.y) && (z >= minimum.z) && (z <= maximum.z)); + } + + /** + * Changes the extents of the box as needed to include the given + * {@link org.sunflow.math.Point3 point}into this box. Does nothing if the + * parameter is null. + * + * @param p point to be included + */ + public final void include(Point3 p) { + if (p != null) { + if (p.x < minimum.x) + minimum.x = p.x; + if (p.x > maximum.x) + maximum.x = p.x; + if (p.y < minimum.y) + minimum.y = p.y; + if (p.y > maximum.y) + maximum.y = p.y; + if (p.z < minimum.z) + minimum.z = p.z; + if (p.z > maximum.z) + maximum.z = p.z; + } + } + + /** + * Changes the extents of the box as needed to include the given point into + * this box. + * + * @param x x coordinate of the point + * @param y y coordinate of the point + * @param z z coordinate of the point + */ + public final void include(float x, float y, float z) { + if (x < minimum.x) + minimum.x = x; + if (x > maximum.x) + maximum.x = x; + if (y < minimum.y) + minimum.y = y; + if (y > maximum.y) + maximum.y = y; + if (z < minimum.z) + minimum.z = z; + if (z > maximum.z) + maximum.z = z; + } + + /** + * Changes the extents of the box as needed to include the given box into + * this box. Does nothing if the parameter is null. + * + * @param b box to be included + */ + public final void include(BoundingBox b) { + if (b != null) { + if (b.minimum.x < minimum.x) + minimum.x = b.minimum.x; + if (b.maximum.x > maximum.x) + maximum.x = b.maximum.x; + if (b.minimum.y < minimum.y) + minimum.y = b.minimum.y; + if (b.maximum.y > maximum.y) + maximum.y = b.maximum.y; + if (b.minimum.z < minimum.z) + minimum.z = b.minimum.z; + if (b.maximum.z > maximum.z) + maximum.z = b.maximum.z; + } + } + + @Override + public final String toString() { + return String.format("(%.2f, %.2f, %.2f) to (%.2f, %.2f, %.2f)", minimum.x, minimum.y, minimum.z, maximum.x, maximum.y, maximum.z); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/math/MathUtils.java b/src/main/java/org/sunflow/math/MathUtils.java new file mode 100644 index 0000000..f763d98 --- /dev/null +++ b/src/main/java/org/sunflow/math/MathUtils.java @@ -0,0 +1,131 @@ +package org.sunflow.math; + +public final class MathUtils { + private MathUtils() { + } + + public static final int clamp(int x, int min, int max) { + if (x > max) + return max; + if (x > min) + return x; + return min; + } + + public static final float clamp(float x, float min, float max) { + if (x > max) + return max; + if (x > min) + return x; + return min; + } + + public static final double clamp(double x, double min, double max) { + if (x > max) + return max; + if (x > min) + return x; + return min; + } + + public static final int min(int a, int b, int c) { + if (a > b) + a = b; + if (a > c) + a = c; + return a; + } + + public static final float min(float a, float b, float c) { + if (a > b) + a = b; + if (a > c) + a = c; + return a; + } + + public static final double min(double a, double b, double c) { + if (a > b) + a = b; + if (a > c) + a = c; + return a; + } + + public static final float min(float a, float b, float c, float d) { + if (a > b) + a = b; + if (a > c) + a = c; + if (a > d) + a = d; + return a; + } + + public static final int max(int a, int b, int c) { + if (a < b) + a = b; + if (a < c) + a = c; + return a; + } + + public static final float max(float a, float b, float c) { + if (a < b) + a = b; + if (a < c) + a = c; + return a; + } + + public static final double max(double a, double b, double c) { + if (a < b) + a = b; + if (a < c) + a = c; + return a; + } + + public static final float max(float a, float b, float c, float d) { + if (a < b) + a = b; + if (a < c) + a = c; + if (a < d) + a = d; + return a; + } + + public static final float smoothStep(float a, float b, float x) { + if (x <= a) + return 0; + if (x >= b) + return 1; + float t = clamp((x - a) / (b - a), 0.0f, 1.0f); + return t * t * (3 - 2 * t); + } + + public static final float frac(float x) { + return x < 0 ? x - (int) x + 1 : x - (int) x; + } + + /** + * Computes a fast approximation to Math.pow(a, b). Adapted + * from http://www.dctsystems.co.uk/Software/power.html. + * + * @param a a positive number + * @param b a number + * @return a^b + */ + public static final float fastPow(float a, float b) { + // adapted from: http://www.dctsystems.co.uk/Software/power.html + float x = Float.floatToRawIntBits(a); + x *= 1.0f / (1 << 23); + x = x - 127; + float y = x - (int) Math.floor(x); + b *= x + (y - y * y) * 0.346607f; + y = b - (int) Math.floor(b); + y = (y - y * y) * 0.33971f; + return Float.intBitsToFloat((int) ((b + 127 - y) * (1 << 23))); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/math/Matrix4.java b/src/main/java/org/sunflow/math/Matrix4.java new file mode 100644 index 0000000..8d74b7b --- /dev/null +++ b/src/main/java/org/sunflow/math/Matrix4.java @@ -0,0 +1,561 @@ +package org.sunflow.math; + +/** + * This class is used to represent general affine transformations in 3D. The + * bottom row of the matrix is assumed to be [0,0,0,1]. Note that the rotation + * matrices assume a right-handed convention. + */ +public final class Matrix4 { + // matrix elements, m(row,col) + private float m00; + private float m01; + private float m02; + private float m03; + private float m10; + private float m11; + private float m12; + private float m13; + private float m20; + private float m21; + private float m22; + private float m23; + + // usefull constant matrices + public static final Matrix4 ZERO = new Matrix4(); + public static final Matrix4 IDENTITY = Matrix4.scale(1); + + /** + * Creates an empty matrix. All elements are 0. + */ + private Matrix4() { + } + + /** + * Creates a matrix with the specified elements + * + * @param m00 value at row 0, col 0 + * @param m01 value at row 0, col 1 + * @param m02 value at row 0, col 2 + * @param m03 value at row 0, col 3 + * @param m10 value at row 1, col 0 + * @param m11 value at row 1, col 1 + * @param m12 value at row 1, col 2 + * @param m13 value at row 1, col 3 + * @param m20 value at row 2, col 0 + * @param m21 value at row 2, col 1 + * @param m22 value at row 2, col 2 + * @param m23 value at row 2, col 3 + */ + public Matrix4(float m00, float m01, float m02, float m03, float m10, float m11, float m12, float m13, float m20, float m21, float m22, float m23) { + this.m00 = m00; + this.m01 = m01; + this.m02 = m02; + this.m03 = m03; + this.m10 = m10; + this.m11 = m11; + this.m12 = m12; + this.m13 = m13; + this.m20 = m20; + this.m21 = m21; + this.m22 = m22; + this.m23 = m23; + } + + /** + * Initialize a matrix from the specified 16 element array. The matrix may + * be given in row or column major form. + * + * @param m a 16 element array in row or column major form + * @param rowMajor true if the array is in row major form, + * falseif it is in column major form + */ + public Matrix4(float[] m, boolean rowMajor) { + if (rowMajor) { + m00 = m[0]; + m01 = m[1]; + m02 = m[2]; + m03 = m[3]; + m10 = m[4]; + m11 = m[5]; + m12 = m[6]; + m13 = m[7]; + m20 = m[8]; + m21 = m[9]; + m22 = m[10]; + m23 = m[11]; + if (m[12] != 0 || m[13] != 0 || m[14] != 0 || m[15] != 1) + throw new RuntimeException(String.format("Matrix is not affine! Bottom row is: [%.3f, %.3f, %.3f, %.3f]", m[12], m[13], m[14], m[15])); + } else { + m00 = m[0]; + m01 = m[4]; + m02 = m[8]; + m03 = m[12]; + m10 = m[1]; + m11 = m[5]; + m12 = m[9]; + m13 = m[13]; + m20 = m[2]; + m21 = m[6]; + m22 = m[10]; + m23 = m[14]; + if (m[3] != 0 || m[7] != 0 || m[11] != 0 || m[15] != 1) + throw new RuntimeException(String.format("Matrix is not affine! Bottom row is: [%.3f, %.3f, %.3f, %.3f]", m[12], m[13], m[14], m[15])); + } + } + + public final boolean isIndentity() { + return equals(IDENTITY); + } + + public final boolean equals(Matrix4 m) { + if (m == null) + return false; + if (this == m) + return true; + return m00 == m.m00 && m01 == m.m01 && m02 == m.m02 && m03 == m.m03 && m10 == m.m10 && m11 == m.m11 && m12 == m.m12 && m13 == m.m13 && m20 == m.m20 && m21 == m.m21 && m22 == m.m22 && m23 == m.m23; + } + + public final float[] asRowMajor() { + return new float[] { m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, + m22, m23, 0, 0, 0, 1 }; + } + + public final float[] asColMajor() { + return new float[] { m00, m10, m20, 0, m01, m11, m21, 0, m02, m12, m22, + 0, m03, m13, m23, 1 }; + } + + /** + * Compute the matrix determinant. + * + * @return determinant of this matrix + */ + public final float determinant() { + float A0 = m00 * m11 - m01 * m10; + float A1 = m00 * m12 - m02 * m10; + float A3 = m01 * m12 - m02 * m11; + + return A0 * m22 - A1 * m21 + A3 * m20; + } + + /** + * Compute the inverse of this matrix and return it as a new object. If the + * matrix is not invertible, null is returned. + * + * @return the inverse of this matrix, or null if not + * invertible + */ + public final Matrix4 inverse() { + float A0 = m00 * m11 - m01 * m10; + float A1 = m00 * m12 - m02 * m10; + float A3 = m01 * m12 - m02 * m11; + float det = A0 * m22 - A1 * m21 + A3 * m20; + if (Math.abs(det) < 1e-12f) + return null; // matrix is not invertible + float invDet = 1 / det; + float A2 = m00 * m13 - m03 * m10; + float A4 = m01 * m13 - m03 * m11; + float A5 = m02 * m13 - m03 * m12; + Matrix4 inv = new Matrix4(); + inv.m00 = (+m11 * m22 - m12 * m21) * invDet; + inv.m10 = (-m10 * m22 + m12 * m20) * invDet; + inv.m20 = (+m10 * m21 - m11 * m20) * invDet; + inv.m01 = (-m01 * m22 + m02 * m21) * invDet; + inv.m11 = (+m00 * m22 - m02 * m20) * invDet; + inv.m21 = (-m00 * m21 + m01 * m20) * invDet; + inv.m02 = +A3 * invDet; + inv.m12 = -A1 * invDet; + inv.m22 = +A0 * invDet; + inv.m03 = (-m21 * A5 + m22 * A4 - m23 * A3) * invDet; + inv.m13 = (+m20 * A5 - m22 * A2 + m23 * A1) * invDet; + inv.m23 = (-m20 * A4 + m21 * A2 - m23 * A0) * invDet; + return inv; + } + + /** + * Computes this*m and return the result as a new Matrix4 + * + * @param m right hand side of the multiplication + * @return a new Matrix4 object equal to this*m + */ + public final Matrix4 multiply(Matrix4 m) { + // matrix multiplication is m[r][c] = (row[r]).(col[c]) + float rm00 = m00 * m.m00 + m01 * m.m10 + m02 * m.m20; + float rm01 = m00 * m.m01 + m01 * m.m11 + m02 * m.m21; + float rm02 = m00 * m.m02 + m01 * m.m12 + m02 * m.m22; + float rm03 = m00 * m.m03 + m01 * m.m13 + m02 * m.m23 + m03; + + float rm10 = m10 * m.m00 + m11 * m.m10 + m12 * m.m20; + float rm11 = m10 * m.m01 + m11 * m.m11 + m12 * m.m21; + float rm12 = m10 * m.m02 + m11 * m.m12 + m12 * m.m22; + float rm13 = m10 * m.m03 + m11 * m.m13 + m12 * m.m23 + m13; + + float rm20 = m20 * m.m00 + m21 * m.m10 + m22 * m.m20; + float rm21 = m20 * m.m01 + m21 * m.m11 + m22 * m.m21; + float rm22 = m20 * m.m02 + m21 * m.m12 + m22 * m.m22; + float rm23 = m20 * m.m03 + m21 * m.m13 + m22 * m.m23 + m23; + + return new Matrix4(rm00, rm01, rm02, rm03, rm10, rm11, rm12, rm13, rm20, rm21, rm22, rm23); + } + + /** + * Transforms each corner of the specified axis-aligned bounding box and + * returns a new bounding box which incloses the transformed corners. + * + * @param b original bounding box + * @return a new BoundingBox object which encloses the transform version of + * b + */ + public final BoundingBox transform(BoundingBox b) { + if (b.isEmpty()) + return new BoundingBox(); + // special case extreme corners + BoundingBox rb = new BoundingBox(transformP(b.getMinimum())); + rb.include(transformP(b.getMaximum())); + // do internal corners + for (int i = 1; i < 7; i++) + rb.include(transformP(b.getCorner(i))); + return rb; + } + + /** + * Computes this*v and returns the result as a new Vector3 object. This + * method assumes the bottom row of the matrix is [0,0,0,1]. + * + * @param v vector to multiply + * @return a new Vector3 object equal to this*v + */ + public final Vector3 transformV(Vector3 v) { + Vector3 rv = new Vector3(); + rv.x = m00 * v.x + m01 * v.y + m02 * v.z; + rv.y = m10 * v.x + m11 * v.y + m12 * v.z; + rv.z = m20 * v.x + m21 * v.y + m22 * v.z; + return rv; + } + + /** + * Computes (this^T)*v and returns the result as a new Vector3 object. This + * method assumes the bottom row of the matrix is [0,0,0,1]. + * + * @param v vector to multiply + * @return a new Vector3 object equal to (this^T)*v + */ + public final Vector3 transformTransposeV(Vector3 v) { + Vector3 rv = new Vector3(); + rv.x = m00 * v.x + m10 * v.y + m20 * v.z; + rv.y = m01 * v.x + m11 * v.y + m21 * v.z; + rv.z = m02 * v.x + m12 * v.y + m22 * v.z; + return rv; + } + + /** + * Computes this*p and returns the result as a new Point3 object. This + * method assumes the bottom row of the matrix is [0,0,0,1]. + * + * @param p point to multiply + * @return a new Point3 object equal to this*v + */ + public final Point3 transformP(Point3 p) { + Point3 rp = new Point3(); + rp.x = m00 * p.x + m01 * p.y + m02 * p.z + m03; + rp.y = m10 * p.x + m11 * p.y + m12 * p.z + m13; + rp.z = m20 * p.x + m21 * p.y + m22 * p.z + m23; + return rp; + } + + /** + * Computes the x component of this*(x,y,z,0). + * + * @param x x coordinate of the vector to multiply + * @param y y coordinate of the vector to multiply + * @param z z coordinate of the vector to multiply + * @return x coordinate transformation result + */ + public final float transformVX(float x, float y, float z) { + return m00 * x + m01 * y + m02 * z; + } + + /** + * Computes the y component of this*(x,y,z,0). + * + * @param x x coordinate of the vector to multiply + * @param y y coordinate of the vector to multiply + * @param z z coordinate of the vector to multiply + * @return y coordinate transformation result + */ + public final float transformVY(float x, float y, float z) { + return m10 * x + m11 * y + m12 * z; + } + + /** + * Computes the z component of this*(x,y,z,0). + * + * @param x x coordinate of the vector to multiply + * @param y y coordinate of the vector to multiply + * @param z z coordinate of the vector to multiply + * @return z coordinate transformation result + */ + public final float transformVZ(float x, float y, float z) { + return m20 * x + m21 * y + m22 * z; + } + + /** + * Computes the x component of (this^T)*(x,y,z,0). + * + * @param x x coordinate of the vector to multiply + * @param y y coordinate of the vector to multiply + * @param z z coordinate of the vector to multiply + * @return x coordinate transformation result + */ + public final float transformTransposeVX(float x, float y, float z) { + return m00 * x + m10 * y + m20 * z; + } + + /** + * Computes the y component of (this^T)*(x,y,z,0). + * + * @param x x coordinate of the vector to multiply + * @param y y coordinate of the vector to multiply + * @param z z coordinate of the vector to multiply + * @return y coordinate transformation result + */ + public final float transformTransposeVY(float x, float y, float z) { + return m01 * x + m11 * y + m21 * z; + } + + /** + * Computes the z component of (this^T)*(x,y,z,0). + * + * @param x x coordinate of the vector to multiply + * @param y y coordinate of the vector to multiply + * @param z z coordinate of the vector to multiply + * @return zcoordinate transformation result + */ + public final float transformTransposeVZ(float x, float y, float z) { + return m02 * x + m12 * y + m22 * z; + } + + /** + * Computes the x component of this*(x,y,z,1). + * + * @param x x coordinate of the vector to multiply + * @param y y coordinate of the vector to multiply + * @param z z coordinate of the vector to multiply + * @return x coordinate transformation result + */ + public final float transformPX(float x, float y, float z) { + return m00 * x + m01 * y + m02 * z + m03; + } + + /** + * Computes the y component of this*(x,y,z,1). + * + * @param x x coordinate of the vector to multiply + * @param y y coordinate of the vector to multiply + * @param z z coordinate of the vector to multiply + * @return y coordinate transformation result + */ + public final float transformPY(float x, float y, float z) { + return m10 * x + m11 * y + m12 * z + m13; + } + + /** + * Computes the z component of this*(x,y,z,1). + * + * @param x x coordinate of the vector to multiply + * @param y y coordinate of the vector to multiply + * @param z z coordinate of the vector to multiply + * @return z coordinate transformation result + */ + public final float transformPZ(float x, float y, float z) { + return m20 * x + m21 * y + m22 * z + m23; + } + + /** + * Create a translation matrix for the specified vector. + * + * @param x x component of translation + * @param y y component of translation + * @param z z component of translation + * @return a new Matrix4 object representing the translation + */ + public final static Matrix4 translation(float x, float y, float z) { + Matrix4 m = new Matrix4(); + m.m00 = m.m11 = m.m22 = 1; + m.m03 = x; + m.m13 = y; + m.m23 = z; + return m; + } + + /** + * Creates a rotation matrix about the X axis. + * + * @param theta angle to rotate about the X axis in radians + * @return a new Matrix4 object representing the rotation + */ + public final static Matrix4 rotateX(float theta) { + Matrix4 m = new Matrix4(); + float s = (float) Math.sin(theta); + float c = (float) Math.cos(theta); + m.m00 = 1; + m.m11 = m.m22 = c; + m.m12 = -s; + m.m21 = +s; + return m; + } + + /** + * Creates a rotation matrix about the Y axis. + * + * @param theta angle to rotate about the Y axis in radians + * @return a new Matrix4 object representing the rotation + */ + public final static Matrix4 rotateY(float theta) { + Matrix4 m = new Matrix4(); + float s = (float) Math.sin(theta); + float c = (float) Math.cos(theta); + m.m11 = 1; + m.m00 = m.m22 = c; + m.m02 = +s; + m.m20 = -s; + return m; + } + + /** + * Creates a rotation matrix about the Z axis. + * + * @param theta angle to rotate about the Z axis in radians + * @return a new Matrix4 object representing the rotation + */ + public final static Matrix4 rotateZ(float theta) { + Matrix4 m = new Matrix4(); + float s = (float) Math.sin(theta); + float c = (float) Math.cos(theta); + m.m22 = 1; + m.m00 = m.m11 = c; + m.m01 = -s; + m.m10 = +s; + return m; + } + + /** + * Creates a rotation matrix about the specified axis. The axis vector need + * not be normalized. + * + * @param x x component of the axis vector + * @param y y component of the axis vector + * @param z z component of the axis vector + * @param theta angle to rotate about the axis in radians + * @return a new Matrix4 object representing the rotation + */ + public final static Matrix4 rotate(float x, float y, float z, float theta) { + Matrix4 m = new Matrix4(); + float invLen = 1 / (float) Math.sqrt(x * x + y * y + z * z); + x *= invLen; + y *= invLen; + z *= invLen; + float s = (float) Math.sin(theta); + float c = (float) Math.cos(theta); + float t = 1 - c; + m.m00 = t * x * x + c; + m.m11 = t * y * y + c; + m.m22 = t * z * z + c; + float txy = t * x * y; + float sz = s * z; + m.m01 = txy - sz; + m.m10 = txy + sz; + float txz = t * x * z; + float sy = s * y; + m.m02 = txz + sy; + m.m20 = txz - sy; + float tyz = t * y * z; + float sx = s * x; + m.m12 = tyz - sx; + m.m21 = tyz + sx; + return m; + } + + /** + * Create a uniform scaling matrix. + * + * @param s scale factor for all three axes + * @return a new Matrix4 object representing the uniform scale + */ + public final static Matrix4 scale(float s) { + Matrix4 m = new Matrix4(); + m.m00 = m.m11 = m.m22 = s; + return m; + } + + /** + * Creates a non-uniform scaling matrix. + * + * @param sx scale factor in the x dimension + * @param sy scale factor in the y dimension + * @param sz scale factor in the z dimension + * @return a new Matrix4 object representing the non-uniform scale + */ + public final static Matrix4 scale(float sx, float sy, float sz) { + Matrix4 m = new Matrix4(); + m.m00 = sx; + m.m11 = sy; + m.m22 = sz; + return m; + } + + /** + * Creates a rotation matrix from an OrthonormalBasis. + * + * @param basis + */ + public final static Matrix4 fromBasis(OrthoNormalBasis basis) { + Matrix4 m = new Matrix4(); + Vector3 u = basis.transform(new Vector3(1, 0, 0)); + Vector3 v = basis.transform(new Vector3(0, 1, 0)); + Vector3 w = basis.transform(new Vector3(0, 0, 1)); + m.m00 = u.x; + m.m01 = v.x; + m.m02 = w.x; + m.m10 = u.y; + m.m11 = v.y; + m.m12 = w.y; + m.m20 = u.z; + m.m21 = v.z; + m.m22 = w.z; + return m; + } + + /** + * Creates a camera positioning matrix from the given eye and target points + * and up vector. + * + * @param eye location of the eye + * @param target location of the target + * @param up vector pointing upwards + * @return + */ + public final static Matrix4 lookAt(Point3 eye, Point3 target, Vector3 up) { + Matrix4 m = Matrix4.fromBasis(OrthoNormalBasis.makeFromWV(Point3.sub(eye, target, new Vector3()), up)); + return Matrix4.translation(eye.x, eye.y, eye.z).multiply(m); + } + + public final static Matrix4 blend(Matrix4 m0, Matrix4 m1, float t) { + Matrix4 m = new Matrix4(); + m.m00 = (1 - t) * m0.m00 + t * m1.m00; + m.m01 = (1 - t) * m0.m01 + t * m1.m01; + m.m02 = (1 - t) * m0.m02 + t * m1.m02; + m.m03 = (1 - t) * m0.m03 + t * m1.m03; + + m.m10 = (1 - t) * m0.m10 + t * m1.m10; + m.m11 = (1 - t) * m0.m11 + t * m1.m11; + m.m12 = (1 - t) * m0.m12 + t * m1.m12; + m.m13 = (1 - t) * m0.m13 + t * m1.m13; + + m.m20 = (1 - t) * m0.m20 + t * m1.m20; + m.m21 = (1 - t) * m0.m21 + t * m1.m21; + m.m22 = (1 - t) * m0.m22 + t * m1.m22; + m.m23 = (1 - t) * m0.m23 + t * m1.m23; + return m; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/math/MovingMatrix4.java b/src/main/java/org/sunflow/math/MovingMatrix4.java new file mode 100644 index 0000000..4906cf7 --- /dev/null +++ b/src/main/java/org/sunflow/math/MovingMatrix4.java @@ -0,0 +1,115 @@ +package org.sunflow.math; + +/** + * This class describes a transformation matrix that changes over time. Note + * that while unlimited motion segments are supported, it is assumed that these + * segments represent equidistant samples within a given time range. + */ +public final class MovingMatrix4 { + private Matrix4[] transforms; + private float t0, t1, inv; + + /** + * Constructs a simple static matrix. + * + * @param m matrix value at all times + */ + public MovingMatrix4(Matrix4 m) { + transforms = new Matrix4[] { m }; + t0 = t1 = 0; + inv = 1; + } + + private MovingMatrix4(int n, float t0, float t1, float inv) { + transforms = new Matrix4[n]; + this.t0 = t0; + this.t1 = t1; + this.inv = inv; + } + + /** + * Redefines the number of steps in the matrix. The contents are only + * re-allocated if the number of steps changes. This is to allow the matrix + * to be incrementally specified. + * + * @param n + */ + public void setSteps(int n) { + if (transforms.length != n) { + transforms = new Matrix4[n]; + if (t0 < t1) + inv = (transforms.length - 1) / (t1 - t0); + else + inv = 1; + } + } + + /** + * Updates the matrix for the given time step. + * + * @param i time step to update + * @param m new value for the matrix at this time step + */ + public void updateData(int i, Matrix4 m) { + transforms[i] = m; + } + + /** + * Get the matrix for the given time step. + * + * @param i time step to get + * @return matrix for the specfied time step + */ + public Matrix4 getData(int i) { + return transforms[i]; + } + + /** + * Get the number of matrix segments + * + * @return number of segments + */ + public int numSegments() { + return transforms.length; + } + + /** + * Update the time extents over which the matrix data is changing. If the + * interval is empty, no motion will be produced, even if multiple values + * have been specified. + * + * @param t0 + * @param t1 + */ + public void updateTimes(float t0, float t1) { + this.t0 = t0; + this.t1 = t1; + if (t0 < t1) + inv = (transforms.length - 1) / (t1 - t0); + else + inv = 1; + } + + public MovingMatrix4 inverse() { + MovingMatrix4 mi = new MovingMatrix4(transforms.length, t0, t1, inv); + for (int i = 0; i < transforms.length; i++) { + if (transforms[i] != null) { + mi.transforms[i] = transforms[i].inverse(); + if (mi.transforms[i] == null) + return null; // unable to invert + } + } + return mi; + } + + public Matrix4 sample(float time) { + if (transforms.length == 1 || t0 >= t1) + return transforms[0]; + else { + float nt = (MathUtils.clamp(time, t0, t1) - t0) * inv; + int idx0 = (int) nt; + int idx1 = Math.min(idx0 + 1, transforms.length - 1); + return Matrix4.blend(transforms[idx0], transforms[idx1], (float) (nt - idx0)); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/math/OrthoNormalBasis.java b/src/main/java/org/sunflow/math/OrthoNormalBasis.java new file mode 100644 index 0000000..4c16c71 --- /dev/null +++ b/src/main/java/org/sunflow/math/OrthoNormalBasis.java @@ -0,0 +1,109 @@ +package org.sunflow.math; + +public final class OrthoNormalBasis { + private Vector3 u, v, w; + + private OrthoNormalBasis() { + u = new Vector3(); + v = new Vector3(); + w = new Vector3(); + } + + public void flipU() { + u.negate(); + } + + public void flipV() { + v.negate(); + } + + public void flipW() { + w.negate(); + } + + public void swapUV() { + Vector3 t = u; + u = v; + v = t; + } + + public void swapVW() { + Vector3 t = v; + v = w; + w = t; + } + + public void swapWU() { + Vector3 t = w; + w = u; + u = t; + } + + public Vector3 transform(Vector3 a, Vector3 dest) { + dest.x = (a.x * u.x) + (a.y * v.x) + (a.z * w.x); + dest.y = (a.x * u.y) + (a.y * v.y) + (a.z * w.y); + dest.z = (a.x * u.z) + (a.y * v.z) + (a.z * w.z); + return dest; + } + + public Vector3 transform(Vector3 a) { + float x = (a.x * u.x) + (a.y * v.x) + (a.z * w.x); + float y = (a.x * u.y) + (a.y * v.y) + (a.z * w.y); + float z = (a.x * u.z) + (a.y * v.z) + (a.z * w.z); + return a.set(x, y, z); + } + + public Vector3 untransform(Vector3 a, Vector3 dest) { + dest.x = Vector3.dot(a, u); + dest.y = Vector3.dot(a, v); + dest.z = Vector3.dot(a, w); + return dest; + } + + public Vector3 untransform(Vector3 a) { + float x = Vector3.dot(a, u); + float y = Vector3.dot(a, v); + float z = Vector3.dot(a, w); + return a.set(x, y, z); + } + + public float untransformX(Vector3 a) { + return Vector3.dot(a, u); + } + + public float untransformY(Vector3 a) { + return Vector3.dot(a, v); + } + + public float untransformZ(Vector3 a) { + return Vector3.dot(a, w); + } + + public static final OrthoNormalBasis makeFromW(Vector3 w) { + OrthoNormalBasis onb = new OrthoNormalBasis(); + w.normalize(onb.w); + if ((Math.abs(onb.w.x) < Math.abs(onb.w.y)) && (Math.abs(onb.w.x) < Math.abs(onb.w.z))) { + onb.v.x = 0; + onb.v.y = onb.w.z; + onb.v.z = -onb.w.y; + } else if (Math.abs(onb.w.y) < Math.abs(onb.w.z)) { + onb.v.x = onb.w.z; + onb.v.y = 0; + onb.v.z = -onb.w.x; + } else { + onb.v.x = onb.w.y; + onb.v.y = -onb.w.x; + onb.v.z = 0; + } + Vector3.cross(onb.v.normalize(), onb.w, onb.u); + return onb; + } + + public static final OrthoNormalBasis makeFromWV(Vector3 w, Vector3 v) { + OrthoNormalBasis onb = new OrthoNormalBasis(); + w.normalize(onb.w); + Vector3.cross(v, onb.w, onb.u).normalize(); + Vector3.cross(onb.w, onb.u, onb.v); + return onb; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/math/PerlinScalar.java b/src/main/java/org/sunflow/math/PerlinScalar.java new file mode 100644 index 0000000..87f999a --- /dev/null +++ b/src/main/java/org/sunflow/math/PerlinScalar.java @@ -0,0 +1,330 @@ +package org.sunflow.math; + +/** + * Noise function from Ken Perlin. Additional routines are provided to emulate + * standard Renderman calls. This code was adapted mainly from the mrclasses + * package by Gonzalo Garramuno (http://sourceforge.net/projects/mrclasses/). + * + * @link http://mrl.nyu.edu/~perlin/noise/ + */ +public final class PerlinScalar { + private static final float[] G1 = { -1, 1 }; + private static final float[][] G2 = { { 1, 0 }, { -1, 0 }, { 0, 1 }, + { 0, -1 } }; + private static final float[][] G3 = { { 1, 1, 0 }, { -1, 1, 0 }, + { 1, -1, 0 }, { -1, -1, 0 }, { 1, 0, 1 }, { -1, 0, 1 }, + { 1, 0, -1 }, { -1, 0, -1 }, { 0, 1, 1 }, { 0, -1, 1 }, + { 0, 1, -1 }, { 0, -1, -1 }, { 1, 1, 0 }, { -1, 1, 0 }, + { 0, -1, 1 }, { 0, -1, -1 } }; + private static final float[][] G4 = { { -1, -1, -1, 0 }, { -1, -1, 1, 0 }, + { -1, 1, -1, 0 }, { -1, 1, 1, 0 }, { 1, -1, -1, 0 }, + { 1, -1, 1, 0 }, { 1, 1, -1, 0 }, { 1, 1, 1, 0 }, + { -1, -1, 0, -1 }, { -1, 1, 0, -1 }, { 1, -1, 0, -1 }, + { 1, 1, 0, -1 }, { -1, -1, 0, 1 }, { -1, 1, 0, 1 }, + { 1, -1, 0, 1 }, { 1, 1, 0, 1 }, { -1, 0, -1, -1 }, + { 1, 0, -1, -1 }, { -1, 0, -1, 1 }, { 1, 0, -1, 1 }, + { -1, 0, 1, -1 }, { 1, 0, 1, -1 }, { -1, 0, 1, 1 }, { 1, 0, 1, 1 }, + { 0, -1, -1, -1 }, { 0, -1, -1, 1 }, { 0, -1, 1, -1 }, + { 0, -1, 1, 1 }, { 0, 1, -1, -1 }, { 0, 1, -1, 1 }, + { 0, 1, 1, -1 }, { 0, 1, 1, 1 } }; + private static final int[] p = { 151, 160, 137, 91, 90, 15, 131, 13, 201, + 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, + 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, + 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, + 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, + 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, + 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, + 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, + 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, + 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, + 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, + 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, + 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, + 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, + 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, + 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, + 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, + 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180, + 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, + 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, + 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, + 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, + 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, + 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, + 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, + 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, + 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, + 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, + 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, + 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, + 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, + 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, + 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, + 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, + 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, + 141, 128, 195, 78, 66, 215, 61, 156, 180 }; + + public static final float snoise(float x) { + int xf = (int) Math.floor(x); + int X = xf & 255; + x -= xf; + float u = fade(x); + int A = p[X], B = p[X + 1]; + return lerp(u, grad(p[A], x), grad(p[B], x - 1)); + } + + public static final float snoise(float x, float y) { + int xf = (int) Math.floor(x); + int yf = (int) Math.floor(y); + int X = xf & 255; + int Y = yf & 255; + x -= xf; + y -= yf; + float u = fade(x); + float v = fade(y); + int A = p[X] + Y, B = p[X + 1] + Y; + return lerp(v, lerp(u, grad(p[A], x, y), grad(p[B], x - 1, y)), lerp(u, grad(p[A + 1], x, y - 1), grad(p[B + 1], x - 1, y - 1))); + } + + public static final float snoise(float x, float y, float z) { + int xf = (int) Math.floor(x); + int yf = (int) Math.floor(y); + int zf = (int) Math.floor(z); + int X = xf & 255; + int Y = yf & 255; + int Z = zf & 255; + x -= xf; + y -= yf; + z -= zf; + float u = fade(x); + float v = fade(y); + float w = fade(z); + int A = p[X] + Y, AA = p[A] + Z, AB = p[A + 1] + Z, B = p[X + 1] + Y, BA = p[B] + Z, BB = p[B + 1] + Z; + return lerp(w, lerp(v, lerp(u, grad(p[AA], x, y, z), grad(p[BA], x - 1, y, z)), lerp(u, grad(p[AB], x, y - 1, z), grad(p[BB], x - 1, y - 1, z))), lerp(v, lerp(u, grad(p[AA + 1], x, y, z - 1), grad(p[BA + 1], x - 1, y, z - 1)), lerp(u, grad(p[AB + 1], x, y - 1, z - 1), grad(p[BB + 1], x - 1, y - 1, z - 1)))); + } + + public static final float snoise(float x, float y, float z, float w) { + int xf = (int) Math.floor(x); + int yf = (int) Math.floor(y); + int zf = (int) Math.floor(z); + int wf = (int) Math.floor(w); + int X = xf & 255; + int Y = yf & 255; + int Z = zf & 255; + int W = wf & 255; + x -= xf; + y -= yf; + z -= zf; + w -= wf; + float u = fade(x); + float v = fade(y); + float t = fade(z); + float s = fade(w); + int A = p[X] + Y, AA = p[A] + Z, AB = p[A + 1] + Z, B = p[X + 1] + Y, BA = p[B] + Z, BB = p[B + 1] + Z, AAA = p[AA] + W, AAB = p[AA + 1] + W, ABA = p[AB] + W, ABB = p[AB + 1] + W, BAA = p[BA] + W, BAB = p[BA + 1] + W, BBA = p[BB] + W, BBB = p[BB + 1] + W; + return lerp(s, lerp(t, lerp(v, lerp(u, grad(p[AAA], x, y, z, w), grad(p[BAA], x - 1, y, z, w)), lerp(u, grad(p[ABA], x, y - 1, z, w), grad(p[BBA], x - 1, y - 1, z, w))), lerp(v, lerp(u, grad(p[AAB], x, y, z - 1, w), grad(p[BAB], x - 1, y, z - 1, w)), lerp(u, grad(p[ABB], x, y - 1, z - 1, w), grad(p[BBB], x - 1, y - 1, z - 1, w)))), lerp(t, lerp(v, lerp(u, grad(p[AAA + 1], x, y, z, w - 1), grad(p[BAA + 1], x - 1, y, z, w - 1)), lerp(u, grad(p[ABA + 1], x, y - 1, z, w - 1), grad(p[BBA + 1], x - 1, y - 1, z, w - 1))), lerp(v, lerp(u, grad(p[AAB + 1], x, y, z - 1, w - 1), grad(p[BAB + 1], x - 1, y, z - 1, w - 1)), lerp(u, grad(p[ABB + 1], x, y - 1, z - 1, w - 1), grad(p[BBB + 1], x - 1, y - 1, z - 1, w - 1))))); + } + + public static final float snoise(Point2 p) { + return snoise(p.x, p.y); + } + + public static final float snoise(Point3 p) { + return snoise(p.x, p.y, p.z); + } + + public static final float snoise(Point3 p, float t) { + return snoise(p.x, p.y, p.z, t); + } + + public static final float noise(float x) { + return 0.5f + 0.5f * snoise(x); + } + + public static final float noise(float x, float y) { + return 0.5f + 0.5f * snoise(x, y); + } + + public static final float noise(float x, float y, float z) { + return 0.5f + 0.5f * snoise(x, y, z); + } + + public static final float noise(float x, float y, float z, float t) { + return 0.5f + 0.5f * snoise(x, y, z, t); + } + + public static final float noise(Point2 p) { + return 0.5f + 0.5f * snoise(p.x, p.y); + } + + public static final float noise(Point3 p) { + return 0.5f + 0.5f * snoise(p.x, p.y, p.z); + } + + public static final float noise(Point3 p, float t) { + return 0.5f + 0.5f * snoise(p.x, p.y, p.z, t); + } + + public static final float pnoise(float xi, float period) { + float x = (xi % period) + ((xi < 0) ? period : 0); + return ((period - x) * noise(x) + x * noise(x - period)) / period; + } + + public static final float pnoise(float xi, float yi, float w, float h) { + float x = (xi % w) + ((xi < 0) ? w : 0); + float y = (yi % h) + ((yi < 0) ? h : 0); + float w_x = w - x; + float h_y = h - y; + float x_w = x - w; + float y_h = y - h; + return (noise(x, y) * (w_x) * (h_y) + noise(x_w, y) * (x) * (h_y) + noise(x_w, y_h) * (x) * (y) + noise(x, y_h) * (w_x) * (y)) / (w * h); + } + + public static final float pnoise(float xi, float yi, float zi, float w, float h, float d) { + float x = (xi % w) + ((xi < 0) ? w : 0); + float y = (yi % h) + ((yi < 0) ? h : 0); + float z = (zi % d) + ((zi < 0) ? d : 0); + float w_x = w - x; + float h_y = h - y; + float d_z = d - z; + float x_w = x - w; + float y_h = y - h; + float z_d = z - d; + float xy = x * y; + float h_yXd_z = h_y * d_z; + float h_yXz = h_y * z; + float w_xXy = w_x * y; + return (noise(x, y, z) * (w_x) * h_yXd_z + noise(x, y_h, z) * w_xXy * (d_z) + noise(x_w, y, z) * (x) * h_yXd_z + noise(x_w, y_h, z) * (xy) * (d_z) + noise(x_w, y_h, z_d) * (xy) * (z) + noise(x, y, z_d) * (w_x) * h_yXz + noise(x, y_h, z_d) * w_xXy * (z) + noise(x_w, y, z_d) * (x) * h_yXz) / (w * h * d); + } + + public static final float pnoise(float xi, float yi, float zi, float ti, float w, float h, float d, float p) { + float x = (xi % w) + ((xi < 0) ? w : 0); + float y = (yi % h) + ((yi < 0) ? h : 0); + float z = (zi % d) + ((zi < 0) ? d : 0); + float t = (ti % p) + ((ti < 0) ? p : 0); + float w_x = w - x; + float h_y = h - y; + float d_z = d - z; + float p_t = p - t; + float x_w = x - w; + float y_h = y - h; + float z_d = z - d; + float t_p = t - p; + float xy = x * y; + float d_zXp_t = (d_z) * (p_t); + float zXp_t = z * (p_t); + float zXt = z * t; + float d_zXt = d_z * t; + float w_xXy = w_x * y; + float w_xXh_y = w_x * h_y; + float xXh_y = x * h_y; + return (noise(x, y, z, t) * (w_xXh_y) * d_zXp_t + noise(x_w, y, z, t) * (xXh_y) * d_zXp_t + noise(x_w, y_h, z, t) * (xy) * d_zXp_t + noise(x, y_h, z, t) * (w_xXy) * d_zXp_t + noise(x_w, y_h, z_d, t) * (xy) * (zXp_t) + noise(x, y, z_d, t) * (w_xXh_y) * (zXp_t) + noise(x, y_h, z_d, t) * (w_xXy) * (zXp_t) + noise(x_w, y, z_d, t) * (xXh_y) * (zXp_t) + noise(x, y, z, t_p) * (w_xXh_y) * (d_zXt) + noise(x_w, y, z, t_p) * (xXh_y) * (d_zXt) + noise(x_w, y_h, z, t_p) * (xy) * (d_zXt) + noise(x, y_h, z, t_p) * (w_xXy) * (d_zXt) + noise(x_w, y_h, z_d, t_p) * (xy) * (zXt) + noise(x, y, z_d, t_p) * (w_xXh_y) * (zXt) + noise(x, y_h, z_d, t_p) * (w_xXy) * (zXt) + noise(x_w, y, z_d, t_p) * (xXh_y) * (zXt)) / (w * h * d * t); + } + + public static final float pnoise(Point2 p, float periodx, float periody) { + return pnoise(p.x, p.y, periodx, periody); + } + + public static final float pnoise(Point3 p, Vector3 period) { + return pnoise(p.x, p.y, p.z, period.x, period.y, period.z); + } + + public static final float pnoise(Point3 p, float t, Vector3 pperiod, float tperiod) { + return pnoise(p.x, p.y, p.z, t, pperiod.x, pperiod.y, pperiod.z, tperiod); + } + + public static final float spnoise(float xi, float period) { + float x = (xi % period) + ((xi < 0) ? period : 0); + return (((period - x) * snoise(x) + x * snoise(x - period)) / period); + } + + public static final float spnoise(float xi, float yi, float w, float h) { + float x = (xi % w) + ((xi < 0) ? w : 0); + float y = (yi % h) + ((yi < 0) ? h : 0); + float w_x = w - x; + float h_y = h - y; + float x_w = x - w; + float y_h = y - h; + return ((snoise(x, y) * (w_x) * (h_y) + snoise(x_w, y) * (x) * (h_y) + snoise(x_w, y_h) * (x) * (y) + snoise(x, y_h) * (w_x) * (y)) / (w * h)); + } + + public static final float spnoise(float xi, float yi, float zi, float w, float h, float d) { + float x = (xi % w) + ((xi < 0) ? w : 0); + float y = (yi % h) + ((yi < 0) ? h : 0); + float z = (zi % d) + ((zi < 0) ? d : 0); + float w_x = w - x; + float h_y = h - y; + float d_z = d - z; + float x_w = x - w; + float y_h = y - h; + float z_d = z - d; + float xy = x * y; + float h_yXd_z = h_y * d_z; + float h_yXz = h_y * z; + float w_xXy = w_x * y; + return ((snoise(x, y, z) * (w_x) * h_yXd_z + snoise(x, y_h, z) * w_xXy * (d_z) + snoise(x_w, y, z) * (x) * h_yXd_z + snoise(x_w, y_h, z) * (xy) * (d_z) + snoise(x_w, y_h, z_d) * (xy) * (z) + snoise(x, y, z_d) * (w_x) * h_yXz + snoise(x, y_h, z_d) * w_xXy * (z) + snoise(x_w, y, z_d) * (x) * h_yXz) / (w * h * d)); + } + + public static final float spnoise(float xi, float yi, float zi, float ti, float w, float h, float d, float p) { + float x = (xi % w) + ((xi < 0) ? w : 0); + float y = (yi % h) + ((yi < 0) ? h : 0); + float z = (zi % d) + ((zi < 0) ? d : 0); + float t = (ti % p) + ((ti < 0) ? p : 0); + float w_x = w - x; + float h_y = h - y; + float d_z = d - z; + float p_t = p - t; + float x_w = x - w; + float y_h = y - h; + float z_d = z - d; + float t_p = t - p; + float xy = x * y; + float d_zXp_t = (d_z) * (p_t); + float zXp_t = z * (p_t); + float zXt = z * t; + float d_zXt = d_z * t; + float w_xXy = w_x * y; + float w_xXh_y = w_x * h_y; + float xXh_y = x * h_y; + return ((snoise(x, y, z, t) * (w_xXh_y) * d_zXp_t + snoise(x_w, y, z, t) * (xXh_y) * d_zXp_t + snoise(x_w, y_h, z, t) * (xy) * d_zXp_t + snoise(x, y_h, z, t) * (w_xXy) * d_zXp_t + snoise(x_w, y_h, z_d, t) * (xy) * (zXp_t) + snoise(x, y, z_d, t) * (w_xXh_y) * (zXp_t) + snoise(x, y_h, z_d, t) * (w_xXy) * (zXp_t) + snoise(x_w, y, z_d, t) * (xXh_y) * (zXp_t) + snoise(x, y, z, t_p) * (w_xXh_y) * (d_zXt) + snoise(x_w, y, z, t_p) * (xXh_y) * (d_zXt) + snoise(x_w, y_h, z, t_p) * (xy) * (d_zXt) + snoise(x, y_h, z, t_p) * (w_xXy) * (d_zXt) + snoise(x_w, y_h, z_d, t_p) * (xy) * (zXt) + snoise(x, y, z_d, t_p) * (w_xXh_y) * (zXt) + snoise(x, y_h, z_d, t_p) * (w_xXy) * (zXt) + snoise(x_w, y, z_d, t_p) * (xXh_y) * (zXt)) / (w * h * d * t)); + } + + public static final float spnoise(Point2 p, float periodx, float periody) { + return spnoise(p.x, p.y, periodx, periody); + } + + public static final float spnoise(Point3 p, Vector3 period) { + return spnoise(p.x, p.y, p.z, period.x, period.y, period.z); + } + + public static final float spnoise(Point3 p, float t, Vector3 pperiod, float tperiod) { + return spnoise(p.x, p.y, p.z, t, pperiod.x, pperiod.y, pperiod.z, tperiod); + } + + private static final float fade(float t) { + return t * t * t * (t * (t * 6 - 15) + 10); + } + + private static final float lerp(float t, float a, float b) { + return a + t * (b - a); + } + + private static final float grad(int hash, float x) { + int h = hash & 0x1; + return x * G1[h]; + } + + private static final float grad(int hash, float x, float y) { + int h = hash & 0x3; + return x * G2[h][0] + y * G2[h][1]; + } + + private static final float grad(int hash, float x, float y, float z) { + int h = hash & 15; + return x * G3[h][0] + y * G3[h][1] + z * G3[h][2]; + } + + private static final float grad(int hash, float x, float y, float z, float w) { + int h = hash & 31; + return x * G4[h][0] + y * G4[h][1] + z * G4[h][2] + w * G4[h][3]; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/math/PerlinVector.java b/src/main/java/org/sunflow/math/PerlinVector.java new file mode 100644 index 0000000..fa62004 --- /dev/null +++ b/src/main/java/org/sunflow/math/PerlinVector.java @@ -0,0 +1,131 @@ +package org.sunflow.math; + +/** + * Vector versions of the standard noise functions. These are provided to + * emulate standard renderman calls.This code was adapted mainly from the + * mrclasses package by Gonzalo Garramuno + * (http://sourceforge.net/projects/mrclasses/). + */ +public class PerlinVector { + private static final float P1x = 0.34f; + private static final float P1y = 0.66f; + private static final float P1z = 0.237f; + private static final float P2x = 0.011f; + private static final float P2y = 0.845f; + private static final float P2z = 0.037f; + private static final float P3x = 0.34f; + private static final float P3y = 0.12f; + private static final float P3z = 0.9f; + + public static final Vector3 snoise(float x) { + return new Vector3(PerlinScalar.snoise(x + P1x), PerlinScalar.snoise(x + P2x), PerlinScalar.snoise(x + P3x)); + } + + public static final Vector3 snoise(float x, float y) { + return new Vector3(PerlinScalar.snoise(x + P1x, y + P1y), PerlinScalar.snoise(x + P2x, y + P2y), PerlinScalar.snoise(x + P3x, y + P3y)); + } + + public static final Vector3 snoise(float x, float y, float z) { + return new Vector3(PerlinScalar.snoise(x + P1x, y + P1y, z + P1z), PerlinScalar.snoise(x + P2x, y + P2y, z + P2z), PerlinScalar.snoise(x + P3x, y + P3y, z + P3z)); + } + + public static final Vector3 snoise(float x, float y, float z, float t) { + return new Vector3(PerlinScalar.snoise(x + P1x, y + P1y, z + P1z, t), PerlinScalar.snoise(x + P2x, y + P2y, z + P2z, t), PerlinScalar.snoise(x + P3x, y + P3y, z + P3z, t)); + } + + public static final Vector3 snoise(Point2 p) { + return snoise(p.x, p.y); + } + + public static final Vector3 snoise(Point3 p) { + return snoise(p.x, p.y, p.z); + } + + public static final Vector3 snoise(Point3 p, float t) { + return snoise(p.x, p.y, p.z, t); + } + + public static final Vector3 noise(float x) { + return new Vector3(PerlinScalar.noise(x + P1x), PerlinScalar.noise(x + P2x), PerlinScalar.noise(x + P3x)); + } + + public static final Vector3 noise(float x, float y) { + return new Vector3(PerlinScalar.noise(x + P1x, y + P1y), PerlinScalar.noise(x + P2x, y + P2y), PerlinScalar.noise(x + P3x, y + P3y)); + } + + public static final Vector3 noise(float x, float y, float z) { + return new Vector3(PerlinScalar.noise(x + P1x, y + P1y, z + P1z), PerlinScalar.noise(x + P2x, y + P2y, z + P2z), PerlinScalar.noise(x + P3x, y + P3y, z + P3z)); + } + + public static final Vector3 noise(float x, float y, float z, float t) { + return new Vector3(PerlinScalar.noise(x + P1x, y + P1y, z + P1z, t), PerlinScalar.noise(x + P2x, y + P2y, z + P2z, t), PerlinScalar.noise(x + P3x, y + P3y, z + P3z, t)); + } + + public static final Vector3 noise(Point2 p) { + return noise(p.x, p.y); + } + + public static final Vector3 noise(Point3 p) { + return noise(p.x, p.y, p.z); + } + + public static final Vector3 noise(Point3 p, float t) { + return noise(p.x, p.y, p.z, t); + } + + public static final Vector3 pnoise(float x, float period) { + return new Vector3(PerlinScalar.pnoise(x + P1x, period), PerlinScalar.pnoise(x + P2x, period), PerlinScalar.pnoise(x + P3x, period)); + } + + public static final Vector3 pnoise(float x, float y, float w, float h) { + return new Vector3(PerlinScalar.pnoise(x + P1x, y + P1y, w, h), PerlinScalar.pnoise(x + P2x, y + P2y, w, h), PerlinScalar.pnoise(x + P3x, y + P3y, w, h)); + } + + public static final Vector3 pnoise(float x, float y, float z, float w, float h, float d) { + return new Vector3(PerlinScalar.pnoise(x + P1x, y + P1y, z + P1z, w, h, d), PerlinScalar.pnoise(x + P2x, y + P2y, z + P2z, w, h, d), PerlinScalar.pnoise(x + P3x, y + P3y, z + P3z, w, h, d)); + } + + public static final Vector3 pnoise(float x, float y, float z, float t, float w, float h, float d, float p) { + return new Vector3(PerlinScalar.pnoise(x + P1x, y + P1y, z + P1z, t, w, h, d, p), PerlinScalar.pnoise(x + P2x, y + P2y, z + P2z, t, w, h, d, p), PerlinScalar.pnoise(x + P3x, y + P3y, z + P3z, t, w, h, d, p)); + } + + public static final Vector3 pnoise(Point2 p, float periodx, float periody) { + return pnoise(p.x, p.y, periodx, periody); + } + + public static final Vector3 pnoise(Point3 p, Vector3 period) { + return pnoise(p.x, p.y, p.z, period.x, period.y, period.z); + } + + public static final Vector3 pnoise(Point3 p, float t, Vector3 pperiod, float tperiod) { + return pnoise(p.x, p.y, p.z, t, pperiod.x, pperiod.y, pperiod.z, tperiod); + } + + public static final Vector3 spnoise(float x, float period) { + return new Vector3(PerlinScalar.spnoise(x + P1x, period), PerlinScalar.spnoise(x + P2x, period), PerlinScalar.spnoise(x + P3x, period)); + } + + public static final Vector3 spnoise(float x, float y, float w, float h) { + return new Vector3(PerlinScalar.spnoise(x + P1x, y + P1y, w, h), PerlinScalar.spnoise(x + P2x, y + P2y, w, h), PerlinScalar.spnoise(x + P3x, y + P3y, w, h)); + } + + public static final Vector3 spnoise(float x, float y, float z, float w, float h, float d) { + return new Vector3(PerlinScalar.spnoise(x + P1x, y + P1y, z + P1z, w, h, d), PerlinScalar.spnoise(x + P2x, y + P2y, z + P2z, w, h, d), PerlinScalar.spnoise(x + P3x, y + P3y, z + P3z, w, h, d)); + } + + public static final Vector3 spnoise(float x, float y, float z, float t, float w, float h, float d, float p) { + return new Vector3(PerlinScalar.spnoise(x + P1x, y + P1y, z + P1z, t, w, h, d, p), PerlinScalar.spnoise(x + P2x, y + P2y, z + P2z, t, w, h, d, p), PerlinScalar.spnoise(x + P3x, y + P3y, z + P3z, t, w, h, d, p)); + } + + public static final Vector3 spnoise(Point2 p, float periodx, float periody) { + return spnoise(p.x, p.y, periodx, periody); + } + + public static final Vector3 spnoise(Point3 p, Vector3 period) { + return spnoise(p.x, p.y, p.z, period.x, period.y, period.z); + } + + public static final Vector3 spnoise(Point3 p, float t, Vector3 pperiod, float tperiod) { + return spnoise(p.x, p.y, p.z, t, pperiod.x, pperiod.y, pperiod.z, tperiod); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/math/Point2.java b/src/main/java/org/sunflow/math/Point2.java new file mode 100644 index 0000000..6c1d78f --- /dev/null +++ b/src/main/java/org/sunflow/math/Point2.java @@ -0,0 +1,35 @@ +package org.sunflow.math; + +public final class Point2 { + public float x, y; + + public Point2() { + } + + public Point2(float x, float y) { + this.x = x; + this.y = y; + } + + public Point2(Point2 p) { + x = p.x; + y = p.y; + } + + public final Point2 set(float x, float y) { + this.x = x; + this.y = y; + return this; + } + + public final Point2 set(Point2 p) { + x = p.x; + y = p.y; + return this; + } + + @Override + public final String toString() { + return String.format("(%.2f, %.2f)", x, y); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/math/Point3.java b/src/main/java/org/sunflow/math/Point3.java new file mode 100644 index 0000000..cc1bf13 --- /dev/null +++ b/src/main/java/org/sunflow/math/Point3.java @@ -0,0 +1,132 @@ +package org.sunflow.math; + +public final class Point3 { + public float x, y, z; + + public Point3() { + } + + public Point3(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Point3(Point3 p) { + x = p.x; + y = p.y; + z = p.z; + } + + public float get(int i) { + switch (i) { + case 0: + return x; + case 1: + return y; + default: + return z; + } + } + + public final float distanceTo(Point3 p) { + float dx = x - p.x; + float dy = y - p.y; + float dz = z - p.z; + return (float) Math.sqrt((dx * dx) + (dy * dy) + (dz * dz)); + } + + public final float distanceTo(float px, float py, float pz) { + float dx = x - px; + float dy = y - py; + float dz = z - pz; + return (float) Math.sqrt((dx * dx) + (dy * dy) + (dz * dz)); + } + + public final float distanceToSquared(Point3 p) { + float dx = x - p.x; + float dy = y - p.y; + float dz = z - p.z; + return (dx * dx) + (dy * dy) + (dz * dz); + } + + public final float distanceToSquared(float px, float py, float pz) { + float dx = x - px; + float dy = y - py; + float dz = z - pz; + return (dx * dx) + (dy * dy) + (dz * dz); + } + + public final Point3 set(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + return this; + } + + public final Point3 set(Point3 p) { + x = p.x; + y = p.y; + z = p.z; + return this; + } + + public static final Point3 add(Point3 p, Vector3 v, Point3 dest) { + dest.x = p.x + v.x; + dest.y = p.y + v.y; + dest.z = p.z + v.z; + return dest; + } + + public static final Vector3 sub(Point3 p1, Point3 p2, Vector3 dest) { + dest.x = p1.x - p2.x; + dest.y = p1.y - p2.y; + dest.z = p1.z - p2.z; + return dest; + } + + public static final Point3 mid(Point3 p1, Point3 p2, Point3 dest) { + dest.x = 0.5f * (p1.x + p2.x); + dest.y = 0.5f * (p1.y + p2.y); + dest.z = 0.5f * (p1.z + p2.z); + return dest; + } + + public static final Point3 blend(Point3 p0, Point3 p1, float blend, Point3 dest) { + dest.x = (1 - blend) * p0.x + blend * p1.x; + dest.y = (1 - blend) * p0.y + blend * p1.y; + dest.z = (1 - blend) * p0.z + blend * p1.z; + return dest; + } + + public static final Vector3 normal(Point3 p0, Point3 p1, Point3 p2) { + float edge1x = p1.x - p0.x; + float edge1y = p1.y - p0.y; + float edge1z = p1.z - p0.z; + float edge2x = p2.x - p0.x; + float edge2y = p2.y - p0.y; + float edge2z = p2.z - p0.z; + float nx = edge1y * edge2z - edge1z * edge2y; + float ny = edge1z * edge2x - edge1x * edge2z; + float nz = edge1x * edge2y - edge1y * edge2x; + return new Vector3(nx, ny, nz); + } + + public static final Vector3 normal(Point3 p0, Point3 p1, Point3 p2, Vector3 dest) { + float edge1x = p1.x - p0.x; + float edge1y = p1.y - p0.y; + float edge1z = p1.z - p0.z; + float edge2x = p2.x - p0.x; + float edge2y = p2.y - p0.y; + float edge2z = p2.z - p0.z; + dest.x = edge1y * edge2z - edge1z * edge2y; + dest.y = edge1z * edge2x - edge1x * edge2z; + dest.z = edge1x * edge2y - edge1y * edge2x; + return dest; + } + + @Override + public final String toString() { + return String.format("(%.2f, %.2f, %.2f)", x, y, z); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/math/QMC.java b/src/main/java/org/sunflow/math/QMC.java new file mode 100644 index 0000000..e581b94 --- /dev/null +++ b/src/main/java/org/sunflow/math/QMC.java @@ -0,0 +1,194 @@ +package org.sunflow.math; + +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +public final class QMC { + public static final int MAX_SIGMA_ORDER = 15; + private static final int NUM = 128; + private static final int[][] SIGMA = new int[NUM][]; + private static final int[] PRIMES = new int[NUM]; + private static final int[] FIBONACCI = new int[47]; + private static final double[] FIBONACCI_INV = new double[FIBONACCI.length]; + private static final double[] KOROBOV = new double[NUM]; + + static { + UI.printInfo(Module.QMC, "Initializing Faure scrambling tables ..."); + // build table of first primes + PRIMES[0] = 2; + for (int i = 1; i < PRIMES.length; i++) + PRIMES[i] = nextPrime(PRIMES[i - 1]); + int[][] table = new int[PRIMES[PRIMES.length - 1] + 1][]; + table[2] = new int[2]; + table[2][0] = 0; + table[2][1] = 1; + for (int i = 3; i <= PRIMES[PRIMES.length - 1]; i++) { + table[i] = new int[i]; + if ((i & 1) == 0) { + int[] prev = table[i >> 1]; + for (int j = 0; j < prev.length; j++) + table[i][j] = 2 * prev[j]; + for (int j = 0; j < prev.length; j++) + table[i][prev.length + j] = 2 * prev[j] + 1; + } else { + int[] prev = table[i - 1]; + int med = (i - 1) >> 1; + for (int j = 0; j < med; j++) + table[i][j] = prev[j] + ((prev[j] >= med) ? 1 : 0); + table[i][med] = med; + for (int j = 0; j < med; j++) + table[i][med + j + 1] = prev[j + med] + ((prev[j + med] >= med) ? 1 : 0); + } + } + for (int i = 0; i < PRIMES.length; i++) { + int p = PRIMES[i]; + SIGMA[i] = new int[p]; + System.arraycopy(table[p], 0, SIGMA[i], 0, p); + } + UI.printInfo(Module.QMC, "Initializing lattice tables ..."); + FIBONACCI[0] = 0; + FIBONACCI[1] = 1; + for (int i = 2; i < FIBONACCI.length; i++) { + FIBONACCI[i] = FIBONACCI[i - 1] + FIBONACCI[i - 2]; + FIBONACCI_INV[i] = 1.0 / FIBONACCI[i]; + } + KOROBOV[0] = 1; + for (int i = 1; i < KOROBOV.length; i++) + KOROBOV[i] = 203 * KOROBOV[i - 1]; + } + + private static final int nextPrime(int p) { + p = p + (p & 1) + 1; + while (true) { + int div = 3; + boolean isPrime = true; + while (isPrime && ((div * div) <= p)) { + isPrime = ((p % div) != 0); + div += 2; + } + if (isPrime) + return p; + p += 2; + } + } + + private QMC() { + } + + public static double riVDC(int bits, int r) { + bits = (bits << 16) | (bits >>> 16); + bits = ((bits & 0x00ff00ff) << 8) | ((bits & 0xff00ff00) >>> 8); + bits = ((bits & 0x0f0f0f0f) << 4) | ((bits & 0xf0f0f0f0) >>> 4); + bits = ((bits & 0x33333333) << 2) | ((bits & 0xcccccccc) >>> 2); + bits = ((bits & 0x55555555) << 1) | ((bits & 0xaaaaaaaa) >>> 1); + bits ^= r; + return (double) (bits & 0xFFFFFFFFL) / (double) 0x100000000L; + } + + public static double riS(int i, int r) { + for (int v = 1 << 31; i != 0; i >>>= 1, v ^= v >>> 1) + if ((i & 1) != 0) + r ^= v; + return (double) (r & 0xFFFFFFFFL) / (double) 0x100000000L; + } + + public static double riLP(int i, int r) { + for (int v = 1 << 31; i != 0; i >>>= 1, v |= v >>> 1) + if ((i & 1) != 0) + r ^= v; + return (double) (r & 0xFFFFFFFFL) / (double) 0x100000000L; + } + + public static final double halton(int d, int i) { + // generalized Halton sequence + switch (d) { + case 0: { + i = (i << 16) | (i >>> 16); + i = ((i & 0x00ff00ff) << 8) | ((i & 0xff00ff00) >>> 8); + i = ((i & 0x0f0f0f0f) << 4) | ((i & 0xf0f0f0f0) >>> 4); + i = ((i & 0x33333333) << 2) | ((i & 0xcccccccc) >>> 2); + i = ((i & 0x55555555) << 1) | ((i & 0xaaaaaaaa) >>> 1); + return (double) (i & 0xFFFFFFFFL) / (double) 0x100000000L; + } + case 1: { + double v = 0; + double inv = 1.0 / 3; + double p; + int n; + for (p = inv, n = i; n != 0; p *= inv, n /= 3) + v += (n % 3) * p; + return v; + } + default: + } + int base = PRIMES[d]; + int[] perm = SIGMA[d]; + double v = 0; + double inv = 1.0 / base; + double p; + int n; + for (p = inv, n = i; n != 0; p *= inv, n /= base) + v += perm[n % base] * p; + return v; + } + + /** + * Compute mod(x,1), assuming that x is positive or 0. + * + * @param x any number >= 0 + * @return mod(x,1) + */ + public static final double mod1(double x) { + // assumes x >= 0 + return x - (int) x; + } + + /** + * Compute sigma function used to seed QMC sequence trees. The sigma table + * is exactly 2^order elements long, and therefore i should be in the: [0, + * 2^order) interval. This function is equal to 2^order*halton(0,i) + * + * @param i index + * @param order + * @return sigma function + */ + public static final int sigma(int i, int order) { + assert order > 0 && order < 32; + assert i >= 0 && i < (1 << order); + i = (i << 16) | (i >>> 16); + i = ((i & 0x00ff00ff) << 8) | ((i & 0xff00ff00) >>> 8); + i = ((i & 0x0f0f0f0f) << 4) | ((i & 0xf0f0f0f0) >>> 4); + i = ((i & 0x33333333) << 2) | ((i & 0xcccccccc) >>> 2); + i = ((i & 0x55555555) << 1) | ((i & 0xaaaaaaaa) >>> 1); + return i >>> (32 - order); + } + + public static final int getFibonacciRank(int n) { + int k = 3; + while (FIBONACCI[k] <= n) + k++; + return k - 1; + } + + public static final int fibonacci(int k) { + return FIBONACCI[k]; + } + + public static final double fibonacciLattice(int k, int i, int d) { + return d == 0 ? i * FIBONACCI_INV[k] : mod1((i * FIBONACCI[k - 1]) * FIBONACCI_INV[k]); + } + + public static final double reducedCPRotation(int k, int d, double x0, double x1) { + int j1 = FIBONACCI[2 * ((k - 1) >> 2) + 1]; + int j2 = FIBONACCI[2 * ((k + 1) >> 2)]; + if (d == 1) { + j1 = ((j1 * FIBONACCI[k - 1]) % FIBONACCI[k]); + j2 = ((j2 * FIBONACCI[k - 1]) % FIBONACCI[k]) - FIBONACCI[k]; + } + return (x0 * j1 + x1 * j2) * FIBONACCI_INV[k]; + } + + public static final double korobovLattice(int m, int i, int d) { + return mod1(i * KOROBOV[d] / (1 << m)); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/math/Solvers.java b/src/main/java/org/sunflow/math/Solvers.java new file mode 100644 index 0000000..5377d43 --- /dev/null +++ b/src/main/java/org/sunflow/math/Solvers.java @@ -0,0 +1,137 @@ +package org.sunflow.math; + +public final class Solvers { + /** + * Solves the equation ax^2+bx+c=0. Solutions are returned in a sorted array + * if they exist. + * + * @param a coefficient of x^2 + * @param b coefficient of x^1 + * @param c coefficient of x^0 + * @return an array containing the two real roots, or null if + * no real solutions exist + */ + public static final double[] solveQuadric(double a, double b, double c) { + double disc = b * b - 4 * a * c; + if (disc < 0) + return null; + disc = Math.sqrt(disc); + double q = ((b < 0) ? -0.5 * (b - disc) : -0.5 * (b + disc)); + double t0 = q / a; + double t1 = c / q; + // return sorted array + return (t0 > t1) ? new double[] { t1, t0 } : new double[] { t0, t1 }; + } + + /** + * Solve a quartic equation of the form ax^4+bx^3+cx^2+cx^1+d=0. The roots + * are returned in a sorted array of doubles in increasing order. + * + * @param a coefficient of x^4 + * @param b coefficient of x^3 + * @param c coefficient of x^2 + * @param d coefficient of x^1 + * @param e coefficient of x^0 + * @return a sorted array of roots, or null if no solutions + * exist + */ + public static double[] solveQuartic(double a, double b, double c, double d, double e) { + double inva = 1 / a; + double c1 = b * inva; + double c2 = c * inva; + double c3 = d * inva; + double c4 = e * inva; + // cubic resolvant + double c12 = c1 * c1; + double p = -0.375 * c12 + c2; + double q = 0.125 * c12 * c1 - 0.5 * c1 * c2 + c3; + double r = -0.01171875 * c12 * c12 + 0.0625 * c12 * c2 - 0.25 * c1 * c3 + c4; + double z = solveCubicForQuartic(-0.5 * p, -r, 0.5 * r * p - 0.125 * q * q); + double d1 = 2.0 * z - p; + if (d1 < 0) { + if (d1 > 1.0e-10) + d1 = 0; + else + return null; + } + double d2; + if (d1 < 1.0e-10) { + d2 = z * z - r; + if (d2 < 0) + return null; + d2 = Math.sqrt(d2); + } else { + d1 = Math.sqrt(d1); + d2 = 0.5 * q / d1; + } + // setup usefull values for the quadratic factors + double q1 = d1 * d1; + double q2 = -0.25 * c1; + double pm = q1 - 4 * (z - d2); + double pp = q1 - 4 * (z + d2); + if (pm >= 0 && pp >= 0) { + // 4 roots (!) + pm = Math.sqrt(pm); + pp = Math.sqrt(pp); + double[] results = new double[4]; + results[0] = -0.5 * (d1 + pm) + q2; + results[1] = -0.5 * (d1 - pm) + q2; + results[2] = 0.5 * (d1 + pp) + q2; + results[3] = 0.5 * (d1 - pp) + q2; + // tiny insertion sort + for (int i = 1; i < 4; i++) { + for (int j = i; j > 0 && results[j - 1] > results[j]; j--) { + double t = results[j]; + results[j] = results[j - 1]; + results[j - 1] = t; + } + } + return results; + } else if (pm >= 0) { + pm = Math.sqrt(pm); + double[] results = new double[2]; + results[0] = -0.5 * (d1 + pm) + q2; + results[1] = -0.5 * (d1 - pm) + q2; + return results; + } else if (pp >= 0) { + pp = Math.sqrt(pp); + double[] results = new double[2]; + results[0] = 0.5 * (d1 - pp) + q2; + results[1] = 0.5 * (d1 + pp) + q2; + return results; + } + return null; + } + + /** + * Return only one root for the specified cubic equation. This routine is + * only meant to be called by the quartic solver. It assumes the cubic is of + * the form: x^3+px^2+qx+r. + * + * @param p + * @param q + * @param r + * @return + */ + private static final double solveCubicForQuartic(double p, double q, double r) { + double A2 = p * p; + double Q = (A2 - 3.0 * q) / 9.0; + double R = (p * (A2 - 4.5 * q) + 13.5 * r) / 27.0; + double Q3 = Q * Q * Q; + double R2 = R * R; + double d = Q3 - R2; + double an = p / 3.0; + if (d >= 0) { + d = R / Math.sqrt(Q3); + double theta = Math.acos(d) / 3.0; + double sQ = -2.0 * Math.sqrt(Q); + return sQ * Math.cos(theta) - an; + } else { + double sQ = Math.pow(Math.sqrt(R2 - Q3) + Math.abs(R), 1.0 / 3.0); + if (R < 0) + return (sQ + Q / sQ) - an; + else + return -(sQ + Q / sQ) - an; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/math/Vector3.java b/src/main/java/org/sunflow/math/Vector3.java new file mode 100644 index 0000000..30ed598 --- /dev/null +++ b/src/main/java/org/sunflow/math/Vector3.java @@ -0,0 +1,195 @@ +package org.sunflow.math; + +public final class Vector3 { + private static final float[] COS_THETA = new float[256]; + private static final float[] SIN_THETA = new float[256]; + private static final float[] COS_PHI = new float[256]; + private static final float[] SIN_PHI = new float[256]; + + public float x, y, z; + + static { + // precompute tables to compress unit vectors + for (int i = 0; i < 256; i++) { + double angle = (i * Math.PI) / 256.0; + COS_THETA[i] = (float) Math.cos(angle); + SIN_THETA[i] = (float) Math.sin(angle); + COS_PHI[i] = (float) Math.cos(2 * angle); + SIN_PHI[i] = (float) Math.sin(2 * angle); + } + } + + public Vector3() { + } + + public Vector3(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vector3(Vector3 v) { + x = v.x; + y = v.y; + z = v.z; + } + + public static final Vector3 decode(short n, Vector3 dest) { + int t = (n & 0xFF00) >>> 8; + int p = n & 0xFF; + dest.x = SIN_THETA[t] * COS_PHI[p]; + dest.y = SIN_THETA[t] * SIN_PHI[p]; + dest.z = COS_THETA[t]; + return dest; + } + + public static final Vector3 decode(short n) { + return decode(n, new Vector3()); + } + + public final short encode() { + int theta = (int) (Math.acos(z) * (256.0 / Math.PI)); + if (theta > 255) + theta = 255; + int phi = (int) (Math.atan2(y, x) * (128.0 / Math.PI)); + if (phi < 0) + phi += 256; + else if (phi > 255) + phi = 255; + return (short) (((theta & 0xFF) << 8) | (phi & 0xFF)); + } + + public float get(int i) { + switch (i) { + case 0: + return x; + case 1: + return y; + default: + return z; + } + } + + public final float length() { + return (float) Math.sqrt((x * x) + (y * y) + (z * z)); + } + + public final float lengthSquared() { + return (x * x) + (y * y) + (z * z); + } + + public final Vector3 negate() { + x = -x; + y = -y; + z = -z; + return this; + } + + public final Vector3 negate(Vector3 dest) { + dest.x = -x; + dest.y = -y; + dest.z = -z; + return dest; + } + + public final Vector3 mul(float s) { + x *= s; + y *= s; + z *= s; + return this; + } + + public final Vector3 mul(float s, Vector3 dest) { + dest.x = x * s; + dest.y = y * s; + dest.z = z * s; + return dest; + } + + public final Vector3 div(float d) { + x /= d; + y /= d; + z /= d; + return this; + } + + public final Vector3 div(float d, Vector3 dest) { + dest.x = x / d; + dest.y = y / d; + dest.z = z / d; + return dest; + } + + public final float normalizeLength() { + float n = (float) Math.sqrt(x * x + y * y + z * z); + float in = 1.0f / n; + x *= in; + y *= in; + z *= in; + return n; + } + + public final Vector3 normalize() { + float in = 1.0f / (float) Math.sqrt((x * x) + (y * y) + (z * z)); + x *= in; + y *= in; + z *= in; + return this; + } + + public final Vector3 normalize(Vector3 dest) { + float in = 1.0f / (float) Math.sqrt((x * x) + (y * y) + (z * z)); + dest.x = x * in; + dest.y = y * in; + dest.z = z * in; + return dest; + } + + public final Vector3 set(float x, float y, float z) { + this.x = x; + this.y = y; + this.z = z; + return this; + } + + public final Vector3 set(Vector3 v) { + x = v.x; + y = v.y; + z = v.z; + return this; + } + + public final float dot(float vx, float vy, float vz) { + return vx * x + vy * y + vz * z; + } + + public static final float dot(Vector3 v1, Vector3 v2) { + return (v1.x * v2.x) + (v1.y * v2.y) + (v1.z * v2.z); + } + + public static final Vector3 cross(Vector3 v1, Vector3 v2, Vector3 dest) { + dest.x = (v1.y * v2.z) - (v1.z * v2.y); + dest.y = (v1.z * v2.x) - (v1.x * v2.z); + dest.z = (v1.x * v2.y) - (v1.y * v2.x); + return dest; + } + + public static final Vector3 add(Vector3 v1, Vector3 v2, Vector3 dest) { + dest.x = v1.x + v2.x; + dest.y = v1.y + v2.y; + dest.z = v1.z + v2.z; + return dest; + } + + public static final Vector3 sub(Vector3 v1, Vector3 v2, Vector3 dest) { + dest.x = v1.x - v2.x; + dest.y = v1.y - v2.y; + dest.z = v1.z - v2.z; + return dest; + } + + @Override + public final String toString() { + return String.format("(%.2f, %.2f, %.2f)", x, y, z); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/BenchmarkFramework.java b/src/main/java/org/sunflow/system/BenchmarkFramework.java new file mode 100644 index 0000000..b675261 --- /dev/null +++ b/src/main/java/org/sunflow/system/BenchmarkFramework.java @@ -0,0 +1,67 @@ +package org.sunflow.system; + +import org.sunflow.system.UI.Module; + +/** + * This class provides a very simple framework for running a BenchmarkTest + * kernel several times and time the results. + */ +public class BenchmarkFramework { + private Timer[] timers; + private int timeLimit; // time limit in seconds + + public BenchmarkFramework(int iterations, int timeLimit) { + this.timeLimit = timeLimit; + timers = new Timer[iterations]; + } + + public void execute(BenchmarkTest test) { + // clear previous results + for (int i = 0; i < timers.length; i++) + timers[i] = null; + // loop for the specified number of iterations or until the time limit + long startTime = System.nanoTime(); + for (int i = 0; i < timers.length && ((System.nanoTime() - startTime) / 1000000000) < timeLimit; i++) { + UI.printInfo(Module.BENCH, "Running iteration %d", (i + 1)); + timers[i] = new Timer(); + test.kernelBegin(); + timers[i].start(); + test.kernelMain(); + timers[i].end(); + test.kernelEnd(); + } + // report stats + double avg = 0; + double min = Double.POSITIVE_INFINITY; + double max = Double.NEGATIVE_INFINITY; + int n = 0; + for (Timer t : timers) { + if (t == null) + break; + double s = t.seconds(); + min = Math.min(min, s); + max = Math.max(max, s); + avg += s; + n++; + } + if (n == 0) + return; + avg /= n; + double stdDev = 0; + for (Timer t : timers) { + if (t == null) + break; + double s = t.seconds(); + stdDev += (s - avg) * (s - avg); + } + stdDev = Math.sqrt(stdDev / n); + UI.printInfo(Module.BENCH, "Benchmark results:"); + UI.printInfo(Module.BENCH, " * Iterations: %d", n); + UI.printInfo(Module.BENCH, " * Average: %s", Timer.toString(avg)); + UI.printInfo(Module.BENCH, " * Fastest: %s", Timer.toString(min)); + UI.printInfo(Module.BENCH, " * Longest: %s", Timer.toString(max)); + UI.printInfo(Module.BENCH, " * Deviation: %s", Timer.toString(stdDev)); + for (int i = 0; i < timers.length && timers[i] != null; i++) + UI.printDetailed(Module.BENCH, " * Iteration %d: %s", i + 1, timers[i]); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/BenchmarkTest.java b/src/main/java/org/sunflow/system/BenchmarkTest.java new file mode 100644 index 0000000..aa30a91 --- /dev/null +++ b/src/main/java/org/sunflow/system/BenchmarkTest.java @@ -0,0 +1,17 @@ +package org.sunflow.system; + +/** + * This interface is used to represent a piece of code which is to be + * benchmarked by repeatedly running and timing the kernel code. The begin/end + * routines are called per-iteration to do any local initialization which is not + * meant to be taken into acount in the timing (like preparing or destroying + * data structures). + */ +public interface BenchmarkTest { + + public void kernelBegin(); + + public void kernelMain(); + + public void kernelEnd(); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/ByteUtil.java b/src/main/java/org/sunflow/system/ByteUtil.java new file mode 100644 index 0000000..3869cf5 --- /dev/null +++ b/src/main/java/org/sunflow/system/ByteUtil.java @@ -0,0 +1,118 @@ +package org.sunflow.system; + +public class ByteUtil { + + public static final byte[] get2Bytes(int i) { + byte[] b = new byte[2]; + + b[0] = (byte) (i & 0xFF); + b[1] = (byte) ((i >> 8) & 0xFF); + + return b; + } + + public static final byte[] get4Bytes(int i) { + byte[] b = new byte[4]; + + b[0] = (byte) (i & 0xFF); + b[1] = (byte) ((i >> 8) & 0xFF); + b[2] = (byte) ((i >> 16) & 0xFF); + b[3] = (byte) ((i >> 24) & 0xFF); + + return b; + } + + public static final byte[] get4BytesInv(int i) { + byte[] b = new byte[4]; + + b[3] = (byte) (i & 0xFF); + b[2] = (byte) ((i >> 8) & 0xFF); + b[1] = (byte) ((i >> 16) & 0xFF); + b[0] = (byte) ((i >> 24) & 0xFF); + + return b; + } + + public static final byte[] get8Bytes(long i) { + byte[] b = new byte[8]; + + b[0] = (byte) (i & 0xFF); + b[1] = (byte) ((i >> 8) & 0xFF); + b[2] = (byte) ((i >> 16) & 0xFF); + b[3] = (byte) ((i >> 24) & 0xFF); + + b[4] = (byte) ((i >> 32) & 0xFF); + b[5] = (byte) ((i >> 40) & 0xFF); + b[6] = (byte) ((i >> 48) & 0xFF); + b[7] = (byte) ((i >> 56) & 0xFF); + + return b; + } + + public static final long toLong(byte[] in) { + return (((toInt(in[0], in[1], in[2], in[3]))) | ((long) (toInt(in[4], in[5], in[6], in[7])) << (long) 32)); + } + + public static final int toInt(byte in0, byte in1, byte in2, byte in3) { + return (in0 & 0xFF) | ((in1 & 0xFF) << 8) | ((in2 & 0xFF) << 16) | ((in3 & 0xFF) << 24); + } + + public static final int toInt(byte[] in) { + return toInt(in[0], in[1], in[2], in[3]); + } + + public static final int toInt(byte[] in, int ofs) { + return toInt(in[ofs + 0], in[ofs + 1], in[ofs + 2], in[ofs + 3]); + } + + public static final int floatToHalf(float f) { + int i = Float.floatToRawIntBits(f); + // unpack the s, e and m of the float + int s = (i >> 16) & 0x00008000; + int e = ((i >> 23) & 0x000000ff) - (127 - 15); + int m = i & 0x007fffff; + // pack them back up, forming a half + if (e <= 0) { + if (e < -10) { + // E is less than -10. The absolute value of f is less than + // HALF_MIN + // convert f to 0 + return 0; + } + // E is between -10 and 0. + m = (m | 0x00800000) >> (1 - e); + // Round to nearest, round "0.5" up. + if ((m & 0x00001000) == 0x00001000) + m += 0x00002000; + // Assemble the half from s, e (zero) and m. + return s | (m >> 13); + } else if (e == 0xff - (127 - 15)) { + if (m == 0) { + // F is an infinity; convert f to a half infinity + return s | 0x7c00; + } else { + // F is a NAN; we produce a half NAN that preserves the sign bit + // and the 10 leftmost bits of the significand of f + m >>= 13; + return s | 0x7c00 | m | ((m == 0) ? 0 : 1); + } + } else { + // E is greater than zero. F is a normalized float. Round to + // nearest, round "0.5" up + if ((m & 0x00001000) == 0x00001000) { + m += 0x00002000; + if ((m & 0x00800000) == 0x00800000) { + m = 0; + e += 1; + } + } + // Handle exponent overflow + if (e > 30) { + // overflow (); // Cause a hardware floating point overflow; + return s | 0x7c00; // if this returns, the half becomes an + } // infinity with the same sign as f. + // Assemble the half from s, e and m. + return s | (e << 10) | (m >> 13); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/FileUtils.java b/src/main/java/org/sunflow/system/FileUtils.java new file mode 100644 index 0000000..3db7e7c --- /dev/null +++ b/src/main/java/org/sunflow/system/FileUtils.java @@ -0,0 +1,24 @@ +package org.sunflow.system; + +import java.io.File; +import java.util.Locale; + +public final class FileUtils { + /** + * Extract the file extension from the specified filename. + * + * @param filename filename to get the extension of + * @return a string representing the file extension, or null + * if the filename doesn't have any extension, or is not a file + */ + public static final String getExtension(String filename) { + if (filename == null) + return null; + File f = new File(filename); + if (f.isDirectory()) + return null; + String name = new File(filename).getName(); + int idx = name.lastIndexOf('.'); + return idx == -1 ? null : name.substring(idx + 1).toLowerCase(Locale.ENGLISH); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/ImagePanel.java b/src/main/java/org/sunflow/system/ImagePanel.java new file mode 100644 index 0000000..77c7e18 --- /dev/null +++ b/src/main/java/org/sunflow/system/ImagePanel.java @@ -0,0 +1,262 @@ +package org.sunflow.system; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.event.InputEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; +import javax.swing.JPanel; +import javax.swing.event.MouseInputAdapter; + +import org.sunflow.core.Display; +import org.sunflow.image.Color; + +@SuppressWarnings("serial") +public class ImagePanel extends JPanel implements Display { + private static final int[] BORDERS = { Color.RED.toRGB(), + Color.GREEN.toRGB(), Color.BLUE.toRGB(), Color.YELLOW.toRGB(), + Color.CYAN.toRGB(), Color.MAGENTA.toRGB() }; + private BufferedImage image; + private float xo, yo; + private float w, h; + private long repaintCounter; + + private class ScrollZoomListener extends MouseInputAdapter { + int mx; + int my; + boolean dragging; + boolean zooming; + + @Override + public void mousePressed(MouseEvent e) { + mx = e.getX(); + my = e.getY(); + switch (e.getButton()) { + case MouseEvent.BUTTON1: + dragging = true; + zooming = false; + break; + case MouseEvent.BUTTON2: { + dragging = zooming = false; + // if CTRL is pressed + if ((e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) == InputEvent.CTRL_DOWN_MASK) + fit(); + else + reset(); + break; + } + case MouseEvent.BUTTON3: + zooming = true; + dragging = false; + break; + default: + return; + } + repaint(); + } + + @Override + public void mouseDragged(MouseEvent e) { + int mx2 = e.getX(); + int my2 = e.getY(); + if (dragging) + drag(mx2 - mx, my2 - my); + if (zooming) + zoom(mx2 - mx, my2 - my); + mx = mx2; + my = my2; + } + + @Override + public void mouseReleased(MouseEvent e) { + // same behaviour + mouseDragged(e); + } + + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + zoom(-20 * e.getWheelRotation(), 0); + } + } + + public ImagePanel() { + setPreferredSize(new Dimension(640, 480)); + image = null; + xo = yo = 0; + w = h = 0; + ScrollZoomListener listener = new ScrollZoomListener(); + addMouseListener(listener); + addMouseMotionListener(listener); + addMouseWheelListener(listener); + } + + public void save(String filename) { + // Bitmap.save(image, filename); + try { + ImageIO.write(image, "png", new File(filename)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private synchronized void drag(int dx, int dy) { + xo += dx; + yo += dy; + repaint(); + } + + private synchronized void zoom(int dx, int dy) { + int a = Math.max(dx, dy); + int b = Math.min(dx, dy); + if (Math.abs(b) > Math.abs(a)) + a = b; + if (a == 0) + return; + // window center + float cx = getWidth() * 0.5f; + float cy = getHeight() * 0.5f; + + // origin of the image in window space + float x = xo + (getWidth() - w) * 0.5f; + float y = yo + (getHeight() - h) * 0.5f; + + // coordinates of the pixel we are over + float sx = cx - x; + float sy = cy - y; + + // scale + if (w + a > 100) { + h = (w + a) * h / w; + sx = (w + a) * sx / w; + sy = (w + a) * sy / w; + w = (w + a); + } + + // restore center pixel + + float x2 = cx - sx; + float y2 = cy - sy; + + xo = (x2 - (getWidth() - w) * 0.5f); + yo = (y2 - (getHeight() - h) * 0.5f); + + repaint(); + } + + public synchronized void reset() { + xo = yo = 0; + if (image != null) { + w = image.getWidth(); + h = image.getHeight(); + } + repaint(); + } + + public synchronized void fit() { + xo = yo = 0; + if (image != null) { + float wx = Math.max(getWidth() - 10, 100); + float hx = wx * image.getHeight() / image.getWidth(); + float hy = Math.max(getHeight() - 10, 100); + float wy = hy * image.getWidth() / image.getHeight(); + if (hx > hy) { + w = wy; + h = hy; + } else { + w = wx; + h = hx; + } + repaint(); + } + } + + public synchronized void imageBegin(int w, int h, int bucketSize) { + if (image != null && w == image.getWidth() && h == image.getHeight()) { + // dull image if it has same resolution (75%) + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int rgba = image.getRGB(x, y); + image.setRGB(x, y, ((rgba & 0xFEFEFEFE) >>> 1) + ((rgba & 0xFCFCFCFC) >>> 2)); + } + } + } else { + // allocate new framebuffer + image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + // center + this.w = w; + this.h = h; + xo = yo = 0; + } + repaintCounter = System.nanoTime(); + repaint(); + } + + public synchronized void imagePrepare(int x, int y, int w, int h, int id) { + int border = BORDERS[id % BORDERS.length] | 0xFF000000; + for (int by = 0; by < h; by++) { + for (int bx = 0; bx < w; bx++) { + if (bx == 0 || bx == w - 1) { + if (5 * by < h || 5 * (h - by - 1) < h) + image.setRGB(x + bx, y + by, border); + } else if (by == 0 || by == h - 1) { + if (5 * bx < w || 5 * (w - bx - 1) < w) + image.setRGB(x + bx, y + by, border); + } + } + } + repaint(); + } + + public synchronized void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha) { + for (int j = 0, index = 0; j < h; j++) + for (int i = 0; i < w; i++, index++) + image.setRGB(x + i, y + j, data[index].copy().mul(1.0f / alpha[index]).toNonLinear().toRGBA(alpha[index])); + repaint(); + } + + public synchronized void imageFill(int x, int y, int w, int h, Color c, float alpha) { + int rgba = c.copy().mul(1.0f / alpha).toNonLinear().toRGBA(alpha); + for (int j = 0, index = 0; j < h; j++) + for (int i = 0; i < w; i++, index++) + image.setRGB(x + i, y + j, rgba); + fastRepaint(); + } + + public void imageEnd() { + repaint(); + } + + private void fastRepaint() { + long t = System.nanoTime(); + if (repaintCounter + 125000000 < t) { + repaintCounter = t; + repaint(); + } + } + + @Override + public synchronized void paintComponent(Graphics g) { + super.paintComponent(g); + if (image == null) + return; + int x = Math.round(xo + (getWidth() - w) * 0.5f); + int y = Math.round(yo + (getHeight() - h) * 0.5f); + int iw = Math.round(w); + int ih = Math.round(h); + int x0 = x - 1; + int y0 = y - 1; + int x1 = x + iw + 1; + int y1 = y + ih + 1; + g.setColor(java.awt.Color.WHITE); + g.drawLine(x0, y0, x1, y0); + g.drawLine(x1, y0, x1, y1); + g.drawLine(x1, y1, x0, y1); + g.drawLine(x0, y1, x0, y0); + g.drawImage(image, x, y, iw, ih, java.awt.Color.BLACK, this); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/Memory.java b/src/main/java/org/sunflow/system/Memory.java new file mode 100644 index 0000000..a77666c --- /dev/null +++ b/src/main/java/org/sunflow/system/Memory.java @@ -0,0 +1,15 @@ +package org.sunflow.system; + +public final class Memory { + public static final String sizeof(int[] array) { + return bytesToString(array == null ? 0 : 4 * array.length); + } + + public static final String bytesToString(long bytes) { + if (bytes < 1024) + return String.format("%db", bytes); + if (bytes < 1024 * 1024) + return String.format("%dKb", (bytes + 512) >>> 10); + return String.format("%dMb", (bytes + 512 * 1024) >>> 20); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/Parser.java b/src/main/java/org/sunflow/system/Parser.java new file mode 100644 index 0000000..ae351b4 --- /dev/null +++ b/src/main/java/org/sunflow/system/Parser.java @@ -0,0 +1,155 @@ +package org.sunflow.system; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; + +public class Parser { + private FileReader file; + private BufferedReader bf; + private String[] lineTokens; + private int index; + + public Parser(String filename) throws FileNotFoundException { + file = new FileReader(filename); + bf = new BufferedReader(file); + lineTokens = new String[0]; + index = 0; + } + + public void close() throws IOException { + if (file != null) + file.close(); + bf = null; + } + + public String getNextToken() throws IOException { + while (true) { + String tok = fetchNextToken(); + if (tok == null) + return null; + if (tok.equals("/*")) { + do { + tok = fetchNextToken(); + if (tok == null) + return null; + } while (!tok.equals("*/")); + } else + return tok; + } + } + + public boolean peekNextToken(String tok) throws IOException { + while (true) { + String t = fetchNextToken(); + if (t == null) + return false; // nothing left + if (t.equals("/*")) { + do { + t = fetchNextToken(); + if (t == null) + return false; // nothing left + } while (!t.equals("*/")); + } else if (t.equals(tok)) { + // we found the right token, keep parsing + return true; + } else { + // rewind the token so we can try again + index--; + return false; + } + } + } + + private String fetchNextToken() throws IOException { + if (bf == null) + return null; + while (true) { + if (index < lineTokens.length) + return lineTokens[index++]; + else if (!getNextLine()) + return null; + } + } + + private boolean getNextLine() throws IOException { + String line = bf.readLine(); + + if (line == null) + return false; + + ArrayList tokenList = new ArrayList(); + String current = new String(); + boolean inQuotes = false; + + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (current.length() == 0 && (c == '%' || c == '#')) + break; + + boolean quote = c == '\"'; + inQuotes = inQuotes ^ quote; + + if (!quote && (inQuotes || !Character.isWhitespace(c))) + current += c; + else if (current.length() > 0) { + tokenList.add(current); + current = new String(); + } + } + + if (current.length() > 0) + tokenList.add(current); + lineTokens = tokenList.toArray(new String[0]); + index = 0; + return true; + } + + public String getNextCodeBlock() throws ParserException, IOException { + // read a java code block + String code = new String(); + checkNextToken(""); + while (true) { + String line; + try { + line = bf.readLine(); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + if (line.trim().equals("")) + return code; + code += line; + code += "\n"; + } + } + + public boolean getNextBoolean() throws IOException { + return Boolean.valueOf(getNextToken()).booleanValue(); + } + + public int getNextInt() throws IOException { + return Integer.parseInt(getNextToken()); + } + + public float getNextFloat() throws IOException { + return Float.parseFloat(getNextToken()); + } + + public void checkNextToken(String token) throws ParserException, IOException { + String found = getNextToken(); + if (!token.equals(found)) { + close(); + throw new ParserException(token, found); + } + } + + @SuppressWarnings("serial") + public static class ParserException extends Exception { + private ParserException(String token, String found) { + super(String.format("Expecting %s found %s", token, found)); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/Plugins.java b/src/main/java/org/sunflow/system/Plugins.java new file mode 100644 index 0000000..991e9ee --- /dev/null +++ b/src/main/java/org/sunflow/system/Plugins.java @@ -0,0 +1,153 @@ +package org.sunflow.system; + +import org.codehaus.janino.ClassBodyEvaluator; +import org.codehaus.janino.CompileException; +import org.codehaus.janino.Parser.ParseException; +import org.codehaus.janino.Scanner.ScanException; +import org.sunflow.system.UI.Module; +import org.sunflow.util.FastHashMap; + +/** + * This class represents a list of plugins which implement a certain interface + * or extend a certain class. Many plugins may be registered and created at a + * later time by recalling their unique name only. + * + * @param Default constructible type or interface all plugins will derive + * from or implement + */ +public final class Plugins { + private final FastHashMap> pluginClasses; + private final Class baseClass; + + /** + * Create an empty plugin list. You must specify T.class as + * an argument. + * + * @param baseClass + */ + public Plugins(Class baseClass) { + pluginClasses = new FastHashMap>(); + this.baseClass = baseClass; + } + + /** + * Create an object from the specified type name. If this type name is + * unknown or invalid, null is returned. + * + * @param name plugin type name + * @return an instance of the specified plugin type, or null + * if not found or invalid + */ + public T createObject(String name) { + if (name == null || name.equals("none")) + return null; + Class c = pluginClasses.get(name); + if (c == null) { + // don't print an error, this will be handled by the caller + return null; + } + try { + return c.newInstance(); + } catch (InstantiationException e) { + UI.printError(Module.API, "Cannot create object of type \"%s\" - %s", name, e.getLocalizedMessage()); + return null; + } catch (IllegalAccessException e) { + UI.printError(Module.API, "Cannot create object of type \"%s\" - %s", name, e.getLocalizedMessage()); + return null; + } + } + + /** + * Check this plugin list for the presence of the specified type name + * + * @param name plugin type name + * @return true if this name has been registered, + * false otherwise + */ + public boolean hasType(String name) { + return pluginClasses.get(name) != null; + } + + /** + * Generate a unique plugin type name which has not yet been registered. + * This is meant to be used when the actual type name is not crucial, but + * succesfully registration is. + * + * @param prefix a prefix to be used in generating the unique name + * @return a unique plugin type name not yet in use + */ + public String generateUniqueName(String prefix) { + String type; + for (int i = 1; hasType(type = String.format("%s_%d", prefix, i)); i++) { + } + return type; + } + + /** + * Define a new plugin type from java source code. The code string contains + * import declarations and a class body only. The implemented type is + * implicitly the one of the plugin list being registered against.If the + * plugin type name was previously associated with a different class, it + * will be overriden. This allows the behavior core classes to be modified + * at runtime. + * + * @param name plugin type name + * @param sourceCode Java source code definition for the plugin + * @return true if the code compiled and registered + * successfully, false otherwise + */ + @SuppressWarnings("unchecked") + public boolean registerPlugin(String name, String sourceCode) { + try { + ClassBodyEvaluator cbe = new ClassBodyEvaluator(); + cbe.setClassName(name); + if (baseClass.isInterface()) + cbe.setImplementedTypes(new Class[] { baseClass }); + else + cbe.setExtendedType(baseClass); + cbe.cook(sourceCode); + return registerPlugin(name, cbe.getClazz()); + } catch (CompileException e) { + UI.printError(Module.API, "Plugin \"%s\" could not be declared - %s", name, e.getLocalizedMessage()); + return false; + } catch (ParseException e) { + UI.printError(Module.API, "Plugin \"%s\" could not be declared - %s", name, e.getLocalizedMessage()); + return false; + } catch (ScanException e) { + UI.printError(Module.API, "Plugin \"%s\" could not be declared - %s", name, e.getLocalizedMessage()); + return false; + } + } + + /** + * Define a new plugin type from an existing class. This checks to make sure + * the provided class is default constructible (ie: has a constructor with + * no parameters). If the plugin type name was previously associated with a + * different class, it will be overriden. This allows the behavior core + * classes to be modified at runtime. + * + * @param name plugin type name + * @param pluginClass class object for the plugin class + * @return true if the plugin registered successfully, + * false otherwise + */ + public boolean registerPlugin(String name, Class pluginClass) { + // check that the given class is compatible with the base class + try { + if (pluginClass.getConstructor() == null) { + UI.printError(Module.API, "Plugin \"%s\" could not be declared - default constructor was not found", name); + return false; + } + } catch (SecurityException e) { + UI.printError(Module.API, "Plugin \"%s\" could not be declared - default constructor is not visible (%s)", name, e.getLocalizedMessage()); + return false; + } catch (NoSuchMethodException e) { + UI.printError(Module.API, "Plugin \"%s\" could not be declared - default constructor was not found (%s)", name, e.getLocalizedMessage()); + return false; + } + if (pluginClasses.get(name) != null) + UI.printWarning(Module.API, "Plugin \"%s\" was already defined - overwriting previous definition", name); + pluginClasses.put(name, pluginClass); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/RenderGlobalsPanel.java b/src/main/java/org/sunflow/system/RenderGlobalsPanel.java new file mode 100644 index 0000000..993358b --- /dev/null +++ b/src/main/java/org/sunflow/system/RenderGlobalsPanel.java @@ -0,0 +1,207 @@ +package org.sunflow.system; + +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GridLayout; + +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.ComboBoxModel; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JTabbedPane; +import javax.swing.JTextField; +import javax.swing.WindowConstants; +import javax.swing.border.BevelBorder; +import javax.swing.border.TitledBorder; + +/** + * This code was edited or generated using CloudGarden's Jigloo SWT/Swing GUI + * Builder, which is free for non-commercial use. If Jigloo is being used + * commercially (ie, by a corporation, company or business for any purpose + * whatever) then you should purchase a license for each developer using Jigloo. + * Please visit www.cloudgarden.com for details. Use of Jigloo implies + * acceptance of these licensing terms. A COMMERCIAL LICENSE HAS NOT BEEN + * PURCHASED FOR THIS MACHINE, SO JIGLOO OR THIS CODE CANNOT BE USED LEGALLY FOR + * ANY CORPORATE OR COMMERCIAL PURPOSE. + */ +@SuppressWarnings("serial") +public class RenderGlobalsPanel extends JTabbedPane { + private JPanel generalPanel; + private JComboBox maxSamplingComboxBox; + private JPanel samplingPanel; + private JComboBox minSamplingComboBox; + private JLabel jLabel6; + private JLabel jLabel5; + private JRadioButton defaultRendererRadioButton; + private JRadioButton bucketRendererRadioButton; + private JPanel bucketRendererPanel; + private JLabel jLabel2; + private JPanel rendererPanel; + private JTextField threadTextField; + private JCheckBox threadCheckBox; + private JLabel jLabel3; + private JPanel threadsPanel; + private JLabel jLabel1; + private JPanel resolutionPanel; + private JTextField resolutionYTextField; + private JTextField resolutionXTextField; + private JCheckBox resolutionCheckBox; + + /** + * This method initializes this + */ + private void initialize() { + + } + + /** + * Auto-generated main method to display this JPanel inside a new JFrame. + */ + public static void main(String[] args) { + JFrame frame = new JFrame(); + frame.getContentPane().add(new RenderGlobalsPanel()); + frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + frame.pack(); + frame.setVisible(true); + } + + public RenderGlobalsPanel() { + super(); + initialize(); + initGUI(); + } + + private void initGUI() { + try { + setPreferredSize(new Dimension(400, 300)); + { + generalPanel = new JPanel(); + FlowLayout generalPanelLayout = new FlowLayout(); + generalPanelLayout.setAlignment(FlowLayout.LEFT); + generalPanel.setLayout(generalPanelLayout); + this.addTab("General", null, generalPanel, null); + { + resolutionPanel = new JPanel(); + generalPanel.add(resolutionPanel); + FlowLayout resolutionPanelLayout = new FlowLayout(); + resolutionPanel.setLayout(resolutionPanelLayout); + resolutionPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(BevelBorder.LOWERED), "Resolution", TitledBorder.LEADING, TitledBorder.TOP)); + { + resolutionCheckBox = new JCheckBox(); + resolutionPanel.add(resolutionCheckBox); + resolutionCheckBox.setText("Override"); + } + { + jLabel1 = new JLabel(); + resolutionPanel.add(jLabel1); + jLabel1.setText("Image Width:"); + } + { + resolutionXTextField = new JTextField(); + resolutionPanel.add(resolutionXTextField); + resolutionXTextField.setText("640"); + resolutionXTextField.setPreferredSize(new java.awt.Dimension(50, 20)); + } + { + jLabel2 = new JLabel(); + resolutionPanel.add(jLabel2); + jLabel2.setText("Image Height:"); + } + { + resolutionYTextField = new JTextField(); + resolutionPanel.add(resolutionYTextField); + resolutionYTextField.setText("480"); + resolutionYTextField.setPreferredSize(new java.awt.Dimension(50, 20)); + } + } + { + threadsPanel = new JPanel(); + generalPanel.add(threadsPanel); + threadsPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(BevelBorder.LOWERED), "Threads", TitledBorder.LEADING, TitledBorder.TOP)); + { + threadCheckBox = new JCheckBox(); + threadsPanel.add(threadCheckBox); + threadCheckBox.setText("Use All Processors"); + } + { + jLabel3 = new JLabel(); + threadsPanel.add(jLabel3); + jLabel3.setText("Threads:"); + } + { + threadTextField = new JTextField(); + threadsPanel.add(threadTextField); + threadTextField.setText("1"); + threadTextField.setPreferredSize(new java.awt.Dimension(50, 20)); + } + } + } + { + rendererPanel = new JPanel(); + FlowLayout rendererPanelLayout = new FlowLayout(); + rendererPanelLayout.setAlignment(FlowLayout.LEFT); + rendererPanel.setLayout(rendererPanelLayout); + this.addTab("Renderer", null, rendererPanel, null); + { + defaultRendererRadioButton = new JRadioButton(); + rendererPanel.add(defaultRendererRadioButton); + defaultRendererRadioButton.setText("Default Renderer"); + } + { + bucketRendererPanel = new JPanel(); + BoxLayout bucketRendererPanelLayout = new BoxLayout(bucketRendererPanel, javax.swing.BoxLayout.Y_AXIS); + bucketRendererPanel.setLayout(bucketRendererPanelLayout); + rendererPanel.add(bucketRendererPanel); + bucketRendererPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(BevelBorder.LOWERED), "Bucket Renderer", TitledBorder.LEADING, TitledBorder.TOP)); + { + bucketRendererRadioButton = new JRadioButton(); + bucketRendererPanel.add(bucketRendererRadioButton); + bucketRendererRadioButton.setText("Enable"); + } + { + samplingPanel = new JPanel(); + GridLayout samplingPanelLayout = new GridLayout(2, 2); + samplingPanelLayout.setColumns(2); + samplingPanelLayout.setHgap(5); + samplingPanelLayout.setVgap(5); + samplingPanelLayout.setRows(2); + samplingPanel.setLayout(samplingPanelLayout); + bucketRendererPanel.add(samplingPanel); + { + jLabel5 = new JLabel(); + samplingPanel.add(jLabel5); + jLabel5.setText("Min:"); + } + { + ComboBoxModel minSamplingComboBoxModel = new DefaultComboBoxModel(new String[] { + "Item One", "Item Two" }); + minSamplingComboBox = new JComboBox(); + samplingPanel.add(minSamplingComboBox); + minSamplingComboBox.setModel(minSamplingComboBoxModel); + } + { + jLabel6 = new JLabel(); + samplingPanel.add(jLabel6); + jLabel6.setText("Max:"); + } + { + ComboBoxModel maxSamplingComboxBoxModel = new DefaultComboBoxModel(new String[] { + "Item One", "Item Two" }); + maxSamplingComboxBox = new JComboBox(); + samplingPanel.add(maxSamplingComboxBox); + maxSamplingComboxBox.setModel(maxSamplingComboxBoxModel); + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/SearchPath.java b/src/main/java/org/sunflow/system/SearchPath.java new file mode 100644 index 0000000..2a6882c --- /dev/null +++ b/src/main/java/org/sunflow/system/SearchPath.java @@ -0,0 +1,61 @@ +package org.sunflow.system; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; + +import org.sunflow.system.UI.Module; + +public class SearchPath { + private LinkedList searchPath; + private String type; + + public SearchPath(String type) { + this.type = type; + searchPath = new LinkedList(); + } + + public void resetSearchPath() { + searchPath.clear(); + } + + public void addSearchPath(String path) { + File f = new File(path); + if (f.exists() && f.isDirectory()) { + try { + path = f.getCanonicalPath(); + for (String prefix : searchPath) + if (prefix.equals(path)) + return; + UI.printInfo(Module.SYS, "Adding %s search path: \"%s\"", type, path); + searchPath.add(path); + } catch (IOException e) { + UI.printError(Module.SYS, "Invalid %s search path specification: \"%s\" - %s", type, path, e.getMessage()); + } + } else + UI.printError(Module.SYS, "Invalid %s search path specification: \"%s\" - invalid directory", type, path); + } + + public String resolvePath(String filename) { + // account for relative naming schemes from 3rd party softwares + if (filename.startsWith("//")) + filename = filename.substring(2); + UI.printDetailed(Module.SYS, "Resolving %s path \"%s\" ...", type, filename); + File f = new File(filename); + if (!f.isAbsolute()) { + for (String prefix : searchPath) { + UI.printDetailed(Module.SYS, " * searching: \"%s\" ...", prefix); + if (prefix.endsWith(File.separator) || filename.startsWith(File.separator)) + f = new File(prefix + filename); + else + f = new File(prefix + File.separator + filename); + if (f.exists()) { + // suggested path exists - try it + return f.getAbsolutePath(); + } + } + } + // file was not found in the search paths - return the filename itself + return filename; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/Timer.java b/src/main/java/org/sunflow/system/Timer.java new file mode 100644 index 0000000..ebf55b7 --- /dev/null +++ b/src/main/java/org/sunflow/system/Timer.java @@ -0,0 +1,51 @@ +package org.sunflow.system; + +public class Timer { + private long startTime, endTime; + + public Timer() { + startTime = endTime = 0; + } + + public void start() { + startTime = endTime = System.nanoTime(); + } + + public void end() { + endTime = System.nanoTime(); + } + + public long nanos() { + return endTime - startTime; + } + + public double seconds() { + return (endTime - startTime) * 1e-9; + } + + public static String toString(long nanos) { + Timer t = new Timer(); + t.endTime = nanos; + return t.toString(); + } + + public static String toString(double seconds) { + Timer t = new Timer(); + t.endTime = (long) (seconds * 1e9); + return t.toString(); + } + + @Override + public String toString() { + long millis = nanos() / (1000 * 1000); + if (millis < 10000) + return String.format("%dms", millis); + long hours = millis / (60 * 60 * 1000); + millis -= hours * 60 * 60 * 1000; + long minutes = millis / (60 * 1000); + millis -= minutes * 60 * 1000; + long seconds = millis / 1000; + millis -= seconds * 1000; + return String.format("%d:%02d:%02d.%1d", hours, minutes, seconds, millis / 100); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/UI.java b/src/main/java/org/sunflow/system/UI.java new file mode 100644 index 0000000..9f5cfd1 --- /dev/null +++ b/src/main/java/org/sunflow/system/UI.java @@ -0,0 +1,103 @@ +package org.sunflow.system; + +import java.util.Locale; + +import org.sunflow.system.ui.ConsoleInterface; +import org.sunflow.system.ui.SilentInterface; + +/** + * Static singleton interface to a UserInterface object. This is set to a text + * console by default. + */ +public final class UI { + private static UserInterface ui = new ConsoleInterface(); + private static boolean canceled = false; + private static int verbosity = 3; + + public enum Module { + API, GEOM, HAIR, ACCEL, BCKT, IPR, LIGHT, GUI, SCENE, BENCH, TEX, IMG, DISP, QMC, SYS, USER, CAM, + } + + public enum PrintLevel { + ERROR, WARN, INFO, DETAIL + } + + private UI() { + } + + /** + * Sets the active user interface implementation. Passing null + * silences printing completely. + * + * @param ui object to recieve all user interface calls + */ + public final static void set(UserInterface ui) { + if (ui == null) + ui = new SilentInterface(); + UI.ui = ui; + } + + public final static void verbosity(int verbosity) { + UI.verbosity = verbosity; + } + + public final static String formatOutput(Module m, PrintLevel level, String s) { + return String.format("%-5s %-6s: %s", m.name(), level.name().toLowerCase(Locale.ENGLISH), s); + } + + public final static synchronized void printDetailed(Module m, String s, Object... args) { + if (verbosity > 3) + ui.print(m, PrintLevel.DETAIL, String.format(s, args)); + } + + public final static synchronized void printInfo(Module m, String s, Object... args) { + if (verbosity > 2) + ui.print(m, PrintLevel.INFO, String.format(s, args)); + } + + public final static synchronized void printWarning(Module m, String s, Object... args) { + if (verbosity > 1) + ui.print(m, PrintLevel.WARN, String.format(s, args)); + } + + public final static synchronized void printError(Module m, String s, Object... args) { + if (verbosity > 0) + ui.print(m, PrintLevel.ERROR, String.format(s, args)); + } + + public final static synchronized void taskStart(String s, int min, int max) { + ui.taskStart(s, min, max); + } + + public final static synchronized void taskUpdate(int current) { + ui.taskUpdate(current); + } + + public final static synchronized void taskStop() { + ui.taskStop(); + // reset canceled status + // this assume the parent application will deal with it immediately + canceled = false; + } + + /** + * Cancel the currently active task. This forces the application to abort as + * soon as possible. + */ + public final static synchronized void taskCancel() { + printInfo(Module.GUI, "Abort requested by the user ..."); + canceled = true; + } + + /** + * Check to see if the current task should be aborted. + * + * @return true if the current task should be stopped, + * false otherwise + */ + public final static synchronized boolean taskCanceled() { + if (canceled) + printInfo(Module.GUI, "Abort request noticed by the current task"); + return canceled; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/UserInterface.java b/src/main/java/org/sunflow/system/UserInterface.java new file mode 100644 index 0000000..9863b31 --- /dev/null +++ b/src/main/java/org/sunflow/system/UserInterface.java @@ -0,0 +1,45 @@ +package org.sunflow.system; + +import org.sunflow.system.UI.Module; +import org.sunflow.system.UI.PrintLevel; + +public interface UserInterface { + /** + * Displays some information to the user from the specified module with the + * specified print level. A user interface is free to show or ignore any + * message. Level filtering is done in the core and shouldn't be + * re-implemented by the user interface. All messages will be short enough + * to fit on one line. + * + * @param m module the message came from + * @param level seriousness of the message + * @param s string to display + */ + void print(Module m, PrintLevel level, String s); + + /** + * Prepare a progress bar representing a lengthy task. The actual progress + * is first shown by the call to update and closed when update is closed + * with the max value. It is currently not possible to nest calls to + * setTask, so only one task needs to be tracked at a time. + * + * @param s desriptive string + * @param min minimum value of the task + * @param max maximum value of the task + */ + void taskStart(String s, int min, int max); + + /** + * Updates the current progress bar to a value between the current min and + * max. When min or max are passed the progressed bar is shown or hidden + * respectively. + * + * @param current current value of the task in progress. + */ + void taskUpdate(int current); + + /** + * Closes the current progress bar to indicate the task is over + */ + void taskStop(); +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/ui/ConsoleInterface.java b/src/main/java/org/sunflow/system/ui/ConsoleInterface.java new file mode 100644 index 0000000..5d85595 --- /dev/null +++ b/src/main/java/org/sunflow/system/ui/ConsoleInterface.java @@ -0,0 +1,42 @@ +package org.sunflow.system.ui; + +import org.sunflow.system.UI; +import org.sunflow.system.UserInterface; +import org.sunflow.system.UI.Module; +import org.sunflow.system.UI.PrintLevel; + +/** + * Basic console implementation of a user interface. + */ +public class ConsoleInterface implements UserInterface { + private int min; + private int max; + private float invP; + private String task; + private int lastP; + + public ConsoleInterface() { + } + + public void print(Module m, PrintLevel level, String s) { + System.err.println(UI.formatOutput(m, level, s)); + } + + public void taskStart(String s, int min, int max) { + task = s; + this.min = min; + this.max = max; + lastP = -1; + invP = 100.0f / (max - min); + } + + public void taskUpdate(int current) { + int p = (min == max) ? 0 : (int) ((current - min) * invP); + if (p != lastP) + System.err.print(task + " [" + (lastP = p) + "%]\r"); + } + + public void taskStop() { + System.err.print(" \r"); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/ui/SilentInterface.java b/src/main/java/org/sunflow/system/ui/SilentInterface.java new file mode 100644 index 0000000..556d509 --- /dev/null +++ b/src/main/java/org/sunflow/system/ui/SilentInterface.java @@ -0,0 +1,23 @@ +package org.sunflow.system.ui; + +import org.sunflow.system.UserInterface; +import org.sunflow.system.UI.Module; +import org.sunflow.system.UI.PrintLevel; + +/** + * Null implementation of a user interface. This is usefull to silence the + * output. + */ +public class SilentInterface implements UserInterface { + public void print(Module m, PrintLevel level, String s) { + } + + public void taskStart(String s, int min, int max) { + } + + public void taskUpdate(int current) { + } + + public void taskStop() { + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/util/FastHashMap.java b/src/main/java/org/sunflow/util/FastHashMap.java new file mode 100644 index 0000000..61e89a9 --- /dev/null +++ b/src/main/java/org/sunflow/util/FastHashMap.java @@ -0,0 +1,206 @@ +package org.sunflow.util; + +import java.util.Iterator; + +/** + * Fast hash map implementation which uses array storage along with quadratic + * probing to resolve collisions. The capacity is doubled when the load goes + * beyond 50% and is halved when the load drops below 20%. + * + * @param + * @param + */ +public class FastHashMap implements Iterable> { + private static final int MIN_SIZE = 4; + + public static class Entry { + private final K k; + private V v; + + private Entry(K k, V v) { + this.k = k; + this.v = v; + } + + private boolean isRemoved() { + return v == null; + } + + private void remove() { + v = null; + } + + public K getKey() { + return k; + } + + public V getValue() { + return v; + } + } + + private Entry[] entries; + private int size; + + public FastHashMap() { + clear(); + } + + public void clear() { + size = 0; + entries = alloc(MIN_SIZE); + } + + public V put(K k, V v) { + int hash = k.hashCode(), t = 0; + int pos = entries.length; // mark invalid position + for (;;) { + hash &= entries.length - 1; + if (entries[hash] == null) + break; // done probing + else if (entries[hash].isRemoved() && pos == entries.length) + pos = hash; // store, but keep searching + else if (entries[hash].k.equals(k)) { + // update entry + V old = entries[hash].v; + entries[hash].v = v; + return old; + } + t++; + hash += t; + } + // did we find a spot for insertion among the deleted values ? + if (pos < entries.length) + hash = pos; + entries[hash] = new Entry(k, v); + size++; + if (size * 2 > entries.length) + resize(entries.length * 2); + return null; + } + + public V get(K k) { + int hash = k.hashCode(), t = 0; + for (;;) { + hash &= entries.length - 1; + if (entries[hash] == null) + return null; + else if (!entries[hash].isRemoved() && entries[hash].k.equals(k)) + return entries[hash].v; + t++; + hash += t; + } + } + + public boolean containsKey(K k) { + int hash = k.hashCode(), t = 0; + for (;;) { + hash &= entries.length - 1; + if (entries[hash] == null) + return false; + else if (!entries[hash].isRemoved() && entries[hash].k.equals(k)) + return true; + t++; + hash += t; + } + } + + public void remove(K k) { + int hash = k.hashCode(), t = 0; + for (;;) { + hash &= entries.length - 1; + if (entries[hash] == null) + return; // not found, return + else if (!entries[hash].isRemoved() && entries[hash].k.equals(k)) { + entries[hash].remove(); // flag as removed + size--; + break; + } + t++; + hash += t; + } + // do we need to shrink? + if (entries.length > MIN_SIZE && size * 10 < 2 * entries.length) + resize(entries.length / 2); + } + + /** + * Resize internal storage to the specified capacity. The capacity must be a + * power of two. + * + * @param capacity new capacity for the internal array + */ + private void resize(int capacity) { + assert (capacity & (capacity - 1)) == 0; + assert capacity >= MIN_SIZE; + Entry[] newentries = alloc(capacity); + for (Entry e : entries) { + if (e == null || e.isRemoved()) + continue; + int hash = e.k.hashCode(), t = 0; + for (;;) { + hash &= newentries.length - 1; + if (newentries[hash] == null) + break; + assert !newentries[hash].k.equals(e.k); + t++; + hash += t; + } + newentries[hash] = new Entry(e.k, e.v); + } + // copy new entries over old ones + entries = newentries; + } + + /** + * Wrap the entry array allocation because it requires silencing some + * generics warnings. + * + * @param size number of elements to allocate + * @return + */ + @SuppressWarnings("unchecked") + private Entry[] alloc(int size) { + return new Entry[size]; + } + + private class EntryIterator implements Iterator> { + private int index; + + private EntryIterator() { + index = 0; + if (!readable()) + inc(); + } + + private boolean readable() { + return !(entries[index] == null || entries[index].isRemoved()); + } + + private void inc() { + do { + index++; + } while (hasNext() && !readable()); + } + + public boolean hasNext() { + return index < entries.length; + } + + public Entry next() { + try { + return entries[index]; + } finally { + inc(); + } + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } + + public Iterator> iterator() { + return new EntryIterator(); + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/util/FloatArray.java b/src/main/java/org/sunflow/util/FloatArray.java new file mode 100644 index 0000000..d028486 --- /dev/null +++ b/src/main/java/org/sunflow/util/FloatArray.java @@ -0,0 +1,76 @@ +package org.sunflow.util; + +public final class FloatArray { + private float[] array; + private int size; + + public FloatArray() { + array = new float[10]; + size = 0; + } + + public FloatArray(int capacity) { + array = new float[capacity]; + size = 0; + } + + /** + * Append a float to the end of the array. + * + * @param f + */ + public final void add(float f) { + if (size == array.length) { + float[] oldArray = array; + array = new float[(size * 3) / 2 + 1]; + System.arraycopy(oldArray, 0, array, 0, size); + } + array[size] = f; + size++; + } + + /** + * Write a value to the specified index. Assumes the array is already big + * enough. + * + * @param index + * @param value + */ + public final void set(int index, float value) { + array[index] = value; + } + + /** + * Read value from the array. + * + * @param index index into the array + * @return value at the specified index + */ + public final float get(int index) { + return array[index]; + } + + /** + * Returns the number of elements added to the array. + * + * @return current size of the array + */ + public final int getSize() { + return size; + } + + /** + * Return a copy of the array, trimmed to fit the size of its contents + * exactly. + * + * @return a new array of exactly the right length + */ + public final float[] trim() { + if (size < array.length) { + float[] oldArray = array; + array = new float[size]; + System.arraycopy(oldArray, 0, array, 0, size); + } + return array; + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/util/IntArray.java b/src/main/java/org/sunflow/util/IntArray.java new file mode 100644 index 0000000..b66406a --- /dev/null +++ b/src/main/java/org/sunflow/util/IntArray.java @@ -0,0 +1,76 @@ +package org.sunflow.util; + +public final class IntArray { + private int[] array; + private int size; + + public IntArray() { + array = new int[10]; + size = 0; + } + + public IntArray(int capacity) { + array = new int[capacity]; + size = 0; + } + + /** + * Append an integer to the end of the array. + * + * @param i + */ + public final void add(int i) { + if (size == array.length) { + int[] oldArray = array; + array = new int[(size * 3) / 2 + 1]; + System.arraycopy(oldArray, 0, array, 0, size); + } + array[size] = i; + size++; + } + + /** + * Write a value to the specified index. Assumes the array is already big + * enough. + * + * @param index + * @param value + */ + public final void set(int index, int value) { + array[index] = value; + } + + /** + * Read value from the array. + * + * @param index index into the array + * @return value at the specified index + */ + public final int get(int index) { + return array[index]; + } + + /** + * Returns the number of elements added to the array. + * + * @return current size of the array + */ + public final int getSize() { + return size; + } + + /** + * Return a copy of the array, trimmed to fit the size of its contents + * exactly. + * + * @return a new array of exactly the right length + */ + public final int[] trim() { + if (size < array.length) { + int[] oldArray = array; + array = new int[size]; + System.arraycopy(oldArray, 0, array, 0, size); + } + return array; + } +} \ No newline at end of file From 0e21ca8a89e1cd1978910b1e95a85e84122a4592 Mon Sep 17 00:00:00 2001 From: Karel Petranek Date: Mon, 5 Sep 2016 21:00:56 -0500 Subject: [PATCH 03/37] Fixed infinite loop in bih caused by small triangle (by k-matsuzaki) --- .../org/sunflow/core/accel/BoundingIntervalHierarchy.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/sunflow/core/accel/BoundingIntervalHierarchy.java b/src/main/java/org/sunflow/core/accel/BoundingIntervalHierarchy.java index 6335006..b05cd9e 100644 --- a/src/main/java/org/sunflow/core/accel/BoundingIntervalHierarchy.java +++ b/src/main/java/org/sunflow/core/accel/BoundingIntervalHierarchy.java @@ -256,7 +256,7 @@ else if (d[1] > d[2]) // ensure we are making progress in the subdivision if (right == rightOrig) { // all left - if (clipL <= split) { + if (clipL <= split && gridBox[2 * axis + 1] != split) { // keep looping on left half gridBox[2 * axis + 1] = split; prevClip = clipL; @@ -274,7 +274,7 @@ else if (d[1] > d[2]) } else if (left > right) { // all right right = rightOrig; - if (clipR >= split) { + if (clipR >= split && gridBox[2 * axis + 0] != split) { // keep looping on right half gridBox[2 * axis + 0] = split; prevClip = clipR; From adf45180081d5e1a723b6c7aa605897527b7e35b Mon Sep 17 00:00:00 2001 From: Karel Petranek Date: Tue, 6 Sep 2016 10:23:04 -0500 Subject: [PATCH 04/37] Fix missing TriangleMeshLight shader in PluginRegistry --- src/main/java/org/sunflow/PluginRegistry.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/sunflow/PluginRegistry.java b/src/main/java/org/sunflow/PluginRegistry.java index def4853..1337ff5 100644 --- a/src/main/java/org/sunflow/PluginRegistry.java +++ b/src/main/java/org/sunflow/PluginRegistry.java @@ -204,6 +204,9 @@ public final class PluginRegistry { shaderPlugins.registerPlugin("view_caustics", ViewCausticsShader.class); shaderPlugins.registerPlugin("view_global", ViewGlobalPhotonsShader.class); shaderPlugins.registerPlugin("view_irradiance", ViewIrradianceShader.class); + + // light shaders + shaderPlugins.registerPlugin("triangle_mesh_light", TriangleMeshLight.class); } static { From 7e8d24ededf2e4ea27d501398afa8738b97cb438 Mon Sep 17 00:00:00 2001 From: Karel Petranek Date: Sun, 11 Sep 2016 21:15:15 -0500 Subject: [PATCH 05/37] Improve efficiency of TriangleMeshLight using FastMath library (up to 10x speedup with big area lights) --- pom.xml | 7 ++++++- .../org/sunflow/core/light/TriangleMeshLight.java | 13 +++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index 0a50986..6d9b07e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ sunflow net.sourceforge - 0.07.3-SNAPSHOT + 0.07.4-SNAPSHOT http://sunflow.sourceforge.net Sunflow Global Illumination Rendering System @@ -21,6 +21,11 @@ janino 2.5.16 + + net.jafama + jafama + 2.1.0 + diff --git a/src/main/java/org/sunflow/core/light/TriangleMeshLight.java b/src/main/java/org/sunflow/core/light/TriangleMeshLight.java index 3af8071..c9c94d2 100644 --- a/src/main/java/org/sunflow/core/light/TriangleMeshLight.java +++ b/src/main/java/org/sunflow/core/light/TriangleMeshLight.java @@ -1,5 +1,6 @@ package org.sunflow.core.light; +import net.jafama.FastMath; import org.sunflow.SunflowAPI; import org.sunflow.core.Instance; import org.sunflow.core.LightSample; @@ -202,14 +203,14 @@ public void getSamples(ShadingState state) { float cosBeta = MathUtils.clamp(-Vector3.dot(n0, n1), -1.0f, 1.0f); float cosGamma = MathUtils.clamp(-Vector3.dot(n1, n2), -1.0f, 1.0f); - float alpha = (float) Math.acos(cosAlpha); - float beta = (float) Math.acos(cosBeta); - float gamma = (float) Math.acos(cosGamma); + float alpha = (float) FastMath.acos(cosAlpha); + float beta = (float) FastMath.acos(cosBeta); + float gamma = (float) FastMath.acos(cosGamma); float area = alpha + beta + gamma - (float) Math.PI; float cosC = MathUtils.clamp(Vector3.dot(p0, p1), -1.0f, 1.0f); - float salpha = (float) Math.sin(alpha); + float salpha = (float) FastMath.sinQuick(alpha); float product = salpha * cosC; // use lower sampling depth for diffuse bounces @@ -221,8 +222,8 @@ public void getSamples(ShadingState state) { double randY = state.getRandom(j, 1, samples); float phi = (float) randX * area - alpha + (float) Math.PI; - float sinPhi = (float) Math.sin(phi); - float cosPhi = (float) Math.cos(phi); + float sinPhi = (float) FastMath.sinQuick(phi); + float cosPhi = (float) FastMath.cosQuick(phi); float u = cosPhi + cosAlpha; float v = sinPhi - product; From 02d9b9f61e0a7c34bfa374e4729550334ab43be2 Mon Sep 17 00:00:00 2001 From: Karel Petranek Date: Sun, 11 Sep 2016 21:16:35 -0500 Subject: [PATCH 06/37] Implement back-face culling for triangle meshes --- .../java/org/sunflow/core/primitive/TriangleMesh.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/org/sunflow/core/primitive/TriangleMesh.java b/src/main/java/org/sunflow/core/primitive/TriangleMesh.java index 399a043..447df32 100644 --- a/src/main/java/org/sunflow/core/primitive/TriangleMesh.java +++ b/src/main/java/org/sunflow/core/primitive/TriangleMesh.java @@ -30,6 +30,7 @@ public class TriangleMesh implements PrimitiveList { private FloatParameter normals; private FloatParameter uvs; private byte[] faceShaders; + private boolean backfaceCull = false; public static void setSmallTriangles(boolean smallTriangles) { if (smallTriangles) @@ -62,6 +63,7 @@ public void writeObj(String filename) { } public boolean update(ParameterList pl, SunflowAPI api) { + this.backfaceCull = pl.getBoolean("backfaceCull", false); boolean updatedTopology = false; { int[] triangles = pl.getIntArray("triangles"); @@ -192,6 +194,14 @@ public void intersectPrimitive(Ray r, int primID, IntersectionState state) { // alternative test -- disabled for now // intersectPrimitiveRobust(r, primID, state); + + if (backfaceCull && normals != null) { + int tri = primID * 3; + Vector3 normal = new Vector3(normals.data[tri], normals.data[tri + 1], normals.data[tri + 2]); + if (Vector3.dot(normal, r.getDirection()) >= 1e-5) + return; + } + if (triaccel != null) { // optional fast intersection method triaccel[primID].intersect(r, primID, state); From 89c4756db3086ff53bf39519fb87583ce9b25658 Mon Sep 17 00:00:00 2001 From: Karel Petranek Date: Sun, 11 Sep 2016 21:18:06 -0500 Subject: [PATCH 07/37] Use kd-tree for meshes with 2 and more triangles as it helps even for such small meshes (about 20 % improvement on scenes with large flat rectangular surfaces) --- .../java/org/sunflow/core/AccelerationStructureFactory.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/sunflow/core/AccelerationStructureFactory.java b/src/main/java/org/sunflow/core/AccelerationStructureFactory.java index 90b6be9..c891a3c 100644 --- a/src/main/java/org/sunflow/core/AccelerationStructureFactory.java +++ b/src/main/java/org/sunflow/core/AccelerationStructureFactory.java @@ -12,12 +12,12 @@ static final AccelerationStructure create(String name, int n, boolean primitives name = "uniformgrid"; else if (n > 2000000) name = "bih"; - else if (n > 2) + else if (n > 1) name = "kdtree"; else name = "null"; } else { - if (n > 2) + if (n > 1) name = "bih"; else name = "null"; From a06c0bc896bf58c1efb75678933d29d4b296e499 Mon Sep 17 00:00:00 2001 From: Karel Petranek Date: Sat, 17 Sep 2016 11:33:27 -0500 Subject: [PATCH 08/37] Allow changing shading cache parameters for a quality/speed trade-off --- .../java/org/sunflow/core/ShadingCache.java | 27 +++++++++++-------- .../core/renderer/MultipassRenderer.java | 5 +++- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/sunflow/core/ShadingCache.java b/src/main/java/org/sunflow/core/ShadingCache.java index c7e9faa..d1dd148 100644 --- a/src/main/java/org/sunflow/core/ShadingCache.java +++ b/src/main/java/org/sunflow/core/ShadingCache.java @@ -2,8 +2,10 @@ import org.sunflow.image.Color; +import java.util.Arrays; + public class ShadingCache { - private Sample first; + private final Sample[] samples = new Sample[256]; private int depth; // stats long hits; @@ -11,26 +13,29 @@ public class ShadingCache { long sumDepth; long numCaches; + private final float dirTolerance, normalTolerance; + private static class Sample { Instance i; Shader s; float nx, ny, nz; float dx, dy, dz; Color c; - Sample next; // linked list } - public ShadingCache() { + public ShadingCache(float dirTolerance, float normalTolerance) { reset(); hits = 0; misses = 0; + this.dirTolerance = dirTolerance; + this.normalTolerance = normalTolerance; } public void reset() { sumDepth += depth; if (depth > 0) numCaches++; - first = null; + Arrays.fill(samples, null); depth = 0; } @@ -38,14 +43,15 @@ public Color lookup(ShadingState state, Shader shader) { if (state.getNormal() == null) return null; // search further - for (Sample s = first; s != null; s = s.next) { + for (int i = 0; i < depth; i++) { + Sample s = samples[i]; if (s.i != state.getInstance()) continue; if (s.s != shader) continue; - if (state.getRay().dot(s.dx, s.dy, s.dz) < 0.999f) + if (state.getRay().dot(s.dx, s.dy, s.dz) < 1 - dirTolerance) continue; - if (state.getNormal().dot(s.nx, s.ny, s.nz) < 0.99f) + if (state.getNormal().dot(s.nx, s.ny, s.nz) < 1 - normalTolerance) continue; // we have a match hits++; @@ -56,9 +62,8 @@ public Color lookup(ShadingState state, Shader shader) { } public void add(ShadingState state, Shader shader, Color c) { - if (state.getNormal() == null) + if (state.getNormal() == null || depth >= samples.length) return; - depth++; Sample s = new Sample(); s.i = state.getInstance(); s.s = shader; @@ -69,7 +74,7 @@ public void add(ShadingState state, Shader shader, Color c) { s.nx = state.getNormal().x; s.ny = state.getNormal().y; s.nz = state.getNormal().z; - s.next = first; - first = s; + samples[depth] = s; + depth++; } } \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/renderer/MultipassRenderer.java b/src/main/java/org/sunflow/core/renderer/MultipassRenderer.java index 6331042..89fa52f 100644 --- a/src/main/java/org/sunflow/core/renderer/MultipassRenderer.java +++ b/src/main/java/org/sunflow/core/renderer/MultipassRenderer.java @@ -33,6 +33,7 @@ public class MultipassRenderer implements ImageSampler { private int numSamples; private float invNumSamples; private boolean shadingCache; + private float cacheDirTolerance = 1e-5f, cacheNormalTolerance = 1e-4f; public MultipassRenderer() { bucketSize = 32; @@ -51,6 +52,8 @@ public boolean prepare(Options options, Scene scene, int w, int h) { bucketOrderName = options.getString("bucket.order", bucketOrderName); numSamples = options.getInt("aa.samples", numSamples); shadingCache = options.getBoolean("aa.cache", shadingCache); + cacheDirTolerance = options.getFloat("aa.cacheDirTolerance", cacheDirTolerance); + cacheNormalTolerance = options.getFloat("aa.cacheNormalTolerance", cacheNormalTolerance); // limit bucket size and compute number of buckets in each direction bucketSize = MathUtils.clamp(bucketSize, 16, 512); @@ -109,7 +112,7 @@ private class BucketThread extends Thread { BucketThread(int threadID) { this.threadID = threadID; istate = new IntersectionState(); - cache = shadingCache ? new ShadingCache() : null; + cache = shadingCache ? new ShadingCache(cacheDirTolerance, cacheNormalTolerance) : null; } @Override From 8460c0d7355b5c54e64efdd1a3452f98865bd70f Mon Sep 17 00:00:00 2001 From: Karel Petranek Date: Mon, 3 Oct 2016 18:51:50 -0500 Subject: [PATCH 09/37] Fix canceling for MultipassRenderer, make dimming previously rendered image optional --- pom.xml | 2 +- .../core/renderer/MultipassRenderer.java | 6 ++++++ .../java/org/sunflow/system/ImagePanel.java | 18 +++++++++++++----- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 6d9b07e..50f48fb 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ sunflow net.sourceforge - 0.07.4-SNAPSHOT + 0.07.5-SNAPSHOT http://sunflow.sourceforge.net Sunflow Global Illumination Rendering System diff --git a/src/main/java/org/sunflow/core/renderer/MultipassRenderer.java b/src/main/java/org/sunflow/core/renderer/MultipassRenderer.java index 89fa52f..05455ca 100644 --- a/src/main/java/org/sunflow/core/renderer/MultipassRenderer.java +++ b/src/main/java/org/sunflow/core/renderer/MultipassRenderer.java @@ -120,6 +120,9 @@ public void run() { while (true) { int bx, by; synchronized (MultipassRenderer.this) { + if (UI.taskCanceled()) + return; + if (bucketCounter >= bucketCoords.length) return; UI.taskUpdate(bucketCounter); @@ -178,6 +181,9 @@ private void renderBucket(Display display, int bx, int by, int threadID, Interse bucketAlpha[i] = a * invNumSamples; if (cache != null) cache.reset(); + + if (UI.taskCanceled()) + return; } } // update pixels diff --git a/src/main/java/org/sunflow/system/ImagePanel.java b/src/main/java/org/sunflow/system/ImagePanel.java index 77c7e18..d760380 100644 --- a/src/main/java/org/sunflow/system/ImagePanel.java +++ b/src/main/java/org/sunflow/system/ImagePanel.java @@ -25,6 +25,7 @@ public class ImagePanel extends JPanel implements Display { private float xo, yo; private float w, h; private long repaintCounter; + private final boolean dullPreviousImage; private class ScrollZoomListener extends MouseInputAdapter { int mx; @@ -85,7 +86,12 @@ public void mouseWheelMoved(MouseWheelEvent e) { } public ImagePanel() { + this(false); + } + + public ImagePanel(boolean dullPreviousImage) { setPreferredSize(new Dimension(640, 480)); + this.dullPreviousImage = dullPreviousImage; image = null; xo = yo = 0; w = h = 0; @@ -177,11 +183,13 @@ public synchronized void fit() { public synchronized void imageBegin(int w, int h, int bucketSize) { if (image != null && w == image.getWidth() && h == image.getHeight()) { - // dull image if it has same resolution (75%) - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - int rgba = image.getRGB(x, y); - image.setRGB(x, y, ((rgba & 0xFEFEFEFE) >>> 1) + ((rgba & 0xFCFCFCFC) >>> 2)); + if (dullPreviousImage) { + // dull image if it has same resolution (75%) + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int rgba = image.getRGB(x, y); + image.setRGB(x, y, ((rgba & 0xFEFEFEFE) >>> 1) + ((rgba & 0xFCFCFCFC) >>> 2)); + } } } } else { From f694466a8fd4478a07d0aa3482f5f5c25ca279a9 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Tue, 15 Aug 2017 19:55:37 -0300 Subject: [PATCH 10/37] Update janino version to 2.7.8 --- pom.xml | 3 +-- src/main/java/org/sunflow/SunflowAPI.java | 18 +----------------- src/main/java/org/sunflow/system/Plugins.java | 10 +--------- 3 files changed, 3 insertions(+), 28 deletions(-) diff --git a/pom.xml b/pom.xml index 50f48fb..526a825 100644 --- a/pom.xml +++ b/pom.xml @@ -14,12 +14,11 @@ UTF-8 - org.codehaus.janino janino - 2.5.16 + 2.7.8 net.jafama diff --git a/src/main/java/org/sunflow/SunflowAPI.java b/src/main/java/org/sunflow/SunflowAPI.java index 49ea83a..ec008fe 100644 --- a/src/main/java/org/sunflow/SunflowAPI.java +++ b/src/main/java/org/sunflow/SunflowAPI.java @@ -6,11 +6,9 @@ import java.io.StringReader; import java.util.Locale; +import org.codehaus.commons.compiler.CompileException; import org.codehaus.janino.ClassBodyEvaluator; -import org.codehaus.janino.CompileException; import org.codehaus.janino.Scanner; -import org.codehaus.janino.Parser.ParseException; -import org.codehaus.janino.Scanner.ScanException; import org.sunflow.core.Camera; import org.sunflow.core.CameraLens; import org.sunflow.core.Display; @@ -580,14 +578,6 @@ public static SunflowAPI create(String filename, int frameNumber) { UI.printError(Module.API, "Could not compile: \"%s\"", filename); UI.printError(Module.API, "%s", e.getMessage()); return null; - } catch (ParseException e) { - UI.printError(Module.API, "Could not compile: \"%s\"", filename); - UI.printError(Module.API, "%s", e.getMessage()); - return null; - } catch (ScanException e) { - UI.printError(Module.API, "Could not compile: \"%s\"", filename); - UI.printError(Module.API, "%s", e.getMessage()); - return null; } catch (IOException e) { UI.printError(Module.API, "Could not compile: \"%s\"", filename); UI.printError(Module.API, "%s", e.getMessage()); @@ -671,12 +661,6 @@ public static SunflowAPI compile(String code) { } catch (CompileException e) { UI.printError(Module.API, "%s", e.getMessage()); return null; - } catch (ParseException e) { - UI.printError(Module.API, "%s", e.getMessage()); - return null; - } catch (ScanException e) { - UI.printError(Module.API, "%s", e.getMessage()); - return null; } catch (IOException e) { UI.printError(Module.API, "%s", e.getMessage()); return null; diff --git a/src/main/java/org/sunflow/system/Plugins.java b/src/main/java/org/sunflow/system/Plugins.java index 991e9ee..a346cfc 100644 --- a/src/main/java/org/sunflow/system/Plugins.java +++ b/src/main/java/org/sunflow/system/Plugins.java @@ -1,9 +1,7 @@ package org.sunflow.system; +import org.codehaus.commons.compiler.CompileException; import org.codehaus.janino.ClassBodyEvaluator; -import org.codehaus.janino.CompileException; -import org.codehaus.janino.Parser.ParseException; -import org.codehaus.janino.Scanner.ScanException; import org.sunflow.system.UI.Module; import org.sunflow.util.FastHashMap; @@ -110,12 +108,6 @@ public boolean registerPlugin(String name, String sourceCode) { } catch (CompileException e) { UI.printError(Module.API, "Plugin \"%s\" could not be declared - %s", name, e.getLocalizedMessage()); return false; - } catch (ParseException e) { - UI.printError(Module.API, "Plugin \"%s\" could not be declared - %s", name, e.getLocalizedMessage()); - return false; - } catch (ScanException e) { - UI.printError(Module.API, "Plugin \"%s\" could not be declared - %s", name, e.getLocalizedMessage()); - return false; } } From 2211efd5b68841df5841bac1692fe2a855975a34 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Tue, 15 Aug 2017 20:09:00 -0300 Subject: [PATCH 11/37] Update README --- README | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README b/README index e9b2002..d122461 100644 --- a/README +++ b/README @@ -1,5 +1,5 @@ Sunflow Global Illumination Rendering System -v0.07.3 +v0.07.5 Contact: Christopher Kulla fpsunflower@users.sourceforge.net @@ -95,7 +95,7 @@ Box and triangle are best for previews as they are small and fast. The other fil Bucket rendering: -Sunflow proceses the image to be rendered in small squares called buckets. The size of these buckets can be controlled by a pixel width. Each rendering thread will be a assigned a single bucket. You may not get the bucket size you expect if you try to make them really small or really big, as there are some hard-coded limits to prevent excessive memory usage or excessive overhead. +Sunflow proceses the image to be rendered in small squares called buckets. The size of these buckets can be controlled by a pixel width. Each rendering thread will be assigned to a single bucket. You may not get the bucket size you expect if you try to make them really small or really big, as there are some hard-coded limits to prevent excessive memory usage or excessive overhead. The bucket ordering simply affects the order in which the buckets appear. They shouldn't have too much of an effect on overall rendering speed. From 57593aac90bd838c77f4271e61e616bf975a2143 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Wed, 18 Jul 2018 21:12:06 -0300 Subject: [PATCH 12/37] Extract parameters --- examples/bump_demo_small.sc | 176 + pom.xml | 21 +- src/main/java/SunflowGUI.java | 40 +- .../java/examples/CornellBoxJensenScene.java | 127 + src/main/java/org/sunflow/Benchmark.java | 23 +- src/main/java/org/sunflow/PluginRegistry.java | 29 +- .../java/org/sunflow/RealtimeBenchmark.java | 21 +- .../java/org/sunflow/RenderObjectMap.java | 12 +- src/main/java/org/sunflow/SunflowAPI.java | 57 +- src/main/java/org/sunflow/core/Camera.java | 21 +- .../java/org/sunflow/core/ParameterList.java | 4 + .../org/sunflow/core/camera/PinholeLens.java | 32 + .../org/sunflow/core/camera/ThinLens.java | 72 + .../core/gi/AmbientOcclusionGIEngine.java | 9 +- .../org/sunflow/core/gi/FakeGIEngine.java | 7 +- .../java/org/sunflow/core/gi/InstantGI.java | 12 +- .../core/gi/IrradianceCacheGIEngine.java | 11 +- .../sunflow/core/gi/PathTracingGIEngine.java | 3 +- .../core/light/DirectionalSpotlight.java | 89 +- .../sunflow/core/light/ImageBasedLight.java | 12 +- .../org/sunflow/core/light/PointLight.java | 34 +- .../org/sunflow/core/light/SphereLight.java | 54 +- .../org/sunflow/core/light/SunSkyLight.java | 16 +- .../sunflow/core/light/TriangleMeshLight.java | 5 +- .../core/modifiers/BumpMappingModifier.java | 11 +- .../core/modifiers/PerlinModifier.java | 24 + .../core/parameter/BackgroundParameter.java | 33 + .../core/parameter/BucketParameter.java | 47 + .../core/parameter/IlluminationParameter.java | 41 + .../core/parameter/ImageParameter.java | 152 + .../core/parameter/InstanceParameter.java | 69 + .../core/parameter/OverrideParameter.java | 40 + .../org/sunflow/core/parameter/Parameter.java | 7 + .../core/parameter/PhotonParameter.java | 47 + .../core/parameter/TraceDepthsParameter.java | 51 + .../core/parameter/TransformParameter.java | 45 + .../parameter/camera/CameraParameter.java | 57 + .../camera/FishEyeCameraParameter.java | 17 + .../camera/PinholeCameraParameter.java | 59 + .../camera/SphericalCameraParameter.java | 17 + .../camera/ThinLensCameraParameter.java | 107 + .../parameter/geometry/BanchOffParameter.java | 12 + .../geometry/BezierMeshParameter.java | 82 + .../parameter/geometry/CylinderParameter.java | 12 + .../parameter/geometry/FileMeshParameter.java | 33 + .../geometry/GenericMeshParameter.java | 97 + .../parameter/geometry/GumboParameter.java | 39 + .../parameter/geometry/HairParameter.java | 43 + .../parameter/geometry/JuliaParameter.java | 74 + .../parameter/geometry/ObjectParameter.java | 61 + .../geometry/ParticlesParameter.java | 43 + .../parameter/geometry/PlaneParameter.java | 58 + .../geometry/SphereFlakeParameter.java | 59 + .../parameter/geometry/SphereParameter.java | 44 + .../parameter/geometry/TeapotParameter.java | 33 + .../parameter/geometry/TorusParameter.java | 33 + .../geometry/TriangleMeshParameter.java | 56 + .../gi/AmbientOcclusionGIParameter.java | 65 + .../parameter/gi/DisabledGIParameter.java | 16 + .../core/parameter/gi/FakeGIParameter.java | 48 + .../gi/GlobalIlluminationParameter.java | 27 + .../core/parameter/gi/InstantGIParameter.java | 61 + .../parameter/gi/IrrCacheGIParameter.java | 83 + .../parameter/gi/PathTracingGIParameter.java | 25 + .../light/CornellBoxLightParameter.java | 109 + .../light/DirectionalLightParameter.java | 67 + .../light/ImageBasedLightParameter.java | 82 + .../core/parameter/light/LightParameter.java | 27 + .../parameter/light/PointLightParameter.java | 41 + .../parameter/light/SphereLightParameter.java | 67 + .../parameter/light/SunSkyLightParameter.java | 96 + .../light/TriangleMeshLightParameter.java | 56 + .../modifier/BumpMapModifierParameter.java | 32 + .../parameter/modifier/ModifierParameter.java | 20 + .../modifier/NormalMapModifierParameter.java | 22 + .../modifier/PerlinModifierParameter.java | 42 + .../AmbientOcclusionShaderParameter.java | 80 + .../shader/ConstantShaderParameter.java | 28 + .../shader/DiffuseShaderParameter.java | 42 + .../shader/GlassShaderParameter.java | 62 + .../parameter/shader/IDShaderParameter.java | 15 + .../shader/MirrorShaderParameter.java | 28 + .../shader/PhongShaderParameter.java | 77 + .../parameter/shader/ShaderParameter.java | 46 + .../shader/ShinyShaderParameter.java | 54 + .../parameter/shader/UberShaderParameter.java | 111 + .../shader/ViewCausticsShaderParameter.java | 15 + .../shader/ViewGlobalShaderParameter.java | 15 + .../shader/ViewIrradianceShaderParameter.java | 15 + .../parameter/shader/WardShaderParameter.java | 84 + .../org/sunflow/core/parser/SCNewParser.java | 1594 +++++++++ .../org/sunflow/core/parser/SCParser.java | 2874 +++++++++-------- .../sunflow/core/primitive/CornellBox.java | 60 +- .../sunflow/core/primitive/TriangleMesh.java | 19 +- .../sunflow/core/renderer/BucketRenderer.java | 20 +- .../sunflow/core/tesselatable/FileMesh.java | 6 +- src/main/java/org/sunflow/image/Color.java | 3 + src/main/java/org/sunflow/math/Point3.java | 20 + src/main/java/org/sunflow/math/Vector3.java | 17 + .../java/org/sunflow/system/SystemUtil.java | 24 + src/test/java/org/sunflow/SunflowAPITest.java | 33 + .../parameter/BackgroundParameterTest.java | 41 + .../core/parameter/BucketParameterTest.java | 35 + .../core/parameter/ImageParameterTest.java | 55 + .../core/parameter/InstanceParameterTest.java | 31 + .../core/parameter/OverrideParameterTest.java | 35 + .../core/parameter/PhotonParameterTest.java | 39 + .../parameter/TraceDepthsParameterTest.java | 33 + .../camera/FishEyeCameraParameterTest.java | 33 + .../camera/PinholeCameraParameterTest.java | 33 + .../camera/SphericalCameraParameterTest.java | 33 + .../camera/ThinLensCameraParameterTest.java | 33 + .../geometry/BanchoffParameterTest.java | 32 + .../geometry/BezierMeshParameterTest.java | 34 + .../geometry/CylinderParameterTest.java | 32 + .../geometry/FileMeshParameterTest.java | 32 + .../geometry/GenericMeshParameterTest.java | 34 + .../geometry/GumboParameterTest.java | 32 + .../parameter/geometry/HairParameterTest.java | 32 + .../geometry/JuliaParameterTest.java | 32 + .../geometry/ParticlesParameterTest.java | 32 + .../geometry/PlaneParameterTest.java | 32 + .../geometry/SphereFlakeParameterTest.java | 32 + .../geometry/SphereParameterTest.java | 35 + .../geometry/TeapotParameterTest.java | 32 + .../geometry/TorusParameterTest.java | 32 + .../geometry/TriangleMeshParameterTest.java | 34 + .../gi/AmbientOcclusionGIParameterTest.java | 38 + .../parameter/gi/FakeGIParameterTest.java | 37 + .../parameter/gi/InstantGIParameterTest.java | 37 + .../parameter/gi/IrrCacheGIParameterTest.java | 55 + .../gi/PathTracingGIParameterTest.java | 31 + .../light/CornellBoxLightParameterTest.java | 63 + .../light/DirectionalLightParameterTest.java | 44 + .../light/ImageBaseLightParameterTest.java | 35 + .../light/PointLightParameterTest.java | 37 + .../light/SphereLightParameterTest.java | 40 + .../light/SunSkyLightParameterTest.java | 40 + .../light/TriangleMeshLightParameterTest.java | 35 + .../modifer/BumpMapModifierParameterTest.java | 37 + .../NormalMapModifierParameterTest.java | 36 + .../modifer/PerlinParameterTest.java | 42 + 142 files changed, 8840 insertions(+), 1475 deletions(-) create mode 100644 examples/bump_demo_small.sc create mode 100644 src/main/java/examples/CornellBoxJensenScene.java create mode 100644 src/main/java/org/sunflow/core/parameter/BackgroundParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/BucketParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/IlluminationParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/ImageParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/InstanceParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/OverrideParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/Parameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/PhotonParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/TraceDepthsParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/TransformParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/camera/CameraParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/camera/FishEyeCameraParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/camera/PinholeCameraParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/camera/SphericalCameraParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/camera/ThinLensCameraParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/BanchOffParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/BezierMeshParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/CylinderParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/FileMeshParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/GenericMeshParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/GumboParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/HairParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/JuliaParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/ParticlesParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/PlaneParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/SphereFlakeParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/SphereParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/TorusParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/TriangleMeshParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/gi/AmbientOcclusionGIParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/gi/DisabledGIParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/gi/FakeGIParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/gi/GlobalIlluminationParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/gi/InstantGIParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/gi/IrrCacheGIParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/gi/PathTracingGIParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/light/CornellBoxLightParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/light/DirectionalLightParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/light/ImageBasedLightParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/light/LightParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/light/PointLightParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/light/SphereLightParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/light/SunSkyLightParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/light/TriangleMeshLightParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/modifier/BumpMapModifierParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/modifier/ModifierParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/modifier/NormalMapModifierParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/modifier/PerlinModifierParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/shader/AmbientOcclusionShaderParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/shader/ConstantShaderParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/shader/DiffuseShaderParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/shader/GlassShaderParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/shader/IDShaderParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/shader/MirrorShaderParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/shader/PhongShaderParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/shader/ShaderParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/shader/ShinyShaderParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/shader/UberShaderParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/shader/ViewCausticsShaderParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/shader/ViewGlobalShaderParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/shader/ViewIrradianceShaderParameter.java create mode 100644 src/main/java/org/sunflow/core/parameter/shader/WardShaderParameter.java create mode 100644 src/main/java/org/sunflow/core/parser/SCNewParser.java create mode 100644 src/main/java/org/sunflow/system/SystemUtil.java create mode 100644 src/test/java/org/sunflow/SunflowAPITest.java create mode 100644 src/test/java/org/sunflow/core/parameter/BackgroundParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/BucketParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/ImageParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/InstanceParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/OverrideParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/PhotonParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/TraceDepthsParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/camera/FishEyeCameraParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/camera/PinholeCameraParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/camera/SphericalCameraParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/camera/ThinLensCameraParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/BanchoffParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/BezierMeshParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/CylinderParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/FileMeshParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/GenericMeshParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/GumboParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/HairParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/JuliaParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/ParticlesParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/PlaneParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/SphereFlakeParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/SphereParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/TeapotParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/TorusParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/TriangleMeshParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/gi/AmbientOcclusionGIParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/gi/FakeGIParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/gi/InstantGIParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/gi/IrrCacheGIParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/gi/PathTracingGIParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/light/CornellBoxLightParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/light/DirectionalLightParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/light/ImageBaseLightParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/light/PointLightParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/light/SphereLightParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/light/SunSkyLightParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/light/TriangleMeshLightParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/modifer/BumpMapModifierParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/modifer/NormalMapModifierParameterTest.java create mode 100644 src/test/java/org/sunflow/core/parameter/modifer/PerlinParameterTest.java diff --git a/examples/bump_demo_small.sc b/examples/bump_demo_small.sc new file mode 100644 index 0000000..4410663 --- /dev/null +++ b/examples/bump_demo_small.sc @@ -0,0 +1,176 @@ +image { + resolution 400 225 + aa 0 1 + filter triangle +} + +% |persp|perspShape +camera { + type pinhole + eye -18.19 8.97 -0.93 + target -0.690 0.97 -0.93 + up 0 1 0 + fov 30 + aspect 1.777777777777 +} + +light { + type sunsky + up 0 1 0 + east 0 0 1 + sundir -1 1 -1 + turbidity 2 + samples 32 +} + +modifier { + name bumpy_01 + type normalmap + texture textures/brick_normal.jpg +} + +modifier { + name bumpy_02 + type bump + texture textures/dirty_bump.jpg + scale 0.02 +} + +modifier { + name bumpy_03 + type bump + texture textures/reptileskin_bump.png + scale 0.02 +} + +modifier { + name bumpy_04 + type bump + texture textures/shiphull_bump.png + scale 0.015 +} + +modifier { + name bumpy_05 + type bump + texture textures/slime_bump.jpg + scale 0.015 +} + + +shader { + name default + type shiny + diff 0.2 0.2 0.2 + refl 0.3 +} + +shader { + name glassy + type glass + eta 1.2 + color 0.8 0.8 0.8 + absorbtion.distance 7 + absorbtion.color { "sRGB nonlinear" 0.2 0.7 0.2 } +} + +shader { + name simple_red + type diffuse + diff { "sRGB nonlinear" 0.70 0.15 0.15 } +} + +shader { + name simple_green + type diffuse + diff { "sRGB nonlinear" 0.15 0.70 0.15 } +} + +shader { + name simple_yellow + type diffuse + diff { "sRGB nonlinear" 0.70 0.70 0.15 } +} + + +shader { + name floor + type diffuse +% diff 0.3 0.3 0.3 + texture textures/brick_color.jpg +} + +object { + shader floor + modifier bumpy_01 + type plane + p 0 0 0 + p 4 0 3 + p -3 0 4 +} + +object { + shader simple_green + modifier bumpy_03 + transform { + rotatex -90 + scaleu 0.018 + rotatey 245 + translate 1.5 0 -1 + } + type teapot + name teapot_0 + subdivs 20 +} + +object { + shader glassy + modifier bumpy_05 + transform { + rotatex 35 + scaleu 1.5 + rotatey 245 + translate 1.5 1.5 3 + } + type sphere + name sphere_0 +} + +object { + shader default + modifier bumpy_05 + transform { + rotatex 35 + scaleu 1.5 + rotatey 245 + translate 1.5 1.5 -5 + } + type sphere + name sphere_1 +} + +instance { + name teapot_1 + geometry teapot_0 + transform { + rotatex -90 + scaleu 0.018 + rotatey 245 + translate -1.5 0 -3 + } + shader simple_yellow + modifier bumpy_04 +} + +instance { + name teapot_3 + geometry teapot_0 + transform { + rotatex -90 + scaleu 0.018 + rotatey 245 + translate -1.5 0 +1 + } + shader simple_red + modifier bumpy_02 +} diff --git a/pom.xml b/pom.xml index 526a825..81d5ed1 100644 --- a/pom.xml +++ b/pom.xml @@ -25,8 +25,19 @@ jafama 2.1.0 + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + - @@ -51,6 +62,14 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + + diff --git a/src/main/java/SunflowGUI.java b/src/main/java/SunflowGUI.java index 831fd3f..b79e725 100644 --- a/src/main/java/SunflowGUI.java +++ b/src/main/java/SunflowGUI.java @@ -42,6 +42,9 @@ import org.sunflow.Benchmark; import org.sunflow.RealtimeBenchmark; import org.sunflow.SunflowAPI; +import org.sunflow.core.parameter.BucketParameter; +import org.sunflow.core.parameter.ImageParameter; +import org.sunflow.core.parameter.OverrideParameter; import org.sunflow.core.Display; import org.sunflow.core.TextureCache; import org.sunflow.core.accel.KDTree; @@ -49,10 +52,7 @@ import org.sunflow.core.display.FrameDisplay; import org.sunflow.core.display.ImgPipeDisplay; import org.sunflow.core.primitive.TriangleMesh; -import org.sunflow.system.ImagePanel; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.UserInterface; +import org.sunflow.system.*; import org.sunflow.system.UI.Module; import org.sunflow.system.UI.PrintLevel; @@ -361,18 +361,18 @@ else if (baketype.equals("ortho")) } } if (runBenchmark) { - SunflowAPI.runSystemCheck(); + SystemUtil.runSystemCheck(); new Benchmark().execute(); return; } if (runRTBenchmark) { - SunflowAPI.runSystemCheck(); + SystemUtil.runSystemCheck(); new RealtimeBenchmark(showFrame, threads); return; } if (input == null) usage(false); - SunflowAPI.runSystemCheck(); + SystemUtil.runSystemCheck(); if (translateFilename != null) { SunflowAPI.translate(input, translateFilename); return; @@ -392,20 +392,20 @@ else if (baketype.equals("ortho")) if (noRender) continue; if (resolutionW > 0 && resolutionH > 0) { - api.parameter("resolutionX", resolutionW); - api.parameter("resolutionY", resolutionH); + api.parameter(ImageParameter.PARAM_RESOLUTION_X, resolutionW); + api.parameter(ImageParameter.PARAM_RESOLUTION_Y, resolutionH); } if (aaMin != -5 || aaMax != -5) { - api.parameter("aa.min", aaMin); - api.parameter("aa.max", aaMax); + api.parameter(ImageParameter.PARAM_AA_MIN, aaMin); + api.parameter(ImageParameter.PARAM_AA_MAX, aaMax); } if (samples >= 0) - api.parameter("aa.samples", samples); + api.parameter(ImageParameter.PARAM_AA_SAMPLES, samples); if (bucketSize > 0) - api.parameter("bucket.size", bucketSize); + api.parameter(BucketParameter.PARAM_BUCKET_SIZE, bucketSize); if (bucketOrder != null) - api.parameter("bucket.order", bucketOrder); - api.parameter("aa.display", showAA); + api.parameter(BucketParameter.PARAM_BUCKET_ORDER, bucketOrder); + api.parameter(ImageParameter.PARAM_AA_DISPLAY, showAA); api.parameter("threads", threads); api.parameter("threads.lowPriority", lowPriority); if (bakingName != null) { @@ -413,7 +413,7 @@ else if (baketype.equals("ortho")) api.parameter("baking.viewdep", bakeViewdep); } if (filterType != null) - api.parameter("filter", filterType); + api.parameter(ImageParameter.PARAM_FILTER, filterType); if (noGI) api.parameter("gi.engine", "none"); else if (pathGI > 0) { @@ -423,14 +423,14 @@ else if (pathGI > 0) { if (noCaustics) api.parameter("caustics", "none"); if (sampler != null) - api.parameter("sampler", sampler); + api.parameter(ImageParameter.PARAM_SAMPLER, sampler); api.options(SunflowAPI.DEFAULT_OPTIONS); if (shaderOverride != null) { if (shaderOverride.equals("ambient_occlusion")) api.parameter("maxdist", maxDist); api.shader("cmdline_override", shaderOverride); - api.parameter("override.shader", "cmdline_override"); - api.parameter("override.photons", true); + api.parameter(OverrideParameter.PARAM_OVERRIDE_SHADER, "cmdline_override"); + api.parameter(OverrideParameter.PARAM_OVERRIDE_PHOTONS, true); api.options(SunflowAPI.DEFAULT_OPTIONS); } // create display @@ -454,7 +454,7 @@ else if (pathGI > 0) { if (screenRes.getWidth() <= DEFAULT_WIDTH || screenRes.getHeight() <= DEFAULT_HEIGHT) gui.setExtendedState(MAXIMIZED_BOTH); gui.tileWindowMenuItem.doClick(); - SunflowAPI.runSystemCheck(); + SystemUtil.runSystemCheck(); } } diff --git a/src/main/java/examples/CornellBoxJensenScene.java b/src/main/java/examples/CornellBoxJensenScene.java new file mode 100644 index 0000000..6c53058 --- /dev/null +++ b/src/main/java/examples/CornellBoxJensenScene.java @@ -0,0 +1,127 @@ +package examples; + +import org.sunflow.PluginRegistry; +import org.sunflow.SunflowAPI; +import org.sunflow.core.ImageSampler; +import org.sunflow.core.Options; +import org.sunflow.core.parameter.ImageParameter; +import org.sunflow.core.parameter.InstanceParameter; +import org.sunflow.core.parameter.PhotonParameter; +import org.sunflow.core.parameter.TraceDepthsParameter; +import org.sunflow.core.parameter.camera.PinholeCameraParameter; +import org.sunflow.core.parameter.geometry.SphereParameter; +import org.sunflow.core.parameter.gi.InstantGIParameter; +import org.sunflow.core.parameter.light.CornellBoxLightParameter; +import org.sunflow.core.parameter.shader.DiffuseShaderParameter; +import org.sunflow.core.parameter.shader.GlassShaderParameter; +import org.sunflow.core.parameter.shader.MirrorShaderParameter; +import org.sunflow.image.Color; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class CornellBoxJensenScene { + + public static void main(String[] args) { + SunflowAPI api = new SunflowAPI(); + api.reset(); + + ImageParameter image = new ImageParameter(); + image.setResolutionX(800); + image.setResolutionY(600); + image.setAAMin(0); + image.setAAMax(2); + image.setFilter(ImageParameter.FILTER_GAUSSIAN); + image.setup(api); + + TraceDepthsParameter traceDepths = new TraceDepthsParameter(); + traceDepths.setDiffuse(4); + traceDepths.setReflection(3); + traceDepths.setRefraction(2); + traceDepths.setup(api); + + PhotonParameter photons = new PhotonParameter(); + photons.setNumEmit(1000000); + photons.setCaustics("kd"); + photons.setCausticsGather(100); + photons.setCausticsRadius(0.5f); + photons.setup(api); + + InstantGIParameter gi = new InstantGIParameter(); + gi.setSamples(64); + gi.setSets(1); + gi.setBias(0.00003f); + gi.setBiasSamples(0); + gi.setup(api); + + PinholeCameraParameter camera = new PinholeCameraParameter(); + + camera.setName("camera"); + Point3 eye = new Point3(0,-205,50); + Point3 target = new Point3(0,0,50); + Vector3 up = new Vector3(0,0,1); + // TODO Move logic to camera + api.parameter("transform", Matrix4.lookAt(eye, target, up)); + + camera.setFov(45f); + camera.setAspect(1.333333f); + camera.setup(api); + + // Materials + MirrorShaderParameter mirror = new MirrorShaderParameter("Mirror"); + mirror.setReflection(new Color(0.7f,0.7f,0.7f)); + mirror.setup(api); + + GlassShaderParameter glass = new GlassShaderParameter("Glass"); + glass.setEta(1.6f); + glass.setAbsorptionColor(new Color(1,1,1)); + glass.setup(api); + + // Lights + CornellBoxLightParameter lightParameter = new CornellBoxLightParameter(); + lightParameter.setName("cornell-box-light"); + lightParameter.setMin(new Point3(-60,-60,0)); + lightParameter.setMax(new Point3(60,60,100)); + lightParameter.setLeft(new Color(0.8f,0.25f,0.25f)); + lightParameter.setRight(new Color(0.25f,0.25f,0.8f)); + lightParameter.setTop(new Color(0.7f,0.7f,0.7f)); + lightParameter.setBottom(new Color(0.7f,0.7f,0.7f)); + lightParameter.setBack(new Color(0.7f,0.7f,0.7f)); + lightParameter.setRadiance(new Color(15,15,15)); + lightParameter.setSamples(32); + lightParameter.setup(api); + + InstanceParameter mirrorSphereInstance = new InstanceParameter(); + mirrorSphereInstance.setShaders(new String[]{"Mirror"}); + SphereParameter mirrorSphere = new SphereParameter(); + mirrorSphere.setName("mirror-sphere"); + mirrorSphere.setCenter(new Point3(-30,30,20)); + mirrorSphere.setInstanceParameter(mirrorSphereInstance); + mirrorSphere.setRadius(20); + mirrorSphere.setup(api); + + InstanceParameter glassSphereInstance = new InstanceParameter(); + glassSphereInstance.setShaders(new String[]{"Glass"}); + SphereParameter glassSphere = new SphereParameter(); + glassSphere.setName("glass-sphere"); + glassSphere.setCenter(new Point3(28,2,20)); + glassSphere.setInstanceParameter(glassSphereInstance); + glassSphere.setRadius(20); + glassSphere.setup(api); + + finalRender(api); + } + + private static void previewRender(SunflowAPI api) { + api.parameter("sampler", "ipr"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + + private static void finalRender(SunflowAPI api) { + api.parameter("sampler", "bucket"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + +} diff --git a/src/main/java/org/sunflow/Benchmark.java b/src/main/java/org/sunflow/Benchmark.java index 956c5b5..070e0d5 100644 --- a/src/main/java/org/sunflow/Benchmark.java +++ b/src/main/java/org/sunflow/Benchmark.java @@ -6,6 +6,9 @@ import javax.imageio.ImageIO; +import org.sunflow.core.parameter.BucketParameter; +import org.sunflow.core.parameter.ImageParameter; +import org.sunflow.core.parameter.TraceDepthsParameter; import org.sunflow.core.Display; import org.sunflow.core.display.FileDisplay; import org.sunflow.core.display.FrameDisplay; @@ -116,16 +119,16 @@ public void build() { parameter("threads", threads); // spawn regular priority threads parameter("threads.lowPriority", false); - parameter("resolutionX", resolution); - parameter("resolutionY", resolution); - parameter("aa.min", -1); - parameter("aa.max", 1); - parameter("filter", "triangle"); - parameter("depths.diffuse", 2); - parameter("depths.reflection", 2); - parameter("depths.refraction", 2); - parameter("bucket.order", "hilbert"); - parameter("bucket.size", 32); + parameter(ImageParameter.PARAM_RESOLUTION_X, resolution); + parameter(ImageParameter.PARAM_RESOLUTION_Y, resolution); + parameter(ImageParameter.PARAM_AA_MIN, -1); + parameter(ImageParameter.PARAM_AA_MAX, 1); + parameter(ImageParameter.PARAM_FILTER, ImageParameter.FILTER_TRIANGLE); + parameter(TraceDepthsParameter.PARAM_DEPTHS_DIFFUSE, 2); + parameter(TraceDepthsParameter.PARAM_DEPTHS_REFLECTION, 2); + parameter(TraceDepthsParameter.PARAM_DEPTHS_REFRACTION, 2); + parameter(BucketParameter.PARAM_BUCKET_ORDER, BucketParameter.ORDER_HILBERT); + parameter(BucketParameter.PARAM_BUCKET_SIZE, 32); // gi options parameter("gi.engine", "igi"); parameter("gi.igi.samples", 90); diff --git a/src/main/java/org/sunflow/PluginRegistry.java b/src/main/java/org/sunflow/PluginRegistry.java index 1337ff5..88bad8b 100644 --- a/src/main/java/org/sunflow/PluginRegistry.java +++ b/src/main/java/org/sunflow/PluginRegistry.java @@ -51,6 +51,9 @@ import org.sunflow.core.modifiers.BumpMappingModifier; import org.sunflow.core.modifiers.NormalMapModifier; import org.sunflow.core.modifiers.PerlinModifier; +import org.sunflow.core.parameter.BucketParameter; +import org.sunflow.core.parameter.camera.CameraParameter; +import org.sunflow.core.parameter.modifier.ModifierParameter; import org.sunflow.core.parser.RA2Parser; import org.sunflow.core.parser.RA3Parser; import org.sunflow.core.parser.SCAsciiParser; @@ -211,9 +214,9 @@ public final class PluginRegistry { static { // modifiers - modifierPlugins.registerPlugin("bump_map", BumpMappingModifier.class); - modifierPlugins.registerPlugin("normal_map", NormalMapModifier.class); - modifierPlugins.registerPlugin("perlin", PerlinModifier.class); + modifierPlugins.registerPlugin(ModifierParameter.TYPE_BUMP_MAP, BumpMappingModifier.class); + modifierPlugins.registerPlugin(ModifierParameter.TYPE_NORMAL_MAP, NormalMapModifier.class); + modifierPlugins.registerPlugin(ModifierParameter.TYPE_PERLIN, PerlinModifier.class); } static { @@ -229,10 +232,10 @@ public final class PluginRegistry { static { // camera lenses - cameraLensPlugins.registerPlugin("pinhole", PinholeLens.class); - cameraLensPlugins.registerPlugin("thinlens", ThinLens.class); - cameraLensPlugins.registerPlugin("fisheye", FisheyeLens.class); - cameraLensPlugins.registerPlugin("spherical", SphericalLens.class); + cameraLensPlugins.registerPlugin(CameraParameter.TYPE_PINHOLE, PinholeLens.class); + cameraLensPlugins.registerPlugin(CameraParameter.TYPE_THINLENS, ThinLens.class); + cameraLensPlugins.registerPlugin(CameraParameter.TYPE_FISH_EYE, FisheyeLens.class); + cameraLensPlugins.registerPlugin(CameraParameter.TYPE_SPHERICAL, SphericalLens.class); } static { @@ -245,12 +248,12 @@ public final class PluginRegistry { static { // bucket orders - bucketOrderPlugins.registerPlugin("column", ColumnBucketOrder.class); - bucketOrderPlugins.registerPlugin("diagonal", DiagonalBucketOrder.class); - bucketOrderPlugins.registerPlugin("hilbert", HilbertBucketOrder.class); - bucketOrderPlugins.registerPlugin("random", RandomBucketOrder.class); - bucketOrderPlugins.registerPlugin("row", RowBucketOrder.class); - bucketOrderPlugins.registerPlugin("spiral", SpiralBucketOrder.class); + bucketOrderPlugins.registerPlugin(BucketParameter.ORDER_COLUMN, ColumnBucketOrder.class); + bucketOrderPlugins.registerPlugin(BucketParameter.ORDER_DIAGONAL, DiagonalBucketOrder.class); + bucketOrderPlugins.registerPlugin(BucketParameter.ORDER_HILBERT, HilbertBucketOrder.class); + bucketOrderPlugins.registerPlugin(BucketParameter.ORDER_RANDOM, RandomBucketOrder.class); + bucketOrderPlugins.registerPlugin(BucketParameter.ORDER_ROW, RowBucketOrder.class); + bucketOrderPlugins.registerPlugin(BucketParameter.ORDER_SPIRAL, SpiralBucketOrder.class); } static { diff --git a/src/main/java/org/sunflow/RealtimeBenchmark.java b/src/main/java/org/sunflow/RealtimeBenchmark.java index af20648..966b118 100644 --- a/src/main/java/org/sunflow/RealtimeBenchmark.java +++ b/src/main/java/org/sunflow/RealtimeBenchmark.java @@ -1,5 +1,8 @@ package org.sunflow; +import org.sunflow.core.parameter.BucketParameter; +import org.sunflow.core.parameter.ImageParameter; +import org.sunflow.core.parameter.TraceDepthsParameter; import org.sunflow.core.Display; import org.sunflow.core.display.FastDisplay; import org.sunflow.core.display.FileDisplay; @@ -19,15 +22,15 @@ public RealtimeBenchmark(boolean showGUI, int threads) { parameter("threads", threads); // spawn regular priority threads parameter("threads.lowPriority", false); - parameter("resolutionX", 512); - parameter("resolutionY", 512); - parameter("aa.min", -3); - parameter("aa.max", 0); - parameter("depths.diffuse", 1); - parameter("depths.reflection", 1); - parameter("depths.refraction", 0); - parameter("bucket.order", "hilbert"); - parameter("bucket.size", 32); + parameter(ImageParameter.PARAM_RESOLUTION_X, 512); + parameter(ImageParameter.PARAM_RESOLUTION_Y, 512); + parameter(ImageParameter.PARAM_AA_MIN, -3); + parameter(ImageParameter.PARAM_AA_MAX, 0); + parameter(TraceDepthsParameter.PARAM_DEPTHS_DIFFUSE, 1); + parameter(TraceDepthsParameter.PARAM_DEPTHS_REFLECTION, 1); + parameter(TraceDepthsParameter.PARAM_DEPTHS_REFRACTION, 0); + parameter(BucketParameter.PARAM_BUCKET_ORDER, BucketParameter.ORDER_HILBERT); + parameter(BucketParameter.PARAM_BUCKET_SIZE, 32); options(SunflowAPI.DEFAULT_OPTIONS); // camera Point3 eye = new Point3(30, 0, 10.967f); diff --git a/src/main/java/org/sunflow/RenderObjectMap.java b/src/main/java/org/sunflow/RenderObjectMap.java index fa49cd2..83acb36 100644 --- a/src/main/java/org/sunflow/RenderObjectMap.java +++ b/src/main/java/org/sunflow/RenderObjectMap.java @@ -19,7 +19,7 @@ import org.sunflow.system.UI.Module; import org.sunflow.util.FastHashMap; -final class RenderObjectMap { +public final class RenderObjectMap { private FastHashMap renderObjects; private boolean rebuildInstanceList; private boolean rebuildLightList; @@ -33,10 +33,14 @@ private enum RenderObjectType { rebuildInstanceList = rebuildLightList = false; } - final boolean has(String name) { + public final boolean has(String name) { return renderObjects.containsKey(name); } + public final RenderObjectHandle get(String name) { + return renderObjects.get(name); + } + final void remove(String name) { RenderObjectHandle obj = renderObjects.get(name); if (obj == null) { @@ -249,8 +253,8 @@ final LightSource lookupLight(String name) { return (handle == null) ? null : handle.getLight(); } - private static final class RenderObjectHandle { - private final RenderObject obj; + public static final class RenderObjectHandle { + public final RenderObject obj; private final RenderObjectType type; private RenderObjectHandle(Shader shader) { diff --git a/src/main/java/org/sunflow/SunflowAPI.java b/src/main/java/org/sunflow/SunflowAPI.java index ec008fe..b31b0fa 100644 --- a/src/main/java/org/sunflow/SunflowAPI.java +++ b/src/main/java/org/sunflow/SunflowAPI.java @@ -9,6 +9,7 @@ import org.codehaus.commons.compiler.CompileException; import org.codehaus.janino.ClassBodyEvaluator; import org.codehaus.janino.Scanner; +import org.sunflow.core.parameter.shader.ShaderParameter; import org.sunflow.core.Camera; import org.sunflow.core.CameraLens; import org.sunflow.core.Display; @@ -48,31 +49,12 @@ public class SunflowAPI implements SunflowAPIInterface { public static final String VERSION = "0.07.3"; public static final String DEFAULT_OPTIONS = "::options"; - private Scene scene; - private SearchPath includeSearchPath; - private SearchPath textureSearchPath; - private ParameterList parameterList; - private RenderObjectMap renderObjects; - private int currentFrame; - - /** - * This is a quick system test which verifies that the user has launched - * Java properly. - */ - public static void runSystemCheck() { - final long RECOMMENDED_MAX_SIZE = 800; - long maxMb = Runtime.getRuntime().maxMemory() / 1048576; - if (maxMb < RECOMMENDED_MAX_SIZE) - UI.printError(Module.API, "JVM available memory is below %d MB (found %d MB only).\nPlease make sure you launched the program with the -Xmx command line options.", RECOMMENDED_MAX_SIZE, maxMb); - String compiler = System.getProperty("java.vm.name"); - if (compiler == null || !(compiler.contains("HotSpot") && compiler.contains("Server"))) - UI.printError(Module.API, "You do not appear to be running Sun's server JVM\nPerformance may suffer"); - UI.printDetailed(Module.API, "Java environment settings:"); - UI.printDetailed(Module.API, " * Max memory available : %d MB", maxMb); - UI.printDetailed(Module.API, " * Virtual machine name : %s", compiler == null ? " list; private int numVerts, numFaces, numFaceVerts; diff --git a/src/main/java/org/sunflow/core/camera/PinholeLens.java b/src/main/java/org/sunflow/core/camera/PinholeLens.java index 0a9b86e..092c0dd 100644 --- a/src/main/java/org/sunflow/core/camera/PinholeLens.java +++ b/src/main/java/org/sunflow/core/camera/PinholeLens.java @@ -37,4 +37,36 @@ public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lens float dv = shiftY - av + ((2.0f * av * y) / (imageHeight - 1.0f)); return new Ray(0, 0, 0, du, dv, -1); } + + public float getAspect() { + return aspect; + } + + public void setAspect(float aspect) { + this.aspect = aspect; + } + + public float getFov() { + return fov; + } + + public void setFov(float fov) { + this.fov = fov; + } + + public float getShiftX() { + return shiftX; + } + + public void setShiftX(float shiftX) { + this.shiftX = shiftX; + } + + public float getShiftY() { + return shiftY; + } + + public void setShiftY(float shiftY) { + this.shiftY = shiftY; + } } \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/camera/ThinLens.java b/src/main/java/org/sunflow/core/camera/ThinLens.java index 092a6e4..6f63fac 100644 --- a/src/main/java/org/sunflow/core/camera/ThinLens.java +++ b/src/main/java/org/sunflow/core/camera/ThinLens.java @@ -100,4 +100,76 @@ public Ray getRay(float x, float y, int imageWidth, int imageHeight, double lens // ray return new Ray(eyeX, eyeY, eyeZ, dirX - eyeX, dirY - eyeY, dirZ - eyeZ); } + + public float getAspect() { + return aspect; + } + + public void setAspect(float aspect) { + this.aspect = aspect; + } + + public float getFov() { + return fov; + } + + public void setFov(float fov) { + this.fov = fov; + } + + public float getShiftX() { + return shiftX; + } + + public void setShiftX(float shiftX) { + this.shiftX = shiftX; + } + + public float getShiftY() { + return shiftY; + } + + public void setShiftY(float shiftY) { + this.shiftY = shiftY; + } + + public float getFocusDistance() { + return focusDistance; + } + + public void setFocusDistance(float focusDistance) { + this.focusDistance = focusDistance; + } + + public float getLensRadius() { + return lensRadius; + } + + public void setLensRadius(float lensRadius) { + this.lensRadius = lensRadius; + } + + public int getLensSides() { + return lensSides; + } + + public void setLensSides(int lensSides) { + this.lensSides = lensSides; + } + + public float getLensRotation() { + return lensRotation; + } + + public void setLensRotation(float lensRotation) { + this.lensRotation = lensRotation; + } + + public float getLensRotationRadians() { + return lensRotationRadians; + } + + public void setLensRotationRadians(float lensRotationRadians) { + this.lensRotationRadians = lensRotationRadians; + } } \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/gi/AmbientOcclusionGIEngine.java b/src/main/java/org/sunflow/core/gi/AmbientOcclusionGIEngine.java index 80a9dea..9dde12a 100644 --- a/src/main/java/org/sunflow/core/gi/AmbientOcclusionGIEngine.java +++ b/src/main/java/org/sunflow/core/gi/AmbientOcclusionGIEngine.java @@ -5,6 +5,7 @@ import org.sunflow.core.Ray; import org.sunflow.core.Scene; import org.sunflow.core.ShadingState; +import org.sunflow.core.parameter.gi.AmbientOcclusionGIParameter; import org.sunflow.image.Color; import org.sunflow.math.OrthoNormalBasis; import org.sunflow.math.Vector3; @@ -20,10 +21,10 @@ public Color getGlobalRadiance(ShadingState state) { } public boolean init(Options options, Scene scene) { - bright = options.getColor("gi.ambocc.bright", Color.WHITE); - dark = options.getColor("gi.ambocc.dark", Color.BLACK); - samples = options.getInt("gi.ambocc.samples", 32); - maxDist = options.getFloat("gi.ambocc.maxdist", 0); + bright = options.getColor(AmbientOcclusionGIParameter.PARAM_BRIGHT, Color.WHITE); + dark = options.getColor(AmbientOcclusionGIParameter.PARAM_DARK, Color.BLACK); + samples = options.getInt(AmbientOcclusionGIParameter.PARAM_SAMPLES, 32); + maxDist = options.getFloat(AmbientOcclusionGIParameter.PARAM_MAXDIST, 0); maxDist = (maxDist <= 0) ? Float.POSITIVE_INFINITY : maxDist; return true; } diff --git a/src/main/java/org/sunflow/core/gi/FakeGIEngine.java b/src/main/java/org/sunflow/core/gi/FakeGIEngine.java index c1cc46c..61a5737 100644 --- a/src/main/java/org/sunflow/core/gi/FakeGIEngine.java +++ b/src/main/java/org/sunflow/core/gi/FakeGIEngine.java @@ -4,6 +4,7 @@ import org.sunflow.core.Options; import org.sunflow.core.Scene; import org.sunflow.core.ShadingState; +import org.sunflow.core.parameter.gi.FakeGIParameter; import org.sunflow.image.Color; import org.sunflow.math.Vector3; @@ -33,9 +34,9 @@ public Color getGlobalRadiance(ShadingState state) { } public boolean init(Options options, Scene scene) { - up = options.getVector("gi.fake.up", new Vector3(0, 1, 0)).normalize(); - sky = options.getColor("gi.fake.sky", Color.WHITE).copy(); - ground = options.getColor("gi.fake.ground", Color.BLACK).copy(); + up = options.getVector(FakeGIParameter.PARAM_UP, new Vector3(0, 1, 0)).normalize(); + sky = options.getColor(FakeGIParameter.PARAM_SKY, Color.WHITE).copy(); + ground = options.getColor(FakeGIParameter.PARAM_GROUND, Color.BLACK).copy(); sky.mul((float) Math.PI); ground.mul((float) Math.PI); return true; diff --git a/src/main/java/org/sunflow/core/gi/InstantGI.java b/src/main/java/org/sunflow/core/gi/InstantGI.java index 6f41a60..1e2d985 100644 --- a/src/main/java/org/sunflow/core/gi/InstantGI.java +++ b/src/main/java/org/sunflow/core/gi/InstantGI.java @@ -8,6 +8,7 @@ import org.sunflow.core.Ray; import org.sunflow.core.Scene; import org.sunflow.core.ShadingState; +import org.sunflow.core.parameter.gi.InstantGIParameter; import org.sunflow.image.Color; import org.sunflow.math.BoundingBox; import org.sunflow.math.OrthoNormalBasis; @@ -44,10 +45,10 @@ public Color getGlobalRadiance(ShadingState state) { } public boolean init(Options options, Scene scene) { - numPhotons = options.getInt("gi.igi.samples", 64); - numSets = options.getInt("gi.igi.sets", 1); - c = options.getFloat("gi.igi.c", 0.00003f); - numBias = options.getInt("gi.igi.bias_samples", 0); + numPhotons = options.getInt(InstantGIParameter.PARAM_SAMPLES, 64); + numSets = options.getInt(InstantGIParameter.PARAM_SETS, 1); + c = options.getFloat(InstantGIParameter.PARAM_BIAS, 0.00003f); + numBias = options.getInt(InstantGIParameter.PARAM_BIAS_SAMPLES, 0); virtualLights = null; if (numSets < 1) numSets = 1; @@ -93,8 +94,9 @@ public Color getIrradiance(ShadingState state, Color diffuseReflectance) { } // bias compensation int nb = (state.getDiffuseDepth() == 0 || numBias <= 0) ? numBias : 1; - if (nb <= 0) + if (nb <= 0) { return irr; + } OrthoNormalBasis onb = state.getBasis(); Vector3 w = new Vector3(); float scale = (float) Math.PI / nb; diff --git a/src/main/java/org/sunflow/core/gi/IrradianceCacheGIEngine.java b/src/main/java/org/sunflow/core/gi/IrradianceCacheGIEngine.java index 230198b..bd47359 100644 --- a/src/main/java/org/sunflow/core/gi/IrradianceCacheGIEngine.java +++ b/src/main/java/org/sunflow/core/gi/IrradianceCacheGIEngine.java @@ -9,6 +9,7 @@ import org.sunflow.core.Ray; import org.sunflow.core.Scene; import org.sunflow.core.ShadingState; +import org.sunflow.core.parameter.gi.IrrCacheGIParameter; import org.sunflow.image.Color; import org.sunflow.math.MathUtils; import org.sunflow.math.OrthoNormalBasis; @@ -29,14 +30,14 @@ public class IrradianceCacheGIEngine implements GIEngine { public boolean init(Options options, Scene scene) { // get settings - samples = options.getInt("gi.irr-cache.samples", 256); - tolerance = options.getFloat("gi.irr-cache.tolerance", 0.05f); + samples = options.getInt(IrrCacheGIParameter.PARAM_SAMPLES, 256); + tolerance = options.getFloat(IrrCacheGIParameter.PARAM_TOLERANCE, 0.05f); invTolerance = 1.0f / tolerance; - minSpacing = options.getFloat("gi.irr-cache.min_spacing", 0.05f); - maxSpacing = options.getFloat("gi.irr-cache.max_spacing", 5.00f); + minSpacing = options.getFloat(IrrCacheGIParameter.PARAM_MIN_SPACING, 0.05f); + maxSpacing = options.getFloat(IrrCacheGIParameter.PARAM_MAX_SPACING, 5.00f); root = null; rwl = new ReentrantReadWriteLock(); - globalPhotonMap = PluginRegistry.globalPhotonMapPlugins.createObject(options.getString("gi.irr-cache.gmap", null)); + globalPhotonMap = PluginRegistry.globalPhotonMapPlugins.createObject(options.getString(IrrCacheGIParameter.PARAM_GLOBAL, null)); // check settings samples = Math.max(0, samples); minSpacing = Math.max(0.001f, minSpacing); diff --git a/src/main/java/org/sunflow/core/gi/PathTracingGIEngine.java b/src/main/java/org/sunflow/core/gi/PathTracingGIEngine.java index 09b499f..2b66136 100644 --- a/src/main/java/org/sunflow/core/gi/PathTracingGIEngine.java +++ b/src/main/java/org/sunflow/core/gi/PathTracingGIEngine.java @@ -5,6 +5,7 @@ import org.sunflow.core.Ray; import org.sunflow.core.Scene; import org.sunflow.core.ShadingState; +import org.sunflow.core.parameter.gi.PathTracingGIParameter; import org.sunflow.image.Color; import org.sunflow.math.OrthoNormalBasis; import org.sunflow.math.Vector3; @@ -15,7 +16,7 @@ public class PathTracingGIEngine implements GIEngine { private int samples; public boolean init(Options options, Scene scene) { - samples = options.getInt("gi.path.samples", 16); + samples = options.getInt(PathTracingGIParameter.PARAM_SAMPLES, 16); samples = Math.max(0, samples); UI.printInfo(Module.LIGHT, "Path tracer settings:"); UI.printInfo(Module.LIGHT, " * Samples: %d", samples); diff --git a/src/main/java/org/sunflow/core/light/DirectionalSpotlight.java b/src/main/java/org/sunflow/core/light/DirectionalSpotlight.java index d95a38f..4991298 100644 --- a/src/main/java/org/sunflow/core/light/DirectionalSpotlight.java +++ b/src/main/java/org/sunflow/core/light/DirectionalSpotlight.java @@ -7,36 +7,41 @@ import org.sunflow.core.ParameterList; import org.sunflow.core.Ray; import org.sunflow.core.ShadingState; +import org.sunflow.core.parameter.light.DirectionalLightParameter; +import org.sunflow.core.parameter.light.LightParameter; import org.sunflow.image.Color; import org.sunflow.math.OrthoNormalBasis; import org.sunflow.math.Point3; import org.sunflow.math.Vector3; public class DirectionalSpotlight implements LightSource { - private Point3 src; - private Vector3 dir; + private Point3 source; + private Vector3 direction; private OrthoNormalBasis basis; - private float r, r2; + // Radius + private float r; + // Radius^2 + private float r2; private Color radiance; public DirectionalSpotlight() { - src = new Point3(0, 0, 0); - dir = new Vector3(0, 0, -1); - dir.normalize(); - basis = OrthoNormalBasis.makeFromW(dir); + source = new Point3(0, 0, 0); + direction = new Vector3(0, 0, -1); + direction.normalize(); + basis = OrthoNormalBasis.makeFromW(direction); r = 1; r2 = r * r; radiance = Color.WHITE; } public boolean update(ParameterList pl, SunflowAPI api) { - src = pl.getPoint("source", src); - dir = pl.getVector("dir", dir); - dir.normalize(); - r = pl.getFloat("radius", r); - basis = OrthoNormalBasis.makeFromW(dir); + source = pl.getPoint(DirectionalLightParameter.PARAM_SOURCE, source); + direction = pl.getVector(DirectionalLightParameter.PARAM_DIRECTION, direction); + direction.normalize(); + r = pl.getFloat(DirectionalLightParameter.PARAM_RADIUS, r); + basis = OrthoNormalBasis.makeFromW(direction); r2 = r * r; - radiance = pl.getColor("radiance", radiance); + radiance = pl.getColor(LightParameter.PARAM_RADIANCE, radiance); return true; } @@ -49,21 +54,21 @@ public int getLowSamples() { } public void getSamples(ShadingState state) { - if (Vector3.dot(dir, state.getGeoNormal()) < 0 && Vector3.dot(dir, state.getNormal()) < 0) { + if (Vector3.dot(direction, state.getGeoNormal()) < 0 && Vector3.dot(direction, state.getNormal()) < 0) { // project point onto source plane - float x = state.getPoint().x - src.x; - float y = state.getPoint().y - src.y; - float z = state.getPoint().z - src.z; - float t = ((x * dir.x) + (y * dir.y) + (z * dir.z)); + float x = state.getPoint().x - source.x; + float y = state.getPoint().y - source.y; + float z = state.getPoint().z - source.z; + float t = ((x * direction.x) + (y * direction.y) + (z * direction.z)); if (t >= 0.0) { - x -= (t * dir.x); - y -= (t * dir.y); - z -= (t * dir.z); + x -= (t * direction.x); + y -= (t * direction.y); + z -= (t * direction.z); if (((x * x) + (y * y) + (z * z)) <= r2) { Point3 p = new Point3(); - p.x = src.x + x; - p.y = src.y + y; - p.z = src.z + z; + p.x = source.x + x; + p.y = source.y + y; + p.z = source.z + z; LightSample dest = new LightSample(); dest.setShadowRay(new Ray(state.getPoint(), p)); dest.setRadiance(radiance, radiance); @@ -81,8 +86,8 @@ public void getPhoton(double randX1, double randY1, double randX2, double randY2 dir.y = r * (float) Math.sin(phi) * s; dir.z = 0; basis.transform(dir); - Point3.add(src, dir, p); - dir.set(this.dir); + Point3.add(source, dir, p); + dir.set(this.direction); power.set(radiance).mul((float) Math.PI * r2); } @@ -93,4 +98,36 @@ public float getPower() { public Instance createInstance() { return null; } + + public Point3 getSource() { + return source; + } + + public void setSource(Point3 source) { + this.source = source; + } + + public Vector3 getDirection() { + return direction; + } + + public void setDirection(Vector3 direction) { + this.direction = direction; + } + + public float getR() { + return r; + } + + public void setR(float r) { + this.r = r; + } + + public Color getRadiance() { + return radiance; + } + + public void setRadiance(Color radiance) { + this.radiance = radiance; + } } \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/light/ImageBasedLight.java b/src/main/java/org/sunflow/core/light/ImageBasedLight.java index 8d3b465..4b20577 100644 --- a/src/main/java/org/sunflow/core/light/ImageBasedLight.java +++ b/src/main/java/org/sunflow/core/light/ImageBasedLight.java @@ -12,6 +12,8 @@ import org.sunflow.core.ShadingState; import org.sunflow.core.Texture; import org.sunflow.core.TextureCache; +import org.sunflow.core.parameter.light.ImageBasedLightParameter; +import org.sunflow.core.parameter.light.LightParameter; import org.sunflow.image.Bitmap; import org.sunflow.image.Color; import org.sunflow.math.BoundingBox; @@ -50,10 +52,10 @@ private void updateBasis(Vector3 center, Vector3 up) { } public boolean update(ParameterList pl, SunflowAPI api) { - updateBasis(pl.getVector("center", null), pl.getVector("up", null)); - numSamples = pl.getInt("samples", numSamples); - numLowSamples = pl.getInt("lowsamples", numLowSamples); - String filename = pl.getString("texture", null); + updateBasis(pl.getVector(ImageBasedLightParameter.PARAM_CENTER, null), pl.getVector(ImageBasedLightParameter.PARAM_UP, null)); + numSamples = pl.getInt(LightParameter.PARAM_SAMPLES, numSamples); + numLowSamples = pl.getInt(ImageBasedLightParameter.PARAM_LOW_SAMPLES, numLowSamples); + String filename = pl.getString(ImageBasedLightParameter.PARAM_TEXTURE, null); if (filename != null) texture = TextureCache.getTexture(api.resolveTextureFilename(filename), false); @@ -90,7 +92,7 @@ public boolean update(ParameterList pl, SunflowAPI api) { jacobian = (float) (2 * Math.PI * Math.PI) / (b.getWidth() * b.getHeight()); } // take fixed samples - if (pl.getBoolean("fixed", samples != null)) { + if (pl.getBoolean(ImageBasedLightParameter.PARAM_FIXED, samples != null)) { // high density samples samples = new Vector3[numSamples]; colors = new Color[numSamples]; diff --git a/src/main/java/org/sunflow/core/light/PointLight.java b/src/main/java/org/sunflow/core/light/PointLight.java index 86e5955..61cf2a8 100644 --- a/src/main/java/org/sunflow/core/light/PointLight.java +++ b/src/main/java/org/sunflow/core/light/PointLight.java @@ -7,22 +7,26 @@ import org.sunflow.core.ParameterList; import org.sunflow.core.Ray; import org.sunflow.core.ShadingState; +import org.sunflow.core.parameter.light.PointLightParameter; import org.sunflow.image.Color; import org.sunflow.math.Point3; import org.sunflow.math.Vector3; public class PointLight implements LightSource { + // Center private Point3 lightPoint; - private Color power; + + // Radiance + private Color color; public PointLight() { lightPoint = new Point3(0, 0, 0); - power = Color.WHITE; + color = Color.WHITE; } public boolean update(ParameterList pl, SunflowAPI api) { - lightPoint = pl.getPoint("center", lightPoint); - power = pl.getColor("power", power); + lightPoint = pl.getPoint(PointLightParameter.PARAM_CENTER, lightPoint); + color = pl.getColor(PointLightParameter.PARAM_POWER, color); return true; } @@ -37,7 +41,7 @@ public void getSamples(ShadingState state) { // prepare shadow ray dest.setShadowRay(new Ray(state.getPoint(), lightPoint)); float scale = 1.0f / (float) (4 * Math.PI * lightPoint.distanceToSquared(state.getPoint())); - dest.setRadiance(power, power); + dest.setRadiance(color, color); dest.getDiffuseRadiance().mul(scale); dest.getSpecularRadiance().mul(scale); dest.traceShadow(state); @@ -52,11 +56,27 @@ public void getPhoton(double randX1, double randY1, double randX2, double randY2 dir.x = (float) Math.cos(phi) * s; dir.y = (float) Math.sin(phi) * s; dir.z = (float) (1 - 2 * randY1); - power.set(this.power); + power.set(this.color); + } + + public Point3 getLightPoint() { + return lightPoint; + } + + public void setLightPoint(Point3 lightPoint) { + this.lightPoint = lightPoint; } public float getPower() { - return power.getLuminance(); + return color.getLuminance(); + } + + public void setColor(Color color) { + this.color = color; + } + + public Color getColor() { + return color; } public Instance createInstance() { diff --git a/src/main/java/org/sunflow/core/light/SphereLight.java b/src/main/java/org/sunflow/core/light/SphereLight.java index d589a02..b78eefe 100644 --- a/src/main/java/org/sunflow/core/light/SphereLight.java +++ b/src/main/java/org/sunflow/core/light/SphereLight.java @@ -8,6 +8,8 @@ import org.sunflow.core.Ray; import org.sunflow.core.Shader; import org.sunflow.core.ShadingState; +import org.sunflow.core.parameter.light.LightParameter; +import org.sunflow.core.parameter.light.SphereLightParameter; import org.sunflow.core.primitive.Sphere; import org.sunflow.image.Color; import org.sunflow.math.Matrix4; @@ -31,18 +33,14 @@ public SphereLight() { } public boolean update(ParameterList pl, SunflowAPI api) { - radiance = pl.getColor("radiance", radiance); - numSamples = pl.getInt("samples", numSamples); - radius = pl.getFloat("radius", radius); + radiance = pl.getColor(LightParameter.PARAM_RADIANCE, radiance); + numSamples = pl.getInt(LightParameter.PARAM_SAMPLES, numSamples); + radius = pl.getFloat(SphereLightParameter.PARAM_RADIUS, radius); r2 = radius * radius; - center = pl.getPoint("center", center); + center = pl.getPoint(SphereLightParameter.PARAM_CENTER, center); return true; } - public int getNumSamples() { - return numSamples; - } - public int getLowSamples() { return 1; } @@ -150,4 +148,44 @@ public void scatterPhoton(ShadingState state, Color power) { public Instance createInstance() { return Instance.createTemporary(new Sphere(), Matrix4.translation(center.x, center.y, center.z).multiply(Matrix4.scale(radius)), this); } + + public Color getRadiance() { + return radiance; + } + + public void setRadiance(Color radiance) { + this.radiance = radiance; + } + + public int getNumSamples() { + return numSamples; + } + + public void setNumSamples(int numSamples) { + this.numSamples = numSamples; + } + + public Point3 getCenter() { + return center; + } + + public void setCenter(Point3 center) { + this.center = center; + } + + public float getRadius() { + return radius; + } + + public void setRadius(float radius) { + this.radius = radius; + } + + public float getR2() { + return r2; + } + + public void setR2(float r2) { + this.r2 = r2; + } } \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/light/SunSkyLight.java b/src/main/java/org/sunflow/core/light/SunSkyLight.java index 8b9c794..af7db49 100644 --- a/src/main/java/org/sunflow/core/light/SunSkyLight.java +++ b/src/main/java/org/sunflow/core/light/SunSkyLight.java @@ -10,6 +10,8 @@ import org.sunflow.core.Ray; import org.sunflow.core.Shader; import org.sunflow.core.ShadingState; +import org.sunflow.core.parameter.light.LightParameter; +import org.sunflow.core.parameter.light.SunSkyLightParameter; import org.sunflow.image.ChromaticitySpectrum; import org.sunflow.image.Color; import org.sunflow.image.ConstantSpectralCurve; @@ -190,17 +192,17 @@ private void initSunSky() { } public boolean update(ParameterList pl, SunflowAPI api) { - Vector3 up = pl.getVector("up", null); - Vector3 east = pl.getVector("east", null); + Vector3 up = pl.getVector(SunSkyLightParameter.PARAM_UP, null); + Vector3 east = pl.getVector(SunSkyLightParameter.PARAM_EAST, null); if (up != null && east != null) basis = OrthoNormalBasis.makeFromWV(up, east); else if (up != null) basis = OrthoNormalBasis.makeFromW(up); - numSkySamples = pl.getInt("samples", numSkySamples); - sunDirWorld = pl.getVector("sundir", sunDirWorld); - turbidity = pl.getFloat("turbidity", turbidity); - groundExtendSky = pl.getBoolean("ground.extendsky", groundExtendSky); - groundColor = pl.getColor("ground.color", groundColor); + numSkySamples = pl.getInt(LightParameter.PARAM_SAMPLES, numSkySamples); + sunDirWorld = pl.getVector(SunSkyLightParameter.PARAM_SUN_DIRECTION, sunDirWorld); + turbidity = pl.getFloat(SunSkyLightParameter.PARAM_TURBIDITY, turbidity); + groundExtendSky = pl.getBoolean(SunSkyLightParameter.PARAM_GROUND_EXTENDSKY, groundExtendSky); + groundColor = pl.getColor(SunSkyLightParameter.PARAM_GROUND_COLOR, groundColor); // recompute model initSunSky(); return true; diff --git a/src/main/java/org/sunflow/core/light/TriangleMeshLight.java b/src/main/java/org/sunflow/core/light/TriangleMeshLight.java index c9c94d2..5c174d6 100644 --- a/src/main/java/org/sunflow/core/light/TriangleMeshLight.java +++ b/src/main/java/org/sunflow/core/light/TriangleMeshLight.java @@ -9,6 +9,7 @@ import org.sunflow.core.Ray; import org.sunflow.core.Shader; import org.sunflow.core.ShadingState; +import org.sunflow.core.parameter.light.LightParameter; import org.sunflow.core.primitive.TriangleMesh; import org.sunflow.image.Color; import org.sunflow.math.MathUtils; @@ -30,8 +31,8 @@ public TriangleMeshLight() { @Override public boolean update(ParameterList pl, SunflowAPI api) { - radiance = pl.getColor("radiance", radiance); - numSamples = pl.getInt("samples", numSamples); + radiance = pl.getColor(LightParameter.PARAM_RADIANCE, radiance); + numSamples = pl.getInt(LightParameter.PARAM_SAMPLES, numSamples); if (super.update(pl, api)) { // precompute triangle areas and normals areas = new float[getNumPrimitives()]; diff --git a/src/main/java/org/sunflow/core/modifiers/BumpMappingModifier.java b/src/main/java/org/sunflow/core/modifiers/BumpMappingModifier.java index ef525db..5614775 100644 --- a/src/main/java/org/sunflow/core/modifiers/BumpMappingModifier.java +++ b/src/main/java/org/sunflow/core/modifiers/BumpMappingModifier.java @@ -19,8 +19,9 @@ public BumpMappingModifier() { public boolean update(ParameterList pl, SunflowAPI api) { String filename = pl.getString("texture", null); - if (filename != null) + if (filename != null) { bumpTexture = TextureCache.getTexture(api.resolveTextureFilename(filename), true); + } scale = pl.getFloat("scale", scale); return bumpTexture != null; } @@ -30,4 +31,12 @@ public void modify(ShadingState state) { state.getNormal().set(bumpTexture.getBump(state.getUV().x, state.getUV().y, state.getBasis(), scale)); state.setBasis(OrthoNormalBasis.makeFromW(state.getNormal())); } + + public float getScale() { + return scale; + } + + public void setScale(float scale) { + this.scale = scale; + } } \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/modifiers/PerlinModifier.java b/src/main/java/org/sunflow/core/modifiers/PerlinModifier.java index 08bfc77..e1a3438 100644 --- a/src/main/java/org/sunflow/core/modifiers/PerlinModifier.java +++ b/src/main/java/org/sunflow/core/modifiers/PerlinModifier.java @@ -73,4 +73,28 @@ private static final double noise(double x, double y, double z, double freq) { x1 = .707 * x1 - .707 * y; return PerlinScalar.snoise((float) (freq * x1 + 100), (float) (freq * y1), (float) (freq * z1)); } + + public int getFunction() { + return function; + } + + public void setFunction(int function) { + this.function = function; + } + + public float getScale() { + return scale; + } + + public void setScale(float scale) { + this.scale = scale; + } + + public float getSize() { + return size; + } + + public void setSize(float size) { + this.size = size; + } } \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/parameter/BackgroundParameter.java b/src/main/java/org/sunflow/core/parameter/BackgroundParameter.java new file mode 100644 index 0000000..6f7cd0b --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/BackgroundParameter.java @@ -0,0 +1,33 @@ +package org.sunflow.core.parameter; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.ParameterList; +import org.sunflow.core.parameter.shader.ShaderParameter; +import org.sunflow.image.Color; + +public class BackgroundParameter implements Parameter { + + public static final String PARAM_BACKGROUND = "background"; + public static final String PARAM_BACKGROUND_SHADER = "background.shader"; + public static final String PARAM_BACKGROUND_INSTANCE = "background.instance"; + public static final String PARAM_TYPE_BACKGROUND = "background"; + + Color color; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(ParameterList.PARAM_COLOR, null, color.getRGB()); + api.shader(PARAM_BACKGROUND_SHADER, ShaderParameter.TYPE_CONSTANT); + api.geometry(PARAM_BACKGROUND, PARAM_TYPE_BACKGROUND); + api.parameter(ParameterList.PARAM_SHADERS, PARAM_BACKGROUND_SHADER); + api.instance(PARAM_BACKGROUND_INSTANCE, PARAM_TYPE_BACKGROUND); + } + + public Color getColor() { + return color; + } + + public void setColor(Color color) { + this.color = color; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/BucketParameter.java b/src/main/java/org/sunflow/core/parameter/BucketParameter.java new file mode 100644 index 0000000..7b995e8 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/BucketParameter.java @@ -0,0 +1,47 @@ +package org.sunflow.core.parameter; + +import org.sunflow.SunflowAPI; +import org.sunflow.SunflowAPIInterface; + +public class BucketParameter implements Parameter { + + public static final String PARAM_BUCKET_SIZE = "bucket.size"; + public static final String PARAM_BUCKET_ORDER = "bucket.order"; + public static final String ORDER_COLUMN = "column"; + public static final String ORDER_DIAGONAL = "diagonal"; + public static final String ORDER_HILBERT = "hilbert"; + public static final String ORDER_SPIRAL = "spiral"; + public static final String ORDER_RANDOM = "random"; + public static final String ORDER_ROW = "row"; + + private int size = 0; + private String order = ""; + + @Override + public void setup(SunflowAPIInterface api) { + if (size > 0) { + api.parameter(PARAM_BUCKET_SIZE, size); + } + if (!order.isEmpty()) { + api.parameter(PARAM_BUCKET_ORDER, order); + } + + api.options(SunflowAPI.DEFAULT_OPTIONS); + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public String getOrder() { + return order; + } + + public void setOrder(String order) { + this.order = order; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/IlluminationParameter.java b/src/main/java/org/sunflow/core/parameter/IlluminationParameter.java new file mode 100644 index 0000000..38922c9 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/IlluminationParameter.java @@ -0,0 +1,41 @@ +package org.sunflow.core.parameter; + +public class IlluminationParameter { + + int emit = 0; + String map = ""; + int gather = 0; + float radius = 0; + + public int getEmit() { + return emit; + } + + public void setEmit(int emit) { + this.emit = emit; + } + + public String getMap() { + return map; + } + + public void setMap(String map) { + this.map = map; + } + + public int getGather() { + return gather; + } + + public void setGather(int gather) { + this.gather = gather; + } + + public float getRadius() { + return radius; + } + + public void setRadius(float radius) { + this.radius = radius; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/ImageParameter.java b/src/main/java/org/sunflow/core/parameter/ImageParameter.java new file mode 100644 index 0000000..04a60e6 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/ImageParameter.java @@ -0,0 +1,152 @@ +package org.sunflow.core.parameter; + +import org.sunflow.SunflowAPI; +import org.sunflow.SunflowAPIInterface; + +/** + * Image Block based on SCParser.parseImageBlock() + */ +public class ImageParameter implements Parameter { + + public static final String PARAM_AA_CACHE = "aa.cache"; + public static final String PARAM_AA_CONTRAST = "aa.contrast"; + public static final String PARAM_AA_DISPLAY = "aa.display"; + public static final String PARAM_AA_JITTER = "aa.jitter"; + public static final String PARAM_AA_MIN = "aa.min"; + public static final String PARAM_AA_MAX = "aa.max"; + public static final String PARAM_AA_SAMPLES = "aa.samples"; + public static final String PARAM_RESOLUTION_X = "resolutionX"; + public static final String PARAM_RESOLUTION_Y = "resolutionY"; + public static final String PARAM_SAMPLER = "sampler"; + public static final String PARAM_FILTER = "filter"; + + public static final String FILTER_TRIANGLE = "triangle"; + public static final String FILTER_GAUSSIAN = "gaussian"; + public static final String FILTER_MITCHELL = "mitchel"; + public static final String FILTER_BLACKMAN_HARRIS = "blackman-harris"; + + int resolutionX = 1920; + int resolutionY = 1080; + int aaMin = 0; + int aaMax = 2; + int aaSamples = 4; + float aaContrast = 0; + boolean aaJitter = false; + boolean aaCache = false; + + String sampler = ""; + String filter = ""; + + public void setup(SunflowAPIInterface api) { + if (resolutionX > 0) { + api.parameter(PARAM_RESOLUTION_X, resolutionX); + } + if (resolutionY > 0) { + api.parameter(PARAM_RESOLUTION_Y, resolutionY); + } + + // Always set AA params + api.parameter(PARAM_AA_MIN, aaMin); + api.parameter(PARAM_AA_MAX, aaMax); + + if (aaSamples > 0) { + api.parameter(PARAM_AA_SAMPLES, aaSamples); + } + if (aaContrast != 0) { + api.parameter(PARAM_AA_CONTRAST, aaContrast); + } + + api.parameter(PARAM_AA_JITTER, aaJitter); + + if (!sampler.isEmpty()) { + api.parameter(PARAM_SAMPLER, sampler); + } + if (!filter.isEmpty()) { + api.parameter(PARAM_FILTER, filter); + } + + api.parameter(PARAM_AA_CACHE, aaCache); + + api.options(SunflowAPI.DEFAULT_OPTIONS); + } + + public int getResolutionX() { + return resolutionX; + } + + public void setResolutionX(int resolutionX) { + this.resolutionX = resolutionX; + } + + public int getResolutionY() { + return resolutionY; + } + + public void setResolutionY(int resolutionY) { + this.resolutionY = resolutionY; + } + + public int getAAMin() { + return aaMin; + } + + public void setAAMin(int aaMin) { + this.aaMin = aaMin; + } + + public int getAAMax() { + return aaMax; + } + + public void setAAMax(int aaMax) { + this.aaMax = aaMax; + } + + public int getAASamples() { + return aaSamples; + } + + public void setAASamples(int aaSamples) { + this.aaSamples = aaSamples; + } + + public float getAAContrast() { + return aaContrast; + } + + public void setAAContrast(float aaContrast) { + this.aaContrast = aaContrast; + } + + public boolean isAAJitter() { + return aaJitter; + } + + public void setAAJitter(boolean aaJitter) { + this.aaJitter = aaJitter; + } + + public boolean isAACache() { + return aaCache; + } + + public void setAACache(boolean aaCache) { + this.aaCache = aaCache; + } + + public String getSampler() { + return sampler; + } + + public void setSampler(String sampler) { + this.sampler = sampler; + } + + public String getFilter() { + return filter; + } + + public void setFilter(String filter) { + this.filter = filter; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/InstanceParameter.java b/src/main/java/org/sunflow/core/parameter/InstanceParameter.java new file mode 100644 index 0000000..7c97b05 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/InstanceParameter.java @@ -0,0 +1,69 @@ +package org.sunflow.core.parameter; + +import org.sunflow.SunflowAPIInterface; + +public class InstanceParameter implements Parameter { + + String name; + String geometry; + + String[] shaders = null; + String[] modifiers = null; + + TransformParameter transform = null; + + @Override + public void setup(SunflowAPIInterface api) { + if (transform != null) { + transform.setup(api); + } + if (shaders != null) { + api.parameter("shaders", shaders); + } + if (modifiers != null) { + api.parameter("modifiers", modifiers); + } + + api.instance(name, geometry); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getGeometry() { + return geometry; + } + + public void setGeometry(String geometry) { + this.geometry = geometry; + } + + public String[] getShaders() { + return shaders; + } + + public void setShaders(String[] shaders) { + this.shaders = shaders; + } + + public String[] getModifiers() { + return modifiers; + } + + public void setModifiers(String[] modifiers) { + this.modifiers = modifiers; + } + + public TransformParameter getTransform() { + return transform; + } + + public void setTransform(TransformParameter transform) { + this.transform = transform; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/OverrideParameter.java b/src/main/java/org/sunflow/core/parameter/OverrideParameter.java new file mode 100644 index 0000000..3667184 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/OverrideParameter.java @@ -0,0 +1,40 @@ +package org.sunflow.core.parameter; + +import org.sunflow.SunflowAPI; +import org.sunflow.SunflowAPIInterface; + +public class OverrideParameter implements Parameter { + + public static final String PARAM_OVERRIDE_SHADER = "override.shader"; + public static final String PARAM_OVERRIDE_PHOTONS = "override.photons"; + + String shader = ""; + boolean photons = false; + + @Override + public void setup(SunflowAPIInterface api) { + + if (!shader.isEmpty()) { + api.parameter(PARAM_OVERRIDE_SHADER, shader); + } + api.parameter(PARAM_OVERRIDE_PHOTONS, photons); + + api.options(SunflowAPI.DEFAULT_OPTIONS); + } + + public String getShader() { + return shader; + } + + public void setShader(String shader) { + this.shader = shader; + } + + public boolean isPhotons() { + return photons; + } + + public void setPhotons(boolean photons) { + this.photons = photons; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/Parameter.java b/src/main/java/org/sunflow/core/parameter/Parameter.java new file mode 100644 index 0000000..1d3e9ec --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/Parameter.java @@ -0,0 +1,7 @@ +package org.sunflow.core.parameter; + +import org.sunflow.SunflowAPIInterface; + +public interface Parameter { + void setup(SunflowAPIInterface api); +} diff --git a/src/main/java/org/sunflow/core/parameter/PhotonParameter.java b/src/main/java/org/sunflow/core/parameter/PhotonParameter.java new file mode 100644 index 0000000..5fba9e1 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/PhotonParameter.java @@ -0,0 +1,47 @@ +package org.sunflow.core.parameter; + +import org.sunflow.SunflowAPI; +import org.sunflow.SunflowAPIInterface; + +public class PhotonParameter implements Parameter { + + public static final String PARAM_CAUSTICS = "caustics"; + public static final String PARAM_CAUSTICS_EMIT = "caustics.emit"; + public static final String PARAM_CAUSTICS_GATHER = "caustics.gather"; + public static final String PARAM_CAUSTICS_RADIUS = "caustics.radius"; + + IlluminationParameter caustics; + + public PhotonParameter() { + caustics = new IlluminationParameter(); + } + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(PARAM_CAUSTICS, caustics.map); + api.parameter(PARAM_CAUSTICS_EMIT, caustics.emit); + api.parameter(PARAM_CAUSTICS_GATHER, caustics.gather); + api.parameter(PARAM_CAUSTICS_RADIUS, caustics.radius); + api.options(SunflowAPI.DEFAULT_OPTIONS); + } + + public int getNumEmit() { + return caustics.emit; + } + + public void setNumEmit(int numEmit) { + caustics.emit = numEmit; + } + + public void setCaustics(String caustics) { + this.caustics.map = caustics; + } + + public void setCausticsGather(int causticsGather) { + caustics.gather = causticsGather; + } + + public void setCausticsRadius(float causticsRadius) { + caustics.radius = causticsRadius; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/TraceDepthsParameter.java b/src/main/java/org/sunflow/core/parameter/TraceDepthsParameter.java new file mode 100644 index 0000000..b78d0d9 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/TraceDepthsParameter.java @@ -0,0 +1,51 @@ +package org.sunflow.core.parameter; + +import org.sunflow.SunflowAPIInterface; + +public class TraceDepthsParameter implements Parameter { + + public static final String PARAM_DEPTHS_DIFFUSE = "depths.diffuse"; + public static final String PARAM_DEPTHS_REFLECTION = "depths.reflection"; + public static final String PARAM_DEPTHS_REFRACTION = "depths.refraction"; + + int diffuse = 0; + int reflection = 0; + int refraction = 0; + + @Override + public void setup(SunflowAPIInterface api) { + if (diffuse > 0) { + api.parameter(PARAM_DEPTHS_DIFFUSE, diffuse); + } + if (reflection > 0) { + api.parameter(PARAM_DEPTHS_REFLECTION, reflection); + } + if (refraction > 0) { + api.parameter(PARAM_DEPTHS_REFRACTION, refraction); + } + } + + public int getDiffuse() { + return diffuse; + } + + public void setDiffuse(int diffuse) { + this.diffuse = diffuse; + } + + public int getReflection() { + return reflection; + } + + public void setReflection(int reflection) { + this.reflection = reflection; + } + + public int getRefraction() { + return refraction; + } + + public void setRefraction(int refraction) { + this.refraction = refraction; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/TransformParameter.java b/src/main/java/org/sunflow/core/parameter/TransformParameter.java new file mode 100644 index 0000000..b6c0876 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/TransformParameter.java @@ -0,0 +1,45 @@ +package org.sunflow.core.parameter; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.math.Matrix4; + +public class TransformParameter implements Parameter { + + public static final String INTERPOLATION_NONE = "none"; + + float[] times; + Matrix4[] transforms; + + String interpolation = INTERPOLATION_NONE; + + @Override + public void setup(SunflowAPIInterface api) { + if (times == null) { + api.parameter("transform", transforms[0]); + } else { + int steps = times.length; + api.parameter("transform.steps", steps); + api.parameter("transform.times", "float", interpolation, times); + for (int i = 0; i < steps; i++) { + api.parameter(String.format("transform[%d]", i), transforms[i]); + } + } + } + + public float[] getTimes() { + return times; + } + + public void setTimes(float[] times) { + this.times = times; + } + + public Matrix4[] getTransforms() { + return transforms; + } + + public void setTransforms(Matrix4[] transforms) { + this.transforms = transforms; + } + +} diff --git a/src/main/java/org/sunflow/core/parameter/camera/CameraParameter.java b/src/main/java/org/sunflow/core/parameter/camera/CameraParameter.java new file mode 100644 index 0000000..95a1742 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/camera/CameraParameter.java @@ -0,0 +1,57 @@ +package org.sunflow.core.parameter.camera; + +import org.sunflow.SunflowAPI; +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.parameter.Parameter; + +public class CameraParameter implements Parameter { + + public static final String TYPE_FISH_EYE = "fisheye"; + public static final String TYPE_PINHOLE = "pinhole"; + public static final String TYPE_SPHERICAL = "spherical"; + public static final String TYPE_THINLENS = "thinlens"; + + public static final String PARAM_FOV = "fov"; + public static final String PARAM_ASPECT = "aspect"; + public static final String PARAM_SHIFT_X = "shift.x"; + public static final String PARAM_SHIFT_Y = "shift.y"; + public static final String PARAM_SHUTTER_OPEN = "shutter.open"; + public static final String PARAM_SHUTTER_CLOSE = "shutter.close"; + public static final String PARAM_CAMERA = "camera"; + + // Default values from Camera + protected float shutterOpen = 0; + protected float shutterClose = 0; + + protected String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public float getShutterOpen() { + return shutterOpen; + } + + public void setShutterOpen(float shutterOpen) { + this.shutterOpen = shutterOpen; + } + + public float getShutterClose() { + return shutterClose; + } + + public void setShutterClose(float shutterClose) { + this.shutterClose = shutterClose; + } + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(PARAM_CAMERA, name); + api.options(SunflowAPI.DEFAULT_OPTIONS); + } +} diff --git a/src/main/java/org/sunflow/core/parameter/camera/FishEyeCameraParameter.java b/src/main/java/org/sunflow/core/parameter/camera/FishEyeCameraParameter.java new file mode 100644 index 0000000..605ab88 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/camera/FishEyeCameraParameter.java @@ -0,0 +1,17 @@ +package org.sunflow.core.parameter.camera; + +import org.sunflow.SunflowAPIInterface; + +public class FishEyeCameraParameter extends CameraParameter { + + // FisheyeLens lens; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(PARAM_SHUTTER_OPEN, shutterOpen); + api.parameter(PARAM_SHUTTER_CLOSE, shutterClose); + api.camera(name, TYPE_FISH_EYE); + super.setup(api); + } + +} diff --git a/src/main/java/org/sunflow/core/parameter/camera/PinholeCameraParameter.java b/src/main/java/org/sunflow/core/parameter/camera/PinholeCameraParameter.java new file mode 100644 index 0000000..e942dd2 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/camera/PinholeCameraParameter.java @@ -0,0 +1,59 @@ +package org.sunflow.core.parameter.camera; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.camera.PinholeLens; + +public class PinholeCameraParameter extends CameraParameter { + + private PinholeLens lens; + + public PinholeCameraParameter() { + lens = new PinholeLens(); + } + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(PARAM_SHUTTER_OPEN, shutterOpen); + api.parameter(PARAM_SHUTTER_CLOSE, shutterClose); + api.parameter(PARAM_FOV, lens.getFov()); + api.parameter(PARAM_ASPECT, lens.getAspect()); + api.parameter(PARAM_SHIFT_X, lens.getShiftX()); + api.parameter(PARAM_SHIFT_Y, lens.getShiftY()); + + api.camera(name, TYPE_PINHOLE); + super.setup(api); + } + + public float getFov() { + return lens.getFov(); + } + + public void setFov(float fov) { + lens.setFov(fov); + } + + public float getAspect() { + return lens.getAspect(); + } + + public void setAspect(float aspect) { + lens.setAspect(aspect); + } + + public float getShiftX() { + return lens.getShiftX(); + } + + public void setShiftX(float shiftX) { + lens.setShiftX(shiftX); + } + + public float getShiftY() { + return lens.getShiftY(); + } + + public void setShiftY(float shiftY) { + lens.setShiftY(shiftY); + } + +} diff --git a/src/main/java/org/sunflow/core/parameter/camera/SphericalCameraParameter.java b/src/main/java/org/sunflow/core/parameter/camera/SphericalCameraParameter.java new file mode 100644 index 0000000..8d72f07 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/camera/SphericalCameraParameter.java @@ -0,0 +1,17 @@ +package org.sunflow.core.parameter.camera; + +import org.sunflow.SunflowAPIInterface; + +public class SphericalCameraParameter extends CameraParameter { + + // SphericalLens lens; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(PARAM_SHUTTER_OPEN, shutterOpen); + api.parameter(PARAM_SHUTTER_CLOSE, shutterClose); + api.camera(name, TYPE_SPHERICAL); + super.setup(api); + } + +} diff --git a/src/main/java/org/sunflow/core/parameter/camera/ThinLensCameraParameter.java b/src/main/java/org/sunflow/core/parameter/camera/ThinLensCameraParameter.java new file mode 100644 index 0000000..6acd2da --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/camera/ThinLensCameraParameter.java @@ -0,0 +1,107 @@ +package org.sunflow.core.parameter.camera; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.camera.ThinLens; + +public class ThinLensCameraParameter extends CameraParameter { + + public static final String PARAM_FOCUS_DISTANCE = "focus.distance"; + public static final String PARAM_LENS_RADIUS = "lens.radius"; + public static final String PARAM_LENS_SIDES = "lens.sides"; + public static final String PARAM_LENS_ROTATION = "lens.rotation"; + + private ThinLens lens; + + public ThinLensCameraParameter() { + lens = new ThinLens(); + } + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(PARAM_SHUTTER_OPEN, shutterOpen); + api.parameter(PARAM_SHUTTER_CLOSE, shutterClose); + api.parameter(PARAM_FOV, lens.getFov()); + api.parameter(PARAM_ASPECT, lens.getAspect()); + api.parameter(PARAM_SHIFT_X, lens.getShiftX()); + api.parameter(PARAM_SHIFT_Y, lens.getShiftY()); + api.parameter(PARAM_FOCUS_DISTANCE, lens.getFocusDistance()); + api.parameter(PARAM_LENS_RADIUS, lens.getLensRadius()); + api.parameter(PARAM_LENS_SIDES, lens.getLensSides()); + api.parameter(PARAM_LENS_ROTATION, lens.getLensRotation()); + api.camera(name, TYPE_THINLENS); + super.setup(api); + } + + public float getAspect() { + return lens.getAspect(); + } + + public void setAspect(float aspect) { + lens.setAspect(aspect); + } + + public float getFov() { + return lens.getFov(); + } + + public void setFov(float fov) { + lens.setFov(fov); + } + + public float getShiftX() { + return lens.getShiftX(); + } + + public void setShiftX(float shiftX) { + lens.setShiftX(shiftX); + } + + public float getShiftY() { + return lens.getShiftY(); + } + + public void setShiftY(float shiftY) { + lens.setShiftY(shiftY); + } + + public float getFocusDistance() { + return lens.getFocusDistance(); + } + + public void setFocusDistance(float focusDistance) { + lens.setFocusDistance(focusDistance); + } + + public float getLensRadius() { + return lens.getLensRadius(); + } + + public void setLensRadius(float lensRadius) { + lens.setLensRadius(lensRadius); + } + + public int getLensSides() { + return lens.getLensSides(); + } + + public void setLensSides(int lensSides) { + lens.setLensSides(lensSides); + } + + public float getLensRotation() { + return lens.getLensRotation(); + } + + public void setLensRotation(float lensRotation) { + lens.setLensRotation(lensRotation); + } + + public float getLensRotationRadians() { + return lens.getLensRotationRadians(); + } + + public void setLensRotationRadians(float lensRotationRadians) { + lens.setLensRotationRadians(lensRotationRadians); + } + +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/BanchOffParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/BanchOffParameter.java new file mode 100644 index 0000000..82a407b --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/BanchOffParameter.java @@ -0,0 +1,12 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; + +public class BanchOffParameter extends ObjectParameter { + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + api.geometry(name, TYPE_BANCHOFF); + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/BezierMeshParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/BezierMeshParameter.java new file mode 100644 index 0000000..4f95853 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/BezierMeshParameter.java @@ -0,0 +1,82 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; + +public class BezierMeshParameter extends ObjectParameter { + + int nu, nv; + boolean uwrap = false, vwrap = false; + float[] points; + + int subdivs = 1; + boolean smooth = false; + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + api.parameter("nu", nu); + api.parameter("nv", nv); + api.parameter("uwrap", uwrap); + api.parameter("vwrap", vwrap); + api.parameter("points", "point", "vertex", points); + api.parameter("subdivs", subdivs); + api.parameter("smooth", smooth); + api.geometry(name, TYPE_BEZIER_MESH); + } + + public int getNu() { + return nu; + } + + public void setNu(int nu) { + this.nu = nu; + } + + public int getNv() { + return nv; + } + + public void setNv(int nv) { + this.nv = nv; + } + + public boolean isUwrap() { + return uwrap; + } + + public void setUwrap(boolean uwrap) { + this.uwrap = uwrap; + } + + public boolean isVwrap() { + return vwrap; + } + + public void setVwrap(boolean vwrap) { + this.vwrap = vwrap; + } + + public float[] getPoints() { + return points; + } + + public void setPoints(float[] points) { + this.points = points; + } + + public int getSubdivs() { + return subdivs; + } + + public void setSubdivs(int subdivs) { + this.subdivs = subdivs; + } + + public boolean isSmooth() { + return smooth; + } + + public void setSmooth(boolean smooth) { + this.smooth = smooth; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/CylinderParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/CylinderParameter.java new file mode 100644 index 0000000..6125928 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/CylinderParameter.java @@ -0,0 +1,12 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; + +public class CylinderParameter extends ObjectParameter { + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + api.geometry(name, TYPE_CYLINDER); + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/FileMeshParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/FileMeshParameter.java new file mode 100644 index 0000000..b0e1172 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/FileMeshParameter.java @@ -0,0 +1,33 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; + +public class FileMeshParameter extends ObjectParameter { + + String filename; + boolean smoothNormals = false; + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + api.parameter("filename", filename); + api.parameter("smooth_normals", smoothNormals); + api.geometry(name, TYPE_FILE_MESH); + } + + public String getFilename() { + return filename; + } + + public void setFilename(String filename) { + this.filename = filename; + } + + public boolean isSmoothNormals() { + return smoothNormals; + } + + public void setSmoothNormals(boolean smoothNormals) { + this.smoothNormals = smoothNormals; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/GenericMeshParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/GenericMeshParameter.java new file mode 100644 index 0000000..7d72a5f --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/GenericMeshParameter.java @@ -0,0 +1,97 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; + +public class GenericMeshParameter extends ObjectParameter { + + float[] points; + int[] triangles; + float[] normals; + float[] uvs; + + boolean faceVaryingNormals = false; + boolean faceVaryingTextures = false; + + int[] faceShaders = null; + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + api.parameter("points", "point", "vertex", points); + api.parameter("triangles", triangles); + + if (!faceVaryingNormals) { + api.parameter("normals", "vector", "vertex", normals); + } else { + api.parameter("normals", "vector", "facevarying", normals); + } + + if (!faceVaryingTextures) { + api.parameter("uvs", "texcoord", "vertex", uvs); + } else { + api.parameter("uvs", "texcoord", "facevarying", uvs); + } + + if (faceShaders != null) { + api.parameter("faceshaders", faceShaders); + } + + api.geometry(name, TYPE_TRIANGLE_MESH); + } + + public float[] getPoints() { + return points; + } + + public void setPoints(float[] points) { + this.points = points; + } + + public int[] getTriangles() { + return triangles; + } + + public void setTriangles(int[] triangles) { + this.triangles = triangles; + } + + public float[] getNormals() { + return normals; + } + + public void setNormals(float[] normals) { + this.normals = normals; + } + + public boolean isFaceVaryingNormals() { + return faceVaryingNormals; + } + + public void setFaceVaryingNormals(boolean faceVaryingNormals) { + this.faceVaryingNormals = faceVaryingNormals; + } + + public boolean isFaceVaryingTextures() { + return faceVaryingTextures; + } + + public void setFaceVaryingTextures(boolean faceVaryingTextures) { + this.faceVaryingTextures = faceVaryingTextures; + } + + public float[] getUvs() { + return uvs; + } + + public void setUvs(float[] uvs) { + this.uvs = uvs; + } + + public int[] getFaceShaders() { + return faceShaders; + } + + public void setFaceShaders(int[] faceShaders) { + this.faceShaders = faceShaders; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/GumboParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/GumboParameter.java new file mode 100644 index 0000000..c1a05a6 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/GumboParameter.java @@ -0,0 +1,39 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; + +public class GumboParameter extends ObjectParameter { + + int subdivs; + boolean smooth; + + public GumboParameter() { + // Default values from BezierMesh + subdivs = 8; + smooth = true; + } + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + api.parameter("subdivs", subdivs); + api.parameter("smooth", smooth); + api.geometry(name, TYPE_GUMBO); + } + + public int getSubdivs() { + return subdivs; + } + + public void setSubdivs(int subdivs) { + this.subdivs = subdivs; + } + + public boolean isSmooth() { + return smooth; + } + + public void setSmooth(boolean smooth) { + this.smooth = smooth; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/HairParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/HairParameter.java new file mode 100644 index 0000000..d09626d --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/HairParameter.java @@ -0,0 +1,43 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; + +public class HairParameter extends ObjectParameter { + + int segments; + float width; + float[] points; + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + api.parameter("segments", segments); + api.parameter("widths", width); + api.parameter("points", "point", "vertex", points); + api.geometry(name, TYPE_HAIR); + } + + public int getSegments() { + return segments; + } + + public void setSegments(int segments) { + this.segments = segments; + } + + public float getWidth() { + return width; + } + + public void setWidth(float width) { + this.width = width; + } + + public float[] getPoints() { + return points; + } + + public void setPoints(float[] points) { + this.points = points; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/JuliaParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/JuliaParameter.java new file mode 100644 index 0000000..5d568fd --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/JuliaParameter.java @@ -0,0 +1,74 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; + +public class JuliaParameter extends ObjectParameter { + + // Quaternion + float cx, cy, cz, cw; + + int iterations = 1; + float epsilon; + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + + api.parameter("cw", cw); + api.parameter("cx", cx); + api.parameter("cy", cy); + api.parameter("cz", cz); + api.parameter("iterations", iterations); + api.parameter("epsilon", epsilon); + + api.geometry(name, TYPE_JULIA); + } + + public float getCx() { + return cx; + } + + public void setCx(float cx) { + this.cx = cx; + } + + public float getCy() { + return cy; + } + + public void setCy(float cy) { + this.cy = cy; + } + + public float getCz() { + return cz; + } + + public void setCz(float cz) { + this.cz = cz; + } + + public float getCw() { + return cw; + } + + public void setCw(float cw) { + this.cw = cw; + } + + public int getIterations() { + return iterations; + } + + public void setIterations(int iterations) { + this.iterations = iterations; + } + + public float getEpsilon() { + return epsilon; + } + + public void setEpsilon(float epsilon) { + this.epsilon = epsilon; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java new file mode 100644 index 0000000..d0f36db --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java @@ -0,0 +1,61 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.parameter.InstanceParameter; +import org.sunflow.core.parameter.Parameter; + +public class ObjectParameter implements Parameter { + + public static final String TYPE_BANCHOFF = "banchoff"; + public static final String TYPE_BEZIER_MESH = "bezier_mesh"; + public static final String TYPE_CYLINDER = "cylinder"; + public static final String TYPE_GUMBO = "gumbo"; + public static final String TYPE_HAIR = "hair"; + public static final String TYPE_JULIA = "julia"; + public static final String TYPE_TORUS = "torus"; + public static final String TYPE_SPHERE = "sphere"; + public static final String TYPE_SPHEREFLAKE = "sphereflake"; + public static final String TYPE_PARTICLES = "particles"; + public static final String TYPE_PLANE = "plane"; + public static final String TYPE_TEAPOT = "teapot"; + public static final String TYPE_TRIANGLE_MESH = "triangle_mesh"; + public static final String TYPE_FILE_MESH = "file_mesh"; + + public static final String PARAM_ACCEL = "accel"; + + protected String name; + protected String accel = ""; + + protected InstanceParameter instanceParameter; + + public String getAccel() { + return accel; + } + + public void setAccel(String accel) { + this.accel = accel; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public InstanceParameter getInstanceParameter() { + return instanceParameter; + } + + public void setInstanceParameter(InstanceParameter instanceParameter) { + this.instanceParameter = instanceParameter; + } + + @Override + public void setup(SunflowAPIInterface api) { + if (!accel.isEmpty()) { + api.parameter(PARAM_ACCEL, accel); + } + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/ParticlesParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/ParticlesParameter.java new file mode 100644 index 0000000..89e312d --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/ParticlesParameter.java @@ -0,0 +1,43 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; + +public class ParticlesParameter extends ObjectParameter { + + int num; + float radius; + float[] points; + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + api.parameter("particles", "point", "vertex", points); + api.parameter("num", num); + api.parameter("radius", radius); + api.geometry(name, TYPE_PARTICLES); + } + + public int getNum() { + return num; + } + + public void setNum(int num) { + this.num = num; + } + + public float getRadius() { + return radius; + } + + public void setRadius(float radius) { + this.radius = radius; + } + + public float[] getPoints() { + return points; + } + + public void setPoints(float[] points) { + this.points = points; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/PlaneParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/PlaneParameter.java new file mode 100644 index 0000000..72391ba --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/PlaneParameter.java @@ -0,0 +1,58 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class PlaneParameter extends ObjectParameter { + + Point3 center; + Point3 point1; + Point3 point2; + Vector3 normal; + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + api.parameter("center", center); + if (normal != null) { + api.parameter("normal", normal); + } else { + api.parameter("point1", point1); + api.parameter("point2", point2); + } + api.geometry(name, TYPE_PLANE); + } + + public Point3 getCenter() { + return center; + } + + public void setCenter(Point3 center) { + this.center = center; + } + + public Point3 getPoint1() { + return point1; + } + + public void setPoint1(Point3 point1) { + this.point1 = point1; + } + + public Point3 getPoint2() { + return point2; + } + + public void setPoint2(Point3 point2) { + this.point2 = point2; + } + + public Vector3 getNormal() { + return normal; + } + + public void setNormal(Vector3 normal) { + this.normal = normal; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/SphereFlakeParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/SphereFlakeParameter.java new file mode 100644 index 0000000..87d3ec5 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/SphereFlakeParameter.java @@ -0,0 +1,59 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.math.Vector3; + +public class SphereFlakeParameter extends ObjectParameter { + + int level = 2; + float radius = 1; + Vector3 axis; + + public SphereFlakeParameter() { + // Default values from SphereFlake + level = 2; + radius = 1; + axis = new Vector3(0, 0, 1); + } + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + + api.parameter("level", level); + + if (axis != null) { + api.parameter("axis", axis); + } + + if (radius > 0) { + api.parameter("radius", radius); + } + + api.geometry(name, TYPE_SPHEREFLAKE); + } + + public int getLevel() { + return level; + } + + public void setLevel(int level) { + this.level = level; + } + + public float getRadius() { + return radius; + } + + public void setRadius(float radius) { + this.radius = radius; + } + + public Vector3 getAxis() { + return axis; + } + + public void setAxis(Vector3 axis) { + this.axis = axis; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/SphereParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/SphereParameter.java new file mode 100644 index 0000000..9d442ba --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/SphereParameter.java @@ -0,0 +1,44 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point3; + +public class SphereParameter extends ObjectParameter { + + Point3 center; + float radius; + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + api.geometry(name, TYPE_SPHERE); + api.parameter("transform", Matrix4.translation(center.x, center.y, center.z).multiply(Matrix4.scale(radius))); + + if (instanceParameter != null) { + if (instanceParameter.getShaders() != null) { + api.parameter("shaders", instanceParameter.getShaders()); + } + if (instanceParameter.getModifiers() != null) { + api.parameter("modifiers", instanceParameter.getModifiers()); + } + } + api.instance(name + ".instance", name); + } + + public Point3 getCenter() { + return center; + } + + public void setCenter(Point3 center) { + this.center = center; + } + + public float getRadius() { + return radius; + } + + public void setRadius(float radius) { + this.radius = radius; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java new file mode 100644 index 0000000..9abfc82 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java @@ -0,0 +1,33 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; + +public class TeapotParameter extends ObjectParameter { + + int subdivs = 1; + boolean smooth = false; + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + api.parameter("subdivs", subdivs); + api.parameter("smooth", smooth); + api.geometry(name, TYPE_TEAPOT); + } + + public int getSubdivs() { + return subdivs; + } + + public void setSubdivs(int subdivs) { + this.subdivs = subdivs; + } + + public boolean isSmooth() { + return smooth; + } + + public void setSmooth(boolean smooth) { + this.smooth = smooth; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/TorusParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/TorusParameter.java new file mode 100644 index 0000000..7a9afb1 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/TorusParameter.java @@ -0,0 +1,33 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; + +public class TorusParameter extends ObjectParameter { + + float radiusInner; + float radiusOuter; + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + api.parameter("radiusInner", radiusInner); + api.parameter("radiusOuter", radiusOuter); + api.geometry(name, TYPE_TORUS); + } + + public float getRadiusInner() { + return radiusInner; + } + + public void setRadiusInner(float radiusInner) { + this.radiusInner = radiusInner; + } + + public float getRadiusOuter() { + return radiusOuter; + } + + public void setRadiusOuter(float radiusOuter) { + this.radiusOuter = radiusOuter; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/TriangleMeshParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/TriangleMeshParameter.java new file mode 100644 index 0000000..0898008 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/TriangleMeshParameter.java @@ -0,0 +1,56 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; + +public class TriangleMeshParameter extends ObjectParameter { + + float[] points; + float[] normals; + float[] uvs; + int[] triangles; + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + // create geometry + api.parameter("triangles", triangles); + api.parameter("points", "point", "vertex", points); + if (normals != null) { + api.parameter("normals", "vector", "vertex", normals); + } + api.parameter("uvs", "texcoord", "vertex", uvs); + api.geometry(name, "triangle_mesh"); + } + + public float[] getPoints() { + return points; + } + + public void setPoints(float[] points) { + this.points = points; + } + + public float[] getNormals() { + return normals; + } + + public void setNormals(float[] normals) { + this.normals = normals; + } + + public float[] getUvs() { + return uvs; + } + + public void setUvs(float[] uvs) { + this.uvs = uvs; + } + + public int[] getTriangles() { + return triangles; + } + + public void setTriangles(int[] triangles) { + this.triangles = triangles; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/gi/AmbientOcclusionGIParameter.java b/src/main/java/org/sunflow/core/parameter/gi/AmbientOcclusionGIParameter.java new file mode 100644 index 0000000..c1c8d12 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/gi/AmbientOcclusionGIParameter.java @@ -0,0 +1,65 @@ +package org.sunflow.core.parameter.gi; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.image.Color; + +/** + * Global Illumination with Ambient Occlusion + */ +public class AmbientOcclusionGIParameter extends GlobalIlluminationParameter { + + public static final String PARAM_BRIGHT = "gi.ambocc.bright"; + public static final String PARAM_DARK = "gi.ambocc.dark"; + public static final String PARAM_SAMPLES = "gi.ambocc.samples"; + public static final String PARAM_MAXDIST = "gi.ambocc.maxdist"; + + Color bright; + Color dark; + int samples; + float maxDist = 0; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(GlobalIlluminationParameter.PARAM_ENGINE, GlobalIlluminationParameter.TYPE_AMBOCC); + api.parameter(PARAM_BRIGHT, null, bright.getRGB()); + api.parameter(PARAM_DARK, null, dark.getRGB()); + api.parameter(PARAM_SAMPLES, samples); + + if (maxDist > 0) { + api.parameter(PARAM_MAXDIST, maxDist); + } + super.setup(api); + } + + public Color getBright() { + return bright; + } + + public void setBright(Color bright) { + this.bright = bright; + } + + public Color getDark() { + return dark; + } + + public void setDark(Color dark) { + this.dark = dark; + } + + public int getSamples() { + return samples; + } + + public void setSamples(int samples) { + this.samples = samples; + } + + public float getMaxDist() { + return maxDist; + } + + public void setMaxDist(float maxDist) { + this.maxDist = maxDist; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/gi/DisabledGIParameter.java b/src/main/java/org/sunflow/core/parameter/gi/DisabledGIParameter.java new file mode 100644 index 0000000..23dccb1 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/gi/DisabledGIParameter.java @@ -0,0 +1,16 @@ +package org.sunflow.core.parameter.gi; + +import org.sunflow.SunflowAPIInterface; + +/** + * Disabled Global Illumination + */ +public class DisabledGIParameter extends GlobalIlluminationParameter { + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(GlobalIlluminationParameter.PARAM_ENGINE, GlobalIlluminationParameter.TYPE_NONE); + super.setup(api); + } + +} diff --git a/src/main/java/org/sunflow/core/parameter/gi/FakeGIParameter.java b/src/main/java/org/sunflow/core/parameter/gi/FakeGIParameter.java new file mode 100644 index 0000000..4d7378f --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/gi/FakeGIParameter.java @@ -0,0 +1,48 @@ +package org.sunflow.core.parameter.gi; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.image.Color; +import org.sunflow.math.Vector3; + +public class FakeGIParameter extends GlobalIlluminationParameter { + + public static final String PARAM_SKY = "gi.fake.sky"; + public static final String PARAM_GROUND = "gi.fake.ground"; + public static final String PARAM_UP = "gi.fake.up"; + Color ground; + Color sky; + Vector3 up; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(GlobalIlluminationParameter.PARAM_ENGINE, GlobalIlluminationParameter.TYPE_FAKE); + api.parameter(PARAM_SKY, null, sky.getRGB()); + api.parameter(PARAM_GROUND, null, ground.getRGB()); + api.parameter(PARAM_UP, up); + super.setup(api); + } + + public Color getGround() { + return ground; + } + + public void setGround(Color ground) { + this.ground = ground; + } + + public Color getSky() { + return sky; + } + + public void setSky(Color sky) { + this.sky = sky; + } + + public Vector3 getUp() { + return up; + } + + public void setUp(Vector3 up) { + this.up = up; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/gi/GlobalIlluminationParameter.java b/src/main/java/org/sunflow/core/parameter/gi/GlobalIlluminationParameter.java new file mode 100644 index 0000000..55c6d96 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/gi/GlobalIlluminationParameter.java @@ -0,0 +1,27 @@ +package org.sunflow.core.parameter.gi; + +import org.sunflow.SunflowAPI; +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.parameter.Parameter; + +/** + * Global Illumination (GI) + */ +public class GlobalIlluminationParameter implements Parameter { + + public static final String PARAM_ENGINE = "gi.engine"; + + public static final String TYPE_AMBOCC = "ambocc"; + public static final String TYPE_FAKE = "fake"; + public static final String TYPE_IGI = "igi"; + public static final String TYPE_IRR_CACHE = "irr-cache"; + public static final String TYPE_PATH = "path"; + + + public static final String TYPE_NONE = "none"; + + @Override + public void setup(SunflowAPIInterface api) { + api.options(SunflowAPI.DEFAULT_OPTIONS); + } +} diff --git a/src/main/java/org/sunflow/core/parameter/gi/InstantGIParameter.java b/src/main/java/org/sunflow/core/parameter/gi/InstantGIParameter.java new file mode 100644 index 0000000..21bb9be --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/gi/InstantGIParameter.java @@ -0,0 +1,61 @@ +package org.sunflow.core.parameter.gi; + +import org.sunflow.SunflowAPIInterface; + +/** + * Instant Global Illumination + */ +public class InstantGIParameter extends GlobalIlluminationParameter { + + public static final String PARAM_SAMPLES = "gi.igi.samples"; + public static final String PARAM_SETS = "gi.igi.sets"; + public static final String PARAM_BIAS = "gi.igi.bias"; + public static final String PARAM_BIAS_SAMPLES = "gi.igi.bias_samples"; + + int samples; + int sets; + float bias; + int biasSamples; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(GlobalIlluminationParameter.PARAM_ENGINE, GlobalIlluminationParameter.TYPE_IGI); + api.parameter(PARAM_SAMPLES, samples); + api.parameter(PARAM_SETS, sets); + api.parameter(PARAM_BIAS, bias); + api.parameter(PARAM_BIAS_SAMPLES, biasSamples); + super.setup(api); + } + + public int getSamples() { + return samples; + } + + public void setSamples(int samples) { + this.samples = samples; + } + + public int getSets() { + return sets; + } + + public void setSets(int sets) { + this.sets = sets; + } + + public float getBias() { + return bias; + } + + public void setBias(float bias) { + this.bias = bias; + } + + public int getBiasSamples() { + return biasSamples; + } + + public void setBiasSamples(int biasSamples) { + this.biasSamples = biasSamples; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/gi/IrrCacheGIParameter.java b/src/main/java/org/sunflow/core/parameter/gi/IrrCacheGIParameter.java new file mode 100644 index 0000000..235fb7e --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/gi/IrrCacheGIParameter.java @@ -0,0 +1,83 @@ +package org.sunflow.core.parameter.gi; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.parameter.IlluminationParameter; + +/** + * Global Illumination Irradiance Cache + */ +public class IrrCacheGIParameter extends GlobalIlluminationParameter { + + public static final String PARAM_TOLERANCE = "gi.irr-cache.tolerance"; + public static final String PARAM_SAMPLES = "gi.irr-cache.samples"; + public static final String PARAM_MIN_SPACING = "gi.irr-cache.min_spacing"; + public static final String PARAM_MAX_SPACING = "gi.irr-cache.max_spacing"; + public static final String PARAM_GLOBAL_EMIT = "gi.irr-cache.gmap.emit"; + public static final String PARAM_GLOBAL = "gi.irr-cache.gmap"; + public static final String PARAM_GLOBAL_GATHER = "gi.irr-cache.gmap.gather"; + public static final String PARAM_GLOBAL_RADIUS = "gi.irr-cache.gmap.radius"; + + int samples = 0; + float tolerance; + float minSpacing; + float maxSpacing; + + IlluminationParameter global = null; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(GlobalIlluminationParameter.PARAM_ENGINE, GlobalIlluminationParameter.TYPE_IRR_CACHE); + api.parameter(PARAM_SAMPLES, samples); + api.parameter(PARAM_TOLERANCE, tolerance); + api.parameter(PARAM_MIN_SPACING, minSpacing); + api.parameter(PARAM_MAX_SPACING, maxSpacing); + + if (global != null) { + api.parameter(PARAM_GLOBAL_EMIT, global.getEmit()); + api.parameter(PARAM_GLOBAL, global.getMap()); + api.parameter(PARAM_GLOBAL_GATHER, global.getGather()); + api.parameter(PARAM_GLOBAL_RADIUS, global.getRadius()); + } + super.setup(api); + } + + public int getSamples() { + return samples; + } + + public void setSamples(int samples) { + this.samples = samples; + } + + public float getTolerance() { + return tolerance; + } + + public void setTolerance(float tolerance) { + this.tolerance = tolerance; + } + + public float getMinSpacing() { + return minSpacing; + } + + public void setMinSpacing(float minSpacing) { + this.minSpacing = minSpacing; + } + + public float getMaxSpacing() { + return maxSpacing; + } + + public void setMaxSpacing(float maxSpacing) { + this.maxSpacing = maxSpacing; + } + + public IlluminationParameter getGlobal() { + return global; + } + + public void setGlobal(IlluminationParameter global) { + this.global = global; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/gi/PathTracingGIParameter.java b/src/main/java/org/sunflow/core/parameter/gi/PathTracingGIParameter.java new file mode 100644 index 0000000..0847acb --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/gi/PathTracingGIParameter.java @@ -0,0 +1,25 @@ +package org.sunflow.core.parameter.gi; + +import org.sunflow.SunflowAPIInterface; + +public class PathTracingGIParameter extends GlobalIlluminationParameter { + + public static final String PARAM_SAMPLES = "gi.path.samples"; + + int samples; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(GlobalIlluminationParameter.PARAM_ENGINE, GlobalIlluminationParameter.TYPE_PATH); + api.parameter(PARAM_SAMPLES, samples); + super.setup(api); + } + + public int getSamples() { + return samples; + } + + public void setSamples(int samples) { + this.samples = samples; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/light/CornellBoxLightParameter.java b/src/main/java/org/sunflow/core/parameter/light/CornellBoxLightParameter.java new file mode 100644 index 0000000..81d00b4 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/light/CornellBoxLightParameter.java @@ -0,0 +1,109 @@ +package org.sunflow.core.parameter.light; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.image.Color; +import org.sunflow.math.Point3; + +public class CornellBoxLightParameter extends LightParameter { + + public static final String PARAM_MIN_CORNER = "corner0"; + public static final String PARAM_MAX_CORNER = "corner1"; + public static final String PARAM_LEFT_COLOR = "leftColor"; + public static final String PARAM_RIGHT_COLOR = "rightColor"; + public static final String PARAM_TOP_COLOR = "topColor"; + public static final String PARAM_BOTTOM_COLOR = "bottomColor"; + public static final String PARAM_BACK_COLOR = "backColor"; + + int samples; + Point3 min; + Point3 max; + Color left, right, top, bottom, back; + Color radiance; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(PARAM_MIN_CORNER, min); + api.parameter(PARAM_MAX_CORNER, max); + api.parameter(PARAM_LEFT_COLOR, null, left.getRGB()); + api.parameter(PARAM_RIGHT_COLOR, null, right.getRGB()); + api.parameter(PARAM_TOP_COLOR, null, top.getRGB()); + api.parameter(PARAM_BOTTOM_COLOR, null, bottom.getRGB()); + api.parameter(PARAM_BACK_COLOR, null, back.getRGB()); + api.parameter(PARAM_RADIANCE, null, radiance.getRGB()); + api.parameter(PARAM_SAMPLES, samples); + + api.light(name, TYPE_CORNELL_BOX); + } + + public Point3 getMin() { + return min; + } + + public void setMin(Point3 min) { + this.min = min; + } + + public Point3 getMax() { + return max; + } + + public void setMax(Point3 max) { + this.max = max; + } + + public Color getLeft() { + return left; + } + + public void setLeft(Color left) { + this.left = left; + } + + public Color getRight() { + return right; + } + + public void setRight(Color right) { + this.right = right; + } + + public Color getTop() { + return top; + } + + public void setTop(Color top) { + this.top = top; + } + + public Color getBottom() { + return bottom; + } + + public void setBottom(Color bottom) { + this.bottom = bottom; + } + + public Color getBack() { + return back; + } + + public void setBack(Color back) { + this.back = back; + } + + public Color getRadiance() { + return radiance; + } + + public void setRadiance(Color radiance) { + this.radiance = radiance; + } + + public int getSamples() { + return samples; + } + + public void setSamples(int samples) { + this.samples = samples; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/light/DirectionalLightParameter.java b/src/main/java/org/sunflow/core/parameter/light/DirectionalLightParameter.java new file mode 100644 index 0000000..3a0977f --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/light/DirectionalLightParameter.java @@ -0,0 +1,67 @@ +package org.sunflow.core.parameter.light; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.light.DirectionalSpotlight; +import org.sunflow.image.Color; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class DirectionalLightParameter extends LightParameter { + + public static final String PARAM_SOURCE = "source"; + public static final String PARAM_DIRECTION = "dir"; + public static final String PARAM_RADIUS = "radius"; + + DirectionalSpotlight light; + + public DirectionalLightParameter() { + light = new DirectionalSpotlight(); + } + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(PARAM_SOURCE, light.getSource()); + api.parameter(PARAM_DIRECTION, light.getDirection()); + api.parameter(PARAM_RADIUS, light.getR()); + api.parameter(PARAM_RADIANCE, null, light.getRadiance().getRGB()); + api.light(name, LightParameter.TYPE_DIRECTIONAL); + } + + public Point3 getSource() { + return light.getSource(); + } + + public void setSource(Point3 source) { + light.setSource(source); + } + + public Vector3 getDirection() { + return light.getDirection(); + } + + public void setDirection(Point3 target) { + Vector3 direction = Point3.sub(target, light.getSource(), new Vector3()); + light.setDirection(direction); + } + + public void setDirection(Vector3 direction) { + light.setDirection(direction); + } + + public float getRadius() { + return light.getR(); + } + + public void setRadius(float r) { + light.setR(r); + } + + public Color getRadiance() { + return light.getRadiance(); + } + + public void setRadiance(Color radiance) { + light.setRadiance(radiance); + } + +} diff --git a/src/main/java/org/sunflow/core/parameter/light/ImageBasedLightParameter.java b/src/main/java/org/sunflow/core/parameter/light/ImageBasedLightParameter.java new file mode 100644 index 0000000..a5ff5b7 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/light/ImageBasedLightParameter.java @@ -0,0 +1,82 @@ +package org.sunflow.core.parameter.light; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.math.Vector3; + +public class ImageBasedLightParameter extends LightParameter { + + public static final String PARAM_CENTER = "center"; + public static final String PARAM_UP = "up"; + public static final String PARAM_FIXED = "fixed"; + public static final String PARAM_TEXTURE = "texture"; + public static final String PARAM_LOW_SAMPLES = "lowsamples"; + int samples; + int lowSamples = 0; + + String texture = ""; + Vector3 center; + Vector3 up; + boolean fixed; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(PARAM_TEXTURE, texture); + api.parameter(PARAM_CENTER, center); + api.parameter(PARAM_UP, up); + api.parameter(PARAM_FIXED, fixed); + api.parameter(PARAM_SAMPLES, samples); + + if (lowSamples == 0) { + api.parameter(PARAM_LOW_SAMPLES, samples); + } + api.light(name, TYPE_IMAGE_BASED); + } + + public String getTexture() { + return texture; + } + + public void setTexture(String texture) { + this.texture = texture; + } + + public Vector3 getCenter() { + return center; + } + + public void setCenter(Vector3 center) { + this.center = center; + } + + public Vector3 getUp() { + return up; + } + + public void setUp(Vector3 up) { + this.up = up; + } + + public boolean isFixed() { + return fixed; + } + + public void setFixed(boolean fixed) { + this.fixed = fixed; + } + + public int getSamples() { + return samples; + } + + public void setSamples(int samples) { + this.samples = samples; + } + + public int getLowSamples() { + return lowSamples; + } + + public void setLowSamples(int lowSamples) { + this.lowSamples = lowSamples; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/light/LightParameter.java b/src/main/java/org/sunflow/core/parameter/light/LightParameter.java new file mode 100644 index 0000000..8ded393 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/light/LightParameter.java @@ -0,0 +1,27 @@ +package org.sunflow.core.parameter.light; + +import org.sunflow.core.parameter.Parameter; + +public abstract class LightParameter implements Parameter { + + public static final String PARAM_RADIANCE = "radiance"; + public static final String PARAM_SAMPLES = "samples"; + + public static final String TYPE_CORNELL_BOX = "cornell_box"; + public static final String TYPE_DIRECTIONAL = "directional"; + public static final String TYPE_IMAGE_BASED = "ibl"; + public static final String TYPE_POINTLIGHT = "point"; + public static final String TYPE_SPHERE = "sphere"; + public static final String TYPE_SUNSKY = "sunsky"; + public static final String TYPE_TRIANGLE_MESH = "triangle_mesh"; + + protected String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/light/PointLightParameter.java b/src/main/java/org/sunflow/core/parameter/light/PointLightParameter.java new file mode 100644 index 0000000..e714f6b --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/light/PointLightParameter.java @@ -0,0 +1,41 @@ +package org.sunflow.core.parameter.light; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.light.PointLight; +import org.sunflow.image.Color; +import org.sunflow.math.Point3; + +public class PointLightParameter extends LightParameter { + + public static final String PARAM_CENTER = "center"; + public static final String PARAM_POWER = "power"; + private PointLight light; + + public PointLightParameter() { + light = new PointLight(); + } + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(PARAM_CENTER, getCenter()); + api.parameter(PARAM_POWER, null, getColor().getRGB()); + api.light(name, LightParameter.TYPE_POINTLIGHT); + } + + public Point3 getCenter() { + return light.getLightPoint(); + } + + public void setCenter(Point3 lightPoint) { + light.setLightPoint(lightPoint); + } + + public Color getColor() { + return light.getColor(); + } + + public void setColor(Color color) { + light.setColor(color); + } + +} diff --git a/src/main/java/org/sunflow/core/parameter/light/SphereLightParameter.java b/src/main/java/org/sunflow/core/parameter/light/SphereLightParameter.java new file mode 100644 index 0000000..d5830d6 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/light/SphereLightParameter.java @@ -0,0 +1,67 @@ +package org.sunflow.core.parameter.light; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.light.SphereLight; +import org.sunflow.image.Color; +import org.sunflow.math.Point3; + +public class SphereLightParameter extends LightParameter { + + public static final String PARAM_CENTER = "center"; + public static final String PARAM_RADIUS = "radius"; + private SphereLight light; + + public SphereLightParameter() { + light = new SphereLight(); + } + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(LightParameter.PARAM_RADIANCE, null, light.getRadiance().getRGB()); + api.parameter(PARAM_CENTER, light.getCenter()); + api.parameter(PARAM_RADIUS, light.getRadius()); + api.parameter(LightParameter.PARAM_SAMPLES, light.getNumSamples()); + api.light(name, LightParameter.TYPE_SPHERE); + } + + public Color getRadiance() { + return light.getRadiance(); + } + + public void setRadiance(Color radiance) { + light.setRadiance(radiance); + } + + public int getSamples() { + return light.getNumSamples(); + } + + public void setSamples(int numSamples) { + light.setNumSamples(numSamples); + } + + public Point3 getCenter() { + return light.getCenter(); + } + + public void setCenter(Point3 center) { + light.setCenter(center); + } + + public float getRadius() { + return light.getRadius(); + } + + public void setRadius(float radius) { + light.setRadius(radius); + } + + public float getR2() { + return light.getR2(); + } + + public void setR2(float r2) { + light.setR2(r2); + } + +} diff --git a/src/main/java/org/sunflow/core/parameter/light/SunSkyLightParameter.java b/src/main/java/org/sunflow/core/parameter/light/SunSkyLightParameter.java new file mode 100644 index 0000000..79e29f4 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/light/SunSkyLightParameter.java @@ -0,0 +1,96 @@ +package org.sunflow.core.parameter.light; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.image.Color; +import org.sunflow.math.Vector3; + +public class SunSkyLightParameter extends LightParameter { + + public static final String PARAM_TURBIDITY = "turbidity"; + public static final String PARAM_SUN_DIRECTION = "sundir"; + public static final String PARAM_EAST = "east"; + public static final String PARAM_UP = "up"; + public static final String PARAM_GROUND_EXTENDSKY = "ground.extendsky"; + public static final String PARAM_GROUND_COLOR = "ground.color"; + Vector3 up; + Vector3 east; + Vector3 sunDirection; + + float turbidity; + int samples; + boolean extendSky = false; + + Color groundColor = null; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(PARAM_UP, up); + api.parameter(PARAM_EAST, east); + api.parameter(PARAM_SUN_DIRECTION, sunDirection); + api.parameter(PARAM_TURBIDITY, turbidity); + api.parameter(LightParameter.PARAM_SAMPLES, samples); + api.parameter(PARAM_GROUND_EXTENDSKY, extendSky); + + if (groundColor != null) { + api.parameter(PARAM_GROUND_COLOR, null, groundColor.getRGB()); + } + + api.light(name, TYPE_SUNSKY); + } + + public Vector3 getUp() { + return up; + } + + public void setUp(Vector3 up) { + this.up = up; + } + + public Vector3 getEast() { + return east; + } + + public void setEast(Vector3 east) { + this.east = east; + } + + public Vector3 getSunDirection() { + return sunDirection; + } + + public void setSunDirection(Vector3 sunDirection) { + this.sunDirection = sunDirection; + } + + public float getTurbidity() { + return turbidity; + } + + public void setTurbidity(float turbidity) { + this.turbidity = turbidity; + } + + public int getSamples() { + return samples; + } + + public void setSamples(int samples) { + this.samples = samples; + } + + public boolean isExtendSky() { + return extendSky; + } + + public void setExtendSky(boolean extendSky) { + this.extendSky = extendSky; + } + + public Color getGroundColor() { + return groundColor; + } + + public void setGroundColor(Color groundColor) { + this.groundColor = groundColor; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/light/TriangleMeshLightParameter.java b/src/main/java/org/sunflow/core/parameter/light/TriangleMeshLightParameter.java new file mode 100644 index 0000000..5833e1b --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/light/TriangleMeshLightParameter.java @@ -0,0 +1,56 @@ +package org.sunflow.core.parameter.light; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.image.Color; + +public class TriangleMeshLightParameter extends LightParameter { + + public static final String PARAM_POINTS = "points"; + public static final String PARAM_TRIANGLES = "triangles"; + + int samples; + Color radiance; + float[] points; + int[] triangles; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter(PARAM_RADIANCE, null, radiance.getRGB()); + api.parameter(PARAM_SAMPLES, samples); + api.parameter(PARAM_POINTS, "point", "vertex", points); + api.parameter(PARAM_TRIANGLES, triangles); + api.light(name, TYPE_TRIANGLE_MESH); + } + + public Color getRadiance() { + return radiance; + } + + public void setRadiance(Color radiance) { + this.radiance = radiance; + } + + public int getSamples() { + return samples; + } + + public void setSamples(int samples) { + this.samples = samples; + } + + public float[] getPoints() { + return points; + } + + public void setPoints(float[] points) { + this.points = points; + } + + public int[] getTriangles() { + return triangles; + } + + public void setTriangles(int[] triangles) { + this.triangles = triangles; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/modifier/BumpMapModifierParameter.java b/src/main/java/org/sunflow/core/parameter/modifier/BumpMapModifierParameter.java new file mode 100644 index 0000000..62d7a0d --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/modifier/BumpMapModifierParameter.java @@ -0,0 +1,32 @@ +package org.sunflow.core.parameter.modifier; + +import org.sunflow.SunflowAPIInterface; + +public class BumpMapModifierParameter extends ModifierParameter { + + float scale; + String texture = ""; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter("texture", texture); + api.parameter("scale", scale); + api.modifier(name, TYPE_BUMP_MAP); + } + + public String getTexture() { + return texture; + } + + public void setTexture(String texture) { + this.texture = texture; + } + + public float getScale() { + return scale; + } + + public void setScale(float scale) { + this.scale = scale; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/modifier/ModifierParameter.java b/src/main/java/org/sunflow/core/parameter/modifier/ModifierParameter.java new file mode 100644 index 0000000..c55e4c6 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/modifier/ModifierParameter.java @@ -0,0 +1,20 @@ +package org.sunflow.core.parameter.modifier; + +import org.sunflow.core.parameter.Parameter; + +public abstract class ModifierParameter implements Parameter { + + public static final String TYPE_BUMP_MAP = "bump_map"; + public static final String TYPE_NORMAL_MAP = "normal_map"; + public static final String TYPE_PERLIN = "perlin"; + + protected String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/modifier/NormalMapModifierParameter.java b/src/main/java/org/sunflow/core/parameter/modifier/NormalMapModifierParameter.java new file mode 100644 index 0000000..1a4e62c --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/modifier/NormalMapModifierParameter.java @@ -0,0 +1,22 @@ +package org.sunflow.core.parameter.modifier; + +import org.sunflow.SunflowAPIInterface; + +public class NormalMapModifierParameter extends ModifierParameter { + + String texture = ""; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter("texture", texture); + api.modifier(name, TYPE_NORMAL_MAP); + } + + public String getTexture() { + return texture; + } + + public void setTexture(String texture) { + this.texture = texture; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/modifier/PerlinModifierParameter.java b/src/main/java/org/sunflow/core/parameter/modifier/PerlinModifierParameter.java new file mode 100644 index 0000000..42f69e0 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/modifier/PerlinModifierParameter.java @@ -0,0 +1,42 @@ +package org.sunflow.core.parameter.modifier; + +import org.sunflow.SunflowAPIInterface; + +public class PerlinModifierParameter extends ModifierParameter { + + int function; + float size; + float scale; + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter("function", function); + api.parameter("size", size); + api.parameter("scale", scale); + api.modifier(name, TYPE_PERLIN); + } + + public int getFunction() { + return function; + } + + public void setFunction(int function) { + this.function = function; + } + + public float getSize() { + return size; + } + + public void setSize(float size) { + this.size = size; + } + + public float getScale() { + return scale; + } + + public void setScale(float scale) { + this.scale = scale; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/shader/AmbientOcclusionShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/AmbientOcclusionShaderParameter.java new file mode 100644 index 0000000..f1c812b --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/shader/AmbientOcclusionShaderParameter.java @@ -0,0 +1,80 @@ +package org.sunflow.core.parameter.shader; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.image.Color; + +public class AmbientOcclusionShaderParameter extends ShaderParameter { + + String texture = ""; + Color bright; + Color dark = null; + int samples = 1; + float maxDist = 1; + + public AmbientOcclusionShaderParameter(String name) { + super(name); + // Default values from AmbientOcclusionShader + bright = Color.WHITE; + dark = Color.BLACK; + samples = 32; + maxDist = Float.POSITIVE_INFINITY; + } + + @Override + public void setup(SunflowAPIInterface api) { + + if(dark!=null) { + api.parameter("dark", null, dark.getRGB()); + api.parameter("samples", samples); + api.parameter("maxdist", maxDist); + } + + if(texture.isEmpty()) { + api.parameter("bright", null, bright.getRGB()); + api.shader(name, TYPE_AMBIENT_OCCLUSION); + } else { + api.parameter("texture", texture); + api.shader(name, TYPE_TEXTURED_AMBIENT_OCCLUSION); + } + } + + public String getTexture() { + return texture; + } + + public void setTexture(String texture) { + this.texture = texture; + } + + public Color getBright() { + return bright; + } + + public void setBright(Color bright) { + this.bright = bright; + } + + public Color getDark() { + return dark; + } + + public void setDark(Color dark) { + this.dark = dark; + } + + public int getSamples() { + return samples; + } + + public void setSamples(int samples) { + this.samples = samples; + } + + public float getMaxDist() { + return maxDist; + } + + public void setMaxDist(float maxDist) { + this.maxDist = maxDist; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/shader/ConstantShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/ConstantShaderParameter.java new file mode 100644 index 0000000..bbae705 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/shader/ConstantShaderParameter.java @@ -0,0 +1,28 @@ +package org.sunflow.core.parameter.shader; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.image.Color; + +public class ConstantShaderParameter extends ShaderParameter { + + private Color color; + + public ConstantShaderParameter(String name) { + super(name); + color = Color.WHITE; + } + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter("color", null, color.getRGB()); + api.shader(name, TYPE_CONSTANT); + } + + public Color getColor() { + return color; + } + + public void setColor(Color color) { + this.color = color; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/shader/DiffuseShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/DiffuseShaderParameter.java new file mode 100644 index 0000000..7eaaf0f --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/shader/DiffuseShaderParameter.java @@ -0,0 +1,42 @@ +package org.sunflow.core.parameter.shader; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.image.Color; + +public class DiffuseShaderParameter extends ShaderParameter { + + String texture = ""; + Color diffuse; + + public DiffuseShaderParameter(String name) { + super(name); + diffuse = Color.WHITE; + } + + @Override + public void setup(SunflowAPIInterface api) { + if (texture.isEmpty()) { + api.parameter("diffuse", null, diffuse.getRGB()); + api.shader(name, TYPE_DIFFUSE); + } else { + api.parameter("texture", texture); + api.shader(name, TYPE_TEXTURED_DIFFUSE); + } + } + + public Color getDiffuse() { + return diffuse; + } + + public void setDiffuse(Color diffuse) { + this.diffuse = diffuse; + } + + public String getTexture() { + return texture; + } + + public void setTexture(String texture) { + this.texture = texture; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/shader/GlassShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/GlassShaderParameter.java new file mode 100644 index 0000000..462a08c --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/shader/GlassShaderParameter.java @@ -0,0 +1,62 @@ +package org.sunflow.core.parameter.shader; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.image.Color; + +public class GlassShaderParameter extends ShaderParameter { + + float eta; + Color color; + float absorptionDistance; + Color absorptionColor; + + public GlassShaderParameter(String name) { + super(name); + // Default values from GlassShader + eta = 1.3f; + color = Color.WHITE; + absorptionDistance = 0; // disabled by default + absorptionColor = Color.GRAY; // 50% absorbtion + } + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter("eta", eta); + api.parameter("color", null, color.getRGB()); + api.parameter("absorption.distance", absorptionDistance); + api.parameter("absorption.color", null, absorptionColor.getRGB()); + api.shader(name, TYPE_GLASS); + } + + public float getEta() { + return eta; + } + + public void setEta(float eta) { + this.eta = eta; + } + + public Color getColor() { + return color; + } + + public void setColor(Color color) { + this.color = color; + } + + public float getAbsorptionDistance() { + return absorptionDistance; + } + + public void setAbsorptionDistance(float absorptionDistance) { + this.absorptionDistance = absorptionDistance; + } + + public Color getAbsorptionColor() { + return absorptionColor; + } + + public void setAbsorptionColor(Color absorptionColor) { + this.absorptionColor = absorptionColor; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/shader/IDShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/IDShaderParameter.java new file mode 100644 index 0000000..1a357b6 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/shader/IDShaderParameter.java @@ -0,0 +1,15 @@ +package org.sunflow.core.parameter.shader; + +import org.sunflow.SunflowAPIInterface; + +public class IDShaderParameter extends ShaderParameter { + + public IDShaderParameter(String name) { + super(name); + } + + @Override + public void setup(SunflowAPIInterface api) { + api.shader(name, TYPE_SHOW_INSTANCE_ID); + } +} diff --git a/src/main/java/org/sunflow/core/parameter/shader/MirrorShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/MirrorShaderParameter.java new file mode 100644 index 0000000..493cda0 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/shader/MirrorShaderParameter.java @@ -0,0 +1,28 @@ +package org.sunflow.core.parameter.shader; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.image.Color; + +public class MirrorShaderParameter extends ShaderParameter { + + private Color color; + + public MirrorShaderParameter(String name) { + super(name); + color = Color.WHITE; + } + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter("color", null, color.getRGB()); + api.shader(name, TYPE_MIRROR); + } + + public Color getColor() { + return color; + } + + public void setReflection(Color color) { + this.color = color; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/shader/PhongShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/PhongShaderParameter.java new file mode 100644 index 0000000..6e7efda --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/shader/PhongShaderParameter.java @@ -0,0 +1,77 @@ +package org.sunflow.core.parameter.shader; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.image.Color; + +public class PhongShaderParameter extends ShaderParameter { + + String texture = ""; + Color diffuse; + Color specular; + float power; + int samples; + + public PhongShaderParameter(String name) { + super(name); + diffuse = Color.GRAY; + specular = Color.GRAY; + power = 20; + // Number of Rays + samples = 4; + } + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter("specular", null, specular.getRGB()); + api.parameter("power", power); + api.parameter("samples", samples); + + if (texture.isEmpty()) { + api.parameter("diffuse", null, diffuse.getRGB()); + api.shader(name, TYPE_PHONG); + } else { + api.parameter("texture", texture); + api.shader(name, TYPE_TEXTURED_PHONG); + } + } + + public Color getDiffuse() { + return diffuse; + } + + public void setDiffuse(Color diffuse) { + this.diffuse = diffuse; + } + + public String getTexture() { + return texture; + } + + public void setTexture(String texture) { + this.texture = texture; + } + + public Color getSpecular() { + return specular; + } + + public void setSpecular(Color specular) { + this.specular = specular; + } + + public float getPower() { + return power; + } + + public void setPower(float power) { + this.power = power; + } + + public int getSamples() { + return samples; + } + + public void setSamples(int samples) { + this.samples = samples; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/shader/ShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/ShaderParameter.java new file mode 100644 index 0000000..464e467 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/shader/ShaderParameter.java @@ -0,0 +1,46 @@ +package org.sunflow.core.parameter.shader; + +import org.sunflow.core.parameter.Parameter; + +public abstract class ShaderParameter implements Parameter { + + public static final String TYPE_AMBIENT_OCCLUSION = "ambient_occlusion"; + public static final String TYPE_TEXTURED_AMBIENT_OCCLUSION = "textured_ambient_occlusion"; + public static final String TYPE_CONSTANT = "constant"; + public static final String TYPE_DIFFUSE = "diffuse"; + public static final String TYPE_TEXTURED_DIFFUSE = "textured_diffuse"; + public static final String TYPE_GLASS = "glass"; + public static final String TYPE_MIRROR = "mirror"; + public static final String TYPE_PHONG = "phong"; + public static final String TYPE_TEXTURED_PHONG = "textured_phong"; + public static final String TYPE_SHINY_DIFFUSE = "shiny_diffuse"; + public static final String TYPE_TEXTURED_SHINY_DIFFUSE = "textured_shiny_diffuse"; + public static final String TYPE_UBER = "uber"; + public static final String TYPE_WARD = "ward"; + public static final String TYPE_SHOW_INSTANCE_ID = "show_instance_id"; + public static final String TYPE_TEXTURED_WARD = "textured_ward"; + public static final String TYPE_VIEW_CAUSTICS = "view_caustics"; + public static final String TYPE_VIEW_IRRADIANCE = "view_irradiance"; + public static final String TYPE_VIEW_GLOBAL = "view_global"; + public static final String TYPE_NONE = "none"; + + protected String name; + + public ShaderParameter() { + super(); + } + + public ShaderParameter(String name) { + super(); + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/src/main/java/org/sunflow/core/parameter/shader/ShinyShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/ShinyShaderParameter.java new file mode 100644 index 0000000..e76df6d --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/shader/ShinyShaderParameter.java @@ -0,0 +1,54 @@ +package org.sunflow.core.parameter.shader; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.image.Color; + +public class ShinyShaderParameter extends ShaderParameter { + + String texture = ""; + Color diffuse; + float shiny; + + public ShinyShaderParameter(String name) { + super(name); + diffuse = Color.GRAY; + shiny = 0.5f; + } + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter("shiny", shiny); + + if (texture.isEmpty()) { + api.parameter("diffuse", null, diffuse.getRGB()); + api.shader(name, TYPE_SHINY_DIFFUSE); + } else { + api.parameter("texture", texture); + api.shader(name, TYPE_TEXTURED_SHINY_DIFFUSE); + } + } + + public String getTexture() { + return texture; + } + + public void setTexture(String texture) { + this.texture = texture; + } + + public Color getDiffuse() { + return diffuse; + } + + public void setDiffuse(Color diffuse) { + this.diffuse = diffuse; + } + + public float getShiny() { + return shiny; + } + + public void setShiny(float shiny) { + this.shiny = shiny; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/shader/UberShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/UberShaderParameter.java new file mode 100644 index 0000000..790948e --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/shader/UberShaderParameter.java @@ -0,0 +1,111 @@ +package org.sunflow.core.parameter.shader; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.image.Color; + +public class UberShaderParameter extends ShaderParameter { + + Color diffuse; + float diffuseBlend; + String diffuseTexture; + + Color specular; + float specularBlend; + String specularTexture; + + int samples; + float glossyness; + + + public UberShaderParameter(String name) { + super(name); + // Default values from UberShader + diffuse = specular = Color.GRAY; + diffuseTexture = specularTexture = ""; + diffuseBlend = specularBlend = 1; + glossyness = 0; + samples = 4; + } + + @Override + public void setup(SunflowAPIInterface api) { + api.parameter("diffuse", null, diffuse.getRGB()); + if (!diffuseTexture.isEmpty()) { + api.parameter("diffuse.texture", diffuseTexture); + } + api.parameter("diffuse.blend", diffuseBlend); + api.parameter("specular", null, specular.getRGB()); + if (!specularTexture.isEmpty()) { + api.parameter("specular.texture", specularTexture); + } + api.parameter("specular.blend", specularBlend); + api.parameter("glossyness", glossyness); + api.parameter("samples", samples); + + api.shader(name, TYPE_UBER); + } + + public Color getDiffuse() { + return diffuse; + } + + public void setDiffuse(Color diffuse) { + this.diffuse = diffuse; + } + + public float getDiffuseBlend() { + return diffuseBlend; + } + + public void setDiffuseBlend(float diffuseBlend) { + this.diffuseBlend = diffuseBlend; + } + + public String getDiffuseTexture() { + return diffuseTexture; + } + + public void setDiffuseTexture(String diffuseTexture) { + this.diffuseTexture = diffuseTexture; + } + + public Color getSpecular() { + return specular; + } + + public void setSpecular(Color specular) { + this.specular = specular; + } + + public String getSpecularTexture() { + return specularTexture; + } + + public void setSpecularTexture(String specularTexture) { + this.specularTexture = specularTexture; + } + + public float getSpecularBlend() { + return specularBlend; + } + + public void setSpecularBlend(float specularBlend) { + this.specularBlend = specularBlend; + } + + public int getSamples() { + return samples; + } + + public void setSamples(int samples) { + this.samples = samples; + } + + public float getGlossyness() { + return glossyness; + } + + public void setGlossyness(float glossyness) { + this.glossyness = glossyness; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/shader/ViewCausticsShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/ViewCausticsShaderParameter.java new file mode 100644 index 0000000..7a8b9a3 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/shader/ViewCausticsShaderParameter.java @@ -0,0 +1,15 @@ +package org.sunflow.core.parameter.shader; + +import org.sunflow.SunflowAPIInterface; + +public class ViewCausticsShaderParameter extends ShaderParameter { + + public ViewCausticsShaderParameter(String name) { + super(name); + } + + @Override + public void setup(SunflowAPIInterface api) { + api.shader(name, TYPE_VIEW_CAUSTICS); + } +} diff --git a/src/main/java/org/sunflow/core/parameter/shader/ViewGlobalShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/ViewGlobalShaderParameter.java new file mode 100644 index 0000000..319cea9 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/shader/ViewGlobalShaderParameter.java @@ -0,0 +1,15 @@ +package org.sunflow.core.parameter.shader; + +import org.sunflow.SunflowAPIInterface; + +public class ViewGlobalShaderParameter extends ShaderParameter { + + public ViewGlobalShaderParameter(String name) { + super(name); + } + + @Override + public void setup(SunflowAPIInterface api) { + api.shader(name, TYPE_VIEW_GLOBAL); + } +} diff --git a/src/main/java/org/sunflow/core/parameter/shader/ViewIrradianceShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/ViewIrradianceShaderParameter.java new file mode 100644 index 0000000..0cbf97f --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/shader/ViewIrradianceShaderParameter.java @@ -0,0 +1,15 @@ +package org.sunflow.core.parameter.shader; + +import org.sunflow.SunflowAPIInterface; + +public class ViewIrradianceShaderParameter extends ShaderParameter { + + public ViewIrradianceShaderParameter(String name) { + super(name); + } + + @Override + public void setup(SunflowAPIInterface api) { + api.shader(name, TYPE_VIEW_IRRADIANCE); + } +} diff --git a/src/main/java/org/sunflow/core/parameter/shader/WardShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/WardShaderParameter.java new file mode 100644 index 0000000..dd62b37 --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/shader/WardShaderParameter.java @@ -0,0 +1,84 @@ +package org.sunflow.core.parameter.shader; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.image.Color; + +public class WardShaderParameter extends ShaderParameter { + + + String texture = ""; + Color diffuse; + Color specular; + + int samples; + float roughnessX, roughnessY; + + public WardShaderParameter(String name) { + super(name); + } + + @Override + public void setup(SunflowAPIInterface api) { + + api.parameter("specular", null, specular.getRGB()); + api.parameter("roughnessX", roughnessX); + api.parameter("roughnessY", roughnessY); + api.parameter("samples", samples); + + if (texture.isEmpty()) { + api.parameter("diffuse", null, diffuse.getRGB()); + api.shader(name, TYPE_WARD); + } else { + api.parameter("texture", texture); + api.shader(name, TYPE_TEXTURED_WARD); + } + } + + public String getTexture() { + return texture; + } + + public void setTexture(String texture) { + this.texture = texture; + } + + public Color getDiffuse() { + return diffuse; + } + + public void setDiffuse(Color diffuse) { + this.diffuse = diffuse; + } + + public Color getSpecular() { + return specular; + } + + public void setSpecular(Color specular) { + this.specular = specular; + } + + public float getRoughnessX() { + return roughnessX; + } + + public void setRoughnessX(float roughnessX) { + this.roughnessX = roughnessX; + } + + public float getRoughnessY() { + return roughnessY; + } + + public void setRoughnessY(float roughnessY) { + this.roughnessY = roughnessY; + } + + public int getSamples() { + return samples; + } + + public void setSamples(int samples) { + this.samples = samples; + } +} diff --git a/src/main/java/org/sunflow/core/parser/SCNewParser.java b/src/main/java/org/sunflow/core/parser/SCNewParser.java new file mode 100644 index 0000000..cf28874 --- /dev/null +++ b/src/main/java/org/sunflow/core/parser/SCNewParser.java @@ -0,0 +1,1594 @@ +package org.sunflow.core.parser; + +import org.sunflow.PluginRegistry; +import org.sunflow.SunflowAPI; +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.SceneParser; +import org.sunflow.core.parameter.*; +import org.sunflow.core.parameter.camera.*; +import org.sunflow.core.parameter.geometry.*; +import org.sunflow.core.parameter.gi.*; +import org.sunflow.core.parameter.light.*; +import org.sunflow.core.parameter.modifier.BumpMapModifierParameter; +import org.sunflow.core.parameter.modifier.NormalMapModifierParameter; +import org.sunflow.core.parameter.modifier.PerlinModifierParameter; +import org.sunflow.core.parameter.shader.*; +import org.sunflow.image.Color; +import org.sunflow.image.ColorFactory; +import org.sunflow.image.ColorFactory.ColorSpecificationException; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.Parser; +import org.sunflow.system.Parser.ParserException; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.HashMap; + +/** + * This class provides a static method for loading files in the Sunflow scene + * file format. + */ +public class SCNewParser implements SceneParser { + private static int instanceCounter = 0; + private int instanceNumber; + private Parser p; + private int numLightSamples; + // used to generate unique names inside this parser + private HashMap objectNames; + + public SCNewParser() { + objectNames = new HashMap(); + instanceCounter++; + instanceNumber = instanceCounter; + } + + private String generateUniqueName(String prefix) { + // generate a unique name for this class: + int index = 1; + Integer value = objectNames.get(prefix); + if (value != null) { + index = value; + objectNames.put(prefix, index + 1); + } else { + objectNames.put(prefix, index + 1); + } + return String.format("@sc_%d::%s_%d", instanceNumber, prefix, index); + } + + public boolean parse(String filename, SunflowAPIInterface api) { + String localDir = new File(filename).getAbsoluteFile().getParentFile().getAbsolutePath(); + numLightSamples = 1; + Timer timer = new Timer(); + timer.start(); + UI.printInfo(Module.API, "Parsing \"%s\" ...", filename); + try { + p = new Parser(filename); + while (true) { + String token = p.getNextToken(); + if (token == null) + break; + if (token.equals("image")) { + UI.printInfo(Module.API, "Reading image settings ..."); + parseImageBlock(api); + } else if (token.equals("background")) { + UI.printInfo(Module.API, "Reading background ..."); + parseBackgroundBlock(api); + } else if (token.equals("accel")) { + UI.printInfo(Module.API, "Reading accelerator type ..."); + p.getNextToken(); + UI.printWarning(Module.API, "Setting accelerator type is not recommended - ignoring"); + } else if (token.equals("filter")) { + // Deprecated + UI.printInfo(Module.API, "Reading image filter type ..."); + parseFilter(api); + } else if (token.equals("bucket")) { + UI.printInfo(Module.API, "Reading bucket settings ..."); + parseBucketBlock(api); + } else if (token.equals("photons")) { + UI.printInfo(Module.API, "Reading photon settings ..."); + parsePhotonBlock(api); + } else if (token.equals("gi")) { + UI.printInfo(Module.API, "Reading global illumination settings ..."); + parseGIBlock(api); + } else if (token.equals("lightserver")) { + // Deprecated + UI.printInfo(Module.API, "Reading light server settings ..."); + parseLightserverBlock(api); + } else if (token.equals("trace-depths")) { + UI.printInfo(Module.API, "Reading trace depths ..."); + parseTraceBlock(api); + } else if (token.equals("camera")) { + parseCamera(api); + } else if (token.equals("shader")) { + if (!parseShader(api)) { + // Close before return + p.close(); + return false; + } + } else if (token.equals("modifier")) { + if (!parseModifier(api)) { + // Close before return + p.close(); + return false; + } + } else if (token.equals("override")) { + parseOverrideBlock(api); + } else if (token.equals("object")) { + parseObjectBlock(api); + } else if (token.equals("instance")) { + parseInstanceBlock(api); + } else if (token.equals("light")) { + parseLightBlock(api); + } else if (token.equals("texturepath")) { + String path = p.getNextToken(); + if (!new File(path).isAbsolute()) { + path = localDir + File.separator + path; + } + api.searchpath("texture", path); + } else if (token.equals("includepath")) { + String path = p.getNextToken(); + if (!new File(path).isAbsolute()) { + path = localDir + File.separator + path; + } + api.searchpath("include", path); + } else if (token.equals("include")) { + String file = p.getNextToken(); + UI.printInfo(Module.API, "Including: \"%s\" ...", file); + api.include(file); + } else + UI.printWarning(Module.API, "Unrecognized token %s", token); + } + p.close(); + } catch (ParserException e) { + UI.printError(Module.API, "%s", e.getMessage()); + e.printStackTrace(); + return false; + } catch (FileNotFoundException e) { + UI.printError(Module.API, "%s", e.getMessage()); + return false; + } catch (IOException e) { + UI.printError(Module.API, "%s", e.getMessage()); + return false; + } catch (ColorSpecificationException e) { + UI.printError(Module.API, "%s", e.getMessage()); + return false; + } + timer.end(); + UI.printInfo(Module.API, "Done parsing."); + UI.printInfo(Module.API, "Parsing time: %s", timer.toString()); + return true; + } + + private void parseBucketBlock(SunflowAPIInterface api) throws IOException { + BucketParameter bucket = new BucketParameter(); + bucket.setSize(p.getNextInt()); + bucket.setOrder(p.getNextToken()); + bucket.setup(api); + } + + private void parseImageBlock(SunflowAPIInterface api) throws IOException, ParserException { + ImageParameter image = new ImageParameter(); + + p.checkNextToken("{"); + if (p.peekNextToken("resolution")) { + image.setResolutionX(p.getNextInt()); + image.setResolutionY(p.getNextInt()); + } + if (p.peekNextToken("sampler")) + image.setSampler(p.getNextToken()); + if (p.peekNextToken("aa")) { + image.setAAMin(p.getNextInt()); + image.setAAMax(p.getNextInt()); + } + if (p.peekNextToken("samples")) { + image.setAASamples(p.getNextInt()); + } + if (p.peekNextToken("contrast")) { + image.setAAContrast(p.getNextFloat()); + } + if (p.peekNextToken("filter")) { + image.setFilter(p.getNextToken()); + } + if (p.peekNextToken("jitter")) { + image.setAAJitter(p.getNextBoolean()); + } + if (p.peekNextToken("show-aa")) { + UI.printWarning(Module.API, "Deprecated: show-aa ignored"); + p.getNextBoolean(); + } + if (p.peekNextToken("cache")) { + image.setAACache(p.getNextBoolean()); + } + if (p.peekNextToken("output")) { + UI.printWarning(Module.API, "Deprecated: output statement ignored"); + p.getNextToken(); + } + + image.setup(api); + p.checkNextToken("}"); + } + + private void parseBackgroundBlock(SunflowAPIInterface api) throws IOException, ParserException, ColorSpecificationException { + BackgroundParameter background = new BackgroundParameter(); + + p.checkNextToken("{"); + p.checkNextToken("color"); + + background.setColor(parseColor()); + background.setup(api); + + p.checkNextToken("}"); + } + + private void parseFilter(SunflowAPIInterface api) throws IOException, ParserException { + UI.printWarning(Module.API, "Deprecated keyword \"filter\" - set this option in the image block"); + String name = p.getNextToken(); + api.parameter("filter", name); + api.options(SunflowAPI.DEFAULT_OPTIONS); + boolean hasSizeParams = name.equals("box") || name.equals("gaussian") || name.equals("blackman-harris") || name.equals("sinc") || name.equals("triangle"); + if (hasSizeParams) { + p.getNextFloat(); + p.getNextFloat(); + } + } + + private void parsePhotonBlock(SunflowAPIInterface api) throws ParserException, IOException { + PhotonParameter photon = new PhotonParameter(); + + boolean globalEmit = false; + + p.checkNextToken("{"); + if (p.peekNextToken("emit")) { + UI.printWarning(Module.API, "Shared photon emit values are deprecated - specify number of photons to emit per map"); + photon.setNumEmit(p.getNextInt()); + globalEmit = true; + } + if (p.peekNextToken("global")) { + UI.printWarning(Module.API, "Global photon map setting belongs inside the gi block - ignoring"); + if (!globalEmit) { + p.getNextInt(); + } + p.getNextToken(); + p.getNextInt(); + p.getNextFloat(); + } + p.checkNextToken("caustics"); + if (!globalEmit) { + photon.setNumEmit(p.getNextInt()); + } + + photon.setCaustics(p.getNextToken()); + photon.setCausticsGather(p.getNextInt()); + photon.setCausticsRadius(p.getNextFloat()); + + photon.setup(api); + p.checkNextToken("}"); + } + + private void parseGIBlock(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { + p.checkNextToken("{"); + p.checkNextToken("type"); + if (p.peekNextToken("irr-cache")) { + IrrCacheGIParameter gi = new IrrCacheGIParameter(); + + p.checkNextToken("samples"); + gi.setSamples(p.getNextInt()); + p.checkNextToken("tolerance"); + gi.setTolerance(p.getNextFloat()); + p.checkNextToken("spacing"); + gi.setMinSpacing(p.getNextFloat()); + gi.setMaxSpacing(p.getNextFloat()); + + // parse global photon map info + if (p.peekNextToken("global")) { + gi.setGlobal(new IlluminationParameter()); + gi.getGlobal().setEmit(p.getNextInt()); + gi.getGlobal().setMap(p.getNextToken()); + gi.getGlobal().setGather(p.getNextInt()); + gi.getGlobal().setRadius(p.getNextFloat()); + } + } else if (p.peekNextToken("path")) { + PathTracingGIParameter gi = new PathTracingGIParameter(); + + p.checkNextToken("samples"); + gi.setSamples(p.getNextInt()); + + if (p.peekNextToken("bounces")) { + UI.printWarning(Module.API, "Deprecated setting: bounces - use diffuse trace depth instead"); + p.getNextInt(); + } + + gi.setup(api); + } else if (p.peekNextToken("fake")) { + FakeGIParameter gi = new FakeGIParameter(); + + p.checkNextToken("up"); + gi.setUp(parseVector()); + p.checkNextToken("sky"); + gi.setSky(parseColor()); + p.checkNextToken("ground"); + gi.setGround(parseColor()); + + gi.setup(api); + } else if (p.peekNextToken("igi")) { + InstantGIParameter gi = new InstantGIParameter(); + + p.checkNextToken("samples"); + gi.setSamples(p.getNextInt()); + p.checkNextToken("sets"); + gi.setSets(p.getNextInt()); + + if (!p.peekNextToken("b")) { + p.checkNextToken("c"); + } + gi.setBias(p.getNextFloat()); + + p.checkNextToken("bias-samples"); + gi.setBiasSamples(p.getNextInt()); + + gi.setup(api); + } else if (p.peekNextToken("ambocc")) { + AmbientOcclusionGIParameter gi = new AmbientOcclusionGIParameter(); + + p.checkNextToken("bright"); + gi.setBright(parseColor()); + p.checkNextToken("dark"); + gi.setDark(parseColor()); + p.checkNextToken("samples"); + gi.setSamples(p.getNextInt()); + if (p.peekNextToken("maxdist")) { + gi.setMaxDist(p.getNextFloat()); + } + + gi.setup(api); + } else if (p.peekNextToken("none") || p.peekNextToken("null")) { + DisabledGIParameter gi = new DisabledGIParameter(); + // disable GI + gi.setup(api); + } else { + UI.printWarning(Module.API, "Unrecognized gi engine type \"%s\" - ignoring", p.getNextToken()); + } + api.options(SunflowAPI.DEFAULT_OPTIONS); + p.checkNextToken("}"); + } + + private void parseOverrideBlock(SunflowAPIInterface api) throws IOException { + OverrideParameter block = new OverrideParameter(); + + block.setShader(p.getNextToken()); + block.setPhotons(p.getNextBoolean()); + block.setup(api); + } + + private void parseLightserverBlock(SunflowAPIInterface api) throws ParserException, IOException { + p.checkNextToken("{"); + if (p.peekNextToken("shadows")) { + UI.printWarning(Module.API, "Deprecated: shadows setting ignored"); + p.getNextBoolean(); + } + if (p.peekNextToken("direct-samples")) { + UI.printWarning(Module.API, "Deprecated: use samples keyword in area light definitions"); + numLightSamples = p.getNextInt(); + } + if (p.peekNextToken("glossy-samples")) { + UI.printWarning(Module.API, "Deprecated: use samples keyword in glossy shader definitions"); + p.getNextInt(); + } + if (p.peekNextToken("max-depth")) { + UI.printWarning(Module.API, "Deprecated: max-depth setting - use trace-depths block instead"); + int d = p.getNextInt(); + api.parameter("depths.diffuse", 1); + api.parameter("depths.reflection", d - 1); + api.parameter("depths.refraction", 0); + api.options(SunflowAPI.DEFAULT_OPTIONS); + } + if (p.peekNextToken("global")) { + UI.printWarning(Module.API, "Deprecated: global settings ignored - use photons block instead"); + p.getNextBoolean(); + p.getNextInt(); + p.getNextInt(); + p.getNextInt(); + p.getNextFloat(); + } + if (p.peekNextToken("caustics")) { + UI.printWarning(Module.API, "Deprecated: caustics settings ignored - use photons block instead"); + p.getNextBoolean(); + p.getNextInt(); + p.getNextFloat(); + p.getNextInt(); + p.getNextFloat(); + } + if (p.peekNextToken("irr-cache")) { + UI.printWarning(Module.API, "Deprecated: irradiance cache settings ignored - use gi block instead"); + p.getNextInt(); + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + } + p.checkNextToken("}"); + } + + private void parseTraceBlock(SunflowAPIInterface api) throws ParserException, IOException { + TraceDepthsParameter traceDepthsParameter = new TraceDepthsParameter(); + p.checkNextToken("{"); + if (p.peekNextToken("diff")) { + traceDepthsParameter.setDiffuse(p.getNextInt()); + } + if (p.peekNextToken("refl")) { + traceDepthsParameter.setReflection(p.getNextInt()); + } + if (p.peekNextToken("refr")) { + traceDepthsParameter.setRefraction(p.getNextInt()); + } + p.checkNextToken("}"); + + traceDepthsParameter.setup(api); + api.options(SunflowAPI.DEFAULT_OPTIONS); + } + + private void parseCamera(SunflowAPIInterface api) throws ParserException, IOException { + p.checkNextToken("{"); + p.checkNextToken("type"); + String type = p.getNextToken(); + UI.printInfo(Module.API, "Reading %s camera ...", type); + + float shutterOpen = 0, shutterClose = 0; + + if (p.peekNextToken("shutter")) { + shutterOpen = p.getNextFloat(); + shutterClose = p.getNextFloat(); + } + parseCameraTransform(api); + String name = generateUniqueName("camera"); + if (type.equals(CameraParameter.TYPE_PINHOLE)) { + PinholeCameraParameter camera = new PinholeCameraParameter(); + camera.setName(name); + camera.setShutterOpen(shutterOpen); + camera.setShutterClose(shutterClose); + + p.checkNextToken("fov"); + camera.setFov(p.getNextFloat()); + p.checkNextToken("aspect"); + camera.setAspect(p.getNextFloat()); + + if (p.peekNextToken("shift")) { + camera.setShiftX(p.getNextFloat()); + camera.setShiftY(p.getNextFloat()); + } + camera.setup(api); + } else if (type.equals(CameraParameter.TYPE_THINLENS)) { + ThinLensCameraParameter camera = new ThinLensCameraParameter(); + camera.setName(name); + camera.setShutterOpen(shutterOpen); + camera.setShutterClose(shutterClose); + + p.checkNextToken("fov"); + camera.setFov(p.getNextFloat()); + p.checkNextToken("aspect"); + camera.setAspect(p.getNextFloat()); + if (p.peekNextToken("shift")) { + camera.setShiftX(p.getNextFloat()); + camera.setShiftY(p.getNextFloat()); + } + p.checkNextToken("fdist"); + camera.setFocusDistance(p.getNextFloat()); + p.checkNextToken("lensr"); + camera.setLensRadius(p.getNextFloat()); + if (p.peekNextToken("sides")) { + camera.setLensSides(p.getNextInt()); + } + if (p.peekNextToken("rotation")) { + camera.setLensRotation(p.getNextFloat()); + } + camera.setup(api); + } else if (type.equals(CameraParameter.TYPE_SPHERICAL)) { + SphericalCameraParameter camera = new SphericalCameraParameter(); + camera.setName(name); + camera.setShutterOpen(shutterOpen); + camera.setShutterClose(shutterClose); + // no extra arguments + camera.setup(api); + } else if (type.equals(CameraParameter.TYPE_FISH_EYE)) { + FishEyeCameraParameter camera = new FishEyeCameraParameter(); + camera.setName(name); + camera.setShutterOpen(shutterOpen); + camera.setShutterClose(shutterClose); + // no extra arguments + camera.setup(api); + } else { + UI.printWarning(Module.API, "Unrecognized camera type: %s", p.getNextToken()); + p.checkNextToken("}"); + return; + } + p.checkNextToken("}"); + /*if (name != null) { + api.parameter("camera", name); + api.options(SunflowAPI.DEFAULT_OPTIONS); + }*/ + } + + private void parseCameraTransform(SunflowAPIInterface api) throws ParserException, IOException { + if (p.peekNextToken("steps")) { + // motion blur camera + int n = p.getNextInt(); + api.parameter("transform.steps", n); + // parse time extents + p.checkNextToken("times"); + float t0 = p.getNextFloat(); + float t1 = p.getNextFloat(); + api.parameter("transform.times", "float", "none", new float[]{t0, t1}); + for (int i = 0; i < n; i++) { + parseCameraMatrix(i, api); + } + } else { + parseCameraMatrix(-1, api); + } + } + + private void parseCameraMatrix(int index, SunflowAPIInterface api) throws IOException, ParserException { + String offset = index < 0 ? "" : String.format("[%d]", index); + if (p.peekNextToken("transform")) { + // advanced camera + api.parameter(String.format("transform%s", offset), parseMatrix()); + } else { + if (index >= 0) { + p.checkNextToken("{"); + } + // regular camera specification + p.checkNextToken("eye"); + Point3 eye = parsePoint(); + p.checkNextToken("target"); + Point3 target = parsePoint(); + p.checkNextToken("up"); + Vector3 up = parseVector(); + + // TODO Move logic to camera + api.parameter(String.format("transform%s", offset), Matrix4.lookAt(eye, target, up)); + if (index >= 0) { + p.checkNextToken("}"); + } + } + } + + private boolean parseShader(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { + p.checkNextToken("{"); + p.checkNextToken("name"); + String name = p.getNextToken(); + UI.printInfo(Module.API, "Reading shader: %s ...", name); + p.checkNextToken("type"); + if (p.peekNextToken("diffuse")) { + DiffuseShaderParameter shader = new DiffuseShaderParameter(name); + + if (p.peekNextToken("diff")) { + shader.setDiffuse(parseColor()); + } else if (p.peekNextToken("texture")) { + shader.setTexture(p.getNextToken()); + } else { + UI.printWarning(Module.API, "Unrecognized option in diffuse shader block: %s", p.getNextToken()); + } + shader.setup(api); + } else if (p.peekNextToken("phong")) { + PhongShaderParameter shaderParameter = new PhongShaderParameter(name); + if (p.peekNextToken("texture")) { + shaderParameter.setTexture(p.getNextToken()); + } else { + p.checkNextToken("diff"); + shaderParameter.setDiffuse(parseColor()); + } + p.checkNextToken("spec"); + shaderParameter.setSpecular(parseColor()); + shaderParameter.setPower(p.getNextFloat()); + + if (p.peekNextToken("samples")) { + shaderParameter.setSamples(p.getNextInt()); + } + shaderParameter.setup(api); + } else if (p.peekNextToken("amb-occ") || p.peekNextToken("amb-occ2")) { + AmbientOcclusionShaderParameter shaderParameter = new AmbientOcclusionShaderParameter(name); + + if (p.peekNextToken("diff") || p.peekNextToken("bright")) { + shaderParameter.setBright(parseColor()); + } else if (p.peekNextToken("texture")) { + shaderParameter.setTexture(p.getNextToken()); + } + + if (p.peekNextToken("dark")) { + shaderParameter.setDark(parseColor()); + p.checkNextToken("samples"); + shaderParameter.setSamples(p.getNextInt()); + p.checkNextToken("dist"); + shaderParameter.setMaxDist(p.getNextFloat()); + } + shaderParameter.setup(api); + } else if (p.peekNextToken("mirror")) { + MirrorShaderParameter shaderParameter = new MirrorShaderParameter(name); + p.checkNextToken("refl"); + shaderParameter.setReflection(parseColor()); + shaderParameter.setup(api); + } else if (p.peekNextToken("glass")) { + GlassShaderParameter shaderParameter = new GlassShaderParameter(name); + + p.checkNextToken("eta"); + shaderParameter.setEta(p.getNextFloat()); + p.checkNextToken("color"); + shaderParameter.setColor(parseColor()); + + if (p.peekNextToken("absorption.distance") || p.peekNextToken("absorbtion.distance")) { + shaderParameter.setAbsorptionDistance(p.getNextFloat()); + } + if (p.peekNextToken("absorption.color") || p.peekNextToken("absorbtion.color")) { + shaderParameter.setAbsorptionColor(parseColor()); + } + shaderParameter.setup(api); + } else if (p.peekNextToken("shiny")) { + ShinyShaderParameter shaderParameter = new ShinyShaderParameter(name); + + if (p.peekNextToken("texture")) { + shaderParameter.setTexture(p.getNextToken()); + } else { + p.checkNextToken("diff"); + shaderParameter.setDiffuse(parseColor()); + } + p.checkNextToken("refl"); + shaderParameter.setShiny(p.getNextFloat()); + shaderParameter.setup(api); + } else if (p.peekNextToken("ward")) { + WardShaderParameter shaderParameter = new WardShaderParameter(name); + + if (p.peekNextToken("texture")) { + shaderParameter.setTexture(p.getNextToken()); + } else { + p.checkNextToken("diff"); + shaderParameter.setDiffuse(parseColor()); + } + p.checkNextToken("spec"); + shaderParameter.setSpecular(parseColor()); + + p.checkNextToken("rough"); + shaderParameter.setRoughnessX(p.getNextFloat()); + shaderParameter.setRoughnessY(p.getNextFloat()); + + if (p.peekNextToken("samples")) { + shaderParameter.setSamples(p.getNextInt()); + } + shaderParameter.setup(api); + } else if (p.peekNextToken("view-caustics")) { + ViewCausticsShaderParameter shaderParameter = new ViewCausticsShaderParameter(name); + shaderParameter.setup(api); + } else if (p.peekNextToken("view-irradiance")) { + ViewIrradianceShaderParameter shaderParameter = new ViewIrradianceShaderParameter(name); + shaderParameter.setup(api); + } else if (p.peekNextToken("view-global")) { + ViewGlobalShaderParameter shaderParameter = new ViewGlobalShaderParameter(name); + shaderParameter.setup(api); + } else if (p.peekNextToken("constant")) { + ConstantShaderParameter shaderParameter = new ConstantShaderParameter(name); + // backwards compatibility -- peek only + p.peekNextToken("color"); + shaderParameter.setColor(parseColor()); + shaderParameter.setup(api); + } else if (p.peekNextToken("janino")) { + String typename = p.peekNextToken("typename") ? p.getNextToken() : PluginRegistry.shaderPlugins.generateUniqueName("janino_shader"); + if (!PluginRegistry.shaderPlugins.registerPlugin(typename, p.getNextCodeBlock())) + return false; + api.shader(name, typename); + } else if (p.peekNextToken("id")) { + IDShaderParameter shaderParameter = new IDShaderParameter(name); + shaderParameter.setup(api); + } else if (p.peekNextToken("uber")) { + UberShaderParameter shaderParameter = new UberShaderParameter(name); + + if (p.peekNextToken("diff")) { + shaderParameter.setDiffuse(parseColor()); + } + if (p.peekNextToken("diff.texture")) { + shaderParameter.setDiffuseTexture(p.getNextToken()); + } + if (p.peekNextToken("diff.blend")) { + shaderParameter.setDiffuseBlend(p.getNextFloat()); + } + if (p.peekNextToken("refl") || p.peekNextToken("spec")) { + shaderParameter.setSpecular(parseColor()); + } + if (p.peekNextToken("texture")) { + // deprecated + UI.printWarning(Module.API, "Deprecated uber shader parameter \"texture\" - please use \"diffuse.texture\" and \"diffuse.blend\" instead"); + shaderParameter.setDiffuseTexture(p.getNextToken()); + shaderParameter.setDiffuseBlend(p.getNextFloat()); + } + if (p.peekNextToken("spec.texture")) { + shaderParameter.setSpecularTexture(p.getNextToken()); + } + if (p.peekNextToken("spec.blend")) { + shaderParameter.setSpecularBlend(p.getNextFloat()); + } + if (p.peekNextToken("glossy")) { + shaderParameter.setGlossyness(p.getNextFloat()); + } + if (p.peekNextToken("samples")) { + shaderParameter.setSamples(p.getNextInt()); + } + shaderParameter.setup(api); + } else { + UI.printWarning(Module.API, "Unrecognized shader type: %s", p.getNextToken()); + } + p.checkNextToken("}"); + return true; + } + + private boolean parseModifier(SunflowAPIInterface api) throws ParserException, IOException { + p.checkNextToken("{"); + p.checkNextToken("name"); + String name = p.getNextToken(); + UI.printInfo(Module.API, "Reading modifier: %s ...", name); + p.checkNextToken("type"); + if (p.peekNextToken("bump")) { + BumpMapModifierParameter parameter = new BumpMapModifierParameter(); + parameter.setName(name); + p.checkNextToken("texture"); + parameter.setTexture(p.getNextToken()); + p.checkNextToken("scale"); + parameter.setScale(p.getNextFloat()); + parameter.setup(api); + } else if (p.peekNextToken("normalmap")) { + NormalMapModifierParameter parameter = new NormalMapModifierParameter(); + parameter.setName(name); + p.checkNextToken("texture"); + parameter.setTexture(p.getNextToken()); + parameter.setup(api); + } else if (p.peekNextToken("perlin")) { + PerlinModifierParameter parameter = new PerlinModifierParameter(); + p.checkNextToken("function"); + parameter.setFunction(p.getNextInt()); + p.checkNextToken("size"); + parameter.setSize(p.getNextFloat()); + p.checkNextToken("scale"); + parameter.setScale(p.getNextFloat()); + parameter.setup(api); + } else { + UI.printWarning(Module.API, "Unrecognized modifier type: %s", p.getNextToken()); + } + p.checkNextToken("}"); + return true; + } + + private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, IOException { + p.checkNextToken("{"); + InstanceParameter instanceParameter = new InstanceParameter(); + + String name = ""; + String accel = ""; + + boolean noInstance = false; + //Matrix4[] transform = null; + //float transformTime0 = 0, transformTime1 = 0; + String[] shaders = null; + String[] modifiers = null; + if (p.peekNextToken("noinstance")) { + // this indicates that the geometry is to be created, but not + // instanced into the scene + noInstance = true; + } else { + // these are the parameters to be passed to the instance + if (p.peekNextToken("shaders")) { + int n = p.getNextInt(); + shaders = new String[n]; + for (int i = 0; i < n; i++) + shaders[i] = p.getNextToken(); + } else { + p.checkNextToken("shader"); + shaders = new String[]{p.getNextToken()}; + } + instanceParameter.setShaders(shaders); + + if (p.peekNextToken("modifiers")) { + int n = p.getNextInt(); + modifiers = new String[n]; + for (int i = 0; i < n; i++) + modifiers[i] = p.getNextToken(); + } else if (p.peekNextToken("modifier")) { + modifiers = new String[]{p.getNextToken()}; + } + instanceParameter.setModifiers(modifiers); + + // Can be null + TransformParameter transform = checkParseTransform(); + instanceParameter.setTransform(transform); + } + if (p.peekNextToken("accel")) { + accel = p.getNextToken(); + } + p.checkNextToken("type"); + String type = p.getNextToken(); + if (p.peekNextToken("name")) { + name = p.getNextToken(); + } else { + name = generateUniqueName(type); + } + + if (type.equals("mesh")) { + UI.printWarning(Module.API, "Deprecated object type: mesh"); + UI.printInfo(Module.API, "Reading mesh: %s ...", name); + + TriangleMeshParameter geometry = new TriangleMeshParameter(); + geometry.setName(name); + geometry.setAccel(accel); + geometry.setInstanceParameter(instanceParameter); + + int numVertices = p.getNextInt(); + int numTriangles = p.getNextInt(); + float[] points = new float[numVertices * 3]; + float[] normals = new float[numVertices * 3]; + float[] uvs = new float[numVertices * 2]; + for (int i = 0; i < numVertices; i++) { + p.checkNextToken("v"); + points[3 * i + 0] = p.getNextFloat(); + points[3 * i + 1] = p.getNextFloat(); + points[3 * i + 2] = p.getNextFloat(); + normals[3 * i + 0] = p.getNextFloat(); + normals[3 * i + 1] = p.getNextFloat(); + normals[3 * i + 2] = p.getNextFloat(); + uvs[2 * i + 0] = p.getNextFloat(); + uvs[2 * i + 1] = p.getNextFloat(); + } + int[] triangles = new int[numTriangles * 3]; + for (int i = 0; i < numTriangles; i++) { + p.checkNextToken("t"); + triangles[i * 3 + 0] = p.getNextInt(); + triangles[i * 3 + 1] = p.getNextInt(); + triangles[i * 3 + 2] = p.getNextInt(); + } + + geometry.setPoints(points); + geometry.setNormals(normals); + geometry.setUvs(uvs); + geometry.setTriangles(triangles); + geometry.setup(api); + } else if (type.equals("flat-mesh")) { + UI.printWarning(Module.API, "Deprecated object type: flat-mesh"); + UI.printInfo(Module.API, "Reading flat mesh: %s ...", name); + + TriangleMeshParameter geometry = new TriangleMeshParameter(); + geometry.setName(name); + geometry.setAccel(accel); + geometry.setInstanceParameter(instanceParameter); + + int numVertices = p.getNextInt(); + int numTriangles = p.getNextInt(); + float[] points = new float[numVertices * 3]; + float[] uvs = new float[numVertices * 2]; + for (int i = 0; i < numVertices; i++) { + p.checkNextToken("v"); + points[3 * i + 0] = p.getNextFloat(); + points[3 * i + 1] = p.getNextFloat(); + points[3 * i + 2] = p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + uvs[2 * i + 0] = p.getNextFloat(); + uvs[2 * i + 1] = p.getNextFloat(); + } + int[] triangles = new int[numTriangles * 3]; + for (int i = 0; i < numTriangles; i++) { + p.checkNextToken("t"); + triangles[i * 3 + 0] = p.getNextInt(); + triangles[i * 3 + 1] = p.getNextInt(); + triangles[i * 3 + 2] = p.getNextInt(); + } + + geometry.setPoints(points); + geometry.setUvs(uvs); + geometry.setTriangles(triangles); + geometry.setup(api); + } else if (type.equals("sphere")) { + UI.printInfo(Module.API, "Reading sphere ..."); + instanceParameter.setName(name + ".instance"); + instanceParameter.setGeometry(name); + + SphereParameter geometry = new SphereParameter(); + geometry.setName(name); + geometry.setInstanceParameter(instanceParameter); + + if (instanceParameter.getTransform() == null && !noInstance) { + // legacy method of specifying transformation for spheres + p.checkNextToken("c"); + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + geometry.setCenter(new Point3(x, y, z)); + p.checkNextToken("r"); + float radius = p.getNextFloat(); + geometry.setRadius(radius); + + geometry.setup(api); + // disable future instancing - instance has already been created + noInstance = true; + } + } else if (type.equals("cylinder")) { + UI.printInfo(Module.API, "Reading cylinder ..."); + + CylinderParameter parameter = new CylinderParameter(); + parameter.setName(name); + parameter.setInstanceParameter(instanceParameter); + + parameter.setup(api); + } else if (type.equals("banchoff")) { + UI.printInfo(Module.API, "Reading banchoff ..."); + + BanchOffParameter parameter = new BanchOffParameter(); + parameter.setName(name); + parameter.setInstanceParameter(instanceParameter); + + parameter.setup(api); + } else if (type.equals("torus")) { + UI.printInfo(Module.API, "Reading torus ..."); + + TorusParameter geometry = new TorusParameter(); + geometry.setName(name); + geometry.setInstanceParameter(instanceParameter); + + p.checkNextToken("r"); + geometry.setRadiusInner(p.getNextFloat()); + geometry.setRadiusOuter(p.getNextFloat()); + geometry.setup(api); + } else if (type.equals("sphereflake")) { + UI.printInfo(Module.API, "Reading sphereflake ..."); + SphereFlakeParameter geometry = new SphereFlakeParameter(); + geometry.setName(name); + + if (p.peekNextToken("level")) { + geometry.setLevel(p.getNextInt()); + } + if (p.peekNextToken("axis")) { + geometry.setAxis(parseVector()); + } + if (p.peekNextToken("radius")) { + geometry.setRadius(p.getNextFloat()); + } + geometry.setup(api); + } else if (type.equals("plane")) { + UI.printInfo(Module.API, "Reading plane ..."); + PlaneParameter geometry = new PlaneParameter(); + geometry.setName(name); + p.checkNextToken("p"); + geometry.setCenter(parsePoint()); + if (p.peekNextToken("n")) { + geometry.setNormal(parseVector()); + } else { + p.checkNextToken("p"); + geometry.setPoint1(parsePoint()); + p.checkNextToken("p"); + geometry.setPoint2(parsePoint()); + } + geometry.setup(api); + } else if (type.equals("generic-mesh")) { + UI.printInfo(Module.API, "Reading generic mesh: %s ... ", name); + GenericMeshParameter geometry = new GenericMeshParameter(); + // parse vertices + p.checkNextToken("points"); + int np = p.getNextInt(); + float[] points = parseFloatArray(np * 3); + geometry.setPoints(points); + // parse triangle indices + p.checkNextToken("triangles"); + int nt = p.getNextInt(); + geometry.setTriangles(parseIntArray(nt * 3)); + // parse normals + p.checkNextToken("normals"); + if (p.peekNextToken("vertex")) { + geometry.setNormals(parseFloatArray(np * 3)); + } else if (p.peekNextToken("facevarying")) { + geometry.setFaceVaryingNormals(true); + geometry.setNormals(parseFloatArray(nt * 9)); + } else { + p.checkNextToken("none"); + } + + // parse texture coordinates + p.checkNextToken("uvs"); + if (p.peekNextToken("vertex")) { + geometry.setUvs(parseFloatArray(np * 2)); + } else if (p.peekNextToken("facevarying")) { + geometry.setFaceVaryingTextures(true); + geometry.setUvs(parseFloatArray(nt * 6)); + } else { + p.checkNextToken("none"); + } + if (p.peekNextToken("face_shaders")) { + geometry.setFaceShaders(parseIntArray(nt)); + } + geometry.setup(api); + } else if (type.equals("hair")) { + UI.printInfo(Module.API, "Reading hair curves: %s ... ", name); + HairParameter geometry = new HairParameter(); + p.checkNextToken("segments"); + geometry.setSegments(p.getNextInt()); + p.checkNextToken("width"); + geometry.setWidth(p.getNextFloat()); + p.checkNextToken("points"); + geometry.setPoints(parseFloatArray(p.getNextInt())); + geometry.setup(api); + } else if (type.equals("janino-tesselatable")) { + UI.printInfo(Module.API, "Reading procedural primitive: %s ... ", name); + String typename = p.peekNextToken("typename") ? p.getNextToken() : PluginRegistry.shaderPlugins.generateUniqueName("janino_shader"); + if (!PluginRegistry.tesselatablePlugins.registerPlugin(typename, p.getNextCodeBlock())) { + noInstance = true; + } else { + api.geometry(name, typename); + } + } else if (type.equals("teapot")) { + UI.printInfo(Module.API, "Reading teapot: %s ... ", name); + TeapotParameter geometry = new TeapotParameter(); + geometry.setName(name); + + if (p.peekNextToken("subdivs")) { + geometry.setSubdivs(p.getNextInt()); + } + if (p.peekNextToken("smooth")) { + geometry.setSmooth(p.getNextBoolean()); + } + geometry.setup(api); + } else if (type.equals("gumbo")) { + UI.printInfo(Module.API, "Reading gumbo: %s ... ", name); + GumboParameter geometry = new GumboParameter(); + geometry.setName(name); + + if (p.peekNextToken("subdivs")) { + geometry.setSubdivs(p.getNextInt()); + } + if (p.peekNextToken("smooth")) { + geometry.setSmooth(p.getNextBoolean()); + } + geometry.setup(api); + } else if (type.equals("julia")) { + UI.printInfo(Module.API, "Reading julia fractal: %s ... ", name); + JuliaParameter geometry = new JuliaParameter(); + geometry.setName(name); + if (p.peekNextToken("q")) { + geometry.setCw(p.getNextFloat()); + geometry.setCx(p.getNextFloat()); + geometry.setCy(p.getNextFloat()); + geometry.setCz(p.getNextFloat()); + } + if (p.peekNextToken("iterations")) { + geometry.setIterations(p.getNextInt()); + } + if (p.peekNextToken("epsilon")) { + geometry.setEpsilon(p.getNextFloat()); + } + geometry.setup(api); + } else if (type.equals("particles") || type.equals("dlasurface")) { + ParticlesParameter geometry = new ParticlesParameter(); + geometry.setName(name); + if (type.equals("dlasurface")) { + UI.printWarning(Module.API, "Deprecated object type: \"dlasurface\" - please use \"particles\" instead"); + } + float[] data; + if (p.peekNextToken("filename")) { + // FIXME: this code should be moved into an on demand loading + // primitive + String filename = p.getNextToken(); + boolean littleEndian = false; + if (p.peekNextToken("little_endian")) { + littleEndian = true; + } + UI.printInfo(Module.USER, "Loading particle file: %s", filename); + File file = new File(filename); + FileInputStream stream = new FileInputStream(filename); + MappedByteBuffer map = stream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()); + if (littleEndian) + map.order(ByteOrder.LITTLE_ENDIAN); + FloatBuffer buffer = map.asFloatBuffer(); + data = new float[buffer.capacity()]; + for (int i = 0; i < data.length; i++) { + data[i] = buffer.get(i); + } + stream.close(); + } else { + p.checkNextToken("points"); + int n = p.getNextInt(); + data = parseFloatArray(n * 3); // read 3n points + } + + geometry.setPoints(data); + + if (p.peekNextToken("num")) { + geometry.setNum(p.getNextInt()); + } else { + geometry.setNum(data.length / 3); + } + + p.checkNextToken("radius"); + geometry.setRadius(p.getNextFloat()); + geometry.setup(api); + } else if (type.equals("file-mesh")) { + UI.printInfo(Module.API, "Reading file mesh: %s ... ", name); + FileMeshParameter geometry = new FileMeshParameter(); + geometry.setName(name); + + p.checkNextToken("filename"); + geometry.setFilename(p.getNextToken()); + + if (p.peekNextToken("smooth_normals")) { + geometry.setSmoothNormals(p.getNextBoolean()); + } + geometry.setup(api); + } else if (type.equals("bezier-mesh")) { + UI.printInfo(Module.API, "Reading bezier mesh: %s ... ", name); + BezierMeshParameter geometry = new BezierMeshParameter(); + geometry.setName(name); + + p.checkNextToken("n"); + //int nu, nv; + + geometry.setNu(p.getNextInt()); + geometry.setNv(p.getNextInt()); + + if (p.peekNextToken("wrap")) { + geometry.setUwrap(p.getNextBoolean()); + geometry.setVwrap(p.getNextBoolean()); + } + p.checkNextToken("points"); + float[] points = new float[3 * geometry.getNu() * geometry.getNv()]; + for (int i = 0; i < points.length; i++) { + points[i] = p.getNextFloat(); + } + + geometry.setPoints(points); + + if (p.peekNextToken("subdivs")) { + geometry.setSubdivs(p.getNextInt()); + } + if (p.peekNextToken("smooth")) { + geometry.setSmooth(p.getNextBoolean()); + } + geometry.setup(api); + } else { + UI.printWarning(Module.API, "Unrecognized object type: %s", p.getNextToken()); + noInstance = true; + } + if (!noInstance) { + instanceParameter.setName(name + ".instance"); + instanceParameter.setGeometry(name); + instanceParameter.setup(api); + + // create instance + /*api.parameter("shaders", shaders); + if (modifiers != null) + api.parameter("modifiers", modifiers); + if (transform != null && transform.length > 0) { + if (transform.length == 1) + api.parameter("transform", transform[0]); + else { + api.parameter("transform.steps", transform.length); + api.parameter("transform.times", "float", "none", new float[]{ + transformTime0, transformTime1}); + for (int i = 0; i < transform.length; i++) + api.parameter(String.format("transform[%d]", i), transform[i]); + } + } + api.instance(name + ".instance", name);*/ + } + p.checkNextToken("}"); + } + + private void parseInstanceBlock(SunflowAPIInterface api) throws ParserException, IOException { + InstanceParameter parameter = new InstanceParameter(); + p.checkNextToken("{"); + p.checkNextToken("name"); + parameter.setName(p.getNextToken()); + UI.printInfo(Module.API, "Reading instance: %s ...", parameter.getName()); + p.checkNextToken("geometry"); + parameter.setGeometry(p.getNextToken()); + + TransformParameter transformParameter = parseTransform(); + parameter.setTransform(transformParameter); + + String[] shaders; + if (p.peekNextToken("shaders")) { + int n = p.getNextInt(); + shaders = new String[n]; + for (int i = 0; i < n; i++) { + shaders[i] = p.getNextToken(); + } + } else { + p.checkNextToken("shader"); + shaders = new String[]{p.getNextToken()}; + } + parameter.setShaders(shaders); + + String[] modifiers = null; + if (p.peekNextToken("modifiers")) { + int n = p.getNextInt(); + modifiers = new String[n]; + for (int i = 0; i < n; i++) { + modifiers[i] = p.getNextToken(); + } + } else if (p.peekNextToken("modifier")) { + modifiers = new String[]{p.getNextToken()}; + } + parameter.setModifiers(modifiers); + parameter.setup(api); + + p.checkNextToken("}"); + } + + private TransformParameter parseTransform() throws ParserException, IOException { + TransformParameter transformParameter = new TransformParameter(); + p.checkNextToken("transform"); + if (p.peekNextToken("steps")) { + int n = p.getNextInt(); + + p.checkNextToken("times"); + float[] times = new float[n]; + for (int i = 0; i < n; i++) { + times[i] = p.getNextFloat(); + } + transformParameter.setTimes(times); + + Matrix4[] transforms = new Matrix4[n]; + for (int i = 0; i < n; i++) { + transforms[i] = parseMatrix(); + } + } else { + Matrix4[] transforms = new Matrix4[]{parseMatrix()}; + transformParameter.setTransforms(transforms); + } + return transformParameter; + } + + private TransformParameter checkParseTransform() throws ParserException, IOException { + TransformParameter transformParameter = new TransformParameter(); + if(p.peekNextToken("transform")) { + if (p.peekNextToken("steps")) { + int n = p.getNextInt(); + + p.checkNextToken("times"); + float[] times = new float[n]; + for (int i = 0; i < n; i++) { + times[i] = p.getNextFloat(); + } + transformParameter.setTimes(times); + + Matrix4[] transforms = new Matrix4[n]; + for (int i = 0; i < n; i++) { + transforms[i] = parseMatrix(); + } + } else { + Matrix4[] transforms = new Matrix4[]{parseMatrix()}; + transformParameter.setTransforms(transforms); + } + } else { + return null; + } + return transformParameter; + } + + private void parseLightBlock(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { + p.checkNextToken("{"); + p.checkNextToken("type"); + if (p.peekNextToken("mesh")) { + UI.printWarning(Module.API, "Deprecated light type: mesh"); + p.checkNextToken("name"); + String name = p.getNextToken(); + UI.printInfo(Module.API, "Reading light mesh: %s ...", name); + p.checkNextToken("emit"); + api.parameter("radiance", null, parseColor().getRGB()); + int samples = numLightSamples; + if (p.peekNextToken("samples")) + samples = p.getNextInt(); + else + UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", samples); + api.parameter("samples", samples); + int numVertices = p.getNextInt(); + int numTriangles = p.getNextInt(); + float[] points = new float[3 * numVertices]; + int[] triangles = new int[3 * numTriangles]; + for (int i = 0; i < numVertices; i++) { + p.checkNextToken("v"); + points[3 * i + 0] = p.getNextFloat(); + points[3 * i + 1] = p.getNextFloat(); + points[3 * i + 2] = p.getNextFloat(); + // ignored + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + } + for (int i = 0; i < numTriangles; i++) { + p.checkNextToken("t"); + triangles[3 * i + 0] = p.getNextInt(); + triangles[3 * i + 1] = p.getNextInt(); + triangles[3 * i + 2] = p.getNextInt(); + } + api.parameter("points", "point", "vertex", points); + api.parameter("triangles", triangles); + api.light(name, "triangle_mesh"); + } else if (p.peekNextToken("point")) { + UI.printInfo(Module.API, "Reading point light ..."); + PointLightParameter light = new PointLightParameter(); + light.setName(generateUniqueName("pointlight")); + + Color color; + if (p.peekNextToken("color")) { + color = parseColor(); + p.checkNextToken("power"); + float power = p.getNextFloat(); + color.mul(power); + } else { + UI.printWarning(Module.API, "Deprecated color specification - please use color and power instead"); + p.checkNextToken("power"); + color = parseColor(); + } + light.setColor(color); + + p.checkNextToken("p"); + light.setCenter(parsePoint()); + light.setup(api); + } else if (p.peekNextToken("spherical")) { + UI.printInfo(Module.API, "Reading spherical light ..."); + SphereLightParameter light = new SphereLightParameter(); + light.setName(generateUniqueName("spherelight")); + p.checkNextToken("color"); + Color color = parseColor(); + p.checkNextToken("radiance"); + float power = p.getNextFloat(); + color.mul(power); + light.setRadiance(color); + p.checkNextToken("center"); + light.setCenter(parsePoint()); + p.checkNextToken("radius"); + light.setRadius(p.getNextFloat()); + p.checkNextToken("samples"); + light.setSamples(p.getNextInt()); + light.setup(api); + } else if (p.peekNextToken("directional")) { + UI.printInfo(Module.API, "Reading directional light ..."); + DirectionalLightParameter light = new DirectionalLightParameter(); + light.setName(generateUniqueName("dirlight")); + p.checkNextToken("source"); + light.setSource(parsePoint()); + p.checkNextToken("target"); + light.setDirection(parsePoint()); + p.checkNextToken("radius"); + light.setRadius(p.getNextFloat()); + p.checkNextToken("emit"); + Color color = parseColor(); + if (p.peekNextToken("intensity")) { + float power = p.getNextFloat(); + color.mul(power); + } else { + UI.printWarning(Module.API, "Deprecated color specification - please use emit and intensity instead"); + } + light.setRadiance(color); + light.setup(api); + } else if (p.peekNextToken("ibl")) { + UI.printInfo(Module.API, "Reading image based light ..."); + ImageBasedLightParameter light = new ImageBasedLightParameter(); + light.setName(generateUniqueName("ibl")); + p.checkNextToken("image"); + light.setTexture(p.getNextToken()); + p.checkNextToken("center"); + light.setCenter(parseVector()); + p.checkNextToken("up"); + light.setUp(parseVector()); + p.checkNextToken("lock"); + light.setFixed(p.getNextBoolean()); + + light.setSamples(numLightSamples); + + if (p.peekNextToken("samples")) { + light.setSamples(p.getNextInt()); + } else { + UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", numLightSamples); + } + + if (p.peekNextToken("lowsamples")) { + light.setLowSamples(p.getNextInt()); + } + + light.setup(api); + } else if (p.peekNextToken("meshlight")) { + p.checkNextToken("name"); + TriangleMeshLightParameter light = new TriangleMeshLightParameter(); + light.setName(p.getNextToken()); + + UI.printInfo(Module.API, "Reading meshlight: %s ...", light.getName()); + p.checkNextToken("emit"); + Color color = parseColor(); + if (p.peekNextToken("radiance")) { + float r = p.getNextFloat(); + color.mul(r); + } else { + UI.printWarning(Module.API, "Deprecated color specification - please use emit and radiance instead"); + } + light.setRadiance(color); + light.setSamples(numLightSamples); + + if (p.peekNextToken("samples")) { + light.setSamples(p.getNextInt()); + } else { + UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", light.getSamples()); + } + + // parse vertices + p.checkNextToken("points"); + int np = p.getNextInt(); + float[] points = parseFloatArray(np * 3); + light.setPoints(points); + + // parse triangle indices + p.checkNextToken("triangles"); + int nt = p.getNextInt(); + int[] triangles = parseIntArray(nt * 3); + light.setTriangles(triangles); + light.setup(api); + } else if (p.peekNextToken("sunsky")) { + SunSkyLightParameter light = new SunSkyLightParameter(); + light.setName(generateUniqueName("sunsky")); + p.checkNextToken("up"); + light.setUp(parseVector()); + p.checkNextToken("east"); + light.setEast(parseVector()); + p.checkNextToken("sundir"); + light.setSunDirection(parseVector()); + p.checkNextToken("turbidity"); + light.setTurbidity(p.getNextFloat()); + + // TODO Is possible not to have samples in sun sky? + // light.setSamples(numLightSamples); + if (p.peekNextToken("samples")) { + light.setSamples(p.getNextInt()); + } + + if (p.peekNextToken("ground.extendsky")) { + light.setExtendSky(p.getNextBoolean()); + } else if (p.peekNextToken("ground.color")) { + light.setGroundColor(parseColor()); + } + light.setup(api); + } else if (p.peekNextToken("cornellbox")) { + UI.printInfo(Module.API, "Reading cornell box ..."); + CornellBoxLightParameter light = new CornellBoxLightParameter(); + light.setName(generateUniqueName("cornellbox")); + + p.checkNextToken("corner0"); + light.setMin(parsePoint()); + p.checkNextToken("corner1"); + light.setMax(parsePoint()); + p.checkNextToken("left"); + light.setLeft(parseColor()); + p.checkNextToken("right"); + light.setRight(parseColor()); + p.checkNextToken("top"); + light.setTop(parseColor()); + p.checkNextToken("bottom"); + light.setBottom(parseColor()); + p.checkNextToken("back"); + light.setBack(parseColor()); + p.checkNextToken("emit"); + light.setRadiance(parseColor()); + + // TODO Is possible not to have samples in cornell box? + // light.setSamples(numLightSamples); + if (p.peekNextToken("samples")) { + light.setSamples(p.getNextInt()); + } + light.setup(api); + } else { + UI.printWarning(Module.API, "Unrecognized object type: %s", p.getNextToken()); + } + p.checkNextToken("}"); + } + + private Color parseColor() throws IOException, ParserException, ColorSpecificationException { + if (p.peekNextToken("{")) { + String space = p.getNextToken(); + int req = ColorFactory.getRequiredDataValues(space); + if (req == -2) { + UI.printWarning(Module.API, "Unrecognized color space: %s", space); + return null; + } else if (req == -1) { + // array required, parse how many values are required + req = p.getNextInt(); + } + Color c = ColorFactory.createColor(space, parseFloatArray(req)); + p.checkNextToken("}"); + return c; + } else { + float r = p.getNextFloat(); + float g = p.getNextFloat(); + float b = p.getNextFloat(); + return ColorFactory.createColor(null, r, g, b); + } + } + + private Point3 parsePoint() throws IOException { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + return new Point3(x, y, z); + } + + private Vector3 parseVector() throws IOException { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + return new Vector3(x, y, z); + } + + private int[] parseIntArray(int size) throws IOException { + int[] data = new int[size]; + for (int i = 0; i < size; i++) { + data[i] = p.getNextInt(); + } + return data; + } + + private float[] parseFloatArray(int size) throws IOException { + float[] data = new float[size]; + for (int i = 0; i < size; i++) { + data[i] = p.getNextFloat(); + } + return data; + } + + private Matrix4 parseMatrix() throws IOException, ParserException { + if (p.peekNextToken("row")) { + return new Matrix4(parseFloatArray(16), true); + } else if (p.peekNextToken("col")) { + return new Matrix4(parseFloatArray(16), false); + } else { + Matrix4 m = Matrix4.IDENTITY; + p.checkNextToken("{"); + while (!p.peekNextToken("}")) { + Matrix4 t = null; + if (p.peekNextToken("translate")) { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + t = Matrix4.translation(x, y, z); + } else if (p.peekNextToken("scaleu")) { + float s = p.getNextFloat(); + t = Matrix4.scale(s); + } else if (p.peekNextToken("scale")) { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + t = Matrix4.scale(x, y, z); + } else if (p.peekNextToken("rotatex")) { + float angle = p.getNextFloat(); + t = Matrix4.rotateX((float) Math.toRadians(angle)); + } else if (p.peekNextToken("rotatey")) { + float angle = p.getNextFloat(); + t = Matrix4.rotateY((float) Math.toRadians(angle)); + } else if (p.peekNextToken("rotatez")) { + float angle = p.getNextFloat(); + t = Matrix4.rotateZ((float) Math.toRadians(angle)); + } else if (p.peekNextToken("rotate")) { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + float angle = p.getNextFloat(); + t = Matrix4.rotate(x, y, z, (float) Math.toRadians(angle)); + } else + UI.printWarning(Module.API, "Unrecognized transformation type: %s", p.getNextToken()); + if (t != null) + m = t.multiply(m); + } + return m; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/parser/SCParser.java b/src/main/java/org/sunflow/core/parser/SCParser.java index eee7286..e7207e1 100644 --- a/src/main/java/org/sunflow/core/parser/SCParser.java +++ b/src/main/java/org/sunflow/core/parser/SCParser.java @@ -1,1284 +1,1592 @@ -package org.sunflow.core.parser; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.ByteOrder; -import java.nio.FloatBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.util.HashMap; - -import org.sunflow.PluginRegistry; -import org.sunflow.SunflowAPI; -import org.sunflow.SunflowAPIInterface; -import org.sunflow.core.SceneParser; -import org.sunflow.image.Color; -import org.sunflow.image.ColorFactory; -import org.sunflow.image.ColorFactory.ColorSpecificationException; -import org.sunflow.math.Matrix4; -import org.sunflow.math.Point3; -import org.sunflow.math.Vector3; -import org.sunflow.system.Parser; -import org.sunflow.system.Timer; -import org.sunflow.system.UI; -import org.sunflow.system.Parser.ParserException; -import org.sunflow.system.UI.Module; - -/** - * This class provides a static method for loading files in the Sunflow scene - * file format. - */ -public class SCParser implements SceneParser { - private static int instanceCounter = 0; - private int instanceNumber; - private Parser p; - private int numLightSamples; - // used to generate unique names inside this parser - private HashMap objectNames; - - public SCParser() { - objectNames = new HashMap(); - instanceCounter++; - instanceNumber = instanceCounter; - } - - private String generateUniqueName(String prefix) { - // generate a unique name for this class: - int index = 1; - Integer value = objectNames.get(prefix); - if (value != null) { - index = value; - objectNames.put(prefix, index + 1); - } else { - objectNames.put(prefix, index + 1); - } - return String.format("@sc_%d::%s_%d", instanceNumber, prefix, index); - } - - public boolean parse(String filename, SunflowAPIInterface api) { - String localDir = new File(filename).getAbsoluteFile().getParentFile().getAbsolutePath(); - numLightSamples = 1; - Timer timer = new Timer(); - timer.start(); - UI.printInfo(Module.API, "Parsing \"%s\" ...", filename); - try { - p = new Parser(filename); - while (true) { - String token = p.getNextToken(); - if (token == null) - break; - if (token.equals("image")) { - UI.printInfo(Module.API, "Reading image settings ..."); - parseImageBlock(api); - } else if (token.equals("background")) { - UI.printInfo(Module.API, "Reading background ..."); - parseBackgroundBlock(api); - } else if (token.equals("accel")) { - UI.printInfo(Module.API, "Reading accelerator type ..."); - p.getNextToken(); - UI.printWarning(Module.API, "Setting accelerator type is not recommended - ignoring"); - } else if (token.equals("filter")) { - UI.printInfo(Module.API, "Reading image filter type ..."); - parseFilter(api); - } else if (token.equals("bucket")) { - UI.printInfo(Module.API, "Reading bucket settings ..."); - api.parameter("bucket.size", p.getNextInt()); - api.parameter("bucket.order", p.getNextToken()); - api.options(SunflowAPI.DEFAULT_OPTIONS); - } else if (token.equals("photons")) { - UI.printInfo(Module.API, "Reading photon settings ..."); - parsePhotonBlock(api); - } else if (token.equals("gi")) { - UI.printInfo(Module.API, "Reading global illumination settings ..."); - parseGIBlock(api); - } else if (token.equals("lightserver")) { - UI.printInfo(Module.API, "Reading light server settings ..."); - parseLightserverBlock(api); - } else if (token.equals("trace-depths")) { - UI.printInfo(Module.API, "Reading trace depths ..."); - parseTraceBlock(api); - } else if (token.equals("camera")) { - parseCamera(api); - } else if (token.equals("shader")) { - if (!parseShader(api)) - return false; - } else if (token.equals("modifier")) { - if (!parseModifier(api)) - return false; - } else if (token.equals("override")) { - api.parameter("override.shader", p.getNextToken()); - api.parameter("override.photons", p.getNextBoolean()); - api.options(SunflowAPI.DEFAULT_OPTIONS); - } else if (token.equals("object")) { - parseObjectBlock(api); - } else if (token.equals("instance")) { - parseInstanceBlock(api); - } else if (token.equals("light")) { - parseLightBlock(api); - } else if (token.equals("texturepath")) { - String path = p.getNextToken(); - if (!new File(path).isAbsolute()) - path = localDir + File.separator + path; - api.searchpath("texture", path); - } else if (token.equals("includepath")) { - String path = p.getNextToken(); - if (!new File(path).isAbsolute()) - path = localDir + File.separator + path; - api.searchpath("include", path); - } else if (token.equals("include")) { - String file = p.getNextToken(); - UI.printInfo(Module.API, "Including: \"%s\" ...", file); - api.include(file); - } else - UI.printWarning(Module.API, "Unrecognized token %s", token); - } - p.close(); - } catch (ParserException e) { - UI.printError(Module.API, "%s", e.getMessage()); - e.printStackTrace(); - return false; - } catch (FileNotFoundException e) { - UI.printError(Module.API, "%s", e.getMessage()); - return false; - } catch (IOException e) { - UI.printError(Module.API, "%s", e.getMessage()); - return false; - } catch (ColorSpecificationException e) { - UI.printError(Module.API, "%s", e.getMessage()); - return false; - } - timer.end(); - UI.printInfo(Module.API, "Done parsing."); - UI.printInfo(Module.API, "Parsing time: %s", timer.toString()); - return true; - } - - private void parseImageBlock(SunflowAPIInterface api) throws IOException, ParserException { - p.checkNextToken("{"); - if (p.peekNextToken("resolution")) { - api.parameter("resolutionX", p.getNextInt()); - api.parameter("resolutionY", p.getNextInt()); - } - if (p.peekNextToken("sampler")) - api.parameter("sampler", p.getNextToken()); - if (p.peekNextToken("aa")) { - api.parameter("aa.min", p.getNextInt()); - api.parameter("aa.max", p.getNextInt()); - } - if (p.peekNextToken("samples")) - api.parameter("aa.samples", p.getNextInt()); - if (p.peekNextToken("contrast")) - api.parameter("aa.contrast", p.getNextFloat()); - if (p.peekNextToken("filter")) - api.parameter("filter", p.getNextToken()); - if (p.peekNextToken("jitter")) - api.parameter("aa.jitter", p.getNextBoolean()); - if (p.peekNextToken("show-aa")) { - UI.printWarning(Module.API, "Deprecated: show-aa ignored"); - p.getNextBoolean(); - } - if (p.peekNextToken("cache")) - api.parameter("aa.cache", p.getNextBoolean()); - if (p.peekNextToken("output")) { - UI.printWarning(Module.API, "Deprecated: output statement ignored"); - p.getNextToken(); - } - api.options(SunflowAPI.DEFAULT_OPTIONS); - p.checkNextToken("}"); - } - - private void parseBackgroundBlock(SunflowAPIInterface api) throws IOException, ParserException, ColorSpecificationException { - p.checkNextToken("{"); - p.checkNextToken("color"); - api.parameter("color", null, parseColor().getRGB()); - api.shader("background.shader", "constant"); - api.geometry("background", "background"); - api.parameter("shaders", "background.shader"); - api.instance("background.instance", "background"); - p.checkNextToken("}"); - } - - private void parseFilter(SunflowAPIInterface api) throws IOException, ParserException { - UI.printWarning(Module.API, "Deprecated keyword \"filter\" - set this option in the image block"); - String name = p.getNextToken(); - api.parameter("filter", name); - api.options(SunflowAPI.DEFAULT_OPTIONS); - boolean hasSizeParams = name.equals("box") || name.equals("gaussian") || name.equals("blackman-harris") || name.equals("sinc") || name.equals("triangle"); - if (hasSizeParams) { - p.getNextFloat(); - p.getNextFloat(); - } - } - - private void parsePhotonBlock(SunflowAPIInterface api) throws ParserException, IOException { - int numEmit = 0; - boolean globalEmit = false; - p.checkNextToken("{"); - if (p.peekNextToken("emit")) { - UI.printWarning(Module.API, "Shared photon emit values are deprectated - specify number of photons to emit per map"); - numEmit = p.getNextInt(); - globalEmit = true; - } - if (p.peekNextToken("global")) { - UI.printWarning(Module.API, "Global photon map setting belonds inside the gi block - ignoring"); - if (!globalEmit) - p.getNextInt(); - p.getNextToken(); - p.getNextInt(); - p.getNextFloat(); - } - p.checkNextToken("caustics"); - if (!globalEmit) - numEmit = p.getNextInt(); - api.parameter("caustics.emit", numEmit); - api.parameter("caustics", p.getNextToken()); - api.parameter("caustics.gather", p.getNextInt()); - api.parameter("caustics.radius", p.getNextFloat()); - api.options(SunflowAPI.DEFAULT_OPTIONS); - p.checkNextToken("}"); - } - - private void parseGIBlock(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { - p.checkNextToken("{"); - p.checkNextToken("type"); - if (p.peekNextToken("irr-cache")) { - api.parameter("gi.engine", "irr-cache"); - p.checkNextToken("samples"); - api.parameter("gi.irr-cache.samples", p.getNextInt()); - p.checkNextToken("tolerance"); - api.parameter("gi.irr-cache.tolerance", p.getNextFloat()); - p.checkNextToken("spacing"); - api.parameter("gi.irr-cache.min_spacing", p.getNextFloat()); - api.parameter("gi.irr-cache.max_spacing", p.getNextFloat()); - // parse global photon map info - if (p.peekNextToken("global")) { - api.parameter("gi.irr-cache.gmap.emit", p.getNextInt()); - api.parameter("gi.irr-cache.gmap", p.getNextToken()); - api.parameter("gi.irr-cache.gmap.gather", p.getNextInt()); - api.parameter("gi.irr-cache.gmap.radius", p.getNextFloat()); - } - } else if (p.peekNextToken("path")) { - api.parameter("gi.engine", "path"); - p.checkNextToken("samples"); - api.parameter("gi.path.samples", p.getNextInt()); - if (p.peekNextToken("bounces")) { - UI.printWarning(Module.API, "Deprecated setting: bounces - use diffuse trace depth instead"); - p.getNextInt(); - } - } else if (p.peekNextToken("fake")) { - api.parameter("gi.engine", "fake"); - p.checkNextToken("up"); - api.parameter("gi.fake.up", parseVector()); - p.checkNextToken("sky"); - api.parameter("gi.fake.sky", null, parseColor().getRGB()); - p.checkNextToken("ground"); - api.parameter("gi.fake.ground", null, parseColor().getRGB()); - } else if (p.peekNextToken("igi")) { - api.parameter("gi.engine", "igi"); - p.checkNextToken("samples"); - api.parameter("gi.igi.samples", p.getNextInt()); - p.checkNextToken("sets"); - api.parameter("gi.igi.sets", p.getNextInt()); - if (!p.peekNextToken("b")) - p.checkNextToken("c"); - api.parameter("gi.igi.c", p.getNextFloat()); - p.checkNextToken("bias-samples"); - api.parameter("gi.igi.bias_samples", p.getNextInt()); - } else if (p.peekNextToken("ambocc")) { - api.parameter("gi.engine", "ambocc"); - p.checkNextToken("bright"); - api.parameter("gi.ambocc.bright", null, parseColor().getRGB()); - p.checkNextToken("dark"); - api.parameter("gi.ambocc.dark", null, parseColor().getRGB()); - p.checkNextToken("samples"); - api.parameter("gi.ambocc.samples", p.getNextInt()); - if (p.peekNextToken("maxdist")) - api.parameter("gi.ambocc.maxdist", p.getNextFloat()); - } else if (p.peekNextToken("none") || p.peekNextToken("null")) { - // disable GI - api.parameter("gi.engine", "none"); - } else - UI.printWarning(Module.API, "Unrecognized gi engine type \"%s\" - ignoring", p.getNextToken()); - api.options(SunflowAPI.DEFAULT_OPTIONS); - p.checkNextToken("}"); - } - - private void parseLightserverBlock(SunflowAPIInterface api) throws ParserException, IOException { - p.checkNextToken("{"); - if (p.peekNextToken("shadows")) { - UI.printWarning(Module.API, "Deprecated: shadows setting ignored"); - p.getNextBoolean(); - } - if (p.peekNextToken("direct-samples")) { - UI.printWarning(Module.API, "Deprecated: use samples keyword in area light definitions"); - numLightSamples = p.getNextInt(); - } - if (p.peekNextToken("glossy-samples")) { - UI.printWarning(Module.API, "Deprecated: use samples keyword in glossy shader definitions"); - p.getNextInt(); - } - if (p.peekNextToken("max-depth")) { - UI.printWarning(Module.API, "Deprecated: max-depth setting - use trace-depths block instead"); - int d = p.getNextInt(); - api.parameter("depths.diffuse", 1); - api.parameter("depths.reflection", d - 1); - api.parameter("depths.refraction", 0); - api.options(SunflowAPI.DEFAULT_OPTIONS); - } - if (p.peekNextToken("global")) { - UI.printWarning(Module.API, "Deprecated: global settings ignored - use photons block instead"); - p.getNextBoolean(); - p.getNextInt(); - p.getNextInt(); - p.getNextInt(); - p.getNextFloat(); - } - if (p.peekNextToken("caustics")) { - UI.printWarning(Module.API, "Deprecated: caustics settings ignored - use photons block instead"); - p.getNextBoolean(); - p.getNextInt(); - p.getNextFloat(); - p.getNextInt(); - p.getNextFloat(); - } - if (p.peekNextToken("irr-cache")) { - UI.printWarning(Module.API, "Deprecated: irradiance cache settings ignored - use gi block instead"); - p.getNextInt(); - p.getNextFloat(); - p.getNextFloat(); - p.getNextFloat(); - } - p.checkNextToken("}"); - } - - private void parseTraceBlock(SunflowAPIInterface api) throws ParserException, IOException { - p.checkNextToken("{"); - if (p.peekNextToken("diff")) - api.parameter("depths.diffuse", p.getNextInt()); - if (p.peekNextToken("refl")) - api.parameter("depths.reflection", p.getNextInt()); - if (p.peekNextToken("refr")) - api.parameter("depths.refraction", p.getNextInt()); - p.checkNextToken("}"); - api.options(SunflowAPI.DEFAULT_OPTIONS); - } - - private void parseCamera(SunflowAPIInterface api) throws ParserException, IOException { - p.checkNextToken("{"); - p.checkNextToken("type"); - String type = p.getNextToken(); - UI.printInfo(Module.API, "Reading %s camera ...", type); - if (p.peekNextToken("shutter")) { - api.parameter("shutter.open", p.getNextFloat()); - api.parameter("shutter.close", p.getNextFloat()); - } - parseCameraTransform(api); - String name = generateUniqueName("camera"); - if (type.equals("pinhole")) { - p.checkNextToken("fov"); - api.parameter("fov", p.getNextFloat()); - p.checkNextToken("aspect"); - api.parameter("aspect", p.getNextFloat()); - if (p.peekNextToken("shift")) { - api.parameter("shift.x", p.getNextFloat()); - api.parameter("shift.y", p.getNextFloat()); - } - api.camera(name, "pinhole"); - } else if (type.equals("thinlens")) { - p.checkNextToken("fov"); - api.parameter("fov", p.getNextFloat()); - p.checkNextToken("aspect"); - api.parameter("aspect", p.getNextFloat()); - if (p.peekNextToken("shift")) { - api.parameter("shift.x", p.getNextFloat()); - api.parameter("shift.y", p.getNextFloat()); - } - p.checkNextToken("fdist"); - api.parameter("focus.distance", p.getNextFloat()); - p.checkNextToken("lensr"); - api.parameter("lens.radius", p.getNextFloat()); - if (p.peekNextToken("sides")) - api.parameter("lens.sides", p.getNextInt()); - if (p.peekNextToken("rotation")) - api.parameter("lens.rotation", p.getNextFloat()); - api.camera(name, "thinlens"); - } else if (type.equals("spherical")) { - // no extra arguments - api.camera(name, "spherical"); - } else if (type.equals("fisheye")) { - // no extra arguments - api.camera(name, "fisheye"); - } else { - UI.printWarning(Module.API, "Unrecognized camera type: %s", p.getNextToken()); - p.checkNextToken("}"); - return; - } - p.checkNextToken("}"); - if (name != null) { - api.parameter("camera", name); - api.options(SunflowAPI.DEFAULT_OPTIONS); - } - } - - private void parseCameraTransform(SunflowAPIInterface api) throws ParserException, IOException { - if (p.peekNextToken("steps")) { - // motion blur camera - int n = p.getNextInt(); - api.parameter("transform.steps", n); - // parse time extents - p.checkNextToken("times"); - float t0 = p.getNextFloat(); - float t1 = p.getNextFloat(); - api.parameter("transform.times", "float", "none", new float[] { t0, - t1 }); - for (int i = 0; i < n; i++) - parseCameraMatrix(i, api); - } else - parseCameraMatrix(-1, api); - } - - private void parseCameraMatrix(int index, SunflowAPIInterface api) throws IOException, ParserException { - String offset = index < 0 ? "" : String.format("[%d]", index); - if (p.peekNextToken("transform")) { - // advanced camera - api.parameter(String.format("transform%s", offset), parseMatrix()); - } else { - if (index >= 0) - p.checkNextToken("{"); - // regular camera specification - p.checkNextToken("eye"); - Point3 eye = parsePoint(); - p.checkNextToken("target"); - Point3 target = parsePoint(); - p.checkNextToken("up"); - Vector3 up = parseVector(); - api.parameter(String.format("transform%s", offset), Matrix4.lookAt(eye, target, up)); - if (index >= 0) - p.checkNextToken("}"); - } - } - - private boolean parseShader(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { - p.checkNextToken("{"); - p.checkNextToken("name"); - String name = p.getNextToken(); - UI.printInfo(Module.API, "Reading shader: %s ...", name); - p.checkNextToken("type"); - if (p.peekNextToken("diffuse")) { - if (p.peekNextToken("diff")) { - api.parameter("diffuse", null, parseColor().getRGB()); - api.shader(name, "diffuse"); - } else if (p.peekNextToken("texture")) { - api.parameter("texture", p.getNextToken()); - api.shader(name, "textured_diffuse"); - } else - UI.printWarning(Module.API, "Unrecognized option in diffuse shader block: %s", p.getNextToken()); - } else if (p.peekNextToken("phong")) { - String tex = null; - if (p.peekNextToken("texture")) - api.parameter("texture", tex = p.getNextToken()); - else { - p.checkNextToken("diff"); - api.parameter("diffuse", null, parseColor().getRGB()); - } - p.checkNextToken("spec"); - api.parameter("specular", null, parseColor().getRGB()); - api.parameter("power", p.getNextFloat()); - if (p.peekNextToken("samples")) - api.parameter("samples", p.getNextInt()); - if (tex != null) - api.shader(name, "textured_phong"); - else - api.shader(name, "phong"); - } else if (p.peekNextToken("amb-occ") || p.peekNextToken("amb-occ2")) { - String tex = null; - if (p.peekNextToken("diff") || p.peekNextToken("bright")) - api.parameter("bright", null, parseColor().getRGB()); - else if (p.peekNextToken("texture")) - api.parameter("texture", tex = p.getNextToken()); - if (p.peekNextToken("dark")) { - api.parameter("dark", null, parseColor().getRGB()); - p.checkNextToken("samples"); - api.parameter("samples", p.getNextInt()); - p.checkNextToken("dist"); - api.parameter("maxdist", p.getNextFloat()); - } - if (tex == null) - api.shader(name, "ambient_occlusion"); - else - api.shader(name, "textured_ambient_occlusion"); - } else if (p.peekNextToken("mirror")) { - p.checkNextToken("refl"); - api.parameter("color", null, parseColor().getRGB()); - api.shader(name, "mirror"); - } else if (p.peekNextToken("glass")) { - p.checkNextToken("eta"); - api.parameter("eta", p.getNextFloat()); - p.checkNextToken("color"); - api.parameter("color", null, parseColor().getRGB()); - if (p.peekNextToken("absorption.distance") || p.peekNextToken("absorbtion.distance")) - api.parameter("absorption.distance", p.getNextFloat()); - if (p.peekNextToken("absorption.color") || p.peekNextToken("absorbtion.color")) - api.parameter("absorption.color", null, parseColor().getRGB()); - api.shader(name, "glass"); - } else if (p.peekNextToken("shiny")) { - String tex = null; - if (p.peekNextToken("texture")) - api.parameter("texture", tex = p.getNextToken()); - else { - p.checkNextToken("diff"); - api.parameter("diffuse", null, parseColor().getRGB()); - } - p.checkNextToken("refl"); - api.parameter("shiny", p.getNextFloat()); - if (tex == null) - api.shader(name, "shiny_diffuse"); - else - api.shader(name, "textured_shiny_diffuse"); - } else if (p.peekNextToken("ward")) { - String tex = null; - if (p.peekNextToken("texture")) - api.parameter("texture", tex = p.getNextToken()); - else { - p.checkNextToken("diff"); - api.parameter("diffuse", null, parseColor().getRGB()); - } - p.checkNextToken("spec"); - api.parameter("specular", null, parseColor().getRGB()); - p.checkNextToken("rough"); - api.parameter("roughnessX", p.getNextFloat()); - api.parameter("roughnessY", p.getNextFloat()); - if (p.peekNextToken("samples")) - api.parameter("samples", p.getNextInt()); - if (tex != null) - api.shader(name, "textured_ward"); - else - api.shader(name, "ward"); - } else if (p.peekNextToken("view-caustics")) { - api.shader(name, "view_caustics"); - } else if (p.peekNextToken("view-irradiance")) { - api.shader(name, "view_irradiance"); - } else if (p.peekNextToken("view-global")) { - api.shader(name, "view_global"); - } else if (p.peekNextToken("constant")) { - // backwards compatibility -- peek only - p.peekNextToken("color"); - api.parameter("color", null, parseColor().getRGB()); - api.shader(name, "constant"); - } else if (p.peekNextToken("janino")) { - String typename = p.peekNextToken("typename") ? p.getNextToken() : PluginRegistry.shaderPlugins.generateUniqueName("janino_shader"); - if (!PluginRegistry.shaderPlugins.registerPlugin(typename, p.getNextCodeBlock())) - return false; - api.shader(name, typename); - } else if (p.peekNextToken("id")) { - api.shader(name, "show_instance_id"); - } else if (p.peekNextToken("uber")) { - if (p.peekNextToken("diff")) - api.parameter("diffuse", null, parseColor().getRGB()); - if (p.peekNextToken("diff.texture")) - api.parameter("diffuse.texture", p.getNextToken()); - if (p.peekNextToken("diff.blend")) - api.parameter("diffuse.blend", p.getNextFloat()); - if (p.peekNextToken("refl") || p.peekNextToken("spec")) - api.parameter("specular", null, parseColor().getRGB()); - if (p.peekNextToken("texture")) { - // deprecated - UI.printWarning(Module.API, "Deprecated uber shader parameter \"texture\" - please use \"diffuse.texture\" and \"diffuse.blend\" instead"); - api.parameter("diffuse.texture", p.getNextToken()); - api.parameter("diffuse.blend", p.getNextFloat()); - } - if (p.peekNextToken("spec.texture")) - api.parameter("specular.texture", p.getNextToken()); - if (p.peekNextToken("spec.blend")) - api.parameter("specular.blend", p.getNextFloat()); - if (p.peekNextToken("glossy")) - api.parameter("glossyness", p.getNextFloat()); - if (p.peekNextToken("samples")) - api.parameter("samples", p.getNextInt()); - api.shader(name, "uber"); - } else - UI.printWarning(Module.API, "Unrecognized shader type: %s", p.getNextToken()); - p.checkNextToken("}"); - return true; - } - - private boolean parseModifier(SunflowAPIInterface api) throws ParserException, IOException { - p.checkNextToken("{"); - p.checkNextToken("name"); - String name = p.getNextToken(); - UI.printInfo(Module.API, "Reading modifier: %s ...", name); - p.checkNextToken("type"); - if (p.peekNextToken("bump")) { - p.checkNextToken("texture"); - api.parameter("texture", p.getNextToken()); - p.checkNextToken("scale"); - api.parameter("scale", p.getNextFloat()); - api.modifier(name, "bump_map"); - } else if (p.peekNextToken("normalmap")) { - p.checkNextToken("texture"); - api.parameter("texture", p.getNextToken()); - api.modifier(name, "normal_map"); - } else if (p.peekNextToken("perlin")) { - p.checkNextToken("function"); - api.parameter("function", p.getNextInt()); - p.checkNextToken("size"); - api.parameter("size", p.getNextFloat()); - p.checkNextToken("scale"); - api.parameter("scale", p.getNextFloat()); - api.modifier(name, "perlin"); - } else { - UI.printWarning(Module.API, "Unrecognized modifier type: %s", p.getNextToken()); - } - p.checkNextToken("}"); - return true; - } - - private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, IOException { - p.checkNextToken("{"); - boolean noInstance = false; - Matrix4[] transform = null; - float transformTime0 = 0, transformTime1 = 0; - String name = null; - String[] shaders = null; - String[] modifiers = null; - if (p.peekNextToken("noinstance")) { - // this indicates that the geometry is to be created, but not - // instanced into the scene - noInstance = true; - } else { - // these are the parameters to be passed to the instance - if (p.peekNextToken("shaders")) { - int n = p.getNextInt(); - shaders = new String[n]; - for (int i = 0; i < n; i++) - shaders[i] = p.getNextToken(); - } else { - p.checkNextToken("shader"); - shaders = new String[] { p.getNextToken() }; - } - if (p.peekNextToken("modifiers")) { - int n = p.getNextInt(); - modifiers = new String[n]; - for (int i = 0; i < n; i++) - modifiers[i] = p.getNextToken(); - } else if (p.peekNextToken("modifier")) - modifiers = new String[] { p.getNextToken() }; - if (p.peekNextToken("transform")) { - if (p.peekNextToken("steps")) { - transform = new Matrix4[p.getNextInt()]; - p.checkNextToken("times"); - transformTime0 = p.getNextFloat(); - transformTime1 = p.getNextFloat(); - for (int i = 0; i < transform.length; i++) - transform[i] = parseMatrix(); - } else - transform = new Matrix4[] { parseMatrix() }; - } - } - if (p.peekNextToken("accel")) - api.parameter("accel", p.getNextToken()); - p.checkNextToken("type"); - String type = p.getNextToken(); - if (p.peekNextToken("name")) - name = p.getNextToken(); - else - name = generateUniqueName(type); - if (type.equals("mesh")) { - UI.printWarning(Module.API, "Deprecated object type: mesh"); - UI.printInfo(Module.API, "Reading mesh: %s ...", name); - int numVertices = p.getNextInt(); - int numTriangles = p.getNextInt(); - float[] points = new float[numVertices * 3]; - float[] normals = new float[numVertices * 3]; - float[] uvs = new float[numVertices * 2]; - for (int i = 0; i < numVertices; i++) { - p.checkNextToken("v"); - points[3 * i + 0] = p.getNextFloat(); - points[3 * i + 1] = p.getNextFloat(); - points[3 * i + 2] = p.getNextFloat(); - normals[3 * i + 0] = p.getNextFloat(); - normals[3 * i + 1] = p.getNextFloat(); - normals[3 * i + 2] = p.getNextFloat(); - uvs[2 * i + 0] = p.getNextFloat(); - uvs[2 * i + 1] = p.getNextFloat(); - } - int[] triangles = new int[numTriangles * 3]; - for (int i = 0; i < numTriangles; i++) { - p.checkNextToken("t"); - triangles[i * 3 + 0] = p.getNextInt(); - triangles[i * 3 + 1] = p.getNextInt(); - triangles[i * 3 + 2] = p.getNextInt(); - } - // create geometry - api.parameter("triangles", triangles); - api.parameter("points", "point", "vertex", points); - api.parameter("normals", "vector", "vertex", normals); - api.parameter("uvs", "texcoord", "vertex", uvs); - api.geometry(name, "triangle_mesh"); - } else if (type.equals("flat-mesh")) { - UI.printWarning(Module.API, "Deprecated object type: flat-mesh"); - UI.printInfo(Module.API, "Reading flat mesh: %s ...", name); - int numVertices = p.getNextInt(); - int numTriangles = p.getNextInt(); - float[] points = new float[numVertices * 3]; - float[] uvs = new float[numVertices * 2]; - for (int i = 0; i < numVertices; i++) { - p.checkNextToken("v"); - points[3 * i + 0] = p.getNextFloat(); - points[3 * i + 1] = p.getNextFloat(); - points[3 * i + 2] = p.getNextFloat(); - p.getNextFloat(); - p.getNextFloat(); - p.getNextFloat(); - uvs[2 * i + 0] = p.getNextFloat(); - uvs[2 * i + 1] = p.getNextFloat(); - } - int[] triangles = new int[numTriangles * 3]; - for (int i = 0; i < numTriangles; i++) { - p.checkNextToken("t"); - triangles[i * 3 + 0] = p.getNextInt(); - triangles[i * 3 + 1] = p.getNextInt(); - triangles[i * 3 + 2] = p.getNextInt(); - } - // create geometry - api.parameter("triangles", triangles); - api.parameter("points", "point", "vertex", points); - api.parameter("uvs", "texcoord", "vertex", uvs); - api.geometry(name, "triangle_mesh"); - } else if (type.equals("sphere")) { - UI.printInfo(Module.API, "Reading sphere ..."); - api.geometry(name, "sphere"); - if (transform == null && !noInstance) { - // legacy method of specifying transformation for spheres - p.checkNextToken("c"); - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - p.checkNextToken("r"); - float radius = p.getNextFloat(); - api.parameter("transform", Matrix4.translation(x, y, z).multiply(Matrix4.scale(radius))); - api.parameter("shaders", shaders); - if (modifiers != null) - api.parameter("modifiers", modifiers); - api.instance(name + ".instance", name); - // disable future instancing - instance has already been created - noInstance = true; - } - } else if (type.equals("cylinder")) { - UI.printInfo(Module.API, "Reading cylinder ..."); - api.geometry(name, "cylinder"); - } else if (type.equals("banchoff")) { - UI.printInfo(Module.API, "Reading banchoff ..."); - api.geometry(name, "banchoff"); - } else if (type.equals("torus")) { - UI.printInfo(Module.API, "Reading torus ..."); - p.checkNextToken("r"); - api.parameter("radiusInner", p.getNextFloat()); - api.parameter("radiusOuter", p.getNextFloat()); - api.geometry(name, "torus"); - } else if (type.equals("sphereflake")) { - UI.printInfo(Module.API, "Reading sphereflake ..."); - if (p.peekNextToken("level")) - api.parameter("level", p.getNextInt()); - if (p.peekNextToken("axis")) - api.parameter("axis", parseVector()); - if (p.peekNextToken("radius")) - api.parameter("radius", p.getNextFloat()); - api.geometry(name, "sphereflake"); - } else if (type.equals("plane")) { - UI.printInfo(Module.API, "Reading plane ..."); - p.checkNextToken("p"); - api.parameter("center", parsePoint()); - if (p.peekNextToken("n")) { - api.parameter("normal", parseVector()); - } else { - p.checkNextToken("p"); - api.parameter("point1", parsePoint()); - p.checkNextToken("p"); - api.parameter("point2", parsePoint()); - } - api.geometry(name, "plane"); - } else if (type.equals("generic-mesh")) { - UI.printInfo(Module.API, "Reading generic mesh: %s ... ", name); - // parse vertices - p.checkNextToken("points"); - int np = p.getNextInt(); - api.parameter("points", "point", "vertex", parseFloatArray(np * 3)); - // parse triangle indices - p.checkNextToken("triangles"); - int nt = p.getNextInt(); - api.parameter("triangles", parseIntArray(nt * 3)); - // parse normals - p.checkNextToken("normals"); - if (p.peekNextToken("vertex")) - api.parameter("normals", "vector", "vertex", parseFloatArray(np * 3)); - else if (p.peekNextToken("facevarying")) - api.parameter("normals", "vector", "facevarying", parseFloatArray(nt * 9)); - else - p.checkNextToken("none"); - // parse texture coordinates - p.checkNextToken("uvs"); - if (p.peekNextToken("vertex")) - api.parameter("uvs", "texcoord", "vertex", parseFloatArray(np * 2)); - else if (p.peekNextToken("facevarying")) - api.parameter("uvs", "texcoord", "facevarying", parseFloatArray(nt * 6)); - else - p.checkNextToken("none"); - if (p.peekNextToken("face_shaders")) - api.parameter("faceshaders", parseIntArray(nt)); - api.geometry(name, "triangle_mesh"); - } else if (type.equals("hair")) { - UI.printInfo(Module.API, "Reading hair curves: %s ... ", name); - p.checkNextToken("segments"); - api.parameter("segments", p.getNextInt()); - p.checkNextToken("width"); - api.parameter("widths", p.getNextFloat()); - p.checkNextToken("points"); - api.parameter("points", "point", "vertex", parseFloatArray(p.getNextInt())); - api.geometry(name, "hair"); - } else if (type.equals("janino-tesselatable")) { - UI.printInfo(Module.API, "Reading procedural primitive: %s ... ", name); - String typename = p.peekNextToken("typename") ? p.getNextToken() : PluginRegistry.shaderPlugins.generateUniqueName("janino_shader"); - if (!PluginRegistry.tesselatablePlugins.registerPlugin(typename, p.getNextCodeBlock())) - noInstance = true; - else - api.geometry(name, typename); - } else if (type.equals("teapot")) { - UI.printInfo(Module.API, "Reading teapot: %s ... ", name); - if (p.peekNextToken("subdivs")) - api.parameter("subdivs", p.getNextInt()); - if (p.peekNextToken("smooth")) - api.parameter("smooth", p.getNextBoolean()); - api.geometry(name, "teapot"); - } else if (type.equals("gumbo")) { - UI.printInfo(Module.API, "Reading gumbo: %s ... ", name); - if (p.peekNextToken("subdivs")) - api.parameter("subdivs", p.getNextInt()); - if (p.peekNextToken("smooth")) - api.parameter("smooth", p.getNextBoolean()); - api.geometry(name, "gumbo"); - } else if (type.equals("julia")) { - UI.printInfo(Module.API, "Reading julia fractal: %s ... ", name); - if (p.peekNextToken("q")) { - api.parameter("cw", p.getNextFloat()); - api.parameter("cx", p.getNextFloat()); - api.parameter("cy", p.getNextFloat()); - api.parameter("cz", p.getNextFloat()); - } - if (p.peekNextToken("iterations")) - api.parameter("iterations", p.getNextInt()); - if (p.peekNextToken("epsilon")) - api.parameter("epsilon", p.getNextFloat()); - api.geometry(name, "julia"); - } else if (type.equals("particles") || type.equals("dlasurface")) { - if (type.equals("dlasurface")) - UI.printWarning(Module.API, "Deprecated object type: \"dlasurface\" - please use \"particles\" instead"); - float[] data; - if (p.peekNextToken("filename")) { - // FIXME: this code should be moved into an on demand loading - // primitive - String filename = p.getNextToken(); - boolean littleEndian = false; - if (p.peekNextToken("little_endian")) - littleEndian = true; - UI.printInfo(Module.USER, "Loading particle file: %s", filename); - File file = new File(filename); - FileInputStream stream = new FileInputStream(filename); - MappedByteBuffer map = stream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()); - if (littleEndian) - map.order(ByteOrder.LITTLE_ENDIAN); - FloatBuffer buffer = map.asFloatBuffer(); - data = new float[buffer.capacity()]; - for (int i = 0; i < data.length; i++) - data[i] = buffer.get(i); - stream.close(); - } else { - p.checkNextToken("points"); - int n = p.getNextInt(); - data = parseFloatArray(n * 3); // read 3n points - } - api.parameter("particles", "point", "vertex", data); - if (p.peekNextToken("num")) - api.parameter("num", p.getNextInt()); - else - api.parameter("num", data.length / 3); - p.checkNextToken("radius"); - api.parameter("radius", p.getNextFloat()); - api.geometry(name, "particles"); - } else if (type.equals("file-mesh")) { - UI.printInfo(Module.API, "Reading file mesh: %s ... ", name); - p.checkNextToken("filename"); - api.parameter("filename", p.getNextToken()); - if (p.peekNextToken("smooth_normals")) - api.parameter("smooth_normals", p.getNextBoolean()); - api.geometry(name, "file_mesh"); - } else if (type.equals("bezier-mesh")) { - UI.printInfo(Module.API, "Reading bezier mesh: %s ... ", name); - p.checkNextToken("n"); - int nu, nv; - api.parameter("nu", nu = p.getNextInt()); - api.parameter("nv", nv = p.getNextInt()); - if (p.peekNextToken("wrap")) { - api.parameter("uwrap", p.getNextBoolean()); - api.parameter("vwrap", p.getNextBoolean()); - } - p.checkNextToken("points"); - float[] points = new float[3 * nu * nv]; - for (int i = 0; i < points.length; i++) - points[i] = p.getNextFloat(); - api.parameter("points", "point", "vertex", points); - if (p.peekNextToken("subdivs")) - api.parameter("subdivs", p.getNextInt()); - if (p.peekNextToken("smooth")) - api.parameter("smooth", p.getNextBoolean()); - api.geometry(name, "bezier_mesh"); - } else { - UI.printWarning(Module.API, "Unrecognized object type: %s", p.getNextToken()); - noInstance = true; - } - if (!noInstance) { - // create instance - api.parameter("shaders", shaders); - if (modifiers != null) - api.parameter("modifiers", modifiers); - if (transform != null && transform.length > 0) { - if (transform.length == 1) - api.parameter("transform", transform[0]); - else { - api.parameter("transform.steps", transform.length); - api.parameter("transform.times", "float", "none", new float[] { - transformTime0, transformTime1 }); - for (int i = 0; i < transform.length; i++) - api.parameter(String.format("transform[%d]", i), transform[i]); - } - } - api.instance(name + ".instance", name); - } - p.checkNextToken("}"); - } - - private void parseInstanceBlock(SunflowAPIInterface api) throws ParserException, IOException { - p.checkNextToken("{"); - p.checkNextToken("name"); - String name = p.getNextToken(); - UI.printInfo(Module.API, "Reading instance: %s ...", name); - p.checkNextToken("geometry"); - String geoname = p.getNextToken(); - p.checkNextToken("transform"); - if (p.peekNextToken("steps")) { - int n = p.getNextInt(); - api.parameter("transform.steps", n); - p.checkNextToken("times"); - float[] times = new float[2]; - times[0] = p.getNextFloat(); - times[1] = p.getNextFloat(); - api.parameter("transform.times", "float", "none", times); - for (int i = 0; i < n; i++) - api.parameter(String.format("transform[%d]", i), parseMatrix()); - } else - api.parameter("transform", parseMatrix()); - String[] shaders; - if (p.peekNextToken("shaders")) { - int n = p.getNextInt(); - shaders = new String[n]; - for (int i = 0; i < n; i++) - shaders[i] = p.getNextToken(); - } else { - p.checkNextToken("shader"); - shaders = new String[] { p.getNextToken() }; - } - api.parameter("shaders", shaders); - String[] modifiers = null; - if (p.peekNextToken("modifiers")) { - int n = p.getNextInt(); - modifiers = new String[n]; - for (int i = 0; i < n; i++) - modifiers[i] = p.getNextToken(); - } else if (p.peekNextToken("modifier")) - modifiers = new String[] { p.getNextToken() }; - if (modifiers != null) - api.parameter("modifiers", modifiers); - api.instance(name, geoname); - p.checkNextToken("}"); - } - - private void parseLightBlock(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { - p.checkNextToken("{"); - p.checkNextToken("type"); - if (p.peekNextToken("mesh")) { - UI.printWarning(Module.API, "Deprecated light type: mesh"); - p.checkNextToken("name"); - String name = p.getNextToken(); - UI.printInfo(Module.API, "Reading light mesh: %s ...", name); - p.checkNextToken("emit"); - api.parameter("radiance", null, parseColor().getRGB()); - int samples = numLightSamples; - if (p.peekNextToken("samples")) - samples = p.getNextInt(); - else - UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", samples); - api.parameter("samples", samples); - int numVertices = p.getNextInt(); - int numTriangles = p.getNextInt(); - float[] points = new float[3 * numVertices]; - int[] triangles = new int[3 * numTriangles]; - for (int i = 0; i < numVertices; i++) { - p.checkNextToken("v"); - points[3 * i + 0] = p.getNextFloat(); - points[3 * i + 1] = p.getNextFloat(); - points[3 * i + 2] = p.getNextFloat(); - // ignored - p.getNextFloat(); - p.getNextFloat(); - p.getNextFloat(); - p.getNextFloat(); - p.getNextFloat(); - } - for (int i = 0; i < numTriangles; i++) { - p.checkNextToken("t"); - triangles[3 * i + 0] = p.getNextInt(); - triangles[3 * i + 1] = p.getNextInt(); - triangles[3 * i + 2] = p.getNextInt(); - } - api.parameter("points", "point", "vertex", points); - api.parameter("triangles", triangles); - api.light(name, "triangle_mesh"); - } else if (p.peekNextToken("point")) { - UI.printInfo(Module.API, "Reading point light ..."); - Color pow; - if (p.peekNextToken("color")) { - pow = parseColor(); - p.checkNextToken("power"); - float po = p.getNextFloat(); - pow.mul(po); - } else { - UI.printWarning(Module.API, "Deprecated color specification - please use color and power instead"); - p.checkNextToken("power"); - pow = parseColor(); - } - p.checkNextToken("p"); - api.parameter("center", parsePoint()); - api.parameter("power", null, pow.getRGB()); - api.light(generateUniqueName("pointlight"), "point"); - } else if (p.peekNextToken("spherical")) { - UI.printInfo(Module.API, "Reading spherical light ..."); - p.checkNextToken("color"); - Color pow = parseColor(); - p.checkNextToken("radiance"); - pow.mul(p.getNextFloat()); - api.parameter("radiance", null, pow.getRGB()); - p.checkNextToken("center"); - api.parameter("center", parsePoint()); - p.checkNextToken("radius"); - api.parameter("radius", p.getNextFloat()); - p.checkNextToken("samples"); - api.parameter("samples", p.getNextInt()); - api.light(generateUniqueName("spherelight"), "sphere"); - } else if (p.peekNextToken("directional")) { - UI.printInfo(Module.API, "Reading directional light ..."); - p.checkNextToken("source"); - Point3 s = parsePoint(); - api.parameter("source", s); - p.checkNextToken("target"); - Point3 t = parsePoint(); - api.parameter("dir", Point3.sub(t, s, new Vector3())); - p.checkNextToken("radius"); - api.parameter("radius", p.getNextFloat()); - p.checkNextToken("emit"); - Color e = parseColor(); - if (p.peekNextToken("intensity")) { - float i = p.getNextFloat(); - e.mul(i); - } else - UI.printWarning(Module.API, "Deprecated color specification - please use emit and intensity instead"); - api.parameter("radiance", null, e.getRGB()); - api.light(generateUniqueName("dirlight"), "directional"); - } else if (p.peekNextToken("ibl")) { - UI.printInfo(Module.API, "Reading image based light ..."); - p.checkNextToken("image"); - api.parameter("texture", p.getNextToken()); - p.checkNextToken("center"); - api.parameter("center", parseVector()); - p.checkNextToken("up"); - api.parameter("up", parseVector()); - p.checkNextToken("lock"); - api.parameter("fixed", p.getNextBoolean()); - int samples = numLightSamples; - if (p.peekNextToken("samples")) - samples = p.getNextInt(); - else - UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", samples); - api.parameter("samples", samples); - if (p.peekNextToken("lowsamples")) - api.parameter("lowsamples", p.getNextInt()); - else - api.parameter("lowsamples", samples); - api.light(generateUniqueName("ibl"), "ibl"); - } else if (p.peekNextToken("meshlight")) { - p.checkNextToken("name"); - String name = p.getNextToken(); - UI.printInfo(Module.API, "Reading meshlight: %s ...", name); - p.checkNextToken("emit"); - Color e = parseColor(); - if (p.peekNextToken("radiance")) { - float r = p.getNextFloat(); - e.mul(r); - } else - UI.printWarning(Module.API, "Deprecated color specification - please use emit and radiance instead"); - api.parameter("radiance", null, e.getRGB()); - int samples = numLightSamples; - if (p.peekNextToken("samples")) - samples = p.getNextInt(); - else - UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", samples); - api.parameter("samples", samples); - // parse vertices - p.checkNextToken("points"); - int np = p.getNextInt(); - api.parameter("points", "point", "vertex", parseFloatArray(np * 3)); - // parse triangle indices - p.checkNextToken("triangles"); - int nt = p.getNextInt(); - api.parameter("triangles", parseIntArray(nt * 3)); - api.light(name, "triangle_mesh"); - } else if (p.peekNextToken("sunsky")) { - p.checkNextToken("up"); - api.parameter("up", parseVector()); - p.checkNextToken("east"); - api.parameter("east", parseVector()); - p.checkNextToken("sundir"); - api.parameter("sundir", parseVector()); - p.checkNextToken("turbidity"); - api.parameter("turbidity", p.getNextFloat()); - if (p.peekNextToken("samples")) - api.parameter("samples", p.getNextInt()); - if (p.peekNextToken("ground.extendsky")) - api.parameter("ground.extendsky", p.getNextBoolean()); - else if (p.peekNextToken("ground.color")) - api.parameter("ground.color", null, parseColor().getRGB()); - api.light(generateUniqueName("sunsky"), "sunsky"); - } else if (p.peekNextToken("cornellbox")) { - UI.printInfo(Module.API, "Reading cornell box ..."); - p.checkNextToken("corner0"); - api.parameter("corner0", parsePoint()); - p.checkNextToken("corner1"); - api.parameter("corner1", parsePoint()); - p.checkNextToken("left"); - api.parameter("leftColor", null, parseColor().getRGB()); - p.checkNextToken("right"); - api.parameter("rightColor", null, parseColor().getRGB()); - p.checkNextToken("top"); - api.parameter("topColor", null, parseColor().getRGB()); - p.checkNextToken("bottom"); - api.parameter("bottomColor", null, parseColor().getRGB()); - p.checkNextToken("back"); - api.parameter("backColor", null, parseColor().getRGB()); - p.checkNextToken("emit"); - api.parameter("radiance", null, parseColor().getRGB()); - if (p.peekNextToken("samples")) - api.parameter("samples", p.getNextInt()); - api.light(generateUniqueName("cornellbox"), "cornell_box"); - } else - UI.printWarning(Module.API, "Unrecognized object type: %s", p.getNextToken()); - p.checkNextToken("}"); - } - - private Color parseColor() throws IOException, ParserException, ColorSpecificationException { - if (p.peekNextToken("{")) { - String space = p.getNextToken(); - int req = ColorFactory.getRequiredDataValues(space); - if (req == -2) { - UI.printWarning(Module.API, "Unrecognized color space: %s", space); - return null; - } else if (req == -1) { - // array required, parse how many values are required - req = p.getNextInt(); - } - Color c = ColorFactory.createColor(space, parseFloatArray(req)); - p.checkNextToken("}"); - return c; - } else { - float r = p.getNextFloat(); - float g = p.getNextFloat(); - float b = p.getNextFloat(); - return ColorFactory.createColor(null, r, g, b); - } - } - - private Point3 parsePoint() throws IOException { - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - return new Point3(x, y, z); - } - - private Vector3 parseVector() throws IOException { - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - return new Vector3(x, y, z); - } - - private int[] parseIntArray(int size) throws IOException { - int[] data = new int[size]; - for (int i = 0; i < size; i++) - data[i] = p.getNextInt(); - return data; - } - - private float[] parseFloatArray(int size) throws IOException { - float[] data = new float[size]; - for (int i = 0; i < size; i++) - data[i] = p.getNextFloat(); - return data; - } - - private Matrix4 parseMatrix() throws IOException, ParserException { - if (p.peekNextToken("row")) { - return new Matrix4(parseFloatArray(16), true); - } else if (p.peekNextToken("col")) { - return new Matrix4(parseFloatArray(16), false); - } else { - Matrix4 m = Matrix4.IDENTITY; - p.checkNextToken("{"); - while (!p.peekNextToken("}")) { - Matrix4 t = null; - if (p.peekNextToken("translate")) { - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - t = Matrix4.translation(x, y, z); - } else if (p.peekNextToken("scaleu")) { - float s = p.getNextFloat(); - t = Matrix4.scale(s); - } else if (p.peekNextToken("scale")) { - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - t = Matrix4.scale(x, y, z); - } else if (p.peekNextToken("rotatex")) { - float angle = p.getNextFloat(); - t = Matrix4.rotateX((float) Math.toRadians(angle)); - } else if (p.peekNextToken("rotatey")) { - float angle = p.getNextFloat(); - t = Matrix4.rotateY((float) Math.toRadians(angle)); - } else if (p.peekNextToken("rotatez")) { - float angle = p.getNextFloat(); - t = Matrix4.rotateZ((float) Math.toRadians(angle)); - } else if (p.peekNextToken("rotate")) { - float x = p.getNextFloat(); - float y = p.getNextFloat(); - float z = p.getNextFloat(); - float angle = p.getNextFloat(); - t = Matrix4.rotate(x, y, z, (float) Math.toRadians(angle)); - } else - UI.printWarning(Module.API, "Unrecognized transformation type: %s", p.getNextToken()); - if (t != null) - m = t.multiply(m); - } - return m; - } - } +package org.sunflow.core.parser; + +import org.sunflow.PluginRegistry; +import org.sunflow.SunflowAPI; +import org.sunflow.SunflowAPIInterface; +import org.sunflow.core.SceneParser; +import org.sunflow.core.parameter.*; +import org.sunflow.core.parameter.camera.*; +import org.sunflow.core.parameter.geometry.*; +import org.sunflow.core.parameter.gi.*; +import org.sunflow.core.parameter.light.*; +import org.sunflow.core.parameter.modifier.BumpMapModifierParameter; +import org.sunflow.core.parameter.modifier.NormalMapModifierParameter; +import org.sunflow.core.parameter.modifier.PerlinModifierParameter; +import org.sunflow.core.parameter.shader.*; +import org.sunflow.image.Color; +import org.sunflow.image.ColorFactory; +import org.sunflow.image.ColorFactory.ColorSpecificationException; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; +import org.sunflow.system.Parser; +import org.sunflow.system.Parser.ParserException; +import org.sunflow.system.Timer; +import org.sunflow.system.UI; +import org.sunflow.system.UI.Module; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.HashMap; + +/** + * This class provides a static method for loading files in the Sunflow scene + * file format. + */ +public class SCParser implements SceneParser { + private static int instanceCounter = 0; + private int instanceNumber; + private Parser p; + private int numLightSamples; + // used to generate unique names inside this parser + private HashMap objectNames; + + public SCParser() { + objectNames = new HashMap(); + instanceCounter++; + instanceNumber = instanceCounter; + } + + private String generateUniqueName(String prefix) { + // generate a unique name for this class: + int index = 1; + Integer value = objectNames.get(prefix); + if (value != null) { + index = value; + objectNames.put(prefix, index + 1); + } else { + objectNames.put(prefix, index + 1); + } + return String.format("@sc_%d::%s_%d", instanceNumber, prefix, index); + } + + public boolean parse(String filename, SunflowAPIInterface api) { + String localDir = new File(filename).getAbsoluteFile().getParentFile().getAbsolutePath(); + numLightSamples = 1; + Timer timer = new Timer(); + timer.start(); + UI.printInfo(Module.API, "Parsing \"%s\" ...", filename); + try { + p = new Parser(filename); + while (true) { + String token = p.getNextToken(); + if (token == null) + break; + if (token.equals("image")) { + UI.printInfo(Module.API, "Reading image settings ..."); + parseImageBlock(api); + } else if (token.equals("background")) { + UI.printInfo(Module.API, "Reading background ..."); + parseBackgroundBlock(api); + } else if (token.equals("accel")) { + UI.printInfo(Module.API, "Reading accelerator type ..."); + p.getNextToken(); + UI.printWarning(Module.API, "Setting accelerator type is not recommended - ignoring"); + } else if (token.equals("filter")) { + // Deprecated + UI.printInfo(Module.API, "Reading image filter type ..."); + parseFilter(api); + } else if (token.equals("bucket")) { + UI.printInfo(Module.API, "Reading bucket settings ..."); + parseBucketBlock(api); + } else if (token.equals("photons")) { + UI.printInfo(Module.API, "Reading photon settings ..."); + parsePhotonBlock(api); + } else if (token.equals("gi")) { + UI.printInfo(Module.API, "Reading global illumination settings ..."); + parseGIBlock(api); + } else if (token.equals("lightserver")) { + // Deprecated + UI.printInfo(Module.API, "Reading light server settings ..."); + parseLightserverBlock(api); + } else if (token.equals("trace-depths")) { + UI.printInfo(Module.API, "Reading trace depths ..."); + parseTraceBlock(api); + } else if (token.equals("camera")) { + parseCamera(api); + } else if (token.equals("shader")) { + if (!parseShader(api)) { + // Close before return + p.close(); + return false; + } + } else if (token.equals("modifier")) { + if (!parseModifier(api)) { + // Close before return + p.close(); + return false; + } + } else if (token.equals("override")) { + parseOverrideBlock(api); + } else if (token.equals("object")) { + parseObjectBlock(api); + } else if (token.equals("instance")) { + parseInstanceBlock(api); + } else if (token.equals("light")) { + parseLightBlock(api); + } else if (token.equals("texturepath")) { + String path = p.getNextToken(); + if (!new File(path).isAbsolute()) { + path = localDir + File.separator + path; + } + api.searchpath("texture", path); + } else if (token.equals("includepath")) { + String path = p.getNextToken(); + if (!new File(path).isAbsolute()) { + path = localDir + File.separator + path; + } + api.searchpath("include", path); + } else if (token.equals("include")) { + String file = p.getNextToken(); + UI.printInfo(Module.API, "Including: \"%s\" ...", file); + api.include(file); + } else + UI.printWarning(Module.API, "Unrecognized token %s", token); + } + p.close(); + } catch (ParserException e) { + UI.printError(Module.API, "%s", e.getMessage()); + e.printStackTrace(); + return false; + } catch (FileNotFoundException e) { + UI.printError(Module.API, "%s", e.getMessage()); + return false; + } catch (IOException e) { + UI.printError(Module.API, "%s", e.getMessage()); + return false; + } catch (ColorSpecificationException e) { + UI.printError(Module.API, "%s", e.getMessage()); + return false; + } + timer.end(); + UI.printInfo(Module.API, "Done parsing."); + UI.printInfo(Module.API, "Parsing time: %s", timer.toString()); + return true; + } + + private void parseBucketBlock(SunflowAPIInterface api) throws IOException { + BucketParameter bucket = new BucketParameter(); + bucket.setSize(p.getNextInt()); + bucket.setOrder(p.getNextToken()); + bucket.setup(api); + } + + private void parseImageBlock(SunflowAPIInterface api) throws IOException, ParserException { + ImageParameter image = new ImageParameter(); + + p.checkNextToken("{"); + if (p.peekNextToken("resolution")) { + image.setResolutionX(p.getNextInt()); + image.setResolutionY(p.getNextInt()); + } + if (p.peekNextToken("sampler")) + image.setSampler(p.getNextToken()); + if (p.peekNextToken("aa")) { + image.setAAMin(p.getNextInt()); + image.setAAMax(p.getNextInt()); + } + if (p.peekNextToken("samples")) { + image.setAASamples(p.getNextInt()); + } + if (p.peekNextToken("contrast")) { + image.setAAContrast(p.getNextFloat()); + } + if (p.peekNextToken("filter")) { + image.setFilter(p.getNextToken()); + } + if (p.peekNextToken("jitter")) { + image.setAAJitter(p.getNextBoolean()); + } + if (p.peekNextToken("show-aa")) { + UI.printWarning(Module.API, "Deprecated: show-aa ignored"); + p.getNextBoolean(); + } + if (p.peekNextToken("cache")) { + image.setAACache(p.getNextBoolean()); + } + if (p.peekNextToken("output")) { + UI.printWarning(Module.API, "Deprecated: output statement ignored"); + p.getNextToken(); + } + + image.setup(api); + p.checkNextToken("}"); + } + + private void parseBackgroundBlock(SunflowAPIInterface api) throws IOException, ParserException, ColorSpecificationException { + BackgroundParameter background = new BackgroundParameter(); + + p.checkNextToken("{"); + p.checkNextToken("color"); + + background.setColor(parseColor()); + background.setup(api); + + p.checkNextToken("}"); + } + + private void parseFilter(SunflowAPIInterface api) throws IOException, ParserException { + UI.printWarning(Module.API, "Deprecated keyword \"filter\" - set this option in the image block"); + String name = p.getNextToken(); + api.parameter("filter", name); + api.options(SunflowAPI.DEFAULT_OPTIONS); + boolean hasSizeParams = name.equals("box") || name.equals("gaussian") || name.equals("blackman-harris") || name.equals("sinc") || name.equals("triangle"); + if (hasSizeParams) { + p.getNextFloat(); + p.getNextFloat(); + } + } + + private void parsePhotonBlock(SunflowAPIInterface api) throws ParserException, IOException { + PhotonParameter photon = new PhotonParameter(); + + boolean globalEmit = false; + + p.checkNextToken("{"); + if (p.peekNextToken("emit")) { + UI.printWarning(Module.API, "Shared photon emit values are deprecated - specify number of photons to emit per map"); + photon.setNumEmit(p.getNextInt()); + globalEmit = true; + } + if (p.peekNextToken("global")) { + UI.printWarning(Module.API, "Global photon map setting belongs inside the gi block - ignoring"); + if (!globalEmit) { + p.getNextInt(); + } + p.getNextToken(); + p.getNextInt(); + p.getNextFloat(); + } + p.checkNextToken("caustics"); + if (!globalEmit) { + photon.setNumEmit(p.getNextInt()); + } + + photon.setCaustics(p.getNextToken()); + photon.setCausticsGather(p.getNextInt()); + photon.setCausticsRadius(p.getNextFloat()); + + photon.setup(api); + p.checkNextToken("}"); + } + + private void parseGIBlock(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { + p.checkNextToken("{"); + p.checkNextToken("type"); + if (p.peekNextToken("irr-cache")) { + IrrCacheGIParameter gi = new IrrCacheGIParameter(); + + p.checkNextToken("samples"); + gi.setSamples(p.getNextInt()); + p.checkNextToken("tolerance"); + gi.setTolerance(p.getNextFloat()); + p.checkNextToken("spacing"); + gi.setMinSpacing(p.getNextFloat()); + gi.setMaxSpacing(p.getNextFloat()); + + // parse global photon map info + if (p.peekNextToken("global")) { + gi.setGlobal(new IlluminationParameter()); + gi.getGlobal().setEmit(p.getNextInt()); + gi.getGlobal().setMap(p.getNextToken()); + gi.getGlobal().setGather(p.getNextInt()); + gi.getGlobal().setRadius(p.getNextFloat()); + } + gi.setup(api); + } else if (p.peekNextToken("path")) { + PathTracingGIParameter gi = new PathTracingGIParameter(); + + p.checkNextToken("samples"); + gi.setSamples(p.getNextInt()); + + if (p.peekNextToken("bounces")) { + UI.printWarning(Module.API, "Deprecated setting: bounces - use diffuse trace depth instead"); + p.getNextInt(); + } + gi.setup(api); + } else if (p.peekNextToken("fake")) { + FakeGIParameter gi = new FakeGIParameter(); + + p.checkNextToken("up"); + gi.setUp(parseVector()); + p.checkNextToken("sky"); + gi.setSky(parseColor()); + p.checkNextToken("ground"); + gi.setGround(parseColor()); + + gi.setup(api); + } else if (p.peekNextToken("igi")) { + InstantGIParameter gi = new InstantGIParameter(); + + p.checkNextToken("samples"); + gi.setSamples(p.getNextInt()); + p.checkNextToken("sets"); + gi.setSets(p.getNextInt()); + + if (!p.peekNextToken("b")) { + p.checkNextToken("c"); + } + gi.setBias(p.getNextFloat()); + + p.checkNextToken("bias-samples"); + gi.setBiasSamples(p.getNextInt()); + + gi.setup(api); + } else if (p.peekNextToken("ambocc")) { + AmbientOcclusionGIParameter gi = new AmbientOcclusionGIParameter(); + + p.checkNextToken("bright"); + gi.setBright(parseColor()); + p.checkNextToken("dark"); + gi.setDark(parseColor()); + p.checkNextToken("samples"); + gi.setSamples(p.getNextInt()); + if (p.peekNextToken("maxdist")) { + gi.setMaxDist(p.getNextFloat()); + } + + gi.setup(api); + } else if (p.peekNextToken("none") || p.peekNextToken("null")) { + DisabledGIParameter gi = new DisabledGIParameter(); + // disable GI + gi.setup(api); + } else { + UI.printWarning(Module.API, "Unrecognized gi engine type \"%s\" - ignoring", p.getNextToken()); + } + + p.checkNextToken("}"); + } + + private void parseOverrideBlock(SunflowAPIInterface api) throws IOException { + OverrideParameter block = new OverrideParameter(); + + block.setShader(p.getNextToken()); + block.setPhotons(p.getNextBoolean()); + block.setup(api); + } + + private void parseLightserverBlock(SunflowAPIInterface api) throws ParserException, IOException { + p.checkNextToken("{"); + if (p.peekNextToken("shadows")) { + UI.printWarning(Module.API, "Deprecated: shadows setting ignored"); + p.getNextBoolean(); + } + if (p.peekNextToken("direct-samples")) { + UI.printWarning(Module.API, "Deprecated: use samples keyword in area light definitions"); + numLightSamples = p.getNextInt(); + } + if (p.peekNextToken("glossy-samples")) { + UI.printWarning(Module.API, "Deprecated: use samples keyword in glossy shader definitions"); + p.getNextInt(); + } + if (p.peekNextToken("max-depth")) { + UI.printWarning(Module.API, "Deprecated: max-depth setting - use trace-depths block instead"); + int d = p.getNextInt(); + api.parameter("depths.diffuse", 1); + api.parameter("depths.reflection", d - 1); + api.parameter("depths.refraction", 0); + api.options(SunflowAPI.DEFAULT_OPTIONS); + } + if (p.peekNextToken("global")) { + UI.printWarning(Module.API, "Deprecated: global settings ignored - use photons block instead"); + p.getNextBoolean(); + p.getNextInt(); + p.getNextInt(); + p.getNextInt(); + p.getNextFloat(); + } + if (p.peekNextToken("caustics")) { + UI.printWarning(Module.API, "Deprecated: caustics settings ignored - use photons block instead"); + p.getNextBoolean(); + p.getNextInt(); + p.getNextFloat(); + p.getNextInt(); + p.getNextFloat(); + } + if (p.peekNextToken("irr-cache")) { + UI.printWarning(Module.API, "Deprecated: irradiance cache settings ignored - use gi block instead"); + p.getNextInt(); + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + } + p.checkNextToken("}"); + } + + private void parseTraceBlock(SunflowAPIInterface api) throws ParserException, IOException { + TraceDepthsParameter traceDepthsParameter = new TraceDepthsParameter(); + p.checkNextToken("{"); + if (p.peekNextToken("diff")) { + traceDepthsParameter.setDiffuse(p.getNextInt()); + } + if (p.peekNextToken("refl")) { + traceDepthsParameter.setReflection(p.getNextInt()); + } + if (p.peekNextToken("refr")) { + traceDepthsParameter.setRefraction(p.getNextInt()); + } + p.checkNextToken("}"); + + traceDepthsParameter.setup(api); + api.options(SunflowAPI.DEFAULT_OPTIONS); + } + + private void parseCamera(SunflowAPIInterface api) throws ParserException, IOException { + p.checkNextToken("{"); + p.checkNextToken("type"); + String type = p.getNextToken(); + UI.printInfo(Module.API, "Reading %s camera ...", type); + + float shutterOpen = 0, shutterClose = 0; + + if (p.peekNextToken("shutter")) { + shutterOpen = p.getNextFloat(); + shutterClose = p.getNextFloat(); + } + parseCameraTransform(api); + String name = generateUniqueName("camera"); + if (type.equals(CameraParameter.TYPE_PINHOLE)) { + PinholeCameraParameter camera = new PinholeCameraParameter(); + camera.setName(name); + camera.setShutterOpen(shutterOpen); + camera.setShutterClose(shutterClose); + + p.checkNextToken("fov"); + camera.setFov(p.getNextFloat()); + p.checkNextToken("aspect"); + camera.setAspect(p.getNextFloat()); + + if (p.peekNextToken("shift")) { + camera.setShiftX(p.getNextFloat()); + camera.setShiftY(p.getNextFloat()); + } + camera.setup(api); + } else if (type.equals(CameraParameter.TYPE_THINLENS)) { + ThinLensCameraParameter camera = new ThinLensCameraParameter(); + camera.setName(name); + camera.setShutterOpen(shutterOpen); + camera.setShutterClose(shutterClose); + + p.checkNextToken("fov"); + camera.setFov(p.getNextFloat()); + p.checkNextToken("aspect"); + camera.setAspect(p.getNextFloat()); + if (p.peekNextToken("shift")) { + camera.setShiftX(p.getNextFloat()); + camera.setShiftY(p.getNextFloat()); + } + p.checkNextToken("fdist"); + camera.setFocusDistance(p.getNextFloat()); + p.checkNextToken("lensr"); + camera.setLensRadius(p.getNextFloat()); + if (p.peekNextToken("sides")) { + camera.setLensSides(p.getNextInt()); + } + if (p.peekNextToken("rotation")) { + camera.setLensRotation(p.getNextFloat()); + } + camera.setup(api); + } else if (type.equals(CameraParameter.TYPE_SPHERICAL)) { + SphericalCameraParameter camera = new SphericalCameraParameter(); + camera.setName(name); + camera.setShutterOpen(shutterOpen); + camera.setShutterClose(shutterClose); + // no extra arguments + camera.setup(api); + } else if (type.equals(CameraParameter.TYPE_FISH_EYE)) { + FishEyeCameraParameter camera = new FishEyeCameraParameter(); + camera.setName(name); + camera.setShutterOpen(shutterOpen); + camera.setShutterClose(shutterClose); + // no extra arguments + camera.setup(api); + } else { + UI.printWarning(Module.API, "Unrecognized camera type: %s", p.getNextToken()); + p.checkNextToken("}"); + return; + } + p.checkNextToken("}"); + if (name != null) { + api.parameter("camera", name); + api.options(SunflowAPI.DEFAULT_OPTIONS); + } + } + + private void parseCameraTransform(SunflowAPIInterface api) throws ParserException, IOException { + if (p.peekNextToken("steps")) { + // motion blur camera + int n = p.getNextInt(); + api.parameter("transform.steps", n); + // parse time extents + p.checkNextToken("times"); + float t0 = p.getNextFloat(); + float t1 = p.getNextFloat(); + api.parameter("transform.times", "float", "none", new float[]{t0, t1}); + for (int i = 0; i < n; i++) { + parseCameraMatrix(i, api); + } + } else { + parseCameraMatrix(-1, api); + } + } + + private void parseCameraMatrix(int index, SunflowAPIInterface api) throws IOException, ParserException { + String offset = index < 0 ? "" : String.format("[%d]", index); + if (p.peekNextToken("transform")) { + // advanced camera + api.parameter(String.format("transform%s", offset), parseMatrix()); + } else { + if (index >= 0) { + p.checkNextToken("{"); + } + // regular camera specification + p.checkNextToken("eye"); + Point3 eye = parsePoint(); + p.checkNextToken("target"); + Point3 target = parsePoint(); + p.checkNextToken("up"); + Vector3 up = parseVector(); + api.parameter(String.format("transform%s", offset), Matrix4.lookAt(eye, target, up)); + if (index >= 0) { + p.checkNextToken("}"); + } + } + } + + private boolean parseShader(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { + p.checkNextToken("{"); + p.checkNextToken("name"); + String name = p.getNextToken(); + UI.printInfo(Module.API, "Reading shader: %s ...", name); + p.checkNextToken("type"); + if (p.peekNextToken("diffuse")) { + DiffuseShaderParameter shader = new DiffuseShaderParameter(name); + + if (p.peekNextToken("diff")) { + shader.setDiffuse(parseColor()); + } else if (p.peekNextToken("texture")) { + shader.setTexture(p.getNextToken()); + } else { + UI.printWarning(Module.API, "Unrecognized option in diffuse shader block: %s", p.getNextToken()); + } + shader.setup(api); + } else if (p.peekNextToken("phong")) { + PhongShaderParameter shaderParameter = new PhongShaderParameter(name); + if (p.peekNextToken("texture")) { + shaderParameter.setTexture(p.getNextToken()); + } else { + p.checkNextToken("diff"); + shaderParameter.setDiffuse(parseColor()); + } + p.checkNextToken("spec"); + shaderParameter.setSpecular(parseColor()); + shaderParameter.setPower(p.getNextFloat()); + + if (p.peekNextToken("samples")) { + shaderParameter.setSamples(p.getNextInt()); + } + shaderParameter.setup(api); + } else if (p.peekNextToken("amb-occ") || p.peekNextToken("amb-occ2")) { + AmbientOcclusionShaderParameter shaderParameter = new AmbientOcclusionShaderParameter(name); + + if (p.peekNextToken("diff") || p.peekNextToken("bright")) { + shaderParameter.setBright(parseColor()); + } else if (p.peekNextToken("texture")) { + shaderParameter.setTexture(p.getNextToken()); + } + + if (p.peekNextToken("dark")) { + shaderParameter.setDark(parseColor()); + p.checkNextToken("samples"); + shaderParameter.setSamples(p.getNextInt()); + p.checkNextToken("dist"); + shaderParameter.setMaxDist(p.getNextFloat()); + } + shaderParameter.setup(api); + } else if (p.peekNextToken("mirror")) { + MirrorShaderParameter shaderParameter = new MirrorShaderParameter(name); + p.checkNextToken("refl"); + shaderParameter.setReflection(parseColor()); + shaderParameter.setup(api); + } else if (p.peekNextToken("glass")) { + GlassShaderParameter shaderParameter = new GlassShaderParameter(name); + + p.checkNextToken("eta"); + shaderParameter.setEta(p.getNextFloat()); + p.checkNextToken("color"); + shaderParameter.setColor(parseColor()); + + if (p.peekNextToken("absorption.distance") || p.peekNextToken("absorbtion.distance")) { + shaderParameter.setAbsorptionDistance(p.getNextFloat()); + } + if (p.peekNextToken("absorption.color") || p.peekNextToken("absorbtion.color")) { + shaderParameter.setAbsorptionColor(parseColor()); + } + shaderParameter.setup(api); + } else if (p.peekNextToken("shiny")) { + ShinyShaderParameter shaderParameter = new ShinyShaderParameter(name); + + if (p.peekNextToken("texture")) { + shaderParameter.setTexture(p.getNextToken()); + } else { + p.checkNextToken("diff"); + shaderParameter.setDiffuse(parseColor()); + } + p.checkNextToken("refl"); + shaderParameter.setShiny(p.getNextFloat()); + shaderParameter.setup(api); + } else if (p.peekNextToken("ward")) { + WardShaderParameter shaderParameter = new WardShaderParameter(name); + + if (p.peekNextToken("texture")) { + shaderParameter.setTexture(p.getNextToken()); + } else { + p.checkNextToken("diff"); + shaderParameter.setDiffuse(parseColor()); + } + p.checkNextToken("spec"); + shaderParameter.setSpecular(parseColor()); + + p.checkNextToken("rough"); + shaderParameter.setRoughnessX(p.getNextFloat()); + shaderParameter.setRoughnessY(p.getNextFloat()); + + if (p.peekNextToken("samples")) { + shaderParameter.setSamples(p.getNextInt()); + } + shaderParameter.setup(api); + } else if (p.peekNextToken("view-caustics")) { + ViewCausticsShaderParameter shaderParameter = new ViewCausticsShaderParameter(name); + shaderParameter.setup(api); + } else if (p.peekNextToken("view-irradiance")) { + ViewIrradianceShaderParameter shaderParameter = new ViewIrradianceShaderParameter(name); + shaderParameter.setup(api); + } else if (p.peekNextToken("view-global")) { + ViewGlobalShaderParameter shaderParameter = new ViewGlobalShaderParameter(name); + shaderParameter.setup(api); + } else if (p.peekNextToken("constant")) { + ConstantShaderParameter shaderParameter = new ConstantShaderParameter(name); + // backwards compatibility -- peek only + p.peekNextToken("color"); + shaderParameter.setColor(parseColor()); + shaderParameter.setup(api); + } else if (p.peekNextToken("janino")) { + String typename = p.peekNextToken("typename") ? p.getNextToken() : PluginRegistry.shaderPlugins.generateUniqueName("janino_shader"); + if (!PluginRegistry.shaderPlugins.registerPlugin(typename, p.getNextCodeBlock())) + return false; + api.shader(name, typename); + } else if (p.peekNextToken("id")) { + IDShaderParameter shaderParameter = new IDShaderParameter(name); + shaderParameter.setup(api); + } else if (p.peekNextToken("uber")) { + UberShaderParameter shaderParameter = new UberShaderParameter(name); + + if (p.peekNextToken("diff")) { + shaderParameter.setDiffuse(parseColor()); + } + if (p.peekNextToken("diff.texture")) { + shaderParameter.setDiffuseTexture(p.getNextToken()); + } + if (p.peekNextToken("diff.blend")) { + shaderParameter.setDiffuseBlend(p.getNextFloat()); + } + if (p.peekNextToken("refl") || p.peekNextToken("spec")) { + shaderParameter.setSpecular(parseColor()); + } + if (p.peekNextToken("texture")) { + // deprecated + UI.printWarning(Module.API, "Deprecated uber shader parameter \"texture\" - please use \"diffuse.texture\" and \"diffuse.blend\" instead"); + shaderParameter.setDiffuseTexture(p.getNextToken()); + shaderParameter.setDiffuseBlend(p.getNextFloat()); + } + if (p.peekNextToken("spec.texture")) { + shaderParameter.setSpecularTexture(p.getNextToken()); + } + if (p.peekNextToken("spec.blend")) { + shaderParameter.setSpecularBlend(p.getNextFloat()); + } + if (p.peekNextToken("glossy")) { + shaderParameter.setGlossyness(p.getNextFloat()); + } + if (p.peekNextToken("samples")) { + shaderParameter.setSamples(p.getNextInt()); + } + shaderParameter.setup(api); + } else { + UI.printWarning(Module.API, "Unrecognized shader type: %s", p.getNextToken()); + } + p.checkNextToken("}"); + return true; + } + + private boolean parseModifier(SunflowAPIInterface api) throws ParserException, IOException { + p.checkNextToken("{"); + p.checkNextToken("name"); + String name = p.getNextToken(); + UI.printInfo(Module.API, "Reading modifier: %s ...", name); + p.checkNextToken("type"); + if (p.peekNextToken("bump")) { + BumpMapModifierParameter parameter = new BumpMapModifierParameter(); + parameter.setName(name); + p.checkNextToken("texture"); + parameter.setTexture(p.getNextToken()); + p.checkNextToken("scale"); + parameter.setScale(p.getNextFloat()); + parameter.setup(api); + } else if (p.peekNextToken("normalmap")) { + NormalMapModifierParameter parameter = new NormalMapModifierParameter(); + parameter.setName(name); + p.checkNextToken("texture"); + parameter.setTexture(p.getNextToken()); + parameter.setup(api); + } else if (p.peekNextToken("perlin")) { + PerlinModifierParameter parameter = new PerlinModifierParameter(); + p.checkNextToken("function"); + parameter.setFunction(p.getNextInt()); + p.checkNextToken("size"); + parameter.setSize(p.getNextFloat()); + p.checkNextToken("scale"); + parameter.setScale(p.getNextFloat()); + parameter.setup(api); + } else { + UI.printWarning(Module.API, "Unrecognized modifier type: %s", p.getNextToken()); + } + p.checkNextToken("}"); + return true; + } + + private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, IOException { + p.checkNextToken("{"); + InstanceParameter instanceParameter = new InstanceParameter(); + + String name = ""; + String accel = ""; + + boolean noInstance = false; + //Matrix4[] transform = null; + //float transformTime0 = 0, transformTime1 = 0; + String[] shaders = null; + String[] modifiers = null; + if (p.peekNextToken("noinstance")) { + // this indicates that the geometry is to be created, but not + // instanced into the scene + noInstance = true; + } else { + // these are the parameters to be passed to the instance + if (p.peekNextToken("shaders")) { + int n = p.getNextInt(); + shaders = new String[n]; + for (int i = 0; i < n; i++) + shaders[i] = p.getNextToken(); + } else { + p.checkNextToken("shader"); + shaders = new String[]{p.getNextToken()}; + } + instanceParameter.setShaders(shaders); + + if (p.peekNextToken("modifiers")) { + int n = p.getNextInt(); + modifiers = new String[n]; + for (int i = 0; i < n; i++) + modifiers[i] = p.getNextToken(); + } else if (p.peekNextToken("modifier")) { + modifiers = new String[]{p.getNextToken()}; + } + instanceParameter.setModifiers(modifiers); + + // Can be null + TransformParameter transform = checkParseTransform(); + instanceParameter.setTransform(transform); + } + if (p.peekNextToken("accel")) { + accel = p.getNextToken(); + } + p.checkNextToken("type"); + String type = p.getNextToken(); + if (p.peekNextToken("name")) { + name = p.getNextToken(); + } else { + name = generateUniqueName(type); + } + + if (type.equals("mesh")) { + UI.printWarning(Module.API, "Deprecated object type: mesh"); + UI.printInfo(Module.API, "Reading mesh: %s ...", name); + + TriangleMeshParameter geometry = new TriangleMeshParameter(); + geometry.setName(name); + geometry.setAccel(accel); + geometry.setInstanceParameter(instanceParameter); + + int numVertices = p.getNextInt(); + int numTriangles = p.getNextInt(); + float[] points = new float[numVertices * 3]; + float[] normals = new float[numVertices * 3]; + float[] uvs = new float[numVertices * 2]; + for (int i = 0; i < numVertices; i++) { + p.checkNextToken("v"); + points[3 * i + 0] = p.getNextFloat(); + points[3 * i + 1] = p.getNextFloat(); + points[3 * i + 2] = p.getNextFloat(); + normals[3 * i + 0] = p.getNextFloat(); + normals[3 * i + 1] = p.getNextFloat(); + normals[3 * i + 2] = p.getNextFloat(); + uvs[2 * i + 0] = p.getNextFloat(); + uvs[2 * i + 1] = p.getNextFloat(); + } + int[] triangles = new int[numTriangles * 3]; + for (int i = 0; i < numTriangles; i++) { + p.checkNextToken("t"); + triangles[i * 3 + 0] = p.getNextInt(); + triangles[i * 3 + 1] = p.getNextInt(); + triangles[i * 3 + 2] = p.getNextInt(); + } + + geometry.setPoints(points); + geometry.setNormals(normals); + geometry.setUvs(uvs); + geometry.setTriangles(triangles); + geometry.setup(api); + } else if (type.equals("flat-mesh")) { + UI.printWarning(Module.API, "Deprecated object type: flat-mesh"); + UI.printInfo(Module.API, "Reading flat mesh: %s ...", name); + + TriangleMeshParameter geometry = new TriangleMeshParameter(); + geometry.setName(name); + geometry.setAccel(accel); + geometry.setInstanceParameter(instanceParameter); + + int numVertices = p.getNextInt(); + int numTriangles = p.getNextInt(); + float[] points = new float[numVertices * 3]; + float[] uvs = new float[numVertices * 2]; + for (int i = 0; i < numVertices; i++) { + p.checkNextToken("v"); + points[3 * i + 0] = p.getNextFloat(); + points[3 * i + 1] = p.getNextFloat(); + points[3 * i + 2] = p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + uvs[2 * i + 0] = p.getNextFloat(); + uvs[2 * i + 1] = p.getNextFloat(); + } + int[] triangles = new int[numTriangles * 3]; + for (int i = 0; i < numTriangles; i++) { + p.checkNextToken("t"); + triangles[i * 3 + 0] = p.getNextInt(); + triangles[i * 3 + 1] = p.getNextInt(); + triangles[i * 3 + 2] = p.getNextInt(); + } + + geometry.setPoints(points); + geometry.setUvs(uvs); + geometry.setTriangles(triangles); + geometry.setup(api); + } else if (type.equals("sphere")) { + UI.printInfo(Module.API, "Reading sphere ..."); + instanceParameter.setName(name + ".instance"); + instanceParameter.setGeometry(name); + + SphereParameter geometry = new SphereParameter(); + geometry.setName(name); + geometry.setInstanceParameter(instanceParameter); + + if (instanceParameter.getTransform() == null && !noInstance) { + // legacy method of specifying transformation for spheres + p.checkNextToken("c"); + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + geometry.setCenter(new Point3(x, y, z)); + p.checkNextToken("r"); + float radius = p.getNextFloat(); + geometry.setRadius(radius); + + geometry.setup(api); + // disable future instancing - instance has already been created + noInstance = true; + } + } else if (type.equals("cylinder")) { + UI.printInfo(Module.API, "Reading cylinder ..."); + + CylinderParameter parameter = new CylinderParameter(); + parameter.setName(name); + parameter.setInstanceParameter(instanceParameter); + + parameter.setup(api); + } else if (type.equals("banchoff")) { + UI.printInfo(Module.API, "Reading banchoff ..."); + + BanchOffParameter parameter = new BanchOffParameter(); + parameter.setName(name); + parameter.setInstanceParameter(instanceParameter); + + parameter.setup(api); + } else if (type.equals("torus")) { + UI.printInfo(Module.API, "Reading torus ..."); + + TorusParameter geometry = new TorusParameter(); + geometry.setName(name); + geometry.setInstanceParameter(instanceParameter); + + p.checkNextToken("r"); + geometry.setRadiusInner(p.getNextFloat()); + geometry.setRadiusOuter(p.getNextFloat()); + geometry.setup(api); + } else if (type.equals("sphereflake")) { + UI.printInfo(Module.API, "Reading sphereflake ..."); + SphereFlakeParameter geometry = new SphereFlakeParameter(); + geometry.setName(name); + + if (p.peekNextToken("level")) { + geometry.setLevel(p.getNextInt()); + } + if (p.peekNextToken("axis")) { + geometry.setAxis(parseVector()); + } + if (p.peekNextToken("radius")) { + geometry.setRadius(p.getNextFloat()); + } + geometry.setup(api); + } else if (type.equals("plane")) { + UI.printInfo(Module.API, "Reading plane ..."); + PlaneParameter geometry = new PlaneParameter(); + geometry.setName(name); + p.checkNextToken("p"); + geometry.setCenter(parsePoint()); + if (p.peekNextToken("n")) { + geometry.setNormal(parseVector()); + } else { + p.checkNextToken("p"); + geometry.setPoint1(parsePoint()); + p.checkNextToken("p"); + geometry.setPoint2(parsePoint()); + } + geometry.setup(api); + } else if (type.equals("generic-mesh")) { + UI.printInfo(Module.API, "Reading generic mesh: %s ... ", name); + GenericMeshParameter geometry = new GenericMeshParameter(); + // parse vertices + p.checkNextToken("points"); + int np = p.getNextInt(); + float[] points = parseFloatArray(np * 3); + geometry.setPoints(points); + // parse triangle indices + p.checkNextToken("triangles"); + int nt = p.getNextInt(); + geometry.setTriangles(parseIntArray(nt * 3)); + // parse normals + p.checkNextToken("normals"); + if (p.peekNextToken("vertex")) { + geometry.setNormals(parseFloatArray(np * 3)); + } else if (p.peekNextToken("facevarying")) { + geometry.setFaceVaryingNormals(true); + geometry.setNormals(parseFloatArray(nt * 9)); + } else { + p.checkNextToken("none"); + } + + // parse texture coordinates + p.checkNextToken("uvs"); + if (p.peekNextToken("vertex")) { + geometry.setUvs(parseFloatArray(np * 2)); + } else if (p.peekNextToken("facevarying")) { + geometry.setFaceVaryingTextures(true); + geometry.setUvs(parseFloatArray(nt * 6)); + } else { + p.checkNextToken("none"); + } + if (p.peekNextToken("face_shaders")) { + geometry.setFaceShaders(parseIntArray(nt)); + } + geometry.setup(api); + } else if (type.equals("hair")) { + UI.printInfo(Module.API, "Reading hair curves: %s ... ", name); + HairParameter geometry = new HairParameter(); + p.checkNextToken("segments"); + geometry.setSegments(p.getNextInt()); + p.checkNextToken("width"); + geometry.setWidth(p.getNextFloat()); + p.checkNextToken("points"); + geometry.setPoints(parseFloatArray(p.getNextInt())); + geometry.setup(api); + } else if (type.equals("janino-tesselatable")) { + UI.printInfo(Module.API, "Reading procedural primitive: %s ... ", name); + String typename = p.peekNextToken("typename") ? p.getNextToken() : PluginRegistry.shaderPlugins.generateUniqueName("janino_shader"); + if (!PluginRegistry.tesselatablePlugins.registerPlugin(typename, p.getNextCodeBlock())) { + noInstance = true; + } else { + api.geometry(name, typename); + } + } else if (type.equals("teapot")) { + UI.printInfo(Module.API, "Reading teapot: %s ... ", name); + TeapotParameter geometry = new TeapotParameter(); + geometry.setName(name); + + if (p.peekNextToken("subdivs")) { + geometry.setSubdivs(p.getNextInt()); + } + if (p.peekNextToken("smooth")) { + geometry.setSmooth(p.getNextBoolean()); + } + geometry.setup(api); + } else if (type.equals("gumbo")) { + UI.printInfo(Module.API, "Reading gumbo: %s ... ", name); + GumboParameter geometry = new GumboParameter(); + geometry.setName(name); + + if (p.peekNextToken("subdivs")) { + geometry.setSubdivs(p.getNextInt()); + } + if (p.peekNextToken("smooth")) { + geometry.setSmooth(p.getNextBoolean()); + } + geometry.setup(api); + } else if (type.equals("julia")) { + UI.printInfo(Module.API, "Reading julia fractal: %s ... ", name); + JuliaParameter geometry = new JuliaParameter(); + geometry.setName(name); + if (p.peekNextToken("q")) { + geometry.setCw(p.getNextFloat()); + geometry.setCx(p.getNextFloat()); + geometry.setCy(p.getNextFloat()); + geometry.setCz(p.getNextFloat()); + } + if (p.peekNextToken("iterations")) { + geometry.setIterations(p.getNextInt()); + } + if (p.peekNextToken("epsilon")) { + geometry.setEpsilon(p.getNextFloat()); + } + geometry.setup(api); + } else if (type.equals("particles") || type.equals("dlasurface")) { + ParticlesParameter geometry = new ParticlesParameter(); + geometry.setName(name); + if (type.equals("dlasurface")) { + UI.printWarning(Module.API, "Deprecated object type: \"dlasurface\" - please use \"particles\" instead"); + } + float[] data; + if (p.peekNextToken("filename")) { + // FIXME: this code should be moved into an on demand loading + // primitive + String filename = p.getNextToken(); + boolean littleEndian = false; + if (p.peekNextToken("little_endian")) { + littleEndian = true; + } + UI.printInfo(Module.USER, "Loading particle file: %s", filename); + File file = new File(filename); + FileInputStream stream = new FileInputStream(filename); + MappedByteBuffer map = stream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()); + if (littleEndian) + map.order(ByteOrder.LITTLE_ENDIAN); + FloatBuffer buffer = map.asFloatBuffer(); + data = new float[buffer.capacity()]; + for (int i = 0; i < data.length; i++) { + data[i] = buffer.get(i); + } + stream.close(); + } else { + p.checkNextToken("points"); + int n = p.getNextInt(); + data = parseFloatArray(n * 3); // read 3n points + } + + geometry.setPoints(data); + + if (p.peekNextToken("num")) { + geometry.setNum(p.getNextInt()); + } else { + geometry.setNum(data.length / 3); + } + + p.checkNextToken("radius"); + geometry.setRadius(p.getNextFloat()); + geometry.setup(api); + } else if (type.equals("file-mesh")) { + UI.printInfo(Module.API, "Reading file mesh: %s ... ", name); + FileMeshParameter geometry = new FileMeshParameter(); + geometry.setName(name); + + p.checkNextToken("filename"); + geometry.setFilename(p.getNextToken()); + + if (p.peekNextToken("smooth_normals")) { + geometry.setSmoothNormals(p.getNextBoolean()); + } + geometry.setup(api); + } else if (type.equals("bezier-mesh")) { + UI.printInfo(Module.API, "Reading bezier mesh: %s ... ", name); + BezierMeshParameter geometry = new BezierMeshParameter(); + geometry.setName(name); + + p.checkNextToken("n"); + //int nu, nv; + + geometry.setNu(p.getNextInt()); + geometry.setNv(p.getNextInt()); + + if (p.peekNextToken("wrap")) { + geometry.setUwrap(p.getNextBoolean()); + geometry.setVwrap(p.getNextBoolean()); + } + p.checkNextToken("points"); + float[] points = new float[3 * geometry.getNu() * geometry.getNv()]; + for (int i = 0; i < points.length; i++) { + points[i] = p.getNextFloat(); + } + + geometry.setPoints(points); + + if (p.peekNextToken("subdivs")) { + geometry.setSubdivs(p.getNextInt()); + } + if (p.peekNextToken("smooth")) { + geometry.setSmooth(p.getNextBoolean()); + } + geometry.setup(api); + } else { + UI.printWarning(Module.API, "Unrecognized object type: %s", p.getNextToken()); + noInstance = true; + } + if (!noInstance) { + instanceParameter.setName(name + ".instance"); + instanceParameter.setGeometry(name); + instanceParameter.setup(api); + + // create instance + /*api.parameter("shaders", shaders); + if (modifiers != null) + api.parameter("modifiers", modifiers); + if (transform != null && transform.length > 0) { + if (transform.length == 1) + api.parameter("transform", transform[0]); + else { + api.parameter("transform.steps", transform.length); + api.parameter("transform.times", "float", "none", new float[]{ + transformTime0, transformTime1}); + for (int i = 0; i < transform.length; i++) + api.parameter(String.format("transform[%d]", i), transform[i]); + } + } + api.instance(name + ".instance", name);*/ + } + p.checkNextToken("}"); + } + + private void parseInstanceBlock(SunflowAPIInterface api) throws ParserException, IOException { + InstanceParameter parameter = new InstanceParameter(); + p.checkNextToken("{"); + p.checkNextToken("name"); + parameter.setName(p.getNextToken()); + UI.printInfo(Module.API, "Reading instance: %s ...", parameter.getName()); + p.checkNextToken("geometry"); + parameter.setGeometry(p.getNextToken()); + + TransformParameter transformParameter = parseTransform(); + parameter.setTransform(transformParameter); + + String[] shaders; + if (p.peekNextToken("shaders")) { + int n = p.getNextInt(); + shaders = new String[n]; + for (int i = 0; i < n; i++) { + shaders[i] = p.getNextToken(); + } + } else { + p.checkNextToken("shader"); + shaders = new String[]{p.getNextToken()}; + } + parameter.setShaders(shaders); + + String[] modifiers = null; + if (p.peekNextToken("modifiers")) { + int n = p.getNextInt(); + modifiers = new String[n]; + for (int i = 0; i < n; i++) { + modifiers[i] = p.getNextToken(); + } + } else if (p.peekNextToken("modifier")) { + modifiers = new String[]{p.getNextToken()}; + } + parameter.setModifiers(modifiers); + parameter.setup(api); + + p.checkNextToken("}"); + } + + private TransformParameter parseTransform() throws ParserException, IOException { + TransformParameter transformParameter = new TransformParameter(); + p.checkNextToken("transform"); + if (p.peekNextToken("steps")) { + int n = p.getNextInt(); + + p.checkNextToken("times"); + float[] times = new float[n]; + for (int i = 0; i < n; i++) { + times[i] = p.getNextFloat(); + } + transformParameter.setTimes(times); + + Matrix4[] transforms = new Matrix4[n]; + for (int i = 0; i < n; i++) { + transforms[i] = parseMatrix(); + } + } else { + Matrix4[] transforms = new Matrix4[]{parseMatrix()}; + transformParameter.setTransforms(transforms); + } + return transformParameter; + } + + private TransformParameter checkParseTransform() throws ParserException, IOException { + TransformParameter transformParameter = new TransformParameter(); + if(p.peekNextToken("transform")) { + if (p.peekNextToken("steps")) { + int n = p.getNextInt(); + + p.checkNextToken("times"); + float[] times = new float[n]; + for (int i = 0; i < n; i++) { + times[i] = p.getNextFloat(); + } + transformParameter.setTimes(times); + + Matrix4[] transforms = new Matrix4[n]; + for (int i = 0; i < n; i++) { + transforms[i] = parseMatrix(); + } + } else { + Matrix4[] transforms = new Matrix4[]{parseMatrix()}; + transformParameter.setTransforms(transforms); + } + } else { + return null; + } + return transformParameter; + } + + private void parseLightBlock(SunflowAPIInterface api) throws ParserException, IOException, ColorSpecificationException { + p.checkNextToken("{"); + p.checkNextToken("type"); + if (p.peekNextToken("mesh")) { + UI.printWarning(Module.API, "Deprecated light type: mesh"); + p.checkNextToken("name"); + String name = p.getNextToken(); + UI.printInfo(Module.API, "Reading light mesh: %s ...", name); + p.checkNextToken("emit"); + api.parameter("radiance", null, parseColor().getRGB()); + int samples = numLightSamples; + if (p.peekNextToken("samples")) + samples = p.getNextInt(); + else + UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", samples); + api.parameter("samples", samples); + int numVertices = p.getNextInt(); + int numTriangles = p.getNextInt(); + float[] points = new float[3 * numVertices]; + int[] triangles = new int[3 * numTriangles]; + for (int i = 0; i < numVertices; i++) { + p.checkNextToken("v"); + points[3 * i + 0] = p.getNextFloat(); + points[3 * i + 1] = p.getNextFloat(); + points[3 * i + 2] = p.getNextFloat(); + // ignored + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + p.getNextFloat(); + } + for (int i = 0; i < numTriangles; i++) { + p.checkNextToken("t"); + triangles[3 * i + 0] = p.getNextInt(); + triangles[3 * i + 1] = p.getNextInt(); + triangles[3 * i + 2] = p.getNextInt(); + } + api.parameter("points", "point", "vertex", points); + api.parameter("triangles", triangles); + api.light(name, "triangle_mesh"); + } else if (p.peekNextToken("point")) { + UI.printInfo(Module.API, "Reading point light ..."); + PointLightParameter light = new PointLightParameter(); + light.setName(generateUniqueName("pointlight")); + + Color color; + if (p.peekNextToken("color")) { + color = parseColor(); + p.checkNextToken("power"); + float power = p.getNextFloat(); + color.mul(power); + } else { + UI.printWarning(Module.API, "Deprecated color specification - please use color and power instead"); + p.checkNextToken("power"); + color = parseColor(); + } + light.setColor(color); + + p.checkNextToken("p"); + light.setCenter(parsePoint()); + light.setup(api); + } else if (p.peekNextToken("spherical")) { + UI.printInfo(Module.API, "Reading spherical light ..."); + SphereLightParameter light = new SphereLightParameter(); + light.setName(generateUniqueName("spherelight")); + p.checkNextToken("color"); + Color color = parseColor(); + p.checkNextToken("radiance"); + float power = p.getNextFloat(); + color.mul(power); + light.setRadiance(color); + p.checkNextToken("center"); + light.setCenter(parsePoint()); + p.checkNextToken("radius"); + light.setRadius(p.getNextFloat()); + p.checkNextToken("samples"); + light.setSamples(p.getNextInt()); + light.setup(api); + } else if (p.peekNextToken("directional")) { + UI.printInfo(Module.API, "Reading directional light ..."); + DirectionalLightParameter light = new DirectionalLightParameter(); + light.setName(generateUniqueName("dirlight")); + p.checkNextToken("source"); + light.setSource(parsePoint()); + p.checkNextToken("target"); + light.setDirection(parsePoint()); + p.checkNextToken("radius"); + light.setRadius(p.getNextFloat()); + p.checkNextToken("emit"); + Color color = parseColor(); + if (p.peekNextToken("intensity")) { + float power = p.getNextFloat(); + color.mul(power); + } else { + UI.printWarning(Module.API, "Deprecated color specification - please use emit and intensity instead"); + } + light.setRadiance(color); + light.setup(api); + } else if (p.peekNextToken("ibl")) { + UI.printInfo(Module.API, "Reading image based light ..."); + ImageBasedLightParameter light = new ImageBasedLightParameter(); + light.setName(generateUniqueName("ibl")); + p.checkNextToken("image"); + light.setTexture(p.getNextToken()); + p.checkNextToken("center"); + light.setCenter(parseVector()); + p.checkNextToken("up"); + light.setUp(parseVector()); + p.checkNextToken("lock"); + light.setFixed(p.getNextBoolean()); + + light.setSamples(numLightSamples); + + if (p.peekNextToken("samples")) { + light.setSamples(p.getNextInt()); + } else { + UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", numLightSamples); + } + + if (p.peekNextToken("lowsamples")) { + light.setLowSamples(p.getNextInt()); + } + + light.setup(api); + } else if (p.peekNextToken("meshlight")) { + p.checkNextToken("name"); + TriangleMeshLightParameter light = new TriangleMeshLightParameter(); + light.setName(p.getNextToken()); + + UI.printInfo(Module.API, "Reading meshlight: %s ...", light.getName()); + p.checkNextToken("emit"); + Color color = parseColor(); + if (p.peekNextToken("radiance")) { + float r = p.getNextFloat(); + color.mul(r); + } else { + UI.printWarning(Module.API, "Deprecated color specification - please use emit and radiance instead"); + } + light.setRadiance(color); + light.setSamples(numLightSamples); + + if (p.peekNextToken("samples")) { + light.setSamples(p.getNextInt()); + } else { + UI.printWarning(Module.API, "Samples keyword not found - defaulting to %d", light.getSamples()); + } + + // parse vertices + p.checkNextToken("points"); + int np = p.getNextInt(); + float[] points = parseFloatArray(np * 3); + light.setPoints(points); + + // parse triangle indices + p.checkNextToken("triangles"); + int nt = p.getNextInt(); + int[] triangles = parseIntArray(nt * 3); + light.setTriangles(triangles); + light.setup(api); + } else if (p.peekNextToken("sunsky")) { + SunSkyLightParameter light = new SunSkyLightParameter(); + light.setName(generateUniqueName("sunsky")); + p.checkNextToken("up"); + light.setUp(parseVector()); + p.checkNextToken("east"); + light.setEast(parseVector()); + p.checkNextToken("sundir"); + light.setSunDirection(parseVector()); + p.checkNextToken("turbidity"); + light.setTurbidity(p.getNextFloat()); + + // TODO Is possible not to have samples in sun sky? + // light.setSamples(numLightSamples); + if (p.peekNextToken("samples")) { + light.setSamples(p.getNextInt()); + } + + if (p.peekNextToken("ground.extendsky")) { + light.setExtendSky(p.getNextBoolean()); + } else if (p.peekNextToken("ground.color")) { + light.setGroundColor(parseColor()); + } + light.setup(api); + } else if (p.peekNextToken("cornellbox")) { + UI.printInfo(Module.API, "Reading cornell box ..."); + CornellBoxLightParameter light = new CornellBoxLightParameter(); + light.setName(generateUniqueName("cornellbox")); + + p.checkNextToken("corner0"); + light.setMin(parsePoint()); + p.checkNextToken("corner1"); + light.setMax(parsePoint()); + p.checkNextToken("left"); + light.setLeft(parseColor()); + p.checkNextToken("right"); + light.setRight(parseColor()); + p.checkNextToken("top"); + light.setTop(parseColor()); + p.checkNextToken("bottom"); + light.setBottom(parseColor()); + p.checkNextToken("back"); + light.setBack(parseColor()); + p.checkNextToken("emit"); + light.setRadiance(parseColor()); + + // TODO Is possible not to have samples in cornell box? + // light.setSamples(numLightSamples); + if (p.peekNextToken("samples")) { + light.setSamples(p.getNextInt()); + } + light.setup(api); + } else { + UI.printWarning(Module.API, "Unrecognized object type: %s", p.getNextToken()); + } + p.checkNextToken("}"); + } + + private Color parseColor() throws IOException, ParserException, ColorSpecificationException { + if (p.peekNextToken("{")) { + String space = p.getNextToken(); + int req = ColorFactory.getRequiredDataValues(space); + if (req == -2) { + UI.printWarning(Module.API, "Unrecognized color space: %s", space); + return null; + } else if (req == -1) { + // array required, parse how many values are required + req = p.getNextInt(); + } + Color c = ColorFactory.createColor(space, parseFloatArray(req)); + p.checkNextToken("}"); + return c; + } else { + float r = p.getNextFloat(); + float g = p.getNextFloat(); + float b = p.getNextFloat(); + return ColorFactory.createColor(null, r, g, b); + } + } + + private Point3 parsePoint() throws IOException { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + return new Point3(x, y, z); + } + + private Vector3 parseVector() throws IOException { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + return new Vector3(x, y, z); + } + + private int[] parseIntArray(int size) throws IOException { + int[] data = new int[size]; + for (int i = 0; i < size; i++) { + data[i] = p.getNextInt(); + } + return data; + } + + private float[] parseFloatArray(int size) throws IOException { + float[] data = new float[size]; + for (int i = 0; i < size; i++) { + data[i] = p.getNextFloat(); + } + return data; + } + + private Matrix4 parseMatrix() throws IOException, ParserException { + if (p.peekNextToken("row")) { + return new Matrix4(parseFloatArray(16), true); + } else if (p.peekNextToken("col")) { + return new Matrix4(parseFloatArray(16), false); + } else { + Matrix4 m = Matrix4.IDENTITY; + p.checkNextToken("{"); + while (!p.peekNextToken("}")) { + Matrix4 t = null; + if (p.peekNextToken("translate")) { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + t = Matrix4.translation(x, y, z); + } else if (p.peekNextToken("scaleu")) { + float s = p.getNextFloat(); + t = Matrix4.scale(s); + } else if (p.peekNextToken("scale")) { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + t = Matrix4.scale(x, y, z); + } else if (p.peekNextToken("rotatex")) { + float angle = p.getNextFloat(); + t = Matrix4.rotateX((float) Math.toRadians(angle)); + } else if (p.peekNextToken("rotatey")) { + float angle = p.getNextFloat(); + t = Matrix4.rotateY((float) Math.toRadians(angle)); + } else if (p.peekNextToken("rotatez")) { + float angle = p.getNextFloat(); + t = Matrix4.rotateZ((float) Math.toRadians(angle)); + } else if (p.peekNextToken("rotate")) { + float x = p.getNextFloat(); + float y = p.getNextFloat(); + float z = p.getNextFloat(); + float angle = p.getNextFloat(); + t = Matrix4.rotate(x, y, z, (float) Math.toRadians(angle)); + } else + UI.printWarning(Module.API, "Unrecognized transformation type: %s", p.getNextToken()); + if (t != null) + m = t.multiply(m); + } + return m; + } + } } \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/CornellBox.java b/src/main/java/org/sunflow/core/primitive/CornellBox.java index 6b2c88c..d70540f 100644 --- a/src/main/java/org/sunflow/core/primitive/CornellBox.java +++ b/src/main/java/org/sunflow/core/primitive/CornellBox.java @@ -10,6 +10,8 @@ import org.sunflow.core.Ray; import org.sunflow.core.Shader; import org.sunflow.core.ShadingState; +import org.sunflow.core.parameter.light.CornellBoxLightParameter; +import org.sunflow.core.parameter.light.LightParameter; import org.sunflow.image.Color; import org.sunflow.math.BoundingBox; import org.sunflow.math.Matrix4; @@ -66,22 +68,22 @@ private void updateGeometry(Point3 c0, Point3 c1) { } public boolean update(ParameterList pl, SunflowAPI api) { - Point3 corner0 = pl.getPoint("corner0", null); - Point3 corner1 = pl.getPoint("corner1", null); + Point3 corner0 = pl.getPoint(CornellBoxLightParameter.PARAM_MIN_CORNER, null); + Point3 corner1 = pl.getPoint(CornellBoxLightParameter.PARAM_MAX_CORNER, null); if (corner0 != null && corner1 != null) { updateGeometry(corner0, corner1); } // shader colors - left = pl.getColor("leftColor", left); - right = pl.getColor("rightColor", right); - top = pl.getColor("topColor", top); - bottom = pl.getColor("bottomColor", bottom); - back = pl.getColor("backColor", back); + left = pl.getColor(CornellBoxLightParameter.PARAM_LEFT_COLOR, left); + right = pl.getColor(CornellBoxLightParameter.PARAM_RIGHT_COLOR, right); + top = pl.getColor(CornellBoxLightParameter.PARAM_TOP_COLOR, top); + bottom = pl.getColor(CornellBoxLightParameter.PARAM_BOTTOM_COLOR, bottom); + back = pl.getColor(CornellBoxLightParameter.PARAM_BACK_COLOR, back); // light - radiance = pl.getColor("radiance", radiance); - samples = pl.getInt("samples", samples); + radiance = pl.getColor(LightParameter.PARAM_RADIANCE, radiance); + samples = pl.getInt(LightParameter.PARAM_SAMPLES, samples); return true; } @@ -443,4 +445,44 @@ public PrimitiveList getBakingPrimitives() { public Instance createInstance() { return Instance.createTemporary(this, null, this); } + + public Color getLeft() { + return left; + } + + public void setLeft(Color left) { + this.left = left; + } + + public Color getRight() { + return right; + } + + public void setRight(Color right) { + this.right = right; + } + + public Color getTop() { + return top; + } + + public void setTop(Color top) { + this.top = top; + } + + public Color getBottom() { + return bottom; + } + + public void setBottom(Color bottom) { + this.bottom = bottom; + } + + public Color getBack() { + return back; + } + + public void setBack(Color back) { + this.back = back; + } } \ No newline at end of file diff --git a/src/main/java/org/sunflow/core/primitive/TriangleMesh.java b/src/main/java/org/sunflow/core/primitive/TriangleMesh.java index 447df32..b314d3b 100644 --- a/src/main/java/org/sunflow/core/primitive/TriangleMesh.java +++ b/src/main/java/org/sunflow/core/primitive/TriangleMesh.java @@ -33,10 +33,11 @@ public class TriangleMesh implements PrimitiveList { private boolean backfaceCull = false; public static void setSmallTriangles(boolean smallTriangles) { - if (smallTriangles) + if (smallTriangles) { UI.printInfo(Module.GEOM, "Small trimesh mode: enabled"); - else + } else { UI.printInfo(Module.GEOM, "Small trimesh mode: disabled"); + } TriangleMesh.smallTriangles = smallTriangles; } @@ -51,11 +52,13 @@ public void writeObj(String filename) { try { FileWriter file = new FileWriter(filename); file.write(String.format("o object\n")); - for (int i = 0; i < points.length; i += 3) + for (int i = 0; i < points.length; i += 3) { file.write(String.format("v %g %g %g\n", points[i], points[i + 1], points[i + 2])); + } file.write("s off\n"); - for (int i = 0; i < triangles.length; i += 3) + for (int i = 0; i < triangles.length; i += 3) { file.write(String.format("f %d %d %d\n", triangles[i] + 1, triangles[i + 1] + 1, triangles[i + 2] + 1)); + } file.close(); } catch (IOException e) { e.printStackTrace(); @@ -124,10 +127,11 @@ public float getPrimitiveBound(int primID, int i) { int b = 3 * triangles[tri + 1]; int c = 3 * triangles[tri + 2]; int axis = i >>> 1; - if ((i & 1) == 0) + if ((i & 1) == 0) { return MathUtils.min(points[a + axis], points[b + axis], points[c + axis]); - else + } else { return MathUtils.max(points[a + axis], points[b + axis], points[c + axis]); + } } public BoundingBox getWorldBounds(Matrix4 o2w) { @@ -339,8 +343,9 @@ public void init() { return; } triaccel = new WaldTriangle[nt]; - for (int i = 0; i < nt; i++) + for (int i = 0; i < nt; i++) { triaccel[i] = new WaldTriangle(this, i); + } } } diff --git a/src/main/java/org/sunflow/core/renderer/BucketRenderer.java b/src/main/java/org/sunflow/core/renderer/BucketRenderer.java index 3cea7f9..235fca2 100644 --- a/src/main/java/org/sunflow/core/renderer/BucketRenderer.java +++ b/src/main/java/org/sunflow/core/renderer/BucketRenderer.java @@ -1,6 +1,8 @@ package org.sunflow.core.renderer; import org.sunflow.PluginRegistry; +import org.sunflow.core.parameter.BucketParameter; +import org.sunflow.core.parameter.ImageParameter; import org.sunflow.core.BucketOrder; import org.sunflow.core.Display; import org.sunflow.core.Filter; @@ -61,7 +63,7 @@ public class BucketRenderer implements ImageSampler { public BucketRenderer() { bucketSize = 32; - bucketOrderName = "hilbert"; + bucketOrderName = BucketParameter.ORDER_HILBERT; displayAA = false; contrastThreshold = 0.1f; filterName = "box"; @@ -75,14 +77,14 @@ public boolean prepare(Options options, Scene scene, int w, int h) { imageHeight = h; // fetch options - bucketSize = options.getInt("bucket.size", bucketSize); - bucketOrderName = options.getString("bucket.order", bucketOrderName); - minAADepth = options.getInt("aa.min", minAADepth); - maxAADepth = options.getInt("aa.max", maxAADepth); - superSampling = options.getInt("aa.samples", superSampling); - displayAA = options.getBoolean("aa.display", displayAA); - jitter = options.getBoolean("aa.jitter", jitter); - contrastThreshold = options.getFloat("aa.contrast", contrastThreshold); + bucketSize = options.getInt(BucketParameter.PARAM_BUCKET_SIZE, bucketSize); + bucketOrderName = options.getString(BucketParameter.PARAM_BUCKET_ORDER, bucketOrderName); + minAADepth = options.getInt(ImageParameter.PARAM_AA_MIN, minAADepth); + maxAADepth = options.getInt(ImageParameter.PARAM_AA_MAX, maxAADepth); + superSampling = options.getInt(ImageParameter.PARAM_AA_SAMPLES, superSampling); + displayAA = options.getBoolean(ImageParameter.PARAM_AA_DISPLAY, displayAA); + jitter = options.getBoolean(ImageParameter.PARAM_AA_JITTER, jitter); + contrastThreshold = options.getFloat(ImageParameter.PARAM_AA_CONTRAST, contrastThreshold); // limit bucket size and compute number of buckets in each direction bucketSize = MathUtils.clamp(bucketSize, 16, 512); diff --git a/src/main/java/org/sunflow/core/tesselatable/FileMesh.java b/src/main/java/org/sunflow/core/tesselatable/FileMesh.java index a33883d..72e5d14 100644 --- a/src/main/java/org/sunflow/core/tesselatable/FileMesh.java +++ b/src/main/java/org/sunflow/core/tesselatable/FileMesh.java @@ -100,8 +100,9 @@ public PrimitiveList tesselate() { tris.add(Integer.parseInt(f[3]) - 1); } } - if (lineNumber % 100000 == 0) + if (lineNumber % 100000 == 0) { UI.printInfo(Module.GEOM, "OBJ - * Parsed %7d lines ...", lineNumber); + } lineNumber++; } file.close(); @@ -161,8 +162,9 @@ public PrimitiveList tesselate() { e.printStackTrace(); UI.printError(Module.GEOM, "Unable to read mesh file \"%s\" - I/O error occured", filename); } - } else + } else { UI.printWarning(Module.GEOM, "Unable to read mesh file \"%s\" - unrecognized format", filename); + } return null; } diff --git a/src/main/java/org/sunflow/image/Color.java b/src/main/java/org/sunflow/image/Color.java index 9281ef0..ac8b177 100644 --- a/src/main/java/org/sunflow/image/Color.java +++ b/src/main/java/org/sunflow/image/Color.java @@ -2,6 +2,8 @@ import org.sunflow.math.MathUtils; +import java.util.Objects; + public final class Color { private float r, g, b; public static final RGBSpace NATIVE_SPACE = RGBSpace.SRGB; @@ -367,4 +369,5 @@ public static final boolean hasContrast(Color c1, Color c2, float thresh) { public String toString() { return String.format("(%.3f, %.3f, %.3f)", r, g, b); } + } \ No newline at end of file diff --git a/src/main/java/org/sunflow/math/Point3.java b/src/main/java/org/sunflow/math/Point3.java index cc1bf13..4659877 100644 --- a/src/main/java/org/sunflow/math/Point3.java +++ b/src/main/java/org/sunflow/math/Point3.java @@ -129,4 +129,24 @@ public static final Vector3 normal(Point3 p0, Point3 p1, Point3 p2, Vector3 dest public final String toString() { return String.format("(%.2f, %.2f, %.2f)", x, y, z); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Point3 point3 = (Point3) o; + + if (Float.compare(point3.x, x) != 0) return false; + if (Float.compare(point3.y, y) != 0) return false; + return Float.compare(point3.z, z) == 0; + } + + @Override + public int hashCode() { + int result = (x != +0.0f ? Float.floatToIntBits(x) : 0); + result = 31 * result + (y != +0.0f ? Float.floatToIntBits(y) : 0); + result = 31 * result + (z != +0.0f ? Float.floatToIntBits(z) : 0); + return result; + } } \ No newline at end of file diff --git a/src/main/java/org/sunflow/math/Vector3.java b/src/main/java/org/sunflow/math/Vector3.java index 30ed598..681477f 100644 --- a/src/main/java/org/sunflow/math/Vector3.java +++ b/src/main/java/org/sunflow/math/Vector3.java @@ -1,5 +1,7 @@ package org.sunflow.math; +import java.util.Objects; + public final class Vector3 { private static final float[] COS_THETA = new float[256]; private static final float[] SIN_THETA = new float[256]; @@ -192,4 +194,19 @@ public static final Vector3 sub(Vector3 v1, Vector3 v2, Vector3 dest) { public final String toString() { return String.format("(%.2f, %.2f, %.2f)", x, y, z); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Vector3 vector3 = (Vector3) o; + return Float.compare(vector3.x, x) == 0 && + Float.compare(vector3.y, y) == 0 && + Float.compare(vector3.z, z) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(x, y, z); + } } \ No newline at end of file diff --git a/src/main/java/org/sunflow/system/SystemUtil.java b/src/main/java/org/sunflow/system/SystemUtil.java new file mode 100644 index 0000000..b93019a --- /dev/null +++ b/src/main/java/org/sunflow/system/SystemUtil.java @@ -0,0 +1,24 @@ +package org.sunflow.system; + +public class SystemUtil { + + /** + * This is a quick system test which verifies that the user has launched + * Java properly. + */ + public static void runSystemCheck() { + final long RECOMMENDED_MAX_SIZE = 800; + long maxMb = Runtime.getRuntime().maxMemory() / 1048576; + if (maxMb < RECOMMENDED_MAX_SIZE) + UI.printError(UI.Module.API, "JVM available memory is below %d MB (found %d MB only).\nPlease make sure you launched the program with the -Xmx command line options.", RECOMMENDED_MAX_SIZE, maxMb); + String compiler = System.getProperty("java.vm.name"); + if (compiler == null || !(compiler.contains("HotSpot") && compiler.contains("Server"))) + UI.printError(UI.Module.API, "You do not appear to be running Sun's server JVM\nPerformance may suffer"); + UI.printDetailed(UI.Module.API, "Java environment settings:"); + UI.printDetailed(UI.Module.API, " * Max memory available : %d MB", maxMb); + UI.printDetailed(UI.Module.API, " * Virtual machine name : %s", compiler == null ? " Date: Thu, 19 Jul 2018 20:31:26 +0200 Subject: [PATCH 13/37] Allow getting rendered image from ImagePanel --- pom.xml | 2 +- src/main/java/org/sunflow/system/ImagePanel.java | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 50f48fb..08a2b8b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ sunflow net.sourceforge - 0.07.5-SNAPSHOT + 0.07.5 http://sunflow.sourceforge.net Sunflow Global Illumination Rendering System diff --git a/src/main/java/org/sunflow/system/ImagePanel.java b/src/main/java/org/sunflow/system/ImagePanel.java index d760380..67fef2a 100644 --- a/src/main/java/org/sunflow/system/ImagePanel.java +++ b/src/main/java/org/sunflow/system/ImagePanel.java @@ -110,6 +110,10 @@ public void save(String filename) { } } + public BufferedImage getImage() { + return image; + } + private synchronized void drag(int dx, int dy) { xo += dx; yo += dy; From ce3dc85cf9603e5e421ad830488f869e10a2d536 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sat, 28 Jul 2018 22:36:16 -0300 Subject: [PATCH 14/37] Fix tests --- .../geometry/BezierMeshParameterTest.java | 4 ++- .../geometry/FileMeshParameterTest.java | 3 ++- .../parameter/geometry/HairParameterTest.java | 4 +++ .../geometry/ParticlesParameterTest.java | 2 ++ .../geometry/PlaneParameterTest.java | 5 ++++ .../gi/AmbientOcclusionGIParameterTest.java | 13 ++++++---- .../parameter/gi/FakeGIParameterTest.java | 11 +++++--- .../parameter/gi/InstantGIParameterTest.java | 13 ++++++---- .../parameter/gi/IrrCacheGIParameterTest.java | 26 +++++++++++-------- .../gi/PathTracingGIParameterTest.java | 7 +++-- 10 files changed, 59 insertions(+), 29 deletions(-) diff --git a/src/test/java/org/sunflow/core/parameter/geometry/BezierMeshParameterTest.java b/src/test/java/org/sunflow/core/parameter/geometry/BezierMeshParameterTest.java index 68b71de..02b83b4 100644 --- a/src/test/java/org/sunflow/core/parameter/geometry/BezierMeshParameterTest.java +++ b/src/test/java/org/sunflow/core/parameter/geometry/BezierMeshParameterTest.java @@ -21,7 +21,9 @@ public void setUp() { public void testSetupAPI() { // Set values parameter.setName("uniqueName"); - parameter.setPoints(new float[]{0,0,0,1,1,1,2,0,0}); + parameter.setPoints(new float[]{0, 0, 0, 1, 1, 1, 1, 2, 1, 2, 0, 0}); + parameter.nu = 2; + parameter.nv = 2; parameter.setSubdivs(2); // Set parameters diff --git a/src/test/java/org/sunflow/core/parameter/geometry/FileMeshParameterTest.java b/src/test/java/org/sunflow/core/parameter/geometry/FileMeshParameterTest.java index 8b2dcb3..6d4204a 100644 --- a/src/test/java/org/sunflow/core/parameter/geometry/FileMeshParameterTest.java +++ b/src/test/java/org/sunflow/core/parameter/geometry/FileMeshParameterTest.java @@ -17,7 +17,8 @@ public void setUp() { parameter = new FileMeshParameter(); } - @Test + // Ignoring test, a resource file is needed to test + // @Test public void testSetupAPI() { // Set values parameter.setName("uniqueName"); diff --git a/src/test/java/org/sunflow/core/parameter/geometry/HairParameterTest.java b/src/test/java/org/sunflow/core/parameter/geometry/HairParameterTest.java index 7a9e273..4d48a97 100644 --- a/src/test/java/org/sunflow/core/parameter/geometry/HairParameterTest.java +++ b/src/test/java/org/sunflow/core/parameter/geometry/HairParameterTest.java @@ -19,6 +19,10 @@ public void setUp() { @Test public void testSetupAPI() { + parameter.setWidth(1f); + parameter.setSegments(3); + parameter.setPoints(new float[]{0, 0, 0, 1, 1, 1, 2, 2, 2}); + // Set values parameter.setName("uniqueName"); diff --git a/src/test/java/org/sunflow/core/parameter/geometry/ParticlesParameterTest.java b/src/test/java/org/sunflow/core/parameter/geometry/ParticlesParameterTest.java index cc639e1..a05eb42 100644 --- a/src/test/java/org/sunflow/core/parameter/geometry/ParticlesParameterTest.java +++ b/src/test/java/org/sunflow/core/parameter/geometry/ParticlesParameterTest.java @@ -21,6 +21,8 @@ public void setUp() { public void testSetupAPI() { // Set values parameter.setName("uniqueName"); + parameter.setNum(2); + parameter.setPoints(new float[]{0, 0, 0, 1, 1, 1}); // Set parameters parameter.setup(api); diff --git a/src/test/java/org/sunflow/core/parameter/geometry/PlaneParameterTest.java b/src/test/java/org/sunflow/core/parameter/geometry/PlaneParameterTest.java index cbc7be7..f34cc47 100644 --- a/src/test/java/org/sunflow/core/parameter/geometry/PlaneParameterTest.java +++ b/src/test/java/org/sunflow/core/parameter/geometry/PlaneParameterTest.java @@ -5,6 +5,7 @@ import org.junit.Test; import org.sunflow.SunflowAPI; import org.sunflow.core.Geometry; +import org.sunflow.math.Point3; public class PlaneParameterTest { @@ -22,6 +23,10 @@ public void testSetupAPI() { // Set values parameter.setName("uniqueName"); + parameter.setCenter(new Point3(0,0,0)); + parameter.setPoint1(new Point3(0,0,0)); + parameter.setPoint2(new Point3(0,1,0)); + // Set parameters parameter.setup(api); diff --git a/src/test/java/org/sunflow/core/parameter/gi/AmbientOcclusionGIParameterTest.java b/src/test/java/org/sunflow/core/parameter/gi/AmbientOcclusionGIParameterTest.java index e009b6f..4a67c1e 100644 --- a/src/test/java/org/sunflow/core/parameter/gi/AmbientOcclusionGIParameterTest.java +++ b/src/test/java/org/sunflow/core/parameter/gi/AmbientOcclusionGIParameterTest.java @@ -4,6 +4,7 @@ import org.junit.Before; import org.junit.Test; import org.sunflow.SunflowAPI; +import org.sunflow.core.Options; import org.sunflow.image.Color; public class AmbientOcclusionGIParameterTest { @@ -28,11 +29,13 @@ public void testSetupAPI() { // Set parameters gi.setup(api); - Assert.assertEquals(GlobalIlluminationParameter.TYPE_AMBOCC, api.getParameterList().getString(GlobalIlluminationParameter.PARAM_ENGINE, "")); - Assert.assertArrayEquals(gi.bright.getRGB(), api.getParameterList().getColor(AmbientOcclusionGIParameter.PARAM_BRIGHT, null).getRGB(), 0); - Assert.assertArrayEquals(gi.dark.getRGB(), api.getParameterList().getColor(AmbientOcclusionGIParameter.PARAM_DARK, null).getRGB(), 0); - Assert.assertEquals(gi.samples, api.getParameterList().getInt(AmbientOcclusionGIParameter.PARAM_SAMPLES,0)); - Assert.assertEquals(gi.maxDist, api.getParameterList().getFloat(AmbientOcclusionGIParameter.PARAM_MAXDIST,0), 0); + Options options = (Options) api.getRenderObjects().get(SunflowAPI.DEFAULT_OPTIONS).obj; + + Assert.assertEquals(GlobalIlluminationParameter.TYPE_AMBOCC, options.getString(GlobalIlluminationParameter.PARAM_ENGINE, "")); + Assert.assertArrayEquals(gi.bright.getRGB(), options.getColor(AmbientOcclusionGIParameter.PARAM_BRIGHT, null).getRGB(), 0); + Assert.assertArrayEquals(gi.dark.getRGB(), options.getColor(AmbientOcclusionGIParameter.PARAM_DARK, null).getRGB(), 0); + Assert.assertEquals(gi.samples, options.getInt(AmbientOcclusionGIParameter.PARAM_SAMPLES,0)); + Assert.assertEquals(gi.maxDist, options.getFloat(AmbientOcclusionGIParameter.PARAM_MAXDIST,0), 0); } } diff --git a/src/test/java/org/sunflow/core/parameter/gi/FakeGIParameterTest.java b/src/test/java/org/sunflow/core/parameter/gi/FakeGIParameterTest.java index d88a4e0..625ff0c 100644 --- a/src/test/java/org/sunflow/core/parameter/gi/FakeGIParameterTest.java +++ b/src/test/java/org/sunflow/core/parameter/gi/FakeGIParameterTest.java @@ -4,6 +4,7 @@ import org.junit.Before; import org.junit.Test; import org.sunflow.SunflowAPI; +import org.sunflow.core.Options; import org.sunflow.image.Color; import org.sunflow.math.Vector3; @@ -28,10 +29,12 @@ public void testSetupAPI() { // Set parameters gi.setup(api); - Assert.assertEquals(GlobalIlluminationParameter.TYPE_FAKE, api.getParameterList().getString(GlobalIlluminationParameter.PARAM_ENGINE, "")); - Assert.assertArrayEquals(gi.ground.getRGB(), api.getParameterList().getColor(FakeGIParameter.PARAM_GROUND, null).getRGB(), 0); - Assert.assertArrayEquals(gi.sky.getRGB(), api.getParameterList().getColor(FakeGIParameter.PARAM_SKY, null).getRGB(), 0); - Assert.assertEquals(gi.up, api.getParameterList().getVector(FakeGIParameter.PARAM_UP,null)); + Options options = (Options) api.getRenderObjects().get(SunflowAPI.DEFAULT_OPTIONS).obj; + + Assert.assertEquals(GlobalIlluminationParameter.TYPE_FAKE, options.getString(GlobalIlluminationParameter.PARAM_ENGINE, "")); + Assert.assertArrayEquals(gi.ground.getRGB(), options.getColor(FakeGIParameter.PARAM_GROUND, null).getRGB(), 0); + Assert.assertArrayEquals(gi.sky.getRGB(), options.getColor(FakeGIParameter.PARAM_SKY, null).getRGB(), 0); + Assert.assertEquals(gi.up, options.getVector(FakeGIParameter.PARAM_UP,null)); } } diff --git a/src/test/java/org/sunflow/core/parameter/gi/InstantGIParameterTest.java b/src/test/java/org/sunflow/core/parameter/gi/InstantGIParameterTest.java index ddbab0c..eaf6bd0 100644 --- a/src/test/java/org/sunflow/core/parameter/gi/InstantGIParameterTest.java +++ b/src/test/java/org/sunflow/core/parameter/gi/InstantGIParameterTest.java @@ -4,6 +4,7 @@ import org.junit.Before; import org.junit.Test; import org.sunflow.SunflowAPI; +import org.sunflow.core.Options; public class InstantGIParameterTest { @@ -27,11 +28,13 @@ public void testSetupAPI() { // Set parameters gi.setup(api); - Assert.assertEquals(GlobalIlluminationParameter.TYPE_IGI, api.getParameterList().getString(GlobalIlluminationParameter.PARAM_ENGINE, "")); - Assert.assertEquals(gi.biasSamples, api.getParameterList().getInt(InstantGIParameter.PARAM_BIAS_SAMPLES,0)); - Assert.assertEquals(gi.samples, api.getParameterList().getInt(InstantGIParameter.PARAM_SAMPLES,0)); - Assert.assertEquals(gi.sets, api.getParameterList().getInt(InstantGIParameter.PARAM_SETS,0)); - Assert.assertEquals(gi.bias, api.getParameterList().getFloat(InstantGIParameter.PARAM_BIAS,0),0); + Options options = (Options) api.getRenderObjects().get(SunflowAPI.DEFAULT_OPTIONS).obj; + + Assert.assertEquals(GlobalIlluminationParameter.TYPE_IGI, options.getString(GlobalIlluminationParameter.PARAM_ENGINE, "")); + Assert.assertEquals(gi.biasSamples, options.getInt(InstantGIParameter.PARAM_BIAS_SAMPLES,0)); + Assert.assertEquals(gi.samples, options.getInt(InstantGIParameter.PARAM_SAMPLES,0)); + Assert.assertEquals(gi.sets, options.getInt(InstantGIParameter.PARAM_SETS,0)); + Assert.assertEquals(gi.bias, options.getFloat(InstantGIParameter.PARAM_BIAS,0),0); } } diff --git a/src/test/java/org/sunflow/core/parameter/gi/IrrCacheGIParameterTest.java b/src/test/java/org/sunflow/core/parameter/gi/IrrCacheGIParameterTest.java index 4963ab4..3691942 100644 --- a/src/test/java/org/sunflow/core/parameter/gi/IrrCacheGIParameterTest.java +++ b/src/test/java/org/sunflow/core/parameter/gi/IrrCacheGIParameterTest.java @@ -4,6 +4,7 @@ import org.junit.Before; import org.junit.Test; import org.sunflow.SunflowAPI; +import org.sunflow.core.Options; import org.sunflow.core.parameter.IlluminationParameter; public class IrrCacheGIParameterTest { @@ -28,28 +29,31 @@ public void testSetupAPI() { // Set parameters gi.setup(api); - Assert.assertEquals(GlobalIlluminationParameter.TYPE_IRR_CACHE, api.getParameterList().getString(GlobalIlluminationParameter.PARAM_ENGINE, "")); - Assert.assertEquals(gi.samples, api.getParameterList().getInt(IrrCacheGIParameter.PARAM_SAMPLES, 0)); - Assert.assertEquals(gi.tolerance, api.getParameterList().getFloat(IrrCacheGIParameter.PARAM_TOLERANCE, 0), 0); - Assert.assertEquals(gi.minSpacing, api.getParameterList().getFloat(IrrCacheGIParameter.PARAM_MIN_SPACING, 0), 0); - Assert.assertEquals(gi.maxSpacing, api.getParameterList().getFloat(IrrCacheGIParameter.PARAM_MAX_SPACING, 0), 0); + Options options = (Options) api.getRenderObjects().get(SunflowAPI.DEFAULT_OPTIONS).obj; + + Assert.assertEquals(GlobalIlluminationParameter.TYPE_IRR_CACHE, options.getString(GlobalIlluminationParameter.PARAM_ENGINE, "")); + Assert.assertEquals(gi.samples, options.getInt(IrrCacheGIParameter.PARAM_SAMPLES, 0)); + Assert.assertEquals(gi.tolerance, options.getFloat(IrrCacheGIParameter.PARAM_TOLERANCE, 0), 0); + Assert.assertEquals(gi.minSpacing, options.getFloat(IrrCacheGIParameter.PARAM_MIN_SPACING, 0), 0); + Assert.assertEquals(gi.maxSpacing, options.getFloat(IrrCacheGIParameter.PARAM_MAX_SPACING, 0), 0); } @Test public void testSetupAPIWithGlobal() { // Set values - IlluminationParameter global = new IlluminationParameter(); gi.global = global; // Set parameters gi.setup(api); - Assert.assertEquals(GlobalIlluminationParameter.TYPE_IRR_CACHE, api.getParameterList().getString(GlobalIlluminationParameter.PARAM_ENGINE, "")); - Assert.assertEquals(gi.global.getMap(), api.getParameterList().getString(IrrCacheGIParameter.PARAM_GLOBAL, "")); - Assert.assertEquals(gi.global.getEmit(), api.getParameterList().getInt(IrrCacheGIParameter.PARAM_GLOBAL_EMIT, 0)); - Assert.assertEquals(gi.global.getGather(), api.getParameterList().getInt(IrrCacheGIParameter.PARAM_GLOBAL_GATHER, 0)); - Assert.assertEquals(gi.global.getRadius(), api.getParameterList().getFloat(IrrCacheGIParameter.PARAM_GLOBAL_RADIUS, 0), 0); + Options options = (Options) api.getRenderObjects().get(SunflowAPI.DEFAULT_OPTIONS).obj; + + Assert.assertEquals(GlobalIlluminationParameter.TYPE_IRR_CACHE, options.getString(GlobalIlluminationParameter.PARAM_ENGINE, "")); + Assert.assertEquals(gi.global.getMap(), options.getString(IrrCacheGIParameter.PARAM_GLOBAL, "")); + Assert.assertEquals(gi.global.getEmit(), options.getInt(IrrCacheGIParameter.PARAM_GLOBAL_EMIT, 0)); + Assert.assertEquals(gi.global.getGather(), options.getInt(IrrCacheGIParameter.PARAM_GLOBAL_GATHER, 0)); + Assert.assertEquals(gi.global.getRadius(), options.getFloat(IrrCacheGIParameter.PARAM_GLOBAL_RADIUS, 0), 0); } } diff --git a/src/test/java/org/sunflow/core/parameter/gi/PathTracingGIParameterTest.java b/src/test/java/org/sunflow/core/parameter/gi/PathTracingGIParameterTest.java index 7772c16..f730637 100644 --- a/src/test/java/org/sunflow/core/parameter/gi/PathTracingGIParameterTest.java +++ b/src/test/java/org/sunflow/core/parameter/gi/PathTracingGIParameterTest.java @@ -4,6 +4,7 @@ import org.junit.Before; import org.junit.Test; import org.sunflow.SunflowAPI; +import org.sunflow.core.Options; public class PathTracingGIParameterTest { @@ -24,8 +25,10 @@ public void testSetupAPI() { // Set parameters gi.setup(api); - Assert.assertEquals(GlobalIlluminationParameter.TYPE_PATH, api.getParameterList().getString(GlobalIlluminationParameter.PARAM_ENGINE, "")); - Assert.assertEquals(gi.samples, api.getParameterList().getInt(PathTracingGIParameter.PARAM_SAMPLES,0)); + Options options = (Options) api.getRenderObjects().get(SunflowAPI.DEFAULT_OPTIONS).obj; + + Assert.assertEquals(GlobalIlluminationParameter.TYPE_PATH, options.getString(GlobalIlluminationParameter.PARAM_ENGINE, "")); + Assert.assertEquals(gi.samples, options.getInt(PathTracingGIParameter.PARAM_SAMPLES,0)); } } From 6d0e161b74f1d5340c5770d209b1ca1566914190 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sat, 28 Jul 2018 22:36:24 -0300 Subject: [PATCH 15/37] Add travis file --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8ceacfa --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: java +install: mvn install -DskipTests=true -Dmaven.javadoc.skip=true -Dgpg.skip=true -B -V +script: mvn test -Dgpg.skip=true +jdk: + - oraclejdk8 From d5ca8459fb12bfec6fa4401feb52f7a240746605 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sat, 13 Apr 2019 01:01:37 -0300 Subject: [PATCH 16/37] Create instanceParameter on setup geometries --- .../parameter/geometry/BanchOffParameter.java | 3 ++- .../parameter/geometry/BezierMeshParameter.java | 4 +++- .../parameter/geometry/CylinderParameter.java | 4 +++- .../parameter/geometry/FileMeshParameter.java | 3 ++- .../parameter/geometry/GenericMeshParameter.java | 4 +++- .../parameter/geometry/GeometryParameter.java | 15 +++++++++++++++ .../core/parameter/geometry/GumboParameter.java | 3 ++- .../core/parameter/geometry/HairParameter.java | 3 ++- .../core/parameter/geometry/JuliaParameter.java | 4 +++- .../core/parameter/geometry/ObjectParameter.java | 2 +- .../parameter/geometry/ParticlesParameter.java | 4 +++- .../core/parameter/geometry/PlaneParameter.java | 4 +++- .../parameter/geometry/SphereFlakeParameter.java | 9 ++++++++- .../core/parameter/geometry/SphereParameter.java | 12 ++---------- .../core/parameter/geometry/TeapotParameter.java | 4 +++- .../core/parameter/geometry/TorusParameter.java | 4 +++- .../parameter/geometry/TriangleMeshParameter.java | 11 +++++++++-- 17 files changed, 67 insertions(+), 26 deletions(-) create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java diff --git a/src/main/java/org/sunflow/core/parameter/geometry/BanchOffParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/BanchOffParameter.java index 82a407b..c7495bf 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/BanchOffParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/BanchOffParameter.java @@ -2,11 +2,12 @@ import org.sunflow.SunflowAPIInterface; -public class BanchOffParameter extends ObjectParameter { +public class BanchOffParameter extends GeometryParameter { @Override public void setup(SunflowAPIInterface api) { super.setup(api); api.geometry(name, TYPE_BANCHOFF); + setupInstance(api); } } diff --git a/src/main/java/org/sunflow/core/parameter/geometry/BezierMeshParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/BezierMeshParameter.java index 4f95853..437b5eb 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/BezierMeshParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/BezierMeshParameter.java @@ -2,7 +2,7 @@ import org.sunflow.SunflowAPIInterface; -public class BezierMeshParameter extends ObjectParameter { +public class BezierMeshParameter extends GeometryParameter { int nu, nv; boolean uwrap = false, vwrap = false; @@ -22,6 +22,8 @@ public void setup(SunflowAPIInterface api) { api.parameter("subdivs", subdivs); api.parameter("smooth", smooth); api.geometry(name, TYPE_BEZIER_MESH); + + setupInstance(api); } public int getNu() { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/CylinderParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/CylinderParameter.java index 6125928..a1087e4 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/CylinderParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/CylinderParameter.java @@ -2,11 +2,13 @@ import org.sunflow.SunflowAPIInterface; -public class CylinderParameter extends ObjectParameter { +public class CylinderParameter extends GeometryParameter { @Override public void setup(SunflowAPIInterface api) { super.setup(api); api.geometry(name, TYPE_CYLINDER); + + setupInstance(api); } } diff --git a/src/main/java/org/sunflow/core/parameter/geometry/FileMeshParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/FileMeshParameter.java index b0e1172..edf1b58 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/FileMeshParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/FileMeshParameter.java @@ -2,7 +2,7 @@ import org.sunflow.SunflowAPIInterface; -public class FileMeshParameter extends ObjectParameter { +public class FileMeshParameter extends GeometryParameter { String filename; boolean smoothNormals = false; @@ -13,6 +13,7 @@ public void setup(SunflowAPIInterface api) { api.parameter("filename", filename); api.parameter("smooth_normals", smoothNormals); api.geometry(name, TYPE_FILE_MESH); + setupInstance(api); } public String getFilename() { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/GenericMeshParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/GenericMeshParameter.java index 7d72a5f..e59b557 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/GenericMeshParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/GenericMeshParameter.java @@ -2,7 +2,7 @@ import org.sunflow.SunflowAPIInterface; -public class GenericMeshParameter extends ObjectParameter { +public class GenericMeshParameter extends GeometryParameter { float[] points; int[] triangles; @@ -37,6 +37,8 @@ public void setup(SunflowAPIInterface api) { } api.geometry(name, TYPE_TRIANGLE_MESH); + + setupInstance(api); } public float[] getPoints() { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java new file mode 100644 index 0000000..35c8d3f --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java @@ -0,0 +1,15 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; + +public abstract class GeometryParameter extends ObjectParameter { + + public void setupInstance(SunflowAPIInterface api) { + if (instanceParameter != null) { + instanceParameter.setName(name + ".instance"); + instanceParameter.setGeometry(name); + instanceParameter.setup(api); + } + } + +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/GumboParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/GumboParameter.java index c1a05a6..1264fb9 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/GumboParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/GumboParameter.java @@ -2,7 +2,7 @@ import org.sunflow.SunflowAPIInterface; -public class GumboParameter extends ObjectParameter { +public class GumboParameter extends GeometryParameter { int subdivs; boolean smooth; @@ -19,6 +19,7 @@ public void setup(SunflowAPIInterface api) { api.parameter("subdivs", subdivs); api.parameter("smooth", smooth); api.geometry(name, TYPE_GUMBO); + setupInstance(api); } public int getSubdivs() { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/HairParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/HairParameter.java index d09626d..bd5a4d9 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/HairParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/HairParameter.java @@ -2,7 +2,7 @@ import org.sunflow.SunflowAPIInterface; -public class HairParameter extends ObjectParameter { +public class HairParameter extends GeometryParameter { int segments; float width; @@ -15,6 +15,7 @@ public void setup(SunflowAPIInterface api) { api.parameter("widths", width); api.parameter("points", "point", "vertex", points); api.geometry(name, TYPE_HAIR); + setupInstance(api); } public int getSegments() { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/JuliaParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/JuliaParameter.java index 5d568fd..89b6c7c 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/JuliaParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/JuliaParameter.java @@ -2,7 +2,7 @@ import org.sunflow.SunflowAPIInterface; -public class JuliaParameter extends ObjectParameter { +public class JuliaParameter extends GeometryParameter { // Quaternion float cx, cy, cz, cw; @@ -22,6 +22,8 @@ public void setup(SunflowAPIInterface api) { api.parameter("epsilon", epsilon); api.geometry(name, TYPE_JULIA); + + setupInstance(api); } public float getCx() { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java index d0f36db..36727ba 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java @@ -23,7 +23,7 @@ public class ObjectParameter implements Parameter { public static final String PARAM_ACCEL = "accel"; - protected String name; + protected String name = "none"; protected String accel = ""; protected InstanceParameter instanceParameter; diff --git a/src/main/java/org/sunflow/core/parameter/geometry/ParticlesParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/ParticlesParameter.java index 89e312d..7464405 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/ParticlesParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/ParticlesParameter.java @@ -2,7 +2,7 @@ import org.sunflow.SunflowAPIInterface; -public class ParticlesParameter extends ObjectParameter { +public class ParticlesParameter extends GeometryParameter { int num; float radius; @@ -15,6 +15,8 @@ public void setup(SunflowAPIInterface api) { api.parameter("num", num); api.parameter("radius", radius); api.geometry(name, TYPE_PARTICLES); + + setupInstance(api); } public int getNum() { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/PlaneParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/PlaneParameter.java index 72391ba..4b6aa73 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/PlaneParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/PlaneParameter.java @@ -4,7 +4,7 @@ import org.sunflow.math.Point3; import org.sunflow.math.Vector3; -public class PlaneParameter extends ObjectParameter { +public class PlaneParameter extends GeometryParameter { Point3 center; Point3 point1; @@ -22,6 +22,8 @@ public void setup(SunflowAPIInterface api) { api.parameter("point2", point2); } api.geometry(name, TYPE_PLANE); + + setupInstance(api); } public Point3 getCenter() { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/SphereFlakeParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/SphereFlakeParameter.java index 87d3ec5..7074c1c 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/SphereFlakeParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/SphereFlakeParameter.java @@ -3,7 +3,7 @@ import org.sunflow.SunflowAPIInterface; import org.sunflow.math.Vector3; -public class SphereFlakeParameter extends ObjectParameter { +public class SphereFlakeParameter extends GeometryParameter { int level = 2; float radius = 1; @@ -16,6 +16,11 @@ public SphereFlakeParameter() { axis = new Vector3(0, 0, 1); } + public SphereFlakeParameter(String name) { + this(); + this.name = name; + } + @Override public void setup(SunflowAPIInterface api) { super.setup(api); @@ -31,6 +36,8 @@ public void setup(SunflowAPIInterface api) { } api.geometry(name, TYPE_SPHEREFLAKE); + + setupInstance(api); } public int getLevel() { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/SphereParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/SphereParameter.java index 9d442ba..c52eaef 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/SphereParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/SphereParameter.java @@ -4,7 +4,7 @@ import org.sunflow.math.Matrix4; import org.sunflow.math.Point3; -public class SphereParameter extends ObjectParameter { +public class SphereParameter extends GeometryParameter { Point3 center; float radius; @@ -15,15 +15,7 @@ public void setup(SunflowAPIInterface api) { api.geometry(name, TYPE_SPHERE); api.parameter("transform", Matrix4.translation(center.x, center.y, center.z).multiply(Matrix4.scale(radius))); - if (instanceParameter != null) { - if (instanceParameter.getShaders() != null) { - api.parameter("shaders", instanceParameter.getShaders()); - } - if (instanceParameter.getModifiers() != null) { - api.parameter("modifiers", instanceParameter.getModifiers()); - } - } - api.instance(name + ".instance", name); + setupInstance(api); } public Point3 getCenter() { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java index 9abfc82..3c6e48e 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java @@ -2,7 +2,7 @@ import org.sunflow.SunflowAPIInterface; -public class TeapotParameter extends ObjectParameter { +public class TeapotParameter extends GeometryParameter { int subdivs = 1; boolean smooth = false; @@ -13,6 +13,8 @@ public void setup(SunflowAPIInterface api) { api.parameter("subdivs", subdivs); api.parameter("smooth", smooth); api.geometry(name, TYPE_TEAPOT); + + setupInstance(api); } public int getSubdivs() { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/TorusParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/TorusParameter.java index 7a9afb1..a9069c8 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/TorusParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/TorusParameter.java @@ -2,7 +2,7 @@ import org.sunflow.SunflowAPIInterface; -public class TorusParameter extends ObjectParameter { +public class TorusParameter extends GeometryParameter { float radiusInner; float radiusOuter; @@ -13,6 +13,8 @@ public void setup(SunflowAPIInterface api) { api.parameter("radiusInner", radiusInner); api.parameter("radiusOuter", radiusOuter); api.geometry(name, TYPE_TORUS); + + setupInstance(api); } public float getRadiusInner() { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/TriangleMeshParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/TriangleMeshParameter.java index 0898008..df040e2 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/TriangleMeshParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/TriangleMeshParameter.java @@ -2,7 +2,7 @@ import org.sunflow.SunflowAPIInterface; -public class TriangleMeshParameter extends ObjectParameter { +public class TriangleMeshParameter extends GeometryParameter { float[] points; float[] normals; @@ -12,14 +12,21 @@ public class TriangleMeshParameter extends ObjectParameter { @Override public void setup(SunflowAPIInterface api) { super.setup(api); + if (name == null || name.isEmpty()) { + throw new RuntimeException("Name cannot be null"); + } // create geometry api.parameter("triangles", triangles); api.parameter("points", "point", "vertex", points); if (normals != null) { api.parameter("normals", "vector", "vertex", normals); } - api.parameter("uvs", "texcoord", "vertex", uvs); + if (uvs != null) { + api.parameter("uvs", "texcoord", "vertex", uvs); + } api.geometry(name, "triangle_mesh"); + + setupInstance(api); } public float[] getPoints() { From 57f68c7b2e6d87c5b7272ad82a0e38070c6ab5f8 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sat, 13 Apr 2019 14:37:25 -0300 Subject: [PATCH 17/37] Adds convenient methods to ObjectParameter and turns InstanceParameter into fluent API --- .../java/examples/CornellBoxJensenScene.java | 46 ++++------ .../core/parameter/InstanceParameter.java | 35 ++++--- .../core/parameter/TransformParameter.java | 37 +++++++- .../parameter/camera/CameraParameter.java | 7 ++ .../parameter/geometry/GeometryParameter.java | 7 +- .../parameter/geometry/ObjectParameter.java | 91 +++++++++++++++++++ .../parameter/geometry/TeapotParameter.java | 8 ++ .../shader/ShinyShaderParameter.java | 18 ++-- .../org/sunflow/core/parser/SCNewParser.java | 32 ++++--- .../org/sunflow/core/parser/SCParser.java | 32 ++++--- .../core/parameter/InstanceParameterTest.java | 3 +- 11 files changed, 231 insertions(+), 85 deletions(-) diff --git a/src/main/java/examples/CornellBoxJensenScene.java b/src/main/java/examples/CornellBoxJensenScene.java index 6c53058..6d6efa2 100644 --- a/src/main/java/examples/CornellBoxJensenScene.java +++ b/src/main/java/examples/CornellBoxJensenScene.java @@ -1,9 +1,6 @@ package examples; -import org.sunflow.PluginRegistry; import org.sunflow.SunflowAPI; -import org.sunflow.core.ImageSampler; -import org.sunflow.core.Options; import org.sunflow.core.parameter.ImageParameter; import org.sunflow.core.parameter.InstanceParameter; import org.sunflow.core.parameter.PhotonParameter; @@ -12,7 +9,6 @@ import org.sunflow.core.parameter.geometry.SphereParameter; import org.sunflow.core.parameter.gi.InstantGIParameter; import org.sunflow.core.parameter.light.CornellBoxLightParameter; -import org.sunflow.core.parameter.shader.DiffuseShaderParameter; import org.sunflow.core.parameter.shader.GlassShaderParameter; import org.sunflow.core.parameter.shader.MirrorShaderParameter; import org.sunflow.image.Color; @@ -57,11 +53,11 @@ public static void main(String[] args) { PinholeCameraParameter camera = new PinholeCameraParameter(); camera.setName("camera"); - Point3 eye = new Point3(0,-205,50); - Point3 target = new Point3(0,0,50); - Vector3 up = new Vector3(0,0,1); - // TODO Move logic to camera - api.parameter("transform", Matrix4.lookAt(eye, target, up)); + Point3 eye = new Point3(0, -205, 50); + Point3 target = new Point3(0, 0, 50); + Vector3 up = new Vector3(0, 0, 1); + + camera.setupTransform(api, eye,target,up); camera.setFov(45f); camera.setAspect(1.333333f); @@ -69,43 +65,39 @@ public static void main(String[] args) { // Materials MirrorShaderParameter mirror = new MirrorShaderParameter("Mirror"); - mirror.setReflection(new Color(0.7f,0.7f,0.7f)); + mirror.setReflection(new Color(0.7f, 0.7f, 0.7f)); mirror.setup(api); GlassShaderParameter glass = new GlassShaderParameter("Glass"); glass.setEta(1.6f); - glass.setAbsorptionColor(new Color(1,1,1)); + glass.setAbsorptionColor(new Color(1, 1, 1)); glass.setup(api); // Lights CornellBoxLightParameter lightParameter = new CornellBoxLightParameter(); lightParameter.setName("cornell-box-light"); - lightParameter.setMin(new Point3(-60,-60,0)); - lightParameter.setMax(new Point3(60,60,100)); - lightParameter.setLeft(new Color(0.8f,0.25f,0.25f)); - lightParameter.setRight(new Color(0.25f,0.25f,0.8f)); - lightParameter.setTop(new Color(0.7f,0.7f,0.7f)); - lightParameter.setBottom(new Color(0.7f,0.7f,0.7f)); - lightParameter.setBack(new Color(0.7f,0.7f,0.7f)); - lightParameter.setRadiance(new Color(15,15,15)); + lightParameter.setMin(new Point3(-60, -60, 0)); + lightParameter.setMax(new Point3(60, 60, 100)); + lightParameter.setLeft(new Color(0.8f, 0.25f, 0.25f)); + lightParameter.setRight(new Color(0.25f, 0.25f, 0.8f)); + lightParameter.setTop(new Color(0.7f, 0.7f, 0.7f)); + lightParameter.setBottom(new Color(0.7f, 0.7f, 0.7f)); + lightParameter.setBack(new Color(0.7f, 0.7f, 0.7f)); + lightParameter.setRadiance(new Color(15, 15, 15)); lightParameter.setSamples(32); lightParameter.setup(api); - InstanceParameter mirrorSphereInstance = new InstanceParameter(); - mirrorSphereInstance.setShaders(new String[]{"Mirror"}); SphereParameter mirrorSphere = new SphereParameter(); mirrorSphere.setName("mirror-sphere"); - mirrorSphere.setCenter(new Point3(-30,30,20)); - mirrorSphere.setInstanceParameter(mirrorSphereInstance); + mirrorSphere.setCenter(new Point3(-30, 30, 20)); + mirrorSphere.setInstanceParameter(new InstanceParameter().shaders("Mirror")); mirrorSphere.setRadius(20); mirrorSphere.setup(api); - InstanceParameter glassSphereInstance = new InstanceParameter(); - glassSphereInstance.setShaders(new String[]{"Glass"}); SphereParameter glassSphere = new SphereParameter(); glassSphere.setName("glass-sphere"); - glassSphere.setCenter(new Point3(28,2,20)); - glassSphere.setInstanceParameter(glassSphereInstance); + glassSphere.setCenter(new Point3(28, 2, 20)); + glassSphere.setInstanceParameter(new InstanceParameter().shaders("Glass")); glassSphere.setRadius(20); glassSphere.setup(api); diff --git a/src/main/java/org/sunflow/core/parameter/InstanceParameter.java b/src/main/java/org/sunflow/core/parameter/InstanceParameter.java index 7c97b05..0f509e4 100644 --- a/src/main/java/org/sunflow/core/parameter/InstanceParameter.java +++ b/src/main/java/org/sunflow/core/parameter/InstanceParameter.java @@ -4,13 +4,13 @@ public class InstanceParameter implements Parameter { - String name; - String geometry; + private String name; + private String geometry; - String[] shaders = null; - String[] modifiers = null; + private String[] shaders = null; + private String[] modifiers = null; - TransformParameter transform = null; + private TransformParameter transform = null; @Override public void setup(SunflowAPIInterface api) { @@ -27,43 +27,48 @@ public void setup(SunflowAPIInterface api) { api.instance(name, geometry); } - public String getName() { + public String name() { return name; } - public void setName(String name) { + public InstanceParameter name(String name) { this.name = name; + return this; } - public String getGeometry() { + public String geometry() { return geometry; } - public void setGeometry(String geometry) { + public InstanceParameter geometry(String geometry) { this.geometry = geometry; + return this; } - public String[] getShaders() { + public String[] shaders() { return shaders; } - public void setShaders(String[] shaders) { + public InstanceParameter shaders(String... shaders) { this.shaders = shaders; + return this; } - public String[] getModifiers() { + public String[] modifiers() { return modifiers; } - public void setModifiers(String[] modifiers) { + public InstanceParameter modifiers(String... modifiers) { this.modifiers = modifiers; + return this; } - public TransformParameter getTransform() { + public TransformParameter transform() { return transform; } - public void setTransform(TransformParameter transform) { + public InstanceParameter transform(TransformParameter transform) { this.transform = transform; + return this; } } diff --git a/src/main/java/org/sunflow/core/parameter/TransformParameter.java b/src/main/java/org/sunflow/core/parameter/TransformParameter.java index b6c0876..20e122a 100644 --- a/src/main/java/org/sunflow/core/parameter/TransformParameter.java +++ b/src/main/java/org/sunflow/core/parameter/TransformParameter.java @@ -8,7 +8,7 @@ public class TransformParameter implements Parameter { public static final String INTERPOLATION_NONE = "none"; float[] times; - Matrix4[] transforms; + Matrix4[] transforms = new Matrix4[]{Matrix4.IDENTITY}; String interpolation = INTERPOLATION_NONE; @@ -42,4 +42,39 @@ public void setTransforms(Matrix4[] transforms) { this.transforms = transforms; } + public TransformParameter rotateX(float angle) { + Matrix4 t = Matrix4.rotateX((float) Math.toRadians(angle)); + transforms[0] = t.multiply(transforms[0]); + return this; + } + + public TransformParameter rotateY(float angle) { + Matrix4 t = Matrix4.rotateY((float) Math.toRadians(angle)); + transforms[0] = t.multiply(transforms[0]); + return this; + } + + public TransformParameter rotateZ(float angle) { + Matrix4 t = Matrix4.rotateZ((float) Math.toRadians(angle)); + transforms[0] = t.multiply(transforms[0]); + return this; + } + + public TransformParameter scale(float scale) { + Matrix4 t = Matrix4.scale(scale); + transforms[0] = t.multiply(transforms[0]); + return this; + } + + public TransformParameter scale(float x, float y, float z) { + Matrix4 t = Matrix4.scale(x, y, z); + transforms[0] = t.multiply(transforms[0]); + return this; + } + + public TransformParameter translate(float x, float y, float z) { + Matrix4 t = Matrix4.translation(x, y, z); + transforms[0] = t.multiply(transforms[0]); + return this; + } } diff --git a/src/main/java/org/sunflow/core/parameter/camera/CameraParameter.java b/src/main/java/org/sunflow/core/parameter/camera/CameraParameter.java index 95a1742..70d9675 100644 --- a/src/main/java/org/sunflow/core/parameter/camera/CameraParameter.java +++ b/src/main/java/org/sunflow/core/parameter/camera/CameraParameter.java @@ -3,6 +3,9 @@ import org.sunflow.SunflowAPI; import org.sunflow.SunflowAPIInterface; import org.sunflow.core.parameter.Parameter; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; public class CameraParameter implements Parameter { @@ -54,4 +57,8 @@ public void setup(SunflowAPIInterface api) { api.parameter(PARAM_CAMERA, name); api.options(SunflowAPI.DEFAULT_OPTIONS); } + + public void setupTransform(SunflowAPI api, Point3 eye, Point3 target, Vector3 up) { + api.parameter("transform", Matrix4.lookAt(eye, target, up)); + } } diff --git a/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java index 35c8d3f..42c9282 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java @@ -6,9 +6,10 @@ public abstract class GeometryParameter extends ObjectParameter { public void setupInstance(SunflowAPIInterface api) { if (instanceParameter != null) { - instanceParameter.setName(name + ".instance"); - instanceParameter.setGeometry(name); - instanceParameter.setup(api); + instanceParameter + .name(name + ".instance") + .geometry(name) + .setup(api); } } diff --git a/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java index 36727ba..d779b4a 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java @@ -3,6 +3,9 @@ import org.sunflow.SunflowAPIInterface; import org.sunflow.core.parameter.InstanceParameter; import org.sunflow.core.parameter.Parameter; +import org.sunflow.core.parameter.TransformParameter; +import org.sunflow.core.parameter.modifier.ModifierParameter; +import org.sunflow.core.parameter.shader.ShaderParameter; public class ObjectParameter implements Parameter { @@ -58,4 +61,92 @@ public void setup(SunflowAPIInterface api) { api.parameter(PARAM_ACCEL, accel); } } + + public void shaders(String... shaders) { + if (instanceParameter == null) { + instanceParameter = new InstanceParameter(); + } + + instanceParameter.shaders(shaders); + } + + public void shaders(ShaderParameter... shaders) { + if (instanceParameter == null) { + instanceParameter = new InstanceParameter(); + } + + String[] names = new String[shaders.length]; + for (int i = 0; i < shaders.length; i++) { + names[i] = shaders[i].getName(); + } + + instanceParameter.shaders(names); + + } + + public void modifiers(String... modifiers) { + if (instanceParameter == null) { + instanceParameter = new InstanceParameter(); + } + + instanceParameter.shaders(modifiers); + } + + public void modifiers(ModifierParameter... modifiers) { + if (instanceParameter == null) { + instanceParameter = new InstanceParameter(); + } + + String[] names = new String[modifiers.length]; + for (int i = 0; i < modifiers.length; i++) { + names[i] = modifiers[i].getName(); + } + + instanceParameter.modifiers(names); + } + + public void rotateX(float angle) { + TransformParameter transformParameter = getInstanceTransform(); + transformParameter.rotateX(angle); + } + + public void rotateY(float angle) { + TransformParameter transformParameter = getInstanceTransform(); + transformParameter.rotateY(angle); + } + + public void rotateZ(float angle) { + TransformParameter transformParameter = getInstanceTransform(); + transformParameter.rotateZ(angle); + } + + public void scale(float scale) { + TransformParameter transformParameter = getInstanceTransform(); + transformParameter.scale(scale); + } + + public void scale(float x, float y, float z) { + TransformParameter transformParameter = getInstanceTransform(); + transformParameter.scale(x, y, z); + } + + public void translate(float x, float y, float z) { + TransformParameter transformParameter = getInstanceTransform(); + transformParameter.translate(x, y, z); + } + + private TransformParameter getInstanceTransform() { + if (instanceParameter == null) { + instanceParameter = new InstanceParameter(); + } + + TransformParameter transformParameter = instanceParameter.transform(); + + if (transformParameter == null) { + transformParameter = new TransformParameter(); + instanceParameter.transform(transformParameter); + } + return transformParameter; + } + } diff --git a/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java index 3c6e48e..62dc9f8 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java @@ -7,6 +7,14 @@ public class TeapotParameter extends GeometryParameter { int subdivs = 1; boolean smooth = false; + public TeapotParameter() { + + } + + public TeapotParameter(String name) { + this.name = name; + } + @Override public void setup(SunflowAPIInterface api) { super.setup(api); diff --git a/src/main/java/org/sunflow/core/parameter/shader/ShinyShaderParameter.java b/src/main/java/org/sunflow/core/parameter/shader/ShinyShaderParameter.java index e76df6d..0da87b1 100644 --- a/src/main/java/org/sunflow/core/parameter/shader/ShinyShaderParameter.java +++ b/src/main/java/org/sunflow/core/parameter/shader/ShinyShaderParameter.java @@ -7,17 +7,17 @@ public class ShinyShaderParameter extends ShaderParameter { String texture = ""; Color diffuse; - float shiny; + float shininess; public ShinyShaderParameter(String name) { super(name); diffuse = Color.GRAY; - shiny = 0.5f; + shininess = 0.5f; } @Override public void setup(SunflowAPIInterface api) { - api.parameter("shiny", shiny); + api.parameter("shiny", shininess); if (texture.isEmpty()) { api.parameter("diffuse", null, diffuse.getRGB()); @@ -44,11 +44,15 @@ public void setDiffuse(Color diffuse) { this.diffuse = diffuse; } - public float getShiny() { - return shiny; + public float getShininess() { + return shininess; } - public void setShiny(float shiny) { - this.shiny = shiny; + /** + * Refl parameter + * @param shininess + */ + public void setShininess(float shininess) { + this.shininess = shininess; } } diff --git a/src/main/java/org/sunflow/core/parser/SCNewParser.java b/src/main/java/org/sunflow/core/parser/SCNewParser.java index cf28874..ad46f91 100644 --- a/src/main/java/org/sunflow/core/parser/SCNewParser.java +++ b/src/main/java/org/sunflow/core/parser/SCNewParser.java @@ -641,7 +641,7 @@ private boolean parseShader(SunflowAPIInterface api) throws ParserException, IOE shaderParameter.setDiffuse(parseColor()); } p.checkNextToken("refl"); - shaderParameter.setShiny(p.getNextFloat()); + shaderParameter.setShininess(p.getNextFloat()); shaderParameter.setup(api); } else if (p.peekNextToken("ward")) { WardShaderParameter shaderParameter = new WardShaderParameter(name); @@ -790,7 +790,7 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I p.checkNextToken("shader"); shaders = new String[]{p.getNextToken()}; } - instanceParameter.setShaders(shaders); + instanceParameter.shaders(shaders); if (p.peekNextToken("modifiers")) { int n = p.getNextInt(); @@ -800,11 +800,11 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I } else if (p.peekNextToken("modifier")) { modifiers = new String[]{p.getNextToken()}; } - instanceParameter.setModifiers(modifiers); + instanceParameter.modifiers(modifiers); // Can be null TransformParameter transform = checkParseTransform(); - instanceParameter.setTransform(transform); + instanceParameter.transform(transform); } if (p.peekNextToken("accel")) { accel = p.getNextToken(); @@ -893,14 +893,15 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I geometry.setup(api); } else if (type.equals("sphere")) { UI.printInfo(Module.API, "Reading sphere ..."); - instanceParameter.setName(name + ".instance"); - instanceParameter.setGeometry(name); + instanceParameter + .name(name + ".instance") + .geometry(name); SphereParameter geometry = new SphereParameter(); geometry.setName(name); geometry.setInstanceParameter(instanceParameter); - if (instanceParameter.getTransform() == null && !noInstance) { + if (instanceParameter.transform() == null && !noInstance) { // legacy method of specifying transformation for spheres p.checkNextToken("c"); float x = p.getNextFloat(); @@ -1159,8 +1160,9 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I noInstance = true; } if (!noInstance) { - instanceParameter.setName(name + ".instance"); - instanceParameter.setGeometry(name); + instanceParameter + .name(name + ".instance") + .geometry(name); instanceParameter.setup(api); // create instance @@ -1187,13 +1189,13 @@ private void parseInstanceBlock(SunflowAPIInterface api) throws ParserException, InstanceParameter parameter = new InstanceParameter(); p.checkNextToken("{"); p.checkNextToken("name"); - parameter.setName(p.getNextToken()); - UI.printInfo(Module.API, "Reading instance: %s ...", parameter.getName()); + parameter.name(p.getNextToken()); + UI.printInfo(Module.API, "Reading instance: %s ...", parameter.name()); p.checkNextToken("geometry"); - parameter.setGeometry(p.getNextToken()); + parameter.geometry(p.getNextToken()); TransformParameter transformParameter = parseTransform(); - parameter.setTransform(transformParameter); + parameter.transform(transformParameter); String[] shaders; if (p.peekNextToken("shaders")) { @@ -1206,7 +1208,7 @@ private void parseInstanceBlock(SunflowAPIInterface api) throws ParserException, p.checkNextToken("shader"); shaders = new String[]{p.getNextToken()}; } - parameter.setShaders(shaders); + parameter.shaders(shaders); String[] modifiers = null; if (p.peekNextToken("modifiers")) { @@ -1218,7 +1220,7 @@ private void parseInstanceBlock(SunflowAPIInterface api) throws ParserException, } else if (p.peekNextToken("modifier")) { modifiers = new String[]{p.getNextToken()}; } - parameter.setModifiers(modifiers); + parameter.modifiers(modifiers); parameter.setup(api); p.checkNextToken("}"); diff --git a/src/main/java/org/sunflow/core/parser/SCParser.java b/src/main/java/org/sunflow/core/parser/SCParser.java index e7207e1..72848fa 100644 --- a/src/main/java/org/sunflow/core/parser/SCParser.java +++ b/src/main/java/org/sunflow/core/parser/SCParser.java @@ -639,7 +639,7 @@ private boolean parseShader(SunflowAPIInterface api) throws ParserException, IOE shaderParameter.setDiffuse(parseColor()); } p.checkNextToken("refl"); - shaderParameter.setShiny(p.getNextFloat()); + shaderParameter.setShininess(p.getNextFloat()); shaderParameter.setup(api); } else if (p.peekNextToken("ward")) { WardShaderParameter shaderParameter = new WardShaderParameter(name); @@ -788,7 +788,7 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I p.checkNextToken("shader"); shaders = new String[]{p.getNextToken()}; } - instanceParameter.setShaders(shaders); + instanceParameter.shaders(shaders); if (p.peekNextToken("modifiers")) { int n = p.getNextInt(); @@ -798,11 +798,11 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I } else if (p.peekNextToken("modifier")) { modifiers = new String[]{p.getNextToken()}; } - instanceParameter.setModifiers(modifiers); + instanceParameter.modifiers(modifiers); // Can be null TransformParameter transform = checkParseTransform(); - instanceParameter.setTransform(transform); + instanceParameter.transform(transform); } if (p.peekNextToken("accel")) { accel = p.getNextToken(); @@ -891,14 +891,15 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I geometry.setup(api); } else if (type.equals("sphere")) { UI.printInfo(Module.API, "Reading sphere ..."); - instanceParameter.setName(name + ".instance"); - instanceParameter.setGeometry(name); + instanceParameter + .name(name + ".instance") + .geometry(name); SphereParameter geometry = new SphereParameter(); geometry.setName(name); geometry.setInstanceParameter(instanceParameter); - if (instanceParameter.getTransform() == null && !noInstance) { + if (instanceParameter.transform() == null && !noInstance) { // legacy method of specifying transformation for spheres p.checkNextToken("c"); float x = p.getNextFloat(); @@ -1157,8 +1158,9 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I noInstance = true; } if (!noInstance) { - instanceParameter.setName(name + ".instance"); - instanceParameter.setGeometry(name); + instanceParameter + .name(name + ".instance") + .geometry(name); instanceParameter.setup(api); // create instance @@ -1185,13 +1187,13 @@ private void parseInstanceBlock(SunflowAPIInterface api) throws ParserException, InstanceParameter parameter = new InstanceParameter(); p.checkNextToken("{"); p.checkNextToken("name"); - parameter.setName(p.getNextToken()); - UI.printInfo(Module.API, "Reading instance: %s ...", parameter.getName()); + parameter.name(p.getNextToken()); + UI.printInfo(Module.API, "Reading instance: %s ...", parameter.name()); p.checkNextToken("geometry"); - parameter.setGeometry(p.getNextToken()); + parameter.geometry(p.getNextToken()); TransformParameter transformParameter = parseTransform(); - parameter.setTransform(transformParameter); + parameter.transform(transformParameter); String[] shaders; if (p.peekNextToken("shaders")) { @@ -1204,7 +1206,7 @@ private void parseInstanceBlock(SunflowAPIInterface api) throws ParserException, p.checkNextToken("shader"); shaders = new String[]{p.getNextToken()}; } - parameter.setShaders(shaders); + parameter.shaders(shaders); String[] modifiers = null; if (p.peekNextToken("modifiers")) { @@ -1216,7 +1218,7 @@ private void parseInstanceBlock(SunflowAPIInterface api) throws ParserException, } else if (p.peekNextToken("modifier")) { modifiers = new String[]{p.getNextToken()}; } - parameter.setModifiers(modifiers); + parameter.modifiers(modifiers); parameter.setup(api); p.checkNextToken("}"); diff --git a/src/test/java/org/sunflow/core/parameter/InstanceParameterTest.java b/src/test/java/org/sunflow/core/parameter/InstanceParameterTest.java index 344c8a3..4948e37 100644 --- a/src/test/java/org/sunflow/core/parameter/InstanceParameterTest.java +++ b/src/test/java/org/sunflow/core/parameter/InstanceParameterTest.java @@ -22,8 +22,7 @@ public void setUp() { @Test public void testSetupAPI() { // Set values - parameter.setName("uniqueName"); - + parameter.name("uniqueName"); parameter.setup(api); verify(api, times(1)).instance(anyString(), anyString()); From 540be2dd484387f4f106079a5e237e0486d8ca1d Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sat, 13 Apr 2019 14:38:44 -0300 Subject: [PATCH 18/37] Adds GumboAndTeapot and SphereFlake as procedural scenes --- .../java/examples/GumboAndTeapotScene.java | 205 ++++++++++++++++++ src/main/java/examples/SphereFlakeScene.java | 119 ++++++++++ 2 files changed, 324 insertions(+) create mode 100644 src/main/java/examples/GumboAndTeapotScene.java create mode 100644 src/main/java/examples/SphereFlakeScene.java diff --git a/src/main/java/examples/GumboAndTeapotScene.java b/src/main/java/examples/GumboAndTeapotScene.java new file mode 100644 index 0000000..15a6322 --- /dev/null +++ b/src/main/java/examples/GumboAndTeapotScene.java @@ -0,0 +1,205 @@ +package examples; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.parameter.ImageParameter; +import org.sunflow.core.parameter.camera.PinholeCameraParameter; +import org.sunflow.core.parameter.geometry.GumboParameter; +import org.sunflow.core.parameter.geometry.PlaneParameter; +import org.sunflow.core.parameter.geometry.TeapotParameter; +import org.sunflow.core.parameter.light.SunSkyLightParameter; +import org.sunflow.core.parameter.shader.DiffuseShaderParameter; +import org.sunflow.core.parameter.shader.ShinyShaderParameter; +import org.sunflow.image.Color; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class GumboAndTeapotScene { + + public static void main(String[] args) { + SunflowAPI api = new SunflowAPI(); + api.reset(); + + ImageParameter image = new ImageParameter(); + image.setResolutionX(800); + image.setResolutionY(450); + image.setAAMin(0); + image.setAAMax(1); + image.setFilter(ImageParameter.FILTER_TRIANGLE); + image.setup(api); + + PinholeCameraParameter camera = new PinholeCameraParameter(); + + camera.setName("camera"); + Point3 eye = new Point3(-18.19f, 8.97f, -0.93f); + Point3 target = new Point3(-0.690f, 0.97f, -0.93f); + Vector3 up = new Vector3(0, 1, 0); + + camera.setupTransform(api, eye, target, up); + + camera.setFov(30f); + camera.setAspect(1.777777777777f); + camera.setup(api); + + SunSkyLightParameter lightParameter = new SunSkyLightParameter(); + lightParameter.setName("sunsky"); + lightParameter.setUp(new Vector3(0, 1, 0)); + lightParameter.setEast(new Vector3(0, 0, 1)); + lightParameter.setSunDirection(new Vector3(1, 1, 1)); + lightParameter.setTurbidity(4); + lightParameter.setSamples(64); + lightParameter.setup(api); + + // Materials + ShinyShaderParameter shiny = new ShinyShaderParameter("default"); + shiny.setDiffuse(new Color(0.2f, 0.2f, 0.2f)); + shiny.setShininess(0.1f); + shiny.setup(api); + + DiffuseShaderParameter simple = new DiffuseShaderParameter("simple"); + simple.setDiffuse(new Color(0.2f, 0.2f, 0.2f)); + simple.setup(api); + + DiffuseShaderParameter simpleRed = new DiffuseShaderParameter("simple_red"); + simpleRed.setDiffuse(new Color(0.8f, 0.2f, 0.2f).toLinear()); + simpleRed.setup(api); + + DiffuseShaderParameter simpleGreen = new DiffuseShaderParameter("simple_green"); + simpleGreen.setDiffuse(new Color(0.2f, 0.8f, 0.2f).toLinear()); + simpleGreen.setup(api); + + DiffuseShaderParameter simpleBlue = new DiffuseShaderParameter("simple_blue"); + simpleBlue.setDiffuse(new Color(0.2f, 0.2f, 0.8f).toLinear()); + simpleBlue.setup(api); + + DiffuseShaderParameter simpleYellow = new DiffuseShaderParameter("simple_yellow"); + simpleYellow.setDiffuse(new Color(0.8f, 0.8f, 0.2f).toLinear()); + simpleYellow.setup(api); + + DiffuseShaderParameter floorShader = new DiffuseShaderParameter("floor"); + floorShader.setDiffuse(new Color(0.1f, 0.1f, 0.1f)); + floorShader.setup(api); + + GumboParameter gumbo0 = new GumboParameter(); + gumbo0.setName("gumbo_0"); + gumbo0.setSubdivs(7); + gumbo0.shaders(shiny); + gumbo0.rotateX(-90); + gumbo0.scale(0.1f); + gumbo0.rotateY(75); + gumbo0.translate(-0.25f, 0, 0.63f); + gumbo0.setup(api); + + GumboParameter gumbo1 = new GumboParameter(); + gumbo1.setName("gumbo_1"); + gumbo1.setSubdivs(4); + gumbo1.shaders(simpleRed); + gumbo1.rotateX(-90); + gumbo1.scale(0.1f); + gumbo1.rotateY(25); + gumbo1.translate(1.5f, 0, -1.5f); + gumbo1.setup(api); + + GumboParameter gumbo2 = new GumboParameter(); + gumbo2.setName("gumbo_2"); + gumbo2.setSubdivs(3); + gumbo2.shaders(simpleBlue); + gumbo2.rotateX(-90); + gumbo2.scale(0.1f); + gumbo2.rotateY(25); + gumbo2.translate(0, 0, -3f); + gumbo2.setSmooth(false); + gumbo2.setup(api); + + GumboParameter gumbo3 = new GumboParameter(); + gumbo3.setName("gumbo_3"); + gumbo3.setSubdivs(6); + gumbo3.shaders(simpleGreen); + gumbo3.rotateX(-90); + gumbo3.scale(0.1f); + gumbo3.rotateY(-25); + gumbo3.translate(1.5f, 0, 1.5f); + gumbo3.setSmooth(false); + gumbo3.setup(api); + + GumboParameter gumbo4 = new GumboParameter(); + gumbo4.setName("gumbo_4"); + gumbo4.setSubdivs(8); + gumbo4.shaders(simpleYellow); + gumbo4.rotateX(-90); + gumbo4.scale(0.1f); + gumbo4.rotateY(-25); + gumbo4.translate(0f, 0, 3f); + gumbo4.setSmooth(false); + gumbo4.setup(api); + + PlaneParameter floor = new PlaneParameter(); + floor.shaders(floorShader); + floor.setCenter(new Point3(0, 0, 0)); + floor.setNormal(new Vector3(0, 1, 0)); + floor.setup(api); + + TeapotParameter teapot0 = new TeapotParameter("teapot_0"); + teapot0.shaders(shiny); + teapot0.rotateX(-90); + teapot0.scale(0.008f); + teapot0.rotateY(245f); + teapot0.translate(-3, 0, -1); + teapot0.setSubdivs(7); + teapot0.setup(api); + + TeapotParameter teapot1 = new TeapotParameter("teapot_1"); + teapot1.shaders(simpleYellow); + teapot1.rotateX(-90); + teapot1.scale(0.008f); + teapot1.rotateY(245f); + teapot1.translate(-1.5f, 0, -3); + teapot1.setSubdivs(4); + teapot1.setSmooth(false); + teapot1.setup(api); + + TeapotParameter teapot2 = new TeapotParameter("teapot_2"); + teapot2.shaders(simpleGreen); + teapot2.rotateX(-90); + teapot2.scale(0.008f); + teapot2.rotateY(245f); + teapot2.translate(0, 0, -5); + teapot2.setSubdivs(3); + teapot2.setSmooth(false); + teapot2.setup(api); + + TeapotParameter teapot3 = new TeapotParameter("teapot_3"); + teapot3.shaders(simpleRed); + teapot3.rotateX(-90); + teapot3.scale(0.008f); + teapot3.rotateY(245f); + teapot3.translate(-1.5f, 0, 1); + teapot3.setSubdivs(5); + teapot3.setSmooth(false); + teapot3.setup(api); + + TeapotParameter teapot4 = new TeapotParameter("teapot_4"); + teapot4.shaders(simpleBlue); + teapot4.rotateX(-90); + teapot4.scale(0.008f); + teapot4.rotateY(245f); + teapot4.translate(0, 0, 3); + teapot4.setSubdivs(7); + teapot4.setSmooth(false); + teapot4.setup(api); + + finalRender(api); + } + + private static void previewRender(SunflowAPI api) { + api.parameter("sampler", "ipr"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + + private static void finalRender(SunflowAPI api) { + api.parameter("sampler", "bucket"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + +} diff --git a/src/main/java/examples/SphereFlakeScene.java b/src/main/java/examples/SphereFlakeScene.java new file mode 100644 index 0000000..0c7b98d --- /dev/null +++ b/src/main/java/examples/SphereFlakeScene.java @@ -0,0 +1,119 @@ +package examples; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.parameter.ImageParameter; +import org.sunflow.core.parameter.InstanceParameter; +import org.sunflow.core.parameter.TraceDepthsParameter; +import org.sunflow.core.parameter.camera.PinholeCameraParameter; +import org.sunflow.core.parameter.camera.ThinLensCameraParameter; +import org.sunflow.core.parameter.geometry.PlaneParameter; +import org.sunflow.core.parameter.geometry.SphereFlakeParameter; +import org.sunflow.core.parameter.geometry.TeapotParameter; +import org.sunflow.core.parameter.gi.GlobalIlluminationParameter; +import org.sunflow.core.parameter.gi.PathTracingGIParameter; +import org.sunflow.core.parameter.light.SunSkyLightParameter; +import org.sunflow.core.parameter.modifier.BumpMapModifierParameter; +import org.sunflow.core.parameter.modifier.NormalMapModifierParameter; +import org.sunflow.core.parameter.shader.DiffuseShaderParameter; +import org.sunflow.core.parameter.shader.GlassShaderParameter; +import org.sunflow.core.parameter.shader.PhongShaderParameter; +import org.sunflow.core.parameter.shader.ShinyShaderParameter; +import org.sunflow.image.Color; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class SphereFlakeScene { + + public static void main(String[] args) { + SunflowAPI api = new SunflowAPI(); + api.reset(); + + ImageParameter image = new ImageParameter(); + image.setResolutionX(1920); + image.setResolutionY(1080); + image.setAAMin(4); + image.setAAMax(4); + image.setAASamples(4); + image.setFilter(ImageParameter.FILTER_GAUSSIAN); + image.setup(api); + + TraceDepthsParameter traceDepths = new TraceDepthsParameter(); + traceDepths.setDiffuse(1); + traceDepths.setReflection(1); + traceDepths.setRefraction(0); + traceDepths.setup(api); + + ThinLensCameraParameter camera = new ThinLensCameraParameter(); + + camera.setName("camera"); + + Point3 eye = new Point3(-5, 0, -0.9f); + Point3 target = new Point3(0, 0, 0.2f); + Vector3 up = new Vector3(0, 0, 1); + + camera.setupTransform(api, eye, target, up); + + camera.setFov(60f); + camera.setAspect(1.777777777777f); + camera.setFocusDistance(5); + camera.setLensRadius(0.01f); + camera.setup(api); + + PathTracingGIParameter gi = new PathTracingGIParameter(); + gi.setSamples(16); + gi.setup(api); + + DiffuseShaderParameter simple1 = new DiffuseShaderParameter("simple1"); + simple1.setDiffuse(new Color(0.5f, 0.5f, 0.5f)); + simple1.setup(api); + + GlassShaderParameter glassy = new GlassShaderParameter("glassy"); + glassy.setEta(1.333f); + glassy.setColor(new Color(0.8f, 0.8f, 0.8f)); + glassy.setAbsorptionDistance(15); + glassy.setAbsorptionColor(new Color(0.2f, 0.7f, 0.2f).toNonLinear()); + glassy.setup(api); + + SunSkyLightParameter lightParameter = new SunSkyLightParameter(); + lightParameter.setName("sunsky"); + lightParameter.setUp(new Vector3(0, 0, 1)); + lightParameter.setEast(new Vector3(0, 1, 0)); + lightParameter.setSunDirection(new Vector3(-1, 1, 0.2f)); + lightParameter.setTurbidity(2); + lightParameter.setSamples(32); + lightParameter.setup(api); + + PhongShaderParameter metal = new PhongShaderParameter("metal"); + metal.setDiffuse(new Color(0.1f,0.1f,0.1f)); + metal.setSpecular(new Color(0.1f,0.1f,0.1f)); + metal.setSamples(4); + metal.setup(api); + + SphereFlakeParameter sphereFlakeParameter = new SphereFlakeParameter("flake"); + sphereFlakeParameter.setInstanceParameter(new InstanceParameter().shaders("metal")); + sphereFlakeParameter.setLevel(7); + sphereFlakeParameter.setup(api); + + PlaneParameter planeParameter = new PlaneParameter(); + planeParameter.shaders(simple1); + planeParameter.setCenter(new Point3(0,0,-1)); + planeParameter.setNormal(new Vector3(0,0,1)); + planeParameter.setup(api); + + finalRender(api); + } + + private static void previewRender(SunflowAPI api) { + api.parameter("sampler", "ipr"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + + private static void finalRender(SunflowAPI api) { + api.parameter("sampler", "bucket"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + +} From 294653374ab86c76962bc1f2dab72bf3034aea0c Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Fri, 3 Aug 2018 10:22:22 -0300 Subject: [PATCH 19/37] Add self-intersection bias parameter based on k-matsuzaki's repository --- src/main/java/org/sunflow/core/Scene.java | 3 +++ src/main/java/org/sunflow/core/ShadingState.java | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/sunflow/core/Scene.java b/src/main/java/org/sunflow/core/Scene.java index 97b2cae..a413f7c 100644 --- a/src/main/java/org/sunflow/core/Scene.java +++ b/src/main/java/org/sunflow/core/Scene.java @@ -313,6 +313,9 @@ public void render(Options options, ImageSampler sampler, Display display) { // prepare lights createAreaLightInstances(); + // prepare ShadingState + ShadingState.init(options); + // get acceleration structure info // count scene primitives long numPrimitives = 0; diff --git a/src/main/java/org/sunflow/core/ShadingState.java b/src/main/java/org/sunflow/core/ShadingState.java index d4d3613..0a319ea 100644 --- a/src/main/java/org/sunflow/core/ShadingState.java +++ b/src/main/java/org/sunflow/core/ShadingState.java @@ -47,6 +47,12 @@ public final class ShadingState implements Iterable { private LightSample lightSample; private PhotonStore map; + private static float minBias = 0.001f; + + public static void init(Options options) { + minBias = options.getFloat("bias", 0.001f); + } + static ShadingState createPhotonState(Ray r, IntersectionState istate, int i, PhotonStore map, LightServer server) { ShadingState s = new ShadingState(null, istate, r, i, 4); s.server = server; @@ -134,7 +140,7 @@ private ShadingState(ShadingState previous, IntersectionState istate, Ray r, int qmcD0I = QMC.halton(this.d, this.i); qmcD1I = QMC.halton(this.d + 1, this.i); result = null; - bias = 0.001f; + bias = minBias; } final void setRay(Ray r) { From 70445903e3ae90c9613127a9f93753abe8594686 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sat, 13 Apr 2019 18:40:24 -0300 Subject: [PATCH 20/37] Adds wireframe scene and fixes GumboAndTeapot --- .../java/examples/GumboAndTeapotScene.java | 8 ++- .../java/examples/WireframeDemoScene.java | 60 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 src/main/java/examples/WireframeDemoScene.java diff --git a/src/main/java/examples/GumboAndTeapotScene.java b/src/main/java/examples/GumboAndTeapotScene.java index 15a6322..3ed9f4f 100644 --- a/src/main/java/examples/GumboAndTeapotScene.java +++ b/src/main/java/examples/GumboAndTeapotScene.java @@ -18,7 +18,12 @@ public class GumboAndTeapotScene { public static void main(String[] args) { SunflowAPI api = new SunflowAPI(); api.reset(); + buildScene(api); + finalRender(api); + } + + public static void buildScene(SunflowAPI api){ ImageParameter image = new ImageParameter(); image.setResolutionX(800); image.setResolutionY(450); @@ -97,6 +102,7 @@ public static void main(String[] args) { gumbo1.scale(0.1f); gumbo1.rotateY(25); gumbo1.translate(1.5f, 0, -1.5f); + gumbo1.setSmooth(false); gumbo1.setup(api); GumboParameter gumbo2 = new GumboParameter(); @@ -186,8 +192,6 @@ public static void main(String[] args) { teapot4.setSubdivs(7); teapot4.setSmooth(false); teapot4.setup(api); - - finalRender(api); } private static void previewRender(SunflowAPI api) { diff --git a/src/main/java/examples/WireframeDemoScene.java b/src/main/java/examples/WireframeDemoScene.java new file mode 100644 index 0000000..a17d664 --- /dev/null +++ b/src/main/java/examples/WireframeDemoScene.java @@ -0,0 +1,60 @@ +package examples; + +import org.sunflow.PluginRegistry; +import org.sunflow.SunflowAPI; +import org.sunflow.core.ShadingState; +import org.sunflow.core.parameter.ImageParameter; +import org.sunflow.core.shader.WireframeShader; +import org.sunflow.image.Color; + +public class WireframeDemoScene { + + public static void main(String[] args) { + SunflowAPI api = new SunflowAPI(); + api.reset(); + + // Loading a custom procedural shader + PluginRegistry.shaderPlugins.registerPlugin("custom_wireframe", CustomWireShader.class); + api.parameter("width", (float) (Math.PI * 0.5 / 8192)); + api.shader("ao_wire", "custom_wireframe"); + + // Including a scene + GumboAndTeapotScene.buildScene(api); + + // Overriding existent shaders + api.parameter("override.shader", "ao_wire"); + api.parameter("override.photons", true); + + ImageParameter image = new ImageParameter(); + image.setResolutionX(320); + image.setResolutionY(240); + image.setAAMin(2); + image.setAAMax(2); + image.setFilter(ImageParameter.FILTER_BLACKMAN_HARRIS); + image.setup(api); + + finalRender(api); + } + + private static void previewRender(SunflowAPI api) { + api.parameter("sampler", "ipr"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + + private static void finalRender(SunflowAPI api) { + api.parameter("sampler", "bucket"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + + public static class CustomWireShader extends WireframeShader { + // set to false to overlay wires on regular shaders + private boolean ambocc = true; + + public Color getFillColor(ShadingState state) { + return ambocc ? state.occlusion(16, 6.0f) : state.getShader().getRadiance(state); + } + } + +} From 6eff7beec3bca47c08825adb25dde266e9ad8f41 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sat, 13 Apr 2019 18:53:48 -0300 Subject: [PATCH 21/37] Refactoring --- .../core/modifiers/NormalMapModifier.java | 3 +- .../core/parameter/InstanceParameter.java | 5 +- .../parameter/geometry/GeometryParameter.java | 11 ++-- .../parameter/geometry/ObjectParameter.java | 17 +++++- .../parameter/geometry/SphereParameter.java | 26 ++++++++- .../parameter/geometry/TeapotParameter.java | 5 +- .../modifier/BumpMapModifierParameter.java | 8 +++ .../modifier/NormalMapModifierParameter.java | 8 +++ .../org/sunflow/core/parser/SCParser.java | 53 ++++++------------- 9 files changed, 88 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/sunflow/core/modifiers/NormalMapModifier.java b/src/main/java/org/sunflow/core/modifiers/NormalMapModifier.java index a297cc3..157060d 100644 --- a/src/main/java/org/sunflow/core/modifiers/NormalMapModifier.java +++ b/src/main/java/org/sunflow/core/modifiers/NormalMapModifier.java @@ -17,8 +17,9 @@ public NormalMapModifier() { public boolean update(ParameterList pl, SunflowAPI api) { String filename = pl.getString("texture", null); - if (filename != null) + if (filename != null) { normalMap = TextureCache.getTexture(api.resolveTextureFilename(filename), true); + } return normalMap != null; } diff --git a/src/main/java/org/sunflow/core/parameter/InstanceParameter.java b/src/main/java/org/sunflow/core/parameter/InstanceParameter.java index 0f509e4..73a8f59 100644 --- a/src/main/java/org/sunflow/core/parameter/InstanceParameter.java +++ b/src/main/java/org/sunflow/core/parameter/InstanceParameter.java @@ -23,8 +23,9 @@ public void setup(SunflowAPIInterface api) { if (modifiers != null) { api.parameter("modifiers", modifiers); } - - api.instance(name, geometry); + if (geometry != null) { + api.instance(name, geometry); + } } public String name() { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java index 42c9282..fe17e6f 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java @@ -6,10 +6,13 @@ public abstract class GeometryParameter extends ObjectParameter { public void setupInstance(SunflowAPIInterface api) { if (instanceParameter != null) { - instanceParameter - .name(name + ".instance") - .geometry(name) - .setup(api); + instanceParameter.name(name + ".instance"); + + if (instanceParameter.geometry() == null) { + instanceParameter.geometry(name); + } + + instanceParameter.setup(api); } } diff --git a/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java index d779b4a..5119890 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java @@ -62,6 +62,22 @@ public void setup(SunflowAPIInterface api) { } } + public void geometry(String geometry) { + if (instanceParameter == null) { + instanceParameter = new InstanceParameter(); + } + + instanceParameter.geometry(geometry); + } + + public void geometry(ObjectParameter objectParameter) { + if (instanceParameter == null) { + instanceParameter = new InstanceParameter(); + } + + instanceParameter.geometry(objectParameter.name); + } + public void shaders(String... shaders) { if (instanceParameter == null) { instanceParameter = new InstanceParameter(); @@ -81,7 +97,6 @@ public void shaders(ShaderParameter... shaders) { } instanceParameter.shaders(names); - } public void modifiers(String... modifiers) { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/SphereParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/SphereParameter.java index c52eaef..65e9d3b 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/SphereParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/SphereParameter.java @@ -9,13 +9,35 @@ public class SphereParameter extends GeometryParameter { Point3 center; float radius; + public SphereParameter() { + + } + + public SphereParameter(String name) { + this.name = name; + } + @Override public void setup(SunflowAPIInterface api) { super.setup(api); api.geometry(name, TYPE_SPHERE); - api.parameter("transform", Matrix4.translation(center.x, center.y, center.z).multiply(Matrix4.scale(radius))); - setupInstance(api); + // Legacy instantiation + if (center != null) { + api.parameter("transform", Matrix4.translation(center.x, center.y, center.z).multiply(Matrix4.scale(radius))); + + if (instanceParameter != null) { + if (instanceParameter.shaders() != null) { + api.parameter("shaders", instanceParameter.shaders()); + } + if (instanceParameter.modifiers() != null) { + api.parameter("modifiers", instanceParameter.modifiers()); + } + } + api.instance(name + ".instance", name); + } else { + setupInstance(api); + } } public Point3 getCenter() { diff --git a/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java index 62dc9f8..a165343 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java @@ -20,7 +20,10 @@ public void setup(SunflowAPIInterface api) { super.setup(api); api.parameter("subdivs", subdivs); api.parameter("smooth", smooth); - api.geometry(name, TYPE_TEAPOT); + + if (instanceParameter.geometry() == null) { + api.geometry(name, TYPE_TEAPOT); + } setupInstance(api); } diff --git a/src/main/java/org/sunflow/core/parameter/modifier/BumpMapModifierParameter.java b/src/main/java/org/sunflow/core/parameter/modifier/BumpMapModifierParameter.java index 62d7a0d..e21c98f 100644 --- a/src/main/java/org/sunflow/core/parameter/modifier/BumpMapModifierParameter.java +++ b/src/main/java/org/sunflow/core/parameter/modifier/BumpMapModifierParameter.java @@ -7,6 +7,14 @@ public class BumpMapModifierParameter extends ModifierParameter { float scale; String texture = ""; + public BumpMapModifierParameter() { + + } + + public BumpMapModifierParameter(String name) { + this.name = name; + } + @Override public void setup(SunflowAPIInterface api) { api.parameter("texture", texture); diff --git a/src/main/java/org/sunflow/core/parameter/modifier/NormalMapModifierParameter.java b/src/main/java/org/sunflow/core/parameter/modifier/NormalMapModifierParameter.java index 1a4e62c..913c088 100644 --- a/src/main/java/org/sunflow/core/parameter/modifier/NormalMapModifierParameter.java +++ b/src/main/java/org/sunflow/core/parameter/modifier/NormalMapModifierParameter.java @@ -6,6 +6,14 @@ public class NormalMapModifierParameter extends ModifierParameter { String texture = ""; + public NormalMapModifierParameter() { + + } + + public NormalMapModifierParameter(String name) { + this.name = name; + } + @Override public void setup(SunflowAPIInterface api) { api.parameter("texture", texture); diff --git a/src/main/java/org/sunflow/core/parser/SCParser.java b/src/main/java/org/sunflow/core/parser/SCParser.java index 72848fa..10e46bd 100644 --- a/src/main/java/org/sunflow/core/parser/SCParser.java +++ b/src/main/java/org/sunflow/core/parser/SCParser.java @@ -768,15 +768,12 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I String name = ""; String accel = ""; - boolean noInstance = false; - //Matrix4[] transform = null; - //float transformTime0 = 0, transformTime1 = 0; String[] shaders = null; String[] modifiers = null; if (p.peekNextToken("noinstance")) { // this indicates that the geometry is to be created, but not // instanced into the scene - noInstance = true; + instanceParameter = null; } else { // these are the parameters to be passed to the instance if (p.peekNextToken("shaders")) { @@ -891,15 +888,12 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I geometry.setup(api); } else if (type.equals("sphere")) { UI.printInfo(Module.API, "Reading sphere ..."); - instanceParameter - .name(name + ".instance") - .geometry(name); SphereParameter geometry = new SphereParameter(); geometry.setName(name); geometry.setInstanceParameter(instanceParameter); - if (instanceParameter.transform() == null && !noInstance) { + if (instanceParameter == null || instanceParameter.transform() == null) { // legacy method of specifying transformation for spheres p.checkNextToken("c"); float x = p.getNextFloat(); @@ -911,8 +905,8 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I geometry.setRadius(radius); geometry.setup(api); - // disable future instancing - instance has already been created - noInstance = true; + } else { + geometry.setup(api); } } else if (type.equals("cylinder")) { UI.printInfo(Module.API, "Reading cylinder ..."); @@ -944,6 +938,7 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I } else if (type.equals("sphereflake")) { UI.printInfo(Module.API, "Reading sphereflake ..."); SphereFlakeParameter geometry = new SphereFlakeParameter(); + geometry.setInstanceParameter(instanceParameter); geometry.setName(name); if (p.peekNextToken("level")) { @@ -959,6 +954,7 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I } else if (type.equals("plane")) { UI.printInfo(Module.API, "Reading plane ..."); PlaneParameter geometry = new PlaneParameter(); + geometry.setInstanceParameter(instanceParameter); geometry.setName(name); p.checkNextToken("p"); geometry.setCenter(parsePoint()); @@ -974,6 +970,7 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I } else if (type.equals("generic-mesh")) { UI.printInfo(Module.API, "Reading generic mesh: %s ... ", name); GenericMeshParameter geometry = new GenericMeshParameter(); + geometry.setInstanceParameter(instanceParameter); // parse vertices p.checkNextToken("points"); int np = p.getNextInt(); @@ -1011,6 +1008,7 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I } else if (type.equals("hair")) { UI.printInfo(Module.API, "Reading hair curves: %s ... ", name); HairParameter geometry = new HairParameter(); + geometry.setInstanceParameter(instanceParameter); p.checkNextToken("segments"); geometry.setSegments(p.getNextInt()); p.checkNextToken("width"); @@ -1021,14 +1019,13 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I } else if (type.equals("janino-tesselatable")) { UI.printInfo(Module.API, "Reading procedural primitive: %s ... ", name); String typename = p.peekNextToken("typename") ? p.getNextToken() : PluginRegistry.shaderPlugins.generateUniqueName("janino_shader"); - if (!PluginRegistry.tesselatablePlugins.registerPlugin(typename, p.getNextCodeBlock())) { - noInstance = true; - } else { + if (PluginRegistry.tesselatablePlugins.registerPlugin(typename, p.getNextCodeBlock())) { api.geometry(name, typename); } } else if (type.equals("teapot")) { UI.printInfo(Module.API, "Reading teapot: %s ... ", name); TeapotParameter geometry = new TeapotParameter(); + geometry.setInstanceParameter(instanceParameter); geometry.setName(name); if (p.peekNextToken("subdivs")) { @@ -1041,6 +1038,7 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I } else if (type.equals("gumbo")) { UI.printInfo(Module.API, "Reading gumbo: %s ... ", name); GumboParameter geometry = new GumboParameter(); + geometry.setInstanceParameter(instanceParameter); geometry.setName(name); if (p.peekNextToken("subdivs")) { @@ -1053,6 +1051,7 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I } else if (type.equals("julia")) { UI.printInfo(Module.API, "Reading julia fractal: %s ... ", name); JuliaParameter geometry = new JuliaParameter(); + geometry.setInstanceParameter(instanceParameter); geometry.setName(name); if (p.peekNextToken("q")) { geometry.setCw(p.getNextFloat()); @@ -1069,6 +1068,7 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I geometry.setup(api); } else if (type.equals("particles") || type.equals("dlasurface")) { ParticlesParameter geometry = new ParticlesParameter(); + geometry.setInstanceParameter(instanceParameter); geometry.setName(name); if (type.equals("dlasurface")) { UI.printWarning(Module.API, "Deprecated object type: \"dlasurface\" - please use \"particles\" instead"); @@ -1114,6 +1114,7 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I } else if (type.equals("file-mesh")) { UI.printInfo(Module.API, "Reading file mesh: %s ... ", name); FileMeshParameter geometry = new FileMeshParameter(); + geometry.setInstanceParameter(instanceParameter); geometry.setName(name); p.checkNextToken("filename"); @@ -1126,6 +1127,7 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I } else if (type.equals("bezier-mesh")) { UI.printInfo(Module.API, "Reading bezier mesh: %s ... ", name); BezierMeshParameter geometry = new BezierMeshParameter(); + geometry.setInstanceParameter(instanceParameter); geometry.setName(name); p.checkNextToken("n"); @@ -1155,31 +1157,8 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I geometry.setup(api); } else { UI.printWarning(Module.API, "Unrecognized object type: %s", p.getNextToken()); - noInstance = true; - } - if (!noInstance) { - instanceParameter - .name(name + ".instance") - .geometry(name); - instanceParameter.setup(api); - - // create instance - /*api.parameter("shaders", shaders); - if (modifiers != null) - api.parameter("modifiers", modifiers); - if (transform != null && transform.length > 0) { - if (transform.length == 1) - api.parameter("transform", transform[0]); - else { - api.parameter("transform.steps", transform.length); - api.parameter("transform.times", "float", "none", new float[]{ - transformTime0, transformTime1}); - for (int i = 0; i < transform.length; i++) - api.parameter(String.format("transform[%d]", i), transform[i]); - } - } - api.instance(name + ".instance", name);*/ } + p.checkNextToken("}"); } From 0a01cdd8a19ad70ffaee87bf7e83825417bd0475 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sat, 13 Apr 2019 18:54:08 -0300 Subject: [PATCH 22/37] Adds BumpScene --- src/main/java/examples/BumpScene.java | 182 ++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 src/main/java/examples/BumpScene.java diff --git a/src/main/java/examples/BumpScene.java b/src/main/java/examples/BumpScene.java new file mode 100644 index 0000000..3192f3b --- /dev/null +++ b/src/main/java/examples/BumpScene.java @@ -0,0 +1,182 @@ +package examples; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.parameter.ImageParameter; +import org.sunflow.core.parameter.camera.PinholeCameraParameter; +import org.sunflow.core.parameter.geometry.PlaneParameter; +import org.sunflow.core.parameter.geometry.SphereParameter; +import org.sunflow.core.parameter.geometry.TeapotParameter; +import org.sunflow.core.parameter.light.SunSkyLightParameter; +import org.sunflow.core.parameter.modifier.BumpMapModifierParameter; +import org.sunflow.core.parameter.modifier.NormalMapModifierParameter; +import org.sunflow.core.parameter.shader.DiffuseShaderParameter; +import org.sunflow.core.parameter.shader.GlassShaderParameter; +import org.sunflow.core.parameter.shader.ShinyShaderParameter; +import org.sunflow.image.Color; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class BumpScene { + + public static void main(String[] args) { + SunflowAPI api = new SunflowAPI(); + api.reset(); + api.searchpath("texture", System.getProperty("user.dir") + "/examples/"); + buildScene(api); + + finalRender(api); + } + + public static void buildScene(SunflowAPI api) { + ImageParameter image = new ImageParameter(); + image.setResolutionX(800); + image.setResolutionY(450); + image.setAAMin(0); + image.setAAMax(1); + image.setFilter(ImageParameter.FILTER_TRIANGLE); + image.setup(api); + + PinholeCameraParameter camera = new PinholeCameraParameter(); + + camera.setName("camera"); + Point3 eye = new Point3(-18.19f, 8.97f, -0.93f); + Point3 target = new Point3(-0.690f, 0.97f, -0.93f); + Vector3 up = new Vector3(0, 1, 0); + + camera.setupTransform(api, eye, target, up); + + camera.setFov(30f); + camera.setAspect(1.777777777777f); + camera.setup(api); + + SunSkyLightParameter lightParameter = new SunSkyLightParameter(); + lightParameter.setName("sunsky"); + lightParameter.setUp(new Vector3(0, 1, 0)); + lightParameter.setEast(new Vector3(0, 0, 1)); + lightParameter.setSunDirection(new Vector3(-1, 1, -1)); + lightParameter.setTurbidity(2); + lightParameter.setSamples(32); + lightParameter.setup(api); + + NormalMapModifierParameter bumpy01 = new NormalMapModifierParameter("bumpy_01"); + bumpy01.setTexture("textures/brick_normal.jpg"); + bumpy01.setup(api); + + BumpMapModifierParameter bumpy02 = new BumpMapModifierParameter("bumpy_02"); + bumpy02.setTexture("textures/dirty_bump.jpg"); + bumpy02.setScale(0.02f); + bumpy02.setup(api); + + BumpMapModifierParameter bumpy03 = new BumpMapModifierParameter("bumpy_03"); + bumpy03.setTexture("textures/reptileskin_bump.png"); + bumpy03.setScale(0.02f); + bumpy03.setup(api); + + BumpMapModifierParameter bumpy04 = new BumpMapModifierParameter("bumpy_04"); + bumpy04.setTexture("textures/shiphull_bump.png"); + bumpy04.setScale(0.15f); + bumpy04.setup(api); + + BumpMapModifierParameter bumpy05 = new BumpMapModifierParameter("bumpy_05"); + bumpy05.setTexture("textures/slime_bump.jpg"); + bumpy05.setScale(0.15f); + bumpy05.setup(api); + + ShinyShaderParameter shiny = new ShinyShaderParameter("default"); + shiny.setDiffuse(new Color(0.2f, 0.2f, 0.2f)); + shiny.setShininess(0.3f); + shiny.setup(api); + + GlassShaderParameter glassy = new GlassShaderParameter("glassy"); + glassy.setEta(1.2f); + glassy.setColor(new Color(0.8f, 0.8f, 0.8f)); + glassy.setAbsorptionDistance(7); + glassy.setAbsorptionColor(new Color(0.2f, 0.7f, 0.2f).toLinear()); + glassy.setup(api); + + DiffuseShaderParameter simpleRed = new DiffuseShaderParameter("simple_red"); + simpleRed.setDiffuse(new Color(0.7f, 0.15f, 0.15f).toLinear()); + simpleRed.setup(api); + + DiffuseShaderParameter simpleGreen = new DiffuseShaderParameter("simple_green"); + simpleGreen.setDiffuse(new Color(0.15f, 0.7f, 0.15f).toLinear()); + simpleGreen.setup(api); + + DiffuseShaderParameter simpleYellow = new DiffuseShaderParameter("simple_yellow"); + simpleYellow.setDiffuse(new Color(0.8f, 0.8f, 0.2f).toLinear()); + simpleYellow.setup(api); + + DiffuseShaderParameter floorShader = new DiffuseShaderParameter("floor"); + //floorShader.setDiffuse(new Color(0.3f, 0.3f, 0.3f)); + floorShader.setTexture("textures/brick_color.jpg"); + floorShader.setup(api); + + PlaneParameter floor = new PlaneParameter(); + floor.shaders(floorShader); + floor.modifiers(bumpy01); + floor.setCenter(new Point3(0, 0, 0)); + floor.setPoint1(new Point3(4, 0, 3)); + floor.setPoint2(new Point3(-3, 0, 4)); + floor.setup(api); + + TeapotParameter teapot0 = new TeapotParameter("teapot_0"); + teapot0.shaders(simpleGreen); + teapot0.modifiers(bumpy03); + teapot0.rotateX(-90); + teapot0.scale(0.018f); + teapot0.rotateY(245f); + teapot0.translate(1.5f, 0, -1); + teapot0.setSubdivs(20); + teapot0.setup(api); + + SphereParameter sphere0 = new SphereParameter("sphere_0"); + sphere0.shaders(glassy); + sphere0.rotateX(35); + sphere0.scale(1.5f); + sphere0.rotateY(245); + sphere0.translate(1.5f, 1.5f, 3); + sphere0.setup(api); + + SphereParameter sphere1 = new SphereParameter("sphere_1"); + sphere1.shaders(shiny); + sphere1.modifiers(bumpy05); + sphere1.rotateX(35); + sphere1.scale(1.5f); + sphere1.rotateY(245); + sphere1.translate(1.5f, 1.5f, -5); + sphere1.setup(api); + + TeapotParameter teapot1 = new TeapotParameter("teapot_1"); + teapot1.geometry(teapot0); + teapot1.rotateX(-90); + teapot1.scale(0.018f); + teapot1.rotateY(245f); + teapot1.translate(-1.5f, 0, -3); + teapot1.shaders(simpleYellow); + teapot1.modifiers(bumpy04); + teapot1.setup(api); + + TeapotParameter teapot3 = new TeapotParameter("teapot_3"); + teapot3.geometry(teapot0); + teapot3.shaders(simpleRed); + teapot3.modifiers(bumpy02); + teapot3.rotateX(-90); + teapot3.scale(0.018f); + teapot3.rotateY(245f); + teapot3.translate(-1.5f, 0, 1); + teapot3.setup(api); + } + + private static void previewRender(SunflowAPI api) { + api.parameter("sampler", "ipr"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + + private static void finalRender(SunflowAPI api) { + api.parameter("sampler", "bucket"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + +} From 2a481e2796aa65b59d465d7209057d44e6b927a6 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sat, 13 Apr 2019 20:15:18 -0300 Subject: [PATCH 23/37] Generate unique name to lights --- .../core/parameter/light/CornellBoxLightParameter.java | 4 ++++ .../core/parameter/light/DirectionalLightParameter.java | 1 + .../core/parameter/light/ImageBasedLightParameter.java | 4 ++++ .../org/sunflow/core/parameter/light/LightParameter.java | 7 +++++++ .../sunflow/core/parameter/light/PointLightParameter.java | 1 + .../sunflow/core/parameter/light/SphereLightParameter.java | 1 + .../sunflow/core/parameter/light/SunSkyLightParameter.java | 4 ++++ .../core/parameter/light/TriangleMeshLightParameter.java | 4 ++++ 8 files changed, 26 insertions(+) diff --git a/src/main/java/org/sunflow/core/parameter/light/CornellBoxLightParameter.java b/src/main/java/org/sunflow/core/parameter/light/CornellBoxLightParameter.java index 81d00b4..f02061c 100644 --- a/src/main/java/org/sunflow/core/parameter/light/CornellBoxLightParameter.java +++ b/src/main/java/org/sunflow/core/parameter/light/CornellBoxLightParameter.java @@ -20,6 +20,10 @@ public class CornellBoxLightParameter extends LightParameter { Color left, right, top, bottom, back; Color radiance; + public CornellBoxLightParameter() { + generateUniqueName("cornellbox"); + } + @Override public void setup(SunflowAPIInterface api) { api.parameter(PARAM_MIN_CORNER, min); diff --git a/src/main/java/org/sunflow/core/parameter/light/DirectionalLightParameter.java b/src/main/java/org/sunflow/core/parameter/light/DirectionalLightParameter.java index 3a0977f..707227c 100644 --- a/src/main/java/org/sunflow/core/parameter/light/DirectionalLightParameter.java +++ b/src/main/java/org/sunflow/core/parameter/light/DirectionalLightParameter.java @@ -16,6 +16,7 @@ public class DirectionalLightParameter extends LightParameter { public DirectionalLightParameter() { light = new DirectionalSpotlight(); + generateUniqueName("directional"); } @Override diff --git a/src/main/java/org/sunflow/core/parameter/light/ImageBasedLightParameter.java b/src/main/java/org/sunflow/core/parameter/light/ImageBasedLightParameter.java index a5ff5b7..93a9f88 100644 --- a/src/main/java/org/sunflow/core/parameter/light/ImageBasedLightParameter.java +++ b/src/main/java/org/sunflow/core/parameter/light/ImageBasedLightParameter.java @@ -18,6 +18,10 @@ public class ImageBasedLightParameter extends LightParameter { Vector3 up; boolean fixed; + public ImageBasedLightParameter() { + generateUniqueName("ibl"); + } + @Override public void setup(SunflowAPIInterface api) { api.parameter(PARAM_TEXTURE, texture); diff --git a/src/main/java/org/sunflow/core/parameter/light/LightParameter.java b/src/main/java/org/sunflow/core/parameter/light/LightParameter.java index 8ded393..00a5767 100644 --- a/src/main/java/org/sunflow/core/parameter/light/LightParameter.java +++ b/src/main/java/org/sunflow/core/parameter/light/LightParameter.java @@ -15,6 +15,7 @@ public abstract class LightParameter implements Parameter { public static final String TYPE_SUNSKY = "sunsky"; public static final String TYPE_TRIANGLE_MESH = "triangle_mesh"; + private static int count = 0; protected String name; public String getName() { @@ -24,4 +25,10 @@ public String getName() { public void setName(String name) { this.name = name; } + + protected void generateUniqueName(String prefix) { + name = prefix + count; + count++; + } + } diff --git a/src/main/java/org/sunflow/core/parameter/light/PointLightParameter.java b/src/main/java/org/sunflow/core/parameter/light/PointLightParameter.java index e714f6b..a18828a 100644 --- a/src/main/java/org/sunflow/core/parameter/light/PointLightParameter.java +++ b/src/main/java/org/sunflow/core/parameter/light/PointLightParameter.java @@ -13,6 +13,7 @@ public class PointLightParameter extends LightParameter { public PointLightParameter() { light = new PointLight(); + generateUniqueName("pointlight"); } @Override diff --git a/src/main/java/org/sunflow/core/parameter/light/SphereLightParameter.java b/src/main/java/org/sunflow/core/parameter/light/SphereLightParameter.java index d5830d6..a295e2a 100644 --- a/src/main/java/org/sunflow/core/parameter/light/SphereLightParameter.java +++ b/src/main/java/org/sunflow/core/parameter/light/SphereLightParameter.java @@ -13,6 +13,7 @@ public class SphereLightParameter extends LightParameter { public SphereLightParameter() { light = new SphereLight(); + generateUniqueName("spherelight"); } @Override diff --git a/src/main/java/org/sunflow/core/parameter/light/SunSkyLightParameter.java b/src/main/java/org/sunflow/core/parameter/light/SunSkyLightParameter.java index 79e29f4..0b7e092 100644 --- a/src/main/java/org/sunflow/core/parameter/light/SunSkyLightParameter.java +++ b/src/main/java/org/sunflow/core/parameter/light/SunSkyLightParameter.java @@ -22,6 +22,10 @@ public class SunSkyLightParameter extends LightParameter { Color groundColor = null; + public SunSkyLightParameter() { + generateUniqueName("sunsky"); + } + @Override public void setup(SunflowAPIInterface api) { api.parameter(PARAM_UP, up); diff --git a/src/main/java/org/sunflow/core/parameter/light/TriangleMeshLightParameter.java b/src/main/java/org/sunflow/core/parameter/light/TriangleMeshLightParameter.java index 5833e1b..0879c96 100644 --- a/src/main/java/org/sunflow/core/parameter/light/TriangleMeshLightParameter.java +++ b/src/main/java/org/sunflow/core/parameter/light/TriangleMeshLightParameter.java @@ -13,6 +13,10 @@ public class TriangleMeshLightParameter extends LightParameter { float[] points; int[] triangles; + public TriangleMeshLightParameter() { + generateUniqueName("meshlight"); + } + @Override public void setup(SunflowAPIInterface api) { api.parameter(PARAM_RADIANCE, null, radiance.getRGB()); From 39b23f16e3150b4527346fab65b1bea7aa899c81 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sat, 13 Apr 2019 20:15:40 -0300 Subject: [PATCH 24/37] Remove bump_demo_small --- examples/bump_demo_small.sc | 176 ------------------------------------ 1 file changed, 176 deletions(-) delete mode 100644 examples/bump_demo_small.sc diff --git a/examples/bump_demo_small.sc b/examples/bump_demo_small.sc deleted file mode 100644 index 4410663..0000000 --- a/examples/bump_demo_small.sc +++ /dev/null @@ -1,176 +0,0 @@ -image { - resolution 400 225 - aa 0 1 - filter triangle -} - -% |persp|perspShape -camera { - type pinhole - eye -18.19 8.97 -0.93 - target -0.690 0.97 -0.93 - up 0 1 0 - fov 30 - aspect 1.777777777777 -} - -light { - type sunsky - up 0 1 0 - east 0 0 1 - sundir -1 1 -1 - turbidity 2 - samples 32 -} - -modifier { - name bumpy_01 - type normalmap - texture textures/brick_normal.jpg -} - -modifier { - name bumpy_02 - type bump - texture textures/dirty_bump.jpg - scale 0.02 -} - -modifier { - name bumpy_03 - type bump - texture textures/reptileskin_bump.png - scale 0.02 -} - -modifier { - name bumpy_04 - type bump - texture textures/shiphull_bump.png - scale 0.015 -} - -modifier { - name bumpy_05 - type bump - texture textures/slime_bump.jpg - scale 0.015 -} - - -shader { - name default - type shiny - diff 0.2 0.2 0.2 - refl 0.3 -} - -shader { - name glassy - type glass - eta 1.2 - color 0.8 0.8 0.8 - absorbtion.distance 7 - absorbtion.color { "sRGB nonlinear" 0.2 0.7 0.2 } -} - -shader { - name simple_red - type diffuse - diff { "sRGB nonlinear" 0.70 0.15 0.15 } -} - -shader { - name simple_green - type diffuse - diff { "sRGB nonlinear" 0.15 0.70 0.15 } -} - -shader { - name simple_yellow - type diffuse - diff { "sRGB nonlinear" 0.70 0.70 0.15 } -} - - -shader { - name floor - type diffuse -% diff 0.3 0.3 0.3 - texture textures/brick_color.jpg -} - -object { - shader floor - modifier bumpy_01 - type plane - p 0 0 0 - p 4 0 3 - p -3 0 4 -} - -object { - shader simple_green - modifier bumpy_03 - transform { - rotatex -90 - scaleu 0.018 - rotatey 245 - translate 1.5 0 -1 - } - type teapot - name teapot_0 - subdivs 20 -} - -object { - shader glassy - modifier bumpy_05 - transform { - rotatex 35 - scaleu 1.5 - rotatey 245 - translate 1.5 1.5 3 - } - type sphere - name sphere_0 -} - -object { - shader default - modifier bumpy_05 - transform { - rotatex 35 - scaleu 1.5 - rotatey 245 - translate 1.5 1.5 -5 - } - type sphere - name sphere_1 -} - -instance { - name teapot_1 - geometry teapot_0 - transform { - rotatex -90 - scaleu 0.018 - rotatey 245 - translate -1.5 0 -3 - } - shader simple_yellow - modifier bumpy_04 -} - -instance { - name teapot_3 - geometry teapot_0 - transform { - rotatex -90 - scaleu 0.018 - rotatey 245 - translate -1.5 0 +1 - } - shader simple_red - modifier bumpy_02 -} From 9fdc1f975874c9a5a2da0a53d7730b6f436f5ed4 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sat, 13 Apr 2019 20:16:00 -0300 Subject: [PATCH 25/37] Adds JuliaScene --- src/main/java/examples/JuliaScene.java | 95 +++++++++++++++++++ .../parameter/geometry/JuliaParameter.java | 14 +++ 2 files changed, 109 insertions(+) create mode 100644 src/main/java/examples/JuliaScene.java diff --git a/src/main/java/examples/JuliaScene.java b/src/main/java/examples/JuliaScene.java new file mode 100644 index 0000000..3ed2ca6 --- /dev/null +++ b/src/main/java/examples/JuliaScene.java @@ -0,0 +1,95 @@ +package examples; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.parameter.ImageParameter; +import org.sunflow.core.parameter.TraceDepthsParameter; +import org.sunflow.core.parameter.camera.PinholeCameraParameter; +import org.sunflow.core.parameter.geometry.JuliaParameter; +import org.sunflow.core.parameter.gi.PathTracingGIParameter; +import org.sunflow.core.parameter.light.SphereLightParameter; +import org.sunflow.core.parameter.shader.DiffuseShaderParameter; +import org.sunflow.image.Color; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class JuliaScene { + + public static void main(String[] args) { + SunflowAPI api = new SunflowAPI(); + api.reset(); + + ImageParameter image = new ImageParameter(); + image.setResolutionX(512); + image.setResolutionY(512); + image.setAAMin(0); + image.setAAMax(2); + image.setFilter(ImageParameter.FILTER_GAUSSIAN); + image.setup(api); + + TraceDepthsParameter traceDepths = new TraceDepthsParameter(); + traceDepths.setDiffuse(1); + traceDepths.setReflection(0); + traceDepths.setRefraction(0); + traceDepths.setup(api); + + PinholeCameraParameter camera = new PinholeCameraParameter(); + + camera.setName("camera"); + Point3 eye = new Point3(-5, 0, 0); + Point3 target = new Point3(0, 0, 0); + Vector3 up = new Vector3(0, 1, 0); + + camera.setupTransform(api, eye, target, up); + + camera.setFov(58f); + camera.setAspect(1); + camera.setup(api); + + PathTracingGIParameter gi = new PathTracingGIParameter(); + gi.setSamples(16); + gi.setup(api); + + DiffuseShaderParameter simple1 = new DiffuseShaderParameter("simple1"); + simple1.setDiffuse(new Color(0.5f, 0.5f, 0.5f).toLinear()); + simple1.setup(api); + + SphereLightParameter light0 = new SphereLightParameter(); + light0.setRadiance(new Color(1, 1, 0.6f).toLinear().mul(60)); + light0.setCenter(new Point3(-5, 7, 5)); + light0.setRadius(2); + light0.setSamples(8); + light0.setup(api); + + SphereLightParameter light1 = new SphereLightParameter(); + light1.setRadiance(new Color(0.6f, 0.6f, 1f).toLinear().mul(20)); + light1.setCenter(new Point3(-15, -17, -15)); + light1.setRadius(5); + light1.setSamples(8); + light1.setup(api); + + JuliaParameter left = new JuliaParameter("left"); + left.shaders(simple1); + left.scale(2); + left.rotateY(45); + left.rotateX(-55); + left.setIterations(8); + left.setEpsilon(0.001f); + left.setQuaternion(-0.125f, -0.256f, 0.847f, 0.0895f); + left.setup(api); + + finalRender(api); + } + + private static void previewRender(SunflowAPI api) { + api.parameter("sampler", "ipr"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + + private static void finalRender(SunflowAPI api) { + api.parameter("sampler", "bucket"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/JuliaParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/JuliaParameter.java index 89b6c7c..c805433 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/JuliaParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/JuliaParameter.java @@ -10,6 +10,13 @@ public class JuliaParameter extends GeometryParameter { int iterations = 1; float epsilon; + public JuliaParameter() { + } + + public JuliaParameter(String name) { + this.name = name; + } + @Override public void setup(SunflowAPIInterface api) { super.setup(api); @@ -73,4 +80,11 @@ public float getEpsilon() { public void setEpsilon(float epsilon) { this.epsilon = epsilon; } + + public void setQuaternion(float cx, float cy, float cz, float cw) { + this.cx = cx; + this.cy = cy; + this.cz = cz; + this.cw = cw; + } } From a4f7e9630f9fcf798e30350f5bdb9a32c802720e Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sat, 13 Apr 2019 20:29:03 -0300 Subject: [PATCH 26/37] Fix test --- .../org/sunflow/core/parameter/geometry/TeapotParameter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java index a165343..a22102d 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/TeapotParameter.java @@ -21,7 +21,7 @@ public void setup(SunflowAPIInterface api) { api.parameter("subdivs", subdivs); api.parameter("smooth", smooth); - if (instanceParameter.geometry() == null) { + if (instanceParameter == null || instanceParameter.geometry() == null) { api.geometry(name, TYPE_TEAPOT); } From cfd77ddbab1a8f4b9dd1b54e3ea6c4a69750c928 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sun, 14 Apr 2019 01:29:58 -0300 Subject: [PATCH 27/37] Fix sponge scene --- src/main/java/org/sunflow/core/primitive/CubeGrid.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/sunflow/core/primitive/CubeGrid.java b/src/main/java/org/sunflow/core/primitive/CubeGrid.java index 75b1224..52c8a58 100644 --- a/src/main/java/org/sunflow/core/primitive/CubeGrid.java +++ b/src/main/java/org/sunflow/core/primitive/CubeGrid.java @@ -285,4 +285,8 @@ public BoundingBox getWorldBounds(Matrix4 o2w) { return bounds; return o2w.transform(bounds); } + + public PrimitiveList getBakingPrimitives() { + return null; + } } \ No newline at end of file From 1130b7f6624020a59002a32bc188652c3d5774aa Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sun, 14 Apr 2019 02:22:19 -0300 Subject: [PATCH 28/37] Add Sunflow Manual (v0.07.2) --- Sunflow-Manual.pdf | Bin 0 -> 848515 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Sunflow-Manual.pdf diff --git a/Sunflow-Manual.pdf b/Sunflow-Manual.pdf new file mode 100644 index 0000000000000000000000000000000000000000..26ae47cfd44e75a8d8200a2febdbfe470722bb22 GIT binary patch literal 848515 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

    H{s&PF;3O746b)BL7i+Abt&*b!~&@x1T!HwCdD8 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 Date: Sun, 14 Apr 2019 15:46:03 -0300 Subject: [PATCH 29/37] Update CHANGELOG --- CHANGELOG | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 05c6ef9..9f46533 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +v0.07.5 +* Added procedural scenes +v0.07.4 +* Updated janino version to 2.7.8 +* Allow changing shading cache parameters for a quality/speed trade-off +* Fixed missing TriangleMeshLight shader in PluginRegistry +* Fixed self-intersection bias +* Fixed infinite loop in bih caused by small triangle v0.07.3 * Fixed bump mapping to correctly treat black as the lowest point in the map * Added command line override for samples paramter (affect bucket and multipas samplers) From 26cea680b79dd1f6fefc814857882a927dcbf3f2 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sun, 14 Apr 2019 15:47:06 -0300 Subject: [PATCH 30/37] Prepare to Maven Central release --- pom.xml | 106 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 90 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index 1887a9a..d5463e4 100644 --- a/pom.xml +++ b/pom.xml @@ -2,18 +2,44 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - - sunflow - net.sourceforge + com.harium.sunflow + core 0.07.5 - http://sunflow.sourceforge.net + Sunflow Sunflow Global Illumination Rendering System - + http://github.com/Harium/sunflow + + scm:git:git://github.com/harium/sunflow.git + scm:git:ssh://github.com:harium/sunflow.git + http://github.com/harium/sunflow/tree/master + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + - + -Xdoclint:none UTF-8 + gpg2 - + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + Contributors + https://github.com/Harium/sunflow/graphs/contributors + + org.codehaus.janino @@ -41,35 +67,83 @@ + org.apache.maven.plugins maven-compiler-plugin - 3.3 + 2.5.1 - 1.6 - 1.6 - UTF-8 + 1.8 + 1.8 + true + + **/examples/* + **/exporters/* + **/resources/* + org.apache.maven.plugins maven-source-plugin - 2.4 + 2.2.1 attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs jar + + ${javadoc.opts} + org.apache.maven.plugins - maven-compiler-plugin + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.7 + true + + + default-deploy + deploy + + deploy + + + - 1.7 - 1.7 + ossrh + https://oss.sonatype.org/ + true - + \ No newline at end of file From 56ae099d08dd1c892cba41d5a399dc9c14566990 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sun, 14 Apr 2019 17:47:56 -0300 Subject: [PATCH 31/37] Bump version to 0.07.5 --- build.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.xml b/build.xml index 147007a..8ae3bee 100644 --- a/build.xml +++ b/build.xml @@ -1,7 +1,7 @@ - + From 2a5da4cc8f1336f7ca9bc9c4eda5dc1e3884055b Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Sun, 14 Apr 2019 17:55:56 -0300 Subject: [PATCH 32/37] Update README --- README => README.md | 75 +++++++++++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 23 deletions(-) rename README => README.md (75%) diff --git a/README b/README.md similarity index 75% rename from README rename to README.md index d122461..7f4305a 100644 --- a/README +++ b/README.md @@ -1,4 +1,4 @@ -Sunflow Global Illumination Rendering System +# Sunflow Global Illumination Rendering System v0.07.5 Contact: Christopher Kulla @@ -9,12 +9,10 @@ Contact: Christopher Kulla Sunflow is a rendering system for photo-realistic image synthesis. It is written in Java and built around a flexible ray tracing core and an extensible object-oriented design. -Please consult the LICENSE file for license information. - ----------------------------------------------------------------- - -Quickstart: +Please consult the [LICENSE file](https://github.com/Harium/sunflow/blob/master/LICENSE) for license information. +

    + Quickstart The fastest way to get started rendering images if you are not familiar with Java development is to get the binary release from the website. You will also need the latest Java SE JDK (to get the server VM) from http://java.sun.com/. Launch sunflow from the command prompt like this: @@ -31,12 +29,18 @@ You can find some simple demo scenes on the website's data distribution. A simpl DISCLAIMER: Keep in mind that this is still an early version. At the moment you will need to dig around the scene files in order to get the most out of the software. If you have any questions, feel free to e-mail me at the adress at the top. +
    ----------------------------------------------------------------- +
    + Build instructions +Download the latest JDK (6.0 at the time of this writing) if you don't have it already. Please note that the source code makes use of some new features like generics. Keep this in mind if you are trying to compile/run the code under a different JVM. -Build instructions: +Using mvn, type: +```bash +mvn clean package +``` -Download the latest JDK (6.0 at the time of this writing) if you don't have it already. Please note that the source code makes use of some new features like generics. Keep this in mind if you are trying to compile/run the code under a different JVM. +Or, using javac Create a main directory to hold the code (for example, "sunflow") and unzip the contents of the source package into it, preserving sub-directories. Create the "classes" subdirectory if your unzip program did not. You may now compile the software from the main directory by running: @@ -48,15 +52,18 @@ once the compiling is complete, run the code with: The tips above apply here as well (-Xmx and -server command line options). ----------------------------------------------------------------- +
    -Scene file format: + +## Scene file format The SunflowGUI program accepts input in the .sc file format. As this is only a temporary file format, the best documentation for it is SCParser.java. You may also get a feel for what is supported by examining the example scene files provided in the data distribution. ----------------------------------------------------------------- +More information can be found at the [manual](https://github.com/Harium/sunflow/blob/master/Sunflow-Manual.pdf). -Rendering options: +---------------------------------------------------------------- +
    + Rendering options Here is a quick explanation of the basic rendering options. @@ -117,21 +124,43 @@ Caustics are produced by light shining through refractive objects or being bounc Once you have a number of photons to emit, you must pick a way to store them. Only a kd engine is currently available for caustics. You can then set a value for the number of photons to gather at each shading point (start with ~50 to ~100) as well as a maximum search radius. These settings are highly scene dependent so experiment with them until you get satisfactory results. ----------------------------------------------------------------- +
    -Third party libraries: +## Third party libraries Sunflow makes use of the following libraries, distributed according to the following terms: +
    + Janino License Janino - An embedded Java[TM] compiler -Copyright (c) 2006, Arno Unkrig +Copyright (c) 2001-2016, Arno Unkrig +Copyright (c) 2015-2016 TIBCO Software Inc. All rights reserved. -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials + provided with the distribution. + 3. Neither the name of JANINO nor the names of its contributors + may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +
    From 451879eac0a86bd0e3d98115cfeaf8dca38bd77e Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Mon, 15 Apr 2019 13:44:40 -0300 Subject: [PATCH 33/37] Add box type --- .../CornellBoxJensenWithBoxesScene.java | 119 ++++++++++++++++++ src/main/java/examples/GlassScene.java | 118 +++++++++++++++++ .../core/parameter/geometry/BoxParameter.java | 46 +++++++ .../parameter/geometry/GeometryParameter.java | 8 ++ .../parameter/geometry/ObjectParameter.java | 1 + .../org/sunflow/core/parser/SCParser.java | 14 +++ .../java/org/sunflow/core/primitive/Box.java | 28 ++--- .../parameter/geometry/BoxParameterTest.java | 36 ++++++ 8 files changed, 353 insertions(+), 17 deletions(-) create mode 100644 src/main/java/examples/CornellBoxJensenWithBoxesScene.java create mode 100644 src/main/java/examples/GlassScene.java create mode 100644 src/main/java/org/sunflow/core/parameter/geometry/BoxParameter.java create mode 100644 src/test/java/org/sunflow/core/parameter/geometry/BoxParameterTest.java diff --git a/src/main/java/examples/CornellBoxJensenWithBoxesScene.java b/src/main/java/examples/CornellBoxJensenWithBoxesScene.java new file mode 100644 index 0000000..6d6efa2 --- /dev/null +++ b/src/main/java/examples/CornellBoxJensenWithBoxesScene.java @@ -0,0 +1,119 @@ +package examples; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.parameter.ImageParameter; +import org.sunflow.core.parameter.InstanceParameter; +import org.sunflow.core.parameter.PhotonParameter; +import org.sunflow.core.parameter.TraceDepthsParameter; +import org.sunflow.core.parameter.camera.PinholeCameraParameter; +import org.sunflow.core.parameter.geometry.SphereParameter; +import org.sunflow.core.parameter.gi.InstantGIParameter; +import org.sunflow.core.parameter.light.CornellBoxLightParameter; +import org.sunflow.core.parameter.shader.GlassShaderParameter; +import org.sunflow.core.parameter.shader.MirrorShaderParameter; +import org.sunflow.image.Color; +import org.sunflow.math.Matrix4; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class CornellBoxJensenScene { + + public static void main(String[] args) { + SunflowAPI api = new SunflowAPI(); + api.reset(); + + ImageParameter image = new ImageParameter(); + image.setResolutionX(800); + image.setResolutionY(600); + image.setAAMin(0); + image.setAAMax(2); + image.setFilter(ImageParameter.FILTER_GAUSSIAN); + image.setup(api); + + TraceDepthsParameter traceDepths = new TraceDepthsParameter(); + traceDepths.setDiffuse(4); + traceDepths.setReflection(3); + traceDepths.setRefraction(2); + traceDepths.setup(api); + + PhotonParameter photons = new PhotonParameter(); + photons.setNumEmit(1000000); + photons.setCaustics("kd"); + photons.setCausticsGather(100); + photons.setCausticsRadius(0.5f); + photons.setup(api); + + InstantGIParameter gi = new InstantGIParameter(); + gi.setSamples(64); + gi.setSets(1); + gi.setBias(0.00003f); + gi.setBiasSamples(0); + gi.setup(api); + + PinholeCameraParameter camera = new PinholeCameraParameter(); + + camera.setName("camera"); + Point3 eye = new Point3(0, -205, 50); + Point3 target = new Point3(0, 0, 50); + Vector3 up = new Vector3(0, 0, 1); + + camera.setupTransform(api, eye,target,up); + + camera.setFov(45f); + camera.setAspect(1.333333f); + camera.setup(api); + + // Materials + MirrorShaderParameter mirror = new MirrorShaderParameter("Mirror"); + mirror.setReflection(new Color(0.7f, 0.7f, 0.7f)); + mirror.setup(api); + + GlassShaderParameter glass = new GlassShaderParameter("Glass"); + glass.setEta(1.6f); + glass.setAbsorptionColor(new Color(1, 1, 1)); + glass.setup(api); + + // Lights + CornellBoxLightParameter lightParameter = new CornellBoxLightParameter(); + lightParameter.setName("cornell-box-light"); + lightParameter.setMin(new Point3(-60, -60, 0)); + lightParameter.setMax(new Point3(60, 60, 100)); + lightParameter.setLeft(new Color(0.8f, 0.25f, 0.25f)); + lightParameter.setRight(new Color(0.25f, 0.25f, 0.8f)); + lightParameter.setTop(new Color(0.7f, 0.7f, 0.7f)); + lightParameter.setBottom(new Color(0.7f, 0.7f, 0.7f)); + lightParameter.setBack(new Color(0.7f, 0.7f, 0.7f)); + lightParameter.setRadiance(new Color(15, 15, 15)); + lightParameter.setSamples(32); + lightParameter.setup(api); + + SphereParameter mirrorSphere = new SphereParameter(); + mirrorSphere.setName("mirror-sphere"); + mirrorSphere.setCenter(new Point3(-30, 30, 20)); + mirrorSphere.setInstanceParameter(new InstanceParameter().shaders("Mirror")); + mirrorSphere.setRadius(20); + mirrorSphere.setup(api); + + SphereParameter glassSphere = new SphereParameter(); + glassSphere.setName("glass-sphere"); + glassSphere.setCenter(new Point3(28, 2, 20)); + glassSphere.setInstanceParameter(new InstanceParameter().shaders("Glass")); + glassSphere.setRadius(20); + glassSphere.setup(api); + + finalRender(api); + } + + private static void previewRender(SunflowAPI api) { + api.parameter("sampler", "ipr"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + + private static void finalRender(SunflowAPI api) { + api.parameter("sampler", "bucket"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + +} diff --git a/src/main/java/examples/GlassScene.java b/src/main/java/examples/GlassScene.java new file mode 100644 index 0000000..5db1528 --- /dev/null +++ b/src/main/java/examples/GlassScene.java @@ -0,0 +1,118 @@ +package examples; + +import org.sunflow.SunflowAPI; +import org.sunflow.core.parameter.ImageParameter; +import org.sunflow.core.parameter.InstanceParameter; +import org.sunflow.core.parameter.PhotonParameter; +import org.sunflow.core.parameter.TraceDepthsParameter; +import org.sunflow.core.parameter.camera.PinholeCameraParameter; +import org.sunflow.core.parameter.geometry.SphereParameter; +import org.sunflow.core.parameter.gi.InstantGIParameter; +import org.sunflow.core.parameter.light.CornellBoxLightParameter; +import org.sunflow.core.parameter.shader.GlassShaderParameter; +import org.sunflow.core.parameter.shader.MirrorShaderParameter; +import org.sunflow.image.Color; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class GlassScene { + + public static void main(String[] args) { + SunflowAPI api = new SunflowAPI(); + api.reset(); + + ImageParameter image = new ImageParameter(); + image.setResolutionX(800); + image.setResolutionY(600); + image.setAAMin(0); + image.setAAMax(2); + image.setFilter(ImageParameter.FILTER_GAUSSIAN); + image.setup(api); + + TraceDepthsParameter traceDepths = new TraceDepthsParameter(); + traceDepths.setDiffuse(4); + traceDepths.setReflection(3); + traceDepths.setRefraction(2); + traceDepths.setup(api); + + PhotonParameter photons = new PhotonParameter(); + photons.setNumEmit(1000000); + photons.setCaustics("kd"); + photons.setCausticsGather(100); + photons.setCausticsRadius(0.5f); + photons.setup(api); + + InstantGIParameter gi = new InstantGIParameter(); + gi.setSamples(64); + gi.setSets(1); + gi.setBias(0.00003f); + gi.setBiasSamples(0); + gi.setup(api); + + PinholeCameraParameter camera = new PinholeCameraParameter(); + + camera.setName("camera"); + Point3 eye = new Point3(0, -205, 50); + Point3 target = new Point3(0, 0, 50); + Vector3 up = new Vector3(0, 0, 1); + + camera.setupTransform(api, eye,target,up); + + camera.setFov(45f); + camera.setAspect(1.333333f); + camera.setup(api); + + // Materials + MirrorShaderParameter mirror = new MirrorShaderParameter("Mirror"); + mirror.setReflection(new Color(0.7f, 0.7f, 0.7f)); + mirror.setup(api); + + GlassShaderParameter glass = new GlassShaderParameter("Glass"); + glass.setEta(1.6f); + glass.setAbsorptionColor(new Color(1, 1, 1)); + glass.setup(api); + + // Lights + CornellBoxLightParameter lightParameter = new CornellBoxLightParameter(); + lightParameter.setName("cornell-box-light"); + lightParameter.setMin(new Point3(-60, -60, 0)); + lightParameter.setMax(new Point3(60, 60, 100)); + lightParameter.setLeft(new Color(0.8f, 0.25f, 0.25f)); + lightParameter.setRight(new Color(0.25f, 0.25f, 0.8f)); + lightParameter.setTop(new Color(0.7f, 0.7f, 0.7f)); + lightParameter.setBottom(new Color(0.7f, 0.7f, 0.7f)); + lightParameter.setBack(new Color(0.7f, 0.7f, 0.7f)); + lightParameter.setRadiance(new Color(15, 15, 15)); + lightParameter.setSamples(32); + lightParameter.setup(api); + + SphereParameter mirrorSphere = new SphereParameter(); + mirrorSphere.setName("mirror-sphere"); + mirrorSphere.setCenter(new Point3(-30, 30, 20)); + mirrorSphere.setInstanceParameter(new InstanceParameter().shaders("Mirror")); + mirrorSphere.setRadius(20); + mirrorSphere.setup(api); + + SphereParameter glassSphere = new SphereParameter(); + glassSphere.setName("glass-sphere"); + glassSphere.setCenter(new Point3(28, 2, 20)); + glassSphere.setInstanceParameter(new InstanceParameter().shaders("Glass")); + glassSphere.setRadius(20); + glassSphere.setup(api); + + finalRender(api); + } + + private static void previewRender(SunflowAPI api) { + api.parameter("sampler", "ipr"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + + private static void finalRender(SunflowAPI api) { + api.parameter("sampler", "bucket"); + api.options(SunflowAPI.DEFAULT_OPTIONS); + api.render(SunflowAPI.DEFAULT_OPTIONS, null); + } + +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/BoxParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/BoxParameter.java new file mode 100644 index 0000000..a83cceb --- /dev/null +++ b/src/main/java/org/sunflow/core/parameter/geometry/BoxParameter.java @@ -0,0 +1,46 @@ +package org.sunflow.core.parameter.geometry; + +import org.sunflow.SunflowAPIInterface; +import org.sunflow.math.Point3; +import org.sunflow.math.Vector3; + +public class BoxParameter extends GeometryParameter { + + Point3 min; + Point3 max; + + public BoxParameter() { + + } + + public BoxParameter(String name) { + super(name); + } + + @Override + public void setup(SunflowAPIInterface api) { + super.setup(api); + api.parameter("min", min); + api.parameter("max", max); + + api.geometry(name, TYPE_BOX); + + setupInstance(api); + } + + public Point3 getMin() { + return min; + } + + public void setMin(Point3 min) { + this.min = min; + } + + public Point3 getMax() { + return max; + } + + public void setMax(Point3 max) { + this.max = max; + } +} diff --git a/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java index fe17e6f..6832d67 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/GeometryParameter.java @@ -4,6 +4,14 @@ public abstract class GeometryParameter extends ObjectParameter { + public GeometryParameter() { + + } + + public GeometryParameter(String name) { + this.name = name; + } + public void setupInstance(SunflowAPIInterface api) { if (instanceParameter != null) { instanceParameter.name(name + ".instance"); diff --git a/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java index 5119890..6fca44d 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/ObjectParameter.java @@ -11,6 +11,7 @@ public class ObjectParameter implements Parameter { public static final String TYPE_BANCHOFF = "banchoff"; public static final String TYPE_BEZIER_MESH = "bezier_mesh"; + public static final String TYPE_BOX = "box"; public static final String TYPE_CYLINDER = "cylinder"; public static final String TYPE_GUMBO = "gumbo"; public static final String TYPE_HAIR = "hair"; diff --git a/src/main/java/org/sunflow/core/parser/SCParser.java b/src/main/java/org/sunflow/core/parser/SCParser.java index 10e46bd..7cdc01a 100644 --- a/src/main/java/org/sunflow/core/parser/SCParser.java +++ b/src/main/java/org/sunflow/core/parser/SCParser.java @@ -908,6 +908,20 @@ private void parseObjectBlock(SunflowAPIInterface api) throws ParserException, I } else { geometry.setup(api); } + } else if (type.equals("box")) { + UI.printInfo(Module.API, "Reading box ..."); + BoxParameter geometry = new BoxParameter(); + geometry.setInstanceParameter(instanceParameter); + geometry.setName(name); + + if (p.peekNextToken("min")) { + geometry.setMin(parsePoint()); + } + if (p.peekNextToken("max")) { + geometry.setMax(parsePoint()); + } + + geometry.setup(api); } else if (type.equals("cylinder")) { UI.printInfo(Module.API, "Reading cylinder ..."); diff --git a/src/main/java/org/sunflow/core/primitive/Box.java b/src/main/java/org/sunflow/core/primitive/Box.java index 3964631..c6e7aea 100644 --- a/src/main/java/org/sunflow/core/primitive/Box.java +++ b/src/main/java/org/sunflow/core/primitive/Box.java @@ -7,10 +7,7 @@ import org.sunflow.core.Ray; import org.sunflow.core.ShadingState; import org.sunflow.core.ParameterList.FloatParameter; -import org.sunflow.math.BoundingBox; -import org.sunflow.math.Matrix4; -import org.sunflow.math.OrthoNormalBasis; -import org.sunflow.math.Vector3; +import org.sunflow.math.*; public class Box implements PrimitiveList { private float minX, minY, minZ; @@ -22,19 +19,16 @@ public Box() { } public boolean update(ParameterList pl, SunflowAPI api) { - FloatParameter pts = pl.getPointArray("points"); - if (pts != null) { - BoundingBox bounds = new BoundingBox(); - for (int i = 0; i < pts.data.length; i += 3) - bounds.include(pts.data[i], pts.data[i + 1], pts.data[i + 2]); - // cube extents - minX = bounds.getMinimum().x; - minY = bounds.getMinimum().y; - minZ = bounds.getMinimum().z; - maxX = bounds.getMaximum().x; - maxY = bounds.getMaximum().y; - maxZ = bounds.getMaximum().z; - } + Point3 min = pl.getPoint("min", null); + Point3 max = pl.getPoint("max", null); + + minX = min.x; + minY = min.y; + minZ = min.z; + maxX = max.x; + maxY = max.y; + maxZ = max.z; + return true; } diff --git a/src/test/java/org/sunflow/core/parameter/geometry/BoxParameterTest.java b/src/test/java/org/sunflow/core/parameter/geometry/BoxParameterTest.java new file mode 100644 index 0000000..463b45f --- /dev/null +++ b/src/test/java/org/sunflow/core/parameter/geometry/BoxParameterTest.java @@ -0,0 +1,36 @@ +package org.sunflow.core.parameter.geometry; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.sunflow.SunflowAPI; +import org.sunflow.core.Geometry; +import org.sunflow.math.Point3; + +public class BoxParameterTest { + + SunflowAPI api; + BoxParameter parameter; + + @Before + public void setUp() { + api = new SunflowAPI(); + parameter = new BoxParameter(); + } + + @Test + public void testSetupAPI() { + // Set values + parameter.setName("uniqueName"); + + parameter.setMin(new Point3(-1,-1,-1)); + parameter.setMax(new Point3(1,1,1)); + + // Set parameters + parameter.setup(api); + + Geometry geometry = (Geometry) api.getRenderObjects().get(parameter.getName()).obj; + Assert.assertNotNull(geometry); + } + +} From c39511ede177a92383e208f1b329c1843befd6cc Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Mon, 15 Apr 2019 13:45:24 -0300 Subject: [PATCH 34/37] Add a Cornell box scene with boxes --- .../CornellBoxJensenWithBoxesScene.java | 30 +++++++++---------- .../parameter/geometry/PlaneParameter.java | 8 +++++ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/main/java/examples/CornellBoxJensenWithBoxesScene.java b/src/main/java/examples/CornellBoxJensenWithBoxesScene.java index 6d6efa2..99f26ba 100644 --- a/src/main/java/examples/CornellBoxJensenWithBoxesScene.java +++ b/src/main/java/examples/CornellBoxJensenWithBoxesScene.java @@ -6,17 +6,17 @@ import org.sunflow.core.parameter.PhotonParameter; import org.sunflow.core.parameter.TraceDepthsParameter; import org.sunflow.core.parameter.camera.PinholeCameraParameter; +import org.sunflow.core.parameter.geometry.BoxParameter; import org.sunflow.core.parameter.geometry.SphereParameter; import org.sunflow.core.parameter.gi.InstantGIParameter; import org.sunflow.core.parameter.light.CornellBoxLightParameter; import org.sunflow.core.parameter.shader.GlassShaderParameter; import org.sunflow.core.parameter.shader.MirrorShaderParameter; import org.sunflow.image.Color; -import org.sunflow.math.Matrix4; import org.sunflow.math.Point3; import org.sunflow.math.Vector3; -public class CornellBoxJensenScene { +public class CornellBoxJensenWithBoxesScene { public static void main(String[] args) { SunflowAPI api = new SunflowAPI(); @@ -87,19 +87,19 @@ public static void main(String[] args) { lightParameter.setSamples(32); lightParameter.setup(api); - SphereParameter mirrorSphere = new SphereParameter(); - mirrorSphere.setName("mirror-sphere"); - mirrorSphere.setCenter(new Point3(-30, 30, 20)); - mirrorSphere.setInstanceParameter(new InstanceParameter().shaders("Mirror")); - mirrorSphere.setRadius(20); - mirrorSphere.setup(api); - - SphereParameter glassSphere = new SphereParameter(); - glassSphere.setName("glass-sphere"); - glassSphere.setCenter(new Point3(28, 2, 20)); - glassSphere.setInstanceParameter(new InstanceParameter().shaders("Glass")); - glassSphere.setRadius(20); - glassSphere.setup(api); + BoxParameter mirrorBox = new BoxParameter(); + mirrorBox.setName("mirror-box"); + mirrorBox.setMin(new Point3(-50, 10, 0)); + mirrorBox.setMax(new Point3(-10, 50, 40)); + mirrorBox.shaders(mirror); + mirrorBox.setup(api); + + BoxParameter glassBox = new BoxParameter(); + glassBox.setName("glass-box"); + glassBox.setMin(new Point3(8, -22, 0)); + glassBox.setMax(new Point3(48, 22, 40)); + glassBox.shaders(glass); + glassBox.setup(api); finalRender(api); } diff --git a/src/main/java/org/sunflow/core/parameter/geometry/PlaneParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/PlaneParameter.java index 4b6aa73..cc35602 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/PlaneParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/PlaneParameter.java @@ -11,6 +11,14 @@ public class PlaneParameter extends GeometryParameter { Point3 point2; Vector3 normal; + public PlaneParameter() { + + } + + public PlaneParameter(String name) { + super(name); + } + @Override public void setup(SunflowAPIInterface api) { super.setup(api); From 0eb16d9829d3f80f1226b40563ace5b4273938e4 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Mon, 15 Apr 2019 13:47:49 -0300 Subject: [PATCH 35/37] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 9f46533..c2fc9e4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ v0.07.5 * Added procedural scenes +* Added box type v0.07.4 * Updated janino version to 2.7.8 * Allow changing shading cache parameters for a quality/speed trade-off From 35aaa8cc5f653d20dda4da1cc98cab42bce121e1 Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Mon, 15 Apr 2019 13:48:07 -0300 Subject: [PATCH 36/37] Update version in SunflowAPI --- src/main/java/org/sunflow/SunflowAPI.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/sunflow/SunflowAPI.java b/src/main/java/org/sunflow/SunflowAPI.java index b31b0fa..65ebf34 100644 --- a/src/main/java/org/sunflow/SunflowAPI.java +++ b/src/main/java/org/sunflow/SunflowAPI.java @@ -46,7 +46,7 @@ * scene. */ public class SunflowAPI implements SunflowAPIInterface { - public static final String VERSION = "0.07.3"; + public static final String VERSION = "0.07.5"; public static final String DEFAULT_OPTIONS = "::options"; protected Scene scene; From d1976f86ec68cc1e312838fb8375eb33831b381c Mon Sep 17 00:00:00 2001 From: Yuri Pourre Date: Mon, 15 Apr 2019 23:37:17 -0300 Subject: [PATCH 37/37] Check if normals and uvs are null --- .../geometry/GenericMeshParameter.java | 20 +++++++------ .../core/primitive/TriangleMeshTest.java | 28 +++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 src/test/java/org/sunflow/core/primitive/TriangleMeshTest.java diff --git a/src/main/java/org/sunflow/core/parameter/geometry/GenericMeshParameter.java b/src/main/java/org/sunflow/core/parameter/geometry/GenericMeshParameter.java index e59b557..684efc5 100644 --- a/src/main/java/org/sunflow/core/parameter/geometry/GenericMeshParameter.java +++ b/src/main/java/org/sunflow/core/parameter/geometry/GenericMeshParameter.java @@ -20,16 +20,20 @@ public void setup(SunflowAPIInterface api) { api.parameter("points", "point", "vertex", points); api.parameter("triangles", triangles); - if (!faceVaryingNormals) { - api.parameter("normals", "vector", "vertex", normals); - } else { - api.parameter("normals", "vector", "facevarying", normals); + if (normals != null) { + if (!faceVaryingNormals) { + api.parameter("normals", "vector", "vertex", normals); + } else { + api.parameter("normals", "vector", "facevarying", normals); + } } - if (!faceVaryingTextures) { - api.parameter("uvs", "texcoord", "vertex", uvs); - } else { - api.parameter("uvs", "texcoord", "facevarying", uvs); + if (uvs != null) { + if (!faceVaryingTextures) { + api.parameter("uvs", "texcoord", "vertex", uvs); + } else { + api.parameter("uvs", "texcoord", "facevarying", uvs); + } } if (faceShaders != null) { diff --git a/src/test/java/org/sunflow/core/primitive/TriangleMeshTest.java b/src/test/java/org/sunflow/core/primitive/TriangleMeshTest.java new file mode 100644 index 0000000..98e0fae --- /dev/null +++ b/src/test/java/org/sunflow/core/primitive/TriangleMeshTest.java @@ -0,0 +1,28 @@ +package org.sunflow.core.primitive; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.sunflow.math.Point3; + +public class TriangleMeshTest { + + TriangleMesh mesh; + + @Before + public void setUp() { + mesh = new TriangleMesh(); + } + + @Test + public void testInit() { + mesh.points = new float[]{0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8}; + mesh.triangles = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8}; + + Assert.assertEquals(3, mesh.getNumPrimitives()); + + Assert.assertEquals(new Point3(0, 0, 0), mesh.getPoint(0)); + Assert.assertEquals(new Point3(1, 1, 1), mesh.getPoint(1)); + Assert.assertEquals(new Point3(2, 2, 2), mesh.getPoint(2)); + } +}

    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�pM{G`jCuO19@2Z<(AR_?z}DWQ#;2L9&yv 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�@kvQ&M$V^+dvG*eM%&&+K@C9G>0a{qdW-ak=-7Khbgudv!| zR)^IRDYw}ha}JNo;$oO~OMAE^j~^=L=5ScFTF`=UmENojr_6Snr7`Tw;|J%1<(Yc8 zE0##O+TW4O1bs_8AV{1Sb`0pO=$r)hbxuxCvX3Ug8tKXD`)PUV{`7sPt8TaSd#LNM zR%xY9k{X$UthK8Wi1!2`k4Zk2s!b)a2!{hP9E-+Eagm!4!@wR$Q(ZOHQ87#uxjjiE z)(NH$>HV=vJU2vaixs(}{OXEtZK$s6p1!w%xYDt-u{P%O84VBKTlXVuJwvFMR%W+f zSSG7~!Q3yNe)Z0;`;Vx*#*4$=471@%IzxA`MpxL)E)Fbyyo*MnhoOwcV&nt$okOp^ zg_Xw-ae8QVpL%qEHV3DTO%=Y3iLXPf z4hhbF1Ot$mSZ%7-$;OCS%EP;yL~B>a^6x#}!2P&&Z~cm^{+|y05pPl-4eouREg-z~ z?dy0G4t|935uxXO_3!Fwz;=KrB{f^MqU{h=l!j!6^xM2Kl62QgEjR1kA@fZ)N@v!stPn5rsf@GnGSk|&fve$QO;xxi21AHSfsyCY)-TcP#%zNXC68l66=IH?eAZ6jSQiWH z331^G&Gg2dssx5GQ6xA;r%)r{!a@A8nmwp0c$BiJ%0V@QFCNt9xu#CwN08%lz@g}G zDNGVZk{}H>f;6&kFqR^i?`H;VDCfp(K6lZWuw#xvh24|i^gfw!vM!l?!XlV>9R>Vp z!rBIB!cWYOA01xJt#5ako8!6qYYvdNmVacwy6iObmK!rD`AJ25K_#j zk;Hx)`6xP%--F$x&wGcxi=@uUsssn0){XzAnh5(oeJ@PRe1-3PD01_jq)Xc(3?YCQce1J4B(dj3q3PXs(?VDox&3l~ z(@W~D%P$Xob=OP;4unz}>&yPO5J*3hN4=xpcGopTY{vvL}#D#)p*V|yf2;GM~ zXshBIbie8*qV8HZVJ&Pl-U^fd*~r`y-M4Ic{Nn%EuGZitt}}Y?KBQeqE3ItFTCL>A z+Im=$CD|6T!DHh!#C9-m0!(?Ryqq@J7&kW0F~;%K#5N%*~`L{kiB zXF0t|g1J)k5H1`vnFOapI)yg?{`+wPjU^cfOJid$uLMd?9~pjtM?xXcHn>=Z-D=Bl z*p)cY(@G3Tr;mGM)N(xM2cwrZ)=ByDa~|AmVi&Odf2K_jGVUy0$wK5FVVa-ku0nl}xvLaXu0bh@6}X}NE?Z(~@C&StlGZA^%>LGHtn(_q9#15G2DmYi0n zWHew_XVW#BS-WjOR*~W|(LL5v&XW`ekqy-MXr?=Q=}YncJ(#Xs0f7wGxpJ6nSIRud ziHJ4i5cy+m_HIVxym|qq%o|Kb)B0LZoFUd2HM*CtZr>$nLUfa@?^tSNaldAn_e;(fp6qvpohy_k~A zcD6OO%r020fBo3ItC!7=ObC>&+pF#rKqGoTPF)Qd8o}vYW zaci0&&{#kwnlT^@Dtr>JfzDvUy9Nz3YcLsxaF)uW0ltv=3qvtOxa4?_PSy46Xx&N5 zfGo%a&{)hGeG>N)<)dGM!$WBpDYvGg2Z5wJ5@JA=5S%Rp7al9=F>B18r&$FxFKNeL zPg-*2%3GsnnC+u=bk~DTTarJ=^R{R+u6p=)x`XXN1?cyx|791Bb2d9hRKUU7a&2~_ ziMKdp(PC*$bGc*}oER2LBQ936y!?R6MafQiKm}J48S1`QU>kFf_1llwDLY|*1O~=AE6o@0Ng(iK$&Q3f=4$%vg@Btx8)3uGzLk3U?D;y-eA`)W> zGyZU*xb;x-_U^6>BS+EHb^KmVo?D#3c~kh=RRAP{P^zj^7>Htc<03(m~N@0 zdCth+WKtgLW~8#RFKX?6o}#EZ78d5bK7E^lynMVD=6 zvkSXwT$E~1cs94661+XgfgNB9ANs}Y9+tR*-v>J}+I~&~^fd6~ir>jn+JtpcDKTtK z?SrNlz8sHTMvT)Z+r4qD(Aja6hv6h;xZ*g6$&Diq$|d-WIYVb>C&`6*xyUMdOB9D0 z11x$7h-7Pger<{DwlsntefsNDN0NWW(l>ZK+df!3y@4B zS79-E9oOMBylnJS#WAz?*_%ziw46U(8vp%6i%T}|OaA6!PfAbn!6SFEQ;`X^sO~mH zH@8nuuhg&Mw(7fhhT}OC;yIpWXmEqD>LBLKKz8A=8DWMUKqmNQLWEgn#KU5k1;Q-N z7qq^s$*F_v10mC+GLuTdIDzfp*-A<%O6gt68C|$9Ic?dzsyygP>{R8Ov&;qcdt(38 zNuWlr2kAlP7<*j# zEdM@}ozHlrI87TEgWoAQJ>^W9uiU?oUF2LOFN!rV4Qzw6L2ihN&u}rH#4;AU_GL`# z&t{GN1%jn9_c+yv0{sCh03=e4aeJft7}xl#D2@^?#m51c62l~9kY`F_9jU6Iai};L z$y7ue4?1NCbA&9AFVeH@ab(T0fd52{kTinuuw|yjv8beJPV|^iWaYq+!I3;Ar16 z%f`1K{OM=M_I))w{=?jtmNs?wmK>VZ`!jlCczV;4Eo+|p?%rBM<L+`h)O~k7= zpX}IvZg5A(vdZtIcCg~%W%@A8(TqaqggWb%bjvv+jX0Sa_x#NDuJ!J2SGRkQxYO0= zJ}CY&@Y}$3@p`}<;hd7>%+H}Q%^ICvi5xiZ%q3=Qo?@1`{tzn(Y!9+xD@k!-V^2Q8 zI9!lQab#I5hsmBMq+!4pSg7GldLm&Xpq=Xr>O{c@aPkCGy#=Jqiy{?XFZaklAg!wc1$ zrZ#kld3E*ULI*DlpP(wH$%fAS%b5A#=uiGd`;vRFy`DVZ_M>XQJzsJa)jt34x$=0s z>FzsMu}#Bq|HC`DmwpfS!GYqcn+kGzYtUhIl13Ud8-pxsc1q5(%7f-dv29p<9M4Ip zMPogT5P*6ztanux6i!ql1=Vj>snKZ7q*7X_e5N2$RT+J7WKuLdxhhgnqrJmH=r*6g zJEWs*v`{TP|82X;NVS{npi{3mx`P?ng7Rd~$x+TJt6uouo-}UPT(lq{118{ZBO@>O*KBlgWqV%!hZN|GAM4J+GFqI$cp3O*1-#$O(i_ z$I?2Ag$r>MqIFCWyY;_pSACEZjq^z53?O-!N?Q%k zP=Sq#4Nqev;EfdKqK!k67DlzBWX201RV~%Z=nx^Y+S*nB-HCCyerO0jJv5~2yB*cI zQm=8jlA}lpkZC(X;1r-pkhVjJQK&)!pc-)!qhK1KBFQw-fblf=|3y~yBI~+Npdx8h z8=)0R>qR=W65F7iSUj;8pN;=)SS52RISe1F_x@%yZ|0JBAaEOpNq48( z(w^eEQGS%y1`R0I_&1rNZe8=&WtES`Tf|1QH!itRpc8^njCtI!#q0^zjl3qEfp0a2 zy)KUaY`w2HuT}s@$X6}#YrTt;$<(CsmF_|nX z3DUfO$HwD-JQCcT$vn~fRB^?=1tDiSEVbU!dDriBd@A33ME!!i8{yLnURNH-`@4)! zhaH*^Q$fvLr*KF#Ztidk!ER_f%1 zDsyo_2Q9Vc4_xZJ4l8G?3#S&DKIMw-pYojc4NX!{RcFxu9oNz^0&{^eTWADaxa=+7!ZL33(E7j6=z(yi?CbmjNlc- z7#<@cR36V0d8*(oh<&C$b_gFLw$Pi5gXTfYpmmTL@(zh3_z1Dxyxp?hx}6#Ej);LS zbC(4(&@=^DrxRivhc1^35(54##RyijUB2Z1Ae;Cgq>Um?QE@x^qS0iE z*wLGm3wN4~v_x8<%W0wrPJ}eT;w)EzeM+NIk&HAhS}i#3pKe5<#sW{FWYmMo!7(H7 zrN($yt;T6Cu7G@8fiE$&0!j$^$;hYc+x3Kic22*E={=Uu>!e75Doin7YAd?5o6QWl z{$yb@Q!p?Ev{?#BxR5t#fUphCN|=Ej$RZKb`zr+{&8wh9&ya4|k`vhK!nUXXknT22?V3UOA zgx?*C7@0=eohEIeL~C#QhaZseEfSJsusM-Llc+Hn2n6X!1!fex>-*SzMSMs+D&k^x ziZNniDj5D!HiL`^0+D&A?)3@`3SPQql?Q4cqv%*iPq)Xhn66V|G%opB4#1e~ljCfp z4+1~d2cuk~4??N24}8n%Y5k^~Jbx61Y{-s~2uK_9d~H%Ial_joKOzCm{)Kc2v*j@L zov)6iTzp|=?Zmp7GuL6?8HZATe$~%MFFx=>?N01y*TyHm_nhK+?`Z>+_)d+X#A0ox-x4OS;>M_BrCdr~k40$#(i-Rx2JEfz;9 zj5;J+=X)y{`H~#eO`Tf)7{R9}SQNc<=m6h-iRZ=;lCyEsV#W0~WC{2XEkbUA1n?WgLDzbfGkc1WFen1fDplVm`e8vc;V>#T=CLG7zEP|NMg}RYB@C!Y)nX&G~WD=Tp~-@>bBP}!M*9y=R2>S+^2qaOIlrb zUsuN}EDYJ>aLM`&b8dV3liHadiA%S^WpLW=EuPH8wVyQRSL0jnQ)qd~UUk6^^~%zM z_hyYdEb7xU2$L{~fZr=C`a^3%kA!f`XoFxiLDM>mdzcrjF6c_QaR~_FaDb2mI_z&6 zW(B}OF45#}vfwUnz{mwdCOjbf2#1BX$_IVPur*0HIpS`QCmf*>5hD@Y#jQ5>npOjO zj*Q6_%yc+r_eKa@12Yz#1arJRHa1q!$yU@h?K9+2wf42^jSK4yTsN#=mSOcuELPtq zACcIVb2m_P_><7DBPnTIv+0jo*J@9=1lfVkj#Y$W$ntl;qFzok!)b#(&M8yao{j3M z(y_Ns&(4=84q!`yI)E!XX<7a2mO1XQn!9zaDz7c6WV>_mLG?HPCA{imn>g?Ay?1B( z&SyJk$6x0=pL0In`E18=NGMsrX-o_W38e)o6pEHYfv(*UA4ZVCC}Q12r*$iphEP$Z z3Bkl#N~?CQ=s<&Z1;R%tbdz8kM5yap5Jg7&V;z%5eC+q0?eL*aiJ_+6=6K@aA1Mq;1-F4(Ty*1DPuFU^{>;HoVZV}o%18^!jIs+RHnm1A*WK81p z*V!8?$2E8Nc50*w&?|GL)FeQKQa7tvd27nuFKv54r*cFq@o_=CP9lR*e<;dt#05o+ z8tVN*v%>L!l^`SYbLQr{Y)iu>J#e6Dy1GK=?~f( z1~XW<+NRXX=ny6mcq;!@RlP7UfCb+bDPz$>aJhwLthTk1(c<=|w5;wmuz0m#@k(Ry zYJO0>zD>HO-2*a!BbL`sPs*4|t-kJU*4$Y>*InFveAK*z(&O67Y=5FyBAt&3X>-r7 z&5IYlJaiiMD-ph-2zy>eS;E(Ql{`MU)LySHv`&YJ{S- zjKI5TnrZMfCp1kC_*sU`q+>F}t0^G@2p`J2^&0K6%{AKL#8MA1*3Qm@1QW7WG*kP6 zm|D6I9*7)X<-WT`F&s#|HM%LA@&HU{X%@K}q^+}5ES4&#H_xc-oCQCtTwBw_=sMH0 z#w?*%>-UM$x^9RhuEKV-!(6hi0L-&=pWFu6G!lMdalydAWaC)Q8nYG{S=FP6J~-Vp zMK|J!8-0vF2*Vaw4{RhTK204VDI$==Hu@|a2Us^dL0oHYbewhjzzC7;G; zWgp6EZ0AVT&vpCE(nM*blhrKYKt$l~4e~bJBgB>hq==WpPzrsD=*h-eMRaFXie@6L z&%KOXM}AKJgQUsckjG8hT0$(#fFJ#yqzQ?LknC?IG56_LP+9SR1Uu63$)Rw~vFpyn z`-4EwAhQ(5lz?q1kwJXkV)dg48Uhgcfk_-z^$K*UvwJinHn4$wY@~0+ieD~U^-k_! z?;>i?`U1@3==(v1zp!HE;%QYKgpIeo1@lZnfx=OT=!p8oXCrIXRr=4hpC#Xx-qy!` zmm6I{aj;=g^hx<8`DOie@pbutd_+Dgor#{2uSi$qviP<1wR|mgE&VV3n^bTrmzO%z zQ(I^lINF%fqEn-D#bVT9PubG}co9h3-fKtcR4V0*X2ht(dpym)Oj;r{5Z1 zEBQP~=kQ4@d}ES#<#J)*LnV~Aj@qkS*un6jJk4kLlW@!Mc{EhQbq%8U&_i^Y)?o!o z>&e01pU;!XQ6-bMHD4Si=WOadw&^eZMKZj+w9?#yu5Mp8RN1jGpG=!C%my7kra5Q- zm>O9-6ZXMAYmNEcy`}ThQxRQpPG4^Rq*8gck5e@&5cBr7ptok2JJxqOwYqPewfI*m zbUiC<6kZZ`3HyaZg8kmQMC#fhgo2cZ3i@d`6Mom<5`m40Fhsa1JU>i@F<_FlG8iyS z$C@=^L!XD89Q+#-(h`RU&APriGHP$K?AdZ>vRIlIN`~5dk@pWvrmnp6{yj6+?T>zM z!iXnr6uIPx`Kw6}w%>qRG;o>kUsnK7ma|I&7m`w3C)X=IDLp5Rv7D22a$K`d6eTCi zrhKv-l|?5-INT1=#lc8~8lwDSP>LorTYxD@N;F`&3~$hgB#aniXa)s;AS&4+F)@UAf4a2=FCTB}~5d3pNLzp`o(bfGG@aY(vC- z$gt>Pp+qY3TD46HZJX2k0@{VHpi#7YeNWGNC>ULpZ=_0PO4C|1rn#TmM-|oLn)!bd^xg17|h3AE<+)dBTz%|Lv z;nFJzVSx)X0?iPOJovTGNy>y6W5TSWP{DA>8R5K!i)#*re1@f>E>(ybk!G4TnhXma zH4f;M%lS$f04nEk=PlO*9u9sJ7#CPGykYR&1*^o{GCZBf>r*34vIPMy$O{Fl&YVd^ z5I`bjySvd+D$u5s&=(sYS0{!oG{_4U9zRtc5lDSPC*L{Ht~hnwwPf#|F?z+s-z_At zC{sii4RB8utcAa)TlcCUO)9lcC!~w1dTlZ=x%yke{ z&<0Jx12-7G1A(@85VRi-r;F)8b|2GWWf4?2*cZ_d5N3$*wYT9kqQ*%t3Av*w6!h{A zyMl8FLYjHx5JKj}`-kt|xNr>lQQ+A5iyxW)H2?O|C*&Tq+&qV}XfApJrOk8ZQ8ee& zp$|vR!{!I0@0~b}zC)fMtQ-e^N-wl}RFW7i%sCYqlgJN+vr*DlaLO6RH_#v^>pd#2 zhx-_J7QO`m|3UIh3=R$&rPz&#+wik*1!OaDhjX9PQjH5{b z*8|_fdq&_82Z0TA2fEiI=w9cd0}$fkx>{X4YIxtG?cLFiIRDCmof8K)PM^7kln{;l z=7sCZb3)&i$_aXBuP$UNH*KWRe`exwu5TMPvT7EqkL~}{QOf+qhCFNSS(mxfw#(v{ zL401wY2n{+W1i#u@!(kEyf*H+$X^VOC&o1w5H5Mye>F)jU<#tBNFnCGbeDZ>6XzYr z?|FRo-T7ks?DN~*`SpBvem}qE#Q~?e07)qhFv#tG8YO8Eii18W$ta(pUofhqiAye^Y=_-{r`RctgA` ze&L%H30!oEu1LZsi$))L*8i3t+E%n6FqWFAY?Gw~L(maXM10{#Q$h6kBpT3aWR3J4 zX;eBd;gZzCvI3zxVW=f0#6{BQ7b8Ak49kWAZij|=Q1^JsEXNs}?(tOeLFlbEsiDCU z2&5Y(&s46~My`6B9S}Xo&}aw z+(b$h^pha}ytdZ8%?;fX3LBFxW*iTy9jpOYRZ43W+NQyvX2XPb3uBhWm_T%J0*}rv z`hvOnX6q1`Z1V5PxuhZ4*}h(!1_v%-OqPL6XEAgAMrEXW+soXjp!@9~sd{I< ze08>(Y0M@>Eev|$n@t6|T zlVq!fOqk0c9;3{r6kf){_F%bLJE(e^O=%%#vZzROAV=~wYNV~Ty@}|0_Edo`DH?e} z;BJVicPi0DL=503t$jbd_{`DenU~M4&a8X?Y~PGk@so*kF8`rKw`p*b*(YRZEh(;hRuXAs~ya1Nq5G7jM;unEE-2tsIIf%1z;hJ}`NgniE~ zKo4ujOn@Zq789!i3|d-94%Nj4xDreQQnM0d+!3^IvX9%(*>O8Mx`3_pT!s|p&CJfu zR#17&&VJGu33|+IMjP~peOv<7yQuk8(1)qs&1KoJenm;}HE3$wP7FUt=cJt%_MlYu z#*eid*+nxT_0kA6AhMBu%Bs9mH7do;@=Q>g?=S) zO1R>`7o=oDrrU{jx|iss*YbnHpnouM#(yTzf>;@giJomJW{TErdcX{a1vwT8d#pIF z$gBf{lledNy{ z!OcT`sVDE;Uz=UC3)~uhqGw7C;~w?g*|~$1e^9c9(Pc*(+&bc@PMFA&mGco6wigc9G7w+NEdiMj#fh8 zIRX#TUX~KPG({l7)-Q|aC|k3IP9E363S6E%P628ftkNu@HNnv!)Pev*p3}V&%FH4g z@La7@t9U=DH0)9;p%$yVr4y}V{c6%W!ps=QRBvOGtBH` z98gS-+qk{fVZaN0?!~Sas6;3}I&^;&-*&Xm7(sVU&3#JjMR#2Um$jF(6ksq0IJpoO z0x@$3>Z3(-F~6bhd)%Nf%r{&Yi6gCB}*sd^_?h%UCnI?XG;?CxAsBS#U|yIPJ}myx<%IVTeQx zAVLF(&cIw3uhKM#-P$a)hkBsY?^j6%!~o_djqQOF_(g}mmA_TqAN=-geJ1$x^E=8L zk*y$QPdk;smvy-0-^)B!peVp>Z*vHV8U`;dR(Yn=ZGnnH+Wn8$#y_jSScOAh{tBE0 ze+IEJqtfi@F2qR_$ah_!MLX3g74z_r?GSrVKB64S9LXNZA#5{QAxq@l2A9djdiUjBC~*h+rBo=S91yw+IYn}@CP*f}+I0m3W(szdk_j@wcXx4*bx#BLf(>pn z4`gqMj?}gyxsjw}R-`RjF&(o2`g*bdvRyT>O`K==`@XyLo$vg6@#QZ5zc~JPYzN0l zoH&W|M@U09N=Jb~IuL^q7-N(P2wkW^6DbX4tdxzhQq=uug$*Iru8TS*p(!h4WMwS{ zsem@AMbTE(Qdb4kj@a3I=TKGCZ0Ebv#q!JPka@5zc zDeZz*OeiHJ&Sp&MiL+8eu&%EEEuyhvrL;lQ7!vTq?@6lk*yB&*!UxB8Zy;M%yP^(W zyz#G$C?s6{cxHKh|48{dKGgY@M@uiS>|6WxCs|2t^gU9p3mO-{h@Zs+H%91Ch%%ZJ z?y$pIp2p7hV6|9pH9~!f65d}vQkq`VU=$OauC*%JvSf)#(gK7i zq};vuQb)tmua(~(I#@a~(NmtC`1Ud~sr804ZVvm)IsBFgG=RBv8&J|FNqnVXHz34;{No8~f~jGL zmMd^u+pIEFD%I%HuHApA_)yg0A(CNK!+^hO`}c2j4>*HzG3s5p@<% zGT(v>X+p_@%w5JN)@0G1)PznlHD=36b@6t691WFnIsH+=wf$ZD9UKTIamBmKRe18T z@}E=>48DY8g0@_iB2G`x<0x-~rx~38H+6`54en7uU7*36J`6nw5jMQoKum#?{K?!I z{*3U7`J(YJ#;d8T={v?d>01pS8H;J_N-oh5lifat4`r-YY#msXY|1qxtUNL}I7cSP zCOeu0uA^P(V!Lu-n~>^?6&gmPso41#iA@qz;Cqy?tGBkB?k!9be4(cs3}|t-1O*?? z&j8e={9V`%xKMa&ZL52uVMV8@UJE_@v~_d_A|@HLBxnv9LShJ8przOj+KZ$**^6NR zA)6P6Ba{h+p!^mq)5&>}J?!v)9)YkPgW{Ff{{31G0^{{iP_;Nz*W47WL|T2IO5k@P z`{)2e5UM!hre;kt;BvTf$YL_MbMR>h=6jD0`*0sSdczZ3AgcSI2Flu~5 zE6(A2dI=P_-(>Tfydmti!QwNkA?&fbK-l71uv~z%3PdymSiukjR!R<}K?HdrfGUAg zOOleEee!jdI2+gQIrg{mCwI2r^tWQ;vyF$x_dVDD4E<@XvtTfkPN(C8pT7BS`JGh> zT5WB3u)hAC(3Xq$GEygYU zh%{2Y*Eq`WmG)K(5l&WE-X<$VR;pOu%$PG!>|5GyXChZ4R}vp2 zW)lXqE&1K#MDi!eU&Vi&d?Wt*#BV2Wv{Lv&ce)hBM*A7Fg(I-F>M#WDLuuG za(hi<_NT=$$Cxx{xo{Ik*@m+ex_`a2E=Zw6pP)42@dCDN?Y!s&YDTCX+ z!BfRPZ}_Q!7`n`YM*(y0v0dXaud&xh-L`4a`@9wQ4vV?j;sSq*3;J4m*x~S5AaTs% zLW~6F+9B$S>OzY5zjRl7Y!mk#zn9}@{60S}KihXc-<=)DKEGneulN$jiA`Q4JVFo# z!eeZdF(@M4s;TQkwB0JTDH7Ytq>gS=R?&4`yK2DH0_*xuCnf>nkEtt_x^-yklxosI z6NQ-le&-~xYLjfA?|1jl_xt_4zPQ(;F_R+ZvH|!Xhd-31L!AN~!>XRD)JMqX{5a}6 zToP6~j=~j(fSx0EU0Xh8IRE6=7Rr3&lb^ox$ImbPqQsizr;j-;TqzLm?OrSm z=-3zan{8U&`fqrA3Ljv1hEGQ|Iv6ze7~LUxUIlx8Md$YhA(ZX_?2>wG%lL1&i;UpP zBP<4tt42oE^r|&Htgl<=XEaeHm1AcWQp`GAvbd)jjx_F%z;G6w+ek)aDd?AkgY*}C z67V-;qsCu~6et8NB^Q39hKB-yk&#lxCg{r*f8QUD(=g_kS(@;(5g`#U8qJgwrt2H- z?4|A5r}tG*9L_d_2`VV})NQlu|Ir*GG-w=3PE7rUAbD+bw8GGb0)*5nw}*7L{w_G? z5=iXq)Zeqxuh`+Lw_OzR*Z|bM3`W56jvZUyxDL-owo7+u+qg>dw~{rU<6e5+h1%4c zV=lQCt-#G9;dOnP8=Amkq=j1GU!jQ+ml5;u@o2xRAa_p~cx>O&^>|5i>1{D!+YUL{ z@Sc6Y+u-d;#l>M7+S*&+(-1JbzNP7-@6lR$Pq8;NK(Vf8lZrH!79K;KAIlQ0SspFB z<|BBx1^uc9u%LfE#OwN;t`jL`QDj=P@~frm7&(*Wsy}!zuKzyu7$h5{l)5?G$z>(2 z-65+^hwix=ezG2xrsJr<7W?A~3NpR#lBoY@kF_s273#LZvk}NJStn@=0xwF$by&n% z402pTgRU%RnZ&TQDA;vv0zXVzt*BHg&Xl8!_^{1<_pg%vUU@r7-HSAK5iT28(z!Cf zqnnTlgmp4*AN89ZGj>&JIn)cvzpD|-)()#9t1w-ncNf6h5JqsR4?GFVEt&4E zxfUyqHy+fWJ_=Rj(cD#`+#J307aag>(`g)03{wtw%IGKqW>Xyl$E8Rp@xIJmQz3?= zUeWmUYE{Qz`2>VIMq}-b@gn!|?bt z5v1#C#2X*JCCavozD@X_W9Depm#CE0GT0`D4wDV!wVcZN?#n+$TXC)sECsyb+U;LA zBbeMDfoNd|g1}%_0O+n*TX`KFCr@oH667?nZ36t&n+w>(@3j@#K44U!cQ0~@XyFa@OIn2;X zHI;Y5qCZyg7Jfycf;X1I9-0sl@&m}GjO5Sla62?aFtq%&g>cf9TzSCh6=9Z?19#717s+1F~Y-*EuhQDE2`}K|5m!%ikKcOzaAyH?A4Q z3q;;G(ubacV9|Z!C)vZ#lV(U(#g}xjn>r>>Wxk|6sMj}kf%@|F_;oUzUR|H=3j9rY zm|m1phgS19zz%jY995}a}yN{Aan)T34(0Y)h=i-^8>Z{ z44~|FC`a7^O+2m>lRw+)BOg~ZsLF>w*-%{tu(u1^V=+2lijZg1MlbytX2S*6Zu}SlC@Q~k}+>gSAOw+N*JO|im zJ=QJj@&3X;o9ZBGD`JdcCP$c;OsdOrDU~Co?vB_;vB?uJ01_2O}W-K63zD91XABD3J8>Nkd20*rQg)CMrLhoDg!WlAl zl>`M-q5he9?EsmZX_Z>x6U5>K3??3Noa#y*jVCFRbd9B`Zb(oiR!fj_AI#F_R?+gS z1A*LxFrATyQJbtI$9q4)E})_cZ@kB?EU=pBV;R`OT?5!|W*y0!$!V)>i!Qlzi|dPI zWEl}hy2@ouB6l&q-7Z){^bdIT6)eqR579dgCl6h3N7FnTCl9}m?hZEZFr9riIOkRp zg0XSQGO|KHOhtZZOToWL&_+mDc*Vt6A<6Vfs1!~3Ra?qT(vF}(Ug66StBC}$5-kMl z)<~vV>!I)#amD%#C+t}2&9pH;JUimuI}aY-w70~hT19LVpOy;wrqymeSs9JK`L$6c zePz;&+<6ig_l5{6`0*giJ})4Cj4so*_xVm7A1$zeXobMXPun&?6#!mB|s-)X8eoExIbDyOe&rB*bZlm<65q1$(gZN>Apt!f8B1zsLFLrn+9v5`G9=f5Vfa+xp(PX88X@z zrxR$8eN*_*^d$aJew%$eC#z?G1~l@Nm*QCBG?5(A#|xQ3#HrM>V;9G zk%jGtc(60?*`>SZSPJtE!qr^Yu{z*~n3{yB2yq5p{Qy!X_H~KO4K5plCBqhz(%XNP*z*--FTy%Y_+XANrzWqgsc(;{6ZF>`Ud^f1b z>JSZoR%auC+p@LRREAs5heHKB`vz3#5BYpwA)X52j_}pW3T7^>J}jpXp_iKDXM>47 z&Chm=#Ch34j!2{$MBl1&i5SY0!=~G*N4_J4Y$X~Y9eDP8JU788jYnaS_d`J0dcp=@_)YU=d2kjuV7*xbrg$9Iu5GBK zbG0T{CV;T)U15phWMD=2_V{kRQ8PdEwEK01B_2ZY%-|h%$mru1E?;!v`+j(WPL$hNGycM|mM9VUrhHQvZ?t7QE6669Q^r6G!2VMVKqLK&WE$$w`P1# z>l%oydhkNMmw0k_E2L{%9ZX2HCk6Le;5dgx73GgiO`U+)$7N> zh0f>L(gN*@&O@tbA&g_~zDHCSmWcbx+487^#xw4l8N|`D&8W+%6-?adSa|DN9#A_HY~smC7DzSh zn=J?}o-Qe26|-oJs;D1DP1ej!9NlT_*gJw(VR*UPCE78Jb}pL%qGrxwcf|3-vGL-> z!mAWt;jV0zfl*{L42Hu2?0K?q6`INNQq40NQ|Fy2AwbR~F@8K-eBH4#d9HvD+g6X> zUR;Rug?djMiB;>s$-4{ktpRk{n3FqW$k~BZr((#Cz^iDNuQq6Oebx}qW)A@9#7qT% zmP!JA{BI1D9Hy^NR|fEBB3_K>)I9Q6Hlr~KudBPmRtgp?W_{we5v~PJrc_-dXw&;; zO`!5tY6eCEckaR>2AQOI%ohVu18hw^gmU4~X7Oip(@Xt%nF`=EAr+vf2ov3NHQu*H>@D@`X7><svQ`WWb*_la7wE4w&ZpAr=@Yz1({loM#t%$3B-Q=} zIrU3%v(}mS7y#Wj+$1;mR5bGf{VtVxm-Zvrw-dE8sn~r91o?b->_3Sl>H<=|*_s(k z!RHb5ZO&#SE0BX;BxH6Hb6`Ne%Ij(zMJA@Mxd z<+T(5hE<5t{*6n?K~JyIsVuLRqQizOTf%j1Ted8kPrR$2UF5U=4V8!=qfT{$g4XKW z&9t?&t+kg+17i~ttD2FI%q^Ur>^M(0U{^_~#`ME;K1i0lAK21k@|MDS(k`N`84jaSwwcM|(ujb2n%eUHGP9{xn zzNTGf*mulkXP>1c1tKkfd=FJk_Zu#ATtPmpk1p?!bqDR z@usV$@_NY_`3>?BHd-AS9>KY<9dG3qqqxV3#&X1B!69+jW0-u86Q<`P>=AvE@tYeu zlo}9n&9UW8zUma|FlQyphR`s$7pT%%tc7U=k5~ z-U-S0Ayh|eCQp!ebIts{_e9m^9K<$}VN}6TpLGrLqbxbJUkfCBWgFgH56k3QBJbGb z5T+xjN{TmZYYT$1XAgduYv155J#cYiMY8NeEMJwMrP=Ca3Uiw`Zlv%KYLQd@1J#6p z#elI~*uE&9s@K1%bmpK%*KnIKR7k^cPC<3oCWL&=vw=o59wvv=zfRVIZqkdVLDWr= zYL5%W7;ap!R#|zTmu!gq)F=%ii|?S}Q(VtOyeRtin+oR2d01*wqj_Vh!ppgmhM5mH zF{*HqHkznx68woMJH}8o^sV;xEEb$-6p!pSGajsIt97t)#xZSRV6?Wr8Z;csu-!Wz zdvFxa|AlMc38wypWAHu)pwG@9Ns3fPAZcSu_9SL2XinS0>O|~B;zWIfE|!-%UZfc6 zUBNsPfiFv)OwW@fuYg*C?rUO9EpX2xk-!bIfPR*QGiCWO-(Vb(;yM@6S@lHQkI!)a@q4P-+*6i#|+U<=%cO@rzFi#|#6!2Q4xl@=1=wLxLb{IbDQ zcqVDXbwh+SIodzXGg0QsD=8}Zq;#ftJkm7Kpllb%sTMW%SVVwI>LIK;M#&_OH}vWv zPf%s^`1%aj?}WvSO%&cAg%4&@t~jf44h^uo&B@pe(?O4cKKvWs7=0HibcjA3qS!=eoe&jo#tu#!MG4eKRj%1HgKg6HYf*miY z&OEnY4S5)m3@tDGh__(3FRql3)>D`w>1~SJIY{JSQO|B~Q;^q~@&%-vsqqXNKKcud z0=&#_0W3dE)xs5oG@OC`I5YtYKd?4Akyj|iXJPO`np`d;1vaK{OfN#C8U2&P-L8g|jX99^NmYZTvrQWvfWT@Np+nfkE-p^wevk0p!Rs!kdOp-8Usx-N1ldd5-St&KqZK29K?852>9nFs7RFm-)sEiET{7&!9uHe6D?(HaG53Fd!Bg0YZC%^(%3wl1yF#1=GD zagdXzM3b5>ePNi_jEZX?$MpwVdZKxw`(myn`a>AU>-LU7Je(B!eLGf8wZu}&!xVPgE}^ljEq0rk*>V(avL5+Oq$L4#^g_dJFs|F zy@hp0t8E2`cQkN2>S9ni6^#Td)a&8xpXR0Vx#!;R^zRh<7MuQsmTfZ)Gbnhh_j0Gx zb3f5uT?b4lLpq(`zXk6vLoQsOX48hcsVwTQ43U)|3e=bzD)lVOW^VeUmgl+>Q*Ww9zCe6}aVOR7KAO~772t(~^Y#K3%U z3(JLAMD}#w21+u{oWwhXp_ENkGbwl7cg+ZQyKpHip*&^pt42$0t9&Lin&T)C>}M<$>NTb(DT3lV?UnV&dE{(c zLK2>bx5e_P#pl{^w}ccdyX*L!yUu=rTy%TDZ&eA1@y}qTdx~_t+7SIdT5#f38c4f=gQ+&VUzbxbtnXv6q_<;2#`JP3vd|)# zTOoYz$p;Ie)M8k=V{$8X1^$N=^!^x@EPs*AYKqx5sVpfupYxc)yV2^Oa6xsOyor~$ zOFM3X2mQeZqBP?so0L$M>kl=^Y#_LZO*;lvdrv!F!P8uJ5x#TsVn$jQbud_Wz}6IX zK&&l>O^QxRPY@ba2Utac2t-b0r7$dCgxKhqMe<(P6wVQ5CA{!m%EA=pMQ-@zFL8Fd z=&U|5$YQ3UBwQ5hBf#opK3kfL3HX6XKYkR zirqn(eeRdHU(vysMcJutLx%$;DnEsw_K$NmNx5FVbo)da%nPjnLzZDweNj{A7qG~& zen79{(9WG18MypRqOk5!RaNH9O%DEGVNysvQqY5D%nt8BCCLVx@=s3@Qz^K-%I-x(H6|{~wxdu2CgI4ZQ(k>51|ot0R~$Wn z<{-TENf$-w4t-wH@$0nw=0~oo@;cn)+FMt5zCy_y9uuWpA@1<8xqo=dNjfONYXe$s z1gm{yfi)hd>P1{Oq-!gNGkcPEuE9boNFHkiJf7Okgmp!%%69DhIAn#5 z8wZ6st1P7lodtXC13GVJp!ynkzRgcJV&B^F1e3z)!2RSKmnialMsJ$n2cMQ9>daDC zE323SPCO}%BI`Hr{j447{_H+d@reT(Gts}DGQ9j9@xjfRhT&~{|n7@-m(6;Y;A&V-k z{Wxr7k$ij$JTv?E-peH8YxbCIDld*;rqFV$6a$!D6Wl~E0*#)s=g zaq}|@xem$xQza~S9X&_vBbkrRza_<$&UOCQ#Qs0ZZA9u?=5#D97MFU$CLVaounMm9KMpcAXCLcnv=% z|IH)VcXaGXD;fRqd?tS<{6X5EC3t$$)Z?bSohT<6Hv8j^UD$h0%dY;JX=Q=vn*6GO zjXpD5#&>aNjuv=(R)g)AS>;(7KGuJYkXpV8$GTkXquQPDP+7#aHk52|fpx|yToc*x z+J*^(<191=LYdrxC-pNz_X$!HxmkylL9sUqx^SvwaaGve3{s;Q&04FasBD+7&=V89 zjJ)`q=HAWOxij?`nK#n-d69K2r#oXuTz$Cx4jlklOI4qPnQ^prTCOb$AM_jfk!e46 zg;SLU8KBrZ#{a1MhV8bgiQx@p^`;Uw@rEaT@@jxCm?-fwRAzArbpmH*5@JxDg1qW} zFm09fj+HulLY?Y+MJk^(cB$jR87_&s60VB!`V-y5qV~2FdhtIC@$f%nb+7Ew1t02? zGoJr6?@HnS09!^wVI4Jjtg%{ptnD_}4paE60HgVt_(9_Pf3Qg2)Uv!O1b(7+JFvcy zy+OV~^nl;;UU#s8zp<~>IJtn~c-a0t)8UjZ*~0l*56vs3AIDefUv(WC9CETG%*j}UC|(;dZ7y~ft+u&!(P=|$q9TmqBU&3H8?$2zGV7L(s=-Q$Qx;s8EN z@ak8Ljs(%{)cdxYox#yhYu2uZ7NAxG`yg7;M`yovf>yQ!EU)d9O(RF!mm1YK@S-3B`dRU?pU|+T1G5_K}V0}No&6)siN;L>bmjjyityb z$Jv#)tg|(S@mcMmXL__OuSI>1G+J@X@#x?njnX0!?o7w@kcHo>LvSEXAr9mI;FkrY_YxU+9RB$ ziq{1O2QL&;+S=6?+oDz&%nnNcc$R=vIrTD40Nso4oHvKLXpg;A5qGYcXtw_RxOt04 z(}e9fXSNbp$6F3#jIvBSh_@3TC)XIvL?mCU#M356Peg2zn3qNo{;a!&dkX3Jqdg=i zomh{#q_3Pt%WJ9Tr^IVEb3L%dAJ`QjeC{B6ZTGSsvpiU`C)mVwO}MpXs`R%k)|66b zve2f$r%Xk#-rvf<*~i(Tf&Soqi`)OTY<@VoO1&oAYCzQH*jb;n@M_3ack*aGHNndj z@j&%l{uKH*8Ete?Z1h_EUW(%!<_A8dlGK_;>!TwNOuc?aR>xj5evFq=TToTP(s4|E z^!Jlp>@H28;fOSRA+d)ndc$%@%cGJz+8$%baomWhXLQs6d4`Ew_9rcPkxP(zr>)PTXi1}-6HKQ}ELzG~dvod)6zjk<0k^~4c6l}Q9< zh&(0ttxs^<-yBWR$xN{0(w@et9StYiH#wa&+9j&@a5v-wTm<%m^ZBN#H;FsAlr3t6 znd?!Tlt;-7-gcs6kNEFVo|c37)|so(3(`E34HD-ni6-(I$uh8_bcb+rz6bwOC9Klp zLnjh*sed{oZrNZF?eWVP$4zI{x!n5RBC|-xjoW527MzppR@kk0UmcS(rZGjiKK;?E zyP|x#V(-zrzW#12201@s{v*1|uvCpfmxC z4FPb5NyBn;tEw0XFCsjcSbhWs2jc4p`_l@3l)X}i6wt(t<}8=r zQG%QFK|0MpP4R!?3h8SJ1MyyCCFwPikmBsy`rhVJ=gEzCW~3U{k~VSOZAv|nENOOW z7xnfH6!jGK1=Dq{(CQDC7S5t(+_Y;XCN|$YY0Og`qU+Z^>eK$3A3b_l_Ogx5E2>dT z|EI71Z1CYb(hv7Vx&)cv5tLq&#@bA(tTRnLaUGQ zoUe^K8)IzxJ5R2(iSr0A7}!EHu!)}#tZR6V@1h<$6io{nb)+SMwcw|SI$0-Q*d!*G+!~WhR9h9t$jNGrQIW}l{{E#=Iy-hs zNqkevnV(QdEHdr7vQONWiCN*z6H12n;NZyuh440$Zkr-Y7@9+t5YX<+7q}Rnw%rV7x9u>A*?pX$>W0Qpzc* zT}iF*j%8*i6L!ZwC;$DckFOl&@6K~l+k}^alF{U+Lwypc#xA6?XtCMCLu1z1y!D5q z8g~LLR#ee>o#KlKb@I-c{qK&-;S$JKE@+9%9=xwIlKdW(#`#cR`HWqW zu3)v{DCZxAlc-t2OuLYtkhYqb zUdM-jGmUU2HQwxGja(Fds>5YvqLSF6VquA9P$9iY#)I|z#ojkYO|(nS^*ex8tiDvB|DAhH<< zDGy1i$6!57-HM`Mi)B56)2eX3tJR9NU<*HPM_YWrGa2j73f*AGi`$DmnbiJ?FAdY@ z$<;5%6%NG}JW}`TlcCGbM*eNwHKQ70;2NN-?3Lh(IH`rIxZ_&h1+5&4Pz`xq@u>Ea%XZr5V zDa~JW5uuC!7imnrY>cyP1am>3{Iuo^i!mY+X<|S^7bm@%2$irJIc7PH+^kEnuo{16 zIRsF->4mqk%Bt6rxUZGg+kysy;toM|y4=g;hY-~Aq4&l0oTHfGX?9q0PJT|9Z+Cn_q zV(@MSy$567AYUF7aCc2UGj(nyy@w=l4+*@d2DthQK7)7eIdKnOxTf)3{dw<6zbAEG zDP`|jv-fYfrg&dTD-L;$pa`T0g#w`(YD4J|Xb6Q0LlNillS`@3L)!^O7Ln@myG`*i z!`bG<1|xac%;U0sL z$ZZKw%)~;lykY_28TNwU)lQ*#>*E469|e72dih}bbz%DCUd&MIAO(XkMBL`fi zdWEF=4W;^iqvej1y;76?mXm#&lLMYqy#iGI`c!@LR0HBm}!!&!^eaf`Y=%()uHy&B=Q8UxH0>S6~Ga)-M;0{&|&uYd6N z(4!APWcCd_{S{u3bM}~~cd4fLxOhg;UJ-TnfN1tDJOd4F*k<2+-9-~L40 z>7klH^7O}hCdSkuOFq~o>!~FhxGC!jEAJzD#wWcYSJYBe)Nxqcn(+*zdnPxjX3W&V zUDV@VGyvwDKwFNYt%cIo(d!r^iPVu7tVPmWPOA>nXH&1nLs||Ak<>A7(4|?eMPIG? z;jD#atp#bVMQyG97e)>4xj2E`kv_>xswBfJl^>q*tSCn9Qj)hJZ9wzczz1%Z4{l!< zZch&Gz&3hUEPCHGdQUa_z*S~fNM_$qW=~P(z;Sw4YI@&tdQWruz>{WIfM#ExW>22x zz`lA{qI%!FdXLgG$>I%D=j7L*z^o5RtdCKxk660DLf7whd-W__(1BmRsfp@>cXJXDB)66Z^%pDi+3EF48&K(f#y@mIH z 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벧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