├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── after_effects_export ├── adobe_tools.nim ├── after_effects.nim ├── convert_to_null.nim ├── gradient_property.nim ├── nakefile.nim ├── rod_export.nim └── samples │ ├── main.nim │ └── res │ └── compositions │ ├── TEST1.json │ ├── TEST2.json │ ├── TEST3.json │ └── TEST4.json ├── deployGHPages.sh ├── doc └── index.rst ├── editor ├── main.nim ├── nakefile.nim ├── res │ ├── BoyzRGross.ttf │ ├── Jsondata │ │ ├── Spot.json │ │ ├── Spot.png │ │ ├── explosion.png │ │ ├── particle.png │ │ └── ps.json │ └── collada │ │ ├── T-shape.dae │ │ ├── arrow.dae │ │ ├── balloon_animation_test.dae │ │ ├── balloons_test.dae │ │ ├── baloon_star_color.png │ │ ├── baloon_star_normals.png │ │ ├── baloon_star_reflective.jpg │ │ └── motion.dae ├── rodedit.html ├── rodedit.nim └── rodedit_main.nim ├── nakefile.nim ├── rod.nimble ├── rod.nims ├── rod ├── animated_image.nim ├── animation │ ├── animation_sampler.nim │ └── property_animation.nim ├── asset_bundle.nim ├── component.nim ├── component │ ├── ae_composition.nim │ ├── all_components.nim │ ├── animation │ │ └── skeleton.nim │ ├── animation_runner.nim │ ├── blink.nim │ ├── blur_component.nim │ ├── camera.nim │ ├── channel_levels.nim │ ├── clipping_rect_component.nim │ ├── color_balance_hls.nim │ ├── color_fill.nim │ ├── comp_ref.nim │ ├── fxaa_post.nim │ ├── gradient_fill.nim │ ├── light.nim │ ├── mask.nim │ ├── material.nim │ ├── material_shaders.nim │ ├── mesh_component.nim │ ├── nine_part_sprite.nim │ ├── overlay.nim │ ├── particle_emitter.nim │ ├── particle_helpers.nim │ ├── particle_system.nim │ ├── primitives │ │ ├── cone.nim │ │ ├── cube.nim │ │ ├── icosphere.nim │ │ └── sphere.nim │ ├── rti.nim │ ├── solid.nim │ ├── sprite.nim │ ├── text_component.nim │ ├── tint.nim │ ├── tracer.nim │ ├── trail.nim │ ├── ui_component.nim │ ├── vector_shape.nim │ └── visual_modifier.nim ├── dae_animation.nim ├── edit_view.nim ├── editor │ ├── animation │ │ ├── animation_chart_view.nim │ │ ├── animation_editor_types.nim │ │ ├── animation_key_inspector.nim │ │ ├── dopesheet_view.nim │ │ └── editor_animation_view.nim │ ├── editor_assets_view.nim │ ├── editor_console.nim │ ├── editor_default_tabs.nim │ ├── editor_error_handling.nim │ ├── editor_inspector_view.nim │ ├── editor_project_settings.nim │ ├── editor_tab_registry.nim │ ├── editor_tree_view.nim │ ├── editor_types.nim │ ├── editor_workspace_view.nim │ ├── file_system │ │ ├── editor_asset_container_view.nim │ │ ├── editor_asset_icon_view.nim │ │ └── filesystem_view.nim │ └── scene │ │ ├── components │ │ ├── editor_component.nim │ │ ├── grid.nim │ │ └── viewport_rect.nim │ │ ├── editor_camera_controller.nim │ │ ├── editor_scene_settings.nim │ │ ├── editor_scene_view.nim │ │ ├── gizmo.nim │ │ ├── gizmo_move.nim │ │ └── node_selector.nim ├── material │ └── shader.nim ├── mesh.nim ├── message_queue.nim ├── node.nim ├── postprocess_context.nim ├── property_editors │ ├── propedit_registry.nim │ ├── rodedit_editors.nim │ └── standard_editors.nim ├── quaternion.nim ├── ray.nim ├── rod_types.nim ├── scene_composition.nim ├── system │ └── messaging.nim ├── systems.nim ├── tools │ ├── debug_draw.nim │ ├── rodasset │ │ ├── asset_cache.nim │ │ ├── binformat.nim │ │ ├── imgtool.nim │ │ ├── migrator.nim │ │ ├── rodasset.nim │ │ ├── rodasset.nims │ │ ├── rodasset_main.nim │ │ ├── settings.nim │ │ └── tree_traversal.nim │ ├── serializer.nim │ └── tool_wrapper.nim ├── utils │ ├── attributed_text.nim │ ├── bin_deserializer.nim │ ├── bin_serializer.nim │ ├── image_serialization.nim │ ├── json_deserializer.nim │ ├── json_serializer.nim │ ├── property_desc.nim │ ├── serialization_codegen.nim │ ├── serialization_hash_calculator.nim │ ├── serialization_helpers.nim │ └── text_helpers.nim ├── vertex_data_info.nim └── viewport.nim └── template ├── .gitignore ├── main.nim ├── nakefile.nim ├── res └── example_bundle │ ├── composition2.jcomp │ ├── config.rab │ └── logo-crown.png ├── src ├── core │ ├── asset_loader.nim │ └── game_scene.nim └── game │ └── example_scene.nim └── template.nimble /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | editor/main 3 | editor/nakefile 4 | build 5 | nvcompatlog.txt 6 | rod/tools/rodimgtool 7 | template/nimcache 8 | template/nakefile 9 | *.exe 10 | 11 | nakefile 12 | baner_01.png 13 | banner_2+.dae 14 | banner_2.dae 15 | rod/tools/rodasset/rodasset 16 | editor/res/collada/bug3.dae 17 | editor/res/collada/bug4.dae 18 | editor/res/collada/bug_sceene_1.mb 19 | editor/res/Jsondata/box.png 20 | editor/res/Jsondata/circle.png 21 | editor/res/Jsondata/phys.json 22 | editor/res/Jsondata/phys_ball.json 23 | editor/res/Jsondata/phys_box.json 24 | editor/res/Jsondata/test.json 25 | editor/res/Jsondata/wall.png 26 | editor/rodedit -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | before_install: 5 | - docker pull yglukhov/nim-ui-base 6 | script: 7 | - docker run -v "$(pwd):/project" -w /project yglukhov/nim-ui-base run "nimble install -y && nake tests" 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yuriy Glukhov 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rod [![Build Status](https://travis-ci.org/yglukhov/rod.svg?branch=master)](https://travis-ci.org/yglukhov/rod) 2 | 3 | Rod is a cross-platform 2d and 3d game engine for Nim. 4 | 5 | 8 | 9 | Installation: 10 | ``` 11 | $ git clone https://github.com/yglukhov/rod 12 | $ cd rod 13 | $ nimble install -y 14 | ``` 15 | 16 | Update all dependencies: 17 | ``` 18 | $ cd rod 19 | $ nimble install -y -d 20 | ``` 21 | 22 | Supported platforms: 23 | * iOS 24 | * Android 25 | * Windows 26 | * OSX 27 | * Linux 28 | * HTML5 (Emscripten) 29 | 30 | Build Requirements: 31 | * Nim latest devel version 32 | * Nimble package manager 33 | -------------------------------------------------------------------------------- /after_effects_export/adobe_tools.nim: -------------------------------------------------------------------------------- 1 | # Definitions according to Adobe JAVASCRIPT TOOLS GUIDE 2 | 3 | type 4 | File* = ref FileObj 5 | FileObj {.importc.} = object of RootObj 6 | name*: cstring 7 | path*: cstring 8 | encoding*: cstring 9 | lineFeed*: cstring # lfWindows, lfUnix or lfMacintosh 10 | parent*: Folder 11 | 12 | Folder* = ref FolderObj 13 | FolderObj {.importc.} = object of RootObj 14 | name*: cstring 15 | exists*: bool 16 | 17 | proc initMathTruncPolyfill() = 18 | # After effects doesn't have Math.truc func... 19 | {.emit: """ 20 | if (!Math.trunc) { 21 | Math.trunc = function(v) { 22 | v = +v; 23 | return (v - v % 1) || (!isFinite(v) || v === 0 ? v : v < 0 ? -0 : 0); 24 | }; 25 | } 26 | """.} 27 | 28 | initMathTruncPolyfill() 29 | 30 | const lfWindows*: cstring = "Windows" 31 | const lfUnix*: cstring = "Unix" 32 | const lfMacintosh*: cstring = "Macintosh" 33 | 34 | proc newFile*(path: cstring): File {.importc: "new File".} 35 | proc open*(f: File, mode: cstring) {.importcpp.} 36 | template openForWriting*(f: File) = f.open("w") 37 | proc write*(f: File, content: cstring) {.importcpp.} 38 | proc close*(f: File) {.importcpp.} 39 | proc copy*(src, target: File): bool {.importcpp.} 40 | proc copy*(src: File, targetPath: cstring): bool {.importcpp.} 41 | 42 | proc newFolder*(path: cstring): Folder {.importc: "new Folder".} 43 | proc getFiles*(f: Folder): seq[File] {.importcpp.} 44 | proc create*(f: Folder): bool {.importcpp.} 45 | proc execute*(f: Folder): bool {.importcpp.} 46 | proc remove*(f: Folder): bool {.importcpp.} 47 | -------------------------------------------------------------------------------- /after_effects_export/convert_to_null.nim: -------------------------------------------------------------------------------- 1 | import tables, dom, math 2 | import after_effects 3 | import times 4 | import json 5 | import algorithm 6 | import strutils 7 | import nimx/matrixes, nimx/pathutils 8 | import rod/quaternion 9 | 10 | type File = after_effects.File 11 | 12 | var logTextField: EditText 13 | 14 | proc logi(args: varargs[string, `$`]) = 15 | var text = $logTextField.text 16 | for i in args: 17 | text &= i 18 | text &= "\n" 19 | logTextField.text = text 20 | 21 | proc exportSelectedCompositions(exportFolderPath: cstring) {.exportc.} = 22 | let comp = app.project.activeItem(Composition) 23 | let layers = comp.selectedLayers 24 | for layer in layers: 25 | if not layer.nullLayer: 26 | logi "Converting layer: ", layer.name 27 | let newLayer = comp.layers.addNull() 28 | newLayer.name = layer.name 29 | newLayer.property("Position", Vector3).setValue(layer.property("Position", Vector3).value) 30 | layer.remove() 31 | 32 | logi("Done. ", epochTime()) 33 | 34 | {.emit: """ 35 | 36 | function buildUI(contextObj) { 37 | var mainWindow = null; 38 | if (contextObj instanceof Panel) { 39 | mainWindow = contextObj; 40 | } else { 41 | mainWindow = new Window("palette", "Animations", undefined, { 42 | resizeable: true 43 | }); 44 | mainWindow.size = [640, 300]; 45 | } 46 | //mainWindow.alignment = ['fill', 'fill']; 47 | 48 | var topGroup = mainWindow.add("group{orientation:'row'}"); 49 | topGroup.alignment = ["fill", "top"]; 50 | 51 | var setPathButton = topGroup.add("button", undefined, "Browse"); 52 | setPathButton.alignment = ["left", "center"]; 53 | 54 | var filePath = topGroup.add("statictext"); 55 | filePath.alignment = ["fill", "fill"]; 56 | 57 | var exportButton = topGroup.add("button", undefined, 58 | "Export selected compositions"); 59 | exportButton.alignment = ["right", "center"]; 60 | exportButton.enabled = false; 61 | 62 | if (app.settings.haveSetting("rodExport", "outputPath")) { 63 | exportButton.enabled = true; 64 | filePath.text = app.settings.getSetting("rodExport", "outputPath"); 65 | } else { 66 | filePath.text = "Output: (not specified)"; 67 | } 68 | 69 | var resultText = mainWindow.add( 70 | "edittext{alignment:['fill','fill'], properties: { multiline:true } }"); 71 | `logTextField`[0] = resultText; 72 | 73 | setPathButton.onClick = function(e) { 74 | var outputFile = Folder.selectDialog("Choose an output folder"); 75 | if (outputFile) { 76 | exportButton.enabled = true; 77 | filePath.text = outputFile.absoluteURI; 78 | app.settings.saveSetting("rodExport", "outputPath", outputFile.absoluteURI); 79 | } else { 80 | exportButton.enabled = false; 81 | } 82 | }; 83 | 84 | exportButton.onClick = function(e) { 85 | `logTextField`[0].text = ""; 86 | exportSelectedCompositions(filePath.text); 87 | }; 88 | 89 | mainWindow.addEventListener("resize", function(e) { 90 | this.layout.resize(); 91 | }); 92 | 93 | mainWindow.addEventListener("close", function(e) { 94 | app.cancelTask(taskId); 95 | stopServer(); 96 | }); 97 | 98 | mainWindow.onResizing = mainWindow.onResize = function() { 99 | this.layout.resize(); 100 | }; 101 | 102 | if (mainWindow instanceof Window) { 103 | // mainWindow.onShow = function() { 104 | // readMetaData(); 105 | // } 106 | mainWindow.show(); 107 | } else { 108 | mainWindow.layout.layout(true); 109 | // readMetaData(); 110 | } 111 | } 112 | 113 | buildUI(this); 114 | 115 | """.} 116 | -------------------------------------------------------------------------------- /after_effects_export/gradient_property.nim: -------------------------------------------------------------------------------- 1 | import after_effects 2 | import json 3 | 4 | proc getGradientColors*(gradientOverlay: PropertyGroup, comp: Composition): array[2, array[4, float]] = 5 | const undoGroupId = "__rod_export_script_undo_group__" 6 | app.beginUndoGroup(undoGroupId) 7 | 8 | let color = [1.0, 1.0, 1.0] 9 | let composSize = comp 10 | let tempLayer = comp.layers.addSolid(color, "temp", comp.width, comp.height, 1.0) 11 | 12 | tempLayer.selected = true 13 | app.executeCommand(app.findMenuCommandId("Gradient Overlay")) 14 | tempLayer.selected = false 15 | gradientOverlay.property("Colors").selected = true 16 | app.executeCommand(app.findMenuCommandId("Copy")) 17 | gradientOverlay.property("Colors").selected = false 18 | tempLayer.propertyGroup("Layer Styles").propertyGroup("Gradient Overlay").selected = true 19 | app.executeCommand(app.findMenuCommandId("Paste")) 20 | tempLayer.propertyGroup("Layer Styles").propertyGroup("Gradient Overlay").selected = false 21 | tempLayer.property("Position", array[3, float32]).setValue([0'f32, 0, 0]) 22 | tempLayer.property("anchorPoint", array[3, float32]).setValue([0'f32, 0, 0]) 23 | 24 | let newComp = comp.layers.precompose(@[1], "tempComp", true) 25 | let tempLayerText = comp.layers.addText() 26 | let tempText = tempLayerText.propertyGroup("Text").property("Source Text", TextDocument) 27 | 28 | tempText.expression = "thisComp.layer(\"tempComp\").sampleImage([0, 0], [0.5, 0.5], true).toSource()" 29 | for i in 0 ..< 1000: 30 | if $tempText.valueAtTime(0, false).text != "[0, 0, 0, 0]": break 31 | 32 | # end color 33 | let c0 = parseJson($tempText.value.text) 34 | result[1] = [c0[0].getFloat(), c0[1].getFloat(), c0[2].getFloat(), c0[3].getFloat()] 35 | 36 | tempText.expression = "thisComp.layer(\"tempComp\").sampleImage([0, " & $(comp.height - 1) & "], [0.5, 0.5], true).toSource()" 37 | for i in 0 ..< 1000: 38 | if $tempText.valueAtTime(0, false).text != "[0, 0, 0, 0]": break 39 | 40 | # start color 41 | let c1 = parseJson($tempText.value.text) 42 | result[0] = [c1[0].getFloat(), c1[1].getFloat(), c1[2].getFloat(), c1[3].getFloat()] 43 | 44 | app.endUndoGroup() 45 | app.undo(undoGroupId) -------------------------------------------------------------------------------- /after_effects_export/nakefile.nim: -------------------------------------------------------------------------------- 1 | import nake 2 | import os 3 | 4 | when defined(windows): 5 | const searchPaths = [ 6 | "C:/Program Files/Adobe/Adobe After Effects CC 2015.3/Support Files/Scripts", 7 | "C:/Program Files/Adobe/Adobe After Effects CC 2017/Support Files/Scripts", 8 | "C:/Program Files/Adobe/Adobe After Effects CC 2018/Support Files/Scripts" 9 | ] 10 | else: 11 | const searchPaths = [ 12 | "/Applications/Adobe After Effects CC 2015/Scripts", 13 | "/Applications/Adobe After Effects CC 2015.3/Scripts", 14 | "/Applications/Adobe After Effects CC 2017/Scripts", 15 | "/Applications/Adobe After Effects CC 2018/Scripts" 16 | ] 17 | 18 | task defaultTask, "Build and install": 19 | direShell nimExe, "js", "--warning[LockLevel]:off", "rod_export.nim" 20 | direShell nimExe, "js", "--warning[LockLevel]:off", "convert_to_null.nim" 21 | var ok = false 22 | for sp in searchPaths: 23 | if dirExists(sp): 24 | createDir(sp / "ScriptUI Panels") 25 | copyFile("nimcache/rod_export.js", sp / "ScriptUI Panels/rod_export.jsx") 26 | copyFile("nimcache/convert_to_null.js", sp / "ScriptUI Panels/convert_to_null.jsx") 27 | ok = true 28 | if not ok: 29 | raise newException(Exception, "After Effect not installed or not found!") -------------------------------------------------------------------------------- /after_effects_export/samples/main.nim: -------------------------------------------------------------------------------- 1 | import nimx/matrixes 2 | import rod/viewport 3 | import rod/edit_view 4 | import rod/component/camera 5 | import rod/node 6 | 7 | import rod/component/solid 8 | 9 | when defined js: 10 | import nimx.js_canvas_window 11 | type PlatformWindow = JSCanvasWindow 12 | else: 13 | import nimx.sdl_window 14 | type PlatformWindow = SdlWindow 15 | 16 | const isMobile = defined(ios) or defined(android) 17 | 18 | type SampleView = ref object of View 19 | viewport: Viewport 20 | 21 | method draw*(ev: SampleView, r: Rect) = 22 | ev.viewport.draw() 23 | 24 | proc startApplication() = 25 | var mainWindow : PlatformWindow 26 | mainWindow.new() 27 | 28 | when isMobile: 29 | mainWindow.initFullscreen() 30 | else: 31 | mainWindow.init(newRect(40, 40, 1200, 600)) 32 | 33 | mainWindow.title = "Rod" 34 | 35 | let editView = SampleView.new(mainWindow.bounds) 36 | editView.autoresizingMask = { afFlexibleWidth, afFlexibleHeight } 37 | editView.viewport.new() 38 | editView.viewport.view = editView 39 | editView.viewport.rootNode = newNode() 40 | let cameraNode = editView.viewport.rootNode.newChild("camera") 41 | let camera = cameraNode.component(Camera) 42 | camera.projectionMode = cpOrtho 43 | cameraNode.positionZ = 1 44 | 45 | mainWindow.addSubview(editView) 46 | mainWindow.addAnimation(c.animationNamed("anim1")) 47 | 48 | 49 | runApplication: 50 | startApplication() 51 | -------------------------------------------------------------------------------- /after_effects_export/samples/res/compositions/TEST1.json: -------------------------------------------------------------------------------- 1 | { "name": "TEST1", "children": [ { "name": "Blue Solid 1", "translation": [ 0, 0, 0 ], "scale": [ 0.29605263157895, 0.3, 1 ], "components": { "Solid": { "color": [ 0, 0.15431976318359, 1 ], "size": [ 512, 512 ] } } } ], "animations": { "anim1": { "Blue Solid 1.translation": { "duration": 0.73333333333333, "values": [ [ 0, 0, 0 ], [ 72.9417802377778, -8.36383182947478, 0 ], [ 145.692164841849, -18.5025008485039, 0 ], [ 218.803330587628, -25.4667145009578, 0 ], [ 292.182724738109, -24.656946944831, 0 ], [ 360, 0, 0 ], [ 392.357058474652, 67.0652239755088, 0 ], [ 403.713694895556, 141.38692530722, 0 ], [ 403.714291318908, 216.604453875111, 0 ], [ 392.356973614675, 290.935106949027, 0 ], [ 360, 358, 0 ], [ 292.470941549469, 390.199895070584, 0 ], [ 217.777091129965, 401.473994300279, 0 ], [ 142.214104696735, 401.473392563907, 0 ], [ 67.5294017906095, 390.199982028381, 0 ], [ 0, 358, 0 ], [ -24.7854537917066, 290.645099378283, 0 ], [ -25.6122641825846, 217.644673436292, 0 ], [ -18.6100608109513, 144.916035565569, 0 ], [ -8.41329818435354, 72.5550852432787, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ] } } } } -------------------------------------------------------------------------------- /after_effects_export/samples/res/compositions/TEST2.json: -------------------------------------------------------------------------------- 1 | { "name": "TEST2", "children": [ { "name": "Blue Solid 1", "translation": [ 66, 68, 0 ], "scale": [ 0.3, 0.3, 1 ], "components": { "Solid": { "color": [ 0, 0.15431976318359, 1 ], "size": [ 512, 512 ] } } } ], "animations": { "anim1": { "Blue Solid 1.scale": { "duration": 0.73333333333333, "values": [ [ 0.3, 0.3, 1 ], [ 0.34, 0.34, 1 ], [ 0.38, 0.38, 1 ], [ 0.42, 0.42, 1 ], [ 0.46, 0.46, 1 ], [ 0.5, 0.5, 1 ], [ 0.46, 0.46, 1 ], [ 0.42, 0.42, 1 ], [ 0.38, 0.38, 1 ], [ 0.34, 0.34, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ] ], "numberOfLoops": -1 } } } } -------------------------------------------------------------------------------- /after_effects_export/samples/res/compositions/TEST3.json: -------------------------------------------------------------------------------- 1 | { "name": "TEST3", "children": [ { "name": "Blue Solid 1$AUX", "translation": [ 200, 200, 0 ], "scale": [ 0.3, 0.3, 1 ], "children": [ { "name": "Blue Solid 1", "translation": [ -260, -260, 0 ], "components": { "Solid": { "color": [ 0, 0.15431976318359, 1 ], "size": [ 512, 512 ] } } } ] }, { "name": "Yellow Solid", "translation": [ 200, 200, 0 ], "scale": [ 0.109375, 0.1015625, 1 ], "components": { "Solid": { "color": [ 0.95946687459946, 1, 0 ], "size": [ 512, 512 ] } } } ], "animations": { "anim1": { "Blue Solid 1$AUX.scale": { "duration": 0.73333333333333, "values": [ [ 0.3, 0.3, 1 ], [ 0.34, 0.34, 1 ], [ 0.38, 0.38, 1 ], [ 0.42, 0.42, 1 ], [ 0.46, 0.46, 1 ], [ 0.5, 0.5, 1 ], [ 0.46, 0.46, 1 ], [ 0.42, 0.42, 1 ], [ 0.38, 0.38, 1 ], [ 0.34, 0.34, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ] ], "numberOfLoops": -1 } } } } -------------------------------------------------------------------------------- /after_effects_export/samples/res/compositions/TEST4.json: -------------------------------------------------------------------------------- 1 | { "name": "TEST4", "children": [ { "name": "Blue Solid 1$AUX", "translation": [ 200, 200, 0 ], "scale": [ 0.3, 0.3, 1 ], "children": [ { "name": "Blue Solid 1", "translation": [ -260, -260, 0 ], "components": { "Solid": { "color": [ 0, 0.15431976318359, 1 ], "size": [ 512, 512 ] } } } ] }, { "name": "Yellow Solid", "translation": [ 200, 200, 0 ], "scale": [ 0.109375, 0.1015625, 1 ], "components": { "Solid": { "color": [ 0.95946687459946, 1, 0 ], "size": [ 512, 512 ] } } } ], "animations": { "anim1": { "Blue Solid 1$AUX.scale": { "duration": 0.73333333333333, "values": [ [ 0.3, 0.3, 1 ], [ 0.34, 0.34, 1 ], [ 0.38, 0.38, 1 ], [ 0.42, 0.42, 1 ], [ 0.46, 0.46, 1 ], [ 0.5, 0.5, 1 ], [ 0.46, 0.46, 1 ], [ 0.42, 0.42, 1 ], [ 0.38, 0.38, 1 ], [ 0.34, 0.34, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ], [ 0.3, 0.3, 1 ] ], "numberOfLoops": -1 }, "Blue Solid 1$AUX.rotation": { "duration": 0.73333333333333, "values": [ [ 0, 0, 0, 1 ], [ 0, 0, 0.07845909572784, 0.99691733373313 ], [ 0, 0, 0.15643446504023, 0.98768834059514 ], [ 0, 0, 0.23344536385591, 0.97236992039768 ], [ 0, 0, 0.30901699437495, 0.95105651629515 ], [ 0, 0, 0.38268343236509, 0.92387953251129 ], [ 0, 0, 0.45399049973955, 0.89100652418837 ], [ 0, 0, 0.52249856471595, 0.85264016435409 ], [ 0, 0, 0.58778525229247, 0.80901699437495 ], [ 0, 0, 0.64944804833018, 0.76040596560003 ], [ 0, 0, 0.70710678118655, 0.70710678118655 ], [ 0, 0, 0.70710678118655, 0.70710678118655 ], [ 0, 0, 0.70710678118655, 0.70710678118655 ], [ 0, 0, 0.70710678118655, 0.70710678118655 ], [ 0, 0, 0.70710678118655, 0.70710678118655 ], [ 0, 0, 0.70710678118655, 0.70710678118655 ], [ 0, 0, 0.70710678118655, 0.70710678118655 ], [ 0, 0, 0.70710678118655, 0.70710678118655 ], [ 0, 0, 0.70710678118655, 0.70710678118655 ], [ 0, 0, 0.70710678118655, 0.70710678118655 ], [ 0, 0, 0.70710678118655, 0.70710678118655 ], [ 0, 0, 0.70710678118655, 0.70710678118655 ] ], "numberOfLoops": -1 } } } } -------------------------------------------------------------------------------- /deployGHPages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script requires GH_KEY environment variable to be set to Github access 4 | # token. The token may be generated at https://github.com/settings/tokens and 5 | # has to have permissions to commit (e.g. public_repo) 6 | 7 | # The variable itself then has to be set in travis-ci project settings. 8 | 9 | if [ "$GH_KEY" \!= "" ] 10 | then 11 | export GIT_DIR=/tmp/gh-pages.tmp 12 | rm -rf "$GIT_DIR" 13 | mkdir -p "$GIT_DIR" 14 | 15 | cd "$1" 16 | export GIT_WORK_TREE=$(pwd) 17 | git init 18 | 19 | git config user.name "Travis CI" 20 | git config user.email "autodocgen@example.com" 21 | 22 | git add . 23 | 24 | git commit -m "Deploy to GitHub Pages" 25 | 26 | git push --force --quiet "https://$GH_KEY@github.com/$TRAVIS_REPO_SLUG" master:gh-pages > /dev/null 2>&1 27 | fi 28 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | ==== 2 | Home 3 | ==== 4 | 5 | Welcome to rod 6 | -------------- 7 | 8 | **rod** is a game engine written in `Nim`_. 9 | -------------------------------------------------------------------------------- /editor/main.nim: -------------------------------------------------------------------------------- 1 | 2 | include rodedit_main 3 | -------------------------------------------------------------------------------- /editor/nakefile.nim: -------------------------------------------------------------------------------- 1 | 2 | import nimx/naketools 3 | 4 | withDir "..": 5 | direShell "nimble install -y" 6 | 7 | beforeBuild = proc(b: Builder) = 8 | #b.disableClosureCompiler = true 9 | b.mainFile = "rodedit_main" 10 | b.additionalCompilerFlags.add "-g" 11 | -------------------------------------------------------------------------------- /editor/res/BoyzRGross.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/rod/3362ca3a6af0480e7af864b8db6e7fb079203f68/editor/res/BoyzRGross.ttf -------------------------------------------------------------------------------- /editor/res/Jsondata/Spot.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bottom", 3 | "translation": [ 4 | 0, 5 | 5, 6 | 0 7 | ], 8 | "children": [ 9 | { 10 | "name": "Spot.png", 11 | "translation": [ 12 | 20, 13 | 0, 14 | 0 15 | ], 16 | "scale": [ 17 | 0.05, 18 | 0.05, 19 | 1.0 20 | ], 21 | "components": 22 | { 23 | "Sprite": 24 | { 25 | "fileNames": [ 26 | "Spot.png" 27 | ] 28 | } 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /editor/res/Jsondata/Spot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/rod/3362ca3a6af0480e7af864b8db6e7fb079203f68/editor/res/Jsondata/Spot.png -------------------------------------------------------------------------------- /editor/res/Jsondata/explosion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/rod/3362ca3a6af0480e7af864b8db6e7fb079203f68/editor/res/Jsondata/explosion.png -------------------------------------------------------------------------------- /editor/res/Jsondata/particle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/rod/3362ca3a6af0480e7af864b8db6e7fb079203f68/editor/res/Jsondata/particle.png -------------------------------------------------------------------------------- /editor/res/Jsondata/ps.json: -------------------------------------------------------------------------------- 1 | { 2 | "layer": 0, 3 | "name": "New Node", 4 | "components": [ 5 | { 6 | "is2D": true, 7 | "_c": "ConePSGenShape", 8 | "angle": -20.0, 9 | "radius": 2.0 10 | }, 11 | { 12 | "airDensity": 0.0, 13 | "startVelocity": 8.0, 14 | "lifetime": 8.0, 15 | "modifierNode": "attr", 16 | "duration": 3.0, 17 | "birthRate": 100.0, 18 | "randVelocityTo": 0.0, 19 | "randScaleTo": 0.0, 20 | "_c": "ParticleSystem", 21 | "randRotVelocityTo": [ 22 | 0.0, 23 | 0.0, 24 | 0.0 25 | ], 26 | "scaleSeq": [], 27 | "startScale": [ 28 | 3.0, 29 | 3.0, 30 | 3.0 31 | ], 32 | "randScaleFrom": 0.0, 33 | "colorMode": 0, 34 | "isLooped": true, 35 | "startColor": [ 36 | 1.0, 37 | 0.0, 38 | 0.0, 39 | 1.0 40 | ], 41 | "randVelocityFrom": 0.0, 42 | "scaleMode": 0, 43 | "dstScale": [ 44 | 0.0, 45 | 0.0, 46 | 0.0 47 | ], 48 | "genShapeNode": "New Node", 49 | "is3dRotation": false, 50 | "dstColor": [ 51 | 1.0, 52 | 0.0, 53 | 0.0, 54 | 1.0 55 | ], 56 | "isBlendAdd": false, 57 | "gravity": [ 58 | 0.0, 59 | 0.0, 60 | 0.0 61 | ], 62 | "randRotVelocityFrom": [ 63 | 0.0, 64 | 0.0, 65 | 0.0 66 | ], 67 | "colorSeq": [ 68 | [ 69 | 0.0, 70 | 1.0, 71 | 0.0, 72 | 0.0, 73 | 1.0 74 | ], 75 | [ 76 | 0.5, 77 | 0.0, 78 | 1.0, 79 | 0.0, 80 | 1.0 81 | ], 82 | [ 83 | 1.0, 84 | 0.0, 85 | 0.0, 86 | 1.0, 87 | 1.0 88 | ] 89 | ], 90 | "isPlayed": true 91 | } 92 | ], 93 | "alpha": 1.0, 94 | "translation": [ 95 | 0.0, 96 | 0.0, 97 | 0.0 98 | ], 99 | "scale": [ 100 | 1.0, 101 | 1.0, 102 | 1.0 103 | ], 104 | "children": [], 105 | "rotation": [ 106 | -0.5, 107 | 0.0, 108 | 0.0, 109 | 0.8660253882408142 110 | ] 111 | } -------------------------------------------------------------------------------- /editor/res/collada/baloon_star_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/rod/3362ca3a6af0480e7af864b8db6e7fb079203f68/editor/res/collada/baloon_star_color.png -------------------------------------------------------------------------------- /editor/res/collada/baloon_star_normals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/rod/3362ca3a6af0480e7af864b8db6e7fb079203f68/editor/res/collada/baloon_star_normals.png -------------------------------------------------------------------------------- /editor/res/collada/baloon_star_reflective.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/rod/3362ca3a6af0480e7af864b8db6e7fb079203f68/editor/res/collada/baloon_star_reflective.jpg -------------------------------------------------------------------------------- /editor/rodedit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /editor/rodedit.nim: -------------------------------------------------------------------------------- 1 | # This is a wrapper to rodedit_main. It compiles rodedit_main with rod 2 | # plugins of the project that calls the wrapper. 3 | import ../rod/tools/tool_wrapper 4 | runEditorWrapper("rodedit", "editor/rodedit_main.nim") 5 | -------------------------------------------------------------------------------- /editor/rodedit_main.nim: -------------------------------------------------------------------------------- 1 | import tables, logging, strutils 2 | 3 | import nimx / [ matrixes, window, autotest ] 4 | import rod / [ edit_view ] 5 | import rod/editor/editor_error_handling 6 | import rod/component/all_components 7 | 8 | import rod/editor/editor_project_settings 9 | 10 | const rodPluginFile {.strdefine.} = "" 11 | when rodPluginFile.len != 0: 12 | import macros 13 | macro doImport(): untyped = 14 | newTree(nnkImportStmt, newLit(rodPluginFile)) 15 | doImport() 16 | 17 | when defined(rodedit): 18 | import os 19 | 20 | const isMobile = defined(ios) or defined(android) 21 | 22 | proc runAutoTestsIfNeeded() = 23 | uiTest generalUITest: 24 | discard 25 | quitApplication() 26 | 27 | registerTest(generalUITest) 28 | when defined(runAutoTests): 29 | startRegisteredTests() 30 | 31 | proc switchToEditView(w: Window, proj: EditorProject)= 32 | discard w.startEditorForProject(proj) 33 | 34 | proc startApplication() = 35 | when isMobile or defined(js): 36 | var mainWindow = newFullscreenWindow() 37 | else: 38 | var mainWindow = newWindow(newRect(140, 40, 1600, 1000)) 39 | when loadingAndSavingAvailable: 40 | when defined(rodedit): 41 | var proj: EditorProject 42 | proj.name = getAppDir().lastPathPart 43 | proj.path = getAppDir() 44 | mainWindow.title = "Project " & proj.name 45 | mainWindow.switchToEditView(proj) 46 | else: 47 | var proj: EditorProject 48 | mainWindow.title = "Rod" 49 | mainWindow.switchToEditView(proj) 50 | 51 | runAutoTestsIfNeeded() 52 | 53 | when defined(rodedit): 54 | onUnhandledException = proc(msg: string) = 55 | var msg = msg.indent(8) 56 | error "Exception caught:\n ", msg 57 | 58 | runApplication: 59 | startApplication() 60 | -------------------------------------------------------------------------------- /nakefile.nim: -------------------------------------------------------------------------------- 1 | import nimx/naketools 2 | import osproc 3 | 4 | beforeBuild = proc(b: Builder) = 5 | #b.disableClosureCompiler = true 6 | b.mainFile = "editor/rodedit_main" 7 | b.originalResourcePath = "editor/res" 8 | b.additionalNimFlags.add("-d:rodplugin") 9 | b.additionalNimFlags.add("-d:rodedit") 10 | b.additionalNimFlags.add("--putenv:PREFS_FILE_NAME=rodedit.json") 11 | 12 | proc filterExceptions(name: string): bool = 13 | let exc = @["main", "nakefile", "rodedit", "rodasset"] 14 | 15 | for e in exc: 16 | let fileName = e & ".nim" 17 | if name.contains(e): 18 | return false 19 | 20 | result = true 21 | 22 | proc doMainModuleTests() = 23 | for f in walkDirRec "../rod/": 24 | if f.endsWith(".nim") and f.filterExceptions(): 25 | let content = readFile(f) 26 | 27 | if content.contains("isMainModule"): 28 | direShell "nim c -r --threads:on " & f 29 | 30 | task "mtests", "Main module tests": 31 | doMainModuleTests() 32 | 33 | task "tests", "Build and run autotests": 34 | let b = newBuilder() 35 | 36 | if b.platform == "js": 37 | b.runAfterBuild = false 38 | 39 | b.additionalNimFlags.add "-d:runAutoTests" 40 | b.build() 41 | 42 | if b.platform == "js": 43 | b.runAutotestsInFirefox() 44 | doMainModuleTests() 45 | 46 | task "docs", "Build documentation": 47 | createDir "./build/doc" 48 | withDir "./build/doc": 49 | for t, f in walkDir "../../rod": 50 | if f.endsWith(".nim"): 51 | shell "nim doc2 -d:js " & f & " &>/dev/null" 52 | 53 | for t, f in walkDir "../../doc": 54 | if f.endsWith(".rst"): 55 | direShell "nim rst2html " & f & " &>/dev/null" 56 | 57 | copyDir "../js", "./livedemo" 58 | -------------------------------------------------------------------------------- /rod.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | version = "0.1.0" 3 | author = "Anonymous" 4 | description = "Graphics engine" 5 | license = "MIT" 6 | 7 | bin = @["rod/tools/rodasset/rodasset", "editor/rodedit"] 8 | installExt = @["nim"] 9 | 10 | # Dependencies 11 | requires "nimx" 12 | requires "https://github.com/SSPKrolik/nimasset#head" 13 | requires "variant" 14 | requires "os_files" 15 | requires "https://github.com/yglukhov/imgtools" 16 | requires "cligen" 17 | requires "untar#head" 18 | requires "tempfile" 19 | requires "https://github.com/yglukhov/threadpools" 20 | requires "https://github.com/yglukhov/preferences" 21 | requires "sha1" 22 | requires "https://github.com/yglukhov/clipboard" 23 | -------------------------------------------------------------------------------- /rod.nims: -------------------------------------------------------------------------------- 1 | 2 | proc commonSetup() = 3 | --threads:on 4 | --noMain 5 | #switch("warning[LockLevel]", "off") 6 | 7 | task tests, "Autotests": 8 | commonSetup() 9 | --d:runAutoTests 10 | setCommand "c", "editor/rodedit" 11 | 12 | task editor, "Editor": 13 | commonSetup() 14 | setCommand "c", "editor/rodedit" 15 | -------------------------------------------------------------------------------- /rod/animated_image.nim: -------------------------------------------------------------------------------- 1 | import nimx/[types, image, animation, portable_gl] 2 | import math 3 | 4 | 5 | type AnimatedImage* = ref object of Image 6 | images*: seq[Image] 7 | currentFrame*: int 8 | anim*: Animation 9 | 10 | proc image*(ai: AnimatedImage): Image = 11 | if ai.images.len > ai.currentFrame: 12 | result = ai.images[ai.currentFrame] 13 | 14 | proc `image=`*(ai: AnimatedImage, i: Image) = 15 | ai.images.setLen(1) 16 | ai.images[0] = i 17 | ai.currentFrame = 0 18 | 19 | proc newAnimatedImageWithImage*(i: Image): AnimatedImage = 20 | result.new() 21 | result.image = i 22 | 23 | proc newAnimatedImageWithImageSeq*(imgs: seq[Image]): AnimatedImage = 24 | result.new() 25 | result.images = imgs 26 | 27 | proc frameAnimation*(ai: AnimatedImage, desiredFramerate: int = 30): Animation = 28 | if ai.anim.isNil: 29 | if desiredFramerate == 0: 30 | raise newException(Exception, "Division by zero") 31 | let a = newAnimation() 32 | let fps = 1.0 / float(desiredFramerate) 33 | a.loopDuration = float(ai.images.len) * fps 34 | a.continueUntilEndOfLoopOnCancel = true 35 | a.onAnimate = proc(p: float) = 36 | ai.currentFrame = round(float(ai.images.len - 1) * p).int 37 | ai.anim = a 38 | result = ai.anim 39 | 40 | method isLoaded*(ai: AnimatedImage): bool = 41 | result = ai.images[ai.currentFrame].isLoaded() 42 | 43 | method getTextureQuad*(ai: AnimatedImage, gl: GL, texCoords: var array[4, GLfloat]): TextureRef = 44 | result = getTextureQuad(ai.images[ai.currentFrame], gl, texCoords) 45 | 46 | proc size*(ai: AnimatedImage): Size = ai.images[ai.currentFrame].size() 47 | -------------------------------------------------------------------------------- /rod/component/all_components.nim: -------------------------------------------------------------------------------- 1 | # This is needed to include the components into the compilation so they 2 | # register themselves 3 | 4 | import blink, particle_system, particle_helpers, camera, channel_levels, 5 | mesh_component, overlay, particle_emitter, solid, 6 | sprite, text_component, light, clipping_rect_component, 7 | blur_component, visual_modifier, tracer, trail, fxaa_post, 8 | color_balance_hls, gradient_fill, color_fill, tint, ae_composition, 9 | primitives/cube, primitives/sphere, primitives/icosphere, primitives/cone, mask, 10 | comp_ref, rti, vector_shape, animation_runner, nine_part_sprite 11 | -------------------------------------------------------------------------------- /rod/component/animation_runner.nim: -------------------------------------------------------------------------------- 1 | import rod / [ rod_types, component, viewport, node ] 2 | import nimx / [ animation, animation_runner, window ] 3 | 4 | method init*(c: AnimationRunnerComponent) = 5 | c.runner = newAnimationRunner() 6 | 7 | method componentNodeWasAddedToSceneView*(c: AnimationRunnerComponent) = 8 | c.node.sceneView.addAnimationRunner(c.runner) 9 | 10 | method componentNodeWillBeRemovedFromSceneView*(c: AnimationRunnerComponent) = 11 | c.node.sceneView.removeAnimationRunner(c.runner) 12 | 13 | registerComponent(AnimationRunnerComponent) 14 | -------------------------------------------------------------------------------- /rod/component/blink.nim: -------------------------------------------------------------------------------- 1 | import nimx/[types, context, image, composition, property_visitor] 2 | import rod/[rod_types, viewport, component ] 3 | import rod / utils / [ property_desc, serialization_codegen ] 4 | import json 5 | 6 | var blinkComposition = newComposition """ 7 | uniform Image uMask; 8 | uniform Image uLight; 9 | uniform vec2 uLightSize; 10 | uniform vec4 uFromRect; 11 | uniform float uLightPos; 12 | uniform float uScale; 13 | 14 | void compose() { 15 | vec2 destuv = (vPos - bounds.xy) / bounds.zw; 16 | vec2 duv = uMask.texCoords.zw - uMask.texCoords.xy; 17 | vec2 srcxy = uMask.texCoords.xy + duv * uFromRect.xy; 18 | vec2 srczw = uMask.texCoords.xy + duv * uFromRect.zw; 19 | vec2 uv = srcxy + (srczw - srcxy) * destuv; 20 | vec4 mask = texture2D(uMask.tex, uv); 21 | 22 | vec2 lightdestuv = (vPos - vec2(uLightPos, 0.0)) / uLightSize; 23 | vec2 lightduv = uLight.texCoords.zw - uLight.texCoords.xy; 24 | vec2 lightsrcxy = uLight.texCoords.xy + lightduv * uFromRect.xy; 25 | vec2 lightsrczw = uLight.texCoords.xy + lightduv * uFromRect.zw; 26 | vec2 lightuv = lightsrcxy + (lightsrczw - lightsrcxy) * lightdestuv; 27 | 28 | float rect_alpha = 1.0; 29 | if (lightuv.x < uLight.texCoords.x || lightuv.x > uLight.texCoords.z || lightuv.y < uLight.texCoords.y || lightuv.y > uLight.texCoords.w) { 30 | rect_alpha = 0.0; 31 | } 32 | vec4 light = texture2D(uLight.tex, lightuv); 33 | 34 | gl_FragColor = light; 35 | gl_FragColor.a *= mask.a * step(0.0, lightuv.x) * step(0.0, uLight.texCoords.z - lightuv.x) * rect_alpha; 36 | } 37 | """ 38 | 39 | type Blink* = ref object of RenderComponent 40 | mask: Image 41 | light: Image 42 | currLightPos: float 43 | remainingTime: float 44 | period: float32 45 | speed: float32 46 | 47 | Blink.properties: 48 | mask 49 | light 50 | period 51 | speed 52 | 53 | method init(b: Blink) = 54 | b.speed = 1.0 55 | b.period = 2.0 56 | b.remainingTime = b.period 57 | 58 | method draw*(b: Blink) = 59 | if b.mask.isNil or b.light.isNil: 60 | return 61 | 62 | let c = currentContext() 63 | var r: Rect 64 | r.size = b.mask.size 65 | var fr = newRect(0, 0, 1, 1) 66 | let scale = b.mask.size.width / b.light.size.width 67 | 68 | let dt = getDeltaTime() 69 | b.currLightPos += b.speed * dt 70 | b.remainingTime -= dt 71 | if b.remainingTime <= 0.0: 72 | b.remainingTime = b.period 73 | b.currLightPos = 0.0 74 | 75 | blinkComposition.draw r: 76 | setUniform("uMask", b.mask) 77 | setUniform("uLight", b.light) 78 | setUniform("uLightSize", b.light.size) 79 | setUniform("uFromRect", fr) 80 | setUniform("uLightPos", b.currLightPos ) 81 | setUniform("uScale", scale) 82 | 83 | method getBBox*(b: Blink): BBox = 84 | let img = b.mask 85 | if not img.isNil: 86 | result.maxPoint = newVector3(0, 0, 0.0) 87 | result.minPoint = newVector3(img.size.width, img.size.height, 0.0) 88 | 89 | genSerializationCodeForComponent(Blink) 90 | 91 | method visitProperties*(b: Blink, p: var PropertyVisitor) = 92 | p.visitProperty("mask", b.mask) 93 | p.visitProperty("light", b.light) 94 | p.visitProperty("speed", b.speed) 95 | p.visitProperty("period", b.period) 96 | 97 | registerComponent(Blink) 98 | -------------------------------------------------------------------------------- /rod/component/camera.nim: -------------------------------------------------------------------------------- 1 | import nimx / [matrixes, types, context, property_visitor, view] 2 | import rod / [component, rod_types, node] 3 | import rod / utils / [ property_desc, serialization_codegen ] 4 | import math 5 | 6 | export CameraProjection 7 | export Camera 8 | 9 | Camera.properties: 10 | projectionMode 11 | zNear 12 | zFar 13 | fov 14 | viewportSize 15 | 16 | method init*(c: Camera) = 17 | c.projectionMode = cpPerspective 18 | c.zNear = 1 19 | c.zFar = 10000 20 | c.fov = 30 21 | 22 | proc calculateOrthoData(c: Camera): tuple[top, bottom, left, right: float] = 23 | let absBounds = c.node.sceneView.convertRectToWindow(c.node.sceneView.bounds) 24 | var winSize = absBounds.size 25 | if not c.node.sceneView.window.isNil: 26 | winSize = c.node.sceneView.window.bounds.size 27 | 28 | let cy = absBounds.y + absBounds.height / 2 29 | let cx = absBounds.x + absBounds.width / 2 30 | 31 | var logicalSize = c.viewportSize 32 | if logicalSize == zeroSize: 33 | logicalSize = absBounds.size 34 | c.viewportSize = logicalSize 35 | let k = absBounds.height / logicalSize.height 36 | result.top = -cy / k 37 | result.bottom = (winSize.height - cy) / k 38 | result.left = -cx / k 39 | result.right = (winSize.width - cx) / k 40 | 41 | proc getFrustum*(c: Camera): Frustum = 42 | let d = c.calculateOrthoData() 43 | let frustumOffset = -10.0 44 | let wt = c.node.worldTransform() 45 | result.minPoint = wt * newVector3(d.left + frustumOffset, d.top + frustumOffset, 0.0) 46 | result.maxPoint = wt * newVector3(d.right - frustumOffset, d.bottom - frustumOffset, 0.0) 47 | 48 | proc getProjectionMatrix*(c: Camera, viewportBounds: Rect, mat: var Transform3D) = 49 | doAssert(not c.node.sceneView.isNil) 50 | let absBounds = c.node.sceneView.convertRectToWindow(c.node.sceneView.bounds) 51 | var winSize = absBounds.size 52 | if not c.node.sceneView.window.isNil: 53 | winSize = c.node.sceneView.window.bounds.size 54 | 55 | let cy = absBounds.y + absBounds.height / 2 56 | let cx = absBounds.x + absBounds.width / 2 57 | 58 | case c.projectionMode 59 | of cpOrtho: 60 | let d = c.calculateOrthoData() 61 | mat.ortho(d.left, d.right, d.bottom, d.top, c.zNear, c.zFar) 62 | 63 | of cpPerspective: 64 | let top = -cy 65 | let bottom = winSize.height - cy 66 | let left = -cx 67 | let right = winSize.width - cx 68 | 69 | let angle = degToRad(c.fov) / 2.0 70 | let Z = absBounds.height / 2.0 / tan(angle) 71 | 72 | # near plane space 73 | let nLeft = c.zNear * left / Z 74 | let nRight = c.zNear * right / Z 75 | let nTop = c.zNear * top / Z 76 | let nBottom = c.zNear * bottom / Z 77 | 78 | mat.frustum(nLeft, nRight, -nBottom, -nTop, c.zNear, c.zFar) 79 | 80 | method visitProperties*(c: Camera, p: var PropertyVisitor) = 81 | p.visitProperty("zNear", c.zNear) 82 | p.visitProperty("zFar", c.zFar) 83 | p.visitProperty("fov", c.fov) 84 | p.visitProperty("vp size", c.viewportSize) 85 | p.visitProperty("projMode", c.projectionMode) 86 | 87 | genSerializationCodeForComponent(Camera) 88 | registerComponent(Camera) 89 | -------------------------------------------------------------------------------- /rod/component/channel_levels.nim: -------------------------------------------------------------------------------- 1 | import nimx / [types, context, composition, portable_gl, property_visitor] 2 | import rod / utils / [ property_desc, serialization_codegen ] 3 | import rod / component 4 | import rod / tools / serializer 5 | import json 6 | 7 | type ChannelLevels* = ref object of RenderComponent 8 | inWhite*, inBlack*, inGamma*, outWhite*, outBlack*: Coord 9 | inWhiteV*, inBlackV*, inGammaV*, outWhiteV*, outBlackV*: Vector3 10 | active: bool 11 | 12 | ChannelLevels.properties: 13 | inWhite 14 | inBlack 15 | inGamma 16 | outWhite 17 | outBlack 18 | inWhiteV 19 | inBlackV 20 | inGammaV 21 | outWhiteV 22 | outBlackV 23 | 24 | var levelsPostEffect = newPostEffect(""" 25 | vec3 colorPow(vec3 i, vec3 p) { 26 | return vec3(pow(i.r, p.r), pow(i.g, p.g), pow(i.b, p.b)); 27 | } 28 | 29 | vec3 colorPow(vec3 i, float p) { 30 | return vec3(pow(i.r, p), pow(i.g, p), pow(i.b, p)); 31 | } 32 | 33 | void channelLevels(vec3 inWhiteV, vec3 inBlackV, vec3 inGammaV, vec3 outWhiteV, 34 | vec3 outBlackV, float inWhite, float inBlack, float inGamma, float outWhite, float outBlack) { 35 | vec3 inPixel = gl_FragColor.rgb; 36 | inPixel = colorPow(clamp((inPixel - inBlackV) / (inWhiteV - inBlackV), 0.0, 1.0), 1.0 / inGammaV) * (outWhiteV - outBlackV) + outBlackV; 37 | inPixel = colorPow(clamp((inPixel - inBlack) / (inWhite - inBlack), 0.0, 1.0), 1.0 / inGamma) * (outWhite - outBlack) + outBlack; 38 | 39 | gl_FragColor.rgb = inPixel; 40 | } 41 | """, "channelLevels", ["vec3", "vec3", "vec3", "vec3", "vec3", "float", "float", "float", "float", "float"]) 42 | 43 | # Dirty hack to optimize out extra drawing: 44 | template `~==`(f1, f2: float): bool = (f1 > f2 - 0.2 and f1 < f2 + 0.2) 45 | proc `~==`(v: Vector3, f2: float): bool {.inline.} = v[0] ~== f2 and v[1] ~== f2 and v[2] ~== f2 46 | 47 | template areValuesNormal(c: ChannelLevels): bool = 48 | c.inWhiteV ~== 1 and c.inWhite ~== 1 and 49 | c.inBlackV ~== 1 and c.inBlack ~== 0 and 50 | c.inGammaV ~== 1 and c.inGamma ~== 1 and 51 | c.outWhiteV ~== 1 and c.outWhite ~== 1 and 52 | c.outBlackV ~== 1 and c.outBlack ~== 0 53 | 54 | method init*(c: ChannelLevels) = 55 | c.inWhiteV = newVector3(1, 1, 1) 56 | c.inBlackV = newVector3(0, 0, 0) 57 | c.inGammaV = newVector3(1, 1, 1) 58 | c.outWhiteV = newVector3(1, 1, 1) 59 | c.outBlackV = newVector3(0, 0, 0) 60 | 61 | c.inWhite = 1 62 | c.inBlack = 0 63 | c.inGamma = 1 64 | c.outWhite = 1 65 | c.outBlack = 0 66 | 67 | method deserialize*(c: ChannelLevels, j: JsonNode, s: Serializer) = 68 | var v = j{"inWhiteV"} 69 | if not v.isNil: 70 | c.inWhiteV = newVector3(v[0].getFloat(), v[1].getFloat(), v[2].getFloat()) 71 | c.inWhite = j["inWhite"].getFloat() 72 | 73 | v = j{"inBlackV"} 74 | if not v.isNil: 75 | c.inBlackV = newVector3(v[0].getFloat(), v[1].getFloat(), v[2].getFloat()) 76 | c.inBlack = j["inBlack"].getFloat() 77 | 78 | v = j{"inGammaV"} 79 | if not v.isNil: 80 | c.inGammaV = newVector3(v[0].getFloat(), v[1].getFloat(), v[2].getFloat()) 81 | c.inGamma = j["inGamma"].getFloat() 82 | 83 | v = j{"outWhiteV"} 84 | if not v.isNil: 85 | c.outWhiteV = newVector3(v[0].getFloat(), v[1].getFloat(), v[2].getFloat()) 86 | c.outWhite = j["outWhite"].getFloat() 87 | 88 | v = j{"outBlackV"} 89 | if not v.isNil: 90 | c.outBlackV = newVector3(v[0].getFloat(), v[1].getFloat(), v[2].getFloat()) 91 | c.outBlack = j["outBlack"].getFloat() 92 | 93 | method beforeDraw*(c: ChannelLevels, index: int): bool = 94 | c.active = not c.areValuesNormal() 95 | if c.active: 96 | pushPostEffect(levelsPostEffect, c.inWhiteV, c.inBlackV, c.inGammaV, c.outWhiteV, c.outBlackV, c.inWhite, c.inBlack, c.inGamma, c.outWhite, c.outBlack) 97 | 98 | method afterDraw*(c: ChannelLevels, index: int) = 99 | if c.active: 100 | popPostEffect() 101 | 102 | method serialize*(c: ChannelLevels, s: Serializer): JsonNode = 103 | result = newJObject() 104 | 105 | result.add("inWhiteV", s.getValue(c.inWhiteV)) 106 | result.add("inBlackV", s.getValue(c.inBlackV)) 107 | result.add("inGammaV", s.getValue(c.inGammaV)) 108 | result.add("outWhiteV", s.getValue(c.outWhiteV)) 109 | result.add("outBlackV", s.getValue(c.outBlackV)) 110 | 111 | result.add("inWhite", s.getValue(c.inWhite)) 112 | result.add("inBlack", s.getValue(c.inBlack)) 113 | result.add("inGamma", s.getValue(c.inGamma)) 114 | result.add("outWhite", s.getValue(c.outWhite)) 115 | result.add("outBlack", s.getValue(c.outBlack)) 116 | 117 | method visitProperties*(c: ChannelLevels, p: var PropertyVisitor) = 118 | p.visitProperty("inWhiteV", c.inWhiteV) 119 | p.visitProperty("inBlackV", c.inBlackV) 120 | p.visitProperty("inGammaV", c.inGammaV) 121 | p.visitProperty("outWhiteV", c.outWhiteV) 122 | p.visitProperty("outBlackV", c.outBlackV) 123 | 124 | p.visitProperty("inWhite", c.inWhite) 125 | p.visitProperty("inBlack", c.inBlack) 126 | p.visitProperty("inGamma", c.inGamma) 127 | p.visitProperty("outWhite", c.outWhite) 128 | p.visitProperty("outBlack", c.outBlack) 129 | 130 | p.visitProperty("redInWhite", c.inWhiteV[0], {pfAnimatable}) 131 | p.visitProperty("greenInWhite", c.inWhiteV[1], {pfAnimatable}) 132 | p.visitProperty("blueInWhite", c.inWhiteV[2], {pfAnimatable}) 133 | 134 | p.visitProperty("redInBlack", c.inBlackV[0], {pfAnimatable}) 135 | p.visitProperty("greenInBlack", c.inBlackV[1], {pfAnimatable}) 136 | p.visitProperty("blueInBlack", c.inBlackV[2], {pfAnimatable}) 137 | 138 | p.visitProperty("redInGamma", c.inGammaV[0], {pfAnimatable}) 139 | p.visitProperty("greenInGamma", c.inGammaV[1], {pfAnimatable}) 140 | p.visitProperty("blueInGamma", c.inGammaV[2], {pfAnimatable}) 141 | 142 | p.visitProperty("redOutWhite", c.outWhiteV[0], {pfAnimatable}) 143 | p.visitProperty("greenOutWhite", c.outWhiteV[1], {pfAnimatable}) 144 | p.visitProperty("blueOutWhite", c.outWhiteV[2], {pfAnimatable}) 145 | 146 | p.visitProperty("redOutBlack", c.outBlackV[0], {pfAnimatable}) 147 | p.visitProperty("greenOutBlack", c.outBlackV[1], {pfAnimatable}) 148 | p.visitProperty("blueOutBlack", c.outBlackV[2], {pfAnimatable}) 149 | 150 | genSerializationCodeForComponent(ChannelLevels) 151 | registerComponent(ChannelLevels, "Effects") 152 | -------------------------------------------------------------------------------- /rod/component/clipping_rect_component.nim: -------------------------------------------------------------------------------- 1 | import nimx/[types, context, composition, portable_gl, view, property_visitor] 2 | import rod/[ node, viewport, component, tools/serializer, rod_types] 3 | import rod / utils / [ property_desc, serialization_codegen ] 4 | import json 5 | import opengl 6 | 7 | 8 | const clippingRectWithScissors = true 9 | 10 | type ClippingRectComponent* = ref object of RenderComponent 11 | clippingRect*: Rect 12 | 13 | ClippingRectComponent.properties: 14 | clippingRect: 15 | serializationKey: "rect" 16 | 17 | when not clippingRectWithScissors: 18 | var clippingRectPostEffect = newPostEffect(""" 19 | uniform vec2 uTopLeft; 20 | uniform vec2 uBottomRight; 21 | uniform vec2 viewportSize; 22 | 23 | float insideBox(vec2 v, vec2 bottomLeft, vec2 topRight) { 24 | vec2 s = step(bottomLeft, v) - step(topRight, v); 25 | return s.x * s.y; 26 | } 27 | 28 | void clipRect() { 29 | vec2 pos = gl_FragCoord.xy; 30 | pos.y = viewportSize.y - pos.y; 31 | gl_FragColor.a *= insideBox(pos, uTopLeft, uBottomRight); 32 | } 33 | """, "clipRect") 34 | 35 | import rod/tools/debug_draw 36 | 37 | proc debugDraw(cl: ClippingRectComponent, rect: Rect) = 38 | let gl = currentContext().gl 39 | gl.disable(gl.DEPTH_TEST) 40 | DDdrawRect(rect, newColor(1.0, 0.2, 0.2, 1.0)) 41 | gl.disable(gl.DEPTH_TEST) 42 | 43 | method draw*(cl: ClippingRectComponent) = 44 | let tl = cl.clippingRect.minCorner() 45 | let br = cl.clippingRect.maxCorner() 46 | let tlv = newVector3(tl.x, tl.y) 47 | let brv = newVector3(br.x, br.y) 48 | 49 | let sv = cl.node.sceneView 50 | let tlvw = sv.worldToScreenPoint(cl.node.localToWorld(tlv)) 51 | let brvw = sv.worldToScreenPoint(cl.node.localToWorld(brv)) 52 | 53 | let tlp = sv.convertPointToWindow(newPoint(tlvw.x, tlvw.y)) 54 | let brp = sv.convertPointToWindow(newPoint(brvw.x, brvw.y)) 55 | 56 | when clippingRectWithScissors: 57 | let gl = currentContext().gl 58 | gl.enable(gl.SCISSOR_TEST) 59 | var pr = 1.0'f32 60 | var b: Rect 61 | if sv.window.isNil: 62 | b = sv.bounds 63 | else: 64 | pr = sv.window.viewportPixelRatio 65 | b = sv.window.bounds 66 | 67 | var x = GLint(tlp.x * pr) 68 | var y = GLint((b.height - brp.y) * pr) 69 | var w = GLsizei((brp.x - tlp.x) * pr) 70 | var h = GLSizei((brp.y - tlp.y) * pr) 71 | if w >= 0 and h >= 0: 72 | gl.scissor(x, y, w, h) 73 | 74 | for c in cl.node.children: c.recursiveDraw() 75 | gl.disable(gl.SCISSOR_TEST) 76 | 77 | else: 78 | pushPostEffect clippingRectPostEffect: 79 | setUniform("uTopLeft", tl2) 80 | setUniform("uBottomRight", br2) 81 | setUniform("viewportSize", vpSize) 82 | 83 | for c in cl.node.children: c.recursiveDraw() 84 | 85 | popPostEffect() 86 | 87 | if cl.node.sceneView.editing: 88 | cl.debugDraw(cl.clippingRect) 89 | 90 | method isPosteffectComponent*(c: ClippingRectComponent): bool = true 91 | 92 | method getBBox*(c: ClippingRectComponent): BBox = 93 | result.minPoint = newVector3(c.clippingRect.x, c.clippingRect.y, 0.0) 94 | result.maxPoint = newVector3(c.clippingRect.width, c.clippingRect.height, 0.0) 95 | 96 | method visitProperties*(cl: ClippingRectComponent, p: var PropertyVisitor) = 97 | p.visitProperty("rect", cl.clippingRect) 98 | 99 | method serialize*(c: ClippingRectComponent, s: Serializer): JsonNode = 100 | result = newJObject() 101 | result.add("rect", s.getValue(c.clippingRect)) 102 | 103 | method deserialize*(c: ClippingRectComponent, j: JsonNode, s: Serializer) = 104 | s.deserializeValue(j, "rect", c.clippingRect) 105 | 106 | genSerializationCodeForComponent(ClippingRectComponent) 107 | 108 | registerComponent(ClippingRectComponent) 109 | -------------------------------------------------------------------------------- /rod/component/color_balance_hls.nim: -------------------------------------------------------------------------------- 1 | import nimx / [types, context, composition, portable_gl, property_visitor] 2 | import rod / utils / [ property_desc, serialization_codegen ] 3 | import rod / [ component, tools/serializer ] 4 | import json 5 | 6 | type ColorBalanceHLS* = ref object of RenderComponent 7 | hue*: float32 8 | saturation*: float32 9 | lightness*: float32 10 | hlsMin*: float32 11 | hlsMax*: float32 12 | enabled: bool 13 | 14 | # This effect is borrowed from https://github.com/greggman/hsva-unity / HSL version 15 | 16 | ColorBalanceHLS.properties: 17 | hue 18 | saturation 19 | lightness 20 | hlsMin 21 | hlsMax 22 | 23 | var effect = newPostEffect(""" 24 | float cbhls_effect_Epsilon = 1e-10; 25 | 26 | vec3 cbhls_effect_rgb2hcv(vec3 RGB) { 27 | // Based on work by Sam Hocevar and Emil Persson 28 | vec4 P = mix(vec4(RGB.bg, -1.0, 2.0/3.0), vec4(RGB.gb, 0.0, -1.0/3.0), step(RGB.b, RGB.g)); 29 | vec4 Q = mix(vec4(P.xyw, RGB.r), vec4(RGB.r, P.yzx), step(P.x, RGB.r)); 30 | float C = Q.x - min(Q.w, Q.y); 31 | float H = abs((Q.w - Q.y) / (6.0 * C + cbhls_effect_Epsilon) + Q.z); 32 | return vec3(H, C, Q.x); 33 | } 34 | 35 | vec3 cbhls_effect_rgb2hsl(vec3 RGB) { 36 | vec3 HCV = cbhls_effect_rgb2hcv(RGB); 37 | float L = HCV.z - HCV.y * 0.5; 38 | float S = HCV.y / (1.0 - abs(L * 2.0 - 1.0) + cbhls_effect_Epsilon); 39 | return vec3(HCV.x, S, L); 40 | } 41 | 42 | vec3 cbhls_effect_hsl2rgb(vec3 c) { 43 | c = vec3(fract(c.x), clamp(c.yz, 0.0, 1.0)); 44 | vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0); 45 | return c.z + c.y * (rgb - 0.5) * (1.0 - abs(2.0 * c.z - 1.0)); 46 | } 47 | 48 | void cbhls_effect(float hue, float saturation, float lightness, float hlsMin, float hlsMax) { 49 | vec3 hsl = cbhls_effect_rgb2hsl(gl_FragColor.rgb); 50 | vec3 adjustment = vec3(hue, saturation, lightness); 51 | adjustment.xy *= step(0.001, hsl.x + hsl.y); 52 | float affectMult = step(hlsMin, hsl.r) * step(hsl.r, hlsMax); 53 | gl_FragColor.rgb = cbhls_effect_hsl2rgb(hsl + adjustment * affectMult); 54 | } 55 | """, "cbhls_effect", ["float", "float", "float", "float", "float"]) 56 | 57 | method init*(c: ColorBalanceHLS) = 58 | procCall c.Component.init() 59 | c.hlsMax = 1.0 60 | 61 | # Dirty hack to optimize out extra drawing: 62 | template `~==`(f1, f2: float32): bool = (f1 > f2 - 0.02 and f1 < f2 + 0.02) 63 | 64 | template areValuesNormal(c: ColorBalanceHLS): bool = 65 | c.hue ~== 0 and c.saturation ~== 0 and c.lightness ~== 0 66 | 67 | method deserialize*(c: ColorBalanceHLS, j: JsonNode, s: Serializer) = 68 | c.hue = j["hue"].getFloat() 69 | c.saturation = j["saturation"].getFloat() 70 | c.lightness = j["lightness"].getFloat() 71 | c.hlsMin = j{"hlsMin"}.getFloat() 72 | c.hlsMax = j{"hlsMax"}.getFloat(1.0) 73 | 74 | method serialize*(c: ColorBalanceHLS, s: Serializer): JsonNode = 75 | result = newJObject() 76 | result.add("hue", s.getValue(c.hue)) 77 | result.add("saturation", s.getValue(c.saturation)) 78 | result.add("lightness", s.getValue(c.lightness)) 79 | result.add("hlsMin", s.getValue(c.hlsMin)) 80 | result.add("hlsMax", s.getValue(c.hlsMax)) 81 | 82 | method beforeDraw*(c: ColorBalanceHLS, index: int): bool = 83 | c.enabled = not c.areValuesNormal() 84 | if c.enabled: 85 | pushPostEffect(effect, c.hue, c.saturation, c.lightness, c.hlsMin, c.hlsMax) 86 | 87 | method afterDraw*(c: ColorBalanceHLS, index: int) = 88 | if c.enabled: 89 | popPostEffect() 90 | 91 | method visitProperties*(c: ColorBalanceHLS, p: var PropertyVisitor) = 92 | p.visitProperty("hue", c.hue) 93 | p.visitProperty("saturation", c.saturation) 94 | p.visitProperty("lightness", c.lightness) 95 | p.visitProperty("hlsMin", c.hlsMin) 96 | p.visitProperty("hlsMax", c.hlsMax) 97 | 98 | genSerializationCodeForComponent(ColorBalanceHLS) 99 | registerComponent(ColorBalanceHLS, "Effects") 100 | -------------------------------------------------------------------------------- /rod/component/color_fill.nim: -------------------------------------------------------------------------------- 1 | import nimx/[types, context, composition, portable_gl, property_visitor] 2 | import rod / [component, tools/serializer] 3 | import rod / utils / [ property_desc, serialization_codegen ] 4 | import json 5 | 6 | type ColorFill* = ref object of RenderComponent 7 | color*: Color 8 | 9 | ColorFill.properties: 10 | color 11 | 12 | var effect = newPostEffect(""" 13 | void color_fill_effect(vec4 color, float dummy) { 14 | color.a *= gl_FragColor.a; 15 | gl_FragColor = color; 16 | } 17 | """, "color_fill_effect", ["vec4", "float"]) 18 | 19 | method deserialize*(c: ColorFill, j: JsonNode, s: Serializer) = 20 | var v = j["color"] 21 | c.color = newColor(v[0].getFloat(), v[1].getFloat(), v[2].getFloat(), v[3].getFloat()) 22 | 23 | method beforeDraw*(c: ColorFill, index: int): bool = 24 | const dummyUniform = 0.0'f32 # This unpleasantness is originated from the fact 25 | # that new `pushPostEffect` is conflicting with the 26 | # old one when number of uniforms is 1. 27 | # Should be cleaned up when old `pushPostEffect` is removed 28 | pushPostEffect(effect, c.color, dummyUniform) 29 | 30 | method afterDraw*(c: ColorFill, index: int) = 31 | popPostEffect() 32 | 33 | method visitProperties*(c: ColorFill, p: var PropertyVisitor) = 34 | p.visitProperty("color", c.color) 35 | 36 | genSerializationCodeForComponent(ColorFill) 37 | 38 | registerComponent(ColorFill, "Effects") 39 | -------------------------------------------------------------------------------- /rod/component/comp_ref.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ types, matrixes, property_visitor ] 2 | import rod / [ node, rod_types, component, tools/serializer ] 3 | import rod / utils / [property_desc, serialization_codegen ] 4 | import rod/component/nine_part_sprite 5 | import json 6 | 7 | 8 | type CompRef* = ref object of ScriptComponent 9 | size*: Size 10 | path: string 11 | refNode: Node 12 | 13 | CompRef.properties: 14 | size 15 | path 16 | 17 | proc setSize(n: Node, sz: Size) = 18 | for c in n.components: 19 | if c of NinePartSprite: 20 | let s = NinePartSprite(c) 21 | s.size = sz 22 | for c in n.children: 23 | c.setSize(sz) 24 | 25 | proc awake(c: CompRef) = 26 | let n = newNodeWithResource(c.path) 27 | n.setSize(c.size) 28 | n.isSerializable = false 29 | c.node.addChild(n) 30 | c.refNode = n 31 | 32 | proc setSize*(c: CompRef, s: Size)= 33 | if not c.refNode.isNil: 34 | c.refNode.setSize(s) 35 | 36 | method deserialize*(s: CompRef, j: JsonNode, serializer: Serializer) = 37 | let v = j{"size"} 38 | if not v.isNil: 39 | s.size = newSize(v[0].getFloat(), v[1].getFloat()) 40 | s.path = j["path"].str 41 | s.awake() 42 | 43 | genSerializationCodeForComponent(CompRef) 44 | 45 | method getBBox*(s: CompRef): BBox = 46 | result.minPoint = newVector3(0.0, 0.0, 0.0) 47 | result.maxPoint = newVector3(s.size.width, s.size.height, 0.0) 48 | 49 | method visitProperties*(c: CompRef, p: var PropertyVisitor) = 50 | p.visitProperty("size", c.size) 51 | c.setSize(c.size) 52 | 53 | method serialize*(c: CompRef, s: Serializer): JsonNode = 54 | result = newJObject() 55 | result.add("size", s.getValue(c.size)) 56 | result.add("path", s.getValue(c.path)) 57 | 58 | registerComponent(CompRef) 59 | -------------------------------------------------------------------------------- /rod/component/gradient_fill.nim: -------------------------------------------------------------------------------- 1 | import nimx/[view, context, matrixes, composition, portable_gl, property_visitor ] 2 | import rod / utils / [ property_desc, serialization_codegen ] 3 | import rod/[node, viewport, component, tools/serializer] 4 | 5 | import json 6 | 7 | var effectLinearLocal = newPostEffect(""" 8 | void grad_fill_effect_linear_local(vec2 gradientStartPos, vec2 gradientEndPos, vec4 startColor, vec4 endColor) { 9 | float alpha = atan( -gradientEndPos.y + gradientStartPos.y, gradientEndPos.x - gradientStartPos.x ); 10 | float gradientStartPosRotatedX = gradientStartPos.x*cos(alpha) - gradientStartPos.y*sin(alpha); 11 | float gradientEndPosRotatedX = gradientEndPos.x*cos(alpha) - gradientEndPos.y*sin(alpha); 12 | float d = gradientEndPosRotatedX - gradientStartPosRotatedX; 13 | float y = vPos.y; 14 | float x = vPos.x; 15 | float xLocRotated = x*cos( alpha ) - y*sin( alpha ); 16 | vec4 gradientColor = mix(startColor, endColor, smoothstep( gradientStartPosRotatedX, gradientStartPosRotatedX + d, xLocRotated ) ); 17 | 18 | gl_FragColor.rgb = mix(gl_FragColor, gradientColor, gradientColor.a).rgb; 19 | } 20 | """, "grad_fill_effect_linear_local", ["vec2", "vec2", "vec4", "vec4"]) 21 | 22 | var effectLinear = newPostEffect(""" 23 | void grad_fill_effect_linear(vec2 startPoint, vec2 diff, vec4 startColor, vec4 endColor) { 24 | float s = dot(gl_FragCoord.xy-startPoint, diff) / dot(diff, diff); 25 | vec4 color = mix(startColor, endColor, s); 26 | color.a *= gl_FragColor.a; 27 | gl_FragColor = color; 28 | } 29 | """, "grad_fill_effect_linear", ["vec2", "vec2", "vec4", "vec4"]) 30 | 31 | var effectRadialLocal = newPostEffect(""" 32 | void grad_fill_effect_radial_local(vec2 center, float radius, vec4 startColor, vec4 endColor) { 33 | float dist = distance(center, vPos.xy); 34 | float d = smoothstep(0.0, 1.0, dist / radius); 35 | vec4 color = mix(startColor, endColor, d); 36 | 37 | gl_FragColor.rgb = mix(gl_FragColor, color, color.a).rgb; 38 | } 39 | """, "grad_fill_effect_radial_local", ["vec2", "float", "vec4", "vec4"]) 40 | 41 | var effectRadial = newPostEffect(""" 42 | void grad_fill_effect_radial(vec2 center, float radius, vec4 startColor, vec4 endColor) { 43 | float dist = distance(center, gl_FragCoord.xy); 44 | float d = dist / radius; 45 | vec4 color = mix(startColor, endColor, d); 46 | color.a *= gl_FragColor.a; 47 | gl_FragColor = color; 48 | } 49 | """, "grad_fill_effect_radial", ["vec2", "float", "vec4", "vec4"]) 50 | 51 | type RampShape* = enum 52 | LinearRamp 53 | RadialRamp 54 | 55 | type GradientFill* = ref object of RenderComponent 56 | startPoint*: Point 57 | endPoint*: Point 58 | startColor*: Color 59 | endColor*: Color 60 | shape*: RampShape 61 | localCoords*: bool 62 | 63 | GradientFill.properties: 64 | startPoint 65 | endPoint 66 | startColor 67 | endColor 68 | shape 69 | localCoords 70 | 71 | method serialize*(gf: GradientFill, serealizer: Serializer): JsonNode = 72 | result = newJObject() 73 | result.add("startPoint", serealizer.getValue(gf.startPoint)) 74 | result.add("endPoint", serealizer.getValue(gf.endPoint)) 75 | result.add("startColor", serealizer.getValue(gf.startColor)) 76 | result.add("endColor", serealizer.getValue(gf.endColor)) 77 | result.add("shape", serealizer.getValue(gf.shape)) 78 | result.add("localCoords", serealizer.getValue(gf.localCoords)) 79 | 80 | method deserialize*(gf: GradientFill, j: JsonNode, serealizer: Serializer) = 81 | serealizer.deserializeValue(j, "startPoint", gf.startPoint) 82 | serealizer.deserializeValue(j, "endPoint", gf.endPoint) 83 | serealizer.deserializeValue(j, "startColor", gf.startColor) 84 | serealizer.deserializeValue(j, "endColor", gf.endColor) 85 | serealizer.deserializeValue(j, "shape", gf.shape) 86 | serealizer.deserializeValue(j, "localCoords", gf.localCoords) 87 | 88 | method beforeDraw*(gf: GradientFill, index: int): bool = 89 | var tlp: Point 90 | var brp: Point 91 | 92 | if gf.localCoords: 93 | tlp = gf.startPoint 94 | brp = gf.endPoint 95 | else: 96 | let tl = gf.startPoint 97 | let br = gf.endPoint 98 | let tlv = newVector3(tl.x, tl.y) 99 | let brv = newVector3(br.x, br.y) 100 | 101 | let sv = gf.node.sceneView 102 | 103 | var screenBounds = sv.bounds 104 | if not sv.window.isNil: 105 | screenBounds = sv.window.bounds 106 | 107 | let tlvw = sv.worldToScreenPoint(gf.node.localToWorld(tlv)) 108 | let brvw = sv.worldToScreenPoint(gf.node.localToWorld(brv)) 109 | 110 | tlp = sv.convertPointToWindow(newPoint(tlvw.x, tlvw.y)) 111 | brp = sv.convertPointToWindow(newPoint(brvw.x, brvw.y)) 112 | 113 | tlp.y = screenBounds.height - tlp.y 114 | brp.y = screenBounds.height - brp.y 115 | 116 | if gf.shape == RadialRamp: 117 | let radius = distanceTo(tlp, brp) 118 | if gf.localCoords: 119 | pushPostEffect(effectRadialLocal, tlp, radius, gf.startColor, gf.endColor) 120 | else: 121 | pushPostEffect(effectRadial, tlp, radius, gf.startColor, gf.endColor) 122 | else: 123 | let diff = brp - tlp 124 | if gf.localCoords: 125 | pushPostEffect(effectLinearLocal, tlp, brp, gf.startColor, gf.endColor) 126 | else: 127 | pushPostEffect(effectLinear, tlp, brp, gf.startColor, gf.endColor) 128 | 129 | method afterDraw*(gf: GradientFill, index: int) = 130 | popPostEffect() 131 | 132 | method visitProperties*(gf: GradientFill, p: var PropertyVisitor) = 133 | p.visitProperty("startPoint", gf.startPoint) 134 | p.visitProperty("startColor", gf.startColor) 135 | p.visitProperty("endPoint", gf.endPoint) 136 | p.visitProperty("endColor", gf.endColor) 137 | p.visitProperty("shape", gf.shape) 138 | p.visitProperty("localCoords", gf.localCoords) 139 | 140 | genSerializationCodeForComponent(GradientFill) 141 | 142 | registerComponent(GradientFill, "Effects") 143 | -------------------------------------------------------------------------------- /rod/component/light.nim: -------------------------------------------------------------------------------- 1 | import rod/[rod_types, component, node, tools/serializer] 2 | import nimx/[types, matrixes, property_visitor] 3 | import json, tables, logging 4 | 5 | export LightSource 6 | 7 | proc addLightSource*(v: SceneView, ls: LightSource) = 8 | if v.lightSources.isNil(): 9 | v.lightSources = newTable[string, LightSource]() 10 | if v.lightSources.len() < rod_types.maxLightsCount: 11 | v.lightSources[ls.node.name] = ls 12 | else: 13 | warn "Count of light sources is limited. Current count: ", rod_types.maxLightsCount 14 | 15 | proc removeLightSource*(v: SceneView, ls: LightSource) = 16 | if v.lightSources.isNil() or v.lightSources.len() <= 0: 17 | info "Current light sources count equals 0." 18 | else: 19 | v.lightSources.del(ls.node.name) 20 | 21 | proc `lightAmbient=`*(ls: LightSource, val: Coord) = 22 | ls.mLightAmbient = val 23 | ls.lightAmbientInited = true 24 | proc `lightDiffuse=`*(ls: LightSource, val: Coord) = 25 | ls.mLightDiffuse = val 26 | ls.lightDiffuseInited = true 27 | proc `lightSpecular=`*(ls: LightSource, val: Coord) = 28 | ls.mLightSpecular = val 29 | ls.lightSpecularInited = true 30 | proc `lightConstant=`*(ls: LightSource, val: Coord) = 31 | ls.mLightConstant = val 32 | ls.lightConstantInited = true 33 | proc `lightLinear=`*(ls: LightSource, val: Coord) = 34 | ls.mLightLinear = val 35 | ls.lightLinearInited = true 36 | proc `lightQuadratic=`*(ls: LightSource, val: Coord) = 37 | ls.mLightQuadratic = val 38 | ls.lightQuadraticInited = true 39 | proc `lightAttenuationInited=`*(ls: LightSource, val: bool) = 40 | ls.mLightAttenuationInited = val 41 | if ls.mLightAttenuationInited: 42 | ls.lightConstantInited = false 43 | ls.lightLinearInited = false 44 | ls.lightQuadraticInited = false 45 | else: 46 | ls.lightConstantInited = true 47 | ls.lightLinearInited = true 48 | ls.lightQuadraticInited = true 49 | proc `lightAttenuation=`*(ls: LightSource, val: Coord) = 50 | ls.mLightAttenuation = val 51 | proc `lightColor=`*(ls: LightSource, val: Color) = 52 | ls.mLightColor = val 53 | 54 | template lightAmbient*(ls: LightSource): Coord = ls.mLightAmbient 55 | template lightDiffuse*(ls: LightSource): Coord = ls.mLightDiffuse 56 | template lightSpecular*(ls: LightSource): Coord = ls.mLightSpecular 57 | template lightConstant*(ls: LightSource): Coord = ls.mLightConstant 58 | template lightLinear*(ls: LightSource): Coord = ls.mLightLinear 59 | template lightQuadratic*(ls: LightSource): Coord = ls.mLightQuadratic 60 | template lightAttenuation*(ls: LightSource): Coord = ls.mLightAttenuation 61 | template lightAttenuationInited*(ls: LightSource): bool = ls.mLightAttenuationInited 62 | template lightColor*(ls: LightSource): Color = ls.mLightColor 63 | 64 | proc setDefaultLightSource*(ls: LightSource) = 65 | ls.lightAmbient = 1.0 66 | ls.lightDiffuse = 1.0 67 | ls.lightSpecular = 1.0 68 | ls.lightConstant = 1.0 69 | ls.lightLinear = 0.000014 70 | ls.lightQuadratic = 0.00000007 71 | # ls.lightAttenuationInited = false 72 | ls.lightColor = newColor(1.0, 1.0, 1.0, 1.0) 73 | ls.lightAttenuation = 1.0 74 | ls.lightAttenuationInited = true 75 | 76 | method init*(ls: LightSource) = 77 | procCall ls.Component.init() 78 | ls.setDefaultLightSource() 79 | 80 | method componentNodeWasAddedToSceneView*(ls: LightSource) = 81 | ls.node.sceneView.addLightSource(ls) 82 | 83 | method componentNodeWillBeRemovedFromSceneView(ls: LightSource) = 84 | ls.node.sceneView.removeLightSource(ls) 85 | 86 | method getBBox*(ls: LightSource): BBox = 87 | result.minPoint = newVector3(-3, -3, -3) 88 | result.maxPoint = newVector3(3, 3, 3) 89 | 90 | method deserialize*(ls: LightSource, j: JsonNode, s: Serializer) = 91 | var v = j{"ambient"} 92 | if not v.isNil: 93 | ls.lightAmbient = v.getFloat() 94 | 95 | v = j{"diffuse"} 96 | if not v.isNil: 97 | ls.lightDiffuse = v.getFloat() 98 | 99 | v = j{"specular"} 100 | if not v.isNil: 101 | ls.lightSpecular = v.getFloat() 102 | 103 | v = j{"constant"} 104 | if not v.isNil: 105 | ls.lightConstant = v.getFloat() 106 | 107 | v = j{"linear"} 108 | if not v.isNil: 109 | ls.lightLinear = v.getFloat() 110 | 111 | v = j{"quadratic"} 112 | if not v.isNil: 113 | ls.lightQuadratic = v.getFloat() 114 | 115 | v = j{"is_precomp_attenuation"} 116 | if not v.isNil: 117 | ls.lightAttenuationInited = v.getBool() 118 | 119 | v = j{"attenuation"} 120 | if not v.isNil: 121 | ls.lightAttenuation = v.getFloat() 122 | 123 | v = j{"color"} 124 | if not v.isNil: 125 | ls.lightColor.r = v[0].getFloat() 126 | ls.lightColor.g = v[1].getFloat() 127 | ls.lightColor.b = v[2].getFloat() 128 | ls.lightColor.a = v[3].getFloat() 129 | 130 | method serialize*(c: LightSource, s: Serializer): JsonNode = 131 | result = newJObject() 132 | result.add("ambient", s.getValue(c.lightAmbient)) 133 | result.add("diffuse", s.getValue(c.lightDiffuse)) 134 | result.add("specular", s.getValue(c.lightSpecular)) 135 | result.add("constant", s.getValue(c.lightConstant)) 136 | result.add("linear", s.getValue(c.lightLinear)) 137 | result.add("quadratic", s.getValue(c.lightQuadratic)) 138 | result.add("is_precomp_attenuation", s.getValue(c.lightAttenuationInited)) 139 | result.add("attenuation", s.getValue(c.lightAttenuation)) 140 | result.add("color", s.getValue(c.lightColor)) 141 | 142 | method visitProperties*(ls: LightSource, p: var PropertyVisitor) = 143 | p.visitProperty("ambient", ls.lightAmbient) 144 | p.visitProperty("diffuse", ls.lightDiffuse) 145 | p.visitProperty("specular", ls.lightSpecular) 146 | p.visitProperty("constant", ls.lightConstant) 147 | p.visitProperty("linear", ls.lightLinear) 148 | p.visitProperty("quadratic", ls.lightQuadratic) 149 | 150 | p.visitProperty("precomp_att", ls.lightAttenuation) 151 | p.visitProperty("use_precomp", ls.lightAttenuationInited) 152 | 153 | p.visitProperty("color", ls.lightColor) 154 | 155 | registerComponent(LightSource) 156 | -------------------------------------------------------------------------------- /rod/component/nine_part_sprite.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ types, context, image, animation, property_visitor ] 2 | import rod / [rod_types, node, component, tools/serializer] 3 | import rod / utils / [ property_desc, serialization_codegen ] 4 | import rod/component/sprite 5 | import json 6 | 7 | type NinePartSprite* = ref object of Sprite 8 | mSize: Size 9 | segments: Vector4 10 | 11 | NinePartSprite.properties: 12 | mSize: 13 | serializationKey: "size" 14 | segments 15 | 16 | proc size*(s: NinePartSprite): Size = 17 | s.mSize 18 | 19 | proc `size=`*(s: NinePartSprite, v: Size) = 20 | s.mSize = v 21 | 22 | template marginLeft(s: NinePartSprite): float32 = s.segments.x 23 | template marginRight(s: NinePartSprite): float32 = s.segments.y 24 | template marginTop(s: NinePartSprite): float32 = s.segments.z 25 | template marginBottom(s: NinePartSprite): float32 = s.segments.w 26 | 27 | proc `image=`*(s: NinePartSprite, i: Image) = 28 | procCall s.Sprite.`image=`(i) 29 | s.segments.x = i.size.width * 0.4 30 | s.segments.y = i.size.width * 0.4 31 | s.segments.z = i.size.height * 0.4 32 | s.segments.w = i.size.height * 0.4 33 | 34 | s.size = i.size 35 | 36 | method getBBox*(s: NinePartSprite): BBox = 37 | let sz = s.size 38 | result.maxPoint = newVector3(sz.width + s.getOffset.x, sz.height + s.getOffset.y, 0.0) 39 | result.minPoint = newVector3(s.getOffset.x, s.getOffset.y, 0.0) 40 | 41 | method draw*(s: NinePartSprite) = 42 | let c = currentContext() 43 | let i = s.image 44 | if not i.isNil: 45 | var r: Rect 46 | r.origin = s.getOffset() 47 | r.size = s.size 48 | c.drawNinePartImage(i, r, s.marginLeft, s.marginTop, s.marginRight, s.marginBottom) 49 | 50 | # hack serializer/deserializer from sprite 51 | method serialize*(c: NinePartSprite, s: Serializer): JsonNode = 52 | result = procCall c.Sprite.serialize(s) 53 | var r2 = procCall c.Component.serialize(s) 54 | for k, v in r2: 55 | result[k] = v 56 | 57 | method deserialize*(c: NinePartSprite, j: JsonNode, s: Serializer) = 58 | procCall c.Sprite.deserialize(j, s) 59 | procCall c.Component.deserialize(j, s) 60 | 61 | genSerializationCodeForComponent(NinePartSprite) 62 | registerComponent(NinePartSprite) 63 | 64 | when defined(rodedit): 65 | type NinePartSegmentsAUX* = ref object 66 | segments*: Vector4 67 | image*: Image 68 | size*: Size 69 | 70 | proc segmentsAUX(s: NinePartSprite): NinePartSegmentsAUX = 71 | result = NinePartSegmentsAUX(segments: s.segments, image: s.image, size: s.size) 72 | 73 | proc `segmentsAUX=`*(s: NinePartSprite, v: NinePartSegmentsAUX) = 74 | s.segments = v.segments 75 | 76 | method visitProperties*(s: NinePartSprite, p: var PropertyVisitor) = 77 | p.visitProperty("image", s.image) 78 | p.visitProperty("size", s.size) 79 | 80 | when defined(rodedit): 81 | p.visitProperty("nine part", s.segmentsAUX) 82 | -------------------------------------------------------------------------------- /rod/component/overlay.nim: -------------------------------------------------------------------------------- 1 | import nimx/[types, composition, portable_gl, matrixes] 2 | import rod / component 3 | 4 | type Overlay* = ref object of RenderComponent 5 | 6 | var overlayPostEffect = newPostEffect(""" 7 | 8 | void overlay_effect(float spike, float spike1) { 9 | vec4 maskColor = gl_FragColor; 10 | gl_FragColor.rgba = vec4(maskColor.a); 11 | } 12 | """, "overlay_effect", ["float", "float"]) 13 | 14 | method beforeDraw*(o: Overlay, index: int): bool = 15 | let gl = currentContext().gl 16 | gl.blendFunc(gl.DST_COLOR, gl.ONE) 17 | 18 | pushPostEffect(overlayPostEffect, 0.0, 0.0) 19 | 20 | method afterDraw*(o: Overlay, index: int) = 21 | let gl = currentContext().gl 22 | gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA) 23 | popPostEffect() 24 | 25 | method supportsNewSerialization*(cm: Overlay): bool = true 26 | 27 | registerComponent(Overlay, "Effects") 28 | -------------------------------------------------------------------------------- /rod/component/primitives/cone.nim: -------------------------------------------------------------------------------- 1 | import nimx/[matrixes, types, property_visitor] 2 | import rod / utils / [ property_desc, serialization_codegen ] 3 | import rod/[vertex_data_info, component] 4 | import rod/component/[ mesh_component, material ] 5 | import rod/tools/serializer 6 | import opengl, math, json 7 | 8 | type ConeComponent* = ref object of MeshComponent 9 | mRadius1: float32 10 | mRadius2: float32 11 | mHeight: float32 12 | mSegments: int32 13 | 14 | ConeComponent.properties: 15 | mRadius1 16 | mRadius2 17 | mHeight 18 | mSegments 19 | 20 | proc fillBuffers(c: ConeComponent, vertCoords, texCoords, normals: var seq[float32], indices: var seq[GLushort]) = 21 | let angle_step = 2.0 * PI.float32 / c.mSegments.float32 22 | var angle = 0.0 23 | var v0, v1, v2, v3: Vector3 24 | var index_offset = 0 25 | 26 | # cone body 27 | while angle <= 2 * PI + angle_step: 28 | v0 = newVector(c.mRadius1 * cos(angle).float32, 0.0, c.mRadius1 * sin(angle)) 29 | v1 = newVector(c.mRadius2 * cos(angle).float32, c.mHeight, c.mRadius2 * sin(angle)) 30 | vertCoords.add(v0) 31 | vertCoords.add(v1) 32 | 33 | texCoords.add([0.0.float32, 0.0]) 34 | texCoords.add([1.0.float32, 1.0]) 35 | 36 | normals.add([cos(angle).float32, 0.0, sin(angle)]) 37 | normals.add([cos(angle).float32, 0.0, sin(angle)]) 38 | 39 | angle += angle_step 40 | 41 | for i in 0 ..< c.mSegments: 42 | indices.add([(2*i + 0).GLushort, (2*i + 1).GLushort, (2*i + 2).GLushort]) 43 | indices.add([(2*i + 3).GLushort, (2*i + 2).GLushort, (2*i + 1).GLushort]) 44 | 45 | index_offset = int(vertCoords.len() / 3) 46 | 47 | # # cap bottom 48 | # # central vertex 49 | vertCoords.add([0.0.float32, 0.0, 0.0]) 50 | texCoords.add([0.0.float32, 0.0]) 51 | normals.add([0.0.float32, -1.0, 0.0]) 52 | angle = 0.0 53 | 54 | for i in 0 .. c.mSegments: 55 | vertCoords.add([c.mRadius1 * cos(angle).float32, 0.0, c.mRadius1 * sin(angle)]) 56 | texCoords.add([0.0.float32, 0.0]) 57 | normals.add([0.0.float32, -1.0, 0.0]) 58 | angle += angle_step 59 | 60 | if i < c.mSegments: 61 | indices.add([index_offset.GLushort, (i + index_offset + 1).GLushort, (i + index_offset + 2).GLushort]) 62 | 63 | index_offset = int(vertCoords.len() / 3) 64 | 65 | # cap top 66 | # central vertex 67 | vertCoords.add([0.0.float32, c.mHeight, 0.0]) 68 | texCoords.add([0.0.float32, 0.0]) 69 | normals.add([0.0.float32, 1.0, 0.0]) 70 | angle = 0.0 71 | 72 | for i in 0 .. c.mSegments: 73 | vertCoords.add([c.mRadius2 * cos(angle).float32, c.mHeight, c.mRadius2 * sin(angle)]) 74 | texCoords.add([0.0.float32, 0.0]) 75 | normals.add([0.0.float32, 1.0, 0.0]) 76 | angle += angle_step 77 | 78 | if i < c.mSegments: 79 | indices.add([index_offset.GLushort, (i + index_offset + 2).GLushort, (i + index_offset + 1).GLushort]) 80 | 81 | 82 | method init*(c: ConeComponent) = 83 | procCall c.MeshComponent.init() 84 | c.mRadius1 = 1.0 85 | c.mRadius2 = 1.0 86 | c.mHeight = 4.0 87 | c.mSegments = 12 88 | 89 | c.material.ambient = newColor(1.0, 1.0, 1.0, 0.2) 90 | c.material.diffuse = newColor(1.0, 1.0, 1.0, 1.0) 91 | 92 | 93 | proc generateMesh*(c: ConeComponent) = 94 | let mesh = c 95 | 96 | var vertCoords = newSeq[float32]() 97 | var texCoords = newSeq[float32]() 98 | var normals = newSeq[float32]() 99 | var indices = newSeq[GLushort]() 100 | 101 | c.fillBuffers(vertCoords, texCoords, normals, indices) 102 | 103 | mesh.vboData.minCoord = newVector3(high(int).Coord, high(int).Coord, high(int).Coord) 104 | mesh.vboData.maxCoord = newVector3(low(int).Coord, low(int).Coord, low(int).Coord) 105 | mesh.vboData.vertInfo = newVertexInfoWithVertexData(vertCoords.len, texCoords.len, normals.len, 0) 106 | 107 | let stride = int32( mesh.vboData.vertInfo.stride / sizeof(GLfloat) ) 108 | let size = int32(vertCoords.len * stride / 3) 109 | var vertexData = c.createVertexData(stride, size, vertCoords, texCoords, normals, @[]) 110 | 111 | mesh.createVBO(indices, vertexData) 112 | 113 | template radius1*(cc: ConeComponent): float = c.mRadius1 114 | template `radius1=`*(cc: ConeComponent, v: float) = 115 | cc.mRadius1 = v 116 | cc.generateMesh() 117 | 118 | template radius2*(cc: ConeComponent): float = c.mRadius2 119 | template `radius2=`*(cc: ConeComponent, v: float) = 120 | cc.mRadius2 = v 121 | cc.generateMesh() 122 | 123 | template height*(cc: ConeComponent): float = c.mHeight 124 | template `height=`*(cc: ConeComponent, v: float) = 125 | cc.mHeight = v 126 | cc.generateMesh() 127 | 128 | template segments*(cc: ConeComponent): int = c.mSegments.int 129 | template `segments=`*(cc: ConeComponent, v: int) = 130 | cc.mSegments = v.int32 131 | cc.generateMesh() 132 | 133 | method componentNodeWasAddedToSceneView*(c: ConeComponent) = 134 | c.generateMesh() 135 | 136 | method deserialize*(c: ConeComponent, j: JsonNode, s: Serializer) = 137 | if j.isNil: 138 | return 139 | 140 | s.deserializeValue(j, "radius1", c.radius1) 141 | s.deserializeValue(j, "radius2", c.radius2) 142 | s.deserializeValue(j, "height", c.height) 143 | s.deserializeValue(j, "segments", c.segments) 144 | 145 | method serialize*(c: ConeComponent, s: Serializer): JsonNode = 146 | result = newJObject() 147 | result.add("radius1", s.getValue(c.radius1)) 148 | result.add("radius2", s.getValue(c.radius2)) 149 | result.add("height", s.getValue(c.height)) 150 | result.add("segments", s.getValue(c.segments)) 151 | 152 | method visitProperties*(c: ConeComponent, p: var PropertyVisitor) = 153 | p.visitProperty("radius1", c.radius1) 154 | p.visitProperty("radius2", c.radius2) 155 | p.visitProperty("height", c.height) 156 | p.visitProperty("segments", c.segments) 157 | procCall c.MeshComponent.visitProperties(p) 158 | 159 | genSerializationCodeForComponent(ConeComponent) 160 | registerComponent(ConeComponent, "Primitives") 161 | -------------------------------------------------------------------------------- /rod/component/primitives/cube.nim: -------------------------------------------------------------------------------- 1 | import nimx/[matrixes, types, property_visitor] 2 | import rod / utils / [ property_desc, serialization_codegen ] 3 | import rod/[vertex_data_info, component] 4 | import rod/component/[ mesh_component, material ] 5 | import rod/tools/serializer 6 | import opengl, json 7 | 8 | type CubeComponent* = ref object of MeshComponent 9 | mSize: Vector3 10 | 11 | CubeComponent.properties: 12 | mSize 13 | 14 | proc fillVertexBuffers(vertCoords, texCoords, normals: var seq[float32], size: Vector3) = 15 | #front 16 | vertCoords.add([-size.x, -size.y, -size.z]) 17 | vertCoords.add([-size.x, size.y, -size.z]) 18 | vertCoords.add([ size.x, size.y, -size.z]) 19 | vertCoords.add([ size.x, -size.y, -size.z]) 20 | 21 | for i in 0 .. 3: 22 | normals.add([ 0.0f, 0.0f, -1.0f]) 23 | 24 | texCoords.add([ 0.0f, 0.0f]) 25 | texCoords.add([ 1.0f, 0.0f]) 26 | texCoords.add([ 1.0f, 1.0f]) 27 | texCoords.add([ 0.0f, 1.0f]) 28 | 29 | #right 30 | vertCoords.add([ size.x, -size.y, -size.z]) 31 | vertCoords.add([ size.x, size.y, -size.z]) 32 | vertCoords.add([ size.x, size.y, size.z]) 33 | vertCoords.add([ size.x, -size.y, size.z]) 34 | 35 | for i in 0 .. 3: 36 | normals.add([ 1.0f, 0.0f, 0.0f]) 37 | 38 | texCoords.add([ 0.0f, 0.0f]) 39 | texCoords.add([ 1.0f, 0.0f]) 40 | texCoords.add([ 1.0f, 1.0f]) 41 | texCoords.add([ 0.0f, 1.0f]) 42 | 43 | #back 44 | vertCoords.add([ size.x, -size.y, size.z]) 45 | vertCoords.add([ size.x, size.y, size.z]) 46 | vertCoords.add([-size.x, size.y, size.z]) 47 | vertCoords.add([-size.x, -size.y, size.z]) 48 | 49 | for i in 0 .. 3: 50 | normals.add([ 0.0f, 0.0f, 1.0f]) 51 | 52 | texCoords.add([ 0.0f, 0.0f]) 53 | texCoords.add([ 1.0f, 0.0f]) 54 | texCoords.add([ 1.0f, 1.0f]) 55 | texCoords.add([ 0.0f, 1.0f]) 56 | 57 | #left 58 | vertCoords.add([-size.x, -size.y, size.z]) 59 | vertCoords.add([-size.x, size.y, size.z]) 60 | vertCoords.add([-size.x, size.y, -size.z]) 61 | vertCoords.add([-size.x, -size.y, -size.z]) 62 | 63 | for i in 0 .. 3: 64 | normals.add([-1.0f, 0.0f, 1.0f]) 65 | 66 | texCoords.add([ 0.0f, 0.0f]) 67 | texCoords.add([ 1.0f, 0.0f]) 68 | texCoords.add([ 1.0f, 1.0f]) 69 | texCoords.add([ 0.0f, 1.0f]) 70 | 71 | #top 72 | vertCoords.add([-size.x, size.y, -size.z]) 73 | vertCoords.add([-size.x, size.y, size.z]) 74 | vertCoords.add([ size.x, size.y, size.z]) 75 | vertCoords.add([ size.x, size.y, -size.z]) 76 | 77 | for i in 0 .. 3: 78 | normals.add([0.0f, 1.0f, 0.0f]) 79 | 80 | texCoords.add([ 0.0f, 0.0f]) 81 | texCoords.add([ 1.0f, 0.0f]) 82 | texCoords.add([ 1.0f, 1.0f]) 83 | texCoords.add([ 0.0f, 1.0f]) 84 | 85 | #bottom 86 | vertCoords.add([-size.x, -size.y, size.z]) 87 | vertCoords.add([-size.x, -size.y, -size.z]) 88 | vertCoords.add([ size.x, -size.y, -size.z]) 89 | vertCoords.add([ size.x, -size.y, size.z]) 90 | 91 | for i in 0 .. 3: 92 | normals.add([0.0f, -1.0f, 0.0f]) 93 | 94 | texCoords.add([ 0.0f, 0.0f]) 95 | texCoords.add([ 1.0f, 0.0f]) 96 | texCoords.add([ 1.0f, 1.0f]) 97 | texCoords.add([ 0.0f, 1.0f]) 98 | 99 | proc fillIndexBuffer(indices: var seq[GLushort]) = 100 | #front 101 | indices.add([0.GLushort, 1, 2]) 102 | indices.add([3.GLushort, 0, 2]) 103 | 104 | #right 105 | indices.add([4.GLushort, 5, 6]) 106 | indices.add([7.GLushort, 4, 6]) 107 | 108 | #back 109 | indices.add([8.GLushort, 9, 10]) 110 | indices.add([11.GLushort, 8, 10]) 111 | 112 | #left 113 | indices.add([12.GLushort, 13, 14]) 114 | indices.add([15.GLushort, 12, 14]) 115 | 116 | #top 117 | indices.add([16.GLushort, 17, 18]) 118 | indices.add([19.GLushort, 16, 18]) 119 | 120 | #bottom 121 | indices.add([20.GLushort, 21, 22]) 122 | indices.add([23.GLushort, 20, 22]) 123 | 124 | method init*(c: CubeComponent) = 125 | procCall c.MeshComponent.init() 126 | c.mSize = newVector3(1.0, 1.0, 1.0) 127 | 128 | c.material.ambient = newColor(1.0, 1.0, 1.0, 0.2) 129 | c.material.diffuse = newColor(1.0, 1.0, 1.0, 1.0) 130 | 131 | proc generateMesh(c: CubeComponent) = 132 | let mesh = c 133 | var vertCoords = newSeq[float32]() 134 | var texCoords = newSeq[float32]() 135 | var normals = newSeq[float32]() 136 | var indices = newSeq[GLushort]() 137 | 138 | fillVertexBuffers(vertCoords, texCoords, normals, c.mSize) 139 | fillIndexBuffer(indices) 140 | 141 | mesh.vboData.minCoord = newVector3(high(int).Coord, high(int).Coord, high(int).Coord) 142 | mesh.vboData.maxCoord = newVector3(low(int).Coord, low(int).Coord, low(int).Coord) 143 | mesh.vboData.vertInfo = newVertexInfoWithVertexData(vertCoords.len, texCoords.len, normals.len, 0) 144 | 145 | let stride = int32( mesh.vboData.vertInfo.stride / sizeof(GLfloat) ) 146 | let mSize = int32(vertCoords.len * stride / 3) 147 | var vertexData = c.createVertexData(stride, mSize, vertCoords, texCoords, normals, @[]) 148 | mesh.createVBO(indices, vertexData) 149 | 150 | proc size*(cc: CubeComponent): Vector3 = cc.mSize 151 | proc `size=`*(cc: CubeComponent, v: Vector3) = 152 | cc.mSize = v 153 | cc.generateMesh() 154 | 155 | method componentNodeWasAddedToSceneView*(c: CubeComponent) = 156 | c.generateMesh() 157 | 158 | method deserialize*(c: CubeComponent, j: JsonNode, s: Serializer) = 159 | if j.isNil: 160 | return 161 | 162 | s.deserializeValue(j, "size", c.mSize) 163 | 164 | method serialize*(c: CubeComponent, s: Serializer): JsonNode = 165 | result = newJObject() 166 | result.add("size", s.getValue(c.mSize)) 167 | 168 | method visitProperties*(c: CubeComponent, p: var PropertyVisitor) = 169 | p.visitProperty("size", c.size) 170 | procCall c.MeshComponent.visitProperties(p) 171 | 172 | genSerializationCodeForComponent(CubeComponent) 173 | registerComponent(CubeComponent, "Primitives") 174 | -------------------------------------------------------------------------------- /rod/component/primitives/sphere.nim: -------------------------------------------------------------------------------- 1 | import nimx/[matrixes, types, property_visitor] 2 | import rod / utils / [ property_desc, serialization_codegen ] 3 | import rod/[vertex_data_info, component] 4 | import rod/component/[ mesh_component, material ] 5 | import rod/tools/serializer 6 | import opengl, json 7 | 8 | type SphereComponent* = ref object of MeshComponent 9 | radius: float32 10 | segments: int32 11 | 12 | SphereComponent.properties: 13 | radius 14 | segments 15 | 16 | proc fillBuffers(c: SphereComponent, vertCoords, texCoords, normals: var seq[float32], indices: var seq[GLushort]) = 17 | let segments = c.segments 18 | let segStep = 3.14 * 2.0f / float(segments * 2) 19 | var mR: Matrix4 20 | var tPos: Vector3 21 | 22 | for i in 0 .. segments * 2: 23 | vertCoords.add([ 0.0f, c.radius, 0.0f]) 24 | normals.add([ 0.0f, 1.0f, 0.0f]) 25 | var tx:float32 = (1.0f / (segments.float32 * 2.0)) * (segments.float32 * 2.0 - i.float32) - (0.5f / (segments.float32 * 2.0)) 26 | texCoords.add([tx, 0.0f]) 27 | 28 | for i in 0 .. segments * 2: 29 | vertCoords.add([ 0.0f, -c.radius, 0.0f]) 30 | normals.add([ 0.0f, -1.0f, 0.0f]) 31 | var tx: float32 = (1.0f / (segments.float32 * 2.0)) * (segments.float32 * 2.0 - i.float32) - (0.5f / (segments.float32 * 2.0)) 32 | texCoords.add([tx, 1.0f]) 33 | 34 | for i in 1 .. segments: 35 | var startVertex = int(vertCoords.len() / 3) 36 | 37 | for j in 0 .. segments * 2 + 1: 38 | mR.loadIdentity() 39 | mR.rotateY(segStep * j.float32) 40 | mR.rotateX(segStep * i.float32) 41 | var vec = newVector3(0.0, c.radius, 0.0) 42 | tPos = mR * vec 43 | 44 | vertCoords.add([tPos.x, tPos.y, tPos.z]) 45 | var norm = tPos.normalized() 46 | normals.add([norm.x, norm.y, norm.z]) 47 | var tx: float32 = (1.0f / (segments.float32 * 2.0)) * (segments.float32 * 2.0 - j.float32) 48 | var ty: float32 = (1.0f / segments.float32) * i.float32 49 | texCoords.add([tx, ty]) 50 | 51 | if i == 1: 52 | if j != segments * 2: 53 | indices.add(GLushort(startVertex + (j + 1))) 54 | indices.add(j.GLushort) 55 | indices.add(GLushort(startVertex + (j + 0))) 56 | 57 | if i == segments - 1: 58 | if j != segments * 2: 59 | indices.add(GLushort(j + segments*2)) 60 | indices.add(GLushort(startVertex + (j + 1))) 61 | indices.add(GLushort(startVertex + (j + 0))) 62 | 63 | if i != 1 and segments != 2: 64 | if j != segments * 2: 65 | indices.add(GLushort(startVertex + (j + 1))) 66 | indices.add(GLushort(startVertex - (segments * 2 + 1) + (j + 1))) 67 | indices.add(GLushort(startVertex - (segments * 2 + 1) + (j + 0))) 68 | indices.add(GLushort(startVertex + (j + 0))) 69 | indices.add(GLushort(startVertex + (j + 1))) 70 | indices.add(GLushort(startVertex - (segments * 2 + 1) + (j + 0))) 71 | 72 | method init*(c: SphereComponent) = 73 | procCall c.MeshComponent.init() 74 | c.radius = 1.0 75 | c.segments = 15 76 | 77 | proc generateMesh*(c: SphereComponent) = 78 | let mesh = c 79 | 80 | var vertCoords = newSeq[float32]() 81 | var texCoords = newSeq[float32]() 82 | var normals = newSeq[float32]() 83 | var indices = newSeq[GLushort]() 84 | 85 | c.fillBuffers(vertCoords, texCoords, normals, indices) 86 | 87 | mesh.vboData.minCoord = newVector3(high(int).Coord, high(int).Coord, high(int).Coord) 88 | mesh.vboData.maxCoord = newVector3(low(int).Coord, low(int).Coord, low(int).Coord) 89 | mesh.vboData.vertInfo = newVertexInfoWithVertexData(vertCoords.len, texCoords.len, normals.len, 0) 90 | 91 | let stride = int32( mesh.vboData.vertInfo.stride / sizeof(GLfloat) ) 92 | let size = int32(vertCoords.len * stride / 3) 93 | var vertexData = c.createVertexData(stride, size, vertCoords, texCoords, normals, @[]) 94 | 95 | mesh.createVBO(indices, vertexData) 96 | 97 | mesh.material.ambient = newColor(1.0, 1.0, 1.0, 0.2) 98 | mesh.material.diffuse = newColor(1.0, 1.0, 1.0, 1.0) 99 | 100 | method componentNodeWasAddedToSceneView*(c: SphereComponent) = 101 | c.generateMesh() 102 | 103 | method deserialize*(c: SphereComponent, j: JsonNode, s: Serializer) = 104 | if j.isNil: 105 | return 106 | 107 | s.deserializeValue(j, "radius", c.radius) 108 | s.deserializeValue(j, "segments", c.segments) 109 | 110 | method serialize*(c: SphereComponent, s: Serializer): JsonNode = 111 | result = newJObject() 112 | result.add("radius", s.getValue(c.radius)) 113 | result.add("segments", s.getValue(c.segments)) 114 | 115 | method visitProperties*(c: SphereComponent, p: var PropertyVisitor) = 116 | template radiusAux(cc: SphereComponent): float = c.radius 117 | template `radiusAux=`(cc: SphereComponent, v: float) = 118 | cc.radius = v 119 | cc.generateMesh() 120 | 121 | template segmentsAux(cc: SphereComponent): int32 = c.segments 122 | template `segmentsAux=`(cc: SphereComponent, v: int32) = 123 | cc.segments = v 124 | cc.generateMesh() 125 | 126 | p.visitProperty("radius", c.radiusAux) 127 | p.visitProperty("segments", c.segmentsAux) 128 | procCall c.MeshComponent.visitProperties(p) 129 | 130 | genSerializationCodeForComponent(SphereComponent) 131 | registerComponent(SphereComponent, "Primitives") 132 | -------------------------------------------------------------------------------- /rod/component/solid.nim: -------------------------------------------------------------------------------- 1 | import nimx/[types, context, matrixes, property_visitor] 2 | import rod / utils / [ property_desc, serialization_codegen ] 3 | import rod/[rod_types, component] 4 | import rod/tools/serializer 5 | import json 6 | 7 | type Solid* = ref object of RenderComponent 8 | size*: Size 9 | color*: Color 10 | 11 | Solid.properties: 12 | size 13 | color 14 | 15 | method init*(s: Solid) = 16 | s.color = whiteColor() 17 | s.size = newSize(10, 10) 18 | 19 | method deserialize*(s: Solid, j: JsonNode, serializer: Serializer) = 20 | var v = j{"color"} 21 | if not v.isNil: 22 | s.color = newColor(v[0].getFloat(), v[1].getFloat(), v[2].getFloat()) 23 | v = j{"alpha"} # Deprecated. 24 | if not v.isNil: 25 | s.node.alpha = v.getFloat(1.0) 26 | 27 | v = j{"size"} 28 | if not v.isNil: 29 | s.size = newSize(v[0].getFloat(), v[1].getFloat()) 30 | 31 | genSerializationCodeForComponent(Solid) 32 | 33 | method draw*(s: Solid) = 34 | let c = currentContext() 35 | var r: Rect 36 | r.size = s.size 37 | c.fillColor = s.color 38 | c.strokeWidth = 0 39 | c.drawRect(r) 40 | 41 | method getBBox*(s: Solid): BBox = 42 | result.minPoint = newVector3(0.0, 0.0, 0.0) 43 | result.maxPoint = newVector3(s.size.width, s.size.height, 0.0) 44 | 45 | method visitProperties*(c: Solid, p: var PropertyVisitor) = 46 | p.visitProperty("size", c.size) 47 | p.visitProperty("color", c.color) 48 | 49 | method serialize*(c: Solid, s: Serializer): JsonNode = 50 | result = newJObject() 51 | result.add("size", s.getValue(c.size)) 52 | result.add("color", s.getValue(c.color)) 53 | 54 | registerComponent(Solid) 55 | -------------------------------------------------------------------------------- /rod/component/sprite.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ types, context, image, animation, property_visitor ] 2 | import rod / [rod_types, node, component, tools/serializer] 3 | import rod / utils / [ property_desc, serialization_codegen ] 4 | 5 | import json, logging 6 | 7 | type Sprite* = ref object of RenderComponent 8 | offset*: Point 9 | frameOffsets*: seq[Point] 10 | images*: seq[Image] 11 | mCurrentFrame*: int16 12 | 13 | Sprite.properties: 14 | images: 15 | serializationKey: "fileNames" 16 | combinedWith: frameOffsets 17 | mCurrentFrame: 18 | serializationKey: "currentFrame" 19 | offset 20 | 21 | template `currentFrame`*(s: Sprite): int = 22 | int(s.mCurrentFrame) 23 | 24 | template `currentFrame=`*(s: Sprite, v: int) = 25 | s.mCurrentFrame = int16(v) 26 | 27 | proc image*(s: Sprite): Image = 28 | if s.images.len > s.currentFrame and s.currentFrame >= 0: 29 | result = s.images[s.currentFrame] 30 | 31 | proc `image=`*(s: Sprite, i: Image) = 32 | s.images.setLen(1) 33 | s.images[0] = i 34 | s.currentFrame = 0 35 | 36 | proc getOffset*(s: Sprite): Point = 37 | result = s.offset 38 | if s.frameOffsets.len > s.currentFrame and s.currentFrame >= 0: 39 | result += s.frameOffsets[s.currentFrame] 40 | 41 | proc calculatedSize(s: Sprite): Size = 42 | ## If size is zeroSize - return image size. 43 | if not s.image.isNil: 44 | result = s.image.size 45 | 46 | proc effectiveSize*(s: Sprite): Size = 47 | result = s.calculatedSize() 48 | 49 | method draw*(s: Sprite) = 50 | let c = currentContext() 51 | 52 | let i = s.image 53 | if not i.isNil: 54 | var r: Rect 55 | r.origin = s.getOffset() 56 | r.size = s.calculatedSize() 57 | c.drawImage(i, r) 58 | 59 | proc createFrameAnimation(s: Sprite) {.inline.} = 60 | let a = newAnimation() 61 | const fps = 1.0 / 30.0 62 | a.loopDuration = float(s.images.len) * fps 63 | a.continueUntilEndOfLoopOnCancel = true 64 | a.onAnimate = proc(p: float) = 65 | s.currentFrame = int16(float(s.images.len - 1) * p) 66 | s.node.registerAnimation("sprite", a) 67 | 68 | method getBBox*(s: Sprite): BBox = 69 | let sz = s.effectiveSize() 70 | result.maxPoint = newVector3(sz.width + s.getOffset.x, sz.height + s.getOffset.y, 0.0) 71 | result.minPoint = newVector3(s.getOffset.x, s.getOffset.y, 0.0) 72 | 73 | proc awake(c: Sprite) = 74 | if c.images.len > 1: 75 | c.createFrameAnimation() 76 | 77 | genSerializationCodeForComponent(Sprite) 78 | 79 | template curFrameAux(t: Sprite): int16 = t.mCurrentFrame 80 | template `curFrameAux=`(t: Sprite, f: int16) = t.mCurrentFrame = f 81 | 82 | method visitProperties*(t: Sprite, p: var PropertyVisitor) = 83 | p.visitProperty("image", t.image) 84 | p.visitProperty("curFrame", t.curFrameAux) 85 | p.visitProperty("offset", t.offset) 86 | 87 | registerComponent(Sprite) 88 | -------------------------------------------------------------------------------- /rod/component/tint.nim: -------------------------------------------------------------------------------- 1 | import nimx/[types, context, composition, portable_gl, property_visitor] 2 | import rod / utils / [ property_desc, serialization_codegen ] 3 | import rod/[component, tools/serializer] 4 | import json 5 | 6 | 7 | type Tint* = ref object of RenderComponent 8 | black*: Color 9 | white*: Color 10 | amount*: float32 11 | 12 | Tint.properties: 13 | black 14 | white 15 | amount 16 | 17 | var effect = newPostEffect(""" 18 | void tint_effect(vec4 black, vec4 white, float amount) { 19 | float b = (0.2126*gl_FragColor.r + 0.7152*gl_FragColor.g + 0.0722*gl_FragColor.b); // Maybe the koeffs should be adjusted 20 | float a = gl_FragColor.a; 21 | vec4 res = mix(black, white, b); 22 | res.a *= a; 23 | gl_FragColor = mix(gl_FragColor, res, amount); 24 | } 25 | """, "tint_effect", ["vec4", "vec4", "float"]) 26 | 27 | method deserialize*(c: Tint, j: JsonNode, s: Serializer) = 28 | var v = j["black"] 29 | c.black = newColor(v[0].getFloat(), v[1].getFloat(), v[2].getFloat(), v[3].getFloat()) 30 | v = j["white"] 31 | c.white = newColor(v[0].getFloat(), v[1].getFloat(), v[2].getFloat(), v[3].getFloat()) 32 | c.amount = j{"amount"}.getFloat(1) 33 | 34 | method beforeDraw*(c: Tint, index: int): bool = 35 | pushPostEffect(effect, c.black, c.white, c.amount) 36 | 37 | method afterDraw*(c: Tint, index: int) = 38 | popPostEffect() 39 | 40 | method visitProperties*(c: Tint, p: var PropertyVisitor) = 41 | p.visitProperty("black", c.black) 42 | p.visitProperty("white", c.white) 43 | p.visitProperty("amount", c.amount) 44 | 45 | genSerializationCodeForComponent(Tint) 46 | registerComponent(Tint, "Effects") 47 | -------------------------------------------------------------------------------- /rod/component/ui_component.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ view, matrixes, view_event_handling, property_visitor ] 2 | import rod / [ component, ray, viewport, node, rod_types ] 3 | import logging 4 | export UIComponent 5 | 6 | type UICompView = ref object of View 7 | uiComp: UIComponent 8 | uiCompSubview: View 9 | 10 | method `enabled=`*(c: UIComponent, state: bool) {.base.}= 11 | c.mEnabled = state 12 | 13 | proc enabled*(c: UIComponent): bool = 14 | result = c.mEnabled 15 | 16 | proc view*(c: UIComponent): View = 17 | if not c.mView.isNil: 18 | result = c.mView.UICompView.uiCompSubview 19 | 20 | proc intersectsWithUIPlane(uiComp: UIComponent, r: Ray, res: var Vector3): bool= 21 | let n = uiComp.node 22 | let worldPointOnPlane = n.localToWorld(newVector3()) 23 | var worldNormal = n.localToWorld(newVector3(0, 0, 1)) 24 | worldNormal -= worldPointOnPlane 25 | worldNormal.normalize() 26 | result = r.intersectWithPlane(worldNormal, worldPointOnPlane, res) 27 | 28 | proc intersectsWithUINode*(uiComp: UIComponent, r: Ray, res: var Vector3): bool = 29 | if uiComp.intersectsWithUIPlane(r, res) and not uiComp.mView.isNil: 30 | let v = uiComp.view 31 | if not v.isNil: 32 | var localres : Vector3 33 | if uiComp.node.tryWorldToLocal(res, localres): 34 | result = localres.x >= v.frame.x and localres.x <= v.frame.maxX and 35 | localres.y >= v.frame.y and localres.y <= v.frame.maxY 36 | 37 | method convertPointToParent*(v: UICompView, p: Point): Point = 38 | result = newPoint(-9999999, -9999999) # Some ridiculous value 39 | warn "UICompView.convertPointToParent not implemented" 40 | 41 | method convertPointFromParent*(v: UICompView, p: Point): Point = 42 | result = newPoint(-9999999, -9999999) # Some ridiculous value 43 | if not v.uiComp.node.sceneView.isNil: 44 | let r = v.uiComp.node.sceneView.rayWithScreenCoords(p) 45 | var res : Vector3 46 | if v.uiComp.intersectsWithUIPlane(r, res): 47 | if v.uiComp.node.tryWorldToLocal(res, res): 48 | result = newPoint(res.x, res.y) 49 | 50 | method draw*(c: UIComponent) = 51 | if not c.mView.isNil: 52 | c.mView.recursiveDrawSubviews() 53 | 54 | proc updSuperview(c: UIComponent) = 55 | if not c.mView.isNil and not c.node.sceneView.isNil: 56 | c.mView.superview = c.node.sceneView 57 | c.mView.window = c.node.sceneView.window 58 | c.mView.addSubview(c.mView.UICompView.uiCompSubview) 59 | 60 | proc `view=`*(c: UIComponent, v: View) = 61 | if not c.view.isNil: 62 | c.view.removeFromSuperview() 63 | 64 | if v == nil: 65 | c.mView = nil 66 | return 67 | 68 | let cv = UICompView.new(newRect(0, 0, 20, 20)) 69 | cv.uiComp = c 70 | c.mView = cv 71 | c.enabled = true 72 | cv.uiCompSubview = v 73 | v.name = c.node.name 74 | c.updSuperview() 75 | 76 | proc moveToWindow(v: View, w: Window) = 77 | v.window = w 78 | for s in v.subviews: 79 | s.moveToWindow(w) 80 | 81 | proc handleScrollEv*(c: UIComponent, r: Ray, e: var Event, intersection: Vector3): bool = 82 | var res : Vector3 83 | if c.node.tryWorldToLocal(intersection, res): 84 | let v = c.view 85 | let tmpLocalPosition = e.localPosition 86 | e.localPosition = v.convertPointFromParent(newPoint(res.x, res.y)) 87 | if e.localPosition.inRect(v.bounds): 88 | result = v.processMouseWheelEvent(e) 89 | 90 | e.localPosition = tmpLocalPosition 91 | 92 | proc handleTouchEv*(c: UIComponent, r: Ray, e: var Event, intersection: Vector3): bool = 93 | var res : Vector3 94 | if c.node.tryWorldToLocal(intersection, res): 95 | let v = c.view 96 | let tmpLocalPosition = e.localPosition 97 | e.localPosition = v.convertPointFromParent(newPoint(res.x, res.y)) 98 | if e.localPosition.inRect(v.bounds): 99 | result = v.processTouchEvent(e) 100 | if result and e.buttonState == bsDown: 101 | c.mView.touchTarget = v 102 | 103 | e.localPosition = tmpLocalPosition 104 | 105 | proc sceneViewWillMoveToWindow*(c: UIComponent, w: Window) = 106 | if not c.mView.isNil: 107 | c.mView.viewWillMoveToWindow(w) 108 | c.mView.moveToWindow(w) 109 | 110 | method componentNodeWasAddedToSceneView*(ui: UIComponent) = 111 | let sv = ui.node.sceneView 112 | sv.uiComponents.add(ui) 113 | 114 | ui.updSuperview() 115 | 116 | method componentNodeWillBeRemovedFromSceneView(ui: UIComponent) = 117 | let sv = ui.node.sceneView 118 | let i = sv.uiComponents.find(ui) 119 | if i != -1: 120 | sv.uiComponents.del(i) 121 | 122 | if not ui.view.isNil: 123 | ui.view.removeFromSuperview() 124 | 125 | method visitProperties*(ui: UIComponent, p: var PropertyVisitor) = 126 | p.visitProperty("enabled", ui.enabled) 127 | if not ui.view.isNil: 128 | ui.view.visitProperties(p) 129 | 130 | registerComponent(UIComponent) 131 | -------------------------------------------------------------------------------- /rod/component/vector_shape.nim: -------------------------------------------------------------------------------- 1 | import nimx/[types, context, matrixes, property_visitor] 2 | import rod / utils / [ property_desc, serialization_codegen ] 3 | import rod / [ rod_types, component, tools/serializer] 4 | import json 5 | 6 | type VectorShapeType* = enum 7 | vsRectangle 8 | vsEllipse 9 | vsStar 10 | 11 | type VectorShape* = ref object of RenderComponent 12 | color*: Color 13 | strokeColor*: Color 14 | size*: Size 15 | strokeWidth*: float32 16 | radius*: float32 17 | shapeType*: VectorShapeType 18 | 19 | VectorShape.properties: 20 | size 21 | color 22 | strokeWidth 23 | strokeColor 24 | radius 25 | shapeType 26 | 27 | genSerializationCodeForComponent(VectorShape) 28 | 29 | method init*(vs: VectorShape) = 30 | vs.color = whiteColor() 31 | vs.size = newSize(100, 100) 32 | vs.strokeWidth = 0 33 | vs.strokeColor = whiteColor() 34 | vs.radius = 0 35 | 36 | method serialize*(vs: VectorShape, s: Serializer): JsonNode = 37 | result = newJObject() 38 | result.add("size", s.getValue(vs.size)) 39 | result.add("color", s.getValue(vs.color)) 40 | result.add("strokeWidth", s.getValue(vs.strokeWidth)) 41 | result.add("strokeColor", s.getValue(vs.strokeColor)) 42 | result.add("radius", s.getValue(vs.radius)) 43 | result.add("shapeType", s.getValue(vs.shapeType)) 44 | 45 | method deserialize*(vs: VectorShape, j: JsonNode, serializer: Serializer) = 46 | serializer.deserializeValue(j, "color", vs.color) 47 | serializer.deserializeValue(j, "size", vs.size) 48 | serializer.deserializeValue(j, "strokeWidth", vs.strokeWidth) 49 | serializer.deserializeValue(j, "strokeColor", vs.strokeColor) 50 | serializer.deserializeValue(j, "radius", vs.radius) 51 | serializer.deserializeValue(j, "shapeType", vs.shapeType) 52 | 53 | method getBBox*(vs: VectorShape): BBox = 54 | result.minPoint = newVector3(-vs.size.width/2.0, -vs.size.height/2.0, 0.0) 55 | result.maxPoint = newVector3(vs.size.width/2.0, vs.size.height/2.0, 0.0) 56 | 57 | method visitProperties*(vs: VectorShape, p: var PropertyVisitor) = 58 | p.visitProperty("size", vs.size) 59 | p.visitProperty("color", vs.color) 60 | p.visitProperty("strokeWidth", vs.strokeWidth) 61 | p.visitProperty("strokeColor", vs.strokeColor) 62 | p.visitProperty("radius", vs.radius) 63 | p.visitProperty("shapeType", vs.shapeType) 64 | 65 | method beforeDraw*(vs: VectorShape, index: int): bool = 66 | let c = currentContext() 67 | c.fillColor = vs.color 68 | c.strokeWidth = vs.strokeWidth 69 | c.strokeColor = vs.strokeColor 70 | let strokedWidth = vs.size.width + vs.strokeWidth 71 | let strokedHeight = vs.size.height + vs.strokeWidth 72 | let r = newRect(-strokedWidth/2.0, -strokedHeight/2.0, strokedWidth, strokedHeight) 73 | 74 | case vs.shapeType: 75 | of vsRectangle: 76 | if vs.radius > 0: 77 | c.drawRoundedRect(r, vs.radius + vs.strokeWidth/2.0) 78 | else: 79 | c.drawRect(r) 80 | of vsEllipse: 81 | c.drawEllipseInRect(r) 82 | else: discard 83 | 84 | registerComponent(VectorShape) 85 | -------------------------------------------------------------------------------- /rod/component/visual_modifier.nim: -------------------------------------------------------------------------------- 1 | import nimx/[types, context, portable_gl] 2 | import rod/[component, tools/serializer] 3 | import rod / utils / [ property_desc, serialization_codegen ] 4 | import opengl, json 5 | 6 | type BlendMode * = enum 7 | COLOR_ADD = GL_ONE 8 | COLOR_SCREEN = GL_ONE_MINUS_SRC_COLOR 9 | COLOR_MULTIPLY = GL_ONE_MINUS_SRC_ALPHA 10 | 11 | type VisualModifier* = ref object of RenderComponent 12 | blendMode*: BlendMode 13 | 14 | VisualModifier.properties: 15 | discard 16 | 17 | method init*(vm: VisualModifier) = 18 | procCall vm.Component.init() 19 | vm.blendMode = COLOR_ADD 20 | 21 | method beforeDraw*(vm: VisualModifier, index: int): bool = 22 | let gl = currentContext().gl 23 | 24 | if vm.blendMode == COLOR_SCREEN: 25 | gl.blendFunc(GL_ONE, vm.blendMode.GLenum) 26 | else: 27 | gl.blendFunc(gl.SRC_ALPHA, vm.blendMode.GLenum) 28 | 29 | method afterDraw*(vm: VisualModifier, index: int) = 30 | let gl = currentContext().gl 31 | gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA) 32 | 33 | method deserialize*(vm: VisualModifier, j: JsonNode, serealizer: Serializer) = 34 | var v = j{"blendMode"} 35 | if not v.isNil: 36 | let bm = v.getStr() 37 | case bm 38 | of "ADD": vm.blendMode = COLOR_ADD 39 | else: discard 40 | 41 | method serialize*(vm: VisualModifier, s: Serializer): JsonNode= 42 | result = newJObject() 43 | result.add("blendMode", %($vm.blendMode).substr(5)) 44 | 45 | genSerializationCodeForComponent(VisualModifier) 46 | registerComponent(VisualModifier, "Effects") 47 | -------------------------------------------------------------------------------- /rod/editor/editor_assets_view.nim: -------------------------------------------------------------------------------- 1 | import nimx / [types, matrixes, view, table_view_cell, text_field, 2 | scroll_view, button, event, linear_layout, collection_view, formatted_text, image, 3 | context, drag_and_drop, pasteboard / pasteboard_item, view_render_to_image, 4 | segmented_control ] 5 | 6 | import file_system / [ filesystem_view ] 7 | import variant, tables 8 | import rod / [edit_view] 9 | import times, os, strutils 10 | 11 | type EditorAssetsView* = ref object of EditorTabView 12 | fileSystemView: FileSystemView 13 | 14 | method init*(v: EditorAssetsView, r: Rect)= 15 | procCall v.View.init(r) 16 | 17 | v.fileSystemView = createProjectFSView(v.editor.currentProjectPath(), newRect(0.0, 0.0, v.bounds.width, v.bounds.height - 20.0)) 18 | v.fileSystemView.resizingMask="wh" 19 | v.addSubview(v.fileSystemView) 20 | 21 | var bottomMenu = newView(newRect(0, v.bounds.height - 20.0, v.bounds.width, 20)) 22 | bottomMenu.autoresizingMask = { afFlexibleWidth, afFlexibleMinY } 23 | v.addSubview(bottomMenu) 24 | 25 | var segm = SegmentedControl.new(newRect(bottomMenu.bounds.width - 150.0, 0.0, 150, 20.0)) 26 | segm.segments = @["compact", "normal"] 27 | segm.autoresizingMask = { afFlexibleMinX, afFlexibleHeight } 28 | segm.selectedSegment = 1 29 | bottomMenu.addSubview(segm) 30 | segm.onAction do(): 31 | v.fileSystemView.cachedResources.clear() 32 | v.fileSystemView.contentView.setCompact segm.selectedSegment == 0 33 | 34 | var curPath = newLabel(newRect(0,0,bottomMenu.bounds.width-segm.bounds.width, 20.0)) 35 | bottomMenu.addSubview(curPath) 36 | v.fileSystemView.onPathChanged do(np: string): 37 | curPath.text = np 38 | 39 | var filter = newTextField(newRect(segm.frame.origin.x - 250.0, 0, 220.0, 20.0)) 40 | filter.autoresizingMask = { afFlexibleMinX, afFlexibleHeight } 41 | filter.continuous = true 42 | filter.onAction do(): 43 | v.fileSystemView.onFilterContent() do(path: string) -> bool: 44 | let sp = path.splitFile() 45 | result = sp.name == filter.text or filter.text in sp.name 46 | if not result: 47 | result = sp.ext == filter.text or filter.text in sp.ext 48 | 49 | bottomMenu.addSubview(filter) 50 | 51 | v.fileSystemView.onDragStart do(fileViews: seq[FilePreview]): 52 | var drag_data = "" 53 | var drag_kind = "" 54 | 55 | for fileView in fileViews: 56 | var pbk = "" 57 | 58 | if fileView.kind == akComposition: 59 | pbk = rodPbComposition 60 | elif fileView.kind == akImage: 61 | pbk = rodPbSprite 62 | 63 | if pbk.len > 0: 64 | if drag_kind.len > 0: 65 | drag_kind = rodPbFiles 66 | drag_data &= ":" & fileView.path 67 | else: 68 | drag_kind = pbk 69 | drag_data = fileView.path 70 | 71 | if drag_data.len > 0 and drag_kind.len > 0: 72 | var dpi = newPasteboardItem(drag_kind, drag_data) 73 | var img: Image = nil 74 | img = fileViews[0].screenShot() 75 | startDrag(dpi, img) 76 | 77 | v.fileSystemView.onDoubleClicked do(item: FilePreview): 78 | if item.kind == akComposition: 79 | v.editor.openComposition(item.path) 80 | 81 | method tabSize*(v: EditorAssetsView, bounds: Rect): Size= 82 | result = newSize(bounds.width, 450.0) 83 | 84 | method tabAnchor*(v: EditorAssetsView): EditorTabAnchor = 85 | result = etaBottom 86 | 87 | method update*(v: EditorAssetsView)= 88 | v.fileSystemView.reloadIfNeeded() 89 | 90 | registerEditorTab("Assets", EditorAssetsView) 91 | 92 | #[ 93 | HELPERS 94 | ]# 95 | 96 | -------------------------------------------------------------------------------- /rod/editor/editor_console.nim: -------------------------------------------------------------------------------- 1 | import nimx/[view, text_field, button, popup_button, scroll_view] 2 | import strutils, tables, logging 3 | import rod/edit_view 4 | import rod/editor/editor_error_handling 5 | import variant 6 | 7 | export view 8 | 9 | 10 | type EditorConsole* = ref object of EditorTabView 11 | contentView: View 12 | 13 | scView: ScrollView 14 | currentLevel: Level 15 | 16 | proc reloadConsole(v: EditorConsole) = 17 | var msgs = gEditorLogger.dump(v.currentLevel) 18 | 19 | while v.contentView.subviews.len > 0: 20 | v.contentView.subviews[0].removeFromSuperView() 21 | 22 | var lblText = "\n" 23 | for msg in msgs: 24 | lblText &= msg & "\n" 25 | 26 | var lines = max(lblText.count("\n"),1) 27 | # info "lines ", lines 28 | 29 | var lbl = newLabel(newRect(10, 10, v.contentView.bounds.width, lines.float * 20.0)) 30 | lbl.resizingMask = "wh" 31 | lbl.backgroundColor = whiteColor() 32 | lbl.text = lblText 33 | # lbl.setBoundsSize(newSize(v.contentView.bounds.width, lines.float * 20.0)) 34 | v.contentView.setFrameSize(newSize(v.contentView.bounds.width, lines.float * 20.0 + 10.0)) 35 | v.contentView.addSubview(lbl) 36 | 37 | 38 | method init*(v: EditorConsole, r: Rect) = 39 | procCall v.View.init(r) 40 | v.resizingMask = "wh" 41 | 42 | v.contentView = newView(newRect(0, 0, v.bounds.width, v.bounds.height)) 43 | v.contentView.resizingMask = "wh" 44 | 45 | v.scView = newScrollView(v.contentView) 46 | v.scView.horizontalScrollBar = nil 47 | v.scView.resizingMask = "wh" 48 | # v.scView.setFrame(newRect(0.0, 20.0, v.bounds.width, v.bounds.height - 20.0)) 49 | v.addSubview(v.scView) 50 | 51 | v.currentLevel = 0.Level 52 | var popupButton = PopupButton.new(newRect(v.bounds.width - 240.0, 0, 240, 20)) 53 | popupButton.autoresizingMask = {afFlexibleMinX, afFlexibleMaxY} 54 | 55 | var items = newSeq[string]() 56 | for lvl in low(Level) .. high(Level): 57 | items.add($lvl) 58 | popupButton.items = items 59 | popupButton.onAction do(): 60 | v.currentLevel = popupButton.selectedIndex.Level 61 | v.reloadConsole() 62 | v.addSubview(popupButton) 63 | 64 | v.reloadConsole() 65 | 66 | method tabSize*(v: EditorConsole, bounds: Rect): Size= 67 | result = newSize(bounds.width, 250.0) 68 | 69 | method tabAnchor*(v: EditorConsole): EditorTabAnchor = 70 | result = etaBottom 71 | 72 | import times 73 | const updateRate = 0.25 74 | var lastUpdate = 0.0 75 | 76 | method update*(v: EditorConsole)= 77 | let t = epochTime() 78 | if t - lastUpdate > updateRate: 79 | lastUpdate = t 80 | v.reloadConsole() 81 | 82 | registerEditorTab("Console", EditorConsole) 83 | -------------------------------------------------------------------------------- /rod/editor/editor_default_tabs.nim: -------------------------------------------------------------------------------- 1 | import rod/editor/[editor_inspector_view, editor_tree_view, 2 | animation/editor_animation_view, scene/editor_scene_view, scene/editor_scene_settings, 3 | editor_console 4 | ] 5 | 6 | when not (defined(js) or defined(emscripten) or defined(android) or defined(ios)): 7 | import rod/editor/editor_assets_view 8 | -------------------------------------------------------------------------------- /rod/editor/editor_error_handling.nim: -------------------------------------------------------------------------------- 1 | import logging, tables, strutils, times 2 | 3 | type EditorLogger* = ref object of Logger 4 | msgDump*: TableRef[Level, seq[string]] 5 | 6 | method log*(logger: EditorLogger, level: Level, args: varargs[string,`$`])= 7 | var t = "[" & getClockStr() & "] " 8 | var msg = "" 9 | for arg in args: 10 | if arg.len == 0: 11 | msg &= "nil" 12 | else: 13 | msg &= arg 14 | 15 | var msgseq = msg.split("\n") 16 | 17 | if msgseq.len > 1: 18 | msg = "" 19 | for i, m in msgseq: 20 | msg &= (if i < msgseq.len - 1: "\n" else: "") & t & m 21 | else: 22 | msg = t & msg 23 | 24 | if logger.msgDump.isNil: 25 | logger.msgDump = newTable[Level, seq[string]]() 26 | 27 | var dump = logger.msgDump.getOrDefault(level) 28 | dump.add(msg) 29 | logger.msgDump[level] = dump 30 | 31 | proc clear*(logger: EditorLogger, level: Level)= 32 | var dump = logger.msgDump.getOrDefault(level) 33 | dump.setLen(0) 34 | logger.msgDump[level] = dump 35 | 36 | proc clearAll*(logger: EditorLogger)= 37 | for level in low(Level) .. high(Level): 38 | logger.clear(level) 39 | 40 | proc dump*(logger: EditorLogger, level: Level):seq[string]= 41 | result = @[] 42 | if logger.msgDump.isNil: return 43 | if level == lvlAll: 44 | for k, v in logger.msgDump: 45 | result.add(v) 46 | else: 47 | var dump = logger.msgDump.getOrDefault(level) 48 | result.add(dump) 49 | 50 | var gEditorLogger* {.threadvar.}: EditorLogger 51 | gEditorLogger.new() 52 | 53 | addHandler(gEditorLogger) 54 | -------------------------------------------------------------------------------- /rod/editor/editor_project_settings.nim: -------------------------------------------------------------------------------- 1 | import nimx/[matrixes, types] 2 | import rod / rod_types 3 | import json, os, logging, options 4 | 5 | type 6 | EditorTabSettings* = object 7 | name*: string 8 | frame*: Rect 9 | 10 | EditorSettings* = object 11 | AutosaveInterval: Option[float] 12 | EditorCameras: Option[seq[EditorCameraSettings]] 13 | 14 | EditorCameraSettings* = object 15 | name*: string 16 | viewportSize*: Size 17 | projectionMode*: CameraProjection 18 | 19 | EditorProject* = object 20 | name*: string 21 | path*: string 22 | tabs*: seq[EditorTabSettings] 23 | composition*: string 24 | settings*: EditorSettings 25 | 26 | const SettingsFileName = "settings.json" 27 | 28 | let UserSettingsPath = getHomeDir() & "/.rodedit/" & SettingsFileName 29 | let ProjSettingsPath = getAppDir() & "/.rodedit/" & SettingsFileName 30 | 31 | template settingsFiles(): seq[string] = 32 | @[UserSettingsPath, ProjSettingsPath] 33 | 34 | proc merge(a, b: JsonNode): JsonNode = 35 | if a.isNil and b.isNil: return 36 | if a.isNil: 37 | result = parseJson($b) 38 | else: 39 | if b.kind == a.kind and b.kind == JObject: 40 | result = parseJson($a) 41 | for k, v in b: 42 | if k notin a: 43 | result[k] = v 44 | else: 45 | result[k] = merge(a[k], b[k]) 46 | else: 47 | result = parseJson($b) 48 | 49 | proc getEditorSettingsJson(): JsonNode = 50 | var jsettings: JsonNode 51 | try: 52 | for sf in settingsFiles: 53 | info "getEditorSettingsJson: ", sf 54 | if fileExists(sf): 55 | jsettings = merge(jsettings, parseFile(sf)) 56 | except Exception as e: 57 | warn "Can't parse editor settings! ", e.msg, "\n", e.getStackTrace() 58 | result = jsettings 59 | 60 | proc loadEditorSettings*(proj: var EditorProject) = 61 | let jsettings = getEditorSettingsJson() 62 | if not jsettings.isNil: 63 | proj.settings = jsettings.to(EditorSettings) 64 | echo "loadEditorSettings ", if jsettings.isNil: "nil" else: $jsettings 65 | 66 | proc autosaveInterval*(p: EditorProject): float = 67 | result = 120.0 68 | if p.settings.AutosaveInterval.isSome: 69 | result = p.settings.AutosaveInterval.get() 70 | 71 | proc editorCameras*(p: EditorProject): seq[EditorCameraSettings] = 72 | result = @[ 73 | EditorCameraSettings(name: "[EditorCamera2D]", viewportSize: newSize(1920, 1080), projectionMode: cpOrtho), 74 | EditorCameraSettings(name: "[EditorCamera3D]", viewportSize: newSize(1920, 1080), projectionMode: cpPerspective) 75 | ] 76 | if not p.settings.EditorCameras.isSome: return 77 | var r = p.settings.EditorCameras.get() 78 | if r.len == 0: return 79 | result = r 80 | -------------------------------------------------------------------------------- /rod/editor/editor_tab_registry.nim: -------------------------------------------------------------------------------- 1 | import typetraits 2 | import nimx / [ types, view ] 3 | 4 | const RodInternalTab* = "RodInternalTab" 5 | 6 | type 7 | EditViewEntry* = tuple 8 | name: string 9 | create: proc(): View {.gcsafe.} 10 | 11 | var gRegisteredViews {.threadvar.}: seq[EditViewEntry] 12 | gRegisteredViews = newSeq[EditViewEntry]() 13 | 14 | proc registerEditorTabAux(tn: string, t: typedesc) = 15 | var evr: EditViewEntry 16 | evr.name = tn 17 | let typename = typetraits.name(t) 18 | evr.create = proc(): View {.gcsafe.} = 19 | result = newObjectOfClass(typename).View 20 | result.name = evr.name 21 | 22 | gRegisteredViews.add(evr) 23 | 24 | template registerEditorTab*(tn: string, t: typedesc) = 25 | doAssert(tn.len > 0, "Tab must have the name") 26 | registerClass(t) 27 | registerEditorTabAux(tn, t) 28 | 29 | iterator registeredEditorTabs*():EditViewEntry= 30 | for rt in gRegisteredViews: 31 | yield rt 32 | 33 | proc getRegisteredEditorTab*(name: string): EditViewEntry= 34 | for rt in registeredEditorTabs(): 35 | echo "find ", name, " != ", rt.name 36 | if rt.name == name: 37 | echo "find tab ", name 38 | return rt 39 | -------------------------------------------------------------------------------- /rod/editor/editor_types.nim: -------------------------------------------------------------------------------- 1 | import nimx / [view, button, editor/tab_view, linear_layout, popup_button, 2 | toolbar, notification_center, event, animation ] 3 | import rod / [node, viewport, editor/editor_project_settings] 4 | import rod / editor / animation / animation_editor_types 5 | 6 | export notification_center, editor_project_settings 7 | 8 | const toolbarHeight* = 30 9 | 10 | const loadingAndSavingAvailable* = not defined(android) and not defined(ios) and 11 | not defined(emscripten) and not defined(js) 12 | 13 | type 14 | EditMode* = enum 15 | emScene 16 | emAnimation 17 | 18 | EditorTabAnchor* = enum 19 | etaLeft 20 | etaRight 21 | etaBottom 22 | etaCenter 23 | 24 | EditorTabView* = ref object of View 25 | rootNode*: Node 26 | editor*: Editor 27 | composition*: CompositionDocument 28 | 29 | CompositionDocument* = ref object 30 | path*: string 31 | rootNode*: Node 32 | selectedNode*: Node 33 | owner*: EditorTabView 34 | 35 | animations*: seq[EditedAnimation] 36 | currentAnimation*: EditedAnimation 37 | 38 | Editor* = ref object 39 | mode*: EditMode 40 | sceneInput*: bool 41 | currentProject*: EditorProject 42 | mCurrentComposition*: CompositionDocument 43 | rootNode*: Node 44 | sceneView*: SceneView 45 | window*: Window 46 | mSelectedNode*: Node 47 | startFromGame*: bool 48 | workspaceView*: WorkspaceView 49 | cameraSelector*: PopupButton 50 | notifCenter*: NotificationCenter 51 | 52 | WorkspaceView* = ref object of View 53 | editor*: Editor 54 | toolbar*: Toolbar 55 | tabs*: seq[EditorTabView] 56 | tabViews*: seq[TabView] 57 | compositionEditors*: seq[EditorTabView] 58 | anchors*: array[4, TabView] 59 | horizontalLayout*: LinearLayout 60 | verticalLayout*: LinearLayout 61 | onKeyDown*: proc(e: var Event): bool {.gcsafe.} 62 | 63 | template selectedNode*(e: Editor): Node = e.mSelectedNode 64 | 65 | method setEditedNode*(v: EditorTabView, n: Node) {.base.}= 66 | discard 67 | 68 | method update*(v: EditorTabView) {.base.}= discard 69 | 70 | method tabSize*(v: EditorTabView, bounds: Rect): Size {.base.}= 71 | result = bounds.size 72 | 73 | method tabAnchor*(v: EditorTabView): EditorTabAnchor {.base.}= 74 | result = etaCenter 75 | 76 | method onEditorTouchDown*(v: EditorTabView, e: var Event) {.base.}= 77 | discard 78 | 79 | method onSceneChanged*(v: EditorTabView) {.base, deprecated.}= 80 | discard 81 | 82 | method onCompositionChanged*(v: EditorTabView, comp: CompositionDocument) {.base.} = 83 | v.composition = comp 84 | 85 | method onCompositionSaved*(v: EditorTabView, comp: CompositionDocument) {.base.} = discard 86 | 87 | method onEditModeChanged*(v: EditorTabView, mode: EditMode) {.base.} = discard 88 | 89 | # Notifications 90 | const RodEditorNotif_onCompositionOpen* = "RodEditorNotif_onCompositionOpen" 91 | const RodEditorNotif_onCompositionAdd* = "RodEditorNotif_onCompositionAdd" 92 | const RodEditorNotif_onCompositionSave* = "RodEditorNotif_onCompositionSave" 93 | const RodEditorNotif_onCompositionSaveAs* = "RodEditorNotif_onCompositionSaveAs" 94 | const RodEditorNotif_onCompositionNew* = "RodEditorNotif_onCompositionNew" 95 | const RodEditorNotif_onConvertToComp* = "RodEditorNotif_onConvertToComp" 96 | 97 | 98 | # Pasteboard 99 | const rodPbComposition* = "rod.composition" 100 | const rodPbSprite* = "rod.sprite" 101 | const rodPbFiles* = "rod.files" 102 | const NodePboardKind* = "io.github.yglukhov.rod.node" 103 | const BezierPboardKind* = "io.github.yglukhov.rod.bezier" 104 | 105 | # Editor's nodes 106 | const EditorRootNodeName* = "[EditorRoot]" 107 | 108 | # Default open tabs 109 | const defaultTabs* = ["Inspector", "Tree", "EditScene Settings", "Assets" ] 110 | -------------------------------------------------------------------------------- /rod/editor/scene/components/editor_component.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ event ] 2 | import rod / [ rod_types ] 3 | 4 | #will be called for all components 5 | method onDrawGizmo*(c: Component) {.base.} = discard 6 | 7 | #will be called only for components on selected node 8 | method onKeyDown*(c: Component, e: Event): bool {.base.} = discard 9 | method onKeyUp*(c: Component, e: Event): bool {.base.} = discard 10 | method onTouchDown*(c: Component, e: Event): bool {.base.} = discard 11 | method onTouchMove*(c: Component, e: Event): bool {.base.} = discard 12 | method onTouchUp*(c: Component, e: Event): bool {.base.} = discard 13 | -------------------------------------------------------------------------------- /rod/editor/scene/components/grid.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ types, matrixes, context, view, property_visitor ] 2 | import rod / [ rod_types, node, component, viewport ] 3 | import rod / tools / debug_draw 4 | import editor_component 5 | import math 6 | 7 | type EditorGrid* = ref object of RenderComponent 8 | gridSize: Size 9 | offset: Point 10 | snap: bool 11 | 12 | method componentNodeWasAddedToSceneView(c: EditorGrid) = 13 | c.gridSize = newSize(100, 100) 14 | 15 | method onDrawGizmo*(c: EditorGrid) = 16 | let scene = c.node.sceneView 17 | let ss = scene.bounds 18 | let p0 = scene.screenToWorldPoint(newVector3()) 19 | let p1 = scene.screenToWorldPoint(newVector3(ss.width, ss.height)) 20 | 21 | let ctx = currentContext() 22 | 23 | template drawGrid(r: Rect, s: Size, color: Color) = 24 | var gr = r 25 | gr.origin.x = gr.x - (s.width + (gr.x mod s.width)) 26 | gr.origin.y = gr.y - (s.height + (gr.y mod s.height)) 27 | gr.size.width += s.width * 2 28 | gr.size.height += s.height * 2 29 | ctx.strokeColor = color 30 | 31 | DDdrawGrid(gr, s, c.offset) 32 | 33 | var gr = newRect(p0.x, p0.y, p1.x - p0.x, p1.y - p0.y) 34 | drawGrid(gr, c.gridSize, newColor(0.0, 0.0, 0.0, 0.1)) 35 | drawGrid(gr, newSize(c.gridSize.width * 5, c.gridSize.height * 5), newColor(0.0, 0.0, 0.0, 0.3)) 36 | 37 | method visitProperties*(c: EditorGrid, p: var PropertyVisitor) = 38 | p.visitProperty("size", c.gridSize) 39 | if c.gridSize.width < 1.0: 40 | c.gridSize.width = 1.0 41 | if c.gridSize.height < 1.0: 42 | c.gridSize.height = 1.0 43 | p.visitProperty("offset", c.offset) 44 | p.visitProperty("snap", c.snap) 45 | 46 | proc snappingEnabled*(c: EditorGrid): bool = c.snap 47 | 48 | proc snappedWorldPosition*(c: EditorGrid, p: Vector3): Vector3 = 49 | let x1 = splitDecimal((p.x - c.offset.x) / c.gridSize.width).floatPart 50 | let y1 = splitDecimal((p.y - c.offset.y) / c.gridSize.height).floatPart 51 | let xo = if x1 < 0.5: - c.gridSize.width * x1 else: (1 - x1) * c.gridSize.width 52 | let yo = if y1 < 0.5: - c.gridSize.height * y1 else: (1 - y1) * c.gridSize.height 53 | result = newVector3(p.x + xo, p.y + yo) 54 | 55 | registerComponent(EditorGrid, "Editor") 56 | -------------------------------------------------------------------------------- /rod/editor/scene/components/viewport_rect.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ types ] 2 | import rod / [ rod_types, node, component, viewport ] 3 | import rod / tools / debug_draw 4 | import editor_component 5 | 6 | type ViewportRect* = ref object of RenderComponent 7 | 8 | method onDrawGizmo*(c: ViewportRect) = 9 | let scene = c.node.sceneView 10 | DDdrawRect(newRect(0, 0, scene.camera.viewportSize.width, scene.camera.viewportSize.height)) 11 | 12 | registerComponent(ViewportRect, "Editor") 13 | -------------------------------------------------------------------------------- /rod/editor/scene/editor_camera_controller.nim: -------------------------------------------------------------------------------- 1 | import nimx/[matrixes, types, event, view] 2 | import rod/[viewport, rod_types, node, quaternion ] 3 | 4 | type EditorCameraController* = ref object 5 | camera*: Node 6 | editedView: SceneView 7 | camPivot: Node 8 | camAnchor: Node 9 | 10 | startPos: Vector3 11 | startPivotPos: Vector3 12 | startPivotRot: Quaternion 13 | startAngle: Vector3 14 | currentAngle: Vector3 15 | currNode: Node 16 | currKey: VirtualKey 17 | currMouseKey: VirtualKey 18 | prev_x, prev_y: float 19 | 20 | 21 | proc calculatePivotPos(camNode: Node): Vector3 = 22 | var dir = newVector3(0, 0, -1) 23 | dir = camNode.worldTransform().transformDirection(dir) 24 | dir.normalize() 25 | dir = dir * 100.0 26 | 27 | result = camNode.worldPos + dir 28 | 29 | proc setCamera*(cc: EditorCameraController, camNode: Node) = 30 | cc.camPivot.worldPos = calculatePivotPos(camNode) 31 | 32 | var scale: Vector3 33 | var rot: Vector4 34 | discard camNode.worldTransform.tryGetScaleRotationFromModel(scale, rot) 35 | cc.camPivot.rotation = newQuaternion(rot.x, rot.y, rot.z, rot.w) 36 | 37 | cc.camAnchor.worldPos = camNode.worldPos 38 | cc.startPos = cc.camAnchor.worldPos 39 | cc.startPivotPos = cc.camPivot.worldPos 40 | cc.startPivotRot = cc.camPivot.rotation 41 | 42 | var q = cc.camPivot.rotation 43 | q.w = -q.w 44 | cc.currentAngle = q.eulerAngles() 45 | cc.startAngle = cc.currentAngle 46 | 47 | proc newEditorCameraController*(view: SceneView) : EditorCameraController = 48 | new(result) 49 | result.editedView = view 50 | result.camPivot = newNode("EditorCameraPivot") 51 | result.camAnchor = newNode("EditorCameraAnchor") 52 | result.currentAngle = newVector3(0) 53 | 54 | result.editedView.rootNode.addChild(result.camPivot) 55 | result.camPivot.addChild(result.camAnchor) 56 | 57 | result.setCamera(result.editedView.camera.node) 58 | 59 | proc updateCamera(cc: EditorCameraController) = 60 | var worldMat = cc.camAnchor.worldTransform() 61 | var pos: Vector3 62 | var scale: Vector3 63 | var rot: Vector4 64 | discard worldMat.tryGetTranslationFromModel(pos) 65 | discard worldMat.tryGetScaleRotationFromModel(scale, rot) 66 | 67 | cc.editedView.camera.node.worldPos = pos 68 | cc.editedView.camera.node.rotation = newQuaternion(rot.x, rot.y, rot.z, rot.w) 69 | 70 | proc setToNode*(cc: EditorCameraController, n: Node) = 71 | cc.currNode = n 72 | if not cc.currNode.isNil and (cc.currNode != cc.editedView.camera.node): 73 | cc.camPivot.worldPos = cc.currNode.worldPos 74 | 75 | cc.updateCamera() 76 | 77 | proc onTapDown*(cc: EditorCameraController, e : var Event) = 78 | cc.camAnchor.worldPos = cc.editedView.camera.node.worldPos 79 | cc.currMouseKey = e.keyCode 80 | cc.prev_x = 0.0 81 | cc.prev_y = 0.0 82 | 83 | proc onTapUp*(cc: EditorCameraController, dx, dy : float32, e : var Event) = 84 | cc.currMouseKey = 0.VirtualKey 85 | 86 | proc onKeyDown*(cc: EditorCameraController, e: var Event) = 87 | cc.currKey = e.keyCode 88 | 89 | proc onKeyUp*(cc: EditorCameraController, e: var Event) = 90 | if e.keyCode == VirtualKey.R: 91 | cc.camPivot.rotation = cc.startPivotRot 92 | cc.camPivot.worldPos = cc.startPivotPos 93 | cc.camAnchor.worldPos = cc.startPos 94 | cc.currentAngle = cc.startAngle 95 | 96 | cc.updateCamera() 97 | 98 | cc.currKey = 0.VirtualKey 99 | 100 | proc onScrollProgress*(cc: EditorCameraController, dx, dy : float, e : var Event) = 101 | if cc.editedView.camera.projectionMode == cpOrtho: 102 | if cc.currMouseKey == VirtualKey.MouseButtonSecondary: 103 | 104 | var shift_pos = newVector3(cc.prev_x - dx, cc.prev_y - dy, 0.0) * cc.editedView.camera.node.scale 105 | cc.prev_x = dx 106 | cc.prev_y = dy 107 | cc.camPivot.worldPos = cc.camPivot.worldPos + shift_pos 108 | cc.updateCamera() 109 | 110 | return 111 | 112 | if cc.currKey == VirtualKey.LeftAlt or cc.currKey == VirtualKey.RightAlt: 113 | cc.currentAngle.x += cc.prev_y - dy 114 | cc.currentAngle.y += cc.prev_x - dx 115 | 116 | let q = newQuaternionFromEulerXYZ(cc.currentAngle.x, cc.currentAngle.y, cc.currentAngle.z) 117 | cc.camPivot.rotation = q 118 | 119 | if cc.currMouseKey == VirtualKey.MouseButtonMiddle: 120 | var speed = 0.1 121 | if cc.currKey == VirtualKey.LeftShift: 122 | speed = 1.0 123 | 124 | var shift_pos = newVector3(cc.prev_x - dx, -cc.prev_y + dy, 0.0) * speed 125 | var rotMat = cc.camPivot.rotation.toMatrix4() 126 | rotMat.multiply(shift_pos, shift_pos) 127 | 128 | cc.camPivot.worldPos = cc.camPivot.worldPos + shift_pos 129 | 130 | cc.prev_x = dx 131 | cc.prev_y = dy 132 | 133 | cc.updateCamera() 134 | 135 | proc onMouseScrroll*(cc: EditorCameraController, e : var Event) = 136 | var dir: Vector3 = cc.camPivot.worldPos - cc.camAnchor.worldPos 137 | let ndir: Vector3 = normalized(dir) 138 | let offset: Vector3 = ndir * e.offset.y - ndir * e.offset.x * 10.0 139 | 140 | if cc.editedView.camera.projectionMode == cpOrtho: 141 | cc.editedView.camera.node.scale = cc.editedView.camera.node.scale + cc.editedView.camera.node.scale * e.offset.y * 0.005 142 | if cc.editedView.camera.node.scale.x < 0.01: 143 | cc.editedView.camera.node.scale = newVector3(0.01, 0.01, 0.01) 144 | elif cc.editedView.camera.node.scale.x > 100.0: 145 | cc.editedView.camera.node.scale = newVector3(100, 100, 100) 146 | return 147 | 148 | # 0 coord lock protection 149 | if length(offset) > length(dir) - 0.1 and dot(offset, dir) > 0.5: 150 | return 151 | 152 | cc.camAnchor.worldPos = cc.camAnchor.worldPos + offset 153 | 154 | cc.updateCamera() 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /rod/editor/scene/editor_scene_settings.nim: -------------------------------------------------------------------------------- 1 | import nimx/[view, text_field, button, popup_button, 2 | scroll_view, linear_layout, slider, 3 | property_visitor, expanding_view 4 | ] 5 | import rod/[node, component, rod_types, viewport] 6 | import rod/property_editors/[propedit_registry, standard_editors] 7 | import rod/component/camera 8 | import rod/edit_view 9 | import rod/editor/scene/components/grid 10 | import tables 11 | import variant 12 | 13 | export view 14 | 15 | type SceneSettingsView* = ref object of EditorTabView 16 | propView: LinearLayout 17 | scView: ScrollView 18 | currCameras: seq[Node] 19 | currSceneView: SceneView 20 | autoUpdate: bool 21 | 22 | proc reloadEditScene(v: SceneSettingsView) {.gcsafe.} 23 | 24 | method init*(i: SceneSettingsView, r: Rect) = 25 | procCall i.View.init(r) 26 | i.resizingMask = "wh" 27 | 28 | i.propView = newVerticalLayout(newRect(0, 20, i.bounds.width, 20)) 29 | i.propView.resizingMask = "wb" 30 | i.propView.topMargin = 5 31 | i.propView.bottomMargin = 5 32 | i.propView.leftMargin = 5 33 | i.propView.rightMargin = 5 34 | 35 | i.scView = newScrollView(i.propView) 36 | i.scView.horizontalScrollBar = nil 37 | i.scView.resizingMask = "wh" 38 | i.scView.setFrame(newRect(0.0, 0.0, i.bounds.width, i.bounds.height - 20.0)) 39 | i.addSubview(i.scView) 40 | 41 | i.reloadEditScene() 42 | 43 | proc inspect(i: SceneSettingsView, expView: var ExpandingView, visitor: var PropertyVisitor, comps: seq[Component]) = 44 | for v in comps: 45 | expView = newExpandingView(newRect(0, 0, 328, 20.0)) 46 | expView.title = v.className 47 | expView.expand() 48 | 49 | v.visitProperties(visitor) 50 | i.propView.addSubview(expView) 51 | 52 | proc `inspectedNode=`*(i: SceneSettingsView, n: Node) = 53 | let scrollBar = i.scView.verticalScrollBar() 54 | let oldPos = scrollBar.value() 55 | 56 | while i.propView.subviews.len() > 1: 57 | i.propView.subviews[1].removeFromSuperview() 58 | 59 | if not n.isNil: 60 | proc changeSceneSettingsView() = 61 | i.inspectedNode = n 62 | 63 | var expView= newExpandingView(newRect(0, 50, 328, 20.0)) 64 | expView.title = "Camera node" 65 | expView.expand() 66 | 67 | var visitor : PropertyVisitor 68 | visitor.requireName = true 69 | visitor.requireSetter = true 70 | visitor.requireGetter = true 71 | visitor.flags = { pfEditable } 72 | visitor.commit = proc() = 73 | let propView = propertyEditorForProperty(n, visitor.name, visitor.setterAndGetter, nil, changeSceneSettingsView) 74 | propView.autoresizingMask = {afFlexibleWidth} 75 | let propHolder = newView(propView.frame) 76 | propHolder.addSubview(propView) 77 | expView.addContent(propHolder) 78 | 79 | n.visitProperties(visitor) 80 | i.propView.addSubview(expView) 81 | 82 | var comps: seq[Component] 83 | for c in n.components: 84 | comps.add(c) 85 | 86 | var gridComp = n.sceneView.rootNode.componentIfAvailable(EditorGrid) 87 | if not gridComp.isNil: 88 | comps.add(gridComp) 89 | 90 | i.inspect(expView, visitor, comps) 91 | 92 | scrollBar.value = oldPos 93 | scrollBar.sendAction() 94 | else: 95 | discard 96 | 97 | method tabSize*(v: SceneSettingsView, bounds: Rect): Size= 98 | result = newSize(300.0, bounds.height) 99 | 100 | method tabAnchor*(v: SceneSettingsView): EditorTabAnchor = 101 | result = etaRight 102 | 103 | proc getAllCameras(n: Node): seq[Node]= 104 | result = @[] 105 | if not n.componentIfAvailable(Camera).isNil: 106 | result.add(n) 107 | 108 | for ch in n.children: 109 | result.add(ch.getAllCameras) 110 | 111 | proc reloadEditScene(v: SceneSettingsView) {.gcsafe.} = 112 | while v.propView.subviews.len > 0: 113 | v.propView.subviews[0].removeFromSuperview() 114 | 115 | var curScene: SceneView 116 | var root: Node 117 | var curCameraNode: Node 118 | 119 | if not v.editor.currentComposition.isNil: 120 | curScene = v.editor.currentComposition.rootNode.sceneView 121 | else: 122 | if v.editor.sceneView.isNil: return 123 | curScene = v.editor.sceneView 124 | 125 | root = curScene.rootNode 126 | curCameraNode = curScene.camera.node 127 | 128 | var cameraNodes = root.getAllCameras() 129 | var cameraNames = newSeq[string]() 130 | for c in cameraNodes: 131 | cameraNames.add(c.name) 132 | 133 | var curCameraIdx = cameraNodes.find(curCameraNode) 134 | if curCameraIdx < 0: return 135 | v.currSceneView = curScene 136 | v.currCameras = cameraNodes 137 | var cameraSel = PopupButton.new(newRect(10, 10, v.bounds.width, 20)) 138 | cameraSel.items = cameraNames 139 | cameraSel.selectedIndex = curCameraIdx 140 | v.propView.addSubview(cameraSel) 141 | cameraSel.onAction do(): 142 | let camn = cameraNodes[cameraSel.selectedIndex] 143 | curScene.camera = camn.component(Camera) 144 | v.inspectedNode = camn 145 | 146 | v.inspectedNode = curCameraNode 147 | 148 | method onCompositionChanged*(v: SceneSettingsView, comp: CompositionDocument) = 149 | procCall v.EditorTabView.onCompositionChanged(comp) 150 | v.reloadEditScene() 151 | 152 | method update*(v: SceneSettingsView)= 153 | let cams = v.currSceneView.rootNode.getAllCameras() 154 | if cams != v.currCameras: 155 | v.reloadEditScene() 156 | echo "reload cameras" 157 | 158 | registerEditorTab("EditScene Settings", SceneSettingsView) 159 | -------------------------------------------------------------------------------- /rod/editor/scene/gizmo.nim: -------------------------------------------------------------------------------- 1 | import nimx/[context, portable_gl, types, matrixes, event] 2 | import rod/[node, viewport] 3 | 4 | type Gizmo* = ref object of RootObj 5 | gizmoNode*: Node 6 | axisMask*: Vector3 7 | mEditedNode*: Node 8 | mPrevCastedAxis: Node 9 | 10 | method updateGizmo*(g: Gizmo) {.base.} = discard 11 | method startTransform*(g: Gizmo, selectedGizmo: Node, position: Point) {.base.} = discard 12 | method proccesTransform*(g: Gizmo, position: Point) {.base.} = discard 13 | method stopTransform*(g: Gizmo) {.base.} = discard 14 | method onMouseIn*(g: Gizmo, castedNode: Node) {.base.} = discard 15 | method onMouseOut*(g: Gizmo, castedNode: Node) {.base.} = discard 16 | 17 | proc newGizmo*(): Gizmo = 18 | result = new(Gizmo) 19 | result.gizmoNode = newNode() 20 | result.gizmoNode.alpha = 0.0 21 | 22 | proc `editedNode=`*(g: Gizmo, n: Node) = 23 | g.mEditedNode = n 24 | if not n.isNil: 25 | g.gizmoNode.alpha = 1.0 26 | g.updateGizmo() 27 | else: 28 | g.gizmoNode.alpha = 0.0 29 | 30 | proc editedNode*(g: Gizmo): Node = g.mEditedNode 31 | 32 | proc onTouchEv*(g: Gizmo, e: var Event): bool = 33 | case e.buttonState: 34 | of bsUp: 35 | g.stopTransform() 36 | 37 | of bsDown: 38 | let castedGizmo = g.gizmoNode.sceneView.rayCastFirstNode(g.gizmoNode, e.localPosition) 39 | if not castedGizmo.isNil: 40 | result = true 41 | g.startTransform(castedGizmo, e.localPosition) 42 | 43 | of bsUnknown: 44 | g.proccesTransform(e.localPosition) 45 | 46 | proc onMouseOver*(g: Gizmo, e: var Event) = 47 | if g.isNil or g.gizmoNode.sceneView.isNil: return 48 | 49 | let castedGizmo = g.gizmoNode.sceneView.rayCastFirstNode(g.gizmoNode, e.localPosition) 50 | 51 | if castedGizmo != g.mPrevCastedAxis: 52 | if not g.mPrevCastedAxis.isNil: 53 | g.onMouseOut(g.mPrevCastedAxis) 54 | if not castedGizmo.isNil: 55 | g.onMouseIn(castedGizmo) 56 | 57 | g.mPrevCastedAxis = castedGizmo 58 | -------------------------------------------------------------------------------- /rod/editor/scene/gizmo_move.nim: -------------------------------------------------------------------------------- 1 | import nimx/[context, portable_gl, types, matrixes, event] 2 | import rod/component/[primitives/cone, primitives/cube, mesh_component, material] 3 | import rod/[node, viewport, quaternion, editor/scene/gizmo] 4 | import rod/editor/scene/components/grid 5 | import strutils 6 | 7 | type MoveGizmo* = ref object of Gizmo 8 | screenPoint: Vector3 9 | offset: Vector3 10 | mGrid: EditorGrid 11 | 12 | proc grid(c: MoveGizmo): EditorGrid = 13 | if c.mGrid.isNil: 14 | c.mGrid = c.gizmoNode.sceneView.rootNode.componentIfAvailable(EditorGrid) 15 | result = c.mGrid 16 | 17 | method updateGizmo*(g: MoveGizmo) = 18 | if g.mEditedNode.isNil: 19 | return 20 | 21 | g.gizmoNode.position = g.mEditedNode.worldPos 22 | 23 | var dist = (g.gizmoNode.sceneView.camera.node.worldPos - g.gizmoNode.worldPos).length 24 | let wp1 = g.gizmoNode.sceneView.camera.node.worldTransform() * newVector3(0.0, 0.0, -dist) 25 | let wp2 = g.gizmoNode.sceneView.camera.node.worldTransform() * newVector3(100.0, 0.0, -dist) 26 | 27 | let p1 = g.gizmoNode.sceneView.worldToScreenPoint(wp1) 28 | let p2 = g.gizmoNode.sceneView.worldToScreenPoint(wp2) 29 | 30 | let cameraScale = g.gizmoNode.sceneView.camera.node.scale 31 | let scale = 450.0 / abs(p2.x - p1.x) * cameraScale.x 32 | g.gizmoNode.scale = newVector3(scale, scale, scale) 33 | 34 | 35 | proc getAxisColor(name: string): Color = 36 | if name.contains("_x"): 37 | result = newColor(1, 0, 0, 1) 38 | elif name.contains("_y"): 39 | result = newColor(0, 1, 0, 1) 40 | elif name.contains("_z"): 41 | result = newColor(0, 0, 1, 0.3) 42 | 43 | proc createPlane(name: string): Node = 44 | result = newNode(name) 45 | let size = 4.0 46 | let cubeNode = newNode() 47 | result.addChild(cubeNode) 48 | 49 | cubeNode.positionX = size 50 | cubeNode.positionY = size 51 | cubeNode.alpha = 0.8 52 | 53 | let cube = cubeNode.addComponent(CubeComponent) 54 | cube.size = newVector3(size, size, 0.1) 55 | cube.material.diffuse = getAxisColor(name) 56 | 57 | proc createArrow(name: string): Node = 58 | result = newNode(name) 59 | let h = 16.0 60 | let cylNode = newNode() 61 | let coneNode = newNode() 62 | 63 | coneNode.positionY = h 64 | 65 | result.addChild(cylNode) 66 | result.addChild(coneNode) 67 | 68 | let cylinder = cylNode.addComponent(ConeComponent) 69 | cylinder.height = h 70 | cylinder.material.diffuse = getAxisColor(name) 71 | cylinder.radius1 = 0.6 72 | cylinder.radius2 = 0.6 73 | 74 | let cone = coneNode.addComponent(ConeComponent) 75 | cone.radius1 = 1.5 76 | cone.radius2 = 0.0 77 | cone.material.diffuse = getAxisColor(name) 78 | 79 | proc createMesh(): Node = 80 | result = newNode("gizmo_axis") 81 | 82 | let axis_x = createArrow("axis_x") 83 | let axis_y = createArrow("axis_y") 84 | let axis_z = createArrow("axis_z") 85 | result.addChild(axis_x) 86 | result.addChild(axis_y) 87 | result.addChild(axis_z) 88 | 89 | axis_x.rotation = newQuaternionFromEulerYXZ(0, 0, -90) 90 | axis_z.rotation = newQuaternionFromEulerYXZ(90, 0, 0) 91 | 92 | let plane_x = createPlane("plane_x") 93 | let plane_y = createPlane("plane_y") 94 | let plane_z = createPlane("plane_z") 95 | result.addChild(plane_x) 96 | result.addChild(plane_y) 97 | result.addChild(plane_z) 98 | 99 | plane_x.rotation = newQuaternionFromEulerYXZ(0, -90, 0) 100 | plane_y.rotation = newQuaternionFromEulerYXZ(90, 0, 0) 101 | 102 | 103 | proc newMoveGizmo*(): MoveGizmo = 104 | result = new(MoveGizmo) 105 | result.gizmoNode = createMesh() 106 | # result.gizmoNode.loadComposition( getMoveAxisJson() ) 107 | result.gizmoNode.alpha = 0.0 108 | 109 | method startTransform*(ga: MoveGizmo, selectedGizmo: Node, position: Point) = 110 | let axis = selectedGizmo.parent 111 | if axis.name.contains("axis_x"): 112 | ga.axisMask = newVector3(1.0, 0.0, 0.0) 113 | elif axis.name.contains("axis_y"): 114 | ga.axisMask = newVector3(0.0, 1.0, 0.0) 115 | elif axis.name.contains("axis_z"): 116 | ga.axisMask = newVector3(0.0, 0.0, 1.0) 117 | 118 | elif axis.name.contains("plane_x"): 119 | ga.axisMask = newVector3(0.0, 1.0, 1.0) 120 | elif axis.name.contains("plane_y"): 121 | ga.axisMask = newVector3(1.0, 0.0, 1.0) 122 | elif axis.name.contains("plane_z"): 123 | ga.axisMask = newVector3(1.0, 1.0, 0.0) 124 | 125 | ga.screenPoint = ga.gizmoNode.sceneView.worldToScreenPoint(ga.gizmoNode.worldPos) 126 | ga.offset = ga.gizmoNode.worldPos - ga.gizmoNode.sceneView.screenToWorldPoint(newVector3(position.x, position.y, ga.screenPoint.z)) 127 | 128 | method proccesTransform*(g: MoveGizmo, position: Point) = 129 | if g.mEditedNode.isNil: 130 | return 131 | 132 | let curScreenPoint = newVector3(position.x, position.y, g.screenPoint.z) 133 | var curPosition: Vector3 134 | curPosition = g.mEditedNode.sceneView.screenToWorldPoint(curScreenPoint) + g.offset 135 | curPosition = curPosition - g.gizmoNode.worldPos 136 | g.gizmoNode.position = g.gizmoNode.worldPos + curPosition * g.axisMask 137 | if not g.mEditedNode.parent.isNil: 138 | try: 139 | var p = g.gizmoNode.position 140 | if not g.grid.isNil and g.grid.snappingEnabled: 141 | p = g.grid.snappedWorldPosition(p) 142 | g.mEditedNode.position = g.mEditedNode.parent.worldToLocal(p) 143 | except: 144 | discard 145 | else: 146 | g.mEditedNode.position = g.gizmoNode.position 147 | 148 | method stopTransform*(g: MoveGizmo) = 149 | g.axisMask = newVector3(0.0, 0.0, 0.0) 150 | 151 | method onMouseIn*(g: MoveGizmo, castedNode: Node) = 152 | for ch in castedNode.parent.children: 153 | ch.getComponent(MeshComponent).material.diffuse = newColor(1, 1, 0, 1) 154 | 155 | method onMouseOut*(g: MoveGizmo, castedNode: Node) = 156 | let parent = castedNode.parent 157 | for ch in parent.children: 158 | ch.getComponent(MeshComponent).material.diffuse = getAxisColor(parent.name) 159 | -------------------------------------------------------------------------------- /rod/editor/scene/node_selector.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ context, portable_gl, types, matrixes ] 2 | import rod / [ component, node, viewport ] 3 | import opengl 4 | 5 | const vertexShader = """ 6 | attribute vec4 aPosition; 7 | uniform mat4 mvpMatrix; 8 | void main() { gl_Position = mvpMatrix * vec4(aPosition.xyz, 1.0); } 9 | """ 10 | const fragmentShader = """ 11 | #ifdef GL_ES 12 | #extension GL_OES_standard_derivatives : enable 13 | precision mediump float; 14 | #endif 15 | uniform vec4 uColor; 16 | void main() { gl_FragColor = uColor; } 17 | """ 18 | 19 | # let vertexData = [-0.5.GLfloat,-0.5, 0.5, 0.5,-0.5, 0.5, 0.5,0.5, 0.5, -0.5,0.5, 0.5, 20 | # -0.5 ,-0.5,-0.5, 0.5,-0.5,-0.5, 0.5,0.5,-0.5, -0.5,0.5,-0.5] 21 | let indexData = [0.GLushort, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 3, 7, 2, 6, 0, 4, 1, 5] 22 | 23 | var selectorSharedIndexBuffer: BufferRef 24 | var selectorSharedVertexBuffer: BufferRef 25 | var selectorSharedNumberOfIndexes: GLsizei 26 | var selectorSharedShader: ProgramRef 27 | 28 | type Attrib = enum 29 | aPosition 30 | 31 | type NodeSelector* = ref object 32 | mSelectedNode: Node 33 | vertexData: seq[GLfloat] 34 | color*: Color 35 | 36 | proc createVBO(ns: NodeSelector) = 37 | let c = currentContext() 38 | let gl = c.gl 39 | 40 | if selectorSharedIndexBuffer == invalidBuffer: 41 | selectorSharedIndexBuffer = gl.createBuffer() 42 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, selectorSharedIndexBuffer) 43 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW) 44 | 45 | if selectorSharedVertexBuffer == invalidBuffer: 46 | selectorSharedVertexBuffer = gl.createBuffer() 47 | gl.bindBuffer(gl.ARRAY_BUFFER, selectorSharedVertexBuffer) 48 | gl.bufferData(gl.ARRAY_BUFFER, ns.vertexData, gl.STATIC_DRAW) 49 | selectorSharedNumberOfIndexes = indexData.len.GLsizei 50 | 51 | gl.bindBuffer(gl.ARRAY_BUFFER, invalidBuffer) 52 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, invalidBuffer) 53 | 54 | proc newNodeSelector*(): NodeSelector = 55 | result.new() 56 | result.color = newColor(0.12, 1, 0, 1) 57 | 58 | proc createBoxes(ns: NodeSelector) = 59 | ns.vertexData.setLen(0) 60 | let bbox = ns.mSelectedNode.nodeBounds() 61 | if not bbox.isEmpty: 62 | ns.vertexData.add([bbox.minPoint.x, bbox.minPoint.y, bbox.minPoint.z]) 63 | ns.vertexData.add([bbox.maxPoint.x, bbox.minPoint.y, bbox.minPoint.z]) 64 | ns.vertexData.add([bbox.maxPoint.x, bbox.maxPoint.y, bbox.minPoint.z]) 65 | ns.vertexData.add([bbox.minPoint.x, bbox.maxPoint.y, bbox.minPoint.z]) 66 | 67 | ns.vertexData.add([bbox.minPoint.x, bbox.minPoint.y, bbox.maxPoint.z]) 68 | ns.vertexData.add([bbox.maxPoint.x, bbox.minPoint.y, bbox.maxPoint.z]) 69 | ns.vertexData.add([bbox.maxPoint.x, bbox.maxPoint.y, bbox.maxPoint.z]) 70 | ns.vertexData.add([bbox.minPoint.x, bbox.maxPoint.y, bbox.maxPoint.z]) 71 | 72 | ns.createVBO() 73 | 74 | proc `selectedNode=`*(ns: NodeSelector, n: Node) = 75 | ns.mSelectedNode = n 76 | 77 | proc draw*(ns: NodeSelector) = 78 | let node = ns.mSelectedNode 79 | if not node.isNil: 80 | ns.createBoxes() 81 | let c = currentContext() 82 | let gl = c.gl 83 | 84 | if selectorSharedShader == invalidProgram: 85 | selectorSharedShader = gl.newShaderProgram(vertexShader, fragmentShader, [(aPosition.GLuint, $aPosition)]) 86 | if selectorSharedShader == invalidProgram: 87 | return 88 | 89 | gl.enable(gl.DEPTH_TEST) 90 | gl.bindBuffer(gl.ARRAY_BUFFER, selectorSharedVertexBuffer) 91 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, selectorSharedIndexBuffer) 92 | 93 | gl.enableVertexAttribArray(aPosition.GLuint) 94 | gl.vertexAttribPointer(aPosition.GLuint, 3.GLint, gl.FLOAT, false, (3 * sizeof(GLfloat)).GLsizei , 0) 95 | 96 | gl.useProgram(selectorSharedShader) 97 | 98 | c.setColorUniform(selectorSharedShader, "uColor", ns.color) 99 | 100 | let vp = node.sceneView 101 | if not vp.isNil: 102 | let mvpMatrix = vp.getViewProjectionMatrix() 103 | gl.uniformMatrix4fv(gl.getUniformLocation(selectorSharedShader, "mvpMatrix"), false, mvpMatrix) 104 | 105 | when not defined(js): glLineWidth(2.0) 106 | gl.drawElements(gl.LINES, selectorSharedNumberOfIndexes, gl.UNSIGNED_SHORT) 107 | when not defined(js): glLineWidth(1.0) 108 | 109 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, invalidBuffer) 110 | gl.bindBuffer(gl.ARRAY_BUFFER, invalidBuffer) 111 | 112 | #TODO to default settings 113 | gl.disable(gl.DEPTH_TEST) 114 | -------------------------------------------------------------------------------- /rod/material/shader.nim: -------------------------------------------------------------------------------- 1 | import math, tables, hashes, sets, variant 2 | import nimx/[types, portable_gl, context] 3 | 4 | type Shader* = ref object 5 | vertShader*, fragShader*: string 6 | program: ProgramRef 7 | shaderMacroFlags: HashSet[string] 8 | shadersCache: Table[HashSet[string], tuple[shader: ProgramRef, refCount: int]] 9 | attributes: seq[tuple[index: GLuint, name: string]] 10 | 11 | needUpdate: bool 12 | 13 | # var shadersCache = initTable[HashSet[string], tuple[shader: ProgramRef, refCount: int]]() 14 | 15 | proc hash(sm: HashSet[string]): Hash = 16 | var sum = "" 17 | for macros in sm: 18 | sum &= macros 19 | result = sum.hash() 20 | result = !$result 21 | 22 | proc createShader(sh: Shader) = 23 | if currentContext().isNil: 24 | return 25 | 26 | let gl = currentContext().gl 27 | if not sh.shadersCache.contains(sh.shaderMacroFlags): 28 | var commonShaderDefines = "" 29 | 30 | var vShader = sh.vertShader 31 | var fShader = sh.fragShader 32 | 33 | for mcrs in sh.shaderMacroFlags: 34 | commonShaderDefines &= """#define """ & mcrs & "\n" 35 | 36 | vShader = commonShaderDefines & vShader 37 | fShader = commonShaderDefines & fShader 38 | 39 | sh.program = gl.newShaderProgram(vShader, fShader, sh.attributes) 40 | sh.needUpdate = false 41 | 42 | sh.shadersCache[sh.shaderMacroFlags] = (sh.program, 1) 43 | 44 | else: 45 | sh.program = sh.shadersCache[sh.shaderMacroFlags].shader 46 | sh.shadersCache[sh.shaderMacroFlags].refCount += 1 47 | sh.needUpdate = false 48 | 49 | proc finalize(s: Shader) = 50 | if s.program != invalidProgram: 51 | currentContext().gl.deleteProgram(s.program) 52 | 53 | proc newShader*(vs, fs: string, attributes: seq[tuple[index: GLuint, name: string]]): Shader = 54 | result.new(finalize) 55 | result.vertShader = vs 56 | result.fragShader = fs 57 | result.attributes = attributes 58 | result.needUpdate = true 59 | result.shaderMacroFlags = initHashSet[string]() 60 | result.shadersCache = initTable[HashSet[string], tuple[shader: ProgramRef, refCount: int]]() 61 | 62 | result.createShader() 63 | 64 | proc bindAttribLocation*(sh: Shader, index: GLuint, name: string) = 65 | currentContext().gl.bindAttribLocation(sh.program, index, name) 66 | 67 | proc addDefine*(sh: Shader, def: string) = 68 | sh.shaderMacroFlags.incl(def) 69 | sh.needUpdate = true 70 | 71 | proc removeDefine*(sh: Shader, def: string) = 72 | sh.shaderMacroFlags.excl(def) 73 | sh.needUpdate = true 74 | 75 | proc bindShader*(sh: Shader) = 76 | if sh.needUpdate: 77 | sh.createShader() 78 | 79 | if sh.needUpdate == true: 80 | echo "ERROR! Try to use not created shader program" 81 | return 82 | 83 | let gl = currentContext().gl 84 | gl.useProgram(sh.program) 85 | 86 | proc setUniform*(sh: Shader, name: string, uniform: int) = 87 | let gl = currentContext().gl 88 | gl.uniform1i(gl.getUniformLocation(sh.program, name), uniform.GLint) 89 | 90 | proc setUniform*(sh: Shader, name: string, uniform: float) = 91 | let gl = currentContext().gl 92 | gl.uniform1f(gl.getUniformLocation(sh.program, name), uniform) 93 | 94 | proc setUniform*(sh: Shader, name: string, uniform: Vector2) = 95 | let gl = currentContext().gl 96 | gl.uniform2fv(gl.getUniformLocation(sh.program, name), uniform) 97 | 98 | proc setUniform*(sh: Shader, name: string, uniform: Vector3) = 99 | let gl = currentContext().gl 100 | gl.uniform3fv(gl.getUniformLocation(sh.program, name), uniform) 101 | 102 | proc setUniform*(sh: Shader, name: string, uniform: Vector4) = 103 | let gl = currentContext().gl 104 | gl.uniform4fv(gl.getUniformLocation(sh.program, name), uniform) 105 | 106 | proc setUniform*(sh: Shader, name: string, uniform: Color) = 107 | let gl = currentContext().gl 108 | let arr = [uniform.r, uniform.g, uniform.b, uniform.a] 109 | gl.uniform4fv(gl.getUniformLocation(sh.program, name), arr) 110 | 111 | proc setUniform*(sh: Shader, name: string, uniform: Size) = 112 | let gl = currentContext().gl 113 | currentContext().setPointUniform(gl.getUniformLocation(sh.program, name), newPoint(uniform.width, uniform.height)) 114 | 115 | proc setUniform*(sh: Shader, name: string, uniform: Matrix4) = 116 | let gl = currentContext().gl 117 | gl.uniformMatrix4fv(gl.getUniformLocation(sh.program, name), false, uniform) 118 | 119 | proc setTransformUniform*(sh: Shader) = 120 | currentContext().setTransformUniform(sh.program) 121 | 122 | proc setUniform[T](sh: Shader, name: string, uniform: T) = 123 | sh.setUniform(name, uniform) 124 | 125 | 126 | # static uniforms 127 | # this uniforms assign only one time 128 | # but they will setup automatically after bind shader 129 | var procRegistry = initTable[TypeId, proc(sh: Shader, name: string, v: Variant)]() 130 | 131 | proc registerProc[T]( setUniformProc: proc(sh: Shader, name: string, value: T) ) = 132 | procRegistry[getTypeId(T)] = proc(sh: Shader, name: string, v: Variant) = 133 | let value = v.get(T) 134 | sh.setUniformProc(name, value) 135 | 136 | proc addStaticUniform*[T](sh: Shader, name: string, uniform: T) = 137 | var u = newVariant(uniform) 138 | let setter = procRegistry.getOrDefault(u.typeId) 139 | sh.setter(name, u) 140 | 141 | registerProc(setUniform[float]) 142 | registerProc(setUniform[int]) 143 | registerProc(setUniform[Vector2]) 144 | registerProc(setUniform[Vector3]) 145 | registerProc(setUniform[Vector4]) 146 | registerProc(setUniform[Color]) 147 | registerProc(setUniform[Size]) 148 | registerProc(setUniform[Matrix4]) 149 | -------------------------------------------------------------------------------- /rod/message_queue.nim: -------------------------------------------------------------------------------- 1 | import deques, hashes, macros 2 | 3 | type 4 | MessageId* = distinct int 5 | MessageQueue*[M] = ref object 6 | messages: Deque[tuple[id: MessageId, msg: M]] 7 | 8 | proc toMessageId*(str: string): MessageId = cast[MessageId](hash(str)) 9 | 10 | proc `==`*(id: MessageId, str: string): bool = int(id) == int(str.toMessageId) 11 | 12 | proc `$`*(id: MessageId): string = $(int(id)) 13 | 14 | proc createMessageQueue*[M](): MessageQueue[M] = 15 | result = new(MessageQueue[M]) 16 | result.messages = initDeque[tuple[id: MessageId, msg: M]]() 17 | 18 | proc post*[M](q: MessageQueue[M], id: string, m: M = default(M)) = 19 | q.messages.addLast((id: id.toMessageId, msg: m)) 20 | 21 | proc isEmpty*[M](q: MessageQueue[M]): bool = q.messages.len == 0 22 | 23 | iterator popChunk*[M](q: MessageQueue[M], chunk: int = 100): tuple[id: MessageId, msg:M] = 24 | var i = 0 25 | while q.messages.len > 0 and i < chunk: 26 | var v = q.messages.popFirst() 27 | yield v 28 | inc i 29 | -------------------------------------------------------------------------------- /rod/postprocess_context.nim: -------------------------------------------------------------------------------- 1 | import nimx/[types, portable_gl] 2 | import rod/component 3 | import rod_types 4 | 5 | export PostprocessContext 6 | 7 | proc newPostprocessContext*(): PostprocessContext = 8 | result.new() 9 | result.shader = invalidProgram 10 | result.setupProc = proc(c: Component) = discard 11 | result.drawProc = proc(c: Component) = discard 12 | -------------------------------------------------------------------------------- /rod/property_editors/propedit_registry.nim: -------------------------------------------------------------------------------- 1 | import nimx/view 2 | import nimx/property_editors/propedit_registry as npr 3 | 4 | export npr 5 | 6 | import rod/node 7 | import variant 8 | 9 | proc propertyEditorForProperty*(n: Node, title: string, v: Variant, onChangeCallback, changeInspectorCallback: proc() {.gcsafe.}): View = 10 | propertyEditorForProperty(newVariant(n), title, v, onChangeCallback, changeInspectorCallback) 11 | -------------------------------------------------------------------------------- /rod/ray.nim: -------------------------------------------------------------------------------- 1 | import nimx/matrixes 2 | import nimx/types 3 | import rod/rod_types 4 | 5 | type RayCastInfo* = object 6 | node*: Node 7 | distance*: float32 8 | position*: Vector3 9 | 10 | type Ray* = object 11 | origin*: Vector3 12 | direction*: Vector3 13 | 14 | proc transform*(r:Ray, mat:Matrix4): Ray = 15 | result.direction.x = r.direction.x * mat[0] + r.direction.y * mat[4] + r.direction.z * mat[8] 16 | result.direction.y = r.direction.x * mat[1] + r.direction.y * mat[5] + r.direction.z * mat[9] 17 | result.direction.z = r.direction.x * mat[2] + r.direction.y * mat[6] + r.direction.z * mat[10] 18 | # result.direction.normalize() 19 | 20 | result.origin = mat * r.origin 21 | 22 | proc intersectWithPlane*(r: Ray, planeNormal: Vector3, dist: Coord, output: var Vector3): bool = 23 | var denom = dot(r.direction, planeNormal) 24 | if denom != 0: 25 | var t = -(dot(r.origin, planeNormal) + dist) / denom 26 | if t < 0: 27 | return false 28 | output = r.origin + r.direction * t 29 | return true 30 | elif dot(planeNormal, r.origin) + dist == 0: 31 | output = r.origin 32 | return true 33 | else: 34 | return false 35 | 36 | proc intersectWithPlane*(r: Ray, planeNormal, pointOnPlane: Vector3, output: var Vector3): bool = 37 | r.intersectWithPlane(planeNormal, -dot(planeNormal, pointOnPlane), output) 38 | 39 | 40 | proc intersectWithAABB*(r: Ray, minCoord, maxCoord: Vector3, distance: var float32): bool = 41 | # r.direction is unit direction vector of ray 42 | var dirfrac = newVector3(1.0 / r.direction.x, 1.0 / r.direction.y, 1.0 / r.direction.z) 43 | 44 | # minCoord is the corner of AABB with minimal coordinates - left bottom, maxCoord is maximal corner 45 | # r.origin is origin of ray 46 | let t1 = (minCoord.x - r.origin.x)*dirfrac.x 47 | let t2 = (maxCoord.x - r.origin.x)*dirfrac.x 48 | let t3 = (minCoord.y - r.origin.y)*dirfrac.y 49 | let t4 = (maxCoord.y - r.origin.y)*dirfrac.y 50 | let t5 = (minCoord.z - r.origin.z)*dirfrac.z 51 | let t6 = (maxCoord.z - r.origin.z)*dirfrac.z 52 | 53 | let tmin = max(max(min(t1, t2), min(t3, t4)), min(t5, t6)) 54 | let tmax = min(min(max(t1, t2), max(t3, t4)), max(t5, t6)) 55 | 56 | # if tmax < 0, ray (line) is intersecting AABB, but whole AABB is behing us 57 | if tmax < 0.0 or tmin < 0.0: 58 | return false 59 | 60 | # if tmin > tmax, ray doesn't intersect AABB 61 | if tmin > tmax : 62 | return false 63 | 64 | distance = tmin 65 | return true 66 | 67 | proc intersectWithTriangle(r: Ray, v0, v1, v2: Vector3, distance: var float32): bool {.used.} = 68 | let edge1 = v1 - v0 69 | let edge2 = v2 - v0 70 | let pvec = cross(r.direction, edge2) 71 | var u, v: float32 72 | 73 | let det = dot(edge1, pvec) #edge1.x * pvec.x + edge1.y * pvec.y + edge1.z * pvec.z #edge1 & pvec; 74 | var tvec = newVector3(0.0) 75 | var qvec = newVector3(0.0) 76 | 77 | if det > 0.0001 : 78 | tvec = r.origin - v0 79 | u = dot(tvec, pvec) # tvec.x * pvec.x + tvec.y * pvec.y + tvec.z * pvec.z #tvec & pvec; 80 | if u < 0.0f or u > det : 81 | return false 82 | 83 | qvec = cross(tvec, edge1) 84 | v = dot(r.direction, qvec) #r.direction.x * qvec.x + r.direction.y * qvec.y + r.direction.z * qvec.z #dir & qvec; 85 | if v < 0.0f or u + v > det : 86 | return false 87 | 88 | elif det < -0.0001 : 89 | tvec = r.origin - v0 90 | u = dot(tvec, pvec) #tvec.x * pvec.x + tvec.y * pvec.y + tvec.z * pvec.z #tvec & pvec; 91 | if u > 0.0f or u < det : 92 | return false 93 | 94 | qvec = cross(tvec, edge1) 95 | v = dot(r.direction, qvec) #r.direction.x * qvec.x + r.direction.y * qvec.y + r.direction.z * qvec.z #dir & qvec; 96 | if v > 0.0f or u + v < det : 97 | return false 98 | else: 99 | return false 100 | 101 | distance = edge2.x * qvec.x + edge2.y * qvec.y + edge2.z * qvec.z # edge2 & qvec; 102 | var fInvDet = 1.0 / det 103 | distance *= fInvDet 104 | return true 105 | -------------------------------------------------------------------------------- /rod/rod_types.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ types, matrixes, animation, view, image, portable_gl ] 2 | import rod / message_queue 3 | import quaternion 4 | import tables 5 | const maxLightsCount* = 8 6 | 7 | when defined(rodedit): 8 | import json 9 | 10 | type 11 | NodeFlags* = enum # Don't change the order! 12 | enabled 13 | affectsChildren # Should posteffects affect only this node or its children as well 14 | dirty 15 | serializable 16 | 17 | Node* = ref object 18 | mTranslation*: Vector3 19 | mRotation*: Quaternion 20 | mScale*: Vector3 21 | renderComponents*: seq[RenderComponent] 22 | scriptComponents*: seq[ScriptComponent] 23 | children*: seq[Node] 24 | mParent*: Node 25 | name*: string 26 | animations*: TableRef[string, Animation] 27 | mSceneView*: SceneView 28 | mMatrix*: Matrix4 29 | worldMatrix*: Matrix4 30 | alpha*: Coord 31 | composition*: Composition 32 | mAnchorPoint*: Vector3 33 | mFlags*: set[NodeFlags] 34 | 35 | when defined(rodedit): 36 | jAnimations*: JsonNode 37 | 38 | BBox* = object 39 | minPoint*: Vector3 40 | maxPoint*: Vector3 41 | 42 | Frustum* = BBox 43 | 44 | Component* = ref object of RootRef 45 | node*: Node 46 | 47 | ScriptComponent* = ref object of Component 48 | RenderComponent* = ref object of Component 49 | 50 | AnimationRunnerComponent* = ref object of ScriptComponent 51 | runner*: AnimationRunner 52 | 53 | PostprocessContext* = ref object 54 | shader*: ProgramRef 55 | setupProc*: proc(c: Component) 56 | drawProc*: proc(c: Component) 57 | depthImage*: SelfContainedImage 58 | depthMatrix*: Matrix4 59 | 60 | System* = ref object of RootRef 61 | sceneView*: SceneView 62 | 63 | SceneView* = ref object of View 64 | systems*: seq[System] 65 | viewMatrixCached*: Matrix4 66 | viewProjMatrix*: Matrix4 67 | mCamera*: Camera 68 | mRootNode*: Node 69 | animationRunners*: seq[AnimationRunner] 70 | lightSources*: TableRef[string, LightSource] 71 | uiComponents*: seq[UIComponent] 72 | postprocessContext*: PostprocessContext 73 | editing*: bool 74 | afterDrawProc*: proc() {.gcsafe.} # PRIVATE DO NOT USE!!! 75 | 76 | Composition* = ref object 77 | url*: string 78 | node*: Node 79 | when defined(rodedit): 80 | originalUrl*: string # used in editor to restore url 81 | 82 | CameraProjection* = enum 83 | cpOrtho, 84 | cpPerspective 85 | 86 | Camera* = ref object of ScriptComponent 87 | projectionMode*: CameraProjection 88 | zNear*, zFar*, fov*: Coord 89 | viewportSize*: Size 90 | 91 | UIComponent* = ref object of RenderComponent 92 | mView*: View 93 | mEnabled*: bool 94 | 95 | LightSource* = ref object of ScriptComponent 96 | mLightAmbient*: float32 97 | mLightDiffuse*: float32 98 | mLightSpecular*: float32 99 | mLightConstant*: float32 100 | mLightLinear*: float32 101 | mLightQuadratic*: float32 102 | mLightAttenuation*: float32 103 | 104 | mLightColor*: Color 105 | 106 | lightPosInited*: bool 107 | lightAmbientInited*: bool 108 | lightDiffuseInited*: bool 109 | lightSpecularInited*: bool 110 | lightConstantInited*: bool 111 | lightLinearInited*: bool 112 | lightQuadraticInited*: bool 113 | mLightAttenuationInited*: bool 114 | -------------------------------------------------------------------------------- /rod/system/messaging.nim: -------------------------------------------------------------------------------- 1 | import rod / [ rod_types, message_queue, node, systems, viewport, component ] 2 | import strutils, typetraits, logging 3 | 4 | export message_queue, systems 5 | 6 | type 7 | NodeMessage = object 8 | path: string 9 | sender: Node 10 | data: string 11 | 12 | NodeMessageQueue = MessageQueue[NodeMessage] 13 | 14 | MessageSystem* = ref object of System 15 | messageQueue: NodeMessageQueue 16 | 17 | method onMessage*(c: ScriptComponent, id: MessageId, data: string, sender: Node) {.base.} = discard 18 | 19 | method init*(s: MessageSystem) = 20 | s.messageQueue = createMessageQueue[NodeMessage]() 21 | 22 | proc proceedMessage(s: MessageSystem, id: MessageId, msg: NodeMessage) = 23 | var sp = msg.path.split("/") 24 | var targetComponent = "" 25 | let lsp = sp[^1].split("#") 26 | if lsp.len > 1: 27 | sp[^1] = lsp[0] 28 | targetComponent = lsp[1] 29 | 30 | var receiver: Node 31 | try: 32 | receiver = msg.sender.findNode(sp) 33 | except Exception as e: 34 | warn "receiver not found ", $sp, " ", e.msg 35 | 36 | if receiver.isNil: 37 | warn "receiver not found ", $sp 38 | return 39 | 40 | if targetComponent.len == 0: 41 | warn "to target component" 42 | return 43 | 44 | let comp = receiver.componentIfAvailable(targetComponent) 45 | if comp.isNil or comp.isRenderComponent: return 46 | comp.ScriptComponent.onMessage(id, msg.data, msg.sender) 47 | 48 | method update*(s: MessageSystem, dt: float) = 49 | for id, msg in s.messageQueue.popChunk(chunk = 50): 50 | s.proceedMessage(id, msg) 51 | 52 | proc post(s: MessageSystem, id: string, msg: NodeMessage) = 53 | s.messageQueue.post(id, msg) 54 | 55 | proc post*(n: Node, path: string, id: string, data: string = "") = 56 | if n.sceneView.isNil: 57 | warn "Node sendMessage \"", id, "\" from node \"", n.name, "\" failed, sceneView is nil!" 58 | return 59 | var msg = NodeMessage(path: path, sender: n, data: data) 60 | n.sceneView.system(MessageSystem).post(id, msg) 61 | 62 | registerSystem(MessageSystem) 63 | -------------------------------------------------------------------------------- /rod/systems.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ class_registry ] 2 | import rod / rod_types 3 | import tables 4 | 5 | export System 6 | 7 | method init*(s: System) {.base.} = discard 8 | method update*(s: System, dt: float) {.base.} = discard 9 | method draw*(s: System) {.base.} = discard 10 | 11 | proc createSystem*(name: string): System = 12 | if isClassRegistered(name) == false: 13 | raise newException(Exception, "System " & name & " is not registered") 14 | 15 | result = newObjectOfClass(name).System 16 | result.init() 17 | 18 | proc createSystem*(T: typedesc): System = 19 | result = createSystem($T) 20 | 21 | template registerSystem*(T: typedesc) = 22 | registerClass(T) 23 | -------------------------------------------------------------------------------- /rod/tools/rodasset/migrator.nim: -------------------------------------------------------------------------------- 1 | import json, os, strutils 2 | 3 | import tree_traversal 4 | 5 | proc hasComponentOfType(n: JsonNode, typ: string): bool = 6 | for c in n.componentNodes: 7 | if c.typ == typ: return true 8 | 9 | proc convertDictComponentsToArray*(node: JsonNode) = 10 | let comps = node{"components"} 11 | if not comps.isNil and comps.kind == JObject: 12 | let newComps = newJArray() 13 | for k, v in comps: 14 | v["_c"] = %k 15 | newComps.add(v) 16 | node["components"] = newComps 17 | 18 | proc removeComponentsOfType(n: JsonNode, typ: string) = 19 | var i = 0 20 | let comps = n["components"] 21 | while i < comps.len: 22 | if comps[i]["_c"].str == typ: 23 | comps.elems.del(i) 24 | else: 25 | inc i 26 | 27 | proc removeAnimationsForProperties(composition: JsonNode, nodeName, propertyName: string) = 28 | let anims = composition{"animations"} 29 | let fqpn = nodeName & "." & propertyName 30 | if not anims.isNil: 31 | for animName, anim in anims: 32 | if fqpn in anim: 33 | echo "Deleting ", fqpn, " from animation ", animName 34 | anim.delete(fqpn) 35 | 36 | proc fixupChannelLevelsWithDrawablesOnTheSameNode(composition, n: JsonNode) = 37 | if n.hasComponentOfType("ChannelLevels") and (n.hasComponentOfType("Sprite") or n.hasComponentOfType("Text") or n.hasComponentOfType("Solid")): 38 | let children = n{"children"} 39 | if not children.isNil and children.len > 0: 40 | raise newException(Exception, "Found Channel levels in node with drawable and children") 41 | n.removeComponentsOfType("ChannelLevels") 42 | let name = n{"name"} 43 | if not name.isNil: 44 | echo "removing ChannelLevels from ", name 45 | for p in ["inWhite", "inBlack", "inGamma", "outWhite", "outBlack", "redInWhite", 46 | "redInBlack", "redInGamma", "redOutWhite", "redOutBlack", "greenInWhite", 47 | "greenInBlack", "greenInGamma", "greenOutWhite", "greenOutBlack", "blueInWhite", 48 | "blueInBlack", "blueInGamma", "blueOutWhite", "blueOutBlack"]: 49 | 50 | composition.removeAnimationsForProperties(name.str, p) 51 | 52 | proc upgradeTo1(composition: JsonNode): JsonNode = 53 | for n in composition.allNodes: 54 | n.convertDictComponentsToArray() 55 | fixupChannelLevelsWithDrawablesOnTheSameNode(composition, n) 56 | 57 | const upgraders = { 58 | 1: upgradeTo1 59 | } 60 | 61 | proc upgradeAssetBundle*(path: string) = 62 | for f in walkDirRec(path): 63 | if f.endsWith(".json") or f.endsWith(".jcomp"): 64 | echo "processing ", f 65 | try: 66 | var j = parseJson(readFile(f)) 67 | for upgrader in upgraders: 68 | let v = j{"version"}.getInt() 69 | if v < upgrader[0]: 70 | let newV = upgrader[1](j) 71 | if not newV.isNil: 72 | j = newV 73 | j["version"] = %upgrader[0] 74 | writeFile(f, j.pretty().replace(" \l", "\l")) 75 | except: 76 | echo "ERROR: ", getCurrentExceptionMsg() 77 | echo getCurrentException().getStackTrace() 78 | -------------------------------------------------------------------------------- /rod/tools/rodasset/rodasset.nim: -------------------------------------------------------------------------------- 1 | # This is a wrapper to rodasset_main. It compiles rodasset_main with rod 2 | # plugins of the project that calls the wrapper. 3 | import ../tool_wrapper 4 | runWrapper("rodasset", "rod/tools/rodasset/rodasset_main.nim") 5 | -------------------------------------------------------------------------------- /rod/tools/rodasset/rodasset.nims: -------------------------------------------------------------------------------- 1 | --d:release 2 | --opt:speed 3 | --stackTrace:on 4 | --lineTrace:on 5 | --d:nimTrMacros 6 | --checks:on 7 | --threads:on 8 | -------------------------------------------------------------------------------- /rod/tools/rodasset/settings.nim: -------------------------------------------------------------------------------- 1 | import hashes, strutils, os 2 | 3 | type AudioSettings* = object 4 | extension*: string # Audio file extension 5 | 6 | type GraphicsSettings* = object 7 | downsampleRatio*: float 8 | compressOutput*: bool 9 | compressToPVR*: bool 10 | extrusion*: int 11 | disablePotAdjustment*: bool 12 | packCompositions*: bool 13 | quantizeExceptions*: seq[string] 14 | posterizeExceptions*: seq[string] 15 | noDownsample*: seq[string] 16 | noAlphaCrop*: seq[string] 17 | useWebp*: bool 18 | webpQuality*: float 19 | webpLossless*: bool 20 | 21 | type Settings* = ref object 22 | graphics*: GraphicsSettings 23 | audio*: AudioSettings 24 | androidExternal*: bool 25 | debugOnly*: bool 26 | 27 | proc init(g: var GraphicsSettings) {.inline.} = 28 | g.downsampleRatio = 1.0 29 | g.compressOutput = true 30 | g.compressToPVR = false 31 | g.extrusion = 1 32 | g.disablePotAdjustment = true 33 | g.webpLossless = true 34 | g.quantizeExceptions = @[] 35 | g.posterizeExceptions = @[] 36 | 37 | proc newSettings*(): Settings = 38 | result.new() 39 | result.audio.extension = "ogg" 40 | result.graphics.init() 41 | 42 | proc hash*(s: AudioSettings | GraphicsSettings): Hash = hash($(s)) 43 | 44 | proc parseExceptions(val, dir: string): seq[string] = 45 | let values = val.split(",") 46 | 47 | result = @[] 48 | for v in values: 49 | let vStripped = v.strip() 50 | if vStripped.contains("*"): 51 | for file in walkFiles(dir / vStripped): 52 | result.add(splitFile(file).name) 53 | else: 54 | result.add(vStripped) 55 | 56 | proc parseConfig*(rabFilePath: string, fast: bool = false): Settings = 57 | result = newSettings() 58 | 59 | result.graphics.extrusion = 1 60 | result.graphics.downsampleRatio = 1.0 61 | result.graphics.packCompositions = true 62 | 63 | for line in lines(rabFilePath): 64 | let pairs = line.split(" ", 1) 65 | doAssert(pairs.len == 2, "Wrong rab config params format in "& pairs[0]) 66 | let key = pairs[0] 67 | let val = pairs[1] 68 | case key: 69 | of "extrusion": 70 | result.graphics.extrusion = parseInt(val) 71 | of "disablePotAdjustment": 72 | result.graphics.disablePotAdjustment = parseBool(val) 73 | of "downsampleRatio": 74 | result.graphics.downsampleRatio = parseFloat(val) 75 | of "exceptions": 76 | if not fast: 77 | result.graphics.quantizeExceptions = parseExceptions(val, rabFilePath.parentDir) 78 | of "noquant": 79 | if not fast: 80 | result.graphics.quantizeExceptions = parseExceptions(val, rabFilePath.parentDir) 81 | of "noposterize": 82 | if not fast: 83 | result.graphics.posterizeExceptions = parseExceptions(val, rabFilePath.parentDir) 84 | of "noDownsample": 85 | if not fast: 86 | result.graphics.noDownsample = parseExceptions(val, rabFilePath.parentDir) 87 | of "noAlphaCrop": 88 | if not fast: 89 | result.graphics.noAlphaCrop = parseExceptions(val, rabFilePath.parentDir) 90 | of "debugOnly": 91 | result.debugOnly = parseBool(val) 92 | of "packCompositions": 93 | result.graphics.packCompositions = parseBool(val) 94 | of "androidExternal": 95 | result.androidExternal = parseBool(val) 96 | of "quality": 97 | result.graphics.webpQuality = parseFloat(val) 98 | of "webpLossless": 99 | result.graphics.webpLossless = parseBool(val) 100 | -------------------------------------------------------------------------------- /rod/tools/rodasset/tree_traversal.nim: -------------------------------------------------------------------------------- 1 | import json, lists 2 | 3 | proc append[T](lst: var DoublyLinkedList[T], elems: openarray[T]) = 4 | for e in elems: lst.append(e) 5 | 6 | iterator allNodes*(n: JsonNode): JsonNode = 7 | var lst = initDoublyLinkedList[JsonNode]() # SinglyLinkedList would be sufficient here, but its api is currently underdeveloped 8 | lst.append(n) 9 | 10 | while not lst.head.isNil: 11 | let n = lst.head.value 12 | lst.remove(lst.head) 13 | yield n 14 | let children = n{"children"} 15 | if not children.isNil: 16 | lst.append(children.elems) 17 | 18 | iterator componentNodes*(n: JsonNode): tuple[typ: string, node: JsonNode] = 19 | let components = n{"components"} 20 | if not components.isNil: 21 | var comps = newSeq[(string, JsonNode)]() 22 | if components.kind == JObject: 23 | for k, v in components: 24 | comps.add((k, v)) 25 | elif components.kind == JArray: 26 | for c in components.elems: 27 | comps.add((c["_c"].str, c)) 28 | for c in comps: 29 | yield c 30 | 31 | iterator componentNodesOfType*(n: JsonNode, typ: string): JsonNode = 32 | for c in n.componentNodes: 33 | if c.typ == typ: yield c.node 34 | 35 | iterator allComponentNodesOfType*(n: JsonNode, typ: string): (JsonNode, JsonNode) = 36 | for n in n.allNodes: 37 | for c in n.componentNodesOfType(typ): 38 | yield(n, c) 39 | 40 | iterator allSpriteNodes*(n: JsonNode): (JsonNode, JsonNode) = 41 | for n, c in allComponentNodesOfType(n, "Sprite"): yield(n, c) 42 | for n, c in allComponentNodesOfType(n, "NinePartSprite"): yield(n, c) 43 | 44 | iterator tilemapImageLayersNodes*(n: JsonNode): (JsonNode, JsonNode) = 45 | for n, c in allComponentNodesOfType(n, "ImageMapLayer"): yield(n, c) 46 | 47 | iterator tilemapNodes*(n: JsonNode): (JsonNode, JsonNode) = 48 | for n, c in allComponentNodesOfType(n, "TileMap"): yield(n, c) 49 | 50 | 51 | iterator allMeshComponentNodes*(n: JsonNode): (JsonNode, JsonNode) = 52 | for n, c in allComponentNodesOfType(n, "MeshComponent"): yield(n, c) 53 | 54 | iterator compositionAnimationsForNodeProperty*(compositionNode: JsonNode, nodeName, propertyName: string): (string, JsonNode) = 55 | let animName = nodeName & "." & propertyName 56 | for k, v in compositionNode["animations"]: 57 | let a = v{animName} 58 | if not a.isNil: 59 | yield(k, a) 60 | 61 | proc findNode*(n: JsonNode, name: string): JsonNode = 62 | for c in n.allNodes: 63 | if c{"name"}.getStr() == name: return c 64 | 65 | proc firstComponentOfType*(n: JsonNode, typ: string): JsonNode = 66 | for c in n.componentNodesOfType(typ): return c 67 | -------------------------------------------------------------------------------- /rod/tools/tool_wrapper.nim: -------------------------------------------------------------------------------- 1 | import os, osproc, strutils 2 | 3 | proc rodPluginFile(): string = 4 | result = getCurrentDir() / "rodplugin.nim" 5 | if fileExists(result): return 6 | result = "" 7 | 8 | proc nimblePath(package: string): string = 9 | var (packageDir, err) = execCmdEx("nimble path " & package) 10 | if err == 0: 11 | let lines = packageDir.splitLines() 12 | if lines.len > 1: 13 | result = lines[^2] 14 | 15 | if result.len == 0: 16 | raise newException(Exception, "Package " & package & " not found in nimble packages") 17 | 18 | proc compileRealBin(bin, toolName, mainNim: string, useDanger: bool, cflags: seq[string]) = 19 | createDir(bin.parentDir()) 20 | var args = @["c", "--threads:on", "-d:release", 21 | "-d:rodplugin", "--warning[LockLevel]:off"] 22 | if useDanger: 23 | args.add(@["-d:danger" ]) 24 | else: 25 | args.add(@["--stackTrace:on", "--lineTrace:on"]) 26 | 27 | args.add(cflags) 28 | args.add("--out:" & bin) 29 | let plug = rodPluginFile() 30 | if plug.len != 0: 31 | args.add("-d:rodPluginFile=" & plug) 32 | args.add("--path:" & plug.parentDir / "src") # TODO: "src" should be gone 33 | args.add(nimblePath("rod") / mainNim) 34 | let nim = findExe("nim") 35 | echo nim, " ", args.join(" ") 36 | if startProcess(nim, args = args, options = {poParentStreams}).waitForExit != 0: 37 | raise newException(Exception, toolName & " compilation failed") 38 | 39 | proc wrapperAUX(bin, toolName, pathToToolMainNim: string, useDanger:bool, cflags: seq[string] = @[]) = 40 | var needsCompile = not fileExists(bin) 41 | var passArgs = newSeq[string]() 42 | 43 | for i in 1 .. paramCount(): 44 | let p = paramStr(i) 45 | if p == "--recompile": 46 | needsCompile = true 47 | else: 48 | passArgs.add(p) 49 | 50 | if needsCompile: 51 | echo "Compiling ", toolName 52 | compileRealBin(bin, toolName, pathToToolMainNim, useDanger, cflags) 53 | 54 | # Run the tool 55 | if startProcess(bin, args = passArgs, options = {poParentStreams}).waitForExit != 0: 56 | raise newException(Exception, toolName & " failed") 57 | 58 | 59 | proc runWrapper*(toolName, pathToToolMainNim: string) = 60 | var prefix = getEnv("ROD_ASSET_BIN") 61 | if prefix.len == 0: 62 | prefix = getTempDir() 63 | let cd = getCurrentDir() 64 | let projName = splitPath(cd).tail 65 | let bin = prefix / projName & "_" & toolName & (when defined(windows): ".exe" else: "") 66 | wrapperAUX(bin, toolName, pathToToolMainNim, useDanger = true) 67 | 68 | proc runEditorWrapper*(toolName, pathToToolMainNim: string) = 69 | let cd = getCurrentDir() 70 | let bin = cd / splitPath(cd).tail & "_" & toolName & (when defined(windows): ".exe" else: "") 71 | wrapperAUX(bin, toolName, pathToToolMainNim, useDanger = false, @["-d:rodedit"]) 72 | -------------------------------------------------------------------------------- /rod/utils/bin_serializer.nim: -------------------------------------------------------------------------------- 1 | import tables, streams, json, os, strutils 2 | import nimx / [image, types, pathutils ] 3 | import rod/quaternion 4 | import serialization_helpers 5 | 6 | when not defined(js): 7 | import os 8 | 9 | type 10 | BinSerializer* = ref object 11 | strTab*: Table[int16, string] 12 | revStrTab*: Table[string, int16] 13 | compsTable*: Table[string, int32] 14 | stream*: (when defined(js): Stream else: StringStream) 15 | stringEntries*: seq[int32] 16 | images*: JsonNode 17 | totalAlignBytes*: int 18 | assetBundlePath*: string 19 | 20 | proc newBinSerializer*(): BinSerializer = 21 | result.new() 22 | 23 | proc align*(b: BinSerializer, sz: int) = 24 | doAssert(sz <= 4) 25 | let p = b.stream.getPosition() 26 | let m = p mod sz 27 | if m != 0: 28 | inc(b.totalAlignBytes, sz - m) 29 | for i in 0 ..< (sz - m): 30 | b.stream.write(0xff'u8) 31 | 32 | proc newString*(b: BinSerializer, s: string): int16 = 33 | doAssert(not s.endsWith(".json")) 34 | if s in b.revStrTab: 35 | result = b.revStrTab[s] 36 | else: 37 | result = int16(b.strTab.len) 38 | b.strTab[result] = s 39 | b.revStrTab[s] = result 40 | 41 | 42 | type Serializable = 43 | array | openarray | tuple | object | seq | string | int8 | int16 | int32 | bool | enum | uint8 | uint16 | uint32 | Image 44 | 45 | proc write*(b: BinSerializer, data: float32) = 46 | b.align(sizeof(data)) 47 | b.stream.write(data) 48 | 49 | proc write*[T: Serializable](b: BinSerializer, data: T) 50 | 51 | proc writeArrayNoLen*[T](b: BinSerializer, data: openarray[T]) = 52 | when isPODType(T): 53 | if data.len != 0: 54 | b.align(alignsize(type(data[0]))) 55 | b.stream.writeData(unsafeAddr data[0], data.len * sizeof(data[0])) 56 | else: 57 | for i in 0 ..< data.len: 58 | b.write(data[i]) 59 | 60 | proc writeStrNoLen*(b: BinSerializer, data: string) = 61 | if data.len != 0: 62 | b.stream.writeData(unsafeAddr data[0], data.len) 63 | 64 | proc getNeighbourImageBundlePath(b: BinSerializer, p2: string):tuple[asset:string, bundle:string]= 65 | when not defined(js): 66 | var curDir = getCurrentDir() / "res" 67 | var path = curDir / b.assetBundlePath / p2 68 | 69 | normalizePath(path, false) 70 | var dir = path.splitFile().dir 71 | 72 | for p in parentDirs(dir): 73 | if p == curDir: break 74 | 75 | if fileExists(p / "config.rab"): 76 | var p3 = path.substr(p.len + 1) 77 | path = p.subStr(curDir.len + 1) 78 | normalizePath(path, false) 79 | normalizePath(p3, false) 80 | 81 | return (asset:p3, bundle: path) 82 | 83 | raise newException(Exception, "Neighbour assetbundle not found for " & p2 ) 84 | 85 | proc write*[T: Serializable](b: BinSerializer, data: T) = 86 | when T is array: 87 | b.writeArrayNoLen(data) 88 | 89 | elif T is tuple|object: 90 | when isPODType(T): 91 | b.align(alignsize(type(data[0]))) 92 | b.stream.write(data) 93 | else: 94 | for k, v in fieldPairs(data): 95 | b.write(v) 96 | 97 | elif T is seq or T is openarray: 98 | doAssert(data.len <= uint16.high.int) 99 | b.write(data.len.uint16) 100 | if data.len != 0: 101 | b.writeArrayNoLen(data) 102 | 103 | elif T is string: 104 | if data.len == 0: 105 | b.write(int16(-1)) 106 | else: 107 | b.align(sizeof(int16)) 108 | b.stringEntries.add(b.stream.getPosition().int32) 109 | let off = b.newString(data) 110 | b.write(off) 111 | 112 | elif T is int16 | int32 | uint16 | uint32: 113 | b.align(sizeof(data)) 114 | b.stream.write(data) 115 | 116 | elif T is int8 | uint8: 117 | b.stream.write(data) 118 | 119 | elif T is bool: 120 | b.stream.write(data.uint8) 121 | 122 | elif T is enum: 123 | var v = (when ord(high(T)) < high(int8): int8 else: int16)data 124 | b.write(v) 125 | 126 | elif T is Image: 127 | if data.isNil: 128 | b.write(int16(-1)) 129 | else: 130 | var idx = 0 131 | let path = filePath(data) 132 | 133 | for j in b.images: 134 | if j["orig"].str == path: 135 | break 136 | inc idx 137 | 138 | if idx == b.images.len: 139 | b.write(int16(-2)) 140 | let (asset, bundle) = b.getNeighbourImageBundlePath(path) 141 | b.write(asset) 142 | b.write(bundle) 143 | else: 144 | b.write(idx.int16) 145 | 146 | elif T is int | int64: 147 | {.error: "int and int64 not supported " .} 148 | else: 149 | {.error: "Unknown type " .} 150 | 151 | proc visit*(b: BinSerializer, v: float32) {.inline.} = 152 | b.write(v) 153 | 154 | proc visit*[T: Serializable](b: BinSerializer, v: T) {.inline.} = 155 | b.write(v) 156 | 157 | proc visit*(b: BinSerializer, v: Quaternion) = 158 | b.write(v.x) 159 | b.write(v.y) 160 | b.write(v.z) 161 | b.write(v.w) 162 | 163 | proc visit*(b: BinSerializer, images: seq[Image], frameOffsets: seq[Point]) = 164 | let sz = images.len 165 | b.write(sz.int16) 166 | for i in 0 ..< sz: 167 | b.write(images[i]) 168 | -------------------------------------------------------------------------------- /rod/utils/image_serialization.nim: -------------------------------------------------------------------------------- 1 | {.deprecated.} 2 | import rod/tools/serializer 3 | export deserializeImage 4 | -------------------------------------------------------------------------------- /rod/utils/json_deserializer.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ image, types, pathutils, assets/asset_manager ] 2 | import rod/quaternion 3 | import json, strutils, os, strutils 4 | 5 | 6 | type JsonDeserializer* = ref object 7 | node*: JsonNode 8 | disableAwake*: bool 9 | compPath*: string # Path relative to bundle root 10 | getImageForPath*: proc(path: string, offset: var Point): Image 11 | 12 | proc newJsonDeserializer*(): JsonDeserializer = 13 | result.new() 14 | 15 | proc deserializeImage(b: JsonDeserializer, j: JsonNode): Image 16 | 17 | proc get*[T](b: JsonDeserializer, j: JsonNode, v: var T) = 18 | when T is float | float32 | float64: 19 | v = j.getFloat() 20 | elif T is int | int32 | int64 | int16 | uint64 | uint32 | uint16: 21 | v = T(j.getBiggestInt()) 22 | elif T is string: 23 | v = j.getStr() 24 | elif T is bool: 25 | v = j.getBool() 26 | elif T is Rect: 27 | v = newRect(j[0].getFloat(), j[1].getFloat(), j[2].getFloat(), j[3].getFloat()) 28 | elif T is Quaternion: 29 | v = newQuaternion(j[0].getFloat(), j[1].getFloat(), j[2].getFloat(), j[3].getFloat()) 30 | elif T is Color: 31 | v = newColor(j[0].getFloat(), j[1].getFloat(), j[2].getFloat()) 32 | if j.len > 3: 33 | v.a = j[3].getFloat() 34 | elif T is tuple: 35 | var i = 0 36 | for k, vv in fieldPairs(v): 37 | b.get(j[i], vv) 38 | inc i 39 | elif T is object: 40 | for k, vv in fieldPairs(v): 41 | b.get(j[k], vv) 42 | elif T is Image: 43 | v = b.deserializeImage(j) 44 | 45 | elif T is array: 46 | for i in 0 ..< v.len: 47 | b.get(j[i], v[i]) 48 | 49 | elif T is enum: 50 | case j.kind 51 | of JString: 52 | v = parseEnum[T](j.str) 53 | of JInt: 54 | v = cast[T](j.num) 55 | else: 56 | discard 57 | 58 | elif T is seq: 59 | let sz = j.len 60 | v.setLen(sz) 61 | for i in 0 ..< sz: 62 | b.get(j[i], v[i]) 63 | else: 64 | {.error: "unknown type " & $T .} 65 | 66 | proc imagePath(b: JsonDeserializer, jimage: JsonNode): string = 67 | case jimage.kind 68 | of JString: 69 | if jimage.str.len == 0: 70 | result = "" 71 | else: 72 | let parentDir = b.compPath.parentDirEx 73 | if parentDir.len > 0: 74 | result = parentDir & "/" & jimage.str 75 | else: 76 | result = jimage.str 77 | normalizePath(result, false) 78 | of JObject: 79 | result = jimage["orig"].getStr() 80 | else: doAssert(false) 81 | 82 | proc deserializeImage(b: JsonDeserializer, j: JsonNode, offset: var Point): Image = 83 | let path = b.imagePath(j) 84 | b.getImageForPath(path, offset) 85 | 86 | proc deserializeImage(b: JsonDeserializer, j: JsonNode): Image = 87 | var path = b.imagePath(j) 88 | if path.len == 0: 89 | return nil 90 | 91 | when not defined(rodplugin): 92 | var p = newPoint(0,0) 93 | result = b.getImageForPath(path, p) 94 | 95 | if result.isNil: 96 | result = imageWithSize(newSize(1.0, 1.0)) 97 | result.setFilePath(path) 98 | 99 | proc visit*[T](b: JsonDeserializer, v: var T, key: string) = 100 | let j = b.node{key} 101 | if not j.isNil: 102 | b.get(j, v) 103 | 104 | proc visit*[T](b: JsonDeserializer, v: var T, key: string, default: T) = 105 | let j = b.node{key} 106 | if not j.isNil: 107 | b.get(j, v) 108 | else: 109 | v = default 110 | 111 | proc visit*(b: JsonDeserializer, images: var seq[Image], imagesKey: string, frameOffsets: var seq[Point], frameOffsetsKey: string) = 112 | let jimages = b.node[imagesKey] 113 | let sz = jimages.len 114 | images.setLen(sz) 115 | frameOffsets.setLen(sz) 116 | 117 | for i in 0 ..< sz: 118 | images[i] = b.deserializeImage(jimages[i], frameOffsets[i]) 119 | -------------------------------------------------------------------------------- /rod/utils/json_serializer.nim: -------------------------------------------------------------------------------- 1 | import json, os, strutils 2 | import nimx / [ types, image, pathutils ] 3 | 4 | type JsonSerializer* = ref object 5 | node*: JsonNode 6 | url*: string 7 | 8 | proc newJsonSerializer*(): JsonSerializer = 9 | result.new() 10 | result.node = newJObject() 11 | 12 | proc getRelativeResourcePath(b: JsonSerializer, path: string): string = 13 | var resourcePath = path 14 | when not defined(js) and not defined(android) and not defined(ios): 15 | resourcePath = urlParentDir(b.url) 16 | resourcePath.removePrefix("file://") 17 | 18 | var fixedPath = path 19 | fixedPath.removePrefix("file://") 20 | result = relativePathToPath(resourcePath, fixedPath) 21 | echo "save path = ", resourcePath, " relative = ", result, " url ", b.url 22 | 23 | proc writePath(b: JsonSerializer, data: string): JsonNode = 24 | %b.getRelativeResourcePath(data) 25 | 26 | proc write[T](b: JsonSerializer, data: T): JsonNode = 27 | when T is Rect: 28 | result = %[data.x, data.y, data.width, data.height] 29 | elif T is tuple: 30 | result = newJArray() 31 | for k, v in fieldPairs(data): 32 | result.add(b.write(v)) 33 | elif T is object: 34 | result = newJObject() 35 | for k, v in fieldPairs(data): 36 | result[k] = b.write(v) 37 | elif T is seq | openarray: 38 | result = newJArray() 39 | for v in data: 40 | result.add(b.write(v)) 41 | elif T is Image: 42 | if data.isNil: 43 | result = %"" 44 | else: 45 | result = b.writePath(filePath(data)) 46 | else: 47 | result = %data 48 | 49 | proc visit*(b: JsonSerializer, v: Image, key: string) = 50 | if not v.isNil: 51 | b.node[key] = b.writePath(filePath(v)) 52 | 53 | proc visit*[T](b: JsonSerializer, v: seq[T], key: string) = 54 | if v.len > 0: 55 | b.node[key] = b.write(v) 56 | 57 | proc visit*[T](b: JsonSerializer, v: T, key: string) = 58 | b.node[key] = b.write(v) 59 | 60 | proc visit*(b: JsonSerializer, images: seq[Image], imagesKey: string, frameOffsets: seq[Point], frameOffsetsKey: string) = 61 | let jImages = newJArray() 62 | let jOffs = newJArray() 63 | var haveNonZeroOffset = false 64 | 65 | for i in 0 ..< images.len: 66 | jImages.add(b.write(images[i])) 67 | 68 | for i in 0 ..< frameOffsets.len: 69 | if frameOffsets[i] != zeroPoint: 70 | haveNonZeroOffset = true 71 | jOffs.add(b.write(frameOffsets[i])) 72 | 73 | if jImages.len != 0: 74 | b.node[imagesKey] = jImages 75 | if haveNonZeroOffset: 76 | b.node[frameOffsetsKey] = jOffs 77 | -------------------------------------------------------------------------------- /rod/utils/property_desc.nim: -------------------------------------------------------------------------------- 1 | import macros, tables 2 | 3 | type 4 | PropertyDesc* = tuple 5 | name: string 6 | attributes: Table[string, NimNode] 7 | 8 | var props {.compileTime.} = initTable[string, seq[PropertyDesc]]() 9 | 10 | proc hasAttr*(d: PropertyDesc, attr: string): bool = 11 | attr in d.attributes 12 | 13 | proc hasProperties*(t: typedesc): bool {.compileTime.} = 14 | result = $t in props 15 | 16 | iterator propertyDescs*(typdesc: NimNode): PropertyDesc = 17 | let k = $typdesc 18 | if k in props: 19 | for p in props[k]: 20 | yield p 21 | 22 | proc addPropertyAttr(d: var PropertyDesc, a: NimNode) = 23 | case a.kind 24 | of nnkIdent: 25 | d.attributes[$a] = nil 26 | of nnkExprEqExpr: 27 | a[0].expectKind(nnkIdent) 28 | d.attributes[$a[0]] = a[1] 29 | of nnkStmtList: 30 | for c in a: addPropertyAttr(d, c) 31 | of nnkCall: 32 | a[0].expectKind(nnkIdent) 33 | assert(a.len == 2) 34 | var b = a[1] 35 | if b.kind == nnkStmtList: 36 | assert(b.len == 1) 37 | b = b[0] 38 | d.attributes[$a[0]] = b 39 | of nnkProcDef: 40 | d.attributes[$a.name] = a 41 | else: 42 | echo "Unexpected attr kind: ", treeRepr(a) 43 | assert(false) 44 | 45 | proc parsePropertyDescs(properties: NimNode): seq[PropertyDesc] = 46 | result = @[] 47 | for p in properties: 48 | case p.kind 49 | of nnkStmtList: 50 | for c in p: result.add(parsePropertyDescs(c)) 51 | of nnkIdent: 52 | var pd: PropertyDesc 53 | pd.name = $p 54 | pd.attributes = initTable[string, NimNode]() 55 | result.add(pd) 56 | of nnkCall: 57 | var pd: PropertyDesc 58 | pd.name = $p[0] 59 | pd.attributes = initTable[string, NimNode]() 60 | for i in 1 ..< p.len: 61 | addPropertyAttr(pd, p[i]) 62 | result.add(pd) 63 | of nnkDiscardStmt: 64 | discard 65 | else: 66 | echo "Unexpected property desc kind: ", treeRepr(p) 67 | assert(false) 68 | 69 | proc verifyProperties(pr: seq[PropertyDesc]) = 70 | for p in pr: 71 | for a in keys(p.attributes): 72 | assert(a in ["serializationKey", "noserialize", "phantom", "combinedWith", "default"], "Invalid attribute: " & a) 73 | 74 | macro properties*(typdesc: typed{nkSym}, body: untyped): untyped = 75 | let k = $typdesc 76 | # if k notin props: 77 | # props[k] = @[] 78 | assert(k notin props) 79 | props[k] = parsePropertyDescs(body) 80 | verifyProperties(props[k]) 81 | -------------------------------------------------------------------------------- /rod/utils/serialization_codegen.nim: -------------------------------------------------------------------------------- 1 | import nimx/class_registry 2 | import macros, tables 3 | import property_desc 4 | 5 | proc genPhantomTypeSection(typdesc: NimNode): NimNode = 6 | let fields = newNimNode(nnkRecList) 7 | for p in typdesc.propertyDescs: 8 | if p.hasAttr("phantom"): 9 | fields.add(newIdentDefs(newIdentNode(p.name), p.attributes["phantom"])) 10 | if fields.len > 0: 11 | result = newNimNode(nnkTypeSection).add(newNimNode(nnkTypeDef).add(newIdentNode("Phantom"), newEmptyNode(), 12 | newNimNode(nnkObjectTy).add(newEmptyNode(), newEmptyNode(), fields))) 13 | 14 | iterator serializablePropertyDescs(typdesc: NimNode): PropertyDesc = 15 | for p in typdesc.propertyDescs: 16 | if not p.hasAttr("noserialize"): 17 | yield p 18 | 19 | proc serializationKey(p: PropertyDesc): NimNode = 20 | if p.hasAttr("serializationKey"): 21 | result = copyNimTree(p.attributes["serializationKey"]) 22 | else: 23 | result = newLit(p.name) 24 | 25 | proc actualReference(p: PropertyDesc, v: NimNode): NimNode = 26 | let o = if p.hasAttr("phantom"): newIdentNode("phantom") else: v 27 | newNimNode(nnkDotExpr).add(o, newIdentNode(p.name)) 28 | 29 | proc propertyDescWithName(typdesc: NimNode, name: string): PropertyDesc = 30 | for p in typdesc.propertyDescs: 31 | if p.name == name: return p 32 | 33 | # TODO: The following is a hack. Instead we should get property descs from the typdesc itself 34 | result.name = name 35 | result.attributes = initTable[string, NimNode]() 36 | 37 | macro genSerializerProc(typdesc: typed{nkSym}, serTyp: typed{nkSym}, v: typed, s: typed, 38 | keyed: static[bool], serialize: static[bool], 39 | bin: static[bool], skipPhantom: static[bool]): untyped = 40 | let phantomIdent = newIdentNode("phantom") 41 | 42 | let phantomTyp = genPhantomTypeSection(typdesc) 43 | 44 | var paramTyp = typdesc 45 | 46 | let impl = getImpl(typdesc) 47 | 48 | # echo treeRepr(getImpl(typdesc.symbol)) 49 | 50 | if not serialize and impl.kind == nnkTypeDef and impl.len >= 3 and impl[2].kind != nnkRefTy: 51 | paramTyp = newNimNode(nnkVarTy).add(paramTyp) 52 | 53 | result = newNimNode(nnkStmtList) 54 | 55 | if not phantomTyp.isNil and phantomTyp.kind != nnkEmpty: 56 | result.add(phantomTyp) 57 | let pv = quote do: 58 | var `phantomIdent`: Phantom 59 | result.add(pv) 60 | 61 | if serialize and not skipPhantom: 62 | result.add(newCall("toPhantom", v, phantomIdent)) 63 | 64 | for p in typdesc.serializablePropertyDescs: 65 | let visitCall = newCall(ident("visit"), s, actualReference(p, v)) 66 | if keyed: visitCall.add(p.serializationKey()) 67 | 68 | if p.hasAttr("combinedWith"): 69 | let p1 = typdesc.propertyDescWithName($p.attributes["combinedWith"]) 70 | visitCall.add(actualReference(p1, v)) 71 | if keyed: visitCall.add(p1.serializationKey()) 72 | 73 | if keyed and not serialize and not bin and p.hasAttr("default"): 74 | visitCall.add(p.attributes["default"]) 75 | 76 | result.add(visitCall) 77 | # let echoPrefix = newLit($serTyp & " " & p.name & ": ") 78 | # let echoValue = actualReference(p, v) 79 | # result.add quote do: 80 | # when compiles(echo(`echoPrefix`, `echoValue`)): 81 | # echo `echoPrefix`, `echoValue` 82 | # else: 83 | # echo `echoPrefix`, "..." 84 | 85 | if not phantomTyp.isNil and phantomTyp.kind != nnkEmpty: 86 | if not serialize and not skipPhantom: 87 | result.add(newCall("fromPhantom", v, phantomIdent)) 88 | 89 | if not serialize: 90 | result.add quote do: 91 | when compiles(awake(`v`)): 92 | if not `s`.disableAwake: 93 | awake(`v`) 94 | 95 | # echo repr(result) 96 | 97 | template genSerializationCodeForComponent*(c: typed) = 98 | import rod / utils / [ bin_deserializer ] 99 | 100 | when defined(rodplugin): 101 | import rod / utils / [ json_deserializer, json_serializer, 102 | bin_serializer, serialization_hash_calculator ] 103 | 104 | bind className 105 | method deserialize*(v: c, b: JsonDeserializer) = 106 | when hasProperties(superType(c)): 107 | procCall deserialize(superType(c)(v), b) 108 | 109 | genSerializerProc(c, JsonDeserializer, v, b, true, false, false, false) 110 | 111 | method serialize*(v: c, b: JsonSerializer) = 112 | when hasProperties(superType(c)): 113 | procCall serialize(superType(c)(v), b) 114 | 115 | b.visit(className(v), "_c") 116 | genSerializerProc(c, JsonSerializer, v, b, true, true, false, false) 117 | 118 | method serialize*(v: c, b: BinSerializer) = 119 | when hasProperties(superType(c)): 120 | procCall serialize(superType(c)(v), b) 121 | 122 | genSerializerProc(c, BinSerializer, v, b, false, true, true, false) 123 | 124 | method serializationHash*(v: c, b: SerializationHashCalculator) = 125 | when hasProperties(superType(c)): 126 | procCall serializationHash(superType(c)(v), b) 127 | 128 | genSerializerProc(c, SerializationHashCalculator, v, b, true, true, false, true) 129 | 130 | method deserialize*(v: c, b: BinDeserializer) = 131 | when hasProperties(superType(c)): 132 | procCall deserialize(superType(c)(v), b) 133 | 134 | genSerializerProc(c, BinDeserializer, v, b, false, false, true, false) 135 | 136 | template genJsonSerializationrFor*(c: typed) = 137 | import rod / utils / [ json_deserializer, json_serializer ] 138 | 139 | proc toJson*(v: c): JsonNode= 140 | var b = newJsonSerializer() 141 | genSerializerProc(c, JsonSerializer, v, b, true, true, false, false) 142 | result = b.node 143 | 144 | proc `to c`*(jn: JsonNode): c = 145 | var b = newJsonDeserializer() 146 | b.node = jn 147 | when c is ref: 148 | result.new() 149 | genSerializerProc(c, JsonDeserializer, result, b, true, false, false, false) 150 | -------------------------------------------------------------------------------- /rod/utils/serialization_hash_calculator.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ types, image ] 2 | import rod/quaternion 3 | import hashes 4 | 5 | type SerializationHashCalculator* = ref object 6 | hash*: Hash 7 | 8 | proc newSerializationHashCalculator*(): SerializationHashCalculator = 9 | result.new() 10 | 11 | type ValueType = enum 12 | tyFloat32 13 | tyInt16 14 | tyInt32 15 | tyEnum 16 | tyBool 17 | tyString 18 | tyImage 19 | tySeq 20 | tyArray 21 | tyTuple 22 | tyQuaternion 23 | 24 | proc mix(b: SerializationHashCalculator, t: int) = 25 | b.hash = b.hash !& t 26 | 27 | proc mix(b: SerializationHashCalculator, t: ValueType) {.inline.} = 28 | b.mix(int(t)) 29 | 30 | proc mix(b: SerializationHashCalculator, k: string) = 31 | b.hash = b.hash !& hash(k) 32 | 33 | proc mixValueType(b: SerializationHashCalculator, T: typedesc) = 34 | when T is int16: 35 | b.mix(tyInt16) 36 | elif T is int32: 37 | b.mix(tyInt32) 38 | elif T is float32: 39 | b.mix(tyFloat32) 40 | elif T is bool: 41 | b.mix(tyBool) 42 | elif T is string: 43 | b.mix(tyString) 44 | elif T is Image: 45 | b.mix(tyImage) 46 | elif T is seq: 47 | b.mix(tySeq) 48 | var v: T 49 | b.mixValueType(type(v[0])) 50 | elif T is array: 51 | b.mix(tyArray) 52 | var v: T 53 | b.mixValueType(type(v[0])) 54 | b.mix(v.len) 55 | elif T is enum: 56 | b.mix(tyEnum) 57 | b.mix(sizeof(T)) 58 | elif T is tuple: 59 | b.mix(tyTuple) 60 | var v: T 61 | for k, vv in fieldPairs(v): 62 | b.mixValueType(type(vv)) 63 | 64 | proc mix[T](b: SerializationHashCalculator, v: T, k: string) {.inline.} = 65 | b.mixValueType(T) 66 | b.mix(k) 67 | 68 | proc visit*(b: SerializationHashCalculator, images: seq[Image], imagesKey: string, frameOffsets: seq[Point], frameOffsetsKey: string) = 69 | b.mix(tyImage, imagesKey) 70 | b.mix(tySeq, frameOffsetsKey) 71 | b.mix(111) 72 | 73 | proc visit*(b: SerializationHashCalculator, v: Quaternion, key: string) = 74 | b.mix(tyQuaternion, key) 75 | 76 | proc visit*[T](b: SerializationHashCalculator, v: T, key: string) = 77 | b.mix(v, key) 78 | -------------------------------------------------------------------------------- /rod/utils/serialization_helpers.nim: -------------------------------------------------------------------------------- 1 | 2 | proc arrayElementType[T](a: typedesc[openarray[T]]): T = discard 3 | 4 | proc isPODType*(T: typedesc): bool {.compileTime.} = 5 | when T is float32 | int16 | int8 | int32: 6 | return true 7 | elif T is array: 8 | var dummy: T 9 | return isPODType(type(dummy[0])) 10 | elif T is openarray: 11 | return isPODType(type(arrayElementType(T))) 12 | elif T is tuple: 13 | var v: T 14 | for k, vv in fieldPairs(v): 15 | if not isPODType(type(vv)): return false 16 | return true 17 | else: 18 | return false 19 | 20 | var a : tuple[a: int32, b: float32] 21 | when isPODType(type(a)): 22 | discard 23 | else: 24 | {.error: "no POD".} 25 | 26 | 27 | template alignsize*(t: typedesc): int = 28 | if sizeof(t) > 4: 29 | 4 30 | else: 31 | sizeof(t) 32 | 33 | # const testa:tuple[a: int32, b: float32] 34 | 35 | # static: 36 | # doAssert(isPODType(int32)) 37 | # doAssert(isPODType(testa)) 38 | # doAssert(not isPODType(tuple[a: int32, b: string])) 39 | -------------------------------------------------------------------------------- /rod/utils/text_helpers.nim: -------------------------------------------------------------------------------- 1 | import nimx/types 2 | import strutils 3 | 4 | proc fromHexColor*(clr: string): Color = 5 | doAssert(clr.len == 8) 6 | 7 | template getComp(start: int): float = 8 | parseHexInt(clr.substr(start, start + 1)).float / 255.0 9 | 10 | result.r = getComp(0) 11 | result.g = getComp(2) 12 | result.b = getComp(4) 13 | result.a = getComp(6) 14 | 15 | proc removeTextAttributes*(text: string): string = 16 | var tagOpened: bool 17 | for i in 0..": 24 | tagOpened = false -------------------------------------------------------------------------------- /rod/vertex_data_info.nim: -------------------------------------------------------------------------------- 1 | import nimx/portable_gl 2 | 3 | type VertexDataInfo* = object 4 | numOfCoordPerVert*: GLint 5 | numOfCoordPerTexCoord*: GLint 6 | numOfCoordPerNormal*: GLint 7 | numOfCoordPerTangent*: GLint 8 | numOfCoordPerBinormal*: GLint 9 | stride*: int 10 | 11 | proc newVertexInfoWithVertexData*(vertexDataLen = 0, texCoordDataLen = 0, normalDataLen = 0, tangentDataLen = 0, binormalDataLen: int = 0): VertexDataInfo = 12 | if vertexDataLen != 0: 13 | result.numOfCoordPerVert = 3 14 | if texCoordDataLen != 0: 15 | result.numOfCoordPerTexCoord = 2 16 | if normalDataLen != 0: 17 | result.numOfCoordPerNormal = 3 18 | if binormalDataLen != 0: 19 | result.numOfCoordPerBinormal = 3 20 | if tangentDataLen != 0: 21 | result.numOfCoordPerTangent = 3 22 | result.stride = (result.numOfCoordPerVert + 23 | result.numOfCoordPerTexCoord + 24 | result.numOfCoordPerNormal + 25 | result.numOfCoordPerBinormal + 26 | result.numOfCoordPerTangent) * sizeof(GLfloat) 27 | -------------------------------------------------------------------------------- /template/.gitignore: -------------------------------------------------------------------------------- 1 | .rodedit/ 2 | build/ 3 | *.exe 4 | -------------------------------------------------------------------------------- /template/main.nim: -------------------------------------------------------------------------------- 1 | import nimx/[view, app, scroll_view, table_view, text_field, autotest, window] 2 | import src/game/example_scene 3 | 4 | const isMobile = defined(ios) or defined(android) 5 | 6 | let wndRect = newRect(40, 40, 1280, 720) 7 | proc startApplication() = 8 | var mainWindow : Window 9 | 10 | when isMobile: 11 | mainWindow = newFullscreenWindow() 12 | else: 13 | mainWindow = newWindow(wndRect) 14 | 15 | mainWindow.title = "Template" 16 | 17 | let gs = new(ExampleScene) 18 | gs.init(mainWindow.bounds) 19 | gs.autoresizingMask = { afFlexibleWidth, afFlexibleHeight } 20 | mainWindow.addSubview(gs) 21 | 22 | gs.setFrame(mainWindow.bounds) 23 | gs.resizeSubviews(mainWindow.bounds.size) 24 | 25 | runApplication: 26 | startApplication() 27 | -------------------------------------------------------------------------------- /template/nakefile.nim: -------------------------------------------------------------------------------- 1 | import nimx/naketools 2 | import osproc, os 3 | 4 | 5 | const additionalFonts: seq[string] = @[] 6 | 7 | proc rodasset(b: Builder, command: string, arguments: varargs[string]) = 8 | let downsampleRatio = 1 9 | var args = @["rodasset", command, "--platform=" & b.platform, "--downsampleRatio=" & $downsampleRatio] 10 | args.add(arguments) 11 | direShell(args) 12 | 13 | 14 | beforeBuild = proc(b: Builder) = 15 | b.mainFile = "main.nim" 16 | b.originalResourcePath = "res" 17 | # b.resourcePath = "build" 18 | b.additionalNimFlags.add(@["--path:res", "--path:src"]) 19 | 20 | 21 | preprocessResources = proc(b: Builder) = 22 | if b.platform == "ios": 23 | copyDir(b.originalResourcePath / "ios", b.resourcePath) 24 | 25 | if b.platform == "ios" or b.platform == "ios-sim" or b.platform == "emscripten": 26 | b.copyResourceAsIs("OpenSans-Regular.ttf") 27 | for f in additionalFonts: 28 | b.copyResourceAsIs(f) 29 | 30 | var args = newSeq[string]() 31 | if b.debugMode: 32 | args.add("--debug") 33 | args.add(["--src=" & b.originalResourcePath, "--dst=" & b.resourcePath]) 34 | b.rodasset("pack", args) 35 | -------------------------------------------------------------------------------- /template/res/example_bundle/config.rab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/rod/3362ca3a6af0480e7af864b8db6e7fb079203f68/template/res/example_bundle/config.rab -------------------------------------------------------------------------------- /template/res/example_bundle/logo-crown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/rod/3362ca3a6af0480e7af864b8db6e7fb079203f68/template/res/example_bundle/logo-crown.png -------------------------------------------------------------------------------- /template/src/core/asset_loader.nim: -------------------------------------------------------------------------------- 1 | import nimx/assets/asset_manager 2 | from nimx/assets/abstract_asset_bundle import nil 3 | import nimx / [ notification_center, abstract_window ] 4 | import rod / asset_bundle 5 | 6 | import logging 7 | export asset_bundle 8 | 9 | type AssetsLoader* = object 10 | assets: seq[AssetBundleDescriptor] 11 | 12 | onRodAssetBundleDownloadingStart = proc(asset: string) = 13 | info "[AssetsLoader] Asset downloading started ", asset 14 | 15 | onRodAssetBundleDownloadingEnd = proc(asset: string, error: string) = 16 | info "[AssetsLoader] Asset downloading ended ", asset, " with error ", error.len > 0 17 | 18 | onRodAssetBundleDownloadingProgress = proc(asset: string, p: float) = 19 | info "[AssetsLoader] Asset downloading ", asset, " progress ", p 20 | 21 | proc load*(a: var AssetsLoader, assets: openarray[AssetBundleDescriptor], onLoadProgress: proc(p: float) {.gcsafe.} = nil, onLoaded: proc() {.gcsafe.} ) = 22 | a.assets = @assets 23 | assert(a.assets.len > 0, "[AssetsLoader] nothing to load") 24 | assets.loadAssetBundles() do(mountPaths: openarray[string], abs: openarray[AssetBundle], err: string): 25 | if err.len != 0: 26 | sharedNotificationCenter().postNotification("SHOW_RESOURCE_LOADING_ALERT") 27 | return 28 | let am = sharedAssetManager() 29 | for i, mnt in mountPaths: 30 | info "[AssetsLoader] mounting ", mnt 31 | am.mount(mnt, abs[i]) 32 | var newAbs = newSeqOfCap[abstract_asset_bundle.AssetBundle](abs.len) 33 | for ab in abs: newAbs.add(ab) 34 | am.loadAssetsInBundles(newAbs, onLoadProgress) do(): 35 | # info am.dump() 36 | if not onLoaded.isNil: 37 | onLoaded() 38 | 39 | proc free*(a: var AssetsLoader) = 40 | let am = sharedAssetManager() 41 | assert(a.assets.len > 0, "[AssetsLoader] nothing to unload") 42 | for ab in a.assets: 43 | info "[AssetsLoader] unmounting ", ab.path 44 | am.unmount(ab.path) 45 | a.assets.setLen(0) 46 | # info am.dump() 47 | requestGCFullCollect() 48 | -------------------------------------------------------------------------------- /template/src/core/game_scene.nim: -------------------------------------------------------------------------------- 1 | import nimx/[view, types, button, animation, mini_profiler, matrixes, view_event_handling] 2 | import rod/[viewport, rod_types, node, component, component/ui_component, edit_view] 3 | import asset_loader 4 | 5 | export asset_loader, viewport 6 | 7 | const viewportSize = newSize(1920, 1080) 8 | 9 | type GameScene* = ref object of SceneView 10 | assetLoader: AssetsLoader 11 | 12 | proc centerOrthoCameraPosition*(gs: GameScene) = 13 | assert(not gs.camera.isNil, "GameSceneBase's camera is nil") 14 | let cameraNode = gs.camera.node 15 | 16 | cameraNode.positionX = viewportSize.width / 2 17 | cameraNode.positionY = viewportSize.height / 2 18 | 19 | proc addDefaultOrthoCamera*(gs: GameScene, cameraName: string) = 20 | let cameraNode = gs.rootNode.newChild(cameraName) 21 | let camera = cameraNode.component(Camera) 22 | 23 | camera.projectionMode = cpOrtho 24 | camera.viewportSize = viewportSize 25 | cameraNode.positionZ = 1 26 | gs.centerOrthoCameraPosition() 27 | 28 | method acceptsFirstResponder(v: GameScene): bool = true 29 | 30 | method onKeyDown*(gs: GameScene, e: var Event): bool = 31 | if e.keyCode == VirtualKey.E: 32 | ## start's editor 33 | discard startEditingNodeInView(gs.rootNode, gs) 34 | result = true 35 | 36 | method assetBundles*(gs: GameScene): seq[AssetBundleDescriptor] {.base.} = discard 37 | method onResourcesLoaded*(gs: GameScene) {.base.} = discard 38 | 39 | method init*(gs: GameScene, frame: Rect)= 40 | procCall gs.SceneView.init(frame) 41 | gs.rootNode = newNode("root") 42 | gs.addDefaultOrthoCamera("camera") 43 | 44 | proc afterResourcesPreloaded() = 45 | gs.onResourcesLoaded() 46 | 47 | let abd = gs.assetBundles() 48 | if abd.len > 0: 49 | gs.assetLoader.load(abd, onLoadProgress = nil, onLoaded = afterResourcesPreloaded) 50 | else: 51 | afterResourcesPreloaded() 52 | 53 | method viewOnExit*(gs: GameScene) = 54 | if gs.assetBundles().len > 0: 55 | gs.assetLoader.free() 56 | 57 | -------------------------------------------------------------------------------- /template/src/game/example_scene.nim: -------------------------------------------------------------------------------- 1 | import rod / [ rod_types, node, component ] 2 | import nimx / animation 3 | import .. / core / game_scene 4 | 5 | type 6 | ExampleScene* = ref object of GameScene 7 | 8 | method assetBundles*(gs: ExampleScene): seq[AssetBundleDescriptor] = 9 | const assetBundles = @[ 10 | assetBundleDescriptor("example_bundle") 11 | ] 12 | result = assetBundles 13 | 14 | method onResourcesLoaded*(gs: ExampleScene) = 15 | var comp = newNodeWithResource("example_bundle/composition2") 16 | var anim = comp.animationNamed("idle") 17 | anim.numberOfLoops = -1 18 | comp.addAnimation(anim) 19 | gs.rootNode.addChild(comp) 20 | -------------------------------------------------------------------------------- /template/template.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | version = "0.1.0" 3 | author = "Anonymous" 4 | description = "Rod project template" 5 | license = "MIT" 6 | 7 | requires "https://github.com/yglukhov/rod" 8 | --------------------------------------------------------------------------------