[HN Gopher] A Shader Trick
___________________________________________________________________
A Shader Trick
Author : tobr
Score : 157 points
Date : 2022-02-23 07:15 UTC (15 hours ago)
(HTM) web link (the-witness.net)
(TXT) w3m dump (the-witness.net)
| jstanley wrote:
| I've been playing The Witness recently. I've enjoyed it a lot,
| and had a lot of "aha moments", but I wouldn't say I altogether
| _understand_ the game. It has a very elusive nature. I 'm working
| through (what I believe to be) the "ending" at the moment, but
| there is still a lot of the pre-ending stuff that I haven't
| figured out yet.
|
| At first glance The Witness is a puzzle game, made annoying by
| the fact that you have to walk everywhere instead of clicking
| "Next Puzzle". But then you occasionally see things in the
| environment that are just really pleasing. An example that
| springs to mind is a bunch of broken metal in a window[0], and
| some branches on a tree, that line up with the sun (which doesn't
| move) to cast a shadow of a woman sitting underneath a tree[1].
|
| And there's loads of this stuff. And noticing it doesn't
| contribute anything at all to the apparent objective of the game
| (except where it does!), but it adds so much that you just
| wouldn't get if it were the simple puzzle game that it initially
| appears to be.
|
| I really like this game.
|
| [0] https://img.incoherency.co.uk/3706
|
| [1] https://img.incoherency.co.uk/3705
| nurbl wrote:
| I think it's the game I've played that comes closest to feeling
| like a genuine mystery. Even though I think I've finished it,
| there are many aspects I still don't "get". It may just be that
| it's good at simulating this experience (sort of like a Lynch
| movie) but I don't think it matters. It's a great puzzle game
| wrapped in an enigma. And a piece of art too.
| webkike wrote:
| I think that instead of this, my solution would be to have each
| function that depends on dt to accumulate the dt themselves, and
| reset it at their individual periods.
|
| A Rust trait with an associated const would help with this:
| trait TimeDependentFn { const PERIOD: f32;
| type Output; fn call(&self, dt: f32) ->
| Self::Output; }
| nmilo wrote:
| But then you would need a local dt for each time-dependent
| function, and shaders don't really have packaged-up member
| variables in structs like you think they would if you come from
| Rust. That means you would have to maintain a massive list of
| dt's for each time-dependent function your shader calls, and
| then change this list everytime you introduce or remove a time-
| dependent function to the shader code. It's a solution that
| only works in a language that can hide the complexity from the
| programmer like Rust.
| ruined wrote:
| this is about shaders, which have no persistent memory even
| pixel-to-pixel on the same frame. there is no way to accumulate
| over time. and calculating outside of the shader would miss the
| point.
|
| so you're stuck doing it from scratch every pixel. that's fine,
| shaders are fast.
|
| at most it might make sense to calculate a truncated time
| globally per frame and provide that as a uniform.
| Scene_Cast2 wrote:
| Could you use a non-float integer instead? Do GPUs have cheap int
| % float -> float operations?
| hwillis wrote:
| 1. Integers would make the situation easier, but still overflow
| after ~50 days. You can't just increment delta every frame;
| frame times vary significantly and and if you aren't precise to
| the millisecond things will jump around.
|
| 2. It's not exactly cheap, and I don't think compilers put much
| effort into making sure you actually retain that precision. I
| have only really used floats in shaders though, and don't know
| what would happen.
|
| 3. I'm pretty sure that in practice you'd lose out on a lot of
| hardware-accelerated functions, doing trig and interpolations
| with multiple messy conversions. It's also possible you'd fuck
| up some compiler optimizations.
| jblow wrote:
| > if you aren't precise to the millisecond things will jump
| around
|
| It is correct that precision is very important here, but, a
| millisecond is way too coarse: at 120fps, a millisecond is
| 1/8 of the frame time, and you'd get horrible jitter.
| hwillis wrote:
| Ehh. The framerate doesn't actually matter that much,
| because in the end the actual pixel changes color at the
| same rate regardless of FPS. No shader is periodic at 120
| Hz; they're very rarely periodic at even 10 Hz.
|
| 10 Hz is already a slow strobe light; 1 ms deltas means
| that each flash is within ~1% of the correct color.
|
| If you're doing something like raymarching in the pixel
| shader, then you might want sub-millisecond resolution. In
| 99% of normal shaders, I don't think so. That kind of
| precision comes into play more with moving objects, where a
| tiny time delta can mean the difference between a pixel
| being completely lit or completely dark. Even then though,
| bad time resolution is just as likely to manifest as motion
| blur or something.
| aqme28 wrote:
| I'm confused why you need a global total game-time anyways, or
| if you have one why it has to be precise. Could you use your
| precise time in float, and then increment a larger but less-
| precise total gametime value every 10 minutes?
| zamadatix wrote:
| It doesn't have to be game time, as the article says it could
| be time since level or some other trigger. It's basically
| about any time based counter you need for a time based
| effect, such as a predefined cyclical wind effect on trees or
| waves in water. The article also explains why it has to be
| precise, looping effects will behave oddly if the loop is not
| occurring precisely. Splitting the number into a large part
| and a precise part doesn't actually solve for anything, it
| just moves the problem into "how do I make arbitrary effects
| precisely loop based on the 2 parts of the time" instead of
| "how do I make arbitrary effects precisely loop based on the
| time".
| dataangel wrote:
| If it moves the problem far enough forward in practice it
| won't matter though. If somebody runs The Witness for 100
| days that's on them
| zamadatix wrote:
| It doesn't move it to 100 days, it'd move the problem to
| 10 minutes. Shaders can't just take a large number as a
| large piece and small piece unmodified, solving for this
| in the logic is the same as solving for the original
| cycle matching problem.
| Jasper_ wrote:
| int % float -> float is not a native operation I'm aware of on
| any CPU or GPU. Even C doesn't have this operation -- the %
| operator only applies to integers, and the float variant is a
| function, fmodf(float, float); shading languages are similar.
| Also note that GPUs don't have any sort of integer division
| instruction.
| ruined wrote:
| i have been doing this for a while playing around with shadertoy.
| i don't think i learned it from anywhere, i just wanted some
| things to match up and repeat over hours and it was obvious that
| sin/cos are periodic, and cycling the time value would improve
| the imprecision at the seam.
|
| seeing it delivered as a clever trick in a blog post makes me
| wonder if i'm more competent than i thought, or if everyone else
| is generally less competent than i thought.
| sgtnoodle wrote:
| It's a little bit of both, probably?
| EveYoung wrote:
| You can use a similar approach for adding time features to a ML
| model. In this context, I think it is a quite common technique.
| gfd wrote:
| I think I've read that minecraft behaves differently depending on
| how far away you are standing from the origin for similar
| reasons:
|
| [1]https://minecraft.fandom.com/wiki/Bedrock_Edition_distance_e..
| .
|
| [2]https://minecraft.fandom.com/wiki/Java_Edition_distance_effe..
| .
| rezmason wrote:
| I've encountered that, too, in my Excel 97 easter egg
| reproduction. My terrain loops in the shader, but camera
| position is governed by the JavaScript, so it can get weird far
| from origin if I don't sanitize.
|
| Here's how the terrain normally looks, zoomed out with the grid
| on:
|
| https://rezmason.github.io/excel_97_egg/?o=bq
|
| And here's how it looks 8589990 units from the origin, with
| sanitizing off:
|
| https://rezmason.github.io/excel_97_egg/?o=bq&sanitizePositi...
|
| There's two weird phenomena happening there: the lack of camera
| position precision causes the terrain to shift left and right,
| but a little bit further, you can see that all the terrain
| quads collapse in one dimension for some reason.
|
| Someone also made a game out of this, called Floating Point
| Leviathan:
|
| https://yanknoopdev.itch.io/fpl
| bicarbonato wrote:
| I believe that every game have this problem if you are far
| enough away from the origin. 3kliksphilip did a great video on
| this subject: https://www.youtube.com/watch?v=eK7eNgiQfhk
| sgtnoodle wrote:
| I first encountered this in Active Worlds around 2000 or so, as
| a teenager. I managed to get the head developer to check it
| out, and he seemed surprised by it.
| bemmu wrote:
| In Roblox this happens if you get thrown too far:
| https://www.youtube.com/watch?v=Fw1GZLodmtw
| SquareWheel wrote:
| Yes, the Minecraft Farlands are caused by precision issues of
| floating point numbers. The game becomes jittery as it tries
| snapping to fewer and fewer precise digits. This is most
| obvious at first in the selection box around blocks, but the
| terrain also becomes unstable after a certain point.
|
| Note that one Youtuber KurtJMac has been walking to the
| Farlands since 2011. He's been raising money for charity as
| part of those videos/streams. He's roughly 40% of the way
| there.
|
| https://www.farlandsorbust.com/
|
| https://www.youtube.com/user/kurtjmac
| skykooler wrote:
| Huh, I remember seeing some of his videos about five years
| ago and wondering if he'd ever make it. Glad to see he's
| still going!
| hedgehog wrote:
| The Patriot range gate bug is a relatively famous issue with a
| similar cause related to numeric precision and timers:
| https://cs.nyu.edu/exact/resource/mirror/patriot.htm
| ChrisLomont wrote:
| I've used a similar trick for decades: for example I put it in
| the code for my Hypnocube (www.hypnocube.com) product in 2005
| (Hypnocube). I used it in games and digital art projects before
| (and after) that.
|
| For embedded or low resource computing, sin/cos may be expensive,
| so I use a table based fixed point version. I pick the table to
| have size power of 2, making lots of things easier. Then to make
| time wrap, I use a large power of 2, which is exactly the same as
| this trick, with base 10 replaced with base 2 (and using fixed
| point math).
|
| You also hit problems where delta times can go negative, so those
| also need to be max time aware. In short, I always make a timing
| module, it tracks time (and stretches it as needed), and doles
| out a few things used everywhere: a delta frame time, a large
| time (say 64 bits as ns for 584 year wraparound), and a capped
| time (say 16 or 24 bits) to use in places where you know the wrap
| amount and still give space for computations not to overflow.
|
| As far as I know the Hypnocube never repeats nor does anything
| flicker at any time due to bad wraparounds. But that took work to
| ensure.
| Const-me wrote:
| When I need something like that, I usually passing [ 0 .. 1 ]
| float phase to the shaders, and when updating the number on CPU I
| wrap the value into that range after the increment.
| hwers wrote:
| Can't believe it's almost as long since this game was released as
| the time we waited for it.
| GrantS wrote:
| Aha! This explains a 20-year-old mystery bug! In undergrad, one
| of my side projects was a procedural 3D world from scratch (kind
| of Myst meets Halo), including physics, graphics (OpenGL), and
| the raw synthesized sound of ocean waves and plasma grenade
| explosions from sine waves via granular synthesis. After about 10
| minutes exploring the world (or running around throwing tiny
| spheres down hills and scattering them with plasma grenades) my
| relaxing synthesized ocean wave sounds started to sound garbled
| and distorted so I always had to restart the program -- now I
| know why :)
| Waterluvian wrote:
| The blog suggests that you need to use integers to describe time
| because floats have problems. Granted.
|
| Why limit one's self to absolutely having to describe it in a
| single integer value? Why not some wrapper around a handful of
| integer values that can handle a much bigger max?
|
| Would this have a meaningful performance cost?
| sgtnoodle wrote:
| You could simply use a single 64 bit integer in, say,
| nanoseconds. That would give you 584 years of range.
|
| Just convert the integer into a float before passing it into
| the shader. For periodic effects, apply the appropriate modulo.
| Fog doesn't change very rapidly, so if wrapping is a pain, you
| could just accept the loss in precision. You could round the
| value as part of the conversion so that the precision doesn't
| change over the range. With 1 second precision, you should be
| good for a few months with a 32 bit float.
| merlincorey wrote:
| You can't put a 64 bit integer into a 32 or even 64 bit
| floating point number since at least some of those bits
| aren't available even in the 64 bit floating point number.
| hwillis wrote:
| > Just convert the integer into a float before passing it
| into the shader.
|
| That solves no problems at all. The number needs to stay an
| integer until _after_ it is fed to a periodic function, which
| will restore it to a small enough number to be precisely
| represented as a float.
|
| > Fog doesn't change very rapidly, so if wrapping is a pain,
| you could just accept the loss in precision. You could round
| the value as part of the conversion so that the precision
| doesn't change over the range.
|
| That doesn't help either. Unless you round the time uniform
| CPU side- sending a counter that is incremented once per
| second to the fog shader, and different counter incremented
| once per millisecond to the shimmer shader- you're still
| sending a giant number to a function that is periodic over a
| 1,000,000x smaller window. Precision errors will still cause
| wildly varying outputs.
|
| Sending separate uniforms only solves the problem for very,
| very slowly varying functions.
|
| > With 1 second precision, you should be good for a few
| months with a 32 bit float.
|
| Human vision is exceptionally well-tuned for noticing sudden
| changes, even relatively subtle ones. A gradual change over 1
| second can be hundreds of times larger than a sudden change
| before it's noticeable.
| ruined wrote:
| you'd still end up either losing precision or using a truncated
| (cycling) part when you wanted to use it in a float
| calculation. it's effectively the same.
| hwillis wrote:
| It doesn't suggest using integers. It suggests resetting your
| time uniform every 1000, 10000, 100000... etc. In values of ten
| raised to some (integer) power. The sole reason for picking a
| number like that is because programmers will tend to use
| constants like 32.768 instead of some fractional value. If you
| use 32.768 and reset your time uniform at 1000, the functions
| used with 32.768 will still loop seamlessly.
|
| Double precision operations are much slower on GPUs. This can
| get very bad indeed for certain optimizations, like LUTs. 32
| bit integers can accumulate much more time delta than floats
| without precision errors, but have similar problems.
|
| You can pass in an integer and convert it to a float, but that
| doesn't really solve any problems. The accumulated time is
| being used in functions that noticeably change over a dozen
| milliseconds. The total accumulation is simply too large to for
| floats to represent with that precision; you would also need to
| convert most of the math surrounding the time uniform.
|
| It is a much better solution to limit the time uniform. The
| periodic functions depending on it are sensitive to millisecond
| changes and loop every 100-10000 milliseconds; there's no
| reason for time to ever be much larger than 10000.
___________________________________________________________________
(page generated 2022-02-23 23:01 UTC)