From d57056e8a0e0cf06818907cab5ff4e9902fc070f Mon Sep 17 00:00:00 2001 From: XProger Date: Sun, 16 Jul 2017 05:21:48 +0300 Subject: [PATCH 1/7] #23 trapezoidal texturing --- src/format.h | 3 +- src/lara.h | 6 +-- src/level.h | 6 +++ src/mesh.h | 114 +++++++++++++++++++++++++++++++--------- src/shader.h | 3 +- src/shaders/gui.glsl | 4 +- src/shaders/shader.glsl | 82 ++++++++++++++++------------- src/utils.h | 19 +++++-- 8 files changed, 163 insertions(+), 74 deletions(-) diff --git a/src/format.h b/src/format.h index 625694a..cd57665 100644 --- a/src/format.h +++ b/src/format.h @@ -412,7 +412,8 @@ namespace TR { struct Vertex { int16 x, y, z; - operator vec3() const { return vec3((float)x, (float)y, (float)z); }; + operator vec3() const { return vec3((float)x, (float)y, (float)z); } + operator short3() const { return *((short3*)this); } }; struct Rectangle { diff --git a/src/lara.h b/src/lara.h index 2f2a92b..d023af4 100644 --- a/src/lara.h +++ b/src/lara.h @@ -425,10 +425,8 @@ struct Lara : Character { if (level->extra.braid > -1) braid = new Braid(this, vec3(-4.0f, 24.0f, -48.0f)); - #ifdef _DEBUG - //reset(14, vec3(40448, 3584, 60928), PI * 0.5f, true); // gym (pool) - + //reset(14, vec3(40448, 3584, 60928), PI * 0.5f, STAND_ONWATER); // gym (pool) //reset(14, vec3(20215, 6656, 52942), PI); // level 1 (bridge) //reset(15, vec3(70067, -256, 29104), -0.68f); // level 2 (pool) //reset(61, vec3(27221, -1024, 29205), PI * 0.5f); // level 2 (blade) @@ -1562,7 +1560,7 @@ struct Lara : Character { if (getRoom().flags.water) { wpnHide(); - if (stand != STAND_UNDERWATER && (state != STATE_FALL && state != STATE_FALL_BACK && state != STATE_SWAN_DIVE && state != STATE_FAST_DIVE)) + if (stand != STAND_UNDERWATER && stand != STAND_ONWATER && (state != STATE_FALL && state != STATE_FALL_BACK && state != STATE_SWAN_DIVE && state != STATE_FAST_DIVE)) animation.setAnim(ANIM_FALL_FORTH); return STAND_UNDERWATER; } diff --git a/src/level.h b/src/level.h index 4253331..59065f0 100644 --- a/src/level.h +++ b/src/level.h @@ -411,6 +411,12 @@ struct Level : IGame { fwrite(data, 1024 * 1024 * 4, 1, f); fclose(f); */ + /* + memset(data, 255, 4 * 1024 * 1024); + for (int i = 0; i < 1024; i++) + for (int j = 0; j < 1024; j++) + data[i * 1024 + j].b = data[i * 1024 + j].g = ((i % 8 == 0) || (j % 8 == 0)) ? 0 : 255; + */ atlas = new Texture(1024, 1024, Texture::RGBA, false, data); PROFILE_LABEL(TEXTURE, atlas->ID, "atlas"); diff --git a/src/mesh.h b/src/mesh.h index e607baf..69fe8a8 100644 --- a/src/mesh.h +++ b/src/mesh.h @@ -13,9 +13,10 @@ typedef unsigned short Index; struct Vertex { - short4 coord; // xyz - position, w - unused - short4 texCoord; // xy - texture coordinates, z - anim tex range index, w - anim tex frame index + short4 coord; // xyz - position, w - joint index (for entities only) short4 normal; // xyz - vertex normalá w - unused + short4 texCoord; // xy - texture coordinates, zw - trapezoid warping + ubyte4 param; // xy - anim tex range and frame index, zw - unused ubyte4 color; // xyz - color, w - intensity }; @@ -29,14 +30,16 @@ struct MeshRange { void setup() const { glEnableVertexAttribArray(aCoord); - glEnableVertexAttribArray(aTexCoord); glEnableVertexAttribArray(aNormal); + glEnableVertexAttribArray(aTexCoord); + glEnableVertexAttribArray(aParam); glEnableVertexAttribArray(aColor); Vertex *v = (Vertex*)NULL + vStart; glVertexAttribPointer(aCoord, 4, GL_SHORT, false, sizeof(Vertex), &v->coord); - glVertexAttribPointer(aTexCoord, 4, GL_SHORT, false, sizeof(Vertex), &v->texCoord); glVertexAttribPointer(aNormal, 4, GL_SHORT, true, sizeof(Vertex), &v->normal); + glVertexAttribPointer(aTexCoord, 4, GL_SHORT, true, sizeof(Vertex), &v->texCoord); + glVertexAttribPointer(aParam, 4, GL_UNSIGNED_BYTE, false, sizeof(Vertex), &v->param); glVertexAttribPointer(aColor, 4, GL_UNSIGNED_BYTE, true, sizeof(Vertex), &v->color); } @@ -234,7 +237,7 @@ struct MeshBuilder { if (Core::settings.detail.water) roomRemoveWaterSurfaces(r, iCount, vCount); - + for (int j = 0; j < r.meshesCount; j++) { TR::Room::Mesh &m = r.meshes[j]; TR::StaticMesh *s = &level.staticMeshes[m.meshIndex]; @@ -402,8 +405,9 @@ struct MeshBuilder { // build shadow blob for (int i = 0; i < 9; i++) { Vertex &v0 = vertices[vCount + i * 2 + 0]; - v0.normal = { 0, -1, 0, 1 }; - v0.texCoord = { 32688, 32688, 0, 0 }; + v0.normal = { 0, -1, 0, 0 }; + v0.texCoord = { 32688, 32688, 32767, 32767 }; + v0.param = { 0, 0, 0, 0 }; v0.color = { 0, 0, 0, 0 }; if (i == 8) { @@ -507,7 +511,8 @@ struct MeshBuilder { Vertex &v = vertices[vCount + i]; v.normal = { 0, 0, 0, 0 }; v.color = { 255, 255, 255, 255 }; - v.texCoord = { 32688, 32688, 0, 0 }; + v.texCoord = { 32688, 32688, 32767, 32767 }; + v.param = { 0, 0, 0, 0 }; } vCount += 4; aCount++; @@ -522,7 +527,8 @@ struct MeshBuilder { v.coord = { short(pos.x), short(pos.y), 0, 0 }; v.normal = { 0, 0, 0, 0 }; v.color = { 255, 255, 255, 255 }; - v.texCoord = { 32688, 32688, 0, 0 }; + v.texCoord = { 32688, 32688, 32767, 32767 }; + v.param = { 0, 0, 0, 0 }; indices[iCount++] = i; indices[iCount++] = (i + 1) % CIRCLE_SEGS; @@ -700,8 +706,11 @@ struct MeshBuilder { if (opaque != (t.attribute == 0)) continue; - - addQuad(indices, iCount, vCount, vStart, vertices, &t); + addQuad(indices, iCount, vCount, vStart, vertices, &t, + d.vertices[f.vertices[0]].vertex, + d.vertices[f.vertices[1]].vertex, + d.vertices[f.vertices[2]].vertex, + d.vertices[f.vertices[3]].vertex); TR::Vertex n; CHECK_ROOM_NORMAL(n); @@ -761,7 +770,11 @@ struct MeshBuilder { TR::Color24 c = textured ? COLOR_WHITE : level.getColor(f.texture); - addQuad(indices, iCount, vCount, vStart, vertices, &t); + addQuad(indices, iCount, vCount, vStart, vertices, &t, + mesh.vertices[f.vertices[0]].coord, + mesh.vertices[f.vertices[1]].coord, + mesh.vertices[f.vertices[2]].coord, + mesh.vertices[f.vertices[3]].coord); for (int k = 0; k < 4; k++) { TR::Mesh::Vertex &v = mesh.vertices[f.vertices[k]]; @@ -808,7 +821,7 @@ struct MeshBuilder { int tx = (tile % 4) * 256; int ty = (tile / 4) * 256; return vec2( (float)(((tx + tex.texCoord[0].x) << 5) + 16), - (float)(((ty + tex.texCoord[0].y) << 5) + 16) ); + (float)(((ty + tex.texCoord[0].y) << 5) + 16) ) * (1.0f / 32767.0f); } void initAnimTextures(TR::Level &level) { @@ -846,7 +859,7 @@ struct MeshBuilder { } } - TR::ObjectTexture* getAnimTexture(TR::ObjectTexture *tex, int &range, int &frame) { + TR::ObjectTexture* getAnimTexture(TR::ObjectTexture *tex, uint8 &range, uint8 &frame) { range = frame = 0; if (!level->animTexturesDataSize) return tex; @@ -869,28 +882,41 @@ struct MeshBuilder { } void addTexCoord(Vertex *vertices, int vCount, TR::ObjectTexture *tex, bool triangle) { - int range, frame; + uint8 range, frame; tex = getAnimTexture(tex, range, frame); int tile = tex->tile.index; int tx = (tile % 4) * 256; int ty = (tile / 4) * 256; + int count = triangle ? 3 : 4; for (int i = 0; i < count; i++) { Vertex &v = vertices[vCount + i]; v.texCoord.x = ((tx + tex->texCoord[i].x) << 5) + 16; v.texCoord.y = ((ty + tex->texCoord[i].y) << 5) + 16; - v.texCoord.z = range; - v.texCoord.w = frame; + v.texCoord.z = 32767; + v.texCoord.w = 32767; + v.param = { range, frame, 0, 0 }; } if (level->version == TR::Level::VER_TR1_PSX && !triangle) swap(vertices[vCount + 2].texCoord, vertices[vCount + 3].texCoord); + /* + short2 uv[4] = { + {0, 0}, {32767, 0}, {32767, 32767}, {0, 32767} + }; + + for (int i = 0; i < count; i++) { + Vertex &v = vertices[vCount + i]; + v.texCoord.x = uv[i].x; + v.texCoord.y = uv[i].y; + } + */ } void addTriangle(Index *indices, int &iCount, int vCount, int vStart, Vertex *vertices, TR::ObjectTexture *tex) { - int vIndex = vCount - vStart; + int vIndex = vCount - vStart; indices[iCount + 0] = vIndex + 0; indices[iCount + 1] = vIndex + 1; @@ -902,7 +928,7 @@ struct MeshBuilder { } void addQuad(Index *indices, int &iCount, int vCount, int vStart, Vertex *vertices, TR::ObjectTexture *tex) { - int vIndex = vCount - vStart; + int vIndex = vCount - vStart; indices[iCount + 0] = vIndex + 0; indices[iCount + 1] = vIndex + 1; @@ -917,6 +943,43 @@ struct MeshBuilder { if (tex) addTexCoord(vertices, vCount, tex, false); } + void addQuad(Index *indices, int &iCount, int &vCount, int vStart, Vertex *vertices, TR::ObjectTexture *tex, + const short3 &c0, const short3 &c1, const short3 &c2, const short3 &c3) { + addQuad(indices, iCount, vCount, vStart, vertices, tex); + + vec3 a = c0 - c1; + vec3 b = c3 - c2; + vec3 c = c0 - c3; + vec3 d = c1 - c2; + + float aL = a.length(); + float bL = b.length(); + float cL = c.length(); + float dL = d.length(); + + float ab = a.dot(b) / (aL * bL); + float cd = c.dot(d) / (cL * dL); + + int16 tx = abs(vertices[vCount + 0].texCoord.x - vertices[vCount + 3].texCoord.x); + int16 ty = abs(vertices[vCount + 0].texCoord.y - vertices[vCount + 3].texCoord.y); + + if (ab > cd) { + int k = (tx > ty) ? 3 : 2; + + if (aL > bL) + vertices[vCount + 2].texCoord[k] = vertices[vCount + 3].texCoord[k] = int16(bL / aL * 32767.0f); + else + vertices[vCount + 0].texCoord[k] = vertices[vCount + 1].texCoord[k] = int16(aL / bL * 32767.0f); + } else { + int k = (tx > ty) ? 2 : 3; + + if (cL > dL) { + vertices[vCount + 1].texCoord[k] = vertices[vCount + 2].texCoord[k] = int16(dL / cL * 32767.0f); + } else + vertices[vCount + 0].texCoord[k] = vertices[vCount + 3].texCoord[k] = int16(cL / dL * 32767.0f); + } + } + void addSprite(Index *indices, Vertex *vertices, int &iCount, int &vCount, int vStart, int16 x, int16 y, int16 z, const TR::SpriteTexture &sprite, uint8 intensity, bool expand = false) { addQuad(indices, iCount, vCount, vStart, NULL, NULL); @@ -939,12 +1002,12 @@ struct MeshBuilder { quad[2].coord = { x1, y1, z, 0 }; quad[3].coord = { x0, y1, z, 0 }; - quad[0].normal = quad[1].normal = quad[2].normal = quad[3].normal = { 0, 0, 0, 0 }; quad[0].color = quad[1].color = quad[2].color = quad[3].color = { 255, 255, 255, intensity }; + quad[0].param = quad[1].param = quad[2].param = quad[3].param = { 0, 0, 0, 0 }; - int tx = (sprite.tile % 4) * 256; - int ty = (sprite.tile / 4) * 256; + int tx = (sprite.tile % 4) * 256; + int ty = (sprite.tile / 4) * 256; int16 u0 = (((tx + sprite.texCoord[0].x) << 5)); int16 v0 = (((ty + sprite.texCoord[0].y) << 5)); @@ -992,7 +1055,8 @@ struct MeshBuilder { s = int(s) * 32767 / 1024; t = int(t) * 32767 / 1024; - v.texCoord = { s, t, 0, 0 }; + v.texCoord = { s, t, 32767, 32767 }; + v.param = { 0, 0, 0, 0 }; } vCount += 4; @@ -1018,7 +1082,7 @@ struct MeshBuilder { Vertex &v = vertices[vCount + i]; v.normal = { 0, 0, 0, 0 }; v.color = *((ubyte4*)&color1); - v.texCoord = { 32688, 32688, 0, 0 }; + v.texCoord = { 32688, 32688, 32767, 32767 }; } addQuad(indices, iCount, vCount, 0, vertices, NULL); vCount += 4; @@ -1038,7 +1102,7 @@ struct MeshBuilder { Vertex &v = vertices[vCount + i]; v.normal = { 0, 0, 0, 0 }; v.color = *((ubyte4*)&color2); - v.texCoord = { 32688, 32688, 0, 0 }; + v.texCoord = { 32688, 32688, 32767, 32767 }; } addQuad(indices, iCount, vCount, 0, vertices, NULL); vCount += 4; diff --git a/src/shader.h b/src/shader.h index 1882603..ffeb37a 100644 --- a/src/shader.h +++ b/src/shader.h @@ -5,8 +5,9 @@ #define SHADER_ATTRIBS(E) \ E( aCoord ) \ - E( aTexCoord ) \ E( aNormal ) \ + E( aTexCoord ) \ + E( aParam ) \ E( aColor ) #define SHADER_SAMPLERS(E) \ diff --git a/src/shaders/gui.glsl b/src/shaders/gui.glsl index 4764b7c..5988d58 100644 --- a/src/shaders/gui.glsl +++ b/src/shaders/gui.glsl @@ -16,10 +16,8 @@ varying vec4 vColor; attribute vec4 aTexCoord; attribute vec4 aColor; - #define TEXCOORD_SCALE (1.0 / 32767.0) - void main() { - vTexCoord = aTexCoord.xy * TEXCOORD_SCALE; + vTexCoord = aTexCoord.xy; vColor = aColor * uMaterial; gl_Position = uViewProj * vec4(aCoord.xy * uPosScale.zw + uPosScale.xy, 0.0, 1.0); } diff --git a/src/shaders/shader.glsl b/src/shaders/shader.glsl index 4345cf3..2bbeac9 100644 --- a/src/shaders/shader.glsl +++ b/src/shaders/shader.glsl @@ -4,19 +4,23 @@ R"====( precision highp float; #endif -varying vec4 vTexCoord; // xy - atlas coords, zw - caustics coords +varying vec4 vTexCoord; // xy - atlas coords, zw - trapezoidal correction + +#if defined(OPT_WATER) && defined(UNDERWATER) + varying vec2 vCausticsCoord; // - xy caustics texture coord +#endif #ifndef PASS_SHADOW - varying vec4 vViewVec; // xyz - dir * dist, w - coord.y uniform vec3 uViewPos; + varying vec4 vViewVec; // xyz - dir * dist, w - coord.y varying vec4 vDiffuse; #ifndef TYPE_FLASH #ifdef PASS_COMPOSE - varying vec4 vNormal; // xyz - normal dir, w - fog factor + varying vec3 vNormal; // xyz - normal dir varying vec4 vLightProj; - varying vec3 vLightVec; + varying vec4 vLightVec; // xyz - dir, w - fog factor #ifdef OPT_SHADOW varying vec3 vAmbient; @@ -26,7 +30,7 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - caustics coords uniform vec4 uLightColor[4]; // xyz - color, w - radius * intensity #endif - varying vec4 vLight; // 4 lights intensity + varying vec4 vLight; // 4 lights intensity #if defined(OPT_WATER) && defined(UNDERWATER) uniform vec4 uRoomSize; // xy - minXZ, zw - maxXZ @@ -38,15 +42,15 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - caustics coords vec3 calcAmbient(vec3 n) { vec3 sqr = n * n; vec3 pos = step(0.0, n); - return sqr.x * mix(uAmbient[1], uAmbient[0], pos.x) + - sqr.y * mix(uAmbient[3], uAmbient[2], pos.y) + - sqr.z * mix(uAmbient[5], uAmbient[4], pos.z); + return sqr.x * mix(uAmbient[1], uAmbient[0], pos.x) + + sqr.y * mix(uAmbient[3], uAmbient[2], pos.y) + + sqr.z * mix(uAmbient[5], uAmbient[4], pos.z); } #endif #endif - uniform vec4 uParam; // x - time, y - water height, z - clip plane sign, w - clip plane height - uniform vec4 uMaterial; // x - diffuse, y - ambient, z - specular, w - alpha + uniform vec4 uParam; // x - time, y - water height, z - clip plane sign, w - clip plane height + uniform vec4 uMaterial; // x - diffuse, y - ambient, z - specular, w - alpha #endif #ifdef VERTEX @@ -71,6 +75,7 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - caustics coords attribute vec4 aCoord; attribute vec4 aTexCoord; + attribute vec4 aParam; #ifndef PASS_AMBIENT attribute vec4 aNormal; @@ -80,7 +85,7 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - caustics coords attribute vec4 aColor; #endif - #define TEXCOORD_SCALE (1.0 / 32767.0) + #define TEXCOORD_SCALE 32767.0 vec3 mulQuat(vec4 q, vec3 v) { return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + v * q.w); @@ -103,7 +108,7 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - caustics coords vec4 coord = vec4(mulBasis(rBasisRot, rBasisPos, aCoord.xyz), rBasisPos.w); #ifdef TYPE_SPRITE - coord.xyz += uViewInv[0].xyz * aTexCoord.z - uViewInv[1].xyz * aTexCoord.w; + coord.xyz += (uViewInv[0].xyz * aTexCoord.z - uViewInv[1].xyz * aTexCoord.w) * TEXCOORD_SCALE; #endif #ifndef PASS_SHADOW @@ -129,7 +134,7 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - caustics coords fog = length(vViewVec.xyz); #endif - vNormal.w = clamp(1.0 / exp(fog), 0.0, 1.0); + vLightVec.w = clamp(1.0 / exp(fog), 0.0, 1.0); #endif return coord; @@ -161,22 +166,24 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - caustics coords vec3 lv2 = (uLightPos[2].xyz - coord) * uLightColor[2].w; vec3 lv3 = (uLightPos[3].xyz - coord) * uLightColor[3].w; - vLightVec = lv0; + vLightVec.xyz = lv0; vec4 lum, att; #ifdef TYPE_ENTITY - lum.x = dot(vNormal.xyz, normalize(lv0)); att.x = dot(lv0, lv0); + lum.x = dot(vNormal.xyz, normalize(lv0)); + att.x = dot(lv0, lv0); #else - lum.x = aColor.w; att.x = 0.0; + lum.x = aColor.w; + att.x = 0.0; #ifdef TYPE_SPRITE lum.x *= uMaterial.y; #endif #endif - lum.y = dot(vNormal.xyz, normalize(lv1)); att.y = dot(lv1, lv1); - lum.z = dot(vNormal.xyz, normalize(lv2)); att.z = dot(lv2, lv2); - lum.w = dot(vNormal.xyz, normalize(lv3)); att.w = dot(lv3, lv3); + lum.y = dot(vNormal.xyz, normalize(lv1)); att.y = dot(lv1, lv1); + lum.z = dot(vNormal.xyz, normalize(lv2)); att.z = dot(lv2, lv2); + lum.w = dot(vNormal.xyz, normalize(lv3)); att.w = dot(lv3, lv3); vec4 light = max(vec4(0.0), lum) * max(vec4(0.0), vec4(1.0) - att); #ifdef UNDERWATER @@ -219,20 +226,18 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - caustics coords } void _uv(vec3 coord) { + vTexCoord = aTexCoord; #if defined(PASS_COMPOSE) && !defined(TYPE_SPRITE) // animated texture coordinates - vec2 range = uAnimTexRanges[int(aTexCoord.z)]; // x - start index, y - count - float frame = fract((aTexCoord.w + uParam.x * 4.0 - range.x) / range.y) * range.y; - vec2 offset = uAnimTexOffsets[int(range.x + frame)]; // texCoord offset from first frame - vTexCoord.xy = (aTexCoord.xy + offset) * TEXCOORD_SCALE; - #else - vTexCoord.xy = aTexCoord.xy * TEXCOORD_SCALE; + vec2 range = uAnimTexRanges[int(aParam.x)]; // x - start index, y - count + float frame = fract((aParam.y + uParam.x * 4.0 - range.x) / range.y) * range.y; + vec2 offset = uAnimTexOffsets[int(range.x + frame)]; // texCoord offset from first frame + vTexCoord.xy += offset; + vTexCoord.xy *= vTexCoord.zw; #endif #if defined(OPT_WATER) && defined(UNDERWATER) - vTexCoord.zw = clamp((coord.xz - uRoomSize.xy) / (uRoomSize.zw - uRoomSize.xy), vec2(0.0), vec2(1.0)); - #else - vTexCoord.zw = vec2(1.0); + vCausticsCoord.xy = clamp((coord.xz - uRoomSize.xy) / (uRoomSize.zw - uRoomSize.xy), vec2(0.0), vec2(1.0)); #endif } @@ -346,13 +351,13 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - caustics coords } else rShadow /= 4.0; - float fade = clamp(dot(vLightVec, vLightVec), 0.0, 1.0); + float fade = clamp(dot(vLightVec.xyz, vLightVec.xyz), 0.0, 1.0); return rShadow + (1.0 - rShadow) * fade; } float getShadow() { #ifdef TYPE_ROOM - float vis = min(dot(vNormal.xyz, vLightVec), vLightProj.w); + float vis = min(dot(vNormal.xyz, vLightVec.xyz), vLightProj.w); #else float vis = vLightProj.w; #endif @@ -369,9 +374,10 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - caustics coords #if defined(OPT_WATER) && defined(UNDERWATER) float calcCaustics(vec3 n) { + vec2 cc = vCausticsCoord.xy; vec2 border = vec2(256.0) / (uRoomSize.zw - uRoomSize.xy); - vec2 fade = smoothstep(vec2(0.0), border, vTexCoord.zw) * (1.0 - smoothstep(vec2(1.0) - border, vec2(1.0), vTexCoord.zw)); - return texture2D(sReflect, vTexCoord.zw).g * max(0.0, -n.y) * fade.x * fade.y; + vec2 fade = smoothstep(vec2(0.0), border, cc) * (1.0 - smoothstep(vec2(1.0) - border, vec2(1.0), cc)); + return texture2D(sReflect, cc).g * max(0.0, -n.y) * fade.x * fade.y; } #endif #endif @@ -391,7 +397,11 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - caustics coords color = textureCube(sEnvironment, normalize(rv)); #endif #else - color = texture2D(sDiffuse, vTexCoord.xy); + #if defined(PASS_COMPOSE) && !defined(TYPE_SPRITE) + color = texture2D(sDiffuse, vTexCoord.xy / vTexCoord.zw); + #else + color = texture2D(sDiffuse, vTexCoord.xy); + #endif #endif #ifdef ALPHA_TEST @@ -444,7 +454,7 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - caustics coords #endif #ifdef TYPE_ENTITY - color.xyz += calcSpecular(n, vViewVec.xyz, vLightVec, uLightColor[0], uMaterial.z * rShadow + 0.03); + color.xyz += calcSpecular(n, vViewVec.xyz, vLightVec.xyz, uLightColor[0], uMaterial.z * rShadow + 0.03); #endif #endif @@ -460,9 +470,9 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - caustics coords #if defined(PASS_COMPOSE) && !defined(TYPE_FLASH) #ifdef UNDERWATER - color.xyz = mix(UNDERWATER_COLOR * 0.2, color.xyz, vNormal.w); + color.xyz = mix(UNDERWATER_COLOR * 0.2, color.xyz, vLightVec.w); #else - color.xyz = mix(vec3(0.0), color.xyz, vNormal.w); + color.xyz = mix(vec3(0.0), color.xyz, vLightVec.w); #endif #endif diff --git a/src/utils.h b/src/utils.h index d55b9a7..7aaa4ee 100644 --- a/src/utils.h +++ b/src/utils.h @@ -140,9 +140,9 @@ int nextPow2(uint32 x) { } uint32 fnv32(const char *data, int32 size, uint32 hash = 0x811c9dc5) { - for (int i = 0; i < size; i++) - hash = (hash ^ data[i]) * 0x01000193; - return hash; + for (int i = 0; i < size; i++) + hash = (hash ^ data[i]) * 0x01000193; + return hash; } struct vec2 { @@ -711,12 +711,23 @@ struct short2 { struct short3 { int16 x, y, z; + + short3() {} + short3(int16 x, int16 y, int16 z) : x(x), y(y), z(z) {} + + operator vec3() const { return vec3((float)x, (float)y, (float)z); }; + + short3 operator + (const short3 &v) const { return short3(x + v.x, y + v.y, z + v.z); } + short3 operator - (const short3 &v) const { return short3(x - v.x, y - v.y, z - v.z); } }; struct short4 { int16 x, y, z, w; - operator vec3() const { return vec3((float)x, (float)y, (float)z); }; + operator vec3() const { return vec3((float)x, (float)y, (float)z); }; + operator short3() const { return *((short3*)this); } + + inline int16& operator [] (int index) const { ASSERT(index >= 0 && index <= 3); return ((int16*)this)[index]; } }; quat rotYXZ(const vec3 &a) { From 8e60fe17a2e021156ad5ba0b2ec53b731f6a68d0 Mon Sep 17 00:00:00 2001 From: XProger Date: Sun, 16 Jul 2017 05:27:03 +0300 Subject: [PATCH 2/7] #3 remap jump key from ALT to D (for Firefox and IE support) --- src/input.h | 2 +- src/shaders/water.glsl | 270 ++++++++++++++++++++--------------------- src/ui.h | 2 +- 3 files changed, 137 insertions(+), 137 deletions(-) diff --git a/src/input.h b/src/input.h index 7b7de9f..f6b436b 100644 --- a/src/input.h +++ b/src/input.h @@ -33,7 +33,7 @@ namespace Input { { ikRight, ikJoyRight }, { ikUp, ikJoyUp }, { ikDown, ikJoyDown }, - { ikAlt, ikJoyX }, + { ikD, ikJoyX }, { ikShift, ikJoyRB }, { ikCtrl, ikJoyA }, { ikSpace, ikJoyY }, diff --git a/src/shaders/water.glsl b/src/shaders/water.glsl index c3db46e..8418596 100644 --- a/src/shaders/water.glsl +++ b/src/shaders/water.glsl @@ -1,7 +1,7 @@ R"====( #ifdef GL_ES - precision lowp int; - precision highp float; + precision lowp int; + precision highp float; #endif varying vec3 vCoord; @@ -23,181 +23,181 @@ uniform vec4 uParam; uniform sampler2D sNormal; #ifdef VERTEX - #define ETA_AIR 1.000 - #define ETA_WATER 1.333 + #define ETA_AIR 1.000 + #define ETA_WATER 1.333 - attribute vec4 aCoord; + attribute vec4 aCoord; - void main() { - vTexCoord = (aCoord.xy * 0.5 + 0.5) * uTexParam.zw; + void main() { + vTexCoord = (aCoord.xy * 0.5 + 0.5) * uTexParam.zw; - #if defined(WATER_MASK) || defined(WATER_COMPOSE) + #if defined(WATER_MASK) || defined(WATER_COMPOSE) - float height = 0.0; + float height = 0.0; - #ifdef WATER_COMPOSE - #ifdef WATER_USE_GRID - vTexCoord = (aCoord.xy * (1.0 / 48.0) * 0.5 + 0.5) * uTexParam.zw; - height = texture2D(sNormal, vTexCoord).x; - #endif - #endif + #ifdef WATER_COMPOSE + #ifdef WATER_USE_GRID + vTexCoord = (aCoord.xy * (1.0 / 48.0) * 0.5 + 0.5) * uTexParam.zw; + height = texture2D(sNormal, vTexCoord).x; + #endif + #endif - vCoord = vec3(aCoord.x, height, aCoord.y) * uPosScale[1] + uPosScale[0]; + vCoord = vec3(aCoord.x, height, aCoord.y) * uPosScale[1] + uPosScale[0]; - vec4 cp = uViewProj * vec4(vCoord, 1.0); + vec4 cp = uViewProj * vec4(vCoord, 1.0); - vProjCoord = cp; - gl_Position = cp; - #else - vProjCoord = vec4(0.0); - vCoord = vec3(aCoord.xy, 0.0); - #ifdef WATER_CAUSTICS - vec3 rCoord = vec3(aCoord.x, aCoord.y, 0.0) * uPosScale[1].xzy; + vProjCoord = cp; + gl_Position = cp; + #else + vProjCoord = vec4(0.0); + vCoord = vec3(aCoord.xy, 0.0); + #ifdef WATER_CAUSTICS + vec3 rCoord = vec3(aCoord.x, aCoord.y, 0.0) * uPosScale[1].xzy; - vec4 info = texture2D(sNormal, (rCoord.xy * 0.5 + 0.5) * uTexParam.zw); - vec3 normal = vec3(info.z, info.w, sqrt(1.0 - dot(info.zw, info.zw))); + vec4 info = texture2D(sNormal, (rCoord.xy * 0.5 + 0.5) * uTexParam.zw); + vec3 normal = vec3(info.z, info.w, sqrt(1.0 - dot(info.zw, info.zw))); - vec3 light = vec3(0.0, 0.0, 1.0); - vec3 refOld = refract(-light, vec3(0.0, 0.0, 1.0), 0.75); - vec3 refNew = refract(-light, normal, 0.75); - - vOldPos = vec4(rCoord + refOld * (-1.0 / refOld.z) + refOld * ((-refOld.z - 1.0) / refOld.z), 1.0); - vNewPos = vec4(rCoord + refNew * ((info.r - 1.0) / refNew.z) + refOld * ((-refNew.z - 1.0) / refOld.z), 1.0); - - gl_Position = vec4(vNewPos.xy + refOld.xy / refOld.z, 0.0, 1.0); - #else - vOldPos = vNewPos = vec4(0.0); - gl_Position = vec4(aCoord.xyz, 1.0); - #endif - #endif - vViewVec = uViewPos - vCoord.xyz; - vLightVec = uLightPos - vCoord.xyz; - } + vec3 light = vec3(0.0, 0.0, 1.0); + vec3 refOld = refract(-light, vec3(0.0, 0.0, 1.0), 0.75); + vec3 refNew = refract(-light, normal, 0.75); + + vOldPos = vec4(rCoord + refOld * (-1.0 / refOld.z) + refOld * ((-refOld.z - 1.0) / refOld.z), 1.0); + vNewPos = vec4(rCoord + refNew * ((info.r - 1.0) / refNew.z) + refOld * ((-refNew.z - 1.0) / refOld.z), 1.0); + + gl_Position = vec4(vNewPos.xy + refOld.xy / refOld.z, 0.0, 1.0); + #else + vOldPos = vNewPos = vec4(0.0); + gl_Position = vec4(aCoord.xyz, 1.0); + #endif + #endif + vViewVec = uViewPos - vCoord.xyz; + vLightVec = uLightPos - vCoord.xyz; + } #else - uniform sampler2D sDiffuse; - uniform sampler2D sReflect; - uniform sampler2D sMask; + uniform sampler2D sDiffuse; + uniform sampler2D sReflect; + uniform sampler2D sMask; - uniform vec4 uLightColor; + uniform vec4 uLightColor; - #define PI 3.141592653589793 + #define PI 3.141592653589793 - float calcFresnel(float NdotL, float fbias, float fpow) { - float f = 1.0 - abs(NdotL); - return clamp(fbias + (1.0 - fbias) * pow(f, fpow), 0.0, 1.0); - } + float calcFresnel(float NdotL, float fbias, float fpow) { + float f = 1.0 - abs(NdotL); + return clamp(fbias + (1.0 - fbias) * pow(f, fpow), 0.0, 1.0); + } - vec3 applyFog(vec3 color, vec3 fogColor, float factor) { - float fog = clamp(1.0 / exp(factor), 0.0, 1.0); - return mix(fogColor, color, fog); - } + vec3 applyFog(vec3 color, vec3 fogColor, float factor) { + float fog = clamp(1.0 / exp(factor), 0.0, 1.0); + return mix(fogColor, color, fog); + } - vec4 drop() { - vec2 tc = gl_FragCoord.xy * uTexParam.xy; - vec4 v = texture2D(sDiffuse, tc); + vec4 drop() { + vec2 tc = gl_FragCoord.xy * uTexParam.xy; + vec4 v = texture2D(sDiffuse, tc); - float drop = max(0.0, 1.0 - length(uParam.xy - gl_FragCoord.xy) / uParam.z); - drop = 0.5 - cos(drop * PI) * 0.5; - v.x += drop * uParam.w; + float drop = max(0.0, 1.0 - length(uParam.xy - gl_FragCoord.xy) / uParam.z); + drop = 0.5 - cos(drop * PI) * 0.5; + v.x += drop * uParam.w; - return v; - } + return v; + } - vec4 calc() { - vec2 tc = gl_FragCoord.xy * uTexParam.xy; + vec4 calc() { + vec2 tc = gl_FragCoord.xy * uTexParam.xy; - if (texture2D(sMask, tc).x < 0.5) - return vec4(0.0); + if (texture2D(sMask, tc).x < 0.5) + return vec4(0.0); - vec4 v = texture2D(sDiffuse, tc); // height, speed, normal.xz + vec4 v = texture2D(sDiffuse, tc); // height, speed, normal.xz - vec3 d = vec3(uTexParam.xy, 0.0); - vec4 f = vec4(texture2D(sDiffuse, tc + d.xz).x, texture2D(sDiffuse, tc + d.zy).x, - texture2D(sDiffuse, tc - d.xz).x, texture2D(sDiffuse, tc - d.zy).x); - float average = dot(f, vec4(0.25)); + vec3 d = vec3(uTexParam.xy, 0.0); + vec4 f = vec4(texture2D(sDiffuse, tc + d.xz).x, texture2D(sDiffuse, tc + d.zy).x, + texture2D(sDiffuse, tc - d.xz).x, texture2D(sDiffuse, tc - d.zy).x); + float average = dot(f, vec4(0.25)); - // normal - v.zw = normalize( vec3(f.x - f.z, 64.0 / (1024.0 * 2.0), f.y - f.w) ).xz; + // normal + v.zw = normalize( vec3(f.x - f.z, 64.0 / (1024.0 * 2.0), f.y - f.w) ).xz; - // integrate - const float vel = 1.4; - const float vis = 0.995; + // integrate + const float vel = 1.4; + const float vis = 0.995; - v.y += (average - v.x) * vel; - v.y *= vis; - v.x += v.y; + v.y += (average - v.x) * vel; + v.y *= vis; + v.x += v.y; - return v; - } + return v; + } - vec4 caustics() { - float rOldArea = length(dFdx(vOldPos.xyz)) * length(dFdy(vOldPos.xyz)); - float rNewArea = length(dFdx(vNewPos.xyz)) * length(dFdy(vNewPos.xyz)); - float value = clamp(rOldArea / rNewArea * 0.2, 0.0, 1.0) * vOldPos.w; - return vec4(0.0, value, 0.0, 0.0); - } + vec4 caustics() { + float rOldArea = length(dFdx(vOldPos.xyz)) * length(dFdy(vOldPos.xyz)); + float rNewArea = length(dFdx(vNewPos.xyz)) * length(dFdy(vNewPos.xyz)); + float value = clamp(rOldArea / rNewArea * 0.2, 0.0, 1.0) * vOldPos.w; + return vec4(0.0, value, 0.0, 0.0); + } - vec4 mask() { - return vec4(0.0); - } + vec4 mask() { + return vec4(0.0); + } - vec4 compose() { - vec4 value = texture2D(sNormal, vTexCoord); + vec4 compose() { + vec4 value = texture2D(sNormal, vTexCoord); - vec3 normal = vec3(value.z, -sqrt(1.0 - dot(value.zw, value.zw)), value.w); + vec3 normal = vec3(value.z, -sqrt(1.0 - dot(value.zw, value.zw)), value.w); - vec2 dudv = (uViewProj * vec4(normal.x, 0.0, normal.z, 0.0)).xy; + vec2 dudv = (uViewProj * vec4(normal.x, 0.0, normal.z, 0.0)).xy; - vec3 viewVec = normalize(vViewVec); - vec3 rv = reflect(-viewVec, normal); - vec3 lv = normalize(vLightVec); + vec3 viewVec = normalize(vViewVec); + vec3 rv = reflect(-viewVec, normal); + vec3 lv = normalize(vLightVec); - float spec = pow(max(0.0, dot(rv, lv)), 64.0) * 0.5; - - vec2 tc = vProjCoord.xy / vProjCoord.w * 0.5 + 0.5; + float spec = pow(max(0.0, dot(rv, lv)), 64.0) * 0.5; - vec4 refrA = texture2D(sDiffuse, uParam.xy * clamp(tc + dudv * uParam.z, 0.0, 0.999) ); - vec4 refrB = texture2D(sDiffuse, uParam.xy * tc ); - vec4 refr = vec4(mix(refrA.xyz, refrB.xyz, refrA.w), 1.0); - vec4 refl = texture2D(sReflect, vec2(tc.x, 1.0 - tc.y) + dudv * uParam.w); + vec2 tc = vProjCoord.xy / vProjCoord.w * 0.5 + 0.5; - float fresnel = calcFresnel(dot(normal, viewVec), 0.1, 2.0); + vec4 refrA = texture2D(sDiffuse, uParam.xy * clamp(tc + dudv * uParam.z, 0.0, 0.999) ); + vec4 refrB = texture2D(sDiffuse, uParam.xy * tc ); + vec4 refr = vec4(mix(refrA.xyz, refrB.xyz, refrA.w), 1.0); + vec4 refl = texture2D(sReflect, vec2(tc.x, 1.0 - tc.y) + dudv * uParam.w); - vec4 color = mix(refr, refl, fresnel) + spec * 1.5; + float fresnel = calcFresnel(dot(normal, viewVec), 0.1, 2.0); - float d = abs((vCoord.y - uViewPos.y) / normalize(vViewVec).y); - d *= step(0.0, uViewPos.y - vCoord.y); // apply fog only when camera is underwater - color.xyz = applyFog(color.xyz, UNDERWATER_COLOR * 0.2, d * WATER_FOG_DIST); + vec4 color = mix(refr, refl, fresnel) + spec * 1.5; - return color; - } - - vec4 pass() { - #ifdef WATER_DROP - return drop(); - #endif + float d = abs((vCoord.y - uViewPos.y) / normalize(vViewVec).y); + d *= step(0.0, uViewPos.y - vCoord.y); // apply fog only when camera is underwater + color.xyz = applyFog(color.xyz, UNDERWATER_COLOR * 0.2, d * WATER_FOG_DIST); - #ifdef WATER_STEP - return calc(); - #endif + return color; + } - #ifdef WATER_CAUSTICS - return caustics(); - #endif + vec4 pass() { + #ifdef WATER_DROP + return drop(); + #endif - #ifdef WATER_MASK - return mask(); - #endif + #ifdef WATER_STEP + return calc(); + #endif - #ifdef WATER_COMPOSE - return compose(); - #endif + #ifdef WATER_CAUSTICS + return caustics(); + #endif - return vec4(1.0, 0.0, 1.0, 1.0); - } - - void main() { - gl_FragColor = pass(); - } + #ifdef WATER_MASK + return mask(); + #endif + + #ifdef WATER_COMPOSE + return compose(); + #endif + + return vec4(1.0, 0.0, 1.0, 1.0); + } + + void main() { + gl_FragColor = pass(); + } #endif )====" \ No newline at end of file diff --git a/src/ui.h b/src/ui.h index 3f6a1f7..5b78fcb 100644 --- a/src/ui.h +++ b/src/ui.h @@ -205,7 +205,7 @@ namespace UI { " SHIFT - Walk@" " SPACE - Draw Weapon@" " CTRL - Action@" - " ALT - Jump@" + " D - Jump@" " Z - Step Left@" " X - Step Right@" " A - Roll@" From dd77fc47d19aaf8e7534296ca2e0065c1c24d40a Mon Sep 17 00:00:00 2001 From: XProger Date: Sun, 16 Jul 2017 05:27:46 +0300 Subject: [PATCH 3/7] #15 latest Win executable --- bin/OpenLara.exe | Bin 212992 -> 214016 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/bin/OpenLara.exe b/bin/OpenLara.exe index 0b255ee9d8409ba94299c3e7fe53a7c654d554f1..6ae96e045a4c006315fc3b07935446ce6e808459 100644 GIT binary patch delta 52496 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 Cux@bx delta 52399 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 From e28e5c79e10c72501c94abacea6894c57031febe Mon Sep 17 00:00:00 2001 From: XProger Date: Mon, 24 Jul 2017 06:03:51 +0300 Subject: [PATCH 4/7] deferred graphical state change; #23 ripples on static water surfaces; #3 fix water out bug; #11 fix lighting, fix key selector, fix water above all in inventory background; #8 screen-space portals for rooms clipping --- src/cache.h | 109 ++++++------ src/controller.h | 3 +- src/core.h | 359 ++++++++++++++++++++-------------------- src/debug.h | 11 +- src/format.h | 6 +- src/inventory.h | 37 +++-- src/lara.h | 11 +- src/level.h | 353 +++++++++++++++++++++++---------------- src/mesh.h | 93 +---------- src/shaders/shader.glsl | 6 +- src/shaders/water.glsl | 33 +++- src/utils.h | 3 + 12 files changed, 532 insertions(+), 492 deletions(-) diff --git a/src/cache.h b/src/cache.h index ecde068..4c884b3 100644 --- a/src/cache.h +++ b/src/cache.h @@ -282,17 +282,14 @@ struct AmbientCache { // get result color from 1x1 textures for (int j = 0; j < 6; j++) { Core::setTarget(textures[j * 4 + 3]); - - TR::Color32 color; - glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &color); - colors[j] = vec3(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f); - // colors[j] *= colors[j]; // to "linear" space + colors[j] = Core::copyPixel(0, 0).xyz; } Core::setDepthTest(true); } void precessQueue() { + game->setupBinding(); for (int i = 0; i < tasksCount; i++) { Task &task = tasks[i]; renderAmbient(task.room, task.sector, &task.cube->colors[0]); @@ -411,13 +408,11 @@ struct WaterCache { mask = new Texture(w, h, Texture::RGB16, false, m, false); delete[] m; - Core::setTarget(data[0], true); - Core::setTarget(NULL); - blank = false; - // Core::setTarget(data[0], true); - // Core::invalidateTarget(false, true); + // texture may be initialized with trash, so... + Core::setTarget(data[0], true); + Core::validateRenderState(); // immediate clear } void free() { @@ -430,7 +425,6 @@ struct WaterCache { } items[MAX_SURFACES]; int count, visible; - bool checkVisibility; int dropCount; struct Drop { @@ -441,7 +435,7 @@ struct WaterCache { Drop(const vec3 &pos, float radius, float strength) : pos(pos), radius(radius), strength(strength) {} } drops[MAX_DROPS]; - WaterCache(IGame *game) : game(game), level(game->getLevel()), refract(NULL), count(0), checkVisibility(false), dropCount(0) { + WaterCache(IGame *game) : game(game), level(game->getLevel()), refract(NULL), count(0), dropCount(0) { reflect = new Texture(512, 512, Texture::RGB16, false); } @@ -473,8 +467,6 @@ struct WaterCache { } void setVisible(int roomIndex, int nextRoom = TR::NO_ROOM) { - if (!checkVisibility) return; - if (nextRoom == TR::NO_ROOM) { // setVisible(underwaterRoom) for caustics update for (int i = 0; i < count; i++) if (items[i].caust == roomIndex) { @@ -569,7 +561,7 @@ struct WaterCache { game->setShader(Core::passWater, Shader::WATER_STEP); Core::active.shader->setParam(uTexParam, vec4(1.0f / item.data[0]->width, 1.0f / item.data[0]->height, 1.0f, 1.0f)); - Core::active.shader->setParam(uParam, vec4(0.995f, 1.0f, 0, 0)); + Core::active.shader->setParam(uParam, vec4(0.995f, 1.0f, 0, Core::params.x)); while (item.timer >= SIMULATE_TIMESTEP) { // water step @@ -581,7 +573,6 @@ struct WaterCache { swap(item.data[0], item.data[1]); item.timer -= SIMULATE_TIMESTEP; } - // calc caustics game->setShader(Core::passWater, Shader::WATER_CAUSTICS); @@ -602,13 +593,6 @@ struct WaterCache { void renderMask() { // mask underwater geometry by zero alpha - for (int i = 0; i < count; i++) { - Item &item = items[i]; - if (item.visible && item.blank) - item.init(game); - } - Core::setTarget(NULL); - game->setShader(Core::passWater, Shader::WATER_MASK); Core::active.shader->setParam(uTexParam, vec4(1.0f)); @@ -631,15 +615,15 @@ struct WaterCache { } void getRefract() { - int w = int(Core::viewport.z); - int h = int(Core::viewport.w); + int w = int(Core::viewportDef.z); + int h = int(Core::viewportDef.w); // get refraction texture if (!refract || w != refract->width || h != refract->height) { delete refract; refract = new Texture(w, h, Texture::RGBA, false); } - Core::copyTarget(refract, 0, 0, int(Core::viewport.x), int(Core::viewport.y), w, h); // copy framebuffer into refraction texture + Core::copyTarget(refract, 0, 0, int(Core::viewportDef.x), int(Core::viewportDef.y), w, h); // copy framebuffer into refraction texture } void simulate() { @@ -658,7 +642,48 @@ struct WaterCache { } } Core::setDepthTest(true); - Core::setTarget(NULL); + } + + void renderReflect() { + if (!visible) return; + + for (int i = 0; i < count; i++) { + Item &item = items[i]; + if (item.visible && item.blank) + item.init(game); + } + + // render mirror reflection + Core::setTarget(reflect, true); + Camera *camera = (Camera*)game->getCamera(); + game->setupBinding(); + + for (int i = 0; i < count; i++) { + Item &item = items[i]; + if (!item.visible) continue; + + vec3 p = item.pos; + vec3 n = vec3(0, 1, 0); + + vec4 reflectPlane = vec4(n.x, n.y, n.z, -n.dot(p)); + bool underwater = level->rooms[camera->getRoomIndex()].flags.water; + + //bool underwater = level->camera->pos.y > item.pos.y; + + camera->reflectPlane = &reflectPlane; + camera->setup(true); + + float sign = underwater ? -1.0f : 1.0f; + game->setClipParams(sign, item.pos.y * sign); + game->updateParams(); + game->renderView(underwater ? item.from : item.to, false); + } + Core::invalidateTarget(false, true); + game->setClipParams(1.0f, NO_CLIP_PLANE); + game->updateParams(); + + camera->reflectPlane = NULL; + camera->setup(true); } void render() { @@ -666,34 +691,6 @@ struct WaterCache { Item &item = items[i]; if (!item.visible) continue; - // render mirror reflection - Core::setTarget(reflect, true); - Core::viewport = Core::viewportDef; - vec3 p = item.pos; - vec3 n = vec3(0, 1, 0); - - vec4 reflectPlane = vec4(n.x, n.y, n.z, -n.dot(p)); - - Camera *camera = (Camera*)game->getCamera(); - - bool underwater = level->rooms[camera->getRoomIndex()].flags.water; - - //bool underwater = level->camera->pos.y > item.pos.y; - - camera->reflectPlane = &reflectPlane; - float sign = underwater ? -1.0f : 1.0f; - game->setClipParams(sign, item.pos.y * sign); - game->updateParams(); - game->renderCompose(underwater ? item.from : item.to); - Core::invalidateTarget(false, true); - game->setClipParams(1.0f, NO_CLIP_PLANE); - game->updateParams(); - - camera->reflectPlane = NULL; - Core::setTarget(NULL); - - camera->setup(true); - // render water plane if (level->rooms[item.from].lightsCount) { TR::Room::Light &light = level->rooms[item.from].lights[0]; @@ -705,7 +702,7 @@ struct WaterCache { game->setShader(Core::passWater, Shader::WATER_COMPOSE); Core::active.shader->setParam(uLightPos, Core::lightPos[0], 1); Core::active.shader->setParam(uLightColor, Core::lightColor[0], 1); - Core::active.shader->setParam(uParam, vec4(float(Core::width) / refract->width, float(Core::height) / refract->height, 0.05f, 0.02f)); + Core::active.shader->setParam(uParam, vec4(Core::viewportDef.z / refract->width, Core::viewportDef.w / refract->height, 0.05f, 0.02f)); float sx = item.size.x * DETAIL / (item.data[0]->width / 2); float sz = item.size.z * DETAIL / (item.data[0]->height / 2); diff --git a/src/controller.h b/src/controller.h index 230d481..cc2facc 100644 --- a/src/controller.h +++ b/src/controller.h @@ -30,7 +30,8 @@ struct IGame { virtual void setShader(Core::Pass pass, Shader::Type type, bool underwater = false, bool alphaTest = false) {} virtual void setupBinding() {} virtual void renderEnvironment(int roomIndex, const vec3 &pos, Texture **targets, int stride = 0) {} - virtual void renderCompose(int roomIndex, bool genShadowMask = false) {} + virtual void renderCompose(int roomIndex) {} + virtual void renderView(int roomIndex, bool water) {} virtual void fxQuake(float time) {} virtual bool invUse(TR::Entity::Type type) { return false; } diff --git a/src/core.h b/src/core.h index 6ee1269..31b0643 100644 --- a/src/core.h +++ b/src/core.h @@ -43,8 +43,6 @@ #define glProgramBinary glProgramBinaryOES #define GL_PROGRAM_BINARY_LENGTH GL_PROGRAM_BINARY_LENGTH_OES - #define GL_STENCIL_TEST_TWO_SIDE_EXT 0 - #define glActiveStencilFaceEXT(...) #elif __linux__ #define LINUX 1 #include @@ -72,9 +70,6 @@ #define GL_TEXTURE_COMPARE_MODE GL_TEXTURE_COMPARE_MODE_EXT #define GL_TEXTURE_COMPARE_FUNC GL_TEXTURE_COMPARE_FUNC_EXT #define GL_COMPARE_REF_TO_TEXTURE GL_COMPARE_REF_TO_TEXTURE_EXT - - #define GL_STENCIL_TEST_TWO_SIDE_EXT 0 - #define glActiveStencilFaceEXT(...) #else #include #include @@ -117,10 +112,8 @@ #define GL_CLAMP_TO_BORDER GL_CLAMP_TO_BORDER_EXT #define GL_TEXTURE_BORDER_COLOR GL_TEXTURE_BORDER_COLOR_EXT - #define GL_STENCIL_TEST_TWO_SIDE_EXT 0 #define glGetProgramBinary(...) #define glProgramBinary(...) - #define glActiveStencilFaceEXT(...) #endif namespace Core { @@ -204,10 +197,6 @@ namespace Core { PFNGLBINDBUFFERARBPROC glBindBuffer; PFNGLBUFFERDATAARBPROC glBufferData; PFNGLBUFFERSUBDATAARBPROC glBufferSubData; - // Stencil - PFNGLACTIVESTENCILFACEEXTPROC glActiveStencilFaceEXT; - PFNGLSTENCILFUNCSEPARATEPROC glStencilFuncSeparate; - PFNGLSTENCILOPSEPARATEPROC glStencilOpSeparate; #endif PFNGLGENVERTEXARRAYSPROC glGenVertexArrays; @@ -228,6 +217,36 @@ namespace Core { struct Shader; struct Texture; +enum RenderState : int32 { + RS_TARGET = 1 << 0, + RS_VIEWPORT = 1 << 1, + RS_DEPTH_TEST = 1 << 2, + RS_DEPTH_WRITE = 1 << 3, + RS_COLOR_WRITE_R = 1 << 4, + RS_COLOR_WRITE_G = 1 << 5, + RS_COLOR_WRITE_B = 1 << 6, + RS_COLOR_WRITE_A = 1 << 7, + RS_COLOR_WRITE = RS_COLOR_WRITE_R | RS_COLOR_WRITE_G | RS_COLOR_WRITE_B | RS_COLOR_WRITE_A, + RS_CULL_BACK = 1 << 8, + RS_CULL_FRONT = 1 << 9, + RS_CULL = RS_CULL_BACK | RS_CULL_FRONT, + RS_BLEND_ALPHA = 1 << 10, + RS_BLEND_ADD = 1 << 11, + RS_BLEND_MULTIPLY = 1 << 12, + RS_BLEND_SCREEN = 1 << 13, + RS_BLEND = RS_BLEND_ADD | RS_BLEND_ALPHA | RS_BLEND_MULTIPLY | RS_BLEND_SCREEN, +}; + +typedef unsigned short Index; + +struct Vertex { + short4 coord; // xyz - position, w - joint index (for entities only) + short4 normal; // xyz - vertex normalá w - unused + short4 texCoord; // xy - texture coordinates, zw - trapezoid warping + ubyte4 param; // xy - anim tex range and frame index, zw - unused + ubyte4 color; // xyz - color, w - intensity +}; + namespace Core { struct { bool shaderBinary; @@ -240,7 +259,6 @@ namespace Core { bool texBorder; bool texFloat, texFloatLinear; bool texHalf, texHalfLinear; - char stencil; #ifdef PROFILE bool profMarker; bool profTiming; @@ -298,7 +316,6 @@ extern int getTime(); namespace Core { float eye; vec4 viewport, viewportDef; - vec4 scissor; mat4 mView, mProj, mViewProj, mViewInv, mLightProj; Basis basis; vec3 viewPos; @@ -322,18 +339,25 @@ namespace Core { } items[MAX_RENDER_BUFFERS]; } rtCache[2]; + int32 renderState; + struct { Shader *shader; Texture *textures[8]; Texture *target; int targetFace; + vec4 viewport; GLuint VAO; GLuint iBuffer; GLuint vBuffer; - BlendMode blendMode; - CullMode cullMode; - bool stencilTwoSide; + int32 renderState; } active; + + struct { + Texture *texture; + bool clear; + uint8 face; + } reqTarget; struct Stats { int dips, tris, frame, fps, fpsTime; @@ -445,10 +469,6 @@ namespace Core { GetProcOGL(glBindBuffer); GetProcOGL(glBufferData); GetProcOGL(glBufferSubData); - - GetProcOGL(glActiveStencilFaceEXT); - GetProcOGL(glStencilFuncSeparate); - GetProcOGL(glStencilOpSeparate); #endif #if defined(ANDROID) || defined(__EMSCRIPTEN__) @@ -478,14 +498,6 @@ namespace Core { support.texHalfLinear = extSupport(ext, "GL_ARB_texture_float") || extSupport(ext, "_texture_half_float_linear"); support.texHalf = support.texHalfLinear || extSupport(ext, "_texture_half_float"); - if (extSupport(ext, "_ATI_separate_stencil")) - support.stencil = 2; - else - if (extSupport(ext, "_stencil_two_side")) - support.stencil = 1; - else - support.stencil = 0; - #ifdef PROFILE support.profMarker = extSupport(ext, "_KHR_debug"); support.profTiming = extSupport(ext, "_timer_query"); @@ -508,7 +520,6 @@ namespace Core { LOG(" float textures : float = %s, half = %s\n", support.texFloat ? (support.texFloatLinear ? "linear" : "nearest") : "false", support.texHalf ? (support.texHalfLinear ? "linear" : "nearest") : "false"); - LOG(" stencil : %s\n", support.stencil == 2 ? "separate" : (support.stencil == 1 ? "two_side" : "false")); LOG("\n"); glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&defaultFBO); @@ -567,143 +578,142 @@ namespace Core { return cache.count++; } - void clear(bool clearColor, bool clearDepth, bool clearStencil = false) { - if (GLbitfield mask = (clearColor ? GL_COLOR_BUFFER_BIT : 0) | (clearDepth ? GL_DEPTH_BUFFER_BIT : 0) | (clearStencil ? GL_STENCIL_BUFFER_BIT : 0)) - glClear(mask); + void validateRenderState() { + int32 mask = renderState ^ active.renderState; + if (!mask) return; + + if (mask & RS_TARGET) { + Texture *target = reqTarget.texture; + uint8 face = reqTarget.face; + + if (target != active.target || face != active.targetFace) { + + if (!target) { // may be a null + glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO); + } else { + GLenum texTarget = GL_TEXTURE_2D; + if (target->cube) + texTarget = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face; + + bool depth = target->format == Texture::DEPTH || target->format == Texture::SHADOW; + int rtIndex = cacheRenderTarget(depth, target->width, target->height); + + glBindFramebuffer(GL_FRAMEBUFFER, FBO); + glFramebufferTexture2D (GL_FRAMEBUFFER, depth ? GL_DEPTH_ATTACHMENT : GL_COLOR_ATTACHMENT0, texTarget, target->ID, 0); + glFramebufferRenderbuffer (GL_FRAMEBUFFER, depth ? GL_COLOR_ATTACHMENT0 : GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rtCache[depth].items[rtIndex].ID); + } + + active.target = target; + active.targetFace = face; + } + } + + if (mask & RS_VIEWPORT) { + if (viewport != active.viewport) { + active.viewport = viewport; + glViewport(int(viewport.x), int(viewport.y), int(viewport.z), int(viewport.w)); + } + renderState &= ~RS_VIEWPORT; + } + + if (mask & RS_DEPTH_TEST) { + if (renderState & RS_DEPTH_TEST) + glEnable(GL_DEPTH_TEST); + else + glDisable(GL_DEPTH_TEST); + } + + if (mask & RS_DEPTH_WRITE) { + glDepthMask((renderState & RS_DEPTH_WRITE) != 0); + } + + if (mask & RS_COLOR_WRITE) { + glColorMask((renderState & RS_COLOR_WRITE_R) != 0, + (renderState & RS_COLOR_WRITE_G) != 0, + (renderState & RS_COLOR_WRITE_B) != 0, + (renderState & RS_COLOR_WRITE_A) != 0); + } + + if (mask & RS_CULL) { + if (!(active.renderState & RS_CULL)) + glEnable(GL_CULL_FACE); + switch (renderState & RS_CULL) { + case RS_CULL_BACK : glCullFace(GL_BACK); break; + case RS_CULL_FRONT : glCullFace(GL_FRONT); break; + default : glDisable(GL_CULL_FACE); + } + } + + if (mask & RS_BLEND) { + if (!(active.renderState & RS_BLEND)) + glEnable(GL_BLEND); + switch (renderState & RS_BLEND) { + case RS_BLEND_ALPHA : glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); break; + case RS_BLEND_ADD : glBlendFunc(GL_ONE, GL_ONE); break; + case RS_BLEND_MULTIPLY : glBlendFunc(GL_DST_COLOR, GL_ZERO); break; + case RS_BLEND_SCREEN : glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_COLOR); break; + default : glDisable(GL_BLEND); + } + } + + if (mask & RS_TARGET) { // for cler the RT & reset mask + if (reqTarget.clear) + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + renderState &= ~RS_TARGET; + } + + active.renderState = renderState; } void setClearColor(const vec4 &color) { glClearColor(color.x, color.y, color.z, color.w); } - void setClearStencil(int value) { - glClearStencil(value); - } - - void setViewport(const vec4 &vp) { - glViewport(int(vp.x), int(vp.y), int(vp.z), int(vp.w)); - viewport = vp; - } - void setViewport(int x, int y, int width, int height) { - setViewport(vec4(float(x), float(y), float(width), float(height))); - } - - void setScissor(int x, int y, int width, int height) { - glScissor(x, y, width, height); - scissor = vec4(float(x), float(y), float(width), float(height)); + viewport = vec4(float(x), float(y), float(width), float(height)); + renderState |= RS_VIEWPORT; } void setCulling(CullMode mode) { - if (active.cullMode == mode) - return; - + renderState &= ~RS_CULL; switch (mode) { - case cfNone : - glDisable(GL_CULL_FACE); - case cfBack : - glCullFace(GL_BACK); - break; - case cfFront : - glCullFace(GL_FRONT); - break; + case cfNone : break; + case cfBack : renderState |= RS_CULL_BACK; break; + case cfFront : renderState |= RS_CULL_FRONT; break; } - - if (mode != cfNone && active.cullMode == cfNone) - glEnable(GL_CULL_FACE); - - active.cullMode = mode; } void setBlending(BlendMode mode) { - if (active.blendMode == mode) - return; - + renderState &= ~RS_BLEND; switch (mode) { - case bmNone : - glDisable(GL_BLEND); - break; - case bmAlpha : - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - break; - case bmAdd : - glBlendFunc(GL_ONE, GL_ONE); - break; - case bmMultiply : - glBlendFunc(GL_DST_COLOR, GL_ZERO); - break; - case bmScreen : - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_COLOR); - break; + case bmNone : break; + case bmAlpha : renderState |= RS_BLEND_ALPHA; break; + case bmAdd : renderState |= RS_BLEND_ADD; break; + case bmMultiply : renderState |= RS_BLEND_MULTIPLY; break; + case bmScreen : renderState |= RS_BLEND_SCREEN; break; } - - if (mode != bmNone && active.blendMode == bmNone) - glEnable(GL_BLEND); - - active.blendMode = mode; } void setColorWrite(bool r, bool g, bool b, bool a) { - glColorMask(r, g, b, a); + renderState &= ~RS_COLOR_WRITE; + if (r) renderState |= RS_COLOR_WRITE_R; + if (g) renderState |= RS_COLOR_WRITE_G; + if (b) renderState |= RS_COLOR_WRITE_B; + if (a) renderState |= RS_COLOR_WRITE_A; } void setDepthWrite(bool write) { - glDepthMask(write); + if (write) + renderState |= RS_DEPTH_WRITE; + else + renderState &= ~RS_DEPTH_WRITE; } void setDepthTest(bool test) { if (test) - glEnable(GL_DEPTH_TEST); + renderState |= RS_DEPTH_TEST; else - glDisable(GL_DEPTH_TEST); - } - - void setStencilTest(bool test) { - if (test) - glEnable(GL_STENCIL_TEST); - else - glDisable(GL_STENCIL_TEST); - } - - void setScissorTest(bool test) { - if (test) - glEnable(GL_SCISSOR_TEST); - else - glDisable(GL_SCISSOR_TEST); - } - - void setStencilTwoSide(int ref, bool test) { // preset for z-fail shadow volumes - active.stencilTwoSide = test; - if (test) { - switch (Core::support.stencil) { - case 0 : - glStencilFunc(GL_ALWAYS, ref, ~0); - break; - case 1 : - setCulling(cfNone); - glEnable(GL_STENCIL_TEST_TWO_SIDE_EXT); - glActiveStencilFaceEXT(GL_BACK); - glStencilOp(GL_KEEP, GL_DECR, GL_KEEP); - glStencilFunc(GL_ALWAYS, ref, ~0); - glActiveStencilFaceEXT(GL_FRONT); - glStencilOp(GL_KEEP, GL_INCR, GL_KEEP); - glStencilFunc(GL_ALWAYS, ref, ~0); - break; - case 2 : - setCulling(cfNone); - glStencilFuncSeparate(GL_FRONT, GL_ALWAYS, ref, ~0); - glStencilFuncSeparate(GL_BACK, GL_ALWAYS, ref, ~0); - glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_INCR, GL_KEEP); - glStencilOpSeparate(GL_BACK, GL_KEEP, GL_DECR, GL_KEEP); - break; - } - } else { - if (Core::support.stencil == 1) - glDisable(GL_STENCIL_TEST_TWO_SIDE_EXT); - glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); - glStencilFunc(GL_NOTEQUAL, ref, ~0); - setCulling(cfFront); - } + renderState &= ~RS_DEPTH_TEST; } void invalidateTarget(bool color, bool depth) { @@ -719,64 +729,59 @@ namespace Core { } void setTarget(Texture *target, bool clear = false, int face = 0) { - if (!target && defaultTarget) + if (!target) target = defaultTarget; - if (target != active.target || face != active.targetFace) { - if (!target) { - glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO); - glColorMask(true, true, true, true); + bool color = !target || (target->format != Texture::DEPTH && target->format != Texture::SHADOW); + setColorWrite(color, color, color, color); - setViewport(int(viewportDef.x), int(viewportDef.y), int(viewportDef.z), int(viewportDef.w)); - } else { - if (active.target == NULL || active.target == defaultTarget) - viewportDef = viewport; - GLenum texTarget = GL_TEXTURE_2D; - if (target->cube) - texTarget = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face; + if (!target) // backbuffer + setViewport(int(viewportDef.x), int(viewportDef.y), int(viewportDef.z), int(viewportDef.w)); + else + setViewport(0, 0, target->width, target->height); - bool depth = target->format == Texture::DEPTH || target->format == Texture::SHADOW; - int rtIndex = cacheRenderTarget(depth, target->width, target->height); - - glBindFramebuffer(GL_FRAMEBUFFER, FBO); - glFramebufferTexture2D (GL_FRAMEBUFFER, depth ? GL_DEPTH_ATTACHMENT : GL_COLOR_ATTACHMENT0, texTarget, target->ID, 0); - glFramebufferRenderbuffer (GL_FRAMEBUFFER, depth ? GL_COLOR_ATTACHMENT0 : GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rtCache[depth].items[rtIndex].ID); - - if (depth) - glColorMask(false, false, false, false); - else - glColorMask(true, true, true, true); - setViewport(0, 0, target->width, target->height); - } - } - - if (clear) - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - - active.target = target; - active.targetFace = face; + reqTarget.texture = target; + reqTarget.clear = clear; + reqTarget.face = face; + renderState |= RS_TARGET; } - void copyTarget(Texture *texture, int xOffset, int yOffset, int x, int y, int width, int height) { - texture->bind(sDiffuse); + void copyTarget(Texture *dst, int xOffset, int yOffset, int x, int y, int width, int height) { + validateRenderState(); + dst->bind(sDiffuse); glCopyTexSubImage2D(GL_TEXTURE_2D, 0, xOffset, yOffset, x, y, width, height); } + vec4 copyPixel(int x, int y) { // GPU sync! + validateRenderState(); + ubyte4 c; + glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &c); + return vec4(float(c.x), float(c.y), float(c.z), float(c.w)) * (1.0f / 255.0f); + } + void beginFrame() { - memset(&active, 0, sizeof(active)); + //memset(&active, 0, sizeof(active)); setViewport(0, 0, Core::width, Core::height); viewportDef = viewport; - setDepthTest(true); - active.blendMode = bmAlpha; - active.cullMode = cfNone; setCulling(cfFront); - setBlending(bmNone); + setBlending(bmAlpha); + setDepthTest(true); + setDepthWrite(true); + setColorWrite(true, true, true, true); + Core::stats.start(); } void endFrame() { Core::stats.stop(); } + + void DIP(int iStart, int iCount) { + validateRenderState(); + glDrawElements(GL_TRIANGLES, iCount, GL_UNSIGNED_SHORT, (Index*)NULL + iStart); + stats.dips++; + stats.tris += iCount / 3; + } } #include "mesh.h" diff --git a/src/debug.h b/src/debug.h index 4a53801..4b43cbe 100644 --- a/src/debug.h +++ b/src/debug.h @@ -178,15 +178,16 @@ namespace Debug { glLoadIdentity(); glOrtho(0, Core::width, Core::height, 0, 0, 1); - glDisable(GL_DEPTH_TEST); - glDisable(GL_CULL_FACE); - glDisable(GL_TEXTURE_2D); + Core::setDepthTest(false); + Core::setCulling(cfNone); + Core::validateRenderState(); + glColor4fv((GLfloat*)&color); glRasterPos2f(pos.x, pos.y); glListBase(font); glCallLists(strlen(str), GL_UNSIGNED_BYTE, str); - glEnable(GL_CULL_FACE); - glEnable(GL_DEPTH_TEST); + Core::setDepthTest(true); + Core::setCulling(cfFront); glPopMatrix(); glMatrixMode(GL_MODELVIEW); diff --git a/src/format.h b/src/format.h index cd57665..3bfd48c 100644 --- a/src/format.h +++ b/src/format.h @@ -481,7 +481,7 @@ namespace TR { uint16 meshesCount; int16 alternateRoom; struct { - uint16 water:1, unused:14, rendered:1; + uint16 water:1, unused:14, visible:1; } flags; struct Portal { @@ -525,6 +525,10 @@ namespace TR { uint16 meshID; uint16 meshIndex; // index into static meshes array } *meshes; + + vec3 getOffset() const { + return vec3(float(info.x), 0.0f, float(info.z)); + } }; union FloorData { diff --git a/src/inventory.h b/src/inventory.h index 1c0897f..e99cd85 100644 --- a/src/inventory.h +++ b/src/inventory.h @@ -297,10 +297,10 @@ struct Inventory { if (type != TR::Entity::NONE) { int i = contains(type); if (i >= 0) - pageItemIndex[page] = getItemIndex(page, i); + pageItemIndex[page] = getLocalIndex(i); } - index = targetIndex = pageItemIndex[page]; + index = targetIndex = pageItemIndex[page]; } } return active; @@ -322,16 +322,27 @@ struct Inventory { } } - int getItemIndex(Page page, int index) { + int getGlobalIndex(Page page, int localIndex) { for (int i = 0; i < itemsCount; i++) if (items[i]->desc.page == page) { - if (!index) + if (!localIndex) return i; - index--; + localIndex--; } return 0; } + int getLocalIndex(int globalIndex) { + Page page = items[globalIndex]->desc.page; + + int idx = 0; + for (int i = 0; i < globalIndex; i++) + if (items[i]->desc.page == page) + idx++; + + return idx; + } + float getAngle(int index, int count) { return PI * 2.0f / float(count) * index; } @@ -347,7 +358,7 @@ struct Inventory { } bool showHealthBar() { - int idx = getItemIndex(page, index); + int idx = getGlobalIndex(page, index); TR::Entity::Type type = items[idx]->type; return active && phaseRing == 1.0f && index == targetIndex && phasePage == 1.0f && (type == TR::Entity::INV_MEDIKIT_SMALL || type == TR::Entity::INV_MEDIKIT_BIG); } @@ -385,7 +396,7 @@ struct Inventory { vec3 p; - Item *item = items[getItemIndex(page, index)]; + Item *item = items[getGlobalIndex(page, index)]; if (index == targetIndex && ready) { if (Input::state[cAction] && (phaseChoose == 0.0f || (phaseChoose == 1.0f && item->anim->isEnded))) { @@ -410,7 +421,7 @@ struct Inventory { float w = 90.0f * DEG2RAD * Core::deltaTime; - int itemIndex = index == targetIndex ? getItemIndex(page, index) : -1; + int itemIndex = index == targetIndex ? getGlobalIndex(page, index) : -1; for (int i = 0; i < itemsCount; i++) { items[i]->update(); @@ -610,10 +621,10 @@ struct Inventory { vec3(0.4f), vec3(0.2f), vec3(0.4f), vec3(0.5f), vec3(0.4f), vec3(0.6f) }; - Core::lightPos[0] = vec3(1000, 2000, 1000); - Core::lightColor[0] = vec4(1, 1, 1, 8192); - for (int i = 1; i < MAX_LIGHTS; i++) - Core::lightColor[1] = vec4(0, 0, 0, 1); + for (int i = 0; i < MAX_LIGHTS; i++) { + Core::lightPos[i] = vec3(0, 0, 0); + Core::lightColor[i] = vec4(0, 0, 0, 1); + } Core::active.shader->setParam(uLightPos, Core::lightPos[0], MAX_LIGHTS); Core::active.shader->setParam(uLightColor, Core::lightColor[0], MAX_LIGHTS); @@ -642,7 +653,7 @@ struct Inventory { } if (index == targetIndex) - renderItemText(items[getItemIndex(page, index)], UI::width); + renderItemText(items[getGlobalIndex(page, index)], UI::width); } }; diff --git a/src/lara.h b/src/lara.h index d023af4..9d98126 100644 --- a/src/lara.h +++ b/src/lara.h @@ -1299,6 +1299,7 @@ struct Lara : Character { damageTime = LARA_DAMAGE_TIME; health = min(LARA_MAX_HEALTH, health + (item == TR::Entity::INV_MEDIKIT_SMALL ? LARA_MAX_HEALTH / 2 : LARA_MAX_HEALTH)); playSound(TR::SND_HEALTH, pos, Sound::PAN); + //TODO: remove medikit item break; case TR::Entity::INV_PUZZLE_1 : case TR::Entity::INV_PUZZLE_2 : @@ -1555,12 +1556,16 @@ struct Lara : Character { if (state == STATE_HANDSTAND || state == STATE_HANG_UP) return STAND_HANG; - if (stand == STAND_ONWATER && state != STATE_DIVE && state != STATE_STOP) - return stand; + if (stand == STAND_ONWATER && state != STATE_STOP) { + if (!getRoom().flags.water && state != STATE_WATER_OUT) + return STAND_AIR; + if (state != STATE_DIVE) + return stand; + } if (getRoom().flags.water) { wpnHide(); - if (stand != STAND_UNDERWATER && stand != STAND_ONWATER && (state != STATE_FALL && state != STATE_FALL_BACK && state != STATE_SWAN_DIVE && state != STATE_FAST_DIVE)) + if (stand != STAND_UNDERWATER && stand != STAND_ONWATER && (state != STATE_FALL && state != STATE_REACH && state != STATE_SWAN_DIVE && state != STATE_FAST_DIVE)) animation.setAnim(ANIM_FALL_FORTH); return STAND_UNDERWATER; } diff --git a/src/level.h b/src/level.h index 59065f0..94cf2e5 100644 --- a/src/level.h +++ b/src/level.h @@ -101,28 +101,18 @@ struct Level : IGame { virtual void renderEnvironment(int roomIndex, const vec3 &pos, Texture **targets, int stride = 0) { PROFILE_MARKER("ENVIRONMENT"); Core::eye = 0.0f; + setupBinding(); // first pass render level into cube faces for (int i = 0; i < 6; i++) { setupCubeCamera(pos, i); Core::pass = Core::passAmbient; Texture *target = targets[0]->cube ? targets[0] : targets[i * stride]; Core::setTarget(target, true, i); - renderScene(roomIndex); + renderView(roomIndex, false); Core::invalidateTarget(false, true); } } - virtual void renderCompose(int roomIndex, bool genShadowMask = false) { - PROFILE_MARKER("PASS_COMPOSE"); - - if (shadow) shadow->bind(sShadow); - - Core::setDepthTest(true); - Core::setDepthWrite(true); - Core::pass = Core::passCompose; - renderScene(roomIndex); - } - virtual void fxQuake(float time) { camera->shake = time; } @@ -502,89 +492,52 @@ struct Level : IGame { Core::active.shader->setParam(uMaterial, vec4(diffuse, ambient, specular, alpha)); } - void renderRoom(int roomIndex, int from = TR::NO_ROOM) { + void renderRoom(int roomIndex) { ASSERT(roomIndex >= 0 && roomIndex < level.roomsCount); PROFILE_MARKER("ROOM"); TR::Room &room = level.rooms[roomIndex]; vec3 offset = vec3(float(room.info.x), 0.0f, float(room.info.z)); + room.flags.visible = true; + // room geometry & sprites - if (!room.flags.rendered) { // skip if already rendered - if (waterCache && room.flags.water) - waterCache->setVisible(roomIndex); + if (Core::pass != Core::passShadow) { + Basis qTemp = Core::basis; + Core::basis.translate(offset); - room.flags.rendered = true; + MeshBuilder::RoomRange &range = mesh->rooms[roomIndex]; - if (Core::pass != Core::passShadow) { + for (int transp = 0; transp < 2; transp++) { + if (!range.geometry[transp].iCount) + continue; - Basis qTemp = Core::basis; - Core::basis.translate(offset); + Core::setBlending(transp ? bmAlpha : bmNone); - MeshBuilder::RoomRange &range = mesh->rooms[roomIndex]; + setRoomParams(roomIndex, Shader::ROOM, 1.0f, intensityf(room.ambient), 0.0f, 1.0f, transp > 0); + Shader *sh = Core::active.shader; + sh->setParam(uLightColor, Core::lightColor[0], MAX_LIGHTS); + sh->setParam(uLightPos, Core::lightPos[0], MAX_LIGHTS); + sh->setParam(uBasis, Core::basis); - for (int transp = 0; transp < 2; transp++) { - if (!range.geometry[transp].iCount) - continue; - - Core::setBlending(transp ? bmAlpha : bmNone); - - setRoomParams(roomIndex, Shader::ROOM, 1.0f, intensityf(room.ambient), 0.0f, 1.0f, transp > 0); - Shader *sh = Core::active.shader; - sh->setParam(uLightColor, Core::lightColor[0], MAX_LIGHTS); - sh->setParam(uLightPos, Core::lightPos[0], MAX_LIGHTS); - sh->setParam(uBasis, Core::basis); - - // render room geometry - mesh->renderRoomGeometry(roomIndex, transp > 0); - } - - // render room sprites - if (range.sprites.iCount) { - Core::setBlending(bmAlpha); - setRoomParams(roomIndex, Shader::SPRITE, 1.0f, 1.0f, 0.0f, 1.0f, true); - Shader *sh = Core::active.shader; - sh->setParam(uLightColor, Core::lightColor[0], MAX_LIGHTS); - sh->setParam(uLightPos, Core::lightPos[0], MAX_LIGHTS); - sh->setParam(uBasis, Core::basis); - mesh->renderRoomSprites(roomIndex); - } - - Core::basis = qTemp; + // render room geometry + mesh->renderRoomGeometry(roomIndex, transp > 0); } - Core::setBlending(bmNone); - } else - return; - #ifdef LEVEL_EDITOR - return; - #endif - - // render rooms through portals recursively - Frustum *camFrustum = camera->frustum; // push camera frustum - Frustum frustum; - camera->frustum = &frustum; - - for (int i = 0; i < room.portalsCount; i++) { - TR::Room::Portal &p = room.portals[i]; - - if (p.roomIndex == from) continue; - - vec3 v[] = { - offset + p.vertices[0], - offset + p.vertices[1], - offset + p.vertices[2], - offset + p.vertices[3], - }; - - frustum = *camFrustum; - if (frustum.clipByPortal(v, 4, p.normal)) { - if (waterCache &&(level.rooms[roomIndex].flags.water ^ level.rooms[p.roomIndex].flags.water) && v[0].y == v[1].y && v[0].y == v[2].y) - waterCache->setVisible(roomIndex, p.roomIndex); - renderRoom(p.roomIndex, roomIndex); + // render room sprites + if (range.sprites.iCount) { + Core::setBlending(bmAlpha); + setRoomParams(roomIndex, Shader::SPRITE, 1.0f, 1.0f, 0.0f, 1.0f, true); + Shader *sh = Core::active.shader; + sh->setParam(uLightColor, Core::lightColor[0], MAX_LIGHTS); + sh->setParam(uLightPos, Core::lightPos[0], MAX_LIGHTS); + sh->setParam(uBasis, Core::basis); + mesh->renderRoomSprites(roomIndex); } + + Core::basis = qTemp; } - camera->frustum = camFrustum; // pop camera frustum + Core::setBlending(bmNone); } void setMainLight(Controller *controller) { @@ -603,10 +556,9 @@ struct Level : IGame { Controller *controller = (Controller*)entity.controller; - TR::Room &room = level.rooms[entity.room]; if (entity.type != TR::Entity::LARA) // TODO: remove this hack (collect conjugate room entities) - if (!room.flags.rendered || entity.flags.invisible || entity.flags.rendered) + if (!room.flags.visible || entity.flags.invisible || entity.flags.rendered) return; int16 lum = entity.intensity == -1 ? room.ambient : entity.intensity; @@ -694,28 +646,24 @@ struct Level : IGame { camera->setup(Core::pass == Core::passCompose); setupBinding(); - - // clear visibility flag for rooms - for (int i = 0; i < level.roomsCount; i++) - level.rooms[i].flags.rendered = false; - - if (Core::pass != Core::passAmbient) - for (int i = 0; i < level.entitiesCount; i++) - level.entities[i].flags.rendered = false; } - void renderRooms(int roomIndex) { + void renderRooms(int *roomsList, int roomsCount) { PROFILE_MARKER("ROOMS"); + for (int i = 0; i < level.roomsCount; i++) + level.rooms[i].flags.visible = false; + setMainLight(lara); #ifdef LEVEL_EDITOR for (int i = 0; i < level.roomsCount; i++) renderRoom(i); #else - if (!camera->cutscene) - renderRoom(roomIndex); - else // TODO: use brain + if (!camera->cutscene) { + for (int i = 0; i < roomsCount; i++) + renderRoom(roomsList[i]); + } else // TODO: use brain (track the camera room) for (int i = 0; i < level.roomsCount; i++) renderRoom(i); #endif @@ -733,12 +681,154 @@ struct Level : IGame { } } - void renderScene(int roomIndex) { - PROFILE_MARKER("SCENE"); - setup(); - renderRooms(roomIndex); + bool checkPortal(const TR::Room &room, const TR::Room::Portal &portal, const vec4 &viewPort, vec4 &clipPort) { + vec3 n = portal.normal; + vec3 v = Core::viewPos - (room.getOffset() + portal.vertices[0]); + + if (n.dot(v) <= 0.0f) + return false; + + int zClip = 0; + vec4 p[4]; + + clipPort = vec4(INF, INF, -INF, -INF); + + for (int i = 0; i < 4; i++) { + p[i] = Core::mViewProj * vec4(vec3(portal.vertices[i]) + room.getOffset(), 1.0f); + + if (p[i].w > 0.0f) { + p[i].xyz *= (1.0f / p[i].w); + + clipPort.x = min(clipPort.x, p[i].x); + clipPort.y = min(clipPort.y, p[i].y); + clipPort.z = max(clipPort.z, p[i].x); + clipPort.w = max(clipPort.w, p[i].y); + } else + zClip++; + } + + if (zClip == 4) + return false; + + if (zClip > 0) { + for (int i = 0; i < 4; i++) { + vec4 &a = p[i]; + vec4 &b = p[(i + 1) % 4]; + + if ((a.w > 0.0f) ^ (b.w > 0.0f)) { + + if (a.x < 0.0f && b.x < 0.0f) + clipPort.x = -1.0f; + else + if (a.x > 0.0f && b.x > 0.0f) + clipPort.z = 1.0f; + else { + clipPort.x = -1.0f; + clipPort.z = 1.0f; + } + + if (a.y < 0.0f && b.y < 0.0f) + clipPort.y = -1.0f; + else + if (a.y > 0.0f && b.y > 0.0f) + clipPort.w = 1.0f; + else { + clipPort.y = -1.0f; + clipPort.w = 1.0f; + } + + } + } + } + + if (clipPort.x > clipPort.z || clipPort.y > clipPort.w) + return false; + + if (clipPort.x > viewPort.z || clipPort.y > viewPort.w || clipPort.z < viewPort.x || clipPort.w < viewPort.y) + return false; + + clipPort.x = max(clipPort.x, viewPort.x); + clipPort.y = max(clipPort.y, viewPort.y); + clipPort.z = min(clipPort.z, viewPort.z); + clipPort.w = min(clipPort.w, viewPort.w); + + return true; + } + + void getVisibleRooms(int *roomsList, int &roomsCount, int from, int to, const vec4 &viewPort, bool water, int count = 0) { + if (count > 16) { + ASSERT(false); + return; + } + + TR::Room &room = level.rooms[to]; + + if (!room.flags.visible) { + if (Core::pass == Core::passCompose && water && waterCache && from != TR::NO_ROOM && (level.rooms[from].flags.water ^ level.rooms[to].flags.water)) + waterCache->setVisible(from, to); + + room.flags.visible = true; + roomsList[roomsCount++] = to; + } + + vec4 clipPort; + for (int i = 0; i < room.portalsCount; i++) { + TR::Room::Portal &p = room.portals[i]; + + if (from == room.portals[i].roomIndex || !checkPortal(room, p, viewPort, clipPort)) + continue; + + getVisibleRooms(roomsList, roomsCount, to, p.roomIndex, clipPort, water, count + 1); + } + } + + virtual void renderView(int roomIndex, bool water) { + PROFILE_MARKER("VIEW"); + Core::Pass pass = Core::pass; + + if (water && waterCache) { + waterCache->reset(); + } + + for (int i = 0; i < level.roomsCount; i++) + level.rooms[i].flags.visible = false; + + int roomsList[128]; + int roomsCount = 0; + + getVisibleRooms(roomsList, roomsCount, TR::NO_ROOM, roomIndex, vec4(-1.0f, -1.0f, 1.0f, 1.0f), water); + + if (water && waterCache) { + for (int i = 0; i < roomsCount; i++) + waterCache->setVisible(roomsList[i]); + waterCache->renderReflect(); + waterCache->simulate(); + } + + // clear visibility flag for rooms + if (Core::pass != Core::passAmbient) + for (int i = 0; i < level.entitiesCount; i++) + level.entities[i].flags.rendered = false; + + if (water) { + Core::setTarget(NULL, true); // render to back buffer + setupBinding(); + } + + camera->setup(Core::pass == Core::passCompose); + + Core::pass = pass; + + renderRooms(roomsList, roomsCount); + if (Core::pass != Core::passAmbient) renderEntities(); + + if (water && waterCache && waterCache->visible) { + waterCache->renderMask(); + waterCache->getRefract(); + waterCache->render(); + } } void setupCubeCamera(const vec3 &pos, int face) { @@ -753,12 +843,13 @@ struct Level : IGame { case 5 : dir = vec3( 0, 0, -1); break; } - Core::mViewInv = mat4(pos, pos + dir, up); - Core::mView = Core::mViewInv.inverse(); - Core::mProj = mat4(90, 1.0f, camera->znear, camera->zfar); + Core::mViewInv = mat4(pos, pos + dir, up); + Core::mView = Core::mViewInv.inverse(); + Core::mProj = mat4(90, 1.0f, camera->znear, camera->zfar); + Core::mViewProj = Core::mProj * Core::mView; } - bool setupLightCamera() { + void setupLightCamera() { vec3 pos = lara->getBoundingBox().center(); Core::mViewInv = mat4(lara->mainLightPos, pos, vec3(0, -1, 0)); @@ -770,8 +861,6 @@ struct Level : IGame { bias.e03 = bias.e13 = bias.e23 = bias.e00 = bias.e11 = bias.e22 = 0.5f; Core::mLightProj = bias * (Core::mProj * Core::mView); - - return true; } void renderShadows(int roomIndex) { @@ -781,16 +870,18 @@ struct Level : IGame { shadow->unbind(sShadow); bool colorShadow = shadow->format == Texture::Format::RGBA ? true : false; if (colorShadow) - Core::setClearColor(vec4(1.0f, 1.0f, 1.0f, 1.0f)); - Core::setTarget(shadow); - if (!setupLightCamera()) return; - Core::clear(true, true); + Core::setClearColor(vec4(1.0f)); + Core::setTarget(shadow, true); + setupLightCamera(); Core::setCulling(cfBack); - renderScene(roomIndex); + + setup(); + renderView(roomIndex, false); + Core::invalidateTarget(!colorShadow, colorShadow); Core::setCulling(cfFront); if (colorShadow) - Core::setClearColor(vec4(0.0f, 0.0f, 0.0f, 0.0f)); + Core::setClearColor(vec4(0.0f)); } #ifdef _DEBUG @@ -875,14 +966,14 @@ struct Level : IGame { glLoadIdentity(); glOrtho(0, Core::width, 0, Core::height, 0, 1); - if (shadow) - shadow->bind(sDiffuse); + if (waterCache->reflect) + waterCache->reflect->bind(sDiffuse); else atlas->bind(sDiffuse); glEnable(GL_TEXTURE_2D); - glDisable(GL_CULL_FACE); - glDisable(GL_DEPTH_TEST); - glDisable(GL_BLEND); + Core::setCulling(cfNone); + Core::setDepthTest(false); + Core::setBlending(bmNone); glColor3f(10, 10, 10); int w = Core::active.textures[sDiffuse]->width / 2; @@ -896,9 +987,6 @@ struct Level : IGame { glColor3f(1, 1, 1); glDisable(GL_TEXTURE_2D); - glEnable(GL_CULL_FACE); - glEnable(GL_DEPTH_TEST); - glEnable(GL_BLEND); glMatrixMode(GL_PROJECTION); glPopMatrix(); @@ -920,7 +1008,7 @@ struct Level : IGame { // Box bbox = lara->getBoundingBox(); // Debug::Draw::box(bbox.min, bbox.max, vec4(1, 0, 1, 1)); - Core::setBlending(bmAlpha); + // Core::setBlending(bmAlpha); // Debug::Level::rooms(level, lara->pos, lara->getEntity().room); // Debug::Level::lights(level, lara->getRoomIndex(), lara); // Debug::Level::sectors(level, lara->getRoomIndex(), (int)lara->pos.y); @@ -934,8 +1022,8 @@ struct Level : IGame { // Debug::Level::path(level, (Enemy*)level.entities[86].controller); // Debug::Level::debugOverlaps(level, lara->box); - Core::setBlending(bmNone); - + // Core::setBlending(bmNone); + Debug::Level::info(level, lara->getEntity(), lara->animation); @@ -951,28 +1039,14 @@ struct Level : IGame { if (ambientCache) ambientCache->precessQueue(); - if (waterCache) - waterCache->reset(); if (shadow) renderShadows(lara->getRoomIndex()); - Core::setClearStencil(128); - Core::setTarget(NULL, true); - - if (waterCache) - waterCache->checkVisibility = true; + if (shadow) shadow->bind(sShadow); + Core::pass = Core::passCompose; - renderCompose(camera->getRoomIndex(), true); - - if (waterCache) { - waterCache->checkVisibility = false; - if (waterCache->visible) { - waterCache->renderMask(); - waterCache->getRefract(); - waterCache->simulate(); - waterCache->render(); - } - } + setup(); + renderView(camera->getRoomIndex(), true); } void renderInventory() { @@ -1019,11 +1093,8 @@ struct Level : IGame { bool copyBg = title && lastTitle != title; if (copyBg) { - vec4 vp = Core::viewportDef; Core::defaultTarget = inventory.background[0]; - Core::setTarget(Core::defaultTarget, true); renderGame(); - Core::viewportDef = vp; Core::defaultTarget = NULL; inventory.prepareBackground(); diff --git a/src/mesh.h b/src/mesh.h index 69fe8a8..14fadc2 100644 --- a/src/mesh.h +++ b/src/mesh.h @@ -10,16 +10,6 @@ #define TEX_OXYGEN_BAR_X 1002 #define TEX_OXYGEN_BAR_Y 1000 -typedef unsigned short Index; - -struct Vertex { - short4 coord; // xyz - position, w - joint index (for entities only) - short4 normal; // xyz - vertex normalá w - unused - short4 texCoord; // xy - texture coordinates, zw - trapezoid warping - ubyte4 param; // xy - anim tex range and frame index, zw - unused - ubyte4 color; // xyz - color, w - intensity -}; - struct MeshRange { int iStart; int iCount; @@ -117,12 +107,6 @@ struct Mesh { glBindBuffer(GL_ARRAY_BUFFER, Core::active.vBuffer = ID[1]); } - void DIP(const MeshRange &range) { - glDrawElements(GL_TRIANGLES, range.iCount, GL_UNSIGNED_SHORT, (Index*)NULL + range.iStart); - Core::stats.dips++; - Core::stats.tris += range.iCount / 3; - } - void render(const MeshRange &range) { range.bind(VAO); @@ -131,15 +115,7 @@ struct Mesh { range.setup(); }; - if (Core::active.stencilTwoSide && Core::support.stencil == 0) { - Core::setCulling(cfBack); - glStencilOp(GL_KEEP, GL_DECR, GL_KEEP); - DIP(range); - Core::setCulling(cfFront); - glStencilOp(GL_KEEP, GL_INCR, GL_KEEP); - } - - DIP(range); + Core::DIP(range.iStart, range.iCount); } }; @@ -192,7 +168,7 @@ struct MeshBuilder { } *models; MeshRange *sequences; // procedured - MeshRange shadowBlob, shadowBox; + MeshRange shadowBlob; MeshRange quad, circle; MeshRange plane; @@ -289,13 +265,6 @@ struct MeshBuilder { iCount += shadowBlob.iCount; vCount += 8 * 2 + 1; - // shadow box (for stencil shadow volumes with degenerate triangles) - shadowBox.vStart = vCount; - shadowBox.iStart = iCount; - shadowBox.iCount = (3 * (2 + 4)) * 6; - iCount += shadowBox.iCount; - vCount += 4 * 6; - // quad (post effect filter) quad.vStart = vCount; quad.iStart = iCount; @@ -447,59 +416,6 @@ struct MeshBuilder { iCount += shadowBlob.iCount; aCount++; - // build shadow box volume - { - static const Index tmpIndices[] = { - 0,1,2, 0,2,3, 0,7,1, 0,4,7, - 1,11,2, 1,8,11, 2,15,3, 2,12,15, - 3,19,0, 3,16,19, 21,6,5, 21,20,6, - 20,10,9, 20,23,10, 23,14,13, 23,22,14, - 22,18,17, 22,21,18, 20,21,22, 20,22,23, - 7,6,9, 7,9,8, 11,10,13, 11,13,12, - 15,14,17, 15,17,16, 19,5,4, 19,18,5, - 4,6,7, 4,5,6, 8,9,10, 8,10,11, - 12,14,15, 12,13,14, 16,18,19, 16,17,18 - }; - static const short4 tmpCoords[] = { - { -1, -1, -1, 0 }, { 1, -1, -1, 0 }, { 1, 1, -1, 0 }, { -1, 1, -1, 0 }, - { -1, -1, -1, 0 }, { -1, -1, 1, 0 }, { 1, -1, 1, 0 }, { 1, -1, -1, 0 }, - { 1, -1, -1, 0 }, { 1, -1, 1, 0 }, { 1, 1, 1, 0 }, { 1, 1, -1, 0 }, - { 1, 1, -1, 0 }, { 1, 1, 1, 0 }, { -1, 1, 1, 0 }, { -1, 1, -1, 0 }, - { -1, 1, -1, 0 }, { -1, 1, 1, 0 }, { -1, -1, 1, 0 }, { -1, -1, -1, 0 }, - { 1, -1, 1, 0 }, { -1, -1, 1, 0 }, { -1, 1, 1, 0 }, { 1, 1, 1, 0 }, - }; - - const short n = 32767; - static const short4 tmpNormals[] = { - { 0, 0, -n, 0 }, - { 0, -n, 0, 0 }, - { n, 0, 0, 0 }, - { 0, n, 0, 0 }, - { -n, 0, 0, 0 }, - { 0, 0, n, 0 }, - }; - - static const ubyte4 tmpColors[] = { - { 255, 0, 0, 0 }, - { 0, 255, 0, 0 }, - { 0, 0, 255, 0 }, - { 255, 0, 255, 0 }, - { 255, 255, 0, 0 }, - { 0, 255, 255, 0 }, - }; - - memcpy(&indices[iCount], &tmpIndices[0], shadowBox.iCount * sizeof(Index)); - memset(&vertices[vCount], 0, 4 * 6 * sizeof(Vertex)); - for (int i = 0; i < 4 * 6; i++) { - vertices[vCount + i].coord = tmpCoords[i]; - vertices[vCount + i].normal = tmpNormals[i / 4]; - vertices[vCount + i].color = tmpColors[i / 4]; - } - iCount += shadowBox.iCount; - vCount += 4 * 6; - aCount++; - } - // quad addQuad(indices, iCount, vCount, quad.vStart, vertices, &whiteTile); vertices[vCount + 3].coord = { -1, -1, 0, 0 }; @@ -582,7 +498,6 @@ struct MeshBuilder { for (int i = 0; i < level.modelsCount; i++) mesh->initRange(models[i].geometry); mesh->initRange(shadowBlob); - mesh->initRange(shadowBox); mesh->initRange(quad); mesh->initRange(circle); mesh->initRange(plane); @@ -1146,10 +1061,6 @@ struct MeshBuilder { mesh->render(shadowBlob); } - void renderShadowBox() { - mesh->render(shadowBox); - } - void renderQuad() { mesh->render(quad); } diff --git a/src/shaders/shader.glsl b/src/shaders/shader.glsl index 2bbeac9..588ae0a 100644 --- a/src/shaders/shader.glsl +++ b/src/shaders/shader.glsl @@ -26,11 +26,11 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - trapezoidal correction varying vec3 vAmbient; #endif - uniform vec3 uLightPos[4]; - uniform vec4 uLightColor[4]; // xyz - color, w - radius * intensity + uniform vec3 uLightPos[MAX_LIGHTS]; + uniform vec4 uLightColor[MAX_LIGHTS]; // xyz - color, w - radius * intensity #endif - varying vec4 vLight; // 4 lights intensity + varying vec4 vLight; // lights intensity (MAX_LIGHTS == 4) #if defined(OPT_WATER) && defined(UNDERWATER) uniform vec4 uRoomSize; // xy - minXZ, zw - maxXZ diff --git a/src/shaders/water.glsl b/src/shaders/water.glsl index 8418596..328f8ab 100644 --- a/src/shaders/water.glsl +++ b/src/shaders/water.glsl @@ -103,6 +103,37 @@ uniform sampler2D sNormal; return v; } + vec3 hash33(vec3 p3) { + p3 = fract(p3 * vec3(.1031,.11369,.13787)); + p3 += dot(p3, p3.yxz+19.19); + return -1.0 + 2.0 * fract(vec3((p3.x + p3.y)*p3.z, (p3.x+p3.z)*p3.y, (p3.y+p3.z)*p3.x)); + } + + float simplex_noise(vec3 p) { // https://www.shadertoy.com/view/4sc3z2 + const float K1 = 0.333333333; + const float K2 = 0.166666667; + + vec3 i = floor(p + (p.x + p.y + p.z) * K1); + vec3 d0 = p - (i - (i.x + i.y + i.z) * K2); + + vec3 e = step(vec3(0.0), d0 - d0.yzx); + vec3 i1 = e * (1.0 - e.zxy); + vec3 i2 = 1.0 - e.zxy * (1.0 - e); + + vec3 d1 = d0 - (i1 - 1.0 * K2); + vec3 d2 = d0 - (i2 - 2.0 * K2); + vec3 d3 = d0 - (1.0 - 3.0 * K2); + + vec4 h = max(0.6 - vec4(dot(d0, d0), dot(d1, d1), dot(d2, d2), dot(d3, d3)), 0.0); + vec4 n = h * h * h * h * vec4(dot(d0, hash33(i)), dot(d1, hash33(i + i1)), dot(d2, hash33(i + i2)), dot(d3, hash33(i + 1.0))); + + return dot(vec4(31.316), n); + } + + float h(vec2 tc) { + return simplex_noise(vec3(tc * 16.0, uParam.w)) * 0.001; + } + vec4 calc() { vec2 tc = gl_FragCoord.xy * uTexParam.xy; @@ -125,7 +156,7 @@ uniform sampler2D sNormal; v.y += (average - v.x) * vel; v.y *= vis; - v.x += v.y; + v.x += v.y + h(tc); return v; } diff --git a/src/utils.h b/src/utils.h index 7aaa4ee..6236a08 100644 --- a/src/utils.h +++ b/src/utils.h @@ -271,6 +271,9 @@ struct vec4 { vec4(const vec3 &xyz, float w) : x(xyz.x), y(xyz.y), z(xyz.z), w(w) {} vec4(const vec2 &xy, const vec2 &zw) : x(xy.x), y(xy.y), z(zw.x), w(zw.y) {} + inline bool operator == (const vec4 &v) const { return x == v.x && y == v.y && z == v.z && w == v.w; } + inline bool operator != (const vec4 &v) const { return !(*this == v); } + vec4 operator + (const vec4 &v) const { return vec4(x + v.x, y + v.y, z + v.z, w + v.w); } vec4 operator - (const vec4 &v) const { return vec4(x - v.x, y - v.y, z - v.z, w - v.w); } vec4 operator * (const vec4 &v) const { return vec4(x*v.x, y*v.y, z*v.z, w*v.w); } From 79d213ff18e03a531be03893a1525ee06048d77c Mon Sep 17 00:00:00 2001 From: XProger Date: Mon, 24 Jul 2017 06:04:50 +0300 Subject: [PATCH 5/7] #8 remove poly-portals clipping code --- src/frustum.h | 71 --------------------------------------------------- 1 file changed, 71 deletions(-) diff --git a/src/frustum.h b/src/frustum.h index 2e5a523..cac0e62 100644 --- a/src/frustum.h +++ b/src/frustum.h @@ -6,12 +6,6 @@ #define MAX_CLIP_PLANES 16 struct Frustum { - - struct Poly { - vec3 vertices[MAX_CLIP_PLANES]; - int count; - }; - vec3 pos; vec4 planes[MAX_CLIP_PLANES * 2]; // + buffer for OBB visibility test int start, count; @@ -35,71 +29,6 @@ struct Frustum { planes[i] *= 1.0f / planes[i].xyz.length(); } - void calcPlanes(const Poly &poly) { - count = 1 + poly.count; // add one for near plane (not changing) - ASSERT(count < MAX_CLIP_PLANES); - if (count < 4) return; - - vec3 e1 = poly.vertices[0] - pos; - for (int i = 1; i < count; i++) { - vec3 e2 = poly.vertices[i % poly.count] - pos; - planes[i].xyz = e1.cross(e2).normal(); - planes[i].w = -(pos.dot(planes[i].xyz)); - e1 = e2; - } - } - - void clipPlane(const Poly &src, Poly &dst, const vec4 &plane) { - dst.count = 0; - if (!src.count) return; - - float t1 = src.vertices[0].dot(plane.xyz) + plane.w; - - for (int i = 0; i < src.count; i++) { - const vec3 &v1 = src.vertices[i]; - const vec3 &v2 = src.vertices[(i + 1) % src.count]; - - float t2 = v2.dot(plane.xyz) + plane.w; - - // hack for big float numbers - int s1 = (int)t1; - int s2 = (int)t2; - - if (s1 >= 0) { - dst.vertices[dst.count++] = v1; - ASSERT(dst.count < MAX_CLIP_PLANES); - } - - if ((s1 ^ s2) < 0) { // check for opposite signs - float k1 = t2 / (t2 - t1); - float k2 = t1 / (t2 - t1); - dst.vertices[dst.count++] = v1 * (float)k1 - v2 * (float)k2; - ASSERT(dst.count < MAX_CLIP_PLANES); - } - - t1 = t2; - } - } - - bool clipByPortal(const vec3 *vertices, int vCount, const vec3 &normal) { - if (normal.dot(pos - vertices[0]) < 0.0f) // check portal winding order - return false; - - Poly poly[2]; - - poly[0].count = vCount; - memmove(poly[0].vertices, vertices, sizeof(vec3) * poly[0].count); -#ifdef _DEBUG - debugPoly.count = 0; -#endif - int j = 0; - for (int i = 1; i < count; i++, j ^= 1) - clipPlane(poly[j], poly[j ^ 1], planes[i]); - - calcPlanes(poly[j]); - return count >= 4; - } - // AABB visibility check bool isVisible(const vec3 &min, const vec3 &max) const { if (count < 4) return false; From e2643cd78aec4c155afdc44bab3a51a6fadbde29 Mon Sep 17 00:00:00 2001 From: XProger Date: Mon, 24 Jul 2017 13:54:52 +0300 Subject: [PATCH 6/7] #8 remove frustum clipping debug poly --- src/frustum.h | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/frustum.h b/src/frustum.h index cac0e62..670b1f4 100644 --- a/src/frustum.h +++ b/src/frustum.h @@ -9,15 +9,8 @@ struct Frustum { vec3 pos; vec4 planes[MAX_CLIP_PLANES * 2]; // + buffer for OBB visibility test int start, count; -#ifdef _DEBUG - int dbg; - Poly debugPoly; -#endif void calcPlanes(const mat4 &m) { - #ifdef _DEBUG - dbg = 0; - #endif start = 0; count = 5; planes[0] = vec4(m.e30 - m.e20, m.e31 - m.e21, m.e32 - m.e22, m.e33 - m.e23); // near From 5739de604403f592efed4d2b96ec08b08646ecab Mon Sep 17 00:00:00 2001 From: XProger Date: Thu, 27 Jul 2017 04:09:49 +0300 Subject: [PATCH 7/7] #23 room render optimization for mobile devices; contact ambient occlusion (WIP); #11 inventory touch button --- src/cache.h | 4 +- src/core.h | 3 + src/debug.h | 1 + src/game.h | 1 + src/input.h | 24 +++--- src/inventory.h | 10 ++- src/level.h | 164 ++++++++++++++++++++++------------------ src/shader.h | 4 +- src/shaders/shader.glsl | 88 +++++++++++---------- src/sprite.h | 4 +- src/texture.h | 5 +- src/utils.h | 4 +- 12 files changed, 175 insertions(+), 137 deletions(-) diff --git a/src/cache.h b/src/cache.h index 4c884b3..466cb56 100644 --- a/src/cache.h +++ b/src/cache.h @@ -131,7 +131,7 @@ struct ShaderCache { typ = typeNames[type]; int animTexRangesCount = game->getMesh()->animTexRangesCount; int animTexOffsetsCount = game->getMesh()->animTexOffsetsCount; - sprintf(def, "%s#define PASS_%s\n#define TYPE_%s\n#define MAX_LIGHTS %d\n#define MAX_RANGES %d\n#define MAX_OFFSETS %d\n#define FOG_DIST (1.0/%d.0)\n#define WATER_FOG_DIST (1.0/%d.0)\n#define SHADOW_TEX_SIZE %d.0\n", ext, passNames[pass], typ, MAX_LIGHTS, animTexRangesCount, animTexOffsetsCount, FOG_DIST, WATER_FOG_DIST, SHADOW_TEX_SIZE); + sprintf(def, "%s#define PASS_%s\n#define TYPE_%s\n#define MAX_LIGHTS %d\n#define MAX_RANGES %d\n#define MAX_OFFSETS %d\n#define MAX_CONTACTS %d\n#define FOG_DIST (1.0/%d.0)\n#define WATER_FOG_DIST (1.0/%d.0)\n#define SHADOW_TEX_SIZE %d.0\n", ext, passNames[pass], typ, MAX_LIGHTS, animTexRangesCount, animTexOffsetsCount, MAX_CONTACTS, FOG_DIST, WATER_FOG_DIST, SHADOW_TEX_SIZE); if (fx & FX_UNDERWATER) strcat(def, "#define UNDERWATER\n" UNDERWATER_COLOR); if (fx & FX_ALPHA_TEST) strcat(def, "#define ALPHA_TEST\n"); if (fx & FX_CLIP_PLANE) strcat(def, "#define CLIP_PLANE\n"); @@ -139,6 +139,7 @@ struct ShaderCache { if (Core::settings.detail.lighting) strcat(def, "#define OPT_LIGHTING\n"); if (Core::settings.detail.shadows) strcat(def, "#define OPT_SHADOW\n"); if (Core::settings.detail.water) strcat(def, "#define OPT_WATER\n"); + if (Core::settings.detail.contact) strcat(def, "#define OPT_CONTACT\n"); break; } case Core::passWater : { @@ -181,7 +182,6 @@ struct ShaderCache { // TODO: bindable uniform block shader->setParam(uViewProj, Core::mViewProj); shader->setParam(uLightProj, Core::mLightProj); - shader->setParam(uViewInv, Core::mViewInv); shader->setParam(uViewPos, Core::viewPos); shader->setParam(uParam, Core::params); MeshBuilder *mesh = game->getMesh(); diff --git a/src/core.h b/src/core.h index 31b0643..aa10ddd 100644 --- a/src/core.h +++ b/src/core.h @@ -213,6 +213,7 @@ namespace Core { #define MAX_LIGHTS 4 #define MAX_CACHED_LIGHTS 3 #define MAX_RENDER_BUFFERS 32 +#define MAX_CONTACTS 15 struct Shader; struct Texture; @@ -322,6 +323,7 @@ namespace Core { vec3 lightPos[MAX_LIGHTS]; vec4 lightColor[MAX_LIGHTS]; vec4 params; + vec4 contacts[MAX_CONTACTS]; Texture *blackTex, *whiteTex; @@ -391,6 +393,7 @@ namespace Core { bool lighting; bool shadows; bool water; + bool contact; } detail; struct { diff --git a/src/debug.h b/src/debug.h index 4b43cbe..18eef15 100644 --- a/src/debug.h +++ b/src/debug.h @@ -60,6 +60,7 @@ namespace Debug { glUseProgram(0); Core::active.shader = NULL; Core::active.textures[0] = NULL; + Core::validateRenderState(); } void end() { diff --git a/src/game.h b/src/game.h index 6d88517..0fd2ef9 100644 --- a/src/game.h +++ b/src/game.h @@ -23,6 +23,7 @@ namespace Game { Core::settings.detail.lighting = true; Core::settings.detail.shadows = true; Core::settings.detail.water = Core::support.texFloat || Core::support.texHalf; + Core::settings.detail.contact = false; Core::settings.controls.retarget = true; diff --git a/src/input.h b/src/input.h index f6b436b..172eeae 100644 --- a/src/input.h +++ b/src/input.h @@ -111,7 +111,7 @@ namespace Input { } } head; - enum TouchButton { bNone, bWeapon, bWalk, bAction, bJump, bMAX }; + enum TouchButton { bNone, bWeapon, bWalk, bAction, bJump, bInventory, bMAX }; enum TouchZone { zMove, zLook, zButton, zMAX }; float touchTimerVis, touchTimerTap; @@ -245,11 +245,12 @@ namespace Input { float radius = offset; vec2 center = vec2(Core::width - offset * 0.7f, Core::height - offset * 0.7f); - btnPos[bWeapon] = center; - btnPos[bJump] = center + vec2(cosf(-PI * 0.5f), sinf(-PI * 0.5f)) * radius; - btnPos[bAction] = center + vec2(cosf(-PI * 3.0f / 4.0f), sinf(-PI * 3.0f / 4.0f)) * radius; - btnPos[bWalk] = center + vec2(cosf(-PI), sinf(-PI)) * radius; - btnRadius = Core::height * (25.0f / 1080.0f); + btnRadius = Core::height * (25.0f / 1080.0f); + btnPos[bWeapon] = center; + btnPos[bJump] = center + vec2(cosf(-PI * 0.5f), sinf(-PI * 0.5f)) * radius; + btnPos[bAction] = center + vec2(cosf(-PI * 3.0f / 4.0f), sinf(-PI * 3.0f / 4.0f)) * radius; + btnPos[bWalk] = center + vec2(cosf(-PI), sinf(-PI)) * radius; + btnPos[bInventory] = vec2(Core::width - btnRadius * 2.0f, btnRadius * 2.0f); // touch update if (checkTouchZone(zMove)) @@ -305,11 +306,12 @@ namespace Input { } switch (btn) { - case bWeapon : state[cWeapon] = true; break; - case bWalk : state[cWalk] = true; break; - case bAction : state[cAction] = true; break; - case bJump : state[cJump] = true; break; - default : ; + case bWeapon : state[cWeapon] = true; break; + case bWalk : state[cWalk] = true; break; + case bAction : state[cAction] = true; break; + case bJump : state[cJump] = true; break; + case bInventory : state[cInventory] = true; break; + default : ; } } diff --git a/src/inventory.h b/src/inventory.h index e99cd85..7926f0f 100644 --- a/src/inventory.h +++ b/src/inventory.h @@ -383,10 +383,12 @@ struct Inventory { bool ready = active && phaseRing == 1.0f && phasePage == 1.0f; if (index == targetIndex && targetPage == page && ready && !chosen) { - if (Input::state[cLeft]) { phaseSelect = 0.0f; targetIndex = (targetIndex - 1 + count) % count; } - if (Input::state[cRight]) { phaseSelect = 0.0f; targetIndex = (targetIndex + 1) % count; } - if (Input::state[cUp] && page < PAGE_ITEMS && getItemsCount(page + 1)) { phasePage = 0.0f; targetPage = Page(page + 1); } - if (Input::state[cDown] && page > PAGE_OPTION && getItemsCount(page - 1)) { phasePage = 0.0f; targetPage = Page(page - 1); } + float s = Input::touchTimerVis > 0.0f ? -1.0f : 1.0f; + + if (Input::state[cLeft] || Input::joy.L.x < -0.5f || Input::joy.R.x > 0.5f) { phaseSelect = 0.0f; targetIndex = (targetIndex - 1 + count) % count; } + if (Input::state[cRight] || Input::joy.L.x > 0.5f || Input::joy.R.x < -0.5f) { phaseSelect = 0.0f; targetIndex = (targetIndex + 1) % count; } + if ((Input::state[cUp] || Input::joy.L.y < -0.5f || Input::joy.R.y > 0.5f) && page < PAGE_ITEMS && getItemsCount(page + 1)) { phasePage = 0.0f; targetPage = Page(page + 1); } + if ((Input::state[cDown] || Input::joy.L.y > 0.5f || Input::joy.R.y < -0.5f) && page > PAGE_OPTION && getItemsCount(page - 1)) { phasePage = 0.0f; targetPage = Page(page - 1); } if (index != targetIndex) { vec3 p; diff --git a/src/level.h b/src/level.h index 94cf2e5..8ed8c19 100644 --- a/src/level.h +++ b/src/level.h @@ -408,7 +408,7 @@ struct Level : IGame { data[i * 1024 + j].b = data[i * 1024 + j].g = ((i % 8 == 0) || (j % 8 == 0)) ? 0 : 255; */ - atlas = new Texture(1024, 1024, Texture::RGBA, false, data); + atlas = new Texture(1024, 1024, Texture::RGBA, false, data, true, false); // TODO: generate mips PROFILE_LABEL(TEXTURE, atlas->ID, "atlas"); uint32 whitePix = 0xFFFFFFFF; @@ -490,61 +490,97 @@ struct Level : IGame { } Core::active.shader->setParam(uMaterial, vec4(diffuse, ambient, specular, alpha)); + if (Core::settings.detail.contact) + Core::active.shader->setParam(uContacts, Core::contacts[0], MAX_CONTACTS); } - void renderRoom(int roomIndex) { - ASSERT(roomIndex >= 0 && roomIndex < level.roomsCount); - PROFILE_MARKER("ROOM"); - - TR::Room &room = level.rooms[roomIndex]; - vec3 offset = vec3(float(room.info.x), 0.0f, float(room.info.z)); - - room.flags.visible = true; - - // room geometry & sprites - if (Core::pass != Core::passShadow) { - Basis qTemp = Core::basis; - Core::basis.translate(offset); - - MeshBuilder::RoomRange &range = mesh->rooms[roomIndex]; - - for (int transp = 0; transp < 2; transp++) { - if (!range.geometry[transp].iCount) - continue; - - Core::setBlending(transp ? bmAlpha : bmNone); - - setRoomParams(roomIndex, Shader::ROOM, 1.0f, intensityf(room.ambient), 0.0f, 1.0f, transp > 0); - Shader *sh = Core::active.shader; - sh->setParam(uLightColor, Core::lightColor[0], MAX_LIGHTS); - sh->setParam(uLightPos, Core::lightPos[0], MAX_LIGHTS); - sh->setParam(uBasis, Core::basis); - - // render room geometry - mesh->renderRoomGeometry(roomIndex, transp > 0); - } - - // render room sprites - if (range.sprites.iCount) { - Core::setBlending(bmAlpha); - setRoomParams(roomIndex, Shader::SPRITE, 1.0f, 1.0f, 0.0f, 1.0f, true); - Shader *sh = Core::active.shader; - sh->setParam(uLightColor, Core::lightColor[0], MAX_LIGHTS); - sh->setParam(uLightPos, Core::lightPos[0], MAX_LIGHTS); - sh->setParam(uBasis, Core::basis); - mesh->renderRoomSprites(roomIndex); - } - - Core::basis = qTemp; - } - Core::setBlending(bmNone); - } - void setMainLight(Controller *controller) { Core::lightPos[0] = controller->mainLightPos; Core::lightColor[0] = vec4(controller->mainLightColor.xyz, 1.0f / controller->mainLightColor.w); } + void renderRooms(int *roomsList, int roomsCount) { + PROFILE_MARKER("ROOMS"); + + for (int i = 0; i < level.roomsCount; i++) + level.rooms[i].flags.visible = false; + + for (int i = 0; i < roomsCount; i++) + level.rooms[roomsList[i]].flags.visible = true; + + if (Core::pass == Core::passShadow) + return; + + if (Core::settings.detail.contact) { + Sphere spheres[MAX_CONTACTS]; + int spheresCount; + lara->getSpheres(spheres, spheresCount); + + for (int i = 0; i < MAX_CONTACTS; i++) + if (i < spheresCount) + Core::contacts[i] = vec4(spheres[i].center, PI * spheres[i].radius * spheres[i].radius * 0.25f); + else + Core::contacts[i] = vec4(0.0f); + } + + setMainLight(lara); + + bool hasGeom[2], hasSprite; + hasGeom[0] = hasGeom[1] = hasSprite = false; + + Basis basis; + basis.identity(); + + Core::setBlending(bmNone); + for (int transp = 0; transp < 2; transp++) { + for (int i = 0; i < roomsCount; i++) { + int roomIndex = roomsList[i]; + MeshBuilder::RoomRange &range = mesh->rooms[roomIndex]; + + if (!range.geometry[transp].iCount) + continue; + + setRoomParams(roomIndex, Shader::ROOM, 1.0f, intensityf(level.rooms[roomIndex].ambient), 0.0f, 1.0f, transp > 0); + Shader *sh = Core::active.shader; + + if (!hasGeom[transp]) { + hasGeom[transp] = true; + sh->setParam(uLightColor, Core::lightColor[0], MAX_LIGHTS); + sh->setParam(uLightPos, Core::lightPos[0], MAX_LIGHTS); + } + + basis.pos = level.rooms[roomIndex].getOffset(); + sh->setParam(uBasis, basis); + mesh->renderRoomGeometry(roomIndex, transp > 0); + } + Core::setBlending(bmAlpha); + } + + basis.rot = Core::mViewInv.getRot(); + for (int i = 0; i < roomsCount; i++) { + level.rooms[roomsList[i]].flags.visible = true; + + int roomIndex = roomsList[i]; + MeshBuilder::RoomRange &range = mesh->rooms[roomIndex]; + + if (!range.sprites.iCount) + continue; + + setRoomParams(roomIndex, Shader::SPRITE, 1.0f, 1.0f, 0.0f, 1.0f, true); + Shader *sh = Core::active.shader; + if (!hasSprite) { + sh->setParam(uLightColor, Core::lightColor[0], MAX_LIGHTS); + sh->setParam(uLightPos, Core::lightPos[0], MAX_LIGHTS); + } + + basis.pos = level.rooms[roomIndex].getOffset(); + sh->setParam(uBasis, basis); + mesh->renderRoomSprites(roomIndex); + } + + Core::setBlending(bmNone); + } + void renderEntity(const TR::Entity &entity) { //if (entity.room != lara->getRoomIndex()) return; if (entity.type == TR::Entity::NONE || !entity.modelIndex) return; @@ -568,8 +604,6 @@ struct Level : IGame { type = Shader::MIRROR; int roomIndex = entity.room; -// if (entity.type == TR::Entity::LARA && ((Lara*)entity.controller)->state == Lara::STATE_WATER_OUT) -// roomIndex = ((Lara*)entity.controller)->roomPrev; setRoomParams(roomIndex, type, 1.0f, intensityf(lum), controller->specular, 1.0f, isModel ? !mesh->models[entity.modelIndex - 1].opaque : true); @@ -648,27 +682,6 @@ struct Level : IGame { setupBinding(); } - void renderRooms(int *roomsList, int roomsCount) { - PROFILE_MARKER("ROOMS"); - - for (int i = 0; i < level.roomsCount; i++) - level.rooms[i].flags.visible = false; - - setMainLight(lara); - - #ifdef LEVEL_EDITOR - for (int i = 0; i < level.roomsCount; i++) - renderRoom(i); - #else - if (!camera->cutscene) { - for (int i = 0; i < roomsCount; i++) - renderRoom(roomsList[i]); - } else // TODO: use brain (track the camera room) - for (int i = 0; i < level.roomsCount; i++) - renderRoom(i); - #endif - } - void renderEntities() { PROFILE_MARKER("ENTITIES"); for (int i = 0; i < level.entitiesCount; i++) @@ -756,6 +769,13 @@ struct Level : IGame { } void getVisibleRooms(int *roomsList, int &roomsCount, int from, int to, const vec4 &viewPort, bool water, int count = 0) { + if (camera->cutscene) { + roomsCount = level.roomsCount; + for (int i = 0; i < roomsCount; i++) + roomsList[i] = i; + return; + } + if (count > 16) { ASSERT(false); return; diff --git a/src/shader.h b/src/shader.h index ffeb37a..842ea7b 100644 --- a/src/shader.h +++ b/src/shader.h @@ -22,7 +22,6 @@ E( uParam ) \ E( uTexParam ) \ E( uViewProj ) \ - E( uViewInv ) \ E( uBasis ) \ E( uLightProj ) \ E( uMaterial ) \ @@ -33,7 +32,8 @@ E( uAnimTexRanges ) \ E( uAnimTexOffsets ) \ E( uRoomSize ) \ - E( uPosScale ) + E( uPosScale ) \ + E( uContacts ) enum AttribType { SHADER_ATTRIBS(DECL_ENUM) aMAX }; enum SamplerType { SHADER_SAMPLERS(DECL_ENUM) sMAX }; diff --git a/src/shaders/shader.glsl b/src/shaders/shader.glsl index 588ae0a..db9b99f 100644 --- a/src/shaders/shader.glsl +++ b/src/shaders/shader.glsl @@ -4,15 +4,32 @@ R"====( precision highp float; #endif +#ifdef OPT_CONTACT + varying vec3 vCoord; +#endif + varying vec4 vTexCoord; // xy - atlas coords, zw - trapezoidal correction #if defined(OPT_WATER) && defined(UNDERWATER) varying vec2 vCausticsCoord; // - xy caustics texture coord #endif -#ifndef PASS_SHADOW - uniform vec3 uViewPos; +//uniform vec4 data[MAX_RANGES + MAX_OFFSETS + 4 + 4 + 1 + 1 + MAX_LIGHTS + MAX_LIGHTS + 1 + 6 + 1 + 32 * 2]; +uniform vec2 uAnimTexRanges[MAX_RANGES]; +uniform vec2 uAnimTexOffsets[MAX_OFFSETS]; +uniform mat4 uLightProj; +uniform mat4 uViewProj; +uniform vec3 uViewPos; +uniform vec4 uParam; // x - time, y - water height, z - clip plane sign, w - clip plane height +uniform vec3 uLightPos[MAX_LIGHTS]; +uniform vec4 uLightColor[MAX_LIGHTS]; // xyz - color, w - radius * intensity +uniform vec4 uRoomSize; // xy - minXZ, zw - maxXZ +uniform vec3 uAmbient[6]; +uniform vec4 uMaterial; // x - diffuse, y - ambient, z - specular, w - alpha +uniform vec4 uBasis[32 * 2]; + +#ifndef PASS_SHADOW varying vec4 vViewVec; // xyz - dir * dist, w - coord.y varying vec4 vDiffuse; @@ -25,20 +42,11 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - trapezoidal correction #ifdef OPT_SHADOW varying vec3 vAmbient; #endif - - uniform vec3 uLightPos[MAX_LIGHTS]; - uniform vec4 uLightColor[MAX_LIGHTS]; // xyz - color, w - radius * intensity #endif varying vec4 vLight; // lights intensity (MAX_LIGHTS == 4) - #if defined(OPT_WATER) && defined(UNDERWATER) - uniform vec4 uRoomSize; // xy - minXZ, zw - maxXZ - #endif - #if defined(OPT_AMBIENT) && defined(TYPE_ENTITY) - uniform vec3 uAmbient[6]; - vec3 calcAmbient(vec3 n) { vec3 sqr = n * n; vec3 pos = step(0.0, n); @@ -48,31 +56,9 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - trapezoidal correction } #endif #endif - - uniform vec4 uParam; // x - time, y - water height, z - clip plane sign, w - clip plane height - uniform vec4 uMaterial; // x - diffuse, y - ambient, z - specular, w - alpha #endif #ifdef VERTEX - uniform mat4 uViewProj; - - uniform mat4 uViewInv; - - #ifndef PASS_AMBIENT - uniform mat4 uLightProj; - #endif - - #ifdef PASS_COMPOSE - uniform vec2 uAnimTexRanges[MAX_RANGES]; - uniform vec2 uAnimTexOffsets[MAX_OFFSETS]; - #endif - - #ifdef TYPE_ENTITY - uniform vec4 uBasis[32 * 2]; - #else - uniform vec4 uBasis[2]; - #endif - attribute vec4 aCoord; attribute vec4 aTexCoord; attribute vec4 aParam; @@ -85,8 +71,6 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - trapezoidal correction attribute vec4 aColor; #endif - #define TEXCOORD_SCALE 32767.0 - vec3 mulQuat(vec4 q, vec3 v) { return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + v * q.w); } @@ -105,10 +89,12 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - trapezoidal correction vec4 rBasisPos = uBasis[1]; #endif - vec4 coord = vec4(mulBasis(rBasisRot, rBasisPos, aCoord.xyz), rBasisPos.w); - + vec4 coord; + coord.w = rBasisPos.w; // visible flag #ifdef TYPE_SPRITE - coord.xyz += (uViewInv[0].xyz * aTexCoord.z - uViewInv[1].xyz * aTexCoord.w) * TEXCOORD_SCALE; + coord.xyz = mulBasis(rBasisRot, rBasisPos + aCoord.xyz, vec3(aTexCoord.z, -aTexCoord.w, 0.0) * 32767.0); + #else + coord.xyz = mulBasis(rBasisRot, rBasisPos, aCoord.xyz); #endif #ifndef PASS_SHADOW @@ -137,6 +123,9 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - trapezoidal correction vLightVec.w = clamp(1.0 / exp(fog), 0.0, 1.0); #endif + #ifdef OPT_CONTACT + vCoord = coord.xyz; + #endif return coord; } @@ -374,7 +363,7 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - trapezoidal correction #if defined(OPT_WATER) && defined(UNDERWATER) float calcCaustics(vec3 n) { - vec2 cc = vCausticsCoord.xy; + vec2 cc = vCausticsCoord.xy; vec2 border = vec2(256.0) / (uRoomSize.zw - uRoomSize.xy); vec2 fade = smoothstep(vec2(0.0), border, cc) * (1.0 - smoothstep(vec2(1.0) - border, vec2(1.0), cc)); return texture2D(sReflect, cc).g * max(0.0, -n.y) * fade.x * fade.y; @@ -382,6 +371,21 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - trapezoidal correction #endif #endif +#ifdef OPT_CONTACT + uniform vec4 uContacts[MAX_CONTACTS]; + + float getContactAO(vec3 p, vec3 n) { + float res = 1.0; + for (int i = 0; i < MAX_CONTACTS; i++) { + vec3 v = uContacts[i].xyz - p; + float a = uContacts[i].w; + float o = a * clamp(dot(n, v), 0.0, 1.0) / dot(v, v); + res *= clamp(1.0 - o, 0.0, 1.0); + } + return res; + } +#endif + void main() { #ifdef PASS_COMPOSE #ifdef CLIP_PLANE @@ -439,10 +443,16 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - trapezoidal correction #endif #ifdef TYPE_ROOM + light += mix(vAmbient.x, vLight.x, getShadow()); #if defined(OPT_WATER) && defined(UNDERWATER) light += calcCaustics(n); #endif + + #ifdef OPT_CONTACT + light *= getContactAO(vCoord, n) * 0.5 + 0.5; + #endif + #endif #ifdef TYPE_SPRITE diff --git a/src/sprite.h b/src/sprite.h index 5d8f2a0..3bdf41e 100644 --- a/src/sprite.h +++ b/src/sprite.h @@ -62,9 +62,7 @@ struct Sprite : Controller { } virtual void render(Frustum *frustum, MeshBuilder *mesh, Shader::Type type, bool caustics) { - Basis basis(Core::basis); - basis.translate(pos); - Core::active.shader->setParam(uBasis, basis); + Core::active.shader->setParam(uBasis, Basis(Core::mViewInv.getRot(), pos)); mesh->renderSprite(-(getEntity().modelIndex + 1), frame); } }; diff --git a/src/texture.h b/src/texture.h index 3de96df..0183f05 100644 --- a/src/texture.h +++ b/src/texture.h @@ -11,7 +11,7 @@ struct Texture { Format format; bool cube; - Texture(int width, int height, Format format, bool cube, void *data = NULL, bool filter = true) : cube(cube) { + Texture(int width, int height, Format format, bool cube, void *data = NULL, bool filter = true, bool mips = false) : cube(cube) { if (!Core::support.texNPOT) { width = nextPow2(width); height = nextPow2(height); @@ -65,8 +65,9 @@ struct Texture { float color[] = { 1.0f, 1.0f, 1.0f, 1.0f }; glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, color); } + + glTexParameteri(target, GL_TEXTURE_MIN_FILTER, filter ? (mips ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR ) : ( mips ? GL_NEAREST_MIPMAP_NEAREST : GL_NEAREST )); glTexParameteri(target, GL_TEXTURE_MAG_FILTER, filter ? GL_LINEAR : GL_NEAREST); - glTexParameteri(target, GL_TEXTURE_MIN_FILTER, filter ? GL_LINEAR : GL_NEAREST); struct FormatDesc { GLuint ifmt, fmt; diff --git a/src/utils.h b/src/utils.h index 6236a08..a8ce1cd 100644 --- a/src/utils.h +++ b/src/utils.h @@ -602,7 +602,7 @@ struct mat4 { quat getRot() const { float t, s; t = 1.0f + e00 + e11 + e22; - if (t > EPS) { + if (t > 0.0001f) { s = 0.5f / sqrtf(t); return quat((e21 - e12) * s, (e02 - e20) * s, (e10 - e01) * s, 0.25f / s); } else @@ -657,7 +657,7 @@ struct mat4 { }; struct Basis { - quat rot; + quat rot; vec3 pos; float w;