I figured out the mystery curve problem. The issue is, I'm not sure why it works...
Basically, the mystery curve was a measurement of error between what my recentering was actually doing, and what my recentering needed to be doing; it wasn't an additional offset at all. The actual ideal recentering is much simpler.
Code: Select all
var camcenter = (-Math.tan(((cam.pitch - 90) * Math.PI) / 360) * normalizedHeight) * (cam.y / normalizedHeight);
I'd like to retouch my guide, but first I have to wrap my head around what exactly this is accomplishing and why...
Anyway, here's the full script, with new recentering:
Code: Select all
var baseXOffset = 128;
var baseYOffset = 112;
var normalizedHeight = 112 * Math.sqrt(3);
var distanceToScale = 8/7;
var lerp = function(v0, v1, t) {
return v0 + t * (v1 - v0);
};
var rad = function(d) {
return d * Math.PI / 180;
};
var dist = function(cam, a) {
return cam.y / Math.cos(rad(a));
}
var texelSpan = function(cam, a) {
return Math.tan(rad(a)) * cam.y;
}
var getm7y = function(cam, topa, btma, neg) {
var i, ta, ba, td, bd;
ta = topa;
ba = btma;
td = dist(cam, ta);
bd = dist(cam, ba);
if (neg) {
i = texelSpan(cam, ta) + texelSpan(cam, ba);
}
else {
i = texelSpan(cam, ta) - texelSpan(cam, ba);
}
var dd = td - bd;
var ib = i - bd;
if (dd == 0) {
return 112;
}
else {
return ib / dd * 223;
}
}
var getModelViewMatrix = function(cam) {
var p = cam.pitch;
var w = 360 - cam.yaw;
var x = cam.x;
var y = cam.y;
var z = cam.z;
return [
[Math.cos(rad(w)), Math.sin(rad(p)) * Math.sin(rad(w)), -Math.sin(rad(w)) * Math.cos(rad(p)), 0],
[0, Math.cos(rad(p)), Math.sin(rad(p)), 0],
[-Math.sin(rad(w)), Math.sin(rad(p)) * Math.cos(rad(w)), -Math.cos(rad(p)) * Math.cos(rad(w)), 0],
[
x * -Math.cos(rad(w)) + z * Math.sin(rad(w)),
x * Math.sin(rad(p)) * -Math.sin(rad(w)) + y * -Math.cos(rad(p)) + z * Math.sin(rad(p)) * -Math.cos(rad(w)),
x * Math.sin(rad(w)) * Math.cos(rad(p)) + y * -Math.sin(rad(p)) + z * Math.cos(rad(p)) * Math.cos(rad(w)),
1
]
];
}
var getProjectionMatrix = function(cam) {
//far and near clipping planes, hardcoding because they're not really used otherwise
var n = 0.3;
var f = 10000;
return [
[(1/Math.tan((cam.fov/2)*(Math.PI/180)))/(8/7), 0, 0, 0],
[0, 1/Math.tan((cam.fov/2)*(Math.PI/180)), 0, 0],
[0, 0, -(f + n) / (f - n), -1],
[0, 0, -2 * n * f / (f - n), 0]
];
}
var pointTimesMatrix = function(p, m) {
return [
m[0][0] * p[0] + m[1][0] * p[1] + m[2][0] * p[2] + m[3][0] * p[3],
m[0][1] * p[0] + m[1][1] * p[1] + m[2][1] * p[2] + m[3][1] * p[3],
m[0][2] * p[0] + m[1][2] * p[1] + m[2][2] * p[2] + m[3][2] * p[3],
m[0][3] * p[0] + m[1][3] * p[1] + m[2][3] * p[2] + m[3][3] * p[3]
];
}
var normalizePoint = function(p) {
return [p[0]/p[3], p[1]/p[3], p[2]/p[3], p[3]/p[3]];
}
var calcPlane = function(cam) {
//prep (per-frame)
var da = 90 - cam.pitch;
var hfov = cam.fov / 2;
var topa = da + hfov;
var btma = da - hfov;
var negedge = btma < 0;
btma = Math.abs(btma);
//centering offset (per-frame)
var lineoffs = getm7y(cam, topa, btma, negedge);
//rectangle(125, lineoffs-3, 6, 6, "green", true)
//texel recentering (per-frame)
topa = Math.abs(topa);
var camcenter = (-Math.tan(((cam.pitch - 90) * Math.PI) / 360) * normalizedHeight) * (cam.y / normalizedHeight);
var voffscentercomp = Math.cos(cam.yaw/180 * Math.PI) * camcenter;
var hoffscentercomp = -Math.sin(cam.yaw/180 * Math.PI) * camcenter;
//rotation (per-frame)
var a = rad(cam.yaw);
//scale part 1 (per-frame)
var topdist = dist(cam, topa);
var btmdist = dist(cam, btma);
//scale part 2 (per-scanline)
var sl = lerp(1/topdist, 1/btmdist, scanline / 223);
var scale = (1/sl) * distanceToScale;
var ret = {};
ret.sl = sl;
ret.m7a = Math.cos(a) * scale;
ret.m7b = Math.sin(a) * scale;
ret.m7c = -Math.sin(a) * scale;
ret.m7d = Math.cos(a) * scale;
ret.m7hofs = -baseXOffset + hoffscentercomp + cam.x;
ret.m7vofs = -baseYOffset + (112 - lineoffs) - voffscentercomp - cam.z;
ret.m7x = 128 + m7hofs;
ret.m7y = m7vofs + lineoffs;
return ret;
}
var groundCam = {
x: Math.sin(((framecount / 10) % 256) / 256 * 2 * Math.PI) * 512,
y: normalizedHeight + var2 * 5,
z: Math.cos(((framecount / 10) % 256) / 256 * 2 * Math.PI) * 512,
fov: 60,
pitch: var1,
yaw: 360 - var3 * 5,
}
//Do sprite transforms
var objs = [
{
x: 0, y: 0, z: 0
},
{
x: 50, y: Math.abs(Math.sin(framecount / 30)) * 50, z: 50
},
{
x: 100, y: Math.abs(Math.sin(framecount / 30)) * 100, z: 100
}
]
for (var e in objs) {
var cur = objs[e];
var matmv = getModelViewMatrix(groundCam);
var matp = getProjectionMatrix(groundCam);
var intermediary = pointTimesMatrix([cur.x, cur.y, cur.z, 1], matmv);
var transformed = pointTimesMatrix(intermediary, matp);
var normalized = normalizePoint(transformed);
cur.ny = 112 + normalized[1] * 112;
if (normalized[2] < 1) {
var sprxpos = 128 + normalized[0] * 128;
var sprypos = 112 + normalized[1] * 112;
rectangle(sprxpos - 3, 224 - sprypos - 3, 6, 6, "green", true);
}
}
//Do plane effect
var gp = calcPlane(groundCam);
if (gp.sl > 0) {
m7a = gp.m7a;
m7b = gp.m7b;
m7c = gp.m7c;
m7d = gp.m7d;
m7x = gp.m7x;
m7y = gp.m7y;
m7hofs = gp.m7hofs;
m7vofs = gp.m7vofs;
}
else {
m7a = 0x0000;
m7b = 0x0000;
m7c = 0x0000;
m7d = 0x0000;
m7x = 128;
m7y = 112;
m7hofs = 0;
m7vofs = 0;
}
return [m7a, m7b, m7c, m7d, m7x, m7y, m7hofs, m7vofs];
And some back-and-forth comparisons of Unity vs. my script:
Again, this is an "ideal" script, not optimized in any way. I'm working on what can be simplified into LUTs for the SNES, and I think I have an idea of how this whole thing can be done with absolutely zero actual per-line multiplies or divides, without compromising camera pitch/yaw/height control...still working on that though!