Files
jak-project/game/graphics/opengl_renderer/ocean/OceanEnvmap.cpp
T
Hat Kid 25e105aa5d jak2/3: implement ocean envmap (#4303)
Implements the envmap for the ocean generated by
`ocean-method-89`/`ocean-method-88`.

While the resulting envmap looks accurate compared to PCSX2 in
Renderdoc, the end result does not seem 100% identical, but it is a big
improvement over the default placeholder texture.

Closes #3417
2026-06-07 07:27:43 +02:00

308 lines
9.4 KiB
C++

#include "OceanEnvmap.h"
#include <cstring>
#include "common/dma/gs.h"
#include "game/graphics/texture/TexturePool.h"
#include "fmt/format.h"
#include "third-party/imgui/imgui.h"
namespace {
/*!
* check if a gif packet from a dma-buffer-add-gs-set contains a given register address
*/
bool scan_gs_set(const u8* data, const u32 size, GsRegisterAddress reg, u64* out) {
if (size < 16) {
return false;
}
GifTag tag(data);
if (tag.flg() != GifTag::Format::PACKED) {
return false;
}
u32 nreg = tag.nreg();
u32 offset = 16;
for (u32 loop = 0; loop < tag.nloop(); loop++) {
for (u32 r = 0; r < nreg; r++) {
if (offset + 16 > size) {
return false;
}
if (tag.reg(r) == GifTag::RegisterDescriptor::AD) {
u64 value;
u8 addr;
memcpy(&value, data + offset, sizeof(value));
memcpy(&addr, data + offset + 8, sizeof(addr));
if (addr == (u8)reg) {
*out = value;
return true;
}
}
offset += 16;
}
}
return false;
}
bool is_untextured_draw(const u8* data, u32 size) {
if (size < 16) {
return false;
}
GifTag tag(data);
if (!tag.pre()) {
return false;
}
return !GsPrim(tag.prim()).tme();
}
bool find_sky_color(DmaFollower dma, u32 next_bucket, u8 out[4]) {
for (int i = 0; i < 256 && dma.current_tag_offset() != next_bucket; i++) {
auto d = dma.read_and_advance();
if (d.size_bytes >= 32 && is_untextured_draw(d.data, d.size_bytes)) {
out[0] = d.data[16 + 0];
out[1] = d.data[16 + 4];
out[2] = d.data[16 + 8];
out[3] = d.data[16 + 12];
return true;
}
}
return false;
}
} // namespace
OceanEnvmap::OceanEnvmap(const std::string& name, int my_id, int batch_size)
: DirectRenderer(name, my_id, batch_size),
m_first_pass_fb(ENVMAP_WIDTH, ENVMAP_HEIGHT, GL_UNSIGNED_INT_8_8_8_8_REV),
m_envmap_fb(ENVMAP_WIDTH, ENVMAP_HEIGHT, GL_UNSIGNED_INT_8_8_8_8_REV) {}
static void make_single_level_linear(const GLuint tex) {
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
glBindTexture(GL_TEXTURE_2D, 0);
}
void OceanEnvmap::init_textures(TexturePool& pool, GameVersion version) {
TextureInput in;
in.w = ENVMAP_WIDTH;
in.h = ENVMAP_HEIGHT;
in.debug_page_name = "PC-OCEAN-ENVMAP";
in.gpu_texture = m_envmap_fb.texture();
in.debug_name = "ocean-envmap";
in.id = pool.allocate_pc_port_texture(version);
m_envmap_gpu_tex = pool.give_texture_and_load_to_vram(in, ENVMAP_VRAM_ADDR);
make_single_level_linear(m_first_pass_fb.texture());
make_single_level_linear(m_envmap_fb.texture());
glGenVertexArrays(1, &m_radial_vao);
glBindVertexArray(m_radial_vao);
glGenBuffers(1, &m_radial_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_radial_vbo);
const float verts[8] = {-1.f, -1.f, -1.f, 1.f, 1.f, -1.f, 1.f, 1.f};
glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glGenVertexArrays(1, &m_haze_vao);
glBindVertexArray(m_haze_vao);
glGenBuffers(1, &m_haze_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_haze_vbo);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 6, (void*)nullptr);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(float) * 6, (void*)(2 * sizeof(float)));
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
void OceanEnvmap::render_haze(const u8* gif_data, u32 size, SharedRenderState* render_state) {
if (size < 16) {
return;
}
const float x_off = m_prim_buffer.x_off;
const float y_off = m_prim_buffer.y_off;
GifTag tag(gif_data);
if (tag.flg() != GifTag::Format::PACKED) {
return;
}
const u32 nreg = tag.nreg();
std::vector<float> verts;
verts.reserve(tag.nloop() * 2 * 6);
float cur[4] = {1.f, 1.f, 1.f, 1.f};
u32 offset = 16;
for (u32 loop = 0; loop < tag.nloop(); loop++) {
for (u32 r = 0; r < nreg; r++) {
if (offset + 16 > size) {
break;
}
const u8* d = gif_data + offset;
switch (tag.reg(r)) {
case GifTag::RegisterDescriptor::RGBAQ:
cur[0] = d[0] / 255.f;
cur[1] = d[4] / 255.f;
cur[2] = d[8] / 255.f;
cur[3] = d[12] / 255.f;
break;
case GifTag::RegisterDescriptor::XYZF2: {
u16 rawx, rawy;
memcpy(&rawx, d + 0, 2);
memcpy(&rawy, d + 4, 2);
float px = rawx / 65536.f + x_off;
float py = rawy / 65536.f + y_off;
float ndc_x = (px - 0.453125f) * 64.f;
float ndc_y = (py - 0.5f + (2.25f / 64.f)) * 64.f;
verts.insert(verts.end(), {ndc_x, ndc_y, cur[0], cur[1], cur[2], cur[3] * 2.f});
} break;
default:
break;
}
offset += 16;
}
}
if (verts.size() < 6 * 3) {
return;
}
GLboolean depth = glIsEnabled(GL_DEPTH_TEST);
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
render_state->shaders[ShaderId::OCEAN_ENVMAP_HAZE].activate();
glBindVertexArray(m_haze_vao);
glBindBuffer(GL_ARRAY_BUFFER, m_haze_vbo);
glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(), GL_STREAM_DRAW);
glDrawArrays(GL_TRIANGLE_STRIP, 0, (GLsizei)(verts.size() / 6));
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
if (depth) {
glEnable(GL_DEPTH_TEST);
}
}
void OceanEnvmap::render_envmap(SharedRenderState* render_state) {
GLboolean blend = glIsEnabled(GL_BLEND);
GLboolean depth_test = glIsEnabled(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
FramebufferTexturePairContext ctxt(m_envmap_fb);
glViewport(0, 0, ENVMAP_WIDTH, ENVMAP_HEIGHT);
auto shader = &render_state->shaders[ShaderId::OCEAN_ENVMAP];
shader->activate();
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, m_first_pass_fb.texture());
glBindVertexArray(m_radial_vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
if (blend) {
glEnable(GL_BLEND);
}
if (depth_test) {
glEnable(GL_DEPTH_TEST);
}
}
void OceanEnvmap::handle_ocean_envmap_jak2(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& prof) {
u8 sky[4] = {0, 0, 0, 255};
find_sky_color(dma, render_state->next_bucket, sky);
auto scissor_backup = m_scissor;
bool active = false;
bool registered = false;
int setup64_count = 0;
for (int guard = 0; guard < 4096; guard++) {
DmaFollower peek = dma;
if (peek.current_tag_offset() == render_state->next_bucket) {
break;
}
auto next = peek.read_and_advance();
u64 scissor = 0;
u64 frame = 0;
bool has_scissor =
scan_gs_set(next.data, next.size_bytes, GsRegisterAddress::SCISSOR_1, &scissor);
bool has_frame = scan_gs_set(next.data, next.size_bytes, GsRegisterAddress::FRAME_1, &frame);
if (has_scissor && GsScissor(scissor).x1() == 127) {
break;
}
bool is_reset = has_scissor && GsScissor(scissor).x1() != ENVMAP_WIDTH - 1;
auto data = dma.read_and_advance();
if (has_scissor && GsScissor(scissor).x1() == ENVMAP_WIDTH - 1) {
setup64_count++;
u32 page_tbp = has_frame ? GsFrame(frame).fbp() << 5 : 0;
if (active) {
flush_pending(render_state, prof);
}
m_fb_ctxt.reset();
reset_state();
if (setup64_count == 1) {
m_offscreen_mode = true;
m_fb_ctxt.emplace(m_first_pass_fb);
glViewport(0, 0, ENVMAP_WIDTH * 2, ENVMAP_HEIGHT * 2);
glClearColor(sky[0] / 255.f, sky[1] / 255.f, sky[2] / 255.f, sky[3] / 255.f);
glClear(GL_COLOR_BUFFER_BIT);
active = true;
} else if (setup64_count == 2) {
m_offscreen_mode = false;
if (page_tbp) {
render_state->texture_pool->move_existing_to_vram(m_envmap_gpu_tex, page_tbp);
registered = true;
}
}
}
if (active && setup64_count == 1 && !is_reset && data.size_bytes >= 16 &&
data.vifcode1().kind == VifCode::Kind::DIRECT &&
!is_untextured_draw(data.data, data.size_bytes)) {
render_gif(data.data, data.size_bytes, render_state, prof);
}
// "haze" effect
if (active && setup64_count == 1 && !is_reset && data.size_bytes >= 16 &&
is_untextured_draw(data.data, data.size_bytes) &&
GsPrim(GifTag(data.data).prim()).kind() == GsPrim::Kind::TRI_STRIP) {
flush_pending(render_state, prof);
render_haze(data.data, data.size_bytes, render_state);
reinitialize_gl_state();
}
}
if (active) {
flush_pending(render_state, prof);
m_fb_ctxt.reset();
m_offscreen_mode = false;
render_envmap(render_state);
m_scissor = scissor_backup;
if (!registered) {
render_state->texture_pool->move_existing_to_vram(m_envmap_gpu_tex, ENVMAP_VRAM_ADDR);
}
}
}
void OceanEnvmap::draw_debug_window() {
ImGui::Text("first pass envmap");
ImGui::Image((ImTextureID)(intptr_t)m_first_pass_fb.texture(),
ImVec2(ENVMAP_WIDTH * 2, ENVMAP_HEIGHT * 2));
ImGui::SameLine();
ImGui::Image((ImTextureID)(intptr_t)m_envmap_fb.texture(),
ImVec2(ENVMAP_WIDTH * 2, ENVMAP_HEIGHT * 2));
}