scummvm/engines/stark/gfx/openglactor.cpp
2021-12-20 20:15:17 +01:00

489 lines
16 KiB
C++

/* ResidualVM - A 3D game interpreter
*
* ResidualVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the AUTHORS
* file distributed with this source distribution.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#include "engines/stark/gfx/openglactor.h"
#include "engines/stark/model/model.h"
#include "engines/stark/model/animhandler.h"
#include "engines/stark/scene.h"
#include "engines/stark/services/services.h"
#include "engines/stark/services/settings.h"
#include "engines/stark/gfx/texture.h"
#include "math/vector2d.h"
#if defined(USE_OPENGL_GAME)
namespace Stark {
namespace Gfx {
OpenGLActorRenderer::OpenGLActorRenderer(OpenGLDriver *gfx) :
VisualActor(),
_gfx(gfx),
_faceVBO(nullptr) {
}
OpenGLActorRenderer::~OpenGLActorRenderer() {
clearVertices();
}
void OpenGLActorRenderer::render(const Math::Vector3d &position, float direction, const LightEntryArray &lights) {
if (_modelIsDirty) {
clearVertices();
uploadVertices();
_modelIsDirty = false;
}
// TODO: Move updates outside of the rendering code
_animHandler->animate(_time);
_model->updateBoundingBox();
bool drawShadow = false;
if (_castsShadow &&
StarkScene->shouldRenderShadows() &&
StarkSettings->getBoolSetting(Settings::kShadow)) {
drawShadow = true;
}
Math::Vector3d lightDirection;
_gfx->set3DMode();
if (!_gfx->computeLightsEnabled())
_gfx->setupLights(lights);
Math::Matrix4 model = getModelMatrix(position, direction);
Math::Matrix4 view = StarkScene->getViewMatrix();
Math::Matrix4 projection = StarkScene->getProjectionMatrix();
Math::Matrix4 modelViewMatrix = view * model;
modelViewMatrix.transpose(); // OpenGL expects matrices transposed
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(modelViewMatrix.getData());
Math::Matrix4 projectionMatrix = projection;
projectionMatrix.transpose(); // OpenGL expects matrices transposed
glMatrixMode(GL_PROJECTION);
glLoadMatrixf(projectionMatrix.getData());
Math::Matrix4 normalMatrix;
if (_gfx->computeLightsEnabled()) {
projectionMatrix.transpose();
modelViewMatrix.transpose();
normalMatrix = modelViewMatrix;
normalMatrix.invertAffineOrthonormal();
}
Math::Matrix4 mvp;
if (drawShadow) {
mvp = view * model;
mvp.transpose();
Math::Matrix4 modelInverse = model;
modelInverse.inverse();
lightDirection = getShadowLightDirection(lights, position, modelInverse.getRotation());
}
Common::Array<Face *> faces = _model->getFaces();
Common::Array<Material *> mats = _model->getMaterials();
const Common::Array<BoneNode *> &bones = _model->getBones();
if (!_gfx->computeLightsEnabled()) {
glColorMaterial(GL_FRONT_AND_BACK, GL_DIFFUSE);
glEnable(GL_COLOR_MATERIAL);
}
for (Common::Array<Face *>::const_iterator face = faces.begin(); face != faces.end(); ++face) {
const Material *material = mats[(*face)->materialId];
Math::Vector3d color;
const Gfx::Texture *tex = resolveTexture(material);
if (tex) {
tex->bind();
glEnable(GL_TEXTURE_2D);
} else {
glBindTexture(GL_TEXTURE_2D, 0);
glDisable(GL_TEXTURE_2D);
}
auto vertexIndices = _faceEBO[*face];
auto numVertexIndices = (*face)->vertexIndices.size();
for (uint32 i = 0; i < numVertexIndices; i++) {
if (tex) {
if (_gfx->computeLightsEnabled())
color = Math::Vector3d(1.0f, 1.0f, 1.0f);
else
glColor3f(1.0f, 1.0f, 1.0f);
} else {
if (_gfx->computeLightsEnabled())
color = Math::Vector3d(material->r, material->g, material->b);
else
glColor3f(material->r, material->g, material->b);
}
uint32 index = vertexIndices[i];
auto vertex = _faceVBO[index];
uint32 bone1 = vertex.bone1;
uint32 bone2 = vertex.bone2;
Math::Vector3d position1 = Math::Vector3d(vertex.pos1x, vertex.pos1y, vertex.pos1z);
Math::Vector3d position2 = Math::Vector3d(vertex.pos2x, vertex.pos2y, vertex.pos2z);
Math::Vector3d bone1Position = Math::Vector3d(bones[bone1]->_animPos.x(),
bones[bone1]->_animPos.y(),
bones[bone1]->_animPos.z());
Math::Vector3d bone2Position = Math::Vector3d(bones[bone2]->_animPos.x(),
bones[bone2]->_animPos.y(),
bones[bone2]->_animPos.z());
Math::Quaternion bone1Rotation = Math::Quaternion(bones[bone1]->_animRot.x(),
bones[bone1]->_animRot.y(),
bones[bone1]->_animRot.z(),
bones[bone1]->_animRot.w());
Math::Quaternion bone2Rotation = Math::Quaternion(bones[bone2]->_animRot.x(),
bones[bone2]->_animRot.y(),
bones[bone2]->_animRot.z(),
bones[bone2]->_animRot.w());
float boneWeight = vertex.boneWeight;
Math::Vector3d normal = Math::Vector3d(vertex.normalx, vertex.normaly, vertex.normalz);
// Compute the vertex position in eye-space
bone1Rotation.transform(position1);
position1 += bone1Position;
bone2Rotation.transform(position2);
position2 += bone2Position;
Math::Vector3d modelPosition = Math::Vector3d::interpolate(position2, position1, boneWeight);
vertex.x = modelPosition.x();
vertex.y = modelPosition.y();
vertex.z = modelPosition.z();
Math::Vector4d modelEyePosition;
if (_gfx->computeLightsEnabled()) {
modelEyePosition = modelViewMatrix * Math::Vector4d(modelPosition.x(),
modelPosition.y(),
modelPosition.z(),
1.0);
}
// Compute the vertex normal in eye-space
Math::Vector3d n1 = normal;
bone1Rotation.transform(n1);
Math::Vector3d n2 = normal;
bone2Rotation.transform(n2);
Math::Vector3d modelNormal = Math::Vector3d(Math::Vector3d::interpolate(n2, n1, boneWeight)).getNormalized();
vertex.nx = modelNormal.x();
vertex.ny = modelNormal.y();
vertex.nz = modelNormal.z();
Math::Vector3d modelEyeNormal;
if (_gfx->computeLightsEnabled()) {
modelEyeNormal = normalMatrix.getRotation() * modelNormal;
modelEyeNormal.normalize();
}
if (drawShadow) {
Math::Vector3d shadowPosition = modelPosition + lightDirection * (-modelPosition.y() / lightDirection.y());
vertex.sx = shadowPosition.x();
vertex.sy = 0.0f;
vertex.sz = shadowPosition.z();
}
if (_gfx->computeLightsEnabled()) {
static const uint maxLights = 10;
assert(lights.size() >= 1);
assert(lights.size() <= maxLights);
const LightEntry *ambient = lights[0];
assert(ambient->type == LightEntry::kAmbient); // The first light must be the ambient light
Math::Vector3d lightColor = ambient->color;
for (uint li = 0; li < lights.size() - 1; li++) {
const LightEntry *l = lights[li + 1];
switch (l->type) {
case LightEntry::kPoint: {
Math::Vector3d vertexToLight = l->eyePosition.getXYZ() - modelEyePosition.getXYZ();
float dist = vertexToLight.length();
vertexToLight.normalize();
float attn = CLIP((l->falloffFar - dist) / MAX(0.001f, l->falloffFar - l->falloffNear), 0.0f, 1.0f);
float incidence = MAX(0.0f, Math::Vector3d::dotProduct(modelEyeNormal, vertexToLight));
lightColor += l->color * attn * incidence;
break;
}
case LightEntry::kDirectional: {
float incidence = MAX(0.0f, Math::Vector3d::dotProduct(modelEyeNormal, -l->eyeDirection));
lightColor += (l->color * incidence);
break;
}
case LightEntry::kSpot: {
Math::Vector3d vertexToLight = l->eyePosition.getXYZ() - modelEyePosition.getXYZ();
float dist = vertexToLight.length();
float attn = CLIP((l->falloffFar - dist) / MAX(0.001f, l->falloffFar - l->falloffNear), 0.0f, 1.0f);
vertexToLight.normalize();
float incidence = MAX(0.0f, modelEyeNormal.dotProduct(vertexToLight));
float cosAngle = MAX(0.0f, vertexToLight.dotProduct(-l->eyeDirection));
float cone = CLIP((cosAngle - l->innerConeAngle.getCosine()) / MAX(0.001f, l->outerConeAngle.getCosine() - l->innerConeAngle.getCosine()), 0.0f, 1.0f);
lightColor += l->color * attn * incidence * cone;
break;
}
default:
break;
}
}
lightColor.x() = CLIP(lightColor.x(), 0.0f, 1.0f);
lightColor.y() = CLIP(lightColor.y(), 0.0f, 1.0f);
lightColor.z() = CLIP(lightColor.z(), 0.0f, 1.0f);
color = color * lightColor;
vertex.r = color.x();
vertex.g = color.y();
vertex.b = color.z();
}
_faceVBO[index] = vertex;
}
glEnableClientState(GL_VERTEX_ARRAY);
if (_gfx->computeLightsEnabled())
glEnableClientState(GL_COLOR_ARRAY);
if (tex)
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);
glVertexPointer(3, GL_FLOAT, sizeof(ActorVertex), &_faceVBO[0].x);
if (tex)
glTexCoordPointer(2, GL_FLOAT, sizeof(ActorVertex), &_faceVBO[0].texS);
glNormalPointer(GL_FLOAT, sizeof(ActorVertex), &_faceVBO[0].nx);
if (_gfx->computeLightsEnabled())
glColorPointer(3, GL_FLOAT, sizeof(ActorVertex), &_faceVBO[0].r);
glDrawElements(GL_TRIANGLES, numVertexIndices, GL_UNSIGNED_INT, vertexIndices);
glDisableClientState(GL_VERTEX_ARRAY);
if (_gfx->computeLightsEnabled())
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
}
if (!_gfx->computeLightsEnabled())
glDisable(GL_COLOR_MATERIAL);
if (drawShadow) {
glEnable(GL_BLEND);
glEnable(GL_STENCIL_TEST);
glDisable(GL_TEXTURE_2D);
if (!_gfx->computeLightsEnabled())
glDisable(GL_LIGHTING);
glColor4f(0.0f, 0.0f, 0.0f, 0.5f);
for (Common::Array<Face *>::const_iterator face = faces.begin(); face != faces.end(); ++face) {
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, sizeof(ActorVertex), &_faceVBO[0].sx);
glDrawElements(GL_TRIANGLES, (*face)->vertexIndices.size(), GL_UNSIGNED_INT, _faceEBO[*face]);
glDisableClientState(GL_VERTEX_ARRAY);
}
if (!_gfx->computeLightsEnabled())
glEnable(GL_LIGHTING);
glDisable(GL_BLEND);
glDisable(GL_STENCIL_TEST);
}
}
void OpenGLActorRenderer::clearVertices() {
delete[] _faceVBO;
_faceVBO = nullptr;
for (FaceBufferMap::iterator it = _faceEBO.begin(); it != _faceEBO.end(); ++it) {
delete[] it->_value;
}
_faceEBO.clear();
}
void OpenGLActorRenderer::uploadVertices() {
_faceVBO = createModelVBO(_model);
Common::Array<Face *> faces = _model->getFaces();
for (Common::Array<Face *>::const_iterator face = faces.begin(); face != faces.end(); ++face) {
_faceEBO[*face] = createFaceEBO(*face);
}
}
ActorVertex *OpenGLActorRenderer::createModelVBO(const Model *model) {
const Common::Array<VertNode *> &modelVertices = model->getVertices();
auto vertices = new ActorVertex[modelVertices.size()];
// Build a vertex array
int i = 0;
for (Common::Array<VertNode *>::const_iterator tri = modelVertices.begin(); tri != modelVertices.end(); ++tri, i++) {
vertices[i].pos1x = (*tri)->_pos1.x();
vertices[i].pos1y = (*tri)->_pos1.y();
vertices[i].pos1z = (*tri)->_pos1.z();
vertices[i].pos2x = (*tri)->_pos2.x();
vertices[i].pos2y = (*tri)->_pos2.y();
vertices[i].pos2z = (*tri)->_pos2.z();
vertices[i].bone1 = (*tri)->_bone1;
vertices[i].bone2 = (*tri)->_bone2;
vertices[i].boneWeight = (*tri)->_boneWeight;
vertices[i].normalx = (*tri)->_normal.x();
vertices[i].normaly = (*tri)->_normal.y();
vertices[i].normalz = (*tri)->_normal.z();
vertices[i].texS = -(*tri)->_texS;
vertices[i].texT = (*tri)->_texT;
}
return vertices;
}
uint32 *OpenGLActorRenderer::createFaceEBO(const Face *face) {
auto indices = new uint32[face->vertexIndices.size()];
for (uint32 index = 0; index < face->vertexIndices.size(); index++) {
indices[index] = face->vertexIndices[index];
}
return indices;
}
Math::Vector3d OpenGLActorRenderer::getShadowLightDirection(const LightEntryArray &lights, const Math::Vector3d &actorPosition,
Math::Matrix3 worldToModelRot) {
Math::Vector3d sumDirection;
bool hasLight = false;
// Compute the contribution from each lights
// The ambient light is skipped intentionally
for (uint i = 1; i < lights.size(); ++i) {
LightEntry *light = lights[i];
bool contributes = false;
Math::Vector3d lightDirection;
switch (light->type) {
case LightEntry::kPoint:
contributes = getPointLightContribution(light, actorPosition, lightDirection);
break;
case LightEntry::kDirectional:
contributes = getDirectionalLightContribution(light, lightDirection);
break;
case LightEntry::kSpot:
contributes = getSpotLightContribution(light, actorPosition, lightDirection);
break;
case LightEntry::kAmbient:
default:
break;
}
if (contributes) {
sumDirection += lightDirection;
hasLight = true;
}
}
if (hasLight) {
// Clip the horizontal length
Math::Vector2d horizontalProjection(sumDirection.x(), sumDirection.y());
float shadowLength = MIN(horizontalProjection.getMagnitude(), StarkScene->getMaxShadowLength());
horizontalProjection.normalize();
horizontalProjection *= shadowLength;
sumDirection.x() = horizontalProjection.getX();
sumDirection.y() = horizontalProjection.getY();
sumDirection.z() = -1;
} else {
// Cast from above by default
sumDirection.x() = 0;
sumDirection.y() = 0;
sumDirection.z() = -1;
}
//Transform the direction to the model space and pass to the shader
return worldToModelRot * sumDirection;
}
bool OpenGLActorRenderer::getPointLightContribution(LightEntry *light, const Math::Vector3d &actorPosition,
Math::Vector3d &direction, float weight) {
float distance = light->position.getDistanceTo(actorPosition);
if (distance > light->falloffFar) {
return false;
}
float factor;
if (distance > light->falloffNear) {
if (light->falloffFar - light->falloffNear > 1) {
factor = 1 - (distance - light->falloffNear) / (light->falloffFar - light->falloffNear);
} else {
factor = 0;
}
} else {
factor = 1;
}
float brightness = (light->color.x() + light->color.y() + light->color.z()) / 3.0f;
if (factor <= 0 || brightness <= 0) {
return false;
}
direction = actorPosition - light->position;
direction.normalize();
direction *= factor * brightness * weight;
return true;
}
bool OpenGLActorRenderer::getDirectionalLightContribution(LightEntry *light, Math::Vector3d &direction) {
float brightness = (light->color.x() + light->color.y() + light->color.z()) / 3.0f;
if (brightness <= 0) {
return false;
}
direction = light->direction;
direction.normalize();
direction *= brightness;
return true;
}
bool OpenGLActorRenderer::getSpotLightContribution(LightEntry *light, const Math::Vector3d &actorPosition,
Math::Vector3d &direction) {
Math::Vector3d lightToActor = actorPosition - light->position;
lightToActor.normalize();
float cosAngle = MAX(0.0f, lightToActor.dotProduct(light->direction));
float cone = (cosAngle - light->innerConeAngle.getCosine()) /
MAX(0.001f, light->outerConeAngle.getCosine() - light->innerConeAngle.getCosine());
cone = CLIP(cone, 0.0f, 1.0f);
if (cone <= 0) {
return false;
}
return getPointLightContribution(light, actorPosition, direction, cone);
}
} // End of namespace Gfx
} // End of namespace Stark
#endif // defined(USE_OPENGL_GAME)