├── .gitignore ├── LICENSE ├── assets └── CompoSnake.gif ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src └── main └── kotlin └── com └── arkivanov └── compose3d ├── Camera.kt ├── Main.kt ├── Math.kt ├── MouseObserver.kt ├── Orientation.kt ├── Triangle.kt ├── Vector.kt ├── World.kt ├── draw ├── Canvas3D.kt ├── Draw3DScope.kt └── Draw3DScopeImpl.kt └── entities ├── Entity.kt ├── Parallelepiped.kt ├── Rectangle.kt ├── Sphere.kt └── SquarePyramid.kt /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | .DS_Store 12 | /node_modules 13 | /npm-debug.log* 14 | __pycache__ 15 | venv 16 | /MANIFEST 17 | /manifest.json 18 | /site 19 | /dist 20 | /mkdocs_material.egg-info 21 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Arkadii Ivanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/CompoSnake.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arkivanov/compose-3d/bbfe8b117b078c65fdd1b7a7505b1b5ebbda4c7d/assets/CompoSnake.gif -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.compose 2 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 3 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 4 | 5 | plugins { 6 | kotlin("jvm") version "1.4.31" 7 | id("org.jetbrains.compose") version "0.3.2" 8 | } 9 | 10 | group = "me.aivanov" 11 | version = "1.0" 12 | 13 | repositories { 14 | jcenter() 15 | mavenCentral() 16 | maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } 17 | } 18 | 19 | dependencies { 20 | implementation(compose.desktop.currentOs) 21 | } 22 | 23 | tasks.withType() { 24 | kotlinOptions.jvmTarget = "11" 25 | } 26 | 27 | compose.desktop { 28 | application { 29 | mainClass = "com.arkivanov.compose3d.MainKt" 30 | jvmArgs.add("-Dsun.java2d.uiScale=2.0") 31 | nativeDistributions { 32 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 33 | packageName = "Compose 3D" 34 | windows.msiPackageVersion = "1.0.0" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arkivanov/compose-3d/bbfe8b117b078c65fdd1b7a7505b1b5ebbda4c7d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | maven { url = uri("https://maven.pkg.jetbrains.space/public/p/compose/dev") } 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/Camera.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d 2 | 3 | data class Camera( 4 | val position: Vector, 5 | val orientation: Orientation, 6 | val surfacePosition: Vector 7 | ) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/Main.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d 2 | 3 | import androidx.compose.desktop.LocalAppWindow 4 | import androidx.compose.desktop.Window 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.DisposableEffect 10 | import androidx.compose.runtime.LaunchedEffect 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.setValue 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.focus.FocusRequester 17 | import androidx.compose.ui.focus.focusModifier 18 | import androidx.compose.ui.focus.focusRequester 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.input.key.Key 21 | import androidx.compose.ui.input.key.NativeKeyEvent 22 | import androidx.compose.ui.input.key.key 23 | import androidx.compose.ui.input.key.onKeyEvent 24 | import androidx.compose.ui.unit.IntOffset 25 | import androidx.compose.ui.unit.IntSize 26 | import com.arkivanov.compose3d.draw.Canvas3D 27 | import com.arkivanov.compose3d.entities.Entity 28 | import com.arkivanov.compose3d.entities.Parallelepiped 29 | import com.arkivanov.compose3d.entities.Sphere 30 | import com.arkivanov.compose3d.entities.SquarePyramid 31 | import kotlinx.coroutines.delay 32 | import kotlinx.coroutines.isActive 33 | import java.awt.event.MouseEvent 34 | import java.awt.event.MouseMotionListener 35 | import java.util.Comparator 36 | import java.util.PriorityQueue 37 | import java.util.Queue 38 | import kotlin.math.tan 39 | 40 | fun main() { 41 | val focalLength = getFocalLength(size = 2000.0, fov = degToRad(90.0)) 42 | 43 | val initialCameraPosition = Vector(y = 700.0, z = 550.0) 44 | val initialCameraOrientation = Orientation(x = degToRad(-35.0)) 45 | 46 | val cube = 47 | Parallelepiped( 48 | position = Vector(x = -100.0, z = -100.0), 49 | length = Vector(z = 200.0), 50 | width = Vector(x = 200.0), 51 | height = Vector(y = 100.0), 52 | color = Color.Magenta 53 | ) 54 | 55 | val pyramid = 56 | SquarePyramid( 57 | position = Vector(x = -100.0, y = 100.0, z = -100.0), 58 | length = Vector(z = 200.0), 59 | width = Vector(x = 200.0), 60 | height = Vector(y = 100.0), 61 | color = Color.Magenta 62 | ) 63 | 64 | val sphere1 = 65 | Sphere( 66 | position = Vector(), 67 | radius = 200.0, 68 | color = Color.Magenta, 69 | n = 3 70 | ) 71 | 72 | val sphere2 = 73 | Sphere( 74 | position = Vector(y = 300.0), 75 | radius = 100.0, 76 | color = Color.Magenta, 77 | n = 2 78 | ) 79 | 80 | val sphere3 = 81 | Sphere( 82 | position = Vector(y = 450.0), 83 | radius = 50.0, 84 | color = Color.Magenta, 85 | n = 1 86 | ) 87 | 88 | val nose = 89 | SquarePyramid( 90 | position = Vector(y = 450.0, z = 50.0), 91 | length = Vector(x = 5.0), 92 | width = Vector(y = 5.0), 93 | height = Vector(z = 30.0), 94 | color = Color.Red 95 | ) 96 | 97 | val hand1 = 98 | Parallelepiped( 99 | position = Vector(x = 75.0, y = 360.0), 100 | length = Vector(x = 5.0), 101 | width = Vector(z = 5.0), 102 | height = Vector(y = -200.0), 103 | color = Color.DarkGray 104 | ).run { 105 | copy( 106 | length = length.rotate(Vector(z = 1.0), degToRad(35.0)), 107 | width = width.rotate(Vector(z = 1.0), degToRad(35.0)), 108 | height = height.rotate(Vector(z = 1.0), degToRad(35.0)), 109 | ) 110 | } 111 | 112 | val hand2 = 113 | Parallelepiped( 114 | position = Vector(x = -80.0, y = 360.0), 115 | length = Vector(x = 5.0), 116 | width = Vector(z = 5.0), 117 | height = Vector(y = -200.0), 118 | color = Color.DarkGray 119 | ).run { 120 | copy( 121 | length = length.rotate(Vector(z = 1.0), degToRad(-35.0)), 122 | width = width.rotate(Vector(z = 1.0), degToRad(-35.0)), 123 | height = height.rotate(Vector(z = 1.0), degToRad(-35.0)), 124 | ) 125 | } 126 | 127 | // val hand1 = 128 | // SquarePyramid( 129 | // position = Vector(), 130 | // length = Vector(x = 10.0), 131 | // width = Vector(z = 10.0), 132 | // height = Vector(y = -400.0), 133 | // color = Color.DarkGray 134 | // ) 135 | 136 | Window( 137 | size = IntSize(width = 1000, height = 1000), 138 | title = "Compose-3D" 139 | ) { 140 | var camera by remember { 141 | mutableStateOf( 142 | Camera( 143 | position = initialCameraPosition, 144 | orientation = initialCameraOrientation, 145 | surfacePosition = Vector(z = 1.0) * focalLength 146 | ) 147 | ) 148 | } 149 | 150 | fun move(block: Camera.() -> Camera) { 151 | camera = camera.block() 152 | } 153 | 154 | val w = LocalAppWindow.current 155 | 156 | val obs = remember { 157 | MouseObserver(w.window) 158 | } 159 | DisposableEffect(Unit) { 160 | obs.addMouseMotionListener(object : MouseMotionListener { 161 | private var lastPos: IntOffset? = null 162 | 163 | override fun mouseDragged(e: MouseEvent?) { 164 | } 165 | 166 | override fun mouseMoved(e: MouseEvent) { 167 | val pos = lastPos 168 | lastPos = IntOffset(e.xOnScreen, e.yOnScreen) 169 | if (!w.window.isFocused) { 170 | return 171 | } 172 | 173 | if (pos != null) { 174 | val dx = e.xOnScreen - pos.x 175 | val dy = e.yOnScreen - pos.y 176 | move { 177 | copy( 178 | orientation = Orientation( 179 | x = orientation.x - degToRad(dy.toDouble()), 180 | y = orientation.y + degToRad(dx.toDouble()), 181 | ) 182 | ) 183 | } 184 | } 185 | } 186 | }) 187 | obs.start() 188 | onDispose { obs.stop() } 189 | } 190 | 191 | val o = Vector(x = 0.0, y = 0.0, z = 0.0) 192 | val ox1 = Vector(x = 400.0, y = 0.0, z = 0.0) 193 | val ox2 = -ox1 194 | val oy1 = Vector(x = 0.0, y = 600.0, z = 0.0) 195 | val oy2 = -oy1 196 | val oz1 = Vector(x = 0.0, y = 0.0, z = 400.0) 197 | val oz2 = -oz1 198 | 199 | var deg = 0.0 200 | 201 | // LaunchedEffect(Unit) { 202 | // while (isActive) { 203 | // delay(32) 204 | // deg += degToRad(2.0) 205 | // if (deg >= 2 * PI) { 206 | // deg -= 2 * PI 207 | // } 208 | // 209 | // camera = camera.copy( 210 | // position = initialCameraPosition.rotate(Vector(y = 1.0), deg), 211 | // orientation = initialCameraOrientation.copy(y = deg) 212 | // ) 213 | // } 214 | // } 215 | 216 | MaterialTheme { 217 | 218 | 219 | KeyListener( 220 | onForward = { 221 | move { copy(position = position - UNIT_O_Z.rotate(orientation).copy(y = 0.0) * 10.0) } 222 | }, 223 | onBackward = { 224 | move { copy(position = position + UNIT_O_Z.rotate(orientation).copy(y = 0.0) * 10.0) } 225 | }, 226 | onLeft = { 227 | move { copy(position = position + UNIT_O_X.rotate(orientation).copy(y = 0.0) * 10.0) } 228 | }, 229 | onRight = { 230 | move { copy(position = position - UNIT_O_X.rotate(orientation).copy(y = 0.0) * 10.0) } 231 | }, 232 | onUp = { 233 | move { copy(position = position.offset(y = 10.0)) } 234 | }, 235 | onDown = { 236 | move { copy(position = position.offset(y = -10.0)) } 237 | }, 238 | ) 239 | 240 | val triangles = 241 | sortTriangles( 242 | cameraPosition = camera.position, 243 | sphere1, 244 | sphere2, 245 | sphere3, 246 | nose, 247 | hand1, 248 | hand2 249 | ) 250 | 251 | Canvas3D(camera = camera, modifier = Modifier.fillMaxSize()) { 252 | drawLine(start = o, end = ox1, color = Color.Red) 253 | drawLine(start = o, end = ox2, color = Color.Red) 254 | drawLine(start = o, end = oy1, color = Color.Green) 255 | drawLine(start = o, end = oy2, color = Color.Green) 256 | drawLine(start = o, end = oz1, color = Color.Blue) 257 | drawLine(start = o, end = oz2, color = Color.Blue) 258 | // cube.triangles(::drawTriangle) 259 | // pyramid.triangles(::drawTriangle) 260 | // sphere1.triangles(::drawTriangle) 261 | // sphere2.triangles(::drawTriangle) 262 | // sphere3.triangles(::drawTriangle) 263 | // nose.triangles(::drawTriangle) 264 | // hand1.triangles(::drawTriangle) 265 | // hand2.triangles(::drawTriangle) 266 | 267 | while (triangles.isNotEmpty()) { 268 | drawTriangle(triangles.poll()) 269 | } 270 | } 271 | } 272 | } 273 | } 274 | 275 | private fun sortTriangles(cameraPosition: Vector, vararg entities: Entity): Queue { 276 | val queue = 277 | PriorityQueue( 278 | Comparator.comparingDouble { 279 | distanceBetween(it.a, cameraPosition) 280 | }.reversed() 281 | ) 282 | 283 | entities.forEach { entity -> 284 | entity.triangles { 285 | queue.offer(it) 286 | } 287 | } 288 | 289 | return queue 290 | } 291 | 292 | @Composable 293 | private fun KeyListener( 294 | onForward: () -> Unit, 295 | onBackward: () -> Unit, 296 | onLeft: () -> Unit, 297 | onRight: () -> Unit, 298 | onUp: () -> Unit, 299 | onDown: () -> Unit, 300 | ) { 301 | val focusRequester = remember { FocusRequester() } 302 | val pressedKeys = remember { HashSet() } 303 | 304 | LaunchedEffect(Unit) { 305 | focusRequester.requestFocus() 306 | while (isActive) { 307 | if (Key.W in pressedKeys) { 308 | onForward() 309 | } 310 | 311 | if (Key.S in pressedKeys) { 312 | onBackward() 313 | } 314 | 315 | if (Key.A in pressedKeys) { 316 | onLeft() 317 | } 318 | 319 | if (Key.D in pressedKeys) { 320 | onRight() 321 | } 322 | 323 | if (Key.Spacebar in pressedKeys) { 324 | onUp() 325 | } 326 | 327 | if (Key.CtrlLeft in pressedKeys) { 328 | onDown() 329 | } 330 | 331 | delay(20) 332 | } 333 | } 334 | 335 | Box( 336 | modifier = Modifier 337 | .fillMaxSize() 338 | .focusRequester(focusRequester) 339 | .focusModifier() 340 | .onKeyEvent { 341 | when (it.nativeKeyEvent.id) { 342 | NativeKeyEvent.KEY_PRESSED -> pressedKeys += it.key 343 | NativeKeyEvent.KEY_RELEASED -> pressedKeys -= it.key 344 | } 345 | true 346 | } 347 | ) 348 | } 349 | 350 | private fun getFocalLength(size: Double, fov: Double): Double = 351 | size / (2 * tan(fov / 2)) 352 | 353 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/Math.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d 2 | 3 | import kotlin.math.PI 4 | import kotlin.math.acos 5 | import kotlin.math.cos 6 | import kotlin.math.sin 7 | import kotlin.math.sqrt 8 | 9 | val UNIT_O_X = Vector(x = 1.0) 10 | val UNIT_O_Y = Vector(y = 1.0) 11 | val UNIT_O_Z = Vector(z = 1.0) 12 | 13 | fun Vector.length(): Double = sqrt(x * x + y * y + z * z) 14 | 15 | fun Vector.normalize(): Vector { 16 | val len = length().takeUnless { it == 0.0 } ?: return this 17 | 18 | return copy(x = x / len, y = y / len, z = z / len) 19 | } 20 | 21 | fun Vector.offset(x: Double = 0.0, y: Double = 0.0, z: Double = 0.0): Vector = 22 | copy(x = this.x + x, y = this.y + y, z = this.z + z) 23 | 24 | operator fun Vector.plus(other: Vector): Vector = 25 | offset(x = other.x, y = other.y, z = other.z) 26 | 27 | operator fun Vector.minus(other: Vector): Vector = 28 | offset(x = -other.x, y = -other.y, z = -other.z) 29 | 30 | operator fun Vector.unaryMinus(): Vector = 31 | Vector(x = -x, y = -y, z = -z) 32 | 33 | operator fun Vector.times(other: Double): Vector = 34 | Vector(x = x * other, y = y * other, z = z * other) 35 | 36 | infix fun Vector.dotProduct(other: Vector): Double = 37 | x * other.x + y * other.y + z * other.z 38 | 39 | fun Vector.rotate(origin: Vector, angle: Double): Vector { 40 | val c = cos(angle) 41 | val s = sin(angle) 42 | 43 | return multiplyByMatrix( 44 | v11 = c + (1 - c) * origin.x * origin.x, 45 | v12 = (1 - c) * origin.x * origin.y - s * origin.z, 46 | v13 = (1 - c) * origin.x * origin.z + s * origin.y, 47 | v21 = (1 - c) * origin.y * origin.x + s * origin.z, 48 | v22 = c + (1 - c) * origin.y * origin.y, 49 | v23 = (1 - c) * origin.y * origin.z - s * origin.x, 50 | v31 = (1 - c) * origin.z * origin.x - s * origin.y, 51 | v32 = (1 - c) * origin.z * origin.y + s * origin.x, 52 | v33 = c + (1 - c) * origin.z * origin.z 53 | ) 54 | } 55 | 56 | fun Vector.rotate(orientation: Orientation): Vector = 57 | rotate(UNIT_O_X, orientation.x) 58 | .rotate(UNIT_O_Y, orientation.y) 59 | .rotate(UNIT_O_Z, orientation.z) 60 | 61 | fun angleBetween(a: Vector, b: Vector): Double = 62 | acos((a dotProduct b) / (a.length() * b.length())) 63 | 64 | fun distanceBetween(a: Vector, b: Vector): Double = 65 | sqrt(sqr(a.x - b.x) + sqr(a.y - b.y) + sqr(a.z - b.z)) 66 | 67 | fun Vector.multiplyByMatrix( 68 | v11: Double, 69 | v12: Double, 70 | v13: Double, 71 | v21: Double, 72 | v22: Double, 73 | v23: Double, 74 | v31: Double, 75 | v32: Double, 76 | v33: Double, 77 | ): Vector = 78 | Vector( 79 | x = v11 * x + v12 * y + v13 * z, 80 | y = v21 * x + v22 * y + v23 * z, 81 | z = v31 * x + v32 * y + v33 * z 82 | ) 83 | 84 | operator fun Triangle.plus(vector: Vector): Triangle = 85 | copy(a = a + vector, b = b + vector, c = c + vector) 86 | 87 | fun degToRad(deg: Double): Double = deg * PI / 180.0 88 | 89 | fun Double.fixZero(): Double = if (this == 0.0) 0.001 else this 90 | 91 | fun sqr(value: Double): Double = value * value 92 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/MouseObserver.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d 2 | 3 | import java.awt.event.MouseMotionListener 4 | import javax.swing.SwingUtilities 5 | import java.lang.Runnable 6 | import kotlin.jvm.JvmStatic 7 | import javax.swing.JFrame 8 | import com.arkivanov.compose3d.MouseObserver 9 | import java.awt.Component 10 | import java.lang.IllegalArgumentException 11 | import java.awt.event.ActionListener 12 | import java.awt.MouseInfo 13 | import java.awt.Point 14 | import kotlin.jvm.Synchronized 15 | import java.awt.event.ActionEvent 16 | import java.awt.event.MouseEvent 17 | import java.util.HashSet 18 | import javax.swing.Timer 19 | 20 | class MouseObserver constructor(component: Component?) { 21 | val component: Component 22 | private val timer: Timer 23 | private val mouseMotionListeners: MutableSet 24 | fun start() { 25 | timer.start() 26 | } 27 | 28 | fun stop() { 29 | timer.stop() 30 | } 31 | 32 | fun addMouseMotionListener(listener: MouseMotionListener) { 33 | synchronized(mouseMotionListeners) { mouseMotionListeners.add(listener) } 34 | } 35 | 36 | fun removeMouseMotionListener(listener: MouseMotionListener) { 37 | synchronized(mouseMotionListeners) { mouseMotionListeners.remove(listener) } 38 | } 39 | 40 | protected fun fireMouseMotionEvent(point: Point) { 41 | synchronized(mouseMotionListeners) { 42 | for (listener: MouseMotionListener in mouseMotionListeners) { 43 | val event = MouseEvent( 44 | component, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 45 | 0, point.x, point.y, 0, false 46 | ) 47 | SwingUtilities.invokeLater(Runnable { listener.mouseMoved(event) }) 48 | } 49 | } 50 | } 51 | 52 | companion object { 53 | /* the resolution of the mouse motion */ 54 | private const val DELAY = 10 55 | 56 | /* Testing the ovserver */ 57 | @JvmStatic 58 | fun main(args: Array) { 59 | val main = JFrame("dummy...") 60 | main.setSize(100, 100) 61 | main.isVisible = true 62 | val mo = MouseObserver(main) 63 | mo.addMouseMotionListener(object : MouseMotionListener { 64 | override fun mouseMoved(e: MouseEvent) { 65 | println("mouse moved: " + e.point) 66 | } 67 | 68 | override fun mouseDragged(e: MouseEvent) { 69 | println("mouse dragged: " + e.point) 70 | } 71 | }) 72 | mo.start() 73 | } 74 | } 75 | 76 | init { 77 | requireNotNull(component) { "Null component not allowed." } 78 | this.component = component 79 | 80 | /* poll mouse coordinates at the given rate */timer = Timer(DELAY, object : ActionListener { 81 | private var lastPoint = MouseInfo.getPointerInfo().location 82 | 83 | /* called every DELAY milliseconds to fetch the 84 | * current mouse coordinates */ 85 | @Synchronized 86 | override fun actionPerformed(e: ActionEvent) { 87 | val point = MouseInfo.getPointerInfo().location 88 | if (point != lastPoint) { 89 | fireMouseMotionEvent(point) 90 | } 91 | lastPoint = point 92 | } 93 | }) 94 | mouseMotionListeners = HashSet() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/Orientation.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d 2 | 3 | data class Orientation( 4 | val x: Double = 0.0, 5 | val y: Double = 0.0, 6 | val z: Double = 0.0 7 | ) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/Triangle.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | data class Triangle( 6 | val a: Vector, 7 | val b: Vector, 8 | val c: Vector, 9 | val color: Color 10 | ) 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/Vector.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d 2 | 3 | data class Vector( 4 | val x: Double = 0.0, 5 | val y: Double = 0.0, 6 | val z: Double = 0.0 7 | ) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/World.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d 2 | 3 | data class World( 4 | val camera: Camera, 5 | val triangles: List 6 | ) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/draw/Canvas3D.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d.draw 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import com.arkivanov.compose3d.Camera 7 | 8 | @Composable 9 | fun Canvas3D(camera: Camera, modifier: Modifier, onDraw: Draw3DScope.() -> Unit) { 10 | Canvas(modifier) { 11 | Draw3DScopeImpl( 12 | camera = camera, 13 | drawScope = this 14 | ).onDraw() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/draw/Draw3DScope.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d.draw 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.arkivanov.compose3d.Triangle 5 | import com.arkivanov.compose3d.Vector 6 | 7 | interface Draw3DScope { 8 | 9 | fun drawLine(start: Vector, end: Vector, color: Color) 10 | 11 | fun drawTriangle(triangle: Triangle) 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/draw/Draw3DScopeImpl.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d.draw 2 | 3 | import androidx.compose.ui.geometry.Offset 4 | import androidx.compose.ui.graphics.Color 5 | import androidx.compose.ui.graphics.Path 6 | import androidx.compose.ui.graphics.drawscope.DrawScope 7 | import com.arkivanov.compose3d.Camera 8 | import com.arkivanov.compose3d.Triangle 9 | import com.arkivanov.compose3d.Vector 10 | import com.arkivanov.compose3d.distanceBetween 11 | import com.arkivanov.compose3d.fixZero 12 | import kotlin.math.cos 13 | import kotlin.math.min 14 | import kotlin.math.sin 15 | 16 | internal class Draw3DScopeImpl( 17 | private val camera: Camera, 18 | drawScope: DrawScope 19 | ) : Draw3DScope, DrawScope by drawScope { 20 | 21 | override fun drawLine(start: Vector, end: Vector, color: Color) { 22 | drawLine( 23 | start = start.project(), 24 | end = end.project(), 25 | color = color 26 | ) 27 | } 28 | 29 | override fun drawTriangle(triangle: Triangle) { 30 | val a = triangle.a.project() 31 | val b = triangle.b.project() 32 | val c = triangle.c.project() 33 | val f = lightFactor(triangle.a) 34 | 35 | val red = triangle.color.red + (1F - triangle.color.red) * f 36 | val green = triangle.color.green + (1F - triangle.color.green) * f 37 | val blue = triangle.color.blue + (1F - triangle.color.blue) * f 38 | 39 | drawPath( 40 | path = Path().apply { 41 | moveTo(a.x, a.y) 42 | lineTo(b.x, b.y) 43 | lineTo(c.x, c.y) 44 | lineTo(a.x, a.y) 45 | }, 46 | color = Color(red = red, green = green, blue = blue), 47 | ) 48 | } 49 | 50 | private fun lightFactor(point: Vector): Float = 51 | min(distanceBetween(camera.position, point).toFloat(), 2000F) / 2000F 52 | 53 | // Source: https://en.wikipedia.org/wiki/3D_projection#Mathematical_formula 54 | private fun Vector.project(): Offset { 55 | val x = x - camera.position.x 56 | val y = y - camera.position.y 57 | val z = z - camera.position.z 58 | 59 | val cx = cos(camera.orientation.x) 60 | val cy = cos(camera.orientation.y) 61 | val cz = cos(camera.orientation.z) 62 | val sx = sin(camera.orientation.x) 63 | val sy = sin(camera.orientation.y) 64 | val sz = sin(camera.orientation.z) 65 | 66 | val dx = cy * (sz * y + cz * x) - sy * z 67 | val dy = sx * (cy * z + sy * (sz * y + cz * x)) + cx * (cz * y - sz * x) 68 | val dz = cx * (cy * z + sy * (sz * y + cz * x)) - sx * (cz * y - sz * x) 69 | 70 | val bx = camera.surfacePosition.z * dx / dz.fixZero() + camera.surfacePosition.x 71 | val by = camera.surfacePosition.z * dy / dz.fixZero() + camera.surfacePosition.y 72 | 73 | return Offset( 74 | x = bx.toFloat() + 1000, 75 | y = by.toFloat() + 1000 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/entities/Entity.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d.entities 2 | 3 | import com.arkivanov.compose3d.Triangle 4 | 5 | interface Entity { 6 | 7 | fun triangles(consumer: (Triangle) -> Unit) 8 | } 9 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/entities/Parallelepiped.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d.entities 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.arkivanov.compose3d.Triangle 5 | import com.arkivanov.compose3d.Vector 6 | import com.arkivanov.compose3d.plus 7 | 8 | data class Parallelepiped( 9 | val position: Vector, 10 | val length: Vector, 11 | val width: Vector, 12 | val height: Vector, 13 | val color: Color 14 | ) : Entity { 15 | 16 | private val r1 = Rectangle(position = position, length = length, width = width, color = color) 17 | private val r2 = Rectangle(position = position, length = length, width = height, color = color) 18 | private val r3 = Rectangle(position = position + width, length = length, width = height, color = color) 19 | private val r4 = Rectangle(position = position, length = width, width = height, color = color) 20 | private val r5 = Rectangle(position = position + length, length = width, width = height, color = color) 21 | private val r6 = Rectangle(position = position + height, length = length, width = width, color = color) 22 | 23 | override fun triangles(consumer: (Triangle) -> Unit) { 24 | r1.triangles(consumer) 25 | r2.triangles(consumer) 26 | r3.triangles(consumer) 27 | r4.triangles(consumer) 28 | r5.triangles(consumer) 29 | r6.triangles(consumer) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/entities/Rectangle.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d.entities 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.arkivanov.compose3d.Triangle 5 | import com.arkivanov.compose3d.Vector 6 | import com.arkivanov.compose3d.plus 7 | 8 | data class Rectangle( 9 | val position: Vector, 10 | val length: Vector, 11 | val width: Vector, 12 | val color: Color 13 | ) : Entity { 14 | 15 | private val t1: Triangle 16 | private val t2: Triangle 17 | 18 | init { 19 | val a = position 20 | val b = position + length 21 | val c = position + width 22 | val d = c + length 23 | t1 = Triangle(a = a, b = b, c = c, color = color) 24 | t2 = Triangle(a = b, b = c, c = d, color = color) 25 | } 26 | 27 | override fun triangles(consumer: (Triangle) -> Unit) { 28 | consumer(t1) 29 | consumer(t2) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/entities/Sphere.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d.entities 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.arkivanov.compose3d.Triangle 5 | import com.arkivanov.compose3d.Vector 6 | import com.arkivanov.compose3d.degToRad 7 | import com.arkivanov.compose3d.length 8 | import com.arkivanov.compose3d.plus 9 | import com.arkivanov.compose3d.rotate 10 | import com.arkivanov.compose3d.unaryMinus 11 | import kotlin.math.atan 12 | 13 | class Sphere( 14 | val position: Vector, 15 | val radius: Double, 16 | val color: Color, 17 | val n: Int 18 | ) : Entity { 19 | 20 | private val triangles: List 21 | 22 | // TODO: Optimize memory consumption here 23 | init { 24 | val top = Vector(y = radius) 25 | val bottom = -top 26 | 27 | val top1 = top.rotate(OX, VERTICAL_DELTA_RAD) 28 | val top2 = top1.rotate(OY, HORIZONTAL_DELTA_RAD) 29 | val top3 = top2.rotate(OY, HORIZONTAL_DELTA_RAD) 30 | val top4 = top3.rotate(OY, HORIZONTAL_DELTA_RAD) 31 | val top5 = top4.rotate(OY, HORIZONTAL_DELTA_RAD) 32 | 33 | val bottom1 = bottom.rotate(OX, -VERTICAL_DELTA_RAD).rotate(OY, HORIZONTAL_DELTA_RAD / 2.0) 34 | val bottom2 = bottom1.rotate(OY, HORIZONTAL_DELTA_RAD) 35 | val bottom3 = bottom2.rotate(OY, HORIZONTAL_DELTA_RAD) 36 | val bottom4 = bottom3.rotate(OY, HORIZONTAL_DELTA_RAD) 37 | val bottom5 = bottom4.rotate(OY, HORIZONTAL_DELTA_RAD) 38 | 39 | val queue = ArrayDeque() 40 | queue += Triangle(a = top, b = top1, c = top2, color = color) 41 | queue += Triangle(a = top, b = top2, c = top3, color = color) 42 | queue += Triangle(a = top, b = top3, c = top4, color = color) 43 | queue += Triangle(a = top, b = top4, c = top5, color = color) 44 | queue += Triangle(a = top, b = top5, c = top1, color = color) 45 | queue += Triangle(a = bottom, b = bottom1, c = bottom2, color = color) 46 | queue += Triangle(a = bottom, b = bottom2, c = bottom3, color = color) 47 | queue += Triangle(a = bottom, b = bottom3, c = bottom4, color = color) 48 | queue += Triangle(a = bottom, b = bottom4, c = bottom5, color = color) 49 | queue += Triangle(a = bottom, b = bottom5, c = bottom1, color = color) 50 | queue += Triangle(a = top1, b = bottom1, c = top2, color = color) 51 | queue += Triangle(a = bottom1, b = top2, c = bottom2, color = color) 52 | queue += Triangle(a = top2, b = bottom2, c = top3, color = color) 53 | queue += Triangle(a = bottom2, b = top3, c = bottom3, color = color) 54 | queue += Triangle(a = top3, b = bottom3, c = top4, color = color) 55 | queue += Triangle(a = bottom3, b = top4, c = bottom4, color = color) 56 | queue += Triangle(a = top4, b = bottom4, c = top5, color = color) 57 | queue += Triangle(a = bottom4, b = top5, c = bottom5, color = color) 58 | queue += Triangle(a = top5, b = bottom5, c = top1, color = color) 59 | queue += Triangle(a = bottom5, b = top1, c = bottom1, color = color) 60 | 61 | repeat(n) { 62 | repeat(queue.size) { 63 | queue.removeFirst().splitTo(queue) 64 | } 65 | } 66 | 67 | repeat(queue.size) { 68 | queue.addLast(queue.removeFirst() + position) 69 | } 70 | 71 | triangles = queue 72 | } 73 | 74 | private fun Triangle.splitTo(queue: ArrayDeque) { 75 | val ab = split(a, b).radius() 76 | val bc = split(b, c).radius() 77 | val ac = split(c, a).radius() 78 | queue.addLast(Triangle(a = a, b = ab, c = ac, color = color)) 79 | queue.addLast(Triangle(a = b, b = ab, c = bc, color = color)) 80 | queue.addLast(Triangle(a = c, b = ac, c = bc, color = color)) 81 | queue.addLast(Triangle(a = ab, b = bc, c = ac, color = color)) 82 | } 83 | 84 | private fun Vector.radius(): Vector { 85 | val len = length() 86 | 87 | return Vector( 88 | x = x * radius / len, 89 | y = y * radius / len, 90 | z = z * radius / len 91 | ) 92 | } 93 | 94 | private fun split(a: Vector, b: Vector): Vector = 95 | Vector( 96 | x = (a.x + b.x) / 2.0, 97 | y = (a.y + b.y) / 2.0, 98 | z = (a.z + b.z) / 2.0 99 | ) 100 | 101 | override fun triangles(consumer: (Triangle) -> Unit) { 102 | triangles.forEach(consumer) 103 | } 104 | 105 | private companion object { 106 | private val VERTICAL_DELTA_RAD = degToRad(90.0) - atan(0.5) 107 | private val HORIZONTAL_DELTA_RAD = degToRad(72.0) 108 | private val OX = Vector(x = 1.0) 109 | private val OY = Vector(y = 1.0) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/kotlin/com/arkivanov/compose3d/entities/SquarePyramid.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.compose3d.entities 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.arkivanov.compose3d.Triangle 5 | import com.arkivanov.compose3d.Vector 6 | import com.arkivanov.compose3d.plus 7 | import com.arkivanov.compose3d.times 8 | 9 | data class SquarePyramid( 10 | val position: Vector, 11 | val length: Vector, 12 | val width: Vector, 13 | val height: Vector, 14 | val color: Color 15 | ) : Entity { 16 | 17 | private val t1: Triangle 18 | private val t2: Triangle 19 | private val t3: Triangle 20 | private val t4: Triangle 21 | private val t5: Triangle 22 | private val t6: Triangle 23 | 24 | init { 25 | val a = position 26 | val b = position + length 27 | val c = position + width 28 | val d = c + length 29 | t1 = Triangle(a = a, b = b, c = c, color = color) 30 | t2 = Triangle(a = b, b = c, c = d, color = color) 31 | val o = position + length * 0.5 + width * 0.5 32 | val h = o + height 33 | t3 = Triangle(a = a, b = b, c = h, color = color) 34 | t4 = Triangle(a = a, b = c, c = h, color = color) 35 | t5 = Triangle(a = c, b = d, c = h, color = color) 36 | t6 = Triangle(a = b, b = d, c = h, color = color) 37 | } 38 | 39 | override fun triangles(consumer: (Triangle) -> Unit) { 40 | consumer(t1) 41 | consumer(t2) 42 | consumer(t3) 43 | consumer(t4) 44 | consumer(t5) 45 | consumer(t6) 46 | } 47 | } 48 | --------------------------------------------------------------------------------