Skip to content

Detect underwater camera from the water surface while swimming - #1015

Open
Solessfir wants to merge 5 commits into
Try:masterfrom
Solessfir:underwater-camera-swim
Open

Solessfir wants to merge 5 commits into
Try:masterfrom
Solessfir:underwater-camera-swim

Conversation

@Solessfir

@Solessfir Solessfir commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

While swimming, the camera counted as underwater only when it looked up by more than 8 degrees. The orbit camera can dip below the surface at a shallower pitch, so water got rendered with above-water shading from below, which shows up as vertical streaks over the surface.

The camera now uses waterRay(origin, 0.f).hasCol to classify its current position while swimming or diving. This avoids relying on an animated head or orbit target as an above-water reference and retains the water query's cave-occlusion handling. There is no angle fallback.

Before:
Screenshot (167)

After:
Screenshot (168)

@LordKobra

Copy link
Copy Markdown
Contributor

Just a note: When I worked on an underwater shader, I noticed the same problem with free-camera. But a ray from the player, who can be very distant, may not cause a correct intersection. So I implemented state tracking, where the camera always starts above water on game load. and then it tracks intersections between pos and last pos. This makes it independent from player state too.
Check camera.cpp changes here, water height can be ignored for this purpose:
master...LordKobra:OpenGothic:water-shader

@Try

Try commented Oct 17, 2026

Copy link
Copy Markdown
Owner

@LordKobra

and then it tracks intersections between pos and last pos

This is pretty much what current implementation already does in case of free-camera. Yet still, complete solution that works is practically impossible and we have to relay on various heuristics.

Comment thread common/camera.cpp Outdated
if(pl!=nullptr && pl->isSwim()) {
inWater = (angles.x < -8.f);
// The swimmer stays at the surface while the orbit camera can dip below it at any pitch.
inWater = physic.waterRay(origin, 0.f).hasCol;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be OK as complementary heuristics, alongside angle one.
Why you not using physic.cameraRay ? It also reports hit-count water mesh, what make sit more general.

@Solessfir Solessfir Oct 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried cameraRay(origin, pl->cameraBone(true)), but that version regressed while moving: the swimming animation dips the head below the surface while the camera stays above, so it incorrectly enables underwater shading and produces black bands. Standing still hides the problem. The orbit target can be submerged too, so neither is a reliable above-water reference.

Reverted to the direct waterRay(origin, 0.f).hasCol query and included diving, which previously used persistent crossing tracking. The camera-position query handled the reproduction correctly, including when both camera and head were underwater. It also keeps the cave-occlusion check. The angle fallback is removed.

Regression:
Screenshot 2026-10-17 212724

@Solessfir

Solessfir commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Water has another issue. I have a local fix, but it even touches water shader. Let me know if you want to this 3e534ef as a PR.

Broken:
Screenshot (191)

Fixed:
Screenshot (192)

@LordKobra

Copy link
Copy Markdown
Contributor

Is the water surface gone in the second screenshot or is it just a different angle?

In case it's gone, correct way should look like this:
cameraState ( inWater = true/false)
Then calculate per pixel: distance intersection pixel to water surface from camera: d_ws
distance pixel to near plane: d_np
if(d_ws < d_np): pixel_state = !cameraState
else pixel_state = cameraState

@Solessfir

Copy link
Copy Markdown
Contributor Author

Just slightly different angle. Water surface is still there. I was fixing blue water tint.

@Try

Try commented Oct 19, 2026

Copy link
Copy Markdown
Owner

I've just pushes version with camera ray, as it was intended.
Also no diving, because ray-query wont make any sense there.

Two common test-cases, that typically cause issues:

  1. Watefall near fire-dragon (non-planar water):
image
  1. Jarkendar underwater cave (no water top layer, when diving from one side to other):
image

There is also swap in the valley of mines, but it's not directly related.

@Solessfir
Solessfir force-pushed the underwater-camera-swim branch from 443180a to 4a061d1 Compare October 20, 2026 23:53
@Solessfir

Copy link
Copy Markdown
Contributor Author

Tested your version on both cases:

  • Waterfall near the fire dragon: swim detection matches the local surface everywhere I checked (24 camera positions around the swimmer, the surface varies by ~1.7 m there). The odd band you can get there is the spill-over sheet seen from above the lower basin, so detection is right.
  • Jharkendar cave: diving through works. It stays underwater through the passage and switches off when surfacing on the other side.

Found one more case: swimming close to the overhang at the cave entrance with the camera dipping under the rock. The ray stops at the rock, finds no water, and the camera renders as above water (streaky surface seen from below).

Pushed a fallback for that: if the ray finds no water, the camera still counts as underwater when it's below the swimmer's surface and the line from the hero to the camera crosses no rock and no water surface. 9/9 placements at that spot are correct now, waterfall results unchanged, diving untouched.

@Try

Try commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Found one more case: swimming close to the overhang at the cave entrance with the camera dipping under the rock

Can you please share screenshoot? I think I see what case is it, just to be extra sure.

@Solessfir

Solessfir commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Before (without the fallback): the ray stops at the rock above the camera, so it renders as above water and you see the surface from below with the streaks.
before

After: camera counts as underwater.
after

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants