Add angular fisheye texture mapping for spheres (#3837)

* Add equirectangular to fisheye coordinate mapping for spheres

* Code cleanup and some image examples

* Add documentation and video sphere example

* Fix renderable video sphere not updating on changed sphere settings

* Increase example video sphere resolution and clarify orientation setting

* Apply suggestions from code review

Co-authored-by: Malin E <malin.ejdbo@gmail.com>

* Address more code review comments

* Update docs to match recent changes in #3835

* Apply suggestions from code review

Co-authored-by: Alexander Bock <alexander.bock@liu.se>
Co-authored-by: Andreas Engberg <48772850+engbergandreas@users.noreply.github.com>

* Move texture mapping to fragment shader

Fixes issues at edges due to interpolation of texture coordinates

---------

Co-authored-by: Malin E <malin.ejdbo@gmail.com>
Co-authored-by: Alexander Bock <alexander.bock@liu.se>
Co-authored-by: Andreas Engberg <48772850+engbergandreas@users.noreply.github.com>
This commit is contained in:
Emma Broman
2025-12-04 10:37:41 +01:00
committed by GitHub
parent 20b1a5ccfe
commit 7e712aca38
10 changed files with 196 additions and 12 deletions
@@ -0,0 +1,28 @@
-- Fisheye Mapping
-- This example shows a sphere that is covered with an image which is retrieved from
-- a local file path and mapped to the sphere using Angular Fisheye projection. The image
-- will cover half of the sphere.
local Node = {
Identifier = "RenderableSphereImageLocal_Example_FisheyeMapping",
Renderable = {
Type = "RenderableSphereImageLocal",
Texture = openspace.absPath("${DATA}/test3.jpg"),
TextureProjection = "Angular Fisheye",
-- Set orientation to also render the inside of the sphere (which is the correct view
-- for a fisheye/fulldome image)
Orientation = "Both"
},
GUI = {
Name = "RenderableSphereImageLocal - Fisheye Mapping",
Path = "/Examples"
}
}
asset.onInitialize(function()
openspace.addSceneGraphNode(Node)
end)
asset.onDeinitialize(function()
openspace.removeSceneGraphNode(Node)
end)
@@ -0,0 +1,28 @@
-- Fisheye Mapping
-- This example shows a sphere that is covered with an image which is retrieved from an
-- online URL and mapped to the sphere using Angular Fisheye projection. The image will
-- cover half of the sphere.
local Node = {
Identifier = "RenderableSphereImageOnline_Example_FisheyeMapping",
Renderable = {
Type = "RenderableSphereImageOnline",
URL = "http://data.openspaceproject.com/examples/renderableplaneimageonline.jpg",
TextureProjection = "Angular Fisheye",
-- Set orientation to also render the inside of the sphere (which is the correct view
-- for a fisheye/fulldome image)
Orientation = "Both"
},
GUI = {
Name = "RenderableSphereImageOnline - Fisheye Mapping",
Path = "/Examples"
}
}
asset.onInitialize(function()
openspace.addSceneGraphNode(Node)
end)
asset.onDeinitialize(function()
openspace.removeSceneGraphNode(Node)
end)
@@ -0,0 +1,42 @@
-- Fulldome Fisheye Video
-- Creates a 3D sphere with an angular fisheye video (fulldome) mapped onto its surface.
-- The video will be shown on half of the sphere.
-- The video file is here downloaded from a URL. This code returns the path to a folder
-- where the file is stored after download
local data = asset.resource({
Name = "Example Video Angular Fisheye",
Type = "UrlSynchronization",
Identifier = "example_video_angularfisheye",
Url = "https://liu-se.cdn.openspaceproject.com/files/examples/video/examplevideo_fisheye.mp4"
})
-- For a local file, use "asset.resource("path/to/local/video.mp4")" here instead
local video = data .. "examplevideo_fisheye.mp4"
local Node = {
Identifier = "RenderableVideoSphere_Example",
Renderable = {
Type = "RenderableVideoSphere",
Video = video,
TextureProjection = "Angular Fisheye",
-- Set orientation to also render the inside of the sphere (which is the correct view
-- for a fisheye/fulldome video)
Orientation = "Both",
-- Increasing the number of segments makes the sphere smoother and reduces distortion
-- at the edge of the video
Segments = 64
},
GUI = {
Name = "RenderableVideoSphere - Fisheye Video",
Path = "/Examples"
}
}
asset.onInitialize(function()
openspace.addSceneGraphNode(Node)
end)
asset.onDeinitialize(function()
openspace.removeSceneGraphNode(Node)
end)
+41 -6
View File
@@ -67,8 +67,8 @@ namespace {
openspace::properties::Property::Visibility::AdvancedUser
};
enum class Orientation : int {
Outside = 0,
enum class Orientation {
Outside,
Inside,
Both
};
@@ -88,6 +88,20 @@ namespace {
openspace::properties::Property::Visibility::AdvancedUser
};
enum class TextureProjection {
Equirectangular,
AngularFisheye
};
constexpr openspace::properties::Property::PropertyInfo TextureProjectionInfo = {
"TextureProjection",
"Texture Projection",
"Specifies the projection mapping to use for any texture loaded onto the sphere "
"(assumes Equirectangular per default). Note that for \"Angular Fisheye\" only "
"half the sphere will be textured - the hemisphere centered around the z-axis.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo DisableFadeInOutInfo = {
"DisableFadeInOut",
"Disable fade-in/fade-out effects",
@@ -145,10 +159,11 @@ namespace {
openspace::properties::Property::Visibility::AdvancedUser
};
// This `Renderable` represents a simple sphere with an image. The image that is shown
// should be in an equirectangular projection/spherical panoramic image or else
// distortions will be introduced. The `Orientation` parameter determines whether the
// provided image is shown on the inside, outside, or both sides of the sphere.
// This `Renderable` represents a simple sphere with an image. Per default, the
// sphere uses an equirectangular projection for the image mapping.
//
// The `Orientation` parameter determines whether the provided image is shown on
// the inside, outside, or both sides of the sphere.
struct [[codegen::Dictionary(RenderableSphere)]] Parameters {
// [[codegen::verbatim(SizeInfo.description)]]
std::optional<float> size [[codegen::greater(0.f)]];
@@ -168,6 +183,14 @@ namespace {
// [[codegen::verbatim(MirrorTextureInfo.description)]]
std::optional<bool> mirrorTexture;
enum class [[codegen::map(TextureProjection)]] TextureProjection {
Equirectangular,
AngularFisheye [[codegen::key("Angular Fisheye")]]
};
// [[codegen::verbatim(TextureProjectionInfo.description)]]
std::optional<TextureProjection> textureProjection;
// [[codegen::verbatim(DisableFadeInOutInfo.description)]]
std::optional<bool> disableFadeInOut;
@@ -204,6 +227,7 @@ RenderableSphere::RenderableSphere(const ghoul::Dictionary& dictionary)
, _segments(SegmentsInfo, 16, 4, 1000)
, _orientation(OrientationInfo)
, _mirrorTexture(MirrorTextureInfo, false)
, _textureProjection(TextureProjectionInfo)
, _disableFadeInDistance(DisableFadeInOutInfo, false)
, _fadeInThreshold(FadeInThresholdInfo, 0.f, 0.f, 1.f, 0.001f)
, _fadeOutThreshold(FadeOutThresholdInfo, 0.f, 0.f, 1.f, 0.001f)
@@ -243,6 +267,15 @@ RenderableSphere::RenderableSphere(const ghoul::Dictionary& dictionary)
_mirrorTexture = p.mirrorTexture.value_or(_mirrorTexture);
addProperty(_mirrorTexture);
_textureProjection.addOptions({
{ static_cast<int>(TextureProjection::Equirectangular), "Equirectangular" },
{ static_cast<int>(TextureProjection::AngularFisheye), "Angular Fisheye" }
});
_textureProjection = p.textureProjection.has_value() ?
static_cast<int>(codegen::map<TextureProjection>(*p.textureProjection)) :
static_cast<int>(TextureProjection::Equirectangular);
addProperty(_textureProjection);
_disableFadeInDistance = p.disableFadeInOut.value_or(_disableFadeInDistance);
addProperty(_disableFadeInDistance);
@@ -439,6 +472,8 @@ void RenderableSphere::render(const RenderData& data, RendererTasks&) {
defer{ unbindTexture(); };
_shader->setUniform(_uniformCache.colorTexture, unit);
_shader->setUniform(_uniformCache.textureProjection, _textureProjection.value());
// Setting these states should not be necessary,
// since they are the default state in OpenSpace.
glEnable(GL_CULL_FACE);
+2 -1
View File
@@ -65,6 +65,7 @@ protected:
properties::OptionProperty _orientation;
properties::BoolProperty _mirrorTexture;
properties::OptionProperty _textureProjection;
properties::BoolProperty _disableFadeInDistance;
properties::FloatProperty _fadeInThreshold;
@@ -84,7 +85,7 @@ private:
std::unique_ptr<TransferFunction> _transferFunction;
UniformCache(opacity, modelViewProjection, modelViewTransform, modelViewRotation,
colorTexture, mirrorTexture) _uniformCache;
colorTexture, mirrorTexture, textureProjection) _uniformCache;
};
} // namespace openspace
@@ -44,6 +44,10 @@ namespace {
// This `Renderable` shows a sphere with an image provided by a local file on disk. To
// show a sphere with an image from an online source, see
// [RenderableSphereImageOnline](#base_screenspace_image_online).
//
// Per default, the sphere uses an equirectangular projection for the image mapping
// and hence expects an equirectangular image. However, it can also be used to show
// fisheye images by changing the `TextureProjection`.
struct [[codegen::Dictionary(RenderableSphereImageLocal)]] Parameters {
// [[codegen::verbatim(TextureInfo.description)]]
std::filesystem::path texture;
@@ -68,6 +68,10 @@ namespace {
// will be downloaded when the `Renderable` is added to a scene graph node. To show a
// sphere with an image from a local file, see
// [RenderableSphereImageLocal](#base_screenspace_image_local).
//
// Per default, the sphere uses an equirectangular projection for the image mapping
// and hence expects an equirectangular image. However, it can also be used to show
// fisheye images by changing the `TextureProjection`.
struct [[codegen::Dictionary(RenderableSphereImageOnline)]] Parameters {
// [[codegen::verbatim(TextureInfo.description)]]
std::string url [[codegen::key("URL")]];
+40 -1
View File
@@ -38,9 +38,48 @@ uniform vec2 dataMinMaxValues;
uniform float opacity;
uniform bool mirrorTexture;
const int Equirectangular = 0;
const int AngularFisheye = 1;
uniform int textureProjection;
const float M_PI = 3.14159265358979323846;
// Remap equirectangular texture coordinates into angular fisheye
vec2 equiToAngularFisheye(vec2 textureCoords) {
vec2 pos2 = textureCoords * 2.0 - 1.0; // Map [0,1] tex coords to [-1,1]
// 2D equi to 3D vector
float lat = pos2.y * 0.5 * M_PI;
float lon = pos2.x * M_PI;
// Map to 3D position, with Z being the north pole
vec3 pos3 = vec3(
cos(lat) * cos(lon),
sin(lat),
cos(lat) * sin(lon)
);
float coverAngle = M_PI; // 180 degrees
// 3D vector to normalized 2D fisheye [-1,1]
float r = 2.0 / coverAngle * atan(sqrt(dot(pos3.xz, pos3.xz)), pos3.y);
float theta = atan(pos3.z, pos3.x);
vec2 fisheye2D = vec2(r * cos(theta), r * sin(theta));
if (r > 1.0) {
discard; // Invalid coordinates (outside fisheye frame)
}
// Remap to [0,1]
return 0.5 * fisheye2D + 0.5;
}
Fragment getFragment() {
vec2 texCoord = vs_textureCoords;
vec2 texCoord = vs_textureCoords; // Equirectangular
if (textureProjection == AngularFisheye) {
texCoord = equiToAngularFisheye(vs_textureCoords);
}
Fragment frag;
if (mirrorTexture) {
+1 -2
View File
@@ -36,11 +36,10 @@ uniform mat4 modelViewProjection;
uniform mat4 modelViewTransform;
uniform mat3 modelViewRotation;
void main() {
vs_normal = modelViewRotation * normalize(in_position.xyz);
vs_textureCoords = in_textureCoords;
vs_normal = modelViewRotation * normalize(in_position.xyz);
vec4 position = modelViewProjection * vec4(in_position.xyz, 1.0);
vs_position = modelViewTransform * vec4(in_position.xyz, 1.0);
+6 -2
View File
@@ -29,7 +29,10 @@
#include <openspace/util/sphere.h>
namespace {
// This `Renderable` creates a textured 3D sphere where the texture is a video.
// This `Renderable` creates a textured 3D sphere where the texture is a video. Per
// default, the sphere uses an equirectangular projection for the image mapping
// and hence expects a video in equirectangular format. However, it can also be used
// to play fisheye videos by changing the `TextureProjection`.
//
// The video can either be played back based on a given simulation time
// (`PlaybackMode` MapToSimulationTime) or through the user interface (for
@@ -83,7 +86,8 @@ void RenderableVideoSphere::render(const RenderData& data, RendererTasks& render
}
}
void RenderableVideoSphere::update(const UpdateData&) {
void RenderableVideoSphere::update(const UpdateData& data) {
RenderableSphere::update(data);
if (!_videoPlayer.isInitialized()) {
return;
}