public Vector4 GetForceAndHeightAt(float x, float z, float spectrumStart, float spectrumEnd, float time)
        {
            vector4 result = new vector4();

            x = -(x + surfaceOffset.x);
            z = -(z + surfaceOffset.y);

            // sample FFT results
            if (spectrumStart == 0.0f)
            {
                for (int scaleIndex = numTiles - 1; scaleIndex >= 0; --scaleIndex)
                {
                    if (tileSpectra[scaleIndex].resolveByFFT)
                    {
                        float     fx, invFx, fy, invFy, t; int index0, index1, index2, index3;
                        Vector2[] da, db; vector4[] fa, fb;

                        lock (tileSpectra[scaleIndex])
                        {
                            InterpolationParams(x, z, scaleIndex, windWaves.TileSizes[scaleIndex], out fx, out invFx, out fy, out invFy, out index0, out index1, out index2, out index3);
                            tileSpectra[scaleIndex].GetResults(time, out da, out db, out fa, out fb, out t);
                        }

                        result += FastMath.Interpolate(
                            fa[index0], fa[index1], fa[index2], fa[index3],
                            fb[index0], fb[index1], fb[index2], fb[index3],
                            fx, invFx, fy, invFy, t
                            );
                    }
                }
            }

            // sample waves directly
            if (filteredCpuWavesCount != 0)
            {
                SampleWavesDirectly(spectrumStart, spectrumEnd, (cpuWaves, startIndex, endIndex) =>
                {
                    Vector4 subResult = new Vector4();

                    for (int i = startIndex; i < endIndex; ++i)
                    {
                        cpuWaves[i].GetForceAndHeightAt(x, z, time, ref subResult);
                    }

#if WATER_SIMD
                    result += new vector4(subResult.x, subResult.y, subResult.z, subResult.w);
#else
                    result += subResult;
#endif
                });
            }

            float horizontalScale = water.HorizontalDisplacementScale * uniformWaterScale;

#if WATER_SIMD
            return(new Vector4(result.X * horizontalScale, result.Y * 0.5f, result.Z * horizontalScale, result.W));
#else
            result.x  = result.x * horizontalScale;
            result.z  = result.z * horizontalScale;
            result.y *= 0.5f * uniformWaterScale;
            result.w *= uniformWaterScale;                          // not 100% sure about this

            return(result);
#endif
        }
        public Vector3 GetDisplacementAt(float x, float z, float spectrumStart, float spectrumEnd, float time)
        {
            Vector3 result = new Vector3();

            x = -(x + surfaceOffset.x);
            z = -(z + surfaceOffset.y);

            // sample FFT results
            if (spectrumStart == 0.0f)
            {
                for (int scaleIndex = numTiles - 1; scaleIndex >= 0; --scaleIndex)
                {
                    if (tileSpectra[scaleIndex].resolveByFFT)
                    {
                        float     fx, invFx, fy, invFy, t; int index0, index1, index2, index3;
                        Vector2[] da, db; vector4[] fa, fb;

                        lock (tileSpectra[scaleIndex])
                        {
                            InterpolationParams(x, z, scaleIndex, windWaves.TileSizes[scaleIndex], out fx, out invFx, out fy, out invFy, out index0, out index1, out index2, out index3);
                            tileSpectra[scaleIndex].GetResults(time, out da, out db, out fa, out fb, out t);
                        }

                        Vector2 subResult = FastMath.Interpolate(
                            ref da[index0], ref da[index1], ref da[index2], ref da[index3],
                            ref db[index0], ref db[index1], ref db[index2], ref db[index3],
                            fx, invFx, fy, invFy, t
                            );

                        result.x -= subResult.x;
                        result.z -= subResult.y;

#if WATER_SIMD
                        result.y += FastMath.Interpolate(
                            fa[index0].W, fa[index1].W, fa[index2].W, fa[index3].W,
                            fb[index0].W, fb[index1].W, fb[index2].W, fb[index3].W,
                            fx, invFx, fy, invFy, t
                            );
#else
                        result.y += FastMath.Interpolate(
                            fa[index0].w, fa[index1].w, fa[index2].w, fa[index3].w,
                            fb[index0].w, fb[index1].w, fb[index2].w, fb[index3].w,
                            fx, invFx, fy, invFy, t
                            );
#endif
                    }
                }
            }

            // sample waves directly
            if (filteredCpuWavesCount != 0)
            {
                SampleWavesDirectly(spectrumStart, spectrumEnd, (cpuWaves, startIndex, endIndex) =>
                {
                    Vector3 subResult = new Vector3();

                    for (int i = startIndex; i < endIndex; ++i)
                    {
                        subResult += cpuWaves[i].GetDisplacementAt(x, z, time);
                    }

                    result += subResult;
                });
            }

            float horizontalScale = -water.HorizontalDisplacementScale * uniformWaterScale;
            result.x  = result.x * horizontalScale;
            result.y *= uniformWaterScale;
            result.z  = result.z * horizontalScale;

            return(result);
        }