Parcourir la source

Added normalmap guided roughness mipmap generator, and a global roughness limiter.

Juan Linietsky il y a 5 ans
Parent
commit
2049dec79e

+ 247 - 29
core/image.cpp

@@ -330,6 +330,17 @@ int Image::get_mipmap_offset(int p_mipmap) const {
 	return ofs;
 }
 
+int Image::get_mipmap_byte_size(int p_mipmap) const {
+
+	ERR_FAIL_INDEX_V(p_mipmap, get_mipmap_count() + 1, -1);
+
+	int ofs, w, h;
+	_get_mipmap_offset_and_size(p_mipmap, ofs, w, h);
+	int ofs2;
+	_get_mipmap_offset_and_size(p_mipmap + 1, ofs2, w, h);
+	return ofs2 - ofs;
+}
+
 void Image::get_mipmap_offset_and_size(int p_mipmap, int &r_ofs, int &r_size) const {
 
 	int ofs, w, h;
@@ -1567,6 +1578,206 @@ Error Image::generate_mipmaps(bool p_renormalize) {
 	return OK;
 }
 
+Error Image::generate_mipmap_roughness(RoughnessChannel p_roughness_channel, const Ref<Image> &p_normal_map) {
+
+	Vector<double> normal_sat_vec; //summed area table
+	double *normal_sat = nullptr; //summed area table for normalmap
+	int normal_w = 0, normal_h = 0;
+
+	ERR_FAIL_COND_V_MSG(p_normal_map.is_null() || p_normal_map->empty(), ERR_INVALID_PARAMETER, "Must provide a valid normalmap for roughness mipmaps");
+
+	Ref<Image> nm = p_normal_map->duplicate();
+	if (nm->is_compressed()) {
+		nm->decompress();
+	}
+
+	normal_w = nm->get_width();
+	normal_h = nm->get_height();
+
+	normal_sat_vec.resize(normal_w * normal_h * 3);
+
+	normal_sat = normal_sat_vec.ptrw();
+
+	//create summed area table
+	nm->lock();
+
+	for (int y = 0; y < normal_h; y++) {
+		double line_sum[3] = { 0, 0, 0 };
+		for (int x = 0; x < normal_w; x++) {
+			double normal[3];
+			Color color = nm->get_pixel(x, y);
+			normal[0] = color.r * 2.0 - 1.0;
+			normal[1] = color.g * 2.0 - 1.0;
+			normal[2] = Math::sqrt(MAX(0.0, 1.0 - (normal[0] * normal[0] + normal[1] * normal[1]))); //reconstruct if missing
+
+			line_sum[0] += normal[0];
+			line_sum[1] += normal[1];
+			line_sum[2] += normal[2];
+
+			uint32_t ofs = (y * normal_w + x) * 3;
+
+			normal_sat[ofs + 0] = line_sum[0];
+			normal_sat[ofs + 1] = line_sum[1];
+			normal_sat[ofs + 2] = line_sum[2];
+
+			if (y > 0) {
+				uint32_t prev_ofs = ((y - 1) * normal_w + x) * 3;
+				normal_sat[ofs + 0] += normal_sat[prev_ofs + 0];
+				normal_sat[ofs + 1] += normal_sat[prev_ofs + 1];
+				normal_sat[ofs + 2] += normal_sat[prev_ofs + 2];
+			}
+		}
+	}
+
+#if 0
+	{
+		Vector3 beg(normal_sat_vec[0], normal_sat_vec[1], normal_sat_vec[2]);
+		Vector3 end(normal_sat_vec[normal_sat_vec.size() - 3], normal_sat_vec[normal_sat_vec.size() - 2], normal_sat_vec[normal_sat_vec.size() - 1]);
+		Vector3 avg = (end - beg) / (normal_w * normal_h);
+		print_line("average: " + avg);
+	}
+#endif
+
+	int mmcount;
+
+	_get_dst_image_size(width, height, format, mmcount);
+
+	lock();
+
+	uint8_t *base_ptr = write_lock.ptr();
+
+	for (int i = 1; i <= mmcount; i++) {
+
+		int ofs, w, h;
+		_get_mipmap_offset_and_size(i, ofs, w, h);
+		uint8_t *ptr = &base_ptr[ofs];
+
+		for (int x = 0; x < w; x++) {
+			for (int y = 0; y < h; y++) {
+				int from_x = x * normal_w / w;
+				int from_y = y * normal_h / h;
+				int to_x = (x + 1) * normal_w / w;
+				int to_y = (y + 1) * normal_h / h;
+				to_x = MIN(to_x - 1, normal_w);
+				to_y = MIN(to_y - 1, normal_h);
+
+				int size_x = (to_x - from_x) + 1;
+				int size_y = (to_y - from_y) + 1;
+
+				//summed area table version (much faster)
+
+				double avg[3] = { 0, 0, 0 };
+
+				if (from_x > 0 && from_y > 0) {
+					uint32_t tofs = ((from_y - 1) * normal_w + (from_x - 1)) * 3;
+					avg[0] += normal_sat[tofs + 0];
+					avg[1] += normal_sat[tofs + 1];
+					avg[2] += normal_sat[tofs + 2];
+				}
+
+				if (from_y > 0) {
+					uint32_t tofs = ((from_y - 1) * normal_w + to_x) * 3;
+					avg[0] -= normal_sat[tofs + 0];
+					avg[1] -= normal_sat[tofs + 1];
+					avg[2] -= normal_sat[tofs + 2];
+				}
+
+				if (from_x > 0) {
+					uint32_t tofs = (to_y * normal_w + (from_x - 1)) * 3;
+					avg[0] -= normal_sat[tofs + 0];
+					avg[1] -= normal_sat[tofs + 1];
+					avg[2] -= normal_sat[tofs + 2];
+				}
+
+				uint32_t tofs = (to_y * normal_w + to_x) * 3;
+				avg[0] += normal_sat[tofs + 0];
+				avg[1] += normal_sat[tofs + 1];
+				avg[2] += normal_sat[tofs + 2];
+
+				double div = double(size_x * size_y);
+				Vector3 vec(avg[0] / div, avg[1] / div, avg[2] / div);
+
+				float r = vec.length();
+
+				int pixel_ofs = y * w + x;
+				Color c = _get_color_at_ofs(ptr, pixel_ofs);
+
+				float roughness;
+
+				switch (p_roughness_channel) {
+					case ROUGHNESS_CHANNEL_R: {
+						roughness = c.r;
+					} break;
+					case ROUGHNESS_CHANNEL_G: {
+						roughness = c.g;
+					} break;
+					case ROUGHNESS_CHANNEL_B: {
+						roughness = c.b;
+					} break;
+					case ROUGHNESS_CHANNEL_L: {
+						roughness = c.gray();
+					} break;
+					case ROUGHNESS_CHANNEL_A: {
+						roughness = c.a;
+					} break;
+				}
+
+				float variance = 0;
+				if (r < 1.0f) {
+					float r2 = r * r;
+					float kappa = (3.0f * r - r * r2) / (1.0f - r2);
+					variance = 0.25f / kappa;
+				}
+
+				float threshold = 0.4;
+				roughness = Math::sqrt(roughness * roughness + MIN(3.0f * variance, threshold * threshold));
+
+				switch (p_roughness_channel) {
+					case ROUGHNESS_CHANNEL_R: {
+						c.r = roughness;
+					} break;
+					case ROUGHNESS_CHANNEL_G: {
+						c.g = roughness;
+					} break;
+					case ROUGHNESS_CHANNEL_B: {
+						c.b = roughness;
+					} break;
+					case ROUGHNESS_CHANNEL_L: {
+						c.r = roughness;
+						c.g = roughness;
+						c.b = roughness;
+					} break;
+					case ROUGHNESS_CHANNEL_A: {
+						c.a = roughness;
+					} break;
+				}
+
+				_set_color_at_ofs(ptr, pixel_ofs, c);
+			}
+		}
+#if 0
+		{
+			int size = get_mipmap_byte_size(i);
+			print_line("size for mimpap " + itos(i) + ": " + itos(size));
+			PoolVector<uint8_t> imgdata;
+			imgdata.resize(size);
+			PoolVector<uint8_t>::Write wr = imgdata.write();
+			copymem(wr.ptr(), ptr, size);
+			wr = PoolVector<uint8_t>::Write();
+			Ref<Image> im;
+			im.instance();
+			im->create(w, h, false, format, imgdata);
+			im->save_png("res://mipmap_" + itos(i) + ".png");
+		}
+#endif
+	}
+
+	unlock();
+	nm->unlock();
+
+	return OK;
+}
+
 void Image::clear_mipmaps() {
 
 	if (!mipmaps)
@@ -2440,18 +2651,7 @@ Color Image::get_pixelv(const Point2 &p_src) const {
 	return get_pixel(p_src.x, p_src.y);
 }
 
-Color Image::get_pixel(int p_x, int p_y) const {
-
-	uint8_t *ptr = write_lock.ptr();
-#ifdef DEBUG_ENABLED
-	ERR_FAIL_COND_V_MSG(!ptr, Color(), "Image must be locked with 'lock()' before using get_pixel().");
-
-	ERR_FAIL_INDEX_V(p_x, width, Color());
-	ERR_FAIL_INDEX_V(p_y, height, Color());
-
-#endif
-
-	uint32_t ofs = p_y * width + p_x;
+Color Image::_get_color_at_ofs(uint8_t *ptr, uint32_t ofs) const {
 
 	switch (format) {
 		case FORMAT_L8: {
@@ -2564,23 +2764,7 @@ Color Image::get_pixel(int p_x, int p_y) const {
 	}
 }
 
-void Image::set_pixelv(const Point2 &p_dst, const Color &p_color) {
-	set_pixel(p_dst.x, p_dst.y, p_color);
-}
-
-void Image::set_pixel(int p_x, int p_y, const Color &p_color) {
-
-	uint8_t *ptr = write_lock.ptr();
-#ifdef DEBUG_ENABLED
-	ERR_FAIL_COND_MSG(!ptr, "Image must be locked with 'lock()' before using set_pixel().");
-
-	ERR_FAIL_INDEX(p_x, width);
-	ERR_FAIL_INDEX(p_y, height);
-
-#endif
-
-	uint32_t ofs = p_y * width + p_x;
-
+void Image::_set_color_at_ofs(uint8_t *ptr, uint32_t ofs, const Color &p_color) {
 	switch (format) {
 		case FORMAT_L8: {
 			ptr[ofs] = uint8_t(CLAMP(p_color.get_v() * 255.0, 0, 255));
@@ -2688,6 +2872,40 @@ void Image::set_pixel(int p_x, int p_y, const Color &p_color) {
 	}
 }
 
+Color Image::get_pixel(int p_x, int p_y) const {
+
+	uint8_t *ptr = write_lock.ptr();
+#ifdef DEBUG_ENABLED
+	ERR_FAIL_COND_V_MSG(!ptr, Color(), "Image must be locked with 'lock()' before using get_pixel().");
+
+	ERR_FAIL_INDEX_V(p_x, width, Color());
+	ERR_FAIL_INDEX_V(p_y, height, Color());
+
+#endif
+
+	uint32_t ofs = p_y * width + p_x;
+	return _get_color_at_ofs(ptr, ofs);
+}
+
+void Image::set_pixelv(const Point2 &p_dst, const Color &p_color) {
+	set_pixel(p_dst.x, p_dst.y, p_color);
+}
+
+void Image::set_pixel(int p_x, int p_y, const Color &p_color) {
+
+	uint8_t *ptr = write_lock.ptr();
+#ifdef DEBUG_ENABLED
+	ERR_FAIL_COND_MSG(!ptr, "Image must be locked with 'lock()' before using set_pixel().");
+
+	ERR_FAIL_INDEX(p_x, width);
+	ERR_FAIL_INDEX(p_y, height);
+
+#endif
+
+	uint32_t ofs = p_y * width + p_x;
+	_set_color_at_ofs(ptr, ofs, p_color);
+}
+
 Image::UsedChannels Image::detect_used_channels(CompressSource p_source) {
 
 	ERR_FAIL_COND_V(data.size() == 0, USED_CHANNELS_RGBA);

+ 15 - 0
core/image.h

@@ -159,6 +159,9 @@ public:
 
 	PoolVector<uint8_t>::Write write_lock;
 
+	_FORCE_INLINE_ Color _get_color_at_ofs(uint8_t *ptr, uint32_t ofs) const;
+	_FORCE_INLINE_ void _set_color_at_ofs(uint8_t *ptr, uint32_t ofs, const Color &p_color);
+
 protected:
 	static void _bind_methods();
 
@@ -223,6 +226,7 @@ public:
 	 */
 	Format get_format() const;
 
+	int get_mipmap_byte_size(int p_mipmap) const; //get where the mipmap begins in data
 	int get_mipmap_offset(int p_mipmap) const; //get where the mipmap begins in data
 	void get_mipmap_offset_and_size(int p_mipmap, int &r_ofs, int &r_size) const; //get where the mipmap begins in data
 	void get_mipmap_offset_size_and_dimensions(int p_mipmap, int &r_ofs, int &r_size, int &w, int &h) const; //get where the mipmap begins in data
@@ -249,6 +253,16 @@ public:
 	 */
 	Error generate_mipmaps(bool p_renormalize = false);
 
+	enum RoughnessChannel {
+		ROUGHNESS_CHANNEL_R,
+		ROUGHNESS_CHANNEL_G,
+		ROUGHNESS_CHANNEL_B,
+		ROUGHNESS_CHANNEL_A,
+		ROUGHNESS_CHANNEL_L,
+	};
+
+	Error generate_mipmap_roughness(RoughnessChannel p_roughness_channel, const Ref<Image> &p_normal_map);
+
 	void clear_mipmaps();
 	void normalize(); //for normal maps
 
@@ -385,5 +399,6 @@ VARIANT_ENUM_CAST(Image::CompressMode)
 VARIANT_ENUM_CAST(Image::CompressSource)
 VARIANT_ENUM_CAST(Image::UsedChannels)
 VARIANT_ENUM_CAST(Image::AlphaMode)
+VARIANT_ENUM_CAST(Image::RoughnessChannel)
 
 #endif

+ 1 - 0
editor/editor_node.cpp

@@ -361,6 +361,7 @@ void EditorNode::_notification(int p_what) {
 				bool dof_jitter = GLOBAL_GET("rendering/quality/filters/depth_of_field_use_jitter");
 				VS::get_singleton()->camera_effects_set_dof_blur_quality(dof_quality, dof_jitter);
 				VS::get_singleton()->environment_set_ssao_quality(VS::EnvironmentSSAOQuality(int(GLOBAL_GET("rendering/quality/ssao/quality"))), GLOBAL_GET("rendering/quality/ssao/half_size"));
+				VS::get_singleton()->screen_space_roughness_limiter_set_active(GLOBAL_GET("rendering/quality/filters/screen_space_roughness_limiter"), GLOBAL_GET("rendering/quality/filters/screen_space_roughness_limiter_curve"));
 			}
 
 			ResourceImporterTexture::get_singleton()->update_imports();

+ 20 - 6
editor/import/resource_importer_texture.cpp

@@ -326,7 +326,7 @@ void ResourceImporterTexture::save_to_stex_format(FileAccess *f, const Ref<Image
 	}
 }
 
-void ResourceImporterTexture::_save_stex(const Ref<Image> &p_image, const String &p_to_path, CompressMode p_compress_mode, float p_lossy_quality, Image::CompressMode p_vram_compression, bool p_mipmaps, bool p_streamable, bool p_detect_3d, bool p_detect_roughness, bool p_force_rgbe, bool p_detect_normal, bool p_force_normal, bool p_srgb_friendly, bool p_force_po2_for_compressed, uint32_t p_limit_mipmap) {
+void ResourceImporterTexture::_save_stex(const Ref<Image> &p_image, const String &p_to_path, CompressMode p_compress_mode, float p_lossy_quality, Image::CompressMode p_vram_compression, bool p_mipmaps, bool p_streamable, bool p_detect_3d, bool p_detect_roughness, bool p_force_rgbe, bool p_detect_normal, bool p_force_normal, bool p_srgb_friendly, bool p_force_po2_for_compressed, uint32_t p_limit_mipmap, const Ref<Image> &p_normal, Image::RoughnessChannel p_roughness_channel) {
 
 	FileAccess *f = FileAccess::open(p_to_path, FileAccess::WRITE);
 	f->store_8('G');
@@ -385,6 +385,10 @@ void ResourceImporterTexture::_save_stex(const Ref<Image> &p_image, const String
 		image->clear_mipmaps();
 	}
 
+	if (image->has_mipmaps() && p_normal.is_valid()) {
+		image->generate_mipmap_roughness(p_roughness_channel, p_normal);
+	}
+
 	if (p_force_rgbe && image->get_format() >= Image::FORMAT_RF && image->get_format() < Image::FORMAT_RGBE9995) {
 		image->convert(Image::FORMAT_RGBE9995);
 	}
@@ -421,7 +425,17 @@ Error ResourceImporterTexture::import(const String &p_source_file, const String
 	bool force_rgbe = int(p_options["compress/hdr_mode"]) == 1;
 	int bptc_ldr = p_options["compress/bptc_ldr"];
 	int roughness = p_options["roughness/mode"];
+	String normal_map = p_options["roughness/src_normal"];
+
+	Ref<Image> normal_image;
+	Image::RoughnessChannel roughness_channel;
 
+	if (mipmaps && roughness > 1 && FileAccess::exists(normal_map)) {
+		normal_image.instance();
+		if (ImageLoader::load_image(normal_map, normal_image) == OK) {
+			roughness_channel = Image::RoughnessChannel(roughness - 2);
+		}
+	}
 	Ref<Image> image;
 	image.instance();
 	Error err = ImageLoader::load_image(p_source_file, image, NULL, hdr_as_srgb, scale);
@@ -516,7 +530,7 @@ Error ResourceImporterTexture::import(const String &p_source_file, const String
 		}
 
 		if (can_bptc || can_s3tc) {
-			_save_stex(image, p_save_path + ".s3tc.stex", compress_mode, lossy, can_bptc ? Image::COMPRESS_BPTC : Image::COMPRESS_S3TC, mipmaps, stream, detect_3d, detect_roughness, force_rgbe, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit);
+			_save_stex(image, p_save_path + ".s3tc.stex", compress_mode, lossy, can_bptc ? Image::COMPRESS_BPTC : Image::COMPRESS_S3TC, mipmaps, stream, detect_3d, detect_roughness, force_rgbe, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit, normal_image, roughness_channel);
 			r_platform_variants->push_back("s3tc");
 			formats_imported.push_back("s3tc");
 			ok_on_pc = true;
@@ -524,20 +538,20 @@ Error ResourceImporterTexture::import(const String &p_source_file, const String
 
 		if (ProjectSettings::get_singleton()->get("rendering/vram_compression/import_etc2")) {
 
-			_save_stex(image, p_save_path + ".etc2.stex", compress_mode, lossy, Image::COMPRESS_ETC2, mipmaps, stream, detect_3d, detect_roughness, force_rgbe, detect_normal, force_normal, srgb_friendly_pack, true, mipmap_limit);
+			_save_stex(image, p_save_path + ".etc2.stex", compress_mode, lossy, Image::COMPRESS_ETC2, mipmaps, stream, detect_3d, detect_roughness, force_rgbe, detect_normal, force_normal, srgb_friendly_pack, true, mipmap_limit, normal_image, roughness_channel);
 			r_platform_variants->push_back("etc2");
 			formats_imported.push_back("etc2");
 		}
 
 		if (ProjectSettings::get_singleton()->get("rendering/vram_compression/import_etc")) {
-			_save_stex(image, p_save_path + ".etc.stex", compress_mode, lossy, Image::COMPRESS_ETC, mipmaps, stream, detect_3d, detect_roughness, force_rgbe, detect_normal, force_normal, srgb_friendly_pack, true, mipmap_limit);
+			_save_stex(image, p_save_path + ".etc.stex", compress_mode, lossy, Image::COMPRESS_ETC, mipmaps, stream, detect_3d, detect_roughness, force_rgbe, detect_normal, force_normal, srgb_friendly_pack, true, mipmap_limit, normal_image, roughness_channel);
 			r_platform_variants->push_back("etc");
 			formats_imported.push_back("etc");
 		}
 
 		if (ProjectSettings::get_singleton()->get("rendering/vram_compression/import_pvrtc")) {
 
-			_save_stex(image, p_save_path + ".pvrtc.stex", compress_mode, lossy, Image::COMPRESS_PVRTC4, mipmaps, stream, detect_3d, detect_roughness, force_rgbe, detect_normal, force_normal, srgb_friendly_pack, true, mipmap_limit);
+			_save_stex(image, p_save_path + ".pvrtc.stex", compress_mode, lossy, Image::COMPRESS_PVRTC4, mipmaps, stream, detect_3d, detect_roughness, force_rgbe, detect_normal, force_normal, srgb_friendly_pack, true, mipmap_limit, normal_image, roughness_channel);
 			r_platform_variants->push_back("pvrtc");
 			formats_imported.push_back("pvrtc");
 		}
@@ -547,7 +561,7 @@ Error ResourceImporterTexture::import(const String &p_source_file, const String
 		}
 	} else {
 		//import normally
-		_save_stex(image, p_save_path + ".stex", compress_mode, lossy, Image::COMPRESS_S3TC /*this is ignored */, mipmaps, stream, detect_3d, detect_roughness, force_rgbe, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit);
+		_save_stex(image, p_save_path + ".stex", compress_mode, lossy, Image::COMPRESS_S3TC /*this is ignored */, mipmaps, stream, detect_3d, detect_roughness, force_rgbe, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit, normal_image, roughness_channel);
 	}
 
 	if (r_metadata) {

+ 1 - 1
editor/import/resource_importer_texture.h

@@ -79,7 +79,7 @@ protected:
 	static ResourceImporterTexture *singleton;
 	static const char *compression_formats[];
 
-	void _save_stex(const Ref<Image> &p_image, const String &p_to_path, CompressMode p_compress_mode, float p_lossy_quality, Image::CompressMode p_vram_compression, bool p_mipmaps, bool p_streamable, bool p_detect_3d, bool p_detect_srgb, bool p_force_rgbe, bool p_detect_normal, bool p_force_normal, bool p_srgb_friendly, bool p_force_po2_for_compressed, uint32_t p_limit_mipmap);
+	void _save_stex(const Ref<Image> &p_image, const String &p_to_path, CompressMode p_compress_mode, float p_lossy_quality, Image::CompressMode p_vram_compression, bool p_mipmaps, bool p_streamable, bool p_detect_3d, bool p_detect_srgb, bool p_force_rgbe, bool p_detect_normal, bool p_force_normal, bool p_srgb_friendly, bool p_force_po2_for_compressed, uint32_t p_limit_mipmap, const Ref<Image> &p_normal, Image::RoughnessChannel p_roughness_channel);
 
 public:
 	void save_to_stex_format(FileAccess *f, const Ref<Image> &p_image, CompressMode p_compress_mode, Image::UsedChannels p_channels, Image::CompressMode p_compress_format, float p_lossy_quality, bool p_force_rgbe);

+ 12 - 2
editor/plugins/spatial_editor_plugin.cpp

@@ -2764,13 +2764,15 @@ void SpatialEditorViewport::_menu_option(int p_option) {
 		case VIEW_DISPLAY_OVERDRAW:
 		case VIEW_DISPLAY_SHADELESS:
 		case VIEW_DISPLAY_LIGHTING:
+		case VIEW_DISPLAY_NORMAL_BUFFER:
 		case VIEW_DISPLAY_DEBUG_SHADOW_ATLAS:
 		case VIEW_DISPLAY_DEBUG_DIRECTIONAL_SHADOW_ATLAS:
 		case VIEW_DISPLAY_DEBUG_GIPROBE_ALBEDO:
 		case VIEW_DISPLAY_DEBUG_GIPROBE_LIGHTING:
 		case VIEW_DISPLAY_DEBUG_GIPROBE_EMISSION:
 		case VIEW_DISPLAY_DEBUG_SCENE_LUMINANCE:
-		case VIEW_DISPLAY_DEBUG_SSAO: {
+		case VIEW_DISPLAY_DEBUG_SSAO:
+		case VIEW_DISPLAY_DEBUG_ROUGHNESS_LIMITER: {
 
 			static const int display_options[] = {
 				VIEW_DISPLAY_NORMAL,
@@ -2778,6 +2780,7 @@ void SpatialEditorViewport::_menu_option(int p_option) {
 				VIEW_DISPLAY_OVERDRAW,
 				VIEW_DISPLAY_SHADELESS,
 				VIEW_DISPLAY_LIGHTING,
+				VIEW_DISPLAY_NORMAL_BUFFER,
 				VIEW_DISPLAY_WIREFRAME,
 				VIEW_DISPLAY_DEBUG_SHADOW_ATLAS,
 				VIEW_DISPLAY_DEBUG_DIRECTIONAL_SHADOW_ATLAS,
@@ -2786,6 +2789,7 @@ void SpatialEditorViewport::_menu_option(int p_option) {
 				VIEW_DISPLAY_DEBUG_GIPROBE_EMISSION,
 				VIEW_DISPLAY_DEBUG_SCENE_LUMINANCE,
 				VIEW_DISPLAY_DEBUG_SSAO,
+				VIEW_DISPLAY_DEBUG_ROUGHNESS_LIMITER,
 				VIEW_MAX
 			};
 			static const Viewport::DebugDraw debug_draw_modes[] = {
@@ -2794,6 +2798,7 @@ void SpatialEditorViewport::_menu_option(int p_option) {
 				Viewport::DEBUG_DRAW_OVERDRAW,
 				Viewport::DEBUG_DRAW_UNSHADED,
 				Viewport::DEBUG_DRAW_LIGHTING,
+				Viewport::DEBUG_DRAW_NORMAL_BUFFER,
 				Viewport::DEBUG_DRAW_WIREFRAME,
 				Viewport::DEBUG_DRAW_SHADOW_ATLAS,
 				Viewport::DEBUG_DRAW_DIRECTIONAL_SHADOW_ATLAS,
@@ -2801,7 +2806,8 @@ void SpatialEditorViewport::_menu_option(int p_option) {
 				Viewport::DEBUG_DRAW_GI_PROBE_LIGHTING,
 				Viewport::DEBUG_DRAW_GI_PROBE_EMISSION,
 				Viewport::DEBUG_DRAW_SCENE_LUMINANCE,
-				Viewport::DEBUG_DRAW_SSAO
+				Viewport::DEBUG_DRAW_SSAO,
+				Viewport::DEBUG_DRAW_ROUGHNESS_LIMITER,
 			};
 
 			int idx = 0;
@@ -3639,6 +3645,8 @@ SpatialEditorViewport::SpatialEditorViewport(SpatialEditor *p_spatial_editor, Ed
 	view_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_lighting", TTR("Display Lighting")), VIEW_DISPLAY_LIGHTING);
 	view_menu->get_popup()->add_radio_check_shortcut(ED_SHORTCUT("spatial_editor/view_display_unshaded", TTR("Display Unshaded")), VIEW_DISPLAY_SHADELESS);
 	view_menu->get_popup()->set_item_checked(view_menu->get_popup()->get_item_index(VIEW_DISPLAY_NORMAL), true);
+	display_submenu->add_radio_check_item(TTR("Normal Buffer"), VIEW_DISPLAY_NORMAL_BUFFER);
+	display_submenu->add_separator();
 	display_submenu->add_radio_check_item(TTR("Shadow Atlas"), VIEW_DISPLAY_DEBUG_SHADOW_ATLAS);
 	display_submenu->add_radio_check_item(TTR("Directional Shadow"), VIEW_DISPLAY_DEBUG_DIRECTIONAL_SHADOW_ATLAS);
 	display_submenu->add_separator();
@@ -3649,6 +3657,8 @@ SpatialEditorViewport::SpatialEditorViewport(SpatialEditor *p_spatial_editor, Ed
 	display_submenu->add_radio_check_item(TTR("Scene Luminance"), VIEW_DISPLAY_DEBUG_SCENE_LUMINANCE);
 	display_submenu->add_separator();
 	display_submenu->add_radio_check_item(TTR("SSAO"), VIEW_DISPLAY_DEBUG_SSAO);
+	display_submenu->add_separator();
+	display_submenu->add_radio_check_item(TTR("Roughness Limiter"), VIEW_DISPLAY_DEBUG_ROUGHNESS_LIMITER);
 	display_submenu->set_name("display_advanced");
 	view_menu->get_popup()->add_submenu_item(TTR("Display Advanced..."), "display_advanced");
 	view_menu->get_popup()->add_separator();

+ 2 - 0
editor/plugins/spatial_editor_plugin.h

@@ -168,6 +168,7 @@ class SpatialEditorViewport : public Control {
 		VIEW_DISPLAY_OVERDRAW,
 		VIEW_DISPLAY_SHADELESS,
 		VIEW_DISPLAY_LIGHTING,
+		VIEW_DISPLAY_NORMAL_BUFFER,
 		VIEW_DISPLAY_DEBUG_SHADOW_ATLAS,
 		VIEW_DISPLAY_DEBUG_DIRECTIONAL_SHADOW_ATLAS,
 		VIEW_DISPLAY_DEBUG_GIPROBE_ALBEDO,
@@ -175,6 +176,7 @@ class SpatialEditorViewport : public Control {
 		VIEW_DISPLAY_DEBUG_GIPROBE_EMISSION,
 		VIEW_DISPLAY_DEBUG_SCENE_LUMINANCE,
 		VIEW_DISPLAY_DEBUG_SSAO,
+		VIEW_DISPLAY_DEBUG_ROUGHNESS_LIMITER,
 		VIEW_LOCK_ROTATION,
 		VIEW_CINEMATIC_PREVIEW,
 		VIEW_MAX

+ 2 - 1
scene/main/viewport.h

@@ -131,6 +131,7 @@ public:
 		DEBUG_DRAW_LIGHTING,
 		DEBUG_DRAW_OVERDRAW,
 		DEBUG_DRAW_WIREFRAME,
+		DEBUG_DRAW_NORMAL_BUFFER,
 		DEBUG_DRAW_GI_PROBE_ALBEDO,
 		DEBUG_DRAW_GI_PROBE_LIGHTING,
 		DEBUG_DRAW_GI_PROBE_EMISSION,
@@ -138,7 +139,7 @@ public:
 		DEBUG_DRAW_DIRECTIONAL_SHADOW_ATLAS,
 		DEBUG_DRAW_SCENE_LUMINANCE,
 		DEBUG_DRAW_SSAO,
-
+		DEBUG_DRAW_ROUGHNESS_LIMITER
 	};
 
 	enum ClearMode {

+ 3 - 0
servers/visual/rasterizer.h

@@ -260,6 +260,9 @@ public:
 	virtual RID render_buffers_create() = 0;
 	virtual void render_buffers_configure(RID p_render_buffers, RID p_render_target, int p_width, int p_height, VS::ViewportMSAA p_msaa) = 0;
 
+	virtual void screen_space_roughness_limiter_set_active(bool p_enable, float p_curve) = 0;
+	virtual bool screen_space_roughness_limiter_is_active() const = 0;
+
 	virtual bool free(RID p_rid) = 0;
 
 	virtual void update() = 0;

+ 56 - 20
servers/visual/rasterizer_rd/rasterizer_effects_rd.cpp

@@ -664,7 +664,7 @@ void RasterizerEffectsRD::generate_ssao(RID p_depth_buffer, RID p_normal_buffer,
 	// Blur horizontal
 
 	ssao.blur_push_constant.edge_sharpness = p_edge_sharpness;
-	ssao.blur_push_constant.filter_scale = p_blur + 1;
+	ssao.blur_push_constant.filter_scale = p_blur;
 	ssao.blur_push_constant.screen_size[0] = ssao.gather_push_constant.screen_size[0];
 	ssao.blur_push_constant.screen_size[1] = ssao.gather_push_constant.screen_size[1];
 	ssao.blur_push_constant.z_far = p_projection.get_z_far();
@@ -673,33 +673,35 @@ void RasterizerEffectsRD::generate_ssao(RID p_depth_buffer, RID p_normal_buffer,
 	ssao.blur_push_constant.axis[0] = 1;
 	ssao.blur_push_constant.axis[1] = 0;
 
-	RD::get_singleton()->compute_list_bind_compute_pipeline(compute_list, ssao.pipelines[p_half_size ? SSAO_BLUR_PASS_HALF : SSAO_BLUR_PASS]);
-	RD::get_singleton()->compute_list_bind_uniform_set(compute_list, _get_compute_uniform_set_from_texture(p_ao1), 0);
-	if (p_half_size) {
-		RD::get_singleton()->compute_list_bind_uniform_set(compute_list, _get_compute_uniform_set_from_texture(p_depth_mipmaps_texture), 1);
-	} else {
-		RD::get_singleton()->compute_list_bind_uniform_set(compute_list, _get_compute_uniform_set_from_texture(p_depth_buffer), 1);
-	}
-	RD::get_singleton()->compute_list_bind_uniform_set(compute_list, _get_uniform_set_from_image(p_ao2), 3);
+	if (p_blur != VS::ENV_SSAO_BLUR_DISABLED) {
 
-	RD::get_singleton()->compute_list_set_push_constant(compute_list, &ssao.blur_push_constant, sizeof(SSAOBlurPushConstant));
+		RD::get_singleton()->compute_list_bind_compute_pipeline(compute_list, ssao.pipelines[p_half_size ? SSAO_BLUR_PASS_HALF : SSAO_BLUR_PASS]);
+		RD::get_singleton()->compute_list_bind_uniform_set(compute_list, _get_compute_uniform_set_from_texture(p_ao1), 0);
+		if (p_half_size) {
+			RD::get_singleton()->compute_list_bind_uniform_set(compute_list, _get_compute_uniform_set_from_texture(p_depth_mipmaps_texture), 1);
+		} else {
+			RD::get_singleton()->compute_list_bind_uniform_set(compute_list, _get_compute_uniform_set_from_texture(p_depth_buffer), 1);
+		}
+		RD::get_singleton()->compute_list_bind_uniform_set(compute_list, _get_uniform_set_from_image(p_ao2), 3);
 
-	RD::get_singleton()->compute_list_dispatch(compute_list, x_groups, y_groups, 1);
-	RD::get_singleton()->compute_list_add_barrier(compute_list);
+		RD::get_singleton()->compute_list_set_push_constant(compute_list, &ssao.blur_push_constant, sizeof(SSAOBlurPushConstant));
 
-	/* THIRD PASS */
-	// Blur vertical
+		RD::get_singleton()->compute_list_dispatch(compute_list, x_groups, y_groups, 1);
+		RD::get_singleton()->compute_list_add_barrier(compute_list);
 
-	ssao.blur_push_constant.axis[0] = 0;
-	ssao.blur_push_constant.axis[1] = 1;
+		/* THIRD PASS */
+		// Blur vertical
 
-	RD::get_singleton()->compute_list_bind_uniform_set(compute_list, _get_compute_uniform_set_from_texture(p_ao2), 0);
-	RD::get_singleton()->compute_list_bind_uniform_set(compute_list, _get_uniform_set_from_image(p_ao1), 3);
+		ssao.blur_push_constant.axis[0] = 0;
+		ssao.blur_push_constant.axis[1] = 1;
 
-	RD::get_singleton()->compute_list_set_push_constant(compute_list, &ssao.blur_push_constant, sizeof(SSAOBlurPushConstant));
+		RD::get_singleton()->compute_list_bind_uniform_set(compute_list, _get_compute_uniform_set_from_texture(p_ao2), 0);
+		RD::get_singleton()->compute_list_bind_uniform_set(compute_list, _get_uniform_set_from_image(p_ao1), 3);
 
-	RD::get_singleton()->compute_list_dispatch(compute_list, x_groups, y_groups, 1);
+		RD::get_singleton()->compute_list_set_push_constant(compute_list, &ssao.blur_push_constant, sizeof(SSAOBlurPushConstant));
 
+		RD::get_singleton()->compute_list_dispatch(compute_list, x_groups, y_groups, 1);
+	}
 	if (p_half_size) { //must upscale
 
 		/* FOURTH PASS */
@@ -727,6 +729,27 @@ void RasterizerEffectsRD::generate_ssao(RID p_depth_buffer, RID p_normal_buffer,
 	RD::get_singleton()->compute_list_end();
 }
 
+void RasterizerEffectsRD::roughness_limit(RID p_source_normal, RID p_roughness, const Size2i &p_size, float p_curve) {
+
+	roughness_limiter.push_constant.screen_size[0] = p_size.x;
+	roughness_limiter.push_constant.screen_size[1] = p_size.y;
+	roughness_limiter.push_constant.curve = p_curve;
+
+	RD::ComputeListID compute_list = RD::get_singleton()->compute_list_begin();
+	RD::get_singleton()->compute_list_bind_compute_pipeline(compute_list, roughness_limiter.pipeline);
+	RD::get_singleton()->compute_list_bind_uniform_set(compute_list, _get_compute_uniform_set_from_texture(p_source_normal), 0);
+	RD::get_singleton()->compute_list_bind_uniform_set(compute_list, _get_uniform_set_from_image(p_roughness), 1);
+
+	int x_groups = (p_size.x - 1) / 8 + 1;
+	int y_groups = (p_size.y - 1) / 8 + 1;
+
+	RD::get_singleton()->compute_list_set_push_constant(compute_list, &roughness_limiter.push_constant, sizeof(RoughnessLimiterPushConstant)); //not used but set anyway
+
+	RD::get_singleton()->compute_list_dispatch(compute_list, x_groups, y_groups, 1);
+
+	RD::get_singleton()->compute_list_end();
+}
+
 RasterizerEffectsRD::RasterizerEffectsRD() {
 
 	{
@@ -905,6 +928,19 @@ RasterizerEffectsRD::RasterizerEffectsRD() {
 
 		ERR_FAIL_COND(pipeline != SSAO_MAX);
 	}
+
+	{
+		// Initialize copier
+		Vector<String> shader_modes;
+		shader_modes.push_back("");
+
+		roughness_limiter.shader.initialize(shader_modes);
+
+		roughness_limiter.shader_version = roughness_limiter.shader.version_create();
+
+		roughness_limiter.pipeline = RD::get_singleton()->compute_pipeline_create(roughness_limiter.shader.version_get_shader(roughness_limiter.shader_version, 0));
+	}
+
 	RD::SamplerState sampler;
 	sampler.mag_filter = RD::SAMPLER_FILTER_LINEAR;
 	sampler.min_filter = RD::SAMPLER_FILTER_LINEAR;

+ 18 - 0
servers/visual/rasterizer_rd/rasterizer_effects_rd.h

@@ -38,6 +38,7 @@
 #include "servers/visual/rasterizer_rd/shaders/copy.glsl.gen.h"
 #include "servers/visual/rasterizer_rd/shaders/cubemap_roughness.glsl.gen.h"
 #include "servers/visual/rasterizer_rd/shaders/luminance_reduce.glsl.gen.h"
+#include "servers/visual/rasterizer_rd/shaders/roughness_limiter.glsl.gen.h"
 #include "servers/visual/rasterizer_rd/shaders/sky.glsl.gen.h"
 #include "servers/visual/rasterizer_rd/shaders/ssao.glsl.gen.h"
 #include "servers/visual/rasterizer_rd/shaders/ssao_blur.glsl.gen.h"
@@ -339,6 +340,21 @@ class RasterizerEffectsRD {
 		RID pipelines[SSAO_MAX];
 	} ssao;
 
+	struct RoughnessLimiterPushConstant {
+		int32_t screen_size[2];
+		float curve;
+		uint32_t pad;
+	};
+
+	struct RoughnessLimiter {
+
+		RoughnessLimiterPushConstant push_constant;
+		RoughnessLimiterShaderRD shader;
+		RID shader_version;
+		RID pipeline;
+
+	} roughness_limiter;
+
 	RID default_sampler;
 	RID default_mipmap_sampler;
 	RID index_buffer;
@@ -407,6 +423,8 @@ public:
 
 	void generate_ssao(RID p_depth_buffer, RID p_normal_buffer, const Size2i &p_depth_buffer_size, RID p_depth_mipmaps_texture, const Vector<RID> &depth_mipmaps, RID p_ao1, bool p_half_size, RID p_ao2, RID p_upscale_buffer, float p_intensity, float p_radius, float p_bias, const CameraMatrix &p_projection, VS::EnvironmentSSAOQuality p_quality, VS::EnvironmentSSAOBlur p_blur, float p_edge_sharpness);
 
+	void roughness_limit(RID p_source_normal, RID p_roughness, const Size2i &p_size, float p_curve);
+
 	RasterizerEffectsRD();
 	~RasterizerEffectsRD();
 };

+ 38 - 6
servers/visual/rasterizer_rd/rasterizer_scene_high_end_rd.cpp

@@ -260,6 +260,8 @@ void RasterizerSceneHighEndRD::ShaderData::set_code(const String &p_code) {
 	blend_state_blend.attachments.push_back(blend_attachment);
 	RD::PipelineColorBlendState blend_state_opaque = RD::PipelineColorBlendState::create_disabled(1);
 	RD::PipelineColorBlendState blend_state_opaque_specular = RD::PipelineColorBlendState::create_disabled(2);
+	RD::PipelineColorBlendState blend_state_depth_normal = RD::PipelineColorBlendState::create_disabled(1);
+	RD::PipelineColorBlendState blend_state_depth_normal_roughness = RD::PipelineColorBlendState::create_disabled(2);
 
 	//update pipelines
 
@@ -326,8 +328,10 @@ void RasterizerSceneHighEndRD::ShaderData::set_code(const String &p_code) {
 						blend_state = blend_state_opaque;
 					} else if (k == SHADER_VERSION_DEPTH_PASS || k == SHADER_VERSION_DEPTH_PASS_DP) {
 						//none, leave empty
-					} else if (k == SHADER_VERSION_DEPTH_PASS_WITH_NORMAL || k == SHADER_VERSION_DEPTH_PASS_WITH_NORMAL_AND_ROUGHNESS) {
-						blend_state = blend_state_opaque; //writes to normal and roughness in opaque way
+					} else if (k == SHADER_VERSION_DEPTH_PASS_WITH_NORMAL) {
+						blend_state = blend_state_depth_normal;
+					} else if (k == SHADER_VERSION_DEPTH_PASS_WITH_NORMAL_AND_ROUGHNESS) {
+						blend_state = blend_state_depth_normal;
 					} else if (k == SHADER_VERSION_DEPTH_PASS_WITH_MATERIAL) {
 						blend_state = RD::PipelineColorBlendState::create_disabled(5); //writes to normal and roughness in opaque way
 
@@ -941,7 +945,7 @@ void RasterizerSceneHighEndRD::_render_list(RenderingDevice::DrawListID p_draw_l
 	}
 }
 
-void RasterizerSceneHighEndRD::_setup_environment(RID p_environment, const CameraMatrix &p_cam_projection, const Transform &p_cam_transform, RID p_reflection_probe, bool p_no_fog, const Size2 &p_screen_pixel_size, RID p_shadow_atlas, bool p_flip_y, const Color &p_default_bg_color, float p_znear, float p_zfar) {
+void RasterizerSceneHighEndRD::_setup_environment(RID p_environment, const CameraMatrix &p_cam_projection, const Transform &p_cam_transform, RID p_reflection_probe, bool p_no_fog, const Size2 &p_screen_pixel_size, RID p_shadow_atlas, bool p_flip_y, const Color &p_default_bg_color, float p_znear, float p_zfar, bool p_opaque_render_buffers) {
 
 	//CameraMatrix projection = p_cam_projection;
 	//projection.flip_y(); // Vulkan and modern APIs use Y-Down
@@ -1031,7 +1035,7 @@ void RasterizerSceneHighEndRD::_setup_environment(RID p_environment, const Camer
 			scene_state.ubo.use_reflection_cubemap = false;
 		}
 
-		scene_state.ubo.ssao_enabled = environment_is_ssao_enabled(p_environment);
+		scene_state.ubo.ssao_enabled = p_opaque_render_buffers && environment_is_ssao_enabled(p_environment);
 		scene_state.ubo.ssao_ao_affect = environment_get_ssao_ao_affect(p_environment);
 		scene_state.ubo.ssao_light_affect = environment_get_ssao_light_affect(p_environment);
 
@@ -1059,6 +1063,8 @@ void RasterizerSceneHighEndRD::_setup_environment(RID p_environment, const Camer
 		scene_state.ubo.use_reflection_cubemap = false;
 	}
 
+	scene_state.ubo.roughness_limiter_enabled = p_opaque_render_buffers && screen_space_roughness_limiter_is_active();
+
 	RD::get_singleton()->buffer_update(scene_state.uniform_buffer, 0, sizeof(SceneState::UBO), &scene_state.ubo, true);
 }
 
@@ -1723,9 +1729,15 @@ void RasterizerSceneHighEndRD::_render_scene(RID p_render_buffer, const Transfor
 		screen_pixel_size.height = 1.0 / render_buffer->height;
 
 		opaque_framebuffer = render_buffer->color_fb;
+
 		if (p_environment.is_valid() && environment_is_ssr_enabled(p_environment)) {
 			depth_pass_mode = PASS_MODE_DEPTH_NORMAL_ROUGHNESS;
-		} else if (p_environment.is_valid() && environment_is_ssao_enabled(p_environment)) {
+		} else if (screen_space_roughness_limiter_is_active()) {
+			depth_pass_mode = PASS_MODE_DEPTH_NORMAL;
+			//we need to allocate both these, if not allocated
+			_allocate_normal_texture(render_buffer);
+			_allocate_roughness_texture(render_buffer);
+		} else if (p_environment.is_valid() && (environment_is_ssao_enabled(p_environment) || get_debug_draw_mode() == VS::VIEWPORT_DEBUG_DRAW_NORMAL_BUFFER)) {
 			depth_pass_mode = PASS_MODE_DEPTH_NORMAL;
 		}
 
@@ -1773,7 +1785,7 @@ void RasterizerSceneHighEndRD::_render_scene(RID p_render_buffer, const Transfor
 	_setup_lights(p_light_cull_result, p_light_cull_count, p_cam_transform.affine_inverse(), p_shadow_atlas, using_shadows);
 	_setup_reflections(p_reflection_probe_cull_result, p_reflection_probe_cull_count, p_cam_transform.affine_inverse(), p_environment);
 	_setup_gi_probes(p_gi_probe_cull_result, p_gi_probe_cull_count, p_cam_transform);
-	_setup_environment(p_environment, p_cam_projection, p_cam_transform, p_reflection_probe, p_reflection_probe.is_valid(), screen_pixel_size, p_shadow_atlas, !p_reflection_probe.is_valid(), p_default_bg_color, p_cam_projection.get_z_near(), p_cam_projection.get_z_far());
+	_setup_environment(p_environment, p_cam_projection, p_cam_transform, p_reflection_probe, p_reflection_probe.is_valid(), screen_pixel_size, p_shadow_atlas, !p_reflection_probe.is_valid(), p_default_bg_color, p_cam_projection.get_z_near(), p_cam_projection.get_z_far(), false);
 
 	cluster_builder.bake_cluster(); //bake to cluster
 
@@ -1855,12 +1867,18 @@ void RasterizerSceneHighEndRD::_render_scene(RID p_render_buffer, const Transfor
 		_process_ssao(p_render_buffer, p_environment, render_buffer->normal_buffer, p_cam_projection);
 	}
 
+	if (p_render_buffer.is_valid() && screen_space_roughness_limiter_is_active()) {
+		storage->get_effects()->roughness_limit(render_buffer->normal_buffer, render_buffer->roughness_buffer, Size2(render_buffer->width, render_buffer->height), screen_space_roughness_limiter_get_curve());
+	}
+
 	if (p_render_buffer.is_valid()) {
 		//update the render buffers uniform set in case it changed
 		_update_render_buffers_uniform_set(p_render_buffer);
 		render_buffers_uniform_set = render_buffer->uniform_set;
 	}
 
+	_setup_environment(p_environment, p_cam_projection, p_cam_transform, p_reflection_probe, p_reflection_probe.is_valid(), screen_pixel_size, p_shadow_atlas, !p_reflection_probe.is_valid(), p_default_bg_color, p_cam_projection.get_z_near(), p_cam_projection.get_z_far(), p_render_buffer.is_valid());
+
 	RENDER_TIMESTAMP("Render Opaque Pass");
 
 	{
@@ -1901,6 +1919,8 @@ void RasterizerSceneHighEndRD::_render_scene(RID p_render_buffer, const Transfor
 
 	RENDER_TIMESTAMP("Render Transparent Pass");
 
+	_setup_environment(p_environment, p_cam_projection, p_cam_transform, p_reflection_probe, p_reflection_probe.is_valid(), screen_pixel_size, p_shadow_atlas, !p_reflection_probe.is_valid(), p_default_bg_color, p_cam_projection.get_z_near(), p_cam_projection.get_z_far(), false);
+
 	render_list.sort_by_reverse_depth_and_priority(true);
 
 	_fill_instances(&render_list.elements[render_list.max_elements - render_list.alpha_element_count], render_list.alpha_element_count, false);
@@ -2275,6 +2295,18 @@ void RasterizerSceneHighEndRD::_render_buffers_uniform_set_changed(RID p_render_
 	_render_buffers_clear_uniform_set(rb);
 }
 
+RID RasterizerSceneHighEndRD::_render_buffers_get_roughness_texture(RID p_render_buffers) {
+	RenderBufferDataHighEnd *rb = (RenderBufferDataHighEnd *)render_buffers_get_data(p_render_buffers);
+
+	return rb->roughness_buffer;
+}
+
+RID RasterizerSceneHighEndRD::_render_buffers_get_normal_texture(RID p_render_buffers) {
+	RenderBufferDataHighEnd *rb = (RenderBufferDataHighEnd *)render_buffers_get_data(p_render_buffers);
+
+	return rb->normal_buffer;
+}
+
 void RasterizerSceneHighEndRD::_update_render_buffers_uniform_set(RID p_render_buffers) {
 
 	RenderBufferDataHighEnd *rb = (RenderBufferDataHighEnd *)render_buffers_get_data(p_render_buffers);

+ 4 - 2
servers/visual/rasterizer_rd/rasterizer_scene_high_end_rd.h

@@ -229,6 +229,8 @@ class RasterizerSceneHighEndRD : public RasterizerSceneRD {
 	virtual void _base_uniforms_changed();
 	void _render_buffers_clear_uniform_set(RenderBufferDataHighEnd *rb);
 	virtual void _render_buffers_uniform_set_changed(RID p_render_buffers);
+	virtual RID _render_buffers_get_roughness_texture(RID p_render_buffers);
+	virtual RID _render_buffers_get_normal_texture(RID p_render_buffers);
 
 	void _update_render_base_uniform_set();
 	void _setup_view_dependant_uniform_set(RID p_shadow_atlas, RID p_reflection_atlas);
@@ -347,7 +349,7 @@ class RasterizerSceneHighEndRD : public RasterizerSceneRD {
 			uint32_t ssao_enabled;
 			float ssao_light_affect;
 			float ssao_ao_affect;
-			uint32_t pad_ssao;
+			uint32_t roughness_limiter_enabled;
 
 			float ao_color[4];
 		};
@@ -555,7 +557,7 @@ class RasterizerSceneHighEndRD : public RasterizerSceneRD {
 		PASS_MODE_DEPTH_MATERIAL,
 	};
 
-	void _setup_environment(RID p_environment, const CameraMatrix &p_cam_projection, const Transform &p_cam_transform, RID p_reflection_probe, bool p_no_fog, const Size2 &p_screen_pixel_size, RID p_shadow_atlas, bool p_flip_y, const Color &p_default_bg_color, float p_znear, float p_zfar);
+	void _setup_environment(RID p_environment, const CameraMatrix &p_cam_projection, const Transform &p_cam_transform, RID p_reflection_probe, bool p_no_fog, const Size2 &p_screen_pixel_size, RID p_shadow_atlas, bool p_flip_y, const Color &p_default_bg_color, float p_znear, float p_zfar, bool p_opaque_render_buffers = false);
 	void _setup_lights(RID *p_light_cull_result, int p_light_cull_count, const Transform &p_camera_inverse_transform, RID p_shadow_atlas, bool p_using_shadows);
 	void _setup_reflections(RID *p_reflection_probe_cull_result, int p_reflection_probe_cull_count, const Transform &p_camera_inverse_transform, RID p_environment);
 	void _setup_gi_probes(RID *p_gi_probe_probe_cull_result, int p_gi_probe_probe_cull_count, const Transform &p_camera_transform);

+ 25 - 0
servers/visual/rasterizer_rd/rasterizer_scene_rd.cpp

@@ -2640,6 +2640,16 @@ void RasterizerSceneRD::_render_buffers_debug_draw(RID p_render_buffers, RID p_s
 		RID ao_buf = rb->ssao.ao_full.is_valid() ? rb->ssao.ao_full : rb->ssao.ao[0];
 		effects->copy_to_rect(ao_buf, storage->render_target_get_rd_framebuffer(rb->render_target), Rect2(Vector2(), rtsize), false, true);
 	}
+
+	if (debug_draw == VS::VIEWPORT_DEBUG_DRAW_ROUGHNESS_LIMITER && _render_buffers_get_roughness_texture(p_render_buffers).is_valid()) {
+		Size2 rtsize = storage->render_target_get_size(rb->render_target);
+		effects->copy_to_rect(_render_buffers_get_roughness_texture(p_render_buffers), storage->render_target_get_rd_framebuffer(rb->render_target), Rect2(Vector2(), rtsize), false, true);
+	}
+
+	if (debug_draw == VS::VIEWPORT_DEBUG_DRAW_NORMAL_BUFFER && _render_buffers_get_normal_texture(p_render_buffers).is_valid()) {
+		Size2 rtsize = storage->render_target_get_size(rb->render_target);
+		effects->copy_to_rect(_render_buffers_get_normal_texture(p_render_buffers), storage->render_target_get_rd_framebuffer(rb->render_target), Rect2(Vector2(), rtsize));
+	}
 }
 
 RID RasterizerSceneRD::render_buffers_get_back_buffer_texture(RID p_render_buffers) {
@@ -2999,6 +3009,19 @@ void RasterizerSceneRD::set_time(double p_time, double p_step) {
 	time_step = p_step;
 }
 
+void RasterizerSceneRD::screen_space_roughness_limiter_set_active(bool p_enable, float p_curve) {
+	screen_space_roughness_limiter = p_enable;
+	screen_space_roughness_limiter_curve = p_curve;
+}
+
+bool RasterizerSceneRD::screen_space_roughness_limiter_is_active() const {
+	return screen_space_roughness_limiter;
+}
+
+float RasterizerSceneRD::screen_space_roughness_limiter_get_curve() const {
+	return screen_space_roughness_limiter_curve;
+}
+
 RasterizerSceneRD::RasterizerSceneRD(RasterizerStorageRD *p_storage) {
 	storage = p_storage;
 
@@ -3096,6 +3119,8 @@ RasterizerSceneRD::RasterizerSceneRD(RasterizerStorageRD *p_storage) {
 	camera_effects_set_dof_blur_bokeh_shape(VS::DOFBokehShape(int(GLOBAL_GET("rendering/quality/filters/depth_of_field_bokeh_shape"))));
 	camera_effects_set_dof_blur_quality(VS::DOFBlurQuality(int(GLOBAL_GET("rendering/quality/filters/depth_of_field_bokeh_quality"))), GLOBAL_GET("rendering/quality/filters/depth_of_field_use_jitter"));
 	environment_set_ssao_quality(VS::EnvironmentSSAOQuality(int(GLOBAL_GET("rendering/quality/ssao/quality"))), GLOBAL_GET("rendering/quality/ssao/half_size"));
+	screen_space_roughness_limiter = GLOBAL_GET("rendering/quality/filters/screen_space_roughness_limiter");
+	screen_space_roughness_limiter_curve = GLOBAL_GET("rendering/quality/filters/screen_space_roughness_limiter_curve");
 }
 
 RasterizerSceneRD::~RasterizerSceneRD() {

+ 9 - 0
servers/visual/rasterizer_rd/rasterizer_scene_rd.h

@@ -64,6 +64,8 @@ protected:
 
 	virtual void _base_uniforms_changed() = 0;
 	virtual void _render_buffers_uniform_set_changed(RID p_render_buffers) = 0;
+	virtual RID _render_buffers_get_roughness_texture(RID p_render_buffers) = 0;
+	virtual RID _render_buffers_get_normal_texture(RID p_render_buffers) = 0;
 
 	void _process_ssao(RID p_render_buffers, RID p_environment, RID p_normal_buffer, const CameraMatrix &p_projection);
 
@@ -575,6 +577,9 @@ private:
 		} ssao;
 	};
 
+	bool screen_space_roughness_limiter = false;
+	float screen_space_roughness_limiter_curve = 1.0;
+
 	mutable RID_Owner<RenderBuffers> render_buffers_owner;
 
 	void _free_render_buffer_data(RenderBuffers *rb);
@@ -923,6 +928,10 @@ public:
 	virtual void set_scene_pass(uint64_t p_pass) { scene_pass = p_pass; }
 	_FORCE_INLINE_ uint64_t get_scene_pass() { return scene_pass; }
 
+	virtual void screen_space_roughness_limiter_set_active(bool p_enable, float p_curve);
+	virtual bool screen_space_roughness_limiter_is_active() const;
+	virtual float screen_space_roughness_limiter_get_curve() const;
+
 	int get_roughness_layers() const;
 	bool is_using_radiance_cubemap_array() const;
 

+ 1 - 0
servers/visual/rasterizer_rd/shaders/SCsub

@@ -19,3 +19,4 @@ if 'RD_GLSL' in env['BUILDERS']:
     env.RD_GLSL('ssao.glsl');
     env.RD_GLSL('ssao_minify.glsl');
     env.RD_GLSL('ssao_blur.glsl');
+    env.RD_GLSL('roughness_limiter.glsl');

+ 73 - 0
servers/visual/rasterizer_rd/shaders/roughness_limiter.glsl

@@ -0,0 +1,73 @@
+/* clang-format off */
+[compute]
+/* clang-format on */
+
+#version 450
+
+VERSION_DEFINES
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+
+layout(set = 0, binding = 0) uniform sampler2D source_normal;
+layout(r8, set = 1, binding = 0) uniform restrict writeonly image2D dest_roughness;
+
+layout(push_constant, binding = 1, std430) uniform Params {
+	ivec2 screen_size;
+	float curve;
+	uint pad;
+} params;
+
+#define HALF_PI 1.5707963267948966
+
+void main() {
+
+	// Pixel being shaded
+	ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
+	if (any(greaterThan(pos,params.screen_size))) { //too large, do nothing
+		return;
+	}
+
+	vec3 normal_accum = vec3(0.0);
+	float accum = 0.0;
+	for(int i=0;i<=1;i++) {
+		for(int j=0;j<=1;j++) {
+			normal_accum += normalize(texelFetch(source_normal,pos+ivec2(i,j),0).xyz * 2.0 - 1.0);
+			accum+=1.0;
+		}
+	}
+
+	normal_accum /= accum;
+
+	float r = length(normal_accum);
+
+	float limit;
+
+	if (r < 1.0) {
+		float threshold = 0.4;
+
+/*
+		//Formula from Filament, does not make sense to me.
+
+		float r2 = r * r;
+		float kappa = (3.0f * r - r * r2) / (1.0f - r2);
+		float variance = 0.25f / kappa;
+		limit = sqrt(min(2.0f * variance, threshold * threshold));
+//*/
+/*
+		//Formula based on probability distribution graph
+
+		float width = acos(max(0.0,r)); // convert to angle (width)
+		float roughness = pow(width,1.7)*0.854492; //approximate (crappy) formula to convert to roughness
+		limit = min(sqrt(roughness), threshold); //convert to perceptual roughness and apply threshold
+//*/
+
+		limit = min(sqrt(pow(acos(max(0.0,r)) / HALF_PI ,params.curve)), threshold); //convert to perceptual roughness and apply threshold
+
+		//limit = 0.5;
+	} else {
+		limit = 0.0;
+	}
+
+	imageStore(dest_roughness,pos,vec4(limit));
+}

+ 8 - 0
servers/visual/rasterizer_rd/shaders/scene_high_end.glsl

@@ -1272,6 +1272,7 @@ FRAGMENT_SHADER_CODE
 
 #endif // !USE_SHADOW_TO_OPACITY
 
+
 #if defined(NORMALMAP_USED)
 
 	normalmap.xy = normalmap.xy * 2.0 - 1.0;
@@ -1310,6 +1311,13 @@ FRAGMENT_SHADER_CODE
 
 #if !defined(MODE_RENDER_DEPTH) && !defined(MODE_UNSHADED)
 
+	if (scene_data.roughness_limiter_enabled) {
+		float limit = texelFetch(sampler2D(roughness_buffer, material_samplers[SAMPLER_NEAREST_CLAMP]),ivec2(gl_FragCoord.xy),0).r;
+		roughness = max(roughness,limit);
+
+	}
+
+
 	if (scene_data.use_reflection_cubemap) {
 
 		vec3 ref_vec = reflect(-view, normal);

+ 1 - 1
servers/visual/rasterizer_rd/shaders/scene_high_end_inc.glsl

@@ -63,7 +63,7 @@ layout(set = 0, binding = 3, std140) uniform SceneData {
 	bool ssao_enabled;
 	float ssao_light_affect;
 	float ssao_ao_affect;
-	uint pad_ssao;
+	bool roughness_limiter_enabled;
 
 	vec4 ao_color;
 

+ 2 - 0
servers/visual/visual_server_raster.h

@@ -538,6 +538,8 @@ public:
 	BIND7(environment_set_fog_depth, RID, bool, float, float, float, bool, float)
 	BIND5(environment_set_fog_height, RID, bool, float, float, float)
 
+	BIND2(screen_space_roughness_limiter_set_active, bool, float)
+
 	/* CAMERA EFFECTS */
 
 	BIND0R(RID, camera_effects_create)

+ 2 - 0
servers/visual/visual_server_wrap_mt.h

@@ -453,6 +453,8 @@ public:
 	FUNC7(environment_set_fog_depth, RID, bool, float, float, float, bool, float)
 	FUNC5(environment_set_fog_height, RID, bool, float, float, float)
 
+	FUNC2(screen_space_roughness_limiter_set_active, bool, float)
+
 	FUNCRID(camera_effects)
 
 	FUNC2(camera_effects_set_dof_blur_quality, DOFBlurQuality, bool)

+ 5 - 0
servers/visual_server.cpp

@@ -2326,6 +2326,11 @@ VisualServer::VisualServer() {
 	GLOBAL_DEF("rendering/quality/ssao/quality", 1);
 	ProjectSettings::get_singleton()->set_custom_property_info("rendering/quality/ssao/quality", PropertyInfo(Variant::INT, "rendering/quality/ssao/quality", PROPERTY_HINT_ENUM, "Low (Fast),Medium,High (Slow),Ultra (Very Slow)"));
 	GLOBAL_DEF("rendering/quality/ssao/half_size", false);
+
+	GLOBAL_DEF("rendering/quality/filters/screen_space_roughness_limiter", 0);
+	ProjectSettings::get_singleton()->set_custom_property_info("rendering/quality/filters/screen_space_roughness_limiter", PropertyInfo(Variant::INT, "rendering/quality/filters/screen_space_roughness_limiter", PROPERTY_HINT_ENUM, "Disabled,Enabled (Small Cost)"));
+	GLOBAL_DEF("rendering/quality/filters/screen_space_roughness_limiter_curve", 1.0);
+	ProjectSettings::get_singleton()->set_custom_property_info("rendering/quality/filters/screen_space_roughness_limiter_curve", PropertyInfo(Variant::REAL, "rendering/quality/filters/screen_space_roughness_limiter_curve", PROPERTY_HINT_EXP_EASING, "0.01,8,0.01"));
 }
 
 VisualServer::~VisualServer() {

+ 4 - 0
servers/visual_server.h

@@ -658,6 +658,7 @@ public:
 		VIEWPORT_DEBUG_DRAW_LIGHTING,
 		VIEWPORT_DEBUG_DRAW_OVERDRAW,
 		VIEWPORT_DEBUG_DRAW_WIREFRAME,
+		VIEWPORT_DEBUG_DRAW_NORMAL_BUFFER,
 		VIEWPORT_DEBUG_DRAW_GI_PROBE_ALBEDO,
 		VIEWPORT_DEBUG_DRAW_GI_PROBE_LIGHTING,
 		VIEWPORT_DEBUG_DRAW_GI_PROBE_EMISSION,
@@ -665,6 +666,7 @@ public:
 		VIEWPORT_DEBUG_DRAW_DIRECTIONAL_SHADOW_ATLAS,
 		VIEWPORT_DEBUG_DRAW_SCENE_LUMINANCE,
 		VIEWPORT_DEBUG_DRAW_SSAO,
+		VIEWPORT_DEBUG_DRAW_ROUGHNESS_LIMITER,
 
 	};
 
@@ -768,6 +770,8 @@ public:
 	virtual void environment_set_fog_depth(RID p_env, bool p_enable, float p_depth_begin, float p_depth_end, float p_depth_curve, bool p_transmit, float p_transmit_curve) = 0;
 	virtual void environment_set_fog_height(RID p_env, bool p_enable, float p_min_height, float p_max_height, float p_height_curve) = 0;
 
+	virtual void screen_space_roughness_limiter_set_active(bool p_enable, float p_curve) = 0;
+
 	/* CAMERA EFFECTS */
 
 	virtual RID camera_effects_create() = 0;