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.