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
This commit is contained in:
Hat Kid
2026-06-07 07:27:43 +02:00
committed by GitHub
parent b948e954d0
commit 25e105aa5d
14 changed files with 417 additions and 28 deletions
+1
View File
@@ -59,6 +59,7 @@ set(RUNTIME_SOURCE
graphics/opengl_renderer/loader/Loader.cpp
graphics/opengl_renderer/loader/LoaderStages.cpp
graphics/opengl_renderer/ocean/CommonOceanRenderer.cpp
graphics/opengl_renderer/ocean/OceanEnvmap.cpp
graphics/opengl_renderer/ocean/OceanMid_PS2.cpp
graphics/opengl_renderer/ocean/OceanMid.cpp
graphics/opengl_renderer/ocean/OceanMidAndFar.cpp
+2
View File
@@ -134,6 +134,8 @@ ShaderLibrary::ShaderLibrary(GameVersion version) {
at(ShaderId::TIE_WIND) = {"tie_wind", version};
at(ShaderId::SIMPLE_TEXTURE) = {"simple_texture", version};
at(ShaderId::SLOW_TIME) = {"slow_time", version};
at(ShaderId::OCEAN_ENVMAP) = {"ocean_envmap", version};
at(ShaderId::OCEAN_ENVMAP_HAZE) = {"ocean_envmap_haze", version};
for (auto& shader : m_shaders) {
ASSERT_MSG(shader.okay(), "error compiling shader");
+2
View File
@@ -67,6 +67,8 @@ enum class ShaderId {
TIE_WIND = 40,
SIMPLE_TEXTURE = 41,
SLOW_TIME = 42,
OCEAN_ENVMAP = 43,
OCEAN_ENVMAP_HAZE = 44,
MAX_SHADERS
};
@@ -0,0 +1,307 @@
#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));
}
@@ -0,0 +1,40 @@
#pragma once
#include <optional>
#include "game/graphics/opengl_renderer/DirectRenderer.h"
#include "game/graphics/opengl_renderer/opengl_utils.h"
/*!
* This class generates the ocean envmap texture using the sky + time of day (ocean-method-89).
*/
class OceanEnvmap : public DirectRenderer {
public:
// (-> *ocean-envmap-texture-base* vram-block)
static constexpr int ENVMAP_VRAM_ADDR = 0xf80;
static constexpr int ENVMAP_WIDTH = 64;
static constexpr int ENVMAP_HEIGHT = 64;
OceanEnvmap(const std::string& name, int my_id, int batch_size);
void init_textures(TexturePool& pool, GameVersion version) override;
void handle_ocean_envmap_jak2(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& prof);
void draw_debug_window() override;
private:
FramebufferTexturePair m_first_pass_fb;
FramebufferTexturePair m_envmap_fb;
std::optional<FramebufferTexturePairContext> m_fb_ctxt;
GpuTexture* m_envmap_gpu_tex = nullptr;
// ocean-method-85
GLuint m_haze_vao = 0;
GLuint m_haze_vbo = 0;
void render_haze(const u8* gif_data, u32 size, SharedRenderState* render_state);
// ocean-method-83
GLuint m_radial_vao = 0;
GLuint m_radial_vbo = 0;
void render_envmap(SharedRenderState* render_state);
};
@@ -3,15 +3,23 @@
#include "third-party/imgui/imgui.h"
OceanMidAndFar::OceanMidAndFar(const std::string& name, int my_id)
: BucketRenderer(name, my_id), m_direct(name, my_id, 4096), m_texture_renderer(true) {}
: BucketRenderer(name, my_id),
m_direct(name, my_id, 4096),
m_envmap_renderer(name + "-envmap", my_id, 4096),
m_texture_renderer(true) {}
void OceanMidAndFar::draw_debug_window() {
if (ImGui::TreeNode("envmap")) {
m_envmap_renderer.draw_debug_window();
ImGui::TreePop();
}
m_texture_renderer.draw_debug_window();
m_direct.draw_debug_window();
}
void OceanMidAndFar::init_textures(TexturePool& pool, GameVersion version) {
m_texture_renderer.init_textures(pool, version);
m_envmap_renderer.init_textures(pool, version);
}
void OceanMidAndFar::render(DmaFollower& dma,
@@ -96,15 +104,16 @@ void OceanMidAndFar::render_jak2(DmaFollower& dma,
}
m_direct.reset_state();
// TODO handle ocean::89 and ocean::79
// handle_ocean_89_jak2(dma, render_state, prof);
{
auto p = prof.make_scoped_child("envmap");
m_envmap_renderer.handle_ocean_envmap_jak2(dma, render_state, p);
}
{
auto p = prof.make_scoped_child("texture");
m_texture_renderer.handle_ocean_texture_jak2(dma, render_state, p);
}
// handle_ocean_79_jak2(dma, render_state, prof);
handle_ocean_far(dma, render_state, prof);
m_direct.flush_pending(render_state, prof);
@@ -188,8 +197,4 @@ void OceanMidAndFar::handle_ocean_mid(DmaFollower& dma,
while (!is_end_tag(dma.current_tag(), dma.current_tag_vifcode0(), dma.current_tag_vifcode1())) {
dma.read_and_advance();
}
}
void handle_ocean_89_jak2(DmaFollower&, SharedRenderState*, ScopedProfilerNode&) {}
void handle_ocean_79_jak2(DmaFollower&, SharedRenderState*, ScopedProfilerNode&) {}
}
@@ -2,6 +2,7 @@
#include "game/graphics/opengl_renderer/BucketRenderer.h"
#include "game/graphics/opengl_renderer/DirectRenderer.h"
#include "game/graphics/opengl_renderer/ocean/OceanEnvmap.h"
#include "game/graphics/opengl_renderer/ocean/OceanMid.h"
#include "game/graphics/opengl_renderer/ocean/OceanTexture.h"
#include "game/graphics/opengl_renderer/opengl_utils.h"
@@ -29,14 +30,9 @@ class OceanMidAndFar : public BucketRenderer {
void handle_ocean_mid(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& prof);
void handle_ocean_89_jak2(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& prof);
void handle_ocean_79_jak2(DmaFollower& dma,
SharedRenderState* render_state,
ScopedProfilerNode& prof);
DirectRenderer m_direct;
OceanEnvmap m_envmap_renderer;
OceanTexture m_texture_renderer;
OceanMid m_mid_renderer;
};
@@ -584,13 +584,13 @@ void OceanTexture::run_L3_PC_jak2() {
// iaddi vi01, vi01, -0x1 | ftoi0.xyzw vf19, vf19 78
vtx3.ftoi0(Mask::xyzw, vtx3); loop_idx = loop_idx + -1;
// sq.xyzw vf16, 1(vi06) | add.xyzw vf28, vf28, vf07 79
cout0.add(Mask::xyzw, cout0, m_texture_constants.cam_nrm); sq_buffer(Mask::xyzw, vtx0, vu.dbuf_write + 1);
cout0.add(Mask::xyzw, cout0, m_texture_constants.constants); sq_buffer(Mask::xyzw, vtx0, vu.dbuf_write + 1);
// sq.xyzw vf17, 4(vi06) | add.xyzw vf29, vf29, vf07 80
cout1.add(Mask::xyzw, cout1, m_texture_constants.cam_nrm); sq_buffer(Mask::xyzw, vtx1, vu.dbuf_write + 4);
cout1.add(Mask::xyzw, cout1, m_texture_constants.constants); sq_buffer(Mask::xyzw, vtx1, vu.dbuf_write + 4);
// sq.xyzw vf18, 7(vi06) | add.xyzw vf30, vf30, vf07 81
cout2.add(Mask::xyzw, cout2, m_texture_constants.cam_nrm); sq_buffer(Mask::xyzw, vtx2, vu.dbuf_write + 7);
cout2.add(Mask::xyzw, cout2, m_texture_constants.constants); sq_buffer(Mask::xyzw, vtx2, vu.dbuf_write + 7);
// sq.xyzw vf19, 10(vi06) | add.xyzw vf31, vf31, vf07 82
cout3.add(Mask::xyzw, cout3, m_texture_constants.cam_nrm); sq_buffer(Mask::xyzw, vtx3, vu.dbuf_write + 10);
cout3.add(Mask::xyzw, cout3, m_texture_constants.constants); sq_buffer(Mask::xyzw, vtx3, vu.dbuf_write + 10);
// lq.xyzw vf24, 1(vi05) | sub.zw vf28, vf01, vf00 83
cout0.sub(Mask::zw, ones, vf00); lq_buffer(Mask::xyzw, nrm0, vu.in_ptr + 1);
// lq.xyzw vf26, 5(vi05) | sub.zw vf29, vf01, vf00 84
@@ -0,0 +1,13 @@
#version 410 core
uniform sampler2D tex_T1;
in vec2 tex_coord;
out vec4 color;
void main() {
const float PI = 3.14159265358979;
float u = tex_coord.x;
float v = tex_coord.y;
float theta = (0.5 - u) * 2.0 * PI;
float t = 1.0 - abs(2.0 * v - 1.0);
vec2 st = vec2(0.5) + t * 0.5 * vec2(sin(theta), cos(theta));
color = texture(tex_T1, st);
}
@@ -0,0 +1,7 @@
#version 410 core
layout (location = 0) in vec2 position_in;
out vec2 tex_coord;
void main() {
gl_Position = vec4(position_in, 0.0, 1.0);
tex_coord = (position_in + 1.0) * 0.5;
}
@@ -0,0 +1,8 @@
#version 410 core
in vec4 color;
out vec4 out_color;
void main() {
out_color = color;
}
@@ -0,0 +1,11 @@
#version 410 core
layout (location = 0) in vec2 ndc_in;
layout (location = 1) in vec4 color_in;
out vec4 color;
void main() {
gl_Position = vec4(ndc_in, 0.0, 1.0);
color = color_in;
}
+3 -5
View File
@@ -1167,11 +1167,9 @@
(let* ((s4-0 (-> *display* frames (-> *display* on-screen) global-buf))
(s5-2 (-> s4-0 base))
)
;; TODO handle ocean::79 and ocean::89
;; diasble tod ocean stuff
;; (if (-> *time-of-day-context* sky)
;; (ocean-method-89 this s4-0)
;; )
(if (-> *time-of-day-context* sky)
(ocean-method-89 this s4-0)
)
(draw-ocean-texture this s4-0 (the-as int (-> this verts)))
;; disable whatever this is
;; (ocean-method-79 this s4-0)
+3 -4
View File
@@ -1191,10 +1191,9 @@
(let* ((s4-1 (-> *display* frames (-> *display* on-screen) global-buf))
(s5-2 (-> s4-1 base))
)
;; og:preserve-this not-yet-implemented
; (if (-> *time-of-day-context* sky)
; (ocean-method-88 this s4-1)
; )
(if (-> *time-of-day-context* sky)
(ocean-method-88 this s4-1)
)
(draw-ocean-texture this s4-1 (the-as int (-> this verts)))
; (ocean-method-89 this s4-1)
(init-buffer! this s4-1)