Can rendercam support fisheye perspective?

Has anyone experimented w/ modifying Rendercam to allow for a fisheye perspective? I.e. objects on the edge of the screen should become smaller relative to objects close to the focal point?

EDIT: See the screenshot below as an example of the issue I want to correct:

1 Like

What are your current RenderCam camera settings?

Support: No. Allow with modifications: Yes. Usually this sort of thing is done in a shader, either a vertex shader on all of your models or a post-process shader to warp the screen.

A long time ago I had a FPS project using a Panini projection shader that my brother wrote for me. Sadly, it used a native extension that doesn’t build with newer Defold versions, but here’s the vertex shader:

Vertex Shader
attribute mediump vec4 position;
attribute mediump vec2 texcoord0;
attribute mediump vec3 normal;

uniform mediump mat4 mtx_view;
uniform mediump mat4 mtx_proj;
uniform mediump mat4 mtx_viewproj;
uniform mediump vec4 camera; // = vec4(kFoV, nearz, farz, aspect)

varying mediump vec4 var_position;
varying mediump vec3 var_normal;
varying mediump vec2 var_texcoord0;
varying mediump float var_dist;

#define M_PI 3.14159265358979323846264338327950288
const float d = 1.0;

void main() {
	var_texcoord0 = texcoord0;
	var_normal = normalize(normal);

	vec4 q = mtx_view * position;
	float r = length(q.xyz);

	vec2 p = normalize(q.xz);
	float S = camera.x * (d + 1.0) / (d - p.y);
	float depth = 2.0 * (-sign(q.z)*r-camera.y) / (camera.z-camera.y) - 1.0;

	vec4 final_pos = vec4(S*p.x, S*q.y*camera.w/length(q.xz), depth, 1.0);

	// un-comment for normal rendering
	//final_pos = mtx_viewproj * position;

	var_dist = length(q.xz);
	var_position = final_pos;
	gl_Position = final_pos;
}

It requires a 'camera' vertex constant set up in the material (type: “User”)

The modified render script I used

local rendercam = require "rendercam.rendercam"
local vp = rendercam.viewport

local IDENTITY_MATRIX = vmath.matrix4()
local CLEAR_COLOR = hash("clear_color")
local WINDOW_RESIZED = hash("window_resized")
local UPDATE_WINDOW = hash("update window")
local SET_CAMERA_CONSTANTS = hash("set camera constants")

local function update_panini_kfov(self, fov)
	local d = 1.0
	local halfFoV = fov * 0.5
	self.camConstants.x = (d + math.cos(halfFoV)) / ((d+1) * math.sin(halfFoV))
	self.constants.camera = self.camConstants
end

local function update_window(self)
	rendercam.update_window(render.get_window_width(), render.get_window_height())
	self.gui_proj = vmath.matrix4_orthographic(vp.x, vp.x + vp.width, vp.y, vp.y + vp.height, -1, 1)
	self.camConstants.w = vp.width / vp.height
	self.constants.camera = self.camConstants
end

function init(self)
    self.tile_pred = render.predicate({"tile"})
    self.gui_pred = render.predicate({"gui"})
    self.text_pred = render.predicate({"text"})
    self.model_pred = render.predicate({"model"})
    self.particle_pred = render.predicate({"particle"})

    self.clear_color = vmath.vector4(0)

	rendercam.update_window_size(render.get_window_width(), render.get_window_height())
	rendercam.configWin.x = render.get_width();  rendercam.configWin.y = render.get_height()

	self.constants = render.constant_buffer()
	self.camConstants = vmath.vector4(135, 0.1, 1000, 960/640)
	self.constants.camera = self.camConstants
end

