diff --git a/.gitmodules b/.gitmodules index da892d5c8..e2e79d499 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "lib/rapidjson"] path = lib/rapidjson url = https://github.com/Tencent/rapidjson.git +[submodule "lib/im3d"] + path = lib/im3d + url = https://github.com/john-chapman/im3d.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 62ff40649..2c2f6d71c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,27 @@ target_sources(${PROJECT_NAME} PRIVATE ${OPENGOTHIC_SOURCES} ${ObjCSOURCES}) add_subdirectory(shader) target_link_libraries(${PROJECT_NAME} GothicShaders) +# Spacer-only editor shaders. add_shader() is defined by shader/CMakeLists.txt; +# point its output back at the GothicShaders binary directory while registering +# the additional generated SPIR-V files. +set(OPENGOTHIC_ROOT_PROJECT_BINARY_DIR ${PROJECT_BINARY_DIR}) +set(PROJECT_BINARY_DIR "${CMAKE_BINARY_DIR}/shader") +set(OPENGOTHIC_SHADERS_SPRV "") +add_shader(im3d_points.vert editor/im3d.vert -DPOINTS) +add_shader(im3d_points.frag editor/im3d.frag -DPOINTS) +add_shader(im3d_lines.vert editor/im3d.vert -DLINES) +add_shader(im3d_lines.geom editor/im3d.geom) +add_shader(im3d_lines.frag editor/im3d.frag -DLINES) +add_shader(im3d_triangles.vert editor/im3d.vert -DTRIANGLES) +add_shader(im3d_triangles.frag editor/im3d.frag -DTRIANGLES) +add_shader(selection_outline.vert editor/selection_outline.vert) +add_shader(selection_outline.frag editor/selection_outline.frag) +add_shader(selection_outline_composite.frag editor/selection_outline_composite.frag) +set(SPACER_EDITOR_SHADERS ${OPENGOTHIC_SHADERS_SPRV}) +set(PROJECT_BINARY_DIR ${OPENGOTHIC_ROOT_PROJECT_BINARY_DIR}) +add_custom_target(spacer_editor_shaders DEPENDS ${SPACER_EDITOR_SHADERS}) +add_dependencies(shader spacer_editor_shaders) + # editor icons and such add_subdirectory(spacer/assets) @@ -144,7 +165,7 @@ target_link_libraries(${PROJECT_NAME} miniz) # bullet physics set(BULLET2_MULTITHREADING ON CACHE INTERNAL "") -set(USE_MSVC_RUNTIME_LIBRARY_DLL ON CACHE INTERNAL "") +set(USE_MSVC_RUNTIME_LIBRARY_DLL ON CACHE INTERNAL "") set(USE_GRAPHICAL_BENCHMARK OFF CACHE INTERNAL "") set(BUILD_BULLET2_DEMOS OFF CACHE INTERNAL "") # No samples set(BUILD_OPENGL3_DEMOS OFF CACHE INTERNAL "") # No samples @@ -159,7 +180,7 @@ add_subdirectory(lib/bullet3) if(NOT MSVC) target_compile_options(BulletDynamics PRIVATE -Wno-argument-outside-range) target_compile_options(BulletInverseDynamics PRIVATE -Wno-argument-outside-range) - target_compile_options(BulletSoftBody PRIVATE -Wno-argument-outside-range) + target_compile_options(BulletSoftBody PRIVATE -Wno-argument-outside-range) target_compile_options(Bullet3Common PRIVATE -Wno-argument-outside-range) target_compile_options(BulletCollision PRIVATE -Wno-argument-outside-range) target_compile_options(LinearMath PRIVATE -Wno-argument-outside-range) @@ -167,7 +188,7 @@ endif() if(NOT MSVC AND NOT APPLE AND NOT ${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") target_compile_options(BulletDynamics PRIVATE -Wno-stringop-overflow) target_compile_options(BulletInverseDynamics PRIVATE -Wno-stringop-overflow) - target_compile_options(BulletSoftBody PRIVATE -Wno-stringop-overflow) + target_compile_options(BulletSoftBody PRIVATE -Wno-stringop-overflow) target_compile_options(Bullet3Common PRIVATE -Wno-stringop-overflow) target_compile_options(BulletCollision PRIVATE -Wno-stringop-overflow) target_compile_options(LinearMath PRIVATE -Wno-stringop-overflow) @@ -198,8 +219,8 @@ target_sources(Gothic2Notr PRIVATE game/main.cpp icon.rc) target_link_libraries(Gothic2Notr PRIVATE OpenGothic) # editor -target_sources(Spacer PRIVATE ${SPACER_SOURCES} spacer/main.cpp) -target_include_directories(Spacer PRIVATE "spacer") +target_sources(Spacer PRIVATE ${SPACER_SOURCES} spacer/main.cpp lib/im3d/im3d.cpp) +target_include_directories(Spacer PRIVATE "spacer" "lib/im3d") target_link_libraries(Spacer PRIVATE OpenGothic SpacerAssets) # installation diff --git a/common/graphics/renderer.h b/common/graphics/renderer.h index bef6c3d4e..60348d621 100644 --- a/common/graphics/renderer.h +++ b/common/graphics/renderer.h @@ -32,6 +32,9 @@ class Renderer final { Tempest::Attachment screenshoot(uint8_t frameId); + // Editor overlays may reuse the completed scene depth without owning another renderer. + Tempest::ZBuffer& sceneDepthBuffer() { return zbuffer; } + private: enum Quality : uint8_t { None, diff --git a/lib/im3d b/lib/im3d new file mode 160000 index 000000000..3fd7afaf3 --- /dev/null +++ b/lib/im3d @@ -0,0 +1 @@ +Subproject commit 3fd7afaf3192ed50bca29c191969aa41252b53d5 diff --git a/shader/editor/im3d.frag b/shader/editor/im3d.frag new file mode 100644 index 000000000..0a280a84f --- /dev/null +++ b/shader/editor/im3d.frag @@ -0,0 +1,36 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable + +#if !defined(POINTS) && !defined(LINES) && !defined(TRIANGLES) +#error im3d primitive type is not defined +#endif + +#if defined(LINES) +layout(location = 0) in noperspective float inEdgeDistance; +layout(location = 1) in noperspective float inSize; +layout(location = 2) in smooth vec4 inColor; +#elif defined(POINTS) +layout(location = 0) in float inSize; +layout(location = 1) in vec4 inColor; +#else +layout(location = 0) in vec4 inColor; +#endif + +layout(location = 0) out vec4 outColor; + +const float antialiasing = 2.0; + +void main() { + outColor = inColor; + +#if defined(LINES) + float coverage = abs(inEdgeDistance)/inSize; + coverage = smoothstep(1.0,1.0-antialiasing/inSize,coverage); + outColor.a *= coverage; +#elif defined(POINTS) + float coverage = length(gl_PointCoord.xy-vec2(0.5)); + coverage = smoothstep(0.5,0.5-antialiasing/inSize,coverage); + outColor.a *= coverage; +#endif + } diff --git a/shader/editor/im3d.geom b/shader/editor/im3d.geom new file mode 100644 index 000000000..31bf1d068 --- /dev/null +++ b/shader/editor/im3d.geom @@ -0,0 +1,44 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable + +layout(lines) in; +layout(triangle_strip, max_vertices = 4) out; + +layout(push_constant, std430) uniform Push { + mat4 viewProject; + vec2 viewport; + } push; + +layout(location = 0) in float inSize[]; +layout(location = 1) in vec4 inColor[]; + +layout(location = 0) out noperspective float outEdgeDistance; +layout(location = 1) out noperspective float outSize; +layout(location = 2) out smooth vec4 outColor; + +void emitLineVertex(vec2 position, int endpoint, float edgeDistance) { + gl_Position = vec4(position*gl_in[endpoint].gl_Position.w, + gl_in[endpoint].gl_Position.zw); + outEdgeDistance = edgeDistance; + outSize = inSize[endpoint]; + outColor = inColor[endpoint]; + EmitVertex(); + } + +void main() { + vec2 pos0 = gl_in[0].gl_Position.xy/gl_in[0].gl_Position.w; + vec2 pos1 = gl_in[1].gl_Position.xy/gl_in[1].gl_Position.w; + + vec2 dir = pos0-pos1; + dir = normalize(vec2(dir.x,dir.y*push.viewport.y/push.viewport.x)); + vec2 tangent = vec2(-dir.y,dir.x); + vec2 tangent0 = tangent*inSize[0]/push.viewport; + vec2 tangent1 = tangent*inSize[1]/push.viewport; + + emitLineVertex(pos0-tangent0,0,-inSize[0]); + emitLineVertex(pos0+tangent0,0, inSize[0]); + emitLineVertex(pos1-tangent1,1,-inSize[1]); + emitLineVertex(pos1+tangent1,1, inSize[1]); + EndPrimitive(); + } diff --git a/shader/editor/im3d.vert b/shader/editor/im3d.vert new file mode 100644 index 000000000..868a75dfc --- /dev/null +++ b/shader/editor/im3d.vert @@ -0,0 +1,47 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable + +#if !defined(POINTS) && !defined(LINES) && !defined(TRIANGLES) +#error im3d primitive type is not defined +#endif + +layout(location = 0) in vec4 inPositionSize; +layout(location = 1) in vec4 inColor; + +layout(push_constant, std430) uniform Push { + mat4 viewProject; + vec2 viewport; + } push; + +#if defined(LINES) +layout(location = 0) out float outSize; +layout(location = 1) out vec4 outColor; +#elif defined(POINTS) +layout(location = 0) out float outSize; +layout(location = 1) out vec4 outColor; +#else +layout(location = 0) out vec4 outColor; +#endif + +out gl_PerVertex { + vec4 gl_Position; + float gl_PointSize; + }; + +const float antialiasing = 2.0; + +void main() { + gl_Position = push.viewProject * vec4(inPositionSize.xyz,1.0); + +#if defined(POINTS) || defined(LINES) + outSize = max(inPositionSize.w,antialiasing); + outColor = inColor; + outColor.a *= smoothstep(0.0,1.0,inPositionSize.w/antialiasing); +#if defined(POINTS) + gl_PointSize = outSize; +#endif +#else + outColor = inColor; +#endif + } diff --git a/shader/editor/selection_outline.frag b/shader/editor/selection_outline.frag new file mode 100644 index 000000000..65524faff --- /dev/null +++ b/shader/editor/selection_outline.frag @@ -0,0 +1,9 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(1.0); + } diff --git a/shader/editor/selection_outline.vert b/shader/editor/selection_outline.vert new file mode 100644 index 000000000..b276d7a34 --- /dev/null +++ b/shader/editor/selection_outline.vert @@ -0,0 +1,28 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable + +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec3 inNormal; +layout(location = 2) in vec2 inUv; +layout(location = 3) in uint inColor; + +layout(push_constant, std430) uniform Push { + mat4 model; + mat4 viewProject; + vec4 params; + } push; + +out gl_PerVertex { + vec4 gl_Position; + }; + +void main() { + vec4 clip = push.viewProject * push.model * vec4(inPosition,1.0); + + // A minute bias toward the camera makes an equal-depth replay of the + // selected mesh robust against floating-point differences. Occluding scene + // geometry still wins the regular LEqual depth test. + clip.z -= push.params.x * clip.w; + gl_Position = clip; + } diff --git a/shader/editor/selection_outline_composite.frag b/shader/editor/selection_outline_composite.frag new file mode 100644 index 000000000..3fc02bcee --- /dev/null +++ b/shader/editor/selection_outline_composite.frag @@ -0,0 +1,40 @@ +#version 450 + +#extension GL_ARB_separate_shader_objects : enable + +layout(binding = 0) uniform sampler2D selectionMask; + +layout(push_constant, std430) uniform Push { + vec4 color; + vec2 viewport; + float radius; + float padding; + } push; + +layout(location = 0) out vec4 outColor; + +void main() { + const vec2 uv = gl_FragCoord.xy / push.viewport; + // Offsets are measured in final editor pixels even when the renderer uses a + // reduced internal resolution. + const vec2 texelSize = 1.0 / push.viewport; + const float center = texture(selectionMask,uv).r; + + float expanded = 0.0; + const int maxRadius = 4; + for(int y=-maxRadius; y<=maxRadius; ++y) { + for(int x=-maxRadius; x<=maxRadius; ++x) { + if(abs(x)>int(push.radius) || abs(y)>int(push.radius)) + continue; + if(x*x+y*y>int(push.radius*push.radius)) + continue; + expanded = max(expanded, + texture(selectionMask,uv+vec2(x,y)*texelSize).r); + } + } + + const float outline = expanded * (1.0-center); + if(outline<=0.0) + discard; + outColor = vec4(push.color.rgb,push.color.a*outline); + } diff --git a/spacer/gizmo/im3dgizmo.cpp b/spacer/gizmo/im3dgizmo.cpp new file mode 100644 index 000000000..e28e52362 --- /dev/null +++ b/spacer/gizmo/im3dgizmo.cpp @@ -0,0 +1,239 @@ +#include "im3dgizmo.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "camera.h" +#include "resources.h" +#include "shader.h" + +namespace { + +struct GpuVertex { + float positionSize[4]; + float color[4]; + }; + +struct Push { + Tempest::Matrix4x4 viewProject; + Tempest::Vec2 viewport; + }; + +static_assert(sizeof(GpuVertex)==sizeof(float)*8); +static_assert(sizeof(Push)==sizeof(float)*18); + +GpuVertex toGpuVertex(const Im3d::VertexData& src) { + GpuVertex ret; + ret.positionSize[0] = src.m_positionSize.x; + ret.positionSize[1] = src.m_positionSize.y; + ret.positionSize[2] = src.m_positionSize.z; + ret.positionSize[3] = src.m_positionSize.w; + ret.color[0] = src.m_color.getR(); + ret.color[1] = src.m_color.getG(); + ret.color[2] = src.m_color.getB(); + ret.color[3] = src.m_color.getA(); + return ret; + } + +Tempest::RenderState overlayState() { + Tempest::RenderState state; + state.setBlendSource(Tempest::RenderState::BlendMode::SrcAlpha); + state.setBlendDest(Tempest::RenderState::BlendMode::OneMinusSrcAlpha); + state.setZTestMode(Tempest::RenderState::ZTestMode::Always); + state.setZWriteEnabled(false); + state.setCullFaceMode(Tempest::RenderState::CullMode::NoCull); + return state; + } + +Tempest::Shader loadShader(Tempest::Device& device, const char* name) { + const auto src = GothicShader::get(name); + return device.shader(src.data,src.len); + } + +} + +struct Im3dGizmo::Impl { + enum class Primitive : uint8_t { + Points, + Lines, + Triangles, + }; + + struct Draw { + Primitive primitive = Primitive::Triangles; + uint32_t offset = 0; + uint32_t count = 0; + }; + + struct FrameBuffer { + Tempest::VertexBuffer vertices; + size_t capacity = 0; + }; + + std::vector cpuVertices; + std::vector draws; + Push push = {}; + std::array gpu; + + Tempest::RenderPipeline points; + Tempest::RenderPipeline lines; + Tempest::RenderPipeline triangles; + bool pipelinesReady = false; + + void ensurePipelines() { + if(pipelinesReady) + return; + + auto& device = Resources::device(); + const auto state = overlayState(); + points = device.pipeline(Tempest::Points,state, + loadShader(device,"im3d_points.vert.sprv"), + loadShader(device,"im3d_points.frag.sprv")); + lines = device.pipeline(Tempest::Lines,state, + loadShader(device,"im3d_lines.vert.sprv"), + loadShader(device,"im3d_lines.geom.sprv"), + loadShader(device,"im3d_lines.frag.sprv")); + triangles = device.pipeline(Tempest::Triangles,state, + loadShader(device,"im3d_triangles.vert.sprv"), + loadShader(device,"im3d_triangles.frag.sprv")); + pipelinesReady = true; + } + + void copyDrawLists() { + cpuVertices.clear(); + draws.clear(); + + const auto* lists = Im3d::GetDrawLists(); + const auto count = Im3d::GetDrawListCount(); + for(Im3d::U32 listId=0; listId(256,frame.capacity); + while(capacitypush.viewProject = camera.viewProj(); + impl->push.viewport = Tempest::Vec2(float(width),float(height)); + impl->copyDrawLists(); + return changed; + } + +void Im3dGizmo::render(Tempest::Encoder& cmd, + Tempest::Attachment& target, uint8_t frameId) { + if(impl->cpuVertices.empty()) + return; + + impl->ensurePipelines(); + impl->upload(frameId); + auto& vertices = impl->gpu[frameId%impl->gpu.size()].vertices; + + cmd.setFramebuffer({{target,Tempest::Preserve,Tempest::Preserve}}); + cmd.setViewport(0,0,target.w(),target.h()); + + for(const auto& draw:impl->draws) { + switch(draw.primitive) { + case Impl::Primitive::Points: cmd.setPipeline(impl->points); break; + case Impl::Primitive::Lines: cmd.setPipeline(impl->lines); break; + case Impl::Primitive::Triangles: cmd.setPipeline(impl->triangles); break; + } + cmd.setPushData(impl->push); + cmd.draw(vertices,draw.offset,draw.count); + } + } diff --git a/spacer/gizmo/im3dgizmo.h b/spacer/gizmo/im3dgizmo.h new file mode 100644 index 000000000..e498b1ad6 --- /dev/null +++ b/spacer/gizmo/im3dgizmo.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include +#include + +class Camera; + +namespace Tempest { +class Attachment; +class CommandBuffer; +template +class Encoder; +} + +class Im3dGizmo final { + public: + enum class Mode : uint8_t { + Translation, + Rotation, + Scale, + }; + + Im3dGizmo(); + ~Im3dGizmo(); + + Im3dGizmo(const Im3dGizmo&) = delete; + Im3dGizmo& operator=(const Im3dGizmo&) = delete; + + void setMode(Mode mode); + Mode mode() const; + + bool isHovered() const; + bool isActive() const; + + // Builds im3d's frame and copies its transient draw lists for the native Tempest pass. + bool prepare(const Camera& camera, + const Tempest::Vec3& cursorRayOrigin, const Tempest::Vec3& cursorRayDirection, + bool mouseDown, int width, int height, Tempest::Matrix4x4* transform); + + // Records the gizmo directly into the editor scene framebuffer. + void render(Tempest::Encoder& cmd, + Tempest::Attachment& target, uint8_t frameId); + + private: + struct Impl; + std::unique_ptr impl; + Mode currentMode = Mode::Translation; + }; diff --git a/spacer/gizmo/selectionoutline.cpp b/spacer/gizmo/selectionoutline.cpp new file mode 100644 index 000000000..bdc47038b --- /dev/null +++ b/spacer/gizmo/selectionoutline.cpp @@ -0,0 +1,130 @@ +#include "selectionoutline.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "camera.h" +#include "resources.h" +#include "shader.h" +#include "ui/objects/worldedit.h" + +namespace { + +struct MaskPush { + Tempest::Matrix4x4 model; + Tempest::Matrix4x4 viewProject; + Tempest::Vec4 params; + }; + +struct CompositePush { + Tempest::Vec4 color; + Tempest::Vec2 viewport; + float radius; + float padding; + }; + +static_assert(sizeof(MaskPush)==sizeof(float)*36); +static_assert(sizeof(CompositePush)==sizeof(float)*8); + +Tempest::Shader loadShader(Tempest::Device& device, const char* name) { + const auto src = GothicShader::get(name); + return device.shader(src.data,src.len); + } + +} + +struct SelectionOutline::Impl { + Tempest::Attachment mask; + Tempest::RenderPipeline maskPipeline; + Tempest::RenderPipeline compositePipeline; + bool ready = false; + + void ensureResources(Tempest::Size size) { + auto& device = Resources::device(); + if(mask.size()!=size) { + if(!mask.isEmpty()) + Resources::recycle(std::move(mask)); + mask = device.attachment(Tempest::TextureFormat::RGBA8,size); + } + if(ready) + return; + + Tempest::RenderState maskState; + maskState.setCullFaceMode(Tempest::RenderState::CullMode::NoCull); + maskState.setZTestMode(Tempest::RenderState::ZTestMode::LEqual); + maskState.setZWriteEnabled(false); + maskPipeline = device.pipeline(Tempest::Triangles,maskState, + loadShader(device,"selection_outline.vert.sprv"), + loadShader(device,"selection_outline.frag.sprv")); + + Tempest::RenderState compositeState; + compositeState.setCullFaceMode(Tempest::RenderState::CullMode::NoCull); + compositeState.setZTestMode(Tempest::RenderState::ZTestMode::Always); + compositeState.setZWriteEnabled(false); + compositeState.setBlendSource(Tempest::RenderState::BlendMode::SrcAlpha); + compositeState.setBlendDest(Tempest::RenderState::BlendMode::OneMinusSrcAlpha); + compositePipeline = device.pipeline(Tempest::Triangles,compositeState, + loadShader(device,"triangle.vert.sprv"), + loadShader(device,"selection_outline_composite.frag.sprv")); + ready = true; + } + }; + +SelectionOutline::SelectionOutline() + :impl(new Impl()) { + } + +SelectionOutline::~SelectionOutline() = default; + +void SelectionOutline::render(Tempest::Encoder& cmd, + Tempest::Attachment& target, Tempest::ZBuffer& sceneDepth, + const Camera& camera, const WorldEdit& world) { + if(!world.hasSelection() || target.isEmpty() || sceneDepth.isEmpty()) + return; + + std::vector meshes; + world.selectedMeshes(meshes); + if(meshes.empty()) + return; + + // The mask must use exactly the renderer depth-buffer resolution. Otherwise + // depth samples and selected fragments do not address the same pixels and a + // dilated mask turns the mismatch into a dense moire pattern. + impl->ensureResources(sceneDepth.size()); + + MaskPush maskPush; + maskPush.viewProject = camera.viewProj(); + maskPush.params = Tempest::Vec4(0.00001f,0.f,0.f,0.f); + + cmd.setDebugMarker("Selection outline mask"); + cmd.setFramebuffer({{impl->mask,Tempest::Vec4(0.f),Tempest::Preserve}}, + {sceneDepth,Tempest::Readonly}); + cmd.setViewport(0,0,sceneDepth.w(),sceneDepth.h()); + cmd.setPipeline(impl->maskPipeline); + for(const auto& draw:meshes) { + if(draw.mesh==nullptr || draw.indexCount==0) + continue; + maskPush.model = draw.transform; + cmd.setPushData(maskPush); + cmd.draw(draw.mesh->vbo,draw.mesh->ibo,draw.indexOffset,draw.indexCount); + } + + cmd.setDebugMarker("Selection outline composite"); + cmd.setFramebuffer({{target,Tempest::Preserve,Tempest::Preserve}}); + cmd.setViewport(0,0,target.w(),target.h()); + cmd.setBinding(0,impl->mask,Tempest::Sampler::nearest(Tempest::ClampMode::ClampToEdge)); + cmd.setPipeline(impl->compositePipeline); + CompositePush compositePush; + compositePush.color = Tempest::Vec4(1.f,0.68f,0.05f,0.95f); + compositePush.viewport = Tempest::Vec2(float(target.w()),float(target.h())); + compositePush.radius = 3.f; + compositePush.padding = 0.f; + cmd.setPushData(compositePush); + cmd.draw(nullptr,0,3); + } diff --git a/spacer/gizmo/selectionoutline.h b/spacer/gizmo/selectionoutline.h new file mode 100644 index 000000000..b4e827278 --- /dev/null +++ b/spacer/gizmo/selectionoutline.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +class Camera; +class WorldEdit; + +namespace Tempest { +class Attachment; +class CommandBuffer; +class ZBuffer; +template +class Encoder; +} + +class SelectionOutline final { + public: + SelectionOutline(); + ~SelectionOutline(); + + SelectionOutline(const SelectionOutline&) = delete; + SelectionOutline& operator=(const SelectionOutline&) = delete; + + void render(Tempest::Encoder& cmd, + Tempest::Attachment& target, Tempest::ZBuffer& sceneDepth, + const Camera& camera, const WorldEdit& world); + + private: + struct Impl; + std::unique_ptr impl; + }; diff --git a/spacer/ui/editors/worldeditor.cpp b/spacer/ui/editors/worldeditor.cpp index b6d3872cb..0ba9ffa1f 100644 --- a/spacer/ui/editors/worldeditor.cpp +++ b/spacer/ui/editors/worldeditor.cpp @@ -3,12 +3,15 @@ #include #include +#include + #include "ui/property/property.h" #include "ui/property/propertylist.h" #include "ui/objects/worldedit.h" #include "editorwindow.h" #include "resources.h" +#include "utils/dbgpainter.h" using namespace Tempest; @@ -85,6 +88,18 @@ void WorldEditor::processKeyboard(Tempest::KeyEvent& e) { } void WorldEditor::keyDownEvent(Tempest::KeyEvent& e) { + if(e.key==KeyEvent::K_1) { + gizmo.setMode(Im3dGizmo::Mode::Translation); + e.accept(); + } + else if(e.key==KeyEvent::K_2) { + gizmo.setMode(Im3dGizmo::Mode::Rotation); + e.accept(); + } + else if(e.key==KeyEvent::K_3) { + gizmo.setMode(Im3dGizmo::Mode::Scale); + e.accept(); + } processKeyboard(e); update(); } @@ -96,6 +111,31 @@ void WorldEditor::keyUpEvent(Tempest::KeyEvent& e) { void WorldEditor::mouseDownEvent(Tempest::MouseEvent& e) { mpos = e.pos(); + if(e.button==MouseEvent::ButtonLeft) { + leftMouseDown = true; + if(level!=nullptr && !gizmo.isHovered() && !gizmo.isActive()) { + updateCursorRay(); + level->select(cursorRayOrigin,cursorRayDirection,camera.zFar()); + suppressGizmo = true; + } + } + e.accept(); + update(); + } + +void WorldEditor::mouseUpEvent(Tempest::MouseEvent& e) { + mpos = e.pos(); + if(e.button==MouseEvent::ButtonLeft) { + leftMouseDown = false; + suppressGizmo = false; + } + e.accept(); + update(); + } + +void WorldEditor::mouseMoveEvent(Tempest::MouseEvent& e) { + mpos = e.pos(); + e.accept(); update(); } @@ -103,6 +143,12 @@ void WorldEditor::mouseDragEvent(Tempest::MouseEvent& e) { const auto dp = (e.pos()-mpos); mpos = e.pos(); + if(e.button!=MouseEvent::ButtonRight) { + e.accept(); + update(); + return; + } + PointF dpScaled = PointF(dp.x, dp.y); dpScaled.x/=float(w()); dpScaled.y/=float(h()); @@ -113,6 +159,7 @@ void WorldEditor::mouseDragEvent(Tempest::MouseEvent& e) { auto rot = camera.spin() + PointF(dpScaled.y,-dpScaled.x); camera.setSpin(rot); camera.setAngles(camera.spin()); + e.accept(); update(); } @@ -130,6 +177,8 @@ void WorldEditor::paintEvent(PaintEvent& e) { p.setBrush(textureCast(sceneImage)); p.drawRect(0, 0, w(), h(), 0, 0, sceneImage.w(), sceneImage.h()); + + paintEditorOverlay(p); } void WorldEditor::resizeEvent(SizeEvent& e) { @@ -151,7 +200,62 @@ void WorldEditor::update3d(Tempest::Encoder& cmd, uint8_ return; tickCamera(16); //TODO - renderer.draw(sceneImage, cmd, cmdId, level->view(), camera); + if(level!=nullptr) { + renderer.draw(sceneImage, cmd, cmdId, level->view(), camera); + selectionOutline.render(cmd,sceneImage,renderer.sceneDepthBuffer(),camera,*level); + } + gizmo.render(cmd,sceneImage,cmdId); + } + +void WorldEditor::updateCursorRay() { + if(w()<=0 || h()<=0) + return; + + auto invViewProj = camera.viewProj(); + invViewProj.inverse(); + + const float x = float(mpos.x)*2.f/float(w())-1.f; + const float y = float(mpos.y)*2.f/float(h())-1.f; + Tempest::Vec3 farPoint = {x,y,1.f}; + invViewProj.project(farPoint); + + cursorRayOrigin = camera.originLwc(); + const auto direction = farPoint-cursorRayOrigin; + if(direction.length()>1e-7f) + cursorRayDirection = Tempest::Vec3::normalize(direction); + } + +void WorldEditor::paintEditorOverlay(Tempest::Painter& painter) { + if(level==nullptr || w()<=0 || h()<=0) + return; + + updateCursorRay(); + bool changed = false; + Tempest::Matrix4x4 transform; + if(level->hasSelection()) { + transform = level->selectedTransform(); + changed = gizmo.prepare(camera,cursorRayOrigin,cursorRayDirection, + leftMouseDown && !suppressGizmo,w(),h(),&transform); + if(changed) { + level->setSelectedTransform(transform); + update(); + } + } + else { + gizmo.prepare(camera,cursorRayOrigin,cursorRayDirection, + leftMouseDown && !suppressGizmo,w(),h(),nullptr); + } + + DbgPainter text(painter,camera.viewProj(),w(),h()); + const char* mode = gizmo.mode()==Im3dGizmo::Mode::Translation ? "[1] Move 2 Rotate 3 Scale" : + gizmo.mode()==Im3dGizmo::Mode::Rotation ? "1 Move [2] Rotate 3 Scale" : + "1 Move 2 Rotate [3] Scale"; + text.drawText(8,8,mode); + if(level->hasSelection()) { + std::string label = "Selected: "; + label += level->selectedName(); + text.drawText(8,28,label); + } } void WorldEditor::tickCamera(uint64_t dt) { diff --git a/spacer/ui/editors/worldeditor.h b/spacer/ui/editors/worldeditor.h index 795e084b6..f33424429 100644 --- a/spacer/ui/editors/worldeditor.h +++ b/spacer/ui/editors/worldeditor.h @@ -9,6 +9,8 @@ #include "graphics/renderer.h" #include "utils/keycodec.h" #include "camera.h" +#include "gizmo/im3dgizmo.h" +#include "gizmo/selectionoutline.h" class WorldEdit; @@ -29,6 +31,8 @@ class WorldEditor: public BaseEditor, void keyUpEvent(Tempest::KeyEvent& e) override; void mouseDownEvent(Tempest::MouseEvent& e) override; + void mouseUpEvent (Tempest::MouseEvent& e) override; + void mouseMoveEvent(Tempest::MouseEvent& e) override; void mouseDragEvent(Tempest::MouseEvent& e) override; void moveDropOver (DropOverEvent& ev) override; @@ -42,6 +46,8 @@ class WorldEditor: public BaseEditor, void update3d(Tempest::Encoder& cmd, uint8_t cmdId); void processKeyboard(Tempest::KeyEvent& e); void tickCamera(uint64_t dt); + void updateCursorRay(); + void paintEditorOverlay(Tempest::Painter& painter); std::vector props; @@ -57,4 +63,10 @@ class WorldEditor: public BaseEditor, bool ctrl[KeyCodec::Last] = {}; Tempest::Point mpos = {}; - }; \ No newline at end of file + Tempest::Vec3 cursorRayOrigin = {}; + Tempest::Vec3 cursorRayDirection = {0,0,1}; + SelectionOutline selectionOutline; + Im3dGizmo gizmo; + bool leftMouseDown = false; + bool suppressGizmo = false; + }; diff --git a/spacer/ui/objects/worldedit.cpp b/spacer/ui/objects/worldedit.cpp index f01abd689..9bf53b0d8 100644 --- a/spacer/ui/objects/worldedit.cpp +++ b/spacer/ui/objects/worldedit.cpp @@ -3,9 +3,14 @@ #include #include #include +#include +#include +#include +#include #include #include "graphics/mesh/submesh/packedmesh.h" +#include "graphics/mesh/submesh/staticmesh.h" #include "graphics/worldview.h" #include "physics/dynamicworld.h" #include "utils/workers.h" @@ -13,6 +18,105 @@ using namespace Tempest; +namespace { + +Tempest::Matrix4x4 vobTransform(const zenkit::VirtualObject& vob) { + return Tempest::Matrix4x4(vob.rotation.columns[0].x, vob.rotation.columns[1].x, vob.rotation.columns[2].x, vob.position.x, + vob.rotation.columns[0].y, vob.rotation.columns[1].y, vob.rotation.columns[2].y, vob.position.y, + vob.rotation.columns[0].z, vob.rotation.columns[1].z, vob.rotation.columns[2].z, vob.position.z, + 0, 0, 0, 1); + } + +bool rayAabb(const Tempest::Vec3& origin, const Tempest::Vec3& direction, + const Tempest::Vec3& bmin, const Tempest::Vec3& bmax, float& distance) { + float tMin = 0.f; + float tMax = std::numeric_limits::max(); + const float org[3] = {origin.x, origin.y, origin.z}; + const float dir[3] = {direction.x, direction.y, direction.z}; + const float mn [3] = {bmin.x, bmin.y, bmin.z}; + const float mx [3] = {bmax.x, bmax.y, bmax.z}; + + for(size_t i=0; i<3; ++i) { + if(std::abs(dir[i])<1e-7f) { + if(org[i]mx[i]) + return false; + continue; + } + + float a = (mn[i]-org[i])/dir[i]; + float b = (mx[i]-org[i])/dir[i]; + if(a>b) + std::swap(a,b); + tMin = std::max(tMin,a); + tMax = std::min(tMax,b); + if(tMin>tMax) + return false; + } + + distance = tMin; + return true; + } + +bool rayObb(const Tempest::Vec3& origin, const Tempest::Vec3& direction, + const Tempest::Matrix4x4& transform, const Tempest::Vec3 bbox[2], float& distance) { + auto inv = transform; + inv.inverse(); + + auto localOrigin = origin; + inv.project(localOrigin); + + float x = direction.x; + float y = direction.y; + float z = direction.z; + float w = 0.f; + inv.project(x,y,z,w); + return rayAabb(localOrigin, Tempest::Vec3(x,y,z), bbox[0], bbox[1], distance); + } + +bool validBounds(const Tempest::Vec3 bbox[2]) { + return bbox[0].x1.f) + return false; + + const auto q = Tempest::Vec3::crossProduct(t,edge1); + const float v = Tempest::Vec3::dotProduct(direction,q)*invDeterminant; + if(v<0.f || u+v>1.f) + return false; + + distance = Tempest::Vec3::dotProduct(edge2,q)*invDeterminant; + return distance>=0.f; + } + +} + +struct WorldEdit::PickMesh { + std::vector vertices; + std::vector indices; + }; + WorldEdit::WorldEdit(std::string_view wname) { const auto* entry = Resources::vdfsIndex().find(wname); @@ -64,15 +168,13 @@ void WorldEdit::initView(Vob& out) { assert(out.orig!=nullptr); const auto& vob = *out.orig; const auto& visName = vob.visual_name; + out.transform = vobTransform(vob); + if(!isLevelVob(vob.vob_name)) + selectable.push_back(&out); + if(visName.empty()) return; - //TODO: hierarchical transform? - auto pos = Tempest::Matrix4x4(vob.rotation.columns[0].x, vob.rotation.columns[1].x, vob.rotation.columns[2].x, vob.position.x, - vob.rotation.columns[0].y, vob.rotation.columns[1].y, vob.rotation.columns[2].y, vob.position.y, - vob.rotation.columns[0].z, vob.rotation.columns[1].z, vob.rotation.columns[2].z, vob.position.z, - 0, 0, 0, 1); - //FIXME: copypaste from ObjVisual if(out.orig->type==zenkit::VirtualObjectType::zCVob) { switch (vob.visual->type) { @@ -85,7 +187,7 @@ void WorldEdit::initView(Vob& out) { if(vob.show_visual) { out.mesh = wview->addStaticView(view, true); out.mesh.setWind(vob.anim_mode,vob.anim_strength); - out.mesh.setObjMatrix(pos); + out.mesh.setObjMatrix(out.transform); } } case zenkit::VisualType::DECAL: @@ -98,3 +200,163 @@ void WorldEdit::initView(Vob& out) { } } } + +bool WorldEdit::select(const Tempest::Vec3& rayOrigin, const Tempest::Vec3& rayDirection, float maxDistance) { + Vob* nearest = nullptr; + float nearestDistance = maxDistance; + + if(physics!=nullptr) { + const auto landscape = physics->ray(rayOrigin,rayOrigin+rayDirection*maxDistance); + if(landscape.hasCol) + nearestDistance = (landscape.v-rayOrigin).length(); + } + + for(auto* vob:selectable) { + float distance = 0.f; + bool hit = false; + if(!vob->mesh.isEmpty()) { + const auto bounds = vob->mesh.bounds(); + float broadDistance = 0.f; + hit = validBounds(bounds.bbox) && + rayObb(rayOrigin, rayDirection, vob->transform, bounds.bbox, broadDistance) && + broadDistance<=nearestDistance && + rayMesh(*vob,rayOrigin,rayDirection,distance); + } + + if(hit && distancesecond; + + auto data = std::make_shared(); + data->vertices.resize(mesh.vbo.size()); + data->indices.resize(mesh.ibo.size()); + if(!data->vertices.empty()) + Resources::device().readBytes(mesh.vbo,data->vertices.data(),mesh.vbo.byteSize()); + if(!data->indices.empty()) + Resources::device().readBytes(mesh.ibo,data->indices.data(),mesh.ibo.byteSize()); + auto result = pickMeshes.emplace(&mesh,std::move(data)); + return *result.first->second; + } + +bool WorldEdit::rayMesh(Vob& vob, const Tempest::Vec3& rayOrigin, + const Tempest::Vec3& rayDirection, float& distance) { + bool hit = false; + float nearest = std::numeric_limits::max(); + for(size_t nodeId=0; nodeId(slice.first+slice.second,geometry.indices.size()); + for(size_t index=slice.first; index+2=geometry.vertices.size() || ib>=geometry.vertices.size() || ic>=geometry.vertices.size()) + continue; + const auto& va = geometry.vertices[ia]; + const auto& vb = geometry.vertices[ib]; + const auto& vc = geometry.vertices[ic]; + float triangleDistance = 0.f; + if(rayTriangle(localOrigin,localDirection, + {va.pos[0],va.pos[1],va.pos[2]}, + {vb.pos[0],vb.pos[1],vb.pos[2]}, + {vc.pos[0],vc.pos[1],vc.pos[2]},triangleDistance) && triangleDistancetransform; + } + +void WorldEdit::setSelectedTransform(const Tempest::Matrix4x4& transform) { + if(selected==nullptr) + return; + + const auto oldPosition = selected->orig->position; + selected->transform = transform; + selected->mesh.setObjMatrix(transform); + + auto& vob = *selected->orig; + vob.position = {transform.at(3,0), transform.at(3,1), transform.at(3,2)}; + for(size_t column=0; column<3; ++column) { + const Tempest::Vec3 axis = {transform.at(int(column),0), transform.at(int(column),1), transform.at(int(column),2)}; + const float scale = axis.length(); + if(scale<=1e-7f) + continue; + vob.rotation.columns[column] = {axis.x/scale, axis.y/scale, axis.z/scale}; + } + + if(selected->mesh.isEmpty()) { + const float dx = vob.position.x-oldPosition.x; + const float dy = vob.position.y-oldPosition.y; + const float dz = vob.position.z-oldPosition.z; + vob.bbox.min.x += dx; vob.bbox.max.x += dx; + vob.bbox.min.y += dy; vob.bbox.max.y += dy; + vob.bbox.min.z += dz; vob.bbox.max.z += dz; + } + } + +std::string_view WorldEdit::selectedName() const { + if(selected==nullptr) + return {}; + if(!selected->orig->vob_name.empty()) + return selected->orig->vob_name; + return selected->orig->visual_name; + } + +void WorldEdit::selectedMeshes(std::vector& out) const { + out.clear(); + if(selected==nullptr) + return; + + out.reserve(selected->mesh.nodesCount()); + for(size_t nodeId=0; nodeIdmesh.nodesCount(); ++nodeId) { + const auto node = selected->mesh.node(nodeId); + const auto* mesh = node.mesh(); + if(mesh==nullptr) + continue; + const auto slice = node.meshSlice(); + SelectedMesh draw; + draw.mesh = mesh; + draw.transform = node.position(); + draw.indexOffset = slice.first; + draw.indexCount = slice.second; + out.push_back(draw); + } + } diff --git a/spacer/ui/objects/worldedit.h b/spacer/ui/objects/worldedit.h index cf52e2235..f3e165f4f 100644 --- a/spacer/ui/objects/worldedit.h +++ b/spacer/ui/objects/worldedit.h @@ -2,6 +2,11 @@ #include #include +#include +#include + +#include +#include #include @@ -9,27 +14,49 @@ class DynamicWorld; class WorldView; +class StaticMesh; class WorldEdit { public: + struct SelectedMesh { + const StaticMesh* mesh = nullptr; + Tempest::Matrix4x4 transform; + uint32_t indexOffset = 0; + uint32_t indexCount = 0; + }; + WorldEdit(std::string_view wname); ~WorldEdit(); WorldView& view() { return *wview; } + bool select(const Tempest::Vec3& rayOrigin, const Tempest::Vec3& rayDirection, float maxDistance); + bool hasSelection() const; + Tempest::Matrix4x4 selectedTransform() const; + void setSelectedTransform(const Tempest::Matrix4x4& transform); + std::string_view selectedName() const; + void selectedMeshes(std::vector& out) const; + private: struct Vob { std::vector child; std::shared_ptr orig; MeshObjects::Mesh mesh; + Tempest::Matrix4x4 transform; }; + struct PickMesh; + void load(Vob& out, std::vector>& child); void initView(Vob& out); + const PickMesh& pickMesh(const StaticMesh& mesh); + bool rayMesh(Vob& vob, const Tempest::Vec3& rayOrigin, const Tempest::Vec3& rayDirection, float& distance); Vob root; + std::vector selectable; + Vob* selected = nullptr; + std::unordered_map> pickMeshes; std::unique_ptr physics; std::unique_ptr wview; }; -