diff --git a/apple/oskiewar/Sources/InkOutlines.swift b/apple/oskiewar/Sources/InkOutlines.swift index f51b0610d2..3c83b94988 100644 --- a/apple/oskiewar/Sources/InkOutlines.swift +++ b/apple/oskiewar/Sources/InkOutlines.swift @@ -48,16 +48,66 @@ final class InkOutlines { float2 p = float2(float((id << 1) & 2), float(id & 2)); InkRaster out; out.position = float4(p * 2.0 - 1.0, 0.0, 1.0); return out; } + struct SkyView { float4 camPos, right, up, forward, proj, depthMap, viewport, target, stage; }; + static float hash(float2 p) { return fract(sin(dot(p, float2(127.1, 311.7))) * 43758.5453); } + static float noise(float2 p) { + float2 i = floor(p), f = fract(p), u = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + float2(1, 0)), u.x), mix(hash(i + float2(0, 1)), hash(i + float2(1, 1)), u.x), u.y); + } + static float fbm(float2 p) { + float v = 0.0, a = 0.5; + for (int i = 0; i < 5; ++i) { v += a * noise(p); p = p * 2.03 + 17.1; a *= 0.5; } + return v; + } + // The sky, in the game's y-down world: elevation is -rd.y. Cel-banded + // clouds with an ink rim keep it in the same drawn language as the park. + static float3 skyColor(float3 rd, float time) { + float e = -rd.y; + float3 zenith = float3(0.30, 0.55, 0.90), horizon = float3(0.80, 0.88, 0.95); + float3 haze = float3(0.93, 0.86, 0.78), ground = float3(0.62, 0.66, 0.70); + float3 color = e >= 0.0 ? mix(horizon, zenith, pow(saturate(e), 0.5)) : mix(horizon, ground, saturate(-e * 5.0)); + color = mix(color, haze, exp(-abs(e) * 18.0) * 0.55); + float3 sun = normalize(float3(0.35, -0.8, -0.45)); + float s = max(dot(rd, sun), 0.0); + color += float3(1.0, 0.86, 0.6) * (pow(s, 12.0) * 0.18 + pow(s, 180.0) * 0.5); + color = mix(color, float3(1.0, 0.97, 0.88), smoothstep(0.9993, 0.9996, s)); + if (e > 0.015) { + float2 uv = rd.xz / e * 0.35 + float2(time * 0.012, time * 0.004); + float n = fbm(uv); + float body = smoothstep(0.52, 0.56, n), shade = smoothstep(0.64, 0.68, n); + float fade = smoothstep(0.015, 0.2, e); + float3 cloud = mix(float3(0.80, 0.84, 0.92), float3(1.0, 0.99, 0.96), shade); + float rim = (smoothstep(0.50, 0.52, n) - smoothstep(0.52, 0.54, n)) * fade; + color = mix(color, cloud, body * fade); + color = mix(color, float3(20.0, 17.0, 28.0) / 255.0, rim * 0.55); + } + return color; + } fragment float4 ink_fragment(InkRaster in [[stage_in]], texture2d scene [[texture(0)]], depth2d depth [[texture(1)]], - constant float& scale [[buffer(0)]]) { + constant float& scale [[buffer(0)]], + constant SkyView& v [[buffer(1)]]) { uint2 size = uint2(scene.get_width(), scene.get_height()); int2 p = int2(in.position.xy); int w = max(1, int(round(scale))); auto at = [&](int2 q) { return depth.read(uint2(clamp(q, int2(0), int2(size) - 1))); }; float c = at(p); float3 color = scene.read(uint2(p)).rgb; + // Behind everything solid: the cleared background always, and in a + // perspective scene the piece's flat backdrop too. + bool perspective = v.stage.z > 0.5 && v.proj.z > 0.5; + if (c >= 0.999 || (perspective && c >= 0.975)) { + float2 logical = in.position.xy * (v.stage.xy / float2(size)); + float3 rd; + if (perspective) { + float sx = logical.x - v.right.w, sy = v.up.w - logical.y; + rd = normalize(v.right.xyz * (sx / v.proj.y) + v.up.xyz * (sy / v.proj.y) + v.forward.xyz); + } else { + rd = normalize(float3(0.0, -(0.55 - logical.y / max(1.0, v.stage.y)), 1.0)); + } + return float4(skyColor(rd, v.target.w), 1.0); + } // HUD furniture sits in front of everything (depth near 0): leave it be. if (c < 0.02) return float4(color, 1.0); float horizontal = abs(at(p + int2(w, 0)) + at(p - int2(w, 0)) - 2.0 * c); diff --git a/apple/oskiewar/Sources/MetalSceneView.swift b/apple/oskiewar/Sources/MetalSceneView.swift index cb7d603d16..ece4b4cfa7 100644 --- a/apple/oskiewar/Sources/MetalSceneView.swift +++ b/apple/oskiewar/Sources/MetalSceneView.swift @@ -60,6 +60,16 @@ final class MetalSceneView: MTKView, MTKViewDelegate { /// The shared ink line; nil falls back to drawing straight to the screen. private let ink: InkOutlines? var inkOutlines = true + private var skyCamera: [Float]? + private var skyTime: Float = 0 + /// The piece's camera for this frame, so the sky turns with the view. + func sceneCamera(_ camera: UnsafePointer, time: Float) { + sceneLock.lock() + defer { sceneLock.unlock() } + for i in 0..<27 where !camera[i].isFinite { return } + skyCamera = Array(UnsafeBufferPointer(start: camera, count: 27)) + skyTime = time + } private let uploads = (0..<3).map { _ in Uploads() } private let inFlight = DispatchSemaphore(value: 3) private var uploadIndex = 0 @@ -429,6 +439,15 @@ final class MetalSceneView: MTKView, MTKViewDelegate { encoder.setFragmentTexture(color, index: 0) encoder.setFragmentTexture(depth, index: 1) encoder.setFragmentBytes(&scale, length: MemoryLayout.stride, index: 0) + var sky = SdfViewConstants() + if let camera = skyCamera { + sky = SdfFigures.constants(camera: camera, first: 0, drawable: drawableSize, stage: stage) + sky.stage.z = 1 + } else { + sky.stage = SIMD4(Float(stage.width), Float(stage.height), 0, 0) + } + sky.target.w = skyTime + encoder.setFragmentBytes(&sky, length: MemoryLayout.stride, index: 1) encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3) } // Text last and depth-free: the HUD is not in the world, it is over it. diff --git a/xbox/live/oskiewar.js b/xbox/live/oskiewar.js index 80e6bbcc18..792d773cee 100644 --- a/xbox/live/oskiewar.js +++ b/xbox/live/oskiewar.js @@ -2881,6 +2881,10 @@ const nativeRetainedMeshes = nativeMeshUpload !== null && nativeMeshDraw !== nul // tessellated figure draws instead. const nativeSdfFigure = typeof sdfFigure === "function" ? sdfFigure : null; const nativeSdfBubble = typeof sdfBubble === "function" ? sdfBubble : null; +// The host paints the sky behind everything (procedural gradient, sun and +// cel clouds) from the frame's camera; the piece's own backdrop still draws +// on hosts without it. +const nativeSceneCamera = typeof sceneCamera === "function" ? sceneCamera : null; const nativeTriangles3d = typeof triangles3d === "function" ? triangles3d : null; const nativePostEffects = typeof postEffects === "function" ? postEffects : null; const hostComicWrite = typeof comicWrite === "function" ? comicWrite : null; @@ -19366,7 +19370,9 @@ function drawLooseRunner(player,world,t,lod){ triangleDepth=h.depth-world.head.radius*cameraDoll.depthSlope*(sdfBody?1.08:.5); if(!sdfBody){filledDisc(h.x,h.y,h.radius+(lod?1.2:2.2),ink);filledDisc(h.x,h.y,h.radius,front<-.2?hair:skin);} if(front>=-.2){ - drawHairline(player,h); + // On an SDF head the flat fringe only reads face-on; from the side the + // shaded hair cap carries the hair. + if(!sdfBody||front>.45)drawHairline(player,h); const faceYaw=Math.atan2(forward.x*view.right.x+forward.z*view.right.z,front); drawTrioFace(player,h,runtime().monotonicUs,faceYaw); } @@ -23370,6 +23376,9 @@ function gamePaint() { wipe(...outside); // A plain sky behind every theme. The underpass photograph was busy behind // the fighters and painted its own floor wherever a course had none. + if (nativeSceneCamera) { + try { nativeSceneCamera(mainNativeCamera(), runtime().monotonicUs / 1e6); } catch {} + } if (renderFlags.sky !== false) drawSkyAtmosphere(sky, arena); if (PAL_SELECT && selecting) { box(0, 0, viewWidth(), viewHeight, ...menuArena); diff --git a/xbox/macos-native/main.swift b/xbox/macos-native/main.swift index 5e39949593..f00c7c4900 100644 --- a/xbox/macos-native/main.swift +++ b/xbox/macos-native/main.swift @@ -1097,6 +1097,11 @@ private final class NativeGameHost { radius: v[3], tint: SIMD3(v[4], v[5], v[6])) } javascript.setObject(sdfBubble, forKeyedSubscript: "sdfBubble" as NSString) + let sceneCamera: @convention(block) (JSValue, Double) -> Void = { [weak self] camera, time in + guard let self, let view = Self.float32(camera, minimum: 27) else { return } + self.view.scene.sceneCamera(view, time: Float(time)) + } + javascript.setObject(sceneCamera, forKeyedSubscript: "sceneCamera" as NSString) javascript.setObject(clearDecals, forKeyedSubscript: "decalClear" as NSString) javascript.setObject(stampDecal, forKeyedSubscript: "decalStamp" as NSString) javascript.setObject(stampDecal, forKeyedSubscript: "decalTint" as NSString)