function update(self)
	-- Set view and projection with latest matrices calculated by the module
	render.set_view(rendercam.calculate_view())
	render.set_projection(rendercam.calculate_proj())

	-- Set viewport (x and y will be zero unless using a fixed aspect ratio)
	render.set_viewport(vp.x, vp.y, vp.width, vp.height)

    render.set_depth_mask(true)
    render.clear({[render.BUFFER_COLOR_BIT] = self.clear_color, [render.BUFFER_DEPTH_BIT] = 1, [render.BUFFER_STENCIL_BIT] = 0})

    render.set_depth_mask(false)
    render.disable_state(render.STATE_DEPTH_TEST)
    render.disable_state(render.STATE_STENCIL_TEST)
    render.enable_state(render.STATE_BLEND)
    render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)
    render.disable_state(render.STATE_CULL_FACE)

    render.draw(self.tile_pred)
    render.draw(self.particle_pred)

	render.enable_state(render.STATE_CULL_FACE)
    render.enable_state(render.STATE_DEPTH_TEST)
    render.set_depth_mask(true)
    render.draw(self.model_pred, self.constants)
    render.draw_debug3d()

	-- GUI Rendering
    render.set_view(IDENTITY_MATRIX)
	render.set_projection(self.gui_proj) -- gui_proj only calculated on update_window

	render.disable_state(render.STATE_CULL_FACE)
    render.disable_state(render.STATE_DEPTH_TEST)
    render.enable_state(render.STATE_STENCIL_TEST)
    render.draw(self.gui_pred)
    render.draw(self.text_pred)
    render.disable_state(render.STATE_STENCIL_TEST)

    render.set_depth_mask(false)
    render.draw_debug2d()
end

function on_message(self, message_id, message)
    if message_id == CLEAR_COLOR then
        self.clear_color = message.color
	elseif message_id == WINDOW_RESIZED then -- sent by engine
		update_window(self)
	elseif message_id == UPDATE_WINDOW then -- sent by rendercam when a camera is activated ("window_resized" engine message requires data)
		update_window(self)
	elseif message_id == SET_CAMERA_CONSTANTS then
		self.camConstants = vmath.vector4(message.fov, message.nearz, message.farz, message.aspect)
		update_panini_kfov(self, message.fov)
    end
end
A bit of setup in some script init
local camUrl = msg.url("camera#script")
local fov = go.get(camUrl, "fov")
if fov > 5 then fov = math.rad(fov) end
msg.post("@render:", "set camera constants", { fov = fov, nearz = go.get(camUrl, "nearZ"), farz = go.get(camUrl, "farZ"), aspect = 960/640 })

Other than that, I think you’re on your own. Though I have heard that some AAA games are starting to use curvilinear perspective, so maybe you can find some info about it.

I could send you my whole project if you’re really interested in trying to get it working again.

5 Likes

Thanks, I’ll see if I can figure it out! What native extension were you using for the FPS project that doesn’t build, out of curiosity?

EDIT: I tried out the shader in another project but I wasn’t able to get it working. It was saying something like self.constants doesn’t exist. Not sure what that’s referring to. Do you have any screenshots of the game / videos using the shader? I’m mostly just curious to see it in action.

Hmm, that sounds like something in the render script. The last few lines of init() set its initial value.

It turns out there was nothing wrong with the project or the extension, just something weird with my editor version. It wasn’t building any native extensions. I just updated and its fine.

Here’s the project: Ross FPS.zip (542.7 KB)

The “fisheye” effect is especially noticeable if you back into one of the corners of the room. You can see both walls like you can in real life, without any wild stretching. The panini projection is cylindrical, it doesn’t mess with the verticals.

Graphics cards still only know how to draw straight lines, so the smoothness of the curves depend on the number of vertices in your models. You can’t just make cubes with 6 faces and get a nice result.

1 Like

That’s pretty cool! Thanks for sharing.

Can you post this? I want to know if you have a high FOV or not but other settings can matter too. The FPS project @ross.grams posted does have a high FOV too at 135.

https://www.decarpentier.nl/lens-distortion

I can post the new settings I’m using, I modified them too frequently since then to know what I used for that screenshot. Here’s what the game looks like (for that same area) now:

and here are my camera settings:

1 Like

It does look better now. It looks like the set FOV value is not being used in that case but I’d have to read the source again to be sure.