From 23001377fda7411aa2cdff50ccc3b6c945e3b396 Mon Sep 17 00:00:00 2001 From: Neven Johnson Date: Mon, 22 Dec 2025 23:45:35 -0500 Subject: [PATCH 01/14] Implement initial normal mapping --- simulator/model.cpp | 23 ++++++++++++++++++- simulator/shaders/shaders.wgsl | 40 +++++++++++++++++++++++++++------- simulator/simulator.hpp | 3 +++ simulator/simulator.render.cpp | 39 ++++++++++++++++++++++++++------- urdf/textures/soil_normal.png | 3 +++ 5 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 urdf/textures/soil_normal.png diff --git a/simulator/model.cpp b/simulator/model.cpp index 0c27d4fec..702d12f3b 100644 --- a/simulator/model.cpp +++ b/simulator/model.cpp @@ -34,7 +34,7 @@ namespace mrover { if (!mesh->HasNormals()) throw std::invalid_argument{std::format("Mesh #{} has no normals", meshIndex)}; if (!mesh->HasTextureCoords(0)) throw std::invalid_argument{std::format("Mesh #{} has no texture coordinates", meshIndex)}; - auto& [vertices, normals, uvs, indices, texture] = meshes.emplace_back(); + auto& [vertices, normals, tangents, bitangents, uvs, indices, texture, normal_map] = meshes.emplace_back(); assert(mesh->HasPositions()); vertices.data.resize(mesh->mNumVertices); @@ -67,6 +67,17 @@ namespace mrover { uvs.data[uvIndex] = {uv.x, uv.y}; } + assert(mesh->HasTangentsAndBitangents()); + tangents.data.resize(mesh->mNumVertices); + bitangents.data.resize(mesh->mNumVertices); + for (std::size_t vertexIndex = 0; vertexIndex < mesh->mNumVertices; ++vertexIndex) { + aiVector3D const& tangent = mesh->mTangents[vertexIndex]; + aiVector3D const& bitangent = mesh->mBitangents[vertexIndex]; + tangents.data[vertexIndex] = Eigen::Vector3f{tangent.x, tangent.y, tangent.z}; + // from testing it seems like the bitangents blender exports are backwards, but this seems sus + bitangents.data[vertexIndex] = Eigen::Vector3f{-bitangent.x, -bitangent.y, -bitangent.z}; + } + if (aiMaterial const* material = scene->mMaterials[mesh->mMaterialIndex]) { if (aiString path; material->GetTextureCount(aiTextureType_DIFFUSE) > 0 && material->GetTexture(aiTextureType_DIFFUSE, 0, &path) == AI_SUCCESS) { texture.data = readTexture(path.C_Str()); @@ -82,6 +93,16 @@ namespace mrover { } aiString name; material->Get(AI_MATKEY_NAME, name); + + if (aiString path; material->GetTextureCount(aiTextureType_NORMALS) > 0 && material->GetTexture(aiTextureType_NORMALS, 0, &path) == AI_SUCCESS) { + normal_map.data = readTexture(path.C_Str()); + } else { + // create a 1x1 texture with normal pointing straight out + // this is simply the vector (0, 0, 1) (points up in the Z direction) + cv::Scalar color(255, 128, 128); + normal_map.data = cv::Mat{1, 1, CV_8UC4, color}; + } + RCLCPP_INFO_STREAM(logger, std::format("\tLoaded material: {}", name.C_Str())); } diff --git a/simulator/shaders/shaders.wgsl b/simulator/shaders/shaders.wgsl index d751dcc23..1786f82a1 100644 --- a/simulator/shaders/shaders.wgsl +++ b/simulator/shaders/shaders.wgsl @@ -18,17 +18,22 @@ struct MeshUniforms { @group(0) @binding(0) var mu: MeshUniforms; @group(0) @binding(1) var texture: texture_2d; @group(0) @binding(2) var textureSampler: sampler; +@group(0) @binding(3) var normalTexture: texture_2d; struct InVertex { @location(0) positionInModel: vec3f, @location(1) normalInModel: vec3f, - @location(2) uv: vec2f, + @location(2) tangentInModel: vec3f, + @location(3) bitangentInModel: vec3f, + @location(4) uv: vec2f, } struct OutVertex { @builtin(position) positionInClip: vec4f, @location(0) positionInWorld: vec4f, @location(1) normalInWorld: vec4f, - @location(2) uv: vec2f, + @location(2) tangentInWorld: vec4f, + @location(3) bitangentInWorld: vec4f, + @location(4) uv: vec2f, } @vertex fn vs_main(in: InVertex) -> OutVertex { let positionInWorld = mu.modelToWorld * vec4(in.positionInModel, 1); @@ -36,6 +41,9 @@ struct OutVertex { out.positionInClip = su.cameraToClip * su.worldToCamera * positionInWorld; out.positionInWorld = positionInWorld; out.normalInWorld = mu.modelToWorldForNormals * vec4(in.normalInModel, 0); + // TODO: should I just use the regular modelToWorld matrix here? + out.tangentInWorld = mu.modelToWorldForNormals * vec4(in.tangentInModel, 0); + out.bitangentInWorld = mu.modelToWorldForNormals * vec4(in.bitangentInModel, 0); out.uv = in.uv; return out; } @@ -47,24 +55,40 @@ struct OutFragment { @fragment fn fs_main(in: OutVertex) -> OutFragment { let baseColor = textureSample(texture, textureSampler, in.uv); + let normalMapStrength = 1.0; + // each component is in the range [0,1] + let encodedNormal = textureSample(normalTexture, textureSampler, in.uv).rgb; + // shift to get negative values, then normalize because we only care about direction + // this normal is in a frame local to the surface of this face + let normalInLocal = normalize(encodedNormal - 0.5); + + let localToWorld = mat3x3f( + normalize(in.tangentInWorld).xyz, + normalize(in.bitangentInWorld).xyz, + normalize(in.normalInWorld).xyz, + ); + + let normalInWorld = normalize(localToWorld * normalInLocal); + let mixedNormal = normalize(mix(in.normalInWorld, vec4(normalInWorld, 0), normalMapStrength)); + var out : OutFragment; - out.normalInCamera = (su.worldToCamera * in.normalInWorld + vec4(1, 1, 1, 0)) / 2; + out.normalInCamera = (su.worldToCamera * mixedNormal + vec4(1, 1, 1, 0)) / 2; out.normalInCamera.a = 1; // Ambient - let ambientStrength = 0.6; + let ambientStrength = 0.3; let ambient = ambientStrength * su.lightColor; // Diffuse let lightDirInWorld = normalize(su.lightInWorld - in.positionInWorld); - let diff = max(dot(in.normalInWorld, lightDirInWorld), 0.0); + let diff = max(dot(mixedNormal, lightDirInWorld), 0.0); let diffuse = diff * su.lightColor; // Specular - let specularStrength = 0.5; + let specularStrength = 0.1; let viewDirInWolrd = normalize(su.cameraInWorld - in.positionInWorld); - let reflectDir = reflect(-lightDirInWorld, in.normalInWorld); + let reflectDir = reflect(-lightDirInWorld, mixedNormal); let spec = pow(max(dot(viewDirInWolrd, reflectDir), 0.0), 32); let specular = specularStrength * spec * su.lightColor; // Combination - out.color = vec4(((ambient + diffuse + specular) * baseColor).rgb, 1); + out.color = vec4(((ambient + diffuse) * baseColor + specular).rgb, 1); return out; } diff --git a/simulator/simulator.hpp b/simulator/simulator.hpp index 32bb0050a..91d67c767 100644 --- a/simulator/simulator.hpp +++ b/simulator/simulator.hpp @@ -63,9 +63,12 @@ namespace mrover { struct Mesh { SharedBuffer vertices; SharedBuffer normals; + SharedBuffer tangents; + SharedBuffer bitangents; SharedBuffer uvs; SharedBuffer indices; MeshTexture texture; + MeshTexture normal_map; }; // DO NOT access the mesh unless you are certain it has been set from the async loader diff --git a/simulator/simulator.render.cpp b/simulator/simulator.render.cpp index 82acd4709..91b97aa0c 100644 --- a/simulator/simulator.render.cpp +++ b/simulator/simulator.render.cpp @@ -70,14 +70,18 @@ namespace mrover { descriptor.depthStencil = &depthStencil; - std::array attributes{}; + std::array attributes{}; attributes[0].format = wgpu::VertexFormat::Float32x3; attributes[0].shaderLocation = 0; attributes[1].format = wgpu::VertexFormat::Float32x3; attributes[1].shaderLocation = 1; - attributes[2].format = wgpu::VertexFormat::Float32x2; + attributes[2].format = wgpu::VertexFormat::Float32x3; attributes[2].shaderLocation = 2; - std::array vertexBufferLayout{}; + attributes[3].format = wgpu::VertexFormat::Float32x3; + attributes[3].shaderLocation = 3; + attributes[4].format = wgpu::VertexFormat::Float32x2; + attributes[4].shaderLocation = 4; + std::array vertexBufferLayout{}; vertexBufferLayout[0].arrayStride = sizeof(float) * 3; vertexBufferLayout[0].stepMode = wgpu::VertexStepMode::Vertex; vertexBufferLayout[0].attributeCount = 1; @@ -86,10 +90,18 @@ namespace mrover { vertexBufferLayout[1].stepMode = wgpu::VertexStepMode::Vertex; vertexBufferLayout[1].attributeCount = 1; vertexBufferLayout[1].attributes = attributes.data() + 1; - vertexBufferLayout[2].arrayStride = sizeof(float) * 2; + vertexBufferLayout[2].arrayStride = sizeof(float) * 3; vertexBufferLayout[2].stepMode = wgpu::VertexStepMode::Vertex; vertexBufferLayout[2].attributeCount = 1; vertexBufferLayout[2].attributes = attributes.data() + 2; + vertexBufferLayout[3].arrayStride = sizeof(float) * 3; + vertexBufferLayout[3].stepMode = wgpu::VertexStepMode::Vertex; + vertexBufferLayout[3].attributeCount = 1; + vertexBufferLayout[3].attributes = attributes.data() + 3; + vertexBufferLayout[4].arrayStride = sizeof(float) * 2; + vertexBufferLayout[4].stepMode = wgpu::VertexStepMode::Vertex; + vertexBufferLayout[4].attributeCount = 1; + vertexBufferLayout[4].attributes = attributes.data() + 4; descriptor.vertex.entryPoint = "vs_main"; descriptor.vertex.module = mShaderModule; @@ -130,7 +142,7 @@ namespace mrover { fragment.targets = targets.data(); descriptor.fragment = &fragment; - std::array meshBindGroupLayoutEntries{}; + std::array meshBindGroupLayoutEntries{}; meshBindGroupLayoutEntries[0].binding = 0; meshBindGroupLayoutEntries[0].visibility = wgpu::ShaderStage::Fragment | wgpu::ShaderStage::Vertex; meshBindGroupLayoutEntries[0].buffer.type = wgpu::BufferBindingType::Uniform; @@ -142,6 +154,10 @@ namespace mrover { meshBindGroupLayoutEntries[2].binding = 2; meshBindGroupLayoutEntries[2].visibility = wgpu::ShaderStage::Fragment; meshBindGroupLayoutEntries[2].sampler.type = wgpu::SamplerBindingType::Filtering; + meshBindGroupLayoutEntries[3].binding = 3; + meshBindGroupLayoutEntries[3].visibility = wgpu::ShaderStage::Fragment; + meshBindGroupLayoutEntries[3].texture.sampleType = wgpu::TextureSampleType::Float; + meshBindGroupLayoutEntries[3].texture.viewDimension = wgpu::TextureViewDimension::_2D; wgpu::BindGroupLayoutDescriptor meshBindGroupLayourDescriptor; meshBindGroupLayourDescriptor.entryCount = meshBindGroupLayoutEntries.size(); @@ -295,7 +311,7 @@ namespace mrover { mAdapter.getLimits(&limits); wgpu::RequiredLimits requiredLimits = wgpu::Default; - requiredLimits.limits.maxVertexAttributes = 4; + requiredLimits.limits.maxVertexAttributes = 5; requiredLimits.limits.maxVertexBuffers = 8; requiredLimits.limits.maxBindGroups = 2; requiredLimits.limits.maxUniformBuffersPerShaderStage = 4; @@ -419,10 +435,13 @@ namespace mrover { mesh.indices.enqueueWriteIfUnitialized(mDevice, mQueue, wgpu::BufferUsage::Index); mesh.vertices.enqueueWriteIfUnitialized(mDevice, mQueue, wgpu::BufferUsage::Vertex); mesh.normals.enqueueWriteIfUnitialized(mDevice, mQueue, wgpu::BufferUsage::Vertex); + mesh.tangents.enqueueWriteIfUnitialized(mDevice, mQueue, wgpu::BufferUsage::Vertex); + mesh.bitangents.enqueueWriteIfUnitialized(mDevice, mQueue, wgpu::BufferUsage::Vertex); mesh.uvs.enqueueWriteIfUnitialized(mDevice, mQueue, wgpu::BufferUsage::Vertex); mesh.texture.enqueWriteIfUnitialized(mDevice); + mesh.normal_map.enqueWriteIfUnitialized(mDevice); - std::array bindGroupEntires{}; + std::array bindGroupEntires{}; bindGroupEntires[0].binding = 0; bindGroupEntires[0].buffer = uniforms.buffer; bindGroupEntires[0].size = sizeof(ModelUniforms); @@ -430,6 +449,8 @@ namespace mrover { bindGroupEntires[1].textureView = mesh.texture.view; bindGroupEntires[2].binding = 2; bindGroupEntires[2].sampler = mesh.texture.sampler; + bindGroupEntires[3].binding = 3; + bindGroupEntires[3].textureView = mesh.normal_map.view; wgpu::BindGroupDescriptor descriptor; descriptor.layout = mPbrPipeline.getBindGroupLayout(0); descriptor.entryCount = bindGroupEntires.size(); @@ -439,7 +460,9 @@ namespace mrover { pass.setBindGroup(0, bindGroup, 0, nullptr); pass.setVertexBuffer(0, mesh.vertices.buffer, 0, mesh.vertices.sizeBytes()); pass.setVertexBuffer(1, mesh.normals.buffer, 0, mesh.normals.sizeBytes()); - pass.setVertexBuffer(2, mesh.uvs.buffer, 0, mesh.uvs.sizeBytes()); + pass.setVertexBuffer(2, mesh.tangents.buffer, 0, mesh.tangents.sizeBytes()); + pass.setVertexBuffer(3, mesh.bitangents.buffer, 0, mesh.bitangents.sizeBytes()); + pass.setVertexBuffer(4, mesh.uvs.buffer, 0, mesh.uvs.sizeBytes()); pass.setIndexBuffer(mesh.indices.buffer, wgpu::IndexFormat::Uint32, 0, mesh.indices.sizeBytes()); static_assert(std::is_same_v); diff --git a/urdf/textures/soil_normal.png b/urdf/textures/soil_normal.png new file mode 100644 index 000000000..c74a1b90e --- /dev/null +++ b/urdf/textures/soil_normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93f8fdb399ff4c50137fafd804efb0b2589e5ef7b2fffbd153aa4eeed3763118 +size 2605044 From 61f35207800ff6d236b0ed120f78a9cf8d6386ec Mon Sep 17 00:00:00 2001 From: Neven Johnson Date: Tue, 23 Dec 2025 00:04:29 -0500 Subject: [PATCH 02/14] Switch to Blinn-Phong --- simulator/shaders/shaders.wgsl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simulator/shaders/shaders.wgsl b/simulator/shaders/shaders.wgsl index 1786f82a1..52c414b18 100644 --- a/simulator/shaders/shaders.wgsl +++ b/simulator/shaders/shaders.wgsl @@ -84,8 +84,8 @@ struct OutFragment { // Specular let specularStrength = 0.1; let viewDirInWolrd = normalize(su.cameraInWorld - in.positionInWorld); - let reflectDir = reflect(-lightDirInWorld, mixedNormal); - let spec = pow(max(dot(viewDirInWolrd, reflectDir), 0.0), 32); + let halfwayDir = normalize(lightDirInWorld + viewDirInWolrd); + let spec = pow(max(dot(mixedNormal, halfwayDir), 0.0), 32); let specular = specularStrength * spec * su.lightColor; // Combination out.color = vec4(((ambient + diffuse) * baseColor + specular).rgb, 1); From bb0856c9728f28047feefa15657e22cbb6b456da Mon Sep 17 00:00:00 2001 From: Neven Johnson Date: Tue, 23 Dec 2025 23:18:13 -0500 Subject: [PATCH 03/14] Fix typo --- simulator/shaders/shaders.wgsl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simulator/shaders/shaders.wgsl b/simulator/shaders/shaders.wgsl index 52c414b18..bfbf28f67 100644 --- a/simulator/shaders/shaders.wgsl +++ b/simulator/shaders/shaders.wgsl @@ -83,8 +83,8 @@ struct OutFragment { let diffuse = diff * su.lightColor; // Specular let specularStrength = 0.1; - let viewDirInWolrd = normalize(su.cameraInWorld - in.positionInWorld); - let halfwayDir = normalize(lightDirInWorld + viewDirInWolrd); + let viewDirInWorld = normalize(su.cameraInWorld - in.positionInWorld); + let halfwayDir = normalize(lightDirInWorld + viewDirInWorld); let spec = pow(max(dot(mixedNormal, halfwayDir), 0.0), 32); let specular = specularStrength * spec * su.lightColor; // Combination From 77c5db47b6875f108e318d41cb019b1ed844b83e Mon Sep 17 00:00:00 2001 From: Neven Johnson Date: Sat, 27 Dec 2025 17:34:57 -0500 Subject: [PATCH 04/14] Implement basic PBR shading --- simulator/shaders/shaders.wgsl | 97 +++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 18 deletions(-) diff --git a/simulator/shaders/shaders.wgsl b/simulator/shaders/shaders.wgsl index bfbf28f67..8888762f3 100644 --- a/simulator/shaders/shaders.wgsl +++ b/simulator/shaders/shaders.wgsl @@ -52,9 +52,49 @@ struct OutFragment { @location(0) color: vec4f, @location(1) normalInCamera: vec4f, } +// PBR FS +fn fresnelSchlick(cosTheta: f32, F0: vec3f) -> vec3f { + return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); +} + +fn DistributionGGX(normal: vec3f, halfway: vec3f, roughness: f32) -> f32 { + let a = roughness * roughness; + let a2 = a * a; + let NdotH = max(dot(normal, halfway), 0.0); + let NdotH2 = NdotH * NdotH; + + let num = a2; + let x = (NdotH2 * (a2 - 1.0) + 1.0); + let denom = 3.14159 * x * x; + + return num / denom; +} + +fn GeometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 { + let r = roughness + 1.0; + let k = r * r / 8.0; + + let num = NdotV; + let denom = NdotV * (1.0 - k) + k; + + return num / denom; +} + +fn GeometrySmith(normal: vec3f, viewDir: vec3f, lightDir: vec3f, roughness: f32) -> f32 { + let NdotV = max(dot(normal, viewDir), 0.0); + let NdotL = max(dot(normal, lightDir), 0.0); + let ggx1 = GeometrySchlickGGX(NdotL, roughness); + let ggx2 = GeometrySchlickGGX(NdotV, roughness); + + return ggx1 * ggx2; +} + @fragment fn fs_main(in: OutVertex) -> OutFragment { - let baseColor = textureSample(texture, textureSampler, in.uv); + let metallic = 0.0; + let roughness = 0.93; + let ao = 0.1; + // get the normal vector let normalMapStrength = 1.0; // each component is in the range [0,1] let encodedNormal = textureSample(normalTexture, textureSampler, in.uv).rgb; @@ -69,26 +109,47 @@ struct OutFragment { ); let normalInWorld = normalize(localToWorld * normalInLocal); - let mixedNormal = normalize(mix(in.normalInWorld, vec4(normalInWorld, 0), normalMapStrength)); + let mixedNormal = normalize(mix(in.normalInWorld.xyz, normalInWorld, normalMapStrength)); + + let viewDirInWorld = normalize((su.cameraInWorld - in.positionInWorld).xyz); + let lightDirInWorld = normalize((su.lightInWorld - in.positionInWorld).xyz); + let halfwayDir = normalize((lightDirInWorld + viewDirInWorld).xyz); + + // now PBR stuff + let distanceToLight = length(su.lightInWorld - in.positionInWorld); + // let attenuation = 1 / (distanceToLight * distanceToLight); + let attenuation = 1.0; + let radiance = su.lightColor.rgb * attenuation * 5; + + let albedo = textureSample(texture, textureSampler, in.uv).rgb; + let F0 = mix(vec3(0.04), albedo, metallic); + let F = fresnelSchlick(max(dot(halfwayDir, viewDirInWorld), 0), F0); + + let NDF = DistributionGGX(mixedNormal, halfwayDir, roughness); + let G = GeometrySmith(mixedNormal, viewDirInWorld, lightDirInWorld, roughness); + let numerator = NDF * G * F; + let denominator = 4.0 * max(dot(mixedNormal, viewDirInWorld), 0.0) * max(dot(mixedNormal, lightDirInWorld), 0.0) + 0.0001; + let specular = numerator / denominator; + + let kS = F; + let kD = (vec3(1.0) - kS) * (1.0 - metallic); + + let PI = 3.14159265; + let NdotL = max(dot(mixedNormal, lightDirInWorld), 0.0); + let Lo = (kD * albedo / PI + specular) * radiance * NdotL; + let ambient = 0.03 * albedo * ao; + + var color = ambient + Lo; + // gamma correct + // TODO: why does this make it look bad... + // color = color / (color + vec3(1.0)); + // color = pow(color, vec3(1.0/2.2)); var out : OutFragment; - out.normalInCamera = (su.worldToCamera * mixedNormal + vec4(1, 1, 1, 0)) / 2; + out.normalInCamera = (su.worldToCamera * vec4(mixedNormal, 0) + vec4(1, 1, 1, 0)) / 2; out.normalInCamera.a = 1; - // Ambient - let ambientStrength = 0.3; - let ambient = ambientStrength * su.lightColor; - // Diffuse - let lightDirInWorld = normalize(su.lightInWorld - in.positionInWorld); - let diff = max(dot(mixedNormal, lightDirInWorld), 0.0); - let diffuse = diff * su.lightColor; - // Specular - let specularStrength = 0.1; - let viewDirInWorld = normalize(su.cameraInWorld - in.positionInWorld); - let halfwayDir = normalize(lightDirInWorld + viewDirInWorld); - let spec = pow(max(dot(mixedNormal, halfwayDir), 0.0), 32); - let specular = specularStrength * spec * su.lightColor; - // Combination - out.color = vec4(((ambient + diffuse) * baseColor + specular).rgb, 1); + // TODO: think about alpha?? + out.color = vec4(color, 1.0); return out; } From 4ae9393c8923447f4aa8f4d1fee681d511021cb3 Mon Sep 17 00:00:00 2001 From: Neven Johnson Date: Fri, 2 Jan 2026 12:32:43 -0500 Subject: [PATCH 05/14] Add per-mesh roughness and metallic --- simulator/model.cpp | 8 +++++++- simulator/shaders/shaders.wgsl | 8 ++++++-- simulator/simulator.hpp | 5 +++++ simulator/simulator.render.cpp | 2 ++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/simulator/model.cpp b/simulator/model.cpp index 702d12f3b..ab81cd13c 100644 --- a/simulator/model.cpp +++ b/simulator/model.cpp @@ -34,7 +34,7 @@ namespace mrover { if (!mesh->HasNormals()) throw std::invalid_argument{std::format("Mesh #{} has no normals", meshIndex)}; if (!mesh->HasTextureCoords(0)) throw std::invalid_argument{std::format("Mesh #{} has no texture coordinates", meshIndex)}; - auto& [vertices, normals, tangents, bitangents, uvs, indices, texture, normal_map] = meshes.emplace_back(); + auto& [vertices, normals, tangents, bitangents, uvs, indices, texture, normal_map, roughness, metallic] = meshes.emplace_back(); assert(mesh->HasPositions()); vertices.data.resize(mesh->mNumVertices); @@ -103,6 +103,12 @@ namespace mrover { normal_map.data = cv::Mat{1, 1, CV_8UC4, color}; } + material->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness); + RCLCPP_INFO_STREAM(logger, std::format("\t\troughness: {}", roughness)); + + material->Get(AI_MATKEY_METALLIC_FACTOR, metallic); + RCLCPP_INFO_STREAM(logger, std::format("\t\tmetallic: {}", metallic)); + RCLCPP_INFO_STREAM(logger, std::format("\tLoaded material: {}", name.C_Str())); } diff --git a/simulator/shaders/shaders.wgsl b/simulator/shaders/shaders.wgsl index 8888762f3..79a40ed98 100644 --- a/simulator/shaders/shaders.wgsl +++ b/simulator/shaders/shaders.wgsl @@ -14,6 +14,8 @@ struct SceneUniforms { struct MeshUniforms { modelToWorld: mat4x4f, modelToWorldForNormals: mat4x4f, + roughness: f32, + metallic: f32, } @group(0) @binding(0) var mu: MeshUniforms; @group(0) @binding(1) var texture: texture_2d; @@ -90,8 +92,10 @@ fn GeometrySmith(normal: vec3f, viewDir: vec3f, lightDir: vec3f, roughness: f32) } @fragment fn fs_main(in: OutVertex) -> OutFragment { - let metallic = 0.0; - let roughness = 0.93; + // let metallic = 0.0; + let metallic = mu.metallic; + // let roughness = 0.93; + let roughness = mu.roughness; let ao = 0.1; // get the normal vector diff --git a/simulator/simulator.hpp b/simulator/simulator.hpp index 91d67c767..c4c3a8fae 100644 --- a/simulator/simulator.hpp +++ b/simulator/simulator.hpp @@ -37,6 +37,8 @@ namespace mrover { struct ModelUniforms { Eigen::Matrix4f modelToWorld{}; Eigen::Matrix4f modelToWorldForNormals{}; + float roughness; + float metallic; EIGEN_MAKE_ALIGNED_OPERATOR_NEW }; @@ -69,6 +71,9 @@ namespace mrover { SharedBuffer indices; MeshTexture texture; MeshTexture normal_map; + // could make these also textures, but that might be doing too much + float roughness; + float metallic; }; // DO NOT access the mesh unless you are certain it has been set from the async loader diff --git a/simulator/simulator.render.cpp b/simulator/simulator.render.cpp index 91b97aa0c..f89f163ed 100644 --- a/simulator/simulator.render.cpp +++ b/simulator/simulator.render.cpp @@ -440,6 +440,8 @@ namespace mrover { mesh.uvs.enqueueWriteIfUnitialized(mDevice, mQueue, wgpu::BufferUsage::Vertex); mesh.texture.enqueWriteIfUnitialized(mDevice); mesh.normal_map.enqueWriteIfUnitialized(mDevice); + uniforms.value.roughness = mesh.roughness; + uniforms.value.metallic = mesh.metallic; std::array bindGroupEntires{}; bindGroupEntires[0].binding = 0; From a3c0eaf3922ed4783634b7ee77e565a27533f35a Mon Sep 17 00:00:00 2001 From: Neven Johnson Date: Fri, 2 Jan 2026 12:39:05 -0500 Subject: [PATCH 06/14] Make PBR toggleable --- simulator/shaders/shaders.wgsl | 52 ++++++++++++++++++++++++++++++++- simulator/simulator.gui.cpp | 1 + simulator/simulator.hpp | 2 ++ simulator/simulator.render.cpp | 11 +++++-- simulator/simulator.sensors.cpp | 3 +- 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/simulator/shaders/shaders.wgsl b/simulator/shaders/shaders.wgsl index 79a40ed98..83b30078b 100644 --- a/simulator/shaders/shaders.wgsl +++ b/simulator/shaders/shaders.wgsl @@ -54,6 +54,56 @@ struct OutFragment { @location(0) color: vec4f, @location(1) normalInCamera: vec4f, } +// Blinn-Phong shader +@fragment fn fs_main(in: OutVertex) -> OutFragment { + let baseColor = textureSample(texture, textureSampler, in.uv); + + let normalMapStrength = 1.0; + // each component is in the range [0,1] + let encodedNormal = textureSample(normalTexture, textureSampler, in.uv).rgb; + // shift to get negative values, then normalize because we only care about direction + // this normal is in a frame local to the surface of this face + let normalInLocal = normalize(encodedNormal - 0.5); + + let localToWorld = mat3x3f( + normalize(in.tangentInWorld).xyz, + normalize(in.bitangentInWorld).xyz, + normalize(in.normalInWorld).xyz, + ); + + let normalInWorld = normalize(localToWorld * normalInLocal); + let mixedNormal = normalize(mix(in.normalInWorld, vec4(normalInWorld, 0), normalMapStrength)); + + var out : OutFragment; + out.normalInCamera = (su.worldToCamera * mixedNormal + vec4(1, 1, 1, 0)) / 2; + out.normalInCamera.a = 1; + // Ambient + let ambientStrength = 0.3; + let ambient = ambientStrength * su.lightColor; + // Diffuse + let lightDirInWorld = normalize(su.lightInWorld - in.positionInWorld); + let diff = max(dot(mixedNormal, lightDirInWorld), 0.0); + let diffuse = diff * su.lightColor; + // Specular + let specularStrength = 0.1; + let viewDirInWorld = normalize(su.cameraInWorld - in.positionInWorld); + // classic phong + //let reflectDir = reflect(-lightDirInWorld, mixedNormal); + //let spec = pow(max(dot(viewDirInWorld, reflectDir), 0.0), 32); + // blinn-phong + let halfwayDir = normalize(lightDirInWorld + viewDirInWorld); + let spec = pow(max(dot(mixedNormal, halfwayDir), 0.0), 32); + + let specular = specularStrength * spec * su.lightColor; + // Combination + // out.color = vec4(((ambient + diffuse + specular) * baseColor).rgb, 1); + out.color = vec4(((ambient + diffuse) * baseColor + specular).rgb, 1); + // out.color = vec4(normalInLocal, 1); + // out.color = vec4(normalInWorld, 1); + // out.color = vec4(in.normalInWorld.xyz, 1); + return out; +} + // PBR FS fn fresnelSchlick(cosTheta: f32, F0: vec3f) -> vec3f { return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); @@ -91,7 +141,7 @@ fn GeometrySmith(normal: vec3f, viewDir: vec3f, lightDir: vec3f, roughness: f32) return ggx1 * ggx2; } -@fragment fn fs_main(in: OutVertex) -> OutFragment { +@fragment fn fs_main_pbr(in: OutVertex) -> OutFragment { // let metallic = 0.0; let metallic = mu.metallic; // let roughness = 0.93; diff --git a/simulator/simulator.gui.cpp b/simulator/simulator.gui.cpp index d3fbbde52..283da05ee 100644 --- a/simulator/simulator.gui.cpp +++ b/simulator/simulator.gui.cpp @@ -68,6 +68,7 @@ namespace mrover { ImGui::Checkbox("Enable Physics (P)", &mEnablePhysics); ImGui::Checkbox("Render Models (M)", &mRenderModels); ImGui::Checkbox("Render Wireframe Colliders (C)", &mRenderWireframeColliders); + ImGui::Checkbox("Use PBR Rendering", &mPbrEnabled); ImGui::Text("Camera Locked: %s", mCameraInRoverTarget ? "True" : "False"); ImGui::SliderFloat("Camera Lock Lerp", &mCameraLockSlerp, 0.0f, 1.0f); diff --git a/simulator/simulator.hpp b/simulator/simulator.hpp index c4c3a8fae..f381af590 100644 --- a/simulator/simulator.hpp +++ b/simulator/simulator.hpp @@ -250,6 +250,7 @@ namespace mrover { bool mEnablePhysics{}; bool mRenderModels = true; bool mRenderWireframeColliders = false; + bool mPbrEnabled = false; double mPublishHammerDistanceThreshold = 3; double mPublishBottleDistanceThreshold = 3; float mCameraLockSlerp = 0.02; @@ -321,6 +322,7 @@ namespace mrover { wgpu::TextureView mNormalTextureView; wgpu::ShaderModule mShaderModule; + wgpu::RenderPipeline mBlinnPhongPipeline; wgpu::RenderPipeline mPbrPipeline; wgpu::RenderPipeline mWireframePipeline; diff --git a/simulator/simulator.render.cpp b/simulator/simulator.render.cpp index f89f163ed..e9ac6729a 100644 --- a/simulator/simulator.render.cpp +++ b/simulator/simulator.render.cpp @@ -182,8 +182,13 @@ namespace mrover { pipelineLayoutDescriptor.bindGroupLayouts = reinterpret_cast(bindGroupLayouts.data()); descriptor.layout = mDevice.createPipelineLayout(pipelineLayoutDescriptor); + mBlinnPhongPipeline = mDevice.createRenderPipeline(descriptor); + if (!mBlinnPhongPipeline) throw std::runtime_error("Failed to create WGPU Blinn-Phong pipeline"); + + fragment.entryPoint = "fs_main_pbr"; + mPbrPipeline = mDevice.createRenderPipeline(descriptor); - if (!mPbrPipeline) throw std::runtime_error("Failed to create WGPU render pipeline"); + if (!mPbrPipeline) throw std::runtime_error("Failed to create WGPU PBR pipeline"); // TODO(quintin): This is technically not correct. As far as I can tell getting actual wireframe rendering is pretty difficult in WGPU descriptor.primitive.topology = wgpu::PrimitiveTopology::LineList; @@ -454,6 +459,7 @@ namespace mrover { bindGroupEntires[3].binding = 3; bindGroupEntires[3].textureView = mesh.normal_map.view; wgpu::BindGroupDescriptor descriptor; + // we can just use mPbrPipeline here because both PBR and Blinn-Phone use the same bind groups descriptor.layout = mPbrPipeline.getBindGroupLayout(0); descriptor.entryCount = bindGroupEntires.size(); descriptor.entries = bindGroupEntires.data(); @@ -782,7 +788,7 @@ namespace mrover { depthStencilAttachment.view = mDepthTextureView; wgpu::RenderPassEncoder pass = encoder.beginRenderPass(renderPassDescriptor); - pass.setPipeline(mPbrPipeline); + pass.setPipeline(mPbrEnabled ? mPbrPipeline : mBlinnPhongPipeline); if (!mSceneUniforms.buffer) { mSceneUniforms.init(mDevice); @@ -803,6 +809,7 @@ namespace mrover { entry.buffer = mSceneUniforms.buffer; entry.size = sizeof(SceneUniforms); wgpu::BindGroupDescriptor descriptor; + // we can just use mPbrPipeline here because both PBR and Blinn-Phone use the same bind groups descriptor.layout = mPbrPipeline.getBindGroupLayout(1); descriptor.entryCount = 1; descriptor.entries = &entry; diff --git a/simulator/simulator.sensors.cpp b/simulator/simulator.sensors.cpp index 634532989..fda8e327b 100644 --- a/simulator/simulator.sensors.cpp +++ b/simulator/simulator.sensors.cpp @@ -4,7 +4,7 @@ namespace mrover { auto Simulator::renderCamera(Camera& camera, wgpu::CommandEncoder& encoder, wgpu::RenderPassDescriptor const& passDescriptor) -> void { wgpu::RenderPassEncoder colorPass = encoder.beginRenderPass(passDescriptor); - colorPass.setPipeline(mPbrPipeline); + colorPass.setPipeline(mPbrEnabled ? mPbrPipeline : mBlinnPhongPipeline); if (!camera.sceneUniforms.buffer) camera.sceneUniforms.init(mDevice); camera.sceneUniforms.value.lightColor = {1, 1, 1, 1}; @@ -21,6 +21,7 @@ namespace mrover { entry.buffer = camera.sceneUniforms.buffer; entry.size = sizeof(SceneUniforms); wgpu::BindGroupDescriptor descriptor; + // we can just use mPbrPipeline here because both PBR and Blinn-Phone use the same bind groups descriptor.layout = mPbrPipeline.getBindGroupLayout(1); descriptor.entryCount = 1; descriptor.entries = &entry; From 8d81d25cfaae0a235fc97b856d393a014f366bde Mon Sep 17 00:00:00 2001 From: Neven Johnson Date: Fri, 2 Jan 2026 12:44:10 -0500 Subject: [PATCH 07/14] Tune Blinn-Phong a bit --- simulator/shaders/shaders.wgsl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simulator/shaders/shaders.wgsl b/simulator/shaders/shaders.wgsl index 83b30078b..3cfb48328 100644 --- a/simulator/shaders/shaders.wgsl +++ b/simulator/shaders/shaders.wgsl @@ -78,14 +78,14 @@ struct OutFragment { out.normalInCamera = (su.worldToCamera * mixedNormal + vec4(1, 1, 1, 0)) / 2; out.normalInCamera.a = 1; // Ambient - let ambientStrength = 0.3; + let ambientStrength = 0.2; let ambient = ambientStrength * su.lightColor; // Diffuse let lightDirInWorld = normalize(su.lightInWorld - in.positionInWorld); let diff = max(dot(mixedNormal, lightDirInWorld), 0.0); let diffuse = diff * su.lightColor; // Specular - let specularStrength = 0.1; + let specularStrength = 0.08; let viewDirInWorld = normalize(su.cameraInWorld - in.positionInWorld); // classic phong //let reflectDir = reflect(-lightDirInWorld, mixedNormal); From 070c11e64a272d0deeb2cecd8c27b24056217cdf Mon Sep 17 00:00:00 2001 From: Neven Johnson Date: Fri, 2 Jan 2026 18:49:54 -0500 Subject: [PATCH 08/14] Fix asset importing issues --- simulator/model.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/simulator/model.cpp b/simulator/model.cpp index ab81cd13c..bc437b050 100644 --- a/simulator/model.cpp +++ b/simulator/model.cpp @@ -18,8 +18,9 @@ namespace mrover { Assimp::Importer importer; importer.SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE, aiPrimitiveType_POINT | aiPrimitiveType_LINE); // Drop points and lines - // aiScene const* scene = importer.ReadFile(uri.data(),aiProcessPreset_TargetRealtime_MaxQuality); - aiScene const* scene = importer.ReadFile(uriToPath(uri), aiProcessPreset_TargetRealtime_Fast | aiProcess_FlipUVs); + // we switch to GenSmoothNormals because this seems to fix normals on some meshes + // if you shade flat in blender it shouldn't smooth it + aiScene const* scene = importer.ReadFile(uriToPath(uri), (aiProcessPreset_TargetRealtime_Fast ^ aiProcess_GenNormals) | aiProcess_GenSmoothNormals | aiProcess_FlipUVs); if (!scene) throw std::runtime_error{std::format("Scene import error: {} on path: {}", importer.GetErrorString(), uri)}; RCLCPP_INFO_STREAM(logger, std::format("Loaded scene: {} with mesh count: {}", uri, scene->mNumMeshes)); @@ -106,7 +107,8 @@ namespace mrover { material->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness); RCLCPP_INFO_STREAM(logger, std::format("\t\troughness: {}", roughness)); - material->Get(AI_MATKEY_METALLIC_FACTOR, metallic); + // don't ask me why metallic is this and not AI_MATKEY_METALLIC_FACTOR + material->Get(AI_MATKEY_REFLECTIVITY, metallic); RCLCPP_INFO_STREAM(logger, std::format("\t\tmetallic: {}", metallic)); RCLCPP_INFO_STREAM(logger, std::format("\tLoaded material: {}", name.C_Str())); From 02e15ad7771a40c361617770d7d1070ac82e258a Mon Sep 17 00:00:00 2001 From: Neven Johnson Date: Fri, 2 Jan 2026 18:53:48 -0500 Subject: [PATCH 09/14] Tune PBR slightly --- simulator/shaders/shaders.wgsl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/simulator/shaders/shaders.wgsl b/simulator/shaders/shaders.wgsl index 3cfb48328..af0a70ccc 100644 --- a/simulator/shaders/shaders.wgsl +++ b/simulator/shaders/shaders.wgsl @@ -146,7 +146,8 @@ fn GeometrySmith(normal: vec3f, viewDir: vec3f, lightDir: vec3f, roughness: f32) let metallic = mu.metallic; // let roughness = 0.93; let roughness = mu.roughness; - let ao = 0.1; + // crank up the ambient lighting effect otherwise everything is really dark + let ao = 0.8; // get the normal vector let normalMapStrength = 1.0; @@ -173,7 +174,7 @@ fn GeometrySmith(normal: vec3f, viewDir: vec3f, lightDir: vec3f, roughness: f32) let distanceToLight = length(su.lightInWorld - in.positionInWorld); // let attenuation = 1 / (distanceToLight * distanceToLight); let attenuation = 1.0; - let radiance = su.lightColor.rgb * attenuation * 5; + let radiance = su.lightColor.rgb * attenuation * 4; let albedo = textureSample(texture, textureSampler, in.uv).rgb; let F0 = mix(vec3(0.04), albedo, metallic); @@ -191,7 +192,7 @@ fn GeometrySmith(normal: vec3f, viewDir: vec3f, lightDir: vec3f, roughness: f32) let PI = 3.14159265; let NdotL = max(dot(mixedNormal, lightDirInWorld), 0.0); let Lo = (kD * albedo / PI + specular) * radiance * NdotL; - let ambient = 0.03 * albedo * ao; + let ambient = 0.3 * albedo * ao; var color = ambient + Lo; // gamma correct From f344fd1269c22367931b57c17bcddfb99ac7275f Mon Sep 17 00:00:00 2001 From: Neven Johnson Date: Fri, 2 Jan 2026 19:20:08 -0500 Subject: [PATCH 10/14] Clean up shaders --- simulator/shaders/shaders.wgsl | 7 ------- 1 file changed, 7 deletions(-) diff --git a/simulator/shaders/shaders.wgsl b/simulator/shaders/shaders.wgsl index af0a70ccc..8697d6d8a 100644 --- a/simulator/shaders/shaders.wgsl +++ b/simulator/shaders/shaders.wgsl @@ -87,20 +87,13 @@ struct OutFragment { // Specular let specularStrength = 0.08; let viewDirInWorld = normalize(su.cameraInWorld - in.positionInWorld); - // classic phong - //let reflectDir = reflect(-lightDirInWorld, mixedNormal); - //let spec = pow(max(dot(viewDirInWorld, reflectDir), 0.0), 32); // blinn-phong let halfwayDir = normalize(lightDirInWorld + viewDirInWorld); let spec = pow(max(dot(mixedNormal, halfwayDir), 0.0), 32); let specular = specularStrength * spec * su.lightColor; // Combination - // out.color = vec4(((ambient + diffuse + specular) * baseColor).rgb, 1); out.color = vec4(((ambient + diffuse) * baseColor + specular).rgb, 1); - // out.color = vec4(normalInLocal, 1); - // out.color = vec4(normalInWorld, 1); - // out.color = vec4(in.normalInWorld.xyz, 1); return out; } From 75e049e72d35fc6d0ca874654aa5e2067301c505 Mon Sep 17 00:00:00 2001 From: Neven Johnson Date: Fri, 2 Jan 2026 22:19:19 -0500 Subject: [PATCH 11/14] Add source for PBR shaders --- simulator/shaders/shaders.wgsl | 1 + 1 file changed, 1 insertion(+) diff --git a/simulator/shaders/shaders.wgsl b/simulator/shaders/shaders.wgsl index 8697d6d8a..c8ebb8c8f 100644 --- a/simulator/shaders/shaders.wgsl +++ b/simulator/shaders/shaders.wgsl @@ -98,6 +98,7 @@ struct OutFragment { } // PBR FS +// see https://learnopengl.com/PBR/Lighting fn fresnelSchlick(cosTheta: f32, F0: vec3f) -> vec3f { return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); } From 29e654ad295a9212439af3e02255567aafb3148a Mon Sep 17 00:00:00 2001 From: Neven Johnson Date: Fri, 9 Jan 2026 12:26:52 -0500 Subject: [PATCH 12/14] Make normal mapping toggleable --- simulator/simulator.gui.cpp | 1 + simulator/simulator.hpp | 6 ++++++ simulator/simulator.render.cpp | 7 +++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/simulator/simulator.gui.cpp b/simulator/simulator.gui.cpp index 283da05ee..044f06a8c 100644 --- a/simulator/simulator.gui.cpp +++ b/simulator/simulator.gui.cpp @@ -69,6 +69,7 @@ namespace mrover { ImGui::Checkbox("Render Models (M)", &mRenderModels); ImGui::Checkbox("Render Wireframe Colliders (C)", &mRenderWireframeColliders); ImGui::Checkbox("Use PBR Rendering", &mPbrEnabled); + ImGui::Checkbox("Use Normal Mapping", &mNormalMapEnabled); ImGui::Text("Camera Locked: %s", mCameraInRoverTarget ? "True" : "False"); ImGui::SliderFloat("Camera Lock Lerp", &mCameraLockSlerp, 0.0f, 1.0f); diff --git a/simulator/simulator.hpp b/simulator/simulator.hpp index f381af590..67c4de034 100644 --- a/simulator/simulator.hpp +++ b/simulator/simulator.hpp @@ -251,6 +251,7 @@ namespace mrover { bool mRenderModels = true; bool mRenderWireframeColliders = false; bool mPbrEnabled = false; + bool mNormalMapEnabled = true; double mPublishHammerDistanceThreshold = 3; double mPublishBottleDistanceThreshold = 3; float mCameraLockSlerp = 0.02; @@ -337,6 +338,11 @@ namespace mrover { Eigen::Vector4f mSkyColor{0.05f, 0.8f, 0.92f, 1.0f}; + // 1x1 normal map texture that's just flat + MeshTexture mDefaultNormalMap { + .data = cv::Mat{1, 1, CV_8UC4, {255, 128, 128}} + }; + // Physics std::unique_ptr mCollisionConfig; diff --git a/simulator/simulator.render.cpp b/simulator/simulator.render.cpp index e9ac6729a..047f2b7e1 100644 --- a/simulator/simulator.render.cpp +++ b/simulator/simulator.render.cpp @@ -444,7 +444,10 @@ namespace mrover { mesh.bitangents.enqueueWriteIfUnitialized(mDevice, mQueue, wgpu::BufferUsage::Vertex); mesh.uvs.enqueueWriteIfUnitialized(mDevice, mQueue, wgpu::BufferUsage::Vertex); mesh.texture.enqueWriteIfUnitialized(mDevice); - mesh.normal_map.enqueWriteIfUnitialized(mDevice); + if (mNormalMapEnabled) + mesh.normal_map.enqueWriteIfUnitialized(mDevice); + else + mDefaultNormalMap.enqueWriteIfUnitialized(mDevice); uniforms.value.roughness = mesh.roughness; uniforms.value.metallic = mesh.metallic; @@ -457,7 +460,7 @@ namespace mrover { bindGroupEntires[2].binding = 2; bindGroupEntires[2].sampler = mesh.texture.sampler; bindGroupEntires[3].binding = 3; - bindGroupEntires[3].textureView = mesh.normal_map.view; + bindGroupEntires[3].textureView = mNormalMapEnabled ? mesh.normal_map.view : mDefaultNormalMap.view; wgpu::BindGroupDescriptor descriptor; // we can just use mPbrPipeline here because both PBR and Blinn-Phone use the same bind groups descriptor.layout = mPbrPipeline.getBindGroupLayout(0); From 3eef08fde30726b264d9657e214f4b4b996da31a Mon Sep 17 00:00:00 2001 From: Neven Johnson Date: Fri, 9 Jan 2026 13:50:23 -0500 Subject: [PATCH 13/14] Fix style --- simulator/simulator.hpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/simulator/simulator.hpp b/simulator/simulator.hpp index 67c4de034..fcde11b53 100644 --- a/simulator/simulator.hpp +++ b/simulator/simulator.hpp @@ -339,9 +339,7 @@ namespace mrover { Eigen::Vector4f mSkyColor{0.05f, 0.8f, 0.92f, 1.0f}; // 1x1 normal map texture that's just flat - MeshTexture mDefaultNormalMap { - .data = cv::Mat{1, 1, CV_8UC4, {255, 128, 128}} - }; + MeshTexture mDefaultNormalMap = {.data = cv::Mat{1, 1, CV_8UC4, {255, 128, 128}}}; // Physics From 1fc0c6ab6c72ddef1fd0cf5e266b476665eb2b93 Mon Sep 17 00:00:00 2001 From: Neven Johnson Date: Thu, 23 Apr 2026 16:04:18 -0400 Subject: [PATCH 14/14] Default normal map off, make brighter --- simulator/shaders/shaders.wgsl | 4 ++-- simulator/simulator.hpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/simulator/shaders/shaders.wgsl b/simulator/shaders/shaders.wgsl index c8ebb8c8f..5b83449ec 100644 --- a/simulator/shaders/shaders.wgsl +++ b/simulator/shaders/shaders.wgsl @@ -78,7 +78,7 @@ struct OutFragment { out.normalInCamera = (su.worldToCamera * mixedNormal + vec4(1, 1, 1, 0)) / 2; out.normalInCamera.a = 1; // Ambient - let ambientStrength = 0.2; + let ambientStrength = 0.4; let ambient = ambientStrength * su.lightColor; // Diffuse let lightDirInWorld = normalize(su.lightInWorld - in.positionInWorld); @@ -186,7 +186,7 @@ fn GeometrySmith(normal: vec3f, viewDir: vec3f, lightDir: vec3f, roughness: f32) let PI = 3.14159265; let NdotL = max(dot(mixedNormal, lightDirInWorld), 0.0); let Lo = (kD * albedo / PI + specular) * radiance * NdotL; - let ambient = 0.3 * albedo * ao; + let ambient = 0.4 * albedo * ao; var color = ambient + Lo; // gamma correct diff --git a/simulator/simulator.hpp b/simulator/simulator.hpp index fcde11b53..ca1c5b06b 100644 --- a/simulator/simulator.hpp +++ b/simulator/simulator.hpp @@ -251,7 +251,7 @@ namespace mrover { bool mRenderModels = true; bool mRenderWireframeColliders = false; bool mPbrEnabled = false; - bool mNormalMapEnabled = true; + bool mNormalMapEnabled = false; double mPublishHammerDistanceThreshold = 3; double mPublishBottleDistanceThreshold = 3; float mCameraLockSlerp = 0.02;