Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add first person view model example #13828

Merged
merged 22 commits into from
Jun 17, 2024

Conversation

janhohenheim
Copy link
Member

@janhohenheim janhohenheim commented Jun 13, 2024

Objective

A very common way to organize a first-person view is to split it into two kinds of models:

  • The view model is the model that represents the player's body.
  • The world model is everything else.

The reason for this distinction is that these two models should be rendered with different FOVs.
The view model is typically designed and animated with a very specific FOV in mind, so it is
generally fixed and cannot be changed by a player. The world model, on the other hand, should
be able to change its FOV to accommodate the player's preferences for the following reasons:

  • Accessibility: How prone is the player to motion sickness? A wider FOV can help.
  • Tactical preference: Does the player want to see more of the battlefield?
    Or have a more zoomed-in view for precision aiming?
  • Physical considerations: How well does the in-game FOV match the player's real-world FOV?
    Are they sitting in front of a monitor or playing on a TV in the living room? How big is the screen?

Solution

I've added an example implementing the described setup as follows.

The Player is an entity holding two cameras, one for each model. The view model camera has a fixed
FOV of 70 degrees, while the world model camera has a variable FOV that can be changed by the player.

I use different RenderLayers to select what to render.

  • The world model camera has no explicit RenderLayers component, so it uses the layer 0.
    All static objects in the scene are also on layer 0 for the same reason.
  • The view model camera has a RenderLayers component with layer 1, so it only renders objects
    explicitly assigned to layer 1. The arm of the player is one such object.
    The order of the view model camera is additionally bumped to 1 to ensure it renders on top of the world model.
  • The light source in the scene must illuminate both the view model and the world model, so it is
    assigned to both layers 0 and 1.

To better see the effect, the player can move the camera by dragging their mouse and change the world model's FOV with the arrow keys. The arrow up key maps to "decrease FOV" and the arrow down key maps to "increase FOV". This sounds backwards on paper, but is more intuitive when actually changing the FOV in-game since a decrease in FOV looks like a zoom-in.
I intentionally do not allow changing the view model's FOV even though it would be illustrative because that would be an anti-pattern and bloat the code a bit.

The example is called first_person_view_model and not just first_person because I want to highlight that this is not a simple flycam, but actually renders the player.

Testing

Default FOV:
image

Decreased FOV:
image

Increased FOV:
image

Note that the white bar on the right represents the player's arm, which is more obvious in-game because you can move the camera around.
The box on top is there to make sure that the view model is receiving shadows.

I tested only on macOS.


Changelog

I don't think new examples go in here, do they?

Caveat

The solution used here was implemented with help by @robtfm on Discord:

shadow maps are specific to lights, not to layers
if you want shadows from some meshes that are not visible, you could have light on layer 1+2, meshes on layer 2, camera on layer 1 (for example)
but this might change in future, it's not exactly an intended feature

In other words, the example code as-is is not guaranteed to work in the future. I want to bring this up because the use-case presented here is extremely common in first-person games and important for accessibility.
It would be good to have a blessed and easy way of how to achieve it.

I'm also not happy about how I get the perspective variable in change_fov. Very open to suggestions :)

Related issues

@janhohenheim janhohenheim changed the title Add first person view model example with different FOVs Add first person view model example Jun 13, 2024
@janhohenheim janhohenheim added A-Rendering Drawing game state to the screen X-Contentious There are nontrivial implications that should be thought through D-Straightforward Simple bug fixes and API improvements, docs, test and examples C-Examples An addition or correction to our examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward A-Accessibility A problem that prevents users with disabilities from using Bevy labels Jun 13, 2024
@robtfm
Copy link
Contributor

robtfm commented Jun 13, 2024

i think this will cause weird shadow behaviour where the shadow cast by the arm onto the world won't have the same size as the arm itself unless the FOVs match?

i would imagine the more normal approach for this would be to disable arm/"view model" shadows entirely, which would avoid relying on the light/layer behaviour. alternatively you could scale the view model somehow to make it an appropriate size for the FOV and then put it in the base layer directly.

@janhohenheim
Copy link
Member Author

janhohenheim commented Jun 13, 2024

@robtfm maybe I'm misunderstanding you, but your suggestion is already the case. The view model does not cast shadows. In a real game, you would additionally have an invisible model representing the whole player body that casts shadows (not included in this example).

My comment about the lights referred to me wanting to ensure that the view model is receiving shadows.

I also have an alternative implementation that uses a vertex shader to change the view model's FOV. There are multiple issues with this other approach:

  • It is prone to culling issues
  • It requires copy-pasting Bevy shader boilerplate
  • It looks like magic to the uninitiated, which I'd like to avoid for a common use-case

From what I researched, the dual-camera approach used in this example is what is preferred in other game engines as well. If anyone reading this has experience implementing first person games in other engines, feel free to correct me :)

Copy link
Contributor

@killercup killercup left a comment

Choose a reason for hiding this comment

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

Looks good! Left some small suggestions

