└── addons └── godot360 ├── src ├── camera360.gd.uid ├── camera360.gdshader.uid ├── camera360.svg.import ├── camera360.svg ├── camera360.gd └── camera360.gdshader ├── LICENSE └── readme.md /addons/godot360/src/camera360.gd.uid: -------------------------------------------------------------------------------- 1 | uid://7m0ekawqrbcw 2 | -------------------------------------------------------------------------------- /addons/godot360/src/camera360.gdshader.uid: -------------------------------------------------------------------------------- 1 | uid://b6q7jc2axr3m2 2 | -------------------------------------------------------------------------------- /addons/godot360/src/camera360.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://xlxemjbhqo1s" 6 | path="res://.godot/imported/camera360.svg-f0ba38dc4bb8ed5ba101a99d888c90fc.ctex" 7 | metadata={ 8 | "has_editor_variant": true, 9 | "vram_texture": false 10 | } 11 | 12 | [deps] 13 | 14 | source_file="res://addons/godot360/src/camera360.svg" 15 | dest_files=["res://.godot/imported/camera360.svg-f0ba38dc4bb8ed5ba101a99d888c90fc.ctex"] 16 | 17 | [params] 18 | 19 | compress/mode=0 20 | compress/high_quality=false 21 | compress/lossy_quality=0.7 22 | compress/hdr_compression=1 23 | compress/normal_map=0 24 | compress/channel_pack=0 25 | mipmaps/generate=false 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/hdr_as_srgb=false 33 | process/hdr_clamp_exposure=false 34 | process/size_limit=0 35 | detect_3d/compress_to=1 36 | svg/scale=1.0 37 | editor/scale_with_editor_scale=true 38 | editor/convert_colors_with_editor_theme=true 39 | -------------------------------------------------------------------------------- /addons/godot360/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Cyril Bissey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/godot360/src/camera360.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /addons/godot360/readme.md: -------------------------------------------------------------------------------- 1 | # Godot360 2 | ![Fisheye projection (~350 degrees)](./images/fisheye_350deg.jpg) 3 | 4 | This is an attempt at making a 360° camera in Godot, to allow for both panoramic shots and non-rectilinear FoVs, such as the Panini projection, which can display FoVs of 120-150° with less distortion than the standard rectilinear projection. 5 | 6 | I started working on this for two reasons: one of my own projects could benefit from a proper fisheye camera, and to follow up on [this issue on Godot's repo](https://github.com/godotengine/godot/issues/7340). 7 | 8 | **Important note**: Godot 4.3 introduced a change in how shaders handle depth, which is incompatible with versions 4.2 and earlier. If you need an earlier version of Godot, you will need to revert commit f22d98e. 9 | 10 | ![Panini projection (~150 degrees)](./images/panini_150deg.jpg) 11 | 12 | ## Globes and lenses 13 | The code is based on [these](https://github.com/shaunlebron/blinky) [repos](https://github.com/shaunlebron/flex-fov), with a single standard cube "globe" (I may try to add edge-centered and corner-centered cubes for performance), and the following lenses: 14 | * Rectilinear: This is the standard projection, the FoV cannot reach nor exceed 180°; distortion in the corners is quite noticeable at an FoV of 120°, and the center of the image gets heavily compressed. 15 | * Panini: Allows for natural looking scenes at wider angles than the rectilinear projection, can lose straight lines when looking up or down 16 | * Fisheye: Popular wide angle lens, edges get compressed and bent 17 | * Stereographic: Displays both poles at 360° FoV 18 | * Cylindrical: What you would obtain if you flattened a cylinder 19 | * Equirectangular: General panorama projection 20 | * Mercator: Usually used for maps 21 | * Fulldome: An adaptation of the fisheye lens that conforms to a format used to do video projection in planetariums 22 | 23 | Note that I am no expert on those projections, you can find much more details about them from better sources. 24 | 25 | ## Getting started 26 | The process of enabling 360° rendering is fairly easy: 27 | * Add a Camera360 node to your scene (you can replace an existing camera or add it as a child of your original camera if you do not want to move code around) 28 | * Set up exports of the Camera360 node, including the SubViewport (which you should add as a child of the camera) 29 | * Make sure the Camera360's cull mask only sees the same layer as the MeshQuad used for rendering (more details below), and remove this layer from all other cameras, meshes, etc. 30 | 31 | Note that the subviewport export gets duplicated and applied to all six cameras, and the clip_near and clip_far properties also apply to all cameras. 32 | I recommend not changing the Camera360's own clip planes nor its FoV as they have no effect. 33 | 34 | ## Demo controls 35 | The included demo allows you to change lenses (projections) and display an overlay of the original cameras: 36 | * L switches to the next lens/projection (hold shift for previous lens) 37 | * G toggles the grid overlay 38 | * Use Numpad +/- or the mouse wheel to change the horizontal FoV in increments of 5° 39 | * Click the left or right mouse buttons to make the camera spin either direction 40 | 41 | Feel free to add this camera to other projects, the above screenshots are from the [Sponza demo](https://github.com/Calinou/godot-sponza). 42 | 43 | ## How this works 44 | The Camera360 node creates 6 cameras with a 90° FoV looking in all directions. Each camera renders to a dedicated Viewport, whose texture is passed to the QuadMesh's fragment shader. The QuadMesh is made fullscreen and displays the final image obtained from the distorted views according to the chosen projection. 45 | 46 | ## Drawbacks of this method 47 | While it allows for more control over what is displayed on screen, and allows for panoramic shots, this technique comes with several problems of its own: 48 | * Rendering time is greatly increased as the game needs to render 6 images for a complete 360° capture. 49 | * Artefacts are visible at the seams, due to screen-space effects (e.g. bloom). 50 | -------------------------------------------------------------------------------- /addons/godot360/src/camera360.gd: -------------------------------------------------------------------------------- 1 | @icon("res://addons/godot360/src/camera360.svg") 2 | class_name Camera360 3 | extends Camera3D 4 | ## Special-purpose camera for omnidirectional rendering 5 | ## 6 | ## This camera can render 360-degree views and project them onto a fullscreen quad mesh 7 | ## using a number of projections, ranging from fisheye to cylindrical projections, and 8 | ## also includes the popular Panini projection.[br] 9 | ## At this time, only one camera setup is supported, with (up to) 6 cameras forming a cube. 10 | ## Ideally, forward-focused setups should be added, to allow e.g. 3 cameras on rectangular 11 | ## viewports to cover a 180-degree FoV. 12 | 13 | enum Lens { 14 | RECTILINEAR, 15 | PANINI, 16 | FISHEYE, 17 | STEREOGRAPHIC, 18 | CYLINDRICAL, 19 | EQUIRECTANGULAR, 20 | MERCATOR, 21 | FULLDOME, 22 | } 23 | 24 | ## The horizontal FoV of the camera, in degrees.[br] 25 | ## [b]Warning:[/b] This value is not accurate for most lenses. 26 | @export_range(10, 360) var fovx := 150.0: 27 | set(value): 28 | fovx = value 29 | if fovx < 10: 30 | fovx = 10 31 | if fovx > 360: 32 | fovx = 360 33 | mat.set_shader_parameter("fovx", fovx) 34 | ## The current lens, which governs how each of the cameras are assembled and deformed 35 | ## to produce the final image. 36 | @export var lens := Lens.RECTILINEAR: set = set_lens 37 | ## Near clip plane distance for subcameras. 38 | @export_range(0.001, 10) var clip_near := 0.1 39 | ## Far clip plane distance for subcameras. 40 | @export_range(0.01, 10000) var clip_far := 1000.0 41 | ## The number of subcameras to use for rendering. 42 | @export_range(1, 6) var num_cameras := 6 43 | ## The FoV of all subcameras, in degrees. Values above 90 are more expensive to render, 44 | ## but can alleviate rendering artifacts near subviewport seams caused by screenspace effects. 45 | @export_range(90, 120) var camera_fov := 100 46 | ## The render layer used for rendering of the final image. 47 | @export_range(1, 20) var render_layer := 11 48 | ## The [Environment] to use for the subcameras. 49 | @export var camera_environment: Environment = null 50 | ## The [SubViewport] to use for all subcameras. Use this to set viewport settings such as 51 | ## antialiasing and other quality settings. 52 | @export var subviewport: SubViewport = null 53 | 54 | var viewports : Array[SubViewport] = [] 55 | var cameras : Array[Camera3D] = [] 56 | 57 | ## The fullscreen quad mesh where the final image is drawn. 58 | var render_quad: MeshInstance3D = null 59 | ## The material used to produce the final image. 60 | var mat := ShaderMaterial.new() 61 | 62 | 63 | func _ready() -> void: 64 | render_layer = int(pow(2, render_layer - 1)) 65 | cull_mask = render_layer 66 | 67 | render_quad = MeshInstance3D.new() 68 | add_child(render_quad) 69 | render_quad.translate_object_local(Vector3.FORWARD * (near + 0.1 * (far - near))) 70 | var quad_mesh := QuadMesh.new() 71 | quad_mesh.size = Vector2(2, 2) 72 | render_quad.mesh = quad_mesh 73 | render_quad.layers = render_layer 74 | render_quad.mesh.surface_set_material(0, mat) 75 | 76 | mat.shader = preload("res://addons/godot360/src/camera360.gdshader") 77 | mat.set_shader_parameter("fovx", fovx) 78 | mat.set_shader_parameter("lens", lens) 79 | @warning_ignore("unsafe_property_access") 80 | mat.set_shader_parameter("resolution", get_viewport().size) 81 | mat.set_shader_parameter("subcamera_fov", camera_fov) 82 | 83 | for i in num_cameras: 84 | var viewport := subviewport.duplicate() as SubViewport 85 | add_child(viewport) 86 | viewports.append(viewport) 87 | mat.set_shader_parameter("Texture%d" % [i], viewport.get_texture()) 88 | 89 | var camera := Camera3D.new() 90 | viewport.add_child(camera) 91 | camera.fov = camera_fov 92 | camera.near = clip_near 93 | camera.far = clip_far 94 | camera.cull_mask -= render_layer 95 | camera.environment = camera_environment 96 | cameras.append(camera) 97 | 98 | if num_cameras < 6: 99 | for i in range(num_cameras + 1, 6): 100 | mat.set_shader_parameter("Texture%d" % [i], Texture2D.new()) 101 | 102 | 103 | func _process(_delta: float) -> void: 104 | for camera in cameras: 105 | camera.global_transform = global_transform 106 | if num_cameras >= 2: 107 | cameras[1].rotate_object_local(Vector3.UP, PI/2) 108 | if num_cameras >= 3: 109 | cameras[2].rotate_object_local(Vector3.UP, -PI/2) 110 | if num_cameras >= 4: 111 | cameras[3].rotate_object_local(Vector3.RIGHT, -PI/2) 112 | if num_cameras >= 5: 113 | cameras[4].rotate_object_local(Vector3.RIGHT, PI/2) 114 | if num_cameras >= 6: 115 | cameras[5].rotate_object_local(Vector3.UP, PI) 116 | 117 | 118 | @warning_ignore("unused_parameter") 119 | func _set_viewport_settings(viewport: SubViewport) -> void: 120 | pass 121 | 122 | 123 | func set_lens(l: Lens) -> void: 124 | lens = l 125 | if lens > Lens.size() - 1 or lens < 0: 126 | lens = Lens.RECTILINEAR 127 | mat.set_shader_parameter("lens", lens) 128 | -------------------------------------------------------------------------------- /addons/godot360/src/camera360.gdshader: -------------------------------------------------------------------------------- 1 | shader_type spatial; 2 | render_mode unshaded, cull_front; 3 | 4 | uniform float subcamera_fov = 100.0; 5 | uniform float fovx; 6 | 7 | uniform int lens; 8 | uniform vec2 camera_resolution; 9 | uniform bool show_grid; 10 | 11 | uniform sampler2D Grid; 12 | uniform sampler2D Texture0 : hint_default_black; 13 | uniform sampler2D Texture1 : hint_default_black; 14 | uniform sampler2D Texture2 : hint_default_black; 15 | uniform sampler2D Texture3 : hint_default_black; 16 | uniform sampler2D Texture4 : hint_default_black; 17 | uniform sampler2D Texture5 : hint_default_black; 18 | 19 | 20 | vec3 latlon_to_ray(vec2 latlon) { 21 | float lat = latlon.x; 22 | float lon = latlon.y; 23 | return vec3(sin(lon) * cos(lat), sin(lat), -cos(lon) * cos(lat)); 24 | } 25 | 26 | vec3 rectilinear_inverse(vec2 p) { 27 | float r = sqrt(p.x * p.x + p.y * p.y); 28 | float theta = atan(r); 29 | float s = sin(theta); 30 | return vec3(p.x / r * s, p.y / r * s, -cos(theta)); 31 | } 32 | vec2 rectilinear_forward(vec2 latlon) { 33 | vec3 ray = latlon_to_ray(latlon); 34 | float theta = acos(-ray.z); 35 | float r = tan(theta); 36 | float c = r / length(ray.xy); 37 | return vec2(ray.x * c, ray.y * c); 38 | } 39 | vec3 rectilinear_ray(vec2 p) { 40 | float scale = rectilinear_forward(vec2(0.0, radians(fovx) / 2.0)).x; 41 | return rectilinear_inverse(p * scale); 42 | } 43 | 44 | vec3 panini_inverse(vec2 p) { 45 | float d = 1.0; 46 | float k = p.x * p.x / ((d + 1.0) * (d + 1.0)); 47 | float dscr = k * k * d * d - (k + 1.0) * (k * d * d - 1.0); 48 | float clon = (-k * d + sqrt(dscr)) / (k + 1.0); 49 | float s = (d + 1.0) / (d + clon); 50 | float lon = atan(p.x, (s * clon)); 51 | float lat = atan(p.y, s); 52 | return latlon_to_ray(vec2(lat, lon)); 53 | } 54 | vec2 panini_forward(vec2 latlon) { 55 | float d = 1.0; 56 | float s = (d + 1.0) / (d + cos(latlon.y)); 57 | float x = s * sin(latlon.y); 58 | float y = s * tan(latlon.x); 59 | return vec2(x, y); 60 | } 61 | vec3 panini_ray(vec2 p) { 62 | float scale = panini_forward(vec2(0.0, radians(fovx) / 2.0)).x; 63 | return panini_inverse(p * scale); 64 | } 65 | 66 | vec3 fisheye_inverse(vec2 p) { 67 | float r = sqrt(p.x * p.x + p.y * p.y); 68 | 69 | if (r > PI) { 70 | return vec3(0.0, 0.0, 0.0); 71 | } 72 | else { 73 | float theta = r; 74 | float s = sin(theta); 75 | return vec3(p.x / r * s, p.y / r * s, -cos(theta)); 76 | } 77 | } 78 | vec2 fisheye_forward(vec2 latlon) { 79 | vec3 ray = latlon_to_ray(latlon); 80 | float theta = acos(-ray.z); 81 | float r = theta; 82 | float c = r / length(ray.xy); 83 | return vec2(ray.x * c, ray.y * c); 84 | } 85 | vec3 fisheye_ray(vec2 p) { 86 | float scale = fisheye_forward(vec2(0.0, radians(fovx) / 2.0)).x; 87 | return fisheye_inverse(p * scale); 88 | } 89 | vec3 fulldome(vec2 p) { 90 | // This will dezoom enough that we should get exactly 91 | // all of the fov in the viewport 92 | vec2 dezoomed = p * 2.0; 93 | // For fulldome output, we want to crop the ouput so that we only render 94 | // the circle corresponding to the fov 95 | if (length(dezoomed) > 1.0) { 96 | return vec3(0.0, 0.0, 0.0); 97 | } 98 | // If we are within the 1 unit circle, we want the fisheye perspective. 99 | return fisheye_ray(dezoomed); 100 | } 101 | 102 | vec3 stereographic_inverse(vec2 p) { 103 | float scale = 0.5; 104 | float r = sqrt(p.x * p.x + p.y * p.y); 105 | float theta = atan(r) / scale; 106 | float s = sin(theta); 107 | return vec3(p.x / r * s, p.y / r * s, -cos(theta)); 108 | } 109 | vec2 stereographic_forward(vec2 latlon) { 110 | vec3 ray = latlon_to_ray(latlon); 111 | float theta = acos(-ray.z); 112 | float scale = 0.5; 113 | float r = tan(theta * scale); 114 | float c = r / length(ray.xy); 115 | return vec2(ray.x * c, ray.y * c); 116 | } 117 | vec3 stereographic_ray(vec2 p) { 118 | float scale = stereographic_forward(vec2(0.0, radians(fovx) / 2.0)).x; 119 | return stereographic_inverse(p * scale); 120 | } 121 | 122 | vec3 cylindrical_inverse(vec2 p) { 123 | if (abs(p.x) > PI) { 124 | return vec3(0.0, 0.0, 0.0); 125 | } else { 126 | float lon = p.x; 127 | float lat = atan(p.y); 128 | return latlon_to_ray(vec2(lat, lon)); 129 | } 130 | } 131 | vec2 cylindrical_forward(vec2 latlon) { 132 | return(vec2(latlon.y, tan(latlon.x))); 133 | } 134 | vec3 cylindrical_ray(vec2 p) { 135 | float scale = cylindrical_forward(vec2(0.0, radians(fovx) / 2.0)).x; 136 | return cylindrical_inverse(p * scale); 137 | } 138 | 139 | vec3 equirectangular_inverse(vec2 p) { 140 | if (abs(p.y) > PI / 2.0 || abs(p.x) > PI) { 141 | return vec3(0.0, 0.0, 0.0); 142 | } else { 143 | float lon = p.x; 144 | float lat = p.y; 145 | return latlon_to_ray(vec2(lat, lon)); 146 | } 147 | } 148 | vec2 equirectangular_forward(vec2 latlon) { 149 | return vec2(latlon.y, latlon.x); 150 | } 151 | vec3 equirectangular_ray(vec2 p) { 152 | float scale = equirectangular_forward(vec2(0.0, radians(fovx) / 2.0)).x; 153 | return equirectangular_inverse(p * scale); 154 | } 155 | 156 | vec3 mercator_inverse(vec2 p) { 157 | if (abs(p.x) > PI) { 158 | return vec3(0.0, 0.0, 0.0); 159 | } else { 160 | float lon = p.x; 161 | float lat = atan(sinh(p.y)); 162 | return latlon_to_ray(vec2(lat, lon)); 163 | } 164 | } 165 | vec2 mercator_forward(vec2 latlon) { 166 | return vec2(latlon.y, log(tan(PI / 4.0 + latlon.x / 2.0))); 167 | } 168 | vec3 mercator_ray(vec2 p) { 169 | float scale = mercator_forward(vec2(0.0, radians(fovx) / 2.0)).x; 170 | return mercator_inverse(p * scale); 171 | } 172 | 173 | vec3 get_transformation(vec2 uv) { 174 | switch (lens) { 175 | case 0: return(rectilinear_ray(uv)); 176 | case 1: return(panini_ray(uv)); 177 | case 2: return(fisheye_ray(uv)); 178 | case 3: return(stereographic_ray(uv)); 179 | case 4: return(cylindrical_ray(uv)); 180 | case 5: return(equirectangular_ray(uv)); 181 | case 6: return(mercator_ray(uv)); 182 | case 7: return(fulldome(uv)); 183 | } 184 | } 185 | 186 | vec3 srgb_to_linear(vec3 color) { 187 | return color * (color * (color * 0.305306011 + 0.682171111) + 0.012522878); 188 | } 189 | 190 | void vertex() { 191 | POSITION = vec4(VERTEX.xy, 1.0, 1.0); 192 | } 193 | 194 | void fragment() { 195 | bool debug = false; 196 | 197 | vec3 color_front = vec3(1.0, 1.0, 1.0); 198 | vec3 color_back = vec3(1.0, 1.0, 0.0); 199 | vec3 color_left = vec3(1.0, 0.0, 0.0); 200 | vec3 color_right = vec3(1.0, 0.2, 0.0); 201 | vec3 color_bottom = vec3(0.0, 1.0, 0.0); 202 | vec3 color_top = vec3(0.0, 0.0, 1.0); 203 | float alpha = 0.5; 204 | 205 | float view_ratio = VIEWPORT_SIZE.x / VIEWPORT_SIZE.y; 206 | vec2 uv = FRAGCOORD.xy / min(VIEWPORT_SIZE.x, VIEWPORT_SIZE.y); 207 | vec3 pos = vec3(0.0, 0.0, 0.0); 208 | if (view_ratio > 1.0) { 209 | pos = get_transformation(vec2(uv.x - 0.5 * view_ratio, uv.y - 0.5) * 1.0); 210 | } else { 211 | pos = get_transformation(vec2(uv.x - 0.5, uv.y - 0.5 * view_ratio) * 1.0); 212 | } 213 | if (pos == vec3(0.0, 0.0, 0.0)) { 214 | ALBEDO = pos; 215 | } else { 216 | float ax = abs(pos.x); 217 | float ay = abs(pos.y); 218 | float az = abs(pos.z); 219 | 220 | float fov_fix = 1.0 / tan(radians(subcamera_fov / 2.0)); 221 | if (az >= ax && az >= ay) { 222 | if (pos.z < 0.0) { 223 | uv = vec2(fov_fix * 0.5 * (vec2(pos.x / az, pos.y / az)) + 0.5); 224 | ALBEDO = srgb_to_linear(texture(Texture0, uv).rgb); 225 | if (show_grid) { 226 | ALBEDO = mix(ALBEDO, ALBEDO * (vec4(color_front, 1.0) * texture(Grid, uv)).rgb, alpha); 227 | } 228 | } else { 229 | uv = vec2(fov_fix * 0.5 * (vec2(-pos.x / az, pos.y / az)) + 0.5); 230 | ALBEDO = srgb_to_linear(texture(Texture5, uv).rgb); 231 | if (show_grid) { 232 | ALBEDO = mix(ALBEDO, ALBEDO * (vec4(color_back, 1.0) * texture(Grid, uv)).rgb, alpha); 233 | } 234 | } 235 | } else if (ax >= ay && ax >= az) { 236 | if (pos.x < 0.0) { 237 | uv = vec2(fov_fix * 0.5 * (vec2(-pos.z / ax, pos.y / ax)) + 0.5); 238 | ALBEDO = srgb_to_linear(texture(Texture1, uv).rgb); 239 | if (show_grid) { 240 | ALBEDO = mix(ALBEDO, ALBEDO * (vec4(color_left, 1.0) * texture(Grid, uv)).rgb, alpha); 241 | } 242 | } else { 243 | ALBEDO = color_right; 244 | uv = vec2(fov_fix * 0.5 * (vec2(pos.z / ax, pos.y / ax)) + 0.5); 245 | ALBEDO = srgb_to_linear(texture(Texture2, uv).rgb); 246 | if (show_grid) { 247 | ALBEDO = mix(ALBEDO, ALBEDO * (vec4(color_right, 1.0) * texture(Grid, uv)).rgb, alpha); 248 | } 249 | } 250 | } else if (ay >= ax && ay >= az) { 251 | if (pos.y > 0.0) { 252 | uv = vec2(fov_fix * 0.5 * (vec2(pos.x / ay, pos.z / ay)) + 0.5); 253 | ALBEDO = srgb_to_linear(texture(Texture3, uv).rgb); 254 | if (show_grid) { 255 | ALBEDO = mix(ALBEDO, ALBEDO * (vec4(color_bottom, 1.0) * texture(Grid, uv)).rgb, alpha); 256 | } 257 | } else { 258 | uv = vec2(fov_fix * 0.5 * (vec2(pos.x / ay, -pos.z / ay)) + 0.5); 259 | ALBEDO = srgb_to_linear(texture(Texture4, uv).rgb); 260 | if (show_grid) { 261 | ALBEDO = mix(ALBEDO, ALBEDO * (vec4(color_top, 1.0) * texture(Grid, uv)).rgb, alpha); 262 | } 263 | } 264 | } else { 265 | ALBEDO = vec3(0.0, 0.0, 0.0); 266 | } 267 | } 268 | if (debug) { 269 | ALBEDO = vec3(uv.x, uv.y, 0.0); // debug UV display 270 | if (show_grid) { 271 | ALBEDO = ALBEDO * texture(Grid, uv).rgb; 272 | } 273 | } 274 | } 275 | --------------------------------------------------------------------------------