From e5a4e7d1cb94597e5dc9ec5e66d55c88d101908a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tur=C3=A1nszki=20J=C3=A1nos?= Date: Wed, 4 Oct 2023 19:48:34 +0200 Subject: [PATCH] water improvements --- WickedEngine/shaders/ShaderInterop_Renderer.h | 9 ++- WickedEngine/shaders/objectHF.hlsli | 28 +++++-- WickedEngine/shaders/oceanSurfacePS.hlsl | 77 ++++++++++--------- WickedEngine/shaders/underwaterCS.hlsl | 31 ++++++-- WickedEngine/wiGraphicsDevice_DX12.cpp | 29 +++++++ WickedEngine/wiRenderPath3D.cpp | 4 +- WickedEngine/wiRenderer.cpp | 4 +- WickedEngine/wiScene_Components.cpp | 2 + WickedEngine/wiScene_Components.h | 2 + WickedEngine/wiVersion.cpp | 2 +- 10 files changed, 131 insertions(+), 57 deletions(-) diff --git a/WickedEngine/shaders/ShaderInterop_Renderer.h b/WickedEngine/shaders/ShaderInterop_Renderer.h index cfb81fb91..5d343f80a 100644 --- a/WickedEngine/shaders/ShaderInterop_Renderer.h +++ b/WickedEngine/shaders/ShaderInterop_Renderer.h @@ -955,7 +955,7 @@ struct ShaderCamera uint output_index; // viewport or rendertarget array index float4 clip_plane; - float4 reflection_clip_plane; + float4 reflection_plane; // not clip plane (not reversed when camera is under), but the original plane float3 forward; float z_near; @@ -984,6 +984,7 @@ struct ShaderCamera float4x4 previous_view_projection; float4x4 previous_inverse_view_projection; float4x4 reflection_view_projection; + float4x4 reflection_inverse_view_projection; float4x4 reprojection; // view_projection_inverse_matrix * previous_view_projection_matrix float2 aperture_shape; @@ -1018,18 +1019,18 @@ struct ShaderCamera int buffer_entitytiles_transparent_index; int texture_reflection_index; + int texture_reflection_depth_index; int texture_refraction_index; - int texture_waterriples_index; + int texture_waterriples_index; int texture_ao_index; int texture_ssr_index; int texture_rtshadow_index; - int texture_surfelgi_index; + int texture_surfelgi_index; int texture_depth_index_prev; int texture_vxgi_diffuse_index; int texture_vxgi_specular_index; - int padding1; #ifdef __cplusplus void init() diff --git a/WickedEngine/shaders/objectHF.hlsli b/WickedEngine/shaders/objectHF.hlsli index 494f8ef0a..7a8dce2bd 100644 --- a/WickedEngine/shaders/objectHF.hlsli +++ b/WickedEngine/shaders/objectHF.hlsli @@ -726,10 +726,10 @@ float4 main(PixelInput input, in bool is_frontface : SV_IsFrontFace) : SV_Target Lighting lighting; lighting.create(0, 0, ambient, 0); - + + float4 color = surface.baseColor; #ifdef WATER - //NORMALMAP float2 bumpColor0 = 0; float2 bumpColor1 = 0; @@ -757,8 +757,18 @@ float4 main(PixelInput input, in bool is_frontface : SV_IsFrontFace) : SV_Target //REFLECTION float4 reflectionUV = mul(GetCamera().reflection_view_projection, float4(surface.P, 1)); reflectionUV.xy /= reflectionUV.w; - reflectionUV.xy = clipspace_to_uv(reflectionUV.xy); - lighting.indirect.specular += bindless_textures[GetCamera().texture_reflection_index].SampleLevel(sampler_linear_mirror, reflectionUV.xy + surface.bumpColor.rg, 0).rgb * surface.F; + reflectionUV.xy = clipspace_to_uv(reflectionUV.xy) + surface.bumpColor.rg; + float3 reflectiveColor = bindless_textures[GetCamera().texture_reflection_index].SampleLevel(sampler_linear_mirror, reflectionUV.xy, 0).rgb; + [branch] + if(GetCamera().texture_reflection_depth_index >= 0) + { + float reflectiveDepth = bindless_textures[GetCamera().texture_reflection_depth_index].SampleLevel(sampler_point_clamp, reflectionUV.xy, 0).r; + float3 reflectivePosition = reconstruct_position(reflectionUV.xy, reflectiveDepth, GetCamera().reflection_inverse_view_projection); + float4 water_plane = GetCamera().reflection_plane; + float water_depth = -dot(float4(reflectivePosition, 1), water_plane); + reflectiveColor.rgb = lerp(color.rgb, reflectiveColor.rgb, saturate(exp(-water_depth * color.a))); + } + lighting.indirect.specular += reflectiveColor * surface.F; } #endif // WATER @@ -831,21 +841,21 @@ float4 main(PixelInput input, in bool is_frontface : SV_IsFrontFace) : SV_Target #endif // ENVMAPRENDERING #endif // WATER - - float4 color = surface.baseColor; - #ifdef WATER [branch] if (GetCamera().texture_refraction_index >= 0) { // Water refraction: + float4 water_plane = GetCamera().reflection_plane; + const float camera_above_water = dot(float4(GetCamera().position, 1), water_plane) < 0; Texture2D texture_refraction = bindless_textures[GetCamera().texture_refraction_index]; - float4 water_plane = GetCamera().reflection_clip_plane; // First sample using full perturbation: float2 refraction_uv = ScreenCoord.xy + surface.bumpColor.rg; float refraction_depth = find_max_depth(refraction_uv, 2, 2); float3 refraction_position = reconstruct_position(refraction_uv, refraction_depth); float water_depth = -dot(float4(refraction_position, 1), water_plane); + if(camera_above_water) + water_depth = -water_depth; if(water_depth <= 0) { // Above water, fill holes by taking unperturbed sample: @@ -861,6 +871,8 @@ float4 main(PixelInput input, in bool is_frontface : SV_IsFrontFace) : SV_Target refraction_depth = texture_depth.SampleLevel(sampler_point_clamp, refraction_uv, 0); refraction_position = reconstruct_position(refraction_uv, refraction_depth); water_depth = max(water_depth, -dot(float4(refraction_position, 1), water_plane)); + if(camera_above_water) + water_depth = -water_depth; // Water fog computation: surface.refraction.a = saturate(exp(-water_depth * color.a)); color.a = 1; diff --git a/WickedEngine/shaders/oceanSurfacePS.hlsl b/WickedEngine/shaders/oceanSurfacePS.hlsl index 5bd36929c..cc15057ed 100644 --- a/WickedEngine/shaders/oceanSurfacePS.hlsl +++ b/WickedEngine/shaders/oceanSurfacePS.hlsl @@ -21,10 +21,6 @@ float4 main(PSIn input) : SV_TARGET float emissive = 0; uint2 pixel = input.pos.xy; - float ocean_level_at_camera_pos = xOceanWaterHeight; - ocean_level_at_camera_pos += texture_ocean_displacementmap.SampleLevel(sampler_linear_wrap, GetCamera().position.xz * xOceanPatchSizeRecip, 0).z; // texture contains xzy! - const bool camera_above_water = GetCamera().position.y > ocean_level_at_camera_pos; - const float gradient_fade = saturate(dist * 0.001); const float4 gradientNear = texture_gradientmap.Sample(sampler_aniso_wrap, input.uv); const float4 gradientFar = texture_gradientmap.Sample(sampler_aniso_wrap, input.uv * 0.125); @@ -37,7 +33,7 @@ float4 main(PSIn input) : SV_TARGET surface.pixel = pixel; float depth = input.pos.z; surface.albedo = color.rgb; - surface.f0 = camera_above_water ? 0.02 : 0.1; + surface.f0 = 0.02; surface.roughness = 0.1; surface.P = input.pos3D; surface.N = normalize(float3(gradient.x, xOceanTexelLength * 2, gradient.y)); @@ -52,59 +48,68 @@ float4 main(PSIn input) : SV_TARGET TiledLighting(surface, lighting, GetFlatTileIndex(pixel)); float2 ScreenCoord = surface.pixel * GetCamera().internal_resolution_rcp; - const float bump_strength = camera_above_water ? 0.04 : 0.1; - + const float bump_strength = 0.1; + + float4 water_plane = GetCamera().reflection_plane; + [branch] if (GetCamera().texture_reflection_index >= 0) { //REFLECTION float4 reflectionPos = mul(GetCamera().reflection_view_projection, float4(input.pos3D, 1)); - float2 reflectionUV = clipspace_to_uv(reflectionPos.xy / reflectionPos.w); - float4 reflectiveColor = bindless_textures[GetCamera().texture_reflection_index].SampleLevel(sampler_linear_mirror, reflectionUV + surface.N.xz * bump_strength, 0); + float2 reflectionUV = clipspace_to_uv(reflectionPos.xy / reflectionPos.w) + surface.N.xz * bump_strength; + float4 reflectiveColor = bindless_textures[GetCamera().texture_reflection_index].SampleLevel(sampler_linear_mirror, reflectionUV, 0); + [branch] + if(GetCamera().texture_reflection_depth_index >=0) + { + float reflectiveDepth = bindless_textures[GetCamera().texture_reflection_depth_index].SampleLevel(sampler_point_clamp, reflectionUV, 0).r; + float3 reflectivePosition = reconstruct_position(reflectionUV, reflectiveDepth, GetCamera().reflection_inverse_view_projection); + float water_depth = -dot(float4(reflectivePosition, 1), water_plane); + water_depth += texture_ocean_displacementmap.SampleLevel(sampler_linear_wrap, reflectivePosition.xz * xOceanPatchSizeRecip, 0).z; // texture contains xzy! + reflectiveColor.rgb = lerp(color.rgb, reflectiveColor.rgb, saturate(exp(-water_depth * color.a))); + } lighting.indirect.specular = reflectiveColor.rgb * surface.F; } [branch] if (GetCamera().texture_refraction_index >= 0) { + // Water refraction: + const float camera_above_water = dot(float4(GetCamera().position, 1), water_plane) < 0; + Texture2D texture_refraction = bindless_textures[GetCamera().texture_refraction_index]; + // First sample using full perturbation: + float2 refraction_uv = ScreenCoord.xy + surface.N.xz * bump_strength; + float refraction_depth = find_max_depth(refraction_uv, 2, 2); + float3 refraction_position = reconstruct_position(refraction_uv, refraction_depth); + float water_depth = -dot(float4(refraction_position, 1), water_plane); + water_depth += texture_ocean_displacementmap.SampleLevel(sampler_linear_wrap, refraction_position.xz * xOceanPatchSizeRecip, 0).z; // texture contains xzy! if (camera_above_water) + water_depth = -water_depth; + if (water_depth <= 0) { - // Water refraction: - Texture2D texture_refraction = bindless_textures[GetCamera().texture_refraction_index]; - float4 water_plane = GetCamera().reflection_clip_plane; - // First sample using full perturbation: - float2 refraction_uv = ScreenCoord.xy + surface.N.xz * bump_strength; - float refraction_depth = find_max_depth(refraction_uv, 2, 2); - float3 refraction_position = reconstruct_position(refraction_uv, refraction_depth); - float water_depth = -dot(float4(refraction_position, 1), water_plane); - if (water_depth <= 0) - { // Above water, fill holes by taking unperturbed sample: - refraction_uv = ScreenCoord.xy; - } - else - { - // Below water, compute perturbation according to first sample water depth: - refraction_uv = ScreenCoord.xy + surface.N.xz * bump_strength * saturate(1 - exp(-water_depth)); - } - surface.refraction.rgb = texture_refraction.SampleLevel(sampler_linear_mirror, refraction_uv, 0).rgb; - // Recompute depth params again with actual perturbation: - refraction_depth = texture_depth.SampleLevel(sampler_point_clamp, refraction_uv, 0); - refraction_position = reconstruct_position(refraction_uv, refraction_depth); - water_depth = max(water_depth, -dot(float4(refraction_position, 1), water_plane)); - // Water fog computation: - surface.refraction.a = saturate(exp(-water_depth * color.a)); - color.a = 1; + refraction_uv = ScreenCoord.xy; } else { - surface.refraction.a = 1; // no refraction fog when looking from under water to above water + // Below water, compute perturbation according to first sample water depth: + refraction_uv = ScreenCoord.xy + surface.N.xz * bump_strength * saturate(1 - exp(-water_depth)); } + surface.refraction.rgb = texture_refraction.SampleLevel(sampler_linear_mirror, refraction_uv, 0).rgb; + // Recompute depth params again with actual perturbation: + refraction_depth = texture_depth.SampleLevel(sampler_point_clamp, refraction_uv, 0); + refraction_position = reconstruct_position(refraction_uv, refraction_depth); + water_depth = max(water_depth, -dot(float4(refraction_position, 1), water_plane)); + water_depth += texture_ocean_displacementmap.SampleLevel(sampler_linear_wrap, refraction_position.xz * xOceanPatchSizeRecip, 0).z; // texture contains xzy! + if (camera_above_water) + water_depth = -water_depth; + // Water fog computation: + surface.refraction.a = saturate(exp(-water_depth * color.a)); + color.a = 1; } // Blend out at distance: color.a = saturate(1 - saturate(dist / GetCamera().z_far - 0.8) * 5.0); // fade will be on edge and inwards 20% - color.a = lerp(0, color.a, saturate(pow(lineardepth, 2))); // fade when very close to camera ApplyLighting(surface, lighting, color); diff --git a/WickedEngine/shaders/underwaterCS.hlsl b/WickedEngine/shaders/underwaterCS.hlsl index fc901683b..f9e02d2ae 100644 --- a/WickedEngine/shaders/underwaterCS.hlsl +++ b/WickedEngine/shaders/underwaterCS.hlsl @@ -15,7 +15,7 @@ void main(uint3 DTid : SV_DispatchThreadID) float2 uv = (DTid.xy + 0.5f) * postprocess.resolution_rcp; // Unproject near plane and determine for every pixel if it's below water surface: - float4 clipspace = float4(uv_to_clipspace(uv), 0.1, 1); // pushed away from near plane to better match up with ocean mesh (and reversed z) + float4 clipspace = float4(uv_to_clipspace(uv), 1, 1); float4 unproj = mul(GetCamera().inverse_view_projection, clipspace); unproj.xyz /= unproj.w; float3 world_pos = unproj.xyz; @@ -38,10 +38,11 @@ void main(uint3 DTid : SV_DispatchThreadID) // It's not realistic to apply much refraction underwater to camera, but looks cool: uv += sin(uv * 10 + GetFrame().time * 5) * 0.005; uv += sin(-uv.y * 5 + GetFrame().time * 2) * 0.005; -#endif color = input.SampleLevel(sampler_linear_mirror, uv, 0); +#endif - const float lineardepth = texture_lineardepth.SampleLevel(sampler_linear_clamp, uv, 0).r * GetCamera().z_far; + const float depth = texture_depth[DTid.xy]; + const float lineardepth = compute_lineardepth(depth); // The ocean is not rendered into the lineardepth unfortunately, so we also trace it: // Otherwise the ocean surface could be same as infinite depth and incorrectly fogged @@ -53,9 +54,29 @@ void main(uint3 DTid : SV_DispatchThreadID) const float3 displacement = texture_displacementmap.SampleLevel(sampler_linear_wrap, ocean_surface_uv, 0).xzy; ocean_surface_pos += displacement; const float ocean_dist = length(ocean_surface_pos - o); + + float3 surface_position = reconstruct_position(uv, depth); + [branch] + if (ocean.texture_displacementmap >= 0) + { + const float2 ocean_uv = surface_position.xz * ocean.patch_size_rcp; + Texture2D texture_displacementmap = bindless_textures[ocean.texture_displacementmap]; + const float3 displacement = texture_displacementmap.SampleLevel(sampler_linear_wrap, ocean_uv, 0).xzy; + surface_position += displacement; + } + float water_depth = ocean_pos.y - surface_position.y; + water_depth = max(min(lineardepth, ocean_dist), water_depth); - float3 modified_color = lerp(color.rgb, ocean.water_color.rgb, 1 - saturate(exp(-lineardepth * ocean.water_color.a))); - color.rgb = lerp(color.rgb, modified_color, pow(saturate(ocean_pos.y - world_pos.y), 0.25)); + color.rgb = lerp(ocean.water_color.rgb, color.rgb, saturate(exp(-water_depth * ocean.water_color.a))); + //color = float4(1, 0, 0, 1); } + + // Transition at intersection: + float intersection_direction = world_pos.y - ocean_pos.y; + float intersection_distance = abs(intersection_direction); + float intersection_blend = saturate(exp(-intersection_distance * 100)); + float3 intersection_color = ocean.water_color.rgb; + color.rgb = lerp(color.rgb, intersection_color, intersection_blend); + output[DTid.xy] = color; } diff --git a/WickedEngine/wiGraphicsDevice_DX12.cpp b/WickedEngine/wiGraphicsDevice_DX12.cpp index 91875ce63..70119fccb 100644 --- a/WickedEngine/wiGraphicsDevice_DX12.cpp +++ b/WickedEngine/wiGraphicsDevice_DX12.cpp @@ -2717,6 +2717,35 @@ using namespace dx12_internal; } } + if (features.HighestShaderModel() < D3D_SHADER_MODEL_6_0) + { + std::string error = "Shader model 6.0 is required, but not supported by your system!\n"; + error += "Adapter name: " + adapterName + "\n"; + error += "Adapter type: "; + switch (adapterType) + { + case wi::graphics::AdapterType::IntegratedGpu: + error += "Integrated GPU"; + break; + case wi::graphics::AdapterType::DiscreteGpu: + error += "Discrete GPU"; + break; + case wi::graphics::AdapterType::VirtualGpu: + error += "Virtual GPU"; + break; + case wi::graphics::AdapterType::Cpu: + error += "CPU"; + break; + default: + error += "Unknown"; + break; + } + error += "\nExiting."; + wi::helper::messageBox(error, "Error!"); + wi::backlog::post(error, wi::backlog::LogLevel::Error); + wi::platform::Exit(); + } + if (features.ConservativeRasterizationTier() >= D3D12_CONSERVATIVE_RASTERIZATION_TIER_1) { capabilities |= GraphicsDeviceCapability::CONSERVATIVE_RASTERIZATION; diff --git a/WickedEngine/wiRenderPath3D.cpp b/WickedEngine/wiRenderPath3D.cpp index 20faed89b..cd7cfff9e 100644 --- a/WickedEngine/wiRenderPath3D.cpp +++ b/WickedEngine/wiRenderPath3D.cpp @@ -634,6 +634,7 @@ namespace wi camera->buffer_entitytiles_opaque_index = device->GetDescriptorIndex(&tiledLightResources.entityTiles_Opaque, SubresourceType::SRV); camera->buffer_entitytiles_transparent_index = device->GetDescriptorIndex(&tiledLightResources.entityTiles_Transparent, SubresourceType::SRV); camera->texture_reflection_index = device->GetDescriptorIndex(&rtReflection, SubresourceType::SRV); + camera->texture_reflection_depth_index = device->GetDescriptorIndex(&depthBuffer_Reflection, SubresourceType::SRV); camera->texture_refraction_index = device->GetDescriptorIndex(&rtSceneCopy, SubresourceType::SRV); camera->texture_waterriples_index = device->GetDescriptorIndex(&rtWaterRipple, SubresourceType::SRV); camera->texture_ao_index = device->GetDescriptorIndex(&rtAO, SubresourceType::SRV); @@ -668,6 +669,7 @@ namespace wi camera_reflection.buffer_entitytiles_opaque_index = device->GetDescriptorIndex(&tiledLightResources_planarReflection.entityTiles_Opaque, SubresourceType::SRV); camera_reflection.buffer_entitytiles_transparent_index = device->GetDescriptorIndex(&tiledLightResources_planarReflection.entityTiles_Transparent, SubresourceType::SRV); camera_reflection.texture_reflection_index = -1; + camera_reflection.texture_reflection_depth_index = -1; camera_reflection.texture_refraction_index = -1; camera_reflection.texture_waterriples_index = -1; camera_reflection.texture_ao_index = -1; @@ -2471,8 +2473,6 @@ namespace wi desc.bind_flags = BindFlag::DEPTH_STENCIL | BindFlag::SHADER_RESOURCE; desc.format = wi::renderer::format_depthbuffer_main; - desc.width = internalResolution.x / 2; - desc.height = internalResolution.y / 2; desc.layout = ResourceState::DEPTHSTENCIL; device->CreateTexture(&desc, nullptr, &depthBuffer_Reflection); device->SetName(&depthBuffer_Reflection, "depthBuffer_Reflection"); diff --git a/WickedEngine/wiRenderer.cpp b/WickedEngine/wiRenderer.cpp index 334121029..af8d3ab0d 100644 --- a/WickedEngine/wiRenderer.cpp +++ b/WickedEngine/wiRenderer.cpp @@ -9467,7 +9467,7 @@ void BindCameraCB( shadercam.z_range = abs(shadercam.z_far - shadercam.z_near); shadercam.z_range_rcp = 1.0f / std::max(0.0001f, shadercam.z_range); shadercam.clip_plane = camera.clipPlane; - shadercam.reflection_clip_plane = camera_reflection.clipPlane; + shadercam.reflection_plane = camera_reflection.clipPlaneOriginal; static_assert(arraysize(camera.frustum.planes) == arraysize(shadercam.frustum.planes), "Mismatch!"); for (int i = 0; i < arraysize(camera.frustum.planes); ++i) @@ -9483,6 +9483,7 @@ void BindCameraCB( XMStoreFloat4x4(&shadercam.previous_view_projection, camera_previous.GetViewProjection()); XMStoreFloat4x4(&shadercam.previous_inverse_view_projection, camera_previous.GetInvViewProjection()); XMStoreFloat4x4(&shadercam.reflection_view_projection, camera_reflection.GetViewProjection()); + XMStoreFloat4x4(&shadercam.reflection_inverse_view_projection, camera_reflection.GetInvViewProjection()); XMStoreFloat4x4(&shadercam.reprojection, camera.GetInvViewProjection() * camera_previous.GetViewProjection()); shadercam.focal_length = camera.focal_length; @@ -9519,6 +9520,7 @@ void BindCameraCB( shadercam.buffer_entitytiles_opaque_index = camera.buffer_entitytiles_opaque_index; shadercam.buffer_entitytiles_transparent_index = camera.buffer_entitytiles_transparent_index; shadercam.texture_reflection_index = camera.texture_reflection_index; + shadercam.texture_reflection_depth_index = camera.texture_reflection_depth_index; shadercam.texture_refraction_index = camera.texture_refraction_index; shadercam.texture_waterriples_index = camera.texture_waterriples_index; shadercam.texture_ao_index = camera.texture_ao_index; diff --git a/WickedEngine/wiScene_Components.cpp b/WickedEngine/wiScene_Components.cpp index ab632a527..f7fd128e2 100644 --- a/WickedEngine/wiScene_Components.cpp +++ b/WickedEngine/wiScene_Components.cpp @@ -1733,6 +1733,8 @@ namespace wi::scene XMVECTOR _Up = XMLoadFloat3(&Up); XMMATRIX _Ref = XMMatrixReflect(XMLoadFloat4(&plane)); + clipPlaneOriginal = plane; + // reverse clipping if behind clip plane ("if underwater") clipPlane = plane; float d = XMVectorGetX(XMPlaneDotCoord(XMLoadFloat4(&clipPlane), _Eye)); diff --git a/WickedEngine/wiScene_Components.h b/WickedEngine/wiScene_Components.h index 853900a18..3cbafb7ea 100644 --- a/WickedEngine/wiScene_Components.h +++ b/WickedEngine/wiScene_Components.h @@ -976,6 +976,7 @@ namespace wi::scene XMFLOAT4X4 InvView, InvProjection, InvVP; XMFLOAT2 jitter; XMFLOAT4 clipPlane = XMFLOAT4(0, 0, 0, 0); // default: no clip plane + XMFLOAT4 clipPlaneOriginal = XMFLOAT4(0, 0, 0, 0); // not reversed clip plane wi::Canvas canvas; wi::graphics::Rect scissor; uint32_t sample_count = 1; @@ -986,6 +987,7 @@ namespace wi::scene int texture_normal_index = -1; int texture_roughness_index = -1; int texture_reflection_index = -1; + int texture_reflection_depth_index = -1; int texture_refraction_index = -1; int texture_waterriples_index = -1; int texture_ao_index = -1; diff --git a/WickedEngine/wiVersion.cpp b/WickedEngine/wiVersion.cpp index 0d0aedd1e..2d783fdca 100644 --- a/WickedEngine/wiVersion.cpp +++ b/WickedEngine/wiVersion.cpp @@ -9,7 +9,7 @@ namespace wi::version // minor features, major updates, breaking compatibility changes const int minor = 71; // minor bug fixes, alterations, refactors, updates - const int revision = 310; + const int revision = 311; const std::string version_string = std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(revision);