examples/camera/first_person_view_model.rs Outdated Show resolved Hide resolved
examples/camera/first_person_view_model.rs Outdated Show resolved Hide resolved
examples/camera/first_person_view_model.rs Outdated Show resolved Hide resolved
examples/camera/first_person_view_model.rs Outdated Show resolved Hide resolved
examples/camera/first_person_view_model.rs Outdated Show resolved Hide resolved
examples/camera/first_person_view_model.rs Outdated Show resolved Hide resolved
@janhohenheim
Copy link
Member Author

Thanks @killercup. I've incorporated all of your suggestions :)

Copy link
Contributor

@killercup killercup left a comment

Choose a reason for hiding this comment

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

LGTM with CI fixed

examples/camera/first_person_view_model.rs Outdated Show resolved Hide resolved
@alice-i-cecile alice-i-cecile added this to the 0.14 milestone Jun 13, 2024
Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Awesome! Very pleased to see more pragmatic, task-focused examples.

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jun 13, 2024
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Jun 13, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Jun 13, 2024
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Jun 17, 2024
Merged via the queue into bevyengine:main with commit 48f7078 Jun 17, 2024
32 checks passed
@janhohenheim janhohenheim deleted the first_person_view_model branch June 17, 2024 15:19
mockersf pushed a commit that referenced this pull request Jun 19, 2024
# Objective

A very common way to organize a first-person view is to split it into
two kinds of models:

 - The *view model* is the model that represents the player's body.
 - The *world model* is everything else.

The reason for this distinction is that these two models should be
rendered with different FOVs.
The view model is typically designed and animated with a very specific
FOV in mind, so it is
generally *fixed* and cannot be changed by a player. The world model, on
the other hand, should
be able to change its FOV to accommodate the player's preferences for
the following reasons:
- *Accessibility*: How prone is the player to motion sickness? A wider
FOV can help.
- *Tactical preference*: Does the player want to see more of the
battlefield?
 Or have a more zoomed-in view for precision aiming?
- *Physical considerations*: How well does the in-game FOV match the
player's real-world FOV?
Are they sitting in front of a monitor or playing on a TV in the living
room? How big is the screen?

## Solution

I've added an example implementing the described setup as follows.

The `Player` is an entity holding two cameras, one for each model. The
view model camera has a fixed
FOV of 70 degrees, while the world model camera has a variable FOV that
can be changed by the player.

 I use different `RenderLayers` to select what to render.

- The world model camera has no explicit `RenderLayers` component, so it
uses the layer 0.
All static objects in the scene are also on layer 0 for the same reason.
- The view model camera has a `RenderLayers` component with layer 1, so
it only renders objects
explicitly assigned to layer 1. The arm of the player is one such
object.
The order of the view model camera is additionally bumped to 1 to ensure
it renders on top of the world model.
- The light source in the scene must illuminate both the view model and
the world model, so it is
 assigned to both layers 0 and 1.

To better see the effect, the player can move the camera by dragging
their mouse and change the world model's FOV with the arrow keys. The
arrow up key maps to "decrease FOV" and the arrow down key maps to
"increase FOV". This sounds backwards on paper, but is more intuitive
when actually changing the FOV in-game since a decrease in FOV looks
like a zoom-in.
I intentionally do not allow changing the view model's FOV even though
it would be illustrative because that would be an anti-pattern and bloat
the code a bit.

The example is called `first_person_view_model` and not just
`first_person` because I want to highlight that this is not a simple
flycam, but actually renders the player.

## Testing

Default FOV:
<img width="1392" alt="image"
src="https://github.com/bevyengine/bevy/assets/9047632/8c2e804f-fac2-48c7-8a22-d85af999dfb2">

Decreased FOV:
<img width="1392" alt="image"
src="https://github.com/bevyengine/bevy/assets/9047632/1733b3e5-f583-4214-a454-3554e3cbd066">

Increased FOV:
<img width="1392" alt="image"
src="https://github.com/bevyengine/bevy/assets/9047632/0b0640e6-5743-46f6-a79a-7181ba9678e8">

Note that the white bar on the right represents the player's arm, which
is more obvious in-game because you can move the camera around.
The box on top is there to make sure that the view model is receiving
shadows.

I tested only on macOS.

---

## Changelog

I don't think new examples go in here, do they?

## Caveat

The solution used here was implemented with help by @robtfm on
[Discord](https://discord.com/channels/691052431525675048/866787577687310356/1241019224491561000):
> shadow maps are specific to lights, not to layers
> if you want shadows from some meshes that are not visible, you could
have light on layer 1+2, meshes on layer 2, camera on layer 1 (for
example)
> but this might change in future, it's not exactly an intended feature

In other words, the example code as-is is not guaranteed to work in the
future. I want to bring this up because the use-case presented here is
extremely common in first-person games and important for accessibility.
It would be good to have a blessed and easy way of how to achieve it.

I'm also not happy about how I get the `perspective` variable in
`change_fov`. Very open to suggestions :)

## Related issues

- Addresses parts of #12658
- Addresses parts of #12588

---------

Co-authored-by: Pascal Hertleif <killercup@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Accessibility A problem that prevents users with disabilities from using Bevy A-Rendering Drawing game state to the screen C-Examples An addition or correction to our examples D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Contentious There are nontrivial implications that should be thought through
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants